1
0
mirror of https://github.com/nomic-ai/gpt4all.git synced 2025-04-29 20:24:20 +00:00
gpt4all/gpt4all-chat/qml/ChatView.qml
AT 8790586e57
Server view fix ()
Signed-off-by: Adam Treat <treat.adam@gmail.com>
2025-01-24 10:29:28 -05:00

1420 lines
63 KiB
QML

import Qt5Compat.GraphicalEffects
import QtCore
import QtQuick
import QtQuick.Controls
import QtQuick.Controls.Basic
import QtQuick.Dialogs
import QtQuick.Layouts
import chatlistmodel
import download
import gpt4all
import llm
import localdocs
import modellist
import mysettings
import network
Rectangle {
id: window
Theme {
id: theme
}
property var currentChat: ChatListModel.currentChat
property var chatModel: currentChat.chatModel
property var currentModelInfo: currentChat && currentChat.modelInfo
property var currentModelId: null
onCurrentModelInfoChanged: {
const newId = currentModelInfo && currentModelInfo.id;
if (currentModelId !== newId) { currentModelId = newId; }
}
signal addCollectionViewRequested()
signal addModelViewRequested()
color: theme.viewBackground
Connections {
target: currentChat
// FIXME: https://github.com/nomic-ai/gpt4all/issues/3334
// function onResponseInProgressChanged() {
// if (MySettings.networkIsActive && !currentChat.responseInProgress)
// Network.sendConversation(currentChat.id, getConversationJson());
// }
function onModelLoadingErrorChanged() {
if (currentChat.modelLoadingError !== "")
modelLoadingErrorPopup.open()
}
function onModelLoadingWarning(warning) {
modelLoadingWarningPopup.open_(warning)
}
}
function currentModelName() {
return ModelList.modelInfo(currentChat.modelInfo.id).name;
}
function currentModelInstalled() {
return currentModelName() !== "" && ModelList.modelInfo(currentChat.modelInfo.id).installed;
}
PopupDialog {
id: modelLoadingErrorPopup
anchors.centerIn: parent
shouldTimeOut: false
text: qsTr("<h3>Encountered an error loading model:</h3><br>"
+ "<i>\"%1\"</i>"
+ "<br><br>Model loading failures can happen for a variety of reasons, but the most common "
+ "causes include a bad file format, an incomplete or corrupted download, the wrong file "
+ "type, not enough system RAM or an incompatible model type. Here are some suggestions for resolving the problem:"
+ "<br><ul>"
+ "<li>Ensure the model file has a compatible format and type"
+ "<li>Check the model file is complete in the download folder"
+ "<li>You can find the download folder in the settings dialog"
+ "<li>If you've sideloaded the model ensure the file is not corrupt by checking md5sum"
+ "<li>Read more about what models are supported in our <a href=\"https://docs.gpt4all.io/\">documentation</a> for the gui"
+ "<li>Check out our <a href=\"https://discord.gg/4M2QFmTt2k\">discord channel</a> for help").arg(currentChat.modelLoadingError);
}
PopupDialog {
id: modelLoadingWarningPopup
property string message
anchors.centerIn: parent
shouldTimeOut: false
text: qsTr("<h3>Warning</h3><p>%1</p>").arg(message)
function open_(msg) { message = msg; open(); }
}
ConfirmationDialog {
id: switchModelDialog
property int index: -1
dialogTitle: qsTr("Erase conversation?")
description: qsTr("Changing the model will erase the current conversation.")
}
PopupDialog {
id: copyMessage
anchors.centerIn: parent
text: qsTr("Conversation copied to clipboard.")
font.pixelSize: theme.fontSizeLarge
}
PopupDialog {
id: copyCodeMessage
anchors.centerIn: parent
text: qsTr("Code copied to clipboard.")
font.pixelSize: theme.fontSizeLarge
}
ConfirmationDialog {
id: resetContextDialog
dialogTitle: qsTr("Erase conversation?")
description: qsTr("The entire chat will be erased.")
onAccepted: {
Network.trackChatEvent("reset_context", { "length": chatModel.count });
currentChat.reset();
}
}
// FIXME: https://github.com/nomic-ai/gpt4all/issues/3334
// function getConversation() {
// var conversation = "";
// for (var i = 0; i < chatModel.count; i++) {
// var item = chatModel.get(i)
// var string = item.name;
// var isResponse = item.name === "Response: "
// string += chatModel.get(i).value
// if (isResponse && item.stopped)
// string += " <stopped>"
// string += "\n"
// conversation += string
// }
// return conversation
// }
// FIXME: https://github.com/nomic-ai/gpt4all/issues/3334
// function getConversationJson() {
// var str = "{\"conversation\": [";
// for (var i = 0; i < chatModel.count; i++) {
// var item = chatModel.get(i)
// var isResponse = item.name === "Response: "
// str += "{\"content\": ";
// str += JSON.stringify(item.value)
// str += ", \"role\": \"" + (isResponse ? "assistant" : "user") + "\"";
// if (isResponse && item.thumbsUpState !== item.thumbsDownState)
// str += ", \"rating\": \"" + (item.thumbsUpState ? "positive" : "negative") + "\"";
// if (isResponse && item.newResponse !== "")
// str += ", \"edited_content\": " + JSON.stringify(item.newResponse);
// if (isResponse && item.stopped)
// str += ", \"stopped\": \"true\""
// if (!isResponse)
// str += "},"
// else
// str += ((i < chatModel.count - 1) ? "}," : "}")
// }
// return str + "]}"
// }
ChatDrawer {
id: chatDrawer
anchors.left: parent.left
anchors.top: parent.top
anchors.bottom: parent.bottom
width: Math.max(180, Math.min(600, 0.23 * window.width))
}
PopupDialog {
id: referenceContextDialog
anchors.centerIn: parent
shouldTimeOut: false
shouldShowBusy: false
modal: true
}
Item {
id: mainArea
anchors.left: chatDrawer.right
anchors.right: parent.right
anchors.top: parent.top
anchors.bottom: parent.bottom
state: "expanded"
states: [
State {
name: "expanded"
AnchorChanges {
target: mainArea
anchors.left: chatDrawer.right
}
},
State {
name: "collapsed"
AnchorChanges {
target: mainArea
anchors.left: parent.left
}
}
]
function toggleLeftPanel() {
if (mainArea.state === "expanded") {
mainArea.state = "collapsed";
} else {
mainArea.state = "expanded";
}
}
transitions: Transition {
AnchorAnimation {
easing.type: Easing.InOutQuad
duration: 200
}
}
Rectangle {
id: header
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
height: 100
color: theme.conversationBackground
RowLayout {
id: comboLayout
height: 80
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
spacing: 0
Rectangle {
Layout.alignment: Qt.AlignLeft
Layout.leftMargin: 30
Layout.fillWidth: true
color: "transparent"
Layout.preferredHeight: childrenRect.height
MyToolButton {
id: drawerButton
anchors.left: parent.left
backgroundColor: theme.iconBackgroundLight
width: 40
height: 40
imageWidth: 40
imageHeight: 40
padding: 15
source: mainArea.state === "expanded" ? "qrc:/gpt4all/icons/left_panel_open.svg" : "qrc:/gpt4all/icons/left_panel_closed.svg"
Accessible.role: Accessible.ButtonMenu
Accessible.name: qsTr("Chat panel")
Accessible.description: qsTr("Chat panel with options")
onClicked: {
mainArea.toggleLeftPanel()
}
}
}
ComboBox {
id: comboBox
Layout.alignment: Qt.AlignHCenter
Layout.fillHeight: true
Layout.preferredWidth: 550
Layout.leftMargin: {
// This function works in tandem with the preferredWidth and the layout to
// provide the maximum size combobox we can have at the smallest window width
// we allow with the largest font size we allow. It is unfortunately based
// upon a magic number that was produced through trial and error for something
// I don't fully understand.
return -Math.max(0, comboBox.width / 2 + collectionsButton.width + 110 /*magic*/ - comboLayout.width / 2);
}
enabled: !currentChat.isServer
&& !currentChat.trySwitchContextInProgress
&& !currentChat.isCurrentlyLoading
&& ModelList.selectableModels.count !== 0
model: ModelList.selectableModels
valueRole: "id"
textRole: "name"
function changeModel(index) {
currentChat.stopGenerating()
currentChat.reset();
currentChat.modelInfo = ModelList.modelInfo(comboBox.valueAt(index))
}
Connections {
target: switchModelDialog
function onAccepted() {
comboBox.changeModel(switchModelDialog.index)
}
}
background: Rectangle {
color: theme.mainComboBackground
radius: 10
ProgressBar {
id: modelProgress
anchors.bottom: parent.bottom
anchors.horizontalCenter: parent.horizontalCenter
width: contentRow.width + 20
visible: currentChat.isCurrentlyLoading
height: 10
value: currentChat.modelLoadingPercentage
background: Rectangle {
color: theme.progressBackground
radius: 10
}
contentItem: Item {
Rectangle {
anchors.bottom: parent.bottom
width: modelProgress.visualPosition * parent.width
height: 10
radius: 2
color: theme.progressForeground
}
}
}
}
contentItem: Item {
RowLayout {
id: contentRow
anchors.centerIn: parent
spacing: 0
Layout.maximumWidth: 550
RowLayout {
id: miniButtonsRow
clip: true
Layout.maximumWidth: 550
Behavior on Layout.preferredWidth {
NumberAnimation {
duration: 300
easing.type: Easing.InOutQuad
}
}
Layout.preferredWidth: {
if (!(comboBox.hovered || reloadButton.hovered || ejectButton.hovered))
return 0
return (reloadButton.visible ? reloadButton.width : 0) + (ejectButton.visible ? ejectButton.width : 0)
}
MyMiniButton {
id: reloadButton
Layout.alignment: Qt.AlignCenter
visible: currentChat.modelLoadingError === ""
&& !currentChat.trySwitchContextInProgress
&& !currentChat.isCurrentlyLoading
&& (currentChat.isModelLoaded || currentModelInstalled())
source: "qrc:/gpt4all/icons/regenerate.svg"
backgroundColor: theme.textColor
backgroundColorHovered: theme.styledTextColor
onClicked: {
if (currentChat.isModelLoaded)
currentChat.forceReloadModel();
else
currentChat.reloadModel();
}
ToolTip.text: qsTr("Reload the currently loaded model")
ToolTip.visible: hovered
}
MyMiniButton {
id: ejectButton
Layout.alignment: Qt.AlignCenter
visible: currentChat.isModelLoaded && !currentChat.isCurrentlyLoading
source: "qrc:/gpt4all/icons/eject.svg"
backgroundColor: theme.textColor
backgroundColorHovered: theme.styledTextColor
onClicked: {
currentChat.forceUnloadModel();
}
ToolTip.text: qsTr("Eject the currently loaded model")
ToolTip.visible: hovered
}
}
Text {
Layout.maximumWidth: 520
id: comboBoxText
leftPadding: 10
rightPadding: 10
text: {
if (ModelList.selectableModels.count === 0)
return qsTr("No model installed.")
if (currentChat.modelLoadingError !== "")
return qsTr("Model loading error.")
if (currentChat.trySwitchContextInProgress === 1)
return qsTr("Waiting for model...")
if (currentChat.trySwitchContextInProgress === 2)
return qsTr("Switching context...")
if (currentModelName() === "")
return qsTr("Choose a model...")
if (!currentModelInstalled())
return qsTr("Not found: %1").arg(currentModelName())
if (currentChat.modelLoadingPercentage === 0.0)
return qsTr("Reload \u00B7 %1").arg(currentModelName())
if (currentChat.isCurrentlyLoading)
return qsTr("Loading \u00B7 %1").arg(currentModelName())
return currentModelName()
}
font.pixelSize: theme.fontSizeLarger
color: theme.iconBackgroundLight
verticalAlignment: Text.AlignVCenter
horizontalAlignment: Text.AlignHCenter
elide: Text.ElideRight
}
Item {
Layout.minimumWidth: updown.width
Layout.minimumHeight: updown.height
Image {
id: updown
anchors.verticalCenter: parent.verticalCenter
sourceSize.width: comboBoxText.font.pixelSize
sourceSize.height: comboBoxText.font.pixelSize
mipmap: true
visible: false
source: "qrc:/gpt4all/icons/up_down.svg"
}
ColorOverlay {
anchors.fill: updown
source: updown
color: comboBoxText.color
}
}
}
}
delegate: ItemDelegate {
id: comboItemDelegate
width: comboItemPopup.width -20
contentItem: Text {
text: name
color: theme.textColor
font: comboBox.font
elide: Text.ElideRight
verticalAlignment: Text.AlignVCenter
}
background: Rectangle {
radius: 10
color: highlighted ? theme.menuHighlightColor : theme.menuBackgroundColor
}
highlighted: comboBox.highlightedIndex === index
}
indicator: Item {
}
popup: Popup {
id: comboItemPopup
y: comboBox.height - 1
width: comboBox.width
implicitHeight: Math.min(window.height - y, contentItem.implicitHeight + 20)
padding: 0
contentItem: Rectangle {
implicitWidth: comboBox.width
implicitHeight: comboItemPopupListView.implicitHeight
color: "transparent"
radius: 10
ScrollView {
anchors.fill: parent
anchors.margins: 10
clip: true
ScrollBar.vertical.policy: ScrollBar.AsNeeded
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
ListView {
id: comboItemPopupListView
implicitHeight: contentHeight
model: comboBox.popup.visible ? comboBox.delegateModel : null
currentIndex: comboBox.highlightedIndex
ScrollIndicator.vertical: ScrollIndicator { }
}
}
}
background: Rectangle {
border.color: theme.menuBorderColor
border.width: 1
color: theme.menuBackgroundColor
radius: 10
}
}
Accessible.name: currentModelName()
Accessible.description: qsTr("The top item is the current model")
onActivated: function (index) {
var newInfo = ModelList.modelInfo(comboBox.valueAt(index));
if (newInfo === currentChat.modelInfo) {
currentChat.reloadModel();
} else if (currentModelName() !== "" && chatModel.count !== 0) {
switchModelDialog.index = index;
switchModelDialog.open();
} else {
comboBox.changeModel(index);
}
}
}
Rectangle {
color: "transparent"
Layout.alignment: Qt.AlignRight
Layout.rightMargin: 30
Layout.fillWidth: true
Layout.preferredHeight: childrenRect.height
clip: true
MyButton {
id: collectionsButton
clip: true
anchors.right: parent.right
borderWidth: 0
backgroundColor: theme.collectionsButtonBackground
backgroundColorHovered: theme.collectionsButtonBackgroundHovered
backgroundRadius: 5
padding: 15
topPadding: 8
bottomPadding: 8
contentItem: RowLayout {
spacing: 10
Item {
visible: currentChat.collectionModel.count === 0
Layout.minimumWidth: collectionsImage.width
Layout.minimumHeight: collectionsImage.height
Image {
id: collectionsImage
anchors.verticalCenter: parent.verticalCenter
sourceSize.width: 24
sourceSize.height: 24
mipmap: true
visible: false
source: "qrc:/gpt4all/icons/db.svg"
}
ColorOverlay {
anchors.fill: collectionsImage
source: collectionsImage
color: theme.collectionsButtonForeground
}
}
MyBusyIndicator {
visible: currentChat.collectionModel.updatingCount !== 0
color: theme.collectionsButtonProgress
size: 24
Layout.minimumWidth: 24
Layout.minimumHeight: 24
Text {
anchors.centerIn: parent
text: currentChat.collectionModel.updatingCount
color: theme.collectionsButtonForeground
font.pixelSize: 14 // fixed regardless of theme
}
}
Rectangle {
visible: currentChat.collectionModel.count !== 0
radius: 6
color: theme.collectionsButtonForeground
Layout.minimumWidth: collectionsImage.width
Layout.minimumHeight: collectionsImage.height
Text {
anchors.centerIn: parent
text: currentChat.collectionModel.count
color: theme.collectionsButtonText
font.pixelSize: 14 // fixed regardless of theme
}
}
Text {
text: qsTr("LocalDocs")
color: theme.collectionsButtonForeground
font.pixelSize: theme.fontSizeLarge
}
}
fontPixelSize: theme.fontSizeLarge
background: Rectangle {
radius: collectionsButton.backgroundRadius
// TODO(jared): either use collectionsButton-specific theming, or don't - this is inconsistent
color: conversation.state === "expanded" ? (
collectionsButton.hovered ? theme.lightButtonBackgroundHovered : theme.lightButtonBackground
) : (
collectionsButton.hovered ? theme.lighterButtonBackground : theme.lighterButtonBackgroundHovered
)
}
Accessible.name: qsTr("Add documents")
Accessible.description: qsTr("add collections of documents to the chat")
onClicked: {
conversation.toggleRightPanel()
}
}
}
}
}
Rectangle {
id: conversationDivider
anchors.top: header.bottom
anchors.left: parent.left
anchors.right: parent.right
color: theme.conversationDivider
height: 1
}
CollectionsDrawer {
id: collectionsDrawer
anchors.right: parent.right
anchors.top: conversationDivider.bottom
anchors.bottom: parent.bottom
width: Math.max(180, Math.min(600, 0.23 * window.width))
color: theme.conversationBackground
onAddDocsClicked: {
addCollectionViewRequested()
}
}
Rectangle {
id: conversation
color: theme.conversationBackground
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
anchors.top: conversationDivider.bottom
state: "collapsed"
states: [
State {
name: "expanded"
AnchorChanges {
target: conversation
anchors.right: collectionsDrawer.left
}
},
State {
name: "collapsed"
AnchorChanges {
target: conversation
anchors.right: parent.right
}
}
]
function toggleRightPanel() {
if (conversation.state === "expanded") {
conversation.state = "collapsed";
} else {
conversation.state = "expanded";
}
}
transitions: Transition {
AnchorAnimation {
easing.type: Easing.InOutQuad
duration: 300
}
}
ScrollView {
id: scrollView
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
anchors.bottom: !currentChat.isServer ? textInputView.top : parent.bottom
anchors.bottomMargin: !currentChat.isServer ? 30 : 0
ScrollBar.vertical.policy: ScrollBar.AlwaysOff
Rectangle {
anchors.fill: parent
color: currentChat.isServer ? theme.black : theme.conversationBackground
Rectangle {
id: homePage
color: "transparent"
anchors.fill: parent
z: 200
visible: !currentChat.isModelLoaded && (ModelList.selectableModels.count === 0 || currentModelName() === "") && !currentChat.isServer
ColumnLayout {
visible: ModelList.selectableModels.count !== 0
id: modelInstalledLabel
anchors.centerIn: parent
spacing: 0
Rectangle {
Layout.alignment: Qt.AlignCenter
Layout.preferredWidth: image.width
Layout.preferredHeight: image.height
color: "transparent"
Image {
id: image
anchors.centerIn: parent
sourceSize.width: 160
sourceSize.height: 110
fillMode: Image.PreserveAspectFit
mipmap: true
visible: false
source: "qrc:/gpt4all/icons/nomic_logo.svg"
}
ColorOverlay {
anchors.fill: image
source: image
color: theme.containerBackground
}
}
}
MyButton {
id: loadDefaultModelButton
visible: ModelList.selectableModels.count !== 0
anchors.top: modelInstalledLabel.bottom
anchors.topMargin: 50
anchors.horizontalCenter: modelInstalledLabel.horizontalCenter
rightPadding: 60
leftPadding: 60
property string defaultModel: ""
property string defaultModelName: ""
function updateDefaultModel() {
var i = comboBox.find(MySettings.userDefaultModel)
if (i !== -1) {
defaultModel = comboBox.valueAt(i);
} else {
defaultModel = comboBox.count ? comboBox.valueAt(0) : "";
}
if (defaultModel !== "") {
defaultModelName = ModelList.modelInfo(defaultModel).name;
} else {
defaultModelName = "";
}
}
text: qsTr("Load \u00B7 %1 (default) \u2192").arg(defaultModelName);
onClicked: {
var i = comboBox.find(MySettings.userDefaultModel)
if (i !== -1) {
comboBox.changeModel(i);
} else {
comboBox.changeModel(0);
}
}
// This requires a bit of work because apparently the combobox valueAt
// function only works after the combobox component is loaded so we have
// to use our own component loaded to make this work along with a signal
// from MySettings for when the setting for user default model changes
Connections {
target: MySettings
function onUserDefaultModelChanged() {
loadDefaultModelButton.updateDefaultModel()
}
}
Component.onCompleted: {
loadDefaultModelButton.updateDefaultModel()
}
Accessible.role: Accessible.Button
Accessible.name: qsTr("Load the default model")
Accessible.description: qsTr("Loads the default model which can be changed in settings")
}
ColumnLayout {
id: noModelInstalledLabel
visible: ModelList.selectableModels.count === 0
anchors.centerIn: parent
spacing: 0
Text {
Layout.alignment: Qt.AlignCenter
text: qsTr("No Model Installed")
color: theme.mutedLightTextColor
font.pixelSize: theme.fontSizeBannerSmall
}
Text {
Layout.topMargin: 15
horizontalAlignment: Qt.AlignHCenter
color: theme.mutedLighterTextColor
text: qsTr("GPT4All requires that you install at least one\nmodel to get started")
font.pixelSize: theme.fontSizeLarge
}
}
MyButton {
visible: ModelList.selectableModels.count === 0
anchors.top: noModelInstalledLabel.bottom
anchors.topMargin: 50
anchors.horizontalCenter: noModelInstalledLabel.horizontalCenter
rightPadding: 60
leftPadding: 60
text: qsTr("Install a Model")
onClicked: {
addModelViewRequested();
}
Accessible.role: Accessible.Button
Accessible.name: qsTr("Shows the add model view")
}
}
ColumnLayout {
anchors.fill: parent
visible: ModelList.selectableModels.count !== 0
ListView {
id: listView
Layout.maximumWidth: 1280
Layout.fillHeight: true
Layout.fillWidth: true
Layout.margins: 20
Layout.leftMargin: 50
Layout.rightMargin: 50
Layout.alignment: Qt.AlignHCenter
spacing: 10
model: chatModel
cacheBuffer: 2147483647
ScrollBar.vertical: ScrollBar {
policy: ScrollBar.AsNeeded
}
Accessible.role: Accessible.List
Accessible.name: qsTr("Conversation with the model")
Accessible.description: qsTr("prompt / response pairs from the conversation")
delegate: ChatItemView {
width: listView.contentItem.width - 15
inputBoxText: textInput.text
onSetInputBoxText: text => {
textInput.text = text;
textInput.forceActiveFocus();
textInput.cursorPosition = text.length;
}
height: visible ? implicitHeight : 0
visible: name !== "ToolResponse: " && name !== "System: "
}
remove: Transition {
OpacityAnimator { to: 0; duration: 500 }
}
function scrollToEnd() {
listView.positionViewAtEnd()
}
onContentHeightChanged: {
if (atYEnd)
scrollToEnd()
}
}
}
}
}
Rectangle {
id: conversationTrayContent
anchors.bottom: conversationTrayButton.top
anchors.horizontalCenter: conversationTrayButton.horizontalCenter
width: conversationTrayContentLayout.width
height: conversationTrayContentLayout.height
color: theme.containerBackground
radius: 5
opacity: 0
visible: false
clip: true
z: 400
property bool isHovered: (
conversationTrayButton.isHovered || resetContextButton.hovered || copyChatButton.hovered
)
state: conversationTrayContent.isHovered ? "expanded" : "collapsed"
states: [
State {
name: "expanded"
PropertyChanges { target: conversationTrayContent; opacity: 1 }
},
State {
name: "collapsed"
PropertyChanges { target: conversationTrayContent; opacity: 0 }
}
]
transitions: [
Transition {
from: "collapsed"
to: "expanded"
SequentialAnimation {
ScriptAction {
script: conversationTrayContent.visible = true
}
PropertyAnimation {
target: conversationTrayContent
property: "opacity"
duration: 300
easing.type: Easing.InOutQuad
}
}
},
Transition {
from: "expanded"
to: "collapsed"
SequentialAnimation {
PropertyAnimation {
target: conversationTrayContent
property: "opacity"
duration: 300
easing.type: Easing.InOutQuad
}
ScriptAction {
script: conversationTrayContent.visible = false
}
}
}
]
RowLayout {
id: conversationTrayContentLayout
spacing: 0
MyToolButton {
id: resetContextButton
Layout.preferredWidth: 40
Layout.preferredHeight: 40
source: "qrc:/gpt4all/icons/recycle.svg"
imageWidth: 20
imageHeight: 20
onClicked: resetContextDialog.open()
ToolTip.visible: resetContextButton.hovered
ToolTip.text: qsTr("Erase and reset chat session")
}
MyToolButton {
id: copyChatButton
Layout.preferredWidth: 40
Layout.preferredHeight: 40
source: "qrc:/gpt4all/icons/copy.svg"
imageWidth: 20
imageHeight: 20
TextEdit{
id: copyEdit
visible: false
}
onClicked: {
chatModel.copyToClipboard()
copyMessage.open()
}
ToolTip.visible: copyChatButton.hovered
ToolTip.text: qsTr("Copy chat session to clipboard")
}
}
}
Item {
id: conversationTrayButton
anchors.bottom: textInputView.top
anchors.horizontalCenter: textInputView.horizontalCenter
width: 40
height: 30
visible: chatModel.count && !currentChat.isServer && currentChat.isModelLoaded
property bool isHovered: conversationTrayMouseAreaButton.containsMouse
MouseArea {
id: conversationTrayMouseAreaButton
anchors.fill: parent
hoverEnabled: true
}
Text {
id: conversationTrayTextButton
anchors.centerIn: parent
horizontalAlignment: Qt.AlignHCenter
leftPadding: 5
rightPadding: 5
text: "\u00B7\u00B7\u00B7"
color: theme.textColor
font.pixelSize: 30 // fixed size
font.bold: true
}
}
MyButton {
anchors.bottom: textInputView.top
anchors.horizontalCenter: textInputView.horizontalCenter
anchors.bottomMargin: 20
textColor: theme.textColor
visible: !currentChat.isServer
&& !currentChat.isModelLoaded
&& currentChat.modelLoadingError === ""
&& !currentChat.trySwitchContextInProgress
&& !currentChat.isCurrentlyLoading
&& currentModelInstalled()
Image {
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
anchors.leftMargin: 15
sourceSize.width: 15
sourceSize.height: 15
source: "qrc:/gpt4all/icons/regenerate.svg"
}
leftPadding: 40
onClicked: {
currentChat.reloadModel();
}
borderWidth: 1
backgroundColor: theme.conversationButtonBackground
backgroundColorHovered: theme.conversationButtonBackgroundHovered
backgroundRadius: 5
padding: 15
topPadding: 8
bottomPadding: 8
text: qsTr("Reload \u00B7 %1").arg(currentChat.modelInfo.name)
fontPixelSize: theme.fontSizeSmall
Accessible.description: qsTr("Reloads the model")
}
Text {
id: statusBar
property string externalHoveredLink: ""
anchors.top: textInputView.bottom
anchors.bottom: parent.bottom
anchors.right: parent.right
anchors.rightMargin: 30
anchors.left: parent.left
anchors.leftMargin: 30
horizontalAlignment: Qt.AlignRight
verticalAlignment: Qt.AlignVCenter
color: textInputView.error !== null ? theme.textErrorColor : theme.mutedTextColor
visible: currentChat.tokenSpeed !== "" || externalHoveredLink !== "" || textInputView.error !== null
elide: Text.ElideRight
wrapMode: Text.WordWrap
text: {
if (externalHoveredLink !== "")
return externalHoveredLink
if (textInputView.error !== null)
return textInputView.error;
const segments = [currentChat.tokenSpeed];
const device = currentChat.device;
const backend = currentChat.deviceBackend;
if (device !== null) { // device is null if we have no model loaded
var deviceSegment = device;
if (backend === "CUDA" || backend === "Vulkan")
deviceSegment += ` (${backend})`;
segments.push(deviceSegment);
}
const fallbackReason = currentChat.fallbackReason;
if (fallbackReason !== null && fallbackReason !== "")
segments.push(fallbackReason);
return segments.join(" \u00B7 ");
}
font.pixelSize: theme.fontSizeSmaller
font.bold: true
onLinkActivated: function(link) { Qt.openUrlExternally(link) }
}
RectangularGlow {
id: effect
visible: !currentChat.isServer && ModelList.selectableModels.count !== 0
anchors.fill: textInputView
glowRadius: 50
spread: 0
color: theme.sendGlow
cornerRadius: 10
opacity: 0.1
}
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: error === null ? 1 : 2
border.color: error === null ? theme.controlBorder : theme.textErrorColor
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: textInputViewLayout.implicitHeight
visible: !currentChat.isServer && ModelList.selectableModels.count !== 0
property var error: null
function checkError() {
const info = currentModelInfo;
if (info === null || !info.id) {
error = null;
} else if (info.chatTemplate.isLegacy) {
error = qsTr("Legacy prompt template needs to be " +
"<a href=\"https://docs.gpt4all.io/gpt4all_desktop/chat_templates.html\">updated" +
"</a> in Settings.");
} else if (!info.chatTemplate.isSet) {
error = qsTr("No <a href=\"https://docs.gpt4all.io/gpt4all_desktop/chat_templates.html\">" +
"chat template</a> configured.");
} else if (/^\s*$/.test(info.chatTemplate.value)) {
error = qsTr("The <a href=\"https://docs.gpt4all.io/gpt4all_desktop/chat_templates.html\">" +
"chat template</a> cannot be blank.");
} else if (info.systemMessage.isLegacy) {
error = qsTr("Legacy system prompt needs to be " +
"<a href=\"https://docs.gpt4all.io/gpt4all_desktop/chat_templates.html\">updated" +
"</a> in Settings.");
} else
error = null;
}
Component.onCompleted: checkError()
Connections {
target: window
function onCurrentModelIdChanged() { textInputView.checkError(); }
}
Connections {
target: MySettings
function onChatTemplateChanged(info)
{ if (info.id === window.currentModelId) textInputView.checkError(); }
function onSystemMessageChanged(info)
{ if (info.id === window.currentModelId) textInputView.checkError(); }
}
MouseArea {
id: textInputViewMouseArea
anchors.fill: parent
onClicked: (mouse) => {
if (textInput.enabled)
textInput.forceActiveFocus();
}
}
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
Repeater {
model: attachmentModel
Rectangle {
width: 350
height: 50
radius: 5
color: theme.attachmentBackground
border.color: theme.controlBorder
Row {
spacing: 5
anchors.fill: parent
anchors.margins: 5
MyFileIcon {
iconSize: 40
fileName: model.file
}
Text {
width: 265
height: 40
text: model.file
color: theme.textColor
horizontalAlignment: Text.AlignHLeft
verticalAlignment: Text.AlignVCenter
font.pixelSize: theme.fontSizeMedium
font.bold: true
wrapMode: Text.WrapAnywhere
elide: Qt.ElideRight
}
}
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();
}
}
}
}
}
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 if (!chatModel.hasError && textInputView.error === null) {
editingFinished();
sendMessage();
}
}
function sendMessage() {
if ((textInput.text === "" && attachmentModel.count === 0) || currentChat.responseInProgress)
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()
}
}
}
}
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: {
// FIXME: This no longer sets a 'stopped' field so conversations that
// are copied to clipboard or to datalake don't indicate if the user
// has prematurely stopped the response. This has been broken since
// v3.0.0 at least.
currentChat.stopGenerating()
}
}
MyToolButton {
id: sendButton
backgroundColor: theme.conversationInputButtonBackground
backgroundColorHovered: theme.conversationInputButtonBackgroundHovered
imageWidth: theme.fontSizeLargest
imageHeight: theme.fontSizeLargest
visible: !currentChat.responseInProgress && !currentChat.isServer && ModelList.selectableModels.count !== 0
enabled: !chatModel.hasError && textInputView.error === null
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()
}
}
}
}
}
MyFileDialog {
id: fileDialog
nameFilters: ["All Supported Files (*.txt *.md *.rst *.xlsx)", "Text Files (*.txt *.md *.rst)", "Excel Worksheets (*.xlsx)"]
}
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();
})
}
}
}
}
}
}