@farming-labs/theme 0.1.60 → 0.1.63

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.
@@ -16,7 +16,8 @@ declare function DocsSearchDialog({
16
16
  loaderVariant,
17
17
  loadingComponentHtml,
18
18
  models,
19
- defaultModelId
19
+ defaultModelId,
20
+ analytics
20
21
  }: {
21
22
  open: boolean;
22
23
  onOpenChange: (open: boolean) => void;
@@ -27,6 +28,7 @@ declare function DocsSearchDialog({
27
28
  loadingComponentHtml?: string;
28
29
  models?: AIModelOption[];
29
30
  defaultModelId?: string;
31
+ analytics?: boolean;
30
32
  }): react.ReactPortal | null;
31
33
  type FloatingPosition = "bottom-right" | "bottom-left" | "bottom-center";
32
34
  type FloatingStyle = "panel" | "modal" | "popover" | "full-modal";
@@ -40,7 +42,8 @@ declare function FloatingAIChat({
40
42
  loaderVariant,
41
43
  loadingComponentHtml,
42
44
  models,
43
- defaultModelId
45
+ defaultModelId,
46
+ analytics
44
47
  }: {
45
48
  api?: string;
46
49
  position?: FloatingPosition;
@@ -52,6 +55,7 @@ declare function FloatingAIChat({
52
55
  loadingComponentHtml?: string;
53
56
  models?: AIModelOption[];
54
57
  defaultModelId?: string;
58
+ analytics?: boolean;
55
59
  }): react_jsx_runtime0.JSX.Element | null;
56
60
  declare function AIModalDialog({
57
61
  open,
@@ -62,7 +66,8 @@ declare function AIModalDialog({
62
66
  loaderVariant,
63
67
  loadingComponentHtml,
64
68
  models,
65
- defaultModelId
69
+ defaultModelId,
70
+ analytics
66
71
  }: {
67
72
  open: boolean;
68
73
  onOpenChange: (open: boolean) => void;
@@ -73,6 +78,7 @@ declare function AIModalDialog({
73
78
  loadingComponentHtml?: string;
74
79
  models?: AIModelOption[];
75
80
  defaultModelId?: string;
81
+ analytics?: boolean;
76
82
  }): react.ReactPortal | null;
77
83
  //#endregion
78
84
  export { AIModalDialog, DocsSearchDialog, FloatingAIChat };
@@ -1,5 +1,6 @@
1
1
  "use client";
2
2
 
3
+ import { emitClientAnalyticsEvent } from "./client-analytics.mjs";
3
4
  import { useCallback, useEffect, useRef, useState } from "react";
4
5
  import { createPortal } from "react-dom";
5
6
  import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
@@ -345,7 +346,7 @@ function ModelSelector({ models, selectedId, onChange, disabled }) {
345
346
  })]
346
347
  });
347
348
  }
348
- function AIChat({ api, messages, setMessages, aiInput, setAiInput, isStreaming, setIsStreaming, suggestedQuestions, aiLabel, loaderVariant, loadingComponentHtml, models, defaultModelId }) {
349
+ function AIChat({ api, messages, setMessages, aiInput, setAiInput, isStreaming, setIsStreaming, suggestedQuestions, aiLabel, loaderVariant, loadingComponentHtml, models, defaultModelId, analytics, surface = "chat" }) {
349
350
  const label = aiLabel || "AI";
350
351
  const aiInputRef = useRef(null);
351
352
  const messagesEndRef = useRef(null);
@@ -373,6 +374,16 @@ function AIChat({ api, messages, setMessages, aiInput, setAiInput, isStreaming,
373
374
  role: "assistant",
374
375
  content: ""
375
376
  }]);
377
+ const startedAt = Date.now();
378
+ if (analytics) emitClientAnalyticsEvent({
379
+ type: "ai_question",
380
+ properties: {
381
+ surface,
382
+ questionLength: question.length,
383
+ messageCount: newMessages.length,
384
+ model: effectiveModelId
385
+ }
386
+ });
376
387
  try {
377
388
  const res = await fetch(api, {
378
389
  method: "POST",
@@ -395,6 +406,15 @@ function AIChat({ api, messages, setMessages, aiInput, setAiInput, isStreaming,
395
406
  content: errMsg
396
407
  }]);
397
408
  setIsStreaming(false);
409
+ if (analytics) emitClientAnalyticsEvent({
410
+ type: "ai_error",
411
+ properties: {
412
+ surface,
413
+ status: res.status,
414
+ questionLength: question.length,
415
+ durationMs: Math.max(0, Date.now() - startedAt)
416
+ }
417
+ });
398
418
  return;
399
419
  }
400
420
  const reader = res.body.getReader();
@@ -426,11 +446,29 @@ function AIChat({ api, messages, setMessages, aiInput, setAiInput, isStreaming,
426
446
  role: "assistant",
427
447
  content: assistantContent
428
448
  }]);
449
+ if (analytics) emitClientAnalyticsEvent({
450
+ type: "ai_response",
451
+ properties: {
452
+ surface,
453
+ questionLength: question.length,
454
+ responseLength: assistantContent.length,
455
+ durationMs: Math.max(0, Date.now() - startedAt),
456
+ model: effectiveModelId
457
+ }
458
+ });
429
459
  } catch {
430
460
  setMessages([...newMessages, {
431
461
  role: "assistant",
432
462
  content: "Failed to connect. Please try again."
433
463
  }]);
464
+ if (analytics) emitClientAnalyticsEvent({
465
+ type: "ai_error",
466
+ properties: {
467
+ surface,
468
+ questionLength: question.length,
469
+ durationMs: Math.max(0, Date.now() - startedAt)
470
+ }
471
+ });
434
472
  }
435
473
  setIsStreaming(false);
436
474
  }, [
@@ -440,7 +478,9 @@ function AIChat({ api, messages, setMessages, aiInput, setAiInput, isStreaming,
440
478
  setMessages,
441
479
  setAiInput,
442
480
  setIsStreaming,
443
- effectiveModelId
481
+ effectiveModelId,
482
+ analytics,
483
+ surface
444
484
  ]);
445
485
  const handleAskAI = useCallback(async () => {
446
486
  await submitQuestion(aiInput);
@@ -527,6 +567,10 @@ function AIChat({ api, messages, setMessages, aiInput, setAiInput, isStreaming,
527
567
  onClick: () => {
528
568
  setMessages([]);
529
569
  setAiInput("");
570
+ if (analytics) emitClientAnalyticsEvent({
571
+ type: "ai_clear",
572
+ properties: { surface }
573
+ });
530
574
  },
531
575
  className: "fd-ai-clear-btn",
532
576
  children: "Clear chat"
@@ -556,7 +600,7 @@ function AIChat({ api, messages, setMessages, aiInput, setAiInput, isStreaming,
556
600
  })]
557
601
  });
558
602
  }
559
- function DocsSearchDialog({ open, onOpenChange, api = "/api/docs", suggestedQuestions, aiLabel, loaderVariant, loadingComponentHtml, models, defaultModelId }) {
603
+ function DocsSearchDialog({ open, onOpenChange, api = "/api/docs", suggestedQuestions, aiLabel, loaderVariant, loadingComponentHtml, models, defaultModelId, analytics = false }) {
560
604
  const [tab, setTab] = useState("search");
561
605
  const [searchQuery, setSearchQuery] = useState("");
562
606
  const [searchResults, setSearchResults] = useState([]);
@@ -572,6 +616,13 @@ function DocsSearchDialog({ open, onOpenChange, api = "/api/docs", suggestedQues
572
616
  })() || (Array.isArray(models) && models.length > 0 ? models[0].id : void 0);
573
617
  useEffect(() => {
574
618
  if (open) {
619
+ if (analytics) emitClientAnalyticsEvent({
620
+ type: "search_open",
621
+ properties: {
622
+ mode: "ai-dialog",
623
+ tab
624
+ }
625
+ });
575
626
  setSearchQuery("");
576
627
  setSearchResults([]);
577
628
  setActiveIndex(0);
@@ -579,7 +630,11 @@ function DocsSearchDialog({ open, onOpenChange, api = "/api/docs", suggestedQues
579
630
  if (tab === "search") searchInputRef.current?.focus();
580
631
  }, 50);
581
632
  }
582
- }, [open, tab]);
633
+ }, [
634
+ analytics,
635
+ open,
636
+ tab
637
+ ]);
583
638
  useEffect(() => {
584
639
  if (!open) return;
585
640
  const handler = (e) => {
@@ -603,19 +658,40 @@ function DocsSearchDialog({ open, onOpenChange, api = "/api/docs", suggestedQues
603
658
  }
604
659
  setIsSearching(true);
605
660
  const timer = setTimeout(async () => {
661
+ const startedAt = Date.now();
606
662
  try {
607
663
  const requestUrl = new URL(api, window.location.origin);
608
664
  requestUrl.searchParams.set("query", searchQuery);
609
665
  const res = await fetch(requestUrl.toString());
610
666
  if (res.ok) {
611
- setSearchResults(await res.json());
667
+ const data = await res.json();
668
+ setSearchResults(data);
612
669
  setActiveIndex(0);
670
+ if (analytics) emitClientAnalyticsEvent({
671
+ type: "search_query",
672
+ properties: {
673
+ mode: "ai-dialog",
674
+ queryLength: searchQuery.length,
675
+ resultCount: Array.isArray(data) ? data.length : 0,
676
+ durationMs: Math.max(0, Date.now() - startedAt)
677
+ }
678
+ });
613
679
  }
614
- } catch {}
680
+ } catch {
681
+ if (analytics) emitClientAnalyticsEvent({
682
+ type: "search_error",
683
+ properties: {
684
+ mode: "ai-dialog",
685
+ queryLength: searchQuery.length,
686
+ durationMs: Math.max(0, Date.now() - startedAt)
687
+ }
688
+ });
689
+ }
615
690
  setIsSearching(false);
616
691
  }, 150);
617
692
  return () => clearTimeout(timer);
618
693
  }, [
694
+ analytics,
619
695
  searchQuery,
620
696
  api,
621
697
  tab
@@ -630,6 +706,17 @@ function DocsSearchDialog({ open, onOpenChange, api = "/api/docs", suggestedQues
630
706
  } else if (e.key === "Enter" && searchResults[activeIndex]) {
631
707
  e.preventDefault();
632
708
  onOpenChange(false);
709
+ if (analytics) emitClientAnalyticsEvent({
710
+ type: "search_result_click",
711
+ path: searchResults[activeIndex].url,
712
+ properties: {
713
+ mode: "ai-dialog",
714
+ resultId: searchResults[activeIndex].id,
715
+ resultUrl: searchResults[activeIndex].url,
716
+ resultType: searchResults[activeIndex].type,
717
+ queryLength: searchQuery.length
718
+ }
719
+ });
633
720
  window.location.href = searchResults[activeIndex].url;
634
721
  }
635
722
  };
@@ -662,7 +749,16 @@ function DocsSearchDialog({ open, onOpenChange, api = "/api/docs", suggestedQues
662
749
  children: [/* @__PURE__ */ jsx(SearchIcon, {}), " Search"]
663
750
  }),
664
751
  /* @__PURE__ */ jsxs("button", {
665
- onClick: () => setTab("ai"),
752
+ onClick: () => {
753
+ setTab("ai");
754
+ if (analytics) emitClientAnalyticsEvent({
755
+ type: "ai_open",
756
+ properties: {
757
+ mode: "ai-dialog",
758
+ trigger: "tab"
759
+ }
760
+ });
761
+ },
666
762
  className: "fd-ai-tab",
667
763
  "data-active": tab === "ai",
668
764
  children: [
@@ -712,6 +808,17 @@ function DocsSearchDialog({ open, onOpenChange, api = "/api/docs", suggestedQues
712
808
  children: searchResults.length > 0 ? searchResults.map((result, i) => /* @__PURE__ */ jsxs("button", {
713
809
  onClick: () => {
714
810
  onOpenChange(false);
811
+ if (analytics) emitClientAnalyticsEvent({
812
+ type: "search_result_click",
813
+ path: result.url,
814
+ properties: {
815
+ mode: "ai-dialog",
816
+ resultId: result.id,
817
+ resultUrl: result.url,
818
+ resultType: result.type,
819
+ queryLength: searchQuery.length
820
+ }
821
+ });
715
822
  window.location.href = result.url;
716
823
  },
717
824
  onMouseEnter: () => setActiveIndex(i),
@@ -740,7 +847,9 @@ function DocsSearchDialog({ open, onOpenChange, api = "/api/docs", suggestedQues
740
847
  loaderVariant,
741
848
  loadingComponentHtml,
742
849
  models,
743
- defaultModelId: effectiveModelId
850
+ defaultModelId: effectiveModelId,
851
+ analytics,
852
+ surface: "ai-dialog"
744
853
  })
745
854
  ]
746
855
  })] }), document.body);
@@ -814,7 +923,7 @@ function getContainerStyles(style, position) {
814
923
  function getAnimation(style) {
815
924
  return style === "modal" ? "fd-ai-float-center-in 200ms ease-out" : "fd-ai-float-in 200ms ease-out";
816
925
  }
817
- function FloatingAIChat({ api = "/api/docs", position = "bottom-right", floatingStyle = "panel", triggerComponentHtml, suggestedQuestions, aiLabel, loaderVariant, loadingComponentHtml, models, defaultModelId }) {
926
+ function FloatingAIChat({ api = "/api/docs", position = "bottom-right", floatingStyle = "panel", triggerComponentHtml, suggestedQuestions, aiLabel, loaderVariant, loadingComponentHtml, models, defaultModelId, analytics = false }) {
818
927
  const [mounted, setMounted] = useState(false);
819
928
  const [isOpen, setIsOpen] = useState(false);
820
929
  const [messages, setMessages] = useState([]);
@@ -823,15 +932,27 @@ function FloatingAIChat({ api = "/api/docs", position = "bottom-right", floating
823
932
  useEffect(() => {
824
933
  setMounted(true);
825
934
  }, []);
935
+ const closeFloatingAI = useCallback((trigger) => {
936
+ setIsOpen((wasOpen) => {
937
+ if (wasOpen && analytics) emitClientAnalyticsEvent({
938
+ type: "ai_close",
939
+ properties: {
940
+ mode: floatingStyle === "full-modal" ? "full-modal" : "floating",
941
+ trigger
942
+ }
943
+ });
944
+ return false;
945
+ });
946
+ }, [analytics, floatingStyle]);
826
947
  useEffect(() => {
827
948
  if (isOpen) {
828
949
  const handler = (e) => {
829
- if (e.key === "Escape") setIsOpen(false);
950
+ if (e.key === "Escape") closeFloatingAI("escape");
830
951
  };
831
952
  document.addEventListener("keydown", handler);
832
953
  return () => document.removeEventListener("keydown", handler);
833
954
  }
834
- }, [isOpen]);
955
+ }, [closeFloatingAI, isOpen]);
835
956
  useEffect(() => {
836
957
  if (isOpen && (floatingStyle === "modal" || floatingStyle === "full-modal")) document.body.style.overflow = "hidden";
837
958
  else document.body.style.overflow = "";
@@ -844,6 +965,7 @@ function FloatingAIChat({ api = "/api/docs", position = "bottom-right", floating
844
965
  api,
845
966
  isOpen,
846
967
  setIsOpen,
968
+ closeAI: closeFloatingAI,
847
969
  messages,
848
970
  setMessages,
849
971
  aiInput,
@@ -857,7 +979,8 @@ function FloatingAIChat({ api = "/api/docs", position = "bottom-right", floating
857
979
  triggerComponentHtml,
858
980
  position,
859
981
  models,
860
- defaultModelId
982
+ defaultModelId,
983
+ analytics
861
984
  });
862
985
  const btnPosition = BTN_POSITIONS[position] || BTN_POSITIONS["bottom-right"];
863
986
  const isModal = floatingStyle === "modal";
@@ -865,7 +988,7 @@ function FloatingAIChat({ api = "/api/docs", position = "bottom-right", floating
865
988
  const aiName = aiLabel || "AI";
866
989
  return createPortal(/* @__PURE__ */ jsxs(Fragment$1, { children: [
867
990
  isOpen && isModal && /* @__PURE__ */ jsx("div", {
868
- onClick: () => setIsOpen(false),
991
+ onClick: () => closeFloatingAI("overlay"),
869
992
  className: "fd-ai-overlay"
870
993
  }),
871
994
  isOpen && /* @__PURE__ */ jsxs("div", {
@@ -888,7 +1011,7 @@ function FloatingAIChat({ api = "/api/docs", position = "bottom-right", floating
888
1011
  children: "ESC"
889
1012
  }),
890
1013
  /* @__PURE__ */ jsx("button", {
891
- onClick: () => setIsOpen(false),
1014
+ onClick: () => closeFloatingAI("button"),
892
1015
  className: "fd-ai-close-btn",
893
1016
  children: /* @__PURE__ */ jsx(XIcon, {})
894
1017
  })
@@ -904,16 +1027,36 @@ function FloatingAIChat({ api = "/api/docs", position = "bottom-right", floating
904
1027
  suggestedQuestions,
905
1028
  aiLabel,
906
1029
  loaderVariant,
907
- loadingComponentHtml
1030
+ loadingComponentHtml,
1031
+ analytics,
1032
+ surface: "floating"
908
1033
  })]
909
1034
  }),
910
1035
  !isOpen && (triggerComponentHtml ? /* @__PURE__ */ jsx("div", {
911
- onClick: () => setIsOpen(true),
1036
+ onClick: () => {
1037
+ setIsOpen(true);
1038
+ if (analytics) emitClientAnalyticsEvent({
1039
+ type: "ai_open",
1040
+ properties: {
1041
+ mode: "floating",
1042
+ trigger: "custom-trigger"
1043
+ }
1044
+ });
1045
+ },
912
1046
  className: "fd-ai-floating-trigger",
913
1047
  style: btnPosition,
914
1048
  dangerouslySetInnerHTML: { __html: triggerComponentHtml }
915
1049
  }) : /* @__PURE__ */ jsxs("button", {
916
- onClick: () => setIsOpen(true),
1050
+ onClick: () => {
1051
+ setIsOpen(true);
1052
+ if (analytics) emitClientAnalyticsEvent({
1053
+ type: "ai_open",
1054
+ properties: {
1055
+ mode: "floating",
1056
+ trigger: "button"
1057
+ }
1058
+ });
1059
+ },
917
1060
  "aria-label": `Ask ${aiName}`,
918
1061
  className: "fd-ai-floating-btn",
919
1062
  style: btnPosition,
@@ -921,7 +1064,7 @@ function FloatingAIChat({ api = "/api/docs", position = "bottom-right", floating
921
1064
  }))
922
1065
  ] }), document.body);
923
1066
  }
924
- function FullModalAIChat({ api, isOpen, setIsOpen, messages, setMessages, aiInput, setAiInput, isStreaming, setIsStreaming, suggestedQuestions, aiLabel, loaderVariant, loadingComponentHtml, triggerComponentHtml, position, models, defaultModelId }) {
1067
+ function FullModalAIChat({ api, isOpen, setIsOpen, closeAI, messages, setMessages, aiInput, setAiInput, isStreaming, setIsStreaming, suggestedQuestions, aiLabel, loaderVariant, loadingComponentHtml, triggerComponentHtml, position, models, defaultModelId, analytics }) {
925
1068
  const label = aiLabel || "AI";
926
1069
  const inputRef = useRef(null);
927
1070
  const listRef = useRef(null);
@@ -953,6 +1096,16 @@ function FullModalAIChat({ api, isOpen, setIsOpen, messages, setMessages, aiInpu
953
1096
  role: "assistant",
954
1097
  content: ""
955
1098
  }]);
1099
+ const startedAt = Date.now();
1100
+ if (analytics) emitClientAnalyticsEvent({
1101
+ type: "ai_question",
1102
+ properties: {
1103
+ surface: "full-modal",
1104
+ questionLength: question.length,
1105
+ messageCount: newMessages.length,
1106
+ model: effectiveModelId
1107
+ }
1108
+ });
956
1109
  try {
957
1110
  const res = await fetch(api, {
958
1111
  method: "POST",
@@ -975,6 +1128,15 @@ function FullModalAIChat({ api, isOpen, setIsOpen, messages, setMessages, aiInpu
975
1128
  content: errMsg
976
1129
  }]);
977
1130
  setIsStreaming(false);
1131
+ if (analytics) emitClientAnalyticsEvent({
1132
+ type: "ai_error",
1133
+ properties: {
1134
+ surface: "full-modal",
1135
+ status: res.status,
1136
+ questionLength: question.length,
1137
+ durationMs: Math.max(0, Date.now() - startedAt)
1138
+ }
1139
+ });
978
1140
  return;
979
1141
  }
980
1142
  const reader = res.body.getReader();
@@ -1006,11 +1168,29 @@ function FullModalAIChat({ api, isOpen, setIsOpen, messages, setMessages, aiInpu
1006
1168
  role: "assistant",
1007
1169
  content: assistantContent
1008
1170
  }]);
1171
+ if (analytics) emitClientAnalyticsEvent({
1172
+ type: "ai_response",
1173
+ properties: {
1174
+ surface: "full-modal",
1175
+ questionLength: question.length,
1176
+ responseLength: assistantContent.length,
1177
+ durationMs: Math.max(0, Date.now() - startedAt),
1178
+ model: effectiveModelId
1179
+ }
1180
+ });
1009
1181
  } catch {
1010
1182
  setMessages([...newMessages, {
1011
1183
  role: "assistant",
1012
1184
  content: "Failed to connect. Please try again."
1013
1185
  }]);
1186
+ if (analytics) emitClientAnalyticsEvent({
1187
+ type: "ai_error",
1188
+ properties: {
1189
+ surface: "full-modal",
1190
+ questionLength: question.length,
1191
+ durationMs: Math.max(0, Date.now() - startedAt)
1192
+ }
1193
+ });
1014
1194
  }
1015
1195
  setIsStreaming(false);
1016
1196
  }, [
@@ -1020,7 +1200,8 @@ function FullModalAIChat({ api, isOpen, setIsOpen, messages, setMessages, aiInpu
1020
1200
  setMessages,
1021
1201
  setAiInput,
1022
1202
  setIsStreaming,
1023
- effectiveModelId
1203
+ effectiveModelId,
1204
+ analytics
1024
1205
  ]);
1025
1206
  const canSend = !!(aiInput.trim() && !isStreaming);
1026
1207
  const showSuggestions = messages.length === 0 && !isStreaming;
@@ -1033,12 +1214,12 @@ function FullModalAIChat({ api, isOpen, setIsOpen, messages, setMessages, aiInpu
1033
1214
  return createPortal(/* @__PURE__ */ jsxs(Fragment$1, { children: [isOpen && /* @__PURE__ */ jsxs("div", {
1034
1215
  className: "fd-ai-fm-overlay",
1035
1216
  onClick: (e) => {
1036
- if (e.target === e.currentTarget) setIsOpen(false);
1217
+ if (e.target === e.currentTarget) closeAI("overlay");
1037
1218
  },
1038
1219
  children: [/* @__PURE__ */ jsx("div", {
1039
1220
  className: "fd-ai-fm-topbar",
1040
1221
  children: /* @__PURE__ */ jsx("button", {
1041
- onClick: () => setIsOpen(false),
1222
+ onClick: () => closeAI("button"),
1042
1223
  className: "fd-ai-fm-close-btn",
1043
1224
  children: /* @__PURE__ */ jsx(XIcon, {})
1044
1225
  })
@@ -1071,10 +1252,28 @@ function FullModalAIChat({ api, isOpen, setIsOpen, messages, setMessages, aiInpu
1071
1252
  className: `fd-ai-fm-input-bar ${isOpen ? "fd-ai-fm-input-bar--open" : "fd-ai-fm-input-bar--closed"}`,
1072
1253
  style: isOpen ? void 0 : btnPosition,
1073
1254
  children: !isOpen ? triggerComponentHtml ? /* @__PURE__ */ jsx("div", {
1074
- onClick: () => setIsOpen(true),
1255
+ onClick: () => {
1256
+ setIsOpen(true);
1257
+ if (analytics) emitClientAnalyticsEvent({
1258
+ type: "ai_open",
1259
+ properties: {
1260
+ mode: "full-modal",
1261
+ trigger: "custom-trigger"
1262
+ }
1263
+ });
1264
+ },
1075
1265
  dangerouslySetInnerHTML: { __html: triggerComponentHtml }
1076
1266
  }) : /* @__PURE__ */ jsxs("button", {
1077
- onClick: () => setIsOpen(true),
1267
+ onClick: () => {
1268
+ setIsOpen(true);
1269
+ if (analytics) emitClientAnalyticsEvent({
1270
+ type: "ai_open",
1271
+ properties: {
1272
+ mode: "full-modal",
1273
+ trigger: "button"
1274
+ }
1275
+ });
1276
+ },
1078
1277
  className: "fd-ai-fm-trigger-btn",
1079
1278
  "aria-label": `Ask ${label}`,
1080
1279
  children: [/* @__PURE__ */ jsx(SparklesIcon, { size: 16 }), /* @__PURE__ */ jsxs("span", { children: ["Ask ", label] })]
@@ -1135,6 +1334,10 @@ function FullModalAIChat({ api, isOpen, setIsOpen, messages, setMessages, aiInpu
1135
1334
  if (!isStreaming) {
1136
1335
  setMessages([]);
1137
1336
  setAiInput("");
1337
+ if (analytics) emitClientAnalyticsEvent({
1338
+ type: "ai_clear",
1339
+ properties: { surface: "full-modal" }
1340
+ });
1138
1341
  }
1139
1342
  },
1140
1343
  "aria-disabled": isStreaming,
@@ -1165,18 +1368,28 @@ function TrashIcon() {
1165
1368
  ]
1166
1369
  });
1167
1370
  }
1168
- function AIModalDialog({ open, onOpenChange, api = "/api/docs", suggestedQuestions, aiLabel, loaderVariant, loadingComponentHtml, models, defaultModelId }) {
1371
+ function AIModalDialog({ open, onOpenChange, api = "/api/docs", suggestedQuestions, aiLabel, loaderVariant, loadingComponentHtml, models, defaultModelId, analytics = false }) {
1169
1372
  const [messages, setMessages] = useState([]);
1170
1373
  const [aiInput, setAiInput] = useState("");
1171
1374
  const [isStreaming, setIsStreaming] = useState(false);
1375
+ const closeModalAI = useCallback((trigger) => {
1376
+ if (analytics) emitClientAnalyticsEvent({
1377
+ type: "ai_close",
1378
+ properties: {
1379
+ mode: "modal",
1380
+ trigger
1381
+ }
1382
+ });
1383
+ onOpenChange(false);
1384
+ }, [analytics, onOpenChange]);
1172
1385
  useEffect(() => {
1173
1386
  if (!open) return;
1174
1387
  const handler = (e) => {
1175
- if (e.key === "Escape") onOpenChange(false);
1388
+ if (e.key === "Escape") closeModalAI("escape");
1176
1389
  };
1177
1390
  document.addEventListener("keydown", handler);
1178
1391
  return () => document.removeEventListener("keydown", handler);
1179
- }, [open, onOpenChange]);
1392
+ }, [closeModalAI, open]);
1180
1393
  useEffect(() => {
1181
1394
  if (open) document.body.style.overflow = "hidden";
1182
1395
  else document.body.style.overflow = "";
@@ -1187,7 +1400,7 @@ function AIModalDialog({ open, onOpenChange, api = "/api/docs", suggestedQuestio
1187
1400
  if (!open) return null;
1188
1401
  const aiName = aiLabel || "AI";
1189
1402
  return createPortal(/* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("div", {
1190
- onClick: () => onOpenChange(false),
1403
+ onClick: () => closeModalAI("overlay"),
1191
1404
  className: "fd-ai-overlay"
1192
1405
  }), /* @__PURE__ */ jsxs("div", {
1193
1406
  role: "dialog",
@@ -1216,7 +1429,7 @@ function AIModalDialog({ open, onOpenChange, api = "/api/docs", suggestedQuestio
1216
1429
  children: "ESC"
1217
1430
  }),
1218
1431
  /* @__PURE__ */ jsx("button", {
1219
- onClick: () => onOpenChange(false),
1432
+ onClick: () => closeModalAI("button"),
1220
1433
  className: "fd-ai-close-btn",
1221
1434
  children: /* @__PURE__ */ jsx(XIcon, {})
1222
1435
  })
@@ -1235,7 +1448,9 @@ function AIModalDialog({ open, onOpenChange, api = "/api/docs", suggestedQuestio
1235
1448
  loaderVariant,
1236
1449
  loadingComponentHtml,
1237
1450
  models,
1238
- defaultModelId
1451
+ defaultModelId,
1452
+ analytics,
1453
+ surface: "modal"
1239
1454
  }),
1240
1455
  /* @__PURE__ */ jsx("div", {
1241
1456
  className: "fd-ai-modal-footer",
@@ -1245,6 +1460,10 @@ function AIModalDialog({ open, onOpenChange, api = "/api/docs", suggestedQuestio
1245
1460
  if (!isStreaming) {
1246
1461
  setMessages([]);
1247
1462
  setAiInput("");
1463
+ if (analytics) emitClientAnalyticsEvent({
1464
+ type: "ai_clear",
1465
+ properties: { surface: "modal" }
1466
+ });
1248
1467
  }
1249
1468
  },
1250
1469
  "aria-disabled": isStreaming,
@@ -0,0 +1,23 @@
1
+ "use client";
2
+
3
+ //#region src/client-analytics.ts
4
+ function emitClientAnalyticsEvent(event) {
5
+ if (typeof window === "undefined") return;
6
+ const normalized = {
7
+ ...event,
8
+ source: "client",
9
+ path: window.location.pathname,
10
+ url: window.location.href,
11
+ referrer: document.referrer || void 0,
12
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
13
+ };
14
+ const target = window;
15
+ try {
16
+ if (target.__fdAnalytics__) Promise.resolve(target.__fdAnalytics__(normalized)).catch(() => {});
17
+ else target.__fdAnalyticsQueue__ = [...target.__fdAnalyticsQueue__ ?? [], normalized].slice(-50);
18
+ window.dispatchEvent(new CustomEvent("fd:analytics", { detail: normalized }));
19
+ } catch {}
20
+ }
21
+
22
+ //#endregion
23
+ export { emitClientAnalyticsEvent };
@@ -17,6 +17,7 @@ interface DocsAIFeaturesProps {
17
17
  label: string;
18
18
  }[];
19
19
  defaultModelId?: string;
20
+ analytics?: boolean;
20
21
  }
21
22
  declare function DocsAIFeatures({
22
23
  mode,
@@ -30,7 +31,8 @@ declare function DocsAIFeatures({
30
31
  loaderVariant,
31
32
  loadingComponentHtml,
32
33
  models,
33
- defaultModelId
34
+ defaultModelId,
35
+ analytics
34
36
  }: DocsAIFeaturesProps): react_jsx_runtime0.JSX.Element;
35
37
  //#endregion
36
38
  export { DocsAIFeatures };