@elytracms/studio 0.0.45 → 0.0.46
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/convex/cms.ts +1 -0
- package/convex/schema.ts +3 -0
- package/dist/components/ai/ai-chat-panel-lazy.d.ts +1 -0
- package/dist/components/ai/ai-chat-panel-lazy.js +21 -0
- package/dist/components/ai/ai-chat-panel-lazy.js.map +1 -0
- package/dist/components/ai/ai-chat-panel.d.ts +10 -0
- package/dist/components/ai/ai-chat-panel.js +202 -0
- package/dist/components/ai/ai-chat-panel.js.map +1 -0
- package/dist/components/ai/ai-cms-panel.d.ts +9 -0
- package/dist/components/ai/ai-cms-panel.js +24 -0
- package/dist/components/ai/ai-cms-panel.js.map +1 -0
- package/dist/components/ai/ai-panel-toggle.d.ts +8 -0
- package/dist/components/ai/ai-panel-toggle.js +27 -0
- package/dist/components/ai/ai-panel-toggle.js.map +1 -0
- package/dist/components/ai/chat-ui.d.ts +36 -0
- package/dist/components/ai/chat-ui.js +68 -0
- package/dist/components/ai/chat-ui.js.map +1 -0
- package/dist/components/ai/proposal-card.d.ts +15 -0
- package/dist/components/ai/proposal-card.js +42 -0
- package/dist/components/ai/proposal-card.js.map +1 -0
- package/dist/components/app-views/default-modules.js +2 -1
- package/dist/components/app-views/default-modules.js.map +1 -1
- package/dist/components/cms/document-diff-view.d.ts +41 -0
- package/dist/components/cms/document-diff-view.js +299 -0
- package/dist/components/cms/document-diff-view.js.map +1 -0
- package/dist/components/cms/version-compare.d.ts +7 -13
- package/dist/components/cms/version-compare.js +24 -365
- package/dist/components/cms/version-compare.js.map +1 -1
- package/dist/components/cms/versions-pane.js +1 -1
- package/dist/components/cms/versions-pane.js.map +1 -1
- package/dist/components/studio-shell.js +2 -1
- package/dist/components/studio-shell.js.map +1 -1
- package/dist/lib/ai/chat-state.d.ts +73 -0
- package/dist/lib/ai/chat-state.js +101 -0
- package/dist/lib/ai/chat-state.js.map +1 -0
- package/dist/lib/ai/create-flow.d.ts +116 -0
- package/dist/lib/ai/create-flow.js +194 -0
- package/dist/lib/ai/create-flow.js.map +1 -0
- package/dist/lib/ai/index.d.ts +4 -0
- package/dist/lib/ai/index.js +4 -0
- package/dist/lib/ai/index.js.map +1 -1
- package/dist/lib/ai/route-provider.d.ts +50 -0
- package/dist/lib/ai/route-provider.js +60 -0
- package/dist/lib/ai/route-provider.js.map +1 -0
- package/dist/lib/ai/route.d.ts +12 -0
- package/dist/lib/ai/route.js +42 -3
- package/dist/lib/ai/route.js.map +1 -1
- package/dist/lib/ai/tools.d.ts +65 -0
- package/dist/lib/ai/tools.js +63 -0
- package/dist/lib/ai/tools.js.map +1 -0
- package/dist/lib/ai-panel.d.ts +20 -0
- package/dist/lib/ai-panel.js +48 -0
- package/dist/lib/ai-panel.js.map +1 -0
- package/dist/lib/i18n/messages.en.js +34 -0
- package/dist/lib/i18n/messages.en.js.map +1 -1
- package/dist/lib/persistence/convex-adapter.js +2 -0
- package/dist/lib/persistence/convex-adapter.js.map +1 -1
- package/package.json +2 -2
package/dist/lib/ai/route.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"route.js","sourceRoot":"","sources":["../../../src/lib/ai/route.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Server-route factory (EC-331): a streaming chat/completion endpoint the consuming\n * studio app mounts. Key custody = host route (never the browser, never Convex): the\n * `provider` holds the host-env key and this handler is a THIN model proxy — NO data\n * access, NO service credentials.\n *\n * Framework-agnostic on purpose: it returns a Web `Request → Response` handler the\n * host wires into whatever server entry it has (a TanStack Start server route, a\n * Convex `httpAction`, a plain fetch handler). The current demo studio builds as a\n * pure client SPA with server functions disabled (EC-234), so actually mounting this\n * is an EC-199 integration step; phase 1 ships the handler + its contract.\n *\n * Session-gated: `getSession` (host-wired, reading its own auth — this module never\n * touches `lib/auth`) must return a session for authenticated callers; anonymous\n * callers are rejected. Plus a soft, in-memory per-project rate counter (spend guard).\n */\nimport type {\n AiChatMessage,\n AiRouteHandler,\n AiRouteHandlerOptions,\n AiSession,\n StreamChatRequest,\n} from './types'\n\nfunction jsonResponse(value: unknown, status: number): Response {\n return new Response(JSON.stringify(value), {\n status,\n headers: { 'content-type': 'application/json' },\n })\n}\n\ninterface RateBucket {\n windowStart: number\n count: number\n}\n\nfunction isChatMessage(value: unknown): value is AiChatMessage {\n if (typeof value !== 'object' || value === null) return false\n const m = value as { role?: unknown; content?: unknown }\n return (\n (m.role === 'system' || m.role === 'user' || m.role === 'assistant') &&\n typeof m.content === 'string'\n )\n}\n\n/** Parse a request body into a {@link StreamChatRequest}, or `null` when malformed. */\nfunction parseChatBody(body: unknown): StreamChatRequest | null {\n if (typeof body !== 'object' || body === null) return null\n const { messages, system } = body as { messages?: unknown; system?: unknown }\n if (!Array.isArray(messages) || messages.length === 0 || !messages.every(isChatMessage)) {\n return null\n }\n return {\n messages: messages as AiChatMessage[],\n ...(typeof system === 'string' ? { system } : {}),\n }\n}\n\n/**\n * Build the mounted route handler. The returned function is a plain\n * `(Request) => Promise<Response>` — session-gated, soft-rate-limited, and a thin\n * proxy to `provider.streamChat`. Never throws to the caller: every failure is a\n * structured JSON response with an appropriate status.\n */\nexport function createAiRouteHandler(options: AiRouteHandlerOptions): AiRouteHandler {\n const maxPerMinute = options.limits?.maxRequestsPerMinute\n const buckets = new Map<string, RateBucket>()\n const projectKey = options.projectKey ?? (() => 'global')\n\n function rateLimited(key: string): boolean {\n if (maxPerMinute === undefined || maxPerMinute <= 0) return false\n const now = Date.now()\n const bucket = buckets.get(key)\n if (!bucket || now - bucket.windowStart >= 60_000) {\n buckets.set(key, { windowStart: now, count: 1 })\n return false\n }\n bucket.count += 1\n return bucket.count > maxPerMinute\n }\n\n return async (request: Request): Promise<Response> => {\n // 1. Session gate — reject anonymous callers (the route holds spend, not data).\n let session: AiSession | null\n try {\n session = await options.getSession(request)\n } catch {\n return jsonResponse({ error: 'unauthorized' }, 401)\n }\n if (!session) return jsonResponse({ error: 'unauthorized' }, 401)\n\n // 2. Method.\n if (request.method !== 'POST') return jsonResponse({ error: 'method-not-allowed' }, 405)\n\n // 3. Soft per-project rate / spend counter.\n if (rateLimited(projectKey(request))) return jsonResponse({ error: 'rate-limited' }, 429)\n\n // 4. Body → chat request.\n let body: unknown\n try {\n body = await request.json()\n } catch {\n return jsonResponse({ error: 'invalid-json' }, 400)\n }\n const chat = parseChatBody(body)\n if (!chat) {\n return jsonResponse(\n { error: 'invalid-request', message: 'expected { messages: AiChatMessage[] }' },\n 400,\n )\n }\n\n // 5. Thin proxy to the provider stream.\n if (!options.provider.streamChat) {\n return jsonResponse({ error: 'provider-cannot-stream' }, 501)\n }\n try {\n return await options.provider.streamChat(chat)\n } catch {\n return jsonResponse({ error: 'provider-error' }, 502)\n }\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"route.js","sourceRoot":"","sources":["../../../src/lib/ai/route.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Server-route factory (EC-331): a streaming chat/completion endpoint the consuming\n * studio app mounts. Key custody = host route (never the browser, never Convex): the\n * `provider` holds the host-env key and this handler is a THIN model proxy — NO data\n * access, NO service credentials.\n *\n * Framework-agnostic on purpose: it returns a Web `Request → Response` handler the\n * host wires into whatever server entry it has (a TanStack Start server route, a\n * Convex `httpAction`, a plain fetch handler). The current demo studio builds as a\n * pure client SPA with server functions disabled (EC-234), so actually mounting this\n * is an EC-199 integration step; phase 1 ships the handler + its contract.\n *\n * Session-gated: `getSession` (host-wired, reading its own auth — this module never\n * touches `lib/auth`) must return a session for authenticated callers; anonymous\n * callers are rejected. Plus a soft, in-memory per-project rate counter (spend guard).\n *\n * EC-199: the route serves TWO model operations — both thin proxies, neither with any\n * data access — discriminated by the request body's `kind`:\n * - `{ kind: 'chat', messages, system? }` → `provider.streamChat` (text stream);\n * - `{ kind: 'generate', request }` → `provider.generateStructured` (JSON).\n * The structured mode is what lets `applyContentWithShape` run CLIENT-side against a\n * {@link createRouteProvider} whose key never touches the browser — the creation flow's\n * whole authoring loop proxies its model calls here while the tools (search/list/apply)\n * execute in the client with the user's own session. A body with `messages` and no\n * `kind` is still treated as a chat request (back-compat with the phase-1 contract).\n */\nimport type {\n AiChatMessage,\n AiRouteHandler,\n AiRouteHandlerOptions,\n AiSession,\n GenerateStructuredRequest,\n StreamChatRequest,\n} from './types'\n\nfunction jsonResponse(value: unknown, status: number): Response {\n return new Response(JSON.stringify(value), {\n status,\n headers: { 'content-type': 'application/json' },\n })\n}\n\ninterface RateBucket {\n windowStart: number\n count: number\n}\n\nfunction isChatMessage(value: unknown): value is AiChatMessage {\n if (typeof value !== 'object' || value === null) return false\n const m = value as { role?: unknown; content?: unknown }\n return (\n (m.role === 'system' || m.role === 'user' || m.role === 'assistant') &&\n typeof m.content === 'string'\n )\n}\n\n/** Parse a request body into a {@link StreamChatRequest}, or `null` when malformed. */\nfunction parseChatBody(body: unknown): StreamChatRequest | null {\n if (typeof body !== 'object' || body === null) return null\n const { messages, system } = body as { messages?: unknown; system?: unknown }\n if (!Array.isArray(messages) || messages.length === 0 || !messages.every(isChatMessage)) {\n return null\n }\n return {\n messages: messages as AiChatMessage[],\n ...(typeof system === 'string' ? { system } : {}),\n }\n}\n\n/** The wire discriminator selecting which model operation the route proxies. */\nexport type AiRouteKind = 'chat' | 'generate'\n\n/** Structurally validate a {@link GenerateStructuredRequest}, or `null` when malformed. */\nfunction parseGenerateBody(body: unknown): GenerateStructuredRequest | null {\n if (typeof body !== 'object' || body === null) return null\n const request = (body as { request?: unknown }).request\n if (typeof request !== 'object' || request === null) return null\n const { prompt, schema } = request as { prompt?: unknown; schema?: unknown }\n if (typeof prompt !== 'string' || typeof schema !== 'object' || schema === null) return null\n return request as GenerateStructuredRequest\n}\n\n/** The `kind` a body selects: explicit `kind`, else `messages` ⇒ chat (back-compat). */\nfunction bodyKind(body: unknown): AiRouteKind | null {\n if (typeof body !== 'object' || body === null) return null\n const kind = (body as { kind?: unknown }).kind\n if (kind === 'chat' || kind === 'generate') return kind\n if (Array.isArray((body as { messages?: unknown }).messages)) return 'chat'\n return null\n}\n\n/**\n * Build the mounted route handler. The returned function is a plain\n * `(Request) => Promise<Response>` — session-gated, soft-rate-limited, and a thin\n * proxy to `provider.streamChat`. Never throws to the caller: every failure is a\n * structured JSON response with an appropriate status.\n */\nexport function createAiRouteHandler(options: AiRouteHandlerOptions): AiRouteHandler {\n const maxPerMinute = options.limits?.maxRequestsPerMinute\n const buckets = new Map<string, RateBucket>()\n const projectKey = options.projectKey ?? (() => 'global')\n\n function rateLimited(key: string): boolean {\n if (maxPerMinute === undefined || maxPerMinute <= 0) return false\n const now = Date.now()\n const bucket = buckets.get(key)\n if (!bucket || now - bucket.windowStart >= 60_000) {\n buckets.set(key, { windowStart: now, count: 1 })\n return false\n }\n bucket.count += 1\n return bucket.count > maxPerMinute\n }\n\n return async (request: Request): Promise<Response> => {\n // 1. Session gate — reject anonymous callers (the route holds spend, not data).\n let session: AiSession | null\n try {\n session = await options.getSession(request)\n } catch {\n return jsonResponse({ error: 'unauthorized' }, 401)\n }\n if (!session) return jsonResponse({ error: 'unauthorized' }, 401)\n\n // 2. Method.\n if (request.method !== 'POST') return jsonResponse({ error: 'method-not-allowed' }, 405)\n\n // 3. Soft per-project rate / spend counter.\n if (rateLimited(projectKey(request))) return jsonResponse({ error: 'rate-limited' }, 429)\n\n // 4. Body → the requested model operation.\n let body: unknown\n try {\n body = await request.json()\n } catch {\n return jsonResponse({ error: 'invalid-json' }, 400)\n }\n\n const kind = bodyKind(body)\n\n // 5a. Structured generation — the thin proxy that lets `applyContentWithShape`\n // run client-side without the key ever reaching the browser.\n if (kind === 'generate') {\n const generate = parseGenerateBody(body)\n if (!generate) {\n return jsonResponse(\n { error: 'invalid-request', message: 'expected { kind: \"generate\", request }' },\n 400,\n )\n }\n try {\n const result = await options.provider.generateStructured(generate)\n return jsonResponse(result, 200)\n } catch {\n return jsonResponse({ error: 'provider-error' }, 502)\n }\n }\n\n // 5b. Chat stream (default / back-compat).\n const chat = kind === 'chat' ? parseChatBody(body) : null\n if (!chat) {\n return jsonResponse(\n { error: 'invalid-request', message: 'expected { messages: AiChatMessage[] }' },\n 400,\n )\n }\n if (!options.provider.streamChat) {\n return jsonResponse({ error: 'provider-cannot-stream' }, 501)\n }\n try {\n return await options.provider.streamChat(chat)\n } catch {\n return jsonResponse({ error: 'provider-error' }, 502)\n }\n }\n}\n"]}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The narrow tool set the creation flow's model works against (EC-199).
|
|
3
|
+
*
|
|
4
|
+
* Per the ratified AD, tools execute CLIENT-side against the operations layer with
|
|
5
|
+
* the user's own session — this module only declares their SHAPES (the model-facing
|
|
6
|
+
* planner schema) and the reader interface the studio binds to operations. The engine
|
|
7
|
+
* never gains a write path: `searchDocuments` / `listAssets` are reads that gather the
|
|
8
|
+
* real ids a proposal may reference (a relation id is otherwise structurally
|
|
9
|
+
* impossible — see `buildTargetShape`), and `createDocument` is the terminal action
|
|
10
|
+
* that runs `applyContentWithShape` and yields a reviewable proposal. `updateFields`
|
|
11
|
+
* and `translate` are deliberately NOT here — they are later slices (EC-332 / post-v1).
|
|
12
|
+
*/
|
|
13
|
+
import type { JsonSchema } from '@elytracms/core/cms-core';
|
|
14
|
+
/** A document the model may reference as a relation target (from a `searchDocuments` read). */
|
|
15
|
+
export interface DocumentSearchResult {
|
|
16
|
+
id: string;
|
|
17
|
+
title?: string;
|
|
18
|
+
}
|
|
19
|
+
/** An asset the model may reference (from a `listAssets` read). */
|
|
20
|
+
export interface AssetSearchResult {
|
|
21
|
+
id: string;
|
|
22
|
+
title?: string;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* The client-side, operations-backed readers the orchestrator calls. The studio binds
|
|
26
|
+
* these to the user's authenticated operations client; tests bind an in-memory fake.
|
|
27
|
+
* Reads only — there is no write tool (apply is a separate, human-gated draft write).
|
|
28
|
+
*/
|
|
29
|
+
export interface CreateFlowTools {
|
|
30
|
+
searchDocuments(input: {
|
|
31
|
+
collection: string;
|
|
32
|
+
query?: string;
|
|
33
|
+
limit?: number;
|
|
34
|
+
}): Promise<DocumentSearchResult[]>;
|
|
35
|
+
listAssets(input: {
|
|
36
|
+
query?: string;
|
|
37
|
+
limit?: number;
|
|
38
|
+
}): Promise<AssetSearchResult[]>;
|
|
39
|
+
}
|
|
40
|
+
/** One planned step the model returns — the discriminated tool/action union. */
|
|
41
|
+
export type PlannerAction = {
|
|
42
|
+
action: 'searchDocuments';
|
|
43
|
+
collection: string;
|
|
44
|
+
query?: string;
|
|
45
|
+
} | {
|
|
46
|
+
action: 'listAssets';
|
|
47
|
+
query?: string;
|
|
48
|
+
} | {
|
|
49
|
+
action: 'createDocument';
|
|
50
|
+
collection: string;
|
|
51
|
+
instruction?: string;
|
|
52
|
+
} | {
|
|
53
|
+
action: 'respond';
|
|
54
|
+
message: string;
|
|
55
|
+
};
|
|
56
|
+
/** The schema name the planner call carries — how a FakeProvider tells plan vs. author. */
|
|
57
|
+
export declare const PLANNER_SCHEMA_NAME = "create_flow_action";
|
|
58
|
+
/**
|
|
59
|
+
* The structured-output schema for ONE planning step. `action` is a closed enum and
|
|
60
|
+
* `collection` is leashed to the known collection ids, so the model can neither invent
|
|
61
|
+
* an action nor target a collection that does not exist.
|
|
62
|
+
*/
|
|
63
|
+
export declare function plannerSchema(collectionIds: readonly string[]): JsonSchema;
|
|
64
|
+
/** Coerce a raw structured-output object into a {@link PlannerAction}; `null` if unusable. */
|
|
65
|
+
export declare function parsePlannerAction(raw: unknown): PlannerAction | null;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/** The schema name the planner call carries — how a FakeProvider tells plan vs. author. */
|
|
2
|
+
export const PLANNER_SCHEMA_NAME = 'create_flow_action';
|
|
3
|
+
/**
|
|
4
|
+
* The structured-output schema for ONE planning step. `action` is a closed enum and
|
|
5
|
+
* `collection` is leashed to the known collection ids, so the model can neither invent
|
|
6
|
+
* an action nor target a collection that does not exist.
|
|
7
|
+
*/
|
|
8
|
+
export function plannerSchema(collectionIds) {
|
|
9
|
+
return {
|
|
10
|
+
type: 'object',
|
|
11
|
+
properties: {
|
|
12
|
+
action: {
|
|
13
|
+
type: 'string',
|
|
14
|
+
enum: ['searchDocuments', 'listAssets', 'createDocument', 'respond'],
|
|
15
|
+
description: 'searchDocuments: find real ids for a relation target before creating. ' +
|
|
16
|
+
'listAssets: find real asset ids. createDocument: author the document once ' +
|
|
17
|
+
'you have what you need. respond: reply without creating anything.',
|
|
18
|
+
},
|
|
19
|
+
collection: {
|
|
20
|
+
type: 'string',
|
|
21
|
+
...(collectionIds.length > 0 ? { enum: [...collectionIds] } : {}),
|
|
22
|
+
description: 'Target collection id (for searchDocuments / createDocument).',
|
|
23
|
+
},
|
|
24
|
+
query: { type: 'string', description: 'Optional search text (searchDocuments / listAssets).' },
|
|
25
|
+
instruction: {
|
|
26
|
+
type: 'string',
|
|
27
|
+
description: 'What to write, refining the user request (createDocument).',
|
|
28
|
+
},
|
|
29
|
+
message: { type: 'string', description: 'Assistant reply text (respond).' },
|
|
30
|
+
},
|
|
31
|
+
required: ['action'],
|
|
32
|
+
additionalProperties: false,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
/** Coerce a raw structured-output object into a {@link PlannerAction}; `null` if unusable. */
|
|
36
|
+
export function parsePlannerAction(raw) {
|
|
37
|
+
if (typeof raw !== 'object' || raw === null)
|
|
38
|
+
return null;
|
|
39
|
+
const value = raw;
|
|
40
|
+
const action = value.action;
|
|
41
|
+
const str = (k) => typeof value[k] === 'string' && value[k].length > 0 ? value[k] : undefined;
|
|
42
|
+
switch (action) {
|
|
43
|
+
case 'searchDocuments': {
|
|
44
|
+
const collection = str('collection');
|
|
45
|
+
if (!collection)
|
|
46
|
+
return null;
|
|
47
|
+
return { action, collection, ...(str('query') ? { query: str('query') } : {}) };
|
|
48
|
+
}
|
|
49
|
+
case 'listAssets':
|
|
50
|
+
return { action, ...(str('query') ? { query: str('query') } : {}) };
|
|
51
|
+
case 'createDocument': {
|
|
52
|
+
const collection = str('collection');
|
|
53
|
+
if (!collection)
|
|
54
|
+
return null;
|
|
55
|
+
return { action, collection, ...(str('instruction') ? { instruction: str('instruction') } : {}) };
|
|
56
|
+
}
|
|
57
|
+
case 'respond':
|
|
58
|
+
return { action, message: str('message') ?? '' };
|
|
59
|
+
default:
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
//# sourceMappingURL=tools.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tools.js","sourceRoot":"","sources":["../../../src/lib/ai/tools.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The narrow tool set the creation flow's model works against (EC-199).\n *\n * Per the ratified AD, tools execute CLIENT-side against the operations layer with\n * the user's own session — this module only declares their SHAPES (the model-facing\n * planner schema) and the reader interface the studio binds to operations. The engine\n * never gains a write path: `searchDocuments` / `listAssets` are reads that gather the\n * real ids a proposal may reference (a relation id is otherwise structurally\n * impossible — see `buildTargetShape`), and `createDocument` is the terminal action\n * that runs `applyContentWithShape` and yields a reviewable proposal. `updateFields`\n * and `translate` are deliberately NOT here — they are later slices (EC-332 / post-v1).\n */\nimport type { JsonSchema } from '@elytracms/core/cms-core'\n\n/** A document the model may reference as a relation target (from a `searchDocuments` read). */\nexport interface DocumentSearchResult {\n id: string\n title?: string\n}\n\n/** An asset the model may reference (from a `listAssets` read). */\nexport interface AssetSearchResult {\n id: string\n title?: string\n}\n\n/**\n * The client-side, operations-backed readers the orchestrator calls. The studio binds\n * these to the user's authenticated operations client; tests bind an in-memory fake.\n * Reads only — there is no write tool (apply is a separate, human-gated draft write).\n */\nexport interface CreateFlowTools {\n searchDocuments(input: {\n collection: string\n query?: string\n limit?: number\n }): Promise<DocumentSearchResult[]>\n listAssets(input: { query?: string; limit?: number }): Promise<AssetSearchResult[]>\n}\n\n/** One planned step the model returns — the discriminated tool/action union. */\nexport type PlannerAction =\n | { action: 'searchDocuments'; collection: string; query?: string }\n | { action: 'listAssets'; query?: string }\n | { action: 'createDocument'; collection: string; instruction?: string }\n | { action: 'respond'; message: string }\n\n/** The schema name the planner call carries — how a FakeProvider tells plan vs. author. */\nexport const PLANNER_SCHEMA_NAME = 'create_flow_action'\n\n/**\n * The structured-output schema for ONE planning step. `action` is a closed enum and\n * `collection` is leashed to the known collection ids, so the model can neither invent\n * an action nor target a collection that does not exist.\n */\nexport function plannerSchema(collectionIds: readonly string[]): JsonSchema {\n return {\n type: 'object',\n properties: {\n action: {\n type: 'string',\n enum: ['searchDocuments', 'listAssets', 'createDocument', 'respond'],\n description:\n 'searchDocuments: find real ids for a relation target before creating. ' +\n 'listAssets: find real asset ids. createDocument: author the document once ' +\n 'you have what you need. respond: reply without creating anything.',\n },\n collection: {\n type: 'string',\n ...(collectionIds.length > 0 ? { enum: [...collectionIds] } : {}),\n description: 'Target collection id (for searchDocuments / createDocument).',\n },\n query: { type: 'string', description: 'Optional search text (searchDocuments / listAssets).' },\n instruction: {\n type: 'string',\n description: 'What to write, refining the user request (createDocument).',\n },\n message: { type: 'string', description: 'Assistant reply text (respond).' },\n },\n required: ['action'],\n additionalProperties: false,\n }\n}\n\n/** Coerce a raw structured-output object into a {@link PlannerAction}; `null` if unusable. */\nexport function parsePlannerAction(raw: unknown): PlannerAction | null {\n if (typeof raw !== 'object' || raw === null) return null\n const value = raw as Record<string, unknown>\n const action = value.action\n const str = (k: string): string | undefined =>\n typeof value[k] === 'string' && (value[k] as string).length > 0 ? (value[k] as string) : undefined\n switch (action) {\n case 'searchDocuments': {\n const collection = str('collection')\n if (!collection) return null\n return { action, collection, ...(str('query') ? { query: str('query')! } : {}) }\n }\n case 'listAssets':\n return { action, ...(str('query') ? { query: str('query')! } : {}) }\n case 'createDocument': {\n const collection = str('collection')\n if (!collection) return null\n return { action, collection, ...(str('instruction') ? { instruction: str('instruction')! } : {}) }\n }\n case 'respond':\n return { action, message: str('message') ?? '' }\n default:\n return null\n }\n}\n"]}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The AI assistant panel's open/closed state (EC-199) — a framework-free module
|
|
3
|
+
* store, like the ambient locale (`lib/locale`). Module-level on purpose: the toggle
|
|
4
|
+
* lives in the workspace header (`StudioShell`) while the panel mounts deep in the CMS
|
|
5
|
+
* area (the view resolver's subtree) — two disjoint React subtrees that must share one
|
|
6
|
+
* open state without threading a provider through everything between them. The choice
|
|
7
|
+
* persists across sessions (best-effort, like the rail collapse preference).
|
|
8
|
+
*
|
|
9
|
+
* This module imports NO AI runtime — it is just a boolean store, safe to import from
|
|
10
|
+
* the always-mounted shell. The heavy chat panel stays behind its own lazy boundary.
|
|
11
|
+
*/
|
|
12
|
+
export declare const AI_PANEL_STORAGE_KEY = "elytra-ai-panel-open";
|
|
13
|
+
/** Whether the AI panel is currently open. */
|
|
14
|
+
export declare function isAiPanelOpen(): boolean;
|
|
15
|
+
/** Open/close the panel (persisted best-effort). */
|
|
16
|
+
export declare function setAiPanelOpen(next: boolean): void;
|
|
17
|
+
/** Toggle the panel. */
|
|
18
|
+
export declare function toggleAiPanel(): void;
|
|
19
|
+
/** React binding — the header toggle and the CMS-area panel both read this. */
|
|
20
|
+
export declare function useAiPanelOpen(): boolean;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { useSyncExternalStore } from 'react';
|
|
2
|
+
import { createBooleanPreference } from './local-preference';
|
|
3
|
+
/**
|
|
4
|
+
* The AI assistant panel's open/closed state (EC-199) — a framework-free module
|
|
5
|
+
* store, like the ambient locale (`lib/locale`). Module-level on purpose: the toggle
|
|
6
|
+
* lives in the workspace header (`StudioShell`) while the panel mounts deep in the CMS
|
|
7
|
+
* area (the view resolver's subtree) — two disjoint React subtrees that must share one
|
|
8
|
+
* open state without threading a provider through everything between them. The choice
|
|
9
|
+
* persists across sessions (best-effort, like the rail collapse preference).
|
|
10
|
+
*
|
|
11
|
+
* This module imports NO AI runtime — it is just a boolean store, safe to import from
|
|
12
|
+
* the always-mounted shell. The heavy chat panel stays behind its own lazy boundary.
|
|
13
|
+
*/
|
|
14
|
+
export const AI_PANEL_STORAGE_KEY = 'elytra-ai-panel-open';
|
|
15
|
+
const openPreference = createBooleanPreference(AI_PANEL_STORAGE_KEY, false);
|
|
16
|
+
let open = openPreference.read();
|
|
17
|
+
const listeners = new Set();
|
|
18
|
+
function emit() {
|
|
19
|
+
for (const listener of listeners)
|
|
20
|
+
listener();
|
|
21
|
+
}
|
|
22
|
+
/** Whether the AI panel is currently open. */
|
|
23
|
+
export function isAiPanelOpen() {
|
|
24
|
+
return open;
|
|
25
|
+
}
|
|
26
|
+
/** Open/close the panel (persisted best-effort). */
|
|
27
|
+
export function setAiPanelOpen(next) {
|
|
28
|
+
if (next === open)
|
|
29
|
+
return;
|
|
30
|
+
open = next;
|
|
31
|
+
openPreference.store(next);
|
|
32
|
+
emit();
|
|
33
|
+
}
|
|
34
|
+
/** Toggle the panel. */
|
|
35
|
+
export function toggleAiPanel() {
|
|
36
|
+
setAiPanelOpen(!open);
|
|
37
|
+
}
|
|
38
|
+
function subscribe(listener) {
|
|
39
|
+
listeners.add(listener);
|
|
40
|
+
return () => {
|
|
41
|
+
listeners.delete(listener);
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
/** React binding — the header toggle and the CMS-area panel both read this. */
|
|
45
|
+
export function useAiPanelOpen() {
|
|
46
|
+
return useSyncExternalStore(subscribe, isAiPanelOpen, isAiPanelOpen);
|
|
47
|
+
}
|
|
48
|
+
//# sourceMappingURL=ai-panel.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ai-panel.js","sourceRoot":"","sources":["../../src/lib/ai-panel.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,EAAE,MAAM,OAAO,CAAA;AAC5C,OAAO,EAAE,uBAAuB,EAAE,MAAM,oBAAoB,CAAA;AAE5D;;;;;;;;;;GAUG;AACH,MAAM,CAAC,MAAM,oBAAoB,GAAG,sBAAsB,CAAA;AAE1D,MAAM,cAAc,GAAG,uBAAuB,CAAC,oBAAoB,EAAE,KAAK,CAAC,CAAA;AAE3E,IAAI,IAAI,GAAG,cAAc,CAAC,IAAI,EAAE,CAAA;AAChC,MAAM,SAAS,GAAG,IAAI,GAAG,EAAc,CAAA;AAEvC,SAAS,IAAI;IACX,KAAK,MAAM,QAAQ,IAAI,SAAS;QAAE,QAAQ,EAAE,CAAA;AAC9C,CAAC;AAED,8CAA8C;AAC9C,MAAM,UAAU,aAAa;IAC3B,OAAO,IAAI,CAAA;AACb,CAAC;AAED,oDAAoD;AACpD,MAAM,UAAU,cAAc,CAAC,IAAa;IAC1C,IAAI,IAAI,KAAK,IAAI;QAAE,OAAM;IACzB,IAAI,GAAG,IAAI,CAAA;IACX,cAAc,CAAC,KAAK,CAAC,IAAI,CAAC,CAAA;IAC1B,IAAI,EAAE,CAAA;AACR,CAAC;AAED,wBAAwB;AACxB,MAAM,UAAU,aAAa;IAC3B,cAAc,CAAC,CAAC,IAAI,CAAC,CAAA;AACvB,CAAC;AAED,SAAS,SAAS,CAAC,QAAoB;IACrC,SAAS,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAA;IACvB,OAAO,GAAG,EAAE;QACV,SAAS,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAA;IAC5B,CAAC,CAAA;AACH,CAAC;AAED,+EAA+E;AAC/E,MAAM,UAAU,cAAc;IAC5B,OAAO,oBAAoB,CAAC,SAAS,EAAE,aAAa,EAAE,aAAa,CAAC,CAAA;AACtE,CAAC","sourcesContent":["import { useSyncExternalStore } from 'react'\nimport { createBooleanPreference } from './local-preference'\n\n/**\n * The AI assistant panel's open/closed state (EC-199) — a framework-free module\n * store, like the ambient locale (`lib/locale`). Module-level on purpose: the toggle\n * lives in the workspace header (`StudioShell`) while the panel mounts deep in the CMS\n * area (the view resolver's subtree) — two disjoint React subtrees that must share one\n * open state without threading a provider through everything between them. The choice\n * persists across sessions (best-effort, like the rail collapse preference).\n *\n * This module imports NO AI runtime — it is just a boolean store, safe to import from\n * the always-mounted shell. The heavy chat panel stays behind its own lazy boundary.\n */\nexport const AI_PANEL_STORAGE_KEY = 'elytra-ai-panel-open'\n\nconst openPreference = createBooleanPreference(AI_PANEL_STORAGE_KEY, false)\n\nlet open = openPreference.read()\nconst listeners = new Set<() => void>()\n\nfunction emit(): void {\n for (const listener of listeners) listener()\n}\n\n/** Whether the AI panel is currently open. */\nexport function isAiPanelOpen(): boolean {\n return open\n}\n\n/** Open/close the panel (persisted best-effort). */\nexport function setAiPanelOpen(next: boolean): void {\n if (next === open) return\n open = next\n openPreference.store(next)\n emit()\n}\n\n/** Toggle the panel. */\nexport function toggleAiPanel(): void {\n setAiPanelOpen(!open)\n}\n\nfunction subscribe(listener: () => void): () => void {\n listeners.add(listener)\n return () => {\n listeners.delete(listener)\n }\n}\n\n/** React binding — the header toggle and the CMS-area panel both read this. */\nexport function useAiPanelOpen(): boolean {\n return useSyncExternalStore(subscribe, isAiPanelOpen, isAiPanelOpen)\n}\n"]}
|
|
@@ -366,6 +366,9 @@ export const EN_MESSAGES = {
|
|
|
366
366
|
'cms.version.compareVersionLabel': msg('Compare v{version}'),
|
|
367
367
|
'cms.version.switchToDraftToRestore': msg('Switch to Draft to restore'),
|
|
368
368
|
'cms.version.restore': msg('Restore'),
|
|
369
|
+
// EC-199 provenance — a subtle marker on versions recorded from AI-authored content.
|
|
370
|
+
'cms.version.aiAuthored': msg('AI', 'Short badge marking an AI-authored version.'),
|
|
371
|
+
'cms.version.aiAuthoredTitle': msg('Authored with the AI assistant'),
|
|
369
372
|
// EC-317 — descriptor lane, unified/side-by-side toggle, truncation, zebra.
|
|
370
373
|
'cms.version.fieldColumnHeader': msg('Field'),
|
|
371
374
|
'cms.version.viewSideBySide': msg('Side by side'),
|
|
@@ -806,5 +809,36 @@ export const EN_MESSAGES = {
|
|
|
806
809
|
'richText.slash.text': msg('Text'),
|
|
807
810
|
'richText.slash.empty': msg('No matching blocks'),
|
|
808
811
|
'richText.slash.componentsGroup': msg('Components'),
|
|
812
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
813
|
+
// EC-199 — AI assistant chat panel + creation flow
|
|
814
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
815
|
+
'ai.panel.title': msg('Assistant'),
|
|
816
|
+
'ai.panel.toggle': msg('Toggle the AI assistant'),
|
|
817
|
+
'ai.panel.close': msg('Close the assistant'),
|
|
818
|
+
'ai.panel.loading': msg('Loading the assistant…'),
|
|
819
|
+
'ai.panel.working': msg('Working…'),
|
|
820
|
+
'ai.panel.placeholder': msg('Ask the assistant to create content…'),
|
|
821
|
+
'ai.panel.send': msg('Send'),
|
|
822
|
+
'ai.panel.emptyTitle': msg('Create content with AI'),
|
|
823
|
+
'ai.panel.emptyHint': msg('Describe what you want to create — for example, “Write a blog post about our new opening hours.” The assistant drafts it for you to review.'),
|
|
824
|
+
'ai.panel.unavailable': msg('The assistant is not available in this project.'),
|
|
825
|
+
'ai.step.search': msg('Looked up {collection} — {count} found', 'Tool status line. {count} is a number.'),
|
|
826
|
+
'ai.step.listAssets': msg('Browsed assets — {count} found'),
|
|
827
|
+
'ai.step.drafting': msg('Drafting the {collection}…'),
|
|
828
|
+
'ai.proposal.title': msg('Proposed {collection}'),
|
|
829
|
+
'ai.proposal.reviewHint': msg('Review the draft, then apply it or discard it.'),
|
|
830
|
+
'ai.proposal.apply': msg('Apply as draft'),
|
|
831
|
+
'ai.proposal.discard': msg('Discard'),
|
|
832
|
+
'ai.proposal.applied': msg('Applied as draft'),
|
|
833
|
+
'ai.proposal.discarded': msg('Discarded'),
|
|
834
|
+
'ai.proposal.discardedNote': msg('This proposal was discarded.'),
|
|
835
|
+
'ai.proposal.open': msg('Open document'),
|
|
836
|
+
'ai.proposal.unavailable': msg('This proposal can no longer be shown.'),
|
|
837
|
+
'ai.proposal.diffBefore': msg('Empty'),
|
|
838
|
+
'ai.proposal.diffAfter': msg('Proposed'),
|
|
839
|
+
'ai.proposal.diffUnified': msg('Proposed content'),
|
|
840
|
+
'ai.error.route': msg('The assistant is unavailable right now. Please try again.'),
|
|
841
|
+
'ai.error.generic': msg('Something went wrong. Please try again.'),
|
|
842
|
+
'ai.error.applyFailed': msg('The draft could not be applied.'),
|
|
809
843
|
};
|
|
810
844
|
//# sourceMappingURL=messages.en.js.map
|