360 lines
12 KiB
QML
360 lines
12 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
|
|
|
|
Component.onCompleted: {
|
|
var log = ollie.readLog()
|
|
if (log) chatModel.appendData(log)
|
|
}
|
|
|
|
Connections {
|
|
target: ollie
|
|
function onActiveSessionIdChanged() {
|
|
chatModel.reset()
|
|
var log = ollie.readLog()
|
|
if (log) chatModel.appendData(log)
|
|
}
|
|
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
|
|
|
|
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: palette.mid }
|
|
|
|
// 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) {
|
|
positionViewAtEnd()
|
|
}
|
|
}
|
|
|
|
Connections {
|
|
target: chatModel
|
|
function onDataChanged() {
|
|
if (chatView.atYEnd)
|
|
chatView.positionViewAtEnd()
|
|
}
|
|
}
|
|
|
|
delegate: ColumnLayout {
|
|
width: chatView.width - 8
|
|
spacing: 2
|
|
|
|
// Header decoration
|
|
Rectangle {
|
|
Layout.fillWidth: true
|
|
height: 22
|
|
radius: 3
|
|
color: {
|
|
switch (blockType) {
|
|
case "user": return "#e3f2fd"
|
|
case "assistant": return "#f3e5f5"
|
|
case "reasoning": return "#fff3e0"
|
|
case "call": return "#e8f5e9"
|
|
case "tool": return "#fce4ec"
|
|
case "error": return "#ffebee"
|
|
case "info": return "#f5f5f5"
|
|
default: return "#fafafa"
|
|
}
|
|
}
|
|
|
|
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
|
|
opacity: 0.8
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Rectangle { Layout.fillWidth: true; height: 1; color: palette.mid }
|
|
|
|
// 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()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} |