迁移画布生成器与 Agent 输入提交
生成面板改用公共 Lexical 输入组件 移除原生 form 提交并改为显式 React 回调 更新画布 Agent 草稿输入与相关行为测试
This commit is contained in:
+59
-55
@@ -16,6 +16,7 @@ import type {
|
||||
EditorAgentMessage,
|
||||
EditorAgentMessageResponse,
|
||||
} from '@/packages/shared/src/contracts';
|
||||
import { setPlainTextEditorValue } from '@/src/components/common/AutoGrowTextArea.test-utils.ts';
|
||||
import type { EditorAgentConversationClient } from '@/src/components/image-editor/EditorAgentConversation/useEditorAgentConversation.ts';
|
||||
import { EDITOR_AGENT_PATIENCE_NOTICE_DELAY_MS } from '@/src/components/image-editor/EditorAgentConversation/useEditorAgentConversation.ts';
|
||||
import { ImageCanvasActionsProvider } from '@/src/components/image-editor/ImageCanvasActionsProvider.tsx';
|
||||
@@ -135,9 +136,10 @@ function createClient(): EditorAgentConversationClient {
|
||||
}
|
||||
|
||||
function enterAttachmentPrompt() {
|
||||
fireEvent.change(screen.getByLabelText('发送给画布 Agent'), {
|
||||
target: { value: ATTACHMENT_PROMPT },
|
||||
});
|
||||
setPlainTextEditorValue(
|
||||
screen.getByLabelText('发送给画布 Agent'),
|
||||
ATTACHMENT_PROMPT,
|
||||
);
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
@@ -294,9 +296,10 @@ describe('EditorAgentConversationPanelView', () => {
|
||||
name: '新建对话',
|
||||
}) as HTMLButtonElement;
|
||||
expect(newConversationButton.disabled).toBe(false);
|
||||
fireEvent.change(screen.getByLabelText('发送给画布 Agent'), {
|
||||
target: { value: '应保留到新会话的草稿' },
|
||||
});
|
||||
setPlainTextEditorValue(
|
||||
screen.getByLabelText('发送给画布 Agent'),
|
||||
'应保留到新会话的草稿',
|
||||
);
|
||||
fireEvent.click(screen.getByRole('button', { name: '添加附件' }));
|
||||
const attachmentDialog = screen.getByRole('dialog', {
|
||||
name: '选择图片附件',
|
||||
@@ -332,25 +335,27 @@ describe('EditorAgentConversationPanelView', () => {
|
||||
).toBe('conversation-2');
|
||||
expect(newConversationButton.disabled).toBe(true);
|
||||
});
|
||||
expect(
|
||||
(screen.getByLabelText('发送给画布 Agent') as HTMLTextAreaElement).value,
|
||||
).toBe('应保留到新会话的草稿');
|
||||
expect(screen.getByLabelText('发送给画布 Agent').textContent).toBe(
|
||||
'应保留到新会话的草稿',
|
||||
);
|
||||
expect(
|
||||
screen.getByText('一二三四五六七八九十甲乙丙丁戊己庚辛壬癸子丑寅卯'),
|
||||
).toBeTruthy();
|
||||
expect(screen.getByRole('option', { name: '角色参考' })).toBeTruthy();
|
||||
expect(screen.getByRole('option', { name: '新对话' })).toBeTruthy();
|
||||
|
||||
fireEvent.change(screen.getByLabelText('发送给画布 Agent'), {
|
||||
target: { value: '尚未发送的草稿' },
|
||||
});
|
||||
setPlainTextEditorValue(
|
||||
screen.getByLabelText('发送给画布 Agent'),
|
||||
'尚未发送的草稿',
|
||||
);
|
||||
expect(newConversationButton.disabled).toBe(true);
|
||||
fireEvent.click(newConversationButton);
|
||||
expect(client.createConversation).toHaveBeenCalledTimes(1);
|
||||
|
||||
fireEvent.change(screen.getByLabelText('发送给画布 Agent'), {
|
||||
target: { value: '参考附件做像素风' },
|
||||
});
|
||||
setPlainTextEditorValue(
|
||||
screen.getByLabelText('发送给画布 Agent'),
|
||||
'参考附件做像素风',
|
||||
);
|
||||
fireEvent.click(screen.getByRole('button', { name: '发送' }));
|
||||
|
||||
await waitFor(() => {
|
||||
@@ -491,9 +496,10 @@ describe('EditorAgentConversationPanelView', () => {
|
||||
}) as HTMLButtonElement;
|
||||
expect(newConversationButton.disabled).toBe(true);
|
||||
|
||||
fireEvent.change(screen.getByLabelText('发送给画布 Agent'), {
|
||||
target: { value: '未发送内容' },
|
||||
});
|
||||
setPlainTextEditorValue(
|
||||
screen.getByLabelText('发送给画布 Agent'),
|
||||
'未发送内容',
|
||||
);
|
||||
expect(newConversationButton.disabled).toBe(true);
|
||||
fireEvent.click(newConversationButton);
|
||||
expect(client.createConversation).not.toHaveBeenCalled();
|
||||
@@ -516,17 +522,18 @@ describe('EditorAgentConversationPanelView', () => {
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('已经看到画布内容')).toBeTruthy();
|
||||
});
|
||||
fireEvent.change(screen.getByLabelText('发送给画布 Agent'), {
|
||||
target: { value: '需要保留的草稿' },
|
||||
});
|
||||
setPlainTextEditorValue(
|
||||
screen.getByLabelText('发送给画布 Agent'),
|
||||
'需要保留的草稿',
|
||||
);
|
||||
fireEvent.click(screen.getByRole('button', { name: '新建对话' }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('创建新会话失败')).toBeTruthy();
|
||||
});
|
||||
expect(
|
||||
(screen.getByLabelText('发送给画布 Agent') as HTMLTextAreaElement).value,
|
||||
).toBe('需要保留的草稿');
|
||||
expect(screen.getByLabelText('发送给画布 Agent').textContent).toBe(
|
||||
'需要保留的草稿',
|
||||
);
|
||||
expect(screen.getByText('已经看到画布内容')).toBeTruthy();
|
||||
expect((screen.getByLabelText('当前对话') as HTMLSelectElement).value).toBe(
|
||||
'conversation-1',
|
||||
@@ -556,15 +563,13 @@ describe('EditorAgentConversationPanelView', () => {
|
||||
});
|
||||
const draftInput = screen.getByLabelText(
|
||||
'发送给画布 Agent',
|
||||
) as HTMLTextAreaElement;
|
||||
fireEvent.change(draftInput, {
|
||||
target: { value: '旧项目草稿' },
|
||||
});
|
||||
) as HTMLElement;
|
||||
setPlainTextEditorValue(draftInput, '旧项目草稿');
|
||||
fireEvent.click(screen.getByRole('button', { name: '新建对话' }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(client.createConversation).toHaveBeenCalledWith('project-1', {});
|
||||
expect(draftInput.disabled).toBe(false);
|
||||
expect(draftInput.getAttribute('aria-disabled')).toBeNull();
|
||||
});
|
||||
const conversationSelect = screen.getByLabelText(
|
||||
'当前对话',
|
||||
@@ -578,12 +583,10 @@ describe('EditorAgentConversationPanelView', () => {
|
||||
(screen.getByRole('button', { name: '添加附件' }) as HTMLButtonElement)
|
||||
.disabled,
|
||||
).toBe(false);
|
||||
fireEvent.change(draftInput, {
|
||||
target: { value: '创建期间更新的草稿' },
|
||||
});
|
||||
fireEvent.submit(draftInput.closest('form') as HTMLFormElement);
|
||||
setPlainTextEditorValue(draftInput, '创建期间更新的草稿');
|
||||
fireEvent.click(sendButton);
|
||||
expect(client.sendMessage).not.toHaveBeenCalled();
|
||||
expect(draftInput.value).toBe('创建期间更新的草稿');
|
||||
expect(draftInput.textContent).toBe('创建期间更新的草稿');
|
||||
|
||||
await act(async () => {
|
||||
resolveCreate({
|
||||
@@ -596,7 +599,7 @@ describe('EditorAgentConversationPanelView', () => {
|
||||
});
|
||||
await Promise.resolve();
|
||||
});
|
||||
expect(draftInput.value).toBe('创建期间更新的草稿');
|
||||
expect(draftInput.textContent).toBe('创建期间更新的草稿');
|
||||
expect(conversationSelect.value).toBe('conversation-2');
|
||||
expect(conversationSelect.disabled).toBe(false);
|
||||
expect(sendButton.disabled).toBe(false);
|
||||
@@ -659,13 +662,11 @@ describe('EditorAgentConversationPanelView', () => {
|
||||
});
|
||||
const draftInput = screen.getByLabelText(
|
||||
'发送给画布 Agent',
|
||||
) as HTMLTextAreaElement;
|
||||
) as HTMLElement;
|
||||
const conversationSelect = screen.getByLabelText(
|
||||
'当前对话',
|
||||
) as HTMLSelectElement;
|
||||
fireEvent.change(draftInput, {
|
||||
target: { value: '切换期间必须保留的草稿' },
|
||||
});
|
||||
setPlainTextEditorValue(draftInput, '切换期间必须保留的草稿');
|
||||
fireEvent.change(conversationSelect, {
|
||||
target: { value: 'conversation-2' },
|
||||
});
|
||||
@@ -677,9 +678,9 @@ describe('EditorAgentConversationPanelView', () => {
|
||||
expect(client.getConversation).toHaveBeenCalledWith('conversation-2');
|
||||
expect(sendButton.disabled).toBe(true);
|
||||
});
|
||||
fireEvent.submit(draftInput.closest('form') as HTMLFormElement);
|
||||
fireEvent.click(sendButton);
|
||||
expect(client.sendMessage).not.toHaveBeenCalled();
|
||||
expect(draftInput.value).toBe('切换期间必须保留的草稿');
|
||||
expect(draftInput.textContent).toBe('切换期间必须保留的草稿');
|
||||
|
||||
await act(async () => {
|
||||
resolveConversationLoad({
|
||||
@@ -702,7 +703,7 @@ describe('EditorAgentConversationPanelView', () => {
|
||||
await Promise.resolve();
|
||||
});
|
||||
expect(await screen.findByText('背景会话内容')).toBeTruthy();
|
||||
expect(draftInput.value).toBe('切换期间必须保留的草稿');
|
||||
expect(draftInput.textContent).toBe('切换期间必须保留的草稿');
|
||||
expect(sendButton.disabled).toBe(false);
|
||||
});
|
||||
|
||||
@@ -729,9 +730,10 @@ describe('EditorAgentConversationPanelView', () => {
|
||||
});
|
||||
vi.useFakeTimers();
|
||||
|
||||
fireEvent.change(screen.getByLabelText('发送给画布 Agent'), {
|
||||
target: { value: '继续规划' },
|
||||
});
|
||||
setPlainTextEditorValue(
|
||||
screen.getByLabelText('发送给画布 Agent'),
|
||||
'继续规划',
|
||||
);
|
||||
fireEvent.click(screen.getByRole('button', { name: '发送' }));
|
||||
|
||||
await act(async () => {
|
||||
@@ -1316,7 +1318,7 @@ describe('EditorAgentConversationPanelView', () => {
|
||||
}) as HTMLButtonElement;
|
||||
expect(sendButton.disabled).toBe(true);
|
||||
|
||||
fireEvent.submit(sendButton.closest('form')!);
|
||||
fireEvent.click(sendButton);
|
||||
|
||||
expect(client.sendMessage).not.toHaveBeenCalled();
|
||||
expect(screen.getByText('角色图层')).toBeTruthy();
|
||||
@@ -1368,15 +1370,16 @@ describe('EditorAgentConversationPanelView', () => {
|
||||
fireEvent.click(
|
||||
within(attachmentDialog).getByRole('button', { name: '应用' }),
|
||||
);
|
||||
fireEvent.change(screen.getByLabelText('发送给画布 Agent'), {
|
||||
target: { value: '失败后恢复这条草稿' },
|
||||
});
|
||||
setPlainTextEditorValue(
|
||||
screen.getByLabelText('发送给画布 Agent'),
|
||||
'失败后恢复这条草稿',
|
||||
);
|
||||
fireEvent.click(screen.getByRole('button', { name: '发送' }));
|
||||
|
||||
expect(await screen.findByText('Network error')).toBeTruthy();
|
||||
expect(
|
||||
(screen.getByLabelText('发送给画布 Agent') as HTMLTextAreaElement).value,
|
||||
).toBe('失败后恢复这条草稿');
|
||||
expect(screen.getByLabelText('发送给画布 Agent').textContent).toBe(
|
||||
'失败后恢复这条草稿',
|
||||
);
|
||||
expect(screen.getByText('角色图层')).toBeTruthy();
|
||||
expect(
|
||||
within(
|
||||
@@ -1760,9 +1763,10 @@ describe('EditorAgentConversationPanelView', () => {
|
||||
expect(screen.getByText('已经看到画布内容')).toBeTruthy();
|
||||
});
|
||||
|
||||
fireEvent.change(screen.getByLabelText('发送给画布 Agent'), {
|
||||
target: { value: '生成一张图片' },
|
||||
});
|
||||
setPlainTextEditorValue(
|
||||
screen.getByLabelText('发送给画布 Agent'),
|
||||
'生成一张图片',
|
||||
);
|
||||
fireEvent.click(screen.getByRole('button', { name: '发送' }));
|
||||
|
||||
await waitFor(() => {
|
||||
|
||||
+7
-10
@@ -10,7 +10,6 @@ import {
|
||||
X,
|
||||
} from 'lucide-react';
|
||||
import {
|
||||
type FormEvent,
|
||||
useEffect,
|
||||
useState,
|
||||
type WheelEvent as ReactWheelEvent,
|
||||
@@ -139,8 +138,7 @@ export function EditorAgentConversationPanelView({
|
||||
void createConversation().catch(() => undefined);
|
||||
};
|
||||
|
||||
const submitMessage = (event: FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault();
|
||||
const submitMessage = () => {
|
||||
if (isMessageSubmissionBlocked) {
|
||||
return;
|
||||
}
|
||||
@@ -328,10 +326,7 @@ export function EditorAgentConversationPanelView({
|
||||
{attachmentError}
|
||||
</div>
|
||||
) : null}
|
||||
<form
|
||||
className="border-t border-slate-200 bg-white/95 p-3"
|
||||
onSubmit={submitMessage}
|
||||
>
|
||||
<div className="border-t border-slate-200 bg-white/95 p-3">
|
||||
{attachments.length ? (
|
||||
<div className="mb-2 flex flex-wrap gap-1.5">
|
||||
{attachments.map((attachment) => (
|
||||
@@ -354,19 +349,21 @@ export function EditorAgentConversationPanelView({
|
||||
</button>
|
||||
<EditorAgentDraftTextarea
|
||||
value={draftText}
|
||||
onChange={setDraftText}
|
||||
onValueChange={setDraftText}
|
||||
onPaste={handleInputPaste}
|
||||
onSubmitRequest={submitMessage}
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
type="button"
|
||||
className="inline-flex h-10 min-w-16 shrink-0 items-center justify-center gap-1.5 rounded-full bg-slate-900 px-3 text-sm font-semibold text-white disabled:opacity-45"
|
||||
disabled={isMessageSubmissionBlocked}
|
||||
onClick={submitMessage}
|
||||
>
|
||||
<Send className="h-3.5 w-3.5" aria-hidden="true" />
|
||||
发送
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</aside>
|
||||
<AttachmentPicker
|
||||
open={attachmentPickerOpen}
|
||||
|
||||
+66
-40
@@ -1,60 +1,86 @@
|
||||
/* @vitest-environment jsdom */
|
||||
|
||||
import { fireEvent, render, screen } from '@testing-library/react';
|
||||
import type { FormEvent } from 'react';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { useState } from 'react';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { EditorAgentDraftTextarea } from './EditorAgentDraftTextarea.tsx';
|
||||
|
||||
beforeEach(() => {
|
||||
vi.stubGlobal(
|
||||
'ResizeObserver',
|
||||
class ResizeObserver {
|
||||
observe() {}
|
||||
unobserve() {}
|
||||
disconnect() {}
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
function DraftHarness({
|
||||
onPaste,
|
||||
onSubmitRequest,
|
||||
}: {
|
||||
onPaste?: (event: ClipboardEvent) => void;
|
||||
onSubmitRequest: () => void;
|
||||
}) {
|
||||
const [value, setValue] = useState('');
|
||||
return (
|
||||
<EditorAgentDraftTextarea
|
||||
value={value}
|
||||
onValueChange={setValue}
|
||||
onPaste={onPaste}
|
||||
onSubmitRequest={onSubmitRequest}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
describe('EditorAgentDraftTextarea', () => {
|
||||
it('forwards draft changes and paste events through the Agent wrapper', () => {
|
||||
const onChange = vi.fn();
|
||||
const onPaste = vi.fn();
|
||||
it('forwards controlled draft edits and paste events', async () => {
|
||||
const user = userEvent.setup();
|
||||
const onPaste = vi.fn((event: ClipboardEvent) => event.preventDefault());
|
||||
render(<DraftHarness onPaste={onPaste} onSubmitRequest={vi.fn()} />);
|
||||
|
||||
render(
|
||||
<EditorAgentDraftTextarea
|
||||
value=""
|
||||
onChange={onChange}
|
||||
onPaste={onPaste}
|
||||
/>,
|
||||
);
|
||||
|
||||
const input = screen.getByLabelText(
|
||||
'发送给画布 Agent',
|
||||
) as HTMLTextAreaElement;
|
||||
expect(input.className).toContain('auto-grow-text-area');
|
||||
expect(input.className).toContain(
|
||||
'editor-agent-conversation__draft-input',
|
||||
);
|
||||
|
||||
fireEvent.change(input, { target: { value: '新草稿' } });
|
||||
expect(onChange).toHaveBeenCalledWith('新草稿');
|
||||
fireEvent.paste(input);
|
||||
const editor = screen.getByRole('textbox', {
|
||||
name: '发送给画布 Agent',
|
||||
});
|
||||
await user.click(editor);
|
||||
fireEvent.paste(editor, {
|
||||
clipboardData: {
|
||||
files: [],
|
||||
getData: () => '',
|
||||
items: [],
|
||||
types: [],
|
||||
},
|
||||
});
|
||||
expect(onPaste).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('submits on Enter and keeps line breaks or IME confirmation from submitting', () => {
|
||||
const onSubmit = vi.fn((event: FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault();
|
||||
});
|
||||
render(
|
||||
<form onSubmit={onSubmit}>
|
||||
<EditorAgentDraftTextarea value="草稿" onChange={vi.fn()} />
|
||||
</form>,
|
||||
it('submits on Enter but preserves Shift+Enter and IME confirmation', async () => {
|
||||
const user = userEvent.setup();
|
||||
const onSubmitRequest = vi.fn();
|
||||
const { container } = render(
|
||||
<DraftHarness onSubmitRequest={onSubmitRequest} />,
|
||||
);
|
||||
|
||||
const input = screen.getByLabelText('发送给画布 Agent');
|
||||
fireEvent.keyDown(input, { key: 'Enter', shiftKey: true });
|
||||
expect(onSubmit).not.toHaveBeenCalled();
|
||||
const editor = screen.getByRole('textbox', {
|
||||
name: '发送给画布 Agent',
|
||||
});
|
||||
await user.click(editor);
|
||||
fireEvent.keyDown(editor, { key: 'Enter', shiftKey: true });
|
||||
expect(onSubmitRequest).not.toHaveBeenCalled();
|
||||
|
||||
fireEvent.keyDown(input, {
|
||||
fireEvent.keyDown(editor, {
|
||||
key: 'Enter',
|
||||
isComposing: false,
|
||||
isComposing: true,
|
||||
keyCode: 229,
|
||||
});
|
||||
expect(onSubmit).not.toHaveBeenCalled();
|
||||
expect(onSubmitRequest).not.toHaveBeenCalled();
|
||||
|
||||
fireEvent.keyDown(input, { key: 'Enter' });
|
||||
expect(onSubmit).toHaveBeenCalledTimes(1);
|
||||
fireEvent.keyDown(editor, { key: 'Enter' });
|
||||
expect(onSubmitRequest).toHaveBeenCalledTimes(1);
|
||||
expect(container.querySelector('form')).toBeNull();
|
||||
expect(container.querySelector('textarea')).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,39 +1,24 @@
|
||||
import {
|
||||
type ClipboardEventHandler,
|
||||
type KeyboardEvent,
|
||||
} from 'react';
|
||||
|
||||
import { AutoGrowTextArea } from '@/src/components/common/AutoGrowTextArea';
|
||||
|
||||
export function EditorAgentDraftTextarea({
|
||||
value,
|
||||
onChange,
|
||||
onValueChange,
|
||||
onPaste,
|
||||
onSubmitRequest,
|
||||
}: {
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
onPaste?: ClipboardEventHandler<HTMLTextAreaElement>;
|
||||
onValueChange: (value: string) => void;
|
||||
onPaste?: (event: ClipboardEvent) => void;
|
||||
onSubmitRequest: () => void;
|
||||
}) {
|
||||
const handleKeyDown = (event: KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
if (
|
||||
event.key === 'Enter' &&
|
||||
!event.shiftKey &&
|
||||
!event.nativeEvent.isComposing &&
|
||||
event.nativeEvent.keyCode !== 229
|
||||
) {
|
||||
event.preventDefault();
|
||||
event.currentTarget.form?.requestSubmit();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<AutoGrowTextArea
|
||||
className="editor-agent-conversation__draft-input min-w-0 flex-1 rounded-3xl border border-slate-200 bg-slate-50 px-3 py-2 text-sm text-slate-800 outline-none focus:border-slate-400"
|
||||
className="editor-agent-conversation__draft-input min-w-0 flex-1 border border-slate-200 bg-slate-50 text-sm text-slate-800 outline-none focus-within:border-slate-400"
|
||||
aria-label="发送给画布 Agent"
|
||||
value={value}
|
||||
onChange={(event) => onChange(event.currentTarget.value)}
|
||||
onValueChange={onValueChange}
|
||||
onPaste={onPaste}
|
||||
onKeyDown={handleKeyDown}
|
||||
onSubmitRequest={onSubmitRequest}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import {
|
||||
type ClipboardEvent as ReactClipboardEvent,
|
||||
useCallback,
|
||||
useMemo,
|
||||
useRef,
|
||||
@@ -305,7 +304,7 @@ export function useConversationAttachments({
|
||||
);
|
||||
|
||||
const handleInputPaste = useCallback(
|
||||
(event: ReactClipboardEvent<HTMLTextAreaElement>) => {
|
||||
(event: ClipboardEvent) => {
|
||||
const imageFiles = extractClipboardImageFiles(event.clipboardData);
|
||||
if (!imageFiles.length || isPastingAttachmentRef.current) {
|
||||
return;
|
||||
|
||||
@@ -76,7 +76,7 @@ type ImageCanvasBasicGenerationComposerViewProps = {
|
||||
referenceAriaLabelFormatter?: ReferenceLabelFormatter;
|
||||
referenceRemoveLabelFormatter?: ReferenceLabelFormatter;
|
||||
showReferenceAddButton?: boolean;
|
||||
formClassName?: string;
|
||||
panelClassName?: string;
|
||||
footerClassName?: string;
|
||||
promptClassName?: string;
|
||||
referenceSlotClassName?: string;
|
||||
@@ -130,7 +130,7 @@ export function ImageCanvasBasicGenerationComposerView({
|
||||
referenceAriaLabelFormatter,
|
||||
referenceRemoveLabelFormatter,
|
||||
showReferenceAddButton = true,
|
||||
formClassName,
|
||||
panelClassName,
|
||||
footerClassName,
|
||||
promptClassName = 'image-canvas-editor__generation-prompt',
|
||||
referenceSlotClassName,
|
||||
@@ -166,8 +166,8 @@ export function ImageCanvasBasicGenerationComposerView({
|
||||
? '修改中'
|
||||
: submittingStatusLabel;
|
||||
const hasReferenceMenu = shouldIncludeReferences && onRequestUpload;
|
||||
const finalFormClassName =
|
||||
formClassName ??
|
||||
const finalPanelClassName =
|
||||
panelClassName ??
|
||||
'image-canvas-editor__generation-composer image-canvas-editor__generation-composer--image';
|
||||
const finalFooterClassName =
|
||||
footerClassName ?? 'image-canvas-editor__generation-composer-footer';
|
||||
@@ -180,18 +180,12 @@ export function ImageCanvasBasicGenerationComposerView({
|
||||
|
||||
return (
|
||||
<>
|
||||
<form
|
||||
className={finalFormClassName}
|
||||
<div
|
||||
className={finalPanelClassName}
|
||||
style={style}
|
||||
role="dialog"
|
||||
aria-label={resolvedDialogLabel}
|
||||
onPointerDown={(event) => event.stopPropagation()}
|
||||
onSubmit={(event) => {
|
||||
event.preventDefault();
|
||||
if (dialog.status !== 'generating') {
|
||||
onSubmit(dialog);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{shouldIncludeReferences ? (
|
||||
<div className="image-canvas-editor__reference-strip">
|
||||
@@ -255,12 +249,12 @@ export function ImageCanvasBasicGenerationComposerView({
|
||||
disabled={dialog.status === 'generating'}
|
||||
placeholder={resolvedPromptPlaceholder}
|
||||
className={promptClassName}
|
||||
onChange={(event) =>
|
||||
onValueChange={(value) =>
|
||||
setGenerateDialog((currentDialog) =>
|
||||
currentDialog
|
||||
? {
|
||||
...resetFailedDialogStatus(currentDialog),
|
||||
prompt: event.target.value,
|
||||
prompt: value,
|
||||
}
|
||||
: currentDialog,
|
||||
)
|
||||
@@ -284,6 +278,11 @@ export function ImageCanvasBasicGenerationComposerView({
|
||||
submitLabel={resolvedSubmitLabel}
|
||||
submitAriaLabel={resolvedSubmitAriaLabel}
|
||||
submitButtonClassName={submitButtonClassName}
|
||||
onSubmitRequest={() => {
|
||||
if (dialog.status !== 'generating') {
|
||||
onSubmit(dialog);
|
||||
}
|
||||
}}
|
||||
renderEditorPortal={renderEditorPortal}
|
||||
buildPortalMenuStyle={buildPortalMenuStyle}
|
||||
/>
|
||||
@@ -310,7 +309,7 @@ export function ImageCanvasBasicGenerationComposerView({
|
||||
{dialog.errorMessage}
|
||||
</PlatformStatusMessage>
|
||||
) : null}
|
||||
</form>
|
||||
</div>
|
||||
{hasReferenceMenu &&
|
||||
isGenerationReferenceMenuOpen &&
|
||||
generationReferenceButtonRef
|
||||
|
||||
@@ -161,18 +161,12 @@ export function ImageCanvasCharacterAnimationPanelView({
|
||||
|
||||
return (
|
||||
<>
|
||||
<form
|
||||
<div
|
||||
className="image-canvas-editor__character-animation-panel image-canvas-editor__generation-composer image-canvas-editor__generation-composer--image image-canvas-editor__generation-composer--character-animation"
|
||||
style={style}
|
||||
role="dialog"
|
||||
aria-label="角色动画生成面板"
|
||||
onPointerDown={(event) => event.stopPropagation()}
|
||||
onSubmit={(event) => {
|
||||
event.preventDefault();
|
||||
if (!isGenerating) {
|
||||
onSubmit();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="image-canvas-editor__reference-strip">
|
||||
<ImageCanvasReferenceSlot
|
||||
@@ -201,9 +195,7 @@ export function ImageCanvasCharacterAnimationPanelView({
|
||||
disabled={isGenerating}
|
||||
placeholder="你希望角色做什么动作?"
|
||||
className="max-h-64 image-canvas-editor__generation-prompt image-canvas-editor__character-animation-textarea"
|
||||
onChange={(event) =>
|
||||
updatePanel({ promptText: event.target.value.slice(0, 4000) })
|
||||
}
|
||||
onValueChange={(value) => updatePanel({ promptText: value })}
|
||||
/>
|
||||
<div className="image-canvas-editor__character-animation-presets">
|
||||
{CHARACTER_ANIMATION_ACTION_PROMPTS.map((preset) => (
|
||||
@@ -244,12 +236,17 @@ export function ImageCanvasCharacterAnimationPanelView({
|
||||
</PlatformInlineOptionButton>
|
||||
</div>
|
||||
<PlatformActionButton
|
||||
type="submit"
|
||||
type="button"
|
||||
tone="secondary"
|
||||
size="xs"
|
||||
shape="pill"
|
||||
className="image-canvas-editor__character-animation-submit"
|
||||
disabled={isGenerating}
|
||||
onClick={() => {
|
||||
if (!isGenerating) {
|
||||
onSubmit();
|
||||
}
|
||||
}}
|
||||
>
|
||||
{isGenerating ? '生成中' : `生成${price}泥点`}
|
||||
</PlatformActionButton>
|
||||
@@ -281,7 +278,7 @@ export function ImageCanvasCharacterAnimationPanelView({
|
||||
{panel.errorMessage}
|
||||
</PlatformStatusMessage>
|
||||
) : null}
|
||||
</form>
|
||||
</div>
|
||||
{isParameterMenuOpen
|
||||
? renderPanelPortal(
|
||||
<PlatformFloatingMenu
|
||||
|
||||
@@ -111,18 +111,12 @@ export function ImageCanvasCharacterGenerationComposerView({
|
||||
});
|
||||
|
||||
return (
|
||||
<form
|
||||
<div
|
||||
className="image-canvas-editor__character-composer"
|
||||
style={style}
|
||||
role="dialog"
|
||||
aria-label="生成角色形象"
|
||||
onPointerDown={(event) => event.stopPropagation()}
|
||||
onSubmit={(event) => {
|
||||
event.preventDefault();
|
||||
if (dialog.status !== 'generating') {
|
||||
onSubmit(dialog);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="image-canvas-editor__reference-strip">
|
||||
<ImageCanvasReferenceSlot
|
||||
@@ -255,25 +249,25 @@ export function ImageCanvasCharacterGenerationComposerView({
|
||||
)
|
||||
: null}
|
||||
</div>
|
||||
<label className="image-canvas-editor__field-block image-canvas-editor__field-block--single">
|
||||
<div className="image-canvas-editor__field-block image-canvas-editor__field-block--single">
|
||||
<AutoGrowTextArea
|
||||
aria-label="角色设定"
|
||||
value={dialog.prompt}
|
||||
disabled={dialog.status === 'generating'}
|
||||
placeholder="你希望角色如何设计?"
|
||||
className="image-canvas-editor__generation-prompt"
|
||||
onChange={(event) =>
|
||||
onValueChange={(value) =>
|
||||
setGenerateDialog((currentDialog) =>
|
||||
currentDialog?.mode === 'character'
|
||||
? {
|
||||
...resetFailedDialogStatus(currentDialog),
|
||||
prompt: event.target.value,
|
||||
prompt: value,
|
||||
}
|
||||
: currentDialog,
|
||||
)
|
||||
}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
{dialog.status === 'failed' ? (
|
||||
<PlatformStatusMessage
|
||||
tone="error"
|
||||
@@ -300,10 +294,15 @@ export function ImageCanvasCharacterGenerationComposerView({
|
||||
})}
|
||||
submitLabel="生成"
|
||||
submitAriaLabel="生成"
|
||||
onSubmitRequest={() => {
|
||||
if (dialog.status !== 'generating') {
|
||||
onSubmit(dialog);
|
||||
}
|
||||
}}
|
||||
renderEditorPortal={renderEditorPortal}
|
||||
buildPortalMenuStyle={buildPortalMenuStyle}
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -41,18 +41,12 @@ export function ImageCanvasCropExpandPanelView({
|
||||
onSubmit,
|
||||
}: ImageCanvasCropExpandPanelViewProps) {
|
||||
return (
|
||||
<form
|
||||
<div
|
||||
className="image-canvas-editor__crop-expand-panel"
|
||||
style={style}
|
||||
role="dialog"
|
||||
aria-label="裁扩图片"
|
||||
onPointerDown={(event) => event.stopPropagation()}
|
||||
onSubmit={(event) => {
|
||||
event.preventDefault();
|
||||
if (panel.status !== 'processing') {
|
||||
onSubmit();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="image-canvas-editor__crop-expand-head">
|
||||
<strong>裁扩</strong>
|
||||
@@ -107,14 +101,19 @@ export function ImageCanvasCropExpandPanelView({
|
||||
取消
|
||||
</PlatformActionButton>
|
||||
<PlatformActionButton
|
||||
type="submit"
|
||||
type="button"
|
||||
tone="primary"
|
||||
size="sm"
|
||||
disabled={panel.status === 'processing'}
|
||||
onClick={() => {
|
||||
if (panel.status !== 'processing') {
|
||||
onSubmit();
|
||||
}
|
||||
}}
|
||||
>
|
||||
{panel.status === 'processing' ? '处理中' : '完成'}
|
||||
</PlatformActionButton>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -60,7 +60,7 @@ export function ImageCanvasEditGenerationModalView({
|
||||
submitLabel="修改"
|
||||
submitAriaLabel={`修改${editPrice}泥点`}
|
||||
submittingStatusLabel="修改中"
|
||||
formClassName="image-canvas-editor__generate-form"
|
||||
panelClassName="image-canvas-editor__generate-form"
|
||||
footerClassName="image-canvas-editor__generate-body"
|
||||
promptClassName="image-canvas-editor__generate-prompt"
|
||||
submitButtonClassName="image-canvas-editor__generate-submit"
|
||||
|
||||
@@ -438,18 +438,12 @@ function ImageCanvasVideoGenerationComposerView({
|
||||
|
||||
return (
|
||||
<>
|
||||
<form
|
||||
<div
|
||||
className="image-canvas-editor__generation-composer image-canvas-editor__generation-composer--image image-canvas-editor__generation-composer--video"
|
||||
style={style}
|
||||
role="dialog"
|
||||
aria-label="生成视频"
|
||||
onPointerDown={(event) => event.stopPropagation()}
|
||||
onSubmit={(event) => {
|
||||
event.preventDefault();
|
||||
if (!isGenerating) {
|
||||
onSubmit(dialog);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{supportsReferences ? (
|
||||
<div className="image-canvas-editor__reference-strip">
|
||||
@@ -485,9 +479,7 @@ function ImageCanvasVideoGenerationComposerView({
|
||||
disabled={isGenerating}
|
||||
placeholder="你希望生成什么视频?"
|
||||
className="image-canvas-editor__generation-prompt"
|
||||
onChange={(event) =>
|
||||
updateVideoDialog({ prompt: event.target.value })
|
||||
}
|
||||
onValueChange={(value) => updateVideoDialog({ prompt: value })}
|
||||
/>
|
||||
<div className="image-canvas-editor__generation-composer-footer">
|
||||
<div className="image-canvas-editor__option-popover-anchor image-canvas-editor__option-popover-anchor--dimensions">
|
||||
@@ -697,13 +689,18 @@ function ImageCanvasVideoGenerationComposerView({
|
||||
: null}
|
||||
</div>
|
||||
<PlatformActionButton
|
||||
type="submit"
|
||||
type="button"
|
||||
tone="secondary"
|
||||
size="xs"
|
||||
shape="pill"
|
||||
className="image-canvas-editor__generation-submit"
|
||||
disabled={isGenerating}
|
||||
aria-label="生成视频"
|
||||
onClick={() => {
|
||||
if (!isGenerating) {
|
||||
onSubmit(dialog);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{isGenerating ? (
|
||||
'生成中'
|
||||
@@ -717,7 +714,7 @@ function ImageCanvasVideoGenerationComposerView({
|
||||
)}
|
||||
</PlatformActionButton>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{isGenerationReferenceMenuOpen && supportsReferences
|
||||
? renderEditorPortal(
|
||||
<PlatformFloatingMenu
|
||||
@@ -902,20 +899,13 @@ function ImageCanvasAudioGenerationComposerView({
|
||||
};
|
||||
|
||||
return (
|
||||
<form
|
||||
<div
|
||||
className="image-canvas-editor__generation-composer image-canvas-editor__generation-composer--image image-canvas-editor__generation-composer--audio image-canvas-editor__generation-composer--background-music"
|
||||
style={style}
|
||||
role="dialog"
|
||||
aria-label={BACKGROUND_MUSIC_COMPOSER_LABEL}
|
||||
aria-busy={isLocked}
|
||||
onPointerDown={(event) => event.stopPropagation()}
|
||||
onSubmit={(event) => {
|
||||
event.preventDefault();
|
||||
if (!canGenerate) {
|
||||
return;
|
||||
}
|
||||
onSubmit(backgroundMusicDialog);
|
||||
}}
|
||||
>
|
||||
{/* 与生图 / 生视频 / 角色 / 图标同源:自动增高到 max-h-32 后内部滚动。 */}
|
||||
<AutoGrowTextArea
|
||||
@@ -926,7 +916,7 @@ function ImageCanvasAudioGenerationComposerView({
|
||||
readOnly={isLocked}
|
||||
placeholder="你希望生成什么音乐?"
|
||||
className="image-canvas-editor__generation-prompt"
|
||||
onChange={(event) => updateBackgroundMusicPrompt(event.target.value)}
|
||||
onValueChange={updateBackgroundMusicPrompt}
|
||||
/>
|
||||
<ImageCanvasBackgroundMusicPresetMarquee
|
||||
presets={BACKGROUND_MUSIC_PROMPT_PRESETS}
|
||||
@@ -1059,13 +1049,18 @@ function ImageCanvasAudioGenerationComposerView({
|
||||
</PlatformInlineOptionButton>
|
||||
</div>
|
||||
<PlatformActionButton
|
||||
type="submit"
|
||||
type="button"
|
||||
tone="secondary"
|
||||
size="xs"
|
||||
shape="pill"
|
||||
className="image-canvas-editor__generation-submit"
|
||||
disabled={!canGenerate}
|
||||
aria-label={BACKGROUND_MUSIC_COMPOSER_LABEL}
|
||||
onClick={() => {
|
||||
if (canGenerate) {
|
||||
onSubmit(backgroundMusicDialog);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{isGenerating ? (
|
||||
'生成中'
|
||||
@@ -1079,7 +1074,7 @@ function ImageCanvasAudioGenerationComposerView({
|
||||
)}
|
||||
</PlatformActionButton>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1113,18 +1108,12 @@ function ImageCanvasAudioGenerationComposerView({
|
||||
};
|
||||
|
||||
return (
|
||||
<form
|
||||
<div
|
||||
className="image-canvas-editor__generation-composer image-canvas-editor__generation-composer--image image-canvas-editor__generation-composer--audio"
|
||||
style={style}
|
||||
role="dialog"
|
||||
aria-label={dialogLabel}
|
||||
onPointerDown={(event) => event.stopPropagation()}
|
||||
onSubmit={(event) => {
|
||||
event.preventDefault();
|
||||
if (!isGenerating) {
|
||||
onSubmit(dialog);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<AutoGrowTextArea
|
||||
aria-label="prompt"
|
||||
@@ -1132,7 +1121,7 @@ function ImageCanvasAudioGenerationComposerView({
|
||||
disabled={isGenerating}
|
||||
placeholder="你希望生成什么音效?"
|
||||
className="image-canvas-editor__generation-prompt"
|
||||
onChange={(event) => updateAudioDialog({ prompt: event.target.value })}
|
||||
onValueChange={(value) => updateAudioDialog({ prompt: value })}
|
||||
/>
|
||||
{dialog.status === 'failed' ? (
|
||||
<PlatformStatusMessage
|
||||
@@ -1214,13 +1203,18 @@ function ImageCanvasAudioGenerationComposerView({
|
||||
</PlatformInlineOptionButton>
|
||||
</div>
|
||||
<PlatformActionButton
|
||||
type="submit"
|
||||
type="button"
|
||||
tone="secondary"
|
||||
size="xs"
|
||||
shape="pill"
|
||||
className="image-canvas-editor__generation-submit"
|
||||
disabled={isGenerating}
|
||||
aria-label={dialogLabel}
|
||||
onClick={() => {
|
||||
if (!isGenerating) {
|
||||
onSubmit(dialog);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{isGenerating ? (
|
||||
'生成中'
|
||||
@@ -1234,7 +1228,7 @@ function ImageCanvasAudioGenerationComposerView({
|
||||
)}
|
||||
</PlatformActionButton>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -44,6 +44,7 @@ function ImageOptionsHarness({
|
||||
})}
|
||||
submitLabel="生成"
|
||||
submitAriaLabel="生成角色形象"
|
||||
onSubmitRequest={() => undefined}
|
||||
/>
|
||||
<output aria-label="当前模型">{dialog.imageModel}</output>
|
||||
<output aria-label="当前比例">{dialog.aspectRatio}</output>
|
||||
|
||||
@@ -41,6 +41,7 @@ type ImageCanvasGenerationImageOptionsViewProps = {
|
||||
submitLabel?: string;
|
||||
submitAriaLabel?: string;
|
||||
submitButtonClassName?: string;
|
||||
onSubmitRequest: () => void;
|
||||
lockedModel?: string;
|
||||
renderEditorPortal?: (node: ReactNode) => ReactNode;
|
||||
buildPortalMenuStyle?: (
|
||||
@@ -133,6 +134,7 @@ export function ImageCanvasGenerationImageOptionsView({
|
||||
submitLabel = '生成',
|
||||
submitAriaLabel = '生成',
|
||||
submitButtonClassName = 'image-canvas-editor__generation-submit',
|
||||
onSubmitRequest,
|
||||
lockedModel,
|
||||
renderEditorPortal = (node) => node,
|
||||
buildPortalMenuStyle = () => ({}),
|
||||
@@ -419,13 +421,14 @@ export function ImageCanvasGenerationImageOptionsView({
|
||||
) : null}
|
||||
{typeof cost === 'number' ? (
|
||||
<PlatformActionButton
|
||||
type="submit"
|
||||
type="button"
|
||||
tone="secondary"
|
||||
size="xs"
|
||||
shape="pill"
|
||||
className={submitButtonClassName}
|
||||
disabled={isGenerating || hasPendingImageReferenceUploads}
|
||||
aria-label={submitAriaLabel}
|
||||
onClick={onSubmitRequest}
|
||||
>
|
||||
{isGenerating ? (
|
||||
'生成中'
|
||||
|
||||
@@ -86,18 +86,12 @@ export function ImageCanvasIconSpritesheetComposerView({
|
||||
const references = dialog.generationReferences ?? [];
|
||||
|
||||
return (
|
||||
<form
|
||||
<div
|
||||
className="image-canvas-editor__icon-composer"
|
||||
style={style}
|
||||
role="dialog"
|
||||
aria-label="生成图标素材"
|
||||
onPointerDown={(event) => event.stopPropagation()}
|
||||
onSubmit={(event) => {
|
||||
event.preventDefault();
|
||||
if (dialog.status !== 'generating') {
|
||||
onSubmit({ ...dialog, prompt: descriptionText });
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="image-canvas-editor__reference-strip">
|
||||
<ImageCanvasReferenceSlot
|
||||
@@ -209,16 +203,16 @@ export function ImageCanvasIconSpritesheetComposerView({
|
||||
onClick={() => onRequestUpload('generation-reference')}
|
||||
/>
|
||||
</div>
|
||||
<label className="image-canvas-editor__field-block image-canvas-editor__field-block--single">
|
||||
<div className="image-canvas-editor__field-block image-canvas-editor__field-block--single">
|
||||
<AutoGrowTextArea
|
||||
aria-label="素材描述"
|
||||
value={descriptionText}
|
||||
disabled={dialog.status === 'generating'}
|
||||
placeholder="你需要哪些图标?"
|
||||
className="image-canvas-editor__generation-prompt"
|
||||
onChange={(event) => onUpdateIconDescriptionText(event.target.value)}
|
||||
onValueChange={onUpdateIconDescriptionText}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
{dialog.status === 'failed' ? (
|
||||
<PlatformStatusMessage
|
||||
tone="error"
|
||||
@@ -244,10 +238,15 @@ export function ImageCanvasIconSpritesheetComposerView({
|
||||
)}
|
||||
submitLabel="生成"
|
||||
submitAriaLabel="生成"
|
||||
onSubmitRequest={() => {
|
||||
if (dialog.status !== 'generating') {
|
||||
onSubmit({ ...dialog, prompt: descriptionText });
|
||||
}
|
||||
}}
|
||||
renderEditorPortal={renderEditorPortal}
|
||||
buildPortalMenuStyle={buildPortalMenuStyle}
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -126,18 +126,12 @@ export function ImageCanvasPublicationMaterialsDemoPanelView({
|
||||
...dialog.publicationGameInfo,
|
||||
};
|
||||
return (
|
||||
<form
|
||||
<div
|
||||
className="image-canvas-editor__character-composer image-canvas-editor__publication-composer"
|
||||
style={style}
|
||||
role="dialog"
|
||||
aria-label={`${workflow.label}生成卡片`}
|
||||
onPointerDown={(event) => event.stopPropagation()}
|
||||
onSubmit={(event) => {
|
||||
event.preventDefault();
|
||||
if (dialog.status !== 'generating') {
|
||||
onSubmit(dialog);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="image-canvas-editor__reference-strip">
|
||||
{references.map((reference, index) => (
|
||||
@@ -260,7 +254,7 @@ export function ImageCanvasPublicationMaterialsDemoPanelView({
|
||||
}
|
||||
/>
|
||||
</label>
|
||||
<label className="image-canvas-editor__field-block image-canvas-editor__publication-description-field">
|
||||
<div className="image-canvas-editor__field-block image-canvas-editor__publication-description-field">
|
||||
<PlatformFieldLabel
|
||||
variant="field"
|
||||
className="image-canvas-editor__field-title"
|
||||
@@ -273,17 +267,17 @@ export function ImageCanvasPublicationMaterialsDemoPanelView({
|
||||
disabled={dialog.status === 'generating'}
|
||||
placeholder="神奇数字马戏团里,帕姆尼失控了,找到钥匙,开门逃离马戏团,千万别被抓住了"
|
||||
className="image-canvas-editor__generation-prompt image-canvas-editor__publication-description"
|
||||
onChange={(event) =>
|
||||
onValueChange={(value) =>
|
||||
setGenerateDialog((currentDialog) =>
|
||||
updatePublicationGameInfoField(
|
||||
currentDialog,
|
||||
'gameDescription',
|
||||
event.target.value,
|
||||
value,
|
||||
),
|
||||
)
|
||||
}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{dialog.status === 'failed' ? (
|
||||
@@ -313,10 +307,15 @@ export function ImageCanvasPublicationMaterialsDemoPanelView({
|
||||
)}
|
||||
submitLabel="生成"
|
||||
submitAriaLabel="生成"
|
||||
onSubmitRequest={() => {
|
||||
if (dialog.status !== 'generating') {
|
||||
onSubmit(dialog);
|
||||
}
|
||||
}}
|
||||
renderEditorPortal={renderEditorPortal}
|
||||
buildPortalMenuStyle={buildPortalMenuStyle}
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -175,7 +175,7 @@ export function ImageCanvasQuickEditPanelView({
|
||||
referenceLabelFormatter={(_, index) => `图${index + 1}`}
|
||||
referenceAriaLabelFormatter={(_, index) => `图${index + 1}`}
|
||||
referenceRemoveLabelFormatter={(_, index) => `删除图${index + 1}`}
|
||||
formClassName="image-canvas-editor__quick-edit-panel image-canvas-editor__generation-composer image-canvas-editor__generation-composer--image image-canvas-editor__generation-composer--quick-edit"
|
||||
panelClassName="image-canvas-editor__quick-edit-panel image-canvas-editor__generation-composer image-canvas-editor__generation-composer--image image-canvas-editor__generation-composer--quick-edit"
|
||||
footerClassName="image-canvas-editor__generation-composer-footer image-canvas-editor__quick-edit-footer"
|
||||
promptClassName="image-canvas-editor__generation-prompt image-canvas-editor__quick-edit-prompt"
|
||||
referenceSlotClassName="image-canvas-editor__quick-edit-reference"
|
||||
|
||||
@@ -122,18 +122,12 @@ export function ImageCanvasSpecGenerationPanelView({
|
||||
|
||||
return (
|
||||
<>
|
||||
<form
|
||||
<div
|
||||
className="image-canvas-editor__generation-composer image-canvas-editor__generation-composer--image image-canvas-editor__spec-composer"
|
||||
style={style}
|
||||
role="dialog"
|
||||
aria-label={isUiDesignDialog ? '生成UI设计图' : '生成规范'}
|
||||
onPointerDown={(event) => event.stopPropagation()}
|
||||
onSubmit={(event) => {
|
||||
event.preventDefault();
|
||||
if (dialog.status !== 'generating') {
|
||||
onSubmit(dialog);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="image-canvas-editor__reference-strip">
|
||||
{isUiDesignDialog || dialog.specType ? (
|
||||
@@ -251,15 +245,14 @@ export function ImageCanvasSpecGenerationPanelView({
|
||||
</div>
|
||||
<div className="image-canvas-editor__spec-fields">
|
||||
{isUiDesignDialog ? (
|
||||
<label className="image-canvas-editor__field-block image-canvas-editor__field-block--single">
|
||||
<div className="image-canvas-editor__field-block image-canvas-editor__field-block--single">
|
||||
<AutoGrowTextArea
|
||||
aria-label="UI设计要求"
|
||||
value={dialog.prompt}
|
||||
disabled={isGenerating}
|
||||
placeholder="你希望这个 UI 长什么样?"
|
||||
className="image-canvas-editor__generation-prompt"
|
||||
onChange={(event) => {
|
||||
const nextPrompt = event.target.value;
|
||||
onValueChange={(nextPrompt) => {
|
||||
if (setGenerateDialog) {
|
||||
setGenerateDialog((currentDialog) =>
|
||||
currentDialog?.mode === 'ui-design'
|
||||
@@ -282,9 +275,9 @@ export function ImageCanvasSpecGenerationPanelView({
|
||||
onUpdateSpecFormValue('customPrompt', nextPrompt);
|
||||
}}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
) : dialog.specType === 'custom' ? (
|
||||
<label className="image-canvas-editor__field-block">
|
||||
<div className="image-canvas-editor__field-block">
|
||||
<PlatformFieldLabel
|
||||
variant="form"
|
||||
className="image-canvas-editor__field-title"
|
||||
@@ -297,11 +290,11 @@ export function ImageCanvasSpecGenerationPanelView({
|
||||
placeholder="写下你想要的任何内容,陶泥儿帮你形成新的约束"
|
||||
disabled={isGenerating}
|
||||
className="image-canvas-editor__generation-prompt image-canvas-editor__spec-textarea"
|
||||
onChange={(event) =>
|
||||
onUpdateSpecFormValue('customPrompt', event.target.value)
|
||||
onValueChange={(value) =>
|
||||
onUpdateSpecFormValue('customPrompt', value)
|
||||
}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<label className="image-canvas-editor__field-block">
|
||||
@@ -438,6 +431,11 @@ export function ImageCanvasSpecGenerationPanelView({
|
||||
)}
|
||||
submitLabel="生成"
|
||||
submitAriaLabel="生成UI设计图"
|
||||
onSubmitRequest={() => {
|
||||
if (dialog.status !== 'generating') {
|
||||
onSubmit(dialog);
|
||||
}
|
||||
}}
|
||||
optionLabelPrefix="生成图片"
|
||||
renderEditorPortal={renderEditorPortal}
|
||||
buildPortalMenuStyle={buildPortalMenuStyle}
|
||||
@@ -471,13 +469,18 @@ export function ImageCanvasSpecGenerationPanelView({
|
||||
</PlatformInlineOptionButton>
|
||||
</div>
|
||||
<PlatformActionButton
|
||||
type="submit"
|
||||
type="button"
|
||||
tone="secondary"
|
||||
size="xs"
|
||||
shape="pill"
|
||||
className="image-canvas-editor__generation-submit image-canvas-editor__spec-submit"
|
||||
disabled={isGenerating || hasPendingImageReferenceUploads}
|
||||
aria-label="提交生成规范"
|
||||
onClick={() => {
|
||||
if (dialog.status !== 'generating') {
|
||||
onSubmit(dialog);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{isGenerating ? (
|
||||
'生成中'
|
||||
@@ -493,7 +496,7 @@ export function ImageCanvasSpecGenerationPanelView({
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{isGenerationReferenceMenuOpen && generationReferenceButtonRef
|
||||
? renderEditorPortal(
|
||||
<PlatformFloatingMenu
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
import { readFileSync } from 'node:fs';
|
||||
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
const stylesheet = readFileSync(
|
||||
new URL('../../index.css', import.meta.url),
|
||||
'utf8',
|
||||
);
|
||||
|
||||
function readCssRule(selector: string) {
|
||||
const ruleStart = stylesheet.indexOf(selector);
|
||||
expect(ruleStart).toBeGreaterThanOrEqual(0);
|
||||
const bodyStart = stylesheet.indexOf('{', ruleStart) + 1;
|
||||
const bodyEnd = stylesheet.indexOf('}', bodyStart);
|
||||
expect(bodyEnd).toBeGreaterThan(bodyStart);
|
||||
return stylesheet.slice(bodyStart, bodyEnd);
|
||||
}
|
||||
|
||||
describe('image canvas textarea chrome', () => {
|
||||
it('keeps complete chrome in the canonical generation prompt styles', () => {
|
||||
const generationRule = readCssRule(
|
||||
'.image-canvas-editor__generation-prompt',
|
||||
);
|
||||
expect(generationRule).toContain('border: 1px solid');
|
||||
expect(generationRule).toContain('border-radius: 1.05rem;');
|
||||
expect(generationRule).toContain('background: #f8fafc;');
|
||||
expect(generationRule).toContain('padding: 0.52rem 0.82rem;');
|
||||
|
||||
const generationFocusRule = readCssRule(
|
||||
'.image-canvas-editor__generation-prompt:focus,',
|
||||
);
|
||||
expect(generationFocusRule).toContain(
|
||||
'border-color: var(--image-canvas-brand-border-strong);',
|
||||
);
|
||||
expect(generationFocusRule).toContain(
|
||||
'box-shadow: 0 0 0 3px var(--image-canvas-brand-focus-ring);',
|
||||
);
|
||||
|
||||
const modalRule = readCssRule('.image-canvas-editor__generate-prompt');
|
||||
expect(modalRule).toContain('border: 1px solid #d9dee8;');
|
||||
expect(modalRule).toContain('border-radius: 0.45rem;');
|
||||
expect(modalRule).toContain('background: #f8fafc;');
|
||||
expect(modalRule).toContain('padding: 0.7rem;');
|
||||
|
||||
const modalFocusRule = readCssRule(
|
||||
'.image-canvas-editor__generate-prompt:focus',
|
||||
);
|
||||
expect(modalFocusRule).toContain(
|
||||
'border-color: var(--image-canvas-brand-border-strong);',
|
||||
);
|
||||
expect(modalFocusRule).toContain(
|
||||
'box-shadow: 0 0 0 3px var(--image-canvas-brand-focus-ring);',
|
||||
);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user