| import { useState, useEffect, Fragment } from "react"; |
| import type { CorpusStats } from "./types"; |
| import { api, checkConnection } from "./api"; |
| import TrainingPanel from "./components/TrainingPanel"; |
| import EngineSetup from "./components/EngineSetup"; |
| import SemanticSearch from "./components/SemanticSearch"; |
| import TextCompare from "./components/TextCompare"; |
| import SimilarWords from "./components/SimilarWords"; |
| import ContextAnalysis from "./components/ContextAnalysis"; |
| import AnomalyPanel from "./components/AnomalyPanel"; |
| import Word2VecPanel from "./components/Word2VecPanel"; |
| import DatasetPanel from "./components/DatasetPanel"; |
| import MetricCard from "./components/MetricCard"; |
| import "./styles.css"; |
|
|
| type NavGroup = "data" | "training"; |
| type TrainingTab = "model" | "w2v"; |
| type AnalysisTab = "context" | "anomalies"; |
|
|
| const STEPS: { id: NavGroup; label: string }[] = [ |
| { id: "data", label: "Data & Setup" }, |
| { id: "training", label: "Training" }, |
| ]; |
|
|
| const TRAINING_TABS: { id: TrainingTab; label: string }[] = [ |
| { id: "model", label: "Fine-tune Model" }, |
| { id: "w2v", label: "Word2Vec Baseline" }, |
| ]; |
|
|
| const ANALYSIS_TABS: { id: AnalysisTab; label: string }[] = [ |
| { id: "context", label: "Context" }, |
| { id: "anomalies", label: "Anomalies" }, |
| ]; |
|
|
| export default function App() { |
| const [group, setGroup] = useState<NavGroup>("data"); |
| const [trainingTab, setTrainingTab] = useState<TrainingTab>("w2v"); |
| const [analysisTab, setAnalysisTab] = useState<AnalysisTab>("context"); |
| const [stats, setStats] = useState<CorpusStats | null>(null); |
| const [showManualSetup, setShowManualSetup] = useState(false); |
| const [serverError, setServerError] = useState<string | null>(null); |
| const [w2vReady, setW2vReady] = useState(false); |
| const [w2vInfo, setW2vInfo] = useState<{ vocab_size: number; sentences: number; vector_size: number } | null>(null); |
|
|
| useEffect(() => { |
| checkConnection().then((err) => { |
| setServerError(err); |
| if (!err) { |
| api.getStats().then(setStats).catch(() => {}); |
| api.w2vStatus().then(res => { |
| if (res.ready) { |
| setW2vReady(true); |
| setW2vInfo({ vocab_size: res.vocab_size!, sentences: res.sentences!, vector_size: res.vector_size! }); |
| setGroup("training"); |
| } |
| }).catch(() => {}); |
| } |
| }); |
| const interval = setInterval(() => { |
| checkConnection().then(setServerError); |
| }, 15000); |
| return () => clearInterval(interval); |
| }, []); |
|
|
| function handleW2vReady(ready: boolean, info?: { vocab_size: number; sentences: number; vector_size: number }) { |
| setW2vReady(ready); |
| setW2vInfo(ready && info ? info : null); |
| } |
|
|
| return ( |
| <div className="app"> |
| <header className="app-header"> |
| <h1>Contextual Similarity Engine</h1> |
| {stats && ( |
| <div className="header-stats"> |
| <span className="badge">{stats.model_name}</span> |
| <span className="badge">{stats.total_documents} docs</span> |
| <span className="badge">{stats.total_chunks} chunks</span> |
| </div> |
| )} |
| </header> |
| |
| {serverError && ( |
| <div className="server-error-banner"> |
| <strong>Server unavailable:</strong> {serverError} |
| </div> |
| )} |
| |
| {/* Progress Stepper — hidden once training is complete */} |
| {!w2vReady && ( |
| <nav className="stepper"> |
| {STEPS.map((step, i) => { |
| const active = group === step.id; |
| const done = (step.id === "data" && stats !== null && stats.index_built) |
| || (step.id === "training" && w2vReady); |
| return ( |
| <Fragment key={step.id}> |
| {i > 0 && <div className="stepper-line stepper-line-active" />} |
| <div className="stepper-item"> |
| <button |
| className={`stepper-circle ${active ? "stepper-active" : ""} ${done && !active ? "stepper-done" : ""}`} |
| onClick={() => setGroup(step.id)} |
| > |
| {done && !active ? "✓" : i + 1} |
| </button> |
| <span className={`stepper-label ${active ? "stepper-label-active" : ""}`}> |
| {step.label} |
| </span> |
| </div> |
| </Fragment> |
| ); |
| })} |
| </nav> |
| )} |
| |
| {/* Content */} |
| <main className="content"> |
| {group === "data" && ( |
| <> |
| <DatasetPanel onStatsUpdate={setStats} onLoaded={() => setGroup("training")} /> |
| <button |
| className="collapsible-toggle" |
| onClick={() => setShowManualSetup(!showManualSetup)} |
| > |
| <span className="collapsible-arrow">{showManualSetup ? "▾" : "▸"}</span> |
| Or add documents manually |
| </button> |
| {showManualSetup && <EngineSetup onStatsUpdate={setStats} />} |
| </> |
| )} |
| |
| {group === "training" && !w2vReady && ( |
| <> |
| <nav className="subtabs"> |
| {TRAINING_TABS.map((t) => ( |
| <button |
| key={t.id} |
| className={`subtab ${trainingTab === t.id ? "subtab-active" : ""}`} |
| onClick={() => setTrainingTab(t.id)} |
| > |
| {t.label} |
| </button> |
| ))} |
| </nav> |
| {trainingTab === "model" && <TrainingPanel />} |
| {trainingTab === "w2v" && <Word2VecPanel onReady={handleW2vReady} />} |
| </> |
| )} |
|
|
| {group === "training" && w2vReady && w2vInfo && ( |
| <> |
| <div className="panel"> |
| <h2 style={{ marginTop: 0 }}>Trained Corpus</h2> |
| <p className="panel-desc"> |
| Word2Vec model is trained and persisted. Use the tools below to explore similarity. |
| </p> |
| <div className="metric-grid"> |
| <MetricCard value={w2vInfo.vocab_size} label="Vocabulary" /> |
| <MetricCard value={w2vInfo.sentences} label="Sentences" /> |
| <MetricCard value={w2vInfo.vector_size} label="Dimensions" /> |
| </div> |
| </div> |
| |
| <nav className="subtabs"> |
| {ANALYSIS_TABS.map((t) => ( |
| <button |
| key={t.id} |
| className={`subtab ${analysisTab === t.id ? "subtab-active" : ""}`} |
| onClick={() => setAnalysisTab(t.id)} |
| > |
| {t.label} |
| </button> |
| ))} |
| </nav> |
| |
| {analysisTab === "context" && ( |
| <> |
| <SimilarWords /> |
| <TextCompare /> |
| <SemanticSearch /> |
| <ContextAnalysis /> |
| </> |
| )} |
| {analysisTab === "anomalies" && <AnomalyPanel />} |
| </> |
| )} |
| </main> |
| </div> |
| ); |
| } |
|
|