refine realtime canvas toolbar layout
This commit is contained in:
parent
b2d53444ae
commit
356c46f480
@ -2253,65 +2253,300 @@ export function RealtimeCanvasDialog({
|
||||
<DialogDescription>左侧绘制草图,右侧实时预览。</DialogDescription>
|
||||
</DialogHeader>
|
||||
)}
|
||||
{standalone ? (
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="absolute right-4 top-4 z-[60] h-9 w-9 rounded-full bg-background/85 shadow-sm"
|
||||
onClick={() => onOpenChange(false)}
|
||||
title="返回画布"
|
||||
>
|
||||
<X className="h-5 w-5" />
|
||||
</Button>
|
||||
) : null}
|
||||
{/* Top Header - Minimal */}
|
||||
<div className="absolute left-0 right-0 top-0 z-50 flex h-14 items-center justify-start px-6 bg-gradient-to-b from-black/20 to-transparent pointer-events-none">
|
||||
<div className="pointer-events-auto flex items-center gap-3 rounded-full bg-background/95 backdrop-blur-sm px-4 py-1.5 border border-border/50 shadow-sm">
|
||||
<span className="font-semibold text-sm">Realtime Canvas</span>
|
||||
<div className="h-4 w-[1px] bg-border" />
|
||||
<span className={cn("flex items-center gap-1.5 text-xs font-medium", statusBadge.className, "bg-transparent p-0")}>
|
||||
<div className={cn("h-2 w-2 rounded-full", status === "live" ? "bg-emerald-500 animate-pulse" : status === "error" ? "bg-rose-500" : "bg-zinc-400")} />
|
||||
{statusBadge.label}
|
||||
</span>
|
||||
<div className="h-4 w-[1px] bg-border" />
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
className="h-8 rounded-full px-3 text-xs"
|
||||
onClick={() => void saveCurrentProject()}
|
||||
>
|
||||
<Save className="mr-1.5 h-3.5 w-3.5" />
|
||||
保存项目
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
className="h-8 rounded-full px-3 text-xs"
|
||||
onClick={() => {
|
||||
refreshSavedProjects();
|
||||
setProjectPanelOpen((openState) => !openState);
|
||||
}}
|
||||
>
|
||||
<FolderOpen className="mr-1.5 h-3.5 w-3.5" />
|
||||
项目列表
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
className="h-8 rounded-full px-3 text-xs"
|
||||
onClick={startBlankProject}
|
||||
>
|
||||
<Plus className="mr-1.5 h-3.5 w-3.5" />
|
||||
新建空白
|
||||
</Button>
|
||||
<div className="absolute left-4 right-4 top-4 z-50 pointer-events-none">
|
||||
<div className="pointer-events-auto rounded-[28px] border border-black/10 bg-white/96 shadow-[0_22px_70px_rgba(15,23,42,0.14)] backdrop-blur-sm">
|
||||
<div className="flex flex-wrap items-center gap-2 px-3 py-3">
|
||||
<div className="flex items-center gap-3 rounded-full border border-black/10 bg-zinc-50 px-4 py-2">
|
||||
<span className="text-sm font-semibold text-zinc-900">Realtime Canvas</span>
|
||||
<span
|
||||
className={cn(
|
||||
"flex items-center gap-1.5 rounded-full px-2.5 py-1 text-xs font-medium",
|
||||
statusBadge.className
|
||||
)}
|
||||
>
|
||||
<span
|
||||
className={cn(
|
||||
"h-2 w-2 rounded-full",
|
||||
status === "live"
|
||||
? "bg-emerald-500 animate-pulse"
|
||||
: status === "error"
|
||||
? "bg-rose-500"
|
||||
: "bg-zinc-400"
|
||||
)}
|
||||
/>
|
||||
{statusBadge.label}
|
||||
</span>
|
||||
<span className="hidden text-xs text-zinc-500 sm:inline">
|
||||
延迟 {adaptiveThrottleMs}ms
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap items-center gap-1 rounded-full border border-black/10 bg-white px-1.5 py-1">
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
className="h-9 rounded-full px-3 text-xs"
|
||||
onClick={() => void saveCurrentProject()}
|
||||
>
|
||||
<Save className="mr-1.5 h-3.5 w-3.5" />
|
||||
保存项目
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
className="h-9 rounded-full px-3 text-xs"
|
||||
onClick={() => {
|
||||
refreshSavedProjects();
|
||||
setProjectPanelOpen((openState) => !openState);
|
||||
}}
|
||||
>
|
||||
<FolderOpen className="mr-1.5 h-3.5 w-3.5" />
|
||||
项目列表
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
className="h-9 rounded-full px-3 text-xs"
|
||||
onClick={startBlankProject}
|
||||
>
|
||||
<Plus className="mr-1.5 h-3.5 w-3.5" />
|
||||
新建空白
|
||||
</Button>
|
||||
{standalone ? (
|
||||
<Button
|
||||
type="button"
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
className="h-9 w-9 rounded-full"
|
||||
onClick={() => onOpenChange(false)}
|
||||
title="返回画布"
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap items-center gap-1 rounded-full border border-black/10 bg-white px-1.5 py-1">
|
||||
<Button
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
className={cn(
|
||||
"h-10 w-10 rounded-full text-zinc-600 hover:bg-zinc-100",
|
||||
brushMode === "brush" && "bg-black text-white hover:bg-black"
|
||||
)}
|
||||
onClick={() => setBrushMode("brush")}
|
||||
title="画笔"
|
||||
>
|
||||
<Paintbrush className="h-5 w-5" />
|
||||
</Button>
|
||||
<Button
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
className={cn(
|
||||
"h-10 w-10 rounded-full text-zinc-600 hover:bg-zinc-100",
|
||||
brushMode === "eraser" && "bg-black text-white hover:bg-black"
|
||||
)}
|
||||
onClick={() => setBrushMode("eraser")}
|
||||
title="橡皮"
|
||||
>
|
||||
<Eraser className="h-5 w-5" />
|
||||
</Button>
|
||||
<Button
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
className={cn(
|
||||
"h-10 w-10 rounded-full text-zinc-600 hover:bg-zinc-100",
|
||||
brushMode === "rect" && "bg-black text-white hover:bg-black"
|
||||
)}
|
||||
onClick={() => setBrushMode("rect")}
|
||||
title="矩形"
|
||||
>
|
||||
<Square className="h-5 w-5" />
|
||||
</Button>
|
||||
<Button
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
className={cn(
|
||||
"h-10 w-10 rounded-full text-zinc-600 hover:bg-zinc-100",
|
||||
brushMode === "circle" && "bg-black text-white hover:bg-black"
|
||||
)}
|
||||
onClick={() => setBrushMode("circle")}
|
||||
title="圆形"
|
||||
>
|
||||
<CircleIcon className="h-5 w-5" />
|
||||
</Button>
|
||||
<Button
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
className={cn(
|
||||
"h-10 w-10 rounded-full text-zinc-600 hover:bg-zinc-100",
|
||||
brushMode === "line" && "bg-black text-white hover:bg-black"
|
||||
)}
|
||||
onClick={() => setBrushMode("line")}
|
||||
title="直线"
|
||||
>
|
||||
<Slash className="h-5 w-5" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap items-center gap-2 rounded-full border border-black/10 bg-zinc-50 px-3 py-2">
|
||||
<button
|
||||
type="button"
|
||||
className="flex h-10 items-center gap-2 rounded-full border border-black/10 bg-white px-3 text-xs font-medium text-zinc-700 transition hover:bg-zinc-50"
|
||||
onClick={() => colorInputRef.current?.click()}
|
||||
title="选择画笔颜色"
|
||||
>
|
||||
<span
|
||||
className="h-5 w-5 rounded-full border border-black/15 shadow-inner"
|
||||
style={{ backgroundColor: brushColor }}
|
||||
/>
|
||||
颜色
|
||||
</button>
|
||||
<input
|
||||
ref={colorInputRef}
|
||||
type="color"
|
||||
value={brushColor}
|
||||
onChange={(event) => setBrushColor(event.target.value)}
|
||||
className="sr-only"
|
||||
title="自定义颜色"
|
||||
/>
|
||||
<span className="min-w-[64px] text-[11px] uppercase tracking-wide text-zinc-500">
|
||||
{brushColor}
|
||||
</span>
|
||||
<div className="flex min-w-[210px] flex-1 items-center gap-3 rounded-full border border-black/10 bg-white px-3 py-2">
|
||||
<span className="whitespace-nowrap text-xs font-medium text-zinc-600">
|
||||
粗细 {brushSize}
|
||||
</span>
|
||||
<Slider
|
||||
value={[brushSize]}
|
||||
min={2}
|
||||
max={50}
|
||||
step={1}
|
||||
onValueChange={([val]) => setBrushSize(val)}
|
||||
className="min-w-[140px] flex-1 [&>span:first-child]:h-2.5 [&>span:first-child]:bg-zinc-200 [&>span:first-child]:shadow-inner [&>span:first-child>span]:bg-sky-500 [&>span:last-child]:h-5 [&>span:last-child]:w-5 [&>span:last-child]:border-2 [&>span:last-child]:border-sky-500 [&>span:last-child]:bg-white"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-1 rounded-full border border-black/10 bg-white px-2 py-2">
|
||||
{CANVAS_BACKGROUND_PRESETS.map((color) => (
|
||||
<button
|
||||
key={color}
|
||||
type="button"
|
||||
className={cn(
|
||||
"h-5 w-5 rounded-full border border-black/15 transition-all",
|
||||
canvasBackground === color && "scale-110 ring-2 ring-black/20"
|
||||
)}
|
||||
style={{ backgroundColor: color }}
|
||||
onClick={() => handleCanvasBackgroundChange(color)}
|
||||
title="画布底色"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap items-center gap-1 rounded-full border border-black/10 bg-white px-1.5 py-1">
|
||||
<Button
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
className="h-10 w-10 rounded-full text-zinc-600 hover:bg-zinc-100"
|
||||
onClick={() => uploadInputRef.current?.click()}
|
||||
title="上传图片"
|
||||
>
|
||||
<Upload className="h-5 w-5" />
|
||||
</Button>
|
||||
<Button
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
className="h-10 w-10 rounded-full text-zinc-600 hover:bg-zinc-100"
|
||||
onClick={clearSketch}
|
||||
title="清空"
|
||||
>
|
||||
<Trash2 className="h-5 w-5" />
|
||||
</Button>
|
||||
<Button
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
className="h-10 w-10 rounded-full text-zinc-600 hover:bg-zinc-100"
|
||||
onClick={undo}
|
||||
disabled={!canUndo}
|
||||
title="撤销"
|
||||
>
|
||||
<Undo2 className="h-5 w-5" />
|
||||
</Button>
|
||||
<Button
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
className="h-10 w-10 rounded-full text-zinc-600 hover:bg-zinc-100"
|
||||
onClick={redo}
|
||||
disabled={!canRedo}
|
||||
title="重做"
|
||||
>
|
||||
<Redo2 className="h-5 w-5" />
|
||||
</Button>
|
||||
<Button
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
className="h-10 w-10 rounded-full text-zinc-600 hover:bg-zinc-100"
|
||||
onClick={handleDownloadCanvas}
|
||||
title="保存图片"
|
||||
>
|
||||
<Download className="h-5 w-5" />
|
||||
</Button>
|
||||
<Button
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
className="h-10 w-10 rounded-full text-zinc-600 hover:bg-zinc-100"
|
||||
onClick={forceReconnect}
|
||||
title="重连"
|
||||
>
|
||||
<RefreshCw className="h-5 w-5" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="flex min-w-[280px] flex-1 flex-wrap items-center gap-2 rounded-[22px] border border-black/10 bg-zinc-50 px-2 py-2">
|
||||
<div className="flex h-10 w-10 items-center justify-center rounded-full bg-primary/10 text-primary">
|
||||
<Sparkles className="h-4 w-4" />
|
||||
</div>
|
||||
<Input
|
||||
value={prompt}
|
||||
onChange={(e) => setPrompt(e.target.value)}
|
||||
onBlur={() => scheduleFlush(true, false)}
|
||||
placeholder="输入指令,基于当前画布实时生成"
|
||||
className="h-10 min-w-[180px] flex-1 border-0 bg-transparent px-2 text-sm shadow-none focus-visible:ring-0"
|
||||
/>
|
||||
<Button
|
||||
size="sm"
|
||||
className="h-10 rounded-xl px-4 font-medium"
|
||||
onClick={triggerGenerate}
|
||||
disabled={status === "connecting"}
|
||||
>
|
||||
{status === "connecting" ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
"生成"
|
||||
)}
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="h-10 rounded-xl px-4 font-medium"
|
||||
onClick={handleApplyToCanvas}
|
||||
disabled={!previewUrl}
|
||||
>
|
||||
<Send className="mr-1.5 h-4 w-4" />
|
||||
发送到画布
|
||||
</Button>
|
||||
<span className="ml-auto text-xs text-zinc-500">
|
||||
{statusMessage}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{projectPanelOpen ? (
|
||||
<div className="absolute left-6 top-16 z-40 w-[22rem] max-w-[calc(100vw-2rem)] rounded-[24px] border border-black/10 bg-white/95 p-4 shadow-2xl backdrop-blur-xl">
|
||||
<div className="absolute left-4 top-28 z-40 w-[22rem] max-w-[calc(100vw-2rem)] rounded-[24px] border border-black/10 bg-white/95 p-4 shadow-2xl backdrop-blur-xl">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<div className="text-sm font-semibold text-zinc-900">项目列表</div>
|
||||
@ -2450,7 +2685,7 @@ export function RealtimeCanvasDialog({
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="absolute right-4 top-20 bottom-32 z-10 flex w-[min(16rem,42vw)] flex-col rounded-[22px] border border-black/10 bg-white/92 p-3 shadow-xl backdrop-blur-xl">
|
||||
<div className="absolute left-3 top-28 bottom-4 z-10 flex w-[11.75rem] max-w-[38vw] flex-col rounded-[20px] border border-black/10 bg-white/96 p-3 shadow-xl">
|
||||
<div className="flex items-center justify-between border-b border-black/10 pb-3">
|
||||
<div className="flex items-center gap-2 text-sm font-semibold text-zinc-900">
|
||||
<Layers3 className="h-4 w-4" />
|
||||
@ -2597,6 +2832,7 @@ export function RealtimeCanvasDialog({
|
||||
scheduleFlush(true);
|
||||
}
|
||||
}}
|
||||
className="flex-1 [&>span:first-child]:h-2.5 [&>span:first-child]:bg-zinc-200 [&>span:first-child]:shadow-inner [&>span:first-child>span]:bg-sky-500 [&>span:last-child]:h-5 [&>span:last-child]:w-5 [&>span:last-child]:border-2 [&>span:last-child]:border-sky-500 [&>span:last-child]:bg-white"
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
@ -2656,254 +2892,14 @@ export function RealtimeCanvasDialog({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Floating Toolbar - Top (Lovart-like) */}
|
||||
<div className="absolute left-1/2 top-[72px] z-20 -translate-x-1/2 pointer-events-none">
|
||||
<div className="pointer-events-auto flex items-center gap-1 rounded-[30px] border border-black/10 bg-white/90 p-2 shadow-xl backdrop-blur-xl">
|
||||
<Button
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
className={cn(
|
||||
"h-10 w-10 rounded-full text-zinc-600 hover:bg-zinc-100",
|
||||
brushMode === "brush" && "bg-black text-white hover:bg-black"
|
||||
)}
|
||||
onClick={() => setBrushMode("brush")}
|
||||
title="画笔"
|
||||
>
|
||||
<Paintbrush className="h-5 w-5" />
|
||||
</Button>
|
||||
<Button
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
className={cn(
|
||||
"h-10 w-10 rounded-full text-zinc-600 hover:bg-zinc-100",
|
||||
brushMode === "eraser" && "bg-black text-white hover:bg-black"
|
||||
)}
|
||||
onClick={() => setBrushMode("eraser")}
|
||||
title="橡皮"
|
||||
>
|
||||
<Eraser className="h-5 w-5" />
|
||||
</Button>
|
||||
<Button
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
className={cn(
|
||||
"h-10 w-10 rounded-full text-zinc-600 hover:bg-zinc-100",
|
||||
brushMode === "rect" && "bg-black text-white hover:bg-black"
|
||||
)}
|
||||
onClick={() => setBrushMode("rect")}
|
||||
title="矩形"
|
||||
>
|
||||
<Square className="h-5 w-5" />
|
||||
</Button>
|
||||
<Button
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
className={cn(
|
||||
"h-10 w-10 rounded-full text-zinc-600 hover:bg-zinc-100",
|
||||
brushMode === "circle" && "bg-black text-white hover:bg-black"
|
||||
)}
|
||||
onClick={() => setBrushMode("circle")}
|
||||
title="圆形"
|
||||
>
|
||||
<CircleIcon className="h-5 w-5" />
|
||||
</Button>
|
||||
<Button
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
className={cn(
|
||||
"h-10 w-10 rounded-full text-zinc-600 hover:bg-zinc-100",
|
||||
brushMode === "line" && "bg-black text-white hover:bg-black"
|
||||
)}
|
||||
onClick={() => setBrushMode("line")}
|
||||
title="直线"
|
||||
>
|
||||
<Slash className="h-5 w-5" />
|
||||
</Button>
|
||||
|
||||
<div className="mx-1 h-7 w-px bg-black/10" />
|
||||
|
||||
<div className="flex items-center gap-2 px-1">
|
||||
<button
|
||||
type="button"
|
||||
className="flex h-10 items-center gap-2 rounded-full border border-black/10 bg-white px-3 text-xs font-medium text-zinc-700 transition hover:bg-zinc-50"
|
||||
onClick={() => colorInputRef.current?.click()}
|
||||
title="选择画笔颜色"
|
||||
>
|
||||
<span
|
||||
className="h-5 w-5 rounded-full border border-black/15 shadow-inner"
|
||||
style={{ backgroundColor: brushColor }}
|
||||
/>
|
||||
颜色
|
||||
</button>
|
||||
<input
|
||||
ref={colorInputRef}
|
||||
type="color"
|
||||
value={brushColor}
|
||||
onChange={(event) => setBrushColor(event.target.value)}
|
||||
className="sr-only"
|
||||
title="自定义颜色"
|
||||
/>
|
||||
<span className="min-w-[58px] text-[11px] uppercase tracking-wide text-zinc-400">
|
||||
{brushColor}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="mx-1 h-7 w-px bg-black/10" />
|
||||
|
||||
<div className="flex min-w-[132px] items-center gap-2 px-1">
|
||||
<span className="whitespace-nowrap text-xs font-medium text-zinc-600">
|
||||
粗细 {brushSize}
|
||||
</span>
|
||||
<Slider
|
||||
value={[brushSize]}
|
||||
min={2}
|
||||
max={50}
|
||||
step={1}
|
||||
onValueChange={([val]) => setBrushSize(val)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mx-1 h-7 w-px bg-black/10" />
|
||||
|
||||
<div className="flex items-center gap-1">
|
||||
{CANVAS_BACKGROUND_PRESETS.map((color) => (
|
||||
<button
|
||||
key={color}
|
||||
type="button"
|
||||
className={cn(
|
||||
"h-4 w-4 rounded border border-black/15 transition-all",
|
||||
canvasBackground === color && "scale-110 ring-2 ring-black/20"
|
||||
)}
|
||||
style={{ backgroundColor: color }}
|
||||
onClick={() => handleCanvasBackgroundChange(color)}
|
||||
title="画布底色"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="mx-1 h-7 w-px bg-black/10" />
|
||||
|
||||
<Button
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
className="h-10 w-10 rounded-full text-zinc-600 hover:bg-zinc-100"
|
||||
onClick={() => uploadInputRef.current?.click()}
|
||||
title="上传图片"
|
||||
>
|
||||
<Upload className="h-5 w-5" />
|
||||
</Button>
|
||||
<Button
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
className="h-10 w-10 rounded-full text-zinc-600 hover:bg-zinc-100"
|
||||
onClick={clearSketch}
|
||||
title="清空"
|
||||
>
|
||||
<Trash2 className="h-5 w-5" />
|
||||
</Button>
|
||||
<Button
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
className="h-10 w-10 rounded-full text-zinc-600 hover:bg-zinc-100"
|
||||
onClick={undo}
|
||||
disabled={!canUndo}
|
||||
title="撤销"
|
||||
>
|
||||
<Undo2 className="h-5 w-5" />
|
||||
</Button>
|
||||
<Button
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
className="h-10 w-10 rounded-full text-zinc-600 hover:bg-zinc-100"
|
||||
onClick={redo}
|
||||
disabled={!canRedo}
|
||||
title="重做"
|
||||
>
|
||||
<Redo2 className="h-5 w-5" />
|
||||
</Button>
|
||||
<Button
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
className="h-10 w-10 rounded-full text-zinc-600 hover:bg-zinc-100"
|
||||
onClick={triggerGenerate}
|
||||
disabled={status === "connecting"}
|
||||
title="生成"
|
||||
>
|
||||
{status === "connecting" ? (
|
||||
<Loader2 className="h-5 w-5 animate-spin" />
|
||||
) : (
|
||||
<Sparkles className="h-5 w-5" />
|
||||
)}
|
||||
</Button>
|
||||
<Button
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
className="h-10 w-10 rounded-full text-zinc-600 hover:bg-zinc-100"
|
||||
onClick={handleDownloadCanvas}
|
||||
title="保存"
|
||||
>
|
||||
<Download className="h-5 w-5" />
|
||||
</Button>
|
||||
<Button
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
className="h-10 w-10 rounded-full text-zinc-600 hover:bg-zinc-100"
|
||||
onClick={forceReconnect}
|
||||
title="重连"
|
||||
>
|
||||
<RefreshCw className="h-5 w-5" />
|
||||
</Button>
|
||||
</div>
|
||||
<input
|
||||
ref={uploadInputRef}
|
||||
type="file"
|
||||
accept="image/*"
|
||||
multiple
|
||||
className="hidden"
|
||||
onChange={handleSketchUpload}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Floating Toolbar - Bottom (Prompt) */}
|
||||
<div className="absolute bottom-8 left-1/2 -translate-x-1/2 w-full max-w-2xl px-4 z-20 pointer-events-none">
|
||||
<div className="pointer-events-auto flex flex-col gap-2 p-1.5 bg-background/80 backdrop-blur-xl border border-border/50 shadow-2xl rounded-[24px]">
|
||||
<div className="flex items-center gap-2 pl-2 pr-1.5 py-1">
|
||||
<div className="flex h-8 w-8 items-center justify-center rounded-full bg-primary/10 text-primary">
|
||||
<Sparkles className="h-4 w-4" />
|
||||
</div>
|
||||
<Input
|
||||
value={prompt}
|
||||
onChange={(e) => setPrompt(e.target.value)}
|
||||
onBlur={() => scheduleFlush(true, false)}
|
||||
placeholder="Describe what you want to create..."
|
||||
className="flex-1 h-10 border-0 bg-transparent focus-visible:ring-0 px-2 text-base shadow-none"
|
||||
/>
|
||||
<Button
|
||||
size="sm"
|
||||
className="h-9 rounded-xl px-4 font-medium"
|
||||
onClick={triggerGenerate}
|
||||
disabled={status === 'connecting'}
|
||||
>
|
||||
{status === 'connecting' ? <Loader2 className="h-4 w-4 animate-spin" /> : 'Generate'}
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="h-9 rounded-xl px-4 font-medium"
|
||||
onClick={handleApplyToCanvas}
|
||||
disabled={!previewUrl}
|
||||
>
|
||||
<Send className="h-4 w-4 mr-1.5" />
|
||||
发送到画布
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-center mt-2">
|
||||
<span className="text-[10px] text-white/50 bg-black/40 px-3 py-1 rounded-full backdrop-blur-sm">
|
||||
{statusMessage} • Latency: {adaptiveThrottleMs}ms
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<input
|
||||
ref={uploadInputRef}
|
||||
type="file"
|
||||
accept="image/*"
|
||||
multiple
|
||||
className="hidden"
|
||||
onChange={handleSketchUpload}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user