import { FormEvent, useEffect, useMemo, useRef, useState } from 'react'; import { useNavigate } from 'react-router-dom'; import { Archive, Clock3, FolderKanban, FolderOpen, Plus, RotateCcw, Sparkles } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Badge } from '@/components/ui/badge'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { CollaborationProject, getStageName, isProjectInactive, useCollaborationProjectsStore, } from '@/stores/collaboration-projects'; import { cn } from '@/lib/utils'; import { invokeIpc } from '@/lib/api-client'; type OpenDialogResult = { canceled: boolean; filePaths: string[]; }; function formatRelativeTime(value: string) { const date = new Date(value); if (Number.isNaN(date.getTime())) return value; const diff = Date.now() - date.getTime(); const hours = Math.max(1, Math.floor(diff / (60 * 60 * 1000))); if (hours < 24) return `${hours} 小时前`; return `${Math.floor(hours / 24)} 天前`; } function ProjectCard({ project, archived = false, onOpen, onArchive, onRestore, }: { project: CollaborationProject; archived?: boolean; onOpen: () => void; onArchive: () => void; onRestore: () => void; }) { return (

{project.name}

{archived ? ( 归档 ) : null}
{formatRelativeTime(project.lastActiveAt)} {getStageName(project.currentStage)} {project.workspacePath ? ( {project.workspacePath} ) : null}
{project.lastResult}
交付物 {project.deliverables.length} 个 · 文件 {project.files.length} 个
{archived ? ( ) : ( )}
); } export function Home() { const navigate = useNavigate(); const projects = useCollaborationProjectsStore((state) => state.projects); const initialized = useCollaborationProjectsStore((state) => state.initialized); const init = useCollaborationProjectsStore((state) => state.init); const createProject = useCollaborationProjectsStore((state) => state.createProject); const archiveProject = useCollaborationProjectsStore((state) => state.archiveProject); const restoreProject = useCollaborationProjectsStore((state) => state.restoreProject); const touchProject = useCollaborationProjectsStore((state) => state.touchProject); const [name, setName] = useState(''); const [workspacePath, setWorkspacePath] = useState(''); const [nameError, setNameError] = useState(''); const [createOpen, setCreateOpen] = useState(false); const [showInactive, setShowInactive] = useState(false); const nameInputRef = useRef(null); const folderInputRef = useRef(null); useEffect(() => { init(); }, [init]); const resetCreateForm = () => { setName(''); setWorkspacePath(''); setNameError(''); }; const openCreateDialog = () => { resetCreateForm(); setCreateOpen(true); window.setTimeout(() => nameInputRef.current?.focus(), 50); }; const cancelCreateDialog = () => { resetCreateForm(); setCreateOpen(false); }; useEffect(() => { const handleOpenCreateProject = () => openCreateDialog(); window.addEventListener('niancode:create-project', handleOpenCreateProject); return () => window.removeEventListener('niancode:create-project', handleOpenCreateProject); }, []); const visibleGroups = useMemo(() => { const active = projects.filter((project) => !project.archivedAt && !isProjectInactive(project)); const inactive = projects.filter((project) => !project.archivedAt && isProjectInactive(project)); const archived = projects.filter((project) => Boolean(project.archivedAt)); return { active, inactive, archived }; }, [projects]); const openProject = (project: CollaborationProject) => { touchProject(project.id); navigate(`/workbench/${project.id}`); }; const submit = (event: FormEvent) => { event.preventDefault(); const trimmed = name.trim(); if (!trimmed) { setNameError('请输入项目名称'); nameInputRef.current?.focus(); return; } const project = createProject(trimmed, workspacePath); resetCreateForm(); setCreateOpen(false); navigate(`/workbench/${project.id}`); }; const chooseProjectFolder = async () => { const electronApi = (window as Partial).electron; if (electronApi?.ipcRenderer?.invoke) { const result = await invokeIpc('dialog:open', { properties: ['openDirectory'], title: '选择项目储存地址', }); if (!result.canceled && result.filePaths[0]) { setWorkspacePath(result.filePaths[0]); } return; } folderInputRef.current?.click(); }; if (!initialized) { return (
正在读取本地项目
); } return (
AI 项目协作平台

项目导航

进行中项目 归档项目
{visibleGroups.active.length === 0 && visibleGroups.inactive.length === 0 ? (

还没有项目

创建一个项目后,会直接进入阶段一:项目背景。

) : (
{visibleGroups.active.map((project) => ( openProject(project)} onArchive={() => archiveProject(project.id)} onRestore={() => restoreProject(project.id)} /> ))}
)} {visibleGroups.inactive.length > 0 ? (
{showInactive ? (
{visibleGroups.inactive.map((project) => ( openProject(project)} onArchive={() => archiveProject(project.id)} onRestore={() => restoreProject(project.id)} /> ))}
) : null}
) : null}
{visibleGroups.archived.length === 0 ? (
暂无归档项目
) : (
{visibleGroups.archived.map((project) => ( openProject(project)} onArchive={() => archiveProject(project.id)} onRestore={() => restoreProject(project.id)} /> ))}
)}
{ if (open) { openCreateDialog(); } else { cancelCreateDialog(); } }} > 新建项目 只需要先填写项目名称,其他信息会在阶段一继续补充。
{ setName(event.target.value); if (nameError) setNameError(''); }} placeholder="例如:AI 项目协作平台 MVP" className="mt-2" /> {nameError ? (

{nameError}

) : null}
{workspacePath || '选择本地文件夹作为项目储存地址'}
)} onChange={(event) => { const file = event.target.files?.[0]; const relativePath = file && 'webkitRelativePath' in file ? String((file as File & { webkitRelativePath?: string }).webkitRelativePath) : ''; const folderName = relativePath.split('/')[0] || file?.name || ''; if (folderName) setWorkspacePath(folderName); event.target.value = ''; }} />

浏览器预览会记录文件夹名;桌面版会记录完整路径。

); } export default Home;