working through the plan + UI/ UX
This commit is contained in:
@@ -0,0 +1,176 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { Trash2 } from "lucide-react";
|
||||
|
||||
interface RagSource {
|
||||
source: string;
|
||||
chunks: number;
|
||||
}
|
||||
|
||||
interface RagHit {
|
||||
text: string;
|
||||
source: string;
|
||||
score: number;
|
||||
}
|
||||
|
||||
export function LorePanel() {
|
||||
const [source, setSource] = useState("");
|
||||
const [text, setText] = useState("");
|
||||
const [sources, setSources] = useState<RagSource[]>([]);
|
||||
const [adding, setAdding] = useState(false);
|
||||
const [msg, setMsg] = useState("");
|
||||
|
||||
const [query, setQuery] = useState("");
|
||||
const [hits, setHits] = useState<RagHit[]>([]);
|
||||
const [searching, setSearching] = useState(false);
|
||||
|
||||
async function loadSources() {
|
||||
try {
|
||||
setSources(await invoke<RagSource[]>("rag_list"));
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
loadSources();
|
||||
}, []);
|
||||
|
||||
async function add() {
|
||||
if (!source.trim() || !text.trim()) return;
|
||||
setAdding(true);
|
||||
setMsg("");
|
||||
try {
|
||||
const n = await invoke<number>("rag_add", { req: { source, text } });
|
||||
setMsg(`Indexed ${n} chunk${n === 1 ? "" : "s"}`);
|
||||
setText("");
|
||||
loadSources();
|
||||
} catch (e) {
|
||||
setMsg(String(e));
|
||||
}
|
||||
setAdding(false);
|
||||
setTimeout(() => setMsg(""), 3000);
|
||||
}
|
||||
|
||||
async function clearOne(s: string) {
|
||||
await invoke("rag_clear", { source: s });
|
||||
loadSources();
|
||||
}
|
||||
|
||||
async function clearAll() {
|
||||
await invoke("rag_clear", { source: null });
|
||||
loadSources();
|
||||
}
|
||||
|
||||
async function search() {
|
||||
if (!query.trim()) return;
|
||||
setSearching(true);
|
||||
try {
|
||||
setHits(await invoke<RagHit[]>("rag_search", { query, topK: 5 }));
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
setSearching(false);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4 text-sm">
|
||||
{/* Add lore */}
|
||||
<div className="flex flex-col gap-2">
|
||||
<h3 className="font-heading text-[var(--color-gold-bright)] text-sm">Add to Lore</h3>
|
||||
<input
|
||||
className="rounded-lg bg-[var(--color-bg-surface)] border border-[var(--color-border-glass)] px-3 py-1.5 text-[var(--color-text-primary)] placeholder-[var(--color-text-dim)] focus:outline-none focus:border-[var(--color-gold-bright)] text-sm"
|
||||
value={source}
|
||||
onChange={(e) => setSource(e.target.value)}
|
||||
placeholder="Source name (e.g. World Bible, Session 3 Notes)"
|
||||
/>
|
||||
<textarea
|
||||
className="rounded-lg bg-[var(--color-bg-surface)] border border-[var(--color-border-glass)] px-3 py-2 text-[var(--color-text-primary)] placeholder-[var(--color-text-dim)] focus:outline-none focus:border-[var(--color-gold-bright)] text-sm min-h-40 resize-y"
|
||||
value={text}
|
||||
onChange={(e) => setText(e.target.value)}
|
||||
placeholder="Paste lore text. It will be chunked on paragraphs, embedded via nomic-embed-text, and stored locally."
|
||||
/>
|
||||
<button
|
||||
onClick={add}
|
||||
disabled={adding}
|
||||
className="rounded-lg bg-[var(--color-gold-bright)] text-[var(--color-bg-deep)] px-4 py-2 text-sm font-semibold hover:bg-[var(--color-gold-muted)] transition-colors cursor-pointer disabled:opacity-50"
|
||||
>
|
||||
{adding ? "Embedding…" : "Add to Lore"}
|
||||
</button>
|
||||
{msg && <span className="text-xs text-[var(--color-text-secondary)]">{msg}</span>}
|
||||
</div>
|
||||
|
||||
{/* Sources */}
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="font-heading text-[var(--color-gold-bright)] text-sm">Indexed Sources</h3>
|
||||
{sources.length > 0 && (
|
||||
<button
|
||||
onClick={clearAll}
|
||||
className="text-xs text-[var(--color-text-dim)] hover:text-[var(--color-danger)] cursor-pointer transition-colors"
|
||||
>
|
||||
clear all
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
{sources.length === 0 ? (
|
||||
<p className="text-xs text-[var(--color-text-dim)]">No lore indexed yet.</p>
|
||||
) : (
|
||||
<ul className="flex flex-col gap-1">
|
||||
{sources.map((s) => (
|
||||
<li key={s.source} className="flex items-center justify-between rounded-lg bg-[var(--color-bg-surface)] border border-[var(--color-border-glass)] px-3 py-1.5">
|
||||
<span className="text-[var(--color-text-primary)] text-xs truncate">
|
||||
{s.source} <span className="text-[var(--color-text-dim)]">· {s.chunks} chunks</span>
|
||||
</span>
|
||||
<button
|
||||
onClick={() => clearOne(s.source)}
|
||||
className="text-[var(--color-text-dim)] hover:text-[var(--color-danger)] cursor-pointer transition-colors shrink-0"
|
||||
title="Clear this source"
|
||||
>
|
||||
<Trash2 size={14} />
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Test retrieval */}
|
||||
<div className="flex flex-col gap-2 border-t border-[var(--color-border-subtle)] pt-3">
|
||||
<h3 className="font-heading text-[var(--color-gold-bright)] text-sm">Test Retrieval</h3>
|
||||
<div className="flex gap-2">
|
||||
<input
|
||||
className="flex-1 rounded-lg bg-[var(--color-bg-surface)] border border-[var(--color-border-glass)] px-3 py-1.5 text-[var(--color-text-primary)] placeholder-[var(--color-text-dim)] focus:outline-none focus:border-[var(--color-gold-bright)] text-sm"
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
onKeyDown={(e) => e.key === "Enter" && search()}
|
||||
placeholder="Ask something your lore should answer…"
|
||||
/>
|
||||
<button
|
||||
onClick={search}
|
||||
disabled={searching}
|
||||
className="rounded-lg bg-[var(--color-gold-bright)] text-[var(--color-bg-deep)] px-3 py-1.5 text-sm font-semibold hover:bg-[var(--color-gold-muted)] transition-colors cursor-pointer disabled:opacity-50"
|
||||
>
|
||||
{searching ? "…" : "Search"}
|
||||
</button>
|
||||
</div>
|
||||
{hits.length > 0 && (
|
||||
<ul className="flex flex-col gap-2">
|
||||
{hits.map((h, i) => (
|
||||
<li key={i} className="rounded-lg bg-[var(--color-bg-surface)] border border-[var(--color-border-glass)] p-2">
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<span className="text-[10px] text-[var(--color-gold-bright)]">{h.source}</span>
|
||||
<span className="text-[10px] text-[var(--color-text-dim)]">score {h.score.toFixed(3)}</span>
|
||||
</div>
|
||||
<p className="text-xs text-[var(--color-text-primary)] leading-relaxed whitespace-pre-wrap">{h.text}</p>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
{hits.length === 0 && query && !searching && (
|
||||
<p className="text-xs text-[var(--color-text-dim)]">No matches.</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user