@marimo-team/islands 0.19.8-dev41 → 0.19.8-dev49
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/dist/assets/__vite-browser-external-WSlCcXn_.js +1 -0
- package/dist/assets/worker-DUYMdbtA.js +73 -0
- package/dist/main.js +1740 -1691
- package/dist/style.css +1 -1
- package/dist/{useDeepCompareMemoize-CMGprt3H.js → useDeepCompareMemoize-BhZZsis0.js} +7 -3
- package/dist/{vega-component-DU3aSp4m.js → vega-component-DCxUyPnb.js} +1 -1
- package/package.json +1 -1
- package/src/components/app-config/optional-features.tsx +1 -1
- package/src/components/chat/__tests__/useFileState.test.tsx +93 -0
- package/src/components/chat/acp/agent-panel.tsx +26 -77
- package/src/components/chat/chat-components.tsx +114 -1
- package/src/components/chat/chat-panel.tsx +32 -104
- package/src/components/chat/chat-utils.ts +42 -0
- package/src/components/editor/ai/add-cell-with-ai.tsx +85 -53
- package/src/components/editor/ai/ai-completion-editor.tsx +15 -38
- package/src/components/editor/chrome/panels/packages-panel.tsx +12 -9
- package/src/core/islands/__tests__/bridge.test.ts +7 -2
- package/src/core/islands/bridge.ts +1 -1
- package/src/core/islands/main.ts +7 -0
- package/src/core/network/types.ts +2 -2
- package/src/core/wasm/bridge.ts +1 -1
- package/src/core/websocket/useMarimoKernelConnection.tsx +5 -15
- package/src/plugins/impl/anywidget/AnyWidgetPlugin.tsx +86 -167
- package/src/plugins/impl/anywidget/__tests__/AnyWidgetPlugin.test.tsx +37 -123
- package/src/plugins/impl/anywidget/__tests__/model.test.ts +128 -122
- package/src/{utils/__tests__/data-views.test.ts → plugins/impl/anywidget/__tests__/serialization.test.ts} +42 -96
- package/src/plugins/impl/anywidget/model.ts +348 -223
- package/src/plugins/impl/anywidget/schemas.ts +32 -0
- package/src/{utils/data-views.ts → plugins/impl/anywidget/serialization.ts} +13 -36
- package/src/plugins/impl/anywidget/types.ts +27 -0
- package/src/plugins/impl/chat/chat-ui.tsx +22 -20
- package/src/utils/Deferred.ts +21 -0
- package/src/utils/json/base64.ts +38 -8
- package/dist/assets/__vite-browser-external-6-UwTyQC.js +0 -1
- package/dist/assets/worker-D3e5wDxM.js +0 -73
|
@@ -33,15 +33,19 @@ function isWasm() {
|
|
|
33
33
|
var Deferred = class {
|
|
34
34
|
constructor() {
|
|
35
35
|
__publicField(this, "status", "pending");
|
|
36
|
+
__publicField(this, "value");
|
|
36
37
|
this.promise = new Promise((e, x) => {
|
|
37
38
|
this.reject = (e2) => {
|
|
38
39
|
this.status = "rejected", x(e2);
|
|
39
40
|
}, this.resolve = (x2) => {
|
|
40
|
-
this.status = "resolved", e(x2);
|
|
41
|
+
this.status = "resolved", isPromiseLike(x2) || (this.value = x2), e(x2);
|
|
41
42
|
};
|
|
42
43
|
});
|
|
43
44
|
}
|
|
44
45
|
};
|
|
46
|
+
function isPromiseLike(e) {
|
|
47
|
+
return typeof e == "object" && !!e && "then" in e && typeof e.then == "function";
|
|
48
|
+
}
|
|
45
49
|
const WebSocketState = {
|
|
46
50
|
NOT_STARTED: "NOT_STARTED",
|
|
47
51
|
CONNECTING: "CONNECTING",
|
|
@@ -147,7 +151,7 @@ var require__u64 = /* @__PURE__ */ __commonJSMin(((e) => {
|
|
|
147
151
|
})), require_crypto = /* @__PURE__ */ __commonJSMin(((e) => {
|
|
148
152
|
Object.defineProperty(e, "__esModule", { value: true }), e.crypto = void 0, e.crypto = typeof globalThis == "object" && "crypto" in globalThis ? globalThis.crypto : void 0;
|
|
149
153
|
})), require_utils = /* @__PURE__ */ __commonJSMin(((e) => {
|
|
150
|
-
Object.defineProperty(e, "__esModule", { value: true }), e.wrapXOFConstructorWithOpts = e.wrapConstructorWithOpts = e.wrapConstructor = e.Hash = e.nextTick = e.swap32IfBE = e.byteSwapIfBE = e.swap8IfBE = e.isLE = void 0, e.isBytes = S, e.anumber = C, e.abytes = w, e.ahash = T, e.aexists = E, e.aoutput = D, e.u8 = O, e.u32 = k, e.clean = A, e.createView = j, e.rotr = M, e.rotl = N, e.byteSwap = P, e.byteSwap32 = F, e.bytesToHex = R, e.hexToBytes = V, e.asyncLoop =
|
|
154
|
+
Object.defineProperty(e, "__esModule", { value: true }), e.wrapXOFConstructorWithOpts = e.wrapConstructorWithOpts = e.wrapConstructor = e.Hash = e.nextTick = e.swap32IfBE = e.byteSwapIfBE = e.swap8IfBE = e.isLE = void 0, e.isBytes = S, e.anumber = C, e.abytes = w, e.ahash = T, e.aexists = E, e.aoutput = D, e.u8 = O, e.u32 = k, e.clean = A, e.createView = j, e.rotr = M, e.rotl = N, e.byteSwap = P, e.byteSwap32 = F, e.bytesToHex = R, e.hexToBytes = V, e.asyncLoop = H, e.utf8ToBytes = W, e.bytesToUtf8 = G, e.toBytes = K, e.kdfInputToBytes = q, e.concatBytes = J, e.checkOpts = Y, e.createHasher = X, e.createOptHasher = Z, e.createXOFer = Q, e.randomBytes = $;
|
|
151
155
|
var x = require_crypto();
|
|
152
156
|
function S(e2) {
|
|
153
157
|
return e2 instanceof Uint8Array || ArrayBuffer.isView(e2) && e2.constructor.name === "Uint8Array";
|
|
@@ -238,7 +242,7 @@ var require__u64 = /* @__PURE__ */ __commonJSMin(((e) => {
|
|
|
238
242
|
}
|
|
239
243
|
e.nextTick = async () => {
|
|
240
244
|
};
|
|
241
|
-
async function
|
|
245
|
+
async function H(x2, S2, C2) {
|
|
242
246
|
let w2 = Date.now();
|
|
243
247
|
for (let T2 = 0; T2 < x2; T2++) {
|
|
244
248
|
C2(T2);
|
|
@@ -2,7 +2,7 @@ import { s as __toESM } from "./chunk-BNovOVIE.js";
|
|
|
2
2
|
import { t as require_react } from "./react-Bs6Z0kvn.js";
|
|
3
3
|
import { t as require_compiler_runtime } from "./compiler-runtime-B_OLMU9S.js";
|
|
4
4
|
import "./Combination-BTMrlhzT.js";
|
|
5
|
-
import { S as CircleQuestionMark, a as AlertTitle, m as asRemoteURL, n as arrow, o as isValid, r as Alert, t as useDeepCompareMemoize } from "./useDeepCompareMemoize-
|
|
5
|
+
import { S as CircleQuestionMark, a as AlertTitle, m as asRemoteURL, n as arrow, o as isValid, r as Alert, t as useDeepCompareMemoize } from "./useDeepCompareMemoize-BhZZsis0.js";
|
|
6
6
|
import { l as Events } from "./button-Cy0ElmIm.js";
|
|
7
7
|
import { o as Objects, s as Logger } from "./hotkeys-B5WnGZXF.js";
|
|
8
8
|
import { t as require_jsx_runtime } from "./jsx-runtime-CTBg5pdT.js";
|
package/package.json
CHANGED
|
@@ -211,7 +211,7 @@ const InstallButton: React.FC<{
|
|
|
211
211
|
return pkg.name;
|
|
212
212
|
})
|
|
213
213
|
.join(" ");
|
|
214
|
-
const response = await addPackage({ package: packageSpec,
|
|
214
|
+
const response = await addPackage({ package: packageSpec, group: "dev" });
|
|
215
215
|
if (response.success) {
|
|
216
216
|
onSuccess();
|
|
217
217
|
toast({
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
|
|
3
|
+
import { act, renderHook } from "@testing-library/react";
|
|
4
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
5
|
+
import { toast } from "@/components/ui/use-toast";
|
|
6
|
+
import { useFileState } from "../chat-utils";
|
|
7
|
+
|
|
8
|
+
vi.mock("@/components/ui/use-toast", () => ({
|
|
9
|
+
toast: vi.fn(),
|
|
10
|
+
}));
|
|
11
|
+
|
|
12
|
+
describe("useFileState", () => {
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
vi.clearAllMocks();
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("initializes with no files", () => {
|
|
18
|
+
const { result } = renderHook(() => useFileState());
|
|
19
|
+
expect(result.current.files).toEqual([]);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("adds files when under size limit", () => {
|
|
23
|
+
const { result } = renderHook(() => useFileState());
|
|
24
|
+
const fileA = { name: "a.txt", size: 10 } as File;
|
|
25
|
+
const fileB = { name: "b.txt", size: 20 } as File;
|
|
26
|
+
|
|
27
|
+
act(() => result.current.addFiles([fileA, fileB]));
|
|
28
|
+
|
|
29
|
+
expect(result.current.files).toEqual([fileA, fileB]);
|
|
30
|
+
expect(toast).not.toHaveBeenCalled();
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("appends new files to existing files", () => {
|
|
34
|
+
const { result } = renderHook(() => useFileState());
|
|
35
|
+
const fileA = { name: "a.txt", size: 10 } as File;
|
|
36
|
+
const fileB = { name: "b.txt", size: 20 } as File;
|
|
37
|
+
|
|
38
|
+
act(() => result.current.addFiles([fileA]));
|
|
39
|
+
act(() => result.current.addFiles([fileB]));
|
|
40
|
+
|
|
41
|
+
expect(result.current.files).toEqual([fileA, fileB]);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("ignores empty file list", () => {
|
|
45
|
+
const { result } = renderHook(() => useFileState());
|
|
46
|
+
const fileA = { name: "a.txt", size: 10 } as File;
|
|
47
|
+
|
|
48
|
+
act(() => result.current.addFiles([fileA]));
|
|
49
|
+
act(() => result.current.addFiles([]));
|
|
50
|
+
|
|
51
|
+
expect(result.current.files).toEqual([fileA]);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("shows toast and skips adding when size exceeds limit", () => {
|
|
55
|
+
const { result } = renderHook(() => useFileState());
|
|
56
|
+
const bigFile = {
|
|
57
|
+
name: "big.txt",
|
|
58
|
+
size: 1024 * 1024 * 50 + 1, // > 50MB
|
|
59
|
+
} as File;
|
|
60
|
+
|
|
61
|
+
act(() => result.current.addFiles([bigFile]));
|
|
62
|
+
|
|
63
|
+
expect(result.current.files).toEqual([]);
|
|
64
|
+
expect(toast).toHaveBeenCalledWith({
|
|
65
|
+
title: "File size exceeded",
|
|
66
|
+
description: "Attachments must be under 50 MB",
|
|
67
|
+
variant: "danger",
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("removes a file", () => {
|
|
72
|
+
const { result } = renderHook(() => useFileState());
|
|
73
|
+
const fileA = { name: "a.txt", size: 10 } as File;
|
|
74
|
+
const fileB = { name: "b.txt", size: 20 } as File;
|
|
75
|
+
|
|
76
|
+
act(() => result.current.addFiles([fileA, fileB]));
|
|
77
|
+
act(() => result.current.removeFile(fileA));
|
|
78
|
+
|
|
79
|
+
expect(result.current.files).toEqual([fileB]);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("clears all files", () => {
|
|
83
|
+
const { result } = renderHook(() => useFileState());
|
|
84
|
+
const fileA = { name: "a.txt", size: 10 } as File;
|
|
85
|
+
const fileB = { name: "b.txt", size: 20 } as File;
|
|
86
|
+
|
|
87
|
+
act(() => result.current.addFiles([fileA, fileB]));
|
|
88
|
+
expect(result.current.files).toEqual([fileA, fileB]);
|
|
89
|
+
|
|
90
|
+
act(() => result.current.clearFiles());
|
|
91
|
+
expect(result.current.files).toEqual([]);
|
|
92
|
+
});
|
|
93
|
+
});
|
|
@@ -3,12 +3,8 @@
|
|
|
3
3
|
import { useAtom, useAtomValue } from "jotai";
|
|
4
4
|
import { capitalize } from "lodash-es";
|
|
5
5
|
import {
|
|
6
|
-
AtSignIcon,
|
|
7
6
|
BotMessageSquareIcon,
|
|
8
|
-
PaperclipIcon,
|
|
9
7
|
RefreshCwIcon,
|
|
10
|
-
SendIcon,
|
|
11
|
-
SquareIcon,
|
|
12
8
|
StopCircleIcon,
|
|
13
9
|
} from "lucide-react";
|
|
14
10
|
import React, { memo, useEffect, useMemo, useRef, useState } from "react";
|
|
@@ -25,8 +21,7 @@ import {
|
|
|
25
21
|
import { PanelEmptyState } from "@/components/editor/chrome/panels/empty-state";
|
|
26
22
|
import { Spinner } from "@/components/icons/spinner";
|
|
27
23
|
import { Button } from "@/components/ui/button";
|
|
28
|
-
import {
|
|
29
|
-
import { Tooltip, TooltipProvider } from "@/components/ui/tooltip";
|
|
24
|
+
import { TooltipProvider } from "@/components/ui/tooltip";
|
|
30
25
|
import { cn } from "@/utils/cn";
|
|
31
26
|
import { Logger } from "@/utils/Logger";
|
|
32
27
|
import { AgentDocs } from "./agent-docs";
|
|
@@ -70,7 +65,13 @@ import { store } from "@/core/state/jotai";
|
|
|
70
65
|
import { ErrorBanner } from "@/plugins/impl/common/error-banner";
|
|
71
66
|
import { Functions } from "@/utils/functions";
|
|
72
67
|
import { Paths } from "@/utils/paths";
|
|
73
|
-
import {
|
|
68
|
+
import {
|
|
69
|
+
AddContextButton,
|
|
70
|
+
AttachFileButton,
|
|
71
|
+
FileAttachmentPill,
|
|
72
|
+
SendButton,
|
|
73
|
+
} from "../chat-components";
|
|
74
|
+
import { useFileState } from "../chat-utils";
|
|
74
75
|
import { ReadyToChatBlock } from "./blocks";
|
|
75
76
|
import {
|
|
76
77
|
convertFilesToResourceLinks,
|
|
@@ -89,9 +90,6 @@ import type {
|
|
|
89
90
|
|
|
90
91
|
const logger = Logger.get("agents");
|
|
91
92
|
|
|
92
|
-
// File attachment constants
|
|
93
|
-
const SUPPORTED_ATTACHMENT_TYPES = ["image/*", "text/*"];
|
|
94
|
-
|
|
95
93
|
interface AgentTitleProps {
|
|
96
94
|
currentAgentId?: ExternalAgentId;
|
|
97
95
|
}
|
|
@@ -392,55 +390,21 @@ const PromptArea = memo<PromptAreaProps>(
|
|
|
392
390
|
)}
|
|
393
391
|
</div>
|
|
394
392
|
<div className="flex flex-row">
|
|
395
|
-
<
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
className="cursor-pointer"
|
|
410
|
-
onClick={() => fileInputRef.current?.click()}
|
|
411
|
-
title="Attach a file"
|
|
412
|
-
disabled={isLoading}
|
|
413
|
-
>
|
|
414
|
-
<PaperclipIcon className="h-3.5 w-3.5" />
|
|
415
|
-
</Button>
|
|
416
|
-
</Tooltip>
|
|
417
|
-
<Input
|
|
418
|
-
ref={fileInputRef}
|
|
419
|
-
type="file"
|
|
420
|
-
multiple={true}
|
|
421
|
-
hidden={true}
|
|
422
|
-
onChange={(event) => {
|
|
423
|
-
if (event.target.files) {
|
|
424
|
-
onAddFiles([...event.target.files]);
|
|
425
|
-
}
|
|
426
|
-
}}
|
|
427
|
-
accept={SUPPORTED_ATTACHMENT_TYPES.join(",")}
|
|
393
|
+
<AddContextButton
|
|
394
|
+
handleAddContext={handleAddContext}
|
|
395
|
+
isLoading={isLoading}
|
|
396
|
+
/>
|
|
397
|
+
<AttachFileButton
|
|
398
|
+
fileInputRef={fileInputRef}
|
|
399
|
+
isLoading={isLoading}
|
|
400
|
+
onAddFiles={onAddFiles}
|
|
401
|
+
/>
|
|
402
|
+
<SendButton
|
|
403
|
+
isLoading={isLoading}
|
|
404
|
+
onStop={onStop}
|
|
405
|
+
onSendClick={handleSendClick}
|
|
406
|
+
isEmpty={!promptValue.trim()}
|
|
428
407
|
/>
|
|
429
|
-
<Tooltip content={isLoading ? "Stop" : "Submit"}>
|
|
430
|
-
<Button
|
|
431
|
-
variant="text"
|
|
432
|
-
size="sm"
|
|
433
|
-
className="h-6 w-6 p-0 hover:bg-muted/30 cursor-pointer"
|
|
434
|
-
onClick={isLoading ? onStop : handleSendClick}
|
|
435
|
-
disabled={isLoading ? false : !promptValue.trim()}
|
|
436
|
-
>
|
|
437
|
-
{isLoading ? (
|
|
438
|
-
<SquareIcon className="h-3 w-3 fill-current" />
|
|
439
|
-
) : (
|
|
440
|
-
<SendIcon className="h-3 w-3" />
|
|
441
|
-
)}
|
|
442
|
-
</Button>
|
|
443
|
-
</Tooltip>
|
|
444
408
|
</div>
|
|
445
409
|
</div>
|
|
446
410
|
</TooltipProvider>
|
|
@@ -664,7 +628,7 @@ const AgentPanel: React.FC = () => {
|
|
|
664
628
|
const [isLoading, setIsLoading] = useState(false);
|
|
665
629
|
const [error, setError] = useState<Error | string | null>(null);
|
|
666
630
|
const [promptValue, setPromptValue] = useState("");
|
|
667
|
-
const
|
|
631
|
+
const { files, addFiles, clearFiles, removeFile } = useFileState();
|
|
668
632
|
const [sessionModels, setSessionModels] = useState<SessionModelState | null>(
|
|
669
633
|
null,
|
|
670
634
|
);
|
|
@@ -891,7 +855,7 @@ const AgentPanel: React.FC = () => {
|
|
|
891
855
|
});
|
|
892
856
|
setIsLoading(true);
|
|
893
857
|
setPromptValue("");
|
|
894
|
-
|
|
858
|
+
clearFiles();
|
|
895
859
|
|
|
896
860
|
// Update session title with first message if it's still the default
|
|
897
861
|
if (selectedTab?.title.startsWith("New ")) {
|
|
@@ -967,21 +931,6 @@ const AgentPanel: React.FC = () => {
|
|
|
967
931
|
setIsLoading(false);
|
|
968
932
|
});
|
|
969
933
|
|
|
970
|
-
// Handler for adding files
|
|
971
|
-
const handleAddFiles = useEvent((newFiles: File[]) => {
|
|
972
|
-
if (newFiles.length === 0) {
|
|
973
|
-
return;
|
|
974
|
-
}
|
|
975
|
-
setFiles((prev) => [...(prev ?? []), ...newFiles]);
|
|
976
|
-
});
|
|
977
|
-
|
|
978
|
-
// Handler for removing files
|
|
979
|
-
const handleRemoveFile = useEvent((fileToRemove: File) => {
|
|
980
|
-
if (files) {
|
|
981
|
-
setFiles(files.filter((f) => f !== fileToRemove));
|
|
982
|
-
}
|
|
983
|
-
});
|
|
984
|
-
|
|
985
934
|
// Handler for manual connect
|
|
986
935
|
const handleManualConnect = useEvent(() => {
|
|
987
936
|
logger.debug("Manual connect requested", {
|
|
@@ -1148,7 +1097,7 @@ const AgentPanel: React.FC = () => {
|
|
|
1148
1097
|
<FileAttachmentPill
|
|
1149
1098
|
file={file}
|
|
1150
1099
|
key={file.name}
|
|
1151
|
-
onRemove={() =>
|
|
1100
|
+
onRemove={() => removeFile(file)}
|
|
1152
1101
|
/>
|
|
1153
1102
|
))}
|
|
1154
1103
|
</div>
|
|
@@ -1160,7 +1109,7 @@ const AgentPanel: React.FC = () => {
|
|
|
1160
1109
|
promptValue={promptValue}
|
|
1161
1110
|
onPromptValueChange={setPromptValue}
|
|
1162
1111
|
onPromptSubmit={handlePromptSubmit}
|
|
1163
|
-
onAddFiles={
|
|
1112
|
+
onAddFiles={addFiles}
|
|
1164
1113
|
onStop={handleStop}
|
|
1165
1114
|
fileInputRef={fileInputRef}
|
|
1166
1115
|
commands={availableCommands}
|
|
@@ -1,9 +1,23 @@
|
|
|
1
1
|
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
2
|
|
|
3
3
|
import type { FileUIPart } from "ai";
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
AtSignIcon,
|
|
6
|
+
FileIcon,
|
|
7
|
+
FileTextIcon,
|
|
8
|
+
ImageIcon,
|
|
9
|
+
PaperclipIcon,
|
|
10
|
+
SendHorizontalIcon,
|
|
11
|
+
StopCircleIcon,
|
|
12
|
+
XIcon,
|
|
13
|
+
} from "lucide-react";
|
|
5
14
|
import { useState } from "react";
|
|
6
15
|
import { cn } from "@/utils/cn";
|
|
16
|
+
import { Spinner } from "../icons/spinner";
|
|
17
|
+
import { Button } from "../ui/button";
|
|
18
|
+
import { Input } from "../ui/input";
|
|
19
|
+
import { Tooltip } from "../ui/tooltip";
|
|
20
|
+
import { SUPPORTED_ATTACHMENT_TYPES } from "./chat-utils";
|
|
7
21
|
|
|
8
22
|
export const AttachmentRenderer = ({
|
|
9
23
|
attachment,
|
|
@@ -58,6 +72,105 @@ export const FileAttachmentPill = ({
|
|
|
58
72
|
);
|
|
59
73
|
};
|
|
60
74
|
|
|
75
|
+
export const SendButton = ({
|
|
76
|
+
isLoading,
|
|
77
|
+
onStop,
|
|
78
|
+
onSendClick,
|
|
79
|
+
isEmpty,
|
|
80
|
+
showStopLabel = false, // Show a stop label and spinner instead of just the stop icon when loading
|
|
81
|
+
}: {
|
|
82
|
+
isLoading: boolean;
|
|
83
|
+
onStop: () => void;
|
|
84
|
+
onSendClick: () => void;
|
|
85
|
+
isEmpty: boolean;
|
|
86
|
+
showStopLabel?: boolean;
|
|
87
|
+
}) => {
|
|
88
|
+
const loadingContent = showStopLabel ? (
|
|
89
|
+
<div className="flex flex-row items-center gap-1 px-1.5">
|
|
90
|
+
<span className="text-xs text-error">Stop</span>
|
|
91
|
+
<Spinner size="small" />
|
|
92
|
+
</div>
|
|
93
|
+
) : (
|
|
94
|
+
<StopCircleIcon className="size-4 text-error" />
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
return (
|
|
98
|
+
<Tooltip content={isLoading ? "Stop" : "Submit"}>
|
|
99
|
+
<Button
|
|
100
|
+
variant="text"
|
|
101
|
+
size="sm"
|
|
102
|
+
className="h-6 min-w-6 p-0 hover:bg-muted/30 cursor-pointer"
|
|
103
|
+
onClick={isLoading ? onStop : onSendClick}
|
|
104
|
+
disabled={isLoading ? false : isEmpty}
|
|
105
|
+
>
|
|
106
|
+
{isLoading ? (
|
|
107
|
+
loadingContent
|
|
108
|
+
) : (
|
|
109
|
+
<SendHorizontalIcon className="h-3.5 w-3.5" />
|
|
110
|
+
)}
|
|
111
|
+
</Button>
|
|
112
|
+
</Tooltip>
|
|
113
|
+
);
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
export const AddContextButton = ({
|
|
117
|
+
handleAddContext,
|
|
118
|
+
isLoading,
|
|
119
|
+
}: {
|
|
120
|
+
handleAddContext: () => void;
|
|
121
|
+
isLoading: boolean;
|
|
122
|
+
}) => {
|
|
123
|
+
return (
|
|
124
|
+
<Tooltip content="Add context">
|
|
125
|
+
<Button
|
|
126
|
+
variant="text"
|
|
127
|
+
size="icon"
|
|
128
|
+
onClick={handleAddContext}
|
|
129
|
+
disabled={isLoading}
|
|
130
|
+
>
|
|
131
|
+
<AtSignIcon className="h-3.5 w-3.5" />
|
|
132
|
+
</Button>
|
|
133
|
+
</Tooltip>
|
|
134
|
+
);
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
export const AttachFileButton = ({
|
|
138
|
+
fileInputRef,
|
|
139
|
+
isLoading,
|
|
140
|
+
onAddFiles,
|
|
141
|
+
}: {
|
|
142
|
+
fileInputRef: React.RefObject<HTMLInputElement | null>;
|
|
143
|
+
isLoading: boolean;
|
|
144
|
+
onAddFiles: (files: File[]) => void;
|
|
145
|
+
}) => {
|
|
146
|
+
return (
|
|
147
|
+
<>
|
|
148
|
+
<Tooltip content="Attach a file">
|
|
149
|
+
<Button
|
|
150
|
+
variant="text"
|
|
151
|
+
size="icon"
|
|
152
|
+
onClick={() => fileInputRef.current?.click()}
|
|
153
|
+
disabled={isLoading}
|
|
154
|
+
>
|
|
155
|
+
<PaperclipIcon className="h-3.5 w-3.5" />
|
|
156
|
+
</Button>
|
|
157
|
+
</Tooltip>
|
|
158
|
+
<Input
|
|
159
|
+
ref={fileInputRef}
|
|
160
|
+
type="file"
|
|
161
|
+
multiple={true}
|
|
162
|
+
hidden={true}
|
|
163
|
+
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
|
|
164
|
+
if (event.target.files) {
|
|
165
|
+
onAddFiles([...event.target.files]);
|
|
166
|
+
}
|
|
167
|
+
}}
|
|
168
|
+
accept={SUPPORTED_ATTACHMENT_TYPES.join(",")}
|
|
169
|
+
/>
|
|
170
|
+
</>
|
|
171
|
+
);
|
|
172
|
+
};
|
|
173
|
+
|
|
61
174
|
function renderFileIcon(file: File): React.ReactNode {
|
|
62
175
|
const classNames = "h-3 w-3 mt-0.5";
|
|
63
176
|
|