@jxsuite/studio 0.34.0 → 0.35.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 +9 -1
- package/dist/iframe-entry.js.map +3 -3
- package/dist/studio.js +21771 -7715
- package/dist/studio.js.map +258 -24
- package/package.json +5 -5
- package/src/files/files.ts +3 -0
- package/src/new-project/design-fields.ts +260 -0
- package/src/new-project/import-tab.ts +322 -0
- package/src/new-project/new-project-modal.ts +472 -89
- package/src/new-project/templates.ts +61 -0
- package/src/packages/ensure-deps.ts +6 -0
- package/src/packages/pull-package-sync.ts +339 -0
- package/src/panels/ai-chat/attached-context.ts +49 -0
- package/src/panels/ai-chat/chat-markdown.ts +38 -0
- package/src/panels/ai-chat/chat-view.ts +250 -0
- package/src/panels/ai-chat/composer.ts +315 -0
- package/src/panels/ai-chat/sessions-view.ts +98 -0
- package/src/panels/ai-panel.ts +189 -455
- package/src/panels/drag-ghost.ts +1 -1
- package/src/panels/git-panel.ts +16 -1
- package/src/panels/right-panel.ts +21 -5
- package/src/platforms/devserver.ts +30 -1
- package/src/services/agent-seed.ts +91 -0
- package/src/services/ai-models.ts +64 -0
- package/src/services/ai-session-store.ts +250 -0
- package/src/services/ai-settings.ts +5 -0
- package/src/services/automation.ts +140 -0
- package/src/services/document-assistant.ts +98 -38
- package/src/services/import-client.ts +134 -0
- package/src/services/settings-store.ts +93 -0
- package/src/studio.ts +25 -0
- package/src/types.ts +84 -0
- package/src/ui/ai-credentials-form.ts +205 -0
- package/src/ui/spectrum.ts +8 -0
package/src/types.ts
CHANGED
|
@@ -123,6 +123,47 @@ export interface RenameResult {
|
|
|
123
123
|
error?: string;
|
|
124
124
|
}
|
|
125
125
|
|
|
126
|
+
/** A starter template surfaced in the New Project picker (mirrors @jxsuite/starters StarterMeta). */
|
|
127
|
+
export interface StarterInfo {
|
|
128
|
+
id: string;
|
|
129
|
+
name: string;
|
|
130
|
+
industry: string;
|
|
131
|
+
tagline: string;
|
|
132
|
+
description: string;
|
|
133
|
+
features: string[];
|
|
134
|
+
accent: string;
|
|
135
|
+
/** Preview image as a self-contained `data:` URI. */
|
|
136
|
+
thumbnail: string;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** A progress line from the AI-guided site import (mirrors @jxsuite/import ImportProgressEvent). */
|
|
140
|
+
export interface ImportProgressEvent {
|
|
141
|
+
phase: string;
|
|
142
|
+
message: string;
|
|
143
|
+
current?: number;
|
|
144
|
+
total?: number;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** Options for {@link StudioPlatform.importSite}. */
|
|
148
|
+
export interface ImportSiteOptions {
|
|
149
|
+
/** The live site to clone; must be http(s). */
|
|
150
|
+
url: string;
|
|
151
|
+
/** Display name for the new project. */
|
|
152
|
+
name: string;
|
|
153
|
+
/** Destination directory (platform-interpreted: project-relative on the dev server). */
|
|
154
|
+
directory: string;
|
|
155
|
+
/** Max crawl depth; 0 = single page. */
|
|
156
|
+
depth: number;
|
|
157
|
+
/** Max pages to capture. */
|
|
158
|
+
maxPages: number;
|
|
159
|
+
/** Refine component/prop names with the LLM (requires a key). */
|
|
160
|
+
aiComponents: boolean;
|
|
161
|
+
/** OpenAI-compatible credentials, from the user's AI settings. */
|
|
162
|
+
apiKey?: string;
|
|
163
|
+
baseUrl?: string;
|
|
164
|
+
model?: string;
|
|
165
|
+
}
|
|
166
|
+
|
|
126
167
|
export interface StudioPlatform {
|
|
127
168
|
id: string;
|
|
128
169
|
projectRoot: string;
|
|
@@ -227,7 +268,41 @@ export interface StudioPlatform {
|
|
|
227
268
|
url?: string;
|
|
228
269
|
adapter?: string;
|
|
229
270
|
directory: string;
|
|
271
|
+
/** Id of a starter template to clone, or "blank"/undefined for the minimal template. */
|
|
272
|
+
starter?: string;
|
|
273
|
+
/** Id of a built-in template variant (from `@jxsuite/create/templates`); undefined = blank. */
|
|
274
|
+
template?: string;
|
|
275
|
+
/** Design quickstart (colors, fonts, logo, breakpoints) applied on top of the scaffold. */
|
|
276
|
+
design?: {
|
|
277
|
+
accent?: string;
|
|
278
|
+
background?: string;
|
|
279
|
+
text?: string;
|
|
280
|
+
bodyFont?: string;
|
|
281
|
+
headingFont?: string;
|
|
282
|
+
media?: Record<string, string>;
|
|
283
|
+
logo?: { name: string; base64: string };
|
|
284
|
+
};
|
|
230
285
|
}) => Promise<{ root: string; config: ProjectConfig }>;
|
|
286
|
+
/**
|
|
287
|
+
* List the starter templates available in the New Project picker. Absent on platforms that don't
|
|
288
|
+
* ship starters (the picker then offers only a blank project).
|
|
289
|
+
*/
|
|
290
|
+
listStarters?: () => Promise<StarterInfo[]>;
|
|
291
|
+
/**
|
|
292
|
+
* AI-guided import of an existing site into a new project. Runs in the backend (headless Chrome +
|
|
293
|
+
* fs), streaming progress until the project is written. Absent on platforms without a backend
|
|
294
|
+
* import pipeline — the New Project modal hides its Import tab then.
|
|
295
|
+
*/
|
|
296
|
+
importSite?: (
|
|
297
|
+
opts: ImportSiteOptions,
|
|
298
|
+
onProgress: (evt: ImportProgressEvent) => void,
|
|
299
|
+
signal?: AbortSignal,
|
|
300
|
+
) => Promise<{ root: string; config: ProjectConfig }>;
|
|
301
|
+
/**
|
|
302
|
+
* Open a native directory picker and return the chosen absolute path (null when cancelled).
|
|
303
|
+
* Desktop only; the dev server interprets directories relative to its root instead.
|
|
304
|
+
*/
|
|
305
|
+
pickDirectory?: () => Promise<string | null>;
|
|
231
306
|
/** Stack B AI assistant: URL of the OpenAI-compatible SSE chat proxy (`/__studio/ai/chat`). */
|
|
232
307
|
aiChatUrl: () => string | Promise<string>;
|
|
233
308
|
// ─── Multi-window (desktop only; undefined on dev-server) ───────────────────
|
|
@@ -251,6 +326,15 @@ export interface StudioPlatform {
|
|
|
251
326
|
getRecentProjects?: () => Promise<RecentProjectEntry[]>;
|
|
252
327
|
/** Persist the full recent-projects list to the backend store. */
|
|
253
328
|
saveRecentProjects?: (projects: RecentProjectEntry[]) => Promise<void>;
|
|
329
|
+
// ─── User settings (backend-persisted; undefined on dev-server) ─────────────
|
|
330
|
+
/**
|
|
331
|
+
* Read the user-level settings map (e.g. the AI connection parameters) from a backend store
|
|
332
|
+
* shared across all projects/windows. Platforms without a native backend (dev server) omit it and
|
|
333
|
+
* settings live in localStorage only.
|
|
334
|
+
*/
|
|
335
|
+
getSettings?: () => Promise<Record<string, string>>;
|
|
336
|
+
/** Persist the full user-level settings map to the backend store. */
|
|
337
|
+
saveSettings?: (settings: Record<string, string>) => Promise<void>;
|
|
254
338
|
}
|
|
255
339
|
|
|
256
340
|
/** A recently-opened project, keyed by its re-openable `root` (platform-specific). */
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
/// <reference lib="dom" />
|
|
2
|
+
/**
|
|
3
|
+
* Ai-credentials-form.ts — reusable AI provider credentials form (key / model / endpoint).
|
|
4
|
+
*
|
|
5
|
+
* Extracted from the ai-panel key gate so other hosts (e.g. the New Project modal) can embed the
|
|
6
|
+
* same form. All draft/model-list state lives inside the closure, so multiple instances never
|
|
7
|
+
* share state. Persists via src/services/ai-settings.ts on Save.
|
|
8
|
+
*
|
|
9
|
+
* @license MIT
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { html, nothing } from "lit-html";
|
|
13
|
+
import type { TemplateResult } from "lit-html";
|
|
14
|
+
import { fetchAvailableModels, invalidateModelCache } from "../services/ai-models";
|
|
15
|
+
import {
|
|
16
|
+
getBaseUrl,
|
|
17
|
+
getModel,
|
|
18
|
+
getOpenAiKey,
|
|
19
|
+
hasOpenAiKey,
|
|
20
|
+
setBaseUrl,
|
|
21
|
+
setModel,
|
|
22
|
+
setOpenAiKey,
|
|
23
|
+
} from "../services/ai-settings";
|
|
24
|
+
|
|
25
|
+
export interface AiCredentialsFormOptions {
|
|
26
|
+
/** Host re-render scheduler — called whenever the form's internal state changes. */
|
|
27
|
+
requestRender: () => void;
|
|
28
|
+
/** Called after Save persists the credentials. */
|
|
29
|
+
onSaved?: () => void;
|
|
30
|
+
/** Called when Cancel dismisses the form (Cancel is only offered when a key already exists). */
|
|
31
|
+
onCancel?: () => void;
|
|
32
|
+
/** Optional context line (TemplateResult | string) replacing the default blurb. */
|
|
33
|
+
intro?: unknown;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface AiCredentialsForm {
|
|
37
|
+
render: () => TemplateResult;
|
|
38
|
+
/** Preload drafts from the stored ai-settings and auto-fetch the model list. */
|
|
39
|
+
startEdit: () => void;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Create an AI credentials form instance bound to a host's render scheduler.
|
|
44
|
+
*
|
|
45
|
+
* @param {AiCredentialsFormOptions} opts
|
|
46
|
+
* @returns {AiCredentialsForm}
|
|
47
|
+
*/
|
|
48
|
+
export function createAiCredentialsForm(opts: AiCredentialsFormOptions): AiCredentialsForm {
|
|
49
|
+
let keyDraft = "";
|
|
50
|
+
let baseUrlDraft = "";
|
|
51
|
+
let modelDraft = "";
|
|
52
|
+
|
|
53
|
+
/** Fetched from the proxy's /models endpoint (sibling of the chat endpoint). */
|
|
54
|
+
let availableModels: { id: string; name: string }[] = [];
|
|
55
|
+
let modelsLoading = false;
|
|
56
|
+
let modelsError = "";
|
|
57
|
+
|
|
58
|
+
/** Open the form pre-filled with the current settings, and load the model list. */
|
|
59
|
+
function startEdit() {
|
|
60
|
+
keyDraft = getOpenAiKey();
|
|
61
|
+
baseUrlDraft = getBaseUrl();
|
|
62
|
+
modelDraft = getModel();
|
|
63
|
+
opts.requestRender();
|
|
64
|
+
// Auto-fetch available models if not already loaded.
|
|
65
|
+
if (availableModels.length === 0 && !modelsLoading) {
|
|
66
|
+
void fetchModels();
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Persist the drafted key + endpoint + model and notify the host. */
|
|
71
|
+
function save() {
|
|
72
|
+
setOpenAiKey(keyDraft);
|
|
73
|
+
setBaseUrl(baseUrlDraft);
|
|
74
|
+
setModel(modelDraft);
|
|
75
|
+
keyDraft = "";
|
|
76
|
+
baseUrlDraft = "";
|
|
77
|
+
modelDraft = "";
|
|
78
|
+
// Clear fetched models so they're re-fetched with the new credentials next time.
|
|
79
|
+
availableModels = [];
|
|
80
|
+
invalidateModelCache();
|
|
81
|
+
opts.onSaved?.();
|
|
82
|
+
opts.requestRender();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Dismiss the form without saving (only offered when a key already exists). */
|
|
86
|
+
function cancel() {
|
|
87
|
+
keyDraft = "";
|
|
88
|
+
baseUrlDraft = "";
|
|
89
|
+
modelDraft = "";
|
|
90
|
+
opts.onCancel?.();
|
|
91
|
+
opts.requestRender();
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Fetch available models via src/services/ai-models.ts, preferring the in-form drafts over the
|
|
96
|
+
* stored settings so the list reflects the credentials being edited. Always forces past the
|
|
97
|
+
* module cache — this runs on explicit user action (or first open) with possibly-new drafts.
|
|
98
|
+
*/
|
|
99
|
+
async function fetchModels() {
|
|
100
|
+
modelsLoading = true;
|
|
101
|
+
modelsError = "";
|
|
102
|
+
opts.requestRender();
|
|
103
|
+
try {
|
|
104
|
+
availableModels = await fetchAvailableModels({
|
|
105
|
+
apiKey: getOpenAiKey() || keyDraft,
|
|
106
|
+
baseUrl: baseUrlDraft || getBaseUrl(),
|
|
107
|
+
force: true,
|
|
108
|
+
});
|
|
109
|
+
} catch (error: unknown) {
|
|
110
|
+
modelsError = (error as Error).message || "Failed to fetch models";
|
|
111
|
+
} finally {
|
|
112
|
+
modelsLoading = false;
|
|
113
|
+
opts.requestRender();
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** The key + model + endpoint form column. */
|
|
118
|
+
function render(): TemplateResult {
|
|
119
|
+
const haveKey = hasOpenAiKey();
|
|
120
|
+
return html`
|
|
121
|
+
<div
|
|
122
|
+
class="ai-creds-form"
|
|
123
|
+
style="display:flex;flex-direction:column;gap:10px;max-width:320px;text-align:left"
|
|
124
|
+
>
|
|
125
|
+
<div style="font-weight:600;align-self:center">AI provider key</div>
|
|
126
|
+
${opts.intro === undefined
|
|
127
|
+
? html`
|
|
128
|
+
<div style="font-size:11px;color:var(--fg-dim)">
|
|
129
|
+
Any OpenAI-compatible key works. Stored locally in this browser; sent only to the
|
|
130
|
+
Studio proxy (never to a third party except your chosen endpoint).
|
|
131
|
+
</div>
|
|
132
|
+
`
|
|
133
|
+
: html`<div style="font-size:11px;color:var(--fg-dim)">${opts.intro}</div>`}
|
|
134
|
+
<input
|
|
135
|
+
type="password"
|
|
136
|
+
style="width:100%;box-sizing:border-box;padding:6px 8px;border-radius:4px;border:1px solid var(--border);background:var(--bg-input);color:var(--fg);font-size:12px"
|
|
137
|
+
placeholder="sk-… or any compatible key"
|
|
138
|
+
.value=${keyDraft}
|
|
139
|
+
@input=${(e: Event) => {
|
|
140
|
+
keyDraft = (e.target as HTMLInputElement).value;
|
|
141
|
+
}}
|
|
142
|
+
/>
|
|
143
|
+
<div style="font-weight:500;font-size:11px;margin-top:4px">Model</div>
|
|
144
|
+
${availableModels.length > 0
|
|
145
|
+
? html`
|
|
146
|
+
<sp-combobox
|
|
147
|
+
size="s"
|
|
148
|
+
allows-custom-value
|
|
149
|
+
.value=${modelDraft}
|
|
150
|
+
@change=${(e: Event) => {
|
|
151
|
+
modelDraft = (e.target as HTMLInputElement).value;
|
|
152
|
+
}}
|
|
153
|
+
@input=${(e: Event) => {
|
|
154
|
+
modelDraft = (e.target as HTMLInputElement).value;
|
|
155
|
+
}}
|
|
156
|
+
>
|
|
157
|
+
${availableModels.map(
|
|
158
|
+
(m) => html`<sp-menu-item value=${m.id}>${m.name}</sp-menu-item>`,
|
|
159
|
+
)}
|
|
160
|
+
</sp-combobox>
|
|
161
|
+
`
|
|
162
|
+
: html`
|
|
163
|
+
<input
|
|
164
|
+
type="text"
|
|
165
|
+
style="width:100%;box-sizing:border-box;padding:6px 8px;border-radius:4px;border:1px solid var(--border);background:var(--bg-input);color:var(--fg);font-size:12px"
|
|
166
|
+
placeholder="Model ID (e.g. gpt-4o, claude-sonnet-4-20250514, etc.)"
|
|
167
|
+
.value=${modelDraft}
|
|
168
|
+
@input=${(e: Event) => {
|
|
169
|
+
modelDraft = (e.target as HTMLInputElement).value;
|
|
170
|
+
}}
|
|
171
|
+
/>
|
|
172
|
+
`}
|
|
173
|
+
<div style="display:flex;gap:8px;align-items:center">
|
|
174
|
+
<sp-button size="s" variant="secondary" ?disabled=${modelsLoading} @click=${fetchModels}>
|
|
175
|
+
${modelsLoading
|
|
176
|
+
? "Fetching…"
|
|
177
|
+
: availableModels.length > 0
|
|
178
|
+
? "Refresh models"
|
|
179
|
+
: "Fetch models"}
|
|
180
|
+
</sp-button>
|
|
181
|
+
${modelsError
|
|
182
|
+
? html`<span style="font-size:10px;color:var(--danger)">${modelsError}</span>`
|
|
183
|
+
: nothing}
|
|
184
|
+
</div>
|
|
185
|
+
<input
|
|
186
|
+
type="text"
|
|
187
|
+
style="width:100%;box-sizing:border-box;padding:6px 8px;border-radius:4px;border:1px solid var(--border);background:var(--bg-input);color:var(--fg);font-size:12px"
|
|
188
|
+
placeholder="Endpoint (optional, e.g. http://localhost:11434/v1)"
|
|
189
|
+
.value=${baseUrlDraft}
|
|
190
|
+
@input=${(e: Event) => {
|
|
191
|
+
baseUrlDraft = (e.target as HTMLInputElement).value;
|
|
192
|
+
}}
|
|
193
|
+
/>
|
|
194
|
+
<div style="display:flex;gap:8px;align-self:flex-end">
|
|
195
|
+
${haveKey
|
|
196
|
+
? html`<sp-button size="s" variant="secondary" @click=${cancel}>Cancel</sp-button>`
|
|
197
|
+
: nothing}
|
|
198
|
+
<sp-button size="s" variant="primary" @click=${save}>Save</sp-button>
|
|
199
|
+
</div>
|
|
200
|
+
</div>
|
|
201
|
+
`;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return { render, startEdit };
|
|
205
|
+
}
|
package/src/ui/spectrum.ts
CHANGED
|
@@ -127,6 +127,10 @@ import { IconVisibility } from "@spectrum-web-components/icons-workflow/src/elem
|
|
|
127
127
|
import { IconVisibilityOff } from "@spectrum-web-components/icons-workflow/src/elements/IconVisibilityOff.js";
|
|
128
128
|
import { IconArtboard } from "@spectrum-web-components/icons-workflow/src/elements/IconArtboard.js";
|
|
129
129
|
import { IconChat } from "@spectrum-web-components/icons-workflow/src/elements/IconChat.js";
|
|
130
|
+
import { IconSend } from "@spectrum-web-components/icons-workflow/src/elements/IconSend.js";
|
|
131
|
+
import { IconStop } from "@spectrum-web-components/icons-workflow/src/elements/IconStop.js";
|
|
132
|
+
import { IconHistory } from "@spectrum-web-components/icons-workflow/src/elements/IconHistory.js";
|
|
133
|
+
import { IconAttach } from "@spectrum-web-components/icons-workflow/src/elements/IconAttach.js";
|
|
130
134
|
import { IconViewList } from "@spectrum-web-components/icons-workflow/src/elements/IconViewList.js";
|
|
131
135
|
import { IconRailRightClose } from "@spectrum-web-components/icons-workflow/src/elements/IconRailRightClose.js";
|
|
132
136
|
import { IconRailRightOpen } from "@spectrum-web-components/icons-workflow/src/elements/IconRailRightOpen.js";
|
|
@@ -272,6 +276,10 @@ const components = [
|
|
|
272
276
|
["sp-icon-visibility-off", IconVisibilityOff],
|
|
273
277
|
["sp-icon-artboard", IconArtboard],
|
|
274
278
|
["sp-icon-chat", IconChat],
|
|
279
|
+
["sp-icon-send", IconSend],
|
|
280
|
+
["sp-icon-stop", IconStop],
|
|
281
|
+
["sp-icon-history", IconHistory],
|
|
282
|
+
["sp-icon-attach", IconAttach],
|
|
275
283
|
["sp-icon-view-list", IconViewList],
|
|
276
284
|
["sp-icon-text-bold", IconTextBold],
|
|
277
285
|
["sp-icon-text-italic", IconTextItalic],
|