@elytracms/studio 0.0.46 → 0.0.47

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.
Files changed (37) hide show
  1. package/dist/components/ai/ai-chat-panel.js +9 -0
  2. package/dist/components/ai/ai-chat-panel.js.map +1 -1
  3. package/dist/components/ai/chat-ui.js +5 -1
  4. package/dist/components/ai/chat-ui.js.map +1 -1
  5. package/dist/components/ai/proposal-card.js +12 -5
  6. package/dist/components/ai/proposal-card.js.map +1 -1
  7. package/dist/components/ai/translate-document-action.d.ts +14 -0
  8. package/dist/components/ai/translate-document-action.js +38 -0
  9. package/dist/components/ai/translate-document-action.js.map +1 -0
  10. package/dist/components/ai/translate-document-dialog.d.ts +5 -0
  11. package/dist/components/ai/translate-document-dialog.js +97 -0
  12. package/dist/components/ai/translate-document-dialog.js.map +1 -0
  13. package/dist/components/cms/detail-editor.js +5 -1
  14. package/dist/components/cms/detail-editor.js.map +1 -1
  15. package/dist/lib/ai/chat-state.d.ts +9 -1
  16. package/dist/lib/ai/chat-state.js +2 -0
  17. package/dist/lib/ai/chat-state.js.map +1 -1
  18. package/dist/lib/ai/create-flow.d.ts +17 -1
  19. package/dist/lib/ai/create-flow.js +86 -0
  20. package/dist/lib/ai/create-flow.js.map +1 -1
  21. package/dist/lib/ai/index.d.ts +2 -0
  22. package/dist/lib/ai/index.js +2 -0
  23. package/dist/lib/ai/index.js.map +1 -1
  24. package/dist/lib/ai/rich-text-translate.d.ts +45 -0
  25. package/dist/lib/ai/rich-text-translate.js +229 -0
  26. package/dist/lib/ai/rich-text-translate.js.map +1 -0
  27. package/dist/lib/ai/tools.d.ts +13 -1
  28. package/dist/lib/ai/tools.js +15 -4
  29. package/dist/lib/ai/tools.js.map +1 -1
  30. package/dist/lib/ai/translate-document.d.ts +53 -0
  31. package/dist/lib/ai/translate-document.js +316 -0
  32. package/dist/lib/ai/translate-document.js.map +1 -0
  33. package/dist/lib/cms-views/view-model.d.ts +3 -3
  34. package/dist/lib/i18n/messages.en.js +23 -0
  35. package/dist/lib/i18n/messages.en.js.map +1 -1
  36. package/dist/ui/badge.d.ts +1 -1
  37. package/package.json +2 -2
@@ -74,6 +74,14 @@ export function AiChatPanel() {
74
74
  .map((asset) => ({ id: asset.id, title: asset.title ?? asset.filename }))
75
75
  .filter((r) => matches(query, r.title, r.id));
76
76
  },
77
+ // EC-332: the translate flow loads a full document (draft values + localized).
78
+ async getDocument({ collection, id }) {
79
+ if (projectId === undefined)
80
+ return null;
81
+ const { operations } = await getStudioOperations();
82
+ const result = await operations.documents.get({ projectId, ref: { collection, id } });
83
+ return result.ok ? result.value?.document ?? null : null;
84
+ },
77
85
  };
78
86
  }, [projectId, cms, localeValues]);
79
87
  /** A fresh, collision-free draft id for the target collection. */
@@ -115,6 +123,7 @@ export function AiChatPanel() {
115
123
  instruction: text,
116
124
  knowledge,
117
125
  locale: activeLocale,
126
+ localeConfig: cms.localeConfig,
118
127
  mintDocumentId,
119
128
  validateRichTextEmbeds,
120
129
  validateComposition,
@@ -1 +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"]}
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 // EC-332: the translate flow loads a full document (draft values + localized).\n async getDocument({ collection, id }) {\n if (projectId === undefined) return null\n const { operations } = await getStudioOperations()\n const result = await operations.documents.get({ projectId, ref: { collection, id } })\n return result.ok ? result.value?.document ?? null : null\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 localeConfig: cms.localeConfig,\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"]}
@@ -1,6 +1,6 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useCallback, useEffect, useRef } from 'react';
3
- import { FileText, Images, Search, Sparkles } from 'lucide-react';
3
+ import { FileText, Images, Languages, Search, Sparkles } from 'lucide-react';
4
4
  import { Button, Row, Stack, Text } from '@elytracms/studio/ui';
5
5
  import { translate } from '@elytracms/studio/lib/i18n';
6
6
  import { cn } from '@elytracms/studio/lib/utils';
@@ -45,6 +45,8 @@ function StepIcon({ step }) {
45
45
  return _jsx(Search, { className: className });
46
46
  if (step.kind === 'listAssets')
47
47
  return _jsx(Images, { className: className });
48
+ if (step.kind === 'translating')
49
+ return _jsx(Languages, { className: className });
48
50
  return _jsx(FileText, { className: className });
49
51
  }
50
52
  function stepLabel(step) {
@@ -52,6 +54,8 @@ function stepLabel(step) {
52
54
  return translate('ai.step.search', { collection: step.collection, count: step.resultCount });
53
55
  if (step.kind === 'listAssets')
54
56
  return translate('ai.step.listAssets', { count: step.resultCount });
57
+ if (step.kind === 'translating')
58
+ return translate('ai.step.translating', { completed: step.completed, total: step.total });
55
59
  return translate('ai.step.drafting', { collection: step.collection });
56
60
  }
57
61
  /** The prompt input — a growing textarea + send button; Enter sends, Shift+Enter newline. */
@@ -1 +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"]}
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, Languages, 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 if (step.kind === 'translating') return <Languages 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 if (step.kind === 'translating')\n return translate('ai.step.translating', { completed: step.completed, total: step.total })\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"]}
@@ -25,18 +25,25 @@ export function ProposalCard({ proposal, onApply, onDiscard, onOpen, }) {
25
25
  const diff = useMemo(() => {
26
26
  if (!collection)
27
27
  return null;
28
- const { before, after } = proposalDocuments(proposal.patch);
28
+ const { before, after } = proposalDocuments(proposal.patch, proposal.before);
29
29
  return diffDocuments(collection, before, after);
30
- }, [collection, proposal.patch]);
30
+ }, [collection, proposal.patch, proposal.before]);
31
+ const isTranslation = proposal.origin === 'translate';
31
32
  const collectionLabel = (collection?.form?.labelSingular && resolveLabel(collection.form.labelSingular)) ??
32
33
  (collection?.form?.label && resolveLabel(collection.form.label)) ??
33
34
  proposal.collection;
34
35
  const applied = proposal.status === 'applied';
35
36
  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
+ 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: isTranslation
38
+ ? translate('ai.translate.proposalTitle', { collection: collectionLabel })
39
+ : 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
40
  unified: translate('ai.proposal.diffUnified'),
38
- before: translate('ai.proposal.diffBefore'),
39
- after: translate('ai.proposal.diffAfter'),
41
+ before: isTranslation
42
+ ? translate('ai.translate.diffBefore')
43
+ : translate('ai.proposal.diffBefore'),
44
+ after: isTranslation
45
+ ? translate('ai.translate.diffAfter')
46
+ : translate('ai.proposal.diffAfter'),
40
47
  } }) })) : (_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
48
  }
42
49
  //# sourceMappingURL=proposal-card.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"proposal-card.js","sourceRoot":"","sources":["../../../src/components/ai/proposal-card.tsx"],"names":[],"mappings":"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","sourcesContent":["import { useMemo } from 'react'\nimport { ArrowUpRight, Check, Sparkles, X } from 'lucide-react'\nimport { diffDocuments } from '@elytracms/core/content'\nimport { resolveLabel } from '@elytracms/core/cms-core'\nimport { Badge, Button, Row, Spacer, Stack, Text } from '@elytracms/studio/ui'\nimport { translate } from '@elytracms/studio/lib/i18n'\nimport { proposalDocuments } from '@elytracms/studio/lib/ai'\nimport type { ProposalReview } from '@elytracms/studio/lib/ai'\nimport { useCms } from '../cms/cms-store'\nimport { useCmsCollectionViewSource } from '../collection-view/cms-collection-view-source'\nimport { CollectionViewProvider } from '../collection-view/collection-view-context'\nimport { DocumentDiffView } from '../cms/document-diff-view'\n\n/**\n * The AI proposal review (EC-199): a valid, leash-compliant draft the editor reviews\n * as a DIFF (the shared EC-317 {@link DocumentDiffView} — never a second renderer)\n * before applying it as a draft or discarding it. The diff is `empty-before` vs\n * `proposed-after`, wrapped in a `CollectionViewProvider` for the target collection so\n * relations/assets render as titles/thumbnails. Apply/discard are handled by the panel\n * (the operations write + provenance snapshot happen there); this card is the surface.\n */\nexport function ProposalCard({\n proposal,\n onApply,\n onDiscard,\n onOpen,\n}: {\n proposal: ProposalReview\n onApply: () => void\n onDiscard: () => void\n onOpen: () => void\n}) {\n const cms = useCms()\n const collection = cms.getCollection(proposal.collection)\n const source = useCmsCollectionViewSource(proposal.collection)\n\n const diff = useMemo(() => {\n if (!collection) return null\n const { before, after } = proposalDocuments(proposal.patch)\n return diffDocuments(collection, before, after)\n }, [collection, proposal.patch])\n\n const collectionLabel =\n (collection?.form?.labelSingular && resolveLabel(collection.form.labelSingular)) ??\n (collection?.form?.label && resolveLabel(collection.form.label)) ??\n proposal.collection\n\n const applied = proposal.status === 'applied'\n const rejected = proposal.status === 'rejected'\n\n return (\n <Stack\n gap=\"2\"\n className=\"overflow-hidden rounded-xl border border-border bg-card\"\n >\n <Row gap=\"2\" align=\"center\" className=\"border-b border-border/60 bg-muted/30 px-3 py-2\">\n <Sparkles className=\"size-4 shrink-0 text-primary\" />\n <Text size=\"sm\" weight=\"medium\" className=\"min-w-0 truncate\">\n {translate('ai.proposal.title', { collection: collectionLabel })}\n </Text>\n <Spacer />\n {applied ? <Badge variant=\"success\">{translate('ai.proposal.applied')}</Badge> : null}\n {rejected ? <Badge variant=\"outline\">{translate('ai.proposal.discarded')}</Badge> : null}\n </Row>\n\n <div className=\"px-3\">\n {diff && source ? (\n <CollectionViewProvider source={source}>\n <DocumentDiffView\n diff={diff}\n collection={collection!}\n columnHeaders={{\n unified: translate('ai.proposal.diffUnified'),\n before: translate('ai.proposal.diffBefore'),\n after: translate('ai.proposal.diffAfter'),\n }}\n />\n </CollectionViewProvider>\n ) : (\n <Text size=\"xs\" tone=\"muted\" className=\"py-2\">\n {translate('ai.proposal.unavailable')}\n </Text>\n )}\n </div>\n\n <Row gap=\"2\" align=\"center\" className=\"border-t border-border/60 px-3 py-2\">\n {proposal.status === 'pending' ? (\n <>\n <Text size=\"xs\" tone=\"muted\" className=\"min-w-0 flex-1\">\n {translate('ai.proposal.reviewHint')}\n </Text>\n <Button size=\"sm\" variant=\"ghost\" onClick={onDiscard}>\n <X /> {translate('ai.proposal.discard')}\n </Button>\n <Button size=\"sm\" onClick={onApply}>\n <Check /> {translate('ai.proposal.apply')}\n </Button>\n </>\n ) : applied ? (\n <>\n <Spacer />\n <Button size=\"sm\" variant=\"outline\" onClick={onOpen}>\n <ArrowUpRight /> {translate('ai.proposal.open')}\n </Button>\n </>\n ) : (\n <Text size=\"xs\" tone=\"muted\">\n {translate('ai.proposal.discardedNote')}\n </Text>\n )}\n </Row>\n </Stack>\n )\n}\n"]}
1
+ {"version":3,"file":"proposal-card.js","sourceRoot":"","sources":["../../../src/components/ai/proposal-card.tsx"],"names":[],"mappings":"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","sourcesContent":["import { useMemo } from 'react'\nimport { ArrowUpRight, Check, Sparkles, X } from 'lucide-react'\nimport { diffDocuments } from '@elytracms/core/content'\nimport { resolveLabel } from '@elytracms/core/cms-core'\nimport { Badge, Button, Row, Spacer, Stack, Text } from '@elytracms/studio/ui'\nimport { translate } from '@elytracms/studio/lib/i18n'\nimport { proposalDocuments } from '@elytracms/studio/lib/ai'\nimport type { ProposalReview } from '@elytracms/studio/lib/ai'\nimport { useCms } from '../cms/cms-store'\nimport { useCmsCollectionViewSource } from '../collection-view/cms-collection-view-source'\nimport { CollectionViewProvider } from '../collection-view/collection-view-context'\nimport { DocumentDiffView } from '../cms/document-diff-view'\n\n/**\n * The AI proposal review (EC-199): a valid, leash-compliant draft the editor reviews\n * as a DIFF (the shared EC-317 {@link DocumentDiffView} — never a second renderer)\n * before applying it as a draft or discarding it. The diff is `empty-before` vs\n * `proposed-after`, wrapped in a `CollectionViewProvider` for the target collection so\n * relations/assets render as titles/thumbnails. Apply/discard are handled by the panel\n * (the operations write + provenance snapshot happen there); this card is the surface.\n */\nexport function ProposalCard({\n proposal,\n onApply,\n onDiscard,\n onOpen,\n}: {\n proposal: ProposalReview\n onApply: () => void\n onDiscard: () => void\n onOpen: () => void\n}) {\n const cms = useCms()\n const collection = cms.getCollection(proposal.collection)\n const source = useCmsCollectionViewSource(proposal.collection)\n\n const diff = useMemo(() => {\n if (!collection) return null\n const { before, after } = proposalDocuments(proposal.patch, proposal.before)\n return diffDocuments(collection, before, after)\n }, [collection, proposal.patch, proposal.before])\n\n const isTranslation = proposal.origin === 'translate'\n\n const collectionLabel =\n (collection?.form?.labelSingular && resolveLabel(collection.form.labelSingular)) ??\n (collection?.form?.label && resolveLabel(collection.form.label)) ??\n proposal.collection\n\n const applied = proposal.status === 'applied'\n const rejected = proposal.status === 'rejected'\n\n return (\n <Stack\n gap=\"2\"\n className=\"overflow-hidden rounded-xl border border-border bg-card\"\n >\n <Row gap=\"2\" align=\"center\" className=\"border-b border-border/60 bg-muted/30 px-3 py-2\">\n <Sparkles className=\"size-4 shrink-0 text-primary\" />\n <Text size=\"sm\" weight=\"medium\" className=\"min-w-0 truncate\">\n {isTranslation\n ? translate('ai.translate.proposalTitle', { collection: collectionLabel })\n : translate('ai.proposal.title', { collection: collectionLabel })}\n </Text>\n <Spacer />\n {applied ? <Badge variant=\"success\">{translate('ai.proposal.applied')}</Badge> : null}\n {rejected ? <Badge variant=\"outline\">{translate('ai.proposal.discarded')}</Badge> : null}\n </Row>\n\n <div className=\"px-3\">\n {diff && source ? (\n <CollectionViewProvider source={source}>\n <DocumentDiffView\n diff={diff}\n collection={collection!}\n columnHeaders={{\n unified: translate('ai.proposal.diffUnified'),\n before: isTranslation\n ? translate('ai.translate.diffBefore')\n : translate('ai.proposal.diffBefore'),\n after: isTranslation\n ? translate('ai.translate.diffAfter')\n : translate('ai.proposal.diffAfter'),\n }}\n />\n </CollectionViewProvider>\n ) : (\n <Text size=\"xs\" tone=\"muted\" className=\"py-2\">\n {translate('ai.proposal.unavailable')}\n </Text>\n )}\n </div>\n\n <Row gap=\"2\" align=\"center\" className=\"border-t border-border/60 px-3 py-2\">\n {proposal.status === 'pending' ? (\n <>\n <Text size=\"xs\" tone=\"muted\" className=\"min-w-0 flex-1\">\n {translate('ai.proposal.reviewHint')}\n </Text>\n <Button size=\"sm\" variant=\"ghost\" onClick={onDiscard}>\n <X /> {translate('ai.proposal.discard')}\n </Button>\n <Button size=\"sm\" onClick={onApply}>\n <Check /> {translate('ai.proposal.apply')}\n </Button>\n </>\n ) : applied ? (\n <>\n <Spacer />\n <Button size=\"sm\" variant=\"outline\" onClick={onOpen}>\n <ArrowUpRight /> {translate('ai.proposal.open')}\n </Button>\n </>\n ) : (\n <Text size=\"xs\" tone=\"muted\">\n {translate('ai.proposal.discardedNote')}\n </Text>\n )}\n </Row>\n </Stack>\n )\n}\n"]}
@@ -0,0 +1,14 @@
1
+ import type { CmsDocument, CollectionDef, DocumentRef, LocaleConfig } from '@elytracms/core/cms-core';
2
+ export interface TranslateDocumentActionProps {
3
+ projectId: string | undefined;
4
+ collection: CollectionDef;
5
+ docRef: DocumentRef;
6
+ /** The live editor draft — the translation source + the target for the fills. */
7
+ document: CmsDocument;
8
+ localeConfig: LocaleConfig;
9
+ /** Whether the collection view exposes per-locale editing (`source.capabilities.locales`). */
10
+ localesEnabled: boolean;
11
+ /** Reflect the translated draft into the open editor (save + replaceDraft). */
12
+ onApplied: (next: CmsDocument) => void;
13
+ }
14
+ export declare function TranslateDocumentAction(props: TranslateDocumentActionProps): import("react").JSX.Element | null;
@@ -0,0 +1,38 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { Suspense, lazy, useMemo, useState } from 'react';
3
+ import { Languages } from 'lucide-react';
4
+ import { isFieldLocalized } from '@elytracms/core/cms-core';
5
+ import { Button } from '@elytracms/studio/ui';
6
+ import { translate } from '@elytracms/studio/lib/i18n';
7
+ import { resolveDeploymentAi } from '@elytracms/studio/lib/workspace';
8
+ /**
9
+ * The "Translate document" header action (EC-332), gate side.
10
+ *
11
+ * Rendered ALWAYS in the detail-editor header but returns null unless the studio is
12
+ * `ai`-configured (EC-331 presence-enable), the collection is localized with a real
13
+ * localized field, and there is a target locale to fill. Like the AI panel toggle, the
14
+ * gate reads only the config object + pure core helpers — it never statically imports
15
+ * the AI engine (`@elytracms/studio/lib/ai`) or the AI SDK. The heavy dialog (which runs
16
+ * the translation engine + operations write) loads ONLY when the editor opens it, via a
17
+ * dynamic `import()`, so an unconfigured studio pulls none of it onto the critical path.
18
+ */
19
+ const TranslateDocumentDialog = lazy(() => import('./translate-document-dialog').then((m) => ({ default: m.TranslateDocumentDialog })));
20
+ export function TranslateDocumentAction(props) {
21
+ const [open, setOpen] = useState(false);
22
+ const { collection, localeConfig, localesEnabled, projectId } = props;
23
+ const eligible = useMemo(() => {
24
+ const ai = resolveDeploymentAi();
25
+ if (!ai || ai.enabled === false)
26
+ return false;
27
+ if (!localesEnabled || projectId === undefined)
28
+ return false;
29
+ const base = localeConfig.default;
30
+ if (!localeConfig.locales.some((l) => l !== base))
31
+ return false;
32
+ return collection.fields.some((f) => isFieldLocalized(collection, f.name));
33
+ }, [collection, localeConfig, localesEnabled, projectId]);
34
+ if (!eligible)
35
+ return null;
36
+ return (_jsxs(_Fragment, { children: [_jsxs(Button, { size: "sm", variant: "outline", onClick: () => setOpen(true), "data-testid": "doc-translate", title: translate('ai.translate.tooltip'), children: [_jsx(Languages, {}), " ", translate('ai.translate.action')] }), open ? (_jsx(Suspense, { fallback: null, children: _jsx(TranslateDocumentDialog, { ...props, open: true, onOpenChange: setOpen }) })) : null] }));
37
+ }
38
+ //# sourceMappingURL=translate-document-action.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"translate-document-action.js","sourceRoot":"","sources":["../../../src/components/ai/translate-document-action.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAA;AACzD,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAA;AACxC,OAAO,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAA;AAE3D,OAAO,EAAE,MAAM,EAAE,MAAM,sBAAsB,CAAA;AAC7C,OAAO,EAAE,SAAS,EAAE,MAAM,4BAA4B,CAAA;AACtD,OAAO,EAAE,mBAAmB,EAAE,MAAM,iCAAiC,CAAA;AAErE;;;;;;;;;;GAUG;AACH,MAAM,uBAAuB,GAAG,IAAI,CAAC,GAAG,EAAE,CACxC,MAAM,CAAC,6BAA6B,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,CAAC,uBAAuB,EAAE,CAAC,CAAC,CAC5F,CAAA;AAeD,MAAM,UAAU,uBAAuB,CAAC,KAAmC;IACzE,MAAM,CAAC,IAAI,EAAE,OAAO,CAAC,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAA;IACvC,MAAM,EAAE,UAAU,EAAE,YAAY,EAAE,cAAc,EAAE,SAAS,EAAE,GAAG,KAAK,CAAA;IAErE,MAAM,QAAQ,GAAG,OAAO,CAAC,GAAG,EAAE;QAC5B,MAAM,EAAE,GAAG,mBAAmB,EAAE,CAAA;QAChC,IAAI,CAAC,EAAE,IAAI,EAAE,CAAC,OAAO,KAAK,KAAK;YAAE,OAAO,KAAK,CAAA;QAC7C,IAAI,CAAC,cAAc,IAAI,SAAS,KAAK,SAAS;YAAE,OAAO,KAAK,CAAA;QAC5D,MAAM,IAAI,GAAG,YAAY,CAAC,OAAO,CAAA;QACjC,IAAI,CAAC,YAAY,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,KAAK,IAAI,CAAC;YAAE,OAAO,KAAK,CAAA;QAC/D,OAAO,UAAU,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,gBAAgB,CAAC,UAAU,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,CAAA;IAC5E,CAAC,EAAE,CAAC,UAAU,EAAE,YAAY,EAAE,cAAc,EAAE,SAAS,CAAC,CAAC,CAAA;IAEzD,IAAI,CAAC,QAAQ;QAAE,OAAO,IAAI,CAAA;IAE1B,OAAO,CACL,8BACE,MAAC,MAAM,IACL,IAAI,EAAC,IAAI,EACT,OAAO,EAAC,SAAS,EACjB,OAAO,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,iBAChB,eAAe,EAC3B,KAAK,EAAE,SAAS,CAAC,sBAAsB,CAAC,aAExC,KAAC,SAAS,KAAG,OAAE,SAAS,CAAC,qBAAqB,CAAC,IACxC,EACR,IAAI,CAAC,CAAC,CAAC,CACN,KAAC,QAAQ,IAAC,QAAQ,EAAE,IAAI,YACtB,KAAC,uBAAuB,OAAK,KAAK,EAAE,IAAI,QAAC,YAAY,EAAE,OAAO,GAAI,GACzD,CACZ,CAAC,CAAC,CAAC,IAAI,IACP,CACJ,CAAA;AACH,CAAC","sourcesContent":["import { Suspense, lazy, useMemo, useState } from 'react'\nimport { Languages } from 'lucide-react'\nimport { isFieldLocalized } from '@elytracms/core/cms-core'\nimport type { CmsDocument, CollectionDef, DocumentRef, LocaleConfig } from '@elytracms/core/cms-core'\nimport { Button } from '@elytracms/studio/ui'\nimport { translate } from '@elytracms/studio/lib/i18n'\nimport { resolveDeploymentAi } from '@elytracms/studio/lib/workspace'\n\n/**\n * The \"Translate document\" header action (EC-332), gate side.\n *\n * Rendered ALWAYS in the detail-editor header but returns null unless the studio is\n * `ai`-configured (EC-331 presence-enable), the collection is localized with a real\n * localized field, and there is a target locale to fill. Like the AI panel toggle, the\n * gate reads only the config object + pure core helpers — it never statically imports\n * the AI engine (`@elytracms/studio/lib/ai`) or the AI SDK. The heavy dialog (which runs\n * the translation engine + operations write) loads ONLY when the editor opens it, via a\n * dynamic `import()`, so an unconfigured studio pulls none of it onto the critical path.\n */\nconst TranslateDocumentDialog = lazy(() =>\n import('./translate-document-dialog').then((m) => ({ default: m.TranslateDocumentDialog })),\n)\n\nexport interface TranslateDocumentActionProps {\n projectId: string | undefined\n collection: CollectionDef\n docRef: DocumentRef\n /** The live editor draft — the translation source + the target for the fills. */\n document: CmsDocument\n localeConfig: LocaleConfig\n /** Whether the collection view exposes per-locale editing (`source.capabilities.locales`). */\n localesEnabled: boolean\n /** Reflect the translated draft into the open editor (save + replaceDraft). */\n onApplied: (next: CmsDocument) => void\n}\n\nexport function TranslateDocumentAction(props: TranslateDocumentActionProps) {\n const [open, setOpen] = useState(false)\n const { collection, localeConfig, localesEnabled, projectId } = props\n\n const eligible = useMemo(() => {\n const ai = resolveDeploymentAi()\n if (!ai || ai.enabled === false) return false\n if (!localesEnabled || projectId === undefined) return false\n const base = localeConfig.default\n if (!localeConfig.locales.some((l) => l !== base)) return false\n return collection.fields.some((f) => isFieldLocalized(collection, f.name))\n }, [collection, localeConfig, localesEnabled, projectId])\n\n if (!eligible) return null\n\n return (\n <>\n <Button\n size=\"sm\"\n variant=\"outline\"\n onClick={() => setOpen(true)}\n data-testid=\"doc-translate\"\n title={translate('ai.translate.tooltip')}\n >\n <Languages /> {translate('ai.translate.action')}\n </Button>\n {open ? (\n <Suspense fallback={null}>\n <TranslateDocumentDialog {...props} open onOpenChange={setOpen} />\n </Suspense>\n ) : null}\n </>\n )\n}\n"]}
@@ -0,0 +1,5 @@
1
+ import type { TranslateDocumentActionProps } from './translate-document-action';
2
+ export declare function TranslateDocumentDialog({ projectId, collection, docRef, document, localeConfig, onApplied, open, onOpenChange, }: TranslateDocumentActionProps & {
3
+ open: boolean;
4
+ onOpenChange: (open: boolean) => void;
5
+ }): import("react").JSX.Element;
@@ -0,0 +1,97 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { useCallback, useEffect, useRef, useState } from 'react';
3
+ import { Check, Languages, Loader2, X } from 'lucide-react';
4
+ import { Badge, Button, Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, Row, Stack, Text, } from '@elytracms/studio/ui';
5
+ import { translate } from '@elytracms/studio/lib/i18n';
6
+ import { localeChipLabel } from '@elytracms/studio/lib/locale';
7
+ import { getStudioOperations } from '@elytracms/studio/lib/operations';
8
+ import { resolveDeploymentAi } from '@elytracms/studio/lib/workspace';
9
+ import { resolveAiConfig, resolveKnowledge, translateDocument } from '@elytracms/studio/lib/ai';
10
+ export function TranslateDocumentDialog({ projectId, collection, docRef, document, localeConfig, onApplied, open, onOpenChange, }) {
11
+ const [phase, setPhase] = useState('confirm');
12
+ const [progress, setProgress] = useState(null);
13
+ const [summary, setSummary] = useState(null);
14
+ const abortRef = useRef(null);
15
+ useEffect(() => () => abortRef.current?.abort(), []);
16
+ const base = localeConfig.default;
17
+ const targets = localeConfig.locales.filter((l) => l !== base);
18
+ const run = useCallback(async () => {
19
+ if (projectId === undefined) {
20
+ setPhase('error');
21
+ return;
22
+ }
23
+ const resolved = resolveAiConfig(resolveDeploymentAi());
24
+ const provider = resolved.resolved?.provider;
25
+ if (!provider || !resolved.ok) {
26
+ setPhase('error');
27
+ return;
28
+ }
29
+ const controller = new AbortController();
30
+ abortRef.current = controller;
31
+ setPhase('running');
32
+ setProgress({ completed: 0, total: 0 });
33
+ try {
34
+ const knowledge = await resolveKnowledge(resolved.resolved?.knowledge ?? []);
35
+ const result = await translateDocument({
36
+ provider,
37
+ collection,
38
+ document,
39
+ localeConfig,
40
+ knowledge,
41
+ signal: controller.signal,
42
+ onProgress: setProgress,
43
+ });
44
+ if (result.translated > 0 || result.slugsDerived > 0) {
45
+ const { operations } = await getStudioOperations();
46
+ const upserted = await operations.documents.upsert({ projectId, document: result.document });
47
+ if (!upserted.ok) {
48
+ setPhase('error');
49
+ return;
50
+ }
51
+ // Provenance: an AI-tagged version snapshot (best-effort — the draft already landed).
52
+ try {
53
+ await operations.documents.snapshot({
54
+ projectId,
55
+ ref: docRef,
56
+ label: translate('cms.version.aiAuthoredTitle'),
57
+ origin: 'assistant',
58
+ });
59
+ }
60
+ catch {
61
+ // ignore — provenance is additive
62
+ }
63
+ onApplied(result.document);
64
+ }
65
+ setSummary({
66
+ translated: result.translated,
67
+ slugs: result.slugsDerived,
68
+ locales: Object.keys(result.perLocale).length,
69
+ cancelled: result.cancelled,
70
+ nothing: result.total === 0,
71
+ });
72
+ setPhase('done');
73
+ }
74
+ catch {
75
+ setPhase('error');
76
+ }
77
+ finally {
78
+ abortRef.current = null;
79
+ }
80
+ }, [projectId, collection, document, localeConfig, docRef, onApplied]);
81
+ const close = () => {
82
+ if (phase === 'running')
83
+ return; // never close mid-run — Cancel aborts instead
84
+ onOpenChange(false);
85
+ };
86
+ const doneMessage = (s) => {
87
+ if (s.nothing)
88
+ return translate('ai.translate.nothing');
89
+ if (s.cancelled)
90
+ return translate('ai.translate.cancelled');
91
+ return translate('ai.translate.applied', { count: s.translated, locales: s.locales });
92
+ };
93
+ return (_jsx(Dialog, { open: open, onOpenChange: (next) => (!next ? close() : undefined), children: _jsxs(DialogContent, { children: [_jsxs(DialogHeader, { children: [_jsx(DialogTitle, { children: _jsxs(Row, { gap: "2", align: "center", children: [_jsx(Languages, { className: "size-4" }), " ", translate('ai.translate.dialogTitle')] }) }), _jsx(DialogDescription, { children: translate('ai.translate.dialogDescription', { base: localeChipLabel(base) }) })] }), phase === 'confirm' ? (_jsxs(Stack, { gap: "4", className: "py-2", children: [_jsxs(Stack, { gap: "1", children: [_jsx(Text, { size: "xs", weight: "medium", tone: "muted", children: translate('ai.translate.targetsLabel') }), _jsx(Row, { gap: "1", wrap: true, children: targets.map((l) => (_jsx(Badge, { variant: "outline", children: localeChipLabel(l) }, l))) })] }), _jsxs(Stack, { gap: "1", children: [_jsx(Text, { size: "xs", weight: "medium", tone: "muted", children: translate('ai.translate.scopeLabel') }), _jsxs(Row, { gap: "2", align: "center", children: [_jsx(Badge, { variant: "default", children: translate('ai.translate.fillMissing') }), _jsx(Button, { size: "sm", variant: "ghost", disabled: true, title: translate('ai.translate.retranslateSoon'), children: translate('ai.translate.retranslateAll') }), _jsx(Text, { size: "xs", tone: "muted", children: translate('ai.translate.retranslateSoon') })] })] })] })) : null, phase === 'running' ? (_jsxs(Stack, { gap: "3", align: "center", className: "py-6", children: [_jsx(Loader2, { className: "size-6 animate-spin text-primary" }), _jsx(Text, { size: "sm", tone: "muted", "data-testid": "translate-progress", children: progress && progress.total > 0
94
+ ? translate('ai.translate.progress', { completed: progress.completed, total: progress.total })
95
+ : translate('ai.translate.preparing') }), progress && progress.total > 0 ? (_jsx("div", { className: "h-1.5 w-56 overflow-hidden rounded-full bg-muted", children: _jsx("div", { className: "h-full rounded-full bg-primary transition-all", style: { width: `${Math.round((progress.completed / progress.total) * 100)}%` } }) })) : null] })) : null, phase === 'done' && summary ? (_jsxs(Stack, { gap: "2", align: "center", className: "py-6", children: [_jsx(Check, { className: "size-6 text-success" }), _jsx(Text, { size: "sm", tone: "muted", className: "text-center", "data-testid": "translate-summary", children: doneMessage(summary) })] })) : null, phase === 'error' ? (_jsxs(Stack, { gap: "2", align: "center", className: "py-6", children: [_jsx(X, { className: "size-6 text-destructive" }), _jsx(Text, { size: "sm", tone: "muted", children: translate('ai.translate.failed') })] })) : null, _jsx(DialogFooter, { className: "mt-2", children: phase === 'confirm' ? (_jsxs(_Fragment, { children: [_jsx(Button, { variant: "ghost", size: "sm", onClick: close, children: translate('common.cancel') }), _jsxs(Button, { size: "sm", onClick: () => void run(), "data-testid": "translate-confirm", children: [_jsx(Languages, {}), " ", translate('ai.translate.confirm')] })] })) : phase === 'running' ? (_jsx(Button, { variant: "outline", size: "sm", onClick: () => abortRef.current?.abort(), "data-testid": "translate-cancel", children: translate('common.cancel') })) : (_jsx(Button, { size: "sm", onClick: close, "data-testid": "translate-close", children: translate('ai.translate.close') })) })] }) }));
96
+ }
97
+ //# sourceMappingURL=translate-document-dialog.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"translate-document-dialog.js","sourceRoot":"","sources":["../../../src/components/ai/translate-document-dialog.tsx"],"names":[],"mappings":"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","sourcesContent":["import { useCallback, useEffect, useRef, useState } from 'react'\nimport { Check, Languages, Loader2, X } from 'lucide-react'\nimport {\n Badge,\n Button,\n Dialog,\n DialogContent,\n DialogDescription,\n DialogFooter,\n DialogHeader,\n DialogTitle,\n Row,\n Stack,\n Text,\n} from '@elytracms/studio/ui'\nimport { translate } from '@elytracms/studio/lib/i18n'\nimport { localeChipLabel } from '@elytracms/studio/lib/locale'\nimport { getStudioOperations } from '@elytracms/studio/lib/operations'\nimport { resolveDeploymentAi } from '@elytracms/studio/lib/workspace'\nimport { resolveAiConfig, resolveKnowledge, translateDocument } from '@elytracms/studio/lib/ai'\nimport type { TranslateProgress } from '@elytracms/studio/lib/ai'\nimport type { TranslateDocumentActionProps } from './translate-document-action'\n\n/**\n * The \"Translate document\" confirm + progress dialog (EC-332), heavy side — loaded lazily\n * by the gate so the AI engine stays off the critical path (EC-331 bundle discipline). A\n * modal is allowed here (a picker/confirmation, per the house rule — property forms are\n * not). It resolves the EC-331 provider + knowledge, runs the ONE `translateDocument`\n * engine over the live draft (fill-empty-only on EC-318 connection state), streams\n * progress, then writes the result to the DRAFT via the operations layer with a\n * provenance-tagged snapshot (mirroring the EC-199 apply) and reflects it into the open\n * editor. Cancel aborts cleanly — any already-translated units stay on the draft, visible\n * in Compare (EC-317) and rejectable via Discard (EC-316).\n */\ntype Phase = 'confirm' | 'running' | 'done' | 'error'\n\ninterface RunSummary {\n translated: number\n slugs: number\n locales: number\n cancelled: boolean\n nothing: boolean\n}\n\nexport function TranslateDocumentDialog({\n projectId,\n collection,\n docRef,\n document,\n localeConfig,\n onApplied,\n open,\n onOpenChange,\n}: TranslateDocumentActionProps & { open: boolean; onOpenChange: (open: boolean) => void }) {\n const [phase, setPhase] = useState<Phase>('confirm')\n const [progress, setProgress] = useState<TranslateProgress | null>(null)\n const [summary, setSummary] = useState<RunSummary | null>(null)\n const abortRef = useRef<AbortController | null>(null)\n\n useEffect(() => () => abortRef.current?.abort(), [])\n\n const base = localeConfig.default\n const targets = localeConfig.locales.filter((l) => l !== base)\n\n const run = useCallback(async () => {\n if (projectId === undefined) {\n setPhase('error')\n return\n }\n const resolved = resolveAiConfig(resolveDeploymentAi())\n const provider = resolved.resolved?.provider\n if (!provider || !resolved.ok) {\n setPhase('error')\n return\n }\n const controller = new AbortController()\n abortRef.current = controller\n setPhase('running')\n setProgress({ completed: 0, total: 0 })\n try {\n const knowledge = await resolveKnowledge(resolved.resolved?.knowledge ?? [])\n const result = await translateDocument({\n provider,\n collection,\n document,\n localeConfig,\n knowledge,\n signal: controller.signal,\n onProgress: setProgress,\n })\n if (result.translated > 0 || result.slugsDerived > 0) {\n const { operations } = await getStudioOperations()\n const upserted = await operations.documents.upsert({ projectId, document: result.document })\n if (!upserted.ok) {\n setPhase('error')\n return\n }\n // Provenance: an AI-tagged version snapshot (best-effort — the draft already landed).\n try {\n await operations.documents.snapshot({\n projectId,\n ref: docRef,\n label: translate('cms.version.aiAuthoredTitle'),\n origin: 'assistant',\n })\n } catch {\n // ignore — provenance is additive\n }\n onApplied(result.document)\n }\n setSummary({\n translated: result.translated,\n slugs: result.slugsDerived,\n locales: Object.keys(result.perLocale).length,\n cancelled: result.cancelled,\n nothing: result.total === 0,\n })\n setPhase('done')\n } catch {\n setPhase('error')\n } finally {\n abortRef.current = null\n }\n }, [projectId, collection, document, localeConfig, docRef, onApplied])\n\n const close = () => {\n if (phase === 'running') return // never close mid-run — Cancel aborts instead\n onOpenChange(false)\n }\n\n const doneMessage = (s: RunSummary): string => {\n if (s.nothing) return translate('ai.translate.nothing')\n if (s.cancelled) return translate('ai.translate.cancelled')\n return translate('ai.translate.applied', { count: s.translated, locales: s.locales })\n }\n\n return (\n <Dialog open={open} onOpenChange={(next) => (!next ? close() : undefined)}>\n <DialogContent>\n <DialogHeader>\n <DialogTitle>\n <Row gap=\"2\" align=\"center\">\n <Languages className=\"size-4\" /> {translate('ai.translate.dialogTitle')}\n </Row>\n </DialogTitle>\n <DialogDescription>\n {translate('ai.translate.dialogDescription', { base: localeChipLabel(base) })}\n </DialogDescription>\n </DialogHeader>\n\n {phase === 'confirm' ? (\n <Stack gap=\"4\" className=\"py-2\">\n <Stack gap=\"1\">\n <Text size=\"xs\" weight=\"medium\" tone=\"muted\">\n {translate('ai.translate.targetsLabel')}\n </Text>\n <Row gap=\"1\" wrap>\n {targets.map((l) => (\n <Badge key={l} variant=\"outline\">\n {localeChipLabel(l)}\n </Badge>\n ))}\n </Row>\n </Stack>\n <Stack gap=\"1\">\n <Text size=\"xs\" weight=\"medium\" tone=\"muted\">\n {translate('ai.translate.scopeLabel')}\n </Text>\n <Row gap=\"2\" align=\"center\">\n <Badge variant=\"default\">{translate('ai.translate.fillMissing')}</Badge>\n <Button size=\"sm\" variant=\"ghost\" disabled title={translate('ai.translate.retranslateSoon')}>\n {translate('ai.translate.retranslateAll')}\n </Button>\n <Text size=\"xs\" tone=\"muted\">\n {translate('ai.translate.retranslateSoon')}\n </Text>\n </Row>\n </Stack>\n </Stack>\n ) : null}\n\n {phase === 'running' ? (\n <Stack gap=\"3\" align=\"center\" className=\"py-6\">\n <Loader2 className=\"size-6 animate-spin text-primary\" />\n <Text size=\"sm\" tone=\"muted\" data-testid=\"translate-progress\">\n {progress && progress.total > 0\n ? translate('ai.translate.progress', { completed: progress.completed, total: progress.total })\n : translate('ai.translate.preparing')}\n </Text>\n {progress && progress.total > 0 ? (\n <div className=\"h-1.5 w-56 overflow-hidden rounded-full bg-muted\">\n <div\n className=\"h-full rounded-full bg-primary transition-all\"\n style={{ width: `${Math.round((progress.completed / progress.total) * 100)}%` }}\n />\n </div>\n ) : null}\n </Stack>\n ) : null}\n\n {phase === 'done' && summary ? (\n <Stack gap=\"2\" align=\"center\" className=\"py-6\">\n <Check className=\"size-6 text-success\" />\n <Text size=\"sm\" tone=\"muted\" className=\"text-center\" data-testid=\"translate-summary\">\n {doneMessage(summary)}\n </Text>\n </Stack>\n ) : null}\n\n {phase === 'error' ? (\n <Stack gap=\"2\" align=\"center\" className=\"py-6\">\n <X className=\"size-6 text-destructive\" />\n <Text size=\"sm\" tone=\"muted\">\n {translate('ai.translate.failed')}\n </Text>\n </Stack>\n ) : null}\n\n <DialogFooter className=\"mt-2\">\n {phase === 'confirm' ? (\n <>\n <Button variant=\"ghost\" size=\"sm\" onClick={close}>\n {translate('common.cancel')}\n </Button>\n <Button size=\"sm\" onClick={() => void run()} data-testid=\"translate-confirm\">\n <Languages /> {translate('ai.translate.confirm')}\n </Button>\n </>\n ) : phase === 'running' ? (\n <Button variant=\"outline\" size=\"sm\" onClick={() => abortRef.current?.abort()} data-testid=\"translate-cancel\">\n {translate('common.cancel')}\n </Button>\n ) : (\n <Button size=\"sm\" onClick={close} data-testid=\"translate-close\">\n {translate('ai.translate.close')}\n </Button>\n )}\n </DialogFooter>\n </DialogContent>\n </Dialog>\n )\n}\n"]}
@@ -26,6 +26,7 @@ import { ReferenceConfirmDialog } from './reference-confirm';
26
26
  import { ReferencesPanel } from '../references/references-panel';
27
27
  import { InspectorSidebar } from '../inspector/inspector-sidebar';
28
28
  import { DocumentEditorProvider } from './document-editor-context';
29
+ import { TranslateDocumentAction } from '../ai/translate-document-action';
29
30
  import { VersionsPane } from './versions-pane';
30
31
  // EC-322: the lazy-wrapped compare view — its diff UI code-splits off the detail-view
31
32
  // critical path and loads only when the Compare tab opens.
@@ -488,7 +489,10 @@ function EditorBody({ projectId, collection, doc, focusPath, }) {
488
489
  : translate('cms.editor.saved')
489
490
  : dirty
490
491
  ? translate('cms.editor.saving')
491
- : translate('cms.editor.saved') }), canPublish ? _jsx(DocStateBadge, { published: published }) : null, canPublish ? (_jsxs(Row, { gap: "px", align: "center", children: [_jsx(Button, { size: "sm", onClick: handlePublish, disabled: publishDisabled, title: publishTitle, className: canPublishClose ? 'rounded-r-none' : undefined, "data-testid": "doc-publish", children: publishButtonLabel }), canPublishClose ? (_jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsx(Button, { size: "sm", disabled: publishDisabled, "aria-label": translate('cms.editor.publishActions'), "data-testid": "doc-publish-actions", className: "rounded-l-none border-l border-primary-foreground/25 px-1.5", children: _jsx(ChevronDown, { className: "size-3.5" }) }) }), _jsx(DropdownMenuContent, { align: "end", children: _jsxs(DropdownMenuItem, { onClick: handlePublishAndClose, "data-testid": "doc-publish-and-close", children: [_jsx(Save, {}), " ", translate('cms.editor.publishAndClose')] }) })] })) : null] })) : null, explicitSave ? (_jsxs(Button, { size: "sm", onClick: save, disabled: !dirty, "data-testid": "doc-save", children: [_jsx(Check, {}), " ", translate('common.save')] })) : null, published || !singleton ? (_jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsx(Button, { variant: "outline", size: "icon-sm", "aria-label": translate('cms.editor.moreActions'), children: _jsx(MoreHorizontal, {}) }) }), _jsxs(DropdownMenuContent, { align: "end", children: [discardStatus !== 'hidden' ? (_jsxs(DropdownMenuItem, { "data-testid": "doc-discard-draft", disabled: discardStatus !== 'ready', title: discardDisabledReason, onClick: () => setDiscardConfirm(true), children: [_jsx(RotateCcw, {}), " ", translate('cms.editor.discardDraft')] })) : null, published ? (_jsxs(DropdownMenuItem, { onClick: () => setConfirmAction('unpublish'), children: [_jsx(EyeOff, {}), " ", translate('cms.editor.unpublish')] })) : null, singleton ? null : (_jsxs(DropdownMenuItem, { "data-testid": "doc-delete", className: "text-destructive", onClick: () => setConfirmAction('delete'), children: [_jsx(Trash2, {}), " ", translate('common.delete')] }))] })] })) : null] }), showLensRow ? (_jsxs(Row, { gap: "3", className: "mt-3", wrap: true, children: [_jsxs(Row, { gap: "1", align: "center", children: [showContentTabs ? tabs.map((tab) => renderContentTab(tab)) : null, showReferences ? (_jsxs(_Fragment, { children: [_jsx("div", { className: "mx-1 h-4 w-px bg-border", "aria-hidden": true }), renderContentTab(REFERENCES_TAB)] })) : null, compare ? (_jsxs("span", { className: cn('inline-flex items-center gap-1 rounded-md px-2.5 py-1 text-xs font-medium transition-colors', activeTab === COMPARE_TAB
492
+ : translate('cms.editor.saved') }), canPublish ? _jsx(DocStateBadge, { published: published }) : null, canPublish ? (_jsxs(Row, { gap: "px", align: "center", children: [_jsx(Button, { size: "sm", onClick: handlePublish, disabled: publishDisabled, title: publishTitle, className: canPublishClose ? 'rounded-r-none' : undefined, "data-testid": "doc-publish", children: publishButtonLabel }), canPublishClose ? (_jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsx(Button, { size: "sm", disabled: publishDisabled, "aria-label": translate('cms.editor.publishActions'), "data-testid": "doc-publish-actions", className: "rounded-l-none border-l border-primary-foreground/25 px-1.5", children: _jsx(ChevronDown, { className: "size-3.5" }) }) }), _jsx(DropdownMenuContent, { align: "end", children: _jsxs(DropdownMenuItem, { onClick: handlePublishAndClose, "data-testid": "doc-publish-and-close", children: [_jsx(Save, {}), " ", translate('cms.editor.publishAndClose')] }) })] })) : null] })) : null, explicitSave ? (_jsxs(Button, { size: "sm", onClick: save, disabled: !dirty, "data-testid": "doc-save", children: [_jsx(Check, {}), " ", translate('common.save')] })) : null, !viewReadOnly ? (_jsx(TranslateDocumentAction, { projectId: projectId, collection: collection, docRef: ref, document: draft, localeConfig: source.localeConfig, localesEnabled: canLocales, onApplied: (next) => {
493
+ source.save(next);
494
+ replaceDraft(next);
495
+ } })) : null, published || !singleton ? (_jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsx(Button, { variant: "outline", size: "icon-sm", "aria-label": translate('cms.editor.moreActions'), children: _jsx(MoreHorizontal, {}) }) }), _jsxs(DropdownMenuContent, { align: "end", children: [discardStatus !== 'hidden' ? (_jsxs(DropdownMenuItem, { "data-testid": "doc-discard-draft", disabled: discardStatus !== 'ready', title: discardDisabledReason, onClick: () => setDiscardConfirm(true), children: [_jsx(RotateCcw, {}), " ", translate('cms.editor.discardDraft')] })) : null, published ? (_jsxs(DropdownMenuItem, { onClick: () => setConfirmAction('unpublish'), children: [_jsx(EyeOff, {}), " ", translate('cms.editor.unpublish')] })) : null, singleton ? null : (_jsxs(DropdownMenuItem, { "data-testid": "doc-delete", className: "text-destructive", onClick: () => setConfirmAction('delete'), children: [_jsx(Trash2, {}), " ", translate('common.delete')] }))] })] })) : null] }), showLensRow ? (_jsxs(Row, { gap: "3", className: "mt-3", wrap: true, children: [_jsxs(Row, { gap: "1", align: "center", children: [showContentTabs ? tabs.map((tab) => renderContentTab(tab)) : null, showReferences ? (_jsxs(_Fragment, { children: [_jsx("div", { className: "mx-1 h-4 w-px bg-border", "aria-hidden": true }), renderContentTab(REFERENCES_TAB)] })) : null, compare ? (_jsxs("span", { className: cn('inline-flex items-center gap-1 rounded-md px-2.5 py-1 text-xs font-medium transition-colors', activeTab === COMPARE_TAB
492
496
  ? 'bg-accent text-accent-foreground'
493
497
  : 'text-muted-foreground hover:bg-muted'), children: [_jsxs("button", { type: "button", onClick: () => setActiveTab(COMPARE_TAB), "aria-pressed": activeTab === COMPARE_TAB, className: "inline-flex items-center gap-1", children: [_jsx(GitCompareArrows, { className: "size-3", "aria-hidden": true }), " ", translate('cms.version.compare')] }), _jsx("button", { type: "button", onClick: exitCompare, "aria-label": translate('cms.version.closeCompare'), className: "ml-0.5 rounded p-0.5 hover:bg-background/40", children: _jsx(X, { className: "size-3" }) })] })) : null] }), _jsx(Spacer, {}), canLocales ? _jsx(LocaleSwitcher, {}) : null, canPublish ? (_jsxs(Row, { gap: "px", className: "rounded-md bg-muted/60 p-0.5", children: [_jsx(PerspectiveButton, { label: translate('cms.editor.draftState'), active: perspective === 'draft', onClick: () => setPerspective('draft') }), _jsx(PerspectiveButton, { label: translate('cms.editor.publishedState'), active: perspective === 'published', accent: "info", disabled: !published, title: published ? undefined : translate('cms.editor.publishToViewPublished'), onClick: () => setPerspective('published') })] })) : null] })) : null] }), viewReadOnly ? (_jsxs(Row, { gap: "2", className: "shrink-0 border-b border-info/30 bg-info/10 px-4 py-1.5 text-info sm:px-6", "data-testid": "published-view-banner", children: [_jsx(Lock, { className: "size-3.5 shrink-0", "aria-hidden": true }), _jsx(Text, { size: "xs", className: "text-info", children: translate('cms.editor.publishedReadOnlyBanner') }), _jsx(Spacer, {}), _jsx("button", { type: "button", onClick: () => setPerspective('draft'), className: "text-xs font-medium underline-offset-2 hover:underline", children: translate('cms.editor.switchToDraft') })] })) : null, publishedPending ? (_jsx("div", { className: "grid min-h-0 flex-1 place-items-center p-8", children: _jsx(Text, { size: "sm", tone: "muted", children: publishedSnapshot.status === 'loading'
494
498
  ? translate('cms.editor.loadingPublishedVersion')