| import { useState } from "react"; |
| import { api } from "../api"; |
| import type { ContextAnalysisResponse } from "../types"; |
| import { useApiCall } from "../hooks/useApiCall"; |
| import StatusMessage from "./StatusMessage"; |
|
|
| export default function ContextAnalysis() { |
| const [keyword, setKeyword] = useState(""); |
| const { data: result, loading, error, run } = useApiCall<ContextAnalysisResponse>(); |
|
|
| async function handleAnalyze() { |
| if (!keyword.trim()) return; |
| await run(() => api.analyzeContext({ keyword: keyword.trim() })); |
| } |
|
|
| return ( |
| <div> |
| <div className="panel"> |
| <h2>Context Analysis</h2> |
| <p className="panel-desc"> |
| Enter a keyword to discover what it likely means based on how it's used in the corpus. |
| The engine clusters all occurrences and extracts the most associated words for each meaning. |
| </p> |
| <div className="flex-row" style={{ alignItems: "flex-end" }}> |
| <div className="form-group form-group-lg"> |
| <label>Keyword</label> |
| <input |
| value={keyword} |
| onChange={(e) => setKeyword(e.target.value)} |
| onKeyDown={(e) => e.key === "Enter" && handleAnalyze()} |
| placeholder="e.g. Epstein, flight, island" |
| /> |
| </div> |
| <button |
| className="btn btn-primary" |
| onClick={handleAnalyze} |
| disabled={loading || !keyword.trim()} |
| style={{ height: 38 }} |
| > |
| {loading ? "Analyzing..." : "Analyze"} |
| </button> |
| </div> |
| </div> |
| |
| {error && <StatusMessage type="err" message={error} />} |
| |
| {result && result.total_occurrences === 0 && ( |
| <StatusMessage type="err" message={`No occurrences of "${result.keyword}" found in the corpus.`} /> |
| )} |
| |
| {result && result.meanings.length > 0 && ( |
| <div className="panel"> |
| <h2> |
| "{result.keyword}" — {result.total_occurrences} occurrences, {result.meanings.length} meaning{result.meanings.length > 1 ? "s" : ""} |
| </h2> |
| |
| <div className="flex-col gap-3"> |
| {result.meanings.map((meaning, idx) => ( |
| <div key={meaning.cluster_id} className="result-card"> |
| <div className="result-header"> |
| <span style={{ fontWeight: 600, fontSize: "0.9rem" }}> |
| Meaning {idx + 1} |
| </span> |
| <div className="flex-row"> |
| <span className="badge"> |
| {meaning.occurrences} occurrence{meaning.occurrences > 1 ? "s" : ""} |
| </span> |
| <span |
| className="badge" |
| style={{ |
| background: `rgba(${meaning.confidence > 0.5 ? "74, 222, 128" : "108, 140, 255"}, 0.15)`, |
| color: meaning.confidence > 0.5 ? "var(--ok)" : "var(--accent)", |
| }} |
| > |
| {(meaning.confidence * 100).toFixed(1)}% |
| </span> |
| </div> |
| </div> |
| |
| {/* Associated words bar chart */} |
| <div className="mt-2"> |
| {meaning.associated_words.map((aw) => { |
| const maxScore = meaning.associated_words[0]?.score || 1; |
| const pct = Math.round((aw.score / maxScore) * 100); |
| return ( |
| <div key={aw.word} className="context-bar-row"> |
| <span className="context-bar-label">{aw.word}</span> |
| <div className="context-bar-track"> |
| <div className="context-bar-fill" style={{ width: `${pct}%` }} /> |
| </div> |
| <span className="context-bar-value">{(aw.score * 100).toFixed(0)}</span> |
| </div> |
| ); |
| })} |
| </div> |
| |
| {/* Example snippets */} |
| {meaning.example_contexts.length > 0 && ( |
| <div className="mt-2"> |
| <div className="section-label">Example contexts</div> |
| {meaning.example_contexts.map((ex, i) => ( |
| <div key={i} className="context-snippet"> |
| <span className="context-snippet-source">{ex.doc_id}</span> |
| {ex.snippet} |
| </div> |
| ))} |
| </div> |
| )} |
| </div> |
| ))} |
| </div> |
| </div> |
| )} |
| </div> |
| ); |
| } |
|
|