290 lines
12 KiB
QML
290 lines
12 KiB
QML
import QtQuick 2.15
|
|
import QtQuick.Controls 2.15
|
|
import QtQuick.Layouts 1.15
|
|
|
|
Item {
|
|
id: chatPane
|
|
property bool sidebarVisible: true
|
|
signal toggleSidebar()
|
|
function scrollToBottom() {
|
|
chatView.positionViewAtEnd()
|
|
}
|
|
function loadChat() {
|
|
chatModel.reset()
|
|
if (ollie.activeSessionId && ollie.activeAgentId) {
|
|
var log = ollie.readLogForSession(ollie.activeSessionId, ollie.activeAgentId)
|
|
if (log) { chatModel.appendData(log); chatModel.trimFront(20) }
|
|
}
|
|
scrollToBottom()
|
|
}
|
|
|
|
Component.onCompleted: {
|
|
loadChat()
|
|
scrollToBottom()
|
|
}
|
|
|
|
ColumnLayout {
|
|
anchors.fill: parent
|
|
spacing: 0
|
|
|
|
// Header - shows active session/agent and state
|
|
RowLayout {
|
|
Layout.fillWidth: true
|
|
Layout.margins: 6
|
|
spacing: 8
|
|
|
|
// Sidebar toggle button
|
|
ToolButton {
|
|
text: chatPane.sidebarVisible ? "◀" : "▶"
|
|
font.pixelSize: 12
|
|
implicitWidth: 24
|
|
implicitHeight: 24
|
|
onClicked: chatPane.toggleSidebar()
|
|
ToolTip.text: chatPane.sidebarVisible ? "Hide sidebar (Ctrl+B)" : "Show sidebar (Ctrl+B)"
|
|
ToolTip.visible: hovered
|
|
}
|
|
|
|
// Active session/agent label
|
|
Label {
|
|
text: {
|
|
var sessionName = ""
|
|
var agentName = ""
|
|
for (var i = 0; i < sessionModel.count; ++i) {
|
|
var item = sessionModel.get(i)
|
|
if (item.type === "session" && item.id === ollie.activeSessionId) {
|
|
sessionName = item.displayName || item.id
|
|
}
|
|
if (item.type === "agent" && item.agentId === ollie.activeAgentId && item.parentId === ollie.activeSessionId) {
|
|
agentName = item.displayName || item.agentId
|
|
}
|
|
}
|
|
if (sessionName && agentName) {
|
|
return sessionName + " / " + agentName
|
|
} else if (sessionName) {
|
|
return sessionName + " (no agent)"
|
|
}
|
|
return "(no session)"
|
|
}
|
|
font.bold: true
|
|
font.pointSize: theme.fontSize > 0 ? theme.fontSize : Qt.application.font.pointSize
|
|
elide: Text.ElideMiddle
|
|
Layout.fillWidth: true
|
|
}
|
|
|
|
// State indicator
|
|
Rectangle {
|
|
width: 10; height: 10; radius: 5
|
|
color: {
|
|
if (!ollie.activeAgentId) return "#9e9e9e" // gray when no agent
|
|
if (ollie.activeSessionId && ollie.isSessionPaused(ollie.activeSessionId)) return "#9e9e9e" // gray when paused
|
|
if (ollie.activeState === "idle") return "#4caf50"
|
|
if (ollie.activeState.startsWith("calling")) return "#ff9800"
|
|
return "#2196f3"
|
|
}
|
|
}
|
|
|
|
Label {
|
|
text: {
|
|
if (!ollie.activeAgentId) return ""
|
|
if (ollie.activeSessionId && ollie.isSessionPaused(ollie.activeSessionId)) return "inactive"
|
|
return ollie.activeState
|
|
}
|
|
opacity: 0.7
|
|
font.pointSize: theme.fontSize > 0 ? theme.fontSize * 0.85 : 11
|
|
}
|
|
}
|
|
|
|
Rectangle { Layout.fillWidth: true; height: 1; color: theme.midColor }
|
|
|
|
// Chat display — block-based ListView with scroll-to-bottom button
|
|
Item {
|
|
Layout.fillWidth: true
|
|
Layout.fillHeight: true
|
|
clip: true
|
|
|
|
// Placeholder when no agent selected or disconnected
|
|
Label {
|
|
anchors.centerIn: parent
|
|
text: {
|
|
if (!ollie.daemonConnected) return "Not connected"
|
|
if (!ollie.activeSessionId) return "Select a session"
|
|
return "Select an agent to view chat"
|
|
}
|
|
color: theme.midColor
|
|
font.italic: true
|
|
font.pixelSize: theme.fontSize > 0 ? theme.fontSize : 12
|
|
visible: !ollie.activeAgentId || !ollie.daemonConnected
|
|
}
|
|
|
|
ListView {
|
|
id: chatView
|
|
model: chatModel
|
|
spacing: 4
|
|
anchors.fill: parent
|
|
anchors.margins: 4
|
|
visible: ollie.daemonConnected && ollie.activeAgentId !== ""
|
|
|
|
delegate: ChatBlockDelegate {
|
|
width: chatView.width - 8
|
|
}
|
|
}
|
|
|
|
// Chat overview and navigation minimap
|
|
Rectangle {
|
|
id: chatMinimap
|
|
anchors.top: chatView.top
|
|
anchors.right: parent.right
|
|
anchors.bottom: chatView.bottom
|
|
width: 24
|
|
color: Qt.rgba(theme.midColor.r, theme.midColor.g, theme.midColor.b, 0.05)
|
|
visible: chatView.visible && chatModel.count > 0
|
|
|
|
Column {
|
|
anchors.fill: parent
|
|
spacing: 1
|
|
Repeater {
|
|
model: chatModel
|
|
Rectangle {
|
|
width: chatMinimap.width
|
|
height: Math.max(2, chatMinimap.height / Math.max(1, chatModel.count) - 1)
|
|
color: {
|
|
switch (model.blockType) {
|
|
case "user": return "#4caf50"
|
|
case "assistant": return "#2196f3"
|
|
case "error": return "#f44336"
|
|
case "tool": return "#ff9800"
|
|
default: return theme.midColor
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
MouseArea {
|
|
anchors.fill: parent
|
|
onClicked: chatView.positionViewAtIndex(
|
|
Math.floor(mouse.y / height * chatModel.count), ListView.Beginning)
|
|
}
|
|
}
|
|
Button {
|
|
anchors.right: parent.right
|
|
anchors.bottom: parent.bottom
|
|
anchors.margins: 12
|
|
text: "↓"
|
|
visible: chatView.visible && !chatView.atYEnd
|
|
onClicked: chatPane.scrollToBottom()
|
|
ToolTip.text: "Scroll to bottom"
|
|
ToolTip.visible: hovered
|
|
}
|
|
|
|
}
|
|
|
|
Rectangle { Layout.fillWidth: true; height: 1; color: theme.midColor }
|
|
|
|
// Input
|
|
RowLayout {
|
|
Layout.fillWidth: true
|
|
Layout.margins: 6
|
|
spacing: 4
|
|
|
|
// TextArea in ScrollView for overflow, with context menu overlay
|
|
Item {
|
|
Layout.fillWidth: true
|
|
Layout.maximumHeight: 200
|
|
Layout.preferredHeight: Math.min(promptInput.contentHeight + 16, 200)
|
|
clip: true
|
|
|
|
ScrollView {
|
|
id: promptScroll
|
|
anchors.fill: parent
|
|
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
|
|
|
|
TextArea {
|
|
id: promptInput
|
|
width: promptScroll.width
|
|
placeholderText: {
|
|
if (!ollie.daemonConnected) return "Not connected"
|
|
if (!ollie.activeSessionId) return "No session"
|
|
if (!ollie.activeAgentId) return "No agent"
|
|
if (ollie.isSessionPaused(ollie.activeSessionId)) return "Session paused"
|
|
return "Message..."
|
|
}
|
|
enabled: ollie.daemonConnected && ollie.activeSessionId !== "" && ollie.activeAgentId !== "" && !ollie.isSessionPaused(ollie.activeSessionId)
|
|
wrapMode: TextEdit.Wrap
|
|
selectByMouse: true
|
|
persistentSelection: true
|
|
|
|
property string _lastText: ""
|
|
property int _lastCursorPos: 0
|
|
|
|
onTextChanged: {
|
|
_lastText = text
|
|
_lastCursorPos = cursorPosition
|
|
}
|
|
|
|
Keys.onReturnPressed: function(event) {
|
|
if (event.modifiers & Qt.ShiftModifier) {
|
|
event.accepted = false
|
|
} else {
|
|
if (text.trim() !== "") {
|
|
ollie.submit(text.trim())
|
|
text = ""
|
|
}
|
|
event.accepted = true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Context menu overlay - only catches right-click, on top of ScrollView
|
|
MouseArea {
|
|
anchors.fill: parent
|
|
acceptedButtons: Qt.RightButton
|
|
cursorShape: Qt.IBeamCursor
|
|
onClicked: promptContextMenu.popup()
|
|
}
|
|
|
|
Menu {
|
|
id: promptContextMenu
|
|
MenuItem {
|
|
text: "Cut"
|
|
enabled: promptInput.selectedText.length > 0
|
|
onTriggered: promptInput.cut()
|
|
}
|
|
MenuItem {
|
|
text: "Copy"
|
|
enabled: promptInput.selectedText.length > 0
|
|
onTriggered: promptInput.copy()
|
|
}
|
|
MenuItem {
|
|
text: "Paste"
|
|
enabled: promptInput.canPaste
|
|
onTriggered: promptInput.paste()
|
|
}
|
|
MenuSeparator {}
|
|
MenuItem {
|
|
text: "Select All"
|
|
onTriggered: promptInput.selectAll()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
Connections {
|
|
target: ollie
|
|
function onSessionsChanged() { chatPane.loadChat() }
|
|
function onActiveSessionIdChanged() { chatPane.loadChat() }
|
|
function onActiveAgentIdChanged() { chatPane.loadChat() }
|
|
}
|
|
|
|
Timer {
|
|
interval: 0
|
|
repeat: false
|
|
running: true
|
|
onTriggered: chatPane.loadChat()
|
|
}
|
|
|
|
}
|