Integrate project classroom with teacher and friend consultations

This commit is contained in:
鲨鱼辣椒 committed 2026-09-22 15:50:18 +08:00
1 parent e5d271bc45
commit 68e676cb62
28 files changed
+957 -314

No files matched your search

+39 -21
View File
@@ -6,6 +6,8 @@ import {
FolderKanban,
Plus,
RefreshCw,
MessageSquare,
Play,
} from 'lucide-react';
import { Button } from '@/components/ui/button';
import {
@@ -46,6 +48,8 @@ import { CodingConversationTimeline } from './CodingConversationTimeline';
import { CodingInteractionPanel } from './CodingInteractionPanel';
import { CodingWelcomeHero } from './CodingWelcomeHero';
import { createLocalConversationSnapshot } from './coding-chat-snapshot';
import { useAuthStore } from '@/stores/auth';
import type { ConsultationRole } from '../../../shared/coding-teacher';
import { TeacherChatPanel } from './TeacherChatPanel';
import type { TeacherReference } from '../../../shared/coding-teacher';
@@ -166,7 +170,9 @@ export function CodingChatPanel({
Record<string, LocalComposerAttachment[]>
>({});
const [agentBrowserOpen, setAgentBrowserOpen] = useState(false);
const [teacherOpen, setTeacherOpen] = useState(false);
const [consultationRole, setConsultationRole] = useState<ConsultationRole | null>(null);
const [consultationWidth, setConsultationWidth] = useState(380);
const account = useAuthStore((state) => state.user?.userId ?? state.user?.username ?? 'signed-out');
const [teacherQuote, setTeacherQuote] = useState<TeacherReference | undefined>();
const [teacherQuoteSource, setTeacherQuoteSource] = useState<string | null>(null);
const appliedNavigationDraftRef = useRef<string | null>(null);
@@ -666,8 +672,8 @@ export function CodingChatPanel({
}
return (
<section className="flex min-h-0 flex-1 overflow-hidden bg-background text-foreground" data-testid="coding-chat-panel">
<div className="flex min-w-0 flex-1 flex-col">
<section className="relative flex min-h-0 min-w-0 flex-1 overflow-hidden bg-background text-foreground" data-testid="coding-chat-panel" data-consultation-open={Boolean(consultationRole)}>
<div className="classroom-main flex min-w-0 flex-1 flex-col">
<CodingConversationHeader
key={`header:${targetConversationId ?? 'none'}`}
conversation={selectedConversation}
@@ -684,14 +690,26 @@ export function CodingChatPanel({
browserOpen={agentBrowserOpen}
browserAvailable={Boolean(activeProject)}
onToggleBrowser={() => setAgentBrowserOpen((current) => !current)}
projectName={activeProject?.name}
consultationRole={consultationRole}
onAskFriend={() => { setTeacherQuote(undefined); setConsultationRole((current) => current === 'friend' ? null : 'friend'); }}
onAskTeacher={() => {
const selectedText = window.getSelection()?.toString().trim();
setTeacherQuote(selectedText ? { kind: 'code', text: selectedText.slice(0, 12000) } : undefined);
setTeacherQuoteSource(`${activeProject?.id}:${targetConversationId}`);
setTeacherOpen(true);
setConsultationRole((current) => current === 'teacher' ? null : 'teacher');
}}
/>
<div className="classroom-tabs" role="tablist" aria-label="当前工作">
<button role="tab" id="classroom-chat-tab" aria-controls="classroom-chat-view" aria-selected={!agentBrowserOpen} tabIndex={!agentBrowserOpen ? 0 : -1} onClick={() => setAgentBrowserOpen(false)} onKeyDown={(event) => { if (event.key === 'ArrowRight') { setAgentBrowserOpen(true); document.getElementById('classroom-work-tab')?.focus(); } }}><MessageSquare className="h-3.5 w-3.5" />操作对话</button>
<button role="tab" id="classroom-work-tab" aria-controls="classroom-work-view" aria-selected={agentBrowserOpen} tabIndex={agentBrowserOpen ? 0 : -1} onClick={() => setAgentBrowserOpen(true)} onKeyDown={(event) => { if (event.key === 'ArrowLeft') { setAgentBrowserOpen(false); document.getElementById('classroom-chat-tab')?.focus(); } }}><Play className="h-3.5 w-3.5" />作品</button>
</div>
<div id="classroom-work-view" role="tabpanel" aria-labelledby="classroom-work-tab" hidden={!agentBrowserOpen} className="classroom-work-view min-h-0 flex-1 flex-col p-5">
<AgentBrowserPanel projectId={activeProject?.id ?? null} open={agentBrowserOpen} onOpenChange={setAgentBrowserOpen} embedded />
</div>
<div id="classroom-chat-view" role="tabpanel" aria-labelledby="classroom-chat-tab" hidden={agentBrowserOpen} className="classroom-chat-view min-h-0 flex-1 flex-col">
{(workspaceError || conversationMetadataError || connectionError) && (
<div className="mx-4 mt-3 flex min-h-10 items-center gap-2 rounded-xl bg-destructive/5 px-3 py-2 text-xs text-destructive sm:mx-5">
<CircleAlert className="h-4 w-4 shrink-0" aria-hidden="true" />
@@ -750,7 +768,7 @@ export function CodingChatPanel({
)
: (
<div className="flex min-h-0 flex-1 items-center justify-center text-sm text-muted-foreground" data-testid="coding-conversation-pending">
开始输入,创建项目的第一条会话
<div className="pb-12 text-center"><h1 className="text-2xl font-medium tracking-tight text-foreground">今天,想做点什么?</h1><p className="mt-4 text-sm leading-7">从一个小想法开始,<br />我们一起把它变成作品。</p></div>
</div>
)}
@@ -794,7 +812,7 @@ export function CodingChatPanel({
acceptedCount={acceptedRequestCount}
attachments={localAttachments.map(({ id, name, previewUrl }) => ({ id, name, previewUrl }))}
submitting={submittingRequestCount > 0}
placeholder={selectedAgent ? '随心输入' : '项目编程配置尚未就绪'}
placeholder={selectedAgent ? '说说你的想法,接下来想做什么?' : '项目编程配置尚未就绪'}
conversation={selectedConversation}
snapshot={snapshot}
voiceScopeKey={draftKey}
@@ -838,25 +856,25 @@ export function CodingChatPanel({
if (targetConversationId) await loadConversationSnapshot(targetConversationId, 'silent');
}}
/>}
</div>
</div>
{teacherOpen && targetConversationId && activeProject && (
<div className="fixed inset-y-10 right-0 z-40 w-[min(400px,100vw)] shadow-xl xl:static xl:z-auto xl:w-[360px] xl:shrink-0 xl:shadow-none">
<TeacherChatPanel key={`${activeProject.id}:${targetConversationId}`}
projectId={activeProject.id} sourceId={targetConversationId}
quote={teacherQuoteSource === `${activeProject.id}:${targetConversationId}` ? teacherQuote : undefined}
onClose={() => setTeacherOpen(false)}
onBringBack={selectedConversation?.archivedAt ? undefined : (text) => {
const current = codingConversationStore.getState().draftsByConversationId[targetConversationId]?.text ?? '';
setConversationDraft(targetConversationId, current ? current + '\n\n' + text : text);
}} />
{consultationRole && activeProject && (
<div id="coding-consultation-dock" className="classroom-consultation-dock relative min-h-0 shrink-0 border-l" style={{ width: consultationWidth }}
onKeyDown={(event) => { if (event.key === 'Escape') { setConsultationRole(null); document.querySelector<HTMLButtonElement>(`[aria-label="${consultationRole === 'friend' ? '朋友' : '老师'}"]`)?.focus(); } }}>
<div role="separator" aria-label="调整咨询栏宽度" aria-orientation="vertical" aria-valuemin={320} aria-valuemax={520} aria-valuenow={consultationWidth} tabIndex={0} className="classroom-consultation-resizer"
onPointerDown={(event) => { if (event.button === 0) { event.preventDefault(); event.currentTarget.setPointerCapture(event.pointerId); } }}
onPointerMove={(event) => { if (event.currentTarget.hasPointerCapture(event.pointerId)) setConsultationWidth(Math.max(320, Math.min(520, window.innerWidth - event.clientX))); }}
onPointerUp={(event) => { if (event.currentTarget.hasPointerCapture(event.pointerId)) event.currentTarget.releasePointerCapture(event.pointerId); }}
onKeyDown={(event) => { if (event.key === 'ArrowLeft' || event.key === 'ArrowRight') { event.preventDefault(); setConsultationWidth((width) => Math.max(320, Math.min(520, width + (event.key === 'ArrowLeft' ? 20 : -20)))); } }} />
<TeacherChatPanel key={`${account}:${activeProject.id}:${consultationRole}`}
projectId={activeProject.id} projectName={activeProject.name} sourceId={targetConversationId ?? ''} role={consultationRole}
expanded={consultationWidth > 400} onExpand={() => setConsultationWidth((width) => width > 400 ? 380 : 520)}
quote={consultationRole === 'teacher' && teacherQuoteSource === `${activeProject.id}:${targetConversationId}` ? teacherQuote : undefined}
onClose={() => { setConsultationRole(null); document.querySelector<HTMLButtonElement>(`[aria-label="${consultationRole === 'friend' ? '朋友' : '老师'}"]`)?.focus(); }}
onReturnToWork={() => { setAgentBrowserOpen(false); if (window.innerWidth < 1000) setConsultationRole(null); document.querySelector<HTMLTextAreaElement>('[data-testid="coding-message-composer"] textarea')?.focus(); }} />
</div>
)}
<AgentBrowserPanel
projectId={activeProject?.id ?? null}
open={agentBrowserOpen}
onOpenChange={setAgentBrowserOpen}
/>
</section>
);
}