aboutsummaryrefslogtreecommitdiffstats
path: root/desktop/assistant/Message.qml
blob: c724c2efb04b316353d49304b5cac32e11d4b8a4 (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
// 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 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
        }
    }

    // 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 on: msg.live && msg.running === modelData
                width: chip.implicitWidth + 14
                height: chip.implicitHeight + 6
                radius: height / 2
                color: Qt.alpha(Theme.surface, 0.9)
                opacity: on ? 1 : 0.55
                Text {
                    id: chip
                    anchors.centerIn: parent
                    text: parent.modelData
                    color: parent.on ? 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 }
    }
}