feat: 支持 AI 设计项目多会话交互
需求:用户可在同一设计项目中新建和切换会话,任务列表保持项目级共享。 实现: - 增加会话选择、新建入口及本地数据迁移 - Conversation 独立消息、Brief、Quote 与流式状态 - 保留项目任务并修复 Task revision 与 ABA 异步竞态 - 保持 Enter 发送、Shift+Enter 换行及 IME 保护
This commit is contained in:
@@ -2,12 +2,14 @@ import { useEffect, useState } from 'react';
|
||||
import {
|
||||
FolderKanban,
|
||||
Loader2,
|
||||
MessageSquareText,
|
||||
Pencil,
|
||||
Plus,
|
||||
RefreshCw,
|
||||
X,
|
||||
} from 'lucide-react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { toast } from 'sonner';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
Dialog,
|
||||
@@ -36,11 +38,16 @@ export function ImageWorkspaceSidebar({ sidebarCollapsed }: ImageWorkspaceSideba
|
||||
const status = useImageWorkspaceStore((state) => state.status);
|
||||
const bootstrap = useImageWorkspaceStore((state) => state.bootstrap);
|
||||
const activeWorkspaceId = useImageWorkspaceStore((state) => state.activeWorkspaceId);
|
||||
const activeConversationId = useImageWorkspaceStore((state) => state.activeConversationId);
|
||||
const workspace = useImageWorkspaceStore((state) => state.workspace);
|
||||
const creatingConversation = useImageWorkspaceStore((state) => state.creatingConversation);
|
||||
const workspaceError = useImageWorkspaceStore((state) => state.error);
|
||||
const load = useImageWorkspaceStore((state) => state.load);
|
||||
const createProject = useImageWorkspaceStore((state) => state.createProject);
|
||||
const createConversation = useImageWorkspaceStore((state) => state.createConversation);
|
||||
const renameProject = useImageWorkspaceStore((state) => state.renameProject);
|
||||
const selectProject = useImageWorkspaceStore((state) => state.selectProject);
|
||||
const selectConversation = useImageWorkspaceStore((state) => state.selectConversation);
|
||||
const [projectsOpen, setProjectsOpen] = useState(true);
|
||||
const [dialog, setDialog] = useState<ProjectDialogState | null>(null);
|
||||
const [projectName, setProjectName] = useState('');
|
||||
@@ -107,6 +114,15 @@ export function ImageWorkspaceSidebar({ sidebarCollapsed }: ImageWorkspaceSideba
|
||||
navigate('/image-canvas');
|
||||
};
|
||||
|
||||
const handleCreateConversation = async () => {
|
||||
try {
|
||||
await createConversation();
|
||||
navigate('/image-canvas');
|
||||
} catch (error) {
|
||||
toast.error(error instanceof Error ? error.message : String(error));
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div data-testid="sidebar-image-workspace" className="min-h-0 space-y-1">
|
||||
<Button
|
||||
@@ -207,7 +223,7 @@ export function ImageWorkspaceSidebar({ sidebarCollapsed }: ImageWorkspaceSideba
|
||||
<span className="min-w-0 flex-1">
|
||||
<span className="block truncate text-sm font-medium">{project.title}</span>
|
||||
<span className="block truncate text-[10px] font-medium text-muted-foreground">
|
||||
{project.brief.summary}
|
||||
{project.conversationCount} 个会话
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
@@ -220,6 +236,58 @@ export function ImageWorkspaceSidebar({ sidebarCollapsed }: ImageWorkspaceSideba
|
||||
<Pencil className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
{active ? (
|
||||
<div className="mt-1 border-t border-brand/15 pt-1.5">
|
||||
<div className="flex min-h-10 items-center justify-between gap-2 px-1">
|
||||
<span className="text-[10px] font-semibold text-muted-foreground">设计会话</span>
|
||||
<button
|
||||
type="button"
|
||||
aria-label="新建设计会话"
|
||||
title="新建设计会话"
|
||||
aria-busy={creatingConversation}
|
||||
className="flex h-10 w-10 shrink-0 items-center justify-center rounded-lg text-muted-foreground transition-[background-color,color,transform] hover:bg-background hover:text-brand active:scale-[0.96]"
|
||||
disabled={creatingConversation}
|
||||
onClick={() => void handleCreateConversation()}
|
||||
>
|
||||
{creatingConversation
|
||||
? <Loader2 className="h-3.5 w-3.5 animate-spin" />
|
||||
: <Plus className="h-3.5 w-3.5" />}
|
||||
</button>
|
||||
</div>
|
||||
<div className="space-y-0.5 pb-1">
|
||||
{workspace?.workspaceId === project.workspaceId
|
||||
? workspace.conversations.slice(0, 6).map((conversation) => {
|
||||
const selected = conversation.conversationId === activeConversationId;
|
||||
return (
|
||||
<button
|
||||
key={conversation.conversationId}
|
||||
type="button"
|
||||
data-testid={`sidebar-image-conversation-${conversation.conversationId}`}
|
||||
aria-current={selected ? 'page' : undefined}
|
||||
className={cn(
|
||||
'flex min-h-10 w-full items-center gap-2 rounded-lg px-2 text-left transition-[background-color,color,transform] active:scale-[0.96]',
|
||||
selected
|
||||
? 'bg-background text-foreground shadow-soft'
|
||||
: 'text-muted-foreground hover:bg-background/70 hover:text-foreground',
|
||||
)}
|
||||
onClick={() => void selectConversation(conversation.conversationId)}
|
||||
>
|
||||
<MessageSquareText className="h-3.5 w-3.5 shrink-0" />
|
||||
<span className="min-w-0 flex-1">
|
||||
<span className="block truncate text-[11px] font-semibold">
|
||||
{conversation.title}
|
||||
</span>
|
||||
<span className="block truncate text-[10px]">
|
||||
{conversation.brief.summary}
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
})
|
||||
: null}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</section>
|
||||
);
|
||||
}) : null}
|
||||
|
||||
Reference in New Issue
Block a user