- Miuix day/night dual theme CSS variables - themeStore: light default, follows prefers-color-scheme - Header: sun/moon toggle button - All components migrated to CSS variables - Cyrene avatar image replaces 'C' logo everywhere - Favicon added
102 lines
4.5 KiB
TypeScript
102 lines
4.5 KiB
TypeScript
import { useState, useCallback } from 'react'
|
|
import { api, getToken } from '../../api/client'
|
|
|
|
interface MemoryEntry {
|
|
id: string; user_id: string; content: string; summary: string;
|
|
keywords: string[]; category: string; priority: number;
|
|
}
|
|
|
|
export default function MemoryPage() {
|
|
const [memories, setMemories] = useState<MemoryEntry[]>([])
|
|
const [search, setSearch] = useState('')
|
|
const [loading, setLoading] = useState(false)
|
|
const [newContent, setNewContent] = useState('')
|
|
|
|
const fetchMemories = useCallback(async (q?: string) => {
|
|
setLoading(true)
|
|
try {
|
|
const token = getToken()
|
|
const path = q ? `/api/v1/memory/search?q=${encodeURIComponent(q)}` : '/api/v1/memory'
|
|
const res = await fetch(path, { headers: { Authorization: `Bearer ${token}` } })
|
|
const data = await res.json()
|
|
setMemories(Array.isArray(data?.data) ? data.data : [])
|
|
} catch { } finally { setLoading(false) }
|
|
}, [])
|
|
|
|
const handleAdd = async () => {
|
|
if (!newContent.trim()) return
|
|
const token = getToken()
|
|
await fetch('/api/v1/memory', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` },
|
|
body: JSON.stringify({ content: newContent, category: 'fact', priority: 0.5 }),
|
|
})
|
|
setNewContent('')
|
|
fetchMemories(search || undefined)
|
|
}
|
|
|
|
const handleDelete = async (id: string) => {
|
|
const token = getToken()
|
|
await fetch(`/api/v1/memory/${id}`, { method: 'DELETE', headers: { Authorization: `Bearer ${token}` } })
|
|
fetchMemories(search || undefined)
|
|
}
|
|
|
|
return (
|
|
<div className="p-6 space-y-4">
|
|
<h2 className="text-lg font-semibold">记忆管理</h2>
|
|
|
|
{/* Search */}
|
|
<div className="flex gap-2">
|
|
<input
|
|
type="text" value={search}
|
|
onChange={e => setSearch(e.target.value)}
|
|
onKeyDown={e => { if (e.key === 'Enter') fetchMemories(search) }}
|
|
placeholder="搜索记忆..."
|
|
className="flex-1 bg-[var(--mx-surface)] border border-[var(--mx-outline)] focus:border-[var(--mx-primary)] rounded-[14px] px-4 py-2.5 text-sm text-[var(--mx-text)] placeholder:text-[var(--mx-text-dim)] outline-none"
|
|
/>
|
|
<button onClick={() => fetchMemories(search)} className="px-4 py-2.5 bg-[var(--mx-primary)] hover:bg-[var(--mx-primary-hover)] text-white text-sm font-medium rounded-full">搜索</button>
|
|
<button onClick={() => { setSearch(''); fetchMemories() }} className="px-4 py-2.5 bg-[var(--mx-surface)] border border-[var(--mx-outline)] text-[var(--mx-text-dim)] text-sm rounded-full">全部</button>
|
|
</div>
|
|
|
|
{/* Add */}
|
|
<div className="flex gap-2">
|
|
<input
|
|
type="text" value={newContent}
|
|
onChange={e => setNewContent(e.target.value)}
|
|
placeholder="添加新记忆..."
|
|
className="flex-1 bg-[var(--mx-surface)] border border-[var(--mx-outline)] focus:border-[var(--mx-primary)] rounded-[14px] px-4 py-2.5 text-sm text-[var(--mx-text)] placeholder:text-[var(--mx-text-dim)] outline-none"
|
|
/>
|
|
<button onClick={handleAdd} disabled={!newContent.trim()}
|
|
className="px-4 py-2.5 bg-[var(--mx-primary)] hover:bg-[var(--mx-primary-hover)] disabled:bg-[#253E64] text-white text-sm font-medium rounded-full"
|
|
>添加</button>
|
|
</div>
|
|
|
|
{/* List */}
|
|
{loading ? (
|
|
<p className="text-center text-[var(--mx-text-dim)] py-8 text-sm">加载中...</p>
|
|
) : memories.length === 0 ? (
|
|
<p className="text-center text-[var(--mx-text-dim)] py-8 text-sm">暂无记忆</p>
|
|
) : (
|
|
<div className="space-y-2">
|
|
{memories.map(m => (
|
|
<div key={m.id} className="bg-[var(--mx-surface)] border border-[var(--mx-outline)] rounded-[18px] px-4 py-3 flex items-start justify-between">
|
|
<div className="flex-1 min-w-0">
|
|
<p className="text-sm text-[var(--mx-text)] truncate">{m.content}</p>
|
|
<div className="flex items-center gap-2 mt-1">
|
|
<span className="text-xs px-1.5 py-0.5 rounded bg-[var(--mx-badge-blue)]/50 text-[var(--mx-tertiary-text)]">{m.category}</span>
|
|
{m.keywords?.slice(0, 3).map((kw, i) => (
|
|
<span key={i} className="text-xs text-[var(--mx-text-dim)]">{kw}</span>
|
|
))}
|
|
</div>
|
|
</div>
|
|
<button onClick={() => handleDelete(m.id)}
|
|
className="text-xs text-[var(--mx-error)]/70 hover:text-[var(--mx-error)] shrink-0 ml-3 mt-0.5"
|
|
>删除</button>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|