439 lines
16 KiB
QML
439 lines
16 KiB
QML
import QtQuick 2.15
|
|
import QtQuick.Controls 2.15
|
|
import QtQuick.Layouts 1.15
|
|
|
|
ApplicationWindow {
|
|
id: root
|
|
visible: true
|
|
width: 1000
|
|
height: 600
|
|
title: "Ollie"
|
|
font.family: theme.fontFamily || Qt.application.font.family
|
|
font.pointSize: theme.fontSize > 0 ? theme.fontSize : Qt.application.font.pointSize
|
|
|
|
// Apply theme palette to all Qt Quick Controls
|
|
palette.active.window: theme.windowColor
|
|
palette.active.windowText: theme.windowTextColor
|
|
palette.active.base: theme.baseColor
|
|
palette.active.text: theme.textColor
|
|
palette.active.button: theme.buttonColor
|
|
palette.active.buttonText: theme.buttonTextColor
|
|
palette.active.highlight: theme.highlightColor
|
|
palette.active.highlightedText: theme.highlightedTextColor
|
|
palette.active.mid: theme.midColor
|
|
palette.active.dark: theme.darkColor
|
|
palette.active.light: theme.lightColor
|
|
palette.active.shadow: theme.shadowColor
|
|
palette.active.link: theme.linkColor
|
|
palette.active.toolTipBase: theme.tooltipBaseColor
|
|
palette.active.toolTipText: theme.tooltipTextColor
|
|
palette.active.placeholderText: theme.placeholderTextColor
|
|
palette.active.alternateBase: theme.alternateBaseColor
|
|
palette.inactive.window: theme.windowColor
|
|
palette.inactive.windowText: theme.windowTextColor
|
|
palette.inactive.base: theme.baseColor
|
|
palette.inactive.text: theme.textColor
|
|
palette.inactive.button: theme.buttonColor
|
|
palette.inactive.buttonText: theme.buttonTextColor
|
|
palette.inactive.highlight: theme.highlightColor
|
|
palette.inactive.highlightedText: theme.highlightedTextColor
|
|
palette.inactive.mid: theme.midColor
|
|
palette.inactive.dark: theme.darkColor
|
|
palette.inactive.light: theme.lightColor
|
|
palette.inactive.shadow: theme.shadowColor
|
|
palette.inactive.link: theme.linkColor
|
|
palette.inactive.toolTipBase: theme.tooltipBaseColor
|
|
palette.inactive.toolTipText: theme.tooltipTextColor
|
|
palette.inactive.placeholderText: theme.placeholderTextColor
|
|
palette.inactive.alternateBase: theme.alternateBaseColor
|
|
palette.disabled.window: theme.windowColor
|
|
palette.disabled.windowText: theme.midColor
|
|
palette.disabled.base: theme.baseColor
|
|
palette.disabled.text: theme.midColor
|
|
palette.disabled.button: theme.buttonColor
|
|
palette.disabled.buttonText: theme.midColor
|
|
palette.disabled.highlight: theme.highlightColor
|
|
palette.disabled.highlightedText: theme.highlightedTextColor
|
|
palette.disabled.mid: theme.midColor
|
|
palette.disabled.dark: theme.darkColor
|
|
palette.disabled.light: theme.lightColor
|
|
palette.disabled.shadow: theme.shadowColor
|
|
|
|
Component.onCompleted: {
|
|
var log = ollie.readLog()
|
|
if (log) {
|
|
chatModel.appendData(log)
|
|
chatModel.trimFront(30)
|
|
}
|
|
}
|
|
|
|
Connections {
|
|
target: ollie
|
|
function onActiveSessionIdChanged() {
|
|
chatModel.reset()
|
|
var log = ollie.readLog()
|
|
if (log) {
|
|
chatModel.appendData(log)
|
|
chatModel.trimFront(30)
|
|
}
|
|
}
|
|
function onStreamingDone() {
|
|
chatModel.appendData("\n[info]\nsession ended\n")
|
|
}
|
|
}
|
|
|
|
// --- Settings Dialog ---
|
|
Dialog {
|
|
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()
|
|
}
|
|
}
|
|
}
|
|
|
|
SplitView {
|
|
anchors.fill: parent
|
|
|
|
// Left pane: session list
|
|
ColumnLayout {
|
|
SplitView.preferredWidth: 200
|
|
SplitView.minimumWidth: 120
|
|
spacing: 0
|
|
|
|
ToolBar {
|
|
Layout.fillWidth: true
|
|
RowLayout {
|
|
anchors.fill: parent
|
|
Label { text: "Sessions"; Layout.fillWidth: true; font.bold: true; leftPadding: 8 }
|
|
ToolButton {
|
|
text: "\u21BB"
|
|
onClicked: ollie.refreshSessions()
|
|
}
|
|
ToolButton {
|
|
text: "\u2699"
|
|
onClicked: settingsDialog.open()
|
|
ToolTip.text: "Settings"
|
|
ToolTip.visible: hovered
|
|
}
|
|
}
|
|
}
|
|
|
|
ListView {
|
|
id: sessionList
|
|
Layout.fillWidth: true
|
|
Layout.fillHeight: true
|
|
model: ollie.sessions
|
|
clip: true
|
|
|
|
delegate: ItemDelegate {
|
|
width: sessionList.width
|
|
text: modelData.id || ""
|
|
highlighted: (modelData.id === ollie.activeSessionId)
|
|
onClicked: ollie.activeSessionId = modelData.id
|
|
contentItem: Label {
|
|
text: modelData.id || ""
|
|
color: theme.textColor
|
|
font.pointSize: theme.fontSize > 0 ? theme.fontSize : Qt.application.font.pointSize
|
|
verticalAlignment: Text.AlignVCenter
|
|
leftPadding: 8
|
|
}
|
|
|
|
Rectangle {
|
|
width: 8; height: 8; radius: 4
|
|
anchors.right: parent.right
|
|
anchors.rightMargin: 8
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
color: {
|
|
var s = modelData.state || "idle"
|
|
if (s === "idle") return "#4caf50"
|
|
if (s.startsWith("calling")) return "#ff9800"
|
|
return "#2196f3"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Right pane: chat + input
|
|
ColumnLayout {
|
|
spacing: 0
|
|
|
|
// Header
|
|
RowLayout {
|
|
Layout.fillWidth: true
|
|
Layout.margins: 6
|
|
spacing: 8
|
|
|
|
Rectangle {
|
|
width: 10; height: 10; radius: 5
|
|
color: {
|
|
if (ollie.activeState === "idle") return "#4caf50"
|
|
if (ollie.activeState.startsWith("calling")) return "#ff9800"
|
|
return "#2196f3"
|
|
}
|
|
}
|
|
|
|
Label {
|
|
text: ollie.activeState
|
|
opacity: 0.7
|
|
font.pointSize: theme.fontSize > 0 ? theme.fontSize * 0.85 : 11
|
|
}
|
|
|
|
Item { Layout.fillWidth: true }
|
|
|
|
ToolButton {
|
|
text: "Stop"
|
|
onClicked: ollie.interrupt()
|
|
}
|
|
}
|
|
|
|
Rectangle { Layout.fillWidth: true; height: 1; color: theme.midColor }
|
|
|
|
// Chat display — block-based ListView
|
|
ScrollView {
|
|
Layout.fillWidth: true
|
|
Layout.fillHeight: true
|
|
clip: true
|
|
|
|
ListView {
|
|
id: chatView
|
|
model: chatModel
|
|
spacing: 4
|
|
anchors.fill: parent
|
|
anchors.margins: 4
|
|
boundsBehavior: Flickable.StopAtBounds
|
|
|
|
onCountChanged: {
|
|
Qt.callLater(function() { positionViewAtEnd() })
|
|
}
|
|
|
|
Connections {
|
|
target: chatModel
|
|
function onDataChanged() {
|
|
if (chatView.atYEnd)
|
|
Qt.callLater(function() { chatView.positionViewAtEnd() })
|
|
}
|
|
}
|
|
|
|
delegate: ColumnLayout {
|
|
width: chatView.width - 8
|
|
spacing: 2
|
|
|
|
// Header decoration
|
|
Rectangle {
|
|
Layout.fillWidth: true
|
|
height: 22
|
|
radius: 3
|
|
color: {
|
|
// Derive header colors from base background with subtle tint.
|
|
// Always readable because text uses theme.textColor.
|
|
var bg = theme.baseColor
|
|
if (theme.isDark) {
|
|
// Dark theme: lighter tints against dark background
|
|
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)
|
|
default: return bg
|
|
}
|
|
} else {
|
|
// Light theme: slightly darker tints
|
|
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)
|
|
default: return bg
|
|
}
|
|
}
|
|
}
|
|
|
|
RowLayout {
|
|
anchors.fill: parent
|
|
anchors.leftMargin: 6
|
|
spacing: 4
|
|
|
|
Label {
|
|
text: {
|
|
switch (blockType) {
|
|
case "user": return "User"
|
|
case "assistant": return "Assistant" + (blockName ? " (" + blockName + ")" : "")
|
|
case "reasoning": return "Reasoning"
|
|
case "call": return "Call: " + blockName
|
|
case "tool": return "Tool: " + blockName
|
|
case "error": return "Error"
|
|
case "info": return "Info"
|
|
default: return ""
|
|
}
|
|
}
|
|
font.pointSize: theme.fontSize > 0 ? theme.fontSize * 0.85 : 11
|
|
font.bold: true
|
|
color: theme.textColor
|
|
opacity: 0.9
|
|
}
|
|
}
|
|
}
|
|
|
|
// Content
|
|
Label {
|
|
Layout.fillWidth: true
|
|
Layout.leftMargin: 4
|
|
Layout.rightMargin: 4
|
|
Layout.bottomMargin: 4
|
|
text: blockContent || ""
|
|
wrapMode: Text.Wrap
|
|
textFormat: Text.PlainText
|
|
font.family: theme.fontFamily || "monospace"
|
|
font.pointSize: theme.fontSize > 0 ? theme.fontSize : 12
|
|
color: theme.textColor
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Rectangle { Layout.fillWidth: true; height: 1; color: theme.midColor }
|
|
|
|
// Input
|
|
RowLayout {
|
|
Layout.fillWidth: true
|
|
Layout.margins: 6
|
|
spacing: 4
|
|
|
|
TextField {
|
|
id: promptInput
|
|
Layout.fillWidth: true
|
|
placeholderText: ollie.activeSessionId ? "Message..." : "No session"
|
|
enabled: ollie.activeSessionId !== ""
|
|
onAccepted: {
|
|
if (text.trim() !== "") {
|
|
ollie.submit(text.trim())
|
|
text = ""
|
|
}
|
|
}
|
|
}
|
|
|
|
Button {
|
|
text: "Send"
|
|
enabled: promptInput.text.trim() !== "" && ollie.activeSessionId !== ""
|
|
onClicked: promptInput.accepted()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} |