@retrivora-ai/rag-engine 2.3.0 → 2.3.2
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.
- package/dist/{LicenseValidator-CENvo9o2.d.mts → BatchProcessor-7yV-UCHW.d.mts} +142 -3
- package/dist/{LicenseValidator-CsjJp2PP.d.ts → BatchProcessor-BfzuU4cK.d.ts} +142 -3
- package/dist/handlers/index.d.mts +1 -1
- package/dist/handlers/index.d.ts +1 -1
- package/dist/handlers/index.js +881 -331
- package/dist/handlers/index.mjs +881 -331
- package/dist/index-DR_O_B-W.d.ts +394 -0
- package/dist/index-fnpaCuma.d.mts +394 -0
- package/dist/index.css +58 -0
- package/dist/index.d.mts +63 -3
- package/dist/index.d.ts +63 -3
- package/dist/index.js +454 -36
- package/dist/index.mjs +453 -38
- package/dist/server.d.mts +35 -73
- package/dist/server.d.ts +35 -73
- package/dist/server.js +911 -345
- package/dist/server.mjs +911 -345
- package/package.json +3 -2
- package/src/components/ChatWidget.tsx +149 -48
- package/src/components/ChatWindow.tsx +52 -9
- package/src/components/CodeViewer.tsx +1 -1
- package/src/core/BatchProcessor.ts +42 -4
- package/src/core/CircuitBreaker.ts +118 -0
- package/src/core/DatabaseStorage.ts +55 -24
- package/src/core/FreeTierLimitsGuard.ts +281 -0
- package/src/core/LangChainAgent.ts +0 -2
- package/src/core/LicenseValidator.ts +66 -2
- package/src/core/LicenseVerifier.ts +31 -3
- package/src/core/VectorPlugin.ts +30 -0
- package/src/handlers/index.ts +491 -36
- package/src/index.css +58 -0
- package/src/index.ts +4 -0
- package/src/providers/vectordb/MilvusProvider.ts +0 -1
- package/src/providers/vectordb/MongoDBProvider.ts +0 -1
- package/src/providers/vectordb/MultiTablePostgresProvider.ts +2 -2
- package/src/providers/vectordb/RedisProvider.ts +0 -1
- package/src/providers/vectordb/WeaviateProvider.ts +0 -1
- package/src/rag/LlamaIndexIngestor.ts +0 -1
- package/src/server.ts +1 -0
- package/dist/index-BPJ3KDYI.d.ts +0 -195
- package/dist/index-Dmq5lH0j.d.mts +0 -195
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@retrivora-ai/rag-engine",
|
|
3
|
-
"version": "2.3.
|
|
3
|
+
"version": "2.3.2",
|
|
4
4
|
"description": "Retrivora AI is a plug-and-play AI engine for RAG chat experiences — generic vector DB + LLM provider, embeddable or standalone.",
|
|
5
5
|
"author": "Abhinav Alkuchi",
|
|
6
6
|
"license": "UNLICENSED",
|
|
@@ -79,6 +79,7 @@
|
|
|
79
79
|
"build": "npm run build:pkg",
|
|
80
80
|
"build:pkg": "npx @tailwindcss/cli -i src/tailwind.css -o src/index.css && tsup src/index.ts src/handlers/index.ts src/server.ts --format cjs,esm --dts --clean --no-splitting --tsconfig tsconfig.build.json --external react,react-dom,next,mongodb,pg,openai,@anthropic-ai/sdk,@pinecone-database/pinecone,axios,lucide-react,mammoth,pdf-parse,xlsx,react-markdown,remark-gfm,next-themes,langchain,@langchain/core,@langchain/openai,llamaindex --inject-style && npx @tailwindcss/cli -i src/tailwind.css -o dist/index.css",
|
|
81
81
|
"lint": "eslint",
|
|
82
|
+
"typecheck": "tsc --noEmit -p tsconfig.build.json",
|
|
82
83
|
"clean": "rm -rf dist"
|
|
83
84
|
},
|
|
84
85
|
"peerDependencies": {
|
|
@@ -135,4 +136,4 @@
|
|
|
135
136
|
"tsup": "^8.5.1",
|
|
136
137
|
"typescript": "^5"
|
|
137
138
|
}
|
|
138
|
-
}
|
|
139
|
+
}
|
|
@@ -1,13 +1,21 @@
|
|
|
1
|
-
import React, { useState } from 'react';
|
|
2
|
-
import { MessageSquare, X, AlertTriangle } from 'lucide-react';
|
|
1
|
+
import React, { useState, useEffect, useCallback } from 'react';
|
|
2
|
+
import { MessageSquare, X, AlertTriangle, Lock } from 'lucide-react';
|
|
3
3
|
import { ChatWindow } from './ChatWindow';
|
|
4
4
|
import { useConfig } from './ConfigProvider';
|
|
5
5
|
import { ChatWidgetProps } from '../types';
|
|
6
6
|
import { SDK_VERSION } from '../version';
|
|
7
|
-
import { LicenseValidator } from '../core/LicenseValidator';
|
|
7
|
+
import { LicenseValidator, LicenseValidationResponse } from '../core/LicenseValidator';
|
|
8
8
|
|
|
9
|
+
/**
|
|
10
|
+
* Default widget dimensions (width x height) in pixels.
|
|
11
|
+
* Applied when the widget first opens and when the user resets sizing.
|
|
12
|
+
*/
|
|
9
13
|
const DEFAULT_DIMENSIONS = { width: 380, height: 600 };
|
|
10
14
|
|
|
15
|
+
/**
|
|
16
|
+
* Keyframes and CSS custom properties for the Gemini-style
|
|
17
|
+
* shape-shifting floating button background.
|
|
18
|
+
*/
|
|
11
19
|
const GEMINI_STYLES = `
|
|
12
20
|
@keyframes shapeshift {
|
|
13
21
|
0% { clip-path: var(--circle); rotate: 0turn; }
|
|
@@ -33,11 +41,6 @@ const GEMINI_STYLES = `
|
|
|
33
41
|
100% { translate: -75% -75%; }
|
|
34
42
|
}
|
|
35
43
|
|
|
36
|
-
@keyframes gradientMove {
|
|
37
|
-
0% { translate: 0 0; }
|
|
38
|
-
100% { translate: -75% -75%; }
|
|
39
|
-
}
|
|
40
|
-
|
|
41
44
|
:root {
|
|
42
45
|
--flower: shape(evenodd from 17.9665% 82.0335%, curve by -12.349% -32.0335% with -13.239% -5.129% / -18.021% -15.402%, curve by -0.0275% -22.203% with -3.1825% -9.331% / -3.074% -16.6605%, curve by 12.3765% -9.8305% with 2.3835% -4.3365% / 6.565% -7.579%, curve by 32.0335% -12.349% with 5.129% -13.239% / 15.402% -18.021%, curve by 20.4535% -0.8665% with 8.3805% -2.858% / 15.1465% -3.062%, curve by 11.58% 13.2155% with 5.225% 2.161% / 9.0355% 6.6475%, curve by 12.349% 32.0335% with 13.239% 5.129% / 18.021% 15.402%, curve by 0.5715% 21.1275% with 2.9805% 8.7395% / 3.0745% 15.723%, curve by -12.9205% 10.906% with -2.26% 4.88% / -6.638% 8.472%, curve by -32.0335% 12.349% with -5.129% 13.239% / -15.402% 18.021%, curve by -21.1215% 0.5745% with -8.736% 2.9795% / -15.718% 3.0745%, curve by -10.912% -12.9235% with -4.883% -2.2595% / -8.477% -6.6385%, close);
|
|
43
46
|
--hexagon: shape(evenodd from 6.47% 67.001%, curve by 0% -34.002% with -1.1735% -7.7% / -1.1735% -26.302%, curve by 7.0415% -12.1965% with 0.7075% -4.641% / 3.3765% -9.2635%, curve by 29.447% -17.001% with 6.0815% -4.8665% / 22.192% -14.1675%, curve by 14.083% 0% with 4.3725% -1.708% / 9.7105% -1.708%, curve by 29.447% 17.001% with 7.255% 2.8335% / 23.3655% 12.1345%, curve by 7.0415% 12.1965% with 3.665% 2.933% / 6.334% 7.5555%, curve by 0% 34.002% with 1.1735% 7.7% / 1.1735% 26.302%, curve by -7.0415% 12.1965% with -0.7075% 4.641% / -3.3765% 9.2635%, curve by -29.447% 17.001% with -6.0815% 4.8665% / -22.192% 14.1675%, curve by -14.083% 0% with -4.3725% 1.708% / -9.7105% 1.708%, curve by -29.447% -17.001% with -7.255% -2.8335% / -23.3655% -12.1345%, curve by -7.0415% -12.1965% with -3.665% -2.933% / -6.334% -7.5555%, close);
|
|
@@ -46,6 +49,24 @@ const GEMINI_STYLES = `
|
|
|
46
49
|
}
|
|
47
50
|
`;
|
|
48
51
|
|
|
52
|
+
/**
|
|
53
|
+
* ChatWidget — Floating Action Button (FAB) that toggles the chat window.
|
|
54
|
+
*
|
|
55
|
+
* Responsibilities:
|
|
56
|
+
* - Validates the Retrivora license key BEFORE allowing the chat to open
|
|
57
|
+
* - Proactively validates on mount to visually lock the widget when no
|
|
58
|
+
* valid license is present (TERMINATED / SUSPENDED / missing)
|
|
59
|
+
* - Manages window resize, maximize, and visual positioning
|
|
60
|
+
* - Shows an error toast when license validation fails on user attempt
|
|
61
|
+
*
|
|
62
|
+
* License key resolution order (highest to lowest priority):
|
|
63
|
+
* 1. Explicit `licenseKey` prop
|
|
64
|
+
* 2. `x-license-key` header (from `headers` prop)
|
|
65
|
+
* 3. `Authorization: Bearer <key>` header (from `headers` prop)
|
|
66
|
+
* 4. Environment variables `NEXT_PUBLIC_RETRIVORA_LICENSE_KEY` / `RETRIVORA_LICENSE_KEY`
|
|
67
|
+
*
|
|
68
|
+
* @param props - Widget position, callbacks, API overrides, and license overrides
|
|
69
|
+
*/
|
|
49
70
|
export function ChatWidget({ position = 'bottom-right', onAddToCart, apiUrl, retrivoraApiBase, licenseKey: licenseKeyProp, headers }: ChatWidgetProps) {
|
|
50
71
|
const { ui, projectId } = useConfig();
|
|
51
72
|
const [isOpen, setIsOpen] = useState(false);
|
|
@@ -56,8 +77,7 @@ export function ChatWidget({ position = 'bottom-right', onAddToCart, apiUrl, ret
|
|
|
56
77
|
const [prevDimensions, setPrevDimensions] = useState(DEFAULT_DIMENSIONS);
|
|
57
78
|
const [widgetError, setWidgetError] = useState<string | null>(null);
|
|
58
79
|
const [isValidating, setIsValidating] = useState(false);
|
|
59
|
-
|
|
60
|
-
if (ui.showWidget === false) return null;
|
|
80
|
+
const [hasMountValidationFailed, setHasMountValidationFailed] = useState(false);
|
|
61
81
|
|
|
62
82
|
const positionClass =
|
|
63
83
|
position === 'bottom-left'
|
|
@@ -69,26 +89,75 @@ export function ChatWidget({ position = 'bottom-right', onAddToCart, apiUrl, ret
|
|
|
69
89
|
? 'bottom-20 left-6'
|
|
70
90
|
: 'bottom-20 right-6';
|
|
71
91
|
|
|
72
|
-
|
|
92
|
+
/**
|
|
93
|
+
* Resolves the active license key from all possible sources
|
|
94
|
+
* (explicit prop → headers → environment variables).
|
|
95
|
+
*/
|
|
96
|
+
const resolveLicenseKey = useCallback((): string => {
|
|
97
|
+
const getHeader = (name: string) => {
|
|
98
|
+
if (!headers) return undefined;
|
|
99
|
+
const target = name.toLowerCase();
|
|
100
|
+
for (const [k, v] of Object.entries(headers)) {
|
|
101
|
+
if (k.toLowerCase() === target) return v;
|
|
102
|
+
}
|
|
103
|
+
return undefined;
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
return (
|
|
107
|
+
licenseKeyProp ||
|
|
108
|
+
getHeader('x-license-key') ||
|
|
109
|
+
getHeader('authorization')?.replace(/^Bearer\s+/i, '') ||
|
|
110
|
+
(typeof process !== 'undefined'
|
|
111
|
+
? (process.env.NEXT_PUBLIC_RETRIVORA_LICENSE_KEY || process.env.RETRIVORA_LICENSE_KEY)
|
|
112
|
+
: '') ||
|
|
113
|
+
''
|
|
114
|
+
);
|
|
115
|
+
}, [licenseKeyProp, headers]);
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Proactively validate the license on mount.
|
|
119
|
+
* If validation fails, mark the widget locked so the FAB button appears
|
|
120
|
+
* disabled and we don't waste a network call on every subsequent click.
|
|
121
|
+
*/
|
|
122
|
+
useEffect(() => {
|
|
123
|
+
let isMounted = true;
|
|
124
|
+
async function validateOnMount() {
|
|
125
|
+
try {
|
|
126
|
+
const resolvedLicenseKey = resolveLicenseKey();
|
|
127
|
+
await LicenseValidator.getInstance().validate({
|
|
128
|
+
licenseKey: resolvedLicenseKey,
|
|
129
|
+
projectId,
|
|
130
|
+
retrivoraApiBase,
|
|
131
|
+
headers,
|
|
132
|
+
});
|
|
133
|
+
if (isMounted) {
|
|
134
|
+
setHasMountValidationFailed(false);
|
|
135
|
+
setWidgetError(null);
|
|
136
|
+
}
|
|
137
|
+
} catch (err: any) {
|
|
138
|
+
if (isMounted) {
|
|
139
|
+
setHasMountValidationFailed(true);
|
|
140
|
+
setWidgetError(err?.message || 'Access denied.');
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
validateOnMount();
|
|
145
|
+
return () => { isMounted = false; };
|
|
146
|
+
}, [resolveLicenseKey, projectId, retrivoraApiBase, headers]);
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Validates the license key and opens the chat window.
|
|
150
|
+
* If the license was already marked invalid on mount we immediately
|
|
151
|
+
* re-display the error toast without re-validating.
|
|
152
|
+
*/
|
|
153
|
+
const handleOpen = useCallback(async () => {
|
|
154
|
+
if (hasMountValidationFailed && widgetError) {
|
|
155
|
+
setWidgetError(widgetError);
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
73
158
|
setIsValidating(true);
|
|
74
159
|
try {
|
|
75
|
-
const
|
|
76
|
-
if (!headers) return undefined;
|
|
77
|
-
const target = name.toLowerCase();
|
|
78
|
-
for (const [k, v] of Object.entries(headers)) {
|
|
79
|
-
if (k.toLowerCase() === target) return v;
|
|
80
|
-
}
|
|
81
|
-
return undefined;
|
|
82
|
-
};
|
|
83
|
-
|
|
84
|
-
const resolvedLicenseKey =
|
|
85
|
-
licenseKeyProp ||
|
|
86
|
-
getHeader('x-license-key') ||
|
|
87
|
-
getHeader('authorization')?.replace(/^Bearer\s+/i, '') ||
|
|
88
|
-
(typeof process !== 'undefined'
|
|
89
|
-
? (process.env.NEXT_PUBLIC_RETRIVORA_LICENSE_KEY || process.env.RETRIVORA_LICENSE_KEY)
|
|
90
|
-
: '') ||
|
|
91
|
-
'';
|
|
160
|
+
const resolvedLicenseKey = resolveLicenseKey();
|
|
92
161
|
|
|
93
162
|
await LicenseValidator.getInstance().validate({
|
|
94
163
|
licenseKey: resolvedLicenseKey,
|
|
@@ -98,15 +167,20 @@ export function ChatWidget({ position = 'bottom-right', onAddToCart, apiUrl, ret
|
|
|
98
167
|
});
|
|
99
168
|
|
|
100
169
|
setWidgetError(null);
|
|
170
|
+
setHasMountValidationFailed(false);
|
|
101
171
|
setIsOpen(true);
|
|
102
172
|
setHasUnread(false);
|
|
103
173
|
} catch (err: any) {
|
|
174
|
+
setHasMountValidationFailed(true);
|
|
104
175
|
setWidgetError(err?.message || 'Access denied.');
|
|
105
|
-
setIsOpen(false);
|
|
176
|
+
setIsOpen(false);
|
|
106
177
|
} finally {
|
|
107
178
|
setIsValidating(false);
|
|
108
179
|
}
|
|
109
|
-
};
|
|
180
|
+
}, [hasMountValidationFailed, widgetError, resolveLicenseKey, projectId, retrivoraApiBase, headers]);
|
|
181
|
+
|
|
182
|
+
/** True when the license has been proven invalid and the widget must be locked. */
|
|
183
|
+
const isWidgetLocked = hasMountValidationFailed || isValidating;
|
|
110
184
|
|
|
111
185
|
const handleResizeStart = (e: React.MouseEvent) => {
|
|
112
186
|
e.preventDefault();
|
|
@@ -157,6 +231,8 @@ export function ChatWidget({ position = 'bottom-right', onAddToCart, apiUrl, ret
|
|
|
157
231
|
|
|
158
232
|
const isResized = dimensions.width !== DEFAULT_DIMENSIONS.width || dimensions.height !== DEFAULT_DIMENSIONS.height;
|
|
159
233
|
|
|
234
|
+
if (ui.showWidget === false) return null;
|
|
235
|
+
|
|
160
236
|
return (
|
|
161
237
|
<>
|
|
162
238
|
<style dangerouslySetInnerHTML={{ __html: GEMINI_STYLES }} />
|
|
@@ -224,40 +300,65 @@ export function ChatWidget({ position = 'bottom-right', onAddToCart, apiUrl, ret
|
|
|
224
300
|
{/* ── Floating Action Button ── */}
|
|
225
301
|
<button
|
|
226
302
|
onClick={isOpen ? () => setIsOpen(false) : handleOpen}
|
|
227
|
-
|
|
228
|
-
aria-label=
|
|
303
|
+
disabled={isWidgetLocked && !isOpen}
|
|
304
|
+
aria-label={isWidgetLocked ? 'Chat unavailable - invalid or missing license key' : (isOpen ? 'Close chat' : 'Open chat')}
|
|
305
|
+
aria-disabled={isWidgetLocked && !isOpen}
|
|
306
|
+
className={`group fixed z-[9999] w-14 h-14 drop-shadow-2xl flex items-center justify-center transition-all duration-300 ${positionClass} ${
|
|
307
|
+
isWidgetLocked && !isOpen
|
|
308
|
+
? 'grayscale opacity-60 cursor-not-allowed'
|
|
309
|
+
: 'cursor-pointer hover:scale-110 active:scale-95'
|
|
310
|
+
}`}
|
|
229
311
|
>
|
|
230
|
-
{/* Animated Background */}
|
|
312
|
+
{/* Animated Background (disabled when locked) */}
|
|
231
313
|
<div
|
|
232
|
-
className=
|
|
314
|
+
className={`absolute inset-0 transition-all duration-300 overflow-hidden rounded-full ${
|
|
315
|
+
!(isWidgetLocked && !isOpen) && 'group-hover:rounded-none group-hover:animate-[shapeshift_5s_ease-in-out_infinite_forwards]'
|
|
316
|
+
}`}
|
|
317
|
+
style={{
|
|
318
|
+
background: isWidgetLocked && !isOpen
|
|
319
|
+
? 'linear-gradient(135deg, #64748b, #475569)'
|
|
320
|
+
: undefined,
|
|
321
|
+
}}
|
|
233
322
|
>
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
323
|
+
{!(isWidgetLocked && !isOpen) && (
|
|
324
|
+
<div
|
|
325
|
+
className="absolute top-0 left-0 w-[400%] h-[400%] transition-transform duration-300 group-hover:animate-[gradientMove_3.5s_ease-in-out_infinite_alternate]"
|
|
326
|
+
style={{
|
|
327
|
+
background: `linear-gradient(135deg, ${ui.primaryColor}, ${ui.accentColor}, ${ui.primaryColor}, ${ui.accentColor})`
|
|
328
|
+
}}
|
|
329
|
+
/>
|
|
330
|
+
)}
|
|
240
331
|
</div>
|
|
241
332
|
|
|
242
|
-
{/* Ripple ring */}
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
333
|
+
{/* Ripple ring (hidden when locked) */}
|
|
334
|
+
{!(isWidgetLocked && !isOpen) && (
|
|
335
|
+
<span
|
|
336
|
+
className="absolute inset-0 rounded-full animate-ping opacity-20 pointer-events-none"
|
|
337
|
+
style={{ background: ui.primaryColor }}
|
|
338
|
+
/>
|
|
339
|
+
)}
|
|
247
340
|
|
|
248
341
|
{/* Icon Layer */}
|
|
249
342
|
<span
|
|
250
|
-
className={`relative z-10 transition-transform duration-300 ${
|
|
343
|
+
className={`relative z-10 transition-transform duration-300 ${
|
|
344
|
+
isOpen
|
|
345
|
+
? 'rotate-90 scale-90'
|
|
346
|
+
: isWidgetLocked
|
|
347
|
+
? 'scale-90'
|
|
348
|
+
: 'rotate-0 scale-100'
|
|
349
|
+
}`}
|
|
251
350
|
>
|
|
252
351
|
{isOpen ? (
|
|
253
352
|
<X className="w-6 h-6 text-white" />
|
|
353
|
+
) : isWidgetLocked ? (
|
|
354
|
+
<Lock className="w-6 h-6 text-white" />
|
|
254
355
|
) : (
|
|
255
356
|
<MessageSquare className="w-6 h-6 text-white" />
|
|
256
357
|
)}
|
|
257
358
|
</span>
|
|
258
359
|
|
|
259
|
-
{/* Unread badge */}
|
|
260
|
-
{hasUnread && !isOpen && (
|
|
360
|
+
{/* Unread badge (hidden when locked or open) */}
|
|
361
|
+
{hasUnread && !isOpen && !isWidgetLocked && (
|
|
261
362
|
<span className="absolute top-0 right-0 w-4 h-4 bg-rose-500 rounded-full border-2 border-white flex items-center justify-center text-[9px] font-bold text-white">
|
|
262
363
|
1
|
|
263
364
|
</span>
|
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* ChatWindow — The primary chat interface container component.
|
|
5
|
+
*
|
|
6
|
+
* Renders a fully functional chat experience including:
|
|
7
|
+
* - Header with branding, online indicator, and window controls (clear/resize/maximize/close)
|
|
8
|
+
* - Scrollable message area with SSE-streamed assistant replies, sources, and welcome state
|
|
9
|
+
* - Input area with textarea, voice input, document upload, auto-suggestions, and send controls
|
|
10
|
+
* - Mandatory license validation on mount (shows a blocking error overlay if invalid / terminated)
|
|
11
|
+
*
|
|
12
|
+
* All chat-send-side inputs (textarea, voice, upload, suggestion chips, send button) are
|
|
13
|
+
* automatically disabled when the license validation fails or SDK version is unsupported.
|
|
14
|
+
*/
|
|
15
|
+
|
|
3
16
|
import React, { useState, useRef, useEffect, useCallback } from 'react';
|
|
4
17
|
import {
|
|
5
18
|
Bot,
|
|
@@ -25,6 +38,7 @@ import { ChatWindowProps } from '../types';
|
|
|
25
38
|
import { SDK_VERSION } from '../version';
|
|
26
39
|
import { LicenseValidator } from '../core/LicenseValidator';
|
|
27
40
|
|
|
41
|
+
/** Browser Web Speech API extended event shape for continuous transcript delivery. */
|
|
28
42
|
interface SpeechRecognitionEvent {
|
|
29
43
|
resultIndex: number;
|
|
30
44
|
results: {
|
|
@@ -36,10 +50,12 @@ interface SpeechRecognitionEvent {
|
|
|
36
50
|
};
|
|
37
51
|
}
|
|
38
52
|
|
|
53
|
+
/** Browser Web Speech API error event shape. */
|
|
39
54
|
interface SpeechRecognitionErrorEvent {
|
|
40
55
|
error: string;
|
|
41
56
|
}
|
|
42
57
|
|
|
58
|
+
/** Browser Web Speech API controller interface. */
|
|
43
59
|
interface ISpeechRecognition {
|
|
44
60
|
continuous: boolean;
|
|
45
61
|
interimResults: boolean;
|
|
@@ -50,11 +66,30 @@ interface ISpeechRecognition {
|
|
|
50
66
|
stop: () => void;
|
|
51
67
|
}
|
|
52
68
|
|
|
69
|
+
/** Augmented Window global that surfaces both prefixed and unprefixed SpeechRecognition constructors. */
|
|
53
70
|
interface WindowWithSpeech extends Window {
|
|
54
71
|
SpeechRecognition?: new () => ISpeechRecognition;
|
|
55
72
|
webkitSpeechRecognition?: new () => ISpeechRecognition;
|
|
56
73
|
}
|
|
57
74
|
|
|
75
|
+
/**
|
|
76
|
+
* ChatWindow component.
|
|
77
|
+
*
|
|
78
|
+
* @param className - Additional CSS class for the outer wrapper.
|
|
79
|
+
* @param style - Inline styles for the outer wrapper.
|
|
80
|
+
* @param onClose - Callback for the close (X) button (widget mode only).
|
|
81
|
+
* @param showClose - Whether to render the close button.
|
|
82
|
+
* @param onResizeStart - Callback fired when user begins dragging the resize handle.
|
|
83
|
+
* @param onResetResize - Callback fired when the reset-size button is clicked.
|
|
84
|
+
* @param isResized - True if the window has been resized away from defaults.
|
|
85
|
+
* @param onMaximize - Callback for the maximize/minimize toggle button.
|
|
86
|
+
* @param isMaximized - True if the window is currently maximized.
|
|
87
|
+
* @param onAddToCart - Callback fired when user clicks "Add to Cart" on a product card.
|
|
88
|
+
* @param apiUrl - Override URL for the chat endpoint (bypasses retrivoraApiBase).
|
|
89
|
+
* @param retrivoraApiBase - Base URL for Retrivora catch-all endpoints (chat/suggestions/history/etc).
|
|
90
|
+
* @param licenseKey - Override license key (highest priority; falls back to headers/env).
|
|
91
|
+
* @param headers - Custom headers forwarded with every network request.
|
|
92
|
+
*/
|
|
58
93
|
export function ChatWindow({
|
|
59
94
|
className = '',
|
|
60
95
|
style,
|
|
@@ -95,6 +130,12 @@ export function ChatWindow({
|
|
|
95
130
|
const [versionError, setVersionError] = useState<string | null>(null);
|
|
96
131
|
const recognitionRef = useRef<ISpeechRecognition | null>(null);
|
|
97
132
|
|
|
133
|
+
/**
|
|
134
|
+
* True when all chat-send interactions must be blocked
|
|
135
|
+
* (license validation failed or SDK version is no longer supported).
|
|
136
|
+
*/
|
|
137
|
+
const isDisabled = !!licenseError || !!versionError;
|
|
138
|
+
|
|
98
139
|
// Validate server license and SDK version on ChatWindow session initialization
|
|
99
140
|
useEffect(() => {
|
|
100
141
|
let isMounted = true;
|
|
@@ -199,7 +240,6 @@ export function ChatWindow({
|
|
|
199
240
|
};
|
|
200
241
|
|
|
201
242
|
useEffect(() => {
|
|
202
|
-
// eslint-disable-next-line react-hooks/set-state-in-effect
|
|
203
243
|
setMounted(true);
|
|
204
244
|
}, []);
|
|
205
245
|
|
|
@@ -361,7 +401,8 @@ export function ChatWindow({
|
|
|
361
401
|
<button
|
|
362
402
|
onClick={clearHistory}
|
|
363
403
|
title="Clear conversation"
|
|
364
|
-
|
|
404
|
+
disabled={isDisabled}
|
|
405
|
+
className="w-7 h-7 rounded-md flex items-center justify-center text-slate-400 hover:text-rose-500 dark:text-white/40 dark:hover:text-rose-400 hover:bg-white dark:hover:bg-white/10 transition-all cursor-pointer disabled:opacity-40 disabled:cursor-not-allowed disabled:hover:text-slate-400 dark:disabled:hover:text-white/40"
|
|
365
406
|
>
|
|
366
407
|
<Trash2 className="w-3.5 h-3.5" />
|
|
367
408
|
</button>
|
|
@@ -524,18 +565,20 @@ export function ChatWindow({
|
|
|
524
565
|
{/* ── Input ── */}
|
|
525
566
|
<div className={`px-4 pb-4 pt-2 border-t border-slate-200 dark:border-white/10 ${isGlass ? 'bg-transparent' : 'bg-white dark:bg-[#0f0f1a]'}`}>
|
|
526
567
|
|
|
527
|
-
{/* Dynamic Suggestions */}
|
|
568
|
+
{/* Dynamic Suggestions — hidden/blocked when license/version error */}
|
|
528
569
|
{(suggestions.length > 0 || isSuggesting) && !isLoading && (
|
|
529
570
|
<div className="mb-3 flex flex-wrap gap-2 animate-in fade-in slide-in-from-bottom-2 duration-300">
|
|
530
571
|
{suggestions.map((suggestion) => (
|
|
531
572
|
<button
|
|
532
573
|
key={suggestion}
|
|
533
574
|
onClick={() => {
|
|
575
|
+
if (isDisabled) return;
|
|
534
576
|
setInput(suggestion);
|
|
535
577
|
setSuggestions([]);
|
|
536
578
|
inputRef.current?.focus();
|
|
537
579
|
}}
|
|
538
|
-
|
|
580
|
+
disabled={isDisabled}
|
|
581
|
+
className="flex items-center gap-1.5 px-3 py-1.5 rounded-xl text-[11px] font-medium bg-white dark:bg-white/5 border border-slate-200 dark:border-white/10 text-slate-600 dark:text-white/60 hover:text-slate-900 dark:hover:text-white/90 hover:border-slate-300 dark:hover:border-white/20 transition-all shadow-sm cursor-pointer group disabled:opacity-40 disabled:cursor-not-allowed disabled:hover:text-slate-600 dark:disabled:hover:text-white/60"
|
|
539
582
|
>
|
|
540
583
|
<Sparkles className="w-3 h-3 text-amber-400" />
|
|
541
584
|
{suggestion}
|
|
@@ -564,7 +607,7 @@ export function ChatWindow({
|
|
|
564
607
|
onKeyDown={handleKeyDown}
|
|
565
608
|
placeholder={ui.placeholder}
|
|
566
609
|
rows={1}
|
|
567
|
-
disabled={isLoading ||
|
|
610
|
+
disabled={isLoading || isDisabled}
|
|
568
611
|
className="flex-1 bg-transparent text-slate-900 dark:text-white/90 placeholder-slate-400 dark:placeholder-white/30 text-sm resize-none outline-none py-1.5 px-2 max-h-32 leading-relaxed disabled:opacity-50"
|
|
569
612
|
style={{ scrollbarWidth: 'none' }}
|
|
570
613
|
/>
|
|
@@ -573,7 +616,7 @@ export function ChatWindow({
|
|
|
573
616
|
<button
|
|
574
617
|
type="button"
|
|
575
618
|
onClick={toggleListening}
|
|
576
|
-
disabled={isLoading ||
|
|
619
|
+
disabled={isLoading || isDisabled}
|
|
577
620
|
className={`flex-shrink-0 w-9 h-9 rounded-xl flex items-center justify-center transition-all hover:scale-105 active:scale-95 disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:scale-100 disabled:active:scale-100 ${isListening
|
|
578
621
|
? 'bg-rose-500 text-white animate-pulse shadow-md shadow-rose-500/20'
|
|
579
622
|
: 'text-slate-400 dark:text-white/40 hover:bg-slate-200 dark:hover:bg-white/10'
|
|
@@ -587,8 +630,8 @@ export function ChatWindow({
|
|
|
587
630
|
{ui.allowUpload !== false && (
|
|
588
631
|
<button
|
|
589
632
|
type="button"
|
|
590
|
-
onClick={() => setIsUploadModalOpen(true)}
|
|
591
|
-
disabled={isLoading ||
|
|
633
|
+
onClick={() => { if (!isDisabled) setIsUploadModalOpen(true); }}
|
|
634
|
+
disabled={isLoading || isDisabled}
|
|
592
635
|
className="flex-shrink-0 w-9 h-9 rounded-xl flex items-center justify-center transition-all hover:scale-105 active:scale-95 text-slate-400 dark:text-white/40 hover:bg-slate-200 dark:hover:bg-white/10 disabled:opacity-50 disabled:cursor-not-allowed"
|
|
593
636
|
title="Upload Document"
|
|
594
637
|
>
|
|
@@ -607,7 +650,7 @@ export function ChatWindow({
|
|
|
607
650
|
) : (
|
|
608
651
|
<button
|
|
609
652
|
onClick={sendMessage}
|
|
610
|
-
disabled={!input.trim() ||
|
|
653
|
+
disabled={!input.trim() || isDisabled}
|
|
611
654
|
className={`flex-shrink-0 w-9 h-9 flex items-center justify-center transition-all shadow-md active:scale-95 cursor-pointer disabled:opacity-30 disabled:cursor-not-allowed disabled:active:scale-100 ${ui.borderRadius === 'full' ? 'rounded-full' : 'rounded-xl'}`}
|
|
612
655
|
style={{ background: `linear-gradient(135deg, ${ui.primaryColor}, ${ui.accentColor})` }}
|
|
613
656
|
title="Send message"
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import React from 'react';
|
|
4
4
|
import { Check, Copy } from 'lucide-react';
|
|
5
|
-
import { Snippet } from '
|
|
5
|
+
import type { Snippet } from '../app/types';
|
|
6
6
|
|
|
7
7
|
export function CodeViewer({ snippet }: { snippet: Snippet }) {
|
|
8
8
|
const [copied, setCopied] = React.useState(false);
|
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
* configurable batch sizing for efficient bulk operations.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
import { CircuitBreaker } from './CircuitBreaker';
|
|
9
|
+
|
|
8
10
|
export interface BatchOptions {
|
|
9
11
|
/** Maximum number of items per batch */
|
|
10
12
|
batchSize?: number;
|
|
@@ -30,7 +32,7 @@ export interface BatchResult<T> {
|
|
|
30
32
|
/**
|
|
31
33
|
* Determines if an error is transient (should retry)
|
|
32
34
|
*/
|
|
33
|
-
function isTransientError(error: unknown): boolean {
|
|
35
|
+
export function isTransientError(error: unknown): boolean {
|
|
34
36
|
if (!(error instanceof Error)) return false;
|
|
35
37
|
|
|
36
38
|
const message = error.message.toLowerCase();
|
|
@@ -86,6 +88,42 @@ function sleep(ms: number): Promise<void> {
|
|
|
86
88
|
}
|
|
87
89
|
|
|
88
90
|
export class BatchProcessor {
|
|
91
|
+
private static readonly cb = new CircuitBreaker({
|
|
92
|
+
failureThreshold: 5,
|
|
93
|
+
resetTimeoutMs: 30_000,
|
|
94
|
+
halfOpenMaxCalls: 1,
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Execute a processor call through the circuit breaker.
|
|
99
|
+
* Only transient failures count toward opening the circuit.
|
|
100
|
+
*/
|
|
101
|
+
private static async executeWithCircuitBreaker<T>(processor: () => Promise<T>): Promise<T> {
|
|
102
|
+
if (!this.cb.canExecute()) {
|
|
103
|
+
const retryAfterSec = Math.ceil(
|
|
104
|
+
Math.max(0, 30_000 - (Date.now() - (this.cb as any).lastFailureAt || 0)) / 1000
|
|
105
|
+
);
|
|
106
|
+
throw new Error(
|
|
107
|
+
`[BatchProcessor] Circuit breaker is open. Try again in ${retryAfterSec}s.`
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if ((this.cb as any).state === 'half-open') {
|
|
112
|
+
(this.cb as any).halfOpenCalls++;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
try {
|
|
116
|
+
const result = await processor();
|
|
117
|
+
this.cb.record(true);
|
|
118
|
+
return result;
|
|
119
|
+
} catch (err) {
|
|
120
|
+
if (isTransientError(err)) {
|
|
121
|
+
this.cb.record(false);
|
|
122
|
+
}
|
|
123
|
+
throw err;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
89
127
|
/**
|
|
90
128
|
* Processes an array of items in configurable batches with retry logic.
|
|
91
129
|
*
|
|
@@ -137,7 +175,7 @@ export class BatchProcessor {
|
|
|
137
175
|
|
|
138
176
|
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
139
177
|
try {
|
|
140
|
-
const result = await processor(batch);
|
|
178
|
+
const result = await BatchProcessor.executeWithCircuitBreaker(() => processor(batch));
|
|
141
179
|
results.push(result);
|
|
142
180
|
success = true;
|
|
143
181
|
break;
|
|
@@ -212,7 +250,7 @@ export class BatchProcessor {
|
|
|
212
250
|
|
|
213
251
|
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
214
252
|
try {
|
|
215
|
-
const result = await processor(item);
|
|
253
|
+
const result = await BatchProcessor.executeWithCircuitBreaker(() => processor(item));
|
|
216
254
|
results.push(result);
|
|
217
255
|
success = true;
|
|
218
256
|
break;
|
|
@@ -311,7 +349,7 @@ export class BatchProcessor {
|
|
|
311
349
|
|
|
312
350
|
const chunkPromises = chunk.map(async ({ item, originalIndex }) => {
|
|
313
351
|
try {
|
|
314
|
-
const result = await processor(item);
|
|
352
|
+
const result = await BatchProcessor.executeWithCircuitBreaker(() => processor(item));
|
|
315
353
|
return { success: true, result, index: originalIndex };
|
|
316
354
|
} catch (err) {
|
|
317
355
|
const error = err instanceof Error ? err : new Error(String(err));
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
type CircuitState = 'closed' | 'open' | 'half-open';
|
|
2
|
+
|
|
3
|
+
export interface CircuitBreakerOptions {
|
|
4
|
+
failureThreshold?: number;
|
|
5
|
+
resetTimeoutMs?: number;
|
|
6
|
+
halfOpenMaxCalls?: number;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export class CircuitBreaker {
|
|
10
|
+
private state: CircuitState = 'closed';
|
|
11
|
+
private failureCount = 0;
|
|
12
|
+
private successCount = 0;
|
|
13
|
+
private lastFailureAt = 0;
|
|
14
|
+
private halfOpenCalls = 0;
|
|
15
|
+
|
|
16
|
+
private readonly failureThreshold: number;
|
|
17
|
+
private readonly resetTimeoutMs: number;
|
|
18
|
+
private readonly halfOpenMaxCalls: number;
|
|
19
|
+
|
|
20
|
+
constructor(options: CircuitBreakerOptions = {}) {
|
|
21
|
+
this.failureThreshold = options.failureThreshold ?? 5;
|
|
22
|
+
this.resetTimeoutMs = options.resetTimeoutMs ?? 30_000;
|
|
23
|
+
this.halfOpenMaxCalls = options.halfOpenMaxCalls ?? 1;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
record(success: boolean): void {
|
|
27
|
+
const now = Date.now();
|
|
28
|
+
|
|
29
|
+
if (success) {
|
|
30
|
+
this.successCount++;
|
|
31
|
+
if (this.state === 'half-open') {
|
|
32
|
+
this.state = 'closed';
|
|
33
|
+
this.failureCount = 0;
|
|
34
|
+
this.halfOpenCalls = 0;
|
|
35
|
+
this.successCount = 1;
|
|
36
|
+
} else if (this.state === 'closed') {
|
|
37
|
+
if (this.successCount >= this.failureThreshold) {
|
|
38
|
+
this.failureCount = Math.max(0, this.failureCount - 1);
|
|
39
|
+
this.successCount = 0;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
} else {
|
|
43
|
+
this.failureCount++;
|
|
44
|
+
this.lastFailureAt = now;
|
|
45
|
+
this.successCount = 0;
|
|
46
|
+
if (this.state === 'half-open') {
|
|
47
|
+
this.state = 'open';
|
|
48
|
+
this.halfOpenCalls = 0;
|
|
49
|
+
} else if (this.state === 'closed' && this.failureCount >= this.failureThreshold) {
|
|
50
|
+
this.state = 'open';
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
canExecute(): boolean {
|
|
56
|
+
const now = Date.now();
|
|
57
|
+
|
|
58
|
+
if (this.state === 'closed') {
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (this.state === 'open') {
|
|
63
|
+
if (now - this.lastFailureAt >= this.resetTimeoutMs) {
|
|
64
|
+
this.state = 'half-open';
|
|
65
|
+
this.halfOpenCalls = 0;
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (this.state === 'half-open') {
|
|
72
|
+
return this.halfOpenCalls < this.halfOpenMaxCalls;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async wrap<T>(fn: () => Promise<T>): Promise<T> {
|
|
79
|
+
if (!this.canExecute()) {
|
|
80
|
+
const retryAfterSec = Math.ceil(
|
|
81
|
+
Math.max(0, this.resetTimeoutMs - (Date.now() - this.lastFailureAt)) / 1000
|
|
82
|
+
);
|
|
83
|
+
throw new Error(
|
|
84
|
+
`Circuit breaker is open. Try again in ${retryAfterSec}s. ` +
|
|
85
|
+
`Failed ${this.failureCount}/${this.failureThreshold} consecutive calls.`
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (this.state === 'half-open') {
|
|
90
|
+
this.halfOpenCalls++;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
try {
|
|
94
|
+
const result = await fn();
|
|
95
|
+
this.record(true);
|
|
96
|
+
return result;
|
|
97
|
+
} catch (err) {
|
|
98
|
+
this.record(false);
|
|
99
|
+
throw err;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
getState(): CircuitState {
|
|
104
|
+
return this.state;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
getFailureCount(): number {
|
|
108
|
+
return this.failureCount;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
reset(): void {
|
|
112
|
+
this.state = 'closed';
|
|
113
|
+
this.failureCount = 0;
|
|
114
|
+
this.successCount = 0;
|
|
115
|
+
this.lastFailureAt = 0;
|
|
116
|
+
this.halfOpenCalls = 0;
|
|
117
|
+
}
|
|
118
|
+
}
|