working through the plan + UI/ UX

This commit is contained in:
itsamejms
2026-07-12 22:13:43 +01:00
parent 5b256242be
commit a24f3615e0
38 changed files with 5295 additions and 328 deletions
+176
View File
@@ -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>
);
}