This repository has been archived on 2026-08-16. You can view files and clone it, but cannot push or open issues or pull requests.
ollie-kde/gui/main.qml

450 lines
17 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
// Deferred scroll timer — fires once after model changes settle
Timer {
id: scrollTimer
interval: 0
repeat: false
onTriggered: chatView.positionViewAtEnd()
}
Component.onCompleted: {
var log = ollie.readLog()
if (log) {
chatModel.appendData(log)
chatModel.trimFront(30)
}
scrollTimer.start()
}
Connections {
target: ollie
function onActiveSessionIdChanged() {
chatModel.reset()
var log = ollie.readLog()
if (log) {
chatModel.appendData(log)
chatModel.trimFront(30)
}
scrollTimer.start()
}
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: {
if (count > 0)
scrollTimer.start()
}
Connections {
target: chatModel
function onDataChanged() {
if (chatView.atYEnd && chatView.count > 0)
scrollTimer.start()
}
}
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()
}
}
}
}
}