@jxsuite/studio 2.4.3 → 4.0.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/README.md +5 -5
- package/dist/chunks/Overlay.dev-723bv0df.js +23 -0
- package/dist/chunks/cf-settings-45fgyx8y.js +21 -0
- package/dist/chunks/{components-z6cnkyy1.js → components-zmx10t62.js} +6 -5
- package/dist/chunks/content-source-t88vb2dr.js +49 -0
- package/dist/chunks/{convert-to-repeater-k10dvcc7.js → convert-to-repeater-dhw1nrz9.js} +27 -18
- package/dist/chunks/entry-commands-6z6bhxg0.js +46 -0
- package/dist/chunks/{extension-sections-0ada0xcw.js → extension-sections-3ydgjbdy.js} +32 -23
- package/dist/chunks/file-ops-rskmhxyq.js +48 -0
- package/dist/chunks/files-0cratsr0.js +92 -0
- package/dist/chunks/{format-host-wewba4nq.js → format-host-tgcmfs4p.js} +3 -3
- package/dist/chunks/fs-events-hrrg328g.js +21 -0
- package/dist/chunks/{git-panel-2dh1aw50.js → git-panel-0yywyk76.js} +21 -15
- package/dist/chunks/{jx-validate-jj9rnt4n.js → jx-validate-8q2g05x1.js} +6 -4
- package/dist/chunks/layers-panel-z3tqe5cd.js +67 -0
- package/dist/chunks/layers-q75tj2mf.js +45 -0
- package/dist/chunks/lit-html-j055mkh8.js +22 -0
- package/dist/chunks/monaco-buffer-dqw0xxpx.js +25 -0
- package/dist/chunks/{monaco-setup-a5wkmbgh.js → monaco-setup-rws7k4br.js} +4 -4
- package/dist/chunks/notify-yjm79n4s.js +30 -0
- package/dist/chunks/parse-24d0gay3.js +19 -0
- package/dist/chunks/platform-ba1j1gbh.js +16 -0
- package/dist/chunks/publish-panel-54jvhskq.js +28 -0
- package/dist/chunks/quick-search-cgfmjwq5.js +65 -0
- package/dist/chunks/references-3qx8fkyp.js +29 -0
- package/dist/chunks/right-panel-cqp8bc4v.js +46 -0
- package/dist/chunks/sp-overlay.dev-yztmtrs1.js +18 -0
- package/dist/chunks/sp-popover.dev-n2etmqdp.js +9 -0
- package/dist/chunks/sp-progress-circle.dev-hryhj0es.js +15 -0
- package/dist/chunks/sp-tooltip.dev-xcvpnqr0.js +15 -0
- package/dist/chunks/{sp-tray.dev-64v4pxw8.js → sp-tray.dev-tkbscm06.js} +5 -5
- package/dist/chunks/{studio-f5yyvqxv.js → studio-1hbqnt19.js} +37 -31
- package/dist/chunks/studio-2zkz8evn.js +3163 -0
- package/dist/chunks/{studio-0b6fs05q.js → studio-4pqqxbrt.js} +23 -15
- package/dist/chunks/{studio-kvfx9z2c.js → studio-4rzyrtjz.js} +70 -31
- package/dist/chunks/{studio-ef8t53vc.js → studio-5q69gfag.js} +60 -56
- package/dist/chunks/{studio-v2c5pmjj.js → studio-5rywrkxz.js} +565 -133
- package/dist/chunks/{studio-6kv3apa9.js → studio-68n67094.js} +25 -9
- package/dist/chunks/{studio-0rqqhrdy.js → studio-7nte2hj4.js} +2 -2
- package/dist/chunks/{studio-hyn3vhwr.js → studio-8swrjbny.js} +31 -18
- package/dist/chunks/studio-92ab2jqe.js +282 -0
- package/dist/chunks/{studio-sas7mt3k.js → studio-9ab8gqsm.js} +2 -2
- package/dist/chunks/{studio-28xc8ws4.js → studio-9c4k4vhv.js} +14 -10
- package/dist/chunks/studio-9n4mrmnb.js +184 -0
- package/dist/chunks/{studio-tr20m8g8.js → studio-9r6j5ev1.js} +37 -2
- package/dist/chunks/{studio-aweyr4v1.js → studio-ahjfdy6e.js} +9 -7
- package/dist/chunks/{studio-s519zc99.js → studio-bgxt93ga.js} +3 -3
- package/dist/chunks/{studio-26sv44br.js → studio-cb12yz9w.js} +8543 -8784
- package/dist/chunks/studio-cft9r4r7.js +119 -0
- package/dist/chunks/{studio-rqemfhz6.js → studio-eenfkt4e.js} +6 -6
- package/dist/chunks/studio-eesa1myv.js +498 -0
- package/dist/chunks/{studio-ay8wcr2b.js → studio-eqq1w638.js} +7 -9
- package/dist/chunks/{studio-ke244pam.js → studio-ex8zyp98.js} +51 -25
- package/dist/chunks/studio-gv8vvshc.js +118 -0
- package/dist/chunks/studio-hbtp0eh1.js +1315 -0
- package/dist/chunks/{studio-cz2j1c0g.js → studio-htsf0w6m.js} +75 -123
- package/dist/chunks/{studio-pyrw8h0w.js → studio-kjjsjak4.js} +528 -212
- package/dist/chunks/{studio-5x5774ak.js → studio-kryvsw6j.js} +3 -3
- package/dist/chunks/studio-kyh9avvv.js +28 -0
- package/dist/chunks/{studio-jnakhg87.js → studio-kyk2v92a.js} +14 -14
- package/dist/chunks/{studio-twsafr1e.js → studio-m3hwn7y1.js} +3 -3
- package/dist/chunks/{studio-tzx3xmvx.js → studio-mmv72a27.js} +8 -8
- package/dist/chunks/{studio-vhk5fmfv.js → studio-n8b9wqwy.js} +5 -5
- package/dist/chunks/{studio-cjm7fdjf.js → studio-p4ngc046.js} +8 -104
- package/dist/chunks/{studio-sccge2wf.js → studio-p8j86wag.js} +2 -2
- package/dist/chunks/{studio-v1va0vhd.js → studio-pdfjntbk.js} +75 -139
- package/dist/chunks/studio-q43a5y9h.js +394 -0
- package/dist/chunks/studio-q7v10cg2.js +106 -0
- package/dist/chunks/{studio-2w11grav.js → studio-qq6jym04.js} +3 -3
- package/dist/chunks/studio-r1r7afxx.js +1066 -0
- package/dist/chunks/{studio-3fk1ac9z.js → studio-rdx8jcxy.js} +4 -4
- package/dist/chunks/{studio-4mqb9a0j.js → studio-rn9ta2mz.js} +4 -4
- package/dist/chunks/studio-sf7zjgv1.js +91 -0
- package/dist/chunks/studio-t7qb9db8.js +665 -0
- package/dist/chunks/studio-tmc5emrw.js +8 -0
- package/dist/chunks/{studio-wdt9y92a.js → studio-txwkm6p9.js} +2 -2
- package/dist/chunks/{studio-dgj7gq44.js → studio-v38hmvx1.js} +7 -7
- package/dist/chunks/{studio-7ejemhej.js → studio-vadzgkrz.js} +6 -6
- package/dist/chunks/studio-wcy8trfx.js +263 -0
- package/dist/chunks/{studio-hb7c46jy.js → studio-wx5609xd.js} +21 -15
- package/dist/chunks/studio-xrapxwwc.js +1539 -0
- package/dist/chunks/{studio-ws6ef6js.js → studio-yxrkyv8a.js} +41 -29
- package/dist/chunks/{studio-xm3dbxvn.js → studio-zczps70q.js} +2 -2
- package/dist/chunks/workspace-xpres40v.js +89 -0
- package/dist/iframe-entry.js +450 -48
- package/dist/studio.js +2455 -1761
- package/package.json +16 -15
- package/src/browse/library-layouts.ts +3 -3
- package/src/browse/library-pane.ts +59 -5
- package/src/browse/library-preview.ts +19 -3
- package/src/canvas/asset-refs.ts +119 -0
- package/src/canvas/asset-resolve.ts +256 -0
- package/src/canvas/canvas-live-render.ts +23 -15
- package/src/canvas/canvas-render.ts +23 -1
- package/src/canvas/iframe-entry.ts +1 -0
- package/src/canvas/iframe-host.ts +4 -0
- package/src/canvas/iframe-patch.ts +47 -21
- package/src/canvas/iframe-protocol.ts +9 -0
- package/src/canvas/iframe-render.ts +50 -16
- package/src/commands/app-commands.ts +4 -0
- package/src/commands/command-args.ts +29 -0
- package/src/commands/context.ts +15 -1
- package/src/commands/defaults.ts +29 -1
- package/src/commands/levels.ts +16 -0
- package/src/commands/live-context.ts +3 -0
- package/src/content/collection-match.ts +267 -0
- package/src/content/entry-commands.ts +38 -29
- package/src/content/entry-fields.ts +6 -0
- package/src/editor/file-drop-action.ts +2 -1
- package/src/editor/shortcuts.ts +5 -0
- package/src/files/file-ops.ts +10 -42
- package/src/files/files.ts +487 -61
- package/src/files/fs-events.ts +9 -0
- package/src/files/gitignore.ts +432 -0
- package/src/files/media-paths.ts +37 -14
- package/src/files/media-upload.ts +47 -6
- package/src/files/serialize-document.ts +60 -0
- package/src/format/convert-file.ts +511 -0
- package/src/format/format-choices.ts +199 -0
- package/src/grid/grid-idle.ts +45 -0
- package/src/grid/grid-view.ts +35 -0
- package/src/grid/schema-columns.ts +2 -1
- package/src/grid/sources/content-source.ts +24 -3
- package/src/media/media-open.ts +58 -0
- package/src/media/media-pane.ts +310 -0
- package/src/new-project/import-tab.ts +333 -160
- package/src/new-project/new-project-modal.ts +27 -34
- package/src/packages/ensure-deps.ts +14 -7
- package/src/packages/jxsuite-update.ts +11 -11
- package/src/page-params.ts +10 -46
- package/src/panels/activity-bar.ts +45 -16
- package/src/panels/ai-chat/attached-context.ts +6 -1
- package/src/panels/ai-chat/chat-view.ts +255 -13
- package/src/panels/ai-chat/composer.ts +21 -80
- package/src/panels/ai-panel.ts +116 -2
- package/src/panels/frontmatter-fields.ts +6 -2
- package/src/panels/navigator-panels.ts +7 -6
- package/src/panels/properties-panel.ts +8 -2
- package/src/panels/seo-modal.ts +5 -6
- package/src/panels/settings-menu.ts +799 -0
- package/src/panels/signals-panel.ts +2 -1
- package/src/panels/toolbar.ts +135 -15
- package/src/platforms/cloud.ts +34 -3
- package/src/platforms/devserver.ts +72 -34
- package/src/preview/preview-overlay.ts +226 -0
- package/src/services/ai-ask.ts +247 -0
- package/src/services/ai-import-tools.ts +494 -0
- package/src/services/ai-models.ts +101 -11
- package/src/services/ai-project-tools.ts +20 -26
- package/src/services/ai-settings.ts +69 -71
- package/src/services/ai-system-prompt.ts +88 -2
- package/src/services/ai-turn-signal.ts +70 -0
- package/src/services/cf-settings.ts +19 -37
- package/src/services/connection.ts +74 -0
- package/src/services/context-manager.ts +89 -0
- package/src/services/document-assistant.ts +49 -7
- package/src/services/idle.ts +6 -1
- package/src/services/import-client.ts +33 -6
- package/src/services/import-run.ts +158 -0
- package/src/services/import-seed.ts +120 -0
- package/src/services/jx-validate.ts +58 -6
- package/src/services/project-adoption.ts +71 -0
- package/src/services/references.ts +0 -0
- package/src/services/settings/definitions.ts +181 -0
- package/src/services/settings/kernel.ts +335 -0
- package/src/services/settings/write-queue.ts +116 -0
- package/src/services/tool-executor.ts +34 -2
- package/src/settings/contexts-section.ts +45 -3
- package/src/settings/dependencies-editor.ts +43 -23
- package/src/settings/general-settings.ts +30 -12
- package/src/settings/preferences-accounts.ts +6 -19
- package/src/settings/preferences-dialog.ts +18 -41
- package/src/settings/preferences-keymap.ts +8 -18
- package/src/settings/preferences-sections.ts +57 -0
- package/src/settings/schema-field-ui.ts +2 -2
- package/src/settings/settings-document.ts +65 -9
- package/src/shell.ts +6 -14
- package/src/studio.ts +65 -7
- package/src/tabs/transact.ts +42 -19
- package/src/types.ts +164 -8
- package/src/ui/ai-credentials-form.ts +52 -27
- package/src/ui/ai-model-picker.ts +162 -0
- package/src/ui/choice-field.ts +72 -0
- package/src/ui/layers.ts +83 -5
- package/src/ui/media-picker.ts +6 -11
- package/src/utils/studio-utils.ts +43 -31
- package/styles/forced-colors.css +7 -0
- package/styles/overlays.css +256 -0
- package/styles/panels.css +13 -1
- package/styles/shell.css +111 -0
- package/dist/chunks/Overlay.dev-d2ajy6sj.js +0 -23
- package/dist/chunks/cf-settings-g5fbf2zp.js +0 -17
- package/dist/chunks/content-source-k9724vcv.js +0 -39
- package/dist/chunks/files-yew27sn9.js +0 -69
- package/dist/chunks/layers-panel-qnsghwwd.js +0 -59
- package/dist/chunks/publish-panel-a0pdft5w.js +0 -21
- package/dist/chunks/quick-search-9avyc4pc.js +0 -54
- package/dist/chunks/right-panel-kcnhtc5j.js +0 -38
- package/dist/chunks/sp-overlay.dev-s18hymn9.js +0 -18
- package/dist/chunks/sp-popover.dev-dj9x7e7a.js +0 -9
- package/dist/chunks/sp-progress-circle.dev-0zxcyz3r.js +0 -15
- package/dist/chunks/sp-tooltip.dev-5c6d1kjz.js +0 -15
- package/dist/chunks/studio-a7pzveq1.js +0 -110
- package/dist/chunks/studio-c31yb4t6.js +0 -55
- package/dist/chunks/studio-ra7ar50f.js +0 -6682
- package/src/canvas/content-assets.ts +0 -263
- package/src/canvas/site-style-css.ts +0 -121
- package/src/services/settings-store.ts +0 -99
|
@@ -19,16 +19,15 @@
|
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
21
|
import { html, nothing } from "lit-html";
|
|
22
|
+
import { live } from "lit-html/directives/live.js";
|
|
22
23
|
import type { TemplateResult } from "lit-html";
|
|
23
|
-
import { fetchAvailableModels
|
|
24
|
+
import { fetchAvailableModels } from "../services/ai-models";
|
|
24
25
|
import {
|
|
25
26
|
getBaseUrl,
|
|
26
|
-
getModel,
|
|
27
27
|
getOpenAiKey,
|
|
28
28
|
hasOpenAiKey,
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
setOpenAiKey,
|
|
29
|
+
saveAiProvider,
|
|
30
|
+
storedModel,
|
|
32
31
|
} from "../services/ai-settings";
|
|
33
32
|
|
|
34
33
|
export interface AiCredentialsFormOptions {
|
|
@@ -64,11 +63,23 @@ export function createAiCredentialsForm(opts: AiCredentialsFormOptions): AiCrede
|
|
|
64
63
|
let modelsLoading = false;
|
|
65
64
|
let modelsError = "";
|
|
66
65
|
|
|
67
|
-
/**
|
|
68
|
-
|
|
66
|
+
/**
|
|
67
|
+
* Load the drafts from what is stored.
|
|
68
|
+
*
|
|
69
|
+
* The model comes from {@link storedModel} rather than `getModel()`, so a user who has never
|
|
70
|
+
* picked one drafts an empty field instead of the `"gpt-4o"` default. Saving a prefilled default
|
|
71
|
+
* writes a choice nobody made, and `jx.ai.model: "gpt-4o"` is exactly what a broken install was
|
|
72
|
+
* left holding.
|
|
73
|
+
*/
|
|
74
|
+
function loadDrafts() {
|
|
69
75
|
keyDraft = getOpenAiKey();
|
|
70
76
|
baseUrlDraft = getBaseUrl();
|
|
71
|
-
modelDraft =
|
|
77
|
+
modelDraft = storedModel();
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Open the form pre-filled with the current settings, and load the model list. */
|
|
81
|
+
function startEdit() {
|
|
82
|
+
loadDrafts();
|
|
72
83
|
opts.requestRender();
|
|
73
84
|
// Auto-fetch available models if not already loaded.
|
|
74
85
|
if (availableModels.length === 0 && !modelsLoading) {
|
|
@@ -76,26 +87,33 @@ export function createAiCredentialsForm(opts: AiCredentialsFormOptions): AiCrede
|
|
|
76
87
|
}
|
|
77
88
|
}
|
|
78
89
|
|
|
79
|
-
/**
|
|
90
|
+
/**
|
|
91
|
+
* Persist the drafted key + endpoint + model and notify the host.
|
|
92
|
+
*
|
|
93
|
+
* **Re-seeds the drafts; it must never blank them.** Preferences is a place rather than a wizard
|
|
94
|
+
* step, so the sheet stays open across Save and `startEdit` is not called again — which meant
|
|
95
|
+
* every field emptied the instant a save succeeded. That reads as "it didn't take", and the
|
|
96
|
+
* obvious response to it destroyed the credentials: a blank draft is what
|
|
97
|
+
* `setOpenAiKey`/`setBaseUrl` treat as _clear_, so pressing Save a second time on the emptied
|
|
98
|
+
* form deleted the key and endpoint that the first press had just stored. Reading back through
|
|
99
|
+
* the getters also shows what was actually kept — trimmed, and with the endpoint's trailing slash
|
|
100
|
+
* stripped — rather than what was typed.
|
|
101
|
+
*/
|
|
80
102
|
function save() {
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
// Clear fetched models so they're re-fetched with the new credentials next time.
|
|
88
|
-
availableModels = [];
|
|
89
|
-
invalidateModelCache();
|
|
103
|
+
saveAiProvider({ apiKey: keyDraft, baseUrl: baseUrlDraft, model: modelDraft });
|
|
104
|
+
loadDrafts();
|
|
105
|
+
modelsError = "";
|
|
106
|
+
/* The fetched list stays: it was listed under exactly these credentials, and dropping it
|
|
107
|
+
collapsed the model combobox back to a free-text field on every save. The module cache is
|
|
108
|
+
dropped by ai-models' own settings subscription, so this form does not have to remember to. */
|
|
90
109
|
opts.onSaved?.();
|
|
91
110
|
opts.requestRender();
|
|
92
111
|
}
|
|
93
112
|
|
|
94
113
|
/** Dismiss the form without saving (only offered when a key already exists). */
|
|
95
114
|
function cancel() {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
modelDraft = "";
|
|
115
|
+
loadDrafts();
|
|
116
|
+
modelsError = "";
|
|
99
117
|
opts.onCancel?.();
|
|
100
118
|
opts.requestRender();
|
|
101
119
|
}
|
|
@@ -104,6 +122,11 @@ export function createAiCredentialsForm(opts: AiCredentialsFormOptions): AiCrede
|
|
|
104
122
|
* Fetch available models via src/services/ai-models.ts, preferring the in-form drafts over the
|
|
105
123
|
* stored settings so the list reflects the credentials being edited. Always forces past the
|
|
106
124
|
* module cache — this runs on explicit user action (or first open) with possibly-new drafts.
|
|
125
|
+
*
|
|
126
|
+
* Both lines below read draft-first. The key used to read stored-first, against this paragraph
|
|
127
|
+
* and against the line beside it: editing a key in place and pressing Fetch models listed the OLD
|
|
128
|
+
* key's models, and — worse — a form whose drafts had been blanked still fetched successfully
|
|
129
|
+
* from storage, which is what made an emptied form look like it was working.
|
|
107
130
|
*/
|
|
108
131
|
async function fetchModels() {
|
|
109
132
|
modelsLoading = true;
|
|
@@ -111,8 +134,10 @@ export function createAiCredentialsForm(opts: AiCredentialsFormOptions): AiCrede
|
|
|
111
134
|
opts.requestRender();
|
|
112
135
|
try {
|
|
113
136
|
availableModels = await fetchAvailableModels({
|
|
114
|
-
|
|
115
|
-
|
|
137
|
+
credentials: {
|
|
138
|
+
apiKey: keyDraft || getOpenAiKey(),
|
|
139
|
+
baseUrl: baseUrlDraft || getBaseUrl(),
|
|
140
|
+
},
|
|
116
141
|
force: true,
|
|
117
142
|
});
|
|
118
143
|
} catch (error: unknown) {
|
|
@@ -144,7 +169,7 @@ export function createAiCredentialsForm(opts: AiCredentialsFormOptions): AiCrede
|
|
|
144
169
|
type="password"
|
|
145
170
|
size="s"
|
|
146
171
|
placeholder="sk-… or any compatible key"
|
|
147
|
-
.value=${keyDraft}
|
|
172
|
+
.value=${live(keyDraft)}
|
|
148
173
|
@input=${(e: Event) => {
|
|
149
174
|
keyDraft = (e.target as HTMLInputElement).value;
|
|
150
175
|
}}
|
|
@@ -157,7 +182,7 @@ export function createAiCredentialsForm(opts: AiCredentialsFormOptions): AiCrede
|
|
|
157
182
|
class="ai-creds-field"
|
|
158
183
|
size="s"
|
|
159
184
|
allows-custom-value
|
|
160
|
-
.value=${modelDraft}
|
|
185
|
+
.value=${live(modelDraft)}
|
|
161
186
|
@change=${(e: Event) => {
|
|
162
187
|
modelDraft = (e.target as HTMLInputElement).value;
|
|
163
188
|
}}
|
|
@@ -175,7 +200,7 @@ export function createAiCredentialsForm(opts: AiCredentialsFormOptions): AiCrede
|
|
|
175
200
|
class="ai-creds-field"
|
|
176
201
|
size="s"
|
|
177
202
|
placeholder="Model ID (e.g. gpt-4o, claude-sonnet-4-20250514, etc.)"
|
|
178
|
-
.value=${modelDraft}
|
|
203
|
+
.value=${live(modelDraft)}
|
|
179
204
|
@input=${(e: Event) => {
|
|
180
205
|
modelDraft = (e.target as HTMLInputElement).value;
|
|
181
206
|
}}
|
|
@@ -198,7 +223,7 @@ export function createAiCredentialsForm(opts: AiCredentialsFormOptions): AiCrede
|
|
|
198
223
|
class="ai-creds-field"
|
|
199
224
|
size="s"
|
|
200
225
|
placeholder="Endpoint (optional, e.g. http://localhost:11434/v1)"
|
|
201
|
-
.value=${baseUrlDraft}
|
|
226
|
+
.value=${live(baseUrlDraft)}
|
|
202
227
|
@input=${(e: Event) => {
|
|
203
228
|
baseUrlDraft = (e.target as HTMLInputElement).value;
|
|
204
229
|
}}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/// <reference lib="dom" />
|
|
2
|
+
/**
|
|
3
|
+
* Ai-model-picker.ts — the provider's model catalogue as one `sp-picker`, for every surface that
|
|
4
|
+
* chooses a model.
|
|
5
|
+
*
|
|
6
|
+
* It was private to `panels/ai-chat/composer.ts`, which was correct while the chat composer was the
|
|
7
|
+
* only place a model could be chosen. The New Project **Import** source now chooses one too — and
|
|
8
|
+
* that run spends more tokens than any single chat turn — so the choice needed a second host. A
|
|
9
|
+
* copy would have been two implementations of the invariant below, which is exactly the kind that
|
|
10
|
+
* only breaks in the copy nobody looked at.
|
|
11
|
+
*
|
|
12
|
+
* **The list is never held privately.** It is read from `cachedModels(credentials)` on every
|
|
13
|
+
* render, so a list can only ever be shown for the credentials it was listed under: change the key
|
|
14
|
+
* and the catalogue becomes UNAVAILABLE rather than stale. Holding it in a closure is what once let
|
|
15
|
+
* the picker offer one provider's models while another was configured.
|
|
16
|
+
*
|
|
17
|
+
* A closure factory (precedent: `createComposer`, `createAiCredentialsForm`) so two hosts never
|
|
18
|
+
* share a loading flag or a failed-fetch record.
|
|
19
|
+
*
|
|
20
|
+
* @license MIT
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { html, nothing } from "lit-html";
|
|
24
|
+
import { live } from "lit-html/directives/live.js";
|
|
25
|
+
import {
|
|
26
|
+
aiConnection,
|
|
27
|
+
cachedModels,
|
|
28
|
+
fetchAvailableModels,
|
|
29
|
+
preferredModel,
|
|
30
|
+
} from "../services/ai-models";
|
|
31
|
+
import { setModel } from "../services/ai-settings";
|
|
32
|
+
|
|
33
|
+
import type { TemplateResult } from "lit-html";
|
|
34
|
+
import type { AiCredentials } from "../services/ai-models";
|
|
35
|
+
|
|
36
|
+
/** The `sp-menu-item` value that means "try listing again" rather than "select this model". */
|
|
37
|
+
export const RETRY_MODELS = "__retry_models__";
|
|
38
|
+
|
|
39
|
+
/** The `sp-menu-item` value of the disabled placeholder shown during the first fetch. */
|
|
40
|
+
export const LOADING_MODELS = "__loading__";
|
|
41
|
+
|
|
42
|
+
export interface ModelPickerOptions {
|
|
43
|
+
/** Host re-render scheduler — called whenever a fetch settles. */
|
|
44
|
+
requestRender: () => void;
|
|
45
|
+
/**
|
|
46
|
+
* The model to show as selected. Defaults to the stored/proxy-preferred one, which is what the
|
|
47
|
+
* chat composer wants; the Import form passes its own draft, which is not a stored setting until
|
|
48
|
+
* the run starts.
|
|
49
|
+
*/
|
|
50
|
+
getModel?: () => string;
|
|
51
|
+
/**
|
|
52
|
+
* Where a choice goes. Defaults to `setModel` (the roaming application preference). The Import
|
|
53
|
+
* form passes its own setter, because choosing a model for one import must not silently retarget
|
|
54
|
+
* the assistant.
|
|
55
|
+
*/
|
|
56
|
+
onChange?: (id: string) => void;
|
|
57
|
+
/** Extra class on the `sp-picker`, so a host can size it without touching this module. */
|
|
58
|
+
className?: string;
|
|
59
|
+
/** Picker size; `"s"` (the composer's) by default. */
|
|
60
|
+
size?: "s" | "m";
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface ModelPicker {
|
|
64
|
+
render: () => TemplateResult;
|
|
65
|
+
/** Whether a fetch is in flight — for a host that wants to disable a submit button. */
|
|
66
|
+
isLoading: () => boolean;
|
|
67
|
+
/** The last fetch's failure, or `""`. */
|
|
68
|
+
error: () => string;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Create a model picker bound to a host's render scheduler.
|
|
73
|
+
*
|
|
74
|
+
* @param {ModelPickerOptions} opts
|
|
75
|
+
* @returns {ModelPicker}
|
|
76
|
+
*/
|
|
77
|
+
export function createModelPicker(opts: ModelPickerOptions): ModelPicker {
|
|
78
|
+
const getModel = opts.getModel ?? preferredModel;
|
|
79
|
+
const onChange = opts.onChange ?? setModel;
|
|
80
|
+
|
|
81
|
+
let loading = false;
|
|
82
|
+
let failure = "";
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* The credentials the last fetch was made FOR, so a failure is not retried on every render.
|
|
86
|
+
*
|
|
87
|
+
* The list itself is deliberately not held here — see the module docblock.
|
|
88
|
+
*/
|
|
89
|
+
let attempted: AiCredentials | null = null;
|
|
90
|
+
|
|
91
|
+
function sameConnection(a: AiCredentials | null, b: AiCredentials): boolean {
|
|
92
|
+
return a !== null && a.apiKey === b.apiKey && a.baseUrl === b.baseUrl;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function ensureModels(force = false) {
|
|
96
|
+
const credentials = aiConnection();
|
|
97
|
+
const settled = cachedModels(credentials) !== null || sameConnection(attempted, credentials);
|
|
98
|
+
if (loading || (settled && !force)) {
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
loading = true;
|
|
102
|
+
failure = "";
|
|
103
|
+
attempted = credentials;
|
|
104
|
+
fetchAvailableModels({ credentials, force })
|
|
105
|
+
.catch((error: unknown) => {
|
|
106
|
+
failure = (error as Error).message || "Failed to fetch models";
|
|
107
|
+
})
|
|
108
|
+
.finally(() => {
|
|
109
|
+
loading = false;
|
|
110
|
+
opts.requestRender();
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function onPickerChange(e: Event) {
|
|
115
|
+
const { value } = e.target as HTMLInputElement;
|
|
116
|
+
if (value === RETRY_MODELS) {
|
|
117
|
+
attempted = null;
|
|
118
|
+
ensureModels(true);
|
|
119
|
+
// Re-render so the picker snaps back to the current model instead of showing "Retry".
|
|
120
|
+
opts.requestRender();
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
if (value && value !== LOADING_MODELS) {
|
|
124
|
+
onChange(value);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function render(): TemplateResult {
|
|
129
|
+
ensureModels();
|
|
130
|
+
const current = getModel();
|
|
131
|
+
const listed = cachedModels(aiConnection()) ?? [];
|
|
132
|
+
/* A catalogue that does not contain the current model still has to show it selected — an
|
|
133
|
+
unlisted id is the normal case for a self-hosted or newly released model. */
|
|
134
|
+
const items = listed.some((m) => m.id === current)
|
|
135
|
+
? listed
|
|
136
|
+
: [{ id: current, name: current }, ...listed];
|
|
137
|
+
return html`
|
|
138
|
+
<sp-picker
|
|
139
|
+
size=${opts.size ?? "s"}
|
|
140
|
+
quiet
|
|
141
|
+
class=${opts.className ?? "ai-model-picker"}
|
|
142
|
+
title=${failure ? `Couldn't load models: ${failure}` : "Model"}
|
|
143
|
+
.value=${live(current)}
|
|
144
|
+
@change=${onPickerChange}
|
|
145
|
+
>
|
|
146
|
+
${items.map((m) => html`<sp-menu-item value=${m.id}>${m.name}</sp-menu-item>`)}
|
|
147
|
+
${
|
|
148
|
+
loading
|
|
149
|
+
? html`<sp-menu-item disabled value=${LOADING_MODELS}>Loading models…</sp-menu-item>`
|
|
150
|
+
: nothing
|
|
151
|
+
}
|
|
152
|
+
${
|
|
153
|
+
failure
|
|
154
|
+
? html`<sp-menu-item value=${RETRY_MODELS}>Retry loading models</sp-menu-item>`
|
|
155
|
+
: nothing
|
|
156
|
+
}
|
|
157
|
+
</sp-picker>
|
|
158
|
+
`;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return { error: () => failure, isLoading: () => loading, render };
|
|
162
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `choiceField` — one labelled choice, rendered the way every Spectrum picker in Studio is.
|
|
3
|
+
*
|
|
4
|
+
* It exists so the New File dialog's format picker and the Convert dialog's target picker are the
|
|
5
|
+
* SAME control rather than two that drift: same divider idiom for a sentinel row, same `live()`
|
|
6
|
+
* guard, same "one kind is not a choice" collapse to static text.
|
|
7
|
+
*
|
|
8
|
+
* **It imports lit only.** Both callers live in modules the other imports — `ui/layers.ts` is
|
|
9
|
+
* imported by `format/convert-file.ts`, which also wants this fragment — so a shared control that
|
|
10
|
+
* reached into either would be a cycle, and `import/no-cycle` is an error here.
|
|
11
|
+
*
|
|
12
|
+
* `.value=${live(...)}` is not stylistic. `sp-picker` writes its own `value` when the reader picks
|
|
13
|
+
* a row, so lit's dirty check sees the value it last committed and skips the re-commit that would
|
|
14
|
+
* put the control back where the model says it is. `scripts/check-lit-conventions.ts` fails on the
|
|
15
|
+
* unguarded form.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { html, nothing } from "lit-html";
|
|
19
|
+
import { live } from "lit-html/directives/live.js";
|
|
20
|
+
import type { TemplateResult } from "lit-html";
|
|
21
|
+
|
|
22
|
+
/** One row of a {@link choiceField}. */
|
|
23
|
+
export interface ChoiceOption {
|
|
24
|
+
value: string;
|
|
25
|
+
label: string;
|
|
26
|
+
/** Draw an `sp-menu-divider` immediately before this row — how a sentinel row is set apart. */
|
|
27
|
+
dividerBefore?: boolean;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** What a {@link choiceField} renders and reports. */
|
|
31
|
+
export interface ChoiceSpec {
|
|
32
|
+
label: string;
|
|
33
|
+
options: readonly ChoiceOption[];
|
|
34
|
+
value: string;
|
|
35
|
+
onChange: (next: string) => void;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* A labelled picker, or static text when there is nothing to choose.
|
|
40
|
+
*
|
|
41
|
+
* The collapse is the rule `panels/pane-context.ts` already states about editor kinds: **one kind
|
|
42
|
+
* is not a choice**. A picker whose menu holds a single row is a control that cannot do anything,
|
|
43
|
+
* and drawing it invites the reader to open it and find out.
|
|
44
|
+
*
|
|
45
|
+
* @param spec - The label, the rows, the current value and the change sink.
|
|
46
|
+
* @returns The fragment, for a caller's own template.
|
|
47
|
+
*/
|
|
48
|
+
export function choiceField(spec: ChoiceSpec): TemplateResult {
|
|
49
|
+
const current = spec.options.find((option) => option.value === spec.value);
|
|
50
|
+
if (spec.options.length < 2) {
|
|
51
|
+
return html`<div class="choice-row">
|
|
52
|
+
<sp-field-label size="s">${spec.label}</sp-field-label>
|
|
53
|
+
<span class="choice-static">${current?.label ?? spec.value}</span>
|
|
54
|
+
</div>`;
|
|
55
|
+
}
|
|
56
|
+
return html`<div class="choice-row">
|
|
57
|
+
<sp-field-label size="s">${spec.label}</sp-field-label>
|
|
58
|
+
<sp-picker
|
|
59
|
+
size="s"
|
|
60
|
+
label=${spec.label}
|
|
61
|
+
.value=${live(spec.value)}
|
|
62
|
+
@change=${(e: Event) => spec.onChange((e.target as HTMLInputElement).value)}
|
|
63
|
+
>
|
|
64
|
+
${spec.options.map(
|
|
65
|
+
(option) =>
|
|
66
|
+
html`${
|
|
67
|
+
option.dividerBefore === true ? html`<sp-menu-divider></sp-menu-divider>` : nothing
|
|
68
|
+
}<sp-menu-item value=${option.value}>${option.label}</sp-menu-item>`,
|
|
69
|
+
)}
|
|
70
|
+
</sp-picker>
|
|
71
|
+
</div>`;
|
|
72
|
+
}
|
package/src/ui/layers.ts
CHANGED
|
@@ -15,10 +15,12 @@
|
|
|
15
15
|
import { html, render as litRender, nothing } from "lit-html";
|
|
16
16
|
import { ref } from "lit-html/directives/ref.js";
|
|
17
17
|
import { repeat } from "lit-html/directives/repeat.js";
|
|
18
|
+
import { choiceField } from "./choice-field";
|
|
18
19
|
import { overlayRegion, REGION_ATTR } from "./regions";
|
|
19
20
|
import { dismiss, toasts } from "../services/notify";
|
|
20
21
|
import { activeRegistry } from "../commands/active-registry";
|
|
21
22
|
import { effect, effectScope } from "../reactivity";
|
|
23
|
+
import type { ChoiceOption } from "./choice-field";
|
|
22
24
|
import type { Notification, Severity } from "../services/notify";
|
|
23
25
|
import type { EffectScope } from "@vue/reactivity";
|
|
24
26
|
import type { TemplateResult } from "lit-html";
|
|
@@ -338,6 +340,31 @@ export function showSaveDiscardDialog(
|
|
|
338
340
|
);
|
|
339
341
|
}
|
|
340
342
|
|
|
343
|
+
/**
|
|
344
|
+
* A picker rendered above the field, whose selection the dialog OWNS.
|
|
345
|
+
*
|
|
346
|
+
* Owned, not merely displayed: the choice reaches `validate` as its second argument, and picking a
|
|
347
|
+
* row re-runs it. That is the whole reason this lives here rather than being smuggled in through
|
|
348
|
+
* `message` — `check` and `rerender` are private to {@link showPromptDialog}, so a caller-built
|
|
349
|
+
* control could change the selection but could never make the field's refusal catch up with it. The
|
|
350
|
+
* New File dialog's format picker is exactly that case: switching from Markdown to JSON changes
|
|
351
|
+
* whether the typed name is already taken, with no keystroke to notice.
|
|
352
|
+
*/
|
|
353
|
+
export interface PromptChoice {
|
|
354
|
+
/** Label above the picker. */
|
|
355
|
+
label: string;
|
|
356
|
+
/**
|
|
357
|
+
* The rows, re-read on EVERY render rather than captured once. A snapshot taken when the options
|
|
358
|
+
* object is built freezes a list the project can still be loading — the same defect
|
|
359
|
+
* `content/entry-commands.ts`'s `derivedEnumProperty` exists to prevent one layer up.
|
|
360
|
+
*/
|
|
361
|
+
options: () => readonly ChoiceOption[];
|
|
362
|
+
/** Which row starts selected. */
|
|
363
|
+
initial: string;
|
|
364
|
+
/** Told what was picked, for a caller keeping its own derived state. */
|
|
365
|
+
onChange?: (next: string) => void;
|
|
366
|
+
}
|
|
367
|
+
|
|
341
368
|
/** Options accepted by {@link showPromptDialog}. */
|
|
342
369
|
export interface PromptDialogOptions {
|
|
343
370
|
/** Label on the confirming button. */
|
|
@@ -346,17 +373,21 @@ export interface PromptDialogOptions {
|
|
|
346
373
|
cancelLabel?: string;
|
|
347
374
|
/** Explanatory copy rendered above the text field. */
|
|
348
375
|
message?: string | TemplateResult;
|
|
349
|
-
/** Placeholder shown while the field is empty. */
|
|
350
|
-
placeholder?: string;
|
|
376
|
+
/** Placeholder shown while the field is empty. Read on every render, so it may follow the choice. */
|
|
377
|
+
placeholder?: string | (() => string);
|
|
351
378
|
/** How much of the pre-filled value to select once the field takes focus. */
|
|
352
379
|
select?: "all" | "stem" | "none";
|
|
353
380
|
/**
|
|
354
381
|
* Validate the raw field value. Return an empty string when valid, or a message to show as
|
|
355
382
|
* negative help text (which also blocks confirmation). Defaults to "must not be blank".
|
|
383
|
+
*
|
|
384
|
+
* `chosen` is the {@link PromptChoice} selection, or `""` when the dialog offers no choice.
|
|
356
385
|
*/
|
|
357
|
-
validate?: (value: string) => string;
|
|
386
|
+
validate?: (value: string, chosen: string) => string;
|
|
358
387
|
/** Pre-filled value. */
|
|
359
388
|
value?: string;
|
|
389
|
+
/** A picker above the field whose selection this dialog owns. */
|
|
390
|
+
choice?: PromptChoice;
|
|
360
391
|
}
|
|
361
392
|
|
|
362
393
|
/**
|
|
@@ -375,6 +406,7 @@ export function showPromptDialog(
|
|
|
375
406
|
): Promise<string | null> {
|
|
376
407
|
const {
|
|
377
408
|
cancelLabel = "Cancel",
|
|
409
|
+
choice,
|
|
378
410
|
confirmLabel = "OK",
|
|
379
411
|
message,
|
|
380
412
|
placeholder = "",
|
|
@@ -384,12 +416,22 @@ export function showPromptDialog(
|
|
|
384
416
|
} = opts;
|
|
385
417
|
|
|
386
418
|
const check = (candidate: string) =>
|
|
387
|
-
validate ? validate(candidate) : candidate.trim() ? "" : "Enter a value.";
|
|
419
|
+
validate ? validate(candidate, chosen) : candidate.trim() ? "" : "Enter a value.";
|
|
388
420
|
|
|
389
421
|
let value = initialValue;
|
|
422
|
+
let chosen = choice?.initial ?? "";
|
|
390
423
|
let error = "";
|
|
391
424
|
let wrapperEl: HTMLElement | null = null;
|
|
392
425
|
let focusRequested = false;
|
|
426
|
+
/*
|
|
427
|
+
* Whether the reader has typed yet.
|
|
428
|
+
*
|
|
429
|
+
* A dialog opens with a valid prefill and NO error, and picking a format must not be the thing
|
|
430
|
+
* that first paints one under a field nobody has touched — "untitled" is not yet a mistake. So a
|
|
431
|
+
* pick refreshes an error that is already showing and never mints the first one; the confirm
|
|
432
|
+
* still refuses, because `confirm()` runs `check` unconditionally.
|
|
433
|
+
*/
|
|
434
|
+
let touched = false;
|
|
393
435
|
|
|
394
436
|
return showDialog<string | null>((done) => {
|
|
395
437
|
function rerender() {
|
|
@@ -411,6 +453,7 @@ export function showPromptDialog(
|
|
|
411
453
|
}
|
|
412
454
|
|
|
413
455
|
function onInput(e: Event) {
|
|
456
|
+
touched = true;
|
|
414
457
|
value = (e.target as HTMLInputElement).value || "";
|
|
415
458
|
const next = check(value);
|
|
416
459
|
if (next !== error) {
|
|
@@ -419,6 +462,23 @@ export function showPromptDialog(
|
|
|
419
462
|
}
|
|
420
463
|
}
|
|
421
464
|
|
|
465
|
+
/**
|
|
466
|
+
* Take a pick, and re-render UNCONDITIONALLY.
|
|
467
|
+
*
|
|
468
|
+
* `onInput` re-renders only when the error string changed, which is right for a keystroke: the
|
|
469
|
+
* template it would rebuild is identical. A pick is not that. It can change the placeholder,
|
|
470
|
+
* the picker's own selected row, and — for the New File dialog — whether the composed filename
|
|
471
|
+
* is already taken, all with the error text unchanged. Comparing error strings here would leave
|
|
472
|
+
* a dialog showing "about.md already exists" after the reader switched the format to JSON.
|
|
473
|
+
*/
|
|
474
|
+
function onPick(next: string) {
|
|
475
|
+
chosen = next;
|
|
476
|
+
choice?.onChange?.(next);
|
|
477
|
+
const candidate = check(value);
|
|
478
|
+
error = touched || error ? candidate : "";
|
|
479
|
+
rerender();
|
|
480
|
+
}
|
|
481
|
+
|
|
422
482
|
function onKeydown(e: KeyboardEvent) {
|
|
423
483
|
if (e.key === "Enter") {
|
|
424
484
|
confirm();
|
|
@@ -472,9 +532,27 @@ export function showPromptDialog(
|
|
|
472
532
|
// Spectrum resets <p> margins to 0, so without this the copy sits flush on the field.
|
|
473
533
|
message ? html`<p style="margin:0 0 8px">${message}</p>` : nothing
|
|
474
534
|
}
|
|
535
|
+
${
|
|
536
|
+
/*
|
|
537
|
+
* Above the field, and in ONE template position in every mode.
|
|
538
|
+
*
|
|
539
|
+
* The `sp-textfield` below must never move or be rebuilt by a conditional branch: lit
|
|
540
|
+
* would commit a new element, `onFieldRef` would fire again, the `focusRequested` latch
|
|
541
|
+
* would refuse to re-focus it, and the caret would be stranded mid-name. Only the
|
|
542
|
+
* placeholder, the validation rules and the help text vary between modes.
|
|
543
|
+
*/
|
|
544
|
+
choice
|
|
545
|
+
? choiceField({
|
|
546
|
+
label: choice.label,
|
|
547
|
+
onChange: onPick,
|
|
548
|
+
options: choice.options(),
|
|
549
|
+
value: chosen,
|
|
550
|
+
})
|
|
551
|
+
: nothing
|
|
552
|
+
}
|
|
475
553
|
<sp-textfield
|
|
476
554
|
style="width:100%"
|
|
477
|
-
placeholder=${placeholder}
|
|
555
|
+
placeholder=${typeof placeholder === "function" ? placeholder() : placeholder}
|
|
478
556
|
value=${value}
|
|
479
557
|
?invalid=${Boolean(error)}
|
|
480
558
|
@input=${onInput}
|
package/src/ui/media-picker.ts
CHANGED
|
@@ -26,12 +26,11 @@ import { debouncedStyleCommit, renderOnly } from "../store";
|
|
|
26
26
|
import { getLayerSlot, popoverLayerFor } from "./layers";
|
|
27
27
|
import type { LayerKind } from "./layers";
|
|
28
28
|
import { rectOf } from "../utils/geometry";
|
|
29
|
-
import {
|
|
30
|
-
import { previewAssetSrc } from "../canvas/content-assets";
|
|
29
|
+
import { previewAssetSrc } from "../canvas/asset-refs";
|
|
31
30
|
import {
|
|
32
31
|
IMAGE_EXTENSIONS,
|
|
33
32
|
MEDIA_EXTENSIONS,
|
|
34
|
-
|
|
33
|
+
uploadAccept,
|
|
35
34
|
extensionOf,
|
|
36
35
|
uploadAssets,
|
|
37
36
|
} from "../files/media-upload";
|
|
@@ -42,7 +41,7 @@ import {
|
|
|
42
41
|
recordImageSize,
|
|
43
42
|
seedMediaMeta,
|
|
44
43
|
} from "../files/media-meta";
|
|
45
|
-
import { mediaSiteUrl } from "../files/media-paths";
|
|
44
|
+
import { mediaSiteUrl, previewFileSrc } from "../files/media-paths";
|
|
46
45
|
|
|
47
46
|
// ─── Media file cache ────────────────────────────────────────────────────────
|
|
48
47
|
|
|
@@ -283,7 +282,7 @@ function renderMediaPickerPopover() {
|
|
|
283
282
|
m.isImage
|
|
284
283
|
? html`<img
|
|
285
284
|
slot="icon"
|
|
286
|
-
src=${
|
|
285
|
+
src=${previewFileSrc(m.file)}
|
|
287
286
|
alt=""
|
|
288
287
|
style="width:24px;height:24px;object-fit:cover;border-radius:var(--spectrum-corner-radius-75, 2px)"
|
|
289
288
|
@load=${(e: Event) => noteImageSize(m.file, e.target)}
|
|
@@ -394,7 +393,7 @@ function pickAndUpload(onCommit: (val: string) => void) {
|
|
|
394
393
|
const input = document.createElement("input");
|
|
395
394
|
input.type = "file";
|
|
396
395
|
input.multiple = true;
|
|
397
|
-
input.accept =
|
|
396
|
+
input.accept = uploadAccept();
|
|
398
397
|
input.addEventListener("change", () => {
|
|
399
398
|
if (input.files?.length) {
|
|
400
399
|
void uploadAndAssign(input.files, onCommit);
|
|
@@ -422,11 +421,7 @@ export function renderMediaPicker(prop: string, value: string, onCommit: (val: s
|
|
|
422
421
|
<div class="media-picker">
|
|
423
422
|
${
|
|
424
423
|
isImage && currentValue
|
|
425
|
-
? html`<img
|
|
426
|
-
class="media-picker-thumb"
|
|
427
|
-
src=${loopbackAssetSrc(previewAssetSrc(currentValue))}
|
|
428
|
-
alt=""
|
|
429
|
-
/>`
|
|
424
|
+
? html`<img class="media-picker-thumb" src=${previewAssetSrc(currentValue)} alt="" />`
|
|
430
425
|
: nothing
|
|
431
426
|
}
|
|
432
427
|
<sp-textfield
|