fix(canvas): soften design conversation
This commit is contained in:
@@ -2,10 +2,10 @@ import { useEffect, useRef } from 'react';
|
||||
import {
|
||||
Clock3,
|
||||
Loader2,
|
||||
MessageSquareText,
|
||||
Send,
|
||||
} from 'lucide-react';
|
||||
import { toast } from 'sonner';
|
||||
import makeloreMark from '@/assets/logo.svg';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import { ImageWorkspaceApiError } from '@/lib/image-workspace';
|
||||
@@ -118,26 +118,30 @@ export function DesignConversationPane({
|
||||
return (
|
||||
<section
|
||||
data-testid="image-workspace-conversation"
|
||||
className="flex min-h-0 flex-1 flex-col bg-surface-subtle/45"
|
||||
className="flex min-h-0 flex-1 flex-col overflow-hidden bg-background"
|
||||
>
|
||||
<div className="min-h-0 flex-1 overflow-y-auto px-4 py-5 sm:px-6">
|
||||
<div className="min-h-0 flex-1 overflow-y-auto px-4 pb-6 pt-4 sm:px-6">
|
||||
{workspace.turns.length === 0
|
||||
&& 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" />
|
||||
</div>
|
||||
<h3 className="text-lg font-semibold text-foreground">先把脑中的画面说出来</h3>
|
||||
<p className="mt-2 text-sm leading-6 text-muted-foreground">
|
||||
<div className="mx-auto flex h-full max-w-[50rem] flex-col items-center justify-center py-10 text-center">
|
||||
<img
|
||||
src={makeloreMark}
|
||||
alt="麦洛 M 标识"
|
||||
className="h-auto w-12 select-none sm:w-[3.25rem]"
|
||||
draggable={false}
|
||||
/>
|
||||
<h3 className="mt-6 max-w-2xl text-balance text-[clamp(1.65rem,2.45vw,2.1rem)] font-medium leading-[1.28] tracking-[-0.04em] text-foreground">
|
||||
你想让麦洛和你一起创作什么?
|
||||
</h3>
|
||||
<p className="mt-3 max-w-md text-pretty text-sm leading-6 text-muted-foreground">
|
||||
不用懂设计。说说想画什么、发生在哪里,或者希望别人看到后有什么感觉;没有想全也没关系。
|
||||
</p>
|
||||
<p className="mt-5 text-xs font-medium text-foreground">不知道怎么开头?试试这样说</p>
|
||||
<div className="mt-5 space-y-2">
|
||||
<div className="mt-7 flex max-w-2xl flex-wrap justify-center gap-2">
|
||||
{STARTER_MESSAGES.map((message) => (
|
||||
<button
|
||||
key={message}
|
||||
type="button"
|
||||
className="min-h-11 w-full rounded-xl border border-border/70 bg-background px-3 py-2.5 text-left text-xs leading-5 text-foreground transition-colors hover:border-brand/30 hover:bg-brand/[0.03] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand/30"
|
||||
className="motion-press rounded-full bg-surface-subtle px-3.5 py-2 text-xs leading-5 text-foreground transition-[background-color,color] duration-150 hover:bg-surface-tertiary hover:text-brand focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand/30"
|
||||
onClick={() => setChatDraft(message)}
|
||||
>
|
||||
{message}
|
||||
@@ -147,16 +151,16 @@ export function DesignConversationPane({
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mx-auto max-w-5xl space-y-4">
|
||||
<div className="mx-auto max-w-[50rem] space-y-6">
|
||||
{workspace.turns.map((turn) => (
|
||||
<div key={turn.turnId} className="space-y-3">
|
||||
<div key={turn.turnId} className="space-y-5">
|
||||
{turn.userMessage && (
|
||||
<div className="ml-auto max-w-[88%] rounded-2xl rounded-br-md border border-brand/20 bg-brand/[0.055] px-3.5 py-2.5 text-sm leading-6 text-foreground">
|
||||
<div className="chat-user-message-surface ml-auto max-w-[82%] whitespace-pre-wrap break-words rounded-[18px] rounded-tr-md px-3.5 py-2.5 text-pretty text-[15px] leading-6 text-foreground">
|
||||
{turn.userMessage}
|
||||
</div>
|
||||
)}
|
||||
{turn.assistantMessage && (
|
||||
<div className="mr-auto max-w-[92%] rounded-2xl rounded-bl-md border border-border/65 bg-background px-3.5 py-3 text-sm leading-6 text-foreground shadow-sm">
|
||||
<div className="chat-assistant-message-surface mr-auto w-full whitespace-pre-wrap break-words py-0.5 text-pretty text-[15px] leading-6 text-foreground">
|
||||
{turn.assistantMessage}
|
||||
</div>
|
||||
)}
|
||||
@@ -169,9 +173,9 @@ export function DesignConversationPane({
|
||||
data-testid={`pending-design-chat-${message.operationId}`}
|
||||
aria-live="polite"
|
||||
aria-atomic="true"
|
||||
className="ml-auto max-w-[88%]"
|
||||
className="ml-auto max-w-[82%]"
|
||||
>
|
||||
<div className="rounded-2xl rounded-br-md border border-brand/20 bg-brand/[0.055] px-3.5 py-2.5 text-sm leading-6 text-foreground">
|
||||
<div className="chat-user-message-surface whitespace-pre-wrap break-words rounded-[18px] rounded-tr-md px-3.5 py-2.5 text-pretty text-[15px] leading-6 text-foreground">
|
||||
{message.message}
|
||||
</div>
|
||||
<div className="mt-1 flex items-center justify-end gap-1.5 pr-1 text-[11px] text-muted-foreground">
|
||||
@@ -190,7 +194,7 @@ export function DesignConversationPane({
|
||||
role="status"
|
||||
aria-live="polite"
|
||||
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"
|
||||
className="chat-assistant-message-surface mr-auto w-full whitespace-pre-wrap break-words py-0.5 text-pretty text-[15px] leading-6 text-foreground"
|
||||
>
|
||||
{reply.text}
|
||||
<span
|
||||
@@ -214,36 +218,43 @@ export function DesignConversationPane({
|
||||
<div ref={scrollAnchorRef} />
|
||||
</div>
|
||||
|
||||
<footer className="border-t border-border/70 bg-background p-3.5 sm:px-6">
|
||||
<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',
|
||||
submittingDesignInput && 'opacity-80',
|
||||
)}>
|
||||
<form
|
||||
data-testid="design-message-composer"
|
||||
className="relative z-20 mx-auto w-[calc(100%_-_2rem)] max-w-[50rem] shrink-0 pb-3 pt-1"
|
||||
onSubmit={(event) => {
|
||||
event.preventDefault();
|
||||
submit();
|
||||
}}
|
||||
>
|
||||
<div
|
||||
data-testid="design-message-composer-surface"
|
||||
className={cn(
|
||||
'chat-composer-surface border bg-background p-3 transition-[border-color,box-shadow] duration-150 focus-within:border-foreground/25 focus-within:ring-1 focus-within:ring-foreground/10',
|
||||
submittingDesignInput && 'opacity-80',
|
||||
)}
|
||||
>
|
||||
<Textarea
|
||||
id="design-chat-composer"
|
||||
aria-label="告诉 AI 你想创作什么"
|
||||
value={composerDraft}
|
||||
rows={2}
|
||||
rows={1}
|
||||
disabled={submittingDesignInput}
|
||||
placeholder="比如:我想做一张社团招新海报,要热闹、有活力……"
|
||||
className="min-h-[52px] resize-none border-0 bg-transparent px-2 py-1.5 shadow-none focus-visible:ring-0 focus-visible:ring-offset-0"
|
||||
placeholder="描述画面,或告诉麦洛想怎么调整"
|
||||
className="max-h-48 !min-h-[42px] resize-none border-0 bg-transparent px-0 py-1 text-[15px] leading-6 shadow-none placeholder:text-muted-foreground/55 focus-visible:ring-0 focus-visible:ring-offset-0"
|
||||
onChange={(event) => setChatDraft(event.currentTarget.value)}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === 'Enter' && !event.shiftKey) {
|
||||
event.preventDefault();
|
||||
submit();
|
||||
}
|
||||
if (event.key !== 'Enter' || event.shiftKey || event.nativeEvent.isComposing) return;
|
||||
event.preventDefault();
|
||||
submit();
|
||||
}}
|
||||
/>
|
||||
<div className="flex items-center justify-between px-1 pt-1">
|
||||
<span className="text-[11px] text-muted-foreground">Enter 发送,Shift + Enter 换行</span>
|
||||
<div className="flex min-h-8 items-center gap-2">
|
||||
<span className="text-[11px] text-muted-foreground">Enter 发送 · Shift + Enter 换行</span>
|
||||
<Button
|
||||
type="button"
|
||||
size="icon"
|
||||
className="h-11 w-11 rounded-xl"
|
||||
type="submit"
|
||||
className="ml-auto h-8 w-8 shrink-0 rounded-full bg-foreground p-0 text-background shadow-soft hover:bg-foreground/90"
|
||||
disabled={!composerDraft.trim() || submittingDesignInput}
|
||||
aria-label="发送消息"
|
||||
onClick={submit}
|
||||
aria-label="发送"
|
||||
>
|
||||
{submittingDesignInput
|
||||
? <Loader2 className="h-4 w-4 animate-spin" />
|
||||
@@ -251,7 +262,7 @@ export function DesignConversationPane({
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</form>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -346,26 +346,26 @@ export function YouthCreationCard({
|
||||
<section
|
||||
data-testid="youth-creation-card"
|
||||
aria-labelledby="youth-creation-card-title"
|
||||
className="overflow-hidden rounded-2xl border border-border/75 bg-background shadow-sm"
|
||||
className="py-2"
|
||||
>
|
||||
<header className="flex items-start justify-between gap-3 border-b border-border/60 px-4 py-3.5 sm:px-5">
|
||||
<header className="mb-3 flex items-start justify-between gap-3 px-1">
|
||||
<div>
|
||||
<h2 id="youth-creation-card-title" className="text-base font-semibold text-foreground">制作方案</h2>
|
||||
<p className="mt-0.5 text-xs text-muted-foreground">确认前可以直接修改最终内容</p>
|
||||
<p className="mt-0.5 text-xs text-muted-foreground">根据对话持续更新,确认前可以直接修改</p>
|
||||
</div>
|
||||
<span className="inline-flex items-center gap-1.5 rounded-full bg-brand/[0.055] px-2.5 py-1 text-[11px] font-medium text-brand">
|
||||
<span className="inline-flex items-center gap-1.5 pt-1 text-[11px] font-medium text-muted-foreground">
|
||||
<Sparkles className="h-3.5 w-3.5" />
|
||||
AI 已根据对话整理
|
||||
麦洛已整理
|
||||
</span>
|
||||
</header>
|
||||
|
||||
<div className="space-y-3 px-4 py-4 sm:px-5">
|
||||
<section className="rounded-xl border border-border/70 bg-surface-subtle/25 p-3">
|
||||
<div className="space-y-4">
|
||||
<section>
|
||||
<div className="flex flex-wrap items-center justify-between gap-2">
|
||||
<label htmlFor="creator-final-prompt" className="text-xs font-semibold text-foreground">创作提示词</label>
|
||||
<button
|
||||
type="button"
|
||||
className="inline-flex h-8 items-center gap-1.5 rounded-lg border border-border/70 bg-background px-2.5 text-xs font-medium text-foreground hover:border-brand/25 hover:text-brand"
|
||||
className="motion-press inline-flex h-8 items-center gap-1.5 rounded-lg bg-surface-subtle px-2.5 text-xs font-medium text-foreground transition-colors hover:bg-surface-tertiary hover:text-brand"
|
||||
disabled={busy}
|
||||
onMouseDown={(event) => event.preventDefault()}
|
||||
onClick={() => {
|
||||
@@ -387,7 +387,7 @@ export function YouthCreationCard({
|
||||
rows={4}
|
||||
disabled={busy}
|
||||
placeholder="AI 会根据前面的对话整理成最终创作提示词,你也可以直接修改。"
|
||||
className="min-h-[104px] resize-y bg-background pr-12 text-sm leading-6"
|
||||
className="min-h-[104px] resize-y rounded-xl border-border/70 bg-surface-subtle/45 pr-12 text-sm leading-6 shadow-none transition-[border-color,background-color,box-shadow] focus-visible:border-foreground/25 focus-visible:bg-background focus-visible:ring-1 focus-visible:ring-foreground/10"
|
||||
onChange={(event) => {
|
||||
const value = event.currentTarget.value;
|
||||
const cursor = event.currentTarget.selectionStart ?? value.length;
|
||||
@@ -457,6 +457,7 @@ export function YouthCreationCard({
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{(references.length > 0 || unboundReferenceNumbers.length > 0) && (
|
||||
<section aria-label="参考图" className="border-t border-border/60 pt-3">
|
||||
<h3 className="text-xs font-semibold text-foreground">参考图 {references.length > 0 ? references.length : ''}</h3>
|
||||
<div className="mt-2 grid gap-2 lg:grid-cols-2">
|
||||
@@ -546,18 +547,20 @@ export function YouthCreationCard({
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<input
|
||||
ref={uploadInputRef}
|
||||
type="file"
|
||||
accept="image/jpeg,image/png,image/webp"
|
||||
className="hidden"
|
||||
aria-label="选择参考图片"
|
||||
onChange={(event) => {
|
||||
const file = event.currentTarget.files?.[0];
|
||||
if (file) void uploadReference(file);
|
||||
}}
|
||||
/>
|
||||
</section>
|
||||
)}
|
||||
|
||||
<input
|
||||
ref={uploadInputRef}
|
||||
type="file"
|
||||
accept="image/jpeg,image/png,image/webp"
|
||||
className="hidden"
|
||||
aria-label="选择参考图片"
|
||||
onChange={(event) => {
|
||||
const file = event.currentTarget.files?.[0];
|
||||
if (file) void uploadReference(file);
|
||||
}}
|
||||
/>
|
||||
|
||||
<section aria-label="制作参数" className="flex flex-wrap items-center gap-x-5 gap-y-2 border-t border-border/60 pt-3">
|
||||
<label className="flex items-center gap-2 text-xs font-medium text-foreground">
|
||||
@@ -646,7 +649,7 @@ export function YouthCreationCard({
|
||||
))}
|
||||
</div>
|
||||
|
||||
<footer className="flex flex-col gap-3 border-t border-border/70 bg-surface-subtle/20 px-4 py-3 sm:flex-row sm:items-center sm:justify-between sm:px-5">
|
||||
<footer className="mt-4 flex flex-col gap-3 border-t border-border/60 pt-3 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div className="flex min-w-0 items-center gap-2 text-xs text-muted-foreground">
|
||||
<span className={cn(
|
||||
'text-emerald-600',
|
||||
|
||||
Reference in New Issue
Block a user