| 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 KeywordAnalysis() { |
| const [keyword, setKeyword] = useState(""); |
| const [topK, setTopK] = useState(5); |
| const [threshold, setThreshold] = useState(0.4); |
| const { data: analysis, loading, error, run } = useApiCall<KeywordAnalysisResponse>(); |
|
|
| async function handleAnalyze() { |
| if (!keyword.trim()) return; |
| await run(() => api.analyzeKeyword({ keyword, top_k: topK, cluster_threshold: threshold })); |
| } |
|
|
| return ( |
| <div> |
| <div className="panel"> |
| <h2>Keyword Analysis</h2> |
| <p className="panel-desc"> |
| Find all occurrences of a keyword, cluster them by contextual meaning, |
| and discover semantically similar passages for each meaning. |
| </p> |
| <div className="form-row"> |
| <div className="form-group"> |
| <label>Keyword</label> |
| <input |
| value={keyword} |
| onChange={(e) => setKeyword(e.target.value)} |
| placeholder="e.g. pizza" |
| onKeyDown={(e) => e.key === "Enter" && handleAnalyze()} |
| /> |
| </div> |
| <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 className="form-group form-group-sm"> |
| <label> </label> |
| <button className="btn btn-primary" onClick={handleAnalyze} disabled={loading || !keyword.trim()}> |
| {loading ? "Analyzing..." : "Analyze"} |
| </button> |
| </div> |
| </div> |
| </div> |
| |
| {error && <StatusMessage type="err" message={error} />} |
| |
| {analysis && ( |
| <div className="panel"> |
| <h3> |
| "{analysis.keyword}" — {analysis.total_occurrences} occurrence(s),{" "} |
| {analysis.meaning_clusters.length} meaning cluster(s) |
| </h3> |
| |
| {analysis.meaning_clusters.map((cluster) => ( |
| <div key={cluster.cluster_id} className="result-card mt-2"> |
| <div className="result-header"> |
| <div> |
| <strong>Cluster {cluster.cluster_id}</strong>{" "} |
| <span className="tag">{cluster.size} occurrence(s)</span> |
| </div> |
| </div> |
| |
| <div className="mt-1 mb-2"> |
| <div className="section-label">Contexts:</div> |
| {cluster.contexts.map((ctx, i) => ( |
| <div key={i} className="result-text" style={{ marginBottom: 4, paddingLeft: 12 }}> |
| <span className="badge" style={{ marginRight: 6 }}>{ctx.doc_id}</span> |
| {ctx.text.slice(0, 200)}... |
| </div> |
| ))} |
| </div> |
| |
| <div> |
| <div className="section-label">Similar passages:</div> |
| {cluster.similar_passages.map((sp) => ( |
| <div key={sp.rank} className="flex-row" style={{ alignItems: "start", marginBottom: 6 }}> |
| <ScoreBar score={sp.score} /> |
| <span className="result-text" style={{ flex: 1 }}> |
| <span className="badge" style={{ marginRight: 4 }}>{sp.doc_id}</span> |
| {sp.text.slice(0, 150)}... |
| </span> |
| </div> |
| ))} |
| </div> |
| </div> |
| ))} |
| </div> |
| )} |
| </div> |
| ); |
| } |
|
|