import { useEffect, useMemo, useState } from "react"; import { open } from "@tauri-apps/plugin-dialog"; import { invoke } from "@tauri-apps/api/core"; import { getCurrentWebview } from "@tauri-apps/api/webview"; import { markdownToHTML } from "./markdown/markdown"; import "./markdown/markdown-render.css"; 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(action: string, payload: Record): Promise { return (await invoke("run_python_action", { action, payload })) as T; } async function openPath(path: string): Promise { await invoke("open_path", { path }); } 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([]); const [websites, setWebsites] = useState([]); const [selectedRows, setSelectedRows] = useState>(new Set()); const [notes, setNotes] = useState(""); const [rephraseVariants, setRephraseVariants] = useState([]); const [rephraseSelected, setRephraseSelected] = useState(null); const [concept, setConcept] = useState(""); const [markdownPreview, setMarkdownPreview] = useState(false); 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([]); const [busy, setBusy] = useState>({}); const [sessionModalOpen, setSessionModalOpen] = useState(false); const [sessions, setSessions] = useState([]); const [priorModalOpen, setPriorModalOpen] = useState(false); const [priorData, setPriorData] = useState(null); const rows = useMemo(() => [...files, ...websites], [files, websites]); const markdownPreviewHtml = useMemo( () => (markdownPreview ? markdownToHTML(concept) : ""), [concept, markdownPreview] ); useEffect(() => { let mounted = true; const bootstrap = async () => { try { const settings = await runBackend>("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("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 = getCurrentWebview() as any; unlisten = await current.onDragDropEvent((event: any) => { 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 = {}; 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("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("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("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("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("Opening preview PDF..."); try { await openPath(result.pdf_path); setStatus("Preview PDF opened"); } catch (openErr) { setStatus("Preview PDF ready"); console.error(openErr); window.alert(`Preview PDF saved to:\n${result.pdf_path}\n\nCould not open it automatically.`); } } 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("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("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("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 (
{status}

Files & Websites

Drag & drop files; add URLs
Name
Path
Type
Size
Add to Repo
{rows.length === 0 && (
Drop files here or use the buttons below.
)} {rows.map((row) => (
toggleRowSelection(row)} >
{row.name}
{row.kind === "file" ? row.path : row.url}
{row.type}
{row.size}
{ event.stopPropagation(); toggleInclude(row); }} >
{row.include ? "✓" : ""}
))}

Notes / Thoughts

Scratchpad for raw ideas