@jxsuite/studio 0.32.0 → 0.34.0
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/dist/iframe-entry.js +6230 -0
- package/dist/iframe-entry.js.map +35 -0
- package/dist/studio.js +22791 -13709
- package/dist/studio.js.map +181 -74
- package/package.json +12 -7
- package/src/browse/browse.ts +11 -4
- package/src/canvas/canvas-helpers.ts +2 -56
- package/src/canvas/canvas-live-render.ts +102 -435
- package/src/canvas/canvas-origin.ts +66 -0
- package/src/canvas/canvas-patcher.ts +63 -403
- package/src/canvas/canvas-render.ts +70 -212
- package/src/canvas/canvas-utils.ts +37 -65
- package/src/canvas/iframe-channel.ts +154 -0
- package/src/canvas/iframe-drop.ts +484 -0
- package/src/canvas/iframe-entry.ts +600 -0
- package/src/canvas/iframe-host.ts +1373 -0
- package/src/canvas/iframe-inline-edit.ts +367 -0
- package/src/canvas/iframe-insert.ts +164 -0
- package/src/canvas/iframe-interaction.ts +176 -0
- package/src/canvas/iframe-keys.ts +85 -0
- package/src/canvas/iframe-overlay.ts +218 -0
- package/src/canvas/iframe-patch.ts +363 -0
- package/src/canvas/iframe-protocol.ts +361 -0
- package/src/canvas/iframe-render.ts +458 -0
- package/src/canvas/iframe-slash.ts +114 -0
- package/src/canvas/iframe-subtree.ts +113 -0
- package/src/canvas/path-mapping.ts +86 -0
- package/src/canvas/serialize-scope.ts +65 -0
- package/src/editor/canvas-context-menu.ts +40 -0
- package/src/editor/canvas-slash-bridge.ts +21 -0
- package/src/editor/context-menu.ts +2 -1
- package/src/editor/inline-edit-apply.ts +183 -0
- package/src/editor/inline-edit.ts +99 -21
- package/src/editor/inline-link.ts +89 -0
- package/src/editor/insert-zone-action.ts +35 -0
- package/src/editor/merge-tags.ts +26 -2
- package/src/editor/repeater-scope.ts +144 -0
- package/src/editor/shortcuts.ts +14 -28
- package/src/editor/slash-menu.ts +73 -42
- package/src/files/files.ts +2 -1
- package/src/page-params.ts +383 -0
- package/src/panels/ai-panel.ts +386 -328
- package/src/panels/block-action-bar.ts +296 -138
- package/src/panels/canvas-dnd-bridge.ts +397 -0
- package/src/panels/component-preview.ts +56 -0
- package/src/panels/dnd.ts +41 -17
- package/src/panels/drag-ghost.ts +62 -0
- package/src/panels/editors.ts +1 -1
- package/src/panels/overlays.ts +10 -125
- package/src/panels/properties-panel.ts +210 -0
- package/src/panels/right-panel.ts +0 -2
- package/src/panels/signals-panel.ts +136 -22
- package/src/panels/stylebook-doc.ts +373 -0
- package/src/panels/stylebook-panel.ts +46 -689
- package/src/panels/tab-bar.ts +159 -13
- package/src/panels/toolbar.ts +3 -2
- package/src/platforms/devserver.ts +18 -47
- package/src/services/ai-settings.ts +107 -0
- package/src/services/ai-system-prompt.ts +617 -0
- package/src/services/ai-tools.ts +854 -0
- package/src/services/context-manager.ts +200 -0
- package/src/services/document-assistant.ts +183 -0
- package/src/services/jx-validate.ts +65 -0
- package/src/services/monaco-setup.ts +12 -0
- package/src/services/render-critic.ts +75 -0
- package/src/services/token-lint.ts +140 -0
- package/src/services/tool-executor.ts +156 -0
- package/src/settings/css-vars-editor.ts +2 -2
- package/src/state.ts +29 -0
- package/src/store.ts +4 -62
- package/src/studio.ts +90 -40
- package/src/tabs/doc-op-apply.ts +89 -0
- package/src/tabs/patch-ops.ts +6 -2
- package/src/tabs/tab.ts +23 -4
- package/src/tabs/transact.ts +33 -69
- package/src/types.ts +16 -24
- package/src/ui/jx-theme.ts +63 -0
- package/src/ui/media-picker.ts +6 -4
- package/src/ui/spectrum.ts +5 -0
- package/src/utils/canvas-media.ts +0 -137
- package/src/utils/edit-display.ts +23 -3
- package/src/utils/geometry.ts +43 -0
- package/src/utils/link-target.ts +93 -0
- package/src/utils/strip-events.ts +54 -0
- package/src/view.ts +0 -23
- package/src/workspace/workspace.ts +14 -1
- package/src/canvas/canvas-subtree-render.ts +0 -113
- package/src/editor/component-inline-edit.ts +0 -349
- package/src/editor/content-inline-edit.ts +0 -207
- package/src/editor/insertion-helper.ts +0 -308
- package/src/panels/canvas-dnd.ts +0 -329
- package/src/panels/panel-events.ts +0 -306
- package/src/panels/preview-render.ts +0 -132
- package/src/panels/pseudo-preview.ts +0 -75
package/src/panels/tab-bar.ts
CHANGED
|
@@ -1,20 +1,23 @@
|
|
|
1
1
|
/// <reference lib="dom" />
|
|
2
2
|
/**
|
|
3
|
-
* Tab bar — a per-tab
|
|
4
|
-
* Standardizes Back/breadcrumb navigation,
|
|
5
|
-
*
|
|
3
|
+
* Tab bar — a persistent per-tab settings bar rendered between the tab strip and the edit content.
|
|
4
|
+
* Standardizes Back/breadcrumb navigation, the view settings cluster (preview toggle, layout
|
|
5
|
+
* visibility, dynamic route-param pickers — shown in the edit/design base modes), media feature
|
|
6
|
+
* toggles, and mode actions (Code-mode Export) into a single bar shared by every edit mode.
|
|
6
7
|
*
|
|
7
8
|
* Follows the same module shape as tab-strip.ts: mount(host, ctx) → effectScope/effect → render().
|
|
8
|
-
* The bar
|
|
9
|
-
* contextual to show.
|
|
9
|
+
* The bar renders `nothing` (so `#tab-bar:empty` hides the row) only when no tab is active.
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import { html, render as litRender, nothing } from "lit-html";
|
|
13
|
-
import { updateUi } from "../store";
|
|
13
|
+
import { projectState, updateUi } from "../store";
|
|
14
14
|
import { effect, effectScope } from "../reactivity";
|
|
15
15
|
import { activeTab } from "../workspace/workspace";
|
|
16
|
-
import { getEffectiveMedia } from "../site-context";
|
|
16
|
+
import { getEffectiveLayoutPath, getEffectiveMedia } from "../site-context";
|
|
17
|
+
import { dynamicRouteParams, loadParamValues, pagePathsDef } from "../page-params";
|
|
17
18
|
import { mediaDisplayName } from "./shared";
|
|
19
|
+
import type { ParamValues } from "../page-params";
|
|
20
|
+
import type { Tab } from "../tabs/tab";
|
|
18
21
|
import type { DocumentStackEntry, FunctionEditDef } from "../types";
|
|
19
22
|
import type { EffectScope } from "@vue/reactivity";
|
|
20
23
|
import type { TemplateResult } from "lit-html";
|
|
@@ -59,12 +62,16 @@ export function mount(host: HTMLElement, ctx: TabBarCtx) {
|
|
|
59
62
|
if (tab) {
|
|
60
63
|
// Read reactive properties to establish tracking — mirrors the toolbar's subset
|
|
61
64
|
void tab.doc.document;
|
|
65
|
+
void tab.doc.document?.$layout;
|
|
62
66
|
void tab.doc.mode;
|
|
63
67
|
void tab.documentPath;
|
|
64
68
|
void tab.session.documentStack.length;
|
|
65
69
|
void tab.session.ui.canvasMode;
|
|
66
70
|
void tab.session.ui.editingFunction;
|
|
67
71
|
void tab.session.ui.featureToggles;
|
|
72
|
+
void tab.session.ui.preview;
|
|
73
|
+
void tab.session.ui.previewParams;
|
|
74
|
+
void tab.session.ui.showLayout;
|
|
68
75
|
}
|
|
69
76
|
render();
|
|
70
77
|
});
|
|
@@ -143,6 +150,55 @@ function tabBarTemplate(ctx: TabBarCtx): TemplateResult | typeof nothing {
|
|
|
143
150
|
`;
|
|
144
151
|
}
|
|
145
152
|
|
|
153
|
+
// ── Right region: view settings cluster (edit/design base modes only) ──
|
|
154
|
+
// Gates on the BASE mode (not the effective mode): the cluster stays visible while the preview
|
|
155
|
+
// Toggle is on so it can be toggled back off.
|
|
156
|
+
const baseMode = S.ui.canvasMode;
|
|
157
|
+
const isPage = Boolean(
|
|
158
|
+
S.documentPath &&
|
|
159
|
+
projectState?.isSiteProject &&
|
|
160
|
+
(S.documentPath.startsWith("pages/") || S.documentPath.startsWith("./pages/")),
|
|
161
|
+
);
|
|
162
|
+
let settingsTpl: TemplateResult | typeof nothing = nothing;
|
|
163
|
+
if (!editing && (baseMode === "edit" || baseMode === "design")) {
|
|
164
|
+
const canPreview = tab.capabilities.modes.includes("preview");
|
|
165
|
+
const hasLayout = isPage && Boolean(getEffectiveLayoutPath(S.document?.$layout));
|
|
166
|
+
const pickersTpl = isPage ? paramPickersTpl(tab) : nothing;
|
|
167
|
+
const previewTpl = canPreview
|
|
168
|
+
? html`
|
|
169
|
+
<sp-action-button
|
|
170
|
+
toggles
|
|
171
|
+
size="s"
|
|
172
|
+
title="Preview resolved values"
|
|
173
|
+
?selected=${Boolean(S.ui.preview)}
|
|
174
|
+
@click=${() => updateUi("preview", !S.ui.preview)}
|
|
175
|
+
>
|
|
176
|
+
<sp-icon-preview slot="icon"></sp-icon-preview>
|
|
177
|
+
Preview
|
|
178
|
+
</sp-action-button>
|
|
179
|
+
`
|
|
180
|
+
: nothing;
|
|
181
|
+
const layoutTpl = hasLayout
|
|
182
|
+
? html`
|
|
183
|
+
<sp-action-button
|
|
184
|
+
toggles
|
|
185
|
+
size="s"
|
|
186
|
+
title="Show layout elements"
|
|
187
|
+
?selected=${S.ui.showLayout !== false}
|
|
188
|
+
@click=${() => updateUi("showLayout", S.ui.showLayout === false)}
|
|
189
|
+
>
|
|
190
|
+
Layout
|
|
191
|
+
</sp-action-button>
|
|
192
|
+
`
|
|
193
|
+
: nothing;
|
|
194
|
+
if (pickersTpl !== nothing || previewTpl !== nothing || layoutTpl !== nothing) {
|
|
195
|
+
settingsTpl = html`
|
|
196
|
+
${pickersTpl}
|
|
197
|
+
<sp-action-group compact size="s">${layoutTpl} ${previewTpl}</sp-action-group>
|
|
198
|
+
`;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
146
202
|
// ── Right region: media feature toggles ──
|
|
147
203
|
const { featureQueries } = ctx.parseMediaEntries(getEffectiveMedia(S.document?.$media));
|
|
148
204
|
const togglesTpl =
|
|
@@ -183,16 +239,106 @@ function tabBarTemplate(ctx: TabBarCtx): TemplateResult | typeof nothing {
|
|
|
183
239
|
`
|
|
184
240
|
: nothing;
|
|
185
241
|
|
|
186
|
-
// Collapse the bar entirely when there is nothing contextual to show.
|
|
187
|
-
if (navTpl === nothing && togglesTpl === nothing && exportTpl === nothing) {
|
|
188
|
-
return nothing;
|
|
189
|
-
}
|
|
190
|
-
|
|
191
242
|
return html`
|
|
192
243
|
<div class="tab-bar">
|
|
193
244
|
${navTpl}
|
|
194
245
|
<div class="tb-spacer"></div>
|
|
195
|
-
${togglesTpl} ${exportTpl}
|
|
246
|
+
${settingsTpl} ${togglesTpl} ${exportTpl}
|
|
196
247
|
</div>
|
|
197
248
|
`;
|
|
198
249
|
}
|
|
250
|
+
|
|
251
|
+
// ── Dynamic route-param pickers ──────────────────────────────────────────────
|
|
252
|
+
// Candidate values load asynchronously (ContentCollection resolution / data-file read); the module
|
|
253
|
+
// Caches the last result per (documentPath, $paths) and re-renders when it lands — the same lazy
|
|
254
|
+
// Fill pattern as head-panel's loadLayoutEntries. When values arrive, any param without a chosen
|
|
255
|
+
// Value auto-selects the first candidate (matching the compiler, whose first expanded route is the
|
|
256
|
+
// First path entry).
|
|
257
|
+
|
|
258
|
+
let _paramValues: ParamValues | null = null;
|
|
259
|
+
|
|
260
|
+
let _paramValuesKey: string | null = null;
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* @param {Tab} tab
|
|
264
|
+
* @returns {ParamValues | null} — null while loading (or when the doc declares no params)
|
|
265
|
+
*/
|
|
266
|
+
function paramValuesFor(tab: Tab): ParamValues | null {
|
|
267
|
+
const pathsDef = pagePathsDef({
|
|
268
|
+
document: tab.doc.document,
|
|
269
|
+
frontmatter: tab.doc.content.frontmatter,
|
|
270
|
+
});
|
|
271
|
+
if (!pathsDef && dynamicRouteParams(tab.documentPath).length === 0) {
|
|
272
|
+
return null;
|
|
273
|
+
}
|
|
274
|
+
const key = `${tab.documentPath ?? ""}::${JSON.stringify(pathsDef)}`;
|
|
275
|
+
if (_paramValuesKey === key) {
|
|
276
|
+
return _paramValues;
|
|
277
|
+
}
|
|
278
|
+
_paramValuesKey = key;
|
|
279
|
+
_paramValues = null;
|
|
280
|
+
void loadParamValues(tab.documentPath, pathsDef).then((values) => {
|
|
281
|
+
if (_paramValuesKey !== key || activeTab.value !== tab) {
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
_paramValues = values;
|
|
285
|
+
autoSelectParams(tab, values);
|
|
286
|
+
render();
|
|
287
|
+
});
|
|
288
|
+
return _paramValues;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* @param {Tab} tab
|
|
293
|
+
* @param {ParamValues} values
|
|
294
|
+
*/
|
|
295
|
+
function autoSelectParams(tab: Tab, values: ParamValues) {
|
|
296
|
+
const current = tab.session.ui.previewParams ?? {};
|
|
297
|
+
const additions: Record<string, string> = {};
|
|
298
|
+
for (const [name, list] of Object.entries(values)) {
|
|
299
|
+
if (!current[name] && list.length > 0) {
|
|
300
|
+
additions[name] = list[0]!;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
if (Object.keys(additions).length > 0) {
|
|
304
|
+
updateUi("previewParams", { ...current, ...additions });
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* @param {Tab} tab
|
|
310
|
+
* @returns {TemplateResult | typeof nothing}
|
|
311
|
+
*/
|
|
312
|
+
function paramPickersTpl(tab: Tab): TemplateResult | typeof nothing {
|
|
313
|
+
const values = paramValuesFor(tab);
|
|
314
|
+
const names = new Set(dynamicRouteParams(tab.documentPath));
|
|
315
|
+
for (const name of Object.keys(values ?? {})) {
|
|
316
|
+
names.add(name);
|
|
317
|
+
}
|
|
318
|
+
if (names.size === 0) {
|
|
319
|
+
return nothing;
|
|
320
|
+
}
|
|
321
|
+
const { previewParams } = tab.session.ui;
|
|
322
|
+
return html`
|
|
323
|
+
${[...names].map(
|
|
324
|
+
(name) => html`
|
|
325
|
+
<sp-picker
|
|
326
|
+
size="s"
|
|
327
|
+
quiet
|
|
328
|
+
class="tab-bar-param"
|
|
329
|
+
label=${name}
|
|
330
|
+
title=${`Preview value for [${name}]`}
|
|
331
|
+
value=${previewParams?.[name] ?? ""}
|
|
332
|
+
@change=${(e: Event) => {
|
|
333
|
+
const { value } = e.target as HTMLInputElement;
|
|
334
|
+
updateUi("previewParams", { ...tab.session.ui.previewParams, [name]: value });
|
|
335
|
+
}}
|
|
336
|
+
>
|
|
337
|
+
${(values?.[name] ?? []).map(
|
|
338
|
+
(v: string) => html`<sp-menu-item value=${v}>${v}</sp-menu-item>`,
|
|
339
|
+
)}
|
|
340
|
+
</sp-picker>
|
|
341
|
+
`,
|
|
342
|
+
)}
|
|
343
|
+
`;
|
|
344
|
+
}
|
package/src/panels/toolbar.ts
CHANGED
|
@@ -305,7 +305,6 @@ function minimalToolbarTemplate(ctx: ToolbarCtx) {
|
|
|
305
305
|
const modes = [
|
|
306
306
|
{ iconTag: "sp-icon-edit", key: "edit", label: "Edit" },
|
|
307
307
|
{ iconTag: "sp-icon-artboard", key: "design", label: "Design" },
|
|
308
|
-
{ iconTag: "sp-icon-preview", key: "preview", label: "Preview" },
|
|
309
308
|
{ iconTag: "sp-icon-code", key: "source", label: "Code" },
|
|
310
309
|
{ iconTag: "sp-icon-brush", key: "stylebook", label: "Stylebook" },
|
|
311
310
|
];
|
|
@@ -333,7 +332,9 @@ function toolbarTemplate() {
|
|
|
333
332
|
selection: tab.session.selection,
|
|
334
333
|
ui: tab.session.ui,
|
|
335
334
|
};
|
|
336
|
-
|
|
335
|
+
// Base mode, not the effective mode: the switcher keeps Edit/Design highlighted while the
|
|
336
|
+
// Tab-bar preview toggle is on (preview is no longer a switchable mode).
|
|
337
|
+
const { canvasMode } = tab.session.ui;
|
|
337
338
|
|
|
338
339
|
const modeSwitcherTpl = html`
|
|
339
340
|
<sp-action-group selects="single" size="s" compact>
|
|
@@ -588,6 +588,21 @@ export function createDevServerPlatform() {
|
|
|
588
588
|
return schema;
|
|
589
589
|
},
|
|
590
590
|
|
|
591
|
+
// ─── Class resolution (dev-proxy) ─────────────────────────────────────
|
|
592
|
+
|
|
593
|
+
/** @param {Record<string, unknown>} body */
|
|
594
|
+
async resolveClass(body: Record<string, unknown>) {
|
|
595
|
+
const res = await fetch("/__jx_resolve__", {
|
|
596
|
+
body: JSON.stringify(body),
|
|
597
|
+
headers: { "content-type": "application/json" },
|
|
598
|
+
method: "POST",
|
|
599
|
+
});
|
|
600
|
+
if (!res.ok) {
|
|
601
|
+
throw new Error(`Class resolution failed: ${res.status}`);
|
|
602
|
+
}
|
|
603
|
+
return await readJson<unknown>(res);
|
|
604
|
+
},
|
|
605
|
+
|
|
591
606
|
// ─── Git operations ──────────────────────────────────────────────────
|
|
592
607
|
|
|
593
608
|
async gitStatus() {
|
|
@@ -793,54 +808,10 @@ export function createDevServerPlatform() {
|
|
|
793
808
|
}
|
|
794
809
|
},
|
|
795
810
|
|
|
796
|
-
// ─── AI Assistant
|
|
797
|
-
|
|
798
|
-
async aiAuthStatus() {
|
|
799
|
-
const res = await fetch("/__studio/ai/auth-status");
|
|
800
|
-
return await res.json();
|
|
801
|
-
},
|
|
802
|
-
|
|
803
|
-
/** @param {{ message: string; systemPrompt?: string }} opts */
|
|
804
|
-
async aiCreateSession(opts: { message: string; systemPrompt?: string }) {
|
|
805
|
-
const res = await fetch("/__studio/ai/session", {
|
|
806
|
-
body: JSON.stringify(opts),
|
|
807
|
-
headers: { "Content-Type": "application/json" },
|
|
808
|
-
method: "POST",
|
|
809
|
-
});
|
|
810
|
-
if (!res.ok) {
|
|
811
|
-
const body = await readJson<ErrorBody>(res);
|
|
812
|
-
throw new Error(body.error);
|
|
813
|
-
}
|
|
814
|
-
return await res.json();
|
|
815
|
-
},
|
|
816
|
-
|
|
817
|
-
/** @param {string} id @param {string} message */
|
|
818
|
-
async aiSendMessage(id: string, message: string) {
|
|
819
|
-
const res = await fetch(`/__studio/ai/session/${id}/message`, {
|
|
820
|
-
body: JSON.stringify({ message }),
|
|
821
|
-
headers: { "Content-Type": "application/json" },
|
|
822
|
-
method: "POST",
|
|
823
|
-
});
|
|
824
|
-
if (!res.ok) {
|
|
825
|
-
const body = await readJson<ErrorBody>(res);
|
|
826
|
-
throw new Error(body.error);
|
|
827
|
-
}
|
|
828
|
-
return await res.json();
|
|
829
|
-
},
|
|
830
|
-
|
|
831
|
-
/** @param {string} id */
|
|
832
|
-
aiStreamUrl(id: string) {
|
|
833
|
-
return `/__studio/ai/session/${id}/stream`;
|
|
834
|
-
},
|
|
835
|
-
|
|
836
|
-
/** @param {string} id */
|
|
837
|
-
async aiStopSession(id: string) {
|
|
838
|
-
await fetch(`/__studio/ai/session/${id}/stop`, { method: "POST" });
|
|
839
|
-
},
|
|
811
|
+
// ─── AI Assistant (Stack B: OpenAI-compatible SSE proxy) ───────────────────
|
|
840
812
|
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
await fetch(`/__studio/ai/session/${id}`, { method: "DELETE" });
|
|
813
|
+
aiChatUrl() {
|
|
814
|
+
return "/__studio/ai/chat";
|
|
844
815
|
},
|
|
845
816
|
};
|
|
846
817
|
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/// <reference lib="dom" />
|
|
2
|
+
/**
|
|
3
|
+
* Ai-settings.js — local persistence for the AI assistant's provider settings.
|
|
4
|
+
*
|
|
5
|
+
* The Stack B proxy reads the OpenAI key from the `X-Api-Key` header (falling back to the server's
|
|
6
|
+
* `OPENAI_API_KEY` env var). Studio stores a user-supplied key in localStorage so the browser/dev
|
|
7
|
+
* build works without an env var. The key never leaves the machine except to the same-origin proxy.
|
|
8
|
+
*
|
|
9
|
+
* @license MIT
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const KEY_STORAGE = "jx.ai.openaiKey";
|
|
13
|
+
const BASE_URL_STORAGE = "jx.ai.baseUrl";
|
|
14
|
+
const MODEL_STORAGE = "jx.ai.model";
|
|
15
|
+
const DEFAULT_MODEL = "gpt-4o";
|
|
16
|
+
|
|
17
|
+
/** @returns {string} The stored OpenAI key, or "" if none/unavailable. */
|
|
18
|
+
export function getOpenAiKey() {
|
|
19
|
+
try {
|
|
20
|
+
return globalThis.localStorage?.getItem(KEY_STORAGE) ?? "";
|
|
21
|
+
} catch {
|
|
22
|
+
return "";
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Persist (or clear) the OpenAI key.
|
|
28
|
+
*
|
|
29
|
+
* @param {string} key - The key to store; an empty/blank value clears it.
|
|
30
|
+
*/
|
|
31
|
+
export function setOpenAiKey(key: string) {
|
|
32
|
+
try {
|
|
33
|
+
const trimmed = (key || "").trim();
|
|
34
|
+
if (trimmed) {
|
|
35
|
+
globalThis.localStorage?.setItem(KEY_STORAGE, trimmed);
|
|
36
|
+
} else {
|
|
37
|
+
globalThis.localStorage?.removeItem(KEY_STORAGE);
|
|
38
|
+
}
|
|
39
|
+
} catch {
|
|
40
|
+
/* LocalStorage unavailable — settings are not persisted. */
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** @returns {boolean} Whether a non-empty key is stored. */
|
|
45
|
+
export function hasOpenAiKey() {
|
|
46
|
+
return getOpenAiKey().length > 0;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* @returns {string} The OpenAI-compatible base URL override (e.g. a local LLM, OpenRouter, Azure),
|
|
51
|
+
* or "" to use the proxy's default (`https://api.openai.com/v1` / server `OPENAI_BASE_URL`).
|
|
52
|
+
*/
|
|
53
|
+
export function getBaseUrl() {
|
|
54
|
+
try {
|
|
55
|
+
return globalThis.localStorage?.getItem(BASE_URL_STORAGE) ?? "";
|
|
56
|
+
} catch {
|
|
57
|
+
return "";
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Persist (or clear) the base URL override.
|
|
63
|
+
*
|
|
64
|
+
* @param {string} url - The base URL; an empty/blank value clears the override.
|
|
65
|
+
*/
|
|
66
|
+
export function setBaseUrl(url: string) {
|
|
67
|
+
try {
|
|
68
|
+
const trimmed = (url || "").trim().replace(/\/+$/, "");
|
|
69
|
+
if (trimmed) {
|
|
70
|
+
globalThis.localStorage?.setItem(BASE_URL_STORAGE, trimmed);
|
|
71
|
+
} else {
|
|
72
|
+
globalThis.localStorage?.removeItem(BASE_URL_STORAGE);
|
|
73
|
+
}
|
|
74
|
+
} catch {
|
|
75
|
+
/* LocalStorage unavailable — settings are not persisted. */
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ─── Model selection ────────────────────────────────────────────────────────
|
|
80
|
+
|
|
81
|
+
/** @returns {string} The stored model ID, or the default ("gpt-4o"). */
|
|
82
|
+
export function getModel() {
|
|
83
|
+
try {
|
|
84
|
+
return globalThis.localStorage?.getItem(MODEL_STORAGE) || DEFAULT_MODEL;
|
|
85
|
+
} catch {
|
|
86
|
+
return DEFAULT_MODEL;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Persist the selected model ID.
|
|
92
|
+
*
|
|
93
|
+
* @param {string} modelId - The model ID to store; an empty/blank value clears it, reverting to
|
|
94
|
+
* default.
|
|
95
|
+
*/
|
|
96
|
+
export function setModel(modelId: string) {
|
|
97
|
+
try {
|
|
98
|
+
const trimmed = (modelId || "").trim();
|
|
99
|
+
if (trimmed) {
|
|
100
|
+
globalThis.localStorage?.setItem(MODEL_STORAGE, trimmed);
|
|
101
|
+
} else {
|
|
102
|
+
globalThis.localStorage?.removeItem(MODEL_STORAGE);
|
|
103
|
+
}
|
|
104
|
+
} catch {
|
|
105
|
+
/* LocalStorage unavailable — settings are not persisted. */
|
|
106
|
+
}
|
|
107
|
+
}
|