@marimo-team/islands 0.19.5-dev4 → 0.19.5-dev43
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/{ConnectedDataExplorerComponent-D5KcOOzu.js → ConnectedDataExplorerComponent-DjQ_E5BA.js} +3 -3
- package/dist/assets/__vite-browser-external-DRa9CT_O.js +1 -0
- package/dist/assets/{worker-BR7KVExK.js → worker-SqntmiwV.js} +2 -2
- package/dist/{glide-data-editor-DsVDCmV2.js → glide-data-editor-zEomQJ3U.js} +2 -2
- package/dist/main.js +290 -244
- package/dist/{mermaid-DZjjc-kI.js → mermaid-D7wtYc6C.js} +2 -2
- package/dist/{spec-B1PGDiGh.js → spec-Cif4tBMJ.js} +1 -1
- package/dist/style.css +1 -1
- package/dist/{types-CbQF8CBX.js → types-BQOP2pRy.js} +1 -1
- package/dist/{useAsyncData-TLXJC7yx.js → useAsyncData-kqbhbSuf.js} +1 -1
- package/dist/{useDeepCompareMemoize-DVnEG7jx.js → useDeepCompareMemoize-B2QEm3jo.js} +1 -1
- package/dist/{useTheme-BllQjRdW.js → useTheme-CVr6Gb_R.js} +4 -1
- package/dist/{vega-component-B2QrGnW8.js → vega-component-DAeU1_cV.js} +3 -3
- package/package.json +1 -1
- package/src/__mocks__/requests.ts +1 -0
- package/src/components/ai/__tests__/ai-utils.test.ts +276 -0
- package/src/components/ai/ai-utils.ts +101 -0
- package/src/components/app-config/ai-config.tsx +56 -16
- package/src/components/app-config/user-config-form.tsx +63 -1
- package/src/components/chat/chat-panel.tsx +4 -4
- package/src/components/data-table/cell-utils.ts +10 -0
- package/src/components/editor/Output.tsx +21 -14
- package/src/components/editor/actions/useCellActionButton.tsx +2 -2
- package/src/components/editor/actions/useNotebookActions.tsx +61 -21
- package/src/components/editor/cell/cell-actions.tsx +6 -1
- package/src/components/editor/controls/Controls.tsx +1 -8
- package/src/components/editor/file-tree/file-explorer.tsx +4 -2
- package/src/components/editor/file-tree/file-viewer.tsx +9 -6
- package/src/components/editor/navigation/navigation.ts +39 -1
- package/src/components/editor/renderMimeIcon.tsx +2 -0
- package/src/components/editor/renderers/vertical-layout/vertical-layout.tsx +1 -1
- package/src/core/ai/model-registry.ts +21 -3
- package/src/core/codemirror/language/panel/panel.tsx +3 -0
- package/src/core/codemirror/language/panel/sql.tsx +6 -2
- package/src/core/config/config-schema.ts +5 -1
- package/src/core/config/config.ts +4 -0
- package/src/core/config/feature-flag.tsx +2 -0
- package/src/core/export/__tests__/hooks.test.ts +120 -1
- package/src/core/export/hooks.ts +48 -18
- package/src/core/islands/bridge.ts +1 -0
- package/src/core/lsp/__tests__/transport.test.ts +149 -0
- package/src/core/lsp/transport.ts +48 -0
- package/src/core/network/requests-lazy.ts +1 -0
- package/src/core/network/requests-network.ts +9 -0
- 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/css/app/Cell.css +0 -2
- package/src/plugins/layout/TexPlugin.tsx +7 -5
- package/src/utils/__tests__/download.test.tsx +492 -0
- package/src/utils/download.ts +161 -6
- package/src/utils/filenames.ts +3 -0
- package/dist/assets/__vite-browser-external-CgHmDpAZ.js +0 -1
- package/src/components/export/export-output-button.tsx +0 -14
|
@@ -50,6 +50,7 @@ import { Banner } from "@/plugins/impl/common/error-banner";
|
|
|
50
50
|
import { THEMES } from "@/theme/useTheme";
|
|
51
51
|
import { arrayToggle } from "@/utils/arrays";
|
|
52
52
|
import { cn } from "@/utils/cn";
|
|
53
|
+
import { autoPopulateModels } from "../ai/ai-utils";
|
|
53
54
|
import { keyboardShortcutsAtom } from "../editor/controls/keyboard-shortcuts";
|
|
54
55
|
import { Badge } from "../ui/badge";
|
|
55
56
|
import { ExternalLink } from "../ui/links";
|
|
@@ -180,6 +181,28 @@ export const UserConfigForm: React.FC = () => {
|
|
|
180
181
|
defaultValues: config,
|
|
181
182
|
});
|
|
182
183
|
|
|
184
|
+
const setAiModels = (values: UserConfig, dirtyAiConfig: UserConfig["ai"]) => {
|
|
185
|
+
const { chatModel, editModel } = autoPopulateModels(values);
|
|
186
|
+
if (chatModel || editModel) {
|
|
187
|
+
dirtyAiConfig = {
|
|
188
|
+
...dirtyAiConfig,
|
|
189
|
+
models: {
|
|
190
|
+
...dirtyAiConfig?.models,
|
|
191
|
+
...(chatModel && { chat_model: chatModel }),
|
|
192
|
+
...(editModel && { edit_model: editModel }),
|
|
193
|
+
},
|
|
194
|
+
} as typeof dirtyAiConfig;
|
|
195
|
+
if (chatModel) {
|
|
196
|
+
form.setValue("ai.models.chat_model", chatModel);
|
|
197
|
+
}
|
|
198
|
+
if (editModel) {
|
|
199
|
+
form.setValue("ai.models.edit_model", editModel);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return dirtyAiConfig;
|
|
204
|
+
};
|
|
205
|
+
|
|
183
206
|
const onSubmitNotDebounced = async (values: UserConfig) => {
|
|
184
207
|
// Only send values that were actually changed to avoid
|
|
185
208
|
// overwriting backend values the form doesn't manage
|
|
@@ -187,6 +210,12 @@ export const UserConfigForm: React.FC = () => {
|
|
|
187
210
|
if (Object.keys(dirtyValues).length === 0) {
|
|
188
211
|
return; // Nothing changed
|
|
189
212
|
}
|
|
213
|
+
|
|
214
|
+
// Auto-populate AI models when credentials are set, makes it easier to get started
|
|
215
|
+
if (dirtyValues.ai) {
|
|
216
|
+
dirtyValues.ai = setAiModels(values, dirtyValues.ai);
|
|
217
|
+
}
|
|
218
|
+
|
|
190
219
|
await saveUserConfig({ config: dirtyValues }).then(() => {
|
|
191
220
|
// Update local state with form values
|
|
192
221
|
setConfig((prev) => ({ ...prev, ...values }));
|
|
@@ -993,7 +1022,7 @@ export const UserConfigForm: React.FC = () => {
|
|
|
993
1022
|
<br />
|
|
994
1023
|
<br />
|
|
995
1024
|
Running marimo in a{" "}
|
|
996
|
-
<ExternalLink href="https://docs.marimo.io/guides/
|
|
1025
|
+
<ExternalLink href="https://docs.marimo.io/guides/package_management/inlining_dependencies.html">
|
|
997
1026
|
sandboxed environment
|
|
998
1027
|
</ExternalLink>{" "}
|
|
999
1028
|
is only supported by <Kbd className="inline">uv</Kbd>
|
|
@@ -1283,6 +1312,39 @@ export const UserConfigForm: React.FC = () => {
|
|
|
1283
1312
|
</div>
|
|
1284
1313
|
)}
|
|
1285
1314
|
/>
|
|
1315
|
+
<FormField
|
|
1316
|
+
control={form.control}
|
|
1317
|
+
name="experimental.server_side_pdf_export"
|
|
1318
|
+
render={({ field }) => (
|
|
1319
|
+
<div className="flex flex-col gap-y-1">
|
|
1320
|
+
<FormItem className={formItemClasses}>
|
|
1321
|
+
<FormLabel className="font-normal">
|
|
1322
|
+
Better PDF Export
|
|
1323
|
+
</FormLabel>
|
|
1324
|
+
<FormControl>
|
|
1325
|
+
<Checkbox
|
|
1326
|
+
data-testid="server-side-pdf-export-checkbox"
|
|
1327
|
+
checked={field.value === true}
|
|
1328
|
+
onCheckedChange={field.onChange}
|
|
1329
|
+
/>
|
|
1330
|
+
</FormControl>
|
|
1331
|
+
</FormItem>
|
|
1332
|
+
<IsOverridden
|
|
1333
|
+
userConfig={config}
|
|
1334
|
+
name="experimental.server_side_pdf_export"
|
|
1335
|
+
/>
|
|
1336
|
+
<FormDescription>
|
|
1337
|
+
Enable PDF export using{" "}
|
|
1338
|
+
<Kbd className="inline">nbconvert</Kbd> and{" "}
|
|
1339
|
+
<Kbd className="inline">playwright</Kbd>. Refer to{" "}
|
|
1340
|
+
<ExternalLink href="https://docs.marimo.io/guides/exporting/#exporting-to-pdf-slides-or-rst">
|
|
1341
|
+
the docs
|
|
1342
|
+
</ExternalLink>
|
|
1343
|
+
.
|
|
1344
|
+
</FormDescription>
|
|
1345
|
+
</div>
|
|
1346
|
+
)}
|
|
1347
|
+
/>
|
|
1286
1348
|
</SettingGroup>
|
|
1287
1349
|
);
|
|
1288
1350
|
}
|
|
@@ -262,7 +262,7 @@ const ChatInputFooter: React.FC<ChatInputFooterProps> = memo(
|
|
|
262
262
|
|
|
263
263
|
return (
|
|
264
264
|
<TooltipProvider>
|
|
265
|
-
<div className="px-3 py-2 border-t border-border/20 flex flex-row items-center justify-between">
|
|
265
|
+
<div className="px-3 py-2 border-t border-border/20 flex flex-row flex-wrap items-center justify-between gap-1">
|
|
266
266
|
<div className="flex items-center gap-2">
|
|
267
267
|
<FeatureFlagged feature="chat_modes">
|
|
268
268
|
<Select value={currentMode} onValueChange={saveModeChange}>
|
|
@@ -426,14 +426,14 @@ const ChatInput: React.FC<ChatInputProps> = memo(
|
|
|
426
426
|
ChatInput.displayName = "ChatInput";
|
|
427
427
|
|
|
428
428
|
const ChatPanel = () => {
|
|
429
|
-
const
|
|
429
|
+
const aiConfigured = useAtomValue(aiEnabledAtom);
|
|
430
430
|
const { handleClick } = useOpenSettingsToTab();
|
|
431
431
|
|
|
432
|
-
if (!
|
|
432
|
+
if (!aiConfigured) {
|
|
433
433
|
return (
|
|
434
434
|
<PanelEmptyState
|
|
435
435
|
title="Chat with AI"
|
|
436
|
-
description="AI
|
|
436
|
+
description="No AI provider configured or model selected"
|
|
437
437
|
action={
|
|
438
438
|
<Button variant="outline" size="sm" onClick={() => handleClick("ai")}>
|
|
439
439
|
Edit AI settings
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
2
|
|
|
3
|
+
import type { CellId } from "@/core/cells/ids";
|
|
4
|
+
|
|
3
5
|
export const DATA_CELL_ID = "data-cell-id";
|
|
4
6
|
|
|
5
7
|
export function getCellDomProps(cellId: string) {
|
|
@@ -7,3 +9,11 @@ export function getCellDomProps(cellId: string) {
|
|
|
7
9
|
[DATA_CELL_ID]: cellId,
|
|
8
10
|
};
|
|
9
11
|
}
|
|
12
|
+
|
|
13
|
+
export const DATA_FOR_CELL_ID = "data-for-cell-id";
|
|
14
|
+
|
|
15
|
+
export function getCellForDomProps(cellId: CellId) {
|
|
16
|
+
return {
|
|
17
|
+
[DATA_FOR_CELL_ID]: cellId,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
@@ -20,6 +20,7 @@ import { TextOutput } from "./output/TextOutput";
|
|
|
20
20
|
import { VideoOutput } from "./output/VideoOutput";
|
|
21
21
|
|
|
22
22
|
import "./output/Outputs.css";
|
|
23
|
+
import { useAtomValue } from "jotai";
|
|
23
24
|
import {
|
|
24
25
|
ChevronsDownUpIcon,
|
|
25
26
|
ChevronsUpDownIcon,
|
|
@@ -27,6 +28,7 @@ import {
|
|
|
27
28
|
} from "lucide-react";
|
|
28
29
|
import { tooltipHandler } from "@/components/charts/tooltip";
|
|
29
30
|
import { useExpandedOutput } from "@/core/cells/outputs";
|
|
31
|
+
import { viewStateAtom } from "@/core/mode";
|
|
30
32
|
import { useIframeCapabilities } from "@/hooks/useIframeCapabilities";
|
|
31
33
|
import { renderHTML } from "@/plugins/core/RenderHTML";
|
|
32
34
|
import { Banner } from "@/plugins/impl/common/error-banner";
|
|
@@ -46,10 +48,9 @@ import { renderMimeIcon } from "./renderMimeIcon";
|
|
|
46
48
|
|
|
47
49
|
const METADATA_KEY = "__metadata__";
|
|
48
50
|
|
|
49
|
-
type
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
>;
|
|
51
|
+
export type MimeType = OutputMessage["mimetype"];
|
|
52
|
+
|
|
53
|
+
type MimeBundleWithoutMetadata = Record<MimeType, { [key: string]: unknown }>;
|
|
53
54
|
|
|
54
55
|
type MimeBundle = MimeBundleWithoutMetadata & {
|
|
55
56
|
[METADATA_KEY]?: Record<string, { width?: number; height?: number }>;
|
|
@@ -70,7 +71,7 @@ export const OutputRenderer: React.FC<{
|
|
|
70
71
|
onRefactorWithAI?: OnRefactorWithAI;
|
|
71
72
|
wrapText?: boolean;
|
|
72
73
|
metadata?: { width?: number; height?: number };
|
|
73
|
-
renderFallback?: (mimetype:
|
|
74
|
+
renderFallback?: (mimetype: MimeType) => React.ReactNode;
|
|
74
75
|
}> = memo((props) => {
|
|
75
76
|
const {
|
|
76
77
|
message,
|
|
@@ -90,6 +91,8 @@ export const OutputRenderer: React.FC<{
|
|
|
90
91
|
case "application/vnd.marimo+mimebundle":
|
|
91
92
|
case "application/vnd.vegalite.v5+json":
|
|
92
93
|
case "application/vnd.vega.v5+json":
|
|
94
|
+
case "application/vnd.vegalite.v6+json":
|
|
95
|
+
case "application/vnd.vega.v6+json":
|
|
93
96
|
return typeof data === "string" ? JSON.parse(data) : data;
|
|
94
97
|
default:
|
|
95
98
|
return;
|
|
@@ -199,6 +202,8 @@ export const OutputRenderer: React.FC<{
|
|
|
199
202
|
);
|
|
200
203
|
case "application/vnd.vegalite.v5+json":
|
|
201
204
|
case "application/vnd.vega.v5+json":
|
|
205
|
+
case "application/vnd.vegalite.v6+json":
|
|
206
|
+
case "application/vnd.vega.v6+json":
|
|
202
207
|
return (
|
|
203
208
|
<Suspense fallback={<ChartLoadingState />}>
|
|
204
209
|
<LazyVegaEmbed
|
|
@@ -216,9 +221,7 @@ export const OutputRenderer: React.FC<{
|
|
|
216
221
|
return (
|
|
217
222
|
<MimeBundleOutputRenderer
|
|
218
223
|
channel={channel}
|
|
219
|
-
data={
|
|
220
|
-
parsedJsonData as Record<OutputMessage["mimetype"], OutputMessage>
|
|
221
|
-
}
|
|
224
|
+
data={parsedJsonData as Record<MimeType, OutputMessage>}
|
|
222
225
|
/>
|
|
223
226
|
);
|
|
224
227
|
case "application/vnd.jupyter.widget-view+json":
|
|
@@ -258,6 +261,8 @@ const MimeBundleOutputRenderer: React.FC<{
|
|
|
258
261
|
cellId?: CellId;
|
|
259
262
|
}> = memo(({ data, channel, cellId }) => {
|
|
260
263
|
const mimebundle = Array.isArray(data) ? data[0] : data;
|
|
264
|
+
const { mode } = useAtomValue(viewStateAtom);
|
|
265
|
+
const appView = mode === "present" || mode === "read";
|
|
261
266
|
|
|
262
267
|
// Extract metadata if present (e.g., for retina image rendering)
|
|
263
268
|
const metadata = mimebundle[METADATA_KEY];
|
|
@@ -265,10 +270,7 @@ const MimeBundleOutputRenderer: React.FC<{
|
|
|
265
270
|
// Filter out metadata from the mime entries and type narrow
|
|
266
271
|
const mimeEntries = Objects.entries(mimebundle as Record<string, unknown>)
|
|
267
272
|
.filter(([key]) => key !== METADATA_KEY)
|
|
268
|
-
.map(
|
|
269
|
-
([mime, data]) =>
|
|
270
|
-
[mime, data] as [OutputMessage["mimetype"], CellOutput["data"]],
|
|
271
|
-
);
|
|
273
|
+
.map(([mime, data]) => [mime, data] as [MimeType, CellOutput["data"]]);
|
|
272
274
|
|
|
273
275
|
// If there is none, return null
|
|
274
276
|
const first = mimeEntries[0]?.[0];
|
|
@@ -302,7 +304,12 @@ const MimeBundleOutputRenderer: React.FC<{
|
|
|
302
304
|
return (
|
|
303
305
|
<Tabs defaultValue={first} orientation="vertical">
|
|
304
306
|
<div className="flex">
|
|
305
|
-
<TabsList
|
|
307
|
+
<TabsList
|
|
308
|
+
className={cn(
|
|
309
|
+
"self-start max-h-none flex flex-col gap-2 mr-3 shrink-0",
|
|
310
|
+
appView && "mt-4",
|
|
311
|
+
)}
|
|
312
|
+
>
|
|
306
313
|
{mimeEntries.map(([mime]) => (
|
|
307
314
|
<TabsTrigger
|
|
308
315
|
key={mime}
|
|
@@ -315,7 +322,7 @@ const MimeBundleOutputRenderer: React.FC<{
|
|
|
315
322
|
</TabsTrigger>
|
|
316
323
|
))}
|
|
317
324
|
</TabsList>
|
|
318
|
-
<div className="flex-1">
|
|
325
|
+
<div className="flex-1 w-full">
|
|
319
326
|
{mimeEntries.map(([mime, output]) => (
|
|
320
327
|
<TabsContent key={mime} value={mime}>
|
|
321
328
|
<ErrorBoundary>
|
|
@@ -26,7 +26,6 @@ import {
|
|
|
26
26
|
ZapIcon,
|
|
27
27
|
ZapOffIcon,
|
|
28
28
|
} from "lucide-react";
|
|
29
|
-
import { downloadCellOutput } from "@/components/export/export-output-button";
|
|
30
29
|
import { MultiIcon } from "@/components/icons/multi-icon";
|
|
31
30
|
import { useImperativeModal } from "@/components/modal/ImperativeModal";
|
|
32
31
|
import {
|
|
@@ -54,6 +53,7 @@ import { useRequestClient } from "@/core/network/requests";
|
|
|
54
53
|
import type { CellConfig, RuntimeState } from "@/core/network/types";
|
|
55
54
|
import { canLinkToCell, createCellLink } from "@/utils/cell-urls";
|
|
56
55
|
import { copyToClipboard } from "@/utils/copy";
|
|
56
|
+
import { downloadCellOutputAsImage } from "@/utils/download";
|
|
57
57
|
import { MarkdownIcon, PythonIcon } from "../cell/code/icons";
|
|
58
58
|
import { useDeleteCellCallback } from "../cell/useDeleteCell";
|
|
59
59
|
import { useRunCell } from "../cell/useRunCells";
|
|
@@ -341,7 +341,7 @@ export function useCellActionButtons({ cell, closePopover }: Props) {
|
|
|
341
341
|
icon: <ImageIcon size={13} strokeWidth={1.5} />,
|
|
342
342
|
label: "Export output as PNG",
|
|
343
343
|
hidden: !hasOutput,
|
|
344
|
-
handle: () =>
|
|
344
|
+
handle: () => downloadCellOutputAsImage(cellId, "result"),
|
|
345
345
|
},
|
|
346
346
|
{
|
|
347
347
|
icon: <XCircleIcon size={13} strokeWidth={1.5} />,
|
|
@@ -54,7 +54,12 @@ import {
|
|
|
54
54
|
} from "@/core/cells/cells";
|
|
55
55
|
import { disabledCellIds } from "@/core/cells/utils";
|
|
56
56
|
import { useResolvedMarimoConfig } from "@/core/config/config";
|
|
57
|
+
import { getFeatureFlag } from "@/core/config/feature-flag";
|
|
57
58
|
import { Constants } from "@/core/constants";
|
|
59
|
+
import {
|
|
60
|
+
updateCellOutputsWithScreenshots,
|
|
61
|
+
useEnrichCellOutputs,
|
|
62
|
+
} from "@/core/export/hooks";
|
|
58
63
|
import { useLayoutActions, useLayoutState } from "@/core/layout/layout";
|
|
59
64
|
import { useTogglePresenting } from "@/core/layout/useTogglePresenting";
|
|
60
65
|
import { kioskModeAtom, viewStateAtom } from "@/core/mode";
|
|
@@ -64,7 +69,12 @@ import { downloadAsHTML } from "@/core/static/download-html";
|
|
|
64
69
|
import { createShareableLink } from "@/core/wasm/share";
|
|
65
70
|
import { isWasm } from "@/core/wasm/utils";
|
|
66
71
|
import { copyToClipboard } from "@/utils/copy";
|
|
67
|
-
import {
|
|
72
|
+
import {
|
|
73
|
+
downloadAsPDF,
|
|
74
|
+
downloadBlob,
|
|
75
|
+
downloadHTMLAsImage,
|
|
76
|
+
withLoadingToast,
|
|
77
|
+
} from "@/utils/download";
|
|
68
78
|
import { Filenames } from "@/utils/filenames";
|
|
69
79
|
import { Objects } from "@/utils/objects";
|
|
70
80
|
import { newNotebookURL } from "@/utils/urls";
|
|
@@ -110,18 +120,26 @@ export function useNotebookActions() {
|
|
|
110
120
|
const setCommandPaletteOpen = useSetAtom(commandPaletteAtom);
|
|
111
121
|
const setSettingsDialogOpen = useSetAtom(settingDialogAtom);
|
|
112
122
|
const setKeyboardShortcutsOpen = useSetAtom(keyboardShortcutsAtom);
|
|
113
|
-
const { exportAsMarkdown, readCode, saveCellConfig } =
|
|
123
|
+
const { exportAsMarkdown, readCode, saveCellConfig, updateCellOutputs } =
|
|
124
|
+
useRequestClient();
|
|
114
125
|
|
|
115
126
|
const hasDisabledCells = useAtomValue(hasDisabledCellsAtom);
|
|
116
127
|
const canUndoDeletes = useAtomValue(canUndoDeletesAtom);
|
|
117
128
|
const { selectedLayout } = useLayoutState();
|
|
118
129
|
const { setLayoutView } = useLayoutActions();
|
|
119
130
|
const togglePresenting = useTogglePresenting();
|
|
131
|
+
const takeScreenshots = useEnrichCellOutputs();
|
|
120
132
|
|
|
121
133
|
// Fallback: if sharing is undefined, both are enabled by default
|
|
122
134
|
const sharingHtmlEnabled = resolvedConfig.sharing?.html ?? true;
|
|
123
135
|
const sharingWasmEnabled = resolvedConfig.sharing?.wasm ?? true;
|
|
124
136
|
|
|
137
|
+
const isServerSidePdfExportEnabled = getFeatureFlag("server_side_pdf_export");
|
|
138
|
+
// With server side pdf export, it doesn't matter what mode we are in,
|
|
139
|
+
// Default export uses browser print, which is better in present mode
|
|
140
|
+
const pdfDownloadEnabled =
|
|
141
|
+
isServerSidePdfExportEnabled || viewState.mode !== "present";
|
|
142
|
+
|
|
125
143
|
const renderCheckboxElement = (checked: boolean) => (
|
|
126
144
|
<div className="w-8 flex justify-end">
|
|
127
145
|
{checked && <CheckIcon size={14} />}
|
|
@@ -139,11 +157,7 @@ export function useNotebookActions() {
|
|
|
139
157
|
label: "Download as HTML",
|
|
140
158
|
handle: async () => {
|
|
141
159
|
if (!filename) {
|
|
142
|
-
|
|
143
|
-
variant: "danger",
|
|
144
|
-
title: "Error",
|
|
145
|
-
description: "Notebooks must be named to be exported.",
|
|
146
|
-
});
|
|
160
|
+
toastNotebookMustBeNamed();
|
|
147
161
|
return;
|
|
148
162
|
}
|
|
149
163
|
await downloadAsHTML({ filename, includeCode: true });
|
|
@@ -154,11 +168,7 @@ export function useNotebookActions() {
|
|
|
154
168
|
label: "Download as HTML (exclude code)",
|
|
155
169
|
handle: async () => {
|
|
156
170
|
if (!filename) {
|
|
157
|
-
|
|
158
|
-
variant: "danger",
|
|
159
|
-
title: "Error",
|
|
160
|
-
description: "Notebooks must be named to be exported.",
|
|
161
|
-
});
|
|
171
|
+
toastNotebookMustBeNamed();
|
|
162
172
|
return;
|
|
163
173
|
}
|
|
164
174
|
await downloadAsHTML({ filename, includeCode: false });
|
|
@@ -205,21 +215,43 @@ export function useNotebookActions() {
|
|
|
205
215
|
if (!app) {
|
|
206
216
|
return;
|
|
207
217
|
}
|
|
208
|
-
await downloadHTMLAsImage(
|
|
218
|
+
await downloadHTMLAsImage({
|
|
219
|
+
element: app,
|
|
220
|
+
filename: document.title,
|
|
221
|
+
});
|
|
209
222
|
},
|
|
210
223
|
},
|
|
211
224
|
{
|
|
212
225
|
icon: <FileIcon size={14} strokeWidth={1.5} />,
|
|
213
226
|
label: "Download as PDF",
|
|
214
|
-
disabled:
|
|
215
|
-
tooltip:
|
|
216
|
-
|
|
217
|
-
<
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
),
|
|
227
|
+
disabled: !pdfDownloadEnabled,
|
|
228
|
+
tooltip: pdfDownloadEnabled ? undefined : (
|
|
229
|
+
<span>
|
|
230
|
+
Only available in app view. <br />
|
|
231
|
+
Toggle with: {renderShortcut("global.hideCode", false)}
|
|
232
|
+
</span>
|
|
233
|
+
),
|
|
222
234
|
handle: async () => {
|
|
235
|
+
if (isServerSidePdfExportEnabled) {
|
|
236
|
+
if (!filename) {
|
|
237
|
+
toastNotebookMustBeNamed();
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const downloadPDF = async () => {
|
|
242
|
+
await updateCellOutputsWithScreenshots(
|
|
243
|
+
takeScreenshots,
|
|
244
|
+
updateCellOutputs,
|
|
245
|
+
);
|
|
246
|
+
await downloadAsPDF({
|
|
247
|
+
filename: filename,
|
|
248
|
+
webpdf: false,
|
|
249
|
+
});
|
|
250
|
+
};
|
|
251
|
+
await withLoadingToast("Downloading PDF...", downloadPDF);
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
|
|
223
255
|
const beforeprint = new Event("export-beforeprint");
|
|
224
256
|
const afterprint = new Event("export-afterprint");
|
|
225
257
|
function print() {
|
|
@@ -527,3 +559,11 @@ export function useNotebookActions() {
|
|
|
527
559
|
return action;
|
|
528
560
|
});
|
|
529
561
|
}
|
|
562
|
+
|
|
563
|
+
function toastNotebookMustBeNamed() {
|
|
564
|
+
toast({
|
|
565
|
+
title: "Error",
|
|
566
|
+
description: "Notebooks must be named to be exported.",
|
|
567
|
+
variant: "danger",
|
|
568
|
+
});
|
|
569
|
+
}
|
|
@@ -12,6 +12,7 @@ import React, {
|
|
|
12
12
|
useState,
|
|
13
13
|
} from "react";
|
|
14
14
|
import useEvent from "react-use-event-hook";
|
|
15
|
+
import { getCellForDomProps } from "@/components/data-table/cell-utils";
|
|
15
16
|
import {
|
|
16
17
|
renderMinimalShortcut,
|
|
17
18
|
renderShortcut,
|
|
@@ -104,7 +105,11 @@ const CellActionsDropdownInternal = (
|
|
|
104
105
|
{...restoreFocus}
|
|
105
106
|
>
|
|
106
107
|
<Command>
|
|
107
|
-
<CommandInput
|
|
108
|
+
<CommandInput
|
|
109
|
+
placeholder="Search actions..."
|
|
110
|
+
className="h-6 m-1"
|
|
111
|
+
{...getCellForDomProps(props.cellId)}
|
|
112
|
+
/>
|
|
108
113
|
<CommandList>
|
|
109
114
|
<CommandEmpty>No results</CommandEmpty>
|
|
110
115
|
{actions.map((group, i) => (
|
|
@@ -54,9 +54,7 @@ export const Controls = ({
|
|
|
54
54
|
onRun,
|
|
55
55
|
connectionState,
|
|
56
56
|
running,
|
|
57
|
-
appConfig,
|
|
58
57
|
}: ControlsProps): JSX.Element => {
|
|
59
|
-
const appWidth = appConfig.width;
|
|
60
58
|
const undoAvailable = useAtomValue(canUndoDeletesAtom);
|
|
61
59
|
const needsRun = useAtomValue(needsRunAtom);
|
|
62
60
|
const { undoDeleteCell } = useCellActions();
|
|
@@ -103,12 +101,7 @@ export const Controls = ({
|
|
|
103
101
|
</div>
|
|
104
102
|
)}
|
|
105
103
|
|
|
106
|
-
<div
|
|
107
|
-
className={cn(
|
|
108
|
-
bottomRightControls,
|
|
109
|
-
appWidth === "compact" && "xl:flex-row items-end",
|
|
110
|
-
)}
|
|
111
|
-
>
|
|
104
|
+
<div className={cn(bottomRightControls)}>
|
|
112
105
|
<HideInKioskMode>
|
|
113
106
|
<SaveComponent kioskMode={false} />
|
|
114
107
|
</HideInKioskMode>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
2
|
|
|
3
|
-
import { useAtom } from "jotai";
|
|
3
|
+
import { useAtom, useAtomValue } from "jotai";
|
|
4
4
|
import { atomWithStorage } from "jotai/utils";
|
|
5
5
|
import {
|
|
6
6
|
ArrowLeftIcon,
|
|
@@ -47,6 +47,7 @@ import { Tooltip } from "@/components/ui/tooltip";
|
|
|
47
47
|
import { toast } from "@/components/ui/use-toast";
|
|
48
48
|
import { useCellActions } from "@/core/cells/cells";
|
|
49
49
|
import { useLastFocusedCellId } from "@/core/cells/focus";
|
|
50
|
+
import { disableFileDownloadsAtom } from "@/core/config/config";
|
|
50
51
|
import { useRequestClient } from "@/core/network/requests";
|
|
51
52
|
import type { FileInfo } from "@/core/network/types";
|
|
52
53
|
import { isWasm } from "@/core/wasm/utils";
|
|
@@ -396,6 +397,7 @@ const Edit = ({ node }: { node: NodeApi<FileInfo> }) => {
|
|
|
396
397
|
const Node = ({ node, style, dragHandle }: NodeRendererProps<FileInfo>) => {
|
|
397
398
|
const { openFile, sendCreateFileOrFolder, sendFileDetails } =
|
|
398
399
|
useRequestClient();
|
|
400
|
+
const disableFileDownloads = useAtomValue(disableFileDownloadsAtom);
|
|
399
401
|
|
|
400
402
|
const fileType: FileType = node.data.isDirectory
|
|
401
403
|
? "directory"
|
|
@@ -611,7 +613,7 @@ const Node = ({ node, style, dragHandle }: NodeRendererProps<FileInfo>) => {
|
|
|
611
613
|
</>
|
|
612
614
|
)}
|
|
613
615
|
<DropdownMenuSeparator />
|
|
614
|
-
{!node.data.isDirectory && (
|
|
616
|
+
{!node.data.isDirectory && !disableFileDownloads && (
|
|
615
617
|
<>
|
|
616
618
|
<DropdownMenuItem
|
|
617
619
|
onSelect={async () => {
|
|
@@ -15,7 +15,7 @@ import { Suspense, useEffect, useRef, useState } from "react";
|
|
|
15
15
|
import { renderShortcut } from "@/components/shortcuts/renderShortcut";
|
|
16
16
|
import { Alert, AlertDescription } from "@/components/ui/alert";
|
|
17
17
|
import { Tooltip } from "@/components/ui/tooltip";
|
|
18
|
-
import { hotkeysAtom } from "@/core/config/config";
|
|
18
|
+
import { disableFileDownloadsAtom, hotkeysAtom } from "@/core/config/config";
|
|
19
19
|
import { useRequestClient } from "@/core/network/requests";
|
|
20
20
|
import type { FileInfo } from "@/core/network/types";
|
|
21
21
|
import { filenameAtom } from "@/core/saving/file-state";
|
|
@@ -49,6 +49,7 @@ export const FileViewer: React.FC<Props> = ({ file, onOpenNotebook }) => {
|
|
|
49
49
|
const { theme } = useTheme();
|
|
50
50
|
const { sendFileDetails, sendUpdateFile } = useRequestClient();
|
|
51
51
|
const hotkeys = useAtomValue(hotkeysAtom);
|
|
52
|
+
const disableFileDownloads = useAtomValue(disableFileDownloadsAtom);
|
|
52
53
|
const currentNotebookFilename = useAtomValue(filenameAtom);
|
|
53
54
|
// undefined value means not modified yet
|
|
54
55
|
const [internalValue, setInternalValue] = useState<string>("");
|
|
@@ -152,11 +153,13 @@ export const FileViewer: React.FC<Props> = ({ file, onOpenNotebook }) => {
|
|
|
152
153
|
</Button>
|
|
153
154
|
</Tooltip>
|
|
154
155
|
)}
|
|
155
|
-
|
|
156
|
-
<
|
|
157
|
-
<
|
|
158
|
-
|
|
159
|
-
|
|
156
|
+
{!disableFileDownloads && (
|
|
157
|
+
<Tooltip content="Download">
|
|
158
|
+
<Button size="small" onClick={handleDownload}>
|
|
159
|
+
<DownloadIcon />
|
|
160
|
+
</Button>
|
|
161
|
+
</Tooltip>
|
|
162
|
+
)}
|
|
160
163
|
{!isMedia(mimeType) && (
|
|
161
164
|
<>
|
|
162
165
|
<Tooltip content="Copy contents to clipboard">
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
import { useAtomValue, useSetAtom, useStore } from "jotai";
|
|
11
11
|
import { useMemo } from "react";
|
|
12
12
|
import { mergeProps, useFocusWithin, useKeyboard } from "react-aria";
|
|
13
|
+
import { DATA_FOR_CELL_ID } from "@/components/data-table/cell-utils";
|
|
13
14
|
import { aiCompletionCellAtom } from "@/core/ai/state";
|
|
14
15
|
import { cellIdsAtom, notebookAtom, useCellActions } from "@/core/cells/cells";
|
|
15
16
|
import { useCellFocusActions } from "@/core/cells/focus";
|
|
@@ -101,7 +102,23 @@ function useCellFocusProps(
|
|
|
101
102
|
// On focus, set the last focused cell id.
|
|
102
103
|
focusActions.focusCell({ cellId });
|
|
103
104
|
},
|
|
104
|
-
onBlurWithin: () => {
|
|
105
|
+
onBlurWithin: (e) => {
|
|
106
|
+
// Check if blur is happening because of vim search panel interaction
|
|
107
|
+
if (isInVimPanel(e.relatedTarget) || isInVimPanel(e.target)) {
|
|
108
|
+
// Don't hide code if we're just interacting with vim search
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// If the related target is for a cell id (data-for-cell-id), then we don't want to hide the code, otherwise it might
|
|
113
|
+
// close the dropdown.
|
|
114
|
+
if (
|
|
115
|
+
getDataForCellId(e.relatedTarget) === cellId ||
|
|
116
|
+
getDataForCellId(e.relatedTarget?.closest(`[${DATA_FOR_CELL_ID}]`)) ===
|
|
117
|
+
cellId
|
|
118
|
+
) {
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
|
|
105
122
|
// On blur, hide the code if it was temporarily shown.
|
|
106
123
|
temporarilyShownCodeActions.remove(cellId);
|
|
107
124
|
actions.markTouched({ cellId });
|
|
@@ -116,6 +133,27 @@ function useCellFocusProps(
|
|
|
116
133
|
return focusWithinProps;
|
|
117
134
|
}
|
|
118
135
|
|
|
136
|
+
function isInVimPanel(element: Element | null): boolean {
|
|
137
|
+
if (!element) {
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
if (element instanceof HTMLElement) {
|
|
141
|
+
return element.closest(".cm-vim-panel") !== null;
|
|
142
|
+
}
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function getDataForCellId(element: Element | null | undefined): CellId | null {
|
|
147
|
+
if (!element) {
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
const cellId = element.getAttribute(DATA_FOR_CELL_ID);
|
|
151
|
+
if (!cellId) {
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
return cellId as CellId;
|
|
155
|
+
}
|
|
156
|
+
|
|
119
157
|
type KeymapHandlers = Record<string, () => boolean>;
|
|
120
158
|
|
|
121
159
|
/**
|
|
@@ -29,6 +29,8 @@ export function renderMimeIcon(mime: string) {
|
|
|
29
29
|
return "📝";
|
|
30
30
|
case "application/vnd.vegalite.v5+json":
|
|
31
31
|
case "application/vnd.vega.v5+json":
|
|
32
|
+
case "application/vnd.vegalite.v6+json":
|
|
33
|
+
case "application/vnd.vega.v6+json":
|
|
32
34
|
return "📊";
|
|
33
35
|
case "application/vnd.marimo+mimebundle":
|
|
34
36
|
return "📦";
|
|
@@ -21,8 +21,17 @@ export interface AiModel extends AiModelType {
|
|
|
21
21
|
custom: boolean;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
|
|
24
|
+
interface KnownModelMaps {
|
|
25
|
+
/** Map of qualified model ID to model info */
|
|
26
|
+
modelMap: ReadonlyMap<QualifiedModelId, AiModel>;
|
|
27
|
+
/** Map of provider ID to first default model (supports chat or edit) */
|
|
28
|
+
defaultModelByProvider: ReadonlyMap<ProviderId, QualifiedModelId>;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export const getKnownModelMaps = once((): KnownModelMaps => {
|
|
25
32
|
const modelMap = new Map<QualifiedModelId, AiModel>();
|
|
33
|
+
const defaultModelByProvider = new Map<ProviderId, QualifiedModelId>();
|
|
34
|
+
|
|
26
35
|
for (const model of models) {
|
|
27
36
|
const modelId = model.model as ShortModelId;
|
|
28
37
|
const modelInfo: AiModel = {
|
|
@@ -33,12 +42,21 @@ const getKnownModelMap = once((): ReadonlyMap<QualifiedModelId, AiModel> => {
|
|
|
33
42
|
custom: false,
|
|
34
43
|
};
|
|
35
44
|
|
|
45
|
+
const supportsChatOrEdit =
|
|
46
|
+
modelInfo.roles.includes("chat") || modelInfo.roles.includes("edit");
|
|
47
|
+
|
|
36
48
|
for (const provider of modelInfo.providers) {
|
|
37
49
|
const qualifiedModelId: QualifiedModelId = `${provider}/${modelId}`;
|
|
38
50
|
modelMap.set(qualifiedModelId, modelInfo);
|
|
51
|
+
|
|
52
|
+
// Track first model per provider that supports chat or edit
|
|
53
|
+
if (supportsChatOrEdit && !defaultModelByProvider.has(provider)) {
|
|
54
|
+
defaultModelByProvider.set(provider, qualifiedModelId);
|
|
55
|
+
}
|
|
39
56
|
}
|
|
40
57
|
}
|
|
41
|
-
|
|
58
|
+
|
|
59
|
+
return { modelMap, defaultModelByProvider };
|
|
42
60
|
});
|
|
43
61
|
|
|
44
62
|
const getProviderMap = once(
|
|
@@ -125,7 +143,7 @@ export class AiModelRegistry {
|
|
|
125
143
|
}) {
|
|
126
144
|
const { displayedModels, customModels } = opts;
|
|
127
145
|
const hasDisplayedModels = displayedModels.size > 0;
|
|
128
|
-
const knownModelMap =
|
|
146
|
+
const knownModelMap = getKnownModelMaps().modelMap;
|
|
129
147
|
const customModelsMap = new Map<QualifiedModelId, AiModel>();
|
|
130
148
|
|
|
131
149
|
let modelsMap = new Map<QualifiedModelId, AiModel>();
|