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>
This commit is contained in:
AT
2024-10-01 21:17:49 -04:00
committed by GitHub
parent c11b67dfcb
commit db443f2090
28 changed files with 855 additions and 205 deletions

View File

@@ -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
})
}

View File

@@ -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
})
}

View File

@@ -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();
})
}
}
}
}

View File

@@ -0,0 +1,19 @@
import QtCore
import QtQuick
import QtQuick.Dialogs
FileDialog {
id: fileDialog
title: qsTr("Please choose a file")
property var acceptedConnection: null
function openFileDialog(currentFolder, onAccepted) {
fileDialog.currentFolder = currentFolder;
if (acceptedConnection !== null) {
fileDialog.accepted.disconnect(acceptedConnection);
}
acceptedConnection = function() { onAccepted(fileDialog.selectedFile); };
fileDialog.accepted.connect(acceptedConnection);
fileDialog.open();
}
}

View File

@@ -0,0 +1,14 @@
import QtCore
import QtQuick
import QtQuick.Dialogs
FolderDialog {
id: folderDialog
title: qsTr("Please choose a directory")
function openFolderDialog(currentFolder, onAccepted) {
folderDialog.currentFolder = currentFolder;
folderDialog.accepted.connect(function() { onAccepted(folderDialog.selectedFolder); });
folderDialog.open();
}
}

View File

@@ -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

View File

@@ -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
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -9,7 +9,6 @@ Item {
property string title: ""
property Item contentItem: null
property bool showRestoreDefaultsButton: true
property var openFolderDialog
signal restoreDefaultsClicked
onContentItemChanged: function() {

View File

@@ -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: