Krish-05 commited on
Commit
9f7a5a6
·
unverified ·
1 Parent(s): f392216

added voice

Browse files
Dockerfile ADDED
@@ -0,0 +1,55 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.9-slim
2
+
3
+ # Install curl, Node.js, Ollama, and Nginx in a single RUN instruction
4
+ RUN apt-get update && \
5
+ apt-get install -y curl nginx && \
6
+ # Install Ollama (using their official script)
7
+ curl -fsSL https://ollama.ai/install.sh | sh && \
8
+ # Install Node.js 18.x from NodeSource
9
+ curl -fsSL https://deb.nodesource.com/setup_18.x | bash - && \
10
+ apt-get install -y nodejs && \
11
+ # Clean up APT caches to reduce image size
12
+ apt-get clean && rm -rf /var/lib/apt/lists/*
13
+
14
+ # Create Nginx cache/temp and log directories as root
15
+ RUN mkdir -p /var/cache/nginx /var/lib/nginx/body /var/lib/nginx/fastcgi /var/lib/nginx/proxy /var/lib/nginx/scgi /var/lib/nginx/uwsgi /var/log/nginx
16
+
17
+ # Set up user and environment
18
+ # Running as a non-root user is good practice
19
+ RUN useradd -m -u 1000 user
20
+ # NOW chown the directories, after the user 'user' is created
21
+ RUN chown -R user:user /var/cache/nginx /var/lib/nginx /var/log/nginx && \
22
+ chmod -R 755 /var/cache/nginx /var/lib/nginx /var/log/nginx
23
+
24
+ # --- CRITICAL CHANGE: Move USER instruction HERE ---
25
+ USER user
26
+ ENV HOME=/home/user \
27
+ PATH="/home/user/.local/bin:$PATH"
28
+
29
+ WORKDIR $HOME/app
30
+
31
+ # Copy requirements.txt FIRST to leverage Docker caching for Python dependencies
32
+ COPY --chown=user requirements.txt .
33
+ RUN pip install --no-cache-dir --upgrade -r requirements.txt
34
+
35
+ # Copy the entire frontend directory early to ensure Node.js dependencies are installed
36
+ COPY --chown=user frontend frontend/
37
+
38
+ # Install Node.js dependencies for the frontend and build it
39
+ RUN cd frontend && npm install --production=false && npm run build
40
+
41
+ # Copy the Nginx configuration
42
+ COPY --chown=user nginx.conf /etc/nginx/nginx.conf
43
+
44
+ # Copy the rest of the application files AFTER dependencies are installed
45
+ COPY --chown=user . .
46
+
47
+ # Make the start script executable
48
+ RUN chmod +x start.sh
49
+
50
+ # Expose ports for FastAPI (7860) and Nginx (8501)
51
+ EXPOSE 7860
52
+ EXPOSE 8501
53
+
54
+ # Command to execute when the container starts
55
+ CMD ["./start.sh"]
frontend/.gitignore ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Logs
2
+ logs
3
+ *.log
4
+ npm-debug.log*
5
+ yarn-debug.log*
6
+ yarn-error.log*
7
+ pnpm-debug.log*
8
+ lerna-debug.log*
9
+
10
+ node_modules
11
+ dist
12
+ dist-ssr
13
+ *.local
14
+
15
+ # Editor directories and files
16
+ .vscode/*
17
+ !.vscode/extensions.json
18
+ .idea
19
+ .DS_Store
20
+ *.suo
21
+ *.ntvs*
22
+ *.njsproj
23
+ *.sln
24
+ *.sw?
frontend/README.md ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # React + Vite
2
+
3
+ This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
4
+
5
+ Currently, two official plugins are available:
6
+
7
+ - [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) for Fast Refresh
8
+ - [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
9
+
10
+ ## Expanding the ESLint configuration
11
+
12
+ If you are developing a production application, we recommend using TypeScript with type-aware lint rules enabled. Check out the [TS template](https://github.com/vitejs/vite/tree/main/packages/create-vite/template-react-ts) for information on how to integrate TypeScript and [`typescript-eslint`](https://typescript-eslint.io) in your project.
frontend/eslint.config.js ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import js from '@eslint/js'
2
+ import globals from 'globals'
3
+ import reactHooks from 'eslint-plugin-react-hooks'
4
+ import reactRefresh from 'eslint-plugin-react-refresh'
5
+ import { defineConfig, globalIgnores } from 'eslint/config'
6
+
7
+ export default defineConfig([
8
+ globalIgnores(['dist']),
9
+ {
10
+ files: ['**/*.{js,jsx}'],
11
+ extends: [
12
+ js.configs.recommended,
13
+ reactHooks.configs['recommended-latest'],
14
+ reactRefresh.configs.vite,
15
+ ],
16
+ languageOptions: {
17
+ ecmaVersion: 2020,
18
+ globals: globals.browser,
19
+ parserOptions: {
20
+ ecmaVersion: 'latest',
21
+ ecmaFeatures: { jsx: true },
22
+ sourceType: 'module',
23
+ },
24
+ },
25
+ rules: {
26
+ 'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }],
27
+ },
28
+ },
29
+ ])
frontend/index.html ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <link rel="icon" type="image/svg+xml" href="/vite.svg" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
+ <title>Vite + React</title>
8
+ </head>
9
+ <body>
10
+ <div id="root"></div>
11
+ <script type="module" src="/src/main.jsx"></script>
12
+ </body>
13
+ </html>
frontend/package-lock.json ADDED
The diff for this file is too large to render. See raw diff
 
frontend/package.json ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "frontend",
3
+ "private": true,
4
+ "version": "0.0.0",
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite",
8
+ "build": "vite build",
9
+ "lint": "eslint .",
10
+ "preview": "vite preview"
11
+ },
12
+ "dependencies": {
13
+ "autoprefixer": "^10.4.21",
14
+ "firebase": "^12.0.0",
15
+ "postcss": "^8.5.6",
16
+ "react": "^19.1.0",
17
+ "react-dom": "^19.1.0",
18
+ "react-router-dom": "^7.7.1"
19
+ },
20
+ "devDependencies": {
21
+ "@eslint/js": "^9.30.1",
22
+ "@tailwindcss/container-queries": "^0.1.1",
23
+ "@tailwindcss/forms": "^0.5.10",
24
+ "@types/react": "^19.1.8",
25
+ "@types/react-dom": "^19.1.6",
26
+ "@vitejs/plugin-react": "^4.6.0",
27
+ "eslint": "^9.30.1",
28
+ "eslint-plugin-react-hooks": "^5.2.0",
29
+ "eslint-plugin-react-refresh": "^0.4.20",
30
+ "globals": "^16.3.0",
31
+ "tailwindcss": "^3.4.17",
32
+ "vite": "^7.0.4"
33
+ }
34
+ }
frontend/postcss.config.js ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ export default {
2
+ plugins: {
3
+ tailwindcss: {},
4
+ autoprefixer: {},
5
+ },
6
+ }
frontend/public/vite.svg ADDED
frontend/src/App.css ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* frontend/src/App.css */
2
+ @tailwind base;
3
+ @tailwind components;
4
+ @tailwind utilities;
5
+
6
+ /* Optional: Basic body styling */
7
+ body {
8
+ margin: 0;
9
+ font-family: 'Inter', "Noto Sans", sans-serif;
10
+ -webkit-font-smoothing: antialiased;
11
+ -moz-osx-font-smoothing: grayscale;
12
+ }
13
+
14
+ /* Hide scrollbar for webkit browsers */
15
+ .hide-scrollbar::-webkit-scrollbar {
16
+ display: none;
17
+ }
18
+
19
+ /* For IE, Edge and Firefox */
20
+ .hide-scrollbar {
21
+ -ms-overflow-style: none; /* IE and Edge */
22
+ scrollbar-width: none; /* Firefox */
23
+ }
frontend/src/App.jsx ADDED
@@ -0,0 +1,54 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState, useEffect } from 'react';
2
+ import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom';
3
+ import { onAuthStateChanged } from 'firebase/auth';
4
+ import { auth } from './services/firebase';
5
+
6
+ import Header from './components/layout/Header';
7
+ import Login from './components/auth/Login';
8
+ import SignUp from './components/auth/SignUp';
9
+ import ChatInterface from './components/chat/ChatInterface';
10
+ import History from './components/chat/History';
11
+ import './index.css';
12
+
13
+ function App() {
14
+ const [user, setUser] = useState(null);
15
+ const [loading, setLoading] = useState(true);
16
+
17
+ useEffect(() => {
18
+ const unsubscribe = onAuthStateChanged(auth, (currentUser) => {
19
+ setUser(currentUser);
20
+ setLoading(false);
21
+ });
22
+ return () => unsubscribe();
23
+ }, []);
24
+
25
+ if (loading) {
26
+ return <div>Loading...</div>; // Or a proper spinner component
27
+ }
28
+
29
+ return (
30
+ <Router>
31
+ <div className="app-wrapper">
32
+ <Header user={user} />
33
+ <main>
34
+ <Routes>
35
+ <Route path="/login" element={<Login />} />
36
+ <Route path="/signup" element={<SignUp />} />
37
+ <Route
38
+ path="/chat"
39
+ element={user ? <ChatInterface /> : <Navigate to="/login" />}
40
+ />
41
+ <Route
42
+ path="/history"
43
+ element={user ? <History /> : <Navigate to="/login" />}
44
+ />
45
+ {/* Redirect root to chat if logged in, otherwise to login */}
46
+ <Route path="/" element={user ? <Navigate to="/chat" /> : <Navigate to="/login" />} />
47
+ </Routes>
48
+ </main>
49
+ </div>
50
+ </Router>
51
+ );
52
+ }
53
+
54
+ export default App;
frontend/src/assets/react.svg ADDED
frontend/src/components/auth/Login.jsx ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState } from 'react';
2
+ import { signInWithEmailAndPassword } from "firebase/auth";
3
+ import { auth } from '../../services/firebase.js';
4
+ import { useNavigate, Link } from 'react-router-dom';
5
+
6
+ const Login = () => {
7
+ const [email, setEmail] = useState('');
8
+ const [password, setPassword] = useState('');
9
+ const [error, setError] = useState('');
10
+ const navigate = useNavigate();
11
+
12
+ const handleLogin = async (e) => {
13
+ e.preventDefault();
14
+ setError('');
15
+ try {
16
+ await signInWithEmailAndPassword(auth, email, password);
17
+ navigate('/chat'); // Redirect to chat on successful login
18
+ } catch (err) {
19
+ setError(err.message);
20
+ }
21
+ };
22
+
23
+ return (
24
+ <div className="auth-container">
25
+ <div className="auth-form">
26
+ <h2>Login to Your Account</h2>
27
+ <form onSubmit={handleLogin}>
28
+ <div className="form-group">
29
+ <label>Email</label>
30
+ <input
31
+ type="email"
32
+ value={email}
33
+ onChange={(e) => setEmail(e.target.value)}
34
+ placeholder="Enter your email"
35
+ required
36
+ />
37
+ </div>
38
+ <div className="form-group">
39
+ <label>Password</label>
40
+ <input
41
+ type="password"
42
+ value={password}
43
+ onChange={(e) => setPassword(e.target.value)}
44
+ placeholder="Enter your password"
45
+ required
46
+ />
47
+ </div>
48
+ {error && <p className="error-message">{error}</p>}
49
+ <div className="forgot-password">
50
+ <Link to="/forgot-password">Forgot Password?</Link>
51
+ </div>
52
+ <button type="submit" className="btn-primary">Login</button>
53
+ </form>
54
+ </div>
55
+ </div>
56
+ );
57
+ };
58
+
59
+ export default Login;
frontend/src/components/auth/SignUp.jsx ADDED
@@ -0,0 +1,67 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState } from 'react';
2
+ import { createUserWithEmailAndPassword } from "firebase/auth";
3
+ import { auth } from '/src/services/firebase.js';
4
+ import { useNavigate } from 'react-router-dom';
5
+
6
+ const SignUp = () => {
7
+ const [name, setName] = useState('');
8
+ const [email, setEmail] = useState('');
9
+ const [password, setPassword] = useState('');
10
+ const [error, setError] = useState('');
11
+ const navigate = useNavigate();
12
+
13
+ const handleSignUp = async (e) => {
14
+ e.preventDefault();
15
+ setError('');
16
+ try {
17
+ await createUserWithEmailAndPassword(auth, email, password);
18
+ navigate('/chat'); // Redirect to chat on successful sign-up
19
+ } catch (err) {
20
+ setError(err.message);
21
+ }
22
+ };
23
+
24
+ return (
25
+ <div className="auth-container">
26
+ <div className="auth-form">
27
+ <h2>Sign up</h2>
28
+ <form onSubmit={handleSignUp}>
29
+ <div className="form-group">
30
+ <label>Name</label>
31
+ <input
32
+ type="text"
33
+ value={name}
34
+ onChange={(e) => setName(e.target.value)}
35
+ placeholder="Enter your name"
36
+ required
37
+ />
38
+ </div>
39
+ <div className="form-group">
40
+ <label>Email</label>
41
+ <input
42
+ type="email"
43
+ value={email}
44
+ onChange={(e) => setEmail(e.target.value)}
45
+ placeholder="Enter your email"
46
+ required
47
+ />
48
+ </div>
49
+ <div className="form-group">
50
+ <label>Password</label>
51
+ <input
52
+ type="password"
53
+ value={password}
54
+ onChange={(e) => setPassword(e.target.value)}
55
+ placeholder="Enter your password"
56
+ required
57
+ />
58
+ </div>
59
+ {error && <p className="error-message">{error}</p>}
60
+ <button type="submit" className="btn-primary">Sign Up</button>
61
+ </form>
62
+ </div>
63
+ </div>
64
+ );
65
+ };
66
+
67
+ export default SignUp;
frontend/src/components/chat/ChatInputArea.jsx ADDED
@@ -0,0 +1,104 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // frontend/src/components/chat/ChatInputArea.jsx
2
+ import React, { useState, useRef } from 'react';
3
+ import { ReactMic } from 'react-mic'; // For audio recording
4
+ import axios from 'axios'; // For sending audio to backend
5
+
6
+ const ChatInputArea = ({ onSendMessage, onSendVoiceMessage, isLoading }) => {
7
+ const [message, setMessage] = useState('');
8
+ const [isRecording, setIsRecording] = useState(false);
9
+ const [recordedBlob, setRecordedBlob] = useState(null); // To hold the recorded audio blob
10
+
11
+ // Correct BASE_URL to point to Nginx's /api endpoint
12
+ // Nginx will then proxy /api/transcribe-audio to FastAPI's /transcribe-audio
13
+ const BASE_URL = '/api';
14
+
15
+ const handleTextChange = (e) => {
16
+ setMessage(e.target.value);
17
+ };
18
+
19
+ const handleSubmit = (e) => {
20
+ e.preventDefault();
21
+ if (message.trim()) {
22
+ onSendMessage(message);
23
+ setMessage('');
24
+ }
25
+ };
26
+
27
+ // --- Voice Recording Handlers ---
28
+ const startRecording = () => {
29
+ if (isLoading) return; // Prevent recording if already busy
30
+ setIsRecording(true);
31
+ setRecordedBlob(null); // Clear previous recording
32
+ console.log("Recording started...");
33
+ };
34
+
35
+ const stopRecording = () => {
36
+ setIsRecording(false);
37
+ console.log("Recording stopped.");
38
+ };
39
+
40
+ const onStop = (recordedBlob) => {
41
+ // This callback is triggered when recording stops
42
+ console.log('recordedBlob is: ', recordedBlob);
43
+ setRecordedBlob(recordedBlob); // Store the blob
44
+ // Call the voice message handler from props, which will then send to backend
45
+ onSendVoiceMessage(recordedBlob.blob);
46
+ };
47
+
48
+ // The actual sending to server logic is now moved to ChatInterface's onSendVoiceMessage
49
+ // This component just passes the audio blob up.
50
+
51
+ return (
52
+ <div className="chat-input-area">
53
+ <form onSubmit={handleSubmit} className="chat-form">
54
+ <input
55
+ type="text"
56
+ value={message}
57
+ onChange={handleTextChange}
58
+ placeholder={isRecording ? "Recording..." : "Type your message or hold for voice..."}
59
+ disabled={isLoading || isRecording}
60
+ />
61
+ <button type="submit" disabled={isLoading || isRecording || !message.trim()}>
62
+ Send
63
+ </button>
64
+
65
+ {/* Microphone icon and recording area */}
66
+ <div className="voice-input-container">
67
+ <button
68
+ type="button"
69
+ className={`voice-record-btn ${isRecording ? 'recording' : ''}`}
70
+ onMouseDown={startRecording}
71
+ onMouseUp={stopRecording}
72
+ // For mobile touch events
73
+ onTouchStart={startRecording}
74
+ onTouchEnd={stopRecording}
75
+ disabled={isLoading}
76
+ title="Hold to record voice"
77
+ >
78
+ {isRecording ? (
79
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" className="w-6 h-6 animate-pulse">
80
+ <path d="M8.25 4.5a3.75 3.75 0 1 1 7.5 0v8.25a3.75 3.75 0 1 1-7.5 0V4.5Z" />
81
+ <path d="M6 10.5a.75.75 0 0 1 .75.75v1.5a5.25 5.25 0 1 0 10.5 0v-1.5a.75.75 0 0 1 1.5 0v1.5a6.75 6.75 0 1 1-13.5 0v-1.5A.75.75 0 0 1 6 10.5Z" />
82
+ </svg>
83
+ ) : (
84
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" className="w-6 h-6">
85
+ <path d="M8.25 4.5a3.75 3.75 0 1 1 7.5 0v8.25a3.75 3.75 0 1 1-7.5 0V4.5Z" />
86
+ <path d="M6 10.5a.75.75 0 0 1 .75.75v1.5a5.25 5.25 0 1 0 10.5 0v-1.5a.75.75 0 0 1 1.5 0v1.5a6.75 6.75 0 1 1-13.5 0v-1.5A.75.75 0 0 1 6 10.5Z" />
87
+ </svg>
88
+ )}
89
+ </button>
90
+ <ReactMic
91
+ record={isRecording}
92
+ className="sound-wave"
93
+ onStop={onStop}
94
+ strokeColor="#00000000" // Transparent stroke
95
+ backgroundColor="#00000000" // Transparent background for wave
96
+ mimeType="audio/webm" // Common format for browser recording
97
+ />
98
+ </div>
99
+ </form>
100
+ </div>
101
+ );
102
+ };
103
+
104
+ export default ChatInputArea;
frontend/src/components/chat/ChatInterface.css ADDED
@@ -0,0 +1,172 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* General Layout */
2
+ .chat-layout {
3
+ display: flex;
4
+ height: 100vh;
5
+ overflow: hidden; /* Prevent body scroll if content overflows */
6
+ }
7
+
8
+ .chat-main {
9
+ flex-grow: 1;
10
+ display: flex;
11
+ flex-direction: column;
12
+ justify-content: space-between;
13
+ background-color: #f9f9f9;
14
+ }
15
+
16
+ /* Chat Messages Area */
17
+ .chat-messages {
18
+ flex-grow: 1;
19
+ overflow-y: auto; /* Enable scrolling for messages */
20
+ padding: 20px;
21
+ display: flex;
22
+ flex-direction: column;
23
+ gap: 15px; /* Spacing between message wrappers */
24
+ }
25
+
26
+ /* Empty Chat Placeholder */
27
+ .empty-chat-placeholder {
28
+ text-align: center;
29
+ padding: 50px;
30
+ color: #666;
31
+ flex-grow: 1; /* Pushes content to the bottom if few messages */
32
+ display: flex;
33
+ flex-direction: column;
34
+ justify-content: center;
35
+ align-items: center;
36
+ }
37
+ .empty-chat-placeholder h1 {
38
+ font-size: 2.5em;
39
+ margin-bottom: 10px;
40
+ color: #333;
41
+ }
42
+ .empty-chat-placeholder p {
43
+ font-size: 1.1em;
44
+ line-height: 1.6;
45
+ }
46
+
47
+ /* Message Wrapper (contains avatar and bubble) */
48
+ .message-wrapper {
49
+ display: flex; /* Use flexbox for layout of avatar and bubble */
50
+ align-items: flex-start; /* Align items to the top */
51
+ max-width: 100%; /* Ensure it doesn't overflow */
52
+ }
53
+
54
+ /* Specific styles for user messages (align to right) */
55
+ .message-wrapper.user {
56
+ justify-content: flex-end; /* Push user messages to the right */
57
+ flex-direction: row-reverse; /* Put avatar on the right for user */
58
+ }
59
+
60
+ /* Chat Avatar Styling */
61
+ .chat-avatar {
62
+ font-size: 24px; /* Still useful for user emoji */
63
+ padding: 8px;
64
+ border-radius: 50%;
65
+ background-color: #e0e0e0; /* Default background for avatar circle */
66
+ min-width: 40px;
67
+ height: 40px;
68
+ display: flex;
69
+ justify-content: center;
70
+ align-items: center;
71
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
72
+ /* Margins for spacing */
73
+ margin-right: 10px; /* Default for assistant avatar */
74
+ overflow: hidden; /* Crucial to clip image if it's larger than the circle */
75
+ }
76
+
77
+ /* Adjust margin for user avatar (since row-reverse) */
78
+ .message-wrapper.user .chat-avatar {
79
+ margin-left: 10px;
80
+ margin-right: 0;
81
+ }
82
+
83
+ /* Style for the actual image inside the avatar div */
84
+ .chat-avatar .avatar-image {
85
+ width: 100%; /* Make image fill the avatar div */
86
+ height: 100%; /* Make image fill the avatar div */
87
+ object-fit: cover; /* Crop and cover the area without distortion */
88
+ border-radius: 50%; /* Ensure the image itself is also rounded */
89
+ }
90
+
91
+ /* Message Bubble Styling */
92
+ .message-bubble {
93
+ background-color: #f0f0f0;
94
+ padding: 12px 18px;
95
+ border-radius: 20px;
96
+ max-width: 70%; /* Limit message width */
97
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
98
+
99
+ /* --- THE KEY CSS PROPERTY FOR NEWLINES --- */
100
+ white-space: pre-wrap; /* This will make '\n' characters create new lines */
101
+ /* --- END KEY CSS PROPERTY --- */
102
+
103
+ word-break: break-word; /* Ensure long words break within the bubble */
104
+ overflow-wrap: break-word; /* Modern equivalent */
105
+ }
106
+
107
+ /* Specific styling for assistant bubbles */
108
+ .message-wrapper.assistant .message-bubble {
109
+ background-color: #e6f7ff; /* Light blue for assistant */
110
+ border-bottom-left-radius: 5px; /* Pointy bottom-left for assistant */
111
+ margin-right: auto; /* Push to left, next to sidebar */
112
+ }
113
+
114
+ /* Specific styling for user bubbles */
115
+ .message-wrapper.user .message-bubble {
116
+ background-color: #d1e7dd; /* Light green for user */
117
+ border-bottom-right-radius: 5px; /* Pointy bottom-right for user */
118
+ margin-left: auto; /* Push to right */
119
+ }
120
+
121
+ /* Chat Input Area */
122
+ .chat-input-area {
123
+ padding: 20px;
124
+ border-top: 1px solid #eee;
125
+ background-color: #fff;
126
+ }
127
+
128
+ .chat-form {
129
+ display: flex;
130
+ gap: 10px;
131
+ }
132
+
133
+ .chat-form input[type="text"] {
134
+ flex-grow: 1;
135
+ padding: 12px;
136
+ border: 1px solid #ccc;
137
+ border-radius: 25px;
138
+ font-size: 16px;
139
+ outline: none;
140
+ }
141
+
142
+ .chat-form button {
143
+ padding: 12px 20px;
144
+ background-color: #007bff;
145
+ color: white;
146
+ border: none;
147
+ border-radius: 25px;
148
+ cursor: pointer;
149
+ font-size: 16px;
150
+ transition: background-color 0.2s;
151
+ }
152
+
153
+ .chat-form button:hover:not(:disabled) {
154
+ background-color: #0056b3;
155
+ }
156
+
157
+ .chat-form button:disabled {
158
+ background-color: #a0a0a0;
159
+ cursor: not-allowed;
160
+ }
161
+
162
+ /* Blinking Cursor */
163
+ .blinking-cursor {
164
+ animation: blink 1s step-end infinite;
165
+ display: inline-block; /* Essential for it to be visible next to text */
166
+ width: 0.5em; /* Give it a small width */
167
+ }
168
+
169
+ @keyframes blink {
170
+ from, to { opacity: 1; }
171
+ 50% { opacity: 0; }
172
+ }
frontend/src/components/chat/ChatInterface.jsx ADDED
@@ -0,0 +1,254 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState, useEffect, useRef } from 'react';
2
+ import { saveChatHistory } from '../../services/firestore';
3
+ import { auth } from '../../services/firebase';
4
+ import Sidebar from '../layout/Sidebar';
5
+ import ChatInputArea from './ChatInputArea';
6
+ import ChatbotAvatar from '../../assets/chatbot.png';
7
+ import axios from 'axios'; // Import axios here for the voice API call
8
+
9
+ const ChatInterface = () => {
10
+ const [chatHistory, setChatHistory] = useState([]);
11
+ const [isSending, setIsSending] = useState(false);
12
+ const chatEndRef = useRef(null);
13
+
14
+ const FASTAPI_LLM_URL = '/api/ask'; // Endpoint for LLM interaction
15
+ const FASTAPI_TRANSCRIBE_URL = '/api/transcribe-audio'; // Endpoint for audio transcription
16
+
17
+ useEffect(() => {
18
+ chatEndRef.current?.scrollIntoView({ behavior: 'smooth' });
19
+ }, [chatHistory]);
20
+
21
+ const handleNewChat = () => {
22
+ if (auth.currentUser && chatHistory.length > 0) {
23
+ saveChatHistory(chatHistory);
24
+ }
25
+ setChatHistory([]);
26
+ };
27
+
28
+ const handleSendMessage = async (userPrompt) => {
29
+ if (!userPrompt.trim() || isSending) return;
30
+
31
+ const userMessage = { role: 'user', message: userPrompt.trim(), timestamp: new Date() };
32
+ setChatHistory(prev => [
33
+ ...prev,
34
+ userMessage,
35
+ { role: 'assistant', message: 'Thinking...', streaming: true, timestamp: new Date() }
36
+ ]);
37
+ setIsSending(true);
38
+
39
+ let currentFullResponse = '';
40
+ let assistantMessageTimestamp = new Date();
41
+ let assistantMessageIndex = -1;
42
+
43
+ setChatHistory(prev => {
44
+ const updated = [...prev];
45
+ assistantMessageIndex = updated.findIndex(msg => msg.role === 'assistant' && msg.streaming);
46
+ if (assistantMessageIndex === -1) {
47
+ assistantMessageIndex = updated.length - 1;
48
+ }
49
+ return updated;
50
+ });
51
+
52
+ try {
53
+ const response = await fetch(FASTAPI_LLM_URL, {
54
+ method: 'POST',
55
+ headers: { 'Content-Type': 'application/json' },
56
+ body: JSON.stringify({ text: userMessage.message }),
57
+ });
58
+
59
+ if (!response.ok) {
60
+ const errorDetail = await response.text();
61
+ throw new Error(`HTTP error! Status: ${response.status}. Detail: ${errorDetail}`);
62
+ }
63
+
64
+ const reader = response.body.getReader();
65
+ const decoder = new TextDecoder('utf-8');
66
+ let buffer = '';
67
+
68
+ while (true) {
69
+ const { value, done } = await reader.read();
70
+ if (done) {
71
+ console.log("Stream finished.");
72
+ processPartialBuffer(buffer);
73
+ break;
74
+ }
75
+ const decodedChunk = decoder.decode(value, { stream: true });
76
+ buffer += decodedChunk;
77
+
78
+ const events = buffer.split('\n\n');
79
+ buffer = events.pop() || '';
80
+
81
+ for (const eventString of events) {
82
+ processPartialBuffer(eventString);
83
+ }
84
+
85
+ setChatHistory(prev => {
86
+ const updated = [...prev];
87
+ const targetMessage = updated[assistantMessageIndex];
88
+ if (targetMessage && targetMessage.role === 'assistant') {
89
+ targetMessage.message = currentFullResponse + (targetMessage.streaming ? '▌' : '');
90
+ targetMessage.streaming = true;
91
+ }
92
+ return updated;
93
+ });
94
+ }
95
+
96
+ function processPartialBuffer(line) {
97
+ if (line.startsWith('data:')) {
98
+ try {
99
+ const jsonString = line.substring(5);
100
+ const parsedData = JSON.parse(jsonString);
101
+ if (parsedData.token) {
102
+ currentFullResponse += parsedData.token;
103
+ } else if (parsedData.event === 'error') {
104
+ console.error("Backend error received:", parsedData.error);
105
+ currentFullResponse += `\n[AI Error: ${parsedData.error}]`;
106
+ }
107
+ } catch (err) {
108
+ console.error('Error parsing event data:', line, err);
109
+ }
110
+ } else if (line.trim() !== '') {
111
+ console.warn("Non-data line encountered:", `"${line}"`);
112
+ }
113
+ }
114
+
115
+ } catch (err) {
116
+ console.error('General Fetch/Stream Error:', err);
117
+ currentFullResponse += `\nSorry, something went wrong. Please try again. [Error: ${err.message}]`;
118
+ setChatHistory(prev => {
119
+ const updated = [...prev];
120
+ const targetMessage = updated[assistantMessageIndex];
121
+ if (targetMessage) {
122
+ targetMessage.message = currentFullResponse;
123
+ targetMessage.timestamp = new Date();
124
+ targetMessage.streaming = false;
125
+ } else {
126
+ updated.push({ role: 'assistant', message: currentFullResponse, timestamp: new Date(), streaming: false });
127
+ }
128
+ return updated;
129
+ });
130
+ } finally {
131
+ setIsSending(false);
132
+ setChatHistory(prev => {
133
+ const updated = [...prev];
134
+ const targetMessage = updated[assistantMessageIndex];
135
+ if (targetMessage && targetMessage.role === 'assistant') {
136
+ targetMessage.message = currentFullResponse;
137
+ targetMessage.timestamp = assistantMessageTimestamp;
138
+ targetMessage.streaming = false;
139
+ }
140
+ if (!targetMessage || targetMessage.message === '') {
141
+ const fallbackMessage = "Sorry, an unexpected error occurred.";
142
+ updated.push({ role: 'assistant', message: fallbackMessage, timestamp: new Date(), streaming: false });
143
+ }
144
+ return updated;
145
+ });
146
+
147
+ if (auth.currentUser) {
148
+ setChatHistory(finalChatState => {
149
+ const chatToSave = [...finalChatState];
150
+ const lastMessageForSave = chatToSave.at(-1);
151
+ if (lastMessageForSave && lastMessageForSave.role === 'assistant' && lastMessageForSave.message.endsWith('▌')) {
152
+ lastMessageForSave.message = lastMessageForSave.message.slice(0, -1);
153
+ }
154
+ saveChatHistory(chatToSave);
155
+ return finalChatState;
156
+ });
157
+ }
158
+ }
159
+ };
160
+
161
+ const handleSendVoiceMessage = async (audioBlob) => {
162
+ if (isSending) return;
163
+
164
+ // Display a placeholder message while transcribing
165
+ setChatHistory(prev => [
166
+ ...prev,
167
+ { role: 'user', message: '(Recording audio...)', timestamp: new Date() },
168
+ { role: 'assistant', message: 'Transcribing...', streaming: true, timestamp: new Date() } // Assistant thinking message
169
+ ]);
170
+ setIsSending(true);
171
+
172
+ let transcriptionResult = '';
173
+ try {
174
+ const formData = new FormData();
175
+ formData.append('audio_file', audioBlob, 'audio.webm'); // Ensure filename and type are consistent
176
+
177
+ const response = await axios.post(FASTAPI_TRANSCRIBE_URL, formData, {
178
+ headers: {
179
+ 'Content-Type': 'multipart/form-data',
180
+ },
181
+ timeout: 60000, // 60 seconds timeout for transcription
182
+ });
183
+
184
+ transcriptionResult = response.data.transcription;
185
+
186
+ if (transcriptionResult) {
187
+ // Update the user's "Recording audio..." message with the actual transcription
188
+ setChatHistory(prev => {
189
+ const updated = prev.filter(msg => msg.message !== '(Recording audio...)'); // Remove placeholder
190
+ return [...updated, { role: 'user', message: transcriptionResult, timestamp: new Date() }];
191
+ });
192
+ // Now, send the transcribed text to your LLM
193
+ await handleSendMessage(transcriptionResult);
194
+ } else {
195
+ alert('Could not transcribe audio. Please try speaking clearer.');
196
+ // Remove transcription messages if empty
197
+ setChatHistory(prev => prev.filter(msg => msg.message !== '(Recording audio...)' && msg.message !== 'Transcribing...'));
198
+ }
199
+ } catch (error) {
200
+ console.error('Error during voice transcription:', error);
201
+ alert(`Failed to transcribe audio. Error: ${error.message || 'Unknown error'}`);
202
+ // Remove transcription messages if error
203
+ setChatHistory(prev => prev.filter(msg => msg.message !== '(Recording audio...)' && msg.message !== 'Transcribing...'));
204
+ } finally {
205
+ setIsSending(false);
206
+ // Ensure the "Transcribing..." message is removed
207
+ setChatHistory(prev => prev.filter(msg => !(msg.role === 'assistant' && msg.message === 'Transcribing...')));
208
+ }
209
+ };
210
+
211
+
212
+ return (
213
+ <div className="chat-layout">
214
+ <Sidebar onNewChat={handleNewChat} />
215
+ <div className="chat-main">
216
+ <div className="chat-messages hide-scrollbar">
217
+ {chatHistory.length === 0 ? (
218
+ <div className="empty-chat-placeholder">
219
+ <h1>Start a new conversation</h1>
220
+ <p>Type a message or record your voice to begin interacting.</p>
221
+ {!auth.currentUser && <p>Please log in to save your chat history.</p>}
222
+ </div>
223
+ ) : (
224
+ chatHistory.map((chat, index) => (
225
+ <div key={index} className={`message-wrapper ${chat.role}`}>
226
+ <div className="chat-avatar">
227
+ {chat.role === 'assistant' ? (
228
+ <img src={ChatbotAvatar} alt="Chatbot Avatar" className="avatar-image" />
229
+ ) : (
230
+ <div className="user-icon-placeholder">👤</div>
231
+ )}
232
+ </div>
233
+ <div className="message-bubble">
234
+ {chat.message}
235
+ {chat.streaming && chat.role === 'assistant' && (
236
+ <span className="blinking-cursor">▌</span>
237
+ )}
238
+ </div>
239
+ </div>
240
+ ))
241
+ )}
242
+ <div ref={chatEndRef} />
243
+ </div>
244
+ <ChatInputArea
245
+ onSendMessage={handleSendMessage}
246
+ onSendVoiceMessage={handleSendVoiceMessage}
247
+ isLoading={isSending}
248
+ />
249
+ </div>
250
+ </div>
251
+ );
252
+ };
253
+
254
+ export default ChatInterface;
frontend/src/components/chat/History.css ADDED
@@ -0,0 +1,152 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* History.css */
2
+
3
+ .chat-layout {
4
+ display: flex;
5
+ height: 100vh;
6
+ /* Assuming chat-layout takes full viewport height */
7
+ }
8
+
9
+ .chat-main {
10
+ flex-grow: 1;
11
+ padding: 20px;
12
+ background-color: #f0f2f5;
13
+ overflow-y: auto; /* Enable scrolling for main content */
14
+ }
15
+
16
+ .history-title {
17
+ color: #333;
18
+ margin-bottom: 20px;
19
+ font-size: 24px;
20
+ }
21
+
22
+ .history-list {
23
+ list-style: none;
24
+ padding: 0;
25
+ }
26
+
27
+ .history-item {
28
+ background-color: #fff;
29
+ border-radius: 8px;
30
+ padding: 15px 20px;
31
+ margin-bottom: 10px;
32
+ display: flex;
33
+ align-items: center;
34
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
35
+ cursor: pointer;
36
+ transition: background-color 0.2s ease-in-out;
37
+ }
38
+
39
+ .history-item:hover {
40
+ background-color: #e9ecef;
41
+ }
42
+
43
+ .history-icon {
44
+ font-size: 24px;
45
+ margin-right: 15px;
46
+ }
47
+
48
+ .history-details {
49
+ flex-grow: 1;
50
+ }
51
+
52
+ .history-prompt {
53
+ font-weight: bold;
54
+ color: #555;
55
+ margin-bottom: 5px;
56
+ }
57
+
58
+ .history-date {
59
+ font-size: 0.85em;
60
+ color: #888;
61
+ }
62
+
63
+ /* Conversation View Specific Styles */
64
+ .conversation-view {
65
+ background-color: #fff;
66
+ border-radius: 8px;
67
+ padding: 20px;
68
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
69
+ }
70
+
71
+ .back-button {
72
+ background-color: #007bff;
73
+ color: white;
74
+ border: none;
75
+ padding: 10px 15px;
76
+ border-radius: 5px;
77
+ cursor: pointer;
78
+ margin-bottom: 15px;
79
+ font-size: 16px;
80
+ transition: background-color 0.2s ease-in-out;
81
+ }
82
+
83
+ .back-button:hover {
84
+ background-color: #0056b3;
85
+ }
86
+
87
+ .conversation-title {
88
+ color: #333;
89
+ margin-bottom: 20px;
90
+ font-size: 20px;
91
+ border-bottom: 1px solid #eee;
92
+ padding-bottom: 10px;
93
+ }
94
+
95
+ .messages-list {
96
+ max-height: 500px; /* Limit height and enable scrolling for messages */
97
+ overflow-y: auto;
98
+ padding-right: 10px; /* For scrollbar spacing */
99
+ }
100
+
101
+ .message-item {
102
+ margin-bottom: 15px;
103
+ padding: 10px 15px;
104
+ border-radius: 8px;
105
+ max-width: 80%;
106
+ clear: both; /* Clear floats for alignment */
107
+ }
108
+
109
+ .message-item.user {
110
+ background-color: #dcf8c6; /* Light green for user messages */
111
+ align-self: flex-end; /* Align to the right in a flex container */
112
+ margin-left: auto; /* Push to the right */
113
+ }
114
+
115
+ .message-item.chatbot {
116
+ background-color: #e2e2e2; /* Light grey for chatbot messages */
117
+ align-self: flex-start; /* Align to the left */
118
+ margin-right: auto; /* Push to the left */
119
+ }
120
+
121
+ .message-sender {
122
+ font-weight: bold;
123
+ margin-bottom: 5px;
124
+ font-size: 0.9em;
125
+ }
126
+
127
+ .message-item.user .message-sender {
128
+ color: #3d8c1c; /* Darker green for user sender */
129
+ }
130
+
131
+ .message-item.chatbot .message-sender {
132
+ color: #555;
133
+ }
134
+
135
+ .message-content {
136
+ font-size: 1em;
137
+ line-height: 1.4;
138
+ word-wrap: break-word; /* Ensure long words wrap */
139
+ }
140
+
141
+ .message-timestamp {
142
+ font-size: 0.75em;
143
+ color: #999;
144
+ margin-top: 5px;
145
+ text-align: right;
146
+ }
147
+
148
+ /* Ensure messages-list items align correctly */
149
+ .messages-list {
150
+ display: flex;
151
+ flex-direction: column;
152
+ }
frontend/src/components/chat/History.jsx ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ import React, { useState, useEffect } from 'react';
3
+ import { getChatHistory } from '../../services/firestore.js';
4
+ import Sidebar from '../layout/Sidebar';
5
+
6
+ const History = () => {
7
+ const [chats, setChats] = useState([]);
8
+ const [loading, setLoading] = useState(true);
9
+
10
+ useEffect(() => {
11
+ const fetchHistory = async () => {
12
+ const userChats = await getChatHistory();
13
+ setChats(userChats);
14
+ setLoading(false);
15
+ };
16
+ fetchHistory();
17
+ }, []);
18
+
19
+ return (
20
+ <div className="chat-layout">
21
+ <Sidebar onNewChat={() => {}} />
22
+ <div className="chat-main">
23
+ <h2 className="history-title">History</h2>
24
+ {loading ? (
25
+ <p>Loading history...</p>
26
+ ) : (
27
+ <ul className="history-list">
28
+ {chats.map(chat => (
29
+ <li key={chat.id} className="history-item">
30
+ <span className="history-icon">💬</span>
31
+ <div className="history-details">
32
+ <p className="history-prompt">{chat.title}</p>
33
+ <span className="history-date">
34
+ {new Date(chat.createdAt?.toDate()).toLocaleString()}
35
+ </span>
36
+ </div>
37
+ </li>
38
+ ))}
39
+ </ul>
40
+ )}
41
+ </div>
42
+ </div>
43
+ );
44
+ };
45
+
46
+ export default History;
frontend/src/components/layout/Header.jsx ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react';
2
+ import { Link, useNavigate } from 'react-router-dom';
3
+ import { auth } from '../../services/firebase';
4
+ import { signOut } from 'firebase/auth';
5
+ import logo from '../../assets/chatbot.png'; // Assuming this is your Dobby logo
6
+
7
+ const Header = ({ user }) => {
8
+ const navigate = useNavigate();
9
+
10
+ const handleLogout = async () => {
11
+ await signOut(auth);
12
+ navigate('/login');
13
+ };
14
+
15
+ return (
16
+ <header className="app-header">
17
+ <div className="logo">
18
+ <img src={logo} alt="Dobby Chatbot Logo" />
19
+ <div className="brand-names">
20
+ <span className="main-brand">Dobby</span>
21
+ <span className="sub-brand">GUVI Assistant</span>
22
+ </div>
23
+ </div>
24
+ <nav>
25
+ {user ? (
26
+ <>
27
+ <Link to="/chat">Home</Link>
28
+ <Link to="/features">Features</Link>
29
+ <Link to="/pricing">Pricing</Link>
30
+ {/* Log Out: Green background, white text */}
31
+ <button onClick={handleLogout} className="btn-solid-green">Log Out</button>
32
+ </>
33
+ ) : (
34
+ <>
35
+ <Link to="/">Home</Link>
36
+ <Link to="/features">Features</Link>
37
+ <Link to="/pricing">Pricing</Link>
38
+ <Link to="/contact">Contact</Link>
39
+ {/* Log In: Green text only */}
40
+ <Link to="/login" className="link-green">Log In</Link>
41
+ {/* Sign Up: Green background, white text */}
42
+ <Link to="/signup" className="btn-solid-green">Sign Up</Link>
43
+ </>
44
+ )}
45
+ </nav>
46
+ </header>
47
+ );
48
+ };
49
+
50
+ export default Header;
frontend/src/components/layout/Sidebar.jsx ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react';
2
+ import { Link, useNavigate } from 'react-router-dom';
3
+ import { signOut } from "firebase/auth";
4
+ import { auth } from '../../services/firebase.js';
5
+
6
+ const Sidebar = ({ onNewChat }) => {
7
+ const navigate = useNavigate();
8
+
9
+ // No longer needed for sidebar, but keeping if still used elsewhere
10
+ const handleLogout = async () => {
11
+ try {
12
+ await signOut(auth);
13
+ navigate('/login');
14
+ } catch (error) {
15
+ console.error("Error signing out: ", error);
16
+ }
17
+ };
18
+
19
+ return (
20
+ <div className="sidebar">
21
+ <div className="sidebar-top">
22
+ <button className="new-chat-btn" onClick={onNewChat}>
23
+ <span className="plus-icon">+</span> New Chat
24
+ </button>
25
+ <nav className="sidebar-nav">
26
+ <Link to="/history" className="sidebar-nav-link active">
27
+ History
28
+ </Link>
29
+ <Link to="/settings" className="sidebar-nav-link">
30
+ Settings
31
+ </Link>
32
+ </nav>
33
+ </div>
34
+ {/* Removed sidebar-bottom section with Signup/Logout buttons */}
35
+ </div>
36
+ );
37
+ };
38
+
39
+ export default Sidebar;
frontend/src/index.css ADDED
@@ -0,0 +1,516 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* frontend/src/index.css */
2
+ @tailwind base;
3
+ @tailwind components;
4
+ @tailwind utilities;
5
+
6
+ /* Define custom CSS Variables */
7
+ :root {
8
+ --primary-color: #22c55e;
9
+ --background-color: #fff6d7;
10
+ --text-color: #333;
11
+ --white-color: #ffffff;
12
+ --border-color: #e5e7eb;
13
+ --green-button-bg: #4CAF50; /* Specific green for buttons */
14
+ --green-button-hover-bg: #45a049; /* Darker green on hover */
15
+ --dark-green-text: #0dba4b; /* Specific dark green for history/settings text */
16
+ --fontFamily: system-ui; /* Added a fallback for --fontFamily if not defined */
17
+ --header-height: 65px; /* Define a variable for consistent header height if it's fixed */
18
+ }
19
+
20
+ /* Global Styles for no full-page scrolling, and font-family */
21
+ html, body, #root, .app-wrapper {
22
+ height: 100%;
23
+ margin: 0;
24
+ padding: 0;
25
+ /* overflow: hidden is critical for entire page */
26
+ overflow: hidden;
27
+ }
28
+
29
+ body {
30
+ font-family: var(--fontFamily), -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
31
+ -webkit-font-smoothing: antialiased;
32
+ -moz-osx-font-smoothing: grayscale; /* Keep this for Firefox/Mozilla */
33
+ background-color: var(--white-color);
34
+ color: var(--text-color);
35
+ }
36
+
37
+ /* Hide scrollbar for webkit browsers */
38
+ .hide-scrollbar::-webkit-scrollbar {
39
+ display: none;
40
+ }
41
+ /* For IE, Edge and Firefox */
42
+ .hide-scrollbar {
43
+ -ms-overflow-style: none;
44
+ scrollbar-width: none;
45
+ }
46
+
47
+ /* --- App Wrapper (main application container) --- */
48
+ .app-wrapper {
49
+ display: flex;
50
+ flex-direction: column;
51
+ height: 100%; /* Ensure app-wrapper takes full height */
52
+ }
53
+
54
+ main {
55
+ flex-grow: 1;
56
+ display: flex;
57
+ justify-content: center;
58
+ align-items: center;
59
+ }
60
+
61
+ /* --- Header --- */
62
+ .app-header {
63
+ display: flex;
64
+ justify-content: space-between;
65
+ align-items: center;
66
+ padding: 1rem 2rem;
67
+ background-color: var(--white-color);
68
+ border-bottom: 1px solid var(--border-color);
69
+ /* position: sticky and top: 0 ensure it stays visible */
70
+ position: sticky;
71
+ top: 0;
72
+ z-index: 1000;
73
+ height: var(--header-height); /* Set fixed height for header if known */
74
+ box-sizing: border-box; /* Include padding in height calculation */
75
+ }
76
+
77
+ .logo {
78
+ display: flex;
79
+ align-items: center;
80
+ gap: 10px;
81
+ }
82
+
83
+ .logo img {
84
+ height: 40px;
85
+ width: auto;
86
+ }
87
+
88
+ .logo .brand-names {
89
+ display: flex;
90
+ flex-direction: column;
91
+ line-height: 1.2;
92
+ }
93
+
94
+ .logo .main-brand {
95
+ font-size: 1.8em;
96
+ font-weight: bold;
97
+ color: var(--text-color);
98
+ }
99
+
100
+ .logo .sub-brand {
101
+ font-size: 0.9em;
102
+ color: #777;
103
+ }
104
+
105
+ .app-header nav {
106
+ display: flex;
107
+ align-items: center;
108
+ gap: 1rem;
109
+ }
110
+
111
+ .app-header nav a {
112
+ text-decoration: none;
113
+ color: var(--text-color);
114
+ font-weight: 500;
115
+ transition: color 0.3s ease;
116
+ }
117
+
118
+ .app-header nav a:hover {
119
+ color: var(--primary-color);
120
+ }
121
+
122
+ /* Base button styles from root vars */
123
+ .btn-primary {
124
+ background-color: var(--primary-color);
125
+ color: var(--white-color);
126
+ border: none;
127
+ padding: 0.5rem 1rem;
128
+ border-radius: 6px;
129
+ cursor: pointer;
130
+ transition: background-color 0.3s ease;
131
+ }
132
+ .btn-primary:hover {
133
+ background-color: #1e9d4e;
134
+ }
135
+
136
+
137
+ /* Specific Green Text Link (for Login) */
138
+ .link-green {
139
+ color: var(--green-button-bg) !important;
140
+ font-weight: 600;
141
+ transition: color 0.3s ease;
142
+ }
143
+
144
+ .link-green:hover {
145
+ color: var(--green-button-hover-bg) !important;
146
+ }
147
+
148
+ /* Specific Solid Green Button (for Sign Up and Logout in Header) */
149
+ .btn-solid-green {
150
+ padding: 0.5rem 1rem;
151
+ border: none;
152
+ border-radius: 6px;
153
+ cursor: pointer;
154
+ font-size: 1em;
155
+ font-weight: 600;
156
+ text-align: center;
157
+ text-decoration: none;
158
+ display: inline-block;
159
+ box-sizing: border-box;
160
+
161
+ background-color: var(--green-button-bg);
162
+ color: var(--white-color) !important;
163
+ transition: background-color 0.3s ease, color 0.3s ease;
164
+ }
165
+
166
+ .btn-solid-green:hover {
167
+ background-color: var(--green-button-hover-bg);
168
+ color: var(--white-color) !important;
169
+ }
170
+
171
+ .btn-secondary {
172
+ background-color: transparent;
173
+ border: 1px solid var(--border-color);
174
+ color: var(--text-color);
175
+ padding: 0.5rem 1rem;
176
+ border-radius: 6px;
177
+ cursor: pointer;
178
+ transition: background-color 0.3s ease, color 0.3s ease, border-color 0.3s ease;
179
+ }
180
+ .btn-secondary:hover {
181
+ background-color: #f0f0f0;
182
+ border-color: #c0c0c0;
183
+ color: #555;
184
+ }
185
+
186
+
187
+ /* --- Auth Forms --- */
188
+ .auth-container { width: 100%; display: flex; justify-content: center; }
189
+ .auth-form { width: 100%; max-width: 400px; padding: 2rem; }
190
+ .auth-form h2 { text-align: center; margin-bottom: 2rem; }
191
+ .form-group { margin-bottom: 1.5rem; }
192
+ .form-group label { display: block; margin-bottom: 0.5rem; }
193
+ .form-group input {
194
+ width: 100%;
195
+ padding: 0.75rem;
196
+ border: 1px solid var(--border-color);
197
+ border-radius: 6px;
198
+ background-color: #fafafa;
199
+ box-sizing: border-box;
200
+ }
201
+ .forgot-password { text-align: right; margin-bottom: 1rem; font-size: 0.9em;}
202
+ .error-message { color: red; text-align: center; margin-bottom: 1rem; }
203
+ .auth-form .btn-primary { width: 100%; padding: 0.75rem; }
204
+
205
+ /* --- Chat Layout --- */
206
+ .chat-layout {
207
+ display: flex;
208
+ width: 100%;
209
+ /* This is crucial: take remaining height after header */
210
+ height: calc(100% - var(--header-height));
211
+ }
212
+
213
+ /* Sidebar Styling */
214
+ .sidebar {
215
+ width: 260px;
216
+ background-color: #f9fafb;
217
+ padding: 1rem;
218
+ border-right: 1px solid var(--border-color);
219
+ display: flex;
220
+ flex-direction: column;
221
+ justify-content: space-between; /* Pushes content to top and bottom */
222
+ flex-shrink: 0;
223
+ height: 100%; /* Make sidebar take full height of chat-layout */
224
+ box-sizing: border-box;
225
+ }
226
+
227
+ /* sidebar-top now contains only the new chat button */
228
+ .sidebar-top {
229
+ display: flex;
230
+ flex-direction: column;
231
+ }
232
+
233
+ /* New Chat Button Styling */
234
+ .new-chat-btn {
235
+ width: 100%;
236
+ padding: 0.75rem;
237
+ border: none;
238
+ border-radius: 6px;
239
+ background-color: var(--dark-green-text);
240
+ color: var(--white-color);
241
+ cursor: pointer;
242
+ text-align: left;
243
+ font-weight: 500;
244
+ font-size: 1rem;
245
+ line-height: 1.5rem;
246
+ display: flex;
247
+ align-items: center;
248
+ gap: 0.5rem;
249
+ transition: background-color 0.3s ease;
250
+ margin-bottom: 1rem; /* Space below the button */
251
+ }
252
+ .new-chat-btn:hover {
253
+ background-color: var(--green-button-hover-bg);
254
+ }
255
+
256
+ .new-chat-btn .plus-icon {
257
+ font-size: 1.2rem;
258
+ font-weight: bold;
259
+ }
260
+
261
+ /* sidebar-bottom will contain the nav links and be pushed to the bottom */
262
+ .sidebar-bottom {
263
+ display: flex;
264
+ flex-direction: column;
265
+ gap: 0.5rem;
266
+ margin-top: auto; /* This pushes the nav section to the bottom */
267
+ }
268
+
269
+ /* Styling for History and Settings links */
270
+ .sidebar-nav-link {
271
+ display: flex;
272
+ align-items: center;
273
+ gap: 0.5rem;
274
+ padding: 0.75rem;
275
+ text-decoration: none;
276
+ color: var(--dark-green-text);
277
+ border-radius: 6px;
278
+ background: transparent; /* Explicitly remove background */
279
+ border: none;
280
+ width: 100%;
281
+ font-weight: 500;
282
+ font-size: 1rem;
283
+ line-height: 1.5rem;
284
+ transition: background-color 0.3s ease, color 0.3s ease;
285
+ }
286
+ .sidebar-nav-link:hover {
287
+ background-color: transparent; /* Ensure no background on hover */
288
+ color: var(--green-button-hover-bg); /* Darker green on hover for text */
289
+ }
290
+ .sidebar-nav-link.active {
291
+ background-color: transparent; /* Ensure no background when active */
292
+ color: var(--dark-green-text); /* Keep dark green text when active */
293
+ }
294
+
295
+ /* Main chat content area */
296
+ .chat-main {
297
+ flex-grow: 1;
298
+ display: flex;
299
+ flex-direction: column;
300
+ background-color: var(--white-color);
301
+ height: 100%;
302
+ box-sizing: border-box;
303
+ }
304
+
305
+ .chat-messages {
306
+ flex-grow: 1;
307
+ padding: 2rem;
308
+ overflow-y: auto; /* Keep scrollbar ONLY for chat conversation */
309
+ display: flex;
310
+ flex-direction: column;
311
+ gap: 1rem;
312
+ }
313
+
314
+ .message-wrapper {
315
+ display: flex;
316
+ align-items: flex-start;
317
+ max-width: 100%;
318
+ }
319
+ .message-wrapper.user {
320
+ justify-content: flex-end;
321
+ flex-direction: row-reverse;
322
+ }
323
+ .message-wrapper.assistant { justify-content: flex-start; }
324
+
325
+ /* Avatar specific styles */
326
+ .chat-avatar {
327
+ width: 40px;
328
+ height: 40px;
329
+ min-width: 40px;
330
+ border-radius: 50%;
331
+ display: flex;
332
+ justify-content: center;
333
+ align-items: center;
334
+ background-color: #e0e0e0;
335
+ box-shadow: 0 1px 3px rgba(0,0,0,0.1);
336
+ overflow: hidden;
337
+ flex-shrink: 0;
338
+ }
339
+
340
+ .user-icon-placeholder {
341
+ font-size: 24px;
342
+ color: #666;
343
+ }
344
+
345
+ .message-wrapper.assistant .chat-avatar {
346
+ margin-right: 10px;
347
+ }
348
+ .message-wrapper.user .chat-avatar {
349
+ margin-left: 10px;
350
+ }
351
+
352
+ .chat-avatar .avatar-image {
353
+ width: 100%;
354
+ height: 100%;
355
+ object-fit: cover;
356
+ border-radius: 50%;
357
+ }
358
+
359
+ .message-bubble {
360
+ padding: 0.75rem 1rem;
361
+ border-radius: 12px;
362
+ max-width: 70%;
363
+ word-wrap: break-word;
364
+ overflow-wrap: break-word;
365
+ white-space: pre-wrap;
366
+ box-shadow: 0 1px 3px rgba(0,0,0,0.1);
367
+ }
368
+
369
+ .message-wrapper.user .message-bubble {
370
+ background-color: var(--primary-color);
371
+ color: var(--white-color);
372
+ border-bottom-right-radius: 4px;
373
+ }
374
+ .message-wrapper.assistant .message-bubble {
375
+ background-color: #f3f4f6;
376
+ color: var(--text-color);
377
+ border-bottom-left-radius: 4px;
378
+ }
379
+
380
+ /* Blinking cursor for streaming */
381
+ .blinking-cursor {
382
+ display: inline-block;
383
+ width: 0.5em;
384
+ background-color: var(--text-color);
385
+ animation: blink 1s step-end infinite;
386
+ }
387
+
388
+ @keyframes blink {
389
+ from, to { opacity: 1; }
390
+ 50% { opacity: 0; }
391
+ }
392
+
393
+
394
+ .chat-input-area {
395
+ padding: 1rem 2rem;
396
+ border-top: 1px solid var(--border-color);
397
+ background-color: var(--white-color);
398
+ }
399
+ .chat-form { display: flex; gap: 0.5rem; }
400
+ .chat-form input {
401
+ flex-grow: 1;
402
+ padding: 0.75rem;
403
+ border-radius: 6px;
404
+ border: 1px solid var(--border-color);
405
+ background-color: #fafafa;
406
+ box-sizing: border-box;
407
+ outline: none;
408
+ }
409
+ .chat-form button {
410
+ background-color: var(--primary-color);
411
+ color: var(--white-color);
412
+ border: none;
413
+ border-radius: 6px;
414
+ padding: 0.5rem 1rem;
415
+ cursor: pointer;
416
+ transition: background-color 0.3s ease;
417
+ }
418
+ .chat-form button:hover {
419
+ background-color: #1e9d4e;
420
+ }
421
+
422
+ .empty-chat-placeholder {
423
+ text-align: center;
424
+ margin-top: 20%;
425
+ color: #9ca3af;
426
+ flex-grow: 1;
427
+ display: flex;
428
+ flex-direction: column;
429
+ justify-content: center;
430
+ align-items: center;
431
+ }
432
+ .empty-chat-placeholder h1 {
433
+ font-size: 2.5em;
434
+ margin-bottom: 10px;
435
+ color: #333;
436
+ }
437
+ .empty-chat-placeholder p {
438
+ font-size: 1.1em;
439
+ line-height: 1.6;
440
+ }
441
+
442
+ /* --- History Page --- */
443
+ .history-title { padding: 0 2rem; margin-top: 2rem; margin-bottom: 1.5rem; color: var(--text-color); }
444
+ .history-list { list-style: none; padding: 0 2rem; }
445
+ .history-item {
446
+ display: flex;
447
+ align-items: center;
448
+ gap: 1rem;
449
+ padding: 1rem;
450
+ border-bottom: 1px solid var(--border-color);
451
+ cursor: pointer;
452
+ transition: background-color 0.3s ease;
453
+ }
454
+ .history-item:hover {
455
+ background-color: #f9f9f9;
456
+ }
457
+ .history-prompt { font-weight: 500; flex-grow: 1; }
458
+ .history-date { font-size: 0.8rem; color: #6b7280; }
459
+
460
+ /* Responsive adjustments */
461
+ @media (max-width: 768px) {
462
+ .app-header {
463
+ flex-direction: column;
464
+ align-items: flex-start;
465
+ padding: 1rem;
466
+ gap: 10px;
467
+ }
468
+ .app-header nav {
469
+ flex-wrap: wrap;
470
+ justify-content: center;
471
+ width: 100%;
472
+ gap: 10px;
473
+ }
474
+ .chat-layout {
475
+ flex-direction: column;
476
+ /* On mobile, chat-layout height needs to account for header and horizontal sidebar */
477
+ height: calc(100% - var(--header-height) - var(--sidebar-height-mobile, 60px));
478
+ }
479
+ .sidebar {
480
+ width: 100%;
481
+ border-right: none;
482
+ border-bottom: 1px solid var(--border-color);
483
+ padding: 10px;
484
+ flex-direction: row; /* Sidebar items in a row for mobile */
485
+ justify-content: center;
486
+ gap: 10px;
487
+ height: auto; /* Allow sidebar height to be determined by content */
488
+ overflow-y: hidden; /* Hide sidebar scrollbar on mobile if it has one */
489
+ }
490
+ .new-chat-btn {
491
+ width: auto;
492
+ font-size: 0.9em;
493
+ padding: 0.5rem 0.8rem;
494
+ }
495
+ .sidebar-nav {
496
+ margin-top: 0;
497
+ display: flex;
498
+ gap: 10px;
499
+ }
500
+ .sidebar-nav a, .sidebar-link {
501
+ padding: 0.5rem;
502
+ font-size: 0.9em;
503
+ }
504
+ .chat-main {
505
+ height: 100%; /* Take remaining height from parent chat-layout */
506
+ }
507
+ .chat-messages {
508
+ padding: 1rem;
509
+ }
510
+ .message-bubble {
511
+ max-width: 85%;
512
+ }
513
+ .chat-input-area {
514
+ padding: 1rem;
515
+ }
516
+ }
frontend/src/main.jsx ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // frontend/src/main.jsx
2
+ import React from 'react';
3
+ import ReactDOM from 'react-dom/client';
4
+ import App from './App.jsx';
5
+ import './App.css'; // Import Tailwind CSS
6
+
7
+ ReactDOM.createRoot(document.getElementById('root')).render(
8
+ <React.StrictMode>
9
+ <App />
10
+ </React.StrictMode>,
11
+ );
frontend/src/services/firebase.js ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { initializeApp } from "firebase/app";
2
+ import { getAuth } from "firebase/auth";
3
+ import { getFirestore } from "firebase/firestore";
4
+
5
+ // Your web app's Firebase configuration
6
+ const firebaseConfig = {
7
+ apiKey: "AIzaSyCwM9fumgWifh9lf4tA_dINQqpR2vkHVxI",
8
+ authDomain: "custom-chatbot-b7358.firebaseapp.com",
9
+ projectId: "custom-chatbot-b7358",
10
+ storageBucket: "custom-chatbot-b7358.firebasestorage.app",
11
+ messagingSenderId: "313932051840",
12
+ appId: "1:313932051840:web:816fb21654e324451dbd65",
13
+ measurementId: "G-HR3DKV3BNR"
14
+ };
15
+
16
+ // Initialize Firebase
17
+ const app = initializeApp(firebaseConfig);
18
+ const auth = getAuth(app);
19
+ const db = getFirestore(app);
20
+
21
+ export { auth, db };
frontend/src/services/firestore.js ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // src/services/firestore.js (No changes needed, assuming it handles nested Date objects)
2
+
3
+ import { db, auth } from './firebase';
4
+ import { collection, addDoc, query, where, getDocs, orderBy, serverTimestamp } from "firebase/firestore";
5
+
6
+ // Save a new chat session to Firestore
7
+ export const saveChatHistory = async (chatHistory) => {
8
+ if (!auth.currentUser) {
9
+ console.warn("Attempted to save chat history without a logged-in user.");
10
+ return; // Ensure user is logged in
11
+ }
12
+ try {
13
+ const userId = auth.currentUser.uid;
14
+ await addDoc(collection(db, "chats"), {
15
+ userId: userId,
16
+ history: chatHistory, // This array now contains { role, message, timestamp }
17
+ createdAt: serverTimestamp(),
18
+ title: chatHistory[0]?.message || 'New Chat'
19
+ });
20
+ console.log("Chat history saved successfully.");
21
+ } catch (error) {
22
+ console.error("Error saving chat history: ", error);
23
+ }
24
+ };
25
+
26
+ // Get all chat sessions for the current user
27
+ export const getChatHistory = async () => {
28
+ if (!auth.currentUser) return [];
29
+ try {
30
+ const userId = auth.currentUser.uid;
31
+ const q = query(collection(db, "chats"), where("userId", "==", userId), orderBy("createdAt", "desc"));
32
+ const querySnapshot = await getDocs(q);
33
+ const chats = [];
34
+ querySnapshot.forEach((doc) => {
35
+ chats.push({ id: doc.id, ...doc.data() });
36
+ });
37
+ return chats;
38
+ } catch (error) {
39
+ console.error("Error fetching chat history: ", error);
40
+ return [];
41
+ }
42
+ };
frontend/tailwind.config.js ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // tailwind.config.mjs
2
+ import forms from '@tailwindcss/forms'
3
+ import containerQueries from '@tailwindcss/container-queries'
4
+
5
+ /** @type {import('tailwindcss').Config} */
6
+ export default {
7
+ content: [
8
+ "./index.html",
9
+ "./src/**/*.{js,ts,jsx,tsx}",
10
+ ],
11
+ theme: {
12
+ extend: {
13
+ colors: {
14
+ 'primary-green': '#22c55e',
15
+ 'light-cream': '#fff6d7',
16
+ 'white': '#ffffff',
17
+ 'text-dark': '#111712',
18
+ 'text-muted': '#648765',
19
+ 'border-light': '#f0f4f0',
20
+ },
21
+ fontFamily: {
22
+ sans: ['Inter', '"Noto Sans"', 'sans-serif'],
23
+ },
24
+ },
25
+ },
26
+ plugins: [
27
+ forms,
28
+ containerQueries,
29
+ ],
30
+ }
frontend/vite.config.js ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // vite.config.js
2
+ import { defineConfig } from 'vite';
3
+ import react from '@vitejs/plugin-react'; // Make sure you have this import
4
+
5
+ export default defineConfig({
6
+ plugins: [react()],
7
+ optimizeDeps: {
8
+ esbuildOptions: {
9
+ loader: {
10
+ '.js': 'jsx', // This is where the loader mapping should go for dependency pre-bundling
11
+ },
12
+ },
13
+ },
14
+ });
main.py ADDED
@@ -0,0 +1,145 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import logging
3
+ from fastapi import FastAPI, Request, HTTPException, UploadFile, File
4
+ from fastapi.responses import HTMLResponse, StreamingResponse, JSONResponse
5
+ from fastapi.staticfiles import StaticFiles
6
+ from pydantic import BaseModel
7
+ from langchain_community.llms import Ollama
8
+ import asyncio
9
+ import json
10
+ import httpx # For making HTTP requests to Ollama for transcription
11
+
12
+ # Configure logging
13
+ logging.basicConfig(level=logging.INFO)
14
+ logger = logging.getLogger(__name__)
15
+
16
+ # Initialize FastAPI app
17
+ app = FastAPI()
18
+
19
+ # IMPORTANT: Set Ollama host for both langchain and direct httpx calls
20
+ OLLAMA_HOST_URL = "http://127.0.0.1:11434" # Ollama runs locally within the Docker container
21
+ os.environ["OLLAMA_HOST"] = OLLAMA_HOST_URL
22
+
23
+ MODEL_NAME = 'krishna_choudhary/tinyllama:latest' # For your LLM
24
+ WHISPER_MODEL_NAME = 'dimavz/whisper-tiny' # For transcription
25
+
26
+ # Mount static files for the React frontend
27
+ app.mount("/assets", StaticFiles(directory="frontend/dist/assets"), name="assets")
28
+
29
+ @app.get("/", response_class=HTMLResponse)
30
+ async def serve_frontend():
31
+ try:
32
+ with open("frontend/dist/index.html", "r") as f:
33
+ return HTMLResponse(f.read())
34
+ except FileNotFoundError:
35
+ logger.error("frontend/dist/index.html not found. Have you run `npm run build` in your frontend directory?")
36
+ raise HTTPException(status_code=404, detail="Frontend index.html not found. Please ensure React build is complete.")
37
+
38
+ def get_llm():
39
+ return Ollama(model=MODEL_NAME)
40
+
41
+ class Question(BaseModel):
42
+ text: str
43
+
44
+ @app.post("/ask")
45
+ async def ask_question(question: Question):
46
+ try:
47
+ llm = get_llm()
48
+ logger.info(f"Received prompt: {question.text}")
49
+ async def generate_and_stream():
50
+ try:
51
+ async for chunk in llm.astream(question.text):
52
+ for char in chunk:
53
+ yield f"data: {json.dumps({'token': char})}\n\n"
54
+ await asyncio.sleep(0.01)
55
+ yield "data: {\"event\": \"end\"}\n\n"
56
+ except Exception as e:
57
+ logger.error(f"Error during Ollama LLM stream: {e}", exc_info=True)
58
+ yield f"data: {json.dumps({'event': 'error', 'error': str(e)})}\n\n"
59
+ yield "data: {\"event\": \"end\"}\n\n"
60
+ return StreamingResponse(generate_and_stream(), media_type="text/event-stream")
61
+ except Exception as e:
62
+ logger.error(f"Error preparing streaming response for LLM: {e}", exc_info=True)
63
+ raise HTTPException(status_code=500, detail=f"Internal server error: {str(e)}")
64
+
65
+ @app.post("/transcribe-audio")
66
+ async def transcribe_audio(audio_file: UploadFile = File(...)):
67
+ """
68
+ Receives an audio file, sends it to Ollama's Whisper model for transcription,
69
+ and returns the transcribed text.
70
+ """
71
+ if not audio_file.content_type.startswith("audio/"):
72
+ raise HTTPException(status_code=400, detail="Invalid file type. Please upload an audio file.")
73
+
74
+ # Ollama's API for transcription expects a file path or bytes to be sent.
75
+ # We'll save the uploaded file temporarily and then send it.
76
+ temp_audio_path = f"/tmp/{audio_file.filename}" # Use /tmp for temporary files
77
+ try:
78
+ # Save the uploaded file to a temporary location
79
+ with open(temp_audio_path, "wb") as f:
80
+ f.write(await audio_file.read())
81
+
82
+ logger.info(f"Sending audio file {temp_audio_path} to Ollama for transcription.")
83
+
84
+ async with httpx.AsyncClient() as client:
85
+ with open(temp_audio_path, "rb") as f:
86
+ files = {"file": (audio_file.filename, f, audio_file.content_type)}
87
+
88
+ import ollama
89
+ audio_bytes = await audio_file.read()
90
+ import base64
91
+ encoded_audio = base64.b64encode(audio_bytes).decode('utf-8')
92
+
93
+ ollama_transcribe_payload = {
94
+ "model": WHISPER_MODEL_NAME,
95
+ "prompt": "",
96
+ "stream": False,
97
+ "options": {
98
+
99
+ },
100
+ "images": [encoded_audio]
101
+ }
102
+
103
+ ollama_response = await client.post(
104
+ f"{OLLAMA_HOST_URL}/api/generate",
105
+ json=ollama_transcribe_payload,
106
+ timeout=600
107
+ )
108
+ ollama_response.raise_for_status()
109
+
110
+ response_data = ollama_response.json()
111
+ transcription = response_data.get("response", "").strip()
112
+
113
+ if not transcription:
114
+ logger.warning("Ollama Whisper returned empty transcription.")
115
+ raise HTTPException(status_code=500, detail="Failed to get transcription from Whisper model.")
116
+
117
+ return JSONResponse(content={"transcription": transcription})
118
+
119
+ except httpx.RequestError as e:
120
+ logger.error(f"Network error communicating with Ollama: {e}", exc_info=True)
121
+ raise HTTPException(status_code=503, detail=f"Could not connect to Ollama service: {str(e)}")
122
+ except httpx.HTTPStatusError as e:
123
+ logger.error(f"Ollama API returned an error: {e.response.status_code} - {e.response.text}", exc_info=True)
124
+ raise HTTPException(status_code=e.response.status_code, detail=f"Ollama API error: {e.response.text}")
125
+ except Exception as e:
126
+ logger.error(f"Error during audio transcription: {e}", exc_info=True)
127
+ raise HTTPException(status_code=500, detail=f"Transcription failed: {str(e)}")
128
+ finally:
129
+ # remove the audio after processing
130
+ if os.path.exists(temp_audio_path):
131
+ os.remove(temp_audio_path)
132
+
133
+ @app.on_event("startup")
134
+ async def startup_event():
135
+ logger.info(f"Starting up with LLM model: {MODEL_NAME} and Whisper model: {WHISPER_MODEL_NAME}")
136
+ client = ollama.AsyncClient(host=OLLAMA_HOST_URL)
137
+ try:
138
+ await client.list() # Check if Ollama is responsive
139
+ logger.info("Ollama server is accessible.")
140
+ except Exception as e:
141
+ logger.error(f"Ollama server not accessible at startup: {e}")
142
+
143
+ @app.on_event("shutdown")
144
+ async def shutdown_event():
145
+ logger.info("Shutting down FastAPI application.")
nginx.conf ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ worker_processes auto;
2
+ pid /var/lib/nginx/nginx.pid;
3
+
4
+ events {
5
+ worker_connections 1024;
6
+ }
7
+
8
+ http {
9
+ include mime.types;
10
+ default_type application/octet-stream;
11
+
12
+ sendfile on;
13
+ keepalive_timeout 65;
14
+
15
+ # Define the FastAPI upstream server
16
+ upstream fastapi_backend {
17
+ server 127.0.0.1:7860;
18
+ }
19
+
20
+ server {
21
+ listen 8501;
22
+
23
+ root /home/user/app/frontend/dist;
24
+
25
+ index index.html index.htm;
26
+
27
+ # Serve static files for the React app
28
+ location / {
29
+ try_files $uri $uri/ /index.html;
30
+ }
31
+
32
+ # Proxy API requests to the FastAPI backend
33
+ location /api/ {
34
+ proxy_pass http://fastapi_backend/;
35
+ proxy_set_header Host $host;
36
+ proxy_set_header X-Real-IP $remote_addr;
37
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
38
+ proxy_set_header X-Forwarded-Proto $scheme;
39
+
40
+ }
41
+ }
42
+ }
requirements.txt ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ fastapi
2
+ uvicorn
3
+ langchain
4
+ langchain_community
5
+ ollama
6
+ requests
7
+ python-multipart
start.sh ADDED
@@ -0,0 +1,106 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+
3
+ # ==============================================================================
4
+ # Start Script for Ollama, FastAPI Backend, and Nginx Frontend
5
+ # This script initializes the environment, starts dependent services,
6
+ # and then launches the main application components.
7
+ # ==============================================================================
8
+
9
+ # ------------------------------------------------------------------------------
10
+ # 1. Environment Setup
11
+ # Set essential environment variables for performance and GPU usage.
12
+ # ------------------------------------------------------------------------------
13
+ echo "--- Initializing Environment Variables ---"
14
+ export OMP_NUM_THREADS=4
15
+ export MKL_NUM_THREADS=4
16
+ export CUDA_VISIBLE_DEVICES=0 # Specifies which GPU to use, 0 for the first GPU
17
+
18
+ # ------------------------------------------------------------------------------
19
+ # 2. Start Ollama Server and Pull Models
20
+ # Ollama must be running and the required models available before FastAPI starts.
21
+ # ------------------------------------------------------------------------------
22
+ echo "--- Starting Ollama Server ---"
23
+ # Start Ollama in the background
24
+ # The 'serve' command runs the Ollama API server.
25
+ ollama serve &
26
+
27
+ # Define the models to be pulled
28
+ MODEL_TO_PULL="krishna_choudhary/tinyllama:latest"
29
+ WHISPER_MODEL_TO_PULL="dimavz/whisper-tiny"
30
+
31
+ # Pull the LLM model if it's not already present
32
+ echo "Checking for Ollama LLM model: $MODEL_TO_PULL"
33
+ if ! ollama list | grep -q "$MODEL_TO_PULL"; then
34
+ echo "Pulling Ollama LLM model: $MODEL_TO_PULL (This may take some time)..."
35
+ ollama pull "$MODEL_TO_PULL"
36
+ else
37
+ echo "Ollama LLM model $MODEL_TO_PULL already present."
38
+ fi
39
+
40
+ # Pull the Whisper model for transcription if it's not already present
41
+ echo "Checking for Ollama Whisper model: $WHISPER_MODEL_TO_PULL"
42
+ if ! ollama list | grep -q "$WHISPER_MODEL_TO_PULL"; then
43
+ echo "Pulling Ollama Whisper model: $WHISPER_MODEL_TO_PULL (This may take some time)..."
44
+ ollama pull "$WHISPER_MODEL_TO_PULL"
45
+ else
46
+ echo "Ollama Whisper model $WHISPER_MODEL_TO_PULL already present."
47
+ fi
48
+
49
+ # Wait for Ollama to become responsive
50
+ max_attempts=90 # Maximum attempts (90 seconds)
51
+ attempt=0
52
+ echo "Waiting for Ollama API to be ready (max $max_attempts seconds)..."
53
+ while ! curl -s http://localhost:11434/api/tags >/dev/null; do
54
+ sleep 1
55
+ attempt=$((attempt + 1))
56
+ if [ $attempt -eq $max_attempts ]; then
57
+ echo "Error: Ollama failed to start within $((max_attempts)) seconds. Exiting."
58
+ exit 1
59
+ fi
60
+ done
61
+ echo "Ollama is ready and responsive."
62
+
63
+ # ------------------------------------------------------------------------------
64
+ # 3. Debugging: List Files
65
+ # Useful for verifying that application files are correctly copied into the container.
66
+ # ------------------------------------------------------------------------------
67
+ echo "--- Files in current directory ($PWD): ---"
68
+ ls -le
69
+ echo "-------------------------------------------"
70
+
71
+ # ------------------------------------------------------------------------------
72
+ # 4. Start FastAPI Backend Server
73
+ # The FastAPI application serves the API endpoints.
74
+ # ------------------------------------------------------------------------------
75
+ echo "--- Starting FastAPI Server ---"
76
+ # Run Uvicorn to serve the FastAPI application.
77
+ # --host 0.0.0.0 makes it accessible from outside the container (via exposed port).
78
+ # --port 7860 is where FastAPI listens.
79
+ # --workers 1 and --limit-concurrency 20 are performance settings.
80
+ uvicorn main:app --host 0.0.0.0 --port 7860 --workers 1 --limit-concurrency 20 &
81
+ FASTAPI_PID=$!
82
+ echo "FastAPI server started with PID: $FASTAPI_PID"
83
+
84
+ # Give FastAPI a moment to fully initialize (optional but good practice)
85
+ sleep 5
86
+
87
+ # ------------------------------------------------------------------------------
88
+ # 5. Start Nginx Web Server
89
+ # Nginx acts as a reverse proxy for the FastAPI backend and serves the React frontend.
90
+ # ------------------------------------------------------------------------------
91
+ echo "--- Starting Nginx Web Server on Port 8501 ---"
92
+ # Start Nginx in the foreground so the script waits for it to exit,
93
+ # keeping the Docker container alive.
94
+ # 'daemon off;' ensures Nginx runs in the foreground.
95
+ nginx -g 'daemon off;' &
96
+ NGINX_PID=$!
97
+ echo "Nginx started with PID: $NGINX_PID"
98
+
99
+ # ------------------------------------------------------------------------------
100
+ # 6. Keep Container Alive
101
+ # The 'wait' command will keep the script running as long as Nginx is running.
102
+ # ------------------------------------------------------------------------------
103
+ echo "All services initiated. Keeping container alive by waiting for Nginx..."
104
+ wait $NGINX_PID
105
+
106
+ echo "Nginx stopped. Container may exit now."