Initial commit: OmniAI Web Frontend

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-06-02 12:38:01 +08:00
commit bedee3ba8d
183 changed files with 94805 additions and 0 deletions
+151
View File
@@ -0,0 +1,151 @@
import {
CloudSyncOutlined,
DeleteOutlined,
EditOutlined,
FolderOpenOutlined,
MenuFoldOutlined,
MenuUnfoldOutlined,
ReloadOutlined,
} from "@ant-design/icons";
import { useCallback, useMemo, useState } from "react";
import type { WebProjectSummary } from "../../types";
interface ProjectSidebarProps {
projects: WebProjectSummary[];
activeId: string | null;
collapsed: boolean;
filterMode?: "chat" | "image" | "video";
loading?: boolean;
error?: string | null;
onToggle: () => void;
onSelect: (id: string) => void;
onRefresh: () => void;
onRename: (id: string, title: string) => void;
onDelete: (id: string) => void;
}
function formatRelativeTime(dateStr: string): string {
const then = new Date(dateStr).getTime();
if (!Number.isFinite(then)) return "";
const diff = Date.now() - then;
if (diff < 60_000) return "just now";
if (diff < 3_600_000) return `${Math.floor(diff / 60_000)} min ago`;
if (diff < 86_400_000) return `${Math.floor(diff / 3_600_000)} h ago`;
if (diff < 604_800_000) return `${Math.floor(diff / 86_400_000)} d ago`;
return new Date(dateStr).toLocaleDateString("zh-CN");
}
export default function ProjectSidebar({
projects,
activeId,
collapsed,
filterMode,
loading,
error,
onToggle,
onSelect,
onRefresh,
onRename,
onDelete,
}: ProjectSidebarProps) {
const [editingId, setEditingId] = useState<string | null>(null);
const [editValue, setEditValue] = useState("");
const filteredProjects = useMemo(
() => filterMode ? projects.filter((p) => p.mode === filterMode) : projects,
[projects, filterMode],
);
const startRename = useCallback((project: WebProjectSummary) => {
setEditingId(project.id);
setEditValue(project.name);
}, []);
const commitRename = useCallback(() => {
if (editingId && editValue.trim()) {
onRename(editingId, editValue.trim());
}
setEditingId(null);
}, [editValue, editingId, onRename]);
return (
<aside className={`conversation-sidebar${collapsed ? " is-collapsed" : ""}`}>
<div className="conversation-sidebar__header">
<button
type="button"
className="conversation-sidebar__toggle"
onClick={onToggle}
aria-label={collapsed ? "Expand project sidebar" : "Collapse project sidebar"}
>
{collapsed ? <MenuUnfoldOutlined /> : <MenuFoldOutlined />}
</button>
{!collapsed && (
<div className="conversation-sidebar__header-actions">
<button type="button" className="conversation-sidebar__new" onClick={() => onSelect("")}>
<FolderOpenOutlined />
</button>
<button type="button" className="conversation-sidebar__icon-button" onClick={onRefresh} aria-label="刷新记录">
<ReloadOutlined />
</button>
</div>
)}
</div>
{!collapsed && (
<div className="conversation-sidebar__list">
{error ? (
<div className="conversation-sidebar__empty">
<CloudSyncOutlined />
<span>Server data failed to load</span>
</div>
) : filteredProjects.length === 0 ? (
<div className="conversation-sidebar__empty">
<FolderOpenOutlined />
<span>{loading ? "正在加载记录..." : "暂无该类型记录"}</span>
</div>
) : (
filteredProjects.map((project) => (
<div
key={project.id}
className={`conversation-sidebar__item${project.id === activeId ? " is-active" : ""}`}
>
{editingId === project.id ? (
<input
className="conversation-sidebar__rename-input"
value={editValue}
onChange={(event) => setEditValue(event.target.value)}
onBlur={commitRename}
onKeyDown={(event) => {
if (event.key === "Enter") commitRename();
if (event.key === "Escape") setEditingId(null);
}}
autoFocus
/>
) : (
<button
type="button"
className="conversation-sidebar__item-main"
onClick={() => onSelect(project.id)}
>
<span className="conversation-sidebar__item-title">{project.name}</span>
<span className="conversation-sidebar__item-time">
{project.source === "server" ? formatRelativeTime(project.updatedAt) : "preview"}
</span>
</button>
)}
<div className="conversation-sidebar__item-actions">
<button type="button" aria-label="Rename project" onClick={() => startRename(project)}>
<EditOutlined />
</button>
<button type="button" aria-label="Delete project" onClick={() => onDelete(project.id)}>
<DeleteOutlined />
</button>
</div>
</div>
))
)}
</div>
)}
</aside>
);
}