The settings dialog could pick a preset, a model and a key and nothing else.
Two things were missing: a provider the presets do not know, and what the
model is asked to do while it answers. Both live under "Advanced options"
now, next to the entry they belong to.
Custom providers (engine)
- cst_models_user_set_provider() / cst_models_user_drop_provider() write the
engine's own user layer, <config>/cst-agent/models.json:
{"providers": {"myllm": {"api_base": ..., "key_env": ...,
"default_model": ..., "models": [...]}}}
and reload the registry, so the provider is at once in the provider list,
in the wire models[] and in the CLI's /provider -- the file a hand-written
override would go into is the same one, so the GUI and an editor agree.
The name is checked (1-31 of letters, digits, '.', '_', '-'), a preset's
name overrides that preset (a self-hosted endpoint, a proxy), everything
else in the file (model rules) is kept, and a file that is not a JSON
object is never overwritten. Dropping only removes the user's own entry.
- AgentCore::providers() gained "custom" (source == "user"), and
addProvider() / removeProvider() drive the two calls above.
Model parameters (engine + wire)
- cst_agent_config_t and cst_llm_request_t carry temperature, top_p and
max_tokens. 0 means the field is not sent at all -- an absent field is the
provider's own default, which "temperature": 0 is not (deterministic
decoding is a real choice), so cst_llm_build_body() only adds what was set.
tests/test_llm_body.c:test_gen_params pins both halves.
- settings.json keeps them per provider ("params": {...}) via
cst_settings_remember_params(); cst_settings_remember() carries them along
with the provider choice, and cst_agent_create() reads them, so a CLI or
TUI run sends what the window was set to. An all-zero config (the CLI)
leaves what the GUI saved alone.
- AgentCore::genParams() / setGenParams(), and useProvider() applies both the
remembered parameters of the provider it lands on and a base URL that was
edited in the dialog.
The dialog
- Rewritten around one scrolling body with a fixed footer: a long model list
used to push the key field and the buttons out of the dialog. The model
chips are filtered (a filter field appears past twelve) and capped at 40
with "... +N more", which is what a router with hundreds of models needs.
- "Advanced options" folds out the provider entry (name, base URL, key
variable, default model, models; Save provider / Remove from models.json)
and the model parameters (temperature, top-p, max tokens; empty = not
sent). The fields follow the picked provider and are what gets saved, so a
hand-typed edit is never lost to a stale property.
Verified: --uitest 67/67 (15 new checks: parameters round-trip through the
engine, a provider added through the form and removed again, a bad name
refused, a preset not removable), --selftest, ctest cst-app-selftest, and
make check with the new body test.
Version 0.8.15.
mode = coding
740 lines
32 KiB
QML
740 lines
32 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, 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()
|
|
}
|
|
}
|