mirror of
https://github.com/Palm1r/QodeAssist.git
synced 2026-02-21 22:43:06 -05:00
feat: Add open in editor and remove from list (#267)
* refactor: Rename and move chat items * feat: Add hotkeys for open in editor and remove file from list * feat: Add opening by system * feat: Add context action menu
This commit is contained in:
218
ChatView/qml/chatparts/ChatItem.qml
Normal file
218
ChatView/qml/chatparts/ChatItem.qml
Normal file
@ -0,0 +1,218 @@
|
||||
/*
|
||||
* Copyright (C) 2024-2025 Petr Mironychev
|
||||
*
|
||||
* This file is part of QodeAssist.
|
||||
*
|
||||
* QodeAssist is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* QodeAssist is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import QtQuick
|
||||
import ChatView
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import UIControls
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
property alias msgModel: msgCreator.model
|
||||
property alias messageAttachments: attachmentsModel.model
|
||||
property string textFontFamily: Qt.application.font.family
|
||||
property string codeFontFamily: {
|
||||
switch (Qt.platform.os) {
|
||||
case "windows":
|
||||
return "Consolas";
|
||||
case "osx":
|
||||
return "Menlo";
|
||||
case "linux":
|
||||
return "DejaVu Sans Mono";
|
||||
default:
|
||||
return "monospace";
|
||||
}
|
||||
}
|
||||
property int textFontSize: Qt.application.font.pointSize
|
||||
property int codeFontSize: Qt.application.font.pointSize
|
||||
property int textFormat: 0
|
||||
|
||||
property bool isUserMessage: false
|
||||
property int messageIndex: -1
|
||||
|
||||
signal resetChatToMessage(int index)
|
||||
|
||||
height: msgColumn.implicitHeight + 10
|
||||
radius: 8
|
||||
color: isUserMessage ? palette.alternateBase
|
||||
: palette.base
|
||||
|
||||
HoverHandler {
|
||||
id: mouse
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: msgColumn
|
||||
|
||||
x: 5
|
||||
width: parent.width - x
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: 5
|
||||
|
||||
Repeater {
|
||||
id: msgCreator
|
||||
delegate: Loader {
|
||||
id: msgCreatorDelegate
|
||||
// Fix me:
|
||||
// why does `required property MessagePart modelData` not work?
|
||||
required property var modelData
|
||||
|
||||
Layout.preferredWidth: root.width
|
||||
sourceComponent: {
|
||||
// If `required property MessagePart modelData` is used
|
||||
// and conversion to MessagePart fails, you're left
|
||||
// with a nullptr. This tests that to prevent crashing.
|
||||
if(!modelData) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
switch(modelData.type) {
|
||||
case MessagePartType.Text: return textComponent;
|
||||
case MessagePartType.Code: return codeBlockComponent;
|
||||
default: return textComponent;
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: textComponent
|
||||
TextComponent {
|
||||
itemData: msgCreatorDelegate.modelData
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: codeBlockComponent
|
||||
CodeBlockComponent {
|
||||
itemData: msgCreatorDelegate.modelData
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Flow {
|
||||
id: attachmentsFlow
|
||||
|
||||
Layout.fillWidth: true
|
||||
visible: attachmentsModel.model && attachmentsModel.model.length > 0
|
||||
leftPadding: 10
|
||||
rightPadding: 10
|
||||
spacing: 5
|
||||
|
||||
Repeater {
|
||||
id: attachmentsModel
|
||||
|
||||
delegate: Rectangle {
|
||||
required property int index
|
||||
required property var modelData
|
||||
|
||||
height: attachText.implicitHeight + 8
|
||||
width: attachText.implicitWidth + 16
|
||||
radius: 4
|
||||
color: palette.button
|
||||
border.width: 1
|
||||
border.color: palette.mid
|
||||
|
||||
Text {
|
||||
id: attachText
|
||||
|
||||
anchors.centerIn: parent
|
||||
text: modelData
|
||||
color: palette.text
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: userMessageMarker
|
||||
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: 3
|
||||
height: root.height - root.radius
|
||||
color: "#92BD6C"
|
||||
radius: root.radius
|
||||
visible: root.isUserMessage
|
||||
}
|
||||
|
||||
QoAButton {
|
||||
id: stopButtonId
|
||||
|
||||
anchors {
|
||||
right: parent.right
|
||||
top: parent.top
|
||||
}
|
||||
|
||||
icon {
|
||||
source: "qrc:/qt/qml/ChatView/icons/undo-changes-button.svg"
|
||||
height: 15
|
||||
width: 15
|
||||
}
|
||||
visible: root.isUserMessage && mouse.hovered
|
||||
onClicked: function() {
|
||||
root.resetChatToMessage(root.messageIndex)
|
||||
}
|
||||
ToolTip.visible: hovered
|
||||
ToolTip.text: qsTr("Reset chat to this message and edit")
|
||||
ToolTip.delay: 500
|
||||
}
|
||||
|
||||
component TextComponent : TextBlock {
|
||||
required property var itemData
|
||||
height: implicitHeight + 10
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
leftPadding: 10
|
||||
text: textFormat == Text.MarkdownText ? utils.getSafeMarkdownText(itemData.text)
|
||||
: itemData.text
|
||||
font.family: root.textFontFamily
|
||||
font.pointSize: root.textFontSize
|
||||
textFormat: {
|
||||
if (root.textFormat == 0) {
|
||||
return Text.MarkdownText
|
||||
} else if (root.textFormat == 1) {
|
||||
return Text.RichText
|
||||
} else {
|
||||
return Text.PlainText
|
||||
}
|
||||
}
|
||||
|
||||
ChatUtils {
|
||||
id: utils
|
||||
}
|
||||
}
|
||||
|
||||
component CodeBlockComponent : CodeBlock {
|
||||
id: codeblock
|
||||
|
||||
required property var itemData
|
||||
anchors {
|
||||
left: parent.left
|
||||
leftMargin: 10
|
||||
right: parent.right
|
||||
rightMargin: 10
|
||||
}
|
||||
|
||||
code: itemData.text
|
||||
language: itemData.language
|
||||
codeFontFamily: root.codeFontFamily
|
||||
codeFontSize: root.codeFontSize
|
||||
}
|
||||
}
|
||||
173
ChatView/qml/chatparts/CodeBlock.qml
Normal file
173
ChatView/qml/chatparts/CodeBlock.qml
Normal file
@ -0,0 +1,173 @@
|
||||
/*
|
||||
* Copyright (C) 2024-2025 Petr Mironychev
|
||||
*
|
||||
* This file is part of QodeAssist.
|
||||
*
|
||||
* QodeAssist is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* QodeAssist is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import ChatView
|
||||
import UIControls
|
||||
import Qt.labs.platform as Platform
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
property string code: ""
|
||||
property string language: ""
|
||||
property bool expanded: false
|
||||
|
||||
property alias codeFontFamily: codeText.font.family
|
||||
property alias codeFontSize: codeText.font.pointSize
|
||||
readonly property real collapsedHeight: copyButton.height + 10
|
||||
|
||||
color: palette.alternateBase
|
||||
border.color: root.color.hslLightness > 0.5 ? Qt.darker(root.color, 1.3)
|
||||
: Qt.lighter(root.color, 1.3)
|
||||
border.width: 2
|
||||
radius: 4
|
||||
implicitWidth: parent.width
|
||||
clip: true
|
||||
|
||||
Behavior on implicitHeight {
|
||||
NumberAnimation { duration: 200; easing.type: Easing.InOutQuad }
|
||||
}
|
||||
|
||||
ChatUtils {
|
||||
id: utils
|
||||
}
|
||||
|
||||
HoverHandler {
|
||||
id: hoverHandler
|
||||
enabled: true
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: header
|
||||
|
||||
width: parent.width
|
||||
height: root.collapsedHeight
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: root.expanded = !root.expanded
|
||||
|
||||
Row {
|
||||
id: headerRow
|
||||
|
||||
anchors {
|
||||
verticalCenter: parent.verticalCenter
|
||||
left: parent.left
|
||||
leftMargin: 10
|
||||
}
|
||||
spacing: 6
|
||||
|
||||
Text {
|
||||
text: root.language ? qsTr("Code (%1)").arg(root.language) :
|
||||
qsTr("Code")
|
||||
font.pixelSize: 12
|
||||
font.bold: true
|
||||
color: palette.text
|
||||
}
|
||||
|
||||
Text {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: root.expanded ? "▼" : "▶"
|
||||
font.pixelSize: 10
|
||||
color: palette.mid
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TextEdit {
|
||||
id: codeText
|
||||
|
||||
anchors {
|
||||
left: parent.left
|
||||
right: parent.right
|
||||
top: header.bottom
|
||||
margins: 10
|
||||
}
|
||||
text: root.code
|
||||
readOnly: true
|
||||
selectByMouse: true
|
||||
color: parent.color.hslLightness > 0.5 ? "black" : "white"
|
||||
wrapMode: Text.WordWrap
|
||||
selectionColor: palette.highlight
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
acceptedButtons: Qt.RightButton
|
||||
onClicked: contextMenu.open()
|
||||
}
|
||||
}
|
||||
|
||||
Platform.Menu {
|
||||
id: contextMenu
|
||||
|
||||
Platform.MenuItem {
|
||||
text: qsTr("Copy")
|
||||
onTriggered: {
|
||||
const textToCopy = codeText.selectedText || root.code
|
||||
utils.copyToClipboard(textToCopy)
|
||||
}
|
||||
}
|
||||
|
||||
Platform.MenuSeparator {}
|
||||
|
||||
Platform.MenuItem {
|
||||
text: root.expanded ? qsTr("Collapse") : qsTr("Expand")
|
||||
onTriggered: root.expanded = !root.expanded
|
||||
}
|
||||
}
|
||||
|
||||
QoAButton {
|
||||
id: copyButton
|
||||
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: 5
|
||||
|
||||
y: 5
|
||||
text: qsTr("Copy")
|
||||
|
||||
onClicked: {
|
||||
utils.copyToClipboard(root.code)
|
||||
text = qsTr("Copied")
|
||||
copyTimer.start()
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: copyTimer
|
||||
interval: 2000
|
||||
onTriggered: parent.text = qsTr("Copy")
|
||||
}
|
||||
}
|
||||
|
||||
states: [
|
||||
State {
|
||||
when: !root.expanded
|
||||
PropertyChanges {
|
||||
target: root
|
||||
implicitHeight: root.collapsedHeight
|
||||
}
|
||||
},
|
||||
State {
|
||||
when: root.expanded
|
||||
PropertyChanges {
|
||||
target: root
|
||||
implicitHeight: header.height + codeText.implicitHeight + 10
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
469
ChatView/qml/chatparts/FileEditBlock.qml
Normal file
469
ChatView/qml/chatparts/FileEditBlock.qml
Normal file
@ -0,0 +1,469 @@
|
||||
/*
|
||||
* Copyright (C) 2025 Petr Mironychev
|
||||
*
|
||||
* This file is part of QodeAssist.
|
||||
*
|
||||
* QodeAssist is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* QodeAssist is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import UIControls
|
||||
import ChatView
|
||||
import Qt.labs.platform as Platform
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
property string editContent: ""
|
||||
|
||||
readonly property var editData: parseEditData(editContent)
|
||||
readonly property string filePath: editData.file || ""
|
||||
readonly property string fileName: getFileName(filePath)
|
||||
readonly property string editStatus: editData.status || "pending"
|
||||
readonly property string statusMessage: editData.status_message || ""
|
||||
readonly property string oldContent: editData.old_content || ""
|
||||
readonly property string newContent: editData.new_content || ""
|
||||
|
||||
signal applyEdit(string editId)
|
||||
signal rejectEdit(string editId)
|
||||
signal undoEdit(string editId)
|
||||
signal openInEditor(string editId)
|
||||
|
||||
readonly property int borderRadius: 4
|
||||
readonly property int contentMargin: 10
|
||||
readonly property int contentBottomPadding: 20
|
||||
readonly property int headerPadding: 8
|
||||
readonly property int statusIndicatorWidth: 4
|
||||
|
||||
readonly property bool isPending: editStatus === "pending"
|
||||
readonly property bool isApplied: editStatus === "applied"
|
||||
readonly property bool isRejected: editStatus === "rejected"
|
||||
readonly property bool isArchived: editStatus === "archived"
|
||||
|
||||
readonly property color appliedColor: Qt.rgba(0.2, 0.8, 0.2, 0.8)
|
||||
readonly property color revertedColor: Qt.rgba(0.8, 0.6, 0.2, 0.8)
|
||||
readonly property color rejectedColor: Qt.rgba(0.8, 0.2, 0.2, 0.8)
|
||||
readonly property color archivedColor: Qt.rgba(0.5, 0.5, 0.5, 0.8)
|
||||
readonly property color pendingColor: palette.highlight
|
||||
|
||||
readonly property color appliedBgColor: Qt.rgba(0.2, 0.8, 0.2, 0.3)
|
||||
readonly property color revertedBgColor: Qt.rgba(0.8, 0.6, 0.2, 0.3)
|
||||
readonly property color rejectedBgColor: Qt.rgba(0.8, 0.2, 0.2, 0.3)
|
||||
readonly property color archivedBgColor: Qt.rgba(0.5, 0.5, 0.5, 0.3)
|
||||
|
||||
readonly property string codeFontFamily: {
|
||||
switch (Qt.platform.os) {
|
||||
case "windows": return "Consolas"
|
||||
case "osx": return "Menlo"
|
||||
case "linux": return "DejaVu Sans Mono"
|
||||
default: return "monospace"
|
||||
}
|
||||
}
|
||||
readonly property int codeFontSize: Qt.application.font.pointSize
|
||||
|
||||
readonly property color statusColor: {
|
||||
if (isArchived) return archivedColor
|
||||
if (isApplied) return appliedColor
|
||||
if (isRejected) return rejectedColor
|
||||
return pendingColor
|
||||
}
|
||||
|
||||
readonly property color statusBgColor: {
|
||||
if (isArchived) return archivedBgColor
|
||||
if (isApplied) return appliedBgColor
|
||||
if (isRejected) return rejectedBgColor
|
||||
return palette.button
|
||||
}
|
||||
|
||||
readonly property string statusText: {
|
||||
if (isArchived) return qsTr("ARCHIVED")
|
||||
if (isApplied) return qsTr("APPLIED")
|
||||
if (isRejected) return qsTr("REJECTED")
|
||||
return qsTr("PENDING")
|
||||
}
|
||||
|
||||
readonly property int addedLines: countLines(newContent)
|
||||
readonly property int removedLines: countLines(oldContent)
|
||||
|
||||
function parseEditData(content) {
|
||||
try {
|
||||
const marker = "QODEASSIST_FILE_EDIT:";
|
||||
let jsonStr = content;
|
||||
if (content.indexOf(marker) >= 0) {
|
||||
jsonStr = content.substring(content.indexOf(marker) + marker.length);
|
||||
}
|
||||
return JSON.parse(jsonStr);
|
||||
} catch (e) {
|
||||
return {
|
||||
edit_id: "",
|
||||
file: "",
|
||||
old_content: "",
|
||||
new_content: "",
|
||||
status: "error",
|
||||
status_message: ""
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function getFileName(path) {
|
||||
if (!path) return "";
|
||||
const parts = path.split('/');
|
||||
return parts[parts.length - 1];
|
||||
}
|
||||
|
||||
function countLines(text) {
|
||||
if (!text) return 0;
|
||||
return text.split('\n').length;
|
||||
}
|
||||
|
||||
implicitHeight: fileEditView.implicitHeight
|
||||
|
||||
ChatUtils {
|
||||
id: utils
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: fileEditView
|
||||
|
||||
property bool expanded: false
|
||||
|
||||
anchors.fill: parent
|
||||
implicitHeight: expanded ? headerArea.height + contentColumn.implicitHeight + root.contentBottomPadding + root.contentMargin * 2
|
||||
: headerArea.height
|
||||
radius: root.borderRadius
|
||||
|
||||
color: palette.base
|
||||
|
||||
border.width: 1
|
||||
border.color: root.isPending
|
||||
? (color.hslLightness > 0.5 ? Qt.darker(color, 1.3) : Qt.lighter(color, 1.3))
|
||||
: Qt.alpha(root.statusColor, 0.6)
|
||||
|
||||
clip: true
|
||||
|
||||
Behavior on implicitHeight {
|
||||
NumberAnimation {
|
||||
duration: 200
|
||||
easing.type: Easing.InOutQuad
|
||||
}
|
||||
}
|
||||
|
||||
states: [
|
||||
State {
|
||||
name: "expanded"
|
||||
when: fileEditView.expanded
|
||||
PropertyChanges { target: contentColumn; opacity: 1 }
|
||||
},
|
||||
State {
|
||||
name: "collapsed"
|
||||
when: !fileEditView.expanded
|
||||
PropertyChanges { target: contentColumn; opacity: 0 }
|
||||
}
|
||||
]
|
||||
|
||||
transitions: Transition {
|
||||
NumberAnimation {
|
||||
properties: "opacity"
|
||||
duration: 200
|
||||
easing.type: Easing.InOutQuad
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: headerArea
|
||||
|
||||
width: parent.width
|
||||
height: headerRow.height + 16
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: fileEditView.expanded = !fileEditView.expanded
|
||||
|
||||
RowLayout {
|
||||
id: headerRow
|
||||
|
||||
anchors {
|
||||
verticalCenter: parent.verticalCenter
|
||||
left: parent.left
|
||||
right: actionButtons.left
|
||||
leftMargin: root.contentMargin
|
||||
rightMargin: root.contentMargin
|
||||
}
|
||||
spacing: root.headerPadding
|
||||
|
||||
Rectangle {
|
||||
width: root.statusIndicatorWidth
|
||||
height: headerText.height
|
||||
radius: 2
|
||||
color: root.statusColor
|
||||
}
|
||||
|
||||
Text {
|
||||
id: headerText
|
||||
Layout.fillWidth: true
|
||||
text: {
|
||||
var modeText = root.oldContent.length > 0 ? qsTr("Replace") : qsTr("Append")
|
||||
if (root.oldContent.length > 0) {
|
||||
return qsTr("%1: %2 (+%3 -%4)")
|
||||
.arg(modeText)
|
||||
.arg(root.fileName)
|
||||
.arg(root.addedLines)
|
||||
.arg(root.removedLines)
|
||||
} else {
|
||||
return qsTr("%1: %2 (+%3)")
|
||||
.arg(modeText)
|
||||
.arg(root.fileName)
|
||||
.arg(root.addedLines)
|
||||
}
|
||||
}
|
||||
font.pixelSize: 12
|
||||
font.bold: true
|
||||
color: palette.text
|
||||
elide: Text.ElideMiddle
|
||||
}
|
||||
|
||||
Text {
|
||||
text: fileEditView.expanded ? "▼" : "▶"
|
||||
font.pixelSize: 10
|
||||
color: palette.mid
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
visible: !root.isPending
|
||||
Layout.preferredWidth: badgeText.width + 12
|
||||
Layout.preferredHeight: badgeText.height + 4
|
||||
color: root.statusBgColor
|
||||
radius: 3
|
||||
|
||||
Text {
|
||||
id: badgeText
|
||||
anchors.centerIn: parent
|
||||
text: root.statusText
|
||||
font.pixelSize: 9
|
||||
font.bold: true
|
||||
color: root.isArchived ? Qt.rgba(0.6, 0.6, 0.6, 1.0) : palette.text
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
id: actionButtons
|
||||
|
||||
anchors {
|
||||
right: parent.right
|
||||
rightMargin: 5
|
||||
verticalCenter: parent.verticalCenter
|
||||
}
|
||||
spacing: 6
|
||||
|
||||
QoAButton {
|
||||
icon {
|
||||
source: "qrc:/qt/qml/ChatView/icons/open-in-editor.svg"
|
||||
height: 15
|
||||
width: 15
|
||||
}
|
||||
hoverEnabled: true
|
||||
onClicked: root.openInEditor(editData.edit_id)
|
||||
|
||||
ToolTip.visible: hovered
|
||||
ToolTip.text: qsTr("Open file in editor and navigate to changes")
|
||||
ToolTip.delay: 500
|
||||
}
|
||||
|
||||
QoAButton {
|
||||
icon {
|
||||
source: "qrc:/qt/qml/ChatView/icons/apply-changes-button.svg"
|
||||
height: 15
|
||||
width: 15
|
||||
} enabled: (root.isPending || root.isRejected) && !root.isArchived
|
||||
visible: !root.isApplied && !root.isArchived
|
||||
onClicked: root.applyEdit(editData.edit_id)
|
||||
}
|
||||
|
||||
QoAButton {
|
||||
icon {
|
||||
source: "qrc:/qt/qml/ChatView/icons/undo-changes-button.svg"
|
||||
height: 15
|
||||
width: 15
|
||||
}
|
||||
enabled: root.isApplied && !root.isArchived
|
||||
visible: root.isApplied && !root.isArchived
|
||||
onClicked: root.undoEdit(editData.edit_id)
|
||||
}
|
||||
|
||||
QoAButton {
|
||||
icon {
|
||||
source: "qrc:/qt/qml/ChatView/icons/reject-changes-button.svg"
|
||||
height: 15
|
||||
width: 15
|
||||
}
|
||||
enabled: root.isPending && !root.isArchived
|
||||
visible: root.isPending && !root.isArchived
|
||||
onClicked: root.rejectEdit(editData.edit_id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: contentColumn
|
||||
|
||||
anchors {
|
||||
left: parent.left
|
||||
right: parent.right
|
||||
top: headerArea.bottom
|
||||
margins: root.contentMargin
|
||||
}
|
||||
spacing: 8
|
||||
visible: opacity > 0
|
||||
|
||||
Text {
|
||||
Layout.fillWidth: true
|
||||
text: root.filePath
|
||||
font.pixelSize: 10
|
||||
color: palette.mid
|
||||
elide: Text.ElideMiddle
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: oldContentColumn.implicitHeight + 12
|
||||
color: Qt.rgba(1, 0.2, 0.2, 0.1)
|
||||
radius: 4
|
||||
border.width: 1
|
||||
border.color: Qt.rgba(1, 0.2, 0.2, 0.3)
|
||||
visible: root.oldContent.length > 0
|
||||
|
||||
Column {
|
||||
id: oldContentColumn
|
||||
width: parent.width
|
||||
x: 6
|
||||
y: 6
|
||||
spacing: 4
|
||||
|
||||
TextEdit {
|
||||
id: oldContentText
|
||||
|
||||
width: parent.width - 12
|
||||
height: contentHeight
|
||||
text: root.oldContent
|
||||
font.family: root.codeFontFamily
|
||||
font.pixelSize: root.codeFontSize
|
||||
color: palette.text
|
||||
wrapMode: TextEdit.Wrap
|
||||
readOnly: true
|
||||
selectByMouse: true
|
||||
selectByKeyboard: true
|
||||
textFormat: TextEdit.PlainText
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
acceptedButtons: Qt.RightButton
|
||||
onClicked: oldConentContextMenu.open()
|
||||
}
|
||||
}
|
||||
|
||||
Platform.Menu {
|
||||
id: oldConentContextMenu
|
||||
|
||||
Platform.MenuItem {
|
||||
text: qsTr("Copy")
|
||||
onTriggered: {
|
||||
const textToCopy = oldContentText.selectedText || root.oldContent
|
||||
utils.copyToClipboard(textToCopy)
|
||||
}
|
||||
}
|
||||
|
||||
Platform.MenuSeparator {}
|
||||
|
||||
Platform.MenuItem {
|
||||
text: fileEditView.expanded ? qsTr("Collapse") : qsTr("Expand")
|
||||
onTriggered: fileEditView.expanded = !fileEditView.expanded
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: newContentColumn.implicitHeight + 12
|
||||
color: Qt.rgba(0.2, 0.8, 0.2, 0.1)
|
||||
radius: 4
|
||||
border.width: 1
|
||||
border.color: Qt.rgba(0.2, 0.8, 0.2, 0.3)
|
||||
|
||||
Column {
|
||||
id: newContentColumn
|
||||
|
||||
width: parent.width
|
||||
x: 6
|
||||
y: 6
|
||||
spacing: 4
|
||||
|
||||
TextEdit {
|
||||
id: newContentText
|
||||
|
||||
width: parent.width - 12
|
||||
height: contentHeight
|
||||
text: root.newContent
|
||||
font.family: root.codeFontFamily
|
||||
font.pixelSize: root.codeFontSize
|
||||
color: palette.text
|
||||
wrapMode: TextEdit.Wrap
|
||||
readOnly: true
|
||||
selectByMouse: true
|
||||
selectByKeyboard: true
|
||||
textFormat: TextEdit.PlainText
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
acceptedButtons: Qt.RightButton
|
||||
onClicked: newContentContextMenu.open()
|
||||
}
|
||||
}
|
||||
|
||||
Platform.Menu {
|
||||
id: newContentContextMenu
|
||||
|
||||
Platform.MenuItem {
|
||||
text: qsTr("Copy")
|
||||
onTriggered: {
|
||||
const textToCopy = newContentText.selectedText || root.newContent
|
||||
utils.copyToClipboard(textToCopy)
|
||||
}
|
||||
}
|
||||
|
||||
Platform.MenuSeparator {}
|
||||
|
||||
Platform.MenuItem {
|
||||
text: fileEditView.expanded ? qsTr("Collapse") : qsTr("Expand")
|
||||
onTriggered: fileEditView.expanded = !fileEditView.expanded
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
Layout.fillWidth: true
|
||||
visible: root.statusMessage.length > 0
|
||||
text: root.statusMessage
|
||||
font.pixelSize: 10
|
||||
font.italic: true
|
||||
color: root.isApplied
|
||||
? Qt.rgba(0.2, 0.6, 0.2, 1)
|
||||
: Qt.rgba(0.8, 0.2, 0.2, 1)
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
56
ChatView/qml/chatparts/TextBlock.qml
Normal file
56
ChatView/qml/chatparts/TextBlock.qml
Normal file
@ -0,0 +1,56 @@
|
||||
/*
|
||||
* Copyright (C) 2024-2025 Petr Mironychev
|
||||
*
|
||||
* This file is part of QodeAssist.
|
||||
*
|
||||
* QodeAssist is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* QodeAssist is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import QtQuick
|
||||
import Qt.labs.platform as Platform
|
||||
|
||||
TextEdit {
|
||||
id: root
|
||||
|
||||
readOnly: true
|
||||
selectByMouse: true
|
||||
wrapMode: Text.WordWrap
|
||||
selectionColor: palette.highlight
|
||||
color: palette.text
|
||||
|
||||
onLinkActivated: (link) => Qt.openUrlExternally(link)
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
acceptedButtons: Qt.RightButton
|
||||
onClicked: contextMenu.open()
|
||||
cursorShape: root.hoveredLink ? Qt.PointingHandCursor : Qt.IBeamCursor
|
||||
}
|
||||
|
||||
Platform.Menu {
|
||||
id: contextMenu
|
||||
|
||||
Platform.MenuItem {
|
||||
text: qsTr("Copy")
|
||||
enabled: root.selectedText.length > 0
|
||||
onTriggered: root.copy()
|
||||
}
|
||||
|
||||
Platform.MenuItem {
|
||||
text: qsTr("Select All")
|
||||
enabled: root.text.length > 0
|
||||
onTriggered: root.selectAll()
|
||||
}
|
||||
}
|
||||
}
|
||||
183
ChatView/qml/chatparts/ThinkingBlock.qml
Normal file
183
ChatView/qml/chatparts/ThinkingBlock.qml
Normal file
@ -0,0 +1,183 @@
|
||||
/*
|
||||
* Copyright (C) 2025 Petr Mironychev
|
||||
*
|
||||
* This file is part of QodeAssist.
|
||||
*
|
||||
* QodeAssist is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* QodeAssist is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import QtQuick
|
||||
import Qt.labs.platform as Platform
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
property string thinkingContent: ""
|
||||
// property string signature: ""
|
||||
property bool isRedacted: false
|
||||
property bool expanded: false
|
||||
|
||||
property alias headerOpacity: headerRow.opacity
|
||||
|
||||
radius: 6
|
||||
color: palette.base
|
||||
clip: true
|
||||
|
||||
Behavior on implicitHeight {
|
||||
NumberAnimation { duration: 200; easing.type: Easing.InOutQuad }
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: header
|
||||
|
||||
width: parent.width
|
||||
height: headerRow.height + 10
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: root.expanded = !root.expanded
|
||||
|
||||
Row {
|
||||
id: headerRow
|
||||
|
||||
anchors {
|
||||
verticalCenter: parent.verticalCenter
|
||||
left: parent.left
|
||||
leftMargin: 10
|
||||
}
|
||||
width: parent.width
|
||||
spacing: 8
|
||||
|
||||
Text {
|
||||
text: root.isRedacted ? qsTr("Thinking (Redacted)")
|
||||
: qsTr("Thinking")
|
||||
font.pixelSize: 13
|
||||
font.bold: true
|
||||
color: palette.text
|
||||
}
|
||||
|
||||
Text {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: root.expanded ? "▼" : "▶"
|
||||
font.pixelSize: 10
|
||||
color: palette.mid
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
id: contentColumn
|
||||
|
||||
anchors {
|
||||
left: parent.left
|
||||
right: parent.right
|
||||
top: header.bottom
|
||||
margins: 10
|
||||
}
|
||||
spacing: 8
|
||||
|
||||
Text {
|
||||
visible: root.isRedacted
|
||||
width: parent.width
|
||||
text: qsTr("Thinking content was redacted by safety systems")
|
||||
font.pixelSize: 11
|
||||
font.italic: true
|
||||
color: Qt.rgba(0.8, 0.4, 0.4, 1.0)
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
|
||||
TextEdit {
|
||||
id: thinkingText
|
||||
|
||||
visible: !root.isRedacted
|
||||
width: parent.width
|
||||
text: root.thinkingContent
|
||||
readOnly: true
|
||||
selectByMouse: true
|
||||
color: palette.text
|
||||
wrapMode: Text.WordWrap
|
||||
font.family: "monospace"
|
||||
font.pixelSize: 11
|
||||
selectionColor: palette.highlight
|
||||
}
|
||||
|
||||
// Rectangle {
|
||||
// visible: root.signature.length > 0 && root.expanded
|
||||
// width: parent.width
|
||||
// height: signatureText.height + 10
|
||||
// color: palette.alternateBase
|
||||
// radius: 4
|
||||
|
||||
// Text {
|
||||
// id: signatureText
|
||||
|
||||
// anchors {
|
||||
// left: parent.left
|
||||
// right: parent.right
|
||||
// verticalCenter: parent.verticalCenter
|
||||
// margins: 5
|
||||
// }
|
||||
// text: qsTr("Signature: %1").arg(root.signature.substring(0, Math.min(40, root.signature.length)) + "...")
|
||||
// font.pixelSize: 9
|
||||
// font.family: "monospace"
|
||||
// color: palette.mid
|
||||
// elide: Text.ElideRight
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
acceptedButtons: Qt.RightButton
|
||||
onClicked: contextMenu.open()
|
||||
propagateComposedEvents: true
|
||||
}
|
||||
|
||||
Platform.Menu {
|
||||
id: contextMenu
|
||||
|
||||
Platform.MenuItem {
|
||||
text: root.expanded ? qsTr("Collapse") : qsTr("Expand")
|
||||
onTriggered: root.expanded = !root.expanded
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: thinkingMarker
|
||||
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: 3
|
||||
height: root.height - root.radius
|
||||
color: root.isRedacted ? Qt.rgba(0.8, 0.3, 0.3, 0.9)
|
||||
: (root.color.hslLightness > 0.5 ? Qt.darker(palette.alternateBase, 1.3)
|
||||
: Qt.lighter(palette.alternateBase, 1.3))
|
||||
radius: root.radius
|
||||
}
|
||||
|
||||
states: [
|
||||
State {
|
||||
when: !root.expanded
|
||||
PropertyChanges {
|
||||
target: root
|
||||
implicitHeight: header.height
|
||||
}
|
||||
},
|
||||
State {
|
||||
when: root.expanded
|
||||
PropertyChanges {
|
||||
target: root
|
||||
implicitHeight: header.height + contentColumn.height + 20
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
162
ChatView/qml/chatparts/ToolBlock.qml
Normal file
162
ChatView/qml/chatparts/ToolBlock.qml
Normal file
@ -0,0 +1,162 @@
|
||||
/*
|
||||
* Copyright (C) 2025 Petr Mironychev
|
||||
*
|
||||
* This file is part of QodeAssist.
|
||||
*
|
||||
* QodeAssist is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* QodeAssist is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import QtQuick
|
||||
import Qt.labs.platform as Platform
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
property string toolContent: ""
|
||||
property bool expanded: false
|
||||
|
||||
property alias headerOpacity: headerRow.opacity
|
||||
|
||||
readonly property int firstNewline: toolContent.indexOf('\n')
|
||||
readonly property string toolName: firstNewline > 0 ? toolContent.substring(0, firstNewline) : toolContent
|
||||
readonly property string toolResult: firstNewline > 0 ? toolContent.substring(firstNewline + 1) : ""
|
||||
|
||||
radius: 6
|
||||
color: palette.base
|
||||
clip: true
|
||||
|
||||
Behavior on implicitHeight {
|
||||
NumberAnimation { duration: 200; easing.type: Easing.InOutQuad }
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: header
|
||||
|
||||
width: parent.width
|
||||
height: headerRow.height + 10
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: root.expanded = !root.expanded
|
||||
|
||||
Row {
|
||||
id: headerRow
|
||||
|
||||
anchors {
|
||||
verticalCenter: parent.verticalCenter
|
||||
left: parent.left
|
||||
leftMargin: 10
|
||||
}
|
||||
width: parent.width
|
||||
spacing: 8
|
||||
|
||||
Text {
|
||||
text: qsTr("Tool: %1").arg(root.toolName)
|
||||
font.pixelSize: 13
|
||||
font.bold: true
|
||||
color: palette.text
|
||||
}
|
||||
|
||||
Text {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: root.expanded ? "▼" : "▶"
|
||||
font.pixelSize: 10
|
||||
color: palette.mid
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
id: contentColumn
|
||||
|
||||
anchors {
|
||||
left: parent.left
|
||||
right: parent.right
|
||||
top: header.bottom
|
||||
margins: 10
|
||||
}
|
||||
spacing: 8
|
||||
|
||||
TextEdit {
|
||||
id: resultText
|
||||
|
||||
width: parent.width
|
||||
text: root.toolResult
|
||||
readOnly: true
|
||||
selectByMouse: true
|
||||
color: palette.text
|
||||
wrapMode: Text.WordWrap
|
||||
font.family: "monospace"
|
||||
font.pixelSize: 11
|
||||
selectionColor: palette.highlight
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
acceptedButtons: Qt.RightButton
|
||||
onClicked: contextMenu.open()
|
||||
propagateComposedEvents: true
|
||||
}
|
||||
|
||||
Platform.Menu {
|
||||
id: contextMenu
|
||||
|
||||
Platform.MenuItem {
|
||||
text: qsTr("Copy")
|
||||
enabled: resultText.selectedText.length > 0
|
||||
onTriggered: resultText.copy()
|
||||
}
|
||||
|
||||
Platform.MenuItem {
|
||||
text: qsTr("Select All")
|
||||
enabled: resultText.text.length > 0
|
||||
onTriggered: resultText.selectAll()
|
||||
}
|
||||
|
||||
Platform.MenuSeparator {}
|
||||
|
||||
Platform.MenuItem {
|
||||
text: root.expanded ? qsTr("Collapse") : qsTr("Expand")
|
||||
onTriggered: root.expanded = !root.expanded
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: messageMarker
|
||||
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: 3
|
||||
height: root.height - root.radius
|
||||
color: root.color.hslLightness > 0.5 ? Qt.darker(palette.alternateBase, 1.3)
|
||||
: Qt.lighter(palette.alternateBase, 1.3)
|
||||
radius: root.radius
|
||||
}
|
||||
|
||||
|
||||
states: [
|
||||
State {
|
||||
when: !root.expanded
|
||||
PropertyChanges {
|
||||
target: root
|
||||
implicitHeight: header.height
|
||||
}
|
||||
},
|
||||
State {
|
||||
when: root.expanded
|
||||
PropertyChanges {
|
||||
target: root
|
||||
implicitHeight: header.height + contentColumn.height + 20
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user