加了一堆模型和一堆功能
This commit is contained in:
233
art-agent/frontend/src/components/profile-modal.tsx
Normal file
233
art-agent/frontend/src/components/profile-modal.tsx
Normal file
@@ -0,0 +1,233 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState, useCallback } from "react";
|
||||
import { useAuth, type AuthUser } from "@/lib/auth-context";
|
||||
|
||||
const API_URL = process.env.NEXT_PUBLIC_API_URL || "http://localhost:8000";
|
||||
|
||||
interface MemoryItem {
|
||||
id: string;
|
||||
memory: string;
|
||||
created_at?: string;
|
||||
updated_at?: string;
|
||||
}
|
||||
|
||||
interface GroupedMemories {
|
||||
label: string;
|
||||
items: MemoryItem[];
|
||||
}
|
||||
|
||||
function groupByTime(memories: MemoryItem[]): GroupedMemories[] {
|
||||
const now = new Date();
|
||||
const todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
||||
const weekAgo = new Date(todayStart.getTime() - 7 * 24 * 60 * 60 * 1000);
|
||||
|
||||
const today: MemoryItem[] = [];
|
||||
const week: MemoryItem[] = [];
|
||||
const older: MemoryItem[] = [];
|
||||
|
||||
for (const m of memories) {
|
||||
const d = m.created_at ? new Date(m.created_at) : null;
|
||||
if (!d || isNaN(d.getTime())) {
|
||||
older.push(m);
|
||||
} else if (d >= todayStart) {
|
||||
today.push(m);
|
||||
} else if (d >= weekAgo) {
|
||||
week.push(m);
|
||||
} else {
|
||||
older.push(m);
|
||||
}
|
||||
}
|
||||
|
||||
const groups: GroupedMemories[] = [];
|
||||
if (today.length > 0) groups.push({ label: "今天", items: today });
|
||||
if (week.length > 0) groups.push({ label: "最近 7 天", items: week });
|
||||
if (older.length > 0) groups.push({ label: "更早", items: older });
|
||||
return groups;
|
||||
}
|
||||
|
||||
function formatDate(dateStr?: string): string {
|
||||
if (!dateStr) return "";
|
||||
const d = new Date(dateStr);
|
||||
if (isNaN(d.getTime())) return "";
|
||||
const now = new Date();
|
||||
const isThisYear = d.getFullYear() === now.getFullYear();
|
||||
const month = String(d.getMonth() + 1).padStart(2, "0");
|
||||
const day = String(d.getDate()).padStart(2, "0");
|
||||
const hour = String(d.getHours()).padStart(2, "0");
|
||||
const min = String(d.getMinutes()).padStart(2, "0");
|
||||
if (isThisYear) return `${month}-${day} ${hour}:${min}`;
|
||||
return `${d.getFullYear()}-${month}-${day} ${hour}:${min}`;
|
||||
}
|
||||
|
||||
export function ProfileModal({ onClose }: { onClose: () => void }) {
|
||||
const { user, token } = useAuth();
|
||||
const [memories, setMemories] = useState<MemoryItem[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const fetchMemories = useCallback(async () => {
|
||||
if (!token) return;
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const resp = await fetch(`${API_URL}/api/memory/list`, {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
});
|
||||
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
|
||||
const data = await resp.json();
|
||||
setMemories(data.memories || []);
|
||||
if (data.error) setError(data.error);
|
||||
} catch (e) {
|
||||
setError(e instanceof Error ? e.message : "加载记忆失败");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [token]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchMemories();
|
||||
}, [fetchMemories]);
|
||||
|
||||
useEffect(() => {
|
||||
function handleKey(e: KeyboardEvent) {
|
||||
if (e.key === "Escape") onClose();
|
||||
}
|
||||
document.addEventListener("keydown", handleKey);
|
||||
return () => document.removeEventListener("keydown", handleKey);
|
||||
}, [onClose]);
|
||||
|
||||
if (!user) return null;
|
||||
|
||||
const groups = groupByTime(memories);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="fixed inset-0 z-[100] flex items-center justify-center"
|
||||
onClick={onClose}
|
||||
>
|
||||
{/* 遮罩层 */}
|
||||
<div className="absolute inset-0 bg-black/60 backdrop-blur-sm" />
|
||||
|
||||
{/* 弹窗主体 */}
|
||||
<div
|
||||
className="relative w-[90vw] max-w-[560px] max-h-[80vh] rounded-2xl glass-panel
|
||||
shadow-[0_0_40px_rgba(77,184,164,0.1)] border-[var(--border-glow)]
|
||||
flex flex-col overflow-hidden"
|
||||
style={{ animation: "slideUp 200ms ease-out" }}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{/* 关闭按钮 */}
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="absolute top-3 right-3 p-1.5 rounded-lg text-[var(--text-secondary)]
|
||||
hover:text-[var(--text-primary)] hover:bg-[var(--bg-tertiary)]
|
||||
transition-colors cursor-pointer z-10"
|
||||
>
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||||
<path d="M18 6L6 18M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
{/* 用户信息区 */}
|
||||
<div className="px-6 pt-6 pb-4 flex items-center gap-4 flex-shrink-0">
|
||||
<div className="w-14 h-14 rounded-full bg-[var(--accent)]/15 border-2 border-[var(--accent)]/40
|
||||
flex items-center justify-center text-[var(--accent)] text-xl font-bold
|
||||
shadow-[0_0_20px_rgba(77,184,164,0.12)]">
|
||||
{(user.display_name || user.username).charAt(0).toUpperCase()}
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<h2 className="text-lg font-semibold text-[var(--text-primary)] truncate">
|
||||
{user.display_name || user.username}
|
||||
</h2>
|
||||
<div className="flex items-center gap-2 mt-0.5">
|
||||
<span className="text-sm text-[var(--text-secondary)]">@{user.username}</span>
|
||||
{user.is_admin && (
|
||||
<span className="text-xs px-1.5 py-0.5 rounded-md bg-[var(--accent)]/10
|
||||
text-[var(--accent)] border border-[var(--accent)]/30">
|
||||
管理员
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 分割线 */}
|
||||
<div className="mx-6 h-px bg-[var(--border)]" />
|
||||
|
||||
{/* 记忆区标题 */}
|
||||
<div className="px-6 pt-4 pb-2 flex items-center gap-2 flex-shrink-0">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="var(--accent)" strokeWidth="2">
|
||||
<path d="M12 2a7 7 0 017 7c0 2.38-1.19 4.47-3 5.74V17a2 2 0 01-2 2h-4a2 2 0 01-2-2v-2.26C6.19 13.47 5 11.38 5 9a7 7 0 017-7z" />
|
||||
<path d="M10 21h4" />
|
||||
</svg>
|
||||
<span className="text-sm font-medium text-[var(--text-primary)]">记忆系统</span>
|
||||
{!loading && (
|
||||
<span className="text-xs text-[var(--text-secondary)]">
|
||||
{memories.length} 条记录
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 记忆列表 */}
|
||||
<div className="flex-1 overflow-y-auto px-6 pb-6 min-h-0">
|
||||
{loading && (
|
||||
<div className="flex items-center justify-center py-12">
|
||||
<div className="w-5 h-5 border-2 border-[var(--accent)]/30 border-t-[var(--accent)]
|
||||
rounded-full animate-spin" />
|
||||
<span className="ml-3 text-sm text-[var(--text-secondary)]">加载中...</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!loading && error && memories.length === 0 && (
|
||||
<div className="text-center py-12">
|
||||
<p className="text-sm text-[var(--hot)]">{error}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!loading && !error && memories.length === 0 && (
|
||||
<div className="text-center py-12">
|
||||
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="var(--text-secondary)"
|
||||
strokeWidth="1.5" className="mx-auto mb-3 opacity-50">
|
||||
<path d="M12 2a7 7 0 017 7c0 2.38-1.19 4.47-3 5.74V17a2 2 0 01-2 2h-4a2 2 0 01-2-2v-2.26C6.19 13.47 5 11.38 5 9a7 7 0 017-7z" />
|
||||
<path d="M10 21h4" />
|
||||
</svg>
|
||||
<p className="text-sm text-[var(--text-secondary)]">暂无记忆记录</p>
|
||||
<p className="text-xs text-[var(--text-secondary)] mt-1 opacity-60">
|
||||
与助手对话时,系统会自动记录关键信息
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!loading && groups.map((group) => (
|
||||
<div key={group.label} className="mb-4 last:mb-0">
|
||||
<div className="text-xs font-medium text-[var(--text-secondary)] uppercase tracking-wider mb-2">
|
||||
{group.label}
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
{group.items.map((item) => (
|
||||
<div
|
||||
key={item.id}
|
||||
className="px-3 py-2.5 rounded-lg bg-[var(--bg-tertiary)]/50
|
||||
border border-[var(--border)] hover:border-[var(--border-glow)]
|
||||
transition-colors group"
|
||||
>
|
||||
<p className="text-sm text-[var(--text-primary)] leading-relaxed">
|
||||
{item.memory}
|
||||
</p>
|
||||
{item.created_at && (
|
||||
<p className="text-xs text-[var(--text-secondary)] mt-1 opacity-0
|
||||
group-hover:opacity-60 transition-opacity">
|
||||
{formatDate(item.created_at)}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user