kit初版,模型引入,agent优化

This commit is contained in:
2026-04-13 00:41:23 +08:00
parent 9b053e302b
commit c435ab15cf
14097 changed files with 5032 additions and 2676248 deletions

View File

@@ -0,0 +1,196 @@
"use client";
import { useState, useRef, useEffect } from "react";
import { useApp } from "@/lib/app-context";
import { getImageUrl } from "@/lib/api";
function timeAgo(ts: number): string {
const diff = Date.now() - ts;
const mins = Math.floor(diff / 60000);
if (mins < 1) return "刚刚";
if (mins < 60) return `${mins}分钟前`;
const hours = Math.floor(mins / 60);
if (hours < 24) return `${hours}小时前`;
const days = Math.floor(hours / 24);
if (days < 30) return `${days}天前`;
return new Date(ts).toLocaleDateString("zh-CN");
}
export function SessionList() {
const {
sessions, activeSessionId, tags, selectedTagIds,
createSession, switchSession, deleteSession, renameSession,
} = useApp();
const [contextMenu, setContextMenu] = useState<{ x: number; y: number; sessionId: string } | null>(null);
const [editingId, setEditingId] = useState<string | null>(null);
const [editTitle, setEditTitle] = useState("");
const editRef = useRef<HTMLInputElement>(null);
// 标签过滤
const filtered = selectedTagIds.length === 0
? sessions
: sessions.filter((s) => selectedTagIds.some((tid) => s.tags.includes(tid)));
const tagMap = new Map(tags.map((t) => [t.id, t]));
// 关闭右键菜单
useEffect(() => {
const close = () => setContextMenu(null);
window.addEventListener("click", close);
return () => window.removeEventListener("click", close);
}, []);
// 编辑聚焦
useEffect(() => {
if (editingId) editRef.current?.focus();
}, [editingId]);
const handleContextMenu = (e: React.MouseEvent, sessionId: string) => {
e.preventDefault();
setContextMenu({ x: e.clientX, y: e.clientY, sessionId });
};
const startRename = (id: string, currentTitle: string) => {
setEditingId(id);
setEditTitle(currentTitle);
setContextMenu(null);
};
const commitRename = () => {
if (editingId && editTitle.trim()) {
renameSession(editingId, editTitle.trim());
}
setEditingId(null);
};
return (
<div className="flex-1 overflow-y-auto">
{/* 新建按钮 */}
<div className="px-3 py-2">
<button
onClick={() => createSession()}
className="w-full flex items-center justify-center gap-1.5 px-3 py-2
text-sm rounded-lg border border-dashed border-[var(--border)]
text-[var(--text-secondary)] hover:border-[var(--accent)]
hover:text-[var(--accent)] transition-colors cursor-pointer"
>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<path d="M12 5v14M5 12h14" />
</svg>
</button>
</div>
{/* 会话列表 */}
<div className="px-2 pb-2 space-y-0.5">
{filtered.map((session) => (
<div
key={session.id}
onClick={() => switchSession(session.id)}
onContextMenu={(e) => handleContextMenu(e, session.id)}
className={`group relative flex items-start gap-2.5 px-2.5 py-2 rounded-lg cursor-pointer
transition-colors ${
session.id === activeSessionId
? "bg-[var(--bg-tertiary)]"
: "hover:bg-[var(--bg-tertiary)]/50"
}`}
>
{/* 缩略图 */}
{session.thumbnail ? (
<img
src={getImageUrl(session.thumbnail)}
alt=""
className="w-9 h-9 rounded object-cover flex-shrink-0 border border-[var(--border)]"
/>
) : (
<div className="w-9 h-9 rounded bg-[var(--bg-primary)] border border-[var(--border)]
flex items-center justify-center flex-shrink-0 text-[var(--text-secondary)] text-xs">
💬
</div>
)}
{/* 会话信息 */}
<div className="flex-1 min-w-0">
{editingId === session.id ? (
<input
ref={editRef}
value={editTitle}
onChange={(e) => setEditTitle(e.target.value)}
onBlur={commitRename}
onKeyDown={(e) => {
if (e.key === "Enter") commitRename();
if (e.key === "Escape") setEditingId(null);
}}
className="w-full text-sm bg-transparent border-b border-[var(--accent)]
text-[var(--text-primary)] focus:outline-none"
/>
) : (
<div className="text-sm text-[var(--text-primary)] truncate">
{session.title}
</div>
)}
{/* 标签 + 时间 */}
<div className="flex items-center gap-1 mt-0.5 flex-wrap">
{session.tags.slice(0, 3).map((tid) => {
const tag = tagMap.get(tid);
if (!tag) return null;
return (
<span
key={tid}
className="text-[10px] px-1.5 py-px rounded-full"
style={{ backgroundColor: tag.color + "22", color: tag.color }}
>
{tag.name}
</span>
);
})}
<span className="text-[10px] text-[var(--text-secondary)] ml-auto flex-shrink-0">
{timeAgo(session.updatedAt)}
</span>
</div>
</div>
</div>
))}
{filtered.length === 0 && (
<div className="text-center text-xs text-[var(--text-secondary)] py-6">
</div>
)}
</div>
{/* 右键菜单 */}
{contextMenu && (
<div
className="fixed z-50 bg-[var(--bg-secondary)] border border-[var(--border)]
rounded-lg shadow-xl py-1 min-w-[120px]"
style={{ left: contextMenu.x, top: contextMenu.y }}
onClick={(e) => e.stopPropagation()}
>
<button
onClick={() => {
const s = sessions.find((s) => s.id === contextMenu.sessionId);
if (s) startRename(s.id, s.title);
}}
className="w-full text-left px-3 py-1.5 text-sm text-[var(--text-primary)]
hover:bg-[var(--bg-tertiary)] cursor-pointer"
>
</button>
<button
onClick={() => {
deleteSession(contextMenu.sessionId);
setContextMenu(null);
}}
className="w-full text-left px-3 py-1.5 text-sm text-red-400
hover:bg-[var(--bg-tertiary)] cursor-pointer"
>
</button>
</div>
)}
</div>
);
}

View File

@@ -0,0 +1,52 @@
"use client";
import { useApp } from "@/lib/app-context";
import { TagFilter } from "./tag-filter";
import { SessionList } from "./session-list";
export function Sidebar() {
const { sidebarCollapsed, setSidebarCollapsed } = useApp();
return (
<>
{/* 移动端遮罩 */}
{!sidebarCollapsed && (
<div
className="fixed inset-0 z-30 bg-black/50 sidebar-overlay md:hidden"
onClick={() => setSidebarCollapsed(true)}
/>
)}
<aside
className={`
/* 移动端:固定定位抽屉 */
fixed inset-y-0 left-0 z-40 w-[280px]
md:relative md:inset-auto md:z-auto
flex-shrink-0 border-r border-[var(--border)] bg-[var(--bg-secondary)]
flex flex-col transition-transform duration-200
${sidebarCollapsed ? "-translate-x-full md:-translate-x-0 md:w-0 md:border-r-0" : "translate-x-0 md:w-[280px]"}
${sidebarCollapsed ? "md:overflow-hidden" : ""}
`}
>
<div className="flex items-center justify-between px-3 py-2 border-b border-[var(--border)]">
<span className="text-xs font-medium text-[var(--text-secondary)] uppercase tracking-wider">
</span>
<button
onClick={() => setSidebarCollapsed(true)}
className="text-[var(--text-secondary)] hover:text-[var(--text-primary)]
transition-colors cursor-pointer p-0.5"
title="折叠侧边栏"
>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<path d="M11 17l-5-5 5-5M18 17l-5-5 5-5" />
</svg>
</button>
</div>
<TagFilter />
<SessionList />
</aside>
</>
);
}

View File

@@ -0,0 +1,148 @@
"use client";
import { useState } from "react";
import { useApp } from "@/lib/app-context";
import type { Tag } from "@/lib/types";
const TAG_COLORS = [
"#6366f1", "#f59e0b", "#10b981", "#ec4899",
"#8b5cf6", "#06b6d4", "#f97316", "#84cc16",
];
export function TagFilter() {
const { tags, selectedTagIds, setSelectedTagIds, addTag, deleteTag } = useApp();
const [showManager, setShowManager] = useState(false);
const [newTagName, setNewTagName] = useState("");
const [newTagColor, setNewTagColor] = useState(TAG_COLORS[0]);
const toggle = (tagId: string) => {
setSelectedTagIds(
selectedTagIds.includes(tagId)
? selectedTagIds.filter((id) => id !== tagId)
: [...selectedTagIds, tagId]
);
};
const clearFilter = () => setSelectedTagIds([]);
const handleAddTag = () => {
const trimmed = newTagName.trim();
if (!trimmed) return;
addTag(trimmed, newTagColor);
setNewTagName("");
setNewTagColor(TAG_COLORS[Math.floor(Math.random() * TAG_COLORS.length)]);
};
return (
<div className="px-3 py-2 border-b border-[var(--border)]">
{/* 标签 pills */}
<div className="flex flex-wrap gap-1.5">
<button
onClick={clearFilter}
className={`px-2 py-0.5 text-xs rounded-full transition-colors cursor-pointer ${
selectedTagIds.length === 0
? "bg-[var(--accent)] text-white"
: "bg-[var(--bg-tertiary)] text-[var(--text-secondary)] hover:text-[var(--text-primary)]"
}`}
>
</button>
{tags.map((tag) => {
const active = selectedTagIds.includes(tag.id);
return (
<button
key={tag.id}
onClick={() => toggle(tag.id)}
className="px-2 py-0.5 text-xs rounded-full transition-colors cursor-pointer border"
style={{
backgroundColor: active ? tag.color + "22" : "transparent",
borderColor: active ? tag.color : "var(--border)",
color: active ? tag.color : "var(--text-secondary)",
}}
>
{tag.name}
</button>
);
})}
<button
onClick={() => setShowManager(!showManager)}
className="px-2 py-0.5 text-xs rounded-full
text-[var(--text-secondary)] hover:text-[var(--accent)]
transition-colors cursor-pointer"
title="管理标签"
>
</button>
</div>
{/* 标签管理面板 */}
{showManager && (
<div className="mt-2 p-2 rounded-lg bg-[var(--bg-tertiary)] border border-[var(--border)]">
<div className="flex items-center gap-1.5 mb-2">
<input
value={newTagName}
onChange={(e) => setNewTagName(e.target.value)}
onKeyDown={(e) => e.key === "Enter" && handleAddTag()}
placeholder="新标签名称"
className="flex-1 px-2 py-1 text-xs rounded bg-[var(--bg-primary)]
border border-[var(--border)] text-[var(--text-primary)]
placeholder:text-[var(--text-secondary)]
focus:outline-none focus:border-[var(--accent)]"
/>
<div className="flex gap-0.5">
{TAG_COLORS.map((c) => (
<button
key={c}
onClick={() => setNewTagColor(c)}
className="w-4 h-4 rounded-full cursor-pointer transition-transform"
style={{
backgroundColor: c,
transform: newTagColor === c ? "scale(1.3)" : "scale(1)",
boxShadow: newTagColor === c ? `0 0 0 2px var(--bg-tertiary), 0 0 0 3px ${c}` : "none",
}}
/>
))}
</div>
<button
onClick={handleAddTag}
disabled={!newTagName.trim()}
className="px-2 py-1 text-xs rounded bg-[var(--accent)] text-white
disabled:opacity-40 cursor-pointer hover:bg-[var(--accent-hover)]
transition-colors"
>
</button>
</div>
{/* 自定义标签列表(可删除) */}
{tags.filter((t) => !t.builtin).length > 0 && (
<div className="space-y-1 mt-1">
{tags
.filter((t) => !t.builtin)
.map((tag) => (
<div
key={tag.id}
className="flex items-center justify-between px-2 py-0.5 rounded text-xs"
>
<span className="flex items-center gap-1.5">
<span
className="w-2.5 h-2.5 rounded-full"
style={{ backgroundColor: tag.color }}
/>
{tag.name}
</span>
<button
onClick={() => deleteTag(tag.id)}
className="text-[var(--text-secondary)] hover:text-red-400 cursor-pointer"
>
×
</button>
</div>
))}
</div>
)}
</div>
)}
</div>
);
}