import { createContext, useContext, useState, useEffect, useMemo, useRef } from 'react' import { ThinkingIndicator } from '../thinking-indicator' import type { ResponseContextValue, ResponseRootProps, ResponseBlock, ResponseRound, ResponseStep, } from './types' // ===== Demo 数据 ===== const DEMO_STEP_1: ResponseStep = { text: 'Scanning local photo library', description: 'Indexing metadata and building a rename plan', details: [ { type: 'action', action: 'Discover photos', desc: 'Found 1,000 images in /Pictures/Unsorted' }, { type: 'action', action: 'Extract metadata', desc: 'Read EXIF timestamps, GPS, and camera model' }, { type: 'action', action: 'Detect duplicates', desc: 'Hash-based matching + perceptual similarity' }, ], } const DEMO_STEP_2: ResponseStep = { text: 'Classifying photos into albums', description: 'Clustering by time, location, and scene', details: [ { type: 'action', action: 'Time buckets', desc: 'Group into events using temporal gaps' }, { type: 'action', action: 'Location clusters', desc: 'Reverse geocode GPS into city/area labels' }, { type: 'action', action: 'Scene tags', desc: 'Detect indoor/outdoor, portraits, food, and documents' }, ], } const DEMO_STEP_3: ResponseStep = { text: 'Renaming files safely', description: 'Dry-run, collision check, then atomic move', details: [ { type: 'action', action: 'Dry run', desc: 'Preview 1,000 rename operations' }, { type: 'action', action: 'Collision handling', desc: 'Append counter suffix to avoid overwrites' }, { type: 'action', action: 'Write changes', desc: 'Move files into album folders' }, ], } const DEMO_ROUNDS: ResponseRound[] = [ { step: DEMO_STEP_1, simpleMd: "I'm **scanning** your library and building the index. Found 1,000 photos in `Pictures/Unsorted`; EXIF and duplicates are being analyzed.\n\n" }, { step: DEMO_STEP_2, simpleMd: "**Classifying** into albums by time, location, and scene. Time buckets and GPS labels are done; next I'll tag indoor/outdoor and document vs. portrait.\n\n" }, { step: DEMO_STEP_3, simpleMd: "**Dry-run** is done and collision handling is in place. No overwrites; an audit log (CSV) is ready. Pending your confirmation to run the renames.\n\n" }, ] const DEMO_FINAL_MD = `## Plan I will organize your **1,000 local photos** by building an index, clustering them into albums, and then performing a safe rename + move operation. ### Folder structure \`\`\`text Pictures/ Organized/ 2026-01-18 — San Francisco — Golden Gate Park/ 2026-01-05 — Home — Documents/ 2025-12-31 — New Year's Eve/ _Duplicates/ _Unsorted/ \`\`\` ### Naming scheme \`YYYY-MM-DD_HH-mm-ss______.\` Example: \`2026-01-18_15-42-11__San-Francisco__Outdoor__001.jpg\` ### Safety checks - [x] Dry-run all operations - [x] Detect collisions and append counters - [x] Keep an audit log (CSV) - [ ] Execute moves (requires confirmation) --- ## Next step If you want, I can generate a **preview report** first (top 50 renames + album summary) before applying changes. ` // ===== 工具函数 ===== function buildBlocks(rounds: ResponseRound[], finalMd: string): ResponseBlock[] { const blocks: ResponseBlock[] = [] for (const r of rounds) { blocks.push({ type: 'think' }) blocks.push({ type: 'composer', step: r.step }) blocks.push({ type: 'md', md: r.simpleMd }) } blocks.push({ type: 'think' }) blocks.push({ type: 'md', md: finalMd }) return blocks } // ===== Context ===== const ResponseContext = createContext(null) ResponseContext.displayName = 'ResponseContext' export function useResponseContext() { const ctx = useContext(ResponseContext) if (!ctx) throw new Error('Response compound components must be used within Response.Root') return ctx } // ===== Default ThinkingIndicator ===== const DEFAULT_THINK = // ===== Provider ===== export function ResponseRootProvider({ simulate = true, phase = 'thinking', rounds, finalMarkdown, onSimulateComplete, onStepChange, thinkingIndicator, children, }: ResponseRootProps) { const allRounds = rounds ?? DEMO_ROUNDS const finalMd = finalMarkdown ?? DEMO_FINAL_MD const thinkNode = thinkingIndicator ?? DEFAULT_THINK const blocks = useMemo(() => buildBlocks(allRounds, finalMd), [allRounds, finalMd]) const [visibleIndex, setVisibleIndex] = useState(0) const prevBlocksLengthRef = useRef(blocks.length) const isSimDone = simulate ? visibleIndex >= blocks.length - 1 : phase === 'done' const currentBlock = blocks[visibleIndex] ?? null const showThink = currentBlock?.type === 'think' // 步骤变化回调 useEffect(() => { if (!simulate || blocks.length === 0) return const block = blocks[visibleIndex] ?? null if (block?.type === 'composer' && block.step) { onStepChange?.(block.step.text, block.step.toolType) } else if (block?.type === 'think') { onStepChange?.('Thinking...') } if (visibleIndex >= blocks.length) { onSimulateComplete?.() } }, [simulate, blocks, visibleIndex, onStepChange, onSimulateComplete]) // blocks 变化时重置 useEffect(() => { if (!simulate || blocks.length === 0) return if (prevBlocksLengthRef.current !== blocks.length) { prevBlocksLengthRef.current = blocks.length // 使用 requestAnimationFrame 避免同步 setState requestAnimationFrame(() => setVisibleIndex(0)) } }, [simulate, blocks.length]) // 定时器推进 useEffect(() => { if (!simulate || blocks.length === 0) return const block = blocks[visibleIndex] ?? null if (block?.type === 'think' || block?.type === 'composer') { const id = window.setTimeout(() => setVisibleIndex((v) => Math.min(v + 1, blocks.length)), 580) return () => window.clearTimeout(id) } }, [simulate, blocks, visibleIndex]) const ctxValue: ResponseContextValue = { phase, simulate, rounds: allRounds, finalMarkdown: finalMd, blocks, visibleIndex, setVisibleIndex, isSimDone, currentBlock, showThink, onSimulateComplete, onStepChange, thinkingIndicator: thinkNode, } return ( {children} ) } ResponseRootProvider.displayName = 'ResponseRoot' // 导出 Demo 数据供测试使用 export { DEMO_ROUNDS, DEMO_FINAL_MD }