@jiangxiaosheng/digital-agent-platform 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +37 -0
- package/eslint.config.mjs +20 -0
- package/messages/en.json +184 -0
- package/messages/zh.json +184 -0
- package/middleware.ts +9 -0
- package/next-env.d.ts +5 -0
- package/next.config.ts +16 -0
- package/package.json +59 -0
- package/postcss.config.mjs +5 -0
- package/src/app/[locale]/agents/[id]/chat/page.tsx +833 -0
- package/src/app/[locale]/agents/[id]/edit/page.tsx +58 -0
- package/src/app/[locale]/agents/new/page.tsx +52 -0
- package/src/app/[locale]/agents/page.tsx +122 -0
- package/src/app/[locale]/knowledge/page.tsx +305 -0
- package/src/app/[locale]/layout.tsx +46 -0
- package/src/app/[locale]/page.tsx +6 -0
- package/src/app/[locale]/settings/page.tsx +1204 -0
- package/src/app/api/agents/[id]/route.ts +27 -0
- package/src/app/api/agents/route.ts +27 -0
- package/src/app/api/auth-config/test/route.ts +69 -0
- package/src/app/api/bash-output/route.ts +57 -0
- package/src/app/api/chat/[agentId]/route.ts +164 -0
- package/src/app/api/env-vars/[key]/route.ts +22 -0
- package/src/app/api/env-vars/route.ts +26 -0
- package/src/app/api/env-vars/scan/route.ts +66 -0
- package/src/app/api/file-serve/route.ts +58 -0
- package/src/app/api/fs/browse/route.ts +47 -0
- package/src/app/api/knowledge/[id]/documents/[docId]/route.ts +22 -0
- package/src/app/api/knowledge/[id]/route.ts +21 -0
- package/src/app/api/knowledge/[id]/upload/route.ts +90 -0
- package/src/app/api/knowledge/route.ts +15 -0
- package/src/app/api/knowledge/search/route.ts +26 -0
- package/src/app/api/models/route.ts +160 -0
- package/src/app/api/models-config/route.ts +41 -0
- package/src/app/api/models-config/test/route.ts +84 -0
- package/src/app/api/sessions/[agentId]/[sessionId]/route.ts +12 -0
- package/src/app/api/sessions/[agentId]/route.ts +19 -0
- package/src/app/api/sessions/history/route.ts +166 -0
- package/src/app/api/settings/route.ts +45 -0
- package/src/app/api/skills/[name]/route.ts +40 -0
- package/src/app/api/skills/install/route.ts +35 -0
- package/src/app/api/skills/route.ts +54 -0
- package/src/app/api/skills/search/route.ts +60 -0
- package/src/app/api/tts/route.ts +235 -0
- package/src/app/api/voice-provider/route.ts +98 -0
- package/src/app/api/workspace-files/route.ts +151 -0
- package/src/app/globals.css +176 -0
- package/src/app/layout.tsx +13 -0
- package/src/app/page.tsx +6 -0
- package/src/components/agents/agent-form.tsx +457 -0
- package/src/components/agents/model-selector.tsx +290 -0
- package/src/components/agents/skills-manager.tsx +347 -0
- package/src/components/chat/input-bar.tsx +561 -0
- package/src/components/chat/message-bubble.tsx +395 -0
- package/src/components/layout/sidebar.tsx +304 -0
- package/src/components/providers.tsx +19 -0
- package/src/components/ui/badge.tsx +26 -0
- package/src/components/ui/button.tsx +43 -0
- package/src/components/ui/card.tsx +46 -0
- package/src/components/ui/dialog.tsx +74 -0
- package/src/components/ui/directory-picker.tsx +185 -0
- package/src/components/ui/input.tsx +18 -0
- package/src/components/ui/label.tsx +16 -0
- package/src/components/ui/scroll-area.tsx +41 -0
- package/src/components/ui/select.tsx +93 -0
- package/src/components/ui/slider.tsx +35 -0
- package/src/components/ui/switch.tsx +35 -0
- package/src/components/ui/tabs.tsx +48 -0
- package/src/components/ui/textarea.tsx +17 -0
- package/src/components/ui/toaster.tsx +50 -0
- package/src/hooks/use-toast.ts +43 -0
- package/src/i18n/request.ts +13 -0
- package/src/i18n/routing.ts +7 -0
- package/src/lib/agent-factory.ts +200 -0
- package/src/lib/agents/store.ts +95 -0
- package/src/lib/db/client.ts +25 -0
- package/src/lib/db/schema.ts +66 -0
- package/src/lib/env-vars.ts +33 -0
- package/src/lib/knowledge/chunker.ts +34 -0
- package/src/lib/knowledge/embedder.ts +48 -0
- package/src/lib/knowledge/retriever.ts +94 -0
- package/src/lib/knowledge/store.ts +74 -0
- package/src/lib/models-config.ts +55 -0
- package/src/lib/settings.ts +33 -0
- package/src/lib/skills.ts +79 -0
- package/src/lib/system-prompt.ts +52 -0
- package/src/lib/tool-builder.ts +19 -0
- package/src/lib/utils.ts +6 -0
- package/src/lib/voice-providers-config.ts +71 -0
- package/src/middleware.ts +9 -0
- package/src/types/agent.ts +124 -0
- package/src/types/knowledge.ts +39 -0
- package/src/types/settings.ts +31 -0
- package/tsconfig.json +40 -0
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect } from "react";
|
|
4
|
+
import Link from "next/link";
|
|
5
|
+
import { usePathname } from "next/navigation";
|
|
6
|
+
import { useTranslations, useLocale } from "next-intl";
|
|
7
|
+
import {
|
|
8
|
+
Bot, BookOpen, Settings, Sun, Moon, Globe, ChevronDown, ChevronRight,
|
|
9
|
+
Folder, File, FileText, FileCode, FolderOpen, X, Copy, Check, RefreshCw,
|
|
10
|
+
} from "lucide-react";
|
|
11
|
+
import { useTheme } from "next-themes";
|
|
12
|
+
import { cn } from "@/lib/utils";
|
|
13
|
+
import { Button } from "@/components/ui/button";
|
|
14
|
+
import { ScrollArea } from "@/components/ui/scroll-area";
|
|
15
|
+
|
|
16
|
+
interface WorkspaceEntry {
|
|
17
|
+
name: string;
|
|
18
|
+
path: string;
|
|
19
|
+
relativePath: string;
|
|
20
|
+
isDir: boolean;
|
|
21
|
+
sizeDisplay?: string;
|
|
22
|
+
icon: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface WorkspaceData {
|
|
26
|
+
currentDisplay: string;
|
|
27
|
+
parentPath: string | null;
|
|
28
|
+
entries: WorkspaceEntry[];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function Sidebar() {
|
|
32
|
+
const t = useTranslations("nav");
|
|
33
|
+
const pathname = usePathname();
|
|
34
|
+
const locale = useLocale();
|
|
35
|
+
const { resolvedTheme, setTheme } = useTheme();
|
|
36
|
+
const [mounted, setMounted] = useState(false);
|
|
37
|
+
const [workspaceOpen, setWorkspaceOpen] = useState(true);
|
|
38
|
+
const [workspaceData, setWorkspaceData] = useState<WorkspaceData | null>(null);
|
|
39
|
+
const [workspaceLoading, setWorkspaceLoading] = useState(false);
|
|
40
|
+
const [currentSubPath, setCurrentSubPath] = useState("");
|
|
41
|
+
const [copiedPath, setCopiedPath] = useState<string | null>(null);
|
|
42
|
+
|
|
43
|
+
useEffect(() => setMounted(true), []);
|
|
44
|
+
const isDark = resolvedTheme === "dark";
|
|
45
|
+
|
|
46
|
+
// Extract agentId from pathname: /zh/agents/:id/...
|
|
47
|
+
const agentId = pathname.match(/\/agents\/([^/]+)/)?.[1];
|
|
48
|
+
|
|
49
|
+
// Fetch workspace files when agentId changes, with 3s polling for real-time updates
|
|
50
|
+
useEffect(() => {
|
|
51
|
+
if (!agentId) {
|
|
52
|
+
setWorkspaceData(null);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
const fetchWorkspace = async (showLoading = false) => {
|
|
56
|
+
if (showLoading) setWorkspaceLoading(true);
|
|
57
|
+
try {
|
|
58
|
+
const url = `/api/workspace-files?agentId=${agentId}${currentSubPath ? `&path=${encodeURIComponent(currentSubPath)}` : ""}`;
|
|
59
|
+
const res = await fetch(url);
|
|
60
|
+
if (res.ok) {
|
|
61
|
+
const data = await res.json() as WorkspaceData;
|
|
62
|
+
setWorkspaceData(data);
|
|
63
|
+
} else {
|
|
64
|
+
setWorkspaceData(null);
|
|
65
|
+
}
|
|
66
|
+
} catch {
|
|
67
|
+
setWorkspaceData(null);
|
|
68
|
+
} finally {
|
|
69
|
+
if (showLoading) setWorkspaceLoading(false);
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
void fetchWorkspace(true);
|
|
73
|
+
const interval = setInterval(() => fetchWorkspace(false), 3000);
|
|
74
|
+
return () => clearInterval(interval);
|
|
75
|
+
}, [agentId, currentSubPath]);
|
|
76
|
+
|
|
77
|
+
const navItems = [
|
|
78
|
+
{
|
|
79
|
+
group: "主菜单",
|
|
80
|
+
items: [
|
|
81
|
+
{ href: `/${locale}/agents`, icon: Bot, label: t("agents"), tooltip: "管理并对话数字人" },
|
|
82
|
+
{ href: `/${locale}/knowledge`, icon: BookOpen, label: t("knowledge"), tooltip: "上传文档,构建知识库" },
|
|
83
|
+
],
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
group: "系统",
|
|
87
|
+
items: [
|
|
88
|
+
{ href: `/${locale}/settings`, icon: Settings, label: t("settings"), tooltip: "配置模型和 API Keys" },
|
|
89
|
+
],
|
|
90
|
+
},
|
|
91
|
+
];
|
|
92
|
+
|
|
93
|
+
function FileIcon({ icon, isDir }: { icon: string; isDir: boolean }) {
|
|
94
|
+
if (isDir) return <Folder className="h-3.5 w-3.5 text-yellow-500 shrink-0" />;
|
|
95
|
+
if (icon === "file-ts" || icon === "file-js") return <FileCode className="h-3.5 w-3.5 text-blue-400 shrink-0" />;
|
|
96
|
+
if (icon === "file-md") return <FileText className="h-3.5 w-3.5 text-zinc-400 shrink-0" />;
|
|
97
|
+
if (icon === "file-json") return <FileCode className="h-3.5 w-3.5 text-yellow-400 shrink-0" />;
|
|
98
|
+
if (icon === "file-py") return <FileCode className="h-3.5 w-3.5 text-green-400 shrink-0" />;
|
|
99
|
+
if (icon === "file-code") return <FileCode className="h-3.5 w-3.5 text-purple-400 shrink-0" />;
|
|
100
|
+
if (icon === "file-img") return <File className="h-3.5 w-3.5 text-pink-400 shrink-0" />;
|
|
101
|
+
return <File className="h-3.5 w-3.5 text-zinc-400 shrink-0" />;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const handleEntryClick = (entry: WorkspaceEntry) => {
|
|
105
|
+
if (entry.isDir) {
|
|
106
|
+
setCurrentSubPath(entry.relativePath);
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const handleGoUp = () => {
|
|
111
|
+
const parts = currentSubPath.split("/").filter(Boolean);
|
|
112
|
+
parts.pop();
|
|
113
|
+
setCurrentSubPath(parts.join("/"));
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
const copyPath = (path: string) => {
|
|
117
|
+
navigator.clipboard.writeText(path).then(() => {
|
|
118
|
+
setCopiedPath(path);
|
|
119
|
+
setTimeout(() => setCopiedPath(null), 1500);
|
|
120
|
+
});
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
return (
|
|
124
|
+
<aside className="flex h-screen w-[var(--sidebar-width)] flex-col border-r border-gray-200 bg-[hsl(var(--neo-bg))] dark:border-slate-700/50 dark:bg-slate-900/95 dark:backdrop-blur-sm">
|
|
125
|
+
{/* Logo */}
|
|
126
|
+
<div className="flex h-14 items-center border-b border-gray-200 px-4 shrink-0 bg-white/50 dark:border-slate-700/50 dark:bg-slate-800/50">
|
|
127
|
+
<div className="flex items-center justify-center w-8 h-8 rounded-xl neo-card dark:bg-gradient-to-br dark:from-cyan-600 dark:to-blue-600 mr-2 dark:shadow-lg">
|
|
128
|
+
<Bot className="h-4 w-4 text-blue-600 dark:text-white" />
|
|
129
|
+
</div>
|
|
130
|
+
<span className="font-bold text-sm text-gray-800 dark:bg-gradient-to-r dark:from-cyan-400 dark:to-blue-400 dark:bg-clip-text dark:text-transparent">Agent Platform</span>
|
|
131
|
+
</div>
|
|
132
|
+
|
|
133
|
+
{/* Navigation — grouped with labels */}
|
|
134
|
+
<nav className="shrink-0 p-2 space-y-3">
|
|
135
|
+
{navItems.map((group) => (
|
|
136
|
+
<div key={group.group}>
|
|
137
|
+
<p className="px-3 mb-1 text-[10px] font-semibold uppercase tracking-widest text-gray-500">
|
|
138
|
+
{group.group}
|
|
139
|
+
</p>
|
|
140
|
+
<div className="space-y-1">
|
|
141
|
+
{group.items.map((item) => {
|
|
142
|
+
const active = pathname.startsWith(item.href);
|
|
143
|
+
return (
|
|
144
|
+
<Link key={item.href} href={item.href} title={item.tooltip}>
|
|
145
|
+
<span className={cn(
|
|
146
|
+
"flex items-center gap-2.5 rounded-xl px-3 py-2 text-sm font-medium transition-all duration-200 border",
|
|
147
|
+
active
|
|
148
|
+
? "neo-card-inset text-blue-600 border-transparent dark:bg-gradient-to-r dark:from-cyan-600/20 dark:to-blue-600/20 dark:text-cyan-400 dark:border-cyan-500/50 dark:shadow-lg dark:shadow-cyan-500/20"
|
|
149
|
+
: "text-gray-600 hover:text-gray-900 border-transparent neo-button dark:text-gray-400 dark:hover:text-white dark:hover:border-slate-700 dark:hover:bg-slate-800 hover:scale-[1.02]",
|
|
150
|
+
)}>
|
|
151
|
+
<item.icon className={cn("h-4 w-4 shrink-0 transition-transform duration-200", active && "text-blue-600 dark:text-cyan-400", !active && "group-hover:scale-110")} />
|
|
152
|
+
<span className="truncate">{item.label}</span>
|
|
153
|
+
{active && <span className="ml-auto w-2 h-2 rounded-full bg-cyan-400 shadow-lg shadow-cyan-400/50 animate-pulse" />}
|
|
154
|
+
</span>
|
|
155
|
+
</Link>
|
|
156
|
+
);
|
|
157
|
+
})}
|
|
158
|
+
</div>
|
|
159
|
+
</div>
|
|
160
|
+
))}
|
|
161
|
+
</nav>
|
|
162
|
+
|
|
163
|
+
{/* Separator */}
|
|
164
|
+
<div className="mx-3 border-t border-slate-700/50" />
|
|
165
|
+
|
|
166
|
+
{/* Workspace File Tree - 科技感设计 */}
|
|
167
|
+
<div className="flex flex-col flex-1 min-h-0 overflow-hidden">
|
|
168
|
+
<div
|
|
169
|
+
onClick={() => setWorkspaceOpen(!workspaceOpen)}
|
|
170
|
+
className="flex items-center gap-2 px-3 py-2 text-[10px] font-semibold uppercase tracking-widest text-gray-500 hover:text-blue-600 dark:hover:text-cyan-400 transition-all duration-200 w-full text-left shrink-0 hover:bg-gray-50 dark:hover:bg-slate-800/50 cursor-pointer"
|
|
171
|
+
>
|
|
172
|
+
{workspaceOpen ? <ChevronDown className="h-3 w-3 transition-transform duration-200" /> : <ChevronRight className="h-3 w-3 transition-transform duration-200" />}
|
|
173
|
+
工作区
|
|
174
|
+
{workspaceLoading && <span className="ml-auto text-[9px] normal-case tracking-normal animate-pulse text-blue-600 dark:text-cyan-400">加载中…</span>}
|
|
175
|
+
{!workspaceLoading && agentId && (
|
|
176
|
+
<button
|
|
177
|
+
type="button"
|
|
178
|
+
onClick={(e) => {
|
|
179
|
+
e.stopPropagation();
|
|
180
|
+
setWorkspaceLoading(true);
|
|
181
|
+
fetch(`/api/workspace-files?agentId=${agentId}${currentSubPath ? `&path=${encodeURIComponent(currentSubPath)}` : ""}`)
|
|
182
|
+
.then(r => r.ok ? r.json() : null)
|
|
183
|
+
.then(data => setWorkspaceData(data))
|
|
184
|
+
.catch(() => {})
|
|
185
|
+
.finally(() => setWorkspaceLoading(false));
|
|
186
|
+
}}
|
|
187
|
+
title="刷新工作区"
|
|
188
|
+
className="ml-auto p-0.5 rounded-lg neo-button hover:text-blue-600 dark:hover:bg-cyan-500/20 text-gray-500 dark:text-gray-500 dark:hover:text-cyan-400 transition-all duration-200 normal-case hover:scale-110 dark:border-transparent dark:hover:border-cyan-500/50"
|
|
189
|
+
>
|
|
190
|
+
<RefreshCw className="h-3 w-3" />
|
|
191
|
+
</button>
|
|
192
|
+
)}
|
|
193
|
+
</div>
|
|
194
|
+
|
|
195
|
+
{workspaceOpen && (
|
|
196
|
+
<div className="flex flex-col flex-1 min-h-0">
|
|
197
|
+
{!agentId ? (
|
|
198
|
+
<p className="px-4 text-[11px] text-muted-foreground/50 italic">进入数字人对话后显示工作区</p>
|
|
199
|
+
) : !workspaceData ? (
|
|
200
|
+
<p className="px-4 text-[11px] text-muted-foreground/50 italic">
|
|
201
|
+
{workspaceLoading ? "加载中…" : "工作区为空或无法访问"}
|
|
202
|
+
</p>
|
|
203
|
+
) : (
|
|
204
|
+
<>
|
|
205
|
+
{/* 当前路径导航 */}
|
|
206
|
+
<div className="px-3 pb-1 flex items-center gap-1 shrink-0">
|
|
207
|
+
{currentSubPath && (
|
|
208
|
+
<button type="button" onClick={handleGoUp}
|
|
209
|
+
title="返回上级目录"
|
|
210
|
+
className="p-0.5 rounded-lg neo-button text-gray-500 hover:text-blue-600 dark:hover:bg-cyan-500/20 dark:text-gray-400 dark:hover:text-cyan-400 transition-all duration-200 shrink-0 dark:border-transparent dark:hover:border-cyan-500/50">
|
|
211
|
+
<ChevronRight className="h-3 w-3 rotate-180" />
|
|
212
|
+
</button>
|
|
213
|
+
)}
|
|
214
|
+
<span className="text-[10px] text-gray-500 font-mono truncate" title={workspaceData.currentDisplay}>
|
|
215
|
+
{currentSubPath
|
|
216
|
+
? `…/${currentSubPath.split("/").slice(-2).join("/")}`
|
|
217
|
+
: workspaceData.currentDisplay.split("/").slice(-2).join("/")}
|
|
218
|
+
</span>
|
|
219
|
+
{currentSubPath && (
|
|
220
|
+
<button type="button" onClick={() => setCurrentSubPath("")}
|
|
221
|
+
title="回到工作区根目录"
|
|
222
|
+
className="ml-auto p-0.5 rounded-lg neo-button text-gray-700 hover:text-red-600 dark:hover:bg-red-500/20 dark:text-gray-400 dark:hover:text-red-400 transition-all duration-200 shrink-0 dark:border-transparent dark:hover:border-red-500/50">
|
|
223
|
+
<X className="h-3 w-3" />
|
|
224
|
+
</button>
|
|
225
|
+
)}
|
|
226
|
+
</div>
|
|
227
|
+
|
|
228
|
+
{/* 文件列表 */}
|
|
229
|
+
<ScrollArea className="flex-1">
|
|
230
|
+
<div className="px-2 pb-2 space-y-0.5">
|
|
231
|
+
{workspaceData.entries.length === 0 ? (
|
|
232
|
+
<p className="px-2 py-2 text-[11px] text-gray-500 italic">目录为空</p>
|
|
233
|
+
) : (
|
|
234
|
+
workspaceData.entries.map((entry) => (
|
|
235
|
+
<div
|
|
236
|
+
key={entry.relativePath}
|
|
237
|
+
className="group relative flex items-center rounded-xl transition-all duration-200 neo-button hover:bg-gray-100 dark:hover:bg-slate-800/50 border-transparent dark:hover:border-slate-700"
|
|
238
|
+
>
|
|
239
|
+
<button
|
|
240
|
+
type="button"
|
|
241
|
+
onClick={() => handleEntryClick(entry)}
|
|
242
|
+
title={entry.isDir ? `打开 ${entry.name}/` : entry.path}
|
|
243
|
+
className={cn(
|
|
244
|
+
"flex items-center gap-2 px-2 py-1.5 text-left flex-1 min-w-0",
|
|
245
|
+
entry.isDir ? "cursor-pointer" : "cursor-default",
|
|
246
|
+
)}
|
|
247
|
+
>
|
|
248
|
+
{entry.isDir
|
|
249
|
+
? <FolderOpen className="h-3.5 w-3.5 text-yellow-500 shrink-0" />
|
|
250
|
+
: <FileIcon icon={entry.icon} isDir={false} />
|
|
251
|
+
}
|
|
252
|
+
<span className="text-[12px] truncate flex-1 text-gray-700 dark:text-gray-300 group-hover:text-gray-900 dark:group-hover:text-white transition-colors">{entry.name}</span>
|
|
253
|
+
{entry.sizeDisplay && !entry.isDir && (
|
|
254
|
+
<span className="text-[10px] text-gray-500 shrink-0 font-mono mr-1">{entry.sizeDisplay}</span>
|
|
255
|
+
)}
|
|
256
|
+
{entry.isDir && <ChevronRight className="h-3 w-3 text-gray-400 dark:text-gray-500 shrink-0 mr-1" />}
|
|
257
|
+
</button>
|
|
258
|
+
|
|
259
|
+
<button
|
|
260
|
+
type="button"
|
|
261
|
+
onClick={(e) => { e.stopPropagation(); copyPath(entry.path); }}
|
|
262
|
+
title={`复制路径:${entry.path}`}
|
|
263
|
+
className="absolute right-1 opacity-0 group-hover:opacity-100 transition-all duration-200 p-1 rounded-lg neo-button hover:text-blue-600 dark:hover:bg-cyan-500/20 text-gray-500 dark:hover:text-cyan-400 border-transparent dark:hover:border-cyan-500/50"
|
|
264
|
+
>
|
|
265
|
+
{copiedPath === entry.path
|
|
266
|
+
? <Check className="h-3 w-3 text-blue-600 dark:text-cyan-400" />
|
|
267
|
+
: <Copy className="h-3 w-3" />
|
|
268
|
+
}
|
|
269
|
+
</button>
|
|
270
|
+
</div>
|
|
271
|
+
))
|
|
272
|
+
)}
|
|
273
|
+
</div>
|
|
274
|
+
</ScrollArea>
|
|
275
|
+
</>
|
|
276
|
+
)}
|
|
277
|
+
</div>
|
|
278
|
+
)}
|
|
279
|
+
</div>
|
|
280
|
+
|
|
281
|
+
{/* Footer */}
|
|
282
|
+
<div className="flex items-center gap-1 border-t border-gray-200 dark:border-slate-700/50 p-2 shrink-0 bg-white/50 dark:bg-slate-800/30">
|
|
283
|
+
<button
|
|
284
|
+
onClick={() => setTheme(isDark ? "light" : "dark")}
|
|
285
|
+
title={mounted ? (isDark ? "切换浅色模式" : "切换深色模式") : "切换主题"}
|
|
286
|
+
className="h-9 w-9 rounded-xl neo-button hover:text-blue-600 dark:hover:bg-cyan-500/20 text-gray-500 dark:text-gray-400 dark:hover:text-cyan-400 flex items-center justify-center transition-all duration-200 hover:scale-110 dark:border-transparent dark:hover:border-cyan-500/50"
|
|
287
|
+
suppressHydrationWarning
|
|
288
|
+
>
|
|
289
|
+
{mounted
|
|
290
|
+
? (isDark ? <Sun className="h-4 w-4 transition-transform duration-200 hover:rotate-180" /> : <Moon className="h-4 w-4 transition-transform duration-200 hover:rotate-12" />)
|
|
291
|
+
: <Moon className="h-4 w-4" />}
|
|
292
|
+
</button>
|
|
293
|
+
<Link href={`/${locale === "zh" ? "en" : "zh"}${pathname.replace(/^\/(zh|en)/, "")}`}>
|
|
294
|
+
<button
|
|
295
|
+
title={locale === "zh" ? "Switch to English" : "切换为中文"}
|
|
296
|
+
className="h-9 w-9 rounded-xl neo-button hover:text-purple-600 dark:hover:bg-purple-500/20 text-gray-500 dark:text-gray-400 dark:hover:text-purple-400 flex items-center justify-center transition-all duration-200 hover:scale-110 dark:border-transparent dark:hover:border-purple-500/50"
|
|
297
|
+
>
|
|
298
|
+
<Globe className="h-4 w-4 transition-transform duration-200 hover:rotate-12" />
|
|
299
|
+
</button>
|
|
300
|
+
</Link>
|
|
301
|
+
</div>
|
|
302
|
+
</aside>
|
|
303
|
+
);
|
|
304
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { ThemeProvider as NextThemesProvider } from "next-themes";
|
|
5
|
+
import { Toaster } from "@/components/ui/toaster";
|
|
6
|
+
|
|
7
|
+
export function Providers({ children }: { children: React.ReactNode }) {
|
|
8
|
+
return (
|
|
9
|
+
<NextThemesProvider
|
|
10
|
+
attribute="class"
|
|
11
|
+
defaultTheme="system"
|
|
12
|
+
enableSystem
|
|
13
|
+
disableTransitionOnChange
|
|
14
|
+
>
|
|
15
|
+
{children}
|
|
16
|
+
<Toaster />
|
|
17
|
+
</NextThemesProvider>
|
|
18
|
+
);
|
|
19
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
3
|
+
import { cn } from "@/lib/utils";
|
|
4
|
+
|
|
5
|
+
const badgeVariants = cva(
|
|
6
|
+
"inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring",
|
|
7
|
+
{
|
|
8
|
+
variants: {
|
|
9
|
+
variant: {
|
|
10
|
+
default: "border-transparent bg-primary text-primary-foreground",
|
|
11
|
+
secondary: "border-transparent bg-secondary text-secondary-foreground",
|
|
12
|
+
destructive: "border-transparent bg-destructive text-destructive-foreground",
|
|
13
|
+
outline: "text-foreground",
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
defaultVariants: { variant: "default" },
|
|
17
|
+
}
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
export interface BadgeProps extends React.HTMLAttributes<HTMLDivElement>, VariantProps<typeof badgeVariants> {}
|
|
21
|
+
|
|
22
|
+
function Badge({ className, variant, ...props }: BadgeProps) {
|
|
23
|
+
return <div className={cn(badgeVariants({ variant }), className)} {...props} />;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export { Badge, badgeVariants };
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { Slot } from "@radix-ui/react-slot";
|
|
3
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
4
|
+
import { cn } from "@/lib/utils";
|
|
5
|
+
|
|
6
|
+
const buttonVariants = cva(
|
|
7
|
+
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50",
|
|
8
|
+
{
|
|
9
|
+
variants: {
|
|
10
|
+
variant: {
|
|
11
|
+
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
|
12
|
+
destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
|
|
13
|
+
outline: "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
|
|
14
|
+
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
|
15
|
+
ghost: "hover:bg-accent hover:text-accent-foreground",
|
|
16
|
+
link: "text-primary underline-offset-4 hover:underline",
|
|
17
|
+
},
|
|
18
|
+
size: {
|
|
19
|
+
default: "h-9 px-4 py-2",
|
|
20
|
+
sm: "h-8 rounded-md px-3 text-xs",
|
|
21
|
+
lg: "h-10 rounded-md px-8",
|
|
22
|
+
icon: "h-9 w-9",
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
defaultVariants: { variant: "default", size: "default" },
|
|
26
|
+
}
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
export interface ButtonProps
|
|
30
|
+
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
|
31
|
+
VariantProps<typeof buttonVariants> {
|
|
32
|
+
asChild?: boolean;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
|
36
|
+
({ className, variant, size, asChild = false, ...props }, ref) => {
|
|
37
|
+
const Comp = asChild ? Slot : "button";
|
|
38
|
+
return <Comp className={cn(buttonVariants({ variant, size, className }))} ref={ref} {...props} />;
|
|
39
|
+
}
|
|
40
|
+
);
|
|
41
|
+
Button.displayName = "Button";
|
|
42
|
+
|
|
43
|
+
export { Button, buttonVariants };
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { cn } from "@/lib/utils";
|
|
3
|
+
|
|
4
|
+
const Card = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
|
5
|
+
({ className, ...props }, ref) => (
|
|
6
|
+
<div ref={ref} className={cn("rounded-lg border bg-card text-card-foreground shadow-sm", className)} {...props} />
|
|
7
|
+
)
|
|
8
|
+
);
|
|
9
|
+
Card.displayName = "Card";
|
|
10
|
+
|
|
11
|
+
const CardHeader = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
|
12
|
+
({ className, ...props }, ref) => (
|
|
13
|
+
<div ref={ref} className={cn("flex flex-col space-y-1.5 p-6", className)} {...props} />
|
|
14
|
+
)
|
|
15
|
+
);
|
|
16
|
+
CardHeader.displayName = "CardHeader";
|
|
17
|
+
|
|
18
|
+
const CardTitle = React.forwardRef<HTMLHeadingElement, React.HTMLAttributes<HTMLHeadingElement>>(
|
|
19
|
+
({ className, ...props }, ref) => (
|
|
20
|
+
<h3 ref={ref} className={cn("text-lg font-semibold leading-none tracking-tight", className)} {...props} />
|
|
21
|
+
)
|
|
22
|
+
);
|
|
23
|
+
CardTitle.displayName = "CardTitle";
|
|
24
|
+
|
|
25
|
+
const CardDescription = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLParagraphElement>>(
|
|
26
|
+
({ className, ...props }, ref) => (
|
|
27
|
+
<p ref={ref} className={cn("text-sm text-muted-foreground", className)} {...props} />
|
|
28
|
+
)
|
|
29
|
+
);
|
|
30
|
+
CardDescription.displayName = "CardDescription";
|
|
31
|
+
|
|
32
|
+
const CardContent = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
|
33
|
+
({ className, ...props }, ref) => (
|
|
34
|
+
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
|
|
35
|
+
)
|
|
36
|
+
);
|
|
37
|
+
CardContent.displayName = "CardContent";
|
|
38
|
+
|
|
39
|
+
const CardFooter = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
|
40
|
+
({ className, ...props }, ref) => (
|
|
41
|
+
<div ref={ref} className={cn("flex items-center p-6 pt-0", className)} {...props} />
|
|
42
|
+
)
|
|
43
|
+
);
|
|
44
|
+
CardFooter.displayName = "CardFooter";
|
|
45
|
+
|
|
46
|
+
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent };
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import * as DialogPrimitive from "@radix-ui/react-dialog";
|
|
5
|
+
import { X } from "lucide-react";
|
|
6
|
+
import { cn } from "@/lib/utils";
|
|
7
|
+
|
|
8
|
+
const Dialog = DialogPrimitive.Root;
|
|
9
|
+
const DialogTrigger = DialogPrimitive.Trigger;
|
|
10
|
+
const DialogPortal = DialogPrimitive.Portal;
|
|
11
|
+
const DialogClose = DialogPrimitive.Close;
|
|
12
|
+
|
|
13
|
+
const DialogOverlay = React.forwardRef<
|
|
14
|
+
React.ElementRef<typeof DialogPrimitive.Overlay>,
|
|
15
|
+
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
|
|
16
|
+
>(({ className, ...props }, ref) => (
|
|
17
|
+
<DialogPrimitive.Overlay
|
|
18
|
+
ref={ref}
|
|
19
|
+
className={cn("fixed inset-0 z-50 bg-black/60 backdrop-blur-sm data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0", className)}
|
|
20
|
+
{...props}
|
|
21
|
+
/>
|
|
22
|
+
));
|
|
23
|
+
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
|
|
24
|
+
|
|
25
|
+
const DialogContent = React.forwardRef<
|
|
26
|
+
React.ElementRef<typeof DialogPrimitive.Content>,
|
|
27
|
+
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
|
|
28
|
+
>(({ className, children, ...props }, ref) => (
|
|
29
|
+
<DialogPortal>
|
|
30
|
+
<DialogOverlay />
|
|
31
|
+
<DialogPrimitive.Content
|
|
32
|
+
ref={ref}
|
|
33
|
+
className={cn(
|
|
34
|
+
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 rounded-lg",
|
|
35
|
+
className
|
|
36
|
+
)}
|
|
37
|
+
{...props}
|
|
38
|
+
>
|
|
39
|
+
{children}
|
|
40
|
+
<DialogClose className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring disabled:pointer-events-none">
|
|
41
|
+
<X className="h-4 w-4" />
|
|
42
|
+
</DialogClose>
|
|
43
|
+
</DialogPrimitive.Content>
|
|
44
|
+
</DialogPortal>
|
|
45
|
+
));
|
|
46
|
+
DialogContent.displayName = DialogPrimitive.Content.displayName;
|
|
47
|
+
|
|
48
|
+
const DialogHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
|
|
49
|
+
<div className={cn("flex flex-col space-y-1.5 text-center sm:text-left", className)} {...props} />
|
|
50
|
+
);
|
|
51
|
+
DialogHeader.displayName = "DialogHeader";
|
|
52
|
+
|
|
53
|
+
const DialogFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
|
|
54
|
+
<div className={cn("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", className)} {...props} />
|
|
55
|
+
);
|
|
56
|
+
DialogFooter.displayName = "DialogFooter";
|
|
57
|
+
|
|
58
|
+
const DialogTitle = React.forwardRef<
|
|
59
|
+
React.ElementRef<typeof DialogPrimitive.Title>,
|
|
60
|
+
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
|
|
61
|
+
>(({ className, ...props }, ref) => (
|
|
62
|
+
<DialogPrimitive.Title ref={ref} className={cn("text-lg font-semibold leading-none tracking-tight", className)} {...props} />
|
|
63
|
+
));
|
|
64
|
+
DialogTitle.displayName = DialogPrimitive.Title.displayName;
|
|
65
|
+
|
|
66
|
+
const DialogDescription = React.forwardRef<
|
|
67
|
+
React.ElementRef<typeof DialogPrimitive.Description>,
|
|
68
|
+
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
|
|
69
|
+
>(({ className, ...props }, ref) => (
|
|
70
|
+
<DialogPrimitive.Description ref={ref} className={cn("text-sm text-muted-foreground", className)} {...props} />
|
|
71
|
+
));
|
|
72
|
+
DialogDescription.displayName = DialogPrimitive.Description.displayName;
|
|
73
|
+
|
|
74
|
+
export { Dialog, DialogPortal, DialogOverlay, DialogClose, DialogTrigger, DialogContent, DialogHeader, DialogFooter, DialogTitle, DialogDescription };
|