Hemang Thakur commited on
Commit
44ebcd1
·
1 Parent(s): 4aefbee

demo is ready

Browse files
Files changed (39) hide show
  1. frontend/package-lock.json +0 -0
  2. frontend/package.json +2 -1
  3. frontend/public/auth-receiver.html +107 -0
  4. frontend/src/Components/AiComponents/ChatComponents/SourcePopup.css +77 -0
  5. frontend/src/Components/AiComponents/ChatComponents/SourcePopup.js +187 -0
  6. frontend/src/Components/AiComponents/ChatComponents/SourceRef.css +21 -0
  7. frontend/src/Components/AiComponents/ChatComponents/Streaming.css +161 -119
  8. frontend/src/Components/AiComponents/ChatComponents/Streaming.js +10 -64
  9. frontend/src/Components/AiComponents/ChatWindow.css +15 -5
  10. frontend/src/Components/AiComponents/ChatWindow.js +106 -3
  11. frontend/src/Components/AiComponents/Dropdowns/AddContentDropdown.css +150 -0
  12. frontend/src/Components/AiComponents/Dropdowns/AddContentDropdown.js +354 -0
  13. frontend/src/Components/AiComponents/Dropdowns/AddFilesDialog.css +191 -0
  14. frontend/src/Components/AiComponents/Dropdowns/AddFilesDialog.js +282 -0
  15. frontend/src/Components/AiComponents/Markdown/CustomMarkdown.js +489 -0
  16. frontend/src/Components/AiComponents/Markdown/TestMarkdown.js +120 -0
  17. frontend/src/Components/AiComponents/Notifications/Notification.css +379 -0
  18. frontend/src/Components/AiComponents/Notifications/Notification.js +242 -0
  19. frontend/src/Components/AiComponents/Notifications/useNotification.js +43 -0
  20. frontend/src/Components/AiComponents/Sidebars/LeftSideBar.js +38 -0
  21. frontend/src/Components/AiComponents/Sidebars/LeftSidebar.css +59 -0
  22. frontend/src/Components/AiComponents/Sidebars/RightSidebar.css +138 -0
  23. frontend/src/Components/AiComponents/Sidebars/RightSidebar.js +142 -0
  24. frontend/src/Components/AiPage.css +66 -6
  25. frontend/src/Components/AiPage.js +611 -32
  26. frontend/src/Components/IntialSetting.css +1 -1
  27. frontend/src/Components/IntialSetting.js +24 -17
  28. frontend/src/Icons/excerpts.png +0 -0
  29. frontend/src/Icons/excerpts.pngZone.Identifier +4 -0
  30. main.py +467 -48
  31. src/crawl/crawler.py +566 -566
  32. src/helpers/helper.py +33 -1
  33. src/integrations/mcp_client.py +506 -0
  34. src/query_processing/query_processor.py +55 -16
  35. src/rag/graph_rag.py +12 -8
  36. src/rag/neo4j_graphrag.py +31 -5
  37. src/reasoning/reasoner.py +226 -19
  38. src/search/search_engine.py +5 -5
  39. 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
- "@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,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
- font-family: inherit;
3
- line-height: 2rem;
4
- white-space: pre-wrap;
5
- word-wrap: break-word;
6
- margin: 0;
7
- padding: 0;
8
- }
9
-
10
- /* Reset margin/padding for all descendants */
11
- .streaming-content * {
12
- margin: 0;
13
- padding: 0;
14
- }
15
-
16
- /* Top-level elements */
17
- .streaming-content > * {
18
- margin-top: 0.5rem;
19
- margin-bottom: 0.5rem;
20
- }
21
-
22
- /* VERY FIRST element in an AI answer */
23
- .streaming-content > *:first-child {
24
- margin-top: 0 !important;
25
- }
26
-
27
- /* Headings */
28
- .streaming-content h1,
29
- .streaming-content h2,
30
- .streaming-content h3,
31
- .streaming-content h4,
32
- .streaming-content h5,
33
- .streaming-content h6 {
34
- margin-top: 1rem;
35
- margin-bottom: 0.75rem;
36
- }
37
-
38
- /* If heading is the very first element */
39
- .streaming-content > h1:first-child,
40
- .streaming-content > h2:first-child,
41
- .streaming-content > h3:first-child,
42
- .streaming-content > h4:first-child,
43
- .streaming-content > h5:first-child,
44
- .streaming-content > h6:first-child {
45
- margin-top: 0 !important;
46
- }
47
-
48
- /* Paragraphs */
49
- .streaming-content p {
50
- margin-top: 0.25rem;
51
- margin-bottom: 0.25rem;
52
- }
53
-
54
- /* Lists */
55
- .streaming-content ul,
56
- .streaming-content ol {
57
- margin-top: 0.25rem;
58
- margin-bottom: 0.25rem;
59
- padding-left: 1.25rem;
60
- white-space: normal;
61
- }
62
-
63
- .streaming-content li {
64
- margin-bottom: 0.25rem;
65
- }
66
-
67
- .streaming-content li ul,
68
- .streaming-content li ol {
69
- margin-top: 0.15rem;
70
- margin-bottom: 0.15rem;
71
- }
72
-
73
- /* Code Blocks */
74
- .code-block-container {
75
- margin: 0.5rem 0;
76
- border-radius: 4px;
77
- background-color: #2b2b2b;
78
- overflow: hidden;
79
- }
80
-
81
- .code-block-header {
82
- background-color: #1e1e1e;
83
- color: #ffffff;
84
- padding: 0.5rem;
85
- font-size: 0.85rem;
86
- font-weight: bold;
87
- }
88
-
89
- /* Table Container */
90
- .table-container {
91
- margin: 0.5rem 0;
92
- width: 100%;
93
- overflow-x: auto;
94
- border: 1px solid #ddd;
95
- border-radius: 4px;
96
- }
97
-
98
- .table-container th,
99
- .table-container td {
100
- border: 1px solid #ddd;
101
- padding: 0.5rem;
102
- }
103
-
104
- /* Markdown Links */
105
- .markdown-link {
106
- color: #1a73e8;
107
- text-decoration: none;
108
- }
109
- .markdown-link:hover {
110
- text-decoration: underline;
111
- }
112
-
113
- /* Blockquotes */
114
- .markdown-blockquote {
115
- border-left: 4px solid #ccc;
116
- padding-left: 0.75rem;
117
- margin: 0.5rem 0;
118
- color: #555;
119
- font-style: italic;
120
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 ReactMarkdown from 'react-markdown';
3
- import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
4
- import { atomDark } from 'react-syntax-highlighter/dist/esm/styles/prism';
5
- import remarkGfm from 'remark-gfm';
6
- import rehypeRaw from 'rehype-raw';
7
  import './Streaming.css';
 
8
 
9
  // Streaming component for rendering markdown content
10
- const Streaming = ({ content, isStreaming, onContentRef }) => {
11
  const contentRef = useRef(null);
12
 
13
  useEffect(() => {
14
  if (contentRef.current && onContentRef) {
15
  onContentRef(contentRef.current);
16
  }
17
- }, [content, onContentRef]);
18
-
19
- const displayContent = isStreaming ? `${content}▌` : (content || '');
20
 
21
  return (
22
  <div className="streaming-content" ref={contentRef}>
23
- <ReactMarkdown
24
- remarkPlugins={[remarkGfm]}
25
- rehypePlugins={[rehypeRaw]}
26
- components={{
27
- code({node, inline, className, children, ...props}) {
28
- const match = /language-(\w+)/.exec(className || '');
29
- return !inline ? (
30
- <div className="code-block-container">
31
- <div className="code-block-header">
32
- <span>{match ? match[1] : 'code'}</span>
33
- </div>
34
- <SyntaxHighlighter
35
- style={atomDark}
36
- language={match ? match[1] : 'text'}
37
- PreTag="div"
38
- {...props}
39
- >
40
- {String(children).replace(/\n$/, '')}
41
- </SyntaxHighlighter>
42
- </div>
43
- ) : (
44
- <code className={className} {...props}>
45
- {children}
46
- </code>
47
- );
48
- },
49
- table({node, ...props}) {
50
- return (
51
- <div className="table-container">
52
- <table {...props} />
53
- </div>
54
- );
55
- },
56
- a({node, children, href, ...props}) {
57
- return (
58
- <a
59
- href={href}
60
- target="_blank"
61
- rel="noopener noreferrer"
62
- className="markdown-link"
63
- {...props}
64
- >
65
- {children}
66
- </a>
67
- );
68
- },
69
- blockquote({node, ...props}) {
70
- return (
71
- <blockquote className="markdown-blockquote" {...props} />
72
- );
73
- }
74
- }}
75
- >
76
- {displayContent}
77
- </ReactMarkdown>
78
  </div>
79
  );
80
  };
 
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
- /* ChatWindow.css */
 
 
 
 
 
 
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: #21212f;
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: #21212f;
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: 9rem;
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/ChatComponents/RightSidebar';
 
 
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
- setSnackbar({ open: true, message, severity });
45
- };
 
 
 
 
 
 
 
 
 
 
46
 
47
  // Function to close the snackbar
48
  const closeSnackbar = (event, reason) => {
49
  if (reason === 'clickaway') return;
50
- setSnackbar(prev => ({ ...prev, open: false }));
51
  };
52
 
53
  useEffect(() => {
54
  localStorage.setItem("rightSidebarState", isRightSidebarOpen);
55
  }, [isRightSidebarOpen]);
56
 
 
 
 
 
 
 
 
 
 
57
  useEffect(() => {
58
  document.documentElement.style.setProperty('--right-sidebar-width', rightSidebarWidth + 'px');
59
  }, [rightSidebarWidth]);
@@ -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
- <button
430
- className="chat-settings-btn"
431
- onClick={() => setShowSettingsModal(true)}
432
- >
433
- <FaCog />
434
- </button>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
435
  {/* Conditionally render Stop or Send button */}
436
- <button
437
- className={`chat-send-btn ${isProcessing ? 'stop-btn' : ''}`}
438
- onClick={isProcessing ? handleStop : handleSendButtonClick}
439
- >
440
- {isProcessing ? <FaStop size={12} color="black" /> : <FaPaperPlane />}
441
- </button>
 
 
 
442
  </div>
443
  </div>
444
  </>
@@ -458,24 +1004,49 @@ function AiPage() {
458
  />
459
  </div>
460
  <div className="icon-container">
461
- <button
462
- className="settings-btn"
463
- onClick={() => setShowSettingsModal(true)}
464
- >
465
- <FaCog />
466
- </button>
467
- <button
468
- className={`send-btn ${isProcessing ? 'stop-btn' : ''}`}
469
- onClick={isProcessing ? handleStop : handleSendButtonClick}
470
- >
471
- {isProcessing ? <FaStop /> : <FaPaperPlane />}
472
- </button>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
473
  </div>
474
  </div>
475
  </div>
476
  )}
477
  </main>
478
-
479
  {showSettingsModal && (
480
  <IntialSetting
481
  trigger={true}
@@ -485,9 +1056,17 @@ function AiPage() {
485
  closeSnackbar={closeSnackbar}
486
  />
487
  )}
 
 
 
 
 
 
 
 
488
  <Snackbar
489
  open={snackbar.open}
490
- autoHideDuration={snackbar.severity === 'success' ? 3000 : null}
491
  onClose={closeSnackbar}
492
  anchorOrigin={{ vertical: 'top', horizontal: 'center' }}
493
  >
 
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-4 Turbo": "gpt-4-turbo",
26
- "GPT-4o": "gpt-4o",
27
- "GPT-4o Latest": "gpt-4o-2024-11-20",
28
- "GPT-4o Mini": "gpt-4o-mini",
29
- "ChatGPT": "chatgpt-4o-latest",
 
 
30
  },
31
  Anthropic: {
32
- "Claude 3.5 Sonnet": "claude-3-5-sonnet-20241022",
33
- "Claude 3.5 Haiku": "claude-3-5-haiku-20241022",
34
- "Claude 3 Opus": "claude-3-opus-20240229",
35
- "Claude 3 Sonnet": "claude-3-sonnet-20240229",
36
- "Claude 3 Haiku": "claude-3-haiku-20240307",
 
 
 
37
  },
38
  Google: {
39
- "Gemini 1.5 Pro": "gemini-1.5-pro",
40
- "Gemini 1.5 Flash": "gemini-1.5-flash",
41
- "Gemini 2.0 Flash Lite": "gemini-2.0-flash-lite-preview-02-05",
42
- "Gemini 2.0 Flash Experimental": "gemini-2.0-flash-exp",
43
  "Gemini 2.0 Flash": "gemini-2.0-flash",
44
- "Gemini 2.0 Pro Experimental": "gemini-2.0-pro-exp-02-05",
 
 
45
  },
46
  XAI: {
47
- "Grok-2": "grok-2-latest",
48
- "Grok Beta": "grok-beta",
 
 
 
49
  },
50
  };
51
 
 
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.pngZone.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
- from typing import Any, Dict
 
8
  from fastapi.staticfiles import StaticFiles
9
- from fastapi import FastAPI, Request, HTTPException
10
  from fastapi.responses import StreamingResponse, JSONResponse, FileResponse
11
  from fastapi.middleware.cors import CORSMiddleware
12
  from dotenv import load_dotenv
@@ -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(ENV_FILE_PATH, override=True)
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
- SESSION_STORE['search_engine'] = SearchEngine()
65
- SESSION_STORE['query_processor'] = QueryProcessor()
66
- SESSION_STORE['crawler'] = CustomCrawler(max_concurrent_requests=1000)
67
- # SESSION_STORE['graph_rag'] = Neo4jGraphRAG(num_workers=os.cpu_count() * 2)
68
- SESSION_STORE['graph_rag'] = GraphRAG(num_workers=os.cpu_count() * 2)
69
- SESSION_STORE['evaluator'] = Evaluator()
70
- SESSION_STORE['reasoner'] = Reasoner()
71
- SESSION_STORE['model'] = manager.get_llm()
72
- SESSION_STORE['late_chunker'] = LateChunker()
73
- SESSION_STORE["initialized"] = True
74
- SESSION_STORE["session_id"] = None
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
- async for chunk in state["reasoner"].reason(user_query):
92
- await sse_queue.put(("token", json.dumps({"chunk": chunk, "index": chunk_counter})))
93
- response += chunk
94
- chunk_counter += 1
 
 
 
 
 
 
 
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
- contents = ""
 
 
 
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"Document {k}:\n{content}\n\n"
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"].reason(user_query, contents):
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
- urls = [r.get('link', 'No URL') for r in filtered_urls]
 
 
 
 
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"Document {k}:\n{c}\n\n"
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
- contents = "\n\n".join(r for r in results if r.strip())
 
 
 
 
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'].reason(user_query, contents):
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"Document {k}:\n{c}\n\n"
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'].reason(user_query, contents):
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'].reason(user_query, answer):
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
- # 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, Optional #, Tuple
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
- # 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__(
 
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(intent=intent, context=context, query=query)
 
 
 
 
 
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(intent=intent, query=query)
 
 
 
 
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(context=context, query=query)
 
 
 
 
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(query=query)
 
 
 
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(query=query, context=combined_context)
 
 
 
 
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
- response = await llm.ainvoke(prompt.format_messages(query=query))
 
 
 
 
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
- "extensive research dynamic structure",
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.reason(query, answer):
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 #, Crawler
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= "cpu" # "cuda" if torch.cuda.is_available() else "cpu"
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
- "extensive research dynamic structure",
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.reason(query, answer):
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 reason(
13
  self,
14
  query,
15
  context=None,
 
16
  *,
17
  llm
18
  ):
19
  if context is None:
20
  template = \
21
- """You are an expert at reasoning.
22
- Your task is to reason about the given user query and provide an answer.
23
 
24
- Rules:
25
- 1. Your response should only be the answer in valid markdown format.
26
- 2. You must use proper reasoning and logic to answer the query for your internal use but do not show your reasoning process in the response.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 expert at reasoning.
35
- Given the user query and the relevant context, your task is to reason and provide an answer.
36
 
37
- Rules:
38
- 1. Your response should only be the answer in valid markdown format.
39
- 2. You must use proper reasoning and logic to answer the query for your internal use but do not show your reasoning process in the response.
40
- 3. You must not mention the context/documents provided to you in the response. Make it sound like you are the one who is answering the query.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
41
 
42
- Context:
43
- [{context}]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- - Simple External Lookup: Select upto 3 top best matched results
249
- - Complex Moderate Decomposition: Select upto 4 top best matched results
250
- - Complex Advanced Decomposition: Select upto 5 top best matched results
251
- - Extensive Research Dynamic Structuring: 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,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="Simple External Lookup",
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-beta",
82
- "grok-vision-beta",
83
- "grok-2-vision-1212",
84
- "grok-2-1212"
 
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
- # test_without_load_balancing(model_name="gemini-exp-1121", prompt=prompt, test_count=50)
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))