File size: 4,338 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 | import { useState } from "react";
import { api } from "../api";
import type { KeywordAnalysisResponse } from "../types";
import { useApiCall } from "../hooks/useApiCall";
import ScoreBar from "./ScoreBar";
import StatusMessage from "./StatusMessage";
export default function BatchAnalysis() {
const [keywordsText, setKeywordsText] = useState("");
const [topK, setTopK] = useState(5);
const [threshold, setThreshold] = useState(0.4);
const { data: results, loading, error, run } = useApiCall<Record<string, KeywordAnalysisResponse>>();
async function handleAnalyze() {
const keywords = keywordsText.split("\n").map((s) => s.trim()).filter(Boolean);
if (keywords.length === 0) return;
await run(() => api.batchAnalyze({ keywords, top_k: topK, cluster_threshold: threshold, compare_across: true }));
}
return (
<div>
<div className="panel">
<h2>Batch Keyword Analysis</h2>
<p className="panel-desc">
Analyze multiple keywords at once and compare their semantic relationships.
</p>
<div className="form-row">
<div className="form-group">
<label>Keywords (one per line)</label>
<textarea
value={keywordsText}
onChange={(e) => setKeywordsText(e.target.value)}
placeholder={`pizza\nschool\nhomework`}
rows={4}
/>
</div>
<div className="flex-col gap-1">
<div className="form-group form-group-sm">
<label>Top K</label>
<input type="number" value={topK} onChange={(e) => setTopK(+e.target.value)} min={1} max={50} />
</div>
<div className="form-group form-group-md">
<label>Cluster Threshold</label>
<input type="number" value={threshold} onChange={(e) => setThreshold(+e.target.value)} min={0.1} max={1} step={0.05} />
</div>
</div>
</div>
<button className="btn btn-primary" onClick={handleAnalyze} disabled={loading || !keywordsText.trim()}>
{loading ? "Analyzing..." : "Analyze All"}
</button>
</div>
{error && <StatusMessage type="err" message={error} />}
{results && (
<>
{Object.values(results).some((a) => Object.keys(a.cross_keyword_similarities).length > 0) && (
<div className="panel">
<h3>Cross-Keyword Similarity</h3>
<table className="data-table">
<thead>
<tr>
<th>Keyword</th>
{Object.keys(results).map((kw) => (
<th key={kw}>{kw}</th>
))}
</tr>
</thead>
<tbody>
{Object.entries(results).map(([kw, analysis]) => (
<tr key={kw}>
<td style={{ fontWeight: 600 }}>{kw}</td>
{Object.keys(results).map((other) => (
<td key={other}>
{kw === other ? (
<span className="text-dim">-</span>
) : (
<ScoreBar score={analysis.cross_keyword_similarities[other] ?? 0} />
)}
</td>
))}
</tr>
))}
</tbody>
</table>
</div>
)}
{Object.entries(results).map(([kw, analysis]) => (
<div key={kw} className="panel">
<h3>
"{kw}" — {analysis.total_occurrences} occurrence(s),{" "}
{analysis.meaning_clusters.length} cluster(s)
</h3>
{analysis.meaning_clusters.map((cluster) => (
<div key={cluster.cluster_id} className="result-card mt-1">
<div className="result-header">
<strong>Cluster {cluster.cluster_id}</strong>
<span className="tag">{cluster.size} occurrence(s)</span>
</div>
<div className="result-text">{cluster.representative_text.slice(0, 200)}...</div>
</div>
))}
</div>
))}
</>
)}
</div>
);
}
|