Yassine Mhirsi
commited on
Commit
·
ff92aff
1
Parent(s):
c16bf66
added chatbot
Browse files- package-lock.json +10 -0
- package.json +1 -0
- src/app/App.tsx +2 -0
- src/app/components/chat/ChatInput.tsx +170 -0
- src/app/components/common/ThemeToggle.tsx +24 -0
- src/app/components/navigation/Navigation.tsx +38 -0
- src/app/hooks/index.ts +3 -2
- src/app/hooks/useTheme.ts +35 -0
- src/app/layouts/MainLayout.tsx +27 -6
- src/app/pages/AnalysisPage.tsx +65 -65
- src/app/pages/ChatPage.tsx +20 -0
- src/app/pages/HomePage.tsx +12 -12
- src/index.js +8 -0
- tailwind.config.js +12 -1
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 |
-
|
| 9 |
-
|
| 10 |
-
|
| 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
|
| 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 |
};
|