Integrate project classroom with teacher and friend consultations
This commit is contained in:
1 parent
e5d271bc45
commit
68e676cb62
28 files changed
+957
-314
No files matched your search
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user