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