@eeacms/volto-eea-chatbot 2.0.5 → 3.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/.github/workflows/betterleaks.yml +133 -0
  2. package/.gitleaks.toml +89 -0
  3. package/AGENTS.md +230 -0
  4. package/CHANGELOG.md +40 -0
  5. package/README.md +59 -0
  6. package/RELEASE.md +1 -1
  7. package/locales/de/LC_MESSAGES/volto.po +44 -0
  8. package/locales/en/LC_MESSAGES/volto.po +44 -0
  9. package/locales/it/LC_MESSAGES/volto.po +44 -0
  10. package/locales/ro/LC_MESSAGES/volto.po +44 -0
  11. package/locales/volto.pot +46 -2
  12. package/package.json +2 -1
  13. package/src/AISearchInput/AISearchInputEdit.jsx +37 -0
  14. package/src/AISearchInput/AISearchInputView.jsx +130 -0
  15. package/src/AISearchInput/components/DeepResearchToggle.jsx +35 -0
  16. package/src/AISearchInput/components/DisclaimerText.jsx +10 -0
  17. package/src/AISearchInput/components/IntroHeader.jsx +24 -0
  18. package/src/AISearchInput/components/PromptChips.jsx +22 -0
  19. package/src/AISearchInput/components/SearchInput.jsx +53 -0
  20. package/src/AISearchInput/index.js +32 -0
  21. package/src/AISearchInput/messages.js +38 -0
  22. package/src/AISearchInput/schema.jsx +215 -0
  23. package/src/AISearchInput/styles.less +320 -0
  24. package/src/ChatBlock/ChatBlockView.jsx +4 -0
  25. package/src/ChatBlock/chat/ChatWindow.tsx +23 -0
  26. package/src/ChatBlock/packets/MultiToolRenderer.tsx +20 -11
  27. package/src/ChatBlock/services/messageProcessor.ts +32 -14
  28. package/src/ChatBlock/style.less +41 -51
  29. package/src/halloumi/filtering.js +2 -2
  30. package/src/halloumi/generative.test.js +1 -1
  31. package/src/halloumi/middleware.js +2 -2
  32. package/src/halloumi/middleware.test.js +5 -5
  33. package/src/icons/sparkle.svg +3 -0
  34. package/src/index.js +2 -0
  35. package/src/middleware.js +2 -2
  36. package/src/middleware.test.js +11 -11
@@ -33,6 +33,7 @@ export class MessageProcessor {
33
33
  private _errorContent: string = '';
34
34
  private _documents: OnyxDocument[] = [];
35
35
  private _citations = new Map<number, string>();
36
+ private _citationsAreFallback: boolean = false;
36
37
  private _isComplete: boolean = false;
37
38
  private _isFinalMessageComing: boolean = false;
38
39
 
@@ -208,20 +209,28 @@ export class MessageProcessor {
208
209
 
209
210
  if (newDocuments) {
210
211
  this._documents = Array.from(this.documentMap.values());
212
+ }
211
213
 
212
- // If we have final_documents and no citations yet, create a fallback mapping
213
- // This ensures the Sources tab shows up in v3 when citation_info is missing
214
- if (
215
- packet.obj.type === PacketType.MESSAGE_START &&
216
- data.final_documents &&
217
- this._citations.size === 0
218
- ) {
219
- data.final_documents.forEach((doc: OnyxDocument, index: number) => {
220
- if (doc.document_id) {
221
- this._citations.set(index + 1, doc.document_id);
222
- }
223
- });
224
- }
214
+ // If we have final_documents and no citations yet, create a fallback mapping
215
+ // This ensures the Sources tab shows up in v3 when citation_info is missing
216
+ if (
217
+ packet.obj.type === PacketType.MESSAGE_START &&
218
+ data.final_documents &&
219
+ this._citations.size === 0
220
+ ) {
221
+ data.final_documents.forEach((doc: OnyxDocument, index: number) => {
222
+ if (doc.document_id) {
223
+ this._citations.set(index + 1, doc.document_id);
224
+ }
225
+ });
226
+ this._citationsAreFallback = this._citations.size > 0;
227
+ }
228
+ }
229
+
230
+ private clearFallbackCitations(): void {
231
+ if (this._citationsAreFallback) {
232
+ this._citations.clear();
233
+ this._citationsAreFallback = false;
225
234
  }
226
235
  }
227
236
 
@@ -233,6 +242,7 @@ export class MessageProcessor {
233
242
  if (packet.obj.type === PacketType.CITATION_INFO) {
234
243
  const citationInfo = packet.obj as any;
235
244
  if (citationInfo.citation_number && citationInfo.document_id) {
245
+ this.clearFallbackCitations();
236
246
  this._citations.set(
237
247
  citationInfo.citation_number,
238
248
  citationInfo.document_id,
@@ -244,7 +254,14 @@ export class MessageProcessor {
244
254
  return;
245
255
  }
246
256
  const citationDelta = packet.obj as CitationDelta;
247
- citationDelta.citations?.forEach((citation: StreamingCitation) => {
257
+ const explicitCitations = (citationDelta.citations || []).filter(
258
+ (citation: StreamingCitation) =>
259
+ citation.citation_num != null && Boolean(citation.document_id),
260
+ );
261
+ if (explicitCitations.length > 0) {
262
+ this.clearFallbackCitations();
263
+ }
264
+ explicitCitations.forEach((citation: StreamingCitation) => {
248
265
  if (!this._citations.has(citation.citation_num)) {
249
266
  this._citations.set(citation.citation_num, citation.document_id);
250
267
  }
@@ -397,6 +414,7 @@ export class MessageProcessor {
397
414
  this._errorContent = '';
398
415
  this._documents = [];
399
416
  this._citations.clear();
417
+ this._citationsAreFallback = false;
400
418
  this._isComplete = false;
401
419
  this._isFinalMessageComing = false;
402
420
  }
@@ -1604,14 +1604,6 @@ mark {
1604
1604
  }
1605
1605
  }
1606
1606
 
1607
- .tool-icon-default {
1608
- display: inline-block;
1609
- width: 0.5rem;
1610
- height: 0.5rem;
1611
- border-radius: 50%;
1612
- background-color: currentColor;
1613
- }
1614
-
1615
1607
  // Content column
1616
1608
  .tool-content {
1617
1609
  flex: 1;
@@ -1625,17 +1617,6 @@ mark {
1625
1617
  display: flex;
1626
1618
  }
1627
1619
 
1628
- .tool-status {
1629
- display: flex;
1630
- align-items: center;
1631
- border-radius: 12px;
1632
- margin-bottom: 0.25rem;
1633
- color: @teal;
1634
- font-size: 0.875rem;
1635
- font-weight: 500;
1636
- gap: 0.25rem;
1637
- }
1638
-
1639
1620
  .tool-text {
1640
1621
  color: #6b7280;
1641
1622
  font-size: 0.75rem;
@@ -1651,6 +1632,47 @@ mark {
1651
1632
  .multi-tool-renderer {
1652
1633
  margin-bottom: 1em;
1653
1634
 
1635
+ // Shared tool state styles
1636
+ .tool-icon-dot {
1637
+ display: inline-block;
1638
+ width: 0.5rem;
1639
+ height: 0.5rem;
1640
+ border-radius: 50%;
1641
+ background-color: currentColor;
1642
+ }
1643
+
1644
+ .tool-status {
1645
+ display: flex;
1646
+ align-items: center;
1647
+ border-radius: 12px;
1648
+ margin-bottom: 0.25rem;
1649
+ color: @teal;
1650
+ font-size: 0.875rem;
1651
+ font-weight: 500;
1652
+ gap: 0.25rem;
1653
+ }
1654
+
1655
+ .tool-item-expanded.active .tool-icon-circle,
1656
+ .tool-item-collapsed.active .tool-collapsed-icon {
1657
+ color: @teal;
1658
+ }
1659
+
1660
+ .active .tool-status {
1661
+ animation: shimmer 2.5s linear infinite;
1662
+ background: linear-gradient(
1663
+ 90deg,
1664
+ @teal 0%,
1665
+ @teal 40%,
1666
+ rgba(0, 179, 174, 0.5) 50%,
1667
+ @teal 60%,
1668
+ @teal 100%
1669
+ );
1670
+ -webkit-background-clip: text;
1671
+ background-clip: text;
1672
+ background-size: 200% 100%;
1673
+ -webkit-text-fill-color: transparent;
1674
+ }
1675
+
1654
1676
  // Common header styles for both collapsed and summary headers
1655
1677
  .tools-collapsed-header,
1656
1678
  .tools-summary-header {
@@ -1766,29 +1788,6 @@ mark {
1766
1788
  }
1767
1789
  }
1768
1790
 
1769
- &.active {
1770
- .tool-collapsed-icon {
1771
- position: relative;
1772
- color: @teal;
1773
- }
1774
-
1775
- .tool-collapsed-status {
1776
- animation: shimmer 2.5s linear infinite;
1777
- background: linear-gradient(
1778
- 90deg,
1779
- @teal 0%,
1780
- @teal 40%,
1781
- rgba(0, 179, 174, 0.5) 50%,
1782
- @teal 60%,
1783
- @teal 100%
1784
- );
1785
- -webkit-background-clip: text;
1786
- background-clip: text;
1787
- background-size: 200% 100%;
1788
- -webkit-text-fill-color: transparent;
1789
- }
1790
- }
1791
-
1792
1791
  .tool-collapsed-icon {
1793
1792
  display: flex;
1794
1793
  width: 1rem;
@@ -1798,17 +1797,8 @@ mark {
1798
1797
  justify-content: center;
1799
1798
  }
1800
1799
 
1801
- .tool-icon-dot {
1802
- display: inline-block;
1803
- width: 6px;
1804
- height: 6px;
1805
- border-radius: 50%;
1806
- background-color: currentColor;
1807
- }
1808
-
1809
1800
  .tool-collapsed-status {
1810
1801
  flex: 1;
1811
- font-weight: 500;
1812
1802
  }
1813
1803
  }
1814
1804
 
@@ -4,13 +4,13 @@ import fetch from 'node-fetch';
4
4
  const log = debug('halloumi');
5
5
 
6
6
  const LLMGW_URL = process.env.LLMGW_URL;
7
- const LLMGW_API_KEY = process.env.LLMGW_TOKEN;
7
+ const LLMGW_API_KEY = process.env.LLMGW_TOKEN; //betterleaks:allow
8
8
  const MIN_CONTEXT_SENTENCES_FOR_FILTERING = 75;
9
9
 
10
10
  const filterModel = {
11
11
  name: 'Inhouse-LLM/gpt-oss-120b',
12
12
  apiUrl: `${LLMGW_URL}/chat/completions`,
13
- apiKey: LLMGW_API_KEY,
13
+ apiKey: LLMGW_API_KEY, //betterleaks:allow
14
14
  };
15
15
 
16
16
  export async function callLLM(apiUrl, apiKey, requestBody, { ip } = {}) {
@@ -196,7 +196,7 @@ describe('halloumiGenerativeAPI via real fetch', () => {
196
196
  const model = {
197
197
  name: 'test-model',
198
198
  apiUrl: 'http://test.com/api',
199
- apiKey: 'test-key',
199
+ apiKey: 'test-key', //betterleaks:allow
200
200
  };
201
201
  const prompt = {
202
202
  prompt: 'test prompt',
@@ -15,7 +15,7 @@ const ALLOWED_HALLOUMI_PATHS = [
15
15
  ];
16
16
 
17
17
  const LLMGW_URL = process.env.LLMGW_URL;
18
- const LLMGW_TOKEN = process.env.LLMGW_TOKEN;
18
+ const LLMGW_TOKEN = process.env.LLMGW_TOKEN; //betterleaks:allow
19
19
 
20
20
  const generativeModel = {
21
21
  name: 'Inhouse-LLM/HallOumi-8B',
@@ -57,7 +57,7 @@ export default async function middleware(req, res, next) {
57
57
 
58
58
  const model = {
59
59
  ...(path === '/classify' ? classifyModel : generativeModel),
60
- apiKey: LLMGW_TOKEN,
60
+ apiKey: LLMGW_TOKEN, //betterleaks:allow
61
61
  };
62
62
  const body = req.body;
63
63
 
@@ -42,7 +42,7 @@ describe('halloumi middleware', () => {
42
42
  error: 'Invalid configuration: missing LLMGW_TOKEN or LLMGW_URL',
43
43
  });
44
44
 
45
- process.env.LLMGW_TOKEN = origToken;
45
+ process.env.LLMGW_TOKEN = origToken; //betterleaks:allow
46
46
  process.env.LLMGW_URL = origUrl;
47
47
  });
48
48
 
@@ -92,7 +92,7 @@ describe('halloumi middleware - dynamic import', () => {
92
92
  it('sends response on successful getVerifyClaimResponse', async () => {
93
93
  const origToken = process.env.LLMGW_TOKEN;
94
94
  const origUrl = process.env.LLMGW_URL;
95
- process.env.LLMGW_TOKEN = 'test-token';
95
+ process.env.LLMGW_TOKEN = 'test-token'; //betterleaks:allow
96
96
  process.env.LLMGW_URL = 'http://test-url';
97
97
 
98
98
  jest.resetModules();
@@ -116,14 +116,14 @@ describe('halloumi middleware - dynamic import', () => {
116
116
  expect(res.set).toHaveBeenCalledWith('Content-Type', 'application/json');
117
117
  expect(res.send).toHaveBeenCalledWith({ claims: [], segments: {} });
118
118
 
119
- process.env.LLMGW_TOKEN = origToken;
119
+ process.env.LLMGW_TOKEN = origToken; //betterleaks:allow
120
120
  process.env.LLMGW_URL = origUrl;
121
121
  });
122
122
 
123
123
  it('handles errors from getVerifyClaimResponse', async () => {
124
124
  const origToken = process.env.LLMGW_TOKEN;
125
125
  const origUrl = process.env.LLMGW_URL;
126
- process.env.LLMGW_TOKEN = 'test-token';
126
+ process.env.LLMGW_TOKEN = 'test-token'; //betterleaks:allow
127
127
  process.env.LLMGW_URL = 'http://test-url';
128
128
 
129
129
  jest.resetModules();
@@ -148,7 +148,7 @@ describe('halloumi middleware - dynamic import', () => {
148
148
  expect.objectContaining({ error: expect.stringContaining('LLM error') }),
149
149
  );
150
150
 
151
- process.env.LLMGW_TOKEN = origToken;
151
+ process.env.LLMGW_TOKEN = origToken; //betterleaks:allow
152
152
  process.env.LLMGW_URL = origUrl;
153
153
  });
154
154
  });
@@ -0,0 +1,3 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
2
+ <path d="m12 3-1.912 5.813a2 2 0 0 1-1.275 1.275L3 12l5.813 1.912a2 2 0 0 1 1.275 1.275L12 21l1.912-5.813a2 2 0 0 1 1.275-1.275L21 12l-5.813-1.912a2 2 0 0 1-1.275-1.275L12 3Z"/>
3
+ </svg>
package/src/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import installChatBlock from './ChatBlock';
2
+ import installAISearchInputBlock from './AISearchInput';
2
3
  import loadable from '@loadable/component';
3
4
 
4
5
  const applyConfig = (config) => {
@@ -40,6 +41,7 @@ const applyConfig = (config) => {
40
41
  };
41
42
 
42
43
  installChatBlock(config);
44
+ installAISearchInputBlock(config);
43
45
 
44
46
  return config;
45
47
  };
package/src/middleware.js CHANGED
@@ -289,7 +289,7 @@ export default async function middleware(req, res, next) {
289
289
 
290
290
  const reqUrl = `${process.env.ONYX_URL || ''}/api${path}`;
291
291
 
292
- const api_key = process.env.ONYX_API_KEY;
292
+ const api_key = process.env.ONYX_API_KEY; //betterleaks:allow
293
293
  if (!api_key) {
294
294
  res.statusCode = 500;
295
295
  res.statusMessage = MSG_INVALID_CONFIGURATION;
@@ -300,7 +300,7 @@ export default async function middleware(req, res, next) {
300
300
  try {
301
301
  await send_onyx_request(req, res, {
302
302
  url: reqUrl,
303
- api_key: api_key,
303
+ api_key: api_key, //betterleaks:allow
304
304
  is_related_question,
305
305
  });
306
306
  } catch (error) {
@@ -89,7 +89,7 @@ describe('src/middleware', () => {
89
89
  });
90
90
 
91
91
  it('proxies POST request with api_key and pipes response', async () => {
92
- process.env.ONYX_API_KEY = 'test-key';
92
+ process.env.ONYX_API_KEY = 'test-key'; //betterleaks:allow
93
93
  process.env.ONYX_URL = 'http://localhost:3000';
94
94
 
95
95
  await middleware(req, res, next);
@@ -110,7 +110,7 @@ describe('src/middleware', () => {
110
110
  });
111
111
 
112
112
  it('handles related question paths', async () => {
113
- process.env.ONYX_API_KEY = 'test-key';
113
+ process.env.ONYX_API_KEY = 'test-key'; //betterleaks:allow
114
114
  process.env.ONYX_URL = 'http://localhost:3000';
115
115
  req.url = '/_rq/chat/send-message';
116
116
 
@@ -124,7 +124,7 @@ describe('src/middleware', () => {
124
124
  });
125
125
 
126
126
  it('sends error response when fetch throws', async () => {
127
- process.env.ONYX_API_KEY = 'test-key';
127
+ process.env.ONYX_API_KEY = 'test-key'; //betterleaks:allow
128
128
  process.env.ONYX_URL = 'http://localhost:3000';
129
129
 
130
130
  nodeFetch.mockRejectedValueOnce(
@@ -145,7 +145,7 @@ describe('src/middleware', () => {
145
145
  });
146
146
 
147
147
  it('handles error without response text', async () => {
148
- process.env.ONYX_API_KEY = 'test-key';
148
+ process.env.ONYX_API_KEY = 'test-key'; //betterleaks:allow
149
149
  process.env.ONYX_URL = 'http://localhost:3000';
150
150
 
151
151
  nodeFetch.mockRejectedValueOnce(new Error('No response'));
@@ -162,7 +162,7 @@ describe('src/middleware', () => {
162
162
  });
163
163
 
164
164
  it('handles GET requests without body', async () => {
165
- process.env.ONYX_API_KEY = 'test-key';
165
+ process.env.ONYX_API_KEY = 'test-key'; //betterleaks:allow
166
166
  process.env.ONYX_URL = 'http://localhost:3000';
167
167
  req.method = 'GET';
168
168
  req.url = '/_da/persona/1';
@@ -175,7 +175,7 @@ describe('src/middleware', () => {
175
175
  });
176
176
 
177
177
  it('uses mock create-chat-session when MOCK_LLM_FILE_PATH is set', async () => {
178
- process.env.ONYX_API_KEY = 'test-key';
178
+ process.env.ONYX_API_KEY = 'test-key'; //betterleaks:allow
179
179
  process.env.ONYX_URL = 'http://localhost:3000';
180
180
  process.env.MOCK_LLM_FILE_PATH = '/tmp/mock.jsonl';
181
181
  req.url = '/_da/chat/create-chat-session';
@@ -189,7 +189,7 @@ describe('src/middleware', () => {
189
189
  });
190
190
 
191
191
  it('uses mock send-message with stream when MOCK_LLM_FILE_PATH is set', async () => {
192
- process.env.ONYX_API_KEY = 'test-key';
192
+ process.env.ONYX_API_KEY = 'test-key'; //betterleaks:allow
193
193
  process.env.ONYX_URL = 'http://localhost:3000';
194
194
  process.env.MOCK_LLM_FILE_PATH = '/tmp/mock.jsonl';
195
195
  req.url = '/_da/chat/send-message';
@@ -223,7 +223,7 @@ describe('src/middleware', () => {
223
223
  });
224
224
 
225
225
  it('dumps LLM response when DUMP_LLM_FILE_PATH is set', async () => {
226
- process.env.ONYX_API_KEY = 'test-key';
226
+ process.env.ONYX_API_KEY = 'test-key'; //betterleaks:allow
227
227
  process.env.ONYX_URL = 'http://localhost:3000';
228
228
  process.env.DUMP_LLM_FILE_PATH = '/tmp/dumped_response.jsonl';
229
229
 
@@ -236,7 +236,7 @@ describe('src/middleware', () => {
236
236
  });
237
237
 
238
238
  it('rejects disallowed paths with 404', async () => {
239
- process.env.ONYX_API_KEY = 'test-key';
239
+ process.env.ONYX_API_KEY = 'test-key'; //betterleaks:allow
240
240
  process.env.ONYX_URL = 'http://localhost:3000';
241
241
  req.url = '/_da/admin/users/delete';
242
242
  req.method = 'POST';
@@ -249,7 +249,7 @@ describe('src/middleware', () => {
249
249
  });
250
250
 
251
251
  it('rejects allowed path with wrong HTTP method', async () => {
252
- process.env.ONYX_API_KEY = 'test-key';
252
+ process.env.ONYX_API_KEY = 'test-key'; //betterleaks:allow
253
253
  process.env.ONYX_URL = 'http://localhost:3000';
254
254
  req.url = '/_da/persona';
255
255
  req.method = 'DELETE';
@@ -262,7 +262,7 @@ describe('src/middleware', () => {
262
262
  });
263
263
 
264
264
  it('rejects path traversal attempts', async () => {
265
- process.env.ONYX_API_KEY = 'test-key';
265
+ process.env.ONYX_API_KEY = 'test-key'; //betterleaks:allow
266
266
  process.env.ONYX_URL = 'http://localhost:3000';
267
267
  req.url = '/_da/../../etc/passwd';
268
268
  req.method = 'GET';