Yassine Mhirsi commited on
Commit
78b3b2b
·
1 Parent(s): 21d3506

enhance analysis page

Browse files
src/app/components/analysis/UploadCsvModal.tsx ADDED
@@ -0,0 +1,140 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useMemo, useState } from 'react';
2
+ import { parseCsv } from '../../utils/index.ts';
3
+
4
+ import type { CsvRow } from '../../types/index.ts';
5
+
6
+ interface UploadCsvModalProps {
7
+ isOpen: boolean;
8
+ onClose: () => void;
9
+ onAnalyze: (file: File) => Promise<void>;
10
+ isAnalyzing: boolean;
11
+ }
12
+
13
+ const UploadCsvModal = ({
14
+ isOpen,
15
+ onClose,
16
+ onAnalyze,
17
+ isAnalyzing
18
+ }: UploadCsvModalProps) => {
19
+ const [selectedFile, setSelectedFile] = useState<File | null>(null);
20
+ const [rows, setRows] = useState<CsvRow[]>([]);
21
+ const [error, setError] = useState<string | null>(null);
22
+
23
+ const fileName = useMemo(
24
+ () => selectedFile?.name ?? 'Select a CSV file with arguments',
25
+ [selectedFile]
26
+ );
27
+
28
+ const handleFileChange = async (event: React.ChangeEvent<HTMLInputElement>) => {
29
+ const file = event.target.files?.[0] ?? null;
30
+ setSelectedFile(file);
31
+ setError(null);
32
+ setRows([]);
33
+
34
+ if (!file) {
35
+ return;
36
+ }
37
+
38
+ // Parse CSV
39
+ try {
40
+ const content = await file.text();
41
+ const parsed = parseCsv(content);
42
+
43
+ setRows(parsed.rows);
44
+
45
+ if (parsed.rows.length === 0) {
46
+ setError('No data found in the CSV file.');
47
+ }
48
+ } catch (err) {
49
+ setError(
50
+ err instanceof Error ? err.message : 'Unable to read the CSV file.'
51
+ );
52
+ setRows([]);
53
+ }
54
+ };
55
+
56
+ const handleAnalyze = async () => {
57
+ if (!selectedFile) {
58
+ setError('Please choose a CSV file to analyze.');
59
+ return;
60
+ }
61
+
62
+ if (rows.length === 0) {
63
+ setError('No data found in the CSV file.');
64
+ return;
65
+ }
66
+
67
+ await onAnalyze(selectedFile);
68
+ };
69
+
70
+ if (!isOpen) return null;
71
+
72
+ return (
73
+ <div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/50 dark:bg-black/70">
74
+ <div
75
+ className="relative w-full max-w-2xl max-h-[90vh] overflow-hidden rounded-xl border border-slate-200 dark:border-zinc-700 bg-white dark:bg-zinc-900 shadow-xl"
76
+ onClick={(e) => e.stopPropagation()}
77
+ >
78
+ <div className="flex items-center justify-between border-b border-slate-200 dark:border-zinc-700 px-6 py-4">
79
+ <h2 className="text-lg font-semibold text-slate-800 dark:text-white">
80
+ Analyze New Arguments
81
+ </h2>
82
+ <button
83
+ onClick={onClose}
84
+ className="text-slate-500 dark:text-zinc-400 hover:text-slate-700 dark:hover:text-zinc-200 transition-colors"
85
+ aria-label="Close"
86
+ >
87
+ <svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
88
+ <path fillRule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clipRule="evenodd" />
89
+ </svg>
90
+ </button>
91
+ </div>
92
+
93
+ <div className="p-6 space-y-6 overflow-y-auto max-h-[calc(90vh-140px)]">
94
+ <div className="flex flex-col gap-3">
95
+ <label className="text-sm font-medium text-slate-700 dark:text-zinc-300">
96
+ Upload a CSV file
97
+ </label>
98
+
99
+ <div className="flex flex-wrap items-center justify-between gap-3">
100
+ <label className="flex w-full max-w-xl cursor-pointer items-center justify-between rounded-full border border-slate-300 dark:border-zinc-600 bg-slate-50 dark:bg-zinc-800 px-5 py-3 transition hover:border-slate-400 dark:hover:border-zinc-500">
101
+ <span className="truncate text-sm font-semibold text-slate-500 dark:text-zinc-400">
102
+ {fileName}
103
+ </span>
104
+ <input
105
+ type="file"
106
+ accept=".csv,text/csv"
107
+ className="hidden"
108
+ onChange={handleFileChange}
109
+ />
110
+ </label>
111
+
112
+ <div className="flex w-full justify-center">
113
+ <button
114
+ type="button"
115
+ onClick={handleAnalyze}
116
+ disabled={!selectedFile || rows.length === 0 || isAnalyzing}
117
+ className="rounded-md bg-blue-600 dark:bg-blue-500 px-4 py-2 text-sm font-semibold text-white transition hover:bg-blue-700 dark:hover:bg-blue-600 disabled:cursor-not-allowed disabled:opacity-60"
118
+ >
119
+ {isAnalyzing ? 'Analyzing...' : 'Analyze'}
120
+ </button>
121
+ </div>
122
+ </div>
123
+ {error && <p className="text-sm text-red-600 dark:text-red-400">{error}</p>}
124
+ </div>
125
+
126
+ {isAnalyzing && (
127
+ <div className="flex items-center justify-center py-8">
128
+ <div className="flex flex-col items-center">
129
+ <div className="h-10 w-10 animate-spin rounded-full border-4 border-blue-600 border-t-transparent"></div>
130
+ <p className="mt-3 text-sm text-slate-600 dark:text-zinc-400">Analyzing arguments...</p>
131
+ </div>
132
+ </div>
133
+ )}
134
+ </div>
135
+ </div>
136
+ </div>
137
+ );
138
+ };
139
+
140
+ export default UploadCsvModal;
src/app/pages/AnalysisPage.tsx CHANGED
@@ -1,40 +1,33 @@
1
  import React, { useMemo, useState, useEffect } from 'react';
2
- import { parseCsv, formatError, getCachedAnalysisData, cacheAnalysisData } from '../utils/index.ts';
3
- import { CsvRow } from '../types/index.ts';
4
  import { AnalysisResult } from '../types/analysis.types.ts';
5
  import { analyzeArgumentsFromCsv, getAnalysisResults } from '../services/analysis.service.ts';
6
  import {
7
  calculateStanceStats,
8
  calculateTopicFrequency,
9
- calculateTimeStats,
10
  getUniqueTopicsCount,
11
  getAverageArgumentLength,
12
- getMostRecentAnalysisDate,
13
- getOldestAnalysisDate,
14
  } from '../utils/analysis.utils.ts';
 
15
  import StanceDistributionChart from '../components/analysis/StanceDistributionChart.tsx';
16
  import TopicFrequencyChart from '../components/analysis/TopicFrequencyChart.tsx';
17
- import TimeSeriesChart from '../components/analysis/TimeSeriesChart.tsx';
18
  import Loading from '../components/common/Loading.tsx';
 
19
 
20
  const AnalysisPage = () => {
21
- const [selectedFile, setSelectedFile] = useState(null as File | null);
22
- const [headers, setHeaders] = useState(['id', 'argument'] as string[]);
23
- const [rows, setRows] = useState([] as CsvRow[]);
24
- const [error, setError] = useState(null as string | null);
25
  const [isAnalyzing, setIsAnalyzing] = useState(false);
26
- const [analysisResults, setAnalysisResults] = useState([] as AnalysisResult[]);
27
-
 
 
 
 
28
  // User's historical analysis data
29
  const [userAnalysisData, setUserAnalysisData] = useState([] as AnalysisResult[]);
30
  const [isLoadingStats, setIsLoadingStats] = useState(true);
31
  const [isRefreshingStats, setIsRefreshingStats] = useState(false);
32
  const [statsError, setStatsError] = useState(null as string | null);
33
 
34
- const fileName = useMemo(
35
- () => selectedFile?.name ?? 'Select a CSV file with arguments',
36
- [selectedFile]
37
- );
38
 
39
  // Fetch user's analysis data on mount with cache support
40
  useEffect(() => {
@@ -71,6 +64,17 @@ const AnalysisPage = () => {
71
  fetchUserAnalysis();
72
  }, []);
73
 
 
 
 
 
 
 
 
 
 
 
 
74
  // Calculate statistics from user's analysis data
75
  const stanceStats = useMemo(
76
  () => calculateStanceStats(userAnalysisData),
@@ -80,10 +84,6 @@ const AnalysisPage = () => {
80
  () => calculateTopicFrequency(userAnalysisData, 10),
81
  [userAnalysisData]
82
  );
83
- const timeStats = useMemo(
84
- () => calculateTimeStats(userAnalysisData),
85
- [userAnalysisData]
86
- );
87
  const uniqueTopicsCount = useMemo(
88
  () => getUniqueTopicsCount(userAnalysisData),
89
  [userAnalysisData]
@@ -92,69 +92,35 @@ const AnalysisPage = () => {
92
  () => getAverageArgumentLength(userAnalysisData),
93
  [userAnalysisData]
94
  );
95
- const mostRecentDate = useMemo(
96
- () => getMostRecentAnalysisDate(userAnalysisData),
97
- [userAnalysisData]
98
- );
99
- const oldestDate = useMemo(
100
- () => getOldestAnalysisDate(userAnalysisData),
101
- [userAnalysisData]
102
- );
103
-
104
- const handleFileChange = async (event: any) => {
105
- const file = event.target.files?.[0] ?? null;
106
- setSelectedFile(file);
107
- setError(null);
108
- setAnalysisResults([]);
109
-
110
- if (!file) {
111
- setHeaders(['id', 'argument']);
112
- setRows([]);
113
- return;
114
- }
115
 
116
- // Automatically parse and preview CSV
117
- try {
118
- const content = await file.text();
119
- const parsed = parseCsv(content);
120
-
121
- setHeaders(parsed.headers.length > 0 ? parsed.headers : ['id', 'argument']);
122
- setRows(parsed.rows);
123
-
124
- if (parsed.rows.length === 0) {
125
- setError('No data found in the CSV file.');
126
- }
127
- } catch (err) {
128
- setError(
129
- err instanceof Error ? err.message : 'Unable to read the CSV file.'
130
- );
131
- setHeaders(['id', 'argument']);
132
- setRows([]);
133
- }
134
  };
135
 
136
- const handleAnalyze = async () => {
137
- if (!selectedFile) {
138
- setError('Please choose a CSV file to analyze.');
139
- return;
140
- }
141
 
 
142
  setIsAnalyzing(true);
143
- setError(null);
144
- setAnalysisResults([]);
145
 
146
  try {
147
- const response = await analyzeArgumentsFromCsv(selectedFile);
148
  setAnalysisResults(response.results);
 
 
149
  // Refresh user analysis data after successful analysis
150
  const updatedResponse = await getAnalysisResults(1000, 0);
151
  setUserAnalysisData(updatedResponse.results);
152
  // Cache the updated data
153
  cacheAnalysisData(updatedResponse.results);
154
  } catch (err) {
155
- setError(formatError(err));
 
 
156
  } finally {
157
  setIsAnalyzing(false);
 
158
  }
159
  };
160
 
@@ -173,12 +139,20 @@ const AnalysisPage = () => {
173
  Overview of all your analyzed arguments
174
  </p>
175
  </div>
176
- {isRefreshingStats && (
177
- <div className="flex items-center gap-2 text-xs text-slate-500 dark:text-zinc-400">
178
- <div className="h-3 w-3 animate-spin rounded-full border-2 border-slate-300 dark:border-zinc-600 border-t-blue-600 dark:border-t-blue-500"></div>
179
- <span>Updating...</span>
180
- </div>
181
- )}
 
 
 
 
 
 
 
 
182
  </div>
183
  </div>
184
 
@@ -227,7 +201,7 @@ const AnalysisPage = () => {
227
  </div>
228
 
229
  {/* Charts Grid */}
230
- <div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
231
  {/* Stance Distribution */}
232
  <div className="rounded-lg border border-slate-200 dark:border-zinc-700 p-4">
233
  <h3 className="text-sm font-semibold text-slate-700 dark:text-zinc-300 mb-4">
@@ -247,21 +221,6 @@ const AnalysisPage = () => {
247
  </div>
248
  </div>
249
  </div>
250
-
251
- {/* Time Series */}
252
- {timeStats.length > 0 && (
253
- <div className="rounded-lg border border-slate-200 dark:border-zinc-700 p-4">
254
- <h3 className="text-sm font-semibold text-slate-700 dark:text-zinc-300 mb-4">
255
- Analysis Timeline
256
- </h3>
257
- <TimeSeriesChart data={timeStats} />
258
- {oldestDate && mostRecentDate && (
259
- <div className="mt-4 text-center text-xs text-slate-500 dark:text-zinc-400">
260
- From {oldestDate} to {mostRecentDate}
261
- </div>
262
- )}
263
- </div>
264
- )}
265
  </div>
266
 
267
  {/* Topic Frequency */}
@@ -273,167 +232,90 @@ const AnalysisPage = () => {
273
  <TopicFrequencyChart data={topicFrequency} />
274
  </div>
275
  )}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
276
  </div>
277
  )}
278
  </div>
279
 
280
  {/* CSV Upload and Analysis Section */}
281
- <div className="rounded-lg border border-slate-200 dark:border-zinc-700 bg-white dark:bg-zinc-900 shadow-sm">
282
- <div className="border-b border-slate-200 dark:border-zinc-700 px-6 py-4">
283
- <h1 className="text-lg font-semibold text-slate-800 dark:text-white">
284
- Analyze New Arguments
285
- </h1>
286
- <p className="text-sm text-slate-500 dark:text-zinc-400">
287
- Upload a CSV file containing arguments to analyze them.
288
- </p>
289
- </div>
290
-
291
- <div className="space-y-6 p-6">
292
- <div className="flex flex-col gap-3">
293
- <label className="text-sm font-medium text-slate-700 dark:text-zinc-300">
294
- Upload a CSV file
295
- </label>
296
-
297
- <div className="flex flex-wrap items-center gap-3">
298
- <label className="flex w-full max-w-xl cursor-pointer items-center justify-between rounded-full border border-slate-300 dark:border-zinc-600 bg-slate-50 dark:bg-zinc-800 px-5 py-3 transition hover:border-slate-400 dark:hover:border-zinc-500">
299
- <span className="truncate text-sm font-semibold text-slate-500 dark:text-zinc-400">
300
- {fileName}
301
- </span>
302
- <input
303
- type="file"
304
- accept=".csv,text/csv"
305
- className="hidden"
306
- onChange={handleFileChange}
307
- />
308
- </label>
309
-
310
- <button
311
- type="button"
312
- onClick={handleAnalyze}
313
- disabled={!selectedFile || rows.length === 0 || isAnalyzing}
314
- className="rounded-md bg-blue-600 dark:bg-blue-500 px-4 py-2 text-sm font-semibold text-white transition hover:bg-blue-700 dark:hover:bg-blue-600 disabled:cursor-not-allowed disabled:opacity-60"
315
- >
316
- {isAnalyzing ? 'Analyzing...' : 'Analyze'}
317
- </button>
318
- </div>
319
 
320
- {error && <p className="text-sm text-red-600 dark:text-red-400">{error}</p>}
321
- </div>
322
-
323
- {rows.length > 0 && (
324
- <div className="overflow-hidden rounded-lg border border-slate-200 dark:border-zinc-700">
325
- <div className="bg-slate-50 dark:bg-zinc-800 px-4 py-2 border-b border-slate-200 dark:border-zinc-700">
326
- <h2 className="text-sm font-semibold text-slate-700 dark:text-zinc-300">
327
- CSV Preview ({rows.length} rows)
328
- </h2>
329
- </div>
330
- <table className="min-w-full divide-y divide-slate-200 dark:divide-zinc-700">
331
- <thead className="bg-slate-50 dark:bg-zinc-800">
332
- <tr>
333
- {headers.map((header) => (
334
- <th
335
- key={header}
336
- scope="col"
337
- className="px-4 py-3 text-left text-sm font-semibold uppercase tracking-wide text-slate-600 dark:text-zinc-300"
338
- >
339
- {header}
340
- </th>
341
- ))}
342
- </tr>
343
- </thead>
344
- <tbody className="divide-y divide-slate-200 dark:divide-zinc-700 bg-white dark:bg-zinc-900">
345
- {rows.map((row, index) => (
346
- <tr key={row.id ?? `row-${index}`} className="hover:bg-slate-50 dark:hover:bg-zinc-800">
347
- {headers.map((header) => (
348
- <td
349
- key={`${header}-${index}`}
350
- className="whitespace-pre-wrap px-4 py-3 text-sm text-slate-700 dark:text-zinc-300"
351
- >
352
- {row[header] ?? ''}
353
- </td>
354
- ))}
355
- </tr>
356
- ))}
357
- </tbody>
358
- </table>
359
- </div>
360
- )}
361
-
362
- {isAnalyzing && (
363
- <div className="flex items-center justify-center py-12">
364
- <Loading text="Analyzing arguments..." />
365
- </div>
366
- )}
367
-
368
- {!isAnalyzing && analysisResults.length > 0 && (
369
- <div className="overflow-hidden rounded-lg border border-slate-200 dark:border-zinc-700">
370
- <div className="bg-green-50 dark:bg-green-900/20 px-4 py-2 border-b border-slate-200 dark:border-zinc-700">
371
- <h2 className="text-sm font-semibold text-green-800 dark:text-green-400">
372
- Analysis Results ({analysisResults.length} results)
373
- </h2>
374
- </div>
375
- <div className="overflow-x-auto">
376
- <table className="min-w-full divide-y divide-slate-200 dark:divide-zinc-700">
377
- <thead className="bg-slate-50 dark:bg-zinc-800">
378
- <tr>
379
- <th className="px-4 py-3 text-left text-sm font-semibold uppercase tracking-wide text-slate-600 dark:text-zinc-300">
380
- Argument
381
- </th>
382
- <th className="px-4 py-3 text-left text-sm font-semibold uppercase tracking-wide text-slate-600 dark:text-zinc-300">
383
- Topic
384
- </th>
385
- <th className="px-4 py-3 text-left text-sm font-semibold uppercase tracking-wide text-slate-600 dark:text-zinc-300">
386
- Stance
387
- </th>
388
- <th className="px-4 py-3 text-left text-sm font-semibold uppercase tracking-wide text-slate-600 dark:text-zinc-300">
389
- Confidence
390
- </th>
391
- <th className="px-4 py-3 text-left text-sm font-semibold uppercase tracking-wide text-slate-600 dark:text-zinc-300">
392
- PRO
393
- </th>
394
- <th className="px-4 py-3 text-left text-sm font-semibold uppercase tracking-wide text-slate-600 dark:text-zinc-300">
395
- CON
396
- </th>
397
- </tr>
398
- </thead>
399
- <tbody className="divide-y divide-slate-200 dark:divide-zinc-700 bg-white dark:bg-zinc-900">
400
- {analysisResults.map((result) => (
401
- <tr key={result.id} className="hover:bg-slate-50 dark:hover:bg-zinc-800">
402
- <td className="whitespace-pre-wrap px-4 py-3 text-sm text-slate-700 dark:text-zinc-300 max-w-md">
403
- {result.argument}
404
- </td>
405
- <td className="px-4 py-3 text-sm text-slate-700 dark:text-zinc-300">
406
- {result.topic}
407
- </td>
408
- <td className="px-4 py-3 text-sm">
409
- <span
410
- className={`inline-flex rounded-full px-2 py-1 text-xs font-semibold ${
411
- result.predicted_stance === 'PRO'
412
- ? 'bg-green-100 dark:bg-green-900/30 text-green-800 dark:text-green-400'
413
- : 'bg-red-100 dark:bg-red-900/30 text-red-800 dark:text-red-400'
414
- }`}
415
- >
416
- {result.predicted_stance}
417
- </span>
418
- </td>
419
- <td className="px-4 py-3 text-sm text-slate-700 dark:text-zinc-300">
420
- {(result.confidence * 100).toFixed(1)}%
421
- </td>
422
- <td className="px-4 py-3 text-sm text-slate-700 dark:text-zinc-300">
423
- {(result.probability_pro * 100).toFixed(1)}%
424
- </td>
425
- <td className="px-4 py-3 text-sm text-slate-700 dark:text-zinc-300">
426
- {(result.probability_con * 100).toFixed(1)}%
427
- </td>
428
- </tr>
429
- ))}
430
- </tbody>
431
- </table>
432
- </div>
433
- </div>
434
- )}
435
- </div>
436
- </div>
437
  </div>
438
  </div>
439
  );
 
1
  import React, { useMemo, useState, useEffect } from 'react';
2
+ import { formatError, getCachedAnalysisData, cacheAnalysisData } from '../utils/index.ts';
 
3
  import { AnalysisResult } from '../types/analysis.types.ts';
4
  import { analyzeArgumentsFromCsv, getAnalysisResults } from '../services/analysis.service.ts';
5
  import {
6
  calculateStanceStats,
7
  calculateTopicFrequency,
 
8
  getUniqueTopicsCount,
9
  getAverageArgumentLength,
 
 
10
  } from '../utils/analysis.utils.ts';
11
+ import { getCachedRecentAnalysisData, cacheRecentAnalysisData } from '../utils/cache.utils.ts';
12
  import StanceDistributionChart from '../components/analysis/StanceDistributionChart.tsx';
13
  import TopicFrequencyChart from '../components/analysis/TopicFrequencyChart.tsx';
 
14
  import Loading from '../components/common/Loading.tsx';
15
+ import UploadCsvModal from '../components/analysis/UploadCsvModal.tsx';
16
 
17
  const AnalysisPage = () => {
 
 
 
 
18
  const [isAnalyzing, setIsAnalyzing] = useState(false);
19
+ const [analysisResults, setAnalysisResults] = useState<AnalysisResult[]>(() => {
20
+ const cachedResults = getCachedRecentAnalysisData();
21
+ return cachedResults || [];
22
+ });
23
+ const [showUploadModal, setShowUploadModal] = useState(false);
24
+
25
  // User's historical analysis data
26
  const [userAnalysisData, setUserAnalysisData] = useState([] as AnalysisResult[]);
27
  const [isLoadingStats, setIsLoadingStats] = useState(true);
28
  const [isRefreshingStats, setIsRefreshingStats] = useState(false);
29
  const [statsError, setStatsError] = useState(null as string | null);
30
 
 
 
 
 
31
 
32
  // Fetch user's analysis data on mount with cache support
33
  useEffect(() => {
 
64
  fetchUserAnalysis();
65
  }, []);
66
 
67
+ // Update analysisResults when userAnalysisData changes
68
+ useEffect(() => {
69
+ if (userAnalysisData.length > 0) {
70
+ // Set analysisResults to the most recent items (e.g., last 10 items)
71
+ const recentResults = userAnalysisData.slice(0, 10);
72
+ setAnalysisResults(recentResults);
73
+ // Also cache these to localStorage
74
+ cacheRecentAnalysisData(recentResults);
75
+ }
76
+ }, [userAnalysisData]);
77
+
78
  // Calculate statistics from user's analysis data
79
  const stanceStats = useMemo(
80
  () => calculateStanceStats(userAnalysisData),
 
84
  () => calculateTopicFrequency(userAnalysisData, 10),
85
  [userAnalysisData]
86
  );
 
 
 
 
87
  const uniqueTopicsCount = useMemo(
88
  () => getUniqueTopicsCount(userAnalysisData),
89
  [userAnalysisData]
 
92
  () => getAverageArgumentLength(userAnalysisData),
93
  [userAnalysisData]
94
  );
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
95
 
96
+ const handleOpenUploadModal = () => {
97
+ setShowUploadModal(true);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
98
  };
99
 
100
+ const handleCloseUploadModal = () => {
101
+ setShowUploadModal(false);
102
+ };
 
 
103
 
104
+ const handleAnalyzeFromModal = async (file: File) => {
105
  setIsAnalyzing(true);
 
 
106
 
107
  try {
108
+ const response = await analyzeArgumentsFromCsv(file);
109
  setAnalysisResults(response.results);
110
+ // Cache the recent analysis results
111
+ cacheRecentAnalysisData(response.results);
112
  // Refresh user analysis data after successful analysis
113
  const updatedResponse = await getAnalysisResults(1000, 0);
114
  setUserAnalysisData(updatedResponse.results);
115
  // Cache the updated data
116
  cacheAnalysisData(updatedResponse.results);
117
  } catch (err) {
118
+ // For now, just log the error since we don't have a state to store it
119
+ // In a production app, we might want to use a toast notification system
120
+ console.error('Analysis error:', formatError(err));
121
  } finally {
122
  setIsAnalyzing(false);
123
+ setShowUploadModal(false);
124
  }
125
  };
126
 
 
139
  Overview of all your analyzed arguments
140
  </p>
141
  </div>
142
+ <div className="flex items-center gap-2">
143
+ {isRefreshingStats && (
144
+ <div className="flex items-center gap-2 text-xs text-slate-500 dark:text-zinc-400">
145
+ <div className="h-3 w-3 animate-spin rounded-full border-2 border-slate-300 dark:border-zinc-600 border-t-blue-600 dark:border-t-blue-500"></div>
146
+ <span>Updating...</span>
147
+ </div>
148
+ )}
149
+ <button
150
+ onClick={handleOpenUploadModal}
151
+ className="rounded-md bg-blue-600 dark:bg-blue-500 px-4 py-2 text-sm font-semibold text-white transition hover:bg-blue-700 dark:hover:bg-blue-600"
152
+ >
153
+ Upload CSV
154
+ </button>
155
+ </div>
156
  </div>
157
  </div>
158
 
 
201
  </div>
202
 
203
  {/* Charts Grid */}
204
+ <div className="grid grid-cols-1 gap-6">
205
  {/* Stance Distribution */}
206
  <div className="rounded-lg border border-slate-200 dark:border-zinc-700 p-4">
207
  <h3 className="text-sm font-semibold text-slate-700 dark:text-zinc-300 mb-4">
 
221
  </div>
222
  </div>
223
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
224
  </div>
225
 
226
  {/* Topic Frequency */}
 
232
  <TopicFrequencyChart data={topicFrequency} />
233
  </div>
234
  )}
235
+
236
+ {/* Analysis Results Table */}
237
+ {analysisResults.length > 0 && (
238
+ <div className="rounded-lg border border-slate-200 dark:border-zinc-700">
239
+ <div className="bg-green-50 dark:bg-green-900/20 px-4 py-2 border-b border-slate-200 dark:border-zinc-700">
240
+ <h3 className="text-sm font-semibold text-green-800 dark:text-green-400">
241
+ Recent Analysis Results ({analysisResults.length} results)
242
+ </h3>
243
+ </div>
244
+ <div className="overflow-x-auto">
245
+ <table className="min-w-full divide-y divide-slate-200 dark:divide-zinc-700">
246
+ <thead className="bg-slate-50 dark:bg-zinc-800">
247
+ <tr>
248
+ <th className="px-4 py-3 text-left text-sm font-semibold uppercase tracking-wide text-slate-600 dark:text-zinc-300">
249
+ Argument
250
+ </th>
251
+ <th className="px-4 py-3 text-left text-sm font-semibold uppercase tracking-wide text-slate-600 dark:text-zinc-300">
252
+ Topic
253
+ </th>
254
+ <th className="px-4 py-3 text-left text-sm font-semibold uppercase tracking-wide text-slate-600 dark:text-zinc-300">
255
+ Stance
256
+ </th>
257
+ <th className="px-4 py-3 text-left text-sm font-semibold uppercase tracking-wide text-slate-600 dark:text-zinc-300">
258
+ Confidence
259
+ </th>
260
+ <th className="px-4 py-3 text-left text-sm font-semibold uppercase tracking-wide text-slate-600 dark:text-zinc-300">
261
+ PRO
262
+ </th>
263
+ <th className="px-4 py-3 text-left text-sm font-semibold uppercase tracking-wide text-slate-600 dark:text-zinc-300">
264
+ CON
265
+ </th>
266
+ </tr>
267
+ </thead>
268
+ <tbody className="divide-y divide-slate-200 dark:divide-zinc-700 bg-white dark:bg-zinc-900">
269
+ {analysisResults.map((result) => (
270
+ <tr key={result.id} className="hover:bg-slate-50 dark:hover:bg-zinc-800">
271
+ <td className="whitespace-pre-wrap px-4 py-3 text-sm text-slate-700 dark:text-zinc-300 max-w-md">
272
+ {result.argument}
273
+ </td>
274
+ <td className="px-4 py-3 text-sm text-slate-700 dark:text-zinc-300">
275
+ {result.topic}
276
+ </td>
277
+ <td className="px-4 py-3 text-sm">
278
+ <span
279
+ className={`inline-flex rounded-full px-2 py-1 text-xs font-semibold ${
280
+ result.predicted_stance === 'PRO'
281
+ ? 'bg-green-100 dark:bg-green-900/30 text-green-800 dark:text-green-400'
282
+ : 'bg-red-100 dark:bg-red-900/30 text-red-800 dark:text-red-400'
283
+ }`}
284
+ >
285
+ {result.predicted_stance}
286
+ </span>
287
+ </td>
288
+ <td className="px-4 py-3 text-sm text-slate-700 dark:text-zinc-300">
289
+ {(result.confidence * 100).toFixed(1)}%
290
+ </td>
291
+ <td className="px-4 py-3 text-sm text-slate-700 dark:text-zinc-300">
292
+ {(result.probability_pro * 100).toFixed(1)}%
293
+ </td>
294
+ <td className="px-4 py-3 text-sm text-slate-700 dark:text-zinc-300">
295
+ {(result.probability_con * 100).toFixed(1)}%
296
+ </td>
297
+ </tr>
298
+ ))}
299
+ </tbody>
300
+ </table>
301
+ </div>
302
+ </div>
303
+ )}
304
  </div>
305
  )}
306
  </div>
307
 
308
  {/* CSV Upload and Analysis Section */}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
309
 
310
+ {/* Upload CSV Modal */}
311
+ {showUploadModal && (
312
+ <UploadCsvModal
313
+ isOpen={showUploadModal}
314
+ onClose={handleCloseUploadModal}
315
+ onAnalyze={handleAnalyzeFromModal}
316
+ isAnalyzing={isAnalyzing}
317
+ />
318
+ )}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
319
  </div>
320
  </div>
321
  );
src/app/utils/cache.utils.ts CHANGED
@@ -117,3 +117,36 @@ export function clearMcpToolsCache(): void {
117
  }
118
  }
119
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
117
  }
118
  }
119
 
120
+ /**
121
+ * Cache recent analysis results to localStorage
122
+ */
123
+ export function cacheRecentAnalysisData(results: AnalysisResult[]): void {
124
+ try {
125
+ const cacheData: CachedAnalysisData = {
126
+ results,
127
+ timestamp: Date.now(),
128
+ };
129
+ localStorage.setItem('recent_analysis_data_cache', JSON.stringify(cacheData));
130
+ } catch (error) {
131
+ // Silently fail if localStorage is not available or quota exceeded
132
+ console.warn('Failed to cache recent analysis data:', error);
133
+ }
134
+ }
135
+
136
+ /**
137
+ * Get cached recent analysis results from localStorage
138
+ */
139
+ export function getCachedRecentAnalysisData(): AnalysisResult[] | null {
140
+ try {
141
+ const cached = localStorage.getItem('recent_analysis_data_cache');
142
+ if (!cached) return null;
143
+
144
+ const cacheData: CachedAnalysisData = JSON.parse(cached);
145
+ return cacheData.results;
146
+ } catch (error) {
147
+ // Silently fail if localStorage is not available or data is corrupted
148
+ console.warn('Failed to retrieve cached recent analysis data:', error);
149
+ return null;
150
+ }
151
+ }
152
+