init: initial commit

This commit is contained in:
Blizzard
2026-04-01 14:09:33 +08:00
commit aef2e152dc
66 changed files with 6540 additions and 0 deletions
+124
View File
@@ -0,0 +1,124 @@
import { useEffect, useRef, useState } from 'react';
import { CreateLibrary, DeleteLibrary, ImportCSV, ListLibraries, SwitchLibrary } from 'wailsjs/go/main/App';
import type { handler } from 'wailsjs/go/models';
interface LibraryBarProps {
activeName: string;
onSwitch: (name: string) => void;
}
export default function LibraryBar({ activeName, onSwitch }: LibraryBarProps) {
const [open, setOpen] = useState(false);
const [libs, setLibs] = useState<handler.LibraryInfo[]>([]);
const [creating, setCreating] = useState(false);
const [newName, setNewName] = useState('');
const [importing, setImporting] = useState(false);
const [msg, setMsg] = useState('');
const ref = useRef<HTMLDivElement>(null);
const load = async () => { setLibs(await ListLibraries()); };
useEffect(() => {
if (open) load();
}, [open]);
// Close on outside click
useEffect(() => {
const handler = (e: MouseEvent) => {
if (ref.current && !ref.current.contains(e.target as Node)) setOpen(false);
};
document.addEventListener('mousedown', handler);
return () => document.removeEventListener('mousedown', handler);
}, []);
const handleSwitch = async (name: string) => {
await SwitchLibrary(name);
onSwitch(name);
setOpen(false);
};
const handleCreate = async () => {
if (!newName.trim()) return;
const err = await CreateLibrary(newName.trim(), '');
if (!err) { onSwitch(newName.trim()); setNewName(''); setCreating(false); load(); }
else setMsg(err);
};
const handleImport = async () => {
setImporting(true); setMsg('');
const result = await ImportCSV();
setImporting(false);
if (result.error && result.error !== '已取消') setMsg(result.error);
else if (result.imported > 0) { setMsg(`✓ 导入了 ${result.imported} 条(跳过 ${result.skipped} 条)`); load(); }
setTimeout(() => setMsg(''), 4000);
};
return (
<div ref={ref} className="relative px-4 pb-2">
{/* Button row */}
<div className="flex items-center gap-2">
<button onClick={() => setOpen(o => !o)}
className="flex-1 flex items-center gap-2 px-3 py-1.5 rounded-xl
bg-white/5 hover:bg-white/10 border border-white/8 transition-all text-left">
<span className="text-sm">📚</span>
<span className="text-[12px] text-white/80 truncate flex-1">{activeName || '选择知识库'}</span>
<span className="text-[10px] text-white/30">{open ? '▲' : '▾'}</span>
</button>
<button onClick={handleImport} disabled={importing} title="导入 CSV"
className="px-2.5 py-1.5 rounded-xl bg-accent/15 text-accent-light border border-accent/25
hover:bg-accent/25 transition-all text-[11px] font-medium disabled:opacity-50">
{importing ? '…' : '📥 导入'}
</button>
</div>
{msg && <p className={`text-[10px] mt-1 ${msg.startsWith('✓') ? 'text-green-400' : 'text-red-400'}`}>{msg}</p>}
{/* Dropdown */}
{open && (
<div className="absolute left-4 right-4 top-full mt-1 z-40
bg-[rgba(12,10,24,0.98)] border border-white/10 rounded-xl
shadow-2xl overflow-hidden animate-fade-in">
<div className="max-h-48 overflow-y-auto results-scroll">
{libs.map(lib => (
<div key={lib.id}
className={`flex items-center gap-2 px-3 py-2.5 cursor-pointer transition-colors
${lib.is_active ? 'bg-accent/15 border-l-2 border-accent' : 'hover:bg-white/5'}`}
onClick={() => handleSwitch(lib.name)}>
<span className="text-sm">{lib.is_active ? '✅' : '📁'}</span>
<div className="flex-1 min-w-0">
<p className="text-[12px] text-white/85 truncate">{lib.name}</p>
<p className="text-[10px] text-white/35">{lib.entry_count} </p>
</div>
{!lib.is_active && (
<button onClick={e => { e.stopPropagation(); DeleteLibrary(lib.name); load(); }}
className="text-white/20 hover:text-red-400 text-xs transition-colors"></button>
)}
</div>
))}
</div>
{/* Create new */}
<div className="border-t border-white/8 p-2">
{creating ? (
<div className="flex gap-2">
<input autoFocus className="flex-1 search-input text-[11px]" placeholder="知识库名称"
value={newName} onChange={e => setNewName(e.target.value)}
onKeyDown={e => e.key === 'Enter' && handleCreate()}
style={{ paddingLeft: '10px', paddingRight: '10px' }} />
<button onClick={handleCreate}
className="px-3 py-1.5 rounded-lg bg-accent text-white text-[11px]"></button>
<button onClick={() => setCreating(false)} className="text-white/30 text-xs"></button>
</div>
) : (
<button onClick={() => setCreating(true)}
className="w-full flex items-center gap-2 px-2 py-1.5 rounded-lg
text-[11px] text-white/50 hover:text-white/70 hover:bg-white/5 transition-all">
<span>+</span>
</button>
)}
</div>
</div>
)}
</div>
);
}