Remove binary state from high-level API and use Jinja templates (#3147)

Signed-off-by: Jared Van Bortel <jared@nomic.ai>
Signed-off-by: Adam Treat <treat.adam@gmail.com>
Co-authored-by: Adam Treat <treat.adam@gmail.com>
This commit is contained in:
Jared Van Bortel
2024-11-25 10:04:17 -05:00
committed by GitHub
parent 3320094d29
commit 225bf6be93
54 changed files with 3423 additions and 2224 deletions

View File

@@ -10,7 +10,7 @@ import network
import llm
MySettingsTab {
onRestoreDefaultsClicked: {
onRestoreDefaults: {
MySettings.restoreApplicationDefaults();
}
title: qsTr("Application")
@@ -486,23 +486,6 @@ MySettingsTab {
Accessible.name: nThreadsLabel.text
Accessible.description: ToolTip.text
}
MySettingsLabel {
id: saveChatsContextLabel
text: qsTr("Save Chat Context")
helpText: qsTr("Save the chat model's state to disk for faster loading. WARNING: Uses ~2GB per chat.")
Layout.row: 12
Layout.column: 0
}
MyCheckBox {
id: saveChatsContextBox
Layout.row: 12
Layout.column: 2
Layout.alignment: Qt.AlignRight
checked: MySettings.saveChatsContext
onClicked: {
MySettings.saveChatsContext = !MySettings.saveChatsContext
}
}
MySettingsLabel {
id: trayLabel
text: qsTr("Enable System Tray")

View File

@@ -8,8 +8,23 @@ import QtQuick.Layouts
import gpt4all
import mysettings
ColumnLayout {
property var inputBoxText: null
signal setInputBoxText(text: string)
Item {
Layout.fillWidth: true
Layout.maximumWidth: parent.width
Layout.preferredHeight: gridLayout.height
HoverHandler { id: hoverArea }
GridLayout {
rows: 5
id: gridLayout
anchors.left: parent.left
anchors.right: parent.right
columns: 2
Item {
@@ -40,7 +55,7 @@ GridLayout {
to: 360
duration: 1000
loops: Animation.Infinite
running: currentResponse && (currentChat.responseInProgress || currentChat.restoringFromText)
running: isCurrentResponse && currentChat.responseInProgress
}
}
}
@@ -73,13 +88,11 @@ GridLayout {
color: theme.mutedTextColor
}
RowLayout {
visible: currentResponse && ((value === "" && currentChat.responseInProgress) || currentChat.restoringFromText)
visible: isCurrentResponse && (value === "" && currentChat.responseInProgress)
Text {
color: theme.mutedTextColor
font.pixelSize: theme.fontSizeLarger
text: {
if (currentChat.restoringFromText)
return qsTr("restoring from text ...");
switch (currentChat.responseState) {
case Chat.ResponseStopped: return qsTr("response stopped ...");
case Chat.LocalDocsRetrieval: return qsTr("retrieving localdocs: %1 ...").arg(currentChat.collectionList.join(", "));
@@ -99,10 +112,11 @@ GridLayout {
Layout.row: 1
Layout.column: 1
Layout.fillWidth: true
spacing: 20
spacing: 10
Flow {
id: attachedUrlsFlow
Layout.fillWidth: true
Layout.bottomMargin: 10
spacing: 10
visible: promptAttachments.length !== 0
Repeater {
@@ -156,7 +170,7 @@ GridLayout {
focus: false
readOnly: true
font.pixelSize: theme.fontSizeLarge
cursorVisible: currentResponse ? currentChat.responseInProgress : false
cursorVisible: isCurrentResponse ? currentChat.responseInProgress : false
cursorPosition: text.length
TapHandler {
id: tapHandler
@@ -183,12 +197,12 @@ GridLayout {
}
onLinkActivated: function(link) {
if (!currentResponse || !currentChat.responseInProgress)
if (!isCurrentResponse || !currentChat.responseInProgress)
Qt.openUrlExternally(link)
}
onLinkHovered: function (link) {
if (!currentResponse || !currentChat.responseInProgress)
if (!isCurrentResponse || !currentChat.responseInProgress)
statusBar.externalHoveredLink = link
}
@@ -239,13 +253,19 @@ GridLayout {
textProcessor.setValue(value);
}
property bool textProcessorReady: false
Component.onCompleted: {
resetChatViewTextProcessor();
chatModel.valueChanged.connect(function(i, value) {
if (index === i)
textProcessorReady = true;
}
Connections {
target: chatModel
function onValueChanged(i, value) {
if (myTextArea.textProcessorReady && index === i)
textProcessor.setValue(value);
}
);
}
Connections {
@@ -282,67 +302,6 @@ GridLayout {
Network.sendConversation(currentChat.id, getConversationJson());
}
}
Column {
Layout.alignment: Qt.AlignRight
Layout.rightMargin: 15
visible: name === "Response: " &&
(!currentResponse || !currentChat.responseInProgress) && MySettings.networkIsActive
spacing: 10
Item {
width: childrenRect.width
height: childrenRect.height
MyToolButton {
id: thumbsUp
width: 24
height: 24
imageWidth: width
imageHeight: height
opacity: thumbsUpState || thumbsUpState == thumbsDownState ? 1.0 : 0.2
source: "qrc:/gpt4all/icons/thumbs_up.svg"
Accessible.name: qsTr("Thumbs up")
Accessible.description: qsTr("Gives a thumbs up to the response")
onClicked: {
if (thumbsUpState && !thumbsDownState)
return
chatModel.updateNewResponse(index, "")
chatModel.updateThumbsUpState(index, true)
chatModel.updateThumbsDownState(index, false)
Network.sendConversation(currentChat.id, getConversationJson());
}
}
MyToolButton {
id: thumbsDown
anchors.top: thumbsUp.top
anchors.topMargin: 3
anchors.left: thumbsUp.right
anchors.leftMargin: 3
width: 24
height: 24
imageWidth: width
imageHeight: height
checked: thumbsDownState
opacity: thumbsDownState || thumbsUpState == thumbsDownState ? 1.0 : 0.2
transform: [
Matrix4x4 {
matrix: Qt.matrix4x4(-1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1)
},
Translate {
x: thumbsDown.width
}
]
source: "qrc:/gpt4all/icons/thumbs_down.svg"
Accessible.name: qsTr("Thumbs down")
Accessible.description: qsTr("Opens thumbs down dialog")
onClicked: {
thumbsDownDialog.open()
}
}
}
}
}
Item {
@@ -353,11 +312,13 @@ GridLayout {
Layout.preferredWidth: childrenRect.width
Layout.preferredHeight: childrenRect.height
visible: {
if (name !== "Response: ")
return false
if (consolidatedSources.length === 0)
return false
if (!MySettings.localDocsShowReferences)
return false
if (currentResponse && currentChat.responseInProgress
if (isCurrentResponse && currentChat.responseInProgress
&& currentChat.responseState !== Chat.GeneratingQuestions )
return false
return true
@@ -443,7 +404,7 @@ GridLayout {
return false
if (!MySettings.localDocsShowReferences)
return false
if (currentResponse && currentChat.responseInProgress
if (isCurrentResponse && currentChat.responseInProgress
&& currentChat.responseState !== Chat.GeneratingQuestions )
return false
return true
@@ -566,8 +527,139 @@ GridLayout {
}
}
ConfirmationDialog {
id: editPromptDialog
dialogTitle: qsTr("Edit this prompt?")
description: qsTr("The existing response and all later messages will be permanently erased.")
onAccepted: {
const msg = currentChat.popPrompt(index);
if (msg !== null)
setInputBoxText(msg);
}
}
ConfirmationDialog {
id: redoResponseDialog
dialogTitle: qsTr("Redo this response?")
description: qsTr("The existing response and all later messages will be permanently erased.")
onAccepted: currentChat.regenerateResponse(index)
}
RowLayout {
id: buttonRow
Layout.row: 4
Layout.column: 1
Layout.maximumWidth: parent.width
Layout.fillWidth: false
Layout.alignment: Qt.AlignLeft | Qt.AlignTop
spacing: 3
visible: !isCurrentResponse || !currentChat.responseInProgress
enabled: opacity > 0
opacity: hoverArea.hovered
readonly property var canModify: !currentChat.isServer && currentChat.isModelLoaded && !currentChat.responseInProgress
Behavior on opacity {
OpacityAnimator { duration: 30 }
}
ChatMessageButton {
visible: parent.canModify && model.name === "Prompt: "
Layout.maximumWidth: 24
Layout.maximumHeight: 24
Layout.alignment: Qt.AlignVCenter
Layout.fillWidth: false
source: "qrc:/gpt4all/icons/edit.svg"
onClicked: {
if (inputBoxText === "")
editPromptDialog.open();
}
name: qsTr("Edit")
}
ChatMessageButton {
visible: parent.canModify && model.name === "Response: "
Layout.maximumWidth: 24
Layout.maximumHeight: 24
Layout.alignment: Qt.AlignVCenter
Layout.fillWidth: false
name: qsTr("Redo")
source: "qrc:/gpt4all/icons/regenerate.svg"
onClicked: redoResponseDialog.open()
}
ChatMessageButton {
Layout.maximumWidth: 24
Layout.maximumHeight: 24
Layout.alignment: Qt.AlignVCenter
Layout.fillWidth: false
name: qsTr("Copy")
source: "qrc:/gpt4all/icons/copy.svg"
onClicked: {
myTextArea.selectAll();
myTextArea.copy();
myTextArea.deselect();
}
}
Item {
visible: name === "Response: " && MySettings.networkIsActive
Layout.alignment: Qt.AlignVCenter
Layout.preferredWidth: childrenRect.width
Layout.preferredHeight: childrenRect.height
Layout.fillWidth: false
ChatMessageButton {
id: thumbsUp
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
opacity: thumbsUpState || thumbsUpState == thumbsDownState ? 1.0 : 0.2
source: "qrc:/gpt4all/icons/thumbs_up.svg"
name: qsTr("Like response")
onClicked: {
if (thumbsUpState && !thumbsDownState)
return
chatModel.updateNewResponse(index, "")
chatModel.updateThumbsUpState(index, true)
chatModel.updateThumbsDownState(index, false)
Network.sendConversation(currentChat.id, getConversationJson());
}
}
ChatMessageButton {
id: thumbsDown
anchors.top: thumbsUp.top
anchors.topMargin: buttonRow.spacing
anchors.left: thumbsUp.right
anchors.leftMargin: buttonRow.spacing
checked: thumbsDownState
opacity: thumbsDownState || thumbsUpState == thumbsDownState ? 1.0 : 0.2
bgTransform: [
Matrix4x4 {
matrix: Qt.matrix4x4(-1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1)
},
Translate {
x: thumbsDown.width
}
]
source: "qrc:/gpt4all/icons/thumbs_down.svg"
name: qsTr("Dislike response")
onClicked: {
thumbsDownDialog.open()
}
}
}
}
} // GridLayout
} // Item
GridLayout {
Layout.fillWidth: true
Layout.maximumWidth: parent.width
function shouldShowSuggestions() {
if (!currentResponse)
if (!isCurrentResponse)
return false;
if (MySettings.suggestionMode === 2) // Off
return false;
@@ -577,8 +669,8 @@ GridLayout {
}
Item {
visible: shouldShowSuggestions()
Layout.row: 4
visible: parent.shouldShowSuggestions()
Layout.row: 5
Layout.column: 0
Layout.topMargin: 20
Layout.alignment: Qt.AlignVCenter | Qt.AlignRight
@@ -601,8 +693,8 @@ GridLayout {
}
Item {
visible: shouldShowSuggestions()
Layout.row: 4
visible: parent.shouldShowSuggestions()
Layout.row: 5
Layout.column: 1
Layout.topMargin: 20
Layout.fillWidth: true
@@ -627,8 +719,8 @@ GridLayout {
}
ColumnLayout {
visible: shouldShowSuggestions()
Layout.row: 5
visible: parent.shouldShowSuggestions()
Layout.row: 6
Layout.column: 1
Layout.fillWidth: true
Layout.minimumHeight: 1
@@ -786,4 +878,7 @@ GridLayout {
}
}
}
}
} // GridLayout
} // ColumnLayout

View File

@@ -0,0 +1,20 @@
import QtQuick
import QtQuick.Controls
import gpt4all
MyToolButton {
property string name
width: 24
height: 24
imageWidth: width
imageHeight: height
ToolTip {
visible: parent.hovered
y: parent.height * 1.5
text: name
delay: Qt.styleHints.mousePressAndHoldInterval
}
Accessible.name: name
}

View File

@@ -24,6 +24,12 @@ Rectangle {
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()
@@ -79,14 +85,11 @@ Rectangle {
function open_(msg) { message = msg; open(); }
}
SwitchModelDialog {
ConfirmationDialog {
id: switchModelDialog
anchors.centerIn: parent
Item {
Accessible.role: Accessible.Dialog
Accessible.name: qsTr("Switch model dialog")
Accessible.description: qsTr("Warn the user if they switch models, then context will be erased")
}
property int index: -1
dialogTitle: qsTr("Erase conversation?")
description: qsTr("Changing the model will erase the current conversation.")
}
PopupDialog {
@@ -103,6 +106,16 @@ Rectangle {
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();
}
}
function getConversation() {
var conversation = "";
for (var i = 0; i < chatModel.count; i++) {
@@ -703,7 +716,7 @@ Rectangle {
if (i !== -1) {
defaultModel = comboBox.valueAt(i);
} else {
defaultModel = comboBox.valueAt(0);
defaultModel = comboBox.count ? comboBox.valueAt(0) : "";
}
if (defaultModel !== "") {
defaultModelName = ModelList.modelInfo(defaultModel).name;
@@ -790,9 +803,9 @@ Rectangle {
Layout.leftMargin: 50
Layout.rightMargin: 50
Layout.alignment: Qt.AlignHCenter
spacing: 25
spacing: 10
model: chatModel
cacheBuffer: Math.max(0, listView.contentHeight)
cacheBuffer: 2147483647
ScrollBar.vertical: ScrollBar {
policy: ScrollBar.AsNeeded
@@ -804,6 +817,12 @@ Rectangle {
delegate: ChatItemView {
width: listView.contentItem.width - 15
inputBoxText: textInput.text
onSetInputBoxText: text => {
textInput.text = text;
textInput.forceActiveFocus();
textInput.cursorPosition = text.length;
}
}
function scrollToEnd() {
@@ -832,11 +851,9 @@ Rectangle {
clip: true
z: 400
property bool isHovered: {
return conversationTrayButton.isHovered ||
resetContextButton.hovered || copyChatButton.hovered ||
regenerateButton.hovered
}
property bool isHovered: (
conversationTrayButton.isHovered || resetContextButton.hovered || copyChatButton.hovered
)
state: conversationTrayContent.isHovered ? "expanded" : "collapsed"
states: [
@@ -892,11 +909,7 @@ Rectangle {
source: "qrc:/gpt4all/icons/recycle.svg"
imageWidth: 20
imageHeight: 20
onClicked: {
Network.trackChatEvent("reset_context", { "length": chatModel.count })
currentChat.reset();
currentChat.processSystemPrompt();
}
onClicked: resetContextDialog.open()
ToolTip.visible: resetContextButton.hovered
ToolTip.text: qsTr("Erase and reset chat session")
}
@@ -921,34 +934,6 @@ Rectangle {
ToolTip.visible: copyChatButton.hovered
ToolTip.text: qsTr("Copy chat session to clipboard")
}
MyToolButton {
id: regenerateButton
Layout.preferredWidth: 40
Layout.preferredHeight: 40
source: "qrc:/gpt4all/icons/regenerate.svg"
imageWidth: 20
imageHeight: 20
visible: chatModel.count && !currentChat.isServer && currentChat.isModelLoaded && !currentChat.responseInProgress
onClicked: {
if (chatModel.count < 2)
return
var promptIndex = chatModel.count - 2
var promptElement = chatModel.get(promptIndex)
var responseIndex = chatModel.count - 1
var responseElement = chatModel.get(responseIndex)
if (promptElement.name !== "Prompt: " || responseElement.name !== "Response: ")
return
currentChat.regenerateResponse()
chatModel.updateCurrentResponse(responseIndex, true)
chatModel.updateStopped(responseIndex, false)
chatModel.updateThumbsUpState(responseIndex, false)
chatModel.updateThumbsDownState(responseIndex, false)
chatModel.updateNewResponse(responseIndex, "")
currentChat.prompt(promptElement.promptPlusAttachments)
}
ToolTip.visible: regenerateButton.hovered
ToolTip.text: qsTr("Redo last chat response")
}
}
}
@@ -1026,13 +1011,15 @@ Rectangle {
anchors.leftMargin: 30
horizontalAlignment: Qt.AlignRight
verticalAlignment: Qt.AlignVCenter
color: theme.mutedTextColor
visible: currentChat.tokenSpeed !== "" || externalHoveredLink !== ""
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;
@@ -1050,6 +1037,7 @@ Rectangle {
}
font.pixelSize: theme.fontSizeSmaller
font.bold: true
onLinkActivated: function(link) { Qt.openUrlExternally(link) }
}
RectangularGlow {
@@ -1079,8 +1067,8 @@ Rectangle {
Rectangle {
id: textInputView
color: theme.controlBackground
border.width: 1
border.color: theme.controlBorder
border.width: error === null ? 1 : 2
border.color: error === null ? theme.controlBorder : theme.textErrorColor
radius: 10
anchors.left: parent.left
anchors.right: parent.right
@@ -1091,6 +1079,41 @@ Rectangle {
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
@@ -1214,16 +1237,16 @@ Rectangle {
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()
}
}
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 || currentChat.restoringFromText)
if ((textInput.text === "" && attachmentModel.count === 0) || currentChat.responseInProgress)
return
currentChat.stopGenerating()
@@ -1338,6 +1361,7 @@ Rectangle {
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")

View File

@@ -0,0 +1,59 @@
import QtCore
import QtQuick
import QtQuick.Controls
import QtQuick.Controls.Basic
import QtQuick.Layouts
MyDialog {
id: confirmationDialog
anchors.centerIn: parent
modal: true
padding: 20
property alias dialogTitle: titleText.text
property alias description: descriptionText.text
Theme { id: theme }
contentItem: ColumnLayout {
Text {
id: titleText
Layout.alignment: Qt.AlignHCenter
textFormat: Text.StyledText
color: theme.textColor
font.pixelSize: theme.fontSizeLarger
font.bold: true
}
Text {
id: descriptionText
Layout.alignment: Qt.AlignHCenter
textFormat: Text.StyledText
color: theme.textColor
font.pixelSize: theme.fontSizeMedium
}
}
footer: DialogButtonBox {
id: dialogBox
padding: 20
alignment: Qt.AlignRight
spacing: 10
MySettingsButton {
text: qsTr("OK")
textColor: theme.mediumButtonText
backgroundColor: theme.mediumButtonBackground
backgroundColorHovered: theme.mediumButtonBackgroundHovered
DialogButtonBox.buttonRole: DialogButtonBox.AcceptRole
}
MySettingsButton {
text: qsTr("Cancel")
DialogButtonBox.buttonRole: DialogButtonBox.RejectRole
}
background: Rectangle {
color: "transparent"
}
Keys.onEnterPressed: confirmationDialog.accept()
Keys.onReturnPressed: confirmationDialog.accept()
}
Component.onCompleted: dialogBox.forceActiveFocus()
}

View File

@@ -10,7 +10,7 @@ import mysettings
import network
MySettingsTab {
onRestoreDefaultsClicked: {
onRestoreDefaults: {
MySettings.restoreLocalDocsDefaults();
}

View File

@@ -8,10 +8,34 @@ import mysettings
import chatlistmodel
MySettingsTab {
onRestoreDefaultsClicked: {
onRestoreDefaults: {
MySettings.restoreModelDefaults(root.currentModelInfo);
}
title: qsTr("Model")
ConfirmationDialog {
id: resetSystemMessageDialog
property var index: null
property bool resetClears: false
dialogTitle: qsTr("%1 system message?").arg(resetClears ? qsTr("Clear") : qsTr("Reset"))
description: qsTr("The system message will be %1.").arg(resetClears ? qsTr("removed") : qsTr("reset to the default"))
onAccepted: MySettings.resetModelSystemMessage(ModelList.modelInfo(index))
function show(index_, resetClears_) { index = index_; resetClears = resetClears_; open(); }
}
ConfirmationDialog {
id: resetChatTemplateDialog
property bool resetClears: false
property var index: null
dialogTitle: qsTr("%1 chat template?").arg(resetClears ? qsTr("Clear") : qsTr("Reset"))
description: qsTr("The chat template will be %1.").arg(resetClears ? qsTr("erased") : qsTr("reset to the default"))
onAccepted: {
MySettings.resetModelChatTemplate(ModelList.modelInfo(index));
templateTextArea.resetText();
}
function show(index_, resetClears_) { index = index_; resetClears = resetClears_; open(); }
}
contentItem: GridLayout {
id: root
columns: 3
@@ -35,6 +59,7 @@ MySettingsTab {
RowLayout {
Layout.fillWidth: true
Layout.maximumWidth: parent.width
Layout.row: 2
Layout.column: 0
Layout.columnSpan: 2
@@ -153,69 +178,154 @@ MySettingsTab {
Layout.fillWidth: true
}
MySettingsLabel {
visible: !root.currentModelInfo.isOnline
text: qsTr("System Prompt")
helpText: qsTr("Prefixed at the beginning of every conversation. Must contain the appropriate framing tokens.")
RowLayout {
Layout.row: 7
Layout.column: 0
Layout.columnSpan: 2
Layout.topMargin: 15
Layout.fillWidth: true
Layout.maximumWidth: parent.width
spacing: 10
MySettingsLabel {
id: systemMessageLabel
text: qsTr("System Message")
helpText: qsTr("A message to set the context or guide the behavior of the model. Leave blank for " +
"none. NOTE: Since GPT4All 3.5, this should not contain control tokens.")
onReset: () => resetSystemMessageDialog.show(root.currentModelId, resetClears)
function updateResetButton() {
const info = root.currentModelInfo;
// NOTE: checks if the *override* is set, regardless of whether there is a default
canReset = !!info.id && MySettings.isModelSystemMessageSet(info);
resetClears = !info.defaultSystemMessage;
}
Component.onCompleted: updateResetButton()
Connections {
target: root
function onCurrentModelIdChanged() { systemMessageLabel.updateResetButton(); }
}
Connections {
target: MySettings
function onSystemMessageChanged(info)
{ if (info.id === root.currentModelId) systemMessageLabel.updateResetButton(); }
}
}
Label {
id: systemMessageLabelHelp
visible: systemMessageArea.errState !== "ok"
Layout.alignment: Qt.AlignBottom
Layout.fillWidth: true
Layout.rightMargin: 5
Layout.maximumHeight: systemMessageLabel.height
text: qsTr("System message is not " +
"<a href=\"https://docs.gpt4all.io/gpt4all_desktop/chat_templates.html\">plain text</a>.")
color: systemMessageArea.errState === "error" ? theme.textErrorColor : theme.textWarningColor
font.pixelSize: theme.fontSizeLarger
font.bold: true
wrapMode: Text.Wrap
elide: Text.ElideRight
onLinkActivated: function(link) { Qt.openUrlExternally(link) }
}
}
Rectangle {
id: systemPrompt
visible: !root.currentModelInfo.isOnline
id: systemMessage
Layout.row: 8
Layout.column: 0
Layout.columnSpan: 2
Layout.fillWidth: true
color: "transparent"
Layout.minimumHeight: Math.max(100, systemPromptArea.contentHeight + 20)
Layout.minimumHeight: Math.max(100, systemMessageArea.contentHeight + 20)
MyTextArea {
id: systemPromptArea
id: systemMessageArea
anchors.fill: parent
text: root.currentModelInfo.systemPrompt
property bool isBeingReset: false
function resetText() {
const info = root.currentModelInfo;
isBeingReset = true;
text = (info.id ? info.systemMessage.value : null) ?? "";
isBeingReset = false;
}
Component.onCompleted: resetText()
Connections {
target: MySettings
function onSystemPromptChanged() {
systemPromptArea.text = root.currentModelInfo.systemPrompt;
}
function onSystemMessageChanged(info)
{ if (info.id === root.currentModelId) systemMessageArea.resetText(); }
}
Connections {
target: root
function onCurrentModelInfoChanged() {
systemPromptArea.text = root.currentModelInfo.systemPrompt;
}
function onCurrentModelIdChanged() { systemMessageArea.resetText(); }
}
// strict validation, because setModelSystemMessage clears isLegacy
readonly property var reLegacyCheck: (
/(?:^|\s)(?:### *System\b|S(?:ystem|YSTEM):)|<\|(?:im_(?:start|end)|(?:start|end)_header_id|eot_id|SYSTEM_TOKEN)\|>|<<SYS>>/m
)
onTextChanged: {
MySettings.setModelSystemPrompt(root.currentModelInfo, text)
const info = root.currentModelInfo;
if (!info.id) {
errState = "ok";
} else if (info.systemMessage.isLegacy && (isBeingReset || reLegacyCheck.test(text))) {
errState = "error";
} else
errState = reLegacyCheck.test(text) ? "warning" : "ok";
if (info.id && errState !== "error" && !isBeingReset)
MySettings.setModelSystemMessage(info, text);
systemMessageLabel.updateResetButton();
}
Accessible.role: Accessible.EditableText
Accessible.name: systemMessageLabel.text
Accessible.description: systemMessageLabelHelp.text
}
}
RowLayout {
Layout.row: 9
Layout.column: 0
Layout.columnSpan: 2
Layout.topMargin: 15
Layout.fillWidth: true
Layout.maximumWidth: parent.width
spacing: 10
MySettingsLabel {
id: promptTemplateLabel
text: qsTr("Prompt Template")
helpText: qsTr("The template that wraps every prompt.")
id: chatTemplateLabel
text: qsTr("Chat Template")
helpText: qsTr("This Jinja template turns the chat into input for the model.")
onReset: () => resetChatTemplateDialog.show(root.currentModelId, resetClears)
function updateResetButton() {
const info = root.currentModelInfo;
canReset = !!info.id && (
MySettings.isModelChatTemplateSet(info)
|| templateTextArea.text !== (info.chatTemplate.value ?? "")
);
resetClears = !info.defaultChatTemplate;
}
Component.onCompleted: updateResetButton()
Connections {
target: root
function onCurrentModelIdChanged() { chatTemplateLabel.updateResetButton(); }
}
Connections {
target: MySettings
function onChatTemplateChanged(info)
{ if (info.id === root.currentModelId) chatTemplateLabel.updateResetButton(); }
}
}
MySettingsLabel {
id: promptTemplateLabelHelp
text: qsTr("Must contain the string \"%1\" to be replaced with the user's input.")
color: theme.textErrorColor
visible: templateTextArea.text.indexOf("%1") === -1
wrapMode: TextArea.Wrap
Label {
id: chatTemplateLabelHelp
visible: templateTextArea.errState !== "ok"
Layout.alignment: Qt.AlignBottom
Layout.fillWidth: true
Layout.rightMargin: 5
Layout.maximumHeight: chatTemplateLabel.height
text: templateTextArea.errMsg
color: templateTextArea.errState === "error" ? theme.textErrorColor : theme.textWarningColor
font.pixelSize: theme.fontSizeLarger
font.bold: true
wrapMode: Text.Wrap
elide: Text.ElideRight
onLinkActivated: function(link) { Qt.openUrlExternally(link) }
}
}
Rectangle {
id: promptTemplate
id: chatTemplate
Layout.row: 10
Layout.column: 0
Layout.columnSpan: 2
@@ -226,27 +336,71 @@ MySettingsTab {
MyTextArea {
id: templateTextArea
anchors.fill: parent
text: root.currentModelInfo.promptTemplate
font: fixedFont
property bool isBeingReset: false
property var errMsg: null
function resetText() {
const info = root.currentModelInfo;
isBeingReset = true;
text = (info.id ? info.chatTemplate.value : null) ?? "";
isBeingReset = false;
}
Component.onCompleted: resetText()
Connections {
target: MySettings
function onPromptTemplateChanged() {
templateTextArea.text = root.currentModelInfo.promptTemplate;
}
function onChatTemplateChanged() { templateTextArea.resetText(); }
}
Connections {
target: root
function onCurrentModelInfoChanged() {
templateTextArea.text = root.currentModelInfo.promptTemplate;
}
function onCurrentModelIdChanged() { templateTextArea.resetText(); }
}
function legacyCheck() {
return /%[12]\b/.test(text) || !/\{%.*%\}.*\{\{.*\}\}.*\{%.*%\}/.test(text.replace(/\n/g, ''))
|| !/\bcontent\b/.test(text);
}
onTextChanged: {
if (templateTextArea.text.indexOf("%1") !== -1) {
MySettings.setModelPromptTemplate(root.currentModelInfo, text)
const info = root.currentModelInfo;
let jinjaError;
if (!info.id) {
errMsg = null;
errState = "ok";
} else if (info.chatTemplate.isLegacy && (isBeingReset || legacyCheck())) {
errMsg = null;
errState = "error";
} else if (text === "" && !info.chatTemplate.isSet) {
errMsg = qsTr("No <a href=\"https://docs.gpt4all.io/gpt4all_desktop/chat_templates.html\">" +
"chat template</a> configured.");
errState = "error";
} else if (/^\s*$/.test(text)) {
errMsg = qsTr("The <a href=\"https://docs.gpt4all.io/gpt4all_desktop/chat_templates.html\">" +
"chat template</a> cannot be blank.");
errState = "error";
} else if ((jinjaError = MySettings.checkJinjaTemplateError(text)) !== null) {
errMsg = qsTr("<a href=\"https://docs.gpt4all.io/gpt4all_desktop/chat_templates.html\">Syntax" +
" error</a>: %1").arg(jinjaError);
errState = "error";
} else if (legacyCheck()) {
errMsg = qsTr("Chat template is not in " +
"<a href=\"https://docs.gpt4all.io/gpt4all_desktop/chat_templates.html\">" +
"Jinja format</a>.")
errState = "warning";
} else {
errState = "ok";
}
if (info.id && errState !== "error" && !isBeingReset)
MySettings.setModelChatTemplate(info, text);
chatTemplateLabel.updateResetButton();
}
Keys.onPressed: event => {
if (event.key === Qt.Key_Tab) {
const a = templateTextArea;
event.accepted = true; // suppress tab
a.insert(a.cursorPosition, ' '); // four spaces
}
}
Accessible.role: Accessible.EditableText
Accessible.name: promptTemplateLabel.text
Accessible.description: promptTemplateLabelHelp.text
Accessible.name: chatTemplateLabel.text
Accessible.description: chatTemplateLabelHelp.text
}
}

View File

@@ -17,6 +17,7 @@ Button {
property color borderColor: "transparent"
property real fontPixelSize: theme.fontSizeLarge
property string toolTip
property alias backgroundRadius: background.radius
contentItem: Text {
text: myButton.text
@@ -28,6 +29,7 @@ Button {
Accessible.name: text
}
background: Rectangle {
id: background
radius: 10
border.width: borderWidth
border.color: borderColor

View File

@@ -17,13 +17,42 @@ ColumnLayout {
property alias color: mainTextLabel.color
property alias linkColor: mainTextLabel.linkColor
Label {
id: mainTextLabel
color: theme.settingsTitleTextColor
font.pixelSize: theme.fontSizeLarger
font.bold: true
onLinkActivated: function(link) {
root.linkActivated(link);
property var onReset: null
property alias canReset: resetButton.enabled
property bool resetClears: false
Item {
anchors.margins: 5
width: childrenRect.width
height: mainTextLabel.contentHeight
Label {
id: mainTextLabel
anchors.left: parent.left
anchors.top: parent.top
anchors.bottom: parent.bottom
color: theme.settingsTitleTextColor
font.pixelSize: theme.fontSizeLarger
font.bold: true
verticalAlignment: Text.AlignVCenter
onLinkActivated: function(link) {
root.linkActivated(link);
}
}
MySettingsButton {
id: resetButton
anchors.baseline: mainTextLabel.baseline
anchors.left: mainTextLabel.right
height: mainTextLabel.contentHeight
anchors.leftMargin: 10
padding: 2
leftPadding: 10
rightPadding: 10
backgroundRadius: 5
text: resetClears ? qsTr("Clear") : qsTr("Reset")
visible: root.onReset !== null
onClicked: root.onReset()
}
}
Label {

View File

@@ -9,7 +9,7 @@ Item {
property string title: ""
property Item contentItem: null
property bool showRestoreDefaultsButton: true
signal restoreDefaultsClicked
signal restoreDefaults
onContentItemChanged: function() {
if (contentItem) {
@@ -19,6 +19,13 @@ Item {
}
}
ConfirmationDialog {
id: restoreDefaultsDialog
dialogTitle: qsTr("Restore defaults?")
description: qsTr("This page of settings will be reset to the defaults.")
onAccepted: root.restoreDefaults()
}
ScrollView {
id: scrollView
width: parent.width
@@ -47,6 +54,7 @@ Item {
Column {
id: contentInner
Layout.fillWidth: true
Layout.maximumWidth: parent.width
}
Item {
@@ -63,9 +71,7 @@ Item {
Accessible.role: Accessible.Button
Accessible.name: text
Accessible.description: qsTr("Restores settings dialog to a default state")
onClicked: {
root.restoreDefaultsClicked();
}
onClicked: restoreDefaultsDialog.open()
}
}
}

View File

@@ -5,18 +5,27 @@ import QtQuick.Controls.Basic
TextArea {
id: myTextArea
property string errState: "ok" // one of "ok", "error", "warning"
color: enabled ? theme.textColor : theme.mutedTextColor
placeholderTextColor: theme.mutedTextColor
font.pixelSize: theme.fontSizeLarge
background: Rectangle {
implicitWidth: 150
color: theme.controlBackground
border.width: 1
border.color: theme.controlBorder
border.width: errState === "ok" ? 1 : 2
border.color: {
switch (errState) {
case "ok": return theme.controlBorder;
case "warning": return theme.textWarningColor;
case "error": return theme.textErrorColor;
}
}
radius: 10
}
padding: 10
wrapMode: TextArea.Wrap
ToolTip.delay: Qt.styleHints.mousePressAndHoldInterval
}
}

View File

@@ -16,6 +16,7 @@ Button {
property alias fillMode: image.fillMode
property alias imageWidth: image.sourceSize.width
property alias imageHeight: image.sourceSize.height
property alias bgTransform: background.transform
contentItem: Text {
text: myButton.text
horizontalAlignment: Text.AlignHCenter
@@ -26,6 +27,7 @@ Button {
}
background: Item {
id: background
anchors.fill: parent
Rectangle {
anchors.fill: parent

View File

@@ -1,46 +0,0 @@
import QtCore
import QtQuick
import QtQuick.Controls
import QtQuick.Controls.Basic
import QtQuick.Layouts
import llm
import mysettings
MyDialog {
id: switchModelDialog
anchors.centerIn: parent
modal: true
padding: 20
property int index: -1
Theme {
id: theme
}
contentItem: Text {
textFormat: Text.StyledText
text: qsTr("<b>Warning:</b> changing the model will erase the current conversation. Do you wish to continue?")
color: theme.textColor
font.pixelSize: theme.fontSizeLarge
}
footer: DialogButtonBox {
id: dialogBox
padding: 20
alignment: Qt.AlignRight
spacing: 10
MySettingsButton {
text: qsTr("Continue")
Accessible.description: qsTr("Continue with model loading")
DialogButtonBox.buttonRole: DialogButtonBox.AcceptRole
}
MySettingsButton {
text: qsTr("Cancel")
Accessible.description: qsTr("Cancel")
DialogButtonBox.buttonRole: DialogButtonBox.RejectRole
}
background: Rectangle {
color: "transparent"
}
}
}

View File

@@ -64,6 +64,9 @@ QtObject {
property color green800: Qt.hsla(123/360, 0.17, 0.24)
property color green900: Qt.hsla(124/360, 0.17, 0.20)
property color green950: Qt.hsla(125/360, 0.22, 0.10)
property color green300_sat: Qt.hsla(122/360, 0.24, 0.73)
property color green400_sat: Qt.hsla(122/360, 0.23, 0.58)
property color green450_sat: Qt.hsla(122/360, 0.23, 0.52)
// yellow
property color yellow0: Qt.hsla(47/360, 0.90, 0.99)
@@ -99,6 +102,7 @@ QtObject {
property color purple200: Qt.hsla(279/360, 1.0, 0.91)
property color purple300: Qt.hsla(279/360, 1.0, 0.84)
property color purple400: Qt.hsla(279/360, 1.0, 0.73)
property color purple450: Qt.hsla(279/360, 1.0, 0.68)
property color purple500: Qt.hsla(279/360, 1.0, 0.63)
property color purple600: Qt.hsla(279/360, 1.0, 0.53)
property color purple700: Qt.hsla(279/360, 1.0, 0.47)
@@ -408,6 +412,39 @@ QtObject {
}
}
property color mediumButtonBackground: {
switch (MySettings.chatTheme) {
case MySettingsEnums.ChatTheme.LegacyDark:
return purple400
case MySettingsEnums.ChatTheme.Dark:
return green400_sat
default:
return green400_sat
}
}
property color mediumButtonBackgroundHovered: {
switch (MySettings.chatTheme) {
case MySettingsEnums.ChatTheme.LegacyDark:
return purple450
case MySettingsEnums.ChatTheme.Dark:
return green450_sat
default:
return green300_sat
}
}
property color mediumButtonText: {
switch (MySettings.chatTheme) {
case MySettingsEnums.ChatTheme.LegacyDark:
return textColor
case MySettingsEnums.ChatTheme.Dark:
return textColor
default:
return white
}
}
property color darkButtonText: {
switch (MySettings.chatTheme) {
case MySettingsEnums.ChatTheme.LegacyDark:
@@ -922,16 +959,8 @@ QtObject {
}
}
property color textErrorColor: {
switch (MySettings.chatTheme) {
case MySettingsEnums.ChatTheme.LegacyDark:
return red400
case MySettingsEnums.ChatTheme.Dark:
return red400
default:
return red400
}
}
readonly property color textErrorColor: red400
readonly property color textWarningColor: yellow400
property color settingsTitleTextColor: {
switch (MySettings.chatTheme) {