@parallelworks/ui 0.28.0 → 0.30.0
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 +9 -0
- package/dist/ai/components/ChatLayout.d.ts +11 -2
- package/dist/ai/components/ChatLayout.js +53 -5
- package/dist/ai/components/ChatSidebar.js +121 -187
- package/dist/ai/components/ModelSelector.js +5 -1
- package/dist/ai/core/ChatProvider.d.ts +18 -1
- package/dist/ai/core/ChatProvider.js +38 -28
- package/dist/ai/core/chatReducer.d.ts +0 -3
- package/dist/ai/core/chatReducer.js +0 -5
- package/dist/ai/core/sidebarState.d.ts +19 -0
- package/dist/ai/core/sidebarState.js +57 -0
- package/dist/ai/index.d.ts +4 -2
- package/dist/ai/index.js +4 -3
- package/dist/ai/strings.d.ts +8 -0
- package/dist/ai/strings.js +4 -0
- package/dist/ai/ui/ConversationSidebar.d.ts +61 -0
- package/dist/ai/ui/ConversationSidebar.js +165 -0
- package/dist/ai/ui/sidebar.d.ts +13 -2
- package/dist/ai/ui/sidebar.js +37 -2
- package/dist/styles.css +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -114,6 +114,15 @@ export function Chat() {
|
|
|
114
114
|
}
|
|
115
115
|
```
|
|
116
116
|
|
|
117
|
+
`ChatLayout sidebarMode="auto"` shows the conversation list as a drawer over
|
|
118
|
+
the thread when the chat is narrower than `drawerBelowPx` (768 by default),
|
|
119
|
+
measured on the chat's own box; `"drawer"` always does, and `"inline"` (the
|
|
120
|
+
default) never does. The reader's toggle drives the list unless the host
|
|
121
|
+
passes `sidebar` (`'expanded' | 'collapsed' | 'hidden'`) and `drawerOpen` to
|
|
122
|
+
`ChatProvider`, with `onSidebarChange` and `onDrawerOpenChange` to hear the
|
|
123
|
+
reader's requests. `useChat().toggleSidebar` opens or closes whichever the
|
|
124
|
+
layout shows, for a host header's own button.
|
|
125
|
+
|
|
117
126
|
Swap the mock for `createOpenAIChatAdapter({ baseUrl, apiKey })` from
|
|
118
127
|
`@parallelworks/ui/ai/openai`, or implement `ChatAdapter` against your own
|
|
119
128
|
backend. It requires `conversations`, `models` and `streamCompletion`; the
|
|
@@ -1,5 +1,14 @@
|
|
|
1
|
-
import type
|
|
2
|
-
|
|
1
|
+
import { type ReactNode } from 'react';
|
|
2
|
+
/** Where the conversation list goes: beside the thread, in a drawer over it,
|
|
3
|
+
* or whichever fits the width the chat is given. */
|
|
4
|
+
export type SidebarMode = 'inline' | 'drawer' | 'auto';
|
|
5
|
+
export declare const DRAWER_BELOW_PX = 768;
|
|
6
|
+
export default function ChatLayout({ children, sidebarActions, sidebarMode, drawerBelowPx, drawerToggle, }: {
|
|
3
7
|
children: ReactNode;
|
|
4
8
|
sidebarActions?: boolean;
|
|
9
|
+
sidebarMode?: SidebarMode;
|
|
10
|
+
/** The chat's width under which 'auto' shows the drawer. */
|
|
11
|
+
drawerBelowPx?: number;
|
|
12
|
+
/** False when the host's own header opens the drawer. */
|
|
13
|
+
drawerToggle?: boolean;
|
|
5
14
|
}): import("react").JSX.Element;
|
|
@@ -1,15 +1,63 @@
|
|
|
1
|
+
import { useChatConfig } from "../core/config.js";
|
|
2
|
+
import { useReportSidebarPresentation } from "../core/sidebarState.js";
|
|
3
|
+
import { useChat } from "../core/ChatProvider.js";
|
|
1
4
|
import { KeyboardShortcutsProvider } from "../core/KeyboardShortcutsProvider.js";
|
|
5
|
+
import { SidebarToggle } from "../ui/sidebar.js";
|
|
2
6
|
import ChatSidebar from "./ChatSidebar.js";
|
|
7
|
+
import cx from "classnames";
|
|
8
|
+
import { useLayoutEffect, useRef, useState } from "react";
|
|
3
9
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
4
10
|
//#region src/ai/components/ChatLayout.tsx
|
|
5
|
-
|
|
11
|
+
var DRAWER_BELOW_PX = 768;
|
|
12
|
+
function useSidebarPresentation(ref, mode, drawerBelowPx) {
|
|
13
|
+
const [narrow, setNarrow] = useState(false);
|
|
14
|
+
useLayoutEffect(() => {
|
|
15
|
+
const el = ref.current;
|
|
16
|
+
if (mode !== "auto" || !el) return;
|
|
17
|
+
const measure = () => setNarrow(el.clientWidth < drawerBelowPx);
|
|
18
|
+
measure();
|
|
19
|
+
if (typeof ResizeObserver === "undefined") return;
|
|
20
|
+
const observer = new ResizeObserver(measure);
|
|
21
|
+
observer.observe(el);
|
|
22
|
+
return () => observer.disconnect();
|
|
23
|
+
}, [
|
|
24
|
+
ref,
|
|
25
|
+
mode,
|
|
26
|
+
drawerBelowPx
|
|
27
|
+
]);
|
|
28
|
+
const presentation = mode === "drawer" || mode === "auto" && narrow ? "drawer" : "inline";
|
|
29
|
+
const report = useReportSidebarPresentation();
|
|
30
|
+
useLayoutEffect(() => {
|
|
31
|
+
report(presentation);
|
|
32
|
+
}, [report, presentation]);
|
|
33
|
+
useLayoutEffect(() => () => report("inline"), [report]);
|
|
34
|
+
return presentation;
|
|
35
|
+
}
|
|
36
|
+
function ChatLayout({ children, sidebarActions = true, sidebarMode = "inline", drawerBelowPx = 768, drawerToggle = true }) {
|
|
37
|
+
const ref = useRef(null);
|
|
38
|
+
const drawer = useSidebarPresentation(ref, sidebarMode, drawerBelowPx) === "drawer";
|
|
39
|
+
const { drawerOpen, toggleSidebar } = useChat();
|
|
40
|
+
const { strings } = useChatConfig();
|
|
6
41
|
return /* @__PURE__ */ jsx(KeyboardShortcutsProvider, { children: /* @__PURE__ */ jsxs("div", {
|
|
7
|
-
|
|
8
|
-
|
|
42
|
+
ref,
|
|
43
|
+
className: cx("flex h-full", drawer && "relative overflow-hidden"),
|
|
44
|
+
children: [/* @__PURE__ */ jsx(ChatSidebar, { actions: sidebarActions }), /* @__PURE__ */ jsxs("main", {
|
|
45
|
+
inert: drawer && drawerOpen,
|
|
9
46
|
className: "flex-1 flex flex-col min-w-0",
|
|
10
|
-
children
|
|
47
|
+
children: [drawer && drawerToggle && /* @__PURE__ */ jsx("div", {
|
|
48
|
+
className: "flex shrink-0 items-center bg-(--theme-panel-bg) px-2 pt-2",
|
|
49
|
+
children: /* @__PURE__ */ jsx(SidebarToggle, {
|
|
50
|
+
collapsed: !drawerOpen,
|
|
51
|
+
onToggle: toggleSidebar,
|
|
52
|
+
openLabel: strings.sidebar.openSidebar,
|
|
53
|
+
closeLabel: strings.sidebar.closeSidebar
|
|
54
|
+
})
|
|
55
|
+
}), /* @__PURE__ */ jsx("div", {
|
|
56
|
+
className: "flex min-h-0 flex-1 flex-col",
|
|
57
|
+
children
|
|
58
|
+
})]
|
|
11
59
|
})]
|
|
12
60
|
}) });
|
|
13
61
|
}
|
|
14
62
|
//#endregion
|
|
15
|
-
export { ChatLayout as default };
|
|
63
|
+
export { DRAWER_BELOW_PX, ChatLayout as default };
|
|
@@ -7,19 +7,20 @@ import { FaUsers } from "../../icons/fa/FaUsers.js";
|
|
|
7
7
|
import { IoSettingsOutline } from "../../icons/io5/IoSettingsOutline.js";
|
|
8
8
|
import { PiShareBold } from "../../icons/pi/PiShareBold.js";
|
|
9
9
|
import { TbEdit } from "../../icons/tb/TbEdit.js";
|
|
10
|
-
import { positionKeys } from "../../components/keys.js";
|
|
11
10
|
import { useRowMenu } from "../../list/RowContextMenu.js";
|
|
12
11
|
import { useChatConfig } from "../core/config.js";
|
|
13
12
|
import { useChat } from "../core/ChatProvider.js";
|
|
14
13
|
import { FOCUS_SIDEBAR_SEARCH_EVENT } from "../core/events.js";
|
|
14
|
+
import { SidebarDrawer, SidebarRow, useRowDialogs } from "../ui/sidebar.js";
|
|
15
15
|
import { conversationToMarkdown, downloadText, exportFilename } from "../core/exportConversation.js";
|
|
16
|
-
import {
|
|
16
|
+
import { ConversationSidebar } from "../ui/ConversationSidebar.js";
|
|
17
17
|
import ShareDialog from "./ShareDialog.js";
|
|
18
18
|
import cx from "classnames";
|
|
19
19
|
import { useEffect, useMemo, useRef, useState } from "react";
|
|
20
20
|
import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
|
|
21
21
|
import { DateTime } from "luxon";
|
|
22
22
|
//#region src/ai/components/ChatSidebar.tsx
|
|
23
|
+
var SIDEBAR_WIDTH_STORAGE_KEY = "aiChatSidebarWidth";
|
|
23
24
|
function groupByDate(conversations) {
|
|
24
25
|
const now = DateTime.now();
|
|
25
26
|
const groups = {
|
|
@@ -40,7 +41,7 @@ function groupByDate(conversations) {
|
|
|
40
41
|
}
|
|
41
42
|
return Object.entries(groups).filter(([, items]) => items.length > 0);
|
|
42
43
|
}
|
|
43
|
-
function ConversationRow({ conv, isCurrent, sharingAvailable, openMenu, onRename, onShare, onDelete, onExport }) {
|
|
44
|
+
function ConversationRow({ conv, isCurrent, sharingAvailable, openMenu, onRename, onShare, onDelete, onExport, onOpen }) {
|
|
44
45
|
const { LinkComponent, strings } = useChatConfig();
|
|
45
46
|
const t = strings.sidebar;
|
|
46
47
|
const menuItems = [
|
|
@@ -80,6 +81,7 @@ function ConversationRow({ conv, isCurrent, sharingAvailable, openMenu, onRename
|
|
|
80
81
|
id: conv.id
|
|
81
82
|
},
|
|
82
83
|
className: "flex min-w-0 flex-1 items-center gap-1.5 py-1 text-left",
|
|
84
|
+
onClick: onOpen,
|
|
83
85
|
children: [conv.isOwner === false && /* @__PURE__ */ jsx(FaUsers, {
|
|
84
86
|
className: "h-3 w-3 shrink-0 theme-muted-text",
|
|
85
87
|
title: t.sharedWithYou
|
|
@@ -90,68 +92,10 @@ function ConversationRow({ conv, isCurrent, sharingAvailable, openMenu, onRename
|
|
|
90
92
|
})
|
|
91
93
|
});
|
|
92
94
|
}
|
|
93
|
-
var SIDEBAR_WIDTH_STORAGE_KEY = "aiChatSidebarWidth";
|
|
94
|
-
var SIDEBAR_MIN_WIDTH_PX = 200;
|
|
95
|
-
var SIDEBAR_MAX_WIDTH_PX = 480;
|
|
96
|
-
function clampSidebarWidth(width) {
|
|
97
|
-
return Math.min(SIDEBAR_MAX_WIDTH_PX, Math.max(SIDEBAR_MIN_WIDTH_PX, width));
|
|
98
|
-
}
|
|
99
|
-
function readStoredSidebarWidth() {
|
|
100
|
-
try {
|
|
101
|
-
const stored = Number(window.localStorage.getItem(SIDEBAR_WIDTH_STORAGE_KEY));
|
|
102
|
-
return Number.isFinite(stored) && stored > 0 ? clampSidebarWidth(stored) : 256;
|
|
103
|
-
} catch {
|
|
104
|
-
return 256;
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
function storeSidebarWidth(width) {
|
|
108
|
-
try {
|
|
109
|
-
window.localStorage.setItem(SIDEBAR_WIDTH_STORAGE_KEY, String(width));
|
|
110
|
-
} catch {}
|
|
111
|
-
}
|
|
112
|
-
function useResizableSidebarWidth() {
|
|
113
|
-
const [width, setWidth] = useState(readStoredSidebarWidth);
|
|
114
|
-
const [resizing, setResizing] = useState(false);
|
|
115
|
-
const widthRef = useRef(width);
|
|
116
|
-
widthRef.current = width;
|
|
117
|
-
const startResize = (e) => {
|
|
118
|
-
e.preventDefault();
|
|
119
|
-
const startX = e.clientX;
|
|
120
|
-
const startWidth = widthRef.current;
|
|
121
|
-
setResizing(true);
|
|
122
|
-
const onMove = (ev) => {
|
|
123
|
-
setWidth(clampSidebarWidth(startWidth + ev.clientX - startX));
|
|
124
|
-
};
|
|
125
|
-
const onUp = () => {
|
|
126
|
-
window.removeEventListener("pointermove", onMove);
|
|
127
|
-
window.removeEventListener("pointerup", onUp);
|
|
128
|
-
setResizing(false);
|
|
129
|
-
storeSidebarWidth(widthRef.current);
|
|
130
|
-
};
|
|
131
|
-
window.addEventListener("pointermove", onMove);
|
|
132
|
-
window.addEventListener("pointerup", onUp);
|
|
133
|
-
};
|
|
134
|
-
const reset = () => {
|
|
135
|
-
setWidth(256);
|
|
136
|
-
storeSidebarWidth(256);
|
|
137
|
-
};
|
|
138
|
-
const nudge = (delta) => {
|
|
139
|
-
const next = clampSidebarWidth(widthRef.current + delta);
|
|
140
|
-
setWidth(next);
|
|
141
|
-
storeSidebarWidth(next);
|
|
142
|
-
};
|
|
143
|
-
return {
|
|
144
|
-
width,
|
|
145
|
-
resizing,
|
|
146
|
-
startResize,
|
|
147
|
-
reset,
|
|
148
|
-
nudge
|
|
149
|
-
};
|
|
150
|
-
}
|
|
151
95
|
function ChatSidebar({ actions = true }) {
|
|
152
96
|
const { extraLinks, LinkComponent, strings } = useChatConfig();
|
|
153
97
|
const tSidebar = strings.sidebar;
|
|
154
|
-
const { adapter, navigation, notify, activeConversationId, conversations,
|
|
98
|
+
const { adapter, navigation, notify, activeConversationId, conversations, sidebar, sidebarPresentation, drawerOpen, setSidebar, setDrawerOpen, toggleSidebar, isLoading, loadConversations, deleteConversation, updateConversationTitle, clearCurrentConversation } = useChat();
|
|
155
99
|
const sharingAvailable = !!adapter.sharing;
|
|
156
100
|
const attachmentsAvailable = !!adapter.attachments;
|
|
157
101
|
const [shareDialogOpen, setShareDialogOpen] = useState(false);
|
|
@@ -171,23 +115,28 @@ function ChatSidebar({ actions = true }) {
|
|
|
171
115
|
onDelete: (conv) => deleteConversation(conv.id),
|
|
172
116
|
onRename: (conv, title) => updateConversationTitle(conv.id, title)
|
|
173
117
|
});
|
|
174
|
-
const
|
|
118
|
+
const drawer = sidebarPresentation === "drawer";
|
|
119
|
+
const sidebarCollapsed = !drawer && sidebar !== "expanded";
|
|
120
|
+
const closeDrawer = () => setDrawerOpen(false);
|
|
175
121
|
const [search, setSearch] = useState("");
|
|
176
122
|
const searchInputRef = useRef(null);
|
|
177
|
-
const
|
|
178
|
-
|
|
123
|
+
const revealRef = useRef(() => {});
|
|
124
|
+
revealRef.current = () => {
|
|
125
|
+
if (drawer) setDrawerOpen(true);
|
|
126
|
+
else if (sidebar !== "expanded") setSidebar("expanded");
|
|
127
|
+
};
|
|
179
128
|
useEffect(() => {
|
|
180
129
|
loadConversations();
|
|
181
130
|
}, [loadConversations]);
|
|
182
131
|
useEffect(() => {
|
|
183
132
|
if (!actions) return;
|
|
184
133
|
const focusSearch = () => {
|
|
185
|
-
|
|
134
|
+
revealRef.current();
|
|
186
135
|
requestAnimationFrame(() => searchInputRef.current?.focus());
|
|
187
136
|
};
|
|
188
137
|
document.addEventListener(FOCUS_SIDEBAR_SEARCH_EVENT, focusSearch);
|
|
189
138
|
return () => document.removeEventListener(FOCUS_SIDEBAR_SEARCH_EVENT, focusSearch);
|
|
190
|
-
}, [actions
|
|
139
|
+
}, [actions]);
|
|
191
140
|
const filteredConversations = useMemo(() => {
|
|
192
141
|
const query = search.trim().toLowerCase();
|
|
193
142
|
if (!query) return conversations;
|
|
@@ -197,7 +146,11 @@ function ChatSidebar({ actions = true }) {
|
|
|
197
146
|
search,
|
|
198
147
|
tSidebar.untitled
|
|
199
148
|
]);
|
|
200
|
-
const
|
|
149
|
+
const groups = useMemo(() => groupByDate(filteredConversations).map(([key, items]) => ({
|
|
150
|
+
key,
|
|
151
|
+
label: tSidebar[key],
|
|
152
|
+
items
|
|
153
|
+
})), [filteredConversations, tSidebar]);
|
|
201
154
|
const handleExport = async (id) => {
|
|
202
155
|
try {
|
|
203
156
|
const conversation = await adapter.conversations.get(id);
|
|
@@ -211,130 +164,111 @@ function ChatSidebar({ actions = true }) {
|
|
|
211
164
|
setShareDialogOpen(true);
|
|
212
165
|
};
|
|
213
166
|
const handleNewChat = () => {
|
|
167
|
+
closeDrawer();
|
|
214
168
|
clearCurrentConversation();
|
|
215
169
|
navigation.toNewChat();
|
|
216
170
|
};
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
171
|
+
const list = /* @__PURE__ */ jsx(ConversationSidebar, {
|
|
172
|
+
collapsed: sidebarCollapsed,
|
|
173
|
+
fill: drawer,
|
|
174
|
+
onToggle: toggleSidebar,
|
|
175
|
+
toggleLabels: {
|
|
176
|
+
open: tSidebar.openSidebar,
|
|
177
|
+
close: tSidebar.closeSidebar
|
|
178
|
+
},
|
|
179
|
+
resize: {
|
|
180
|
+
storageKey: SIDEBAR_WIDTH_STORAGE_KEY,
|
|
181
|
+
label: tSidebar.resizeLabel,
|
|
182
|
+
hint: tSidebar.resizeHint
|
|
183
|
+
},
|
|
184
|
+
groups,
|
|
185
|
+
getKey: (conv) => conv.id,
|
|
186
|
+
renderRow: (conv) => /* @__PURE__ */ jsx(ConversationRow, {
|
|
187
|
+
conv,
|
|
188
|
+
isCurrent: activeConversationId === conv.id,
|
|
189
|
+
sharingAvailable,
|
|
190
|
+
openMenu,
|
|
191
|
+
onRename: requestRename,
|
|
192
|
+
onShare: handleShareClick,
|
|
193
|
+
onDelete: requestDelete,
|
|
194
|
+
onExport: handleExport,
|
|
195
|
+
onOpen: closeDrawer
|
|
196
|
+
}),
|
|
197
|
+
loading: isLoading,
|
|
198
|
+
placeholder: conversations.length === 0 ? /* @__PURE__ */ jsx("p", {
|
|
199
|
+
className: "theme-muted-text text-sm px-2 whitespace-nowrap",
|
|
200
|
+
children: tSidebar.noConversations
|
|
201
|
+
}) : /* @__PURE__ */ jsx("p", {
|
|
202
|
+
className: "theme-muted-text text-sm px-2",
|
|
203
|
+
children: strings.chrome.noMatches
|
|
204
|
+
}),
|
|
205
|
+
cycle: {
|
|
206
|
+
current: filteredConversations.find((conv) => conv.id === activeConversationId),
|
|
207
|
+
onSelect: (conv) => navigation.toConversation(conv.id)
|
|
208
|
+
},
|
|
209
|
+
top: actions && /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsxs("div", {
|
|
210
|
+
className: "flex flex-col gap-0.5 px-2 pt-2 pb-1",
|
|
211
|
+
children: [/* @__PURE__ */ jsxs("button", {
|
|
212
|
+
type: "button",
|
|
213
|
+
onClick: handleNewChat,
|
|
214
|
+
className: "flex items-center h-9 px-2 gap-3 rounded-lg font-medium transition-colors hover:chat-tint",
|
|
215
|
+
title: tSidebar.newChat,
|
|
216
|
+
children: [/* @__PURE__ */ jsx(TbEdit, { className: cx("flex-shrink-0", "w-4 h-4") }), /* @__PURE__ */ jsx("span", {
|
|
217
|
+
className: cx("text-sm transition-opacity duration-200", sidebarCollapsed ? "opacity-0 w-0 overflow-hidden" : "opacity-100"),
|
|
218
|
+
children: tSidebar.newChat
|
|
219
|
+
})]
|
|
220
|
+
}), attachmentsAvailable && /* @__PURE__ */ jsxs(LinkComponent, {
|
|
221
|
+
target: { kind: "attachments" },
|
|
222
|
+
className: "flex items-center h-9 px-2 gap-3 rounded-lg transition-colors hover:chat-tint",
|
|
223
|
+
title: tSidebar.attachments,
|
|
224
|
+
onClick: closeDrawer,
|
|
225
|
+
children: [/* @__PURE__ */ jsx(FaRegFileImage, { className: "flex-shrink-0 w-4 h-4" }), /* @__PURE__ */ jsx("span", {
|
|
226
|
+
className: cx("text-sm transition-opacity duration-200", sidebarCollapsed ? "opacity-0 w-0 overflow-hidden" : "opacity-100"),
|
|
227
|
+
children: tSidebar.attachments
|
|
228
|
+
})]
|
|
229
|
+
})]
|
|
230
|
+
}), !sidebarCollapsed && conversations.length > 0 && /* @__PURE__ */ jsx("div", {
|
|
231
|
+
className: "px-2 pb-2",
|
|
232
|
+
children: /* @__PURE__ */ jsxs("div", {
|
|
233
|
+
className: "relative",
|
|
234
|
+
children: [/* @__PURE__ */ jsx(FaSearch, { className: "absolute left-2.5 top-1/2 -translate-y-1/2 w-3.5 h-3.5 theme-muted-text pointer-events-none" }), /* @__PURE__ */ jsx("input", {
|
|
235
|
+
ref: searchInputRef,
|
|
236
|
+
type: "text",
|
|
237
|
+
value: search,
|
|
238
|
+
onChange: (e) => setSearch(e.target.value),
|
|
234
239
|
onKeyDown: (e) => {
|
|
235
|
-
if (e.key === "
|
|
236
|
-
|
|
240
|
+
if (e.key === "Escape" && search) {
|
|
241
|
+
e.stopPropagation();
|
|
242
|
+
setSearch("");
|
|
243
|
+
}
|
|
237
244
|
},
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
children: [/* @__PURE__ */ jsx(FaSearch, { className: "absolute left-2.5 top-1/2 -translate-y-1/2 w-3.5 h-3.5 theme-muted-text pointer-events-none" }), /* @__PURE__ */ jsx("input", {
|
|
266
|
-
ref: searchInputRef,
|
|
267
|
-
type: "text",
|
|
268
|
-
value: search,
|
|
269
|
-
onChange: (e) => setSearch(e.target.value),
|
|
270
|
-
onKeyDown: (e) => {
|
|
271
|
-
if (e.key === "Escape" && search) {
|
|
272
|
-
e.stopPropagation();
|
|
273
|
-
setSearch("");
|
|
274
|
-
}
|
|
275
|
-
},
|
|
276
|
-
placeholder: strings.chrome.searchPlaceholder,
|
|
277
|
-
"aria-label": strings.chrome.searchLabel,
|
|
278
|
-
className: "w-full h-9 pl-8 pr-2 text-sm rounded-lg border border-transparent chat-tint theme-text placeholder:theme-muted-text transition-colors focus:outline-none focus:border-(--theme-border) focus:bg-(--theme-panel-bg)"
|
|
279
|
-
})]
|
|
280
|
-
})
|
|
281
|
-
}),
|
|
282
|
-
/* @__PURE__ */ jsx("div", {
|
|
283
|
-
className: cx("min-h-0 flex-1 overflow-y-auto px-2 pb-2 transition-opacity duration-200", sidebarCollapsed ? "opacity-0 pointer-events-none" : "opacity-100"),
|
|
284
|
-
children: isLoading ? /* @__PURE__ */ jsx("div", {
|
|
285
|
-
className: "space-y-3 px-2 pt-2",
|
|
286
|
-
children: positionKeys(5, "skeleton").map((key, i) => /* @__PURE__ */ jsx("div", {
|
|
287
|
-
className: "animate-pulse",
|
|
288
|
-
children: /* @__PURE__ */ jsx("div", {
|
|
289
|
-
className: "h-4 rounded theme-muted-panel",
|
|
290
|
-
style: { width: `${70 - i * 10}%` }
|
|
291
|
-
})
|
|
292
|
-
}, key))
|
|
293
|
-
}) : conversations.length === 0 ? /* @__PURE__ */ jsx("p", {
|
|
294
|
-
className: "theme-muted-text text-sm px-2 whitespace-nowrap",
|
|
295
|
-
children: tSidebar.noConversations
|
|
296
|
-
}) : filteredConversations.length === 0 ? /* @__PURE__ */ jsx("p", {
|
|
297
|
-
className: "theme-muted-text text-sm px-2",
|
|
298
|
-
children: strings.chrome.noMatches
|
|
299
|
-
}) : groupedConversations.map(([label, convs]) => /* @__PURE__ */ jsxs("div", {
|
|
300
|
-
className: "mb-2",
|
|
301
|
-
children: [/* @__PURE__ */ jsx(SidebarGroupHeading, { children: tSidebar[label] }), /* @__PURE__ */ jsx("ul", {
|
|
302
|
-
className: "space-y-0.5",
|
|
303
|
-
children: convs.map((conv) => /* @__PURE__ */ jsx(ConversationRow, {
|
|
304
|
-
conv,
|
|
305
|
-
isCurrent: activeConversationId === conv.id,
|
|
306
|
-
sharingAvailable,
|
|
307
|
-
openMenu,
|
|
308
|
-
onRename: requestRename,
|
|
309
|
-
onShare: handleShareClick,
|
|
310
|
-
onDelete: requestDelete,
|
|
311
|
-
onExport: handleExport
|
|
312
|
-
}, conv.id))
|
|
313
|
-
})]
|
|
314
|
-
}, label))
|
|
315
|
-
}),
|
|
316
|
-
/* @__PURE__ */ jsxs("div", {
|
|
317
|
-
className: cx("flex shrink-0 items-center gap-1 p-1.5", sidebarCollapsed ? "justify-center" : extraLinks.manageProviders ? "justify-between" : "justify-end"),
|
|
318
|
-
children: [!sidebarCollapsed && extraLinks.manageProviders && /* @__PURE__ */ jsxs(LinkComponent, {
|
|
319
|
-
target: {
|
|
320
|
-
kind: "external",
|
|
321
|
-
href: extraLinks.manageProviders
|
|
322
|
-
},
|
|
323
|
-
className: "flex min-w-0 items-center gap-2 rounded-md px-2 py-1.5 text-xs theme-muted-text transition-colors hover:chat-tint hover:theme-text",
|
|
324
|
-
title: tSidebar.manageProviders,
|
|
325
|
-
children: [/* @__PURE__ */ jsx(IoSettingsOutline, { className: "h-3.5 w-3.5 shrink-0" }), /* @__PURE__ */ jsx("span", {
|
|
326
|
-
className: "truncate",
|
|
327
|
-
children: tSidebar.manageProviders
|
|
328
|
-
})]
|
|
329
|
-
}), /* @__PURE__ */ jsx(SidebarToggle, {
|
|
330
|
-
collapsed: sidebarCollapsed,
|
|
331
|
-
onToggle: toggleSidebar,
|
|
332
|
-
openLabel: tSidebar.openSidebar,
|
|
333
|
-
closeLabel: tSidebar.closeSidebar
|
|
334
|
-
})]
|
|
335
|
-
})
|
|
336
|
-
]
|
|
337
|
-
}),
|
|
245
|
+
placeholder: strings.chrome.searchPlaceholder,
|
|
246
|
+
"aria-label": strings.chrome.searchLabel,
|
|
247
|
+
className: "w-full h-9 pl-8 pr-2 text-sm rounded-lg border border-transparent chat-tint theme-text placeholder:theme-muted-text transition-colors focus:outline-none focus:border-(--theme-border) focus:bg-(--theme-panel-bg)"
|
|
248
|
+
})]
|
|
249
|
+
})
|
|
250
|
+
})] }),
|
|
251
|
+
footer: extraLinks.manageProviders && /* @__PURE__ */ jsxs(LinkComponent, {
|
|
252
|
+
target: {
|
|
253
|
+
kind: "external",
|
|
254
|
+
href: extraLinks.manageProviders
|
|
255
|
+
},
|
|
256
|
+
className: "flex min-w-0 items-center gap-2 rounded-md px-2 py-1.5 text-xs theme-muted-text transition-colors hover:chat-tint hover:theme-text",
|
|
257
|
+
title: tSidebar.manageProviders,
|
|
258
|
+
children: [/* @__PURE__ */ jsx(IoSettingsOutline, { className: "h-3.5 w-3.5 shrink-0" }), /* @__PURE__ */ jsx("span", {
|
|
259
|
+
className: "truncate",
|
|
260
|
+
children: tSidebar.manageProviders
|
|
261
|
+
})]
|
|
262
|
+
})
|
|
263
|
+
});
|
|
264
|
+
return /* @__PURE__ */ jsxs(Fragment$1, { children: [
|
|
265
|
+
drawer ? /* @__PURE__ */ jsx(SidebarDrawer, {
|
|
266
|
+
open: drawerOpen,
|
|
267
|
+
onClose: closeDrawer,
|
|
268
|
+
label: tSidebar.drawerLabel,
|
|
269
|
+
closeLabel: tSidebar.closeSidebar,
|
|
270
|
+
children: list
|
|
271
|
+
}) : sidebar !== "hidden" && list,
|
|
338
272
|
contextMenu,
|
|
339
273
|
dialogs,
|
|
340
274
|
conversationToShare && /* @__PURE__ */ jsx(ShareDialog, {
|
|
@@ -13,6 +13,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
|
13
13
|
import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
|
|
14
14
|
import { createPortal } from "react-dom";
|
|
15
15
|
//#region src/ai/components/ModelSelector.tsx
|
|
16
|
+
var ORG_OWNER = "org";
|
|
16
17
|
/** The search field plus the list's max-h-80, so the flip decision matches
|
|
17
18
|
* what renders. */
|
|
18
19
|
var MAX_PANEL_HEIGHT_PX = 380;
|
|
@@ -181,6 +182,8 @@ function ModelSelector({ targetSession, variant = "default" }) {
|
|
|
181
182
|
ref: buttonRef,
|
|
182
183
|
type: "button",
|
|
183
184
|
onClick: () => setIsOpen(!isOpen),
|
|
185
|
+
"aria-haspopup": "dialog",
|
|
186
|
+
"aria-expanded": isOpen,
|
|
184
187
|
disabled: models.length === 0 && !targetSessionStatus && unreachableSessions.length === 0,
|
|
185
188
|
className: cx(boxClass, "transition-all cursor-pointer bg-transparent", "focus:outline-none", "disabled:opacity-50 disabled:cursor-not-allowed", bare ? "hover:chat-tint" : "border theme-border hover:theme-hover"),
|
|
186
189
|
children: [
|
|
@@ -299,6 +302,7 @@ function ModelSelector({ targetSession, variant = "default" }) {
|
|
|
299
302
|
const platform = providerInfo?.platformLabel;
|
|
300
303
|
const platformSuffix = providerInfo && platform && platform.toLowerCase() !== providerName.toLowerCase() ? platform : null;
|
|
301
304
|
const isOwner = providerInfo?.user === currentUser.username;
|
|
305
|
+
const headerSuffix = [platformSuffix, providerInfo && !isOwner && providerInfo.user !== ORG_OWNER ? strings.modelPicker.sharedBy(providerInfo.user) : null].filter(Boolean).join(" · ");
|
|
302
306
|
const settingsHref = providerInfo && extraLinks.providerSettings ? extraLinks.providerSettings(providerInfo.user, providerInfo.name) : null;
|
|
303
307
|
const issue = providerIssueFor(providerIssues, providerModels[0]);
|
|
304
308
|
return /* @__PURE__ */ jsxs("div", { children: [
|
|
@@ -317,7 +321,7 @@ function ModelSelector({ targetSession, variant = "default" }) {
|
|
|
317
321
|
}),
|
|
318
322
|
/* @__PURE__ */ jsx("span", {
|
|
319
323
|
className: "min-w-0 flex-1 truncate text-xs theme-muted-text",
|
|
320
|
-
children:
|
|
324
|
+
children: headerSuffix ? `· ${headerSuffix}` : ""
|
|
321
325
|
}),
|
|
322
326
|
/* @__PURE__ */ jsxs("span", {
|
|
323
327
|
className: "text-xs theme-muted-text",
|
|
@@ -2,6 +2,7 @@ import { type ReactNode } from 'react';
|
|
|
2
2
|
import type { ChatAdapter } from '../adapter/types.js';
|
|
3
3
|
import type { AttachmentMeta, ChatModel, ChatNavigation, ChatNotify, ChatUser, Conversation, ConversationSummary, ChatMessage as Message, MessagePart, ProviderInfo, ProviderIssue, UnreachableSession } from '../types.js';
|
|
4
4
|
import { type ChatUIConfig } from './config.js';
|
|
5
|
+
import { type SidebarPresentation, type SidebarState } from './sidebarState.js';
|
|
5
6
|
interface ChatContextValue {
|
|
6
7
|
adapter: ChatAdapter;
|
|
7
8
|
currentUser: ChatUser;
|
|
@@ -28,7 +29,11 @@ interface ChatContextValue {
|
|
|
28
29
|
streamingReasoning: string;
|
|
29
30
|
streamingParts: MessagePart[];
|
|
30
31
|
thinkingStartTime: number | null;
|
|
32
|
+
sidebar: SidebarState;
|
|
33
|
+
/** True unless the sidebar is expanded. */
|
|
31
34
|
sidebarCollapsed: boolean;
|
|
35
|
+
sidebarPresentation: SidebarPresentation;
|
|
36
|
+
drawerOpen: boolean;
|
|
32
37
|
queuedMessages: Message[];
|
|
33
38
|
loadConversations: () => Promise<void>;
|
|
34
39
|
loadConversation: (id: string) => Promise<void>;
|
|
@@ -44,6 +49,10 @@ interface ChatContextValue {
|
|
|
44
49
|
stopStreaming: () => void;
|
|
45
50
|
removeQueuedMessage: (id: string) => void;
|
|
46
51
|
flushQueuedMessages: () => void;
|
|
52
|
+
setSidebar: (sidebar: SidebarState) => void;
|
|
53
|
+
setDrawerOpen: (open: boolean) => void;
|
|
54
|
+
/** Opens or closes the drawer while the layout shows one; otherwise moves
|
|
55
|
+
* between expanded and the rail. */
|
|
47
56
|
toggleSidebar: () => void;
|
|
48
57
|
updateConversationTitle: (id: string, title: string) => Promise<boolean>;
|
|
49
58
|
navigateToBranch: (messageId: string) => void;
|
|
@@ -57,7 +66,15 @@ export interface ChatProviderProps {
|
|
|
57
66
|
notify: ChatNotify;
|
|
58
67
|
activeConversationId?: string | null;
|
|
59
68
|
config?: ChatUIConfig;
|
|
69
|
+
/** Owns the sidebar's state. Omit to leave it to the reader, remembered
|
|
70
|
+
* per browser. */
|
|
71
|
+
sidebar?: SidebarState;
|
|
72
|
+
onSidebarChange?: (sidebar: SidebarState) => void;
|
|
73
|
+
/** Owns whether the drawer is open while the layout shows the sidebar as
|
|
74
|
+
* one. Omit to leave it to the reader; it starts closed. */
|
|
75
|
+
drawerOpen?: boolean;
|
|
76
|
+
onDrawerOpenChange?: (open: boolean) => void;
|
|
60
77
|
children: ReactNode;
|
|
61
78
|
}
|
|
62
|
-
export declare function ChatProvider({ adapter, currentUser, navigation, notify, activeConversationId, config, children, }: ChatProviderProps): import("react").JSX.Element;
|
|
79
|
+
export declare function ChatProvider({ adapter, currentUser, navigation, notify, activeConversationId, config, sidebar: sidebarProp, onSidebarChange, drawerOpen: drawerOpenProp, onDrawerOpenChange, children, }: ChatProviderProps): import("react").JSX.Element;
|
|
63
80
|
export {};
|