aboutsummaryrefslogtreecommitdiffstats
path: root/desktop/assistant/Message.qml
blob: 0aa686a2137242561228ad901526f9c5e7f59f09 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
// Copyright (C) 2026 Danilo M. <danix@danix.xyz>
//
// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License version 2 as
// published by the Free Software Foundation.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.

import QtQuick
import ".."

// One turn: the question, then the reply with its thinking line, the tool
// chips, and an error or stopped mark. Both texts go through Segments, so
// the question renders as markdown too.
Column {
    id: msg
    required property int index
    required property string user
    required property string reply
    required property string think
    required property string tools
    required property string files       // JSON: [{name, kind, pages, truncated}]
    required property string error
    required property bool live
    required property bool stopped
    required property bool expanded
    required property var model         // the row, for setting expanded
    property string running: ""          // the tool running now, if any

    spacing: 6

    Rectangle {
        width: msg.width
        height: question.height + 16
        radius: 8
        color: Qt.alpha(Theme.surface, 0.7)
        Segments {
            id: question
            anchors { left: parent.left; right: parent.right; top: parent.top; margins: 8 }
            text: msg.user
        }
    }

    // The files that went with the question, as the daemon read them.
    Flow {
        width: msg.width
        spacing: 6
        visible: msg.files !== "[]"
        Repeater {
            model: JSON.parse(msg.files)
            Rectangle {
                required property var modelData
                width: f.implicitWidth + 16
                height: f.implicitHeight + 6
                radius: height / 2
                color: Qt.alpha(Theme.surface, 0.6)
                Text {
                    id: f
                    anchors.centerIn: parent
                    text: parent.modelData.name
                        + (parent.modelData.pages ? ` · ${parent.modelData.pages} pages` : "")
                        + (parent.modelData.truncated ? " · truncated" : "")
                    color: parent.modelData.truncated ? Theme.yellow : Theme.subtext
                    font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 5 }
                }
            }
        }
    }

    // Collapsed by default; a click shows the reasoning as plain text.
    Text {
        visible: msg.think !== ""
        text: (msg.expanded ? "▾ " : "▸ ") + (msg.live && msg.reply === "" ? "thinking…" : "thinking")
        color: Theme.overlay
        font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 4 }
        MouseArea {
            anchors.fill: parent
            cursorShape: Qt.PointingHandCursor
            onClicked: msg.model.expanded = !msg.expanded
        }
    }
    Text {
        width: msg.width
        visible: msg.expanded && msg.think !== ""
        text: msg.think
        textFormat: Text.PlainText
        wrapMode: Text.Wrap
        color: Theme.subtext
        font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 4 }
    }

    Flow {
        width: msg.width
        spacing: 6
        visible: msg.tools !== ""
        Repeater {
            model: msg.tools ? msg.tools.split(",") : []
            Rectangle {
                required property string modelData
                readonly property bool lit: msg.live && msg.running === modelData
                property int elapsed: 0
                width: chip.implicitWidth + 14
                height: chip.implicitHeight + 6
                radius: height / 2
                color: Qt.alpha(Theme.surface, 0.9)
                opacity: lit ? 1 : 0.55
                // A lit chip alone looks frozen on a clone that takes a
                // minute, so the running tool counts seconds.
                onLitChanged: if (!lit) elapsed = 0
                Timer {
                    running: parent.lit
                    interval: 1000
                    repeat: true
                    onTriggered: parent.elapsed++
                }
                Text {
                    id: chip
                    anchors.centerIn: parent
                    text: parent.modelData + (parent.lit ? " · " + parent.elapsed + "s" : "")
                    color: parent.lit ? Theme.text : Theme.subtext
                    font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 5 }
                }
            }
        }
    }

    Segments {
        width: msg.width
        text: msg.reply
        visible: msg.reply !== ""
    }

    Text {
        visible: msg.error !== "" || msg.stopped
        width: msg.width
        text: msg.error !== "" ? msg.error : "stopped"
        color: msg.error !== "" ? Theme.red : Theme.overlay
        wrapMode: Text.Wrap
        font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 4 }
    }
}