@parallelworks/ui 0.29.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 +113 -95
- 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 +3 -2
- package/dist/ai/index.js +3 -3
- package/dist/ai/strings.d.ts +6 -0
- package/dist/ai/strings.js +2 -0
- package/dist/ai/ui/ConversationSidebar.d.ts +4 -1
- package/dist/ai/ui/ConversationSidebar.js +6 -3
- package/dist/ai/ui/sidebar.d.ts +11 -0
- 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 };
|
|
@@ -11,8 +11,8 @@ import { useRowMenu } from "../../list/RowContextMenu.js";
|
|
|
11
11
|
import { useChatConfig } from "../core/config.js";
|
|
12
12
|
import { useChat } from "../core/ChatProvider.js";
|
|
13
13
|
import { FOCUS_SIDEBAR_SEARCH_EVENT } from "../core/events.js";
|
|
14
|
+
import { SidebarDrawer, SidebarRow, useRowDialogs } from "../ui/sidebar.js";
|
|
14
15
|
import { conversationToMarkdown, downloadText, exportFilename } from "../core/exportConversation.js";
|
|
15
|
-
import { SidebarRow, useRowDialogs } from "../ui/sidebar.js";
|
|
16
16
|
import { ConversationSidebar } from "../ui/ConversationSidebar.js";
|
|
17
17
|
import ShareDialog from "./ShareDialog.js";
|
|
18
18
|
import cx from "classnames";
|
|
@@ -41,7 +41,7 @@ function groupByDate(conversations) {
|
|
|
41
41
|
}
|
|
42
42
|
return Object.entries(groups).filter(([, items]) => items.length > 0);
|
|
43
43
|
}
|
|
44
|
-
function ConversationRow({ conv, isCurrent, sharingAvailable, openMenu, onRename, onShare, onDelete, onExport }) {
|
|
44
|
+
function ConversationRow({ conv, isCurrent, sharingAvailable, openMenu, onRename, onShare, onDelete, onExport, onOpen }) {
|
|
45
45
|
const { LinkComponent, strings } = useChatConfig();
|
|
46
46
|
const t = strings.sidebar;
|
|
47
47
|
const menuItems = [
|
|
@@ -81,6 +81,7 @@ function ConversationRow({ conv, isCurrent, sharingAvailable, openMenu, onRename
|
|
|
81
81
|
id: conv.id
|
|
82
82
|
},
|
|
83
83
|
className: "flex min-w-0 flex-1 items-center gap-1.5 py-1 text-left",
|
|
84
|
+
onClick: onOpen,
|
|
84
85
|
children: [conv.isOwner === false && /* @__PURE__ */ jsx(FaUsers, {
|
|
85
86
|
className: "h-3 w-3 shrink-0 theme-muted-text",
|
|
86
87
|
title: t.sharedWithYou
|
|
@@ -94,7 +95,7 @@ function ConversationRow({ conv, isCurrent, sharingAvailable, openMenu, onRename
|
|
|
94
95
|
function ChatSidebar({ actions = true }) {
|
|
95
96
|
const { extraLinks, LinkComponent, strings } = useChatConfig();
|
|
96
97
|
const tSidebar = strings.sidebar;
|
|
97
|
-
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();
|
|
98
99
|
const sharingAvailable = !!adapter.sharing;
|
|
99
100
|
const attachmentsAvailable = !!adapter.attachments;
|
|
100
101
|
const [shareDialogOpen, setShareDialogOpen] = useState(false);
|
|
@@ -114,22 +115,28 @@ function ChatSidebar({ actions = true }) {
|
|
|
114
115
|
onDelete: (conv) => deleteConversation(conv.id),
|
|
115
116
|
onRename: (conv, title) => updateConversationTitle(conv.id, title)
|
|
116
117
|
});
|
|
118
|
+
const drawer = sidebarPresentation === "drawer";
|
|
119
|
+
const sidebarCollapsed = !drawer && sidebar !== "expanded";
|
|
120
|
+
const closeDrawer = () => setDrawerOpen(false);
|
|
117
121
|
const [search, setSearch] = useState("");
|
|
118
122
|
const searchInputRef = useRef(null);
|
|
119
|
-
const
|
|
120
|
-
|
|
123
|
+
const revealRef = useRef(() => {});
|
|
124
|
+
revealRef.current = () => {
|
|
125
|
+
if (drawer) setDrawerOpen(true);
|
|
126
|
+
else if (sidebar !== "expanded") setSidebar("expanded");
|
|
127
|
+
};
|
|
121
128
|
useEffect(() => {
|
|
122
129
|
loadConversations();
|
|
123
130
|
}, [loadConversations]);
|
|
124
131
|
useEffect(() => {
|
|
125
132
|
if (!actions) return;
|
|
126
133
|
const focusSearch = () => {
|
|
127
|
-
|
|
134
|
+
revealRef.current();
|
|
128
135
|
requestAnimationFrame(() => searchInputRef.current?.focus());
|
|
129
136
|
};
|
|
130
137
|
document.addEventListener(FOCUS_SIDEBAR_SEARCH_EVENT, focusSearch);
|
|
131
138
|
return () => document.removeEventListener(FOCUS_SIDEBAR_SEARCH_EVENT, focusSearch);
|
|
132
|
-
}, [actions
|
|
139
|
+
}, [actions]);
|
|
133
140
|
const filteredConversations = useMemo(() => {
|
|
134
141
|
const query = search.trim().toLowerCase();
|
|
135
142
|
if (!query) return conversations;
|
|
@@ -157,100 +164,111 @@ function ChatSidebar({ actions = true }) {
|
|
|
157
164
|
setShareDialogOpen(true);
|
|
158
165
|
};
|
|
159
166
|
const handleNewChat = () => {
|
|
167
|
+
closeDrawer();
|
|
160
168
|
clearCurrentConversation();
|
|
161
169
|
navigation.toNewChat();
|
|
162
170
|
};
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
}), attachmentsAvailable && /* @__PURE__ */ jsxs(LinkComponent, {
|
|
212
|
-
target: { kind: "attachments" },
|
|
213
|
-
className: "flex items-center h-9 px-2 gap-3 rounded-lg transition-colors hover:chat-tint",
|
|
214
|
-
title: tSidebar.attachments,
|
|
215
|
-
children: [/* @__PURE__ */ jsx(FaRegFileImage, { className: "flex-shrink-0 w-4 h-4" }), /* @__PURE__ */ jsx("span", {
|
|
216
|
-
className: cx("text-sm transition-opacity duration-200", sidebarCollapsed ? "opacity-0 w-0 overflow-hidden" : "opacity-100"),
|
|
217
|
-
children: tSidebar.attachments
|
|
218
|
-
})]
|
|
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
219
|
})]
|
|
220
|
-
}),
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
children: [/* @__PURE__ */ jsx(IoSettingsOutline, { className: "h-3.5 w-3.5 shrink-0" }), /* @__PURE__ */ jsx("span", {
|
|
249
|
-
className: "truncate",
|
|
250
|
-
children: tSidebar.manageProviders
|
|
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),
|
|
239
|
+
onKeyDown: (e) => {
|
|
240
|
+
if (e.key === "Escape" && search) {
|
|
241
|
+
e.stopPropagation();
|
|
242
|
+
setSearch("");
|
|
243
|
+
}
|
|
244
|
+
},
|
|
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)"
|
|
251
248
|
})]
|
|
252
249
|
})
|
|
253
|
-
}),
|
|
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,
|
|
254
272
|
contextMenu,
|
|
255
273
|
dialogs,
|
|
256
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 {};
|
|
@@ -2,25 +2,13 @@ import { applyPartDelta, finalizeParts } from "./parts.js";
|
|
|
2
2
|
import { chatReducer, initialState } from "./chatReducer.js";
|
|
3
3
|
import { ChatConfigProvider, resolveChatConfig } from "./config.js";
|
|
4
4
|
import { providerIssueFor } from "./providerIssues.js";
|
|
5
|
+
import { ReportSidebarPresentationContext, readStoredSidebar, storeSidebar, useControllable } from "./sidebarState.js";
|
|
5
6
|
import { useSSEStream } from "./useSSEStream.js";
|
|
6
|
-
import { createContext, useCallback, useContext, useEffect, useMemo, useReducer, useRef } from "react";
|
|
7
|
+
import { createContext, useCallback, useContext, useEffect, useMemo, useReducer, useRef, useState } from "react";
|
|
7
8
|
import { jsx } from "react/jsx-runtime";
|
|
8
9
|
import { DateTime } from "luxon";
|
|
9
10
|
//#region src/ai/core/ChatProvider.tsx
|
|
10
11
|
var SELECTED_MODEL_STORAGE_KEY = "aiChatSelectedModel";
|
|
11
|
-
var SIDEBAR_COLLAPSED_STORAGE_KEY = "aiChatSidebarCollapsed";
|
|
12
|
-
function readStoredSidebarCollapsed() {
|
|
13
|
-
try {
|
|
14
|
-
return globalThis.localStorage?.getItem(SIDEBAR_COLLAPSED_STORAGE_KEY) === "1";
|
|
15
|
-
} catch {
|
|
16
|
-
return false;
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
function storeSidebarCollapsed(collapsed) {
|
|
20
|
-
try {
|
|
21
|
-
globalThis.localStorage?.setItem(SIDEBAR_COLLAPSED_STORAGE_KEY, collapsed ? "1" : "0");
|
|
22
|
-
} catch {}
|
|
23
|
-
}
|
|
24
12
|
function readStoredModel() {
|
|
25
13
|
try {
|
|
26
14
|
return globalThis.localStorage?.getItem(SELECTED_MODEL_STORAGE_KEY) ?? null;
|
|
@@ -73,11 +61,8 @@ function useChat() {
|
|
|
73
61
|
if (!context) throw new Error("useChat must be used within a ChatProvider");
|
|
74
62
|
return context;
|
|
75
63
|
}
|
|
76
|
-
function ChatProvider({ adapter, currentUser, navigation, notify, activeConversationId = null, config, children }) {
|
|
77
|
-
const [state, dispatch] = useReducer(chatReducer, initialState
|
|
78
|
-
...seed,
|
|
79
|
-
sidebarCollapsed: readStoredSidebarCollapsed()
|
|
80
|
-
}));
|
|
64
|
+
function ChatProvider({ adapter, currentUser, navigation, notify, activeConversationId = null, config, sidebar: sidebarProp, onSidebarChange, drawerOpen: drawerOpenProp, onDrawerOpenChange, children }) {
|
|
65
|
+
const [state, dispatch] = useReducer(chatReducer, initialState);
|
|
81
66
|
const hasLoadedConversationsRef = useRef(false);
|
|
82
67
|
const { onStreamingChunk, startStreaming, stopAnimationLoop, resetAnimationRefs, targetStreamContentRef, streamActiveRef, streamingConversationIdRef, backgroundStreamsRef, abortControllerRef } = useSSEStream(dispatch);
|
|
83
68
|
const currentConversationIdRef = useRef(null);
|
|
@@ -700,12 +685,24 @@ function ChatProvider({ adapter, currentUser, navigation, notify, activeConversa
|
|
|
700
685
|
state.currentConversation?.messages,
|
|
701
686
|
state.thinkingStartTime
|
|
702
687
|
]);
|
|
703
|
-
const
|
|
704
|
-
|
|
688
|
+
const [sidebar, setSidebar, sidebarRef] = useControllable(sidebarProp, readStoredSidebar, onSidebarChange, storeSidebar);
|
|
689
|
+
const [drawerOpen, setDrawerOpen, drawerOpenRef] = useControllable(drawerOpenProp, () => false, onDrawerOpenChange);
|
|
690
|
+
const [sidebarPresentation, setSidebarPresentation] = useState("inline");
|
|
691
|
+
const presentationRef = useRef(sidebarPresentation);
|
|
692
|
+
presentationRef.current = sidebarPresentation;
|
|
693
|
+
const reportSidebarPresentation = useCallback((presentation) => {
|
|
694
|
+
setSidebarPresentation(presentation);
|
|
695
|
+
if (presentation === "inline") setDrawerOpen(false);
|
|
696
|
+
}, [setDrawerOpen]);
|
|
705
697
|
const toggleSidebar = useCallback(() => {
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
}, [
|
|
698
|
+
if (presentationRef.current === "drawer") setDrawerOpen(!drawerOpenRef.current.value);
|
|
699
|
+
else setSidebar(sidebarRef.current.value === "expanded" ? "collapsed" : "expanded");
|
|
700
|
+
}, [
|
|
701
|
+
setDrawerOpen,
|
|
702
|
+
drawerOpenRef,
|
|
703
|
+
setSidebar,
|
|
704
|
+
sidebarRef
|
|
705
|
+
]);
|
|
709
706
|
const navigateToBranch = useCallback((messageId) => {
|
|
710
707
|
if (!state.currentConversation) return;
|
|
711
708
|
dispatch({
|
|
@@ -770,7 +767,10 @@ function ChatProvider({ adapter, currentUser, navigation, notify, activeConversa
|
|
|
770
767
|
streamingReasoning: state.streamingReasoning,
|
|
771
768
|
streamingParts: state.streamingParts,
|
|
772
769
|
thinkingStartTime: state.thinkingStartTime,
|
|
773
|
-
|
|
770
|
+
sidebar,
|
|
771
|
+
sidebarCollapsed: sidebar !== "expanded",
|
|
772
|
+
sidebarPresentation,
|
|
773
|
+
drawerOpen,
|
|
774
774
|
queuedMessages: state.queuedMessages,
|
|
775
775
|
loadConversations,
|
|
776
776
|
loadConversation,
|
|
@@ -786,6 +786,8 @@ function ChatProvider({ adapter, currentUser, navigation, notify, activeConversa
|
|
|
786
786
|
stopStreaming,
|
|
787
787
|
removeQueuedMessage,
|
|
788
788
|
flushQueuedMessages,
|
|
789
|
+
setSidebar,
|
|
790
|
+
setDrawerOpen,
|
|
789
791
|
toggleSidebar,
|
|
790
792
|
updateConversationTitle,
|
|
791
793
|
navigateToBranch,
|
|
@@ -797,6 +799,9 @@ function ChatProvider({ adapter, currentUser, navigation, notify, activeConversa
|
|
|
797
799
|
notify,
|
|
798
800
|
activeConversationId,
|
|
799
801
|
state,
|
|
802
|
+
sidebar,
|
|
803
|
+
sidebarPresentation,
|
|
804
|
+
drawerOpen,
|
|
800
805
|
isSelectedModelAvailable,
|
|
801
806
|
loadConversations,
|
|
802
807
|
loadConversation,
|
|
@@ -812,6 +817,8 @@ function ChatProvider({ adapter, currentUser, navigation, notify, activeConversa
|
|
|
812
817
|
stopStreaming,
|
|
813
818
|
removeQueuedMessage,
|
|
814
819
|
flushQueuedMessages,
|
|
820
|
+
setSidebar,
|
|
821
|
+
setDrawerOpen,
|
|
815
822
|
toggleSidebar,
|
|
816
823
|
updateConversationTitle,
|
|
817
824
|
navigateToBranch,
|
|
@@ -819,9 +826,12 @@ function ChatProvider({ adapter, currentUser, navigation, notify, activeConversa
|
|
|
819
826
|
]);
|
|
820
827
|
return /* @__PURE__ */ jsx(ChatConfigProvider, {
|
|
821
828
|
value: resolvedConfig,
|
|
822
|
-
children: /* @__PURE__ */ jsx(
|
|
823
|
-
value,
|
|
824
|
-
children
|
|
829
|
+
children: /* @__PURE__ */ jsx(ReportSidebarPresentationContext.Provider, {
|
|
830
|
+
value: reportSidebarPresentation,
|
|
831
|
+
children: /* @__PURE__ */ jsx(ChatContext.Provider, {
|
|
832
|
+
value,
|
|
833
|
+
children
|
|
834
|
+
})
|
|
825
835
|
})
|
|
826
836
|
});
|
|
827
837
|
}
|
|
@@ -20,7 +20,6 @@ export interface ChatState {
|
|
|
20
20
|
streamingReasoning: string;
|
|
21
21
|
streamingParts: MessagePart[];
|
|
22
22
|
thinkingStartTime: number | null;
|
|
23
|
-
sidebarCollapsed: boolean;
|
|
24
23
|
queuedMessages: ChatMessage[];
|
|
25
24
|
}
|
|
26
25
|
export type ChatAction = {
|
|
@@ -80,8 +79,6 @@ export type ChatAction = {
|
|
|
80
79
|
messageId: string;
|
|
81
80
|
updates: Partial<ChatMessage>;
|
|
82
81
|
conversationId: string;
|
|
83
|
-
} | {
|
|
84
|
-
type: 'TOGGLE_SIDEBAR';
|
|
85
82
|
} | {
|
|
86
83
|
type: 'ADD_CONVERSATION';
|
|
87
84
|
conversation: ConversationSummary;
|
|
@@ -20,7 +20,6 @@ var initialState = {
|
|
|
20
20
|
streamingReasoning: "",
|
|
21
21
|
streamingParts: [],
|
|
22
22
|
thinkingStartTime: null,
|
|
23
|
-
sidebarCollapsed: false,
|
|
24
23
|
queuedMessages: []
|
|
25
24
|
};
|
|
26
25
|
function chatReducer(state, action) {
|
|
@@ -165,10 +164,6 @@ function chatReducer(state, action) {
|
|
|
165
164
|
} : msg)
|
|
166
165
|
}
|
|
167
166
|
};
|
|
168
|
-
case "TOGGLE_SIDEBAR": return {
|
|
169
|
-
...state,
|
|
170
|
-
sidebarCollapsed: !state.sidebarCollapsed
|
|
171
|
-
};
|
|
172
167
|
case "ADD_CONVERSATION":
|
|
173
168
|
if (state.conversations.some((c) => c.id === action.conversation.id)) return state;
|
|
174
169
|
return {
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/** The conversation list beside the thread: full width, a rail of icons, or
|
|
2
|
+
* not shown at all (the host offers its own way back). */
|
|
3
|
+
export type SidebarState = 'expanded' | 'collapsed' | 'hidden';
|
|
4
|
+
/** How the layout shows the list right now: as a column beside the thread,
|
|
5
|
+
* or as a drawer over it. */
|
|
6
|
+
export type SidebarPresentation = 'inline' | 'drawer';
|
|
7
|
+
export declare function readStoredSidebar(): SidebarState;
|
|
8
|
+
export declare function storeSidebar(state: SidebarState): void;
|
|
9
|
+
/** A value the host may own through a prop. Without the prop the provider
|
|
10
|
+
* keeps it; either way the change callback hears every change. The ref
|
|
11
|
+
* reads the latest value from callbacks that should not re-create. */
|
|
12
|
+
export declare function useControllable<T>(controlled: T | undefined, initial: () => T, onChange: ((value: T) => void) | undefined, remember?: (value: T) => void): readonly [T, (next: T) => void, import("react").RefObject<{
|
|
13
|
+
value: T;
|
|
14
|
+
controlled: boolean;
|
|
15
|
+
onChange: ((value: T) => void) | undefined;
|
|
16
|
+
remember: ((value: T) => void) | undefined;
|
|
17
|
+
}>];
|
|
18
|
+
export declare const ReportSidebarPresentationContext: import("react").Context<(presentation: SidebarPresentation) => void>;
|
|
19
|
+
export declare function useReportSidebarPresentation(): (presentation: SidebarPresentation) => void;
|