Merge teacher composer and compact prompt UI into main

This commit is contained in:
鲨鱼辣椒
2026-09-29 20:40:09 +08:00
62 changed files with 2327 additions and 158 deletions

View File

@@ -0,0 +1,52 @@
import { useState, type ComponentProps } from 'react';
import { createRoot } from 'react-dom/client';
import { CodingChatBody } from '../../../src/pages/Chat/CodingChatBody';
import { CodingStartHeading } from '../../../src/pages/Chat/CodingStartHeading';
import { CodingComposer } from '../../../src/pages/Chat/CodingComposer';
import { TeacherCompanion } from '../../../src/pages/Chat/TeacherCompanion';
import { CommunityInvitation } from '../../../src/components/community/CommunityInvitation';
import '../../../src/styles/globals.css';
import '../../../src/pages/Chat/classroom-workspace.css';
export function Layout() {
const [bubble, setBubble] = useState<HTMLDivElement | null>(null);
const [draft, setDraft] = useState('');
const [sent, setSent] = useState(false);
const [dismissed, setDismissed] = useState<string[]>([]);
const agents = ['架构工程师-陈宗琦', '产品工程师-Alice'].map((name, index) => ({
teacher_id: String(index), version: 1, is_default: index === 0,
definition: { config_id: String(index), name, avatar_id: 'avatar-06',
description: index === 0 ? '有什么编程问题想一起讨论?' : '陪你把天马行空的点子,变成能玩、能用、眼睛一亮的小作品。' },
}));
const companion = {
agents, selectedAgent: agents[0], definition: agents[0].definition, enabled: true,
unreadAgentIds: [], invitation: null, welcome: null, loading: false, error: null, checking: false,
refreshing: false, refreshStatus: 'idle', refresh: async () => {}, reloadCatalog: async () => {}, dismiss: () => {},
welcomes: new URLSearchParams(location.search).has('teachers') ? agents.flatMap((agent, index) => dismissed.includes(agent.teacher_id)
? [] : [{ agentId: agent.teacher_id, definition: agent.definition, text: agent.definition.description, tone: index }]) : [],
dismissWelcome: (id: string) => setDismissed(values => [...values, id]),
} as ComponentProps<typeof TeacherCompanion>['companion'];
return <div className="flex h-screen">
{new URLSearchParams(location.search).has('sidebar') && <aside data-testid="project-sidebar" className="shrink-0 border-r p-4 text-sm" style={{ width: 210, background: '#f1f6fc' }}>我的项目</aside>}
<div className="classroom-workspace flex min-w-0 flex-1 flex-col bg-white">
<header className="relative z-50 flex h-12 shrink-0 items-center justify-end border-b px-3" data-testid="coding-conversation-header">
<span className="mr-auto min-w-0 truncate text-xs">测试项目 › 新对话</span>
<TeacherCompanion companion={companion} open={false} onOpen={() => {}} onToggle={() => {}} bubbleRef={setBubble} />
</header>
<main className="classroom-main flex min-h-0 min-w-0 flex-1 flex-col">
<div className="classroom-tabs" role="tablist"><button role="tab" aria-selected="true">操作对话</button><button role="tab">作品</button></div>
<CodingChatBody welcome={!sent} hidden={false} bubble={bubble}>
<div className="classroom-chat-view min-h-0 flex-1 flex-col">{!sent ? <CodingStartHeading /> : <p>第一条消息已发送(测试)</p>}</div>
<div className="classroom-composer-view shrink-0">
{!sent && <CommunityInvitation />}
<CodingComposer value={draft} editable canSend={Boolean(draft)} preparing={false} recovering={false}
runStatus="idle" mode="prompt" queue={{ items: [] }} error={null} recoverableError={false}
attachments={[]} submitting={false} placeholder="说说你的想法,接下来想做什么?" onChange={setDraft}
onModeChange={() => {}} onSubmit={() => { setSent(true); setDraft(''); }} onRecover={() => {}}
onAddFiles={() => {}} onRemoveAttachment={() => {}} />
</div>
</CodingChatBody>
</main>
</div></div>;
}
createRoot(document.getElementById('root')!).render(<Layout />);

View File

@@ -0,0 +1,27 @@
import { createRoot } from 'react-dom/client';
import { CodingConversationTimeline } from '../../../src/pages/Chat/CodingConversationTimeline';
import { TeacherChatPanel } from '../../../src/pages/Chat/TeacherChatPanel';
import { codingConversationStore } from '../../../src/stores/coding-conversations';
import { createProductSnapshot } from '../../fixtures/coding-conversation-product-fixtures';
import type { TeacherDefinition, TeacherTopic } from '../../../shared/coding-teacher';
import '../../../src/styles/globals.css';
import '../../../src/pages/Chat/classroom-workspace.css';
const sent = '2026-09-28T08:01:02.000Z', finished = '2026-09-28T08:01:42.000Z';
const snapshot = createProductSnapshot('timestamp-fixture', 1);
snapshot.run = { status: 'idle' };
snapshot.nodes = [
{ kind: 'message', id: 'u', role: 'user', status: 'complete', timestamp: Date.parse(sent), blocks: [{ kind: 'text', id: 'ut', status: 'complete', text: '我想做一个会长大的小宠物。' }] },
{ kind: 'message', id: 'a', role: 'assistant', status: 'complete', timestamp: Date.parse(finished), blocks: [{ kind: 'text', id: 'at', status: 'complete', text: '小宠物的家搭好啦。你可以试着点一下喂食按钮,看看它的变化。' }] },
];
codingConversationStore.getState().primeConversation(snapshot);
const definition: TeacherDefinition = { schema_version: 1, config_id: 'teacher-fixture', teacher_id: 'teacher-fixture', name: '设计老师', description: '', avatar_id: 'avatar-06', welcome_message: '', suggested_questions: [], system_prompt: '', skills: [], model: { model_id: 'fixture', reasoning_choice: { mode: 'default' } }, limits: { max_input_tokens: 8000, max_output_tokens: 1000 } };
const topic: TeacherTopic = { schemaVersion: 1, revision: 1, id: 'teacher-fixture', accountId: 'fixture', projectId: 'fixture', sourceConversationId: 'timestamp-fixture', definition, version: 1, conversation: { agentId: 'teacher-fixture', segmentTurns: 1, discussions: {} }, createdAt: sent, updatedAt: finished, requests: [{ id: 'r', createdAt: sent, finishedAt: finished, text: '我想让它长大以后变得很特别。', response: '先想一个你最希望它学会的小本领吧。你希望它会帮你做什么?', suggestedQuestions: ['我想让它陪我讲故事。', '我想让它记住我的习惯。'], references: [], sourceCursor: { workerGeneration: 1, seq: 1 }, sourceCapturedAt: sent, includedSourceMessageIds: [], omittedMessages: 0, status: 'completed' }] };
(window as typeof window & { __timestampTopic: TeacherTopic }).__timestampTopic = topic;
const agent = { teacher_id: definition.config_id!, version: 1, is_default: true, definition };
createRoot(document.getElementById('root')!).render(
<div className="classroom-workspace min-h-screen bg-white" style={{ display: 'grid', gridTemplateColumns: innerWidth >= 768 ? 'minmax(0, 1fr) 420px' : 'minmax(0, 1fr)' }}>
<main className="min-w-0 p-6" aria-label="操作对话"><h1 className="mb-8 text-lg">操作对话</h1><CodingConversationTimeline conversationId="timestamp-fixture" /></main>
<div className="consultation-pane h-[850px] border-l"><TeacherChatPanel projectId="fixture" sourceId="timestamp-fixture" selectedAgent={agent} catalogState={{ config: { enabled: true, definition }, catalog: { items: [agent] } }} /></div>
</div>,
);

View File

@@ -18,6 +18,8 @@ export function Layout() {
const [refreshing, setRefreshing] = useState(false);
const [refreshStatus, setRefreshStatus] = useState<'idle' | 'success' | 'error'>('idle');
const [selected, setSelected] = useState(0);
const welcomeMode = new URLSearchParams(location.search).has('welcomes');
const [dismissed, setDismissed] = useState<string[]>([]);
const agents = Array.from({ length: count }, (_, index) => ({
teacher_id: `agent-${index}`, version: index + 1, is_default: index === 0,
definition: { config_id: `agent-${index}`, name: `智能体${index + 1}`, avatar_id: 'avatar-06',
@@ -27,8 +29,10 @@ export function Layout() {
const companion = {
agents, unreadAgentIds: [], selectedAgent: agents[selected],
definition: agents[selected]?.definition ?? { name: '编程老师', avatar_id: 'avatar-06' },
invitation: visible ? { response: long ? '你刚才说想做一个宠物游戏。我们一起想想,什么样的小动作,会让你愿意一直陪它玩下去?可以先挑一个最想试的想法。' : '有什么编程问题想一起讨论?' } : null,
invitation: !welcomeMode && visible ? { response: long ? '你刚才说想做一个宠物游戏。我们一起想想,什么样的小动作,会让你愿意一直陪它玩下去?可以先挑一个最想试的想法。' : '有什么编程问题想一起讨论?' } : null,
welcome: null, enabled: true, loading: false, checking: false, error: null,
welcomes: welcomeMode && !open ? agents.flatMap((agent, index) => dismissed.includes(agent.teacher_id) ? [] : [{ agentId: agent.teacher_id, definition: agent.definition, text: agent.definition.description, tone: index % 3 }]) : [],
dismissWelcome: (id: string) => setDismissed(ids => [...new Set([...ids, id])]),
refreshing, refreshStatus,
refresh: async () => {
setRefreshing(true);

View File

@@ -51,6 +51,7 @@ const sentInputs: TeacherSend[] = [];
Object.defineProperty(window, '__teacherReplyInputs', { value: sentInputs });
const snapshot = () => { current.revision++; stream.dispatchEvent(new MessageEvent('snapshot', { data: JSON.stringify(current) })); };
export const teacherApi = {
models: async () => ({ supported: true, models: [{ id: 'deepseek-flash' }, { id: 'test-second-model' }] }),
conversation: async () => ({ topic: structuredClone(current), before: null }),
seenConversation: async (_projectId: string, _agentId: string, requestId: string) => {
if (current.conversation) current.conversation.seenThrough = requestId;