@retrivora-ai/rag-engine 1.0.6 → 1.0.8

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/index.mjs CHANGED
@@ -18,7 +18,10 @@ import {
18
18
  X,
19
19
  Sparkles,
20
20
  ArrowUp,
21
- TriangleAlert
21
+ TriangleAlert,
22
+ RotateCcw,
23
+ Maximize2,
24
+ Minimize2
22
25
  } from "lucide-react";
23
26
 
24
27
  // src/components/MessageBubble.tsx
@@ -105,7 +108,8 @@ var defaultConfig = {
105
108
  poweredBy: "RAG",
106
109
  visualStyle: "glass",
107
110
  borderRadius: "xl",
108
- allowUpload: true
111
+ allowUpload: true,
112
+ allowResize: true
109
113
  }
110
114
  };
111
115
  var ConfigContext = createContext(defaultConfig);
@@ -329,7 +333,17 @@ var CHAT_SUGGESTIONS = [
329
333
  ];
330
334
 
331
335
  // src/components/ChatWindow.tsx
332
- function ChatWindow({ className = "", style, onClose, showClose = false }) {
336
+ function ChatWindow({
337
+ className = "",
338
+ style,
339
+ onClose,
340
+ showClose = false,
341
+ onResizeStart,
342
+ onResetResize,
343
+ isResized = false,
344
+ onMaximize,
345
+ isMaximized = false
346
+ }) {
333
347
  var _a;
334
348
  const { ui, projectId } = useConfig();
335
349
  const [input, setInput] = useState3("");
@@ -373,9 +387,18 @@ function ChatWindow({ className = "", style, onClose, showClose = false }) {
373
387
  return /* @__PURE__ */ React5.createElement(
374
388
  "div",
375
389
  {
376
- className: `flex flex-col border border-slate-200 dark:border-white/10 shadow-2xl transition-all duration-300 ${currentRadius} ${isGlass ? "bg-white/90 dark:bg-[#0f0f1a]/90 backdrop-blur-xl" : "bg-white dark:bg-[#0f0f1a]"} ${className}`,
390
+ className: `relative flex flex-col border border-slate-200 dark:border-white/10 shadow-2xl transition-all duration-300 ${currentRadius} ${isGlass ? "bg-white/90 dark:bg-[#0f0f1a]/90 backdrop-blur-xl" : "bg-white dark:bg-[#0f0f1a]"} ${className}`,
377
391
  style: __spreadValues({ "--primary": ui.primaryColor, "--accent": ui.accentColor }, style)
378
392
  },
393
+ onResizeStart && /* @__PURE__ */ React5.createElement(
394
+ "div",
395
+ {
396
+ onMouseDown: onResizeStart,
397
+ className: "absolute top-0 left-0 w-6 h-6 cursor-nw-resize z-[100] group flex items-start justify-start p-1",
398
+ title: "Drag to resize"
399
+ },
400
+ /* @__PURE__ */ React5.createElement("div", { className: "w-2.5 h-2.5 border-t-2 border-l-2 border-slate-300 dark:border-white/20 group-hover:border-slate-500 dark:group-hover:border-white/50 transition-colors rounded-tl-[2px]" })
401
+ ),
379
402
  /* @__PURE__ */ React5.createElement(
380
403
  "div",
381
404
  {
@@ -393,23 +416,39 @@ function ChatWindow({ className = "", style, onClose, showClose = false }) {
393
416
  },
394
417
  /* @__PURE__ */ React5.createElement(Bot2, { className: "w-5 h-5 text-white" })
395
418
  ), /* @__PURE__ */ React5.createElement("div", null, /* @__PURE__ */ React5.createElement("h2", { className: "text-slate-900 dark:text-white font-semibold text-sm leading-tight" }, ui.title), ui.poweredBy && /* @__PURE__ */ React5.createElement("p", { className: "text-slate-500 dark:text-white/40 text-[11px] leading-tight" }, `Powered by ${ui.poweredBy}`))),
396
- /* @__PURE__ */ React5.createElement("div", { className: "flex items-center gap-2" }, /* @__PURE__ */ React5.createElement("span", { className: "flex items-center gap-1 text-[10px] text-emerald-500 dark:text-emerald-400" }, /* @__PURE__ */ React5.createElement("span", { className: "w-1.5 h-1.5 rounded-full bg-emerald-500 dark:bg-emerald-400 animate-pulse" }), "Online"), mounted && messages.length > 0 && /* @__PURE__ */ React5.createElement(
419
+ /* @__PURE__ */ React5.createElement("div", { className: "flex items-center gap-1.5" }, /* @__PURE__ */ React5.createElement("span", { className: "flex items-center gap-1 text-[10px] text-emerald-500 dark:text-emerald-400 mr-2" }, /* @__PURE__ */ React5.createElement("span", { className: "w-1.5 h-1.5 rounded-full bg-emerald-500 dark:bg-emerald-400 animate-pulse" }), "Online"), /* @__PURE__ */ React5.createElement("div", { className: "flex items-center bg-slate-100/50 dark:bg-white/5 rounded-lg p-0.5 border border-slate-200/50 dark:border-white/5" }, mounted && messages.length > 0 && /* @__PURE__ */ React5.createElement(
397
420
  "button",
398
421
  {
399
422
  onClick: clearHistory,
400
423
  title: "Clear conversation",
401
- className: "w-7 h-7 rounded-lg flex items-center justify-center text-slate-400 hover:text-slate-600 dark:text-white/40 dark:hover:text-white/70 hover:bg-slate-100 dark:hover:bg-white/10 transition-all cursor-pointer"
424
+ 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"
402
425
  },
403
426
  /* @__PURE__ */ React5.createElement(Trash2, { className: "w-3.5 h-3.5" })
427
+ ), isResized && onResetResize && /* @__PURE__ */ React5.createElement(
428
+ "button",
429
+ {
430
+ onClick: onResetResize,
431
+ title: "Reset to default size",
432
+ className: "w-7 h-7 rounded-md flex items-center justify-center text-slate-400 hover:text-blue-500 dark:text-white/40 dark:hover:text-blue-400 hover:bg-white dark:hover:bg-white/10 transition-all cursor-pointer"
433
+ },
434
+ /* @__PURE__ */ React5.createElement(RotateCcw, { className: "w-3.5 h-3.5" })
435
+ ), onMaximize && /* @__PURE__ */ React5.createElement(
436
+ "button",
437
+ {
438
+ onClick: onMaximize,
439
+ title: isMaximized ? "Minimize" : "Maximize",
440
+ className: "w-7 h-7 rounded-md flex items-center justify-center text-slate-400 hover:text-blue-500 dark:text-white/40 dark:hover:text-blue-400 hover:bg-white dark:hover:bg-white/10 transition-all cursor-pointer"
441
+ },
442
+ isMaximized ? /* @__PURE__ */ React5.createElement(Minimize2, { className: "w-3.5 h-3.5" }) : /* @__PURE__ */ React5.createElement(Maximize2, { className: "w-3.5 h-3.5" })
404
443
  ), showClose && onClose && /* @__PURE__ */ React5.createElement(
405
444
  "button",
406
445
  {
407
446
  onClick: onClose,
408
447
  title: "Close chat",
409
- className: "w-7 h-7 rounded-lg flex items-center justify-center text-slate-400 hover:text-slate-600 dark:text-white/40 dark:hover:text-white/70 hover:bg-slate-100 dark:hover:bg-white/10 transition-all cursor-pointer"
448
+ className: "w-7 h-7 rounded-md flex items-center justify-center text-slate-400 hover:text-slate-600 dark:text-white/40 dark:hover:text-white/70 hover:bg-white dark:hover:bg-white/10 transition-all cursor-pointer"
410
449
  },
411
450
  /* @__PURE__ */ React5.createElement(X, { className: "w-4 h-4" })
412
- ))
451
+ )))
413
452
  ),
414
453
  /* @__PURE__ */ React5.createElement("div", { className: "flex-1 overflow-y-auto px-4 py-4 space-y-5 scrollbar-thin scrollbar-thumb-slate-200 dark:scrollbar-thumb-white/10 scrollbar-track-transparent min-h-0 bg-slate-50 dark:bg-transparent" }, !mounted || isEmpty ? (
415
454
  /* Welcome state */
@@ -484,10 +523,15 @@ function ChatWindow({ className = "", style, onClose, showClose = false }) {
484
523
  }
485
524
 
486
525
  // src/components/ChatWidget.tsx
526
+ var DEFAULT_DIMENSIONS = { width: 380, height: 600 };
487
527
  function ChatWidget({ position = "bottom-right" }) {
488
528
  const { ui } = useConfig();
489
529
  const [isOpen, setIsOpen] = useState4(false);
490
530
  const [hasUnread, setHasUnread] = useState4(false);
531
+ const [dimensions, setDimensions] = useState4(DEFAULT_DIMENSIONS);
532
+ const [isResizing, setIsResizing] = useState4(false);
533
+ const [isMaximized, setIsMaximized] = useState4(false);
534
+ const [prevDimensions, setPrevDimensions] = useState4(DEFAULT_DIMENSIONS);
491
535
  if (ui.showWidget === false) return null;
492
536
  const positionClass = position === "bottom-left" ? "bottom-6 left-6" : "bottom-6 right-6";
493
537
  const windowPositionClass = position === "bottom-left" ? "bottom-20 left-6" : "bottom-20 right-6";
@@ -496,18 +540,68 @@ function ChatWidget({ position = "bottom-right" }) {
496
540
  setIsOpen(true);
497
541
  setHasUnread(false);
498
542
  };
543
+ const handleResizeStart = (e) => {
544
+ e.preventDefault();
545
+ setIsResizing(true);
546
+ const startX = e.clientX;
547
+ const startY = e.clientY;
548
+ const startWidth = dimensions.width;
549
+ const startHeight = dimensions.height;
550
+ const onMouseMove = (moveEvent) => {
551
+ const deltaX = startX - moveEvent.clientX;
552
+ const deltaY = startY - moveEvent.clientY;
553
+ setDimensions({
554
+ width: Math.max(320, Math.min(startWidth + deltaX, window.innerWidth - 48)),
555
+ height: Math.max(400, Math.min(startHeight + deltaY, window.innerHeight - 100))
556
+ });
557
+ };
558
+ const onMouseUp = () => {
559
+ setIsResizing(false);
560
+ window.removeEventListener("mousemove", onMouseMove);
561
+ window.removeEventListener("mouseup", onMouseUp);
562
+ };
563
+ window.addEventListener("mousemove", onMouseMove);
564
+ window.addEventListener("mouseup", onMouseUp);
565
+ };
566
+ const handleResetResize = () => {
567
+ setDimensions(DEFAULT_DIMENSIONS);
568
+ setIsMaximized(false);
569
+ };
570
+ const handleMaximize = () => {
571
+ if (isMaximized) {
572
+ setDimensions(prevDimensions);
573
+ setIsMaximized(false);
574
+ } else {
575
+ setPrevDimensions(dimensions);
576
+ setDimensions({
577
+ width: Math.min(800, window.innerWidth - 48),
578
+ height: Math.min(800, window.innerHeight - 100)
579
+ });
580
+ setIsMaximized(true);
581
+ }
582
+ };
583
+ const isResized = dimensions.width !== DEFAULT_DIMENSIONS.width || dimensions.height !== DEFAULT_DIMENSIONS.height;
499
584
  return /* @__PURE__ */ React6.createElement(React6.Fragment, null, /* @__PURE__ */ React6.createElement(
500
585
  "div",
501
586
  {
502
- className: `fixed z-[9998] w-[380px] max-w-[calc(100vw-3rem)] transition-all duration-300 ease-in-out ${windowPositionClass} ${isOpen ? "opacity-100 translate-y-0 pointer-events-auto" : "opacity-0 translate-y-4 pointer-events-none"}`,
503
- style: { height: "600px", maxHeight: "calc(100vh - 6rem)" }
587
+ className: `fixed z-[9998] max-w-[calc(100vw-3rem)] ease-in-out ${windowPositionClass} ${isOpen ? "opacity-100 translate-y-0 pointer-events-auto" : "opacity-0 translate-y-4 pointer-events-none"} ${isResizing ? "" : "transition-all duration-300"}`,
588
+ style: {
589
+ width: `${dimensions.width}px`,
590
+ height: `${dimensions.height}px`,
591
+ maxHeight: "calc(100vh - 6rem)"
592
+ }
504
593
  },
505
594
  /* @__PURE__ */ React6.createElement(
506
595
  ChatWindow,
507
596
  {
508
597
  className: "h-full relative z-10",
509
598
  showClose: true,
510
- onClose: () => setIsOpen(false)
599
+ onClose: () => setIsOpen(false),
600
+ onResizeStart: ui.allowResize !== false ? handleResizeStart : void 0,
601
+ onResetResize: ui.allowResize !== false ? handleResetResize : void 0,
602
+ isResized,
603
+ onMaximize: ui.allowResize !== false ? handleMaximize : void 0,
604
+ isMaximized
511
605
  }
512
606
  ),
513
607
  /* @__PURE__ */ React6.createElement(
package/dist/server.d.mts CHANGED
@@ -1,7 +1,7 @@
1
- import { i as VectorDBConfig, L as LLMConfig, c as EmbeddingConfig, g as RagConfig, e as IngestDocument, C as ChatMessage, b as ChatResponse, k as RetrievalResult, h as UpsertDocument, V as VectorMatch, G as GraphDBConfig, l as GraphNode, m as Edge, n as GraphSearchResult, I as ILLMProvider, j as VectorDBProvider, f as LLMProvider, d as EmbeddingProvider, R as RAGConfig, U as UIConfig, a as ChatOptions, E as EmbedOptions } from './RagConfig-BjC6zSTV.mjs';
1
+ import { i as VectorDBConfig, L as LLMConfig, c as EmbeddingConfig, g as RagConfig, e as IngestDocument, C as ChatMessage, b as ChatResponse, k as RetrievalResult, h as UpsertDocument, V as VectorMatch, G as GraphDBConfig, l as GraphNode, m as Edge, n as GraphSearchResult, I as ILLMProvider, j as VectorDBProvider, f as LLMProvider, d as EmbeddingProvider, R as RAGConfig, U as UIConfig, a as ChatOptions, E as EmbedOptions } from './RagConfig-DVovvPmd.mjs';
2
2
  export { C as Chunk, a as ChunkOptions, D as DocumentChunker } from './DocumentChunker-Dh9TvmGG.mjs';
3
- import { H as HealthCheckResult, I as IProviderValidator, a as IProviderHealthChecker } from './index-CU_fQq__.mjs';
4
- export { C as ConfigValidator, V as ValidationError, b as VectorPlugin, c as createChatHandler, d as createHealthHandler, e as createIngestHandler, f as createStreamHandler, g as createUploadHandler, s as sseErrorFrame, h as sseFrame, i as sseMetaFrame, j as sseTextFrame } from './index-CU_fQq__.mjs';
3
+ import { H as HealthCheckResult, I as IProviderValidator, a as IProviderHealthChecker } from './index-D0_2f-43.mjs';
4
+ export { C as ConfigValidator, V as ValidationError, b as VectorPlugin, c as createChatHandler, d as createHealthHandler, e as createIngestHandler, f as createStreamHandler, g as createUploadHandler, s as sseErrorFrame, h as sseFrame, i as sseMetaFrame, j as sseTextFrame } from './index-D0_2f-43.mjs';
5
5
  import 'next/server';
6
6
 
7
7
  /**
package/dist/server.d.ts CHANGED
@@ -1,7 +1,7 @@
1
- import { i as VectorDBConfig, L as LLMConfig, c as EmbeddingConfig, g as RagConfig, e as IngestDocument, C as ChatMessage, b as ChatResponse, k as RetrievalResult, h as UpsertDocument, V as VectorMatch, G as GraphDBConfig, l as GraphNode, m as Edge, n as GraphSearchResult, I as ILLMProvider, j as VectorDBProvider, f as LLMProvider, d as EmbeddingProvider, R as RAGConfig, U as UIConfig, a as ChatOptions, E as EmbedOptions } from './RagConfig-BjC6zSTV.js';
1
+ import { i as VectorDBConfig, L as LLMConfig, c as EmbeddingConfig, g as RagConfig, e as IngestDocument, C as ChatMessage, b as ChatResponse, k as RetrievalResult, h as UpsertDocument, V as VectorMatch, G as GraphDBConfig, l as GraphNode, m as Edge, n as GraphSearchResult, I as ILLMProvider, j as VectorDBProvider, f as LLMProvider, d as EmbeddingProvider, R as RAGConfig, U as UIConfig, a as ChatOptions, E as EmbedOptions } from './RagConfig-DVovvPmd.js';
2
2
  export { C as Chunk, a as ChunkOptions, D as DocumentChunker } from './DocumentChunker-Dh9TvmGG.js';
3
- import { H as HealthCheckResult, I as IProviderValidator, a as IProviderHealthChecker } from './index-C3bLmWcR.js';
4
- export { C as ConfigValidator, V as ValidationError, b as VectorPlugin, c as createChatHandler, d as createHealthHandler, e as createIngestHandler, f as createStreamHandler, g as createUploadHandler, s as sseErrorFrame, h as sseFrame, i as sseMetaFrame, j as sseTextFrame } from './index-C3bLmWcR.js';
3
+ import { H as HealthCheckResult, I as IProviderValidator, a as IProviderHealthChecker } from './index-CQ0zQ7Zk.js';
4
+ export { C as ConfigValidator, V as ValidationError, b as VectorPlugin, c as createChatHandler, d as createHealthHandler, e as createIngestHandler, f as createStreamHandler, g as createUploadHandler, s as sseErrorFrame, h as sseFrame, i as sseMetaFrame, j as sseTextFrame } from './index-CQ0zQ7Zk.js';
5
5
  import 'next/server';
6
6
 
7
7
  /**
package/dist/server.js CHANGED
@@ -662,6 +662,18 @@ var init_MongoDBProvider = __esm({
662
662
  }
663
663
  async query(vector, topK, namespace, filter) {
664
664
  const publicFilter = this.sanitizeFilter(filter);
665
+ const vectorSearchFilter = {};
666
+ const matchFilter = {};
667
+ if (namespace) {
668
+ vectorSearchFilter.namespace = namespace;
669
+ }
670
+ for (const [key, value] of Object.entries(publicFilter)) {
671
+ if (key === "namespace") {
672
+ vectorSearchFilter.namespace = value;
673
+ } else {
674
+ matchFilter[key] = value;
675
+ }
676
+ }
665
677
  const pipeline = [
666
678
  {
667
679
  $vectorSearch: __spreadValues({
@@ -669,9 +681,15 @@ var init_MongoDBProvider = __esm({
669
681
  path: this.embeddingKey,
670
682
  queryVector: vector,
671
683
  numCandidates: this.config.options.numCandidates || Math.max(topK * 20, 200),
672
- limit: topK
673
- }, Object.keys(publicFilter).length > 0 || namespace ? { filter: __spreadValues(__spreadValues({}, publicFilter), namespace ? { namespace } : {}) } : {})
674
- },
684
+ limit: topK * 2
685
+ }, Object.keys(vectorSearchFilter).length > 0 ? { filter: vectorSearchFilter } : {})
686
+ }
687
+ ];
688
+ if (Object.keys(matchFilter).length > 0) {
689
+ pipeline.push({ $match: matchFilter });
690
+ }
691
+ pipeline.push(
692
+ { $limit: topK },
675
693
  {
676
694
  $project: {
677
695
  _id: 1,
@@ -681,7 +699,7 @@ var init_MongoDBProvider = __esm({
681
699
  score: { $meta: "vectorSearchScore" }
682
700
  }
683
701
  }
684
- ];
702
+ );
685
703
  const results = await this.collection.aggregate(pipeline).toArray();
686
704
  return results.map((res) => ({
687
705
  id: res._id,
@@ -3466,7 +3484,6 @@ var QueryProcessor = class {
3466
3484
  var _a, _b, _c, _d;
3467
3485
  if (!question.trim()) return [];
3468
3486
  const hints = /* @__PURE__ */ new Map();
3469
- const fieldsToSearch = [.../* @__PURE__ */ new Set([...this.COMMON_METADATA_FIELDS, ...validFields])];
3470
3487
  const addHint = (value, field) => {
3471
3488
  const normalizedValue = this.normalizeHintValue(value);
3472
3489
  if (!normalizedValue) return;
@@ -3497,17 +3514,18 @@ var QueryProcessor = class {
3497
3514
  if (name) addHint(name, "name");
3498
3515
  }
3499
3516
  }
3500
- if (fieldsToSearch.length > 0) {
3501
- for (const field of fieldsToSearch) {
3502
- const forwardPattern = new RegExp(`\\b${field}\\s*(?:is|are|:|of)?\\s+["']?([^"\\n?.!,]{2,60})["']?(?=[?.!,]|$)`, "gi");
3503
- for (const match of question.matchAll(forwardPattern)) {
3504
- const value = match[1];
3505
- if (value && !this.isLikelyPromptPhrase(value)) {
3506
- addHint(value, field);
3507
- }
3508
- }
3509
- const reversePattern = new RegExp(`\\b([^"\\n?.!,\\s]{2,60})\\s+${field}\\b`, "gi");
3510
- for (const match of question.matchAll(reversePattern)) {
3517
+ const genericPattern = /\b([a-z][a-z0-9_]{1,30})\s*[:=]\s*["']?([^"'\n?.!,]{1,60})["']?/gi;
3518
+ for (const match of question.matchAll(genericPattern)) {
3519
+ const field = match[1];
3520
+ const value = match[2];
3521
+ if (field && !this.isLikelyPromptPhrase(field)) {
3522
+ addHint(value, field);
3523
+ }
3524
+ }
3525
+ if (validFields.length > 0) {
3526
+ for (const field of validFields) {
3527
+ const pattern = new RegExp(`\\b${field}\\s*(?:is|are|:|of)?\\s+["']?([^"\\n?.!,]{1,60})["']?(?=[?.!,]|$)`, "gi");
3528
+ for (const match of question.matchAll(pattern)) {
3511
3529
  const value = match[1];
3512
3530
  if (value && !this.isLikelyPromptPhrase(value)) {
3513
3531
  addHint(value, field);
@@ -3583,28 +3601,6 @@ var QueryProcessor = class {
3583
3601
  return filter;
3584
3602
  }
3585
3603
  };
3586
- QueryProcessor.COMMON_METADATA_FIELDS = [
3587
- "category",
3588
- "type",
3589
- "brand",
3590
- "status",
3591
- "priority",
3592
- "id",
3593
- "name",
3594
- "email",
3595
- "phone",
3596
- "price",
3597
- "rating",
3598
- "color",
3599
- "size",
3600
- "material",
3601
- "sku",
3602
- "role",
3603
- "department",
3604
- "location",
3605
- "tag",
3606
- "label"
3607
- ];
3608
3604
 
3609
3605
  // src/core/Pipeline.ts
3610
3606
  var LRUEmbeddingCache = class {
package/dist/server.mjs CHANGED
@@ -40,7 +40,7 @@ import {
40
40
  sseFrame,
41
41
  sseMetaFrame,
42
42
  sseTextFrame
43
- } from "./chunk-ZCDJSGUW.mjs";
43
+ } from "./chunk-3GKA3PJF.mjs";
44
44
  import "./chunk-YLTMFW4M.mjs";
45
45
  import {
46
46
  PineconeProvider
@@ -50,7 +50,7 @@ import {
50
50
  } from "./chunk-FLOSGE6A.mjs";
51
51
  import {
52
52
  MongoDBProvider
53
- } from "./chunk-73I6VWU3.mjs";
53
+ } from "./chunk-5AJ4XHLW.mjs";
54
54
  import {
55
55
  MilvusProvider
56
56
  } from "./chunk-U55XRW3U.mjs";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@retrivora-ai/rag-engine",
3
- "version": "1.0.6",
3
+ "version": "1.0.8",
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": "MIT",
@@ -10,10 +10,16 @@ interface ChatWidgetProps {
10
10
  position?: 'bottom-right' | 'bottom-left';
11
11
  }
12
12
 
13
+ const DEFAULT_DIMENSIONS = { width: 380, height: 600 };
14
+
13
15
  export function ChatWidget({ position = 'bottom-right' }: ChatWidgetProps) {
14
16
  const { ui } = useConfig();
15
17
  const [isOpen, setIsOpen] = useState(false);
16
18
  const [hasUnread, setHasUnread] = useState(false);
19
+ const [dimensions, setDimensions] = useState(DEFAULT_DIMENSIONS);
20
+ const [isResizing, setIsResizing] = useState(false);
21
+ const [isMaximized, setIsMaximized] = useState(false);
22
+ const [prevDimensions, setPrevDimensions] = useState(DEFAULT_DIMENSIONS);
17
23
 
18
24
  if (ui.showWidget === false) return null;
19
25
 
@@ -33,20 +39,78 @@ export function ChatWidget({ position = 'bottom-right' }: ChatWidgetProps) {
33
39
  setHasUnread(false);
34
40
  };
35
41
 
42
+ const handleResizeStart = (e: React.MouseEvent) => {
43
+ e.preventDefault();
44
+ setIsResizing(true);
45
+ const startX = e.clientX;
46
+ const startY = e.clientY;
47
+ const startWidth = dimensions.width;
48
+ const startHeight = dimensions.height;
49
+
50
+ const onMouseMove = (moveEvent: MouseEvent) => {
51
+ const deltaX = startX - moveEvent.clientX;
52
+ const deltaY = startY - moveEvent.clientY;
53
+
54
+ setDimensions({
55
+ width: Math.max(320, Math.min(startWidth + deltaX, window.innerWidth - 48)),
56
+ height: Math.max(400, Math.min(startHeight + deltaY, window.innerHeight - 100)),
57
+ });
58
+ };
59
+
60
+ const onMouseUp = () => {
61
+ setIsResizing(false);
62
+ window.removeEventListener('mousemove', onMouseMove);
63
+ window.removeEventListener('mouseup', onMouseUp);
64
+ };
65
+
66
+ window.addEventListener('mousemove', onMouseMove);
67
+ window.addEventListener('mouseup', onMouseUp);
68
+ };
69
+
70
+ const handleResetResize = () => {
71
+ setDimensions(DEFAULT_DIMENSIONS);
72
+ setIsMaximized(false);
73
+ };
74
+
75
+ const handleMaximize = () => {
76
+ if (isMaximized) {
77
+ setDimensions(prevDimensions);
78
+ setIsMaximized(false);
79
+ } else {
80
+ setPrevDimensions(dimensions);
81
+ setDimensions({
82
+ width: Math.min(800, window.innerWidth - 48),
83
+ height: Math.min(800, window.innerHeight - 100),
84
+ });
85
+ setIsMaximized(true);
86
+ }
87
+ };
88
+
89
+ const isResized = dimensions.width !== DEFAULT_DIMENSIONS.width || dimensions.height !== DEFAULT_DIMENSIONS.height;
90
+
36
91
  return (
37
92
  <>
38
93
  {/* ── Chat Window (slides up) ── */}
39
94
  <div
40
- className={`fixed z-[9998] w-[380px] max-w-[calc(100vw-3rem)] transition-all duration-300 ease-in-out ${windowPositionClass} ${isOpen
95
+ className={`fixed z-[9998] max-w-[calc(100vw-3rem)] ease-in-out ${windowPositionClass} ${isOpen
41
96
  ? 'opacity-100 translate-y-0 pointer-events-auto'
42
97
  : 'opacity-0 translate-y-4 pointer-events-none'
43
- }`}
44
- style={{ height: '600px', maxHeight: 'calc(100vh - 6rem)' }}
98
+ } ${isResizing ? '' : 'transition-all duration-300'}`}
99
+ style={{
100
+ width: `${dimensions.width}px`,
101
+ height: `${dimensions.height}px`,
102
+ maxHeight: 'calc(100vh - 6rem)'
103
+ }}
45
104
  >
46
105
  <ChatWindow
47
106
  className="h-full relative z-10"
48
107
  showClose
49
108
  onClose={() => setIsOpen(false)}
109
+ onResizeStart={ui.allowResize !== false ? handleResizeStart : undefined}
110
+ onResetResize={ui.allowResize !== false ? handleResetResize : undefined}
111
+ isResized={isResized}
112
+ onMaximize={ui.allowResize !== false ? handleMaximize : undefined}
113
+ isMaximized={isMaximized}
50
114
  />
51
115
  {/* Pointer (Tail) */}
52
116
  <div
@@ -8,6 +8,9 @@ import {
8
8
  Sparkles,
9
9
  ArrowUp,
10
10
  TriangleAlert,
11
+ RotateCcw,
12
+ Maximize2,
13
+ Minimize2,
11
14
  } from 'lucide-react';
12
15
  import { MessageBubble } from './MessageBubble';
13
16
  import { useConfig } from './ConfigProvider';
@@ -23,9 +26,29 @@ interface ChatWindowProps {
23
26
  onClose?: () => void;
24
27
  /** Whether to show a close (X) button in the header */
25
28
  showClose?: boolean;
29
+ /** Called when the user starts dragging the resize handle */
30
+ onResizeStart?: (e: React.MouseEvent) => void;
31
+ /** Called when the user clicks the reset size button */
32
+ onResetResize?: () => void;
33
+ /** Whether the window has been resized from its default */
34
+ isResized?: boolean;
35
+ /** Called when the user clicks the maximize/minimize button */
36
+ onMaximize?: () => void;
37
+ /** Whether the window is currently maximized */
38
+ isMaximized?: boolean;
26
39
  }
27
40
 
28
- export function ChatWindow({ className = '', style, onClose, showClose = false }: ChatWindowProps) {
41
+ export function ChatWindow({
42
+ className = '',
43
+ style,
44
+ onClose,
45
+ showClose = false,
46
+ onResizeStart,
47
+ onResetResize,
48
+ isResized = false,
49
+ onMaximize,
50
+ isMaximized = false
51
+ }: ChatWindowProps) {
29
52
  const { ui, projectId } = useConfig();
30
53
  const [input, setInput] = useState('');
31
54
  const [mounted, setMounted] = useState(false);
@@ -73,13 +96,23 @@ export function ChatWindow({ className = '', style, onClose, showClose = false }
73
96
 
74
97
  return (
75
98
  <div
76
- className={`flex flex-col border border-slate-200 dark:border-white/10 shadow-2xl transition-all duration-300 ${currentRadius} ${
99
+ className={`relative flex flex-col border border-slate-200 dark:border-white/10 shadow-2xl transition-all duration-300 ${currentRadius} ${
77
100
  isGlass
78
101
  ? 'bg-white/90 dark:bg-[#0f0f1a]/90 backdrop-blur-xl'
79
102
  : 'bg-white dark:bg-[#0f0f1a]'
80
103
  } ${className}`}
81
104
  style={{ '--primary': ui.primaryColor, '--accent': ui.accentColor, ...style } as React.CSSProperties}
82
105
  >
106
+ {/* Resize Handle (Top-Left) */}
107
+ {onResizeStart && (
108
+ <div
109
+ onMouseDown={onResizeStart}
110
+ className="absolute top-0 left-0 w-6 h-6 cursor-nw-resize z-[100] group flex items-start justify-start p-1"
111
+ title="Drag to resize"
112
+ >
113
+ <div className="w-2.5 h-2.5 border-t-2 border-l-2 border-slate-300 dark:border-white/20 group-hover:border-slate-500 dark:group-hover:border-white/50 transition-colors rounded-tl-[2px]" />
114
+ </div>
115
+ )}
83
116
  {/* ── Header ── */}
84
117
  <div
85
118
  className="flex items-center justify-between px-5 py-4 border-b border-slate-200 dark:border-white/10"
@@ -105,32 +138,54 @@ export function ChatWindow({ className = '', style, onClose, showClose = false }
105
138
  </div>
106
139
  </div>
107
140
 
108
- <div className="flex items-center gap-2">
141
+ <div className="flex items-center gap-1.5">
109
142
  {/* Online indicator */}
110
- <span className="flex items-center gap-1 text-[10px] text-emerald-500 dark:text-emerald-400">
143
+ <span className="flex items-center gap-1 text-[10px] text-emerald-500 dark:text-emerald-400 mr-2">
111
144
  <span className="w-1.5 h-1.5 rounded-full bg-emerald-500 dark:bg-emerald-400 animate-pulse" />
112
145
  Online
113
146
  </span>
114
147
 
115
- {mounted && messages.length > 0 && (
116
- <button
117
- onClick={clearHistory}
118
- title="Clear conversation"
119
- className="w-7 h-7 rounded-lg flex items-center justify-center text-slate-400 hover:text-slate-600 dark:text-white/40 dark:hover:text-white/70 hover:bg-slate-100 dark:hover:bg-white/10 transition-all cursor-pointer"
120
- >
121
- <Trash2 className="w-3.5 h-3.5" />
122
- </button>
123
- )}
148
+ <div className="flex items-center bg-slate-100/50 dark:bg-white/5 rounded-lg p-0.5 border border-slate-200/50 dark:border-white/5">
149
+ {mounted && messages.length > 0 && (
150
+ <button
151
+ onClick={clearHistory}
152
+ title="Clear conversation"
153
+ 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"
154
+ >
155
+ <Trash2 className="w-3.5 h-3.5" />
156
+ </button>
157
+ )}
124
158
 
125
- {showClose && onClose && (
126
- <button
127
- onClick={onClose}
128
- title="Close chat"
129
- className="w-7 h-7 rounded-lg flex items-center justify-center text-slate-400 hover:text-slate-600 dark:text-white/40 dark:hover:text-white/70 hover:bg-slate-100 dark:hover:bg-white/10 transition-all cursor-pointer"
130
- >
131
- <X className="w-4 h-4" />
132
- </button>
133
- )}
159
+ {isResized && onResetResize && (
160
+ <button
161
+ onClick={onResetResize}
162
+ title="Reset to default size"
163
+ className="w-7 h-7 rounded-md flex items-center justify-center text-slate-400 hover:text-blue-500 dark:text-white/40 dark:hover:text-blue-400 hover:bg-white dark:hover:bg-white/10 transition-all cursor-pointer"
164
+ >
165
+ <RotateCcw className="w-3.5 h-3.5" />
166
+ </button>
167
+ )}
168
+
169
+ {onMaximize && (
170
+ <button
171
+ onClick={onMaximize}
172
+ title={isMaximized ? "Minimize" : "Maximize"}
173
+ className="w-7 h-7 rounded-md flex items-center justify-center text-slate-400 hover:text-blue-500 dark:text-white/40 dark:hover:text-blue-400 hover:bg-white dark:hover:bg-white/10 transition-all cursor-pointer"
174
+ >
175
+ {isMaximized ? <Minimize2 className="w-3.5 h-3.5" /> : <Maximize2 className="w-3.5 h-3.5" />}
176
+ </button>
177
+ )}
178
+
179
+ {showClose && onClose && (
180
+ <button
181
+ onClick={onClose}
182
+ title="Close chat"
183
+ className="w-7 h-7 rounded-md flex items-center justify-center text-slate-400 hover:text-slate-600 dark:text-white/40 dark:hover:text-white/70 hover:bg-white dark:hover:bg-white/10 transition-all cursor-pointer"
184
+ >
185
+ <X className="w-4 h-4" />
186
+ </button>
187
+ )}
188
+ </div>
134
189
  </div>
135
190
  </div>
136
191
 
@@ -32,6 +32,7 @@ const defaultConfig: ClientConfig = {
32
32
  visualStyle: 'glass',
33
33
  borderRadius: 'xl',
34
34
  allowUpload: true,
35
+ allowResize: true,
35
36
  },
36
37
  };
37
38
 
@@ -147,6 +147,8 @@ export interface UIConfig {
147
147
  borderRadius?: 'none' | 'sm' | 'md' | 'lg' | 'xl' | 'full';
148
148
  /** Whether to allow file uploads directly from the UI */
149
149
  allowUpload?: boolean;
150
+ /** Whether to allow manual resizing of the chat window. Defaults to true. */
151
+ allowResize?: boolean;
150
152
  }
151
153
 
152
154
  // ---------------------------------------------------------------------------