The Basic style paints its own controls: the two text fields came out as white boxes on the dark panel and the buttons looked nothing like the rest of the window. Both fields now carry the surface, border and focus colors the chat cards use (same shape as AskDialog's field), and the buttons follow the composer: Close is a quiet bordered button, Use this is the accent one. No behavior change; 36/36 UI checks still pass and the dialog was looked at again through CST_APP_SHOT. Version 0.8.11.
362 lines
14 KiB
QML
362 lines
14 KiB
QML
import QtQuick
|
|
import QtQuick.Controls
|
|
import QtQuick.Layouts
|
|
|
|
// Provider, model and key: what the engine needs before it can talk to a model
|
|
// at all. Everything here goes through AgentCore::useProvider(), which moves
|
|
// the engine's own config (its session stays where it is) and remembers the
|
|
// choice. A key is verified against the provider before it is saved and is
|
|
// never read back into this window -- the dialog only ever says where the key
|
|
// it will use comes from.
|
|
Dialog {
|
|
id: dlg
|
|
objectName: "settingsDialog"
|
|
parent: Overlay.overlay
|
|
anchors.centerIn: parent
|
|
modal: true
|
|
closePolicy: Popup.CloseOnEscape
|
|
padding: Theme.pad
|
|
|
|
property var providers: [] // AgentCore::providers()
|
|
property var choices: [] // AgentCore::modelChoices(picked)
|
|
property string picked: ""
|
|
property string model: ""
|
|
property string note: "" // what the last "Use this" answered
|
|
property string keyEnv: ""
|
|
property string keyFrom: ""
|
|
property bool hasKey: false
|
|
|
|
signal applyRequested(string provider, string model, string key)
|
|
|
|
width: Math.min(580, parent ? parent.width - 32 : 580)
|
|
height: Math.min(560, parent ? parent.height - 60 : 560)
|
|
|
|
/* The Basic style paints a light panel; this window is dark, so the
|
|
* dialog brings its own surface like every other one here. */
|
|
background: Rectangle {
|
|
color: Theme.surface
|
|
radius: Theme.radius
|
|
border.width: 1
|
|
border.color: Theme.border
|
|
}
|
|
|
|
/* Fill it in: the presets, the provider the engine is on now, and the
|
|
* model that provider last used. */
|
|
function load(list, provider, currentModel) {
|
|
providers = list
|
|
picked = provider
|
|
model = currentModel
|
|
note = ""
|
|
for (let i = 0; i < list.length; i++)
|
|
if (list[i].current === true)
|
|
picked = list[i].name
|
|
choices = picked.length ? agent.modelChoices(picked) : []
|
|
syncPick()
|
|
}
|
|
|
|
/* Where the picked provider's key would come from. From the entries --
|
|
* a key itself never reaches the QML side. */
|
|
function syncPick() {
|
|
hasKey = false
|
|
keyFrom = ""
|
|
keyEnv = ""
|
|
for (let i = 0; i < providers.length; i++) {
|
|
const p = providers[i]
|
|
if (p.name !== picked)
|
|
continue
|
|
hasKey = p.hasKey === true
|
|
keyFrom = p.keyFrom
|
|
keyEnv = p.keyEnv
|
|
return
|
|
}
|
|
}
|
|
|
|
function keyState() {
|
|
if (hasKey)
|
|
return keyFrom === "saved" ? "a key is saved · paste another to replace it"
|
|
: "key from the environment: " + keyFrom
|
|
return keyEnv.length ? "no key yet — set " + keyEnv + ", or paste one here"
|
|
: "no key yet — paste one here"
|
|
}
|
|
|
|
function usageLine() {
|
|
const u = agent.usage()
|
|
if (!u.requests)
|
|
return "no tokens spent in this session yet"
|
|
let s = u.requests + (u.requests === 1 ? " request" : " requests") +
|
|
" · " + u.prompt + " in · " + u.out + " out"
|
|
if (u.priced)
|
|
s += " · " + u.cost.toFixed(4) + " " + u.currency
|
|
return s
|
|
}
|
|
|
|
contentItem: ColumnLayout {
|
|
objectName: "settingsPanel"
|
|
spacing: Theme.gap
|
|
|
|
Text {
|
|
Layout.fillWidth: true
|
|
text: "Provider, model, key"
|
|
color: Theme.text
|
|
font.pixelSize: Theme.bodySize
|
|
font.bold: true
|
|
}
|
|
Text {
|
|
Layout.fillWidth: true
|
|
text: "Keys are stored under <config>/cst-agent/credentials (mode 0600) and are never shown again."
|
|
color: Theme.textFaint
|
|
font.pixelSize: Theme.smallSize
|
|
wrapMode: Text.Wrap
|
|
}
|
|
|
|
Flickable {
|
|
Layout.fillWidth: true
|
|
Layout.fillHeight: true
|
|
Layout.minimumHeight: 96
|
|
clip: true
|
|
contentWidth: width
|
|
contentHeight: plist.implicitHeight
|
|
boundsBehavior: Flickable.StopAtBounds
|
|
ScrollBar.vertical: ScrollBar {}
|
|
|
|
ColumnLayout {
|
|
id: plist
|
|
width: parent.width
|
|
spacing: 2
|
|
|
|
Repeater {
|
|
model: dlg.providers
|
|
delegate: Rectangle {
|
|
id: prow
|
|
required property var modelData
|
|
readonly property bool selected: dlg.picked === modelData.name
|
|
|
|
objectName: "providerRow:" + modelData.name
|
|
Layout.fillWidth: true
|
|
implicitHeight: prowCol.implicitHeight + 8
|
|
radius: Theme.radiusSm
|
|
color: prow.selected ? Theme.accentDim
|
|
: prowHover.hovered ? Theme.surfaceAlt
|
|
: "transparent"
|
|
border.width: 1
|
|
border.color: prow.selected ? Theme.accent : "transparent"
|
|
|
|
HoverHandler { id: prowHover }
|
|
MouseArea {
|
|
anchors.fill: parent
|
|
onClicked: {
|
|
dlg.picked = prow.modelData.name
|
|
modelField.text = prow.modelData.model
|
|
dlg.choices = agent.modelChoices(prow.modelData.name)
|
|
dlg.syncPick()
|
|
}
|
|
}
|
|
|
|
ColumnLayout {
|
|
id: prowCol
|
|
anchors {
|
|
left: parent.left
|
|
right: parent.right
|
|
top: parent.top
|
|
margins: 4
|
|
}
|
|
spacing: 0
|
|
|
|
RowLayout {
|
|
Layout.fillWidth: true
|
|
spacing: Theme.gap
|
|
Text {
|
|
text: prow.modelData.name
|
|
color: prow.selected ? Theme.text : Theme.textDim
|
|
font.pixelSize: Theme.smallSize
|
|
font.bold: prow.selected
|
|
}
|
|
Text {
|
|
visible: prow.modelData.current === true
|
|
text: "in use"
|
|
color: Theme.ok
|
|
font.pixelSize: Theme.smallSize
|
|
}
|
|
Item { Layout.fillWidth: true }
|
|
Text {
|
|
text: prow.modelData.source
|
|
color: Theme.textFaint
|
|
font.pixelSize: Theme.smallSize
|
|
}
|
|
}
|
|
Text {
|
|
Layout.fillWidth: true
|
|
elide: Text.ElideMiddle
|
|
text: prow.modelData.base + " · " +
|
|
(prow.modelData.hasKey === true
|
|
? (prow.modelData.keyFrom === "saved"
|
|
? "key saved"
|
|
: "key from " + prow.modelData.keyFrom)
|
|
: (prow.modelData.keyEnv.length
|
|
? "no key (" + prow.modelData.keyEnv + ")"
|
|
: "no key"))
|
|
color: Theme.textFaint
|
|
font.pixelSize: Theme.smallSize
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Text {
|
|
Layout.fillWidth: true
|
|
text: "Model"
|
|
color: Theme.textDim
|
|
font.pixelSize: Theme.smallSize
|
|
font.bold: true
|
|
}
|
|
RowLayout {
|
|
Layout.fillWidth: true
|
|
spacing: Theme.gap
|
|
TextField {
|
|
id: modelField
|
|
objectName: "settingsModel"
|
|
Layout.fillWidth: true
|
|
text: dlg.model
|
|
implicitHeight: Theme.touchTarget
|
|
selectByMouse: true
|
|
color: Theme.text
|
|
placeholderText: "model name"
|
|
placeholderTextColor: Theme.textFaint
|
|
selectionColor: Theme.accentDim
|
|
inputMethodHints: Qt.ImhNoPredictiveText
|
|
background: Rectangle {
|
|
radius: Theme.radiusSm
|
|
color: Theme.bg
|
|
border.width: 1
|
|
border.color: modelField.activeFocus ? Theme.accentDim
|
|
: Theme.border
|
|
}
|
|
}
|
|
Text {
|
|
Layout.preferredWidth: 90
|
|
elide: Text.ElideRight
|
|
text: modelField.text.length ? agent.modelInfo(modelField.text) : ""
|
|
color: Theme.textFaint
|
|
font.pixelSize: Theme.smallSize
|
|
}
|
|
}
|
|
Flow {
|
|
Layout.fillWidth: true
|
|
spacing: 6
|
|
visible: dlg.choices.length > 0
|
|
|
|
Repeater {
|
|
model: dlg.choices
|
|
delegate: Rectangle {
|
|
id: chip
|
|
required property var modelData
|
|
readonly property string name: String(chip.modelData)
|
|
|
|
objectName: "modelChoice:" + chip.name
|
|
implicitWidth: chipText.implicitWidth + 14
|
|
implicitHeight: 26
|
|
radius: Theme.radiusSm
|
|
color: chipHover.hovered ? Theme.surfaceHi : "transparent"
|
|
border.width: 1
|
|
border.color: modelField.text === chip.name ? Theme.accent
|
|
: Theme.border
|
|
HoverHandler { id: chipHover }
|
|
Text {
|
|
id: chipText
|
|
anchors.centerIn: parent
|
|
text: chip.name
|
|
color: modelField.text === chip.name ? Theme.text
|
|
: Theme.textDim
|
|
font.pixelSize: Theme.smallSize
|
|
}
|
|
TapHandler { onTapped: modelField.text = chip.name }
|
|
}
|
|
}
|
|
}
|
|
|
|
TextField {
|
|
id: keyField
|
|
objectName: "settingsKey"
|
|
Layout.fillWidth: true
|
|
implicitHeight: Theme.touchTarget
|
|
echoMode: TextInput.Password
|
|
selectByMouse: true
|
|
color: Theme.text
|
|
placeholderText: "API key (leave empty to keep what is there)"
|
|
placeholderTextColor: Theme.textFaint
|
|
selectionColor: Theme.accentDim
|
|
inputMethodHints: Qt.ImhNoPredictiveText | Qt.ImhSensitiveData
|
|
onAccepted: dlg.applyRequested(dlg.picked, modelField.text, keyField.text)
|
|
background: Rectangle {
|
|
radius: Theme.radiusSm
|
|
color: Theme.bg
|
|
border.width: 1
|
|
border.color: keyField.activeFocus ? Theme.accentDim : Theme.border
|
|
}
|
|
}
|
|
Text {
|
|
Layout.fillWidth: true
|
|
text: dlg.keyState()
|
|
color: Theme.textFaint
|
|
font.pixelSize: Theme.smallSize
|
|
wrapMode: Text.Wrap
|
|
}
|
|
|
|
RowLayout {
|
|
Layout.fillWidth: true
|
|
spacing: Theme.gap
|
|
Text {
|
|
objectName: "settingsNote"
|
|
Layout.fillWidth: true
|
|
text: dlg.note.length ? dlg.note : dlg.usageLine()
|
|
color: dlg.note.length ? Theme.warn : Theme.textFaint
|
|
font.pixelSize: Theme.smallSize
|
|
wrapMode: Text.Wrap
|
|
maximumLineCount: 2
|
|
elide: Text.ElideRight
|
|
}
|
|
Button {
|
|
id: closeBtn
|
|
objectName: "settingsClose"
|
|
text: "Close"
|
|
implicitHeight: Theme.touchTarget
|
|
onClicked: dlg.close()
|
|
background: Rectangle {
|
|
radius: Theme.radiusSm
|
|
color: closeBtn.hovered ? Theme.surfaceHi : Theme.surfaceAlt
|
|
border.width: 1
|
|
border.color: Theme.border
|
|
}
|
|
contentItem: Text {
|
|
text: closeBtn.text
|
|
color: Theme.textDim
|
|
font.pixelSize: Theme.bodySize
|
|
horizontalAlignment: Text.AlignHCenter
|
|
verticalAlignment: Text.AlignVCenter
|
|
}
|
|
}
|
|
Button {
|
|
id: applyBtn
|
|
objectName: "settingsApply"
|
|
text: "Use this"
|
|
implicitHeight: Theme.touchTarget
|
|
onClicked: dlg.applyRequested(dlg.picked, modelField.text, keyField.text)
|
|
background: Rectangle {
|
|
radius: Theme.radiusSm
|
|
color: applyBtn.hovered ? "#5cc0f0" : Theme.accent
|
|
}
|
|
contentItem: Text {
|
|
text: applyBtn.text
|
|
color: "#12161a"
|
|
font.pixelSize: Theme.bodySize
|
|
font.bold: true
|
|
horizontalAlignment: Text.AlignHCenter
|
|
verticalAlignment: Text.AlignVCenter
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|