gui: extract dialogs and chat delegate into components
Phase 1 of GUI cleanup - component extraction: - SettingsDialog.qml: theme and font settings (118 lines) - NewSessionDialog.qml: session creation form (79 lines) - NewAgentDialog.qml: agent creation form (174 lines) - ChatBlockDelegate.qml: chat block with text/diagram segments (322 lines) main.qml reduced from 1685 to 1030 lines (39% reduction). Updated CMakeLists.txt for both Qt6 and KF5 builds.
This commit is contained in:
parent
bdad2b740f
commit
2d6e0ed787
|
|
@ -71,6 +71,11 @@ if(OLLIE_KF5)
|
||||||
file(READ ${CMAKE_CURRENT_SOURCE_DIR}/gui/main.qml _QML_SRC)
|
file(READ ${CMAKE_CURRENT_SOURCE_DIR}/gui/main.qml _QML_SRC)
|
||||||
string(REGEX REPLACE "[ \t]*(//[^\n]*palette[^\n]*\n|palette\\.(active|inactive|disabled)\\.[^\n]*\n)" "" _QML_SRC "${_QML_SRC}")
|
string(REGEX REPLACE "[ \t]*(//[^\n]*palette[^\n]*\n|palette\\.(active|inactive|disabled)\\.[^\n]*\n)" "" _QML_SRC "${_QML_SRC}")
|
||||||
file(WRITE ${CMAKE_CURRENT_BINARY_DIR}/gui-kf5/main.qml "${_QML_SRC}")
|
file(WRITE ${CMAKE_CURRENT_BINARY_DIR}/gui-kf5/main.qml "${_QML_SRC}")
|
||||||
|
# Copy component QML files (no palette stripping needed - they don't use grouped palette)
|
||||||
|
configure_file(gui/SettingsDialog.qml ${CMAKE_CURRENT_BINARY_DIR}/gui-kf5/SettingsDialog.qml COPYONLY)
|
||||||
|
configure_file(gui/NewSessionDialog.qml ${CMAKE_CURRENT_BINARY_DIR}/gui-kf5/NewSessionDialog.qml COPYONLY)
|
||||||
|
configure_file(gui/NewAgentDialog.qml ${CMAKE_CURRENT_BINARY_DIR}/gui-kf5/NewAgentDialog.qml COPYONLY)
|
||||||
|
configure_file(gui/ChatBlockDelegate.qml ${CMAKE_CURRENT_BINARY_DIR}/gui-kf5/ChatBlockDelegate.qml COPYONLY)
|
||||||
configure_file(gui/qml.qrc ${CMAKE_CURRENT_BINARY_DIR}/gui-kf5/qml.qrc COPYONLY)
|
configure_file(gui/qml.qrc ${CMAKE_CURRENT_BINARY_DIR}/gui-kf5/qml.qrc COPYONLY)
|
||||||
qt5_add_resources(GUI_KF5_QRC ${CMAKE_CURRENT_BINARY_DIR}/gui-kf5/qml.qrc)
|
qt5_add_resources(GUI_KF5_QRC ${CMAKE_CURRENT_BINARY_DIR}/gui-kf5/qml.qrc)
|
||||||
add_executable(ollie-gui
|
add_executable(ollie-gui
|
||||||
|
|
@ -170,7 +175,12 @@ else()
|
||||||
qt_add_resources(ollie-gui "gui"
|
qt_add_resources(ollie-gui "gui"
|
||||||
PREFIX "/"
|
PREFIX "/"
|
||||||
BASE gui
|
BASE gui
|
||||||
FILES gui/main.qml
|
FILES
|
||||||
|
gui/main.qml
|
||||||
|
gui/SettingsDialog.qml
|
||||||
|
gui/NewSessionDialog.qml
|
||||||
|
gui/NewAgentDialog.qml
|
||||||
|
gui/ChatBlockDelegate.qml
|
||||||
)
|
)
|
||||||
|
|
||||||
# --- Install ---
|
# --- Install ---
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,322 @@
|
||||||
|
// ChatBlockDelegate.qml — Delegate for rendering chat blocks in the ListView
|
||||||
|
import QtQuick 2.15
|
||||||
|
import QtQuick.Controls 2.15
|
||||||
|
import QtQuick.Layouts 1.15
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
id: root
|
||||||
|
spacing: 2
|
||||||
|
|
||||||
|
// Required properties from model
|
||||||
|
required property string blockType
|
||||||
|
required property string blockName
|
||||||
|
required property var blockSegments
|
||||||
|
|
||||||
|
// Reference to parent ListView for scroll control
|
||||||
|
property var chatView: null
|
||||||
|
|
||||||
|
// Expand state based on block type
|
||||||
|
property bool blockExpanded: {
|
||||||
|
switch (blockType) {
|
||||||
|
case "user":
|
||||||
|
case "assistant":
|
||||||
|
case "tool":
|
||||||
|
case "info":
|
||||||
|
case "retry":
|
||||||
|
case "interrupt":
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Header color based on block type and theme
|
||||||
|
function headerColor() {
|
||||||
|
var bg = theme.baseColor
|
||||||
|
if (theme.isDark) {
|
||||||
|
switch (blockType) {
|
||||||
|
case "user": return Qt.lighter(bg, 1.8)
|
||||||
|
case "assistant": return Qt.lighter(bg, 1.5)
|
||||||
|
case "reasoning": return Qt.lighter(bg, 1.3)
|
||||||
|
case "call": return Qt.lighter(bg, 1.7)
|
||||||
|
case "tool": return Qt.lighter(bg, 1.2)
|
||||||
|
case "error": return Qt.rgba(0.6, 0.15, 0.15, 0.7)
|
||||||
|
case "info": return Qt.lighter(bg, 1.4)
|
||||||
|
case "retry": return Qt.rgba(0.6, 0.5, 0.0, 0.5)
|
||||||
|
case "interrupt": return "#3d2e00"
|
||||||
|
default: return bg
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
switch (blockType) {
|
||||||
|
case "user": return Qt.darker(bg, 1.12)
|
||||||
|
case "assistant": return Qt.darker(bg, 1.06)
|
||||||
|
case "reasoning": return Qt.darker(bg, 1.03)
|
||||||
|
case "call": return Qt.darker(bg, 1.1)
|
||||||
|
case "tool": return Qt.darker(bg, 1.04)
|
||||||
|
case "error": return Qt.rgba(1.0, 0.8, 0.8, 0.7)
|
||||||
|
case "info": return Qt.darker(bg, 1.02)
|
||||||
|
case "retry": return Qt.rgba(1.0, 0.9, 0.6, 0.5)
|
||||||
|
case "interrupt": return "#fff8e1"
|
||||||
|
default: return bg
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Header label text based on block type
|
||||||
|
function headerText() {
|
||||||
|
switch (blockType) {
|
||||||
|
case "user": return "\uD83E\uDDD1 You"
|
||||||
|
case "assistant": return "\uD83E\uDD16 Ollie"
|
||||||
|
case "reasoning": return "Reasoning"
|
||||||
|
case "call": return "\u25B6\uFE0F " + blockName
|
||||||
|
case "tool": return "\uD83D\uDDA5\uFE0F " + blockName
|
||||||
|
case "error": return "\u274C Error"
|
||||||
|
case "info": return "\u2139\uFE0F Info"
|
||||||
|
case "retry": return "Retry"
|
||||||
|
case "interrupt": return "\u26A0\uFE0F User Interrupt"
|
||||||
|
default: return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clickable header
|
||||||
|
Rectangle {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
height: 22
|
||||||
|
radius: 3
|
||||||
|
color: root.headerColor()
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: root.blockExpanded = !root.blockExpanded
|
||||||
|
}
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.leftMargin: 6
|
||||||
|
spacing: 4
|
||||||
|
|
||||||
|
Label {
|
||||||
|
text: root.blockExpanded ? "▼" : "▶"
|
||||||
|
font.pixelSize: 8
|
||||||
|
opacity: 0.6
|
||||||
|
}
|
||||||
|
|
||||||
|
Label {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
text: root.headerText()
|
||||||
|
font.pointSize: theme.fontSize > 0 ? theme.fontSize * 0.85 : 11
|
||||||
|
font.bold: true
|
||||||
|
color: theme.textColor
|
||||||
|
opacity: 0.9
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Block content rendered as segments
|
||||||
|
Column {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
visible: root.blockExpanded
|
||||||
|
spacing: 4
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: root.blockSegments || []
|
||||||
|
|
||||||
|
delegate: Loader {
|
||||||
|
width: parent ? parent.width : 0
|
||||||
|
active: modelData !== null && modelData !== undefined
|
||||||
|
sourceComponent: (modelData && modelData.type === "diagram") ? diagramComponent : textComponent
|
||||||
|
property var segmentData: modelData || {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Text segment component
|
||||||
|
Component {
|
||||||
|
id: textComponent
|
||||||
|
|
||||||
|
TextEdit {
|
||||||
|
id: textEdit
|
||||||
|
width: parent ? parent.width : 100
|
||||||
|
text: segmentData.html || ""
|
||||||
|
textFormat: Text.RichText
|
||||||
|
readOnly: true
|
||||||
|
selectByMouse: true
|
||||||
|
persistentSelection: true
|
||||||
|
wrapMode: TextEdit.Wrap
|
||||||
|
font.family: theme.fontFamily || Qt.application.font.family
|
||||||
|
font.pointSize: theme.fontSize > 0 ? theme.fontSize : 12
|
||||||
|
selectionColor: theme.highlightColor
|
||||||
|
selectedTextColor: theme.highlightedTextColor
|
||||||
|
|
||||||
|
onActiveFocusChanged: {
|
||||||
|
if (!activeFocus && root.chatView)
|
||||||
|
root.chatView.interactive = true
|
||||||
|
}
|
||||||
|
|
||||||
|
Menu {
|
||||||
|
id: contextMenu
|
||||||
|
MenuItem {
|
||||||
|
text: "Copy"
|
||||||
|
enabled: textEdit.selectedText.length > 0
|
||||||
|
onTriggered: textEdit.copy()
|
||||||
|
}
|
||||||
|
MenuItem {
|
||||||
|
text: "Select All"
|
||||||
|
onTriggered: textEdit.selectAll()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
acceptedButtons: Qt.LeftButton
|
||||||
|
propagateComposedEvents: true
|
||||||
|
cursorShape: Qt.IBeamCursor
|
||||||
|
onPressed: function(mouse) {
|
||||||
|
if (root.chatView) root.chatView.interactive = false
|
||||||
|
mouse.accepted = false
|
||||||
|
}
|
||||||
|
onReleased: function(mouse) {
|
||||||
|
if (root.chatView) root.chatView.interactive = true
|
||||||
|
mouse.accepted = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
acceptedButtons: Qt.RightButton
|
||||||
|
cursorShape: Qt.IBeamCursor
|
||||||
|
onClicked: contextMenu.popup()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Diagram segment component
|
||||||
|
Component {
|
||||||
|
id: diagramComponent
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: diagramRect
|
||||||
|
width: parent ? parent.width : 100
|
||||||
|
height: diagramContent.height + diagramHeader.height + 8
|
||||||
|
color: "transparent"
|
||||||
|
border.color: theme.midColor
|
||||||
|
border.width: 1
|
||||||
|
radius: 4
|
||||||
|
|
||||||
|
property bool showSource: false
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
id: diagramHeader
|
||||||
|
anchors.top: parent.top
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.margins: 4
|
||||||
|
height: 24
|
||||||
|
|
||||||
|
Label {
|
||||||
|
text: segmentData.lang || "diagram"
|
||||||
|
font.pointSize: theme.fontSize > 0 ? theme.fontSize - 1 : 11
|
||||||
|
color: theme.textColor
|
||||||
|
opacity: 0.7
|
||||||
|
}
|
||||||
|
|
||||||
|
Item { Layout.fillWidth: true }
|
||||||
|
|
||||||
|
Button {
|
||||||
|
text: diagramRect.showSource ? "📊" : "</>"
|
||||||
|
font.pointSize: theme.fontSize > 0 ? theme.fontSize - 2 : 10
|
||||||
|
implicitWidth: 32
|
||||||
|
implicitHeight: 20
|
||||||
|
ToolTip.visible: hovered
|
||||||
|
ToolTip.text: diagramRect.showSource ? "Show diagram" : "Show source"
|
||||||
|
onClicked: diagramRect.showSource = !diagramRect.showSource
|
||||||
|
}
|
||||||
|
|
||||||
|
Button {
|
||||||
|
text: "📋"
|
||||||
|
font.pointSize: theme.fontSize > 0 ? theme.fontSize - 2 : 10
|
||||||
|
implicitWidth: 32
|
||||||
|
implicitHeight: 20
|
||||||
|
ToolTip.visible: hovered
|
||||||
|
ToolTip.text: diagramRect.showSource ? "Copy source" : "Copy image"
|
||||||
|
onClicked: {
|
||||||
|
if (diagramRect.showSource) {
|
||||||
|
clipboard.copyText(segmentData.source)
|
||||||
|
} else if (segmentData.svg) {
|
||||||
|
clipboard.copyImage(segmentData.svg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: diagramContent
|
||||||
|
anchors.top: diagramHeader.bottom
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.margins: 4
|
||||||
|
height: diagramRect.showSource ? sourceEdit.height : imageColumn.height
|
||||||
|
|
||||||
|
TextEdit {
|
||||||
|
id: sourceEdit
|
||||||
|
width: parent.width
|
||||||
|
visible: diagramRect.showSource
|
||||||
|
text: segmentData.html || ""
|
||||||
|
textFormat: Text.RichText
|
||||||
|
readOnly: true
|
||||||
|
selectByMouse: true
|
||||||
|
persistentSelection: true
|
||||||
|
wrapMode: TextEdit.Wrap
|
||||||
|
font.family: theme.fontFamily || Qt.application.font.family
|
||||||
|
font.pointSize: theme.fontSize > 0 ? theme.fontSize : 12
|
||||||
|
selectionColor: theme.highlightColor
|
||||||
|
selectedTextColor: theme.highlightedTextColor
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
acceptedButtons: Qt.LeftButton
|
||||||
|
propagateComposedEvents: true
|
||||||
|
cursorShape: Qt.IBeamCursor
|
||||||
|
onPressed: function(mouse) {
|
||||||
|
if (root.chatView) root.chatView.interactive = false
|
||||||
|
mouse.accepted = false
|
||||||
|
}
|
||||||
|
onReleased: function(mouse) {
|
||||||
|
if (root.chatView) root.chatView.interactive = true
|
||||||
|
mouse.accepted = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
id: imageColumn
|
||||||
|
width: parent.width
|
||||||
|
visible: !diagramRect.showSource
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
visible: segmentData.svg && !segmentData.loading
|
||||||
|
color: "white"
|
||||||
|
width: diagramImg.width
|
||||||
|
height: diagramImg.height
|
||||||
|
|
||||||
|
Image {
|
||||||
|
id: diagramImg
|
||||||
|
source: segmentData.svg || ""
|
||||||
|
fillMode: Image.PreserveAspectFit
|
||||||
|
width: Math.min(implicitWidth, imageColumn.width)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Label {
|
||||||
|
visible: segmentData.loading
|
||||||
|
text: "Loading " + (segmentData.lang || "diagram") + "..."
|
||||||
|
font.italic: true
|
||||||
|
color: theme.textColor
|
||||||
|
opacity: 0.7
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,174 @@
|
||||||
|
// NewAgentDialog.qml — Dialog for creating a new agent in a session
|
||||||
|
import QtQuick 2.15
|
||||||
|
import QtQuick.Controls 2.15
|
||||||
|
import QtQuick.Layouts 1.15
|
||||||
|
|
||||||
|
Dialog {
|
||||||
|
id: root
|
||||||
|
title: "New Agent"
|
||||||
|
anchors.centerIn: parent
|
||||||
|
width: 420
|
||||||
|
modal: true
|
||||||
|
|
||||||
|
// Session ID to create the agent in
|
||||||
|
property string targetSessionId: ""
|
||||||
|
|
||||||
|
signal agentCreated()
|
||||||
|
|
||||||
|
contentItem: ColumnLayout {
|
||||||
|
spacing: 16
|
||||||
|
Layout.margins: 16
|
||||||
|
|
||||||
|
Label {
|
||||||
|
text: "Agent"
|
||||||
|
font.bold: true
|
||||||
|
font.pointSize: theme.fontSize > 0 ? theme.fontSize + 1 : Qt.application.font.pointSize + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
Label {
|
||||||
|
id: errorLabel
|
||||||
|
Layout.fillWidth: true
|
||||||
|
visible: text !== ""
|
||||||
|
color: "red"
|
||||||
|
wrapMode: Text.Wrap
|
||||||
|
}
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Label { text: "Directory:"; Layout.preferredWidth: 80 }
|
||||||
|
TextField {
|
||||||
|
id: dirField
|
||||||
|
Layout.fillWidth: true
|
||||||
|
placeholderText: "/path/to/project"
|
||||||
|
}
|
||||||
|
Button {
|
||||||
|
text: "Browse..."
|
||||||
|
onClicked: {
|
||||||
|
var folder = theme.chooseFolder("Select Project Directory", dirField.text || "")
|
||||||
|
if (folder !== "")
|
||||||
|
dirField.text = folder
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Label { text: "Backend:"; Layout.preferredWidth: 80 }
|
||||||
|
ComboBox {
|
||||||
|
id: backendCombo
|
||||||
|
Layout.fillWidth: true
|
||||||
|
enabled: ollie.rootBackendsLoaded
|
||||||
|
model: ollie.availableBackends
|
||||||
|
currentIndex: 0
|
||||||
|
editable: true
|
||||||
|
onAccepted: backendCombo.editText = backendCombo.currentText
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ListModel { id: modelList }
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Label { text: "Model:"; Layout.preferredWidth: 80 }
|
||||||
|
ComboBox {
|
||||||
|
id: modelCombo
|
||||||
|
Layout.fillWidth: true
|
||||||
|
enabled: backendCombo.currentIndex >= 0 && backendCombo.currentText.trim() !== "" && ollie.rootModelsLoaded
|
||||||
|
model: modelList
|
||||||
|
currentIndex: -1
|
||||||
|
editable: true
|
||||||
|
onAccepted: modelCombo.editText = modelCombo.currentText
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Label { text: "Profile:"; Layout.preferredWidth: 80 }
|
||||||
|
ComboBox {
|
||||||
|
id: agentCombo
|
||||||
|
Layout.fillWidth: true
|
||||||
|
enabled: ollie.rootAgentsLoaded
|
||||||
|
model: ollie.availableAgents
|
||||||
|
currentIndex: 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Label { text: "Name:"; Layout.preferredWidth: 80 }
|
||||||
|
TextField {
|
||||||
|
id: aliasField
|
||||||
|
Layout.fillWidth: true
|
||||||
|
placeholderText: "(auto-generated)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Label { text: "Remote:"; Layout.preferredWidth: 80 }
|
||||||
|
TextField {
|
||||||
|
id: remoteField
|
||||||
|
Layout.fillWidth: true
|
||||||
|
placeholderText: "user@host:port"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Component.onCompleted: {
|
||||||
|
if (!ollie.rootBackendsLoaded) ollie.loadRootBackends()
|
||||||
|
if (!ollie.rootAgentsLoaded) ollie.loadRootAgents()
|
||||||
|
}
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
target: backendCombo
|
||||||
|
function onCurrentTextChanged() {
|
||||||
|
var be = backendCombo.currentText.trim()
|
||||||
|
modelList.clear()
|
||||||
|
if (be !== "" && ollie.rootModelsLoaded) {
|
||||||
|
var models = ollie.getAvailableModels(be)
|
||||||
|
for (var i = 0; i < models.length; ++i) {
|
||||||
|
modelList.append({ modelData: models[i] })
|
||||||
|
}
|
||||||
|
modelCombo.currentIndex = models.length > 0 ? 0 : -1
|
||||||
|
} else {
|
||||||
|
modelCombo.currentIndex = -1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
footer: RowLayout {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Item { Layout.fillWidth: true }
|
||||||
|
Button {
|
||||||
|
text: "OK"
|
||||||
|
onClicked: {
|
||||||
|
errorLabel.text = ""
|
||||||
|
var cwd = dirField.text.trim()
|
||||||
|
if (cwd === "") {
|
||||||
|
errorLabel.text = "Directory is required"
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var backend = backendCombo.currentText.trim()
|
||||||
|
var model = modelCombo.currentIndex >= 0 ? modelCombo.currentText.trim() : ""
|
||||||
|
var agent = agentCombo.currentText
|
||||||
|
var remote = remoteField.text.trim()
|
||||||
|
var agentAlias = aliasField.text.trim()
|
||||||
|
|
||||||
|
var err = ollie.createAgent(root.targetSessionId, cwd, backend, model, agent, remote, agentAlias)
|
||||||
|
if (err === "") {
|
||||||
|
ollie.refreshSessions()
|
||||||
|
root.agentCreated()
|
||||||
|
root.close()
|
||||||
|
} else {
|
||||||
|
errorLabel.text = err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Button {
|
||||||
|
text: "Cancel"
|
||||||
|
onClicked: root.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onOpened: errorLabel.text = ""
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,79 @@
|
||||||
|
// NewSessionDialog.qml — Dialog for creating a new session
|
||||||
|
import QtQuick 2.15
|
||||||
|
import QtQuick.Controls 2.15
|
||||||
|
import QtQuick.Layouts 1.15
|
||||||
|
|
||||||
|
Dialog {
|
||||||
|
id: root
|
||||||
|
title: "New Session"
|
||||||
|
anchors.centerIn: parent
|
||||||
|
width: 380
|
||||||
|
modal: true
|
||||||
|
|
||||||
|
signal sessionCreated()
|
||||||
|
|
||||||
|
contentItem: ColumnLayout {
|
||||||
|
spacing: 16
|
||||||
|
Layout.margins: 16
|
||||||
|
|
||||||
|
Label {
|
||||||
|
id: errorLabel
|
||||||
|
Layout.fillWidth: true
|
||||||
|
visible: text !== ""
|
||||||
|
color: "red"
|
||||||
|
wrapMode: Text.Wrap
|
||||||
|
}
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Label { text: "Name:"; Layout.preferredWidth: 80 }
|
||||||
|
TextField {
|
||||||
|
id: nameField
|
||||||
|
Layout.fillWidth: true
|
||||||
|
placeholderText: "(auto-generated)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Label { text: "Remote:"; Layout.preferredWidth: 80 }
|
||||||
|
TextField {
|
||||||
|
id: remoteField
|
||||||
|
Layout.fillWidth: true
|
||||||
|
placeholderText: "user@host (optional)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
footer: RowLayout {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Item { Layout.fillWidth: true }
|
||||||
|
Button {
|
||||||
|
text: "OK"
|
||||||
|
onClicked: {
|
||||||
|
errorLabel.text = ""
|
||||||
|
var name = nameField.text.trim()
|
||||||
|
var remote = remoteField.text.trim()
|
||||||
|
|
||||||
|
var err = ollie.createSession(name, remote)
|
||||||
|
if (err === "") {
|
||||||
|
ollie.refreshSessions()
|
||||||
|
root.sessionCreated()
|
||||||
|
root.close()
|
||||||
|
} else {
|
||||||
|
errorLabel.text = err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Button {
|
||||||
|
text: "Cancel"
|
||||||
|
onClicked: root.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onOpened: {
|
||||||
|
errorLabel.text = ""
|
||||||
|
nameField.text = ""
|
||||||
|
remoteField.text = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,118 @@
|
||||||
|
// SettingsDialog.qml — Theme and font settings dialog
|
||||||
|
import QtQuick 2.15
|
||||||
|
import QtQuick.Controls 2.15
|
||||||
|
import QtQuick.Layouts 1.15
|
||||||
|
|
||||||
|
Dialog {
|
||||||
|
id: root
|
||||||
|
title: "Settings"
|
||||||
|
anchors.centerIn: parent
|
||||||
|
width: 380
|
||||||
|
modal: true
|
||||||
|
standardButtons: Dialog.Close
|
||||||
|
|
||||||
|
contentItem: ColumnLayout {
|
||||||
|
spacing: 16
|
||||||
|
Layout.margins: 16
|
||||||
|
|
||||||
|
Label {
|
||||||
|
text: "Color Scheme"
|
||||||
|
font.bold: true
|
||||||
|
}
|
||||||
|
|
||||||
|
Label {
|
||||||
|
text: "Uses KDE Breeze color schemes from /usr/share/color-schemes/"
|
||||||
|
opacity: 0.6
|
||||||
|
wrapMode: Text.Wrap
|
||||||
|
Layout.fillWidth: true
|
||||||
|
font.pixelSize: Qt.application.font.pixelSize * 0.9
|
||||||
|
}
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
spacing: 8
|
||||||
|
|
||||||
|
RadioButton {
|
||||||
|
text: "Use system theme"
|
||||||
|
checked: theme.mode === "system"
|
||||||
|
onClicked: theme.mode = "system"
|
||||||
|
}
|
||||||
|
RadioButton {
|
||||||
|
text: "Breeze Light"
|
||||||
|
checked: theme.mode === "light"
|
||||||
|
onClicked: theme.mode = "light"
|
||||||
|
}
|
||||||
|
RadioButton {
|
||||||
|
text: "Breeze Dark"
|
||||||
|
checked: theme.mode === "dark"
|
||||||
|
onClicked: theme.mode = "dark"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Label {
|
||||||
|
text: theme.isDark ? "Current: dark" : "Current: light"
|
||||||
|
opacity: 0.5
|
||||||
|
font.pixelSize: Qt.application.font.pixelSize * 0.85
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
height: 1
|
||||||
|
color: palette.mid
|
||||||
|
Layout.topMargin: 8
|
||||||
|
}
|
||||||
|
|
||||||
|
Label {
|
||||||
|
text: "Font"
|
||||||
|
font.bold: true
|
||||||
|
}
|
||||||
|
|
||||||
|
GridLayout {
|
||||||
|
columns: 2
|
||||||
|
columnSpacing: 12
|
||||||
|
rowSpacing: 8
|
||||||
|
Layout.fillWidth: true
|
||||||
|
|
||||||
|
Label { text: "Family:" }
|
||||||
|
ComboBox {
|
||||||
|
id: fontFamilyCombo
|
||||||
|
Layout.fillWidth: true
|
||||||
|
model: theme.availableFamilies
|
||||||
|
editable: true
|
||||||
|
currentIndex: {
|
||||||
|
let fam = theme.fontFamily || Qt.application.font.family
|
||||||
|
let idx = model.indexOf(fam)
|
||||||
|
return idx >= 0 ? idx : 0
|
||||||
|
}
|
||||||
|
onActivated: theme.fontFamily = currentText
|
||||||
|
onAccepted: theme.fontFamily = editText
|
||||||
|
}
|
||||||
|
|
||||||
|
Label { text: "Style:" }
|
||||||
|
ComboBox {
|
||||||
|
id: fontStyleCombo
|
||||||
|
Layout.fillWidth: true
|
||||||
|
model: theme.availableStyles
|
||||||
|
currentIndex: {
|
||||||
|
let st = theme.fontStyle
|
||||||
|
let idx = model.indexOf(st)
|
||||||
|
return idx >= 0 ? idx : 0
|
||||||
|
}
|
||||||
|
onActivated: theme.fontStyle = currentText
|
||||||
|
}
|
||||||
|
|
||||||
|
Label { text: "Size:" }
|
||||||
|
SpinBox {
|
||||||
|
id: fontSizeSpin
|
||||||
|
from: 6
|
||||||
|
to: 72
|
||||||
|
value: theme.fontSize > 0 ? theme.fontSize : Qt.application.font.pointSize
|
||||||
|
onValueModified: theme.fontSize = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Button {
|
||||||
|
text: "Reset to system font"
|
||||||
|
onClicked: theme.resetFont()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
669
gui/main.qml
669
gui/main.qml
|
|
@ -531,371 +531,19 @@ ApplicationWindow {
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Settings Dialog ---
|
// --- Settings Dialog ---
|
||||||
Dialog {
|
SettingsDialog {
|
||||||
id: settingsDialog
|
id: settingsDialog
|
||||||
title: "Settings"
|
|
||||||
anchors.centerIn: parent
|
|
||||||
width: 380
|
|
||||||
modal: true
|
|
||||||
standardButtons: Dialog.Close
|
|
||||||
|
|
||||||
contentItem: ColumnLayout {
|
|
||||||
spacing: 16
|
|
||||||
Layout.margins: 16
|
|
||||||
|
|
||||||
Label {
|
|
||||||
text: "Color Scheme"
|
|
||||||
font.bold: true
|
|
||||||
}
|
|
||||||
|
|
||||||
Label {
|
|
||||||
text: "Uses KDE Breeze color schemes from /usr/share/color-schemes/"
|
|
||||||
opacity: 0.6
|
|
||||||
wrapMode: Text.Wrap
|
|
||||||
Layout.fillWidth: true
|
|
||||||
font.pixelSize: Qt.application.font.pixelSize * 0.9
|
|
||||||
}
|
|
||||||
|
|
||||||
ColumnLayout {
|
|
||||||
spacing: 8
|
|
||||||
|
|
||||||
RadioButton {
|
|
||||||
id: systemRadio
|
|
||||||
text: "Use system theme"
|
|
||||||
checked: theme.mode === "system"
|
|
||||||
onClicked: theme.mode = "system"
|
|
||||||
}
|
|
||||||
RadioButton {
|
|
||||||
id: lightRadio
|
|
||||||
text: "Breeze Light"
|
|
||||||
checked: theme.mode === "light"
|
|
||||||
onClicked: theme.mode = "light"
|
|
||||||
}
|
|
||||||
RadioButton {
|
|
||||||
id: darkRadio
|
|
||||||
text: "Breeze Dark"
|
|
||||||
checked: theme.mode === "dark"
|
|
||||||
onClicked: theme.mode = "dark"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Label {
|
|
||||||
text: theme.isDark ? "Current: dark" : "Current: light"
|
|
||||||
opacity: 0.5
|
|
||||||
font.pixelSize: Qt.application.font.pixelSize * 0.85
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
height: 1
|
|
||||||
color: palette.mid
|
|
||||||
Layout.topMargin: 8
|
|
||||||
}
|
|
||||||
|
|
||||||
Label {
|
|
||||||
text: "Font"
|
|
||||||
font.bold: true
|
|
||||||
}
|
|
||||||
|
|
||||||
GridLayout {
|
|
||||||
columns: 2
|
|
||||||
columnSpacing: 12
|
|
||||||
rowSpacing: 8
|
|
||||||
Layout.fillWidth: true
|
|
||||||
|
|
||||||
Label { text: "Family:" }
|
|
||||||
ComboBox {
|
|
||||||
id: fontFamilyCombo
|
|
||||||
Layout.fillWidth: true
|
|
||||||
model: theme.availableFamilies
|
|
||||||
editable: true
|
|
||||||
currentIndex: {
|
|
||||||
let fam = theme.fontFamily || Qt.application.font.family
|
|
||||||
let idx = model.indexOf(fam)
|
|
||||||
return idx >= 0 ? idx : 0
|
|
||||||
}
|
|
||||||
onActivated: theme.fontFamily = currentText
|
|
||||||
onAccepted: theme.fontFamily = editText
|
|
||||||
}
|
|
||||||
|
|
||||||
Label { text: "Style:" }
|
|
||||||
ComboBox {
|
|
||||||
id: fontStyleCombo
|
|
||||||
Layout.fillWidth: true
|
|
||||||
model: theme.availableStyles
|
|
||||||
currentIndex: {
|
|
||||||
let st = theme.fontStyle
|
|
||||||
let idx = model.indexOf(st)
|
|
||||||
return idx >= 0 ? idx : 0
|
|
||||||
}
|
|
||||||
onActivated: theme.fontStyle = currentText
|
|
||||||
}
|
|
||||||
|
|
||||||
Label { text: "Size:" }
|
|
||||||
SpinBox {
|
|
||||||
id: fontSizeSpin
|
|
||||||
from: 6
|
|
||||||
to: 72
|
|
||||||
value: theme.fontSize > 0 ? theme.fontSize : Qt.application.font.pointSize
|
|
||||||
onValueModified: theme.fontSize = value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Button {
|
|
||||||
text: "Reset to system font"
|
|
||||||
onClicked: theme.resetFont()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- New Session Dialog ---
|
// --- New Session Dialog ---
|
||||||
Dialog {
|
NewSessionDialog {
|
||||||
id: newSessionDialog
|
id: newSessionDialog
|
||||||
title: "New Session"
|
|
||||||
anchors.centerIn: parent
|
|
||||||
width: 380
|
|
||||||
modal: true
|
|
||||||
|
|
||||||
contentItem: ColumnLayout {
|
|
||||||
spacing: 16
|
|
||||||
Layout.margins: 16
|
|
||||||
|
|
||||||
Label {
|
|
||||||
id: newSessionErrorLabel
|
|
||||||
Layout.fillWidth: true
|
|
||||||
visible: text !== ""
|
|
||||||
color: "red"
|
|
||||||
wrapMode: Text.Wrap
|
|
||||||
}
|
|
||||||
|
|
||||||
// Name
|
|
||||||
RowLayout {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
Label { text: "Name:"; Layout.preferredWidth: 80 }
|
|
||||||
TextField {
|
|
||||||
id: sessionNameField
|
|
||||||
Layout.fillWidth: true
|
|
||||||
placeholderText: "(auto-generated)"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remote
|
|
||||||
RowLayout {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
Label { text: "Remote:"; Layout.preferredWidth: 80 }
|
|
||||||
TextField {
|
|
||||||
id: sessionRemoteField
|
|
||||||
Layout.fillWidth: true
|
|
||||||
placeholderText: "user@host (optional)"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
footer: RowLayout {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
Item { Layout.fillWidth: true }
|
|
||||||
Button {
|
|
||||||
text: "OK"
|
|
||||||
onClicked: {
|
|
||||||
newSessionErrorLabel.text = ""
|
|
||||||
var name = sessionNameField.text.trim()
|
|
||||||
var remote = sessionRemoteField.text.trim()
|
|
||||||
|
|
||||||
var err = ollie.createSession(name, remote)
|
|
||||||
if (err === "") {
|
|
||||||
ollie.refreshSessions()
|
|
||||||
newSessionDialog.close()
|
|
||||||
} else {
|
|
||||||
newSessionErrorLabel.text = err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Button {
|
|
||||||
text: "Cancel"
|
|
||||||
onClicked: newSessionDialog.close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onOpened: {
|
|
||||||
newSessionErrorLabel.text = ""
|
|
||||||
sessionNameField.text = ""
|
|
||||||
sessionRemoteField.text = ""
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- New Agent Dialog ---
|
// --- New Agent Dialog ---
|
||||||
Dialog {
|
NewAgentDialog {
|
||||||
id: newAgentDialog
|
id: newAgentDialog
|
||||||
title: "New Agent"
|
targetSessionId: root.newAgentTargetSessionId
|
||||||
anchors.centerIn: parent
|
|
||||||
width: 420
|
|
||||||
modal: true
|
|
||||||
|
|
||||||
contentItem: ColumnLayout {
|
|
||||||
spacing: 16
|
|
||||||
Layout.margins: 16
|
|
||||||
|
|
||||||
// --- Agent ---
|
|
||||||
Label { text: "Agent"; font.bold: true; font.pointSize: theme.fontSize > 0 ? theme.fontSize + 1 : Qt.application.font.pointSize + 1 }
|
|
||||||
|
|
||||||
// Error label
|
|
||||||
Label {
|
|
||||||
id: newAgentErrorLabel
|
|
||||||
Layout.fillWidth: true
|
|
||||||
visible: text !== ""
|
|
||||||
color: "red"
|
|
||||||
wrapMode: Text.Wrap
|
|
||||||
}
|
|
||||||
|
|
||||||
// Directory
|
|
||||||
RowLayout {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
Label { text: "Directory:"; Layout.preferredWidth: 80 }
|
|
||||||
TextField {
|
|
||||||
id: dirField
|
|
||||||
Layout.fillWidth: true
|
|
||||||
placeholderText: "/path/to/project"
|
|
||||||
readOnly: false
|
|
||||||
}
|
|
||||||
Button {
|
|
||||||
text: "Browse..."
|
|
||||||
onClicked: {
|
|
||||||
var folder = theme.chooseFolder("Select Project Directory", dirField.text || "")
|
|
||||||
if (folder !== "")
|
|
||||||
dirField.text = folder
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Backend
|
|
||||||
RowLayout {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
Label { text: "Backend:"; Layout.preferredWidth: 80 }
|
|
||||||
ComboBox {
|
|
||||||
id: backendCombo
|
|
||||||
Layout.fillWidth: true
|
|
||||||
enabled: ollie.rootBackendsLoaded
|
|
||||||
model: ollie.availableBackends
|
|
||||||
currentIndex: 0
|
|
||||||
editable: true
|
|
||||||
onAccepted: backendCombo.editText = backendCombo.currentText
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Model list (populated reactively from backend selection)
|
|
||||||
ListModel { id: modelList }
|
|
||||||
|
|
||||||
// Model
|
|
||||||
RowLayout {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
Label { text: "Model:"; Layout.preferredWidth: 80 }
|
|
||||||
ComboBox {
|
|
||||||
id: modelCombo
|
|
||||||
Layout.fillWidth: true
|
|
||||||
enabled: backendCombo.currentIndex >= 0 && backendCombo.currentText.trim() !== "" && ollie.rootModelsLoaded
|
|
||||||
model: modelList
|
|
||||||
currentIndex: -1
|
|
||||||
editable: true
|
|
||||||
onAccepted: modelCombo.editText = modelCombo.currentText
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Profile
|
|
||||||
RowLayout {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
Label { text: "Profile:"; Layout.preferredWidth: 80 }
|
|
||||||
ComboBox {
|
|
||||||
id: agentCombo
|
|
||||||
Layout.fillWidth: true
|
|
||||||
enabled: ollie.rootAgentsLoaded
|
|
||||||
model: ollie.availableAgents
|
|
||||||
currentIndex: 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Alias
|
|
||||||
RowLayout {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
Label { text: "Name:"; Layout.preferredWidth: 80 }
|
|
||||||
TextField {
|
|
||||||
id: agentAliasField
|
|
||||||
Layout.fillWidth: true
|
|
||||||
placeholderText: "(auto-generated)"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remote
|
|
||||||
RowLayout {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
Label { text: "Remote:"; Layout.preferredWidth: 80 }
|
|
||||||
TextField {
|
|
||||||
id: remoteField
|
|
||||||
Layout.fillWidth: true
|
|
||||||
placeholderText: "user@host:port"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Component.onCompleted: {
|
|
||||||
if (!ollie.rootBackendsLoaded) ollie.loadRootBackends()
|
|
||||||
if (!ollie.rootAgentsLoaded) ollie.loadRootAgents()
|
|
||||||
}
|
|
||||||
|
|
||||||
Connections {
|
|
||||||
target: backendCombo
|
|
||||||
function onCurrentTextChanged() {
|
|
||||||
var be = backendCombo.currentText.trim()
|
|
||||||
modelList.clear()
|
|
||||||
if (be !== "" && ollie.rootModelsLoaded) {
|
|
||||||
var models = ollie.getAvailableModels(be)
|
|
||||||
for (var i = 0; i < models.length; ++i) {
|
|
||||||
modelList.append({ modelData: models[i] })
|
|
||||||
}
|
|
||||||
if (models.length > 0) {
|
|
||||||
modelCombo.currentIndex = 0
|
|
||||||
} else {
|
|
||||||
modelCombo.currentIndex = -1
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
modelCombo.currentIndex = -1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
footer: RowLayout {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
Item { Layout.fillWidth: true }
|
|
||||||
Button {
|
|
||||||
text: "OK"
|
|
||||||
onClicked: {
|
|
||||||
newAgentErrorLabel.text = ""
|
|
||||||
var cwd = dirField.text.trim()
|
|
||||||
if (cwd === "") {
|
|
||||||
newAgentErrorLabel.text = "Directory is required"
|
|
||||||
return
|
|
||||||
}
|
|
||||||
var backend = backendCombo.currentText.trim()
|
|
||||||
var model = modelCombo.currentIndex >= 0 ? modelCombo.currentText.trim() : ""
|
|
||||||
var agent = agentCombo.currentText
|
|
||||||
var remote = remoteField.text.trim()
|
|
||||||
var agentAlias = agentAliasField.text.trim()
|
|
||||||
|
|
||||||
var err = ollie.createAgent(root.newAgentTargetSessionId, cwd, backend, model, agent, remote, agentAlias)
|
|
||||||
if (err === "") {
|
|
||||||
ollie.refreshSessions()
|
|
||||||
newAgentDialog.close()
|
|
||||||
} else {
|
|
||||||
newAgentErrorLabel.text = err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Button {
|
|
||||||
text: "Cancel"
|
|
||||||
onClicked: newAgentDialog.close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onOpened: newAgentErrorLabel.text = ""
|
|
||||||
}
|
}
|
||||||
|
|
||||||
SplitView {
|
SplitView {
|
||||||
|
|
@ -1279,313 +927,10 @@ ApplicationWindow {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
delegate: ColumnLayout {
|
delegate: ChatBlockDelegate {
|
||||||
id: blockItem
|
width: chatView.width - 8
|
||||||
width: chatView.width - 8
|
chatView: chatView
|
||||||
spacing: 2
|
|
||||||
|
|
||||||
property bool blockExpanded: {
|
|
||||||
switch (blockType) {
|
|
||||||
case "user": return true
|
|
||||||
case "assistant": return true
|
|
||||||
case "tool": return true
|
|
||||||
case "info": return true
|
|
||||||
case "retry": return true
|
|
||||||
case "interrupt": return true
|
|
||||||
default: return false
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Clickable header
|
|
||||||
Rectangle {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
height: 22
|
|
||||||
radius: 3
|
|
||||||
color: {
|
|
||||||
var bg = theme.baseColor
|
|
||||||
if (theme.isDark) {
|
|
||||||
switch (blockType) {
|
|
||||||
case "user": return Qt.lighter(bg, 1.8)
|
|
||||||
case "assistant": return Qt.lighter(bg, 1.5)
|
|
||||||
case "reasoning": return Qt.lighter(bg, 1.3)
|
|
||||||
case "call": return Qt.lighter(bg, 1.7)
|
|
||||||
case "tool": return Qt.lighter(bg, 1.2)
|
|
||||||
case "error": return Qt.rgba(0.6, 0.15, 0.15, 0.7)
|
|
||||||
case "info": return Qt.lighter(bg, 1.4)
|
|
||||||
case "retry": return Qt.rgba(0.6, 0.5, 0.0, 0.5)
|
|
||||||
case "interrupt": return "#3d2e00"
|
|
||||||
default: return bg
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
switch (blockType) {
|
|
||||||
case "user": return Qt.darker(bg, 1.12)
|
|
||||||
case "assistant": return Qt.darker(bg, 1.06)
|
|
||||||
case "reasoning": return Qt.darker(bg, 1.03)
|
|
||||||
case "call": return Qt.darker(bg, 1.1)
|
|
||||||
case "tool": return Qt.darker(bg, 1.04)
|
|
||||||
case "error": return Qt.rgba(1.0, 0.8, 0.8, 0.7)
|
|
||||||
case "info": return Qt.darker(bg, 1.02)
|
|
||||||
case "retry": return Qt.rgba(1.0, 0.9, 0.6, 0.5)
|
|
||||||
case "interrupt": return "#fff8e1"
|
|
||||||
default: return bg
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
anchors.fill: parent
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: blockItem.blockExpanded = !blockItem.blockExpanded
|
|
||||||
}
|
|
||||||
|
|
||||||
RowLayout {
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.leftMargin: 6
|
|
||||||
spacing: 4
|
|
||||||
|
|
||||||
Label {
|
|
||||||
text: blockItem.blockExpanded ? "▼" : "▶"
|
|
||||||
font.pixelSize: 8
|
|
||||||
opacity: 0.6
|
|
||||||
}
|
|
||||||
|
|
||||||
Label {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
text: {
|
|
||||||
switch (blockType) {
|
|
||||||
case "user": return "\uD83E\uDDD1 You"
|
|
||||||
case "assistant": return "\uD83E\uDD16 Ollie"
|
|
||||||
case "reasoning": return "Reasoning"
|
|
||||||
case "call": return "\u25B6\uFE0F " + blockName
|
|
||||||
case "tool": return "\uD83D\uDDA5\uFE0F " + blockName
|
|
||||||
case "error": return "\u274C Error"
|
|
||||||
case "info": return "\u2139\uFE0F Info"
|
|
||||||
case "retry": return "Retry"
|
|
||||||
case "interrupt": return "\u26A0\uFE0F User Interrupt"
|
|
||||||
default: return ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
font.pointSize: theme.fontSize > 0 ? theme.fontSize * 0.85 : 11
|
|
||||||
font.bold: true
|
|
||||||
color: theme.textColor
|
|
||||||
opacity: 0.9
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Block content rendered as segments (text and diagrams)
|
|
||||||
Column {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
visible: blockItem.blockExpanded
|
|
||||||
spacing: 4
|
|
||||||
|
|
||||||
Repeater {
|
|
||||||
model: blockSegments || []
|
|
||||||
|
|
||||||
delegate: Loader {
|
|
||||||
width: parent ? parent.width : 0
|
|
||||||
active: modelData !== null && modelData !== undefined
|
|
||||||
sourceComponent: (modelData && modelData.type === "diagram") ? diagramComponent : textComponent
|
|
||||||
|
|
||||||
property var segmentData: modelData || {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Text segment component
|
|
||||||
Component {
|
|
||||||
id: textComponent
|
|
||||||
|
|
||||||
TextEdit {
|
|
||||||
id: textSegmentEdit
|
|
||||||
width: parent ? parent.width : 100
|
|
||||||
text: segmentData.html || ""
|
|
||||||
textFormat: Text.RichText
|
|
||||||
readOnly: true
|
|
||||||
selectByMouse: true
|
|
||||||
persistentSelection: true
|
|
||||||
wrapMode: TextEdit.Wrap
|
|
||||||
font.family: theme.fontFamily || Qt.application.font.family
|
|
||||||
font.pointSize: theme.fontSize > 0 ? theme.fontSize : 12
|
|
||||||
selectionColor: theme.highlightColor
|
|
||||||
selectedTextColor: theme.highlightedTextColor
|
|
||||||
|
|
||||||
onActiveFocusChanged: {
|
|
||||||
if (!activeFocus)
|
|
||||||
chatView.interactive = true
|
|
||||||
}
|
|
||||||
|
|
||||||
Menu {
|
|
||||||
id: textSegmentMenu
|
|
||||||
MenuItem {
|
|
||||||
text: "Copy"
|
|
||||||
enabled: textSegmentEdit.selectedText.length > 0
|
|
||||||
onTriggered: textSegmentEdit.copy()
|
|
||||||
}
|
|
||||||
MenuItem {
|
|
||||||
text: "Select All"
|
|
||||||
onTriggered: textSegmentEdit.selectAll()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
anchors.fill: parent
|
|
||||||
acceptedButtons: Qt.LeftButton
|
|
||||||
propagateComposedEvents: true
|
|
||||||
cursorShape: Qt.IBeamCursor
|
|
||||||
onPressed: function(mouse) {
|
|
||||||
chatView.interactive = false
|
|
||||||
mouse.accepted = false
|
|
||||||
}
|
|
||||||
onReleased: function(mouse) {
|
|
||||||
chatView.interactive = true
|
|
||||||
mouse.accepted = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
anchors.fill: parent
|
|
||||||
acceptedButtons: Qt.RightButton
|
|
||||||
cursorShape: Qt.IBeamCursor
|
|
||||||
onClicked: textSegmentMenu.popup()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Diagram segment component with source/image toggle
|
|
||||||
Component {
|
|
||||||
id: diagramComponent
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: parent ? parent.width : 100
|
|
||||||
height: diagramContent.height + diagramHeader.height + 8
|
|
||||||
color: "transparent"
|
|
||||||
border.color: theme.midColor
|
|
||||||
border.width: 1
|
|
||||||
radius: 4
|
|
||||||
|
|
||||||
property bool showSource: false
|
|
||||||
|
|
||||||
// Header with language label and toggle button
|
|
||||||
RowLayout {
|
|
||||||
id: diagramHeader
|
|
||||||
anchors.top: parent.top
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.margins: 4
|
|
||||||
height: 24
|
|
||||||
|
|
||||||
Label {
|
|
||||||
text: segmentData.lang || "diagram"
|
|
||||||
font.pointSize: theme.fontSize > 0 ? theme.fontSize - 1 : 11
|
|
||||||
color: theme.textColor
|
|
||||||
opacity: 0.7
|
|
||||||
}
|
|
||||||
|
|
||||||
Item { Layout.fillWidth: true }
|
|
||||||
|
|
||||||
Button {
|
|
||||||
text: parent.parent.showSource ? "📊" : "</>"
|
|
||||||
font.pointSize: theme.fontSize > 0 ? theme.fontSize - 2 : 10
|
|
||||||
implicitWidth: 32
|
|
||||||
implicitHeight: 20
|
|
||||||
ToolTip.visible: hovered
|
|
||||||
ToolTip.text: parent.parent.showSource ? "Show diagram" : "Show source"
|
|
||||||
onClicked: parent.parent.showSource = !parent.parent.showSource
|
|
||||||
}
|
|
||||||
|
|
||||||
Button {
|
|
||||||
text: "📋"
|
|
||||||
font.pointSize: theme.fontSize > 0 ? theme.fontSize - 2 : 10
|
|
||||||
implicitWidth: 32
|
|
||||||
implicitHeight: 20
|
|
||||||
ToolTip.visible: hovered
|
|
||||||
ToolTip.text: parent.parent.showSource ? "Copy source" : "Copy image"
|
|
||||||
onClicked: {
|
|
||||||
if (parent.parent.showSource) {
|
|
||||||
// Copy source code as plain text
|
|
||||||
clipboard.copyText(segmentData.source)
|
|
||||||
} else if (segmentData.svg) {
|
|
||||||
// Copy image
|
|
||||||
clipboard.copyImage(segmentData.svg)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Content area
|
|
||||||
Item {
|
|
||||||
id: diagramContent
|
|
||||||
anchors.top: diagramHeader.bottom
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.margins: 4
|
|
||||||
height: showSource ? diagramSourceEdit.height : diagramImage.height
|
|
||||||
|
|
||||||
// Source view
|
|
||||||
TextEdit {
|
|
||||||
id: diagramSourceEdit
|
|
||||||
width: parent.width
|
|
||||||
visible: showSource
|
|
||||||
text: segmentData.html || ""
|
|
||||||
textFormat: Text.RichText
|
|
||||||
readOnly: true
|
|
||||||
selectByMouse: true
|
|
||||||
persistentSelection: true
|
|
||||||
wrapMode: TextEdit.Wrap
|
|
||||||
font.family: theme.fontFamily || Qt.application.font.family
|
|
||||||
font.pointSize: theme.fontSize > 0 ? theme.fontSize : 12
|
|
||||||
selectionColor: theme.highlightColor
|
|
||||||
selectedTextColor: theme.highlightedTextColor
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
anchors.fill: parent
|
|
||||||
acceptedButtons: Qt.LeftButton
|
|
||||||
propagateComposedEvents: true
|
|
||||||
cursorShape: Qt.IBeamCursor
|
|
||||||
onPressed: function(mouse) {
|
|
||||||
chatView.interactive = false
|
|
||||||
mouse.accepted = false
|
|
||||||
}
|
|
||||||
onReleased: function(mouse) {
|
|
||||||
chatView.interactive = true
|
|
||||||
mouse.accepted = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Image view
|
|
||||||
Column {
|
|
||||||
id: diagramImage
|
|
||||||
width: parent.width
|
|
||||||
visible: !showSource
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
visible: segmentData.svg && !segmentData.loading
|
|
||||||
color: "white"
|
|
||||||
width: diagramImg.width
|
|
||||||
height: diagramImg.height
|
|
||||||
|
|
||||||
Image {
|
|
||||||
id: diagramImg
|
|
||||||
source: segmentData.svg || ""
|
|
||||||
fillMode: Image.PreserveAspectFit
|
|
||||||
width: Math.min(implicitWidth, diagramImage.width)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Label {
|
|
||||||
visible: segmentData.loading
|
|
||||||
text: "Loading " + (segmentData.lang || "diagram") + "..."
|
|
||||||
font.italic: true
|
|
||||||
color: theme.textColor
|
|
||||||
opacity: 0.7
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,9 @@
|
||||||
<RCC>
|
<RCC>
|
||||||
<qresource prefix="/">
|
<qresource prefix="/">
|
||||||
<file>main.qml</file>
|
<file>main.qml</file>
|
||||||
|
<file>SettingsDialog.qml</file>
|
||||||
|
<file>NewSessionDialog.qml</file>
|
||||||
|
<file>NewAgentDialog.qml</file>
|
||||||
|
<file>ChatBlockDelegate.qml</file>
|
||||||
</qresource>
|
</qresource>
|
||||||
</RCC>
|
</RCC>
|
||||||
|
|
|
||||||
Reference in New Issue