feat: integrate pixel teacher presence and resilient classroom preview

This commit is contained in:
鲨鱼辣椒 committed 2026-09-23 18:23:53 +08:00
1 parent 12d800ec51
commit 7951cca700
60 files changed
+3313 -180

No files matched your search

+29 -4
View File
@@ -1,3 +1,6 @@
import { OPEN_WORK_PROMPT } from '../../../shared/coding-work-preview';
import { CONTINUE_CODING_PROMPT, type CodingRecoveryAction } from '../../../shared/coding-recovery';
import { CodingRecoveryCard } from './CodingRecoveryCard';
import {
memo,
type ReactNode,
@@ -488,6 +491,10 @@ const MessageNode = memo(function MessageNode({
}).catch(() => undefined);
}, [answerText]);
if (user && (answerText === OPEN_WORK_PROMPT || answerText === CONTINUE_CODING_PROMPT)) {
return null;
}
return (
<article
data-node-id={node.id}
@@ -1282,6 +1289,9 @@ const AssistantCommentary = memo(function AssistantCommentary({
total + (block.kind === 'image' ? 1 : block.text.length)
), 0);
// The turn-level recovery card carries the next action; an empty error adds no useful detail.
if (blocks.length === 0 && node.status === 'error') return null;
return (
<div
data-node-id={node.id}
@@ -1305,7 +1315,7 @@ const AssistantCommentary = memo(function AssistantCommentary({
preview={contentBlocksProgressPreview(
blocks,
node.status === 'error'
? '本次处理失败。'
? '这一步当时没有完成。'
: node.status === 'aborted'
? '本次处理已中止。'
: streaming
@@ -1317,7 +1327,6 @@ const AssistantCommentary = memo(function AssistantCommentary({
{blocks.map((block) => (
<ContentBlock key={block.id} block={block} markdown compactMarkdown />
))}
{blocks.length === 0 && node.status === 'error' && <p className="text-destructive">本次处理失败。</p>}
{blocks.length === 0 && node.status === 'aborted' && <p className="text-muted-foreground">本次处理已中止。</p>}
</div>
</CompactProcessText>
@@ -1439,7 +1448,7 @@ const ProcessGroup = memo(function ProcessGroup({
const summary = active
? `处理中${duration ? ` ${duration}` : ''}`
: failed
? `${failureMessage ?? '处理失败'}${duration ? ` · ${duration}` : ''}`
? `${failureMessage ?? (latest ? '这一步还没有完成' : '这一步当时没有完成')}${duration ? ` · ${duration}` : ''}`
: `已处理${duration ? ` ${duration}` : ''}`;
return (
@@ -1560,19 +1569,29 @@ const TimelineTurnView = memo(function TimelineTurnView({
latest,
onFork,
assistantName,
onRecovery,
workReady,
}: {
turn: TimelineTurn;
run: ConversationRunState;
latest: boolean;
onFork?(sourceEntryId: string): void;
assistantName?: string;
onRecovery?(action: CodingRecoveryAction, work: boolean): Promise<void> | void;
workReady?: boolean;
}) {
const work = turn.user?.blocks.some((block) => block.kind === 'text' && block.text === OPEN_WORK_PROMPT) ?? false;
const failed = processGroupIsFailed(turn.process, run, latest, turn.final) && !runIsActive(run);
const resolvedWork = work && workReady;
const persistedError = [...turn.process].reverse().find((item) => item.kind === 'notice' && item.node.level === 'error');
const code = run.error?.code ?? (persistedError?.kind === 'notice' ? persistedError.node.code : undefined);
return (
<section className="space-y-3.5" data-testid="coding-conversation-turn">
{turn.user && <MessageNode node={turn.user} onFork={onFork} />}
{turn.process.length > 0 && (
{turn.process.length > 0 && !resolvedWork && (
<ProcessGroup items={turn.process} run={run} latest={latest} final={turn.final} />
)}
{latest && failed && !resolvedWork && onRecovery && <CodingRecoveryCard key={`${turn.id}:${code}`} code={code} work={work} onRecovery={onRecovery} />}
{turn.final && (
<MessageNode
node={turn.final}
@@ -1587,10 +1606,14 @@ export const CodingConversationTimeline = memo(function CodingConversationTimeli
conversationId,
onFork,
assistantName,
onRecovery,
workReady,
}: {
conversationId: string;
onFork?(sourceEntryId: string): void;
assistantName?: string;
onRecovery?(action: CodingRecoveryAction, work: boolean): Promise<void> | void;
workReady?: boolean;
}) {
const selectNodes = useCallback((state: CodingConversationStoreState) => (
selectCodingConversationSnapshot(conversationId)(state)?.nodes ?? EMPTY_NODES
@@ -1670,6 +1693,8 @@ export const CodingConversationTimeline = memo(function CodingConversationTimeli
latest={index === turns.length - 1}
onFork={onFork}
assistantName={assistantName}
onRecovery={onRecovery}
workReady={workReady}
/>
))}
{nodes.length === 0 && (