@kite-copilot/chat-panel 0.2.23 → 0.2.25

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.
@@ -884,6 +884,7 @@ function DataRenderer({ type, data }) {
884
884
  // src/ChatPanel.tsx
885
885
  import * as React4 from "react";
886
886
  import { ArrowLeft, ArrowUp, Command, CornerDownLeft, CheckCircle2 as CheckCircle23, SquarePen, Paperclip, X, FileSpreadsheet, Loader2 as Loader22, ChevronLeft, ChevronRight, Sparkles } from "lucide-react";
887
+ import { createClient } from "@supabase/supabase-js";
887
888
  import { Fragment as Fragment2, jsx as jsx9, jsxs as jsxs5 } from "react/jsx-runtime";
888
889
  var DEFAULT_AGENT_URL = "http://localhost:5002";
889
890
  var PANEL_WIDTH = 340;
@@ -937,7 +938,7 @@ function renderMarkdown(text) {
937
938
  "a",
938
939
  {
939
940
  href: linkMatch[2],
940
- className: "text-blue-600 hover:underline",
941
+ className: "kite-link",
941
942
  target: "_blank",
942
943
  rel: "noopener noreferrer",
943
944
  children: linkMatch[1]
@@ -948,7 +949,44 @@ function renderMarkdown(text) {
948
949
  remaining = remaining.slice(linkMatch[0].length);
949
950
  continue;
950
951
  }
951
- const nextSpecial = remaining.search(/[`*\[]/);
952
+ const urlMatch = remaining.match(/^(https?:\/\/[^\s<>]+|www\.[^\s<>]+)/);
953
+ if (urlMatch) {
954
+ const url = urlMatch[1];
955
+ const href = url.startsWith("www.") ? `https://${url}` : url;
956
+ parts.push(
957
+ /* @__PURE__ */ jsx9(
958
+ "a",
959
+ {
960
+ href,
961
+ className: "kite-link",
962
+ target: "_blank",
963
+ rel: "noopener noreferrer",
964
+ children: url
965
+ },
966
+ keyIndex++
967
+ )
968
+ );
969
+ remaining = remaining.slice(url.length);
970
+ continue;
971
+ }
972
+ const emailMatch = remaining.match(/^([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,})/);
973
+ if (emailMatch) {
974
+ const email = emailMatch[1];
975
+ parts.push(
976
+ /* @__PURE__ */ jsx9(
977
+ "a",
978
+ {
979
+ href: `mailto:${email}`,
980
+ className: "kite-link",
981
+ children: email
982
+ },
983
+ keyIndex++
984
+ )
985
+ );
986
+ remaining = remaining.slice(email.length);
987
+ continue;
988
+ }
989
+ const nextSpecial = remaining.search(/[`*\[@h]/);
952
990
  if (nextSpecial === -1) {
953
991
  parts.push(remaining);
954
992
  break;
@@ -1268,17 +1306,23 @@ var initialMessages = [];
1268
1306
  function ChatPanel({
1269
1307
  isOpen = true,
1270
1308
  onClose,
1309
+ onOpen,
1271
1310
  onBack,
1272
1311
  onNavigate,
1273
1312
  onActionComplete,
1274
1313
  currentPage,
1275
1314
  agentUrl = DEFAULT_AGENT_URL,
1276
1315
  startingQuestions: startingQuestionsProp,
1277
- startingQuestionsEndpoint
1316
+ startingQuestionsEndpoint,
1317
+ supabaseUrl,
1318
+ supabaseAnonKey
1278
1319
  } = {}) {
1279
1320
  const [messages, setMessages] = React4.useState(initialMessages);
1280
1321
  const [input, setInput] = React4.useState("");
1281
1322
  const [sessionId, setSessionId] = React4.useState(() => crypto.randomUUID());
1323
+ const [isEscalated, setIsEscalated] = React4.useState(false);
1324
+ const [supabaseClient, setSupabaseClient] = React4.useState(null);
1325
+ const realtimeChannelRef = React4.useRef(null);
1282
1326
  const resetSession = React4.useCallback(() => {
1283
1327
  setSessionId(crypto.randomUUID());
1284
1328
  }, []);
@@ -1369,6 +1413,9 @@ function ChatPanel({
1369
1413
  const [pendingBulkSession, setPendingBulkSession] = React4.useState(null);
1370
1414
  const pendingBulkSessionRef = React4.useRef(null);
1371
1415
  const fileInputRef = React4.useRef(null);
1416
+ const [searchExpanded, setSearchExpanded] = React4.useState(false);
1417
+ const [searchInput, setSearchInput] = React4.useState("");
1418
+ const searchInputRef = React4.useRef(null);
1372
1419
  React4.useEffect(() => {
1373
1420
  if (!activeGuide || activeGuide.id !== "add-api-key" || activeGuide.stepIndex !== 2) {
1374
1421
  return;
@@ -1500,6 +1547,63 @@ function ChatPanel({
1500
1547
  guideComplete,
1501
1548
  onNavigate
1502
1549
  ]);
1550
+ React4.useEffect(() => {
1551
+ if (supabaseUrl && supabaseAnonKey && !supabaseClient) {
1552
+ const client = createClient(supabaseUrl, supabaseAnonKey);
1553
+ setSupabaseClient(client);
1554
+ }
1555
+ }, [supabaseUrl, supabaseAnonKey, supabaseClient]);
1556
+ const subscribeToAgentMessages = React4.useCallback((currentSessionId) => {
1557
+ if (!supabaseClient) return;
1558
+ if (realtimeChannelRef.current) {
1559
+ supabaseClient.removeChannel(realtimeChannelRef.current);
1560
+ }
1561
+ const channel = supabaseClient.channel(`user-chat-${currentSessionId}`).on(
1562
+ "postgres_changes",
1563
+ {
1564
+ event: "INSERT",
1565
+ schema: "public",
1566
+ table: "chat_history",
1567
+ filter: `session_id=eq.${currentSessionId}`
1568
+ },
1569
+ (payload) => {
1570
+ const newMsg = payload.new;
1571
+ if (newMsg.role === "agent") {
1572
+ setMessages((prev) => [
1573
+ ...prev,
1574
+ {
1575
+ id: Date.now(),
1576
+ role: "agent",
1577
+ kind: "text",
1578
+ content: newMsg.content
1579
+ }
1580
+ ]);
1581
+ }
1582
+ }
1583
+ ).subscribe();
1584
+ realtimeChannelRef.current = channel;
1585
+ }, [supabaseClient]);
1586
+ React4.useEffect(() => {
1587
+ return () => {
1588
+ if (realtimeChannelRef.current && supabaseClient) {
1589
+ supabaseClient.removeChannel(realtimeChannelRef.current);
1590
+ }
1591
+ };
1592
+ }, [supabaseClient]);
1593
+ const sendEscalatedMessage = React4.useCallback(async (content) => {
1594
+ if (!supabaseClient || !isEscalated) return false;
1595
+ try {
1596
+ await supabaseClient.from("chat_history").insert({
1597
+ session_id: sessionId,
1598
+ role: "user",
1599
+ content
1600
+ });
1601
+ return true;
1602
+ } catch (err) {
1603
+ console.error("[KiteChat] Failed to send escalated message:", err);
1604
+ return false;
1605
+ }
1606
+ }, [supabaseClient, isEscalated, sessionId]);
1503
1607
  function streamAssistantMessage(messageId, fullText, followups) {
1504
1608
  let textToStream = fullText;
1505
1609
  let extractedFollowups = followups;
@@ -1610,6 +1714,17 @@ function ChatPanel({
1610
1714
  return;
1611
1715
  }
1612
1716
  if (!trimmed) return;
1717
+ if (isEscalated && supabaseClient) {
1718
+ const userMessage = {
1719
+ id: Date.now(),
1720
+ role: "user",
1721
+ content: trimmed
1722
+ };
1723
+ setMessages((prev) => [...prev, userMessage]);
1724
+ sendEscalatedMessage(trimmed);
1725
+ setInput("");
1726
+ return;
1727
+ }
1613
1728
  startChatFlow(trimmed);
1614
1729
  setInput("");
1615
1730
  }
@@ -1718,7 +1833,11 @@ function ChatPanel({
1718
1833
  const isRespondingToNotification = lastAssistantMessage?.isNotificationMessage === true;
1719
1834
  const now = Date.now();
1720
1835
  const userMessage = { id: now, role: "user", content: userText };
1721
- setMessages((prev) => [...prev, userMessage]);
1836
+ setMessages(
1837
+ (prev) => prev.map(
1838
+ (m) => m.followups && m.followups.length > 0 ? { ...m, followupSelected: true } : m
1839
+ ).concat(userMessage)
1840
+ );
1722
1841
  if (isRespondingToNotification) {
1723
1842
  const thankYouMessageId = Date.now() + 1;
1724
1843
  const thankYouMessage = {
@@ -1934,6 +2053,18 @@ function ChatPanel({
1934
2053
  setProgressSteps([]);
1935
2054
  setPhase("idle");
1936
2055
  streamCompleted = true;
2056
+ } else if (eventType === "escalation") {
2057
+ setIsEscalated(true);
2058
+ setPhase("idle");
2059
+ const escalationMessageId = Date.now() + 2;
2060
+ const escalationMessage = {
2061
+ id: escalationMessageId,
2062
+ role: "assistant",
2063
+ kind: "text",
2064
+ content: data.message || "You've been connected to our support queue. An agent will be with you shortly."
2065
+ };
2066
+ setMessages((prev) => [...prev, escalationMessage]);
2067
+ subscribeToAgentMessages(sessionId);
1937
2068
  }
1938
2069
  } catch (parseError) {
1939
2070
  console.error("Failed to parse SSE event:", parseError);
@@ -2596,6 +2727,81 @@ ${userText}`
2596
2727
  ]);
2597
2728
  }
2598
2729
  }
2730
+ if (!isOpen) {
2731
+ return /* @__PURE__ */ jsx9("div", { className: "fixed bottom-6 left-1/2 -translate-x-1/2 z-50", children: /* @__PURE__ */ jsx9("div", { className: `flex items-center gap-3 rounded-2xl bg-white border border-gray-200 shadow-lg px-4 py-2 hover:shadow-xl transition-all duration-300 ease-out focus-within:border-gray-700 ${searchExpanded ? "w-[480px]" : "w-[320px]"}`, children: !searchExpanded ? /* @__PURE__ */ jsxs5("div", { className: "flex items-center gap-3 w-full", children: [
2732
+ /* @__PURE__ */ jsxs5(
2733
+ "button",
2734
+ {
2735
+ onClick: () => {
2736
+ setSearchExpanded(true);
2737
+ setTimeout(() => searchInputRef.current?.focus(), 100);
2738
+ },
2739
+ className: "flex items-center gap-3 flex-1 group",
2740
+ children: [
2741
+ /* @__PURE__ */ jsx9("span", { className: "text-sm text-gray-500 flex-1 text-left", children: "Ask a question..." }),
2742
+ /* @__PURE__ */ jsx9("span", { className: "text-xs text-gray-400 font-medium", children: "\u2318I" })
2743
+ ]
2744
+ }
2745
+ ),
2746
+ /* @__PURE__ */ jsx9(
2747
+ "button",
2748
+ {
2749
+ onClick: () => {
2750
+ onOpen?.();
2751
+ },
2752
+ className: "h-7 w-7 rounded-lg bg-gray-100 hover:bg-gray-200 flex items-center justify-center transition-colors",
2753
+ children: /* @__PURE__ */ jsx9(Sparkles, { className: "h-3.5 w-3.5 text-black", fill: "currentColor" })
2754
+ }
2755
+ )
2756
+ ] }) : /* @__PURE__ */ jsxs5(Fragment2, { children: [
2757
+ /* @__PURE__ */ jsx9(
2758
+ "input",
2759
+ {
2760
+ ref: searchInputRef,
2761
+ type: "text",
2762
+ value: searchInput,
2763
+ onChange: (e) => setSearchInput(e.target.value),
2764
+ onKeyDown: (e) => {
2765
+ if (e.key === "Enter" && searchInput.trim()) {
2766
+ e.preventDefault();
2767
+ onOpen?.();
2768
+ startChatFlow(searchInput.trim());
2769
+ setSearchInput("");
2770
+ setSearchExpanded(false);
2771
+ } else if (e.key === "Escape") {
2772
+ setSearchExpanded(false);
2773
+ setSearchInput("");
2774
+ }
2775
+ },
2776
+ onBlur: () => {
2777
+ if (!searchInput.trim()) {
2778
+ setTimeout(() => setSearchExpanded(false), 200);
2779
+ }
2780
+ },
2781
+ placeholder: "Ask a question...",
2782
+ className: "flex-1 text-sm text-gray-700 outline-none bg-transparent"
2783
+ }
2784
+ ),
2785
+ /* @__PURE__ */ jsxs5("div", { className: "flex items-center gap-2", children: [
2786
+ /* @__PURE__ */ jsx9("span", { className: "text-xs text-gray-400 font-medium", children: "\u21B5 Enter" }),
2787
+ /* @__PURE__ */ jsx9(
2788
+ "button",
2789
+ {
2790
+ onClick: () => {
2791
+ onOpen?.();
2792
+ if (searchInput.trim()) {
2793
+ setInput(searchInput);
2794
+ }
2795
+ setSearchInput("");
2796
+ setSearchExpanded(false);
2797
+ },
2798
+ className: "h-7 w-7 rounded-lg bg-gray-100 hover:bg-gray-200 flex items-center justify-center transition-colors cursor-pointer",
2799
+ children: /* @__PURE__ */ jsx9(Sparkles, { className: "h-3.5 w-3.5 text-black", fill: "currentColor" })
2800
+ }
2801
+ )
2802
+ ] })
2803
+ ] }) }) });
2804
+ }
2599
2805
  return /* @__PURE__ */ jsxs5(
2600
2806
  "section",
2601
2807
  {
@@ -2726,6 +2932,12 @@ ${userText}`
2726
2932
  if (isUser) {
2727
2933
  return /* @__PURE__ */ jsx9("div", { className: `flex justify-end ${isRoleChange ? "mt-3" : ""}`, children: /* @__PURE__ */ jsx9("div", { className: "max-w-[260px] rounded-2xl rounded-br-md bg-gray-900 px-3.5 py-2.5 text-sm text-white shadow-sm", children: message.content }) }, message.id);
2728
2934
  }
2935
+ if (message.role === "agent") {
2936
+ return /* @__PURE__ */ jsx9("div", { className: `flex justify-start ${isRoleChange ? "mt-3" : ""}`, children: /* @__PURE__ */ jsxs5("div", { className: "max-w-[260px] rounded-2xl rounded-bl-md bg-blue-50 border border-blue-200 px-3.5 py-2.5 text-sm text-gray-900 shadow-sm", children: [
2937
+ /* @__PURE__ */ jsx9("div", { className: "text-xs text-blue-600 font-medium mb-1", children: "Support Agent" }),
2938
+ /* @__PURE__ */ jsx9("div", { className: "whitespace-pre-wrap leading-relaxed", children: renderMarkdown(message.content || "") })
2939
+ ] }) }, message.id);
2940
+ }
2729
2941
  if (message.kind === "searchSummary") {
2730
2942
  return /* @__PURE__ */ jsx9(
2731
2943
  "div",
@@ -3937,7 +4149,9 @@ function ChatPanelWithToggle({
3937
4149
  startingQuestionsEndpoint,
3938
4150
  defaultOpen = false,
3939
4151
  isOpen: controlledIsOpen,
3940
- onOpenChange
4152
+ onOpenChange,
4153
+ supabaseUrl,
4154
+ supabaseAnonKey
3941
4155
  }) {
3942
4156
  const [internalIsOpen, setInternalIsOpen] = React4.useState(defaultOpen);
3943
4157
  const isOpen = controlledIsOpen !== void 0 ? controlledIsOpen : internalIsOpen;
@@ -3957,22 +4171,22 @@ function ChatPanelWithToggle({
3957
4171
  document.body.style.transition = originalTransition;
3958
4172
  };
3959
4173
  }, [isOpen]);
3960
- return /* @__PURE__ */ jsxs5(Fragment2, { children: [
3961
- /* @__PURE__ */ jsx9(PanelToggle, { isOpen, onClick: () => setIsOpen(!isOpen) }),
3962
- /* @__PURE__ */ jsx9(
3963
- ChatPanel,
3964
- {
3965
- isOpen,
3966
- onClose: () => setIsOpen(false),
3967
- onNavigate,
3968
- onActionComplete,
3969
- currentPage,
3970
- agentUrl,
3971
- startingQuestions,
3972
- startingQuestionsEndpoint
3973
- }
3974
- )
3975
- ] });
4174
+ return /* @__PURE__ */ jsx9(
4175
+ ChatPanel,
4176
+ {
4177
+ isOpen,
4178
+ onClose: () => setIsOpen(false),
4179
+ onOpen: () => setIsOpen(true),
4180
+ onNavigate,
4181
+ onActionComplete,
4182
+ currentPage,
4183
+ agentUrl,
4184
+ startingQuestions,
4185
+ startingQuestionsEndpoint,
4186
+ supabaseUrl,
4187
+ supabaseAnonKey
4188
+ }
4189
+ );
3976
4190
  }
3977
4191
  function HelpButton({ onClick, className = "" }) {
3978
4192
  return /* @__PURE__ */ jsx9(
@@ -87,6 +87,8 @@ interface ChatPanelProps {
87
87
  isOpen?: boolean;
88
88
  /** Callback when the panel should close */
89
89
  onClose?: () => void;
90
+ /** Callback when the panel should open */
91
+ onOpen?: () => void;
90
92
  onBack?: () => void;
91
93
  onNavigate?: (page: Page, subtab?: SettingsTab) => void;
92
94
  onActionComplete?: (actionType: ActionType, data: ActionData) => void;
@@ -96,8 +98,12 @@ interface ChatPanelProps {
96
98
  startingQuestions?: StartingQuestion[];
97
99
  /** API endpoint to fetch starting questions */
98
100
  startingQuestionsEndpoint?: string;
101
+ /** Supabase URL for Realtime escalation support */
102
+ supabaseUrl?: string;
103
+ /** Supabase anon key for Realtime escalation support */
104
+ supabaseAnonKey?: string;
99
105
  }
100
- declare function ChatPanel({ isOpen, onClose, onBack, onNavigate, onActionComplete, currentPage, agentUrl, startingQuestions: startingQuestionsProp, startingQuestionsEndpoint, }?: ChatPanelProps): react_jsx_runtime.JSX.Element;
106
+ declare function ChatPanel({ isOpen, onClose, onOpen, onBack, onNavigate, onActionComplete, currentPage, agentUrl, startingQuestions: startingQuestionsProp, startingQuestionsEndpoint, supabaseUrl, supabaseAnonKey, }?: ChatPanelProps): react_jsx_runtime.JSX.Element;
101
107
  /**
102
108
  * PanelToggle - An arrow button on the right edge that toggles the side panel
103
109
  * Shows left arrow when closed (click to open), right arrow when open (click to close)
@@ -142,8 +148,12 @@ interface ChatPanelWithToggleProps {
142
148
  isOpen?: boolean;
143
149
  /** Callback when panel open state changes */
144
150
  onOpenChange?: (isOpen: boolean) => void;
151
+ /** Supabase URL for Realtime escalation support */
152
+ supabaseUrl?: string;
153
+ /** Supabase anon key for Realtime escalation support */
154
+ supabaseAnonKey?: string;
145
155
  }
146
- declare function ChatPanelWithToggle({ onNavigate, onActionComplete, currentPage, agentUrl, startingQuestions, startingQuestionsEndpoint, defaultOpen, isOpen: controlledIsOpen, onOpenChange, }: ChatPanelWithToggleProps): react_jsx_runtime.JSX.Element;
156
+ declare function ChatPanelWithToggle({ onNavigate, onActionComplete, currentPage, agentUrl, startingQuestions, startingQuestionsEndpoint, defaultOpen, isOpen: controlledIsOpen, onOpenChange, supabaseUrl, supabaseAnonKey, }: ChatPanelWithToggleProps): react_jsx_runtime.JSX.Element;
147
157
  /**
148
158
  * @deprecated Use ChatPanelWithToggle instead for the new side panel UX
149
159
  */
@@ -87,6 +87,8 @@ interface ChatPanelProps {
87
87
  isOpen?: boolean;
88
88
  /** Callback when the panel should close */
89
89
  onClose?: () => void;
90
+ /** Callback when the panel should open */
91
+ onOpen?: () => void;
90
92
  onBack?: () => void;
91
93
  onNavigate?: (page: Page, subtab?: SettingsTab) => void;
92
94
  onActionComplete?: (actionType: ActionType, data: ActionData) => void;
@@ -96,8 +98,12 @@ interface ChatPanelProps {
96
98
  startingQuestions?: StartingQuestion[];
97
99
  /** API endpoint to fetch starting questions */
98
100
  startingQuestionsEndpoint?: string;
101
+ /** Supabase URL for Realtime escalation support */
102
+ supabaseUrl?: string;
103
+ /** Supabase anon key for Realtime escalation support */
104
+ supabaseAnonKey?: string;
99
105
  }
100
- declare function ChatPanel({ isOpen, onClose, onBack, onNavigate, onActionComplete, currentPage, agentUrl, startingQuestions: startingQuestionsProp, startingQuestionsEndpoint, }?: ChatPanelProps): react_jsx_runtime.JSX.Element;
106
+ declare function ChatPanel({ isOpen, onClose, onOpen, onBack, onNavigate, onActionComplete, currentPage, agentUrl, startingQuestions: startingQuestionsProp, startingQuestionsEndpoint, supabaseUrl, supabaseAnonKey, }?: ChatPanelProps): react_jsx_runtime.JSX.Element;
101
107
  /**
102
108
  * PanelToggle - An arrow button on the right edge that toggles the side panel
103
109
  * Shows left arrow when closed (click to open), right arrow when open (click to close)
@@ -142,8 +148,12 @@ interface ChatPanelWithToggleProps {
142
148
  isOpen?: boolean;
143
149
  /** Callback when panel open state changes */
144
150
  onOpenChange?: (isOpen: boolean) => void;
151
+ /** Supabase URL for Realtime escalation support */
152
+ supabaseUrl?: string;
153
+ /** Supabase anon key for Realtime escalation support */
154
+ supabaseAnonKey?: string;
145
155
  }
146
- declare function ChatPanelWithToggle({ onNavigate, onActionComplete, currentPage, agentUrl, startingQuestions, startingQuestionsEndpoint, defaultOpen, isOpen: controlledIsOpen, onOpenChange, }: ChatPanelWithToggleProps): react_jsx_runtime.JSX.Element;
156
+ declare function ChatPanelWithToggle({ onNavigate, onActionComplete, currentPage, agentUrl, startingQuestions, startingQuestionsEndpoint, defaultOpen, isOpen: controlledIsOpen, onOpenChange, supabaseUrl, supabaseAnonKey, }: ChatPanelWithToggleProps): react_jsx_runtime.JSX.Element;
147
157
  /**
148
158
  * @deprecated Use ChatPanelWithToggle instead for the new side panel UX
149
159
  */