perf: optimize app startup and background lifecycles
This commit is contained in:
@@ -44,6 +44,7 @@ import {
|
||||
uploadImageWorkspaceAsset,
|
||||
} from '@/lib/image-workspace';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { subscribeHostEvent } from '@/lib/host-events';
|
||||
import { transcribeWorksSpeech, WorksSquareApiError } from '@/lib/works-square';
|
||||
import { useImageWorkspaceStore } from '@/stores/image-workspace';
|
||||
import { useImagePromptMuseumStore } from '@/stores/image-prompt-museum';
|
||||
@@ -254,6 +255,10 @@ function activeQuote(messages: DesignMessage[]): DesignGenerationQuote | null {
|
||||
|
||||
type RenderedDesignMessage = DesignMessage & { streaming?: boolean };
|
||||
|
||||
const CANVAS_MESSAGE_WINDOW_SIZE = 150;
|
||||
const CANVAS_MESSAGE_WINDOW_STEP = 120;
|
||||
const CANVAS_BOTTOM_THRESHOLD_PX = 48;
|
||||
|
||||
type GenerationQuoteDraft = {
|
||||
quoteId: string;
|
||||
finalPrompt: string;
|
||||
@@ -654,7 +659,7 @@ function ImageSourceAssetOption({
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
className="h-10 rounded-xl px-3 text-xs font-semibold transition-transform active:scale-[0.96]"
|
||||
className="h-10 rounded-xl px-3 text-xs font-semibold"
|
||||
disabled={busy || !url}
|
||||
onClick={onSelect}
|
||||
>
|
||||
@@ -928,6 +933,7 @@ export function ImageCanvas() {
|
||||
const workspaceError = useImageWorkspaceStore((state) => state.error);
|
||||
const load = useImageWorkspaceStore((state) => state.load);
|
||||
const connectTaskStream = useImageWorkspaceStore((state) => state.connectTaskStream);
|
||||
const disconnectTaskStream = useImageWorkspaceStore((state) => state.disconnectTaskStream);
|
||||
const markTaskStreamNeedsReconciliation = useImageWorkspaceStore(
|
||||
(state) => state.markTaskStreamNeedsReconciliation,
|
||||
);
|
||||
@@ -955,7 +961,11 @@ export function ImageCanvas() {
|
||||
const [imageSourceError, setImageSourceError] = useState<string | null>(null);
|
||||
const [uploadedImageSourceAsset, setUploadedImageSourceAsset] = useState<DesignAsset | null>(null);
|
||||
const [voiceInputState, setVoiceInputState] = useState<'idle' | 'recording' | 'transcribing'>('idle');
|
||||
const [conversationWindowStart, setConversationWindowStart] = useState<number | null>(null);
|
||||
const conversationScrollRef = useRef<HTMLElement | null>(null);
|
||||
const conversationEndRef = useRef<HTMLDivElement | null>(null);
|
||||
const conversationWindowStartRef = useRef<number | null>(null);
|
||||
const conversationShouldStickRef = useRef(true);
|
||||
const promptRef = useRef<HTMLTextAreaElement | null>(null);
|
||||
const referenceUploadInputRef = useRef<HTMLInputElement | null>(null);
|
||||
const promptSelectionRef = useRef({ start: 0, end: 0 });
|
||||
@@ -1047,6 +1057,13 @@ export function ImageCanvas() {
|
||||
});
|
||||
return messages;
|
||||
}, [conversation, pendingTurn]);
|
||||
const effectiveConversationWindowStart = conversationWindowStart === null
|
||||
? Math.max(0, conversationMessages.length - CANVAS_MESSAGE_WINDOW_SIZE)
|
||||
: Math.max(0, Math.min(conversationWindowStart, conversationMessages.length));
|
||||
const windowedConversationMessages = useMemo(
|
||||
() => conversationMessages.slice(effectiveConversationWindowStart),
|
||||
[conversationMessages, effectiveConversationWindowStart],
|
||||
);
|
||||
const taskWorkspaceId = workspace?.workspaceId ?? null;
|
||||
const taskConversationId = conversation?.conversationId ?? null;
|
||||
const submitting = submittingConversationId === taskConversationId;
|
||||
@@ -1074,15 +1091,27 @@ export function ImageCanvas() {
|
||||
useEffect(() => {
|
||||
if (!taskWorkspaceId || !taskConversationId) return;
|
||||
connectTaskStream();
|
||||
return markTaskStreamNeedsReconciliation;
|
||||
return () => {
|
||||
disconnectTaskStream();
|
||||
markTaskStreamNeedsReconciliation();
|
||||
};
|
||||
}, [
|
||||
connectTaskStream,
|
||||
disconnectTaskStream,
|
||||
markTaskStreamNeedsReconciliation,
|
||||
taskConversationId,
|
||||
taskWorkspaceId,
|
||||
]);
|
||||
|
||||
useEffect(() => subscribeHostEvent('lifecycle:pause', () => {
|
||||
disconnectTaskStream();
|
||||
markTaskStreamNeedsReconciliation();
|
||||
}), [disconnectTaskStream, markTaskStreamNeedsReconciliation]);
|
||||
|
||||
useEffect(() => {
|
||||
conversationWindowStartRef.current = null;
|
||||
conversationShouldStickRef.current = true;
|
||||
setConversationWindowStart(null);
|
||||
setPrompt('');
|
||||
setReferenceAssets([]);
|
||||
setReferenceMentionTrigger(null);
|
||||
@@ -1121,10 +1150,38 @@ export function ImageCanvas() {
|
||||
}, [consumePendingPrompt, pendingPrompt, taskConversationId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!conversationShouldStickRef.current) return;
|
||||
if (typeof conversationEndRef.current?.scrollIntoView === 'function') {
|
||||
conversationEndRef.current.scrollIntoView({ block: 'end' });
|
||||
}
|
||||
}, [conversationMessages.length, pendingTurn?.assistantText]);
|
||||
}, [conversationMessages.length, pendingTurn?.assistantText, effectiveConversationWindowStart]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!conversationShouldStickRef.current) return;
|
||||
conversationWindowStartRef.current = null;
|
||||
setConversationWindowStart(null);
|
||||
}, [conversationMessages.length]);
|
||||
|
||||
const handleConversationScroll = useCallback(() => {
|
||||
const element = conversationScrollRef.current;
|
||||
if (!element) return;
|
||||
const currentStart = conversationWindowStartRef.current
|
||||
?? Math.max(0, conversationMessages.length - CANVAS_MESSAGE_WINDOW_SIZE);
|
||||
if (element.scrollTop < 96 && currentStart > 0) {
|
||||
const previousHeight = element.scrollHeight;
|
||||
const nextStart = Math.max(0, currentStart - CANVAS_MESSAGE_WINDOW_STEP);
|
||||
conversationWindowStartRef.current = nextStart;
|
||||
setConversationWindowStart(nextStart);
|
||||
window.requestAnimationFrame(() => {
|
||||
const nextElement = conversationScrollRef.current;
|
||||
if (!nextElement) return;
|
||||
nextElement.scrollTop += nextElement.scrollHeight - previousHeight;
|
||||
});
|
||||
}
|
||||
conversationShouldStickRef.current = element.scrollHeight
|
||||
- element.scrollTop
|
||||
- element.clientHeight <= CANVAS_BOTTOM_THRESHOLD_PX;
|
||||
}, [conversationMessages.length]);
|
||||
|
||||
const repriceQuote = async (
|
||||
requestedQuoteId: string,
|
||||
@@ -1698,7 +1755,12 @@ export function ImageCanvas() {
|
||||
<div data-testid="image-canvas-page" className="flex h-full min-h-0 min-w-0 flex-col overflow-hidden bg-background font-sans text-foreground">
|
||||
<div className="flex h-full min-h-0 min-w-0 flex-1 flex-col overflow-hidden lg:flex-row">
|
||||
<section data-testid="image-workspace-conversation-pane" className="relative flex min-h-0 min-w-0 flex-1 flex-col overflow-hidden lg:h-full">
|
||||
<main data-testid="image-workspace-conversation" className="min-h-0 flex-1 overflow-y-auto overscroll-contain px-4 py-5 sm:px-6">
|
||||
<main
|
||||
ref={conversationScrollRef}
|
||||
data-testid="image-workspace-conversation"
|
||||
onScroll={handleConversationScroll}
|
||||
className="min-h-0 flex-1 overflow-y-auto overscroll-contain px-4 py-5 sm:px-6"
|
||||
>
|
||||
<div className="mx-auto flex min-h-full w-full flex-col pb-4">
|
||||
{conversationMessages.length === 0 ? (
|
||||
<div
|
||||
@@ -1716,7 +1778,19 @@ export function ImageCanvas() {
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col gap-7">
|
||||
{conversationMessages.map((message) => {
|
||||
{effectiveConversationWindowStart > 0 ? (
|
||||
<button
|
||||
type="button"
|
||||
className="mx-auto block rounded-md border border-border/70 px-3 py-1.5 text-xs text-muted-foreground hover:bg-surface-subtle hover:text-foreground"
|
||||
onClick={() => {
|
||||
conversationWindowStartRef.current = 0;
|
||||
setConversationWindowStart(0);
|
||||
}}
|
||||
>
|
||||
加载更早消息({effectiveConversationWindowStart})
|
||||
</button>
|
||||
) : null}
|
||||
{windowedConversationMessages.map((message) => {
|
||||
const assistant = message.role === 'assistant';
|
||||
return (
|
||||
<article
|
||||
@@ -1832,7 +1906,7 @@ export function ImageCanvas() {
|
||||
>
|
||||
<div
|
||||
data-testid="image-workspace-composer-surface"
|
||||
className="chat-composer-surface relative flex min-w-0 flex-col gap-2 rounded-lg border border-foreground/15 bg-background p-3 text-foreground shadow-soft transition-[background-color,border-color,box-shadow] focus-within:ring-1 focus-within:ring-foreground/10"
|
||||
className="chat-composer-surface relative flex min-w-0 flex-col gap-2 rounded-lg border border-foreground/15 bg-background p-3 text-foreground shadow-soft focus-within:ring-1 focus-within:ring-foreground/10"
|
||||
>
|
||||
{!generationAvailable ? (
|
||||
<p className="mb-0 rounded-xl bg-amber-500/10 px-3 py-2 text-xs font-semibold text-amber-700 dark:text-amber-300">
|
||||
@@ -1929,7 +2003,7 @@ export function ImageCanvas() {
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
className="motion-press h-8 rounded-full border-0 bg-transparent px-2 text-muted-foreground shadow-none hover:bg-surface-subtle hover:text-foreground active:scale-[0.96]"
|
||||
className="motion-press h-8 rounded-full border-0 bg-transparent px-2 text-muted-foreground shadow-none hover:bg-surface-subtle hover:text-foreground"
|
||||
aria-label="上传参考图"
|
||||
aria-busy={referenceUploadBusy}
|
||||
disabled={submitting || Boolean(imageSourceBusyKey) || referenceUploadBusy}
|
||||
@@ -1970,7 +2044,7 @@ export function ImageCanvas() {
|
||||
<Button
|
||||
type="submit"
|
||||
variant="ghost"
|
||||
className="motion-press h-8 w-8 rounded-full border-0 bg-foreground p-0 text-background shadow-sm hover:bg-foreground/90 active:scale-[0.94]"
|
||||
className="motion-press h-8 w-8 rounded-full border-0 bg-foreground p-0 text-background shadow-sm hover:bg-foreground/90"
|
||||
aria-label="发送给设计 Agent"
|
||||
disabled={submitting || !prompt.trim()}
|
||||
>
|
||||
@@ -2036,7 +2110,7 @@ export function ImageCanvas() {
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
aria-label={selectingImageReference ? '关闭参考图选择' : '关闭首帧选择'}
|
||||
className="h-10 w-10 shrink-0 rounded-xl transition-transform active:scale-[0.96]"
|
||||
className="h-10 w-10 shrink-0 rounded-xl"
|
||||
disabled={Boolean(imageSourceBusyKey)}
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
@@ -2056,7 +2130,7 @@ export function ImageCanvas() {
|
||||
htmlFor="design-image-source-upload"
|
||||
aria-busy={imageSourceBusyKey === 'upload'}
|
||||
className={cn(
|
||||
'inline-flex h-10 cursor-pointer items-center justify-center rounded-xl bg-brand px-3 text-xs font-semibold text-primary-foreground shadow-sm transition-transform active:scale-[0.96]',
|
||||
'inline-flex h-10 cursor-pointer items-center justify-center rounded-xl bg-brand px-3 text-xs font-semibold text-primary-foreground shadow-sm',
|
||||
imageSourceBusyKey && 'pointer-events-none opacity-50',
|
||||
)}
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user