@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/convex/cms.ts
CHANGED
|
@@ -129,6 +129,7 @@ export const saveDocumentVersion = mutation({
|
|
|
129
129
|
docId: v.string(),
|
|
130
130
|
snapshot: v.any(),
|
|
131
131
|
label: v.optional(v.string()),
|
|
132
|
+
origin: v.optional(v.string()), // EC-199 provenance ('assistant' for AI-authored)
|
|
132
133
|
createdAt: v.string(),
|
|
133
134
|
},
|
|
134
135
|
handler: async (ctx, args) => {
|
package/convex/schema.ts
CHANGED
|
@@ -120,6 +120,9 @@ export default defineSchema({
|
|
|
120
120
|
version: v.number(),
|
|
121
121
|
snapshot: v.any(),
|
|
122
122
|
label: v.optional(v.string()),
|
|
123
|
+
// EC-199 provenance: authoring origin ('assistant' for AI-authored content).
|
|
124
|
+
// Additive scalar, outside the folded snapshot — invisible when unset.
|
|
125
|
+
origin: v.optional(v.string()),
|
|
123
126
|
createdAt: v.string(),
|
|
124
127
|
})
|
|
125
128
|
.index('by_doc', ['collection', 'docId'])
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function AiChatPanel(): import("react").JSX.Element;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Suspense, lazy } from 'react';
|
|
3
|
+
import { Text } from '@elytracms/studio/ui';
|
|
4
|
+
import { translate } from '@elytracms/studio/lib/i18n';
|
|
5
|
+
/**
|
|
6
|
+
* Lazy boundary for the AI chat panel (EC-199, EC-331 bundle discipline). The panel
|
|
7
|
+
* and everything it imports — the creation orchestrator, `applyContentWithShape`, the
|
|
8
|
+
* provider adapters (and the AI SDK behind the Anthropic one via dynamic import) — load
|
|
9
|
+
* ONLY when a configured studio actually opens the panel. An unconfigured studio never
|
|
10
|
+
* renders the toggle, never mounts this, and so pulls none of the AI chunk onto the CMS
|
|
11
|
+
* critical path (pinned by the `lib/ai` bundle test). Same pattern as
|
|
12
|
+
* `version-compare-lazy` / `rich-text/lazy`.
|
|
13
|
+
*/
|
|
14
|
+
const AiChatPanelImpl = lazy(() => import('./ai-chat-panel').then((module) => ({ default: module.AiChatPanel })));
|
|
15
|
+
function AiPanelFallback() {
|
|
16
|
+
return (_jsx("aside", { className: "flex h-full min-h-0 w-[360px] shrink-0 flex-col border-l border-border bg-card", children: _jsx("div", { className: "grid min-h-0 flex-1 place-items-center p-6", children: _jsx(Text, { size: "sm", tone: "muted", children: translate('ai.panel.loading') }) }) }));
|
|
17
|
+
}
|
|
18
|
+
export function AiChatPanel() {
|
|
19
|
+
return (_jsx(Suspense, { fallback: _jsx(AiPanelFallback, {}), children: _jsx(AiChatPanelImpl, {}) }));
|
|
20
|
+
}
|
|
21
|
+
//# sourceMappingURL=ai-chat-panel-lazy.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ai-chat-panel-lazy.js","sourceRoot":"","sources":["../../../src/components/ai/ai-chat-panel-lazy.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,OAAO,CAAA;AACtC,OAAO,EAAE,IAAI,EAAE,MAAM,sBAAsB,CAAA;AAC3C,OAAO,EAAE,SAAS,EAAE,MAAM,4BAA4B,CAAA;AAEtD;;;;;;;;GAQG;AACH,MAAM,eAAe,GAAG,IAAI,CAAC,GAAG,EAAE,CAChC,MAAM,CAAC,iBAAiB,CAAC,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,EAAE,OAAO,EAAE,MAAM,CAAC,WAAW,EAAE,CAAC,CAAC,CAC9E,CAAA;AAED,SAAS,eAAe;IACtB,OAAO,CACL,gBAAO,SAAS,EAAC,gFAAgF,YAC/F,cAAK,SAAS,EAAC,4CAA4C,YACzD,KAAC,IAAI,IAAC,IAAI,EAAC,IAAI,EAAC,IAAI,EAAC,OAAO,YACzB,SAAS,CAAC,kBAAkB,CAAC,GACzB,GACH,GACA,CACT,CAAA;AACH,CAAC;AAED,MAAM,UAAU,WAAW;IACzB,OAAO,CACL,KAAC,QAAQ,IAAC,QAAQ,EAAE,KAAC,eAAe,KAAG,YACrC,KAAC,eAAe,KAAG,GACV,CACZ,CAAA;AACH,CAAC","sourcesContent":["import { Suspense, lazy } from 'react'\nimport { Text } from '@elytracms/studio/ui'\nimport { translate } from '@elytracms/studio/lib/i18n'\n\n/**\n * Lazy boundary for the AI chat panel (EC-199, EC-331 bundle discipline). The panel\n * and everything it imports — the creation orchestrator, `applyContentWithShape`, the\n * provider adapters (and the AI SDK behind the Anthropic one via dynamic import) — load\n * ONLY when a configured studio actually opens the panel. An unconfigured studio never\n * renders the toggle, never mounts this, and so pulls none of the AI chunk onto the CMS\n * critical path (pinned by the `lib/ai` bundle test). Same pattern as\n * `version-compare-lazy` / `rich-text/lazy`.\n */\nconst AiChatPanelImpl = lazy(() =>\n import('./ai-chat-panel').then((module) => ({ default: module.AiChatPanel })),\n)\n\nfunction AiPanelFallback() {\n return (\n <aside className=\"flex h-full min-h-0 w-[360px] shrink-0 flex-col border-l border-border bg-card\">\n <div className=\"grid min-h-0 flex-1 place-items-center p-6\">\n <Text size=\"sm\" tone=\"muted\">\n {translate('ai.panel.loading')}\n </Text>\n </div>\n </aside>\n )\n}\n\nexport function AiChatPanel() {\n return (\n <Suspense fallback={<AiPanelFallback />}>\n <AiChatPanelImpl />\n </Suspense>\n )\n}\n"]}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The AI assistant chat panel (EC-199) — the right-hand collapsible surface in the CMS
|
|
3
|
+
* area. It owns the chat state (the pure `chatReducer`), resolves the EC-331 provider
|
|
4
|
+
* from `defineStudio({ ai })`, binds the narrow tools (search/list) to the operations
|
|
5
|
+
* layer with the user's session, runs the creation orchestrator, and applies a
|
|
6
|
+
* reviewed proposal as a DRAFT (operations upsert + a provenance-tagged version
|
|
7
|
+
* snapshot). NEVER a modal (house rule). The whole module sits behind a lazy boundary
|
|
8
|
+
* (`ai-chat-panel-lazy`), so an unconfigured studio pulls none of it — and no AI SDK.
|
|
9
|
+
*/
|
|
10
|
+
export declare function AiChatPanel(): import("react").JSX.Element;
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useCallback, useMemo, useReducer, useState } from 'react';
|
|
3
|
+
import { X } from 'lucide-react';
|
|
4
|
+
import { createCollectionRegistry, resolveDocumentTitle, } from '@elytracms/core/cms-core';
|
|
5
|
+
import { Button, Heading, Row, Spacer, Text } from '@elytracms/studio/ui';
|
|
6
|
+
import { translate } from '@elytracms/studio/lib/i18n';
|
|
7
|
+
import { STUDIO_PROJECT_ROUTE, studioPath, useNavigate } from '@elytracms/studio/router';
|
|
8
|
+
import { resolveDeploymentAi } from '@elytracms/studio/lib/workspace';
|
|
9
|
+
import { getStudioOperations } from '@elytracms/studio/lib/operations';
|
|
10
|
+
import { AiRouteError, chatReducer, initialChatState, resolveAiConfig, resolveKnowledge, runCreateFlow, } from '@elytracms/studio/lib/ai';
|
|
11
|
+
import { useCms } from '../cms/cms-store';
|
|
12
|
+
import { validateComposition, validateRichTextEmbeds } from '../cms/cms-composition-validators';
|
|
13
|
+
import { AssistantTurn, Conversation, ConversationEmpty, PromptInput, ToolStatus, UserTurn, } from './chat-ui';
|
|
14
|
+
import { ProposalCard } from './proposal-card';
|
|
15
|
+
import { setAiPanelOpen } from '@elytracms/studio/lib/ai-panel';
|
|
16
|
+
/**
|
|
17
|
+
* The AI assistant chat panel (EC-199) — the right-hand collapsible surface in the CMS
|
|
18
|
+
* area. It owns the chat state (the pure `chatReducer`), resolves the EC-331 provider
|
|
19
|
+
* from `defineStudio({ ai })`, binds the narrow tools (search/list) to the operations
|
|
20
|
+
* layer with the user's session, runs the creation orchestrator, and applies a
|
|
21
|
+
* reviewed proposal as a DRAFT (operations upsert + a provenance-tagged version
|
|
22
|
+
* snapshot). NEVER a modal (house rule). The whole module sits behind a lazy boundary
|
|
23
|
+
* (`ai-chat-panel-lazy`), so an unconfigured studio pulls none of it — and no AI SDK.
|
|
24
|
+
*/
|
|
25
|
+
export function AiChatPanel() {
|
|
26
|
+
const cms = useCms();
|
|
27
|
+
const navigate = useNavigate();
|
|
28
|
+
const [state, dispatch] = useReducer(chatReducer, initialChatState);
|
|
29
|
+
const [input, setInput] = useState('');
|
|
30
|
+
const projectId = cms.projectId;
|
|
31
|
+
const activeLocale = cms.activeLocale;
|
|
32
|
+
// Resolve the AI platform config once (presence + capability gate). The panel only
|
|
33
|
+
// mounts when configured, but a misconfigured provider still fails loud here.
|
|
34
|
+
const resolved = useMemo(() => resolveAiConfig(resolveDeploymentAi()), []);
|
|
35
|
+
const provider = resolved.resolved?.provider ?? null;
|
|
36
|
+
const knowledgeSources = resolved.resolved?.knowledge ?? [];
|
|
37
|
+
const maxRepairAttempts = resolved.resolved?.limits?.maxRepairAttempts;
|
|
38
|
+
const ready = provider !== null && resolved.ok && projectId !== undefined;
|
|
39
|
+
const registry = useMemo(() => createCollectionRegistry(cms.collections), [cms.collections]);
|
|
40
|
+
/** Merge a document's non-localized + active-locale values for title/prose reads. */
|
|
41
|
+
const localeValues = useCallback((doc) => ({
|
|
42
|
+
...doc.values,
|
|
43
|
+
...(doc.localized?.[activeLocale] ?? {}),
|
|
44
|
+
}), [activeLocale]);
|
|
45
|
+
/** Operations-backed tool readers — client-side, with the user's own session. */
|
|
46
|
+
const tools = useMemo(() => {
|
|
47
|
+
const matches = (query, ...fields) => !query || fields.some((f) => f?.toLowerCase().includes(query.toLowerCase()));
|
|
48
|
+
return {
|
|
49
|
+
async searchDocuments({ collection, query }) {
|
|
50
|
+
if (projectId === undefined)
|
|
51
|
+
return [];
|
|
52
|
+
const { operations } = await getStudioOperations();
|
|
53
|
+
const result = await operations.documents.list({ projectId, collection });
|
|
54
|
+
if (!result.ok)
|
|
55
|
+
return [];
|
|
56
|
+
const target = cms.getCollection(collection);
|
|
57
|
+
return result.value
|
|
58
|
+
.map((rec) => ({
|
|
59
|
+
id: rec.document.id,
|
|
60
|
+
title: target
|
|
61
|
+
? resolveDocumentTitle(target, localeValues(rec.document), rec.document.id)
|
|
62
|
+
: rec.document.id,
|
|
63
|
+
}))
|
|
64
|
+
.filter((r) => matches(query, r.title, r.id));
|
|
65
|
+
},
|
|
66
|
+
async listAssets({ query }) {
|
|
67
|
+
if (projectId === undefined)
|
|
68
|
+
return [];
|
|
69
|
+
const { operations } = await getStudioOperations();
|
|
70
|
+
const result = await operations.assets.list({ projectId });
|
|
71
|
+
if (!result.ok)
|
|
72
|
+
return [];
|
|
73
|
+
return result.value
|
|
74
|
+
.map((asset) => ({ id: asset.id, title: asset.title ?? asset.filename }))
|
|
75
|
+
.filter((r) => matches(query, r.title, r.id));
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
}, [projectId, cms, localeValues]);
|
|
79
|
+
/** A fresh, collision-free draft id for the target collection. */
|
|
80
|
+
const mintDocumentId = useCallback((collectionId) => {
|
|
81
|
+
const existing = new Set(cms.documentsOf(collectionId).map((d) => d.id));
|
|
82
|
+
let n = 1;
|
|
83
|
+
while (existing.has(`new-${collectionId}-${n}`))
|
|
84
|
+
n += 1;
|
|
85
|
+
return `new-${collectionId}-${n}`;
|
|
86
|
+
}, [cms]);
|
|
87
|
+
/** Read `fromCollection(...)` knowledge prose through the operations layer. */
|
|
88
|
+
const readCollection = useCallback(async (source) => {
|
|
89
|
+
if (projectId === undefined)
|
|
90
|
+
return [];
|
|
91
|
+
const { operations } = await getStudioOperations();
|
|
92
|
+
const result = await operations.documents.list({ projectId, collection: source.collectionId });
|
|
93
|
+
if (!result.ok)
|
|
94
|
+
return [];
|
|
95
|
+
const target = cms.getCollection(source.collectionId);
|
|
96
|
+
const field = source.field ?? target?.fields.find((f) => f.type === 'text' || f.type === 'richText')?.name;
|
|
97
|
+
return result.value
|
|
98
|
+
.map((rec) => {
|
|
99
|
+
const values = localeValues(rec.document);
|
|
100
|
+
const content = field ? String(values[field] ?? '') : '';
|
|
101
|
+
return { content };
|
|
102
|
+
})
|
|
103
|
+
.filter((item) => item.content.trim().length > 0);
|
|
104
|
+
}, [projectId, cms, localeValues]);
|
|
105
|
+
const runTurn = useCallback(async (text) => {
|
|
106
|
+
if (!provider)
|
|
107
|
+
return;
|
|
108
|
+
try {
|
|
109
|
+
const knowledge = await resolveKnowledge(knowledgeSources, { readCollection });
|
|
110
|
+
const result = await runCreateFlow({
|
|
111
|
+
provider,
|
|
112
|
+
tools,
|
|
113
|
+
registry,
|
|
114
|
+
collections: cms.collections,
|
|
115
|
+
instruction: text,
|
|
116
|
+
knowledge,
|
|
117
|
+
locale: activeLocale,
|
|
118
|
+
mintDocumentId,
|
|
119
|
+
validateRichTextEmbeds,
|
|
120
|
+
validateComposition,
|
|
121
|
+
...(maxRepairAttempts !== undefined ? { maxRepairAttempts } : {}),
|
|
122
|
+
onStep: (step) => dispatch({ type: 'step', step }),
|
|
123
|
+
});
|
|
124
|
+
dispatch({ type: 'result', result });
|
|
125
|
+
}
|
|
126
|
+
catch (error) {
|
|
127
|
+
const message = error instanceof AiRouteError
|
|
128
|
+
? translate('ai.error.route')
|
|
129
|
+
: translate('ai.error.generic');
|
|
130
|
+
dispatch({ type: 'fail', message });
|
|
131
|
+
}
|
|
132
|
+
}, [
|
|
133
|
+
provider,
|
|
134
|
+
knowledgeSources,
|
|
135
|
+
readCollection,
|
|
136
|
+
tools,
|
|
137
|
+
registry,
|
|
138
|
+
cms.collections,
|
|
139
|
+
activeLocale,
|
|
140
|
+
mintDocumentId,
|
|
141
|
+
maxRepairAttempts,
|
|
142
|
+
]);
|
|
143
|
+
const submit = useCallback(() => {
|
|
144
|
+
const text = input.trim();
|
|
145
|
+
if (text.length === 0 || state.status === 'busy' || !ready)
|
|
146
|
+
return;
|
|
147
|
+
setInput('');
|
|
148
|
+
dispatch({ type: 'submit', text });
|
|
149
|
+
void runTurn(text);
|
|
150
|
+
}, [input, state.status, ready, runTurn]);
|
|
151
|
+
const applyProposal = useCallback(async (proposal) => {
|
|
152
|
+
if (projectId === undefined)
|
|
153
|
+
return;
|
|
154
|
+
const doc = {
|
|
155
|
+
collection: proposal.collection,
|
|
156
|
+
id: proposal.documentId,
|
|
157
|
+
values: proposal.patch.values,
|
|
158
|
+
...(proposal.patch.localized ? { localized: proposal.patch.localized } : {}),
|
|
159
|
+
};
|
|
160
|
+
const { operations } = await getStudioOperations();
|
|
161
|
+
const upserted = await operations.documents.upsert({ projectId, document: doc });
|
|
162
|
+
if (!upserted.ok) {
|
|
163
|
+
dispatch({ type: 'fail', message: translate('ai.error.applyFailed') });
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
// Provenance: record an AI-tagged version snapshot (a restore point + the
|
|
167
|
+
// 'assistant' origin surfaced in the versions pane). Best-effort — the draft
|
|
168
|
+
// already landed above; a snapshot failure must not lose the applied content.
|
|
169
|
+
try {
|
|
170
|
+
await operations.documents.snapshot({
|
|
171
|
+
projectId,
|
|
172
|
+
ref: { collection: proposal.collection, id: proposal.documentId },
|
|
173
|
+
label: translate('cms.version.aiAuthoredTitle'),
|
|
174
|
+
origin: 'assistant',
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
catch {
|
|
178
|
+
// ignore — provenance is additive
|
|
179
|
+
}
|
|
180
|
+
// Reflect in the session so the new draft shows in the list + is navigable.
|
|
181
|
+
cms.saveDocument(doc);
|
|
182
|
+
dispatch({ type: 'apply', proposalId: proposal.id });
|
|
183
|
+
}, [projectId, cms]);
|
|
184
|
+
const openProposal = useCallback((proposal) => {
|
|
185
|
+
if (projectId === undefined)
|
|
186
|
+
return;
|
|
187
|
+
void navigate({
|
|
188
|
+
to: STUDIO_PROJECT_ROUTE,
|
|
189
|
+
params: { projectId, _splat: studioPath.cmsDocument(proposal.collection, proposal.documentId) },
|
|
190
|
+
});
|
|
191
|
+
}, [navigate, projectId]);
|
|
192
|
+
return (_jsxs("aside", { className: "flex h-full min-h-0 w-[360px] shrink-0 flex-col border-l border-border bg-card", children: [_jsxs(Row, { gap: "2", align: "center", className: "h-12 shrink-0 border-b border-border px-3", children: [_jsx(Heading, { level: 2, size: "h4", children: translate('ai.panel.title') }), _jsx(Spacer, {}), _jsx(Button, { size: "icon-sm", variant: "ghost", onClick: () => setAiPanelOpen(false), title: translate('ai.panel.close'), "aria-label": translate('ai.panel.close'), children: _jsx(X, {}) })] }), _jsxs(Conversation, { atBottomKey: state.messages, children: [state.messages.length === 0 ? _jsx(ConversationEmpty, {}) : null, state.messages.map((message) => {
|
|
193
|
+
if (message.role === 'user')
|
|
194
|
+
return _jsx(UserTurn, { text: message.text }, message.id);
|
|
195
|
+
if (message.kind === 'text')
|
|
196
|
+
return (_jsx(AssistantTurn, { children: _jsx(Text, { size: "sm", className: "whitespace-pre-wrap break-words", children: message.text }) }, message.id));
|
|
197
|
+
if (message.kind === 'activity')
|
|
198
|
+
return (_jsx(AssistantTurn, { children: _jsx(ToolStatus, { steps: message.steps, active: state.status === 'busy' && state.activityId === message.id }) }, message.id));
|
|
199
|
+
return (_jsx(ProposalCard, { proposal: message.proposal, onApply: () => void applyProposal(message.proposal), onDiscard: () => dispatch({ type: 'reject', proposalId: message.proposal.id }), onOpen: () => openProposal(message.proposal) }, message.id));
|
|
200
|
+
})] }), ready ? (_jsx(PromptInput, { value: input, onChange: setInput, onSubmit: submit, disabled: state.status === 'busy' })) : (_jsx("div", { className: "border-t border-border p-3", children: _jsx(Text, { size: "xs", tone: "muted", children: translate('ai.panel.unavailable') }) }))] }));
|
|
201
|
+
}
|
|
202
|
+
//# sourceMappingURL=ai-chat-panel.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ai-chat-panel.js","sourceRoot":"","sources":["../../../src/components/ai/ai-chat-panel.tsx"],"names":[],"mappings":"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","sourcesContent":["import { useCallback, useMemo, useReducer, useState } from 'react'\nimport { X } from 'lucide-react'\nimport {\n createCollectionRegistry,\n resolveDocumentTitle,\n} from '@elytracms/core/cms-core'\nimport type { CmsDocument, FieldValues } from '@elytracms/core/cms-core'\nimport { Button, Heading, Row, Spacer, Text } from '@elytracms/studio/ui'\nimport { translate } from '@elytracms/studio/lib/i18n'\nimport { STUDIO_PROJECT_ROUTE, studioPath, useNavigate } from '@elytracms/studio/router'\nimport { resolveDeploymentAi } from '@elytracms/studio/lib/workspace'\nimport { getStudioOperations } from '@elytracms/studio/lib/operations'\nimport {\n AiRouteError,\n chatReducer,\n initialChatState,\n resolveAiConfig,\n resolveKnowledge,\n runCreateFlow,\n} from '@elytracms/studio/lib/ai'\nimport type {\n CreateFlowStep,\n CreateFlowTools,\n KnowledgeCollectionReader,\n ProposalReview,\n} from '@elytracms/studio/lib/ai'\nimport { useCms } from '../cms/cms-store'\nimport { validateComposition, validateRichTextEmbeds } from '../cms/cms-composition-validators'\nimport {\n AssistantTurn,\n Conversation,\n ConversationEmpty,\n PromptInput,\n ToolStatus,\n UserTurn,\n} from './chat-ui'\nimport { ProposalCard } from './proposal-card'\nimport { setAiPanelOpen } from '@elytracms/studio/lib/ai-panel'\n\n/**\n * The AI assistant chat panel (EC-199) — the right-hand collapsible surface in the CMS\n * area. It owns the chat state (the pure `chatReducer`), resolves the EC-331 provider\n * from `defineStudio({ ai })`, binds the narrow tools (search/list) to the operations\n * layer with the user's session, runs the creation orchestrator, and applies a\n * reviewed proposal as a DRAFT (operations upsert + a provenance-tagged version\n * snapshot). NEVER a modal (house rule). The whole module sits behind a lazy boundary\n * (`ai-chat-panel-lazy`), so an unconfigured studio pulls none of it — and no AI SDK.\n */\nexport function AiChatPanel() {\n const cms = useCms()\n const navigate = useNavigate()\n const [state, dispatch] = useReducer(chatReducer, initialChatState)\n const [input, setInput] = useState('')\n\n const projectId = cms.projectId\n const activeLocale = cms.activeLocale\n\n // Resolve the AI platform config once (presence + capability gate). The panel only\n // mounts when configured, but a misconfigured provider still fails loud here.\n const resolved = useMemo(() => resolveAiConfig(resolveDeploymentAi()), [])\n const provider = resolved.resolved?.provider ?? null\n const knowledgeSources = resolved.resolved?.knowledge ?? []\n const maxRepairAttempts = resolved.resolved?.limits?.maxRepairAttempts\n const ready = provider !== null && resolved.ok && projectId !== undefined\n\n const registry = useMemo(() => createCollectionRegistry(cms.collections), [cms.collections])\n\n /** Merge a document's non-localized + active-locale values for title/prose reads. */\n const localeValues = useCallback(\n (doc: CmsDocument): FieldValues => ({\n ...doc.values,\n ...(doc.localized?.[activeLocale] ?? {}),\n }),\n [activeLocale],\n )\n\n /** Operations-backed tool readers — client-side, with the user's own session. */\n const tools = useMemo<CreateFlowTools>(() => {\n const matches = (query: string | undefined, ...fields: (string | undefined)[]) =>\n !query || fields.some((f) => f?.toLowerCase().includes(query.toLowerCase()))\n return {\n async searchDocuments({ collection, query }) {\n if (projectId === undefined) return []\n const { operations } = await getStudioOperations()\n const result = await operations.documents.list({ projectId, collection })\n if (!result.ok) return []\n const target = cms.getCollection(collection)\n return result.value\n .map((rec) => ({\n id: rec.document.id,\n title: target\n ? resolveDocumentTitle(target, localeValues(rec.document), rec.document.id)\n : rec.document.id,\n }))\n .filter((r) => matches(query, r.title, r.id))\n },\n async listAssets({ query }) {\n if (projectId === undefined) return []\n const { operations } = await getStudioOperations()\n const result = await operations.assets.list({ projectId })\n if (!result.ok) return []\n return result.value\n .map((asset) => ({ id: asset.id, title: asset.title ?? asset.filename }))\n .filter((r) => matches(query, r.title, r.id))\n },\n }\n }, [projectId, cms, localeValues])\n\n /** A fresh, collision-free draft id for the target collection. */\n const mintDocumentId = useCallback(\n (collectionId: string): string => {\n const existing = new Set(cms.documentsOf(collectionId).map((d) => d.id))\n let n = 1\n while (existing.has(`new-${collectionId}-${n}`)) n += 1\n return `new-${collectionId}-${n}`\n },\n [cms],\n )\n\n /** Read `fromCollection(...)` knowledge prose through the operations layer. */\n const readCollection = useCallback<KnowledgeCollectionReader>(\n async (source) => {\n if (projectId === undefined) return []\n const { operations } = await getStudioOperations()\n const result = await operations.documents.list({ projectId, collection: source.collectionId })\n if (!result.ok) return []\n const target = cms.getCollection(source.collectionId)\n const field =\n source.field ?? target?.fields.find((f) => f.type === 'text' || f.type === 'richText')?.name\n return result.value\n .map((rec) => {\n const values = localeValues(rec.document)\n const content = field ? String(values[field] ?? '') : ''\n return { content }\n })\n .filter((item) => item.content.trim().length > 0)\n },\n [projectId, cms, localeValues],\n )\n\n const runTurn = useCallback(\n async (text: string) => {\n if (!provider) return\n try {\n const knowledge = await resolveKnowledge(knowledgeSources, { readCollection })\n const result = await runCreateFlow({\n provider,\n tools,\n registry,\n collections: cms.collections,\n instruction: text,\n knowledge,\n locale: activeLocale,\n mintDocumentId,\n validateRichTextEmbeds,\n validateComposition,\n ...(maxRepairAttempts !== undefined ? { maxRepairAttempts } : {}),\n onStep: (step: CreateFlowStep) => dispatch({ type: 'step', step }),\n })\n dispatch({ type: 'result', result })\n } catch (error) {\n const message =\n error instanceof AiRouteError\n ? translate('ai.error.route')\n : translate('ai.error.generic')\n dispatch({ type: 'fail', message })\n }\n },\n [\n provider,\n knowledgeSources,\n readCollection,\n tools,\n registry,\n cms.collections,\n activeLocale,\n mintDocumentId,\n maxRepairAttempts,\n ],\n )\n\n const submit = useCallback(() => {\n const text = input.trim()\n if (text.length === 0 || state.status === 'busy' || !ready) return\n setInput('')\n dispatch({ type: 'submit', text })\n void runTurn(text)\n }, [input, state.status, ready, runTurn])\n\n const applyProposal = useCallback(\n async (proposal: ProposalReview) => {\n if (projectId === undefined) return\n const doc: CmsDocument = {\n collection: proposal.collection,\n id: proposal.documentId,\n values: proposal.patch.values,\n ...(proposal.patch.localized ? { localized: proposal.patch.localized } : {}),\n }\n const { operations } = await getStudioOperations()\n const upserted = await operations.documents.upsert({ projectId, document: doc })\n if (!upserted.ok) {\n dispatch({ type: 'fail', message: translate('ai.error.applyFailed') })\n return\n }\n // Provenance: record an AI-tagged version snapshot (a restore point + the\n // 'assistant' origin surfaced in the versions pane). Best-effort — the draft\n // already landed above; a snapshot failure must not lose the applied content.\n try {\n await operations.documents.snapshot({\n projectId,\n ref: { collection: proposal.collection, id: proposal.documentId },\n label: translate('cms.version.aiAuthoredTitle'),\n origin: 'assistant',\n })\n } catch {\n // ignore — provenance is additive\n }\n // Reflect in the session so the new draft shows in the list + is navigable.\n cms.saveDocument(doc)\n dispatch({ type: 'apply', proposalId: proposal.id })\n },\n [projectId, cms],\n )\n\n const openProposal = useCallback(\n (proposal: ProposalReview) => {\n if (projectId === undefined) return\n void navigate({\n to: STUDIO_PROJECT_ROUTE,\n params: { projectId, _splat: studioPath.cmsDocument(proposal.collection, proposal.documentId) },\n })\n },\n [navigate, projectId],\n )\n\n return (\n <aside className=\"flex h-full min-h-0 w-[360px] shrink-0 flex-col border-l border-border bg-card\">\n <Row gap=\"2\" align=\"center\" className=\"h-12 shrink-0 border-b border-border px-3\">\n <Heading level={2} size=\"h4\">\n {translate('ai.panel.title')}\n </Heading>\n <Spacer />\n <Button\n size=\"icon-sm\"\n variant=\"ghost\"\n onClick={() => setAiPanelOpen(false)}\n title={translate('ai.panel.close')}\n aria-label={translate('ai.panel.close')}\n >\n <X />\n </Button>\n </Row>\n\n <Conversation atBottomKey={state.messages}>\n {state.messages.length === 0 ? <ConversationEmpty /> : null}\n {state.messages.map((message) => {\n if (message.role === 'user') return <UserTurn key={message.id} text={message.text} />\n if (message.kind === 'text')\n return (\n <AssistantTurn key={message.id}>\n <Text size=\"sm\" className=\"whitespace-pre-wrap break-words\">\n {message.text}\n </Text>\n </AssistantTurn>\n )\n if (message.kind === 'activity')\n return (\n <AssistantTurn key={message.id}>\n <ToolStatus\n steps={message.steps}\n active={state.status === 'busy' && state.activityId === message.id}\n />\n </AssistantTurn>\n )\n return (\n <ProposalCard\n key={message.id}\n proposal={message.proposal}\n onApply={() => void applyProposal(message.proposal)}\n onDiscard={() => dispatch({ type: 'reject', proposalId: message.proposal.id })}\n onOpen={() => openProposal(message.proposal)}\n />\n )\n })}\n </Conversation>\n\n {ready ? (\n <PromptInput value={input} onChange={setInput} onSubmit={submit} disabled={state.status === 'busy'} />\n ) : (\n <div className=\"border-t border-border p-3\">\n <Text size=\"xs\" tone=\"muted\">\n {translate('ai.panel.unavailable')}\n </Text>\n </div>\n )}\n </aside>\n )\n}\n"]}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The CMS-area mount of the AI assistant (EC-199): a right-hand sibling of the CMS
|
|
3
|
+
* content, shown on BOTH the collection list and the document editor. Gated twice —
|
|
4
|
+
* the studio must be `ai`-configured (EC-331 presence-enable) AND the panel open — so
|
|
5
|
+
* the lazy `AiChatPanel` chunk is requested only when a configured studio actually
|
|
6
|
+
* opens it. Sits inside `CmsProvider`, so the panel reads the live CMS store
|
|
7
|
+
* (collections, active locale) for its tools and applies drafts through it.
|
|
8
|
+
*/
|
|
9
|
+
export declare function AiCmsPanel(): import("react").JSX.Element | null;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useMemo } from 'react';
|
|
3
|
+
import { resolveDeploymentAi } from '@elytracms/studio/lib/workspace';
|
|
4
|
+
import { useAiPanelOpen } from '@elytracms/studio/lib/ai-panel';
|
|
5
|
+
import { AiChatPanel } from './ai-chat-panel-lazy';
|
|
6
|
+
/**
|
|
7
|
+
* The CMS-area mount of the AI assistant (EC-199): a right-hand sibling of the CMS
|
|
8
|
+
* content, shown on BOTH the collection list and the document editor. Gated twice —
|
|
9
|
+
* the studio must be `ai`-configured (EC-331 presence-enable) AND the panel open — so
|
|
10
|
+
* the lazy `AiChatPanel` chunk is requested only when a configured studio actually
|
|
11
|
+
* opens it. Sits inside `CmsProvider`, so the panel reads the live CMS store
|
|
12
|
+
* (collections, active locale) for its tools and applies drafts through it.
|
|
13
|
+
*/
|
|
14
|
+
export function AiCmsPanel() {
|
|
15
|
+
const open = useAiPanelOpen();
|
|
16
|
+
const configured = useMemo(() => {
|
|
17
|
+
const ai = resolveDeploymentAi();
|
|
18
|
+
return !!ai && ai.enabled !== false;
|
|
19
|
+
}, []);
|
|
20
|
+
if (!configured || !open)
|
|
21
|
+
return null;
|
|
22
|
+
return _jsx(AiChatPanel, {});
|
|
23
|
+
}
|
|
24
|
+
//# sourceMappingURL=ai-cms-panel.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ai-cms-panel.js","sourceRoot":"","sources":["../../../src/components/ai/ai-cms-panel.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,OAAO,CAAA;AAC/B,OAAO,EAAE,mBAAmB,EAAE,MAAM,iCAAiC,CAAA;AACrE,OAAO,EAAE,cAAc,EAAE,MAAM,gCAAgC,CAAA;AAC/D,OAAO,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAA;AAElD;;;;;;;GAOG;AACH,MAAM,UAAU,UAAU;IACxB,MAAM,IAAI,GAAG,cAAc,EAAE,CAAA;IAC7B,MAAM,UAAU,GAAG,OAAO,CAAC,GAAG,EAAE;QAC9B,MAAM,EAAE,GAAG,mBAAmB,EAAE,CAAA;QAChC,OAAO,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,OAAO,KAAK,KAAK,CAAA;IACrC,CAAC,EAAE,EAAE,CAAC,CAAA;IACN,IAAI,CAAC,UAAU,IAAI,CAAC,IAAI;QAAE,OAAO,IAAI,CAAA;IACrC,OAAO,KAAC,WAAW,KAAG,CAAA;AACxB,CAAC","sourcesContent":["import { useMemo } from 'react'\nimport { resolveDeploymentAi } from '@elytracms/studio/lib/workspace'\nimport { useAiPanelOpen } from '@elytracms/studio/lib/ai-panel'\nimport { AiChatPanel } from './ai-chat-panel-lazy'\n\n/**\n * The CMS-area mount of the AI assistant (EC-199): a right-hand sibling of the CMS\n * content, shown on BOTH the collection list and the document editor. Gated twice —\n * the studio must be `ai`-configured (EC-331 presence-enable) AND the panel open — so\n * the lazy `AiChatPanel` chunk is requested only when a configured studio actually\n * opens it. Sits inside `CmsProvider`, so the panel reads the live CMS store\n * (collections, active locale) for its tools and applies drafts through it.\n */\nexport function AiCmsPanel() {\n const open = useAiPanelOpen()\n const configured = useMemo(() => {\n const ai = resolveDeploymentAi()\n return !!ai && ai.enabled !== false\n }, [])\n if (!configured || !open) return null\n return <AiChatPanel />\n}\n"]}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The workspace-header toggle for the AI assistant (EC-199). Renders ONLY when the
|
|
3
|
+
* studio config carries an `ai` block that is not kill-switched (`enabled: false`) —
|
|
4
|
+
* the EC-331 presence-enable, checked here without touching the AI runtime (it reads
|
|
5
|
+
* only the config object; the heavy panel stays behind its own lazy boundary). When
|
|
6
|
+
* `ai` is absent, no toggle renders and no AI chunk is ever requested.
|
|
7
|
+
*/
|
|
8
|
+
export declare function AiPanelToggle(): import("react").JSX.Element | null;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useMemo } from 'react';
|
|
3
|
+
import { Sparkles } from 'lucide-react';
|
|
4
|
+
import { Button } from '@elytracms/studio/ui';
|
|
5
|
+
import { translate } from '@elytracms/studio/lib/i18n';
|
|
6
|
+
import { cn } from '@elytracms/studio/lib/utils';
|
|
7
|
+
import { resolveDeploymentAi } from '@elytracms/studio/lib/workspace';
|
|
8
|
+
import { toggleAiPanel, useAiPanelOpen } from '@elytracms/studio/lib/ai-panel';
|
|
9
|
+
/**
|
|
10
|
+
* The workspace-header toggle for the AI assistant (EC-199). Renders ONLY when the
|
|
11
|
+
* studio config carries an `ai` block that is not kill-switched (`enabled: false`) —
|
|
12
|
+
* the EC-331 presence-enable, checked here without touching the AI runtime (it reads
|
|
13
|
+
* only the config object; the heavy panel stays behind its own lazy boundary). When
|
|
14
|
+
* `ai` is absent, no toggle renders and no AI chunk is ever requested.
|
|
15
|
+
*/
|
|
16
|
+
export function AiPanelToggle() {
|
|
17
|
+
const open = useAiPanelOpen();
|
|
18
|
+
// Config is seeded at `createStudio` before mount, so a one-shot read is stable.
|
|
19
|
+
const configured = useMemo(() => {
|
|
20
|
+
const ai = resolveDeploymentAi();
|
|
21
|
+
return !!ai && ai.enabled !== false;
|
|
22
|
+
}, []);
|
|
23
|
+
if (!configured)
|
|
24
|
+
return null;
|
|
25
|
+
return (_jsx(Button, { size: "icon-sm", variant: open ? 'default' : 'ghost', "aria-pressed": open, onClick: toggleAiPanel, title: translate('ai.panel.toggle'), "aria-label": translate('ai.panel.toggle'), className: cn(!open && 'text-muted-foreground'), children: _jsx(Sparkles, {}) }));
|
|
26
|
+
}
|
|
27
|
+
//# sourceMappingURL=ai-panel-toggle.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ai-panel-toggle.js","sourceRoot":"","sources":["../../../src/components/ai/ai-panel-toggle.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,OAAO,CAAA;AAC/B,OAAO,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAA;AACvC,OAAO,EAAE,MAAM,EAAE,MAAM,sBAAsB,CAAA;AAC7C,OAAO,EAAE,SAAS,EAAE,MAAM,4BAA4B,CAAA;AACtD,OAAO,EAAE,EAAE,EAAE,MAAM,6BAA6B,CAAA;AAChD,OAAO,EAAE,mBAAmB,EAAE,MAAM,iCAAiC,CAAA;AACrE,OAAO,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,gCAAgC,CAAA;AAE9E;;;;;;GAMG;AACH,MAAM,UAAU,aAAa;IAC3B,MAAM,IAAI,GAAG,cAAc,EAAE,CAAA;IAC7B,iFAAiF;IACjF,MAAM,UAAU,GAAG,OAAO,CAAC,GAAG,EAAE;QAC9B,MAAM,EAAE,GAAG,mBAAmB,EAAE,CAAA;QAChC,OAAO,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,OAAO,KAAK,KAAK,CAAA;IACrC,CAAC,EAAE,EAAE,CAAC,CAAA;IACN,IAAI,CAAC,UAAU;QAAE,OAAO,IAAI,CAAA;IAC5B,OAAO,CACL,KAAC,MAAM,IACL,IAAI,EAAC,SAAS,EACd,OAAO,EAAE,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,OAAO,kBACrB,IAAI,EAClB,OAAO,EAAE,aAAa,EACtB,KAAK,EAAE,SAAS,CAAC,iBAAiB,CAAC,gBACvB,SAAS,CAAC,iBAAiB,CAAC,EACxC,SAAS,EAAE,EAAE,CAAC,CAAC,IAAI,IAAI,uBAAuB,CAAC,YAE/C,KAAC,QAAQ,KAAG,GACL,CACV,CAAA;AACH,CAAC","sourcesContent":["import { useMemo } from 'react'\nimport { Sparkles } from 'lucide-react'\nimport { Button } from '@elytracms/studio/ui'\nimport { translate } from '@elytracms/studio/lib/i18n'\nimport { cn } from '@elytracms/studio/lib/utils'\nimport { resolveDeploymentAi } from '@elytracms/studio/lib/workspace'\nimport { toggleAiPanel, useAiPanelOpen } from '@elytracms/studio/lib/ai-panel'\n\n/**\n * The workspace-header toggle for the AI assistant (EC-199). Renders ONLY when the\n * studio config carries an `ai` block that is not kill-switched (`enabled: false`) —\n * the EC-331 presence-enable, checked here without touching the AI runtime (it reads\n * only the config object; the heavy panel stays behind its own lazy boundary). When\n * `ai` is absent, no toggle renders and no AI chunk is ever requested.\n */\nexport function AiPanelToggle() {\n const open = useAiPanelOpen()\n // Config is seeded at `createStudio` before mount, so a one-shot read is stable.\n const configured = useMemo(() => {\n const ai = resolveDeploymentAi()\n return !!ai && ai.enabled !== false\n }, [])\n if (!configured) return null\n return (\n <Button\n size=\"icon-sm\"\n variant={open ? 'default' : 'ghost'}\n aria-pressed={open}\n onClick={toggleAiPanel}\n title={translate('ai.panel.toggle')}\n aria-label={translate('ai.panel.toggle')}\n className={cn(!open && 'text-muted-foreground')}\n >\n <Sparkles />\n </Button>\n )\n}\n"]}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import type { CreateFlowStep } from '@elytracms/studio/lib/ai';
|
|
3
|
+
/**
|
|
4
|
+
* AI-Elements-style chat primitives (EC-199), vendored as local shadcn-style
|
|
5
|
+
* components (named, Tailwind-token) rather than a package dependency — only the
|
|
6
|
+
* pieces this one flow needs: a scrolling conversation, user/assistant turns, the
|
|
7
|
+
* tool-status affordance, an empty-state, and the prompt input. They are purely
|
|
8
|
+
* presentational; the panel owns the state (the `chatReducer`).
|
|
9
|
+
*/
|
|
10
|
+
/** The scrolling conversation column; auto-scrolls to the newest turn. */
|
|
11
|
+
export declare function Conversation({ children, atBottomKey }: {
|
|
12
|
+
children: ReactNode;
|
|
13
|
+
atBottomKey: unknown;
|
|
14
|
+
}): import("react").JSX.Element;
|
|
15
|
+
/** A user turn — a right-aligned bubble. */
|
|
16
|
+
export declare function UserTurn({ text }: {
|
|
17
|
+
text: string;
|
|
18
|
+
}): import("react").JSX.Element;
|
|
19
|
+
/** An assistant text turn — a left-aligned calm block. */
|
|
20
|
+
export declare function AssistantTurn({ children }: {
|
|
21
|
+
children: ReactNode;
|
|
22
|
+
}): import("react").JSX.Element;
|
|
23
|
+
/** The empty-state prompt shown before the first message. */
|
|
24
|
+
export declare function ConversationEmpty(): import("react").JSX.Element;
|
|
25
|
+
/** The tool-status affordance — the tool steps the loop ran, as calm status lines. */
|
|
26
|
+
export declare function ToolStatus({ steps, active }: {
|
|
27
|
+
steps: CreateFlowStep[];
|
|
28
|
+
active: boolean;
|
|
29
|
+
}): import("react").JSX.Element | null;
|
|
30
|
+
/** The prompt input — a growing textarea + send button; Enter sends, Shift+Enter newline. */
|
|
31
|
+
export declare function PromptInput({ value, onChange, onSubmit, disabled, }: {
|
|
32
|
+
value: string;
|
|
33
|
+
onChange: (value: string) => void;
|
|
34
|
+
onSubmit: () => void;
|
|
35
|
+
disabled: boolean;
|
|
36
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useCallback, useEffect, useRef } from 'react';
|
|
3
|
+
import { FileText, Images, Search, Sparkles } from 'lucide-react';
|
|
4
|
+
import { Button, Row, Stack, Text } from '@elytracms/studio/ui';
|
|
5
|
+
import { translate } from '@elytracms/studio/lib/i18n';
|
|
6
|
+
import { cn } from '@elytracms/studio/lib/utils';
|
|
7
|
+
/**
|
|
8
|
+
* AI-Elements-style chat primitives (EC-199), vendored as local shadcn-style
|
|
9
|
+
* components (named, Tailwind-token) rather than a package dependency — only the
|
|
10
|
+
* pieces this one flow needs: a scrolling conversation, user/assistant turns, the
|
|
11
|
+
* tool-status affordance, an empty-state, and the prompt input. They are purely
|
|
12
|
+
* presentational; the panel owns the state (the `chatReducer`).
|
|
13
|
+
*/
|
|
14
|
+
/** The scrolling conversation column; auto-scrolls to the newest turn. */
|
|
15
|
+
export function Conversation({ children, atBottomKey }) {
|
|
16
|
+
const ref = useRef(null);
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
const el = ref.current;
|
|
19
|
+
if (el)
|
|
20
|
+
el.scrollTop = el.scrollHeight;
|
|
21
|
+
}, [atBottomKey]);
|
|
22
|
+
return (_jsx("div", { ref: ref, className: "min-h-0 flex-1 overflow-auto", children: _jsx(Stack, { gap: "3", className: "p-3", children: children }) }));
|
|
23
|
+
}
|
|
24
|
+
/** A user turn — a right-aligned bubble. */
|
|
25
|
+
export function UserTurn({ text }) {
|
|
26
|
+
return (_jsx(Row, { justify: "end", children: _jsx("div", { className: "max-w-[85%] rounded-2xl rounded-br-sm bg-primary px-3 py-2 text-primary-foreground", children: _jsx(Text, { size: "sm", className: "whitespace-pre-wrap break-words text-primary-foreground", children: text }) }) }));
|
|
27
|
+
}
|
|
28
|
+
/** An assistant text turn — a left-aligned calm block. */
|
|
29
|
+
export function AssistantTurn({ children }) {
|
|
30
|
+
return (_jsxs(Row, { justify: "start", align: "start", gap: "2", children: [_jsx("div", { className: "mt-0.5 grid size-6 shrink-0 place-items-center rounded-full bg-muted text-muted-foreground", children: _jsx(Sparkles, { className: "size-3.5" }) }), _jsx("div", { className: "min-w-0 flex-1", children: children })] }));
|
|
31
|
+
}
|
|
32
|
+
/** The empty-state prompt shown before the first message. */
|
|
33
|
+
export function ConversationEmpty() {
|
|
34
|
+
return (_jsxs(Stack, { gap: "2", align: "center", className: "px-4 py-10 text-center", children: [_jsx("div", { className: "grid size-10 place-items-center rounded-full bg-muted text-muted-foreground", children: _jsx(Sparkles, { className: "size-5" }) }), _jsx(Text, { size: "sm", weight: "medium", children: translate('ai.panel.emptyTitle') }), _jsx(Text, { size: "xs", tone: "muted", className: "max-w-[24rem]", children: translate('ai.panel.emptyHint') })] }));
|
|
35
|
+
}
|
|
36
|
+
/** The tool-status affordance — the tool steps the loop ran, as calm status lines. */
|
|
37
|
+
export function ToolStatus({ steps, active }) {
|
|
38
|
+
if (steps.length === 0 && !active)
|
|
39
|
+
return null;
|
|
40
|
+
return (_jsxs(Stack, { gap: "1", className: "rounded-lg border border-border/60 bg-muted/30 px-2.5 py-2", children: [steps.map((step, i) => (_jsxs(Row, { gap: "2", align: "center", children: [_jsx(StepIcon, { step: step }), _jsx(Text, { size: "xs", tone: "muted", children: stepLabel(step) })] }, i))), active ? (_jsxs(Row, { gap: "2", align: "center", children: [_jsx("span", { className: "inline-block size-1.5 animate-pulse rounded-full bg-muted-foreground", "aria-hidden": true }), _jsx(Text, { size: "xs", tone: "muted", children: translate('ai.panel.working') })] })) : null] }));
|
|
41
|
+
}
|
|
42
|
+
function StepIcon({ step }) {
|
|
43
|
+
const className = 'size-3.5 shrink-0 text-muted-foreground';
|
|
44
|
+
if (step.kind === 'search')
|
|
45
|
+
return _jsx(Search, { className: className });
|
|
46
|
+
if (step.kind === 'listAssets')
|
|
47
|
+
return _jsx(Images, { className: className });
|
|
48
|
+
return _jsx(FileText, { className: className });
|
|
49
|
+
}
|
|
50
|
+
function stepLabel(step) {
|
|
51
|
+
if (step.kind === 'search')
|
|
52
|
+
return translate('ai.step.search', { collection: step.collection, count: step.resultCount });
|
|
53
|
+
if (step.kind === 'listAssets')
|
|
54
|
+
return translate('ai.step.listAssets', { count: step.resultCount });
|
|
55
|
+
return translate('ai.step.drafting', { collection: step.collection });
|
|
56
|
+
}
|
|
57
|
+
/** The prompt input — a growing textarea + send button; Enter sends, Shift+Enter newline. */
|
|
58
|
+
export function PromptInput({ value, onChange, onSubmit, disabled, }) {
|
|
59
|
+
const handleKeyDown = useCallback((event) => {
|
|
60
|
+
if (event.key === 'Enter' && !event.shiftKey) {
|
|
61
|
+
event.preventDefault();
|
|
62
|
+
if (!disabled && value.trim().length > 0)
|
|
63
|
+
onSubmit();
|
|
64
|
+
}
|
|
65
|
+
}, [disabled, value, onSubmit]);
|
|
66
|
+
return (_jsx("div", { className: "border-t border-border p-2.5", children: _jsxs("div", { className: cn('flex items-end gap-2 rounded-xl border border-border bg-background px-2.5 py-2', 'focus-within:border-primary/60'), children: [_jsx("textarea", { value: value, onChange: (event) => onChange(event.target.value), onKeyDown: handleKeyDown, rows: 1, placeholder: translate('ai.panel.placeholder'), "aria-label": translate('ai.panel.placeholder'), className: "max-h-40 min-h-[1.5rem] flex-1 resize-none bg-transparent text-sm outline-none placeholder:text-muted-foreground" }), _jsx(Button, { size: "sm", onClick: onSubmit, disabled: disabled || value.trim().length === 0, title: translate('ai.panel.send'), "aria-label": translate('ai.panel.send'), children: translate('ai.panel.send') })] }) }));
|
|
67
|
+
}
|
|
68
|
+
//# sourceMappingURL=chat-ui.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"chat-ui.js","sourceRoot":"","sources":["../../../src/components/ai/chat-ui.tsx"],"names":[],"mappings":"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","sourcesContent":["import { useCallback, useEffect, useRef } from 'react'\nimport type { KeyboardEvent, ReactNode } from 'react'\nimport { FileText, Images, Search, Sparkles } from 'lucide-react'\nimport { Button, Row, Stack, Text } from '@elytracms/studio/ui'\nimport { translate } from '@elytracms/studio/lib/i18n'\nimport { cn } from '@elytracms/studio/lib/utils'\nimport type { CreateFlowStep } from '@elytracms/studio/lib/ai'\n\n/**\n * AI-Elements-style chat primitives (EC-199), vendored as local shadcn-style\n * components (named, Tailwind-token) rather than a package dependency — only the\n * pieces this one flow needs: a scrolling conversation, user/assistant turns, the\n * tool-status affordance, an empty-state, and the prompt input. They are purely\n * presentational; the panel owns the state (the `chatReducer`).\n */\n\n/** The scrolling conversation column; auto-scrolls to the newest turn. */\nexport function Conversation({ children, atBottomKey }: { children: ReactNode; atBottomKey: unknown }) {\n const ref = useRef<HTMLDivElement>(null)\n useEffect(() => {\n const el = ref.current\n if (el) el.scrollTop = el.scrollHeight\n }, [atBottomKey])\n return (\n <div ref={ref} className=\"min-h-0 flex-1 overflow-auto\">\n <Stack gap=\"3\" className=\"p-3\">\n {children}\n </Stack>\n </div>\n )\n}\n\n/** A user turn — a right-aligned bubble. */\nexport function UserTurn({ text }: { text: string }) {\n return (\n <Row justify=\"end\">\n <div className=\"max-w-[85%] rounded-2xl rounded-br-sm bg-primary px-3 py-2 text-primary-foreground\">\n <Text size=\"sm\" className=\"whitespace-pre-wrap break-words text-primary-foreground\">\n {text}\n </Text>\n </div>\n </Row>\n )\n}\n\n/** An assistant text turn — a left-aligned calm block. */\nexport function AssistantTurn({ children }: { children: ReactNode }) {\n return (\n <Row justify=\"start\" align=\"start\" gap=\"2\">\n <div className=\"mt-0.5 grid size-6 shrink-0 place-items-center rounded-full bg-muted text-muted-foreground\">\n <Sparkles className=\"size-3.5\" />\n </div>\n <div className=\"min-w-0 flex-1\">{children}</div>\n </Row>\n )\n}\n\n/** The empty-state prompt shown before the first message. */\nexport function ConversationEmpty() {\n return (\n <Stack gap=\"2\" align=\"center\" className=\"px-4 py-10 text-center\">\n <div className=\"grid size-10 place-items-center rounded-full bg-muted text-muted-foreground\">\n <Sparkles className=\"size-5\" />\n </div>\n <Text size=\"sm\" weight=\"medium\">\n {translate('ai.panel.emptyTitle')}\n </Text>\n <Text size=\"xs\" tone=\"muted\" className=\"max-w-[24rem]\">\n {translate('ai.panel.emptyHint')}\n </Text>\n </Stack>\n )\n}\n\n/** The tool-status affordance — the tool steps the loop ran, as calm status lines. */\nexport function ToolStatus({ steps, active }: { steps: CreateFlowStep[]; active: boolean }) {\n if (steps.length === 0 && !active) return null\n return (\n <Stack gap=\"1\" className=\"rounded-lg border border-border/60 bg-muted/30 px-2.5 py-2\">\n {steps.map((step, i) => (\n <Row key={i} gap=\"2\" align=\"center\">\n <StepIcon step={step} />\n <Text size=\"xs\" tone=\"muted\">\n {stepLabel(step)}\n </Text>\n </Row>\n ))}\n {active ? (\n <Row gap=\"2\" align=\"center\">\n <span className=\"inline-block size-1.5 animate-pulse rounded-full bg-muted-foreground\" aria-hidden />\n <Text size=\"xs\" tone=\"muted\">\n {translate('ai.panel.working')}\n </Text>\n </Row>\n ) : null}\n </Stack>\n )\n}\n\nfunction StepIcon({ step }: { step: CreateFlowStep }) {\n const className = 'size-3.5 shrink-0 text-muted-foreground'\n if (step.kind === 'search') return <Search className={className} />\n if (step.kind === 'listAssets') return <Images className={className} />\n return <FileText className={className} />\n}\n\nfunction stepLabel(step: CreateFlowStep): string {\n if (step.kind === 'search')\n return translate('ai.step.search', { collection: step.collection, count: step.resultCount })\n if (step.kind === 'listAssets') return translate('ai.step.listAssets', { count: step.resultCount })\n return translate('ai.step.drafting', { collection: step.collection })\n}\n\n/** The prompt input — a growing textarea + send button; Enter sends, Shift+Enter newline. */\nexport function PromptInput({\n value,\n onChange,\n onSubmit,\n disabled,\n}: {\n value: string\n onChange: (value: string) => void\n onSubmit: () => void\n disabled: boolean\n}) {\n const handleKeyDown = useCallback(\n (event: KeyboardEvent<HTMLTextAreaElement>) => {\n if (event.key === 'Enter' && !event.shiftKey) {\n event.preventDefault()\n if (!disabled && value.trim().length > 0) onSubmit()\n }\n },\n [disabled, value, onSubmit],\n )\n return (\n <div className=\"border-t border-border p-2.5\">\n <div\n className={cn(\n 'flex items-end gap-2 rounded-xl border border-border bg-background px-2.5 py-2',\n 'focus-within:border-primary/60',\n )}\n >\n <textarea\n value={value}\n onChange={(event) => onChange(event.target.value)}\n onKeyDown={handleKeyDown}\n rows={1}\n placeholder={translate('ai.panel.placeholder')}\n aria-label={translate('ai.panel.placeholder')}\n className=\"max-h-40 min-h-[1.5rem] flex-1 resize-none bg-transparent text-sm outline-none placeholder:text-muted-foreground\"\n />\n <Button\n size=\"sm\"\n onClick={onSubmit}\n disabled={disabled || value.trim().length === 0}\n title={translate('ai.panel.send')}\n aria-label={translate('ai.panel.send')}\n >\n {translate('ai.panel.send')}\n </Button>\n </div>\n </div>\n )\n}\n"]}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { ProposalReview } from '@elytracms/studio/lib/ai';
|
|
2
|
+
/**
|
|
3
|
+
* The AI proposal review (EC-199): a valid, leash-compliant draft the editor reviews
|
|
4
|
+
* as a DIFF (the shared EC-317 {@link DocumentDiffView} — never a second renderer)
|
|
5
|
+
* before applying it as a draft or discarding it. The diff is `empty-before` vs
|
|
6
|
+
* `proposed-after`, wrapped in a `CollectionViewProvider` for the target collection so
|
|
7
|
+
* relations/assets render as titles/thumbnails. Apply/discard are handled by the panel
|
|
8
|
+
* (the operations write + provenance snapshot happen there); this card is the surface.
|
|
9
|
+
*/
|
|
10
|
+
export declare function ProposalCard({ proposal, onApply, onDiscard, onOpen, }: {
|
|
11
|
+
proposal: ProposalReview;
|
|
12
|
+
onApply: () => void;
|
|
13
|
+
onDiscard: () => void;
|
|
14
|
+
onOpen: () => void;
|
|
15
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { useMemo } from 'react';
|
|
3
|
+
import { ArrowUpRight, Check, Sparkles, X } from 'lucide-react';
|
|
4
|
+
import { diffDocuments } from '@elytracms/core/content';
|
|
5
|
+
import { resolveLabel } from '@elytracms/core/cms-core';
|
|
6
|
+
import { Badge, Button, Row, Spacer, Stack, Text } from '@elytracms/studio/ui';
|
|
7
|
+
import { translate } from '@elytracms/studio/lib/i18n';
|
|
8
|
+
import { proposalDocuments } from '@elytracms/studio/lib/ai';
|
|
9
|
+
import { useCms } from '../cms/cms-store';
|
|
10
|
+
import { useCmsCollectionViewSource } from '../collection-view/cms-collection-view-source';
|
|
11
|
+
import { CollectionViewProvider } from '../collection-view/collection-view-context';
|
|
12
|
+
import { DocumentDiffView } from '../cms/document-diff-view';
|
|
13
|
+
/**
|
|
14
|
+
* The AI proposal review (EC-199): a valid, leash-compliant draft the editor reviews
|
|
15
|
+
* as a DIFF (the shared EC-317 {@link DocumentDiffView} — never a second renderer)
|
|
16
|
+
* before applying it as a draft or discarding it. The diff is `empty-before` vs
|
|
17
|
+
* `proposed-after`, wrapped in a `CollectionViewProvider` for the target collection so
|
|
18
|
+
* relations/assets render as titles/thumbnails. Apply/discard are handled by the panel
|
|
19
|
+
* (the operations write + provenance snapshot happen there); this card is the surface.
|
|
20
|
+
*/
|
|
21
|
+
export function ProposalCard({ proposal, onApply, onDiscard, onOpen, }) {
|
|
22
|
+
const cms = useCms();
|
|
23
|
+
const collection = cms.getCollection(proposal.collection);
|
|
24
|
+
const source = useCmsCollectionViewSource(proposal.collection);
|
|
25
|
+
const diff = useMemo(() => {
|
|
26
|
+
if (!collection)
|
|
27
|
+
return null;
|
|
28
|
+
const { before, after } = proposalDocuments(proposal.patch);
|
|
29
|
+
return diffDocuments(collection, before, after);
|
|
30
|
+
}, [collection, proposal.patch]);
|
|
31
|
+
const collectionLabel = (collection?.form?.labelSingular && resolveLabel(collection.form.labelSingular)) ??
|
|
32
|
+
(collection?.form?.label && resolveLabel(collection.form.label)) ??
|
|
33
|
+
proposal.collection;
|
|
34
|
+
const applied = proposal.status === 'applied';
|
|
35
|
+
const rejected = proposal.status === 'rejected';
|
|
36
|
+
return (_jsxs(Stack, { gap: "2", className: "overflow-hidden rounded-xl border border-border bg-card", children: [_jsxs(Row, { gap: "2", align: "center", className: "border-b border-border/60 bg-muted/30 px-3 py-2", children: [_jsx(Sparkles, { className: "size-4 shrink-0 text-primary" }), _jsx(Text, { size: "sm", weight: "medium", className: "min-w-0 truncate", children: translate('ai.proposal.title', { collection: collectionLabel }) }), _jsx(Spacer, {}), applied ? _jsx(Badge, { variant: "success", children: translate('ai.proposal.applied') }) : null, rejected ? _jsx(Badge, { variant: "outline", children: translate('ai.proposal.discarded') }) : null] }), _jsx("div", { className: "px-3", children: diff && source ? (_jsx(CollectionViewProvider, { source: source, children: _jsx(DocumentDiffView, { diff: diff, collection: collection, columnHeaders: {
|
|
37
|
+
unified: translate('ai.proposal.diffUnified'),
|
|
38
|
+
before: translate('ai.proposal.diffBefore'),
|
|
39
|
+
after: translate('ai.proposal.diffAfter'),
|
|
40
|
+
} }) })) : (_jsx(Text, { size: "xs", tone: "muted", className: "py-2", children: translate('ai.proposal.unavailable') })) }), _jsx(Row, { gap: "2", align: "center", className: "border-t border-border/60 px-3 py-2", children: proposal.status === 'pending' ? (_jsxs(_Fragment, { children: [_jsx(Text, { size: "xs", tone: "muted", className: "min-w-0 flex-1", children: translate('ai.proposal.reviewHint') }), _jsxs(Button, { size: "sm", variant: "ghost", onClick: onDiscard, children: [_jsx(X, {}), " ", translate('ai.proposal.discard')] }), _jsxs(Button, { size: "sm", onClick: onApply, children: [_jsx(Check, {}), " ", translate('ai.proposal.apply')] })] })) : applied ? (_jsxs(_Fragment, { children: [_jsx(Spacer, {}), _jsxs(Button, { size: "sm", variant: "outline", onClick: onOpen, children: [_jsx(ArrowUpRight, {}), " ", translate('ai.proposal.open')] })] })) : (_jsx(Text, { size: "xs", tone: "muted", children: translate('ai.proposal.discardedNote') })) })] }));
|
|
41
|
+
}
|
|
42
|
+
//# sourceMappingURL=proposal-card.js.map
|