| 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> |
| ); |
| } |
|
|