fix(design): show submitted messages immediately
Some checks failed
Electron E2E / Electron E2E (macos-latest) (push) Has been cancelled
Electron E2E / Electron E2E (ubuntu-latest) (push) Has been cancelled
Electron E2E / Electron E2E (windows-latest) (push) Has been cancelled

This commit is contained in:
2026-09-04 08:38:13 +08:00
parent c6b0490d7f
commit 403236115b
4 changed files with 208 additions and 13 deletions

View File

@@ -1,5 +1,5 @@
import { useEffect, useRef } from 'react';
import { Loader2, MessageSquareText, Send, Sparkles } from 'lucide-react';
import { Clock3, Loader2, MessageSquareText, Send, Sparkles } from 'lucide-react';
import { toast } from 'sonner';
import { Button } from '@/components/ui/button';
import { Textarea } from '@/components/ui/textarea';
@@ -47,6 +47,19 @@ export function DesignConversationPane({ workspace }: { workspace: DesignWorkspa
const assistantStreams = useImageWorkspaceStore((state) => state.assistantStreams);
const pendingOperations = useImageWorkspaceStore((state) => state.pendingOperations);
const scrollAnchorRef = useRef<HTMLDivElement>(null);
const pendingChatMessages = Object.values(pendingOperations).flatMap((operation) => {
const command = operation.command;
if (command.kind !== 'apply_input'
|| command.workspaceId !== workspace.workspace.workspaceId
|| command.input.kind !== 'chat') {
return [];
}
return [{
operationId: operation.id,
message: command.input.message,
status: operation.status,
}];
});
const streamingOperationIds = Object.entries(assistantStreams)
.filter(([, text]) => text.trim())
.map(([operationId]) => operationId);
@@ -54,18 +67,24 @@ export function DesignConversationPane({ workspace }: { workspace: DesignWorkspa
const hasUnknownAssistantProgress = streamingOperationIds.some(
(operationId) => pendingOperations[operationId]?.status === 'unknown',
);
const submittingChat = Object.values(pendingOperations).some(
const submittingDesignInput = Object.values(pendingOperations).some(
(operation) => operation.status === 'submitting' && operation.command.kind === 'apply_input',
);
const projectedDraft = chatDraft.trim() !== ''
&& pendingChatMessages.some((message) => message.message === chatDraft.trim());
const composerDraft = projectedDraft ? '' : chatDraft;
const pendingChatKey = pendingChatMessages
.map((message) => `${message.operationId}:${message.status}`)
.join('|');
useEffect(() => {
const scrollAnchor = scrollAnchorRef.current;
if (typeof scrollAnchor?.scrollIntoView === 'function') {
scrollAnchor.scrollIntoView({ block: 'end' });
}
}, [hasAssistantProgress, workspace.turns.length]);
}, [hasAssistantProgress, pendingChatKey, workspace.turns.length]);
const submit = () => {
if (!chatDraft.trim() || submittingChat) return;
if (!composerDraft.trim() || submittingDesignInput) return;
void sendChat().catch((error) => {
toast.error(chatFailureMessage(error));
});
@@ -87,7 +106,9 @@ export function DesignConversationPane({ workspace }: { workspace: DesignWorkspa
</header>
<div className="min-h-0 flex-1 overflow-y-auto px-4 py-5">
{workspace.turns.length === 0 && !hasAssistantProgress && (
{workspace.turns.length === 0
&& pendingChatMessages.length === 0
&& !hasAssistantProgress && (
<div className="mx-auto flex h-full max-w-sm flex-col justify-center py-8">
<div className="mb-5 flex h-11 w-11 items-center justify-center rounded-2xl bg-brand/[0.09] text-brand">
<MessageSquareText className="h-5 w-5" />
@@ -128,6 +149,26 @@ export function DesignConversationPane({ workspace }: { workspace: DesignWorkspa
</div>
))}
{pendingChatMessages.map((message) => (
<div
key={message.operationId}
data-testid={`pending-design-chat-${message.operationId}`}
aria-live="polite"
aria-atomic="true"
className="ml-auto max-w-[88%]"
>
<div className="rounded-2xl rounded-br-md bg-brand/90 px-3.5 py-2.5 text-sm leading-6 text-primary-foreground">
{message.message}
</div>
<div className="mt-1 flex items-center justify-end gap-1.5 pr-1 text-[11px] text-muted-foreground">
{message.status === 'submitting'
? <Loader2 className="h-3 w-3 animate-spin" aria-hidden="true" />
: <Clock3 className="h-3 w-3" aria-hidden="true" />}
<span>{message.status === 'submitting' ? '发送中' : '正在确认'}</span>
</div>
</div>
))}
{hasAssistantProgress && (
<div
role="status"
@@ -158,14 +199,14 @@ export function DesignConversationPane({ workspace }: { workspace: DesignWorkspa
<footer className="border-t border-border/70 bg-background p-4">
<div className={cn(
'rounded-2xl border border-border/80 bg-surface-input p-2 transition-colors focus-within:border-brand/35 focus-within:ring-2 focus-within:ring-brand/10',
submittingChat && 'opacity-80',
submittingDesignInput && 'opacity-80',
)}>
<Textarea
id="design-chat-composer"
aria-label="告诉 AI 你想创作什么"
value={chatDraft}
value={composerDraft}
rows={3}
disabled={submittingChat}
disabled={submittingDesignInput}
placeholder="比如:我想做一张社团招新海报,要热闹、有活力……"
className="min-h-[72px] resize-none border-0 bg-transparent px-2 py-1.5 shadow-none focus-visible:ring-0 focus-visible:ring-offset-0"
onChange={(event) => setChatDraft(event.currentTarget.value)}
@@ -182,11 +223,11 @@ export function DesignConversationPane({ workspace }: { workspace: DesignWorkspa
type="button"
size="icon"
className="h-11 w-11 rounded-xl"
disabled={!chatDraft.trim() || submittingChat}
disabled={!composerDraft.trim() || submittingDesignInput}
aria-label="发送消息"
onClick={submit}
>
{submittingChat
{submittingDesignInput
? <Loader2 className="h-4 w-4 animate-spin" />
: <Send className="h-4 w-4" />}
</Button>