auto-git:

[add] src/App.tsx
This commit is contained in:
2026-02-04 06:56:56 +01:00
parent 0bd23bdfec
commit 5340bf9844

893
src/App.tsx Normal file
View File

@@ -0,0 +1,893 @@
import { useEffect, useMemo, useState } from "react";
import { open } from "@tauri-apps/plugin-dialog";
import { invoke } from "@tauri-apps/api/core";
import { getCurrent } from "@tauri-apps/api/webviewWindow";
const IMAGE_PROMPT_PLACEHOLDER = "Generated image prompt will appear here.";
type FileEntry = {
kind: "file";
path: string;
name: string;
type: string;
size: string;
include: boolean;
};
type UrlEntry = {
kind: "url";
url: string;
name: string;
type: "url";
size: string;
include: boolean;
};
type RowEntry = FileEntry | UrlEntry;
type RephraseVariant = {
key: string;
label: string;
text: string;
};
type SessionSummary = {
title: string;
description: string;
saved_at: number;
};
type PriorArtResult = {
url: string;
title?: string;
score?: number;
snippet?: string;
};
type PriorArtResponse = {
queries: string[];
lang?: string;
results: PriorArtResult[];
};
async function runBackend<T>(action: string, payload: Record<string, unknown>): Promise<T> {
return (await invoke("run_python_action", { action, payload })) as T;
}
function formatTime(ts: number): string {
if (!ts) return "";
const d = new Date(ts * 1000);
return d.toLocaleString();
}
function rowId(row: RowEntry): string {
return row.kind === "file" ? `file:${row.path}` : `url:${row.url}`;
}
export default function App() {
const [status, setStatus] = useState("Ready");
const [files, setFiles] = useState<FileEntry[]>([]);
const [websites, setWebsites] = useState<UrlEntry[]>([]);
const [selectedRows, setSelectedRows] = useState<Set<string>>(new Set());
const [notes, setNotes] = useState("");
const [rephraseVariants, setRephraseVariants] = useState<RephraseVariant[]>([]);
const [rephraseSelected, setRephraseSelected] = useState<string | null>(null);
const [concept, setConcept] = useState("");
const [title, setTitle] = useState("");
const [description, setDescription] = useState("");
const [imagePrompt, setImagePrompt] = useState(IMAGE_PROMPT_PLACEHOLDER);
const [ollamaHost, setOllamaHost] = useState("http://localhost:11434");
const [ollamaModel, setOllamaModel] = useState("Select model...");
const [gitRemote, setGitRemote] = useState("");
const [searxUrl, setSearxUrl] = useState("http://localhost:8888");
const [models, setModels] = useState<string[]>([]);
const [busy, setBusy] = useState<Record<string, boolean>>({});
const [sessionModalOpen, setSessionModalOpen] = useState(false);
const [sessions, setSessions] = useState<SessionSummary[]>([]);
const [priorModalOpen, setPriorModalOpen] = useState(false);
const [priorData, setPriorData] = useState<PriorArtResponse | null>(null);
const rows = useMemo<RowEntry[]>(() => [...files, ...websites], [files, websites]);
useEffect(() => {
let mounted = true;
const bootstrap = async () => {
try {
const settings = await runBackend<Record<string, string>>("load_settings", {});
if (!mounted) return;
if (settings.ollama_host) setOllamaHost(settings.ollama_host);
if (settings.ollama_model) setOllamaModel(settings.ollama_model);
if (settings.git_remote_url) setGitRemote(settings.git_remote_url);
if (settings.searx_url) setSearxUrl(settings.searx_url);
} catch {
// ignore
}
try {
const list = await runBackend<string[]>("list_models", {});
if (!mounted) return;
setModels(list);
} catch {
// ignore
}
};
void bootstrap();
return () => {
mounted = false;
};
}, []);
useEffect(() => {
const timer = setTimeout(() => {
runBackend("save_settings", {
settings: {
ollama_host: ollamaHost,
ollama_model: ollamaModel,
git_remote_url: gitRemote,
searx_url: searxUrl,
},
}).catch(() => undefined);
}, 400);
return () => clearTimeout(timer);
}, [ollamaHost, ollamaModel, gitRemote, searxUrl]);
useEffect(() => {
let unlisten: (() => void) | undefined;
const attach = async () => {
try {
const current = getCurrent();
unlisten = await current.onFileDropEvent((event) => {
if (event.payload.type === "drop") {
const paths = event.payload.paths ?? [];
if (paths.length) {
void addFilesByPath(paths, true);
}
}
});
} catch {
// optional
}
};
void attach();
return () => {
if (unlisten) unlisten();
};
}, [files]);
const includeMap = useMemo(() => {
const map: Record<string, boolean> = {};
files.forEach((f) => (map[f.path] = f.include));
websites.forEach((w) => (map[w.url] = w.include));
return map;
}, [files, websites]);
const addFilesByPath = async (paths: string[], expandDirs: boolean) => {
if (!paths.length) return;
setStatus("Indexing files...");
try {
const stats = await runBackend<{ name: string; path: string; type: string; size: string }[]>(
"stat_paths",
{ paths, expand_dirs: expandDirs }
);
setFiles((prev) => {
const existing = new Map(prev.map((f) => [f.path, f]));
stats.forEach((item) => {
if (!existing.has(item.path)) {
existing.set(item.path, {
kind: "file",
path: item.path,
name: item.name,
type: item.type,
size: item.size,
include: true,
});
}
});
return Array.from(existing.values());
});
setStatus(`Added ${stats.length} item(s)`);
} catch (err) {
setStatus("Failed to add files");
console.error(err);
}
};
const onAddFiles = async () => {
const selection = await open({ multiple: true, title: "Select files" });
if (!selection) return;
const paths = Array.isArray(selection) ? selection : [selection];
await addFilesByPath(paths as string[], false);
};
const onAddFolder = async () => {
const selection = await open({ directory: true, title: "Select folder" });
if (!selection) return;
const paths = Array.isArray(selection) ? selection : [selection];
await addFilesByPath(paths as string[], true);
};
const onAddWebsite = () => {
const url = window.prompt("Enter a URL (starting with http:// or https://):");
if (!url) return;
const trimmed = url.trim();
if (!/^https?:\/\//i.test(trimmed)) {
window.alert("Please enter a valid http(s) URL.");
return;
}
setWebsites((prev) => {
if (prev.some((w) => w.url === trimmed)) return prev;
const name = trimmed.replace(/^https?:\/\//, "").slice(0, 60);
return [
...prev,
{
kind: "url",
url: trimmed,
name,
type: "url",
size: "web",
include: true,
},
];
});
};
const onRemoveSelected = () => {
if (!selectedRows.size) return;
setFiles((prev) => prev.filter((f) => !selectedRows.has(rowId(f))));
setWebsites((prev) => prev.filter((w) => !selectedRows.has(rowId(w))));
setSelectedRows(new Set());
};
const onClearAll = () => {
setFiles([]);
setWebsites([]);
setSelectedRows(new Set());
};
const toggleRowSelection = (row: RowEntry) => {
const id = rowId(row);
setSelectedRows((prev) => {
const next = new Set(prev);
if (next.has(id)) {
next.delete(id);
} else {
next.add(id);
}
return next;
});
};
const toggleInclude = (row: RowEntry) => {
if (row.kind === "file") {
setFiles((prev) =>
prev.map((f) => (f.path === row.path ? { ...f, include: !f.include } : f))
);
} else {
setWebsites((prev) =>
prev.map((w) => (w.url === row.url ? { ...w, include: !w.include } : w))
);
}
};
const onRephrase = async () => {
if (!ollamaModel || ollamaModel === "Select model...") {
window.alert("Please select a model first.");
return;
}
if (!notes.trim()) {
window.alert("Add some notes to rephrase.");
return;
}
setBusy((prev) => ({ ...prev, rephrase: true }));
setStatus("Rephrasing...");
try {
const variants = await runBackend<RephraseVariant[]>("rephrase", {
note: notes,
ollama_host: ollamaHost,
model: ollamaModel,
});
setRephraseVariants(variants);
setRephraseSelected(variants[0]?.key ?? null);
setStatus("Rephrase complete");
} catch (err) {
setStatus("Rephrase failed");
console.error(err);
window.alert("Rephrase failed. Check console for details.");
} finally {
setBusy((prev) => ({ ...prev, rephrase: false }));
}
};
const onExtend = async () => {
if (!ollamaModel || ollamaModel === "Select model...") {
window.alert("Please select a model first.");
return;
}
if (!notes.trim()) {
window.alert("Add some notes to extend.");
return;
}
setBusy((prev) => ({ ...prev, extend: true }));
setStatus("Extending...");
try {
const extension = await runBackend<string>("extend", {
note: notes,
ollama_host: ollamaHost,
model: ollamaModel,
});
setNotes((prev) => (prev.trim() ? `${prev}\n${extension}` : extension));
setStatus("Extension ready");
} catch (err) {
setStatus("Extend failed");
console.error(err);
window.alert("Extend failed. Check console for details.");
} finally {
setBusy((prev) => ({ ...prev, extend: false }));
}
};
const onGenerateConcept = async () => {
if (!ollamaModel || ollamaModel === "Select model...") {
window.alert("Please select a model first.");
return;
}
if (!notes.trim() && !files.length && !websites.length) {
window.alert("Add files, websites, or notes first.");
return;
}
setBusy((prev) => ({ ...prev, generate: true }));
setStatus("Generating concept...");
try {
const result = await runBackend<{ concept: string; title: string; description: string }>(
"generate_concept",
{
notes,
files: files.map((f) => f.path),
websites: websites.map((w) => w.url),
include_map: includeMap,
ollama_host: ollamaHost,
model: ollamaModel,
}
);
setConcept(result.concept);
setTitle(result.title);
setDescription(result.description);
setStatus("Concept generated");
} catch (err) {
setStatus("Generation failed");
console.error(err);
window.alert("Generation failed. Check console for details.");
} finally {
setBusy((prev) => ({ ...prev, generate: false }));
}
};
const onGenerateImagePrompt = async () => {
if (!ollamaModel || ollamaModel === "Select model...") {
window.alert("Please select a model first.");
return;
}
const ideaText = [title ? `Title: ${title}` : "", description ? `Description: ${description}` : "", concept || notes]
.filter(Boolean)
.join("\n\n");
if (!ideaText.trim()) {
window.alert("Add concept text or notes first.");
return;
}
setBusy((prev) => ({ ...prev, imagePrompt: true }));
setStatus("Generating image prompt...");
try {
const promptText = await runBackend<string>("generate_image_prompt", {
idea_text: ideaText,
ollama_host: ollamaHost,
model: ollamaModel,
});
setImagePrompt(promptText);
setStatus("Image prompt ready");
} catch (err) {
setStatus("Image prompt failed");
console.error(err);
window.alert("Image prompt failed. Check console for details.");
} finally {
setBusy((prev) => ({ ...prev, imagePrompt: false }));
}
};
const onGenerateImage = async () => {
if (!imagePrompt.trim() || imagePrompt === IMAGE_PROMPT_PLACEHOLDER) {
window.alert("Generate an image prompt first.");
return;
}
const output = await open({ directory: true, title: "Select output folder" });
if (!output) return;
const outputDir = Array.isArray(output) ? output[0] : output;
setBusy((prev) => ({ ...prev, imageGen: true }));
setStatus("Generating image...");
try {
const result = await runBackend<{ output_path: string }>("generate_image", {
prompt: imagePrompt,
output_dir: outputDir,
title,
});
setStatus(`Image saved: ${result.output_path}`);
window.alert(`Image saved to:\n${result.output_path}`);
} catch (err) {
setStatus("Image generation failed");
console.error(err);
window.alert("Image generation failed. Check console for details.");
} finally {
setBusy((prev) => ({ ...prev, imageGen: false }));
}
};
const onPriorArt = async () => {
if (!ollamaModel || ollamaModel === "Select model...") {
window.alert("Please select a model first.");
return;
}
setBusy((prev) => ({ ...prev, prior: true }));
setStatus("Searching prior art...");
try {
const data = await runBackend<PriorArtResponse>("prior_art", {
notes,
files: files.map((f) => f.path),
websites: websites.map((w) => w.url),
include_map: includeMap,
ollama_host: ollamaHost,
model: ollamaModel,
searx_url: searxUrl,
});
setPriorData(data);
setPriorModalOpen(true);
setStatus("Prior art ready");
} catch (err) {
setStatus("Prior art failed");
console.error(err);
window.alert("Prior art search failed. Check console for details.");
} finally {
setBusy((prev) => ({ ...prev, prior: false }));
}
};
const onPreview = async () => {
if (!concept.trim()) {
window.alert("Generate or paste concept text first.");
return;
}
setBusy((prev) => ({ ...prev, preview: true }));
setStatus("Preparing preview...");
try {
const result = await runBackend<{ ok: boolean; pdf_path: string; log_path: string }>("preview_pdf", {
concept,
title,
files: files.map((f) => f.path),
include_map: includeMap,
});
if (result.ok) {
setStatus("Preview PDF ready");
window.alert(`Preview PDF saved to:\n${result.pdf_path}`);
} else {
setStatus("Preview failed");
window.alert(`Preview failed. Log: ${result.log_path}`);
}
} catch (err) {
setStatus("Preview failed");
console.error(err);
window.alert("Preview failed. Check console for details.");
} finally {
setBusy((prev) => ({ ...prev, preview: false }));
}
};
const onPush = async () => {
if (!title.trim() || !description.trim()) {
window.alert("Title and Description are required.");
return;
}
if (!concept.trim()) {
window.alert("Generate or paste concept text first.");
return;
}
const repoDir = await open({ directory: true, title: "Select concepts repo folder" });
if (!repoDir) return;
const repoPath = Array.isArray(repoDir) ? repoDir[0] : repoDir;
setBusy((prev) => ({ ...prev, push: true }));
setStatus("Pushing to repo...");
try {
const result = await runBackend<{
repo_dir: string;
concept_dir: string;
pdf_ok: boolean;
pdf_log: string;
pushed: boolean;
}>("push_repo", {
title,
description,
concept,
files: files.map((f) => f.path),
include_map: includeMap,
git_remote_url: gitRemote,
repo_dir: repoPath,
});
if (!result.pdf_ok) {
window.alert(`PDF export failed. Log: ${result.pdf_log}`);
}
setStatus(result.pushed ? "Pushed to remote" : "Committed locally");
window.alert(`Concept saved to:\n${result.concept_dir}`);
} catch (err) {
setStatus("Push failed");
console.error(err);
window.alert("Push failed. Check console for details.");
} finally {
setBusy((prev) => ({ ...prev, push: false }));
}
};
const onNewSession = () => {
if (notes || concept || files.length || websites.length) {
const ok = window.confirm("Clear the current session?");
if (!ok) return;
}
setFiles([]);
setWebsites([]);
setSelectedRows(new Set());
setNotes("");
setRephraseVariants([]);
setRephraseSelected(null);
setConcept("");
setTitle("");
setDescription("");
setImagePrompt(IMAGE_PROMPT_PLACEHOLDER);
};
const onSaveSession = async () => {
if (!title.trim()) {
window.alert("Please enter a Title before saving.");
return;
}
try {
const existing = await runBackend<SessionSummary[]>("list_sessions", {});
const exists = existing.some((s) => s.title === title.trim());
if (exists) {
const ok = window.confirm(`A session titled "${title}" exists. Overwrite it?`);
if (!ok) return;
}
await runBackend("save_session", {
payload: {
title,
description,
notes,
concept,
files: files.map((f) => ({ path: f.path, include: f.include })),
websites: websites.map((w) => ({ url: w.url, include: w.include })),
rephrase_variants: rephraseVariants,
rephrase_selected_key: rephraseSelected,
image_prompt: imagePrompt === IMAGE_PROMPT_PLACEHOLDER ? "" : imagePrompt,
},
allow_overwrite: true,
});
setStatus("Session saved");
} catch (err) {
console.error(err);
window.alert("Failed to save session.");
}
};
const onOpenSession = async () => {
try {
const list = await runBackend<SessionSummary[]>("list_sessions", {});
setSessions(list.sort((a, b) => (b.saved_at || 0) - (a.saved_at || 0)));
setSessionModalOpen(true);
} catch (err) {
console.error(err);
window.alert("Failed to load sessions.");
}
};
const applySession = async (sessionTitle: string) => {
try {
const data = await runBackend<any>("load_session", { title: sessionTitle });
if (!data) return;
setTitle(data.title || "");
setDescription(data.description || "");
setNotes(data.notes || "");
setConcept(data.concept || "");
setFiles(
(data.files || []).map((f: any) => ({
kind: "file",
path: f.path,
name: f.path.split(/[\\/]/).pop() || f.path,
type: (f.path.split(".").pop() || "file").toLowerCase(),
size: "?",
include: f.include ?? true,
}))
);
setWebsites(
(data.websites || []).map((w: any) => ({
kind: "url",
url: w.url,
name: w.url.replace(/^https?:\/\//, "").slice(0, 60),
type: "url",
size: "web",
include: w.include ?? true,
}))
);
setRephraseVariants(data.rephrase_variants || []);
setRephraseSelected(data.rephrase_selected_key || null);
setImagePrompt(data.image_prompt || IMAGE_PROMPT_PLACEHOLDER);
setSessionModalOpen(false);
setStatus("Session loaded");
} catch (err) {
console.error(err);
window.alert("Failed to open session.");
}
};
return (
<div className="app">
<div className="top-bar">
<div className="top-actions">
<button className="ghost" onClick={onNewSession}>New</button>
<button className="ghost" onClick={onOpenSession}>Open</button>
<button className="ghost" onClick={onSaveSession}>Save</button>
</div>
<div className="status-pill">{status}</div>
</div>
<div className="panes">
<div className="column">
<section className="panel">
<div className="panel-title">
<h2>Files & Websites</h2>
<span>Drag & drop files; add URLs</span>
</div>
<div className="table">
<div className="table-header">
<div>Name</div>
<div>Path</div>
<div>Type</div>
<div>Size</div>
<div>Add to Repo</div>
</div>
<div className="table-body">
{rows.length === 0 && (
<div className="table-row" style={{ gridTemplateColumns: "1fr" }}>
<div style={{ color: "#8b7a6a" }}>Drop files here or use the buttons below.</div>
</div>
)}
{rows.map((row) => (
<div
key={rowId(row)}
className={`table-row ${selectedRows.has(rowId(row)) ? "selected" : ""}`}
onClick={() => toggleRowSelection(row)}
>
<div>{row.name}</div>
<div>{row.kind === "file" ? row.path : row.url}</div>
<div>{row.type}</div>
<div>{row.size}</div>
<div
onClick={(event) => {
event.stopPropagation();
toggleInclude(row);
}}
>
<div className={`checkbox ${row.include ? "checked" : ""}`}>{row.include ? "✓" : ""}</div>
</div>
</div>
))}
</div>
</div>
<div className="controls">
<button onClick={onAddFiles}>Add Files</button>
<button onClick={onAddFolder}>Add Folder</button>
<button onClick={onAddWebsite}>Add Website</button>
<button onClick={onRemoveSelected} className="danger">Remove Selected</button>
<button onClick={onClearAll} className="danger">Clear All</button>
</div>
</section>
<section className="panel" style={{ flex: 1 }}>
<div className="panel-title">
<h2>Notes / Thoughts</h2>
<span>Scratchpad for raw ideas</span>
</div>
<textarea
className="notes-area"
value={notes}
onChange={(e) => setNotes(e.target.value)}
placeholder="Drop in fragments, problem statements, or constraints..."
/>
{rephraseVariants.length > 0 && (
<div className="rephrase-panel">
<strong style={{ fontSize: "12px", color: "#5b4c41" }}>Rephrase variants</strong>
<div className="rephrase-list">
{rephraseVariants.map((variant) => (
<div
key={variant.key}
className={`rephrase-item ${rephraseSelected === variant.key ? "active" : ""}`}
onClick={() => {
setRephraseSelected(variant.key);
setNotes(variant.text);
}}
>
<div style={{ fontWeight: 600 }}>{variant.label}</div>
<div style={{ fontSize: "11px", color: "#6d5f52" }}>
{variant.text.slice(0, 120)}{variant.text.length > 120 ? "..." : ""}
</div>
</div>
))}
</div>
</div>
)}
<div className="controls">
<button onClick={onRephrase} disabled={busy.rephrase}>Rephrase</button>
<button onClick={onExtend} disabled={busy.extend}>Extend</button>
<button className="primary" onClick={onGenerateConcept} disabled={busy.generate}>Generate Concept</button>
<button className="secondary" onClick={onPriorArt} disabled={busy.prior}>Find Prior Art</button>
</div>
</section>
</div>
<div className="column">
<section className="panel" style={{ flex: 1 }}>
<div className="panel-title">
<h2>Concept (Editable)</h2>
<span>Refine and publish</span>
</div>
<div className="concept-meta">
<input value={title} onChange={(e) => setTitle(e.target.value)} placeholder="Title" />
<input value={description} onChange={(e) => setDescription(e.target.value)} placeholder="Description" />
</div>
<textarea
value={concept}
onChange={(e) => setConcept(e.target.value)}
placeholder="Generated concept markdown appears here..."
style={{ flex: 1, minHeight: 240 }}
/>
<div className="controls">
<button onClick={onPreview} disabled={busy.preview}>Preview PDF</button>
<button className="primary" onClick={onPush} disabled={busy.push}>Push to Repo</button>
<button onClick={onGenerateImagePrompt} disabled={busy.imagePrompt}>Generate image prompt</button>
<button onClick={onGenerateImage} disabled={busy.imageGen}>Generate Image</button>
</div>
<textarea
className="image-prompt"
value={imagePrompt}
onChange={(e) => setImagePrompt(e.target.value)}
placeholder={IMAGE_PROMPT_PLACEHOLDER}
/>
</section>
</div>
</div>
<div className="bottom-bar">
<div className="input-group">
<label>Ollama host</label>
<input value={ollamaHost} onChange={(e) => setOllamaHost(e.target.value)} />
</div>
<div className="input-group">
<label>Model</label>
<select value={ollamaModel} onChange={(e) => setOllamaModel(e.target.value)}>
<option value="Select model...">Select model...</option>
{models.map((m) => (
<option key={m} value={m}>
{m}
</option>
))}
</select>
</div>
<div className="input-group">
<label>Remote repo</label>
<input value={gitRemote} onChange={(e) => setGitRemote(e.target.value)} placeholder="https://..." />
</div>
<div className="input-group">
<label>SearXNG URL</label>
<input value={searxUrl} onChange={(e) => setSearxUrl(e.target.value)} />
</div>
</div>
{sessionModalOpen && (
<div
style={{
position: "fixed",
inset: 0,
background: "rgba(31, 26, 22, 0.45)",
display: "grid",
placeItems: "center",
zIndex: 10,
}}
>
<div
style={{
background: "#fffaf3",
padding: "20px",
borderRadius: "16px",
width: "min(560px, 90vw)",
maxHeight: "80vh",
overflow: "auto",
boxShadow: "0 20px 50px rgba(0,0,0,0.2)",
}}
>
<h3 style={{ marginTop: 0 }}>Open Session</h3>
{sessions.length === 0 && <p>No saved sessions yet.</p>}
<div style={{ display: "flex", flexDirection: "column", gap: "10px" }}>
{sessions.map((s) => (
<button
key={s.title}
className="ghost"
style={{ textAlign: "left", borderRadius: "12px" }}
onClick={() => applySession(s.title)}
>
<strong>{s.title}</strong>
<div style={{ fontSize: "12px", color: "#6d5f52" }}>{s.description}</div>
<div style={{ fontSize: "11px", color: "#8b7a6a" }}>{formatTime(s.saved_at)}</div>
</button>
))}
</div>
<div style={{ marginTop: "16px", textAlign: "right" }}>
<button onClick={() => setSessionModalOpen(false)}>Close</button>
</div>
</div>
</div>
)}
{priorModalOpen && priorData && (
<div
style={{
position: "fixed",
inset: 0,
background: "rgba(31, 26, 22, 0.45)",
display: "grid",
placeItems: "center",
zIndex: 10,
}}
>
<div
style={{
background: "#fffaf3",
padding: "20px",
borderRadius: "16px",
width: "min(680px, 92vw)",
maxHeight: "85vh",
overflow: "auto",
boxShadow: "0 20px 50px rgba(0,0,0,0.2)",
}}
>
<h3 style={{ marginTop: 0 }}>Prior Art Results</h3>
<div style={{ marginBottom: "12px" }}>
<strong>Queries</strong>
<ul style={{ marginTop: "6px" }}>
{priorData.queries.map((q) => (
<li key={q} style={{ fontSize: "12px", color: "#5b4c41" }}>
{q}
</li>
))}
</ul>
</div>
{priorData.results.length === 0 && <p>No results found.</p>}
<div style={{ display: "flex", flexDirection: "column", gap: "12px" }}>
{priorData.results.map((r) => (
<div key={r.url} style={{ border: "1px solid #eadfce", padding: "12px", borderRadius: "12px" }}>
<div style={{ fontWeight: 600, marginBottom: "4px" }}>{r.title || r.url}</div>
<div style={{ fontSize: "12px", color: "#6d5f52" }}>{r.url}</div>
{r.snippet && (
<div style={{ marginTop: "6px", fontSize: "12px", color: "#5b4c41" }}>{r.snippet}</div>
)}
{typeof r.score === "number" && (
<div style={{ marginTop: "4px", fontSize: "11px", color: "#8b7a6a" }}>Score: {r.score.toFixed(3)}</div>
)}
</div>
))}
</div>
<div style={{ marginTop: "16px", textAlign: "right" }}>
<button onClick={() => setPriorModalOpen(false)}>Close</button>
</div>
</div>
</div>
)}
</div>
);
}