diff --git a/rust-engine/DEMODETAILS.md b/rust-engine/DEMODETAILS.md new file mode 100644 index 0000000..a8a2572 --- /dev/null +++ b/rust-engine/DEMODETAILS.md @@ -0,0 +1,48 @@ +## Demo Runbook: ISS Systems (3-minute showcase) + +This demo uses ~20 public NASA PDFs covering ISS Electrical Power, ECLSS, Avionics, and Structures. They live in `rust-engine/demo-data` and are automatically ingested via the server. + +### 1) Seed demo data (one-click) + +- Trigger ingestion (cloud): POST `/api/files/import-demo` (UI button available when `?debug=1` is present) +- The backend copies PDFs into storage, inserts DB rows with `pending_analysis = true`, and the FileWorker processes them. +- Processing pipeline per file: + - Gemini Flash → comprehensive description (facts/keywords/components) + - Gemini Pro → deep vector graph data (keywords/use cases/relationships) + - Embed + upsert to Qdrant, mark file ready (`pending_analysis = false`) + +Tip: You can list files at `GET /api/files/list`. Ready files will start to appear as analysis completes. + +### 2) Showcase flow (suggested script) + +1. “We ingested real ISS technical PDFs. The worker analyzes each file with Gemini and builds vector graph data for robust retrieval.” +2. Show the files list. Point out a couple of recognizable titles. +3. Run two queries (examples below) and open their results (the app calls `POST /api/query/create` then polls `/api/query/result`). +4. Highlight the grounded answer: ‘related_files’, ‘relationships’, and ‘final_answer’ fields. +5. Call out that if info isn’t present in the PDFs, the system explicitly states uncertainty (no guessing). + +### 3) Demo queries (pick 2–3) + +- Electrical Power System (EPS) + - “Trace the power path from the P6 solar array to the BCDU. Where are likely ground fault points?” + - “What is the role of the DC Switching Unit in array power management?” +- ECLSS + - “Which modules are part of water recovery, and how does the Oxygen Generator Assembly interface?” + - “Summarize the CDRA cycle and downstream subsystems it impacts.” +- C&DH / Avionics + - “In the US Lab, a blue/white wire connects to MDM ‘LAB1’. What are possible data pathways?” + - “Describe the onboard LAN segments and links to MDMs.” +- Structures / Robotics + - “Where does the Latching End Effector connect on S1 truss?” + - “What is the Mobile Transporter’s role in SSRMS operations?” + +### 4) Reset/refresh (optional) + +- POST `/api/files/import-demo?force=1` to overwrite by filename and re-queue analysis. + +### Appendix: Example sources + +- EPS: 20110014867, 20040171627, 19900007297, 20120002931, 20100029672 +- ECLSS: 20170008316, 20070019910, 20080039691, 20100029191, 20070019929 +- C&DH: 20000012543, 20100029690, 19950014639, 20010023477, 19980227289 +- Structures/Robotics: 20020054238, 20010035542, 20140001008, Destiny fact sheet, 20020088289 \ No newline at end of file diff --git a/rust-engine/README.md b/rust-engine/README.md index 32f377a..1f815c4 100644 --- a/rust-engine/README.md +++ b/rust-engine/README.md @@ -66,6 +66,14 @@ 1. docker compose up -d mysql qdrant 2. set env DATABASE_URL and QDRANT_URL 3. cargo run +4. (optional) import demo PDFs + - Ensure demo files are located in `rust-engine/demo-data` (default) or set `DEMO_DATA_DIR` env var to a folder containing PDFs. + - Call the endpoint: + - POST + - Optional query `?force=1` to overwrite existing by filename + - Or run the PowerShell helper: + - `./scripts/import_demo.ps1` (adds all PDFs in demo-data) + - `./scripts/import_demo.ps1 -Force` (overwrite existing) ## Notes diff --git a/rust-engine/demo-data/132-1-Final.pdf b/rust-engine/demo-data/132-1-Final.pdf new file mode 100644 index 0000000..ac0ce14 Binary files /dev/null and b/rust-engine/demo-data/132-1-Final.pdf differ diff --git a/rust-engine/demo-data/179225main_iss_poster_back.pdf b/rust-engine/demo-data/179225main_iss_poster_back.pdf new file mode 100644 index 0000000..9386c43 Binary files /dev/null and b/rust-engine/demo-data/179225main_iss_poster_back.pdf differ diff --git a/rust-engine/demo-data/19790004570.pdf b/rust-engine/demo-data/19790004570.pdf new file mode 100644 index 0000000..a0d22b5 Binary files /dev/null and b/rust-engine/demo-data/19790004570.pdf differ diff --git a/rust-engine/demo-data/19880012104.pdf b/rust-engine/demo-data/19880012104.pdf new file mode 100644 index 0000000..d7f48ea Binary files /dev/null and b/rust-engine/demo-data/19880012104.pdf differ diff --git a/rust-engine/demo-data/19890016674.pdf b/rust-engine/demo-data/19890016674.pdf new file mode 100644 index 0000000..0e315d6 Binary files /dev/null and b/rust-engine/demo-data/19890016674.pdf differ diff --git a/rust-engine/demo-data/19920015843.pdf b/rust-engine/demo-data/19920015843.pdf new file mode 100644 index 0000000..53065cf Binary files /dev/null and b/rust-engine/demo-data/19920015843.pdf differ diff --git a/rust-engine/demo-data/19950014639.pdf b/rust-engine/demo-data/19950014639.pdf new file mode 100644 index 0000000..5b200c2 Binary files /dev/null and b/rust-engine/demo-data/19950014639.pdf differ diff --git a/rust-engine/demo-data/20040171627.pdf b/rust-engine/demo-data/20040171627.pdf new file mode 100644 index 0000000..fe3eb42 Binary files /dev/null and b/rust-engine/demo-data/20040171627.pdf differ diff --git a/rust-engine/demo-data/20050207388.pdf b/rust-engine/demo-data/20050207388.pdf new file mode 100644 index 0000000..23ba263 Binary files /dev/null and b/rust-engine/demo-data/20050207388.pdf differ diff --git a/rust-engine/demo-data/20050210002.pdf b/rust-engine/demo-data/20050210002.pdf new file mode 100644 index 0000000..c7d6e7e Binary files /dev/null and b/rust-engine/demo-data/20050210002.pdf differ diff --git a/rust-engine/demo-data/20080014096.pdf b/rust-engine/demo-data/20080014096.pdf new file mode 100644 index 0000000..2a23fcf Binary files /dev/null and b/rust-engine/demo-data/20080014096.pdf differ diff --git a/rust-engine/demo-data/20100029672.pdf b/rust-engine/demo-data/20100029672.pdf new file mode 100644 index 0000000..d639021 Binary files /dev/null and b/rust-engine/demo-data/20100029672.pdf differ diff --git a/rust-engine/demo-data/20110014867.pdf b/rust-engine/demo-data/20110014867.pdf new file mode 100644 index 0000000..609a6df Binary files /dev/null and b/rust-engine/demo-data/20110014867.pdf differ diff --git a/rust-engine/demo-data/20120002931.pdf b/rust-engine/demo-data/20120002931.pdf new file mode 100644 index 0000000..deb27bf Binary files /dev/null and b/rust-engine/demo-data/20120002931.pdf differ diff --git a/rust-engine/demo-data/20190028718.pdf b/rust-engine/demo-data/20190028718.pdf new file mode 100644 index 0000000..d5211e8 Binary files /dev/null and b/rust-engine/demo-data/20190028718.pdf differ diff --git a/rust-engine/demo-data/20200003149.pdf b/rust-engine/demo-data/20200003149.pdf new file mode 100644 index 0000000..59ef117 Binary files /dev/null and b/rust-engine/demo-data/20200003149.pdf differ diff --git a/rust-engine/demo-data/473486main_iss_atcs_overview.pdf b/rust-engine/demo-data/473486main_iss_atcs_overview.pdf new file mode 100644 index 0000000..d742279 Binary files /dev/null and b/rust-engine/demo-data/473486main_iss_atcs_overview.pdf differ diff --git a/rust-engine/demo-data/8Mod6Prob1.pdf b/rust-engine/demo-data/8Mod6Prob1.pdf new file mode 100644 index 0000000..d719e74 Binary files /dev/null and b/rust-engine/demo-data/8Mod6Prob1.pdf differ diff --git a/rust-engine/demo-data/ICES_2023_311 final 5 15 23.pdf b/rust-engine/demo-data/ICES_2023_311 final 5 15 23.pdf new file mode 100644 index 0000000..e10f860 Binary files /dev/null and b/rust-engine/demo-data/ICES_2023_311 final 5 15 23.pdf differ diff --git a/rust-engine/demo-data/ISwSIS Software Standard_NASA-102020_Draft.docx.pdf b/rust-engine/demo-data/ISwSIS Software Standard_NASA-102020_Draft.docx.pdf new file mode 100644 index 0000000..62f2eec Binary files /dev/null and b/rust-engine/demo-data/ISwSIS Software Standard_NASA-102020_Draft.docx.pdf differ diff --git a/rust-engine/scripts/import_demo.ps1 b/rust-engine/scripts/import_demo.ps1 new file mode 100644 index 0000000..e69de29 diff --git a/rust-engine/src/api.rs b/rust-engine/src/api.rs index 55d1a06..c531f36 100644 --- a/rust-engine/src/api.rs +++ b/rust-engine/src/api.rs @@ -16,6 +16,17 @@ struct DeleteQuery { pub fn routes(pool: MySqlPool) -> impl Filter + Clone { let pool_filter = warp::any().map(move || pool.clone()); + // Import demo files from demo-data directory + let import_demo = warp::path!("files" / "import-demo") + .and(warp::post()) + .and( + warp::query::>() + .or(warp::any().map(|| std::collections::HashMap::new())) + .unify() + ) + .and(pool_filter.clone()) + .and_then(handle_import_demo); + // Upload file let upload = warp::path("files") .and(warp::post()) @@ -64,7 +75,7 @@ pub fn routes(pool: MySqlPool) -> impl Filter Result, pool: MySqlPool) -> Result { + use std::fs; + use std::path::PathBuf; + let force = params.get("force").map(|v| v == "1" || v.eq_ignore_ascii_case("true")).unwrap_or(false); + let demo_dir = std::env::var("DEMO_DATA_DIR").unwrap_or_else(|_| "demo-data".to_string()); + let base = std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")); + let src_dir = base.join(demo_dir); + if !src_dir.exists() { + return Ok(warp::reply::json(&serde_json::json!({ + "imported": 0, + "skipped": 0, + "error": format!("demo dir not found: {}", src_dir.display()) + }))); + } + let mut imported = 0; + let mut skipped = 0; + for entry in fs::read_dir(&src_dir).map_err(|_| warp::reject())? { + let entry = entry.map_err(|_| warp::reject())?; + let path = entry.path(); + if path.extension().and_then(|e| e.to_str()).map(|e| e.eq_ignore_ascii_case("pdf")).unwrap_or(false) { + let filename = path.file_name().and_then(|n| n.to_str()).unwrap_or("unknown.pdf").to_string(); + + // check if exists + if !force { + if let Some(_) = sqlx::query("SELECT id FROM files WHERE filename = ?") + .bind(&filename) + .fetch_optional(&pool) + .await + .map_err(|_| warp::reject())? { + skipped += 1; + continue; + } + } + + // read and save to storage + let data = fs::read(&path).map_err(|_| warp::reject())?; + let stored_path = storage::save_file(&filename, &data).map_err(|_| warp::reject())?; + + // insert or upsert db record + let id = uuid::Uuid::new_v4().to_string(); + if force { + let _ = sqlx::query("DELETE FROM files WHERE filename = ?") + .bind(&filename) + .execute(&pool) + .await; + } + sqlx::query("INSERT INTO files (id, filename, path, description, pending_analysis, analysis_status) VALUES (?, ?, ?, ?, ?, 'Queued')") + .bind(&id) + .bind(&filename) + .bind(stored_path.to_str().unwrap()) + .bind(Option::::None) + .bind(true) + .execute(&pool) + .await + .map_err(|e| { + tracing::error!("DB insert error: {}", e); + warp::reject() + })?; + imported += 1; + } + } + Ok(warp::reply::json(&serde_json::json!({ "imported": imported, "skipped": skipped }))) +} + async fn handle_delete(q: DeleteQuery, pool: MySqlPool) -> Result { if let Some(row) = sqlx::query("SELECT path FROM files WHERE id = ?") .bind(&q.id) @@ -143,7 +218,7 @@ async fn handle_delete(q: DeleteQuery, pool: MySqlPool) -> Result Result { - let rows = sqlx::query("SELECT id, filename, path, description FROM files ORDER BY created_at DESC LIMIT 500") + let rows = sqlx::query("SELECT id, filename, path, description, pending_analysis, analysis_status FROM files ORDER BY created_at DESC LIMIT 500") .fetch_all(&pool) .await .map_err(|e| { @@ -158,7 +233,16 @@ async fn handle_list(pool: MySqlPool) -> Result { let filename: String = r.get("filename"); let path: String = r.get("path"); let description: Option = r.get("description"); - serde_json::json!({"id": id, "filename": filename, "path": path, "description": description}) + let pending: bool = r.get("pending_analysis"); + let status: Option = r.try_get("analysis_status").ok(); + serde_json::json!({ + "id": id, + "filename": filename, + "path": path, + "description": description, + "pending_analysis": pending, + "analysis_status": status + }) }) .collect(); diff --git a/web-app/Dockerfile b/web-app/Dockerfile index 49f8ea8..9552c3f 100644 --- a/web-app/Dockerfile +++ b/web-app/Dockerfile @@ -6,4 +6,4 @@ RUN npm ci COPY . . RUN npm run format && npm run build EXPOSE 3000 -CMD ["npm", "run", "preview"] +CMD ["node", "server.mjs"] diff --git a/web-app/jsconfig.json b/web-app/jsconfig.json index c48ffbb..604c23f 100644 --- a/web-app/jsconfig.json +++ b/web-app/jsconfig.json @@ -1,6 +1,7 @@ { "compilerOptions": { - "baseUrl": "./" + "baseUrl": "./", + "lib": ["es2015", "dom"] }, "include": ["src"] } diff --git a/web-app/server.mjs b/web-app/server.mjs new file mode 100644 index 0000000..cca7665 --- /dev/null +++ b/web-app/server.mjs @@ -0,0 +1,44 @@ +import express from 'express'; +import path from 'node:path'; +import helmet from 'helmet'; +import cors from 'cors'; +import { fileURLToPath } from 'node:url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +const app = express(); +const PORT = process.env.PORT || 3000; +const RUST_ENGINE_BASE = process.env.RUST_ENGINE_BASE || 'http://rust-engine:8000'; + +app.use(helmet()); +app.use(cors()); +app.use(express.json()); + +// Proxy minimal API needed by the UI to the rust-engine container +app.post('/api/files/import-demo', async (req, res) => { + try { + const qs = req.url.includes('?') ? req.url.substring(req.url.indexOf('?')) : ''; + const url = `${RUST_ENGINE_BASE}/api/files/import-demo${qs}`; + const upstream = await fetch(url, { method: 'POST' }); + const text = await upstream.text(); + res.status(upstream.status).type(upstream.headers.get('content-type') || 'application/json').send(text); + } catch (err) { + console.error('import-demo proxy failed:', err); + res.status(502).json({ error: 'proxy_failed' }); + } +}); + +// Serve static frontend +const distDir = path.resolve(__dirname, 'dist'); +app.use(express.static(distDir)); + +// SPA fallback +app.get('*', (req, res) => { + res.sendFile(path.join(distDir, 'index.html')); +}); + +app.listen(PORT, '0.0.0.0', () => { + console.log(`Web app server listening on http://0.0.0.0:${PORT}`); + console.log(`Proxying to rust engine at ${RUST_ENGINE_BASE}`); +}); diff --git a/web-app/src/components/ui/chat/chat-header.jsx b/web-app/src/components/ui/chat/chat-header.jsx index 89b5738..47bec26 100644 --- a/web-app/src/components/ui/chat/chat-header.jsx +++ b/web-app/src/components/ui/chat/chat-header.jsx @@ -1,14 +1,54 @@ -import React from "react"; +import React, { useMemo, useState } from "react"; +import { motion } from "motion/react"; +import { Rocket } from "lucide-react"; export default function ChatHeader({ title = "Title of Chat" }) { + const isDebug = useMemo(() => { + const p = new URLSearchParams(window.location.search); + return p.get("debug") === "1"; + }, []); + const [ingesting, setIngesting] = useState(false); + const [toast, setToast] = useState(""); + + async function triggerDemoIngest() { + try { + setIngesting(true); + const res = await fetch("/api/files/import-demo", { method: "POST" }); + const json = await res.json().catch(() => ({})); + setToast(`Imported: ${json.imported ?? "?"}, Skipped: ${json.skipped ?? "?"}`); + setTimeout(() => setToast(""), 4000); + } catch (e) { + setToast("Import failed"); + setTimeout(() => setToast(""), 4000); + } finally { + setIngesting(false); + } + } return (
-
+

{title}

+ {isDebug && ( + + + {ingesting ? "Seeding…" : "Seed Demo Data"} + + )}
+ {toast && ( +
+ {toast} +
+ )}
);