Spaces:
Paused
Paused
Hemang Thakur
commited on
Commit
·
44ebcd1
1
Parent(s):
4aefbee
demo is ready
Browse files- frontend/package-lock.json +0 -0
- frontend/package.json +2 -1
- frontend/public/auth-receiver.html +107 -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/Streaming.css +161 -119
- frontend/src/Components/AiComponents/ChatComponents/Streaming.js +10 -64
- frontend/src/Components/AiComponents/ChatWindow.css +15 -5
- frontend/src/Components/AiComponents/ChatWindow.js +106 -3
- frontend/src/Components/AiComponents/Dropdowns/AddContentDropdown.css +150 -0
- frontend/src/Components/AiComponents/Dropdowns/AddContentDropdown.js +354 -0
- frontend/src/Components/AiComponents/Dropdowns/AddFilesDialog.css +191 -0
- frontend/src/Components/AiComponents/Dropdowns/AddFilesDialog.js +282 -0
- frontend/src/Components/AiComponents/Markdown/CustomMarkdown.js +489 -0
- frontend/src/Components/AiComponents/Markdown/TestMarkdown.js +120 -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 +66 -6
- frontend/src/Components/AiPage.js +611 -32
- frontend/src/Components/IntialSetting.css +1 -1
- frontend/src/Components/IntialSetting.js +24 -17
- frontend/src/Icons/excerpts.png +0 -0
- frontend/src/Icons/excerpts.pngZone.Identifier +4 -0
- main.py +467 -48
- src/crawl/crawler.py +566 -566
- src/helpers/helper.py +33 -1
- src/integrations/mcp_client.py +506 -0
- src/query_processing/query_processor.py +55 -16
- src/rag/graph_rag.py +12 -8
- src/rag/neo4j_graphrag.py +31 -5
- src/reasoning/reasoner.py +226 -19
- src/search/search_engine.py +5 -5
- src/utils/api_key_manager.py +24 -45
frontend/package-lock.json
CHANGED
The diff for this file is too large to render.
See raw diff
|
|
frontend/package.json
CHANGED
@@ -3,7 +3,7 @@
|
|
3 |
"version": "0.1.0",
|
4 |
"private": true,
|
5 |
"dependencies": {
|
6 |
-
|
7 |
"@emotion/styled": "^11.14.0",
|
8 |
"@fortawesome/fontawesome-free": "^6.7.2",
|
9 |
"@google/generative-ai": "^0.21.0",
|
@@ -23,6 +23,7 @@
|
|
23 |
"rehype-katex": "^6.0.2",
|
24 |
"rehype-raw": "^6.1.1",
|
25 |
"rehype-sanitize": "^5.0.1",
|
|
|
26 |
"remark-gfm": "^3.0.1",
|
27 |
"remark-math": "^5.1.1",
|
28 |
"styled-components": "^6.1.14",
|
|
|
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",
|
|
|
23 |
"rehype-katex": "^6.0.2",
|
24 |
"rehype-raw": "^6.1.1",
|
25 |
"rehype-sanitize": "^5.0.1",
|
26 |
+
"remark-breaks": "^4.0.0",
|
27 |
"remark-gfm": "^3.0.1",
|
28 |
"remark-math": "^5.1.1",
|
29 |
"styled-components": "^6.1.14",
|
frontend/public/auth-receiver.html
ADDED
@@ -0,0 +1,107 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
+
|
53 |
+
// For Google and Microsoft (token in hash)
|
54 |
+
if (window.location.hash) {
|
55 |
+
const hashParams = new URLSearchParams(window.location.hash.substring(1));
|
56 |
+
token = hashParams.get('access_token');
|
57 |
+
}
|
58 |
+
|
59 |
+
// For Slack (token might be in query params)
|
60 |
+
if (!token && window.location.search) {
|
61 |
+
const queryParams = new URLSearchParams(window.location.search);
|
62 |
+
token = queryParams.get('access_token');
|
63 |
+
|
64 |
+
// Slack might return a code instead of token
|
65 |
+
const code = queryParams.get('code');
|
66 |
+
if (code && !token) {
|
67 |
+
updateStatus('Slack requires additional setup. Please implement code exchange.');
|
68 |
+
setTimeout(() => {
|
69 |
+
if (window.opener) {
|
70 |
+
window.opener.postMessage({ type: 'auth-failed', error: 'Slack code exchange not implemented' }, '*');
|
71 |
+
window.close();
|
72 |
+
}
|
73 |
+
}, 3000);
|
74 |
+
}
|
75 |
+
}
|
76 |
+
|
77 |
+
if (token) {
|
78 |
+
updateStatus('Authentication successful! Closing window...');
|
79 |
+
|
80 |
+
// Send token back to parent window
|
81 |
+
if (window.opener) {
|
82 |
+
window.opener.postMessage({
|
83 |
+
type: 'auth-success',
|
84 |
+
token: token
|
85 |
+
}, window.location.origin);
|
86 |
+
|
87 |
+
// Close window after a short delay
|
88 |
+
setTimeout(() => window.close(), 1000);
|
89 |
+
} else {
|
90 |
+
updateStatus('Unable to communicate with main window. Please close this window manually.');
|
91 |
+
}
|
92 |
+
} else {
|
93 |
+
updateStatus('Authentication failed. No token received.');
|
94 |
+
setTimeout(() => {
|
95 |
+
if (window.opener) {
|
96 |
+
window.opener.postMessage({ type: 'auth-failed', error: 'No token received' }, '*');
|
97 |
+
window.close();
|
98 |
+
}
|
99 |
+
}, 3000);
|
100 |
+
}
|
101 |
+
} catch (error) {
|
102 |
+
updateStatus('An error occurred: ' + error.message);
|
103 |
+
console.error('Auth error:', error);
|
104 |
+
}
|
105 |
+
</script>
|
106 |
+
</body>
|
107 |
+
</html>
|
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/Streaming.css
CHANGED
@@ -1,120 +1,162 @@
|
|
1 |
.streaming-content {
|
2 |
-
|
3 |
-
|
4 |
-
|
5 |
-
|
6 |
-
|
7 |
-
|
8 |
-
|
9 |
-
|
10 |
-
|
11 |
-
|
12 |
-
|
13 |
-
|
14 |
-
|
15 |
-
|
16 |
-
|
17 |
-
|
18 |
-
|
19 |
-
|
20 |
-
|
21 |
-
|
22 |
-
|
23 |
-
|
24 |
-
|
25 |
-
|
26 |
-
|
27 |
-
|
28 |
-
|
29 |
-
|
30 |
-
|
31 |
-
|
32 |
-
|
33 |
-
|
34 |
-
|
35 |
-
|
36 |
-
|
37 |
-
|
38 |
-
|
39 |
-
|
40 |
-
|
41 |
-
|
42 |
-
|
43 |
-
|
44 |
-
|
45 |
-
|
46 |
-
|
47 |
-
|
48 |
-
|
49 |
-
|
50 |
-
|
51 |
-
|
52 |
-
|
53 |
-
|
54 |
-
|
55 |
-
|
56 |
-
|
57 |
-
|
58 |
-
|
59 |
-
|
60 |
-
|
61 |
-
|
62 |
-
|
63 |
-
|
64 |
-
|
65 |
-
|
66 |
-
|
67 |
-
|
68 |
-
|
69 |
-
|
70 |
-
|
71 |
-
|
72 |
-
|
73 |
-
|
74 |
-
|
75 |
-
|
76 |
-
|
77 |
-
|
78 |
-
|
79 |
-
|
80 |
-
|
81 |
-
|
82 |
-
|
83 |
-
|
84 |
-
|
85 |
-
|
86 |
-
|
87 |
-
|
88 |
-
|
89 |
-
|
90 |
-
|
91 |
-
|
92 |
-
|
93 |
-
|
94 |
-
|
95 |
-
|
96 |
-
|
97 |
-
|
98 |
-
|
99 |
-
|
100 |
-
|
101 |
-
|
102 |
-
|
103 |
-
|
104 |
-
|
105 |
-
|
106 |
-
|
107 |
-
|
108 |
-
|
109 |
-
|
110 |
-
|
111 |
-
|
112 |
-
|
113 |
-
|
114 |
-
|
115 |
-
|
116 |
-
|
117 |
-
|
118 |
-
|
119 |
-
|
120 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
.streaming-content {
|
2 |
+
font-family: inherit;
|
3 |
+
line-height: 2rem;
|
4 |
+
white-space: pre-wrap;
|
5 |
+
word-wrap: break-word;
|
6 |
+
margin: 0;
|
7 |
+
padding: 0;
|
8 |
+
}
|
9 |
+
|
10 |
+
/* Reset margin/padding for all descendants */
|
11 |
+
.streaming-content * {
|
12 |
+
margin: 0;
|
13 |
+
padding: 0;
|
14 |
+
}
|
15 |
+
|
16 |
+
/* Top-level elements */
|
17 |
+
.streaming-content > * {
|
18 |
+
margin-top: 0.5rem;
|
19 |
+
margin-bottom: 0.5rem;
|
20 |
+
}
|
21 |
+
|
22 |
+
/* VERY FIRST element in an AI answer */
|
23 |
+
.streaming-content > *:first-child {
|
24 |
+
margin-top: 0 !important;
|
25 |
+
}
|
26 |
+
|
27 |
+
/* Headings */
|
28 |
+
.streaming-content h1,
|
29 |
+
.streaming-content h2,
|
30 |
+
.streaming-content h3,
|
31 |
+
.streaming-content h4,
|
32 |
+
.streaming-content h5,
|
33 |
+
.streaming-content h6 {
|
34 |
+
margin-top: 1rem;
|
35 |
+
margin-bottom: 0.75rem;
|
36 |
+
}
|
37 |
+
|
38 |
+
/* If heading is the very first element */
|
39 |
+
.streaming-content > h1:first-child,
|
40 |
+
.streaming-content > h2:first-child,
|
41 |
+
.streaming-content > h3:first-child,
|
42 |
+
.streaming-content > h4:first-child,
|
43 |
+
.streaming-content > h5:first-child,
|
44 |
+
.streaming-content > h6:first-child {
|
45 |
+
margin-top: 0 !important;
|
46 |
+
}
|
47 |
+
|
48 |
+
/* All but the first child in streaming-content */
|
49 |
+
.streaming-content p:not(:first-child),
|
50 |
+
.streaming-content h1:not(:first-child),
|
51 |
+
.streaming-content h2:not(:first-child),
|
52 |
+
.streaming-content h3:not(:first-child),
|
53 |
+
.streaming-content h4:not(:first-child),
|
54 |
+
.streaming-content h5:not(:first-child),
|
55 |
+
.streaming-content h6:not(:first-child) {
|
56 |
+
margin-top: -0.5em !important;
|
57 |
+
}
|
58 |
+
|
59 |
+
.streaming-content h1:not(:first-child),
|
60 |
+
.streaming-content h2:not(:first-child),
|
61 |
+
.streaming-content h3:not(:first-child),
|
62 |
+
.streaming-content h4:not(:first-child),
|
63 |
+
.streaming-content h5:not(:first-child),
|
64 |
+
.streaming-content h6:not(:first-child) {
|
65 |
+
margin-bottom: -0.35em !important;
|
66 |
+
}
|
67 |
+
|
68 |
+
/* When a list follows a paragraph */
|
69 |
+
.streaming-content p + ul,
|
70 |
+
.streaming-content p + ol {
|
71 |
+
margin-top: -2rem !important;
|
72 |
+
}
|
73 |
+
|
74 |
+
/* When a list follows a header */
|
75 |
+
.streaming-content h1 + ul,
|
76 |
+
.streaming-content h2 + ul,
|
77 |
+
.streaming-content h3 + ul,
|
78 |
+
.streaming-content h4 + ul,
|
79 |
+
.streaming-content h5 + ul,
|
80 |
+
.streaming-content h6 + ul,
|
81 |
+
.streaming-content h1 + ol,
|
82 |
+
.streaming-content h2 + ol,
|
83 |
+
.streaming-content h3 + ol,
|
84 |
+
.streaming-content h4 + ol,
|
85 |
+
.streaming-content h5 + ol,
|
86 |
+
.streaming-content h6 + ol {
|
87 |
+
margin-top: -0.25rem !important;
|
88 |
+
}
|
89 |
+
|
90 |
+
/* Paragraphs */
|
91 |
+
.streaming-content p {
|
92 |
+
margin-top: 0.25rem;
|
93 |
+
margin-bottom: 0.25rem;
|
94 |
+
}
|
95 |
+
|
96 |
+
/* Lists */
|
97 |
+
.streaming-content ul,
|
98 |
+
.streaming-content ol {
|
99 |
+
margin-top: 0.25rem;
|
100 |
+
margin-bottom: 0.25rem;
|
101 |
+
padding-left: 1.25rem;
|
102 |
+
white-space: normal;
|
103 |
+
}
|
104 |
+
|
105 |
+
.streaming-content li {
|
106 |
+
margin-bottom: 0.25rem;
|
107 |
+
}
|
108 |
+
|
109 |
+
.streaming-content li ul,
|
110 |
+
.streaming-content li ol {
|
111 |
+
margin-top: 0.15rem;
|
112 |
+
margin-bottom: 0.15rem;
|
113 |
+
}
|
114 |
+
|
115 |
+
/* Code Blocks */
|
116 |
+
.code-block-container {
|
117 |
+
margin: 0.5rem 0;
|
118 |
+
border-radius: 4px;
|
119 |
+
background-color: #2b2b2b;
|
120 |
+
overflow: hidden;
|
121 |
+
}
|
122 |
+
|
123 |
+
.code-block-header {
|
124 |
+
background-color: #1e1e1e;
|
125 |
+
color: #ffffff;
|
126 |
+
padding: 0.5rem;
|
127 |
+
font-size: 0.85rem;
|
128 |
+
font-weight: bold;
|
129 |
+
}
|
130 |
+
|
131 |
+
/* Table Container */
|
132 |
+
.table-container {
|
133 |
+
margin: 0.5rem 0;
|
134 |
+
width: 100%;
|
135 |
+
overflow-x: auto;
|
136 |
+
border: 1px solid #ddd;
|
137 |
+
border-radius: 4px;
|
138 |
+
}
|
139 |
+
|
140 |
+
.table-container th,
|
141 |
+
.table-container td {
|
142 |
+
border: 1px solid #ddd;
|
143 |
+
padding: 0.5rem;
|
144 |
+
}
|
145 |
+
|
146 |
+
/* Markdown Links */
|
147 |
+
.markdown-link {
|
148 |
+
color: #1a73e8;
|
149 |
+
text-decoration: none;
|
150 |
+
}
|
151 |
+
.markdown-link:hover {
|
152 |
+
text-decoration: underline;
|
153 |
+
}
|
154 |
+
|
155 |
+
/* Blockquotes */
|
156 |
+
.markdown-blockquote {
|
157 |
+
border-left: 4px solid #ccc;
|
158 |
+
padding-left: 0.75rem;
|
159 |
+
margin: 0.5rem 0;
|
160 |
+
color: #555;
|
161 |
+
font-style: italic;
|
162 |
+
}
|
frontend/src/Components/AiComponents/ChatComponents/Streaming.js
CHANGED
@@ -1,80 +1,26 @@
|
|
1 |
import React, { useEffect, useRef } from 'react';
|
2 |
-
import
|
3 |
-
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
|
4 |
-
import { atomDark } from 'react-syntax-highlighter/dist/esm/styles/prism';
|
5 |
-
import remarkGfm from 'remark-gfm';
|
6 |
-
import rehypeRaw from 'rehype-raw';
|
7 |
import './Streaming.css';
|
|
|
8 |
|
9 |
// Streaming component for rendering markdown content
|
10 |
-
const Streaming = ({ content, isStreaming, onContentRef }) => {
|
11 |
const contentRef = useRef(null);
|
12 |
|
13 |
useEffect(() => {
|
14 |
if (contentRef.current && onContentRef) {
|
15 |
onContentRef(contentRef.current);
|
16 |
}
|
17 |
-
}, [content, onContentRef]);
|
18 |
-
|
19 |
-
const displayContent = isStreaming ? `${content}▌` : (content || '');
|
20 |
|
21 |
return (
|
22 |
<div className="streaming-content" ref={contentRef}>
|
23 |
-
<
|
24 |
-
|
25 |
-
|
26 |
-
|
27 |
-
|
28 |
-
|
29 |
-
return !inline ? (
|
30 |
-
<div className="code-block-container">
|
31 |
-
<div className="code-block-header">
|
32 |
-
<span>{match ? match[1] : 'code'}</span>
|
33 |
-
</div>
|
34 |
-
<SyntaxHighlighter
|
35 |
-
style={atomDark}
|
36 |
-
language={match ? match[1] : 'text'}
|
37 |
-
PreTag="div"
|
38 |
-
{...props}
|
39 |
-
>
|
40 |
-
{String(children).replace(/\n$/, '')}
|
41 |
-
</SyntaxHighlighter>
|
42 |
-
</div>
|
43 |
-
) : (
|
44 |
-
<code className={className} {...props}>
|
45 |
-
{children}
|
46 |
-
</code>
|
47 |
-
);
|
48 |
-
},
|
49 |
-
table({node, ...props}) {
|
50 |
-
return (
|
51 |
-
<div className="table-container">
|
52 |
-
<table {...props} />
|
53 |
-
</div>
|
54 |
-
);
|
55 |
-
},
|
56 |
-
a({node, children, href, ...props}) {
|
57 |
-
return (
|
58 |
-
<a
|
59 |
-
href={href}
|
60 |
-
target="_blank"
|
61 |
-
rel="noopener noreferrer"
|
62 |
-
className="markdown-link"
|
63 |
-
{...props}
|
64 |
-
>
|
65 |
-
{children}
|
66 |
-
</a>
|
67 |
-
);
|
68 |
-
},
|
69 |
-
blockquote({node, ...props}) {
|
70 |
-
return (
|
71 |
-
<blockquote className="markdown-blockquote" {...props} />
|
72 |
-
);
|
73 |
-
}
|
74 |
-
}}
|
75 |
-
>
|
76 |
-
{displayContent}
|
77 |
-
</ReactMarkdown>
|
78 |
</div>
|
79 |
);
|
80 |
};
|
|
|
1 |
import React, { useEffect, useRef } from 'react';
|
2 |
+
import CustomMarkdown from '../Markdown/CustomMarkdown';
|
|
|
|
|
|
|
|
|
3 |
import './Streaming.css';
|
4 |
+
import './SourceRef.css';
|
5 |
|
6 |
// Streaming component for rendering markdown content
|
7 |
+
const Streaming = ({ content, isStreaming, onContentRef, showSourcePopup, hideSourcePopup }) => {
|
8 |
const contentRef = useRef(null);
|
9 |
|
10 |
useEffect(() => {
|
11 |
if (contentRef.current && onContentRef) {
|
12 |
onContentRef(contentRef.current);
|
13 |
}
|
14 |
+
}, [content, onContentRef]);
|
|
|
|
|
15 |
|
16 |
return (
|
17 |
<div className="streaming-content" ref={contentRef}>
|
18 |
+
<CustomMarkdown
|
19 |
+
content={content}
|
20 |
+
isStreaming={isStreaming}
|
21 |
+
showSourcePopup={showSourcePopup}
|
22 |
+
hideSourcePopup={hideSourcePopup}
|
23 |
+
/>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
24 |
</div>
|
25 |
);
|
26 |
};
|
frontend/src/Components/AiComponents/ChatWindow.css
CHANGED
@@ -136,7 +136,8 @@
|
|
136 |
.post-icons .copy-icon,
|
137 |
.post-icons .evaluate-icon,
|
138 |
.post-icons .sources-icon,
|
139 |
-
.post-icons .graph-icon
|
|
|
140 |
cursor: pointer;
|
141 |
position: relative;
|
142 |
}
|
@@ -145,14 +146,16 @@
|
|
145 |
.post-icons .copy-icon img,
|
146 |
.post-icons .evaluate-icon img,
|
147 |
.post-icons .sources-icon img,
|
148 |
-
.post-icons .graph-icon img
|
|
|
149 |
transition: filter var(--transition-speed);
|
150 |
}
|
151 |
|
152 |
.post-icons .copy-icon:hover img,
|
153 |
.post-icons .evaluate-icon:hover img,
|
154 |
.post-icons .sources-icon:hover img,
|
155 |
-
.post-icons .graph-icon:hover img
|
|
|
156 |
filter: brightness(0.65);
|
157 |
}
|
158 |
|
@@ -179,7 +182,8 @@
|
|
179 |
.post-icons .copy-icon:hover .tooltip,
|
180 |
.post-icons .evaluate-icon:hover .tooltip,
|
181 |
.post-icons .sources-icon:hover .tooltip,
|
182 |
-
.post-icons .graph-icon:hover .tooltip
|
|
|
183 |
opacity: 1;
|
184 |
visibility: visible;
|
185 |
transform: translateX(-50%) translateY(0) scale(1);
|
@@ -207,7 +211,13 @@
|
|
207 |
object-fit: contain;
|
208 |
}
|
209 |
|
210 |
-
/*
|
|
|
|
|
|
|
|
|
|
|
|
|
211 |
|
212 |
/* Container for the loading state with a dark background */
|
213 |
.bot-loading {
|
|
|
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 |
}
|
|
|
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 |
|
|
|
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);
|
|
|
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 {
|
frontend/src/Components/AiComponents/ChatWindow.js
CHANGED
@@ -1,11 +1,12 @@
|
|
1 |
-
import React, { useRef, useState, useEffect } from 'react';
|
2 |
import Box from '@mui/material/Box';
|
3 |
import Snackbar from '@mui/material/Snackbar';
|
4 |
import Slide from '@mui/material/Slide';
|
5 |
import IconButton from '@mui/material/IconButton';
|
6 |
-
import { FaTimes } from 'react-icons/fa';
|
7 |
import GraphDialog from './ChatComponents/Graph';
|
8 |
import Streaming from './ChatComponents/Streaming';
|
|
|
9 |
import './ChatWindow.css';
|
10 |
|
11 |
import bot from '../../Icons/bot.png';
|
@@ -14,6 +15,7 @@ import evaluate from '../../Icons/evaluate.png';
|
|
14 |
import sourcesIcon from '../../Icons/sources.png';
|
15 |
import graphIcon from '../../Icons/graph.png';
|
16 |
import user from '../../Icons/user.png';
|
|
|
17 |
|
18 |
// SlideTransition function for both entry and exit transitions.
|
19 |
function SlideTransition(props) {
|
@@ -28,6 +30,10 @@ function ChatWindow({
|
|
28 |
thinkingTime,
|
29 |
thoughtLabel,
|
30 |
sourcesRead,
|
|
|
|
|
|
|
|
|
31 |
actions,
|
32 |
tasks,
|
33 |
openRightSidebar,
|
@@ -35,9 +41,12 @@ function ChatWindow({
|
|
35 |
isError,
|
36 |
errorMessage
|
37 |
}) {
|
|
|
38 |
const answerRef = useRef(null);
|
39 |
const [graphDialogOpen, setGraphDialogOpen] = useState(false);
|
40 |
const [snackbarOpen, setSnackbarOpen] = useState(false);
|
|
|
|
|
41 |
|
42 |
// Get the graph action from the actions prop.
|
43 |
const graphAction = actions && actions.find(a => a.name === "graph");
|
@@ -101,6 +110,69 @@ function ChatWindow({
|
|
101 |
answerRef.current = ref;
|
102 |
};
|
103 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
104 |
return (
|
105 |
<>
|
106 |
{ !hasTokens ? (
|
@@ -186,10 +258,12 @@ function ChatWindow({
|
|
186 |
</div>
|
187 |
<div className="message-bubble bot-bubble">
|
188 |
<div className="answer">
|
189 |
-
<Streaming
|
190 |
content={combinedAnswer}
|
191 |
isStreaming={isStreaming}
|
192 |
onContentRef={handleContentRef}
|
|
|
|
|
193 |
/>
|
194 |
</div>
|
195 |
</div>
|
@@ -218,6 +292,24 @@ function ChatWindow({
|
|
218 |
<span className="tooltip">View Graph</span>
|
219 |
</div>
|
220 |
)}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
221 |
</div>
|
222 |
</div>
|
223 |
</div>
|
@@ -232,6 +324,17 @@ function ChatWindow({
|
|
232 |
)}
|
233 |
</div>
|
234 |
)}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
235 |
{/* Render error container if there's an error */}
|
236 |
{isError && (
|
237 |
<div className="error-block" style={{ marginTop: '1rem' }}>
|
|
|
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';
|
|
|
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) {
|
|
|
30 |
thinkingTime,
|
31 |
thoughtLabel,
|
32 |
sourcesRead,
|
33 |
+
finalSources,
|
34 |
+
excerptsData,
|
35 |
+
isLoadingExcerpts,
|
36 |
+
onFetchExcerpts,
|
37 |
actions,
|
38 |
tasks,
|
39 |
openRightSidebar,
|
|
|
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");
|
|
|
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 ? (
|
|
|
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>
|
|
|
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>
|
|
|
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' }}>
|
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,354 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
+
</div>
|
345 |
+
</li>
|
346 |
+
</ul>
|
347 |
+
</div>
|
348 |
+
</li>
|
349 |
+
</ul>
|
350 |
+
</div>
|
351 |
+
);
|
352 |
+
}
|
353 |
+
|
354 |
+
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/Markdown/CustomMarkdown.js
ADDED
@@ -0,0 +1,489 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import React, { useEffect, useState, useCallback, useMemo } from 'react';
|
2 |
+
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
|
3 |
+
import { atomDark } from 'react-syntax-highlighter/dist/esm/styles/prism';
|
4 |
+
import '../ChatComponents/Streaming.css';
|
5 |
+
import '../ChatComponents/SourceRef.css';
|
6 |
+
|
7 |
+
// Complete custom markdown parser and renderer that respects Streaming.css
|
8 |
+
const CustomMarkdown = ({ content, isStreaming, showSourcePopup, hideSourcePopup }) => {
|
9 |
+
const [parsedContent, setParsedContent] = useState([]);
|
10 |
+
|
11 |
+
// Display content with cursor if streaming
|
12 |
+
const displayContent = isStreaming ? `${content}▌` : (content || '');
|
13 |
+
|
14 |
+
// Normalize citations like [1,2] to [1][2]
|
15 |
+
const normalizeCitations = useCallback((text) => {
|
16 |
+
if (!text) return '';
|
17 |
+
const citationRegex = /\[(\d+(?:,\s*\d+)+)\]/g;
|
18 |
+
return text.replace(citationRegex, (match, capturedNumbers) => {
|
19 |
+
const numbers = capturedNumbers
|
20 |
+
.split(/,\s*/)
|
21 |
+
.map(numStr => numStr.trim())
|
22 |
+
.filter(Boolean);
|
23 |
+
if (numbers.length <= 1) return match;
|
24 |
+
return numbers.map(num => `[${num}]`).join('');
|
25 |
+
});
|
26 |
+
}, []);
|
27 |
+
|
28 |
+
const normalizedContent = useMemo(() => normalizeCitations(displayContent), [displayContent, normalizeCitations]);
|
29 |
+
|
30 |
+
// Citation component
|
31 |
+
const Citation = ({ number, showSourcePopup, hideSourcePopup, text }) => {
|
32 |
+
const getSentenceForCitation = () => {
|
33 |
+
const sentenceRegex = /[^.!?\n]+[.!?]+[\])'"`'"]*|[^.!?\n]+$/g;
|
34 |
+
const sentences = text.match(sentenceRegex) || [text];
|
35 |
+
|
36 |
+
for (const sentence of sentences) {
|
37 |
+
if (sentence.includes(`[${number}]`)) {
|
38 |
+
return sentence.trim();
|
39 |
+
}
|
40 |
+
}
|
41 |
+
return '';
|
42 |
+
};
|
43 |
+
|
44 |
+
return (
|
45 |
+
<sup
|
46 |
+
className="source-reference"
|
47 |
+
onMouseEnter={(e) => {
|
48 |
+
if (showSourcePopup) {
|
49 |
+
const sentence = getSentenceForCitation();
|
50 |
+
showSourcePopup(number - 1, e.target, sentence);
|
51 |
+
}
|
52 |
+
}}
|
53 |
+
onMouseLeave={hideSourcePopup}
|
54 |
+
>
|
55 |
+
{number}
|
56 |
+
</sup>
|
57 |
+
);
|
58 |
+
};
|
59 |
+
|
60 |
+
// Parse inline markdown elements
|
61 |
+
const parseInline = useCallback((text) => {
|
62 |
+
if (!text) return null;
|
63 |
+
|
64 |
+
// Define regex patterns clearly
|
65 |
+
const TWO_SPACES = ' '; // Exactly 2 spaces (not 1, not 3, but 2!)
|
66 |
+
const lineBreakRegex = new RegExp(TWO_SPACES + '\\n', 'g');
|
67 |
+
|
68 |
+
const elements = [];
|
69 |
+
const patterns = [
|
70 |
+
// Bold + Italic
|
71 |
+
{ regex: /\*\*\*(.+?)\*\*\*/g, handler: (m) => <strong key={m.index}><em>{parseInline(m[1])}</em></strong> },
|
72 |
+
{ regex: /___(.+?)___/g, handler: (m) => <strong key={m.index}><em>{parseInline(m[1])}</em></strong> },
|
73 |
+
// Bold
|
74 |
+
{ regex: /\*\*(.+?)\*\*/g, handler: (m) => <strong key={m.index}>{parseInline(m[1])}</strong> },
|
75 |
+
{ regex: /__(.+?)__/g, handler: (m) => <strong key={m.index}>{parseInline(m[1])}</strong> },
|
76 |
+
// Italic
|
77 |
+
{ regex: /\*([^*]+)\*/g, handler: (m) => <em key={m.index}>{parseInline(m[1])}</em> },
|
78 |
+
{ regex: /_([^_]+)_/g, handler: (m) => <em key={m.index}>{parseInline(m[1])}</em> },
|
79 |
+
// Strikethrough
|
80 |
+
{ regex: /~~(.+?)~~/g, handler: (m) => <del key={m.index}>{parseInline(m[1])}</del> },
|
81 |
+
// Inline code (preserve all spaces)
|
82 |
+
{ regex: /`([^`]+)`/g, handler: (m) => {
|
83 |
+
// Preserve all whitespace in inline code
|
84 |
+
const codeContent = m[1].replace(/ /g, '\u00A0'); // Replace spaces with non-breaking spaces
|
85 |
+
return <code key={m.index}>{codeContent}</code>;
|
86 |
+
}},
|
87 |
+
// Images
|
88 |
+
{ regex: /!\[([^\]]*)\]\(([^)]+)\)/g, handler: (m) => <img key={m.index} src={m[2]} alt={m[1]} style={{ maxWidth: '100%' }} /> },
|
89 |
+
// Links
|
90 |
+
{ regex: /\[([^\]]+)\]\(([^)]+)\)/g, handler: (m) => (
|
91 |
+
<a key={m.index} href={m[2]} target="_blank" rel="noopener noreferrer" className="markdown-link">
|
92 |
+
{parseInline(m[1])}
|
93 |
+
</a>
|
94 |
+
)},
|
95 |
+
// Citations
|
96 |
+
{ regex: /\[(\d+)\]/g, handler: (m) => (
|
97 |
+
<Citation
|
98 |
+
key={m.index}
|
99 |
+
number={parseInt(m[1], 10)}
|
100 |
+
showSourcePopup={showSourcePopup}
|
101 |
+
hideSourcePopup={hideSourcePopup}
|
102 |
+
text={text}
|
103 |
+
/>
|
104 |
+
)},
|
105 |
+
// Line breaks
|
106 |
+
{
|
107 |
+
regex: lineBreakRegex,
|
108 |
+
handler: (m) => <br key={m.index} />
|
109 |
+
},
|
110 |
+
];
|
111 |
+
|
112 |
+
// Apply patterns in order
|
113 |
+
let processedText = text;
|
114 |
+
const replacements = [];
|
115 |
+
|
116 |
+
for (const pattern of patterns) {
|
117 |
+
let match;
|
118 |
+
pattern.regex.lastIndex = 0;
|
119 |
+
while ((match = pattern.regex.exec(text))) {
|
120 |
+
replacements.push({
|
121 |
+
start: match.index,
|
122 |
+
end: match.index + match[0].length,
|
123 |
+
element: pattern.handler(match),
|
124 |
+
priority: patterns.indexOf(pattern)
|
125 |
+
});
|
126 |
+
}
|
127 |
+
}
|
128 |
+
|
129 |
+
// Sort replacements by position and priority
|
130 |
+
replacements.sort((a, b) => {
|
131 |
+
if (a.start !== b.start) return a.start - b.start;
|
132 |
+
return a.priority - b.priority;
|
133 |
+
});
|
134 |
+
|
135 |
+
// Build result without overlapping replacements
|
136 |
+
let lastEnd = 0;
|
137 |
+
const used = new Set();
|
138 |
+
|
139 |
+
for (const replacement of replacements) {
|
140 |
+
// Skip if this overlaps with an already used replacement
|
141 |
+
let overlaps = false;
|
142 |
+
for (const usedRange of used) {
|
143 |
+
if (!(replacement.end <= usedRange.start || replacement.start >= usedRange.end)) {
|
144 |
+
overlaps = true;
|
145 |
+
break;
|
146 |
+
}
|
147 |
+
}
|
148 |
+
|
149 |
+
if (!overlaps) {
|
150 |
+
if (replacement.start > lastEnd) {
|
151 |
+
// Preserve spaces in text segments
|
152 |
+
const textSegment = processedText.substring(lastEnd, replacement.start);
|
153 |
+
elements.push(textSegment);
|
154 |
+
}
|
155 |
+
elements.push(replacement.element);
|
156 |
+
lastEnd = replacement.end;
|
157 |
+
used.add({ start: replacement.start, end: replacement.end });
|
158 |
+
}
|
159 |
+
}
|
160 |
+
|
161 |
+
if (lastEnd < processedText.length) {
|
162 |
+
// Preserve spaces in remaining text
|
163 |
+
elements.push(processedText.substring(lastEnd));
|
164 |
+
}
|
165 |
+
|
166 |
+
return elements.length > 0 ? elements : text;
|
167 |
+
}, [showSourcePopup, hideSourcePopup]);
|
168 |
+
|
169 |
+
// Parse code blocks separately to handle them properly
|
170 |
+
const extractCodeBlocks = useCallback((text) => {
|
171 |
+
const codeBlocks = [];
|
172 |
+
const placeholder = '___CODE_BLOCK_';
|
173 |
+
let counter = 0;
|
174 |
+
|
175 |
+
// Replace code blocks with placeholders, preserve exact formatting
|
176 |
+
const textWithoutCode = text.replace(/```(\w*)\n([\s\S]*?)```/g, (match, lang, code) => {
|
177 |
+
const id = `${placeholder}${counter}___`;
|
178 |
+
// Preserve exact code content without any trimming
|
179 |
+
codeBlocks.push({ id, lang: lang || 'text', code: code.replace(/\n$/, '') }); // Only remove final newline
|
180 |
+
counter++;
|
181 |
+
return `\n${id}\n`;
|
182 |
+
});
|
183 |
+
|
184 |
+
return { textWithoutCode, codeBlocks };
|
185 |
+
}, []);
|
186 |
+
|
187 |
+
// Parse block-level elements
|
188 |
+
const parseBlocks = useCallback((text, codeBlocks = []) => {
|
189 |
+
if (!text) return [];
|
190 |
+
|
191 |
+
const blocks = [];
|
192 |
+
const lines = text.split('\n');
|
193 |
+
let i = 0;
|
194 |
+
|
195 |
+
while (i < lines.length) {
|
196 |
+
const line = lines[i];
|
197 |
+
const trimmedLine = line.trim();
|
198 |
+
|
199 |
+
// Skip empty lines
|
200 |
+
if (!trimmedLine) {
|
201 |
+
i++;
|
202 |
+
continue;
|
203 |
+
}
|
204 |
+
|
205 |
+
// Check for code block placeholder
|
206 |
+
const codeBlockMatch = line.match(/___CODE_BLOCK_(\d+)___/);
|
207 |
+
if (codeBlockMatch) {
|
208 |
+
const codeBlock = codeBlocks.find(cb => cb.id === line.trim());
|
209 |
+
if (codeBlock) {
|
210 |
+
blocks.push({
|
211 |
+
type: 'code',
|
212 |
+
lang: codeBlock.lang,
|
213 |
+
content: codeBlock.code
|
214 |
+
});
|
215 |
+
i++;
|
216 |
+
continue;
|
217 |
+
}
|
218 |
+
}
|
219 |
+
|
220 |
+
// Horizontal rule
|
221 |
+
if (/^[-*_]{3,}$/.test(trimmedLine)) {
|
222 |
+
blocks.push({ type: 'hr' });
|
223 |
+
i++;
|
224 |
+
continue;
|
225 |
+
}
|
226 |
+
|
227 |
+
// Headers
|
228 |
+
const headerMatch = line.match(/^(#{1,6})\s+(.+)$/);
|
229 |
+
if (headerMatch) {
|
230 |
+
blocks.push({
|
231 |
+
type: 'header',
|
232 |
+
level: headerMatch[1].length,
|
233 |
+
content: headerMatch[2]
|
234 |
+
});
|
235 |
+
i++;
|
236 |
+
continue;
|
237 |
+
}
|
238 |
+
|
239 |
+
// Blockquotes
|
240 |
+
if (line.startsWith('>')) {
|
241 |
+
const quoteLines = [line.substring(1).trim()];
|
242 |
+
i++;
|
243 |
+
while (i < lines.length && (lines[i].startsWith('>') || lines[i].trim() === '')) {
|
244 |
+
if (lines[i].startsWith('>')) {
|
245 |
+
quoteLines.push(lines[i].substring(1).trim());
|
246 |
+
} else if (lines[i].trim() === '' && i + 1 < lines.length && lines[i + 1].startsWith('>')) {
|
247 |
+
quoteLines.push('');
|
248 |
+
} else {
|
249 |
+
break;
|
250 |
+
}
|
251 |
+
i++;
|
252 |
+
}
|
253 |
+
blocks.push({
|
254 |
+
type: 'blockquote',
|
255 |
+
content: quoteLines.join('\n')
|
256 |
+
});
|
257 |
+
continue;
|
258 |
+
}
|
259 |
+
|
260 |
+
// Lists (unordered and ordered)
|
261 |
+
const unorderedMatch = line.match(/^([-*+])\s+(.+)$/);
|
262 |
+
const orderedMatch = line.match(/^(\d+)\.\s+(.+)$/);
|
263 |
+
|
264 |
+
if (unorderedMatch || orderedMatch) {
|
265 |
+
const isOrdered = !!orderedMatch;
|
266 |
+
const items = [];
|
267 |
+
const listIndent = line.search(/\S/);
|
268 |
+
|
269 |
+
while (i < lines.length) {
|
270 |
+
const currentLine = lines[i];
|
271 |
+
const currentIndent = currentLine.search(/\S/);
|
272 |
+
|
273 |
+
if (currentIndent === -1) {
|
274 |
+
// Empty line, check if list continues
|
275 |
+
if (i + 1 < lines.length) {
|
276 |
+
const nextIndent = lines[i + 1].search(/\S/);
|
277 |
+
if (nextIndent >= listIndent && (lines[i + 1].match(/^[\s]*[-*+]\s+/) || lines[i + 1].match(/^[\s]*\d+\.\s+/))) {
|
278 |
+
i++;
|
279 |
+
continue;
|
280 |
+
}
|
281 |
+
}
|
282 |
+
break;
|
283 |
+
}
|
284 |
+
|
285 |
+
const itemMatch = isOrdered
|
286 |
+
? currentLine.match(/^(\s*)\d+\.\s+(.+)$/)
|
287 |
+
: currentLine.match(/^(\s*)[-*+]\s+(.+)$/);
|
288 |
+
|
289 |
+
if (itemMatch && currentIndent === listIndent) {
|
290 |
+
items.push({
|
291 |
+
content: itemMatch[2],
|
292 |
+
indent: 0
|
293 |
+
});
|
294 |
+
i++;
|
295 |
+
} else if (currentIndent > listIndent) {
|
296 |
+
// Continuation of previous item or nested list
|
297 |
+
if (items.length > 0) {
|
298 |
+
items[items.length - 1].content += '\n' + currentLine;
|
299 |
+
}
|
300 |
+
i++;
|
301 |
+
} else {
|
302 |
+
break;
|
303 |
+
}
|
304 |
+
}
|
305 |
+
|
306 |
+
blocks.push({
|
307 |
+
type: isOrdered ? 'ol' : 'ul',
|
308 |
+
items: items.map(item => ({
|
309 |
+
...item,
|
310 |
+
content: item.content.trim()
|
311 |
+
}))
|
312 |
+
});
|
313 |
+
continue;
|
314 |
+
}
|
315 |
+
|
316 |
+
// Tables
|
317 |
+
if (i + 1 < lines.length && lines[i + 1].trim().match(/^[-:|]+$/)) {
|
318 |
+
const headerCells = line.split('|').map(cell => cell.trim()).filter(Boolean);
|
319 |
+
const alignmentLine = lines[i + 1];
|
320 |
+
const alignments = alignmentLine.split('|').map(cell => {
|
321 |
+
const trimmed = cell.trim();
|
322 |
+
if (trimmed.startsWith(':') && trimmed.endsWith(':')) return 'center';
|
323 |
+
if (trimmed.endsWith(':')) return 'right';
|
324 |
+
return 'left';
|
325 |
+
}).filter((_, index) => index < headerCells.length);
|
326 |
+
|
327 |
+
const rows = [];
|
328 |
+
i += 2;
|
329 |
+
|
330 |
+
while (i < lines.length && lines[i].includes('|')) {
|
331 |
+
const cells = lines[i].split('|').map(cell => cell.trim()).filter(Boolean);
|
332 |
+
if (cells.length > 0) {
|
333 |
+
rows.push(cells);
|
334 |
+
}
|
335 |
+
i++;
|
336 |
+
}
|
337 |
+
|
338 |
+
blocks.push({
|
339 |
+
type: 'table',
|
340 |
+
headers: headerCells,
|
341 |
+
alignments,
|
342 |
+
rows
|
343 |
+
});
|
344 |
+
continue;
|
345 |
+
}
|
346 |
+
|
347 |
+
// Paragraph
|
348 |
+
const paragraphLines = [line];
|
349 |
+
i++;
|
350 |
+
while (i < lines.length && lines[i].trim() !== '' &&
|
351 |
+
!lines[i].match(/^#{1,6}\s/) &&
|
352 |
+
!lines[i].match(/^[-*+]\s/) &&
|
353 |
+
!lines[i].match(/^\d+\.\s/) &&
|
354 |
+
!lines[i].startsWith('>') &&
|
355 |
+
!lines[i].match(/^[-*_]{3,}$/) &&
|
356 |
+
!lines[i].match(/___CODE_BLOCK_\d+___/)) {
|
357 |
+
paragraphLines.push(lines[i]);
|
358 |
+
i++;
|
359 |
+
}
|
360 |
+
|
361 |
+
blocks.push({
|
362 |
+
type: 'paragraph',
|
363 |
+
content: paragraphLines.join('\n')
|
364 |
+
});
|
365 |
+
}
|
366 |
+
|
367 |
+
return blocks;
|
368 |
+
}, []);
|
369 |
+
|
370 |
+
// Render a single block
|
371 |
+
const renderBlock = useCallback((block, index) => {
|
372 |
+
switch (block.type) {
|
373 |
+
case 'header':
|
374 |
+
const HeaderTag = `h${block.level}`;
|
375 |
+
return <HeaderTag key={index}>{parseInline(block.content)}</HeaderTag>;
|
376 |
+
|
377 |
+
case 'paragraph':
|
378 |
+
return <p key={index}>{parseInline(block.content)}</p>;
|
379 |
+
|
380 |
+
case 'blockquote':
|
381 |
+
const { textWithoutCode, codeBlocks } = extractCodeBlocks(block.content);
|
382 |
+
const quotedBlocks = parseBlocks(textWithoutCode, codeBlocks);
|
383 |
+
return (
|
384 |
+
<blockquote key={index} className="markdown-blockquote">
|
385 |
+
{quotedBlocks.map((b, i) => renderBlock(b, i))}
|
386 |
+
</blockquote>
|
387 |
+
);
|
388 |
+
|
389 |
+
case 'code':
|
390 |
+
return (
|
391 |
+
<div key={index} className="code-block-container">
|
392 |
+
<div className="code-block-header">
|
393 |
+
<span>{block.lang}</span>
|
394 |
+
</div>
|
395 |
+
<SyntaxHighlighter
|
396 |
+
style={atomDark}
|
397 |
+
language={block.lang}
|
398 |
+
PreTag="div"
|
399 |
+
customStyle={{ margin: 0 }}
|
400 |
+
>
|
401 |
+
{block.content}
|
402 |
+
</SyntaxHighlighter>
|
403 |
+
</div>
|
404 |
+
);
|
405 |
+
|
406 |
+
case 'ul':
|
407 |
+
case 'ol':
|
408 |
+
const ListTag = block.type === 'ol' ? 'ol' : 'ul';
|
409 |
+
return (
|
410 |
+
<ListTag key={index}>
|
411 |
+
{block.items.map((item, i) => {
|
412 |
+
// Handle nested content properly
|
413 |
+
if (item.content.includes('\n')) {
|
414 |
+
// For multi-line items, parse as nested markdown
|
415 |
+
const { textWithoutCode, codeBlocks } = extractCodeBlocks(item.content);
|
416 |
+
const nestedBlocks = parseBlocks(textWithoutCode, codeBlocks);
|
417 |
+
return (
|
418 |
+
<li key={i}>
|
419 |
+
{nestedBlocks.map((b, j) => renderBlock(b, `${i}-${j}`))}
|
420 |
+
</li>
|
421 |
+
);
|
422 |
+
}
|
423 |
+
// For single-line items, just parse inline
|
424 |
+
return <li key={i}>{parseInline(item.content)}</li>;
|
425 |
+
})}
|
426 |
+
</ListTag>
|
427 |
+
);
|
428 |
+
|
429 |
+
case 'table':
|
430 |
+
return (
|
431 |
+
<div key={index} className="table-container">
|
432 |
+
<table>
|
433 |
+
<thead>
|
434 |
+
<tr>
|
435 |
+
{block.headers.map((header, i) => (
|
436 |
+
<th key={i} style={{ textAlign: block.alignments[i] || 'left' }}>
|
437 |
+
{parseInline(header)}
|
438 |
+
</th>
|
439 |
+
))}
|
440 |
+
</tr>
|
441 |
+
</thead>
|
442 |
+
<tbody>
|
443 |
+
{block.rows.map((row, rowIndex) => (
|
444 |
+
<tr key={rowIndex}>
|
445 |
+
{row.map((cell, cellIndex) => (
|
446 |
+
<td key={cellIndex} style={{ textAlign: block.alignments[cellIndex] || 'left' }}>
|
447 |
+
{parseInline(cell)}
|
448 |
+
</td>
|
449 |
+
))}
|
450 |
+
</tr>
|
451 |
+
))}
|
452 |
+
</tbody>
|
453 |
+
</table>
|
454 |
+
</div>
|
455 |
+
);
|
456 |
+
|
457 |
+
case 'hr':
|
458 |
+
return <hr key={index} />;
|
459 |
+
|
460 |
+
default:
|
461 |
+
return null;
|
462 |
+
}
|
463 |
+
}, [parseInline, extractCodeBlocks, parseBlocks]);
|
464 |
+
|
465 |
+
// Main parse function
|
466 |
+
const parseMarkdown = useCallback((text) => {
|
467 |
+
if (!text) return [];
|
468 |
+
|
469 |
+
// Extract code blocks first
|
470 |
+
const { textWithoutCode, codeBlocks } = extractCodeBlocks(text);
|
471 |
+
|
472 |
+
// Parse blocks
|
473 |
+
const blocks = parseBlocks(textWithoutCode, codeBlocks);
|
474 |
+
|
475 |
+
// Render blocks
|
476 |
+
return blocks.map((block, index) => renderBlock(block, index));
|
477 |
+
}, [extractCodeBlocks, parseBlocks, renderBlock]);
|
478 |
+
|
479 |
+
// Parse markdown content whenever it changes
|
480 |
+
useEffect(() => {
|
481 |
+
const parsed = parseMarkdown(normalizedContent);
|
482 |
+
setParsedContent(parsed);
|
483 |
+
}, [normalizedContent, parseMarkdown]);
|
484 |
+
|
485 |
+
// Return just the parsed content
|
486 |
+
return <>{parsedContent}</>;
|
487 |
+
};
|
488 |
+
|
489 |
+
export default CustomMarkdown;
|
frontend/src/Components/AiComponents/Markdown/TestMarkdown.js
ADDED
@@ -0,0 +1,120 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import CustomMarkdown from './CustomMarkdown';
|
2 |
+
|
3 |
+
const SpacePreservationTest = () => {
|
4 |
+
const testContent = `# Space Preservation Test
|
5 |
+
|
6 |
+
## Inline Code Spacing
|
7 |
+
|
8 |
+
Here are examples with multiple spaces:
|
9 |
+
|
10 |
+
- One space: \`a b\`
|
11 |
+
- Two spaces: \`a b\`
|
12 |
+
- Three spaces: \`a b\`
|
13 |
+
- Four spaces: \`a b\`
|
14 |
+
- Tab character: \`a b\`
|
15 |
+
- Mixed: \`function ( x, y )\`
|
16 |
+
|
17 |
+
## Code Block Indentation
|
18 |
+
|
19 |
+
\`\`\`python
|
20 |
+
def example():
|
21 |
+
# 4 spaces indentation
|
22 |
+
if True:
|
23 |
+
# 8 spaces indentation
|
24 |
+
print("Hello")
|
25 |
+
|
26 |
+
# Empty line above preserved
|
27 |
+
for i in range(5):
|
28 |
+
# Aligned comments
|
29 |
+
print(i) # End of line comment
|
30 |
+
\`\`\`
|
31 |
+
|
32 |
+
## ASCII Art Test
|
33 |
+
|
34 |
+
\`\`\`
|
35 |
+
_____
|
36 |
+
/ ___ \\
|
37 |
+
| | | |
|
38 |
+
| |___| |
|
39 |
+
\\_____/
|
40 |
+
|
41 |
+
Spacing matters!
|
42 |
+
\`\`\`
|
43 |
+
|
44 |
+
## Table Alignment
|
45 |
+
|
46 |
+
\`\`\`
|
47 |
+
Name Age City
|
48 |
+
---- --- ----
|
49 |
+
Alice 25 NYC
|
50 |
+
Bob 30 LA
|
51 |
+
Charlie 35 Chicago
|
52 |
+
\`\`\`
|
53 |
+
|
54 |
+
## Inline Examples
|
55 |
+
|
56 |
+
The function \`map( x => x * 2 )\` has spaces around the arrow.
|
57 |
+
|
58 |
+
Configuration: \`{ indent: 4, tabs: false }\``;
|
59 |
+
|
60 |
+
return (
|
61 |
+
<div style={{ maxWidth: '800px', margin: '0 auto', padding: '20px' }}>
|
62 |
+
<h1>Space Preservation Test</h1>
|
63 |
+
|
64 |
+
<div style={{
|
65 |
+
marginBottom: '20px',
|
66 |
+
padding: '15px',
|
67 |
+
background: '#e3f2fd',
|
68 |
+
borderRadius: '8px'
|
69 |
+
}}>
|
70 |
+
<p><strong>What to check:</strong></p>
|
71 |
+
<ul>
|
72 |
+
<li>Inline code should preserve exact spacing</li>
|
73 |
+
<li>Code blocks should maintain indentation</li>
|
74 |
+
<li>ASCII art should be properly aligned</li>
|
75 |
+
<li>Empty lines in code blocks should be preserved</li>
|
76 |
+
</ul>
|
77 |
+
</div>
|
78 |
+
|
79 |
+
<div style={{
|
80 |
+
border: '2px solid #333',
|
81 |
+
borderRadius: '8px',
|
82 |
+
background: 'white',
|
83 |
+
padding: '20px'
|
84 |
+
}}>
|
85 |
+
<CustomMarkdown
|
86 |
+
content={testContent}
|
87 |
+
isStreaming={false}
|
88 |
+
showSourcePopup={() => {}}
|
89 |
+
hideSourcePopup={() => {}}
|
90 |
+
/>
|
91 |
+
</div>
|
92 |
+
|
93 |
+
<div style={{
|
94 |
+
marginTop: '20px',
|
95 |
+
padding: '15px',
|
96 |
+
background: '#f5f5f5',
|
97 |
+
borderRadius: '8px',
|
98 |
+
fontFamily: 'monospace',
|
99 |
+
fontSize: '14px'
|
100 |
+
}}>
|
101 |
+
<p><strong>Debug: Raw content preview</strong></p>
|
102 |
+
<pre style={{
|
103 |
+
background: '#333',
|
104 |
+
color: '#fff',
|
105 |
+
padding: '10px',
|
106 |
+
borderRadius: '4px',
|
107 |
+
overflow: 'auto',
|
108 |
+
whiteSpace: 'pre'
|
109 |
+
}}>
|
110 |
+
{`'a b' = one space
|
111 |
+
'a b' = two spaces
|
112 |
+
'a b' = three spaces
|
113 |
+
'a b' = four spaces`}
|
114 |
+
</pre>
|
115 |
+
</div>
|
116 |
+
</div>
|
117 |
+
);
|
118 |
+
};
|
119 |
+
|
120 |
+
export default SpacePreservationTest;
|
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
CHANGED
@@ -7,6 +7,7 @@
|
|
7 |
--text-color: #e0e0e0; /* Off-white text */
|
8 |
--hover-bg: #3a3a3a; /* Slightly lighter for hover effects */
|
9 |
--transition-speed: 0.25s; /* Speed of transitions */
|
|
|
10 |
}
|
11 |
|
12 |
/* Global font settings */
|
@@ -58,7 +59,7 @@ html, body {
|
|
58 |
position: relative;
|
59 |
width: 100%;
|
60 |
border-radius: 0.35rem;
|
61 |
-
background-color:
|
62 |
}
|
63 |
|
64 |
.search-input-wrapper {
|
@@ -100,6 +101,7 @@ html, body {
|
|
100 |
}
|
101 |
|
102 |
.settings-btn,
|
|
|
103 |
.send-btn {
|
104 |
background: transparent;
|
105 |
border: none;
|
@@ -108,11 +110,13 @@ html, body {
|
|
108 |
}
|
109 |
|
110 |
.settings-btn svg,
|
|
|
111 |
.send-btn svg {
|
112 |
font-size: 1.45rem;
|
113 |
}
|
114 |
|
115 |
.settings-btn:hover,
|
|
|
116 |
.send-btn:hover {
|
117 |
color: #888;
|
118 |
}
|
@@ -145,7 +149,7 @@ button.send-btn.stop-btn:hover {
|
|
145 |
left: 50%;
|
146 |
transform: translateX(-50%);
|
147 |
width: 48%;
|
148 |
-
background-color:
|
149 |
border-radius: 0.35rem;
|
150 |
}
|
151 |
|
@@ -173,11 +177,17 @@ button.send-btn.stop-btn:hover {
|
|
173 |
background-color: transparent;
|
174 |
color: var(--text-color);
|
175 |
line-height: 1.4;
|
176 |
-
padding: 0.65rem 3.25rem;
|
177 |
resize: none;
|
178 |
white-space: pre-wrap;
|
179 |
}
|
180 |
|
|
|
|
|
|
|
|
|
|
|
|
|
181 |
.chat-search-input:focus {
|
182 |
outline: none;
|
183 |
}
|
@@ -196,11 +206,19 @@ button.send-btn.stop-btn:hover {
|
|
196 |
}
|
197 |
|
198 |
/* Re-enable pointer events on the actual buttons so they remain clickable */
|
199 |
-
.chat-icon-container button
|
|
|
200 |
pointer-events: auto;
|
201 |
}
|
202 |
|
|
|
|
|
|
|
|
|
|
|
|
|
203 |
.chat-settings-btn,
|
|
|
204 |
.chat-send-btn {
|
205 |
background: transparent;
|
206 |
border: none;
|
@@ -211,10 +229,52 @@ button.send-btn.stop-btn:hover {
|
|
211 |
}
|
212 |
|
213 |
.chat-settings-btn:hover,
|
|
|
214 |
.chat-send-btn:hover {
|
215 |
color: #888;
|
216 |
}
|
217 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
218 |
/* Floating sidebar container for chat mode */
|
219 |
.floating-sidebar {
|
220 |
position: fixed;
|
@@ -228,7 +288,7 @@ button.send-btn.stop-btn:hover {
|
|
228 |
/* Chat container */
|
229 |
.chat-container {
|
230 |
flex-grow: 1;
|
231 |
-
margin-bottom:
|
232 |
}
|
233 |
|
234 |
/* Responsive Adjustments */
|
@@ -244,4 +304,4 @@ button.send-btn.stop-btn:hover {
|
|
244 |
margin: 0;
|
245 |
padding: 1rem;
|
246 |
}
|
247 |
-
}
|
|
|
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 */
|
|
|
59 |
position: relative;
|
60 |
width: 100%;
|
61 |
border-radius: 0.35rem;
|
62 |
+
background-color: var(--search-bar);
|
63 |
}
|
64 |
|
65 |
.search-input-wrapper {
|
|
|
101 |
}
|
102 |
|
103 |
.settings-btn,
|
104 |
+
.add-btn,
|
105 |
.send-btn {
|
106 |
background: transparent;
|
107 |
border: none;
|
|
|
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 |
}
|
|
|
149 |
left: 50%;
|
150 |
transform: translateX(-50%);
|
151 |
width: 48%;
|
152 |
+
background-color: var(--search-bar);
|
153 |
border-radius: 0.35rem;
|
154 |
}
|
155 |
|
|
|
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 |
}
|
|
|
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;
|
|
|
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;
|
|
|
288 |
/* Chat container */
|
289 |
.chat-container {
|
290 |
flex-grow: 1;
|
291 |
+
margin-bottom: 40rem;
|
292 |
}
|
293 |
|
294 |
/* Responsive Adjustments */
|
|
|
304 |
margin: 0;
|
305 |
padding: 1rem;
|
306 |
}
|
307 |
+
}
|
frontend/src/Components/AiPage.js
CHANGED
@@ -2,10 +2,14 @@ import React, { useState, useEffect, useRef, useCallback, useMemo } from 'react'
|
|
2 |
import { flushSync } from 'react-dom';
|
3 |
import Snackbar from '@mui/material/Snackbar';
|
4 |
import Alert from '@mui/material/Alert';
|
5 |
-
import { FaCog, FaPaperPlane, FaStop } from 'react-icons/fa';
|
6 |
import IntialSetting from './IntialSetting';
|
|
|
|
|
7 |
import ChatWindow from './AiComponents/ChatWindow';
|
8 |
-
import RightSidebar from './AiComponents/
|
|
|
|
|
9 |
import './AiPage.css';
|
10 |
|
11 |
function AiPage() {
|
@@ -24,9 +28,18 @@ function AiPage() {
|
|
24 |
const [chatBlocks, setChatBlocks] = useState([]);
|
25 |
const [selectedChatBlockId, setSelectedChatBlockId] = useState(null);
|
26 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
27 |
const [defaultChatHeight, setDefaultChatHeight] = useState(null);
|
28 |
const [chatBottomPadding, setChatBottomPadding] = useState("60px");
|
29 |
|
|
|
|
|
30 |
// States/refs for streaming
|
31 |
const [isProcessing, setIsProcessing] = useState(false);
|
32 |
const [activeBlockId, setActiveBlockId] = useState(null);
|
@@ -39,21 +52,59 @@ function AiPage() {
|
|
39 |
severity: "success",
|
40 |
});
|
41 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
42 |
// Function to open the snackbar
|
43 |
-
const openSnackbar = (message, severity = "success") => {
|
44 |
-
|
45 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
46 |
|
47 |
// Function to close the snackbar
|
48 |
const closeSnackbar = (event, reason) => {
|
49 |
if (reason === 'clickaway') return;
|
50 |
-
setSnackbar(prev => ({ ...prev, open: false }));
|
51 |
};
|
52 |
|
53 |
useEffect(() => {
|
54 |
localStorage.setItem("rightSidebarState", isRightSidebarOpen);
|
55 |
}, [isRightSidebarOpen]);
|
56 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
57 |
useEffect(() => {
|
58 |
document.documentElement.style.setProperty('--right-sidebar-width', rightSidebarWidth + 'px');
|
59 |
}, [rightSidebarWidth]);
|
@@ -89,6 +140,40 @@ function AiPage() {
|
|
89 |
}
|
90 |
}, [searchText, defaultChatHeight]);
|
91 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
92 |
const handleOpenRightSidebar = (content, chatBlockId = null) => {
|
93 |
flushSync(() => {
|
94 |
if (chatBlockId) {
|
@@ -109,6 +194,148 @@ function AiPage() {
|
|
109 |
);
|
110 |
}, []);
|
111 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
112 |
// Initiate the SSE
|
113 |
const initiateSSE = (query, blockId) => {
|
114 |
const startTime = Date.now();
|
@@ -138,6 +365,7 @@ function AiPage() {
|
|
138 |
});
|
139 |
|
140 |
eventSource.addEventListener("final_message", (e) => {
|
|
|
141 |
const endTime = Date.now();
|
142 |
const thinkingTime = ((endTime - startTime) / 1000).toFixed(1);
|
143 |
// Only update thinkingTime so the streaming flag turns false and the cursor disappears
|
@@ -147,6 +375,19 @@ function AiPage() {
|
|
147 |
: block
|
148 |
));
|
149 |
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
150 |
|
151 |
// Listen for the "complete" event to know when to close the connection.
|
152 |
eventSource.addEventListener("complete", (e) => {
|
@@ -337,6 +578,270 @@ function AiPage() {
|
|
337 |
if (searchText.trim()) handleSend();
|
338 |
};
|
339 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
340 |
// Get the chat block whose details should be shown in the sidebar.
|
341 |
const selectedBlock = chatBlocks.find(block => block.id === selectedChatBlockId);
|
342 |
const evaluateAction = selectedBlock && selectedBlock.actions
|
@@ -362,6 +867,18 @@ function AiPage() {
|
|
362 |
: 0,
|
363 |
}}
|
364 |
>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
365 |
{showChatWindow && selectedBlock && (sidebarContent !== "default" || (selectedBlock.tasks && selectedBlock.tasks.length > 0) || (selectedBlock.sources && selectedBlock.sources.length > 0)) && (
|
366 |
<div className="floating-sidebar">
|
367 |
<RightSidebar
|
@@ -397,6 +914,10 @@ function AiPage() {
|
|
397 |
thinkingTime={block.thinkingTime}
|
398 |
thoughtLabel={block.thoughtLabel}
|
399 |
sourcesRead={block.sourcesRead}
|
|
|
|
|
|
|
|
|
400 |
actions={block.actions}
|
401 |
tasks={block.tasks}
|
402 |
openRightSidebar={handleOpenRightSidebar}
|
@@ -426,19 +947,44 @@ function AiPage() {
|
|
426 |
/>
|
427 |
</div>
|
428 |
<div className="chat-icon-container">
|
429 |
-
<
|
430 |
-
className="
|
431 |
-
|
432 |
-
|
433 |
-
|
434 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
435 |
{/* Conditionally render Stop or Send button */}
|
436 |
-
<
|
437 |
-
|
438 |
-
|
439 |
-
|
440 |
-
|
441 |
-
|
|
|
|
|
|
|
442 |
</div>
|
443 |
</div>
|
444 |
</>
|
@@ -458,24 +1004,49 @@ function AiPage() {
|
|
458 |
/>
|
459 |
</div>
|
460 |
<div className="icon-container">
|
461 |
-
<
|
462 |
-
className="
|
463 |
-
|
464 |
-
|
465 |
-
|
466 |
-
|
467 |
-
|
468 |
-
|
469 |
-
|
470 |
-
|
471 |
-
|
472 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
473 |
</div>
|
474 |
</div>
|
475 |
</div>
|
476 |
)}
|
477 |
</main>
|
478 |
-
|
479 |
{showSettingsModal && (
|
480 |
<IntialSetting
|
481 |
trigger={true}
|
@@ -485,9 +1056,17 @@ function AiPage() {
|
|
485 |
closeSnackbar={closeSnackbar}
|
486 |
/>
|
487 |
)}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
488 |
<Snackbar
|
489 |
open={snackbar.open}
|
490 |
-
autoHideDuration={snackbar.
|
491 |
onClose={closeSnackbar}
|
492 |
anchorOrigin={{ vertical: 'top', horizontal: 'center' }}
|
493 |
>
|
|
|
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() {
|
|
|
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);
|
|
|
52 |
severity: "success",
|
53 |
});
|
54 |
|
55 |
+
// State for tracking selected services
|
56 |
+
const [selectedServices, setSelectedServices] = useState({
|
57 |
+
google: [],
|
58 |
+
microsoft: [],
|
59 |
+
slack: false
|
60 |
+
});
|
61 |
+
|
62 |
+
// Notifications
|
63 |
+
const {
|
64 |
+
notifications,
|
65 |
+
addNotification,
|
66 |
+
removeNotification,
|
67 |
+
updateNotification
|
68 |
+
} = useNotification();
|
69 |
+
|
70 |
+
// Token management
|
71 |
+
const tokenExpiryTimersRef = useRef({});
|
72 |
+
const notificationIdsRef = useRef({});
|
73 |
+
|
74 |
// Function to open the snackbar
|
75 |
+
const openSnackbar = useCallback((message, severity = "success", duration) => {
|
76 |
+
let finalDuration;
|
77 |
+
|
78 |
+
if (duration !== undefined) {
|
79 |
+
// If a specific duration is provided (e.g., 5000 or null), use it.
|
80 |
+
finalDuration = duration;
|
81 |
+
} else {
|
82 |
+
// Otherwise, use the default logic.
|
83 |
+
finalDuration = severity === 'success' ? 3000 : null; // Success auto-hides, others are persistent by default.
|
84 |
+
}
|
85 |
+
|
86 |
+
setSnackbar({ open: true, message, severity, duration: finalDuration });
|
87 |
+
}, []);
|
88 |
|
89 |
// Function to close the snackbar
|
90 |
const closeSnackbar = (event, reason) => {
|
91 |
if (reason === 'clickaway') return;
|
92 |
+
setSnackbar(prev => ({ ...prev, open: false, duration: null }));
|
93 |
};
|
94 |
|
95 |
useEffect(() => {
|
96 |
localStorage.setItem("rightSidebarState", isRightSidebarOpen);
|
97 |
}, [isRightSidebarOpen]);
|
98 |
|
99 |
+
// Add cleanup handler for when the user closes the tab/browser
|
100 |
+
useEffect(() => {
|
101 |
+
const handleCleanup = () => {
|
102 |
+
navigator.sendBeacon('/cleanup');
|
103 |
+
};
|
104 |
+
window.addEventListener('beforeunload', handleCleanup);
|
105 |
+
return () => window.removeEventListener('beforeunload', handleCleanup);
|
106 |
+
}, []);
|
107 |
+
|
108 |
useEffect(() => {
|
109 |
document.documentElement.style.setProperty('--right-sidebar-width', rightSidebarWidth + 'px');
|
110 |
}, [rightSidebarWidth]);
|
|
|
140 |
}
|
141 |
}, [searchText, defaultChatHeight]);
|
142 |
|
143 |
+
// Update backend whenever selected services change
|
144 |
+
useEffect(() => {
|
145 |
+
const updateSelectedServices = async () => {
|
146 |
+
try {
|
147 |
+
await fetch('/api/selected-services', {
|
148 |
+
method: 'POST',
|
149 |
+
headers: { 'Content-Type': 'application/json' },
|
150 |
+
body: JSON.stringify({
|
151 |
+
services: selectedServices
|
152 |
+
})
|
153 |
+
});
|
154 |
+
} catch (error) {
|
155 |
+
console.error('Failed to update selected services:', error);
|
156 |
+
}
|
157 |
+
};
|
158 |
+
|
159 |
+
updateSelectedServices();
|
160 |
+
}, [selectedServices]);
|
161 |
+
|
162 |
+
// Clear all tokens on page load
|
163 |
+
useEffect(() => {
|
164 |
+
// Clear all provider tokens on new tab/page load
|
165 |
+
['google', 'microsoft', 'slack'].forEach(provider => {
|
166 |
+
sessionStorage.removeItem(`${provider}_token`);
|
167 |
+
sessionStorage.removeItem(`${provider}_token_expiry`);
|
168 |
+
});
|
169 |
+
|
170 |
+
// Clear any existing timers
|
171 |
+
Object.values(tokenExpiryTimersRef.current).forEach(timer => clearTimeout(timer));
|
172 |
+
tokenExpiryTimersRef.current = {};
|
173 |
+
|
174 |
+
console.log('Cleared all tokens for new session');
|
175 |
+
}, []);
|
176 |
+
|
177 |
const handleOpenRightSidebar = (content, chatBlockId = null) => {
|
178 |
flushSync(() => {
|
179 |
if (chatBlockId) {
|
|
|
194 |
);
|
195 |
}, []);
|
196 |
|
197 |
+
// Function to store token with expiry
|
198 |
+
const storeTokenWithExpiry = (provider, token) => {
|
199 |
+
const expiryTime = Date.now() + (60 * 60 * 1000); // 1 hour from now
|
200 |
+
sessionStorage.setItem(`${provider}_token`, token);
|
201 |
+
sessionStorage.setItem(`${provider}_token_expiry`, expiryTime.toString());
|
202 |
+
|
203 |
+
// Set up expiry timer
|
204 |
+
setupTokenExpiryTimer(provider, expiryTime);
|
205 |
+
};
|
206 |
+
|
207 |
+
// Function to check if token is valid
|
208 |
+
const isTokenValid = (provider) => {
|
209 |
+
const token = sessionStorage.getItem(`${provider}_token`);
|
210 |
+
const expiry = sessionStorage.getItem(`${provider}_token_expiry`);
|
211 |
+
|
212 |
+
if (!token || !expiry) return false;
|
213 |
+
|
214 |
+
const expiryTime = parseInt(expiry);
|
215 |
+
return Date.now() < expiryTime;
|
216 |
+
};
|
217 |
+
|
218 |
+
// Function to get valid token
|
219 |
+
const getValidToken = (provider) => {
|
220 |
+
if (isTokenValid(provider)) {
|
221 |
+
return sessionStorage.getItem(`${provider}_token`);
|
222 |
+
}
|
223 |
+
return null;
|
224 |
+
};
|
225 |
+
|
226 |
+
// Function to get provider icon
|
227 |
+
const getProviderIcon = useCallback((provider) => {
|
228 |
+
switch (provider.toLowerCase()) {
|
229 |
+
case 'google':
|
230 |
+
return <FaGoogle />;
|
231 |
+
case 'microsoft':
|
232 |
+
return <FaMicrosoft />;
|
233 |
+
case 'slack':
|
234 |
+
return <FaSlack />;
|
235 |
+
default:
|
236 |
+
return null;
|
237 |
+
}
|
238 |
+
}, []);
|
239 |
+
|
240 |
+
// Function to get provider color
|
241 |
+
const getProviderColor = useCallback((provider) => {
|
242 |
+
switch (provider.toLowerCase()) {
|
243 |
+
case 'google':
|
244 |
+
return '#4285F4';
|
245 |
+
case 'microsoft':
|
246 |
+
return '#00A4EF';
|
247 |
+
case 'slack':
|
248 |
+
return '#4A154B';
|
249 |
+
default:
|
250 |
+
return '#666';
|
251 |
+
}
|
252 |
+
}, []);
|
253 |
+
|
254 |
+
// Function to set up timer for token expiry notification
|
255 |
+
const setupTokenExpiryTimer = useCallback((provider, expiryTime) => {
|
256 |
+
// Clear existing timer if any
|
257 |
+
if (tokenExpiryTimersRef.current[provider]) {
|
258 |
+
clearTimeout(tokenExpiryTimersRef.current[provider]);
|
259 |
+
}
|
260 |
+
|
261 |
+
// Remove any existing notification for this provider
|
262 |
+
if (notificationIdsRef.current[provider]) {
|
263 |
+
removeNotification(notificationIdsRef.current[provider]);
|
264 |
+
delete notificationIdsRef.current[provider];
|
265 |
+
}
|
266 |
+
|
267 |
+
const timeUntilExpiry = expiryTime - Date.now();
|
268 |
+
|
269 |
+
if (timeUntilExpiry > 0) {
|
270 |
+
tokenExpiryTimersRef.current[provider] = setTimeout(() => {
|
271 |
+
const providerName = provider.charAt(0).toUpperCase() + provider.slice(1);
|
272 |
+
const providerColor = getProviderColor(provider);
|
273 |
+
|
274 |
+
// Add notification
|
275 |
+
const notificationId = addNotification({
|
276 |
+
type: 'warning',
|
277 |
+
title: `${providerName} Authentication Expired`,
|
278 |
+
message: `Your ${providerName} authentication has expired. Please reconnect to continue using ${providerName} services.`,
|
279 |
+
icon: getProviderIcon(provider),
|
280 |
+
dismissible: true,
|
281 |
+
autoDismiss: false,
|
282 |
+
actions: [
|
283 |
+
{
|
284 |
+
id: 'reconnect',
|
285 |
+
label: `Reconnect ${providerName}`,
|
286 |
+
style: {
|
287 |
+
background: providerColor,
|
288 |
+
color: 'white',
|
289 |
+
border: 'none'
|
290 |
+
},
|
291 |
+
data: { provider }
|
292 |
+
}
|
293 |
+
],
|
294 |
+
style: {
|
295 |
+
borderLeftColor: providerColor
|
296 |
+
}
|
297 |
+
});
|
298 |
+
|
299 |
+
// Store notification ID
|
300 |
+
notificationIdsRef.current[provider] = notificationId;
|
301 |
+
|
302 |
+
// Clear token data
|
303 |
+
sessionStorage.removeItem(`${provider}_token`);
|
304 |
+
sessionStorage.removeItem(`${provider}_token_expiry`);
|
305 |
+
|
306 |
+
// Update selected services to reflect disconnection
|
307 |
+
if (provider === 'slack') {
|
308 |
+
setSelectedServices(prev => ({ ...prev, slack: false }));
|
309 |
+
} else {
|
310 |
+
setSelectedServices(prev => ({ ...prev, [provider]: [] }));
|
311 |
+
}
|
312 |
+
|
313 |
+
}, timeUntilExpiry);
|
314 |
+
}
|
315 |
+
}, [addNotification, getProviderColor, getProviderIcon, removeNotification, setSelectedServices]);
|
316 |
+
|
317 |
+
// Check existing tokens on component mount and set up timers
|
318 |
+
useEffect(() => {
|
319 |
+
['google', 'microsoft', 'slack'].forEach(provider => {
|
320 |
+
const expiry = sessionStorage.getItem(`${provider}_token_expiry`);
|
321 |
+
if (expiry) {
|
322 |
+
const expiryTime = parseInt(expiry);
|
323 |
+
if (Date.now() < expiryTime) {
|
324 |
+
setupTokenExpiryTimer(provider, expiryTime);
|
325 |
+
} else {
|
326 |
+
// Token already expired, clear it
|
327 |
+
sessionStorage.removeItem(`${provider}_token`);
|
328 |
+
sessionStorage.removeItem(`${provider}_token_expiry`);
|
329 |
+
}
|
330 |
+
}
|
331 |
+
});
|
332 |
+
|
333 |
+
// Cleanup timers on unmount
|
334 |
+
return () => {
|
335 |
+
Object.values(tokenExpiryTimersRef.current).forEach(timer => clearTimeout(timer));
|
336 |
+
};
|
337 |
+
}, [setupTokenExpiryTimer]);
|
338 |
+
|
339 |
// Initiate the SSE
|
340 |
const initiateSSE = (query, blockId) => {
|
341 |
const startTime = Date.now();
|
|
|
365 |
});
|
366 |
|
367 |
eventSource.addEventListener("final_message", (e) => {
|
368 |
+
console.log("[SSE final message]", e.data);
|
369 |
const endTime = Date.now();
|
370 |
const thinkingTime = ((endTime - startTime) / 1000).toFixed(1);
|
371 |
// Only update thinkingTime so the streaming flag turns false and the cursor disappears
|
|
|
375 |
: block
|
376 |
));
|
377 |
});
|
378 |
+
|
379 |
+
// Listen for the "final_sources" event to update sources in AI answer of this chat block.
|
380 |
+
eventSource.addEventListener("final_sources", (e) => {
|
381 |
+
try {
|
382 |
+
const sources = JSON.parse(e.data);
|
383 |
+
console.log("Final sources received:", sources);
|
384 |
+
setChatBlocks(prev => prev.map(block =>
|
385 |
+
block.id === blockId ? { ...block, finalSources: sources } : block
|
386 |
+
));
|
387 |
+
} catch (err) {
|
388 |
+
console.error("Error parsing final_sources event:", err);
|
389 |
+
}
|
390 |
+
});
|
391 |
|
392 |
// Listen for the "complete" event to know when to close the connection.
|
393 |
eventSource.addEventListener("complete", (e) => {
|
|
|
578 |
if (searchText.trim()) handleSend();
|
579 |
};
|
580 |
|
581 |
+
// Toggle the Add Content dropdown
|
582 |
+
const handleToggleAddContent = (event) => {
|
583 |
+
event.stopPropagation(); // Prevents the click from closing the menu immediately
|
584 |
+
// If we are about to close the dropdown, suppress the tooltip.
|
585 |
+
if (isAddContentOpen) {
|
586 |
+
setIsTooltipSuppressed(true);
|
587 |
+
}
|
588 |
+
setAddContentOpen(prev => !prev);
|
589 |
+
};
|
590 |
+
|
591 |
+
// Handle mouse enter on the Add Content button to suppress tooltip
|
592 |
+
const handleMouseLeaveAddBtn = () => {
|
593 |
+
setIsTooltipSuppressed(false);
|
594 |
+
};
|
595 |
+
|
596 |
+
// Close the Add Content dropdown
|
597 |
+
const closeAddContentDropdown = () => {
|
598 |
+
setAddContentOpen(false);
|
599 |
+
};
|
600 |
+
|
601 |
+
// Open the Add Files dialog
|
602 |
+
const handleOpenAddFilesDialog = () => {
|
603 |
+
setAddContentOpen(false); // Close the dropdown when opening the dialog
|
604 |
+
setIsAddFilesDialogOpen(true);
|
605 |
+
};
|
606 |
+
|
607 |
+
// Fetch excerpts for a specific block
|
608 |
+
const handleFetchExcerpts = useCallback(async (blockId) => {
|
609 |
+
let blockIndex = -1;
|
610 |
+
let currentBlock = null;
|
611 |
+
|
612 |
+
// Find the block to check its current state
|
613 |
+
setChatBlocks(prev => {
|
614 |
+
blockIndex = prev.findIndex(b => b.id === blockId);
|
615 |
+
if (blockIndex !== -1) {
|
616 |
+
currentBlock = prev[blockIndex];
|
617 |
+
}
|
618 |
+
// No state change here, just reading the state
|
619 |
+
return prev;
|
620 |
+
});
|
621 |
+
|
622 |
+
// Prevent fetching if already loaded or currently loading
|
623 |
+
if (blockIndex === -1 || !currentBlock || currentBlock.excerptsData || currentBlock.isLoadingExcerpts) return;
|
624 |
+
|
625 |
+
// Set loading state for the specific block
|
626 |
+
setChatBlocks(prev => prev.map(b =>
|
627 |
+
b.id === blockId ? { ...b, isLoadingExcerpts: true } : b
|
628 |
+
));
|
629 |
+
|
630 |
+
try {
|
631 |
+
// Call the backend endpoint to get excerpts
|
632 |
+
const response = await fetch('/action/excerpts', {
|
633 |
+
method: 'POST',
|
634 |
+
headers: { 'Content-Type': 'application/json' },
|
635 |
+
body: JSON.stringify({ blockId: blockId })
|
636 |
+
});
|
637 |
+
|
638 |
+
if (!response.ok) {
|
639 |
+
const errorData = await response.json();
|
640 |
+
throw new Error(errorData.detail || `HTTP error! status: ${response.status}`);
|
641 |
+
}
|
642 |
+
|
643 |
+
const data = await response.json();
|
644 |
+
console.log("Fetched excerpts data from backend:", data.result);
|
645 |
+
|
646 |
+
// Update the specific block with the fetched excerptsData
|
647 |
+
setChatBlocks(prev => prev.map(b =>
|
648 |
+
b.id === blockId
|
649 |
+
? {
|
650 |
+
...b,
|
651 |
+
excerptsData: data.result, // Store the fetched data
|
652 |
+
isLoadingExcerpts: false, // Turn off loading
|
653 |
+
}
|
654 |
+
: b
|
655 |
+
));
|
656 |
+
openSnackbar("Excerpts loaded successfully!", "success");
|
657 |
+
|
658 |
+
} catch (error) {
|
659 |
+
console.error("Error requesting excerpts:", error);
|
660 |
+
// Reset loading state on error
|
661 |
+
setChatBlocks(prev => prev.map(b =>
|
662 |
+
b.id === blockId ? { ...b, isLoadingExcerpts: false } : b
|
663 |
+
));
|
664 |
+
openSnackbar(`Failed to load excerpts`, "error");
|
665 |
+
}
|
666 |
+
}, [openSnackbar]);
|
667 |
+
|
668 |
+
// Function to handle notification actions
|
669 |
+
const handleNotificationAction = (notificationId, actionId, actionData) => {
|
670 |
+
console.log('Notification action triggered:', { notificationId, actionId, actionData });
|
671 |
+
|
672 |
+
// Handle both 'reconnect' and 'connect' actions
|
673 |
+
if ((actionId === 'reconnect' || actionId === 'connect') && actionData?.provider) {
|
674 |
+
// Remove the notification
|
675 |
+
removeNotification(notificationId);
|
676 |
+
|
677 |
+
// Clean up stored notification ID if it exists
|
678 |
+
if (notificationIdsRef.current[actionData.provider] === notificationId) {
|
679 |
+
delete notificationIdsRef.current[actionData.provider];
|
680 |
+
}
|
681 |
+
|
682 |
+
// Trigger authentication
|
683 |
+
initiateOAuth(actionData.provider);
|
684 |
+
}
|
685 |
+
};
|
686 |
+
|
687 |
+
// Function to initiate OAuth
|
688 |
+
const initiateOAuth = (provider) => {
|
689 |
+
const authUrls = {
|
690 |
+
google: `https://accounts.google.com/o/oauth2/v2/auth?` +
|
691 |
+
`client_id=${process.env.REACT_APP_GOOGLE_CLIENT_ID}&` +
|
692 |
+
`response_type=token&` +
|
693 |
+
`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&` +
|
694 |
+
`redirect_uri=${window.location.origin}/auth-receiver.html&` +
|
695 |
+
`prompt=select_account`,
|
696 |
+
|
697 |
+
microsoft: `https://login.microsoftonline.com/common/oauth2/v2.0/authorize?` +
|
698 |
+
`client_id=${process.env.REACT_APP_MICROSOFT_CLIENT_ID}&` +
|
699 |
+
`response_type=token&` +
|
700 |
+
`scope=openid profile email Files.Read.All Mail.Read Calendars.Read Tasks.Read Notes.Read&` +
|
701 |
+
`redirect_uri=${window.location.origin}/auth-receiver.html&` +
|
702 |
+
`response_mode=fragment&` +
|
703 |
+
`prompt=select_account`,
|
704 |
+
|
705 |
+
slack: `https://slack.com/oauth/v2/authorize?` +
|
706 |
+
`client_id=${process.env.REACT_APP_SLACK_CLIENT_ID}&` +
|
707 |
+
`scope=channels:read,channels:history,files:read,groups:read,im:read,mpim:read,search:read,users:read&` +
|
708 |
+
`redirect_uri=${window.location.origin}/auth-receiver.html`
|
709 |
+
};
|
710 |
+
|
711 |
+
const authWindow = window.open(
|
712 |
+
authUrls[provider],
|
713 |
+
'Connect Account',
|
714 |
+
'width=600,height=700,left=200,top=100'
|
715 |
+
);
|
716 |
+
|
717 |
+
// Show connecting notification
|
718 |
+
const connectingNotificationId = addNotification({
|
719 |
+
type: 'info',
|
720 |
+
title: `Connecting to ${provider.charAt(0).toUpperCase() + provider.slice(1)}`,
|
721 |
+
message: 'Please complete the authentication in the popup window...',
|
722 |
+
icon: getProviderIcon(provider),
|
723 |
+
dismissible: false,
|
724 |
+
autoDismiss: false
|
725 |
+
});
|
726 |
+
|
727 |
+
// Set up message listener
|
728 |
+
const messageHandler = async (event) => {
|
729 |
+
if (event.origin !== window.location.origin) return;
|
730 |
+
|
731 |
+
if (event.data.type === 'auth-success') {
|
732 |
+
const { token } = event.data;
|
733 |
+
|
734 |
+
// Remove connecting notification
|
735 |
+
removeNotification(connectingNotificationId);
|
736 |
+
|
737 |
+
// Store token with expiry
|
738 |
+
storeTokenWithExpiry(provider, token);
|
739 |
+
|
740 |
+
// Send token to backend
|
741 |
+
try {
|
742 |
+
const response = await fetch('/api/session-token', {
|
743 |
+
method: 'POST',
|
744 |
+
headers: { 'Content-Type': 'application/json' },
|
745 |
+
body: JSON.stringify({
|
746 |
+
provider,
|
747 |
+
token
|
748 |
+
})
|
749 |
+
});
|
750 |
+
|
751 |
+
if (response.ok) {
|
752 |
+
// Show success notification
|
753 |
+
addNotification({
|
754 |
+
type: 'success',
|
755 |
+
title: 'Connected Successfully',
|
756 |
+
message: `Successfully connected to ${provider.charAt(0).toUpperCase() + provider.slice(1)}!`,
|
757 |
+
icon: getProviderIcon(provider),
|
758 |
+
autoDismiss: true,
|
759 |
+
duration: 3000,
|
760 |
+
showProgress: true
|
761 |
+
});
|
762 |
+
}
|
763 |
+
} catch (error) {
|
764 |
+
console.error(`Failed to connect to ${provider}:`, error);
|
765 |
+
addNotification({
|
766 |
+
type: 'error',
|
767 |
+
title: 'Connection Failed',
|
768 |
+
message: `Failed to connect to ${provider.charAt(0).toUpperCase() + provider.slice(1)}. Please try again.`,
|
769 |
+
autoDismiss: true,
|
770 |
+
duration: 5000
|
771 |
+
});
|
772 |
+
}
|
773 |
+
|
774 |
+
window.removeEventListener('message', messageHandler);
|
775 |
+
} else if (event.data.type === 'auth-failed') {
|
776 |
+
// Remove connecting notification
|
777 |
+
removeNotification(connectingNotificationId);
|
778 |
+
|
779 |
+
// Show error notification
|
780 |
+
addNotification({
|
781 |
+
type: 'error',
|
782 |
+
title: 'Authentication Failed',
|
783 |
+
message: `Failed to authenticate with ${provider.charAt(0).toUpperCase() + provider.slice(1)}. Please try again.`,
|
784 |
+
autoDismiss: true,
|
785 |
+
duration: 5000
|
786 |
+
});
|
787 |
+
|
788 |
+
window.removeEventListener('message', messageHandler);
|
789 |
+
}
|
790 |
+
};
|
791 |
+
|
792 |
+
window.addEventListener('message', messageHandler);
|
793 |
+
|
794 |
+
// Handle if user closes the popup without authenticating
|
795 |
+
const checkInterval = setInterval(() => {
|
796 |
+
if (authWindow.closed) {
|
797 |
+
clearInterval(checkInterval);
|
798 |
+
removeNotification(connectingNotificationId);
|
799 |
+
window.removeEventListener('message', messageHandler);
|
800 |
+
}
|
801 |
+
}, 1000);
|
802 |
+
};
|
803 |
+
|
804 |
+
// Handle service selection from dropdown
|
805 |
+
const handleServiceClick = useCallback((provider, service) => {
|
806 |
+
// Toggle selection
|
807 |
+
if (provider === 'slack') {
|
808 |
+
setSelectedServices(prev => ({ ...prev, slack: !prev.slack }));
|
809 |
+
} else {
|
810 |
+
setSelectedServices(prev => ({
|
811 |
+
...prev,
|
812 |
+
[provider]: prev[provider].includes(service)
|
813 |
+
? prev[provider].filter(s => s !== service)
|
814 |
+
: [...prev[provider], service]
|
815 |
+
}));
|
816 |
+
}
|
817 |
+
|
818 |
+
// Check if token is valid
|
819 |
+
if (!isTokenValid(provider)) {
|
820 |
+
// Show notification prompting to authenticate
|
821 |
+
const notificationId = addNotification({
|
822 |
+
type: 'info',
|
823 |
+
title: 'Authentication Required',
|
824 |
+
message: `Please connect your ${provider.charAt(0).toUpperCase() + provider.slice(1)} account to use this service.`,
|
825 |
+
icon: getProviderIcon(provider),
|
826 |
+
actions: [
|
827 |
+
{
|
828 |
+
id: 'connect',
|
829 |
+
label: `Connect ${provider.charAt(0).toUpperCase() + provider.slice(1)}`,
|
830 |
+
style: {
|
831 |
+
background: getProviderColor(provider),
|
832 |
+
color: 'white',
|
833 |
+
border: 'none'
|
834 |
+
},
|
835 |
+
data: { provider }
|
836 |
+
}
|
837 |
+
],
|
838 |
+
autoDismiss: true,
|
839 |
+
duration: 5000,
|
840 |
+
showProgress: true
|
841 |
+
});
|
842 |
+
}
|
843 |
+
}, [addNotification, getProviderIcon, getProviderColor]);
|
844 |
+
|
845 |
// Get the chat block whose details should be shown in the sidebar.
|
846 |
const selectedBlock = chatBlocks.find(block => block.id === selectedChatBlockId);
|
847 |
const evaluateAction = selectedBlock && selectedBlock.actions
|
|
|
867 |
: 0,
|
868 |
}}
|
869 |
>
|
870 |
+
<Notification
|
871 |
+
notifications={notifications}
|
872 |
+
position="top-right"
|
873 |
+
animation="slide"
|
874 |
+
stackDirection="down"
|
875 |
+
maxNotifications={5}
|
876 |
+
spacing={12}
|
877 |
+
offset={{ x: 20, y: 20 }}
|
878 |
+
onDismiss={removeNotification}
|
879 |
+
onAction={handleNotificationAction}
|
880 |
+
theme="light"
|
881 |
+
/>
|
882 |
{showChatWindow && selectedBlock && (sidebarContent !== "default" || (selectedBlock.tasks && selectedBlock.tasks.length > 0) || (selectedBlock.sources && selectedBlock.sources.length > 0)) && (
|
883 |
<div className="floating-sidebar">
|
884 |
<RightSidebar
|
|
|
914 |
thinkingTime={block.thinkingTime}
|
915 |
thoughtLabel={block.thoughtLabel}
|
916 |
sourcesRead={block.sourcesRead}
|
917 |
+
finalSources={block.finalSources}
|
918 |
+
excerptsData={block.excerptsData}
|
919 |
+
isLoadingExcerpts={block.isLoadingExcerpts}
|
920 |
+
onFetchExcerpts={handleFetchExcerpts}
|
921 |
actions={block.actions}
|
922 |
tasks={block.tasks}
|
923 |
openRightSidebar={handleOpenRightSidebar}
|
|
|
947 |
/>
|
948 |
</div>
|
949 |
<div className="chat-icon-container">
|
950 |
+
<div className="chat-left-icons">
|
951 |
+
<div className="tooltip-wrapper">
|
952 |
+
<button
|
953 |
+
className="chat-settings-btn"
|
954 |
+
onClick={() => setShowSettingsModal(true)}
|
955 |
+
>
|
956 |
+
<FaCog />
|
957 |
+
</button>
|
958 |
+
<span className="tooltip">Settings</span>
|
959 |
+
</div>
|
960 |
+
<div
|
961 |
+
className="tooltip-wrapper"
|
962 |
+
onMouseLeave={handleMouseLeaveAddBtn}
|
963 |
+
>
|
964 |
+
<button className="chat-add-btn" onClick={handleToggleAddContent} ref={chatAddBtnRef}>
|
965 |
+
<FaPlus />
|
966 |
+
</button>
|
967 |
+
<span className={`tooltip ${isAddContentOpen || isTooltipSuppressed ? 'hidden' : ''}`}>Add Content</span>
|
968 |
+
<AddContentDropdown
|
969 |
+
isOpen={isAddContentOpen}
|
970 |
+
onClose={closeAddContentDropdown}
|
971 |
+
toggleButtonRef={chatAddBtnRef}
|
972 |
+
onAddFilesClick={handleOpenAddFilesDialog}
|
973 |
+
onServiceClick={handleServiceClick}
|
974 |
+
selectedServices={selectedServices}
|
975 |
+
/>
|
976 |
+
</div>
|
977 |
+
</div>
|
978 |
{/* Conditionally render Stop or Send button */}
|
979 |
+
<div className="tooltip-wrapper">
|
980 |
+
<button
|
981 |
+
className={`chat-send-btn ${isProcessing ? 'stop-btn' : ''}`}
|
982 |
+
onClick={isProcessing ? handleStop : handleSendButtonClick}
|
983 |
+
>
|
984 |
+
{isProcessing ? <FaStop size={12} color="black" /> : <FaPaperPlane />}
|
985 |
+
</button>
|
986 |
+
<span className="tooltip">{isProcessing ? 'Stop' : 'Send'}</span>
|
987 |
+
</div>
|
988 |
</div>
|
989 |
</div>
|
990 |
</>
|
|
|
1004 |
/>
|
1005 |
</div>
|
1006 |
<div className="icon-container">
|
1007 |
+
<div className="left-icons">
|
1008 |
+
<div className="tooltip-wrapper">
|
1009 |
+
<button
|
1010 |
+
className="settings-btn"
|
1011 |
+
onClick={() => setShowSettingsModal(true)}
|
1012 |
+
>
|
1013 |
+
<FaCog />
|
1014 |
+
</button>
|
1015 |
+
<span className="tooltip">Settings</span>
|
1016 |
+
</div>
|
1017 |
+
<div
|
1018 |
+
className="tooltip-wrapper"
|
1019 |
+
onMouseLeave={handleMouseLeaveAddBtn}
|
1020 |
+
>
|
1021 |
+
<button className="add-btn" onClick={handleToggleAddContent} ref={addBtnRef}>
|
1022 |
+
<FaPlus />
|
1023 |
+
</button>
|
1024 |
+
<span className={`tooltip ${isAddContentOpen || isTooltipSuppressed ? 'hidden' : ''}`}>Add Content</span>
|
1025 |
+
<AddContentDropdown
|
1026 |
+
isOpen={isAddContentOpen}
|
1027 |
+
onClose={closeAddContentDropdown}
|
1028 |
+
toggleButtonRef={addBtnRef}
|
1029 |
+
onAddFilesClick={handleOpenAddFilesDialog}
|
1030 |
+
onServiceClick={handleServiceClick}
|
1031 |
+
selectedServices={selectedServices}
|
1032 |
+
/>
|
1033 |
+
</div>
|
1034 |
+
</div>
|
1035 |
+
<div className="tooltip-wrapper">
|
1036 |
+
<button
|
1037 |
+
className={`send-btn ${isProcessing ? 'stop-btn' : ''}`}
|
1038 |
+
onClick={isProcessing ? handleStop : handleSendButtonClick}
|
1039 |
+
>
|
1040 |
+
{isProcessing ? <FaStop /> : <FaPaperPlane />}
|
1041 |
+
</button>
|
1042 |
+
<span className="tooltip">{isProcessing ? 'Stop' : 'Send'}</span>
|
1043 |
+
</div>
|
1044 |
</div>
|
1045 |
</div>
|
1046 |
</div>
|
1047 |
)}
|
1048 |
</main>
|
1049 |
+
|
1050 |
{showSettingsModal && (
|
1051 |
<IntialSetting
|
1052 |
trigger={true}
|
|
|
1056 |
closeSnackbar={closeSnackbar}
|
1057 |
/>
|
1058 |
)}
|
1059 |
+
{isAddFilesDialogOpen && (
|
1060 |
+
<AddFilesDialog
|
1061 |
+
isOpen={isAddFilesDialogOpen}
|
1062 |
+
onClose={() => setIsAddFilesDialogOpen(false)}
|
1063 |
+
openSnackbar={openSnackbar}
|
1064 |
+
setSessionContent={setSessionContent}
|
1065 |
+
/>
|
1066 |
+
)}
|
1067 |
<Snackbar
|
1068 |
open={snackbar.open}
|
1069 |
+
autoHideDuration={snackbar.duration}
|
1070 |
onClose={closeSnackbar}
|
1071 |
anchorOrigin={{ vertical: 'top', horizontal: 'center' }}
|
1072 |
>
|
frontend/src/Components/IntialSetting.css
CHANGED
@@ -171,4 +171,4 @@ input, select, textarea {
|
|
171 |
width: 90%;
|
172 |
max-height: 75vh; /* Adjust height for smaller screens */
|
173 |
}
|
174 |
-
}
|
|
|
171 |
width: 90%;
|
172 |
max-height: 75vh; /* Adjust height for smaller screens */
|
173 |
}
|
174 |
+
}
|
frontend/src/Components/IntialSetting.js
CHANGED
@@ -22,30 +22,37 @@ function IntialSetting(props) {
|
|
22 |
// Model options for different providers
|
23 |
const modelOptions = {
|
24 |
OpenAI: {
|
25 |
-
"GPT
|
26 |
-
"GPT
|
27 |
-
"GPT
|
28 |
-
"GPT
|
29 |
-
"
|
|
|
|
|
30 |
},
|
31 |
Anthropic: {
|
32 |
-
"Claude
|
33 |
-
"Claude
|
34 |
-
"Claude 3
|
35 |
-
"Claude 3
|
36 |
-
"Claude 3
|
|
|
|
|
|
|
37 |
},
|
38 |
Google: {
|
39 |
-
"Gemini
|
40 |
-
"Gemini 1.5 Flash": "gemini-1.5-flash",
|
41 |
-
"Gemini 2.0 Flash Lite": "gemini-2.0-flash-lite-preview-02-05",
|
42 |
-
"Gemini 2.0 Flash Experimental": "gemini-2.0-flash-exp",
|
43 |
"Gemini 2.0 Flash": "gemini-2.0-flash",
|
44 |
-
"Gemini 2.
|
|
|
|
|
45 |
},
|
46 |
XAI: {
|
47 |
-
"Grok
|
48 |
-
"Grok
|
|
|
|
|
|
|
49 |
},
|
50 |
};
|
51 |
|
|
|
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-preview-04-17",
|
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 |
|
frontend/src/Icons/excerpts.png
ADDED
![]() |
frontend/src/Icons/excerpts.pngZone.Identifier
ADDED
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
1 |
+
[ZoneTransfer]
|
2 |
+
ZoneId=3
|
3 |
+
ReferrerUrl=https://www.flaticon.com/search?word=quotes
|
4 |
+
HostUrl=about:internet
|
main.py
CHANGED
@@ -1,12 +1,14 @@
|
|
1 |
import os
|
2 |
import re
|
3 |
-
import asyncio
|
4 |
import json
|
5 |
import time
|
|
|
|
|
6 |
import logging
|
7 |
-
|
|
|
8 |
from fastapi.staticfiles import StaticFiles
|
9 |
-
from fastapi import FastAPI, Request, HTTPException
|
10 |
from fastapi.responses import StreamingResponse, JSONResponse, FileResponse
|
11 |
from fastapi.middleware.cors import CORSMiddleware
|
12 |
from dotenv import load_dotenv
|
@@ -14,17 +16,22 @@ from tenacity import RetryError
|
|
14 |
from openai import RateLimitError
|
15 |
from anthropic import RateLimitError as AnthropicRateLimitError
|
16 |
from google.api_core.exceptions import ResourceExhausted
|
|
|
17 |
|
18 |
logger = logging.getLogger()
|
19 |
logger.setLevel(logging.INFO)
|
20 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
21 |
CONTEXT_LENGTH = 128000
|
22 |
BUFFER = 10000
|
23 |
MAX_TOKENS_ALLOWED = CONTEXT_LENGTH - BUFFER
|
24 |
|
25 |
-
# Path to the .env file
|
26 |
-
ENV_FILE_PATH = os.getenv("WRITABLE_DIR", "/tmp") + "/.env"
|
27 |
-
|
28 |
# Per-session state
|
29 |
SESSION_STORE: Dict[str, Dict[str, Any]] = {}
|
30 |
|
@@ -45,9 +52,22 @@ def stop_on_error():
|
|
45 |
state["process_task"].cancel()
|
46 |
del state["process_task"]
|
47 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
48 |
# Initialize the components
|
49 |
-
def initialize_components():
|
50 |
-
load_dotenv(
|
51 |
|
52 |
from src.search.search_engine import SearchEngine
|
53 |
from src.query_processing.query_processor import QueryProcessor
|
@@ -58,40 +78,206 @@ def initialize_components():
|
|
58 |
from src.crawl.crawler import CustomCrawler
|
59 |
from src.utils.api_key_manager import APIKeyManager
|
60 |
from src.query_processing.late_chunking.late_chunker import LateChunker
|
|
|
|
|
|
|
61 |
|
62 |
manager = APIKeyManager()
|
63 |
manager._reinit()
|
64 |
-
|
65 |
-
|
66 |
-
|
67 |
-
#
|
68 |
-
|
69 |
-
|
70 |
-
|
71 |
-
|
72 |
-
|
73 |
-
|
74 |
-
|
75 |
-
|
|
|
|
|
|
|
76 |
async def process_query(user_query: str, sse_queue: asyncio.Queue):
|
77 |
state = SESSION_STORE
|
78 |
|
79 |
try:
|
|
|
80 |
category = await state["query_processor"].classify_query(user_query)
|
81 |
cat_lower = category.lower().strip()
|
82 |
-
|
83 |
-
if state["session_id"] is None:
|
84 |
-
state["session_id"] = await state["crawler"].create_session()
|
85 |
-
|
86 |
user_query = re.sub(r'category:.*', '', user_query, flags=re.IGNORECASE).strip()
|
87 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
88 |
if cat_lower == "basic":
|
89 |
response = ""
|
90 |
chunk_counter = 1
|
91 |
-
|
92 |
-
await sse_queue.put(("
|
93 |
-
|
94 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
95 |
|
96 |
await sse_queue.put(("final_message", response))
|
97 |
SESSION_STORE["chat_history"].append({"query": user_query, "response": response})
|
@@ -121,13 +307,16 @@ async def process_query(user_query: str, sse_queue: asyncio.Queue):
|
|
121 |
max_attempts=1
|
122 |
)
|
123 |
|
124 |
-
|
|
|
|
|
|
|
125 |
if search_contents:
|
126 |
for k, content in enumerate(search_contents, 1):
|
127 |
if isinstance(content, Exception):
|
128 |
print(f"Error fetching content: {content}")
|
129 |
elif content:
|
130 |
-
contents += f"
|
131 |
|
132 |
if len(contents.strip()) > 0:
|
133 |
await sse_queue.put(("step", "Generating Response..."))
|
@@ -140,13 +329,27 @@ async def process_query(user_query: str, sse_queue: asyncio.Queue):
|
|
140 |
|
141 |
response = ""
|
142 |
chunk_counter = 1
|
143 |
-
async for chunk in state["reasoner"].
|
144 |
await sse_queue.put(("token", json.dumps({"chunk": chunk, "index": chunk_counter})))
|
145 |
response += chunk
|
146 |
chunk_counter += 1
|
147 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
148 |
await sse_queue.put(("final_message", response))
|
|
|
|
|
149 |
SESSION_STORE["chat_history"].append({"query": user_query, "response": response})
|
|
|
|
|
150 |
|
151 |
await sse_queue.put(("action", {
|
152 |
"name": "sources",
|
@@ -189,7 +392,11 @@ async def process_query(user_query: str, sse_queue: asyncio.Queue):
|
|
189 |
)
|
190 |
current_search_results.extend(filtered_urls)
|
191 |
|
192 |
-
|
|
|
|
|
|
|
|
|
193 |
search_contents = await state['crawler'].fetch_page_contents(
|
194 |
urls,
|
195 |
sub_query,
|
@@ -198,13 +405,13 @@ async def process_query(user_query: str, sse_queue: asyncio.Queue):
|
|
198 |
)
|
199 |
current_search_contents.extend(search_contents)
|
200 |
|
201 |
-
contents =
|
202 |
if search_contents:
|
203 |
for k, c in enumerate(search_contents, 1):
|
204 |
if isinstance(c, Exception):
|
205 |
logger.info(f"Error fetching content: {c}")
|
206 |
elif c:
|
207 |
-
contents += f"
|
208 |
|
209 |
if len(contents.strip()) > 0:
|
210 |
await sse_queue.put(("task", (sub_query, "DONE")))
|
@@ -225,7 +432,11 @@ async def process_query(user_query: str, sse_queue: asyncio.Queue):
|
|
225 |
results = await asyncio.gather(*tasks)
|
226 |
end = time.time()
|
227 |
|
228 |
-
|
|
|
|
|
|
|
|
|
229 |
|
230 |
unique_results = []
|
231 |
seen = set()
|
@@ -255,7 +466,7 @@ async def process_query(user_query: str, sse_queue: asyncio.Queue):
|
|
255 |
response = ""
|
256 |
chunk_counter = 1
|
257 |
is_first_chunk = True
|
258 |
-
async for chunk in state['reasoner'].
|
259 |
if is_first_chunk:
|
260 |
await sse_queue.put(("step", f"Thought and searched for {int(end - start)} seconds"))
|
261 |
is_first_chunk = False
|
@@ -264,8 +475,21 @@ async def process_query(user_query: str, sse_queue: asyncio.Queue):
|
|
264 |
response += chunk
|
265 |
chunk_counter += 1
|
266 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
267 |
await sse_queue.put(("final_message", response))
|
|
|
|
|
268 |
SESSION_STORE["chat_history"].append({"query": user_query, "response": response})
|
|
|
|
|
269 |
|
270 |
await sse_queue.put(("action", {
|
271 |
"name": "sources",
|
@@ -327,7 +551,7 @@ async def process_query(user_query: str, sse_queue: asyncio.Queue):
|
|
327 |
if isinstance(c, Exception):
|
328 |
logger.info(f"Error fetching content: {c}")
|
329 |
elif c:
|
330 |
-
contents += f"
|
331 |
|
332 |
return contents
|
333 |
|
@@ -361,7 +585,11 @@ async def process_query(user_query: str, sse_queue: asyncio.Queue):
|
|
361 |
results = await asyncio.gather(*tasks)
|
362 |
end = time.time()
|
363 |
|
|
|
364 |
previous_contents = []
|
|
|
|
|
|
|
365 |
for result in results:
|
366 |
if result:
|
367 |
for content in result:
|
@@ -397,7 +625,7 @@ async def process_query(user_query: str, sse_queue: asyncio.Queue):
|
|
397 |
response = ""
|
398 |
chunk_counter = 1
|
399 |
is_first_chunk = True
|
400 |
-
async for chunk in state['reasoner'].
|
401 |
if is_first_chunk:
|
402 |
await sse_queue.put(("step", f"Thought and searched for {int(end - start)} seconds"))
|
403 |
is_first_chunk = False
|
@@ -406,8 +634,21 @@ async def process_query(user_query: str, sse_queue: asyncio.Queue):
|
|
406 |
response += chunk
|
407 |
chunk_counter += 1
|
408 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
409 |
await sse_queue.put(("final_message", response))
|
|
|
|
|
410 |
SESSION_STORE["chat_history"].append({"query": user_query, "response": response})
|
|
|
|
|
411 |
|
412 |
await sse_queue.put(("action", {
|
413 |
"name": "sources",
|
@@ -508,6 +749,11 @@ async def process_query(user_query: str, sse_queue: asyncio.Queue):
|
|
508 |
|
509 |
answer = state['graph_rag'].query_graph(user_query)
|
510 |
if answer:
|
|
|
|
|
|
|
|
|
|
|
511 |
token_count = state['model'].get_num_tokens(answer)
|
512 |
if token_count > MAX_TOKENS_ALLOWED:
|
513 |
answer = await state['late_chunker'].chunker(
|
@@ -523,7 +769,7 @@ async def process_query(user_query: str, sse_queue: asyncio.Queue):
|
|
523 |
response = ""
|
524 |
chunk_counter = 1
|
525 |
is_first_chunk = True
|
526 |
-
async for chunk in state['reasoner'].
|
527 |
if is_first_chunk:
|
528 |
await sse_queue.put(("step", f"Thought and searched for {int(end - start)} seconds"))
|
529 |
is_first_chunk = False
|
@@ -532,8 +778,21 @@ async def process_query(user_query: str, sse_queue: asyncio.Queue):
|
|
532 |
response += chunk
|
533 |
chunk_counter += 1
|
534 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
535 |
await sse_queue.put(("final_message", response))
|
|
|
|
|
536 |
SESSION_STORE["chat_history"].append({"query": user_query, "response": response})
|
|
|
|
|
537 |
|
538 |
await sse_queue.put(("action", {
|
539 |
"name": "sources",
|
@@ -557,6 +816,7 @@ async def process_query(user_query: str, sse_queue: asyncio.Queue):
|
|
557 |
|
558 |
except Exception as e:
|
559 |
await sse_queue.put(("error", str(e)))
|
|
|
560 |
stop()
|
561 |
|
562 |
# Create a FastAPI app
|
@@ -616,11 +876,12 @@ def action_sources(payload: Dict[str, Any]) -> Dict[str, Any]:
|
|
616 |
return {"result": sources}
|
617 |
except Exception as e:
|
618 |
return JSONResponse(content={"error": str(e)}, status_code=500)
|
619 |
-
|
620 |
# Define the route for graph action to display the graph
|
621 |
@app.post("/action/graph")
|
622 |
def action_graph() -> Dict[str, Any]:
|
623 |
state = SESSION_STORE
|
|
|
624 |
try:
|
625 |
html_str = state['graph_rag'].display_graph()
|
626 |
|
@@ -644,7 +905,56 @@ async def action_evaluate(payload: Dict[str, Any]) -> Dict[str, Any]:
|
|
644 |
return {"result": result}
|
645 |
except Exception as e:
|
646 |
return JSONResponse(content={"error": str(e)}, status_code=500)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
647 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
648 |
@app.post("/settings")
|
649 |
async def update_settings(data: Dict[str, Any]):
|
650 |
from src.helpers.helper import (
|
@@ -658,9 +968,6 @@ async def update_settings(data: Dict[str, Any]):
|
|
658 |
multiple_api_keys = data.get("Model_API_Keys", "").strip()
|
659 |
brave_api_key = data.get("Brave_Search_API_Key", "").strip()
|
660 |
proxy_list = data.get("Proxy_List", "").strip()
|
661 |
-
# neo4j_url = data.get("Neo4j_URL", "").strip()
|
662 |
-
# neo4j_username = data.get("Neo4j_Username", "").strip()
|
663 |
-
# neo4j_password = data.get("Neo4j_Password", "").strip()
|
664 |
model_temperature = str(data.get("Model_Temperature", 0.0))
|
665 |
model_top_p = str(data.get("Model_Top_P", 1.0))
|
666 |
|
@@ -674,9 +981,6 @@ async def update_settings(data: Dict[str, Any]):
|
|
674 |
env_updates.update(px)
|
675 |
|
676 |
env_updates["BRAVE_API_KEY"] = brave_api_key
|
677 |
-
# env_updates["NEO4J_URI"] = neo4j_url
|
678 |
-
# env_updates["NEO4J_USER"] = neo4j_username
|
679 |
-
# env_updates["NEO4J_PASSWORD"] = neo4j_password
|
680 |
env_updates["MODEL_PROVIDER"] = prov_lower
|
681 |
env_updates["MODEL_NAME"] = model_name
|
682 |
env_updates["MODEL_TEMPERATURE"] = model_temperature
|
@@ -684,21 +988,136 @@ async def update_settings(data: Dict[str, Any]):
|
|
684 |
|
685 |
update_env_vars(env_updates)
|
686 |
load_dotenv(override=True)
|
687 |
-
initialize_components()
|
688 |
|
689 |
return {"success": True}
|
690 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
691 |
@app.on_event("startup")
|
692 |
def init_chat():
|
693 |
if not SESSION_STORE:
|
694 |
print("Initializing chat...")
|
695 |
|
|
|
|
|
|
|
|
|
|
|
696 |
SESSION_STORE["settings_saved"] = False
|
697 |
SESSION_STORE["session_id"] = None
|
|
|
|
|
698 |
SESSION_STORE["chat_history"] = []
|
|
|
|
|
|
|
|
|
|
|
699 |
|
700 |
print("Chat initialized!")
|
701 |
-
|
702 |
return {"sucess": True}
|
703 |
else:
|
704 |
print("Chat already initialized!")
|
|
|
1 |
import os
|
2 |
import re
|
|
|
3 |
import json
|
4 |
import time
|
5 |
+
import shutil
|
6 |
+
import asyncio
|
7 |
import logging
|
8 |
+
import traceback
|
9 |
+
from typing import List, Dict, Any, Optional
|
10 |
from fastapi.staticfiles import StaticFiles
|
11 |
+
from fastapi import FastAPI, Request, HTTPException, UploadFile, File, Form
|
12 |
from fastapi.responses import StreamingResponse, JSONResponse, FileResponse
|
13 |
from fastapi.middleware.cors import CORSMiddleware
|
14 |
from dotenv import load_dotenv
|
|
|
16 |
from openai import RateLimitError
|
17 |
from anthropic import RateLimitError as AnthropicRateLimitError
|
18 |
from google.api_core.exceptions import ResourceExhausted
|
19 |
+
from src.helpers.helper import get_folder_size, clear_folder
|
20 |
|
21 |
logger = logging.getLogger()
|
22 |
logger.setLevel(logging.INFO)
|
23 |
|
24 |
+
# Path to the .env file
|
25 |
+
ENV_FILE_PATH = os.getenv("WRITABLE_DIR", "/tmp") + "/.env"
|
26 |
+
|
27 |
+
# Define the upload directory and maximum folder size
|
28 |
+
UPLOAD_DIRECTORY = os.getenv("WRITABLE_DIR", "/tmp") + "/uploads"
|
29 |
+
MAX_FOLDER_SIZE = 10 * 1024 * 1024 # 10 MB in bytes
|
30 |
+
|
31 |
CONTEXT_LENGTH = 128000
|
32 |
BUFFER = 10000
|
33 |
MAX_TOKENS_ALLOWED = CONTEXT_LENGTH - BUFFER
|
34 |
|
|
|
|
|
|
|
35 |
# Per-session state
|
36 |
SESSION_STORE: Dict[str, Dict[str, Any]] = {}
|
37 |
|
|
|
52 |
state["process_task"].cancel()
|
53 |
del state["process_task"]
|
54 |
|
55 |
+
# Get OAuth tokens for MCP tools
|
56 |
+
def get_oauth_token(provider: str) -> Optional[str]:
|
57 |
+
if "oauth_tokens" in SESSION_STORE and provider in SESSION_STORE["oauth_tokens"]:
|
58 |
+
token_data = SESSION_STORE["oauth_tokens"][provider]
|
59 |
+
# Check if token is expired (1 hour)
|
60 |
+
if time.time() - token_data["timestamp"] < 3600:
|
61 |
+
return token_data["token"]
|
62 |
+
else:
|
63 |
+
# Token expired, remove it
|
64 |
+
del SESSION_STORE["oauth_tokens"][provider]
|
65 |
+
logger.info(f"{provider} token expired and removed")
|
66 |
+
return None
|
67 |
+
|
68 |
# Initialize the components
|
69 |
+
async def initialize_components():
|
70 |
+
load_dotenv(override=True)
|
71 |
|
72 |
from src.search.search_engine import SearchEngine
|
73 |
from src.query_processing.query_processor import QueryProcessor
|
|
|
78 |
from src.crawl.crawler import CustomCrawler
|
79 |
from src.utils.api_key_manager import APIKeyManager
|
80 |
from src.query_processing.late_chunking.late_chunker import LateChunker
|
81 |
+
from src.integrations.mcp_client import MCPClient
|
82 |
+
|
83 |
+
state = SESSION_STORE
|
84 |
|
85 |
manager = APIKeyManager()
|
86 |
manager._reinit()
|
87 |
+
state['search_engine'] = SearchEngine()
|
88 |
+
state['query_processor'] = QueryProcessor()
|
89 |
+
state['crawler'] = CustomCrawler(max_concurrent_requests=1000)
|
90 |
+
# state['graph_rag'] = Neo4jGraphRAG(num_workers=os.cpu_count() * 2)
|
91 |
+
state['graph_rag'] = GraphRAG(num_workers=os.cpu_count() * 2)
|
92 |
+
state['evaluator'] = Evaluator()
|
93 |
+
state['reasoner'] = Reasoner()
|
94 |
+
state['model'] = manager.get_llm()
|
95 |
+
state['late_chunker'] = LateChunker()
|
96 |
+
state["mcp_client"] = MCPClient()
|
97 |
+
|
98 |
+
state["initialized"] = True
|
99 |
+
state["session_id"] = await state["crawler"].create_session()
|
100 |
+
|
101 |
+
# Main function to process user queries
|
102 |
async def process_query(user_query: str, sse_queue: asyncio.Queue):
|
103 |
state = SESSION_STORE
|
104 |
|
105 |
try:
|
106 |
+
# --- Categorize the query ---
|
107 |
category = await state["query_processor"].classify_query(user_query)
|
108 |
cat_lower = category.lower().strip()
|
|
|
|
|
|
|
|
|
109 |
user_query = re.sub(r'category:.*', '', user_query, flags=re.IGNORECASE).strip()
|
110 |
|
111 |
+
# --- Read and extract user-provided files and links ---
|
112 |
+
# Initialize caches if not present
|
113 |
+
if "user_files_cache" not in state:
|
114 |
+
state["user_files_cache"] = {}
|
115 |
+
if "user_links_cache" not in state:
|
116 |
+
state["user_links_cache"] = {}
|
117 |
+
|
118 |
+
# Extract user-provided context
|
119 |
+
user_context = ""
|
120 |
+
user_links = state.get("user_provided_links", [])
|
121 |
+
|
122 |
+
# Read new uploaded files
|
123 |
+
if state["session_id"]:
|
124 |
+
session_upload_path = os.path.join(UPLOAD_DIRECTORY, state["session_id"])
|
125 |
+
if os.path.exists(session_upload_path):
|
126 |
+
for filename in os.listdir(session_upload_path):
|
127 |
+
file_path = os.path.join(session_upload_path, filename)
|
128 |
+
if os.path.isfile(file_path):
|
129 |
+
# Check if file is already in cache
|
130 |
+
if filename not in state["user_files_cache"]:
|
131 |
+
try:
|
132 |
+
await sse_queue.put(("step", "Reading User-Provided Files..."))
|
133 |
+
with open(file_path, 'r', encoding='utf-8') as f:
|
134 |
+
file_content = f.read()
|
135 |
+
state["user_files_cache"][filename] = file_content
|
136 |
+
except Exception as e:
|
137 |
+
logger.error(f"Error reading file {filename}: {e}")
|
138 |
+
# Try reading as binary and decode
|
139 |
+
try:
|
140 |
+
with open(file_path, 'rb') as f:
|
141 |
+
file_content = f.read().decode('utf-8', errors='ignore')
|
142 |
+
state["user_files_cache"][filename] = file_content
|
143 |
+
except Exception as e2:
|
144 |
+
logger.error(f"Error reading file {filename} as binary: {e2}")
|
145 |
+
state["user_files_cache"][filename] = "" # Cache empty to avoid retrying
|
146 |
+
|
147 |
+
# Add all cached file contents
|
148 |
+
for filename, content in state["user_files_cache"].items():
|
149 |
+
if content:
|
150 |
+
user_context += f"\n[USER PROVIDED FILE: {filename} START]\n{content}\n[USER PROVIDED FILE: {filename} END]\n\n"
|
151 |
+
|
152 |
+
# Crawl new user-provided links
|
153 |
+
if user_links:
|
154 |
+
await sse_queue.put(("step", "Crawling User-Provided Links..."))
|
155 |
+
new_links = [link for link in user_links if link not in state["user_links_cache"]]
|
156 |
+
|
157 |
+
if new_links:
|
158 |
+
# Only crawl new links
|
159 |
+
link_contents = await state['crawler'].fetch_page_contents(
|
160 |
+
new_links,
|
161 |
+
user_query,
|
162 |
+
state["session_id"],
|
163 |
+
max_attempts=1
|
164 |
+
)
|
165 |
+
# Cache the new contents
|
166 |
+
for link, content in zip(new_links, link_contents):
|
167 |
+
if not isinstance(content, Exception) and content:
|
168 |
+
state["user_links_cache"][link] = content
|
169 |
+
else:
|
170 |
+
state["user_links_cache"][link] = "" # Cache empty to avoid retrying
|
171 |
+
|
172 |
+
# Add all cached link contents
|
173 |
+
for link, content in state["user_links_cache"].items():
|
174 |
+
if content:
|
175 |
+
idx = user_links.index(link) + 1 if link in user_links else 0
|
176 |
+
user_context += f"\n[USER PROVIDED LINK {idx} START]\n{content}\n[USER PROVIDED LINK {idx} END]\n\n"
|
177 |
+
|
178 |
+
# --- Fetch apps data from MCP service ---
|
179 |
+
app_context = ""
|
180 |
+
selected_services = state.get("selected_services", {})
|
181 |
+
|
182 |
+
# Check if any services are selected
|
183 |
+
has_google = selected_services.get("google", [])
|
184 |
+
has_microsoft = selected_services.get("microsoft", [])
|
185 |
+
has_slack = selected_services.get("slack", False)
|
186 |
+
|
187 |
+
if has_google or has_microsoft or has_slack:
|
188 |
+
await sse_queue.put(("step", "Fetching Data From Connected Apps..."))
|
189 |
+
|
190 |
+
# Fetch from each provider in parallel
|
191 |
+
tasks = []
|
192 |
+
|
193 |
+
# Google services
|
194 |
+
if has_google and len(has_google) > 0:
|
195 |
+
google_token = get_oauth_token("google")
|
196 |
+
tasks.append(
|
197 |
+
state['mcp_client'].fetch_app_data(
|
198 |
+
provider="google",
|
199 |
+
services=has_google,
|
200 |
+
query=user_query,
|
201 |
+
user_id=state["session_id"],
|
202 |
+
access_token=google_token
|
203 |
+
)
|
204 |
+
)
|
205 |
+
|
206 |
+
# Microsoft services
|
207 |
+
if has_microsoft and len(has_microsoft) > 0:
|
208 |
+
microsoft_token = get_oauth_token("microsoft")
|
209 |
+
tasks.append(
|
210 |
+
state['mcp_client'].fetch_app_data(
|
211 |
+
provider="microsoft",
|
212 |
+
services=has_microsoft,
|
213 |
+
query=user_query,
|
214 |
+
user_id=state["session_id"],
|
215 |
+
access_token=microsoft_token
|
216 |
+
)
|
217 |
+
)
|
218 |
+
|
219 |
+
# Slack
|
220 |
+
if has_slack:
|
221 |
+
slack_token = get_oauth_token("slack")
|
222 |
+
tasks.append(
|
223 |
+
state['mcp_client'].fetch_app_data(
|
224 |
+
provider="slack",
|
225 |
+
services=["messages"], # Slack doesn't have sub-services
|
226 |
+
query=user_query,
|
227 |
+
user_id=state["session_id"],
|
228 |
+
access_token=slack_token
|
229 |
+
)
|
230 |
+
)
|
231 |
+
|
232 |
+
# Execute all requests in parallel
|
233 |
+
if tasks:
|
234 |
+
results = await asyncio.gather(*tasks, return_exceptions=True)
|
235 |
+
|
236 |
+
# Process results
|
237 |
+
for i, result in enumerate(results):
|
238 |
+
if isinstance(result, Exception):
|
239 |
+
logger.error(f"Error fetching app data: {result}")
|
240 |
+
elif isinstance(result, dict):
|
241 |
+
# Determine which provider this result is from
|
242 |
+
if i == 0 and has_google:
|
243 |
+
provider = "google"
|
244 |
+
elif (i == 1 and has_microsoft) or (i == 0 and not has_google and has_microsoft):
|
245 |
+
provider = "microsoft"
|
246 |
+
else:
|
247 |
+
provider = "slack"
|
248 |
+
|
249 |
+
# Format the data
|
250 |
+
formatted_context = state['mcp_client'].format_as_context(provider, result)
|
251 |
+
if formatted_context:
|
252 |
+
app_context += formatted_context
|
253 |
+
|
254 |
+
# Log how much app data we got
|
255 |
+
if app_context:
|
256 |
+
logger.info(f"Retrieved app data: {len(app_context)} characters")
|
257 |
+
|
258 |
+
# Prepend app context to user context
|
259 |
+
if app_context:
|
260 |
+
user_context = app_context + "\n\n" + user_context
|
261 |
+
|
262 |
+
# Upgrade basic to advanced if user has provided links
|
263 |
+
if cat_lower == "basic" and user_links:
|
264 |
+
cat_lower = "advanced"
|
265 |
+
|
266 |
+
# --- Process the query based on the category ---
|
267 |
if cat_lower == "basic":
|
268 |
response = ""
|
269 |
chunk_counter = 1
|
270 |
+
if user_context: # Include user context if available
|
271 |
+
await sse_queue.put(("step", "Generating Response..."))
|
272 |
+
async for chunk in state["reasoner"].answer(user_query, user_context, query_type="basic"):
|
273 |
+
await sse_queue.put(("token", json.dumps({"chunk": chunk, "index": chunk_counter})))
|
274 |
+
response += chunk
|
275 |
+
chunk_counter += 1
|
276 |
+
else: # No user context provided
|
277 |
+
async for chunk in state["reasoner"].answer(user_query):
|
278 |
+
await sse_queue.put(("token", json.dumps({"chunk": chunk, "index": chunk_counter})))
|
279 |
+
response += chunk
|
280 |
+
chunk_counter += 1
|
281 |
|
282 |
await sse_queue.put(("final_message", response))
|
283 |
SESSION_STORE["chat_history"].append({"query": user_query, "response": response})
|
|
|
307 |
max_attempts=1
|
308 |
)
|
309 |
|
310 |
+
# Start with user-provided context
|
311 |
+
contents = user_context
|
312 |
+
|
313 |
+
# Add crawled contents
|
314 |
if search_contents:
|
315 |
for k, content in enumerate(search_contents, 1):
|
316 |
if isinstance(content, Exception):
|
317 |
print(f"Error fetching content: {content}")
|
318 |
elif content:
|
319 |
+
contents += f"[SOURCE {k} START]\n{content}\n[SOURCE {k} END]\n\n"
|
320 |
|
321 |
if len(contents.strip()) > 0:
|
322 |
await sse_queue.put(("step", "Generating Response..."))
|
|
|
329 |
|
330 |
response = ""
|
331 |
chunk_counter = 1
|
332 |
+
async for chunk in state["reasoner"].answer(user_query, contents):
|
333 |
await sse_queue.put(("token", json.dumps({"chunk": chunk, "index": chunk_counter})))
|
334 |
response += chunk
|
335 |
chunk_counter += 1
|
336 |
|
337 |
+
sources_for_answer = []
|
338 |
+
for idx, result in enumerate(search_results, 1):
|
339 |
+
if search_contents[idx-1]: # Only include if content was successfully fetched
|
340 |
+
sources_for_answer.append({
|
341 |
+
"id": idx,
|
342 |
+
"title": result.get('title', 'No Title'),
|
343 |
+
"link": result.get('link', 'No URL')
|
344 |
+
}
|
345 |
+
)
|
346 |
+
|
347 |
await sse_queue.put(("final_message", response))
|
348 |
+
await sse_queue.put(("final_sources", json.dumps(sources_for_answer)))
|
349 |
+
|
350 |
SESSION_STORE["chat_history"].append({"query": user_query, "response": response})
|
351 |
+
SESSION_STORE["answer"] = response
|
352 |
+
SESSION_STORE["source_contents"] = contents
|
353 |
|
354 |
await sse_queue.put(("action", {
|
355 |
"name": "sources",
|
|
|
392 |
)
|
393 |
current_search_results.extend(filtered_urls)
|
394 |
|
395 |
+
# Combine search results with user-provided links
|
396 |
+
all_search_results = search_results + \
|
397 |
+
[{"link": url, "title": f"User provided: {url}", "snippet": ""} for url in user_links]
|
398 |
+
urls = [r.get('link', 'No URL') for r in all_search_results]
|
399 |
+
|
400 |
search_contents = await state['crawler'].fetch_page_contents(
|
401 |
urls,
|
402 |
sub_query,
|
|
|
405 |
)
|
406 |
current_search_contents.extend(search_contents)
|
407 |
|
408 |
+
contents = user_context
|
409 |
if search_contents:
|
410 |
for k, c in enumerate(search_contents, 1):
|
411 |
if isinstance(c, Exception):
|
412 |
logger.info(f"Error fetching content: {c}")
|
413 |
elif c:
|
414 |
+
contents += f"[SOURCE {k} START]\n{c}\n[SOURCE {k} END]\n\n"
|
415 |
|
416 |
if len(contents.strip()) > 0:
|
417 |
await sse_queue.put(("task", (sub_query, "DONE")))
|
|
|
432 |
results = await asyncio.gather(*tasks)
|
433 |
end = time.time()
|
434 |
|
435 |
+
# Start with user-provided context
|
436 |
+
contents = user_context
|
437 |
+
|
438 |
+
# Add searched contents
|
439 |
+
contents += "\n\n".join(r for r in results if r.strip())
|
440 |
|
441 |
unique_results = []
|
442 |
seen = set()
|
|
|
466 |
response = ""
|
467 |
chunk_counter = 1
|
468 |
is_first_chunk = True
|
469 |
+
async for chunk in state['reasoner'].answer(user_query, contents):
|
470 |
if is_first_chunk:
|
471 |
await sse_queue.put(("step", f"Thought and searched for {int(end - start)} seconds"))
|
472 |
is_first_chunk = False
|
|
|
475 |
response += chunk
|
476 |
chunk_counter += 1
|
477 |
|
478 |
+
sources_for_answer = []
|
479 |
+
for idx, (result, content) in enumerate(zip(current_search_results, current_search_contents), 1):
|
480 |
+
if content: # Only include if content was successfully fetched
|
481 |
+
sources_for_answer.append({
|
482 |
+
"id": idx,
|
483 |
+
"title": result.get('title', 'No Title'),
|
484 |
+
"link": result.get('link', 'No URL')
|
485 |
+
})
|
486 |
+
|
487 |
await sse_queue.put(("final_message", response))
|
488 |
+
await sse_queue.put(("final_sources", json.dumps(sources_for_answer)))
|
489 |
+
|
490 |
SESSION_STORE["chat_history"].append({"query": user_query, "response": response})
|
491 |
+
SESSION_STORE["answer"] = response
|
492 |
+
SESSION_STORE["source_contents"] = contents
|
493 |
|
494 |
await sse_queue.put(("action", {
|
495 |
"name": "sources",
|
|
|
551 |
if isinstance(c, Exception):
|
552 |
logger.info(f"Error fetching content: {c}")
|
553 |
elif c:
|
554 |
+
contents += f"[SOURCE {k} START]\n{c}\n[SOURCE {k} END]\n\n"
|
555 |
|
556 |
return contents
|
557 |
|
|
|
585 |
results = await asyncio.gather(*tasks)
|
586 |
end = time.time()
|
587 |
|
588 |
+
# Start with user-provided context
|
589 |
previous_contents = []
|
590 |
+
if user_context:
|
591 |
+
previous_contents.append(user_context)
|
592 |
+
|
593 |
for result in results:
|
594 |
if result:
|
595 |
for content in result:
|
|
|
625 |
response = ""
|
626 |
chunk_counter = 1
|
627 |
is_first_chunk = True
|
628 |
+
async for chunk in state['reasoner'].answer(user_query, contents):
|
629 |
if is_first_chunk:
|
630 |
await sse_queue.put(("step", f"Thought and searched for {int(end - start)} seconds"))
|
631 |
is_first_chunk = False
|
|
|
634 |
response += chunk
|
635 |
chunk_counter += 1
|
636 |
|
637 |
+
sources_for_answer = []
|
638 |
+
for idx, (result, content) in enumerate(zip(current_search_results, current_search_contents), 1):
|
639 |
+
if content: # Only include if content was successfully fetched
|
640 |
+
sources_for_answer.append({
|
641 |
+
"id": idx,
|
642 |
+
"title": result.get('title', 'No Title'),
|
643 |
+
"link": result.get('link', 'No URL')
|
644 |
+
})
|
645 |
+
|
646 |
await sse_queue.put(("final_message", response))
|
647 |
+
await sse_queue.put(("final_sources", json.dumps(sources_for_answer)))
|
648 |
+
|
649 |
SESSION_STORE["chat_history"].append({"query": user_query, "response": response})
|
650 |
+
SESSION_STORE["answer"] = response
|
651 |
+
SESSION_STORE["source_contents"] = contents
|
652 |
|
653 |
await sse_queue.put(("action", {
|
654 |
"name": "sources",
|
|
|
749 |
|
750 |
answer = state['graph_rag'].query_graph(user_query)
|
751 |
if answer:
|
752 |
+
# Start with user-provided context
|
753 |
+
previous_contents = []
|
754 |
+
if user_context:
|
755 |
+
previous_contents.append(user_context)
|
756 |
+
|
757 |
token_count = state['model'].get_num_tokens(answer)
|
758 |
if token_count > MAX_TOKENS_ALLOWED:
|
759 |
answer = await state['late_chunker'].chunker(
|
|
|
769 |
response = ""
|
770 |
chunk_counter = 1
|
771 |
is_first_chunk = True
|
772 |
+
async for chunk in state['reasoner'].answer(user_query, answer):
|
773 |
if is_first_chunk:
|
774 |
await sse_queue.put(("step", f"Thought and searched for {int(end - start)} seconds"))
|
775 |
is_first_chunk = False
|
|
|
778 |
response += chunk
|
779 |
chunk_counter += 1
|
780 |
|
781 |
+
sources_for_answer = []
|
782 |
+
for idx, (result, content) in enumerate(zip(current_search_results, current_search_contents), 1):
|
783 |
+
if content: # Only include if content was successfully fetched
|
784 |
+
sources_for_answer.append({
|
785 |
+
"id": idx,
|
786 |
+
"title": result.get('title', 'No Title'),
|
787 |
+
"link": result.get('link', 'No URL')
|
788 |
+
})
|
789 |
+
|
790 |
await sse_queue.put(("final_message", response))
|
791 |
+
await sse_queue.put(("final_sources", json.dumps(sources_for_answer)))
|
792 |
+
|
793 |
SESSION_STORE["chat_history"].append({"query": user_query, "response": response})
|
794 |
+
SESSION_STORE["answer"] = response
|
795 |
+
SESSION_STORE["source_contents"] = contents
|
796 |
|
797 |
await sse_queue.put(("action", {
|
798 |
"name": "sources",
|
|
|
816 |
|
817 |
except Exception as e:
|
818 |
await sse_queue.put(("error", str(e)))
|
819 |
+
traceback.print_exc()
|
820 |
stop()
|
821 |
|
822 |
# Create a FastAPI app
|
|
|
876 |
return {"result": sources}
|
877 |
except Exception as e:
|
878 |
return JSONResponse(content={"error": str(e)}, status_code=500)
|
879 |
+
|
880 |
# Define the route for graph action to display the graph
|
881 |
@app.post("/action/graph")
|
882 |
def action_graph() -> Dict[str, Any]:
|
883 |
state = SESSION_STORE
|
884 |
+
|
885 |
try:
|
886 |
html_str = state['graph_rag'].display_graph()
|
887 |
|
|
|
905 |
return {"result": result}
|
906 |
except Exception as e:
|
907 |
return JSONResponse(content={"error": str(e)}, status_code=500)
|
908 |
+
|
909 |
+
# Define the route for excerpts action to display excerpts from the sources
|
910 |
+
@app.post("/action/excerpts")
|
911 |
+
async def action_excerpts() -> Dict[str, Any]:
|
912 |
+
def validate_excerpts_format(excerpts):
|
913 |
+
if not isinstance(excerpts, list):
|
914 |
+
return False
|
915 |
+
for item in excerpts:
|
916 |
+
if not isinstance(item, dict):
|
917 |
+
return False
|
918 |
+
for statement, sources in item.items():
|
919 |
+
if not isinstance(statement, str) or not isinstance(sources, dict):
|
920 |
+
return False
|
921 |
+
for src_num, excerpt in sources.items():
|
922 |
+
if not (isinstance(src_num, int) or isinstance(src_num, str)):
|
923 |
+
return False
|
924 |
+
if not isinstance(excerpt, str):
|
925 |
+
return False
|
926 |
+
return True
|
927 |
+
|
928 |
+
try:
|
929 |
+
state = SESSION_STORE
|
930 |
+
response = state["answer"]
|
931 |
+
contents = state["source_contents"]
|
932 |
|
933 |
+
if not response or not contents:
|
934 |
+
raise ValueError("Required data for excerpts not found")
|
935 |
+
|
936 |
+
excerpts_list = await state["reasoner"].get_excerpts(response, contents)
|
937 |
+
cleaned_excerpts = re.sub(
|
938 |
+
r'```[\w\s]*\n?|```|~~~[\w\s]*\n?|~~~', '', excerpts_list, flags=re.MULTILINE | re.DOTALL
|
939 |
+
).strip()
|
940 |
+
|
941 |
+
try:
|
942 |
+
excerpts = eval(cleaned_excerpts)
|
943 |
+
except Exception:
|
944 |
+
print(f"Error parsing excerpts:\n{cleaned_excerpts}")
|
945 |
+
raise ValueError("Excerpts could not be parsed as a Python list.")
|
946 |
+
|
947 |
+
if not validate_excerpts_format(excerpts):
|
948 |
+
print(f"Excerpts format validation failed:\n{excerpts}")
|
949 |
+
raise ValueError("Excerpts are not in the required format.")
|
950 |
+
|
951 |
+
print(f"Excerpts:\n{excerpts}")
|
952 |
+
return {"result": excerpts}
|
953 |
+
except Exception as e:
|
954 |
+
print(f"Error in action_excerpts: {e}")
|
955 |
+
return JSONResponse(content={"error": str(e)}, status_code=500)
|
956 |
+
|
957 |
+
# Define the route for settings to set or update the environment variables
|
958 |
@app.post("/settings")
|
959 |
async def update_settings(data: Dict[str, Any]):
|
960 |
from src.helpers.helper import (
|
|
|
968 |
multiple_api_keys = data.get("Model_API_Keys", "").strip()
|
969 |
brave_api_key = data.get("Brave_Search_API_Key", "").strip()
|
970 |
proxy_list = data.get("Proxy_List", "").strip()
|
|
|
|
|
|
|
971 |
model_temperature = str(data.get("Model_Temperature", 0.0))
|
972 |
model_top_p = str(data.get("Model_Top_P", 1.0))
|
973 |
|
|
|
981 |
env_updates.update(px)
|
982 |
|
983 |
env_updates["BRAVE_API_KEY"] = brave_api_key
|
|
|
|
|
|
|
984 |
env_updates["MODEL_PROVIDER"] = prov_lower
|
985 |
env_updates["MODEL_NAME"] = model_name
|
986 |
env_updates["MODEL_TEMPERATURE"] = model_temperature
|
|
|
988 |
|
989 |
update_env_vars(env_updates)
|
990 |
load_dotenv(override=True)
|
991 |
+
await initialize_components()
|
992 |
|
993 |
return {"success": True}
|
994 |
|
995 |
+
# Define the route for adding/uploading content for a specific session
|
996 |
+
@app.post("/add-content")
|
997 |
+
async def add_content(files: Optional[List[UploadFile]] = File(None), urls: str = Form(...)):
|
998 |
+
state = SESSION_STORE
|
999 |
+
session_id = state.get("session_id")
|
1000 |
+
|
1001 |
+
if not session_id:
|
1002 |
+
raise HTTPException(status_code=400, detail="Session ID is not set. Please start a session first.")
|
1003 |
+
|
1004 |
+
session_upload_path = os.path.join(UPLOAD_DIRECTORY, session_id)
|
1005 |
+
os.makedirs(session_upload_path, exist_ok=True)
|
1006 |
+
|
1007 |
+
saved_filenames = []
|
1008 |
+
if files:
|
1009 |
+
total_new_files_size = sum(file.size for file in files)
|
1010 |
+
current_folder_size = get_folder_size(session_upload_path)
|
1011 |
+
|
1012 |
+
# Check if the total size exceeds the maximum allowed folder size
|
1013 |
+
if current_folder_size + total_new_files_size > MAX_FOLDER_SIZE:
|
1014 |
+
raise HTTPException(
|
1015 |
+
status_code=400,
|
1016 |
+
detail=f"Cannot add files as total storage would exceed 10 MB. Current size: {current_folder_size / (1024 * 1024):.2f} MB"
|
1017 |
+
)
|
1018 |
+
|
1019 |
+
for file in files:
|
1020 |
+
file_path = os.path.join(session_upload_path, file.filename)
|
1021 |
+
try:
|
1022 |
+
with open(file_path, "wb") as buffer:
|
1023 |
+
shutil.copyfileobj(file.file, buffer)
|
1024 |
+
saved_filenames.append(file.filename)
|
1025 |
+
finally:
|
1026 |
+
file.file.close()
|
1027 |
+
|
1028 |
+
try:
|
1029 |
+
parsed_urls = json.loads(urls)
|
1030 |
+
print(f"Received links: {parsed_urls}")
|
1031 |
+
except json.JSONDecodeError:
|
1032 |
+
raise HTTPException(status_code=400, detail="Invalid URL format.")
|
1033 |
+
|
1034 |
+
# Store user-provided links in session
|
1035 |
+
if parsed_urls:
|
1036 |
+
SESSION_STORE["user_provided_links"] = parsed_urls
|
1037 |
+
|
1038 |
+
return {
|
1039 |
+
"message": "Content added successfully",
|
1040 |
+
"files_added": saved_filenames,
|
1041 |
+
"links_added": parsed_urls
|
1042 |
+
}
|
1043 |
+
|
1044 |
+
# Define the route to update the selected services for searching
|
1045 |
+
@app.post("/api/selected-services")
|
1046 |
+
async def update_selected_services(data: Dict[str, Any]):
|
1047 |
+
state = SESSION_STORE
|
1048 |
+
|
1049 |
+
selected_services = data.get("services", {})
|
1050 |
+
state["selected_services"] = selected_services
|
1051 |
+
|
1052 |
+
logger.info(f"Updated selected services: {selected_services}")
|
1053 |
+
|
1054 |
+
return {"success": True, "services": selected_services}
|
1055 |
+
|
1056 |
+
# Define the route to receive OAuth tokens from the frontend
|
1057 |
+
@app.post("/api/session-token")
|
1058 |
+
async def receive_session_token(data: Dict[str, Any]):
|
1059 |
+
provider = data.get("provider") # 'google', 'microsoft', 'slack'
|
1060 |
+
token = data.get("token")
|
1061 |
+
|
1062 |
+
if not provider or not token:
|
1063 |
+
raise HTTPException(status_code=400, detail="Provider and token are required")
|
1064 |
+
|
1065 |
+
SESSION_STORE["oauth_tokens"][provider] = {
|
1066 |
+
"token": token,
|
1067 |
+
"timestamp": time.time()
|
1068 |
+
}
|
1069 |
+
|
1070 |
+
logger.info(f"Stored token {token} for provider {provider}")
|
1071 |
+
|
1072 |
+
return {"success": True, "message": f"{provider} token stored successfully"}
|
1073 |
+
|
1074 |
+
# Define the route for cleaning up a session if the session ID matches
|
1075 |
+
@app.post("/cleanup")
|
1076 |
+
async def cleanup_session():
|
1077 |
+
state = SESSION_STORE
|
1078 |
+
session_id = state.get("session_id")
|
1079 |
+
|
1080 |
+
if not session_id:
|
1081 |
+
raise HTTPException(status_code=400, detail="Session ID is not set. Please start a session first.")
|
1082 |
+
|
1083 |
+
session_upload_path = os.path.join(UPLOAD_DIRECTORY, session_id)
|
1084 |
+
if session_id:
|
1085 |
+
# Clear the session upload directory
|
1086 |
+
clear_folder(session_upload_path)
|
1087 |
+
|
1088 |
+
# Clear user-provided links and caches
|
1089 |
+
SESSION_STORE["user_provided_links"] = []
|
1090 |
+
SESSION_STORE["user_files_cache"] = {}
|
1091 |
+
SESSION_STORE["user_links_cache"] = {}
|
1092 |
+
SESSION_STORE["selected_services"] = {}
|
1093 |
+
SESSION_STORE["oauth_tokens"] = {}
|
1094 |
+
|
1095 |
+
return {"message": "Cleanup successful."}
|
1096 |
+
|
1097 |
+
return {"message": "No session ID provided, cleanup skipped."}
|
1098 |
+
|
1099 |
@app.on_event("startup")
|
1100 |
def init_chat():
|
1101 |
if not SESSION_STORE:
|
1102 |
print("Initializing chat...")
|
1103 |
|
1104 |
+
# Create the upload directory if it doesn't exist
|
1105 |
+
print("Creating upload directory...")
|
1106 |
+
os.makedirs(UPLOAD_DIRECTORY, exist_ok=True)
|
1107 |
+
|
1108 |
+
# Initialize the session store
|
1109 |
SESSION_STORE["settings_saved"] = False
|
1110 |
SESSION_STORE["session_id"] = None
|
1111 |
+
SESSION_STORE["answer"] = None
|
1112 |
+
SESSION_STORE["source_contents"] = None
|
1113 |
SESSION_STORE["chat_history"] = []
|
1114 |
+
SESSION_STORE["user_provided_links"] = []
|
1115 |
+
SESSION_STORE["user_files_cache"] = {}
|
1116 |
+
SESSION_STORE["user_links_cache"] = {}
|
1117 |
+
SESSION_STORE["selected_services"] = {}
|
1118 |
+
SESSION_STORE["oauth_tokens"] = {}
|
1119 |
|
1120 |
print("Chat initialized!")
|
|
|
1121 |
return {"sucess": True}
|
1122 |
else:
|
1123 |
print("Chat already initialized!")
|
src/crawl/crawler.py
CHANGED
@@ -1,17 +1,17 @@
|
|
1 |
-
|
2 |
-
|
3 |
import aiohttp
|
4 |
import asyncio
|
5 |
-
|
6 |
from fast_async import make_async
|
7 |
from bs4 import BeautifulSoup, NavigableString
|
8 |
-
|
9 |
-
|
10 |
-
|
11 |
import os
|
12 |
import re
|
13 |
import uuid
|
14 |
-
from typing import List, Dict, Optional
|
15 |
from io import BytesIO
|
16 |
import PyPDF2
|
17 |
from fake_useragent import FakeUserAgent
|
@@ -20,597 +20,597 @@ from transformers import AutoTokenizer, AutoConfig
|
|
20 |
import torch
|
21 |
import time
|
22 |
|
23 |
-
|
24 |
-
|
25 |
-
|
26 |
-
|
27 |
-
|
28 |
-
|
29 |
-
|
30 |
-
|
31 |
-
|
32 |
-
|
33 |
-
|
34 |
-
|
35 |
-
|
36 |
-
#
|
37 |
-
|
38 |
-
|
39 |
-
|
40 |
-
|
41 |
-
|
42 |
-
|
43 |
-
|
44 |
|
45 |
-
|
46 |
-
|
47 |
-
|
48 |
-
|
49 |
-
|
50 |
|
51 |
-
|
52 |
-
|
53 |
-
|
54 |
-
|
55 |
-
|
56 |
-
|
57 |
-
|
58 |
-
|
59 |
-
|
60 |
-
|
61 |
-
|
62 |
-
|
63 |
-
|
64 |
-
|
65 |
-
|
66 |
-
|
67 |
-
|
68 |
-
|
69 |
-
|
70 |
-
|
71 |
-
|
72 |
-
|
73 |
-
|
74 |
-
|
75 |
-
|
76 |
-
|
77 |
-
|
78 |
-
|
79 |
-
|
80 |
-
|
81 |
-
|
82 |
-
|
83 |
-
|
84 |
-
|
85 |
-
|
86 |
-
|
87 |
-
|
88 |
-
|
89 |
-
|
90 |
-
|
91 |
-
|
92 |
|
93 |
-
#
|
94 |
-
|
95 |
-
|
96 |
-
|
97 |
|
98 |
-
|
99 |
-
#
|
100 |
-
|
101 |
-
|
102 |
|
103 |
-
#
|
104 |
-
|
105 |
-
|
106 |
-
|
107 |
-
|
108 |
-
|
109 |
-
|
110 |
-
|
111 |
-
|
112 |
-
#
|
113 |
-
|
114 |
-
|
115 |
-
|
116 |
-
|
117 |
-
|
118 |
-
|
119 |
-
#
|
120 |
-
|
121 |
-
|
122 |
-
|
123 |
-
|
124 |
-
|
125 |
-
|
126 |
-
|
127 |
-
|
128 |
|
129 |
-
|
130 |
-
|
131 |
-
|
132 |
-
|
133 |
-
|
134 |
-
|
135 |
-
|
136 |
-
|
137 |
-
|
138 |
-
|
139 |
-
|
140 |
-
|
141 |
-
|
142 |
-
|
143 |
-
|
144 |
-
|
145 |
|
146 |
-
#
|
147 |
-
|
148 |
-
|
149 |
-
|
150 |
-
|
151 |
-
|
152 |
-
|
153 |
|
154 |
-
|
155 |
-
|
156 |
-
|
157 |
-
|
158 |
-
|
159 |
-
|
160 |
-
|
161 |
-
#
|
162 |
-
|
163 |
-
|
164 |
-
|
165 |
-
|
166 |
-
|
167 |
-
|
168 |
|
169 |
-
|
170 |
-
|
171 |
-
|
172 |
-
|
173 |
-
|
174 |
-
|
175 |
-
|
176 |
-
#
|
177 |
-
|
178 |
-
|
179 |
-
|
180 |
-
|
181 |
-
|
182 |
-
|
183 |
-
|
184 |
|
185 |
-
|
186 |
-
|
187 |
-
|
188 |
-
|
189 |
-
|
190 |
-
|
191 |
-
|
192 |
-
|
193 |
-
|
194 |
-
|
195 |
-
|
196 |
-
|
197 |
-
|
198 |
-
|
199 |
-
|
200 |
-
|
201 |
|
202 |
-
#
|
203 |
-
|
204 |
-
|
205 |
-
|
206 |
-
|
207 |
-
|
208 |
-
|
209 |
-
|
210 |
-
|
211 |
|
212 |
-
|
213 |
-
|
214 |
-
|
215 |
-
|
216 |
-
|
217 |
-
|
218 |
-
|
219 |
-
|
220 |
-
#
|
221 |
-
|
222 |
-
|
223 |
-
|
224 |
-
|
225 |
-
|
226 |
-
|
227 |
-
|
228 |
-
|
229 |
|
230 |
-
|
231 |
-
|
232 |
-
|
233 |
-
|
234 |
-
|
235 |
-
|
236 |
-
|
237 |
-
|
238 |
-
#
|
239 |
-
|
240 |
-
|
241 |
-
|
242 |
-
|
243 |
-
|
244 |
-
|
245 |
|
246 |
-
|
247 |
-
|
248 |
-
|
249 |
-
|
250 |
-
|
251 |
-
#
|
252 |
-
|
253 |
-
|
254 |
-
|
255 |
-
|
256 |
-
|
257 |
-
|
258 |
-
|
259 |
-
|
260 |
-
|
261 |
-
#
|
262 |
-
|
263 |
-
|
264 |
-
|
265 |
-
|
266 |
-
|
267 |
-
|
268 |
-
|
269 |
|
270 |
-
|
271 |
-
|
272 |
-
|
273 |
-
|
274 |
-
|
275 |
-
|
276 |
-
|
277 |
-
|
278 |
-
|
279 |
-
|
280 |
-
|
281 |
-
|
282 |
-
|
283 |
-
|
284 |
-
|
285 |
-
|
286 |
-
#
|
287 |
-
|
288 |
-
|
289 |
-
|
290 |
-
|
291 |
-
|
292 |
-
|
293 |
-
|
294 |
-
|
295 |
-
|
296 |
-
#
|
297 |
-
|
298 |
-
|
299 |
-
|
300 |
-
|
301 |
-
|
302 |
-
|
303 |
-
|
304 |
-
|
305 |
-
|
306 |
-
|
307 |
-
|
308 |
-
|
309 |
-
|
310 |
-
|
311 |
-
|
312 |
-
|
313 |
-
#
|
314 |
-
|
315 |
-
|
316 |
-
|
317 |
-
|
318 |
-
|
319 |
-
|
320 |
-
|
321 |
-
|
322 |
-
|
323 |
-
|
324 |
-
|
325 |
-
|
326 |
-
|
327 |
-
|
328 |
-
#
|
329 |
-
|
330 |
-
|
331 |
-
|
332 |
-
|
333 |
-
|
334 |
-
|
335 |
-
|
336 |
-
|
337 |
-
|
338 |
-
|
339 |
-
#
|
340 |
-
|
341 |
-
|
342 |
-
|
343 |
-
|
344 |
-
|
345 |
-
|
346 |
-
|
347 |
-
|
348 |
-
|
349 |
-
|
350 |
-
#
|
351 |
-
|
352 |
-
|
353 |
-
|
354 |
-
|
355 |
-
|
356 |
-
|
357 |
-
|
358 |
-
|
359 |
-
#
|
360 |
-
|
361 |
-
|
362 |
-
|
363 |
|
364 |
-
|
365 |
|
366 |
-
#
|
367 |
-
|
368 |
|
369 |
-
#
|
370 |
-
|
371 |
-
|
372 |
-
|
373 |
|
374 |
-
#
|
375 |
-
|
376 |
-
|
377 |
-
|
378 |
-
|
379 |
-
|
380 |
-
|
381 |
-
|
382 |
-
|
383 |
-
|
384 |
-
|
385 |
-
|
386 |
-
|
387 |
-
|
388 |
-
|
389 |
-
|
390 |
-
|
391 |
-
|
392 |
-
|
393 |
-
#
|
394 |
-
|
395 |
-
|
396 |
-
|
397 |
-
|
398 |
-
|
399 |
-
|
400 |
-
|
401 |
-
|
402 |
-
#
|
403 |
-
|
404 |
-
|
405 |
-
|
406 |
-
|
407 |
-
|
408 |
-
|
409 |
-
|
410 |
-
|
411 |
-
|
412 |
-
|
413 |
-
#
|
414 |
-
|
415 |
-
|
416 |
-
|
417 |
-
|
418 |
-
|
419 |
-
|
420 |
-
|
421 |
-
|
422 |
-
|
423 |
-
|
424 |
-
|
425 |
-
|
426 |
-
|
427 |
|
428 |
-
#
|
429 |
-
|
430 |
-
|
431 |
-
|
432 |
-
#
|
433 |
-
|
434 |
-
|
435 |
-
|
436 |
-
|
437 |
-
|
438 |
-
|
439 |
-
|
440 |
-
|
441 |
-
|
442 |
-
|
443 |
-
|
444 |
-
|
445 |
-
|
446 |
-
|
447 |
-
|
448 |
-
|
449 |
-
#
|
450 |
-
|
451 |
-
|
452 |
-
|
453 |
-
|
454 |
-
|
455 |
-
|
456 |
-
|
457 |
-
|
458 |
-
|
459 |
-
#
|
460 |
-
|
461 |
-
|
462 |
-
|
463 |
-
#
|
464 |
-
|
465 |
-
|
466 |
-
|
467 |
-
|
468 |
-
|
469 |
-
|
470 |
-
|
471 |
-
|
472 |
-
|
473 |
-
|
474 |
-
|
475 |
-
|
476 |
-
|
477 |
-
|
478 |
-
|
479 |
-
|
480 |
-
|
481 |
-
|
482 |
-
|
483 |
-
|
484 |
-
|
485 |
-
|
486 |
-
|
487 |
-
#
|
488 |
-
|
489 |
-
|
490 |
-
|
491 |
-
|
492 |
-
|
493 |
|
494 |
-
|
495 |
-
|
496 |
-
|
497 |
-
|
498 |
-
|
499 |
-
|
500 |
|
501 |
-
|
502 |
-
|
503 |
|
504 |
-
|
505 |
|
506 |
-
|
507 |
-
|
508 |
-
|
509 |
-
|
510 |
-
|
511 |
-
|
512 |
-
|
513 |
-
|
514 |
-
|
515 |
-
|
516 |
-
|
517 |
-
|
518 |
-
|
519 |
-
|
520 |
-
|
521 |
-
|
522 |
-
|
523 |
-
|
524 |
-
|
525 |
-
|
526 |
-
|
527 |
-
|
528 |
-
|
529 |
-
|
530 |
-
|
531 |
-
|
532 |
-
|
533 |
-
|
534 |
-
|
535 |
-
|
536 |
-
|
537 |
-
|
538 |
-
|
539 |
-
|
540 |
-
|
541 |
-
|
542 |
-
|
543 |
-
|
544 |
-
|
545 |
-
|
546 |
-
|
547 |
-
|
548 |
-
|
549 |
-
|
550 |
-
|
551 |
-
#
|
552 |
-
|
553 |
-
|
554 |
|
555 |
-
|
556 |
-
|
557 |
-
|
558 |
-
|
559 |
-
|
560 |
-
|
561 |
-
|
562 |
-
|
563 |
-
|
564 |
-
|
565 |
-
|
566 |
-
|
567 |
-
|
568 |
-
|
569 |
-
|
570 |
-
|
571 |
-
|
572 |
-
|
573 |
|
574 |
-
|
575 |
-
|
576 |
-
|
577 |
-
|
578 |
-
|
579 |
-
|
580 |
-
|
581 |
-
|
582 |
-
|
583 |
-
|
584 |
-
|
585 |
-
|
586 |
|
587 |
-
|
588 |
-
#
|
589 |
-
|
590 |
|
591 |
-
#
|
592 |
-
|
593 |
-
|
594 |
|
595 |
-
#
|
596 |
-
|
597 |
-
|
598 |
|
599 |
-
|
600 |
-
|
601 |
-
|
602 |
|
603 |
-
|
604 |
-
|
605 |
-
|
606 |
-
|
607 |
-
|
608 |
-
|
609 |
-
|
610 |
|
611 |
-
|
612 |
-
|
613 |
-
|
614 |
|
615 |
class CustomCrawler:
|
616 |
def __init__(
|
|
|
1 |
+
from crawl4ai import AsyncWebCrawler
|
2 |
+
from urllib.parse import urlparse
|
3 |
import aiohttp
|
4 |
import asyncio
|
5 |
+
from asyncio.exceptions import TimeoutError as async_timeout
|
6 |
from fast_async import make_async
|
7 |
from bs4 import BeautifulSoup, NavigableString
|
8 |
+
import secrets
|
9 |
+
from datetime import datetime
|
10 |
+
import random
|
11 |
import os
|
12 |
import re
|
13 |
import uuid
|
14 |
+
from typing import List, Dict, Tuple, Optional
|
15 |
from io import BytesIO
|
16 |
import PyPDF2
|
17 |
from fake_useragent import FakeUserAgent
|
|
|
20 |
import torch
|
21 |
import time
|
22 |
|
23 |
+
class Crawler:
|
24 |
+
def __init__(self, user_dir=None, rate_limit=1, headless=True, verbose=False):
|
25 |
+
self.session_pool = {} # Track active sessions
|
26 |
+
self.verbose = verbose
|
27 |
+
self.rate_limit = rate_limit
|
28 |
+
self.user_dir = user_dir
|
29 |
+
self.headless = headless
|
30 |
+
self.crawler = AsyncWebCrawler(
|
31 |
+
context_options={"userDataDir": self.user_dir},
|
32 |
+
headless=self.headless,
|
33 |
+
verbose=self.verbose
|
34 |
+
)
|
35 |
+
|
36 |
+
# Browser context management
|
37 |
+
self._browser_contexts = {}
|
38 |
+
self._context_locks = {}
|
39 |
+
|
40 |
+
async def get_browser_context(self, session_id):
|
41 |
+
"""Get or create a browser context with proper locking"""
|
42 |
+
if session_id not in self._context_locks:
|
43 |
+
self._context_locks[session_id] = asyncio.Lock()
|
44 |
|
45 |
+
async with self._context_locks[session_id]:
|
46 |
+
if session_id not in self._browser_contexts:
|
47 |
+
context = await self.crawler.new_context()
|
48 |
+
self._browser_contexts[session_id] = context
|
49 |
+
return self._browser_contexts[session_id]
|
50 |
|
51 |
+
async def cleanup_browser_context(self, session_id):
|
52 |
+
"""Safely cleanup browser context"""
|
53 |
+
if session_id in self._context_locks:
|
54 |
+
async with self._context_locks[session_id]:
|
55 |
+
if session_id in self._browser_contexts:
|
56 |
+
try:
|
57 |
+
await asyncio.shield(
|
58 |
+
self._browser_contexts[session_id].close()
|
59 |
+
)
|
60 |
+
except Exception as e:
|
61 |
+
print(f"Error cleaning up browser context: {e}")
|
62 |
+
finally:
|
63 |
+
del self._browser_contexts[session_id]
|
64 |
+
|
65 |
+
def create_session(self):
|
66 |
+
"""Create a new session with secure ID"""
|
67 |
+
session_id = secrets.token_urlsafe(32) # Secure session ID
|
68 |
+
self.session_pool[session_id] = {
|
69 |
+
'created_at': datetime.now(),
|
70 |
+
'last_used': datetime.now(),
|
71 |
+
'requests_count': 0
|
72 |
+
}
|
73 |
+
return session_id
|
74 |
+
|
75 |
+
def rotate_session(self, session_id):
|
76 |
+
"""Implement session rotation logic"""
|
77 |
+
if self.session_pool[session_id]['requests_count'] > 100:
|
78 |
+
self.cleanup_session(session_id)
|
79 |
+
return self.create_session()
|
80 |
+
return session_id
|
81 |
+
|
82 |
+
def is_dynamic_page(self, html_content: str) -> Tuple[bool, Optional[str]]:
|
83 |
+
"""Analyzes HTML content to determine if a webpage is dynamically loaded"""
|
84 |
+
def _check_structural_indicators(soup: BeautifulSoup) -> Dict[str, int]:
|
85 |
+
"""Check structural indicators of dynamic content loading."""
|
86 |
+
scores = {
|
87 |
+
'empty_containers': 0,
|
88 |
+
'repeated_structures': 0,
|
89 |
+
'api_endpoints': 0,
|
90 |
+
'state_management': 0
|
91 |
+
}
|
92 |
|
93 |
+
# 1. Check for empty content containers
|
94 |
+
main_containers = soup.find_all(['main', 'div', 'section'],
|
95 |
+
class_=lambda x: x and any(term in str(x).lower()
|
96 |
+
for term in ['content', 'main', 'feed', 'list', 'container']))
|
97 |
|
98 |
+
for container in main_containers:
|
99 |
+
# Check if container is empty or has minimal content
|
100 |
+
if len(container.find_all()) < 3:
|
101 |
+
scores['empty_containers'] += 1
|
102 |
|
103 |
+
# Check for repeated similar structures (common in dynamic lists)
|
104 |
+
children = container.find_all(recursive=False)
|
105 |
+
if children:
|
106 |
+
first_child_class = children[0].get('class', [])
|
107 |
+
similar_siblings = [c for c in children[1:]
|
108 |
+
if c.get('class', []) == first_child_class]
|
109 |
+
if len(similar_siblings) > 0:
|
110 |
+
scores['repeated_structures'] += 1
|
111 |
+
|
112 |
+
# 2. Check for API endpoints in scripts
|
113 |
+
scripts = soup.find_all('script', {'src': True})
|
114 |
+
api_patterns = ['/api/', '/graphql', '/rest/', '/v1/', '/v2/']
|
115 |
+
for script in scripts:
|
116 |
+
if any(pattern in script['src'] for pattern in api_patterns):
|
117 |
+
scores['api_endpoints'] += 1
|
118 |
+
|
119 |
+
# 3. Look for state management setup
|
120 |
+
state_patterns = [
|
121 |
+
r'window\.__INITIAL_STATE__',
|
122 |
+
r'window\.__PRELOADED_STATE__',
|
123 |
+
r'__REDUX_STATE__',
|
124 |
+
r'__NUXT__',
|
125 |
+
r'__NEXT_DATA__',
|
126 |
+
r'window\.__data'
|
127 |
+
]
|
128 |
|
129 |
+
inline_scripts = soup.find_all('script')
|
130 |
+
for script in inline_scripts:
|
131 |
+
if script.string:
|
132 |
+
for pattern in state_patterns:
|
133 |
+
if re.search(pattern, script.string):
|
134 |
+
scores['state_management'] += 1
|
135 |
+
|
136 |
+
return scores
|
137 |
+
|
138 |
+
def _check_modern_framework_indicators(soup: BeautifulSoup) -> Dict[str, int]:
|
139 |
+
"""Check for indicators of modern web frameworks and dynamic loading patterns."""
|
140 |
+
scores = {
|
141 |
+
'framework_roots': 0,
|
142 |
+
'hydration': 0,
|
143 |
+
'routing': 0
|
144 |
+
}
|
145 |
|
146 |
+
# 1. Framework-specific root elements
|
147 |
+
framework_roots = {
|
148 |
+
'react': ['react-root', 'react-app', 'root', '__next'],
|
149 |
+
'angular': ['ng-version', 'ng-app'],
|
150 |
+
'vue': ['v-app', '#app', 'nuxt-app'],
|
151 |
+
'modern': ['app-root', 'application', 'spa-root']
|
152 |
+
}
|
153 |
|
154 |
+
for framework, identifiers in framework_roots.items():
|
155 |
+
for id_value in identifiers:
|
156 |
+
if (soup.find(attrs={'id': re.compile(id_value, re.I)}) or
|
157 |
+
soup.find(attrs={'class': re.compile(id_value, re.I)}) or
|
158 |
+
soup.find(attrs={'data-': re.compile(id_value, re.I)})):
|
159 |
+
scores['framework_roots'] += 1
|
160 |
+
|
161 |
+
# 2. Check for hydration indicators
|
162 |
+
hydration_patterns = [
|
163 |
+
r'hydrate',
|
164 |
+
r'createRoot',
|
165 |
+
r'reactive',
|
166 |
+
r'observable'
|
167 |
+
]
|
168 |
|
169 |
+
scripts = soup.find_all('script')
|
170 |
+
for script in scripts:
|
171 |
+
if script.string:
|
172 |
+
for pattern in hydration_patterns:
|
173 |
+
if re.search(pattern, script.string):
|
174 |
+
scores['hydration'] += 1
|
175 |
+
|
176 |
+
# 3. Check for dynamic routing setup
|
177 |
+
router_patterns = [
|
178 |
+
'router-view',
|
179 |
+
'router-link',
|
180 |
+
'route-link',
|
181 |
+
'history.push',
|
182 |
+
'navigation'
|
183 |
+
]
|
184 |
|
185 |
+
for pattern in router_patterns:
|
186 |
+
if soup.find(class_=re.compile(pattern, re.I)) or \
|
187 |
+
soup.find(id=re.compile(pattern, re.I)):
|
188 |
+
scores['routing'] += 1
|
189 |
+
|
190 |
+
return scores
|
191 |
+
|
192 |
+
def _check_dynamic_loading_patterns(soup: BeautifulSoup) -> Dict[str, int]:
|
193 |
+
"""Check for various dynamic content loading patterns."""
|
194 |
+
scores = {
|
195 |
+
'infinite_scroll': 0,
|
196 |
+
'load_more_buttons': 0,
|
197 |
+
'pagination': 0,
|
198 |
+
'lazy_loading': 0,
|
199 |
+
'loading_indicators': 0
|
200 |
+
}
|
201 |
|
202 |
+
# 1. Check for infinite scroll indicators
|
203 |
+
scroll_indicators = [
|
204 |
+
'infinite-scroll',
|
205 |
+
'data-infinite',
|
206 |
+
'data-virtualized',
|
207 |
+
'virtual-scroll',
|
208 |
+
'scroll-container',
|
209 |
+
'scroll-viewport'
|
210 |
+
]
|
211 |
|
212 |
+
for indicator in scroll_indicators:
|
213 |
+
elements = soup.find_all(
|
214 |
+
lambda tag: any(indicator.lower() in str(v).lower()
|
215 |
+
for v in tag.attrs.values())
|
216 |
+
)
|
217 |
+
if elements:
|
218 |
+
scores['infinite_scroll'] += len(elements)
|
219 |
+
|
220 |
+
# 2. Check for load more buttons
|
221 |
+
button_patterns = [
|
222 |
+
r'load[_-]?more',
|
223 |
+
r'show[_-]?more',
|
224 |
+
r'view[_-]?more',
|
225 |
+
r'see[_-]?more',
|
226 |
+
r'more[_-]?posts',
|
227 |
+
r'more[_-]?results'
|
228 |
+
]
|
229 |
|
230 |
+
for pattern in button_patterns:
|
231 |
+
elements = soup.find_all(
|
232 |
+
['button', 'a', 'div', 'span'],
|
233 |
+
text=re.compile(pattern, re.I)
|
234 |
+
)
|
235 |
+
if elements:
|
236 |
+
scores['load_more_buttons'] += len(elements)
|
237 |
+
|
238 |
+
# 3. Check for pagination
|
239 |
+
pagination_patterns = [
|
240 |
+
'pagination',
|
241 |
+
'page-numbers',
|
242 |
+
'page-nav',
|
243 |
+
'page-links'
|
244 |
+
]
|
245 |
|
246 |
+
for pattern in pagination_patterns:
|
247 |
+
elements = soup.find_all(class_=re.compile(pattern, re.I))
|
248 |
+
if elements:
|
249 |
+
scores['pagination'] += len(elements)
|
250 |
+
|
251 |
+
# 4. Check for lazy loading
|
252 |
+
lazy_patterns = ['lazy', 'data-src', 'data-lazy']
|
253 |
+
for pattern in lazy_patterns:
|
254 |
+
elements = soup.find_all(
|
255 |
+
lambda tag: any(pattern.lower() in str(v).lower()
|
256 |
+
for v in tag.attrs.values())
|
257 |
+
)
|
258 |
+
if elements:
|
259 |
+
scores['lazy_loading'] += len(elements)
|
260 |
+
|
261 |
+
# 5. Check for loading indicators
|
262 |
+
loading_patterns = [
|
263 |
+
'loading',
|
264 |
+
'spinner',
|
265 |
+
'skeleton',
|
266 |
+
'placeholder',
|
267 |
+
'shimmer'
|
268 |
+
]
|
269 |
|
270 |
+
for pattern in loading_patterns:
|
271 |
+
elements = soup.find_all(class_=re.compile(pattern, re.I))
|
272 |
+
if elements:
|
273 |
+
scores['loading_indicators'] += len(elements)
|
274 |
+
|
275 |
+
return scores
|
276 |
+
|
277 |
+
def _evaluate_dynamic_indicators(
|
278 |
+
structural: Dict[str, int],
|
279 |
+
framework: Dict[str, int],
|
280 |
+
loading: Dict[str, int]
|
281 |
+
) -> Tuple[bool, Optional[str]]:
|
282 |
+
"""Evaluate dynamic indicators and return JavaScript instructions."""
|
283 |
+
methods = []
|
284 |
+
js_snippets = []
|
285 |
+
|
286 |
+
# Infinite Scroll
|
287 |
+
if loading['infinite_scroll'] > 0:
|
288 |
+
methods.append("scroll")
|
289 |
+
js_snippets.append(
|
290 |
+
"""
|
291 |
+
window.scrollTo(0, document.body.scrollHeight);
|
292 |
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
293 |
+
""".strip().replace('\n', '')
|
294 |
+
)
|
295 |
+
|
296 |
+
# Load More Buttons
|
297 |
+
if loading['load_more_buttons'] > 0:
|
298 |
+
methods.append("button")
|
299 |
+
js_snippets.append(
|
300 |
+
"""
|
301 |
+
const button = Array.from(document.querySelectorAll('button, a, div, span')).find(
|
302 |
+
el => /load[_-]?more|show[_-]?more/i.test(el.textContent)
|
303 |
+
);
|
304 |
+
if (button) {
|
305 |
+
button.click();
|
306 |
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
307 |
+
} else {
|
308 |
+
console.warn("No 'Load More' button found.");
|
309 |
+
}
|
310 |
+
""".strip().replace('\n', '')
|
311 |
+
)
|
312 |
+
|
313 |
+
# Paginated Interfaces
|
314 |
+
if loading.get('pagination', 0) > 0:
|
315 |
+
methods.append("pagination")
|
316 |
+
js_snippets.append(
|
317 |
+
"""
|
318 |
+
const nextPage = document.querySelector('a[rel="next"], .pagination-next, .page-next');
|
319 |
+
if (nextPage) {
|
320 |
+
nextPage.click();
|
321 |
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
322 |
+
} else {
|
323 |
+
console.warn("No pagination link found.");
|
324 |
+
}
|
325 |
+
""".strip().replace('\n', '')
|
326 |
+
)
|
327 |
+
|
328 |
+
# Lazy Loading
|
329 |
+
if loading.get('lazy_loading', 0) > 0:
|
330 |
+
methods.append("lazy")
|
331 |
+
js_snippets.append(
|
332 |
+
"""
|
333 |
+
if (window.__INITIAL_STATE__ || window.__REDUX_STATE__ || window.__NUXT__ || window.__NEXT_DATA__) {
|
334 |
+
console.log('Framework state detected. Consider monitoring network requests for further actions.');
|
335 |
+
}
|
336 |
+
""".strip().replace('\n', '')
|
337 |
+
)
|
338 |
+
|
339 |
+
# Framework and State Management Indicators
|
340 |
+
if framework['framework_roots'] > 0 or structural['state_management'] > 0:
|
341 |
+
methods.append("stateful")
|
342 |
+
js_snippets.append(
|
343 |
+
"""
|
344 |
+
if (window.__INITIAL_STATE__ || window.__REDUX_STATE__ || window.__NUXT__ || window.__NEXT_DATA__) {
|
345 |
+
console.log('Detected stateful framework data loading.');
|
346 |
+
}
|
347 |
+
""".strip().replace('\n', '')
|
348 |
+
)
|
349 |
+
|
350 |
+
# API-Driven Content
|
351 |
+
if structural['api_endpoints'] > 0:
|
352 |
+
methods.append("api")
|
353 |
+
js_snippets.append(
|
354 |
+
"""
|
355 |
+
console.log('API requests detected. Use browser devtools to inspect network activity for specific endpoints.');
|
356 |
+
""".strip().replace('\n', '')
|
357 |
+
)
|
358 |
+
|
359 |
+
# Aggregate and finalize
|
360 |
+
if methods:
|
361 |
+
js_code = "\n".join(js_snippets)
|
362 |
+
return True, js_code
|
363 |
|
364 |
+
return False, None
|
365 |
|
366 |
+
# Main execution
|
367 |
+
soup = BeautifulSoup(html_content, 'html.parser')
|
368 |
|
369 |
+
# Run all checks
|
370 |
+
structural_scores = _check_structural_indicators(soup)
|
371 |
+
framework_scores = _check_modern_framework_indicators(soup)
|
372 |
+
loading_scores = _check_dynamic_loading_patterns(soup)
|
373 |
|
374 |
+
# Evaluate results
|
375 |
+
return _evaluate_dynamic_indicators(structural_scores, framework_scores, loading_scores)
|
376 |
+
|
377 |
+
async def crawl(
|
378 |
+
self,
|
379 |
+
url,
|
380 |
+
depth=2,
|
381 |
+
max_pages=5,
|
382 |
+
session_id=None,
|
383 |
+
human_simulation=True,
|
384 |
+
rotate_user_agent=True,
|
385 |
+
rotate_proxy=True,
|
386 |
+
return_html=False
|
387 |
+
):
|
388 |
+
if not session_id:
|
389 |
+
session_id = self.create_session()
|
390 |
+
|
391 |
+
session_id = self.rotate_session(session_id)
|
392 |
+
|
393 |
+
# List of rotating user agents
|
394 |
+
user_agents = [
|
395 |
+
'Chrome/115.0.0.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.0.0 Safari/537.36',
|
396 |
+
'Chrome/115.0.0.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.0.0 Safari/537.36',
|
397 |
+
'Chrome/115.0.0.0 (iPad; CPU OS 12_2 like Mac OS X) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.0.0 Safari/537.36',
|
398 |
+
'Chrome/115.0.0.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.0.0 Safari/537.36',
|
399 |
+
'Chrome/115.0.0.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.0.0 Safari/537.36'
|
400 |
+
]
|
401 |
+
|
402 |
+
# List of rotating proxies
|
403 |
+
proxies = [
|
404 |
+
"http://50.62.183.123:80",
|
405 |
+
"http://104.129.60.84:6516",
|
406 |
+
"http://156.228.118.163:3128",
|
407 |
+
"http://142.111.104.97:6107",
|
408 |
+
"http://156.228.99.99:3128"
|
409 |
+
]
|
410 |
+
|
411 |
+
try:
|
412 |
+
async with self.crawler as crawler:
|
413 |
+
# Rotate user agent and optimize headers for each attempt
|
414 |
+
headers = {
|
415 |
+
"User-Agent": random.choice(user_agents) if rotate_user_agent else user_agents[0],
|
416 |
+
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8",
|
417 |
+
"Accept-Language": "en-US,en;q=0.5",
|
418 |
+
"Accept-Encoding": "gzip, deflate",
|
419 |
+
"Connection": "keep-alive",
|
420 |
+
"Upgrade-Insecure-Requests": "1",
|
421 |
+
"Sec-Fetch-Dest": "document",
|
422 |
+
"Sec-Fetch-Mode": "navigate",
|
423 |
+
"Sec-Fetch-Site": "none",
|
424 |
+
"Sec-Fetch-User": "?1",
|
425 |
+
"Cache-Control": "max-age=0"
|
426 |
+
}
|
427 |
|
428 |
+
# Update crawler headers for rotation
|
429 |
+
crawler.crawler_strategy.headers = headers
|
430 |
+
|
431 |
+
if rotate_proxy:
|
432 |
+
# Update crawler proxy for rotation
|
433 |
+
crawler.crawler_strategy.proxy = random.choice(proxies)
|
434 |
+
|
435 |
+
result_1 = await crawler.arun(
|
436 |
+
session_id=session_id,
|
437 |
+
url=url,
|
438 |
+
magic=True if human_simulation else False,
|
439 |
+
simulate_user=True if human_simulation else False,
|
440 |
+
override_navigator=True if human_simulation else False,
|
441 |
+
depth=depth,
|
442 |
+
max_pages=max_pages,
|
443 |
+
bypass_cache=True,
|
444 |
+
remove_overlay_elements=True,
|
445 |
+
delay_before_retrieve_html=1.0,
|
446 |
+
verbose=self.verbose
|
447 |
+
)
|
448 |
+
|
449 |
+
# Update session metrics
|
450 |
+
self.session_pool[session_id]['requests_count'] += 1
|
451 |
+
self.session_pool[session_id]['last_used'] = datetime.now()
|
452 |
+
|
453 |
+
if result_1.success:
|
454 |
+
if hasattr(result_1, 'html'):
|
455 |
+
success, js_code = self.is_dynamic_page(result_1.html)
|
456 |
+
|
457 |
+
if success:
|
458 |
+
async with crawler as crawler:
|
459 |
+
# Update crawler headers for rotation
|
460 |
+
crawler.crawler_strategy.headers = headers
|
461 |
+
|
462 |
+
if rotate_proxy:
|
463 |
+
# Update crawler proxy for rotation
|
464 |
+
crawler.crawler_strategy.proxy = random.choice(proxies)
|
465 |
+
|
466 |
+
print(f"Executing JS code: {js_code}")
|
467 |
+
result_2 = await crawler.arun(
|
468 |
+
session_id=session_id,
|
469 |
+
url=url,
|
470 |
+
magic=True if human_simulation else False,
|
471 |
+
simulate_user=True if human_simulation else False,
|
472 |
+
override_navigator=True if human_simulation else False,
|
473 |
+
depth=depth,
|
474 |
+
max_pages=max_pages,
|
475 |
+
js_code=js_code,
|
476 |
+
bypass_cache=True,
|
477 |
+
remove_overlay_elements=True,
|
478 |
+
delay_before_retrieve_html=1.0,
|
479 |
+
verbose=self.verbose
|
480 |
+
)
|
481 |
+
|
482 |
+
if result_2.success:
|
483 |
+
result = result_2
|
484 |
+
else:
|
485 |
+
result = result_1
|
486 |
+
|
487 |
+
# Update session metrics
|
488 |
+
self.session_pool[session_id]['requests_count'] += 1
|
489 |
+
self.session_pool[session_id]['last_used'] = datetime.now()
|
490 |
+
|
491 |
+
else:
|
492 |
+
result = result_1
|
493 |
|
494 |
+
if return_html and hasattr(result, 'html'):
|
495 |
+
return result.html
|
496 |
+
elif hasattr(result, 'fit_markdown'):
|
497 |
+
return result.fit_markdown
|
498 |
+
elif hasattr(result, 'markdown'):
|
499 |
+
return self.extract_content(result.markdown)
|
500 |
|
501 |
+
except Exception as e:
|
502 |
+
print(f"Error crawling {url}: {str(e)}")
|
503 |
|
504 |
+
return None
|
505 |
|
506 |
+
async def crawl_with_retry(
|
507 |
+
self,
|
508 |
+
url,
|
509 |
+
depth=2,
|
510 |
+
max_pages=5,
|
511 |
+
max_retries=3,
|
512 |
+
backoff_factor=1,
|
513 |
+
session_id=None,
|
514 |
+
human_simulation=True,
|
515 |
+
rotate_user_agent=True,
|
516 |
+
rotate_proxy=True,
|
517 |
+
return_html=False,
|
518 |
+
timeout=10.0
|
519 |
+
):
|
520 |
+
"""Crawl with retry logic and anti-blocking measures"""
|
521 |
+
|
522 |
+
async def attempt_crawl(attempt):
|
523 |
+
try:
|
524 |
+
async with async_timeout.timeout(timeout):
|
525 |
+
context = await self.get_browser_context(session_id)
|
526 |
+
return await self.crawl(
|
527 |
+
context,
|
528 |
+
url,
|
529 |
+
depth,
|
530 |
+
max_pages,
|
531 |
+
session_id,
|
532 |
+
human_simulation,
|
533 |
+
rotate_user_agent,
|
534 |
+
rotate_proxy,
|
535 |
+
return_html
|
536 |
+
)
|
537 |
+
except asyncio.TimeoutError:
|
538 |
+
print(f"Timeout on attempt {attempt} for {url}")
|
539 |
+
raise
|
540 |
+
except Exception as e:
|
541 |
+
print(f"Error on attempt {attempt} for {url}: {e}")
|
542 |
+
raise
|
543 |
+
|
544 |
+
if not self.is_valid_url(url) and not self.is_html_url(url):
|
545 |
+
print(f"Invalid URL: {url}")
|
546 |
+
return f"No web results found for query: {url}"
|
547 |
+
|
548 |
+
for attempt in range(max_retries):
|
549 |
+
try:
|
550 |
+
if attempt > 0:
|
551 |
+
# Add delay between retries with exponential backoff
|
552 |
+
delay = backoff_factor * (2 ** (attempt - 1))
|
553 |
+
await asyncio.sleep(delay)
|
554 |
|
555 |
+
return await attempt_crawl(attempt + 1)
|
556 |
+
except Exception as e:
|
557 |
+
if attempt == max_retries - 1:
|
558 |
+
print(f"Max retries ({max_retries}) reached for {url}")
|
559 |
+
return f"Failed to crawl after {max_retries} attempts: {url}"
|
560 |
+
continue
|
561 |
+
|
562 |
+
return f"No content found after {max_retries} attempts for: {url}"
|
563 |
+
|
564 |
+
def extract_content(self, html_content):
|
565 |
+
soup = BeautifulSoup(html_content, 'html.parser')
|
566 |
+
for script in soup(["script", "style"]):
|
567 |
+
script.decompose()
|
568 |
+
text = soup.get_text()
|
569 |
+
lines = (line.strip() for line in text.splitlines())
|
570 |
+
chunks = (phrase.strip() for line in lines for phrase in line.split(" "))
|
571 |
+
text = '\n'.join(chunk for chunk in chunks if chunk)
|
572 |
+
return text
|
573 |
|
574 |
+
def cleanup_session(self, session_id):
|
575 |
+
"""Clean up a session"""
|
576 |
+
print(f"Cleaning up session {session_id}")
|
577 |
+
if session_id in self.session_pool:
|
578 |
+
self.crawler.crawler_strategy.kill_session(session_id)
|
579 |
+
del self.session_pool[session_id]
|
580 |
+
|
581 |
+
def cleanup_expired_sessions(self):
|
582 |
+
"""Regular cleanup of expired sessions using proper time calculation"""
|
583 |
+
try:
|
584 |
+
current_time = datetime.now()
|
585 |
+
expired_sessions = []
|
586 |
|
587 |
+
for sid, data in self.session_pool.items():
|
588 |
+
# Calculate time difference in seconds
|
589 |
+
time_diff = (current_time - data['last_used']).total_seconds()
|
590 |
|
591 |
+
# Check if more than 1 hour (3600 seconds)
|
592 |
+
if time_diff > 3600:
|
593 |
+
expired_sessions.append(sid)
|
594 |
|
595 |
+
# Cleanup expired sessions
|
596 |
+
for session_id in expired_sessions:
|
597 |
+
self.cleanup_session(session_id)
|
598 |
|
599 |
+
except Exception as e:
|
600 |
+
if self.verbose:
|
601 |
+
print(f"Error during session cleanup: {str(e)}")
|
602 |
|
603 |
+
@staticmethod
|
604 |
+
def is_valid_url(url):
|
605 |
+
try:
|
606 |
+
result = urlparse(url)
|
607 |
+
return all([result.scheme, result.netloc])
|
608 |
+
except ValueError:
|
609 |
+
return False
|
610 |
|
611 |
+
@staticmethod
|
612 |
+
def is_html_url(url):
|
613 |
+
return url.endswith(".html") or url.endswith(".htm")
|
614 |
|
615 |
class CustomCrawler:
|
616 |
def __init__(
|
src/helpers/helper.py
CHANGED
@@ -1,11 +1,13 @@
|
|
1 |
import os
|
2 |
import re
|
3 |
import gc
|
|
|
4 |
import torch
|
5 |
import transformers
|
6 |
from langchain.text_splitter import RecursiveCharacterTextSplitter, TokenTextSplitter
|
7 |
|
8 |
ENV_FILE_PATH = os.path.join(os.getenv("WRITABLE_DIR", "/tmp"), ".env")
|
|
|
9 |
|
10 |
def remove_markdown(text: str) -> str:
|
11 |
# Remove code block format type and the code block itself
|
@@ -165,6 +167,17 @@ def read_env():
|
|
165 |
|
166 |
# Function to update .env file
|
167 |
def update_env_vars(new_values: dict):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
168 |
# Overwrite .env file with new values
|
169 |
with open(ENV_FILE_PATH, "w", encoding="utf-8") as f:
|
170 |
for var, val in new_values.items():
|
@@ -198,4 +211,23 @@ def prepare_proxy_list_updates(proxy_list: str) -> list:
|
|
198 |
for i, proxy in enumerate(lines, start=1):
|
199 |
proxies[f"PROXY_{i}"] = proxy
|
200 |
|
201 |
-
return proxies
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
import os
|
2 |
import re
|
3 |
import gc
|
4 |
+
import shutil
|
5 |
import torch
|
6 |
import transformers
|
7 |
from langchain.text_splitter import RecursiveCharacterTextSplitter, TokenTextSplitter
|
8 |
|
9 |
ENV_FILE_PATH = os.path.join(os.getenv("WRITABLE_DIR", "/tmp"), ".env")
|
10 |
+
WEBHOOK_PATH = os.path.join(os.getenv("WRITABLE_DIR", "/tmp"), ".webhook_secret")
|
11 |
|
12 |
def remove_markdown(text: str) -> str:
|
13 |
# Remove code block format type and the code block itself
|
|
|
167 |
|
168 |
# Function to update .env file
|
169 |
def update_env_vars(new_values: dict):
|
170 |
+
# Function to load webhook URL securely
|
171 |
+
def load_webhook_url_securely() -> str:
|
172 |
+
if os.path.exists(WEBHOOK_PATH):
|
173 |
+
with open(WEBHOOK_PATH, "r", encoding="utf-8") as f:
|
174 |
+
return f.read().strip()
|
175 |
+
raise FileNotFoundError(f"Webhook secret file not found at {WEBHOOK_PATH}")
|
176 |
+
|
177 |
+
# Load webhook URL
|
178 |
+
webhook_url = load_webhook_url_securely()
|
179 |
+
new_values["PIPEDREAM_WEBHOOK_URL"] = webhook_url
|
180 |
+
|
181 |
# Overwrite .env file with new values
|
182 |
with open(ENV_FILE_PATH, "w", encoding="utf-8") as f:
|
183 |
for var, val in new_values.items():
|
|
|
211 |
for i, proxy in enumerate(lines, start=1):
|
212 |
proxies[f"PROXY_{i}"] = proxy
|
213 |
|
214 |
+
return proxies
|
215 |
+
|
216 |
+
# Get the size of a folder
|
217 |
+
def get_folder_size(folder_path: str) -> int:
|
218 |
+
total_size = 0
|
219 |
+
if not os.path.exists(folder_path):
|
220 |
+
return 0
|
221 |
+
for entry in os.scandir(folder_path):
|
222 |
+
if entry.is_file():
|
223 |
+
total_size += entry.stat().st_size
|
224 |
+
return total_size
|
225 |
+
|
226 |
+
# Clear a given folder
|
227 |
+
def clear_folder(folder_path: str):
|
228 |
+
if os.path.exists(folder_path):
|
229 |
+
try:
|
230 |
+
shutil.rmtree(folder_path)
|
231 |
+
print(f"Successfully cleared upload directory: {folder_path}")
|
232 |
+
except Exception as e:
|
233 |
+
print(f'Failed to delete {folder_path}. Reason: {e}')
|
src/integrations/mcp_client.py
ADDED
@@ -0,0 +1,506 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import os
|
2 |
+
import json
|
3 |
+
import asyncio
|
4 |
+
import aiohttp
|
5 |
+
import logging
|
6 |
+
import requests
|
7 |
+
from typing import List, Dict, Any
|
8 |
+
from datetime import datetime, timezone
|
9 |
+
|
10 |
+
logger = logging.getLogger(__name__)
|
11 |
+
|
12 |
+
# Client for interacting with the MCP service
|
13 |
+
class MCPClient:
|
14 |
+
def __init__(self):
|
15 |
+
self.webhook_url = os.getenv("PIPEDREAM_WEBHOOK_URL")
|
16 |
+
if not self.webhook_url:
|
17 |
+
logger.warning("PIPEDREAM_WEBHOOK_URL not set in environment variables")
|
18 |
+
|
19 |
+
# Set timeout for requests
|
20 |
+
self.timeout = aiohttp.ClientTimeout(total=60)
|
21 |
+
|
22 |
+
# Fetch app data from MCP service
|
23 |
+
async def fetch_app_data(
|
24 |
+
self,
|
25 |
+
provider: str,
|
26 |
+
services: List[str],
|
27 |
+
query: str,
|
28 |
+
user_id: str,
|
29 |
+
access_token: str
|
30 |
+
) -> Dict[str, Any]:
|
31 |
+
if not self.webhook_url:
|
32 |
+
logger.error("Pipedream webhook URL not configured")
|
33 |
+
return {"error": "Pipedream integration not configured"}
|
34 |
+
|
35 |
+
# Add debugging
|
36 |
+
print(f"=== MCP fetch_app_data called ===")
|
37 |
+
print(f"Provider: {provider}")
|
38 |
+
print(f"Services: {services}")
|
39 |
+
print(f"Query: {query}")
|
40 |
+
print(f"User ID: {user_id}")
|
41 |
+
print(f"Access token exists: {bool(access_token)}")
|
42 |
+
print(f"Access token length: {len(access_token) if access_token else 0}")
|
43 |
+
print("==================================")
|
44 |
+
|
45 |
+
# Check if token is None
|
46 |
+
if not access_token:
|
47 |
+
logger.error(f"No access token for {provider}! Cannot proceed.")
|
48 |
+
return {"error": f"No authentication token for {provider}"}
|
49 |
+
|
50 |
+
payload = {
|
51 |
+
"provider": provider,
|
52 |
+
"services": services,
|
53 |
+
"query": query,
|
54 |
+
"user_id": user_id,
|
55 |
+
"token": access_token,
|
56 |
+
"timestamp": datetime.now(timezone.utc).isoformat()
|
57 |
+
}
|
58 |
+
|
59 |
+
# Manually set headers
|
60 |
+
headers = {'Content-Type': 'application/json'}
|
61 |
+
|
62 |
+
print(f"Fetching {provider} data for services: {services}")
|
63 |
+
print(f"Payload to send: {json.dumps({**payload, 'token': 'REDACTED' if payload['token'] else None}, indent=2)}")
|
64 |
+
|
65 |
+
response = requests.post(self.webhook_url, json={})
|
66 |
+
|
67 |
+
print("Status Code:", response.status_code)
|
68 |
+
print("Response text:", response.text)
|
69 |
+
|
70 |
+
try:
|
71 |
+
print("Parsed JSON:", response.json())
|
72 |
+
except Exception as e:
|
73 |
+
print("JSON parse error:", e)
|
74 |
+
|
75 |
+
# try:
|
76 |
+
# async with aiohttp.ClientSession(timeout=self.timeout) as session:
|
77 |
+
# payload_str = json.dumps(payload)
|
78 |
+
# async with session.post(self.webhook_url, json={'data': payload_str}, headers=headers) as response:
|
79 |
+
# print(f"Response status: {response.status}")
|
80 |
+
# print(f"Response headers: {dict(response.headers)}")
|
81 |
+
|
82 |
+
# if response.status == 200:
|
83 |
+
# # Handle potential empty or null responses from Pipedream
|
84 |
+
# try:
|
85 |
+
# data = await response.json()
|
86 |
+
# print(f"Received data: {json.dumps(data, indent=2)}")
|
87 |
+
# if data is None:
|
88 |
+
# logger.warning("Pipedream returned a null response.")
|
89 |
+
# return {"error": "Received no data from the provider."}
|
90 |
+
# except aiohttp.ContentTypeError:
|
91 |
+
# logger.error("Pipedream returned a non-JSON or empty response.")
|
92 |
+
# return {"error": "Invalid response from the provider."}
|
93 |
+
|
94 |
+
# # Check if any service within the data returned an auth error
|
95 |
+
# auth_error = False
|
96 |
+
# for service_key in data:
|
97 |
+
# if isinstance(data[service_key], dict) and data[service_key].get('error'):
|
98 |
+
# error_details = data[service_key].get('details', '')
|
99 |
+
# if '401' in str(error_details) or 'authError' in str(error_details) or 'UNAUTHENTICATED' in str(error_details):
|
100 |
+
# auth_error = True
|
101 |
+
# break
|
102 |
+
|
103 |
+
# if auth_error:
|
104 |
+
# logger.error(f"Authentication failed for {provider}")
|
105 |
+
# return {"error": f"Authentication failed for {provider}. Please reconnect your account."}
|
106 |
+
|
107 |
+
# logger.info(f"Successfully fetched {provider} data")
|
108 |
+
# return data
|
109 |
+
# else:
|
110 |
+
# error_text = await response.text()
|
111 |
+
# logger.error(f"Pipedream request failed: {response.status} - {error_text}")
|
112 |
+
# return {"error": f"Failed to fetch data: {response.status} - {error_text}"}
|
113 |
+
|
114 |
+
# except asyncio.TimeoutError:
|
115 |
+
# logger.error("Pipedream request timed out")
|
116 |
+
# return {"error": "Request timed out. Please try again."}
|
117 |
+
# except aiohttp.ClientError as e:
|
118 |
+
# logger.error(f"Network error calling Pipedream: {str(e)}")
|
119 |
+
# return {"error": "Network error. Please check your connection."}
|
120 |
+
# except Exception as e:
|
121 |
+
# # Catching TypeError if data is None from response.json()
|
122 |
+
# logger.error(f"Unexpected error calling Pipedream: {str(e)}")
|
123 |
+
# return {"error": "An unexpected error occurred"}
|
124 |
+
|
125 |
+
# Format the raw app data into a context string for LLM
|
126 |
+
def format_as_context(self, provider: str, data: Dict[str, Any]) -> str:
|
127 |
+
if not data or "error" in data:
|
128 |
+
return ""
|
129 |
+
|
130 |
+
context = f"\n[{provider.upper()} APP DATA]\n"
|
131 |
+
context += "=" * 50 + "\n"
|
132 |
+
|
133 |
+
# Format based on provider
|
134 |
+
if provider == "google":
|
135 |
+
context += self._format_google_data(data)
|
136 |
+
elif provider == "microsoft":
|
137 |
+
context += self._format_microsoft_data(data)
|
138 |
+
elif provider == "slack":
|
139 |
+
context += self._format_slack_data(data)
|
140 |
+
else:
|
141 |
+
context += f"Unknown provider: {provider}\n"
|
142 |
+
|
143 |
+
context += "=" * 50 + "\n"
|
144 |
+
return context
|
145 |
+
|
146 |
+
# Helper methods to format data for Google apps
|
147 |
+
def _format_google_data(self, data: Dict[str, Any]) -> str:
|
148 |
+
formatted = ""
|
149 |
+
|
150 |
+
# Google Drive
|
151 |
+
if "drive" in data and isinstance(data["drive"], dict) and "files" in data["drive"]:
|
152 |
+
formatted += "\n📁 GOOGLE DRIVE FILES:\n"
|
153 |
+
formatted += "-" * 30 + "\n"
|
154 |
+
|
155 |
+
files = data["drive"]["files"]
|
156 |
+
if not files:
|
157 |
+
formatted += "No files found matching the query.\n"
|
158 |
+
else:
|
159 |
+
for i, file in enumerate(files[:10], 1): # Limit to 10 files
|
160 |
+
formatted += f"\n{i}. File: {file.get('name', 'Unknown')}\n"
|
161 |
+
formatted += f" Type: {file.get('mimeType', 'Unknown')}\n"
|
162 |
+
formatted += f" Modified: {file.get('modifiedTime', 'Unknown')}\n"
|
163 |
+
|
164 |
+
if file.get('webViewLink'):
|
165 |
+
formatted += f" Link: {file['webViewLink']}\n"
|
166 |
+
|
167 |
+
if file.get('content'):
|
168 |
+
content_preview = file['content'][:500]
|
169 |
+
if len(file['content']) > 500:
|
170 |
+
content_preview += "..."
|
171 |
+
formatted += f" Content Preview:\n {content_preview}\n"
|
172 |
+
|
173 |
+
formatted += "\n"
|
174 |
+
|
175 |
+
# Gmail
|
176 |
+
if "gmail" in data and isinstance(data["gmail"], dict) and "messages" in data["gmail"]:
|
177 |
+
formatted += "\n📧 GMAIL MESSAGES:\n"
|
178 |
+
formatted += "-" * 30 + "\n"
|
179 |
+
|
180 |
+
messages = data["gmail"]["messages"]
|
181 |
+
if not messages:
|
182 |
+
formatted += "No messages found matching the query.\n"
|
183 |
+
else:
|
184 |
+
for i, msg in enumerate(messages[:10], 1):
|
185 |
+
formatted += f"\n{i}. From: {msg.get('from', 'Unknown')}\n"
|
186 |
+
formatted += f" Subject: {msg.get('subject', 'No subject')}\n"
|
187 |
+
|
188 |
+
body_preview = msg.get('body', '')[:300]
|
189 |
+
if msg.get('body', '') and len(msg['body']) > 300:
|
190 |
+
body_preview += "..."
|
191 |
+
formatted += f" Preview: {body_preview}\n"
|
192 |
+
|
193 |
+
# Google Calendar
|
194 |
+
if "calendar" in data and isinstance(data["calendar"], dict) and "events" in data["calendar"]:
|
195 |
+
formatted += "\n📅 GOOGLE CALENDAR EVENTS:\n"
|
196 |
+
formatted += "-" * 30 + "\n"
|
197 |
+
|
198 |
+
events = data["calendar"]["events"]
|
199 |
+
if not events:
|
200 |
+
formatted += "No calendar events found matching the query.\n"
|
201 |
+
else:
|
202 |
+
for i, event in enumerate(events[:10], 1):
|
203 |
+
formatted += f"\n{i}. Event: {event.get('summary', 'No title')}\n"
|
204 |
+
formatted += f" Time: {event.get('start', 'Unknown')}\n"
|
205 |
+
|
206 |
+
if event.get('location'):
|
207 |
+
formatted += f" Location: {event['location']}\n"
|
208 |
+
|
209 |
+
if event.get('description'):
|
210 |
+
desc_preview = event['description'][:200]
|
211 |
+
if len(event['description']) > 200:
|
212 |
+
desc_preview += "..."
|
213 |
+
formatted += f" Description: {desc_preview}\n"
|
214 |
+
|
215 |
+
# Google Docs
|
216 |
+
if "docs" in data and isinstance(data["docs"], dict) and "docs" in data["docs"]:
|
217 |
+
formatted += "\n📄 GOOGLE DOCS:\n"
|
218 |
+
formatted += "-" * 30 + "\n"
|
219 |
+
|
220 |
+
docs = data["docs"]["docs"]
|
221 |
+
if not docs:
|
222 |
+
formatted += "No documents found matching the query.\n"
|
223 |
+
else:
|
224 |
+
for i, doc in enumerate(docs[:5], 1):
|
225 |
+
formatted += f"\n{i}. Document: {doc.get('name', 'Unknown')}\n"
|
226 |
+
formatted += f" Modified: {doc.get('modifiedTime', 'Unknown')}\n"
|
227 |
+
|
228 |
+
if doc.get('content'):
|
229 |
+
content_preview = doc['content'][:500]
|
230 |
+
if len(doc['content']) > 500:
|
231 |
+
content_preview += "..."
|
232 |
+
formatted += f" Content Preview:\n {content_preview}\n"
|
233 |
+
|
234 |
+
# Google Sheets
|
235 |
+
if "sheets" in data and isinstance(data["sheets"], dict) and "sheets" in data["sheets"]:
|
236 |
+
formatted += "\n📊 GOOGLE SHEETS:\n"
|
237 |
+
formatted += "-" * 30 + "\n"
|
238 |
+
|
239 |
+
sheets = data["sheets"]["sheets"]
|
240 |
+
if not sheets:
|
241 |
+
formatted += "No spreadsheets found matching the query.\n"
|
242 |
+
else:
|
243 |
+
for i, sheet in enumerate(sheets[:5], 1):
|
244 |
+
formatted += f"\n{i}. Spreadsheet: {sheet.get('name', 'Unknown')}\n"
|
245 |
+
formatted += f" Modified: {sheet.get('modifiedTime', 'Unknown')}\n"
|
246 |
+
|
247 |
+
if sheet.get('content'):
|
248 |
+
content_preview = sheet['content'][:300]
|
249 |
+
if len(sheet['content']) > 300:
|
250 |
+
content_preview += "..."
|
251 |
+
formatted += f" Data Preview:\n {content_preview}\n"
|
252 |
+
|
253 |
+
# Google Tasks
|
254 |
+
if "tasks" in data and isinstance(data["tasks"], dict) and "tasks" in data["tasks"]:
|
255 |
+
formatted += "\n✅ GOOGLE TASKS:\n"
|
256 |
+
formatted += "-" * 30 + "\n"
|
257 |
+
|
258 |
+
tasks = data["tasks"]["tasks"]
|
259 |
+
if not tasks:
|
260 |
+
formatted += "No tasks found matching the query.\n"
|
261 |
+
else:
|
262 |
+
for i, task in enumerate(tasks[:10], 1):
|
263 |
+
formatted += f"\n{i}. Task: {task.get('title', 'No title')}\n"
|
264 |
+
formatted += f" List: {task.get('listTitle', 'Unknown')}\n"
|
265 |
+
formatted += f" Status: {task.get('status', 'Unknown')}\n"
|
266 |
+
|
267 |
+
if task.get('notes'):
|
268 |
+
formatted += f" Notes: {task['notes'][:200]}...\n"
|
269 |
+
|
270 |
+
if task.get('due'):
|
271 |
+
formatted += f" Due: {task['due']}\n"
|
272 |
+
|
273 |
+
# Add other Google services as needed
|
274 |
+
|
275 |
+
return formatted
|
276 |
+
|
277 |
+
# Helper methods to format data for Microsoft apps
|
278 |
+
def _format_microsoft_data(self, data: Dict[str, Any]) -> str:
|
279 |
+
formatted = ""
|
280 |
+
|
281 |
+
# Word
|
282 |
+
if "word" in data and isinstance(data["word"], dict) and "documents" in data["word"]:
|
283 |
+
formatted += "\n📄 MICROSOFT WORD DOCUMENTS:\n"
|
284 |
+
formatted += "-" * 30 + "\n"
|
285 |
+
|
286 |
+
documents = data["word"]["documents"]
|
287 |
+
if not documents:
|
288 |
+
formatted += "No documents found matching the query.\n"
|
289 |
+
else:
|
290 |
+
for i, doc in enumerate(documents[:5], 1):
|
291 |
+
formatted += f"\n{i}. Document: {doc.get('name', 'Unknown')}\n"
|
292 |
+
formatted += f" Modified: {doc.get('lastModifiedDateTime', 'Unknown')}\n"
|
293 |
+
|
294 |
+
if doc.get('content'):
|
295 |
+
content_preview = doc['content'][:500]
|
296 |
+
if len(doc['content']) > 500:
|
297 |
+
content_preview += "..."
|
298 |
+
formatted += f" Content Preview:\n {content_preview}\n"
|
299 |
+
|
300 |
+
# Excel
|
301 |
+
if "excel" in data and isinstance(data["excel"], dict) and "workbooks" in data["excel"]:
|
302 |
+
formatted += "\n📊 MICROSOFT EXCEL WORKBOOKS:\n"
|
303 |
+
formatted += "-" * 30 + "\n"
|
304 |
+
|
305 |
+
workbooks = data["excel"]["workbooks"]
|
306 |
+
if not workbooks:
|
307 |
+
formatted += "No workbooks found matching the query.\n"
|
308 |
+
else:
|
309 |
+
for i, wb in enumerate(workbooks[:5], 1):
|
310 |
+
formatted += f"\n{i}. Workbook: {wb.get('name', 'Unknown')}\n"
|
311 |
+
formatted += f" Modified: {wb.get('lastModifiedDateTime', 'Unknown')}\n"
|
312 |
+
|
313 |
+
if wb.get('content'):
|
314 |
+
content_preview = wb['content'][:500]
|
315 |
+
if len(wb['content']) > 500:
|
316 |
+
content_preview += "..."
|
317 |
+
formatted += f" Content Preview:\n {content_preview}\n"
|
318 |
+
|
319 |
+
# PowerPoint
|
320 |
+
if "powerpoint" in data and isinstance(data["powerpoint"], dict) and "presentations" in data["powerpoint"]:
|
321 |
+
formatted += "\n📊 MICROSOFT POWERPOINT PRESENTATIONS:\n"
|
322 |
+
formatted += "-" * 30 + "\n"
|
323 |
+
|
324 |
+
presentations = data["powerpoint"]["presentations"]
|
325 |
+
if not presentations:
|
326 |
+
formatted += "No presentations found matching the query.\n"
|
327 |
+
else:
|
328 |
+
for i, pres in enumerate(presentations[:5], 1):
|
329 |
+
formatted += f"\n{i}. Presentation: {pres.get('name', 'Unknown')}\n"
|
330 |
+
formatted += f" Modified: {pres.get('lastModifiedDateTime', 'Unknown')}\n"
|
331 |
+
|
332 |
+
if pres.get('content'):
|
333 |
+
content_preview = pres['content'][:500]
|
334 |
+
if len(pres['content']) > 500:
|
335 |
+
content_preview += "..."
|
336 |
+
formatted += f" Content Preview:\n {content_preview}\n"
|
337 |
+
|
338 |
+
# OneDrive/Files
|
339 |
+
if "onedrive" in data and isinstance(data["onedrive"], dict) and "files" in data["onedrive"]:
|
340 |
+
formatted += "\n📁 ONEDRIVE FILES:\n"
|
341 |
+
formatted += "-" * 30 + "\n"
|
342 |
+
|
343 |
+
files = data["onedrive"]["files"]
|
344 |
+
if not files:
|
345 |
+
formatted += "No files found matching the query.\n"
|
346 |
+
else:
|
347 |
+
for i, file in enumerate(files[:10], 1):
|
348 |
+
formatted += f"\n{i}. File: {file.get('name', 'Unknown')}\n"
|
349 |
+
formatted += f" Modified: {file.get('lastModified', 'Unknown')}\n"
|
350 |
+
|
351 |
+
if file.get('webUrl'):
|
352 |
+
formatted += f" URL: {file['webUrl']}\n"
|
353 |
+
|
354 |
+
if file.get('content'):
|
355 |
+
content_preview = file['content'][:500]
|
356 |
+
if len(file['content']) > 500:
|
357 |
+
content_preview += "..."
|
358 |
+
formatted += f" Content Preview:\n {content_preview}\n"
|
359 |
+
|
360 |
+
# Outlook
|
361 |
+
if "outlook" in data and isinstance(data["outlook"], dict) and "messages" in data["outlook"]:
|
362 |
+
formatted += "\n📧 OUTLOOK MESSAGES:\n"
|
363 |
+
formatted += "-" * 30 + "\n"
|
364 |
+
|
365 |
+
messages = data["outlook"]["messages"]
|
366 |
+
if not messages:
|
367 |
+
formatted += "No messages found matching the query.\n"
|
368 |
+
else:
|
369 |
+
for i, msg in enumerate(messages[:10], 1):
|
370 |
+
formatted += f"\n{i}. From: {msg.get('from', 'Unknown')}\n"
|
371 |
+
formatted += f" Subject: {msg.get('subject', 'No subject')}\n"
|
372 |
+
|
373 |
+
body_preview = msg.get('body', '')[:300]
|
374 |
+
if msg.get('body', '') and len(msg['body']) > 300:
|
375 |
+
body_preview += "..."
|
376 |
+
formatted += f" Preview: {body_preview}\n"
|
377 |
+
|
378 |
+
# OneNote
|
379 |
+
if "onenote" in data and isinstance(data["onenote"], dict) and "pages" in data["onenote"]:
|
380 |
+
formatted += "\n📓 ONENOTE PAGES:\n"
|
381 |
+
formatted += "-" * 30 + "\n"
|
382 |
+
|
383 |
+
pages = data["onenote"]["pages"]
|
384 |
+
if not pages:
|
385 |
+
formatted += "No pages found matching the query.\n"
|
386 |
+
else:
|
387 |
+
for i, page in enumerate(pages[:10], 1):
|
388 |
+
formatted += f"\n{i}. Page: {page.get('title', 'Unknown')}\n"
|
389 |
+
formatted += f" Section: {page.get('parentSection', 'Unknown')}\n"
|
390 |
+
formatted += f" Modified: {page.get('lastModifiedDateTime', 'Unknown')}\n"
|
391 |
+
|
392 |
+
if page.get('contentPreview'):
|
393 |
+
formatted += f" Preview: {page['contentPreview'][:200]}...\n"
|
394 |
+
|
395 |
+
# Microsoft To Do
|
396 |
+
if "todo" in data and isinstance(data["todo"], dict) and "tasks" in data["todo"]:
|
397 |
+
formatted += "\n✅ MICROSOFT TO DO:\n"
|
398 |
+
formatted += "-" * 30 + "\n"
|
399 |
+
|
400 |
+
tasks = data["todo"]["tasks"]
|
401 |
+
if not tasks:
|
402 |
+
formatted += "No tasks found matching the query.\n"
|
403 |
+
else:
|
404 |
+
for i, task in enumerate(tasks[:10], 1):
|
405 |
+
formatted += f"\n{i}. Task: {task.get('title', 'No title')}\n"
|
406 |
+
formatted += f" List: {task.get('listName', 'Unknown')}\n"
|
407 |
+
formatted += f" Status: {'Completed' if task.get('isCompleted') else 'Pending'}\n"
|
408 |
+
|
409 |
+
if task.get('body', {}).get('content'):
|
410 |
+
formatted += f" Notes: {task['body']['content'][:200]}...\n"
|
411 |
+
|
412 |
+
if task.get('dueDateTime'):
|
413 |
+
formatted += f" Due: {task['dueDateTime']['dateTime']}\n"
|
414 |
+
|
415 |
+
# Exchange Calendar
|
416 |
+
if "exchange" in data and isinstance(data["exchange"], dict) and "events" in data["exchange"]:
|
417 |
+
formatted += "\n📅 EXCHANGE CALENDAR:\n"
|
418 |
+
formatted += "-" * 30 + "\n"
|
419 |
+
|
420 |
+
events = data["exchange"]["events"]
|
421 |
+
if not events:
|
422 |
+
formatted += "No calendar events found matching the query.\n"
|
423 |
+
else:
|
424 |
+
for i, event in enumerate(events[:10], 1):
|
425 |
+
formatted += f"\n{i}. Event: {event.get('subject', 'No subject')}\n"
|
426 |
+
formatted += f" Start: {event.get('start', {}).get('dateTime', 'Unknown')}\n"
|
427 |
+
formatted += f" End: {event.get('end', {}).get('dateTime', 'Unknown')}\n"
|
428 |
+
|
429 |
+
if event.get('location', {}).get('displayName'):
|
430 |
+
formatted += f" Location: {event['location']['displayName']}\n"
|
431 |
+
|
432 |
+
if event.get('bodyPreview'):
|
433 |
+
formatted += f" Preview: {event['bodyPreview'][:200]}...\n"
|
434 |
+
|
435 |
+
return formatted
|
436 |
+
|
437 |
+
# Helper methods to format data for Slack
|
438 |
+
def _format_slack_data(self, data: Dict[str, Any]) -> str:
|
439 |
+
formatted = "\n💬 SLACK MESSAGES:\n"
|
440 |
+
formatted += "-" * 30 + "\n"
|
441 |
+
|
442 |
+
if "messages" in data and isinstance(data["messages"], list):
|
443 |
+
messages = data["messages"]
|
444 |
+
if not messages:
|
445 |
+
formatted += "No messages found matching the query.\n"
|
446 |
+
else:
|
447 |
+
for i, msg in enumerate(messages[:15], 1):
|
448 |
+
formatted += f"\n{i}. User: {msg.get('user', 'Unknown')}\n"
|
449 |
+
formatted += f" Channel: #{msg.get('channel', 'Unknown')}\n"
|
450 |
+
formatted += f" Message: {msg.get('text', '')}\n"
|
451 |
+
|
452 |
+
if msg.get('ts'):
|
453 |
+
# Convert timestamp to readable format if needed
|
454 |
+
formatted += f" Time: {msg['ts']}\n"
|
455 |
+
|
456 |
+
# Slack channels with messages
|
457 |
+
if "channels" in data and isinstance(data["channels"], list):
|
458 |
+
formatted += "\n📢 SLACK CHANNELS:\n"
|
459 |
+
formatted += "-" * 30 + "\n"
|
460 |
+
|
461 |
+
for channel in data["channels"][:10]:
|
462 |
+
formatted += f"\nChannel: #{channel.get('name', 'Unknown')}\n"
|
463 |
+
if channel.get('messages'):
|
464 |
+
for msg in channel['messages'][:5]:
|
465 |
+
formatted += f" • {msg.get('user', 'Unknown')}: {msg.get('text', '')}\n"
|
466 |
+
|
467 |
+
# Slack files
|
468 |
+
if "files" in data and isinstance(data["files"], list):
|
469 |
+
formatted += "\n📎 SLACK FILES:\n"
|
470 |
+
formatted += "-" * 30 + "\n"
|
471 |
+
|
472 |
+
files = data["files"]
|
473 |
+
if not files:
|
474 |
+
formatted += "No files found matching the query.\n"
|
475 |
+
else:
|
476 |
+
for i, file in enumerate(files[:10], 1):
|
477 |
+
formatted += f"\n{i}. File: {file.get('name', 'Unknown')}\n"
|
478 |
+
formatted += f" Type: {file.get('mimetype', 'Unknown')}\n"
|
479 |
+
formatted += f" Size: {self._format_file_size(file.get('size', 0))}\n"
|
480 |
+
|
481 |
+
if file.get('preview'):
|
482 |
+
formatted += f" Preview: {file['preview'][:200]}...\n"
|
483 |
+
|
484 |
+
return formatted
|
485 |
+
|
486 |
+
# Helper method to format file sizes
|
487 |
+
def _format_file_size(self, size_bytes: int) -> str:
|
488 |
+
if size_bytes < 1024:
|
489 |
+
return f"{size_bytes} B"
|
490 |
+
elif size_bytes < 1024 * 1024:
|
491 |
+
return f"{size_bytes / 1024:.1f} KB"
|
492 |
+
elif size_bytes < 1024 * 1024 * 1024:
|
493 |
+
return f"{size_bytes / (1024 * 1024):.1f} MB"
|
494 |
+
else:
|
495 |
+
return f"{size_bytes / (1024 * 1024 * 1024):.1f} GB"
|
496 |
+
|
497 |
+
if __name__ == "__main__":
|
498 |
+
# Example usage
|
499 |
+
client = MCPClient()
|
500 |
+
print(asyncio.run(client.fetch_app_data(
|
501 |
+
provider="google",
|
502 |
+
services=["gmail"],
|
503 |
+
query="summarize the information from my last 5 emails",
|
504 |
+
user_id="4c1d92c5-ecec-45d5-b8fd-cb0ce5292403",
|
505 |
+
access_token="ya29.a0AS3H6Nw9WnmYv7goOaxsZiwm6qDdaQq4h6tLwD69VVFPa6s7wwPYtzV3EgPIQHMnW_xRIpbcsDzTNmeOs-8gKhnB0RoW27Kuvv75eWcRed5BcWa08JWH5FFeNoSvzr_lZswEV1PZ4e5R4xNXSrtWmV4vJ-UPmwG48HIZn2lkaCgYKAW8SARcSFQHGX2MiUPjegzd64tClSBXeJNUPvw0175"
|
506 |
+
)))
|
src/query_processing/query_processor.py
CHANGED
@@ -1,5 +1,6 @@
|
|
1 |
import spacy
|
2 |
import json
|
|
|
3 |
from langchain.prompts import ChatPromptTemplate
|
4 |
from src.utils.api_key_manager import with_api_manager
|
5 |
from src.helpers.helper import remove_markdown
|
@@ -117,9 +118,10 @@ Your response should be in JSON format with the following structure (do not incl
|
|
117 |
for internet search purposes, return the original query in plain text without any formatting and/or markdown.
|
118 |
|
119 |
Intent: {intent}
|
120 |
-
Query: {query}
|
|
|
121 |
prompt = ChatPromptTemplate.from_template(template)
|
122 |
-
messages = prompt.format_messages(intent=intent, query=query)
|
123 |
else:
|
124 |
template = \
|
125 |
"""You are an expert in information retrieval and query analysis.
|
@@ -158,9 +160,10 @@ Your response should be in JSON format with the following structure (do not incl
|
|
158 |
[IMPORTANT] If the query is simple or if it is not beneficial to decompose the query,
|
159 |
for internet search purposes, return the original query in plain text without any formatting and/or markdown.
|
160 |
|
161 |
-
Query: {query}
|
|
|
162 |
prompt = ChatPromptTemplate.from_template(template)
|
163 |
-
messages = prompt.format_messages(query=query)
|
164 |
|
165 |
try:
|
166 |
response = await llm.ainvoke(messages)
|
@@ -288,9 +291,16 @@ Previous Queries:
|
|
288 |
{context}
|
289 |
|
290 |
Query:
|
291 |
-
{query}
|
|
|
|
|
292 |
prompt = ChatPromptTemplate.from_template(template)
|
293 |
-
messages = prompt.format_messages(
|
|
|
|
|
|
|
|
|
|
|
294 |
else:
|
295 |
template = \
|
296 |
"""You are an expert in information retrieval and query analysis.
|
@@ -342,9 +352,15 @@ Intent:
|
|
342 |
{intent}
|
343 |
|
344 |
Query:
|
345 |
-
{query}
|
|
|
|
|
346 |
prompt = ChatPromptTemplate.from_template(template)
|
347 |
-
messages = prompt.format_messages(
|
|
|
|
|
|
|
|
|
348 |
else:
|
349 |
if context:
|
350 |
template = \
|
@@ -435,9 +451,15 @@ Previous Queries:
|
|
435 |
{context}
|
436 |
|
437 |
Query:
|
438 |
-
{query}
|
|
|
|
|
439 |
prompt = ChatPromptTemplate.from_template(template)
|
440 |
-
messages = prompt.format_messages(
|
|
|
|
|
|
|
|
|
441 |
else:
|
442 |
template = \
|
443 |
"""You are an expert in information retrieval and query analysis.
|
@@ -485,9 +507,14 @@ Your response should be in JSON format with the following structure (do not incl
|
|
485 |
for internet search purposes, return the original query in plain text without any formatting and/or markdown.
|
486 |
|
487 |
Query:
|
488 |
-
{query}
|
|
|
|
|
489 |
prompt = ChatPromptTemplate.from_template(template)
|
490 |
-
messages = prompt.format_messages(
|
|
|
|
|
|
|
491 |
|
492 |
try:
|
493 |
response = await llm.ainvoke(messages)
|
@@ -550,9 +577,15 @@ Your response should ONLY contain the modified query in plain text without any f
|
|
550 |
Query: {query}
|
551 |
|
552 |
Context:
|
553 |
-
{context}
|
|
|
|
|
554 |
prompt = ChatPromptTemplate.from_template(template)
|
555 |
-
messages = prompt.format_messages(
|
|
|
|
|
|
|
|
|
556 |
response = await llm.ainvoke(messages)
|
557 |
return response.content.strip()
|
558 |
|
@@ -570,10 +603,16 @@ Your analysis should be concise, detailed, and to the point. It should also cont
|
|
570 |
[IMPORTANT] Your response should ONLY be the intent analysis of the user's query without any formatting.
|
571 |
Do not include the reasoning process or the 10 versions of the query in your response.
|
572 |
|
573 |
-
Original query: {query}
|
|
|
|
|
574 |
|
575 |
prompt = ChatPromptTemplate.from_template(template)
|
576 |
-
|
|
|
|
|
|
|
|
|
577 |
return response.content.strip()
|
578 |
|
579 |
if __name__ == "__main__":
|
|
|
1 |
import spacy
|
2 |
import json
|
3 |
+
from datetime import datetime, timezone
|
4 |
from langchain.prompts import ChatPromptTemplate
|
5 |
from src.utils.api_key_manager import with_api_manager
|
6 |
from src.helpers.helper import remove_markdown
|
|
|
118 |
for internet search purposes, return the original query in plain text without any formatting and/or markdown.
|
119 |
|
120 |
Intent: {intent}
|
121 |
+
Query: {query}
|
122 |
+
Current date & time in ISO format (UTC timezone): {date}"""
|
123 |
prompt = ChatPromptTemplate.from_template(template)
|
124 |
+
messages = prompt.format_messages(intent=intent, query=query, date=datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M:%S'))
|
125 |
else:
|
126 |
template = \
|
127 |
"""You are an expert in information retrieval and query analysis.
|
|
|
160 |
[IMPORTANT] If the query is simple or if it is not beneficial to decompose the query,
|
161 |
for internet search purposes, return the original query in plain text without any formatting and/or markdown.
|
162 |
|
163 |
+
Query: {query}
|
164 |
+
Current date & time in ISO format (UTC timezone): {date}"""
|
165 |
prompt = ChatPromptTemplate.from_template(template)
|
166 |
+
messages = prompt.format_messages(query=query, date=datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M:%S'))
|
167 |
|
168 |
try:
|
169 |
response = await llm.ainvoke(messages)
|
|
|
291 |
{context}
|
292 |
|
293 |
Query:
|
294 |
+
{query}
|
295 |
+
|
296 |
+
Current date & time in ISO format (UTC timezone): {date}"""
|
297 |
prompt = ChatPromptTemplate.from_template(template)
|
298 |
+
messages = prompt.format_messages(
|
299 |
+
intent=intent,
|
300 |
+
context=context,
|
301 |
+
query=query,
|
302 |
+
date=datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M:%S')
|
303 |
+
)
|
304 |
else:
|
305 |
template = \
|
306 |
"""You are an expert in information retrieval and query analysis.
|
|
|
352 |
{intent}
|
353 |
|
354 |
Query:
|
355 |
+
{query}
|
356 |
+
|
357 |
+
Current date & time in ISO format (UTC timezone): {date}"""
|
358 |
prompt = ChatPromptTemplate.from_template(template)
|
359 |
+
messages = prompt.format_messages(
|
360 |
+
intent=intent,
|
361 |
+
query=query,
|
362 |
+
date=datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M:%S')
|
363 |
+
)
|
364 |
else:
|
365 |
if context:
|
366 |
template = \
|
|
|
451 |
{context}
|
452 |
|
453 |
Query:
|
454 |
+
{query}
|
455 |
+
|
456 |
+
Current date & time in ISO format (UTC timezone): {date}"""
|
457 |
prompt = ChatPromptTemplate.from_template(template)
|
458 |
+
messages = prompt.format_messages(
|
459 |
+
context=context,
|
460 |
+
query=query,
|
461 |
+
date=datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M:%S')
|
462 |
+
)
|
463 |
else:
|
464 |
template = \
|
465 |
"""You are an expert in information retrieval and query analysis.
|
|
|
507 |
for internet search purposes, return the original query in plain text without any formatting and/or markdown.
|
508 |
|
509 |
Query:
|
510 |
+
{query}
|
511 |
+
|
512 |
+
Current date & time in ISO format (UTC timezone): {date}"""
|
513 |
prompt = ChatPromptTemplate.from_template(template)
|
514 |
+
messages = prompt.format_messages(
|
515 |
+
query=query,
|
516 |
+
date=datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M:%S')
|
517 |
+
)
|
518 |
|
519 |
try:
|
520 |
response = await llm.ainvoke(messages)
|
|
|
577 |
Query: {query}
|
578 |
|
579 |
Context:
|
580 |
+
{context}
|
581 |
+
|
582 |
+
Current date & time in ISO format (UTC timezone): {date}"""
|
583 |
prompt = ChatPromptTemplate.from_template(template)
|
584 |
+
messages = prompt.format_messages(
|
585 |
+
query=query,
|
586 |
+
context=combined_context,
|
587 |
+
date=datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M:%S')
|
588 |
+
)
|
589 |
response = await llm.ainvoke(messages)
|
590 |
return response.content.strip()
|
591 |
|
|
|
603 |
[IMPORTANT] Your response should ONLY be the intent analysis of the user's query without any formatting.
|
604 |
Do not include the reasoning process or the 10 versions of the query in your response.
|
605 |
|
606 |
+
Original query: {query}
|
607 |
+
|
608 |
+
Current date & time in ISO format (UTC timezone): {date}"""
|
609 |
|
610 |
prompt = ChatPromptTemplate.from_template(template)
|
611 |
+
messages = prompt.format_messages(
|
612 |
+
query=query,
|
613 |
+
date=datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M:%S')
|
614 |
+
)
|
615 |
+
response = await llm.ainvoke(messages)
|
616 |
return response.content.strip()
|
617 |
|
618 |
if __name__ == "__main__":
|
src/rag/graph_rag.py
CHANGED
@@ -408,7 +408,7 @@ class GraphRAG:
|
|
408 |
})
|
409 |
filtered_urls = await self.search_engine.filter_urls(
|
410 |
sub_query,
|
411 |
-
"
|
412 |
results
|
413 |
)
|
414 |
await self.emit_event("search_results_filtered", {
|
@@ -823,6 +823,16 @@ class GraphRAG:
|
|
823 |
else:
|
824 |
await self.emit_event("sub_query_failed", {"sub_query": sub_query})
|
825 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
826 |
print("Graph building complete, processing final tasks...")
|
827 |
await self.emit_event("search_process_completed", {
|
828 |
"depth": depth,
|
@@ -1321,17 +1331,11 @@ class GraphRAG:
|
|
1321 |
net.options["layout"] = {"improvedLayout": True}
|
1322 |
net.options["interaction"] = {"dragNodes": True}
|
1323 |
|
1324 |
-
original_dir = os.getcwd()
|
1325 |
-
os.chdir(os.getenv("WRITABLE_DIR", "/tmp"))
|
1326 |
-
|
1327 |
net.save_graph("temp_graph.html")
|
1328 |
|
1329 |
with open("temp_graph.html", "r", encoding="utf-8") as f:
|
1330 |
html_str = f.read()
|
1331 |
-
|
1332 |
os.remove("temp_graph.html")
|
1333 |
-
os.chdir(original_dir)
|
1334 |
-
|
1335 |
return html_str
|
1336 |
|
1337 |
def verify_graph_integrity(self):
|
@@ -1519,7 +1523,7 @@ Present your analysis as a detailed, well-formatted report.""",
|
|
1519 |
answer = graph_search.query_graph(query)
|
1520 |
|
1521 |
response = ""
|
1522 |
-
async for chunk in reasoner.
|
1523 |
response += chunk
|
1524 |
print(response, end="", flush=True)
|
1525 |
|
|
|
408 |
})
|
409 |
filtered_urls = await self.search_engine.filter_urls(
|
410 |
sub_query,
|
411 |
+
"ultra",
|
412 |
results
|
413 |
)
|
414 |
await self.emit_event("search_results_filtered", {
|
|
|
823 |
else:
|
824 |
await self.emit_event("sub_query_failed", {"sub_query": sub_query})
|
825 |
|
826 |
+
for idx, (sub_query, future) in enumerate(futures.items(), 1):
|
827 |
+
if future.done() and future.result().strip():
|
828 |
+
print(f"Sub-query {idx} processed successfully")
|
829 |
+
else:
|
830 |
+
child_futures = all_child_futures.get(sub_query)
|
831 |
+
if any(cf.done() and cf.result().strip() for cf in child_futures):
|
832 |
+
print(f"Sub-query {idx} processed successfully because of child nodes")
|
833 |
+
else:
|
834 |
+
print(f"Sub-query {idx} failed to process because of child nodes")
|
835 |
+
|
836 |
print("Graph building complete, processing final tasks...")
|
837 |
await self.emit_event("search_process_completed", {
|
838 |
"depth": depth,
|
|
|
1331 |
net.options["layout"] = {"improvedLayout": True}
|
1332 |
net.options["interaction"] = {"dragNodes": True}
|
1333 |
|
|
|
|
|
|
|
1334 |
net.save_graph("temp_graph.html")
|
1335 |
|
1336 |
with open("temp_graph.html", "r", encoding="utf-8") as f:
|
1337 |
html_str = f.read()
|
|
|
1338 |
os.remove("temp_graph.html")
|
|
|
|
|
1339 |
return html_str
|
1340 |
|
1341 |
def verify_graph_integrity(self):
|
|
|
1523 |
answer = graph_search.query_graph(query)
|
1524 |
|
1525 |
response = ""
|
1526 |
+
async for chunk in reasoner.answer(query, answer):
|
1527 |
response += chunk
|
1528 |
print(response, end="", flush=True)
|
1529 |
|
src/rag/neo4j_graphrag.py
CHANGED
@@ -12,7 +12,7 @@ from src.query_processing.query_processor import QueryProcessor
|
|
12 |
from src.reasoning.reasoner import Reasoner
|
13 |
from src.utils.api_key_manager import APIKeyManager
|
14 |
from src.search.search_engine import SearchEngine
|
15 |
-
from src.crawl.crawler import CustomCrawler
|
16 |
from sentence_transformers import SentenceTransformer
|
17 |
from bert_score.scorer import BERTScorer
|
18 |
import numpy as np
|
@@ -51,7 +51,7 @@ class Neo4jGraphRAG:
|
|
51 |
model_type="roberta-base",
|
52 |
lang="en",
|
53 |
rescale_with_baseline=True,
|
54 |
-
device= "
|
55 |
)
|
56 |
|
57 |
# Counters and tracking
|
@@ -705,7 +705,7 @@ class Neo4jGraphRAG:
|
|
705 |
# Filter the URLs based on the query
|
706 |
filtered_urls = await self.search_engine.filter_urls(
|
707 |
sub_query,
|
708 |
-
"
|
709 |
results
|
710 |
)
|
711 |
# Emit an event with the filtered URLs
|
@@ -785,6 +785,11 @@ class Neo4jGraphRAG:
|
|
785 |
|
786 |
except Exception as e:
|
787 |
print(f"Error processing node {node_id}: {str(e)}")
|
|
|
|
|
|
|
|
|
|
|
788 |
future.set_exception(e)
|
789 |
raise
|
790 |
|
@@ -829,12 +834,22 @@ class Neo4jGraphRAG:
|
|
829 |
self, node_id, modified_query, session_id, future, depth, max_tokens_allowed
|
830 |
)
|
831 |
except Exception as e:
|
|
|
|
|
|
|
|
|
|
|
832 |
if not future.done():
|
833 |
future.set_exception(e)
|
834 |
raise
|
835 |
|
836 |
except Exception as e:
|
837 |
print(f"Error processing dependent node {node_id}: {str(e)}")
|
|
|
|
|
|
|
|
|
|
|
838 |
if not future.done():
|
839 |
future.set_exception(e)
|
840 |
raise
|
@@ -1159,6 +1174,12 @@ class Neo4jGraphRAG:
|
|
1159 |
# Process completion tasks
|
1160 |
if depth == 0:
|
1161 |
print("Graph building complete, processing final tasks...")
|
|
|
|
|
|
|
|
|
|
|
|
|
1162 |
# Create cross-connections
|
1163 |
create_cross_connections(self)
|
1164 |
print("All cross-connections have been created!")
|
@@ -1180,6 +1201,8 @@ class Neo4jGraphRAG:
|
|
1180 |
node1, node2, query, threshold
|
1181 |
)
|
1182 |
|
|
|
|
|
1183 |
async def process_graph(
|
1184 |
self,
|
1185 |
query: str,
|
@@ -1763,6 +1786,7 @@ class Neo4jGraphRAG:
|
|
1763 |
def prune_edges(self, max_edges: int = 1000):
|
1764 |
"""Prune excess edges while preserving node data."""
|
1765 |
try:
|
|
|
1766 |
with self.transaction() as tx:
|
1767 |
try:
|
1768 |
# Count current edges
|
@@ -1811,6 +1835,8 @@ class Neo4jGraphRAG:
|
|
1811 |
tx.commit()
|
1812 |
print(f"Pruned edges. Kept top {max_edges} edges by weight.")
|
1813 |
|
|
|
|
|
1814 |
except Exception as e:
|
1815 |
tx.rollback()
|
1816 |
raise e
|
@@ -2153,7 +2179,7 @@ Present your analysis as a detailed, well-formatted report.""",
|
|
2153 |
- Evaluate the differences in economy, trade, and military
|
2154 |
- Evaluate the differences in technology and infrastructure
|
2155 |
2. What were the similarities between the two civilizations?
|
2156 |
-
- Evaluate the similarities in governance, society, and culture
|
2157 |
- Evaluate the similarities in economy, trade, and military
|
2158 |
- Evaluate the similarities in technology and infrastructure
|
2159 |
3. How did these two civilizations influence each other?
|
@@ -2186,7 +2212,7 @@ Present your analysis as a detailed, well-formatted report.""",
|
|
2186 |
# Query the graph and generate a response
|
2187 |
answer = graph_search.query_graph(query)
|
2188 |
response = ""
|
2189 |
-
async for chunk in reasoner.
|
2190 |
response += chunk
|
2191 |
print(response, end="", flush=True)
|
2192 |
|
|
|
12 |
from src.reasoning.reasoner import Reasoner
|
13 |
from src.utils.api_key_manager import APIKeyManager
|
14 |
from src.search.search_engine import SearchEngine
|
15 |
+
from src.crawl.crawler import Crawler, CustomCrawler
|
16 |
from sentence_transformers import SentenceTransformer
|
17 |
from bert_score.scorer import BERTScorer
|
18 |
import numpy as np
|
|
|
51 |
model_type="roberta-base",
|
52 |
lang="en",
|
53 |
rescale_with_baseline=True,
|
54 |
+
device= "cuda" if torch.cuda.is_available() else "cpu"
|
55 |
)
|
56 |
|
57 |
# Counters and tracking
|
|
|
705 |
# Filter the URLs based on the query
|
706 |
filtered_urls = await self.search_engine.filter_urls(
|
707 |
sub_query,
|
708 |
+
"ultra",
|
709 |
results
|
710 |
)
|
711 |
# Emit an event with the filtered URLs
|
|
|
785 |
|
786 |
except Exception as e:
|
787 |
print(f"Error processing node {node_id}: {str(e)}")
|
788 |
+
if depth == 0:
|
789 |
+
await self.emit_event("sub_query_failed", {
|
790 |
+
"node_id": node_id,
|
791 |
+
"sub_query": sub_query
|
792 |
+
})
|
793 |
future.set_exception(e)
|
794 |
raise
|
795 |
|
|
|
834 |
self, node_id, modified_query, session_id, future, depth, max_tokens_allowed
|
835 |
)
|
836 |
except Exception as e:
|
837 |
+
if depth == 0:
|
838 |
+
await self.emit_event("sub_query_failed", {
|
839 |
+
"node_id": node_id,
|
840 |
+
"sub_query": sub_query
|
841 |
+
})
|
842 |
if not future.done():
|
843 |
future.set_exception(e)
|
844 |
raise
|
845 |
|
846 |
except Exception as e:
|
847 |
print(f"Error processing dependent node {node_id}: {str(e)}")
|
848 |
+
if depth == 0:
|
849 |
+
await self.emit_event("sub_query_failed", {
|
850 |
+
"node_id": node_id,
|
851 |
+
"sub_query": sub_query
|
852 |
+
})
|
853 |
if not future.done():
|
854 |
future.set_exception(e)
|
855 |
raise
|
|
|
1174 |
# Process completion tasks
|
1175 |
if depth == 0:
|
1176 |
print("Graph building complete, processing final tasks...")
|
1177 |
+
await self.emit_event("search_process_completed", {
|
1178 |
+
"depth": depth,
|
1179 |
+
"sub_queries": sub_queries,
|
1180 |
+
"roles": roles
|
1181 |
+
})
|
1182 |
+
|
1183 |
# Create cross-connections
|
1184 |
create_cross_connections(self)
|
1185 |
print("All cross-connections have been created!")
|
|
|
1201 |
node1, node2, query, threshold
|
1202 |
)
|
1203 |
|
1204 |
+
print("All similarity-based edges have been added!")
|
1205 |
+
|
1206 |
async def process_graph(
|
1207 |
self,
|
1208 |
query: str,
|
|
|
1786 |
def prune_edges(self, max_edges: int = 1000):
|
1787 |
"""Prune excess edges while preserving node data."""
|
1788 |
try:
|
1789 |
+
print(f"Pruning edges to keep top {max_edges} edges by weight...")
|
1790 |
with self.transaction() as tx:
|
1791 |
try:
|
1792 |
# Count current edges
|
|
|
1835 |
tx.commit()
|
1836 |
print(f"Pruned edges. Kept top {max_edges} edges by weight.")
|
1837 |
|
1838 |
+
print("No pruning needed. Current edge count is within limits.")
|
1839 |
+
|
1840 |
except Exception as e:
|
1841 |
tx.rollback()
|
1842 |
raise e
|
|
|
2179 |
- Evaluate the differences in economy, trade, and military
|
2180 |
- Evaluate the differences in technology and infrastructure
|
2181 |
2. What were the similarities between the two civilizations?
|
2182 |
+
- Evaluate the similarities in governance, society, and culture
|
2183 |
- Evaluate the similarities in economy, trade, and military
|
2184 |
- Evaluate the similarities in technology and infrastructure
|
2185 |
3. How did these two civilizations influence each other?
|
|
|
2212 |
# Query the graph and generate a response
|
2213 |
answer = graph_search.query_graph(query)
|
2214 |
response = ""
|
2215 |
+
async for chunk in reasoner.answer(query, answer):
|
2216 |
response += chunk
|
2217 |
print(response, end="", flush=True)
|
2218 |
|
src/reasoning/reasoner.py
CHANGED
@@ -1,3 +1,4 @@
|
|
|
|
1 |
from langchain.prompts import ChatPromptTemplate
|
2 |
from langchain_core.prompts import ChatPromptTemplate
|
3 |
from src.utils.api_key_manager import APIKeyManager, with_api_manager
|
@@ -9,43 +10,182 @@ class Reasoner:
|
|
9 |
self.model = self.manager.get_llm()
|
10 |
|
11 |
@with_api_manager(streaming=True)
|
12 |
-
async def
|
13 |
self,
|
14 |
query,
|
15 |
context=None,
|
|
|
16 |
*,
|
17 |
llm
|
18 |
):
|
19 |
if context is None:
|
20 |
template = \
|
21 |
-
"""You are an
|
22 |
-
|
23 |
|
24 |
-
|
25 |
-
|
26 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
27 |
|
28 |
Query:
|
29 |
-
{query}
|
|
|
|
|
|
|
30 |
prompt = ChatPromptTemplate.from_template(template)
|
31 |
-
messages = prompt.format_messages(query=query)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
32 |
else:
|
33 |
template = \
|
34 |
-
"""You are an
|
35 |
-
|
36 |
|
37 |
-
|
38 |
-
|
39 |
-
|
40 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
41 |
|
42 |
-
|
43 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
44 |
|
45 |
Query:
|
46 |
-
{query}
|
|
|
|
|
|
|
47 |
prompt = ChatPromptTemplate.from_template(template)
|
48 |
-
messages = prompt.format_messages(context=context, query=query)
|
49 |
|
50 |
try:
|
51 |
async for chunk in llm.astream(messages):
|
@@ -101,6 +241,72 @@ Document:
|
|
101 |
response = await llm.ainvoke(messages)
|
102 |
return response.content.strip()
|
103 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
104 |
if __name__ == "__main__":
|
105 |
import asyncio
|
106 |
from src.crawl.crawler import Crawler
|
@@ -115,4 +321,5 @@ if __name__ == "__main__":
|
|
115 |
rotate_proxy=False,
|
116 |
return_html=True
|
117 |
))
|
118 |
-
print(contents)
|
|
|
|
1 |
+
from datetime import datetime, timezone
|
2 |
from langchain.prompts import ChatPromptTemplate
|
3 |
from langchain_core.prompts import ChatPromptTemplate
|
4 |
from src.utils.api_key_manager import APIKeyManager, with_api_manager
|
|
|
10 |
self.model = self.manager.get_llm()
|
11 |
|
12 |
@with_api_manager(streaming=True)
|
13 |
+
async def answer(
|
14 |
self,
|
15 |
query,
|
16 |
context=None,
|
17 |
+
query_type="general",
|
18 |
*,
|
19 |
llm
|
20 |
):
|
21 |
if context is None:
|
22 |
template = \
|
23 |
+
"""You are an AI model skilled in web search and crafting detailed, engaging, and well-structured answers.
|
24 |
+
You excel at summarizing web pages and extracting relevant information to create professional, blog-style responses.
|
25 |
|
26 |
+
Your task is to provide answers that are:
|
27 |
+
- **Informative and relevant**: Thoroughly address the user's query.
|
28 |
+
- **Well-structured**: Include clear headings and subheadings, and use a professional tone to present information concisely and logically.
|
29 |
+
- **Engaging and detailed**: Write responses that read like a high-quality blog post, including extra details and relevant insights.
|
30 |
+
- **Explanatory and Comprehensive**: Strive to explain the topic in depth, offering detailed analysis, insights, and clarifications wherever applicable.
|
31 |
+
|
32 |
+
### Formatting Instructions
|
33 |
+
- **Structure**: Use a well-organized format with proper headings (e.g., "## Example heading 1" or "## Example heading 2").
|
34 |
+
Present information in paragraphs or concise bullet points where appropriate.
|
35 |
+
- **Tone and Style**: Maintain a neutral, journalistic tone with engaging narrative flow.
|
36 |
+
Write as though you're crafting an in-depth article for a professional audience.
|
37 |
+
- **Markdown Usage**: Format your response with Markdown for clarity. Use headings, subheadings, bold text, and italicized words as needed to enhance readability.
|
38 |
+
- **Length and Depth**: Provide comprehensive coverage of the topic. Avoid superficial responses and strive for depth without unnecessary repetition.
|
39 |
+
Expand on technical or complex topics to make them easier to understand for a general audience.
|
40 |
+
- **No main heading/title**: Start your response directly with the introduction unless asked to provide a specific title.
|
41 |
+
- **Conclusion or Summary**: Include a concluding paragraph that synthesizes the provided information or suggests potential next steps, where appropriate.
|
42 |
+
|
43 |
+
### Special Instructions
|
44 |
+
- If the query involves technical, historical, or complex topics, provide detailed background and explanatory sections to ensure clarity.
|
45 |
+
- If the user provides vague input or if relevant information is missing, explain what additional details might help refine the search.
|
46 |
+
- If no relevant information is found, say:
|
47 |
+
"Hmm, sorry I could not find any relevant information on this topic. Would you like me to search again or ask something else?"
|
48 |
+
Be transparent about limitations and suggest alternatives or ways to reframe the query.
|
49 |
+
|
50 |
+
### User instructions
|
51 |
+
- These instructions are shared to you by the user as part of the query itself.
|
52 |
+
- You will have to follow them and give them higher priority than the above instructions.
|
53 |
+
- If the user has provided specific instructions or preferences, incorporate them into your response while adhering to the overall guidelines.
|
54 |
+
- If no instructions are provided, follow the general guidelines and instructions above.
|
55 |
+
|
56 |
+
### Example Output
|
57 |
+
- Begin with a brief introduction summarizing the event or query topic.
|
58 |
+
- Follow with detailed sections under clear headings, covering all aspects of the query if possible.
|
59 |
+
- Provide explanations or historical context as needed to enhance understanding.
|
60 |
+
- End with a conclusion or overall perspective if relevant.
|
61 |
|
62 |
Query:
|
63 |
+
{query}
|
64 |
+
|
65 |
+
Current date & time in ISO format (UTC timezone): {date}"""
|
66 |
+
|
67 |
prompt = ChatPromptTemplate.from_template(template)
|
68 |
+
messages = prompt.format_messages(query=query, date=datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M:%S'))
|
69 |
+
|
70 |
+
elif query_type == "basic" and "[USER PROVIDED" in context:
|
71 |
+
template = \
|
72 |
+
"""You are an AI model skilled in web search and crafting detailed, engaging, and well-structured answers.
|
73 |
+
You excel at summarizing web pages and extracting relevant information to create professional, blog-style responses.
|
74 |
+
|
75 |
+
Your task is to provide answers that are:
|
76 |
+
- **Informative and relevant**: Thoroughly address the user's query.
|
77 |
+
- **Well-structured**: Include clear headings and subheadings, and use a professional tone to present information concisely and logically.
|
78 |
+
- **Engaging and detailed**: Write responses that read like a high-quality blog post, including extra details and relevant insights.
|
79 |
+
- **Explanatory and Comprehensive**: Strive to explain the topic in depth, offering detailed analysis, insights, and clarifications wherever applicable.
|
80 |
+
|
81 |
+
### Formatting Instructions
|
82 |
+
- **Structure**: Use a well-organized format with proper headings (e.g., "## Example heading 1" or "## Example heading 2").
|
83 |
+
Present information in paragraphs or concise bullet points where appropriate.
|
84 |
+
- **Tone and Style**: Maintain a neutral, journalistic tone with engaging narrative flow.
|
85 |
+
Write as though you're crafting an in-depth article for a professional audience.
|
86 |
+
- **Markdown Usage**: Format your response with Markdown for clarity. Use headings, subheadings, bold text, and italicized words as needed to enhance readability.
|
87 |
+
- **Length and Depth**: Provide comprehensive coverage of the topic. Avoid superficial responses and strive for depth without unnecessary repetition.
|
88 |
+
Expand on technical or complex topics to make them easier to understand for a general audience.
|
89 |
+
- **No main heading/title**: Start your response directly with the introduction unless asked to provide a specific title.
|
90 |
+
- **Conclusion or Summary**: Include a concluding paragraph that synthesizes the provided information or suggests potential next steps, where appropriate.
|
91 |
+
|
92 |
+
### Special Instructions
|
93 |
+
- If the query involves technical, historical, or complex topics, provide detailed background and explanatory sections to ensure clarity.
|
94 |
+
- If the user provides vague input or if relevant information is missing, explain what additional details might help refine the search.
|
95 |
+
- All user-provided files and/or links must be given higher priority to those sources when crafting the response.
|
96 |
+
- If no relevant information is found, say:
|
97 |
+
"Hmm, sorry I could not find any relevant information on this topic. Would you like me to search again or ask something else?"
|
98 |
+
Be transparent about limitations and suggest alternatives or ways to reframe the query.
|
99 |
+
|
100 |
+
### User instructions
|
101 |
+
- These instructions are shared to you by the user as part of the query itself.
|
102 |
+
- You will have to follow them and give them higher priority than the above instructions.
|
103 |
+
- If the user has provided specific instructions or preferences, incorporate them into your response while adhering to the overall guidelines.
|
104 |
+
- If no instructions are provided, follow the general guidelines and instructions above.
|
105 |
+
|
106 |
+
### Example Output
|
107 |
+
- Begin with a brief introduction summarizing the event or query topic.
|
108 |
+
- Follow with detailed sections under clear headings, covering all aspects of the query if possible.
|
109 |
+
- Provide explanations or historical context as needed to enhance understanding.
|
110 |
+
- End with a conclusion or overall perspective if relevant.
|
111 |
+
|
112 |
+
Context:
|
113 |
+
{context}
|
114 |
+
|
115 |
+
Query:
|
116 |
+
{query}
|
117 |
+
|
118 |
+
Current date & time in ISO format (UTC timezone): {date}"""
|
119 |
+
|
120 |
+
prompt = ChatPromptTemplate.from_template(template)
|
121 |
+
messages = prompt.format_messages(context=context, query=query, date=datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M:%S'))
|
122 |
+
|
123 |
else:
|
124 |
template = \
|
125 |
+
"""You are an AI model skilled in web search and crafting detailed, engaging, and well-structured answers.
|
126 |
+
You excel at summarizing web pages and extracting relevant information to create professional, blog-style responses.
|
127 |
|
128 |
+
Your task is to provide answers that are:
|
129 |
+
- **Informative and relevant**: Thoroughly address the user's query using the given context.
|
130 |
+
- **Well-structured**: Include clear headings and subheadings, and use a professional tone to present information concisely and logically.
|
131 |
+
- **Engaging and detailed**: Write responses that read like a high-quality blog post, including extra details and relevant insights.
|
132 |
+
- **Cited and credible**: Use inline citations with [number] notation to refer to the context source(s) for each fact or detail included.
|
133 |
+
- **Explanatory and Comprehensive**: Strive to explain the topic in depth, offering detailed analysis, insights, and clarifications wherever applicable.
|
134 |
+
|
135 |
+
### Formatting Instructions
|
136 |
+
- **Structure**: Use a well-organized format with proper headings (e.g., "## Example heading 1" or "## Example heading 2").
|
137 |
+
Present information in paragraphs or concise bullet points where appropriate.
|
138 |
+
- **Tone and Style**: Maintain a neutral, journalistic tone with engaging narrative flow.
|
139 |
+
Write as though you're crafting an in-depth article for a professional audience.
|
140 |
+
- **Markdown Usage**: Format your response with Markdown for clarity. Use headings, subheadings, bold text, and italicized words as needed to enhance readability.
|
141 |
+
- **Length and Depth**: Provide comprehensive coverage of the topic. Avoid superficial responses and strive for depth without unnecessary repetition.
|
142 |
+
Expand on technical or complex topics to make them easier to understand for a general audience.
|
143 |
+
- **No main heading/title**: Start your response directly with the introduction unless asked to provide a specific title.
|
144 |
+
- **Conclusion or Summary**: Include a concluding paragraph that synthesizes the provided information or suggests potential next steps, where appropriate.
|
145 |
+
|
146 |
+
### [IMPORTANT] Citation Requirements
|
147 |
+
- Cite every single fact, statement, or sentence using [number] notation corresponding to the source from the provided `context`.
|
148 |
+
Each source in the `context` will be in the following format, where N is the source number:-
|
149 |
+
[SOURCE N START]
|
150 |
+
source content...
|
151 |
+
[SOURCE N END]
|
152 |
+
- Integrate citations naturally at the end of sentences or clauses as appropriate.
|
153 |
+
For example, "The Eiffel Tower is one of the most visited landmarks in the world[1]."
|
154 |
+
- [IMPORTANT] If applicable, use multiple sources for a single detail, such as, "Paris is a cultural hub, attracting millions of visitors annually[1][2]."
|
155 |
+
*DO NOT* use two numbers in the same citation marker, e.g., [1,2] is *NOT* valid.
|
156 |
+
- Always prioritize credibility and accuracy by linking all statements back to their respective context sources.
|
157 |
+
- Avoid citing unsupported assumptions or personal interpretations; if no source supports a statement, clearly indicate the limitation.
|
158 |
|
159 |
+
### Special Instructions
|
160 |
+
- If the query involves technical, historical, or complex topics, provide detailed background and explanatory sections to ensure clarity.
|
161 |
+
- If the user provides vague input or if relevant information is missing, explain what additional details might help refine the search.
|
162 |
+
- If the context contains any user-provided files and/or links, ensure to give higher priority to those sources when crafting the response.
|
163 |
+
- If no relevant information is found, say:
|
164 |
+
"Hmm, sorry I could not find any relevant information on this topic. Would you like me to search again or ask something else?"
|
165 |
+
Be transparent about limitations and suggest alternatives or ways to reframe the query.
|
166 |
+
|
167 |
+
### User instructions
|
168 |
+
- These instructions are shared to you by the user as part of the query itself.
|
169 |
+
- You will have to follow them and give them higher priority than the above instructions.
|
170 |
+
- If the user has provided specific instructions or preferences, incorporate them into your response while adhering to the overall guidelines.
|
171 |
+
- If no instructions are provided, follow the general guidelines and instructions above.
|
172 |
+
|
173 |
+
### Example Output
|
174 |
+
- Begin with a brief introduction summarizing the event or query topic.
|
175 |
+
- Follow with detailed sections under clear headings, covering all aspects of the query if possible.
|
176 |
+
- Provide explanations or historical context as needed to enhance understanding.
|
177 |
+
- End with a conclusion or overall perspective if relevant.
|
178 |
+
|
179 |
+
Context:
|
180 |
+
{context}
|
181 |
|
182 |
Query:
|
183 |
+
{query}
|
184 |
+
|
185 |
+
Current date & time in ISO format (UTC timezone): {date}"""
|
186 |
+
|
187 |
prompt = ChatPromptTemplate.from_template(template)
|
188 |
+
messages = prompt.format_messages(context=context, query=query, date=datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M:%S'))
|
189 |
|
190 |
try:
|
191 |
async for chunk in llm.astream(messages):
|
|
|
241 |
response = await llm.ainvoke(messages)
|
242 |
return response.content.strip()
|
243 |
|
244 |
+
@with_api_manager()
|
245 |
+
async def get_excerpts(
|
246 |
+
self,
|
247 |
+
answer_text,
|
248 |
+
source_docs,
|
249 |
+
*,
|
250 |
+
llm
|
251 |
+
):
|
252 |
+
template= \
|
253 |
+
"""You are an expert at generating excerpts from long documents.
|
254 |
+
Your task is to find and extract the most relevant, contiguous sentence(s) or short passage from the Source Documents that directly supports the Answer Text.
|
255 |
+
|
256 |
+
The Source Documents are formatted with markers like [SOURCE N START] and [SOURCE N END], where N is the source number.
|
257 |
+
The Answer Text uses citation markers like [N], where N directly corresponds to the source number N in the Source Documents.
|
258 |
+
In case of multiple citations, the Answer Text's citation markers will be like [N][M][...etc] (or in some cases, [N, M, ...etc]).
|
259 |
+
|
260 |
+
[IMPORTANT] Rules:
|
261 |
+
1. You must carefully read and analyse the Answer Text and the Source Documents.
|
262 |
+
2. The excerpts should be concise but detailed, precise and accurate.
|
263 |
+
3. Focus on extracting key information, facts, and data that are directly relevant to the answer.
|
264 |
+
4. Include specific details, numbers, and quotes when they are important.
|
265 |
+
5. Ensure the excerpts are verbatim and extracted directly from the context without any paraphrasing or alteration.
|
266 |
+
6. Your output should be a valid python list as shown in the output format below.
|
267 |
+
7. If you cannot find any relevant excerpts, say "Excerpt not found".
|
268 |
+
|
269 |
+
Output Format:
|
270 |
+
[
|
271 |
+
{{<statement 1>: {{<source number>: <extracted excerpt 1>,
|
272 |
+
<source number>: <extracted excerpt 2>,
|
273 |
+
and so on...}}
|
274 |
+
}},
|
275 |
+
{{<statement 2>: {{<source number>: <extracted excerpt 1>,
|
276 |
+
<source number>: <extracted excerpt 2>,
|
277 |
+
and so on...}}
|
278 |
+
}},
|
279 |
+
...and so on
|
280 |
+
]
|
281 |
+
|
282 |
+
Example Output:
|
283 |
+
[
|
284 |
+
{{"The Treaty of Waitangi is a foundational document in New Zealand's history.": {{
|
285 |
+
1: "The Treaty of Waitangi, signed in 1840, is considered the founding document of New Zealand."
|
286 |
+
}}
|
287 |
+
}},
|
288 |
+
{{"Signed in 1840, the principles of the Treaty are often debated.": {{
|
289 |
+
1: "The Treaty of Waitangi, signed in 1840, is considered the founding document of New Zealand.",
|
290 |
+
2: "The principles of the Treaty are often debated in legal and political contexts."
|
291 |
+
}}
|
292 |
+
}},
|
293 |
+
{{"The Treaty can arguably lead to a civil war in New Zealand.": {{
|
294 |
+
"NA": "Excerpt not found"
|
295 |
+
}}
|
296 |
+
}}
|
297 |
+
]
|
298 |
+
|
299 |
+
Source Documents:
|
300 |
+
{source_docs}
|
301 |
+
|
302 |
+
Answer Text:
|
303 |
+
{answer_text}"""
|
304 |
+
|
305 |
+
prompt = ChatPromptTemplate.from_template(template)
|
306 |
+
messages = prompt.format_messages(answer_text=answer_text, source_docs=source_docs)
|
307 |
+
response = await llm.ainvoke(messages)
|
308 |
+
return response.content.strip()
|
309 |
+
|
310 |
if __name__ == "__main__":
|
311 |
import asyncio
|
312 |
from src.crawl.crawler import Crawler
|
|
|
321 |
rotate_proxy=False,
|
322 |
return_html=True
|
323 |
))
|
324 |
+
print(contents)
|
325 |
+
|
src/search/search_engine.py
CHANGED
@@ -245,10 +245,10 @@ Consider factors such as:
|
|
245 |
Rules:
|
246 |
1. Rerank the URLs based on their relevance to the query according to the criteria listed above, from best match to worst match.
|
247 |
2. Once reranked, select the top best matched results according to the category of the query as defined below:
|
248 |
-
-
|
249 |
-
-
|
250 |
-
-
|
251 |
-
-
|
252 |
3. [IMPORTANT] Select the MINIMUM number of results (based on the categories above) that are required to answer the query.
|
253 |
4. The response should only contain a JSON array of objects, each containing 'link', 'title' and 'snippet' keys after reranking and filtering.
|
254 |
|
@@ -315,7 +315,7 @@ if __name__ == "__main__":
|
|
315 |
print(f"Time taken to fetch search results: {end - start:.2f} seconds")
|
316 |
# filtered_search = search_engine.filter_urls(
|
317 |
# optimized_query,
|
318 |
-
# category="
|
319 |
# search_results=search_results,
|
320 |
# num_results=2
|
321 |
# )
|
|
|
245 |
Rules:
|
246 |
1. Rerank the URLs based on their relevance to the query according to the criteria listed above, from best match to worst match.
|
247 |
2. Once reranked, select the top best matched results according to the category of the query as defined below:
|
248 |
+
- Advanced: Select upto 3 top best matched results
|
249 |
+
- Pro: Select upto 4 top best matched results
|
250 |
+
- Super: Select upto 5 top best matched results
|
251 |
+
- Ultra: Select upto 6 top best matched results
|
252 |
3. [IMPORTANT] Select the MINIMUM number of results (based on the categories above) that are required to answer the query.
|
253 |
4. The response should only contain a JSON array of objects, each containing 'link', 'title' and 'snippet' keys after reranking and filtering.
|
254 |
|
|
|
315 |
print(f"Time taken to fetch search results: {end - start:.2f} seconds")
|
316 |
# filtered_search = search_engine.filter_urls(
|
317 |
# optimized_query,
|
318 |
+
# category="Advanced",
|
319 |
# search_results=search_results,
|
320 |
# num_results=2
|
321 |
# )
|
src/utils/api_key_manager.py
CHANGED
@@ -24,66 +24,45 @@ class APIKeyManager:
|
|
24 |
# Define supported models
|
25 |
SUPPORTED_MODELS = {
|
26 |
"openai": [
|
27 |
-
"gpt-3.5-turbo",
|
28 |
-
"gpt-3.5-turbo-instruct",
|
29 |
-
"gpt-3.5-turbo-1106",
|
30 |
-
"gpt-3.5-turbo-0125",
|
31 |
-
"gpt-4-0314",
|
32 |
-
"gpt-4-0613",
|
33 |
-
"gpt-4",
|
34 |
-
"gpt-4-1106-preview",
|
35 |
-
"gpt-4-0125-preview",
|
36 |
-
"gpt-4-turbo-preview",
|
37 |
-
"gpt-4-turbo-2024-04-09",
|
38 |
-
"gpt-4-turbo",
|
39 |
-
"o1-mini-2024-09-12",
|
40 |
"o1-mini",
|
41 |
-
"o1-preview-2024-09-12",
|
42 |
-
"o1-preview",
|
43 |
"o1",
|
|
|
|
|
|
|
|
|
44 |
"gpt-4o-mini-2024-07-18",
|
45 |
"gpt-4o-mini",
|
46 |
"chatgpt-4o-latest",
|
47 |
"gpt-4o-2024-05-13",
|
48 |
"gpt-4o-2024-08-06",
|
49 |
"gpt-4o-2024-11-20",
|
50 |
-
"gpt-4o"
|
|
|
|
|
|
|
51 |
],
|
52 |
"google": [
|
53 |
-
"gemini-1.5-flash",
|
54 |
-
"gemini-1.5-flash-latest",
|
55 |
-
"gemini-1.5-flash-exp-0827",
|
56 |
-
"gemini-1.5-flash-001",
|
57 |
-
"gemini-1.5-flash-002",
|
58 |
-
"gemini-1.5-flash-8b-exp-0924",
|
59 |
-
"gemini-1.5-flash-8b-exp-0827",
|
60 |
-
"gemini-1.5-flash-8b-001",
|
61 |
-
"gemini-1.5-flash-8b",
|
62 |
-
"gemini-1.5-flash-8b-latest",
|
63 |
-
"gemini-1.5-pro",
|
64 |
-
"gemini-1.5-pro-latest",
|
65 |
-
"gemini-1.5-pro-001",
|
66 |
-
"gemini-1.5-pro-002",
|
67 |
-
"gemini-1.5-pro-exp-0827",
|
68 |
-
"gemini-1.0-pro",
|
69 |
-
"gemini-1.0-pro-latest",
|
70 |
-
"gemini-1.0-pro-001",
|
71 |
-
"gemini-pro",
|
72 |
-
"gemini-exp-1114",
|
73 |
-
"gemini-exp-1121",
|
74 |
"gemini-2.0-pro-exp-02-05",
|
75 |
"gemini-2.0-flash-lite-preview-02-05",
|
76 |
"gemini-2.0-flash-exp",
|
77 |
"gemini-2.0-flash",
|
78 |
"gemini-2.0-flash-thinking-exp-1219",
|
|
|
|
|
|
|
|
|
79 |
],
|
80 |
"xai": [
|
81 |
-
"grok-
|
82 |
-
"grok-
|
83 |
-
"grok-
|
84 |
-
"grok-
|
|
|
85 |
],
|
86 |
"anthropic": [
|
|
|
|
|
|
|
87 |
"claude-3-5-sonnet-20241022",
|
88 |
"claude-3-5-sonnet-latest",
|
89 |
"claude-3-5-haiku-20241022",
|
@@ -275,14 +254,14 @@ class APIKeyManager:
|
|
275 |
api_key = self.get_next_api_key(provider)
|
276 |
print(f"Using provider={provider}, model_name={model_name}, "
|
277 |
f"temperature={temperature}, top_p={top_p}, key={api_key}")
|
278 |
-
|
279 |
kwargs = {
|
280 |
"model": model_name,
|
281 |
"temperature": temperature,
|
282 |
"top_p": top_p,
|
283 |
"max_retries": 0,
|
284 |
"streaming": streaming,
|
285 |
-
"api_key": api_key
|
286 |
}
|
287 |
|
288 |
if max_tokens is not None:
|
@@ -611,5 +590,5 @@ if __name__ == "__main__":
|
|
611 |
except Exception as e:
|
612 |
raise Exception(f"Error with {model_name}: {str(e)}")
|
613 |
|
614 |
-
|
615 |
-
asyncio.run(test_load_balancing(prompt=prompt, test_count=100, stream=True))
|
|
|
24 |
# Define supported models
|
25 |
SUPPORTED_MODELS = {
|
26 |
"openai": [
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
27 |
"o1-mini",
|
|
|
|
|
28 |
"o1",
|
29 |
+
"o1-pro",
|
30 |
+
"o3-mini",
|
31 |
+
"o3",
|
32 |
+
"o4-mini",
|
33 |
"gpt-4o-mini-2024-07-18",
|
34 |
"gpt-4o-mini",
|
35 |
"chatgpt-4o-latest",
|
36 |
"gpt-4o-2024-05-13",
|
37 |
"gpt-4o-2024-08-06",
|
38 |
"gpt-4o-2024-11-20",
|
39 |
+
"gpt-4o",
|
40 |
+
"gpt-4.1-nano",
|
41 |
+
"gpt-4.1-mini",
|
42 |
+
"gpt-4.1"
|
43 |
],
|
44 |
"google": [
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
45 |
"gemini-2.0-pro-exp-02-05",
|
46 |
"gemini-2.0-flash-lite-preview-02-05",
|
47 |
"gemini-2.0-flash-exp",
|
48 |
"gemini-2.0-flash",
|
49 |
"gemini-2.0-flash-thinking-exp-1219",
|
50 |
+
"gemini-2.5-flash-lite-preview-06-17",
|
51 |
+
"gemini-2.5-flash-preview-04-17",
|
52 |
+
"gemini-2.5-flash",
|
53 |
+
"gemini-2.5-pro"
|
54 |
],
|
55 |
"xai": [
|
56 |
+
"grok-2",
|
57 |
+
"grok-3-mini-latest",
|
58 |
+
"grok-3-mini-fast-latest",
|
59 |
+
"grok-3-latest",
|
60 |
+
"grok-3-fast-latest"
|
61 |
],
|
62 |
"anthropic": [
|
63 |
+
"claude-opus-4-20250514",
|
64 |
+
"claude-sonnet-4-20250514",
|
65 |
+
"claude-3-7-sonnet-20250219",
|
66 |
"claude-3-5-sonnet-20241022",
|
67 |
"claude-3-5-sonnet-latest",
|
68 |
"claude-3-5-haiku-20241022",
|
|
|
254 |
api_key = self.get_next_api_key(provider)
|
255 |
print(f"Using provider={provider}, model_name={model_name}, "
|
256 |
f"temperature={temperature}, top_p={top_p}, key={api_key}")
|
257 |
+
|
258 |
kwargs = {
|
259 |
"model": model_name,
|
260 |
"temperature": temperature,
|
261 |
"top_p": top_p,
|
262 |
"max_retries": 0,
|
263 |
"streaming": streaming,
|
264 |
+
"api_key": api_key
|
265 |
}
|
266 |
|
267 |
if max_tokens is not None:
|
|
|
590 |
except Exception as e:
|
591 |
raise Exception(f"Error with {model_name}: {str(e)}")
|
592 |
|
593 |
+
test_without_load_balancing(model_name="gemini-2.5-flash-lite-preview-06-17", prompt=prompt, test_count=50)
|
594 |
+
# asyncio.run(test_load_balancing(prompt=prompt, test_count=100, stream=True))
|