diff --git a/.circleci/continue_config.yml b/.circleci/continue_config.yml
index 712ccf40..4633345b 100644
--- a/.circleci/continue_config.yml
+++ b/.circleci/continue_config.yml
@@ -321,7 +321,7 @@ jobs:
libfreetype6 libgl1-mesa-dev libmysqlclient21 libnvidia-compute-550-server libodbc2 libpq5 libwayland-dev
libx11-6 libx11-xcb1 libxcb-cursor0 libxcb-glx0 libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-randr0
libxcb-render-util0 libxcb-shape0 libxcb-shm0 libxcb-sync1 libxcb-util1 libxcb-xfixes0 libxcb-xinerama0
- libxcb-xkb1 libxcb1 libxext6 libxfixes3 libxi6 libxkbcommon-x11-0 libxkbcommon0 libxrender1 patchelf
+ libxcb-xkb1 libxcb1 libxext6 libxfixes3 libxi6 libxkbcommon-dev libxkbcommon-x11-0 libxrender1 patchelf
python3 vulkan-sdk
)
sudo apt-get update
@@ -397,7 +397,7 @@ jobs:
libfreetype6 libgl1-mesa-dev libmysqlclient21 libnvidia-compute-550-server libodbc2 libpq5 libwayland-dev
libx11-6 libx11-xcb1 libxcb-cursor0 libxcb-glx0 libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-randr0
libxcb-render-util0 libxcb-shape0 libxcb-shm0 libxcb-sync1 libxcb-util1 libxcb-xfixes0 libxcb-xinerama0
- libxcb-xkb1 libxcb1 libxext6 libxfixes3 libxi6 libxkbcommon-x11-0 libxkbcommon0 libxrender1 patchelf
+ libxcb-xkb1 libxcb1 libxext6 libxfixes3 libxi6 libxkbcommon-dev libxkbcommon-x11-0 libxrender1 patchelf
python3 vulkan-sdk
)
sudo apt-get update
@@ -728,7 +728,7 @@ jobs:
libfreetype6 libgl1-mesa-dev libmysqlclient21 libnvidia-compute-550-server libodbc2 libpq5 libwayland-dev
libx11-6 libx11-xcb1 libxcb-cursor0 libxcb-glx0 libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-randr0
libxcb-render-util0 libxcb-shape0 libxcb-shm0 libxcb-sync1 libxcb-util1 libxcb-xfixes0 libxcb-xinerama0
- libxcb-xkb1 libxcb1 libxext6 libxfixes3 libxi6 libxkbcommon-x11-0 libxkbcommon0 libxrender1 python3
+ libxcb-xkb1 libxcb1 libxext6 libxfixes3 libxi6 libxkbcommon-dev libxkbcommon-x11-0 libxrender1 python3
vulkan-sdk
)
sudo apt-get update
diff --git a/.gitmodules b/.gitmodules
index 23528ed7..3be28f5a 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -14,3 +14,6 @@
[submodule "gpt4all-chat/deps/DuckX"]
path = gpt4all-chat/deps/DuckX
url = https://github.com/nomic-ai/DuckX.git
+[submodule "gpt4all-chat/deps/QXlsx"]
+ path = gpt4all-chat/deps/QXlsx
+ url = https://github.com/QtExcel/QXlsx.git
diff --git a/gpt4all-chat/CMakeLists.txt b/gpt4all-chat/CMakeLists.txt
index 9a144121..efd41459 100644
--- a/gpt4all-chat/CMakeLists.txt
+++ b/gpt4all-chat/CMakeLists.txt
@@ -154,6 +154,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}
)
@@ -190,6 +191,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
@@ -222,9 +225,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
@@ -242,7 +247,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
@@ -255,6 +262,7 @@ qt_add_qml_module(chat
icons/trash.svg
icons/twitter.svg
icons/up_down.svg
+ icons/webpage.svg
icons/you.svg
)
@@ -327,7 +335,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 duckx::duckx)
+ PRIVATE llmodel SingleApplication fmt::fmt duckx::duckx QXlsx)
# -- install --
diff --git a/gpt4all-chat/build_and_run.md b/gpt4all-chat/build_and_run.md
index 9aa2e328..c94d9266 100644
--- a/gpt4all-chat/build_and_run.md
+++ b/gpt4all-chat/build_and_run.md
@@ -21,12 +21,12 @@ sudo pacman -S --needed cmake gcc ninja qt6-5compat qt6-base qt6-declarative qt6
On Ubuntu 23.04, this looks like:
```
-sudo apt install cmake g++ libgl-dev libqt6core5compat6 ninja-build qml6-module-qt5compat-graphicaleffects qt6-base-dev qt6-declarative-dev qt6-httpserver-dev qt6-svg-dev qtcreator
+sudo apt install cmake g++ libgl-dev libqt6core5compat6 ninja-build qml6-module-qt5compat-graphicaleffects qt6-base-private-dev qt6-declarative-dev qt6-httpserver-dev qt6-svg-dev qtcreator
```
On Fedora 39, this looks like:
```
-sudo dnf install cmake gcc-c++ ninja-build qt-creator qt5-qtgraphicaleffects qt6-qt5compat qt6-qtbase-devel qt6-qtdeclarative-devel qt6-qthttpserver-devel qt6-qtsvg-devel
+sudo dnf install cmake gcc-c++ ninja-build qt-creator qt5-qtgraphicaleffects qt6-qt5compat qt6-qtbase-private-devel qt6-qtdeclarative-devel qt6-qthttpserver-devel qt6-qtsvg-devel
```
## Download Qt
diff --git a/gpt4all-chat/deps/CMakeLists.txt b/gpt4all-chat/deps/CMakeLists.txt
index 14e9c909..d082c38b 100644
--- a/gpt4all-chat/deps/CMakeLists.txt
+++ b/gpt4all-chat/deps/CMakeLists.txt
@@ -8,3 +8,6 @@ add_subdirectory(SingleApplication)
set(DUCKX_INSTALL OFF)
add_subdirectory(DuckX)
+
+set(QT_VERSION_MAJOR 6)
+add_subdirectory(QXlsx/QXlsx)
diff --git a/gpt4all-chat/deps/QXlsx b/gpt4all-chat/deps/QXlsx
new file mode 160000
index 00000000..fda6b806
--- /dev/null
+++ b/gpt4all-chat/deps/QXlsx
@@ -0,0 +1 @@
+Subproject commit fda6b806e2ceebd81c01cdded07ae84c94f5879c
diff --git a/gpt4all-chat/icons/file-doc.svg b/gpt4all-chat/icons/file-doc.svg
new file mode 100644
index 00000000..590be9bc
--- /dev/null
+++ b/gpt4all-chat/icons/file-doc.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/gpt4all-chat/icons/file-xls.svg b/gpt4all-chat/icons/file-xls.svg
new file mode 100644
index 00000000..b5dc1a97
--- /dev/null
+++ b/gpt4all-chat/icons/file-xls.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/gpt4all-chat/icons/paperclip.svg b/gpt4all-chat/icons/paperclip.svg
new file mode 100644
index 00000000..0c7feb23
--- /dev/null
+++ b/gpt4all-chat/icons/paperclip.svg
@@ -0,0 +1,45 @@
+
+
diff --git a/gpt4all-chat/icons/plus_circle.svg b/gpt4all-chat/icons/plus_circle.svg
new file mode 100644
index 00000000..0365c1a4
--- /dev/null
+++ b/gpt4all-chat/icons/plus_circle.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/gpt4all-chat/icons/webpage.svg b/gpt4all-chat/icons/webpage.svg
new file mode 100644
index 00000000..2a93f543
--- /dev/null
+++ b/gpt4all-chat/icons/webpage.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/gpt4all-chat/qml/AddCollectionView.qml b/gpt4all-chat/qml/AddCollectionView.qml
index 35947b52..ed2ece84 100644
--- a/gpt4all-chat/qml/AddCollectionView.qml
+++ b/gpt4all-chat/qml/AddCollectionView.qml
@@ -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
})
}
diff --git a/gpt4all-chat/qml/ApplicationSettings.qml b/gpt4all-chat/qml/ApplicationSettings.qml
index 0fefe476..f6902192 100644
--- a/gpt4all-chat/qml/ApplicationSettings.qml
+++ b/gpt4all-chat/qml/ApplicationSettings.qml
@@ -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
})
}
diff --git a/gpt4all-chat/qml/ChatView.qml b/gpt4all-chat/qml/ChatView.qml
index 3bc77219..e6f72e58 100644
--- a/gpt4all-chat/qml/ChatView.qml
+++ b/gpt4all-chat/qml/ChatView.qml
@@ -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,65 @@ Rectangle {
Layout.row: 1
Layout.column: 1
Layout.fillWidth: true
+ spacing: 20
+ Flow {
+ id: attachedUrlsFlow
+ Layout.fillWidth: true
+ spacing: 10
+ visible: promptAttachments.length !== 0
+ Repeater {
+ model: promptAttachments
+
+ delegate: Rectangle {
+ width: 350
+ 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.fontSizeMedium
+ font.bold: true
+ wrapMode: Text.WrapAnywhere
+ }
+ }
+ }
+ }
+ }
+
TextArea {
id: myTextArea
Layout.fillWidth: true
@@ -1434,17 +1494,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.newPromptResponsePair(followup)
}
}
Item {
@@ -1708,7 +1758,7 @@ Rectangle {
chatModel.updateThumbsUpState(responseIndex, false)
chatModel.updateThumbsDownState(responseIndex, false)
chatModel.updateNewResponse(responseIndex, "")
- currentChat.prompt(promptElement.value)
+ currentChat.prompt(promptElement.promptPlusAttachments)
}
ToolTip.visible: regenerateButton.hovered
ToolTip.text: qsTr("Redo last chat response")
@@ -1827,170 +1877,339 @@ 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: 10
+ columnSpacing: 0
+ Flow {
+ id: attachmentsFlow
+ visible: attachmentModel.count
+ Layout.row: 0
+ Layout.column: 1
+ Layout.topMargin: 15
+ Layout.leftMargin: 5
+ 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: 350
+ 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.fontSizeMedium
+ 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.rightMargin: 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.newPromptResponsePair(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();
+ })
+ }
}
}
}
diff --git a/gpt4all-chat/qml/MyFileDialog.qml b/gpt4all-chat/qml/MyFileDialog.qml
new file mode 100644
index 00000000..a910b579
--- /dev/null
+++ b/gpt4all-chat/qml/MyFileDialog.qml
@@ -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();
+ }
+}
diff --git a/gpt4all-chat/qml/MyFolderDialog.qml b/gpt4all-chat/qml/MyFolderDialog.qml
new file mode 100644
index 00000000..885d47bf
--- /dev/null
+++ b/gpt4all-chat/qml/MyFolderDialog.qml
@@ -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();
+ }
+}
diff --git a/gpt4all-chat/qml/MyMenu.qml b/gpt4all-chat/qml/MyMenu.qml
index 5e9ca86e..ae103721 100644
--- a/gpt4all-chat/qml/MyMenu.qml
+++ b/gpt4all-chat/qml/MyMenu.qml
@@ -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
diff --git a/gpt4all-chat/qml/MyMenuItem.qml b/gpt4all-chat/qml/MyMenuItem.qml
index eff1fb4e..9af06a99 100644
--- a/gpt4all-chat/qml/MyMenuItem.qml
+++ b/gpt4all-chat/qml/MyMenuItem.qml
@@ -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
+ }
}
}
diff --git a/gpt4all-chat/qml/MySettingsStack.qml b/gpt4all-chat/qml/MySettingsStack.qml
index bc95bca4..9f0273ef 100644
--- a/gpt4all-chat/qml/MySettingsStack.qml
+++ b/gpt4all-chat/qml/MySettingsStack.qml
@@ -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;
}
}
}
diff --git a/gpt4all-chat/qml/MySettingsTab.qml b/gpt4all-chat/qml/MySettingsTab.qml
index a6ff88e6..98ed402e 100644
--- a/gpt4all-chat/qml/MySettingsTab.qml
+++ b/gpt4all-chat/qml/MySettingsTab.qml
@@ -9,7 +9,6 @@ Item {
property string title: ""
property Item contentItem: null
property bool showRestoreDefaultsButton: true
- property var openFolderDialog
signal restoreDefaultsClicked
onContentItemChanged: function() {
diff --git a/gpt4all-chat/qml/Theme.qml b/gpt4all-chat/qml/Theme.qml
index 670c6664..245a4473 100644
--- a/gpt4all-chat/qml/Theme.qml
+++ b/gpt4all-chat/qml/Theme.qml
@@ -177,6 +177,17 @@ QtObject {
}
}
+ property color attachmentBackground: {
+ switch (MySettings.chatTheme) {
+ case MySettingsEnums.ChatTheme.LegacyDark:
+ return blue900
+ case MySettingsEnums.ChatTheme.Dark:
+ return darkgray200
+ default:
+ return gray0
+ }
+ }
+
property color disabledControlBackground: {
switch (MySettings.chatTheme) {
case MySettingsEnums.ChatTheme.LegacyDark:
diff --git a/gpt4all-chat/src/chat.cpp b/gpt4all-chat/src/chat.cpp
index fb5e7763..5eb18473 100644
--- a/gpt4all-chat/src/chat.cpp
+++ b/gpt4all-chat/src/chat.cpp
@@ -5,6 +5,7 @@
#include "network.h"
#include "server.h"
+#include
#include
#include
#include
@@ -122,6 +123,42 @@ void Chat::resetResponseState()
emit responseStateChanged();
}
+void Chat::newPromptResponsePair(const QString &prompt, const QList &attachedUrls)
+{
+ QStringList attachedContexts;
+ QList attachments;
+ 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
+
+ PromptAttachment attached;
+ attached.url = url;
+
+ QFile file(localFilePath);
+ if (file.open(QIODevice::ReadOnly)) {
+ attached.content = file.readAll();
+ file.close();
+ } else {
+ qWarning() << "ERROR: Failed to open the attachment:" << localFilePath;
+ continue;
+ }
+
+ attachments << attached;
+ attachedContexts << attached.processedContent();
+ }
+
+ QString promptPlusAttached = prompt;
+ if (!attachedContexts.isEmpty())
+ promptPlusAttached = attachedContexts.join("\n\n") + "\n\n" + prompt;
+
+ newPromptResponsePairInternal(prompt, attachments);
+ emit resetResponseRequested();
+
+ this->prompt(promptPlusAttached);
+}
+
void Chat::prompt(const QString &prompt)
{
resetResponseState();
@@ -232,23 +269,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 &attachments)
{
- resetResponseState();
- m_chatModel->updateCurrentResponse(m_chatModel->count() - 1, false);
- // the prompt is passed as the prompt item's value and the response item's prompt
- m_chatModel->appendPrompt("Prompt: ", prompt);
- m_chatModel->appendResponse("Response: ");
- emit resetResponseRequested();
+ newPromptResponsePairInternal(prompt, attachments);
}
-// 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 &attachments)
{
resetResponseState();
m_chatModel->updateCurrentResponse(m_chatModel->count() - 1, false);
- // the prompt is passed as the prompt item's value and the response item's prompt
- m_chatModel->appendPrompt("Prompt: ", prompt);
+ m_chatModel->appendPrompt("Prompt: ", prompt, attachments);
m_chatModel->appendResponse("Response: ");
}
diff --git a/gpt4all-chat/src/chat.h b/gpt4all-chat/src/chat.h
index 21f794de..b7caeb0c 100644
--- a/gpt4all-chat/src/chat.h
+++ b/gpt4all-chat/src/chat.h
@@ -77,10 +77,10 @@ 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 newPromptResponsePair(const QString &prompt, const QList &attachedUrls = {});
Q_INVOKABLE void prompt(const QString &prompt);
Q_INVOKABLE void regenerateResponse();
Q_INVOKABLE void stopGenerating();
- Q_INVOKABLE void newPromptResponsePair(const QString &prompt);
QList databaseResults() const { return m_databaseResults; }
@@ -125,7 +125,7 @@ public:
QList generatedQuestions() const { return m_generatedQuestions; }
public Q_SLOTS:
- void serverNewPromptResponsePair(const QString &prompt);
+ void serverNewPromptResponsePair(const QString &prompt, const QList &attachments = {});
Q_SIGNALS:
void idChanged(const QString &id);
@@ -174,6 +174,9 @@ private Q_SLOTS:
void handleModelInfoChanged(const ModelInfo &modelInfo);
void handleTrySwitchContextOfLoadedModelCompleted(int value);
+private:
+ void newPromptResponsePairInternal(const QString &prompt, const QList &attachments);
+
private:
QString m_id;
QString m_name;
diff --git a/gpt4all-chat/src/chatllm.cpp b/gpt4all-chat/src/chatllm.cpp
index fece36fb..91fabe89 100644
--- a/gpt4all-chat/src/chatllm.cpp
+++ b/gpt4all-chat/src/chatllm.cpp
@@ -1333,7 +1333,7 @@ void ChatLLM::processRestoreStateFromText()
// FIXME(jared): this doesn't work well with the "regenerate" button since we are not incrementing
// m_promptTokens or m_promptResponseTokens
m_llModelInfo.model->prompt(
- prompt.value.toStdString(), promptTemplate.toStdString(),
+ prompt.promptPlusAttachments().toStdString(), promptTemplate.toStdString(),
promptFunc, /*responseFunc*/ [](auto &&...) { return true; },
/*allowContextShift*/ true,
m_ctx,
diff --git a/gpt4all-chat/src/chatmodel.h b/gpt4all-chat/src/chatmodel.h
index d971ad26..6ab3ac8d 100644
--- a/gpt4all-chat/src/chatmodel.h
+++ b/gpt4all-chat/src/chatmodel.h
@@ -2,8 +2,10 @@
#define CHATMODEL_H
#include "database.h"
+#include "xlsxtomd.h"
#include
+#include
#include
#include
#include
@@ -16,6 +18,40 @@
#include
#include
+struct PromptAttachment {
+ Q_GADGET
+ Q_PROPERTY(QUrl url MEMBER url)
+ Q_PROPERTY(QByteArray content MEMBER content)
+ Q_PROPERTY(QString file READ file)
+ Q_PROPERTY(QString processedContent READ processedContent)
+
+public:
+ QUrl url;
+ QByteArray content;
+
+ QString file() const
+ {
+ if (!url.isLocalFile())
+ return QString();
+ const QString localFilePath = url.toLocalFile();
+ const QFileInfo info(localFilePath);
+ return info.fileName();
+ }
+
+ QString processedContent() const
+ {
+ QBuffer buffer;
+ buffer.setData(content);
+ buffer.open(QIODevice::ReadOnly);
+ const QString md = XLSXToMD::toMarkdown(&buffer);
+ buffer.close();
+ return md;
+ }
+
+ bool operator==(const PromptAttachment &other) const { return url == other.url; }
+};
+Q_DECLARE_METATYPE(PromptAttachment)
+
struct ChatItem
{
Q_GADGET
@@ -29,8 +65,22 @@ struct ChatItem
Q_PROPERTY(bool thumbsDownState MEMBER thumbsDownState)
Q_PROPERTY(QList sources MEMBER sources)
Q_PROPERTY(QList consolidatedSources MEMBER consolidatedSources)
+ Q_PROPERTY(QList promptAttachments MEMBER promptAttachments);
+ Q_PROPERTY(QString promptPlusAttachments READ promptPlusAttachments);
public:
+ QString promptPlusAttachments() const
+ {
+ QStringList attachedContexts;
+ for (auto attached : promptAttachments)
+ attachedContexts << attached.processedContent();
+
+ QString promptPlus = value;
+ if (!attachedContexts.isEmpty())
+ promptPlus = attachedContexts.join("\n\n") + "\n\n" + value;
+ return promptPlus;
+ }
+
// TODO: Maybe we should include the model name here as well as timestamp?
int id = 0;
QString name;
@@ -38,6 +88,7 @@ public:
QString newResponse;
QList sources;
QList consolidatedSources;
+ QList promptAttachments;
bool currentResponse = false;
bool stopped = false;
bool thumbsUpState = false;
@@ -65,7 +116,8 @@ public:
ThumbsUpStateRole,
ThumbsDownStateRole,
SourcesRole,
- ConsolidatedSourcesRole
+ ConsolidatedSourcesRole,
+ PromptAttachmentsRole
};
int rowCount(const QModelIndex &parent = QModelIndex()) const override
@@ -103,6 +155,8 @@ public:
return QVariant::fromValue(item.sources);
case ConsolidatedSourcesRole:
return QVariant::fromValue(item.consolidatedSources);
+ case PromptAttachmentsRole:
+ return QVariant::fromValue(item.promptAttachments);
}
return QVariant();
@@ -121,14 +175,17 @@ 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 &attachments)
{
ChatItem item;
item.name = name;
item.value = value;
+ item.promptAttachments << attachments;
+
m_mutex.lock();
const int count = m_chatItems.count();
m_mutex.unlock();
@@ -380,6 +437,14 @@ public:
stream << references.join("\n");
stream << referencesContext;
}
+ if (version >= 10) {
+ stream << c.promptAttachments.size();
+ for (const PromptAttachment &a : c.promptAttachments) {
+ Q_ASSERT(!a.url.isEmpty());
+ stream << a.url;
+ stream << a.content;
+ }
+ }
}
return stream.status() == QDataStream::Ok;
}
@@ -423,7 +488,7 @@ public:
}
c.sources = sources;
c.consolidatedSources = consolidateSources(sources);
- }else if (version > 2) {
+ } else if (version > 2) {
QString references;
QList referencesContext;
stream >> references;
@@ -507,6 +572,18 @@ public:
c.consolidatedSources = consolidateSources(sources);
}
}
+ if (version >= 10) {
+ qsizetype count;
+ stream >> count;
+ QList attachments;
+ for (int i = 0; i < count; ++i) {
+ PromptAttachment a;
+ stream >> a.url;
+ stream >> a.content;
+ attachments.append(a);
+ }
+ c.promptAttachments = attachments;
+ }
m_mutex.lock();
const int count = m_chatItems.size();
m_mutex.unlock();
diff --git a/gpt4all-chat/src/server.h b/gpt4all-chat/src/server.h
index a1d46264..a5447d86 100644
--- a/gpt4all-chat/src/server.h
+++ b/gpt4all-chat/src/server.h
@@ -2,6 +2,7 @@
#define SERVER_H
#include "chatllm.h"
+#include "chatmodel.h"
#include "database.h"
#include
@@ -32,7 +33,7 @@ public Q_SLOTS:
void start();
Q_SIGNALS:
- void requestServerNewPromptResponsePair(const QString &prompt);
+ void requestServerNewPromptResponsePair(const QString &prompt, const QList &attachments = {});
private:
auto handleCompletionRequest(const CompletionRequest &request) -> std::pair>;
diff --git a/gpt4all-chat/src/xlsxtomd.cpp b/gpt4all-chat/src/xlsxtomd.cpp
new file mode 100644
index 00000000..763f14bd
--- /dev/null
+++ b/gpt4all-chat/src/xlsxtomd.cpp
@@ -0,0 +1,167 @@
+#include "xlsxtomd.h"
+
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include
+
+using namespace Qt::Literals::StringLiterals;
+
+
+static 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;
+}
+
+static QString getCellValue(QXlsx::Worksheet *sheet, int row, int col)
+{
+ if (!sheet)
+ return QString();
+
+ // Attempt to retrieve the cell directly
+ std::shared_ptr 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(QIODevice *xlsxDevice)
+{
+ // Load the Excel document
+ QXlsx::Document xlsx(xlsxDevice);
+ if (!xlsx.load()) {
+ qCritical() << "Failed to load the Excel from device";
+ 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(xlsx.sheet(sheetName));
+ if (!sheet) {
+ qWarning() << "Failed to load sheet:" << sheetName;
+ continue;
+ }
+
+ markdown += u"## %1\n\n"_s.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;
+}
diff --git a/gpt4all-chat/src/xlsxtomd.h b/gpt4all-chat/src/xlsxtomd.h
new file mode 100644
index 00000000..466903c4
--- /dev/null
+++ b/gpt4all-chat/src/xlsxtomd.h
@@ -0,0 +1,13 @@
+#ifndef XLSXTOMD_H
+#define XLSXTOMD_H
+
+class QIODevice;
+class QString;
+
+class XLSXToMD
+{
+public:
+ static QString toMarkdown(QIODevice *xlsxDevice);
+};
+
+#endif // XLSXTOMD_H