diff --git a/website/theme/components/HomeLayout.tsx b/website/theme/components/HomeLayout.tsx index 7e867d1..620514b 100644 --- a/website/theme/components/HomeLayout.tsx +++ b/website/theme/components/HomeLayout.tsx @@ -1,8 +1,4 @@ -import { - useEffect, - useState, - type KeyboardEvent as ReactKeyboardEvent, -} from 'react'; +import { useEffect, useRef, useState } from 'react'; import { useLang, useSite, useVersion, withBase } from '@rspress/core/runtime'; import { InnerLine, @@ -505,14 +501,36 @@ const runtimeFlowSteps = [ const runtimePlayerPhases = [ { - id: 'prepare', + id: 'input', code: '01', - stage: { zh: '准备', en: 'PREPARE' }, - navTitle: { zh: '准备上下文', en: 'Prepare context' }, - title: { zh: '正在准备上下文', en: 'Preparing context' }, + stage: { zh: '输入', en: 'INPUT' }, + navTitle: { zh: '提交任务', en: 'Submit task' }, + title: { zh: '输入任务', en: 'Enter a task' }, summary: { - zh: '已创建本次运行,并准备好项目指令、历史和可用工具。', - en: 'The run is created with the project instructions, history, and available tools it needs.', + zh: '通过 ? 启动一次本地 DeepResearch 任务。', + en: 'Start a local DeepResearch task with ?.', + }, + action: { + zh: '提交终端输入', + en: 'Submit terminal input', + }, + result: { + zh: '任务进入 Session', + en: 'Task enters the Session', + }, + command: 'AgentSession::stream()', + event: runtimeFlowSteps[0].event, + tags: ['A3S TUI', 'AgentSession'], + }, + { + id: 'prepare', + code: '02', + stage: { zh: '会话', en: 'SESSION' }, + navTitle: { zh: '载入工作区', en: 'Load workspace' }, + title: { zh: '载入工作区与本地策略', en: 'Load workspace and policy' }, + summary: { + zh: 'AGENTS.md · .a3s/config.acl', + en: 'AGENTS.md · .a3s/config.acl', }, action: { zh: '取得运行权,组装 messages[] 与 tools[]', @@ -528,16 +546,16 @@ const runtimePlayerPhases = [ }, { id: 'decide', - code: '02', - stage: { zh: '判断', en: 'DECIDE' }, - navTitle: { zh: '决定下一步', en: 'Choose next step' }, + code: '03', + stage: { zh: '计划', en: 'PLAN' }, + navTitle: { zh: '拆分检查项', en: 'Plan checks' }, title: { - zh: '模型选择只读搜索', - en: 'The model chooses a read-only search', + zh: '规划本地证据检查', + en: 'Plan local evidence checks', }, summary: { - zh: '模型选择 grep 搜索 TODO 和 FIXME;此时还没有改动仓库。', - en: 'The model chooses grep for TODO and FIXME. The repository has not been changed.', + zh: 'CHANGELOG · CI workflow · release checks', + en: 'CHANGELOG · CI workflow · release checks', }, action: { zh: '生成 grep({ pattern: "TODO|FIXME" })', @@ -553,16 +571,16 @@ const runtimePlayerPhases = [ }, { id: 'execute', - code: '03', - stage: { zh: '执行', en: 'EXECUTE' }, - navTitle: { zh: '执行工具', en: 'Run the tool' }, + code: '04', + stage: { zh: '执行', en: 'RUN' }, + navTitle: { zh: '执行检查', en: 'Run checks' }, title: { - zh: '正在工作区执行', - en: 'Running in the workspace', + zh: '执行只读工具与检查命令', + en: 'Run read-only tools and checks', }, summary: { - zh: '参数与权限检查通过,A3S 完成只读搜索并找到 3 条匹配。', - en: 'Argument and permission checks pass. A3S completes the read-only search and finds 3 matches.', + zh: 'read → glob → bash', + en: 'read → glob → bash', }, action: { zh: '参数 → 权限 → Workspace.grep()', @@ -576,18 +594,43 @@ const runtimePlayerPhases = [ event: `${runtimeFlowSteps[3].event} · ${runtimeFlowSteps[4].event}`, tags: ['ToolInvoker', 'Workspace', 'Artifact'], }, + { + id: 'verify', + code: '05', + stage: { zh: '验证', en: 'VERIFY' }, + navTitle: { zh: '验证结论', en: 'Verify result' }, + title: { + zh: '汇总证据并通过质量门', + en: 'Synthesize evidence and pass quality gates', + }, + summary: { + zh: 'workspace evidence · quality gate passed', + en: 'workspace evidence · quality gate passed', + }, + action: { + zh: '检查来源覆盖与验证结果', + en: 'Check source coverage and verification', + }, + result: { + zh: '报告可以发布', + en: 'Report is publishable', + }, + command: 'verify → synthesize', + event: 'verification_end', + tags: ['VerificationReport', 'Trace'], + }, { id: 'record', - code: '04', - stage: { zh: '完成', en: 'DONE' }, - navTitle: { zh: '保存记录', en: 'Save the record' }, + code: '06', + stage: { zh: '制品', en: 'ARTIFACTS' }, + navTitle: { zh: '发布制品', en: 'Publish artifacts' }, title: { - zh: '检查完成', - en: 'Inspection complete', + zh: '报告制品已生成', + en: 'Report artifacts are ready', }, summary: { - zh: '结果、运行记录和快照已经保存,之后可以随时查看或恢复。', - en: 'The result, run record, and snapshot are saved for later inspection or recovery.', + zh: 'report.md · index.html', + en: 'report.md · index.html', }, action: { zh: '发布事件并提交本次 generation', @@ -615,7 +658,7 @@ const runtimePlayerPhases = [ tags: string[]; }>; -type RuntimePlayerPhase = (typeof runtimePlayerPhases)[number]; +const tuiLifecycleDurations = [2200, 1100, 1350, 1800, 1350, 3000]; const copy = { zh: { @@ -669,19 +712,26 @@ const copy = { boundaryContract: 'API 与事件', boundaryHostLabel: '你的应用', boundaryHostRole: '账号、权限与界面', - stackTitle: '一次执行', + stackTitle: 'TUI 生命周期', stackHint: '点击阶段查看过程', stackHintMobile: '点击步骤展开', stackTop: '产品', stackBottom: '记录', - flowTaskLabel: '任务', - flowTask: '检查仓库并列出发布阻塞项', + flowTaskLabel: '输入', + flowTask: '? 不联网检查这个仓库的发布风险,并生成报告', flowReplay: '重播', - flowRunning: '回放中', + flowRunning: '运行中', flowStage: '阶段', - flowSaved: '运行记录已保存', + flowSaved: '制品已发布', flowReplayable: '过程可回放', - flowRecoverable: '结果可恢复', + flowRecoverable: 'Session 可恢复', + tuiWorkspace: '~/workspace/a3s', + tuiMode: 'LOCAL ONLY', + tuiWaiting: '等待任务完成', + tuiArtifacts: '制品', + tuiReady: '2 个文件', + tuiOpenView: '打开预览', + tuiArtifactPath: '.a3s/research/release-risk/', tutorialStep: '步骤', tutorialCode: '代码', tutorialLayers: '当前负责的层', @@ -745,19 +795,26 @@ const copy = { boundaryContract: 'APIs + EVENTS', boundaryHostLabel: 'YOUR APP', boundaryHostRole: 'OWNS UI + ACCESS', - stackTitle: 'One run', + stackTitle: 'TUI lifecycle', stackHint: 'SELECT A PHASE TO INSPECT IT', stackHintMobile: 'TAP A STEP TO EXPAND', stackTop: 'PRODUCT', stackBottom: 'RECORDS', - flowTaskLabel: 'TASK', - flowTask: 'Inspect the repository and list release blockers', + flowTaskLabel: 'INPUT', + flowTask: '? Audit this repo for release risks offline; generate a report', flowReplay: 'REPLAY', - flowRunning: 'REPLAYING', + flowRunning: 'RUNNING', flowStage: 'PHASE', - flowSaved: 'RUN RECORD SAVED', + flowSaved: 'ARTIFACTS PUBLISHED', flowReplayable: 'REPLAYABLE', - flowRecoverable: 'RECOVERABLE', + flowRecoverable: 'SESSION RECOVERABLE', + tuiWorkspace: '~/workspace/a3s', + tuiMode: 'LOCAL ONLY', + tuiWaiting: 'WAITING FOR TASK', + tuiArtifacts: 'ARTIFACTS', + tuiReady: '2 FILES', + tuiOpenView: 'OPEN VIEW', + tuiArtifactPath: '.a3s/research/release-risk/', tutorialStep: 'STEP', tutorialCode: 'CODE', tutorialLayers: 'ACTIVE LAYER', @@ -858,32 +915,43 @@ function InstallSwitcher({ ); } -function RuntimePlayerStage({ - phase, - labels, +function TuiLifecycleRows({ + activeIndex, locale, }: { - phase: RuntimePlayerPhase; - labels: (typeof copy)[Locale]; + activeIndex: number; locale: Locale; }) { return ( -
- -
- - {labels.flowStage} {phase.code} / {localeValue(phase.stage, locale)} - -

{localeValue(phase.title, locale)}

-

{localeValue(phase.summary, locale)}

-
-
+
+ {runtimePlayerPhases.slice(1, -1).map((phase, rowIndex) => { + const phaseIndex = rowIndex + 1; + const isActive = activeIndex === phaseIndex; + const isComplete = activeIndex > phaseIndex; + + return ( +
+ +
+ {localeValue(phase.title, locale)} + {localeValue(phase.summary, locale)} +
+
+ ); + })} +
); } @@ -894,156 +962,190 @@ function RuntimeExecutionFlow({ labels: (typeof copy)[Locale]; locale: Locale; }) { + const playerRef = useRef(null); + const hasStartedRef = useRef(false); const [activeIndex, setActiveIndex] = useState( runtimePlayerPhases.length - 1, ); const [isPlaying, setIsPlaying] = useState(false); + const [isVisible, setIsVisible] = useState(false); + const [typedCount, setTypedCount] = useState(labels.flowTask.length); const active = runtimePlayerPhases[activeIndex] ?? runtimePlayerPhases[0]; + const isRunning = isPlaying && isVisible; + const isArtifactsReady = activeIndex === runtimePlayerPhases.length - 1; useEffect(() => { - if (!isPlaying) return undefined; + const player = playerRef.current; + if (!player) return undefined; + + if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) { + setActiveIndex(runtimePlayerPhases.length - 1); + return undefined; + } + + const observer = new IntersectionObserver( + ([entry]) => { + const visible = entry?.isIntersecting ?? false; + setIsVisible(visible); - const delay = activeIndex === runtimePlayerPhases.length - 1 ? 780 : 1280; + if (visible && !hasStartedRef.current) { + hasStartedRef.current = true; + setTypedCount(0); + setActiveIndex(0); + setIsPlaying(true); + } + }, + { threshold: 0.35 }, + ); + + observer.observe(player); + return () => observer.disconnect(); + }, []); + + useEffect(() => { + if (!isRunning) return undefined; + + const delay = tuiLifecycleDurations[activeIndex] ?? 1200; const timer = window.setTimeout(() => { - if (activeIndex === runtimePlayerPhases.length - 1) { - setIsPlaying(false); + if (activeIndex >= runtimePlayerPhases.length - 1) { + setTypedCount(0); + setActiveIndex(0); } else { - setActiveIndex((index) => - Math.min(index + 1, runtimePlayerPhases.length - 1), - ); + setActiveIndex((index) => index + 1); } }, delay); return () => window.clearTimeout(timer); - }, [activeIndex, isPlaying]); + }, [activeIndex, isRunning]); - function selectStep(index: number) { - setIsPlaying(false); - setActiveIndex(index); - } + useEffect(() => { + if (!isRunning || activeIndex !== 0) return undefined; + if (typedCount >= labels.flowTask.length) return undefined; - function playFlow() { - if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) { - setActiveIndex(runtimePlayerPhases.length - 1); - setIsPlaying(false); - return; - } + const interval = Math.max( + 18, + Math.floor(1550 / Math.max(labels.flowTask.length, 1)), + ); + const timer = window.setTimeout( + () => setTypedCount((count) => count + 1), + interval, + ); + return () => window.clearTimeout(timer); + }, [activeIndex, isRunning, labels.flowTask.length, typedCount]); + + function playFlow() { + if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return; + setTypedCount(0); setActiveIndex(0); setIsPlaying(true); } - function handleStepKeyDown( - event: ReactKeyboardEvent, - index: number, - ) { - let nextIndex: number | undefined; - - if (event.key === 'ArrowDown' || event.key === 'ArrowRight') { - nextIndex = (index + 1) % runtimePlayerPhases.length; - } else if (event.key === 'ArrowUp' || event.key === 'ArrowLeft') { - nextIndex = - (index - 1 + runtimePlayerPhases.length) % runtimePlayerPhases.length; - } else if (event.key === 'Home') { - nextIndex = 0; - } else if (event.key === 'End') { - nextIndex = runtimePlayerPhases.length - 1; - } - - if (nextIndex === undefined) return; - - event.preventDefault(); - selectStep(nextIndex); - const inspector = event.currentTarget.closest('.a3s-runtime-inspector'); - inspector - ?.querySelectorAll('.a3s-run-player-phase') - .item(nextIndex) - .focus(); - } - return (
-
+