@xingjisoft/neutrino 0.4.1 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/app.js +55 -0
- package/dist/bin.js +21 -0
- package/dist/pro/config.js +139 -9
- package/dist/screens/start.js +26 -14
- package/dist/screens/updatePrompt.js +200 -0
- package/dist/ui.js +32 -16
- package/dist/update.js +213 -0
- package/dist/web/clientState.js +82 -0
- package/dist/web/server.js +87 -2
- package/dist/web/service.js +25 -0
- package/dist/web/session.js +1 -0
- package/dist/web/settings.js +9 -0
- package/package.json +2 -2
- package/web/README.md +28 -9
- package/web/app.js +375 -14
- package/web/i18n.js +20 -0
- package/web/index.html +41 -8
- package/web/settings.js +170 -44
- package/web/style.css +44 -78
- package/assets/pro/model-template.json +0 -376
package/web/settings.js
CHANGED
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
import { t, getLanguage } from "./i18n.js";
|
|
4
4
|
|
|
5
5
|
export function createSettingsUI({ apiFetch, applyModel, onLanguageChange, proMode = false, onCatalogChange }) {
|
|
6
|
-
const ids = ["model-picker", "model-
|
|
6
|
+
const ids = ["model-picker", "model-picker-summary", "model-picker-label", "model-provider", "model-name", "model-intensity",
|
|
7
|
+
"model-intensity-label", "model-intensity-slider", "model-intensity-stops", "model-intensity-ticks", "model-feedback",
|
|
7
8
|
"settings-open", "settings-dialog", "settings-form", "settings-close", "settings-cancel", "settings-save", "settings-feedback",
|
|
8
9
|
"default-provider", "default-model", "default-intensity", "settings-context", "settings-compress-note", "settings-reviewer",
|
|
9
10
|
"settings-extreme", "settings-sound", "settings-password", "settings-password-confirm",
|
|
@@ -14,7 +15,10 @@ export function createSettingsUI({ apiFetch, applyModel, onLanguageChange, proMo
|
|
|
14
15
|
let target = null;
|
|
15
16
|
let conversation = null;
|
|
16
17
|
let dirty = false;
|
|
17
|
-
let
|
|
18
|
+
let draftIntensity = "";
|
|
19
|
+
let applying = null;
|
|
20
|
+
let generation = 0;
|
|
21
|
+
let failure = null;
|
|
18
22
|
let feedback = "";
|
|
19
23
|
let feedbackError = false;
|
|
20
24
|
// Value the LAN switch had when the dialog opened. Only an actual flip is
|
|
@@ -88,32 +92,90 @@ export function createSettingsUI({ apiFetch, applyModel, onLanguageChange, proMo
|
|
|
88
92
|
value: preset.name, label: getLanguage() === "zh" ? preset.chinese_name || preset.name : preset.name,
|
|
89
93
|
})), selected);
|
|
90
94
|
}
|
|
95
|
+
function unavailableOption(select, value) {
|
|
96
|
+
const option = document.createElement("option");
|
|
97
|
+
option.value = value;
|
|
98
|
+
option.textContent = `${value} ${t("(unavailable)")}`;
|
|
99
|
+
option.disabled = true;
|
|
100
|
+
select.appendChild(option);
|
|
101
|
+
select.value = value;
|
|
102
|
+
}
|
|
91
103
|
function choice() {
|
|
92
|
-
return { provider: el["model-provider"].value, model: el["model-name"].value, intensity:
|
|
104
|
+
return { provider: el["model-provider"].value, model: el["model-name"].value, intensity: draftIntensity };
|
|
93
105
|
}
|
|
94
106
|
function renderPicker() {
|
|
95
107
|
el["model-picker"].classList.toggle("hidden", proMode || !config || !conversation);
|
|
96
108
|
if (proMode || !config || !conversation) return;
|
|
97
109
|
if (!dirty) {
|
|
98
|
-
providers(el["model-provider"], conversation.provider);
|
|
99
|
-
|
|
100
|
-
|
|
110
|
+
providers(el["model-provider"], conversation.provider || config.selection?.provider);
|
|
111
|
+
if (conversation.provider && !config.providers.some(entry => entry.name === conversation.provider)) {
|
|
112
|
+
unavailableOption(el["model-provider"], conversation.provider);
|
|
113
|
+
}
|
|
114
|
+
models(el["model-provider"], el["model-name"], conversation.model || config.selection?.model);
|
|
115
|
+
if (conversation.model && !modelEntry(el["model-provider"].value, conversation.model)) {
|
|
116
|
+
unavailableOption(el["model-name"], conversation.model);
|
|
117
|
+
}
|
|
118
|
+
draftIntensity = pickIntensity(el["model-provider"].value, el["model-name"].value,
|
|
119
|
+
conversation.intensity || config.selection?.intensity);
|
|
101
120
|
} else {
|
|
102
|
-
//
|
|
103
|
-
|
|
104
|
-
intensities(el["model-intensity"], el["model-intensity"].value,
|
|
105
|
-
el["model-provider"].value, el["model-name"].value);
|
|
121
|
+
// Keep a draft across live snapshots, while honoring the chosen model's limits.
|
|
122
|
+
draftIntensity = pickIntensity(el["model-provider"].value, el["model-name"].value, draftIntensity);
|
|
106
123
|
}
|
|
124
|
+
const selection = choice();
|
|
125
|
+
const displayed = dirty || target?.startsWith("new:") ? selection : {
|
|
126
|
+
model: conversation.model || selection.model,
|
|
127
|
+
intensity: conversation.intensity || selection.intensity,
|
|
128
|
+
};
|
|
129
|
+
el["model-picker-label"].textContent = [displayed.model, displayed.intensity && t(displayed.intensity)]
|
|
130
|
+
.filter(Boolean).join(" · ") || t("Choose a model");
|
|
131
|
+
const summary = t("Conversation model: {selection}", { selection: el["model-picker-label"].textContent });
|
|
132
|
+
el["model-picker-summary"].setAttribute("aria-label", summary);
|
|
133
|
+
el["model-picker-summary"].title = summary;
|
|
134
|
+
el["model-intensity-label"].textContent = selection.intensity ? t(selection.intensity) : "";
|
|
135
|
+
|
|
136
|
+
const levels = modelEntry(selection.provider, selection.model) ? intensitiesFor(selection.provider, selection.model) : [];
|
|
137
|
+
const index = Math.max(0, levels.indexOf(selection.intensity));
|
|
138
|
+
const last = Math.max(0, levels.length - 1);
|
|
107
139
|
const busy = Boolean(conversation.busy || conversation.approval);
|
|
108
|
-
|
|
109
|
-
el["model-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
);
|
|
140
|
+
const locked = busy || Boolean(applying && applying.generation === generation);
|
|
141
|
+
const slider = el["model-intensity-slider"];
|
|
142
|
+
slider.setAttribute("style", `--pro-slider-position: ${last ? index / last : 0}`);
|
|
143
|
+
slider.classList.toggle("is-disabled", locked || !levels.length);
|
|
144
|
+
el["model-picker"].classList.toggle("is-max", last > 0 && index === last);
|
|
145
|
+
el["model-provider"].disabled = locked;
|
|
146
|
+
el["model-name"].disabled = locked || !selection.provider;
|
|
147
|
+
el["model-intensity"].disabled = locked || !levels.length;
|
|
148
|
+
el["model-intensity"].max = String(last);
|
|
149
|
+
el["model-intensity"].value = String(index);
|
|
150
|
+
el["model-intensity"].setAttribute("aria-valuetext", selection.intensity ? t(selection.intensity) : "");
|
|
151
|
+
|
|
152
|
+
const stops = el["model-intensity-stops"];
|
|
153
|
+
const ticks = el["model-intensity-ticks"];
|
|
154
|
+
stops.replaceChildren();
|
|
155
|
+
ticks.replaceChildren();
|
|
156
|
+
levels.forEach((level, levelIndex) => {
|
|
157
|
+
const position = last ? levelIndex / last * 100 : 0;
|
|
158
|
+
const stop = document.createElement("span");
|
|
159
|
+
stop.className = "pro-slider-stop";
|
|
160
|
+
stop.setAttribute("style", `left: ${position}%`);
|
|
161
|
+
stop.classList.toggle("is-filled", levelIndex <= index);
|
|
162
|
+
stops.appendChild(stop);
|
|
163
|
+
const tick = document.createElement("button");
|
|
164
|
+
tick.type = "button";
|
|
165
|
+
tick.textContent = t(level);
|
|
166
|
+
tick.disabled = locked;
|
|
167
|
+
tick.setAttribute("style", `left: ${position}%`);
|
|
168
|
+
tick.setAttribute("aria-pressed", String(level === selection.intensity));
|
|
169
|
+
tick.addEventListener("click", () => {
|
|
170
|
+
if (tick.disabled) return;
|
|
171
|
+
draftIntensity = level;
|
|
172
|
+
changeChoice();
|
|
173
|
+
});
|
|
174
|
+
ticks.appendChild(tick);
|
|
175
|
+
});
|
|
176
|
+
const status = busy ? t("Pause the current turn to change models.")
|
|
177
|
+
: feedback || (locked ? t("Applying…") : t("Applies to the next turn."));
|
|
178
|
+
message(el["model-feedback"], status, !busy && feedbackError);
|
|
117
179
|
}
|
|
118
180
|
async function load() {
|
|
119
181
|
config = await json("/api/settings");
|
|
@@ -122,44 +184,109 @@ export function createSettingsUI({ apiFetch, applyModel, onLanguageChange, proMo
|
|
|
122
184
|
return config;
|
|
123
185
|
}
|
|
124
186
|
function showConversation(info, key) {
|
|
125
|
-
if (target !== key) {
|
|
187
|
+
if (target !== key) {
|
|
188
|
+
generation++;
|
|
189
|
+
dirty = false;
|
|
190
|
+
failure = null;
|
|
191
|
+
feedback = "";
|
|
192
|
+
feedbackError = false;
|
|
193
|
+
el["model-picker"].open = false;
|
|
194
|
+
}
|
|
126
195
|
target = key;
|
|
127
196
|
conversation = info;
|
|
197
|
+
// A turn may start elsewhere while the slider is being dragged. Discard
|
|
198
|
+
// that uncommitted preview so it cannot masquerade as the active setting.
|
|
199
|
+
if (target?.startsWith("live:") && dirty && (info?.busy || info?.approval) && applying?.generation !== generation) {
|
|
200
|
+
dirty = false;
|
|
201
|
+
feedback = "";
|
|
202
|
+
feedbackError = false;
|
|
203
|
+
}
|
|
128
204
|
renderPicker();
|
|
129
205
|
}
|
|
130
206
|
function clear() {
|
|
207
|
+
generation++;
|
|
131
208
|
target = null;
|
|
132
209
|
conversation = null;
|
|
133
210
|
dirty = false;
|
|
211
|
+
failure = null;
|
|
212
|
+
feedback = "";
|
|
213
|
+
el["model-picker"].open = false;
|
|
214
|
+
renderPicker();
|
|
215
|
+
}
|
|
216
|
+
function changeChoice() {
|
|
217
|
+
if (!config || !conversation || conversation.busy || conversation.approval || applying?.generation === generation) return;
|
|
218
|
+
dirty = true;
|
|
219
|
+
failure = null;
|
|
134
220
|
feedback = "";
|
|
221
|
+
feedbackError = false;
|
|
222
|
+
renderPicker();
|
|
223
|
+
// Saved and not-yet-created conversations keep the choice as a local draft.
|
|
224
|
+
if (!target?.startsWith("live:")) return;
|
|
225
|
+
const selection = choice();
|
|
226
|
+
if (!selection.provider || !modelEntry(selection.provider, selection.model) || !selection.intensity) return;
|
|
227
|
+
if (selection.provider === conversation.provider && selection.model === conversation.model && selection.intensity === conversation.intensity) {
|
|
228
|
+
dirty = false;
|
|
229
|
+
renderPicker();
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
const request = { generation, promise: null };
|
|
233
|
+
applying = request;
|
|
135
234
|
renderPicker();
|
|
235
|
+
request.promise = (async () => {
|
|
236
|
+
try {
|
|
237
|
+
await applyModel(selection);
|
|
238
|
+
if (request.generation !== generation) return;
|
|
239
|
+
conversation = { ...conversation, ...selection };
|
|
240
|
+
dirty = false;
|
|
241
|
+
feedback = t("Model and intensity updated.");
|
|
242
|
+
} catch (error) {
|
|
243
|
+
if (request.generation !== generation) return;
|
|
244
|
+
dirty = false;
|
|
245
|
+
failure = error;
|
|
246
|
+
feedback = t(error.message);
|
|
247
|
+
feedbackError = true;
|
|
248
|
+
} finally {
|
|
249
|
+
if (applying === request) applying = null;
|
|
250
|
+
renderPicker();
|
|
251
|
+
}
|
|
252
|
+
})();
|
|
136
253
|
}
|
|
137
254
|
el["model-provider"].addEventListener("change", () => {
|
|
138
255
|
if (!config) return;
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
256
|
+
const provider = config.providers.find(entry => entry.name === el["model-provider"].value);
|
|
257
|
+
models(el["model-provider"], el["model-name"], provider?.models.find(model => model.configured)?.name);
|
|
258
|
+
changeChoice();
|
|
142
259
|
});
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
applying = true;
|
|
260
|
+
el["model-name"].addEventListener("change", changeChoice);
|
|
261
|
+
el["model-intensity"].addEventListener("input", () => {
|
|
262
|
+
if (!config || !conversation || conversation.busy || conversation.approval || applying?.generation === generation) return;
|
|
263
|
+
const levels = intensitiesFor(el["model-provider"].value, el["model-name"].value);
|
|
264
|
+
draftIntensity = levels[Number(el["model-intensity"].value)] || draftIntensity;
|
|
265
|
+
dirty = true;
|
|
266
|
+
failure = null;
|
|
151
267
|
feedback = "";
|
|
268
|
+
feedbackError = false;
|
|
152
269
|
renderPicker();
|
|
153
|
-
try {
|
|
154
|
-
await applyModel(choice());
|
|
155
|
-
if (target === originalTarget) { dirty = false; feedback = t("Model and intensity updated."); feedbackError = false; }
|
|
156
|
-
} catch (error) {
|
|
157
|
-
if (target === originalTarget) { feedback = t(error.message); feedbackError = true; }
|
|
158
|
-
} finally {
|
|
159
|
-
applying = false;
|
|
160
|
-
renderPicker();
|
|
161
|
-
}
|
|
162
270
|
});
|
|
271
|
+
el["model-intensity"].addEventListener("change", changeChoice);
|
|
272
|
+
el["model-picker"].addEventListener("keydown", event => {
|
|
273
|
+
if (event.key === "Escape") { el["model-picker"].open = false; event.stopPropagation(); }
|
|
274
|
+
});
|
|
275
|
+
document.addEventListener?.("pointerdown", event => {
|
|
276
|
+
if (el["model-picker"].open && !el["model-picker"].contains(event.target)) el["model-picker"].open = false;
|
|
277
|
+
});
|
|
278
|
+
async function flush() {
|
|
279
|
+
// A keyboard send can happen before a range input emits its change event.
|
|
280
|
+
if (dirty && target?.startsWith("live:") && applying?.generation !== generation) changeChoice();
|
|
281
|
+
const request = applying;
|
|
282
|
+
const currentGeneration = generation;
|
|
283
|
+
if (request?.generation === currentGeneration) await request.promise;
|
|
284
|
+
if (currentGeneration === generation && failure) {
|
|
285
|
+
const error = failure;
|
|
286
|
+
failure = null;
|
|
287
|
+
throw error;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
163
290
|
|
|
164
291
|
function renderSettings() {
|
|
165
292
|
el["settings-password"].value = "";
|
|
@@ -241,8 +368,6 @@ export function createSettingsUI({ apiFetch, applyModel, onLanguageChange, proMo
|
|
|
241
368
|
if (!config) return;
|
|
242
369
|
providers(el["model-provider"], el["model-provider"].value);
|
|
243
370
|
models(el["model-provider"], el["model-name"], el["model-name"].value);
|
|
244
|
-
intensities(el["model-intensity"], el["model-intensity"].value,
|
|
245
|
-
el["model-provider"].value, el["model-name"].value);
|
|
246
371
|
if (el["settings-dialog"].open) {
|
|
247
372
|
providers(el["default-provider"], el["default-provider"].value);
|
|
248
373
|
models(el["default-provider"], el["default-model"], el["default-model"].value);
|
|
@@ -362,12 +487,13 @@ export function createSettingsUI({ apiFetch, applyModel, onLanguageChange, proMo
|
|
|
362
487
|
clear,
|
|
363
488
|
soundAlerts,
|
|
364
489
|
refreshTexts,
|
|
490
|
+
flush,
|
|
365
491
|
catalog: () => config,
|
|
366
|
-
defaultSelection: () => (config ? {
|
|
492
|
+
defaultSelection: () => (config ? (target?.startsWith("new:") && dirty ? choice() : {
|
|
367
493
|
provider: config.selection.provider,
|
|
368
494
|
model: config.selection.model,
|
|
369
495
|
intensity: config.selection.intensity,
|
|
370
|
-
} : undefined),
|
|
496
|
+
}) : undefined),
|
|
371
497
|
selectionForFile: id => target === `file:${id}` && dirty ? choice() : undefined,
|
|
372
498
|
};
|
|
373
499
|
}
|
package/web/style.css
CHANGED
|
@@ -549,13 +549,28 @@ svg {
|
|
|
549
549
|
color: var(--text-tertiary);
|
|
550
550
|
}
|
|
551
551
|
|
|
552
|
-
.
|
|
552
|
+
/* Sidebar row status: a finished turn, a turn in flight, or a pending approval. */
|
|
553
|
+
.session-state {
|
|
553
554
|
flex: none;
|
|
554
555
|
width: 6px;
|
|
555
556
|
height: 6px;
|
|
556
557
|
border-radius: 50%;
|
|
557
558
|
background: var(--accent);
|
|
558
|
-
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
.session-state.approval {
|
|
562
|
+
background: var(--amber);
|
|
563
|
+
animation: pulse-amber 1.6s ease-out infinite;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
/* A running turn is a ring that keeps turning, never a coloured dot. */
|
|
567
|
+
.session-state.running {
|
|
568
|
+
width: 9px;
|
|
569
|
+
height: 9px;
|
|
570
|
+
background: none;
|
|
571
|
+
border: 1.5px solid rgba(122, 170, 255, 0.25);
|
|
572
|
+
border-top-color: var(--accent);
|
|
573
|
+
animation: spin 0.8s linear infinite;
|
|
559
574
|
}
|
|
560
575
|
|
|
561
576
|
.more {
|
|
@@ -1416,6 +1431,7 @@ svg {
|
|
|
1416
1431
|
|
|
1417
1432
|
.composer-card {
|
|
1418
1433
|
display: flex;
|
|
1434
|
+
flex-wrap: wrap;
|
|
1419
1435
|
align-items: flex-end;
|
|
1420
1436
|
gap: 8px;
|
|
1421
1437
|
padding: 8px 8px 8px 16px;
|
|
@@ -1434,7 +1450,7 @@ svg {
|
|
|
1434
1450
|
}
|
|
1435
1451
|
|
|
1436
1452
|
.composer textarea {
|
|
1437
|
-
flex: 1
|
|
1453
|
+
flex: 1 0 100%;
|
|
1438
1454
|
min-width: 0;
|
|
1439
1455
|
min-height: 36px;
|
|
1440
1456
|
max-height: 240px;
|
|
@@ -1458,8 +1474,6 @@ svg {
|
|
|
1458
1474
|
outline: none;
|
|
1459
1475
|
}
|
|
1460
1476
|
|
|
1461
|
-
.pro-mode .composer-card { flex-wrap: wrap; }
|
|
1462
|
-
.pro-mode .composer textarea { flex: 1 0 100%; }
|
|
1463
1477
|
.pro-preset-control { position: relative; margin-left: auto; align-self: center; }
|
|
1464
1478
|
.pro-preset-control > summary {
|
|
1465
1479
|
display: flex; align-items: center; gap: 10px; padding: 7px 10px;
|
|
@@ -1496,20 +1510,20 @@ svg {
|
|
|
1496
1510
|
border-radius: 50%; background: rgba(255, 255, 255, .18);
|
|
1497
1511
|
}
|
|
1498
1512
|
.pro-slider-stop.is-filled { background: rgba(255, 255, 255, .35); }
|
|
1499
|
-
#pro-preset-range {
|
|
1513
|
+
#pro-preset-range, #model-intensity {
|
|
1500
1514
|
position: relative; display: block; width: 100%; height: 40px; margin: 0; padding: 0;
|
|
1501
1515
|
-webkit-appearance: none; appearance: none; border: 0; border-radius: 999px; background: transparent; cursor: pointer;
|
|
1502
1516
|
}
|
|
1503
|
-
#pro-preset-range::-webkit-slider-runnable-track { height: 24px; border: 0; background: transparent; }
|
|
1504
|
-
#pro-preset-range::-webkit-slider-thumb {
|
|
1517
|
+
#pro-preset-range::-webkit-slider-runnable-track, #model-intensity::-webkit-slider-runnable-track { height: 24px; border: 0; background: transparent; }
|
|
1518
|
+
#pro-preset-range::-webkit-slider-thumb, #model-intensity::-webkit-slider-thumb {
|
|
1505
1519
|
-webkit-appearance: none; appearance: none; width: 28px; height: 28px; margin-top: -2px;
|
|
1506
1520
|
border: 0; border-radius: 50%; background: #fff;
|
|
1507
1521
|
}
|
|
1508
|
-
#pro-preset-range::-moz-range-track { height: 24px; border: 0; background: transparent; }
|
|
1509
|
-
#pro-preset-range::-moz-range-progress { background: transparent; }
|
|
1510
|
-
#pro-preset-range::-moz-range-thumb { width: 28px; height: 28px; border: 0; border-radius: 50%; background: #fff; }
|
|
1511
|
-
#pro-preset-range:focus-visible { outline-offset: 2px; }
|
|
1512
|
-
#pro-preset-range:disabled { cursor: default; }
|
|
1522
|
+
#pro-preset-range::-moz-range-track, #model-intensity::-moz-range-track { height: 24px; border: 0; background: transparent; }
|
|
1523
|
+
#pro-preset-range::-moz-range-progress, #model-intensity::-moz-range-progress { background: transparent; }
|
|
1524
|
+
#pro-preset-range::-moz-range-thumb, #model-intensity::-moz-range-thumb { width: 28px; height: 28px; border: 0; border-radius: 50%; background: #fff; }
|
|
1525
|
+
#pro-preset-range:focus-visible, #model-intensity:focus-visible { outline-offset: 2px; }
|
|
1526
|
+
#pro-preset-range:disabled, #model-intensity:disabled { cursor: default; }
|
|
1513
1527
|
.pro-preset-ticks { position: relative; height: 24px; margin: 6px 14px 16px; }
|
|
1514
1528
|
.pro-preset-ticks button {
|
|
1515
1529
|
position: absolute; transform: translateX(-50%); border: 0; background: transparent;
|
|
@@ -1931,40 +1945,18 @@ svg {
|
|
|
1931
1945
|
|
|
1932
1946
|
/* ----------------------------------------------------- model and settings */
|
|
1933
1947
|
|
|
1934
|
-
.model-picker {
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
align-items: flex-end;
|
|
1939
|
-
gap: 10px;
|
|
1940
|
-
padding: 10px clamp(16px, 4vw, 32px) 12px;
|
|
1941
|
-
border-bottom: 1px solid var(--border-soft);
|
|
1942
|
-
}
|
|
1943
|
-
|
|
1944
|
-
.model-picker label {
|
|
1948
|
+
.model-picker { min-width: 0; max-width: min(75%, 340px); }
|
|
1949
|
+
.model-picker > summary span { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
1950
|
+
.model-picker-fields { display: grid; gap: 12px; margin-bottom: 18px; }
|
|
1951
|
+
.model-picker-fields label {
|
|
1945
1952
|
display: flex;
|
|
1946
1953
|
flex-direction: column;
|
|
1947
1954
|
gap: 5px;
|
|
1948
|
-
flex: 0 1 170px;
|
|
1949
1955
|
min-width: 0;
|
|
1950
|
-
color: var(--text-
|
|
1956
|
+
color: var(--text-tertiary);
|
|
1951
1957
|
font-size: 11px;
|
|
1952
1958
|
}
|
|
1953
|
-
|
|
1954
|
-
.model-picker label:nth-child(2) {
|
|
1955
|
-
flex-grow: 1;
|
|
1956
|
-
max-width: 300px;
|
|
1957
|
-
}
|
|
1958
|
-
|
|
1959
|
-
.model-picker .control {
|
|
1960
|
-
height: 34px;
|
|
1961
|
-
}
|
|
1962
|
-
|
|
1963
|
-
.model-picker .form-feedback {
|
|
1964
|
-
flex: 1 1 200px;
|
|
1965
|
-
align-self: center;
|
|
1966
|
-
margin: 0;
|
|
1967
|
-
}
|
|
1959
|
+
.model-picker-card .form-feedback { margin: 0; }
|
|
1968
1960
|
|
|
1969
1961
|
select,
|
|
1970
1962
|
.settings-fields input[type="password"],
|
|
@@ -1981,10 +1973,6 @@ select,
|
|
|
1981
1973
|
transition: border-color var(--fast), background-color var(--fast);
|
|
1982
1974
|
}
|
|
1983
1975
|
|
|
1984
|
-
.model-picker select {
|
|
1985
|
-
height: 34px;
|
|
1986
|
-
}
|
|
1987
|
-
|
|
1988
1976
|
select {
|
|
1989
1977
|
appearance: none;
|
|
1990
1978
|
padding-right: 32px;
|
|
@@ -2341,11 +2329,6 @@ select option {
|
|
|
2341
2329
|
display: none;
|
|
2342
2330
|
}
|
|
2343
2331
|
|
|
2344
|
-
/* Sit below the model row instead of covering it. */
|
|
2345
|
-
#model-picker:not(.hidden) + #plan-card {
|
|
2346
|
-
top: 138px;
|
|
2347
|
-
}
|
|
2348
|
-
|
|
2349
2332
|
.plan-card-head {
|
|
2350
2333
|
display: flex;
|
|
2351
2334
|
align-items: center;
|
|
@@ -3168,6 +3151,12 @@ html.pro-transitioning body::before {
|
|
|
3168
3151
|
}
|
|
3169
3152
|
}
|
|
3170
3153
|
|
|
3154
|
+
@keyframes spin {
|
|
3155
|
+
to {
|
|
3156
|
+
transform: rotate(360deg);
|
|
3157
|
+
}
|
|
3158
|
+
}
|
|
3159
|
+
|
|
3171
3160
|
@media (prefers-reduced-motion: reduce) {
|
|
3172
3161
|
.pro-slider-meteors { display: none; }
|
|
3173
3162
|
*,
|
|
@@ -3296,27 +3285,6 @@ html.pro-transitioning body::before {
|
|
|
3296
3285
|
height: 34px;
|
|
3297
3286
|
}
|
|
3298
3287
|
|
|
3299
|
-
.model-picker {
|
|
3300
|
-
gap: 8px;
|
|
3301
|
-
padding: 10px 12px;
|
|
3302
|
-
max-height: 30vh;
|
|
3303
|
-
max-height: 30dvh;
|
|
3304
|
-
overflow: auto;
|
|
3305
|
-
}
|
|
3306
|
-
|
|
3307
|
-
.model-picker label {
|
|
3308
|
-
flex: 1 1 calc(50% - 8px);
|
|
3309
|
-
}
|
|
3310
|
-
|
|
3311
|
-
.model-picker label:nth-child(2) {
|
|
3312
|
-
max-width: none;
|
|
3313
|
-
}
|
|
3314
|
-
|
|
3315
|
-
.model-picker .control {
|
|
3316
|
-
flex: 1 1 calc(50% - 8px);
|
|
3317
|
-
height: 38px;
|
|
3318
|
-
}
|
|
3319
|
-
|
|
3320
3288
|
.messages {
|
|
3321
3289
|
padding: 16px 14px 20px;
|
|
3322
3290
|
}
|
|
@@ -3338,6 +3306,11 @@ html.pro-transitioning body::before {
|
|
|
3338
3306
|
border-radius: 20px;
|
|
3339
3307
|
}
|
|
3340
3308
|
|
|
3309
|
+
.model-picker-card {
|
|
3310
|
+
right: -48px;
|
|
3311
|
+
width: min(390px, calc(100vw - 32px));
|
|
3312
|
+
}
|
|
3313
|
+
|
|
3341
3314
|
.composer textarea {
|
|
3342
3315
|
max-height: min(220px, 25dvh);
|
|
3343
3316
|
}
|
|
@@ -3483,13 +3456,6 @@ html.pro-transitioning body::before {
|
|
|
3483
3456
|
overflow-y: auto;
|
|
3484
3457
|
}
|
|
3485
3458
|
|
|
3486
|
-
.model-picker {
|
|
3487
|
-
flex-shrink: 1;
|
|
3488
|
-
min-height: 44px;
|
|
3489
|
-
padding-top: 6px;
|
|
3490
|
-
padding-bottom: 6px;
|
|
3491
|
-
}
|
|
3492
|
-
|
|
3493
3459
|
.messages {
|
|
3494
3460
|
min-height: 40px;
|
|
3495
3461
|
padding-top: 8px;
|