refactor: archive GUI clients; keep minimal TUI with hooks/API
Move Qt/Tauri/full Web SPA under archive/, ship hermit as CLI/TUI plus libcstcore and cst-webui/1 HTTP+SSE (turn lifecycle hooks, capabilities). Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,739 @@
|
||||
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, 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.
|
||||
//
|
||||
// Three things live under "Advanced options", because they are for the odd
|
||||
// provider or the odd model: the provider's own entry (name, base URL, key
|
||||
// variable, models -- the engine keeps those in <config>/models.json, which is
|
||||
// how a provider the presets do not know is added or an existing one is
|
||||
// pointed somewhere else) and the generation parameters (temperature, top-p,
|
||||
// max tokens), which are remembered per provider.
|
||||
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 action answered
|
||||
property string keyEnv: ""
|
||||
property string keyFrom: ""
|
||||
property bool hasKey: false
|
||||
property bool advanced: false
|
||||
property string modelFilter: "" // narrows a long model list
|
||||
/* The advanced form: the picked provider's entry, editable. */
|
||||
property string advName: ""
|
||||
property string advBase: ""
|
||||
property string advKeyEnv: ""
|
||||
property string advModel: ""
|
||||
property string advModels: ""
|
||||
property string tempText: ""
|
||||
property string topPText: ""
|
||||
property string maxTokText: ""
|
||||
|
||||
signal applyRequested(string provider, string model, string key,
|
||||
real temperature, real topP, int maxTokens)
|
||||
|
||||
width: Math.min(660, parent ? parent.width - 32 : 660)
|
||||
height: Math.min(640, parent ? parent.height - 40 : 640)
|
||||
|
||||
/* 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
|
||||
}
|
||||
|
||||
// A long provider list (a router lists hundreds) is a wall of chips: the
|
||||
// filter narrows it and the rest is one line ("+N more").
|
||||
readonly property int chipCap: 40
|
||||
readonly property var chips: {
|
||||
const f = modelFilter.trim().toLowerCase()
|
||||
const out = []
|
||||
for (let i = 0; i < choices.length; i++) {
|
||||
const c = String(choices[i])
|
||||
if (!f.length || c.toLowerCase().indexOf(f) >= 0)
|
||||
out.push(c)
|
||||
}
|
||||
return out
|
||||
}
|
||||
readonly property int chipsHidden: Math.max(0, chips.length - chipCap)
|
||||
|
||||
/* "0.2" -> 0.2; anything empty or unreadable -> 0 (the field is not sent,
|
||||
* which is not the same as sending 0). */
|
||||
function num(text) {
|
||||
const v = parseFloat(String(text).replace(",", "."))
|
||||
return isFinite(v) && v > 0 ? v : 0
|
||||
}
|
||||
|
||||
function pickedEntry() {
|
||||
for (let i = 0; i < providers.length; i++)
|
||||
if (providers[i].name === dlg.picked)
|
||||
return providers[i]
|
||||
return null
|
||||
}
|
||||
|
||||
/* 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) : []
|
||||
modelFilter = ""
|
||||
syncPick()
|
||||
}
|
||||
|
||||
/* Where the picked provider's key would come from and what it is set to.
|
||||
* From the entries -- a key itself never reaches the QML side. */
|
||||
function syncPick() {
|
||||
hasKey = false
|
||||
keyFrom = ""
|
||||
keyEnv = ""
|
||||
const p = pickedEntry()
|
||||
if (p) {
|
||||
hasKey = p.hasKey === true
|
||||
keyFrom = p.keyFrom
|
||||
keyEnv = p.keyEnv
|
||||
/* The advanced form describes the picked provider: edit it, or
|
||||
* type another name to add one. */
|
||||
advName = p.name
|
||||
advBase = p.base
|
||||
advKeyEnv = p.keyEnv
|
||||
advModel = p.model
|
||||
advModels = p.custom === true ? agent.modelChoices(p.name).join(", ") : ""
|
||||
}
|
||||
const g = picked.length ? agent.genParams(picked) : null
|
||||
tempText = g && g.temperature > 0 ? String(g.temperature) : ""
|
||||
topPText = g && g.top_p > 0 ? String(g.top_p) : ""
|
||||
maxTokText = g && g.max_tokens > 0 ? String(g.max_tokens) : ""
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
/* Write the form's entry into the engine's user file and list it again. */
|
||||
function saveProvider() {
|
||||
/* The fields are the truth here, not the properties: a hand-typed
|
||||
* edit is what the user wants saved. */
|
||||
const name = advNameField.text.trim()
|
||||
const err = agent.addProvider(name, advBaseField.text.trim(),
|
||||
advKeyEnvField.text.trim(),
|
||||
advModelField.text.trim(),
|
||||
advModelsField.text)
|
||||
if (err.length) {
|
||||
note = err
|
||||
return
|
||||
}
|
||||
providers = agent.providers()
|
||||
picked = name
|
||||
choices = agent.modelChoices(name)
|
||||
syncPick()
|
||||
note = "saved " + name + " to models.json — it is in the list above now"
|
||||
}
|
||||
|
||||
function removeProvider() {
|
||||
const typed = advNameField.text.trim()
|
||||
const name = typed.length ? typed : picked
|
||||
const rc = agent.removeProvider(name)
|
||||
if (rc < 0) {
|
||||
note = "could not remove " + name
|
||||
return
|
||||
}
|
||||
providers = agent.providers()
|
||||
note = rc === 0
|
||||
? name + " is not in models.json (a preset of that name stays)"
|
||||
: "removed " + name + " from models.json"
|
||||
if (rc === 1 && providers.length > 0)
|
||||
picked = providers[0].name
|
||||
choices = picked.length ? agent.modelChoices(picked) : []
|
||||
syncPick()
|
||||
}
|
||||
|
||||
/* The form's fields follow the picked provider (and a user edit breaks
|
||||
* the binding, so the push is imperative). */
|
||||
onAdvNameChanged: if (advNameField && advNameField.text !== advName) advNameField.text = advName
|
||||
onAdvBaseChanged: if (advBaseField && advBaseField.text !== advBase) advBaseField.text = advBase
|
||||
onAdvKeyEnvChanged: if (advKeyEnvField && advKeyEnvField.text !== advKeyEnv) advKeyEnvField.text = advKeyEnv
|
||||
onAdvModelChanged: if (advModelField && advModelField.text !== advModel) advModelField.text = advModel
|
||||
onAdvModelsChanged: if (advModelsField && advModelsField.text !== advModels) advModelsField.text = advModels
|
||||
onTempTextChanged: if (tempField && tempField.text !== tempText) tempField.text = tempText
|
||||
onTopPTextChanged: if (topPField && topPField.text !== topPText) topPField.text = topPText
|
||||
onMaxTokTextChanged: if (maxTokField && maxTokField.text !== maxTokText) maxTokField.text = maxTokText
|
||||
|
||||
// One labelled field of the advanced form.
|
||||
component AdvField: ColumnLayout {
|
||||
property string label: ""
|
||||
property alias text: field.text
|
||||
property alias placeholder: field.placeholderText
|
||||
property int hintWidth: 0
|
||||
|
||||
Layout.fillWidth: true
|
||||
spacing: 2
|
||||
Text {
|
||||
text: parent.label
|
||||
color: Theme.textDim
|
||||
font.pixelSize: Theme.smallSize
|
||||
}
|
||||
TextField {
|
||||
id: field
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: Theme.touchTarget
|
||||
selectByMouse: true
|
||||
color: Theme.text
|
||||
placeholderTextColor: Theme.textFaint
|
||||
selectionColor: Theme.accentDim
|
||||
inputMethodHints: Qt.ImhNoPredictiveText
|
||||
background: Rectangle {
|
||||
radius: Theme.radiusSm
|
||||
color: Theme.bg
|
||||
border.width: 1
|
||||
border.color: field.activeFocus ? Theme.accentDim : Theme.border
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
/* Everything scrolls, so the buttons stay where they are and a long
|
||||
* model list cannot push them out of the dialog. */
|
||||
Flickable {
|
||||
id: body
|
||||
objectName: "settingsBody"
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
Layout.minimumHeight: 120
|
||||
clip: true
|
||||
contentWidth: width
|
||||
contentHeight: bodyCol.implicitHeight
|
||||
boundsBehavior: Flickable.StopAtBounds
|
||||
ScrollBar.vertical: ScrollBar {}
|
||||
|
||||
ColumnLayout {
|
||||
id: bodyCol
|
||||
width: parent.width
|
||||
spacing: Theme.gap
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
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.modelFilter = ""
|
||||
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.accentText : Theme.textDim
|
||||
font.pixelSize: Theme.smallSize
|
||||
font.bold: prow.selected
|
||||
}
|
||||
Text {
|
||||
visible: prow.modelData.current === true
|
||||
text: "in use"
|
||||
color: prow.selected ? Theme.okOnAccent : Theme.ok
|
||||
font.pixelSize: Theme.smallSize
|
||||
}
|
||||
Item { Layout.fillWidth: true }
|
||||
Text {
|
||||
text: prow.modelData.custom === true
|
||||
? "custom" : prow.modelData.source
|
||||
color: prow.selected ? Theme.accentTextDim : 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: prow.selected ? Theme.accentTextDim : 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
|
||||
}
|
||||
}
|
||||
|
||||
/* A router can list hundreds of models: filter, cap, say how
|
||||
* many are left instead of filling the dialog. */
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
visible: dlg.choices.length > 12
|
||||
spacing: Theme.gap
|
||||
TextField {
|
||||
id: filterField
|
||||
objectName: "settingsFilter"
|
||||
Layout.fillWidth: true
|
||||
text: dlg.modelFilter
|
||||
implicitHeight: Theme.segmentHeight
|
||||
selectByMouse: true
|
||||
color: Theme.text
|
||||
placeholderText: "filter " + dlg.choices.length + " models"
|
||||
placeholderTextColor: Theme.textFaint
|
||||
selectionColor: Theme.accentDim
|
||||
inputMethodHints: Qt.ImhNoPredictiveText
|
||||
onTextChanged: dlg.modelFilter = text
|
||||
background: Rectangle {
|
||||
radius: Theme.radiusSm
|
||||
color: Theme.bg
|
||||
border.width: 1
|
||||
border.color: filterField.activeFocus ? Theme.accentDim
|
||||
: Theme.border
|
||||
}
|
||||
}
|
||||
Text {
|
||||
Layout.preferredWidth: 110
|
||||
elide: Text.ElideRight
|
||||
text: dlg.chips.length + " shown"
|
||||
+ (dlg.chipsHidden ? " · +" + dlg.chipsHidden + " more" : "")
|
||||
color: Theme.textFaint
|
||||
font.pixelSize: Theme.smallSize
|
||||
}
|
||||
}
|
||||
Flow {
|
||||
Layout.fillWidth: true
|
||||
spacing: 6
|
||||
visible: dlg.choices.length > 0
|
||||
|
||||
Repeater {
|
||||
model: dlg.chips.slice(0, dlg.chipCap)
|
||||
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.apply()
|
||||
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
|
||||
}
|
||||
|
||||
/* --- advanced ------------------------------------------- */
|
||||
ToolButton {
|
||||
id: advToggle
|
||||
objectName: "settingsAdvanced"
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: Theme.segmentHeight
|
||||
onClicked: dlg.advanced = !dlg.advanced
|
||||
contentItem: RowLayout {
|
||||
spacing: 6
|
||||
ThemeIcon {
|
||||
name: dlg.advanced ? "arrow-down" : "arrow-right"
|
||||
glyph: dlg.advanced ? "▾" : "▸"
|
||||
size: 14
|
||||
color: Theme.textDim
|
||||
}
|
||||
Text {
|
||||
text: "Advanced options"
|
||||
color: advToggle.hovered ? Theme.text : Theme.textDim
|
||||
font.pixelSize: Theme.smallSize
|
||||
font.bold: true
|
||||
}
|
||||
Item { Layout.fillWidth: true }
|
||||
Text {
|
||||
text: "custom provider · model parameters"
|
||||
color: Theme.textFaint
|
||||
font.pixelSize: Theme.smallSize
|
||||
}
|
||||
}
|
||||
background: Rectangle {
|
||||
radius: Theme.radiusSm
|
||||
color: advToggle.hovered ? Theme.surfaceAlt : "transparent"
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
visible: dlg.advanced
|
||||
spacing: 6
|
||||
|
||||
Text {
|
||||
Layout.fillWidth: true
|
||||
text: "Provider entry — saved in <config>/cst-agent/models.json. " +
|
||||
"A name the presets know is overridden, a new one is added."
|
||||
color: Theme.textFaint
|
||||
font.pixelSize: Theme.smallSize
|
||||
wrapMode: Text.Wrap
|
||||
}
|
||||
GridLayout {
|
||||
Layout.fillWidth: true
|
||||
columns: 2
|
||||
columnSpacing: Theme.gap
|
||||
rowSpacing: 4
|
||||
|
||||
AdvField {
|
||||
id: advNameField
|
||||
objectName: "advName"
|
||||
label: "Name"
|
||||
placeholder: "myllm"
|
||||
Layout.preferredWidth: 180
|
||||
}
|
||||
AdvField {
|
||||
id: advBaseField
|
||||
objectName: "advBase"
|
||||
label: "Base URL"
|
||||
placeholder: "https://api.example.com/v1"
|
||||
}
|
||||
AdvField {
|
||||
id: advKeyEnvField
|
||||
objectName: "advKeyEnv"
|
||||
label: "Key variable"
|
||||
placeholder: "MYLLM_API_KEY"
|
||||
}
|
||||
AdvField {
|
||||
id: advModelField
|
||||
objectName: "advModel"
|
||||
label: "Default model"
|
||||
placeholder: "the model to start with"
|
||||
}
|
||||
AdvField {
|
||||
id: advModelsField
|
||||
objectName: "advModels"
|
||||
label: "Models it offers (comma separated, optional)"
|
||||
placeholder: "m1, m2"
|
||||
Layout.columnSpan: 2
|
||||
}
|
||||
}
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Theme.gap
|
||||
Button {
|
||||
id: saveProviderBtn
|
||||
objectName: "settingsSaveProvider"
|
||||
text: "Save provider"
|
||||
implicitHeight: Theme.touchTarget
|
||||
enabled: advNameField.text.trim().length > 0 &&
|
||||
advBaseField.text.trim().length > 0
|
||||
onClicked: dlg.saveProvider()
|
||||
background: Rectangle {
|
||||
radius: Theme.radiusSm
|
||||
color: saveProviderBtn.hovered ? Theme.surfaceHi : Theme.surfaceAlt
|
||||
border.width: 1
|
||||
border.color: Theme.border
|
||||
}
|
||||
contentItem: Text {
|
||||
text: saveProviderBtn.text
|
||||
color: saveProviderBtn.enabled ? Theme.textDim : Theme.textFaint
|
||||
font.pixelSize: Theme.bodySize
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
}
|
||||
Button {
|
||||
id: removeProviderBtn
|
||||
objectName: "settingsRemoveProvider"
|
||||
text: "Remove from models.json"
|
||||
implicitHeight: Theme.touchTarget
|
||||
readonly property var entry: dlg.pickedEntry()
|
||||
enabled: entry !== null && entry.custom === true
|
||||
ToolTip.visible: hovered
|
||||
ToolTip.text: "Only the user's own entry goes: a built-in preset of the same name stays"
|
||||
onClicked: dlg.removeProvider()
|
||||
background: Rectangle {
|
||||
radius: Theme.radiusSm
|
||||
color: removeProviderBtn.hovered && removeProviderBtn.enabled
|
||||
? Theme.surfaceHi : Theme.surfaceAlt
|
||||
border.width: 1
|
||||
border.color: Theme.border
|
||||
}
|
||||
contentItem: Text {
|
||||
text: removeProviderBtn.text
|
||||
color: removeProviderBtn.enabled ? Theme.textDim
|
||||
: Theme.textFaint
|
||||
font.pixelSize: Theme.bodySize
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
}
|
||||
Item { Layout.fillWidth: true }
|
||||
}
|
||||
|
||||
Text {
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 4
|
||||
text: "Model parameters — remembered for " +
|
||||
(dlg.picked.length ? dlg.picked : "this provider") +
|
||||
", sent with every request. Empty = the provider's own default."
|
||||
color: Theme.textFaint
|
||||
font.pixelSize: Theme.smallSize
|
||||
wrapMode: Text.Wrap
|
||||
}
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Theme.gap
|
||||
AdvField {
|
||||
id: tempField
|
||||
objectName: "advTemperature"
|
||||
label: "Temperature"
|
||||
placeholder: "not sent"
|
||||
Layout.preferredWidth: 130
|
||||
}
|
||||
AdvField {
|
||||
id: topPField
|
||||
objectName: "advTopP"
|
||||
label: "Top-p"
|
||||
placeholder: "not sent"
|
||||
Layout.preferredWidth: 130
|
||||
}
|
||||
AdvField {
|
||||
id: maxTokField
|
||||
objectName: "advMaxTokens"
|
||||
label: "Max tokens"
|
||||
placeholder: "not sent"
|
||||
Layout.preferredWidth: 150
|
||||
}
|
||||
Item { Layout.fillWidth: true }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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.apply()
|
||||
background: Rectangle {
|
||||
radius: Theme.radiusSm
|
||||
color: applyBtn.hovered ? "#5cc0f0" : Theme.accent
|
||||
}
|
||||
contentItem: Text {
|
||||
text: applyBtn.text
|
||||
color: Theme.accentInk
|
||||
font.pixelSize: Theme.bodySize
|
||||
font.bold: true
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* The model parameters belong to the provider that is picked, so they are
|
||||
* applied first: useProvider() then takes over what was remembered for
|
||||
* whichever provider it ends on. */
|
||||
function apply() {
|
||||
dlg.applyRequested(dlg.picked, modelField.text, keyField.text,
|
||||
dlg.num(tempField.text), dlg.num(topPField.text),
|
||||
Math.round(dlg.num(maxTokField.text)))
|
||||
}
|
||||
|
||||
onOpened: {
|
||||
keyField.text = ""
|
||||
modelField.forceActiveFocus()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user