fix(canvas): add full-height history rail

This commit is contained in:
inman
2026-09-07 15:09:12 +08:00
parent 4477d0da7d
commit a705253261
8 changed files with 230 additions and 122 deletions

View File

@@ -53,10 +53,12 @@ export function DesignConversationPane({
workspace,
quoteBlockers,
generationAvailable,
showPlanHistory = true,
}: {
workspace: DesignWorkspace;
quoteBlockers: DesignCompilationIssue[];
generationAvailable: boolean;
showPlanHistory?: boolean;
}) {
const chatDraft = useImageWorkspaceStore((state) => state.chatDraft);
const setChatDraft = useImageWorkspaceStore((state) => state.setChatDraft);
@@ -210,7 +212,7 @@ export function DesignConversationPane({
))}
<DesignPendingOperationsPanel />
<DesignPlanHistory workspace={workspace} />
{showPlanHistory ? <DesignPlanHistory workspace={workspace} /> : null}
{pendingChatMessages.length === 0 && (
<YouthCreationCard
workspace={workspace}

View File

@@ -0,0 +1,66 @@
import { Clock3, Images } from 'lucide-react';
import { cn } from '@/lib/utils';
import type { DesignWorkspace } from '../../../shared/image-workspace';
import { DesignPlanHistory } from './DesignPlanHistory';
export function DesignHistoryRail({ workspace }: { workspace: DesignWorkspace | null }) {
const taskCount = workspace?.tasks.length ?? 0;
const resultCount = workspace?.assets.filter((asset) => asset.generationTaskId).length ?? 0;
const platform = window.electron?.platform;
return (
<aside
data-testid="image-workspace-history-rail"
aria-label="历史作品"
className="flex h-full min-h-0 w-[320px] shrink-0 flex-col border-l border-border/70 bg-background font-sans xl:w-[340px]"
>
<header className={cn(
'flex shrink-0 justify-between gap-3 border-b border-border/70 px-4',
platform === 'darwin' || platform === 'win32'
? 'min-h-[104px] items-end pb-3 pt-10'
: 'min-h-16 items-center',
)}>
<div className="min-w-0">
<div className="flex items-center gap-2">
<Images className="h-4 w-4 shrink-0 text-brand" aria-hidden="true" />
<h2 className="truncate text-sm font-semibold text-foreground"></h2>
</div>
<p className="mt-1 text-[11px] tabular-nums text-muted-foreground">
{taskCount > 0 ? `${taskCount} 次制作 · ${resultCount} 个结果` : '当前设计项目的制作记录'}
</p>
</div>
{workspace?.tasks.some((task) => task.status === 'queued' || task.status === 'running') ? (
<span
role="status"
className="inline-flex shrink-0 items-center gap-1.5 rounded-lg bg-brand/[0.06] px-2 py-1 text-[11px] font-medium text-brand"
>
<Clock3 className="h-3.5 w-3.5 animate-pulse" aria-hidden="true" />
</span>
) : null}
</header>
<div data-testid="image-workspace-history-scroll" className="min-h-0 flex-1 overflow-y-auto overscroll-contain p-3">
{!workspace ? (
<div className="rounded-2xl border border-dashed border-border/80 bg-surface-subtle/35 px-4 py-7 text-center">
<Images className="mx-auto h-5 w-5 text-muted-foreground" aria-hidden="true" />
<p className="mt-3 text-xs font-semibold text-foreground"></p>
<p className="mt-1 text-[11px] leading-5 text-muted-foreground">
</p>
</div>
) : taskCount === 0 ? (
<div className="rounded-2xl border border-dashed border-border/80 bg-surface-subtle/35 px-4 py-7 text-center">
<Images className="mx-auto h-5 w-5 text-muted-foreground" aria-hidden="true" />
<p className="mt-3 text-xs font-semibold text-foreground"></p>
<p className="mt-1 text-[11px] leading-5 text-muted-foreground">
</p>
</div>
) : (
<DesignPlanHistory workspace={workspace} />
)}
</div>
</aside>
);
}

View File

@@ -116,7 +116,7 @@ export function DesignWorksRail({
const saveProject = async () => {
const title = projectName.trim();
if (!title) {
setDialogError('请输入作品名称');
setDialogError('请输入项目名称');
return;
}
setSaving(true);
@@ -124,15 +124,15 @@ export function DesignWorksRail({
try {
if (dialog?.mode === 'rename') {
await renameProject(dialog.project.workspaceId, title);
toast.success('作品名称已更新');
toast.success('项目名称已更新');
} else {
await createProject(title);
toast.success('新作品已创建');
toast.success('新项目已创建');
}
setDialog(null);
onProjectSelected?.();
} catch (error) {
setDialogError(error instanceof Error ? error.message : '保存作品失败');
setDialogError(error instanceof Error ? error.message : '保存项目失败');
} finally {
setSaving(false);
}
@@ -141,7 +141,7 @@ export function DesignWorksRail({
const confirmDelete = async () => {
if (!deleteTarget) return;
if (deleteConfirmation !== deleteTarget.title) {
setDeleteError('请输入完整作品名称');
setDeleteError('请输入完整项目名称');
return;
}
setDeleteError(null);
@@ -149,9 +149,9 @@ export function DesignWorksRail({
await deleteProject(deleteTarget.workspaceId);
setDeleteTarget(null);
setDeleteConfirmation('');
toast.success('作品已删除');
toast.success('项目已删除');
} catch (error) {
setDeleteError(error instanceof Error ? error.message : '删除作品失败');
setDeleteError(error instanceof Error ? error.message : '删除项目失败');
}
};
@@ -159,12 +159,12 @@ export function DesignWorksRail({
<aside
data-testid="image-workspace-works-rail"
className="flex h-full min-h-0 flex-col bg-background font-sans"
aria-label="我的作品"
aria-label="设计项目"
>
<header className="flex min-h-16 shrink-0 items-center justify-between gap-3 border-b border-border/65 px-4">
<div>
<h2 className="text-base font-semibold tracking-tight text-foreground"></h2>
<p className="mt-0.5 text-[11px] text-muted-foreground">{projects.length} </p>
<h2 className="text-base font-semibold tracking-tight text-foreground"></h2>
<p className="mt-0.5 text-[11px] text-muted-foreground">{projects.length} </p>
</div>
<Button
type="button"
@@ -174,7 +174,7 @@ export function DesignWorksRail({
onClick={openCreate}
>
<Plus className="mr-1.5 h-3.5 w-3.5" />
</Button>
</header>
@@ -188,7 +188,7 @@ export function DesignWorksRail({
<span className="flex h-9 w-9 items-center justify-center rounded-xl bg-brand/[0.07] text-brand">
<Plus className="h-4 w-4" />
</span>
<span className="mt-3 block text-sm font-medium text-foreground"></span>
<span className="mt-3 block text-sm font-medium text-foreground"></span>
<span className="mt-1 block text-xs leading-5 text-muted-foreground">AI </span>
</button>
) : (
@@ -209,7 +209,7 @@ export function DesignWorksRail({
>
<button
type="button"
aria-label={`打开作品 ${project.title}`}
aria-label={`打开项目 ${project.title}`}
className="flex min-h-[68px] w-full items-center gap-3 rounded-2xl px-2.5 py-2.5 pr-16 text-left focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand/30"
onClick={() => {
void selectProject(project.workspaceId);
@@ -279,11 +279,11 @@ export function DesignWorksRail({
<Dialog open={dialog !== null} onOpenChange={(open) => !open && !saving && setDialog(null)}>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle>{dialog?.mode === 'rename' ? '重命名作品' : '新建作品'}</DialogTitle>
<DialogTitle>{dialog?.mode === 'rename' ? '重命名项目' : '新建项目'}</DialogTitle>
<DialogDescription></DialogDescription>
</DialogHeader>
<div className="space-y-2">
<label htmlFor="canvas-work-name" className="text-sm font-medium text-foreground"></label>
<label htmlFor="canvas-work-name" className="text-sm font-medium text-foreground"></label>
<Input
id="canvas-work-name"
autoFocus
@@ -314,8 +314,8 @@ export function DesignWorksRail({
>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle></DialogTitle>
<DialogDescription></DialogDescription>
<DialogTitle></DialogTitle>
<DialogDescription></DialogDescription>
</DialogHeader>
<div className="space-y-2">
<label htmlFor="delete-canvas-work" className="text-sm font-medium text-foreground">
@@ -332,7 +332,7 @@ export function DesignWorksRail({
<Button type="button" variant="ghost" disabled={Boolean(deletingWorkspaceId)} onClick={() => setDeleteTarget(null)}></Button>
<Button type="button" variant="destructive" disabled={Boolean(deletingWorkspaceId)} onClick={() => void confirmDelete()}>
{deletingWorkspaceId && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
</Button>
</div>
</DialogContent>

View File

@@ -21,6 +21,7 @@ import { useAuthStore } from '@/stores/auth';
import { useImagePromptMuseumStore } from '@/stores/image-prompt-museum';
import { useImageWorkspaceStore } from '@/stores/image-workspace';
import { DesignConversationPane } from './DesignConversationPane';
import { DesignHistoryRail } from './DesignHistoryRail';
import { DesignWorksRail } from './DesignWorksRail';
import { hasActiveCreationPlan } from './plan-lifecycle';
import { projectYouthCreationCard } from './youth-form-projection';
@@ -46,12 +47,22 @@ function useDesktopLayout(): boolean {
function LoadingWorkspace() {
return (
<div className="flex h-full min-h-0 flex-col bg-background" aria-label="正在加载 AI 设计">
<div className="h-16 animate-pulse border-b border-border/60 bg-surface-subtle/60" />
<div className="min-h-0 flex-1 space-y-4 bg-surface-subtle/35 p-6">
<div className="h-16 animate-pulse rounded-2xl bg-surface-subtle" />
<div className="ml-auto h-20 w-4/5 animate-pulse rounded-2xl bg-brand/[0.07]" />
<div className="h-48 w-5/6 animate-pulse rounded-2xl bg-surface-subtle" />
<div className="flex h-full min-h-0 bg-background" aria-label="正在加载 AI 设计">
<div className="flex min-w-0 flex-1 flex-col">
<div className="h-16 animate-pulse border-b border-border/60 bg-surface-subtle/60" />
<div className="min-h-0 flex-1 space-y-4 bg-surface-subtle/35 p-6">
<div className="h-16 animate-pulse rounded-2xl bg-surface-subtle" />
<div className="ml-auto h-20 w-4/5 animate-pulse rounded-2xl bg-brand/[0.07]" />
<div className="h-48 w-5/6 animate-pulse rounded-2xl bg-surface-subtle" />
</div>
</div>
<div className="hidden h-full w-[320px] shrink-0 flex-col border-l border-border/70 bg-background lg:flex xl:w-[340px]">
<div className="h-16 animate-pulse border-b border-border/60 bg-surface-subtle/45" />
<div className="space-y-3 p-3">
{[0, 1, 2].map((item) => (
<div key={item} className="h-16 animate-pulse rounded-xl bg-surface-subtle" />
))}
</div>
</div>
</div>
);
@@ -165,99 +176,104 @@ export function ImageCanvas() {
};
return (
<div data-testid="image-workspace" className="flex h-full min-h-0 flex-col overflow-hidden bg-background font-sans">
<header className={cn(
'flex min-h-16 shrink-0 items-center gap-4 border-b border-border/70 bg-background py-3',
platform === 'darwin'
? 'pl-4 pr-[116px] sm:pl-5'
: platform === 'win32'
? 'pl-5 pr-[260px]'
: 'px-4 sm:px-5',
)}>
<div className="min-w-0 flex-1">
<div className="flex items-center gap-2">
<Sparkles className="h-4 w-4 shrink-0 text-brand" />
<h1 className="truncate text-base font-semibold text-foreground">
{workspace?.workspace.title ?? 'Makelore Canvas'}
</h1>
<div data-testid="image-workspace" className="flex h-full min-h-0 overflow-hidden bg-background font-sans">
<div className="flex min-w-0 flex-1 flex-col">
<header className={cn(
'flex min-h-16 shrink-0 items-center gap-4 border-b border-border/70 bg-background py-3',
platform === 'darwin'
? 'pl-4 pr-[116px] sm:pl-5'
: platform === 'win32'
? 'pl-5 pr-[260px]'
: 'px-4 sm:px-5',
)}>
<div className="min-w-0 flex-1">
<div className="flex items-center gap-2">
<Sparkles className="h-4 w-4 shrink-0 text-brand" />
<h1 className="truncate text-base font-semibold text-foreground">
{workspace?.workspace.title ?? 'Makelore Canvas'}
</h1>
</div>
<p className="mt-1 truncate text-xs text-muted-foreground">
{updatedAt ? `${readiness} · 更新于 ${updatedAt}` : readiness}
</p>
</div>
<p className="mt-1 truncate text-xs text-muted-foreground">
{updatedAt ? `${readiness} · 更新于 ${updatedAt}` : readiness}
</p>
</div>
<div
className={cn(
'hidden items-center gap-1.5 rounded-lg px-2.5 py-1.5 text-xs sm:flex',
streamHealthy ? 'bg-brand/[0.055] text-brand' : 'bg-surface-subtle text-muted-foreground',
)}
title={streamHealthy ? '已经连接' : '正在重新连接,你的内容不会丢失'}
>
{streamHealthy ? <Cloud className="h-3.5 w-3.5" /> : <CloudOff className="h-3.5 w-3.5" />}
{streamHealthy ? '实时同步' : '恢复连接'}
</div>
{workspace && (
<Button
type="button"
variant="ghost"
size="icon"
className="h-9 w-9 rounded-lg"
aria-label="刷新作品"
title="刷新"
onClick={() => void refreshWorkspace()}
<div
className={cn(
'hidden items-center gap-1.5 rounded-lg px-2.5 py-1.5 text-xs sm:flex',
streamHealthy ? 'bg-brand/[0.055] text-brand' : 'bg-surface-subtle text-muted-foreground',
)}
title={streamHealthy ? '已经连接' : '正在重新连接,你的内容不会丢失'}
>
<RefreshCw className="h-4 w-4" />
</Button>
{streamHealthy ? <Cloud className="h-3.5 w-3.5" /> : <CloudOff className="h-3.5 w-3.5" />}
{streamHealthy ? '实时同步' : '恢复连接'}
</div>
{workspace && (
<Button
type="button"
variant="ghost"
size="icon"
className="h-9 w-9 rounded-lg"
aria-label="刷新作品"
title="刷新"
onClick={() => void refreshWorkspace()}
>
<RefreshCw className="h-4 w-4" />
</Button>
)}
{!desktopLayout && (
<Button
type="button"
variant="outline"
size="sm"
className="h-9 rounded-xl"
onClick={() => setWorksOpen(true)}
>
<PanelLeft className="mr-1.5 h-4 w-4" />
</Button>
)}
</header>
{error && workspace && (
<div className="flex shrink-0 items-center justify-between gap-3 border-b border-amber-200 bg-amber-50/70 px-4 py-2 text-xs text-amber-950">
<span>{error}</span>
<button
type="button"
className="h-8 shrink-0 rounded-lg px-3 font-medium hover:bg-amber-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-amber-700/35"
onClick={() => void refreshWorkspace()}
>
</button>
</div>
)}
{!desktopLayout && (
<Button
type="button"
variant="outline"
size="sm"
className="h-9 rounded-xl"
onClick={() => setWorksOpen(true)}
>
<PanelLeft className="mr-1.5 h-4 w-4" />
</Button>
)}
</header>
<main className="flex min-h-0 flex-1">
<div className="flex min-h-0 min-w-0 flex-1">
{workspace ? (
<DesignConversationPane
workspace={workspace}
quoteBlockers={quoteBlockers}
generationAvailable={generationAvailable}
showPlanHistory={!desktopLayout}
/>
) : (
<EmptyWorkspace onCreate={openCreate} />
)}
</div>
</main>
</div>
{error && workspace && (
<div className="flex shrink-0 items-center justify-between gap-3 border-b border-amber-200 bg-amber-50/70 px-4 py-2 text-xs text-amber-950">
<span>{error}</span>
<button
type="button"
className="h-8 shrink-0 rounded-lg px-3 font-medium hover:bg-amber-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-amber-700/35"
onClick={() => void refreshWorkspace()}
>
</button>
</div>
)}
<main className="flex min-h-0 flex-1">
<div className="flex min-h-0 min-w-0 flex-1">
{workspace ? (
<DesignConversationPane
workspace={workspace}
quoteBlockers={quoteBlockers}
generationAvailable={generationAvailable}
/>
) : (
<EmptyWorkspace onCreate={openCreate} />
)}
</div>
</main>
{desktopLayout ? <DesignHistoryRail workspace={workspace} /> : null}
{!desktopLayout && (
<Sheet open={worksOpen} onOpenChange={setWorksOpen}>
<SheetContent side="left" className="flex w-[min(92vw,360px)] flex-col overflow-hidden p-0 sm:max-w-[360px]">
<SheetHeader className="sr-only">
<SheetTitle></SheetTitle>
<SheetDescription> AI </SheetDescription>
<SheetTitle></SheetTitle>
<SheetDescription> AI </SheetDescription>
</SheetHeader>
<DesignWorksRail workspace={workspace} onProjectSelected={() => setWorksOpen(false)} />
</SheetContent>