Hemang Thakur commited on
Commit
d5c104e
·
1 Parent(s): 5541a18
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .dockerignore +12 -0
  2. .slack_credentials +2 -0
  3. .webhook_secret +1 -0
  4. Dockerfile +84 -0
  5. README.md +1 -11
  6. frontend/package-lock.json +0 -0
  7. frontend/package.json +49 -0
  8. frontend/public/auth-receiver.html +122 -0
  9. frontend/public/favicon.ico +0 -0
  10. frontend/public/index.html +43 -0
  11. frontend/public/logo192.png +0 -0
  12. frontend/public/logo512.png +0 -0
  13. frontend/public/manifest.json +25 -0
  14. frontend/public/robots.txt +3 -0
  15. frontend/src/App.css +40 -0
  16. frontend/src/App.js +95 -0
  17. frontend/src/App.test.js +8 -0
  18. frontend/src/Components/AiComponents/ChatComponents/Evaluate.css +113 -0
  19. frontend/src/Components/AiComponents/ChatComponents/Evaluate.js +142 -0
  20. frontend/src/Components/AiComponents/ChatComponents/Graph.css +85 -0
  21. frontend/src/Components/AiComponents/ChatComponents/Graph.js +73 -0
  22. frontend/src/Components/AiComponents/ChatComponents/SourcePopup.css +77 -0
  23. frontend/src/Components/AiComponents/ChatComponents/SourcePopup.js +187 -0
  24. frontend/src/Components/AiComponents/ChatComponents/SourceRef.css +21 -0
  25. frontend/src/Components/AiComponents/ChatComponents/Sources.css +70 -0
  26. frontend/src/Components/AiComponents/ChatComponents/Sources.js +124 -0
  27. frontend/src/Components/AiComponents/ChatComponents/Streaming.css +732 -0
  28. frontend/src/Components/AiComponents/ChatComponents/Streaming.js +536 -0
  29. frontend/src/Components/AiComponents/ChatWindow.css +277 -0
  30. frontend/src/Components/AiComponents/ChatWindow.js +368 -0
  31. frontend/src/Components/AiComponents/Dropdowns/AddContentDropdown.css +150 -0
  32. frontend/src/Components/AiComponents/Dropdowns/AddContentDropdown.js +359 -0
  33. frontend/src/Components/AiComponents/Dropdowns/AddFilesDialog.css +191 -0
  34. frontend/src/Components/AiComponents/Dropdowns/AddFilesDialog.js +282 -0
  35. frontend/src/Components/AiComponents/Notifications/Notification.css +379 -0
  36. frontend/src/Components/AiComponents/Notifications/Notification.js +242 -0
  37. frontend/src/Components/AiComponents/Notifications/useNotification.js +43 -0
  38. frontend/src/Components/AiComponents/Sidebars/LeftSideBar.js +38 -0
  39. frontend/src/Components/AiComponents/Sidebars/LeftSidebar.css +59 -0
  40. frontend/src/Components/AiComponents/Sidebars/RightSidebar.css +138 -0
  41. frontend/src/Components/AiComponents/Sidebars/RightSidebar.js +142 -0
  42. frontend/src/Components/AiPage.css +434 -0
  43. frontend/src/Components/AiPage.js +1253 -0
  44. frontend/src/Components/IntialSetting.css +174 -0
  45. frontend/src/Components/IntialSetting.js +316 -0
  46. frontend/src/Components/settings-gear-1.svg +47 -0
  47. frontend/src/Icons/bot.png +0 -0
  48. frontend/src/Icons/copy.png +0 -0
  49. frontend/src/Icons/evaluate.png +0 -0
  50. frontend/src/Icons/excerpts.png +0 -0
.dockerignore ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .env
2
+ .env_copy
3
+ .gitignore
4
+ .deepeval
5
+ .deepeval_telemetry.txt
6
+ frontend/node_modules/
7
+ frontend/build/
8
+ venv/
9
+ .files/
10
+ __pycache__/
11
+ LICENSE.md
12
+ README.md
.slack_credentials ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ 9146298917605.9224293005140
2
+ 6a902b3d5070b9511742f8a67c5ec6f8
.webhook_secret ADDED
@@ -0,0 +1 @@
 
 
1
+ https://eoujtjv9jy7d28z.m.pipedream.net
Dockerfile ADDED
@@ -0,0 +1,84 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ----------------------------
2
+ # Stage 1: Build the React Frontend
3
+ # ----------------------------
4
+ FROM node:20-alpine AS builder
5
+ RUN apk add --no-cache libc6-compat
6
+ WORKDIR /app
7
+
8
+ # Create writable directories for Hugging Face Spaces
9
+ RUN mkdir -p /tmp/huggingface && \
10
+ chmod -R 777 /tmp/huggingface && \
11
+ mkdir -p /app/workspace && \
12
+ chmod -R 777 /app/workspace
13
+
14
+ # Set cache environment variables at build time
15
+ ENV HF_HOME=/tmp/huggingface \
16
+ TRANSFORMERS_CACHE=/tmp/huggingface \
17
+ XDG_CACHE_HOME=/tmp \
18
+ WRITABLE_DIR=/app/workspace
19
+
20
+ # Copy the 'frontend' folder from the project root into the container
21
+ COPY frontend ./frontend
22
+
23
+ # Switch to the frontend directory
24
+ WORKDIR /app/frontend
25
+
26
+ # Install dependencies (using yarn, npm, or pnpm)
27
+ RUN if [ -f yarn.lock ]; then yarn --frozen-lockfile; \
28
+ elif [ -f package-lock.json ]; then npm ci; \
29
+ elif [ -f pnpm-lock.yaml ]; then yarn global add pnpm && pnpm i --frozen-lockfile; \
30
+ else echo "No lockfile found. Exiting." && exit 1; \
31
+ fi
32
+
33
+ # Build the React app (produces a production-ready build in the "build" folder)
34
+ RUN npm run build
35
+
36
+ # ----------------------------
37
+ # Stage 2: Set Up the FastAPI Backend and Serve the React App
38
+ # ----------------------------
39
+ FROM python:3.12-slim AS backend
40
+ WORKDIR /app
41
+
42
+ # Install OS-level dependencies
43
+ RUN apt-get update --fix-missing && \
44
+ apt-get install --no-install-recommends -y git curl && \
45
+ apt-get clean && rm -rf /var/lib/apt/lists/*
46
+
47
+ # Install Node.js (if needed for any backend tasks)
48
+ RUN curl -fsSL https://deb.nodesource.com/setup_20.x | bash - && \
49
+ apt-get update --fix-missing && \
50
+ apt-get install --no-install-recommends -y nodejs && \
51
+ apt-get clean && rm -rf /var/lib/apt/lists/*
52
+
53
+ # Copy requirements.txt and install Python dependencies
54
+ COPY requirements.txt .
55
+ RUN pip install --no-cache-dir -r requirements.txt
56
+
57
+ # Install additional dependencies for torch and spaCy
58
+ RUN pip install --no-cache-dir torch==2.5.1 torchvision==0.20.1 torchaudio==2.5.1 --index-url https://download.pytorch.org/whl/cu124
59
+ RUN python -m spacy download en_core_web_sm
60
+
61
+ # Disable telemetry for deepeval
62
+ ENV DEEPEVAL_TELEMETRY_OPT_OUT=YES
63
+
64
+ # Copy the rest of your backend code and resources
65
+ COPY . .
66
+
67
+ # Copy the built React app from the builder stage into the same folder structure as used in your FastAPI code
68
+ COPY --from=builder /app/frontend/build ./frontend/build
69
+
70
+ # Run HuggingFace Spaces as a root user
71
+ # Re-create writable directories in this stage
72
+ RUN mkdir -p /tmp/huggingface /app/workspace && \
73
+ chmod -R 777 /tmp/huggingface /app/workspace && \
74
+ useradd -m spaces-user && \
75
+ chown -R spaces-user:spaces-user /tmp/huggingface /app/workspace
76
+
77
+ # Set the user to spaces-user for running the application
78
+ USER spaces-user
79
+
80
+ # Expose the port
81
+ EXPOSE ${PORT:-7860}
82
+
83
+ # Start the FastAPI backend using Uvicorn, reading the PORT env variable
84
+ CMD ["sh", "-c", "uvicorn main:app --host 0.0.0.0 --port ${PORT:-7860}"]
README.md CHANGED
@@ -1,11 +1 @@
1
- ---
2
- title: Cohex
3
- emoji: 🐢
4
- colorFrom: red
5
- colorTo: purple
6
- sdk: docker
7
- pinned: false
8
- license: unknown
9
- ---
10
-
11
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
1
+ AI Search Project
 
 
 
 
 
 
 
 
 
 
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,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "hemang",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "dependencies": {
6
+ "@emotion/react": "^11.14.0",
7
+ "@emotion/styled": "^11.14.0",
8
+ "@fortawesome/fontawesome-free": "^6.7.2",
9
+ "@google/generative-ai": "^0.21.0",
10
+ "@mui/icons-material": "^6.4.4",
11
+ "@mui/material": "^6.4.3",
12
+ "@mui/styled-engine-sc": "^6.4.2",
13
+ "cra-template": "1.2.0",
14
+ "katex": "^0.16.22",
15
+ "markdown-to-jsx": "^7.7.10",
16
+ "react": "^19.0.0",
17
+ "react-dom": "^19.0.0",
18
+ "react-icons": "^5.4.0",
19
+ "react-markdown": "^10.1.0",
20
+ "react-router-dom": "^7.1.3",
21
+ "react-scripts": "5.0.1",
22
+ "react-syntax-highlighter": "^15.5.0",
23
+ "web-vitals": "^4.2.4"
24
+ },
25
+ "scripts": {
26
+ "start": "react-scripts start",
27
+ "build": "react-scripts build",
28
+ "test": "react-scripts test",
29
+ "eject": "react-scripts eject"
30
+ },
31
+ "eslintConfig": {
32
+ "extends": [
33
+ "react-app",
34
+ "react-app/jest"
35
+ ]
36
+ },
37
+ "browserslist": {
38
+ "production": [
39
+ ">0.2%",
40
+ "not dead",
41
+ "not op_mini all"
42
+ ],
43
+ "development": [
44
+ "last 1 chrome version",
45
+ "last 1 firefox version",
46
+ "last 1 safari version"
47
+ ]
48
+ }
49
+ }
frontend/public/auth-receiver.html ADDED
@@ -0,0 +1,122 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>Completing Authentication...</title>
5
+ <style>
6
+ body {
7
+ font-family: Arial, sans-serif;
8
+ display: flex;
9
+ justify-content: center;
10
+ align-items: center;
11
+ height: 100vh;
12
+ margin: 0;
13
+ background-color: #f5f5f5;
14
+ }
15
+ .container {
16
+ text-align: center;
17
+ padding: 20px;
18
+ background: white;
19
+ border-radius: 8px;
20
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
21
+ }
22
+ .spinner {
23
+ border: 3px solid #f3f3f3;
24
+ border-top: 3px solid #3498db;
25
+ border-radius: 50%;
26
+ width: 40px;
27
+ height: 40px;
28
+ animation: spin 1s linear infinite;
29
+ margin: 20px auto;
30
+ }
31
+ @keyframes spin {
32
+ 0% { transform: rotate(0deg); }
33
+ 100% { transform: rotate(360deg); }
34
+ }
35
+ </style>
36
+ </head>
37
+ <body>
38
+ <div class="container">
39
+ <h2>Completing authentication...</h2>
40
+ <div class="spinner"></div>
41
+ <p id="status">Please wait...</p>
42
+ </div>
43
+
44
+ <script>
45
+ function updateStatus(message) {
46
+ document.getElementById('status').textContent = message;
47
+ }
48
+
49
+ try {
50
+ // Extract token from URL
51
+ let token = null;
52
+ let authData = { type: 'auth-success' };
53
+
54
+ // For Google and Microsoft (token in hash)
55
+ if (window.location.hash) {
56
+ const hashParams = new URLSearchParams(window.location.hash.substring(1));
57
+ token = hashParams.get('access_token');
58
+ }
59
+
60
+ // For Slack (code in query params)
61
+ if (!token && window.location.search) {
62
+ const queryParams = new URLSearchParams(window.location.search);
63
+
64
+ // Check if this is a Slack OAuth response (has code and state=slack)
65
+ const code = queryParams.get('code');
66
+ const state = queryParams.get('state');
67
+
68
+ if (code && state === 'slack') {
69
+ // This is a Slack OAuth response
70
+ token = code; // Use the code as the token for now
71
+ authData.code = code;
72
+ authData.provider = 'slack';
73
+
74
+ // Extract team information if available
75
+ const team = queryParams.get('team');
76
+ const teamId = queryParams.get('team_id');
77
+ const teamName = queryParams.get('team_name');
78
+
79
+ if (team || teamId) {
80
+ authData.team_id = teamId || team;
81
+ authData.team_name = teamName || team;
82
+ }
83
+ } else if (code && !token) {
84
+ // Legacy Slack handling without state parameter
85
+ updateStatus('Slack authentication detected. Using authorization code.');
86
+ token = code;
87
+ authData.code = code;
88
+ authData.provider = 'slack';
89
+ }
90
+ }
91
+
92
+ if (token) {
93
+ updateStatus('Authentication successful! Closing window...');
94
+
95
+ // Send token back to parent window
96
+ if (window.opener) {
97
+ window.opener.postMessage({
98
+ ...authData,
99
+ token: token
100
+ }, window.location.origin);
101
+
102
+ // Close window after a short delay
103
+ setTimeout(() => window.close(), 1000);
104
+ } else {
105
+ updateStatus('Unable to communicate with main window. Please close this window manually.');
106
+ }
107
+ } else {
108
+ updateStatus('Authentication failed');
109
+ setTimeout(() => {
110
+ if (window.opener) {
111
+ window.opener.postMessage({ type: 'auth-failed', error: 'No token received' }, '*');
112
+ window.close();
113
+ }
114
+ }, 3000);
115
+ }
116
+ } catch (error) {
117
+ updateStatus('An error occurred: ' + error.message);
118
+ console.error('Auth error:', error);
119
+ }
120
+ </script>
121
+ </body>
122
+ </html>
frontend/public/favicon.ico ADDED
frontend/public/index.html ADDED
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
7
+ <meta name="theme-color" content="#000000" />
8
+ <meta
9
+ name="description"
10
+ content="Web site created using create-react-app"
11
+ />
12
+ <link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
13
+ <!--
14
+ manifest.json provides metadata used when your web app is installed on a
15
+ user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
16
+ -->
17
+ <link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
18
+ <!--
19
+ Notice the use of %PUBLIC_URL% in the tags above.
20
+ It will be replaced with the URL of the `public` folder during the build.
21
+ Only files inside the `public` folder can be referenced from the HTML.
22
+
23
+ Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
24
+ work correctly both with client-side routing and a non-root public URL.
25
+ Learn how to configure a non-root public URL by running `npm run build`.
26
+ -->
27
+ <title>React App</title>
28
+ </head>
29
+ <body>
30
+ <noscript>You need to enable JavaScript to run this app.</noscript>
31
+ <div id="root"></div>
32
+ <!--
33
+ This HTML file is a template.
34
+ If you open it directly in the browser, you will see an empty page.
35
+
36
+ You can add webfonts, meta tags, or analytics to this file.
37
+ The build step will place the bundled scripts into the <body> tag.
38
+
39
+ To begin the development, run `npm start` or `yarn start`.
40
+ To create a production bundle, use `npm run build` or `yarn build`.
41
+ -->
42
+ </body>
43
+ </html>
frontend/public/logo192.png ADDED
frontend/public/logo512.png ADDED
frontend/public/manifest.json ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "short_name": "React App",
3
+ "name": "Create React App Sample",
4
+ "icons": [
5
+ {
6
+ "src": "favicon.ico",
7
+ "sizes": "64x64 32x32 24x24 16x16",
8
+ "type": "image/x-icon"
9
+ },
10
+ {
11
+ "src": "logo192.png",
12
+ "type": "image/png",
13
+ "sizes": "192x192"
14
+ },
15
+ {
16
+ "src": "logo512.png",
17
+ "type": "image/png",
18
+ "sizes": "512x512"
19
+ }
20
+ ],
21
+ "start_url": ".",
22
+ "display": "standalone",
23
+ "theme_color": "#000000",
24
+ "background_color": "#ffffff"
25
+ }
frontend/public/robots.txt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ # https://www.robotstxt.org/robotstxt.html
2
+ User-agent: *
3
+ Disallow:
frontend/src/App.css ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .App {
2
+ text-align: center;
3
+ }
4
+
5
+ .App-logo {
6
+ height: 6vmin;
7
+ pointer-events: auto;
8
+ }
9
+
10
+ @media (prefers-reduced-motion: no-preference) {
11
+ .App-logo {
12
+ animation: App-logo-spin infinite 18s linear;
13
+ }
14
+ }
15
+
16
+ .App-header {
17
+ background: #190e10; /* Deep, dark maroon base */
18
+ color: #F5E6E8; /* Soft off-white for contrast */
19
+
20
+ min-height: 100vh;
21
+ display: flex;
22
+ flex-direction: column;
23
+ align-items: center;
24
+ justify-content: center;
25
+ font-size: calc(5px + 2vmin);
26
+
27
+ }
28
+
29
+ .App-link {
30
+ color: #61dafb;
31
+ }
32
+
33
+ @keyframes App-logo-spin {
34
+ from {
35
+ transform: rotate(0deg);
36
+ }
37
+ to {
38
+ transform: rotate(360deg);
39
+ }
40
+ }
frontend/src/App.js ADDED
@@ -0,0 +1,95 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState } from 'react';
2
+ import { BrowserRouter, Routes, Route } from 'react-router-dom';
3
+ import CircularProgress from '@mui/material/CircularProgress';
4
+ import Snackbar from '@mui/material/Snackbar';
5
+ import Alert from '@mui/material/Alert';
6
+ import logo from './Icons/settings-2.svg';
7
+ import './App.css';
8
+ import IntialSetting from './Components/IntialSetting.js';
9
+ import AiPage from './Components/AiPage.js';
10
+
11
+ function App() {
12
+ return (
13
+ <BrowserRouter>
14
+ <Routes>
15
+ <Route path='/' element={<Home />} />
16
+ <Route path='/AiPage' element={<AiPage />} />
17
+ </Routes>
18
+ </BrowserRouter>
19
+ );
20
+ }
21
+
22
+ function Home() {
23
+ const [showSettings, setShowSettings] = useState(false);
24
+ const [initializing, setInitializing] = useState(false);
25
+ // Snackbar state
26
+ const [snackbar, setSnackbar] = useState({
27
+ open: false,
28
+ message: "",
29
+ severity: "success",
30
+ });
31
+
32
+ const handleInitializationStart = () => {
33
+ setInitializing(true);
34
+ };
35
+
36
+ // Function to open the snackbar
37
+ const openSnackbar = (message, severity = "success") => {
38
+ setSnackbar({ open: true, message, severity });
39
+ };
40
+
41
+ // Function to close the snackbar
42
+ const closeSnackbar = (event, reason) => {
43
+ if (reason === 'clickaway') return;
44
+ setSnackbar(prev => ({ ...prev, open: false }));
45
+ };
46
+
47
+ return (
48
+ <div className="App">
49
+ <header className="App-header">
50
+ {initializing ? (
51
+ <>
52
+ <CircularProgress style={{ margin: '20px' }} />
53
+ <p>Initializing the app. This may take a few minutes...</p>
54
+ </>
55
+ ) : (
56
+ <>
57
+ <img
58
+ src={logo}
59
+ className="App-logo"
60
+ alt="logo"
61
+ onClick={() => setShowSettings(true)}
62
+ style={{ cursor: 'pointer' }}
63
+ />
64
+ <p>Enter the settings to proceed</p>
65
+ </>
66
+ )}
67
+
68
+ {/* InitialSetting */}
69
+ {showSettings && (
70
+ <IntialSetting
71
+ trigger={showSettings}
72
+ setTrigger={setShowSettings}
73
+ onInitializationStart={handleInitializationStart}
74
+ openSnackbar={openSnackbar}
75
+ closeSnackbar={closeSnackbar}
76
+ />
77
+ )}
78
+ </header>
79
+
80
+ {/* Render the Snackbar*/}
81
+ <Snackbar
82
+ open={snackbar.open}
83
+ autoHideDuration={snackbar.severity === 'success' ? 3000 : null}
84
+ onClose={closeSnackbar}
85
+ anchorOrigin={{ vertical: 'top', horizontal: 'center' }}
86
+ >
87
+ <Alert onClose={closeSnackbar} severity={snackbar.severity} variant="filled" sx={{ width: '100%' }}>
88
+ {snackbar.message}
89
+ </Alert>
90
+ </Snackbar>
91
+ </div>
92
+ );
93
+ }
94
+
95
+ export default App;
frontend/src/App.test.js ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ import { render, screen } from '@testing-library/react';
2
+ import App from './App';
3
+
4
+ test('renders learn react link', () => {
5
+ render(<App />);
6
+ const linkElement = screen.getByText(/learn react/i);
7
+ expect(linkElement).toBeInTheDocument();
8
+ });
frontend/src/Components/AiComponents/ChatComponents/Evaluate.css ADDED
@@ -0,0 +1,113 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* Container for the Evaluate component */
2
+ .evaluate-container {
3
+ display: flex;
4
+ flex-direction: column;
5
+ gap: 16px;
6
+ padding: 16px;
7
+ }
8
+
9
+ /* Form Control */
10
+ .evaluate-form-control {
11
+ width: 100%;
12
+ }
13
+
14
+ /* Input label */
15
+ .evaluate-form-control .MuiInputLabel-root {
16
+ color: #26a8dc !important;
17
+ }
18
+ .evaluate-form-control .MuiInputLabel-root.Mui-focused {
19
+ color: #26a8dc !important;
20
+ }
21
+
22
+ /* Dropdown arrow icon */
23
+ .evaluate-form-control .MuiSelect-icon {
24
+ color: #26a8dc !important;
25
+ }
26
+
27
+ /* Select’s OutlinedInput */
28
+ .evaluate-outlined-input {
29
+ background-color: transparent !important;
30
+ color: #26a8dc !important;
31
+ }
32
+
33
+ /* Override the default notched outline to have a #ddd border */
34
+ .evaluate-outlined-input .MuiOutlinedInput-notchedOutline {
35
+ border-color: #26a8dc !important;
36
+ }
37
+
38
+ /* Container for the rendered chips */
39
+ .chip-container {
40
+ display: flex !important;
41
+ flex-wrap: wrap !important;
42
+ gap: 0.65rem !important;
43
+ }
44
+
45
+ /* Chips */
46
+ .evaluate-chip {
47
+ background-color: #b70303 !important;
48
+ color: #fff !important;
49
+ border-radius: 0.5rem !important;
50
+ }
51
+
52
+ /* Remove background from chip close button and make its icon #ddd */
53
+ .evaluate-chip .MuiChip-deleteIcon {
54
+ background: none !important;
55
+ color: #ddd !important;
56
+ }
57
+
58
+ /* Styling for the dropdown menu */
59
+ .evaluate-menu {
60
+ background-color: #2b2b2b !important;
61
+ border: 0.01rem solid #26a8dc !important;
62
+ color: #ddd !important;
63
+ }
64
+
65
+ /* Dropdown menu item hover effect: lighter shade */
66
+ .evaluate-menu .MuiMenuItem-root:hover {
67
+ background-color: #3b3b3b !important;
68
+ }
69
+
70
+ /* Dropdown menu item selected effect */
71
+ .evaluate-menu .MuiMenuItem-root.Mui-selected {
72
+ background-color: #4b4b4b !important;
73
+ }
74
+
75
+ /* Evaluate button styling */
76
+ .evaluate-button {
77
+ background-color: #FFC300 !important;
78
+ color: #2b2b2b !important;
79
+ width: auto !important;
80
+ padding: 6px 16px !important;
81
+ align-self: flex-start !important;
82
+ }
83
+
84
+ .evaluate-button:hover {
85
+ background-color: #b07508 !important;
86
+ color: #ddd !important;
87
+ }
88
+
89
+ /* No metrics message */
90
+ .no-metrics-message {
91
+ text-align: center;
92
+ color: red;
93
+ }
94
+
95
+ /* Spinner styling */
96
+ .custom-spinner {
97
+ width: 1.35rem;
98
+ height: 1.35rem;
99
+ border: 3px solid #3b7bdc; /* Main Spinner */
100
+ border-top: 3px solid #434343; /* Rotating path */
101
+ border-radius: 50%;
102
+ animation: spin 0.9s linear infinite;
103
+ }
104
+
105
+ /* Spinner animation */
106
+ @keyframes spin {
107
+ 0% {
108
+ transform: rotate(0deg);
109
+ }
110
+ 100% {
111
+ transform: rotate(360deg);
112
+ }
113
+ }
frontend/src/Components/AiComponents/ChatComponents/Evaluate.js ADDED
@@ -0,0 +1,142 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useRef } from 'react';
2
+ import { FaTimes, FaCheck, FaSpinner } from 'react-icons/fa';
3
+ import { BsChevronLeft } from 'react-icons/bs';
4
+ import CircularProgress from '@mui/material/CircularProgress';
5
+ import Sources from './Sources';
6
+ import Evaluate from './Evaluate'
7
+ import '../Sidebars/RightSidebar.css';
8
+
9
+ function RightSidebar({
10
+ isOpen,
11
+ rightSidebarWidth,
12
+ setRightSidebarWidth,
13
+ toggleRightSidebar,
14
+ sidebarContent,
15
+ tasks = [],
16
+ tasksLoading,
17
+ sources = [],
18
+ sourcesLoading,
19
+ onTaskClick,
20
+ onSourceClick,
21
+ evaluation
22
+ }) {
23
+ const minWidth = 200;
24
+ const maxWidth = 450;
25
+ const sidebarRef = useRef(null);
26
+
27
+ // Called when the user starts resizing the sidebar.
28
+ const startResize = (e) => {
29
+ e.preventDefault();
30
+ sidebarRef.current.classList.add("resizing"); // Add the "resizing" class to the sidebar when resizing
31
+ document.addEventListener("mousemove", resizeSidebar);
32
+ document.addEventListener("mouseup", stopResize);
33
+ };
34
+
35
+ const resizeSidebar = (e) => {
36
+ let newWidth = window.innerWidth - e.clientX;
37
+ if (newWidth < minWidth) newWidth = minWidth;
38
+ if (newWidth > maxWidth) newWidth = maxWidth;
39
+ setRightSidebarWidth(newWidth);
40
+ };
41
+
42
+ const stopResize = () => {
43
+ sidebarRef.current.classList.remove("resizing"); // Remove the "resizing" class from the sidebar when resizing stops
44
+ document.removeEventListener("mousemove", resizeSidebar);
45
+ document.removeEventListener("mouseup", stopResize);
46
+ };
47
+
48
+ // Default handler for source clicks: open the link in a new tab.
49
+ const handleSourceClick = (source) => {
50
+ if (source && source.link) {
51
+ window.open(source.link, '_blank');
52
+ }
53
+ };
54
+
55
+ // Helper function to return the proper icon based on task status.
56
+ const getTaskIcon = (task) => {
57
+ // If the task is a simple string, default to the completed icon.
58
+ if (typeof task === 'string') {
59
+ return <FaCheck />;
60
+ }
61
+ // Use the status field to determine which icon to render.
62
+ switch (task.status) {
63
+ case 'RUNNING':
64
+ // FaSpinner is used for running tasks. The CSS class "spin" can be defined to add animation.
65
+ return <FaSpinner className="spin"/>;
66
+ case 'DONE':
67
+ return <FaCheck className="checkmark" />;
68
+ case 'FAILED':
69
+ return <FaTimes className="x" />;
70
+ default:
71
+ return <FaCheck />;
72
+ }
73
+ };
74
+
75
+ return (
76
+ <>
77
+ <nav
78
+ ref={sidebarRef}
79
+ className={`right-side-bar ${isOpen ? "open" : "closed"}`}
80
+ style={{ width: isOpen ? rightSidebarWidth : 0 }}
81
+ >
82
+ <div className="sidebar-header">
83
+ <h3>
84
+ {sidebarContent === "sources"
85
+ ? "Sources"
86
+ : sidebarContent === "evaluate"
87
+ ? "Evaluation"
88
+ : "Tasks"}
89
+ </h3>
90
+ <button className="close-btn" onClick={toggleRightSidebar}>
91
+ <FaTimes />
92
+ </button>
93
+ </div>
94
+ <div className="sidebar-content">
95
+ {sidebarContent === "sources" ? ( // If the sidebar content is "sources", show the sources component
96
+ sourcesLoading ? (
97
+ <div className="tasks-loading">
98
+ <CircularProgress size={20} sx={{ color: '#ccc' }} />
99
+ <span className="loading-tasks-text">Generating sources...</span>
100
+ </div>
101
+ ) : (
102
+ <Sources sources={sources} handleSourceClick={onSourceClick || handleSourceClick} />
103
+ )
104
+ )
105
+ // If the sidebar content is "evaluate", show the evaluation component
106
+ : sidebarContent === "evaluate" ? (
107
+ <Evaluate evaluation={evaluation} />
108
+ ) : (
109
+ // Otherwise, show tasks
110
+ tasksLoading ? (
111
+ <div className="tasks-loading">
112
+ <CircularProgress size={20} sx={{ color: '#ccc' }} />
113
+ <span className="loading-tasks-text">Generating tasks...</span>
114
+ </div>
115
+ ) : (
116
+ <ul className="nav-links" style={{ listStyle: 'none', padding: 0 }}>
117
+ {tasks.map((task, index) => (
118
+ <li key={index} className="task-item">
119
+ <span className="task-icon">
120
+ {getTaskIcon(task)}
121
+ </span>
122
+ <span className="task-text">
123
+ {typeof task === 'string' ? task : task.task}
124
+ </span>
125
+ </li>
126
+ ))}
127
+ </ul>
128
+ )
129
+ )}
130
+ </div>
131
+ <div className="resizer" onMouseDown={startResize}></div>
132
+ </nav>
133
+ {!isOpen && (
134
+ <button className="toggle-btn right-toggle" onClick={toggleRightSidebar}>
135
+ <BsChevronLeft />
136
+ </button>
137
+ )}
138
+ </>
139
+ );
140
+ }
141
+
142
+ export default RightSidebar;
frontend/src/Components/AiComponents/ChatComponents/Graph.css ADDED
@@ -0,0 +1,85 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* Fullscreen overlay */
2
+ .graph-dialog-container {
3
+ position: fixed !important;
4
+ top: 0 !important;
5
+ left: 0 !important;
6
+ width: 100% !important;
7
+ height: 100vh !important;
8
+ background-color: rgba(0, 0, 0, 0.2) !important;
9
+ display: flex !important;
10
+ justify-content: center !important;
11
+ align-items: center !important;
12
+ z-index: 1000 !important;
13
+ overflow: hidden !important;
14
+ }
15
+
16
+ /* Inner dialog container */
17
+ .graph-dialog-inner {
18
+ position: relative !important;
19
+ border-radius: 12px !important;
20
+ padding: 1rem !important;
21
+ width: 45% !important;
22
+ max-width: 100% !important;
23
+ background-color: #1e1e1e !important;
24
+ min-height: 80vh !important;
25
+ overflow: hidden !important; /* Prevent scrolling */
26
+ }
27
+
28
+ /* Header styling */
29
+ .graph-dialog-header {
30
+ display: flex !important;
31
+ justify-content: space-between !important;
32
+ align-items: center !important;
33
+ padding: 16px !important;
34
+ background-color: #1e1e1e !important;
35
+ color: #fff !important;
36
+ }
37
+
38
+ /* Title styling */
39
+ .graph-dialog-title {
40
+ font-weight: bold !important;
41
+ font-size: 1.5rem !important;
42
+ margin: 0 !important;
43
+ }
44
+
45
+ /* Close button styling */
46
+ .graph-dialog .close-btn {
47
+ position: absolute !important;
48
+ top: 16px !important;
49
+ right: 16px !important;
50
+ background: none !important;
51
+ color: white !important;
52
+ padding: 7px !important;
53
+ border-radius: 5px !important;
54
+ cursor: pointer !important;
55
+ }
56
+ .graph-dialog-close-btn:hover {
57
+ background: rgba(255, 255, 255, 0.1) !important;
58
+ color: white !important;
59
+ }
60
+
61
+ /* Content area */
62
+ .graph-dialog-content {
63
+ padding: 0 !important;
64
+ background-color: #1e1e1e !important;
65
+ height: 550px !important;
66
+ overflow: hidden !important;
67
+ }
68
+
69
+ /* Loading state */
70
+ .graph-loading {
71
+ display: flex !important;
72
+ justify-content: center !important;
73
+ align-items: center !important;
74
+ height: 50% !important;
75
+ color: #fff !important;
76
+ }
77
+
78
+ /* Error message */
79
+ .graph-error {
80
+ display: flex !important;
81
+ justify-content: center !important;
82
+ align-items: center !important;
83
+ height: 50% !important;
84
+ color: red !important;
85
+ }
frontend/src/Components/AiComponents/ChatComponents/Graph.js ADDED
@@ -0,0 +1,73 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState, useEffect } from 'react';
2
+ import { FaTimes } from 'react-icons/fa';
3
+ import './Graph.css';
4
+
5
+ export default function Graph({ open, onClose, payload, onError }) {
6
+ const [graphHtml, setGraphHtml] = useState("");
7
+ const [loading, setLoading] = useState(true);
8
+ const [error, setError] = useState("");
9
+
10
+ useEffect(() => {
11
+ // if (open && payload) {
12
+ if (open) {
13
+ setLoading(true);
14
+ setError("");
15
+ fetch("/action/graph", {
16
+ method: "POST",
17
+ headers: { "Content-Type": "application/json" },
18
+ body: JSON.stringify()
19
+ // body: JSON.stringify(payload)
20
+ })
21
+ .then(res => res.json())
22
+ .then(data => {
23
+ // If the API returns an error, throw it to be caught below.
24
+ if (data.error) {
25
+ throw new Error(data.error);
26
+ }
27
+ setGraphHtml(data.result);
28
+ setLoading(false);
29
+ })
30
+ .catch(err => {
31
+ console.error("Error fetching graph:", err);
32
+ const errMsg = err.message || "Error fetching graph.";
33
+ setError(errMsg);
34
+ setLoading(false);
35
+ // Propagate error to parent using the onError callback.
36
+ if (onError && typeof onError === 'function') {
37
+ onError(errMsg);
38
+ }
39
+ });
40
+ }
41
+ }, [open, onError]);
42
+ // }, [open, payload, onError]);
43
+
44
+ if (!open) return null;
45
+
46
+ return (
47
+ <div className="graph-dialog-container" onClick={onClose}>
48
+ <div className="graph-dialog-inner" onClick={e => e.stopPropagation()}>
49
+ <div className="graph-dialog-header">
50
+ <h3 className="graph-dialog-title">Graph Display</h3>
51
+ <button className="graph-dialog close-btn" onClick={onClose}>
52
+ <FaTimes />
53
+ </button>
54
+ </div>
55
+ <div className="graph-dialog-content">
56
+ {loading ? (
57
+ <div className="graph-loading">
58
+ <p>Loading Graph...</p>
59
+ </div>
60
+ ) : error ? (
61
+ <p className="graph-error">{error}</p>
62
+ ) : (
63
+ <iframe
64
+ title="Graph Display"
65
+ srcDoc={graphHtml}
66
+ style={{ border: "none", width: "100%", height: "625px" }}
67
+ />
68
+ )}
69
+ </div>
70
+ </div>
71
+ </div>
72
+ );
73
+ }
frontend/src/Components/AiComponents/ChatComponents/SourcePopup.css ADDED
@@ -0,0 +1,77 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .source-popup {
2
+ position: absolute; /* Crucial for positioning */
3
+ /* transform is set inline based on calculation */
4
+ transition: opacity 0.2s ease-in-out, transform 0.2s ease-in-out;
5
+ opacity: 1; /* Start visible, manage via state */
6
+ pointer-events: auto; /* Allow interaction */
7
+ width: 300px; /* Or max-width */
8
+ max-width: 90vw;
9
+ }
10
+
11
+ .source-popup-card {
12
+ background-color: #333 !important; /* Dark background */
13
+ color: #eee !important; /* Light text */
14
+ border: 1px solid #555 !important;
15
+ border-radius: 8px !important;
16
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3) !important;
17
+ padding: 0.5rem !important; /* Reduced padding */
18
+ }
19
+
20
+ .source-popup-card .MuiCardContent-root {
21
+ padding: 8px !important; /* Further reduce padding inside content */
22
+ padding-bottom: 8px !important; /* Ensure bottom padding is also reduced */
23
+ }
24
+
25
+
26
+ .source-popup-title {
27
+ font-size: 0.9rem !important; /* Slightly smaller title */
28
+ font-weight: 600 !important;
29
+ margin-bottom: 0.3rem !important;
30
+ line-height: 1.3 !important;
31
+ color: #eee !important; /* Ensure title color */
32
+ }
33
+
34
+ .source-popup-title a {
35
+ color: inherit !important; /* Inherit color for link */
36
+ text-decoration: none !important;
37
+ }
38
+ .source-popup-title a:hover {
39
+ text-decoration: underline !important;
40
+ }
41
+
42
+
43
+ .source-popup-link-info {
44
+ display: flex !important;
45
+ align-items: center !important;
46
+ font-size: 0.75rem !important; /* Smaller domain text */
47
+ color: #bbb !important;
48
+ margin-bottom: 0.4rem !important; /* Space below link info */
49
+ }
50
+
51
+ .source-popup-icon {
52
+ width: 14px !important; /* Smaller icon */
53
+ height: 14px !important;
54
+ margin-right: 0.3rem !important;
55
+ vertical-align: middle; /* Align icon better */
56
+ filter: brightness(1.1); /* Slightly brighter icon */
57
+ }
58
+
59
+ .source-popup-domain {
60
+ vertical-align: middle !important;
61
+ white-space: nowrap;
62
+ overflow: hidden;
63
+ text-overflow: ellipsis;
64
+ }
65
+
66
+ .source-popup-description {
67
+ font-size: 0.8rem !important; /* Smaller description text */
68
+ color: #ccc !important;
69
+ line-height: 1.4 !important;
70
+ /* Limit the number of lines shown */
71
+ display: -webkit-box;
72
+ -webkit-line-clamp: 3; /* Show max 3 lines */
73
+ -webkit-box-orient: vertical;
74
+ overflow: hidden;
75
+ text-overflow: ellipsis;
76
+ margin-top: 0.4rem !important; /* Space above description */
77
+ }
frontend/src/Components/AiComponents/ChatComponents/SourcePopup.js ADDED
@@ -0,0 +1,187 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react';
2
+ import Card from '@mui/material/Card';
3
+ import CardContent from '@mui/material/CardContent';
4
+ import Typography from '@mui/material/Typography';
5
+ import Link from '@mui/material/Link';
6
+ import './SourcePopup.css';
7
+
8
+ // Helper function to extract a friendly domain name from a URL.
9
+ const getDomainName = (url) => {
10
+ try {
11
+ if (!url) return 'Unknown Source';
12
+ const hostname = new URL(url).hostname;
13
+ const domain = hostname.startsWith('www.') ? hostname.slice(4) : hostname;
14
+ const parts = domain.split('.');
15
+ return parts[0].charAt(0).toUpperCase() + parts[0].slice(1);
16
+ } catch (err) {
17
+ console.error("Error parsing URL for domain name:", url, err);
18
+ return 'Invalid URL';
19
+ }
20
+ };
21
+
22
+ // Helper function for Levenshtein distance calculation
23
+ function levenshtein(a, b) {
24
+ if (a.length === 0) return b.length;
25
+ if (b.length === 0) return a.length;
26
+ const matrix = [];
27
+ for (let i = 0; i <= b.length; i++) matrix[i] = [i];
28
+ for (let j = 0; j <= a.length; j++) matrix[0][j] = j;
29
+ for (let i = 1; i <= b.length; i++) {
30
+ for (let j = 1; j <= a.length; j++) {
31
+ if (b.charAt(i - 1) === a.charAt(j - 1)) {
32
+ matrix[i][j] = matrix[i - 1][j - 1];
33
+ } else {
34
+ matrix[i][j] = Math.min(
35
+ matrix[i - 1][j - 1] + 1,
36
+ matrix[i][j - 1] + 1,
37
+ matrix[i - 1][j] + 1
38
+ );
39
+ }
40
+ }
41
+ }
42
+ return matrix[b.length][a.length];
43
+ }
44
+
45
+ // SourcePopup component to display source information and excerpts
46
+ function SourcePopup({
47
+ sourceData,
48
+ excerptsData,
49
+ position,
50
+ onMouseEnter,
51
+ onMouseLeave,
52
+ statementText
53
+ }) {
54
+ if (!sourceData || !position) return null;
55
+
56
+ const domain = getDomainName(sourceData.link);
57
+ let hostname = '';
58
+ try {
59
+ hostname = sourceData.link ? new URL(sourceData.link).hostname : '';
60
+ } catch (err) {
61
+ hostname = sourceData.link || ''; // Fallback to link if URL parsing fails
62
+ }
63
+
64
+ let displayExcerpt = null;
65
+ const sourceIdStr = String(sourceData.id);
66
+
67
+ // Find the relevant excerpt
68
+ if (excerptsData && Array.isArray(excerptsData) && statementText) {
69
+ let foundExcerpt = null;
70
+ let foundByFuzzy = false;
71
+ const norm = s => s.replace(/\s+/g, ' ').trim();
72
+ const lower = s => norm(s).toLowerCase();
73
+ const statementNorm = norm(statementText);
74
+ const statementLower = lower(statementText);
75
+ console.log(`[SourcePopup] Searching for excerpt for source ID ${sourceIdStr}: ${statementText}`);
76
+
77
+ // Iterate through the list of statement-to-excerpt mappings
78
+ for (const entry of excerptsData) {
79
+ const [thisStatement, sourcesMap] = Object.entries(entry)[0];
80
+ const thisNorm = norm(thisStatement);
81
+ const thisLower = lower(thisStatement);
82
+ console.log(`[SourcePopup] Checking against statement: ${thisStatement}`);
83
+
84
+ // Normalized exact match
85
+ if (thisNorm === statementNorm && sourcesMap && sourceIdStr in sourcesMap) {
86
+ foundExcerpt = sourcesMap[sourceIdStr];
87
+ break;
88
+ }
89
+ // Case-insensitive match
90
+ if (thisLower === statementLower && sourcesMap && sourceIdStr in sourcesMap) {
91
+ foundExcerpt = sourcesMap[sourceIdStr];
92
+ break;
93
+ }
94
+ // Substring containment
95
+ if (
96
+ (statementNorm && thisNorm && statementNorm.includes(thisNorm)) ||
97
+ (thisNorm && statementNorm && thisNorm.includes(statementNorm))
98
+ ) {
99
+ if (sourcesMap && sourceIdStr in sourcesMap) {
100
+ foundExcerpt = sourcesMap[sourceIdStr];
101
+ foundByFuzzy = true;
102
+ break;
103
+ }
104
+ }
105
+ // Levenshtein distance
106
+ if (
107
+ levenshtein(statementNorm, thisNorm) <= 5 &&
108
+ sourcesMap && sourceIdStr in sourcesMap
109
+ ) {
110
+ foundExcerpt = sourcesMap[sourceIdStr];
111
+ foundByFuzzy = true;
112
+ break;
113
+ }
114
+ }
115
+
116
+ // Set displayExcerpt based on what was found
117
+ if (foundExcerpt && foundExcerpt.toLowerCase() !== 'excerpt not found') {
118
+ if (foundByFuzzy) {
119
+ // Fuzzy match found an excerpt
120
+ console.log("[SourcePopup] Fuzzy match found an excerpt:", foundExcerpt);
121
+ } else {
122
+ // Exact match found an excerpt
123
+ console.log("[SourcePopup] Exact match found an excerpt:", foundExcerpt);
124
+ }
125
+ // Exact match found an excerpt
126
+ displayExcerpt = foundExcerpt;
127
+ } else if (foundExcerpt) {
128
+ // Handle case where LLM explicitly said "Excerpt not found"
129
+ displayExcerpt = "Relevant excerpt could not be automatically extracted.";
130
+ console.log("[SourcePopup] Excerpt marked as not found or invalid type:", foundExcerpt);
131
+ } else {
132
+ // Excerpt for this specific source ID wasn't found in the loaded data
133
+ displayExcerpt = "Excerpt not found for this citation.";
134
+ console.log(`[SourcePopup] Excerpt not found for source ID ${sourceIdStr}: ${statementText}`);
135
+ }
136
+ }
137
+
138
+ return (
139
+ <div
140
+ className="source-popup"
141
+ style={{
142
+ position: 'absolute', // Use absolute positioning
143
+ top: `${position.top}px`,
144
+ left: `${position.left}px`,
145
+ transform: 'translate(-50%, -100%)', // Center above the reference
146
+ zIndex: 1100, // Ensure it's above other content
147
+ }}
148
+ onMouseEnter={onMouseEnter} // Keep popup open when mouse enters it
149
+ onMouseLeave={onMouseLeave} // Hide popup when mouse leaves it
150
+ >
151
+ <Card variant="outlined" className="source-popup-card">
152
+ <CardContent>
153
+ <Typography variant="subtitle2" component="div" className="source-popup-title" gutterBottom>
154
+ <Link href={sourceData.link} target="_blank" rel="noopener noreferrer" underline="hover" color="inherit">
155
+ {sourceData.title || 'Untitled Source'}
156
+ </Link>
157
+ </Typography>
158
+ <Typography variant="body2" className="source-popup-link-info">
159
+ {hostname && (
160
+ <img
161
+ src={`https://www.google.com/s2/favicons?domain=${hostname}&sz=16`}
162
+ alt=""
163
+ className="source-popup-icon"
164
+ />
165
+ )}
166
+ <span className="source-popup-domain">{domain}</span>
167
+ </Typography>
168
+ {displayExcerpt !== null && (
169
+ <Typography variant="caption" className="source-popup-excerpt" display="block" sx={{ mt: 1 }}>
170
+ <Link
171
+ href={`${sourceData.link}#:~:text=${encodeURIComponent(displayExcerpt)}`}
172
+ target="_blank"
173
+ rel="noopener noreferrer"
174
+ underline="none"
175
+ color="inherit"
176
+ >
177
+ {displayExcerpt}
178
+ </Link>
179
+ </Typography>
180
+ )}
181
+ </CardContent>
182
+ </Card>
183
+ </div>
184
+ );
185
+ };
186
+
187
+ export default SourcePopup;
frontend/src/Components/AiComponents/ChatComponents/SourceRef.css ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .source-reference {
2
+ display: inline-block;
3
+ vertical-align: super;
4
+ font-size: 0.75em;
5
+ line-height: 1;
6
+ margin: 0 0.15em;
7
+ padding: 0.2em 0.3em;
8
+ background-color: rgba(135, 131, 120, 0.265);
9
+ color: #a9a9a9;
10
+ border-radius: 0.35em;
11
+ cursor: pointer;
12
+ transition: background-color 0.2s ease, color 0.2s ease;
13
+ font-weight: 540;
14
+ position: relative;
15
+ top: 0.35em;
16
+ }
17
+
18
+ .source-reference:hover {
19
+ background-color: rgba(135, 131, 120, 0.463);
20
+ color: #ffffff;
21
+ }
frontend/src/Components/AiComponents/ChatComponents/Sources.css ADDED
@@ -0,0 +1,70 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* Container for the sources list */
2
+ .sources-container {
3
+ display: flex !important;
4
+ flex-direction: column !important;
5
+ gap: 0.65rem !important;
6
+ padding: 0 !important;
7
+ }
8
+
9
+ /* Styling for the sources loading text */
10
+ .loading-sources {
11
+ padding: 1rem !important;
12
+ }
13
+
14
+ /* Styling for each Card component */
15
+ .source-card {
16
+ background-color: #3e3e3eec !important;
17
+ border-radius: 1rem !important;
18
+ color: #fff !important;
19
+ cursor: pointer !important;
20
+ }
21
+ .source-card:hover {
22
+ background-color: #262626e5 !important;
23
+ }
24
+
25
+ /* Styling for the CardContent title (header) - reduced text size slightly */
26
+ .source-title {
27
+ color: #fff !important;
28
+ margin-top: -0.5rem !important;
29
+ margin-bottom: 0.4rem !important;
30
+ font-size: 1rem !important;
31
+ }
32
+
33
+ /* Styling for the link row (icon, domain, bullet, serial number) */
34
+ .source-link {
35
+ display: flex !important;
36
+ align-items: center !important;
37
+ font-size: 0.8rem !important;
38
+ color: #c1c1c1 !important;
39
+ margin-bottom: 0.5rem !important;
40
+ }
41
+
42
+ /* Styling for the favicon icon - reduced size and increased brightness */
43
+ .source-icon {
44
+ width: 0.88rem !important;
45
+ height: 0.88rem !important;
46
+ margin-right: 0.3rem !important;
47
+ filter: brightness(1.2) !important; /* Makes the icon brighter */
48
+ }
49
+
50
+ /* Styling for the domain text */
51
+ .source-domain {
52
+ vertical-align: middle !important;
53
+ }
54
+
55
+ /* Styling for the separator bullet */
56
+ .separator {
57
+ margin: 0 0.3rem !important;
58
+ }
59
+
60
+ /* Styling for the serial number */
61
+ .source-serial {
62
+ font-size: 0.8rem !important;
63
+ color: #c1c1c1 !important;
64
+ }
65
+
66
+ /* Styling for the CardContent description */
67
+ .source-description {
68
+ color: #e8e8e8 !important;
69
+ margin-bottom: -0.65rem !important;
70
+ }
frontend/src/Components/AiComponents/ChatComponents/Sources.js ADDED
@@ -0,0 +1,124 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from 'react';
2
+ import { useState, useEffect, useCallback } from 'react';
3
+ import Box from '@mui/material/Box';
4
+ import Card from '@mui/material/Card';
5
+ import CardContent from '@mui/material/CardContent';
6
+ import Typography from '@mui/material/Typography';
7
+ import './Sources.css';
8
+
9
+ // Helper function to extract a friendly domain name from a URL.
10
+ const getDomainName = (url) => {
11
+ try {
12
+ const hostname = new URL(url).hostname;
13
+ // Remove "www." if present.
14
+ const domain = hostname.startsWith('www.') ? hostname.slice(4) : hostname;
15
+ // Return the first part in title case.
16
+ const parts = domain.split('.');
17
+ return parts[0].charAt(0).toUpperCase() + parts[0].slice(1);
18
+ } catch (err) {
19
+ return url;
20
+ }
21
+ };
22
+
23
+ export default function Sources({ sources, handleSourceClick }) {
24
+ // "sources" prop is the payload passed from the parent.
25
+ const [fetchedSources, setFetchedSources] = useState([]);
26
+ const [loading, setLoading] = useState(true);
27
+ const [error, setError] = useState(null);
28
+
29
+ const fetchSources = useCallback(async () => {
30
+ setLoading(true);
31
+ setError(null);
32
+ const startTime = Date.now(); // record start time
33
+ try {
34
+ // Use sources.payload if it exists.
35
+ const bodyData = sources && sources.payload ? sources.payload : sources;
36
+ const res = await fetch("/action/sources", {
37
+ method: "POST",
38
+ headers: { "Content-Type": "application/json" },
39
+ body: JSON.stringify(bodyData)
40
+ });
41
+ const data = await res.json();
42
+ // Backend returns {"result": [...]}
43
+ setFetchedSources(data.result);
44
+ } catch (err) {
45
+ console.error("Error fetching sources:", err);
46
+ setError("Error fetching sources.");
47
+ }
48
+ const elapsed = Date.now() - startTime;
49
+ // Ensure that the loading state lasts at least 1 second.
50
+ if (elapsed < 500) {
51
+ setTimeout(() => {
52
+ setLoading(false);
53
+ }, 500 - elapsed);
54
+ } else {
55
+ setLoading(false);
56
+ }
57
+ }, [sources]);
58
+
59
+ useEffect(() => {
60
+ if (sources) {
61
+ fetchSources();
62
+ }
63
+ }, [sources, fetchSources]);
64
+
65
+ if (loading) {
66
+ return (
67
+ <Box className="sources-container">
68
+ <Typography className="loading-sources" variant="body2">Loading Sources...</Typography>
69
+ </Box>
70
+ );
71
+ }
72
+
73
+ if (error) {
74
+ return (
75
+ <Box className="sources-container">
76
+ <Typography variant="body2" color="error">{error}</Typography>
77
+ </Box>
78
+ );
79
+ }
80
+
81
+ return (
82
+ <Box className="sources-container">
83
+ {fetchedSources.map((source, index) => {
84
+ const domain = getDomainName(source.link);
85
+ let hostname = '';
86
+ try {
87
+ hostname = new URL(source.link).hostname;
88
+ } catch (err) {
89
+ hostname = source.link;
90
+ }
91
+ return (
92
+ <Card
93
+ key={index}
94
+ variant="outlined"
95
+ className="source-card"
96
+ onClick={() => handleSourceClick(source)}
97
+ >
98
+ <CardContent>
99
+ {/* Header/Title */}
100
+ <Typography variant="h6" component="div" className="source-title">
101
+ {source.title}
102
+ </Typography>
103
+ {/* Link info: icon, domain, bullet, serial number */}
104
+ <Typography variant="body2" className="source-link">
105
+ <img
106
+ src={`https://www.google.com/s2/favicons?domain=${hostname}`}
107
+ alt={domain}
108
+ className="source-icon"
109
+ />
110
+ <span className="source-domain">{domain}</span>
111
+ <span className="separator"> • </span>
112
+ <span className="source-serial">{index + 1}</span>
113
+ </Typography>
114
+ {/* Description */}
115
+ <Typography variant="body2" className="source-description">
116
+ {source.description}
117
+ </Typography>
118
+ </CardContent>
119
+ </Card>
120
+ );
121
+ })}
122
+ </Box>
123
+ );
124
+ }
frontend/src/Components/AiComponents/ChatComponents/Streaming.css ADDED
@@ -0,0 +1,732 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* Tables and Pre blocks spacing */
2
+ .streaming-content pre + p,
3
+ .streaming-content pre + ul,
4
+ .streaming-content pre + ol,
5
+ .streaming-content pre + blockquote,
6
+ .streaming-content pre + h1,
7
+ .streaming-content pre + h2,
8
+ .streaming-content pre + h3,
9
+ .streaming-content pre + h4,
10
+ .streaming-content pre + h5,
11
+ .streaming-content pre + h6,
12
+ .table-container + p,
13
+ .table-container + ul,
14
+ .table-container + ol,
15
+ .table-container + blockquote,
16
+ .table-container + h1,
17
+ .table-container + h2,
18
+ .table-container + h3,
19
+ .table-container + h4,
20
+ .table-container + h5,
21
+ .table-container + h6 {
22
+ margin-top: 1rem !important;
23
+ }
24
+
25
+ /* Streaming cursor effect */
26
+ @keyframes blink {
27
+ 0%, 50% { opacity: 1; }
28
+ 51%, 100% { opacity: 0; }
29
+ }
30
+
31
+ .streaming-content .streaming-cursor {
32
+ animation: blink 1s infinite;
33
+ font-weight: normal;
34
+ }
35
+
36
+ /* Responsive adjustments */
37
+ @media (max-width: 768px) {
38
+ .streaming-content {
39
+ font-size: 0.95rem;
40
+ }
41
+
42
+ .code-block-container {
43
+ margin: 0.75rem 0.5rem !important;
44
+ border-radius: 0 !important;
45
+ }
46
+
47
+ .streaming-content code {
48
+ font-size: 0.8em;
49
+ }
50
+
51
+ .table-container {
52
+ margin: 0.75rem -0.5rem;
53
+ border-radius: 0;
54
+ border-left: none;
55
+ border-right: none;
56
+ }
57
+ }
58
+
59
+ /* Base streaming content container */
60
+ .streaming-content {
61
+ font-family: inherit;
62
+ line-height: 2rem;
63
+ white-space: pre-wrap;
64
+ word-wrap: break-word;
65
+ margin: 0;
66
+ padding: 0;
67
+ }
68
+
69
+ /* Reset margin/padding for all descendants */
70
+ .streaming-content * {
71
+ margin: 0;
72
+ padding: 0;
73
+ }
74
+
75
+ /* Paragraphs */
76
+ .streaming-content p {
77
+ margin-top: 0.5rem;
78
+ margin-bottom: 0.5rem;
79
+ }
80
+
81
+ /* First paragraph should have no top margin */
82
+ .streaming-content p:first-child {
83
+ margin-top: 0 !important;
84
+ }
85
+
86
+ /* Consecutive paragraphs */
87
+ .streaming-content p + p {
88
+ margin-top: 0.25rem !important;
89
+ }
90
+
91
+ /* Headings */
92
+ .streaming-content h1,
93
+ .streaming-content h2,
94
+ .streaming-content h3,
95
+ .streaming-content h4,
96
+ .streaming-content h5,
97
+ .streaming-content h6 {
98
+ margin-top: 1rem !important;
99
+ margin-bottom: 0.75rem !important;
100
+ font-weight: bold;
101
+ }
102
+
103
+ /* First heading should have no top margin */
104
+ .streaming-content h1:first-child,
105
+ .streaming-content h2:first-child,
106
+ .streaming-content h3:first-child,
107
+ .streaming-content h4:first-child,
108
+ .streaming-content h5:first-child,
109
+ .streaming-content h6:first-child {
110
+ margin-top: 0 !important;
111
+ }
112
+
113
+ /* Heading sizes */
114
+ .streaming-content h1 { font-size: 2em; }
115
+ .streaming-content h2 { font-size: 1.5em; }
116
+ .streaming-content h3 { font-size: 1.17em; }
117
+ .streaming-content h4 { font-size: 1em; }
118
+ .streaming-content h5 { font-size: 0.83em; }
119
+ .streaming-content h6 { font-size: 0.67em; }
120
+
121
+ /* Lists */
122
+ .streaming-content ul,
123
+ .streaming-content ol {
124
+ margin-top: 0.5rem !important;
125
+ margin-bottom: 0.5rem !important;
126
+ padding-left: 1.5rem !important;
127
+ white-space: normal !important;
128
+ }
129
+
130
+ /* Unordered list styling */
131
+ .streaming-content ul {
132
+ list-style-type: disc;
133
+ }
134
+
135
+ /* Nested unordered lists */
136
+ .streaming-content ul ul {
137
+ list-style-type: circle;
138
+ }
139
+
140
+ .streaming-content ul ul ul {
141
+ list-style-type: square;
142
+ }
143
+
144
+ /* Ordered list styling */
145
+ .streaming-content ol {
146
+ list-style-type: decimal;
147
+ }
148
+
149
+ /* Nested ordered lists */
150
+ .streaming-content ol ol {
151
+ list-style-type: lower-alpha;
152
+ }
153
+
154
+ .streaming-content ol ol ol {
155
+ list-style-type: lower-roman;
156
+ }
157
+
158
+ /* Lists after paragraphs need less top margin */
159
+ .streaming-content p + ul,
160
+ .streaming-content p + ol {
161
+ margin-top: -0.25rem !important;
162
+ }
163
+
164
+ /* Lists after headings */
165
+ .streaming-content h1 + ul,
166
+ .streaming-content h2 + ul,
167
+ .streaming-content h3 + ul,
168
+ .streaming-content h4 + ul,
169
+ .streaming-content h5 + ul,
170
+ .streaming-content h6 + ul,
171
+ .streaming-content h1 + ol,
172
+ .streaming-content h2 + ol,
173
+ .streaming-content h3 + ol,
174
+ .streaming-content h4 + ol,
175
+ .streaming-content h5 + ol,
176
+ .streaming-content h6 + ol {
177
+ margin-top: -0.25rem !important;
178
+ }
179
+
180
+ /* List items */
181
+ .streaming-content li {
182
+ margin-bottom: 0.6rem !important;
183
+ line-height: 2rem;
184
+ }
185
+
186
+ /* Last item in a list needs less margin */
187
+ .streaming-content li:last-child {
188
+ margin-bottom: 0.3rem !important;
189
+ }
190
+
191
+ /* Nested lists */
192
+ .streaming-content li ul,
193
+ .streaming-content li ol {
194
+ margin-top: 0.5rem !important;
195
+ margin-bottom: 0.5rem !important;
196
+ }
197
+
198
+ /* Nested list items */
199
+ .streaming-content li ul li,
200
+ .streaming-content li ol li {
201
+ margin-bottom: 0.4rem !important;
202
+ line-height: 2rem;
203
+ }
204
+
205
+ /* Deep nested list items */
206
+ .streaming-content li ul li ul li,
207
+ .streaming-content li ol li ol li,
208
+ .streaming-content li ul li ol li,
209
+ .streaming-content li ol li ul li {
210
+ margin-bottom: 0.3rem !important;
211
+ line-height: 2rem;
212
+ }
213
+
214
+ /* Code Blocks */
215
+ .code-block-container {
216
+ margin: 1rem 0 0 0 !important;
217
+ border-radius: 8px !important;
218
+ background-color: #0d1117 !important;
219
+ position: relative;
220
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
221
+ border: 1px solid rgba(255, 255, 255, 0.08);
222
+ }
223
+
224
+ .code-block-header {
225
+ background-color: #161b22;
226
+ color: #e6edf3;
227
+ padding: 0.5rem 1rem;
228
+ font-size: 0.85rem;
229
+ font-weight: 500;
230
+ display: flex;
231
+ justify-content: space-between;
232
+ align-items: center;
233
+ border-bottom: 1px solid rgba(48, 54, 61, 0.3);
234
+ }
235
+
236
+ /* Copy button */
237
+ .code-copy-button {
238
+ background-color: transparent;
239
+ color: inherit;
240
+ border: none;
241
+ padding: 0;
242
+ cursor: pointer;
243
+ transition: all 0.2s ease;
244
+ display: flex;
245
+ align-items: center;
246
+ gap: 0.25rem;
247
+ font-family: inherit;
248
+ font-size: 1.05rem;
249
+ position: relative;
250
+ }
251
+
252
+ .code-copy-button:hover {
253
+ opacity: 1;
254
+ }
255
+
256
+ .code-copy-button:hover svg {
257
+ filter: brightness(0.65);
258
+ }
259
+
260
+ .code-copy-button.copied {
261
+ color: inherit;
262
+ font-size: 1.05rem;
263
+ }
264
+
265
+ .code-copy-button.copied:hover {
266
+ opacity: 1;
267
+ }
268
+
269
+ /* Code block content */
270
+ .code-block-container pre {
271
+ margin: 0 !important;
272
+ padding: 0 !important;
273
+ overflow-x: auto;
274
+ }
275
+
276
+ /* The actual syntax highlighter container */
277
+ .code-block-container > div:last-child {
278
+ background-color: #0d1117 !important;
279
+ max-height: 65rem !important;
280
+ max-width: 45rem !important;
281
+ overflow: auto !important;
282
+ }
283
+
284
+ /* Plain pre blocks (without syntax highlighting) */
285
+ .streaming-content pre {
286
+ background-color: #0d1117;
287
+ color: #e6edf3;
288
+ padding: 0;
289
+ border-radius: 0.75rem;
290
+ overflow-x: auto;
291
+ margin-top: 1rem;
292
+ margin-bottom: -1.25rem;
293
+ font-family: 'inherit';
294
+ font-size: 1.17rem;
295
+ line-height: 2rem;
296
+ border: 0.3rem solid #30363d4d;
297
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
298
+ }
299
+
300
+ /* Pre blocks inside code blocks */
301
+ .streaming-content pre code {
302
+ padding: 1rem !important;
303
+ display: block;
304
+ }
305
+
306
+ /* Syntax highlighter specific styles */
307
+ .code-block-container pre > div {
308
+ background-color: transparent !important;
309
+ margin: 0 !important;
310
+ padding: 0 !important;
311
+ font-size: 0.875em;
312
+ line-height: 2rem;
313
+ }
314
+
315
+ /* Custom scrollbar for code blocks */
316
+ .code-block-container::-webkit-scrollbar,
317
+ .code-block-container pre::-webkit-scrollbar,
318
+ .code-block-container > div::-webkit-scrollbar,
319
+ .streaming-content pre::-webkit-scrollbar {
320
+ height: 8px;
321
+ width: 8px;
322
+ }
323
+
324
+ .code-block-container::-webkit-scrollbar-track,
325
+ .code-block-container pre::-webkit-scrollbar-track,
326
+ .code-block-container > div::-webkit-scrollbar-track,
327
+ .streaming-content pre::-webkit-scrollbar-track {
328
+ background: rgba(139, 148, 158, 0.1);
329
+ border-radius: 4px;
330
+ }
331
+
332
+ .code-block-container::-webkit-scrollbar-thumb,
333
+ .code-block-container pre::-webkit-scrollbar-thumb,
334
+ .code-block-container > div::-webkit-scrollbar-thumb,
335
+ .streaming-content pre::-webkit-scrollbar-thumb {
336
+ background: rgba(139, 148, 158, 0.4);
337
+ border-radius: 4px;
338
+ }
339
+
340
+ .code-block-container::-webkit-scrollbar-thumb:hover,
341
+ .code-block-container pre::-webkit-scrollbar-thumb:hover,
342
+ .code-block-container > div::-webkit-scrollbar-thumb:hover,
343
+ .streaming-content pre::-webkit-scrollbar-thumb:hover {
344
+ background: rgba(139, 148, 158, 0.6);
345
+ }
346
+
347
+ /* Inline code */
348
+ .streaming-content code {
349
+ background-color: rgba(175, 184, 193, 0.2);
350
+ color: #0969da;
351
+ padding: 0.2rem 0.4rem;
352
+ border-radius: 6px;
353
+ font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
354
+ font-size: 0.85em;
355
+ font-weight: 600;
356
+ white-space: pre-wrap;
357
+ }
358
+
359
+ /* Dark mode support for inline code */
360
+ @media (prefers-color-scheme: dark) {
361
+ .streaming-content code {
362
+ background-color: rgba(110, 118, 129, 0.4);
363
+ color: #79c0ff;
364
+ font-weight: 100;
365
+ }
366
+ }
367
+
368
+ /* Code inside pre blocks */
369
+ .streaming-content pre code,
370
+ .code-block-container code,
371
+ .code-block-container pre code {
372
+ background-color: transparent !important;
373
+ color: inherit;
374
+ padding: 0;
375
+ font-weight: normal;
376
+ font-size: 0.95rem !important;
377
+ }
378
+
379
+ /* Ensure code in blockquotes still has styling */
380
+ .markdown-blockquote code {
381
+ background-color: rgba(31, 35, 40, 0.12);
382
+ color: #0969da;
383
+ font-weight: 600;
384
+ }
385
+
386
+ /* Table Container */
387
+ .table-container {
388
+ margin: 1rem 0 0.5rem 0;
389
+ max-width: 45rem !important;
390
+ position: relative;
391
+ border: none;
392
+ border-radius: none;
393
+ box-shadow: none;
394
+ display: flex;
395
+ flex-direction: column;
396
+ align-items: flex-end;
397
+ }
398
+
399
+ .table-container table {
400
+ width: 100%;
401
+ border-collapse: collapse;
402
+ border: 1px solid #e5e7eb80;
403
+ border-radius: 8px;
404
+ box-shadow: 0 1px 3px rgba(0, 0,0.05);
405
+ overflow-x: auto;
406
+ }
407
+
408
+ .table-container th,
409
+ .table-container td {
410
+ border: 1px solid #e5e7eb80;
411
+ padding: 0.2rem 0.4rem;
412
+ text-align: center !important;
413
+ font-size: 0.95rem;
414
+ vertical-align: middle;
415
+ white-space: wrap;
416
+ }
417
+
418
+ .table-container th {
419
+ background-color: #2b181bb7;
420
+ font-weight: 700;
421
+ color: inherit;
422
+ }
423
+
424
+ /* Table copy button */
425
+ .table-copy-button {
426
+ background-color: transparent;
427
+ color: inherit;
428
+ border: none;
429
+ padding: 0;
430
+ cursor: pointer;
431
+ transition: all 0.2s ease;
432
+ display: flex;
433
+ align-items: center;
434
+ justify-content: center;
435
+ opacity: 0;
436
+ font-size: 1.05rem;
437
+ margin-bottom: 0.5rem;
438
+ position: relative;
439
+ }
440
+
441
+ .table-container:hover .table-copy-button {
442
+ opacity: 1;
443
+ }
444
+
445
+ .table-copy-button:hover {
446
+ opacity: 1;
447
+ }
448
+
449
+ .table-copy-button:hover svg {
450
+ filter: brightness(0.65);
451
+ }
452
+
453
+ .table-copy-button.copied {
454
+ color: inherit;
455
+ opacity: 1 !important;
456
+ font-size: 1.05rem;
457
+ }
458
+
459
+ .table-copy-button.fading {
460
+ opacity: 0 !important;
461
+ }
462
+
463
+ /* Table download button */
464
+ .table-download-button {
465
+ background-color: transparent;
466
+ color: inherit;
467
+ border: none;
468
+ padding: 0;
469
+ cursor: pointer;
470
+ transition: all 0.2s ease;
471
+ display: flex;
472
+ align-items: center;
473
+ justify-content: center;
474
+ opacity: 0;
475
+ font-size: 1.05rem;
476
+ margin-top: 0.5rem;
477
+ position: relative;
478
+ }
479
+
480
+ .table-container:hover .table-download-button {
481
+ opacity: 1;
482
+ }
483
+
484
+ .table-download-button:hover {
485
+ opacity: 1;
486
+ }
487
+
488
+ .table-download-button:hover svg {
489
+ filter: brightness(0.65);
490
+ }
491
+
492
+ .table-download-button.downloaded {
493
+ color: inherit;
494
+ opacity: 1 !important;
495
+ font-size: 1.05rem;
496
+ }
497
+
498
+ .table-download-button.fading {
499
+ opacity: 0 !important;
500
+ }
501
+
502
+ /* Math expressions */
503
+ .inline-math {
504
+ display: inline;
505
+ margin: 0;
506
+ }
507
+
508
+ .block-math {
509
+ display: block;
510
+ margin: 0;
511
+ text-align: left;
512
+ overflow-x: auto;
513
+ overflow-y: hidden;
514
+ }
515
+
516
+ /* KaTeX specific adjustments */
517
+ .katex {
518
+ font-size: 1.1rem;
519
+ vertical-align: baseline;
520
+ }
521
+
522
+ .katex-display {
523
+ margin: 0;
524
+ text-align: left;
525
+ }
526
+
527
+ /* Ensure math expressions don't break responsive layout */
528
+ .block-math .katex-display {
529
+ overflow-x: auto;
530
+ overflow-y: hidden;
531
+ padding: 0;
532
+ }
533
+
534
+ .block-math .katex-display > .katex {
535
+ text-align: left;
536
+ padding-left: 0;
537
+ }
538
+
539
+ /* Make inline math blend with text */
540
+ .streaming-content .inline-math .katex {
541
+ display: inline;
542
+ line-height: inherit;
543
+ }
544
+
545
+ /* Make block math blend with paragraphs */
546
+ .streaming-content .block-math {
547
+ line-height: 2rem;
548
+ font-size: inherit;
549
+ }
550
+
551
+ /* Fix fraction sizing */
552
+ .streaming-content .katex .frac-line {
553
+ border-bottom-width: 0.06em; /* Fraction line */
554
+ }
555
+
556
+ .streaming-content .katex .mfrac {
557
+ font-size: 1.2rem; /* Nested fractions */
558
+ }
559
+
560
+ /* Complex math elements */
561
+ .streaming-content .katex .mord,
562
+ .streaming-content .katex .mbin,
563
+ .streaming-content .katex .mrel,
564
+ .streaming-content .katex .minner,
565
+ .streaming-content .katex .mop {
566
+ font-size: inherit; /* Ensure all math elements scale with text */
567
+ }
568
+
569
+ /* Tooltip styling */
570
+ .tooltip {
571
+ position: absolute;
572
+ bottom: 100%;
573
+ left: 50%;
574
+ transform: translateX(-50%) translateY(10px) scale(0.9);
575
+ transform-origin: bottom center;
576
+ margin-bottom: 0.65rem;
577
+ padding: 0.3rem 0.6rem;
578
+ background-color: #2b2b2b;
579
+ color: #e0e0e0;
580
+ border-radius: 0.25rem;
581
+ white-space: nowrap;
582
+ font-size: 0.85rem;
583
+ opacity: 0;
584
+ visibility: hidden;
585
+ transition: transform 0.3s ease, opacity 0.3s ease;
586
+ }
587
+
588
+ /* Show the tooltip on hover */
589
+ .code-copy-button:hover .tooltip,
590
+ .table-copy-button:hover .tooltip,
591
+ .table-download-button:hover .tooltip {
592
+ opacity: 1;
593
+ visibility: visible;
594
+ transform: translateX(-50%) translateY(0) scale(1);
595
+ }
596
+
597
+ /* Links */
598
+ .markdown-link {
599
+ color: #2563eb;
600
+ text-decoration: none;
601
+ font-weight: 500;
602
+ transition: color 0.15s ease;
603
+ }
604
+
605
+ .markdown-link:hover {
606
+ color: #1d4ed8;
607
+ text-decoration: underline;
608
+ }
609
+
610
+ /* Blockquotes */
611
+ .markdown-blockquote {
612
+ border-left: 4px solid #3b82f6;
613
+ padding-left: 1rem;
614
+ margin: 1rem 0;
615
+ color: #4b5563;
616
+ font-style: italic;
617
+ background-color: #f0f9ff;
618
+ padding: 1rem;
619
+ border-radius: 0 8px 8px 0;
620
+ }
621
+
622
+ /* Strong/Bold text */
623
+ .streaming-content strong {
624
+ font-weight: bold;
625
+ }
626
+
627
+ /* Emphasis/Italic text */
628
+ .streaming-content em {
629
+ font-style: italic;
630
+ }
631
+
632
+ /* Horizontal rule */
633
+ .streaming-content hr {
634
+ margin: 1.5rem 0;
635
+ border: 0;
636
+ height: 1px;
637
+ background: linear-gradient(to right, transparent, #e5e7eb, transparent);
638
+ }
639
+
640
+ /* Images */
641
+ .streaming-content img {
642
+ max-width: 100%;
643
+ height: auto;
644
+ margin: 1rem 0;
645
+ border-radius: 8px;
646
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
647
+ }
648
+
649
+ /* Line breaks */
650
+ .streaming-content br {
651
+ margin: 0;
652
+ }
653
+
654
+ /* Source reference styling (for citations) */
655
+ .source-reference {
656
+ color: #2563eb;
657
+ cursor: pointer;
658
+ font-size: 0.75em;
659
+ padding: 0 0.2rem;
660
+ font-weight: 500;
661
+ transition: all 0.15s ease;
662
+ }
663
+
664
+ .source-reference:hover {
665
+ text-decoration: underline;
666
+ background-color: #dbeafe;
667
+ border-radius: 3px;
668
+ }
669
+
670
+ /* Ensure proper spacing when mixing elements */
671
+ .streaming-content > :first-child {
672
+ margin-top: 0 !important;
673
+ }
674
+
675
+ .streaming-content > :last-child {
676
+ margin-bottom: 0 !important;
677
+ }
678
+
679
+ /* Adjacent element spacing rules */
680
+ .streaming-content p + h1,
681
+ .streaming-content p + h2,
682
+ .streaming-content p + h3,
683
+ .streaming-content p + h4,
684
+ .streaming-content p + h5,
685
+ .streaming-content p + h6,
686
+ .streaming-content ul + h1,
687
+ .streaming-content ul + h2,
688
+ .streaming-content ul + h3,
689
+ .streaming-content ul + h4,
690
+ .streaming-content ul + h5,
691
+ .streaming-content ul + h6,
692
+ .streaming-content ol + h1,
693
+ .streaming-content ol + h2,
694
+ .streaming-content ol + h3,
695
+ .streaming-content ol + h4,
696
+ .streaming-content ol + h5,
697
+ .streaming-content ol + h6 {
698
+ margin-top: 1.5rem !important;
699
+ }
700
+
701
+ /* Blockquote spacing */
702
+ .streaming-content blockquote + p,
703
+ .streaming-content p + blockquote {
704
+ margin-top: 0.75rem !important;
705
+ }
706
+
707
+ /* Code block spacing */
708
+ .code-block-container + p,
709
+ .code-block-container + ul,
710
+ .code-block-container + ol,
711
+ .code-block-container + blockquote,
712
+ .code-block-container + h1,
713
+ .code-block-container + h2,
714
+ .code-block-container + h3,
715
+ .code-block-container + h4,
716
+ .code-block-container + h5,
717
+ .code-block-container + h6 {
718
+ margin-top: 0 !important;
719
+ }
720
+
721
+ .streaming-content p + .code-block-container,
722
+ .streaming-content ul + .code-block-container,
723
+ .streaming-content ol + .code-block-container,
724
+ .streaming-content blockquote + .code-block-container,
725
+ .streaming-content h1 + .code-block-container,
726
+ .streaming-content h2 + .code-block-container,
727
+ .streaming-content h3 + .code-block-container,
728
+ .streaming-content h4 + .code-block-container,
729
+ .streaming-content h5 + .code-block-container,
730
+ .streaming-content h6 + .code-block-container {
731
+ margin-top: 1rem !important;
732
+ }
frontend/src/Components/AiComponents/ChatComponents/Streaming.js ADDED
@@ -0,0 +1,536 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useEffect, useRef, useState } from 'react';
2
+ import Markdown from 'markdown-to-jsx';
3
+ import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
4
+ import { vscDarkPlus } from 'react-syntax-highlighter/dist/esm/styles/prism';
5
+ import { FcCheckmark } from 'react-icons/fc';
6
+ import { BiSolidCopy } from 'react-icons/bi';
7
+ import { LuDownload } from 'react-icons/lu';
8
+ import katex from 'katex';
9
+ import 'katex/dist/katex.min.css';
10
+ import './Streaming.css';
11
+ import './SourceRef.css';
12
+
13
+ // Math rendering components
14
+ const InlineMath = ({ children }) => {
15
+ const mathRef = useRef(null);
16
+
17
+ useEffect(() => {
18
+ if (mathRef.current && children) {
19
+ try {
20
+ katex.render(children.toString(), mathRef.current, {
21
+ throwOnError: false,
22
+ displayMode: false
23
+ });
24
+ } catch (e) {
25
+ mathRef.current.innerHTML = `<span style="color: red;">Error: ${e.message}</span>`;
26
+ }
27
+ }
28
+ }, [children]);
29
+
30
+ return <span ref={mathRef} className="inline-math" />;
31
+ };
32
+
33
+ const BlockMath = ({ children }) => {
34
+ const mathRef = useRef(null);
35
+
36
+ useEffect(() => {
37
+ if (mathRef.current && children) {
38
+ try {
39
+ katex.render(children.toString(), mathRef.current, {
40
+ throwOnError: false,
41
+ displayMode: true
42
+ });
43
+ } catch (e) {
44
+ mathRef.current.innerHTML = `<div style="color: red;">Error: ${e.message}</div>`;
45
+ }
46
+ }
47
+ }, [children]);
48
+
49
+ return <div ref={mathRef} className="block-math" />;
50
+ };
51
+
52
+ // Helper function to normalize various citation formats (e.g., [1,2], [1, 2]) into the standard [1][2] format
53
+ const normalizeCitations = (text) => {
54
+ if (!text) return '';
55
+
56
+ // First, temporarily replace math expressions to protect them from citation processing
57
+ const mathPlaceholders = [];
58
+ let mathIndex = 0;
59
+
60
+ // Replace block math
61
+ text = text.replace(/\$\$([\s\S]*?)\$\$/g, (match) => {
62
+ const placeholder = `__BLOCK_MATH_${mathIndex}__`;
63
+ mathPlaceholders[mathIndex] = match;
64
+ mathIndex++;
65
+ return placeholder;
66
+ });
67
+
68
+ // Replace inline math
69
+ text = text.replace(/\$([^\$\n]+?)\$/g, (match) => {
70
+ const placeholder = `__INLINE_MATH_${mathIndex}__`;
71
+ mathPlaceholders[mathIndex] = match;
72
+ mathIndex++;
73
+ return placeholder;
74
+ });
75
+
76
+ // Process citations
77
+ const citationRegex = /\[(\d+(?:,\s*\d+)+)\]/g;
78
+ text = text.replace(citationRegex, (match, capturedNumbers) => {
79
+ const numbers = capturedNumbers
80
+ .split(/,\s*/)
81
+ .map(numStr => numStr.trim())
82
+ .filter(Boolean);
83
+
84
+ if (numbers.length <= 1) {
85
+ return match;
86
+ }
87
+
88
+ return numbers.map(num => `[${num}]`).join('');
89
+ });
90
+
91
+ // Restore math expressions
92
+ mathPlaceholders.forEach((math, index) => {
93
+ const placeholder = math.startsWith('$$') ? `__BLOCK_MATH_${index}__` : `__INLINE_MATH_${index}__`;
94
+ text = text.replace(placeholder, math);
95
+ });
96
+
97
+ return text;
98
+ };
99
+
100
+ // Streaming component for rendering markdown content
101
+ const Streaming = ({ content, isStreaming, onContentRef, showSourcePopup, hideSourcePopup }) => {
102
+ const contentRef = useRef(null);
103
+
104
+ useEffect(() => {
105
+ if (contentRef.current && onContentRef) {
106
+ onContentRef(contentRef.current);
107
+ }
108
+ }, [content, onContentRef]);
109
+
110
+ const displayContent = isStreaming ? `${content}▌` : (content || '');
111
+ const normalizedContent = normalizeCitations(displayContent);
112
+
113
+ // CodeBlock component with copy functionality
114
+ const CodeBlock = ({ language, codeString }) => {
115
+ const [copied, setCopied] = useState(false);
116
+ const [showCopyTooltip, setShowCopyTooltip] = useState(true);
117
+
118
+ const handleCopy = () => {
119
+ const textToCopy = String(codeString).replace(/\n$/, '');
120
+ navigator.clipboard.writeText(textToCopy).then(() => {
121
+ setCopied(true);
122
+ setShowCopyTooltip(false);
123
+ setTimeout(() => setCopied(false), 2000);
124
+ }).catch(err => {
125
+ console.error('Failed to copy:', err);
126
+ });
127
+ };
128
+
129
+ return (
130
+ <div className="code-block-container">
131
+ <div className="code-block-header">
132
+ <span>{language}</span>
133
+ <button className={`code-copy-button ${copied ? 'copied' : ''}`} onClick={handleCopy}>
134
+ {copied ? <FcCheckmark /> : <BiSolidCopy />}
135
+ {showCopyTooltip && <span className="tooltip">Copy Code</span>}
136
+ </button>
137
+ </div>
138
+ <SyntaxHighlighter
139
+ style={vscDarkPlus}
140
+ language={language}
141
+ PreTag="div"
142
+ >
143
+ {String(codeString).replace(/\n$/, '')}
144
+ </SyntaxHighlighter>
145
+ </div>
146
+ );
147
+ };
148
+
149
+ // TableWithCopy component with copy and download functionality
150
+ const TableWithCopy = ({ children, ...props }) => {
151
+ const [copied, setCopied] = useState(false);
152
+ const [fadingCopy, setFadingCopy] = useState(false);
153
+ const [showCopyTooltip, setShowCopyTooltip] = useState(true);
154
+ const [downloaded, setDownloaded] = useState(false);
155
+ const [fadingDownload, setFadingDownload] = useState(false);
156
+ const [showDownloadTooltip, setShowDownloadTooltip] = useState(true);
157
+ const tableRef = useRef(null);
158
+
159
+ const handleCopy = () => {
160
+ if (tableRef.current) {
161
+ const rows = Array.from(tableRef.current.querySelectorAll('tr'));
162
+ const csvData = rows.map(row => {
163
+ const cells = Array.from(row.querySelectorAll('th, td'));
164
+ return cells.map(cell => {
165
+ let text = cell.innerText.trim();
166
+ if (text.includes('"') || text.includes(',') || text.includes('\n')) {
167
+ text = '"' + text.replace(/"/g, '""') + '"';
168
+ }
169
+ return text;
170
+ }).join(',');
171
+ }).join('\n');
172
+ navigator.clipboard.writeText(csvData).then(() => {
173
+ setCopied(true);
174
+ setShowCopyTooltip(false);
175
+ setTimeout(() => {
176
+ setFadingCopy(true);
177
+ setTimeout(() => {
178
+ setCopied(false);
179
+ setFadingCopy(false);
180
+ }, 200);
181
+ }, 2000);
182
+ }).catch(err => {
183
+ console.error('Failed to copy table:', err);
184
+ });
185
+ }
186
+ };
187
+
188
+ const handleDownload = () => {
189
+ if (tableRef.current) {
190
+ const rows = Array.from(tableRef.current.querySelectorAll('tr'));
191
+ const tsvData = rows.map(row => {
192
+ const cells = Array.from(row.querySelectorAll('th, td'));
193
+ return cells.map(cell => {
194
+ let text = cell.innerText.trim();
195
+ if (text.includes('"') || text.includes('\t') || text.includes('\n')) {
196
+ text = '"' + text.replace(/"/g, '""') + '"';
197
+ }
198
+ return text;
199
+ }).join('\t');
200
+ }).join('\n');
201
+ const blob = new Blob([tsvData], { type: 'application/vnd.ms-excel' });
202
+ const url = URL.createObjectURL(blob);
203
+ const a = document.createElement('a');
204
+ a.href = url;
205
+ a.download = 'table.xls';
206
+ a.click();
207
+ URL.revokeObjectURL(url);
208
+ setDownloaded(true);
209
+ setShowDownloadTooltip(false);
210
+ setTimeout(() => {
211
+ setFadingDownload(true);
212
+ setTimeout(() => {
213
+ setDownloaded(false);
214
+ setFadingDownload(false);
215
+ }, 200);
216
+ }, 2000);
217
+ }
218
+ };
219
+
220
+ return (
221
+ <div className="table-container">
222
+ <button className={`table-copy-button ${copied ? 'copied' : ''} ${fadingCopy ? 'fading' : ''}`} onClick={handleCopy}>
223
+ {copied ? <FcCheckmark /> : <BiSolidCopy />}
224
+ {showCopyTooltip && <span className="tooltip">Copy Table</span>}
225
+ </button>
226
+ <table ref={tableRef} {...props}>{children}</table>
227
+ <button className={`table-download-button ${downloaded ? 'downloaded' : ''} ${fadingDownload ? 'fading' : ''}`} onClick={handleDownload}>
228
+ {downloaded ? <FcCheckmark /> : <LuDownload />}
229
+ {showDownloadTooltip && <span className="tooltip">Download Table</span>}
230
+ </button>
231
+ </div>
232
+ );
233
+ };
234
+
235
+ // Custom renderer for text nodes to handle source references and math
236
+ const renderWithSourceRefsAndMath = (elementType) => {
237
+ const ElementComponent = elementType; // e.g., 'p', 'li'
238
+
239
+ // Helper to gather plain text
240
+ const getFullText = (something) => {
241
+ if (typeof something === 'string') return something;
242
+ if (Array.isArray(something)) return something.map(getFullText).join('');
243
+ if (React.isValidElement(something) && something.props?.children)
244
+ return getFullText(React.Children.toArray(something.props.children));
245
+ return '';
246
+ };
247
+
248
+ return ({ children, ...props }) => {
249
+ // Plain‑text version of this block (paragraph / list‑item)
250
+ const fullText = getFullText(children);
251
+ // Same regex the backend used
252
+ const sentenceRegex = /[^.!?\n]+[.!?]+[\])'"`'"]*|[^.!?\n]+$/g;
253
+ const sentencesArr = fullText.match(sentenceRegex) || [fullText];
254
+
255
+ // Helper function to find the sentence that contains position `pos`
256
+ const sentenceByPos = (pos) => {
257
+ let run = 0;
258
+ for (const s of sentencesArr) {
259
+ const end = run + s.length;
260
+ if (pos >= run && pos < end) return s.trim();
261
+ run = end;
262
+ }
263
+ return fullText.trim();
264
+ };
265
+
266
+ // Cursor that advances through fullText so each subsequent
267
+ // indexOf search starts AFTER the previous match
268
+ let searchCursor = 0;
269
+
270
+ // Recursive renderer that preserves existing markup and adds math support
271
+ const processNode = (node, keyPrefix = 'node') => {
272
+ if (typeof node === 'string') {
273
+ const parts = [];
274
+ let lastIndex = 0;
275
+
276
+ // First process block math
277
+ const blockMathRegex = /\$\$([\s\S]*?)\$\$/g;
278
+ let blockMatch;
279
+
280
+ while ((blockMatch = blockMathRegex.exec(node))) {
281
+ // Process text before the math expression for citations
282
+ const textBefore = node.slice(lastIndex, blockMatch.index);
283
+ if (textBefore) {
284
+ parts.push(...processCitations(textBefore, keyPrefix, lastIndex));
285
+ }
286
+
287
+ // Add the block math component
288
+ parts.push(
289
+ <BlockMath key={`${keyPrefix}-block-math-${blockMatch.index}`}>
290
+ {blockMatch[1]}
291
+ </BlockMath>
292
+ );
293
+
294
+ lastIndex = blockMatch.index + blockMatch[0].length;
295
+ }
296
+
297
+ // Process remaining text for inline math and citations
298
+ const remainingText = node.slice(lastIndex);
299
+ if (remainingText) {
300
+ parts.push(...processInlineMathAndCitations(remainingText, keyPrefix, lastIndex));
301
+ }
302
+
303
+ return parts;
304
+ }
305
+
306
+ // For non‑string children, recurse (preserves <em>, <strong>, links, etc.)
307
+ if (React.isValidElement(node) && node.props?.children) {
308
+ const processed = React.Children.map(node.props.children, (child, i) =>
309
+ processNode(child, `${keyPrefix}-${i}`)
310
+ );
311
+ return React.cloneElement(node, { children: processed });
312
+ }
313
+
314
+ return node; // element without children or unknown type
315
+ };
316
+
317
+ // Helper function to process inline math and citations
318
+ const processInlineMathAndCitations = (text, keyPrefix, offset) => {
319
+ const parts = [];
320
+ let lastIndex = 0;
321
+
322
+ // Combined regex for inline math and citations
323
+ const combinedRegex = /\$([^\$\n]+?)\$|\[(\d+)\]/g;
324
+ let match;
325
+
326
+ while ((match = combinedRegex.exec(text))) {
327
+ // Add text before the match
328
+ if (match.index > lastIndex) {
329
+ parts.push(text.slice(lastIndex, match.index));
330
+ }
331
+
332
+ if (match[1] !== undefined) {
333
+ // It's inline math
334
+ parts.push(
335
+ <InlineMath key={`${keyPrefix}-inline-math-${match.index}`}>
336
+ {match[1]}
337
+ </InlineMath>
338
+ );
339
+ } else if (match[2] !== undefined) {
340
+ // It's a citation
341
+ const num = parseInt(match[2], 10);
342
+ const absIdx = fullText.indexOf(match[0], searchCursor);
343
+ if (absIdx !== -1) searchCursor = absIdx + match[0].length;
344
+ const sentenceForPopup = sentenceByPos(absIdx);
345
+
346
+ parts.push(
347
+ <sup
348
+ key={`${keyPrefix}-ref-${num}-${match.index}`}
349
+ className="source-reference"
350
+ onMouseEnter={(e) =>
351
+ showSourcePopup &&
352
+ showSourcePopup(num - 1, e.target, sentenceForPopup)
353
+ }
354
+ onMouseLeave={hideSourcePopup}
355
+ >
356
+ {num}
357
+ </sup>
358
+ );
359
+ }
360
+
361
+ lastIndex = match.index + match[0].length;
362
+ }
363
+
364
+ // Add remaining text
365
+ if (lastIndex < text.length) {
366
+ parts.push(text.slice(lastIndex));
367
+ }
368
+
369
+ return parts;
370
+ };
371
+
372
+ // Helper function to process only citations
373
+ const processCitations = (text, keyPrefix, offset) => {
374
+ const citationRegex = /\[(\d+)\]/g;
375
+ let last = 0;
376
+ let parts = [];
377
+ let m;
378
+
379
+ while ((m = citationRegex.exec(text))) {
380
+ const sliceBefore = text.slice(last, m.index);
381
+ if (sliceBefore) parts.push(sliceBefore);
382
+
383
+ const localIdx = m.index;
384
+ const num = parseInt(m[1], 10);
385
+ const citStr = m[0];
386
+
387
+ // Find this specific occurrence in fullText, starting at searchCursor
388
+ const absIdx = fullText.indexOf(citStr, searchCursor);
389
+ if (absIdx !== -1) searchCursor = absIdx + citStr.length;
390
+
391
+ const sentenceForPopup = sentenceByPos(absIdx);
392
+
393
+ parts.push(
394
+ <sup
395
+ key={`${keyPrefix}-ref-${num}-${localIdx}`}
396
+ className="source-reference"
397
+ onMouseEnter={(e) =>
398
+ showSourcePopup &&
399
+ showSourcePopup(num - 1, e.target, sentenceForPopup)
400
+ }
401
+ onMouseLeave={hideSourcePopup}
402
+ >
403
+ {num}
404
+ </sup>
405
+ );
406
+ last = localIdx + citStr.length;
407
+ }
408
+
409
+ if (last < text.length) parts.push(text.slice(last));
410
+ return parts;
411
+ };
412
+
413
+ const processedChildren = React.Children.map(children, (child, i) =>
414
+ processNode(child, `root-${i}`)
415
+ );
416
+
417
+ // Render original element (p, li, …) with processed children
418
+ return <ElementComponent {...props}>{processedChildren}</ElementComponent>;
419
+ };
420
+ };
421
+
422
+ return (
423
+ <div className="streaming-content" ref={contentRef}>
424
+ <Markdown
425
+ options={{
426
+ wrapper: React.Fragment, // Use Fragment to avoid wrapper div
427
+ forceBlock: false,
428
+ forceInline: false,
429
+ overrides: {
430
+ p: {
431
+ component: renderWithSourceRefsAndMath('p')
432
+ },
433
+ li: {
434
+ component: renderWithSourceRefsAndMath('li')
435
+ },
436
+ h1: {
437
+ component: renderWithSourceRefsAndMath('h1')
438
+ },
439
+ h2: {
440
+ component: renderWithSourceRefsAndMath('h2')
441
+ },
442
+ h3: {
443
+ component: renderWithSourceRefsAndMath('h3')
444
+ },
445
+ h4: {
446
+ component: renderWithSourceRefsAndMath('h4')
447
+ },
448
+ h5: {
449
+ component: renderWithSourceRefsAndMath('h5')
450
+ },
451
+ h6: {
452
+ component: renderWithSourceRefsAndMath('h6')
453
+ },
454
+ pre: {
455
+ component: ({ children, ...props }) => {
456
+ let codeElement = null;
457
+ let codeString = '';
458
+ let language = '';
459
+
460
+ React.Children.forEach(children, (child) => {
461
+ if (React.isValidElement(child)) { // Check for element, no type restriction
462
+ codeElement = child;
463
+ const className = child.props?.className || '';
464
+ const match = /(?:lang|language)-(\w+)/.exec(className);
465
+ if (match) {
466
+ language = match[1];
467
+ }
468
+ // Flatten children safely
469
+ codeString = React.Children.toArray(child.props?.children).join('');
470
+ }
471
+ });
472
+
473
+ // Always start code blocks on a new line
474
+ return (
475
+ <>
476
+ <div style={{ display: 'block', width: '100%', height: 0, margin: 0, padding: 0 }} />
477
+ {codeElement ? (
478
+ <CodeBlock
479
+ language={language || 'plaintext'} // Default for no-lang blocks
480
+ codeString={codeString}
481
+ />
482
+ ) : (
483
+ <pre {...props}>{children}</pre>
484
+ )}
485
+ </>
486
+ );
487
+ }
488
+ },
489
+ code: {
490
+ component: ({ className, children, ...props }) => {
491
+ // This handles inline code only
492
+ // Block code is handled by the pre component above
493
+ return (
494
+ <code className={className} {...props}>
495
+ {children}
496
+ </code>
497
+ );
498
+ }
499
+ },
500
+ table: {
501
+ component: TableWithCopy
502
+ },
503
+ a: {
504
+ component: ({ children, href, ...props }) => {
505
+ return (
506
+ <a
507
+ href={href}
508
+ target="_blank"
509
+ rel="noopener noreferrer"
510
+ className="markdown-link"
511
+ {...props}
512
+ >
513
+ {children}
514
+ </a>
515
+ );
516
+ }
517
+ },
518
+ blockquote: {
519
+ component: ({ children, ...props }) => {
520
+ return (
521
+ <blockquote className="markdown-blockquote" {...props}>
522
+ {children}
523
+ </blockquote>
524
+ );
525
+ }
526
+ }
527
+ }
528
+ }}
529
+ >
530
+ {normalizedContent}
531
+ </Markdown>
532
+ </div>
533
+ );
534
+ };
535
+
536
+ export default Streaming;
frontend/src/Components/AiComponents/ChatWindow.css ADDED
@@ -0,0 +1,277 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ :root {
2
+ --dark-surface: #190e10; /* Deep, dark maroon base */
3
+ --primary-color: #2b2b2b; /* A dark gray for sidebars and buttons */
4
+ --secondary-color: #03dac6; /* A cool teal for accents */
5
+ --accent-color: #aaabb9; /* A warm accent for highlights and borders */
6
+ --text-color: #e0e0e0; /* Off-white text */
7
+ --hover-bg: #3a3a3a; /* Slightly lighter for hover effects */
8
+ --transition-speed: 0.3s;
9
+ }
10
+
11
+ /* Error message container */
12
+ .error-block {
13
+ background-color: #f25e5ecb;
14
+ color: var(--text-color);
15
+ padding: 0.1rem 1rem;
16
+ border-radius: 0.3rem;
17
+ margin: 2rem 0;
18
+ text-align: left;
19
+ }
20
+
21
+ /* Container for the messages */
22
+ .answer-container {
23
+ display: flex;
24
+ flex-direction: column;
25
+ gap: 2rem;
26
+ padding: 1rem;
27
+ min-width: 800px;
28
+ }
29
+
30
+ .answer-block {
31
+ position: relative;
32
+ padding-left: 45px;
33
+ }
34
+
35
+ /* Common message row styling */
36
+ .message-row {
37
+ display: flex;
38
+ align-items: flex-start;
39
+ }
40
+
41
+ /* ----------------- User Message Styling ----------------- */
42
+ /* User message row: bubble on the left, icon on the right */
43
+ .user-message {
44
+ justify-content: flex-end;
45
+ }
46
+
47
+ .user-message .message-bubble {
48
+ background-color: #FF8C00;
49
+ color: #ffffff;
50
+ border-radius: 0.35rem;
51
+ padding: 0.5rem 1rem;
52
+ max-width: 70%;
53
+ text-align: left;
54
+ }
55
+
56
+ .user-message .user-icon {
57
+ margin-left: 0.5rem;
58
+ }
59
+
60
+ .sources-read {
61
+ font-weight: bold;
62
+ }
63
+
64
+ /* ----------------- Bot Message Styling ----------------- */
65
+ /* Bot message row */
66
+ .bot-icon {
67
+ position: absolute;
68
+ left: 0;
69
+ top: 0.25rem;
70
+ }
71
+
72
+ .bot-message {
73
+ justify-content: flex-start;
74
+ }
75
+
76
+ .bot-message .bot-icon {
77
+ margin: 0;
78
+ }
79
+
80
+
81
+ /* Container for the bot bubble and its post-icons */
82
+ .bot-container {
83
+ display: flex;
84
+ flex-direction: column;
85
+ gap: 0rem;
86
+ }
87
+
88
+ .bot-answer-container {
89
+ display: flex;
90
+ align-items: center;
91
+ gap: 0.5rem;
92
+ }
93
+
94
+ /* Bot message bubble styling */
95
+ .bot-container .message-bubble {
96
+ background-color: none;
97
+ color: var(--text-color);
98
+ padding: 0.25rem 1.15rem 1rem 0.1rem;
99
+ max-width: 97%;
100
+ text-align: left;
101
+ }
102
+
103
+ /* ----------------- Additional Styling ----------------- */
104
+
105
+ /* Styling for the "Thought and searched for..." line */
106
+ .thinking-info {
107
+ font-size: large;
108
+ font-style: italic;
109
+ color: var(--accent-color);
110
+ margin-bottom: 1.3rem;
111
+ margin-left: 3rem;
112
+ }
113
+
114
+ .sources-read {
115
+ font-weight: bold;
116
+ color: var(--text-color);
117
+ margin-bottom: 1rem;
118
+ margin-left: 3rem;
119
+ }
120
+
121
+ /* Styling for the answer text */
122
+ .answer {
123
+ margin: 0;
124
+ line-height: 1.85;
125
+ white-space: pre-wrap;
126
+ }
127
+
128
+ /* Post-answer icons container: placed below the bot bubble */
129
+ .post-icons {
130
+ display: flex;
131
+ padding-left: 0.12rem;
132
+ gap: 1rem;
133
+ }
134
+
135
+ /* Make each post icon container position relative for tooltip positioning */
136
+ .post-icons .copy-icon,
137
+ .post-icons .evaluate-icon,
138
+ .post-icons .sources-icon,
139
+ .post-icons .graph-icon,
140
+ .post-icons .excerpts-icon {
141
+ cursor: pointer;
142
+ position: relative;
143
+ }
144
+
145
+ /* Apply a brightness filter to the icon images on hover */
146
+ .post-icons .copy-icon img,
147
+ .post-icons .evaluate-icon img,
148
+ .post-icons .sources-icon img,
149
+ .post-icons .graph-icon img,
150
+ .post-icons .excerpts-icon img {
151
+ transition: filter var(--transition-speed);
152
+ }
153
+
154
+ .post-icons .copy-icon:hover img,
155
+ .post-icons .evaluate-icon:hover img,
156
+ .post-icons .sources-icon:hover img,
157
+ .post-icons .graph-icon:hover img,
158
+ .post-icons .excerpts-icon:hover img {
159
+ filter: brightness(0.65);
160
+ }
161
+
162
+ /* Tooltip styling */
163
+ .tooltip {
164
+ position: absolute;
165
+ bottom: 100%;
166
+ left: 50%;
167
+ transform: translateX(-50%) translateY(10px) scale(0.9);
168
+ transform-origin: bottom center;
169
+ margin-bottom: 0.65rem;
170
+ padding: 0.3rem 0.6rem;
171
+ background-color: var(--primary-color);
172
+ color: var(--text-color);
173
+ border-radius: 0.25rem;
174
+ white-space: nowrap;
175
+ font-size: 0.85rem;
176
+ opacity: 0;
177
+ visibility: hidden;
178
+ transition: transform 0.3s ease, opacity 0.3s ease;
179
+ }
180
+
181
+ /* Show the tooltip on hover */
182
+ .post-icons .copy-icon:hover .tooltip,
183
+ .post-icons .evaluate-icon:hover .tooltip,
184
+ .post-icons .sources-icon:hover .tooltip,
185
+ .post-icons .graph-icon:hover .tooltip,
186
+ .post-icons .excerpts-icon:hover .tooltip {
187
+ opacity: 1;
188
+ visibility: visible;
189
+ transform: translateX(-50%) translateY(0) scale(1);
190
+ }
191
+
192
+ /* Styling for the question text */
193
+ .question {
194
+ margin: 0;
195
+ white-space: pre-wrap;
196
+ line-height: 1.4;
197
+ }
198
+
199
+ /* Reduce the size of user and bot icons */
200
+ .user-icon img,
201
+ .bot-icon img {
202
+ width: 35px;
203
+ height: 35px;
204
+ object-fit: contain;
205
+ }
206
+
207
+ /* Reduce the size of the post-action icons */
208
+ .post-icons img {
209
+ width: 20px;
210
+ height: 20px;
211
+ object-fit: contain;
212
+ }
213
+
214
+ /* Increase the size of the excerpts icon */
215
+ .post-icons .excerpts-icon img {
216
+ width: 20px;
217
+ height: 26px;
218
+ margin-top: -3.5px;
219
+ object-fit: fill;
220
+ }
221
+
222
+ /* Container for the loading state with a dark background */
223
+ .bot-loading {
224
+ display: flex;
225
+ flex-direction: row;
226
+ align-items: center;
227
+ justify-content: center;
228
+ gap: 10px; /* adds space between the spinner and the text */
229
+ padding: 30px;
230
+ background-color: var(--dark-surface); /* Dark background */
231
+ }
232
+
233
+ .loading-text {
234
+ margin: 0; /* removes any default margins */
235
+ font-size: 1rem;
236
+ color: #ccc;
237
+ }
238
+
239
+ /* Finished state: styling for the thought time info */
240
+ .thinking-info {
241
+ margin-bottom: 4px;
242
+ }
243
+
244
+ .thinking-time {
245
+ font-size: 1rem;
246
+ color: #888;
247
+ cursor: pointer;
248
+ }
249
+
250
+ /* Snackbar styling */
251
+ .custom-snackbar {
252
+ background-color: #1488e7 !important;
253
+ color: var(--text-color) !important;
254
+ padding: 0.35rem 1rem !important;
255
+ border-radius: 4px !important;
256
+ }
257
+
258
+ /* Spinner styling */
259
+ .custom-spinner {
260
+ width: 1.35rem !important;
261
+ height: 1.35rem !important;
262
+ border: 3px solid #3b7bdc !important; /* Main Spinner */
263
+ border-top: 3px solid #434343 !important; /* Rotating path */
264
+ border-radius: 50% !important;
265
+ margin-top: 0.1rem !important;
266
+ animation: spin 0.9s linear infinite !important;
267
+ }
268
+
269
+ /* Spinner animation */
270
+ @keyframes spin {
271
+ 0% {
272
+ transform: rotate(0deg);
273
+ }
274
+ 100% {
275
+ transform: rotate(360deg);
276
+ }
277
+ }
frontend/src/Components/AiComponents/ChatWindow.js ADDED
@@ -0,0 +1,368 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useRef, useState, useCallback, useEffect } from 'react';
2
+ import Box from '@mui/material/Box';
3
+ import Snackbar from '@mui/material/Snackbar';
4
+ import Slide from '@mui/material/Slide';
5
+ import IconButton from '@mui/material/IconButton';
6
+ import { FaTimes, FaSpinner, FaCheckCircle } from 'react-icons/fa';
7
+ import GraphDialog from './ChatComponents/Graph';
8
+ import Streaming from './ChatComponents/Streaming';
9
+ import SourcePopup from './ChatComponents/SourcePopup';
10
+ import './ChatWindow.css';
11
+
12
+ import bot from '../../Icons/bot.png';
13
+ import copy from '../../Icons/copy.png';
14
+ import evaluate from '../../Icons/evaluate.png';
15
+ import sourcesIcon from '../../Icons/sources.png';
16
+ import graphIcon from '../../Icons/graph.png';
17
+ import user from '../../Icons/user.png';
18
+ import excerpts from '../../Icons/excerpts.png';
19
+
20
+ // SlideTransition function for both entry and exit transitions.
21
+ function SlideTransition(props) {
22
+ return <Slide {...props} direction="up" />;
23
+ }
24
+
25
+ function ChatWindow({
26
+ blockId,
27
+ userMessage,
28
+ tokenChunks,
29
+ aiAnswer,
30
+ thinkingTime,
31
+ thoughtLabel,
32
+ sourcesRead,
33
+ finalSources,
34
+ excerptsData,
35
+ isLoadingExcerpts,
36
+ onFetchExcerpts,
37
+ actions,
38
+ tasks,
39
+ openRightSidebar,
40
+ // openLeftSidebar,
41
+ isError,
42
+ errorMessage
43
+ }) {
44
+ console.log(`[ChatWindow ${blockId}] Received excerptsData:`, excerptsData);
45
+ const answerRef = useRef(null);
46
+ const [graphDialogOpen, setGraphDialogOpen] = useState(false);
47
+ const [snackbarOpen, setSnackbarOpen] = useState(false);
48
+ const [hoveredSourceInfo, setHoveredSourceInfo] = useState(null);
49
+ const popupTimeoutRef = useRef(null);
50
+
51
+ // Get the graph action from the actions prop.
52
+ const graphAction = actions && actions.find(a => a.name === "graph");
53
+
54
+ // Handler for copying answer to clipboard.
55
+ const handleCopy = () => {
56
+ if (answerRef.current) {
57
+ const textToCopy = answerRef.current.innerText || answerRef.current.textContent;
58
+ navigator.clipboard.writeText(textToCopy)
59
+ .then(() => {
60
+ console.log('Copied to clipboard:', textToCopy);
61
+ setSnackbarOpen(true);
62
+ })
63
+ .catch((err) => console.error('Failed to copy text:', err));
64
+ }
65
+ };
66
+
67
+ // Snackbar close handler
68
+ const handleSnackbarClose = (event, reason) => {
69
+ if (reason === 'clickaway') return;
70
+ setSnackbarOpen(false);
71
+ };
72
+
73
+ // Combine partial chunks (tokenChunks) if present; else fall back to the aiAnswer string.
74
+ const combinedAnswer = (tokenChunks && tokenChunks.length > 0)
75
+ ? tokenChunks.join("")
76
+ : aiAnswer;
77
+ const hasTokens = combinedAnswer && combinedAnswer.length > 0;
78
+ // Assume streaming is in progress if thinkingTime is not set.
79
+ const isStreaming = thinkingTime === null || thinkingTime === undefined;
80
+
81
+ // Helper to render the thought label.
82
+ const renderThoughtLabel = () => {
83
+ if (!hasTokens) {
84
+ return thoughtLabel;
85
+ } else {
86
+ if (thoughtLabel && thoughtLabel.startsWith("Thought and searched for")) {
87
+ return thoughtLabel;
88
+ }
89
+ return null;
90
+ }
91
+ };
92
+
93
+ // Helper to render sources read.
94
+ const renderSourcesRead = () => {
95
+ if (!sourcesRead && sourcesRead !== 0) return null;
96
+ return sourcesRead;
97
+ };
98
+
99
+ // When tasks first appear, automatically open the sidebar.
100
+ const prevTasksRef = useRef(tasks);
101
+ useEffect(() => {
102
+ if (prevTasksRef.current.length === 0 && tasks && tasks.length > 0) {
103
+ openRightSidebar("tasks", blockId);
104
+ }
105
+ prevTasksRef.current = tasks;
106
+ }, [tasks, blockId, openRightSidebar]);
107
+
108
+ // Handle getting the reference to the content for copy functionality
109
+ const handleContentRef = (ref) => {
110
+ answerRef.current = ref;
111
+ };
112
+
113
+ // Handle showing the source popup
114
+ const showSourcePopup = useCallback((sourceIndex, targetElement, statementText) => {
115
+ // Clear any existing timeout to prevent flickering
116
+ if (popupTimeoutRef.current) {
117
+ clearTimeout(popupTimeoutRef.current);
118
+ popupTimeoutRef.current = null;
119
+ }
120
+
121
+ if (!finalSources || !finalSources[sourceIndex] || !targetElement) return;
122
+
123
+ const rect = targetElement.getBoundingClientRect();
124
+ const scrollY = window.scrollY || window.pageYOffset;
125
+ const scrollX = window.scrollX || window.pageXOffset;
126
+
127
+ const newHoverInfo = {
128
+ index: sourceIndex,
129
+ statementText,
130
+ position: {
131
+ top: rect.top + scrollY - 10, // Position above the reference
132
+ left: rect.left + scrollX + rect.width / 2, // Center horizontally
133
+ }
134
+ };
135
+ setHoveredSourceInfo(newHoverInfo);
136
+ }, [finalSources]);
137
+
138
+ const hideSourcePopup = useCallback(() => {
139
+ if (popupTimeoutRef.current) {
140
+ clearTimeout(popupTimeoutRef.current); // Clear existing timeout if mouse leaves quickly
141
+ }
142
+ popupTimeoutRef.current = setTimeout(() => {
143
+ setHoveredSourceInfo(null);
144
+ popupTimeoutRef.current = null;
145
+ }, 15); // Delay allows moving mouse onto popup
146
+ }, []);
147
+
148
+ // Handle mouse enter on the popup to cancel the hide timeout
149
+ const cancelHidePopup = useCallback(() => {
150
+ // Clear the hide timeout if the mouse enters the popup itself
151
+ if (popupTimeoutRef.current) {
152
+ clearTimeout(popupTimeoutRef.current);
153
+ popupTimeoutRef.current = null;
154
+ }
155
+ }, []);
156
+
157
+ // Determine button state and appearance for excerpts icon
158
+ const excerptsLoaded = !!excerptsData; // True if excerptsData is not null/empty
159
+ const canFetchExcerpts = finalSources && finalSources.length > 0 &&
160
+ !isError && !excerptsLoaded && !isLoadingExcerpts;
161
+ const buttonDisabled = isLoadingExcerpts || excerptsLoaded; // Disable button if loading or loaded
162
+ const buttonIcon = isLoadingExcerpts
163
+ ? <FaSpinner className="spin" style={{ fontSize: 20 }} />
164
+ : excerptsLoaded
165
+ ? <FaCheckCircle
166
+ style={{
167
+ width: 22,
168
+ height: 22,
169
+ color: 'var(--secondary-color)',
170
+ filter: 'brightness(0.75)'
171
+ }}
172
+ />
173
+ : <img src={excerpts} alt="excerpts icon" />;
174
+ const buttonClassName = `excerpts-icon ${isLoadingExcerpts ? 'loading' : ''} ${excerptsLoaded ? 'loaded' : ''}`;
175
+
176
+ return (
177
+ <>
178
+ { !hasTokens ? (
179
+ // If no tokens, render pre-stream UI.
180
+ (!isError && thoughtLabel) ? (
181
+ <div className="answer-container">
182
+ {/* User Message */}
183
+ <div className="message-row user-message">
184
+ <div className="message-bubble user-bubble">
185
+ <p className="question">{userMessage}</p>
186
+ </div>
187
+ <div className="user-icon">
188
+ <img src={user} alt="user icon" />
189
+ </div>
190
+ </div>
191
+ {/* Bot Message (pre-stream with spinner) */}
192
+ <div className="message-row bot-message pre-stream">
193
+ <div className="bot-container">
194
+ <div className="thinking-info">
195
+ <Box mt={1} display="flex" alignItems="center">
196
+ <Box className="custom-spinner" />
197
+ <Box ml={1}>
198
+ <span
199
+ className="thinking-time"
200
+ onClick={() => openRightSidebar("tasks", blockId)}
201
+ >
202
+ {thoughtLabel}
203
+ </span>
204
+ </Box>
205
+ </Box>
206
+ </div>
207
+ </div>
208
+ </div>
209
+ </div>
210
+ ) : (
211
+ // Render without spinner (user message only)
212
+ <div className="answer-container">
213
+ <div className="message-row user-message">
214
+ <div className="message-bubble user-bubble">
215
+ <p className="question">{userMessage}</p>
216
+ </div>
217
+ <div className="user-icon">
218
+ <img src={user} alt="user icon" />
219
+ </div>
220
+ </div>
221
+ </div>
222
+ )
223
+ ) : (
224
+ // Render Full Chat Message
225
+ <div className="answer-container">
226
+ {/* User Message */}
227
+ <div className="message-row user-message">
228
+ <div className="message-bubble user-bubble">
229
+ <p className="question">{userMessage}</p>
230
+ </div>
231
+ <div className="user-icon">
232
+ <img src={user} alt="user icon" />
233
+ </div>
234
+ </div>
235
+ {/* Bot Message */}
236
+ <div className="message-row bot-message">
237
+ <div className="bot-container">
238
+ {!isError && renderThoughtLabel() && (
239
+ <div className="thinking-info">
240
+ <span
241
+ className="thinking-time"
242
+ onClick={() => openRightSidebar("tasks", blockId)}
243
+ >
244
+ {renderThoughtLabel()}
245
+ </span>
246
+ </div>
247
+ )}
248
+ {renderSourcesRead() !== null && (
249
+ <div className="sources-read-container">
250
+ <p className="sources-read">
251
+ Sources Read: {renderSourcesRead()}
252
+ </p>
253
+ </div>
254
+ )}
255
+ <div className="answer-block">
256
+ <div className="bot-icon">
257
+ <img src={bot} alt="bot icon" />
258
+ </div>
259
+ <div className="message-bubble bot-bubble">
260
+ <div className="answer">
261
+ <Streaming
262
+ content={combinedAnswer}
263
+ isStreaming={isStreaming}
264
+ onContentRef={handleContentRef}
265
+ showSourcePopup={showSourcePopup}
266
+ hideSourcePopup={hideSourcePopup}
267
+ />
268
+ </div>
269
+ </div>
270
+ <div className="post-icons">
271
+ {!isStreaming && (
272
+ <div className="copy-icon" onClick={handleCopy}>
273
+ <img src={copy} alt="copy icon" />
274
+ <span className="tooltip">Copy</span>
275
+ </div>
276
+ )}
277
+ {actions && actions.some(a => a.name === "evaluate") && (
278
+ <div className="evaluate-icon" onClick={() => openRightSidebar("evaluate", blockId)}>
279
+ <img src={evaluate} alt="evaluate icon" />
280
+ <span className="tooltip">Evaluate</span>
281
+ </div>
282
+ )}
283
+ {actions && actions.some(a => a.name === "sources") && (
284
+ <div className="sources-icon" onClick={() => openRightSidebar("sources", blockId)}>
285
+ <img src={sourcesIcon} alt="sources icon" />
286
+ <span className="tooltip">Sources</span>
287
+ </div>
288
+ )}
289
+ {actions && actions.some(a => a.name === "graph") && (
290
+ <div className="graph-icon" onClick={() => setGraphDialogOpen(true)}>
291
+ <img src={graphIcon} alt="graph icon" />
292
+ <span className="tooltip">View Graph</span>
293
+ </div>
294
+ )}
295
+ {/* Show Excerpts Button - Conditionally Rendered */}
296
+ {finalSources && finalSources.length > 0 && !isError && (
297
+ <div
298
+ className={buttonClassName}
299
+ onClick={() => canFetchExcerpts && onFetchExcerpts(blockId)}
300
+ style={{
301
+ cursor: buttonDisabled ? 'default' : 'pointer',
302
+ opacity: excerptsLoaded ? 0.6 : 1
303
+ }}
304
+ >
305
+ {buttonIcon}
306
+ <span className="tooltip">
307
+ {excerptsLoaded ? 'Excerpts Loaded'
308
+ : isLoadingExcerpts ? 'Loading Excerpts…'
309
+ : 'Show Excerpts'}
310
+ </span>
311
+ </div>
312
+ )}
313
+ </div>
314
+ </div>
315
+ </div>
316
+ </div>
317
+ {/* Render the GraphDialog when graphDialogOpen is true */}
318
+ {graphDialogOpen && (
319
+ <GraphDialog
320
+ open={graphDialogOpen}
321
+ onClose={() => setGraphDialogOpen(false)}
322
+ payload={graphAction ? graphAction.payload : { query: userMessage }}
323
+ />
324
+ )}
325
+ </div>
326
+ )}
327
+ {/* Render Source Popup */}
328
+ {hoveredSourceInfo && finalSources && finalSources[hoveredSourceInfo.index] && (
329
+ <SourcePopup
330
+ sourceData={finalSources[hoveredSourceInfo.index]}
331
+ excerptsData={excerptsData}
332
+ position={hoveredSourceInfo.position}
333
+ onMouseEnter={cancelHidePopup} // Keep popup open if mouse enters it
334
+ onMouseLeave={hideSourcePopup}
335
+ statementText={hoveredSourceInfo.statementText}
336
+ />
337
+ )}
338
+ {/* Render error container if there's an error */}
339
+ {isError && (
340
+ <div className="error-block" style={{ marginTop: '1rem' }}>
341
+ <h3>Error</h3>
342
+ <p>{errorMessage}</p>
343
+ </div>
344
+ )}
345
+ <Snackbar
346
+ open={snackbarOpen}
347
+ autoHideDuration={3000}
348
+ onClose={handleSnackbarClose}
349
+ message="Copied To Clipboard"
350
+ anchorOrigin={{ vertical: 'bottom', horizontal: 'left' }}
351
+ TransitionComponent={SlideTransition}
352
+ ContentProps={{ classes: { root: 'custom-snackbar' } }}
353
+ action={
354
+ <IconButton
355
+ size="small"
356
+ aria-label="close"
357
+ color="inherit"
358
+ onClick={handleSnackbarClose}
359
+ >
360
+ <FaTimes />
361
+ </IconButton>
362
+ }
363
+ />
364
+ </>
365
+ );
366
+ }
367
+
368
+ export default ChatWindow;
frontend/src/Components/AiComponents/Dropdowns/AddContentDropdown.css ADDED
@@ -0,0 +1,150 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .add-content-dropdown {
2
+ position: absolute;
3
+ bottom: 100%;
4
+ left: 0;
5
+ background-color: #21212f;
6
+ /* border: 0.01rem solid #444; */
7
+ border-radius: 0.35rem;
8
+ box-shadow: 0 0.75rem 0.85rem rgba(0, 0, 0, 0.484);
9
+ z-index: 1010;
10
+ width: 13.5rem;
11
+ padding: 0.3rem 0;
12
+ margin-bottom: 0.75rem;
13
+ opacity: 0;
14
+ visibility: hidden;
15
+ transform: translateY(10px);
16
+ transition: opacity 0.2s ease, transform 0.2s ease;
17
+ }
18
+
19
+ .add-content-dropdown.open {
20
+ opacity: 1;
21
+ visibility: visible;
22
+ transform: translateY(0);
23
+ }
24
+
25
+ .add-content-dropdown ul {
26
+ list-style: none;
27
+ margin: 0;
28
+ padding: 0;
29
+ }
30
+
31
+ .add-content-dropdown li {
32
+ display: flex;
33
+ align-items: center;
34
+ padding: 0.75rem 1rem;
35
+ cursor: pointer;
36
+ color: #e0e0e0;
37
+ font-size: 1rem;
38
+ position: relative;
39
+ transition: background-color 0.2s ease;
40
+ }
41
+
42
+ .add-content-dropdown li:hover {
43
+ background-color: #15151f;
44
+ border-radius: 1.35rem;
45
+ }
46
+
47
+ .add-content-dropdown li.selected:hover {
48
+ background-color: #4caf5033;
49
+ border-radius: 1.35rem;
50
+ }
51
+
52
+ /* Active state for items with open sub-menus */
53
+ .add-content-dropdown li.has-submenu.active {
54
+ background-color: #15151f;
55
+ border-radius: 1.35rem;
56
+ }
57
+
58
+ .add-content-dropdown .dropdown-icon {
59
+ margin-right: 0.75rem;
60
+ font-size: 1rem;
61
+ color: #aaabb9;
62
+ }
63
+
64
+ .selected {
65
+ background-color: #4caf501a;
66
+ }
67
+
68
+ .selected:hover {
69
+ background-color: #4caf5033;
70
+ }
71
+
72
+ .menu-item-content {
73
+ display: flex;
74
+ align-items: center;
75
+ width: 100%;
76
+ }
77
+
78
+ .add-content-dropdown li.has-submenu {
79
+ justify-content: space-between;
80
+ user-select: none; /* Prevent text selection on click */
81
+ }
82
+
83
+ .add-content-dropdown .submenu-arrow {
84
+ font-size: 0.8rem;
85
+ color: #aaabb9;
86
+ margin-left: auto;
87
+ flex-shrink: 0;
88
+ pointer-events: none; /* Prevent arrow from blocking clicks */
89
+ }
90
+
91
+ .dropdown-icon {
92
+ margin-right: 8px;
93
+ }
94
+
95
+ .sub-dropdown {
96
+ position: absolute;
97
+ left: 100%;
98
+ /* Default to opening upwards for chat view */
99
+ bottom: 0;
100
+ background-color: #21212f;
101
+ border-radius: 0.35rem;
102
+ box-shadow: 0 0.75rem 0.85rem rgba(0, 0, 0, 0.484);
103
+ z-index: 1020; /* Higher than main dropdown */
104
+ width: 13.5rem;
105
+ padding: 0.3rem 0;
106
+ opacity: 0;
107
+ visibility: hidden;
108
+ transform: translateX(10px);
109
+ transition: opacity 0.2s ease, transform 0.2s ease;
110
+ }
111
+
112
+ .sub-dropdown.open {
113
+ opacity: 1;
114
+ visibility: visible;
115
+ transform: translateX(0);
116
+ }
117
+
118
+ /* Nested sub-dropdown (third level) */
119
+ .sub-dropdown .sub-dropdown {
120
+ z-index: 1030; /* Higher than second level */
121
+ }
122
+
123
+ .sub-dropdown li.has-submenu {
124
+ justify-content: space-between;
125
+ }
126
+
127
+ /* Initial Chat Window */
128
+ .search-bar .add-content-dropdown {
129
+ top: 100%;
130
+ bottom: auto;
131
+ margin-top: 0.6rem;
132
+ margin-bottom: 0;
133
+ box-shadow: 0 -0.75rem 1rem rgba(0, 0, 0, 0.484);
134
+ transform: translateY(-10px);
135
+ }
136
+
137
+ .search-bar .sub-dropdown {
138
+ top: 0;
139
+ bottom: auto;
140
+ }
141
+
142
+ /* Third level sub-dropdown in search bar - open upward */
143
+ .search-bar .sub-dropdown .sub-dropdown {
144
+ top: auto;
145
+ bottom: 0;
146
+ }
147
+
148
+ .search-bar .add-content-dropdown.open {
149
+ transform: translateY(0);
150
+ }
frontend/src/Components/AiComponents/Dropdowns/AddContentDropdown.js ADDED
@@ -0,0 +1,359 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useEffect, useRef, useState } from 'react';
2
+ import {
3
+ FaPaperclip,
4
+ FaCubes,
5
+ FaGoogle,
6
+ FaMicrosoft,
7
+ FaSlack,
8
+ FaChevronRight,
9
+ FaFileAlt,
10
+ FaTable,
11
+ FaDesktop,
12
+ FaStickyNote,
13
+ FaTasks,
14
+ FaCalendarAlt,
15
+ FaFolderOpen,
16
+ FaEnvelope,
17
+ FaFileWord,
18
+ FaFileExcel,
19
+ FaFilePowerpoint,
20
+ FaClipboardList,
21
+ FaExchangeAlt,
22
+ FaCloud
23
+ } from 'react-icons/fa';
24
+ import './AddContentDropdown.css';
25
+
26
+ function AddContentDropdown({
27
+ isOpen,
28
+ onClose,
29
+ toggleButtonRef,
30
+ onAddFilesClick,
31
+ onServiceClick,
32
+ selectedServices = { google: [], microsoft: [], slack: false }
33
+ }) {
34
+ const dropdownRef = useRef(null);
35
+ const [openSubMenus, setOpenSubMenus] = useState({
36
+ connectApps: false,
37
+ googleWorkspace: false,
38
+ microsoft365: false
39
+ });
40
+
41
+ // Effect to handle clicks outside the dropdown to close it
42
+ useEffect(() => {
43
+ const handleClickOutside = (event) => {
44
+ // Do not close if the click is on the toggle button itself
45
+ if (toggleButtonRef && toggleButtonRef.current && toggleButtonRef.current.contains(event.target)) {
46
+ return;
47
+ }
48
+
49
+ // Close the dropdown if the click is outside of it
50
+ if (dropdownRef.current && !dropdownRef.current.contains(event.target)) {
51
+ onClose();
52
+ // Reset all sub-menus when closing
53
+ setOpenSubMenus({
54
+ connectApps: false,
55
+ googleWorkspace: false,
56
+ microsoft365: false
57
+ });
58
+ }
59
+ };
60
+
61
+ if (isOpen) {
62
+ document.addEventListener('mousedown', handleClickOutside);
63
+ } else {
64
+ document.removeEventListener('mousedown', handleClickOutside);
65
+ }
66
+
67
+ return () => {
68
+ document.removeEventListener('mousedown', handleClickOutside);
69
+ };
70
+ }, [isOpen, onClose, toggleButtonRef]);
71
+
72
+ // Reset sub-menus when dropdown closes
73
+ useEffect(() => {
74
+ if (!isOpen) {
75
+ setOpenSubMenus({
76
+ connectApps: false,
77
+ googleWorkspace: false,
78
+ microsoft365: false
79
+ });
80
+ }
81
+ }, [isOpen]);
82
+
83
+ const handleConnectAppsHover = () => {
84
+ setOpenSubMenus(prev => ({
85
+ ...prev,
86
+ connectApps: true
87
+ }));
88
+ };
89
+
90
+ const handleGoogleWorkspaceHover = () => {
91
+ setOpenSubMenus(prev => ({
92
+ ...prev,
93
+ googleWorkspace: true,
94
+ // Close Microsoft 365 when hovering Google Workspace
95
+ microsoft365: false
96
+ }));
97
+ };
98
+
99
+ const handleMicrosoft365Hover = () => {
100
+ setOpenSubMenus(prev => ({
101
+ ...prev,
102
+ microsoft365: true,
103
+ // Close Google Workspace when hovering Microsoft 365
104
+ googleWorkspace: false
105
+ }));
106
+ };
107
+
108
+ const handleSlackHover = () => {
109
+ // Close service sub-menus when hovering Slack
110
+ setOpenSubMenus(prev => ({
111
+ ...prev,
112
+ googleWorkspace: false,
113
+ microsoft365: false
114
+ }));
115
+ };
116
+
117
+ const handleAddFilesHover = () => {
118
+ // Close Connect Apps menu when hovering Add Files
119
+ setOpenSubMenus({
120
+ connectApps: false,
121
+ googleWorkspace: false,
122
+ microsoft365: false
123
+ });
124
+ };
125
+
126
+ // Simplified handlers - just call onServiceClick
127
+ const handleGoogleServiceClick = (service) => {
128
+ if (onServiceClick && typeof onServiceClick === 'function') {
129
+ onServiceClick('google', service);
130
+ }
131
+ };
132
+
133
+ const handleMicrosoftServiceClick = (service) => {
134
+ if (onServiceClick && typeof onServiceClick === 'function') {
135
+ onServiceClick('microsoft', service);
136
+ }
137
+ };
138
+
139
+ const handleSlackClick = () => {
140
+ if (onServiceClick && typeof onServiceClick === 'function') {
141
+ onServiceClick('slack', 'slack');
142
+ }
143
+ };
144
+
145
+ // Helper to check if a service is selected
146
+ const isServiceSelected = (provider, service) => {
147
+ if (provider === 'slack') {
148
+ return selectedServices.slack || false;
149
+ }
150
+ return selectedServices[provider]?.includes(service) || false;
151
+ };
152
+
153
+ return (
154
+ <div className={`add-content-dropdown ${isOpen ? 'open' : ''}`} ref={dropdownRef}>
155
+ <ul>
156
+ <li onClick={onAddFilesClick} onMouseEnter={handleAddFilesHover}>
157
+ <div className="menu-item-content">
158
+ <FaPaperclip className="dropdown-icon" />
159
+ <span>Add Files and Links</span>
160
+ </div>
161
+ </li>
162
+ <li className={`has-submenu ${openSubMenus.connectApps ? 'active' : ''}`} onMouseEnter={handleConnectAppsHover}>
163
+ <div className="menu-item-content">
164
+ <FaCubes className="dropdown-icon" />
165
+ <span>Connect Apps</span>
166
+ </div>
167
+ <FaChevronRight className="submenu-arrow" />
168
+ <div className={`sub-dropdown ${openSubMenus.connectApps ? 'open' : ''}`}>
169
+ <ul>
170
+ <li className={`has-submenu ${openSubMenus.googleWorkspace ? 'active' : ''}`} onMouseEnter={handleGoogleWorkspaceHover}>
171
+ <div className="menu-item-content">
172
+ <FaGoogle className="dropdown-icon" />
173
+ <span>Google Workspace</span>
174
+ </div>
175
+ <FaChevronRight className="submenu-arrow" />
176
+ <div className={`sub-dropdown ${openSubMenus.googleWorkspace ? 'open' : ''}`}>
177
+ <ul>
178
+ <li
179
+ onClick={() => handleGoogleServiceClick('docs')}
180
+ className={isServiceSelected('google', 'docs') ? 'selected' : ''}
181
+ >
182
+ <div className="menu-item-content">
183
+ <FaFileAlt className="dropdown-icon" />
184
+ <span>Docs</span>
185
+ </div>
186
+ </li>
187
+ <li
188
+ onClick={() => handleGoogleServiceClick('sheets')}
189
+ className={isServiceSelected('google', 'sheets') ? 'selected' : ''}
190
+ >
191
+ <div className="menu-item-content">
192
+ <FaTable className="dropdown-icon" />
193
+ <span>Sheets</span>
194
+ </div>
195
+ </li>
196
+ <li
197
+ onClick={() => handleGoogleServiceClick('slides')}
198
+ className={isServiceSelected('google', 'slides') ? 'selected' : ''}
199
+ >
200
+ <div className="menu-item-content">
201
+ <FaDesktop className="dropdown-icon" />
202
+ <span>Slides</span>
203
+ </div>
204
+ </li>
205
+ <li
206
+ onClick={() => handleGoogleServiceClick('keep')}
207
+ className={isServiceSelected('google', 'keep') ? 'selected' : ''}
208
+ >
209
+ <div className="menu-item-content">
210
+ <FaStickyNote className="dropdown-icon" />
211
+ <span>Keep</span>
212
+ </div>
213
+ </li>
214
+ <li
215
+ onClick={() => handleGoogleServiceClick('tasks')}
216
+ className={isServiceSelected('google', 'tasks') ? 'selected' : ''}
217
+ >
218
+ <div className="menu-item-content">
219
+ <FaTasks className="dropdown-icon" />
220
+ <span>Tasks</span>
221
+ </div>
222
+ </li>
223
+ <li
224
+ onClick={() => handleGoogleServiceClick('calendar')}
225
+ className={isServiceSelected('google', 'calendar') ? 'selected' : ''}
226
+ >
227
+ <div className="menu-item-content">
228
+ <FaCalendarAlt className="dropdown-icon" />
229
+ <span>Calendar</span>
230
+ </div>
231
+ </li>
232
+ <li
233
+ onClick={() => handleGoogleServiceClick('drive')}
234
+ className={isServiceSelected('google', 'drive') ? 'selected' : ''}
235
+ >
236
+ <div className="menu-item-content">
237
+ <FaFolderOpen className="dropdown-icon" />
238
+ <span>Drive</span>
239
+ </div>
240
+ </li>
241
+ <li
242
+ onClick={() => handleGoogleServiceClick('gmail')}
243
+ className={isServiceSelected('google', 'gmail') ? 'selected' : ''}
244
+ >
245
+ <div className="menu-item-content">
246
+ <FaEnvelope className="dropdown-icon" />
247
+ <span>Gmail</span>
248
+ </div>
249
+ </li>
250
+ </ul>
251
+ </div>
252
+ </li>
253
+ <li className={`has-submenu ${openSubMenus.microsoft365 ? 'active' : ''}`} onMouseEnter={handleMicrosoft365Hover}>
254
+ <div className="menu-item-content">
255
+ <FaMicrosoft className="dropdown-icon" />
256
+ <span>Microsoft 365</span>
257
+ </div>
258
+ <FaChevronRight className="submenu-arrow" />
259
+ <div className={`sub-dropdown ${openSubMenus.microsoft365 ? 'open' : ''}`}>
260
+ <ul>
261
+ <li
262
+ onClick={() => handleMicrosoftServiceClick('word')}
263
+ className={isServiceSelected('microsoft', 'word') ? 'selected' : ''}
264
+ >
265
+ <div className="menu-item-content">
266
+ <FaFileWord className="dropdown-icon" />
267
+ <span>Word</span>
268
+ </div>
269
+ </li>
270
+ <li
271
+ onClick={() => handleMicrosoftServiceClick('excel')}
272
+ className={isServiceSelected('microsoft', 'excel') ? 'selected' : ''}
273
+ >
274
+ <div className="menu-item-content">
275
+ <FaFileExcel className="dropdown-icon" />
276
+ <span>Excel</span>
277
+ </div>
278
+ </li>
279
+ <li
280
+ onClick={() => handleMicrosoftServiceClick('powerpoint')}
281
+ className={isServiceSelected('microsoft', 'powerpoint') ? 'selected' : ''}
282
+ >
283
+ <div className="menu-item-content">
284
+ <FaFilePowerpoint className="dropdown-icon" />
285
+ <span>PowerPoint</span>
286
+ </div>
287
+ </li>
288
+ <li
289
+ onClick={() => handleMicrosoftServiceClick('onenote')}
290
+ className={isServiceSelected('microsoft', 'onenote') ? 'selected' : ''}
291
+ >
292
+ <div className="menu-item-content">
293
+ <FaStickyNote className="dropdown-icon" />
294
+ <span>OneNote</span>
295
+ </div>
296
+ </li>
297
+ <li
298
+ onClick={() => handleMicrosoftServiceClick('todo')}
299
+ className={isServiceSelected('microsoft', 'todo') ? 'selected' : ''}
300
+ >
301
+ <div className="menu-item-content">
302
+ <FaClipboardList className="dropdown-icon" />
303
+ <span>To Do</span>
304
+ </div>
305
+ </li>
306
+ <li
307
+ onClick={() => handleMicrosoftServiceClick('exchange')}
308
+ className={isServiceSelected('microsoft', 'exchange') ? 'selected' : ''}
309
+ >
310
+ <div className="menu-item-content">
311
+ <FaExchangeAlt className="dropdown-icon" />
312
+ <span>Exchange</span>
313
+ </div>
314
+ </li>
315
+ <li
316
+ onClick={() => handleMicrosoftServiceClick('onedrive')}
317
+ className={isServiceSelected('microsoft', 'onedrive') ? 'selected' : ''}
318
+ >
319
+ <div className="menu-item-content">
320
+ <FaCloud className="dropdown-icon" />
321
+ <span>OneDrive</span>
322
+ </div>
323
+ </li>
324
+ <li
325
+ onClick={() => handleMicrosoftServiceClick('outlook')}
326
+ className={isServiceSelected('microsoft', 'outlook') ? 'selected' : ''}
327
+ >
328
+ <div className="menu-item-content">
329
+ <FaEnvelope className="dropdown-icon" />
330
+ <span>Outlook</span>
331
+ </div>
332
+ </li>
333
+ </ul>
334
+ </div>
335
+ </li>
336
+ <li
337
+ onMouseEnter={handleSlackHover}
338
+ onClick={handleSlackClick}
339
+ className={isServiceSelected('slack', 'slack') ? 'selected' : ''}
340
+ >
341
+ <div className="menu-item-content">
342
+ <FaSlack className="dropdown-icon" />
343
+ <span>Slack</span>
344
+ {isServiceSelected('slack', 'slack') && sessionStorage.getItem('slack_workspace') && (
345
+ <span className="workspace-info">
346
+ ({JSON.parse(sessionStorage.getItem('slack_workspace')).team_name})
347
+ </span>
348
+ )}
349
+ </div>
350
+ </li>
351
+ </ul>
352
+ </div>
353
+ </li>
354
+ </ul>
355
+ </div>
356
+ );
357
+ }
358
+
359
+ export default AddContentDropdown;
frontend/src/Components/AiComponents/Dropdowns/AddFilesDialog.css ADDED
@@ -0,0 +1,191 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .add-files-dialog {
2
+ position: fixed;
3
+ top: 0;
4
+ left: 0;
5
+ width: 100%;
6
+ height: 100vh;
7
+ background-color: rgba(0, 0, 0, 0.2);
8
+ display: flex;
9
+ justify-content: center;
10
+ align-items: center;
11
+ z-index: 1000;
12
+ overflow: hidden;
13
+ }
14
+
15
+ .add-files-dialog-inner {
16
+ position: relative;
17
+ border-radius: 12px;
18
+ padding: 32px;
19
+ width: 45%;
20
+ max-width: 100%;
21
+ background-color: #1e1e1e;
22
+ max-height: 80vh;
23
+ overflow-y: auto;
24
+ padding-top: 4.5rem;
25
+ }
26
+
27
+ .add-files-dialog-inner .dialog-title {
28
+ position: absolute;
29
+ font-weight: bold;
30
+ font-size: 1.5rem;
31
+ top: 16px;
32
+ left: 16px;
33
+ color: #e0e0e0;
34
+ }
35
+
36
+ .add-files-dialog-inner .close-btn {
37
+ position: absolute;
38
+ top: 16px;
39
+ right: 16px;
40
+ background: none;
41
+ color: white;
42
+ padding: 7px;
43
+ border-radius: 5px;
44
+ cursor: pointer;
45
+ border: none;
46
+ }
47
+
48
+ .add-files-dialog-inner .close-btn:hover {
49
+ background: rgba(255, 255, 255, 0.1);
50
+ color: white;
51
+ }
52
+
53
+ .dialog-content-area {
54
+ color: #e0e0e0;
55
+ }
56
+
57
+ .url-input-container {
58
+ margin-bottom: 1.5rem;
59
+ }
60
+
61
+ .url-input-label {
62
+ display: block;
63
+ margin-bottom: 0.5rem;
64
+ font-size: 0.9rem;
65
+ font-weight: 500;
66
+ }
67
+
68
+ .url-input-textarea {
69
+ width: 100%;
70
+ min-height: 80px;
71
+ background: #1E1E1E;
72
+ color: #DDD;
73
+ border: 1px solid #444;
74
+ padding: 10px;
75
+ border-radius: 5px;
76
+ font-size: 16px;
77
+ resize: vertical;
78
+ transition: border 0.3s ease, background 0.3s ease;
79
+ }
80
+
81
+ .file-drop-zone {
82
+ margin-top: 1rem;
83
+ border: 2px dashed #555;
84
+ border-radius: 8px;
85
+ padding: 2rem;
86
+ text-align: center;
87
+ cursor: pointer;
88
+ transition: border-color 0.2s ease, background-color 0.2s ease;
89
+ display: flex;
90
+ flex-direction: column;
91
+ align-items: center;
92
+ justify-content: center;
93
+ color: #aaa;
94
+ }
95
+
96
+ .file-drop-zone:hover {
97
+ border-color: #777;
98
+ background-color: #2a2a2a;
99
+ }
100
+
101
+ .file-drop-zone.dragging {
102
+ border-color: #26a8dc;
103
+ background-color: rgba(38, 168, 220, 0.1);
104
+ }
105
+
106
+ .file-drop-zone .upload-icon {
107
+ font-size: 3rem;
108
+ margin-bottom: 1rem;
109
+ color: #666;
110
+ }
111
+
112
+ .file-drop-zone p {
113
+ margin: 0;
114
+ font-size: 1rem;
115
+ }
116
+
117
+ .file-list {
118
+ margin-top: 1.5rem;
119
+ max-height: 250px;
120
+ overflow-y: auto;
121
+ padding-right: 0.5rem; /* Space for scrollbar */
122
+ }
123
+
124
+ .file-item {
125
+ display: flex;
126
+ align-items: center;
127
+ background-color: #2a2a2a;
128
+ padding: 0.75rem;
129
+ border-radius: 6px;
130
+ margin-bottom: 0.5rem;
131
+ }
132
+
133
+ .file-icon {
134
+ color: #aaa;
135
+ font-size: 1.5rem;
136
+ margin-right: 1rem;
137
+ }
138
+
139
+ .file-info {
140
+ flex-grow: 1;
141
+ display: flex;
142
+ flex-direction: column;
143
+ overflow: hidden;
144
+ }
145
+
146
+ .file-name {
147
+ white-space: nowrap;
148
+ overflow: hidden;
149
+ text-overflow: ellipsis;
150
+ font-size: 0.9rem;
151
+ }
152
+
153
+ .file-size {
154
+ font-size: 0.75rem;
155
+ color: #888;
156
+ }
157
+
158
+ .progress-bar-container {
159
+ width: 100px;
160
+ height: 8px;
161
+ background-color: #444;
162
+ border-radius: 4px;
163
+ margin: 0 1rem;
164
+ }
165
+
166
+ .progress-bar {
167
+ height: 100%;
168
+ background-color: #26a8dc;
169
+ border-radius: 4px;
170
+ transition: width 0.3s ease;
171
+ }
172
+
173
+ .cancel-file-btn {
174
+ background: none;
175
+ border: none;
176
+ color: #aaa;
177
+ cursor: pointer;
178
+ font-size: 1rem;
179
+ padding: 0.25rem;
180
+ }
181
+
182
+ .cancel-file-btn:hover {
183
+ color: #fff;
184
+ }
185
+
186
+ .dialog-actions {
187
+ margin-top: 1.5rem;
188
+ display: flex;
189
+ justify-content: flex-end;
190
+ gap: 0.75rem;
191
+ }
frontend/src/Components/AiComponents/Dropdowns/AddFilesDialog.js ADDED
@@ -0,0 +1,282 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState, useRef, useCallback } from 'react';
2
+ import { FaTimes, FaFileUpload, FaFileAlt } from 'react-icons/fa';
3
+ import Button from '@mui/material/Button';
4
+ import './AddFilesDialog.css';
5
+
6
+ const MAX_TOTAL_SIZE = 10 * 1024 * 1024; // 10 MB
7
+ const ALLOWED_EXTENSIONS = new Set([
8
+ // Documents
9
+ '.pdf', '.doc', '.docx', '.odt', '.txt', '.rtf', '.md',
10
+ // Spreadsheets
11
+ '.csv', '.xls', '.xlsx',
12
+ // Presentations
13
+ '.ppt', '.pptx',
14
+ // Code files
15
+ '.js', '.jsx', '.ts', '.tsx', '.py', '.java', '.c', '.cpp', '.h',
16
+ '.cs', '.html', '.css', '.scss', '.json', '.xml', '.sql', '.sh',
17
+ '.rb', '.php', '.go'
18
+ ]);
19
+
20
+ function AddFilesDialog({ isOpen, onClose, openSnackbar, setSessionContent }) {
21
+ const [isUploading, setIsUploading] = useState(false);
22
+ const [isDragging, setIsDragging] = useState(false);
23
+ const [files, setFiles] = useState([]);
24
+ const [urlInput, setUrlInput] = useState("");
25
+ const fileInputRef = useRef(null);
26
+
27
+ // Function to handle files dropped or selected
28
+ const handleFiles = useCallback((incomingFiles) => {
29
+ if (incomingFiles && incomingFiles.length > 0) {
30
+ let currentTotalSize = files.reduce((acc, f) => acc + f.file.size, 0);
31
+ const validFiles = [];
32
+
33
+ for (const file of Array.from(incomingFiles)) {
34
+ // 1. Check for duplicates
35
+ if (files.some(existing => existing.file.name === file.name && existing.file.size === file.size)) {
36
+ continue; // Skip duplicate file
37
+ }
38
+
39
+ // 2. Check file type
40
+ const fileExtension = file.name.slice(file.name.lastIndexOf('.')).toLowerCase();
41
+ if (!ALLOWED_EXTENSIONS.has(fileExtension)) {
42
+ openSnackbar(`File type not supported: ${file.name}`, 'error', 5000);
43
+ continue; // Skip unsupported file type
44
+ }
45
+
46
+ // 3. Check total size limit
47
+ if (currentTotalSize + file.size > MAX_TOTAL_SIZE) {
48
+ openSnackbar('Total file size cannot exceed 10 MB', 'error', 5000);
49
+ break; // Stop processing further files as limit is reached
50
+ }
51
+
52
+ currentTotalSize += file.size;
53
+ validFiles.push({
54
+ id: window.crypto.randomUUID(),
55
+ file: file,
56
+ progress: 0,
57
+ });
58
+ }
59
+
60
+ if (validFiles.length > 0) {
61
+ setFiles(prevFiles => [...prevFiles, ...validFiles]);
62
+ }
63
+ }
64
+ }, [files, openSnackbar]);
65
+
66
+ // Function to handle file removal
67
+ const handleRemoveFile = useCallback((fileId) => {
68
+ setFiles(prevFiles => prevFiles.filter(f => f.id !== fileId));
69
+ }, []);
70
+
71
+ // Ensure that the component does not render if isOpen is false
72
+ if (!isOpen) {
73
+ return null;
74
+ }
75
+
76
+ // Function to format file size in a human-readable format
77
+ const formatFileSize = (bytes) => {
78
+ if (bytes === 0) return '0 Bytes';
79
+ const k = 1024;
80
+ const sizes = ['Bytes', 'KB', 'MB', 'GB'];
81
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
82
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
83
+ };
84
+
85
+ // Handlers for drag and drop events
86
+ const handleDragOver = (e) => {
87
+ e.preventDefault();
88
+ e.stopPropagation();
89
+ setIsDragging(true);
90
+ };
91
+
92
+ // Handler for when the drag leaves the drop zone
93
+ const handleDragLeave = (e) => {
94
+ e.preventDefault();
95
+ e.stopPropagation();
96
+ setIsDragging(false);
97
+ };
98
+
99
+ // Handler for when files are dropped into the drop zone
100
+ const handleDrop = (e) => {
101
+ e.preventDefault();
102
+ e.stopPropagation();
103
+ setIsDragging(false);
104
+ handleFiles(e.dataTransfer.files);
105
+ };
106
+
107
+ // Handler for when files are selected via the file input
108
+ const handleFileSelect = (e) => {
109
+ handleFiles(e.target.files);
110
+ // Reset input value to allow selecting the same file again
111
+ e.target.value = null;
112
+ };
113
+
114
+ // Handler for clicking the drop zone to open the file dialog
115
+ const handleBoxClick = () => {
116
+ fileInputRef.current.click();
117
+ };
118
+
119
+ // Handler for resetting the file list
120
+ const handleReset = () => {
121
+ setFiles([]);
122
+ setUrlInput("");
123
+ };
124
+
125
+ // Handler for adding files
126
+ const handleAdd = () => {
127
+ setIsUploading(true); // Start upload state, disable buttons
128
+
129
+ // Regex to validate URL format
130
+ const urlRegex = /^(https?:\/\/)?([\w-]+\.)+[\w-]+(\/[\w-./?%&=]*)?$/;
131
+ const urls = urlInput.split('\n').map(url => url.trim()).filter(url => url);
132
+
133
+ // 1. Validate URLs before proceeding
134
+ if (files.length === 0 && urls.length === 0) {
135
+ openSnackbar("Please add files or URLs before submitting.", "error", 5000);
136
+ return;
137
+ }
138
+
139
+ for (const url of urls) {
140
+ if (!urlRegex.test(url)) {
141
+ openSnackbar(`Invalid URL format: ${url}`, 'error', 5000);
142
+ setIsUploading(false); // Reset upload state on validation error
143
+ return; // Stop the process if an invalid URL is found
144
+ }
145
+ }
146
+
147
+ // 2. If all URLs are valid, proceed with logging/uploading
148
+ const formData = new FormData();
149
+ if (files.length > 0) {
150
+ files.forEach(fileWrapper => {
151
+ formData.append('files', fileWrapper.file, fileWrapper.file.name);
152
+ });
153
+ }
154
+ formData.append('urls', JSON.stringify(urls));
155
+
156
+ const xhr = new XMLHttpRequest();
157
+ xhr.open('POST', '/add-content', true);
158
+
159
+ // Track upload progress
160
+ xhr.upload.onprogress = (event) => {
161
+ if (event.lengthComputable) {
162
+ const percentage = Math.round((event.loaded / event.total) * 100);
163
+ setFiles(prevFiles =>
164
+ prevFiles.map(f => ({ ...f, progress: percentage }))
165
+ );
166
+ }
167
+ };
168
+
169
+ // Handle completion
170
+ xhr.onload = () => {
171
+ if (xhr.status === 200) {
172
+ // --- ARTIFICIAL DELAY FOR LOCAL DEVELOPMENT ---
173
+ // This timeout ensures the 100% progress bar is visible before the dialog closes.
174
+ // This can be removed for production.
175
+ setTimeout(() => {
176
+ const result = JSON.parse(xhr.responseText);
177
+ openSnackbar('Content added successfully!', 'success');
178
+ setSessionContent(prev => ({
179
+ files: [...prev.files, ...result.files_added],
180
+ links: [...prev.links, ...result.links_added],
181
+ }));
182
+ handleReset();
183
+ onClose();
184
+ }, 500); // 0.5-second delay
185
+ } else {
186
+ const errorResult = JSON.parse(xhr.responseText);
187
+ openSnackbar(errorResult.detail || 'Failed to add content.', 'error', 5000);
188
+ setFiles(prevFiles => prevFiles.map(f => ({ ...f, progress: 0 }))); // Reset progress on error
189
+ setIsUploading(false); // End upload state
190
+ }
191
+ };
192
+
193
+ // Handle network errors
194
+ xhr.onerror = () => {
195
+ openSnackbar('An error occurred during the upload. Please check your network.', 'error', 5000);
196
+ setFiles(prevFiles => prevFiles.map(f => ({ ...f, progress: 0 }))); // Reset progress on error
197
+ };
198
+
199
+ xhr.send(formData);
200
+ };
201
+
202
+ return (
203
+ <div className="add-files-dialog" onClick={isUploading ? null : onClose}>
204
+ <div className="add-files-dialog-inner" onClick={(e) => e.stopPropagation()}>
205
+ <label className="dialog-title">Add Files and Links</label>
206
+ <button className="close-btn" onClick={onClose} disabled={isUploading}>
207
+ <FaTimes />
208
+ </button>
209
+ <div className="dialog-content-area">
210
+ <div className="url-input-container">
211
+ <textarea
212
+ id="url-input"
213
+ className="url-input-textarea"
214
+ placeholder="Enter one URL per line"
215
+ value={urlInput}
216
+ onChange={(e) => setUrlInput(e.target.value)}
217
+ />
218
+ </div>
219
+ <div
220
+ className={`file-drop-zone ${isDragging ? 'dragging' : ''}`}
221
+ onClick={handleBoxClick}
222
+ onDragOver={handleDragOver}
223
+ onDragLeave={handleDragLeave}
224
+ onDrop={handleDrop}
225
+ >
226
+ <input
227
+ type="file"
228
+ ref={fileInputRef}
229
+ onChange={handleFileSelect}
230
+ style={{ display: 'none' }}
231
+ multiple
232
+ />
233
+ <FaFileUpload className="upload-icon" />
234
+ <p>Drag and drop files here, or click to select files</p>
235
+ </div>
236
+
237
+ {files.length > 0 && (
238
+ <div className="file-list">
239
+ {files.map(fileWrapper => (
240
+ <div key={fileWrapper.id} className="file-item">
241
+ <FaFileAlt className="file-icon" />
242
+ <div className="file-info">
243
+ <span className="file-name">{fileWrapper.file.name}</span>
244
+ <span className="file-size">{formatFileSize(fileWrapper.file.size)}</span>
245
+ </div>
246
+ {isUploading && (
247
+ <div className="progress-bar-container">
248
+ <div className="progress-bar" style={{ width: `${fileWrapper.progress}%` }}></div>
249
+ </div>
250
+ )}
251
+ <button className="cancel-file-btn" onClick={() => handleRemoveFile(fileWrapper.id)} disabled={isUploading}>
252
+ <FaTimes />
253
+ </button>
254
+ </div>
255
+ ))}
256
+ </div>
257
+ )}
258
+
259
+ <div className="dialog-actions">
260
+ <Button
261
+ disabled={isUploading}
262
+ onClick={handleReset}
263
+ sx={{ color: "#2196f3" }}
264
+ >
265
+ Reset
266
+ </Button>
267
+ <Button
268
+ disabled={isUploading}
269
+ onClick={handleAdd}
270
+ variant="contained"
271
+ color="success"
272
+ >
273
+ Add
274
+ </Button>
275
+ </div>
276
+ </div>
277
+ </div>
278
+ </div>
279
+ );
280
+ }
281
+
282
+ export default AddFilesDialog;
frontend/src/Components/AiComponents/Notifications/Notification.css ADDED
@@ -0,0 +1,379 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .notification-container {
2
+ position: fixed;
3
+ z-index: 9999;
4
+ display: flex;
5
+ flex-direction: column;
6
+ gap: var(--spacing, 10px);
7
+ pointer-events: none;
8
+ }
9
+
10
+ .notification-list {
11
+ display: flex;
12
+ flex-direction: column;
13
+ gap: var(--spacing, 10px);
14
+ }
15
+
16
+ .notification-list.collapsed {
17
+ gap: 0;
18
+ }
19
+
20
+ .notification-list.collapsed .notification:not(:first-child) {
21
+ margin-top: -80%;
22
+ opacity: 0.3;
23
+ transform: scale(0.95);
24
+ }
25
+
26
+ /* Position variations */
27
+ .position-top-left {
28
+ top: var(--offset-y);
29
+ left: var(--offset-x);
30
+ align-items: flex-start;
31
+ }
32
+
33
+ .position-top-center {
34
+ top: var(--offset-y);
35
+ left: 50%;
36
+ transform: translateX(-50%);
37
+ align-items: center;
38
+ }
39
+
40
+ .position-top-right {
41
+ top: var(--offset-y);
42
+ right: var(--offset-x);
43
+ align-items: flex-end;
44
+ }
45
+
46
+ .position-bottom-left {
47
+ bottom: var(--offset-y);
48
+ left: var(--offset-x);
49
+ align-items: flex-start;
50
+ }
51
+
52
+ .position-bottom-center {
53
+ bottom: var(--offset-y);
54
+ left: 50%;
55
+ transform: translateX(-50%);
56
+ align-items: center;
57
+ }
58
+
59
+ .position-bottom-right {
60
+ bottom: var(--offset-y);
61
+ right: var(--offset-x);
62
+ align-items: flex-end;
63
+ }
64
+
65
+ .position-center {
66
+ top: 50%;
67
+ left: 50%;
68
+ transform: translate(-50%, -50%);
69
+ align-items: center;
70
+ }
71
+
72
+ /* Stack direction */
73
+ .stack-up {
74
+ flex-direction: column-reverse;
75
+ }
76
+
77
+ .stack-up .notification-list {
78
+ flex-direction: column-reverse;
79
+ }
80
+
81
+ /* Base notification styles */
82
+ .notification {
83
+ background: white;
84
+ border-radius: 8px;
85
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
86
+ min-width: 300px;
87
+ max-width: 500px;
88
+ position: relative;
89
+ overflow: hidden;
90
+ pointer-events: all;
91
+ transition: all 0.3s ease;
92
+ }
93
+
94
+ .notification-content {
95
+ padding: 16px;
96
+ display: flex;
97
+ gap: 12px;
98
+ align-items: flex-start;
99
+ }
100
+
101
+ .notification-icon {
102
+ flex-shrink: 0;
103
+ font-size: 24px;
104
+ display: flex;
105
+ align-items: center;
106
+ }
107
+
108
+ .notification-body {
109
+ flex: 1;
110
+ min-width: 0;
111
+ }
112
+
113
+ .notification-title {
114
+ font-weight: 600;
115
+ font-size: 16px;
116
+ margin-bottom: 4px;
117
+ color: #333;
118
+ word-wrap: break-word;
119
+ }
120
+
121
+ .notification-message {
122
+ font-size: 14px;
123
+ color: #666;
124
+ line-height: 1.5;
125
+ word-wrap: break-word;
126
+ }
127
+
128
+ .notification-actions {
129
+ display: flex;
130
+ gap: 8px;
131
+ margin-top: 12px;
132
+ flex-wrap: wrap;
133
+ }
134
+
135
+ .notification-action {
136
+ padding: 6px 12px;
137
+ border: none;
138
+ border-radius: 4px;
139
+ font-size: 14px;
140
+ font-weight: 500;
141
+ cursor: pointer;
142
+ transition: all 0.2s;
143
+ background: #1976d2;
144
+ color: white;
145
+ }
146
+
147
+ .notification-action:hover {
148
+ background: #1565c0;
149
+ transform: translateY(-1px);
150
+ }
151
+
152
+ .notification-close {
153
+ background: none;
154
+ border: none;
155
+ color: #999;
156
+ cursor: pointer;
157
+ font-size: 18px;
158
+ padding: 4px;
159
+ transition: color 0.2s;
160
+ display: flex;
161
+ align-items: center;
162
+ }
163
+
164
+ .notification-close:hover {
165
+ color: #666;
166
+ }
167
+
168
+ .notification-footer {
169
+ padding: 12px 16px;
170
+ background: #f5f5f5;
171
+ border-top: 1px solid #e0e0e0;
172
+ font-size: 12px;
173
+ color: #666;
174
+ }
175
+
176
+ /* Notification types */
177
+ .notification-success {
178
+ border-left: 4px solid #4caf50;
179
+ }
180
+
181
+ .notification-success .notification-icon {
182
+ color: #4caf50;
183
+ }
184
+
185
+ .notification-error {
186
+ border-left: 4px solid #f44336;
187
+ }
188
+
189
+ .notification-error .notification-icon {
190
+ color: #f44336;
191
+ }
192
+
193
+ .notification-warning {
194
+ border-left: 4px solid #ff9800;
195
+ }
196
+
197
+ .notification-warning .notification-icon {
198
+ color: #ff9800;
199
+ }
200
+
201
+ .notification-info {
202
+ border-left: 4px solid #2196f3;
203
+ }
204
+
205
+ .notification-info .notification-icon {
206
+ color: #2196f3;
207
+ }
208
+
209
+ /* Dark theme */
210
+ .theme-dark .notification {
211
+ background: #1e1e1e;
212
+ color: #fff;
213
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.5);
214
+ }
215
+
216
+ .theme-dark .notification-title {
217
+ color: #fff;
218
+ }
219
+
220
+ .theme-dark .notification-message {
221
+ color: #ccc;
222
+ }
223
+
224
+ .theme-dark .notification-close {
225
+ color: #666;
226
+ }
227
+
228
+ .theme-dark .notification-close:hover {
229
+ color: #999;
230
+ }
231
+
232
+ .theme-dark .notification-footer {
233
+ background: #2a2a2a;
234
+ border-top-color: #444;
235
+ color: #999;
236
+ }
237
+
238
+ /* Animations */
239
+ /* Slide animation */
240
+ .animation-slide {
241
+ animation: slideIn 0.3s ease-out forwards;
242
+ animation-delay: var(--animation-delay, 0s);
243
+ }
244
+
245
+ @keyframes slideIn {
246
+ from {
247
+ transform: translateX(100%);
248
+ opacity: 0;
249
+ }
250
+ to {
251
+ transform: translateX(0);
252
+ opacity: 1;
253
+ }
254
+ }
255
+
256
+ .position-top-left .animation-slide,
257
+ .position-bottom-left .animation-slide {
258
+ animation-name: slideInLeft;
259
+ }
260
+
261
+ @keyframes slideInLeft {
262
+ from {
263
+ transform: translateX(-100%);
264
+ opacity: 0;
265
+ }
266
+ to {
267
+ transform: translateX(0);
268
+ opacity: 1;
269
+ }
270
+ }
271
+
272
+ /* Fade animation */
273
+ .animation-fade {
274
+ animation: fadeIn 0.3s ease-out forwards;
275
+ animation-delay: var(--animation-delay, 0s);
276
+ }
277
+
278
+ @keyframes fadeIn {
279
+ from {
280
+ opacity: 0;
281
+ }
282
+ to {
283
+ opacity: 1;
284
+ }
285
+ }
286
+
287
+ /* Zoom animation */
288
+ .animation-zoom {
289
+ animation: zoomIn 0.3s ease-out forwards;
290
+ animation-delay: var(--animation-delay, 0s);
291
+ }
292
+
293
+ @keyframes zoomIn {
294
+ from {
295
+ transform: scale(0.8);
296
+ opacity: 0;
297
+ }
298
+ to {
299
+ transform: scale(1);
300
+ opacity: 1;
301
+ }
302
+ }
303
+
304
+ /* Bounce animation */
305
+ .animation-bounce {
306
+ animation: bounceIn 0.6s cubic-bezier(0.68, -0.55, 0.265, 1.55) forwards;
307
+ animation-delay: var(--animation-delay, 0s);
308
+ }
309
+
310
+ @keyframes bounceIn {
311
+ 0% {
312
+ transform: translateY(-100%);
313
+ opacity: 0;
314
+ }
315
+ 60% {
316
+ transform: translateY(10%);
317
+ opacity: 1;
318
+ }
319
+ 100% {
320
+ transform: translateY(0);
321
+ opacity: 1;
322
+ }
323
+ }
324
+
325
+ /* Progress bar */
326
+ .notification-progress {
327
+ position: absolute;
328
+ bottom: 0;
329
+ left: 0;
330
+ height: 3px;
331
+ background: currentColor;
332
+ opacity: 0.3;
333
+ animation: progress linear forwards;
334
+ animation-duration: var(--duration);
335
+ }
336
+
337
+ @keyframes progress {
338
+ from {
339
+ width: 100%;
340
+ }
341
+ to {
342
+ width: 0%;
343
+ }
344
+ }
345
+
346
+ /* Collapse toggle */
347
+ .notification-collapse-toggle {
348
+ align-self: center;
349
+ padding: 8px 16px;
350
+ background: #1976d2;
351
+ color: white;
352
+ border: none;
353
+ border-radius: 20px;
354
+ font-size: 14px;
355
+ cursor: pointer;
356
+ pointer-events: all;
357
+ margin-bottom: 8px;
358
+ transition: all 0.2s;
359
+ }
360
+
361
+ .notification-collapse-toggle:hover {
362
+ background: #1565c0;
363
+ transform: translateY(-1px);
364
+ }
365
+
366
+ /* Responsive */
367
+ @media (max-width: 600px) {
368
+ .notification {
369
+ min-width: calc(100vw - 40px);
370
+ max-width: calc(100vw - 40px);
371
+ }
372
+
373
+ .position-top-center,
374
+ .position-bottom-center {
375
+ transform: none;
376
+ left: 20px;
377
+ right: 20px;
378
+ }
379
+ }
frontend/src/Components/AiComponents/Notifications/Notification.js ADDED
@@ -0,0 +1,242 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState, useEffect, useRef, useCallback } from 'react';
2
+ import {
3
+ FaTimes,
4
+ FaCheckCircle,
5
+ FaExclamationCircle,
6
+ FaInfoCircle,
7
+ FaExclamationTriangle
8
+ } from 'react-icons/fa';
9
+ import './Notification.css';
10
+
11
+ const Notification = ({
12
+ notifications = [],
13
+ position = 'top-right',
14
+ animation = 'slide',
15
+ stackDirection = 'down',
16
+ maxNotifications = 5,
17
+ spacing = 10,
18
+ offset = { x: 20, y: 20 },
19
+ onDismiss,
20
+ onAction,
21
+ autoStackCollapse = false,
22
+ theme = 'light'
23
+ }) => {
24
+ const [internalNotifications, setInternalNotifications] = useState([]);
25
+ const [collapsed, setCollapsed] = useState(false);
26
+ const timersRef = useRef({});
27
+
28
+ const handleDismiss = useCallback((id) => {
29
+ if (timersRef.current[id]) {
30
+ clearTimeout(timersRef.current[id]);
31
+ delete timersRef.current[id];
32
+ }
33
+ onDismiss?.(id);
34
+ }, [onDismiss]);
35
+
36
+ useEffect(() => {
37
+ // Update internal notifications
38
+ const processedNotifications = notifications.slice(
39
+ stackDirection === 'up' ? -maxNotifications : 0,
40
+ stackDirection === 'up' ? undefined : maxNotifications
41
+ );
42
+
43
+ setInternalNotifications(processedNotifications);
44
+
45
+ // Keep track of current timer IDs for this effect
46
+ const currentTimerIds = [];
47
+
48
+ // Set up auto-dismiss timers
49
+ processedNotifications.forEach(notification => {
50
+ if (notification.autoDismiss && notification.duration && !timersRef.current[notification.id]) {
51
+ const timerId = setTimeout(() => {
52
+ handleDismiss(notification.id);
53
+ }, notification.duration);
54
+
55
+ timersRef.current[notification.id] = timerId;
56
+ currentTimerIds.push(notification.id);
57
+ }
58
+ });
59
+
60
+ // Cleanup function
61
+ return () => {
62
+ // Use the captured timer IDs and current ref
63
+ const timers = timersRef.current;
64
+
65
+ // Clear timers for notifications that were removed
66
+ Object.keys(timers).forEach(id => {
67
+ if (!processedNotifications.find(n => n.id === id)) {
68
+ clearTimeout(timers[id]);
69
+ delete timers[id];
70
+ }
71
+ });
72
+ };
73
+ }, [notifications, maxNotifications, stackDirection, handleDismiss]);
74
+
75
+ const handleAction = (notificationId, actionId, actionData) => {
76
+ onAction?.(notificationId, actionId, actionData);
77
+ };
78
+
79
+ const getIcon = (type, customIcon) => {
80
+ if (customIcon) return customIcon;
81
+
82
+ switch (type) {
83
+ case 'success':
84
+ return <FaCheckCircle />;
85
+ case 'error':
86
+ return <FaExclamationCircle />;
87
+ case 'warning':
88
+ return <FaExclamationTriangle />;
89
+ case 'info':
90
+ return <FaInfoCircle />;
91
+ default:
92
+ return null;
93
+ }
94
+ };
95
+
96
+ const getPositionClasses = () => {
97
+ const classes = ['notification-container'];
98
+
99
+ // Position classes
100
+ switch (position) {
101
+ case 'top-left':
102
+ classes.push('position-top-left');
103
+ break;
104
+ case 'top-center':
105
+ classes.push('position-top-center');
106
+ break;
107
+ case 'top-right':
108
+ classes.push('position-top-right');
109
+ break;
110
+ case 'bottom-left':
111
+ classes.push('position-bottom-left');
112
+ break;
113
+ case 'bottom-center':
114
+ classes.push('position-bottom-center');
115
+ break;
116
+ case 'bottom-right':
117
+ classes.push('position-bottom-right');
118
+ break;
119
+ case 'center':
120
+ classes.push('position-center');
121
+ break;
122
+ default:
123
+ classes.push('position-top-right');
124
+ }
125
+
126
+ // Stack direction
127
+ if (stackDirection === 'up') {
128
+ classes.push('stack-up');
129
+ }
130
+
131
+ // Theme
132
+ classes.push(`theme-${theme}`);
133
+
134
+ return classes.join(' ');
135
+ };
136
+
137
+ const getAnimationClass = (index) => {
138
+ return `animation-${animation} animation-${animation}-${index}`;
139
+ };
140
+
141
+ const containerStyle = {
142
+ '--spacing': `${spacing}px`,
143
+ '--offset-x': `${offset.x}px`,
144
+ '--offset-y': `${offset.y}px`,
145
+ };
146
+
147
+ if (internalNotifications.length === 0) return null;
148
+
149
+ return (
150
+ <div
151
+ className={getPositionClasses()}
152
+ style={containerStyle}
153
+ >
154
+ {autoStackCollapse && internalNotifications.length > 3 && (
155
+ <button
156
+ className="notification-collapse-toggle"
157
+ onClick={() => setCollapsed(!collapsed)}
158
+ >
159
+ {collapsed ? `Show ${internalNotifications.length} notifications` : 'Collapse'}
160
+ </button>
161
+ )}
162
+
163
+ <div className={`notification-list ${collapsed ? 'collapsed' : ''}`}>
164
+ {internalNotifications.map((notification, index) => (
165
+ <div
166
+ key={notification.id}
167
+ className={`notification notification-${notification.type || 'default'} ${getAnimationClass(index)} ${notification.className || ''}`}
168
+ style={{
169
+ '--animation-delay': `${index * 0.05}s`,
170
+ ...notification.style
171
+ }}
172
+ >
173
+ {notification.showProgress && notification.duration && (
174
+ <div
175
+ className="notification-progress"
176
+ style={{
177
+ '--duration': `${notification.duration}ms`
178
+ }}
179
+ />
180
+ )}
181
+
182
+ <div className="notification-content">
183
+ {(notification.icon !== false) && (
184
+ <div className="notification-icon">
185
+ {getIcon(notification.type, notification.icon)}
186
+ </div>
187
+ )}
188
+
189
+ <div className="notification-body">
190
+ {notification.title && (
191
+ <div className="notification-title">{notification.title}</div>
192
+ )}
193
+
194
+ {notification.message && (
195
+ <div className="notification-message">
196
+ {typeof notification.message === 'string'
197
+ ? notification.message
198
+ : notification.message
199
+ }
200
+ </div>
201
+ )}
202
+
203
+ {notification.actions && notification.actions.length > 0 && (
204
+ <div className="notification-actions">
205
+ {notification.actions.map((action) => (
206
+ <button
207
+ key={action.id}
208
+ className={`notification-action ${action.className || ''}`}
209
+ onClick={() => handleAction(notification.id, action.id, action.data)}
210
+ style={action.style}
211
+ >
212
+ {action.label}
213
+ </button>
214
+ ))}
215
+ </div>
216
+ )}
217
+ </div>
218
+
219
+ {notification.dismissible !== false && (
220
+ <button
221
+ className="notification-close"
222
+ onClick={() => handleDismiss(notification.id)}
223
+ aria-label="Dismiss notification"
224
+ >
225
+ <FaTimes />
226
+ </button>
227
+ )}
228
+ </div>
229
+
230
+ {notification.footer && (
231
+ <div className="notification-footer">
232
+ {notification.footer}
233
+ </div>
234
+ )}
235
+ </div>
236
+ ))}
237
+ </div>
238
+ </div>
239
+ );
240
+ };
241
+
242
+ export default Notification;
frontend/src/Components/AiComponents/Notifications/useNotification.js ADDED
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState, useCallback } from 'react';
2
+
3
+ export const useNotification = () => {
4
+ const [notifications, setNotifications] = useState([]);
5
+
6
+ const addNotification = useCallback((notification) => {
7
+ const id = notification.id || `notification-${Date.now()}-${Math.random()}`;
8
+ const newNotification = {
9
+ id,
10
+ type: 'info',
11
+ dismissible: true,
12
+ autoDismiss: false,
13
+ duration: 5000,
14
+ showProgress: false,
15
+ ...notification
16
+ };
17
+
18
+ setNotifications(prev => [...prev, newNotification]);
19
+ return id;
20
+ }, []);
21
+
22
+ const removeNotification = useCallback((id) => {
23
+ setNotifications(prev => prev.filter(n => n.id !== id));
24
+ }, []);
25
+
26
+ const clearAll = useCallback(() => {
27
+ setNotifications([]);
28
+ }, []);
29
+
30
+ const updateNotification = useCallback((id, updates) => {
31
+ setNotifications(prev =>
32
+ prev.map(n => n.id === id ? { ...n, ...updates } : n)
33
+ );
34
+ }, []);
35
+
36
+ return {
37
+ notifications,
38
+ addNotification,
39
+ removeNotification,
40
+ clearAll,
41
+ updateNotification
42
+ };
43
+ };
frontend/src/Components/AiComponents/Sidebars/LeftSideBar.js ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState, useEffect } from 'react';
2
+ import { FaBars } from 'react-icons/fa';
3
+ import './LeftSidebar.css';
4
+
5
+ function LeftSidebar() {
6
+ const [isLeftSidebarOpen, setLeftSidebarOpen] = useState(
7
+ localStorage.getItem("leftSidebarState") === "true"
8
+ );
9
+
10
+ useEffect(() => {
11
+ localStorage.setItem("leftSidebarState", isLeftSidebarOpen);
12
+ }, [isLeftSidebarOpen]);
13
+
14
+ const toggleLeftSidebar = () => {
15
+ setLeftSidebarOpen(!isLeftSidebarOpen);
16
+ };
17
+
18
+ return (
19
+ <>
20
+ <nav className={`left-side-bar ${isLeftSidebarOpen ? 'open' : 'closed'}`}>
21
+ ... (left sidebar content)
22
+ </nav>
23
+ {!isLeftSidebarOpen && (
24
+ <button className='toggle-btn left-toggle' onClick={toggleLeftSidebar}>
25
+ <FaBars />
26
+ </button>
27
+ )}
28
+ </>
29
+ );
30
+ // return (
31
+ // <div className="left-side-bar-placeholder">
32
+ // {/* Left sidebar is currently disabled. Uncomment the code in LeftSidebar.js to enable it. */}
33
+ // Left sidebar is disabled.
34
+ // </div>
35
+ // );
36
+ }
37
+
38
+ export default LeftSidebar;
frontend/src/Components/AiComponents/Sidebars/LeftSidebar.css ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* Left Sidebar Specific */
2
+ .left-side-bar {
3
+ background-color: var(--primary-color);
4
+ color: var(--text-color);
5
+ display: flex;
6
+ flex-direction: column;
7
+ padding: 1rem;
8
+ transition: transform var(--transition-speed);
9
+ z-index: 1000;
10
+ position: absolute;
11
+ top: 0;
12
+ left: 0;
13
+ height: 100%;
14
+ }
15
+
16
+ .left-side-bar.closed {
17
+ transform: translateX(-100%);
18
+ }
19
+
20
+ /* Toggle Button for Left Sidebar */
21
+ .toggle-btn.left-toggle {
22
+ background-color: var(--primary-color);
23
+ color: var(--text-color);
24
+ border: none;
25
+ padding: 0.5rem;
26
+ border-radius: 4px;
27
+ cursor: pointer;
28
+ transition: background-color var(--transition-speed);
29
+ z-index: 1100;
30
+ position: fixed;
31
+ top: 50%;
32
+ left: 0;
33
+ transform: translate(-50%, -50%);
34
+ }
35
+
36
+ /* Responsive Adjustments for Left Sidebar */
37
+ @media (max-width: 768px) {
38
+ .left-side-bar {
39
+ width: 200px;
40
+ }
41
+ }
42
+
43
+ @media (max-width: 576px) {
44
+ .left-side-bar {
45
+ width: 100%;
46
+ height: 100%;
47
+ top: 0;
48
+ left: 0;
49
+ transform: translateY(-100%);
50
+ }
51
+ .left-side-bar.open {
52
+ transform: translateY(0);
53
+ }
54
+ .toggle-btn.left-toggle {
55
+ top: auto;
56
+ bottom: 1rem;
57
+ left: 1rem;
58
+ }
59
+ }
frontend/src/Components/AiComponents/Sidebars/RightSidebar.css ADDED
@@ -0,0 +1,138 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ :root {
2
+ /* Dark theme variables */
3
+ --sidebar-background: #2b2b2b;
4
+ --text-light: #eee;
5
+ --border-dark: #333;
6
+ }
7
+
8
+ /* Main sidebar container */
9
+ .right-side-bar {
10
+ display: flex;
11
+ flex-direction: column;
12
+ position: fixed;
13
+ top: 0;
14
+ right: 0;
15
+ height: 100%;
16
+ background-color: var(--sidebar-background); /* Keep background uniform */
17
+ color: var(--text-light);
18
+ box-shadow: -2px 0 8px rgba(0, 0, 0, 0.5);
19
+ transition: width 0.4s ease;
20
+ overflow-y: auto;
21
+ z-index: 1000;
22
+ }
23
+
24
+ /* Sidebar resizing */
25
+ .right-side-bar.resizing {
26
+ transition: none;
27
+ }
28
+
29
+ /* When the sidebar is closed */
30
+ .right-side-bar.closed {
31
+ width: 0;
32
+ overflow: hidden;
33
+ }
34
+
35
+ /* Sidebar header styling */
36
+ .sidebar-header {
37
+ display: flex;
38
+ align-items: center;
39
+ justify-content: space-between;
40
+ padding: 16px;
41
+ border-bottom: 3px solid var(--border-dark);
42
+ }
43
+
44
+ .sidebar-header h3 {
45
+ margin: 0;
46
+ font-size: 1.2rem;
47
+ }
48
+
49
+ /* Close button styling */
50
+ .close-btn {
51
+ background: none;
52
+ border: none;
53
+ padding: 6px;
54
+ color: var(--text-color);
55
+ font-size: 1.2rem;
56
+ cursor: pointer;
57
+ transition: color var(--transition-speed);
58
+ }
59
+
60
+ .close-btn:hover {
61
+ background: rgba(255, 255, 255, 0.1);
62
+ color: white;
63
+ }
64
+
65
+ /* Ensure the sidebar background remains uniform */
66
+ .sidebar-content {
67
+ padding: 16px;
68
+ background: transparent;
69
+ overflow-x: hidden;
70
+ overflow-y: auto;
71
+ }
72
+
73
+ /* Also clear any default marker via the pseudo-element */
74
+ .nav-links.no-bullets li::marker {
75
+ content: "";
76
+ }
77
+
78
+ /* Lay out each task item using flex so that the icon and text align */
79
+ .task-item {
80
+ display: flex;
81
+ align-items: flex-start;
82
+ margin-bottom: 1rem;
83
+ }
84
+
85
+ /* Icon span: fixed width and margin for spacing */
86
+ .task-icon {
87
+ flex-shrink: 0;
88
+ margin-right: 1rem;
89
+ }
90
+
91
+ /* Task list text */
92
+ .task-text {
93
+ white-space: pre-wrap;
94
+ }
95
+
96
+ /* Resizer for sidebar width adjustment */
97
+ .resizer {
98
+ position: absolute;
99
+ left: 0;
100
+ top: 0;
101
+ width: 5px;
102
+ height: 100%;
103
+ cursor: ew-resize;
104
+ }
105
+
106
+ /* Toggle button (when sidebar is closed) */
107
+ .toggle-btn.right-toggle {
108
+ position: fixed;
109
+ top: 50%;
110
+ right: 0;
111
+ transform: translateY(-50%);
112
+ background-color: var(--dark-surface);
113
+ color: var(--text-light);
114
+ border: none;
115
+ padding: 8px;
116
+ cursor: pointer;
117
+ z-index: 1001;
118
+ box-shadow: -2px 0 4px rgba(0, 0, 0, 0.5);
119
+ }
120
+
121
+ .spin {
122
+ animation: spin 1s linear infinite;
123
+ color: #328bff;
124
+ }
125
+
126
+ .checkmark {
127
+ color: #03c203;
128
+ }
129
+
130
+ .x {
131
+ color: #d10808;
132
+ }
133
+
134
+ /* Keyframes for the spinner animation */
135
+ @keyframes spin {
136
+ from { transform: rotate(0deg); }
137
+ to { transform: rotate(360deg); }
138
+ }
frontend/src/Components/AiComponents/Sidebars/RightSidebar.js ADDED
@@ -0,0 +1,142 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useRef } from 'react';
2
+ import { FaTimes, FaCheck, FaSpinner } from 'react-icons/fa';
3
+ import { BsChevronLeft } from 'react-icons/bs';
4
+ import CircularProgress from '@mui/material/CircularProgress';
5
+ import Sources from '../ChatComponents/Sources';
6
+ import Evaluate from '../ChatComponents/Evaluate';
7
+ import './RightSidebar.css';
8
+
9
+ function RightSidebar({
10
+ isOpen,
11
+ rightSidebarWidth,
12
+ setRightSidebarWidth,
13
+ toggleRightSidebar,
14
+ sidebarContent,
15
+ tasks = [],
16
+ tasksLoading,
17
+ sources = [],
18
+ sourcesLoading,
19
+ onTaskClick,
20
+ onSourceClick,
21
+ evaluation
22
+ }) {
23
+ const minWidth = 200;
24
+ const maxWidth = 450;
25
+ const sidebarRef = useRef(null);
26
+
27
+ // Called when the user starts resizing the sidebar.
28
+ const startResize = (e) => {
29
+ e.preventDefault();
30
+ sidebarRef.current.classList.add("resizing"); // Add the "resizing" class to the sidebar when resizing
31
+ document.addEventListener("mousemove", resizeSidebar);
32
+ document.addEventListener("mouseup", stopResize);
33
+ };
34
+
35
+ const resizeSidebar = (e) => {
36
+ let newWidth = window.innerWidth - e.clientX;
37
+ if (newWidth < minWidth) newWidth = minWidth;
38
+ if (newWidth > maxWidth) newWidth = maxWidth;
39
+ setRightSidebarWidth(newWidth);
40
+ };
41
+
42
+ const stopResize = () => {
43
+ sidebarRef.current.classList.remove("resizing"); // Remove the "resizing" class from the sidebar when resizing stops
44
+ document.removeEventListener("mousemove", resizeSidebar);
45
+ document.removeEventListener("mouseup", stopResize);
46
+ };
47
+
48
+ // Default handler for source clicks: open the link in a new tab.
49
+ const handleSourceClick = (source) => {
50
+ if (source && source.link) {
51
+ window.open(source.link, '_blank');
52
+ }
53
+ };
54
+
55
+ // Helper function to return the proper icon based on task status.
56
+ const getTaskIcon = (task) => {
57
+ // If the task is a simple string, default to the completed icon.
58
+ if (typeof task === 'string') {
59
+ return <FaCheck />;
60
+ }
61
+ // Use the status field to determine which icon to render.
62
+ switch (task.status) {
63
+ case 'RUNNING':
64
+ // FaSpinner is used for running tasks. The CSS class "spin" can be defined to add animation.
65
+ return <FaSpinner className="spin"/>;
66
+ case 'DONE':
67
+ return <FaCheck className="checkmark" />;
68
+ case 'FAILED':
69
+ return <FaTimes className="x" />;
70
+ default:
71
+ return <FaCheck />;
72
+ }
73
+ };
74
+
75
+ return (
76
+ <>
77
+ <nav
78
+ ref={sidebarRef}
79
+ className={`right-side-bar ${isOpen ? "open" : "closed"}`}
80
+ style={{ width: isOpen ? rightSidebarWidth : 0 }}
81
+ >
82
+ <div className="sidebar-header">
83
+ <h3>
84
+ {sidebarContent === "sources"
85
+ ? "Sources"
86
+ : sidebarContent === "evaluate"
87
+ ? "Evaluation"
88
+ : "Tasks"}
89
+ </h3>
90
+ <button className="close-btn" onClick={toggleRightSidebar}>
91
+ <FaTimes />
92
+ </button>
93
+ </div>
94
+ <div className="sidebar-content">
95
+ {sidebarContent === "sources" ? ( // If the sidebar content is "sources", show the sources component
96
+ sourcesLoading ? (
97
+ <div className="tasks-loading">
98
+ <CircularProgress size={20} sx={{ color: '#ccc' }} />
99
+ <span className="loading-tasks-text">Generating sources...</span>
100
+ </div>
101
+ ) : (
102
+ <Sources sources={sources} handleSourceClick={onSourceClick || handleSourceClick} />
103
+ )
104
+ )
105
+ // If the sidebar content is "evaluate", show the evaluation component
106
+ : sidebarContent === "evaluate" ? (
107
+ <Evaluate evaluation={evaluation} />
108
+ ) : (
109
+ // Otherwise, show tasks
110
+ tasksLoading ? (
111
+ <div className="tasks-loading">
112
+ <CircularProgress size={20} sx={{ color: '#ccc' }} />
113
+ <span className="loading-tasks-text">Generating tasks...</span>
114
+ </div>
115
+ ) : (
116
+ <ul className="nav-links" style={{ listStyle: 'none', padding: 0 }}>
117
+ {tasks.map((task, index) => (
118
+ <li key={index} className="task-item">
119
+ <span className="task-icon">
120
+ {getTaskIcon(task)}
121
+ </span>
122
+ <span className="task-text">
123
+ {typeof task === 'string' ? task : task.task}
124
+ </span>
125
+ </li>
126
+ ))}
127
+ </ul>
128
+ )
129
+ )}
130
+ </div>
131
+ <div className="resizer" onMouseDown={startResize}></div>
132
+ </nav>
133
+ {!isOpen && (
134
+ <button className="toggle-btn right-toggle" onClick={toggleRightSidebar}>
135
+ <BsChevronLeft />
136
+ </button>
137
+ )}
138
+ </>
139
+ );
140
+ }
141
+
142
+ export default RightSidebar;
frontend/src/Components/AiPage.css ADDED
@@ -0,0 +1,434 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* Define a modern dark theme */
2
+ :root {
3
+ --dark-surface: #190e10; /* Deep, dark maroon base */
4
+ --primary-color: #2b2b2b; /* A dark gray for sidebars and buttons */
5
+ --secondary-color: #03dac6; /* A cool teal for accents */
6
+ --accent-color: #444a89; /* A warm accent for highlights and borders */
7
+ --text-color: #e0e0e0; /* Off-white text */
8
+ --hover-bg: #3a3a3a; /* Slightly lighter for hover effects */
9
+ --transition-speed: 0.25s; /* Speed of transitions */
10
+ --search-bar: #21212f; /* Search bar background */
11
+ }
12
+
13
+ /* Global font settings */
14
+ html, body {
15
+ font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
16
+ margin: 0;
17
+ padding: 0;
18
+ background: var(--dark-surface);
19
+ }
20
+
21
+ /* Main container styling */
22
+ .app-container {
23
+ background: var(--dark-surface);
24
+ color: var(--text-color);
25
+ display: flex;
26
+ min-height: 100vh;
27
+ position: relative;
28
+ overflow-y: auto;
29
+ }
30
+
31
+ /* Main Content */
32
+ .main-content {
33
+ display: flex;
34
+ flex-direction: column;
35
+ align-items: center;
36
+ justify-content: center;
37
+ flex-grow: 1;
38
+ padding: 2rem;
39
+ transition: 0.1s;
40
+ width: 99%;
41
+ max-width: 900px;
42
+ margin: 0 auto;
43
+ }
44
+
45
+ /* Search Area for Initial Mode */
46
+ .search-area h1 {
47
+ margin-bottom: 1.5rem;
48
+ display: flex;
49
+ flex-direction: column;
50
+ align-items: center;
51
+ justify-content: center;
52
+ }
53
+
54
+ .search-area {
55
+ width: 83%;
56
+ }
57
+
58
+ .search-bar {
59
+ position: relative;
60
+ width: 100%;
61
+ border-radius: 0.35rem;
62
+ background-color: var(--search-bar);
63
+ }
64
+
65
+ .search-input-wrapper {
66
+ padding: 0rem 0.6rem 4.15rem 0.6rem;
67
+ }
68
+
69
+ .search-input {
70
+ width: 100%;
71
+ max-height: 200px;
72
+ overflow-y: auto;
73
+ font-size: 1.2rem;
74
+ border: none;
75
+ background-color: transparent;
76
+ color: var(--text-color);
77
+ line-height: 1.4;
78
+ padding: 0.65rem 0.65rem;
79
+ resize: none;
80
+ white-space: pre-wrap;
81
+ }
82
+
83
+ .search-input:focus {
84
+ outline: none;
85
+ }
86
+
87
+ .search-input::placeholder {
88
+ color: #888;
89
+ }
90
+
91
+ .icon-container {
92
+ position: absolute;
93
+ bottom: 0.25rem;
94
+ left: 0;
95
+ right: 0;
96
+ height: 3rem;
97
+ display: flex;
98
+ justify-content: space-between;
99
+ align-items: center;
100
+ padding: 0 0.75rem;
101
+ }
102
+
103
+ .settings-btn,
104
+ .add-btn,
105
+ .send-btn {
106
+ background: transparent;
107
+ border: none;
108
+ color: var(--text-color);
109
+ cursor: pointer;
110
+ }
111
+
112
+ .settings-btn svg,
113
+ .add-btn svg,
114
+ .send-btn svg {
115
+ font-size: 1.45rem;
116
+ }
117
+
118
+ .settings-btn:hover,
119
+ .add-btn:hover,
120
+ .send-btn:hover {
121
+ color: #888;
122
+ }
123
+
124
+ /* Stop button */
125
+ button.chat-send-btn.stop-btn,
126
+ button.send-btn.stop-btn {
127
+ background-color: var(--text-color) !important;
128
+ border-radius: 50%;
129
+ width: 28.5px;
130
+ height: 28.5px;
131
+ border: none;
132
+ display: flex;
133
+ align-items: center;
134
+ justify-content: center;
135
+ cursor: pointer;
136
+ padding: 0;
137
+ margin: 0 0 10px;
138
+ }
139
+
140
+ button.chat-send-btn.stop-btn:hover,
141
+ button.send-btn.stop-btn:hover {
142
+ background-color: #888 !important;
143
+ }
144
+
145
+ /* Chat Mode Search Bar and Textarea Styling */
146
+ .floating-chat-search-bar {
147
+ position: fixed;
148
+ bottom: 1.5rem;
149
+ left: 50%;
150
+ transform: translateX(-50%);
151
+ width: 48%;
152
+ background-color: var(--search-bar);
153
+ border-radius: 0.35rem;
154
+ }
155
+
156
+ .floating-chat-search-bar::after {
157
+ content: "";
158
+ position: absolute;
159
+ left: 0;
160
+ right: 0;
161
+ bottom: -1.5rem;
162
+ height: 1.5rem;
163
+ background-color: var(--dark-surface);
164
+ }
165
+
166
+ .chat-search-input-wrapper {
167
+ padding: 0.25rem 0.6rem;
168
+ }
169
+
170
+ .chat-search-input {
171
+ width: 100%;
172
+ min-width: 700px;
173
+ max-height: 200px;
174
+ overflow-y: auto;
175
+ font-size: 1.2rem;
176
+ border: none;
177
+ background-color: transparent;
178
+ color: var(--text-color);
179
+ line-height: 1.4;
180
+ padding: 0.65rem 3.25rem 0.65rem 5.5rem;
181
+ resize: none;
182
+ white-space: pre-wrap;
183
+ }
184
+
185
+ .left-icons {
186
+ display: flex;
187
+ align-items: center;
188
+ gap: 0.15rem;
189
+ }
190
+
191
+ .chat-search-input:focus {
192
+ outline: none;
193
+ }
194
+
195
+ .chat-icon-container {
196
+ position: absolute;
197
+ left: 0;
198
+ right: 0;
199
+ bottom: 0;
200
+ height: 3rem;
201
+ display: flex;
202
+ justify-content: space-between;
203
+ align-items: center;
204
+ padding: 0 0.75rem;
205
+ pointer-events: none;
206
+ }
207
+
208
+ /* Re-enable pointer events on the actual buttons so they remain clickable */
209
+ .chat-icon-container button,
210
+ .chat-left-icons {
211
+ pointer-events: auto;
212
+ }
213
+
214
+ .chat-left-icons {
215
+ display: flex;
216
+ align-items: center;
217
+ gap: 0.15rem;
218
+ }
219
+
220
+ .chat-settings-btn,
221
+ .chat-add-btn,
222
+ .chat-send-btn {
223
+ background: transparent;
224
+ border: none;
225
+ color: var(--text-color);
226
+ font-size: 1.45rem;
227
+ margin-bottom: 0.25rem;
228
+ cursor: pointer;
229
+ }
230
+
231
+ .chat-settings-btn:hover,
232
+ .chat-add-btn:hover,
233
+ .chat-send-btn:hover {
234
+ color: #888;
235
+ }
236
+
237
+ /* Tooltip Wrapper */
238
+ .tooltip-wrapper {
239
+ position: relative;
240
+ display: flex;
241
+ align-items: center;
242
+ justify-content: center;
243
+ }
244
+
245
+ /* Tooltip styling */
246
+ .tooltip {
247
+ position: absolute;
248
+ bottom: 100%;
249
+ left: 50%;
250
+ transform: translateX(-50%) translateY(10px) scale(0.9);
251
+ transform-origin: bottom center;
252
+ margin-bottom: 0.65rem;
253
+ padding: 0.3rem 0.6rem;
254
+ background-color: var(--primary-color);
255
+ color: var(--text-color);
256
+ border-radius: 0.25rem;
257
+ white-space: nowrap;
258
+ font-size: 0.85rem;
259
+ opacity: 0;
260
+ visibility: hidden;
261
+ transition: transform 0.3s ease, opacity 0.3s ease;
262
+ z-index: 10;
263
+ }
264
+
265
+ /* Show the tooltip on hover */
266
+ .tooltip-wrapper:hover .tooltip {
267
+ opacity: 1;
268
+ visibility: visible;
269
+ transform: translateX(-50%) translateY(0) scale(1);
270
+ }
271
+
272
+ /* Hide tooltip when its associated dropdown is open */
273
+ .tooltip-wrapper .tooltip.hidden {
274
+ opacity: 0;
275
+ visibility: hidden;
276
+ }
277
+
278
+ /* Floating sidebar container for chat mode */
279
+ .floating-sidebar {
280
+ position: fixed;
281
+ right: 0;
282
+ top: 0;
283
+ bottom: 0;
284
+ width: var(--right-sidebar-width, 300px);
285
+ z-index: 1000;
286
+ }
287
+
288
+ /* Chat container */
289
+ .chat-container {
290
+ flex-grow: 1;
291
+ margin-bottom: 9rem;
292
+ }
293
+
294
+ /* When processing a new prompt */
295
+ .chat-container.processing {
296
+ margin-bottom: 46rem;
297
+ }
298
+
299
+ /* Slack Authentication Modal */
300
+ .slack-auth-modal {
301
+ position: fixed;
302
+ top: 0;
303
+ left: 0;
304
+ right: 0;
305
+ bottom: 0;
306
+ background: rgba(0, 0, 0, 0.5);
307
+ display: flex;
308
+ align-items: center;
309
+ justify-content: center;
310
+ z-index: 10000;
311
+ animation: fadeIn 0.2s ease-out;
312
+ }
313
+
314
+ .slack-auth-modal-content {
315
+ background: white;
316
+ padding: 2rem;
317
+ border-radius: 12px;
318
+ box-shadow: 0 10px 40px rgba(0, 0, 0, 0.15);
319
+ max-width: 500px;
320
+ width: 90%;
321
+ animation: slideUp 0.3s ease-out;
322
+ }
323
+
324
+ .slack-auth-modal-header {
325
+ display: flex;
326
+ align-items: center;
327
+ margin-bottom: 1.5rem;
328
+ gap: 1rem;
329
+ }
330
+
331
+ .slack-auth-modal-icon {
332
+ width: 48px;
333
+ height: 48px;
334
+ background: #4A154B;
335
+ border-radius: 12px;
336
+ display: flex;
337
+ align-items: center;
338
+ justify-content: center;
339
+ color: white;
340
+ font-size: 24px;
341
+ }
342
+
343
+ .slack-auth-modal-title {
344
+ font-size: 1.5rem;
345
+ font-weight: 600;
346
+ color: #333;
347
+ margin: 0;
348
+ }
349
+
350
+ .slack-auth-modal-body {
351
+ margin-bottom: 2rem;
352
+ color: #666;
353
+ line-height: 1.6;
354
+ }
355
+
356
+ .slack-auth-modal-tips {
357
+ background: #f8f9fa;
358
+ border-radius: 8px;
359
+ padding: 1rem;
360
+ margin: 1rem 0;
361
+ }
362
+
363
+ .slack-auth-modal-tips-title {
364
+ font-weight: 600;
365
+ color: #333;
366
+ margin-bottom: 0.5rem;
367
+ }
368
+
369
+ .slack-auth-modal-tips ul {
370
+ margin: 0.5rem 0 0 0;
371
+ padding-left: 1.5rem;
372
+ }
373
+
374
+ .slack-auth-modal-tips li {
375
+ margin: 0.5rem 0;
376
+ }
377
+
378
+ .slack-auth-modal-buttons {
379
+ display: flex;
380
+ gap: 1rem;
381
+ justify-content: flex-end;
382
+ }
383
+
384
+ .slack-auth-modal-button {
385
+ padding: 0.75rem 1.5rem;
386
+ border-radius: 6px;
387
+ border: none;
388
+ font-weight: 500;
389
+ cursor: pointer;
390
+ transition: all 0.2s;
391
+ }
392
+
393
+ .slack-auth-modal-button-primary {
394
+ background: #4A154B;
395
+ color: white;
396
+ }
397
+
398
+ .slack-auth-modal-button-primary:hover {
399
+ background: #3a0f3b;
400
+ }
401
+
402
+ .slack-auth-modal-button-secondary {
403
+ background: #f1f1f1;
404
+ color: #666;
405
+ }
406
+
407
+ .slack-auth-modal-button-secondary:hover {
408
+ background: #e1e1e1;
409
+ }
410
+
411
+ @keyframes fadeIn {
412
+ from { opacity: 0; }
413
+ to { opacity: 1; }
414
+ }
415
+
416
+ @keyframes slideUp {
417
+ from { transform: translateY(20px); opacity: 0; }
418
+ to { transform: translateY(0); opacity: 1; }
419
+ }
420
+
421
+ /* Responsive Adjustments */
422
+ @media (max-width: 768px) {
423
+ .main-content {
424
+ margin-left: 0;
425
+ margin-right: 0;
426
+ }
427
+ }
428
+
429
+ @media (max-width: 576px) {
430
+ .main-content {
431
+ margin: 0;
432
+ padding: 1rem;
433
+ }
434
+ }
frontend/src/Components/AiPage.js ADDED
@@ -0,0 +1,1253 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState, useEffect, useRef, useCallback, useMemo } from 'react';
2
+ import { flushSync } from 'react-dom';
3
+ import Snackbar from '@mui/material/Snackbar';
4
+ import Alert from '@mui/material/Alert';
5
+ import { FaCog, FaPaperPlane, FaStop, FaPlus, FaGoogle, FaMicrosoft, FaSlack } from 'react-icons/fa';
6
+ import IntialSetting from './IntialSetting';
7
+ import AddContentDropdown from './AiComponents/Dropdowns/AddContentDropdown';
8
+ import AddFilesDialog from './AiComponents/Dropdowns/AddFilesDialog';
9
+ import ChatWindow from './AiComponents/ChatWindow';
10
+ import RightSidebar from './AiComponents/Sidebars/RightSidebar';
11
+ import Notification from '../Components/AiComponents/Notifications/Notification';
12
+ import { useNotification } from '../Components/AiComponents/Notifications/useNotification';
13
+ import './AiPage.css';
14
+
15
+ function AiPage() {
16
+ // Sidebar and other states
17
+ const [isRightSidebarOpen, setRightSidebarOpen] = useState(
18
+ localStorage.getItem("rightSidebarState") === "true"
19
+ );
20
+ const [rightSidebarWidth, setRightSidebarWidth] = useState(300);
21
+ const [sidebarContent, setSidebarContent] = useState("default");
22
+
23
+ const [searchText, setSearchText] = useState("");
24
+ const textAreaRef = useRef(null);
25
+ const [showSettingsModal, setShowSettingsModal] = useState(false);
26
+
27
+ const [showChatWindow, setShowChatWindow] = useState(false);
28
+ const [chatBlocks, setChatBlocks] = useState([]);
29
+ const [selectedChatBlockId, setSelectedChatBlockId] = useState(null);
30
+
31
+ const addBtnRef = useRef(null);
32
+ const chatAddBtnRef = useRef(null);
33
+ const [isAddContentOpen, setAddContentOpen] = useState(false);
34
+ const [isTooltipSuppressed, setIsTooltipSuppressed] = useState(false);
35
+
36
+ const [isAddFilesDialogOpen, setIsAddFilesDialogOpen] = useState(false);
37
+
38
+ const [defaultChatHeight, setDefaultChatHeight] = useState(null);
39
+ const [chatBottomPadding, setChatBottomPadding] = useState("60px");
40
+
41
+ const [sessionContent, setSessionContent] = useState({ files: [], links: [] });
42
+
43
+ // States/refs for streaming
44
+ const [isProcessing, setIsProcessing] = useState(false);
45
+ const [activeBlockId, setActiveBlockId] = useState(null);
46
+ const activeEventSourceRef = useRef(null);
47
+
48
+ // State to track if we should auto-scroll to the bottom
49
+ const [autoScrollEnabled, setAutoScrollEnabled] = useState(false);
50
+
51
+ // Snackbar state
52
+ const [snackbar, setSnackbar] = useState({
53
+ open: false,
54
+ message: "",
55
+ severity: "success",
56
+ });
57
+
58
+ // State for tracking selected services
59
+ const [selectedServices, setSelectedServices] = useState({
60
+ google: [],
61
+ microsoft: [],
62
+ slack: false
63
+ });
64
+
65
+ // State for Slack authentication modal
66
+ const [showSlackAuthModal, setShowSlackAuthModal] = useState(false);
67
+ const [pendingSlackAuth, setPendingSlackAuth] = useState(null);
68
+
69
+ // Notifications
70
+ const {
71
+ notifications,
72
+ addNotification,
73
+ removeNotification,
74
+ updateNotification
75
+ } = useNotification();
76
+
77
+ // Token management
78
+ const tokenExpiryTimersRef = useRef({});
79
+ const notificationIdsRef = useRef({});
80
+
81
+ // Function to check if we are near the bottom of the page
82
+ const checkIfNearBottom = (threshold = 400) => {
83
+ const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
84
+ const scrollHeight = document.documentElement.scrollHeight;
85
+ const clientHeight = window.innerHeight;
86
+ const distanceFromBottom = scrollHeight - (scrollTop + clientHeight);
87
+
88
+ return distanceFromBottom <= threshold;
89
+ };
90
+
91
+ // Helper to scroll to bottom
92
+ const scrollToBottom = (smooth = true) => {
93
+ window.scrollTo({
94
+ top: document.documentElement.scrollHeight,
95
+ behavior: smooth ? 'smooth' : 'auto'
96
+ });
97
+ };
98
+
99
+ // Function to open the snackbar
100
+ const openSnackbar = useCallback((message, severity = "success", duration) => {
101
+ let finalDuration;
102
+
103
+ if (duration !== undefined) {
104
+ // If a specific duration is provided (e.g., 5000 or null), use it.
105
+ finalDuration = duration;
106
+ } else {
107
+ // Otherwise, use the default logic.
108
+ finalDuration = severity === 'success' ? 3000 : null; // Success auto-hides, others are persistent by default.
109
+ }
110
+
111
+ setSnackbar({ open: true, message, severity, duration: finalDuration });
112
+ }, []);
113
+
114
+ // Function to close the snackbar
115
+ const closeSnackbar = (event, reason) => {
116
+ if (reason === 'clickaway') return;
117
+ setSnackbar(prev => ({ ...prev, open: false, duration: null }));
118
+ };
119
+
120
+ useEffect(() => {
121
+ localStorage.setItem("rightSidebarState", isRightSidebarOpen);
122
+ }, [isRightSidebarOpen]);
123
+
124
+ // Add cleanup handler for when the user closes the tab/browser
125
+ useEffect(() => {
126
+ const handleCleanup = () => {
127
+ navigator.sendBeacon('/cleanup');
128
+ };
129
+ window.addEventListener('beforeunload', handleCleanup);
130
+ return () => window.removeEventListener('beforeunload', handleCleanup);
131
+ }, []);
132
+
133
+ useEffect(() => {
134
+ document.documentElement.style.setProperty('--right-sidebar-width', rightSidebarWidth + 'px');
135
+ }, [rightSidebarWidth]);
136
+
137
+ // Dynamically increase height of chat input field based on newlines entered
138
+ useEffect(() => {
139
+ if (textAreaRef.current) {
140
+ if (!defaultChatHeight) {
141
+ setDefaultChatHeight(textAreaRef.current.scrollHeight);
142
+ }
143
+ textAreaRef.current.style.height = "auto";
144
+ textAreaRef.current.style.overflowY = "hidden";
145
+
146
+ const newHeight = textAreaRef.current.scrollHeight;
147
+ let finalHeight = newHeight;
148
+ if (newHeight > 200) {
149
+ finalHeight = 200;
150
+ textAreaRef.current.style.overflowY = "auto";
151
+ }
152
+ textAreaRef.current.style.height = `${finalHeight}px`;
153
+
154
+ const minPaddingPx = 0;
155
+ const maxPaddingPx = 59;
156
+ let newPaddingPx = minPaddingPx;
157
+ if (defaultChatHeight && finalHeight > defaultChatHeight) {
158
+ newPaddingPx =
159
+ minPaddingPx +
160
+ ((finalHeight - defaultChatHeight) / (200 - defaultChatHeight)) *
161
+ (maxPaddingPx - minPaddingPx);
162
+ if (newPaddingPx > maxPaddingPx) newPaddingPx = maxPaddingPx;
163
+ }
164
+ setChatBottomPadding(`${newPaddingPx}px`);
165
+ }
166
+ }, [searchText, defaultChatHeight]);
167
+
168
+ // Update backend whenever selected services change
169
+ useEffect(() => {
170
+ const updateSelectedServices = async () => {
171
+ try {
172
+ await fetch('/api/selected-services', {
173
+ method: 'POST',
174
+ headers: { 'Content-Type': 'application/json' },
175
+ body: JSON.stringify({
176
+ services: selectedServices
177
+ })
178
+ });
179
+ } catch (error) {
180
+ console.error('Failed to update selected services:', error);
181
+ }
182
+ };
183
+
184
+ updateSelectedServices();
185
+ }, [selectedServices]);
186
+
187
+ // Clear all tokens on page load
188
+ useEffect(() => {
189
+ // Clear all provider tokens on new tab/page load
190
+ ['google', 'microsoft', 'slack'].forEach(provider => {
191
+ sessionStorage.removeItem(`${provider}_token`);
192
+ sessionStorage.removeItem(`${provider}_token_expiry`);
193
+ });
194
+
195
+ // Clear any existing timers
196
+ Object.values(tokenExpiryTimersRef.current).forEach(timer => clearTimeout(timer));
197
+ tokenExpiryTimersRef.current = {};
198
+
199
+ console.log('Cleared all tokens for new session');
200
+ }, []);
201
+
202
+ const handleOpenRightSidebar = (content, chatBlockId = null) => {
203
+ flushSync(() => {
204
+ if (chatBlockId) {
205
+ setSelectedChatBlockId(chatBlockId);
206
+ }
207
+ setSidebarContent(content ? content : "default");
208
+ setRightSidebarOpen(true);
209
+ });
210
+ };
211
+
212
+ const handleEvaluationError = useCallback((blockId, errorMsg) => {
213
+ setChatBlocks(prev =>
214
+ prev.map(block =>
215
+ block.id === blockId
216
+ ? { ...block, isError: true, errorMessage: errorMsg }
217
+ : block
218
+ )
219
+ );
220
+ }, []);
221
+
222
+ // Function to store token with expiry
223
+ const storeTokenWithExpiry = (provider, token) => {
224
+ const expiryTime = Date.now() + (60 * 60 * 1000); // 1 hour from now
225
+ sessionStorage.setItem(`${provider}_token`, token);
226
+ sessionStorage.setItem(`${provider}_token_expiry`, expiryTime.toString());
227
+
228
+ // Set up expiry timer
229
+ setupTokenExpiryTimer(provider, expiryTime);
230
+ };
231
+
232
+ // Function to check if token is valid
233
+ const isTokenValid = (provider) => {
234
+ const token = sessionStorage.getItem(`${provider}_token`);
235
+ const expiry = sessionStorage.getItem(`${provider}_token_expiry`);
236
+
237
+ if (!token || !expiry) return false;
238
+
239
+ const expiryTime = parseInt(expiry);
240
+ return Date.now() < expiryTime;
241
+ };
242
+
243
+ // Function to get provider icon
244
+ const getProviderIcon = useCallback((provider) => {
245
+ switch (provider.toLowerCase()) {
246
+ case 'google':
247
+ return <FaGoogle />;
248
+ case 'microsoft':
249
+ return <FaMicrosoft />;
250
+ case 'slack':
251
+ return <FaSlack />;
252
+ default:
253
+ return null;
254
+ }
255
+ }, []);
256
+
257
+ // Function to get provider color
258
+ const getProviderColor = useCallback((provider) => {
259
+ switch (provider.toLowerCase()) {
260
+ case 'google':
261
+ return '#4285F4';
262
+ case 'microsoft':
263
+ return '#00A4EF';
264
+ case 'slack':
265
+ return '#4A154B';
266
+ default:
267
+ return '#666';
268
+ }
269
+ }, []);
270
+
271
+ // Function to set up timer for token expiry notification
272
+ const setupTokenExpiryTimer = useCallback((provider, expiryTime) => {
273
+ // Clear existing timer if any
274
+ if (tokenExpiryTimersRef.current[provider]) {
275
+ clearTimeout(tokenExpiryTimersRef.current[provider]);
276
+ }
277
+
278
+ // Remove any existing notification for this provider
279
+ if (notificationIdsRef.current[provider]) {
280
+ removeNotification(notificationIdsRef.current[provider]);
281
+ delete notificationIdsRef.current[provider];
282
+ }
283
+
284
+ const timeUntilExpiry = expiryTime - Date.now();
285
+
286
+ if (timeUntilExpiry > 0) {
287
+ tokenExpiryTimersRef.current[provider] = setTimeout(() => {
288
+ const providerName = provider.charAt(0).toUpperCase() + provider.slice(1);
289
+ const providerColor = getProviderColor(provider);
290
+
291
+ // Add notification
292
+ const notificationId = addNotification({
293
+ type: 'warning',
294
+ title: `${providerName} Authentication Expired`,
295
+ message: `Your ${providerName} authentication has expired. Please reconnect to continue using ${providerName} services.`,
296
+ icon: getProviderIcon(provider),
297
+ dismissible: true,
298
+ autoDismiss: false,
299
+ actions: [
300
+ {
301
+ id: 'reconnect',
302
+ label: `Reconnect ${providerName}`,
303
+ style: {
304
+ background: providerColor,
305
+ color: 'white',
306
+ border: 'none'
307
+ },
308
+ data: { provider }
309
+ }
310
+ ],
311
+ style: {
312
+ borderLeftColor: providerColor
313
+ }
314
+ });
315
+
316
+ // Store notification ID
317
+ notificationIdsRef.current[provider] = notificationId;
318
+
319
+ // Clear token data
320
+ sessionStorage.removeItem(`${provider}_token`);
321
+ sessionStorage.removeItem(`${provider}_token_expiry`);
322
+
323
+ // Update selected services to reflect disconnection
324
+ if (provider === 'slack') {
325
+ setSelectedServices(prev => ({ ...prev, slack: false }));
326
+ } else {
327
+ setSelectedServices(prev => ({ ...prev, [provider]: [] }));
328
+ }
329
+
330
+ }, timeUntilExpiry);
331
+ }
332
+ }, [addNotification, getProviderColor, getProviderIcon, removeNotification, setSelectedServices]);
333
+
334
+ // Check existing tokens on component mount and set up timers
335
+ useEffect(() => {
336
+ ['google', 'microsoft', 'slack'].forEach(provider => {
337
+ const expiry = sessionStorage.getItem(`${provider}_token_expiry`);
338
+ if (expiry) {
339
+ const expiryTime = parseInt(expiry);
340
+ if (Date.now() < expiryTime) {
341
+ setupTokenExpiryTimer(provider, expiryTime);
342
+ } else {
343
+ // Token already expired, clear it
344
+ sessionStorage.removeItem(`${provider}_token`);
345
+ sessionStorage.removeItem(`${provider}_token_expiry`);
346
+ }
347
+ }
348
+ });
349
+
350
+ // Cleanup timers on unmount
351
+ return () => {
352
+ Object.values(tokenExpiryTimersRef.current).forEach(timer => clearTimeout(timer));
353
+ };
354
+ }, [setupTokenExpiryTimer]);
355
+
356
+ // Initiate the SSE
357
+ const initiateSSE = (query, blockId) => {
358
+ const startTime = Date.now();
359
+ const sseUrl = `/message-sse?user_message=${encodeURIComponent(query)}`;
360
+ const eventSource = new EventSource(sseUrl);
361
+ activeEventSourceRef.current = eventSource;
362
+
363
+ eventSource.addEventListener("token", (e) => {
364
+ const { chunk, index } = JSON.parse(e.data);
365
+ console.log("[SSE token chunk]", JSON.stringify(chunk));
366
+ console.log("[SSE token index]", JSON.stringify(index));
367
+
368
+ setChatBlocks(prevBlocks => {
369
+ return prevBlocks.map(block => {
370
+ if (block.id === blockId) {
371
+ const newTokenArray = block.tokenChunks ? [...block.tokenChunks] : [];
372
+ newTokenArray[index] = chunk;
373
+
374
+ return {
375
+ ...block,
376
+ tokenChunks: newTokenArray
377
+ };
378
+ }
379
+ return block;
380
+ });
381
+ });
382
+ });
383
+
384
+ eventSource.addEventListener("final_message", (e) => {
385
+ console.log("[SSE final message]", e.data);
386
+ const endTime = Date.now();
387
+ const thinkingTime = ((endTime - startTime) / 1000).toFixed(1);
388
+ // Only update thinkingTime so the streaming flag turns false and the cursor disappears
389
+ setChatBlocks(prev => prev.map(block =>
390
+ block.id === blockId
391
+ ? { ...block, thinkingTime }
392
+ : block
393
+ ));
394
+ });
395
+
396
+ // Listen for the "final_sources" event to update sources in AI answer of this chat block.
397
+ eventSource.addEventListener("final_sources", (e) => {
398
+ try {
399
+ const sources = JSON.parse(e.data);
400
+ console.log("Final sources received:", sources);
401
+ setChatBlocks(prev => prev.map(block =>
402
+ block.id === blockId ? { ...block, finalSources: sources } : block
403
+ ));
404
+ } catch (err) {
405
+ console.error("Error parsing final_sources event:", err);
406
+ }
407
+ });
408
+
409
+ // Listen for the "complete" event to know when to close the connection.
410
+ eventSource.addEventListener("complete", (e) => {
411
+ console.log("Complete event received:", e.data);
412
+ eventSource.close();
413
+ activeEventSourceRef.current = null;
414
+ setIsProcessing(false);
415
+ setActiveBlockId(null);
416
+ });
417
+
418
+ // Update actions for only this chat block.
419
+ eventSource.addEventListener("action", (e) => {
420
+ try {
421
+ const actionData = JSON.parse(e.data);
422
+ console.log("Action event received:", actionData);
423
+ setChatBlocks(prev => prev.map(block => {
424
+ if (block.id === blockId) {
425
+ let updatedBlock = { ...block, actions: [...(block.actions || []), actionData] };
426
+ if (actionData.name === "sources") {
427
+ updatedBlock.sources = actionData.payload;
428
+ }
429
+ if (actionData.name === "graph") {
430
+ updatedBlock.graph = actionData.payload;
431
+ }
432
+ return updatedBlock;
433
+ }
434
+ return block;
435
+ }));
436
+ } catch (err) {
437
+ console.error("Error parsing action event:", err);
438
+ }
439
+ });
440
+
441
+ // Update the error for this chat block.
442
+ eventSource.addEventListener("error", (e) => {
443
+ console.error("Error from SSE:", e.data);
444
+ setChatBlocks(prev => prev.map(block =>
445
+ block.id === blockId
446
+ ? {
447
+ ...block,
448
+ isError: true,
449
+ errorMessage: e.data,
450
+ aiAnswer: "",
451
+ tasks: []
452
+ }
453
+ : block
454
+ ));
455
+ eventSource.close();
456
+ activeEventSourceRef.current = null;
457
+ setIsProcessing(false);
458
+ setActiveBlockId(null);
459
+ });
460
+
461
+ eventSource.addEventListener("step", (e) => {
462
+ console.log("Step event received:", e.data);
463
+ setChatBlocks(prev => prev.map(block =>
464
+ block.id === blockId
465
+ ? { ...block, thoughtLabel: e.data }
466
+ : block
467
+ ));
468
+ });
469
+
470
+ eventSource.addEventListener("sources_read", (e) => {
471
+ console.log("Sources read event received:", e.data);
472
+ try {
473
+ const parsed = JSON.parse(e.data);
474
+ let count;
475
+ if (typeof parsed === 'number') {
476
+ count = parsed;
477
+ } else if (parsed && typeof parsed.count === 'number') {
478
+ count = parsed.count;
479
+ }
480
+ if (typeof count === 'number') {
481
+ setChatBlocks(prev => prev.map(block =>
482
+ block.id === blockId
483
+ ? { ...block, sourcesRead: count, sources: parsed.sources || [] }
484
+ : block
485
+ ));
486
+ }
487
+ } catch(err) {
488
+ if (e.data.trim() !== "") {
489
+ setChatBlocks(prev => prev.map(block =>
490
+ block.id === blockId
491
+ ? { ...block, sourcesRead: e.data }
492
+ : block
493
+ ));
494
+ }
495
+ }
496
+ });
497
+
498
+ eventSource.addEventListener("task", (e) => {
499
+ console.log("Task event received:", e.data);
500
+ try {
501
+ const taskData = JSON.parse(e.data);
502
+ setChatBlocks(prev => prev.map(block => {
503
+ if (block.id === blockId) {
504
+ const existingTaskIndex = (block.tasks || []).findIndex(t => t.task === taskData.task);
505
+ if (existingTaskIndex !== -1) {
506
+ const updatedTasks = [...block.tasks];
507
+ updatedTasks[existingTaskIndex] = { ...updatedTasks[existingTaskIndex], status: taskData.status };
508
+ return { ...block, tasks: updatedTasks };
509
+ } else {
510
+ return { ...block, tasks: [...(block.tasks || []), taskData] };
511
+ }
512
+ }
513
+ return block;
514
+ }));
515
+ } catch (error) {
516
+ console.error("Error parsing task event:", error);
517
+ }
518
+ });
519
+ };
520
+
521
+ // Create a new chat block and initiate the SSE
522
+ const handleSend = () => {
523
+ if (!searchText.trim()) return;
524
+
525
+ // Check if this is the first prompt (no existing chat blocks)
526
+ const isFirstPrompt = chatBlocks.length === 0;
527
+
528
+ // Only check if user is near bottom if this is NOT the first prompt before adding a new block
529
+ const shouldScroll = !isFirstPrompt && checkIfNearBottom(1000); // 1000px threshold
530
+ setAutoScrollEnabled(shouldScroll);
531
+
532
+ const blockId = new Date().getTime();
533
+ setActiveBlockId(blockId);
534
+ setIsProcessing(true);
535
+ setChatBlocks(prev => [
536
+ ...prev,
537
+ {
538
+ id: blockId,
539
+ userMessage: searchText,
540
+ tokenChunks: [],
541
+ aiAnswer: "",
542
+ thinkingTime: null,
543
+ thoughtLabel: "",
544
+ sourcesRead: "",
545
+ tasks: [],
546
+ sources: [],
547
+ actions: []
548
+ }
549
+ ]);
550
+ setShowChatWindow(true);
551
+ const query = searchText;
552
+ setSearchText("");
553
+ initiateSSE(query, blockId);
554
+ };
555
+
556
+ const handleKeyDown = (e) => {
557
+ if (e.key === "Enter" && !e.shiftKey) {
558
+ e.preventDefault();
559
+ if (!isProcessing) {
560
+ handleSend();
561
+ }
562
+ }
563
+ };
564
+
565
+ // Auto-scroll when chat block is added
566
+ useEffect(() => {
567
+ if (autoScrollEnabled && isProcessing) {
568
+ scrollToBottom();
569
+ }
570
+ }, [isProcessing, autoScrollEnabled]);
571
+
572
+ // Stop the user request and close the active SSE connection
573
+ const handleStop = async () => {
574
+ // Close the active SSE connection if it exists
575
+ if (activeEventSourceRef.current) {
576
+ activeEventSourceRef.current.close();
577
+ activeEventSourceRef.current = null;
578
+ }
579
+ // Send POST request to /stop and update the chat block with the returned message
580
+ try {
581
+ const response = await fetch('/stop', {
582
+ method: 'POST',
583
+ headers: { 'Content-Type': 'application/json' },
584
+ body: JSON.stringify({})
585
+ });
586
+ const data = await response.json();
587
+
588
+ if (activeBlockId) {
589
+ setChatBlocks(prev => prev.map(block =>
590
+ block.id === activeBlockId
591
+ ? { ...block, aiAnswer: data.message, thinkingTime: 0, tasks: [] }
592
+ : block
593
+ ));
594
+ }
595
+ } catch (error) {
596
+ console.error("Error stopping the request:", error);
597
+ if (activeBlockId) {
598
+ setChatBlocks(prev => prev.map(block =>
599
+ block.id === activeBlockId
600
+ ? { ...block, aiAnswer: "Error stopping task", thinkingTime: 0, tasks: [] }
601
+ : block
602
+ ));
603
+ }
604
+ }
605
+ setIsProcessing(false);
606
+ setActiveBlockId(null);
607
+ };
608
+
609
+ const handleSendButtonClick = () => {
610
+ if (searchText.trim()) handleSend();
611
+ };
612
+
613
+ // Toggle the Add Content dropdown
614
+ const handleToggleAddContent = (event) => {
615
+ event.stopPropagation(); // Prevents the click from closing the menu immediately
616
+ // If we are about to close the dropdown, suppress the tooltip.
617
+ if (isAddContentOpen) {
618
+ setIsTooltipSuppressed(true);
619
+ }
620
+ setAddContentOpen(prev => !prev);
621
+ };
622
+
623
+ // Handle mouse enter on the Add Content button to suppress tooltip
624
+ const handleMouseLeaveAddBtn = () => {
625
+ setIsTooltipSuppressed(false);
626
+ };
627
+
628
+ // Close the Add Content dropdown
629
+ const closeAddContentDropdown = () => {
630
+ setAddContentOpen(false);
631
+ };
632
+
633
+ // Open the Add Files dialog
634
+ const handleOpenAddFilesDialog = () => {
635
+ setAddContentOpen(false); // Close the dropdown when opening the dialog
636
+ setIsAddFilesDialogOpen(true);
637
+ };
638
+
639
+ // Fetch excerpts for a specific block
640
+ const handleFetchExcerpts = useCallback(async (blockId) => {
641
+ let blockIndex = -1;
642
+ let currentBlock = null;
643
+
644
+ // Find the block to check its current state
645
+ setChatBlocks(prev => {
646
+ blockIndex = prev.findIndex(b => b.id === blockId);
647
+ if (blockIndex !== -1) {
648
+ currentBlock = prev[blockIndex];
649
+ }
650
+ // No state change here, just reading the state
651
+ return prev;
652
+ });
653
+
654
+ // Prevent fetching if already loaded or currently loading
655
+ if (blockIndex === -1 || !currentBlock || currentBlock.excerptsData || currentBlock.isLoadingExcerpts) return;
656
+
657
+ // Set loading state for the specific block
658
+ setChatBlocks(prev => prev.map(b =>
659
+ b.id === blockId ? { ...b, isLoadingExcerpts: true } : b
660
+ ));
661
+
662
+ try {
663
+ // Call the backend endpoint to get excerpts
664
+ const response = await fetch('/action/excerpts', {
665
+ method: 'POST',
666
+ headers: { 'Content-Type': 'application/json' },
667
+ body: JSON.stringify({ blockId: blockId })
668
+ });
669
+
670
+ if (!response.ok) {
671
+ const errorData = await response.json();
672
+ throw new Error(errorData.detail || `HTTP error! status: ${response.status}`);
673
+ }
674
+
675
+ const data = await response.json();
676
+ console.log("Fetched excerpts data from backend:", data.result);
677
+
678
+ // Update the specific block with the fetched excerptsData
679
+ setChatBlocks(prev => prev.map(b =>
680
+ b.id === blockId
681
+ ? {
682
+ ...b,
683
+ excerptsData: data.result, // Store the fetched data
684
+ isLoadingExcerpts: false, // Turn off loading
685
+ }
686
+ : b
687
+ ));
688
+ openSnackbar("Excerpts loaded successfully!", "success");
689
+
690
+ } catch (error) {
691
+ console.error("Error requesting excerpts:", error);
692
+ // Reset loading state on error
693
+ setChatBlocks(prev => prev.map(b =>
694
+ b.id === blockId ? { ...b, isLoadingExcerpts: false } : b
695
+ ));
696
+ openSnackbar(`Failed to load excerpts`, "error");
697
+ }
698
+ }, [openSnackbar]);
699
+
700
+ // Function to handle notification actions
701
+ const handleNotificationAction = (notificationId, actionId, actionData) => {
702
+ console.log('Notification action triggered:', { notificationId, actionId, actionData });
703
+
704
+ // Handle both 'reconnect' and 'connect' actions
705
+ if ((actionId === 'reconnect' || actionId === 'connect') && actionData?.provider) {
706
+ // Remove the notification
707
+ removeNotification(notificationId);
708
+
709
+ // Clean up stored notification ID if it exists
710
+ if (notificationIdsRef.current[actionData.provider] === notificationId) {
711
+ delete notificationIdsRef.current[actionData.provider];
712
+ }
713
+
714
+ // Trigger authentication
715
+ initiateOAuth(actionData.provider);
716
+ }
717
+ };
718
+
719
+ // Slack Auth Modal Component
720
+ const SlackAuthModal = () => {
721
+ if (!showSlackAuthModal) return null;
722
+
723
+ return (
724
+ <div className="slack-auth-modal" onClick={() => setShowSlackAuthModal(false)}>
725
+ <div className="slack-auth-modal-content" onClick={(e) => e.stopPropagation()}>
726
+ <div className="slack-auth-modal-header">
727
+ <div className="slack-auth-modal-icon">
728
+ <FaSlack />
729
+ </div>
730
+ <h2 className="slack-auth-modal-title">Connect to Slack</h2>
731
+ </div>
732
+
733
+ <div className="slack-auth-modal-body">
734
+ <p>
735
+ Slack will use your currently logged-in workspace. If you're logged into multiple workspaces,
736
+ you'll be able to select which one to use.
737
+ </p>
738
+
739
+ <div className="slack-auth-modal-tips">
740
+ <div className="slack-auth-modal-tips-title">
741
+ To use a different workspace:
742
+ </div>
743
+ <ul>
744
+ <li>Sign out of Slack in your browser first</li>
745
+ <li>Use an incognito/private browser window</li>
746
+ <li>Or switch workspaces in Slack before continuing</li>
747
+ </ul>
748
+ </div>
749
+
750
+ <p style={{ marginTop: '1rem', fontSize: '0.9rem', color: '#888' }}>
751
+ You'll be redirected to Slack to authorize access. This allows the app to read messages
752
+ and channels from your selected workspace.
753
+ </p>
754
+ </div>
755
+
756
+ <div className="slack-auth-modal-buttons">
757
+ <button
758
+ className="slack-auth-modal-button slack-auth-modal-button-secondary"
759
+ onClick={() => {
760
+ setShowSlackAuthModal(false);
761
+ if (pendingSlackAuth?.notificationId) {
762
+ removeNotification(pendingSlackAuth.notificationId);
763
+ }
764
+ setPendingSlackAuth(null);
765
+ }}
766
+ >
767
+ Cancel
768
+ </button>
769
+ <button
770
+ className="slack-auth-modal-button slack-auth-modal-button-primary"
771
+ onClick={() => {
772
+ setShowSlackAuthModal(false);
773
+ if (pendingSlackAuth) {
774
+ // Continue with the actual OAuth flow
775
+ proceedWithSlackAuth(pendingSlackAuth);
776
+ }
777
+ }}
778
+ >
779
+ Continue to Slack
780
+ </button>
781
+ </div>
782
+ </div>
783
+ </div>
784
+ );
785
+ };
786
+
787
+ // Function to proceed with Slack authentication
788
+ const proceedWithSlackAuth = (pendingAuth) => {
789
+ const { provider, notificationId } = pendingAuth;
790
+ setPendingSlackAuth(null);
791
+
792
+ // Update the notification
793
+ if (notificationId) {
794
+ updateNotification(notificationId, {
795
+ message: 'Redirecting to Slack for authentication...'
796
+ });
797
+ }
798
+
799
+ // Now proceed with OAuth
800
+ proceedWithOAuth(provider, notificationId);
801
+ };
802
+
803
+ // Function to initiate OAuth
804
+ const initiateOAuth = (provider) => {
805
+ // Special handling for Slack - show modal first
806
+ if (provider === 'slack') {
807
+ // Show the modal first
808
+ const connectingNotificationId = addNotification({
809
+ type: 'info',
810
+ title: `Preparing to connect to Slack`,
811
+ message: 'Please review the connection information...',
812
+ icon: getProviderIcon(provider),
813
+ dismissible: false,
814
+ autoDismiss: false
815
+ });
816
+
817
+ setPendingSlackAuth({ provider, notificationId: connectingNotificationId });
818
+ setShowSlackAuthModal(true);
819
+ return;
820
+ }
821
+
822
+ // For Google and Microsoft, proceed normally
823
+ proceedWithOAuth(provider);
824
+ };
825
+
826
+ // Function to proceed with OAuth (renamed from initiateOAuth)
827
+ const proceedWithOAuth = (provider, existingNotificationId = null) => {
828
+ const authUrls = {
829
+ google: `https://accounts.google.com/o/oauth2/v2/auth?` +
830
+ `client_id=${process.env.REACT_APP_GOOGLE_CLIENT_ID}&` +
831
+ `response_type=token&` +
832
+ `scope=email profile https://www.googleapis.com/auth/drive.readonly https://www.googleapis.com/auth/gmail.readonly https://www.googleapis.com/auth/calendar.readonly https://www.googleapis.com/auth/tasks.readonly&` +
833
+ `redirect_uri=${window.location.origin}/auth-receiver.html&` +
834
+ `state=google&` +
835
+ `prompt=select_account`,
836
+
837
+ microsoft: `https://login.microsoftonline.com/common/oauth2/v2.0/authorize?` +
838
+ `client_id=${process.env.REACT_APP_MICROSOFT_CLIENT_ID}&` +
839
+ `response_type=token&` +
840
+ `scope=openid profile email Files.Read.All Mail.Read Calendars.Read Tasks.Read Notes.Read&` +
841
+ `redirect_uri=${window.location.origin}/auth-receiver.html&` +
842
+ `response_mode=fragment&` +
843
+ `state=microsoft&` +
844
+ `prompt=select_account`,
845
+
846
+ slack: `https://slack.com/oauth/v2/authorize?` +
847
+ `client_id=${process.env.REACT_APP_SLACK_CLIENT_ID}&` +
848
+ `user_scope=channels:read,channels:history,files:read,groups:read,im:read,mpim:read,search:read,users:read&` +
849
+ `redirect_uri=${window.location.origin}/auth-receiver.html&` +
850
+ `state=slack`
851
+ };
852
+
853
+ const authWindow = window.open(
854
+ authUrls[provider],
855
+ 'Connect Account',
856
+ 'width=600,height=700,left=200,top=100'
857
+ );
858
+
859
+ // Show connecting notification
860
+ const connectingNotificationId = existingNotificationId || addNotification({
861
+ type: 'info',
862
+ title: `Connecting to ${provider.charAt(0).toUpperCase() + provider.slice(1)}`,
863
+ message: 'Please complete the authentication in the popup window...',
864
+ icon: getProviderIcon(provider),
865
+ dismissible: false,
866
+ autoDismiss: false
867
+ });
868
+
869
+ // Set up message listener
870
+ const messageHandler = async (event) => {
871
+ if (event.origin !== window.location.origin) return;
872
+
873
+ if (event.data.type === 'auth-success') {
874
+ const { token, code, team_id, team_name, provider: authProvider } = event.data;
875
+
876
+ // Remove connecting notification
877
+ removeNotification(connectingNotificationId);
878
+
879
+ // Store token with expiry
880
+ storeTokenWithExpiry(provider, token);
881
+
882
+ // Store workspace info in sessionStorage for Slack
883
+ if (provider === 'slack' && team_id) {
884
+ sessionStorage.setItem('slack_workspace', JSON.stringify({
885
+ team_id,
886
+ team_name: team_name || 'Unknown Workspace'
887
+ }));
888
+ }
889
+
890
+ // Build payload for backend
891
+ const payload = {
892
+ provider: authProvider || provider, // Use provider from auth data if available
893
+ token,
894
+ origin: window.location.origin // Add origin for backend
895
+ };
896
+
897
+ // Include code for Slack and Microsoft (if using code flow)
898
+ if (code) {
899
+ payload.code = code;
900
+ }
901
+
902
+ // Include workspace info for Slack
903
+ if (provider === 'slack' || authProvider === 'slack') {
904
+ if (team_id) payload.team_id = team_id;
905
+ if (team_name) payload.team_name = team_name;
906
+ }
907
+
908
+ // Send token and workspace info to backend
909
+ try {
910
+ const response = await fetch('/api/session-token', {
911
+ method: 'POST',
912
+ headers: { 'Content-Type': 'application/json' },
913
+ body: JSON.stringify(payload)
914
+ });
915
+
916
+ if (response.ok) {
917
+ // Show success notification with workspace info
918
+ const workspaceInfo = provider === 'slack' && team_name
919
+ ? ` (Workspace: ${team_name})`
920
+ : '';
921
+
922
+ addNotification({
923
+ type: 'success',
924
+ title: 'Connected Successfully',
925
+ message: `Successfully connected to ${provider.charAt(0).toUpperCase() + provider.slice(1)}${workspaceInfo}!`,
926
+ icon: getProviderIcon(provider),
927
+ autoDismiss: true,
928
+ duration: 3000,
929
+ showProgress: true
930
+ });
931
+ }
932
+ } catch (error) {
933
+ console.error(`Failed to connect to ${provider}:`, error);
934
+ addNotification({
935
+ type: 'error',
936
+ title: 'Connection Failed',
937
+ message: `Failed to connect to ${provider.charAt(0).toUpperCase() + provider.slice(1)}. Please try again.`,
938
+ autoDismiss: true,
939
+ duration: 5000
940
+ });
941
+ }
942
+
943
+ window.removeEventListener('message', messageHandler);
944
+ } else if (event.data.type === 'auth-failed') {
945
+ console.error(`Authentication failed for ${provider}:`, event.data.error);
946
+
947
+ // Remove connecting notification
948
+ removeNotification(connectingNotificationId);
949
+
950
+ // Show error notification
951
+ addNotification({
952
+ type: 'error',
953
+ title: 'Authentication Failed',
954
+ message: `Failed to authenticate with ${provider.charAt(0).toUpperCase() + provider.slice(1)}. ${event.data.error_description || 'Please try again.'}`,
955
+ autoDismiss: true,
956
+ duration: 5000
957
+ });
958
+
959
+ window.removeEventListener('message', messageHandler);
960
+ }
961
+ };
962
+
963
+ window.addEventListener('message', messageHandler);
964
+
965
+ // Handle if user closes the popup without authenticating
966
+ const checkInterval = setInterval(() => {
967
+ if (authWindow.closed) {
968
+ clearInterval(checkInterval);
969
+ removeNotification(connectingNotificationId);
970
+ window.removeEventListener('message', messageHandler);
971
+ }
972
+ }, 1000);
973
+ };
974
+
975
+ // Handle service selection from dropdown
976
+ const handleServiceClick = useCallback((provider, service) => {
977
+ // Toggle selection
978
+ if (provider === 'slack') {
979
+ setSelectedServices(prev => ({ ...prev, slack: !prev.slack }));
980
+ } else {
981
+ setSelectedServices(prev => ({
982
+ ...prev,
983
+ [provider]: prev[provider].includes(service)
984
+ ? prev[provider].filter(s => s !== service)
985
+ : [...prev[provider], service]
986
+ }));
987
+ }
988
+
989
+ // Check if token is valid
990
+ if (!isTokenValid(provider)) {
991
+ // Show notification prompting to authenticate
992
+ const notificationId = addNotification({
993
+ type: 'info',
994
+ title: 'Authentication Required',
995
+ message: `Please connect your ${provider.charAt(0).toUpperCase() + provider.slice(1)} account to use this service.`,
996
+ icon: getProviderIcon(provider),
997
+ actions: [
998
+ {
999
+ id: 'connect',
1000
+ label: `Connect ${provider.charAt(0).toUpperCase() + provider.slice(1)}`,
1001
+ style: {
1002
+ background: getProviderColor(provider),
1003
+ color: 'white',
1004
+ border: 'none'
1005
+ },
1006
+ data: { provider }
1007
+ }
1008
+ ],
1009
+ autoDismiss: true,
1010
+ duration: 5000,
1011
+ showProgress: true
1012
+ });
1013
+ }
1014
+ }, [addNotification, getProviderIcon, getProviderColor]);
1015
+
1016
+ // Get the chat block whose details should be shown in the sidebar.
1017
+ const selectedBlock = chatBlocks.find(block => block.id === selectedChatBlockId);
1018
+ const evaluateAction = selectedBlock && selectedBlock.actions
1019
+ ? selectedBlock.actions.find(a => a.name === "evaluate")
1020
+ : null;
1021
+
1022
+ // Memoized evaluation object
1023
+ const evaluation = useMemo(() => {
1024
+ if (!evaluateAction) return null;
1025
+ return {
1026
+ ...evaluateAction.payload,
1027
+ blockId: selectedBlock?.id,
1028
+ onError: handleEvaluationError,
1029
+ };
1030
+ }, [evaluateAction, selectedBlock?.id, handleEvaluationError]);
1031
+
1032
+ return (
1033
+ <div
1034
+ className="app-container"
1035
+ style={{
1036
+ paddingRight: isRightSidebarOpen
1037
+ ? Math.max(0, rightSidebarWidth - 250) + 'px'
1038
+ : 0,
1039
+ }}
1040
+ >
1041
+ <Notification
1042
+ notifications={notifications}
1043
+ position="top-right"
1044
+ animation="slide"
1045
+ stackDirection="down"
1046
+ maxNotifications={5}
1047
+ spacing={12}
1048
+ offset={{ x: 20, y: 20 }}
1049
+ onDismiss={removeNotification}
1050
+ onAction={handleNotificationAction}
1051
+ theme="light"
1052
+ />
1053
+ {showSlackAuthModal && <SlackAuthModal />}
1054
+ {showChatWindow && selectedBlock && (sidebarContent !== "default" || (selectedBlock.tasks && selectedBlock.tasks.length > 0) || (selectedBlock.sources && selectedBlock.sources.length > 0)) && (
1055
+ <div className="floating-sidebar">
1056
+ <RightSidebar
1057
+ isOpen={isRightSidebarOpen}
1058
+ rightSidebarWidth={rightSidebarWidth}
1059
+ setRightSidebarWidth={setRightSidebarWidth}
1060
+ toggleRightSidebar={() => setRightSidebarOpen(!isRightSidebarOpen)}
1061
+ sidebarContent={sidebarContent}
1062
+ tasks={selectedBlock.tasks || []}
1063
+ tasksLoading={false}
1064
+ sources={selectedBlock.sources || []}
1065
+ sourcesLoading={false}
1066
+ onSourceClick={(source) => {
1067
+ if (!source || !source.link) return;
1068
+ window.open(source.link, '_blank');
1069
+ }}
1070
+ evaluation={evaluation}
1071
+ />
1072
+ </div>
1073
+ )}
1074
+
1075
+ <main className="main-content">
1076
+ {showChatWindow ? (
1077
+ <>
1078
+ <div className={`chat-container ${isProcessing ? 'processing' : ''}`}>
1079
+ {chatBlocks.map((block) => (
1080
+ <ChatWindow
1081
+ key={block.id}
1082
+ blockId={block.id}
1083
+ userMessage={block.userMessage}
1084
+ tokenChunks={block.tokenChunks}
1085
+ aiAnswer={block.aiAnswer}
1086
+ thinkingTime={block.thinkingTime}
1087
+ thoughtLabel={block.thoughtLabel}
1088
+ sourcesRead={block.sourcesRead}
1089
+ finalSources={block.finalSources}
1090
+ excerptsData={block.excerptsData}
1091
+ isLoadingExcerpts={block.isLoadingExcerpts}
1092
+ onFetchExcerpts={handleFetchExcerpts}
1093
+ actions={block.actions}
1094
+ tasks={block.tasks}
1095
+ openRightSidebar={handleOpenRightSidebar}
1096
+ openLeftSidebar={() => { /* if needed */ }}
1097
+ isError={block.isError}
1098
+ errorMessage={block.errorMessage}
1099
+ />
1100
+ ))}
1101
+ </div>
1102
+ <div
1103
+ className="floating-chat-search-bar"
1104
+ style={{
1105
+ transform: isRightSidebarOpen
1106
+ ? `translateX(calc(-50% - ${Math.max(0, (rightSidebarWidth - 250) / 2)}px))`
1107
+ : 'translateX(-50%)'
1108
+ }}
1109
+ >
1110
+ <div className="chat-search-input-wrapper" style={{ paddingBottom: chatBottomPadding }}>
1111
+ <textarea
1112
+ rows="1"
1113
+ className="chat-search-input"
1114
+ placeholder="Message..."
1115
+ value={searchText}
1116
+ onChange={(e) => setSearchText(e.target.value)}
1117
+ onKeyDown={handleKeyDown}
1118
+ ref={textAreaRef}
1119
+ />
1120
+ </div>
1121
+ <div className="chat-icon-container">
1122
+ <div className="chat-left-icons">
1123
+ <div className="tooltip-wrapper">
1124
+ <button
1125
+ className="chat-settings-btn"
1126
+ onClick={() => setShowSettingsModal(true)}
1127
+ >
1128
+ <FaCog />
1129
+ </button>
1130
+ <span className="tooltip">Settings</span>
1131
+ </div>
1132
+ <div
1133
+ className="tooltip-wrapper"
1134
+ onMouseLeave={handleMouseLeaveAddBtn}
1135
+ >
1136
+ <button className="chat-add-btn" onClick={handleToggleAddContent} ref={chatAddBtnRef}>
1137
+ <FaPlus />
1138
+ </button>
1139
+ <span className={`tooltip ${isAddContentOpen || isTooltipSuppressed ? 'hidden' : ''}`}>Add Content</span>
1140
+ <AddContentDropdown
1141
+ isOpen={isAddContentOpen}
1142
+ onClose={closeAddContentDropdown}
1143
+ toggleButtonRef={chatAddBtnRef}
1144
+ onAddFilesClick={handleOpenAddFilesDialog}
1145
+ onServiceClick={handleServiceClick}
1146
+ selectedServices={selectedServices}
1147
+ />
1148
+ </div>
1149
+ </div>
1150
+ {/* Conditionally render Stop or Send button */}
1151
+ <div className="tooltip-wrapper">
1152
+ <button
1153
+ className={`chat-send-btn ${isProcessing ? 'stop-btn' : ''}`}
1154
+ onClick={isProcessing ? handleStop : handleSendButtonClick}
1155
+ >
1156
+ {isProcessing ? <FaStop size={12} color="black" /> : <FaPaperPlane />}
1157
+ </button>
1158
+ <span className="tooltip">{isProcessing ? 'Stop' : 'Send'}</span>
1159
+ </div>
1160
+ </div>
1161
+ </div>
1162
+ </>
1163
+ ) : (
1164
+ <div className="search-area">
1165
+ <h1>How can I help you today?</h1>
1166
+ <div className="search-bar">
1167
+ <div className="search-input-wrapper">
1168
+ <textarea
1169
+ rows="1"
1170
+ className="search-input"
1171
+ placeholder="Message..."
1172
+ value={searchText}
1173
+ onChange={(e) => setSearchText(e.target.value)}
1174
+ onKeyDown={handleKeyDown}
1175
+ ref={textAreaRef}
1176
+ />
1177
+ </div>
1178
+ <div className="icon-container">
1179
+ <div className="left-icons">
1180
+ <div className="tooltip-wrapper">
1181
+ <button
1182
+ className="settings-btn"
1183
+ onClick={() => setShowSettingsModal(true)}
1184
+ >
1185
+ <FaCog />
1186
+ </button>
1187
+ <span className="tooltip">Settings</span>
1188
+ </div>
1189
+ <div
1190
+ className="tooltip-wrapper"
1191
+ onMouseLeave={handleMouseLeaveAddBtn}
1192
+ >
1193
+ <button className="add-btn" onClick={handleToggleAddContent} ref={addBtnRef}>
1194
+ <FaPlus />
1195
+ </button>
1196
+ <span className={`tooltip ${isAddContentOpen || isTooltipSuppressed ? 'hidden' : ''}`}>Add Content</span>
1197
+ <AddContentDropdown
1198
+ isOpen={isAddContentOpen}
1199
+ onClose={closeAddContentDropdown}
1200
+ toggleButtonRef={addBtnRef}
1201
+ onAddFilesClick={handleOpenAddFilesDialog}
1202
+ onServiceClick={handleServiceClick}
1203
+ selectedServices={selectedServices}
1204
+ />
1205
+ </div>
1206
+ </div>
1207
+ <div className="tooltip-wrapper">
1208
+ <button
1209
+ className={`send-btn ${isProcessing ? 'stop-btn' : ''}`}
1210
+ onClick={isProcessing ? handleStop : handleSendButtonClick}
1211
+ >
1212
+ {isProcessing ? <FaStop /> : <FaPaperPlane />}
1213
+ </button>
1214
+ <span className="tooltip">{isProcessing ? 'Stop' : 'Send'}</span>
1215
+ </div>
1216
+ </div>
1217
+ </div>
1218
+ </div>
1219
+ )}
1220
+ </main>
1221
+
1222
+ {showSettingsModal && (
1223
+ <IntialSetting
1224
+ trigger={true}
1225
+ setTrigger={() => setShowSettingsModal(false)}
1226
+ fromAiPage={true}
1227
+ openSnackbar={openSnackbar}
1228
+ closeSnackbar={closeSnackbar}
1229
+ />
1230
+ )}
1231
+ {isAddFilesDialogOpen && (
1232
+ <AddFilesDialog
1233
+ isOpen={isAddFilesDialogOpen}
1234
+ onClose={() => setIsAddFilesDialogOpen(false)}
1235
+ openSnackbar={openSnackbar}
1236
+ setSessionContent={setSessionContent}
1237
+ />
1238
+ )}
1239
+ <Snackbar
1240
+ open={snackbar.open}
1241
+ autoHideDuration={snackbar.duration}
1242
+ onClose={closeSnackbar}
1243
+ anchorOrigin={{ vertical: 'top', horizontal: 'center' }}
1244
+ >
1245
+ <Alert onClose={closeSnackbar} severity={snackbar.severity} variant="filled" sx={{ width: '100%' }}>
1246
+ {snackbar.message}
1247
+ </Alert>
1248
+ </Snackbar>
1249
+ </div>
1250
+ );
1251
+ }
1252
+
1253
+ export default AiPage;
frontend/src/Components/IntialSetting.css ADDED
@@ -0,0 +1,174 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .showSetting {
2
+ position: fixed;
3
+ top: 0;
4
+ left: 0;
5
+ width: 100%;
6
+ height: 100vh;
7
+ background-color: rgba(0, 0, 0, 0.2);
8
+ display: flex;
9
+ justify-content: center;
10
+ align-items: center;
11
+ z-index: 1000;
12
+ overflow: hidden;
13
+ }
14
+
15
+ .showSetting-inner {
16
+ position: relative;
17
+ border-radius: 12px;
18
+ padding: 32px;
19
+ width: 45%;
20
+ max-width: 100%;
21
+ background-color: #1e1e1e;
22
+ max-height: 80vh; /* Limits height to 80% of the viewport */
23
+ overflow-y: auto; /* Enables vertical scrolling when content overflows */
24
+ }
25
+
26
+ /* Dark Themed Scrollbar */
27
+ .showSetting-inner::-webkit-scrollbar {
28
+ width: 8px; /* Width of the scrollbar */
29
+ }
30
+
31
+ .showSetting-inner::-webkit-scrollbar-track {
32
+ background: #2a2a2a; /* Darker track background */
33
+ border-radius: 5px;
34
+ }
35
+
36
+ .showSetting-inner::-webkit-scrollbar-thumb {
37
+ background: #444; /* Darker scrollbar handle */
38
+ border-radius: 5px;
39
+ }
40
+
41
+ .showSetting-inner::-webkit-scrollbar-thumb:hover {
42
+ background: #555; /* Lighter on hover */
43
+ }
44
+
45
+ /* Setting inner container */
46
+ .showSetting-inner {
47
+ position: relative;
48
+ scrollbar-color: #444 #2a2a2a; /* Scrollbar thumb and track */
49
+ scrollbar-width: thin;
50
+ padding-top: 4.5rem;
51
+ }
52
+
53
+ /* Ensure the close button stays fixed */
54
+ .showSetting-inner .close-btn {
55
+ position: absolute;
56
+ top: 16px;
57
+ right: 16px;
58
+ background: none;
59
+ color: white;
60
+ padding: 7px;
61
+ border-radius: 5px;
62
+ cursor: pointer;
63
+ }
64
+
65
+ /* Close button hover effect */
66
+ .showSetting-inner .close-btn:hover {
67
+ background: rgba(255, 255, 255, 0.1);
68
+ color: white;
69
+ }
70
+
71
+ /* Ensure the title stays at the top */
72
+ .showSetting-inner .setting-size {
73
+ position: absolute;
74
+ font-weight: bold;
75
+ font-size: 1.5rem;
76
+ top: 16px;
77
+ left: 16px;
78
+ }
79
+
80
+ /* Form groups styling */
81
+ .form-group {
82
+ margin-bottom: 20px;
83
+ display: flex;
84
+ flex-direction: column;
85
+ align-items: flex-start;
86
+ }
87
+
88
+ .form-group label {
89
+ font-size: large;
90
+ margin-bottom: 5px;
91
+ }
92
+
93
+ .sliders-container {
94
+ display: flex;
95
+ justify-content: space-between;
96
+ gap: 30px;
97
+ width: 100%;
98
+ }
99
+
100
+ .slider-item {
101
+ flex: 1; /* Each slider item will take up equal available space */
102
+ width: 100%;
103
+ }
104
+
105
+ /* Container for password input and icon */
106
+ .password-wrapper {
107
+ position: relative;
108
+ width: 100%;
109
+ }
110
+
111
+ /* Style the input to have extra padding on the right so text doesn’t run under the icon */
112
+ .password-wrapper input {
113
+ width: 100%;
114
+ padding-right: 40px; /* Adjust based on the icon size */
115
+ }
116
+
117
+ .password-wrapper .password-toggle {
118
+ position: absolute !important;
119
+ color: #DDD;
120
+ top: 50% !important;
121
+ left: 95% !important;
122
+ transform: translateY(-50%) !important;
123
+ }
124
+
125
+ /* Slider styling */
126
+ .slider-item {
127
+ text-align: center;
128
+ }
129
+
130
+ input, select, textarea {
131
+ background: #1E1E1E;
132
+ color: #DDD;
133
+ border: 1px solid #444;
134
+ padding: 10px;
135
+ border-radius: 5px;
136
+ width: 100%;
137
+ font-size: 16px;
138
+ transition: border 0.3s ease, background 0.3s ease;
139
+ }
140
+
141
+ /* Text for re-applying settings snackbar*/
142
+ .re-applying-settings-text {
143
+ color: #DDD;
144
+ margin-top: -0.5rem;
145
+ }
146
+
147
+ /* Spinner styling */
148
+ .re-applying-settings-custom-spinner {
149
+ width: 1.2rem !important;
150
+ height: 1.2rem !important;
151
+ border: 2.5px solid #ececece1 !important; /* Main Spinner */
152
+ border-top: 2.5px solid #303030 !important; /* Rotating path */
153
+ border-radius: 50% !important;
154
+ margin-top: -0.5rem !important;
155
+ animation: spin 0.9s linear infinite !important;
156
+ }
157
+
158
+ /* Spinner animation */
159
+ @keyframes spin {
160
+ 0% {
161
+ transform: rotate(0deg);
162
+ }
163
+ 100% {
164
+ transform: rotate(360deg);
165
+ }
166
+ }
167
+
168
+ /* Mobile Responsiveness */
169
+ @media (max-width: 768px) {
170
+ .showSetting-inner {
171
+ width: 90%;
172
+ max-height: 75vh; /* Adjust height for smaller screens */
173
+ }
174
+ }
frontend/src/Components/IntialSetting.js ADDED
@@ -0,0 +1,316 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState, useRef } from 'react';
2
+ import { useNavigate } from 'react-router-dom';
3
+ import Box from '@mui/material/Box';
4
+ import Slider from '@mui/material/Slider';
5
+ import Stack from '@mui/material/Stack';
6
+ import Button from '@mui/material/Button';
7
+ import IconButton from '@mui/material/IconButton';
8
+ import './IntialSetting.css';
9
+ import { FaTimes, FaEye, FaEyeSlash } from 'react-icons/fa';
10
+
11
+ function IntialSetting(props) {
12
+ // State variables for form controls
13
+ const [selectedProvider, setSelectedProvider] = useState("OpenAI");
14
+ const [modelTemperature, setModelTemperature] = useState(0.0);
15
+ const [modelTopP, setModelTopP] = useState(1.0);
16
+ const [showPassword, setShowPassword] = useState(false);
17
+
18
+ // Ref for the form and navigation hook
19
+ const formRef = useRef(null);
20
+ const navigate = useNavigate();
21
+
22
+ // Model options for different providers
23
+ const modelOptions = {
24
+ OpenAI: {
25
+ "GPT 4o": "gpt-4o",
26
+ "GPT 4o Latest": "gpt-4o-2024-11-20",
27
+ "GPT 4o Mini": "gpt-4o-mini",
28
+ "GPT 4.1": "gpt-4.1",
29
+ "GPT 4.1 Mini": "gpt-4.1-mini",
30
+ "GPT 4.1 Nano": "gpt-4.1-nano",
31
+ "ChatGPT": "chatgpt-4o-latest"
32
+ },
33
+ Anthropic: {
34
+ "Claude 4 Opus": "claude-opus-4-20250514",
35
+ "Claude Sonnet 4": "claude-sonnet-4-20250514",
36
+ "Claude Sonnet 3.7": "claude-3-7-sonnet-20250219",
37
+ "Claude Sonnet 3.5": "claude-3-5-sonnet-20241022",
38
+ "Claude Haiku 3.5": "claude-3-5-haiku-20241022",
39
+ "Claude Opus 3": "claude-3-opus-20240229",
40
+ "Claude Sonnet 3": "claude-3-sonnet-20240229",
41
+ "Claude Haiku 3": "claude-3-haiku-20240307"
42
+ },
43
+ Google: {
44
+ "Gemini 2.0 Flash Lite": "gemini-2.0-flash-lite",
45
+ "Gemini 2.0 Flash": "gemini-2.0-flash",
46
+ "Gemini 2.5 Flash Lite": "gemini-2.5-flash-lite-preview-06-17",
47
+ "Gemini 2.5 Flash": "gemini-2.5-flash",
48
+ "Gemini 2.5 Pro": "gemini-2.5-pro"
49
+ },
50
+ XAI: {
51
+ "Grok 2": "grok-2",
52
+ "Grok 3 Mini": "grok-3-mini-latest",
53
+ "Grok 3 Mini (Fast)": "grok-3-mini-fast-latest",
54
+ "Grok 3": "grok-3-latest",
55
+ "Grok 3 (Fast)": "grok-3-fast-latest"
56
+ },
57
+ };
58
+
59
+ // Reset form and state variables
60
+ const handleReset = (e) => {
61
+ e.preventDefault();
62
+ if (formRef.current) {
63
+ formRef.current.reset();
64
+ }
65
+ setSelectedProvider("OpenAI");
66
+ setModelTemperature(0.0);
67
+ setModelTopP(1.0);
68
+ };
69
+
70
+ // Handle form submission and save settings
71
+ const handleSave = async (e) => {
72
+ e.preventDefault();
73
+ const form = formRef.current;
74
+
75
+ // Retrieve form values
76
+ const modelProvider = form.elements["model-provider"].value;
77
+ const modelName = form.elements["model-name"].value;
78
+ const modelAPIKeys = form.elements["model-api"].value;
79
+ const braveAPIKey = form.elements["brave-api"].value;
80
+ const proxyList = form.elements["proxy-list"].value;
81
+
82
+ // Check for missing required fields
83
+ const missingFields = [];
84
+ if (!modelProvider || modelProvider.trim() === "") missingFields.push("Model Provider");
85
+ if (!modelName || modelName.trim() === "") missingFields.push("Model Name");
86
+ if (!modelAPIKeys || modelAPIKeys.trim() === "") missingFields.push("Model API Key");
87
+ if (!braveAPIKey || braveAPIKey.trim() === "") missingFields.push("Brave Search API Key");
88
+
89
+ if (missingFields.length > 0) {
90
+ props.openSnackbar(
91
+ "Please fill in the following required fields: " + missingFields.join(", "),
92
+ "error"
93
+ );
94
+ return;
95
+ }
96
+
97
+ // Build payload for backend
98
+ const payload = {
99
+ "Model_Provider": modelProvider.toLowerCase(),
100
+ "Model_Name": modelName,
101
+ "Model_API_Keys": modelAPIKeys,
102
+ "Brave_Search_API_Key": braveAPIKey,
103
+ "Model_Temperature": modelTemperature,
104
+ "Model_Top_P": modelTopP,
105
+ };
106
+
107
+ if (proxyList && proxyList.trim() !== "") {
108
+ payload["Proxy_List"] = proxyList;
109
+ }
110
+
111
+ // Show appropriate notification based on context
112
+ if (props.fromAiPage) {
113
+ props.openSnackbar(
114
+ <Box mt={1} display="flex" alignItems="center">
115
+ <Box className="re-applying-settings-custom-spinner" />
116
+ <Box ml={1} className="re-applying-settings-text">
117
+ <span>Re-applying settings. This may take a few minutes...</span>
118
+ </Box>
119
+ </Box>,
120
+ "info"
121
+ );
122
+ } else {
123
+ props.openSnackbar("Settings saved successfully!", "success");
124
+ if (props.onInitializationStart) {
125
+ props.onInitializationStart();
126
+ }
127
+ }
128
+
129
+ // Send settings to backend
130
+ try {
131
+ const response = await fetch("/settings", {
132
+ method: "POST",
133
+ headers: {
134
+ "Content-Type": "application/json",
135
+ },
136
+ body: JSON.stringify(payload),
137
+ });
138
+
139
+ if (response.ok) {
140
+ const data = await response.json();
141
+ if (data.success === true) {
142
+ if (props.fromAiPage) {
143
+ props.openSnackbar("Settings saved successfully!", "success");
144
+ }
145
+ navigate("/AiPage");
146
+ } else {
147
+ props.openSnackbar("Error saving settings. Please try again.", "error");
148
+ }
149
+ } else {
150
+ props.openSnackbar("Error saving settings. Please try again.", "error");
151
+ }
152
+ } catch (error) {
153
+ console.error("Error saving settings:", error);
154
+ props.openSnackbar("Error saving settings. Please try again.", "error");
155
+ }
156
+ };
157
+
158
+ // Render the settings modal
159
+ return props.trigger ? (
160
+ <div className="showSetting" onClick={() => props.setTrigger(false)}>
161
+ <div className="showSetting-inner" onClick={(e) => e.stopPropagation()}>
162
+ <label className="setting-size">Settings</label>
163
+ <button className="close-btn" onClick={() => props.setTrigger(false)}>
164
+ <FaTimes />
165
+ </button>
166
+ <form ref={formRef}>
167
+ <div className="form-group">
168
+ <label htmlFor="model-provider">Model Provider</label>
169
+ <select
170
+ id="model-provider"
171
+ name="model-provider"
172
+ value={selectedProvider}
173
+ onChange={(e) => setSelectedProvider(e.target.value)}
174
+ >
175
+ {Object.keys(modelOptions).map(provider => (
176
+ <option key={provider} value={provider}>
177
+ {provider}
178
+ </option>
179
+ ))}
180
+ </select>
181
+ </div>
182
+ <div className="form-group">
183
+ <label htmlFor="model-name">Model Name</label>
184
+ <select id="model-name" name="model-name">
185
+ {Object.entries(modelOptions[selectedProvider]).map(
186
+ ([displayName, backendName]) => (
187
+ <option key={backendName} value={backendName}>
188
+ {displayName}
189
+ </option>
190
+ )
191
+ )}
192
+ </select>
193
+ </div>
194
+ <div className="form-group">
195
+ <label htmlFor="model-api">Model API Key</label>
196
+ <textarea
197
+ id="model-api"
198
+ name="model-api"
199
+ placeholder="Enter API Key, one per line"
200
+ ></textarea>
201
+ </div>
202
+ <div className="form-group">
203
+ <label htmlFor="brave-api">Brave Search API Key</label>
204
+ <input
205
+ type="text"
206
+ id="brave-api"
207
+ name="brave-api"
208
+ placeholder="Enter API Key"
209
+ />
210
+ </div>
211
+ <div className="form-group">
212
+ <label htmlFor="proxy-list">Proxy List</label>
213
+ <textarea
214
+ id="proxy-list"
215
+ name="proxy-list"
216
+ placeholder="Enter proxies, one per line"
217
+ ></textarea>
218
+ </div>
219
+ {/* Commented Neo4j configuration fields */}
220
+ {/* <div className="form-group">
221
+ <label htmlFor="neo4j-url">Neo4j URL</label>
222
+ <input
223
+ type="text"
224
+ id="neo4j-url"
225
+ name="neo4j-url"
226
+ placeholder="Enter Neo4j URL"
227
+ />
228
+ </div>
229
+ <div className="form-group">
230
+ <label htmlFor="neo4j-username">Neo4j Username</label>
231
+ <input
232
+ type="text"
233
+ id="neo4j-username"
234
+ name="neo4j-username"
235
+ placeholder="Enter Username"
236
+ />
237
+ </div>
238
+ <div className="form-group">
239
+ <label htmlFor="neo4j-password">Neo4j Password</label>
240
+ <div className="password-wrapper">
241
+ <input
242
+ type={showPassword ? "text" : "password"}
243
+ id="neo4j-password"
244
+ name="neo4j-password"
245
+ placeholder="Enter Password"
246
+ />
247
+ <IconButton
248
+ onClick={() => setShowPassword(prev => !prev)}
249
+ className="password-toggle"
250
+ sx={{
251
+ color: "white",
252
+ p: 0,
253
+ m: 0
254
+ }}
255
+ >
256
+ {showPassword ? <FaEyeSlash /> : <FaEye />}
257
+ </IconButton>
258
+ </div>
259
+ </div> */}
260
+ <div className="form-group">
261
+ <div className="sliders-container">
262
+ <div className="slider-item">
263
+ <label htmlFor="temperature">Temperature</label>
264
+ <Slider
265
+ id="temperature"
266
+ value={modelTemperature}
267
+ onChange={(e, newValue) => setModelTemperature(newValue)}
268
+ step={0.05}
269
+ min={0.0}
270
+ max={1.0}
271
+ valueLabelDisplay="auto"
272
+ sx={{ width: '100%', color: 'success.main' }}
273
+ />
274
+ </div>
275
+ <div className="slider-item">
276
+ <label htmlFor="top-p">Top-P</label>
277
+ <Slider
278
+ id="top-p"
279
+ value={modelTopP}
280
+ onChange={(e, newValue) => setModelTopP(newValue)}
281
+ step={0.05}
282
+ min={0.0}
283
+ max={1.0}
284
+ valueLabelDisplay="auto"
285
+ sx={{ width: '100%', color: 'success.main' }}
286
+ />
287
+ </div>
288
+ </div>
289
+ </div>
290
+ <Stack direction="row" spacing={2} sx={{ justifyContent: 'flex-end' }}>
291
+ <Button
292
+ type="button"
293
+ className="reset-btn"
294
+ sx={{ color: "#2196f3" }}
295
+ onClick={handleReset}
296
+ >
297
+ Reset
298
+ </Button>
299
+ <Button
300
+ type="button"
301
+ variant="contained"
302
+ color="success"
303
+ className="save-btn"
304
+ onClick={handleSave}
305
+ >
306
+ Save
307
+ </Button>
308
+ </Stack>
309
+ </form>
310
+ {props.children}
311
+ </div>
312
+ </div>
313
+ ) : null;
314
+ }
315
+
316
+ export default IntialSetting;
frontend/src/Components/settings-gear-1.svg ADDED
frontend/src/Icons/bot.png ADDED
frontend/src/Icons/copy.png ADDED
frontend/src/Icons/evaluate.png ADDED
frontend/src/Icons/excerpts.png ADDED