@marimo-team/islands 0.25.3-dev51 → 0.25.3-dev55
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/{ErrorBoundary-CbxCC2kk.js → ErrorBoundary-DL6Vkcwu.js} +1 -1
- package/dist/{chat-ui-C1Gjgokv.js → chat-ui-C0_lCzql.js} +52 -53
- package/dist/{code-visibility-DoZ9lxzx.js → code-visibility-CtaHItnb.js} +1 -1
- package/dist/{common-D1PacyXO.js → common-Bqcl1bUR.js} +6366 -6327
- package/dist/{data-grid-overlay-editor-B50UpkXd.js → data-grid-overlay-editor-C7NRbnX6.js} +1 -1
- package/dist/{dropdown-menu-MG5hiCIa.js → dropdown-menu-B3eUiVJB.js} +161 -153
- package/dist/{feedback-button-DqPqwryB.js → feedback-button-CpJHBUq4.js} +2 -2
- package/dist/{glide-data-editor-BxMhUd3K.js → glide-data-editor-Cd3d8sya.js} +4 -4
- package/dist/{globals-DTJa_doJ.js → globals-C5LrswXh.js} +1 -1
- package/dist/{html-to-image-By72ALyI.js → html-to-image-6xcoTKhC.js} +1 -1
- package/dist/main.js +2599 -2539
- package/dist/{number-overlay-editor-Z_H6x5b-.js → number-overlay-editor-Do7_pdca.js} +1 -1
- package/dist/{process-output-D-OMW5sV.js → process-output-BJRJFobf.js} +1 -1
- package/dist/{reveal-component-tAB5K3QE.js → reveal-component-CVYLwPCH.js} +12 -12
- package/package.json +1 -1
- package/src/components/app-config/ai-config.tsx +1 -1
- package/src/components/data-table/__tests__/export-actions.test.tsx +17 -0
- package/src/components/data-table/charts/charts.tsx +14 -19
- package/src/components/data-table/export-actions.tsx +5 -3
- package/src/components/editor/actions/__tests__/pair-with-agent-modal.test.tsx +24 -0
- package/src/components/editor/actions/pair-with-agent-modal.tsx +18 -15
- package/src/components/editor/cell/__tests__/useShouldShowInterrupt.test.ts +39 -0
- package/src/components/editor/cell/useShouldShowInterrupt.ts +2 -12
- package/src/components/editor/chrome/panels/__tests__/packages-panel.test.tsx +30 -1
- package/src/components/editor/chrome/panels/packages-panel.tsx +4 -7
- package/src/components/editor/file-tree/__tests__/file-explorer-interactions.test.tsx +6 -0
- package/src/components/editor/file-tree/__tests__/use-file-search.test.tsx +91 -0
- package/src/components/editor/file-tree/file-explorer.tsx +10 -5
- package/src/components/editor/file-tree/use-file-search.ts +35 -21
- package/src/components/icons/__tests__/large-spinner.test.tsx +39 -0
- package/src/components/icons/large-spinner.tsx +3 -14
- package/src/core/network/useConnectionNotice.ts +8 -2
- package/src/hooks/__tests__/useDebounce.test.tsx +51 -0
- package/src/hooks/__tests__/useDeepCompareMemoize.test.tsx +39 -0
- package/src/hooks/__tests__/useDelayElapsed.test.tsx +28 -0
- package/src/hooks/__tests__/useInternalStateWithSync.test.tsx +20 -0
- package/src/hooks/__tests__/useScript.test.tsx +77 -0
- package/src/hooks/__tests__/useStateForKey.test.tsx +257 -0
- package/src/hooks/useDebounce.ts +5 -8
- package/src/hooks/useDeepCompareMemoize.ts +1 -1
- package/src/hooks/useDelayElapsed.ts +14 -10
- package/src/hooks/useInternalStateWithSync.ts +6 -6
- package/src/hooks/useScript.ts +67 -70
- package/src/hooks/useStateForKey.ts +93 -0
- package/src/plugins/impl/DataTablePlugin.tsx +14 -15
- package/src/plugins/impl/FileBrowserPlugin.tsx +4 -16
- package/src/plugins/impl/SliderPlugin.tsx +6 -8
- package/src/plugins/impl/__tests__/DataTablePlugin.test.tsx +117 -1
- package/src/plugins/impl/__tests__/FileBrowserPlugin.test.tsx +45 -0
- package/src/plugins/impl/__tests__/SliderPlugin.test.tsx +24 -3
- package/src/plugins/impl/chat/__tests__/chat-prompts.test.tsx +93 -0
- package/src/plugins/impl/chat/chat-ui.tsx +10 -9
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
2
|
|
|
3
3
|
import { type LucideIcon, ChevronRightIcon, CornerLeftUp } from "lucide-react";
|
|
4
|
-
import { type JSX,
|
|
4
|
+
import { type JSX, useLayoutEffect, useRef, useState } from "react";
|
|
5
5
|
import { z } from "zod";
|
|
6
6
|
import {
|
|
7
7
|
FILE_ICON as FILE_TYPE_ICONS,
|
|
@@ -16,6 +16,7 @@ import { Table, TableBody, TableCell, TableRow } from "@/components/ui/table";
|
|
|
16
16
|
import { toast } from "@/components/ui/use-toast";
|
|
17
17
|
import { RANDOM_ID_ATTR } from "@/core/dom/ui-element-constants";
|
|
18
18
|
import { useAsyncData } from "@/hooks/useAsyncData";
|
|
19
|
+
import { useDelayElapsed } from "@/hooks/useDelayElapsed";
|
|
19
20
|
import { useInternalStateWithSync } from "@/hooks/useInternalStateWithSync";
|
|
20
21
|
import { cn } from "@/utils/cn";
|
|
21
22
|
import { type FilePath, PathBuilder, Paths } from "@/utils/paths";
|
|
@@ -200,7 +201,6 @@ export const FileBrowser = ({
|
|
|
200
201
|
}: FileBrowserProps): JSX.Element | null => {
|
|
201
202
|
const [path, setPath] = useInternalStateWithSync(initialPath);
|
|
202
203
|
const [isUpdatingPath, setIsUpdatingPath] = useState(false);
|
|
203
|
-
const [showLoadingOverlay, setShowLoadingOverlay] = useState(false);
|
|
204
204
|
const [activeIndex, setActiveIndex] = useState(0);
|
|
205
205
|
const rowRefs = useRef<(HTMLTableRowElement | null)[]>([]);
|
|
206
206
|
const gridRef = useRef<HTMLTableElement | null>(null);
|
|
@@ -219,20 +219,8 @@ export const FileBrowser = ({
|
|
|
219
219
|
return list_directory({ path: path });
|
|
220
220
|
}, [path, randomId, list_directory]);
|
|
221
221
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
setShowLoadingOverlay(false);
|
|
225
|
-
return;
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
const timeout = window.setTimeout(() => {
|
|
229
|
-
setShowLoadingOverlay(true);
|
|
230
|
-
}, 200);
|
|
231
|
-
|
|
232
|
-
return () => {
|
|
233
|
-
window.clearTimeout(timeout);
|
|
234
|
-
};
|
|
235
|
-
}, [isPending]);
|
|
222
|
+
const loadingDelayElapsed = useDelayElapsed(isPending ? 200 : 0);
|
|
223
|
+
const showLoadingOverlay = isPending && loadingDelayElapsed;
|
|
236
224
|
|
|
237
225
|
// Reset the roving tabindex whenever the listing reloads (a new path or a
|
|
238
226
|
// same-path refresh) so activeIndex never points past the current rows.
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
-
import { type JSX,
|
|
2
|
+
import { type JSX, useId, useMemo } from "react";
|
|
3
3
|
import { useLocale } from "react-aria";
|
|
4
4
|
import { z } from "zod";
|
|
5
5
|
import { NumberField } from "@/components/ui/number-field";
|
|
6
|
+
import { useInternalStateWithSync } from "@/hooks/useInternalStateWithSync";
|
|
6
7
|
import { cn } from "@/utils/cn";
|
|
7
8
|
import { clamp } from "@/utils/math";
|
|
8
9
|
import {
|
|
@@ -156,13 +157,10 @@ const SliderComponent = ({
|
|
|
156
157
|
const { locale } = useLocale();
|
|
157
158
|
|
|
158
159
|
// Hold internal value
|
|
159
|
-
const [internalValue, setInternalValue] =
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
// oxlint-disable-next-line react/no-deriving-state-in-effects -- Synchronize the editable buffer when the controlled value changes.
|
|
164
|
-
setInternalValue(value);
|
|
165
|
-
}, [value]);
|
|
160
|
+
const [internalValue, setInternalValue] = useInternalStateWithSync(
|
|
161
|
+
value,
|
|
162
|
+
Object.is,
|
|
163
|
+
);
|
|
166
164
|
|
|
167
165
|
const stepsConfig = useMemo(() => computeStepsConfig(steps), [steps]);
|
|
168
166
|
|
|
@@ -12,11 +12,13 @@ import {
|
|
|
12
12
|
waitFor,
|
|
13
13
|
} from "@testing-library/react";
|
|
14
14
|
import { Provider } from "jotai";
|
|
15
|
+
import { useState } from "react";
|
|
15
16
|
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
|
16
17
|
import { SetupMocks } from "@/__mocks__/common";
|
|
17
18
|
import type { DownloadAsFunction } from "@/components/data-table/schemas";
|
|
18
19
|
import type { FieldTypesWithExternalType } from "@/components/data-table/types";
|
|
19
20
|
import { store } from "@/core/state/jotai";
|
|
21
|
+
import { Deferred } from "@/utils/Deferred";
|
|
20
22
|
import {
|
|
21
23
|
type GetDataUrl,
|
|
22
24
|
type GetRowIds,
|
|
@@ -463,7 +465,7 @@ describe("LoadingDataTableComponent", () => {
|
|
|
463
465
|
});
|
|
464
466
|
});
|
|
465
467
|
|
|
466
|
-
describe("static notebook control suppression", () => {
|
|
468
|
+
describe("table pagination and static notebook control suppression", () => {
|
|
467
469
|
const fieldTypes: FieldTypesWithExternalType = [
|
|
468
470
|
["id", ["integer", "integer"]],
|
|
469
471
|
["name", ["string", "string"]],
|
|
@@ -538,6 +540,120 @@ describe("static notebook control suppression", () => {
|
|
|
538
540
|
mockIsStatic.mockReturnValue(false);
|
|
539
541
|
});
|
|
540
542
|
|
|
543
|
+
it("requests only the first page with the new configured page size", async () => {
|
|
544
|
+
const props = makeProps();
|
|
545
|
+
const view = (pageSize: number) => (
|
|
546
|
+
<Wrapper>
|
|
547
|
+
<LoadingDataTableComponent {...props} pageSize={pageSize} />
|
|
548
|
+
</Wrapper>
|
|
549
|
+
);
|
|
550
|
+
const { rerender } = render(view(PAGE_SIZE));
|
|
551
|
+
await waitFor(() =>
|
|
552
|
+
expect(screen.getByTestId("next-page-button")).toBeEnabled(),
|
|
553
|
+
);
|
|
554
|
+
fireEvent.click(screen.getByTestId("next-page-button"));
|
|
555
|
+
await waitFor(() =>
|
|
556
|
+
expect(props.search).toHaveBeenLastCalledWith(
|
|
557
|
+
expect.objectContaining({ page_number: 1, page_size: PAGE_SIZE }),
|
|
558
|
+
),
|
|
559
|
+
);
|
|
560
|
+
await waitFor(() =>
|
|
561
|
+
expect(screen.getByTestId("next-page-button")).toBeEnabled(),
|
|
562
|
+
);
|
|
563
|
+
props.search.mockClear();
|
|
564
|
+
|
|
565
|
+
rerender(view(5));
|
|
566
|
+
await waitFor(() => expect(props.search).toHaveBeenCalled());
|
|
567
|
+
expect(
|
|
568
|
+
props.search.mock.calls.every(
|
|
569
|
+
([request]) => request.page_number === 0 && request.page_size === 5,
|
|
570
|
+
),
|
|
571
|
+
).toBe(true);
|
|
572
|
+
});
|
|
573
|
+
|
|
574
|
+
it("preserves a new page size when a pending response changes the row count in the same batch", async () => {
|
|
575
|
+
const props = makeProps();
|
|
576
|
+
const Harness = () => {
|
|
577
|
+
const [pageSize, setPageSize] = useState(PAGE_SIZE);
|
|
578
|
+
return (
|
|
579
|
+
<Wrapper>
|
|
580
|
+
<button type="button" onClick={() => setPageSize(5)}>
|
|
581
|
+
Change configured page size
|
|
582
|
+
</button>
|
|
583
|
+
<LoadingDataTableComponent {...props} pageSize={pageSize} />
|
|
584
|
+
</Wrapper>
|
|
585
|
+
);
|
|
586
|
+
};
|
|
587
|
+
render(<Harness />);
|
|
588
|
+
await waitFor(() =>
|
|
589
|
+
expect(screen.getByTestId("next-page-button")).toBeEnabled(),
|
|
590
|
+
);
|
|
591
|
+
const response = {
|
|
592
|
+
data: firstPage,
|
|
593
|
+
total_rows: 25,
|
|
594
|
+
cell_styles: null,
|
|
595
|
+
cell_hover_texts: null,
|
|
596
|
+
};
|
|
597
|
+
const pendingPage = new Deferred<typeof response>();
|
|
598
|
+
props.search.mockReturnValueOnce(pendingPage.promise);
|
|
599
|
+
fireEvent.click(screen.getByTestId("next-page-button"));
|
|
600
|
+
await waitFor(() =>
|
|
601
|
+
expect(props.search).toHaveBeenLastCalledWith(
|
|
602
|
+
expect.objectContaining({ page_number: 1, page_size: PAGE_SIZE }),
|
|
603
|
+
),
|
|
604
|
+
);
|
|
605
|
+
props.search.mockClear();
|
|
606
|
+
|
|
607
|
+
await act(async () => {
|
|
608
|
+
pendingPage.resolve(response);
|
|
609
|
+
// Settle the table-loading promises without committing the queued render.
|
|
610
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
611
|
+
fireEvent.click(
|
|
612
|
+
screen.getByRole("button", { name: "Change configured page size" }),
|
|
613
|
+
);
|
|
614
|
+
});
|
|
615
|
+
|
|
616
|
+
await waitFor(() => expect(props.search).toHaveBeenCalled());
|
|
617
|
+
expect(props.search).toHaveBeenLastCalledWith(
|
|
618
|
+
expect.objectContaining({ page_number: 0, page_size: 5 }),
|
|
619
|
+
);
|
|
620
|
+
});
|
|
621
|
+
|
|
622
|
+
it("returns to the first page when a refreshed result changes the row count", async () => {
|
|
623
|
+
const props = makeProps();
|
|
624
|
+
const view = () => (
|
|
625
|
+
<Wrapper>
|
|
626
|
+
<LoadingDataTableComponent {...props} />
|
|
627
|
+
</Wrapper>
|
|
628
|
+
);
|
|
629
|
+
const { rerender } = render(view());
|
|
630
|
+
await waitFor(() =>
|
|
631
|
+
expect(screen.getByTestId("next-page-button")).toBeEnabled(),
|
|
632
|
+
);
|
|
633
|
+
fireEvent.click(screen.getByTestId("next-page-button"));
|
|
634
|
+
await waitFor(() =>
|
|
635
|
+
expect(props.search).toHaveBeenLastCalledWith(
|
|
636
|
+
expect.objectContaining({ page_number: 1 }),
|
|
637
|
+
),
|
|
638
|
+
);
|
|
639
|
+
await waitFor(() =>
|
|
640
|
+
expect(screen.getByTestId("next-page-button")).toBeEnabled(),
|
|
641
|
+
);
|
|
642
|
+
props.search = vi.fn().mockResolvedValue({
|
|
643
|
+
data: firstPage,
|
|
644
|
+
total_rows: 25,
|
|
645
|
+
cell_styles: null,
|
|
646
|
+
cell_hover_texts: null,
|
|
647
|
+
});
|
|
648
|
+
|
|
649
|
+
rerender(view());
|
|
650
|
+
await waitFor(() =>
|
|
651
|
+
expect(props.search).toHaveBeenLastCalledWith(
|
|
652
|
+
expect.objectContaining({ page_number: 0, page_size: PAGE_SIZE }),
|
|
653
|
+
),
|
|
654
|
+
);
|
|
655
|
+
});
|
|
656
|
+
|
|
541
657
|
it("renders kernel-dependent controls in normal mode", async () => {
|
|
542
658
|
const props = makeProps();
|
|
543
659
|
render(
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
|
+
act,
|
|
4
5
|
fireEvent,
|
|
5
6
|
render,
|
|
6
7
|
screen,
|
|
@@ -88,6 +89,50 @@ beforeAll(() => {
|
|
|
88
89
|
});
|
|
89
90
|
|
|
90
91
|
describe("FileBrowserPlugin session cache recovery", () => {
|
|
92
|
+
it("delays the initial loading overlay and hides it when listing completes", async () => {
|
|
93
|
+
vi.useFakeTimers();
|
|
94
|
+
try {
|
|
95
|
+
let resolveListing: (value: {
|
|
96
|
+
files: MockFile[];
|
|
97
|
+
total_count: number;
|
|
98
|
+
is_truncated: boolean;
|
|
99
|
+
}) => void = () => undefined;
|
|
100
|
+
const listDirectory = vi.fn(
|
|
101
|
+
() =>
|
|
102
|
+
new Promise((resolve) => {
|
|
103
|
+
resolveListing = resolve;
|
|
104
|
+
}),
|
|
105
|
+
);
|
|
106
|
+
const { unmount } = renderBrowser({ list_directory: listDirectory });
|
|
107
|
+
|
|
108
|
+
await act(async () => {
|
|
109
|
+
vi.advanceTimersByTime(199);
|
|
110
|
+
});
|
|
111
|
+
expect(screen.queryByText("Listing files...")).not.toBeInTheDocument();
|
|
112
|
+
await act(async () => {
|
|
113
|
+
vi.advanceTimersByTime(1);
|
|
114
|
+
});
|
|
115
|
+
expect(screen.getByText("Listing files...")).toBeInTheDocument();
|
|
116
|
+
|
|
117
|
+
await act(async () => {
|
|
118
|
+
resolveListing({
|
|
119
|
+
files: FILES,
|
|
120
|
+
total_count: FILES.length,
|
|
121
|
+
is_truncated: false,
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
expect(screen.queryByText("Listing files...")).not.toBeInTheDocument();
|
|
125
|
+
expect(screen.getByText("docs")).toBeInTheDocument();
|
|
126
|
+
await act(async () => {
|
|
127
|
+
vi.advanceTimersByTime(200);
|
|
128
|
+
});
|
|
129
|
+
expect(screen.queryByText("Listing files...")).not.toBeInTheDocument();
|
|
130
|
+
unmount();
|
|
131
|
+
} finally {
|
|
132
|
+
vi.useRealTimers();
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
|
|
91
136
|
it("retries a failed cached listing when the plugin resets", async () => {
|
|
92
137
|
const host = document.createElement("marimo-file-browser");
|
|
93
138
|
const wrapper = document.createElement("marimo-ui-element");
|
|
@@ -212,6 +212,28 @@ describe("SliderPlugin", () => {
|
|
|
212
212
|
expect(setValue).toHaveBeenCalledWith(9);
|
|
213
213
|
});
|
|
214
214
|
|
|
215
|
+
it("preserves an uncommitted drag until the controlled value changes", () => {
|
|
216
|
+
const plugin = new SliderPlugin();
|
|
217
|
+
const setValue = vi.fn();
|
|
218
|
+
const props = createProps({
|
|
219
|
+
debounce: true,
|
|
220
|
+
includeInput: false,
|
|
221
|
+
setValue,
|
|
222
|
+
});
|
|
223
|
+
const { getByRole, rerender } = render(plugin.render(props));
|
|
224
|
+
|
|
225
|
+
fireEvent.click(getByRole("button", { name: "Slider change" }));
|
|
226
|
+
rerender(
|
|
227
|
+
plugin.render({ ...props, data: { ...props.data, label: "Updated" } }),
|
|
228
|
+
);
|
|
229
|
+
fireEvent.click(getByRole("button", { name: "Slider commit" }));
|
|
230
|
+
expect(setValue).toHaveBeenLastCalledWith(6);
|
|
231
|
+
|
|
232
|
+
rerender(plugin.render({ ...props, value: 2 }));
|
|
233
|
+
fireEvent.click(getByRole("button", { name: "Slider commit" }));
|
|
234
|
+
expect(setValue).toHaveBeenLastCalledWith(2);
|
|
235
|
+
});
|
|
236
|
+
|
|
215
237
|
describe("editable input with steps (regression for #9850)", () => {
|
|
216
238
|
it("displays the actual step value, not the index", () => {
|
|
217
239
|
const plugin = new SliderPlugin();
|
|
@@ -381,9 +403,8 @@ describe("SliderPlugin", () => {
|
|
|
381
403
|
vi.advanceTimersByTime(0);
|
|
382
404
|
});
|
|
383
405
|
|
|
384
|
-
// Shrink `steps` so the held index (4) is now out of range.
|
|
385
|
-
//
|
|
386
|
-
// the stale, out-of-range index.
|
|
406
|
+
// Shrink `steps` so the held index (4) is now out of range. Updating
|
|
407
|
+
// the controlled index must render the new valid step without throwing.
|
|
387
408
|
const shrunkProps = createStepsProps({
|
|
388
409
|
steps: [10, 20],
|
|
389
410
|
valueIndex: 1,
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
|
|
3
|
+
import { fireEvent, render, screen } from "@testing-library/react";
|
|
4
|
+
import type { ComponentProps } from "react";
|
|
5
|
+
import { afterAll, beforeAll, describe, expect, it, vi } from "vitest";
|
|
6
|
+
import type { PopoverContent } from "@/components/ui/popover";
|
|
7
|
+
import { TooltipProvider } from "@/components/ui/tooltip";
|
|
8
|
+
import { Chatbot } from "../chat-ui";
|
|
9
|
+
|
|
10
|
+
vi.mock("@/components/editor/ai/add-cell-with-ai", () => ({
|
|
11
|
+
PromptInput: ({ value }: { value: string }) => (
|
|
12
|
+
<textarea aria-label="Chat prompt" value={value} readOnly={true} />
|
|
13
|
+
),
|
|
14
|
+
}));
|
|
15
|
+
|
|
16
|
+
vi.mock("@/components/ui/popover", async (importOriginal) => {
|
|
17
|
+
const actual =
|
|
18
|
+
await importOriginal<typeof import("@/components/ui/popover")>();
|
|
19
|
+
return {
|
|
20
|
+
...actual,
|
|
21
|
+
// Keep the form mounted when the prompt menu takes focus so this tests
|
|
22
|
+
// the prompt reset rather than the overlay's incidental unmount.
|
|
23
|
+
PopoverContent: (props: ComponentProps<typeof PopoverContent>) => (
|
|
24
|
+
<actual.PopoverContent
|
|
25
|
+
{...props}
|
|
26
|
+
onInteractOutside={(event) => event.preventDefault()}
|
|
27
|
+
/>
|
|
28
|
+
),
|
|
29
|
+
};
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
describe("chat prompt variables", () => {
|
|
33
|
+
const originalScrollTo = HTMLElement.prototype.scrollTo;
|
|
34
|
+
beforeAll(() => {
|
|
35
|
+
HTMLElement.prototype.scrollTo = vi.fn();
|
|
36
|
+
});
|
|
37
|
+
afterAll(() => {
|
|
38
|
+
HTMLElement.prototype.scrollTo = originalScrollTo;
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("clears entered values and replaces the fields when selecting another prompt", async () => {
|
|
42
|
+
const firstPrompt = "Tell {{name}} about {{topic}}";
|
|
43
|
+
const secondPrompt = "Send {{name}} to {{destination}}";
|
|
44
|
+
render(
|
|
45
|
+
<TooltipProvider>
|
|
46
|
+
<Chatbot
|
|
47
|
+
prompts={[firstPrompt, secondPrompt]}
|
|
48
|
+
config={{
|
|
49
|
+
max_tokens: null,
|
|
50
|
+
temperature: null,
|
|
51
|
+
top_p: null,
|
|
52
|
+
top_k: null,
|
|
53
|
+
frequency_penalty: null,
|
|
54
|
+
presence_penalty: null,
|
|
55
|
+
}}
|
|
56
|
+
showConfigurationControls={false}
|
|
57
|
+
maxHeight={undefined}
|
|
58
|
+
allowAttachments={false}
|
|
59
|
+
disabled={false}
|
|
60
|
+
value={[]}
|
|
61
|
+
setValue={vi.fn()}
|
|
62
|
+
host={document.createElement("div")}
|
|
63
|
+
get_chat_history={vi.fn().mockResolvedValue({ messages: [] })}
|
|
64
|
+
delete_chat_history={vi.fn().mockResolvedValue(null)}
|
|
65
|
+
delete_chat_message={vi.fn().mockResolvedValue(null)}
|
|
66
|
+
send_prompt={vi.fn().mockResolvedValue(null)}
|
|
67
|
+
cancel_prompt={vi.fn().mockResolvedValue(null)}
|
|
68
|
+
/>
|
|
69
|
+
</TooltipProvider>,
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
const promptMenu = screen.getByRole("button", { expanded: false });
|
|
73
|
+
fireEvent.keyDown(promptMenu, { key: "ArrowDown" });
|
|
74
|
+
fireEvent.click(await screen.findByRole("menuitem", { name: firstPrompt }));
|
|
75
|
+
fireEvent.change(await screen.findByLabelText("name"), {
|
|
76
|
+
target: { value: "Ada" },
|
|
77
|
+
});
|
|
78
|
+
fireEvent.change(screen.getByLabelText("topic"), {
|
|
79
|
+
target: { value: "Python" },
|
|
80
|
+
});
|
|
81
|
+
expect(screen.getByRole("button", { name: "Submit" })).toBeEnabled();
|
|
82
|
+
|
|
83
|
+
fireEvent.keyDown(promptMenu, { key: "ArrowDown" });
|
|
84
|
+
fireEvent.click(
|
|
85
|
+
await screen.findByRole("menuitem", { name: secondPrompt }),
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
expect(await screen.findByLabelText("destination")).toHaveValue("");
|
|
89
|
+
expect(screen.getByLabelText("name")).toHaveValue("");
|
|
90
|
+
expect(screen.queryByLabelText("topic")).not.toBeInTheDocument();
|
|
91
|
+
expect(screen.getByRole("button", { name: "Submit" })).toBeDisabled();
|
|
92
|
+
});
|
|
93
|
+
});
|
|
@@ -24,7 +24,7 @@ import {
|
|
|
24
24
|
Trash2Icon,
|
|
25
25
|
X,
|
|
26
26
|
} from "lucide-react";
|
|
27
|
-
import React, { useEffect, useRef, useState } from "react";
|
|
27
|
+
import React, { useEffect, useLayoutEffect, useRef, useState } from "react";
|
|
28
28
|
import { z } from "zod";
|
|
29
29
|
import {
|
|
30
30
|
describeChatAbortReason,
|
|
@@ -163,7 +163,9 @@ export const Chatbot: React.FC<Props> = (props) => {
|
|
|
163
163
|
|
|
164
164
|
// Use a ref to avoid stale closure in the fetch callback
|
|
165
165
|
const configRef = useRef<ChatConfig>(config);
|
|
166
|
-
|
|
166
|
+
useLayoutEffect(() => {
|
|
167
|
+
configRef.current = config;
|
|
168
|
+
}, [config]);
|
|
167
169
|
|
|
168
170
|
// For frontend-managed streaming, create a controller to enqueue chunks to.
|
|
169
171
|
const frontendStreamControllerRef =
|
|
@@ -411,9 +413,8 @@ export const Chatbot: React.FC<Props> = (props) => {
|
|
|
411
413
|
});
|
|
412
414
|
}, [messages.length, scrollContainerRef]);
|
|
413
415
|
|
|
414
|
-
const codemirrorView = codeMirrorInputRef.current?.view;
|
|
415
|
-
|
|
416
416
|
const resetInput = () => {
|
|
417
|
+
const codemirrorView = codeMirrorInputRef.current?.view;
|
|
417
418
|
// Clear input immediately by directly manipulating the editor
|
|
418
419
|
// There is some delay if we use setInput("") only
|
|
419
420
|
if (codemirrorView) {
|
|
@@ -566,6 +567,7 @@ export const Chatbot: React.FC<Props> = (props) => {
|
|
|
566
567
|
onSelect={(prompt) => {
|
|
567
568
|
setInput(prompt);
|
|
568
569
|
requestAnimationFrame(() => {
|
|
570
|
+
const codemirrorView = codeMirrorInputRef.current?.view;
|
|
569
571
|
codemirrorView?.focus();
|
|
570
572
|
moveToEndOfEditor(codemirrorView);
|
|
571
573
|
});
|
|
@@ -862,6 +864,7 @@ const PromptsPopover: React.FC<{
|
|
|
862
864
|
|
|
863
865
|
<PopoverContent side="right" align="end" className="min-w-80 px-2">
|
|
864
866
|
<PromptVariablesForm
|
|
867
|
+
key={selectedPrompt}
|
|
865
868
|
prompt={selectedPrompt}
|
|
866
869
|
onClose={() => setIsPopoverOpen(false)}
|
|
867
870
|
onSelect={onSelect}
|
|
@@ -876,9 +879,7 @@ const PromptVariablesForm: React.FC<{
|
|
|
876
879
|
onClose: () => void;
|
|
877
880
|
onSelect: (prompt: string) => void;
|
|
878
881
|
}> = ({ prompt, onClose, onSelect }) => {
|
|
879
|
-
const [variables, setVariables] = useState<{ [key: string]: string }>({
|
|
880
|
-
|
|
881
|
-
useEffect(() => {
|
|
882
|
+
const [variables, setVariables] = useState<{ [key: string]: string }>(() => {
|
|
882
883
|
const variableRegex = /{{(\w+)}}/g;
|
|
883
884
|
const matches = [...prompt.matchAll(variableRegex)];
|
|
884
885
|
const initialVariables = matches.reduce<{ [key: string]: string }>(
|
|
@@ -888,8 +889,8 @@ const PromptVariablesForm: React.FC<{
|
|
|
888
889
|
},
|
|
889
890
|
{},
|
|
890
891
|
);
|
|
891
|
-
|
|
892
|
-
}
|
|
892
|
+
return initialVariables;
|
|
893
|
+
});
|
|
893
894
|
|
|
894
895
|
const handleVariableChange = (variable: string, value: string) => {
|
|
895
896
|
setVariables((prev) => ({ ...prev, [variable]: value }));
|