feat: add creative welcome and external works square links

This commit is contained in:
鲨鱼辣椒 committed 2026-09-29 15:37:37 +08:00
1 parent 4199cadd1e
commit f8a7880da3
18 files changed
+428 -41

No files matched your search

+29
View File
@@ -0,0 +1,29 @@
import { useLayoutEffect, useRef, type ReactNode } from 'react';
/** Keep the same composer mounted while the empty state becomes a conversation. */
export function CodingChatBody({ welcome, hidden, bubble, children }: {
welcome: boolean; hidden: boolean; bubble?: HTMLElement | null; children: ReactNode;
}) {
const ref = useRef<HTMLDivElement>(null);
useLayoutEffect(() => {
const body = ref.current;
if (!body || !welcome || hidden || !bubble) return;
const update = () => {
const area = body.getBoundingClientRect();
const message = bubble.getBoundingClientRect();
const overlaps = message.height > 0 && message.right > area.left && message.left < area.right && message.bottom > area.top;
// Wide workspaces put the welcome beside the teachers. Compact ones flow below them.
body.style.paddingRight = overlaps && area.width >= 1050 ? `${Math.ceil(area.right - message.left + 24)}px` : '';
body.style.paddingTop = overlaps && area.width < 1050 ? `${Math.ceil(message.bottom - area.top + 24)}px` : '';
};
update();
const observer = typeof ResizeObserver === 'undefined' ? null : new ResizeObserver(update);
observer?.observe(body); observer?.observe(bubble);
window.addEventListener('resize', update);
return () => {
observer?.disconnect(); window.removeEventListener('resize', update);
body.style.paddingRight = ''; body.style.paddingTop = '';
};
}, [welcome, hidden, bubble]);
return <div ref={ref} className="coding-chat-body" data-welcome={welcome} hidden={hidden}>{children}</div>;
}
+9 -1
View File
@@ -47,6 +47,9 @@ import { CodingConversationHeader } from './CodingConversationHeader';
import { CodingConversationTimeline } from './CodingConversationTimeline';
import { CodingInteractionPanel } from './CodingInteractionPanel';
import { CodingWelcomeHero } from './CodingWelcomeHero';
import { CodingStartHeading } from './CodingStartHeading';
import { CodingChatBody } from './CodingChatBody';
import { CommunityInvitation } from '@/components/community/CommunityInvitation';
import { createLocalConversationSnapshot } from './coding-chat-snapshot';
import { useAuthStore } from '@/stores/auth';
import { TeacherChatPanel } from './TeacherChatPanel';
@@ -676,6 +679,8 @@ export function CodingChatPanel({
});
}, [loginRecovery, activeProject, targetConversationId, entryLoadState, workspaceLoadState, handleRecovery, draftKey, onLoginRecoveryConsumed]);
const autoCreating = Boolean(selectedAgent && creatingAgentIds[selectedAgent.id]);
const showWelcome = editable && !selectedConversation?.archivedAt && !running && runStatus === 'idle'
&& (!targetConversationId || (entryLoadState === 'live' && snapshot?.nodes.length === 0));
if (!activeProject && workspaceLoadState !== 'loading') {
return (
@@ -807,6 +812,7 @@ export function CodingChatPanel({
}
}} />
</div>
<CodingChatBody welcome={showWelcome} hidden={agentBrowserOpen} bubble={teacherBubble}>
<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) && (
@@ -829,7 +835,7 @@ export function CodingChatPanel({
</div>
)}
{targetConversationId
{showWelcome ? <CodingStartHeading /> : targetConversationId
? (
<CodingConversationTimeline
key={targetConversationId}
@@ -959,6 +965,8 @@ export function CodingChatPanel({
}}
/>}
</div>
{showWelcome && <CommunityInvitation />}
</CodingChatBody>
</div>
{consultationOpen && activeProject && (
+11
View File
@@ -0,0 +1,11 @@
import { Sparkles, Star } from 'lucide-react';
import mark from '@/assets/consultations/mark.svg';
export function CodingStartHeading() {
return <div className="coding-start-heading" data-testid="coding-start-heading">
<span className="coding-start-mark"><img src={mark} alt="麦洛" /><Sparkles aria-hidden="true" /><Star aria-hidden="true" /></span>
<p className="coding-start-eyebrow">小点子,也能变成大惊喜</p>
<h1>今天,你想<span>创造什么?</span></h1>
<p className="coding-start-caption">一个游戏、一段故事,或是一个天马行空的想法。</p>
</div>;
}
+3 -3
View File
@@ -471,7 +471,7 @@ export function TeacherChatPanel({
(request) => request.status === 'preparing' || request.status === 'running'
);
const helpUnavailable = busy || !topicEnabled || Boolean(running);
const askForSuggestions = () => void send({ text: '不知道问什么,你先帮我看看👀', intent: 'suggestions' });
const askForSuggestions = () => void send({ text: '帮我整体看🧠', intent: 'suggestions' });
const changeText = (next: string) => { setText(next); persistDraft(next, references, pending.current); };
const removeReferences = () => { setReferences([]); persistDraft(text, [], pending.current); };
const questions = definition?.suggested_questions ?? [];
@@ -541,7 +541,7 @@ export function TeacherChatPanel({
{request.suggestedQuestions?.map((question, index) => <button key={`${request.id}:${index}`} type="button" disabled={helpUnavailable} onClick={() => void send({ text: question })} className="consultation-follow-up"><span className="min-w-0 flex-1">{question}</span><ChevronRight className="h-4 w-4 shrink-0 opacity-70" aria-hidden="true" /></button>)}
</div>
</>}
{['failed', 'cancelled', 'interrupted'].includes(request.status) && <button type="button" disabled={helpUnavailable} onClick={askForSuggestions} className="consultation-retry"><span>不知道问什么,<span className="whitespace-nowrap">你先帮我看看👀</span></span></button>}
{['failed', 'cancelled', 'interrupted'].includes(request.status) && <button type="button" disabled={helpUnavailable} onClick={askForSuggestions} className="consultation-retry">帮我整体看🧠</button>}
</> : request.response ? <TeacherReply text={request.response} /> : <div className="text-[13px] leading-7">{['running', 'preparing'].includes(request.status) ? <span role="status" className="inline-flex items-center gap-2 text-muted-foreground"><Loader2 className="h-3.5 w-3.5 animate-spin motion-reduce:animate-none" />{label}正在想…</span> : ['cancelled', 'interrupted'].includes(request.status) ? '这次先停在这里。想继续时可以再说说。' : ''}</div>}
{request.intent !== 'suggestions' && request.status === 'completed' && !request.replyIncomplete && Boolean(request.suggestedQuestions?.length) && <div className="consultation-follow-ups" aria-label="接着聊">
{request.suggestedQuestions?.map((question, index) => <button type="button" key={`${request.id}:${index}`} disabled={helpUnavailable} onClick={() => void send({ text: question })} className="consultation-follow-up"><span className="min-w-0 flex-1">{question}</span><ChevronRight className="h-4 w-4 shrink-0 opacity-70" aria-hidden="true" /></button>)}
@@ -585,7 +585,7 @@ export function TeacherChatPanel({
className="min-h-[60px] w-full resize-none border-0 bg-transparent text-[13px] leading-6 outline-none placeholder:text-muted-foreground/65" />
<div className="mt-2 flex min-h-[68px] items-center justify-between gap-2" data-testid="consultation-composer-actions">
<div className="min-w-0 flex-1">
{!draftRevision && !text.trim() && <button type="button" disabled={helpUnavailable} onClick={askForSuggestions} className="consultation-hint"><span>不知道问什么,<span className="whitespace-nowrap">你先帮我看看👀</span></span></button>}
{!draftRevision && !text.trim() && <button type="button" disabled={helpUnavailable} onClick={askForSuggestions} className="consultation-hint">帮我整体看🧠</button>}
</div>
{running ? <button type="button" className="rounded-lg border px-3 py-2 text-xs" onClick={() => void teacherApi.cancel(topicBase, endpointId, running.id).then(consume).catch(() => setError('暂时无法停止,请稍后重试。'))}>停止回复</button>
: <button type="submit" aria-label="提问" title={busy ? '正在发送…' : '发送'} disabled={busy || !topicEnabled || !text.trim()} className="consultation-send flex h-8 w-8 items-center justify-center rounded-full text-white disabled:opacity-30"><ArrowUp className="h-4 w-4" /></button>}