refactor: Restructure project into sources/tests layout and dissolve PluginLLMCore

This commit is contained in:
Petr Mironychev
2026-07-14 02:50:43 +02:00
parent adef7972ed
commit 34d4f2c517
348 changed files with 809 additions and 852 deletions

View File

@@ -0,0 +1,147 @@
// Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Qt.labs.platform as Platform
import ChatView
import UIControls
Flow {
id: root
property alias attachedFilesModel: attachRepeater.model
property color accentColor: palette.mid
property string iconPath
signal removeFileFromListByIndex(index: int)
spacing: 5
leftPadding: 5
rightPadding: 5
topPadding: attachRepeater.model.length > 0 ? 2 : 0
bottomPadding: attachRepeater.model.length > 0 ? 2 : 0
Repeater {
id: attachRepeater
delegate: FileItem {
id: fileItem
required property int index
required property string modelData
filePath: modelData
height: 30
width: contentRow.width + 10
Rectangle {
anchors.fill: parent
radius: 4
color: palette.button
border.width: 1
border.color: mouse.containsMouse ? palette.highlight : root.accentColor
}
MouseArea {
id: mouse
anchors.fill: parent
hoverEnabled: true
acceptedButtons: Qt.LeftButton | Qt.MiddleButton | Qt.RightButton
onClicked: (mouse) => {
if (mouse.button === Qt.RightButton) {
contextMenu.open()
} else if (mouse.button === Qt.MiddleButton ||
(mouse.button === Qt.LeftButton && (mouse.modifiers & Qt.ControlModifier))) {
root.removeFileFromListByIndex(fileItem.index)
} else if (mouse.modifiers & Qt.ShiftModifier) {
fileItem.openFileInExternalEditor()
} else {
fileItem.openFileInEditor()
}
}
QoAToolTip {
visible: mouse.containsMouse
delay: 500
text: "Click: Open in Qt Creator\nShift+Click: Open in external editor\nCtrl+Click / Middle Click: Remove"
}
}
Platform.Menu {
id: contextMenu
Platform.MenuItem {
text: qsTr("Open in Qt Creator")
onTriggered: fileItem.openFileInEditor()
}
Platform.MenuItem {
text: qsTr("Open in External Editor")
onTriggered: fileItem.openFileInExternalEditor()
}
Platform.MenuSeparator {}
Platform.MenuItem {
text: qsTr("Remove")
onTriggered: root.removeFileFromListByIndex(fileItem.index)
}
}
Row {
id: contentRow
spacing: 5
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
anchors.leftMargin: 5
Image {
id: icon
anchors.verticalCenter: parent.verticalCenter
source: root.iconPath
sourceSize.width: 8
sourceSize.height: 15
}
Text {
id: fileNameText
anchors.verticalCenter: parent.verticalCenter
color: palette.buttonText
text: {
const parts = modelData.split('/');
return parts[parts.length - 1];
}
}
MouseArea {
id: closeButton
anchors.verticalCenter: parent.verticalCenter
width: closeIcon.width + 5
height: closeButton.width + 5
onClicked: root.removeFileFromListByIndex(index)
Image {
id: closeIcon
anchors.centerIn: parent
source: palette.window.hslLightness > 0.5 ? "qrc:/qt/qml/ChatView/icons/close-dark.svg"
: "qrc:/qt/qml/ChatView/icons/close-light.svg"
width: 6
height: 6
}
}
}
}
}
}

View File

@@ -0,0 +1,190 @@
// Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import ChatView
import UIControls
Rectangle {
id: root
property alias sendButton: sendButtonId
property alias syncOpenFiles: syncOpenFilesId
property alias attachFiles: attachFilesId
property alias attachImages: attachImagesId
property alias linkFiles: linkFilesId
property alias compressButton: compressButtonId
property alias cancelCompressButton: cancelCompressButtonId
property bool isCompressing: false
property bool isProcessing: false
property alias sendButtonTooltip: sendButtonTooltipId
color: palette.window.hslLightness > 0.5 ?
Qt.darker(palette.window, 1.1) :
Qt.lighter(palette.window, 1.1)
RowLayout {
id: bottomBar
anchors {
left: parent.left
leftMargin: 5
right: parent.right
rightMargin: 5
verticalCenter: parent.verticalCenter
}
spacing: 10
QoAButton {
id: attachFilesId
icon {
source: "qrc:/qt/qml/ChatView/icons/attach-file-dark.svg"
height: 15
width: 8
}
QoAToolTip {
visible: attachFilesId.hovered
delay: 250
text: qsTr("Attach file to message")
}
}
QoAButton {
id: attachImagesId
icon {
source: "qrc:/qt/qml/ChatView/icons/image-dark.svg"
height: 15
width: 15
}
QoAToolTip {
visible: attachImagesId.hovered
delay: 250
text: qsTr("Attach image to message")
}
}
QoAButton {
id: linkFilesId
icon {
source: "qrc:/qt/qml/ChatView/icons/link-file-dark.svg"
height: 15
width: 8
}
QoAToolTip {
visible: linkFilesId.hovered
delay: 250
text: qsTr("Link file to context")
}
}
CheckBox {
id: syncOpenFilesId
text: qsTr("Sync open files")
QoAToolTip {
visible: syncOpenFilesId.hovered
text: qsTr("Automatically synchronize currently opened files with the model context")
}
}
Item {
Layout.fillWidth: true
}
Row {
id: compressingRow
visible: root.isCompressing
spacing: 6
QoABusyIndicator {
id: compressBusyIndicator
anchors.verticalCenter: parent.verticalCenter
running: root.isCompressing
width: 16
height: 16
}
Text {
text: qsTr("Compressing...")
anchors.verticalCenter: parent.verticalCenter
color: palette.text
font.pixelSize: 12
}
QoAButton {
id: cancelCompressButtonId
anchors.verticalCenter: parent.verticalCenter
text: qsTr("Cancel")
QoAToolTip {
visible: cancelCompressButtonId.hovered
delay: 250
text: qsTr("Cancel compression")
}
}
}
QoAButton {
id: compressButtonId
visible: !root.isCompressing
text: qsTr("Compress")
icon {
source: "qrc:/qt/qml/ChatView/icons/compress-icon.svg"
height: 15
width: 15
}
QoAToolTip {
visible: compressButtonId.hovered
delay: 250
text: qsTr("Compress chat (create summarized copy using LLM)")
}
}
QoAButton {
id: sendButtonId
leftPadding: root.isProcessing ? 22 : 4
icon {
height: 15
width: 15
}
QoABusyIndicator {
id: sendBusyIndicator
anchors.left: parent.left
anchors.leftMargin: 5
anchors.verticalCenter: parent.verticalCenter
width: 14
height: 14
running: root.isProcessing
}
QoAToolTip {
id: sendButtonTooltipId
visible: sendButtonId.hovered
delay: 250
}
}
}
}

View File

@@ -0,0 +1,543 @@
// Copyright (C) 2025-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import QtQuick.Controls.Basic as QQC
import UIControls
import ChatView
Popup {
id: root
property string baseSystemPrompt
property string currentAgentRole
property string currentAgentRoleDescription
property string currentAgentRoleSystemPrompt
property var activeRules
property int activeRulesCount
property string selectedRuleContent
signal openSettings()
signal openAgentRolesSettings()
signal openRulesFolder()
signal refreshRules()
signal ruleSelected(int index)
modal: true
focus: true
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
background: Rectangle {
color: palette.window
border.color: palette.mid
border.width: 1
radius: 4
}
ChatUtils {
id: utils
}
ColumnLayout {
anchors.fill: parent
anchors.margins: 10
spacing: 8
RowLayout {
Layout.fillWidth: true
spacing: 10
Text {
text: qsTr("Chat Context")
font.pixelSize: 16
font.bold: true
color: palette.text
Layout.fillWidth: true
}
QoAButton {
text: qsTr("Refresh")
onClicked: root.refreshRules()
}
QoAButton {
text: qsTr("Close")
onClicked: root.close()
}
}
Rectangle {
Layout.fillWidth: true
height: 1
color: palette.mid
}
Flickable {
id: mainFlickable
Layout.fillWidth: true
Layout.fillHeight: true
contentHeight: sectionsColumn.implicitHeight
clip: true
boundsBehavior: Flickable.StopAtBounds
ColumnLayout {
id: sectionsColumn
width: mainFlickable.width
spacing: 8
CollapsibleSection {
id: systemPromptSection
Layout.fillWidth: true
title: qsTr("Base System Prompt")
badge: root.baseSystemPrompt.length > 0 ? qsTr("Active") : qsTr("Empty")
badgeColor: root.baseSystemPrompt.length > 0 ? Qt.rgba(0.2, 0.6, 0.3, 1.0) : palette.mid
sectionContent: ColumnLayout {
spacing: 5
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: Math.min(Math.max(systemPromptText.implicitHeight + 16, 50), 200)
color: palette.base
border.color: palette.mid
border.width: 1
radius: 2
Flickable {
id: systemPromptFlickable
anchors.fill: parent
anchors.margins: 8
contentHeight: systemPromptText.implicitHeight
clip: true
boundsBehavior: Flickable.StopAtBounds
TextEdit {
id: systemPromptText
width: systemPromptFlickable.width
text: root.baseSystemPrompt.length > 0 ? root.baseSystemPrompt : qsTr("No system prompt configured")
readOnly: true
selectByMouse: true
wrapMode: Text.WordWrap
color: root.baseSystemPrompt.length > 0 ? palette.text : palette.mid
font.family: "monospace"
font.pixelSize: 11
}
QQC.ScrollBar.vertical: QQC.ScrollBar {
policy: systemPromptFlickable.contentHeight > systemPromptFlickable.height ? QQC.ScrollBar.AsNeeded : QQC.ScrollBar.AlwaysOff
}
}
}
RowLayout {
Layout.fillWidth: true
Item { Layout.fillWidth: true }
QoAButton {
text: qsTr("Copy")
enabled: root.baseSystemPrompt.length > 0
onClicked: utils.copyToClipboard(root.baseSystemPrompt)
}
QoAButton {
text: qsTr("Edit in Settings")
onClicked: {
root.openSettings()
root.close()
}
}
}
}
}
CollapsibleSection {
id: agentRoleSection
Layout.fillWidth: true
title: qsTr("Agent Role")
badge: root.currentAgentRole
badgeColor: root.currentAgentRoleSystemPrompt.length > 0 ? Qt.rgba(0.3, 0.4, 0.7, 1.0) : palette.mid
sectionContent: ColumnLayout {
spacing: 8
Text {
text: root.currentAgentRoleDescription
font.pixelSize: 11
font.italic: true
color: palette.mid
wrapMode: Text.WordWrap
Layout.fillWidth: true
visible: root.currentAgentRoleDescription.length > 0
}
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: Math.min(Math.max(agentPromptText.implicitHeight + 16, 50), 200)
color: palette.base
border.color: palette.mid
border.width: 1
radius: 2
visible: root.currentAgentRoleSystemPrompt.length > 0
Flickable {
id: agentPromptFlickable
anchors.fill: parent
anchors.margins: 8
contentHeight: agentPromptText.implicitHeight
clip: true
boundsBehavior: Flickable.StopAtBounds
TextEdit {
id: agentPromptText
width: agentPromptFlickable.width
text: root.currentAgentRoleSystemPrompt
readOnly: true
selectByMouse: true
wrapMode: Text.WordWrap
color: palette.text
font.family: "monospace"
font.pixelSize: 11
}
QQC.ScrollBar.vertical: QQC.ScrollBar {
policy: agentPromptFlickable.contentHeight > agentPromptFlickable.height ? QQC.ScrollBar.AsNeeded : QQC.ScrollBar.AlwaysOff
}
}
}
Text {
text: qsTr("No role selected. Using base system prompt only.")
font.pixelSize: 11
color: palette.mid
wrapMode: Text.WordWrap
Layout.fillWidth: true
visible: root.currentAgentRoleSystemPrompt.length === 0
}
RowLayout {
Layout.fillWidth: true
Item { Layout.fillWidth: true }
QoAButton {
text: qsTr("Copy")
enabled: root.currentAgentRoleSystemPrompt.length > 0
onClicked: utils.copyToClipboard(root.currentAgentRoleSystemPrompt)
}
QoAButton {
text: qsTr("Manage Roles")
onClicked: {
root.openAgentRolesSettings()
root.close()
}
}
}
}
}
CollapsibleSection {
id: projectRulesSection
Layout.fillWidth: true
title: qsTr("Project Rules")
badge: root.activeRulesCount > 0 ? qsTr("%1 active").arg(root.activeRulesCount) : qsTr("None")
badgeColor: root.activeRulesCount > 0 ? Qt.rgba(0.6, 0.5, 0.2, 1.0) : palette.mid
sectionContent: ColumnLayout {
spacing: 8
SplitView {
Layout.fillWidth: true
Layout.preferredHeight: 220
orientation: Qt.Horizontal
visible: root.activeRulesCount > 0
Rectangle {
SplitView.minimumWidth: 120
SplitView.preferredWidth: 180
color: palette.base
border.color: palette.mid
border.width: 1
radius: 2
ColumnLayout {
anchors.fill: parent
anchors.margins: 5
spacing: 5
Text {
text: qsTr("Rules (%1)").arg(rulesList.count)
font.pixelSize: 11
font.bold: true
color: palette.text
Layout.fillWidth: true
}
ListView {
id: rulesList
Layout.fillWidth: true
Layout.fillHeight: true
clip: true
model: root.activeRules
currentIndex: 0
boundsBehavior: Flickable.StopAtBounds
delegate: ItemDelegate {
required property var modelData
required property int index
width: ListView.view.width
height: ruleItemContent.implicitHeight + 8
highlighted: ListView.isCurrentItem
background: Rectangle {
color: {
if (parent.highlighted)
return palette.highlight
if (parent.hovered)
return Qt.tint(palette.base, Qt.rgba(0, 0, 0, 0.05))
return "transparent"
}
radius: 2
}
contentItem: ColumnLayout {
id: ruleItemContent
spacing: 2
Text {
text: modelData.fileName
font.pixelSize: 10
color: parent.parent.highlighted ? palette.highlightedText : palette.text
elide: Text.ElideMiddle
Layout.fillWidth: true
}
Text {
text: modelData.category
font.pixelSize: 9
color: parent.parent.highlighted ? palette.highlightedText : palette.mid
Layout.fillWidth: true
}
}
onClicked: {
rulesList.currentIndex = index
root.ruleSelected(index)
}
}
QQC.ScrollBar.vertical: QQC.ScrollBar {
policy: rulesList.contentHeight > rulesList.height ? QQC.ScrollBar.AsNeeded : QQC.ScrollBar.AlwaysOff
}
}
}
}
Rectangle {
SplitView.fillWidth: true
SplitView.minimumWidth: 200
color: palette.base
border.color: palette.mid
border.width: 1
radius: 2
ColumnLayout {
anchors.fill: parent
anchors.margins: 5
spacing: 5
RowLayout {
Layout.fillWidth: true
spacing: 5
Text {
text: qsTr("Content")
font.pixelSize: 11
font.bold: true
color: palette.text
Layout.fillWidth: true
}
QoAButton {
text: qsTr("Copy")
enabled: root.selectedRuleContent.length > 0
onClicked: utils.copyToClipboard(root.selectedRuleContent)
}
}
Flickable {
id: ruleContentFlickable
Layout.fillWidth: true
Layout.fillHeight: true
contentHeight: ruleContentArea.implicitHeight
clip: true
boundsBehavior: Flickable.StopAtBounds
TextEdit {
id: ruleContentArea
width: ruleContentFlickable.width
text: root.selectedRuleContent
readOnly: true
selectByMouse: true
wrapMode: Text.WordWrap
selectionColor: palette.highlight
color: palette.text
font.family: "monospace"
font.pixelSize: 11
}
QQC.ScrollBar.vertical: QQC.ScrollBar {
policy: ruleContentFlickable.contentHeight > ruleContentFlickable.height ? QQC.ScrollBar.AsNeeded : QQC.ScrollBar.AlwaysOff
}
}
}
}
}
Text {
text: qsTr("No project rules found.\nCreate .md files in .qodeassist/rules/common/ or .qodeassist/rules/chat/")
font.pixelSize: 11
color: palette.mid
wrapMode: Text.WordWrap
horizontalAlignment: Text.AlignHCenter
Layout.fillWidth: true
visible: root.activeRulesCount === 0
}
RowLayout {
Layout.fillWidth: true
Item { Layout.fillWidth: true }
QoAButton {
text: qsTr("Open Rules Folder")
onClicked: root.openRulesFolder()
}
}
}
}
}
QQC.ScrollBar.vertical: QQC.ScrollBar {
policy: mainFlickable.contentHeight > mainFlickable.height ? QQC.ScrollBar.AsNeeded : QQC.ScrollBar.AlwaysOff
}
}
Rectangle {
Layout.fillWidth: true
height: 1
color: palette.mid
}
Text {
text: qsTr("Final prompt: Base System Prompt + Agent Role + Project Info + Project Rules + Linked Files")
font.pixelSize: 9
color: palette.mid
wrapMode: Text.WordWrap
Layout.fillWidth: true
}
}
component CollapsibleSection: ColumnLayout {
id: sectionRoot
property string title
property string badge
property color badgeColor: palette.mid
property Component sectionContent: null
property bool expanded: false
spacing: 0
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 32
color: sectionMouseArea.containsMouse ? Qt.tint(palette.button, Qt.rgba(0, 0, 0, 0.05)) : palette.button
border.color: palette.mid
border.width: 1
radius: 2
MouseArea {
id: sectionMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: sectionRoot.expanded = !sectionRoot.expanded
}
RowLayout {
anchors.fill: parent
anchors.leftMargin: 8
anchors.rightMargin: 8
spacing: 8
Text {
text: sectionRoot.expanded ? "▼" : "▶"
font.pixelSize: 10
color: palette.text
}
Text {
text: sectionRoot.title
font.pixelSize: 12
font.bold: true
color: palette.text
Layout.fillWidth: true
}
Rectangle {
implicitWidth: badgeText.implicitWidth + 12
implicitHeight: 18
color: sectionRoot.badgeColor
radius: 3
Text {
id: badgeText
anchors.centerIn: parent
text: sectionRoot.badge
font.pixelSize: 10
color: "#FFFFFF"
}
}
}
}
Loader {
id: contentLoader
Layout.fillWidth: true
Layout.leftMargin: 12
Layout.topMargin: 8
Layout.bottomMargin: 4
sourceComponent: sectionRoot.sectionContent
visible: sectionRoot.expanded
active: sectionRoot.expanded
}
}
onOpened: {
if (root.activeRulesCount > 0) {
root.ruleSelected(0)
}
}
}

View File

@@ -0,0 +1,150 @@
// Copyright (C) 2025-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import UIControls
Rectangle {
id: root
property int totalEdits: 0
property int appliedEdits: 0
property int pendingEdits: 0
property int rejectedEdits: 0
property bool hasAppliedEdits: appliedEdits > 0
property bool hasRejectedEdits: rejectedEdits > 0
property bool hasPendingEdits: pendingEdits > 0
signal applyAllClicked()
signal undoAllClicked()
visible: totalEdits > 0
implicitHeight: visible ? 40 : 0
color: palette.window.hslLightness > 0.5 ?
Qt.darker(palette.window, 1.05) :
Qt.lighter(palette.window, 1.05)
border.width: 1
border.color: palette.mid
Behavior on implicitHeight {
NumberAnimation {
duration: 200
easing.type: Easing.InOutQuad
}
}
RowLayout {
anchors {
left: parent.left
leftMargin: 10
right: parent.right
rightMargin: 10
verticalCenter: parent.verticalCenter
}
spacing: 10
Rectangle {
Layout.preferredWidth: 24
Layout.preferredHeight: 24
radius: 12
color: {
if (root.hasPendingEdits) return Qt.rgba(0.2, 0.6, 1.0, 0.2)
if (root.hasAppliedEdits) return Qt.rgba(0.2, 0.8, 0.2, 0.2)
return Qt.rgba(0.8, 0.6, 0.2, 0.2)
}
border.width: 2
border.color: {
if (root.hasPendingEdits) return Qt.rgba(0.2, 0.6, 1.0, 0.8)
if (root.hasAppliedEdits) return Qt.rgba(0.2, 0.8, 0.2, 0.8)
return Qt.rgba(0.8, 0.6, 0.2, 0.8)
}
Text {
anchors.centerIn: parent
text: root.totalEdits
font.pixelSize: 10
font.bold: true
color: palette.text
}
}
// Status text
ColumnLayout {
spacing: 2
Text {
text: root.totalEdits === 1
? qsTr("File Edit in Current Message")
: qsTr("%1 File Edits in Current Message").arg(root.totalEdits)
font.pixelSize: 11
font.bold: true
color: palette.text
}
Text {
visible: root.totalEdits > 0
text: {
let parts = [];
if (root.appliedEdits > 0) {
parts.push(qsTr("%1 applied").arg(root.appliedEdits));
}
if (root.pendingEdits > 0) {
parts.push(qsTr("%1 pending").arg(root.pendingEdits));
}
if (root.rejectedEdits > 0) {
parts.push(qsTr("%1 rejected").arg(root.rejectedEdits));
}
return parts.join(", ");
}
font.pixelSize: 9
color: palette.mid
}
}
Item {
Layout.fillWidth: true
}
QoAButton {
id: applyAllButton
visible: root.hasPendingEdits || root.hasRejectedEdits
enabled: root.hasPendingEdits || root.hasRejectedEdits
text: root.hasPendingEdits
? qsTr("Apply All (%1)").arg(root.pendingEdits + root.rejectedEdits)
: qsTr("Reapply All (%1)").arg(root.rejectedEdits)
QoAToolTip {
visible: applyAllButton.hovered
delay: 250
text: root.hasPendingEdits
? qsTr("Apply all pending and rejected edits in this message")
: qsTr("Reapply all rejected edits in this message")
}
onClicked: root.applyAllClicked()
}
QoAButton {
id: undoAllButton
visible: root.hasAppliedEdits
enabled: root.hasAppliedEdits
text: qsTr("Undo All (%1)").arg(root.appliedEdits)
QoAToolTip {
visible: undoAllButton.hovered
delay: 250
text: qsTr("Undo all applied edits in this message")
}
onClicked: root.undoAllClicked()
}
}
}

View File

@@ -0,0 +1,152 @@
// Copyright (C) 2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
FileMentionItem {
id: root
signal selectionRequested()
visible: searchResults.length > 0
height: Math.min(searchResults.length * 36, 36 * 6) + 2
onCurrentIndexChanged: {
listView.positionViewAtIndex(root.currentIndex, ListView.Contain)
}
Rectangle {
id: background
anchors.fill: parent
color: palette.window
border.color: palette.mid
border.width: 1
radius: 4
}
ListView {
id: listView
anchors.fill: parent
anchors.margins: 1
model: root.searchResults
currentIndex: root.currentIndex
clip: true
ScrollBar.vertical: ScrollBar {
policy: ScrollBar.AsNeeded
}
delegate: Rectangle {
id: delegateItem
required property int index
required property var modelData
readonly property bool isProject: modelData.isProject === true
readonly property bool isOpen: modelData.isOpen === true
readonly property string fileName: {
if (isProject)
return modelData.projectName
const parts = modelData.relativePath.split('/')
return parts[parts.length - 1]
}
width: listView.width
height: 36
color: index === root.currentIndex
? palette.highlight
: (hoverArea.containsMouse
? Qt.rgba(palette.highlight.r, palette.highlight.g, palette.highlight.b, 0.25)
: "transparent")
RowLayout {
anchors.fill: parent
anchors.leftMargin: 10
anchors.rightMargin: 10
spacing: 8
Item {
Layout.preferredWidth: 18
Layout.preferredHeight: 18
Rectangle {
anchors.fill: parent
radius: 3
visible: delegateItem.isProject || delegateItem.isOpen
color: {
if (delegateItem.index === root.currentIndex)
return Qt.rgba(palette.highlightedText.r,
palette.highlightedText.g,
palette.highlightedText.b, 0.2)
if (delegateItem.isProject)
return Qt.rgba(palette.highlight.r,
palette.highlight.g,
palette.highlight.b, 0.3)
return Qt.rgba(0.2, 0.7, 0.4, 0.3)
}
Text {
anchors.centerIn: parent
text: delegateItem.isProject ? "P" : "O"
font.bold: true
font.pixelSize: 10
color: {
if (delegateItem.index === root.currentIndex)
return palette.highlightedText
if (delegateItem.isProject)
return palette.highlight
return Qt.rgba(0.1, 0.6, 0.3, 1.0)
}
}
}
}
Text {
Layout.preferredWidth: 160
text: delegateItem.fileName
color: delegateItem.index === root.currentIndex
? palette.highlightedText
: (delegateItem.isProject ? palette.highlight : palette.text)
font.bold: true
font.italic: delegateItem.isProject
elide: Text.ElideRight
}
Text {
Layout.fillWidth: true
text: delegateItem.isProject
? "→"
: (delegateItem.modelData.projectName + " / " + delegateItem.modelData.relativePath)
color: delegateItem.index === root.currentIndex
? (delegateItem.isProject
? palette.highlightedText
: Qt.rgba(palette.highlightedText.r,
palette.highlightedText.g,
palette.highlightedText.b, 0.7))
: palette.mid
font.pixelSize: delegateItem.isProject ? 12 : 11
elide: Text.ElideLeft
horizontalAlignment: delegateItem.isProject ? Text.AlignLeft : Text.AlignRight
}
}
MouseArea {
id: hoverArea
anchors.fill: parent
hoverEnabled: true
onClicked: {
root.currentIndex = delegateItem.index
root.selectionRequested()
}
onEntered: root.currentIndex = delegateItem.index
}
}
}
}

View File

@@ -0,0 +1,188 @@
// Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
import QtQuick
import QtQuick.Controls
import ChatView
import UIControls
Item {
id: nav
property var chatModel
property var entries: []
property color dotColor: "#92BD6C"
property int currentMessageIndex: -1
readonly property int dotCount: entries.length
readonly property int verticalPadding: 8
readonly property int minDotSpacing: 18
readonly property real availableHeight: Math.max(0, height - 2 * verticalPadding)
readonly property real naturalHeight: dotCount > 1 ? (dotCount - 1) * minDotSpacing : 0
readonly property bool needsScrolling: naturalHeight > availableHeight
readonly property real contentHeight: needsScrolling
? naturalHeight + 2 * verticalPadding
: Math.max(height, 2 * verticalPadding)
signal messageClicked(int messageIndex)
implicitWidth: 16
function rebuild() {
entries = chatModel ? chatModel.userMessagePreviews(80) : []
Qt.callLater(scrollCurrentIntoView)
}
function updateCurrentFromModelIndex(modelIdx) {
if (modelIdx < 0) {
currentMessageIndex = -1
return
}
let best = -1
for (let i = 0; i < entries.length; ++i) {
const e = entries[i]
if (!e)
continue
const mi = e.messageIndex
if (mi <= modelIdx)
best = mi
else
break
}
currentMessageIndex = best
}
function uiIndexOf(messageIndex) {
for (let i = 0; i < entries.length; ++i) {
const e = entries[i]
if (e && e.messageIndex === messageIndex)
return i
}
return -1
}
function dotCenterY(uiIndex) {
const count = dotCount
if (count <= 1)
return contentHeight / 2
const spacing = needsScrolling
? minDotSpacing
: availableHeight / (count - 1)
return verticalPadding + spacing * uiIndex
}
function scrollCurrentIntoView() {
if (!needsScrolling || currentMessageIndex < 0)
return
const ui = uiIndexOf(currentMessageIndex)
if (ui < 0)
return
const y = dotCenterY(ui)
const margin = 24
if (y < flick.contentY + margin)
flick.contentY = Math.max(0, y - margin)
else if (y > flick.contentY + flick.height - margin)
flick.contentY = Math.min(
Math.max(0, flick.contentHeight - flick.height),
y - flick.height + margin)
}
onChatModelChanged: rebuild()
onCurrentMessageIndexChanged: scrollCurrentIntoView()
Component.onCompleted: rebuild()
Connections {
target: nav.chatModel
ignoreUnknownSignals: true
function onRowsInserted() { nav.rebuild() }
function onRowsRemoved() { nav.rebuild() }
function onModelReset() { nav.rebuild() }
function onModelReseted() { nav.rebuild() }
function onDataChanged() { nav.rebuild() }
}
Flickable {
id: flick
anchors.fill: parent
contentWidth: width
contentHeight: nav.contentHeight
interactive: nav.needsScrolling
clip: true
boundsBehavior: Flickable.StopAtBounds
Rectangle {
id: spine
visible: nav.dotCount > 1
anchors.horizontalCenter: parent.horizontalCenter
y: nav.verticalPadding
width: 1
height: Math.max(0, flick.contentHeight - 2 * nav.verticalPadding)
color: palette.mid
opacity: 0.4
}
Repeater {
model: nav.entries
delegate: Item {
id: dotItem
required property var modelData
required property int index
readonly property int msgIndex: modelData && modelData.messageIndex !== undefined
? modelData.messageIndex : -1
readonly property string preview: modelData && modelData.preview !== undefined
? modelData.preview : ""
readonly property bool isCurrent: nav.currentMessageIndex === msgIndex
width: 16
height: 14
anchors.horizontalCenter: parent.horizontalCenter
y: nav.dotCenterY(index) - height / 2
Rectangle {
id: dot
anchors.centerIn: parent
width: dotItem.isCurrent ? 11 : (dotArea.containsMouse ? 10 : 7)
height: width
radius: width / 2
color: dotArea.containsMouse
? Qt.lighter(nav.dotColor, 1.2)
: nav.dotColor
border.color: dotItem.isCurrent
? Qt.darker(nav.dotColor, 1.7)
: Qt.darker(nav.dotColor, 1.4)
border.width: dotItem.isCurrent ? 2 : 1
opacity: dotItem.isCurrent || dotArea.containsMouse ? 1.0 : 0.55
Behavior on width { NumberAnimation { duration: 120 } }
Behavior on opacity { NumberAnimation { duration: 120 } }
Behavior on color { ColorAnimation { duration: 120 } }
}
MouseArea {
id: dotArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: nav.messageClicked(dotItem.msgIndex)
QoAToolTip {
visible: dotArea.containsMouse
delay: 350
text: dotItem.preview.length > 0
? qsTr("#%1 · %2").arg(dotItem.index + 1).arg(dotItem.preview)
: qsTr("Jump to message #%1").arg(dotItem.index + 1)
}
}
}
}
}
}

View File

@@ -0,0 +1,126 @@
// Copyright (C) 2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
Rectangle {
id: root
// Object exposing Q_INVOKABLE QVariantList searchSkills(query).
property var skillProvider: null
property var searchResults: []
property int currentIndex: 0
signal selectionRequested()
visible: searchResults.length > 0
height: Math.min(searchResults.length * 40, 40 * 6) + 2
color: palette.window
border.color: palette.mid
border.width: 1
radius: 4
function updateSearch(query) {
searchResults = skillProvider ? skillProvider.searchSkills(query) : []
currentIndex = 0
}
function dismiss() {
searchResults = []
currentIndex = 0
}
function moveUp() {
if (currentIndex > 0)
currentIndex--
}
function moveDown() {
if (currentIndex < searchResults.length - 1)
currentIndex++
}
function currentName() {
if (currentIndex >= 0 && currentIndex < searchResults.length)
return searchResults[currentIndex].name
return ""
}
onCurrentIndexChanged: listView.positionViewAtIndex(currentIndex, ListView.Contain)
ListView {
id: listView
anchors.fill: parent
anchors.margins: 1
model: root.searchResults
currentIndex: root.currentIndex
clip: true
ScrollBar.vertical: ScrollBar {
policy: ScrollBar.AsNeeded
}
delegate: Rectangle {
id: delegateItem
required property int index
required property var modelData
width: listView.width
height: 40
color: index === root.currentIndex
? palette.highlight
: (hoverArea.containsMouse
? Qt.rgba(palette.highlight.r, palette.highlight.g, palette.highlight.b, 0.25)
: "transparent")
ColumnLayout {
anchors.fill: parent
anchors.leftMargin: 10
anchors.rightMargin: 10
anchors.topMargin: 4
anchors.bottomMargin: 4
spacing: 1
Text {
Layout.fillWidth: true
text: "/" + delegateItem.modelData.name
color: delegateItem.index === root.currentIndex
? palette.highlightedText
: palette.text
font.bold: true
elide: Text.ElideRight
}
Text {
Layout.fillWidth: true
text: delegateItem.modelData.description
color: delegateItem.index === root.currentIndex
? Qt.rgba(palette.highlightedText.r,
palette.highlightedText.g,
palette.highlightedText.b, 0.7)
: palette.mid
font.pixelSize: 11
elide: Text.ElideRight
}
}
MouseArea {
id: hoverArea
anchors.fill: parent
hoverEnabled: true
onClicked: {
root.currentIndex = delegateItem.index
root.selectionRequested()
}
onEntered: root.currentIndex = delegateItem.index
}
}
}
}

View File

@@ -0,0 +1,276 @@
// Copyright (C) 2025-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
import QtQuick
import QtQuick.Controls
Item {
id: root
signal filesDroppedToAttach(var urlStrings)
signal filesDroppedToLink(var urlStrings)
property string activeZone: ""
property int filesCount: 0
property bool isDragActive: false
Item {
id: splitDropOverlay
anchors.fill: parent
visible: false
z: 999
opacity: 0
Behavior on opacity {
NumberAnimation { duration: 200; easing.type: Easing.InOutQuad }
}
Rectangle {
anchors.fill: parent
color: Qt.rgba(palette.shadow.r, palette.shadow.g, palette.shadow.b, 0.6)
}
Rectangle {
anchors {
top: parent.top
horizontalCenter: parent.horizontalCenter
topMargin: 30
}
width: fileCountText.width + 40
height: 50
color: Qt.rgba(palette.highlight.r, palette.highlight.g, palette.highlight.b, 0.9)
radius: 25
visible: root.filesCount > 0
Text {
id: fileCountText
anchors.centerIn: parent
text: qsTr("%n file(s) to drop", "", root.filesCount)
font.pixelSize: 16
font.bold: true
color: palette.highlightedText
}
}
Rectangle {
id: leftZone
anchors {
left: parent.left
top: parent.top
bottom: parent.bottom
}
width: parent.width / 2
color: root.activeZone === "left"
? Qt.rgba(palette.highlight.r, palette.highlight.g, palette.highlight.b, 0.3)
: Qt.rgba(palette.mid.r, palette.mid.g, palette.mid.b, 0.15)
border.width: root.activeZone === "left" ? 3 : 2
border.color: root.activeZone === "left"
? palette.highlight
: Qt.rgba(palette.mid.r, palette.mid.g, palette.mid.b, 0.5)
Column {
anchors.centerIn: parent
spacing: 15
Text {
anchors.horizontalCenter: parent.horizontalCenter
text: qsTr("Attach")
font.pixelSize: 24
font.bold: true
color: root.activeZone === "left" ? palette.highlightedText : palette.text
}
Text {
anchors.horizontalCenter: parent.horizontalCenter
text: qsTr("Images & Text Files")
font.pixelSize: 14
color: root.activeZone === "left" ? palette.highlightedText : palette.text
opacity: 0.8
}
Text {
anchors.horizontalCenter: parent.horizontalCenter
text: qsTr("(for one-time use)")
font.pixelSize: 12
font.italic: true
color: root.activeZone === "left" ? palette.highlightedText : palette.text
opacity: 0.6
}
}
Behavior on color { ColorAnimation { duration: 150 } }
Behavior on border.width { NumberAnimation { duration: 150 } }
Behavior on border.color { ColorAnimation { duration: 150 } }
}
Rectangle {
id: rightZone
anchors {
right: parent.right
top: parent.top
bottom: parent.bottom
}
width: parent.width / 2
color: root.activeZone === "right"
? Qt.rgba(palette.highlight.r, palette.highlight.g, palette.highlight.b, 0.3)
: Qt.rgba(palette.mid.r, palette.mid.g, palette.mid.b, 0.15)
border.width: root.activeZone === "right" ? 3 : 2
border.color: root.activeZone === "right"
? palette.highlight
: Qt.rgba(palette.mid.r, palette.mid.g, palette.mid.b, 0.5)
Column {
anchors.centerIn: parent
spacing: 15
Text {
anchors.horizontalCenter: parent.horizontalCenter
text: qsTr("LINK")
font.pixelSize: 24
font.bold: true
color: root.activeZone === "right" ? palette.highlightedText : palette.text
}
Text {
anchors.horizontalCenter: parent.horizontalCenter
text: qsTr("Text Files")
font.pixelSize: 14
color: root.activeZone === "right" ? palette.highlightedText : palette.text
opacity: 0.8
}
Text {
anchors.horizontalCenter: parent.horizontalCenter
text: qsTr("(added to context)")
font.pixelSize: 12
font.italic: true
color: root.activeZone === "right" ? palette.highlightedText : palette.text
opacity: 0.6
}
}
Behavior on color { ColorAnimation { duration: 150 } }
Behavior on border.width { NumberAnimation { duration: 150 } }
Behavior on border.color { ColorAnimation { duration: 150 } }
}
Rectangle {
anchors {
horizontalCenter: parent.horizontalCenter
top: parent.top
bottom: parent.bottom
}
width: 2
color: palette.mid
opacity: 0.4
}
MouseArea {
id: leftDropArea
anchors {
left: parent.left
top: parent.top
bottom: parent.bottom
}
width: parent.width / 2
hoverEnabled: true
onEntered: {
root.activeZone = "left"
}
}
MouseArea {
id: rightDropArea
anchors {
right: parent.right
top: parent.top
bottom: parent.bottom
}
width: parent.width / 2
hoverEnabled: true
onEntered: {
root.activeZone = "right"
}
}
}
DropArea {
id: globalDropArea
anchors.fill: parent
onEntered: (drag) => {
if (drag.hasUrls) {
root.isDragActive = true
root.filesCount = drag.urls.length
splitDropOverlay.visible = true
splitDropOverlay.opacity = 1
root.activeZone = ""
}
}
onExited: {
root.isDragActive = false
root.filesCount = 0
splitDropOverlay.opacity = 0
Qt.callLater(function() {
if (!root.isDragActive) {
splitDropOverlay.visible = false
root.activeZone = ""
}
})
}
onPositionChanged: (drag) => {
if (drag.hasUrls) {
root.activeZone = drag.x < globalDropArea.width / 2 ? "left" : "right"
}
}
onDropped: (drop) => {
const targetZone = root.activeZone
root.isDragActive = false
root.filesCount = 0
splitDropOverlay.opacity = 0
Qt.callLater(function() {
splitDropOverlay.visible = false
root.activeZone = ""
})
if (!drop.hasUrls || drop.urls.length === 0) {
return
}
var urlStrings = []
for (var i = 0; i < drop.urls.length; i++) {
var urlString = drop.urls[i].toString()
if (urlString.startsWith("file://") || urlString.indexOf("://") === -1) {
urlStrings.push(urlString)
}
}
if (urlStrings.length === 0) {
return
}
drop.accept(Qt.CopyAction)
if (targetZone === "right") {
root.filesDroppedToLink(urlStrings)
} else {
root.filesDroppedToAttach(urlStrings)
}
}
}
}

View File

@@ -0,0 +1,88 @@
// Copyright (C) 2025-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
import QtQuick
Rectangle {
id: root
property alias toastTextItem: textItem
property alias toastTextColor: textItem.color
property string errorText: ""
property int displayDuration: 7000
width: Math.min(parent.width - 40, textItem.implicitWidth + radius)
height: visible ? (textItem.implicitHeight + 12) : 0
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top
anchors.topMargin: 10
color: "#d32f2f"
radius: height / 2
border.color: "#b71c1c"
border.width: 1
visible: false
opacity: 0
TextEdit {
id: textItem
anchors.centerIn: parent
anchors.margins: 6
text: root.errorText
color: palette.text
font.pixelSize: 13
wrapMode: TextEdit.Wrap
width: Math.min(implicitWidth, root.parent.width - 60)
horizontalAlignment: TextEdit.AlignHCenter
readOnly: true
selectByMouse: true
selectByKeyboard: true
selectionColor: "#b71c1c"
}
function show(message) {
errorText = message
visible = true
showAnimation.start()
hideTimer.restart()
}
function hide() {
hideAnimation.start()
}
NumberAnimation {
id: showAnimation
target: root
property: "opacity"
from: 0
to: 1
duration: 200
easing.type: Easing.OutQuad
}
NumberAnimation {
id: hideAnimation
target: root
property: "opacity"
from: 1
to: 0
duration: 200
easing.type: Easing.InQuad
onFinished: root.visible = false
}
Timer {
id: hideTimer
interval: root.displayDuration
running: false
repeat: false
onTriggered: root.hide()
}
}

View File

@@ -0,0 +1,361 @@
// Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
import ChatView
import UIControls
Rectangle {
id: root
property bool isInEditor: false
property alias saveButton: saveButtonId
property alias loadButton: loadButtonId
property alias clearButton: clearButtonId
property alias newChatButton: newChatButtonId
property alias tokensBadge: tokensBadgeId
property alias recentPath: recentPathId
property alias openChatHistory: openChatHistoryId
property alias pinButton: pinButtonId
property alias relocateButton: relocateButtonId
property alias contextButton: contextButtonId
property alias toolsButton: toolsButtonId
property alias thinkingMode: thinkingModeId
property alias settingsButton: settingsButtonId
property alias configSelector: configSelectorId
property alias roleSelector: roleSelector
property alias relocateTooltip: relocateTooltipId
color: palette.window.hslLightness > 0.5 ?
Qt.darker(palette.window, 1.1) :
Qt.lighter(palette.window, 1.1)
Flow {
anchors {
left: parent.left
right: parent.right
verticalCenter: parent.verticalCenter
margins: 5
}
spacing: 10
Row {
id: firstRow
spacing: 10
QoAButton {
id: pinButtonId
anchors.verticalCenter: parent.verticalCenter
checkable: true
icon {
source: checked ? "qrc:/qt/qml/ChatView/icons/window-lock.svg"
: "qrc:/qt/qml/ChatView/icons/window-unlock.svg"
color: palette.window.hslLightness > 0.5 ? "#000000" : "#FFFFFF"
height: 15
width: 15
}
QoAToolTip {
visible: pinButtonId.hovered
delay: 250
text: pinButtonId.checked ? qsTr("Unpin chat window")
: qsTr("Pin chat window to the top")
}
}
QoAButton {
id: relocateButtonId
anchors.verticalCenter: parent.verticalCenter
icon {
source: "qrc:/qt/qml/ChatView/icons/open-in-editor.svg"
color: palette.window.hslLightness > 0.5 ? "#000000" : "#FFFFFF"
height: 15
width: 15
}
QoAToolTip {
id: relocateTooltipId
visible: relocateButtonId.hovered
delay: 250
}
}
QoASeparator {
anchors.verticalCenter: parent.verticalCenter
}
QoAButton {
id: clearButtonId
icon {
source: "qrc:/qt/qml/ChatView/icons/clean-icon-dark.svg"
height: 15
width: 8
}
QoAToolTip {
visible: clearButtonId.hovered
delay: 250
text: qsTr("Clean chat")
}
}
QoASeparator {
anchors.verticalCenter: parent.verticalCenter
}
QoAButton {
id: newChatButtonId
visible: root.isInEditor
icon {
source: "qrc:/qt/qml/ChatView/icons/new-chat-icon.svg"
color: palette.window.hslLightness > 0.5 ? "#000000" : "#FFFFFF"
height: 15
width: 15
}
QoAToolTip {
visible: newChatButtonId.hovered
delay: 250
text: qsTr("Open new chat in a new tab")
}
}
QoAComboBox {
id: configSelectorId
implicitHeight: 25
model: []
currentIndex: 0
QoAToolTip {
visible: configSelectorId.hovered
delay: 250
text: qsTr("Switch saved AI configuration")
}
}
QoAComboBox {
id: roleSelector
implicitHeight: 25
model: []
currentIndex: 0
QoAToolTip {
visible: roleSelector.hovered
delay: 250
text: qsTr("Switch agent role (different system prompts)")
}
}
}
Row {
spacing: 10
QoAButton {
id: toolsButtonId
anchors.verticalCenter: parent.verticalCenter
checkable: true
opacity: enabled ? 1.0 : 0.2
icon {
source: checked ? "qrc:/qt/qml/ChatView/icons/tools-icon-on.svg"
: "qrc:/qt/qml/ChatView/icons/tools-icon-off.svg"
color: palette.window.hslLightness > 0.5 ? "#000000" : "#FFFFFF"
height: 15
width: 15
}
QoAToolTip {
visible: toolsButtonId.hovered
delay: 250
text: {
if (!toolsButtonId.enabled) {
return qsTr("Tools are disabled in General Settings")
}
return toolsButtonId.checked
? qsTr("Tools enabled: AI can use tools to read files, search project, and build code")
: qsTr("Tools disabled: Simple conversation without tool access")
}
}
}
QoAButton {
id: thinkingModeId
anchors.verticalCenter: parent.verticalCenter
checkable: true
opacity: enabled ? 1.0 : 0.2
icon {
source: checked ? "qrc:/qt/qml/ChatView/icons/thinking-icon-on.svg"
: "qrc:/qt/qml/ChatView/icons/thinking-icon-off.svg"
color: palette.window.hslLightness > 0.5 ? "#000000" : "#FFFFFF"
height: 15
width: 15
}
QoAToolTip {
visible: thinkingModeId.hovered
delay: 250
text: thinkingModeId.enabled
? (thinkingModeId.checked ? qsTr("Thinking Mode enabled (Check model list support it)")
: qsTr("Thinking Mode disabled"))
: qsTr("Thinking Mode is not available for this provider")
}
}
QoAButton {
id: settingsButtonId
anchors.verticalCenter: parent.verticalCenter
icon {
source: "qrc:/qt/qml/ChatView/icons/settings-icon.svg"
color: palette.window.hslLightness > 0.5 ? "#000000" : "#FFFFFF"
height: 15
width: 15
}
QoAToolTip {
visible: settingsButtonId.hovered
delay: 250
text: qsTr("Open Chat Assistant Settings")
}
}
QoASeparator {
anchors.verticalCenter: parent.verticalCenter
}
}
Item {
height: firstRow.height
width: recentPathId.width
Text {
id: recentPathId
anchors.verticalCenter: parent.verticalCenter
width: Math.min(implicitWidth, root.width)
elide: Text.ElideMiddle
color: palette.text
font.pixelSize: 12
MouseArea {
anchors.fill: parent
hoverEnabled: true
QoAToolTip {
visible: parent.containsMouse && recentPathId.text.length > 0
text: recentPathId.text
delay: 500
}
}
}
}
RowLayout {
id: secondRow
Layout.preferredWidth: root.width
Layout.preferredHeight: firstRow.height
spacing: 10
QoAButton {
id: saveButtonId
icon {
source: "qrc:/qt/qml/ChatView/icons/save-chat-dark.svg"
height: 15
width: 8
}
QoAToolTip {
visible: saveButtonId.hovered
delay: 250
text: qsTr("Save chat to *.json file")
}
}
QoAButton {
id: loadButtonId
icon {
source: "qrc:/qt/qml/ChatView/icons/load-chat-dark.svg"
height: 15
width: 8
}
QoAToolTip {
visible: loadButtonId.hovered
delay: 250
text: qsTr("Load chat from *.json file")
}
}
QoAButton {
id: openChatHistoryId
icon {
source: "qrc:/qt/qml/ChatView/icons/file-in-system.svg"
height: 15
width: 15
}
QoAToolTip {
visible: openChatHistoryId.hovered
delay: 250
text: qsTr("Show in system")
}
}
QoASeparator {}
QoAButton {
id: contextButtonId
icon {
source: "qrc:/qt/qml/ChatView/icons/context-icon.svg"
color: palette.window.hslLightness > 0.5 ? "#000000" : "#FFFFFF"
height: 15
width: 15
}
QoAToolTip {
visible: contextButtonId.hovered
delay: 250
text: qsTr("View chat context (system prompt, role, rules)")
}
}
Badge {
id: tokensBadgeId
ToolTip.visible: hovered
ToolTip.delay: 250
ToolTip.text: qsTr("Current amount tokens in chat and LLM limit threshold")
}
}
}
}