Hemang Thakur commited on
Commit
4aefbee
·
1 Parent(s): 349a5a7

ready to deploy

Browse files
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. Dockerfile +74 -0
  3. README.md +6 -4
  4. frontend/package-lock.json +0 -0
  5. frontend/package.json +55 -0
  6. frontend/public/favicon.ico +0 -0
  7. frontend/public/index.html +43 -0
  8. frontend/public/logo192.png +0 -0
  9. frontend/public/logo512.png +0 -0
  10. frontend/public/manifest.json +25 -0
  11. frontend/public/robots.txt +3 -0
  12. frontend/src/App.css +40 -0
  13. frontend/src/App.js +95 -0
  14. frontend/src/App.test.js +8 -0
  15. frontend/src/Components/AiComponents/ChatComponents/Evaluate.css +113 -0
  16. frontend/src/Components/AiComponents/ChatComponents/Evaluate.js +244 -0
  17. frontend/src/Components/AiComponents/ChatComponents/FormSection.js +9 -0
  18. frontend/src/Components/AiComponents/ChatComponents/Graph.css +85 -0
  19. frontend/src/Components/AiComponents/ChatComponents/Graph.js +73 -0
  20. frontend/src/Components/AiComponents/ChatComponents/LeftSideBar.js +38 -0
  21. frontend/src/Components/AiComponents/ChatComponents/LeftSidebar.css +59 -0
  22. frontend/src/Components/AiComponents/ChatComponents/RightSidebar.css +138 -0
  23. frontend/src/Components/AiComponents/ChatComponents/RightSidebar.js +142 -0
  24. frontend/src/Components/AiComponents/ChatComponents/Sources.css +70 -0
  25. frontend/src/Components/AiComponents/ChatComponents/Sources.js +124 -0
  26. frontend/src/Components/AiComponents/ChatComponents/Streaming.css +120 -0
  27. frontend/src/Components/AiComponents/ChatComponents/Streaming.js +82 -0
  28. frontend/src/Components/AiComponents/ChatWindow.css +267 -0
  29. frontend/src/Components/AiComponents/ChatWindow.js +265 -0
  30. frontend/src/Components/AiPage.css +247 -0
  31. frontend/src/Components/AiPage.js +502 -0
  32. frontend/src/Components/IntialSetting.css +174 -0
  33. frontend/src/Components/IntialSetting.js +309 -0
  34. frontend/src/Components/settings-gear-1.svg +47 -0
  35. frontend/src/Icons/bot.png +0 -0
  36. frontend/src/Icons/copy.png +0 -0
  37. frontend/src/Icons/evaluate.png +0 -0
  38. frontend/src/Icons/graph.png +0 -0
  39. frontend/src/Icons/loading.png +0 -0
  40. frontend/src/Icons/reprocess.png +0 -0
  41. frontend/src/Icons/settings-2.svg +56 -0
  42. frontend/src/Icons/settings.png +0 -0
  43. frontend/src/Icons/sources.png +0 -0
  44. frontend/src/Icons/thinking.gif +0 -0
  45. frontend/src/Icons/user.png +0 -0
  46. frontend/src/index.css +18 -0
  47. frontend/src/index.js +17 -0
  48. frontend/src/logo.svg +1 -0
  49. frontend/src/reportWebVitals.js +13 -0
  50. frontend/src/setupTests.js +5 -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
Dockerfile ADDED
@@ -0,0 +1,74 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ # Expose the port
71
+ EXPOSE ${PORT:-7860}
72
+
73
+ # Start the FastAPI backend using Uvicorn, reading the PORT env variable
74
+ CMD ["sh", "-c", "uvicorn main:app --host 0.0.0.0 --port ${PORT:-7860}"]
README.md CHANGED
@@ -1,10 +1,12 @@
1
  ---
2
- title: Seekr Copy
3
- emoji: 🏢
4
- colorFrom: pink
5
- colorTo: green
6
  sdk: docker
7
  pinned: false
 
 
8
  ---
9
 
10
  Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
1
  ---
2
+ title: Seekr
3
+ emoji: 👀
4
+ colorFrom: blue
5
+ colorTo: yellow
6
  sdk: docker
7
  pinned: false
8
+ license: unknown
9
+ short_description: AI Search Project
10
  ---
11
 
12
  Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
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,55 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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.4",
15
+ "react": "^19.0.0",
16
+ "react-dom": "^19.0.0",
17
+ "react-icons": "^5.4.0",
18
+ "react-markdown": "^9.1.0",
19
+ "react-router-dom": "^7.1.3",
20
+ "react-scripts": "5.0.1",
21
+ "react-syntax-highlighter": "^15.5.0",
22
+ "rehype-highlight": "^7.0.2",
23
+ "rehype-katex": "^6.0.2",
24
+ "rehype-raw": "^6.1.1",
25
+ "rehype-sanitize": "^5.0.1",
26
+ "remark-gfm": "^3.0.1",
27
+ "remark-math": "^5.1.1",
28
+ "styled-components": "^6.1.14",
29
+ "web-vitals": "^4.2.4"
30
+ },
31
+ "scripts": {
32
+ "start": "react-scripts start",
33
+ "build": "react-scripts build",
34
+ "test": "react-scripts test",
35
+ "eject": "react-scripts eject"
36
+ },
37
+ "eslintConfig": {
38
+ "extends": [
39
+ "react-app",
40
+ "react-app/jest"
41
+ ]
42
+ },
43
+ "browserslist": {
44
+ "production": [
45
+ ">0.2%",
46
+ "not dead",
47
+ "not op_mini all"
48
+ ],
49
+ "development": [
50
+ "last 1 chrome version",
51
+ "last 1 firefox version",
52
+ "last 1 safari version"
53
+ ]
54
+ }
55
+ }
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,244 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from 'react';
2
+ import ReactMarkdown from 'react-markdown';
3
+ import { useTheme } from '@mui/material/styles';
4
+ import Box from '@mui/material/Box';
5
+ import OutlinedInput from '@mui/material/OutlinedInput';
6
+ import InputLabel from '@mui/material/InputLabel';
7
+ import MenuItem from '@mui/material/MenuItem';
8
+ import FormControl from '@mui/material/FormControl';
9
+ import Select from '@mui/material/Select';
10
+ import Chip from '@mui/material/Chip';
11
+ import Button from '@mui/material/Button';
12
+ import Typography from '@mui/material/Typography';
13
+ import './Evaluate.css';
14
+
15
+ const MenuProps = {
16
+ PaperProps: {
17
+ className: 'evaluate-menu',
18
+ },
19
+ disableScrollLock: true
20
+ };
21
+
22
+ function getStyles(name, selectedNames, theme) {
23
+ return {
24
+ fontWeight: selectedNames.includes(name.toLowerCase())
25
+ ? theme.typography.fontWeightMedium
26
+ : theme.typography.fontWeightRegular,
27
+ };
28
+ }
29
+
30
+ export default function MultipleSelectChip({ evaluation }) {
31
+ const theme = useTheme();
32
+ const [personName, setPersonName] = React.useState([]);
33
+ const [selectedMetrics, setSelectedMetrics] = React.useState([]);
34
+ const [evaluationResult, setEvaluationResult] = React.useState("");
35
+ const [isEvaluating, setIsEvaluating] = React.useState(false);
36
+ const [localLoading, setLocalLoading] = React.useState(false);
37
+ const [noMetricsError, setNoMetricsError] = React.useState("");
38
+ const [metricOptions, setMetricOptions] = React.useState([]);
39
+
40
+ React.useEffect(() => {
41
+ // If 'contents' is undefined in the payload
42
+ if (evaluation && evaluation.contents === undefined) {
43
+ setMetricOptions([
44
+ "Bias",
45
+ "Toxicity",
46
+ "Summarization",
47
+ "Answer Correctness",
48
+ ]);
49
+ } else {
50
+ // Else, all except "Answer Correctness"
51
+ setMetricOptions([
52
+ "Bias",
53
+ "Toxicity",
54
+ "Summarization",
55
+ "Faithfulness",
56
+ "Hallucination",
57
+ "Answer Relevancy",
58
+ "Contextual Relevancy",
59
+ "Contextual Recall",
60
+ ]);
61
+ }
62
+ }, [evaluation]);
63
+
64
+ // Reset the form fields
65
+ React.useEffect(() => {
66
+ // Reset the form and evaluation result
67
+ setPersonName([]);
68
+ setSelectedMetrics([]);
69
+ setEvaluationResult("");
70
+ setLocalLoading(true);
71
+ setNoMetricsError("");
72
+
73
+ // Simulate a loading delay
74
+ const timer = setTimeout(() => {
75
+ setLocalLoading(false);
76
+ }, 500);
77
+ return () => clearTimeout(timer);
78
+ }, [evaluation]);
79
+
80
+ const handleChange = (event) => {
81
+ const { target: { value } } = event;
82
+ const metrics = typeof value === 'string' ? value.split(',') : value;
83
+ setPersonName(metrics);
84
+ setSelectedMetrics(metrics);
85
+ setNoMetricsError("");
86
+ };
87
+
88
+ const handleDelete = (chipToDelete) => {
89
+ setPersonName((chips) => chips.filter((chip) => chip !== chipToDelete));
90
+ };
91
+
92
+ // Function to convert a string to title case.
93
+ const titleCase = (str) => {
94
+ return str
95
+ .split(' ')
96
+ .map(word => word.charAt(0).toUpperCase() + word.slice(1))
97
+ .join(' ');
98
+ };
99
+
100
+ const handleEvaluateClick = async () => {
101
+ // Clear previous evaluation result immediately.
102
+ setEvaluationResult("");
103
+
104
+ // Check if no metrics selected
105
+ if (selectedMetrics.length === 0) {
106
+ setNoMetricsError("No metrics selected");
107
+ return;
108
+ }
109
+
110
+ setNoMetricsError("");
111
+ setIsEvaluating(true);
112
+
113
+ const payload = { ...evaluation, metrics: selectedMetrics };
114
+ try {
115
+ const res = await fetch("/action/evaluate", {
116
+ method: "POST",
117
+ headers: { "Content-Type": "application/json" },
118
+ body: JSON.stringify(payload),
119
+ });
120
+ if (!res.ok) {
121
+ const error = await res.json();
122
+ throw new Error(`Evaluation Error: ${error.error}`);
123
+ }
124
+
125
+ const data = await res.json();
126
+ if (!data.result) {
127
+ throw new Error("No results returned from evaluation");
128
+ }
129
+
130
+ // Format the JSON into Markdown.
131
+ let markdown = "### Result\n\n";
132
+ for (const [metric, details] of Object.entries(data.result)) {
133
+ let score = details.score;
134
+ if (typeof score === "number") {
135
+ const percentage = score * 100;
136
+ score = Number.isInteger(percentage)
137
+ ? percentage.toFixed(0) + "%"
138
+ : percentage.toFixed(2) + "%";
139
+ }
140
+ let reason = details.reason;
141
+ markdown += `**${titleCase(metric)}:** ${score}\n\n${reason}\n\n`;
142
+ }
143
+ setEvaluationResult(markdown);
144
+ } catch (err) {
145
+ // Use the callback to trigger the error block in ChatWindow
146
+ if (evaluation.onError && evaluation.blockId) {
147
+ evaluation.onError(evaluation.blockId, err.message || "Evaluation failed");
148
+ }
149
+ else {
150
+ console.error("Evaluation prop is missing or incomplete:", evaluation);
151
+ }
152
+ }
153
+ setIsEvaluating(false);
154
+ };
155
+
156
+ // Finds the matching display name for a metric.
157
+ const getDisplayName = (lowerValue) => {
158
+ const found = metricOptions.find(n => n.toLowerCase() === lowerValue);
159
+ return found ? found : lowerValue;
160
+ };
161
+
162
+ return (
163
+ <Box className="evaluate-container">
164
+ {localLoading ? (
165
+ <Box>
166
+ <Typography variant="body2">Loading Evaluation...</Typography>
167
+ </Box>
168
+ ) : (
169
+ <>
170
+ <FormControl className="evaluate-form-control">
171
+ <InputLabel id="chip-label">Select Metrics</InputLabel>
172
+ <Select
173
+ labelId="chip-label"
174
+ id="multiple-chip"
175
+ multiple
176
+ value={personName}
177
+ onChange={handleChange}
178
+ input={
179
+ <OutlinedInput
180
+ id="select-multiple-chip"
181
+ label="Select Metrics"
182
+ className="evaluate-outlined-input"
183
+ />
184
+ }
185
+ renderValue={(selected) => (
186
+ <Box className="chip-container">
187
+ {selected.map((value) => (
188
+ <Chip
189
+ className="evaluate-chip"
190
+ key={value}
191
+ label={getDisplayName(value)}
192
+ onDelete={() => handleDelete(value)}
193
+ onMouseDown={(event) => event.stopPropagation()}
194
+ />
195
+ ))}
196
+ </Box>
197
+ )}
198
+ MenuProps={MenuProps}
199
+ >
200
+ {metricOptions.map((name) => (
201
+ <MenuItem
202
+ key={name}
203
+ value={name.toLowerCase()} // underlying value is lowercase
204
+ style={getStyles(name, personName, theme)}
205
+ >
206
+ {name}
207
+ </MenuItem>
208
+ ))}
209
+ </Select>
210
+ </FormControl>
211
+
212
+ <Box mt={1}>
213
+ <Button
214
+ variant="contained"
215
+ onClick={handleEvaluateClick}
216
+ className="evaluate-button"
217
+ >
218
+ Evaluate
219
+ </Button>
220
+ </Box>
221
+
222
+ {noMetricsError && (
223
+ <Box className="no-metrics-message">
224
+ {noMetricsError}
225
+ </Box>
226
+ )}
227
+
228
+ {isEvaluating && (
229
+ <Box mt={1} display="flex" alignItems="center">
230
+ <Box className="custom-spinner" />
231
+ <Box ml={1}>Evaluating...</Box>
232
+ </Box>
233
+ )}
234
+
235
+ {evaluationResult && (
236
+ <Box mt={2}>
237
+ <ReactMarkdown>{evaluationResult}</ReactMarkdown>
238
+ </Box>
239
+ )}
240
+ </>
241
+ )}
242
+ </Box>
243
+ );
244
+ }
frontend/src/Components/AiComponents/ChatComponents/FormSection.js ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ const { GoogleGenerativeAI } = require("@google/generative-ai");
2
+
3
+ const genAI = new GoogleGenerativeAI("AIzaSyCx3MefHEMw2MNfzB2fI2IvpBnWBGLirmg");
4
+ const model = genAI.getGenerativeModel({ model: "gemini-1.5-flash" });
5
+
6
+ const prompt = "Explain how AI works";
7
+
8
+ const result = await model.generateContent(prompt);
9
+ console.log(result.response.text());
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/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/ChatComponents/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/ChatComponents/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/ChatComponents/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 './Sources';
6
+ import Evaluate from './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/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,120 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .streaming-content {
2
+ font-family: inherit;
3
+ line-height: 2rem;
4
+ white-space: pre-wrap;
5
+ word-wrap: break-word;
6
+ margin: 0;
7
+ padding: 0;
8
+ }
9
+
10
+ /* Reset margin/padding for all descendants */
11
+ .streaming-content * {
12
+ margin: 0;
13
+ padding: 0;
14
+ }
15
+
16
+ /* Top-level elements */
17
+ .streaming-content > * {
18
+ margin-top: 0.5rem;
19
+ margin-bottom: 0.5rem;
20
+ }
21
+
22
+ /* VERY FIRST element in an AI answer */
23
+ .streaming-content > *:first-child {
24
+ margin-top: 0 !important;
25
+ }
26
+
27
+ /* Headings */
28
+ .streaming-content h1,
29
+ .streaming-content h2,
30
+ .streaming-content h3,
31
+ .streaming-content h4,
32
+ .streaming-content h5,
33
+ .streaming-content h6 {
34
+ margin-top: 1rem;
35
+ margin-bottom: 0.75rem;
36
+ }
37
+
38
+ /* If heading is the very first element */
39
+ .streaming-content > h1:first-child,
40
+ .streaming-content > h2:first-child,
41
+ .streaming-content > h3:first-child,
42
+ .streaming-content > h4:first-child,
43
+ .streaming-content > h5:first-child,
44
+ .streaming-content > h6:first-child {
45
+ margin-top: 0 !important;
46
+ }
47
+
48
+ /* Paragraphs */
49
+ .streaming-content p {
50
+ margin-top: 0.25rem;
51
+ margin-bottom: 0.25rem;
52
+ }
53
+
54
+ /* Lists */
55
+ .streaming-content ul,
56
+ .streaming-content ol {
57
+ margin-top: 0.25rem;
58
+ margin-bottom: 0.25rem;
59
+ padding-left: 1.25rem;
60
+ white-space: normal;
61
+ }
62
+
63
+ .streaming-content li {
64
+ margin-bottom: 0.25rem;
65
+ }
66
+
67
+ .streaming-content li ul,
68
+ .streaming-content li ol {
69
+ margin-top: 0.15rem;
70
+ margin-bottom: 0.15rem;
71
+ }
72
+
73
+ /* Code Blocks */
74
+ .code-block-container {
75
+ margin: 0.5rem 0;
76
+ border-radius: 4px;
77
+ background-color: #2b2b2b;
78
+ overflow: hidden;
79
+ }
80
+
81
+ .code-block-header {
82
+ background-color: #1e1e1e;
83
+ color: #ffffff;
84
+ padding: 0.5rem;
85
+ font-size: 0.85rem;
86
+ font-weight: bold;
87
+ }
88
+
89
+ /* Table Container */
90
+ .table-container {
91
+ margin: 0.5rem 0;
92
+ width: 100%;
93
+ overflow-x: auto;
94
+ border: 1px solid #ddd;
95
+ border-radius: 4px;
96
+ }
97
+
98
+ .table-container th,
99
+ .table-container td {
100
+ border: 1px solid #ddd;
101
+ padding: 0.5rem;
102
+ }
103
+
104
+ /* Markdown Links */
105
+ .markdown-link {
106
+ color: #1a73e8;
107
+ text-decoration: none;
108
+ }
109
+ .markdown-link:hover {
110
+ text-decoration: underline;
111
+ }
112
+
113
+ /* Blockquotes */
114
+ .markdown-blockquote {
115
+ border-left: 4px solid #ccc;
116
+ padding-left: 0.75rem;
117
+ margin: 0.5rem 0;
118
+ color: #555;
119
+ font-style: italic;
120
+ }
frontend/src/Components/AiComponents/ChatComponents/Streaming.js ADDED
@@ -0,0 +1,82 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useEffect, useRef } from 'react';
2
+ import ReactMarkdown from 'react-markdown';
3
+ import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
4
+ import { atomDark } from 'react-syntax-highlighter/dist/esm/styles/prism';
5
+ import remarkGfm from 'remark-gfm';
6
+ import rehypeRaw from 'rehype-raw';
7
+ import './Streaming.css';
8
+
9
+ // Streaming component for rendering markdown content
10
+ const Streaming = ({ content, isStreaming, onContentRef }) => {
11
+ const contentRef = useRef(null);
12
+
13
+ useEffect(() => {
14
+ if (contentRef.current && onContentRef) {
15
+ onContentRef(contentRef.current);
16
+ }
17
+ }, [content, onContentRef]);
18
+
19
+ const displayContent = isStreaming ? `${content}▌` : (content || '');
20
+
21
+ return (
22
+ <div className="streaming-content" ref={contentRef}>
23
+ <ReactMarkdown
24
+ remarkPlugins={[remarkGfm]}
25
+ rehypePlugins={[rehypeRaw]}
26
+ components={{
27
+ code({node, inline, className, children, ...props}) {
28
+ const match = /language-(\w+)/.exec(className || '');
29
+ return !inline ? (
30
+ <div className="code-block-container">
31
+ <div className="code-block-header">
32
+ <span>{match ? match[1] : 'code'}</span>
33
+ </div>
34
+ <SyntaxHighlighter
35
+ style={atomDark}
36
+ language={match ? match[1] : 'text'}
37
+ PreTag="div"
38
+ {...props}
39
+ >
40
+ {String(children).replace(/\n$/, '')}
41
+ </SyntaxHighlighter>
42
+ </div>
43
+ ) : (
44
+ <code className={className} {...props}>
45
+ {children}
46
+ </code>
47
+ );
48
+ },
49
+ table({node, ...props}) {
50
+ return (
51
+ <div className="table-container">
52
+ <table {...props} />
53
+ </div>
54
+ );
55
+ },
56
+ a({node, children, href, ...props}) {
57
+ return (
58
+ <a
59
+ href={href}
60
+ target="_blank"
61
+ rel="noopener noreferrer"
62
+ className="markdown-link"
63
+ {...props}
64
+ >
65
+ {children}
66
+ </a>
67
+ );
68
+ },
69
+ blockquote({node, ...props}) {
70
+ return (
71
+ <blockquote className="markdown-blockquote" {...props} />
72
+ );
73
+ }
74
+ }}
75
+ >
76
+ {displayContent}
77
+ </ReactMarkdown>
78
+ </div>
79
+ );
80
+ };
81
+
82
+ export default Streaming;
frontend/src/Components/AiComponents/ChatWindow.css ADDED
@@ -0,0 +1,267 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ cursor: pointer;
141
+ position: relative;
142
+ }
143
+
144
+ /* Apply a brightness filter to the icon images on hover */
145
+ .post-icons .copy-icon img,
146
+ .post-icons .evaluate-icon img,
147
+ .post-icons .sources-icon img,
148
+ .post-icons .graph-icon img {
149
+ transition: filter var(--transition-speed);
150
+ }
151
+
152
+ .post-icons .copy-icon:hover img,
153
+ .post-icons .evaluate-icon:hover img,
154
+ .post-icons .sources-icon:hover img,
155
+ .post-icons .graph-icon:hover img {
156
+ filter: brightness(0.65);
157
+ }
158
+
159
+ /* Tooltip styling */
160
+ .tooltip {
161
+ position: absolute;
162
+ bottom: 100%;
163
+ left: 50%;
164
+ transform: translateX(-50%) translateY(10px) scale(0.9);
165
+ transform-origin: bottom center;
166
+ margin-bottom: 0.65rem;
167
+ padding: 0.3rem 0.6rem;
168
+ background-color: var(--primary-color);
169
+ color: var(--text-color);
170
+ border-radius: 0.25rem;
171
+ white-space: nowrap;
172
+ font-size: 0.85rem;
173
+ opacity: 0;
174
+ visibility: hidden;
175
+ transition: transform 0.3s ease, opacity 0.3s ease;
176
+ }
177
+
178
+ /* Show the tooltip on hover */
179
+ .post-icons .copy-icon:hover .tooltip,
180
+ .post-icons .evaluate-icon:hover .tooltip,
181
+ .post-icons .sources-icon:hover .tooltip,
182
+ .post-icons .graph-icon:hover .tooltip {
183
+ opacity: 1;
184
+ visibility: visible;
185
+ transform: translateX(-50%) translateY(0) scale(1);
186
+ }
187
+
188
+ /* Styling for the question text */
189
+ .question {
190
+ margin: 0;
191
+ white-space: pre-wrap;
192
+ line-height: 1.4;
193
+ }
194
+
195
+ /* Reduce the size of user and bot icons */
196
+ .user-icon img,
197
+ .bot-icon img {
198
+ width: 35px;
199
+ height: 35px;
200
+ object-fit: contain;
201
+ }
202
+
203
+ /* Reduce the size of the post-action icons */
204
+ .post-icons img {
205
+ width: 20px;
206
+ height: 20px;
207
+ object-fit: contain;
208
+ }
209
+
210
+ /* ChatWindow.css */
211
+
212
+ /* Container for the loading state with a dark background */
213
+ .bot-loading {
214
+ display: flex;
215
+ flex-direction: row;
216
+ align-items: center;
217
+ justify-content: center;
218
+ gap: 10px; /* adds space between the spinner and the text */
219
+ padding: 30px;
220
+ background-color: var(--dark-surface); /* Dark background */
221
+ }
222
+
223
+ .loading-text {
224
+ margin: 0; /* removes any default margins */
225
+ font-size: 1rem;
226
+ color: #ccc;
227
+ }
228
+
229
+ /* Finished state: styling for the thought time info */
230
+ .thinking-info {
231
+ margin-bottom: 4px;
232
+ }
233
+
234
+ .thinking-time {
235
+ font-size: 1rem;
236
+ color: #888;
237
+ cursor: pointer;
238
+ }
239
+
240
+ /* Snackbar styling */
241
+ .custom-snackbar {
242
+ background-color: #1488e7 !important;
243
+ color: var(--text-color) !important;
244
+ padding: 0.35rem 1rem !important;
245
+ border-radius: 4px !important;
246
+ }
247
+
248
+ /* Spinner styling */
249
+ .custom-spinner {
250
+ width: 1.35rem !important;
251
+ height: 1.35rem !important;
252
+ border: 3px solid #3b7bdc !important; /* Main Spinner */
253
+ border-top: 3px solid #434343 !important; /* Rotating path */
254
+ border-radius: 50% !important;
255
+ margin-top: 0.1rem !important;
256
+ animation: spin 0.9s linear infinite !important;
257
+ }
258
+
259
+ /* Spinner animation */
260
+ @keyframes spin {
261
+ 0% {
262
+ transform: rotate(0deg);
263
+ }
264
+ 100% {
265
+ transform: rotate(360deg);
266
+ }
267
+ }
frontend/src/Components/AiComponents/ChatWindow.js ADDED
@@ -0,0 +1,265 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useRef, useState, 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 } from 'react-icons/fa';
7
+ import GraphDialog from './ChatComponents/Graph';
8
+ import Streaming from './ChatComponents/Streaming';
9
+ import './ChatWindow.css';
10
+
11
+ import bot from '../../Icons/bot.png';
12
+ import copy from '../../Icons/copy.png';
13
+ import evaluate from '../../Icons/evaluate.png';
14
+ import sourcesIcon from '../../Icons/sources.png';
15
+ import graphIcon from '../../Icons/graph.png';
16
+ import user from '../../Icons/user.png';
17
+
18
+ // SlideTransition function for both entry and exit transitions.
19
+ function SlideTransition(props) {
20
+ return <Slide {...props} direction="up" />;
21
+ }
22
+
23
+ function ChatWindow({
24
+ blockId,
25
+ userMessage,
26
+ tokenChunks,
27
+ aiAnswer,
28
+ thinkingTime,
29
+ thoughtLabel,
30
+ sourcesRead,
31
+ actions,
32
+ tasks,
33
+ openRightSidebar,
34
+ // openLeftSidebar,
35
+ isError,
36
+ errorMessage
37
+ }) {
38
+ const answerRef = useRef(null);
39
+ const [graphDialogOpen, setGraphDialogOpen] = useState(false);
40
+ const [snackbarOpen, setSnackbarOpen] = useState(false);
41
+
42
+ // Get the graph action from the actions prop.
43
+ const graphAction = actions && actions.find(a => a.name === "graph");
44
+
45
+ // Handler for copying answer to clipboard.
46
+ const handleCopy = () => {
47
+ if (answerRef.current) {
48
+ const textToCopy = answerRef.current.innerText || answerRef.current.textContent;
49
+ navigator.clipboard.writeText(textToCopy)
50
+ .then(() => {
51
+ console.log('Copied to clipboard:', textToCopy);
52
+ setSnackbarOpen(true);
53
+ })
54
+ .catch((err) => console.error('Failed to copy text:', err));
55
+ }
56
+ };
57
+
58
+ // Snackbar close handler
59
+ const handleSnackbarClose = (event, reason) => {
60
+ if (reason === 'clickaway') return;
61
+ setSnackbarOpen(false);
62
+ };
63
+
64
+ // Combine partial chunks (tokenChunks) if present; else fall back to the aiAnswer string.
65
+ const combinedAnswer = (tokenChunks && tokenChunks.length > 0)
66
+ ? tokenChunks.join("")
67
+ : aiAnswer;
68
+ const hasTokens = combinedAnswer && combinedAnswer.length > 0;
69
+ // Assume streaming is in progress if thinkingTime is not set.
70
+ const isStreaming = thinkingTime === null || thinkingTime === undefined;
71
+
72
+ // Helper to render the thought label.
73
+ const renderThoughtLabel = () => {
74
+ if (!hasTokens) {
75
+ return thoughtLabel;
76
+ } else {
77
+ if (thoughtLabel && thoughtLabel.startsWith("Thought and searched for")) {
78
+ return thoughtLabel;
79
+ }
80
+ return null;
81
+ }
82
+ };
83
+
84
+ // Helper to render sources read.
85
+ const renderSourcesRead = () => {
86
+ if (!sourcesRead && sourcesRead !== 0) return null;
87
+ return sourcesRead;
88
+ };
89
+
90
+ // When tasks first appear, automatically open the sidebar.
91
+ const prevTasksRef = useRef(tasks);
92
+ useEffect(() => {
93
+ if (prevTasksRef.current.length === 0 && tasks && tasks.length > 0) {
94
+ openRightSidebar("tasks", blockId);
95
+ }
96
+ prevTasksRef.current = tasks;
97
+ }, [tasks, blockId, openRightSidebar]);
98
+
99
+ // Handle getting the reference to the content for copy functionality
100
+ const handleContentRef = (ref) => {
101
+ answerRef.current = ref;
102
+ };
103
+
104
+ return (
105
+ <>
106
+ { !hasTokens ? (
107
+ // If no tokens, render pre-stream UI.
108
+ (!isError && thoughtLabel) ? (
109
+ <div className="answer-container">
110
+ {/* User Message */}
111
+ <div className="message-row user-message">
112
+ <div className="message-bubble user-bubble">
113
+ <p className="question">{userMessage}</p>
114
+ </div>
115
+ <div className="user-icon">
116
+ <img src={user} alt="user icon" />
117
+ </div>
118
+ </div>
119
+ {/* Bot Message (pre-stream with spinner) */}
120
+ <div className="message-row bot-message pre-stream">
121
+ <div className="bot-container">
122
+ <div className="thinking-info">
123
+ <Box mt={1} display="flex" alignItems="center">
124
+ <Box className="custom-spinner" />
125
+ <Box ml={1}>
126
+ <span
127
+ className="thinking-time"
128
+ onClick={() => openRightSidebar("tasks", blockId)}
129
+ >
130
+ {thoughtLabel}
131
+ </span>
132
+ </Box>
133
+ </Box>
134
+ </div>
135
+ </div>
136
+ </div>
137
+ </div>
138
+ ) : (
139
+ // Render without spinner (user message only)
140
+ <div className="answer-container">
141
+ <div className="message-row user-message">
142
+ <div className="message-bubble user-bubble">
143
+ <p className="question">{userMessage}</p>
144
+ </div>
145
+ <div className="user-icon">
146
+ <img src={user} alt="user icon" />
147
+ </div>
148
+ </div>
149
+ </div>
150
+ )
151
+ ) : (
152
+ // Render Full Chat Message
153
+ <div className="answer-container">
154
+ {/* User Message */}
155
+ <div className="message-row user-message">
156
+ <div className="message-bubble user-bubble">
157
+ <p className="question">{userMessage}</p>
158
+ </div>
159
+ <div className="user-icon">
160
+ <img src={user} alt="user icon" />
161
+ </div>
162
+ </div>
163
+ {/* Bot Message */}
164
+ <div className="message-row bot-message">
165
+ <div className="bot-container">
166
+ {!isError && renderThoughtLabel() && (
167
+ <div className="thinking-info">
168
+ <span
169
+ className="thinking-time"
170
+ onClick={() => openRightSidebar("tasks", blockId)}
171
+ >
172
+ {renderThoughtLabel()}
173
+ </span>
174
+ </div>
175
+ )}
176
+ {renderSourcesRead() !== null && (
177
+ <div className="sources-read-container">
178
+ <p className="sources-read">
179
+ Sources Read: {renderSourcesRead()}
180
+ </p>
181
+ </div>
182
+ )}
183
+ <div className="answer-block">
184
+ <div className="bot-icon">
185
+ <img src={bot} alt="bot icon" />
186
+ </div>
187
+ <div className="message-bubble bot-bubble">
188
+ <div className="answer">
189
+ <Streaming
190
+ content={combinedAnswer}
191
+ isStreaming={isStreaming}
192
+ onContentRef={handleContentRef}
193
+ />
194
+ </div>
195
+ </div>
196
+ <div className="post-icons">
197
+ {!isStreaming && (
198
+ <div className="copy-icon" onClick={handleCopy}>
199
+ <img src={copy} alt="copy icon" />
200
+ <span className="tooltip">Copy</span>
201
+ </div>
202
+ )}
203
+ {actions && actions.some(a => a.name === "evaluate") && (
204
+ <div className="evaluate-icon" onClick={() => openRightSidebar("evaluate", blockId)}>
205
+ <img src={evaluate} alt="evaluate icon" />
206
+ <span className="tooltip">Evaluate</span>
207
+ </div>
208
+ )}
209
+ {actions && actions.some(a => a.name === "sources") && (
210
+ <div className="sources-icon" onClick={() => openRightSidebar("sources", blockId)}>
211
+ <img src={sourcesIcon} alt="sources icon" />
212
+ <span className="tooltip">Sources</span>
213
+ </div>
214
+ )}
215
+ {actions && actions.some(a => a.name === "graph") && (
216
+ <div className="graph-icon" onClick={() => setGraphDialogOpen(true)}>
217
+ <img src={graphIcon} alt="graph icon" />
218
+ <span className="tooltip">View Graph</span>
219
+ </div>
220
+ )}
221
+ </div>
222
+ </div>
223
+ </div>
224
+ </div>
225
+ {/* Render the GraphDialog when graphDialogOpen is true */}
226
+ {graphDialogOpen && (
227
+ <GraphDialog
228
+ open={graphDialogOpen}
229
+ onClose={() => setGraphDialogOpen(false)}
230
+ payload={graphAction ? graphAction.payload : { query: userMessage }}
231
+ />
232
+ )}
233
+ </div>
234
+ )}
235
+ {/* Render error container if there's an error */}
236
+ {isError && (
237
+ <div className="error-block" style={{ marginTop: '1rem' }}>
238
+ <h3>Error</h3>
239
+ <p>{errorMessage}</p>
240
+ </div>
241
+ )}
242
+ <Snackbar
243
+ open={snackbarOpen}
244
+ autoHideDuration={3000}
245
+ onClose={handleSnackbarClose}
246
+ message="Copied To Clipboard"
247
+ anchorOrigin={{ vertical: 'bottom', horizontal: 'left' }}
248
+ TransitionComponent={SlideTransition}
249
+ ContentProps={{ classes: { root: 'custom-snackbar' } }}
250
+ action={
251
+ <IconButton
252
+ size="small"
253
+ aria-label="close"
254
+ color="inherit"
255
+ onClick={handleSnackbarClose}
256
+ >
257
+ <FaTimes />
258
+ </IconButton>
259
+ }
260
+ />
261
+ </>
262
+ );
263
+ }
264
+
265
+ export default ChatWindow;
frontend/src/Components/AiPage.css ADDED
@@ -0,0 +1,247 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ }
11
+
12
+ /* Global font settings */
13
+ html, body {
14
+ font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
15
+ margin: 0;
16
+ padding: 0;
17
+ background: var(--dark-surface);
18
+ }
19
+
20
+ /* Main container styling */
21
+ .app-container {
22
+ background: var(--dark-surface);
23
+ color: var(--text-color);
24
+ display: flex;
25
+ min-height: 100vh;
26
+ position: relative;
27
+ overflow-y: auto;
28
+ }
29
+
30
+ /* Main Content */
31
+ .main-content {
32
+ display: flex;
33
+ flex-direction: column;
34
+ align-items: center;
35
+ justify-content: center;
36
+ flex-grow: 1;
37
+ padding: 2rem;
38
+ transition: 0.1s;
39
+ width: 99%;
40
+ max-width: 900px;
41
+ margin: 0 auto;
42
+ }
43
+
44
+ /* Search Area for Initial Mode */
45
+ .search-area h1 {
46
+ margin-bottom: 1.5rem;
47
+ display: flex;
48
+ flex-direction: column;
49
+ align-items: center;
50
+ justify-content: center;
51
+ }
52
+
53
+ .search-area {
54
+ width: 83%;
55
+ }
56
+
57
+ .search-bar {
58
+ position: relative;
59
+ width: 100%;
60
+ border-radius: 0.35rem;
61
+ background-color: #21212f;
62
+ }
63
+
64
+ .search-input-wrapper {
65
+ padding: 0rem 0.6rem 4.15rem 0.6rem;
66
+ }
67
+
68
+ .search-input {
69
+ width: 100%;
70
+ max-height: 200px;
71
+ overflow-y: auto;
72
+ font-size: 1.2rem;
73
+ border: none;
74
+ background-color: transparent;
75
+ color: var(--text-color);
76
+ line-height: 1.4;
77
+ padding: 0.65rem 0.65rem;
78
+ resize: none;
79
+ white-space: pre-wrap;
80
+ }
81
+
82
+ .search-input:focus {
83
+ outline: none;
84
+ }
85
+
86
+ .search-input::placeholder {
87
+ color: #888;
88
+ }
89
+
90
+ .icon-container {
91
+ position: absolute;
92
+ bottom: 0.25rem;
93
+ left: 0;
94
+ right: 0;
95
+ height: 3rem;
96
+ display: flex;
97
+ justify-content: space-between;
98
+ align-items: center;
99
+ padding: 0 0.75rem;
100
+ }
101
+
102
+ .settings-btn,
103
+ .send-btn {
104
+ background: transparent;
105
+ border: none;
106
+ color: var(--text-color);
107
+ cursor: pointer;
108
+ }
109
+
110
+ .settings-btn svg,
111
+ .send-btn svg {
112
+ font-size: 1.45rem;
113
+ }
114
+
115
+ .settings-btn:hover,
116
+ .send-btn:hover {
117
+ color: #888;
118
+ }
119
+
120
+ /* Stop button */
121
+ button.chat-send-btn.stop-btn,
122
+ button.send-btn.stop-btn {
123
+ background-color: var(--text-color) !important;
124
+ border-radius: 50%;
125
+ width: 28.5px;
126
+ height: 28.5px;
127
+ border: none;
128
+ display: flex;
129
+ align-items: center;
130
+ justify-content: center;
131
+ cursor: pointer;
132
+ padding: 0;
133
+ margin: 0 0 10px;
134
+ }
135
+
136
+ button.chat-send-btn.stop-btn:hover,
137
+ button.send-btn.stop-btn:hover {
138
+ background-color: #888 !important;
139
+ }
140
+
141
+ /* Chat Mode Search Bar and Textarea Styling */
142
+ .floating-chat-search-bar {
143
+ position: fixed;
144
+ bottom: 1.5rem;
145
+ left: 50%;
146
+ transform: translateX(-50%);
147
+ width: 48%;
148
+ background-color: #21212f;
149
+ border-radius: 0.35rem;
150
+ }
151
+
152
+ .floating-chat-search-bar::after {
153
+ content: "";
154
+ position: absolute;
155
+ left: 0;
156
+ right: 0;
157
+ bottom: -1.5rem;
158
+ height: 1.5rem;
159
+ background-color: var(--dark-surface);
160
+ }
161
+
162
+ .chat-search-input-wrapper {
163
+ padding: 0.25rem 0.6rem;
164
+ }
165
+
166
+ .chat-search-input {
167
+ width: 100%;
168
+ min-width: 700px;
169
+ max-height: 200px;
170
+ overflow-y: auto;
171
+ font-size: 1.2rem;
172
+ border: none;
173
+ background-color: transparent;
174
+ color: var(--text-color);
175
+ line-height: 1.4;
176
+ padding: 0.65rem 3.25rem;
177
+ resize: none;
178
+ white-space: pre-wrap;
179
+ }
180
+
181
+ .chat-search-input:focus {
182
+ outline: none;
183
+ }
184
+
185
+ .chat-icon-container {
186
+ position: absolute;
187
+ left: 0;
188
+ right: 0;
189
+ bottom: 0;
190
+ height: 3rem;
191
+ display: flex;
192
+ justify-content: space-between;
193
+ align-items: center;
194
+ padding: 0 0.75rem;
195
+ pointer-events: none;
196
+ }
197
+
198
+ /* Re-enable pointer events on the actual buttons so they remain clickable */
199
+ .chat-icon-container button {
200
+ pointer-events: auto;
201
+ }
202
+
203
+ .chat-settings-btn,
204
+ .chat-send-btn {
205
+ background: transparent;
206
+ border: none;
207
+ color: var(--text-color);
208
+ font-size: 1.45rem;
209
+ margin-bottom: 0.25rem;
210
+ cursor: pointer;
211
+ }
212
+
213
+ .chat-settings-btn:hover,
214
+ .chat-send-btn:hover {
215
+ color: #888;
216
+ }
217
+
218
+ /* Floating sidebar container for chat mode */
219
+ .floating-sidebar {
220
+ position: fixed;
221
+ right: 0;
222
+ top: 0;
223
+ bottom: 0;
224
+ width: var(--right-sidebar-width, 300px);
225
+ z-index: 1000;
226
+ }
227
+
228
+ /* Chat container */
229
+ .chat-container {
230
+ flex-grow: 1;
231
+ margin-bottom: 9rem;
232
+ }
233
+
234
+ /* Responsive Adjustments */
235
+ @media (max-width: 768px) {
236
+ .main-content {
237
+ margin-left: 0;
238
+ margin-right: 0;
239
+ }
240
+ }
241
+
242
+ @media (max-width: 576px) {
243
+ .main-content {
244
+ margin: 0;
245
+ padding: 1rem;
246
+ }
247
+ }
frontend/src/Components/AiPage.js ADDED
@@ -0,0 +1,502 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 } from 'react-icons/fa';
6
+ import IntialSetting from './IntialSetting';
7
+ import ChatWindow from './AiComponents/ChatWindow';
8
+ import RightSidebar from './AiComponents/ChatComponents/RightSidebar';
9
+ import './AiPage.css';
10
+
11
+ function AiPage() {
12
+ // Sidebar and other states
13
+ const [isRightSidebarOpen, setRightSidebarOpen] = useState(
14
+ localStorage.getItem("rightSidebarState") === "true"
15
+ );
16
+ const [rightSidebarWidth, setRightSidebarWidth] = useState(300);
17
+ const [sidebarContent, setSidebarContent] = useState("default");
18
+
19
+ const [searchText, setSearchText] = useState("");
20
+ const textAreaRef = useRef(null);
21
+ const [showSettingsModal, setShowSettingsModal] = useState(false);
22
+
23
+ const [showChatWindow, setShowChatWindow] = useState(false);
24
+ const [chatBlocks, setChatBlocks] = useState([]);
25
+ const [selectedChatBlockId, setSelectedChatBlockId] = useState(null);
26
+
27
+ const [defaultChatHeight, setDefaultChatHeight] = useState(null);
28
+ const [chatBottomPadding, setChatBottomPadding] = useState("60px");
29
+
30
+ // States/refs for streaming
31
+ const [isProcessing, setIsProcessing] = useState(false);
32
+ const [activeBlockId, setActiveBlockId] = useState(null);
33
+ const activeEventSourceRef = useRef(null);
34
+
35
+ // Snackbar state
36
+ const [snackbar, setSnackbar] = useState({
37
+ open: false,
38
+ message: "",
39
+ severity: "success",
40
+ });
41
+
42
+ // Function to open the snackbar
43
+ const openSnackbar = (message, severity = "success") => {
44
+ setSnackbar({ open: true, message, severity });
45
+ };
46
+
47
+ // Function to close the snackbar
48
+ const closeSnackbar = (event, reason) => {
49
+ if (reason === 'clickaway') return;
50
+ setSnackbar(prev => ({ ...prev, open: false }));
51
+ };
52
+
53
+ useEffect(() => {
54
+ localStorage.setItem("rightSidebarState", isRightSidebarOpen);
55
+ }, [isRightSidebarOpen]);
56
+
57
+ useEffect(() => {
58
+ document.documentElement.style.setProperty('--right-sidebar-width', rightSidebarWidth + 'px');
59
+ }, [rightSidebarWidth]);
60
+
61
+ // Dynamically increase height of chat input field based on newlines entered
62
+ useEffect(() => {
63
+ if (textAreaRef.current) {
64
+ if (!defaultChatHeight) {
65
+ setDefaultChatHeight(textAreaRef.current.scrollHeight);
66
+ }
67
+ textAreaRef.current.style.height = "auto";
68
+ textAreaRef.current.style.overflowY = "hidden";
69
+
70
+ const newHeight = textAreaRef.current.scrollHeight;
71
+ let finalHeight = newHeight;
72
+ if (newHeight > 200) {
73
+ finalHeight = 200;
74
+ textAreaRef.current.style.overflowY = "auto";
75
+ }
76
+ textAreaRef.current.style.height = `${finalHeight}px`;
77
+
78
+ const minPaddingPx = 0;
79
+ const maxPaddingPx = 59;
80
+ let newPaddingPx = minPaddingPx;
81
+ if (defaultChatHeight && finalHeight > defaultChatHeight) {
82
+ newPaddingPx =
83
+ minPaddingPx +
84
+ ((finalHeight - defaultChatHeight) / (200 - defaultChatHeight)) *
85
+ (maxPaddingPx - minPaddingPx);
86
+ if (newPaddingPx > maxPaddingPx) newPaddingPx = maxPaddingPx;
87
+ }
88
+ setChatBottomPadding(`${newPaddingPx}px`);
89
+ }
90
+ }, [searchText, defaultChatHeight]);
91
+
92
+ const handleOpenRightSidebar = (content, chatBlockId = null) => {
93
+ flushSync(() => {
94
+ if (chatBlockId) {
95
+ setSelectedChatBlockId(chatBlockId);
96
+ }
97
+ setSidebarContent(content ? content : "default");
98
+ setRightSidebarOpen(true);
99
+ });
100
+ };
101
+
102
+ const handleEvaluationError = useCallback((blockId, errorMsg) => {
103
+ setChatBlocks(prev =>
104
+ prev.map(block =>
105
+ block.id === blockId
106
+ ? { ...block, isError: true, errorMessage: errorMsg }
107
+ : block
108
+ )
109
+ );
110
+ }, []);
111
+
112
+ // Initiate the SSE
113
+ const initiateSSE = (query, blockId) => {
114
+ const startTime = Date.now();
115
+ const sseUrl = `/message-sse?user_message=${encodeURIComponent(query)}`;
116
+ const eventSource = new EventSource(sseUrl);
117
+ activeEventSourceRef.current = eventSource;
118
+
119
+ eventSource.addEventListener("token", (e) => {
120
+ const { chunk, index } = JSON.parse(e.data);
121
+ console.log("[SSE token chunk]", JSON.stringify(chunk));
122
+ console.log("[SSE token index]", JSON.stringify(index));
123
+
124
+ setChatBlocks(prevBlocks => {
125
+ return prevBlocks.map(block => {
126
+ if (block.id === blockId) {
127
+ const newTokenArray = block.tokenChunks ? [...block.tokenChunks] : [];
128
+ newTokenArray[index] = chunk;
129
+
130
+ return {
131
+ ...block,
132
+ tokenChunks: newTokenArray
133
+ };
134
+ }
135
+ return block;
136
+ });
137
+ });
138
+ });
139
+
140
+ eventSource.addEventListener("final_message", (e) => {
141
+ const endTime = Date.now();
142
+ const thinkingTime = ((endTime - startTime) / 1000).toFixed(1);
143
+ // Only update thinkingTime so the streaming flag turns false and the cursor disappears
144
+ setChatBlocks(prev => prev.map(block =>
145
+ block.id === blockId
146
+ ? { ...block, thinkingTime }
147
+ : block
148
+ ));
149
+ });
150
+
151
+ // Listen for the "complete" event to know when to close the connection.
152
+ eventSource.addEventListener("complete", (e) => {
153
+ console.log("Complete event received:", e.data);
154
+ eventSource.close();
155
+ activeEventSourceRef.current = null;
156
+ setIsProcessing(false);
157
+ setActiveBlockId(null);
158
+ });
159
+
160
+ // Update actions for only this chat block.
161
+ eventSource.addEventListener("action", (e) => {
162
+ try {
163
+ const actionData = JSON.parse(e.data);
164
+ console.log("Action event received:", actionData);
165
+ setChatBlocks(prev => prev.map(block => {
166
+ if (block.id === blockId) {
167
+ let updatedBlock = { ...block, actions: [...(block.actions || []), actionData] };
168
+ if (actionData.name === "sources") {
169
+ updatedBlock.sources = actionData.payload;
170
+ }
171
+ if (actionData.name === "graph") {
172
+ updatedBlock.graph = actionData.payload;
173
+ }
174
+ return updatedBlock;
175
+ }
176
+ return block;
177
+ }));
178
+ } catch (err) {
179
+ console.error("Error parsing action event:", err);
180
+ }
181
+ });
182
+
183
+ // Update the error for this chat block.
184
+ eventSource.addEventListener("error", (e) => {
185
+ console.error("Error from SSE:", e.data);
186
+ setChatBlocks(prev => prev.map(block =>
187
+ block.id === blockId
188
+ ? {
189
+ ...block,
190
+ isError: true,
191
+ errorMessage: e.data,
192
+ aiAnswer: "",
193
+ tasks: []
194
+ }
195
+ : block
196
+ ));
197
+ eventSource.close();
198
+ activeEventSourceRef.current = null;
199
+ setIsProcessing(false);
200
+ setActiveBlockId(null);
201
+ });
202
+
203
+ eventSource.addEventListener("step", (e) => {
204
+ console.log("Step event received:", e.data);
205
+ setChatBlocks(prev => prev.map(block =>
206
+ block.id === blockId
207
+ ? { ...block, thoughtLabel: e.data }
208
+ : block
209
+ ));
210
+ });
211
+
212
+ eventSource.addEventListener("sources_read", (e) => {
213
+ console.log("Sources read event received:", e.data);
214
+ try {
215
+ const parsed = JSON.parse(e.data);
216
+ let count;
217
+ if (typeof parsed === 'number') {
218
+ count = parsed;
219
+ } else if (parsed && typeof parsed.count === 'number') {
220
+ count = parsed.count;
221
+ }
222
+ if (typeof count === 'number') {
223
+ setChatBlocks(prev => prev.map(block =>
224
+ block.id === blockId
225
+ ? { ...block, sourcesRead: count, sources: parsed.sources || [] }
226
+ : block
227
+ ));
228
+ }
229
+ } catch(err) {
230
+ if (e.data.trim() !== "") {
231
+ setChatBlocks(prev => prev.map(block =>
232
+ block.id === blockId
233
+ ? { ...block, sourcesRead: e.data }
234
+ : block
235
+ ));
236
+ }
237
+ }
238
+ });
239
+
240
+ eventSource.addEventListener("task", (e) => {
241
+ console.log("Task event received:", e.data);
242
+ try {
243
+ const taskData = JSON.parse(e.data);
244
+ setChatBlocks(prev => prev.map(block => {
245
+ if (block.id === blockId) {
246
+ const existingTaskIndex = (block.tasks || []).findIndex(t => t.task === taskData.task);
247
+ if (existingTaskIndex !== -1) {
248
+ const updatedTasks = [...block.tasks];
249
+ updatedTasks[existingTaskIndex] = { ...updatedTasks[existingTaskIndex], status: taskData.status };
250
+ return { ...block, tasks: updatedTasks };
251
+ } else {
252
+ return { ...block, tasks: [...(block.tasks || []), taskData] };
253
+ }
254
+ }
255
+ return block;
256
+ }));
257
+ } catch (error) {
258
+ console.error("Error parsing task event:", error);
259
+ }
260
+ });
261
+ };
262
+
263
+ // Create a new chat block and initiate the SSE
264
+ const handleSend = () => {
265
+ if (!searchText.trim()) return;
266
+ const blockId = new Date().getTime();
267
+ setActiveBlockId(blockId);
268
+ setIsProcessing(true);
269
+ setChatBlocks(prev => [
270
+ ...prev,
271
+ {
272
+ id: blockId,
273
+ userMessage: searchText,
274
+ tokenChunks: [],
275
+ aiAnswer: "",
276
+ thinkingTime: null,
277
+ thoughtLabel: "",
278
+ sourcesRead: "",
279
+ tasks: [],
280
+ sources: [],
281
+ actions: []
282
+ }
283
+ ]);
284
+ setShowChatWindow(true);
285
+ const query = searchText;
286
+ setSearchText("");
287
+ initiateSSE(query, blockId);
288
+ };
289
+
290
+ const handleKeyDown = (e) => {
291
+ if (e.key === "Enter" && !e.shiftKey) {
292
+ e.preventDefault();
293
+ if (!isProcessing) {
294
+ handleSend();
295
+ }
296
+ }
297
+ };
298
+
299
+ // Stop the user request and close the active SSE connection
300
+ const handleStop = async () => {
301
+ // Close the active SSE connection if it exists
302
+ if (activeEventSourceRef.current) {
303
+ activeEventSourceRef.current.close();
304
+ activeEventSourceRef.current = null;
305
+ }
306
+ // Send POST request to /stop and update the chat block with the returned message
307
+ try {
308
+ const response = await fetch('/stop', {
309
+ method: 'POST',
310
+ headers: { 'Content-Type': 'application/json' },
311
+ body: JSON.stringify({})
312
+ });
313
+ const data = await response.json();
314
+
315
+ if (activeBlockId) {
316
+ setChatBlocks(prev => prev.map(block =>
317
+ block.id === activeBlockId
318
+ ? { ...block, aiAnswer: data.message, thinkingTime: 0, tasks: [] }
319
+ : block
320
+ ));
321
+ }
322
+ } catch (error) {
323
+ console.error("Error stopping the request:", error);
324
+ if (activeBlockId) {
325
+ setChatBlocks(prev => prev.map(block =>
326
+ block.id === activeBlockId
327
+ ? { ...block, aiAnswer: "Error stopping task", thinkingTime: 0, tasks: [] }
328
+ : block
329
+ ));
330
+ }
331
+ }
332
+ setIsProcessing(false);
333
+ setActiveBlockId(null);
334
+ };
335
+
336
+ const handleSendButtonClick = () => {
337
+ if (searchText.trim()) handleSend();
338
+ };
339
+
340
+ // Get the chat block whose details should be shown in the sidebar.
341
+ const selectedBlock = chatBlocks.find(block => block.id === selectedChatBlockId);
342
+ const evaluateAction = selectedBlock && selectedBlock.actions
343
+ ? selectedBlock.actions.find(a => a.name === "evaluate")
344
+ : null;
345
+
346
+ // Memoized evaluation object
347
+ const evaluation = useMemo(() => {
348
+ if (!evaluateAction) return null;
349
+ return {
350
+ ...evaluateAction.payload,
351
+ blockId: selectedBlock?.id,
352
+ onError: handleEvaluationError,
353
+ };
354
+ }, [evaluateAction, selectedBlock?.id, handleEvaluationError]);
355
+
356
+ return (
357
+ <div
358
+ className="app-container"
359
+ style={{
360
+ paddingRight: isRightSidebarOpen
361
+ ? Math.max(0, rightSidebarWidth - 250) + 'px'
362
+ : 0,
363
+ }}
364
+ >
365
+ {showChatWindow && selectedBlock && (sidebarContent !== "default" || (selectedBlock.tasks && selectedBlock.tasks.length > 0) || (selectedBlock.sources && selectedBlock.sources.length > 0)) && (
366
+ <div className="floating-sidebar">
367
+ <RightSidebar
368
+ isOpen={isRightSidebarOpen}
369
+ rightSidebarWidth={rightSidebarWidth}
370
+ setRightSidebarWidth={setRightSidebarWidth}
371
+ toggleRightSidebar={() => setRightSidebarOpen(!isRightSidebarOpen)}
372
+ sidebarContent={sidebarContent}
373
+ tasks={selectedBlock.tasks || []}
374
+ tasksLoading={false}
375
+ sources={selectedBlock.sources || []}
376
+ sourcesLoading={false}
377
+ onSourceClick={(source) => {
378
+ if (!source || !source.link) return;
379
+ window.open(source.link, '_blank');
380
+ }}
381
+ evaluation={evaluation}
382
+ />
383
+ </div>
384
+ )}
385
+
386
+ <main className="main-content">
387
+ {showChatWindow ? (
388
+ <>
389
+ <div className="chat-container">
390
+ {chatBlocks.map((block) => (
391
+ <ChatWindow
392
+ key={block.id}
393
+ blockId={block.id}
394
+ userMessage={block.userMessage}
395
+ tokenChunks={block.tokenChunks}
396
+ aiAnswer={block.aiAnswer}
397
+ thinkingTime={block.thinkingTime}
398
+ thoughtLabel={block.thoughtLabel}
399
+ sourcesRead={block.sourcesRead}
400
+ actions={block.actions}
401
+ tasks={block.tasks}
402
+ openRightSidebar={handleOpenRightSidebar}
403
+ openLeftSidebar={() => { /* if needed */ }}
404
+ isError={block.isError}
405
+ errorMessage={block.errorMessage}
406
+ />
407
+ ))}
408
+ </div>
409
+ <div
410
+ className="floating-chat-search-bar"
411
+ style={{
412
+ transform: isRightSidebarOpen
413
+ ? `translateX(calc(-50% - ${Math.max(0, (rightSidebarWidth - 250) / 2)}px))`
414
+ : 'translateX(-50%)'
415
+ }}
416
+ >
417
+ <div className="chat-search-input-wrapper" style={{ paddingBottom: chatBottomPadding }}>
418
+ <textarea
419
+ rows="1"
420
+ className="chat-search-input"
421
+ placeholder="Message..."
422
+ value={searchText}
423
+ onChange={(e) => setSearchText(e.target.value)}
424
+ onKeyDown={handleKeyDown}
425
+ ref={textAreaRef}
426
+ />
427
+ </div>
428
+ <div className="chat-icon-container">
429
+ <button
430
+ className="chat-settings-btn"
431
+ onClick={() => setShowSettingsModal(true)}
432
+ >
433
+ <FaCog />
434
+ </button>
435
+ {/* Conditionally render Stop or Send button */}
436
+ <button
437
+ className={`chat-send-btn ${isProcessing ? 'stop-btn' : ''}`}
438
+ onClick={isProcessing ? handleStop : handleSendButtonClick}
439
+ >
440
+ {isProcessing ? <FaStop size={12} color="black" /> : <FaPaperPlane />}
441
+ </button>
442
+ </div>
443
+ </div>
444
+ </>
445
+ ) : (
446
+ <div className="search-area">
447
+ <h1>How can I help you today?</h1>
448
+ <div className="search-bar">
449
+ <div className="search-input-wrapper">
450
+ <textarea
451
+ rows="1"
452
+ className="search-input"
453
+ placeholder="Message..."
454
+ value={searchText}
455
+ onChange={(e) => setSearchText(e.target.value)}
456
+ onKeyDown={handleKeyDown}
457
+ ref={textAreaRef}
458
+ />
459
+ </div>
460
+ <div className="icon-container">
461
+ <button
462
+ className="settings-btn"
463
+ onClick={() => setShowSettingsModal(true)}
464
+ >
465
+ <FaCog />
466
+ </button>
467
+ <button
468
+ className={`send-btn ${isProcessing ? 'stop-btn' : ''}`}
469
+ onClick={isProcessing ? handleStop : handleSendButtonClick}
470
+ >
471
+ {isProcessing ? <FaStop /> : <FaPaperPlane />}
472
+ </button>
473
+ </div>
474
+ </div>
475
+ </div>
476
+ )}
477
+ </main>
478
+
479
+ {showSettingsModal && (
480
+ <IntialSetting
481
+ trigger={true}
482
+ setTrigger={() => setShowSettingsModal(false)}
483
+ fromAiPage={true}
484
+ openSnackbar={openSnackbar}
485
+ closeSnackbar={closeSnackbar}
486
+ />
487
+ )}
488
+ <Snackbar
489
+ open={snackbar.open}
490
+ autoHideDuration={snackbar.severity === 'success' ? 3000 : null}
491
+ onClose={closeSnackbar}
492
+ anchorOrigin={{ vertical: 'top', horizontal: 'center' }}
493
+ >
494
+ <Alert onClose={closeSnackbar} severity={snackbar.severity} variant="filled" sx={{ width: '100%' }}>
495
+ {snackbar.message}
496
+ </Alert>
497
+ </Snackbar>
498
+ </div>
499
+ );
500
+ }
501
+
502
+ 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,309 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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-4 Turbo": "gpt-4-turbo",
26
+ "GPT-4o": "gpt-4o",
27
+ "GPT-4o Latest": "gpt-4o-2024-11-20",
28
+ "GPT-4o Mini": "gpt-4o-mini",
29
+ "ChatGPT": "chatgpt-4o-latest",
30
+ },
31
+ Anthropic: {
32
+ "Claude 3.5 Sonnet": "claude-3-5-sonnet-20241022",
33
+ "Claude 3.5 Haiku": "claude-3-5-haiku-20241022",
34
+ "Claude 3 Opus": "claude-3-opus-20240229",
35
+ "Claude 3 Sonnet": "claude-3-sonnet-20240229",
36
+ "Claude 3 Haiku": "claude-3-haiku-20240307",
37
+ },
38
+ Google: {
39
+ "Gemini 1.5 Pro": "gemini-1.5-pro",
40
+ "Gemini 1.5 Flash": "gemini-1.5-flash",
41
+ "Gemini 2.0 Flash Lite": "gemini-2.0-flash-lite-preview-02-05",
42
+ "Gemini 2.0 Flash Experimental": "gemini-2.0-flash-exp",
43
+ "Gemini 2.0 Flash": "gemini-2.0-flash",
44
+ "Gemini 2.0 Pro Experimental": "gemini-2.0-pro-exp-02-05",
45
+ },
46
+ XAI: {
47
+ "Grok-2": "grok-2-latest",
48
+ "Grok Beta": "grok-beta",
49
+ },
50
+ };
51
+
52
+ // Reset form and state variables
53
+ const handleReset = (e) => {
54
+ e.preventDefault();
55
+ if (formRef.current) {
56
+ formRef.current.reset();
57
+ }
58
+ setSelectedProvider("OpenAI");
59
+ setModelTemperature(0.0);
60
+ setModelTopP(1.0);
61
+ };
62
+
63
+ // Handle form submission and save settings
64
+ const handleSave = async (e) => {
65
+ e.preventDefault();
66
+ const form = formRef.current;
67
+
68
+ // Retrieve form values
69
+ const modelProvider = form.elements["model-provider"].value;
70
+ const modelName = form.elements["model-name"].value;
71
+ const modelAPIKeys = form.elements["model-api"].value;
72
+ const braveAPIKey = form.elements["brave-api"].value;
73
+ const proxyList = form.elements["proxy-list"].value;
74
+
75
+ // Check for missing required fields
76
+ const missingFields = [];
77
+ if (!modelProvider || modelProvider.trim() === "") missingFields.push("Model Provider");
78
+ if (!modelName || modelName.trim() === "") missingFields.push("Model Name");
79
+ if (!modelAPIKeys || modelAPIKeys.trim() === "") missingFields.push("Model API Key");
80
+ if (!braveAPIKey || braveAPIKey.trim() === "") missingFields.push("Brave Search API Key");
81
+
82
+ if (missingFields.length > 0) {
83
+ props.openSnackbar(
84
+ "Please fill in the following required fields: " + missingFields.join(", "),
85
+ "error"
86
+ );
87
+ return;
88
+ }
89
+
90
+ // Build payload for backend
91
+ const payload = {
92
+ "Model_Provider": modelProvider.toLowerCase(),
93
+ "Model_Name": modelName,
94
+ "Model_API_Keys": modelAPIKeys,
95
+ "Brave_Search_API_Key": braveAPIKey,
96
+ "Model_Temperature": modelTemperature,
97
+ "Model_Top_P": modelTopP,
98
+ };
99
+
100
+ if (proxyList && proxyList.trim() !== "") {
101
+ payload["Proxy_List"] = proxyList;
102
+ }
103
+
104
+ // Show appropriate notification based on context
105
+ if (props.fromAiPage) {
106
+ props.openSnackbar(
107
+ <Box mt={1} display="flex" alignItems="center">
108
+ <Box className="re-applying-settings-custom-spinner" />
109
+ <Box ml={1} className="re-applying-settings-text">
110
+ <span>Re-applying settings. This may take a few minutes...</span>
111
+ </Box>
112
+ </Box>,
113
+ "info"
114
+ );
115
+ } else {
116
+ props.openSnackbar("Settings saved successfully!", "success");
117
+ if (props.onInitializationStart) {
118
+ props.onInitializationStart();
119
+ }
120
+ }
121
+
122
+ // Send settings to backend
123
+ try {
124
+ const response = await fetch("/settings", {
125
+ method: "POST",
126
+ headers: {
127
+ "Content-Type": "application/json",
128
+ },
129
+ body: JSON.stringify(payload),
130
+ });
131
+
132
+ if (response.ok) {
133
+ const data = await response.json();
134
+ if (data.success === true) {
135
+ if (props.fromAiPage) {
136
+ props.openSnackbar("Settings saved successfully!", "success");
137
+ }
138
+ navigate("/AiPage");
139
+ } else {
140
+ props.openSnackbar("Error saving settings. Please try again.", "error");
141
+ }
142
+ } else {
143
+ props.openSnackbar("Error saving settings. Please try again.", "error");
144
+ }
145
+ } catch (error) {
146
+ console.error("Error saving settings:", error);
147
+ props.openSnackbar("Error saving settings. Please try again.", "error");
148
+ }
149
+ };
150
+
151
+ // Render the settings modal
152
+ return props.trigger ? (
153
+ <div className="showSetting" onClick={() => props.setTrigger(false)}>
154
+ <div className="showSetting-inner" onClick={(e) => e.stopPropagation()}>
155
+ <label className="setting-size">Settings</label>
156
+ <button className="close-btn" onClick={() => props.setTrigger(false)}>
157
+ <FaTimes />
158
+ </button>
159
+ <form ref={formRef}>
160
+ <div className="form-group">
161
+ <label htmlFor="model-provider">Model Provider</label>
162
+ <select
163
+ id="model-provider"
164
+ name="model-provider"
165
+ value={selectedProvider}
166
+ onChange={(e) => setSelectedProvider(e.target.value)}
167
+ >
168
+ {Object.keys(modelOptions).map(provider => (
169
+ <option key={provider} value={provider}>
170
+ {provider}
171
+ </option>
172
+ ))}
173
+ </select>
174
+ </div>
175
+ <div className="form-group">
176
+ <label htmlFor="model-name">Model Name</label>
177
+ <select id="model-name" name="model-name">
178
+ {Object.entries(modelOptions[selectedProvider]).map(
179
+ ([displayName, backendName]) => (
180
+ <option key={backendName} value={backendName}>
181
+ {displayName}
182
+ </option>
183
+ )
184
+ )}
185
+ </select>
186
+ </div>
187
+ <div className="form-group">
188
+ <label htmlFor="model-api">Model API Key</label>
189
+ <textarea
190
+ id="model-api"
191
+ name="model-api"
192
+ placeholder="Enter API Key, one per line"
193
+ ></textarea>
194
+ </div>
195
+ <div className="form-group">
196
+ <label htmlFor="brave-api">Brave Search API Key</label>
197
+ <input
198
+ type="text"
199
+ id="brave-api"
200
+ name="brave-api"
201
+ placeholder="Enter API Key"
202
+ />
203
+ </div>
204
+ <div className="form-group">
205
+ <label htmlFor="proxy-list">Proxy List</label>
206
+ <textarea
207
+ id="proxy-list"
208
+ name="proxy-list"
209
+ placeholder="Enter proxies, one per line"
210
+ ></textarea>
211
+ </div>
212
+ {/* Commented Neo4j configuration fields */}
213
+ {/* <div className="form-group">
214
+ <label htmlFor="neo4j-url">Neo4j URL</label>
215
+ <input
216
+ type="text"
217
+ id="neo4j-url"
218
+ name="neo4j-url"
219
+ placeholder="Enter Neo4j URL"
220
+ />
221
+ </div>
222
+ <div className="form-group">
223
+ <label htmlFor="neo4j-username">Neo4j Username</label>
224
+ <input
225
+ type="text"
226
+ id="neo4j-username"
227
+ name="neo4j-username"
228
+ placeholder="Enter Username"
229
+ />
230
+ </div>
231
+ <div className="form-group">
232
+ <label htmlFor="neo4j-password">Neo4j Password</label>
233
+ <div className="password-wrapper">
234
+ <input
235
+ type={showPassword ? "text" : "password"}
236
+ id="neo4j-password"
237
+ name="neo4j-password"
238
+ placeholder="Enter Password"
239
+ />
240
+ <IconButton
241
+ onClick={() => setShowPassword(prev => !prev)}
242
+ className="password-toggle"
243
+ sx={{
244
+ color: "white",
245
+ p: 0,
246
+ m: 0
247
+ }}
248
+ >
249
+ {showPassword ? <FaEyeSlash /> : <FaEye />}
250
+ </IconButton>
251
+ </div>
252
+ </div> */}
253
+ <div className="form-group">
254
+ <div className="sliders-container">
255
+ <div className="slider-item">
256
+ <label htmlFor="temperature">Temperature</label>
257
+ <Slider
258
+ id="temperature"
259
+ value={modelTemperature}
260
+ onChange={(e, newValue) => setModelTemperature(newValue)}
261
+ step={0.05}
262
+ min={0.0}
263
+ max={1.0}
264
+ valueLabelDisplay="auto"
265
+ sx={{ width: '100%', color: 'success.main' }}
266
+ />
267
+ </div>
268
+ <div className="slider-item">
269
+ <label htmlFor="top-p">Top-P</label>
270
+ <Slider
271
+ id="top-p"
272
+ value={modelTopP}
273
+ onChange={(e, newValue) => setModelTopP(newValue)}
274
+ step={0.05}
275
+ min={0.0}
276
+ max={1.0}
277
+ valueLabelDisplay="auto"
278
+ sx={{ width: '100%', color: 'success.main' }}
279
+ />
280
+ </div>
281
+ </div>
282
+ </div>
283
+ <Stack direction="row" spacing={2} sx={{ justifyContent: 'flex-end' }}>
284
+ <Button
285
+ type="button"
286
+ className="reset-btn"
287
+ sx={{ color: "#2196f3" }}
288
+ onClick={handleReset}
289
+ >
290
+ Reset
291
+ </Button>
292
+ <Button
293
+ type="button"
294
+ variant="contained"
295
+ color="success"
296
+ className="save-btn"
297
+ onClick={handleSave}
298
+ >
299
+ Save
300
+ </Button>
301
+ </Stack>
302
+ </form>
303
+ {props.children}
304
+ </div>
305
+ </div>
306
+ ) : null;
307
+ }
308
+
309
+ 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/graph.png ADDED
frontend/src/Icons/loading.png ADDED
frontend/src/Icons/reprocess.png ADDED
frontend/src/Icons/settings-2.svg ADDED
frontend/src/Icons/settings.png ADDED
frontend/src/Icons/sources.png ADDED
frontend/src/Icons/thinking.gif ADDED
frontend/src/Icons/user.png ADDED
frontend/src/index.css ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* Reset some default styles */
2
+ *, *::before, *::after {
3
+ box-sizing: border-box;
4
+ }
5
+
6
+ body {
7
+ margin: 0;
8
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
9
+ 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
10
+ sans-serif;
11
+ -webkit-font-smoothing: antialiased;
12
+ -moz-osx-font-smoothing: grayscale;
13
+ }
14
+
15
+ code {
16
+ font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
17
+ monospace;
18
+ }
frontend/src/index.js ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react';
2
+ import ReactDOM from 'react-dom/client';
3
+ import './index.css';
4
+ import App from './App';
5
+ import reportWebVitals from './reportWebVitals';
6
+
7
+ const root = ReactDOM.createRoot(document.getElementById('root'));
8
+ root.render(
9
+ <React.StrictMode>
10
+ <App />
11
+ </React.StrictMode>
12
+ );
13
+
14
+ // If you want to start measuring performance in your app, pass a function
15
+ // to log results (for example: reportWebVitals(console.log))
16
+ // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
17
+ reportWebVitals();
frontend/src/logo.svg ADDED
frontend/src/reportWebVitals.js ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const reportWebVitals = onPerfEntry => {
2
+ if (onPerfEntry && onPerfEntry instanceof Function) {
3
+ import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
4
+ getCLS(onPerfEntry);
5
+ getFID(onPerfEntry);
6
+ getFCP(onPerfEntry);
7
+ getLCP(onPerfEntry);
8
+ getTTFB(onPerfEntry);
9
+ });
10
+ }
11
+ };
12
+
13
+ export default reportWebVitals;
frontend/src/setupTests.js ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ // jest-dom adds custom jest matchers for asserting on DOM nodes.
2
+ // allows you to do things like:
3
+ // expect(element).toHaveTextContent(/react/i)
4
+ // learn more: https://github.com/testing-library/jest-dom
5
+ import '@testing-library/jest-dom';