From 5340bf9844fe032c133d1261049f229e0d4c4177 Mon Sep 17 00:00:00 2001 From: Victor Giers Date: Wed, 4 Feb 2026 06:56:56 +0100 Subject: [PATCH] auto-git: [add] src/App.tsx --- src/App.tsx | 893 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 893 insertions(+) create mode 100644 src/App.tsx diff --git a/src/App.tsx b/src/App.tsx new file mode 100644 index 0000000..5c992dd --- /dev/null +++ b/src/App.tsx @@ -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(action: string, payload: Record): Promise { + 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([]); + 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 [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]); + + 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 = 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 = {}; + 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("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("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 +
+