Hemang Thakur commited on
Commit
4279593
·
1 Parent(s): e647f62

Deploy project on Hugging Face Spaces

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 +59 -0
  3. frontend/package-lock.json +0 -0
  4. frontend/package.json +47 -0
  5. frontend/public/favicon.ico +0 -0
  6. frontend/public/index.html +43 -0
  7. frontend/public/logo192.png +0 -0
  8. frontend/public/logo512.png +0 -0
  9. frontend/public/manifest.json +25 -0
  10. frontend/public/robots.txt +3 -0
  11. frontend/src/App.css +40 -0
  12. frontend/src/App.js +64 -0
  13. frontend/src/App.test.js +8 -0
  14. frontend/src/Components/AiComponents/ChatWindow.css +278 -0
  15. frontend/src/Components/AiComponents/ChatWindow.js +259 -0
  16. frontend/src/Components/AiComponents/Evaluate.css +107 -0
  17. frontend/src/Components/AiComponents/Evaluate.js +213 -0
  18. frontend/src/Components/AiComponents/FormSection.js +9 -0
  19. frontend/src/Components/AiComponents/Graph.css +85 -0
  20. frontend/src/Components/AiComponents/Graph.js +61 -0
  21. frontend/src/Components/AiComponents/LeftSideBar.js +38 -0
  22. frontend/src/Components/AiComponents/LeftSidebar.css +59 -0
  23. frontend/src/Components/AiComponents/RightSidebar.css +129 -0
  24. frontend/src/Components/AiComponents/RightSidebar.js +138 -0
  25. frontend/src/Components/AiComponents/Sources.css +65 -0
  26. frontend/src/Components/AiComponents/Sources.js +124 -0
  27. frontend/src/Components/AiPage.css +249 -0
  28. frontend/src/Components/AiPage.js +439 -0
  29. frontend/src/Components/IntialSetting.css +174 -0
  30. frontend/src/Components/IntialSetting.js +397 -0
  31. frontend/src/Components/settings-gear-1.svg +47 -0
  32. frontend/src/Icons/bot.png +0 -0
  33. frontend/src/Icons/copy.png +0 -0
  34. frontend/src/Icons/evaluate.png +0 -0
  35. frontend/src/Icons/graph.png +0 -0
  36. frontend/src/Icons/loading.png +0 -0
  37. frontend/src/Icons/reprocess.png +0 -0
  38. frontend/src/Icons/settings-2.svg +56 -0
  39. frontend/src/Icons/settings.png +0 -0
  40. frontend/src/Icons/sources.png +0 -0
  41. frontend/src/Icons/thinking.gif +0 -0
  42. frontend/src/Icons/user.png +0 -0
  43. frontend/src/index.css +18 -0
  44. frontend/src/index.js +17 -0
  45. frontend/src/logo.svg +1 -0
  46. frontend/src/reportWebVitals.js +13 -0
  47. frontend/src/setupTests.js +5 -0
  48. main.py +761 -0
  49. package-lock.json +6 -0
  50. requirements.txt +34 -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,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ # Copy the 'frontend' folder from the project root into the container
9
+ COPY frontend ./frontend
10
+
11
+ # Switch to the frontend directory
12
+ WORKDIR /app/frontend
13
+
14
+ # Install dependencies (using yarn, npm, or pnpm)
15
+ RUN if [ -f yarn.lock ]; then yarn --frozen-lockfile; \
16
+ elif [ -f package-lock.json ]; then npm ci; \
17
+ elif [ -f pnpm-lock.yaml ]; then yarn global add pnpm && pnpm i --frozen-lockfile; \
18
+ else echo "No lockfile found. Exiting." && exit 1; \
19
+ fi
20
+
21
+ # Build the React app (produces a production-ready build in the "build" folder)
22
+ RUN npm run build
23
+
24
+ # ----------------------------
25
+ # Stage 2: Set Up the FastAPI Backend and Serve the React App
26
+ # ----------------------------
27
+ FROM python:3.12-slim AS backend
28
+ WORKDIR /app
29
+
30
+ # Install OS-level dependencies
31
+ RUN apt-get update --fix-missing && \
32
+ apt-get install --no-install-recommends -y git curl && \
33
+ apt-get clean && rm -rf /var/lib/apt/lists/*
34
+
35
+ # Install Node.js (if needed for any backend tasks)
36
+ RUN curl -fsSL https://deb.nodesource.com/setup_20.x | bash - && \
37
+ apt-get update --fix-missing && \
38
+ apt-get install --no-install-recommends -y nodejs && \
39
+ apt-get clean && rm -rf /var/lib/apt/lists/*
40
+
41
+ # Copy requirements.txt and install Python dependencies
42
+ COPY requirements.txt .
43
+ RUN pip install --no-cache-dir -r requirements.txt
44
+
45
+ # Install additional dependencies for torch and spaCy
46
+ RUN pip install --no-cache-dir torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu126
47
+ RUN python -m spacy download en_core_web_sm
48
+
49
+ # Copy the rest of your backend code and resources
50
+ COPY . .
51
+
52
+ # Copy the built React app from the builder stage into a folder named "static"
53
+ COPY --from=builder /app/frontend/build ./static
54
+
55
+ # Expose the port
56
+ EXPOSE ${PORT:-7860}
57
+
58
+ # Start the FastAPI backend using Uvicorn, reading the PORT env variable
59
+ CMD ["sh", "-c", "uvicorn main:app --host 0.0.0.0 --port ${PORT:-7860}"]
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,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ "react": "^19.0.0",
15
+ "react-dom": "^19.0.0",
16
+ "react-icons": "^5.4.0",
17
+ "react-markdown": "^9.0.3",
18
+ "react-router-dom": "^7.1.3",
19
+ "react-scripts": "5.0.1",
20
+ "styled-components": "^6.1.14",
21
+ "web-vitals": "^4.2.4"
22
+ },
23
+ "scripts": {
24
+ "start": "react-scripts start",
25
+ "build": "react-scripts build",
26
+ "test": "react-scripts test",
27
+ "eject": "react-scripts eject"
28
+ },
29
+ "eslintConfig": {
30
+ "extends": [
31
+ "react-app",
32
+ "react-app/jest"
33
+ ]
34
+ },
35
+ "browserslist": {
36
+ "production": [
37
+ ">0.2%",
38
+ "not dead",
39
+ "not op_mini all"
40
+ ],
41
+ "development": [
42
+ "last 1 chrome version",
43
+ "last 1 firefox version",
44
+ "last 1 safari version"
45
+ ]
46
+ }
47
+ }
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,64 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState, useEffect } from 'react';
2
+ import { BrowserRouter, Routes, Route } from 'react-router-dom';
3
+ import CircularProgress from '@mui/material/CircularProgress';
4
+ import logo from './Icons/settings-2.svg';
5
+ import './App.css';
6
+ import IntialSetting from './Components/IntialSetting.js';
7
+ import AiPage from './Components/AiPage.js';
8
+
9
+ function App() {
10
+ return (
11
+ <BrowserRouter>
12
+ <Routes>
13
+ <Route path='/' element={<Home />} />
14
+ <Route path='/AiPage' element={<AiPage />} />
15
+ </Routes>
16
+ </BrowserRouter>
17
+ );
18
+ }
19
+
20
+ function Home() {
21
+ const [showSettings, setShowSettings] = useState(false);
22
+ const [initializing, setInitializing] = useState(false);
23
+
24
+ // This callback is passed to IntialSetting and called when the user clicks Save.
25
+ // It changes the underlying header content to the initializing state.
26
+ const handleInitializationStart = () => {
27
+ setInitializing(true);
28
+ };
29
+
30
+ return (
31
+ <div className="App">
32
+ <header className="App-header">
33
+ {initializing ? (
34
+ <>
35
+ <CircularProgress style={{ margin: '20px' }} />
36
+ <p>Initializing the app. This may take a few minutes...</p>
37
+ </>
38
+ ) : (
39
+ <>
40
+ <img
41
+ src={logo}
42
+ className="App-logo"
43
+ alt="logo"
44
+ onClick={() => setShowSettings(true)}
45
+ style={{ cursor: 'pointer' }}
46
+ />
47
+ <p>Enter the settings to proceed</p>
48
+ </>
49
+ )}
50
+
51
+ {/* Always render the settings modal if showSettings is true */}
52
+ {showSettings && (
53
+ <IntialSetting
54
+ trigger={showSettings}
55
+ setTrigger={setShowSettings}
56
+ onInitializationStart={handleInitializationStart}
57
+ />
58
+ )}
59
+ </header>
60
+ </div>
61
+ );
62
+ }
63
+
64
+ 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/ChatWindow.css ADDED
@@ -0,0 +1,278 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ }
126
+
127
+ .markdown {
128
+ margin: -1rem 0 -0.8rem 0;
129
+ line-height: 2rem;
130
+ }
131
+
132
+ /* Post-answer icons container: placed below the bot bubble */
133
+ .post-icons {
134
+ display: flex;
135
+ padding-left: 0.12rem;
136
+ gap: 1rem;
137
+ }
138
+
139
+ /* Make each post icon container position relative for tooltip positioning */
140
+ .post-icons .copy-icon,
141
+ .post-icons .evaluate-icon,
142
+ .post-icons .sources-icon,
143
+ .post-icons .graph-icon {
144
+ cursor: pointer;
145
+ position: relative;
146
+ }
147
+
148
+ /* Apply a brightness filter to the icon images on hover */
149
+ .post-icons .copy-icon img,
150
+ .post-icons .evaluate-icon img,
151
+ .post-icons .sources-icon img,
152
+ .post-icons .graph-icon img {
153
+ transition: filter var(--transition-speed);
154
+ }
155
+
156
+ .post-icons .copy-icon:hover img,
157
+ .post-icons .evaluate-icon:hover img,
158
+ .post-icons .sources-icon:hover img,
159
+ .post-icons .graph-icon:hover img {
160
+ filter: brightness(0.65);
161
+ }
162
+
163
+ /* .post-icons .copy-icon:active img,
164
+ .post-icons .evaluate-icon:active img,
165
+ .post-icons .sources-icon:active img,
166
+ .post-icons .graph-icon:active img {
167
+ filter: brightness(0.35);
168
+ } */
169
+
170
+ /* Tooltip styling */
171
+ .tooltip {
172
+ position: absolute;
173
+ bottom: 100%;
174
+ left: 50%;
175
+ transform: translateX(-50%) translateY(10px) scale(0.9);
176
+ transform-origin: bottom center;
177
+ margin-bottom: 0.65rem;
178
+ padding: 0.3rem 0.6rem;
179
+ background-color: var(--primary-color);
180
+ color: var(--text-color);
181
+ border-radius: 0.25rem;
182
+ white-space: nowrap;
183
+ font-size: 0.85rem;
184
+ opacity: 0;
185
+ visibility: hidden;
186
+ transition: transform 0.3s ease, opacity 0.3s ease;
187
+ }
188
+
189
+ /* Show the tooltip on hover */
190
+ .post-icons .copy-icon:hover .tooltip,
191
+ .post-icons .evaluate-icon:hover .tooltip,
192
+ .post-icons .sources-icon:hover .tooltip,
193
+ .post-icons .graph-icon:hover .tooltip {
194
+ opacity: 1;
195
+ visibility: visible;
196
+ transform: translateX(-50%) translateY(0) scale(1);
197
+ }
198
+
199
+ /* Styling for the question text */
200
+ .question {
201
+ margin: 0;
202
+ white-space: pre-wrap;
203
+ line-height: 1.4;
204
+ }
205
+
206
+ /* Reduce the size of user and bot icons */
207
+ .user-icon img,
208
+ .bot-icon img {
209
+ width: 35px;
210
+ height: 35px;
211
+ object-fit: contain;
212
+ }
213
+
214
+ /* Reduce the size of the post-action icons */
215
+ .post-icons img {
216
+ width: 20px;
217
+ height: 20px;
218
+ object-fit: contain;
219
+ }
220
+
221
+ /* ChatWindow.css */
222
+
223
+ /* Container for the loading state with a dark background */
224
+ .bot-loading {
225
+ display: flex;
226
+ flex-direction: row;
227
+ align-items: center;
228
+ justify-content: center;
229
+ gap: 10px; /* adds space between the spinner and the text */
230
+ padding: 30px;
231
+ background-color: var(--dark-surface); /* Dark background */
232
+ }
233
+
234
+ .loading-text {
235
+ margin: 0; /* removes any default margins */
236
+ font-size: 1rem;
237
+ color: #ccc;
238
+ }
239
+
240
+ /* Finished state: styling for the thought time info */
241
+ .thinking-info {
242
+ margin-bottom: 4px;
243
+ }
244
+
245
+ .thinking-time {
246
+ font-size: 1rem;
247
+ color: #888;
248
+ cursor: pointer;
249
+ }
250
+
251
+ /* Snackbar styling */
252
+ .custom-snackbar {
253
+ background-color: #1488e7 !important;
254
+ color: var(--text-color) !important;
255
+ padding: 0.35rem 1rem !important;
256
+ border-radius: 4px !important;
257
+ }
258
+
259
+ /* Spinner styling */
260
+ .custom-spinner {
261
+ width: 1.35rem !important;
262
+ height: 1.35rem !important;
263
+ border: 3px solid #3b7bdc !important; /* Main Spinner */
264
+ border-top: 3px solid #434343 !important; /* Rotating path */
265
+ border-radius: 50% !important;
266
+ margin-top: 0.1rem !important;
267
+ animation: spin 0.9s linear infinite !important;
268
+ }
269
+
270
+ /* Spinner animation */
271
+ @keyframes spin {
272
+ 0% {
273
+ transform: rotate(0deg);
274
+ }
275
+ 100% {
276
+ transform: rotate(360deg);
277
+ }
278
+ }
frontend/src/Components/AiComponents/ChatWindow.js ADDED
@@ -0,0 +1,259 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useRef, useEffect, useState } 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 ReactMarkdown from 'react-markdown';
8
+ import GraphDialog from './Graph';
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
+ aiAnswer,
27
+ thinkingTime,
28
+ thoughtLabel,
29
+ sourcesRead,
30
+ actions,
31
+ tasks,
32
+ openRightSidebar,
33
+ // openLeftSidebar,
34
+ isError,
35
+ errorMessage
36
+ }) {
37
+ const answerRef = useRef(null);
38
+ const [graphDialogOpen, setGraphDialogOpen] = useState(false);
39
+ const [snackbarOpen, setSnackbarOpen] = useState(false);
40
+
41
+ // Get the graph action from the actions prop.
42
+ const graphAction = actions && actions.find(a => a.name === "graph");
43
+
44
+ // Handler for copying answer to clipboard.
45
+ const handleCopy = () => {
46
+ if (answerRef.current) {
47
+ const textToCopy = answerRef.current.innerText || answerRef.current.textContent;
48
+ navigator.clipboard.writeText(textToCopy)
49
+ .then(() => {
50
+ console.log('Copied to clipboard:', textToCopy);
51
+ setSnackbarOpen(true);
52
+ })
53
+ .catch((err) => console.error('Failed to copy text:', err));
54
+ }
55
+ };
56
+
57
+ // Snackbar close handler
58
+ const handleSnackbarClose = (event, reason) => {
59
+ if (reason === 'clickaway') return;
60
+ setSnackbarOpen(false);
61
+ };
62
+
63
+ // Determine if any tokens (partial or full answer) have been received.
64
+ const hasTokens = aiAnswer && aiAnswer.length > 0;
65
+ // Assume streaming is in progress if thinkingTime is not set.
66
+ const isStreaming = thinkingTime === null || thinkingTime === undefined;
67
+ // Append a trailing cursor if streaming.
68
+ const displayAnswer = hasTokens ? (isStreaming ? aiAnswer + "▌" : aiAnswer) : "";
69
+
70
+ // Helper to render the thought label.
71
+ const renderThoughtLabel = () => {
72
+ if (!hasTokens) {
73
+ return thoughtLabel;
74
+ } else {
75
+ if (thoughtLabel && thoughtLabel.startsWith("Thought and searched for")) {
76
+ return thoughtLabel;
77
+ }
78
+ return null;
79
+ }
80
+ };
81
+
82
+ // Helper to render sources read.
83
+ const renderSourcesRead = () => {
84
+ if (!sourcesRead && sourcesRead !== 0) return null;
85
+ return sourcesRead;
86
+ };
87
+
88
+ // When tasks first appear, automatically open the sidebar.
89
+ const prevTasksRef = useRef(tasks);
90
+ useEffect(() => {
91
+ if (prevTasksRef.current.length === 0 && tasks && tasks.length > 0) {
92
+ openRightSidebar("tasks", blockId);
93
+ }
94
+ prevTasksRef.current = tasks;
95
+ }, [tasks, blockId, openRightSidebar]);
96
+
97
+ return (
98
+ <>
99
+ { !hasTokens ? (
100
+ // If no tokens, render pre-stream UI.
101
+ (!isError && thoughtLabel) ? (
102
+ <div className="answer-container">
103
+ {/* User Message */}
104
+ <div className="message-row user-message">
105
+ <div className="message-bubble user-bubble">
106
+ <p className="question">{userMessage}</p>
107
+ </div>
108
+ <div className="user-icon">
109
+ <img src={user} alt="user icon" />
110
+ </div>
111
+ </div>
112
+ {/* Bot Message (pre-stream with spinner) */}
113
+ <div className="message-row bot-message pre-stream">
114
+ <div className="bot-container">
115
+ <div className="thinking-info">
116
+ <Box mt={1} display="flex" alignItems="center">
117
+ <Box className="custom-spinner" />
118
+ <Box ml={1}>
119
+ <span
120
+ className="thinking-time"
121
+ onClick={() => openRightSidebar("tasks", blockId)}
122
+ >
123
+ {thoughtLabel}
124
+ </span>
125
+ </Box>
126
+ </Box>
127
+ </div>
128
+ </div>
129
+ </div>
130
+ </div>
131
+ ) : (
132
+ // Render without spinner (user message only)
133
+ <div className="answer-container">
134
+ <div className="message-row user-message">
135
+ <div className="message-bubble user-bubble">
136
+ <p className="question">{userMessage}</p>
137
+ </div>
138
+ <div className="user-icon">
139
+ <img src={user} alt="user icon" />
140
+ </div>
141
+ </div>
142
+ </div>
143
+ )
144
+ ) : (
145
+ // Render Full Chat Message
146
+ <div className="answer-container">
147
+ {/* User Message */}
148
+ <div className="message-row user-message">
149
+ <div className="message-bubble user-bubble">
150
+ <p className="question">{userMessage}</p>
151
+ </div>
152
+ <div className="user-icon">
153
+ <img src={user} alt="user icon" />
154
+ </div>
155
+ </div>
156
+ {/* Bot Message */}
157
+ <div className="message-row bot-message">
158
+ <div className="bot-container">
159
+ {!isError && renderThoughtLabel() && (
160
+ <div className="thinking-info">
161
+ <span
162
+ className="thinking-time"
163
+ onClick={() => openRightSidebar("tasks", blockId)}
164
+ >
165
+ {renderThoughtLabel()}
166
+ </span>
167
+ </div>
168
+ )}
169
+ {renderSourcesRead() !== null && (
170
+ <div className="sources-read-container">
171
+ <p
172
+ className="sources-read"
173
+ onClick={() => openRightSidebar("sources", blockId)}
174
+ >
175
+ Sources Read: {renderSourcesRead()}
176
+ </p>
177
+ </div>
178
+ )}
179
+ <div className="answer-block">
180
+ <div className="bot-icon">
181
+ <img src={bot} alt="bot icon" />
182
+ </div>
183
+ <div className="message-bubble bot-bubble">
184
+ <div className="answer" ref={answerRef}>
185
+ <ReactMarkdown className="markdown">
186
+ {displayAnswer}
187
+ </ReactMarkdown>
188
+ </div>
189
+ </div>
190
+ <div className="post-icons">
191
+ {!isStreaming && (
192
+ <div className="copy-icon" onClick={handleCopy}>
193
+ <img src={copy} alt="copy icon" />
194
+ <span className="tooltip">Copy</span>
195
+ </div>
196
+ )}
197
+ {actions && actions.some(a => a.name === "evaluate") && (
198
+ <div className="evaluate-icon" onClick={() => openRightSidebar("evaluate", blockId)}>
199
+ <img src={evaluate} alt="evaluate icon" />
200
+ <span className="tooltip">Evaluate</span>
201
+ </div>
202
+ )}
203
+ {actions && actions.some(a => a.name === "sources") && (
204
+ <div className="sources-icon" onClick={() => openRightSidebar("sources", blockId)}>
205
+ <img src={sourcesIcon} alt="sources icon" />
206
+ <span className="tooltip">Sources</span>
207
+ </div>
208
+ )}
209
+ {actions && actions.some(a => a.name === "graph") && (
210
+ <div className="graph-icon" onClick={() => setGraphDialogOpen(true)}>
211
+ <img src={graphIcon} alt="graph icon" />
212
+ <span className="tooltip">View Graph</span>
213
+ </div>
214
+ )}
215
+ </div>
216
+ </div>
217
+ </div>
218
+ </div>
219
+ {/* Render the GraphDialog when graphDialogOpen is true */}
220
+ {graphDialogOpen && (
221
+ <GraphDialog
222
+ open={graphDialogOpen}
223
+ onClose={() => setGraphDialogOpen(false)}
224
+ payload={graphAction ? graphAction.payload : { query: userMessage }}
225
+ />
226
+ )}
227
+ </div>
228
+ )}
229
+ {/* Render error container if there's an error */}
230
+ {isError && (
231
+ <div className="error-block" style={{ marginTop: '1rem' }}>
232
+ <h3>Error</h3>
233
+ <p>{errorMessage}</p>
234
+ </div>
235
+ )}
236
+ <Snackbar
237
+ open={snackbarOpen}
238
+ autoHideDuration={3000}
239
+ onClose={handleSnackbarClose}
240
+ message="Copied To Clipboard"
241
+ anchorOrigin={{ vertical: 'bottom', horizontal: 'left' }}
242
+ TransitionComponent={SlideTransition}
243
+ ContentProps={{ classes: { root: 'custom-snackbar' } }}
244
+ action={
245
+ <IconButton
246
+ size="small"
247
+ aria-label="close"
248
+ color="inherit"
249
+ onClick={handleSnackbarClose}
250
+ >
251
+ <FaTimes />
252
+ </IconButton>
253
+ }
254
+ />
255
+ </>
256
+ );
257
+ }
258
+
259
+ export default ChatWindow;
frontend/src/Components/AiComponents/Evaluate.css ADDED
@@ -0,0 +1,107 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ /* Spinner styling */
90
+ .custom-spinner {
91
+ width: 1.35rem;
92
+ height: 1.35rem;
93
+ border: 3px solid #3b7bdc; /* Main Spinner */
94
+ border-top: 3px solid #434343; /* Rotating path */
95
+ border-radius: 50%;
96
+ animation: spin 0.9s linear infinite;
97
+ }
98
+
99
+ /* Spinner animation */
100
+ @keyframes spin {
101
+ 0% {
102
+ transform: rotate(0deg);
103
+ }
104
+ 100% {
105
+ transform: rotate(360deg);
106
+ }
107
+ }
frontend/src/Components/AiComponents/Evaluate.js ADDED
@@ -0,0 +1,213 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ };
20
+
21
+ const names = [
22
+ "Bias",
23
+ "Toxicity",
24
+ "Answer Correctness",
25
+ "Summarization",
26
+ "Faithfulness",
27
+ "Hallucination",
28
+ "Answer Relevancy",
29
+ "Contextual Relevancy",
30
+ "Contextual Recall"
31
+ ];
32
+
33
+ function getStyles(name, selectedNames, theme) {
34
+ return {
35
+ fontWeight: selectedNames.includes(name.toLowerCase())
36
+ ? theme.typography.fontWeightMedium
37
+ : theme.typography.fontWeightRegular,
38
+ };
39
+ }
40
+
41
+ export default function MultipleSelectChip({ evaluation }) {
42
+ const theme = useTheme();
43
+ const [personName, setPersonName] = React.useState([]);
44
+ const [selectedMetrics, setSelectedMetrics] = React.useState([]);
45
+ const [evaluationResult, setEvaluationResult] = React.useState("");
46
+ const [isEvaluating, setIsEvaluating] = React.useState(false);
47
+ const [localLoading, setLocalLoading] = React.useState(false);
48
+
49
+ // Reset the form fields
50
+ React.useEffect(() => {
51
+ // Reset the form and evaluation result
52
+ setPersonName([]);
53
+ setSelectedMetrics([]);
54
+ setEvaluationResult("");
55
+ setLocalLoading(true);
56
+
57
+ // Simulate a loading delay
58
+ const timer = setTimeout(() => {
59
+ setLocalLoading(false);
60
+ }, 500);
61
+ return () => clearTimeout(timer);
62
+ }, [evaluation]);
63
+
64
+ const handleChange = (event) => {
65
+ const { target: { value } } = event;
66
+ const metrics = typeof value === 'string' ? value.split(',') : value;
67
+ setPersonName(metrics);
68
+ setSelectedMetrics(metrics);
69
+ };
70
+
71
+ const handleDelete = (chipToDelete) => {
72
+ setPersonName((chips) => chips.filter((chip) => chip !== chipToDelete));
73
+ };
74
+
75
+ // Function to convert a string to title case.
76
+ const titleCase = (str) => {
77
+ return str
78
+ .split(' ')
79
+ .map(word => word.charAt(0).toUpperCase() + word.slice(1))
80
+ .join(' ');
81
+ };
82
+
83
+ const handleEvaluateClick = async () => {
84
+ // Clear previous evaluation result immediately.
85
+ setEvaluationResult("");
86
+ setIsEvaluating(true);
87
+
88
+ const payload = { ...evaluation, metrics: selectedMetrics };
89
+ try {
90
+ const res = await fetch("http://127.0.0.1:8000/action/evaluate", {
91
+ method: "POST",
92
+ headers: { "Content-Type": "application/json" },
93
+ body: JSON.stringify(payload),
94
+ });
95
+ if (!res.ok) {
96
+ const error = await res.json();
97
+ throw new Error(`Evaluation Error: ${error.error}`);
98
+ }
99
+
100
+ const data = await res.json();
101
+ if (!data.result) {
102
+ throw new Error("No results returned from evaluation");
103
+ }
104
+
105
+ // Format the JSON into Markdown.
106
+ let markdown = "### Result\n\n";
107
+ for (const [metric, details] of Object.entries(data.result)) {
108
+ let score = details.score;
109
+ if (typeof score === "number") {
110
+ const percentage = score * 100;
111
+ score = Number.isInteger(percentage)
112
+ ? percentage.toFixed(0) + "%"
113
+ : percentage.toFixed(2) + "%";
114
+ }
115
+ let reason = details.reason;
116
+ markdown += `**${titleCase(metric)}:** ${score}\n\n${reason}\n\n`;
117
+ }
118
+ setEvaluationResult(markdown);
119
+ } catch (err) {
120
+ // Use the callback to trigger the error block in ChatWindow
121
+ if (evaluation.onError && evaluation.blockId) {
122
+ evaluation.onError(evaluation.blockId, err.message || "Evaluation failed");
123
+ }
124
+ else {
125
+ console.error("Evaluation prop is missing or incomplete:", evaluation);
126
+ }
127
+ }
128
+ setIsEvaluating(false);
129
+ };
130
+
131
+ // Finds the matching display name for a metric.
132
+ const getDisplayName = (lowerValue) => {
133
+ const found = names.find(n => n.toLowerCase() === lowerValue);
134
+ return found ? found : lowerValue;
135
+ };
136
+
137
+ return (
138
+ <Box className="evaluate-container">
139
+ {localLoading ? (
140
+ <Box>
141
+ <Typography variant="body2">Loading Evaluation...</Typography>
142
+ </Box>
143
+ ) : (
144
+ <>
145
+ <FormControl className="evaluate-form-control">
146
+ <InputLabel id="chip-label">Select Metrics</InputLabel>
147
+ <Select
148
+ labelId="chip-label"
149
+ id="multiple-chip"
150
+ multiple
151
+ value={personName}
152
+ onChange={handleChange}
153
+ input={
154
+ <OutlinedInput
155
+ id="select-multiple-chip"
156
+ label="Select Metrics"
157
+ className="evaluate-outlined-input"
158
+ />
159
+ }
160
+ renderValue={(selected) => (
161
+ <Box className="chip-container">
162
+ {selected.map((value) => (
163
+ <Chip
164
+ className="evaluate-chip"
165
+ key={value}
166
+ label={getDisplayName(value)}
167
+ onDelete={() => handleDelete(value)}
168
+ onMouseDown={(event) => event.stopPropagation()}
169
+ />
170
+ ))}
171
+ </Box>
172
+ )}
173
+ MenuProps={MenuProps}
174
+ >
175
+ {names.map((name) => (
176
+ <MenuItem
177
+ key={name}
178
+ value={name.toLowerCase()} // underlying value is lowercase
179
+ style={getStyles(name, personName, theme)}
180
+ >
181
+ {name}
182
+ </MenuItem>
183
+ ))}
184
+ </Select>
185
+ </FormControl>
186
+
187
+ <Box mt={1}>
188
+ <Button
189
+ variant="contained"
190
+ onClick={handleEvaluateClick}
191
+ className="evaluate-button"
192
+ >
193
+ Evaluate
194
+ </Button>
195
+ </Box>
196
+
197
+ {isEvaluating && (
198
+ <Box mt={1} display="flex" alignItems="center">
199
+ <Box className="custom-spinner" />
200
+ <Box ml={1}>Evaluating...</Box>
201
+ </Box>
202
+ )}
203
+
204
+ {evaluationResult && (
205
+ <Box mt={2}>
206
+ <ReactMarkdown>{evaluationResult}</ReactMarkdown>
207
+ </Box>
208
+ )}
209
+ </>
210
+ )}
211
+ </Box>
212
+ );
213
+ }
frontend/src/Components/AiComponents/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/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
+ max-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: 750px !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: 65% !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: 65% !important;
84
+ color: red !important;
85
+ }
frontend/src/Components/AiComponents/Graph.js ADDED
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 }) {
6
+ const [graphHtml, setGraphHtml] = useState("");
7
+ const [loading, setLoading] = useState(true);
8
+ const [error, setError] = useState("");
9
+
10
+ useEffect(() => {
11
+ if (open && payload) {
12
+ setLoading(true);
13
+ setError("");
14
+ fetch("http://127.0.0.1:8000/action/graph", {
15
+ method: "POST",
16
+ headers: { "Content-Type": "application/json" },
17
+ body: JSON.stringify(payload)
18
+ })
19
+ .then(res => res.json())
20
+ .then(data => {
21
+ setGraphHtml(data.result);
22
+ setLoading(false);
23
+ })
24
+ .catch(err => {
25
+ console.error("Error fetching graph:", err);
26
+ setError("Error fetching graph.");
27
+ setLoading(false);
28
+ });
29
+ }
30
+ }, [payload, open]);
31
+
32
+ if (!open) return null;
33
+
34
+ return (
35
+ <div className="graph-dialog-container" onClick={onClose}>
36
+ <div className="graph-dialog-inner" onClick={e => e.stopPropagation()}>
37
+ <div className="graph-dialog-header">
38
+ <h3 className="graph-dialog-title">Graph Display</h3>
39
+ <button className="graph-dialog close-btn" onClick={onClose}>
40
+ <FaTimes />
41
+ </button>
42
+ </div>
43
+ <div className="graph-dialog-content">
44
+ {loading ? (
45
+ <div className="graph-loading">
46
+ <p>Loading Graph...</p>
47
+ </div>
48
+ ) : error ? (
49
+ <p className="graph-error">{error}</p>
50
+ ) : (
51
+ <iframe
52
+ title="Graph Display"
53
+ srcDoc={graphHtml}
54
+ style={{ border: "none", width: "100%", height: "625px" }}
55
+ />
56
+ )}
57
+ </div>
58
+ </div>
59
+ </div>
60
+ );
61
+ }
frontend/src/Components/AiComponents/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/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/RightSidebar.css ADDED
@@ -0,0 +1,129 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ position: fixed;
11
+ top: 0;
12
+ right: 0;
13
+ height: 100%;
14
+ background-color: var(--sidebar-background); /* Keep background uniform */
15
+ color: var(--text-light);
16
+ box-shadow: -2px 0 8px rgba(0, 0, 0, 0.5);
17
+ transition: transform var(--transition-speed);
18
+ overflow-y: auto;
19
+ z-index: 1000;
20
+ }
21
+
22
+ /* When the sidebar is closed */
23
+ .right-side-bar.closed {
24
+ width: 0;
25
+ overflow: hidden;
26
+ }
27
+
28
+ /* Sidebar header styling */
29
+ .sidebar-header {
30
+ display: flex;
31
+ align-items: center;
32
+ justify-content: space-between;
33
+ padding: 16px;
34
+ border-bottom: 3px solid var(--border-dark);
35
+ }
36
+
37
+ .sidebar-header h3 {
38
+ margin: 0;
39
+ font-size: 1.2rem;
40
+ }
41
+
42
+ /* Close button styling */
43
+ .close-btn {
44
+ background: none;
45
+ border: none;
46
+ padding: 6px;
47
+ color: var(--text-color);
48
+ font-size: 1.2rem;
49
+ cursor: pointer;
50
+ transition: color var(--transition-speed);
51
+ }
52
+
53
+ .close-btn:hover {
54
+ background: rgba(255, 255, 255, 0.1);
55
+ color: white;
56
+ }
57
+
58
+ /* Ensure the sidebar background remains uniform */
59
+ .sidebar-content {
60
+ padding: 16px;
61
+ background: transparent;
62
+ }
63
+
64
+ /* Also clear any default marker via the pseudo-element */
65
+ .nav-links.no-bullets li::marker {
66
+ content: "";
67
+ }
68
+
69
+ /* Lay out each task item using flex so that the icon and text align */
70
+ .task-item {
71
+ display: flex;
72
+ align-items: flex-start;
73
+ margin-bottom: 1rem;
74
+ }
75
+
76
+ /* Icon span: fixed width and margin for spacing */
77
+ .task-icon {
78
+ flex-shrink: 0;
79
+ margin-right: 1rem;
80
+ }
81
+
82
+ /* Task list text */
83
+ .task-text {
84
+ white-space: pre-wrap;
85
+ }
86
+
87
+ /* Resizer for sidebar width adjustment */
88
+ .resizer {
89
+ position: absolute;
90
+ left: 0;
91
+ top: 0;
92
+ width: 5px;
93
+ height: 100%;
94
+ cursor: ew-resize;
95
+ }
96
+
97
+ /* Toggle button (when sidebar is closed) */
98
+ .toggle-btn.right-toggle {
99
+ position: fixed;
100
+ top: 50%;
101
+ right: 0;
102
+ transform: translateY(-50%);
103
+ background-color: var(--dark-surface);
104
+ color: var(--text-light);
105
+ border: none;
106
+ padding: 8px;
107
+ cursor: pointer;
108
+ z-index: 1001;
109
+ box-shadow: -2px 0 4px rgba(0, 0, 0, 0.5);
110
+ }
111
+
112
+ .spin {
113
+ animation: spin 1s linear infinite;
114
+ color: #328bff;
115
+ }
116
+
117
+ .checkmark {
118
+ color: #03c203;
119
+ }
120
+
121
+ .x {
122
+ color: #d10808;
123
+ }
124
+
125
+ /* Keyframes for the spinner animation */
126
+ @keyframes spin {
127
+ from { transform: rotate(0deg); }
128
+ to { transform: rotate(360deg); }
129
+ }
frontend/src/Components/AiComponents/RightSidebar.js ADDED
@@ -0,0 +1,138 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React 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
+
26
+ // Called when the user starts resizing the sidebar.
27
+ const startResize = (e) => {
28
+ e.preventDefault();
29
+ document.addEventListener("mousemove", resizeSidebar);
30
+ document.addEventListener("mouseup", stopResize);
31
+ };
32
+
33
+ const resizeSidebar = (e) => {
34
+ let newWidth = window.innerWidth - e.clientX;
35
+ if (newWidth < minWidth) newWidth = minWidth;
36
+ if (newWidth > maxWidth) newWidth = maxWidth;
37
+ setRightSidebarWidth(newWidth);
38
+ };
39
+
40
+ const stopResize = () => {
41
+ document.removeEventListener("mousemove", resizeSidebar);
42
+ document.removeEventListener("mouseup", stopResize);
43
+ };
44
+
45
+ // Default handler for source clicks: open the link in a new tab.
46
+ const handleSourceClick = (source) => {
47
+ if (source && source.link) {
48
+ window.open(source.link, '_blank');
49
+ }
50
+ };
51
+
52
+ // Helper function to return the proper icon based on task status.
53
+ const getTaskIcon = (task) => {
54
+ // If the task is a simple string, default to the completed icon.
55
+ if (typeof task === 'string') {
56
+ return <FaCheck />;
57
+ }
58
+ // Use the status field to determine which icon to render.
59
+ switch (task.status) {
60
+ case 'RUNNING':
61
+ // FaSpinner is used for running tasks. The CSS class "spin" can be defined to add animation.
62
+ return <FaSpinner className="spin"/>;
63
+ case 'DONE':
64
+ return <FaCheck className="checkmark" />;
65
+ case 'FAILED':
66
+ return <FaTimes className="x" />;
67
+ default:
68
+ return <FaCheck />;
69
+ }
70
+ };
71
+
72
+ return (
73
+ <>
74
+ <nav
75
+ className={`right-side-bar ${isOpen ? "open" : "closed"}`}
76
+ style={{ width: isOpen ? rightSidebarWidth : undefined }}
77
+ >
78
+ <div className="sidebar-header">
79
+ <h3>
80
+ {sidebarContent === "sources"
81
+ ? "Sources"
82
+ : sidebarContent === "evaluate"
83
+ ? "Evaluation"
84
+ : "Tasks"}
85
+ </h3>
86
+ <button className="close-btn" onClick={toggleRightSidebar}>
87
+ <FaTimes />
88
+ </button>
89
+ </div>
90
+ <div className="sidebar-content">
91
+ {sidebarContent === "sources" ? ( // If the sidebar content is "sources", show the sources component
92
+ sourcesLoading ? (
93
+ <div className="tasks-loading">
94
+ <CircularProgress size={20} sx={{ color: '#ccc' }} />
95
+ <span className="loading-tasks-text">Generating sources...</span>
96
+ </div>
97
+ ) : (
98
+ <Sources sources={sources} handleSourceClick={onSourceClick || handleSourceClick} />
99
+ )
100
+ )
101
+ // If the sidebar content is "evaluate", show the evaluation component
102
+ : sidebarContent === "evaluate" ? (
103
+ <Evaluate evaluation={evaluation} />
104
+ ) : (
105
+ // Otherwise, show tasks
106
+ tasksLoading ? (
107
+ <div className="tasks-loading">
108
+ <CircularProgress size={20} sx={{ color: '#ccc' }} />
109
+ <span className="loading-tasks-text">Generating tasks...</span>
110
+ </div>
111
+ ) : (
112
+ <ul className="nav-links" style={{ listStyle: 'none', padding: 0 }}>
113
+ {tasks.map((task, index) => (
114
+ <li key={index} className="task-item">
115
+ <span className="task-icon">
116
+ {getTaskIcon(task)}
117
+ </span>
118
+ <span className="task-text">
119
+ {typeof task === 'string' ? task : task.task}
120
+ </span>
121
+ </li>
122
+ ))}
123
+ </ul>
124
+ )
125
+ )}
126
+ </div>
127
+ <div className="resizer" onMouseDown={startResize}></div>
128
+ </nav>
129
+ {!isOpen && (
130
+ <button className="toggle-btn right-toggle" onClick={toggleRightSidebar}>
131
+ <BsChevronLeft />
132
+ </button>
133
+ )}
134
+ </>
135
+ );
136
+ }
137
+
138
+ export default RightSidebar;
frontend/src/Components/AiComponents/Sources.css ADDED
@@ -0,0 +1,65 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 each Card component */
10
+ .source-card {
11
+ background-color: #3e3e3eec !important;
12
+ border-radius: 1rem !important;
13
+ color: #fff !important;
14
+ cursor: pointer !important;
15
+ }
16
+ .source-card:hover {
17
+ background-color: #262626e5 !important;
18
+ }
19
+
20
+ /* Styling for the CardContent title (header) - reduced text size slightly */
21
+ .source-title {
22
+ color: #fff !important;
23
+ margin-top: -0.5rem !important;
24
+ margin-bottom: 0.4rem !important;
25
+ font-size: 1rem !important;
26
+ }
27
+
28
+ /* Styling for the link row (icon, domain, bullet, serial number) */
29
+ .source-link {
30
+ display: flex !important;
31
+ align-items: center !important;
32
+ font-size: 0.8rem !important;
33
+ color: #c1c1c1 !important;
34
+ margin-bottom: 0.5rem !important;
35
+ }
36
+
37
+ /* Styling for the favicon icon - reduced size and increased brightness */
38
+ .source-icon {
39
+ width: 0.88rem !important;
40
+ height: 0.88rem !important;
41
+ margin-right: 0.3rem !important;
42
+ filter: brightness(1.2) !important; /* Makes the icon brighter */
43
+ }
44
+
45
+ /* Styling for the domain text */
46
+ .source-domain {
47
+ vertical-align: middle !important;
48
+ }
49
+
50
+ /* Styling for the separator bullet */
51
+ .separator {
52
+ margin: 0 0.3rem !important;
53
+ }
54
+
55
+ /* Styling for the serial number */
56
+ .source-serial {
57
+ font-size: 0.8rem !important;
58
+ color: #c1c1c1 !important;
59
+ }
60
+
61
+ /* Styling for the CardContent description */
62
+ .source-description {
63
+ color: #e8e8e8 !important;
64
+ margin-bottom: -0.65rem !important;
65
+ }
frontend/src/Components/AiComponents/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("http://127.0.0.1:8000/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 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/AiPage.css ADDED
@@ -0,0 +1,249 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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-x: hidden;
28
+ overflow-y: auto;
29
+ }
30
+
31
+ /* Main Content */
32
+ .main-content {
33
+ display: flex;
34
+ flex-direction: column;
35
+ align-items: center;
36
+ justify-content: center;
37
+ flex-grow: 1;
38
+ padding: 2rem;
39
+ transition: 0.1s;
40
+ width: 99%;
41
+ max-width: 800px;
42
+ margin: 0 auto;
43
+ }
44
+
45
+ /* Search Area for Initial Mode */
46
+ .search-area h1 {
47
+ margin-bottom: 1.5rem;
48
+ display: flex;
49
+ flex-direction: column;
50
+ align-items: center;
51
+ justify-content: center;
52
+ }
53
+
54
+ .search-area {
55
+ width: 99%;
56
+ max-width: 800px;
57
+ }
58
+
59
+ .search-bar {
60
+ position: relative;
61
+ width: 100%;
62
+ border-radius: 0.35rem;
63
+ background-color: #21212f;
64
+ }
65
+
66
+ .search-input-wrapper {
67
+ padding: 0rem 0.6rem 4.15rem 0.6rem;
68
+ }
69
+
70
+ .search-input {
71
+ width: 100%;
72
+ max-height: 200px;
73
+ overflow-y: auto;
74
+ font-size: 1.2rem;
75
+ border: none;
76
+ background-color: transparent;
77
+ color: var(--text-color);
78
+ line-height: 1.4;
79
+ padding: 0.65rem 0.65rem;
80
+ resize: none;
81
+ white-space: pre-wrap;
82
+ }
83
+
84
+ .search-input:focus {
85
+ outline: none;
86
+ }
87
+
88
+ .search-input::placeholder {
89
+ color: #888;
90
+ }
91
+
92
+ .icon-container {
93
+ position: absolute;
94
+ bottom: 0.25rem;
95
+ left: 0;
96
+ right: 0;
97
+ height: 3rem;
98
+ display: flex;
99
+ justify-content: space-between;
100
+ align-items: center;
101
+ padding: 0 0.75rem;
102
+ }
103
+
104
+ .settings-btn,
105
+ .send-btn {
106
+ background: transparent;
107
+ border: none;
108
+ color: var(--text-color);
109
+ }
110
+
111
+ .settings-btn svg,
112
+ .send-btn svg {
113
+ font-size: 1.45rem;
114
+ }
115
+
116
+ .settings-btn:hover,
117
+ .send-btn:hover {
118
+ color: #888;
119
+ }
120
+
121
+ /* Stop button */
122
+ button.chat-send-btn.stop-btn,
123
+ button.send-btn.stop-btn {
124
+ background-color: var(--text-color) !important;
125
+ border-radius: 50%;
126
+ width: 28.5px;
127
+ height: 28.5px;
128
+ border: none;
129
+ display: flex;
130
+ align-items: center;
131
+ justify-content: center;
132
+ cursor: pointer;
133
+ padding: 0;
134
+ margin: 0 0 10px;
135
+ }
136
+
137
+ button.chat-send-btn.stop-btn:hover,
138
+ button.send-btn.stop-btn:hover {
139
+ background-color: #888 !important;
140
+ }
141
+
142
+ /* Chat Mode Search Bar and Textarea Styling */
143
+ .floating-chat-search-bar {
144
+ position: fixed;
145
+ bottom: 1.5rem;
146
+ left: 50%;
147
+ transform: translateX(-50%);
148
+ width: 100%;
149
+ max-width: 800px;
150
+ background-color: #21212f;
151
+ border-radius: 0.35rem;
152
+ }
153
+
154
+ .floating-chat-search-bar::after {
155
+ content: "";
156
+ position: absolute;
157
+ left: 0;
158
+ right: 0;
159
+ bottom: -1.5rem;
160
+ height: 1.5rem;
161
+ background-color: var(--dark-surface);
162
+ }
163
+
164
+ .chat-search-input-wrapper {
165
+ padding: 0.25rem 0.6rem;
166
+ }
167
+
168
+ .chat-search-input {
169
+ width: 100%;
170
+ min-width: 700px;
171
+ max-height: 200px;
172
+ overflow-y: auto;
173
+ font-size: 1.2rem;
174
+ border: none;
175
+ background-color: transparent;
176
+ color: var(--text-color);
177
+ line-height: 1.4;
178
+ padding: 0.65rem 3.25rem;
179
+ resize: none;
180
+ white-space: pre-wrap;
181
+ }
182
+
183
+ .chat-search-input:focus {
184
+ outline: none;
185
+ }
186
+
187
+ .chat-icon-container {
188
+ position: absolute;
189
+ left: 0;
190
+ right: 0;
191
+ bottom: 0;
192
+ height: 3rem;
193
+ display: flex;
194
+ justify-content: space-between;
195
+ align-items: center;
196
+ padding: 0 0.75rem;
197
+ pointer-events: none;
198
+ }
199
+
200
+ /* Re-enable pointer events on the actual buttons so they remain clickable */
201
+ .chat-icon-container button {
202
+ pointer-events: auto;
203
+ }
204
+
205
+ .chat-settings-btn,
206
+ .chat-send-btn {
207
+ background: transparent;
208
+ border: none;
209
+ color: var(--text-color);
210
+ font-size: 1.45rem;
211
+ margin-bottom: 0.25rem;
212
+ cursor: pointer;
213
+ }
214
+
215
+ .chat-settings-btn:hover,
216
+ .chat-send-btn:hover {
217
+ color: #888;
218
+ }
219
+
220
+ /* Floating sidebar container for chat mode */
221
+ .floating-sidebar {
222
+ position: fixed;
223
+ right: 0;
224
+ top: 0;
225
+ bottom: 0;
226
+ width: var(--right-sidebar-width, 300px);
227
+ z-index: 1000;
228
+ }
229
+
230
+ /* Chat container */
231
+ .chat-container {
232
+ flex-grow: 1;
233
+ margin-bottom: 9rem;
234
+ }
235
+
236
+ /* Responsive Adjustments */
237
+ @media (max-width: 768px) {
238
+ .main-content {
239
+ margin-left: 0;
240
+ margin-right: 0;
241
+ }
242
+ }
243
+
244
+ @media (max-width: 576px) {
245
+ .main-content {
246
+ margin: 0;
247
+ padding: 1rem;
248
+ }
249
+ }
frontend/src/Components/AiPage.js ADDED
@@ -0,0 +1,439 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState, useEffect, useRef } from 'react';
2
+ import './AiPage.css';
3
+ import { FaCog, FaPaperPlane, FaStop } from 'react-icons/fa';
4
+ import IntialSetting from './IntialSetting';
5
+ import ChatWindow from './AiComponents/ChatWindow';
6
+ import RightSidebar from './AiComponents/RightSidebar';
7
+
8
+ function AiPage() {
9
+ // Sidebar and other states
10
+ const [isRightSidebarOpen, setRightSidebarOpen] = useState(
11
+ localStorage.getItem("rightSidebarState") === "true"
12
+ );
13
+ const [rightSidebarWidth, setRightSidebarWidth] = useState(300);
14
+ const [sidebarContent, setSidebarContent] = useState("default");
15
+
16
+ const [searchText, setSearchText] = useState("");
17
+ const textAreaRef = useRef(null);
18
+ const [showSettingsModal, setShowSettingsModal] = useState(false);
19
+
20
+ const [showChatWindow, setShowChatWindow] = useState(false);
21
+ const [chatBlocks, setChatBlocks] = useState([]);
22
+ const [selectedChatBlockId, setSelectedChatBlockId] = useState(null);
23
+
24
+ const [defaultChatHeight, setDefaultChatHeight] = useState(null);
25
+ const [chatBottomPadding, setChatBottomPadding] = useState("60px");
26
+
27
+ // States/refs for streaming
28
+ const [isProcessing, setIsProcessing] = useState(false);
29
+ const [activeBlockId, setActiveBlockId] = useState(null);
30
+ const activeEventSourceRef = useRef(null);
31
+
32
+ useEffect(() => {
33
+ localStorage.setItem("rightSidebarState", isRightSidebarOpen);
34
+ }, [isRightSidebarOpen]);
35
+
36
+ useEffect(() => {
37
+ document.documentElement.style.setProperty('--right-sidebar-width', rightSidebarWidth + 'px');
38
+ }, [rightSidebarWidth]);
39
+
40
+ useEffect(() => {
41
+ if (textAreaRef.current) {
42
+ if (!defaultChatHeight) {
43
+ setDefaultChatHeight(textAreaRef.current.scrollHeight);
44
+ }
45
+ textAreaRef.current.style.height = "auto";
46
+ textAreaRef.current.style.overflowY = "hidden";
47
+
48
+ const newHeight = textAreaRef.current.scrollHeight;
49
+ let finalHeight = newHeight;
50
+ if (newHeight > 200) {
51
+ finalHeight = 200;
52
+ textAreaRef.current.style.overflowY = "auto";
53
+ }
54
+ textAreaRef.current.style.height = `${finalHeight}px`;
55
+
56
+ const minPaddingPx = 0;
57
+ const maxPaddingPx = 59;
58
+ let newPaddingPx = minPaddingPx;
59
+ if (defaultChatHeight && finalHeight > defaultChatHeight) {
60
+ newPaddingPx =
61
+ minPaddingPx +
62
+ ((finalHeight - defaultChatHeight) / (200 - defaultChatHeight)) *
63
+ (maxPaddingPx - minPaddingPx);
64
+ if (newPaddingPx > maxPaddingPx) newPaddingPx = maxPaddingPx;
65
+ }
66
+ setChatBottomPadding(`${newPaddingPx}px`);
67
+ }
68
+ }, [searchText, defaultChatHeight]);
69
+
70
+ const handleOpenRightSidebar = (content, chatBlockId = null) => {
71
+ if (chatBlockId) {
72
+ setSelectedChatBlockId(chatBlockId);
73
+ }
74
+ setSidebarContent(content ? content : "default");
75
+ setRightSidebarOpen(true);
76
+ };
77
+
78
+ const handleEvaluationError = (blockId, errorMsg) => {
79
+ setChatBlocks(prev =>
80
+ prev.map(block =>
81
+ block.id === blockId
82
+ ? { ...block, isError: true, errorMessage: errorMsg }
83
+ : block
84
+ )
85
+ );
86
+ };
87
+
88
+ // Initiate the SSE
89
+ const initiateSSE = (query, blockId) => {
90
+ const startTime = Date.now();
91
+ const sseUrl = `http://127.0.0.1:8000/message-sse?user_message=${encodeURIComponent(query)}`;
92
+ const eventSource = new EventSource(sseUrl);
93
+ activeEventSourceRef.current = eventSource;
94
+
95
+ eventSource.addEventListener("token", (e) => {
96
+ setChatBlocks(prev => prev.map(block =>
97
+ block.id === blockId
98
+ ? { ...block, aiAnswer: block.aiAnswer + e.data }
99
+ : block
100
+ ));
101
+ });
102
+
103
+ eventSource.addEventListener("final_message", (e) => {
104
+ const endTime = Date.now();
105
+ const thinkingTime = ((endTime - startTime) / 1000).toFixed(1);
106
+ // Only update thinkingTime so the streaming flag turns false and the cursor disappears
107
+ setChatBlocks(prev => prev.map(block =>
108
+ block.id === blockId
109
+ ? { ...block, thinkingTime }
110
+ : block
111
+ ));
112
+ });
113
+
114
+ // Listen for the "complete" event to know when to close the connection.
115
+ eventSource.addEventListener("complete", (e) => {
116
+ console.log("Complete event received:", e.data);
117
+ eventSource.close();
118
+ activeEventSourceRef.current = null;
119
+ setIsProcessing(false);
120
+ setActiveBlockId(null);
121
+ });
122
+
123
+ // Update actions for only this chat block.
124
+ eventSource.addEventListener("action", (e) => {
125
+ try {
126
+ const actionData = JSON.parse(e.data);
127
+ console.log("Action event received:", actionData);
128
+ setChatBlocks(prev => prev.map(block => {
129
+ if (block.id === blockId) {
130
+ let updatedBlock = { ...block, actions: [...(block.actions || []), actionData] };
131
+ if (actionData.name === "sources") {
132
+ updatedBlock.sources = actionData.payload;
133
+ }
134
+ if (actionData.name === "graph") {
135
+ updatedBlock.graph = actionData.payload;
136
+ }
137
+ return updatedBlock;
138
+ }
139
+ return block;
140
+ }));
141
+ } catch (err) {
142
+ console.error("Error parsing action event:", err);
143
+ }
144
+ });
145
+
146
+ // Update the error for this chat block.
147
+ eventSource.addEventListener("error", (e) => {
148
+ console.error("Error from SSE:", e.data);
149
+ setChatBlocks(prev => prev.map(block =>
150
+ block.id === blockId
151
+ ? { ...block, isError: true, errorMessage: e.data, aiAnswer: "" }
152
+ : block
153
+ ));
154
+ eventSource.close();
155
+ activeEventSourceRef.current = null;
156
+ setIsProcessing(false);
157
+ setActiveBlockId(null);
158
+ });
159
+
160
+ eventSource.addEventListener("step", (e) => {
161
+ console.log("Step event received:", e.data);
162
+ setChatBlocks(prev => prev.map(block =>
163
+ block.id === blockId
164
+ ? { ...block, thoughtLabel: e.data }
165
+ : block
166
+ ));
167
+ });
168
+
169
+ eventSource.addEventListener("sources_read", (e) => {
170
+ console.log("Sources read event received:", e.data);
171
+ try {
172
+ const parsed = JSON.parse(e.data);
173
+ let count;
174
+ if (typeof parsed === 'number') {
175
+ count = parsed;
176
+ } else if (parsed && typeof parsed.count === 'number') {
177
+ count = parsed.count;
178
+ }
179
+ if (typeof count === 'number') {
180
+ setChatBlocks(prev => prev.map(block =>
181
+ block.id === blockId
182
+ ? { ...block, sourcesRead: count, sources: parsed.sources || [] }
183
+ : block
184
+ ));
185
+ }
186
+ } catch(err) {
187
+ if (e.data.trim() !== "") {
188
+ setChatBlocks(prev => prev.map(block =>
189
+ block.id === blockId
190
+ ? { ...block, sourcesRead: e.data }
191
+ : block
192
+ ));
193
+ }
194
+ }
195
+ });
196
+
197
+ eventSource.addEventListener("task", (e) => {
198
+ console.log("Task event received:", e.data);
199
+ try {
200
+ const taskData = JSON.parse(e.data);
201
+ setChatBlocks(prev => prev.map(block => {
202
+ if (block.id === blockId) {
203
+ const existingTaskIndex = (block.tasks || []).findIndex(t => t.task === taskData.task);
204
+ if (existingTaskIndex !== -1) {
205
+ const updatedTasks = [...block.tasks];
206
+ updatedTasks[existingTaskIndex] = { ...updatedTasks[existingTaskIndex], status: taskData.status };
207
+ return { ...block, tasks: updatedTasks };
208
+ } else {
209
+ return { ...block, tasks: [...(block.tasks || []), taskData] };
210
+ }
211
+ }
212
+ return block;
213
+ }));
214
+ } catch (error) {
215
+ console.error("Error parsing task event:", error);
216
+ }
217
+ });
218
+ };
219
+
220
+ // Create a new chat block and initiate the SSE
221
+ const handleSend = () => {
222
+ if (!searchText.trim()) return;
223
+ const blockId = new Date().getTime();
224
+ setActiveBlockId(blockId);
225
+ setIsProcessing(true);
226
+ setChatBlocks(prev => [
227
+ ...prev,
228
+ {
229
+ id: blockId,
230
+ userMessage: searchText,
231
+ aiAnswer: "",
232
+ thinkingTime: null,
233
+ thoughtLabel: "",
234
+ sourcesRead: "",
235
+ tasks: [],
236
+ sources: [],
237
+ actions: []
238
+ }
239
+ ]);
240
+ setShowChatWindow(true);
241
+ const query = searchText;
242
+ setSearchText("");
243
+ initiateSSE(query, blockId);
244
+ };
245
+
246
+ const handleKeyDown = (e) => {
247
+ if (e.key === "Enter" && !e.shiftKey) {
248
+ e.preventDefault();
249
+ if (!isProcessing) {
250
+ handleSend();
251
+ }
252
+ }
253
+ };
254
+
255
+ // Stop the user request and close the active SSE connection
256
+ const handleStop = async () => {
257
+ // Close the active SSE connection if it exists
258
+ if (activeEventSourceRef.current) {
259
+ activeEventSourceRef.current.close();
260
+ activeEventSourceRef.current = null;
261
+ }
262
+ // Send POST request to /stop and update the chat block with the returned message
263
+ try {
264
+ const response = await fetch('http://127.0.0.1:8000/stop', {
265
+ method: 'POST',
266
+ headers: { 'Content-Type': 'application/json' },
267
+ body: JSON.stringify({})
268
+ });
269
+ const data = await response.json();
270
+
271
+ if (activeBlockId) {
272
+ setChatBlocks(prev => prev.map(block =>
273
+ block.id === activeBlockId
274
+ ? { ...block, aiAnswer: data.message, thinkingTime: 0, tasks: [] }
275
+ : block
276
+ ));
277
+ }
278
+ } catch (error) {
279
+ console.error("Error stopping the request:", error);
280
+ if (activeBlockId) {
281
+ setChatBlocks(prev => prev.map(block =>
282
+ block.id === activeBlockId
283
+ ? { ...block, aiAnswer: "Error stopping task", thinkingTime: 0, tasks: [] }
284
+ : block
285
+ ));
286
+ }
287
+ }
288
+ setIsProcessing(false);
289
+ setActiveBlockId(null);
290
+ };
291
+
292
+ const handleSendButtonClick = () => {
293
+ if (searchText.trim()) handleSend();
294
+ };
295
+
296
+ // Get the chat block whose details should be shown in the sidebar.
297
+ const selectedBlock = chatBlocks.find(block => block.id === selectedChatBlockId);
298
+ const evaluateAction = selectedBlock && selectedBlock.actions
299
+ ? selectedBlock.actions.find(a => a.name === "evaluate")
300
+ : null;
301
+
302
+ return (
303
+ <div
304
+ className="app-container"
305
+ style={{
306
+ paddingRight: isRightSidebarOpen
307
+ ? Math.max(0, rightSidebarWidth - 250) + 'px'
308
+ : 0,
309
+ }}
310
+ >
311
+ {showChatWindow && selectedBlock && (sidebarContent !== "default" || (selectedBlock.tasks && selectedBlock.tasks.length > 0) || (selectedBlock.sources && selectedBlock.sources.length > 0)) && (
312
+ <div className="floating-sidebar">
313
+ <RightSidebar
314
+ isOpen={isRightSidebarOpen}
315
+ rightSidebarWidth={rightSidebarWidth}
316
+ setRightSidebarWidth={setRightSidebarWidth}
317
+ toggleRightSidebar={() => setRightSidebarOpen(!isRightSidebarOpen)}
318
+ sidebarContent={sidebarContent}
319
+ tasks={selectedBlock.tasks || []}
320
+ tasksLoading={false}
321
+ sources={selectedBlock.sources || []}
322
+ sourcesLoading={false}
323
+ onSourceClick={(source) => {
324
+ if (!source || !source.link) return;
325
+ window.open(source.link, '_blank');
326
+ }}
327
+ evaluation={
328
+ evaluateAction
329
+ ? { ...evaluateAction.payload, blockId: selectedBlock?.id, onError: handleEvaluationError }
330
+ : null
331
+ }
332
+ />
333
+ </div>
334
+ )}
335
+
336
+ <main className="main-content">
337
+ {showChatWindow ? (
338
+ <>
339
+ <div className="chat-container">
340
+ {chatBlocks.map((block) => (
341
+ <ChatWindow
342
+ key={block.id}
343
+ blockId={block.id}
344
+ userMessage={block.userMessage}
345
+ aiAnswer={block.aiAnswer}
346
+ thinkingTime={block.thinkingTime}
347
+ thoughtLabel={block.thoughtLabel}
348
+ sourcesRead={block.sourcesRead}
349
+ actions={block.actions}
350
+ tasks={block.tasks}
351
+ openRightSidebar={handleOpenRightSidebar}
352
+ openLeftSidebar={() => { /* if needed */ }}
353
+ isError={block.isError}
354
+ errorMessage={block.errorMessage}
355
+ />
356
+ ))}
357
+ </div>
358
+ <div
359
+ className="floating-chat-search-bar"
360
+ style={{
361
+ transform: isRightSidebarOpen
362
+ ? `translateX(calc(-50% - ${Math.max(0, (rightSidebarWidth - 250) / 2)}px))`
363
+ : 'translateX(-50%)'
364
+ }}
365
+ >
366
+ <div className="chat-search-input-wrapper" style={{ paddingBottom: chatBottomPadding }}>
367
+ <textarea
368
+ rows="1"
369
+ className="chat-search-input"
370
+ placeholder="Message..."
371
+ value={searchText}
372
+ onChange={(e) => setSearchText(e.target.value)}
373
+ onKeyDown={handleKeyDown}
374
+ ref={textAreaRef}
375
+ />
376
+ </div>
377
+ <div className="chat-icon-container">
378
+ <button
379
+ className="chat-settings-btn"
380
+ onClick={() => setShowSettingsModal(true)}
381
+ >
382
+ <FaCog />
383
+ </button>
384
+ {/* Conditionally render Stop or Send button */}
385
+ <button
386
+ className={`chat-send-btn ${isProcessing ? 'stop-btn' : ''}`}
387
+ onClick={isProcessing ? handleStop : handleSendButtonClick}
388
+ >
389
+ {isProcessing ? <FaStop size={12} color="black" /> : <FaPaperPlane />}
390
+ </button>
391
+ </div>
392
+ </div>
393
+ </>
394
+ ) : (
395
+ <div className="search-area">
396
+ <h1>How can I help you today?</h1>
397
+ <div className="search-bar">
398
+ <div className="search-input-wrapper">
399
+ <textarea
400
+ rows="1"
401
+ className="search-input"
402
+ placeholder="Message..."
403
+ value={searchText}
404
+ onChange={(e) => setSearchText(e.target.value)}
405
+ onKeyDown={handleKeyDown}
406
+ ref={textAreaRef}
407
+ />
408
+ </div>
409
+ <div className="icon-container">
410
+ <button
411
+ className="settings-btn"
412
+ onClick={() => setShowSettingsModal(true)}
413
+ >
414
+ <FaCog />
415
+ </button>
416
+ <button
417
+ className={`send-btn ${isProcessing ? 'stop-btn' : ''}`}
418
+ onClick={isProcessing ? handleStop : handleSendButtonClick}
419
+ >
420
+ {isProcessing ? <FaStop /> : <FaPaperPlane />}
421
+ </button>
422
+ </div>
423
+ </div>
424
+ </div>
425
+ )}
426
+ </main>
427
+
428
+ {showSettingsModal && (
429
+ <IntialSetting
430
+ trigger={true}
431
+ setTrigger={() => setShowSettingsModal(false)}
432
+ fromAiPage={true}
433
+ />
434
+ )}
435
+ </div>
436
+ );
437
+ }
438
+
439
+ 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,397 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 Snackbar from '@mui/material/Snackbar';
9
+ import Alert from '@mui/material/Alert';
10
+ import './IntialSetting.css';
11
+ import { FaTimes, FaEye, FaEyeSlash } from 'react-icons/fa';
12
+
13
+ function IntialSetting(props) {
14
+ // Controlled states for Model Provider and sliders
15
+ const [selectedProvider, setSelectedProvider] = useState("OpenAI");
16
+ const [modelTemperature, setModelTemperature] = useState(0.0);
17
+ const [modelTopP, setModelTopP] = useState(1.0);
18
+ const [showPassword, setShowPassword] = useState(false);
19
+
20
+ // Snackbar state for notifications
21
+ const [snackbar, setSnackbar] = useState({
22
+ open: false,
23
+ message: "",
24
+ severity: "success", // "success", "error", "info", "warning"
25
+ });
26
+
27
+ // Ref for the form element
28
+ const formRef = useRef(null);
29
+
30
+ // React Router hook to navigate programmatically
31
+ const navigate = useNavigate();
32
+
33
+ // Define model options
34
+ const modelOptions = {
35
+ OpenAI: {
36
+ "GPT-4 Turbo": "gpt-4-turbo",
37
+ "GPT-4o": "gpt-4o",
38
+ "GPT-4o Latest": "gpt-4o-2024-11-20",
39
+ "GPT-4o Mini": "gpt-4o-mini",
40
+ "ChatGPT": "chatgpt-4o-latest",
41
+ },
42
+ Anthropic: {
43
+ "Claude 3.5 Sonnet": "claude-3-5-sonnet-20241022",
44
+ "Claude 3.5 Haiku": "claude-3-5-haiku-20241022",
45
+ "Claude 3 Opus": "claude-3-opus-20240229",
46
+ "Claude 3 Sonnet": "claude-3-sonnet-20240229",
47
+ "Claude 3 Haiku": "claude-3-haiku-20240307",
48
+ },
49
+ Google: {
50
+ "Gemini 1.5 Pro": "gemini-1.5-pro",
51
+ "Gemini 1.5 Flash": "gemini-1.5-flash",
52
+ "Gemini 2.0 Flash Lite": "gemini-2.0-flash-lite-preview-02-05",
53
+ "Gemini 2.0 Flash Experimental": "gemini-2.0-flash-exp",
54
+ "Gemini 2.0 Flash": "gemini-2.0-flash",
55
+ "Gemini 2.0 Pro Experimental": "gemini-2.0-pro-exp-02-05",
56
+ },
57
+ XAI: {
58
+ "Grok-2": "grok-2-latest",
59
+ "Grok Beta": "grok-beta",
60
+ },
61
+ };
62
+
63
+ // Reset handler: resets both the form (uncontrolled fields) and controlled states
64
+ const handleReset = (e) => {
65
+ e.preventDefault();
66
+ if (formRef.current) {
67
+ formRef.current.reset();
68
+ }
69
+ setSelectedProvider("OpenAI");
70
+ setModelTemperature(0.0);
71
+ setModelTopP(1.0);
72
+ };
73
+
74
+ // Snackbar close handler
75
+ const handleSnackbarClose = (event, reason) => {
76
+ if (reason === 'clickaway') return;
77
+ setSnackbar((prev) => ({ ...prev, open: false }));
78
+ };
79
+
80
+ // Save handler: validates the form, shows notifications, calls the parent's callback
81
+ // to update the underlying page (spinner/initializing text), sends the API request, and
82
+ // navigates to /AiPage when the backend returns success.
83
+ const handleSave = async (e) => {
84
+ e.preventDefault();
85
+ const form = formRef.current;
86
+
87
+ // Retrieve values from form fields using their name attribute
88
+ const modelProvider = form.elements["model-provider"].value;
89
+ const modelName = form.elements["model-name"].value;
90
+ const modelAPIKeys = form.elements["model-api"].value;
91
+ const braveAPIKey = form.elements["brave-api"].value;
92
+ const proxyList = form.elements["proxy-list"].value;
93
+ const neo4jURL = form.elements["neo4j-url"].value;
94
+ const neo4jUsername = form.elements["neo4j-username"].value;
95
+ const neo4jPassword = form.elements["neo4j-password"].value;
96
+
97
+ // Validate required fields and collect missing field names
98
+ const missingFields = [];
99
+ if (!modelProvider || modelProvider.trim() === "") missingFields.push("Model Provider");
100
+ if (!modelName || modelName.trim() === "") missingFields.push("Model Name");
101
+ if (!modelAPIKeys || modelAPIKeys.trim() === "") missingFields.push("Model API Key");
102
+ if (!braveAPIKey || braveAPIKey.trim() === "") missingFields.push("Brave Search API Key");
103
+ if (!neo4jURL || neo4jURL.trim() === "") missingFields.push("Neo4j URL");
104
+ if (!neo4jUsername || neo4jUsername.trim() === "") missingFields.push("Neo4j Username");
105
+ if (!neo4jPassword || neo4jPassword.trim() === "") missingFields.push("Neo4j Password");
106
+
107
+ // If any required fields are missing, show an error notification
108
+ if (missingFields.length > 0) {
109
+ setSnackbar({
110
+ open: true,
111
+ message: "Please fill in the following required fields: " + missingFields.join(", "),
112
+ severity: "error",
113
+ });
114
+ return;
115
+ }
116
+
117
+ // Build the JSON payload
118
+ const payload = {
119
+ "Model_Provider": modelProvider.toLowerCase(),
120
+ "Model_Name": modelName,
121
+ "Model_API_Keys": modelAPIKeys,
122
+ "Brave_Search_API_Key": braveAPIKey,
123
+ "Neo4j_URL": neo4jURL,
124
+ "Neo4j_Username": neo4jUsername,
125
+ "Neo4j_Password": neo4jPassword,
126
+ "Model_Temperature": modelTemperature,
127
+ "Model_Top_P": modelTopP,
128
+ };
129
+
130
+ // Include Proxy List if provided
131
+ if (proxyList && proxyList.trim() !== "") {
132
+ payload["Proxy_List"] = proxyList;
133
+ }
134
+
135
+ // If opened from AiPage, show "Re-applying settings..." info notification with spinner
136
+ if (props.fromAiPage) {
137
+ setSnackbar({
138
+ open: true,
139
+ message: (
140
+ <Box mt={1} display="flex" alignItems="center">
141
+ <Box className="re-applying-settings-custom-spinner" />
142
+ <Box ml={1} className="re-applying-settings-text">
143
+ <span>
144
+ Re-applying settings. This may take a few minutes...
145
+ </span>
146
+ </Box>
147
+ </Box>
148
+ ),
149
+ severity: "info",
150
+ });
151
+ } else {
152
+ // Original immediate success notification if opened from Home/App
153
+ setSnackbar({
154
+ open: true,
155
+ message: "Settings saved successfully!",
156
+ severity: "success",
157
+ });
158
+
159
+ // Call the parent's callback to change the underlying page's content (spinner/text)
160
+ if (props.onInitializationStart) {
161
+ props.onInitializationStart();
162
+ }
163
+ }
164
+
165
+ // Send the payload to the backend
166
+ try {
167
+ const response = await fetch("http://127.0.0.1:8000/settings", {
168
+ method: "POST",
169
+ headers: {
170
+ "Content-Type": "application/json",
171
+ },
172
+ body: JSON.stringify(payload),
173
+ });
174
+
175
+ if (response.ok) {
176
+ const data = await response.json();
177
+ // When the backend returns {"success": true}, navigate to /AiPage
178
+ if (data.success === true) {
179
+ // If from AiPage, show the final "Settings saved successfully!" success notification
180
+ if (props.fromAiPage) {
181
+ setSnackbar({
182
+ open: true,
183
+ message: "Settings saved successfully!",
184
+ severity: "success",
185
+ });
186
+ }
187
+ navigate("/AiPage");
188
+ } else {
189
+ // If the response is OK but success is not true
190
+ setSnackbar({
191
+ open: true,
192
+ message: "Error saving settings. Please try again.",
193
+ severity: "error",
194
+ });
195
+ }
196
+ } else {
197
+ // If response is not OK, display error notification
198
+ setSnackbar({
199
+ open: true,
200
+ message: "Error saving settings. Please try again.",
201
+ severity: "error",
202
+ });
203
+ }
204
+ } catch (error) {
205
+ console.error("Error saving settings:", error);
206
+ // Show error notification
207
+ setSnackbar({
208
+ open: true,
209
+ message: "Error saving settings. Please try again.",
210
+ severity: "error",
211
+ });
212
+ }
213
+ };
214
+
215
+ return props.trigger ? (
216
+ <div className="showSetting" onClick={() => props.setTrigger(false)}>
217
+ <div className="showSetting-inner" onClick={(e) => e.stopPropagation()}>
218
+ <label className="setting-size">Settings</label>
219
+ <button className="close-btn" onClick={() => props.setTrigger(false)}>
220
+ <FaTimes />
221
+ </button>
222
+ <form ref={formRef}>
223
+
224
+ {/* Model Provider Selection */}
225
+ <div className="form-group">
226
+ <label htmlFor="model-provider">Model Provider</label>
227
+ <select
228
+ id="model-provider"
229
+ name="model-provider"
230
+ value={selectedProvider}
231
+ onChange={(e) => setSelectedProvider(e.target.value)}
232
+ >
233
+ {Object.keys(modelOptions).map(provider => (
234
+ <option key={provider} value={provider}>
235
+ {provider}
236
+ </option>
237
+ ))}
238
+ </select>
239
+ </div>
240
+
241
+ {/* Model Name Selection */}
242
+ <div className="form-group">
243
+ <label htmlFor="model-name">Model Name</label>
244
+ <select id="model-name" name="model-name">
245
+ {Object.entries(modelOptions[selectedProvider]).map(
246
+ ([displayName, backendName]) => (
247
+ <option key={backendName} value={backendName}>
248
+ {displayName}
249
+ </option>
250
+ )
251
+ )}
252
+ </select>
253
+ </div>
254
+
255
+ {/* API Key Inputs */}
256
+ <div className="form-group">
257
+ <label htmlFor="model-api">Model API Key</label>
258
+ <textarea
259
+ id="model-api"
260
+ name="model-api"
261
+ placeholder="Enter API Key, one per line"
262
+ ></textarea>
263
+ </div>
264
+ <div className="form-group">
265
+ <label htmlFor="brave-api">Brave Search API Key</label>
266
+ <input
267
+ type="text"
268
+ id="brave-api"
269
+ name="brave-api"
270
+ placeholder="Enter API Key"
271
+ />
272
+ </div>
273
+
274
+ {/* Proxy List */}
275
+ <div className="form-group">
276
+ <label htmlFor="proxy-list">Proxy List</label>
277
+ <textarea
278
+ id="proxy-list"
279
+ name="proxy-list"
280
+ placeholder="Enter proxies, one per line"
281
+ ></textarea>
282
+ </div>
283
+
284
+ {/* Neo4j Configuration */}
285
+ <div className="form-group">
286
+ <label htmlFor="neo4j-url">Neo4j URL</label>
287
+ <input
288
+ type="text"
289
+ id="neo4j-url"
290
+ name="neo4j-url"
291
+ placeholder="Enter Neo4j URL"
292
+ />
293
+ </div>
294
+ <div className="form-group">
295
+ <label htmlFor="neo4j-username">Neo4j Username</label>
296
+ <input
297
+ type="text"
298
+ id="neo4j-username"
299
+ name="neo4j-username"
300
+ placeholder="Enter Username"
301
+ />
302
+ </div>
303
+ <div className="form-group">
304
+ <label htmlFor="neo4j-password">Neo4j Password</label>
305
+ <div className="password-wrapper">
306
+ <input
307
+ type={showPassword ? "text" : "password"}
308
+ id="neo4j-password"
309
+ name="neo4j-password"
310
+ placeholder="Enter Password"
311
+ />
312
+ <IconButton
313
+ onClick={() => setShowPassword(prev => !prev)}
314
+ className="password-toggle"
315
+ sx={{
316
+ color: "white", // Change the color of the icon
317
+ p: 0, // Remove internal padding
318
+ m: 0 // Remove any margin
319
+ }}
320
+ >
321
+ {showPassword ? <FaEyeSlash /> : <FaEye />}
322
+ </IconButton>
323
+ </div>
324
+ </div>
325
+
326
+ {/* Model Temperature and Top-P */}
327
+ <div className="form-group">
328
+ <div className="sliders-container">
329
+ <div className="slider-item">
330
+ <label htmlFor="temperature">Temperature</label>
331
+ <Slider
332
+ id="temperature"
333
+ value={modelTemperature}
334
+ onChange={(e, newValue) => setModelTemperature(newValue)}
335
+ step={0.05}
336
+ min={0.0}
337
+ max={1.0}
338
+ valueLabelDisplay="auto"
339
+ sx={{ width: '100%', color: 'success.main' }}
340
+ />
341
+ </div>
342
+ <div className="slider-item">
343
+ <label htmlFor="top-p">Top-P</label>
344
+ <Slider
345
+ id="top-p"
346
+ value={modelTopP}
347
+ onChange={(e, newValue) => setModelTopP(newValue)}
348
+ step={0.05}
349
+ min={0.0}
350
+ max={1.0}
351
+ valueLabelDisplay="auto"
352
+ sx={{ width: '100%', color: 'success.main' }}
353
+ />
354
+ </div>
355
+ </div>
356
+ </div>
357
+
358
+ {/* Buttons */}
359
+ <Stack direction="row" spacing={2} sx={{ justifyContent: 'flex-end' }}>
360
+ <Button
361
+ type="button"
362
+ className="reset-btn"
363
+ sx={{ color: "#2196f3" }}
364
+ onClick={handleReset}
365
+ >
366
+ Reset
367
+ </Button>
368
+ <Button
369
+ type="button"
370
+ variant="contained"
371
+ color="success"
372
+ className="save-btn"
373
+ onClick={handleSave}
374
+ >
375
+ Save
376
+ </Button>
377
+ </Stack>
378
+ </form>
379
+
380
+ {/* Notifications */}
381
+ <Snackbar
382
+ open={snackbar.open}
383
+ autoHideDuration={snackbar.severity === 'success' ? 3000 : null}
384
+ onClose={handleSnackbarClose}
385
+ anchorOrigin={{ vertical: 'top', horizontal: 'center' }}
386
+ >
387
+ <Alert onClose={handleSnackbarClose} severity={snackbar.severity} variant="filled" sx={{ width: '100%' }}>
388
+ {snackbar.message}
389
+ </Alert>
390
+ </Snackbar>
391
+ {props.children}
392
+ </div>
393
+ </div>
394
+ ) : null;
395
+ }
396
+
397
+ 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';
main.py ADDED
@@ -0,0 +1,761 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import re
3
+ import asyncio
4
+ import json
5
+ import time
6
+ import logging
7
+ from typing import Any, Dict
8
+ from fastapi.staticfiles import StaticFiles
9
+ from fastapi import FastAPI, Request, HTTPException
10
+ from fastapi.responses import StreamingResponse, JSONResponse, FileResponse
11
+ from fastapi.middleware.cors import CORSMiddleware
12
+ from dotenv import load_dotenv
13
+ from openai import RateLimitError
14
+ from anthropic import RateLimitError as AnthropicRateLimitError
15
+ from google.api_core.exceptions import ResourceExhausted
16
+
17
+ logger = logging.getLogger()
18
+ logger.setLevel(logging.INFO)
19
+
20
+ CONTEXT_LENGTH = 128000
21
+ BUFFER = 10000
22
+ MAX_TOKENS_ALLOWED = CONTEXT_LENGTH - BUFFER
23
+
24
+ # Per-session state
25
+ SESSION_STORE: Dict[str, Dict[str, Any]] = {}
26
+
27
+ # Format error message for SSE
28
+ def format_error_sse(event_type: str, data: str) -> str:
29
+ lines = data.splitlines()
30
+ sse_message = f"event: {event_type}\n"
31
+ for line in lines:
32
+ sse_message += f"data: {line}\n"
33
+ sse_message += "\n"
34
+ return sse_message
35
+
36
+ # Initialize the components
37
+ def initialize_components():
38
+ load_dotenv(override=True)
39
+
40
+ from src.search.search_engine import SearchEngine
41
+ from src.query_processing.query_processor import QueryProcessor
42
+ from src.rag.neo4j_graphrag import Neo4jGraphRAG
43
+ from src.evaluation.evaluator import Evaluator
44
+ from src.reasoning.reasoner import Reasoner
45
+ from src.crawl.crawler import CustomCrawler
46
+ from src.utils.api_key_manager import APIKeyManager
47
+ from src.query_processing.late_chunking.late_chunker import LateChunker
48
+
49
+ manager = APIKeyManager()
50
+ manager._reinit()
51
+ SESSION_STORE['search_engine'] = SearchEngine()
52
+ SESSION_STORE['query_processor'] = QueryProcessor()
53
+ SESSION_STORE['crawler'] = CustomCrawler(max_concurrent_requests=1000)
54
+ SESSION_STORE['graph_rag'] = Neo4jGraphRAG(num_workers=os.cpu_count() * 2)
55
+ SESSION_STORE['evaluator'] = Evaluator()
56
+ SESSION_STORE['reasoner'] = Reasoner()
57
+ SESSION_STORE['model'] = manager.get_llm()
58
+ SESSION_STORE['late_chunker'] = LateChunker()
59
+ SESSION_STORE["initialized"] = True
60
+ SESSION_STORE["session_id"] = None
61
+
62
+ async def process_query(user_query: str, sse_queue: asyncio.Queue):
63
+ state = SESSION_STORE
64
+
65
+ try:
66
+ category = await state["query_processor"].classify_query(user_query)
67
+ cat_lower = category.lower().strip()
68
+
69
+ if state["session_id"] is None:
70
+ state["session_id"] = await state["crawler"].create_session()
71
+
72
+ user_query = re.sub(r'category:.*', '', user_query, flags=re.IGNORECASE).strip()
73
+
74
+ if cat_lower == "internal knowledge base":
75
+ response = ""
76
+ async for chunk in state["reasoner"].reason(user_query):
77
+ response += chunk
78
+ await sse_queue.put(("token", chunk))
79
+
80
+ await sse_queue.put(("final_message", response))
81
+ SESSION_STORE["chat_history"].append({"query": user_query, "response": response})
82
+
83
+ await sse_queue.put(("action", {
84
+ "name": "evaluate",
85
+ "payload": {"query": user_query, "response": response}
86
+ }))
87
+
88
+ await sse_queue.put(("complete", "done"))
89
+
90
+ elif cat_lower == "simple external lookup":
91
+ await sse_queue.put(("step", "Searching..."))
92
+
93
+ optimized_query = await state['search_engine'].generate_optimized_query(user_query)
94
+ search_results = await state['search_engine'].search(
95
+ optimized_query,
96
+ num_results=3,
97
+ exclude_filetypes=["pdf"]
98
+ )
99
+
100
+ urls = [r.get('link', 'No URL') for r in search_results]
101
+ search_contents = await state['crawler'].fetch_page_contents(
102
+ urls,
103
+ user_query,
104
+ state["session_id"],
105
+ max_attempts=1
106
+ )
107
+
108
+ contents = ""
109
+ if search_contents:
110
+ for k, content in enumerate(search_contents, 1):
111
+ if isinstance(content, Exception):
112
+ print(f"Error fetching content: {content}")
113
+ elif content:
114
+ contents += f"Document {k}:\n{content}\n\n"
115
+
116
+ if len(contents.strip()) > 0:
117
+ await sse_queue.put(("step", "Generating Response..."))
118
+
119
+ token_count = state['model'].get_num_tokens(contents)
120
+ if token_count > MAX_TOKENS_ALLOWED:
121
+ contents = await state['late_chunker'].chunker(contents, user_query, MAX_TOKENS_ALLOWED)
122
+
123
+ await sse_queue.put(("sources_read", len(search_contents)))
124
+
125
+ response = ""
126
+ async for chunk in state["reasoner"].reason(user_query, contents):
127
+ response += chunk
128
+ await sse_queue.put(("token", chunk))
129
+
130
+ await sse_queue.put(("final_message", response))
131
+ SESSION_STORE["chat_history"].append({"query": user_query, "response": response})
132
+
133
+ await sse_queue.put(("action", {
134
+ "name": "sources",
135
+ "payload": {"search_results": search_results, "search_contents": search_contents}
136
+ }))
137
+ await sse_queue.put(("action", {
138
+ "name": "evaluate",
139
+ "payload": {"query": user_query, "contents": [contents], "response": response}
140
+ }))
141
+
142
+ await sse_queue.put(("complete", "done"))
143
+ else:
144
+ await sse_queue.put(("error", "No results found."))
145
+
146
+ elif cat_lower == "complex moderate decomposition":
147
+ current_search_results = []
148
+ current_search_contents = []
149
+
150
+ await sse_queue.put(("step", "Thinking..."))
151
+
152
+ start = time.time()
153
+ intent = await state['query_processor'].get_query_intent(user_query)
154
+ sub_queries, _ = await state['query_processor'].decompose_query(user_query, intent)
155
+
156
+ async def sub_query_task(sub_query):
157
+ try:
158
+ await sse_queue.put(("step", "Searching..."))
159
+ await sse_queue.put(("task", (sub_query, "RUNNING")))
160
+
161
+ optimized_query = await state['search_engine'].generate_optimized_query(sub_query)
162
+ search_results = await state['search_engine'].search(
163
+ optimized_query,
164
+ num_results=10,
165
+ exclude_filetypes=["pdf"]
166
+ )
167
+ filtered_urls = await state['search_engine'].filter_urls(
168
+ sub_query,
169
+ category,
170
+ search_results
171
+ )
172
+ current_search_results.extend(filtered_urls)
173
+
174
+ urls = [r.get('link', 'No URL') for r in filtered_urls]
175
+ search_contents = await state['crawler'].fetch_page_contents(
176
+ urls,
177
+ sub_query,
178
+ state["session_id"],
179
+ max_attempts=1
180
+ )
181
+ current_search_contents.extend(search_contents)
182
+
183
+ contents = ""
184
+ if search_contents:
185
+ for k, c in enumerate(search_contents, 1):
186
+ if isinstance(c, Exception):
187
+ logger.info(f"Error fetching content: {c}")
188
+ elif c:
189
+ contents += f"Document {k}:\n{c}\n\n"
190
+
191
+ if len(contents.strip()) > 0:
192
+ await sse_queue.put(("task", (sub_query, "DONE")))
193
+ else:
194
+ await sse_queue.put(("task", (sub_query, "FAILED")))
195
+
196
+ return contents
197
+
198
+ except (RateLimitError, ResourceExhausted, AnthropicRateLimitError):
199
+ await sse_queue.put(("task", (sub_query, "FAILED")))
200
+ return ""
201
+
202
+ tasks = [sub_query_task(sub_query) for sub_query in sub_queries]
203
+ results = await asyncio.gather(*tasks)
204
+ end = time.time()
205
+
206
+ contents = "\n\n".join(r for r in results if r.strip())
207
+
208
+ unique_results = []
209
+ seen = set()
210
+ for entry in current_search_results:
211
+ link = entry["link"]
212
+ if link not in seen:
213
+ seen.add(link)
214
+ unique_results.append(entry)
215
+ current_search_results = unique_results
216
+ current_search_contents = list(set(current_search_contents))
217
+
218
+ if len(contents.strip()) > 0:
219
+ await sse_queue.put(("step", "Generating Response..."))
220
+
221
+ token_count = state['model'].get_num_tokens(contents)
222
+ if token_count > MAX_TOKENS_ALLOWED:
223
+ contents = await state['late_chunker'].chunker(
224
+ text=contents,
225
+ query=user_query,
226
+ max_tokens=MAX_TOKENS_ALLOWED
227
+ )
228
+ logger.info(f"Number of tokens in the answer: {token_count}")
229
+ logger.info(f"Number of tokens in the content: {state['model'].get_num_tokens(contents)}")
230
+
231
+ await sse_queue.put(("sources_read", len(current_search_contents)))
232
+
233
+ response = ""
234
+ is_first_chunk = True
235
+ async for chunk in state['reasoner'].reason(user_query, contents):
236
+ if is_first_chunk:
237
+ await sse_queue.put(("step", f"Thought and searched for {int(end - start)} seconds"))
238
+ is_first_chunk = False
239
+
240
+ response += chunk
241
+ await sse_queue.put(("token", chunk))
242
+
243
+ await sse_queue.put(("final_message", response))
244
+ SESSION_STORE["chat_history"].append({"query": user_query, "response": response})
245
+
246
+ await sse_queue.put(("action", {
247
+ "name": "sources",
248
+ "payload": {
249
+ "search_results": current_search_results,
250
+ "search_contents": current_search_contents
251
+ }
252
+ }))
253
+ await sse_queue.put(("action", {
254
+ "name": "evaluate",
255
+ "payload": {"query": user_query, "contents": [contents], "response": response}
256
+ }))
257
+
258
+ await sse_queue.put(("complete", "done"))
259
+ else:
260
+ await sse_queue.put(("error", "No results found."))
261
+
262
+ elif cat_lower == "complex advanced decomposition":
263
+ current_search_results = []
264
+ current_search_contents = []
265
+
266
+ await sse_queue.put(("step", "Thinking..."))
267
+
268
+ start = time.time()
269
+ main_query_intent = await state['query_processor'].get_query_intent(user_query)
270
+ sub_queries, _ = await state['query_processor'].decompose_query(user_query, main_query_intent)
271
+
272
+ await sse_queue.put(("step", "Searching..."))
273
+
274
+ async def sub_query_task(sub_query):
275
+ try:
276
+ async def sub_sub_query_task(sub_sub_query):
277
+ optimized_query = await state['search_engine'].generate_optimized_query(sub_sub_query)
278
+ search_results = await state['search_engine'].search(
279
+ optimized_query,
280
+ num_results=10,
281
+ exclude_filetypes=["pdf"]
282
+ )
283
+ filtered_urls = await state['search_engine'].filter_urls(
284
+ sub_sub_query,
285
+ category,
286
+ search_results
287
+ )
288
+ current_search_results.extend(filtered_urls)
289
+
290
+ urls = [r.get('link', 'No URL') for r in filtered_urls]
291
+ search_contents = await state['crawler'].fetch_page_contents(
292
+ urls,
293
+ sub_sub_query,
294
+ state["session_id"],
295
+ max_attempts=1,
296
+ timeout=20
297
+ )
298
+ current_search_contents.extend(search_contents)
299
+
300
+ contents = ""
301
+ if search_contents:
302
+ for k, c in enumerate(search_contents, 1):
303
+ if isinstance(c, Exception):
304
+ logger.info(f"Error fetching content: {c}")
305
+ elif c:
306
+ contents += f"Document {k}:\n{c}\n\n"
307
+
308
+ return contents
309
+
310
+ await sse_queue.put(("task", (sub_query, "RUNNING")))
311
+
312
+ sub_sub_queries, _ = await state['query_processor'].decompose_query(sub_query)
313
+
314
+ tasks = [sub_sub_query_task(sub_sub_query) for sub_sub_query in sub_sub_queries]
315
+ results = await asyncio.gather(*tasks)
316
+
317
+ if any(result.strip() for result in results):
318
+ await sse_queue.put(("task", (sub_query, "DONE")))
319
+ else:
320
+ await sse_queue.put(("task", (sub_query, "FAILED")))
321
+
322
+ return results
323
+
324
+ except (RateLimitError, ResourceExhausted, AnthropicRateLimitError):
325
+ await sse_queue.put(("task", (sub_query, "FAILED")))
326
+ return []
327
+
328
+ tasks = [sub_query_task(sub_query) for sub_query in sub_queries]
329
+ results = await asyncio.gather(*tasks)
330
+ end = time.time()
331
+
332
+ previous_contents = []
333
+ for result in results:
334
+ if result:
335
+ for content in result:
336
+ if isinstance(content, str) and len(content.strip()) > 0:
337
+ previous_contents.append(content)
338
+ contents = "\n\n".join(previous_contents)
339
+
340
+ unique_results = []
341
+ seen = set()
342
+ for entry in current_search_results:
343
+ link = entry["link"]
344
+ if link not in seen:
345
+ seen.add(link)
346
+ unique_results.append(entry)
347
+ current_search_results = unique_results
348
+ current_search_contents = list(set(current_search_contents))
349
+
350
+ if len(contents.strip()) > 0:
351
+ await sse_queue.put(("step", "Generating Response..."))
352
+
353
+ token_count = state['model'].get_num_tokens(contents)
354
+ if token_count > MAX_TOKENS_ALLOWED:
355
+ contents = await state['late_chunker'].chunker(
356
+ text=contents,
357
+ query=user_query,
358
+ max_tokens=MAX_TOKENS_ALLOWED
359
+ )
360
+ logger.info(f"Number of tokens in the answer: {token_count}")
361
+ logger.info(f"Number of tokens in the content: {state['model'].get_num_tokens(contents)}")
362
+
363
+ await sse_queue.put(("sources_read", len(current_search_contents)))
364
+
365
+ response = ""
366
+ is_first_chunk = True
367
+ async for chunk in state['reasoner'].reason(user_query, contents):
368
+ if is_first_chunk:
369
+ await sse_queue.put(("step", f"Thought and searched for {int(end - start)} seconds"))
370
+ is_first_chunk = False
371
+
372
+ response += chunk
373
+ await sse_queue.put(("token", chunk))
374
+
375
+ await sse_queue.put(("final_message", response))
376
+ SESSION_STORE["chat_history"].append({"query": user_query, "response": response})
377
+
378
+ await sse_queue.put(("action", {
379
+ "name": "sources",
380
+ "payload": {
381
+ "search_results": current_search_results,
382
+ "search_contents": current_search_contents
383
+ }
384
+ }))
385
+ await sse_queue.put(("action", {
386
+ "name": "evaluate",
387
+ "payload": {"query": user_query, "contents": [contents], "response": response}
388
+ }))
389
+
390
+ await sse_queue.put(("complete", "done"))
391
+ else:
392
+ await sse_queue.put(("error", "No results found."))
393
+
394
+ elif cat_lower == "extensive research dynamic structuring":
395
+ current_search_results = []
396
+ current_search_contents = []
397
+
398
+ match = re.search(
399
+ r"^This is the previous context of the conversation:\s*.*?\s*Current Query:\s*(.*)$",
400
+ user_query,
401
+ flags=re.DOTALL | re.MULTILINE
402
+ )
403
+ if match:
404
+ user_query = match.group(1)
405
+
406
+ await sse_queue.put(("step", "Thinking..."))
407
+ await asyncio.sleep(0.01) # Sleep for a short time to allow the message to be sent
408
+
409
+ async def on_event_callback(event_type, data):
410
+ if event_type == "graph_operation":
411
+ if data["operation_type"] == "creating_new_graph":
412
+ await sse_queue.put(("step", "Creating New Graph..."))
413
+
414
+ elif data["operation_type"] == "modifying_existing_graph":
415
+ await sse_queue.put(("step", "Modifying Existing Graph..."))
416
+
417
+ elif data["operation_type"] == "loading_existing_graph":
418
+ await sse_queue.put(("step", "Loading Existing Graph..."))
419
+
420
+ elif event_type == "sub_query_created":
421
+ sub_query = data["sub_query"]
422
+ await sse_queue.put(("task", (sub_query, "RUNNING")))
423
+
424
+ elif event_type == "search_process_started":
425
+ await sse_queue.put(("step", "Searching..."))
426
+
427
+ elif event_type == "sub_query_processed":
428
+ sub_query = data["sub_query"]
429
+ await sse_queue.put(("task", (sub_query, "DONE")))
430
+
431
+ elif event_type == "sub_query_failed":
432
+ sub_query = data["sub_query"]
433
+ await sse_queue.put(("task", (sub_query, "FAILED")))
434
+
435
+ elif event_type == "search_results_filtered":
436
+ current_search_results.extend(data["filtered_urls"])
437
+
438
+ filtered_urls = data["filtered_urls"]
439
+ current_search_results.extend(filtered_urls)
440
+
441
+ elif event_type == "search_contents_fetched":
442
+ current_search_contents.extend(data["contents"])
443
+
444
+ contents = data["contents"]
445
+ current_search_contents.extend(contents)
446
+
447
+ state['graph_rag'].set_on_event_callback(on_event_callback)
448
+
449
+ start = time.time()
450
+ state['graph_rag'].initialize_schema()
451
+ await state['graph_rag'].process_graph(
452
+ user_query,
453
+ similarity_threshold=0.8,
454
+ relevance_threshold=0.8,
455
+ max_tokens_allowed=MAX_TOKENS_ALLOWED
456
+ )
457
+ end = time.time()
458
+
459
+ unique_results = []
460
+ seen = set()
461
+ for entry in current_search_results:
462
+ link = entry["link"]
463
+ if link not in seen:
464
+ seen.add(link)
465
+ unique_results.append(entry)
466
+ current_search_results = unique_results
467
+ current_search_contents = list(set(current_search_contents))
468
+
469
+ await sse_queue.put(("step", "Generating Response..."))
470
+
471
+ answer = state['graph_rag'].query_graph(user_query)
472
+ if answer:
473
+ token_count = state['model'].get_num_tokens(answer)
474
+ if token_count > MAX_TOKENS_ALLOWED:
475
+ answer = await state['late_chunker'].chunker(
476
+ text=answer,
477
+ query=user_query,
478
+ max_tokens=MAX_TOKENS_ALLOWED
479
+ )
480
+ logger.info(f"Number of tokens in the answer: {token_count}")
481
+ logger.info(f"Number of tokens in the content: {state['model'].get_num_tokens(answer)}")
482
+
483
+ await sse_queue.put(("sources_read", len(current_search_contents)))
484
+
485
+ response = ""
486
+ is_first_chunk = True
487
+ async for chunk in state['reasoner'].reason(user_query, answer):
488
+ if is_first_chunk:
489
+ await sse_queue.put(("step", f"Thought and searched for {int(end - start)} seconds"))
490
+ is_first_chunk = False
491
+
492
+ response += chunk
493
+ await sse_queue.put(("token", chunk))
494
+
495
+ await sse_queue.put(("final_message", response))
496
+ SESSION_STORE["chat_history"].append({"query": user_query, "response": response})
497
+
498
+ await sse_queue.put(("action", {
499
+ "name": "sources",
500
+ "payload": {"search_results": current_search_results, "search_contents": current_search_contents},
501
+ }))
502
+ await sse_queue.put(("action", {
503
+ "name": "graph",
504
+ "payload": {"query": user_query},
505
+ }))
506
+ await sse_queue.put(("action", {
507
+ "name": "evaluate",
508
+ "payload": {"query": user_query, "contents": [answer], "response": response},
509
+ }))
510
+
511
+ await sse_queue.put(("complete", "done"))
512
+ else:
513
+ await sse_queue.put(("error", "No results found."))
514
+
515
+ else:
516
+ await sse_queue.put(("final_message", "I'm not sure how to handle your query."))
517
+
518
+ except Exception as e:
519
+ await sse_queue.put(("error", str(e)))
520
+
521
+ # Create a FastAPI app
522
+ app = FastAPI()
523
+
524
+ # Define allowed origins
525
+ origins = [
526
+ "http://localhost:3000",
527
+ "http://localhost:7860"
528
+ "http://localhost:8000",
529
+ "http://localhost"
530
+ ]
531
+
532
+ # Add the CORS middleware to your FastAPI app
533
+ app.add_middleware(
534
+ CORSMiddleware,
535
+ allow_origins=origins, # Allows only these origins
536
+ allow_credentials=True,
537
+ allow_methods=["*"], # Allows all HTTP methods (GET, POST, etc.)
538
+ allow_headers=["*"], # Allows all headers
539
+ )
540
+
541
+ # Serve the React app (the production build) at the root URL.
542
+ app.mount("/static", StaticFiles(directory="static/static", html=True), name="static")
543
+
544
+ # Catch-all route for frontend paths.
545
+ @app.get("/{full_path:path}")
546
+ async def serve_frontend(full_path: str, request: Request):
547
+ if full_path.startswith("action") or full_path in ["settings", "message-sse", "stop"]:
548
+ raise HTTPException(status_code=404, detail="Not Found")
549
+
550
+ index_path = os.path.join("static", "index.html")
551
+ if not os.path.exists(index_path):
552
+ raise HTTPException(status_code=500, detail="Frontend build not found")
553
+ return FileResponse(index_path)
554
+
555
+ # Define the routes for the FastAPI app
556
+
557
+ # Define the route for sources action to display search results
558
+ @app.post("/action/sources")
559
+ def action_sources(payload: Dict[str, Any]) -> Dict[str, Any]:
560
+ try:
561
+ search_contents = payload.get("search_contents", [])
562
+ search_results = payload.get("search_results", [])
563
+ sources = []
564
+ word_limit = 15 # Maximum number of words for the description
565
+
566
+ for result, contents in zip(search_results, search_contents):
567
+ if contents:
568
+ title = result.get('title', 'No Title')
569
+ link = result.get('link', 'No URL')
570
+ snippet = result.get('snippet', 'No snippet')
571
+ cleaned = re.sub(r'<[^>]+>|\[\/?.*?\]', '', snippet)
572
+
573
+ words = cleaned.split()
574
+ if len(words) > word_limit:
575
+ description = " ".join(words[:word_limit]) + "..."
576
+ else:
577
+ description = " ".join(words)
578
+
579
+ source_obj = {
580
+ "title": title,
581
+ "link": link,
582
+ "description": description
583
+ }
584
+ sources.append(source_obj)
585
+
586
+ return {"result": sources}
587
+ except Exception as e:
588
+ return JSONResponse(content={"error": str(e)}, status_code=500)
589
+
590
+ # Define the route for graph action to display the graph
591
+ @app.post("/action/graph")
592
+ def action_graph(payload: Dict[str, Any]) -> Dict[str, Any]:
593
+ state = SESSION_STORE
594
+
595
+ try:
596
+ q = payload.get("query", "")
597
+ html_str = state['graph_rag'].display_graph(q)
598
+
599
+ return {"result": html_str}
600
+ except Exception as e:
601
+ return JSONResponse(content={"error": str(e)}, status_code=500)
602
+
603
+ # Define the route for evaluate action to display evaluation results
604
+ @app.post("/action/evaluate")
605
+ async def action_evaluate(payload: Dict[str, Any]) -> Dict[str, Any]:
606
+ try:
607
+ query = payload.get("query", "")
608
+ contents = payload.get("contents", [])
609
+ response = payload.get("response", "")
610
+ metrics = payload.get("metrics", [])
611
+
612
+ state = SESSION_STORE
613
+ evaluator = state["evaluator"]
614
+ result = await evaluator.evaluate_response(query, response, contents, include_metrics=metrics)
615
+
616
+ return {"result": result}
617
+ except Exception as e:
618
+ return JSONResponse(content={"error": str(e)}, status_code=500)
619
+
620
+ @app.post("/settings")
621
+ async def update_settings(data: Dict[str, Any]):
622
+ from src.helpers.helper import (
623
+ prepare_provider_key_updates,
624
+ prepare_proxy_list_updates,
625
+ update_env_vars
626
+ )
627
+
628
+ provider = data.get("Model_Provider", "").strip()
629
+ model_name = data.get("Model_Name", "").strip()
630
+ multiple_api_keys = data.get("Model_API_Keys", "").strip()
631
+ brave_api_key = data.get("Brave_Search_API_Key", "").strip()
632
+ proxy_list = data.get("Proxy_List", "").strip()
633
+ neo4j_url = data.get("Neo4j_URL", "").strip()
634
+ neo4j_username = data.get("Neo4j_Username", "").strip()
635
+ neo4j_password = data.get("Neo4j_Password", "").strip()
636
+ model_temperature = str(data.get("Model_Temperature", 0.0))
637
+ model_top_p = str(data.get("Model_Top_P", 1.0))
638
+
639
+ prov_lower = provider.lower()
640
+ key_updates = prepare_provider_key_updates(prov_lower, multiple_api_keys)
641
+ env_updates = {}
642
+ env_updates.update(key_updates)
643
+ px = prepare_proxy_list_updates(proxy_list)
644
+
645
+ if px:
646
+ env_updates.update(px)
647
+
648
+ env_updates["BRAVE_API_KEY"] = brave_api_key
649
+ env_updates["NEO4J_URI"] = neo4j_url
650
+ env_updates["NEO4J_USER"] = neo4j_username
651
+ env_updates["NEO4J_PASSWORD"] = neo4j_password
652
+ env_updates["MODEL_PROVIDER"] = prov_lower
653
+ env_updates["MODEL_NAME"] = model_name
654
+ env_updates["MODEL_TEMPERATURE"] = model_temperature
655
+ env_updates["MODEL_TOP_P"] = model_top_p
656
+
657
+ update_env_vars(env_updates)
658
+ load_dotenv(override=True)
659
+ initialize_components()
660
+
661
+ return {"success": True}
662
+
663
+ @app.on_event("startup")
664
+ def init_chat():
665
+ if not SESSION_STORE:
666
+ print("Initializing chat...")
667
+
668
+ SESSION_STORE["settings_saved"] = False
669
+ SESSION_STORE["session_id"] = None
670
+ SESSION_STORE["chat_history"] = []
671
+
672
+ print("Chat initialized!")
673
+
674
+ return {"sucess": True}
675
+ else:
676
+ print("Chat already initialized!")
677
+ return {"success": False}
678
+
679
+ @app.get("/message-sse")
680
+ async def sse_message(request: Request, user_message: str):
681
+ state = SESSION_STORE
682
+ sse_queue = asyncio.Queue()
683
+
684
+ async def event_generator():
685
+ # Build the prompt
686
+ context = state["chat_history"][-5:]
687
+ if context:
688
+ prompt = \
689
+ f"""This is the previous context of the conversation:
690
+ {context}
691
+
692
+ Current Query:
693
+ {user_message}"""
694
+ else:
695
+ prompt = user_message
696
+
697
+ task = asyncio.create_task(process_query(prompt, sse_queue))
698
+ state["process_task"] = task
699
+
700
+ while True:
701
+ if await request.is_disconnected():
702
+ task.cancel()
703
+ break
704
+ try:
705
+ event_type, data = await asyncio.wait_for(sse_queue.get(), timeout=5)
706
+
707
+ if event_type == "token":
708
+ yield f"event: token\ndata: {data}\n\n"
709
+
710
+ elif event_type == "final_message":
711
+ yield f"event: final_message\ndata: {data}\n\n"
712
+
713
+ elif event_type == "error":
714
+ yield format_error_sse("error", data)
715
+
716
+ elif event_type == "step":
717
+ yield f"event: step\ndata: {data}\n\n"
718
+
719
+ elif event_type == "task":
720
+ subq, status = data
721
+ j = {"task": subq, "status": status}
722
+ yield f"event: task\ndata: {json.dumps(j)}\n\n"
723
+
724
+ elif event_type == "sources_read":
725
+ yield f"event: sources_read\ndata: {data}\n\n"
726
+
727
+ elif event_type == "action":
728
+ yield f"event: action\ndata: {json.dumps(data)}\n\n"
729
+
730
+ elif event_type == "complete":
731
+ yield f"event: complete\ndata: {data}\n\n"
732
+ break
733
+
734
+ else:
735
+ yield f"event: message\ndata: {data}\n\n"
736
+
737
+ except asyncio.TimeoutError:
738
+ if task.done():
739
+ break
740
+ continue
741
+
742
+ except asyncio.CancelledError:
743
+ break
744
+
745
+ if not task.done():
746
+ task.cancel()
747
+
748
+ if "process_task" in state:
749
+ del state["process_task"]
750
+
751
+ return StreamingResponse(event_generator(), media_type="text/event-stream")
752
+
753
+ @app.post("/stop")
754
+ def stop():
755
+ state = SESSION_STORE
756
+
757
+ if "process_task" in state:
758
+ state["process_task"].cancel()
759
+ del state["process_task"]
760
+
761
+ return {"message": "Stopped task manually"}
package-lock.json ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ {
2
+ "name": "ai_search_project",
3
+ "lockfileVersion": 3,
4
+ "requires": true,
5
+ "packages": {}
6
+ }
requirements.txt ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ fastapi[standard]
2
+ aiohttp==3.10.10
3
+ anthropic==0.42.0
4
+ beautifulsoup4==4.12.3
5
+ bert_score==0.3.13
6
+ crawl4ai[all]==0.3.731
7
+ deepeval==2.0
8
+ fake_useragent==1.5.1
9
+ fast_async==1.1.0
10
+ htmlrag==0.0.4
11
+ langchain==0.3.14
12
+ langchain_anthropic==0.3.1
13
+ langchain_community==0.3.14
14
+ langchain_core==0.3.29
15
+ langchain_google_genai==2.0.7
16
+ langchain_openai==0.2.14
17
+ langchain_xai==0.1.1
18
+ langgraph==0.2.62
19
+ model2vec==0.3.3
20
+ neo4j==5.26.0
21
+ openai==1.59.3
22
+ protobuf==4.23.4
23
+ PyPDF2==3.0.1
24
+ python-dotenv==1.0.1
25
+ pyvis==0.3.2
26
+ spacy==3.8.3
27
+ scikit_learn==1.6.0
28
+ sentence_transformers==3.1.1
29
+ tenacity==8.4.2
30
+ transformers==4.46.2
31
+ xformers==0.0.29.post1
32
+ # Intall the following seperately:-
33
+ # latest torch cuda version compatible with xformers' version
34
+ # python -m spacy download en_core_web_sm