Yassine Mhirsi commited on
Commit
b6bed80
·
1 Parent(s): cbf89ee

Add routing and analysis features

Browse files

- Introduced `react-router-dom` for navigation, enabling routing between Home and Analysis pages.
- Created `HomePage` for user registration and navigation.
- Developed `AnalysisPage` for CSV file upload and argument analysis.
- Implemented API services for user management and analysis.
- Updated constants for new API endpoints.
- Enforced explicit file extensions in imports for TypeScript files.
- Added utility functions for user management and CSV parsing.

.cursor/rules/rules.mdc CHANGED
@@ -54,12 +54,12 @@ const MyComponent: React.FC<MyComponentProps> = ({ title, count = 0 }) => {
54
  - Always type API responses
55
 
56
  ```typescript
57
- // Direct API call
58
- import api from '../services/api-wrapper';
59
  const data = await api.get<User[]>('/api/users');
60
 
61
- // With hook
62
- import { useApi } from '../hooks/useApi';
63
  const { data, loading, error } = useApi<User[]>('/api/users');
64
  ```
65
 
@@ -75,6 +75,9 @@ export type Debate = {
75
  topic: string;
76
  arguments: Argument[];
77
  };
 
 
 
78
  ```
79
 
80
  ### Constants
@@ -90,7 +93,13 @@ export type Debate = {
90
 
91
  ## Conventions
92
 
93
- 1. **Imports**: Use relative paths from `src/`
 
 
 
 
 
 
94
  2. **Styling**: Tailwind-first, avoid custom CSS
95
  3. **Error Handling**: Use ErrorBoundary for React errors, try/catch for API errors
96
  4. **Loading States**: Use `Loading` component from `components/common/Loading`
@@ -105,15 +114,27 @@ export type Debate = {
105
  - ❌ Don't create custom CSS files - use Tailwind
106
  - ❌ Don't commit `.env*` files (except `.env.example`)
107
  - ❌ Don't skip TypeScript types
 
 
 
 
 
108
 
109
  ## When Adding New Features
110
 
111
  1. **New Page**: Create in `src/app/pages/` (`.tsx`), wire via `App.tsx`
 
112
  2. **New Component**: Create in `src/app/components/` (`.tsx`), type props
 
113
  3. **New API Endpoint**: Add to `API_ENDPOINTS` in constants, use `api-wrapper.ts`
 
114
  4. **New Type**: Add to `src/app/types/`, export from `index.ts`
 
115
  5. **New Hook**: Create in `src/app/hooks/` (`.ts`), type return values
 
116
  6. **New Utility**: Add to `src/app/utils/index.ts`, keep pure and typed
 
 
117
 
118
  ## Quality Checks
119
 
 
54
  - Always type API responses
55
 
56
  ```typescript
57
+ // Direct API call (note: include .ts extension)
58
+ import api from '../services/api-wrapper.ts';
59
  const data = await api.get<User[]>('/api/users');
60
 
61
+ // With hook (note: include .ts extension)
62
+ import { useApi } from '../hooks/useApi.ts';
63
  const { data, loading, error } = useApi<User[]>('/api/users');
64
  ```
65
 
 
75
  topic: string;
76
  arguments: Argument[];
77
  };
78
+
79
+ // When importing types, use explicit extension:
80
+ import type { Debate } from '../types/debater.types.ts';
81
  ```
82
 
83
  ### Constants
 
93
 
94
  ## Conventions
95
 
96
+ 1. **Imports**:
97
+ - Use relative paths from `src/`
98
+ - **ALWAYS include explicit file extensions** in imports:
99
+ - Use `.tsx` for React components (e.g., `import Component from './Component.tsx'`)
100
+ - Use `.ts` for TypeScript files (e.g., `import { func } from './utils/index.ts'`)
101
+ - Use `/index.ts` for directory barrel exports (e.g., `import { something } from './utils/index.ts'`)
102
+ - This is required for webpack module resolution in this project
103
  2. **Styling**: Tailwind-first, avoid custom CSS
104
  3. **Error Handling**: Use ErrorBoundary for React errors, try/catch for API errors
105
  4. **Loading States**: Use `Loading` component from `components/common/Loading`
 
114
  - ❌ Don't create custom CSS files - use Tailwind
115
  - ❌ Don't commit `.env*` files (except `.env.example`)
116
  - ❌ Don't skip TypeScript types
117
+ - ❌ **Don't omit file extensions in imports** - Always include `.ts` or `.tsx` extensions
118
+ - ❌ Bad: `import Component from './Component'`
119
+ - ✅ Good: `import Component from './Component.tsx'`
120
+ - ❌ Bad: `export * from './user.utils'`
121
+ - ✅ Good: `export * from './user.utils.ts'`
122
 
123
  ## When Adding New Features
124
 
125
  1. **New Page**: Create in `src/app/pages/` (`.tsx`), wire via `App.tsx`
126
+ - Import with `.tsx` extension: `import NewPage from './pages/NewPage.tsx'`
127
  2. **New Component**: Create in `src/app/components/` (`.tsx`), type props
128
+ - Import with `.tsx` extension: `import Component from './components/Component.tsx'`
129
  3. **New API Endpoint**: Add to `API_ENDPOINTS` in constants, use `api-wrapper.ts`
130
+ - Import services with `.ts` extension: `import { service } from './services/service.ts'`
131
  4. **New Type**: Add to `src/app/types/`, export from `index.ts`
132
+ - Import types with `.ts` extension: `import type { Type } from '../types/type.types.ts'`
133
  5. **New Hook**: Create in `src/app/hooks/` (`.ts`), type return values
134
+ - Import hooks with `.ts` extension: `import { useHook } from '../hooks/useHook.ts'`
135
  6. **New Utility**: Add to `src/app/utils/index.ts`, keep pure and typed
136
+ - Export with `.ts` extension: `export * from './new-util.ts'`
137
+ - Import from index: `import { util } from '../utils/index.ts'`
138
 
139
  ## Quality Checks
140
 
package-lock.json CHANGED
@@ -12,8 +12,10 @@
12
  "@testing-library/jest-dom": "^6.6.3",
13
  "@testing-library/react": "^16.3.0",
14
  "@testing-library/user-event": "^13.5.0",
 
15
  "react": "^19.1.0",
16
  "react-dom": "^19.1.0",
 
17
  "react-scripts": "5.0.1",
18
  "web-vitals": "^2.1.4"
19
  },
@@ -13381,6 +13383,19 @@
13381
  "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
13382
  "license": "MIT"
13383
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
13384
  "node_modules/prelude-ls": {
13385
  "version": "1.2.1",
13386
  "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
@@ -13784,6 +13799,57 @@
13784
  "node": ">=0.10.0"
13785
  }
13786
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
13787
  "node_modules/react-scripts": {
13788
  "version": "5.0.1",
13789
  "resolved": "https://registry.npmjs.org/react-scripts/-/react-scripts-5.0.1.tgz",
@@ -14767,6 +14833,12 @@
14767
  "node": ">= 0.8"
14768
  }
14769
  },
 
 
 
 
 
 
14770
  "node_modules/set-function-length": {
14771
  "version": "1.2.2",
14772
  "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
 
12
  "@testing-library/jest-dom": "^6.6.3",
13
  "@testing-library/react": "^16.3.0",
14
  "@testing-library/user-event": "^13.5.0",
15
+ "postgres": "^3.4.7",
16
  "react": "^19.1.0",
17
  "react-dom": "^19.1.0",
18
+ "react-router-dom": "^7.10.1",
19
  "react-scripts": "5.0.1",
20
  "web-vitals": "^2.1.4"
21
  },
 
13383
  "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
13384
  "license": "MIT"
13385
  },
13386
+ "node_modules/postgres": {
13387
+ "version": "3.4.7",
13388
+ "resolved": "https://registry.npmjs.org/postgres/-/postgres-3.4.7.tgz",
13389
+ "integrity": "sha512-Jtc2612XINuBjIl/QTWsV5UvE8UHuNblcO3vVADSrKsrc6RqGX6lOW1cEo3CM2v0XG4Nat8nI+YM7/f26VxXLw==",
13390
+ "license": "Unlicense",
13391
+ "engines": {
13392
+ "node": ">=12"
13393
+ },
13394
+ "funding": {
13395
+ "type": "individual",
13396
+ "url": "https://github.com/sponsors/porsager"
13397
+ }
13398
+ },
13399
  "node_modules/prelude-ls": {
13400
  "version": "1.2.1",
13401
  "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
 
13799
  "node": ">=0.10.0"
13800
  }
13801
  },
13802
+ "node_modules/react-router": {
13803
+ "version": "7.10.1",
13804
+ "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.10.1.tgz",
13805
+ "integrity": "sha512-gHL89dRa3kwlUYtRQ+m8NmxGI6CgqN+k4XyGjwcFoQwwCWF6xXpOCUlDovkXClS0d0XJN/5q7kc5W3kiFEd0Yw==",
13806
+ "license": "MIT",
13807
+ "dependencies": {
13808
+ "cookie": "^1.0.1",
13809
+ "set-cookie-parser": "^2.6.0"
13810
+ },
13811
+ "engines": {
13812
+ "node": ">=20.0.0"
13813
+ },
13814
+ "peerDependencies": {
13815
+ "react": ">=18",
13816
+ "react-dom": ">=18"
13817
+ },
13818
+ "peerDependenciesMeta": {
13819
+ "react-dom": {
13820
+ "optional": true
13821
+ }
13822
+ }
13823
+ },
13824
+ "node_modules/react-router-dom": {
13825
+ "version": "7.10.1",
13826
+ "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.10.1.tgz",
13827
+ "integrity": "sha512-JNBANI6ChGVjA5bwsUIwJk7LHKmqB4JYnYfzFwyp2t12Izva11elds2jx7Yfoup2zssedntwU0oZ5DEmk5Sdaw==",
13828
+ "license": "MIT",
13829
+ "dependencies": {
13830
+ "react-router": "7.10.1"
13831
+ },
13832
+ "engines": {
13833
+ "node": ">=20.0.0"
13834
+ },
13835
+ "peerDependencies": {
13836
+ "react": ">=18",
13837
+ "react-dom": ">=18"
13838
+ }
13839
+ },
13840
+ "node_modules/react-router/node_modules/cookie": {
13841
+ "version": "1.1.1",
13842
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz",
13843
+ "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==",
13844
+ "license": "MIT",
13845
+ "engines": {
13846
+ "node": ">=18"
13847
+ },
13848
+ "funding": {
13849
+ "type": "opencollective",
13850
+ "url": "https://opencollective.com/express"
13851
+ }
13852
+ },
13853
  "node_modules/react-scripts": {
13854
  "version": "5.0.1",
13855
  "resolved": "https://registry.npmjs.org/react-scripts/-/react-scripts-5.0.1.tgz",
 
14833
  "node": ">= 0.8"
14834
  }
14835
  },
14836
+ "node_modules/set-cookie-parser": {
14837
+ "version": "2.7.2",
14838
+ "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz",
14839
+ "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==",
14840
+ "license": "MIT"
14841
+ },
14842
  "node_modules/set-function-length": {
14843
  "version": "1.2.2",
14844
  "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
package.json CHANGED
@@ -7,8 +7,10 @@
7
  "@testing-library/jest-dom": "^6.6.3",
8
  "@testing-library/react": "^16.3.0",
9
  "@testing-library/user-event": "^13.5.0",
 
10
  "react": "^19.1.0",
11
  "react-dom": "^19.1.0",
 
12
  "react-scripts": "5.0.1",
13
  "web-vitals": "^2.1.4"
14
  },
 
7
  "@testing-library/jest-dom": "^6.6.3",
8
  "@testing-library/react": "^16.3.0",
9
  "@testing-library/user-event": "^13.5.0",
10
+ "postgres": "^3.4.7",
11
  "react": "^19.1.0",
12
  "react-dom": "^19.1.0",
13
+ "react-router-dom": "^7.10.1",
14
  "react-scripts": "5.0.1",
15
  "web-vitals": "^2.1.4"
16
  },
src/app/App.tsx CHANGED
@@ -1,13 +1,35 @@
1
  import React from 'react';
 
2
  import MainLayout from './layouts/MainLayout.tsx';
 
 
 
3
 
4
- const App: React.FC = () => (
5
- <MainLayout>
6
- <div className="container mx-auto px-4 py-8">
7
- {/* Your content here */}
8
- </div>
9
- </MainLayout>
10
- );
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
11
 
12
  export default App;
13
 
 
1
  import React from 'react';
2
+ import { BrowserRouter, Navigate, Route, Routes } from 'react-router-dom';
3
  import MainLayout from './layouts/MainLayout.tsx';
4
+ import HomePage from './pages/HomePage.tsx';
5
+ import AnalysisPage from './pages/AnalysisPage.tsx';
6
+ import { isUserRegistered } from './utils/index.ts';
7
 
8
+ const App: React.FC = () => {
9
+ // Check if user is already registered
10
+ const userRegistered = isUserRegistered();
11
+
12
+ return (
13
+ <BrowserRouter>
14
+ <MainLayout>
15
+ <Routes>
16
+ <Route
17
+ path="/"
18
+ element={
19
+ userRegistered ? (
20
+ <Navigate to="/analysis" replace />
21
+ ) : (
22
+ <HomePage />
23
+ )
24
+ }
25
+ />
26
+ <Route path="/analysis" element={<AnalysisPage />} />
27
+ <Route path="*" element={<Navigate to="/" replace />} />
28
+ </Routes>
29
+ </MainLayout>
30
+ </BrowserRouter>
31
+ );
32
+ };
33
 
34
  export default App;
35
 
src/app/constants/index.ts CHANGED
@@ -4,7 +4,12 @@
4
 
5
  // API Endpoints (add your endpoints here as you build)
6
  export const API_ENDPOINTS = {
7
-
 
 
 
 
 
8
  } as const;
9
 
10
  // App configuration
 
4
 
5
  // API Endpoints (add your endpoints here as you build)
6
  export const API_ENDPOINTS = {
7
+ USER_REGISTER: '/api/v1/user/register',
8
+ USER_ME: '/api/v1/user/me',
9
+ USER_BY_UNIQUE_ID: '/api/v1/user/by-unique-id',
10
+ USER_UPDATE_NAME: '/api/v1/user/me/name',
11
+ ANALYSIS: '/api/v1/analyse',
12
+ ANALYSIS_CSV: '/api/v1/analyse/csv',
13
  } as const;
14
 
15
  // App configuration
src/app/pages/AnalysisPage.tsx ADDED
@@ -0,0 +1,229 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useMemo, useState } from 'react';
2
+ import { parseCsv, formatError } from '../utils/index.ts';
3
+ import { CsvRow } from '../types/index.ts';
4
+ import { AnalysisResult } from '../types/analysis.types.ts';
5
+ import { analyzeArgumentsFromCsv } from '../services/analysis.service.ts';
6
+
7
+ const AnalysisPage: React.FC = () => {
8
+ const [selectedFile, setSelectedFile] = useState<File | null>(null);
9
+ const [headers, setHeaders] = useState<string[]>(['id', 'argument']);
10
+ const [rows, setRows] = useState<CsvRow[]>([]);
11
+ const [error, setError] = useState<string | null>(null);
12
+ const [isAnalyzing, setIsAnalyzing] = useState<boolean>(false);
13
+ const [analysisResults, setAnalysisResults] = useState<AnalysisResult[]>([]);
14
+
15
+ const fileName = useMemo(
16
+ () => selectedFile?.name ?? 'Select a CSV file with arguments',
17
+ [selectedFile]
18
+ );
19
+
20
+ const handleFileChange = async (event: React.ChangeEvent<HTMLInputElement>) => {
21
+ const file = event.target.files?.[0] ?? null;
22
+ setSelectedFile(file);
23
+ setError(null);
24
+ setAnalysisResults([]);
25
+
26
+ if (!file) {
27
+ setHeaders(['id', 'argument']);
28
+ setRows([]);
29
+ return;
30
+ }
31
+
32
+ // Automatically parse and preview CSV
33
+ try {
34
+ const content = await file.text();
35
+ const parsed = parseCsv(content);
36
+
37
+ setHeaders(parsed.headers.length > 0 ? parsed.headers : ['id', 'argument']);
38
+ setRows(parsed.rows);
39
+
40
+ if (parsed.rows.length === 0) {
41
+ setError('No data found in the CSV file.');
42
+ }
43
+ } catch (err) {
44
+ setError(
45
+ err instanceof Error ? err.message : 'Unable to read the CSV file.'
46
+ );
47
+ setHeaders(['id', 'argument']);
48
+ setRows([]);
49
+ }
50
+ };
51
+
52
+ const handleAnalyze = async () => {
53
+ if (!selectedFile) {
54
+ setError('Please choose a CSV file to analyze.');
55
+ return;
56
+ }
57
+
58
+ setIsAnalyzing(true);
59
+ setError(null);
60
+ setAnalysisResults([]);
61
+
62
+ try {
63
+ const response = await analyzeArgumentsFromCsv(selectedFile);
64
+ setAnalysisResults(response.results);
65
+ } catch (err) {
66
+ setError(formatError(err));
67
+ } finally {
68
+ setIsAnalyzing(false);
69
+ }
70
+ };
71
+
72
+ return (
73
+ <div className="min-h-screen bg-slate-50 px-4 py-10">
74
+ <div className="mx-auto max-w-4xl rounded-lg border border-slate-200 bg-white shadow-sm">
75
+ <div className="border-b border-slate-200 px-6 py-4">
76
+ <h1 className="text-lg font-semibold text-slate-800">
77
+ Analysis page (open static)
78
+ </h1>
79
+ <p className="text-sm text-slate-500">
80
+ Upload a CSV file containing arguments to preview its contents.
81
+ </p>
82
+ </div>
83
+
84
+ <div className="space-y-6 p-6">
85
+ <div className="flex flex-col gap-3">
86
+ <label className="text-sm font-medium text-slate-700">
87
+ Upload a CSV file
88
+ </label>
89
+
90
+ <div className="flex flex-wrap items-center gap-3">
91
+ <label className="flex w-full max-w-xl cursor-pointer items-center justify-between rounded-full border border-slate-300 bg-slate-50 px-5 py-3 transition hover:border-slate-400">
92
+ <span className="truncate text-sm font-semibold text-slate-500">
93
+ {fileName}
94
+ </span>
95
+ <input
96
+ type="file"
97
+ accept=".csv,text/csv"
98
+ className="hidden"
99
+ onChange={handleFileChange}
100
+ />
101
+ </label>
102
+
103
+ <button
104
+ type="button"
105
+ onClick={handleAnalyze}
106
+ disabled={!selectedFile || rows.length === 0 || isAnalyzing}
107
+ className="rounded-md bg-blue-600 px-4 py-2 text-sm font-semibold text-white transition hover:bg-blue-700 disabled:cursor-not-allowed disabled:opacity-60"
108
+ >
109
+ {isAnalyzing ? 'Analyzing...' : 'Analyze'}
110
+ </button>
111
+ </div>
112
+
113
+ {error && <p className="text-sm text-red-600">{error}</p>}
114
+ </div>
115
+
116
+ {rows.length > 0 && (
117
+ <div className="overflow-hidden rounded-lg border border-slate-200">
118
+ <div className="bg-slate-50 px-4 py-2 border-b border-slate-200">
119
+ <h2 className="text-sm font-semibold text-slate-700">
120
+ CSV Preview ({rows.length} rows)
121
+ </h2>
122
+ </div>
123
+ <table className="min-w-full divide-y divide-slate-200">
124
+ <thead className="bg-slate-50">
125
+ <tr>
126
+ {headers.map((header) => (
127
+ <th
128
+ key={header}
129
+ scope="col"
130
+ className="px-4 py-3 text-left text-sm font-semibold uppercase tracking-wide text-slate-600"
131
+ >
132
+ {header}
133
+ </th>
134
+ ))}
135
+ </tr>
136
+ </thead>
137
+ <tbody className="divide-y divide-slate-200 bg-white">
138
+ {rows.map((row, index) => (
139
+ <tr key={row.id ?? `row-${index}`} className="hover:bg-slate-50">
140
+ {headers.map((header) => (
141
+ <td
142
+ key={`${header}-${index}`}
143
+ className="whitespace-pre-wrap px-4 py-3 text-sm text-slate-700"
144
+ >
145
+ {row[header] ?? ''}
146
+ </td>
147
+ ))}
148
+ </tr>
149
+ ))}
150
+ </tbody>
151
+ </table>
152
+ </div>
153
+ )}
154
+
155
+ {analysisResults.length > 0 && (
156
+ <div className="overflow-hidden rounded-lg border border-slate-200">
157
+ <div className="bg-green-50 px-4 py-2 border-b border-slate-200">
158
+ <h2 className="text-sm font-semibold text-green-800">
159
+ Analysis Results ({analysisResults.length} results)
160
+ </h2>
161
+ </div>
162
+ <div className="overflow-x-auto">
163
+ <table className="min-w-full divide-y divide-slate-200">
164
+ <thead className="bg-slate-50">
165
+ <tr>
166
+ <th className="px-4 py-3 text-left text-sm font-semibold uppercase tracking-wide text-slate-600">
167
+ Argument
168
+ </th>
169
+ <th className="px-4 py-3 text-left text-sm font-semibold uppercase tracking-wide text-slate-600">
170
+ Topic
171
+ </th>
172
+ <th className="px-4 py-3 text-left text-sm font-semibold uppercase tracking-wide text-slate-600">
173
+ Stance
174
+ </th>
175
+ <th className="px-4 py-3 text-left text-sm font-semibold uppercase tracking-wide text-slate-600">
176
+ Confidence
177
+ </th>
178
+ <th className="px-4 py-3 text-left text-sm font-semibold uppercase tracking-wide text-slate-600">
179
+ PRO
180
+ </th>
181
+ <th className="px-4 py-3 text-left text-sm font-semibold uppercase tracking-wide text-slate-600">
182
+ CON
183
+ </th>
184
+ </tr>
185
+ </thead>
186
+ <tbody className="divide-y divide-slate-200 bg-white">
187
+ {analysisResults.map((result) => (
188
+ <tr key={result.id} className="hover:bg-slate-50">
189
+ <td className="whitespace-pre-wrap px-4 py-3 text-sm text-slate-700 max-w-md">
190
+ {result.argument}
191
+ </td>
192
+ <td className="px-4 py-3 text-sm text-slate-700">
193
+ {result.topic}
194
+ </td>
195
+ <td className="px-4 py-3 text-sm">
196
+ <span
197
+ className={`inline-flex rounded-full px-2 py-1 text-xs font-semibold ${
198
+ result.predicted_stance === 'PRO'
199
+ ? 'bg-green-100 text-green-800'
200
+ : 'bg-red-100 text-red-800'
201
+ }`}
202
+ >
203
+ {result.predicted_stance}
204
+ </span>
205
+ </td>
206
+ <td className="px-4 py-3 text-sm text-slate-700">
207
+ {(result.confidence * 100).toFixed(1)}%
208
+ </td>
209
+ <td className="px-4 py-3 text-sm text-slate-700">
210
+ {(result.probability_pro * 100).toFixed(1)}%
211
+ </td>
212
+ <td className="px-4 py-3 text-sm text-slate-700">
213
+ {(result.probability_con * 100).toFixed(1)}%
214
+ </td>
215
+ </tr>
216
+ ))}
217
+ </tbody>
218
+ </table>
219
+ </div>
220
+ </div>
221
+ )}
222
+ </div>
223
+ </div>
224
+ </div>
225
+ );
226
+ };
227
+
228
+ export default AnalysisPage;
229
+
src/app/pages/HomePage.tsx ADDED
@@ -0,0 +1,115 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState } from 'react';
2
+ import { useNavigate } from 'react-router-dom';
3
+ import { registerUser } from '../services/user.service.ts';
4
+ import { getOrCreateUniqueId, setUserId, formatError } from '../utils/index.ts';
5
+ import Loading from '../components/common/Loading.tsx';
6
+
7
+ const HomePage: React.FC = () => {
8
+ const [name, setName] = useState<string>('');
9
+ const [isLoading, setIsLoading] = useState<boolean>(false);
10
+ const [error, setError] = useState<string | null>(null);
11
+ const navigate = useNavigate();
12
+
13
+ const handleSubmit = async (e: React.FormEvent) => {
14
+ e.preventDefault();
15
+ await registerAndNavigate(name.trim() || null);
16
+ };
17
+
18
+ const handleSkip = async () => {
19
+ await registerAndNavigate(null);
20
+ };
21
+
22
+ const registerAndNavigate = async (userName: string | null) => {
23
+ setIsLoading(true);
24
+ setError(null);
25
+
26
+ try {
27
+ const uniqueId = getOrCreateUniqueId();
28
+ const user = await registerUser({
29
+ unique_id: uniqueId,
30
+ name: userName,
31
+ });
32
+
33
+ // Store user_id for future API calls
34
+ setUserId(user.id);
35
+
36
+ // Navigate to analysis page
37
+ navigate('/analysis');
38
+ } catch (err) {
39
+ const errorMessage = formatError(err);
40
+ setError(errorMessage);
41
+ setIsLoading(false);
42
+ }
43
+ };
44
+
45
+ if (isLoading) {
46
+ return (
47
+ <div className="flex min-h-screen items-center justify-center bg-slate-50">
48
+ <Loading />
49
+ </div>
50
+ );
51
+ }
52
+
53
+ return (
54
+ <div className="flex min-h-screen items-center justify-center bg-slate-50 px-4 py-10">
55
+ <div className="w-full max-w-md rounded-lg border border-slate-200 bg-white shadow-sm">
56
+ <div className="border-b border-slate-200 px-6 py-6">
57
+ <h1 className="text-2xl font-bold text-slate-800">
58
+ Welcome to NLP IBM Debater
59
+ </h1>
60
+ <p className="mt-2 text-sm text-slate-500">
61
+ Enter your name to get started, or skip to use a random name.
62
+ </p>
63
+ </div>
64
+
65
+ <form onSubmit={handleSubmit} className="space-y-6 p-6">
66
+ <div>
67
+ <label
68
+ htmlFor="name"
69
+ className="block text-sm font-medium text-slate-700"
70
+ >
71
+ Your Name (Optional)
72
+ </label>
73
+ <input
74
+ id="name"
75
+ type="text"
76
+ value={name}
77
+ onChange={(e) => setName(e.target.value)}
78
+ placeholder="Enter your name"
79
+ maxLength={100}
80
+ className="mt-2 w-full rounded-md border border-slate-300 px-4 py-2 text-sm text-slate-700 placeholder-slate-400 focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-opacity-20"
81
+ disabled={isLoading}
82
+ />
83
+ </div>
84
+
85
+ {error && (
86
+ <div className="rounded-md bg-red-50 p-3">
87
+ <p className="text-sm text-red-600">{error}</p>
88
+ </div>
89
+ )}
90
+
91
+ <div className="flex flex-col gap-3 sm:flex-row">
92
+ <button
93
+ type="submit"
94
+ disabled={isLoading}
95
+ className="flex-1 rounded-md bg-blue-600 px-4 py-2 text-sm font-semibold text-white transition hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-60"
96
+ >
97
+ Continue
98
+ </button>
99
+ <button
100
+ type="button"
101
+ onClick={handleSkip}
102
+ disabled={isLoading}
103
+ className="flex-1 rounded-md border border-slate-300 bg-white px-4 py-2 text-sm font-semibold text-slate-700 transition hover:bg-slate-50 focus:outline-none focus:ring-2 focus:ring-slate-500 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-60"
104
+ >
105
+ Skip (Random Name)
106
+ </button>
107
+ </div>
108
+ </form>
109
+ </div>
110
+ </div>
111
+ );
112
+ };
113
+
114
+ export default HomePage;
115
+
src/app/services/analysis.service.ts ADDED
@@ -0,0 +1,83 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Analysis service for API calls related to argument analysis
3
+ */
4
+
5
+ import api from './api-wrapper.ts';
6
+ import type {
7
+ AnalysisRequest,
8
+ AnalysisResponse,
9
+ GetAnalysisResponse,
10
+ } from '../types/analysis.types.ts';
11
+ import { API_ENDPOINTS } from '../constants/index.ts';
12
+ import { getUserId } from '../utils/index.ts';
13
+
14
+ /**
15
+ * Analyze arguments from JSON body
16
+ */
17
+ export async function analyzeArguments(
18
+ request: AnalysisRequest
19
+ ): Promise<AnalysisResponse> {
20
+ return api.post<AnalysisResponse, AnalysisRequest>(
21
+ API_ENDPOINTS.ANALYSIS,
22
+ request
23
+ );
24
+ }
25
+
26
+ /**
27
+ * Analyze arguments from CSV file upload
28
+ */
29
+ export async function analyzeArgumentsFromCsv(
30
+ file: File
31
+ ): Promise<AnalysisResponse> {
32
+ const formData = new FormData();
33
+ formData.append('file', file);
34
+
35
+ const BASE_URL =
36
+ process.env.REACT_APP_API_BASE_URL?.replace(/\/$/, '') || '';
37
+ const url = `${BASE_URL}${API_ENDPOINTS.ANALYSIS_CSV}`;
38
+
39
+ // Get user ID for header
40
+ const userId = getUserId();
41
+ const headers: Record<string, string> = {};
42
+ if (userId) {
43
+ headers['X-User-ID'] = userId;
44
+ }
45
+
46
+ const response = await fetch(url, {
47
+ method: 'POST',
48
+ headers,
49
+ body: formData,
50
+ });
51
+
52
+ const contentType = response.headers.get('content-type');
53
+ const isJson = contentType?.includes('application/json');
54
+ const payload = isJson ? await response.json().catch(() => undefined) : undefined;
55
+
56
+ if (!response.ok) {
57
+ const error = {
58
+ status: response.status,
59
+ message:
60
+ (payload as { detail?: string })?.detail ||
61
+ (payload as { message?: string })?.message ||
62
+ response.statusText ||
63
+ 'Request failed',
64
+ details: payload,
65
+ };
66
+ throw error;
67
+ }
68
+
69
+ return payload as AnalysisResponse;
70
+ }
71
+
72
+ /**
73
+ * Get user's analysis results
74
+ */
75
+ export async function getAnalysisResults(
76
+ limit: number = 100,
77
+ offset: number = 0
78
+ ): Promise<GetAnalysisResponse> {
79
+ return api.get<GetAnalysisResponse>(
80
+ `${API_ENDPOINTS.ANALYSIS}?limit=${limit}&offset=${offset}`
81
+ );
82
+ }
83
+
src/app/services/api-wrapper.ts CHANGED
@@ -1,4 +1,5 @@
1
- import type { ApiRequestOptions, ApiError } from '../types/api.types';
 
2
 
3
  const DEFAULT_HEADERS: HeadersInit = {
4
  'Content-Type': 'application/json',
@@ -18,12 +19,29 @@ export async function apiRequest<TResponse = unknown, TBody = unknown>({
18
  }: ApiRequestOptions<TBody>): Promise<TResponse> {
19
  const url = `${BASE_URL}${path.startsWith('/') ? '' : '/'}${path}`;
20
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
21
  const response = await fetch(url, {
22
  method,
23
- headers: {
24
- ...DEFAULT_HEADERS,
25
- ...(headers ?? {}),
26
- },
27
  body: body ? JSON.stringify(body) : undefined,
28
  signal,
29
  });
 
1
+ import type { ApiRequestOptions, ApiError } from '../types/api.types.ts';
2
+ import { getUserId } from '../utils/index.ts';
3
 
4
  const DEFAULT_HEADERS: HeadersInit = {
5
  'Content-Type': 'application/json',
 
19
  }: ApiRequestOptions<TBody>): Promise<TResponse> {
20
  const url = `${BASE_URL}${path.startsWith('/') ? '' : '/'}${path}`;
21
 
22
+ // Automatically include user ID header if available
23
+ const userId = getUserId();
24
+
25
+ // Convert headers to plain object if needed
26
+ let baseHeaders: Record<string, string> = {};
27
+ if (headers instanceof Headers) {
28
+ baseHeaders = Object.fromEntries(headers.entries());
29
+ } else if (headers) {
30
+ baseHeaders = headers as Record<string, string>;
31
+ }
32
+
33
+ const requestHeaders: Record<string, string> = {
34
+ ...(DEFAULT_HEADERS as Record<string, string>),
35
+ ...baseHeaders,
36
+ };
37
+
38
+ if (userId) {
39
+ requestHeaders['X-User-ID'] = userId;
40
+ }
41
+
42
  const response = await fetch(url, {
43
  method,
44
+ headers: requestHeaders,
 
 
 
45
  body: body ? JSON.stringify(body) : undefined,
46
  signal,
47
  });
src/app/services/user.service.ts ADDED
@@ -0,0 +1,76 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * User service for API calls related to user management
3
+ */
4
+
5
+ import api from './api-wrapper.ts';
6
+ import type {
7
+ UserRegisterRequest,
8
+ UserRegisterResponse,
9
+ UserUpdateNameRequest,
10
+ UserUpdateNameResponse,
11
+ User,
12
+ } from '../types/user.types.ts';
13
+ import { API_ENDPOINTS } from '../constants/index.ts';
14
+ import { getUserId } from '../utils/index.ts';
15
+
16
+ /**
17
+ * Register a new user or get existing user by unique_id
18
+ */
19
+ export async function registerUser(
20
+ request: UserRegisterRequest
21
+ ): Promise<UserRegisterResponse> {
22
+ return api.post<UserRegisterResponse, UserRegisterRequest>(
23
+ API_ENDPOINTS.USER_REGISTER,
24
+ request
25
+ );
26
+ }
27
+
28
+ /**
29
+ * Get current user by user_id (requires X-User-ID header)
30
+ */
31
+ export async function getCurrentUser(): Promise<User> {
32
+ const userId = getUserId();
33
+ if (!userId) {
34
+ throw new Error('User ID not found in localStorage');
35
+ }
36
+
37
+ return api.get<User>(API_ENDPOINTS.USER_ME, {
38
+ headers: {
39
+ 'X-User-ID': userId,
40
+ },
41
+ });
42
+ }
43
+
44
+ /**
45
+ * Get user by unique_id
46
+ */
47
+ export async function getUserByUniqueId(
48
+ uniqueId: string
49
+ ): Promise<User> {
50
+ return api.get<User>(
51
+ `${API_ENDPOINTS.USER_BY_UNIQUE_ID}?unique_id=${encodeURIComponent(uniqueId)}`
52
+ );
53
+ }
54
+
55
+ /**
56
+ * Update user's display name
57
+ */
58
+ export async function updateUserName(
59
+ request: UserUpdateNameRequest
60
+ ): Promise<UserUpdateNameResponse> {
61
+ const userId = getUserId();
62
+ if (!userId) {
63
+ throw new Error('User ID not found in localStorage');
64
+ }
65
+
66
+ return api.patch<UserUpdateNameResponse, UserUpdateNameRequest>(
67
+ API_ENDPOINTS.USER_UPDATE_NAME,
68
+ request,
69
+ {
70
+ headers: {
71
+ 'X-User-ID': userId,
72
+ },
73
+ }
74
+ );
75
+ }
76
+
src/app/types/analysis.types.ts ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export type CsvRow = Record<string, string>;
2
+
3
+ export type CsvParseResult = {
4
+ headers: string[];
5
+ rows: CsvRow[];
6
+ };
7
+
8
+ /**
9
+ * Analysis result for a single argument
10
+ */
11
+ export type AnalysisResult = {
12
+ id: string;
13
+ user_id: string;
14
+ argument: string;
15
+ topic: string;
16
+ predicted_stance: 'PRO' | 'CON';
17
+ confidence: number;
18
+ probability_con: number;
19
+ probability_pro: number;
20
+ created_at: string;
21
+ updated_at: string;
22
+ };
23
+
24
+ /**
25
+ * Response from analysis endpoint
26
+ */
27
+ export type AnalysisResponse = {
28
+ results: AnalysisResult[];
29
+ total_processed: number;
30
+ timestamp: string;
31
+ };
32
+
33
+ /**
34
+ * Request for analysis with JSON body
35
+ */
36
+ export type AnalysisRequest = {
37
+ arguments: string[];
38
+ };
39
+
40
+ /**
41
+ * Response from get analysis results endpoint
42
+ */
43
+ export type GetAnalysisResponse = {
44
+ results: AnalysisResult[];
45
+ total: number;
46
+ limit: number;
47
+ offset: number;
48
+ };
49
+
src/app/types/index.ts CHANGED
@@ -1,7 +1,9 @@
1
  /**
2
  * Central export point for all types
3
- * Import types from here: import { SomeType } from '../types';
4
  */
5
 
6
- export * from './api.types';
 
 
7
 
 
1
  /**
2
  * Central export point for all types
3
+ * Import types from here: import { SomeType } from '../types/index.ts';
4
  */
5
 
6
+ export * from './api.types.ts';
7
+ export * from './analysis.types.ts';
8
+ export * from './user.types.ts';
9
 
src/app/types/user.types.ts ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * User-related TypeScript types
3
+ */
4
+
5
+ export type User = {
6
+ id: string;
7
+ unique_id: string;
8
+ name: string;
9
+ created_at: string;
10
+ updated_at: string;
11
+ };
12
+
13
+ export type UserRegisterRequest = {
14
+ unique_id: string;
15
+ name?: string | null;
16
+ };
17
+
18
+ export type UserRegisterResponse = User;
19
+
20
+ export type UserUpdateNameRequest = {
21
+ name: string;
22
+ };
23
+
24
+ export type UserUpdateNameResponse = User;
25
+
src/app/utils/index.ts CHANGED
@@ -2,6 +2,11 @@
2
  * Utility functions and helpers
3
  */
4
 
 
 
 
 
 
5
  /**
6
  * Debounce function - delays execution until after wait time
7
  */
@@ -49,3 +54,65 @@ export function isEmpty(value: unknown): boolean {
49
  return false;
50
  }
51
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2
  * Utility functions and helpers
3
  */
4
 
5
+ import { CsvParseResult, CsvRow } from '../types/index.ts';
6
+
7
+ // Export user utilities
8
+ export * from './user.utils.ts';
9
+
10
  /**
11
  * Debounce function - delays execution until after wait time
12
  */
 
54
  return false;
55
  }
56
 
57
+ /**
58
+ * Parse a CSV string into headers and rows.
59
+ * Supports quoted fields and trims whitespace around values.
60
+ */
61
+ export function parseCsv(content: string): CsvParseResult {
62
+ const lines = content.split(/\r?\n/).filter((line) => line.trim().length > 0);
63
+
64
+ if (lines.length === 0) {
65
+ return { headers: [], rows: [] };
66
+ }
67
+
68
+ const parseLine = (line: string): string[] => {
69
+ const cells: string[] = [];
70
+ let current = '';
71
+ let inQuotes = false;
72
+
73
+ for (let i = 0; i < line.length; i += 1) {
74
+ const char = line[i];
75
+ const nextChar = line[i + 1];
76
+
77
+ if (char === '"') {
78
+ if (inQuotes && nextChar === '"') {
79
+ current += '"';
80
+ i += 1;
81
+ } else {
82
+ inQuotes = !inQuotes;
83
+ }
84
+ } else if (char === ',' && !inQuotes) {
85
+ cells.push(current.trim());
86
+ current = '';
87
+ } else {
88
+ current += char;
89
+ }
90
+ }
91
+
92
+ cells.push(current.trim());
93
+ return cells;
94
+ };
95
+
96
+ const headers = parseLine(lines[0]).map((header, index) =>
97
+ header ? header : `column_${index + 1}`
98
+ );
99
+
100
+ const rows: CsvRow[] = lines.slice(1).map((line) => {
101
+ const values = parseLine(line);
102
+ const row: CsvRow = {};
103
+
104
+ headers.forEach((header, idx) => {
105
+ row[header] = values[idx]?.trim() ?? '';
106
+ });
107
+
108
+ return row;
109
+ });
110
+
111
+ return {
112
+ headers,
113
+ rows: rows.filter((row) =>
114
+ Object.values(row).some((value) => value !== undefined && value !== '')
115
+ ),
116
+ };
117
+ }
118
+
src/app/utils/user.utils.ts ADDED
@@ -0,0 +1,51 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * User-related utility functions
3
+ */
4
+
5
+ const UNIQUE_ID_KEY = 'user_unique_id';
6
+ const USER_ID_KEY = 'user_id';
7
+
8
+ /**
9
+ * Get or create a unique browser identifier
10
+ */
11
+ export function getOrCreateUniqueId(): string {
12
+ let uniqueId = localStorage.getItem(UNIQUE_ID_KEY);
13
+
14
+ if (!uniqueId) {
15
+ // Generate a UUID v4
16
+ uniqueId = crypto.randomUUID();
17
+ localStorage.setItem(UNIQUE_ID_KEY, uniqueId);
18
+ }
19
+
20
+ return uniqueId;
21
+ }
22
+
23
+ /**
24
+ * Get user ID from localStorage
25
+ */
26
+ export function getUserId(): string | null {
27
+ return localStorage.getItem(USER_ID_KEY);
28
+ }
29
+
30
+ /**
31
+ * Set user ID in localStorage
32
+ */
33
+ export function setUserId(userId: string): void {
34
+ localStorage.setItem(USER_ID_KEY, userId);
35
+ }
36
+
37
+ /**
38
+ * Clear user data from localStorage
39
+ */
40
+ export function clearUserData(): void {
41
+ localStorage.removeItem(USER_ID_KEY);
42
+ localStorage.removeItem(UNIQUE_ID_KEY);
43
+ }
44
+
45
+ /**
46
+ * Check if user is registered (has user_id)
47
+ */
48
+ export function isUserRegistered(): boolean {
49
+ return getUserId() !== null;
50
+ }
51
+