Yassine Mhirsi commited on
Commit
ff92aff
·
1 Parent(s): c16bf66

added chatbot

Browse files
package-lock.json CHANGED
@@ -12,6 +12,7 @@
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",
@@ -11453,6 +11454,15 @@
11453
  "yallist": "^3.0.2"
11454
  }
11455
  },
 
 
 
 
 
 
 
 
 
11456
  "node_modules/lz-string": {
11457
  "version": "1.5.0",
11458
  "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.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
+ "lucide-react": "^0.561.0",
16
  "postgres": "^3.4.7",
17
  "react": "^19.1.0",
18
  "react-dom": "^19.1.0",
 
11454
  "yallist": "^3.0.2"
11455
  }
11456
  },
11457
+ "node_modules/lucide-react": {
11458
+ "version": "0.561.0",
11459
+ "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.561.0.tgz",
11460
+ "integrity": "sha512-Y59gMY38tl4/i0qewcqohPdEbieBy7SovpBL9IFebhc2mDd8x4PZSOsiFRkpPcOq6bj1r/mjH/Rk73gSlIJP2A==",
11461
+ "license": "ISC",
11462
+ "peerDependencies": {
11463
+ "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
11464
+ }
11465
+ },
11466
  "node_modules/lz-string": {
11467
  "version": "1.5.0",
11468
  "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz",
package.json CHANGED
@@ -7,6 +7,7 @@
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",
 
7
  "@testing-library/jest-dom": "^6.6.3",
8
  "@testing-library/react": "^16.3.0",
9
  "@testing-library/user-event": "^13.5.0",
10
+ "lucide-react": "^0.561.0",
11
  "postgres": "^3.4.7",
12
  "react": "^19.1.0",
13
  "react-dom": "^19.1.0",
src/app/App.tsx CHANGED
@@ -3,6 +3,7 @@ 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 = () => {
@@ -24,6 +25,7 @@ const App: React.FC = () => {
24
  }
25
  />
26
  <Route path="/analysis" element={<AnalysisPage />} />
 
27
  <Route path="*" element={<Navigate to="/" replace />} />
28
  </Routes>
29
  </MainLayout>
 
3
  import MainLayout from './layouts/MainLayout.tsx';
4
  import HomePage from './pages/HomePage.tsx';
5
  import AnalysisPage from './pages/AnalysisPage.tsx';
6
+ import ChatPage from './pages/ChatPage.tsx';
7
  import { isUserRegistered } from './utils/index.ts';
8
 
9
  const App: React.FC = () => {
 
25
  }
26
  />
27
  <Route path="/analysis" element={<AnalysisPage />} />
28
+ <Route path="/chat" element={<ChatPage />} />
29
  <Route path="*" element={<Navigate to="/" replace />} />
30
  </Routes>
31
  </MainLayout>
src/app/components/chat/ChatInput.tsx ADDED
@@ -0,0 +1,170 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState, useEffect } from 'react';
2
+ import { Plus, ArrowUp, Settings2, Mic, X, Check } from 'lucide-react';
3
+
4
+ type ChatInputProps = {
5
+ onSubmit?: (message: string) => void;
6
+ placeholder?: string;
7
+ };
8
+
9
+ const ChatInput: React.FC<ChatInputProps> = ({
10
+ onSubmit,
11
+ placeholder = 'Ask a follow-up...',
12
+ }) => {
13
+ const [input, setInput] = useState('');
14
+ const [isRecording, setIsRecording] = useState(false);
15
+
16
+ const handleSubmit = (e: React.FormEvent) => {
17
+ e.preventDefault();
18
+ if (input.trim()) {
19
+ if (onSubmit) {
20
+ onSubmit(input);
21
+ }
22
+ console.log('Submitted:', input);
23
+ setInput('');
24
+ }
25
+ };
26
+
27
+ const handleMicClick = () => {
28
+ setIsRecording(true);
29
+ setTimeout(() => {
30
+ setIsRecording(false);
31
+ setInput('When speech to text feature ?');
32
+ }, 5000);
33
+ };
34
+
35
+ const handleCancelRecording = () => {
36
+ setIsRecording(false);
37
+ };
38
+
39
+ const handleConfirmRecording = () => {
40
+ setIsRecording(false);
41
+ setInput('When speech to text feature ?');
42
+ };
43
+
44
+ const WaveAnimation: React.FC = () => {
45
+ const [animationKey, setAnimationKey] = useState(0);
46
+
47
+ useEffect(() => {
48
+ const interval = setInterval(() => {
49
+ setAnimationKey((prev) => prev + 1);
50
+ }, 100);
51
+ return () => clearInterval(interval);
52
+ }, []);
53
+
54
+ const bars = Array.from({ length: 50 }, (_, i) => {
55
+ const height = Math.random() * 20 + 4;
56
+ const delay = Math.random() * 2;
57
+ return (
58
+ <div
59
+ key={`${i}-${animationKey}`}
60
+ className="bg-zinc-400 dark:bg-gray-400 rounded-sm animate-pulse"
61
+ style={{
62
+ width: '2px',
63
+ height: `${height}px`,
64
+ animationDelay: `${delay}s`,
65
+ animationDuration: '1s',
66
+ }}
67
+ />
68
+ );
69
+ });
70
+
71
+ return (
72
+ <div className="flex items-center w-full gap-1">
73
+ <div className="flex-1 border-t-2 border-dotted border-zinc-400 dark:border-gray-500"></div>
74
+ <div className="flex items-center gap-0.5 justify-center px-8">{bars}</div>
75
+ <div className="flex-1 border-t-2 border-dotted border-zinc-400 dark:border-gray-500"></div>
76
+ </div>
77
+ );
78
+ };
79
+
80
+ return (
81
+ <div className="relative">
82
+ <form onSubmit={handleSubmit} className="relative">
83
+ <div
84
+ className="border border-zinc-300 dark:border-zinc-700 rounded-2xl p-4 relative transition-all duration-500 ease-in-out overflow-hidden bg-zinc-100 dark:bg-[#141415]"
85
+ >
86
+ {isRecording ? (
87
+ <div className="flex items-center justify-between h-12 animate-fade-in w-full">
88
+ <WaveAnimation />
89
+ <div className="flex items-center gap-2 ml-4">
90
+ <button
91
+ type="button"
92
+ onClick={handleCancelRecording}
93
+ className="h-8 w-8 p-0 text-zinc-800 dark:text-white hover:text-zinc-900 dark:hover:text-white hover:bg-zinc-200 dark:hover:bg-zinc-700 rounded-lg transition-all duration-200 hover:scale-110 flex items-center justify-center"
94
+ >
95
+ <X className="h-5 w-5" />
96
+ </button>
97
+ <button
98
+ type="button"
99
+ onClick={handleConfirmRecording}
100
+ className="h-8 w-8 p-0 rounded-lg transition-all duration-200 hover:scale-110 flex items-center justify-center bg-teal-400 dark:bg-[#2DD4BF] text-teal-900 dark:text-[#032827]"
101
+ >
102
+ <Check className="h-5 w-5" />
103
+ </button>
104
+ </div>
105
+ </div>
106
+ ) : (
107
+ <div className="animate-fade-in">
108
+ <textarea
109
+ value={input}
110
+ onChange={(e) => setInput(e.target.value)}
111
+ placeholder={placeholder}
112
+ className="w-full bg-transparent text-zinc-800 dark:text-gray-300 placeholder-zinc-400 dark:placeholder-gray-500 resize-none border-none outline-none text-base leading-relaxed min-h-[24px] max-h-32 transition-all duration-200"
113
+ rows={1}
114
+ onInput={(e) => {
115
+ const target = e.target as HTMLTextAreaElement;
116
+ target.style.height = 'auto';
117
+ target.style.height = target.scrollHeight + 'px';
118
+ }}
119
+ />
120
+
121
+ <div className="flex items-center justify-between mt-8">
122
+ <div className="flex items-center gap-2">
123
+ <button
124
+ type="button"
125
+ className="h-8 w-8 p-0 text-zinc-800 dark:text-white hover:text-zinc-900 dark:hover:text-white hover:bg-zinc-200 dark:hover:bg-zinc-700 rounded-lg transition-all duration-200 hover:scale-110 flex items-center justify-center"
126
+ >
127
+ <Plus className="h-5 w-5" />
128
+ </button>
129
+
130
+ <button
131
+ type="button"
132
+ className="h-8 w-8 p-0 text-zinc-800 dark:text-white hover:text-zinc-900 dark:hover:text-white hover:bg-zinc-200 dark:hover:bg-zinc-700 rounded-lg transition-all duration-200 hover:scale-110 flex items-center justify-center"
133
+ >
134
+ <Settings2 className="h-5 w-5" />
135
+ </button>
136
+
137
+ <button
138
+ type="button"
139
+ onClick={handleMicClick}
140
+ className="h-8 w-8 p-0 text-zinc-800 dark:text-white hover:text-zinc-900 dark:hover:text-white hover:bg-zinc-200 dark:hover:bg-zinc-700 rounded-lg transition-all duration-200 hover:scale-110 active:scale-95 active:bg-red-600/20 active:text-red-400 flex items-center justify-center"
141
+ >
142
+ <Mic className="h-5 w-5 transition-transform duration-200" />
143
+ </button>
144
+
145
+ <button
146
+ type="button"
147
+ className="h-8 px-3 rounded-lg text-sm font-medium hover:opacity-90 transition-all duration-200 hover:scale-105 flex items-center justify-center bg-teal-900 dark:bg-[#032827] text-teal-300 dark:text-[#2DD4BF]"
148
+ >
149
+ Agent
150
+ </button>
151
+ </div>
152
+
153
+ <button
154
+ type="submit"
155
+ disabled={!input.trim()}
156
+ className="h-8 w-8 p-0 bg-zinc-300 dark:bg-zinc-700 hover:bg-zinc-400 dark:hover:bg-zinc-600 disabled:bg-zinc-200 dark:disabled:bg-zinc-800 disabled:text-zinc-400 dark:disabled:text-zinc-500 text-zinc-800 dark:text-white rounded-lg transition-all duration-200 hover:scale-110 disabled:hover:scale-100 flex items-center justify-center disabled:cursor-not-allowed"
157
+ >
158
+ <ArrowUp className="h-5 w-5" />
159
+ </button>
160
+ </div>
161
+ </div>
162
+ )}
163
+ </div>
164
+ </form>
165
+ </div>
166
+ );
167
+ };
168
+
169
+ export default ChatInput;
170
+
src/app/components/common/ThemeToggle.tsx ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react';
2
+ import { Moon, Sun } from 'lucide-react';
3
+ import { useTheme } from '../../hooks/useTheme.ts';
4
+
5
+ const ThemeToggle: React.FC = () => {
6
+ const { theme, toggleTheme } = useTheme();
7
+
8
+ return (
9
+ <button
10
+ onClick={toggleTheme}
11
+ className="h-10 w-10 p-0 rounded-lg transition-all duration-200 hover:scale-110 flex items-center justify-center bg-zinc-200 dark:bg-zinc-700 text-zinc-800 dark:text-white hover:bg-zinc-300 dark:hover:bg-zinc-600"
12
+ aria-label="Toggle theme"
13
+ >
14
+ {theme === 'dark' ? (
15
+ <Sun className="h-5 w-5" />
16
+ ) : (
17
+ <Moon className="h-5 w-5" />
18
+ )}
19
+ </button>
20
+ );
21
+ };
22
+
23
+ export default ThemeToggle;
24
+
src/app/components/navigation/Navigation.tsx ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react';
2
+ import { NavLink } from 'react-router-dom';
3
+
4
+ const Navigation: React.FC = () => {
5
+ return (
6
+ <nav className="fixed top-4 left-1/2 transform -translate-x-1/2 z-40">
7
+ <div className="flex items-center gap-2">
8
+ <NavLink
9
+ to="/analysis"
10
+ className={({ isActive }) =>
11
+ `px-4 py-2 text-sm font-medium rounded-lg border transition-all duration-200 ${
12
+ isActive
13
+ ? 'text-zinc-900 dark:text-white border-zinc-300 dark:border-zinc-600 bg-zinc-50 dark:bg-zinc-900'
14
+ : 'text-zinc-500 dark:text-zinc-500 border-zinc-200 dark:border-zinc-700 hover:text-zinc-700 dark:hover:text-zinc-300 hover:border-zinc-300 dark:hover:border-zinc-600'
15
+ }`
16
+ }
17
+ >
18
+ Analysis
19
+ </NavLink>
20
+ <NavLink
21
+ to="/chat"
22
+ className={({ isActive }) =>
23
+ `px-4 py-2 text-sm font-medium rounded-lg border transition-all duration-200 ${
24
+ isActive
25
+ ? 'text-zinc-900 dark:text-white border-zinc-300 dark:border-zinc-600 bg-zinc-50 dark:bg-zinc-900'
26
+ : 'text-zinc-500 dark:text-zinc-500 border-zinc-200 dark:border-zinc-700 hover:text-zinc-700 dark:hover:text-zinc-300 hover:border-zinc-300 dark:hover:border-zinc-600'
27
+ }`
28
+ }
29
+ >
30
+ Chatbot
31
+ </NavLink>
32
+ </div>
33
+ </nav>
34
+ );
35
+ };
36
+
37
+ export default Navigation;
38
+
src/app/hooks/index.ts CHANGED
@@ -2,6 +2,7 @@
2
  * Central export point for all custom hooks
3
  */
4
 
5
- export { default as useApi } from './useApi';
6
- export { useApi as useApiHook } from './useApi';
 
7
 
 
2
  * Central export point for all custom hooks
3
  */
4
 
5
+ export { default as useApi } from './useApi.ts';
6
+ export { useApi as useApiHook } from './useApi.ts';
7
+ export { useTheme } from './useTheme.ts';
8
 
src/app/hooks/useTheme.ts ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState, useEffect } from 'react';
2
+
3
+ type Theme = 'light' | 'dark';
4
+
5
+ const THEME_STORAGE_KEY = 'app-theme';
6
+
7
+ export const useTheme = () => {
8
+ const [theme, setTheme] = useState<Theme>(() => {
9
+ // Get theme from localStorage or default to 'dark'
10
+ if (typeof window !== 'undefined') {
11
+ const stored = localStorage.getItem(THEME_STORAGE_KEY) as Theme | null;
12
+ return stored || 'dark';
13
+ }
14
+ return 'dark';
15
+ });
16
+
17
+ useEffect(() => {
18
+ // Apply theme to document
19
+ const root = document.documentElement;
20
+ if (theme === 'dark') {
21
+ root.classList.add('dark');
22
+ } else {
23
+ root.classList.remove('dark');
24
+ }
25
+ // Save to localStorage
26
+ localStorage.setItem(THEME_STORAGE_KEY, theme);
27
+ }, [theme]);
28
+
29
+ const toggleTheme = () => {
30
+ setTheme((prev) => (prev === 'dark' ? 'light' : 'dark'));
31
+ };
32
+
33
+ return { theme, toggleTheme, setTheme };
34
+ };
35
+
src/app/layouts/MainLayout.tsx CHANGED
@@ -1,14 +1,35 @@
1
- import React from 'react';
 
 
 
2
 
3
  type MainLayoutProps = {
4
  children: React.ReactNode;
5
  };
6
 
7
- const MainLayout: React.FC<MainLayoutProps> = ({ children }) => (
8
- <div className="min-h-screen">
9
- {children}
10
- </div>
11
- );
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
12
 
13
  export default MainLayout;
14
 
 
1
+ import React, { useEffect } from 'react';
2
+ import { useTheme } from '../hooks/useTheme.ts';
3
+ import ThemeToggle from '../components/common/ThemeToggle.tsx';
4
+ import Navigation from '../components/navigation/Navigation.tsx';
5
 
6
  type MainLayoutProps = {
7
  children: React.ReactNode;
8
  };
9
 
10
+ const MainLayout: React.FC<MainLayoutProps> = ({ children }) => {
11
+ const { theme } = useTheme();
12
+
13
+ useEffect(() => {
14
+ // Ensure theme is applied on mount
15
+ const root = document.documentElement;
16
+ if (theme === 'dark') {
17
+ root.classList.add('dark');
18
+ } else {
19
+ root.classList.remove('dark');
20
+ }
21
+ }, [theme]);
22
+
23
+ return (
24
+ <div className="min-h-screen bg-white dark:bg-black transition-colors duration-200 relative">
25
+ <Navigation />
26
+ <div className="fixed top-4 right-4 z-50">
27
+ <ThemeToggle />
28
+ </div>
29
+ {children}
30
+ </div>
31
+ );
32
+ };
33
 
34
  export default MainLayout;
35
 
src/app/pages/AnalysisPage.tsx CHANGED
@@ -139,16 +139,16 @@ const AnalysisPage: React.FC = () => {
139
  };
140
 
141
  return (
142
- <div className="min-h-screen bg-slate-50 px-4 py-10">
143
  <div className="mx-auto max-w-7xl space-y-6">
144
  {/* Statistics Section */}
145
  {!isLoadingStats && userAnalysisData.length > 0 && (
146
- <div className="rounded-lg border border-slate-200 bg-white shadow-sm">
147
- <div className="border-b border-slate-200 px-6 py-4">
148
- <h2 className="text-lg font-semibold text-slate-800">
149
  Your Analysis Statistics
150
  </h2>
151
- <p className="text-sm text-slate-500">
152
  Overview of all your analyzed arguments
153
  </p>
154
  </div>
@@ -159,54 +159,54 @@ const AnalysisPage: React.FC = () => {
159
  </div>
160
  ) : statsError ? (
161
  <div className="px-6 py-4">
162
- <p className="text-sm text-red-600">{statsError}</p>
163
  </div>
164
  ) : (
165
  <div className="p-6 space-y-8">
166
  {/* Summary Statistics */}
167
  <div className="grid grid-cols-2 md:grid-cols-4 gap-4">
168
- <div className="bg-slate-50 rounded-lg p-4">
169
- <div className="text-2xl font-bold text-slate-800">
170
  {stanceStats.total}
171
  </div>
172
- <div className="text-sm text-slate-600">Total Arguments</div>
173
  </div>
174
- <div className="bg-slate-50 rounded-lg p-4">
175
- <div className="text-2xl font-bold text-slate-800">
176
  {uniqueTopicsCount}
177
  </div>
178
- <div className="text-sm text-slate-600">Unique Topics</div>
179
  </div>
180
- <div className="bg-slate-50 rounded-lg p-4">
181
- <div className="text-2xl font-bold text-slate-800">
182
  {avgArgumentLength}
183
  </div>
184
- <div className="text-sm text-slate-600">Avg Length (chars)</div>
185
  </div>
186
- <div className="bg-slate-50 rounded-lg p-4">
187
- <div className="text-2xl font-bold text-slate-800">
188
  {stanceStats.pro}
189
  </div>
190
- <div className="text-sm text-slate-600">PRO Arguments</div>
191
  </div>
192
  </div>
193
 
194
  {/* Charts Grid */}
195
  <div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
196
  {/* Stance Distribution */}
197
- <div className="rounded-lg border border-slate-200 p-4">
198
- <h3 className="text-sm font-semibold text-slate-700 mb-4">
199
  Stance Distribution
200
  </h3>
201
  <StanceDistributionChart stats={stanceStats} />
202
  <div className="mt-4 flex justify-center gap-6 text-sm">
203
  <div>
204
- <span className="font-semibold text-green-700">
205
  PRO: {stanceStats.pro} ({stanceStats.proPercentage.toFixed(1)}%)
206
  </span>
207
  </div>
208
  <div>
209
- <span className="font-semibold text-red-700">
210
  CON: {stanceStats.con} ({stanceStats.conPercentage.toFixed(1)}%)
211
  </span>
212
  </div>
@@ -215,13 +215,13 @@ const AnalysisPage: React.FC = () => {
215
 
216
  {/* Time Series */}
217
  {timeStats.length > 0 && (
218
- <div className="rounded-lg border border-slate-200 p-4">
219
- <h3 className="text-sm font-semibold text-slate-700 mb-4">
220
  Analysis Timeline
221
  </h3>
222
  <TimeSeriesChart data={timeStats} />
223
  {oldestDate && mostRecentDate && (
224
- <div className="mt-4 text-center text-xs text-slate-500">
225
  From {oldestDate} to {mostRecentDate}
226
  </div>
227
  )}
@@ -231,8 +231,8 @@ const AnalysisPage: React.FC = () => {
231
 
232
  {/* Topic Frequency */}
233
  {topicFrequency.length > 0 && (
234
- <div className="rounded-lg border border-slate-200 p-4">
235
- <h3 className="text-sm font-semibold text-slate-700 mb-4">
236
  Most Discussed Topics (Top 10)
237
  </h3>
238
  <TopicFrequencyChart data={topicFrequency} />
@@ -244,25 +244,25 @@ const AnalysisPage: React.FC = () => {
244
  )}
245
 
246
  {/* CSV Upload and Analysis Section */}
247
- <div className="rounded-lg border border-slate-200 bg-white shadow-sm">
248
- <div className="border-b border-slate-200 px-6 py-4">
249
- <h1 className="text-lg font-semibold text-slate-800">
250
  Analyze New Arguments
251
  </h1>
252
- <p className="text-sm text-slate-500">
253
  Upload a CSV file containing arguments to analyze them.
254
  </p>
255
  </div>
256
 
257
  <div className="space-y-6 p-6">
258
  <div className="flex flex-col gap-3">
259
- <label className="text-sm font-medium text-slate-700">
260
  Upload a CSV file
261
  </label>
262
 
263
  <div className="flex flex-wrap items-center gap-3">
264
- <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">
265
- <span className="truncate text-sm font-semibold text-slate-500">
266
  {fileName}
267
  </span>
268
  <input
@@ -277,43 +277,43 @@ const AnalysisPage: React.FC = () => {
277
  type="button"
278
  onClick={handleAnalyze}
279
  disabled={!selectedFile || rows.length === 0 || isAnalyzing}
280
- 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"
281
  >
282
  {isAnalyzing ? 'Analyzing...' : 'Analyze'}
283
  </button>
284
  </div>
285
 
286
- {error && <p className="text-sm text-red-600">{error}</p>}
287
  </div>
288
 
289
  {rows.length > 0 && (
290
- <div className="overflow-hidden rounded-lg border border-slate-200">
291
- <div className="bg-slate-50 px-4 py-2 border-b border-slate-200">
292
- <h2 className="text-sm font-semibold text-slate-700">
293
  CSV Preview ({rows.length} rows)
294
  </h2>
295
  </div>
296
- <table className="min-w-full divide-y divide-slate-200">
297
- <thead className="bg-slate-50">
298
  <tr>
299
  {headers.map((header) => (
300
  <th
301
  key={header}
302
  scope="col"
303
- className="px-4 py-3 text-left text-sm font-semibold uppercase tracking-wide text-slate-600"
304
  >
305
  {header}
306
  </th>
307
  ))}
308
  </tr>
309
  </thead>
310
- <tbody className="divide-y divide-slate-200 bg-white">
311
  {rows.map((row, index) => (
312
- <tr key={row.id ?? `row-${index}`} className="hover:bg-slate-50">
313
  {headers.map((header) => (
314
  <td
315
  key={`${header}-${index}`}
316
- className="whitespace-pre-wrap px-4 py-3 text-sm text-slate-700"
317
  >
318
  {row[header] ?? ''}
319
  </td>
@@ -326,63 +326,63 @@ const AnalysisPage: React.FC = () => {
326
  )}
327
 
328
  {analysisResults.length > 0 && (
329
- <div className="overflow-hidden rounded-lg border border-slate-200">
330
- <div className="bg-green-50 px-4 py-2 border-b border-slate-200">
331
- <h2 className="text-sm font-semibold text-green-800">
332
  Analysis Results ({analysisResults.length} results)
333
  </h2>
334
  </div>
335
  <div className="overflow-x-auto">
336
- <table className="min-w-full divide-y divide-slate-200">
337
- <thead className="bg-slate-50">
338
  <tr>
339
- <th className="px-4 py-3 text-left text-sm font-semibold uppercase tracking-wide text-slate-600">
340
  Argument
341
  </th>
342
- <th className="px-4 py-3 text-left text-sm font-semibold uppercase tracking-wide text-slate-600">
343
  Topic
344
  </th>
345
- <th className="px-4 py-3 text-left text-sm font-semibold uppercase tracking-wide text-slate-600">
346
  Stance
347
  </th>
348
- <th className="px-4 py-3 text-left text-sm font-semibold uppercase tracking-wide text-slate-600">
349
  Confidence
350
  </th>
351
- <th className="px-4 py-3 text-left text-sm font-semibold uppercase tracking-wide text-slate-600">
352
  PRO
353
  </th>
354
- <th className="px-4 py-3 text-left text-sm font-semibold uppercase tracking-wide text-slate-600">
355
  CON
356
  </th>
357
  </tr>
358
  </thead>
359
- <tbody className="divide-y divide-slate-200 bg-white">
360
  {analysisResults.map((result) => (
361
- <tr key={result.id} className="hover:bg-slate-50">
362
- <td className="whitespace-pre-wrap px-4 py-3 text-sm text-slate-700 max-w-md">
363
  {result.argument}
364
  </td>
365
- <td className="px-4 py-3 text-sm text-slate-700">
366
  {result.topic}
367
  </td>
368
  <td className="px-4 py-3 text-sm">
369
  <span
370
  className={`inline-flex rounded-full px-2 py-1 text-xs font-semibold ${
371
  result.predicted_stance === 'PRO'
372
- ? 'bg-green-100 text-green-800'
373
- : 'bg-red-100 text-red-800'
374
  }`}
375
  >
376
  {result.predicted_stance}
377
  </span>
378
  </td>
379
- <td className="px-4 py-3 text-sm text-slate-700">
380
  {(result.confidence * 100).toFixed(1)}%
381
  </td>
382
- <td className="px-4 py-3 text-sm text-slate-700">
383
  {(result.probability_pro * 100).toFixed(1)}%
384
  </td>
385
- <td className="px-4 py-3 text-sm text-slate-700">
386
  {(result.probability_con * 100).toFixed(1)}%
387
  </td>
388
  </tr>
 
139
  };
140
 
141
  return (
142
+ <div className="min-h-screen bg-slate-50 dark:bg-black px-4 pt-20 pb-10 transition-colors duration-200">
143
  <div className="mx-auto max-w-7xl space-y-6">
144
  {/* Statistics Section */}
145
  {!isLoadingStats && userAnalysisData.length > 0 && (
146
+ <div className="rounded-lg border border-slate-200 dark:border-zinc-700 bg-white dark:bg-zinc-900 shadow-sm">
147
+ <div className="border-b border-slate-200 dark:border-zinc-700 px-6 py-4">
148
+ <h2 className="text-lg font-semibold text-slate-800 dark:text-white">
149
  Your Analysis Statistics
150
  </h2>
151
+ <p className="text-sm text-slate-500 dark:text-zinc-400">
152
  Overview of all your analyzed arguments
153
  </p>
154
  </div>
 
159
  </div>
160
  ) : statsError ? (
161
  <div className="px-6 py-4">
162
+ <p className="text-sm text-red-600 dark:text-red-400">{statsError}</p>
163
  </div>
164
  ) : (
165
  <div className="p-6 space-y-8">
166
  {/* Summary Statistics */}
167
  <div className="grid grid-cols-2 md:grid-cols-4 gap-4">
168
+ <div className="bg-slate-50 dark:bg-zinc-800 rounded-lg p-4">
169
+ <div className="text-2xl font-bold text-slate-800 dark:text-white">
170
  {stanceStats.total}
171
  </div>
172
+ <div className="text-sm text-slate-600 dark:text-zinc-400">Total Arguments</div>
173
  </div>
174
+ <div className="bg-slate-50 dark:bg-zinc-800 rounded-lg p-4">
175
+ <div className="text-2xl font-bold text-slate-800 dark:text-white">
176
  {uniqueTopicsCount}
177
  </div>
178
+ <div className="text-sm text-slate-600 dark:text-zinc-400">Unique Topics</div>
179
  </div>
180
+ <div className="bg-slate-50 dark:bg-zinc-800 rounded-lg p-4">
181
+ <div className="text-2xl font-bold text-slate-800 dark:text-white">
182
  {avgArgumentLength}
183
  </div>
184
+ <div className="text-sm text-slate-600 dark:text-zinc-400">Avg Length (chars)</div>
185
  </div>
186
+ <div className="bg-slate-50 dark:bg-zinc-800 rounded-lg p-4">
187
+ <div className="text-2xl font-bold text-slate-800 dark:text-white">
188
  {stanceStats.pro}
189
  </div>
190
+ <div className="text-sm text-slate-600 dark:text-zinc-400">PRO Arguments</div>
191
  </div>
192
  </div>
193
 
194
  {/* Charts Grid */}
195
  <div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
196
  {/* Stance Distribution */}
197
+ <div className="rounded-lg border border-slate-200 dark:border-zinc-700 p-4">
198
+ <h3 className="text-sm font-semibold text-slate-700 dark:text-zinc-300 mb-4">
199
  Stance Distribution
200
  </h3>
201
  <StanceDistributionChart stats={stanceStats} />
202
  <div className="mt-4 flex justify-center gap-6 text-sm">
203
  <div>
204
+ <span className="font-semibold text-green-700 dark:text-green-400">
205
  PRO: {stanceStats.pro} ({stanceStats.proPercentage.toFixed(1)}%)
206
  </span>
207
  </div>
208
  <div>
209
+ <span className="font-semibold text-red-700 dark:text-red-400">
210
  CON: {stanceStats.con} ({stanceStats.conPercentage.toFixed(1)}%)
211
  </span>
212
  </div>
 
215
 
216
  {/* Time Series */}
217
  {timeStats.length > 0 && (
218
+ <div className="rounded-lg border border-slate-200 dark:border-zinc-700 p-4">
219
+ <h3 className="text-sm font-semibold text-slate-700 dark:text-zinc-300 mb-4">
220
  Analysis Timeline
221
  </h3>
222
  <TimeSeriesChart data={timeStats} />
223
  {oldestDate && mostRecentDate && (
224
+ <div className="mt-4 text-center text-xs text-slate-500 dark:text-zinc-400">
225
  From {oldestDate} to {mostRecentDate}
226
  </div>
227
  )}
 
231
 
232
  {/* Topic Frequency */}
233
  {topicFrequency.length > 0 && (
234
+ <div className="rounded-lg border border-slate-200 dark:border-zinc-700 p-4">
235
+ <h3 className="text-sm font-semibold text-slate-700 dark:text-zinc-300 mb-4">
236
  Most Discussed Topics (Top 10)
237
  </h3>
238
  <TopicFrequencyChart data={topicFrequency} />
 
244
  )}
245
 
246
  {/* CSV Upload and Analysis Section */}
247
+ <div className="rounded-lg border border-slate-200 dark:border-zinc-700 bg-white dark:bg-zinc-900 shadow-sm">
248
+ <div className="border-b border-slate-200 dark:border-zinc-700 px-6 py-4">
249
+ <h1 className="text-lg font-semibold text-slate-800 dark:text-white">
250
  Analyze New Arguments
251
  </h1>
252
+ <p className="text-sm text-slate-500 dark:text-zinc-400">
253
  Upload a CSV file containing arguments to analyze them.
254
  </p>
255
  </div>
256
 
257
  <div className="space-y-6 p-6">
258
  <div className="flex flex-col gap-3">
259
+ <label className="text-sm font-medium text-slate-700 dark:text-zinc-300">
260
  Upload a CSV file
261
  </label>
262
 
263
  <div className="flex flex-wrap items-center gap-3">
264
+ <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">
265
+ <span className="truncate text-sm font-semibold text-slate-500 dark:text-zinc-400">
266
  {fileName}
267
  </span>
268
  <input
 
277
  type="button"
278
  onClick={handleAnalyze}
279
  disabled={!selectedFile || rows.length === 0 || isAnalyzing}
280
+ 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"
281
  >
282
  {isAnalyzing ? 'Analyzing...' : 'Analyze'}
283
  </button>
284
  </div>
285
 
286
+ {error && <p className="text-sm text-red-600 dark:text-red-400">{error}</p>}
287
  </div>
288
 
289
  {rows.length > 0 && (
290
+ <div className="overflow-hidden rounded-lg border border-slate-200 dark:border-zinc-700">
291
+ <div className="bg-slate-50 dark:bg-zinc-800 px-4 py-2 border-b border-slate-200 dark:border-zinc-700">
292
+ <h2 className="text-sm font-semibold text-slate-700 dark:text-zinc-300">
293
  CSV Preview ({rows.length} rows)
294
  </h2>
295
  </div>
296
+ <table className="min-w-full divide-y divide-slate-200 dark:divide-zinc-700">
297
+ <thead className="bg-slate-50 dark:bg-zinc-800">
298
  <tr>
299
  {headers.map((header) => (
300
  <th
301
  key={header}
302
  scope="col"
303
+ className="px-4 py-3 text-left text-sm font-semibold uppercase tracking-wide text-slate-600 dark:text-zinc-300"
304
  >
305
  {header}
306
  </th>
307
  ))}
308
  </tr>
309
  </thead>
310
+ <tbody className="divide-y divide-slate-200 dark:divide-zinc-700 bg-white dark:bg-zinc-900">
311
  {rows.map((row, index) => (
312
+ <tr key={row.id ?? `row-${index}`} className="hover:bg-slate-50 dark:hover:bg-zinc-800">
313
  {headers.map((header) => (
314
  <td
315
  key={`${header}-${index}`}
316
+ className="whitespace-pre-wrap px-4 py-3 text-sm text-slate-700 dark:text-zinc-300"
317
  >
318
  {row[header] ?? ''}
319
  </td>
 
326
  )}
327
 
328
  {analysisResults.length > 0 && (
329
+ <div className="overflow-hidden rounded-lg border border-slate-200 dark:border-zinc-700">
330
+ <div className="bg-green-50 dark:bg-green-900/20 px-4 py-2 border-b border-slate-200 dark:border-zinc-700">
331
+ <h2 className="text-sm font-semibold text-green-800 dark:text-green-400">
332
  Analysis Results ({analysisResults.length} results)
333
  </h2>
334
  </div>
335
  <div className="overflow-x-auto">
336
+ <table className="min-w-full divide-y divide-slate-200 dark:divide-zinc-700">
337
+ <thead className="bg-slate-50 dark:bg-zinc-800">
338
  <tr>
339
+ <th className="px-4 py-3 text-left text-sm font-semibold uppercase tracking-wide text-slate-600 dark:text-zinc-300">
340
  Argument
341
  </th>
342
+ <th className="px-4 py-3 text-left text-sm font-semibold uppercase tracking-wide text-slate-600 dark:text-zinc-300">
343
  Topic
344
  </th>
345
+ <th className="px-4 py-3 text-left text-sm font-semibold uppercase tracking-wide text-slate-600 dark:text-zinc-300">
346
  Stance
347
  </th>
348
+ <th className="px-4 py-3 text-left text-sm font-semibold uppercase tracking-wide text-slate-600 dark:text-zinc-300">
349
  Confidence
350
  </th>
351
+ <th className="px-4 py-3 text-left text-sm font-semibold uppercase tracking-wide text-slate-600 dark:text-zinc-300">
352
  PRO
353
  </th>
354
+ <th className="px-4 py-3 text-left text-sm font-semibold uppercase tracking-wide text-slate-600 dark:text-zinc-300">
355
  CON
356
  </th>
357
  </tr>
358
  </thead>
359
+ <tbody className="divide-y divide-slate-200 dark:divide-zinc-700 bg-white dark:bg-zinc-900">
360
  {analysisResults.map((result) => (
361
+ <tr key={result.id} className="hover:bg-slate-50 dark:hover:bg-zinc-800">
362
+ <td className="whitespace-pre-wrap px-4 py-3 text-sm text-slate-700 dark:text-zinc-300 max-w-md">
363
  {result.argument}
364
  </td>
365
+ <td className="px-4 py-3 text-sm text-slate-700 dark:text-zinc-300">
366
  {result.topic}
367
  </td>
368
  <td className="px-4 py-3 text-sm">
369
  <span
370
  className={`inline-flex rounded-full px-2 py-1 text-xs font-semibold ${
371
  result.predicted_stance === 'PRO'
372
+ ? 'bg-green-100 dark:bg-green-900/30 text-green-800 dark:text-green-400'
373
+ : 'bg-red-100 dark:bg-red-900/30 text-red-800 dark:text-red-400'
374
  }`}
375
  >
376
  {result.predicted_stance}
377
  </span>
378
  </td>
379
+ <td className="px-4 py-3 text-sm text-slate-700 dark:text-zinc-300">
380
  {(result.confidence * 100).toFixed(1)}%
381
  </td>
382
+ <td className="px-4 py-3 text-sm text-slate-700 dark:text-zinc-300">
383
  {(result.probability_pro * 100).toFixed(1)}%
384
  </td>
385
+ <td className="px-4 py-3 text-sm text-slate-700 dark:text-zinc-300">
386
  {(result.probability_con * 100).toFixed(1)}%
387
  </td>
388
  </tr>
src/app/pages/ChatPage.tsx ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react';
2
+ import ChatInput from '../components/chat/ChatInput.tsx';
3
+
4
+ const ChatPage: React.FC = () => {
5
+ const handleMessageSubmit = (message: string) => {
6
+ console.log('Message submitted:', message);
7
+ // TODO: Implement chat message handling
8
+ };
9
+
10
+ return (
11
+ <div className="flex min-h-screen items-center justify-center bg-white dark:bg-black px-4 pt-20 pb-10 transition-colors duration-200">
12
+ <div className="w-full max-w-4xl">
13
+ <ChatInput onSubmit={handleMessageSubmit} />
14
+ </div>
15
+ </div>
16
+ );
17
+ };
18
+
19
+ export default ChatPage;
20
+
src/app/pages/HomePage.tsx CHANGED
@@ -44,20 +44,20 @@ const HomePage: React.FC = () => {
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>
@@ -66,7 +66,7 @@ const HomePage: React.FC = () => {
66
  <div>
67
  <label
68
  htmlFor="name"
69
- className="block text-sm font-medium text-slate-700"
70
  >
71
  Your Name (Optional)
72
  </label>
@@ -77,14 +77,14 @@ const HomePage: React.FC = () => {
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
 
@@ -92,7 +92,7 @@ const HomePage: React.FC = () => {
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>
@@ -100,7 +100,7 @@ const HomePage: React.FC = () => {
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>
 
44
 
45
  if (isLoading) {
46
  return (
47
+ <div className="flex min-h-screen items-center justify-center bg-slate-50 dark:bg-black">
48
  <Loading />
49
  </div>
50
  );
51
  }
52
 
53
  return (
54
+ <div className="flex min-h-screen items-center justify-center bg-slate-50 dark:bg-black px-4 py-10 transition-colors duration-200">
55
+ <div className="w-full max-w-md rounded-lg border border-slate-200 dark:border-zinc-700 bg-white dark:bg-zinc-900 shadow-sm">
56
+ <div className="border-b border-slate-200 dark:border-zinc-700 px-6 py-6">
57
+ <h1 className="text-2xl font-bold text-slate-800 dark:text-white">
58
  Welcome to NLP IBM Debater
59
  </h1>
60
+ <p className="mt-2 text-sm text-slate-500 dark:text-zinc-400">
61
  Enter your name to get started, or skip to use a random name.
62
  </p>
63
  </div>
 
66
  <div>
67
  <label
68
  htmlFor="name"
69
+ className="block text-sm font-medium text-slate-700 dark:text-zinc-300"
70
  >
71
  Your Name (Optional)
72
  </label>
 
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 dark:border-zinc-600 bg-white dark:bg-zinc-800 px-4 py-2 text-sm text-slate-700 dark:text-white placeholder-slate-400 dark:placeholder-zinc-500 focus:border-blue-500 dark:focus:border-blue-400 focus:outline-none focus:ring-2 focus:ring-blue-500 dark:focus:ring-blue-400 focus:ring-opacity-20"
81
  disabled={isLoading}
82
  />
83
  </div>
84
 
85
  {error && (
86
+ <div className="rounded-md bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 p-3">
87
+ <p className="text-sm text-red-600 dark:text-red-400">{error}</p>
88
  </div>
89
  )}
90
 
 
92
  <button
93
  type="submit"
94
  disabled={isLoading}
95
+ className="flex-1 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 focus:outline-none focus:ring-2 focus:ring-blue-500 dark:focus:ring-blue-400 focus:ring-offset-2 dark:focus:ring-offset-zinc-900 disabled:cursor-not-allowed disabled:opacity-60"
96
  >
97
  Continue
98
  </button>
 
100
  type="button"
101
  onClick={handleSkip}
102
  disabled={isLoading}
103
+ className="flex-1 rounded-md border border-slate-300 dark:border-zinc-600 bg-white dark:bg-zinc-800 px-4 py-2 text-sm font-semibold text-slate-700 dark:text-zinc-200 transition hover:bg-slate-50 dark:hover:bg-zinc-700 focus:outline-none focus:ring-2 focus:ring-slate-500 dark:focus:ring-zinc-500 focus:ring-offset-2 dark:focus:ring-offset-zinc-900 disabled:cursor-not-allowed disabled:opacity-60"
104
  >
105
  Skip (Random Name)
106
  </button>
src/index.js CHANGED
@@ -5,6 +5,14 @@ import App from './app/App.tsx';
5
  import ErrorBoundary from './app/components/common/ErrorBoundary.tsx';
6
  import reportWebVitals from './reportWebVitals';
7
 
 
 
 
 
 
 
 
 
8
  const root = ReactDOM.createRoot(document.getElementById('root'));
9
  root.render(
10
  <React.StrictMode>
 
5
  import ErrorBoundary from './app/components/common/ErrorBoundary.tsx';
6
  import reportWebVitals from './reportWebVitals';
7
 
8
+ // Initialize theme from localStorage before rendering to prevent flash
9
+ const storedTheme = localStorage.getItem('app-theme') || 'dark';
10
+ if (storedTheme === 'dark') {
11
+ document.documentElement.classList.add('dark');
12
+ } else {
13
+ document.documentElement.classList.remove('dark');
14
+ }
15
+
16
  const root = ReactDOM.createRoot(document.getElementById('root'));
17
  root.render(
18
  <React.StrictMode>
tailwind.config.js CHANGED
@@ -1,8 +1,19 @@
1
  /** @type {import('tailwindcss').Config} */
2
  module.exports = {
3
  content: ['./src/**/*.{js,jsx,ts,tsx}'],
 
4
  theme: {
5
- extend: {},
 
 
 
 
 
 
 
 
 
 
6
  },
7
  plugins: [],
8
  };
 
1
  /** @type {import('tailwindcss').Config} */
2
  module.exports = {
3
  content: ['./src/**/*.{js,jsx,ts,tsx}'],
4
+ darkMode: 'class',
5
  theme: {
6
+ extend: {
7
+ keyframes: {
8
+ 'fade-in': {
9
+ '0%': { opacity: '0' },
10
+ '100%': { opacity: '1' },
11
+ },
12
+ },
13
+ animation: {
14
+ 'fade-in': 'fade-in 0.5s ease-in-out',
15
+ },
16
+ },
17
  },
18
  plugins: [],
19
  };