| import { useState } from "react"; |
| import { api, getErrorMessage } from "../api"; |
| import type { MatchResponse } from "../types"; |
| import { useApiCall } from "../hooks/useApiCall"; |
| import ScoreBar from "./ScoreBar"; |
| import StatusMessage from "./StatusMessage"; |
|
|
| export default function KeywordMatcher() { |
| const [keyword, setKeyword] = useState(""); |
| const [meaningsText, setMeaningsText] = useState(""); |
| const { data: results, loading, error, setError, run } = useApiCall<MatchResponse>(); |
|
|
| async function handleMatch() { |
| if (!keyword.trim() || !meaningsText.trim()) return; |
| const candidates = meaningsText.split("\n").map((s) => s.trim()).filter(Boolean); |
| if (candidates.length < 2) { |
| setError("Provide at least 2 candidate meanings (one per line)."); |
| return; |
| } |
| await run(() => api.matchKeyword({ keyword, candidate_meanings: candidates })); |
| } |
|
|
| return ( |
| <div> |
| <div className="panel"> |
| <h2>Keyword Meaning Matcher</h2> |
| <p className="panel-desc"> |
| Match each occurrence of a keyword to the most likely intended meaning. |
| For example: keyword "pizza" with candidates "food" and "school". |
| </p> |
| <div className="form-row"> |
| <div className="form-group form-group-lg"> |
| <label>Keyword</label> |
| <input value={keyword} onChange={(e) => setKeyword(e.target.value)} placeholder="e.g. pizza" /> |
| </div> |
| </div> |
| <div className="form-group mb-2"> |
| <label>Candidate Meanings (one per line)</label> |
| <textarea |
| value={meaningsText} |
| onChange={(e) => setMeaningsText(e.target.value)} |
| placeholder={`Italian food made with dough, tomato sauce, and cheese\nSchool, education, and academic activities`} |
| rows={4} |
| /> |
| </div> |
| <button className="btn btn-primary" onClick={handleMatch} disabled={loading || !keyword.trim() || !meaningsText.trim()}> |
| {loading ? "Matching..." : "Match"} |
| </button> |
| </div> |
| |
| {error && <StatusMessage type="err" message={error} />} |
| |
| {results && ( |
| <div className="panel"> |
| <h3>Matches for "{results.keyword}" ({results.matches.length} occurrences)</h3> |
| |
| {results.matches.map((m, idx) => ( |
| <div key={idx} className="result-card mt-1"> |
| <div className="result-header"> |
| <div> |
| <span className="badge">{m.doc_id}</span>{" "} |
| <span className="tag">chunk {m.chunk_index}</span> |
| </div> |
| <span className="tag tag-best">{m.best_match}</span> |
| </div> |
| <div className="result-text mb-1">{m.text.slice(0, 250)}...</div> |
| <div className="flex-row flex-wrap gap-2"> |
| {Object.entries(m.all_scores).map(([meaning, score]) => ( |
| <div key={meaning} style={{ flex: "1 1 200px" }}> |
| <div |
| style={{ |
| fontSize: "0.78rem", |
| color: meaning === m.best_match ? "var(--ok)" : "var(--text-dim)", |
| fontWeight: meaning === m.best_match ? 700 : 400, |
| marginBottom: 2, |
| }} |
| > |
| {meaning.slice(0, 60)} |
| </div> |
| <ScoreBar score={score} /> |
| </div> |
| ))} |
| </div> |
| </div> |
| ))} |
| </div> |
| )} |
| </div> |
| ); |
| } |
|
|