diff --git a/apps/presentation/dashboard/src/features/personal-workspace/capability-localization.ts b/apps/presentation/dashboard/src/features/personal-workspace/capability-localization.ts index 94e7a3005f..9043698ee0 100644 --- a/apps/presentation/dashboard/src/features/personal-workspace/capability-localization.ts +++ b/apps/presentation/dashboard/src/features/personal-workspace/capability-localization.ts @@ -14,7 +14,7 @@ const capabilityCopy: Record> = { en: { codex_chat: {displayName: "Codex Goal conversation defaults", description: "Choose the native Codex profile, model, speed tier and reasoning effort for new Goal conversations. Existing conversations keep their original history. This does not start or reconfigure an active Agent Loop, worker or steward."}, decision_context: {displayName: "Find evidence", description: "Find advisory evidence with a temporary private profile. OpenViking and BM25 are retrieval implementations; this page does not save a binding or enable automatic memory."}, - content_ops: {displayName: "Content production", description: "Prepare source-backed outlines and local videos. Review the result before sharing; rendering uses an optional local extension."}, + content_ops: {displayName: "Content production", description: "Prepare source-backed outlines, compose local videos or generate cloud images and footage with the optional media provider. Review the result before sharing."}, goal_storage: { displayName: "New Goal authority", description: "New Goals default to canonical SQLite with hard leases. Choose storage and execution policy for future Goals. Existing Goals require a separate backed-up migration." }, manager_runtime: { displayName: "Runtime", @@ -83,7 +83,7 @@ const capabilityCopy: Record> = { "zh-CN": { codex_chat: {displayName: "Codex Goal 对话默认值", description: "分别选择 Codex 配置来源、模型、速度档和推理强度,作用于本机的新 Goal 对话。已有对话保留原历史;不会启动或重配正在运行的 Agent Loop、工作 Agent 或管家。"}, decision_context: {displayName: "资料召回", description: "通过临时私有配置查找参考证据。OpenViking、BM25 是检索实现;本页不保存绑定,也不启用自动记忆。"}, - content_ops: {displayName: "内容制作", description: "准备有来源的提纲与本机视频,检查作品后再分享;视频制作使用可选本机扩展。"}, + content_ops: {displayName: "内容制作", description: "准备有来源的提纲,使用可选媒体扩展制作本机视频、生成云端图像与镜头,检查作品后再分享。"}, goal_storage: { displayName: "新 Goal 的权威存储", description: "新 Goal 默认使用 canonical SQLite 与 hard lease;可选择之后新 Goal 的存储与执行策略。已有 Goal 仍需单独备份、迁移。" }, manager_runtime: { displayName: "运行环境", diff --git a/apps/presentation/dashboard/src/features/personal-workspace/content-generated-footage.tsx b/apps/presentation/dashboard/src/features/personal-workspace/content-generated-footage.tsx new file mode 100644 index 0000000000..ac609c31d9 --- /dev/null +++ b/apps/presentation/dashboard/src/features/personal-workspace/content-generated-footage.tsx @@ -0,0 +1,116 @@ +import {useEffect, useRef, useState} from "react"; +import {useWorkspaceI18n} from "./i18n"; + +type Footage = { + generation_id: string; revision: number; title: string; status: string; seconds: number; resolution: string; + prompt: string; model: string; task_id: string | null; error_code: string | null; + estimate: {cny: number}; reported_usage_estimate_cny: number | null; + quote: {source_url: string; checked_at: string}; watermark_requested: boolean; + references: {role: string; sha256: string; source_ref: string}[]; + result: {files: Record; measured_seconds: number; frames: number} | null; +}; +type Library = {ok: boolean; generations: Footage[]; provider: {status: string}}; +const ENDPOINT = "/api/chat/content-media"; + +/** Same content-ops workspace, same Goal. Reading drafts has no cloud effect. */ +export function ContentGeneratedFootage({goalId}: {goalId: string}) { + const {locale} = useWorkspaceI18n(); + const copy = (cn: string, en: string) => locale === "zh-CN" ? cn : en; + const [library, setLibrary] = useState(null), [selectedId, select] = useState(""); + const [busyAction, setBusyAction] = useState<"submit" | "check" | "collect" | null>(null), [error, setError] = useState(""), [watch, setWatch] = useState(false); + const busy = busyAction !== null; + const epoch = useRef(0), readSequence = useRef(0), pending = useRef(false), checks = useRef(0); + const current = library?.generations.find(item => item.generation_id === selectedId) ?? library?.generations[0]; + const active = current && ["submitted", "queued", "running"].includes(current.status); + async function request(action: string, item?: Footage) { + const response = await fetch(ENDPOINT, {method: "POST", headers: {"Content-Type": "application/json"}, + body: JSON.stringify({goal_id: goalId, action: `generation_${action}`, + ...(item ? {generation_id: item.generation_id, expected_revision: item.revision} : {})})}); + if (!response.ok) throw new Error(); + return response.json(); + } + async function reload() { + const scope = epoch.current; + const sequence = ++readSequence.current; + try { + const result: Library = await request("list"); + if (scope === epoch.current && sequence === readSequence.current) {setLibrary(result); setError("");} + } catch {if (scope === epoch.current && sequence === readSequence.current) setError(copy("镜头暂不可读,请重新读取。", "Cannot read footage. Recheck this workspace."));} + } + useEffect(() => { + epoch.current += 1; pending.current = false; checks.current = 0; + setLibrary(null); select(""); setBusyAction(null); setWatch(false); void reload(); + return () => {epoch.current += 1;}; + }, [goalId]); + async function operate(action: "submit" | "check" | "collect") { + if (!current || pending.current) return; + const scope = epoch.current; + pending.current = true; setBusyAction(action); setError(""); + try { + const result = await request(action, current); + if (scope !== epoch.current) return; + await reload(); + if (!result.ok) setError(result.error_code === "generation_provider_unavailable" + ? copy("当前 Host 未加载云端生成凭据或扩展未启用。请让 Agent 修复后重试;镜头草稿已保留。", "The current host has no available cloud-generation credential or enabled extension. Ask your Agent to repair it; the draft is retained.") + : copy("本次操作未完成。请检查原任务或保存状态;不会重新提交付费生成。", "The operation is incomplete. Check the existing task or collection state; no new paid task will be submitted.")); + if (action === "submit" && result.ok) {checks.current = 0; setWatch(true);} + } catch {if (scope === epoch.current) setError(copy("操作未确认。请先读取现有任务,勿重新提交付费生成。", "Operation unconfirmed. Read the existing task before submitting any new paid generation."));} + finally {if (scope === epoch.current) {pending.current = false; setBusyAction(null);}} + } + useEffect(() => { + if (!watch || !active) return; + const timer = setInterval(() => { + if (pending.current) return; + if (++checks.current > 30) {setWatch(false); return;} + void operate("check"); + }, 10000); + return () => clearInterval(timer); + }, [watch, current?.generation_id, current?.status, goalId]); + const names: Record = { + prepared: copy("待生成", "Ready to generate"), submitting: copy("提交结果待确认", "Submission awaiting confirmation"), + submitted: copy("已提交,待查询状态", "Submitted; status not yet checked"), + submission_unknown: copy("提交结果未知,请核对原任务", "Submission unknown; reconcile the original task"), + queued: copy("云端排队中", "Queued in the cloud"), running: copy("云端生成中", "Generating in the cloud"), + succeeded: copy("云端已完成,待保存", "Cloud result ready to save"), ready: copy("镜头已保存", "Footage saved"), + failed: copy("云端生成失败", "Cloud generation failed"), rejected: copy("请求被拒绝", "Request rejected"), + expired: copy("云端任务已超时", "Cloud task expired"), cancelled: copy("云端已取消", "Cancelled in the cloud"), + damaged: copy("本机文件校验失败", "Local file verification failed"), + }; + function file(name: string) {return `${ENDPOINT}/file?${new URLSearchParams({goal_id: goalId, generation_id: current!.generation_id, file: name, + revision: String(current!.revision), sha256: current!.result!.files[name].sha256})}`;} + return
+

{copy("生成镜头", "Generate footage")}

+

{copy("用已准备的提示词和参考帧生成素材,再在本机添加准确文字、图表和配音。", "Generate source footage from a prepared prompt and reference frames. Add exact text, diagrams and narration locally.")}

+
+ {error ?

{error}

: null} + {busyAction ?

{busyAction === "collect" ? copy("正在下载并校验镜头…", "Downloading and verifying footage…") : busyAction === "submit" ? copy("正在提交一次生成请求…", "Submitting one generation request…") : copy("正在查询原任务…", "Checking the existing task…")}

: null} + {!library && !error ?

{copy("正在读取镜头…", "Loading footage…")}

: null} + {library && !current ?

{copy("让 Agent 准备需要生成的镜头。普通本机视频制作仍可使用。", "Ask your Agent to prepare a footage intent. Ordinary local composition remains available.")}

: null} + {current ?
+ +

{current.title}

+

{names[current.status] ?? current.status} · {current.seconds}s · {current.resolution}

+ {current.status === "prepared" ? <>

{copy("将提示词和参考帧发送至 Ark,生成一次镜头。预计", "Send the prompt and reference frames to Ark for one generation. Estimated")} ¥{current.estimate.cny.toFixed(2)}{copy(";实际用量与账单可能不同。", "; actual usage and billing may differ.")}

+ : null} + {library!.provider.status !== "ready" ?

{copy("云端生成扩展未启用,草稿已保留。", "Cloud generation extension unavailable; the draft is retained.")}

: null} + {active || ["submitting", "submission_unknown", "succeeded", "damaged"].includes(current.status) ?
+ + {["succeeded", "damaged"].includes(current.status) ? : null} + {active ? : null}
: null} + {active ?

{copy("停止查询不会取消云端任务或费用。此服务不提供可安全自动重试的取消操作。", "Stopping checks does not cancel the cloud task or its charges. This service has no safe automatic cancellation operation.")}

: null} + {current.error_code ?

{copy("本次操作尚未完成。原任务和草稿保留;让 Agent 检查任务读回或配置。", "The operation is incomplete. The existing task and draft are retained; ask your Agent to check task readback or configuration.")}

: null} + {current.result && current.status === "ready" ? <>
: null} +
; +} diff --git a/apps/presentation/dashboard/src/features/personal-workspace/content-generated-images.tsx b/apps/presentation/dashboard/src/features/personal-workspace/content-generated-images.tsx new file mode 100644 index 0000000000..2c97da6909 --- /dev/null +++ b/apps/presentation/dashboard/src/features/personal-workspace/content-generated-images.tsx @@ -0,0 +1,84 @@ +import {useEffect, useRef, useState} from "react"; +import {useWorkspaceI18n} from "./i18n"; + +type ImageIntent = { + image_id: string; revision: number; title: string; prompt: string; status: string; model: string; + width: number; height: number; estimate: {cny: number}; error_code: string | null; + reported_usage_estimate_cny: number | null; watermark_requested: boolean; + quote: {source_url: string; checked_at: string}; + references: {source_ref: string; sha256: string}[]; + result: {files: Record; usage: {generated_images: number; output_tokens: number | null}} | null; +}; +type Library = {images: ImageIntent[]; provider: {status: string}}; +const ENDPOINT = "/api/chat/content-media"; + +export function ContentGeneratedImages({goalId}: {goalId: string}) { + const {locale} = useWorkspaceI18n(); + const copy = (zh: string, en: string) => locale === "zh-CN" ? zh : en; + const [library,setLibrary] = useState(null), [selectedId,select] = useState(""); + const [busy,setBusy] = useState(false), [error,setError] = useState(""); + const epoch = useRef(0), sequence = useRef(0), pending = useRef(false); + const current = library?.images.find(i=>i.image_id === selectedId) ?? library?.images[0]; + async function request(action: string, item?: ImageIntent) { + const response = await fetch(ENDPOINT,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({goal_id:goalId,action:`image_${action}`, + ...(item ? {image_id:item.image_id,expected_revision:item.revision} : {})})}); + if (!response.ok) throw new Error(); + return response.json(); + } + async function reload() { + const scope = epoch.current, read = ++sequence.current; + try {const value: Library = await request("list"); if(scope === epoch.current && read === sequence.current) {setLibrary(value);setError("");}} + catch {if(scope === epoch.current && read === sequence.current) setError(copy("图像暂不可读,请重新读取。","Cannot read images. Recheck this workspace."));} + } + useEffect(()=>{ + epoch.current += 1;pending.current = false;setBusy(false);setLibrary(null);setError("");select("");void reload(); + return ()=>{epoch.current += 1;}; + },[goalId]); + async function operate(action: "submit" | "collect") { + if (!current || pending.current) return; + const scope = epoch.current;pending.current = true;setBusy(true);setError(""); + try { + const result = await request(action,current); + if(scope !== epoch.current) return; + await reload(); + if(!result.ok) setError(result.error_code === "image_provider_unavailable" + ? copy("图像生成暂不可用。请让 Agent 检查扩展与图像专用凭据;草稿保留,未提交生成。","Image generation is unavailable. Ask your Agent to check the extension and image credential; the draft is retained without a generation submission.") + : copy("生成或保存未确认。仅检查本机已保存的响应,不会重新提交付费请求。","Generation or collection is unconfirmed. Only inspect the saved local response; no paid request will be repeated.")); + } catch {if(scope === epoch.current) setError(copy("请求结果未确认,请重新读取并检查已保存响应。","Request unconfirmed. Recheck and inspect the saved response."));} + finally {if(scope === epoch.current) {pending.current = false;setBusy(false);}} + } + const names: Record = { + prepared:copy("待生成","Ready to generate"),submitting:copy("请求结果待确认","Request awaiting confirmation"), + submission_unknown:copy("请求结果未知","Request outcome unknown"),rejected:copy("请求被拒绝","Request rejected"), + ready:copy("图像已保存","Image saved"),damaged:copy("本机图像校验失败","Local image verification failed"), + }; + function file(name: string) {return `${ENDPOINT}/file?${new URLSearchParams({goal_id:goalId,image_id:current!.image_id,file:name, + revision:String(current!.revision),sha256:current!.result!.files[name].sha256})}`;} + return
+

{copy("生成图像","Generate an illustration")}

+

{copy("让 Agent 准备提示词与参考图,在本机保留生成素材,再添加准确文字与图表。","Have your Agent prepare the prompt and reference. Save the generated source locally, then add exact text and diagrams.")}

+
+ {error ?

{error}

: null} + {busy ?

{copy("正在发送并读取一次生图响应…","Sending and reading one image-generation response…")}

: null} + {!library && !error ?

{copy("正在读取图像…","Loading images…")}

: null} + {library && !current ?

{copy("把需要解释的内容交给 Agent,它会准备可检查的生图草稿。","Tell your Agent what to explain; it can prepare an image intent for review.")}

: null} + {current ?
+

{current.title}

{names[current.status] ?? current.status} · {current.width} × {current.height}px

+ {current.status === "prepared" ? <>

{copy("将提示词与参考图发送至 Ark,生成一张图。预计","Send the prompt and reference to Ark for one image. Estimated")} ¥{current.estimate.cny.toFixed(2)}{copy(";实际账单未知。","; actual invoice unknown.")}

+ : null} + {library!.provider.status !== "ready" ?

{copy("云端媒体扩展未启用,草稿已保留。","Cloud media extension unavailable; the draft is retained.")}

: null} + {["submitting","submission_unknown","damaged"].includes(current.status) ? <>

{copy("该请求不能按云端任务查询或自动重发。可读取本机已保存的响应;若没有响应,须先核实服务端用量。","This request cannot be polled as a cloud task or automatically resubmitted. Read its saved local response; if absent, reconcile provider usage first.")}

+ : null} + {current.error_code && !error ?

{copy("本次操作尚未完成,草稿和原请求状态已保留。","The operation is incomplete; the draft and original request state are retained.")}

: null} + {current.status === "ready" && current.result ? <>{current.title} setError(copy("无法读取这个版本的图像,请重新读取。","Cannot read this image version. Recheck the saved result."))} /> + +

{copy("AI 生成的示意素材;完整解码通过,内容含义仍需检查。","AI-generated illustration. Full decode passed; inspect its meaning.")}

: null} +
{copy("提示词、来源与用量","Prompt, sources and usage")}

{current.prompt}

+
    {current.references.map(ref=>
  • {ref.source_ref}
    SHA256 {ref.sha256}
  • )}
+

{current.model} · {copy("请求水印","Watermark requested")}: {current.watermark_requested ? copy("是","Yes") : copy("否","No")}

+

{copy("估价来源","Price source")} · {current.quote.checked_at}

+

{copy("服务报告生成数量","Reported output count")}: {current.result?.usage.generated_images ?? copy("未取得","Unavailable")} · {copy("按用量估价","Usage estimate")}: {current.reported_usage_estimate_cny == null ? "—" : `¥${current.reported_usage_estimate_cny.toFixed(2)}`} · {copy("实际账单未知","Actual invoice unknown")}

+

{copy("图像是机制示意,不是执行轨迹。创意、文字、水印需人工检查,不会自动发布。","This is a mechanism illustration, not an execution trace. Inspect creative quality, text and watermark. Nothing is published automatically.")}

+
: null} +
; +} diff --git a/apps/presentation/dashboard/src/features/personal-workspace/content-media-workbench.css b/apps/presentation/dashboard/src/features/personal-workspace/content-media-workbench.css index 91ea290331..50f1fbf6ff 100644 --- a/apps/presentation/dashboard/src/features/personal-workspace/content-media-workbench.css +++ b/apps/presentation/dashboard/src/features/personal-workspace/content-media-workbench.css @@ -1,4 +1,10 @@ -.content-production-panel {padding:0 24px 24px;} +.content-production-results {padding:0 0 24px;} +.content-production-results h2 {margin:0; font-size:var(--text-heading);} +.content-production-actions {display:flex; flex-wrap:wrap; gap:8px;} +.content-production-actions button {min-height:44px; padding:8px 12px; border:1px solid var(--pw-line-strong); border-radius:6px; background:var(--pw-card); color:inherit; font:inherit; font-size:var(--text-body); cursor:pointer;} +.content-production-actions button:focus-visible {outline:2px solid var(--pw-blue); outline-offset:3px;} +.content-production-results > details {padding:16px 0; border-bottom:1px solid var(--pw-line); font-size:var(--text-body);} +.content-production-results > details > summary {cursor:pointer; min-height:44px; align-content:center;} .content-media {padding:16px 0 24px; border-bottom:1px solid var(--pw-line);} .content-media-heading, .content-media-title {display:flex; justify-content:space-between; align-items:flex-start; gap:16px;} .content-media h3, .content-media h4 {margin:0 0 6px; font-size:var(--text-lead); font-weight:600;} @@ -13,7 +19,7 @@ .content-media nav button[aria-pressed=true] {background:var(--pw-hover); border-color:var(--pw-line-strong);} .content-media small {display:block; color:var(--pw-muted); font-size:var(--text-caption); line-height:1.5;} .content-media-result {min-width:0; overflow-wrap:anywhere;} -.content-media-result video {display:block; width:100%; margin:14px 0; border-radius:8px; background:var(--pw-card);} +.content-media-result video, .content-media-result img {display:block; width:100%; margin:14px 0; border-radius:8px; background:var(--pw-card);} .content-media-timeline {list-style:none; padding:0; margin:14px 0; border-top:1px solid var(--pw-line);} .content-media-timeline li {padding:12px 0; border-bottom:1px solid var(--pw-line); font-size:var(--text-body);} .content-media-timeline small {margin-top:4px;} @@ -22,4 +28,8 @@ .content-media details summary {cursor:pointer; color:var(--pw-muted);} .content-media details li {line-height:1.6; margin:6px 0;} .content-media-empty {padding:22px 0;} -@media(max-width:680px) {.content-production-panel {padding:0 16px 20px;}.content-media-workspace {grid-template-columns:minmax(0, 1fr);}.content-media nav {flex-direction:row; overflow-x:auto;}.content-media nav button {min-width:160px;}.content-media-heading, .content-media-title {flex-wrap:wrap;}} +.content-media-tabs {display:flex; flex-wrap:wrap; gap:8px; padding:16px 0; border-bottom:1px solid var(--pw-line);} +.content-media-tabs button {min-height:44px; padding:8px 12px; font:inherit; color:inherit; border:1px solid transparent; border-radius:6px; background:transparent; cursor:pointer;} +.content-media-tabs button[aria-pressed=true] {background:var(--pw-hover); border-color:var(--pw-line-strong);} +.content-media-tabs button:focus-visible {outline:2px solid var(--pw-blue); outline-offset:3px;} +@media(max-width:680px) {.content-media-workspace {grid-template-columns:minmax(0, 1fr);}.content-media nav {flex-direction:row; overflow-x:auto;}.content-media nav button {min-width:160px;}.content-media-heading, .content-media-title {flex-wrap:wrap;}} diff --git a/apps/presentation/dashboard/src/features/personal-workspace/content-media-workbench.tsx b/apps/presentation/dashboard/src/features/personal-workspace/content-media-workbench.tsx index 3b83635b3c..5913f6ed88 100644 --- a/apps/presentation/dashboard/src/features/personal-workspace/content-media-workbench.tsx +++ b/apps/presentation/dashboard/src/features/personal-workspace/content-media-workbench.tsx @@ -1,6 +1,8 @@ import {useEffect, useRef, useState} from "react"; import {useWorkspaceI18n} from "./i18n"; import "./content-media-workbench.css"; +import {ContentGeneratedImages} from "./content-generated-images"; +import {ContentGeneratedFootage} from "./content-generated-footage"; type Provider = {status: string; enabled: boolean | null; next_action: string | null}; type Composition = { @@ -15,8 +17,11 @@ const ENDPOINT = "/api/chat/content-media"; export function ContentMediaWorkbench({goalId}: {goalId: string}) { const {locale} = useWorkspaceI18n(); const copy = (cn: string, en: string) => locale === "zh-CN" ? cn : en; + const seconds = (value: number) => `${Number(value.toFixed(2))}s`; const [library, setLibrary] = useState(null), [selectedId, setSelectedId] = useState(""); const [error, setError] = useState(""), [busy, setBusy] = useState(false); + const [watching, setWatching] = useState(false); + const [view, setView] = useState<"composition" | "footage" | "images">("composition"); const pending = useRef(false), generation = useRef(0), readSequence = useRef(0); const selected = library?.compositions.find(item => item.composition_id === selectedId) ?? library?.compositions[0]; async function request(action: string, item?: Composition) { @@ -31,41 +36,48 @@ export function ContentMediaWorkbench({goalId}: {goalId: string}) { try { const value: Library = await request("list"); if (current === generation.current && sequence === readSequence.current) {setLibrary(value); setError("");} - } catch {if (current === generation.current && sequence === readSequence.current) setError(copy("作品暂不可读,请重新读取。", "Cannot read compositions. Recheck the workspace."));} + } catch {if (current === generation.current && sequence === readSequence.current) {setWatching(false); setError(copy("作品暂不可读,请重新读取。", "Cannot read compositions. Recheck the workspace."));}} } useEffect(() => { - generation.current += 1; setLibrary(null); setSelectedId(""); pending.current = false; setBusy(false); void reload(); + generation.current += 1; setLibrary(null); setSelectedId(""); pending.current = false; setBusy(false); setWatching(false); void reload(); return () => {generation.current += 1;}; }, [goalId]); - const running = library?.compositions.some(item => item.status === "rendering"); + const rendering = selected?.status === "rendering"; useEffect(() => { - if (!running) return; + if (!watching) return; + if (!rendering || view !== "composition") {setWatching(false); return;} const timer = setInterval(() => void reload(), 2000); return () => clearInterval(timer); - }, [running, goalId]); + }, [watching, rendering, view, goalId]); async function operate(action: "render" | "cancel") { if (!selected || pending.current) return; - pending.current = true; setBusy(true); setError(""); + pending.current = true; setBusy(true); setError(""); setWatching(false); const current = generation.current; try { const value = await request(action, selected); if (current !== generation.current) return; await reload(); + if (current === generation.current && action === "render" && value.ok && value.status === "rendering") setWatching(true); if (!value.ok) setError(copy("渲染器未就绪。请启用已验证的本机视频扩展后重试。", "Renderer unavailable. Enable the verified local video extension, then retry.")); } catch {if (current === generation.current) setError(copy("操作未确认。先重新读取,避免重复启动。", "Operation unconfirmed. Recheck before retrying."));} finally {if (current === generation.current) {pending.current = false; setBusy(false);}} } - function file(name: string) {return `${ENDPOINT}/file?${new URLSearchParams({goal_id: goalId, composition_id: selected!.composition_id, file: name})}`;} + function file(name: string) {return `${ENDPOINT}/file?${new URLSearchParams({goal_id: goalId, composition_id: selected!.composition_id, file: name, + revision: String(selected!.revision), sha256: selected!.result!.files[name].sha256})}`;} const status: Record = { - prepared: copy("待制作", "Ready to render"), rendering: copy("正在制作", "Rendering"), ready: copy("作品已生成", "Video ready"), + prepared: copy("待制作", "Ready to render"), rendering: copy("已请求制作,等待结果", "Render requested; awaiting result"), ready: copy("作品已生成", "Video ready"), failed: copy("制作失败,可修复后重试", "Render failed; repair and retry"), cancelled: copy("已取消,可重新制作", "Cancelled; ready to retry"), interrupted: copy("上次制作中断,可重新制作", "Previous render interrupted; retry available"), damaged: copy("结果校验失败,请重新制作", "Result verification failed; render again"), }; const providerReady = library?.provider.status === "ready"; const hasBlender = selected?.shots.some(shot => shot.kind === "blender"); - return
+ return <>
+ + + +
{view === "composition" ?

{copy("制作视频", "Produce a video")}

-

{copy("让 Agent 准备脚本、来源和时间线,在这里制作并查看作品。", "Have your Agent prepare the story, sources and timeline. Render and review the result here.")}

+

{copy("脚本与来源由当前对话准备;这里只读取已保存状态,制作后可重新读取。", "The current conversation prepares the story and sources. This view reads saved state; recheck after rendering.")}

{error ?

{error}

: null} {!library && !error ?

{copy("正在读取作品…", "Loading compositions…")}

: null} @@ -73,21 +85,21 @@ export function ContentMediaWorkbench({goalId}: {goalId: string}) { {library && !selected ?

{copy("还没有准备好的作品。把想讲的内容交给 Agent,它会通过 Content-Ops 保存可制作的脚本。", "No prepared compositions yet. Tell your Agent what to explain; it can save a renderable brief through Content-Ops.")}

: null} {selected ?
-

{selected.title}

{status[selected.status] ?? selected.status} · {selected.duration_seconds}s · {copy("修订", "Revision")} {selected.revision}

+ disabled={busy} onClick={() => {setWatching(false); setError(""); setSelectedId(item.composition_id);}}>{item.title}{seconds(item.duration_seconds)} · {status[item.status] ?? item.status})} +

{selected.title}

{status[selected.status] ?? selected.status} · {seconds(selected.duration_seconds)} · {copy("修订", "Revision")} {selected.revision}

{selected.status === "rendering" ? : }
{hasBlender && selected.status !== "ready" ?

{copy("包含 Blender 三维场景。制作需要已安装的本机 Blender 扩展依赖;作品会保留可编辑工程与颜色、深度画面。", "Includes Blender scenes. Rendering needs the local Blender extra; the editable project, color and depth frames are retained.")}

: null} - {selected.status === "ready" && selected.result ? <>
: null} -
; +
: view === "footage" ? : }; } diff --git a/apps/presentation/dashboard/src/features/personal-workspace/content-production-results.tsx b/apps/presentation/dashboard/src/features/personal-workspace/content-production-results.tsx new file mode 100644 index 0000000000..782cbd66e3 --- /dev/null +++ b/apps/presentation/dashboard/src/features/personal-workspace/content-production-results.tsx @@ -0,0 +1,39 @@ +import {useRef, useState} from "react"; +import {ContentMediaWorkbench} from "./content-media-workbench"; +import {ContentReferenceWorkbench} from "./content-reference-workbench"; +import {GoalAcceptanceContractSection} from "./goal-acceptance-contract"; +import {deliveryReviewCopy} from "./delivery-review-copy"; +import type {DeliveryReviewRead} from "./delivery-review-state"; +import {useWorkspaceI18n} from "./i18n"; + +/** Production stays in the current work context; checks retain their existing owner. */ +export function ContentProductionResults({goalId, review, onOpenConversation}: { + goalId: string; review: DeliveryReviewRead; onOpenConversation: () => void; +}) { + const {locale} = useWorkspaceI18n(); + const copy = (zh: string, en: string) => locale === "zh-CN" ? zh : en; + const [referencesOpen, setReferencesOpen] = useState(false); + const [checksOpen, setChecksOpen] = useState(false); + const checks = useRef(null); + return
+

{copy("内容作品", "Content artifacts")}

+
+
+ +
setChecksOpen(event.currentTarget.open)}> + {copy("Goal 交付要求与检查", "Goal delivery requirements and checks")} +

{copy("检查范围由当前 Goal 的验收方案决定。", "The current Goal acceptance plan defines the scope of these checks.")}

+ {checksOpen && !review.usable ?

{copy("验收信息暂不可读,请刷新后重试。", "Acceptance information is unavailable. Refresh to retry.")}

+
: null} + {checksOpen && review.usable ? +

{copy("可在当前对话中请 Agent 准备验收方案;准备不会运行检查。", "Ask the Agent in the current conversation to prepare an acceptance plan. Preparation does not run checks.")}

+
: null} +
+
setReferencesOpen(event.currentTarget.open)}> + {copy("准备来源风格参考", "Prepare source and style references")} + {referencesOpen ? : null} +
+
; +} diff --git a/apps/presentation/dashboard/src/features/personal-workspace/goal-capability-settings.tsx b/apps/presentation/dashboard/src/features/personal-workspace/goal-capability-settings.tsx index 29e578440d..bb32a53b80 100644 --- a/apps/presentation/dashboard/src/features/personal-workspace/goal-capability-settings.tsx +++ b/apps/presentation/dashboard/src/features/personal-workspace/goal-capability-settings.tsx @@ -17,8 +17,6 @@ import { withReportScheduleTimezone } from "./periodic-report-schedule-field"; import { localizeCapability, localizedCapabilityFieldCopy } from "./capability-localization"; import { orderCapabilitiesForPresentation, canEditCapability, CapabilityCatalogNavigation, CapabilityConfigurationSummary, CapabilityDetailHeader, CapabilityEditorStatus, CapabilityConfigurationHelp } from "./capability-workbench"; import { GoalAutoNotifyToggle } from "./notification-settings-panel"; -import { ContentReferenceWorkbench } from "./content-reference-workbench"; -import {ContentMediaWorkbench} from "./content-media-workbench"; import { DecisionContextWorkbench } from "./decision-context-workbench"; import { rewardMemoryBindingStatus, rewardMemoryScopeDraft, rewardMemoryWriteValue } from "./reward-memory-settings"; import {isMemoryCapability, memorySettingsTitle, MemorySettingsHeader} from "./memory-settings"; @@ -323,21 +321,18 @@ function CapabilityCatalog({ callbacks, catalog, goalId, loading, notification,
{memorySelected ? void onApplied()} disabled={editorBusy} /> : selected.capability_id === "content_ops" - ?

{localizedSelected.display_name}

{localizedSelected.description}

- : void onApplied()} disabled={editorBusy} /> : } {memorySelected && orderedCapabilities.some(item => item.capability_id === "decision_context") ? : null} -