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:
2026-05-27 22:05:08 +08:00
parent abcced35b8
commit b8561e04c6
22 changed files with 430 additions and 441 deletions
+1 -1
View File
@@ -76,7 +76,7 @@ export default function LoginPage() {
href="/"
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" />
</div>
<span className="text-2xl font-bold">
+1 -1
View File
@@ -87,7 +87,7 @@ export default function RegisterPage() {
href="/"
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" />
</div>
<span className="text-2xl font-bold">
+1 -1
View File
@@ -193,7 +193,7 @@ export default function ForumAdminPage() {
</button>
{confirmDelete?.type === "cat" && confirmDelete.id === cat.id ? (
<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>
</div>
) : (
+4 -4
View File
@@ -91,8 +91,8 @@ export default function KnowledgeAdminPage() {
<span
className={`inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium ${
kb.is_system
? "bg-blue-100 text-blue-700"
: "bg-slate-100 text-slate-600"
? "bg-primary/10 text-primary"
: "bg-muted text-muted-foreground"
}`}
>
{kb.is_system ? "系统" : "用户"}
@@ -108,7 +108,7 @@ export default function KnowledgeAdminPage() {
<div className="flex items-center justify-center gap-1">
<button
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>
@@ -122,7 +122,7 @@ export default function KnowledgeAdminPage() {
) : (
<button
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" />
</button>
+7 -6
View File
@@ -16,12 +16,13 @@ interface DashboardStats {
total_generated_images: number;
}
function TrendChart({ title, icon: Icon, data, color }: {
function TrendChart({ title, icon: Icon, data, variant }: {
title: string;
icon: React.ElementType;
data: { date: string; count: number }[];
color: string;
variant?: "default" | "secondary";
}) {
const barClass = variant === "secondary" ? "bg-emerald-500/35" : "bg-primary/35";
return (
<div className="rounded-xl border p-5 space-y-3">
<h3 className="text-sm font-medium flex items-center gap-2">
@@ -36,7 +37,7 @@ function TrendChart({ title, icon: Icon, data, color }: {
return (
<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>
<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>
</div>
);
@@ -101,8 +102,8 @@ export default function AdminPage() {
</div>
<div className="grid md:grid-cols-2 gap-6">
<TrendChart title="近14天用户注册" icon={Users} data={userTrends} color="#2563eb" />
<TrendChart title="近14天消息数" icon={MessageSquare} data={msgTrends} color="#16a34a" />
<TrendChart title="近14天用户注册" icon={Users} data={userTrends} variant="default" />
<TrendChart title="近14天消息数" icon={MessageSquare} data={msgTrends} variant="secondary" />
</div>
{systemStatus && (
@@ -118,7 +119,7 @@ export default function AdminPage() {
{ label: "嵌入模型", ok: true, sub: systemStatus.embedding_model },
].map((s) => (
<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>
{s.sub && <span className="text-muted-foreground text-xs truncate">{s.sub}</span>}
</div>
+6 -9
View File
@@ -77,10 +77,7 @@ export default function SystemSettingsPage() {
{statusItems.map((s) => (
<div key={s.label} className="flex items-center justify-between px-5 py-3">
<div className="flex items-center gap-3">
<span
className="w-2.5 h-2.5 rounded-full shrink-0"
style={{ backgroundColor: s.ok ? "#22c55e" : "#ef4444" }}
/>
<span className={`w-2.5 h-2.5 rounded-full shrink-0 ${s.ok ? "bg-emerald-500" : "bg-destructive"}`} />
<span className="text-sm">{s.label}</span>
</div>
<span className="text-sm text-muted-foreground">{s.detail}</span>
@@ -115,9 +112,9 @@ export default function SystemSettingsPage() {
</div>
{/* Danger zone */}
<div className="rounded-xl border border-red-200">
<div className="px-5 py-3 border-b border-red-200">
<h3 className="text-sm font-medium text-red-600"></h3>
<div className="rounded-xl border border-destructive/30">
<div className="px-5 py-3 border-b border-destructive/30">
<h3 className="text-sm font-medium text-destructive"></h3>
</div>
<div className="px-5 py-4 space-y-3">
<div className="flex items-center justify-between">
@@ -126,7 +123,7 @@ export default function SystemSettingsPage() {
<div className="text-xs text-muted-foreground"></div>
</div>
<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("此功能需要通过后端命令行执行")}
>
@@ -138,7 +135,7 @@ export default function SystemSettingsPage() {
<div className="text-xs text-muted-foreground"></div>
</div>
<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("此功能需要通过后端命令行执行")}
>
+1 -1
View File
@@ -154,7 +154,7 @@ export default function UsersPage() {
<div className="flex items-center justify-center gap-1">
<button
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>
+12 -12
View File
@@ -188,7 +188,7 @@ export default function AnalyticsPage() {
<Card>
<CardContent className="p-6">
<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">
<p className="text-sm font-medium text-muted-foreground"></p>
<p className="text-2xl font-bold text-foreground">{statistics.total_sessions}</p>
@@ -200,7 +200,7 @@ export default function AnalyticsPage() {
<Card>
<CardContent className="p-6">
<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">
<p className="text-sm font-medium text-muted-foreground"></p>
<p className="text-2xl font-bold text-foreground">{statistics.total_messages}</p>
@@ -212,7 +212,7 @@ export default function AnalyticsPage() {
<Card>
<CardContent className="p-6">
<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">
<p className="text-sm font-medium text-muted-foreground"></p>
<p className="text-2xl font-bold text-foreground">{statistics.total_documents}</p>
@@ -224,7 +224,7 @@ export default function AnalyticsPage() {
<Card>
<CardContent className="p-6">
<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">
<p className="text-sm font-medium text-muted-foreground"></p>
<p className="text-2xl font-bold text-foreground">{statistics.active_days}</p>
@@ -250,7 +250,7 @@ export default function AnalyticsPage() {
</div>
<div className="w-full bg-muted rounded-full h-2">
<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}%` }}
/>
</div>
@@ -278,7 +278,7 @@ export default function AnalyticsPage() {
<p className="text-sm font-medium text-foreground">{item.question}</p>
<p className="text-xs text-muted-foreground">{item.category}</p>
</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>
@@ -300,11 +300,11 @@ export default function AnalyticsPage() {
<div className="space-y-4">
<div className="flex items-center justify-between">
<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 className="w-full bg-muted rounded-full h-4">
<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}%` }}
/>
</div>
@@ -334,8 +334,8 @@ export default function AnalyticsPage() {
<CardContent>
<div className="space-y-3">
{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 className="w-2 h-2 bg-blue-600 rounded-full mt-2 flex-shrink-0" />
<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-primary rounded-full mt-2 flex-shrink-0" />
<p className="text-sm text-foreground">{recommendation}</p>
</div>
))}
@@ -355,8 +355,8 @@ export default function AnalyticsPage() {
<CardContent>
<div className="space-y-3">
{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 className="w-2 h-2 bg-orange-600 rounded-full mt-2 flex-shrink-0" />
<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-muted-foreground rounded-full mt-2 flex-shrink-0" />
<p className="text-sm text-foreground">{gap}</p>
</div>
))}
+9 -2
View File
@@ -14,7 +14,7 @@ import { PanelLeftClose, PanelLeftOpen } from "lucide-react";
export default function ChatPage() {
const router = useRouter();
const { isAuthenticated, user, isLoading: authLoading } = useAuthStore();
const { loadSessions, sessions } = useChatStore();
const { loadSessions, sessions, selectSession, currentSession } = useChatStore();
const [isInitialized, setIsInitialized] = useState(false);
const [sidebarOpen, setSidebarOpen] = useState(true);
const [initialLoading, setInitialLoading] = useState(true);
@@ -26,7 +26,14 @@ export default function ChatPage() {
}
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);
}
}, [isAuthenticated, authLoading, isInitialized, router, loadSessions]);
+1 -1
View File
@@ -131,7 +131,7 @@ export default function CourseContentPage() {
<>
{/* 知识图谱视图 */}
{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} />
</div>
)}
+118 -89
View File
@@ -12,43 +12,26 @@ import {
ChevronRight,
Clock,
User,
Plus,
} from "lucide-react";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { forumAPI } from "@/lib/api";
import type { ForumCategory, ForumPostSummary } from "@/types";
const CATEGORY_ICONS: Record<string, React.ReactNode> = {
default: <MessageSquare className="w-5 h-5" />,
};
import { format } from "date-fns";
const CATEGORY_ORDER = ["公告", "通知", "学习", "讨论", "课程", "反馈", "使用"];
function getCategoryIcon(name: string) {
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("课程"))
return <BookOpen className="w-5 h-5" />;
return <BookOpen className="w-4 h-4" />;
if (name.includes("反馈") || name.includes("使用"))
return <Lightbulb className="w-5 h-5" />;
return CATEGORY_ICONS.default;
return <Lightbulb className="w-4 h-4" />;
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() {
const [categories, setCategories] = useState<ForumCategory[]>([]);
const [categoryPosts, setCategoryPosts] = useState<
@@ -56,17 +39,30 @@ export default function ForumHomePage() {
>({});
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [activeTab, setActiveTab] = useState<string>("");
useEffect(() => {
const loadCategories = async () => {
try {
setIsLoading(true);
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(
data.map(async (category) => {
sorted.map(async (category) => {
try {
const posts = await forumAPI.getPosts(category.id, 3);
const posts = await forumAPI.getPosts(category.id, 10);
return [category.id, posts] as const;
} catch {
return [category.id, []] as const;
@@ -87,69 +83,83 @@ export default function ForumHomePage() {
loadCategories();
}, []);
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">
{isLoading ? (
if (isLoading) {
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="flex items-center justify-center gap-2 text-muted-foreground py-20">
<Loader2 className="h-5 w-5 animate-spin" />
...
</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">
<p className="text-muted-foreground">{error}</p>
</div>
) : (
<div className="space-y-5">
{[...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>
</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">
{posts.length > 0 ? (
posts.map((post) => (
@@ -169,7 +179,10 @@ export default function ForumHomePage() {
<span>{post.author_name}</span>
<span className="inline-flex items-center gap-0.5">
<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 className="inline-flex items-center gap-0.5">
<MessageCircle className="w-3 h-3" />
@@ -181,16 +194,32 @@ export default function ForumHomePage() {
</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>
)}
{/* View all link */}
{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>
);
+12 -12
View File
@@ -209,7 +209,7 @@ export default function KnowledgeBaseDetailPage() {
</Button>
<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>
<h1 className="text-2xl font-bold mb-2">{knowledgeBase.name}</h1>
<p className="text-muted-foreground">
@@ -262,7 +262,7 @@ export default function KnowledgeBaseDetailPage() {
}}
>
<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" />
</Button>
@@ -302,17 +302,17 @@ export default function KnowledgeBaseDetailPage() {
<div key={fileId} className="flex items-center justify-between p-2 border rounded-md">
<div className="flex-1 min-w-0">
<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-xs text-muted-foreground">
({(file.size / 1024 / 1024).toFixed(2)} MB)
</span>
</div>
{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 && (
<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
className="bg-blue-600 h-1 rounded-full transition-all duration-300"
style={{ width: `${progress}%` }}
@@ -320,7 +320,7 @@ export default function KnowledgeBaseDetailPage() {
</div>
)}
{progress === 100 && !error && (
<div className="text-xs text-green-600 mt-1"> </div>
<div className="text-xs text-emerald-500 mt-1"> </div>
)}
</div>
<Button
@@ -330,7 +330,7 @@ export default function KnowledgeBaseDetailPage() {
setUploadFiles(prev => prev.filter((_, i) => i !== index));
}}
disabled={isUploading}
className="text-red-600 hover:text-red-700"
className="text-destructive hover:text-destructive/80"
>
<Trash2 className="w-4 h-4" />
</Button>
@@ -398,7 +398,7 @@ export default function KnowledgeBaseDetailPage() {
<div className="flex items-center space-x-4">
<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
placeholder="搜索文档..."
value={searchQuery}
@@ -450,14 +450,14 @@ export default function KnowledgeBaseDetailPage() {
<TableRow key={doc.id}>
<TableCell>
{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 className="font-medium">
<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>
</div>
</TableCell>
@@ -480,7 +480,7 @@ export default function KnowledgeBaseDetailPage() {
variant="ghost"
size="sm"
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" />
</Button>
+14 -14
View File
@@ -144,7 +144,7 @@ export default function KnowledgePage() {
{/* 创建知识库按钮 */}
<Dialog open={isCreateDialogOpen} onOpenChange={setIsCreateDialogOpen}>
<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" />
</Button>
@@ -206,7 +206,7 @@ export default function KnowledgePage() {
<div className="flex items-center space-x-4">
<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
placeholder="搜索知识库..."
value={searchQuery}
@@ -224,17 +224,17 @@ export default function KnowledgePage() {
{systemKBs.length > 0 && (
<div className="mb-8">
<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>
<span className="text-sm text-muted-foreground">({systemKBs.length})</span>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{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>
<div className="flex items-start justify-between">
<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>
<CardTitle className="text-lg">{kb.name}</CardTitle>
<CardDescription className="text-sm">
@@ -244,9 +244,9 @@ export default function KnowledgePage() {
</div>
<div className="flex items-center space-x-1">
{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>
@@ -263,7 +263,7 @@ export default function KnowledgePage() {
</div>
<div className="flex justify-between text-sm">
<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 ? "已就绪" : "空知识库"}
</span>
</div>
@@ -311,7 +311,7 @@ export default function KnowledgePage() {
<div>
{systemKBs.length > 0 && (
<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>
<span className="text-sm text-muted-foreground">({userKBs.length})</span>
</div>
@@ -322,7 +322,7 @@ export default function KnowledgePage() {
<CardHeader>
<div className="flex items-start justify-between">
<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>
<CardTitle className="text-lg">{kb.name}</CardTitle>
<CardDescription className="text-sm">
@@ -332,9 +332,9 @@ export default function KnowledgePage() {
</div>
<div className="flex items-center space-x-1">
{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>
@@ -351,7 +351,7 @@ export default function KnowledgePage() {
</div>
<div className="flex justify-between text-sm">
<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 ? "已就绪" : "空知识库"}
</span>
</div>
@@ -371,7 +371,7 @@ export default function KnowledgePage() {
variant="outline"
size="sm"
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" />
</Button>
+90 -136
View File
@@ -7,6 +7,7 @@ import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { useAuthStore } from "@/store/auth";
import MobileNav from "@/components/layout/mobile-nav";
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import {
@@ -15,11 +16,9 @@ import {
Edit2,
Save,
X,
MessageSquare,
Database,
FileText
User,
} from "lucide-react";
import { authAPI, analyticsAPI } from "@/lib/api";
import { authAPI } from "@/lib/api";
import { formatDate } from "@/lib/utils";
const profileSchema = z.object({
@@ -37,12 +36,6 @@ export default function ProfilePage() {
const [isSaving, setIsSaving] = useState(false);
const [error, setError] = 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 {
register,
@@ -79,17 +72,6 @@ export default function ProfilePage() {
setError(null);
const userData = await authAPI.getCurrentUser();
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) {
console.error("加载用户信息失败:", err);
setError("加载用户信息失败");
@@ -142,139 +124,111 @@ export default function ProfilePage() {
const initials = (user.full_name || user.username || "U").charAt(0).toUpperCase();
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">
{/* 状态提示 */}
{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 && (
<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}
</div>
)}
<div className="max-w-2xl space-y-8">
{/* 头像与基本信息 */}
<section>
<div className="flex items-center justify-between mb-4 pb-3 border-b">
<h2 className="text-base font-semibold"></h2>
{!isEditing && (
<button
onClick={() => setIsEditing(true)}
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 className="space-y-6">
{/* 个人信息卡片 */}
<Card>
<CardHeader>
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<User className="w-5 h-5 text-primary" />
<CardTitle></CardTitle>
</div>
<div>
<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">
{!isEditing && (
<button
type="submit"
disabled={isSaving}
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"
onClick={() => setIsEditing(true)}
className="inline-flex items-center gap-1.5 text-sm text-muted-foreground hover:text-foreground"
>
{isSaving ? <><Loader2 className="w-4 h-4 animate-spin" />...</> : <><Save className="w-4 h-4" /></>}
</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" />
<Edit2 className="w-3.5 h-3.5" />
</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>
)}
</form>
</section>
<div className="flex-1">
<p className="text-base font-medium">{user.full_name || user.username}</p>
<p className="text-sm text-muted-foreground">{user.email}</p>
</div>
</div>
{/* 学习统计 */}
<section>
<div className="flex items-center gap-2 mb-4 pb-3 border-b">
<h2 className="text-base font-semibold"></h2>
</div>
{statistics ? (
<div className="grid grid-cols-2 sm:grid-cols-4 gap-3">
<div className="p-3 rounded-lg border text-center">
<MessageSquare className="w-4 h-4 mx-auto mb-1.5 text-primary" />
<p className="text-lg font-semibold">{statistics.total_sessions}</p>
<p className="text-[11px] text-muted-foreground"></p>
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<Label className="text-sm text-muted-foreground"></Label>
<Input value={user.username} disabled className="mt-1.5 bg-muted" />
<p className="text-xs text-muted-foreground mt-1"></p>
</div>
<div>
<Label className="text-sm text-muted-foreground"></Label>
<Input value={formatDate(user.created_at)} disabled className="mt-1.5 bg-muted" />
</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" />
<p className="text-lg font-semibold">{statistics.total_messages}</p>
<p className="text-[11px] text-muted-foreground"></p>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<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 className="p-3 rounded-lg border text-center">
<Database className="w-4 h-4 mx-auto mb-1.5 text-purple-600" />
<p className="text-lg font-semibold">{statistics.total_documents}</p>
<p className="text-[11px] text-muted-foreground"></p>
</div>
<div className="p-3 rounded-lg border text-center">
<FileText className="w-4 h-4 mx-auto mb-1.5 text-orange-600" />
<p className="text-lg font-semibold">{statistics.active_days}</p>
<p className="text-[11px] text-muted-foreground"></p>
</div>
</div>
) : (
<div className="text-center py-6">
<Loader2 className="w-5 h-5 animate-spin mx-auto mb-2 text-muted-foreground" />
<p className="text-sm text-muted-foreground">...</p>
</div>
)}
</section>
{isEditing && (
<div className="flex items-center gap-3 pt-4 border-t">
<button
type="submit"
disabled={isSaving}
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" /></>}
</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>
</div>
)}
</form>
</CardContent>
</Card>
</div>
</div>
<MobileNav />
+124 -111
View File
@@ -9,6 +9,7 @@ import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { useAuthStore } from "@/store/auth";
import MobileNav from "@/components/layout/mobile-nav";
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { ThemeToggle } from "@/components/ui/theme-toggle";
@@ -44,9 +45,9 @@ function getPasswordStrength(password: string) {
if (/[0-9]/.test(password)) score++;
if (/[^a-zA-Z0-9]/.test(password)) score++;
if (score <= 2) return { label: "弱", color: "text-red-600", score };
if (score <= 4) return { label: "中", color: "text-yellow-600", score };
return { label: "强", color: "text-green-600", score };
if (score <= 2) return { label: "弱", color: "text-destructive", score };
if (score <= 4) return { label: "中", color: "text-amber-500", score };
return { label: "强", color: "text-emerald-500", score };
}
export default function SettingsPage() {
@@ -106,140 +107,152 @@ export default function SettingsPage() {
}
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">
{/* 状态提示 */}
{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}
</div>
)}
{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}
</div>
)}
<div className="max-w-2xl space-y-8">
<div className="space-y-6">
{/* 账户安全 */}
<section>
<div className="flex items-center gap-2 mb-4 pb-3 border-b">
<Shield className="w-4 h-4 text-primary" />
<h2 className="text-base font-semibold"></h2>
</div>
<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>}
<Card>
<CardHeader>
<div className="flex items-center gap-2">
<Shield className="w-5 h-5 text-primary" />
<CardTitle></CardTitle>
</div>
<div>
<Label htmlFor="new_password" className="text-sm text-muted-foreground"></Label>
<div className="relative mt-1">
<Input
id="new_password"
type={showNewPassword ? "text" : "password"}
{...register("new_password")}
className={errors.new_password ? "border-red-500" : ""}
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-red-500 w-1/3"
: passwordStrength.score <= 4 ? "bg-yellow-500 w-2/3"
: "bg-green-500 w-full"
}`}
<CardDescription></CardDescription>
</CardHeader>
<CardContent>
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<Label htmlFor="old_password" className="text-sm text-muted-foreground"></Label>
<div className="relative mt-1.5">
<Input
id="old_password"
type={showOldPassword ? "text" : "password"}
{...register("old_password")}
className={errors.old_password ? "border-destructive" : ""}
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-destructive mt-1">{errors.old_password.message}</p>}
</div>
)}
{errors.new_password && <p className="text-xs text-red-500 mt-1">{errors.new_password.message}</p>}
</div>
<div>
<Label htmlFor="confirm_password" className="text-sm text-muted-foreground"></Label>
<div className="relative mt-1">
<Input
id="confirm_password"
type={showConfirmPassword ? "text" : "password"}
{...register("confirm_password")}
className={errors.confirm_password ? "border-red-500" : ""}
placeholder="请再次输入新密码"
/>
<div>
<Label htmlFor="new_password" className="text-sm text-muted-foreground"></Label>
<div className="relative mt-1.5">
<Input
id="new_password"
type={showNewPassword ? "text" : "password"}
{...register("new_password")}
className={errors.new_password ? "border-destructive" : ""}
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
type="button"
className="absolute right-3 top-1/2 -translate-y-1/2"
onClick={() => setShowConfirmPassword(!showConfirmPassword)}
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"
>
{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>
</div>
{errors.confirm_password && <p className="text-xs text-red-500 mt-1">{errors.confirm_password.message}</p>}
</div>
<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>
</form>
</CardContent>
</Card>
{/* 偏好设置 */}
<section>
<div className="flex items-center gap-2 mb-4 pb-3 border-b">
<Palette className="w-4 h-4 text-primary" />
<h2 className="text-base font-semibold"></h2>
</div>
<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>
<Card>
<CardHeader>
<div className="flex items-center gap-2">
<Palette className="w-5 h-5 text-primary" />
<CardTitle></CardTitle>
</div>
<ThemeToggle />
</div>
</section>
<CardDescription>使</CardDescription>
</CardHeader>
<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>
<MobileNav />
@@ -120,7 +120,7 @@ export default function TextToImagePage() {
transition={{ duration: 0.5 }}
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
</h1>
<p className="text-lg text-gray-600 max-w-2xl mx-auto">
+1 -1
View File
@@ -130,7 +130,7 @@ export default function ForumCategoryPage() {
{isAuthenticated && (
<button
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" />
{showForm ? "收起" : "发帖"}
+1 -1
View File
@@ -185,7 +185,7 @@ export default function ForumPostPage() {
<button
type="submit"
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 ? (
<>
+1 -1
View File
@@ -188,7 +188,7 @@ export default function ChatInterface() {
"transition-all duration-150",
isStreaming
? "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 ? (
@@ -3,7 +3,6 @@
import { useState, useMemo } from "react";
import { Database, Globe, ChevronDown, ChevronRight, Star } from "lucide-react";
import { Button } from "@/components/ui/button";
import { ScrollArea } from "@/components/ui/scroll-area";
import { cn } from "@/lib/utils";
interface SourceReference {
@@ -249,9 +248,9 @@ export default function SourceReferences({
return (
<div className="mt-3 space-y-2">
{expanded ? (
<ScrollArea className="max-h-96">
<div className="pr-3">{sourceSection}</div>
</ScrollArea>
<div className="max-h-96 overflow-y-auto pr-1">
{sourceSection}
</div>
) : (
sourceSection
)}
+20 -31
View File
@@ -46,8 +46,8 @@ export default function MobileNav() {
return (
<>
{/* 移动端导航栏 */}
<div className="lg:hidden fixed bottom-0 left-0 right-0 bg-white border-t border-gray-200 mobile-safe-area z-50">
{/* Bottom navigation bar */}
<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">
{navItems.map((item) => {
const isActive = pathname === item.href;
@@ -58,7 +58,7 @@ export default function MobileNav() {
size="sm"
onClick={() => handleNavClick(item.href)}
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" />
@@ -69,48 +69,43 @@ export default function MobileNav() {
</div>
</div>
{/* 移动端菜单按钮 */}
{/* Menu toggle button */}
<div className="lg:hidden fixed top-4 right-4 z-50">
<Button
variant="outline"
size="icon"
<button
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" />}
</Button>
</button>
</div>
{/* 移动端侧边菜单 */}
{/* Side menu overlay */}
{isOpen && (
<div className="lg:hidden fixed inset-0 z-40">
{/* 遮罩 */}
<div
className="absolute inset-0 bg-black bg-opacity-50"
className="absolute inset-0 bg-black/50"
onClick={() => setIsOpen(false)}
/>
{/* 菜单内容 */}
<div className="absolute right-0 top-0 h-full w-80 max-w-[85vw] bg-white shadow-xl mobile-safe-area">
<div className="absolute right-0 top-0 h-full w-80 max-w-[85vw] bg-background shadow-xl mobile-safe-area">
<div className="flex flex-col h-full">
{/* 用户信息 */}
<div className="p-6 border-b border-gray-200">
{/* User info */}
<div className="p-6 border-b border-border">
<div className="flex items-center space-x-3">
<div className="w-12 h-12 bg-gray-200 rounded-full flex items-center justify-center">
<User className="w-6 h-6 text-gray-600" />
<div className="w-12 h-12 bg-muted rounded-full flex items-center justify-center">
<User className="w-6 h-6 text-muted-foreground" />
</div>
<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}
</p>
<p className="text-sm text-gray-500 truncate">
<p className="text-sm text-muted-foreground truncate">
{user?.email}
</p>
</div>
</div>
</div>
{/* 导航菜单 */}
{/* Navigation */}
<div className="flex-1 p-6">
<nav className="space-y-2">
{navItems.map((item) => {
@@ -130,8 +125,8 @@ export default function MobileNav() {
</nav>
</div>
{/* 底部操作 */}
<div className="p-6 border-t border-gray-200 space-y-2">
{/* Bottom actions */}
<div className="p-6 border-t border-border space-y-2">
{user?.is_superuser && (
<Button
variant="ghost"
@@ -161,7 +156,7 @@ export default function MobileNav() {
<Button
variant="ghost"
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" />
退
@@ -174,9 +169,3 @@ export default function MobileNav() {
</>
);
}
+2 -2
View File
@@ -91,8 +91,8 @@ export const useChatStore = create<ChatStore>((set, get) => ({
// 选择会话
selectSession: async (sessionId: number) => {
set({ isLoading: true, error: null });
set({ error: null });
try {
const session = get().sessions.find(s => s.id === sessionId);
if (!session) {