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)
|
||||
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}")
|
||||
# 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)
|
||||
qt5_add_resources(GUI_KF5_QRC ${CMAKE_CURRENT_BINARY_DIR}/gui-kf5/qml.qrc)
|
||||
add_executable(ollie-gui
|
||||
|
|
@ -170,7 +175,12 @@ else()
|
|||
qt_add_resources(ollie-gui "gui"
|
||||
PREFIX "/"
|
||||
BASE gui
|
||||
FILES gui/main.qml
|
||||
FILES
|
||||
gui/main.qml
|
||||
gui/SettingsDialog.qml
|
||||
gui/NewSessionDialog.qml
|
||||
gui/NewAgentDialog.qml
|
||||
gui/ChatBlockDelegate.qml
|
||||
)
|
||||
|
||||
# --- 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 ---
|
||||
Dialog {
|
||||
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 ---
|
||||
Dialog {
|
||||
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 ---
|
||||
Dialog {
|
||||
NewAgentDialog {
|
||||
id: newAgentDialog
|
||||
title: "New Agent"
|
||||
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 = ""
|
||||
targetSessionId: root.newAgentTargetSessionId
|
||||
}
|
||||
|
||||
SplitView {
|
||||
|
|
@ -1279,313 +927,10 @@ ApplicationWindow {
|
|||
}
|
||||
}
|
||||
|
||||
delegate: ColumnLayout {
|
||||
id: blockItem
|
||||
width: chatView.width - 8
|
||||
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
|
||||
delegate: ChatBlockDelegate {
|
||||
width: chatView.width - 8
|
||||
chatView: chatView
|
||||
}
|
||||
}
|
||||
|
||||
// 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>
|
||||
<qresource prefix="/">
|
||||
<file>main.qml</file>
|
||||
<file>SettingsDialog.qml</file>
|
||||
<file>NewSessionDialog.qml</file>
|
||||
<file>NewAgentDialog.qml</file>
|
||||
<file>ChatBlockDelegate.qml</file>
|
||||
</qresource>
|
||||
</RCC>
|
||||
|
|
|
|||
Reference in New Issue