mirror of
https://github.com/nomic-ai/gpt4all.git
synced 2024-10-01 01:06:10 -04:00
Add a feature to attach an excel spreadsheet to the chat conversation.
Signed-off-by: Adam Treat <treat.adam@gmail.com>
This commit is contained in:
parent
ea1ade8668
commit
060560cfcc
3
.gitmodules
vendored
3
.gitmodules
vendored
@ -11,3 +11,6 @@
|
||||
[submodule "gpt4all-chat/deps/fmt"]
|
||||
path = gpt4all-chat/deps/fmt
|
||||
url = https://github.com/fmtlib/fmt.git
|
||||
[submodule "gpt4all-chat/deps/QXlsx"]
|
||||
path = gpt4all-chat/deps/QXlsx
|
||||
url = https://github.com/QtExcel/QXlsx.git
|
||||
|
@ -135,6 +135,7 @@ endif()
|
||||
|
||||
set(QAPPLICATION_CLASS QGuiApplication)
|
||||
add_subdirectory(deps/SingleApplication)
|
||||
add_subdirectory(deps/QXlsx/QXlsx)
|
||||
|
||||
if (DEFINED GGML_METALLIB)
|
||||
set_source_files_properties("${GGML_METALLIB}" PROPERTIES GENERATED ON)
|
||||
@ -162,6 +163,7 @@ qt_add_executable(chat
|
||||
src/mysettings.cpp src/mysettings.h
|
||||
src/network.cpp src/network.h
|
||||
src/server.cpp src/server.h
|
||||
src/xlsxtomd.cpp src/xlsxtomd.h
|
||||
${CHAT_EXE_RESOURCES}
|
||||
)
|
||||
|
||||
@ -198,6 +200,8 @@ qt_add_qml_module(chat
|
||||
qml/MyComboBox.qml
|
||||
qml/MyDialog.qml
|
||||
qml/MyDirectoryField.qml
|
||||
qml/MyFileDialog.qml
|
||||
qml/MyFolderDialog.qml
|
||||
qml/MyFancyLink.qml
|
||||
qml/MyMenu.qml
|
||||
qml/MyMenuItem.qml
|
||||
@ -230,9 +234,11 @@ qt_add_qml_module(chat
|
||||
icons/edit.svg
|
||||
icons/eject.svg
|
||||
icons/email.svg
|
||||
icons/file-doc.svg
|
||||
icons/file-md.svg
|
||||
icons/file-pdf.svg
|
||||
icons/file-txt.svg
|
||||
icons/file-xls.svg
|
||||
icons/file.svg
|
||||
icons/github.svg
|
||||
icons/globe.svg
|
||||
@ -250,7 +256,9 @@ qt_add_qml_module(chat
|
||||
icons/network.svg
|
||||
icons/nomic_logo.svg
|
||||
icons/notes.svg
|
||||
icons/paperclip.svg
|
||||
icons/plus.svg
|
||||
icons/plus_circle.svg
|
||||
icons/recycle.svg
|
||||
icons/regenerate.svg
|
||||
icons/search.svg
|
||||
@ -263,6 +271,7 @@ qt_add_qml_module(chat
|
||||
icons/trash.svg
|
||||
icons/twitter.svg
|
||||
icons/up_down.svg
|
||||
icons/webpage.svg
|
||||
icons/you.svg
|
||||
)
|
||||
|
||||
@ -335,7 +344,7 @@ target_include_directories(chat PRIVATE deps/usearch/include
|
||||
target_link_libraries(chat
|
||||
PRIVATE Qt6::Core Qt6::HttpServer Qt6::Pdf Qt6::Quick Qt6::Sql Qt6::Svg)
|
||||
target_link_libraries(chat
|
||||
PRIVATE llmodel SingleApplication fmt::fmt)
|
||||
PRIVATE llmodel SingleApplication QXlsx fmt::fmt)
|
||||
|
||||
|
||||
# -- install --
|
||||
|
1
gpt4all-chat/deps/QXlsx
Submodule
1
gpt4all-chat/deps/QXlsx
Submodule
@ -0,0 +1 @@
|
||||
Subproject commit fda6b806e2ceebd81c01cdded07ae84c94f5879c
|
1
gpt4all-chat/icons/plus_circle.svg
Normal file
1
gpt4all-chat/icons/plus_circle.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="#000000" viewBox="0 0 256 256"><path d="M128,24A104,104,0,1,0,232,128,104.11,104.11,0,0,0,128,24Zm0,192a88,88,0,1,1,88-88A88.1,88.1,0,0,1,128,216Zm48-88a8,8,0,0,1-8,8H136v32a8,8,0,0,1-16,0V136H88a8,8,0,0,1,0-16h32V88a8,8,0,0,1,16,0v32h32A8,8,0,0,1,176,128Z"></path></svg>
|
After Width: | Height: | Size: 340 B |
1
gpt4all-chat/icons/webpage.svg
Normal file
1
gpt4all-chat/icons/webpage.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="#000000" viewBox="0 0 256 256"><path d="M216,40H40A16,16,0,0,0,24,56V200a16,16,0,0,0,16,16H216a16,16,0,0,0,16-16V56A16,16,0,0,0,216,40Zm0,16V88H40V56Zm0,144H40V104H216v96Z"></path></svg>
|
After Width: | Height: | Size: 255 B |
@ -89,15 +89,8 @@ Rectangle {
|
||||
property alias collection: collection.text
|
||||
property alias folder_path: folderEdit.text
|
||||
|
||||
FolderDialog {
|
||||
MyFolderDialog {
|
||||
id: folderDialog
|
||||
title: qsTr("Please choose a directory")
|
||||
}
|
||||
|
||||
function openFolderDialog(currentFolder, onAccepted) {
|
||||
folderDialog.currentFolder = currentFolder;
|
||||
folderDialog.accepted.connect(function() { onAccepted(folderDialog.selectedFolder); });
|
||||
folderDialog.open();
|
||||
}
|
||||
|
||||
Label {
|
||||
@ -170,7 +163,7 @@ Rectangle {
|
||||
id: browseButton
|
||||
text: qsTr("Browse")
|
||||
onClicked: {
|
||||
root.openFolderDialog(StandardPaths.writableLocation(StandardPaths.HomeLocation), function(selectedFolder) {
|
||||
folderDialog.openFolderDialog(StandardPaths.writableLocation(StandardPaths.HomeLocation), function(selectedFolder) {
|
||||
root.folder_path = selectedFolder
|
||||
})
|
||||
}
|
||||
|
@ -394,11 +394,14 @@ MySettingsTab {
|
||||
}
|
||||
}
|
||||
}
|
||||
MyFolderDialog {
|
||||
id: folderDialog
|
||||
}
|
||||
MySettingsButton {
|
||||
text: qsTr("Browse")
|
||||
Accessible.description: qsTr("Choose where to save model files")
|
||||
onClicked: {
|
||||
openFolderDialog("file://" + MySettings.modelPath, function(selectedFolder) {
|
||||
folderDialog.openFolderDialog("file://" + MySettings.modelPath, function(selectedFolder) {
|
||||
MySettings.modelPath = selectedFolder
|
||||
})
|
||||
}
|
||||
|
@ -3,6 +3,7 @@ import QtCore
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Controls.Basic
|
||||
import QtQuick.Dialogs
|
||||
import QtQuick.Layouts
|
||||
|
||||
import chatlistmodel
|
||||
@ -893,6 +894,64 @@ Rectangle {
|
||||
Layout.row: 1
|
||||
Layout.column: 1
|
||||
Layout.fillWidth: true
|
||||
Flow {
|
||||
id: attachedUrlsFlow
|
||||
Layout.fillWidth: true
|
||||
spacing: 10
|
||||
visible: promptAttachments.length !== 0
|
||||
Repeater {
|
||||
model: promptAttachments
|
||||
|
||||
delegate: Rectangle {
|
||||
width: attachmentFileIcon.width + attachmentFileText.width + 20
|
||||
height: 50
|
||||
radius: 5
|
||||
color: theme.attachmentBackground
|
||||
border.color: theme.controlBorder
|
||||
|
||||
Row {
|
||||
spacing: 5
|
||||
anchors.fill: parent
|
||||
anchors.margins: 5
|
||||
|
||||
Item {
|
||||
id: attachmentFileIcon
|
||||
width: 40
|
||||
height: 40
|
||||
Image {
|
||||
id: fileIcon
|
||||
anchors.fill: parent
|
||||
visible: false
|
||||
sourceSize.width: 40
|
||||
sourceSize.height: 40
|
||||
mipmap: true
|
||||
source: {
|
||||
return "qrc:/gpt4all/icons/file-xls.svg"
|
||||
}
|
||||
}
|
||||
ColorOverlay {
|
||||
anchors.fill: fileIcon
|
||||
source: fileIcon
|
||||
color: theme.textColor
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
id: attachmentFileText
|
||||
height: 40
|
||||
text: modelData.file
|
||||
color: theme.textColor
|
||||
horizontalAlignment: Text.AlignHLeft
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
font.pixelSize: theme.fontSizeSmall
|
||||
font.bold: true
|
||||
wrapMode: Text.WrapAnywhere
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TextArea {
|
||||
id: myTextArea
|
||||
Layout.fillWidth: true
|
||||
@ -1434,17 +1493,7 @@ Rectangle {
|
||||
var chat = window.currentChat
|
||||
var followup = modelData
|
||||
chat.stopGenerating()
|
||||
chat.newPromptResponsePair(followup);
|
||||
chat.prompt(followup,
|
||||
MySettings.promptTemplate,
|
||||
MySettings.maxLength,
|
||||
MySettings.topK,
|
||||
MySettings.topP,
|
||||
MySettings.minP,
|
||||
MySettings.temperature,
|
||||
MySettings.promptBatchSize,
|
||||
MySettings.repeatPenalty,
|
||||
MySettings.repeatPenaltyTokens)
|
||||
chat.prompt(followup)
|
||||
}
|
||||
}
|
||||
Item {
|
||||
@ -1825,170 +1874,338 @@ Rectangle {
|
||||
opacity: 0.1
|
||||
}
|
||||
|
||||
ScrollView {
|
||||
ListModel {
|
||||
id: attachmentModel
|
||||
|
||||
function getAttachmentUrls() {
|
||||
var urls = [];
|
||||
for (var i = 0; i < attachmentModel.count; i++) {
|
||||
var item = attachmentModel.get(i);
|
||||
urls.push(item.url);
|
||||
}
|
||||
return urls;
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: textInputView
|
||||
color: theme.controlBackground
|
||||
border.width: 1
|
||||
border.color: theme.controlBorder
|
||||
radius: 10
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.margins: 30
|
||||
anchors.leftMargin: Math.max((parent.width - 1310) / 2, 30)
|
||||
anchors.rightMargin: Math.max((parent.width - 1310) / 2, 30)
|
||||
height: Math.min(contentHeight, 200)
|
||||
height: textInputViewLayout.implicitHeight
|
||||
visible: !currentChat.isServer && ModelList.selectableModels.count !== 0
|
||||
MyTextArea {
|
||||
id: textInput
|
||||
color: theme.textColor
|
||||
topPadding: 15
|
||||
bottomPadding: 15
|
||||
leftPadding: 20
|
||||
rightPadding: 40
|
||||
enabled: currentChat.isModelLoaded && !currentChat.isServer
|
||||
onEnabledChanged: {
|
||||
|
||||
MouseArea {
|
||||
id: textInputViewMouseArea
|
||||
anchors.fill: parent
|
||||
onClicked: (mouse) => {
|
||||
if (textInput.enabled)
|
||||
textInput.forceActiveFocus();
|
||||
}
|
||||
font.pixelSize: theme.fontSizeLarger
|
||||
placeholderText: currentChat.isModelLoaded ? qsTr("Send a message...") : qsTr("Load a model to continue...")
|
||||
Accessible.role: Accessible.EditableText
|
||||
Accessible.name: placeholderText
|
||||
Accessible.description: qsTr("Send messages/prompts to the model")
|
||||
Keys.onReturnPressed: (event)=> {
|
||||
if (event.modifiers & Qt.ControlModifier || event.modifiers & Qt.ShiftModifier)
|
||||
event.accepted = false;
|
||||
else {
|
||||
editingFinished();
|
||||
sendMessage()
|
||||
}
|
||||
}
|
||||
function sendMessage() {
|
||||
if (textInput.text === "" || currentChat.responseInProgress || currentChat.restoringFromText)
|
||||
return
|
||||
}
|
||||
|
||||
currentChat.stopGenerating()
|
||||
currentChat.newPromptResponsePair(textInput.text);
|
||||
currentChat.prompt(textInput.text,
|
||||
MySettings.promptTemplate,
|
||||
MySettings.maxLength,
|
||||
MySettings.topK,
|
||||
MySettings.topP,
|
||||
MySettings.minP,
|
||||
MySettings.temperature,
|
||||
MySettings.promptBatchSize,
|
||||
MySettings.repeatPenalty,
|
||||
MySettings.repeatPenaltyTokens)
|
||||
textInput.text = ""
|
||||
}
|
||||
GridLayout {
|
||||
id: textInputViewLayout
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
rows: 2
|
||||
columns: 3
|
||||
rowSpacing: 0
|
||||
columnSpacing: 0
|
||||
Flow {
|
||||
id: attachmentsFlow
|
||||
visible: attachmentModel.count
|
||||
Layout.row: 0
|
||||
Layout.column: 1
|
||||
Layout.topMargin: 15
|
||||
Layout.leftMargin: 15
|
||||
Layout.rightMargin: 15
|
||||
spacing: 10
|
||||
|
||||
MouseArea {
|
||||
id: textInputMouseArea
|
||||
anchors.fill: parent
|
||||
acceptedButtons: Qt.RightButton
|
||||
Repeater {
|
||||
model: attachmentModel
|
||||
|
||||
onClicked: (mouse) => {
|
||||
if (mouse.button === Qt.RightButton) {
|
||||
textInputContextMenu.x = textInputMouseArea.mouseX
|
||||
textInputContextMenu.y = textInputMouseArea.mouseY
|
||||
textInputContextMenu.open()
|
||||
Rectangle {
|
||||
width: attachmentFileIcon2.width + attachmentFileText2.width + 40
|
||||
height: 50
|
||||
radius: 5
|
||||
color: theme.attachmentBackground
|
||||
border.color: theme.controlBorder
|
||||
|
||||
Row {
|
||||
spacing: 5
|
||||
anchors.fill: parent
|
||||
anchors.margins: 5
|
||||
|
||||
Item {
|
||||
id: attachmentFileIcon2
|
||||
width: 40
|
||||
height: 40
|
||||
Image {
|
||||
id: fileIcon2
|
||||
anchors.fill: parent
|
||||
visible: false
|
||||
sourceSize.width: 40
|
||||
sourceSize.height: 40
|
||||
mipmap: true
|
||||
source: {
|
||||
return "qrc:/gpt4all/icons/file-xls.svg"
|
||||
}
|
||||
}
|
||||
ColorOverlay {
|
||||
anchors.fill: fileIcon2
|
||||
source: fileIcon2
|
||||
color: theme.textColor
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
id: attachmentFileText2
|
||||
height: 40
|
||||
text: model.file
|
||||
color: theme.textColor
|
||||
horizontalAlignment: Text.AlignHLeft
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
font.pixelSize: theme.fontSizeSmall
|
||||
font.bold: true
|
||||
wrapMode: Text.WrapAnywhere
|
||||
}
|
||||
}
|
||||
|
||||
MyMiniButton {
|
||||
id: removeAttachmentButton
|
||||
anchors.top: parent.top
|
||||
anchors.right: parent.right
|
||||
backgroundColor: theme.textColor
|
||||
backgroundColorHovered: theme.iconBackgroundDark
|
||||
source: "qrc:/gpt4all/icons/close.svg"
|
||||
onClicked: {
|
||||
attachmentModel.remove(index)
|
||||
if (textInput.enabled)
|
||||
textInput.forceActiveFocus();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MyMenu {
|
||||
id: textInputContextMenu
|
||||
MyMenuItem {
|
||||
text: qsTr("Cut")
|
||||
enabled: textInput.selectedText !== ""
|
||||
height: enabled ? implicitHeight : 0
|
||||
onTriggered: textInput.cut()
|
||||
MyToolButton {
|
||||
id: plusButton
|
||||
Layout.row: 1
|
||||
Layout.column: 0
|
||||
Layout.leftMargin: 15
|
||||
Layout.alignment: Qt.AlignCenter
|
||||
backgroundColor: theme.conversationInputButtonBackground
|
||||
backgroundColorHovered: theme.conversationInputButtonBackgroundHovered
|
||||
imageWidth: theme.fontSizeLargest
|
||||
imageHeight: theme.fontSizeLargest
|
||||
visible: !currentChat.isServer && ModelList.selectableModels.count !== 0 && currentChat.isModelLoaded
|
||||
enabled: !currentChat.responseInProgress
|
||||
source: "qrc:/gpt4all/icons/paperclip.svg"
|
||||
Accessible.name: qsTr("Add media")
|
||||
Accessible.description: qsTr("Adds media to the prompt")
|
||||
|
||||
onClicked: (mouse) => {
|
||||
addMediaMenu.open()
|
||||
}
|
||||
}
|
||||
|
||||
ScrollView {
|
||||
id: textInputScrollView
|
||||
Layout.row: 1
|
||||
Layout.column: 1
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: plusButton.visible ? 5 : 15
|
||||
Layout.margins: 15
|
||||
height: Math.min(contentHeight, 200)
|
||||
|
||||
MyTextArea {
|
||||
id: textInput
|
||||
color: theme.textColor
|
||||
padding: 0
|
||||
enabled: currentChat.isModelLoaded && !currentChat.isServer
|
||||
onEnabledChanged: {
|
||||
if (textInput.enabled)
|
||||
textInput.forceActiveFocus();
|
||||
}
|
||||
font.pixelSize: theme.fontSizeLarger
|
||||
placeholderText: currentChat.isModelLoaded ? qsTr("Send a message...") : qsTr("Load a model to continue...")
|
||||
Accessible.role: Accessible.EditableText
|
||||
Accessible.name: placeholderText
|
||||
Accessible.description: qsTr("Send messages/prompts to the model")
|
||||
Keys.onReturnPressed: (event)=> {
|
||||
if (event.modifiers & Qt.ControlModifier || event.modifiers & Qt.ShiftModifier)
|
||||
event.accepted = false;
|
||||
else {
|
||||
editingFinished();
|
||||
sendMessage()
|
||||
}
|
||||
}
|
||||
function sendMessage() {
|
||||
if ((textInput.text === "" && attachmentModel.count === 0) || currentChat.responseInProgress || currentChat.restoringFromText)
|
||||
return
|
||||
|
||||
currentChat.stopGenerating()
|
||||
currentChat.prompt(textInput.text, attachmentModel.getAttachmentUrls())
|
||||
attachmentModel.clear();
|
||||
textInput.text = ""
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: textInputMouseArea
|
||||
anchors.fill: parent
|
||||
acceptedButtons: Qt.RightButton
|
||||
|
||||
onClicked: (mouse) => {
|
||||
if (mouse.button === Qt.RightButton) {
|
||||
textInputContextMenu.x = textInputMouseArea.mouseX
|
||||
textInputContextMenu.y = textInputMouseArea.mouseY
|
||||
textInputContextMenu.open()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
background: Rectangle {
|
||||
implicitWidth: 150
|
||||
color: "transparent"
|
||||
}
|
||||
|
||||
MyMenu {
|
||||
id: textInputContextMenu
|
||||
MyMenuItem {
|
||||
text: qsTr("Cut")
|
||||
enabled: textInput.selectedText !== ""
|
||||
height: enabled ? implicitHeight : 0
|
||||
onTriggered: textInput.cut()
|
||||
}
|
||||
MyMenuItem {
|
||||
text: qsTr("Copy")
|
||||
enabled: textInput.selectedText !== ""
|
||||
height: enabled ? implicitHeight : 0
|
||||
onTriggered: textInput.copy()
|
||||
}
|
||||
MyMenuItem {
|
||||
text: qsTr("Paste")
|
||||
onTriggered: textInput.paste()
|
||||
}
|
||||
MyMenuItem {
|
||||
text: qsTr("Select All")
|
||||
onTriggered: textInput.selectAll()
|
||||
}
|
||||
}
|
||||
}
|
||||
MyMenuItem {
|
||||
text: qsTr("Copy")
|
||||
enabled: textInput.selectedText !== ""
|
||||
height: enabled ? implicitHeight : 0
|
||||
onTriggered: textInput.copy()
|
||||
}
|
||||
|
||||
Row {
|
||||
Layout.row: 1
|
||||
Layout.column: 2
|
||||
Layout.rightMargin: 15
|
||||
Layout.alignment: Qt.AlignCenter
|
||||
|
||||
MyToolButton {
|
||||
id: stopButton
|
||||
backgroundColor: theme.conversationInputButtonBackground
|
||||
backgroundColorHovered: theme.conversationInputButtonBackgroundHovered
|
||||
visible: currentChat.responseInProgress && !currentChat.isServer
|
||||
|
||||
background: Item {
|
||||
anchors.fill: parent
|
||||
Image {
|
||||
id: stopImage
|
||||
anchors.centerIn: parent
|
||||
visible: false
|
||||
fillMode: Image.PreserveAspectFit
|
||||
mipmap: true
|
||||
sourceSize.width: theme.fontSizeLargest
|
||||
sourceSize.height: theme.fontSizeLargest
|
||||
source: "qrc:/gpt4all/icons/stop_generating.svg"
|
||||
}
|
||||
Rectangle {
|
||||
anchors.centerIn: stopImage
|
||||
width: theme.fontSizeLargest + 8
|
||||
height: theme.fontSizeLargest + 8
|
||||
color: theme.viewBackground
|
||||
border.pixelAligned: false
|
||||
border.color: theme.controlBorder
|
||||
border.width: 1
|
||||
radius: width / 2
|
||||
}
|
||||
ColorOverlay {
|
||||
anchors.fill: stopImage
|
||||
source: stopImage
|
||||
color: stopButton.hovered ? stopButton.backgroundColorHovered : stopButton.backgroundColor
|
||||
}
|
||||
}
|
||||
|
||||
Accessible.name: qsTr("Stop generating")
|
||||
Accessible.description: qsTr("Stop the current response generation")
|
||||
ToolTip.visible: stopButton.hovered
|
||||
ToolTip.text: Accessible.description
|
||||
|
||||
onClicked: {
|
||||
var index = Math.max(0, chatModel.count - 1);
|
||||
var listElement = chatModel.get(index);
|
||||
listElement.stopped = true
|
||||
currentChat.stopGenerating()
|
||||
}
|
||||
}
|
||||
MyMenuItem {
|
||||
text: qsTr("Paste")
|
||||
onTriggered: textInput.paste()
|
||||
}
|
||||
MyMenuItem {
|
||||
text: qsTr("Select All")
|
||||
onTriggered: textInput.selectAll()
|
||||
|
||||
MyToolButton {
|
||||
id: sendButton
|
||||
backgroundColor: theme.conversationInputButtonBackground
|
||||
backgroundColorHovered: theme.conversationInputButtonBackgroundHovered
|
||||
imageWidth: theme.fontSizeLargest
|
||||
imageHeight: theme.fontSizeLargest
|
||||
visible: !currentChat.responseInProgress && !currentChat.isServer && ModelList.selectableModels.count !== 0
|
||||
source: "qrc:/gpt4all/icons/send_message.svg"
|
||||
Accessible.name: qsTr("Send message")
|
||||
Accessible.description: qsTr("Sends the message/prompt contained in textfield to the model")
|
||||
ToolTip.visible: sendButton.hovered
|
||||
ToolTip.text: Accessible.description
|
||||
|
||||
onClicked: {
|
||||
textInput.sendMessage()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
MyToolButton {
|
||||
id: stopButton
|
||||
backgroundColor: theme.conversationInputButtonBackground
|
||||
backgroundColorHovered: theme.conversationInputButtonBackgroundHovered
|
||||
anchors.right: textInputView.right
|
||||
anchors.verticalCenter: textInputView.verticalCenter
|
||||
anchors.rightMargin: 15
|
||||
visible: currentChat.responseInProgress && !currentChat.isServer
|
||||
|
||||
background: Item {
|
||||
anchors.fill: parent
|
||||
Image {
|
||||
id: stopImage
|
||||
anchors.centerIn: parent
|
||||
visible: false
|
||||
fillMode: Image.PreserveAspectFit
|
||||
mipmap: true
|
||||
sourceSize.width: theme.fontSizeLargest
|
||||
sourceSize.height: theme.fontSizeLargest
|
||||
source: "qrc:/gpt4all/icons/stop_generating.svg"
|
||||
}
|
||||
Rectangle {
|
||||
anchors.centerIn: stopImage
|
||||
width: theme.fontSizeLargest + 8
|
||||
height: theme.fontSizeLargest + 8
|
||||
color: theme.viewBackground
|
||||
border.pixelAligned: false
|
||||
border.color: theme.controlBorder
|
||||
border.width: 1
|
||||
radius: width / 2
|
||||
}
|
||||
ColorOverlay {
|
||||
anchors.fill: stopImage
|
||||
source: stopImage
|
||||
color: stopButton.hovered ? stopButton.backgroundColorHovered : stopButton.backgroundColor
|
||||
}
|
||||
}
|
||||
|
||||
Accessible.name: qsTr("Stop generating")
|
||||
Accessible.description: qsTr("Stop the current response generation")
|
||||
ToolTip.visible: stopButton.hovered
|
||||
ToolTip.text: Accessible.description
|
||||
|
||||
onClicked: {
|
||||
var index = Math.max(0, chatModel.count - 1);
|
||||
var listElement = chatModel.get(index);
|
||||
listElement.stopped = true
|
||||
currentChat.stopGenerating()
|
||||
}
|
||||
MyFileDialog {
|
||||
id: fileDialog
|
||||
nameFilters: ["Excel files (*.xlsx)"]
|
||||
}
|
||||
|
||||
MyToolButton {
|
||||
id: sendButton
|
||||
backgroundColor: theme.conversationInputButtonBackground
|
||||
backgroundColorHovered: theme.conversationInputButtonBackgroundHovered
|
||||
anchors.right: textInputView.right
|
||||
anchors.verticalCenter: textInputView.verticalCenter
|
||||
anchors.rightMargin: 15
|
||||
imageWidth: theme.fontSizeLargest
|
||||
imageHeight: theme.fontSizeLargest
|
||||
visible: !currentChat.responseInProgress && !currentChat.isServer && ModelList.selectableModels.count !== 0
|
||||
source: "qrc:/gpt4all/icons/send_message.svg"
|
||||
Accessible.name: qsTr("Send message")
|
||||
Accessible.description: qsTr("Sends the message/prompt contained in textfield to the model")
|
||||
ToolTip.visible: sendButton.hovered
|
||||
ToolTip.text: Accessible.description
|
||||
|
||||
onClicked: {
|
||||
textInput.sendMessage()
|
||||
MyMenu {
|
||||
id: addMediaMenu
|
||||
x: textInputView.x
|
||||
y: textInputView.y - addMediaMenu.height - 10;
|
||||
title: qsTr("Attach")
|
||||
MyMenuItem {
|
||||
text: qsTr("Single File")
|
||||
icon.source: "qrc:/gpt4all/icons/file.svg"
|
||||
icon.width: 24
|
||||
icon.height: 24
|
||||
onClicked: {
|
||||
fileDialog.openFileDialog(StandardPaths.writableLocation(StandardPaths.HomeLocation), function(selectedFile) {
|
||||
if (selectedFile) {
|
||||
var file = selectedFile.toString().split("/").pop()
|
||||
attachmentModel.append({
|
||||
file: file,
|
||||
url: selectedFile
|
||||
})
|
||||
}
|
||||
if (textInput.enabled)
|
||||
textInput.forceActiveFocus();
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
19
gpt4all-chat/qml/MyFileDialog.qml
Normal file
19
gpt4all-chat/qml/MyFileDialog.qml
Normal file
@ -0,0 +1,19 @@
|
||||
import QtCore
|
||||
import QtQuick
|
||||
import QtQuick.Dialogs
|
||||
|
||||
FileDialog {
|
||||
id: fileDialog
|
||||
title: qsTr("Please choose a file")
|
||||
property var acceptedConnection: null
|
||||
|
||||
function openFileDialog(currentFolder, onAccepted) {
|
||||
fileDialog.currentFolder = currentFolder;
|
||||
if (acceptedConnection !== null) {
|
||||
fileDialog.accepted.disconnect(acceptedConnection);
|
||||
}
|
||||
acceptedConnection = function() { onAccepted(fileDialog.selectedFile); };
|
||||
fileDialog.accepted.connect(acceptedConnection);
|
||||
fileDialog.open();
|
||||
}
|
||||
}
|
14
gpt4all-chat/qml/MyFolderDialog.qml
Normal file
14
gpt4all-chat/qml/MyFolderDialog.qml
Normal file
@ -0,0 +1,14 @@
|
||||
import QtCore
|
||||
import QtQuick
|
||||
import QtQuick.Dialogs
|
||||
|
||||
FolderDialog {
|
||||
id: folderDialog
|
||||
title: qsTr("Please choose a directory")
|
||||
|
||||
function openFolderDialog(currentFolder, onAccepted) {
|
||||
folderDialog.currentFolder = currentFolder;
|
||||
folderDialog.accepted.connect(function() { onAccepted(folderDialog.selectedFolder); });
|
||||
folderDialog.open();
|
||||
}
|
||||
}
|
@ -22,12 +22,30 @@ Menu {
|
||||
|
||||
contentItem: Rectangle {
|
||||
implicitWidth: myListView.contentWidth
|
||||
implicitHeight: myListView.contentHeight
|
||||
implicitHeight: (myTitle.visible ? myTitle.contentHeight + 10: 0) + myListView.contentHeight
|
||||
color: "transparent"
|
||||
|
||||
Text {
|
||||
id: myTitle
|
||||
visible: menu.title !== ""
|
||||
text: menu.title
|
||||
anchors.margins: 10
|
||||
anchors.top: parent.top
|
||||
anchors.right: parent.right
|
||||
anchors.left: parent.left
|
||||
leftPadding: 15
|
||||
rightPadding: 10
|
||||
padding: 5
|
||||
color: theme.styledTextColor
|
||||
font.pixelSize: theme.fontSizeSmall
|
||||
}
|
||||
ListView {
|
||||
id: myListView
|
||||
anchors.margins: 10
|
||||
anchors.fill: parent
|
||||
anchors.top: title.bottom
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.right: parent.right
|
||||
anchors.left: parent.left
|
||||
implicitHeight: contentHeight
|
||||
model: menu.contentModel
|
||||
interactive: Window.window
|
||||
|
@ -1,7 +1,9 @@
|
||||
import Qt5Compat.GraphicalEffects
|
||||
import QtCore
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Controls.Basic
|
||||
import QtQuick.Layouts
|
||||
|
||||
MenuItem {
|
||||
id: item
|
||||
@ -11,12 +13,40 @@ MenuItem {
|
||||
color: item.highlighted ? theme.menuHighlightColor : theme.menuBackgroundColor
|
||||
}
|
||||
|
||||
contentItem: Text {
|
||||
leftPadding: 10
|
||||
rightPadding: 10
|
||||
padding: 5
|
||||
text: item.text
|
||||
color: theme.textColor
|
||||
font.pixelSize: theme.fontSizeLarge
|
||||
contentItem: RowLayout {
|
||||
spacing: 0
|
||||
Item {
|
||||
visible: item.icon.source.toString() !== ""
|
||||
Layout.leftMargin: 6
|
||||
Layout.preferredWidth: item.icon.width
|
||||
Layout.preferredHeight: item.icon.height
|
||||
Image {
|
||||
id: image
|
||||
anchors.centerIn: parent
|
||||
visible: false
|
||||
fillMode: Image.PreserveAspectFit
|
||||
mipmap: true
|
||||
sourceSize.width: item.icon.width
|
||||
sourceSize.height: item.icon.height
|
||||
source: item.icon.source
|
||||
}
|
||||
ColorOverlay {
|
||||
anchors.fill: image
|
||||
source: image
|
||||
color: theme.textColor
|
||||
}
|
||||
}
|
||||
Text {
|
||||
Layout.alignment: Qt.AlignLeft
|
||||
padding: 5
|
||||
text: item.text
|
||||
color: theme.textColor
|
||||
font.pixelSize: theme.fontSizeLarge
|
||||
}
|
||||
Rectangle {
|
||||
color: "transparent"
|
||||
Layout.fillWidth: true
|
||||
height: 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -61,17 +61,6 @@ Item {
|
||||
color: theme.settingsDivider
|
||||
}
|
||||
|
||||
FolderDialog {
|
||||
id: folderDialog
|
||||
title: qsTr("Please choose a directory")
|
||||
}
|
||||
|
||||
function openFolderDialog(currentFolder, onAccepted) {
|
||||
folderDialog.currentFolder = currentFolder;
|
||||
folderDialog.accepted.connect(function() { onAccepted(folderDialog.selectedFolder); });
|
||||
folderDialog.open();
|
||||
}
|
||||
|
||||
StackLayout {
|
||||
id: stackLayout
|
||||
anchors.top: tabTitlesModel.count > 1 ? dividerTabBar.bottom : parent.top
|
||||
@ -88,7 +77,6 @@ Item {
|
||||
sourceComponent: model.modelData
|
||||
onLoaded: {
|
||||
settingsStack.tabTitlesModel.append({ "title": loader.item.title });
|
||||
item.openFolderDialog = settingsStack.openFolderDialog;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -9,7 +9,6 @@ Item {
|
||||
property string title: ""
|
||||
property Item contentItem: null
|
||||
property bool showRestoreDefaultsButton: true
|
||||
property var openFolderDialog
|
||||
signal restoreDefaultsClicked
|
||||
|
||||
onContentItemChanged: function() {
|
||||
|
@ -177,6 +177,17 @@ QtObject {
|
||||
}
|
||||
}
|
||||
|
||||
property color attachmentBackground: {
|
||||
switch (MySettings.chatTheme) {
|
||||
case MySettingsEnums.ChatTheme.LegacyDark:
|
||||
return blue1000
|
||||
case MySettingsEnums.ChatTheme.Dark:
|
||||
return darkgray200
|
||||
default:
|
||||
return gray200
|
||||
}
|
||||
}
|
||||
|
||||
property color disabledControlBackground: {
|
||||
switch (MySettings.chatTheme) {
|
||||
case MySettingsEnums.ChatTheme.LegacyDark:
|
||||
|
@ -4,6 +4,7 @@
|
||||
#include "mysettings.h"
|
||||
#include "network.h"
|
||||
#include "server.h"
|
||||
#include "xlsxtomd.h"
|
||||
|
||||
#include <QDataStream>
|
||||
#include <QDateTime>
|
||||
@ -124,10 +125,28 @@ void Chat::resetResponseState()
|
||||
emit responseStateChanged();
|
||||
}
|
||||
|
||||
void Chat::prompt(const QString &prompt)
|
||||
void Chat::prompt(const QString &prompt, const QList<QUrl> &attachedUrls)
|
||||
{
|
||||
newPromptResponsePairInternal(prompt, attachedUrls);
|
||||
emit resetResponseRequested();
|
||||
|
||||
resetResponseState();
|
||||
emit promptRequested(m_collections, prompt);
|
||||
|
||||
QStringList attachedContexts;
|
||||
for (const QUrl &url : attachedUrls) {
|
||||
Q_ASSERT(url.isLocalFile());
|
||||
const QString localFilePath = url.toLocalFile();
|
||||
const QFileInfo info(localFilePath);
|
||||
Q_ASSERT(info.suffix() == "xlsx"); // We only support excel right now
|
||||
Q_ASSERT(info.exists());
|
||||
Q_ASSERT(info.isFile());
|
||||
attachedContexts << XLSXToMD::toMarkdown(info.canonicalFilePath());
|
||||
}
|
||||
|
||||
if (!attachedContexts.isEmpty())
|
||||
emit promptRequested(m_collections, attachedContexts.join("\n\n") + "\n\n" + prompt);
|
||||
else
|
||||
emit promptRequested(m_collections, prompt);
|
||||
}
|
||||
|
||||
void Chat::regenerateResponse()
|
||||
@ -234,21 +253,17 @@ void Chat::setModelInfo(const ModelInfo &modelInfo)
|
||||
emit modelChangeRequested(modelInfo);
|
||||
}
|
||||
|
||||
void Chat::newPromptResponsePair(const QString &prompt)
|
||||
// the server needs to block until response is reset, so it calls resetResponse on its own m_llmThread
|
||||
void Chat::serverNewPromptResponsePair(const QString &prompt, const QList<QUrl> &attachedUrls)
|
||||
{
|
||||
resetResponseState();
|
||||
m_chatModel->updateCurrentResponse(m_chatModel->count() - 1, false);
|
||||
m_chatModel->appendPrompt("Prompt: ", prompt);
|
||||
m_chatModel->appendResponse("Response: ", QString());
|
||||
emit resetResponseRequested();
|
||||
newPromptResponsePairInternal(prompt, attachedUrls);
|
||||
}
|
||||
|
||||
// the server needs to block until response is reset, so it calls resetResponse on its own m_llmThread
|
||||
void Chat::serverNewPromptResponsePair(const QString &prompt)
|
||||
void Chat::newPromptResponsePairInternal(const QString &prompt, const QList<QUrl> &attachedUrls)
|
||||
{
|
||||
resetResponseState();
|
||||
m_chatModel->updateCurrentResponse(m_chatModel->count() - 1, false);
|
||||
m_chatModel->appendPrompt("Prompt: ", prompt);
|
||||
m_chatModel->appendPrompt("Prompt: ", prompt, attachedUrls);
|
||||
m_chatModel->appendResponse("Response: ", QString());
|
||||
}
|
||||
|
||||
|
@ -76,10 +76,9 @@ public:
|
||||
bool isModelLoaded() const { return m_modelLoadingPercentage == 1.0f; }
|
||||
bool isCurrentlyLoading() const { return m_modelLoadingPercentage > 0.0f && m_modelLoadingPercentage < 1.0f; }
|
||||
float modelLoadingPercentage() const { return m_modelLoadingPercentage; }
|
||||
Q_INVOKABLE void prompt(const QString &prompt);
|
||||
Q_INVOKABLE void prompt(const QString &prompt, const QList<QUrl> &attachedUrls = QList<QUrl>());
|
||||
Q_INVOKABLE void regenerateResponse();
|
||||
Q_INVOKABLE void stopGenerating();
|
||||
Q_INVOKABLE void newPromptResponsePair(const QString &prompt);
|
||||
|
||||
QList<ResultInfo> databaseResults() const { return m_databaseResults; }
|
||||
|
||||
@ -124,7 +123,7 @@ public:
|
||||
QList<QString> generatedQuestions() const { return m_generatedQuestions; }
|
||||
|
||||
public Q_SLOTS:
|
||||
void serverNewPromptResponsePair(const QString &prompt);
|
||||
void serverNewPromptResponsePair(const QString &prompt, const QList<QUrl> &attachedUrls = QList<QUrl>());
|
||||
|
||||
Q_SIGNALS:
|
||||
void idChanged(const QString &id);
|
||||
@ -173,6 +172,9 @@ private Q_SLOTS:
|
||||
void handleModelInfoChanged(const ModelInfo &modelInfo);
|
||||
void handleTrySwitchContextOfLoadedModelCompleted(int value);
|
||||
|
||||
private:
|
||||
void newPromptResponsePairInternal(const QString &prompt, const QList<QUrl> &attachedUrls);
|
||||
|
||||
private:
|
||||
QString m_id;
|
||||
QString m_name;
|
||||
|
@ -16,6 +16,24 @@
|
||||
#include <Qt>
|
||||
#include <QtGlobal>
|
||||
|
||||
struct PromptAttachment {
|
||||
Q_GADGET
|
||||
Q_PROPERTY(QUrl url MEMBER url)
|
||||
Q_PROPERTY(QString file MEMBER file)
|
||||
|
||||
public:
|
||||
QUrl url;
|
||||
QString file;
|
||||
bool operator==(const PromptAttachment &other) const {
|
||||
return url == other.url &&
|
||||
file == other.file;
|
||||
}
|
||||
bool operator!=(const PromptAttachment &other) const {
|
||||
return !(*this == other);
|
||||
}
|
||||
};
|
||||
Q_DECLARE_METATYPE(PromptAttachment)
|
||||
|
||||
struct ChatItem
|
||||
{
|
||||
Q_GADGET
|
||||
@ -30,6 +48,7 @@ struct ChatItem
|
||||
Q_PROPERTY(bool thumbsDownState MEMBER thumbsDownState)
|
||||
Q_PROPERTY(QList<ResultInfo> sources MEMBER sources)
|
||||
Q_PROPERTY(QList<ResultInfo> consolidatedSources MEMBER consolidatedSources)
|
||||
Q_PROPERTY(QList<PromptAttachment> promptAttachments MEMBER promptAttachments);
|
||||
|
||||
public:
|
||||
// TODO: Maybe we should include the model name here as well as timestamp?
|
||||
@ -40,6 +59,7 @@ public:
|
||||
QString newResponse;
|
||||
QList<ResultInfo> sources;
|
||||
QList<ResultInfo> consolidatedSources;
|
||||
QList<PromptAttachment> promptAttachments;
|
||||
bool currentResponse = false;
|
||||
bool stopped = false;
|
||||
bool thumbsUpState = false;
|
||||
@ -66,7 +86,8 @@ public:
|
||||
ThumbsUpStateRole,
|
||||
ThumbsDownStateRole,
|
||||
SourcesRole,
|
||||
ConsolidatedSourcesRole
|
||||
ConsolidatedSourcesRole,
|
||||
PromptAttachmentsRole
|
||||
};
|
||||
|
||||
int rowCount(const QModelIndex &parent = QModelIndex()) const override
|
||||
@ -104,6 +125,8 @@ public:
|
||||
return QVariant::fromValue(item.sources);
|
||||
case ConsolidatedSourcesRole:
|
||||
return QVariant::fromValue(item.consolidatedSources);
|
||||
case PromptAttachmentsRole:
|
||||
return QVariant::fromValue(item.promptAttachments);
|
||||
}
|
||||
|
||||
return QVariant();
|
||||
@ -123,14 +146,29 @@ public:
|
||||
roles[ThumbsDownStateRole] = "thumbsDownState";
|
||||
roles[SourcesRole] = "sources";
|
||||
roles[ConsolidatedSourcesRole] = "consolidatedSources";
|
||||
roles[PromptAttachmentsRole] = "promptAttachments";
|
||||
return roles;
|
||||
}
|
||||
|
||||
void appendPrompt(const QString &name, const QString &value)
|
||||
void appendPrompt(const QString &name, const QString &value, const QList<QUrl> &attachedUrls)
|
||||
{
|
||||
ChatItem item;
|
||||
item.name = name;
|
||||
item.value = value;
|
||||
|
||||
for (const QUrl &url : attachedUrls) {
|
||||
Q_ASSERT(url.isLocalFile());
|
||||
const QString localFilePath = url.toLocalFile();
|
||||
const QFileInfo info(localFilePath);
|
||||
Q_ASSERT(info.suffix() == "xlsx"); // We only support excel right now
|
||||
Q_ASSERT(info.exists());
|
||||
Q_ASSERT(info.isFile());
|
||||
PromptAttachment attached;
|
||||
attached.url = url;
|
||||
attached.file = info.fileName();
|
||||
item.promptAttachments << attached;
|
||||
}
|
||||
|
||||
beginInsertRows(QModelIndex(), m_chatItems.size(), m_chatItems.size());
|
||||
m_chatItems.append(item);
|
||||
endInsertRows();
|
||||
|
@ -32,7 +32,7 @@ public Q_SLOTS:
|
||||
void start();
|
||||
|
||||
Q_SIGNALS:
|
||||
void requestServerNewPromptResponsePair(const QString &prompt);
|
||||
void requestServerNewPromptResponsePair(const QString &prompt, const QList<QUrl> &attachedUrls = QList<QUrl>());
|
||||
|
||||
private:
|
||||
auto handleCompletionRequest(const CompletionRequest &request) -> std::pair<QHttpServerResponse, std::optional<QJsonObject>>;
|
||||
|
150
gpt4all-chat/src/xlsxtomd.cpp
Normal file
150
gpt4all-chat/src/xlsxtomd.cpp
Normal file
@ -0,0 +1,150 @@
|
||||
#include "xlsxtomd.h"
|
||||
|
||||
#include <xlsxcellrange.h>
|
||||
#include <xlsxdocument.h>
|
||||
#include <xlsxworkbook.h>
|
||||
|
||||
QString formatCellText(const QXlsx::Cell* cell)
|
||||
{
|
||||
if (!cell) return QString();
|
||||
|
||||
QVariant value = cell->value();
|
||||
QXlsx::Format format = cell->format();
|
||||
QString cellText;
|
||||
|
||||
// Determine the cell type based on format
|
||||
if (format.isDateTimeFormat()) {
|
||||
// Handle DateTime
|
||||
QDateTime dateTime = value.toDateTime();
|
||||
cellText = dateTime.isValid() ? dateTime.toString("yyyy-MM-dd") : value.toString();
|
||||
} else {
|
||||
cellText = value.toString();
|
||||
}
|
||||
|
||||
if (cellText.isEmpty())
|
||||
return QString();
|
||||
|
||||
// Apply Markdown and HTML formatting based on font styles
|
||||
QString formattedText = cellText;
|
||||
|
||||
if (format.fontBold() && format.fontItalic())
|
||||
formattedText = "***" + formattedText + "***";
|
||||
else if (format.fontBold())
|
||||
formattedText = "**" + formattedText + "**";
|
||||
else if (format.fontItalic())
|
||||
formattedText = "*" + formattedText + "*";
|
||||
|
||||
if (format.fontStrikeOut())
|
||||
formattedText = "~~" + formattedText + "~~";
|
||||
|
||||
// Escape pipe characters to prevent Markdown table issues
|
||||
formattedText.replace("|", "\\|");
|
||||
|
||||
return formattedText;
|
||||
}
|
||||
|
||||
QString getCellValue(QXlsx::Worksheet* sheet, int row, int col) {
|
||||
if (!sheet)
|
||||
return QString();
|
||||
|
||||
// Attempt to retrieve the cell directly
|
||||
std::shared_ptr<QXlsx::Cell> cell = sheet->cellAt(row, col);
|
||||
|
||||
// If the cell is part of a merged range and not directly available
|
||||
if (!cell) {
|
||||
for (const QXlsx::CellRange& range : sheet->mergedCells()) {
|
||||
if (row >= range.firstRow() && row <= range.lastRow() &&
|
||||
col >= range.firstColumn() && col <= range.lastColumn()) {
|
||||
cell = sheet->cellAt(range.firstRow(), range.firstColumn());
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Format and return the cell text if available
|
||||
if (cell)
|
||||
return formatCellText(cell.get());
|
||||
|
||||
// Return empty string if cell is not found
|
||||
return QString();
|
||||
}
|
||||
|
||||
QString XLSXToMD::toMarkdown(const QString &xlsxFilePath)
|
||||
{
|
||||
// Load the Excel document
|
||||
QXlsx::Document xlsx(xlsxFilePath);
|
||||
if (!xlsx.load()) {
|
||||
qCritical() << "Failed to load the Excel file:" << xlsxFilePath;
|
||||
return QString();
|
||||
}
|
||||
|
||||
QString markdown;
|
||||
|
||||
// Retrieve all sheet names
|
||||
QStringList sheetNames = xlsx.sheetNames();
|
||||
if (sheetNames.isEmpty()) {
|
||||
qWarning() << "No sheets found in the Excel document.";
|
||||
return QString();
|
||||
}
|
||||
|
||||
// Iterate through each worksheet by name
|
||||
for (const QString &sheetName : sheetNames) {
|
||||
QXlsx::Worksheet* sheet = dynamic_cast<QXlsx::Worksheet*>(xlsx.sheet(sheetName));
|
||||
if (!sheet) {
|
||||
qWarning() << "Failed to load sheet:" << sheetName;
|
||||
continue;
|
||||
}
|
||||
|
||||
markdown += QString("## %1\n\n").arg(sheetName);
|
||||
|
||||
// Determine the used range
|
||||
QXlsx::CellRange range = sheet->dimension();
|
||||
int firstRow = range.firstRow();
|
||||
int lastRow = range.lastRow();
|
||||
int firstCol = range.firstColumn();
|
||||
int lastCol = range.lastColumn();
|
||||
|
||||
if (firstRow > lastRow || firstCol > lastCol) {
|
||||
qWarning() << "Sheet" << sheetName << "is empty.";
|
||||
markdown += "*No data available.*\n\n";
|
||||
continue;
|
||||
}
|
||||
|
||||
// Assume the first row is the header
|
||||
int headerRow = firstRow;
|
||||
|
||||
// Collect headers
|
||||
QStringList headers;
|
||||
for (int col = firstCol; col <= lastCol; ++col) {
|
||||
QString header = getCellValue(sheet, headerRow, col);
|
||||
headers << header;
|
||||
}
|
||||
|
||||
// Create Markdown header row
|
||||
QString headerRowMarkdown = "|" + headers.join("|") + "|";
|
||||
markdown += headerRowMarkdown + "\n";
|
||||
|
||||
// Create Markdown separator row
|
||||
QStringList separators;
|
||||
for (int i = 0; i < headers.size(); ++i) {
|
||||
separators << "---";
|
||||
}
|
||||
QString separatorRow = "|" + separators.join("|") + "|";
|
||||
markdown += separatorRow + "\n";
|
||||
|
||||
// Iterate through data rows (starting from the row after header)
|
||||
for (int row = headerRow + 1; row <= lastRow; ++row) {
|
||||
QStringList rowData;
|
||||
for (int col = firstCol; col <= lastCol; ++col) {
|
||||
QString cellText = getCellValue(sheet, row, col);
|
||||
rowData << cellText;
|
||||
}
|
||||
|
||||
QString dataRow = "|" + rowData.join("|") + "|";
|
||||
markdown += dataRow + "\n";
|
||||
}
|
||||
|
||||
markdown += "\n"; // Add an empty line between sheets
|
||||
}
|
||||
return markdown;
|
||||
}
|
14
gpt4all-chat/src/xlsxtomd.h
Normal file
14
gpt4all-chat/src/xlsxtomd.h
Normal file
@ -0,0 +1,14 @@
|
||||
#ifndef XLSXTOMD_H
|
||||
#define XLSXTOMD_H
|
||||
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
#include <QtGlobal>
|
||||
|
||||
class XLSXToMD
|
||||
{
|
||||
public:
|
||||
static QString toMarkdown(const QString &xlsxFilePath);
|
||||
};
|
||||
|
||||
#endif // XLSXTOMD_H
|
Loading…
Reference in New Issue
Block a user