@marimo-team/frontend 0.19.3-dev7 → 0.19.3
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/{CellStatus-Ba6Af_Tb.js → CellStatus--kUu6N2K.js} +1 -1
- package/dist/assets/{ConnectedDataExplorerComponent-KlUs_Sz3.js → ConnectedDataExplorerComponent-BKJwCHu7.js} +1 -1
- package/dist/assets/{ErrorBoundary-Drf1manw.js → ErrorBoundary-C7JBxSzd.js} +1 -1
- package/dist/assets/{ImperativeModal-q6QlC2aZ.js → ImperativeModal-DVhvP4lH.js} +1 -1
- package/dist/assets/{JsonOutput-4ruRfyOj.js → JsonOutput-BSGE-MRo.js} +5 -5
- package/dist/assets/{LazyAnyLanguageCodeMirror-jpEDlD0M.js → LazyAnyLanguageCodeMirror-Cp2punaU.js} +2 -2
- package/dist/assets/{MarimoErrorOutput-DnjH3pD8.js → MarimoErrorOutput-CX0SCJOZ.js} +2 -2
- package/dist/assets/{RenderHTML-DaJXe2U2.js → RenderHTML-Do_PVqRy.js} +1 -1
- package/dist/assets/VisuallyHidden-B9t3FhTP.js +1 -0
- package/dist/assets/{add-cell-with-ai-Bsds_6SU.js → add-cell-with-ai-manh7kBT.js} +21 -21
- package/dist/assets/{add-database-form-CqIp3_WN.js → add-database-form-CgkV0MRs.js} +2 -2
- package/dist/assets/agent-panel-D-OmT-rw.js +287 -0
- package/dist/assets/{ai-model-dropdown-LK8Wr5iu.js → ai-model-dropdown-DzyBY5VA.js} +1 -1
- package/dist/assets/{alert-dialog-k5KxevGr.js → alert-dialog-jcHA5geR.js} +1 -1
- package/dist/assets/{any-language-editor-DQu1Tt2N.js → any-language-editor-Cm83E7D_.js} +1 -1
- package/dist/assets/{app-config-button-BaVc4Y5z.js → app-config-button-DC3alCuB.js} +1 -1
- package/dist/assets/button-B8cGZzP5.js +1 -0
- package/dist/assets/{cache-panel-C1So4Zu3.js → cache-panel-1FqnpB9y.js} +1 -1
- package/dist/assets/cell-editor-RHFZmO74.js +23 -0
- package/dist/assets/cell-link-Dqj_nfXA.js +1 -0
- package/dist/assets/{cells-KYKWFk6C.js → cells-BNQUQiDS.js} +49 -49
- package/dist/assets/{chat-components-O6DUIpBx.js → chat-components-CWiXtKu6.js} +1 -1
- package/dist/assets/{chat-display-DD3KokYi.js → chat-display-CGnOamQG.js} +1 -1
- package/dist/assets/{chat-panel-D4DIcOM1.js → chat-panel-Dh1M55c9.js} +2 -2
- package/dist/assets/client-CDjmJmVw.js +4 -0
- package/dist/assets/{column-preview-EpCGr4Xp.js → column-preview-CKxT2s-S.js} +1 -1
- package/dist/assets/{command-Dqe0kvHp.js → command-YPFTinLj.js} +1 -1
- package/dist/assets/{command-palette-DWacsFDk.js → command-palette-7fVEhKGc.js} +1 -1
- package/dist/assets/common-DJkPpBxC.js +1 -0
- package/dist/assets/config-D6nhy4FA.js +1 -0
- package/dist/assets/context-DHfVoQfl.js +1 -0
- package/dist/assets/{copy-icon-B69c-352.js → copy-icon-jWsqdLn1.js} +1 -1
- package/dist/assets/{datasource-JeWYnuIr.js → datasource-DerBLc6V.js} +2 -2
- package/dist/assets/{dependency-graph-panel-BJibnwCO.js → dependency-graph-panel-Vd-OsVLa.js} +4 -4
- package/dist/assets/{dialog-DUEuLcT2.js → dialog-CF5DtF1E.js} +1 -1
- package/dist/assets/{dist-DOFFh6Ii.js → dist-Dg7UO_Vw.js} +1 -1
- package/dist/assets/{documentation-panel-B2W3q2YB.js → documentation-panel-xG2-zpwg.js} +1 -1
- package/dist/assets/{download-NfnO_JCs.js → download-B6EJS7Ar.js} +1 -1
- package/dist/assets/edit-page-7Hkti2j_.js +12 -0
- package/dist/assets/{error-banner-DU5Qb8a8.js → error-banner-DvT0IGDZ.js} +1 -1
- package/dist/assets/{error-panel-Bv-7GYgJ.js → error-panel-BxBpZYvt.js} +1 -1
- package/dist/assets/{es-KtEicG7U.js → es-BoHEdemq.js} +1 -1
- package/dist/assets/{field-DDKGFzpC.js → field-Clr_fqUr.js} +1 -1
- package/dist/assets/{file-explorer-panel-CToUezud.js → file-explorer-panel-C9K0vIPl.js} +1 -1
- package/dist/assets/{floating-outline-Db40vhG8.js → floating-outline-DCrTuu2G.js} +1 -1
- package/dist/assets/{focus-BCdX47jS.js → focus-DM53w5BH.js} +1 -1
- package/dist/assets/{form-DwtJQd_Z.js → form-BcKfhfZc.js} +2 -2
- package/dist/assets/{glide-data-editor-D_bRnWfy.js → glide-data-editor-CRb9AiCG.js} +1 -1
- package/dist/assets/{globals-MS86g8oR.js → globals-Bf30kOQF.js} +1 -1
- package/dist/assets/{home-page-BfVf41OG.js → home-page-BRyNf7fl.js} +2 -2
- package/dist/assets/house-CncUa_LL.js +1 -0
- package/dist/assets/index-CBMqMxiq.js +43 -0
- package/dist/assets/index-DDc_1b-N.css +2 -0
- package/dist/assets/input-B80Yt1uu.js +1 -0
- package/dist/assets/{kiosk-mode-CEhvsEr0.js → kiosk-mode-P-NYHJID.js} +1 -1
- package/dist/assets/{label-qwandMoh.js → label-CNZLffHW.js} +1 -1
- package/dist/assets/{layout-Cvaok8Kj.js → layout-DT91GUei.js} +4 -4
- package/dist/assets/links-D529u6GQ.js +1 -0
- package/dist/assets/{logs-panel-J2FKnKaj.js → logs-panel-C2dfrRig.js} +1 -1
- package/dist/assets/{markdown-renderer-BlG9DgUG.js → markdown-renderer-BPnVa0ym.js} +2 -2
- package/dist/assets/{mermaid-BPkO79lo.js → mermaid--ZwxKP7u.js} +1 -1
- package/dist/assets/mode-Dq8MKjNR.js +1 -0
- package/dist/assets/{multi-map-fjX9ImVF.js → multi-map-CQd4MZr5.js} +1 -1
- package/dist/assets/name-cell-input-BaEPC7ON.js +1 -0
- package/dist/assets/{outline-panel-Doj3GJrQ.js → outline-panel-Cca864H0.js} +1 -1
- package/dist/assets/{packages-panel-nqWXQzKf.js → packages-panel-Cy_KAYmq.js} +1 -1
- package/dist/assets/panels-BzlLZfye.js +1 -0
- package/dist/assets/{process-output-DiSW8Nbo.js → process-output-Dn1rOp26.js} +1 -1
- package/dist/assets/{readonly-python-code-CKY5LsMp.js → readonly-python-code-CXeF74Iq.js} +1 -1
- package/dist/assets/{renderShortcut-D0Pei-OA.js → renderShortcut-eU5Hsfml.js} +1 -1
- package/dist/assets/run-page-CM_n6pXD.js +1 -0
- package/dist/assets/scratchpad-panel-XCkVY3Hp.js +1 -0
- package/dist/assets/{secrets-panel-CDWmmmBS.js → secrets-panel-BMY6PPth.js} +1 -1
- package/dist/assets/{select-D0g5GnIs.js → select-D9lTzMzP.js} +1 -1
- package/dist/assets/{session-panel-CGFRSBw9.js → session-panel-BDt6Y_mU.js} +1 -1
- package/dist/assets/{slides-component-MkPkpql1.js → slides-component-Dp0Yv5b0.js} +1 -1
- package/dist/assets/{snippets-panel-ClHeSpc5.js → snippets-panel-K-JKJQBf.js} +1 -1
- package/dist/assets/state-DWRZTH2y.js +1 -0
- package/dist/assets/state-JzO-Ni5T.js +1 -0
- package/dist/assets/{switch-BmbGJWHc.js → switch-RowEjq0T.js} +1 -1
- package/dist/assets/{terminal-BvgBa6Ri.js → terminal-BhbNfCNw.js} +1 -1
- package/dist/assets/{textarea-WklymBeK.js → textarea-Di1KKcL4.js} +1 -1
- package/dist/assets/{tracing-D0WYhZdr.js → tracing-nvbrZdpf.js} +1 -1
- package/dist/assets/{tracing-panel-CNxN58z7.js → tracing-panel-CTXJaO-A.js} +2 -2
- package/dist/assets/{types-BrgXpvGt.js → types-CT2U5Ljy.js} +1 -1
- package/dist/assets/{useAddCell-a9qZ0_KE.js → useAddCell-COb93CUl.js} +1 -1
- package/dist/assets/{useBoolean-5kuXz69O.js → useBoolean-B_S7yTZz.js} +1 -1
- package/dist/assets/{useCellActionButton-9W_R41MM.js → useCellActionButton-D5Zt1dDz.js} +1 -1
- package/dist/assets/{useDateFormatter-CV0QXb5P.js → useDateFormatter-DsANziQR.js} +1 -1
- package/dist/assets/useDeleteCell-DHF_xvAh.js +1 -0
- package/dist/assets/{useDependencyPanelTab-0reaqvvh.js → useDependencyPanelTab-D59iW_MD.js} +1 -1
- package/dist/assets/useInterval-BGPIviJp.js +1 -0
- package/dist/assets/useNotebookActions-DEl-rH-3.js +1 -0
- package/dist/assets/{useNumberFormatter-D8ks3oPN.js → useNumberFormatter-FoXhpyAb.js} +1 -1
- package/dist/assets/usePress-DTwIUo40.js +7 -0
- package/dist/assets/useRunCells-CKEmgeKM.js +1 -0
- package/dist/assets/useSplitCell-D9YiO-z5.js +1 -0
- package/dist/assets/{useTheme-DfP1CWaW.js → useTheme-CNj0G_ol.js} +1 -1
- package/dist/assets/utilities.esm-DG4qccZc.js +3 -0
- package/dist/assets/utils-pfqq9IdB.js +1 -0
- package/dist/assets/{vega-component-DpAAiTdH.js → vega-component-C1voDf5W.js} +1 -1
- package/dist/assets/{write-secret-modal-CLm48gMe.js → write-secret-modal-hOetwavI.js} +1 -1
- package/dist/index.html +57 -57
- package/package.json +5 -5
- package/src/__mocks__/requests.ts +1 -0
- package/src/__tests__/mount.test.ts +128 -0
- package/src/components/app-config/__tests__/get-dirty-values.test.ts +1 -1
- package/src/components/app-config/ai-config.tsx +328 -28
- package/src/components/app-config/user-config-form.tsx +10 -3
- package/src/components/chat/acp/agent-panel.tsx +56 -43
- package/src/components/chat/chat-utils.ts +0 -19
- package/src/components/data-table/column-header.tsx +1 -1
- package/src/components/editor/KernelStartupErrorModal.tsx +101 -0
- package/src/components/editor/actions/name-cell-input.tsx +10 -4
- package/src/components/editor/ai/completion-handlers.tsx +1 -1
- package/src/components/editor/alerts/connecting-alert.tsx +33 -6
- package/src/components/editor/chrome/types.ts +2 -4
- package/src/components/editor/chrome/wrapper/app-chrome.tsx +55 -58
- package/src/components/editor/chrome/wrapper/footer-items/runtime-settings.tsx +150 -96
- package/src/components/editor/renderers/vertical-layout/__tests__/useFocusFirstEditor.test.ts +27 -0
- package/src/components/editor/renderers/vertical-layout/useFocusFirstEditor.ts +6 -0
- package/src/components/utils/lazy-mount.tsx +29 -8
- package/src/core/MarimoApp.tsx +2 -0
- package/src/core/ai/ids/ids.ts +12 -4
- package/src/core/cells/cells.ts +2 -0
- package/src/core/cells/scrollCellIntoView.ts +3 -2
- package/src/core/codemirror/cm.ts +2 -0
- package/src/core/codemirror/lsp/__tests__/notebook-lsp.test.ts +123 -0
- package/src/core/codemirror/lsp/notebook-lsp.ts +44 -4
- package/src/core/codemirror/misc/__tests__/string-braces.test.ts +200 -0
- package/src/core/codemirror/misc/string-braces.ts +37 -0
- package/src/core/config/__tests__/config-schema.test.ts +36 -0
- package/src/core/config/config-schema.ts +1 -0
- package/src/core/errors/state.ts +7 -1
- package/src/core/export/__tests__/hooks.test.ts +504 -0
- package/src/core/export/hooks.ts +93 -4
- package/src/core/islands/bridge.ts +1 -0
- package/src/core/islands/main.ts +2 -0
- package/src/core/kernel/__tests__/handlers.test.ts +2 -2
- package/src/core/kernel/state.ts +1 -0
- package/src/core/network/__tests__/requests-lazy.test.ts +1 -1
- package/src/core/network/__tests__/requests-network.test.ts +0 -18
- package/src/core/network/requests-lazy.ts +3 -2
- package/src/core/network/requests-network.ts +10 -7
- package/src/core/network/requests-static.ts +1 -0
- package/src/core/network/requests-toasting.tsx +1 -0
- package/src/core/network/types.ts +2 -0
- package/src/core/wasm/bridge.ts +1 -0
- package/src/core/websocket/types.ts +1 -0
- package/src/core/websocket/useMarimoKernelConnection.tsx +18 -1
- package/src/css/globals.css +2 -0
- package/src/hooks/__tests__/useInterval.test.tsx +104 -0
- package/src/hooks/useInterval.ts +32 -6
- package/src/mount.tsx +6 -0
- package/src/plugins/impl/chat/ChatPlugin.tsx +2 -4
- package/src/plugins/impl/chat/chat-ui.tsx +62 -191
- package/src/plugins/impl/chat/types.ts +5 -12
- package/src/plugins/impl/data-frames/DataFramePlugin.tsx +3 -1
- package/src/utils/events.ts +1 -0
- package/dist/assets/VisuallyHidden-BodIky8L.js +0 -1
- package/dist/assets/agent-panel-CaAPVPdJ.js +0 -287
- package/dist/assets/button-DuYGqRtX.js +0 -1
- package/dist/assets/cell-editor-OFm-OSAP.js +0 -23
- package/dist/assets/cell-link-CfLJRl3p.js +0 -1
- package/dist/assets/client-Cha_JfGC.js +0 -4
- package/dist/assets/common-A6YWtmpq.js +0 -1
- package/dist/assets/config-babG4OBR.js +0 -1
- package/dist/assets/context-BAYdLMF_.js +0 -1
- package/dist/assets/edit-page-nuU4FVXi.js +0 -12
- package/dist/assets/globe-CY9im410.js +0 -1
- package/dist/assets/index-BI88xbv4.js +0 -43
- package/dist/assets/index-Chgc_07S.css +0 -2
- package/dist/assets/input-CaEtLL8p.js +0 -1
- package/dist/assets/links-ENMiP32L.js +0 -1
- package/dist/assets/mode-CK5Oq-Jz.js +0 -1
- package/dist/assets/name-cell-input-D7axzd6k.js +0 -1
- package/dist/assets/panels-CdYbZBqo.js +0 -1
- package/dist/assets/run-page-GP8eGE39.js +0 -1
- package/dist/assets/scratchpad-panel-B1p8zqAE.js +0 -1
- package/dist/assets/state-BBgXjqJI.js +0 -1
- package/dist/assets/state-CP7_TGWl.js +0 -1
- package/dist/assets/useDeleteCell-5kJUaejE.js +0 -1
- package/dist/assets/useInterval-DpipYmgs.js +0 -1
- package/dist/assets/useNotebookActions-o341ZCMJ.js +0 -1
- package/dist/assets/usePress-C2LPFxyv.js +0 -7
- package/dist/assets/useRunCells-wXhl9zOP.js +0 -1
- package/dist/assets/useSplitCell-mmm5jxn2.js +0 -1
- package/dist/assets/utilities.esm-Ckt5kMF-.js +0 -3
- package/dist/assets/utils-CJJIceVn.js +0 -1
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
|
|
3
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
4
|
+
import { connectionAtom } from "@/core/network/connection";
|
|
5
|
+
import { store } from "@/core/state/jotai";
|
|
6
|
+
import { WebSocketState } from "@/core/websocket/types";
|
|
7
|
+
import { mount, visibleForTesting } from "../mount";
|
|
8
|
+
|
|
9
|
+
// Mock React DOM
|
|
10
|
+
vi.mock("react-dom/client", () => ({
|
|
11
|
+
createRoot: vi.fn(() => ({
|
|
12
|
+
render: vi.fn(),
|
|
13
|
+
})),
|
|
14
|
+
}));
|
|
15
|
+
|
|
16
|
+
// Mock static state
|
|
17
|
+
vi.mock("@/core/static/static-state", () => ({
|
|
18
|
+
isStaticNotebook: vi.fn(() => false),
|
|
19
|
+
}));
|
|
20
|
+
|
|
21
|
+
// Mock other side-effect modules
|
|
22
|
+
vi.mock("@/core/vscode/vscode-bindings", () => ({
|
|
23
|
+
maybeRegisterVSCodeBindings: vi.fn(),
|
|
24
|
+
}));
|
|
25
|
+
|
|
26
|
+
vi.mock("@/plugins/plugins", () => ({
|
|
27
|
+
initializePlugins: vi.fn(),
|
|
28
|
+
}));
|
|
29
|
+
|
|
30
|
+
vi.mock("@/core/network/auth", () => ({
|
|
31
|
+
cleanupAuthQueryParams: vi.fn(),
|
|
32
|
+
}));
|
|
33
|
+
|
|
34
|
+
vi.mock("@/utils/vitals", () => ({
|
|
35
|
+
reportVitals: vi.fn(),
|
|
36
|
+
}));
|
|
37
|
+
|
|
38
|
+
// Mock preloadPage
|
|
39
|
+
vi.mock("@/core/MarimoApp", () => ({
|
|
40
|
+
MarimoApp: () => null,
|
|
41
|
+
preloadPage: vi.fn(),
|
|
42
|
+
}));
|
|
43
|
+
|
|
44
|
+
describe("mount", () => {
|
|
45
|
+
const mockElement = document.createElement("div");
|
|
46
|
+
|
|
47
|
+
beforeEach(() => {
|
|
48
|
+
visibleForTesting.reset();
|
|
49
|
+
// Reset connection atom to initial state
|
|
50
|
+
store.set(connectionAtom, { state: WebSocketState.NOT_STARTED });
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
afterEach(() => {
|
|
54
|
+
vi.clearAllMocks();
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const baseOptions = {
|
|
58
|
+
filename: "test.py",
|
|
59
|
+
code: "",
|
|
60
|
+
version: "0.0.1",
|
|
61
|
+
mode: "edit" as const,
|
|
62
|
+
config: {},
|
|
63
|
+
configOverrides: {},
|
|
64
|
+
appConfig: {},
|
|
65
|
+
view: { showAppCode: true },
|
|
66
|
+
serverToken: "",
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
describe("connection state initialization", () => {
|
|
70
|
+
it("should set connection to CONNECTING when runtimeConfig has lazy=false", () => {
|
|
71
|
+
mount(
|
|
72
|
+
{
|
|
73
|
+
...baseOptions,
|
|
74
|
+
runtimeConfig: [{ url: "http://localhost:8080", lazy: false }],
|
|
75
|
+
},
|
|
76
|
+
mockElement,
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
const connection = store.get(connectionAtom);
|
|
80
|
+
expect(connection.state).toBe(WebSocketState.CONNECTING);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("should keep connection as NOT_STARTED when runtimeConfig has lazy=true", () => {
|
|
84
|
+
mount(
|
|
85
|
+
{
|
|
86
|
+
...baseOptions,
|
|
87
|
+
runtimeConfig: [{ url: "http://localhost:8080", lazy: true }],
|
|
88
|
+
},
|
|
89
|
+
mockElement,
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
const connection = store.get(connectionAtom);
|
|
93
|
+
expect(connection.state).toBe(WebSocketState.NOT_STARTED);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("should keep connection as NOT_STARTED when no runtimeConfig is provided", () => {
|
|
97
|
+
mount(
|
|
98
|
+
{
|
|
99
|
+
...baseOptions,
|
|
100
|
+
runtimeConfig: [],
|
|
101
|
+
},
|
|
102
|
+
mockElement,
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
const connection = store.get(connectionAtom);
|
|
106
|
+
expect(connection.state).toBe(WebSocketState.NOT_STARTED);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("should keep connection as NOT_STARTED for static notebooks even with lazy=false", async () => {
|
|
110
|
+
const { isStaticNotebook } = await import("@/core/static/static-state");
|
|
111
|
+
vi.mocked(isStaticNotebook).mockReturnValue(true);
|
|
112
|
+
|
|
113
|
+
// Reset mount state to allow another mount
|
|
114
|
+
visibleForTesting.reset();
|
|
115
|
+
|
|
116
|
+
mount(
|
|
117
|
+
{
|
|
118
|
+
...baseOptions,
|
|
119
|
+
runtimeConfig: [{ url: "http://localhost:8080", lazy: false }],
|
|
120
|
+
},
|
|
121
|
+
mockElement,
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
const connection = store.get(connectionAtom);
|
|
125
|
+
expect(connection.state).toBe(WebSocketState.NOT_STARTED);
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
});
|
|
@@ -34,7 +34,8 @@ import { Textarea } from "@/components/ui/textarea";
|
|
|
34
34
|
import type { SupportedRole } from "@/core/ai/config";
|
|
35
35
|
import {
|
|
36
36
|
AiModelId,
|
|
37
|
-
|
|
37
|
+
KNOWN_PROVIDERS,
|
|
38
|
+
type KnownProviderId,
|
|
38
39
|
type ProviderId,
|
|
39
40
|
type QualifiedModelId,
|
|
40
41
|
type ShortModelId,
|
|
@@ -90,6 +91,11 @@ interface AiProviderTitleProps {
|
|
|
90
91
|
children: React.ReactNode;
|
|
91
92
|
}
|
|
92
93
|
|
|
94
|
+
interface CustomProviderConfig {
|
|
95
|
+
api_key?: string;
|
|
96
|
+
base_url?: string;
|
|
97
|
+
}
|
|
98
|
+
|
|
93
99
|
export const AiProviderTitle: React.FC<AiProviderTitleProps> = ({
|
|
94
100
|
provider,
|
|
95
101
|
children,
|
|
@@ -555,15 +561,18 @@ const AccordionFormItem = ({
|
|
|
555
561
|
provider,
|
|
556
562
|
children,
|
|
557
563
|
isConfigured,
|
|
564
|
+
value,
|
|
558
565
|
}: {
|
|
559
566
|
title: string;
|
|
560
567
|
triggerClassName?: string;
|
|
561
568
|
provider: AiProviderIconProps["provider"];
|
|
562
569
|
children: React.ReactNode;
|
|
563
570
|
isConfigured: boolean;
|
|
571
|
+
/** Custom value for the accordion item. Defaults to provider. */
|
|
572
|
+
value?: string;
|
|
564
573
|
}) => {
|
|
565
574
|
return (
|
|
566
|
-
<AccordionItem value={provider}>
|
|
575
|
+
<AccordionItem value={value ?? provider}>
|
|
567
576
|
<AccordionTrigger className={triggerClassName}>
|
|
568
577
|
<AiProviderTitle provider={provider}>
|
|
569
578
|
{title}
|
|
@@ -581,9 +590,228 @@ const AccordionFormItem = ({
|
|
|
581
590
|
);
|
|
582
591
|
};
|
|
583
592
|
|
|
593
|
+
export const CustomProvidersConfig: React.FC<AiConfigProps> = ({
|
|
594
|
+
form,
|
|
595
|
+
config,
|
|
596
|
+
onSubmit,
|
|
597
|
+
}) => {
|
|
598
|
+
const [isAddingProvider, setIsAddingProvider] = useState(false);
|
|
599
|
+
const [newProviderName, setNewProviderName] = useState("");
|
|
600
|
+
const [newProviderApiKey, setNewProviderApiKey] = useState("");
|
|
601
|
+
const [newProviderBaseUrl, setNewProviderBaseUrl] = useState("");
|
|
602
|
+
|
|
603
|
+
const providerNameInputId = useId();
|
|
604
|
+
const apiKeyInputId = useId();
|
|
605
|
+
const baseUrlInputId = useId();
|
|
606
|
+
|
|
607
|
+
const normalizedName = newProviderName.toLowerCase().replaceAll(/\s+/g, "_");
|
|
608
|
+
const customProviders = form.watch("ai.custom_providers");
|
|
609
|
+
const isDuplicate =
|
|
610
|
+
KNOWN_PROVIDERS.includes(normalizedName as KnownProviderId) ||
|
|
611
|
+
(customProviders && Object.keys(customProviders).includes(normalizedName));
|
|
612
|
+
|
|
613
|
+
const hasValidValues =
|
|
614
|
+
normalizedName.trim() && newProviderBaseUrl.trim() && !isDuplicate;
|
|
615
|
+
|
|
616
|
+
const resetForm = () => {
|
|
617
|
+
setNewProviderName("");
|
|
618
|
+
setNewProviderApiKey("");
|
|
619
|
+
setNewProviderBaseUrl("");
|
|
620
|
+
setIsAddingProvider(false);
|
|
621
|
+
};
|
|
622
|
+
|
|
623
|
+
return (
|
|
624
|
+
<FormField
|
|
625
|
+
control={form.control}
|
|
626
|
+
name="ai.custom_providers"
|
|
627
|
+
render={({ field }) => {
|
|
628
|
+
const customProviders = (field.value || {}) as Record<
|
|
629
|
+
string,
|
|
630
|
+
CustomProviderConfig
|
|
631
|
+
>;
|
|
632
|
+
const customProviderEntries = Object.entries(customProviders);
|
|
633
|
+
|
|
634
|
+
const addProvider = () => {
|
|
635
|
+
if (!hasValidValues) {
|
|
636
|
+
return;
|
|
637
|
+
}
|
|
638
|
+
field.onChange({
|
|
639
|
+
...customProviders,
|
|
640
|
+
[normalizedName]: {
|
|
641
|
+
api_key: newProviderApiKey || undefined,
|
|
642
|
+
base_url: newProviderBaseUrl,
|
|
643
|
+
},
|
|
644
|
+
});
|
|
645
|
+
onSubmit(form.getValues());
|
|
646
|
+
resetForm();
|
|
647
|
+
};
|
|
648
|
+
|
|
649
|
+
const removeProvider = (providerName: string) => {
|
|
650
|
+
const { [providerName]: _, ...rest } = customProviders;
|
|
651
|
+
// Reset to clear nested dirty state, then set new value
|
|
652
|
+
form.resetField("ai.custom_providers");
|
|
653
|
+
form.setValue("ai.custom_providers", rest, { shouldDirty: true });
|
|
654
|
+
onSubmit(form.getValues());
|
|
655
|
+
};
|
|
656
|
+
|
|
657
|
+
const providerForm = (
|
|
658
|
+
<div className="flex flex-col gap-3 p-4 border border-border rounded-md bg-muted/20">
|
|
659
|
+
<div className="flex flex-col gap-1.5">
|
|
660
|
+
<Label htmlFor={providerNameInputId}>Provider Name</Label>
|
|
661
|
+
<Input
|
|
662
|
+
id={providerNameInputId}
|
|
663
|
+
placeholder="e.g., together, groq, mistral"
|
|
664
|
+
value={newProviderName}
|
|
665
|
+
onChange={(e) => setNewProviderName(e.target.value)}
|
|
666
|
+
/>
|
|
667
|
+
{isDuplicate && (
|
|
668
|
+
<p className="text-xs text-destructive">
|
|
669
|
+
A provider with this name already exists.
|
|
670
|
+
</p>
|
|
671
|
+
)}
|
|
672
|
+
{newProviderName && (
|
|
673
|
+
<p className="text-xs text-muted-secondary">
|
|
674
|
+
Use models with prefix:{" "}
|
|
675
|
+
<Kbd className="inline text-xs">{normalizedName}/</Kbd>
|
|
676
|
+
</p>
|
|
677
|
+
)}
|
|
678
|
+
</div>
|
|
679
|
+
|
|
680
|
+
<div className="flex flex-col gap-1.5">
|
|
681
|
+
<Label htmlFor={baseUrlInputId}>
|
|
682
|
+
Base URL <span className="text-destructive">*</span>
|
|
683
|
+
</Label>
|
|
684
|
+
<Input
|
|
685
|
+
id={baseUrlInputId}
|
|
686
|
+
placeholder="e.g., https://api.together.xyz/v1"
|
|
687
|
+
value={newProviderBaseUrl}
|
|
688
|
+
onChange={(e) => setNewProviderBaseUrl(e.target.value)}
|
|
689
|
+
/>
|
|
690
|
+
</div>
|
|
691
|
+
|
|
692
|
+
<div className="flex flex-col gap-1.5">
|
|
693
|
+
<Label htmlFor={apiKeyInputId}>API Key (optional)</Label>
|
|
694
|
+
<Input
|
|
695
|
+
id={apiKeyInputId}
|
|
696
|
+
placeholder="sk-..."
|
|
697
|
+
type="password"
|
|
698
|
+
value={newProviderApiKey}
|
|
699
|
+
onChange={(e) => setNewProviderApiKey(e.target.value)}
|
|
700
|
+
/>
|
|
701
|
+
</div>
|
|
702
|
+
|
|
703
|
+
<div className="flex gap-2 mt-1">
|
|
704
|
+
<Button
|
|
705
|
+
onClick={addProvider}
|
|
706
|
+
disabled={!hasValidValues}
|
|
707
|
+
size="xs"
|
|
708
|
+
>
|
|
709
|
+
Add Provider
|
|
710
|
+
</Button>
|
|
711
|
+
<Button variant="outline" onClick={resetForm} size="xs">
|
|
712
|
+
Cancel
|
|
713
|
+
</Button>
|
|
714
|
+
</div>
|
|
715
|
+
</div>
|
|
716
|
+
);
|
|
717
|
+
|
|
718
|
+
const renderAccordionItem = ({
|
|
719
|
+
providerName,
|
|
720
|
+
providerConfig,
|
|
721
|
+
onRemove,
|
|
722
|
+
}: {
|
|
723
|
+
providerName: string;
|
|
724
|
+
providerConfig: CustomProviderConfig;
|
|
725
|
+
onRemove: (name: string) => void;
|
|
726
|
+
}) => {
|
|
727
|
+
const displayName = Strings.startCase(providerName);
|
|
728
|
+
const isConfigured =
|
|
729
|
+
!!providerConfig.api_key || !!providerConfig.base_url;
|
|
730
|
+
|
|
731
|
+
return (
|
|
732
|
+
<AccordionFormItem
|
|
733
|
+
key={`custom-${providerName}`}
|
|
734
|
+
title={displayName}
|
|
735
|
+
provider={providerName}
|
|
736
|
+
value={`custom-${providerName}`}
|
|
737
|
+
isConfigured={isConfigured}
|
|
738
|
+
>
|
|
739
|
+
<ApiKey
|
|
740
|
+
form={form}
|
|
741
|
+
config={config}
|
|
742
|
+
name={
|
|
743
|
+
`ai.custom_providers.${providerName}.api_key` as FieldPath<UserConfig>
|
|
744
|
+
}
|
|
745
|
+
placeholder="sk-..."
|
|
746
|
+
testId={`custom-provider-${providerName}-api-key`}
|
|
747
|
+
/>
|
|
748
|
+
<BaseUrl
|
|
749
|
+
form={form}
|
|
750
|
+
config={config}
|
|
751
|
+
name={
|
|
752
|
+
`ai.custom_providers.${providerName}.base_url` as FieldPath<UserConfig>
|
|
753
|
+
}
|
|
754
|
+
placeholder="https://api.example.com/v1"
|
|
755
|
+
testId={`custom-provider-${providerName}-base-url`}
|
|
756
|
+
/>
|
|
757
|
+
<Button
|
|
758
|
+
variant="destructive"
|
|
759
|
+
size="xs"
|
|
760
|
+
onClick={(e) => {
|
|
761
|
+
e.stopPropagation();
|
|
762
|
+
e.preventDefault();
|
|
763
|
+
onRemove(providerName);
|
|
764
|
+
}}
|
|
765
|
+
className="w-fit self-end"
|
|
766
|
+
>
|
|
767
|
+
<Trash2Icon className="h-4 w-4 mr-2" />
|
|
768
|
+
Remove Provider
|
|
769
|
+
</Button>
|
|
770
|
+
</AccordionFormItem>
|
|
771
|
+
);
|
|
772
|
+
};
|
|
773
|
+
|
|
774
|
+
return (
|
|
775
|
+
<SettingGroup>
|
|
776
|
+
<SettingSubtitle>Custom Providers</SettingSubtitle>
|
|
777
|
+
<p className="text-sm text-muted-secondary">
|
|
778
|
+
Add your own OpenAI-compatible provider. Once added, you can
|
|
779
|
+
configure models in the AI Models tab.
|
|
780
|
+
</p>
|
|
781
|
+
|
|
782
|
+
{customProviderEntries.length > 0 && (
|
|
783
|
+
<Accordion type="multiple" className="-mt-4">
|
|
784
|
+
{customProviderEntries.map(([name, providerConfig]) =>
|
|
785
|
+
renderAccordionItem({
|
|
786
|
+
providerName: name,
|
|
787
|
+
providerConfig,
|
|
788
|
+
onRemove: removeProvider,
|
|
789
|
+
}),
|
|
790
|
+
)}
|
|
791
|
+
</Accordion>
|
|
792
|
+
)}
|
|
793
|
+
|
|
794
|
+
{isAddingProvider ? (
|
|
795
|
+
providerForm
|
|
796
|
+
) : (
|
|
797
|
+
<AddButton
|
|
798
|
+
className="self-start"
|
|
799
|
+
isFormOpen={isAddingProvider}
|
|
800
|
+
setIsFormOpen={setIsAddingProvider}
|
|
801
|
+
label="Add Provider"
|
|
802
|
+
/>
|
|
803
|
+
)}
|
|
804
|
+
</SettingGroup>
|
|
805
|
+
);
|
|
806
|
+
}}
|
|
807
|
+
/>
|
|
808
|
+
);
|
|
809
|
+
};
|
|
810
|
+
|
|
584
811
|
export const AiProvidersConfig: React.FC<AiConfigProps> = ({
|
|
585
812
|
form,
|
|
586
813
|
config,
|
|
814
|
+
onSubmit,
|
|
587
815
|
}) => {
|
|
588
816
|
const isWasmRuntime = isWasm();
|
|
589
817
|
|
|
@@ -903,13 +1131,17 @@ export const AiProvidersConfig: React.FC<AiConfigProps> = ({
|
|
|
903
1131
|
</AccordionFormItem>
|
|
904
1132
|
|
|
905
1133
|
<AccordionFormItem
|
|
906
|
-
title="OpenAI-Compatible"
|
|
1134
|
+
title="OpenAI-Compatible (Legacy)"
|
|
907
1135
|
provider="openai-compatible"
|
|
908
1136
|
isConfigured={
|
|
909
1137
|
hasValue("ai.open_ai_compatible.api_key") &&
|
|
910
1138
|
hasValue("ai.open_ai_compatible.base_url")
|
|
911
1139
|
}
|
|
912
1140
|
>
|
|
1141
|
+
<p className="text-sm text-amber-600 dark:text-amber-400 mb-2">
|
|
1142
|
+
Consider using Custom Providers instead, which allows you to add
|
|
1143
|
+
multiple providers with distinct names.
|
|
1144
|
+
</p>
|
|
913
1145
|
<ApiKey
|
|
914
1146
|
form={form}
|
|
915
1147
|
config={config}
|
|
@@ -933,6 +1165,8 @@ export const AiProvidersConfig: React.FC<AiConfigProps> = ({
|
|
|
933
1165
|
/>
|
|
934
1166
|
</AccordionFormItem>
|
|
935
1167
|
</Accordion>
|
|
1168
|
+
|
|
1169
|
+
<CustomProvidersConfig form={form} config={config} onSubmit={onSubmit} />
|
|
936
1170
|
</SettingGroup>
|
|
937
1171
|
);
|
|
938
1172
|
};
|
|
@@ -1133,6 +1367,16 @@ export const AiModelDisplayConfig: React.FC<AiConfigProps> = ({
|
|
|
1133
1367
|
name: "ai.models.custom_models",
|
|
1134
1368
|
}) as QualifiedModelId[];
|
|
1135
1369
|
|
|
1370
|
+
const customProviders = useWatch({
|
|
1371
|
+
control: form.control,
|
|
1372
|
+
name: "ai.custom_providers",
|
|
1373
|
+
}) as Record<string, CustomProviderConfig> | undefined;
|
|
1374
|
+
|
|
1375
|
+
const customProviderNames = useMemo(
|
|
1376
|
+
() => Object.keys(customProviders || {}),
|
|
1377
|
+
[customProviders],
|
|
1378
|
+
);
|
|
1379
|
+
|
|
1136
1380
|
const aiModelRegistry = useMemo(
|
|
1137
1381
|
() =>
|
|
1138
1382
|
AiModelRegistry.create({
|
|
@@ -1155,7 +1399,9 @@ export const AiModelDisplayConfig: React.FC<AiConfigProps> = ({
|
|
|
1155
1399
|
? currentDisplayedModels.filter((id) => id !== modelId)
|
|
1156
1400
|
: [...currentDisplayedModels, modelId];
|
|
1157
1401
|
|
|
1158
|
-
form.setValue("ai.models.displayed_models", newModels
|
|
1402
|
+
form.setValue("ai.models.displayed_models", newModels, {
|
|
1403
|
+
shouldDirty: true,
|
|
1404
|
+
});
|
|
1159
1405
|
onSubmit(form.getValues());
|
|
1160
1406
|
});
|
|
1161
1407
|
|
|
@@ -1172,14 +1418,18 @@ export const AiModelDisplayConfig: React.FC<AiConfigProps> = ({
|
|
|
1172
1418
|
? [...new Set([...currentDisplayedModels, ...qualifiedModelIds])]
|
|
1173
1419
|
: currentDisplayedModels.filter((id) => !qualifiedModelIds.has(id));
|
|
1174
1420
|
|
|
1175
|
-
form.setValue("ai.models.displayed_models", newModels
|
|
1421
|
+
form.setValue("ai.models.displayed_models", newModels, {
|
|
1422
|
+
shouldDirty: true,
|
|
1423
|
+
});
|
|
1176
1424
|
onSubmit(form.getValues());
|
|
1177
1425
|
},
|
|
1178
1426
|
);
|
|
1179
1427
|
|
|
1180
1428
|
const deleteModel = useEvent((modelId: QualifiedModelId) => {
|
|
1181
1429
|
const newModels = customModels.filter((id) => id !== modelId);
|
|
1182
|
-
form.setValue("ai.models.custom_models", newModels
|
|
1430
|
+
form.setValue("ai.models.custom_models", newModels, {
|
|
1431
|
+
shouldDirty: true,
|
|
1432
|
+
});
|
|
1183
1433
|
onSubmit(form.getValues());
|
|
1184
1434
|
});
|
|
1185
1435
|
|
|
@@ -1212,6 +1462,7 @@ export const AiModelDisplayConfig: React.FC<AiConfigProps> = ({
|
|
|
1212
1462
|
<AddModelForm
|
|
1213
1463
|
form={form}
|
|
1214
1464
|
customModels={customModels}
|
|
1465
|
+
customProviderNames={customProviderNames}
|
|
1215
1466
|
onSubmit={onSubmit}
|
|
1216
1467
|
/>
|
|
1217
1468
|
</SettingGroup>
|
|
@@ -1221,8 +1472,9 @@ export const AiModelDisplayConfig: React.FC<AiConfigProps> = ({
|
|
|
1221
1472
|
export const AddModelForm: React.FC<{
|
|
1222
1473
|
form: UseFormReturn<UserConfig>;
|
|
1223
1474
|
customModels: QualifiedModelId[];
|
|
1475
|
+
customProviderNames: string[];
|
|
1224
1476
|
onSubmit: (values: UserConfig) => void;
|
|
1225
|
-
}> = ({ form, customModels, onSubmit }) => {
|
|
1477
|
+
}> = ({ form, customModels, customProviderNames, onSubmit }) => {
|
|
1226
1478
|
const [isFormOpen, setIsFormOpen] = useState(false);
|
|
1227
1479
|
const [modelAdded, setModelAdded] = useState(false);
|
|
1228
1480
|
const [provider, setProvider] = useState<ProviderId | "custom" | null>(null);
|
|
@@ -1254,7 +1506,9 @@ export const AddModelForm: React.FC<{
|
|
|
1254
1506
|
modelName as ShortModelId,
|
|
1255
1507
|
);
|
|
1256
1508
|
|
|
1257
|
-
form.setValue("ai.models.custom_models", [newModel.id, ...customModels]
|
|
1509
|
+
form.setValue("ai.models.custom_models", [newModel.id, ...customModels], {
|
|
1510
|
+
shouldDirty: true,
|
|
1511
|
+
});
|
|
1258
1512
|
onSubmit(form.getValues());
|
|
1259
1513
|
resetForm();
|
|
1260
1514
|
|
|
@@ -1293,23 +1547,46 @@ export const AddModelForm: React.FC<{
|
|
|
1293
1547
|
</SelectTrigger>
|
|
1294
1548
|
<SelectContent>
|
|
1295
1549
|
<SelectGroup>
|
|
1550
|
+
{customProviderNames.length > 0 && (
|
|
1551
|
+
<>
|
|
1552
|
+
<p className="px-2 py-1 text-xs text-muted-secondary font-medium">
|
|
1553
|
+
Custom Providers
|
|
1554
|
+
</p>
|
|
1555
|
+
{customProviderNames.map((p) => (
|
|
1556
|
+
<SelectItem key={p} value={p}>
|
|
1557
|
+
<div className="flex items-center gap-2">
|
|
1558
|
+
<AiProviderIcon provider={p} className="h-4 w-4" />
|
|
1559
|
+
<span>{Strings.startCase(p)}</span>
|
|
1560
|
+
</div>
|
|
1561
|
+
</SelectItem>
|
|
1562
|
+
))}
|
|
1563
|
+
<p className="px-2 py-1 text-xs text-muted-secondary font-medium mt-1">
|
|
1564
|
+
Built-in Providers
|
|
1565
|
+
</p>
|
|
1566
|
+
</>
|
|
1567
|
+
)}
|
|
1568
|
+
{KNOWN_PROVIDERS.filter(
|
|
1569
|
+
(p) => p !== "marimo" && !customProviderNames.includes(p),
|
|
1570
|
+
).map((p) => (
|
|
1571
|
+
<SelectItem key={p} value={p}>
|
|
1572
|
+
<div className="flex items-center gap-2">
|
|
1573
|
+
<AiProviderIcon provider={p} className="h-4 w-4" />
|
|
1574
|
+
<span>{getProviderLabel(p)}</span>
|
|
1575
|
+
</div>
|
|
1576
|
+
</SelectItem>
|
|
1577
|
+
))}
|
|
1578
|
+
<p className="px-2 py-1 text-xs text-muted-secondary font-medium mt-1">
|
|
1579
|
+
Other
|
|
1580
|
+
</p>
|
|
1296
1581
|
<SelectItem value="custom">
|
|
1297
1582
|
<div className="flex items-center gap-2">
|
|
1298
1583
|
<AiProviderIcon
|
|
1299
1584
|
provider="openai-compatible"
|
|
1300
1585
|
className="h-4 w-4"
|
|
1301
1586
|
/>
|
|
1302
|
-
<span>
|
|
1587
|
+
<span>Enter provider name</span>
|
|
1303
1588
|
</div>
|
|
1304
1589
|
</SelectItem>
|
|
1305
|
-
{PROVIDERS.filter((p) => p !== "marimo").map((p) => (
|
|
1306
|
-
<SelectItem key={p} value={p}>
|
|
1307
|
-
<div className="flex items-center gap-2">
|
|
1308
|
-
<AiProviderIcon provider={p} className="h-4 w-4" />
|
|
1309
|
-
<span>{getProviderLabel(p)}</span>
|
|
1310
|
-
</div>
|
|
1311
|
-
</SelectItem>
|
|
1312
|
-
))}
|
|
1313
1590
|
</SelectGroup>
|
|
1314
1591
|
</SelectContent>
|
|
1315
1592
|
</Select>
|
|
@@ -1379,17 +1656,12 @@ export const AddModelForm: React.FC<{
|
|
|
1379
1656
|
<div>
|
|
1380
1657
|
{isFormOpen && inputForm}
|
|
1381
1658
|
<div className="flex flex-row text-sm">
|
|
1382
|
-
<
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
disabled={isFormOpen}
|
|
1389
|
-
>
|
|
1390
|
-
<PlusIcon className="h-4 w-4 mr-2 mb-0.5" />
|
|
1391
|
-
Add Model
|
|
1392
|
-
</Button>
|
|
1659
|
+
<AddButton
|
|
1660
|
+
isFormOpen={isFormOpen}
|
|
1661
|
+
setIsFormOpen={setIsFormOpen}
|
|
1662
|
+
label="Add Model"
|
|
1663
|
+
className="pl-2"
|
|
1664
|
+
/>
|
|
1393
1665
|
{modelAdded && (
|
|
1394
1666
|
<div className="flex items-center gap-1 text-green-700 bg-green-500/10 px-2 py-1 rounded-md ml-auto">
|
|
1395
1667
|
✓ Model added
|
|
@@ -1400,6 +1672,34 @@ export const AddModelForm: React.FC<{
|
|
|
1400
1672
|
);
|
|
1401
1673
|
};
|
|
1402
1674
|
|
|
1675
|
+
const AddButton = ({
|
|
1676
|
+
isFormOpen,
|
|
1677
|
+
setIsFormOpen,
|
|
1678
|
+
label,
|
|
1679
|
+
className,
|
|
1680
|
+
}: {
|
|
1681
|
+
isFormOpen: boolean;
|
|
1682
|
+
setIsFormOpen: (isOpen: boolean) => void;
|
|
1683
|
+
label: string;
|
|
1684
|
+
className?: string;
|
|
1685
|
+
}) => {
|
|
1686
|
+
return (
|
|
1687
|
+
<Button
|
|
1688
|
+
onClick={(e) => {
|
|
1689
|
+
e.stopPropagation();
|
|
1690
|
+
e.preventDefault();
|
|
1691
|
+
setIsFormOpen(true);
|
|
1692
|
+
}}
|
|
1693
|
+
variant="link"
|
|
1694
|
+
disabled={isFormOpen}
|
|
1695
|
+
className={cn("px-0", className)}
|
|
1696
|
+
>
|
|
1697
|
+
<PlusIcon className="h-4 w-4 mr-2 mb-0.5" />
|
|
1698
|
+
{label}
|
|
1699
|
+
</Button>
|
|
1700
|
+
);
|
|
1701
|
+
};
|
|
1702
|
+
|
|
1403
1703
|
export const AiConfig: React.FC<AiConfigProps> = ({
|
|
1404
1704
|
form,
|
|
1405
1705
|
config,
|
|
@@ -69,14 +69,21 @@ export function getDirtyValues<T extends FieldValues>(
|
|
|
69
69
|
dirtyFields: Partial<Record<keyof T, unknown>>,
|
|
70
70
|
): Partial<T> {
|
|
71
71
|
const result: Partial<T> = {};
|
|
72
|
-
for (const key of Object.keys(dirtyFields) as
|
|
72
|
+
for (const key of Object.keys(dirtyFields) as (keyof T)[]) {
|
|
73
73
|
const dirty = dirtyFields[key];
|
|
74
|
+
const value = values[key];
|
|
75
|
+
|
|
76
|
+
// Skip if the value no longer exists (e.g., deleted from a record)
|
|
77
|
+
if (value === undefined) {
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
|
|
74
81
|
if (dirty === true) {
|
|
75
|
-
result[key] =
|
|
82
|
+
result[key] = value;
|
|
76
83
|
} else if (typeof dirty === "object" && dirty !== null) {
|
|
77
84
|
// Nested object - recurse
|
|
78
85
|
const nested = getDirtyValues(
|
|
79
|
-
|
|
86
|
+
value as FieldValues,
|
|
80
87
|
dirty as Partial<Record<string, unknown>>,
|
|
81
88
|
);
|
|
82
89
|
if (Object.keys(nested).length > 0) {
|