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) } 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 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) { 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: { // 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() } } } } }