opencami 1.8.6 → 1.8.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/README.md +0 -1
- package/dist/client/assets/{CSPContext-6t3O1emU.js → CSPContext-nHSyQniZ.js} +1 -1
- package/dist/client/assets/{DirectionContext-C6goXEY_.js → DirectionContext-B1cuzwIr.js} +1 -1
- package/dist/client/assets/_sessionKey-CNw4O2rY.js +19 -0
- package/dist/client/assets/agents-C6Ev94B1.js +2 -0
- package/dist/client/assets/agents-screen-DpNSh5Ok.js +1 -0
- package/dist/client/assets/bots-B2_u-OmP.js +2 -0
- package/dist/client/assets/bots-screen-Qh_IK9hC.js +1 -0
- package/dist/client/assets/button-DdG8c-XQ.js +1 -0
- package/dist/client/assets/{composite-feK0c-xF.js → composite-Cx-QHT9o.js} +1 -1
- package/dist/client/assets/{connect-02tmQV_v.js → connect-CSbeSBTn.js} +1 -1
- package/dist/client/assets/{dashboard-DQ0zDQKd.js → dashboard-DA1fTRcH.js} +1 -1
- package/dist/client/assets/event-Dwf9IDxK.js +1 -0
- package/dist/client/assets/file-explorer-screen-DzDX4HcB.js +1 -0
- package/dist/client/assets/files-PsZnGOUx.js +2 -0
- package/dist/client/assets/follow-up-suggestions-BYWq-d8P.js +5 -0
- package/dist/client/assets/{index-lK3yGoTI.js → index-B551ln24.js} +1 -1
- package/dist/client/assets/index-BxsgifDH.js +3 -0
- package/dist/client/assets/keyboard-shortcuts-dialog-BX-hH4Wf.js +1 -0
- package/dist/client/assets/main-DBmooBKx.js +210 -0
- package/dist/client/assets/markdown-Bpat4kTr.js +87 -0
- package/dist/client/assets/memory-Bs8hOoEv.js +2 -0
- package/dist/client/assets/memory-screen-1DgDLyZf.js +1 -0
- package/dist/client/assets/menu-ByR1BVmq.js +1 -0
- package/dist/client/assets/{opencami-logo-zuSBm5Br.js → opencami-logo-BV1uPYe6.js} +1 -1
- package/dist/client/assets/popupStateMapping-CGDLUl5Y.js +1 -0
- package/dist/client/assets/proxy-DYrSkM35.js +9 -0
- package/dist/client/assets/{react-BLyCEWpN.js → react-dSDkXQu6.js} +1 -1
- package/dist/client/assets/search-dialog-B3XJLq-O.js +1 -0
- package/dist/client/assets/search-sources-badge-CTd0gLuz.js +1 -0
- package/dist/client/assets/session-export-dialog-Cib97JLm.js +1 -0
- package/dist/client/assets/settings-dialog-B4NLq1ZS.js +1 -0
- package/dist/client/assets/{skills-panel-BH27r3nC.js → skills-panel-Cj7yHGbX.js} +1 -1
- package/dist/client/assets/skills-pueagQNc.js +2 -0
- package/dist/client/assets/styles-Ce2xZzc4.css +1 -0
- package/dist/client/assets/switch-kXs1I0oW.js +1 -0
- package/dist/client/assets/tabs-B6GW7TBf.js +1 -0
- package/dist/client/assets/thinking-DTP9JDQl.js +1 -0
- package/dist/client/assets/tooltip-CtHpm-sQ.js +1 -0
- package/dist/client/assets/use-file-explorer-state-C-D2CShe.js +12 -0
- package/dist/client/assets/{useBaseUiId-MgM4ouhx.js → useBaseUiId-Ckx_aJky.js} +1 -1
- package/dist/client/assets/useCompositeItem-CkvfeGmG.js +1 -0
- package/dist/client/assets/{useControlled-BQxTgsOd.js → useControlled-8D4PSDAL.js} +1 -1
- package/dist/client/assets/{useMutation-12DyB3Ox.js → useMutation-DckvFKPC.js} +1 -1
- package/dist/client/assets/{useQuery-Ctiljcrr.js → useQuery-CtUiG53w.js} +1 -1
- package/dist/server/assets/{_sessionKey-DzsJfprr.js → _sessionKey-LV6xK9IM.js} +548 -947
- package/dist/server/assets/{_tanstack-start-manifest_v-C5HBDfQB.js → _tanstack-start-manifest_v-qVhiIEVc.js} +1 -1
- package/dist/server/assets/{connect-CbgijWz4.js → connect-BNabuqpW.js} +1 -1
- package/dist/server/assets/follow-up-suggestions-CSSc4PDe.js +336 -0
- package/dist/server/assets/{index-Dl2BOKP7.js → index-BEWnDAH6.js} +24 -5
- package/dist/server/assets/{index-BFHEmXpN.js → index-DMKS4aeI.js} +1 -1
- package/dist/server/assets/{markdown-BFE5y9YH.js → markdown-DoX5Q7qh.js} +50 -26
- package/dist/server/assets/{memory-BqZOoD7Q.js → memory-Cxu7i8ej.js} +1 -1
- package/dist/server/assets/{memory-screen-BK5phS8K.js → memory-screen-B5l1NZRY.js} +2 -2
- package/dist/server/assets/{router-BZPatFG9.js → router-Cr2xCvGA.js} +5 -5
- package/dist/server/assets/{search-dialog-DQRkARXw.js → search-dialog-DR6zBnui.js} +4 -4
- package/dist/server/assets/search-sources-badge-B0rAEDs_.js +106 -0
- package/dist/server/assets/{settings-dialog-Bc1ta26X.js → settings-dialog-DEMlCMCP.js} +4 -4
- package/dist/server/assets/thinking-BpAc3itF.js +92 -0
- package/dist/server/server.js +38 -195
- package/package.json +2 -6
- package/dist/client/assets/_sessionKey-B5Viv43f.js +0 -23
- package/dist/client/assets/agents-BmE6QOwl.js +0 -2
- package/dist/client/assets/agents-screen-pHUzJxX5.js +0 -1
- package/dist/client/assets/bots-BeOkwrXr.js +0 -2
- package/dist/client/assets/bots-screen-B79bAYvf.js +0 -1
- package/dist/client/assets/button-CK8tKQ-Z.js +0 -1
- package/dist/client/assets/event-BsD1rqGT.js +0 -1
- package/dist/client/assets/file-explorer-screen-Ds7LeJTd.js +0 -1
- package/dist/client/assets/files-e40B1zFy.js +0 -2
- package/dist/client/assets/index-rljDU_1M.js +0 -3
- package/dist/client/assets/keyboard-shortcuts-dialog-Bb_GOr9L.js +0 -1
- package/dist/client/assets/main-Dq6jpr6-.js +0 -210
- package/dist/client/assets/markdown-C7_Aipwd.js +0 -87
- package/dist/client/assets/memory-C7UG-1le.js +0 -2
- package/dist/client/assets/memory-screen-CUFBWsq5.js +0 -1
- package/dist/client/assets/menu-n6L--M9R.js +0 -1
- package/dist/client/assets/proxy-BU8Bw1Vt.js +0 -9
- package/dist/client/assets/search-dialog-yB4w5ajo.js +0 -1
- package/dist/client/assets/session-export-dialog-qbZgd2Zo.js +0 -1
- package/dist/client/assets/settings-dialog-CHJbvpgk.js +0 -1
- package/dist/client/assets/skills-DoKPPhNY.js +0 -2
- package/dist/client/assets/styles-CXV5jZiD.css +0 -1
- package/dist/client/assets/switch-BD3a0LRm.js +0 -1
- package/dist/client/assets/tabs-DI1e-kzz.js +0 -1
- package/dist/client/assets/tooltip-BbH3QWvK.js +0 -1
- package/dist/client/assets/use-file-explorer-state-DBfLeAyz.js +0 -12
- package/dist/client/assets/useCompositeItem-OhltNFdZ.js +0 -1
- package/dist/client/assets/useOnFirstRender-7qoaK5sI.js +0 -1
|
@@ -4,7 +4,7 @@ import * as React from "react";
|
|
|
4
4
|
import React__default, { useState, useCallback, memo, useDeferredValue, useMemo, Suspense, lazy, useRef, useEffect, useLayoutEffect, createContext, useContext } from "react";
|
|
5
5
|
import { useQueryClient, useMutation, useQuery } from "@tanstack/react-query";
|
|
6
6
|
import { T as TooltipProvider, a as TooltipRoot, b as TooltipTrigger, c as TooltipContent, s as setChatUiState, d as chatUiQueryKey, g as getChatUiState } from "./tooltip-DgsSPocE.js";
|
|
7
|
-
import { Tick01Icon, Cancel01Icon, MoreHorizontalIcon, Pen01Icon, Upload01Icon, Delete01Icon, BotIcon, Clock01Icon, Chat01Icon, ArrowRight01Icon, SidebarLeft01Icon, PencilEdit02Icon, Folder01Icon, AiBrain01Icon, PackageOpenIcon, SmartPhone01Icon, DashboardCircleIcon, Search01Icon, Settings01Icon, Menu01Icon, Tick02Icon, Copy01Icon, Loading02Icon, StopIcon, VolumeHighIcon,
|
|
7
|
+
import { Tick01Icon, Cancel01Icon, MoreHorizontalIcon, Pen01Icon, Upload01Icon, Delete01Icon, BotIcon, Clock01Icon, Chat01Icon, ArrowRight01Icon, SidebarLeft01Icon, PencilEdit02Icon, Folder01Icon, AiBrain01Icon, PackageOpenIcon, SmartPhone01Icon, DashboardCircleIcon, Search01Icon, Settings01Icon, Menu01Icon, Tick02Icon, Copy01Icon, Loading02Icon, StopIcon, VolumeHighIcon, Loading03Icon, ArrowDown01Icon, File01Icon, CommandIcon, Attachment01Icon, Mic02Icon, ArrowUp02Icon } from "@hugeicons/core-free-icons";
|
|
8
8
|
import { HugeiconsIcon } from "@hugeicons/react";
|
|
9
9
|
import { motion, AnimatePresence } from "motion/react";
|
|
10
10
|
import { AlertDialog } from "@base-ui/react/alert-dialog";
|
|
@@ -14,12 +14,12 @@ import { Collapsible as Collapsible$1 } from "@base-ui/react/collapsible";
|
|
|
14
14
|
import { ScrollArea } from "@base-ui/react/scroll-area";
|
|
15
15
|
import { M as MenuRoot, a as MenuTrigger, b as MenuContent, c as MenuItem } from "./menu-D90CDTi2.js";
|
|
16
16
|
import { O as OpenCamiLogo, a as OpenCamiText } from "./opencami-logo-C-43FL3R.js";
|
|
17
|
-
import { M as Markdown } from "./markdown-
|
|
18
|
-
import { u as useChatSettings$1 } from "./index-
|
|
17
|
+
import { M as Markdown } from "./markdown-DoX5Q7qh.js";
|
|
18
|
+
import { u as useChatSettings$1 } from "./index-BEWnDAH6.js";
|
|
19
|
+
import { createPortal } from "react-dom";
|
|
19
20
|
import { create } from "zustand";
|
|
20
21
|
import { persist } from "zustand/middleware";
|
|
21
|
-
import {
|
|
22
|
-
import { a as Route } from "./router-BZPatFG9.js";
|
|
22
|
+
import { a as Route } from "./router-Cr2xCvGA.js";
|
|
23
23
|
function deriveFriendlyIdFromKey(key) {
|
|
24
24
|
if (!key) return "main";
|
|
25
25
|
const trimmed = key.trim();
|
|
@@ -1681,7 +1681,7 @@ function areSidebarSessionsEqual(prev, next) {
|
|
|
1681
1681
|
return true;
|
|
1682
1682
|
}
|
|
1683
1683
|
const SettingsDialog = lazy(
|
|
1684
|
-
() => import("./settings-dialog-
|
|
1684
|
+
() => import("./settings-dialog-DEMlCMCP.js").then((m) => ({ default: m.SettingsDialog }))
|
|
1685
1685
|
);
|
|
1686
1686
|
const SessionExportDialog = lazy(
|
|
1687
1687
|
() => import("./session-export-dialog-C53RRAah.js").then((m) => ({
|
|
@@ -2748,50 +2748,6 @@ function MessageContent({
|
|
|
2748
2748
|
);
|
|
2749
2749
|
return markdown ? /* @__PURE__ */ jsx(Markdown, { className: classNames, ...props, children }) : /* @__PURE__ */ jsx("div", { className: classNames, ...props, children });
|
|
2750
2750
|
}
|
|
2751
|
-
function useIsMobile() {
|
|
2752
|
-
const [isMobile, setIsMobile] = useState(
|
|
2753
|
-
() => typeof window !== "undefined" ? window.innerWidth < 768 : false
|
|
2754
|
-
);
|
|
2755
|
-
useEffect(() => {
|
|
2756
|
-
if (typeof window === "undefined") return;
|
|
2757
|
-
const mql = window.matchMedia("(max-width: 767px)");
|
|
2758
|
-
const handler = (e) => setIsMobile(e.matches);
|
|
2759
|
-
mql.addEventListener("change", handler);
|
|
2760
|
-
setIsMobile(mql.matches);
|
|
2761
|
-
return () => mql.removeEventListener("change", handler);
|
|
2762
|
-
}, []);
|
|
2763
|
-
return isMobile;
|
|
2764
|
-
}
|
|
2765
|
-
function Thinking({ content }) {
|
|
2766
|
-
const isMobile = useIsMobile();
|
|
2767
|
-
return /* @__PURE__ */ jsx("div", { className: "inline-flex flex-col", children: /* @__PURE__ */ jsxs(Collapsible, { defaultOpen: !isMobile, children: [
|
|
2768
|
-
/* @__PURE__ */ jsxs(
|
|
2769
|
-
CollapsibleTrigger,
|
|
2770
|
-
{
|
|
2771
|
-
render: /* @__PURE__ */ jsx(
|
|
2772
|
-
Button,
|
|
2773
|
-
{
|
|
2774
|
-
variant: "ghost",
|
|
2775
|
-
className: "h-auto gap-1.5 px-1.5 py-0.5 -mx-2"
|
|
2776
|
-
}
|
|
2777
|
-
),
|
|
2778
|
-
children: [
|
|
2779
|
-
/* @__PURE__ */ jsx("span", { className: "text-sm font-medium text-primary-900", children: "Thinking" }),
|
|
2780
|
-
/* @__PURE__ */ jsx(
|
|
2781
|
-
HugeiconsIcon,
|
|
2782
|
-
{
|
|
2783
|
-
icon: ArrowDown01Icon,
|
|
2784
|
-
size: 14,
|
|
2785
|
-
strokeWidth: 1.5,
|
|
2786
|
-
className: "text-primary-900 transition-transform duration-150 group-data-panel-open:rotate-180"
|
|
2787
|
-
}
|
|
2788
|
-
)
|
|
2789
|
-
]
|
|
2790
|
-
}
|
|
2791
|
-
),
|
|
2792
|
-
/* @__PURE__ */ jsx(CollapsiblePanel, { children: /* @__PURE__ */ jsx("div", { className: "pt-1 mb-3", children: /* @__PURE__ */ jsx("p", { className: "text-sm text-primary-700 whitespace-pre-wrap", children: content }) }) })
|
|
2793
|
-
] }) });
|
|
2794
|
-
}
|
|
2795
2751
|
function Tool({ toolPart, defaultOpen = false }) {
|
|
2796
2752
|
const { state, input, output, toolCallId } = toolPart;
|
|
2797
2753
|
const serialize = (value, maxLength = 3200) => {
|
|
@@ -2871,99 +2827,16 @@ function Tool({ toolPart, defaultOpen = false }) {
|
|
|
2871
2827
|
] }) })
|
|
2872
2828
|
] }) });
|
|
2873
2829
|
}
|
|
2874
|
-
|
|
2875
|
-
|
|
2876
|
-
|
|
2877
|
-
}
|
|
2878
|
-
|
|
2879
|
-
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
-
|
|
2883
|
-
|
|
2884
|
-
{
|
|
2885
|
-
className: "flex items-center justify-center w-5 h-5 rounded-full bg-white dark:bg-zinc-800 border border-cyan-500/20 overflow-hidden",
|
|
2886
|
-
title: domain,
|
|
2887
|
-
children: /* @__PURE__ */ jsx(
|
|
2888
|
-
"img",
|
|
2889
|
-
{
|
|
2890
|
-
src: `https://www.google.com/s2/favicons?domain=${domain}&sz=32`,
|
|
2891
|
-
alt: "",
|
|
2892
|
-
className: "w-4 h-4 object-contain",
|
|
2893
|
-
loading: "lazy",
|
|
2894
|
-
onError: (e) => {
|
|
2895
|
-
const target = e.target;
|
|
2896
|
-
target.style.display = "none";
|
|
2897
|
-
const parent = target.parentElement;
|
|
2898
|
-
if (parent) {
|
|
2899
|
-
parent.textContent = domain.charAt(0).toUpperCase();
|
|
2900
|
-
parent.classList.add("text-[10px]", "font-medium", "text-cyan-400");
|
|
2901
|
-
}
|
|
2902
|
-
}
|
|
2903
|
-
}
|
|
2904
|
-
)
|
|
2905
|
-
}
|
|
2906
|
-
);
|
|
2907
|
-
}
|
|
2908
|
-
function SearchSourcesBadge({ sources }) {
|
|
2909
|
-
const [expanded, setExpanded] = useState(false);
|
|
2910
|
-
if (!sources.length) return null;
|
|
2911
|
-
const uniqueDomains = [...new Set(sources.map((s) => getDomain(s.url)))];
|
|
2912
|
-
return /* @__PURE__ */ jsxs("div", { className: "mt-2 w-full", children: [
|
|
2913
|
-
/* @__PURE__ */ jsxs(
|
|
2914
|
-
"button",
|
|
2915
|
-
{
|
|
2916
|
-
onClick: () => setExpanded(!expanded),
|
|
2917
|
-
className: cn(
|
|
2918
|
-
"inline-flex items-center gap-1.5 px-3 py-1.5 rounded-full text-xs",
|
|
2919
|
-
"bg-cyan-500/10 hover:bg-cyan-500/20 border border-cyan-500/30",
|
|
2920
|
-
"text-cyan-300 transition-colors"
|
|
2921
|
-
),
|
|
2922
|
-
children: [
|
|
2923
|
-
/* @__PURE__ */ jsx(HugeiconsIcon, { icon: Search01Icon, size: 14 }),
|
|
2924
|
-
/* @__PURE__ */ jsx("span", { className: "font-medium", children: "Sources" }),
|
|
2925
|
-
/* @__PURE__ */ jsx("span", { className: "px-1.5 py-0.5 rounded-full bg-cyan-500/20 text-cyan-400 font-semibold", children: sources.length }),
|
|
2926
|
-
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-0.5 ml-1", children: [
|
|
2927
|
-
uniqueDomains.slice(0, 3).map((domain) => /* @__PURE__ */ jsx(FaviconCircle, { domain }, domain)),
|
|
2928
|
-
uniqueDomains.length > 3 && /* @__PURE__ */ jsxs("span", { className: "text-cyan-500/70 text-[10px] ml-0.5", children: [
|
|
2929
|
-
"+",
|
|
2930
|
-
uniqueDomains.length - 3
|
|
2931
|
-
] })
|
|
2932
|
-
] }),
|
|
2933
|
-
/* @__PURE__ */ jsx(
|
|
2934
|
-
HugeiconsIcon,
|
|
2935
|
-
{
|
|
2936
|
-
icon: ArrowRight01Icon,
|
|
2937
|
-
size: 12,
|
|
2938
|
-
className: cn("transition-transform", expanded && "rotate-90")
|
|
2939
|
-
}
|
|
2940
|
-
)
|
|
2941
|
-
]
|
|
2942
|
-
}
|
|
2943
|
-
),
|
|
2944
|
-
expanded && /* @__PURE__ */ jsx("div", { className: "mt-2 rounded-lg border border-cyan-500/20 bg-cyan-500/5 max-h-80 overflow-y-auto", children: sources.map((source, i) => {
|
|
2945
|
-
const domain = getDomain(source.url);
|
|
2946
|
-
return /* @__PURE__ */ jsxs(
|
|
2947
|
-
"a",
|
|
2948
|
-
{
|
|
2949
|
-
href: source.url,
|
|
2950
|
-
target: "_blank",
|
|
2951
|
-
rel: "noopener noreferrer",
|
|
2952
|
-
className: "flex items-start gap-2 p-2.5 border-b border-cyan-500/10 last:border-b-0 hover:bg-cyan-500/5 transition-colors",
|
|
2953
|
-
children: [
|
|
2954
|
-
/* @__PURE__ */ jsx(FaviconCircle, { domain }),
|
|
2955
|
-
/* @__PURE__ */ jsxs("div", { className: "flex-1 min-w-0", children: [
|
|
2956
|
-
/* @__PURE__ */ jsx("div", { className: "text-sm font-medium text-primary-900 hover:underline line-clamp-1", children: source.title || domain }),
|
|
2957
|
-
/* @__PURE__ */ jsx("div", { className: "text-xs text-muted-foreground", children: domain }),
|
|
2958
|
-
source.snippet && /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground/80 line-clamp-2 mt-0.5", children: source.snippet })
|
|
2959
|
-
] })
|
|
2960
|
-
]
|
|
2961
|
-
},
|
|
2962
|
-
`${source.url}-${i}`
|
|
2963
|
-
);
|
|
2964
|
-
}) })
|
|
2965
|
-
] });
|
|
2966
|
-
}
|
|
2830
|
+
const Thinking = lazy(
|
|
2831
|
+
() => import("./thinking-BpAc3itF.js").then((m) => ({
|
|
2832
|
+
default: m.Thinking
|
|
2833
|
+
}))
|
|
2834
|
+
);
|
|
2835
|
+
const SearchSourcesBadge = lazy(
|
|
2836
|
+
() => import("./search-sources-badge-B0rAEDs_.js").then((m) => ({
|
|
2837
|
+
default: m.SearchSourcesBadge
|
|
2838
|
+
}))
|
|
2839
|
+
);
|
|
2967
2840
|
function mapToolCallToToolPart(toolCall, resultMessage) {
|
|
2968
2841
|
const hasResult = resultMessage !== void 0;
|
|
2969
2842
|
const isError = resultMessage?.isError ?? false;
|
|
@@ -3145,10 +3018,19 @@ function MessageItemComponent({
|
|
|
3145
3018
|
const thinking = thinkingFromMessage(message);
|
|
3146
3019
|
const images = imagesFromMessage(message);
|
|
3147
3020
|
const isUser = role === "user";
|
|
3021
|
+
const imageDataUris = useMemo(
|
|
3022
|
+
() => images.map(
|
|
3023
|
+
(img) => `data:${img.source.media_type};base64,${img.source.data}`
|
|
3024
|
+
),
|
|
3025
|
+
[images]
|
|
3026
|
+
);
|
|
3148
3027
|
const timestamp = getMessageTimestamp(message);
|
|
3149
3028
|
const navigate = useNavigate();
|
|
3150
3029
|
const openInEditor = useFileExplorerState((state) => state.openInEditor);
|
|
3151
|
-
const uploadedFileRefs = useMemo(
|
|
3030
|
+
const uploadedFileRefs = useMemo(
|
|
3031
|
+
() => parseUploadedFileReferences(text),
|
|
3032
|
+
[text]
|
|
3033
|
+
);
|
|
3152
3034
|
const [fileSizes, setFileSizes] = useState({});
|
|
3153
3035
|
const displayText = useMemo(() => stripUploadedFileLines(text), [text]);
|
|
3154
3036
|
useEffect(() => {
|
|
@@ -3158,7 +3040,9 @@ function MessageItemComponent({
|
|
|
3158
3040
|
await Promise.all(
|
|
3159
3041
|
uploadedFileRefs.map(async (ref) => {
|
|
3160
3042
|
try {
|
|
3161
|
-
const response = await fetch(
|
|
3043
|
+
const response = await fetch(
|
|
3044
|
+
`/api/files/info?path=${encodeURIComponent(ref.path)}`
|
|
3045
|
+
);
|
|
3162
3046
|
if (!response.ok) {
|
|
3163
3047
|
nextSizes[ref.path] = null;
|
|
3164
3048
|
return;
|
|
@@ -3208,51 +3092,81 @@ function MessageItemComponent({
|
|
|
3208
3092
|
isUser ? "items-end" : "items-start"
|
|
3209
3093
|
),
|
|
3210
3094
|
children: [
|
|
3211
|
-
thinking && settings.showReasoningBlocks && /* @__PURE__ */ jsx("div", { className: "w-full max-w-[var(--opencami-chat-width)]", children: /* @__PURE__ */ jsx(Thinking, { content: thinking }) }),
|
|
3212
|
-
images.length > 0 && /* @__PURE__ */ jsx(
|
|
3213
|
-
"
|
|
3214
|
-
isUser ? "justify-end" : "justify-start"
|
|
3215
|
-
), children: images.map((img, idx) => /* @__PURE__ */ jsx(
|
|
3216
|
-
"img",
|
|
3217
|
-
{
|
|
3218
|
-
src: `data:${img.source.media_type};base64,${img.source.data}`,
|
|
3219
|
-
alt: `Attachment ${idx + 1}`,
|
|
3220
|
-
loading: "lazy",
|
|
3221
|
-
decoding: "async",
|
|
3222
|
-
className: "max-w-[300px] max-h-[300px] rounded-lg object-cover"
|
|
3223
|
-
},
|
|
3224
|
-
idx
|
|
3225
|
-
)) }),
|
|
3226
|
-
uploadedFileRefs.length > 0 && /* @__PURE__ */ jsx("div", { className: cn("mb-2 flex w-full flex-col gap-2", isUser ? "items-end" : "items-start"), children: uploadedFileRefs.map((fileRef) => /* @__PURE__ */ jsxs(
|
|
3227
|
-
"button",
|
|
3095
|
+
thinking && settings.showReasoningBlocks && /* @__PURE__ */ jsx("div", { className: "w-full max-w-[var(--opencami-chat-width)]", children: /* @__PURE__ */ jsx(Suspense, { fallback: null, children: /* @__PURE__ */ jsx(Thinking, { content: thinking }) }) }),
|
|
3096
|
+
images.length > 0 && /* @__PURE__ */ jsx(
|
|
3097
|
+
"div",
|
|
3228
3098
|
{
|
|
3229
|
-
|
|
3230
|
-
|
|
3231
|
-
|
|
3232
|
-
|
|
3233
|
-
|
|
3234
|
-
|
|
3235
|
-
|
|
3236
|
-
|
|
3237
|
-
|
|
3238
|
-
|
|
3239
|
-
|
|
3240
|
-
|
|
3241
|
-
|
|
3242
|
-
|
|
3243
|
-
|
|
3244
|
-
|
|
3245
|
-
|
|
3099
|
+
className: cn(
|
|
3100
|
+
"flex flex-wrap gap-2 mb-2",
|
|
3101
|
+
isUser ? "justify-end" : "justify-start"
|
|
3102
|
+
),
|
|
3103
|
+
children: images.map((img, idx) => /* @__PURE__ */ jsx(
|
|
3104
|
+
"img",
|
|
3105
|
+
{
|
|
3106
|
+
src: imageDataUris[idx],
|
|
3107
|
+
alt: `Attachment ${idx + 1}`,
|
|
3108
|
+
loading: "lazy",
|
|
3109
|
+
decoding: "async",
|
|
3110
|
+
fetchPriority: "low",
|
|
3111
|
+
className: "max-w-[300px] max-h-[300px] rounded-lg object-cover"
|
|
3112
|
+
},
|
|
3113
|
+
idx
|
|
3114
|
+
))
|
|
3115
|
+
}
|
|
3116
|
+
),
|
|
3117
|
+
uploadedFileRefs.length > 0 && /* @__PURE__ */ jsx(
|
|
3118
|
+
"div",
|
|
3246
3119
|
{
|
|
3247
|
-
markdown: !isUser,
|
|
3248
3120
|
className: cn(
|
|
3249
|
-
"
|
|
3250
|
-
isUser ? "
|
|
3251
|
-
!isUser && isStreaming && "stream-fade-in"
|
|
3121
|
+
"mb-2 flex w-full flex-col gap-2",
|
|
3122
|
+
isUser ? "items-end" : "items-start"
|
|
3252
3123
|
),
|
|
3253
|
-
children:
|
|
3124
|
+
children: uploadedFileRefs.map((fileRef) => /* @__PURE__ */ jsxs(
|
|
3125
|
+
"button",
|
|
3126
|
+
{
|
|
3127
|
+
type: "button",
|
|
3128
|
+
onClick: () => {
|
|
3129
|
+
void handleOpenFile(fileRef.path);
|
|
3130
|
+
},
|
|
3131
|
+
className: "flex max-w-full items-center gap-3 rounded-xl border border-primary-200 bg-primary-50 px-3 py-2 text-left hover:bg-primary-100",
|
|
3132
|
+
children: [
|
|
3133
|
+
/* @__PURE__ */ jsx("div", { className: "flex h-8 w-8 items-center justify-center rounded-lg bg-primary-100", children: /* @__PURE__ */ jsx(
|
|
3134
|
+
HugeiconsIcon,
|
|
3135
|
+
{
|
|
3136
|
+
icon: File01Icon,
|
|
3137
|
+
size: 18,
|
|
3138
|
+
className: "text-primary-600"
|
|
3139
|
+
}
|
|
3140
|
+
) }),
|
|
3141
|
+
/* @__PURE__ */ jsxs("div", { className: "min-w-0", children: [
|
|
3142
|
+
/* @__PURE__ */ jsx("p", { className: "truncate text-sm font-medium text-primary-900", children: fileRef.filename }),
|
|
3143
|
+
/* @__PURE__ */ jsx("p", { className: "text-xs text-primary-600", children: formatFileSize$1(fileSizes[fileRef.path] ?? null) })
|
|
3144
|
+
] })
|
|
3145
|
+
]
|
|
3146
|
+
},
|
|
3147
|
+
fileRef.path
|
|
3148
|
+
))
|
|
3254
3149
|
}
|
|
3255
|
-
)
|
|
3150
|
+
),
|
|
3151
|
+
/* @__PURE__ */ jsx(
|
|
3152
|
+
Message,
|
|
3153
|
+
{
|
|
3154
|
+
className: cn("min-w-0 max-w-full", isUser ? "flex-row-reverse" : ""),
|
|
3155
|
+
children: /* @__PURE__ */ jsx(
|
|
3156
|
+
MessageContent,
|
|
3157
|
+
{
|
|
3158
|
+
markdown: !isUser,
|
|
3159
|
+
isStreaming: !isUser && isStreaming,
|
|
3160
|
+
className: cn(
|
|
3161
|
+
"text-primary-900 opencami-text-size min-w-0 max-w-full",
|
|
3162
|
+
isUser ? "opencami-message-user bg-primary-100 px-4 py-[var(--opencami-user-bubble-py)] max-w-[85%]" : "opencami-message-assistant bg-transparent w-full",
|
|
3163
|
+
!isUser && isStreaming && "stream-fade-in"
|
|
3164
|
+
),
|
|
3165
|
+
children: displayText
|
|
3166
|
+
}
|
|
3167
|
+
)
|
|
3168
|
+
}
|
|
3169
|
+
),
|
|
3256
3170
|
hasToolCalls && settings.showToolMessages && /* @__PURE__ */ jsx("div", { className: "mt-2 flex w-full min-w-0 max-w-[var(--opencami-chat-width)] flex-col gap-3 overflow-x-hidden", children: toolCalls.map((toolCall) => {
|
|
3257
3171
|
const resultMessage = toolCall.id ? toolResultsByCallId?.get(toolCall.id) : void 0;
|
|
3258
3172
|
const toolPart = mapToolCallToToolPart(toolCall, resultMessage);
|
|
@@ -3265,7 +3179,7 @@ function MessageItemComponent({
|
|
|
3265
3179
|
toolCall.id || toolCall.name
|
|
3266
3180
|
);
|
|
3267
3181
|
}) }),
|
|
3268
|
-
searchSources.length > 0 && /* @__PURE__ */ jsx("div", { className: "w-full max-w-[var(--opencami-chat-width)]", children: /* @__PURE__ */ jsx(SearchSourcesBadge, { sources: searchSources }) }),
|
|
3182
|
+
searchSources.length > 0 && /* @__PURE__ */ jsx("div", { className: "w-full max-w-[var(--opencami-chat-width)]", children: /* @__PURE__ */ jsx(Suspense, { fallback: null, children: /* @__PURE__ */ jsx(SearchSourcesBadge, { sources: searchSources }) }) }),
|
|
3269
3183
|
!hasToolCalls && /* @__PURE__ */ jsx(
|
|
3270
3184
|
MessageActionsBar,
|
|
3271
3185
|
{
|
|
@@ -3285,7 +3199,8 @@ function areMessagesEqual(prevProps, nextProps) {
|
|
|
3285
3199
|
}
|
|
3286
3200
|
if (prevProps.isStreaming !== nextProps.isStreaming) return false;
|
|
3287
3201
|
if (prevProps.isLastAssistant !== nextProps.isLastAssistant) return false;
|
|
3288
|
-
if (prevProps.aggregatedSearchSources !== nextProps.aggregatedSearchSources)
|
|
3202
|
+
if (prevProps.aggregatedSearchSources !== nextProps.aggregatedSearchSources)
|
|
3203
|
+
return false;
|
|
3289
3204
|
if (prevProps.wrapperClassName !== nextProps.wrapperClassName) return false;
|
|
3290
3205
|
if (prevProps.wrapperRef !== nextProps.wrapperRef) return false;
|
|
3291
3206
|
if (prevProps.wrapperScrollMarginTop !== nextProps.wrapperScrollMarginTop) {
|
|
@@ -3314,581 +3229,102 @@ function areMessagesEqual(prevProps, nextProps) {
|
|
|
3314
3229
|
return true;
|
|
3315
3230
|
}
|
|
3316
3231
|
const MemoizedMessageItem = memo(MessageItemComponent, areMessagesEqual);
|
|
3317
|
-
|
|
3318
|
-
|
|
3319
|
-
|
|
3320
|
-
|
|
3321
|
-
|
|
3322
|
-
|
|
3323
|
-
const
|
|
3324
|
-
const
|
|
3325
|
-
|
|
3326
|
-
|
|
3327
|
-
|
|
3328
|
-
|
|
3329
|
-
|
|
3330
|
-
|
|
3331
|
-
|
|
3332
|
-
);
|
|
3333
|
-
}
|
|
3334
|
-
|
|
3335
|
-
|
|
3336
|
-
|
|
3337
|
-
|
|
3338
|
-
|
|
3339
|
-
|
|
3340
|
-
|
|
3341
|
-
|
|
3342
|
-
|
|
3343
|
-
|
|
3344
|
-
|
|
3345
|
-
}
|
|
3346
|
-
|
|
3347
|
-
return /\b(alternatively|another|other option|could also|you might|consider)\b/i.test(text);
|
|
3348
|
-
}
|
|
3349
|
-
function isExplanatory(text) {
|
|
3350
|
-
return /\b(because|since|due to|reason|why|how|what|when|where)\b/i.test(text);
|
|
3351
|
-
}
|
|
3352
|
-
function hasCaveats(text) {
|
|
3353
|
-
return /\b(however|but|note|warning|caution|careful|important|keep in mind|be aware)\b/i.test(text);
|
|
3354
|
-
}
|
|
3355
|
-
function generateHeuristicFollowUps(responseText) {
|
|
3356
|
-
const suggestions = [];
|
|
3357
|
-
const topics = extractTopics(responseText);
|
|
3358
|
-
if (hasCode(responseText)) {
|
|
3359
|
-
suggestions.push({
|
|
3360
|
-
text: "Can you explain this code step by step?",
|
|
3361
|
-
type: "clarify"
|
|
3362
|
-
});
|
|
3363
|
-
suggestions.push({
|
|
3364
|
-
text: "How would I modify this for my use case?",
|
|
3365
|
-
type: "actionable"
|
|
3366
|
-
});
|
|
3367
|
-
}
|
|
3368
|
-
if (hasList(responseText)) {
|
|
3369
|
-
suggestions.push({
|
|
3370
|
-
text: "Can you elaborate on the first point?",
|
|
3371
|
-
type: "expand"
|
|
3372
|
-
});
|
|
3373
|
-
suggestions.push({
|
|
3374
|
-
text: "Which of these is most important to start with?",
|
|
3375
|
-
type: "actionable"
|
|
3376
|
-
});
|
|
3377
|
-
}
|
|
3378
|
-
if (hasAlternatives(responseText)) {
|
|
3379
|
-
suggestions.push({
|
|
3380
|
-
text: "What are the pros and cons of each approach?",
|
|
3381
|
-
type: "alternative"
|
|
3382
|
-
});
|
|
3383
|
-
suggestions.push({
|
|
3384
|
-
text: "Which option would you recommend and why?",
|
|
3385
|
-
type: "clarify"
|
|
3386
|
-
});
|
|
3387
|
-
}
|
|
3388
|
-
if (isExplanatory(responseText) && suggestions.length < 3) {
|
|
3389
|
-
suggestions.push({
|
|
3390
|
-
text: "Can you give me a practical example?",
|
|
3391
|
-
type: "example"
|
|
3392
|
-
});
|
|
3393
|
-
}
|
|
3394
|
-
if (hasCaveats(responseText) && suggestions.length < 3) {
|
|
3395
|
-
suggestions.push({
|
|
3396
|
-
text: "What potential issues should I watch out for?",
|
|
3397
|
-
type: "clarify"
|
|
3398
|
-
});
|
|
3399
|
-
}
|
|
3400
|
-
if (topics.length > 0 && suggestions.length < 3) {
|
|
3401
|
-
const topic = topics[0];
|
|
3402
|
-
suggestions.push({
|
|
3403
|
-
text: `Tell me more about ${topic}`,
|
|
3404
|
-
type: "expand"
|
|
3232
|
+
function ScrollButton({
|
|
3233
|
+
className,
|
|
3234
|
+
variant = "outline",
|
|
3235
|
+
scrollRef,
|
|
3236
|
+
...props
|
|
3237
|
+
}) {
|
|
3238
|
+
const [isAtBottom, setIsAtBottom] = useState(true);
|
|
3239
|
+
const [showButton, setShowButton] = useState(false);
|
|
3240
|
+
const lastScrollTopRef = useRef(0);
|
|
3241
|
+
const checkIsAtBottom = useCallback(() => {
|
|
3242
|
+
const element = scrollRef.current;
|
|
3243
|
+
if (!element) return;
|
|
3244
|
+
const isBottom = Math.abs(
|
|
3245
|
+
element.scrollHeight - element.scrollTop - element.clientHeight
|
|
3246
|
+
) < 100;
|
|
3247
|
+
setIsAtBottom(isBottom);
|
|
3248
|
+
}, [scrollRef]);
|
|
3249
|
+
useLayoutEffect(() => {
|
|
3250
|
+
const element = scrollRef.current;
|
|
3251
|
+
if (!element) return;
|
|
3252
|
+
const handleScroll = () => {
|
|
3253
|
+
lastScrollTopRef.current = element.scrollTop;
|
|
3254
|
+
checkIsAtBottom();
|
|
3255
|
+
};
|
|
3256
|
+
const observer = new MutationObserver(() => {
|
|
3257
|
+
if (!element) return;
|
|
3258
|
+
if (element.scrollTop !== lastScrollTopRef.current) {
|
|
3259
|
+
lastScrollTopRef.current = element.scrollTop;
|
|
3260
|
+
}
|
|
3261
|
+
checkIsAtBottom();
|
|
3405
3262
|
});
|
|
3406
|
-
|
|
3407
|
-
|
|
3408
|
-
{
|
|
3409
|
-
|
|
3410
|
-
|
|
3411
|
-
|
|
3412
|
-
|
|
3413
|
-
];
|
|
3414
|
-
|
|
3415
|
-
|
|
3416
|
-
|
|
3417
|
-
|
|
3418
|
-
suggestions.push(fallback);
|
|
3419
|
-
usedTypes.add(fallback.type);
|
|
3263
|
+
checkIsAtBottom();
|
|
3264
|
+
element.addEventListener("scroll", handleScroll);
|
|
3265
|
+
observer.observe(element, { childList: true, subtree: true });
|
|
3266
|
+
return () => {
|
|
3267
|
+
element.removeEventListener("scroll", handleScroll);
|
|
3268
|
+
observer.disconnect();
|
|
3269
|
+
};
|
|
3270
|
+
}, [checkIsAtBottom, scrollRef]);
|
|
3271
|
+
useLayoutEffect(() => {
|
|
3272
|
+
if (isAtBottom) {
|
|
3273
|
+
setShowButton(false);
|
|
3274
|
+
return;
|
|
3420
3275
|
}
|
|
3421
|
-
|
|
3422
|
-
|
|
3423
|
-
|
|
3424
|
-
|
|
3425
|
-
|
|
3276
|
+
const timer = window.setTimeout(() => {
|
|
3277
|
+
setShowButton(true);
|
|
3278
|
+
}, 200);
|
|
3279
|
+
return () => window.clearTimeout(timer);
|
|
3280
|
+
}, [isAtBottom]);
|
|
3281
|
+
return /* @__PURE__ */ jsx(
|
|
3282
|
+
Button,
|
|
3283
|
+
{
|
|
3284
|
+
variant: "secondary",
|
|
3285
|
+
size: "icon-sm",
|
|
3286
|
+
className: cn(
|
|
3287
|
+
"pointer-events-auto rounded-full shadow-md",
|
|
3288
|
+
"transition-all duration-100 ease-in-out",
|
|
3289
|
+
!isAtBottom && showButton ? "translate-y-0 scale-100 opacity-100" : "pointer-events-none translate-y-4 scale-98 opacity-0",
|
|
3290
|
+
className
|
|
3291
|
+
),
|
|
3292
|
+
onClick: () => {
|
|
3293
|
+
const element = scrollRef.current;
|
|
3294
|
+
if (!element) return;
|
|
3295
|
+
element.scrollTop = element.scrollHeight;
|
|
3296
|
+
setIsAtBottom(true);
|
|
3297
|
+
},
|
|
3298
|
+
...props,
|
|
3299
|
+
children: /* @__PURE__ */ jsx(HugeiconsIcon, { icon: ArrowDown01Icon, size: 18, strokeWidth: 1.8 })
|
|
3426
3300
|
}
|
|
3427
|
-
|
|
3428
|
-
return suggestions.slice(0, 3);
|
|
3301
|
+
);
|
|
3429
3302
|
}
|
|
3430
|
-
function
|
|
3431
|
-
|
|
3432
|
-
|
|
3433
|
-
|
|
3434
|
-
|
|
3435
|
-
|
|
3436
|
-
|
|
3437
|
-
|
|
3438
|
-
|
|
3439
|
-
|
|
3440
|
-
|
|
3441
|
-
|
|
3442
|
-
|
|
3443
|
-
|
|
3444
|
-
|
|
3445
|
-
|
|
3446
|
-
|
|
3447
|
-
|
|
3448
|
-
|
|
3449
|
-
|
|
3450
|
-
|
|
3451
|
-
|
|
3452
|
-
|
|
3453
|
-
|
|
3454
|
-
|
|
3455
|
-
const DEFAULT_LLM_SETTINGS = {
|
|
3456
|
-
useLlmTitles: true,
|
|
3457
|
-
useLlmFollowUps: true,
|
|
3458
|
-
llmProvider: "openai",
|
|
3459
|
-
llmBaseUrl: "",
|
|
3460
|
-
llmModel: "",
|
|
3461
|
-
llmApiKey: ""
|
|
3462
|
-
};
|
|
3463
|
-
function getLlmProviderDefaults(provider) {
|
|
3464
|
-
return LLM_PROVIDER_DEFAULTS[provider];
|
|
3465
|
-
}
|
|
3466
|
-
function getEffectiveLlmBaseUrl(settings) {
|
|
3467
|
-
if (settings.llmBaseUrl.trim()) return settings.llmBaseUrl.trim();
|
|
3468
|
-
return getLlmProviderDefaults(settings.llmProvider).baseUrl;
|
|
3469
|
-
}
|
|
3470
|
-
function getEffectiveLlmModel(settings) {
|
|
3471
|
-
if (settings.llmModel.trim()) return settings.llmModel.trim();
|
|
3472
|
-
return getLlmProviderDefaults(settings.llmProvider).model;
|
|
3473
|
-
}
|
|
3474
|
-
function getAvailability(settings, hasEnvKey) {
|
|
3475
|
-
if (settings.llmProvider === "ollama") return true;
|
|
3476
|
-
if (settings.llmProvider === "custom") {
|
|
3477
|
-
return Boolean(settings.llmApiKey.trim()) || Boolean(settings.llmBaseUrl.trim() && settings.llmModel.trim());
|
|
3478
|
-
}
|
|
3479
|
-
return hasEnvKey || Boolean(settings.llmApiKey.trim());
|
|
3480
|
-
}
|
|
3481
|
-
function migratePersistedState(persistedState) {
|
|
3482
|
-
if (!persistedState || typeof persistedState !== "object") {
|
|
3483
|
-
return { settings: DEFAULT_LLM_SETTINGS };
|
|
3484
|
-
}
|
|
3485
|
-
const { settings } = persistedState;
|
|
3486
|
-
if (!settings) {
|
|
3487
|
-
return { settings: DEFAULT_LLM_SETTINGS };
|
|
3488
|
-
}
|
|
3489
|
-
const { openaiApiKey, ...rest } = settings;
|
|
3490
|
-
const llmApiKey = rest.llmApiKey ?? openaiApiKey ?? "";
|
|
3491
|
-
return {
|
|
3492
|
-
settings: {
|
|
3493
|
-
...DEFAULT_LLM_SETTINGS,
|
|
3494
|
-
...rest,
|
|
3495
|
-
llmApiKey
|
|
3496
|
-
}
|
|
3497
|
-
};
|
|
3498
|
-
}
|
|
3499
|
-
const useLlmSettingsStore = create()(
|
|
3500
|
-
persist(
|
|
3501
|
-
(set) => ({
|
|
3502
|
-
settings: {
|
|
3503
|
-
...DEFAULT_LLM_SETTINGS
|
|
3504
|
-
},
|
|
3505
|
-
updateSettings: (updates) => set((state) => ({
|
|
3506
|
-
settings: { ...state.settings, ...updates }
|
|
3507
|
-
})),
|
|
3508
|
-
clearApiKey: () => set((state) => ({
|
|
3509
|
-
settings: { ...state.settings, llmApiKey: "" }
|
|
3510
|
-
}))
|
|
3511
|
-
}),
|
|
3512
|
-
{
|
|
3513
|
-
name: "llm-settings",
|
|
3514
|
-
version: 2,
|
|
3515
|
-
migrate: (persistedState) => migratePersistedState(persistedState)
|
|
3516
|
-
}
|
|
3517
|
-
)
|
|
3518
|
-
);
|
|
3519
|
-
function useLlmSettings() {
|
|
3520
|
-
const settings = useLlmSettingsStore((state) => state.settings);
|
|
3521
|
-
const updateSettings = useLlmSettingsStore((state) => state.updateSettings);
|
|
3522
|
-
const clearApiKey = useLlmSettingsStore((state) => state.clearApiKey);
|
|
3523
|
-
const [status, setStatus] = useState({
|
|
3524
|
-
hasEnvKey: false,
|
|
3525
|
-
hasOpenRouterKey: false,
|
|
3526
|
-
hasKilocodeKey: false,
|
|
3527
|
-
hasUserKey: Boolean(settings.llmApiKey),
|
|
3528
|
-
isAvailable: getAvailability(settings, false),
|
|
3529
|
-
isLoading: true,
|
|
3530
|
-
error: null
|
|
3531
|
-
});
|
|
3532
|
-
useEffect(() => {
|
|
3533
|
-
let cancelled = false;
|
|
3534
|
-
async function checkStatus() {
|
|
3535
|
-
try {
|
|
3536
|
-
const res = await fetch("/api/llm-features");
|
|
3537
|
-
if (!res.ok) throw new Error("Failed to check LLM status");
|
|
3538
|
-
const data = await res.json();
|
|
3539
|
-
if (cancelled) return;
|
|
3540
|
-
const hasUserKey = Boolean(settings.llmApiKey);
|
|
3541
|
-
const hasProviderKey = settings.llmProvider === "openrouter" ? Boolean(data.hasOpenRouterKey) : settings.llmProvider === "kilocode" ? Boolean(data.hasKilocodeKey) : data.hasEnvKey;
|
|
3542
|
-
setStatus({
|
|
3543
|
-
hasEnvKey: data.hasEnvKey,
|
|
3544
|
-
hasOpenRouterKey: Boolean(data.hasOpenRouterKey),
|
|
3545
|
-
hasKilocodeKey: Boolean(data.hasKilocodeKey),
|
|
3546
|
-
hasUserKey,
|
|
3547
|
-
isAvailable: getAvailability(settings, hasProviderKey),
|
|
3548
|
-
isLoading: false,
|
|
3549
|
-
error: null
|
|
3550
|
-
});
|
|
3551
|
-
} catch (err) {
|
|
3552
|
-
if (cancelled) return;
|
|
3553
|
-
setStatus((prev) => ({
|
|
3554
|
-
...prev,
|
|
3555
|
-
isLoading: false,
|
|
3556
|
-
error: err instanceof Error ? err.message : "Failed to check status"
|
|
3557
|
-
}));
|
|
3558
|
-
}
|
|
3559
|
-
}
|
|
3560
|
-
void checkStatus();
|
|
3561
|
-
return () => {
|
|
3562
|
-
cancelled = true;
|
|
3563
|
-
};
|
|
3564
|
-
}, [
|
|
3565
|
-
settings.llmApiKey,
|
|
3566
|
-
settings.llmProvider,
|
|
3567
|
-
settings.llmBaseUrl,
|
|
3568
|
-
settings.llmModel
|
|
3569
|
-
]);
|
|
3570
|
-
const testApiKey = useCallback(async (key) => {
|
|
3571
|
-
try {
|
|
3572
|
-
const headers = buildLlmHeaders({
|
|
3573
|
-
...settings,
|
|
3574
|
-
llmApiKey: key
|
|
3575
|
-
});
|
|
3576
|
-
const res = await fetch("/api/llm-features", {
|
|
3577
|
-
method: "POST",
|
|
3578
|
-
headers: {
|
|
3579
|
-
"Content-Type": "application/json",
|
|
3580
|
-
...headers
|
|
3581
|
-
},
|
|
3582
|
-
body: JSON.stringify({ action: "test" })
|
|
3583
|
-
});
|
|
3584
|
-
const data = await res.json();
|
|
3585
|
-
if (!data.ok) {
|
|
3586
|
-
return { valid: false, error: data.error || "Test failed" };
|
|
3587
|
-
}
|
|
3588
|
-
return { valid: data.valid ?? false, error: data.error };
|
|
3589
|
-
} catch (err) {
|
|
3590
|
-
return {
|
|
3591
|
-
valid: false,
|
|
3592
|
-
error: err instanceof Error ? err.message : "Network error"
|
|
3593
|
-
};
|
|
3594
|
-
}
|
|
3595
|
-
}, [settings]);
|
|
3596
|
-
return {
|
|
3597
|
-
settings,
|
|
3598
|
-
updateSettings,
|
|
3599
|
-
clearApiKey,
|
|
3600
|
-
status,
|
|
3601
|
-
testApiKey
|
|
3602
|
-
};
|
|
3603
|
-
}
|
|
3604
|
-
function buildLlmHeaders(settings) {
|
|
3605
|
-
const apiKey = settings.llmApiKey;
|
|
3606
|
-
const baseUrl = getEffectiveLlmBaseUrl(settings);
|
|
3607
|
-
const model = getEffectiveLlmModel(settings);
|
|
3608
|
-
if (apiKey) {
|
|
3609
|
-
return {
|
|
3610
|
-
"X-OpenAI-API-Key": apiKey,
|
|
3611
|
-
...baseUrl ? { "X-LLM-Base-URL": baseUrl } : {},
|
|
3612
|
-
...model ? { "X-LLM-Model": model } : {}
|
|
3613
|
-
};
|
|
3614
|
-
}
|
|
3615
|
-
return {
|
|
3616
|
-
...baseUrl ? { "X-LLM-Base-URL": baseUrl } : {},
|
|
3617
|
-
...model ? { "X-LLM-Model": model } : {}
|
|
3618
|
-
};
|
|
3619
|
-
}
|
|
3620
|
-
function getLlmHeaders() {
|
|
3621
|
-
const settings = useLlmSettingsStore.getState().settings;
|
|
3622
|
-
return buildLlmHeaders(settings);
|
|
3623
|
-
}
|
|
3624
|
-
async function fetchLlmFollowUps(conversationContext, signal) {
|
|
3625
|
-
const headers = {
|
|
3626
|
-
"Content-Type": "application/json",
|
|
3627
|
-
...getLlmHeaders()
|
|
3628
|
-
};
|
|
3629
|
-
const res = await fetch("/api/llm-features", {
|
|
3630
|
-
method: "POST",
|
|
3631
|
-
headers,
|
|
3632
|
-
body: JSON.stringify({
|
|
3633
|
-
action: "followups",
|
|
3634
|
-
conversationContext
|
|
3635
|
-
}),
|
|
3636
|
-
signal
|
|
3637
|
-
});
|
|
3638
|
-
if (!res.ok) {
|
|
3639
|
-
throw new Error(`API error: ${res.status}`);
|
|
3640
|
-
}
|
|
3641
|
-
const data = await res.json();
|
|
3642
|
-
if (data.ok && Array.isArray(data.suggestions) && data.suggestions.length > 0) {
|
|
3643
|
-
return data.suggestions;
|
|
3644
|
-
}
|
|
3645
|
-
return [];
|
|
3646
|
-
}
|
|
3647
|
-
function useFollowUpSuggestions(responseText, contextSummary, options) {
|
|
3648
|
-
const llmSettings = useLlmSettingsStore((state) => state.settings);
|
|
3649
|
-
const useLlmFollowUps = llmSettings.useLlmFollowUps;
|
|
3650
|
-
const {
|
|
3651
|
-
minResponseLength = 50,
|
|
3652
|
-
timeoutMs = 8e3,
|
|
3653
|
-
// Use heuristics only if explicitly set OR if LLM follow-ups are disabled
|
|
3654
|
-
heuristicsOnly: forceHeuristicsOnly
|
|
3655
|
-
} = options ?? {};
|
|
3656
|
-
const heuristicsOnly = forceHeuristicsOnly ?? !useLlmFollowUps;
|
|
3657
|
-
const [suggestions, setSuggestions] = useState([]);
|
|
3658
|
-
const [isLoading, setIsLoading] = useState(false);
|
|
3659
|
-
const [error, setError] = useState(null);
|
|
3660
|
-
const [source, setSource] = useState(null);
|
|
3661
|
-
const lastResponseRef = useRef("");
|
|
3662
|
-
const abortControllerRef = useRef(null);
|
|
3663
|
-
useEffect(() => {
|
|
3664
|
-
if (!responseText || responseText.trim().length < minResponseLength) {
|
|
3665
|
-
setSuggestions([]);
|
|
3666
|
-
setSource(null);
|
|
3667
|
-
setIsLoading(false);
|
|
3668
|
-
setError(null);
|
|
3669
|
-
return;
|
|
3670
|
-
}
|
|
3671
|
-
const responseKey = responseText.slice(0, 200) + responseText.length;
|
|
3672
|
-
if (responseKey === lastResponseRef.current) {
|
|
3673
|
-
return;
|
|
3674
|
-
}
|
|
3675
|
-
lastResponseRef.current = responseKey;
|
|
3676
|
-
if (abortControllerRef.current) {
|
|
3677
|
-
abortControllerRef.current.abort();
|
|
3678
|
-
}
|
|
3679
|
-
if (heuristicsOnly) {
|
|
3680
|
-
const heuristicSuggestions2 = getHeuristicFollowUpTexts(responseText);
|
|
3681
|
-
setSuggestions(heuristicSuggestions2);
|
|
3682
|
-
setSource("heuristic");
|
|
3683
|
-
setIsLoading(false);
|
|
3684
|
-
setError(null);
|
|
3685
|
-
return;
|
|
3686
|
-
}
|
|
3687
|
-
const controller = new AbortController();
|
|
3688
|
-
abortControllerRef.current = controller;
|
|
3689
|
-
setIsLoading(true);
|
|
3690
|
-
setError(null);
|
|
3691
|
-
const heuristicSuggestions = getHeuristicFollowUpTexts(responseText);
|
|
3692
|
-
setSuggestions(heuristicSuggestions);
|
|
3693
|
-
setSource("heuristic");
|
|
3694
|
-
const conversationContext = contextSummary ? `Context: ${contextSummary}
|
|
3695
|
-
|
|
3696
|
-
Assistant's response:
|
|
3697
|
-
${responseText.slice(0, 2e3)}` : `Assistant's response:
|
|
3698
|
-
${responseText.slice(0, 2e3)}`;
|
|
3699
|
-
fetchLlmFollowUps(conversationContext, controller.signal).then((llmSuggestions) => {
|
|
3700
|
-
if (controller.signal.aborted) {
|
|
3701
|
-
return;
|
|
3702
|
-
}
|
|
3703
|
-
if (llmSuggestions.length > 0) {
|
|
3704
|
-
setSuggestions(llmSuggestions);
|
|
3705
|
-
setSource("llm");
|
|
3706
|
-
}
|
|
3707
|
-
setIsLoading(false);
|
|
3708
|
-
}).catch((err) => {
|
|
3709
|
-
if (controller.signal.aborted) return;
|
|
3710
|
-
setError(err instanceof Error ? err.message : String(err));
|
|
3711
|
-
setIsLoading(false);
|
|
3712
|
-
});
|
|
3713
|
-
return () => {
|
|
3714
|
-
};
|
|
3715
|
-
}, [
|
|
3716
|
-
responseText,
|
|
3717
|
-
contextSummary,
|
|
3718
|
-
minResponseLength,
|
|
3719
|
-
timeoutMs,
|
|
3720
|
-
heuristicsOnly,
|
|
3721
|
-
llmSettings.llmApiKey,
|
|
3722
|
-
llmSettings.llmBaseUrl,
|
|
3723
|
-
llmSettings.llmModel,
|
|
3724
|
-
llmSettings.llmProvider
|
|
3725
|
-
]);
|
|
3726
|
-
return { suggestions, isLoading, error, source };
|
|
3727
|
-
}
|
|
3728
|
-
function FollowUpSuggestionsComponent({
|
|
3729
|
-
responseText,
|
|
3730
|
-
contextSummary,
|
|
3731
|
-
onSuggestionClick,
|
|
3732
|
-
disabled = false,
|
|
3733
|
-
className
|
|
3734
|
-
}) {
|
|
3735
|
-
const { suggestions, isLoading, source } = useFollowUpSuggestions(
|
|
3736
|
-
responseText,
|
|
3737
|
-
contextSummary,
|
|
3738
|
-
{
|
|
3739
|
-
minResponseLength: 50,
|
|
3740
|
-
timeoutMs: 8e3
|
|
3741
|
-
}
|
|
3742
|
-
);
|
|
3743
|
-
if (suggestions.length === 0 && !isLoading) {
|
|
3744
|
-
return null;
|
|
3745
|
-
}
|
|
3746
|
-
return /* @__PURE__ */ jsxs("div", { className: cn("flex flex-col gap-2 mt-3", className), children: [
|
|
3747
|
-
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5 text-xs text-primary-500", children: [
|
|
3748
|
-
/* @__PURE__ */ jsx("span", { className: "text-primary-400", children: "✨" }),
|
|
3749
|
-
/* @__PURE__ */ jsx("span", { children: isLoading ? /* @__PURE__ */ jsxs("span", { className: "flex items-center gap-1", children: [
|
|
3750
|
-
"Thinking of follow-ups",
|
|
3751
|
-
/* @__PURE__ */ jsx(
|
|
3752
|
-
HugeiconsIcon,
|
|
3753
|
-
{
|
|
3754
|
-
icon: Loading03Icon,
|
|
3755
|
-
size: 12,
|
|
3756
|
-
strokeWidth: 2,
|
|
3757
|
-
className: "animate-spin text-primary-400"
|
|
3758
|
-
}
|
|
3759
|
-
)
|
|
3760
|
-
] }) : source === "llm" ? "AI suggestions" : "Follow-up suggestions" })
|
|
3761
|
-
] }),
|
|
3762
|
-
/* @__PURE__ */ jsx("div", { className: "flex flex-wrap gap-2", children: suggestions.map((suggestion, index) => /* @__PURE__ */ jsxs(
|
|
3763
|
-
"button",
|
|
3764
|
-
{
|
|
3765
|
-
type: "button",
|
|
3766
|
-
disabled,
|
|
3767
|
-
onClick: () => onSuggestionClick(suggestion),
|
|
3768
|
-
className: cn(
|
|
3769
|
-
"group inline-flex items-center gap-1.5 px-3 py-1.5 rounded-full",
|
|
3770
|
-
"text-sm text-primary-700 bg-primary-50 border border-primary-200",
|
|
3771
|
-
"hover:bg-primary-100 hover:border-primary-300 hover:text-primary-900",
|
|
3772
|
-
"focus:outline-none focus:ring-2 focus:ring-primary-500/20 focus:ring-offset-1",
|
|
3773
|
-
"transition-all duration-150 cursor-pointer",
|
|
3774
|
-
"disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:bg-primary-50 disabled:hover:border-primary-200",
|
|
3775
|
-
// Subtle animation when suggestions update from heuristic to LLM
|
|
3776
|
-
isLoading && "opacity-75"
|
|
3777
|
-
),
|
|
3778
|
-
children: [
|
|
3779
|
-
/* @__PURE__ */ jsx("span", { children: suggestion }),
|
|
3780
|
-
/* @__PURE__ */ jsx(
|
|
3781
|
-
HugeiconsIcon,
|
|
3782
|
-
{
|
|
3783
|
-
icon: ArrowRight01Icon,
|
|
3784
|
-
size: 14,
|
|
3785
|
-
strokeWidth: 2,
|
|
3786
|
-
className: "text-primary-400 group-hover:text-primary-600 group-hover:translate-x-0.5 transition-all duration-150"
|
|
3787
|
-
}
|
|
3788
|
-
)
|
|
3789
|
-
]
|
|
3790
|
-
},
|
|
3791
|
-
`${index}-${suggestion.slice(0, 20)}`
|
|
3792
|
-
)) })
|
|
3793
|
-
] });
|
|
3794
|
-
}
|
|
3795
|
-
const MemoizedFollowUpSuggestions = memo(FollowUpSuggestionsComponent);
|
|
3796
|
-
function ScrollButton({
|
|
3797
|
-
className,
|
|
3798
|
-
variant = "outline",
|
|
3799
|
-
scrollRef,
|
|
3800
|
-
...props
|
|
3801
|
-
}) {
|
|
3802
|
-
const [isAtBottom, setIsAtBottom] = useState(true);
|
|
3803
|
-
const [showButton, setShowButton] = useState(false);
|
|
3804
|
-
const lastScrollTopRef = useRef(0);
|
|
3805
|
-
const checkIsAtBottom = useCallback(() => {
|
|
3806
|
-
const element = scrollRef.current;
|
|
3807
|
-
if (!element) return;
|
|
3808
|
-
const isBottom = Math.abs(
|
|
3809
|
-
element.scrollHeight - element.scrollTop - element.clientHeight
|
|
3810
|
-
) < 100;
|
|
3811
|
-
setIsAtBottom(isBottom);
|
|
3812
|
-
}, [scrollRef]);
|
|
3813
|
-
useLayoutEffect(() => {
|
|
3814
|
-
const element = scrollRef.current;
|
|
3815
|
-
if (!element) return;
|
|
3816
|
-
const handleScroll = () => {
|
|
3817
|
-
lastScrollTopRef.current = element.scrollTop;
|
|
3818
|
-
checkIsAtBottom();
|
|
3819
|
-
};
|
|
3820
|
-
const observer = new MutationObserver(() => {
|
|
3821
|
-
if (!element) return;
|
|
3822
|
-
if (element.scrollTop !== lastScrollTopRef.current) {
|
|
3823
|
-
lastScrollTopRef.current = element.scrollTop;
|
|
3824
|
-
}
|
|
3825
|
-
checkIsAtBottom();
|
|
3826
|
-
});
|
|
3827
|
-
checkIsAtBottom();
|
|
3828
|
-
element.addEventListener("scroll", handleScroll);
|
|
3829
|
-
observer.observe(element, { childList: true, subtree: true });
|
|
3830
|
-
return () => {
|
|
3831
|
-
element.removeEventListener("scroll", handleScroll);
|
|
3832
|
-
observer.disconnect();
|
|
3833
|
-
};
|
|
3834
|
-
}, [checkIsAtBottom, scrollRef]);
|
|
3835
|
-
useLayoutEffect(() => {
|
|
3836
|
-
if (isAtBottom) {
|
|
3837
|
-
setShowButton(false);
|
|
3838
|
-
return;
|
|
3839
|
-
}
|
|
3840
|
-
const timer = window.setTimeout(() => {
|
|
3841
|
-
setShowButton(true);
|
|
3842
|
-
}, 200);
|
|
3843
|
-
return () => window.clearTimeout(timer);
|
|
3844
|
-
}, [isAtBottom]);
|
|
3845
|
-
return /* @__PURE__ */ jsx(
|
|
3846
|
-
Button,
|
|
3847
|
-
{
|
|
3848
|
-
variant: "secondary",
|
|
3849
|
-
size: "icon-sm",
|
|
3850
|
-
className: cn(
|
|
3851
|
-
"pointer-events-auto rounded-full shadow-md",
|
|
3852
|
-
"transition-all duration-100 ease-in-out",
|
|
3853
|
-
!isAtBottom && showButton ? "translate-y-0 scale-100 opacity-100" : "pointer-events-none translate-y-4 scale-98 opacity-0",
|
|
3854
|
-
className
|
|
3855
|
-
),
|
|
3856
|
-
onClick: () => {
|
|
3857
|
-
const element = scrollRef.current;
|
|
3858
|
-
if (!element) return;
|
|
3859
|
-
element.scrollTop = element.scrollHeight;
|
|
3860
|
-
setIsAtBottom(true);
|
|
3861
|
-
},
|
|
3862
|
-
...props,
|
|
3863
|
-
children: /* @__PURE__ */ jsx(HugeiconsIcon, { icon: ArrowDown01Icon, size: 18, strokeWidth: 1.8 })
|
|
3864
|
-
}
|
|
3865
|
-
);
|
|
3866
|
-
}
|
|
3867
|
-
function ChatContainerShell({
|
|
3868
|
-
className,
|
|
3869
|
-
viewportRef,
|
|
3870
|
-
scrollRef,
|
|
3871
|
-
viewportProps
|
|
3872
|
-
}) {
|
|
3873
|
-
return /* @__PURE__ */ jsxs(
|
|
3874
|
-
ScrollAreaRoot,
|
|
3875
|
-
{
|
|
3876
|
-
className: cn("relative flex flex-1 min-h-0 flex-col", className),
|
|
3877
|
-
children: [
|
|
3878
|
-
/* @__PURE__ */ jsx(
|
|
3879
|
-
ScrollAreaViewport,
|
|
3880
|
-
{
|
|
3881
|
-
className: "relative will-change-transform overflow-x-hidden",
|
|
3882
|
-
ref: viewportRef,
|
|
3883
|
-
...viewportProps
|
|
3884
|
-
}
|
|
3885
|
-
),
|
|
3886
|
-
/* @__PURE__ */ jsx("div", { className: "relative mx-auto w-full min-w-0 max-w-full px-5 sm:max-w-[768px]", children: /* @__PURE__ */ jsx("div", { className: "pointer-events-none absolute bottom-10 right-10 z-50", children: /* @__PURE__ */ jsx(ScrollButton, { scrollRef }) }) }),
|
|
3887
|
-
/* @__PURE__ */ jsx(ScrollAreaScrollbar, { orientation: "vertical", children: /* @__PURE__ */ jsx(ScrollAreaThumb, {}) }),
|
|
3888
|
-
/* @__PURE__ */ jsx(ScrollAreaCorner, {})
|
|
3889
|
-
]
|
|
3890
|
-
}
|
|
3891
|
-
);
|
|
3303
|
+
function ChatContainerShell({
|
|
3304
|
+
className,
|
|
3305
|
+
viewportRef,
|
|
3306
|
+
scrollRef,
|
|
3307
|
+
viewportProps
|
|
3308
|
+
}) {
|
|
3309
|
+
return /* @__PURE__ */ jsxs(
|
|
3310
|
+
ScrollAreaRoot,
|
|
3311
|
+
{
|
|
3312
|
+
className: cn("relative flex flex-1 min-h-0 flex-col", className),
|
|
3313
|
+
children: [
|
|
3314
|
+
/* @__PURE__ */ jsx(
|
|
3315
|
+
ScrollAreaViewport,
|
|
3316
|
+
{
|
|
3317
|
+
className: "relative will-change-transform overflow-x-hidden",
|
|
3318
|
+
ref: viewportRef,
|
|
3319
|
+
...viewportProps
|
|
3320
|
+
}
|
|
3321
|
+
),
|
|
3322
|
+
/* @__PURE__ */ jsx("div", { className: "relative mx-auto w-full min-w-0 max-w-full px-5 sm:max-w-[768px]", children: /* @__PURE__ */ jsx("div", { className: "pointer-events-none absolute bottom-10 right-10 z-50", children: /* @__PURE__ */ jsx(ScrollButton, { scrollRef }) }) }),
|
|
3323
|
+
/* @__PURE__ */ jsx(ScrollAreaScrollbar, { orientation: "vertical", children: /* @__PURE__ */ jsx(ScrollAreaThumb, {}) }),
|
|
3324
|
+
/* @__PURE__ */ jsx(ScrollAreaCorner, {})
|
|
3325
|
+
]
|
|
3326
|
+
}
|
|
3327
|
+
);
|
|
3892
3328
|
}
|
|
3893
3329
|
function areViewportPropsEqual(prevProps, nextProps) {
|
|
3894
3330
|
if (prevProps === nextProps) return true;
|
|
@@ -4032,6 +3468,11 @@ function TypingIndicator({ className }) {
|
|
|
4032
3468
|
/* @__PURE__ */ jsx(TextShimmer, { className: "text-sm", duration: 2, children: "Generating..." })
|
|
4033
3469
|
] });
|
|
4034
3470
|
}
|
|
3471
|
+
const FollowUpSuggestions = lazy(
|
|
3472
|
+
() => import("./follow-up-suggestions-CSSc4PDe.js").then((m) => ({
|
|
3473
|
+
default: m.FollowUpSuggestions
|
|
3474
|
+
}))
|
|
3475
|
+
);
|
|
4035
3476
|
function ChatMessageListComponent({
|
|
4036
3477
|
messages,
|
|
4037
3478
|
loading,
|
|
@@ -4058,6 +3499,7 @@ function ChatMessageListComponent({
|
|
|
4058
3499
|
const displayMessages = useMemo(() => {
|
|
4059
3500
|
return messages.filter((msg) => msg.role !== "toolResult");
|
|
4060
3501
|
}, [messages]);
|
|
3502
|
+
const deferredDisplayMessages = useDeferredValue(displayMessages);
|
|
4061
3503
|
const toolResultsByCallId = useMemo(() => {
|
|
4062
3504
|
const map = /* @__PURE__ */ new Map();
|
|
4063
3505
|
for (const message of messages) {
|
|
@@ -4069,10 +3511,40 @@ function ChatMessageListComponent({
|
|
|
4069
3511
|
}
|
|
4070
3512
|
return map;
|
|
4071
3513
|
}, [messages]);
|
|
3514
|
+
const deferredToolResultsByCallId = useDeferredValue(toolResultsByCallId);
|
|
3515
|
+
const aggregatedSearchSourcesSignature = useMemo(() => {
|
|
3516
|
+
const parts = [];
|
|
3517
|
+
for (const msg of deferredDisplayMessages) {
|
|
3518
|
+
if (msg.role !== "assistant") continue;
|
|
3519
|
+
const toolCalls = getToolCallsFromMessage(msg);
|
|
3520
|
+
for (const tc of toolCalls) {
|
|
3521
|
+
if (!tc.id) continue;
|
|
3522
|
+
const isSearch = tc.name === "web_search";
|
|
3523
|
+
const isFetch = tc.name === "web_fetch";
|
|
3524
|
+
const isExec = tc.name === "exec";
|
|
3525
|
+
if (!isSearch && !isFetch && !isExec) continue;
|
|
3526
|
+
const result = deferredToolResultsByCallId.get(tc.id);
|
|
3527
|
+
const text = result?.content?.map((p) => p.type === "text" ? String(p.text ?? "") : "").join("").trim() ?? "";
|
|
3528
|
+
parts.push(
|
|
3529
|
+
[
|
|
3530
|
+
tc.id,
|
|
3531
|
+
tc.name ?? "",
|
|
3532
|
+
typeof tc.arguments?.url === "string" ? tc.arguments.url : "",
|
|
3533
|
+
result?.isError ? "1" : "0",
|
|
3534
|
+
text
|
|
3535
|
+
].join("::")
|
|
3536
|
+
);
|
|
3537
|
+
}
|
|
3538
|
+
}
|
|
3539
|
+
return parts.join("||");
|
|
3540
|
+
}, [deferredDisplayMessages, deferredToolResultsByCallId]);
|
|
4072
3541
|
const aggregatedSearchSources = useMemo(() => {
|
|
4073
3542
|
const strip = (s) => {
|
|
4074
3543
|
if (!s) return "";
|
|
4075
|
-
return s.replace(
|
|
3544
|
+
return s.replace(
|
|
3545
|
+
/SECURITY NOTICE:[\s\S]*?<<<EXTERNAL_UNTRUSTED_CONTENT>>>/g,
|
|
3546
|
+
""
|
|
3547
|
+
).replace(/<<<\/?EXTERNAL_UNTRUSTED_CONTENT>>>/g, "").replace(/<<<\/?END_EXTERNAL_UNTRUSTED_CONTENT>>>/g, "").replace(/Source: Web (?:Search|Fetch)\n---/g, "").replace(/\n{2,}/g, "\n").trim();
|
|
4076
3548
|
};
|
|
4077
3549
|
const extractResults = (text, sources2, seenUrls2) => {
|
|
4078
3550
|
try {
|
|
@@ -4086,7 +3558,9 @@ function ChatMessageListComponent({
|
|
|
4086
3558
|
sources2.push({
|
|
4087
3559
|
title: strip(item.title),
|
|
4088
3560
|
url: item.url,
|
|
4089
|
-
snippet: strip(
|
|
3561
|
+
snippet: strip(
|
|
3562
|
+
item.description || item.snippet || item.content || ""
|
|
3563
|
+
)
|
|
4090
3564
|
});
|
|
4091
3565
|
found = true;
|
|
4092
3566
|
}
|
|
@@ -4098,7 +3572,7 @@ function ChatMessageListComponent({
|
|
|
4098
3572
|
};
|
|
4099
3573
|
const sources = [];
|
|
4100
3574
|
const seenUrls = /* @__PURE__ */ new Set();
|
|
4101
|
-
for (const msg of
|
|
3575
|
+
for (const msg of deferredDisplayMessages) {
|
|
4102
3576
|
if (msg.role !== "assistant") continue;
|
|
4103
3577
|
const toolCalls = getToolCallsFromMessage(msg);
|
|
4104
3578
|
for (const tc of toolCalls) {
|
|
@@ -4107,7 +3581,7 @@ function ChatMessageListComponent({
|
|
|
4107
3581
|
const isFetch = tc.name === "web_fetch";
|
|
4108
3582
|
const isExec = tc.name === "exec";
|
|
4109
3583
|
if (!isSearch && !isFetch && !isExec) continue;
|
|
4110
|
-
const result =
|
|
3584
|
+
const result = deferredToolResultsByCallId.get(tc.id);
|
|
4111
3585
|
if (!result) continue;
|
|
4112
3586
|
const text = result.content?.map((p) => p.type === "text" ? String(p.text ?? "") : "").join("").trim();
|
|
4113
3587
|
if (!text) continue;
|
|
@@ -4137,7 +3611,11 @@ function ChatMessageListComponent({
|
|
|
4137
3611
|
}
|
|
4138
3612
|
}
|
|
4139
3613
|
return sources;
|
|
4140
|
-
}, [
|
|
3614
|
+
}, [
|
|
3615
|
+
aggregatedSearchSourcesSignature,
|
|
3616
|
+
deferredDisplayMessages,
|
|
3617
|
+
deferredToolResultsByCallId
|
|
3618
|
+
]);
|
|
4141
3619
|
const lastAssistantIndex = displayMessages.map((message, index) => ({ message, index })).filter(({ message }) => message.role !== "user").map(({ index }) => index).pop();
|
|
4142
3620
|
const lastTextAssistantIndex = displayMessages.map((message, index) => ({ message, index })).filter(({ message }) => message.role === "assistant").map(({ index }) => index).pop();
|
|
4143
3621
|
const lastUserIndex = displayMessages.map((message, index) => ({ message, index })).filter(({ message }) => message.role === "user").map(({ index }) => index).pop();
|
|
@@ -4179,7 +3657,9 @@ function ChatMessageListComponent({
|
|
|
4179
3657
|
target.scrollIntoView({ behavior: "smooth", block: "center" });
|
|
4180
3658
|
setHighlightedMessageId(jumpToMessageId);
|
|
4181
3659
|
const timer = window.setTimeout(() => {
|
|
4182
|
-
setHighlightedMessageId(
|
|
3660
|
+
setHighlightedMessageId(
|
|
3661
|
+
(prev) => prev === jumpToMessageId ? null : prev
|
|
3662
|
+
);
|
|
4183
3663
|
}, 1800);
|
|
4184
3664
|
return () => window.clearTimeout(timer);
|
|
4185
3665
|
}, [jumpToMessageId, loading, displayMessages]);
|
|
@@ -4244,14 +3724,14 @@ function ChatMessageListComponent({
|
|
|
4244
3724
|
);
|
|
4245
3725
|
}),
|
|
4246
3726
|
showTypingIndicator ? /* @__PURE__ */ jsx("div", { className: "py-2", children: /* @__PURE__ */ jsx(TypingIndicator, {}) }) : null,
|
|
4247
|
-
showFollowUps && onFollowUpClick ? /* @__PURE__ */ jsx(
|
|
4248
|
-
|
|
3727
|
+
showFollowUps && onFollowUpClick ? /* @__PURE__ */ jsx(Suspense, { fallback: null, children: /* @__PURE__ */ jsx(
|
|
3728
|
+
FollowUpSuggestions,
|
|
4249
3729
|
{
|
|
4250
3730
|
responseText: lastAssistantText,
|
|
4251
3731
|
onSuggestionClick: onFollowUpClick,
|
|
4252
3732
|
disabled: waitingForResponse
|
|
4253
3733
|
}
|
|
4254
|
-
) : null
|
|
3734
|
+
) }) : null
|
|
4255
3735
|
]
|
|
4256
3736
|
}
|
|
4257
3737
|
)
|
|
@@ -4277,14 +3757,14 @@ function ChatMessageListComponent({
|
|
|
4277
3757
|
messageKey
|
|
4278
3758
|
);
|
|
4279
3759
|
}),
|
|
4280
|
-
showFollowUps && onFollowUpClick ? /* @__PURE__ */ jsx(
|
|
4281
|
-
|
|
3760
|
+
showFollowUps && onFollowUpClick ? /* @__PURE__ */ jsx(Suspense, { fallback: null, children: /* @__PURE__ */ jsx(
|
|
3761
|
+
FollowUpSuggestions,
|
|
4282
3762
|
{
|
|
4283
3763
|
responseText: lastAssistantText,
|
|
4284
3764
|
onSuggestionClick: onFollowUpClick,
|
|
4285
3765
|
disabled: waitingForResponse
|
|
4286
3766
|
}
|
|
4287
|
-
) : null
|
|
3767
|
+
) }) : null
|
|
4288
3768
|
] }),
|
|
4289
3769
|
notice && noticePosition === "end" ? notice : null,
|
|
4290
3770
|
/* @__PURE__ */ jsx(
|
|
@@ -4462,159 +3942,27 @@ function PromptInputActions({
|
|
|
4462
3942
|
children,
|
|
4463
3943
|
className,
|
|
4464
3944
|
...props
|
|
4465
|
-
}) {
|
|
4466
|
-
return /* @__PURE__ */ jsx("div", { className: cn("flex items-center gap-2", className), ...props, children });
|
|
4467
|
-
}
|
|
4468
|
-
function PromptInputAction({
|
|
4469
|
-
tooltip,
|
|
4470
|
-
children,
|
|
4471
|
-
className,
|
|
4472
|
-
side = "top",
|
|
4473
|
-
...props
|
|
4474
|
-
}) {
|
|
4475
|
-
const { disabled } = usePromptInput();
|
|
4476
|
-
return /* @__PURE__ */ jsxs(TooltipRoot, { ...props, children: [
|
|
4477
|
-
/* @__PURE__ */ jsx(
|
|
4478
|
-
TooltipTrigger,
|
|
4479
|
-
{
|
|
4480
|
-
disabled,
|
|
4481
|
-
onClick: (event) => event.stopPropagation(),
|
|
4482
|
-
children
|
|
4483
|
-
}
|
|
4484
|
-
),
|
|
4485
|
-
/* @__PURE__ */ jsx(TooltipContent, { side, className, children: tooltip })
|
|
4486
|
-
] });
|
|
4487
|
-
}
|
|
4488
|
-
const STORAGE_KEY = "opencami-selected-model";
|
|
4489
|
-
function getStoredModel() {
|
|
4490
|
-
if (typeof window === "undefined") return null;
|
|
4491
|
-
try {
|
|
4492
|
-
return localStorage.getItem(STORAGE_KEY);
|
|
4493
|
-
} catch {
|
|
4494
|
-
return null;
|
|
4495
|
-
}
|
|
4496
|
-
}
|
|
4497
|
-
function setStoredModel(modelId) {
|
|
4498
|
-
if (typeof window === "undefined") return;
|
|
4499
|
-
try {
|
|
4500
|
-
localStorage.setItem(STORAGE_KEY, modelId);
|
|
4501
|
-
} catch {
|
|
4502
|
-
}
|
|
4503
|
-
}
|
|
4504
|
-
function ModelSelector({ className, onModelChange }) {
|
|
4505
|
-
const [models, setModels] = useState([]);
|
|
4506
|
-
const [selectedModel, setSelectedModel] = useState("");
|
|
4507
|
-
const [isLoading, setIsLoading] = useState(true);
|
|
4508
|
-
const [error, setError] = useState(null);
|
|
4509
|
-
const abortControllerRef = useRef(null);
|
|
4510
|
-
useEffect(() => {
|
|
4511
|
-
let mounted = true;
|
|
4512
|
-
async function fetchModels() {
|
|
4513
|
-
abortControllerRef.current?.abort();
|
|
4514
|
-
const controller = new AbortController();
|
|
4515
|
-
abortControllerRef.current = controller;
|
|
4516
|
-
try {
|
|
4517
|
-
setIsLoading(true);
|
|
4518
|
-
setError(null);
|
|
4519
|
-
const response = await fetch("/api/models", { signal: controller.signal });
|
|
4520
|
-
if (!response.ok) {
|
|
4521
|
-
throw new Error("Failed to fetch models");
|
|
4522
|
-
}
|
|
4523
|
-
const data = await response.json();
|
|
4524
|
-
if (!mounted) return;
|
|
4525
|
-
if (data.ok && data.models.length > 0) {
|
|
4526
|
-
setModels(data.models);
|
|
4527
|
-
const storedModel = getStoredModel();
|
|
4528
|
-
const initialModel = storedModel && data.models.some((m) => m.id === storedModel) ? storedModel : data.defaultModel;
|
|
4529
|
-
setSelectedModel(initialModel);
|
|
4530
|
-
onModelChange?.(initialModel);
|
|
4531
|
-
} else {
|
|
4532
|
-
throw new Error("No models available");
|
|
4533
|
-
}
|
|
4534
|
-
} catch (err) {
|
|
4535
|
-
if (err instanceof Error && err.name === "AbortError") return;
|
|
4536
|
-
if (!mounted) return;
|
|
4537
|
-
console.error("[model-selector] Error fetching models:", err);
|
|
4538
|
-
setError(err instanceof Error ? err.message : "Failed to load models");
|
|
4539
|
-
setModels([{ id: "default", name: "Default Model" }]);
|
|
4540
|
-
setSelectedModel("default");
|
|
4541
|
-
} finally {
|
|
4542
|
-
if (mounted) {
|
|
4543
|
-
setIsLoading(false);
|
|
4544
|
-
}
|
|
4545
|
-
}
|
|
4546
|
-
}
|
|
4547
|
-
fetchModels();
|
|
4548
|
-
return () => {
|
|
4549
|
-
mounted = false;
|
|
4550
|
-
abortControllerRef.current?.abort();
|
|
4551
|
-
};
|
|
4552
|
-
}, [onModelChange]);
|
|
4553
|
-
function handleModelSelect(modelId) {
|
|
4554
|
-
setSelectedModel(modelId);
|
|
4555
|
-
setStoredModel(modelId);
|
|
4556
|
-
onModelChange?.(modelId);
|
|
4557
|
-
}
|
|
4558
|
-
if (isLoading) {
|
|
4559
|
-
return /* @__PURE__ */ jsxs("div", { className: cn("flex items-center gap-2 text-xs text-primary-500", className), children: [
|
|
4560
|
-
/* @__PURE__ */ jsx(HugeiconsIcon, { icon: ArtificialIntelligence02Icon, size: 14 }),
|
|
4561
|
-
/* @__PURE__ */ jsx("span", { children: "Loading..." })
|
|
4562
|
-
] });
|
|
4563
|
-
}
|
|
4564
|
-
if (error || models.length === 0) {
|
|
4565
|
-
return null;
|
|
4566
|
-
}
|
|
4567
|
-
const selectedModelInfo = models.find((m) => m.id === selectedModel);
|
|
4568
|
-
const displayName = selectedModelInfo?.name || "Select Model";
|
|
4569
|
-
const shortDisplayName = (() => {
|
|
4570
|
-
if (!selectedModelInfo?.name) return displayName;
|
|
4571
|
-
const name = selectedModelInfo.name;
|
|
4572
|
-
const clean = name.replace(/\s*\([^)]*\)\s*$/, "").trim();
|
|
4573
|
-
const words = clean.split(/\s+/);
|
|
4574
|
-
if (words.length > 3) return words.slice(0, 3).join(" ");
|
|
4575
|
-
return clean;
|
|
4576
|
-
})();
|
|
4577
|
-
if (models.length === 1) {
|
|
4578
|
-
return /* @__PURE__ */ jsxs("div", { className: cn("flex items-center gap-2 text-xs text-primary-500", className), children: [
|
|
4579
|
-
/* @__PURE__ */ jsx(HugeiconsIcon, { icon: ArtificialIntelligence02Icon, size: 14 }),
|
|
4580
|
-
/* @__PURE__ */ jsx("span", { className: "font-[450] md:hidden", children: shortDisplayName }),
|
|
4581
|
-
/* @__PURE__ */ jsx("span", { className: "font-[450] hidden md:inline", children: displayName })
|
|
4582
|
-
] });
|
|
4583
|
-
}
|
|
4584
|
-
return /* @__PURE__ */ jsxs(MenuRoot, { children: [
|
|
4585
|
-
/* @__PURE__ */ jsxs(
|
|
4586
|
-
MenuTrigger,
|
|
3945
|
+
}) {
|
|
3946
|
+
return /* @__PURE__ */ jsx("div", { className: cn("flex items-center gap-2", className), ...props, children });
|
|
3947
|
+
}
|
|
3948
|
+
function PromptInputAction({
|
|
3949
|
+
tooltip,
|
|
3950
|
+
children,
|
|
3951
|
+
className,
|
|
3952
|
+
side = "top",
|
|
3953
|
+
...props
|
|
3954
|
+
}) {
|
|
3955
|
+
const { disabled } = usePromptInput();
|
|
3956
|
+
return /* @__PURE__ */ jsxs(TooltipRoot, { ...props, children: [
|
|
3957
|
+
/* @__PURE__ */ jsx(
|
|
3958
|
+
TooltipTrigger,
|
|
4587
3959
|
{
|
|
4588
|
-
|
|
4589
|
-
|
|
4590
|
-
|
|
4591
|
-
),
|
|
4592
|
-
children: [
|
|
4593
|
-
/* @__PURE__ */ jsx(HugeiconsIcon, { icon: ArtificialIntelligence02Icon, size: 14 }),
|
|
4594
|
-
/* @__PURE__ */ jsx("span", { className: "md:hidden", children: shortDisplayName }),
|
|
4595
|
-
/* @__PURE__ */ jsx("span", { className: "hidden md:inline", children: displayName })
|
|
4596
|
-
]
|
|
3960
|
+
disabled,
|
|
3961
|
+
onClick: (event) => event.stopPropagation(),
|
|
3962
|
+
children
|
|
4597
3963
|
}
|
|
4598
3964
|
),
|
|
4599
|
-
/* @__PURE__ */ jsx(
|
|
4600
|
-
MenuItem,
|
|
4601
|
-
{
|
|
4602
|
-
onClick: () => handleModelSelect(model.id),
|
|
4603
|
-
className: "justify-between min-w-[180px]",
|
|
4604
|
-
children: [
|
|
4605
|
-
/* @__PURE__ */ jsx("span", { children: model.name }),
|
|
4606
|
-
selectedModel === model.id && /* @__PURE__ */ jsx(
|
|
4607
|
-
HugeiconsIcon,
|
|
4608
|
-
{
|
|
4609
|
-
icon: Tick02Icon,
|
|
4610
|
-
size: 14,
|
|
4611
|
-
className: "text-primary-600"
|
|
4612
|
-
}
|
|
4613
|
-
)
|
|
4614
|
-
]
|
|
4615
|
-
},
|
|
4616
|
-
model.id
|
|
4617
|
-
)) })
|
|
3965
|
+
/* @__PURE__ */ jsx(TooltipContent, { side, className, children: tooltip })
|
|
4618
3966
|
] });
|
|
4619
3967
|
}
|
|
4620
3968
|
const PERSONAS_ENABLED_KEY = "opencami-personas-enabled";
|
|
@@ -5374,6 +4722,7 @@ function ChatComposerComponent({
|
|
|
5374
4722
|
const [isRecording, setIsRecording] = useState(false);
|
|
5375
4723
|
const [recordingTime, setRecordingTime] = useState(0);
|
|
5376
4724
|
const [sttLoading, setSttLoading] = useState(false);
|
|
4725
|
+
const [micError, setMicError] = useState(null);
|
|
5377
4726
|
const mediaRecorderRef = useRef(null);
|
|
5378
4727
|
const recordingChunksRef = useRef([]);
|
|
5379
4728
|
const recordingTimerRef = useRef(null);
|
|
@@ -5406,6 +4755,9 @@ function ChatComposerComponent({
|
|
|
5406
4755
|
promptRef.current?.focus();
|
|
5407
4756
|
});
|
|
5408
4757
|
}, []);
|
|
4758
|
+
const showMicError = useCallback((message) => {
|
|
4759
|
+
setMicError(message);
|
|
4760
|
+
}, []);
|
|
5409
4761
|
const reset = useCallback(() => {
|
|
5410
4762
|
setValue("");
|
|
5411
4763
|
setAttachments([]);
|
|
@@ -5629,11 +4981,12 @@ ${template}`;
|
|
|
5629
4981
|
setRecordingTime(0);
|
|
5630
4982
|
}, []);
|
|
5631
4983
|
const startRecording = useCallback(async () => {
|
|
4984
|
+
setMicError(null);
|
|
5632
4985
|
const provider = getSttProvider();
|
|
5633
4986
|
if (provider === "browser") {
|
|
5634
4987
|
const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
|
|
5635
4988
|
if (!SpeechRecognition) {
|
|
5636
|
-
|
|
4989
|
+
showMicError("Web Speech API is not supported in this browser.");
|
|
5637
4990
|
return;
|
|
5638
4991
|
}
|
|
5639
4992
|
const recognition = new SpeechRecognition();
|
|
@@ -5665,6 +5018,7 @@ ${template}`;
|
|
|
5665
5018
|
setRecordingTime(0);
|
|
5666
5019
|
if (recordingTimerRef.current) clearInterval(recordingTimerRef.current);
|
|
5667
5020
|
if (transcript.trim()) {
|
|
5021
|
+
setMicError(null);
|
|
5668
5022
|
setValue((prev) => prev + (prev ? " " : "") + transcript.trim());
|
|
5669
5023
|
focusPrompt();
|
|
5670
5024
|
}
|
|
@@ -5673,6 +5027,7 @@ ${template}`;
|
|
|
5673
5027
|
setIsRecording(false);
|
|
5674
5028
|
setRecordingTime(0);
|
|
5675
5029
|
if (recordingTimerRef.current) clearInterval(recordingTimerRef.current);
|
|
5030
|
+
showMicError("Speech recognition failed. Try the Browser provider again or switch providers in Settings.");
|
|
5676
5031
|
};
|
|
5677
5032
|
recognition.start();
|
|
5678
5033
|
return;
|
|
@@ -5701,16 +5056,17 @@ ${template}`;
|
|
|
5701
5056
|
const res = await fetch("/api/stt", { method: "POST", body: formData, signal: controller.signal });
|
|
5702
5057
|
const data = await res.json();
|
|
5703
5058
|
if (data.ok && data.text) {
|
|
5059
|
+
setMicError(null);
|
|
5704
5060
|
setValue((prev) => prev + (prev ? " " : "") + data.text);
|
|
5705
5061
|
focusPrompt();
|
|
5706
5062
|
} else if (!data.ok) {
|
|
5707
5063
|
console.warn("STT failed:", data.error);
|
|
5708
|
-
|
|
5064
|
+
showMicError(data.error || "Speech-to-text failed. Try the Browser provider in Settings.");
|
|
5709
5065
|
}
|
|
5710
5066
|
} catch (err) {
|
|
5711
5067
|
if (err instanceof Error && err.name === "AbortError") return;
|
|
5712
5068
|
console.warn("STT request failed:", err);
|
|
5713
|
-
|
|
5069
|
+
showMicError("Could not reach speech-to-text service.");
|
|
5714
5070
|
} finally {
|
|
5715
5071
|
setSttLoading(false);
|
|
5716
5072
|
}
|
|
@@ -5733,18 +5089,18 @@ ${template}`;
|
|
|
5733
5089
|
try {
|
|
5734
5090
|
const status = await navigator.permissions.query({ name: "microphone" });
|
|
5735
5091
|
if (status.state === "denied") {
|
|
5736
|
-
|
|
5092
|
+
showMicError("Microphone access is blocked. Please enable it in your browser/app settings.");
|
|
5737
5093
|
} else {
|
|
5738
|
-
|
|
5094
|
+
showMicError("Microphone permission was not granted. Please try again and allow access when prompted.");
|
|
5739
5095
|
}
|
|
5740
5096
|
} catch {
|
|
5741
|
-
|
|
5097
|
+
showMicError("Could not access microphone. Please check your browser settings and allow microphone access for this site.");
|
|
5742
5098
|
}
|
|
5743
5099
|
} else {
|
|
5744
|
-
|
|
5100
|
+
showMicError("Could not access microphone: " + msg);
|
|
5745
5101
|
}
|
|
5746
5102
|
}
|
|
5747
|
-
}, [getSttProvider, stopRecording, focusPrompt]);
|
|
5103
|
+
}, [getSttProvider, stopRecording, focusPrompt, showMicError]);
|
|
5748
5104
|
const handleMicClick = useCallback(() => {
|
|
5749
5105
|
if (isRecording) {
|
|
5750
5106
|
stopRecording();
|
|
@@ -5780,6 +5136,15 @@ ${template}`;
|
|
|
5780
5136
|
onRemove: handleRemoveAttachment
|
|
5781
5137
|
}
|
|
5782
5138
|
),
|
|
5139
|
+
micError && /* @__PURE__ */ jsx(
|
|
5140
|
+
"div",
|
|
5141
|
+
{
|
|
5142
|
+
className: "mx-3 mt-3 rounded-lg border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-700 dark:border-red-900/40 dark:bg-red-950/40 dark:text-red-200",
|
|
5143
|
+
role: "alert",
|
|
5144
|
+
"aria-live": "polite",
|
|
5145
|
+
children: micError
|
|
5146
|
+
}
|
|
5147
|
+
),
|
|
5783
5148
|
showSlashCommands && filteredSlashCommands.length > 0 && /* @__PURE__ */ jsx(
|
|
5784
5149
|
SlashCommandMenu,
|
|
5785
5150
|
{
|
|
@@ -5798,7 +5163,6 @@ ${template}`;
|
|
|
5798
5163
|
),
|
|
5799
5164
|
/* @__PURE__ */ jsxs(PromptInputActions, { className: "justify-between px-3", children: [
|
|
5800
5165
|
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1", children: [
|
|
5801
|
-
/* @__PURE__ */ jsx(ModelSelector, { onModelChange: setSelectedModel }),
|
|
5802
5166
|
/* @__PURE__ */ jsx(ThinkingLevelSelector, {}),
|
|
5803
5167
|
/* @__PURE__ */ jsx(PersonaPicker, { onSelect: handlePersonaSelect }),
|
|
5804
5168
|
/* @__PURE__ */ jsx(CommandHelp, { onCommandSelect: (cmd) => handleValueChange(cmd + " ") })
|
|
@@ -6066,10 +5430,25 @@ function useChatSessions({
|
|
|
6066
5430
|
isNewChat,
|
|
6067
5431
|
forcedSessionKey
|
|
6068
5432
|
}) {
|
|
5433
|
+
const [isDocumentVisible, setIsDocumentVisible] = useState(() => {
|
|
5434
|
+
if (typeof document === "undefined") return true;
|
|
5435
|
+
return document.visibilityState === "visible";
|
|
5436
|
+
});
|
|
5437
|
+
useEffect(() => {
|
|
5438
|
+
if (typeof document === "undefined") return void 0;
|
|
5439
|
+
const handleVisibilityChange = () => {
|
|
5440
|
+
setIsDocumentVisible(document.visibilityState === "visible");
|
|
5441
|
+
};
|
|
5442
|
+
handleVisibilityChange();
|
|
5443
|
+
document.addEventListener("visibilitychange", handleVisibilityChange);
|
|
5444
|
+
return () => {
|
|
5445
|
+
document.removeEventListener("visibilitychange", handleVisibilityChange);
|
|
5446
|
+
};
|
|
5447
|
+
}, []);
|
|
6069
5448
|
const sessionsQuery = useQuery({
|
|
6070
5449
|
queryKey: chatQueryKeys.sessions,
|
|
6071
5450
|
queryFn: fetchSessions,
|
|
6072
|
-
refetchInterval: 3e4
|
|
5451
|
+
refetchInterval: isDocumentVisible ? 3e4 : false
|
|
6073
5452
|
});
|
|
6074
5453
|
const sessions = useMemo(() => {
|
|
6075
5454
|
const rawSessions = sessionsQuery.data ?? [];
|
|
@@ -6109,6 +5488,197 @@ function useChatSessions({
|
|
|
6109
5488
|
sessionsError
|
|
6110
5489
|
};
|
|
6111
5490
|
}
|
|
5491
|
+
const LLM_PROVIDER_DEFAULTS = {
|
|
5492
|
+
openai: {
|
|
5493
|
+
baseUrl: "https://api.openai.com/v1",
|
|
5494
|
+
model: "gpt-4.1-nano"
|
|
5495
|
+
},
|
|
5496
|
+
openrouter: {
|
|
5497
|
+
baseUrl: "https://openrouter.ai/api/v1",
|
|
5498
|
+
model: "openai/gpt-oss-120b"
|
|
5499
|
+
},
|
|
5500
|
+
kilocode: {
|
|
5501
|
+
baseUrl: "https://api.kilo.ai/api/gateway",
|
|
5502
|
+
model: "google/gemini-2.5-flash"
|
|
5503
|
+
},
|
|
5504
|
+
ollama: {
|
|
5505
|
+
baseUrl: "http://localhost:11434/v1",
|
|
5506
|
+
model: "llama3.2"
|
|
5507
|
+
},
|
|
5508
|
+
custom: {
|
|
5509
|
+
baseUrl: "",
|
|
5510
|
+
model: ""
|
|
5511
|
+
}
|
|
5512
|
+
};
|
|
5513
|
+
const DEFAULT_LLM_SETTINGS = {
|
|
5514
|
+
useLlmTitles: true,
|
|
5515
|
+
useLlmFollowUps: true,
|
|
5516
|
+
llmProvider: "openai",
|
|
5517
|
+
llmBaseUrl: "",
|
|
5518
|
+
llmModel: "",
|
|
5519
|
+
llmApiKey: ""
|
|
5520
|
+
};
|
|
5521
|
+
function getLlmProviderDefaults(provider) {
|
|
5522
|
+
return LLM_PROVIDER_DEFAULTS[provider];
|
|
5523
|
+
}
|
|
5524
|
+
function getEffectiveLlmBaseUrl(settings) {
|
|
5525
|
+
if (settings.llmBaseUrl.trim()) return settings.llmBaseUrl.trim();
|
|
5526
|
+
return getLlmProviderDefaults(settings.llmProvider).baseUrl;
|
|
5527
|
+
}
|
|
5528
|
+
function getEffectiveLlmModel(settings) {
|
|
5529
|
+
if (settings.llmModel.trim()) return settings.llmModel.trim();
|
|
5530
|
+
return getLlmProviderDefaults(settings.llmProvider).model;
|
|
5531
|
+
}
|
|
5532
|
+
function getAvailability(settings, hasEnvKey) {
|
|
5533
|
+
if (settings.llmProvider === "ollama") return true;
|
|
5534
|
+
if (settings.llmProvider === "custom") {
|
|
5535
|
+
return Boolean(settings.llmApiKey.trim()) || Boolean(settings.llmBaseUrl.trim() && settings.llmModel.trim());
|
|
5536
|
+
}
|
|
5537
|
+
return hasEnvKey || Boolean(settings.llmApiKey.trim());
|
|
5538
|
+
}
|
|
5539
|
+
function migratePersistedState(persistedState) {
|
|
5540
|
+
if (!persistedState || typeof persistedState !== "object") {
|
|
5541
|
+
return { settings: DEFAULT_LLM_SETTINGS };
|
|
5542
|
+
}
|
|
5543
|
+
const { settings } = persistedState;
|
|
5544
|
+
if (!settings) {
|
|
5545
|
+
return { settings: DEFAULT_LLM_SETTINGS };
|
|
5546
|
+
}
|
|
5547
|
+
const { openaiApiKey, ...rest } = settings;
|
|
5548
|
+
const llmApiKey = rest.llmApiKey ?? openaiApiKey ?? "";
|
|
5549
|
+
return {
|
|
5550
|
+
settings: {
|
|
5551
|
+
...DEFAULT_LLM_SETTINGS,
|
|
5552
|
+
...rest,
|
|
5553
|
+
llmApiKey
|
|
5554
|
+
}
|
|
5555
|
+
};
|
|
5556
|
+
}
|
|
5557
|
+
const useLlmSettingsStore = create()(
|
|
5558
|
+
persist(
|
|
5559
|
+
(set) => ({
|
|
5560
|
+
settings: {
|
|
5561
|
+
...DEFAULT_LLM_SETTINGS
|
|
5562
|
+
},
|
|
5563
|
+
updateSettings: (updates) => set((state) => ({
|
|
5564
|
+
settings: { ...state.settings, ...updates }
|
|
5565
|
+
})),
|
|
5566
|
+
clearApiKey: () => set((state) => ({
|
|
5567
|
+
settings: { ...state.settings, llmApiKey: "" }
|
|
5568
|
+
}))
|
|
5569
|
+
}),
|
|
5570
|
+
{
|
|
5571
|
+
name: "llm-settings",
|
|
5572
|
+
version: 2,
|
|
5573
|
+
migrate: (persistedState) => migratePersistedState(persistedState)
|
|
5574
|
+
}
|
|
5575
|
+
)
|
|
5576
|
+
);
|
|
5577
|
+
function useLlmSettings() {
|
|
5578
|
+
const settings = useLlmSettingsStore((state) => state.settings);
|
|
5579
|
+
const updateSettings = useLlmSettingsStore((state) => state.updateSettings);
|
|
5580
|
+
const clearApiKey = useLlmSettingsStore((state) => state.clearApiKey);
|
|
5581
|
+
const [status, setStatus] = useState({
|
|
5582
|
+
hasEnvKey: false,
|
|
5583
|
+
hasOpenRouterKey: false,
|
|
5584
|
+
hasKilocodeKey: false,
|
|
5585
|
+
hasUserKey: Boolean(settings.llmApiKey),
|
|
5586
|
+
isAvailable: getAvailability(settings, false),
|
|
5587
|
+
isLoading: true,
|
|
5588
|
+
error: null
|
|
5589
|
+
});
|
|
5590
|
+
useEffect(() => {
|
|
5591
|
+
let cancelled = false;
|
|
5592
|
+
async function checkStatus() {
|
|
5593
|
+
try {
|
|
5594
|
+
const res = await fetch("/api/llm-features");
|
|
5595
|
+
if (!res.ok) throw new Error("Failed to check LLM status");
|
|
5596
|
+
const data = await res.json();
|
|
5597
|
+
if (cancelled) return;
|
|
5598
|
+
const hasUserKey = Boolean(settings.llmApiKey);
|
|
5599
|
+
const hasProviderKey = settings.llmProvider === "openrouter" ? Boolean(data.hasOpenRouterKey) : settings.llmProvider === "kilocode" ? Boolean(data.hasKilocodeKey) : data.hasEnvKey;
|
|
5600
|
+
setStatus({
|
|
5601
|
+
hasEnvKey: data.hasEnvKey,
|
|
5602
|
+
hasOpenRouterKey: Boolean(data.hasOpenRouterKey),
|
|
5603
|
+
hasKilocodeKey: Boolean(data.hasKilocodeKey),
|
|
5604
|
+
hasUserKey,
|
|
5605
|
+
isAvailable: getAvailability(settings, hasProviderKey),
|
|
5606
|
+
isLoading: false,
|
|
5607
|
+
error: null
|
|
5608
|
+
});
|
|
5609
|
+
} catch (err) {
|
|
5610
|
+
if (cancelled) return;
|
|
5611
|
+
setStatus((prev) => ({
|
|
5612
|
+
...prev,
|
|
5613
|
+
isLoading: false,
|
|
5614
|
+
error: err instanceof Error ? err.message : "Failed to check status"
|
|
5615
|
+
}));
|
|
5616
|
+
}
|
|
5617
|
+
}
|
|
5618
|
+
void checkStatus();
|
|
5619
|
+
return () => {
|
|
5620
|
+
cancelled = true;
|
|
5621
|
+
};
|
|
5622
|
+
}, [
|
|
5623
|
+
settings.llmApiKey,
|
|
5624
|
+
settings.llmProvider,
|
|
5625
|
+
settings.llmBaseUrl,
|
|
5626
|
+
settings.llmModel
|
|
5627
|
+
]);
|
|
5628
|
+
const testApiKey = useCallback(async (key) => {
|
|
5629
|
+
try {
|
|
5630
|
+
const headers = buildLlmHeaders({
|
|
5631
|
+
...settings,
|
|
5632
|
+
llmApiKey: key
|
|
5633
|
+
});
|
|
5634
|
+
const res = await fetch("/api/llm-features", {
|
|
5635
|
+
method: "POST",
|
|
5636
|
+
headers: {
|
|
5637
|
+
"Content-Type": "application/json",
|
|
5638
|
+
...headers
|
|
5639
|
+
},
|
|
5640
|
+
body: JSON.stringify({ action: "test" })
|
|
5641
|
+
});
|
|
5642
|
+
const data = await res.json();
|
|
5643
|
+
if (!data.ok) {
|
|
5644
|
+
return { valid: false, error: data.error || "Test failed" };
|
|
5645
|
+
}
|
|
5646
|
+
return { valid: data.valid ?? false, error: data.error };
|
|
5647
|
+
} catch (err) {
|
|
5648
|
+
return {
|
|
5649
|
+
valid: false,
|
|
5650
|
+
error: err instanceof Error ? err.message : "Network error"
|
|
5651
|
+
};
|
|
5652
|
+
}
|
|
5653
|
+
}, [settings]);
|
|
5654
|
+
return {
|
|
5655
|
+
settings,
|
|
5656
|
+
updateSettings,
|
|
5657
|
+
clearApiKey,
|
|
5658
|
+
status,
|
|
5659
|
+
testApiKey
|
|
5660
|
+
};
|
|
5661
|
+
}
|
|
5662
|
+
function buildLlmHeaders(settings) {
|
|
5663
|
+
const apiKey = settings.llmApiKey;
|
|
5664
|
+
const baseUrl = getEffectiveLlmBaseUrl(settings);
|
|
5665
|
+
const model = getEffectiveLlmModel(settings);
|
|
5666
|
+
if (apiKey) {
|
|
5667
|
+
return {
|
|
5668
|
+
"X-OpenAI-API-Key": apiKey,
|
|
5669
|
+
...baseUrl ? { "X-LLM-Base-URL": baseUrl } : {},
|
|
5670
|
+
...model ? { "X-LLM-Model": model } : {}
|
|
5671
|
+
};
|
|
5672
|
+
}
|
|
5673
|
+
return {
|
|
5674
|
+
...baseUrl ? { "X-LLM-Base-URL": baseUrl } : {},
|
|
5675
|
+
...model ? { "X-LLM-Model": model } : {}
|
|
5676
|
+
};
|
|
5677
|
+
}
|
|
5678
|
+
function getLlmHeaders() {
|
|
5679
|
+
const settings = useLlmSettingsStore.getState().settings;
|
|
5680
|
+
return buildLlmHeaders(settings);
|
|
5681
|
+
}
|
|
6112
5682
|
function useSmartTitle() {
|
|
6113
5683
|
const llmSettings = useLlmSettingsStore((state) => state.settings);
|
|
6114
5684
|
const [isGenerating, setIsGenerating] = useState(false);
|
|
@@ -6596,7 +6166,7 @@ const KeyboardShortcutsDialog = lazy(
|
|
|
6596
6166
|
}))
|
|
6597
6167
|
);
|
|
6598
6168
|
const SearchDialog = lazy(
|
|
6599
|
-
() => import("./search-dialog-
|
|
6169
|
+
() => import("./search-dialog-DR6zBnui.js").then((m) => ({
|
|
6600
6170
|
default: m.SearchDialog
|
|
6601
6171
|
}))
|
|
6602
6172
|
);
|
|
@@ -6623,7 +6193,9 @@ function ChatScreen({
|
|
|
6623
6193
|
const [showShortcutsHelp, setShowShortcutsHelp] = useState(false);
|
|
6624
6194
|
const [showSearchDialog, setShowSearchDialog] = useState(false);
|
|
6625
6195
|
const [searchMode, setSearchMode] = useState("global");
|
|
6626
|
-
const [searchJumpMessageId, setSearchJumpMessageId] = useState(
|
|
6196
|
+
const [searchJumpMessageId, setSearchJumpMessageId] = useState(
|
|
6197
|
+
null
|
|
6198
|
+
);
|
|
6627
6199
|
const [isStreaming, setIsStreaming] = useState(false);
|
|
6628
6200
|
const thinkingLevel = useThinkingLevelStore((state) => state.level);
|
|
6629
6201
|
const { maybeNotifyAssistantMessage } = useNotifications();
|
|
@@ -6735,8 +6307,8 @@ function ChatScreen({
|
|
|
6735
6307
|
const llmTitlesEnabled = useLlmTitlesEnabled();
|
|
6736
6308
|
const { generateTitle } = useSmartTitle();
|
|
6737
6309
|
const titleGeneratedRef = useRef(/* @__PURE__ */ new Set());
|
|
6738
|
-
const FAST_POLL_MS =
|
|
6739
|
-
const NORMAL_POLL_MS =
|
|
6310
|
+
const FAST_POLL_MS = 2e3;
|
|
6311
|
+
const NORMAL_POLL_MS = 2e3;
|
|
6740
6312
|
const streamStart = useCallback(() => {
|
|
6741
6313
|
if (!activeFriendlyId || isNewChat) return;
|
|
6742
6314
|
if (streamTimer.current) window.clearInterval(streamTimer.current);
|
|
@@ -6769,12 +6341,9 @@ function ChatScreen({
|
|
|
6769
6341
|
},
|
|
6770
6342
|
[historyQuery, queryClient, stopStream, streamFinish]
|
|
6771
6343
|
);
|
|
6772
|
-
handleStreamErrorRef.current = useCallback(
|
|
6773
|
-
(
|
|
6774
|
-
|
|
6775
|
-
},
|
|
6776
|
-
[]
|
|
6777
|
-
);
|
|
6344
|
+
handleStreamErrorRef.current = useCallback((_err) => {
|
|
6345
|
+
console.warn("[stream] SSE error, falling back to polling");
|
|
6346
|
+
}, []);
|
|
6778
6347
|
const streamingMessage = useMemo(() => {
|
|
6779
6348
|
if (!streaming.text) return null;
|
|
6780
6349
|
const content = [];
|
|
@@ -6917,7 +6486,9 @@ function ChatScreen({
|
|
|
6917
6486
|
if (!sessionKey) return;
|
|
6918
6487
|
if (titleGeneratedRef.current.has(sessionKey)) return;
|
|
6919
6488
|
const userMessages = historyMessages.filter((m) => m.role === "user");
|
|
6920
|
-
const assistantMessages = historyMessages.filter(
|
|
6489
|
+
const assistantMessages = historyMessages.filter(
|
|
6490
|
+
(m) => m.role === "assistant"
|
|
6491
|
+
);
|
|
6921
6492
|
if (userMessages.length === 0 || assistantMessages.length === 0) return;
|
|
6922
6493
|
if (userMessages.length !== 1) return;
|
|
6923
6494
|
const firstUserMessage = textFromMessage(userMessages[0]);
|
|
@@ -6927,7 +6498,12 @@ function ChatScreen({
|
|
|
6927
6498
|
try {
|
|
6928
6499
|
const result = await generateTitle(firstUserMessage);
|
|
6929
6500
|
if (result.title) {
|
|
6930
|
-
await updateSessionLabel(
|
|
6501
|
+
await updateSessionLabel(
|
|
6502
|
+
queryClient,
|
|
6503
|
+
sessionKey,
|
|
6504
|
+
activeFriendlyId,
|
|
6505
|
+
result.title
|
|
6506
|
+
);
|
|
6931
6507
|
}
|
|
6932
6508
|
} catch (err) {
|
|
6933
6509
|
console.error("[smart-title] Error generating title:", err);
|
|
@@ -7025,7 +6601,13 @@ function ChatScreen({
|
|
|
7025
6601
|
}
|
|
7026
6602
|
setWaitingForResponse(true);
|
|
7027
6603
|
setPinToTop(true);
|
|
7028
|
-
sendMessage(
|
|
6604
|
+
sendMessage(
|
|
6605
|
+
pending.sessionKey,
|
|
6606
|
+
pending.friendlyId,
|
|
6607
|
+
pending.message,
|
|
6608
|
+
true,
|
|
6609
|
+
pending.attachments
|
|
6610
|
+
);
|
|
7029
6611
|
}, [
|
|
7030
6612
|
activeFriendlyId,
|
|
7031
6613
|
activeSessionKey,
|
|
@@ -7037,7 +6619,10 @@ function ChatScreen({
|
|
|
7037
6619
|
function sendMessage(sessionKey, friendlyId, body, skipOptimistic = false, attachments, model) {
|
|
7038
6620
|
let optimisticClientId = "";
|
|
7039
6621
|
if (!skipOptimistic) {
|
|
7040
|
-
const { clientId, optimisticMessage } = createOptimisticMessage(
|
|
6622
|
+
const { clientId, optimisticMessage } = createOptimisticMessage(
|
|
6623
|
+
body,
|
|
6624
|
+
attachments
|
|
6625
|
+
);
|
|
7041
6626
|
optimisticClientId = clientId;
|
|
7042
6627
|
appendHistoryMessage(
|
|
7043
6628
|
queryClient,
|
|
@@ -7129,7 +6714,8 @@ function ChatScreen({
|
|
|
7129
6714
|
const send = useCallback(
|
|
7130
6715
|
(body, helpers) => {
|
|
7131
6716
|
const attachments = helpers.attachments;
|
|
7132
|
-
if (body.length === 0 && (!attachments || attachments.length === 0))
|
|
6717
|
+
if (body.length === 0 && (!attachments || attachments.length === 0))
|
|
6718
|
+
return;
|
|
7133
6719
|
helpers.reset();
|
|
7134
6720
|
if (isNewChat) {
|
|
7135
6721
|
const { clientId, optimisticId, optimisticMessage } = createOptimisticMessage(body, attachments);
|
|
@@ -7176,7 +6762,14 @@ function ChatScreen({
|
|
|
7176
6762
|
return;
|
|
7177
6763
|
}
|
|
7178
6764
|
const sessionKeyForSend = forcedSessionKey || resolvedSessionKey || activeSessionKey;
|
|
7179
|
-
sendMessage(
|
|
6765
|
+
sendMessage(
|
|
6766
|
+
sessionKeyForSend,
|
|
6767
|
+
activeFriendlyId,
|
|
6768
|
+
body,
|
|
6769
|
+
false,
|
|
6770
|
+
attachments,
|
|
6771
|
+
helpers.model
|
|
6772
|
+
);
|
|
7180
6773
|
},
|
|
7181
6774
|
[
|
|
7182
6775
|
activeFriendlyId,
|
|
@@ -7431,7 +7024,10 @@ function ChatScreen({
|
|
|
7431
7024
|
} catch {
|
|
7432
7025
|
}
|
|
7433
7026
|
}
|
|
7434
|
-
navigate({
|
|
7027
|
+
navigate({
|
|
7028
|
+
to: "/chat/$sessionKey",
|
|
7029
|
+
params: { sessionKey: result.friendlyId }
|
|
7030
|
+
});
|
|
7435
7031
|
}
|
|
7436
7032
|
}
|
|
7437
7033
|
) })
|
|
@@ -7467,7 +7063,12 @@ const $sessionKey = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineP
|
|
|
7467
7063
|
}, Symbol.toStringTag, { value: "Module" }));
|
|
7468
7064
|
export {
|
|
7469
7065
|
$sessionKey as $,
|
|
7066
|
+
Collapsible as C,
|
|
7067
|
+
useLlmSettingsStore as a,
|
|
7068
|
+
getLlmHeaders as b,
|
|
7470
7069
|
chatQueryKeys as c,
|
|
7070
|
+
CollapsibleTrigger as d,
|
|
7071
|
+
CollapsiblePanel as e,
|
|
7471
7072
|
getLlmProviderDefaults as g,
|
|
7472
7073
|
useLlmSettings as u
|
|
7473
7074
|
};
|