File size: 4,635 Bytes
db764ae | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 | 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>
);
}
|