@parallelworks/ui 0.64.0 → 0.65.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 +14 -0
- package/dist/ai/adapter/mock/mockAdapter.js +24 -2
- package/dist/ai/adapter/openai/createOpenAIStream.d.ts +1 -0
- package/dist/ai/adapter/openai/createOpenAIStream.js +13 -6
- package/dist/ai/adapter/openai/index.d.ts +1 -1
- package/dist/ai/adapter/openai/index.js +2 -2
- package/dist/ai/adapter/types.d.ts +10 -0
- package/dist/ai/components/ChatInput.js +9 -19
- package/dist/ai/components/ComposerAddMenu.d.ts +14 -0
- package/dist/ai/components/ComposerAddMenu.js +274 -0
- package/dist/ai/components/ComposerChrome.d.ts +2 -0
- package/dist/ai/components/ComposerChrome.js +5 -1
- package/dist/ai/core/ChatProvider.d.ts +8 -1
- package/dist/ai/core/ChatProvider.js +41 -2
- package/dist/ai/strings.d.ts +21 -0
- package/dist/ai/strings.js +13 -0
- package/dist/components/Field.d.ts +5 -3
- package/dist/components/Field.js +49 -42
- package/dist/components/Section.d.ts +6 -1
- package/dist/components/Section.js +8 -3
- package/dist/components/stickyTop.d.ts +1 -0
- package/dist/components/stickyTop.js +6 -1
- package/dist/form/Fields/ObjectField.js +2 -1
- package/dist/form/Form.d.ts +4 -0
- package/dist/form/Form.js +10 -2
- package/dist/graph/DependencyGraph.d.ts +3 -3
- package/dist/graph/DependencyGraph.js +6 -6
- package/dist/graph/StepSummary.js +1 -1
- package/dist/icons.d.ts +1 -0
- package/dist/icons.js +2 -1
- package/dist/styles.css +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -845,6 +845,20 @@ optional capability groups (`providers`, `attachments`, `sharing`,
|
|
|
845
845
|
`ChatAdapterError` with a user-facing `message`. Model entries and wire
|
|
846
846
|
messages keep OpenAI-compatible snake_case field names.
|
|
847
847
|
|
|
848
|
+
An adapter with `tools` lists the tools its backend runs itself while it
|
|
849
|
+
answers, each a `ChatToolOption` (`id`, and a `label` and optional
|
|
850
|
+
`description` in the reader's language). The composer's + button then opens a
|
|
851
|
+
menu of Add files (when the adapter stores attachments) and Tools, whose
|
|
852
|
+
submenu has a switch for each tool and the `extraLinks.connectTools` link,
|
|
853
|
+
which leaves the row under the composer. Past six tools (`TOOLS_IN_MENU`),
|
|
854
|
+
Tools opens a modal that searches them and turns a match on or off together. Every tool starts on, a dot on the + shows that some are off, and the
|
|
855
|
+
choice is remembered per user in the browser. A turn the provider doesn't run,
|
|
856
|
+
such as an agent's, offers no tools. Each request carries the enabled ids as
|
|
857
|
+
`StreamRequest.tools` once the list has loaded, possibly empty, and the
|
|
858
|
+
OpenAI-compatible transport sends them as the `X-Platform-Tools` header
|
|
859
|
+
(`TOOLS_HEADER`), comma-separated and empty when every tool is off. A request
|
|
860
|
+
without a tool choice sends no header.
|
|
861
|
+
|
|
848
862
|
Tailwind v4 consumers add the chat rules (markdown rendering, streaming
|
|
849
863
|
animations) and streamdown's classes next to the theme contract:
|
|
850
864
|
|
|
@@ -6,6 +6,24 @@ var CANNED_REPLIES = [
|
|
|
6
6
|
"Branching works too: edit one of your earlier messages or hit regenerate on a reply, then use the arrows to flip between branches."
|
|
7
7
|
];
|
|
8
8
|
var TOOL_REPLY = "Two clusters are running: **gpu-a** with 4 nodes, and **cpu-batch**, idle and scaled to zero.";
|
|
9
|
+
var TOOLS_OFF_REPLY = "Listing clusters is turned off in the tools menu, so I can't check which clusters are running. Turn it back on and ask again.";
|
|
10
|
+
var LIST_CLUSTERS = "list_clusters";
|
|
11
|
+
var MOCK_TOOLS = [
|
|
12
|
+
{
|
|
13
|
+
id: LIST_CLUSTERS,
|
|
14
|
+
label: "List clusters",
|
|
15
|
+
description: "Names your clusters and their state"
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
id: "list_storage",
|
|
19
|
+
label: "List storage",
|
|
20
|
+
description: "Names your buckets and disks"
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
id: "read_docs",
|
|
24
|
+
label: "Read the docs"
|
|
25
|
+
}
|
|
26
|
+
];
|
|
9
27
|
var THINKING_PREAMBLE = "Thinking about the question, weighing a few canned options, and picking the next reply in the rotation...";
|
|
10
28
|
function sleep(ms, signal) {
|
|
11
29
|
return new Promise((resolve, reject) => {
|
|
@@ -29,6 +47,9 @@ function createMockChatAdapter(options) {
|
|
|
29
47
|
let replyIndex = 0;
|
|
30
48
|
return {
|
|
31
49
|
conversations: store.conversations,
|
|
50
|
+
tools: { async list() {
|
|
51
|
+
return MOCK_TOOLS;
|
|
52
|
+
} },
|
|
32
53
|
models: { async list() {
|
|
33
54
|
return {
|
|
34
55
|
models: [
|
|
@@ -69,7 +90,8 @@ function createMockChatAdapter(options) {
|
|
|
69
90
|
reasoning += word;
|
|
70
91
|
handlers.onReasoning?.(word);
|
|
71
92
|
}
|
|
72
|
-
const
|
|
93
|
+
const toolsModel = req.model.includes("tools");
|
|
94
|
+
const usesTools = toolsModel && (req.tools === void 0 || req.tools.includes(LIST_CLUSTERS));
|
|
73
95
|
if (usesTools) {
|
|
74
96
|
const id = `call-${crypto.randomUUID()}`;
|
|
75
97
|
handlers.onPart?.({
|
|
@@ -93,7 +115,7 @@ function createMockChatAdapter(options) {
|
|
|
93
115
|
}])
|
|
94
116
|
});
|
|
95
117
|
}
|
|
96
|
-
const reply = usesTools ? TOOL_REPLY : CANNED_REPLIES[replyIndex++ % CANNED_REPLIES.length] ?? "";
|
|
118
|
+
const reply = usesTools ? TOOL_REPLY : toolsModel ? TOOLS_OFF_REPLY : CANNED_REPLIES[replyIndex++ % CANNED_REPLIES.length] ?? "";
|
|
97
119
|
await sleep(250, signal);
|
|
98
120
|
let content = "";
|
|
99
121
|
for (const word of reply.split(/(?<= )/)) {
|
|
@@ -3,6 +3,17 @@ import { errorMessageFromResponse } from "./errors.js";
|
|
|
3
3
|
import { buildResponsesRequestBody, parseResponsesSSELine } from "./responses.js";
|
|
4
4
|
import { processSSEStream } from "./sse.js";
|
|
5
5
|
//#region src/ai/adapter/openai/createOpenAIStream.ts
|
|
6
|
+
var TOOLS_HEADER = "X-Platform-Tools";
|
|
7
|
+
function requestHeaders(hostHeaders, tools) {
|
|
8
|
+
const headers = {
|
|
9
|
+
"Content-Type": "application/json",
|
|
10
|
+
Accept: "text/event-stream"
|
|
11
|
+
};
|
|
12
|
+
const toolsKey = TOOLS_HEADER.toLowerCase();
|
|
13
|
+
for (const [name, value] of Object.entries(hostHeaders ?? {})) if (tools === void 0 || name.toLowerCase() !== toolsKey) headers[name] = value;
|
|
14
|
+
if (tools !== void 0) headers[TOOLS_HEADER] = tools.join(",");
|
|
15
|
+
return headers;
|
|
16
|
+
}
|
|
6
17
|
function createOpenAICompatibleStream(config) {
|
|
7
18
|
return async (req, handlers, signal) => {
|
|
8
19
|
const responsesUrl = req.useResponses === true ? config.responsesUrl : void 0;
|
|
@@ -15,11 +26,7 @@ function createOpenAICompatibleStream(config) {
|
|
|
15
26
|
};
|
|
16
27
|
const res = await (config.fetch ?? globalThis.fetch.bind(globalThis))(url, {
|
|
17
28
|
method: "POST",
|
|
18
|
-
headers:
|
|
19
|
-
"Content-Type": "application/json",
|
|
20
|
-
Accept: "text/event-stream",
|
|
21
|
-
...config.buildHeaders?.(req)
|
|
22
|
-
},
|
|
29
|
+
headers: requestHeaders(config.buildHeaders?.(req), req.tools),
|
|
23
30
|
...config.credentials ? { credentials: config.credentials } : {},
|
|
24
31
|
body: JSON.stringify(body),
|
|
25
32
|
...signal ? { signal } : {}
|
|
@@ -31,4 +38,4 @@ function createOpenAICompatibleStream(config) {
|
|
|
31
38
|
};
|
|
32
39
|
}
|
|
33
40
|
//#endregion
|
|
34
|
-
export { createOpenAICompatibleStream };
|
|
41
|
+
export { TOOLS_HEADER, createOpenAICompatibleStream };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { createOpenAICompatibleStream, type OpenAIStreamConfig, } from './createOpenAIStream.js';
|
|
1
|
+
export { createOpenAICompatibleStream, type OpenAIStreamConfig, TOOLS_HEADER, } from './createOpenAIStream.js';
|
|
2
2
|
export { apiErrorMessage, errorMessageFromResponse, statusFallbackMessage, } from './errors.js';
|
|
3
3
|
export { createOpenAIChatAdapter, type OpenAIChatAdapterOptions, } from './openaiAdapter.js';
|
|
4
4
|
export { buildResponsesRequestBody, parseResponsesSSELine, } from './responses.js';
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { apiErrorMessage, errorMessageFromResponse, statusFallbackMessage } from "./errors.js";
|
|
2
2
|
import { buildResponsesRequestBody, parseResponsesSSELine } from "./responses.js";
|
|
3
3
|
import { parseSSELine, processSSEStream } from "./sse.js";
|
|
4
|
-
import { createOpenAICompatibleStream } from "./createOpenAIStream.js";
|
|
4
|
+
import { TOOLS_HEADER, createOpenAICompatibleStream } from "./createOpenAIStream.js";
|
|
5
5
|
import { createOpenAIChatAdapter } from "./openaiAdapter.js";
|
|
6
|
-
export { apiErrorMessage, buildResponsesRequestBody, createOpenAIChatAdapter, createOpenAICompatibleStream, errorMessageFromResponse, parseResponsesSSELine, parseSSELine, processSSEStream, statusFallbackMessage };
|
|
6
|
+
export { TOOLS_HEADER, apiErrorMessage, buildResponsesRequestBody, createOpenAIChatAdapter, createOpenAICompatibleStream, errorMessageFromResponse, parseResponsesSSELine, parseSSELine, processSSEStream, statusFallbackMessage };
|
|
@@ -20,6 +20,7 @@ export interface StreamRequest {
|
|
|
20
20
|
userMessageId: string;
|
|
21
21
|
allocation?: string | null;
|
|
22
22
|
useResponses?: boolean;
|
|
23
|
+
tools?: string[];
|
|
23
24
|
}
|
|
24
25
|
export interface StreamHandlers {
|
|
25
26
|
onContent: (content: string) => void;
|
|
@@ -124,6 +125,14 @@ export interface SharingAdapter {
|
|
|
124
125
|
export interface AllocationsAdapter {
|
|
125
126
|
list(): Promise<Allocation[]>;
|
|
126
127
|
}
|
|
128
|
+
export interface ChatToolOption {
|
|
129
|
+
id: string;
|
|
130
|
+
label: string;
|
|
131
|
+
description?: string;
|
|
132
|
+
}
|
|
133
|
+
export interface ChatToolsAdapter {
|
|
134
|
+
list(): Promise<ChatToolOption[]>;
|
|
135
|
+
}
|
|
127
136
|
export interface ChatAdapter {
|
|
128
137
|
conversations: ConversationsAdapter;
|
|
129
138
|
models: {
|
|
@@ -134,4 +143,5 @@ export interface ChatAdapter {
|
|
|
134
143
|
attachments?: AttachmentsAdapter;
|
|
135
144
|
sharing?: SharingAdapter;
|
|
136
145
|
allocations?: AllocationsAdapter;
|
|
146
|
+
tools?: ChatToolsAdapter;
|
|
137
147
|
}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { cx } from "../../cx.js";
|
|
2
|
-
import {
|
|
2
|
+
import { ArrowUpIcon, StopIcon } from "../../icons.js";
|
|
3
3
|
import { useChatConfig } from "../core/config.js";
|
|
4
4
|
import { useChat } from "../core/ChatProvider.js";
|
|
5
5
|
import AttachmentUpload from "./AttachmentUpload.js";
|
|
6
6
|
import useDragDrop from "../core/useDragDrop.js";
|
|
7
|
+
import ComposerAddMenu from "./ComposerAddMenu.js";
|
|
7
8
|
import { ComposerContext, ComposerControls, ComposerSettings } from "./ComposerChrome.js";
|
|
8
9
|
import { clearDraft, readDraft, storeDraft, sweepDrafts } from "./chatDraft.js";
|
|
9
10
|
import DragOverlay from "./DragOverlay.js";
|
|
@@ -27,18 +28,6 @@ function keyAction(e, empty, canEditLast) {
|
|
|
27
28
|
if (e.key === "ArrowUp" && empty && canEditLast) return "editLast";
|
|
28
29
|
return null;
|
|
29
30
|
}
|
|
30
|
-
function AttachButton({ open, disabled, label, onClick }) {
|
|
31
|
-
return /* @__PURE__ */ jsx("button", {
|
|
32
|
-
type: "button",
|
|
33
|
-
onClick,
|
|
34
|
-
disabled,
|
|
35
|
-
"aria-expanded": open,
|
|
36
|
-
className: cx("flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-full transition-colors", open ? "theme-text chat-tint-strong" : "theme-muted-text hover:theme-text hover:chat-tint", "disabled:opacity-50 disabled:cursor-not-allowed"),
|
|
37
|
-
"aria-label": label,
|
|
38
|
-
title: label,
|
|
39
|
-
children: /* @__PURE__ */ jsx(AddIcon, { className: cx("h-5 w-5 transition-transform duration-200", open && "rotate-45") })
|
|
40
|
-
});
|
|
41
|
-
}
|
|
42
31
|
function SendButton({ onClick, enabled, emphasized, label, testId }) {
|
|
43
32
|
return /* @__PURE__ */ jsx("button", {
|
|
44
33
|
type: "button",
|
|
@@ -86,7 +75,7 @@ function ChatInput({ onSend, disabled = false, placeholder, conversationId, onEd
|
|
|
86
75
|
const textareaRef = useRef(null);
|
|
87
76
|
const attachmentUploadRef = useRef(null);
|
|
88
77
|
const chat = useChat();
|
|
89
|
-
const { adapter, flushQueuedMessages, isSelectedModelAvailable } = chat;
|
|
78
|
+
const { adapter, flushQueuedMessages, isSelectedModelAvailable, tools } = chat;
|
|
90
79
|
const { isStreaming, stopStreaming, queuedCount } = streamControls(chat, turn);
|
|
91
80
|
const t = useChatConfig().strings.input;
|
|
92
81
|
const effectivePlaceholder = placeholder ?? t.placeholder;
|
|
@@ -197,7 +186,8 @@ function ChatInput({ onSend, disabled = false, placeholder, conversationId, onEd
|
|
|
197
186
|
if (pastes && handleTextPaste(e.clipboardData.getData("text/plain"))) e.preventDefault();
|
|
198
187
|
};
|
|
199
188
|
const { isDragging, dragHandlers } = useDragDrop({ onFilesDropped: handleFilesDropped });
|
|
200
|
-
const
|
|
189
|
+
const toolsAvailable = !turn && tools.length > 0;
|
|
190
|
+
const toolbar = attachmentsAvailable || toolsAvailable || !!settingsRight;
|
|
201
191
|
const action = isStreaming && !input.trim() ? /* @__PURE__ */ jsx("button", {
|
|
202
192
|
type: "button",
|
|
203
193
|
onClick: stopStreaming,
|
|
@@ -281,11 +271,11 @@ function ChatInput({ onSend, disabled = false, placeholder, conversationId, onEd
|
|
|
281
271
|
}),
|
|
282
272
|
toolbar && /* @__PURE__ */ jsxs("div", {
|
|
283
273
|
className: "flex items-center gap-1 px-2 pb-2",
|
|
284
|
-
children: [
|
|
285
|
-
|
|
274
|
+
children: [/* @__PURE__ */ jsx(ComposerAddMenu, {
|
|
275
|
+
attachmentsOpen: showAttachments && attachmentsAvailable,
|
|
276
|
+
onToggleAttachments: attachmentsAvailable ? () => setShowAttachments(!showAttachments) : void 0,
|
|
286
277
|
disabled,
|
|
287
|
-
|
|
288
|
-
onClick: () => setShowAttachments(!showAttachments)
|
|
278
|
+
withTools: !turn
|
|
289
279
|
}), /* @__PURE__ */ jsxs("div", {
|
|
290
280
|
className: "ml-auto flex min-w-0 items-center gap-1",
|
|
291
281
|
children: [/* @__PURE__ */ jsx("div", {
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** More tools than this leave the menu for a modal, where they can be searched. */
|
|
2
|
+
export declare const TOOLS_IN_MENU = 6;
|
|
3
|
+
/** The composer's + button. With only attachments it opens their panel, as it
|
|
4
|
+
* always has; once the adapter lists tools it opens a menu of Add files and
|
|
5
|
+
* Tools, whose submenu toggles each one. Past TOOLS_IN_MENU, Tools opens a
|
|
6
|
+
* modal instead. A dot on the + says some tools are off. */
|
|
7
|
+
export default function ComposerAddMenu({ attachmentsOpen, onToggleAttachments, disabled, withTools, }: {
|
|
8
|
+
attachmentsOpen: boolean;
|
|
9
|
+
/** Absent when the composer takes no files. */
|
|
10
|
+
onToggleAttachments?: (() => void) | undefined;
|
|
11
|
+
disabled: boolean;
|
|
12
|
+
/** Offer the provider's tools, for a turn the provider runs. */
|
|
13
|
+
withTools: boolean;
|
|
14
|
+
}): import("react").JSX.Element | null;
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
import { cx } from "../../cx.js";
|
|
2
|
+
import { AddIcon, AttachmentIcon, ConnectIcon, ToolsIcon } from "../../icons.js";
|
|
3
|
+
import { panelItemClassName } from "../../components/floating.js";
|
|
4
|
+
import { MenuItem, MenuLinkItem, MenuPopup, MenuSeparator, MenuSubmenuTrigger } from "../../components/Menu.js";
|
|
5
|
+
import { useQueryMatch } from "../../components/search.js";
|
|
6
|
+
import { Button } from "../../components/Button.js";
|
|
7
|
+
import { Modal } from "../../components/Modal.js";
|
|
8
|
+
import { Switch } from "../../components/Switch.js";
|
|
9
|
+
import { SearchInput } from "../../components/SearchInput.js";
|
|
10
|
+
import { useChatConfig } from "../core/config.js";
|
|
11
|
+
import { useChat } from "../core/ChatProvider.js";
|
|
12
|
+
import { Menu } from "@base-ui/react/menu";
|
|
13
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
14
|
+
import { useId, useState, useSyncExternalStore } from "react";
|
|
15
|
+
var NARROW = "(max-width: 639px)";
|
|
16
|
+
function subscribeNarrow(onChange) {
|
|
17
|
+
if (typeof window === "undefined" || typeof window.matchMedia !== "function") return () => {};
|
|
18
|
+
const media = window.matchMedia(NARROW);
|
|
19
|
+
media.addEventListener("change", onChange);
|
|
20
|
+
return () => media.removeEventListener("change", onChange);
|
|
21
|
+
}
|
|
22
|
+
var isNarrow = () => typeof window !== "undefined" && typeof window.matchMedia === "function" && window.matchMedia(NARROW).matches;
|
|
23
|
+
/** A phone has no room beside the menu for a submenu, and no hover to open one. */
|
|
24
|
+
function useNarrow() {
|
|
25
|
+
return useSyncExternalStore(subscribeNarrow, isNarrow, () => false);
|
|
26
|
+
}
|
|
27
|
+
function PlusButton({ open, label, hint, className, ...props }) {
|
|
28
|
+
const name = hint ? `${label} · ${hint}` : label;
|
|
29
|
+
return /* @__PURE__ */ jsxs("button", {
|
|
30
|
+
type: "button",
|
|
31
|
+
...props,
|
|
32
|
+
"aria-label": name,
|
|
33
|
+
title: name,
|
|
34
|
+
className: cx("relative flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-full transition-colors", open ? "theme-text chat-tint-strong" : "theme-muted-text hover:theme-text hover:chat-tint data-popup-open:theme-text data-popup-open:chat-tint", "disabled:opacity-50 disabled:cursor-not-allowed", className),
|
|
35
|
+
children: [/* @__PURE__ */ jsx(AddIcon, { className: cx("h-5 w-5 transition-transform duration-200", open && "rotate-45") }), hint && /* @__PURE__ */ jsx("span", {
|
|
36
|
+
"aria-hidden": "true",
|
|
37
|
+
className: "absolute top-1 right-1 size-1.5 rounded-full bg-(--theme-accent)"
|
|
38
|
+
})]
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
/** A switch drawn for a menu row, which is itself the checkbox the reader toggles. */
|
|
42
|
+
function RowSwitch() {
|
|
43
|
+
return /* @__PURE__ */ jsx("span", {
|
|
44
|
+
"aria-hidden": "true",
|
|
45
|
+
className: "ml-auto inline-flex h-4 w-7 shrink-0 items-center rounded-full bg-(--theme-border-strong) p-0.5 transition-colors duration-150 group-data-checked:bg-(--theme-focus) motion-reduce:transition-none",
|
|
46
|
+
children: /* @__PURE__ */ jsx("span", { className: "size-3 rounded-full bg-white shadow-sm transition-transform duration-150 group-data-checked:translate-x-3 rtl:group-data-checked:-translate-x-3 motion-reduce:transition-none" })
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
function ToolText({ tool, descriptionId }) {
|
|
50
|
+
return /* @__PURE__ */ jsxs("span", {
|
|
51
|
+
className: "min-w-0 flex-1",
|
|
52
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
53
|
+
className: "block truncate",
|
|
54
|
+
children: tool.label
|
|
55
|
+
}), tool.description && /* @__PURE__ */ jsx("span", {
|
|
56
|
+
id: descriptionId,
|
|
57
|
+
className: "block truncate text-xs theme-muted-text",
|
|
58
|
+
children: tool.description
|
|
59
|
+
})]
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
function ToolMenuRow({ tool, enabled, onChange }) {
|
|
63
|
+
const descriptionId = useId();
|
|
64
|
+
return /* @__PURE__ */ jsxs(Menu.CheckboxItem, {
|
|
65
|
+
checked: enabled,
|
|
66
|
+
onCheckedChange: onChange,
|
|
67
|
+
closeOnClick: false,
|
|
68
|
+
className: cx(panelItemClassName, "group gap-3"),
|
|
69
|
+
...tool.description ? { "aria-describedby": descriptionId } : {},
|
|
70
|
+
children: [/* @__PURE__ */ jsx(ToolText, {
|
|
71
|
+
tool,
|
|
72
|
+
descriptionId
|
|
73
|
+
}), /* @__PURE__ */ jsx(RowSwitch, {})]
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
function ToolsList({ listed }) {
|
|
77
|
+
const { enabledToolIds, setToolEnabled } = useChat();
|
|
78
|
+
return listed.map((tool) => /* @__PURE__ */ jsx(ToolMenuRow, {
|
|
79
|
+
tool,
|
|
80
|
+
enabled: enabledToolIds.includes(tool.id),
|
|
81
|
+
onChange: (enabled) => setToolEnabled(tool.id, enabled)
|
|
82
|
+
}, tool.id));
|
|
83
|
+
}
|
|
84
|
+
function ConnectToolsRow({ href }) {
|
|
85
|
+
const { strings } = useChatConfig();
|
|
86
|
+
return /* @__PURE__ */ jsxs(MenuLinkItem, {
|
|
87
|
+
render: /* @__PURE__ */ jsx("a", {
|
|
88
|
+
href,
|
|
89
|
+
target: "_blank",
|
|
90
|
+
rel: "noreferrer"
|
|
91
|
+
}),
|
|
92
|
+
className: "text-xs theme-muted-text data-highlighted:theme-text",
|
|
93
|
+
children: [/* @__PURE__ */ jsx(ConnectIcon, { className: "h-3.5 w-3.5 shrink-0" }), strings.sidebar.connectTools]
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
function OnCount({ enabled, total }) {
|
|
97
|
+
const { strings } = useChatConfig();
|
|
98
|
+
return /* @__PURE__ */ jsx("span", {
|
|
99
|
+
className: "ml-auto pl-3 text-xs tabular-nums theme-muted-text",
|
|
100
|
+
children: strings.composerMenu.toolsOn(enabled, total)
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
function ToolsModalRow({ tool, enabled, onChange }) {
|
|
104
|
+
const descriptionId = useId();
|
|
105
|
+
return /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsxs("label", {
|
|
106
|
+
className: "flex cursor-pointer items-center gap-3 rounded-md px-2 py-2 hover:bg-(--theme-hover)",
|
|
107
|
+
children: [/* @__PURE__ */ jsxs("span", {
|
|
108
|
+
className: "min-w-0 flex-1",
|
|
109
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
110
|
+
className: "block text-sm",
|
|
111
|
+
children: tool.label
|
|
112
|
+
}), tool.description && /* @__PURE__ */ jsx("span", {
|
|
113
|
+
id: descriptionId,
|
|
114
|
+
className: "block text-xs theme-muted-text",
|
|
115
|
+
children: tool.description
|
|
116
|
+
})]
|
|
117
|
+
}), /* @__PURE__ */ jsx(Switch, {
|
|
118
|
+
size: "sm",
|
|
119
|
+
checked: enabled,
|
|
120
|
+
onCheckedChange: onChange,
|
|
121
|
+
...tool.description ? { "aria-describedby": descriptionId } : {}
|
|
122
|
+
})]
|
|
123
|
+
}) });
|
|
124
|
+
}
|
|
125
|
+
/** Every tool, searchable, for a host with more than the menu holds. */
|
|
126
|
+
function ToolsModal({ open, onClose, listed }) {
|
|
127
|
+
const { enabledToolIds, setToolEnabled } = useChat();
|
|
128
|
+
const { LinkComponent, extraLinks, strings } = useChatConfig();
|
|
129
|
+
const t = strings.composerMenu;
|
|
130
|
+
const matches = useQueryMatch();
|
|
131
|
+
const [query, setQuery] = useState("");
|
|
132
|
+
const needle = query.trim();
|
|
133
|
+
const shown = needle ? listed.filter((tool) => matches(needle, tool.label, tool.description, tool.id)) : listed;
|
|
134
|
+
const allOn = shown.every((tool) => enabledToolIds.includes(tool.id));
|
|
135
|
+
const close = () => {
|
|
136
|
+
onClose();
|
|
137
|
+
setQuery("");
|
|
138
|
+
};
|
|
139
|
+
let footerStart = /* @__PURE__ */ jsx("span", {});
|
|
140
|
+
if (extraLinks.connectTools) footerStart = /* @__PURE__ */ jsxs(LinkComponent, {
|
|
141
|
+
target: {
|
|
142
|
+
kind: "external",
|
|
143
|
+
href: extraLinks.connectTools
|
|
144
|
+
},
|
|
145
|
+
className: "mr-auto inline-flex items-center gap-1.5 text-xs theme-muted-text hover:theme-text",
|
|
146
|
+
children: [/* @__PURE__ */ jsx(ConnectIcon, { className: "h-3.5 w-3.5 shrink-0" }), strings.sidebar.connectTools]
|
|
147
|
+
});
|
|
148
|
+
return /* @__PURE__ */ jsxs(Modal, {
|
|
149
|
+
open,
|
|
150
|
+
onClose: close,
|
|
151
|
+
size: "md",
|
|
152
|
+
title: t.tools,
|
|
153
|
+
description: t.toolsDescription,
|
|
154
|
+
footer: /* @__PURE__ */ jsxs(Fragment, { children: [footerStart, /* @__PURE__ */ jsx(Button, {
|
|
155
|
+
variant: "primary",
|
|
156
|
+
onClick: close,
|
|
157
|
+
children: t.done
|
|
158
|
+
})] }),
|
|
159
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
160
|
+
className: "flex items-center gap-2",
|
|
161
|
+
children: [/* @__PURE__ */ jsx(SearchInput, {
|
|
162
|
+
value: query,
|
|
163
|
+
onValueChange: setQuery,
|
|
164
|
+
placeholder: t.searchTools,
|
|
165
|
+
label: t.searchTools,
|
|
166
|
+
className: "flex-1"
|
|
167
|
+
}), shown.length > 0 && /* @__PURE__ */ jsx(Button, {
|
|
168
|
+
variant: "ghost",
|
|
169
|
+
size: "sm",
|
|
170
|
+
onClick: () => {
|
|
171
|
+
for (const tool of shown) setToolEnabled(tool.id, !allOn);
|
|
172
|
+
},
|
|
173
|
+
children: allOn ? t.turnAllOff : t.turnAllOn
|
|
174
|
+
})]
|
|
175
|
+
}), shown.length > 0 ? /* @__PURE__ */ jsx("ul", {
|
|
176
|
+
className: "-mx-2 mt-3 flex flex-col",
|
|
177
|
+
children: shown.map((tool) => /* @__PURE__ */ jsx(ToolsModalRow, {
|
|
178
|
+
tool,
|
|
179
|
+
enabled: enabledToolIds.includes(tool.id),
|
|
180
|
+
onChange: (enabled) => setToolEnabled(tool.id, enabled)
|
|
181
|
+
}, tool.id))
|
|
182
|
+
}) : /* @__PURE__ */ jsx("p", {
|
|
183
|
+
className: "m-0 py-8 text-center text-sm theme-muted-text",
|
|
184
|
+
children: t.noToolsMatch
|
|
185
|
+
})]
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
/** The composer's + button. With only attachments it opens their panel, as it
|
|
189
|
+
* always has; once the adapter lists tools it opens a menu of Add files and
|
|
190
|
+
* Tools, whose submenu toggles each one. Past TOOLS_IN_MENU, Tools opens a
|
|
191
|
+
* modal instead. A dot on the + says some tools are off. */
|
|
192
|
+
function ComposerAddMenu({ attachmentsOpen, onToggleAttachments, disabled, withTools }) {
|
|
193
|
+
const { tools, enabledToolIds } = useChat();
|
|
194
|
+
const { extraLinks, strings } = useChatConfig();
|
|
195
|
+
const [modalOpen, setModalOpen] = useState(false);
|
|
196
|
+
const narrow = useNarrow();
|
|
197
|
+
const t = strings.composerMenu;
|
|
198
|
+
const listed = withTools ? tools : [];
|
|
199
|
+
if (listed.length === 0) {
|
|
200
|
+
if (!onToggleAttachments) return null;
|
|
201
|
+
return /* @__PURE__ */ jsx(PlusButton, {
|
|
202
|
+
open: attachmentsOpen,
|
|
203
|
+
"aria-expanded": attachmentsOpen,
|
|
204
|
+
disabled,
|
|
205
|
+
label: strings.input.attachFiles,
|
|
206
|
+
onClick: onToggleAttachments
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
const enabled = listed.filter((tool) => enabledToolIds.includes(tool.id)).length;
|
|
210
|
+
const someOff = enabled < listed.length;
|
|
211
|
+
const many = listed.length > 6;
|
|
212
|
+
const connect = extraLinks.connectTools;
|
|
213
|
+
const label = onToggleAttachments ? t.label : t.tools;
|
|
214
|
+
if (many && !onToggleAttachments) return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(PlusButton, {
|
|
215
|
+
open: false,
|
|
216
|
+
"aria-haspopup": "dialog",
|
|
217
|
+
disabled,
|
|
218
|
+
label,
|
|
219
|
+
hint: someOff ? t.someToolsOff : void 0,
|
|
220
|
+
onClick: () => setModalOpen(true)
|
|
221
|
+
}), /* @__PURE__ */ jsx(ToolsModal, {
|
|
222
|
+
open: modalOpen,
|
|
223
|
+
onClose: () => setModalOpen(false),
|
|
224
|
+
listed
|
|
225
|
+
})] });
|
|
226
|
+
const toolsEntry = many ? /* @__PURE__ */ jsxs(MenuItem, {
|
|
227
|
+
onClick: () => setModalOpen(true),
|
|
228
|
+
children: [
|
|
229
|
+
/* @__PURE__ */ jsx(ToolsIcon, { className: "h-4 w-4 shrink-0 theme-muted-text" }),
|
|
230
|
+
t.tools,
|
|
231
|
+
/* @__PURE__ */ jsx(OnCount, {
|
|
232
|
+
enabled,
|
|
233
|
+
total: listed.length
|
|
234
|
+
})
|
|
235
|
+
]
|
|
236
|
+
}) : onToggleAttachments && !narrow ? /* @__PURE__ */ jsxs(Menu.SubmenuRoot, { children: [/* @__PURE__ */ jsxs(MenuSubmenuTrigger, { children: [
|
|
237
|
+
/* @__PURE__ */ jsx(ToolsIcon, { className: "h-4 w-4 shrink-0 theme-muted-text" }),
|
|
238
|
+
t.tools,
|
|
239
|
+
someOff && /* @__PURE__ */ jsx(OnCount, {
|
|
240
|
+
enabled,
|
|
241
|
+
total: listed.length
|
|
242
|
+
})
|
|
243
|
+
] }), /* @__PURE__ */ jsxs(MenuPopup, {
|
|
244
|
+
side: "inline-end",
|
|
245
|
+
className: "w-72",
|
|
246
|
+
children: [/* @__PURE__ */ jsx(ToolsList, { listed }), connect && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(MenuSeparator, {}), /* @__PURE__ */ jsx(ConnectToolsRow, { href: connect })] })]
|
|
247
|
+
})] }) : null;
|
|
248
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs(Menu.Root, { children: [/* @__PURE__ */ jsx(Menu.Trigger, {
|
|
249
|
+
disabled,
|
|
250
|
+
render: /* @__PURE__ */ jsx(PlusButton, {
|
|
251
|
+
open: attachmentsOpen,
|
|
252
|
+
label,
|
|
253
|
+
hint: someOff ? t.someToolsOff : void 0
|
|
254
|
+
})
|
|
255
|
+
}), /* @__PURE__ */ jsxs(MenuPopup, {
|
|
256
|
+
side: "top",
|
|
257
|
+
align: "start",
|
|
258
|
+
className: toolsEntry ? "w-56" : "w-72",
|
|
259
|
+
children: [onToggleAttachments && /* @__PURE__ */ jsxs(MenuItem, {
|
|
260
|
+
onClick: onToggleAttachments,
|
|
261
|
+
children: [/* @__PURE__ */ jsx(AttachmentIcon, { className: "h-4 w-4 shrink-0 theme-muted-text" }), t.addFiles]
|
|
262
|
+
}), toolsEntry ?? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
263
|
+
onToggleAttachments && /* @__PURE__ */ jsx(MenuSeparator, {}),
|
|
264
|
+
/* @__PURE__ */ jsx(ToolsList, { listed }),
|
|
265
|
+
connect && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(MenuSeparator, {}), /* @__PURE__ */ jsx(ConnectToolsRow, { href: connect })] })
|
|
266
|
+
] })]
|
|
267
|
+
})] }), many && /* @__PURE__ */ jsx(ToolsModal, {
|
|
268
|
+
open: modalOpen,
|
|
269
|
+
onClose: () => setModalOpen(false),
|
|
270
|
+
listed
|
|
271
|
+
})] });
|
|
272
|
+
}
|
|
273
|
+
//#endregion
|
|
274
|
+
export { ComposerAddMenu as default };
|
|
@@ -25,4 +25,6 @@ export declare function ComposerControls({ targetSession }: {
|
|
|
25
25
|
/** Where the reader manages the providers behind the model picker, at the
|
|
26
26
|
* foot of a conversation list. */
|
|
27
27
|
export declare function ManageProvidersLink(): import("react").JSX.Element | null;
|
|
28
|
+
/** Where the reader connects more tools, under the composer. While the
|
|
29
|
+
* adapter lists tools it sits at the foot of the composer's + menu instead. */
|
|
28
30
|
export declare function ConnectToolsLink(): import("react").JSX.Element | null;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { ConnectIcon, SettingsIcon } from "../../icons.js";
|
|
2
2
|
import { useChatConfig } from "../core/config.js";
|
|
3
|
+
import { useHasChatTools } from "../core/ChatProvider.js";
|
|
3
4
|
import AllocationSelector from "./AllocationSelector.js";
|
|
4
5
|
import ModelSelector from "./ModelSelector.js";
|
|
5
6
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
@@ -60,9 +61,12 @@ function ManageProvidersLink() {
|
|
|
60
61
|
})]
|
|
61
62
|
});
|
|
62
63
|
}
|
|
64
|
+
/** Where the reader connects more tools, under the composer. While the
|
|
65
|
+
* adapter lists tools it sits at the foot of the composer's + menu instead. */
|
|
63
66
|
function ConnectToolsLink() {
|
|
64
67
|
const { extraLinks, LinkComponent, strings } = useChatConfig();
|
|
65
|
-
|
|
68
|
+
const inMenu = useHasChatTools();
|
|
69
|
+
if (!extraLinks.connectTools || inMenu) return null;
|
|
66
70
|
return /* @__PURE__ */ jsxs(LinkComponent, {
|
|
67
71
|
target: {
|
|
68
72
|
kind: "external",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type ReactNode } from 'react';
|
|
2
|
-
import type { ChatAdapter } from '../adapter/types.js';
|
|
2
|
+
import type { ChatAdapter, ChatToolOption } from '../adapter/types.js';
|
|
3
3
|
import type { AttachmentMeta, ChatModel, ChatNavigation, ChatUser, Conversation, ConversationSummary, ChatMessage as Message, MessagePart, ProviderInfo, ProviderIssue, UnreachableSession } from '../types.js';
|
|
4
4
|
import { type ChatUIConfig } from './config.js';
|
|
5
5
|
import { type SidebarPresentation, type SidebarState } from './sidebarState.js';
|
|
@@ -17,6 +17,9 @@ interface ChatContextValue {
|
|
|
17
17
|
selectedProvider: string | null;
|
|
18
18
|
isSelectedModelAvailable: boolean;
|
|
19
19
|
selectedAllocation: string | null;
|
|
20
|
+
/** The host's tools the reader may turn on or off; empty when it lists none. */
|
|
21
|
+
tools: ChatToolOption[];
|
|
22
|
+
enabledToolIds: string[];
|
|
20
23
|
isLoading: boolean;
|
|
21
24
|
isLoadingConversation: boolean;
|
|
22
25
|
isLoadingModels: boolean;
|
|
@@ -40,6 +43,7 @@ interface ChatContextValue {
|
|
|
40
43
|
retryMessage: (messageId: string) => Promise<boolean>;
|
|
41
44
|
setSelectedProvider: (providerId: string | null) => void;
|
|
42
45
|
setSelectedAllocation: (allocation: string | null) => void;
|
|
46
|
+
setToolEnabled: (id: string, enabled: boolean) => void;
|
|
43
47
|
stopStreaming: () => void;
|
|
44
48
|
removeQueuedMessage: (id: string) => void;
|
|
45
49
|
flushQueuedMessages: () => void;
|
|
@@ -64,6 +68,9 @@ export interface ChatStream {
|
|
|
64
68
|
thinkingStartTime: number | null;
|
|
65
69
|
}
|
|
66
70
|
export declare function useChat(): ChatContextValue;
|
|
71
|
+
/** True while the adapter lists tools of its own, which the composer's +
|
|
72
|
+
* menu offers; false outside a ChatProvider. */
|
|
73
|
+
export declare function useHasChatTools(): boolean;
|
|
67
74
|
/** The reply being streamed. A component that reads it re-renders on every
|
|
68
75
|
* token, so read it only where the reply is drawn. */
|
|
69
76
|
export declare function useChatStream(): ChatStream;
|