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