From db443f20902c90a9250f05eb9cfad01f2331c800 Mon Sep 17 00:00:00 2001
From: AT <manyoso@users.noreply.github.com>
Date: Tue, 1 Oct 2024 21:17:49 -0400
Subject: [PATCH] Support attaching an Excel spreadsheet to a chat message
 (#3007)

Signed-off-by: Adam Treat <treat.adam@gmail.com>
Signed-off-by: Jared Van Bortel <jared@nomic.ai>
Co-authored-by: Jared Van Bortel <jared@nomic.ai>
---
 .circleci/continue_config.yml            |   6 +-
 .gitmodules                              |   3 +
 gpt4all-chat/CMakeLists.txt              |  10 +-
 gpt4all-chat/build_and_run.md            |   4 +-
 gpt4all-chat/deps/CMakeLists.txt         |   3 +
 gpt4all-chat/deps/QXlsx                  |   1 +
 gpt4all-chat/icons/file-doc.svg          |   1 +
 gpt4all-chat/icons/file-xls.svg          |   1 +
 gpt4all-chat/icons/paperclip.svg         |  45 ++
 gpt4all-chat/icons/plus_circle.svg       |   1 +
 gpt4all-chat/icons/webpage.svg           |   1 +
 gpt4all-chat/qml/AddCollectionView.qml   |  11 +-
 gpt4all-chat/qml/ApplicationSettings.qml |   5 +-
 gpt4all-chat/qml/ChatView.qml            | 517 ++++++++++++++++-------
 gpt4all-chat/qml/MyFileDialog.qml        |  19 +
 gpt4all-chat/qml/MyFolderDialog.qml      |  14 +
 gpt4all-chat/qml/MyMenu.qml              |  22 +-
 gpt4all-chat/qml/MyMenuItem.qml          |  44 +-
 gpt4all-chat/qml/MySettingsStack.qml     |  12 -
 gpt4all-chat/qml/MySettingsTab.qml       |   1 -
 gpt4all-chat/qml/Theme.qml               |  11 +
 gpt4all-chat/src/chat.cpp                |  53 ++-
 gpt4all-chat/src/chat.h                  |   7 +-
 gpt4all-chat/src/chatllm.cpp             |   2 +-
 gpt4all-chat/src/chatmodel.h             |  83 +++-
 gpt4all-chat/src/server.h                |   3 +-
 gpt4all-chat/src/xlsxtomd.cpp            | 167 ++++++++
 gpt4all-chat/src/xlsxtomd.h              |  13 +
 28 files changed, 855 insertions(+), 205 deletions(-)
 create mode 160000 gpt4all-chat/deps/QXlsx
 create mode 100644 gpt4all-chat/icons/file-doc.svg
 create mode 100644 gpt4all-chat/icons/file-xls.svg
 create mode 100644 gpt4all-chat/icons/paperclip.svg
 create mode 100644 gpt4all-chat/icons/plus_circle.svg
 create mode 100644 gpt4all-chat/icons/webpage.svg
 create mode 100644 gpt4all-chat/qml/MyFileDialog.qml
 create mode 100644 gpt4all-chat/qml/MyFolderDialog.qml
 create mode 100644 gpt4all-chat/src/xlsxtomd.cpp
 create mode 100644 gpt4all-chat/src/xlsxtomd.h

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 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><rect width="256" height="256" fill="none"/><path d="M36,152v56H52a28,28,0,0,0,0-56Z" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><path d="M216,200.87A22.12,22.12,0,0,1,200,208c-13.26,0-24-12.54-24-28s10.74-28,24-28a22.12,22.12,0,0,1,16,7.13" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><path d="M48,112V40a8,8,0,0,1,8-8h96l56,56v24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><polyline points="152 32 152 88 208 88" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><ellipse cx="128" cy="180" rx="24" ry="28" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/></svg>
\ 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 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><rect width="256" height="256" fill="none"/><polyline points="148 208 120 208 120 152" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><path d="M48,112V40a8,8,0,0,1,8-8h96l56,56v24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><polyline points="152 32 152 88 208 88" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><line x1="48" y1="152" x2="88" y2="208" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><line x1="88" y1="152" x2="48" y2="208" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><path d="M203.9,153.6s-29.43-7.78-31.8,11,38.43,10.12,35.78,30.72c-2.47,19.16-31.78,11-31.78,11" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/></svg>
\ 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 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+   viewBox="0 0 256 256"
+   version="1.1"
+   id="svg6"
+   sodipodi:docname="paperclip-horizontal.svg"
+   inkscape:version="1.1.2 (0a00cf5339, 2022-02-04)"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:svg="http://www.w3.org/2000/svg">
+  <defs
+     id="defs10" />
+  <sodipodi:namedview
+     id="namedview8"
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     inkscape:pageshadow="2"
+     inkscape:pageopacity="0.0"
+     inkscape:pagecheckerboard="0"
+     showgrid="false"
+     inkscape:zoom="4.421875"
+     inkscape:cx="127.88693"
+     inkscape:cy="127.88693"
+     inkscape:window-width="2560"
+     inkscape:window-height="1495"
+     inkscape:window-x="0"
+     inkscape:window-y="0"
+     inkscape:window-maximized="1"
+     inkscape:current-layer="svg6" />
+  <rect
+     width="256"
+     height="256"
+     fill="none"
+     id="rect2" />
+  <path
+     d="m 144,80 v 112 a -16,16 0 0 1 -32,0 V 48 a -32,32 0 0 1 64,0 v 144 a -48,48 0 0 1 -96,0 V 80"
+     fill="none"
+     stroke="currentColor"
+     stroke-linecap="round"
+     stroke-linejoin="round"
+     stroke-width="16"
+     id="path4" />
+</svg>
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 @@
+<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>
\ 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 @@
+<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>
\ 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 <QBuffer>
 #include <QDataStream>
 #include <QDebug>
 #include <QLatin1String>
@@ -122,6 +123,42 @@ void Chat::resetResponseState()
     emit responseStateChanged();
 }
 
+void Chat::newPromptResponsePair(const QString &prompt, const QList<QUrl> &attachedUrls)
+{
+    QStringList attachedContexts;
+    QList<PromptAttachment> 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<PromptAttachment> &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<PromptAttachment> &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<QUrl> &attachedUrls = {});
     Q_INVOKABLE void prompt(const QString &prompt);
     Q_INVOKABLE void regenerateResponse();
     Q_INVOKABLE void stopGenerating();
-    Q_INVOKABLE void newPromptResponsePair(const QString &prompt);
 
     QList<ResultInfo> databaseResults() const { return m_databaseResults; }
 
@@ -125,7 +125,7 @@ public:
     QList<QString> generatedQuestions() const { return m_generatedQuestions; }
 
 public Q_SLOTS:
-    void serverNewPromptResponsePair(const QString &prompt);
+    void serverNewPromptResponsePair(const QString &prompt, const QList<PromptAttachment> &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<PromptAttachment> &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 <QAbstractListModel>
+#include <QBuffer>
 #include <QByteArray>
 #include <QDataStream>
 #include <QHash>
@@ -16,6 +18,40 @@
 #include <Qt>
 #include <QtGlobal>
 
+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<ResultInfo> sources MEMBER sources)
     Q_PROPERTY(QList<ResultInfo> consolidatedSources MEMBER consolidatedSources)
+    Q_PROPERTY(QList<PromptAttachment> 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<ResultInfo> sources;
     QList<ResultInfo> consolidatedSources;
+    QList<PromptAttachment> 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<PromptAttachment> &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<QString> referencesContext;
                 stream >> references;
@@ -507,6 +572,18 @@ public:
                     c.consolidatedSources = consolidateSources(sources);
                 }
             }
+            if (version >= 10) {
+                qsizetype count;
+                stream >> count;
+                QList<PromptAttachment> 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 <QHttpServer>
@@ -32,7 +33,7 @@ public Q_SLOTS:
     void start();
 
 Q_SIGNALS:
-    void requestServerNewPromptResponsePair(const QString &prompt);
+    void requestServerNewPromptResponsePair(const QString &prompt, const QList<PromptAttachment> &attachments = {});
 
 private:
     auto handleCompletionRequest(const CompletionRequest &request) -> std::pair<QHttpServerResponse, std::optional<QJsonObject>>;
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 <xlsxabstractsheet.h>
+#include <xlsxcell.h>
+#include <xlsxcellrange.h>
+#include <xlsxdocument.h>
+#include <xlsxformat.h>
+#include <xlsxworksheet.h>
+
+#include <QDateTime>
+#include <QDebug>
+#include <QList>
+#include <QString>
+#include <QStringList>
+#include <QVariant>
+#include <QtGlobal>
+#include <QtLogging>
+
+#include <memory>
+
+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<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(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<QXlsx::Worksheet *>(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