fix(design): show submitted messages immediately
This commit is contained in:
1 parent
c6b0490d7f
commit
403236115b
4 files changed
+208
-13
No files matched your search
@@ -18,6 +18,10 @@
|
|||||||
canonical turn arrives.
|
canonical turn arrives.
|
||||||
- Close the supported duplicate-stream path caused by overlapping event-source opens
|
- Close the supported duplicate-stream path caused by overlapping event-source opens
|
||||||
and repeated/out-of-order `chunkIndex` delivery.
|
and repeated/out-of-order `chunkIndex` delivery.
|
||||||
|
- Project a newly submitted chat command into the conversation immediately, before the
|
||||||
|
server returns the canonical turn. Keep the provisional message visually honest,
|
||||||
|
replace it with the canonical turn without duplication, and restore the draft after
|
||||||
|
a definitive failure.
|
||||||
- Add focused Renderer and Store regression tests. Do not change Works Square server,
|
- Add focused Renderer and Store regression tests. Do not change Works Square server,
|
||||||
Electron Main DTO/API, Current Specification authority, Quote/generation, Plugin
|
Electron Main DTO/API, Current Specification authority, Quote/generation, Plugin
|
||||||
navigation, packaging, publication, or the three foreign root task records.
|
navigation, packaging, publication, or the three foreign root task records.
|
||||||
@@ -37,6 +41,8 @@
|
|||||||
right-side “AI 听懂的想法” remains its secondary projection.
|
right-side “AI 听懂的想法” remains its secondary projection.
|
||||||
- Keep Chinese-only copy, reduced-motion behavior, at least 40 px interactive targets,
|
- Keep Chinese-only copy, reduced-motion behavior, at least 40 px interactive targets,
|
||||||
and the current responsive single-mount behavior.
|
and the current responsive single-mount behavior.
|
||||||
|
- Do not create a second chat state machine: the existing pending operation is the
|
||||||
|
provisional message identity, while only the returned Workspace turn is canonical.
|
||||||
|
|
||||||
## Project Context Loaded
|
## Project Context Loaded
|
||||||
|
|
||||||
@@ -60,6 +66,17 @@
|
|||||||
|
|
||||||
## Outcome
|
## Outcome
|
||||||
|
|
||||||
|
- A chat command now appears in the conversation immediately from its existing
|
||||||
|
`PendingDesignOperation`, before the server returns the canonical Workspace turn.
|
||||||
|
The provisional user bubble carries a small `发送中` state, changes to `正在确认`
|
||||||
|
for an unknown accepted outcome, and is filtered to the active Workspace.
|
||||||
|
- While that exact draft is projected as a provisional bubble, the composer looks
|
||||||
|
cleared without deleting the stored draft. Canonical success atomically replaces
|
||||||
|
the provisional bubble with the persisted turn; definitive failure removes the
|
||||||
|
pending projection and naturally restores the original draft for correction/retry.
|
||||||
|
- First-use guidance is hidden as soon as the first provisional user message exists.
|
||||||
|
Other in-flight Design input commands retain the existing composer lock, so the UX
|
||||||
|
change does not introduce concurrent reducer writes.
|
||||||
- `DesignConversationPane` no longer renders raw `assistantStreams` prose as an
|
- `DesignConversationPane` no longer renders raw `assistantStreams` prose as an
|
||||||
assistant chat bubble. It renders one compact `role=status` surface that explicitly
|
assistant chat bubble. It renders one compact `role=status` surface that explicitly
|
||||||
says the AI is organizing the idea and that the content is not a new reply. A
|
says the AI is organizing the idea and that the content is not a new reply. A
|
||||||
@@ -79,6 +96,20 @@
|
|||||||
|
|
||||||
## Verification
|
## Verification
|
||||||
|
|
||||||
|
- New optimistic-message red baseline: Store publication passed, while the Renderer
|
||||||
|
regression failed because no `发送中` message existed, the first-use prompt remained,
|
||||||
|
and the composer still showed the submitted draft. After the fix, the two focused
|
||||||
|
files pass `27/27`, including submitting → unknown → canonical replacement.
|
||||||
|
- Adjacent ImageCanvas, Store, Fine Tune, youth summary/projection/copy tests now pass
|
||||||
|
`6 files / 51 tests`.
|
||||||
|
- Follow-up TypeScript `tsc --noEmit`, changed-file ESLint, and production
|
||||||
|
Renderer/Main/Preload/utility Vite build pass. Build output contains only the
|
||||||
|
existing browserslist, dynamic-import, and large-chunk warnings.
|
||||||
|
- Electron/Playwright AI Design checks both pass individually: Prompt Museum media
|
||||||
|
isolation passed in the two-test run; the conversation/adjustment/Quote/Task flow
|
||||||
|
initially timed out at the pre-Design auth-fixture readiness gate, then passed `1/1`
|
||||||
|
in isolation in 1.8 seconds. The timeout occurred before this feature path and is
|
||||||
|
not recorded as product success evidence for the new provisional state.
|
||||||
- Red baseline: three new regressions failed against the original implementation—raw
|
- Red baseline: three new regressions failed against the original implementation—raw
|
||||||
unfinished prose had no progress role, an obsolete concurrent EventSource was not
|
unfinished prose had no progress role, an obsolete concurrent EventSource was not
|
||||||
closed, and replaying chunk index `0` produced duplicated text. A fourth targeted red
|
closed, and replaying chunk index `0` produced duplicated text. A fourth targeted red
|
||||||
@@ -108,8 +139,11 @@
|
|||||||
- Proposal: record that `design.assistant.delta` is unfinished progress, never a
|
- Proposal: record that `design.assistant.delta` is unfinished progress, never a
|
||||||
committed conversation turn. Renderer may show an explicit progress/uncertainty
|
committed conversation turn. Renderer may show an explicit progress/uncertainty
|
||||||
status but must not render its raw prose as a chat reply; only canonical persisted
|
status but must not render its raw prose as a chat reply; only canonical persisted
|
||||||
turns enter the timeline. Concurrent/replayed stream delivery must converge by
|
turns enter the timeline. A locally submitted chat command may appear immediately as
|
||||||
connection generation and `chunkIndex`.
|
a clearly provisional user bubble by projecting its existing pending operation; it
|
||||||
|
must disappear when the canonical turn arrives and must not become a second semantic
|
||||||
|
message store. Concurrent/replayed stream delivery must converge by connection
|
||||||
|
generation and `chunkIndex`.
|
||||||
- Evidence: the user screenshot, four red-before/green-after regressions, 49 adjacent
|
- Evidence: the user screenshot, four red-before/green-after regressions, 49 adjacent
|
||||||
tests, typecheck, scoped lint, production build, and 2/2 Electron E2E.
|
tests, typecheck, scoped lint, production build, and 2/2 Electron E2E.
|
||||||
- Future impact: later streaming polish must preserve the distinction between transient
|
- Future impact: later streaming polish must preserve the distinction between transient
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useEffect, useRef } from 'react';
|
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 { toast } from 'sonner';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Textarea } from '@/components/ui/textarea';
|
import { Textarea } from '@/components/ui/textarea';
|
||||||
@@ -47,6 +47,19 @@ export function DesignConversationPane({ workspace }: { workspace: DesignWorkspa
|
|||||||
const assistantStreams = useImageWorkspaceStore((state) => state.assistantStreams);
|
const assistantStreams = useImageWorkspaceStore((state) => state.assistantStreams);
|
||||||
const pendingOperations = useImageWorkspaceStore((state) => state.pendingOperations);
|
const pendingOperations = useImageWorkspaceStore((state) => state.pendingOperations);
|
||||||
const scrollAnchorRef = useRef<HTMLDivElement>(null);
|
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)
|
const streamingOperationIds = Object.entries(assistantStreams)
|
||||||
.filter(([, text]) => text.trim())
|
.filter(([, text]) => text.trim())
|
||||||
.map(([operationId]) => operationId);
|
.map(([operationId]) => operationId);
|
||||||
@@ -54,18 +67,24 @@ export function DesignConversationPane({ workspace }: { workspace: DesignWorkspa
|
|||||||
const hasUnknownAssistantProgress = streamingOperationIds.some(
|
const hasUnknownAssistantProgress = streamingOperationIds.some(
|
||||||
(operationId) => pendingOperations[operationId]?.status === 'unknown',
|
(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',
|
(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(() => {
|
useEffect(() => {
|
||||||
const scrollAnchor = scrollAnchorRef.current;
|
const scrollAnchor = scrollAnchorRef.current;
|
||||||
if (typeof scrollAnchor?.scrollIntoView === 'function') {
|
if (typeof scrollAnchor?.scrollIntoView === 'function') {
|
||||||
scrollAnchor.scrollIntoView({ block: 'end' });
|
scrollAnchor.scrollIntoView({ block: 'end' });
|
||||||
}
|
}
|
||||||
}, [hasAssistantProgress, workspace.turns.length]);
|
}, [hasAssistantProgress, pendingChatKey, workspace.turns.length]);
|
||||||
|
|
||||||
const submit = () => {
|
const submit = () => {
|
||||||
if (!chatDraft.trim() || submittingChat) return;
|
if (!composerDraft.trim() || submittingDesignInput) return;
|
||||||
void sendChat().catch((error) => {
|
void sendChat().catch((error) => {
|
||||||
toast.error(chatFailureMessage(error));
|
toast.error(chatFailureMessage(error));
|
||||||
});
|
});
|
||||||
@@ -87,7 +106,9 @@ export function DesignConversationPane({ workspace }: { workspace: DesignWorkspa
|
|||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div className="min-h-0 flex-1 overflow-y-auto px-4 py-5">
|
<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="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">
|
<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" />
|
<MessageSquareText className="h-5 w-5" />
|
||||||
@@ -128,6 +149,26 @@ export function DesignConversationPane({ workspace }: { workspace: DesignWorkspa
|
|||||||
</div>
|
</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 && (
|
{hasAssistantProgress && (
|
||||||
<div
|
<div
|
||||||
role="status"
|
role="status"
|
||||||
@@ -158,14 +199,14 @@ export function DesignConversationPane({ workspace }: { workspace: DesignWorkspa
|
|||||||
<footer className="border-t border-border/70 bg-background p-4">
|
<footer className="border-t border-border/70 bg-background p-4">
|
||||||
<div className={cn(
|
<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',
|
'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
|
<Textarea
|
||||||
id="design-chat-composer"
|
id="design-chat-composer"
|
||||||
aria-label="告诉 AI 你想创作什么"
|
aria-label="告诉 AI 你想创作什么"
|
||||||
value={chatDraft}
|
value={composerDraft}
|
||||||
rows={3}
|
rows={3}
|
||||||
disabled={submittingChat}
|
disabled={submittingDesignInput}
|
||||||
placeholder="比如:我想做一张社团招新海报,要热闹、有活力……"
|
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"
|
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)}
|
onChange={(event) => setChatDraft(event.currentTarget.value)}
|
||||||
@@ -182,11 +223,11 @@ export function DesignConversationPane({ workspace }: { workspace: DesignWorkspa
|
|||||||
type="button"
|
type="button"
|
||||||
size="icon"
|
size="icon"
|
||||||
className="h-11 w-11 rounded-xl"
|
className="h-11 w-11 rounded-xl"
|
||||||
disabled={!chatDraft.trim() || submittingChat}
|
disabled={!composerDraft.trim() || submittingDesignInput}
|
||||||
aria-label="发送消息"
|
aria-label="发送消息"
|
||||||
onClick={submit}
|
onClick={submit}
|
||||||
>
|
>
|
||||||
{submittingChat
|
{submittingDesignInput
|
||||||
? <Loader2 className="h-4 w-4 animate-spin" />
|
? <Loader2 className="h-4 w-4 animate-spin" />
|
||||||
: <Send className="h-4 w-4" />}
|
: <Send className="h-4 w-4" />}
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { fireEvent, render, screen, waitFor, within } from '@testing-library/react';
|
import { act, fireEvent, render, screen, waitFor, within } from '@testing-library/react';
|
||||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
import { ImageWorkspaceApiError } from '@/lib/image-workspace';
|
import { ImageWorkspaceApiError } from '@/lib/image-workspace';
|
||||||
import { ImageCanvas } from '@/pages/ImageCanvas';
|
import { ImageCanvas } from '@/pages/ImageCanvas';
|
||||||
@@ -126,6 +126,82 @@ describe('youth AI Design Canvas page', () => {
|
|||||||
expect(within(conversation).queryByText(unfinishedDraft)).not.toBeInTheDocument();
|
expect(within(conversation).queryByText(unfinishedDraft)).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('shows a submitted message immediately and replaces it with the canonical turn', async () => {
|
||||||
|
const message = '画一只在月球踢球的熊猫';
|
||||||
|
const { workspace } = prepareWorkspace({ turns: [] });
|
||||||
|
useImageWorkspaceStore.setState({
|
||||||
|
chatDraft: message,
|
||||||
|
pendingOperations: {
|
||||||
|
'operation-chat-1': {
|
||||||
|
id: 'operation-chat-1',
|
||||||
|
label: '发送消息',
|
||||||
|
command: {
|
||||||
|
kind: 'apply_input',
|
||||||
|
workspaceId: 'workspace-1',
|
||||||
|
sessionId: 'session-1',
|
||||||
|
expectedDirectionRevision: 4,
|
||||||
|
clientOperationId: 'operation-chat-1',
|
||||||
|
input: { kind: 'chat', message },
|
||||||
|
},
|
||||||
|
status: 'submitting',
|
||||||
|
error: null,
|
||||||
|
clearDraftPaths: [],
|
||||||
|
clearChatDraft: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
render(<ImageCanvas />);
|
||||||
|
|
||||||
|
const conversation = screen.getByTestId('image-workspace-conversation');
|
||||||
|
const pendingMessage = within(conversation).getByTestId('pending-design-chat-operation-chat-1');
|
||||||
|
expect(within(pendingMessage).getByText(message)).toBeInTheDocument();
|
||||||
|
expect(within(pendingMessage).getByText('发送中')).toBeInTheDocument();
|
||||||
|
expect(within(conversation).queryByRole('heading', { name: '先把脑中的画面说出来' }))
|
||||||
|
.not.toBeInTheDocument();
|
||||||
|
expect(within(conversation).getByRole('textbox', { name: '告诉 AI 你想创作什么' }))
|
||||||
|
.toHaveValue('');
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
useImageWorkspaceStore.setState((state) => ({
|
||||||
|
pendingOperations: {
|
||||||
|
...state.pendingOperations,
|
||||||
|
'operation-chat-1': {
|
||||||
|
...state.pendingOperations['operation-chat-1']!,
|
||||||
|
status: 'unknown',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
expect(within(pendingMessage).getByText('正在确认')).toBeInTheDocument();
|
||||||
|
expect(within(conversation).getByRole('textbox', { name: '告诉 AI 你想创作什么' }))
|
||||||
|
.toHaveValue('');
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
useImageWorkspaceStore.setState({
|
||||||
|
workspace: {
|
||||||
|
...workspace,
|
||||||
|
turns: [{
|
||||||
|
turnId: 'turn-2',
|
||||||
|
rawTurnSequence: 2,
|
||||||
|
userMessage: message,
|
||||||
|
assistantMessage: '听起来很有趣!你希望画面更像漫画,还是更像电影?',
|
||||||
|
}],
|
||||||
|
},
|
||||||
|
chatDraft: '',
|
||||||
|
pendingOperations: {},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(within(conversation).getAllByText(message)).toHaveLength(1);
|
||||||
|
});
|
||||||
|
expect(within(conversation).queryByTestId('pending-design-chat-operation-chat-1'))
|
||||||
|
.not.toBeInTheDocument();
|
||||||
|
expect(within(conversation).queryByText('发送中')).not.toBeInTheDocument();
|
||||||
|
expect(within(conversation).getByText(/更像漫画,还是更像电影/)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
it('invites a free description and does not turn a legacy decision prompt into a form', () => {
|
it('invites a free description and does not turn a legacy decision prompt into a form', () => {
|
||||||
const workspace = designWorkspaceFixture({
|
const workspace = designWorkspaceFixture({
|
||||||
form: designFormFixture({
|
form: designFormFixture({
|
||||||
|
|||||||
@@ -223,6 +223,50 @@ describe('V2 Living Form store', () => {
|
|||||||
expect(useImageWorkspaceStore.getState().pendingOperations).toEqual({});
|
expect(useImageWorkspaceStore.getState().pendingOperations).toEqual({});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('publishes a pending chat operation before the server result settles', async () => {
|
||||||
|
await loadedStore();
|
||||||
|
const message = '画一只在月球踢球的熊猫';
|
||||||
|
const submission = deferred<Awaited<ReturnType<typeof submitImageWorkspaceCommand>>>();
|
||||||
|
submitCommandMock.mockReturnValueOnce(submission.promise);
|
||||||
|
useImageWorkspaceStore.getState().setChatDraft(message);
|
||||||
|
|
||||||
|
const sendPromise = useImageWorkspaceStore.getState().sendChat();
|
||||||
|
|
||||||
|
expect(useImageWorkspaceStore.getState()).toMatchObject({
|
||||||
|
chatDraft: message,
|
||||||
|
pendingOperations: {
|
||||||
|
'operation-1': {
|
||||||
|
id: 'operation-1',
|
||||||
|
status: 'submitting',
|
||||||
|
clearChatDraft: true,
|
||||||
|
command: {
|
||||||
|
kind: 'apply_input',
|
||||||
|
workspaceId: 'workspace-1',
|
||||||
|
clientOperationId: 'operation-1',
|
||||||
|
input: { kind: 'chat', message },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const workspace = designWorkspaceFixture({
|
||||||
|
turns: [{
|
||||||
|
turnId: 'turn-2',
|
||||||
|
rawTurnSequence: 2,
|
||||||
|
userMessage: message,
|
||||||
|
assistantMessage: '你希望画面更像漫画,还是更像电影?',
|
||||||
|
}],
|
||||||
|
});
|
||||||
|
submission.resolve({ clientOperationId: 'operation-1', runId: 'run-2', workspace });
|
||||||
|
await sendPromise;
|
||||||
|
|
||||||
|
expect(useImageWorkspaceStore.getState()).toMatchObject({
|
||||||
|
workspace,
|
||||||
|
chatDraft: '',
|
||||||
|
pendingOperations: {},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('settles a definitive reasoner failure without losing the chat draft', async () => {
|
it('settles a definitive reasoner failure without losing the chat draft', async () => {
|
||||||
const source = await loadedStore();
|
const source = await loadedStore();
|
||||||
useImageWorkspaceStore.getState().setChatDraft('画一只会做饭的机器人');
|
useImageWorkspaceStore.getState().setChatDraft('画一只会做饭的机器人');
|
||||||
|
|||||||
Reference in new issue
Block a user