mirror of
https://github.com/Palm1r/QodeAssist.git
synced 2026-07-23 19:51:05 -04:00
refactor: Restructure project into sources/tests layout and dissolve PluginLLMCore
This commit is contained in:
147
sources/ChatView/qml/controls/AttachedFilesPlace.qml
Normal file
147
sources/ChatView/qml/controls/AttachedFilesPlace.qml
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
190
sources/ChatView/qml/controls/BottomBar.qml
Normal file
190
sources/ChatView/qml/controls/BottomBar.qml
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
543
sources/ChatView/qml/controls/ContextViewer.qml
Normal file
543
sources/ChatView/qml/controls/ContextViewer.qml
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
150
sources/ChatView/qml/controls/FileEditsActionBar.qml
Normal file
150
sources/ChatView/qml/controls/FileEditsActionBar.qml
Normal 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
152
sources/ChatView/qml/controls/FileMentionPopup.qml
Normal file
152
sources/ChatView/qml/controls/FileMentionPopup.qml
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
188
sources/ChatView/qml/controls/MessageNavigator.qml
Normal file
188
sources/ChatView/qml/controls/MessageNavigator.qml
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
126
sources/ChatView/qml/controls/SkillCommandPopup.qml
Normal file
126
sources/ChatView/qml/controls/SkillCommandPopup.qml
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
276
sources/ChatView/qml/controls/SplitDropZone.qml
Normal file
276
sources/ChatView/qml/controls/SplitDropZone.qml
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
88
sources/ChatView/qml/controls/Toast.qml
Normal file
88
sources/ChatView/qml/controls/Toast.qml
Normal 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()
|
||||
}
|
||||
}
|
||||
361
sources/ChatView/qml/controls/TopBar.qml
Normal file
361
sources/ChatView/qml/controls/TopBar.qml
Normal 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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user