fix: source list scroll, citation targeting, and page UI polish
- Replace ScrollArea with native overflow-y-auto for reliable mouse wheel scrolling - Scope source DOM IDs by message ID to fix cross-round citation jumps - Improve profile, settings, forum, knowledge page layouts - Update mobile navigation and chat interface Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -76,7 +76,7 @@ export default function LoginPage() {
|
|||||||
href="/"
|
href="/"
|
||||||
className="flex items-center justify-center space-x-3 mb-6 group cursor-pointer transition-opacity hover:opacity-80"
|
className="flex items-center justify-center space-x-3 mb-6 group cursor-pointer transition-opacity hover:opacity-80"
|
||||||
>
|
>
|
||||||
<div className="w-12 h-12 bg-gradient-to-r from-blue-500 to-purple-600 rounded-xl flex items-center justify-center shadow-lg group-hover:shadow-xl transition-shadow">
|
<div className="w-12 h-12 bg-primary rounded-xl flex items-center justify-center shadow-md group-hover:shadow-lg transition-shadow">
|
||||||
<BookOpen className="w-7 h-7 text-white" />
|
<BookOpen className="w-7 h-7 text-white" />
|
||||||
</div>
|
</div>
|
||||||
<span className="text-2xl font-bold">
|
<span className="text-2xl font-bold">
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ export default function RegisterPage() {
|
|||||||
href="/"
|
href="/"
|
||||||
className="flex items-center justify-center space-x-3 mb-6 group cursor-pointer transition-opacity hover:opacity-80"
|
className="flex items-center justify-center space-x-3 mb-6 group cursor-pointer transition-opacity hover:opacity-80"
|
||||||
>
|
>
|
||||||
<div className="w-12 h-12 bg-gradient-to-r from-blue-500 to-purple-600 rounded-xl flex items-center justify-center shadow-lg group-hover:shadow-xl transition-shadow">
|
<div className="w-12 h-12 bg-primary rounded-xl flex items-center justify-center shadow-md group-hover:shadow-lg transition-shadow">
|
||||||
<BookOpen className="w-7 h-7 text-white" />
|
<BookOpen className="w-7 h-7 text-white" />
|
||||||
</div>
|
</div>
|
||||||
<span className="text-2xl font-bold">
|
<span className="text-2xl font-bold">
|
||||||
|
|||||||
@@ -193,7 +193,7 @@ export default function ForumAdminPage() {
|
|||||||
</button>
|
</button>
|
||||||
{confirmDelete?.type === "cat" && confirmDelete.id === cat.id ? (
|
{confirmDelete?.type === "cat" && confirmDelete.id === cat.id ? (
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
<button onClick={() => handleDeleteCategory(cat.id)} className="text-xs px-1.5 py-0.5 bg-red-600 text-white rounded">确认</button>
|
<button onClick={() => handleDeleteCategory(cat.id)} className="text-xs px-1.5 py-0.5 bg-destructive text-destructive-foreground rounded">确认</button>
|
||||||
<button onClick={() => setConfirmDelete(null)} className="text-xs px-1.5 py-0.5 border rounded">取消</button>
|
<button onClick={() => setConfirmDelete(null)} className="text-xs px-1.5 py-0.5 border rounded">取消</button>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -91,8 +91,8 @@ export default function KnowledgeAdminPage() {
|
|||||||
<span
|
<span
|
||||||
className={`inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium ${
|
className={`inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium ${
|
||||||
kb.is_system
|
kb.is_system
|
||||||
? "bg-blue-100 text-blue-700"
|
? "bg-primary/10 text-primary"
|
||||||
: "bg-slate-100 text-slate-600"
|
: "bg-muted text-muted-foreground"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{kb.is_system ? "系统" : "用户"}
|
{kb.is_system ? "系统" : "用户"}
|
||||||
@@ -108,7 +108,7 @@ export default function KnowledgeAdminPage() {
|
|||||||
<div className="flex items-center justify-center gap-1">
|
<div className="flex items-center justify-center gap-1">
|
||||||
<button
|
<button
|
||||||
onClick={() => handleDelete(kb.id)}
|
onClick={() => handleDelete(kb.id)}
|
||||||
className="text-xs px-2 py-1 bg-red-600 text-white rounded hover:bg-red-700"
|
className="text-xs px-2 py-1 bg-destructive text-destructive-foreground rounded hover:bg-destructive/80"
|
||||||
>
|
>
|
||||||
确认
|
确认
|
||||||
</button>
|
</button>
|
||||||
@@ -122,7 +122,7 @@ export default function KnowledgeAdminPage() {
|
|||||||
) : (
|
) : (
|
||||||
<button
|
<button
|
||||||
onClick={() => setConfirmDelete(kb.id)}
|
onClick={() => setConfirmDelete(kb.id)}
|
||||||
className="text-red-500 hover:text-red-700 transition-colors"
|
className="text-destructive hover:text-destructive/80 transition-colors"
|
||||||
>
|
>
|
||||||
<Trash2 className="w-4 h-4" />
|
<Trash2 className="w-4 h-4" />
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -16,12 +16,13 @@ interface DashboardStats {
|
|||||||
total_generated_images: number;
|
total_generated_images: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
function TrendChart({ title, icon: Icon, data, color }: {
|
function TrendChart({ title, icon: Icon, data, variant }: {
|
||||||
title: string;
|
title: string;
|
||||||
icon: React.ElementType;
|
icon: React.ElementType;
|
||||||
data: { date: string; count: number }[];
|
data: { date: string; count: number }[];
|
||||||
color: string;
|
variant?: "default" | "secondary";
|
||||||
}) {
|
}) {
|
||||||
|
const barClass = variant === "secondary" ? "bg-emerald-500/35" : "bg-primary/35";
|
||||||
return (
|
return (
|
||||||
<div className="rounded-xl border p-5 space-y-3">
|
<div className="rounded-xl border p-5 space-y-3">
|
||||||
<h3 className="text-sm font-medium flex items-center gap-2">
|
<h3 className="text-sm font-medium flex items-center gap-2">
|
||||||
@@ -36,7 +37,7 @@ function TrendChart({ title, icon: Icon, data, color }: {
|
|||||||
return (
|
return (
|
||||||
<div key={d.date} className="flex-1 flex flex-col items-center gap-1">
|
<div key={d.date} className="flex-1 flex flex-col items-center gap-1">
|
||||||
<span className="text-[9px] text-muted-foreground tabular-nums">{d.count || ""}</span>
|
<span className="text-[9px] text-muted-foreground tabular-nums">{d.count || ""}</span>
|
||||||
<div className="w-full rounded-t" style={{ height: `${h}%`, backgroundColor: color, opacity: 0.35 }} title={`${d.date}: ${d.count}`} />
|
<div className={`w-full rounded-t ${barClass}`} style={{ height: `${h}%` }} title={`${d.date}: ${d.count}`} />
|
||||||
<span className="text-[9px] text-muted-foreground">{d.date.slice(5)}</span>
|
<span className="text-[9px] text-muted-foreground">{d.date.slice(5)}</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -101,8 +102,8 @@ export default function AdminPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid md:grid-cols-2 gap-6">
|
<div className="grid md:grid-cols-2 gap-6">
|
||||||
<TrendChart title="近14天用户注册" icon={Users} data={userTrends} color="#2563eb" />
|
<TrendChart title="近14天用户注册" icon={Users} data={userTrends} variant="default" />
|
||||||
<TrendChart title="近14天消息数" icon={MessageSquare} data={msgTrends} color="#16a34a" />
|
<TrendChart title="近14天消息数" icon={MessageSquare} data={msgTrends} variant="secondary" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{systemStatus && (
|
{systemStatus && (
|
||||||
@@ -118,7 +119,7 @@ export default function AdminPage() {
|
|||||||
{ label: "嵌入模型", ok: true, sub: systemStatus.embedding_model },
|
{ label: "嵌入模型", ok: true, sub: systemStatus.embedding_model },
|
||||||
].map((s) => (
|
].map((s) => (
|
||||||
<div key={s.label} className="flex items-center gap-2 text-sm">
|
<div key={s.label} className="flex items-center gap-2 text-sm">
|
||||||
<span className="w-2 h-2 rounded-full shrink-0" style={{ backgroundColor: s.ok ? "#22c55e" : "#ef4444" }} />
|
<span className={`w-2 h-2 rounded-full shrink-0 ${s.ok ? "bg-emerald-500" : "bg-destructive"}`} />
|
||||||
<span>{s.label}</span>
|
<span>{s.label}</span>
|
||||||
{s.sub && <span className="text-muted-foreground text-xs truncate">{s.sub}</span>}
|
{s.sub && <span className="text-muted-foreground text-xs truncate">{s.sub}</span>}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -77,10 +77,7 @@ export default function SystemSettingsPage() {
|
|||||||
{statusItems.map((s) => (
|
{statusItems.map((s) => (
|
||||||
<div key={s.label} className="flex items-center justify-between px-5 py-3">
|
<div key={s.label} className="flex items-center justify-between px-5 py-3">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<span
|
<span className={`w-2.5 h-2.5 rounded-full shrink-0 ${s.ok ? "bg-emerald-500" : "bg-destructive"}`} />
|
||||||
className="w-2.5 h-2.5 rounded-full shrink-0"
|
|
||||||
style={{ backgroundColor: s.ok ? "#22c55e" : "#ef4444" }}
|
|
||||||
/>
|
|
||||||
<span className="text-sm">{s.label}</span>
|
<span className="text-sm">{s.label}</span>
|
||||||
</div>
|
</div>
|
||||||
<span className="text-sm text-muted-foreground">{s.detail}</span>
|
<span className="text-sm text-muted-foreground">{s.detail}</span>
|
||||||
@@ -115,9 +112,9 @@ export default function SystemSettingsPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Danger zone */}
|
{/* Danger zone */}
|
||||||
<div className="rounded-xl border border-red-200">
|
<div className="rounded-xl border border-destructive/30">
|
||||||
<div className="px-5 py-3 border-b border-red-200">
|
<div className="px-5 py-3 border-b border-destructive/30">
|
||||||
<h3 className="text-sm font-medium text-red-600">危险操作</h3>
|
<h3 className="text-sm font-medium text-destructive">危险操作</h3>
|
||||||
</div>
|
</div>
|
||||||
<div className="px-5 py-4 space-y-3">
|
<div className="px-5 py-4 space-y-3">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
@@ -126,7 +123,7 @@ export default function SystemSettingsPage() {
|
|||||||
<div className="text-xs text-muted-foreground">重新处理所有知识库文档并建立向量索引</div>
|
<div className="text-xs text-muted-foreground">重新处理所有知识库文档并建立向量索引</div>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
className="px-3 py-1.5 text-sm border border-red-200 text-red-600 rounded-lg hover:bg-red-50 transition-colors"
|
className="px-3 py-1.5 text-sm border border-destructive/30 text-destructive rounded-lg hover:bg-destructive/10 transition-colors"
|
||||||
onClick={() => alert("此功能需要通过后端命令行执行")}
|
onClick={() => alert("此功能需要通过后端命令行执行")}
|
||||||
>
|
>
|
||||||
重建索引
|
重建索引
|
||||||
@@ -138,7 +135,7 @@ export default function SystemSettingsPage() {
|
|||||||
<div className="text-xs text-muted-foreground">清除系统缓存和临时文件</div>
|
<div className="text-xs text-muted-foreground">清除系统缓存和临时文件</div>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
className="px-3 py-1.5 text-sm border border-red-200 text-red-600 rounded-lg hover:bg-red-50 transition-colors"
|
className="px-3 py-1.5 text-sm border border-destructive/30 text-destructive rounded-lg hover:bg-destructive/10 transition-colors"
|
||||||
onClick={() => alert("此功能需要通过后端命令行执行")}
|
onClick={() => alert("此功能需要通过后端命令行执行")}
|
||||||
>
|
>
|
||||||
清理
|
清理
|
||||||
|
|||||||
@@ -154,7 +154,7 @@ export default function UsersPage() {
|
|||||||
<div className="flex items-center justify-center gap-1">
|
<div className="flex items-center justify-center gap-1">
|
||||||
<button
|
<button
|
||||||
onClick={() => handleDelete(u.id)}
|
onClick={() => handleDelete(u.id)}
|
||||||
className="text-xs px-2 py-1 bg-red-600 text-white rounded hover:bg-red-700"
|
className="text-xs px-2 py-1 bg-destructive text-destructive-foreground rounded hover:bg-destructive/80"
|
||||||
>
|
>
|
||||||
确认
|
确认
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -188,7 +188,7 @@ export default function AnalyticsPage() {
|
|||||||
<Card>
|
<Card>
|
||||||
<CardContent className="p-6">
|
<CardContent className="p-6">
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<MessageSquare className="w-8 h-8 text-blue-600" />
|
<MessageSquare className="w-8 h-8 text-primary" />
|
||||||
<div className="ml-4">
|
<div className="ml-4">
|
||||||
<p className="text-sm font-medium text-muted-foreground">对话会话</p>
|
<p className="text-sm font-medium text-muted-foreground">对话会话</p>
|
||||||
<p className="text-2xl font-bold text-foreground">{statistics.total_sessions}</p>
|
<p className="text-2xl font-bold text-foreground">{statistics.total_sessions}</p>
|
||||||
@@ -200,7 +200,7 @@ export default function AnalyticsPage() {
|
|||||||
<Card>
|
<Card>
|
||||||
<CardContent className="p-6">
|
<CardContent className="p-6">
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<BookOpen className="w-8 h-8 text-green-600" />
|
<BookOpen className="w-8 h-8 text-emerald-500" />
|
||||||
<div className="ml-4">
|
<div className="ml-4">
|
||||||
<p className="text-sm font-medium text-muted-foreground">消息总数</p>
|
<p className="text-sm font-medium text-muted-foreground">消息总数</p>
|
||||||
<p className="text-2xl font-bold text-foreground">{statistics.total_messages}</p>
|
<p className="text-2xl font-bold text-foreground">{statistics.total_messages}</p>
|
||||||
@@ -212,7 +212,7 @@ export default function AnalyticsPage() {
|
|||||||
<Card>
|
<Card>
|
||||||
<CardContent className="p-6">
|
<CardContent className="p-6">
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<FileText className="w-8 h-8 text-purple-600" />
|
<FileText className="w-8 h-8 text-primary" />
|
||||||
<div className="ml-4">
|
<div className="ml-4">
|
||||||
<p className="text-sm font-medium text-muted-foreground">文档数量</p>
|
<p className="text-sm font-medium text-muted-foreground">文档数量</p>
|
||||||
<p className="text-2xl font-bold text-foreground">{statistics.total_documents}</p>
|
<p className="text-2xl font-bold text-foreground">{statistics.total_documents}</p>
|
||||||
@@ -224,7 +224,7 @@ export default function AnalyticsPage() {
|
|||||||
<Card>
|
<Card>
|
||||||
<CardContent className="p-6">
|
<CardContent className="p-6">
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<Calendar className="w-8 h-8 text-orange-600" />
|
<Calendar className="w-8 h-8 text-muted-foreground" />
|
||||||
<div className="ml-4">
|
<div className="ml-4">
|
||||||
<p className="text-sm font-medium text-muted-foreground">活跃天数</p>
|
<p className="text-sm font-medium text-muted-foreground">活跃天数</p>
|
||||||
<p className="text-2xl font-bold text-foreground">{statistics.active_days}</p>
|
<p className="text-2xl font-bold text-foreground">{statistics.active_days}</p>
|
||||||
@@ -250,7 +250,7 @@ export default function AnalyticsPage() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="w-full bg-muted rounded-full h-2">
|
<div className="w-full bg-muted rounded-full h-2">
|
||||||
<div
|
<div
|
||||||
className="bg-blue-600 h-2 rounded-full transition-all duration-300"
|
className="bg-primary h-2 rounded-full transition-all duration-300"
|
||||||
style={{ width: `${item.coverage}%` }}
|
style={{ width: `${item.coverage}%` }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -278,7 +278,7 @@ export default function AnalyticsPage() {
|
|||||||
<p className="text-sm font-medium text-foreground">{item.question}</p>
|
<p className="text-sm font-medium text-foreground">{item.question}</p>
|
||||||
<p className="text-xs text-muted-foreground">{item.category}</p>
|
<p className="text-xs text-muted-foreground">{item.category}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-sm font-medium text-blue-600">{item.count} 次</div>
|
<div className="text-sm font-medium text-primary">{item.count} 次</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -300,11 +300,11 @@ export default function AnalyticsPage() {
|
|||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<span className="text-lg font-medium text-foreground">总体进度</span>
|
<span className="text-lg font-medium text-foreground">总体进度</span>
|
||||||
<span className="text-2xl font-bold text-blue-600">{learningReport.learning_progress}%</span>
|
<span className="text-2xl font-bold text-primary">{learningReport.learning_progress}%</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-full bg-muted rounded-full h-4">
|
<div className="w-full bg-muted rounded-full h-4">
|
||||||
<div
|
<div
|
||||||
className="bg-gradient-to-r from-blue-500 to-purple-600 h-4 rounded-full transition-all duration-500"
|
className="bg-primary h-4 rounded-full transition-all duration-500"
|
||||||
style={{ width: `${learningReport.learning_progress}%` }}
|
style={{ width: `${learningReport.learning_progress}%` }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -334,8 +334,8 @@ export default function AnalyticsPage() {
|
|||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
{learningReport.recommendations.map((recommendation, index) => (
|
{learningReport.recommendations.map((recommendation, index) => (
|
||||||
<div key={index} className="flex items-start space-x-3 p-3 bg-blue-500/10 dark:bg-blue-500/20 rounded-lg">
|
<div key={index} className="flex items-start space-x-3 p-3 bg-primary/10 dark:bg-blue-500/20 rounded-lg">
|
||||||
<div className="w-2 h-2 bg-blue-600 rounded-full mt-2 flex-shrink-0" />
|
<div className="w-2 h-2 bg-primary rounded-full mt-2 flex-shrink-0" />
|
||||||
<p className="text-sm text-foreground">{recommendation}</p>
|
<p className="text-sm text-foreground">{recommendation}</p>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
@@ -355,8 +355,8 @@ export default function AnalyticsPage() {
|
|||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
{learningReport.knowledge_gaps.map((gap, index) => (
|
{learningReport.knowledge_gaps.map((gap, index) => (
|
||||||
<div key={index} className="flex items-start space-x-3 p-3 bg-orange-500/10 dark:bg-orange-500/20 rounded-lg">
|
<div key={index} className="flex items-start space-x-3 p-3 bg-muted dark:bg-orange-500/20 rounded-lg">
|
||||||
<div className="w-2 h-2 bg-orange-600 rounded-full mt-2 flex-shrink-0" />
|
<div className="w-2 h-2 bg-muted-foreground rounded-full mt-2 flex-shrink-0" />
|
||||||
<p className="text-sm text-foreground">{gap}</p>
|
<p className="text-sm text-foreground">{gap}</p>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import { PanelLeftClose, PanelLeftOpen } from "lucide-react";
|
|||||||
export default function ChatPage() {
|
export default function ChatPage() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { isAuthenticated, user, isLoading: authLoading } = useAuthStore();
|
const { isAuthenticated, user, isLoading: authLoading } = useAuthStore();
|
||||||
const { loadSessions, sessions } = useChatStore();
|
const { loadSessions, sessions, selectSession, currentSession } = useChatStore();
|
||||||
const [isInitialized, setIsInitialized] = useState(false);
|
const [isInitialized, setIsInitialized] = useState(false);
|
||||||
const [sidebarOpen, setSidebarOpen] = useState(true);
|
const [sidebarOpen, setSidebarOpen] = useState(true);
|
||||||
const [initialLoading, setInitialLoading] = useState(true);
|
const [initialLoading, setInitialLoading] = useState(true);
|
||||||
@@ -26,7 +26,14 @@ export default function ChatPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (isAuthenticated && !isInitialized) {
|
if (isAuthenticated && !isInitialized) {
|
||||||
loadSessions().then(() => setInitialLoading(false));
|
loadSessions().then(async () => {
|
||||||
|
setInitialLoading(false);
|
||||||
|
// Auto-select the most recent session, or do nothing (welcome page)
|
||||||
|
const store = useChatStore.getState();
|
||||||
|
if (!store.currentSession && store.sessions.length > 0) {
|
||||||
|
await store.selectSession(store.sessions[0].id);
|
||||||
|
}
|
||||||
|
});
|
||||||
setIsInitialized(true);
|
setIsInitialized(true);
|
||||||
}
|
}
|
||||||
}, [isAuthenticated, authLoading, isInitialized, router, loadSessions]);
|
}, [isAuthenticated, authLoading, isInitialized, router, loadSessions]);
|
||||||
|
|||||||
@@ -131,7 +131,7 @@ export default function CourseContentPage() {
|
|||||||
<>
|
<>
|
||||||
{/* 知识图谱视图 */}
|
{/* 知识图谱视图 */}
|
||||||
{viewMode === 'graph' && (
|
{viewMode === 'graph' && (
|
||||||
<div className="w-full h-[calc(100vh-250px)] min-h-[600px] lg:min-h-[700px] rounded-lg border border-border/40 overflow-hidden bg-slate-50">
|
<div className="w-full h-[calc(100vh-250px)] min-h-[600px] lg:min-h-[700px] rounded-lg border border-border overflow-hidden bg-muted">
|
||||||
<KnowledgeGraph bookStructure={bookStructure} />
|
<KnowledgeGraph bookStructure={bookStructure} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -12,43 +12,26 @@ import {
|
|||||||
ChevronRight,
|
ChevronRight,
|
||||||
Clock,
|
Clock,
|
||||||
User,
|
User,
|
||||||
|
Plus,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||||
|
|
||||||
import { forumAPI } from "@/lib/api";
|
import { forumAPI } from "@/lib/api";
|
||||||
import type { ForumCategory, ForumPostSummary } from "@/types";
|
import type { ForumCategory, ForumPostSummary } from "@/types";
|
||||||
|
import { format } from "date-fns";
|
||||||
const CATEGORY_ICONS: Record<string, React.ReactNode> = {
|
|
||||||
default: <MessageSquare className="w-5 h-5" />,
|
|
||||||
};
|
|
||||||
|
|
||||||
const CATEGORY_ORDER = ["公告", "通知", "学习", "讨论", "课程", "反馈", "使用"];
|
const CATEGORY_ORDER = ["公告", "通知", "学习", "讨论", "课程", "反馈", "使用"];
|
||||||
|
|
||||||
function getCategoryIcon(name: string) {
|
function getCategoryIcon(name: string) {
|
||||||
if (name.includes("公告") || name.includes("通知"))
|
if (name.includes("公告") || name.includes("通知"))
|
||||||
return <Megaphone className="w-5 h-5" />;
|
return <Megaphone className="w-4 h-4" />;
|
||||||
if (name.includes("学习") || name.includes("讨论") || name.includes("课程"))
|
if (name.includes("学习") || name.includes("讨论") || name.includes("课程"))
|
||||||
return <BookOpen className="w-5 h-5" />;
|
return <BookOpen className="w-4 h-4" />;
|
||||||
if (name.includes("反馈") || name.includes("使用"))
|
if (name.includes("反馈") || name.includes("使用"))
|
||||||
return <Lightbulb className="w-5 h-5" />;
|
return <Lightbulb className="w-4 h-4" />;
|
||||||
return CATEGORY_ICONS.default;
|
return <MessageSquare className="w-4 h-4" />;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getCategoryAccent(name: string) {
|
|
||||||
if (name.includes("公告") || name.includes("通知")) return "text-rose-600 dark:text-rose-400";
|
|
||||||
if (name.includes("学习") || name.includes("讨论") || name.includes("课程")) return "text-blue-600 dark:text-blue-400";
|
|
||||||
if (name.includes("反馈") || name.includes("使用")) return "text-amber-600 dark:text-amber-400";
|
|
||||||
return "text-slate-600 dark:text-slate-400";
|
|
||||||
}
|
|
||||||
|
|
||||||
function getCategoryBorder(name: string) {
|
|
||||||
if (name.includes("公告") || name.includes("通知")) return "border-l-rose-500";
|
|
||||||
if (name.includes("学习") || name.includes("讨论") || name.includes("课程")) return "border-l-blue-500";
|
|
||||||
if (name.includes("反馈") || name.includes("使用")) return "border-l-amber-500";
|
|
||||||
return "border-l-slate-500";
|
|
||||||
}
|
|
||||||
|
|
||||||
import { format } from "date-fns";
|
|
||||||
|
|
||||||
export default function ForumHomePage() {
|
export default function ForumHomePage() {
|
||||||
const [categories, setCategories] = useState<ForumCategory[]>([]);
|
const [categories, setCategories] = useState<ForumCategory[]>([]);
|
||||||
const [categoryPosts, setCategoryPosts] = useState<
|
const [categoryPosts, setCategoryPosts] = useState<
|
||||||
@@ -56,17 +39,30 @@ export default function ForumHomePage() {
|
|||||||
>({});
|
>({});
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [activeTab, setActiveTab] = useState<string>("");
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const loadCategories = async () => {
|
const loadCategories = async () => {
|
||||||
try {
|
try {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
const data = await forumAPI.getCategories();
|
const data = await forumAPI.getCategories();
|
||||||
setCategories(data);
|
const sorted = [...data].sort((a, b) => {
|
||||||
|
const getOrder = (name: string) => {
|
||||||
|
for (let i = 0; i < CATEGORY_ORDER.length; i++) {
|
||||||
|
if (name.includes(CATEGORY_ORDER[i])) return i;
|
||||||
|
}
|
||||||
|
return CATEGORY_ORDER.length;
|
||||||
|
};
|
||||||
|
return getOrder(a.name) - getOrder(b.name);
|
||||||
|
});
|
||||||
|
setCategories(sorted);
|
||||||
|
if (sorted.length > 0) {
|
||||||
|
setActiveTab(String(sorted[0].id));
|
||||||
|
}
|
||||||
const postsEntries = await Promise.all(
|
const postsEntries = await Promise.all(
|
||||||
data.map(async (category) => {
|
sorted.map(async (category) => {
|
||||||
try {
|
try {
|
||||||
const posts = await forumAPI.getPosts(category.id, 3);
|
const posts = await forumAPI.getPosts(category.id, 10);
|
||||||
return [category.id, posts] as const;
|
return [category.id, posts] as const;
|
||||||
} catch {
|
} catch {
|
||||||
return [category.id, []] as const;
|
return [category.id, []] as const;
|
||||||
@@ -87,69 +83,83 @@ export default function ForumHomePage() {
|
|||||||
loadCategories();
|
loadCategories();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
if (isLoading) {
|
||||||
<div className="min-h-screen bg-background">
|
return (
|
||||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
<div className="min-h-screen bg-background">
|
||||||
{isLoading ? (
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||||
<div className="flex items-center justify-center gap-2 text-muted-foreground py-20">
|
<div className="flex items-center justify-center gap-2 text-muted-foreground py-20">
|
||||||
<Loader2 className="h-5 w-5 animate-spin" />
|
<Loader2 className="h-5 w-5 animate-spin" />
|
||||||
正在加载社区...
|
正在加载社区...
|
||||||
</div>
|
</div>
|
||||||
) : error ? (
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-background">
|
||||||
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||||
<div className="text-center py-16">
|
<div className="text-center py-16">
|
||||||
<p className="text-muted-foreground">{error}</p>
|
<p className="text-muted-foreground">{error}</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
</div>
|
||||||
<div className="space-y-5">
|
</div>
|
||||||
{[...categories]
|
);
|
||||||
.sort((a, b) => {
|
}
|
||||||
const getOrder = (name: string) => {
|
|
||||||
for (let i = 0; i < CATEGORY_ORDER.length; i++) {
|
|
||||||
if (name.includes(CATEGORY_ORDER[i])) return i;
|
|
||||||
}
|
|
||||||
return CATEGORY_ORDER.length;
|
|
||||||
};
|
|
||||||
return getOrder(a.name) - getOrder(b.name);
|
|
||||||
})
|
|
||||||
.map((category) => {
|
|
||||||
const posts = categoryPosts[category.id] || [];
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={category.id}
|
|
||||||
className={`rounded-xl border border-border overflow-hidden border-l-4 ${getCategoryBorder(category.name)}`}
|
|
||||||
>
|
|
||||||
{/* Category header */}
|
|
||||||
<div className="px-5 py-3.5 flex items-center justify-between border-b border-border">
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<span className={getCategoryAccent(category.name)}>
|
|
||||||
{getCategoryIcon(category.name)}
|
|
||||||
</span>
|
|
||||||
<div>
|
|
||||||
<h2 className="text-base font-semibold text-foreground">
|
|
||||||
{category.name}
|
|
||||||
</h2>
|
|
||||||
{category.description && (
|
|
||||||
<p className="text-sm text-muted-foreground mt-0.5">
|
|
||||||
{category.description}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<span className="text-sm text-muted-foreground tabular-nums">
|
|
||||||
{category.post_count} 条讨论
|
|
||||||
</span>
|
|
||||||
<Link
|
|
||||||
href={`/forum/${category.id}`}
|
|
||||||
className="text-sm font-medium text-primary hover:underline flex items-center gap-0.5"
|
|
||||||
>
|
|
||||||
查看全部
|
|
||||||
<ChevronRight className="w-4 h-4" />
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Posts list */}
|
return (
|
||||||
|
<div className="min-h-screen bg-background">
|
||||||
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||||
|
<Tabs
|
||||||
|
value={activeTab}
|
||||||
|
onValueChange={setActiveTab}
|
||||||
|
className="w-full"
|
||||||
|
>
|
||||||
|
<TabsList className="grid w-full grid-cols-3">
|
||||||
|
{categories.map((category) => (
|
||||||
|
<TabsTrigger
|
||||||
|
key={category.id}
|
||||||
|
value={String(category.id)}
|
||||||
|
className="flex items-center gap-2"
|
||||||
|
>
|
||||||
|
{getCategoryIcon(category.name)}
|
||||||
|
{category.name}
|
||||||
|
</TabsTrigger>
|
||||||
|
))}
|
||||||
|
</TabsList>
|
||||||
|
|
||||||
|
{categories.map((category) => {
|
||||||
|
const posts = categoryPosts[category.id] || [];
|
||||||
|
return (
|
||||||
|
<TabsContent
|
||||||
|
key={category.id}
|
||||||
|
value={String(category.id)}
|
||||||
|
className="mt-6"
|
||||||
|
>
|
||||||
|
{/* Category description */}
|
||||||
|
{category.description && (
|
||||||
|
<p className="text-sm text-muted-foreground mb-4">
|
||||||
|
{category.description}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* New post button */}
|
||||||
|
<div className="flex items-center justify-between mb-4">
|
||||||
|
<span className="text-sm text-muted-foreground tabular-nums">
|
||||||
|
{category.post_count} 条讨论
|
||||||
|
</span>
|
||||||
|
<Link
|
||||||
|
href={`/forum/${category.id}`}
|
||||||
|
className="inline-flex items-center gap-1.5 text-sm font-medium text-primary hover:underline"
|
||||||
|
>
|
||||||
|
<Plus className="w-3.5 h-3.5" />
|
||||||
|
发表讨论
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Posts list */}
|
||||||
|
<div className="rounded-xl border border-border overflow-hidden">
|
||||||
<div className="divide-y divide-border">
|
<div className="divide-y divide-border">
|
||||||
{posts.length > 0 ? (
|
{posts.length > 0 ? (
|
||||||
posts.map((post) => (
|
posts.map((post) => (
|
||||||
@@ -169,7 +179,10 @@ export default function ForumHomePage() {
|
|||||||
<span>{post.author_name}</span>
|
<span>{post.author_name}</span>
|
||||||
<span className="inline-flex items-center gap-0.5">
|
<span className="inline-flex items-center gap-0.5">
|
||||||
<Clock className="w-3 h-3" />
|
<Clock className="w-3 h-3" />
|
||||||
{format(new Date(post.created_at), "yyyy/M/d HH:mm")}
|
{format(
|
||||||
|
new Date(post.created_at),
|
||||||
|
"yyyy/M/d HH:mm"
|
||||||
|
)}
|
||||||
</span>
|
</span>
|
||||||
<span className="inline-flex items-center gap-0.5">
|
<span className="inline-flex items-center gap-0.5">
|
||||||
<MessageCircle className="w-3 h-3" />
|
<MessageCircle className="w-3 h-3" />
|
||||||
@@ -181,16 +194,32 @@ export default function ForumHomePage() {
|
|||||||
</Link>
|
</Link>
|
||||||
))
|
))
|
||||||
) : (
|
) : (
|
||||||
<div className="px-5 py-8 text-center text-sm text-muted-foreground">
|
<div className="px-5 py-12 text-center">
|
||||||
暂无讨论,成为第一个发帖的人
|
<MessageSquare className="w-8 h-8 text-muted-foreground/40 mx-auto mb-2" />
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
暂无讨论,成为第一个发帖的人
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
|
||||||
})}
|
{/* View all link */}
|
||||||
</div>
|
{posts.length > 0 && (
|
||||||
)}
|
<div className="flex justify-center mt-4">
|
||||||
|
<Link
|
||||||
|
href={`/forum/${category.id}`}
|
||||||
|
className="text-sm text-muted-foreground hover:text-primary transition-colors inline-flex items-center gap-1"
|
||||||
|
>
|
||||||
|
查看全部 {category.post_count} 条讨论
|
||||||
|
<ChevronRight className="w-4 h-4" />
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</TabsContent>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Tabs>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -209,7 +209,7 @@ export default function KnowledgeBaseDetailPage() {
|
|||||||
返回
|
返回
|
||||||
</Button>
|
</Button>
|
||||||
<div className="flex items-center space-x-4">
|
<div className="flex items-center space-x-4">
|
||||||
<BookOpen className="w-8 h-8 text-blue-600" />
|
<BookOpen className="w-8 h-8 text-primary" />
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-2xl font-bold mb-2">{knowledgeBase.name}</h1>
|
<h1 className="text-2xl font-bold mb-2">{knowledgeBase.name}</h1>
|
||||||
<p className="text-muted-foreground">
|
<p className="text-muted-foreground">
|
||||||
@@ -262,7 +262,7 @@ export default function KnowledgeBaseDetailPage() {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
<Button className="bg-gradient-to-r from-blue-600 to-purple-600 hover:from-blue-700 hover:to-purple-700">
|
<Button variant="default">
|
||||||
<Plus className="w-4 h-4 mr-2" />
|
<Plus className="w-4 h-4 mr-2" />
|
||||||
上传文档
|
上传文档
|
||||||
</Button>
|
</Button>
|
||||||
@@ -302,17 +302,17 @@ export default function KnowledgeBaseDetailPage() {
|
|||||||
<div key={fileId} className="flex items-center justify-between p-2 border rounded-md">
|
<div key={fileId} className="flex items-center justify-between p-2 border rounded-md">
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<FileText className="w-4 h-4 text-blue-600 flex-shrink-0" />
|
<FileText className="w-4 h-4 text-primary flex-shrink-0" />
|
||||||
<span className="text-sm truncate">{file.name}</span>
|
<span className="text-sm truncate">{file.name}</span>
|
||||||
<span className="text-xs text-muted-foreground">
|
<span className="text-xs text-muted-foreground">
|
||||||
({(file.size / 1024 / 1024).toFixed(2)} MB)
|
({(file.size / 1024 / 1024).toFixed(2)} MB)
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{error && (
|
{error && (
|
||||||
<div className="text-xs text-red-600 mt-1">{error}</div>
|
<div className="text-xs text-destructive mt-1">{error}</div>
|
||||||
)}
|
)}
|
||||||
{progress > 0 && progress < 100 && (
|
{progress > 0 && progress < 100 && (
|
||||||
<div className="w-full bg-gray-200 rounded-full h-1 mt-1">
|
<div className="w-full bg-muted rounded-full h-1 mt-1">
|
||||||
<div
|
<div
|
||||||
className="bg-blue-600 h-1 rounded-full transition-all duration-300"
|
className="bg-blue-600 h-1 rounded-full transition-all duration-300"
|
||||||
style={{ width: `${progress}%` }}
|
style={{ width: `${progress}%` }}
|
||||||
@@ -320,7 +320,7 @@ export default function KnowledgeBaseDetailPage() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{progress === 100 && !error && (
|
{progress === 100 && !error && (
|
||||||
<div className="text-xs text-green-600 mt-1">✓ 上传完成</div>
|
<div className="text-xs text-emerald-500 mt-1">✓ 上传完成</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
@@ -330,7 +330,7 @@ export default function KnowledgeBaseDetailPage() {
|
|||||||
setUploadFiles(prev => prev.filter((_, i) => i !== index));
|
setUploadFiles(prev => prev.filter((_, i) => i !== index));
|
||||||
}}
|
}}
|
||||||
disabled={isUploading}
|
disabled={isUploading}
|
||||||
className="text-red-600 hover:text-red-700"
|
className="text-destructive hover:text-destructive/80"
|
||||||
>
|
>
|
||||||
<Trash2 className="w-4 h-4" />
|
<Trash2 className="w-4 h-4" />
|
||||||
</Button>
|
</Button>
|
||||||
@@ -398,7 +398,7 @@ export default function KnowledgeBaseDetailPage() {
|
|||||||
|
|
||||||
<div className="flex items-center space-x-4">
|
<div className="flex items-center space-x-4">
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-4 h-4" />
|
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-muted-foreground w-4 h-4" />
|
||||||
<Input
|
<Input
|
||||||
placeholder="搜索文档..."
|
placeholder="搜索文档..."
|
||||||
value={searchQuery}
|
value={searchQuery}
|
||||||
@@ -450,14 +450,14 @@ export default function KnowledgeBaseDetailPage() {
|
|||||||
<TableRow key={doc.id}>
|
<TableRow key={doc.id}>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
{doc.is_processed ? (
|
{doc.is_processed ? (
|
||||||
<CheckCircle className="w-5 h-5 text-green-600" />
|
<CheckCircle className="w-5 h-5 text-emerald-500" />
|
||||||
) : (
|
) : (
|
||||||
<Clock className="w-5 h-5 text-yellow-600" />
|
<Clock className="w-5 h-5 text-muted-foreground" />
|
||||||
)}
|
)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="font-medium">
|
<TableCell className="font-medium">
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<FileText className="w-4 h-4 text-blue-600" />
|
<FileText className="w-4 h-4 text-primary" />
|
||||||
<span>{doc.title}</span>
|
<span>{doc.title}</span>
|
||||||
</div>
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
@@ -480,7 +480,7 @@ export default function KnowledgeBaseDetailPage() {
|
|||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => handleDeleteDocument(doc.id)}
|
onClick={() => handleDeleteDocument(doc.id)}
|
||||||
className="text-red-600 hover:text-red-700"
|
className="text-destructive hover:text-destructive/80"
|
||||||
>
|
>
|
||||||
<Trash2 className="w-4 h-4" />
|
<Trash2 className="w-4 h-4" />
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -144,7 +144,7 @@ export default function KnowledgePage() {
|
|||||||
{/* 创建知识库按钮 */}
|
{/* 创建知识库按钮 */}
|
||||||
<Dialog open={isCreateDialogOpen} onOpenChange={setIsCreateDialogOpen}>
|
<Dialog open={isCreateDialogOpen} onOpenChange={setIsCreateDialogOpen}>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
<Button className="bg-gradient-to-r from-blue-600 to-purple-600 hover:from-blue-700 hover:to-purple-700">
|
<Button variant="default">
|
||||||
<Plus className="w-4 h-4 mr-2" />
|
<Plus className="w-4 h-4 mr-2" />
|
||||||
创建知识库
|
创建知识库
|
||||||
</Button>
|
</Button>
|
||||||
@@ -206,7 +206,7 @@ export default function KnowledgePage() {
|
|||||||
|
|
||||||
<div className="flex items-center space-x-4">
|
<div className="flex items-center space-x-4">
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-4 h-4" />
|
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-muted-foreground w-4 h-4" />
|
||||||
<Input
|
<Input
|
||||||
placeholder="搜索知识库..."
|
placeholder="搜索知识库..."
|
||||||
value={searchQuery}
|
value={searchQuery}
|
||||||
@@ -224,17 +224,17 @@ export default function KnowledgePage() {
|
|||||||
{systemKBs.length > 0 && (
|
{systemKBs.length > 0 && (
|
||||||
<div className="mb-8">
|
<div className="mb-8">
|
||||||
<div className="flex items-center space-x-2 mb-4">
|
<div className="flex items-center space-x-2 mb-4">
|
||||||
<FolderOpen className="w-5 h-5 text-amber-600" />
|
<FolderOpen className="w-5 h-5 text-primary" />
|
||||||
<h2 className="text-lg font-semibold">系统知识库</h2>
|
<h2 className="text-lg font-semibold">系统知识库</h2>
|
||||||
<span className="text-sm text-muted-foreground">({systemKBs.length})</span>
|
<span className="text-sm text-muted-foreground">({systemKBs.length})</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||||
{systemKBs.map((kb) => (
|
{systemKBs.map((kb) => (
|
||||||
<Card key={kb.id} className="hover:shadow-lg transition-shadow backdrop-blur-sm bg-card/80 border-amber-500/30">
|
<Card key={kb.id} className="hover:shadow-lg transition-shadow backdrop-blur-sm bg-card/80 border-primary/30">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<div className="flex items-start justify-between">
|
<div className="flex items-start justify-between">
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<FolderOpen className="w-5 h-5 text-amber-600" />
|
<FolderOpen className="w-5 h-5 text-primary" />
|
||||||
<div>
|
<div>
|
||||||
<CardTitle className="text-lg">{kb.name}</CardTitle>
|
<CardTitle className="text-lg">{kb.name}</CardTitle>
|
||||||
<CardDescription className="text-sm">
|
<CardDescription className="text-sm">
|
||||||
@@ -244,9 +244,9 @@ export default function KnowledgePage() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex items-center space-x-1">
|
<div className="flex items-center space-x-1">
|
||||||
{kb.document_count > 0 ? (
|
{kb.document_count > 0 ? (
|
||||||
<CheckCircle className="w-4 h-4 text-green-600" />
|
<CheckCircle className="w-4 h-4 text-emerald-500" />
|
||||||
) : (
|
) : (
|
||||||
<Clock className="w-4 h-4 text-gray-400" />
|
<Clock className="w-4 h-4 text-muted-foreground" />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -263,7 +263,7 @@ export default function KnowledgePage() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex justify-between text-sm">
|
<div className="flex justify-between text-sm">
|
||||||
<span>状态</span>
|
<span>状态</span>
|
||||||
<span className={kb.document_count > 0 ? "text-green-600" : "text-gray-500"}>
|
<span className={kb.document_count > 0 ? "text-emerald-500" : "text-muted-foreground"}>
|
||||||
{kb.document_count > 0 ? "已就绪" : "空知识库"}
|
{kb.document_count > 0 ? "已就绪" : "空知识库"}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -311,7 +311,7 @@ export default function KnowledgePage() {
|
|||||||
<div>
|
<div>
|
||||||
{systemKBs.length > 0 && (
|
{systemKBs.length > 0 && (
|
||||||
<div className="flex items-center space-x-2 mb-4">
|
<div className="flex items-center space-x-2 mb-4">
|
||||||
<BookOpen className="w-5 h-5 text-blue-600" />
|
<BookOpen className="w-5 h-5 text-primary" />
|
||||||
<h2 className="text-lg font-semibold">我的知识库</h2>
|
<h2 className="text-lg font-semibold">我的知识库</h2>
|
||||||
<span className="text-sm text-muted-foreground">({userKBs.length})</span>
|
<span className="text-sm text-muted-foreground">({userKBs.length})</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -322,7 +322,7 @@ export default function KnowledgePage() {
|
|||||||
<CardHeader>
|
<CardHeader>
|
||||||
<div className="flex items-start justify-between">
|
<div className="flex items-start justify-between">
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<BookOpen className="w-5 h-5 text-blue-600" />
|
<BookOpen className="w-5 h-5 text-primary" />
|
||||||
<div>
|
<div>
|
||||||
<CardTitle className="text-lg">{kb.name}</CardTitle>
|
<CardTitle className="text-lg">{kb.name}</CardTitle>
|
||||||
<CardDescription className="text-sm">
|
<CardDescription className="text-sm">
|
||||||
@@ -332,9 +332,9 @@ export default function KnowledgePage() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex items-center space-x-1">
|
<div className="flex items-center space-x-1">
|
||||||
{kb.document_count > 0 ? (
|
{kb.document_count > 0 ? (
|
||||||
<CheckCircle className="w-4 h-4 text-green-600" />
|
<CheckCircle className="w-4 h-4 text-emerald-500" />
|
||||||
) : (
|
) : (
|
||||||
<Clock className="w-4 h-4 text-gray-400" />
|
<Clock className="w-4 h-4 text-muted-foreground" />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -351,7 +351,7 @@ export default function KnowledgePage() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex justify-between text-sm">
|
<div className="flex justify-between text-sm">
|
||||||
<span>状态</span>
|
<span>状态</span>
|
||||||
<span className={kb.document_count > 0 ? "text-green-600" : "text-gray-500"}>
|
<span className={kb.document_count > 0 ? "text-emerald-500" : "text-muted-foreground"}>
|
||||||
{kb.document_count > 0 ? "已就绪" : "空知识库"}
|
{kb.document_count > 0 ? "已就绪" : "空知识库"}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -371,7 +371,7 @@ export default function KnowledgePage() {
|
|||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => handleDeleteKnowledgeBase(kb.id)}
|
onClick={() => handleDeleteKnowledgeBase(kb.id)}
|
||||||
className="text-red-600 hover:text-red-700"
|
className="text-destructive hover:text-destructive/80"
|
||||||
>
|
>
|
||||||
<Trash2 className="w-4 h-4" />
|
<Trash2 className="w-4 h-4" />
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { zodResolver } from "@hookform/resolvers/zod";
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { useAuthStore } from "@/store/auth";
|
import { useAuthStore } from "@/store/auth";
|
||||||
import MobileNav from "@/components/layout/mobile-nav";
|
import MobileNav from "@/components/layout/mobile-nav";
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import {
|
import {
|
||||||
@@ -15,11 +16,9 @@ import {
|
|||||||
Edit2,
|
Edit2,
|
||||||
Save,
|
Save,
|
||||||
X,
|
X,
|
||||||
MessageSquare,
|
User,
|
||||||
Database,
|
|
||||||
FileText
|
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { authAPI, analyticsAPI } from "@/lib/api";
|
import { authAPI } from "@/lib/api";
|
||||||
import { formatDate } from "@/lib/utils";
|
import { formatDate } from "@/lib/utils";
|
||||||
|
|
||||||
const profileSchema = z.object({
|
const profileSchema = z.object({
|
||||||
@@ -37,12 +36,6 @@ export default function ProfilePage() {
|
|||||||
const [isSaving, setIsSaving] = useState(false);
|
const [isSaving, setIsSaving] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [success, setSuccess] = useState<string | null>(null);
|
const [success, setSuccess] = useState<string | null>(null);
|
||||||
const [statistics, setStatistics] = useState<{
|
|
||||||
total_sessions: number;
|
|
||||||
total_messages: number;
|
|
||||||
total_documents: number;
|
|
||||||
active_days: number;
|
|
||||||
} | null>(null);
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
register,
|
register,
|
||||||
@@ -79,17 +72,6 @@ export default function ProfilePage() {
|
|||||||
setError(null);
|
setError(null);
|
||||||
const userData = await authAPI.getCurrentUser();
|
const userData = await authAPI.getCurrentUser();
|
||||||
setUser(userData);
|
setUser(userData);
|
||||||
try {
|
|
||||||
const stats = await analyticsAPI.getStatistics();
|
|
||||||
setStatistics({
|
|
||||||
total_sessions: stats.total_sessions || 0,
|
|
||||||
total_messages: stats.total_messages || 0,
|
|
||||||
total_documents: stats.total_documents || 0,
|
|
||||||
active_days: stats.active_days || 0,
|
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
console.warn("加载统计数据失败:", err);
|
|
||||||
}
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("加载用户信息失败:", err);
|
console.error("加载用户信息失败:", err);
|
||||||
setError("加载用户信息失败");
|
setError("加载用户信息失败");
|
||||||
@@ -142,139 +124,111 @@ export default function ProfilePage() {
|
|||||||
const initials = (user.full_name || user.username || "U").charAt(0).toUpperCase();
|
const initials = (user.full_name || user.username || "U").charAt(0).toUpperCase();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-app pb-16 lg:pb-0">
|
<div className="min-h-screen bg-background pb-16 lg:pb-0">
|
||||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||||
|
|
||||||
{/* 状态提示 */}
|
{/* 状态提示 */}
|
||||||
{error && (
|
{error && (
|
||||||
<div className="mb-6 p-3 rounded-lg bg-red-50 border border-red-200 text-red-700 text-sm">{error}</div>
|
<div className="mb-6 p-3 rounded-lg bg-destructive/10 border border-destructive/30 text-destructive text-sm">{error}</div>
|
||||||
)}
|
)}
|
||||||
{success && (
|
{success && (
|
||||||
<div className="mb-6 p-3 rounded-lg bg-green-50 border border-green-200 text-green-700 text-sm flex items-center gap-2">
|
<div className="mb-6 p-3 rounded-lg bg-emerald-50 dark:bg-emerald-950/30 border border-emerald-200 dark:border-emerald-800 text-emerald-700 dark:text-emerald-400 text-sm flex items-center gap-2">
|
||||||
<CheckCircle className="w-4 h-4" />{success}
|
<CheckCircle className="w-4 h-4" />{success}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="max-w-2xl space-y-8">
|
<div className="space-y-6">
|
||||||
{/* 头像与基本信息 */}
|
{/* 个人信息卡片 */}
|
||||||
<section>
|
<Card>
|
||||||
<div className="flex items-center justify-between mb-4 pb-3 border-b">
|
<CardHeader>
|
||||||
<h2 className="text-base font-semibold">个人信息</h2>
|
<div className="flex items-center justify-between">
|
||||||
{!isEditing && (
|
<div className="flex items-center gap-2">
|
||||||
<button
|
<User className="w-5 h-5 text-primary" />
|
||||||
onClick={() => setIsEditing(true)}
|
<CardTitle>个人信息</CardTitle>
|
||||||
className="inline-flex items-center gap-1.5 text-sm text-muted-foreground hover:text-foreground"
|
|
||||||
>
|
|
||||||
<Edit2 className="w-3.5 h-3.5" />编辑
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 头像 */}
|
|
||||||
<div className="flex items-center gap-5 mb-6">
|
|
||||||
<div className="w-16 h-16 bg-primary rounded-full flex items-center justify-center text-primary-foreground text-xl font-bold">
|
|
||||||
{initials}
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p className="text-sm font-medium">{user.full_name || user.username}</p>
|
|
||||||
<p className="text-xs text-muted-foreground">{user.email}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 表单 */}
|
|
||||||
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
|
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
|
||||||
<div>
|
|
||||||
<Label className="text-sm text-muted-foreground">用户名</Label>
|
|
||||||
<Input value={user.username} disabled className="mt-1 bg-muted" />
|
|
||||||
<p className="text-[11px] text-muted-foreground mt-1">用户名创建后无法修改</p>
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
{!isEditing && (
|
||||||
<Label className="text-sm text-muted-foreground">注册时间</Label>
|
|
||||||
<Input value={formatDate(user.created_at)} disabled className="mt-1 bg-muted" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<Label htmlFor="email" className="text-sm text-muted-foreground">邮箱</Label>
|
|
||||||
<Input
|
|
||||||
id="email"
|
|
||||||
type="email"
|
|
||||||
{...register("email")}
|
|
||||||
disabled={!isEditing}
|
|
||||||
className={`mt-1 ${errors.email ? "border-red-500" : ""}`}
|
|
||||||
/>
|
|
||||||
{errors.email && <p className="text-xs text-red-500 mt-1">{errors.email.message}</p>}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<Label htmlFor="full_name" className="text-sm text-muted-foreground">真实姓名</Label>
|
|
||||||
<Input
|
|
||||||
id="full_name"
|
|
||||||
{...register("full_name")}
|
|
||||||
disabled={!isEditing}
|
|
||||||
placeholder="请输入您的真实姓名(可选)"
|
|
||||||
className="mt-1"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{isEditing && (
|
|
||||||
<div className="flex items-center gap-3 pt-2">
|
|
||||||
<button
|
<button
|
||||||
type="submit"
|
onClick={() => setIsEditing(true)}
|
||||||
disabled={isSaving}
|
className="inline-flex items-center gap-1.5 text-sm text-muted-foreground hover:text-foreground"
|
||||||
className="inline-flex items-center gap-2 px-4 py-2 rounded-lg bg-primary text-primary-foreground text-sm font-medium hover:bg-primary/90 disabled:opacity-50"
|
|
||||||
>
|
>
|
||||||
{isSaving ? <><Loader2 className="w-4 h-4 animate-spin" />保存中...</> : <><Save className="w-4 h-4" />保存</>}
|
<Edit2 className="w-3.5 h-3.5" />编辑
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={handleCancel}
|
|
||||||
disabled={isSaving}
|
|
||||||
className="inline-flex items-center gap-2 px-4 py-2 rounded-lg border text-sm hover:bg-muted disabled:opacity-50"
|
|
||||||
>
|
|
||||||
<X className="w-4 h-4" />取消
|
|
||||||
</button>
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<CardDescription>管理您的账户基本信息</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
{/* 头像 + 表单 */}
|
||||||
|
<div className="flex items-center gap-5 mb-6 pb-6 border-b">
|
||||||
|
<div className="w-16 h-16 bg-primary rounded-full flex items-center justify-center text-primary-foreground text-xl font-bold flex-shrink-0">
|
||||||
|
{initials}
|
||||||
</div>
|
</div>
|
||||||
)}
|
<div className="flex-1">
|
||||||
</form>
|
<p className="text-base font-medium">{user.full_name || user.username}</p>
|
||||||
</section>
|
<p className="text-sm text-muted-foreground">{user.email}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* 学习统计 */}
|
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
|
||||||
<section>
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
<div className="flex items-center gap-2 mb-4 pb-3 border-b">
|
<div>
|
||||||
<h2 className="text-base font-semibold">学习统计</h2>
|
<Label className="text-sm text-muted-foreground">用户名</Label>
|
||||||
</div>
|
<Input value={user.username} disabled className="mt-1.5 bg-muted" />
|
||||||
{statistics ? (
|
<p className="text-xs text-muted-foreground mt-1">用户名创建后无法修改</p>
|
||||||
<div className="grid grid-cols-2 sm:grid-cols-4 gap-3">
|
</div>
|
||||||
<div className="p-3 rounded-lg border text-center">
|
<div>
|
||||||
<MessageSquare className="w-4 h-4 mx-auto mb-1.5 text-primary" />
|
<Label className="text-sm text-muted-foreground">注册时间</Label>
|
||||||
<p className="text-lg font-semibold">{statistics.total_sessions}</p>
|
<Input value={formatDate(user.created_at)} disabled className="mt-1.5 bg-muted" />
|
||||||
<p className="text-[11px] text-muted-foreground">对话数</p>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="p-3 rounded-lg border text-center">
|
|
||||||
<MessageSquare className="w-4 h-4 mx-auto mb-1.5 text-green-600" />
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
<p className="text-lg font-semibold">{statistics.total_messages}</p>
|
<div>
|
||||||
<p className="text-[11px] text-muted-foreground">消息数</p>
|
<Label htmlFor="email" className="text-sm text-muted-foreground">邮箱</Label>
|
||||||
|
<Input
|
||||||
|
id="email"
|
||||||
|
type="email"
|
||||||
|
{...register("email")}
|
||||||
|
disabled={!isEditing}
|
||||||
|
className={`mt-1.5 ${errors.email ? "border-destructive" : ""}`}
|
||||||
|
/>
|
||||||
|
{errors.email && <p className="text-xs text-destructive mt-1">{errors.email.message}</p>}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="full_name" className="text-sm text-muted-foreground">真实姓名</Label>
|
||||||
|
<Input
|
||||||
|
id="full_name"
|
||||||
|
{...register("full_name")}
|
||||||
|
disabled={!isEditing}
|
||||||
|
placeholder="请输入您的真实姓名(可选)"
|
||||||
|
className="mt-1.5"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="p-3 rounded-lg border text-center">
|
|
||||||
<Database className="w-4 h-4 mx-auto mb-1.5 text-purple-600" />
|
{isEditing && (
|
||||||
<p className="text-lg font-semibold">{statistics.total_documents}</p>
|
<div className="flex items-center gap-3 pt-4 border-t">
|
||||||
<p className="text-[11px] text-muted-foreground">文档数</p>
|
<button
|
||||||
</div>
|
type="submit"
|
||||||
<div className="p-3 rounded-lg border text-center">
|
disabled={isSaving}
|
||||||
<FileText className="w-4 h-4 mx-auto mb-1.5 text-orange-600" />
|
className="inline-flex items-center gap-2 px-4 py-2 rounded-lg bg-primary text-primary-foreground text-sm font-medium hover:bg-primary/90 disabled:opacity-50"
|
||||||
<p className="text-lg font-semibold">{statistics.active_days}</p>
|
>
|
||||||
<p className="text-[11px] text-muted-foreground">活跃天数</p>
|
{isSaving ? <><Loader2 className="w-4 h-4 animate-spin" />保存中...</> : <><Save className="w-4 h-4" />保存</>}
|
||||||
</div>
|
</button>
|
||||||
</div>
|
<button
|
||||||
) : (
|
type="button"
|
||||||
<div className="text-center py-6">
|
onClick={handleCancel}
|
||||||
<Loader2 className="w-5 h-5 animate-spin mx-auto mb-2 text-muted-foreground" />
|
disabled={isSaving}
|
||||||
<p className="text-sm text-muted-foreground">加载中...</p>
|
className="inline-flex items-center gap-2 px-4 py-2 rounded-lg border text-sm hover:bg-muted disabled:opacity-50"
|
||||||
</div>
|
>
|
||||||
)}
|
<X className="w-4 h-4" />取消
|
||||||
</section>
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</form>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<MobileNav />
|
<MobileNav />
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { zodResolver } from "@hookform/resolvers/zod";
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { useAuthStore } from "@/store/auth";
|
import { useAuthStore } from "@/store/auth";
|
||||||
import MobileNav from "@/components/layout/mobile-nav";
|
import MobileNav from "@/components/layout/mobile-nav";
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { ThemeToggle } from "@/components/ui/theme-toggle";
|
import { ThemeToggle } from "@/components/ui/theme-toggle";
|
||||||
@@ -44,9 +45,9 @@ function getPasswordStrength(password: string) {
|
|||||||
if (/[0-9]/.test(password)) score++;
|
if (/[0-9]/.test(password)) score++;
|
||||||
if (/[^a-zA-Z0-9]/.test(password)) score++;
|
if (/[^a-zA-Z0-9]/.test(password)) score++;
|
||||||
|
|
||||||
if (score <= 2) return { label: "弱", color: "text-red-600", score };
|
if (score <= 2) return { label: "弱", color: "text-destructive", score };
|
||||||
if (score <= 4) return { label: "中", color: "text-yellow-600", score };
|
if (score <= 4) return { label: "中", color: "text-amber-500", score };
|
||||||
return { label: "强", color: "text-green-600", score };
|
return { label: "强", color: "text-emerald-500", score };
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function SettingsPage() {
|
export default function SettingsPage() {
|
||||||
@@ -106,140 +107,152 @@ export default function SettingsPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-app pb-16 lg:pb-0">
|
<div className="min-h-screen bg-background pb-16 lg:pb-0">
|
||||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||||
|
|
||||||
{/* 状态提示 */}
|
{/* 状态提示 */}
|
||||||
{error && (
|
{error && (
|
||||||
<div className="mb-6 p-3 rounded-lg bg-red-50 border border-red-200 text-red-700 text-sm">
|
<div className="mb-6 p-3 rounded-lg bg-destructive/10 border border-destructive/30 text-destructive text-sm">
|
||||||
{error}
|
{error}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{success && (
|
{success && (
|
||||||
<div className="mb-6 p-3 rounded-lg bg-green-50 border border-green-200 text-green-700 text-sm flex items-center gap-2">
|
<div className="mb-6 p-3 rounded-lg bg-emerald-50 dark:bg-emerald-950/30 border border-emerald-200 dark:border-emerald-800 text-emerald-700 dark:text-emerald-400 text-sm flex items-center gap-2">
|
||||||
<CheckCircle className="w-4 h-4" />
|
<CheckCircle className="w-4 h-4" />
|
||||||
{success}
|
{success}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="max-w-2xl space-y-8">
|
<div className="space-y-6">
|
||||||
{/* 账户安全 */}
|
{/* 账户安全 */}
|
||||||
<section>
|
<Card>
|
||||||
<div className="flex items-center gap-2 mb-4 pb-3 border-b">
|
<CardHeader>
|
||||||
<Shield className="w-4 h-4 text-primary" />
|
<div className="flex items-center gap-2">
|
||||||
<h2 className="text-base font-semibold">账户安全</h2>
|
<Shield className="w-5 h-5 text-primary" />
|
||||||
</div>
|
<CardTitle>账户安全</CardTitle>
|
||||||
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
|
|
||||||
<div>
|
|
||||||
<Label htmlFor="old_password" className="text-sm text-muted-foreground">当前密码</Label>
|
|
||||||
<div className="relative mt-1">
|
|
||||||
<Input
|
|
||||||
id="old_password"
|
|
||||||
type={showOldPassword ? "text" : "password"}
|
|
||||||
{...register("old_password")}
|
|
||||||
className={errors.old_password ? "border-red-500" : ""}
|
|
||||||
placeholder="请输入当前密码"
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="absolute right-3 top-1/2 -translate-y-1/2"
|
|
||||||
onClick={() => setShowOldPassword(!showOldPassword)}
|
|
||||||
>
|
|
||||||
{showOldPassword ? <EyeOff className="w-4 h-4 text-muted-foreground" /> : <Eye className="w-4 h-4 text-muted-foreground" />}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
{errors.old_password && <p className="text-xs text-red-500 mt-1">{errors.old_password.message}</p>}
|
|
||||||
</div>
|
</div>
|
||||||
|
<CardDescription>修改您的登录密码</CardDescription>
|
||||||
<div>
|
</CardHeader>
|
||||||
<Label htmlFor="new_password" className="text-sm text-muted-foreground">新密码</Label>
|
<CardContent>
|
||||||
<div className="relative mt-1">
|
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
|
||||||
<Input
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
id="new_password"
|
<div>
|
||||||
type={showNewPassword ? "text" : "password"}
|
<Label htmlFor="old_password" className="text-sm text-muted-foreground">当前密码</Label>
|
||||||
{...register("new_password")}
|
<div className="relative mt-1.5">
|
||||||
className={errors.new_password ? "border-red-500" : ""}
|
<Input
|
||||||
placeholder="请输入新密码(至少6个字符)"
|
id="old_password"
|
||||||
/>
|
type={showOldPassword ? "text" : "password"}
|
||||||
<button
|
{...register("old_password")}
|
||||||
type="button"
|
className={errors.old_password ? "border-destructive" : ""}
|
||||||
className="absolute right-3 top-1/2 -translate-y-1/2"
|
placeholder="请输入当前密码"
|
||||||
onClick={() => setShowNewPassword(!showNewPassword)}
|
|
||||||
>
|
|
||||||
{showNewPassword ? <EyeOff className="w-4 h-4 text-muted-foreground" /> : <Eye className="w-4 h-4 text-muted-foreground" />}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
{newPassword && (
|
|
||||||
<div className="mt-2">
|
|
||||||
<div className="flex items-center gap-2 mb-1">
|
|
||||||
<span className="text-xs text-muted-foreground">密码强度:</span>
|
|
||||||
<span className={`text-xs font-medium ${passwordStrength.color}`}>{passwordStrength.label}</span>
|
|
||||||
</div>
|
|
||||||
<div className="w-full bg-muted rounded-full h-1.5">
|
|
||||||
<div
|
|
||||||
className={`h-1.5 rounded-full transition-all ${
|
|
||||||
passwordStrength.score <= 2 ? "bg-red-500 w-1/3"
|
|
||||||
: passwordStrength.score <= 4 ? "bg-yellow-500 w-2/3"
|
|
||||||
: "bg-green-500 w-full"
|
|
||||||
}`}
|
|
||||||
/>
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="absolute right-3 top-1/2 -translate-y-1/2"
|
||||||
|
onClick={() => setShowOldPassword(!showOldPassword)}
|
||||||
|
>
|
||||||
|
{showOldPassword ? <EyeOff className="w-4 h-4 text-muted-foreground" /> : <Eye className="w-4 h-4 text-muted-foreground" />}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
{errors.old_password && <p className="text-xs text-destructive mt-1">{errors.old_password.message}</p>}
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
{errors.new_password && <p className="text-xs text-red-500 mt-1">{errors.new_password.message}</p>}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<Label htmlFor="confirm_password" className="text-sm text-muted-foreground">确认新密码</Label>
|
<Label htmlFor="new_password" className="text-sm text-muted-foreground">新密码</Label>
|
||||||
<div className="relative mt-1">
|
<div className="relative mt-1.5">
|
||||||
<Input
|
<Input
|
||||||
id="confirm_password"
|
id="new_password"
|
||||||
type={showConfirmPassword ? "text" : "password"}
|
type={showNewPassword ? "text" : "password"}
|
||||||
{...register("confirm_password")}
|
{...register("new_password")}
|
||||||
className={errors.confirm_password ? "border-red-500" : ""}
|
className={errors.new_password ? "border-destructive" : ""}
|
||||||
placeholder="请再次输入新密码"
|
placeholder="请输入新密码(至少6个字符)"
|
||||||
/>
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="absolute right-3 top-1/2 -translate-y-1/2"
|
||||||
|
onClick={() => setShowNewPassword(!showNewPassword)}
|
||||||
|
>
|
||||||
|
{showNewPassword ? <EyeOff className="w-4 h-4 text-muted-foreground" /> : <Eye className="w-4 h-4 text-muted-foreground" />}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{newPassword && (
|
||||||
|
<div className="mt-2">
|
||||||
|
<div className="flex items-center gap-2 mb-1">
|
||||||
|
<span className="text-xs text-muted-foreground">密码强度:</span>
|
||||||
|
<span className={`text-xs font-medium ${passwordStrength.color}`}>{passwordStrength.label}</span>
|
||||||
|
</div>
|
||||||
|
<div className="w-full bg-muted rounded-full h-1.5">
|
||||||
|
<div
|
||||||
|
className={`h-1.5 rounded-full transition-all ${
|
||||||
|
passwordStrength.score <= 2 ? "bg-destructive w-1/3"
|
||||||
|
: passwordStrength.score <= 4 ? "bg-amber-500 w-2/3"
|
||||||
|
: "bg-emerald-500 w-full"
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{errors.new_password && <p className="text-xs text-destructive mt-1">{errors.new_password.message}</p>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="max-w-md">
|
||||||
|
<Label htmlFor="confirm_password" className="text-sm text-muted-foreground">确认新密码</Label>
|
||||||
|
<div className="relative mt-1.5">
|
||||||
|
<Input
|
||||||
|
id="confirm_password"
|
||||||
|
type={showConfirmPassword ? "text" : "password"}
|
||||||
|
{...register("confirm_password")}
|
||||||
|
className={errors.confirm_password ? "border-destructive" : ""}
|
||||||
|
placeholder="请再次输入新密码"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="absolute right-3 top-1/2 -translate-y-1/2"
|
||||||
|
onClick={() => setShowConfirmPassword(!showConfirmPassword)}
|
||||||
|
>
|
||||||
|
{showConfirmPassword ? <EyeOff className="w-4 h-4 text-muted-foreground" /> : <Eye className="w-4 h-4 text-muted-foreground" />}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{errors.confirm_password && <p className="text-xs text-destructive mt-1">{errors.confirm_password.message}</p>}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="pt-4 border-t">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="submit"
|
||||||
className="absolute right-3 top-1/2 -translate-y-1/2"
|
disabled={isSubmitting}
|
||||||
onClick={() => setShowConfirmPassword(!showConfirmPassword)}
|
className="inline-flex items-center gap-2 px-4 py-2 rounded-lg bg-primary text-primary-foreground text-sm font-medium hover:bg-primary/90 disabled:opacity-50"
|
||||||
>
|
>
|
||||||
{showConfirmPassword ? <EyeOff className="w-4 h-4 text-muted-foreground" /> : <Eye className="w-4 h-4 text-muted-foreground" />}
|
{isSubmitting ? (
|
||||||
|
<><Loader2 className="w-4 h-4 animate-spin" />修改中...</>
|
||||||
|
) : (
|
||||||
|
<><Key className="w-4 h-4" />修改密码</>
|
||||||
|
)}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{errors.confirm_password && <p className="text-xs text-red-500 mt-1">{errors.confirm_password.message}</p>}
|
</form>
|
||||||
</div>
|
</CardContent>
|
||||||
|
</Card>
|
||||||
<div className="pt-2">
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
disabled={isSubmitting}
|
|
||||||
className="inline-flex items-center gap-2 px-4 py-2 rounded-lg bg-primary text-primary-foreground text-sm font-medium hover:bg-primary/90 disabled:opacity-50"
|
|
||||||
>
|
|
||||||
{isSubmitting ? (
|
|
||||||
<><Loader2 className="w-4 h-4 animate-spin" />修改中...</>
|
|
||||||
) : (
|
|
||||||
<><Key className="w-4 h-4" />修改密码</>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* 偏好设置 */}
|
{/* 偏好设置 */}
|
||||||
<section>
|
<Card>
|
||||||
<div className="flex items-center gap-2 mb-4 pb-3 border-b">
|
<CardHeader>
|
||||||
<Palette className="w-4 h-4 text-primary" />
|
<div className="flex items-center gap-2">
|
||||||
<h2 className="text-base font-semibold">偏好设置</h2>
|
<Palette className="w-5 h-5 text-primary" />
|
||||||
</div>
|
<CardTitle>偏好设置</CardTitle>
|
||||||
<div className="flex items-center justify-between py-3 border-b last:border-0">
|
|
||||||
<div>
|
|
||||||
<p className="text-sm font-medium">外观主题</p>
|
|
||||||
<p className="text-xs text-muted-foreground mt-0.5">切换浅色或深色模式</p>
|
|
||||||
</div>
|
</div>
|
||||||
<ThemeToggle />
|
<CardDescription>自定义您的使用体验</CardDescription>
|
||||||
</div>
|
</CardHeader>
|
||||||
</section>
|
<CardContent>
|
||||||
|
<div className="flex items-center justify-between py-3 border-b last:border-0">
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-medium">外观主题</p>
|
||||||
|
<p className="text-xs text-muted-foreground mt-0.5">切换浅色或深色模式</p>
|
||||||
|
</div>
|
||||||
|
<ThemeToggle />
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<MobileNav />
|
<MobileNav />
|
||||||
|
|||||||
@@ -120,7 +120,7 @@ export default function TextToImagePage() {
|
|||||||
transition={{ duration: 0.5 }}
|
transition={{ duration: 0.5 }}
|
||||||
className="text-center mb-8"
|
className="text-center mb-8"
|
||||||
>
|
>
|
||||||
<h1 className="text-4xl font-bold bg-gradient-to-r from-blue-600 to-purple-600 bg-clip-text text-transparent mb-4">
|
<h1 className="text-4xl font-bold text-foreground mb-4">
|
||||||
文生图 - Text to Image
|
文生图 - Text to Image
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-lg text-gray-600 max-w-2xl mx-auto">
|
<p className="text-lg text-gray-600 max-w-2xl mx-auto">
|
||||||
|
|||||||
@@ -130,7 +130,7 @@ export default function ForumCategoryPage() {
|
|||||||
{isAuthenticated && (
|
{isAuthenticated && (
|
||||||
<button
|
<button
|
||||||
onClick={() => setShowForm(!showForm)}
|
onClick={() => setShowForm(!showForm)}
|
||||||
className="inline-flex items-center gap-1.5 px-3 py-1.5 text-sm font-medium rounded-lg bg-[#2563eb] text-white hover:bg-[#2563eb]/90 transition-colors"
|
className="inline-flex items-center gap-1.5 px-3 py-1.5 text-sm font-medium rounded-lg bg-primary text-primary-foreground hover:bg-primary/90 transition-colors"
|
||||||
>
|
>
|
||||||
<PenLine className="w-3.5 h-3.5" />
|
<PenLine className="w-3.5 h-3.5" />
|
||||||
{showForm ? "收起" : "发帖"}
|
{showForm ? "收起" : "发帖"}
|
||||||
|
|||||||
@@ -185,7 +185,7 @@ export default function ForumPostPage() {
|
|||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={isSubmitting || !replyContent.trim()}
|
disabled={isSubmitting || !replyContent.trim()}
|
||||||
className="inline-flex items-center gap-1.5 px-4 py-2 text-sm font-medium rounded-lg bg-[#2563eb] text-white hover:bg-[#2563eb]/90 disabled:opacity-40 disabled:cursor-not-allowed transition-colors"
|
className="inline-flex items-center gap-1.5 px-4 py-2 text-sm font-medium rounded-lg bg-primary text-primary-foreground hover:bg-primary/90 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
||||||
>
|
>
|
||||||
{isSubmitting ? (
|
{isSubmitting ? (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -188,7 +188,7 @@ export default function ChatInterface() {
|
|||||||
"transition-all duration-150",
|
"transition-all duration-150",
|
||||||
isStreaming
|
isStreaming
|
||||||
? "bg-destructive hover:bg-destructive/90 text-white"
|
? "bg-destructive hover:bg-destructive/90 text-white"
|
||||||
: "bg-[#2563eb] hover:bg-[#1d4ed8] text-white disabled:bg-[#2563eb]/40 disabled:cursor-not-allowed"
|
: "bg-primary hover:bg-primary/90 text-primary-foreground disabled:opacity-50 disabled:cursor-not-allowed shadow-sm"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{isStreaming ? (
|
{isStreaming ? (
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
import { useState, useMemo } from "react";
|
import { useState, useMemo } from "react";
|
||||||
import { Database, Globe, ChevronDown, ChevronRight, Star } from "lucide-react";
|
import { Database, Globe, ChevronDown, ChevronRight, Star } from "lucide-react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
interface SourceReference {
|
interface SourceReference {
|
||||||
@@ -249,9 +248,9 @@ export default function SourceReferences({
|
|||||||
return (
|
return (
|
||||||
<div className="mt-3 space-y-2">
|
<div className="mt-3 space-y-2">
|
||||||
{expanded ? (
|
{expanded ? (
|
||||||
<ScrollArea className="max-h-96">
|
<div className="max-h-96 overflow-y-auto pr-1">
|
||||||
<div className="pr-3">{sourceSection}</div>
|
{sourceSection}
|
||||||
</ScrollArea>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
sourceSection
|
sourceSection
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -46,8 +46,8 @@ export default function MobileNav() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* 移动端导航栏 */}
|
{/* Bottom navigation bar */}
|
||||||
<div className="lg:hidden fixed bottom-0 left-0 right-0 bg-white border-t border-gray-200 mobile-safe-area z-50">
|
<div className="lg:hidden fixed bottom-0 left-0 right-0 bg-background border-t border-border mobile-safe-area z-50">
|
||||||
<div className="flex items-center justify-around py-2">
|
<div className="flex items-center justify-around py-2">
|
||||||
{navItems.map((item) => {
|
{navItems.map((item) => {
|
||||||
const isActive = pathname === item.href;
|
const isActive = pathname === item.href;
|
||||||
@@ -58,7 +58,7 @@ export default function MobileNav() {
|
|||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => handleNavClick(item.href)}
|
onClick={() => handleNavClick(item.href)}
|
||||||
className={`flex flex-col items-center space-y-1 px-3 py-2 ${
|
className={`flex flex-col items-center space-y-1 px-3 py-2 ${
|
||||||
isActive ? "text-white" : "text-gray-600"
|
isActive ? "" : "text-muted-foreground"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<item.icon className="w-5 h-5" />
|
<item.icon className="w-5 h-5" />
|
||||||
@@ -69,48 +69,43 @@ export default function MobileNav() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 移动端菜单按钮 */}
|
{/* Menu toggle button */}
|
||||||
<div className="lg:hidden fixed top-4 right-4 z-50">
|
<div className="lg:hidden fixed top-4 right-4 z-50">
|
||||||
<Button
|
<button
|
||||||
variant="outline"
|
|
||||||
size="icon"
|
|
||||||
onClick={() => setIsOpen(!isOpen)}
|
onClick={() => setIsOpen(!isOpen)}
|
||||||
className="bg-white shadow-lg"
|
className="inline-flex items-center justify-center w-9 h-9 rounded-lg bg-background border border-border shadow-sm hover:bg-muted transition-colors"
|
||||||
>
|
>
|
||||||
{isOpen ? <X className="w-4 h-4" /> : <Menu className="w-4 h-4" />}
|
{isOpen ? <X className="w-4 h-4" /> : <Menu className="w-4 h-4" />}
|
||||||
</Button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 移动端侧边菜单 */}
|
{/* Side menu overlay */}
|
||||||
{isOpen && (
|
{isOpen && (
|
||||||
<div className="lg:hidden fixed inset-0 z-40">
|
<div className="lg:hidden fixed inset-0 z-40">
|
||||||
{/* 遮罩 */}
|
|
||||||
<div
|
<div
|
||||||
className="absolute inset-0 bg-black bg-opacity-50"
|
className="absolute inset-0 bg-black/50"
|
||||||
onClick={() => setIsOpen(false)}
|
onClick={() => setIsOpen(false)}
|
||||||
/>
|
/>
|
||||||
|
<div className="absolute right-0 top-0 h-full w-80 max-w-[85vw] bg-background shadow-xl mobile-safe-area">
|
||||||
{/* 菜单内容 */}
|
|
||||||
<div className="absolute right-0 top-0 h-full w-80 max-w-[85vw] bg-white shadow-xl mobile-safe-area">
|
|
||||||
<div className="flex flex-col h-full">
|
<div className="flex flex-col h-full">
|
||||||
{/* 用户信息 */}
|
{/* User info */}
|
||||||
<div className="p-6 border-b border-gray-200">
|
<div className="p-6 border-b border-border">
|
||||||
<div className="flex items-center space-x-3">
|
<div className="flex items-center space-x-3">
|
||||||
<div className="w-12 h-12 bg-gray-200 rounded-full flex items-center justify-center">
|
<div className="w-12 h-12 bg-muted rounded-full flex items-center justify-center">
|
||||||
<User className="w-6 h-6 text-gray-600" />
|
<User className="w-6 h-6 text-muted-foreground" />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<p className="text-lg font-medium text-gray-900 truncate">
|
<p className="text-lg font-medium text-foreground truncate">
|
||||||
{user?.full_name || user?.username}
|
{user?.full_name || user?.username}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-sm text-gray-500 truncate">
|
<p className="text-sm text-muted-foreground truncate">
|
||||||
{user?.email}
|
{user?.email}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 导航菜单 */}
|
{/* Navigation */}
|
||||||
<div className="flex-1 p-6">
|
<div className="flex-1 p-6">
|
||||||
<nav className="space-y-2">
|
<nav className="space-y-2">
|
||||||
{navItems.map((item) => {
|
{navItems.map((item) => {
|
||||||
@@ -130,8 +125,8 @@ export default function MobileNav() {
|
|||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 底部操作 */}
|
{/* Bottom actions */}
|
||||||
<div className="p-6 border-t border-gray-200 space-y-2">
|
<div className="p-6 border-t border-border space-y-2">
|
||||||
{user?.is_superuser && (
|
{user?.is_superuser && (
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
@@ -161,7 +156,7 @@ export default function MobileNav() {
|
|||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
onClick={handleLogout}
|
onClick={handleLogout}
|
||||||
className="w-full justify-start text-red-600 hover:text-red-700 hover:bg-red-50"
|
className="w-full justify-start text-destructive hover:bg-destructive/10"
|
||||||
>
|
>
|
||||||
<LogOut className="w-5 h-5 mr-3" />
|
<LogOut className="w-5 h-5 mr-3" />
|
||||||
退出登录
|
退出登录
|
||||||
@@ -174,9 +169,3 @@ export default function MobileNav() {
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -91,7 +91,7 @@ export const useChatStore = create<ChatStore>((set, get) => ({
|
|||||||
|
|
||||||
// 选择会话
|
// 选择会话
|
||||||
selectSession: async (sessionId: number) => {
|
selectSession: async (sessionId: number) => {
|
||||||
set({ isLoading: true, error: null });
|
set({ error: null });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const session = get().sessions.find(s => s.id === sessionId);
|
const session = get().sessions.find(s => s.id === sessionId);
|
||||||
|
|||||||
Reference in New Issue
Block a user