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 7a8894e390..1b9c07f824 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, interactive learning models and local videos. Review before sharing; video rendering uses an optional local extension."}, + content_ops: {displayName: "Content production", description: "Prepare source-backed outlines and local videos. Review the result before sharing; rendering uses an optional local extension."}, 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-experience-scene.ts b/apps/presentation/dashboard/src/features/personal-workspace/content-experience-scene.ts deleted file mode 100644 index 34dea3f323..0000000000 --- a/apps/presentation/dashboard/src/features/personal-workspace/content-experience-scene.ts +++ /dev/null @@ -1,45 +0,0 @@ -import type {ExperiencePlan, ExperienceRun, ExperienceFrame} from "../../../../../../loopx/control_plane/capabilities/content_experience_contract"; - -export type ExperienceSceneRow = {label: string; run: ExperienceRun | null; index: number}; -export function experienceSceneHeight(width: number, count: number) {return 100 + 258 * (width < 640 ? count : 1);} -export function experienceFrameFeedback(frame: ExperienceFrame, status: ExperienceRun["status"] | undefined, cn: boolean) { - if (cn && !frame.applied && status === "budget_exhausted") return "剩余模型预算不足,状态与已用预算保持原值。"; - if (cn && !frame.applied && status === "action_unavailable") return "当前条件没有这一步的转移规则,状态保持原值。"; - return frame.feedback; -} - -/** Draw only this model's own content. Recording never captures another page. */ -export function drawExperienceScene(ctx: CanvasRenderingContext2D, width: number, height: number, - model: ExperiencePlan, rows: ExperienceSceneRow[], digest: string, locale: string) { - ctx.clearRect(0, 0, width, height); ctx.fillStyle = "#f5f6f8"; ctx.fillRect(0, 0, width, height); - const text = (s: string, x: number, y: number, size: number, color = "#1a202c", maxWidth = width - x - 20, lines = 1) => { - ctx.font = `${size >= 20 ? "600" : "400"} ${size}px system-ui, sans-serif`; ctx.fillStyle = color; - const words = s.split(/\s+/u); let line = "", used = 0; - for (const word of words) { - const candidate = line ? `${line} ${word}` : word; - if (ctx.measureText(candidate).width > maxWidth && line) { - if (++used === lines) {ctx.fillText(`${line.slice(0, -1)}…`, x, y, maxWidth); return;} - ctx.fillText(line, x, y, maxWidth); y += size * 1.4; line = word; - } else line = candidate; - } - ctx.fillText(line, x, y, maxWidth); - }; - text(model.title, 20, 31, width < 500 ? 19 : 23); - text(model.boundary, 20, 54, 12, "#596272"); - const vertical = width < 640, gap = 12, cellWidth = vertical ? width - 32 : (width - 32 - gap * (rows.length - 1)) / rows.length; - rows.forEach((row, i) => { - const x = vertical ? 16 : 16 + i * (cellWidth + gap), y = 73 + (vertical ? i * 258 : 0); - ctx.fillStyle = "#fff"; ctx.fillRect(x, y, cellWidth, 244); ctx.strokeStyle = "#d8dce3"; ctx.lineWidth = 1; ctx.strokeRect(x + .5, y + .5, cellWidth - 1, 243); - const trace = row.run?.trace ?? [{state: model.initial_state, action: null, feedback: model.states.find(s => s.id === model.initial_state)!.description, spent: 0, applied: true}]; - const at = Math.min(row.index, trace.length - 1), frame = trace[at]!, state = model.states.find(s => s.id === frame.state)!; - text(row.label, x + 15, y + 23, 14, "#596272", cellWidth - 30); - const conditions = row.run ? model.parameters.map(p => p.choices.find(c => c.id === row.run!.parameters[p.id])!.label).join(" · ") : (locale === "zh-CN" ? "等待运行" : "Awaiting a run"); - text(conditions, x + 15, y + 44, 12, "#596272", cellWidth - 30, 2); - text(state.label, x + 15, y + 88, cellWidth < 340 ? 23 : 28, "#165ed3", cellWidth - 30); - text(experienceFrameFeedback(frame, row.run?.status, locale === "zh-CN"), x + 15, y + 115, 15, "#303a49", cellWidth - 30, 3); - text(`${locale === "zh-CN" ? "模型用量" : "Model effort"} ${frame.spent} / ${model.budget} · ${at} ${locale === "zh-CN" ? "步" : "decisions"}`, x + 15, y + 201, 14, "#303a49", cellWidth - 30); - const trail = trace.slice(Math.max(0, at - 3), at + 1).map(f => model.states.find(s => s.id === f.state)!.label).join(" → "); - text(trail, x + 15, y + 224, 12, "#596272", cellWidth - 30); - }); - text(`Source revision ${digest.slice(7, 19)} · model illustration`, 20, height - 12, 11, "#68717e"); -} diff --git a/apps/presentation/dashboard/src/features/personal-workspace/content-experience-workbench.css b/apps/presentation/dashboard/src/features/personal-workspace/content-experience-workbench.css deleted file mode 100644 index c13c5036ab..0000000000 --- a/apps/presentation/dashboard/src/features/personal-workspace/content-experience-workbench.css +++ /dev/null @@ -1,56 +0,0 @@ -.content-experience {padding:18px 0 26px; color:var(--pw-text);} -.experience-heading {display:flex; align-items:flex-start; justify-content:space-between; gap:18px; margin-bottom:18px;} -.content-experience h3,.content-experience h4 {margin:0 0 8px; font-size:var(--text-lead); font-weight:600; line-height:1.4;} -.content-experience p {margin:6px 0 14px; font-size:var(--text-body); line-height:1.65; color:var(--pw-muted);} -.content-experience button,.content-experience select {min-height:44px; border:1px solid var(--pw-line-strong); border-radius:6px; background:var(--pw-card); color:inherit; padding:9px 12px; font:inherit; font-size:var(--text-body);} -.content-experience button {cursor:pointer;} -.content-experience button:disabled,.content-experience select:disabled {opacity:.5; cursor:default;} -.content-experience button:focus-visible,.content-experience select:focus-visible,.content-experience textarea:focus-visible,.content-experience a:focus-visible,.content-experience input:focus-visible,.content-experience [tabindex]:focus-visible {outline:2px solid var(--pw-blue); outline-offset:3px;} -.content-experience label {display:grid; gap:7px; font-size:var(--text-body); min-width:0;} -.content-experience select {width:100%; min-width:0; text-overflow:ellipsis;} -.experience-model-picker {max-width:500px; margin-bottom:20px;} -.experience-player {min-width:0;} -.experience-player:fullscreen {overflow:auto; box-sizing:border-box; padding:24px max(20px,calc((100vw - 1160px)/2)); background:var(--pw-bg); color:var(--pw-text);} -.experience-focus {margin:2px 0 8px;} -.experience-goal {padding:20px 0 10px; border-top:1px solid var(--pw-line);} -.experience-goal>span {display:block; font-size:var(--text-caption); margin-bottom:7px; color:var(--pw-muted);} -.experience-goal h4 {font-size:24px; letter-spacing:-.02em;} -.experience-modes {display:flex; flex-wrap:wrap; gap:6px; margin:10px 0 22px;} -.experience-modes button {border-color:transparent;} -.experience-modes button[aria-pressed=true] {background:var(--pw-hover); border-color:var(--pw-line-strong); font-weight:600;} -.experience-controls {display:flex; gap:14px; align-items:flex-end; margin-bottom:14px; flex-wrap:wrap;} -.experience-controls label {flex:1; max-width:460px;} -.experience-comparison {display:grid; grid-template-columns:minmax(0,1fr) minmax(0,1fr); gap:16px; margin:14px 0;} -.experience-comparison fieldset {min-width:0; margin:0; border:1px solid var(--pw-line); border-radius:7px; padding:14px; display:grid; gap:12px;} -.experience-comparison legend {padding:0 6px; font-size:var(--text-body); font-weight:600;} -.content-experience .experience-primary {background:var(--pw-text); color:var(--pw-bg); border-color:var(--pw-text); font-weight:600;} -.experience-results {display:grid; grid-template-columns:1fr 1fr; gap:16px; margin:15px 0;} -.experience-results p {margin:0;} -.experience-results strong,.experience-results span {display:block;} -.experience-results strong {color:var(--pw-text); font-size:var(--text-body);} -.experience-results span {font-size:var(--text-caption);} -.experience-game-status {border-left:3px solid var(--pw-blue); padding:4px 0 4px 16px; margin:20px 0;} -.experience-game-status>strong {display:block; font-size:22px; margin-bottom:5px;} -.experience-game-status>span {font-size:var(--text-body); color:var(--pw-muted);} -.experience-game-status p {margin-bottom:4px;} -.experience-actions {display:flex; gap:10px; flex-wrap:wrap; margin:16px 0;} -.experience-actions button {display:grid; text-align:left; gap:5px; min-width:160px;} -.experience-actions small {font-size:var(--text-caption); color:var(--pw-muted);} -.experience-replay {padding:12px 0; font-size:var(--text-caption)!important; line-height:1.6;} -.experience-replay input {width:100%; min-height:32px; accent-color:var(--pw-blue);} -.experience-canvas {display:block; width:100%; height:358px; margin:20px 0; border:1px solid var(--pw-line); border-radius:8px;} -.experience-record {display:flex; align-items:center; flex-wrap:wrap; gap:12px; margin:14px 0; font-size:var(--text-body);} -.experience-record [aria-pressed=true] {border-color:var(--pw-blue); color:var(--pw-blue);} -.experience-reflection {margin:14px 0;} -.experience-reflection textarea {min-height:68px; resize:vertical; padding:10px; border-radius:6px; background:var(--pw-card); color:inherit; border:1px solid var(--pw-line-strong); font:inherit;} -.content-experience details {margin-top:18px; font-size:var(--text-caption); line-height:1.6; overflow-wrap:anywhere;} -.content-experience summary {cursor:pointer; color:var(--pw-muted); min-height:32px;} -.content-experience details ul {padding-left:20px;} -.experience-rules {overflow-x:auto;} -.experience-rules table {width:100%; min-width:520px; border-collapse:collapse; font-size:var(--text-caption);} -.experience-rules th,.experience-rules td {padding:10px; text-align:left; border-bottom:1px solid var(--pw-line); vertical-align:top;} -.content-production-tabs {display:flex; gap:6px; padding:12px 0; border-bottom:1px solid var(--pw-line); overflow-x:auto;} -.content-production-tabs button {min-height:44px; flex-shrink:0; padding:8px 14px; border:1px solid transparent; border-radius:6px; color:var(--pw-muted); background:transparent; font:inherit; cursor:pointer;} -.content-production-tabs button[aria-selected=true] {border-color:var(--pw-line-strong); color:var(--pw-text); background:var(--pw-hover);} -.content-production-tabs button:focus-visible {outline:2px solid var(--pw-blue); outline-offset:2px;} -@media(max-width:680px) {.experience-heading {flex-wrap:wrap;}.experience-comparison,.experience-results {grid-template-columns:minmax(0,1fr);}.experience-modes {display:grid; grid-template-columns:1fr;}.experience-modes button {text-align:left;}.experience-actions {display:grid; grid-template-columns:1fr;}.experience-model-picker {max-width:none;}.experience-goal h4 {font-size:22px;}} diff --git a/apps/presentation/dashboard/src/features/personal-workspace/content-experience-workbench.tsx b/apps/presentation/dashboard/src/features/personal-workspace/content-experience-workbench.tsx deleted file mode 100644 index 6200dad85f..0000000000 --- a/apps/presentation/dashboard/src/features/personal-workspace/content-experience-workbench.tsx +++ /dev/null @@ -1,179 +0,0 @@ -import {useEffect, useRef, useState} from "react"; -import type {ExperiencePlan, ExperienceRun} from "../../../../../../loopx/control_plane/capabilities/content_experience_contract"; -import {drawExperienceScene, experienceSceneHeight, experienceFrameFeedback, type ExperienceSceneRow} from "./content-experience-scene"; -import {useWorkspaceI18n} from "./i18n"; -import "./content-experience-workbench.css"; - -type Artifact = {experience_id: string; revision: number; plan_digest: string; plan: ExperiencePlan}; -type Entry = {experience_id: string; title: string; status: string; revision?: number}; -type Mode = "explore" | "game" | "world"; -const ENDPOINT = "/api/chat/content-experience"; -async function request(goalId: string, action: string, values: Record = {}) { - const response = await fetch(ENDPOINT, {method: "POST", headers: {"Content-Type": "application/json"}, body: JSON.stringify({goal_id: goalId, action, ...values})}); - if (!response.ok) throw new Error("source_unavailable"); - return response.json(); -} -function download(value: unknown, filename: string) { - const url = URL.createObjectURL(new Blob([JSON.stringify(value, null, 2)], {type: "application/json"})); - const anchor = document.createElement("a"); anchor.href = url; anchor.download = filename; anchor.click(); setTimeout(() => URL.revokeObjectURL(url), 1000); -} - -export function ContentExperienceWorkbench({goalId, onRecordingChange}: {goalId: string; onRecordingChange?: (active: boolean) => void}) { - const {locale} = useWorkspaceI18n(), cn = locale === "zh-CN"; - const [entries, setEntries] = useState(null), [selected, setSelected] = useState(null); - const [error, setError] = useState(""), [loading, setLoading] = useState(false), [recording, setRecording] = useState(false); - const generation = useRef(0); - async function load(ident?: string) { - const revision = ++generation.current; setLoading(true); setError(""); - try { - const library = await request(goalId, "list"); - const target = ident ?? library.experiences.find((e: Entry) => e.status === "ready")?.experience_id; - const item = target ? await request(goalId, "inspect", {experience_id: target}) : null; - if (revision === generation.current) {setEntries(library.experiences); setSelected(item);} - } catch {if (revision === generation.current) setError(cn ? "模型不可读或来源已变化。请重新读取,或让 Agent 修复原模型。" : "The model is unreadable or changed. Reload, or ask your Agent to repair its source.");} - finally {if (revision === generation.current) setLoading(false);} - } - useEffect(() => {setSelected(null); setEntries(null); void load(); return () => {generation.current++;};}, [goalId]); - return
-

{cn ? "把机制变成可操作的学习任务" : "Turn a mechanism into a learning task"}

-

{cn ? "让 Agent 准备目标、条件与转移规则,在这里探索、决策并回看结果。" : "Have your Agent prepare the goal, conditions and transition rules. Explore, decide and inspect the result here."}

-
- {error ?

{error}

: null} - {loading ?

{cn ? "正在读取模型…" : "Loading the model…"}

: null} - {entries && !entries.length ?

{cn ? "还没有准备好的模型。把要解释的机制交给 Agent,它会通过 Content-Ops 保存可复用的学习任务。" : "No model is prepared yet. Tell your Agent what to explain; Content-Ops can retain the reusable learning task."}

: null} - {entries?.length ? : null} - {selected ? void load(selected.experience_id)} - onRecording={value => {setRecording(value); onRecordingChange?.(value);}} /> : null} -
; -} - -function ExperiencePlayer({artifact, goalId, stale, reload, onRecording}: {artifact: Artifact; goalId: string; stale: boolean; reload: () => void; onRecording: (active: boolean) => void}) { - const {locale} = useWorkspaceI18n(), cn = locale === "zh-CN", p = artifact.plan; - const [mode, setMode] = useState("explore"), [error, setError] = useState(""), [busy, setBusy] = useState(false); - const [needsReload, setNeedsReload] = useState(false); - useEffect(() => {setNeedsReload(false); setError("");}, [artifact]); - const defaults = Object.fromEntries(p.parameters.map(v => [v.id, v.default])); - const [conditions, setConditions] = useState([ {...defaults, ...p.scenarios[0]!.parameters}, {...defaults, ...(p.scenarios[1] ?? p.scenarios[0])!.parameters} ]); - const [explorePolicy, setExplorePolicy] = useState(p.policies[0]!.id), [explored, setExplored] = useState([]); - const [gameScenario, setGameScenario] = useState(p.scenarios[0]!.id), [actions, setActions] = useState([]), [game, setGame] = useState(null); - const [worldScenarios, setWorldScenarios] = useState([p.scenarios[0]!.id, p.scenarios[0]!.id]), [worldPolicies, setWorldPolicies] = useState([p.policies[0]!.id, (p.policies[1] ?? p.policies[0])!.id]); - const [world, setWorld] = useState([]), [positions, setPositions] = useState([0, 0]); - const [prediction, setPrediction] = useState(""), [explanation, setExplanation] = useState(""); - const [recording, setRecording] = useState(false), [recordUrl, setRecordUrl] = useState(""); - const [recordAvailable, setRecordAvailable] = useState(false); - const pending = useRef(false), mounted = useRef(true), player = useRef(null), canvas = useRef(null), recorder = useRef(null), recordFile = useRef(""); - const [focused, setFocused] = useState(false); - const recordUrlRef = useRef(""); - const releaseRecording = useRef(() => {}); - useEffect(() => {const changed = () => setFocused(document.fullscreenElement === player.current); document.addEventListener("fullscreenchange", changed); return () => document.removeEventListener("fullscreenchange", changed);}, []); - useEffect(() => {mounted.current = true; setRecordAvailable(typeof MediaRecorder !== "undefined" && typeof HTMLCanvasElement.prototype.captureStream === "function"); - return () => {mounted.current = false; if (recorder.current?.state === "recording") recorder.current.stop(); releaseRecording.current(); if (recordUrlRef.current) URL.revokeObjectURL(recordUrlRef.current); onRecording(false);};}, []); - const status = (r?: ExperienceRun | null) => ({in_progress: cn ? "等待下一步" : "Next decision", goal_reached: cn ? "模型目标达成" : "Model goal reached", terminal: cn ? "目标未达成" : "Goal not reached", budget_exhausted: cn ? "预算不足" : "Budget exhausted", action_unavailable: cn ? "条件不允许这一步" : "Action unavailable", step_limit: cn ? "达到步数上限" : "Step limit", policy_incomplete: cn ? "策略未覆盖当前状态" : "Policy incomplete"}[r?.status ?? "in_progress"]); - async function run(input: Record): Promise { - return request(goalId, "run", {experience_id: artifact.experience_id, expected_revision: artifact.revision, run: input}); - } - async function perform(work: () => Promise) { - if (pending.current || stale) return; pending.current = true; setBusy(true); setError(""); - try {await work();} catch {if (mounted.current) {setNeedsReload(true); setError(cn ? "本次结果未确认,原结果保留。请重新读取来源后再操作。" : "This result is unconfirmed; the last result is retained. Reload the source before continuing.");}} - finally {pending.current = false; if (mounted.current) setBusy(false);} - } - const gameState = p.states.find(s => s.id === (game?.trace.at(-1)?.state ?? p.initial_state))!; - const rows: ExperienceSceneRow[] = mode === "game" ? [{label: cn ? "你的决策" : "Your decisions", run: game, index: game ? game.trace.length - 1 : 0}] - : mode === "explore" ? [0, 1].map(i => ({label: `${cn ? "条件" : "Conditions"} ${i ? "B" : "A"}`, run: explored[i] ?? null, index: explored[i] ? explored[i]!.trace.length - 1 : 0})) - : [0, 1].map(i => ({label: p.policies.find(v => v.id === worldPolicies[i])!.label, run: world[i] ?? null, index: positions[i]!})); - useEffect(() => { - const el = canvas.current; if (!el) return; - let animation = 0; - const draw = () => { - const width = Math.max(280, Math.round(el.getBoundingClientRect().width)), height = experienceSceneHeight(width, rows.length), ratio = Math.min(devicePixelRatio || 1, 2); - if (el.width !== Math.round(width * ratio) || el.height !== Math.round(height * ratio)) {el.width = Math.round(width * ratio); el.height = Math.round(height * ratio); el.style.height = `${height}px`;} - const ctx = el.getContext("2d"); if (ctx) {ctx.setTransform(ratio, 0, 0, ratio, 0, 0); drawExperienceScene(ctx, width, height, p, rows, artifact.plan_digest, locale);} - if (recording) animation = requestAnimationFrame(draw); - }; - const observer = new ResizeObserver(() => {if (!recording) draw();}); observer.observe(el); draw(); - return () => {cancelAnimationFrame(animation); observer.disconnect();}; - }, [mode, explored, game, world, positions, locale, recording]); - function startRecording() { - if (!canvas.current || recording || !recordAvailable) return; - let stream: MediaStream | null = null; - try { - const type = ["video/webm;codecs=vp9", "video/webm;codecs=vp8", "video/webm"].find(v => MediaRecorder.isTypeSupported(v)); - if (!type) throw new Error("codec unavailable"); - stream = canvas.current.captureStream(24); const chunks: Blob[] = []; - const capture = new MediaRecorder(stream, {mimeType: type}); recorder.current = capture; - if (recordUrlRef.current) {URL.revokeObjectURL(recordUrlRef.current); recordUrlRef.current = ""; setRecordUrl("");} - recordFile.current = `${artifact.experience_id}-${mode}.webm`; - let failed = false; - capture.ondataavailable = event => {if (event.data.size) chunks.push(event.data);}; - const timer = setTimeout(() => {if (capture.state === "recording") capture.stop();}, 120000); - const activeStream = stream; - const release = () => {clearTimeout(timer); activeStream.getTracks().forEach(t => t.stop());}; - releaseRecording.current = release; - capture.onstop = () => { - release(); - if (!mounted.current || recorder.current !== capture) return; - setRecording(false); onRecording(false); - if (failed) return; - const blob = new Blob(chunks, {type}); - if (!blob.size) {setError(cn ? "录制没有产生文件,请重试。" : "Recording produced no file. Retry."); return;} - if (recordUrlRef.current) URL.revokeObjectURL(recordUrlRef.current); - recordUrlRef.current = URL.createObjectURL(blob); setRecordUrl(recordUrlRef.current); - }; - capture.onerror = () => {failed = true; if (capture.state === "recording") capture.stop(); release(); if (mounted.current && recorder.current === capture) {setRecording(false); onRecording(false); setError(cn ? "录制失败,请重试。" : "Recording failed. Retry.");}}; - capture.start(250); setRecording(true); onRecording(true); - } catch {stream?.getTracks().forEach(t => t.stop()); releaseRecording.current(); setError(cn ? "此浏览器无法录制模型画面,仍可导出操作轨迹。" : "This browser cannot record the model. You can still export its decision trace.");} - } - async function toggleFocus() { - try { - if (focused) await document.exitFullscreen(); - else if (player.current?.requestFullscreen) await player.current.requestFullscreen(); - else throw new Error("fullscreen unavailable"); - } catch {setError(cn ? "专注视图暂不可用,可继续在当前页面操作。" : "Focus view is unavailable; continue in this page.");} - } - const disabled = busy || stale || needsReload; - const scenarioOptions = p.scenarios.map(s => ); - const policyOptions = p.policies.map(s => ); - return
-
{cn ? "学习目标" : "Learning goal"}

{p.title}

{p.learning_goal}

-
-
{(["explore", "game", "world"] as Mode[]).map((value, i) => )}
- {error ?

{error}

: null} - {mode === "explore" ?
-

{cn ? "保持策略相同,调整两组条件。先预测路径和成本,再运行并解释差异。" : "Keep the policy fixed and change the conditions. Predict the path and cost, then run and explain the difference."}

-
-
{[0, 1].map(i =>
{`${cn ? "条件" : "Conditions"} ${i ? "B" : "A"}`}{p.parameters.map(parameter => )}
)}
-
{cn ? "记录你的预测(可选)" : "Record your prediction (optional)"}