merge: integrate remote main
This commit is contained in:
@@ -1,5 +1,12 @@
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { Clock3, Loader2, MessageSquareText, Send, Sparkles } from 'lucide-react';
|
||||
import {
|
||||
Clock3,
|
||||
Loader2,
|
||||
MessageSquareText,
|
||||
Send,
|
||||
Sparkles,
|
||||
WandSparkles,
|
||||
} from 'lucide-react';
|
||||
import { toast } from 'sonner';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
@@ -40,10 +47,19 @@ function chatFailureMessage(error: unknown): string {
|
||||
return '暂时没能确认这条消息的处理结果,内容还在输入框里';
|
||||
}
|
||||
|
||||
export function DesignConversationPane({ workspace }: { workspace: DesignWorkspace }) {
|
||||
export function DesignConversationPane({
|
||||
workspace,
|
||||
canRequestQuote,
|
||||
onQuoteOffered,
|
||||
}: {
|
||||
workspace: DesignWorkspace;
|
||||
canRequestQuote: boolean;
|
||||
onQuoteOffered: () => void;
|
||||
}) {
|
||||
const chatDraft = useImageWorkspaceStore((state) => state.chatDraft);
|
||||
const setChatDraft = useImageWorkspaceStore((state) => state.setChatDraft);
|
||||
const sendChat = useImageWorkspaceStore((state) => state.sendChat);
|
||||
const requestQuote = useImageWorkspaceStore((state) => state.requestQuote);
|
||||
const assistantStreams = useImageWorkspaceStore((state) => state.assistantStreams);
|
||||
const pendingOperations = useImageWorkspaceStore((state) => state.pendingOperations);
|
||||
const scrollAnchorRef = useRef<HTMLDivElement>(null);
|
||||
@@ -60,28 +76,32 @@ export function DesignConversationPane({ workspace }: { workspace: DesignWorkspa
|
||||
status: operation.status,
|
||||
}];
|
||||
});
|
||||
const streamingOperationIds = Object.entries(assistantStreams)
|
||||
.filter(([, text]) => text.trim())
|
||||
.map(([operationId]) => operationId);
|
||||
const hasAssistantProgress = streamingOperationIds.length > 0;
|
||||
const hasUnknownAssistantProgress = streamingOperationIds.some(
|
||||
(operationId) => pendingOperations[operationId]?.status === 'unknown',
|
||||
);
|
||||
const submittingDesignInput = Object.values(pendingOperations).some(
|
||||
(operation) => operation.status === 'submitting' && operation.command.kind === 'apply_input',
|
||||
);
|
||||
const quoteRequestPending = Object.values(pendingOperations).some(
|
||||
(operation) => operation.command.kind === 'request_quote'
|
||||
&& operation.command.workspaceId === workspace.workspace.workspaceId,
|
||||
);
|
||||
const streamingReplies = pendingChatMessages.flatMap((message) => {
|
||||
const text = assistantStreams[message.operationId]?.trim();
|
||||
return text ? [{ ...message, text }] : [];
|
||||
});
|
||||
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('|');
|
||||
const streamingReplyKey = streamingReplies
|
||||
.map((reply) => `${reply.operationId}:${reply.text}`)
|
||||
.join('|');
|
||||
useEffect(() => {
|
||||
const scrollAnchor = scrollAnchorRef.current;
|
||||
if (typeof scrollAnchor?.scrollIntoView === 'function') {
|
||||
scrollAnchor.scrollIntoView({ block: 'end' });
|
||||
}
|
||||
}, [hasAssistantProgress, pendingChatKey, workspace.turns.length]);
|
||||
}, [canRequestQuote, pendingChatKey, streamingReplyKey, workspace.turns.length]);
|
||||
|
||||
const submit = () => {
|
||||
if (!composerDraft.trim() || submittingDesignInput) return;
|
||||
@@ -90,6 +110,15 @@ export function DesignConversationPane({ workspace }: { workspace: DesignWorkspa
|
||||
});
|
||||
};
|
||||
|
||||
const prepareQuote = () => {
|
||||
if (!canRequestQuote || quoteRequestPending || pendingChatMessages.length > 0) return;
|
||||
void requestQuote()
|
||||
.then(onQuoteOffered)
|
||||
.catch(() => {
|
||||
toast.error('制作方案还没准备好,请稍后再试');
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<section
|
||||
data-testid="image-workspace-conversation"
|
||||
@@ -107,8 +136,7 @@ export function DesignConversationPane({ workspace }: { workspace: DesignWorkspa
|
||||
|
||||
<div className="min-h-0 flex-1 overflow-y-auto px-4 py-5">
|
||||
{workspace.turns.length === 0
|
||||
&& pendingChatMessages.length === 0
|
||||
&& !hasAssistantProgress && (
|
||||
&& pendingChatMessages.length === 0 && (
|
||||
<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" />
|
||||
@@ -169,26 +197,45 @@ export function DesignConversationPane({ workspace }: { workspace: DesignWorkspa
|
||||
</div>
|
||||
))}
|
||||
|
||||
{hasAssistantProgress && (
|
||||
{streamingReplies.map((reply) => (
|
||||
<div
|
||||
key={reply.operationId}
|
||||
data-testid={`streaming-design-assistant-${reply.operationId}`}
|
||||
role="status"
|
||||
aria-live="polite"
|
||||
data-testid="design-assistant-progress"
|
||||
className="mr-auto flex max-w-[92%] items-start gap-3 rounded-xl bg-brand/[0.06] px-3.5 py-3 text-muted-foreground shadow-sm"
|
||||
aria-atomic="true"
|
||||
className="mr-auto max-w-[92%] rounded-2xl rounded-bl-md border border-brand/20 bg-background px-3.5 py-3 text-sm leading-6 text-foreground shadow-sm"
|
||||
>
|
||||
<span className="mt-0.5 flex h-7 w-7 shrink-0 items-center justify-center rounded-full bg-background text-brand shadow-sm">
|
||||
<Loader2 className="h-3.5 w-3.5 animate-spin" aria-hidden="true" />
|
||||
</span>
|
||||
<div className="min-w-0">
|
||||
<p className="text-xs font-semibold text-foreground">
|
||||
{hasUnknownAssistantProgress ? '正在确认刚才的整理结果' : 'AI 正在整理你的想法'}
|
||||
</p>
|
||||
<p className="mt-0.5 text-xs leading-5 text-muted-foreground">
|
||||
{hasUnknownAssistantProgress
|
||||
? '结果回来后会显示在对话里,不需要重复发送。'
|
||||
: '这不是新的回复,完成后会显示下一步问题或建议。'}
|
||||
{reply.text}
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="ml-1 inline-block h-3.5 w-1 animate-pulse rounded-full bg-brand align-middle"
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{canRequestQuote && pendingChatMessages.length === 0 && (
|
||||
<div
|
||||
data-testid="design-conversation-quote-action"
|
||||
className="flex flex-col gap-3 rounded-2xl border border-brand/20 bg-brand/[0.045] p-4 sm:flex-row sm:items-center sm:justify-between"
|
||||
>
|
||||
<div>
|
||||
<p className="text-sm font-semibold text-foreground">想法已经清楚了</p>
|
||||
<p className="mt-1 text-xs leading-5 text-muted-foreground">
|
||||
先看看需要多少设计点,确认后才会开始制作。
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
type="button"
|
||||
className="min-h-11 shrink-0 rounded-xl"
|
||||
disabled={quoteRequestPending}
|
||||
onClick={prepareQuote}
|
||||
>
|
||||
{quoteRequestPending
|
||||
? <Loader2 className="mr-2 h-4 w-4 animate-spin" aria-hidden="true" />
|
||||
: <WandSparkles className="mr-2 h-4 w-4" aria-hidden="true" />}
|
||||
{quoteRequestPending ? '正在准备方案' : '下一步:看看制作方案'}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
@@ -177,7 +177,11 @@ export function DesignQuotePanel({ workspace }: { workspace: DesignWorkspace })
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="rounded-2xl border border-brand/25 bg-background p-4 shadow-sm">
|
||||
<section
|
||||
id="design-offered-quote"
|
||||
data-testid="design-offered-quote"
|
||||
className="rounded-2xl border border-brand/25 bg-background p-4 shadow-sm"
|
||||
>
|
||||
<div className="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
|
||||
<div>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
|
||||
@@ -33,6 +33,7 @@ export type YouthCreationCardProps = {
|
||||
generationAvailable: boolean;
|
||||
onOpenFineTune: () => void;
|
||||
onFocusComposer: () => void;
|
||||
onQuoteOffered?: () => void;
|
||||
};
|
||||
|
||||
function formatDuration(seconds: number): string {
|
||||
@@ -335,19 +336,23 @@ export function YouthCreationCard({
|
||||
generationAvailable,
|
||||
onOpenFineTune,
|
||||
onFocusComposer,
|
||||
onQuoteOffered,
|
||||
}: YouthCreationCardProps) {
|
||||
const requestQuote = useImageWorkspaceStore((state) => state.requestQuote);
|
||||
const pendingOperations = useImageWorkspaceStore((state) => state.pendingOperations);
|
||||
const model = projectYouthCreationCard(workspace, { quoteBlockers });
|
||||
const requesting = Object.values(pendingOperations).some(
|
||||
(operation) => operation.status === 'submitting' && operation.command.kind === 'request_quote',
|
||||
(operation) => operation.command.kind === 'request_quote'
|
||||
&& operation.command.workspaceId === workspace.workspace.workspaceId,
|
||||
);
|
||||
|
||||
const request = () => {
|
||||
if (!generationAvailable || requesting || !model.canRequestQuote) return;
|
||||
void requestQuote().catch(() => {
|
||||
toast.error('制作方案还没准备好,请稍后再试');
|
||||
});
|
||||
void requestQuote()
|
||||
.then(() => onQuoteOffered?.())
|
||||
.catch(() => {
|
||||
toast.error('制作方案还没准备好,请稍后再试');
|
||||
});
|
||||
};
|
||||
const hasMoreDetails = model.mustInclude.length > 0
|
||||
|| model.mustPreserve.length > 0
|
||||
|
||||
@@ -185,6 +185,15 @@ export function ImageCanvas() {
|
||||
setCreationCardOpen(false);
|
||||
setFineTuneOpen(true);
|
||||
};
|
||||
const revealOfferedQuote = () => {
|
||||
if (!desktopLayout) setCreationCardOpen(true);
|
||||
globalThis.setTimeout(() => {
|
||||
const quote = document.getElementById('design-offered-quote');
|
||||
if (typeof quote?.scrollIntoView === 'function') {
|
||||
quote.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
||||
}
|
||||
}, 0);
|
||||
};
|
||||
const productionContent = (
|
||||
<div className="space-y-5">
|
||||
<DesignPendingOperationsPanel />
|
||||
@@ -270,7 +279,13 @@ export function ImageCanvas() {
|
||||
|
||||
<main className="grid min-h-0 flex-1 lg:grid-cols-[minmax(0,1.8fr)_minmax(320px,0.72fr)]">
|
||||
<div className="flex min-h-0 lg:border-r lg:border-border/70">
|
||||
<DesignConversationPane workspace={workspace} />
|
||||
<DesignConversationPane
|
||||
workspace={workspace}
|
||||
canRequestQuote={generationAvailable
|
||||
&& cardModel.phase === 'ready'
|
||||
&& cardModel.canRequestQuote}
|
||||
onQuoteOffered={revealOfferedQuote}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{desktopLayout && (
|
||||
@@ -286,6 +301,7 @@ export function ImageCanvas() {
|
||||
generationAvailable={generationAvailable}
|
||||
onOpenFineTune={openFineTune}
|
||||
onFocusComposer={focusComposer}
|
||||
onQuoteOffered={revealOfferedQuote}
|
||||
/>
|
||||
{productionContent}
|
||||
</div>
|
||||
@@ -309,6 +325,7 @@ export function ImageCanvas() {
|
||||
generationAvailable={generationAvailable}
|
||||
onOpenFineTune={openFineTune}
|
||||
onFocusComposer={focusComposer}
|
||||
onQuoteOffered={revealOfferedQuote}
|
||||
/>
|
||||
{productionContent}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user