Spaces:
Sleeping
Sleeping
Download frontend/src/App.jsx from HFPRXV/autoinventory: direct link, hf CLI and curl.
- Browser
- Download file 8.26 kB
-
https://huggingface.co/spaces/HFPRXV/autoinventory/resolve/main/frontend/src/App.jsx
- Command line
-
hf download hf://spaces/HFPRXV/autoinventory/frontend/src/App.jsx
-
curl -L -o App.jsx https://huggingface.co/spaces/HFPRXV/autoinventory/resolve/main/frontend/src/App.jsx
8.26 kB
| import { useEffect, useMemo, useRef, useState } from "react"; | |
| const ACCEPTED_TYPES = ["video/mp4", "video/quicktime"]; | |
| const STAGES = [ | |
| { id: 1, label: "Frame extraction with FFmpeg", minSec: 0 }, | |
| { id: 2, label: "Visual inspection with local vision model", minSec: 8 }, | |
| { id: 3, label: "Executive report generation", minSec: 50 }, | |
| ]; | |
| const initialState = { | |
| file: null, | |
| isDragging: false, | |
| isLoading: false, | |
| status: "Drop your walkthrough video", | |
| error: null | |
| }; | |
| function formatTime(seconds) { | |
| const m = Math.floor(seconds / 60); | |
| const s = seconds % 60; | |
| if (m > 0) return `${m}m ${s}s`; | |
| return `${s}s`; | |
| } | |
| function getCurrentStageIndex(elapsed) { | |
| let idx = 0; | |
| for (let i = STAGES.length - 1; i >= 0; i--) { | |
| if (elapsed >= STAGES[i].minSec) { idx = i; break; } | |
| } | |
| return idx; | |
| } | |
| export default function App() { | |
| const [state, setState] = useState(initialState); | |
| const [elapsed, setElapsed] = useState(0); | |
| const intervalRef = useRef(null); | |
| const stageIndex = getCurrentStageIndex(elapsed); | |
| useEffect(() => { | |
| if (state.isLoading) { | |
| setElapsed(0); | |
| intervalRef.current = setInterval(() => { | |
| setElapsed((prev) => prev + 1); | |
| }, 1000); | |
| } else { | |
| if (intervalRef.current) clearInterval(intervalRef.current); | |
| } | |
| return () => { | |
| if (intervalRef.current) clearInterval(intervalRef.current); | |
| }; | |
| }, [state.isLoading]); | |
| const filename = useMemo(() => { | |
| if (!state.file) return "No file selected"; | |
| return state.file.name; | |
| }, [state.file]); | |
| const onDragOver = (event) => { | |
| event.preventDefault(); | |
| setState((prev) => ({ ...prev, isDragging: true })); | |
| }; | |
| const onDragLeave = () => { | |
| setState((prev) => ({ ...prev, isDragging: false })); | |
| }; | |
| const onDrop = (event) => { | |
| event.preventDefault(); | |
| const droppedFile = event.dataTransfer.files[0]; | |
| handleFile(droppedFile); | |
| }; | |
| const onFileChange = (event) => { | |
| const selectedFile = event.target.files[0]; | |
| handleFile(selectedFile); | |
| }; | |
| const handleFile = (file) => { | |
| if (!file) return; | |
| if (!ACCEPTED_TYPES.includes(file.type)) { | |
| setState((prev) => ({ | |
| ...prev, | |
| error: "Only .mp4 or .mov files are supported.", | |
| file: null | |
| })); | |
| return; | |
| } | |
| setState((prev) => ({ | |
| ...prev, | |
| file, | |
| error: null | |
| })); | |
| }; | |
| const submitFile = async () => { | |
| if (!state.file || state.isLoading) return; | |
| setState((prev) => ({ | |
| ...prev, | |
| isLoading: true, | |
| status: "Uploading and analyzing...", | |
| error: null | |
| })); | |
| const formData = new FormData(); | |
| formData.append("file", state.file); | |
| try { | |
| const response = await fetch("/api/generate-report", { | |
| method: "POST", | |
| body: formData | |
| }); | |
| if (!response.ok) { | |
| const payload = await response.json().catch(() => ({})); | |
| throw new Error(payload.detail || "Upload failed"); | |
| } | |
| const blob = await response.blob(); | |
| const url = window.URL.createObjectURL(blob); | |
| const anchor = document.createElement("a"); | |
| anchor.href = url; | |
| anchor.download = "autoinventory-report.pdf"; | |
| document.body.appendChild(anchor); | |
| anchor.click(); | |
| anchor.remove(); | |
| window.URL.revokeObjectURL(url); | |
| setState((prev) => ({ | |
| ...prev, | |
| isLoading: false, | |
| status: "Report ready for download" | |
| })); | |
| } catch (error) { | |
| setState((prev) => ({ | |
| ...prev, | |
| isLoading: false, | |
| error: error.message || "Something went wrong" | |
| })); | |
| } | |
| }; | |
| return ( | |
| <div className="min-h-screen bg-ink-950 text-sand-50"> | |
| <div className="absolute inset-0 bg-[radial-gradient(circle_at_top,_rgba(196,123,59,0.2),_transparent_55%)]" /> | |
| <div className="relative mx-auto flex min-h-screen max-w-6xl flex-col gap-10 px-6 py-16"> | |
| <header className="space-y-4"> | |
| <p className="text-sm uppercase tracking-[0.2em] text-copper-500"> | |
| AutoInventory | |
| </p> | |
| <h1 className="font-display text-4xl font-semibold md:text-5xl"> | |
| Instant inventory reports from a single walkthrough | |
| </h1> | |
| <p className="max-w-2xl text-lg text-sand-100"> | |
| Drag and drop a one-minute property video. Our agentic pipeline extracts rooms, | |
| fixtures, and conditions into a professional inventory report. | |
| </p> | |
| </header> | |
| <section className="grid gap-6 md:grid-cols-[1.2fr_0.8fr]"> | |
| <div | |
| className={`group flex min-h-[260px] cursor-pointer flex-col items-center justify-center rounded-3xl border border-dashed px-6 text-center transition ${ | |
| state.isDragging | |
| ? "border-copper-500 bg-ink-900" | |
| : state.isLoading | |
| ? "border-copper-500/40 bg-ink-900" | |
| : "border-ink-700 bg-ink-900/60" | |
| }`} | |
| onDragOver={onDragOver} | |
| onDragLeave={onDragLeave} | |
| onDrop={onDrop} | |
| role="button" | |
| tabIndex={0} | |
| onClick={() => !state.isLoading && document.getElementById("file-input").click()} | |
| > | |
| {state.isLoading ? ( | |
| <> | |
| <div className="mb-4 flex items-center gap-3"> | |
| <svg className="animate-spin h-8 w-8 text-copper-500" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"> | |
| <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" /> | |
| <path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" /> | |
| </svg> | |
| <span className="font-display text-4xl font-semibold text-copper-400 tabular-nums"> | |
| {formatTime(elapsed)} | |
| </span> | |
| </div> | |
| <p className="text-sm text-sand-100">{state.status}</p> | |
| </> | |
| ) : ( | |
| <> | |
| <div className="mb-4 h-12 w-12 rounded-2xl bg-ink-700/50" /> | |
| <p className="font-display text-xl">{state.status}</p> | |
| <p className="mt-2 text-sm text-sand-100">MP4 or MOV, up to 1 minute.</p> | |
| </> | |
| )} | |
| <input | |
| id="file-input" | |
| type="file" | |
| accept=".mp4,.mov" | |
| className="hidden" | |
| onChange={onFileChange} | |
| /> | |
| </div> | |
| <div className="rounded-3xl border border-ink-700 bg-ink-900/80 p-6 shadow-lift"> | |
| <div className="space-y-4"> | |
| <div> | |
| <p className="text-xs uppercase tracking-[0.2em] text-sand-100">Selected</p> | |
| <p className="mt-2 text-lg font-semibold text-sand-50">{filename}</p> | |
| </div> | |
| <div className="rounded-2xl bg-ink-700/50 p-4 text-sm text-sand-100"> | |
| <p className="mb-2 font-semibold text-sand-50">Processing stages</p> | |
| <ul className="space-y-1"> | |
| {STAGES.map((s, i) => ( | |
| <li key={s.id} className={`transition-colors ${state.isLoading && i <= stageIndex ? "text-copper-400" : i < stageIndex ? "text-sand-50" : ""}`}> | |
| {state.isLoading && i < stageIndex ? "✓ " : state.isLoading && i === stageIndex ? "▸ " : ""} | |
| {s.id}. {s.label} | |
| </li> | |
| ))} | |
| </ul> | |
| </div> | |
| <button | |
| className="w-full rounded-full bg-copper-500 py-3 text-sm font-semibold text-ink-950 transition hover:bg-copper-600 disabled:cursor-not-allowed disabled:bg-ink-700" | |
| disabled={!state.file || state.isLoading} | |
| onClick={submitFile} | |
| > | |
| {state.isLoading ? `Processing... (${formatTime(elapsed)})` : "Generate report"} | |
| </button> | |
| {state.error && ( | |
| <p className="rounded-lg border border-red-400/40 bg-red-500/10 p-3 text-sm text-red-200"> | |
| {state.error} | |
| </p> | |
| )} | |
| </div> | |
| </div> | |
| </section> | |
| </div> | |
| </div> | |
| ); | |
| } | |