@marimo-team/frontend 0.19.5-dev40 → 0.19.5-dev44
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/{JsonOutput-Cm5B4ITW.js → JsonOutput-BJKlFcc0.js} +3 -3
- package/dist/assets/{add-cell-with-ai-DcCKbUTJ.js → add-cell-with-ai-2okCoA2d.js} +1 -1
- package/dist/assets/{agent-panel-UTyb4ZWt.js → agent-panel-DS2YyDty.js} +1 -1
- package/dist/assets/ai-model-dropdown-C-5PlP5A.js +2 -0
- package/dist/assets/{app-config-button-BZRH0Vm5.js → app-config-button-CWGnhJfC.js} +1 -1
- package/dist/assets/{cell-editor-DvMYGVZI.js → cell-editor-1qg6DEg8.js} +1 -1
- package/dist/assets/{chat-panel-OQJ3Uzfy.js → chat-panel-Biq3a7V5.js} +2 -2
- package/dist/assets/{column-preview-D7tpBZ8D.js → column-preview-oXDBchsB.js} +1 -1
- package/dist/assets/{command-palette-CM7uPgIZ.js → command-palette-Dk8PKzC6.js} +1 -1
- package/dist/assets/{dependency-graph-panel-BnTjJemH.js → dependency-graph-panel-BkcSye1r.js} +1 -1
- package/dist/assets/download-C_slsU-7.js +1 -0
- package/dist/assets/{edit-page-BH1Zauk_.js → edit-page-qFmf6UNC.js} +3 -3
- package/dist/assets/{file-explorer-panel-DHdkeWlc.js → file-explorer-panel-BmTuxLH7.js} +1 -1
- package/dist/assets/{home-page-CEjjqCrm.js → home-page-dF6IF-l_.js} +1 -1
- package/dist/assets/hooks-BKBouwbG.js +1 -0
- package/dist/assets/{index-DlwPGlGp.js → index-iQfK7AJ4.js} +3 -3
- package/dist/assets/{layout-CmYdEP97.js → layout-wTVz6YE7.js} +2 -2
- package/dist/assets/{packages-panel-g0Q5mmiP.js → packages-panel-CFeF0dwu.js} +1 -1
- package/dist/assets/{panels-aGfk1Uim.js → panels-CuOtdNMV.js} +1 -1
- package/dist/assets/{run-page-Dl5uK1BA.js → run-page-BYrxd_KZ.js} +1 -1
- package/dist/assets/{scratchpad-panel-DM1IMG_b.js → scratchpad-panel-B5-yknRc.js} +1 -1
- package/dist/assets/{session-panel-Cb33DPYR.js → session-panel-kCLI973D.js} +1 -1
- package/dist/assets/state-DYlTWGKl.js +1 -0
- package/dist/assets/useCellActionButton-DSlxpMvt.js +1 -0
- package/dist/assets/{useDependencyPanelTab-CelOV6ll.js → useDependencyPanelTab-BzMsMr2G.js} +1 -1
- package/dist/assets/useNotebookActions-sYRj2R7H.js +1 -0
- package/dist/assets/{utilities.esm-h8Z3Fnc4.js → utilities.esm-Dx7lHb2R.js} +1 -1
- package/dist/index.html +7 -7
- package/package.json +1 -1
- 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 +29 -0
- package/src/components/chat/chat-panel.tsx +3 -3
- package/src/components/editor/actions/useCellActionButton.tsx +2 -2
- package/src/components/editor/actions/useNotebookActions.tsx +18 -10
- package/src/components/editor/renderers/vertical-layout/vertical-layout.tsx +1 -1
- package/src/core/ai/model-registry.ts +21 -3
- package/src/core/export/hooks.ts +7 -11
- package/src/utils/__tests__/download.test.tsx +398 -2
- package/src/utils/download.ts +107 -6
- package/dist/assets/ai-model-dropdown-DlakzOQz.js +0 -2
- package/dist/assets/download-B1tR5R3Y.js +0 -1
- package/dist/assets/hooks-DXFCXuj4.js +0 -1
- package/dist/assets/state-AqERlXYZ.js +0 -1
- package/dist/assets/useCellActionButton-CYhHKo1M.js +0 -1
- package/dist/assets/useNotebookActions-73PWLiXf.js +0 -1
- package/src/components/export/export-output-button.tsx +0 -14
|
@@ -115,6 +115,7 @@ interface ApiKeyProps {
|
|
|
115
115
|
placeholder: string;
|
|
116
116
|
testId: string;
|
|
117
117
|
description?: React.ReactNode;
|
|
118
|
+
onChange?: (value: string) => void;
|
|
118
119
|
}
|
|
119
120
|
|
|
120
121
|
export const ApiKey: React.FC<ApiKeyProps> = ({
|
|
@@ -124,6 +125,7 @@ export const ApiKey: React.FC<ApiKeyProps> = ({
|
|
|
124
125
|
placeholder,
|
|
125
126
|
testId,
|
|
126
127
|
description,
|
|
128
|
+
onChange,
|
|
127
129
|
}) => {
|
|
128
130
|
return (
|
|
129
131
|
<FormField
|
|
@@ -141,11 +143,12 @@ export const ApiKey: React.FC<ApiKeyProps> = ({
|
|
|
141
143
|
placeholder={placeholder}
|
|
142
144
|
type="password"
|
|
143
145
|
{...field}
|
|
144
|
-
value={
|
|
146
|
+
value={asStringOrEmpty(field.value)}
|
|
145
147
|
onChange={(e) => {
|
|
146
148
|
const value = e.target.value;
|
|
147
149
|
if (!value.includes("*")) {
|
|
148
150
|
field.onChange(value);
|
|
151
|
+
onChange?.(value);
|
|
149
152
|
}
|
|
150
153
|
}}
|
|
151
154
|
/>
|
|
@@ -168,12 +171,12 @@ interface BaseUrlProps {
|
|
|
168
171
|
testId: string;
|
|
169
172
|
description?: React.ReactNode;
|
|
170
173
|
disabled?: boolean;
|
|
171
|
-
|
|
174
|
+
onChange?: (value: string) => void;
|
|
172
175
|
}
|
|
173
176
|
|
|
174
|
-
function
|
|
177
|
+
function asStringOrEmpty<T>(value: T): string {
|
|
175
178
|
if (value == null) {
|
|
176
|
-
return
|
|
179
|
+
return "";
|
|
177
180
|
}
|
|
178
181
|
|
|
179
182
|
if (typeof value === "string") {
|
|
@@ -191,13 +194,12 @@ export const BaseUrl: React.FC<BaseUrlProps> = ({
|
|
|
191
194
|
testId,
|
|
192
195
|
description,
|
|
193
196
|
disabled = false,
|
|
194
|
-
|
|
197
|
+
onChange,
|
|
195
198
|
}) => {
|
|
196
199
|
return (
|
|
197
200
|
<FormField
|
|
198
201
|
control={form.control}
|
|
199
202
|
name={name}
|
|
200
|
-
disabled={disabled}
|
|
201
203
|
render={({ field }) => (
|
|
202
204
|
<div className="flex flex-col space-y-1">
|
|
203
205
|
<FormItem className={formItemClasses}>
|
|
@@ -208,9 +210,13 @@ export const BaseUrl: React.FC<BaseUrlProps> = ({
|
|
|
208
210
|
rootClassName="flex-1"
|
|
209
211
|
className="m-0 inline-flex h-7"
|
|
210
212
|
placeholder={placeholder}
|
|
211
|
-
defaultValue={defaultValue}
|
|
212
213
|
{...field}
|
|
213
|
-
value={
|
|
214
|
+
value={asStringOrEmpty(field.value)}
|
|
215
|
+
disabled={disabled}
|
|
216
|
+
onChange={(e) => {
|
|
217
|
+
field.onChange(e.target.value);
|
|
218
|
+
onChange?.(e.target.value);
|
|
219
|
+
}}
|
|
214
220
|
/>
|
|
215
221
|
</FormControl>
|
|
216
222
|
<FormMessage />
|
|
@@ -252,9 +258,8 @@ export const ModelSelector: React.FC<ModelSelectorProps> = ({
|
|
|
252
258
|
<FormField
|
|
253
259
|
control={form.control}
|
|
254
260
|
name={name}
|
|
255
|
-
disabled={disabled}
|
|
256
261
|
render={({ field }) => {
|
|
257
|
-
const value =
|
|
262
|
+
const value = asStringOrEmpty(field.value);
|
|
258
263
|
|
|
259
264
|
const selectModel = (modelId: QualifiedModelId) => {
|
|
260
265
|
field.onChange(modelId);
|
|
@@ -286,7 +291,8 @@ export const ModelSelector: React.FC<ModelSelectorProps> = ({
|
|
|
286
291
|
className="w-full border-border shadow-none focus-visible:shadow-xs"
|
|
287
292
|
placeholder={placeholder}
|
|
288
293
|
{...field}
|
|
289
|
-
value={
|
|
294
|
+
value={asStringOrEmpty(field.value)}
|
|
295
|
+
disabled={disabled}
|
|
290
296
|
onKeyDown={Events.stopPropagation()}
|
|
291
297
|
/>
|
|
292
298
|
{value && (
|
|
@@ -339,7 +345,6 @@ export const ProviderSelect: React.FC<ProviderSelectProps> = ({
|
|
|
339
345
|
<FormField
|
|
340
346
|
control={form.control}
|
|
341
347
|
name={name}
|
|
342
|
-
disabled={disabled}
|
|
343
348
|
render={({ field }) => (
|
|
344
349
|
<div className="flex flex-col space-y-1">
|
|
345
350
|
<FormItem className={formItemClasses}>
|
|
@@ -354,14 +359,14 @@ export const ProviderSelect: React.FC<ProviderSelectProps> = ({
|
|
|
354
359
|
field.onChange(e.target.value);
|
|
355
360
|
}
|
|
356
361
|
}}
|
|
357
|
-
value={
|
|
362
|
+
value={asStringOrEmpty(
|
|
358
363
|
field.value === true
|
|
359
364
|
? "github"
|
|
360
365
|
: field.value === false
|
|
361
366
|
? "none"
|
|
362
367
|
: field.value,
|
|
363
368
|
)}
|
|
364
|
-
disabled={
|
|
369
|
+
disabled={disabled}
|
|
365
370
|
className="inline-flex mr-2"
|
|
366
371
|
>
|
|
367
372
|
{options.map((option) => (
|
|
@@ -715,6 +720,22 @@ export const CustomProvidersConfig: React.FC<AiConfigProps> = ({
|
|
|
715
720
|
</div>
|
|
716
721
|
);
|
|
717
722
|
|
|
723
|
+
// Update a provider field by updating the entire custom_providers object.
|
|
724
|
+
// As this config will be replaced, it needs to be sent in its entirety.
|
|
725
|
+
const updateProviderField = (opts: {
|
|
726
|
+
providerName: string;
|
|
727
|
+
fieldName: keyof CustomProviderConfig;
|
|
728
|
+
value: string;
|
|
729
|
+
}) => {
|
|
730
|
+
field.onChange({
|
|
731
|
+
...customProviders,
|
|
732
|
+
[opts.providerName]: {
|
|
733
|
+
...customProviders[opts.providerName],
|
|
734
|
+
[opts.fieldName]: opts.value || undefined,
|
|
735
|
+
},
|
|
736
|
+
});
|
|
737
|
+
};
|
|
738
|
+
|
|
718
739
|
const renderAccordionItem = ({
|
|
719
740
|
providerName,
|
|
720
741
|
providerConfig,
|
|
@@ -744,6 +765,13 @@ export const CustomProvidersConfig: React.FC<AiConfigProps> = ({
|
|
|
744
765
|
}
|
|
745
766
|
placeholder="sk-..."
|
|
746
767
|
testId={`custom-provider-${providerName}-api-key`}
|
|
768
|
+
onChange={(value) =>
|
|
769
|
+
updateProviderField({
|
|
770
|
+
providerName,
|
|
771
|
+
fieldName: "api_key",
|
|
772
|
+
value,
|
|
773
|
+
})
|
|
774
|
+
}
|
|
747
775
|
/>
|
|
748
776
|
<BaseUrl
|
|
749
777
|
form={form}
|
|
@@ -753,6 +781,13 @@ export const CustomProvidersConfig: React.FC<AiConfigProps> = ({
|
|
|
753
781
|
}
|
|
754
782
|
placeholder="https://api.example.com/v1"
|
|
755
783
|
testId={`custom-provider-${providerName}-base-url`}
|
|
784
|
+
onChange={(value) =>
|
|
785
|
+
updateProviderField({
|
|
786
|
+
providerName,
|
|
787
|
+
fieldName: "base_url",
|
|
788
|
+
value,
|
|
789
|
+
})
|
|
790
|
+
}
|
|
756
791
|
/>
|
|
757
792
|
<Button
|
|
758
793
|
variant="destructive"
|
|
@@ -918,7 +953,6 @@ export const AiProvidersConfig: React.FC<AiConfigProps> = ({
|
|
|
918
953
|
config={config}
|
|
919
954
|
name="ai.ollama.base_url"
|
|
920
955
|
placeholder="http://localhost:11434/v1"
|
|
921
|
-
defaultValue="http://localhost:11434/v1"
|
|
922
956
|
testId="ollama-base-url-input"
|
|
923
957
|
/>
|
|
924
958
|
</AccordionFormItem>
|
|
@@ -1038,7 +1072,6 @@ export const AiProvidersConfig: React.FC<AiConfigProps> = ({
|
|
|
1038
1072
|
config={config}
|
|
1039
1073
|
name="ai.azure.base_url"
|
|
1040
1074
|
placeholder="https://<your-resource-name>.openai.azure.com/openai/deployments/<deployment-name>?api-version=<api-version>"
|
|
1041
|
-
defaultValue="https://<your-resource-name>.openai.azure.com/openai/deployments/<deployment-name>?api-version=<api-version>"
|
|
1042
1075
|
testId="ai-azure-base-url-input"
|
|
1043
1076
|
/>
|
|
1044
1077
|
</AccordionFormItem>
|
|
@@ -1427,6 +1460,13 @@ export const AiModelDisplayConfig: React.FC<AiConfigProps> = ({
|
|
|
1427
1460
|
|
|
1428
1461
|
const deleteModel = useEvent((modelId: QualifiedModelId) => {
|
|
1429
1462
|
const newModels = customModels.filter((id) => id !== modelId);
|
|
1463
|
+
// Remove from displayed models if it's in there
|
|
1464
|
+
const newDisplayedModels = currentDisplayedModels.filter(
|
|
1465
|
+
(id) => id !== modelId,
|
|
1466
|
+
);
|
|
1467
|
+
form.setValue("ai.models.displayed_models", newDisplayedModels, {
|
|
1468
|
+
shouldDirty: true,
|
|
1469
|
+
});
|
|
1430
1470
|
form.setValue("ai.models.custom_models", newModels, {
|
|
1431
1471
|
shouldDirty: true,
|
|
1432
1472
|
});
|
|
@@ -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 }));
|
|
@@ -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
|
|
@@ -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} />,
|
|
@@ -134,6 +134,12 @@ export function useNotebookActions() {
|
|
|
134
134
|
const sharingHtmlEnabled = resolvedConfig.sharing?.html ?? true;
|
|
135
135
|
const sharingWasmEnabled = resolvedConfig.sharing?.wasm ?? true;
|
|
136
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
|
+
|
|
137
143
|
const renderCheckboxElement = (checked: boolean) => (
|
|
138
144
|
<div className="w-8 flex justify-end">
|
|
139
145
|
{checked && <CheckIcon size={14} />}
|
|
@@ -209,22 +215,24 @@ export function useNotebookActions() {
|
|
|
209
215
|
if (!app) {
|
|
210
216
|
return;
|
|
211
217
|
}
|
|
212
|
-
await downloadHTMLAsImage(
|
|
218
|
+
await downloadHTMLAsImage({
|
|
219
|
+
element: app,
|
|
220
|
+
filename: document.title,
|
|
221
|
+
});
|
|
213
222
|
},
|
|
214
223
|
},
|
|
215
224
|
{
|
|
216
225
|
icon: <FileIcon size={14} strokeWidth={1.5} />,
|
|
217
226
|
label: "Download as PDF",
|
|
218
|
-
disabled:
|
|
219
|
-
tooltip:
|
|
220
|
-
|
|
221
|
-
<
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
),
|
|
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
|
+
),
|
|
226
234
|
handle: async () => {
|
|
227
|
-
if (
|
|
235
|
+
if (isServerSidePdfExportEnabled) {
|
|
228
236
|
if (!filename) {
|
|
229
237
|
toastNotebookMustBeNamed();
|
|
230
238
|
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>();
|
package/src/core/export/hooks.ts
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
-
import { toPng } from "html-to-image";
|
|
3
2
|
import { atom, useAtom, useAtomValue } from "jotai";
|
|
4
3
|
import type { MimeType } from "@/components/editor/Output";
|
|
5
4
|
import { toast } from "@/components/ui/use-toast";
|
|
6
5
|
import { appConfigAtom } from "@/core/config/config";
|
|
7
6
|
import { useInterval } from "@/hooks/useInterval";
|
|
7
|
+
import { getImageDataUrlForCell } from "@/utils/download";
|
|
8
8
|
import { Logger } from "@/utils/Logger";
|
|
9
9
|
import { Objects } from "@/utils/objects";
|
|
10
10
|
import { cellsRuntimeAtom } from "../cells/cells";
|
|
11
|
-
import {
|
|
11
|
+
import type { CellId } from "../cells/ids";
|
|
12
12
|
import { connectionAtom } from "../network/connection";
|
|
13
13
|
import { useRequestClient } from "../network/requests";
|
|
14
14
|
import type { UpdateCellOutputsRequest } from "../network/types";
|
|
@@ -131,16 +131,12 @@ export function useEnrichCellOutputs() {
|
|
|
131
131
|
// Capture screenshots
|
|
132
132
|
const results = await Promise.all(
|
|
133
133
|
cellsToCaptureScreenshot.map(async ([cellId]) => {
|
|
134
|
-
const outputElement = document.getElementById(
|
|
135
|
-
CellOutputId.create(cellId),
|
|
136
|
-
);
|
|
137
|
-
if (!outputElement) {
|
|
138
|
-
Logger.error(`Output element not found for cell ${cellId}`);
|
|
139
|
-
return null;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
134
|
try {
|
|
143
|
-
const dataUrl = await
|
|
135
|
+
const dataUrl = await getImageDataUrlForCell(cellId);
|
|
136
|
+
if (!dataUrl) {
|
|
137
|
+
Logger.error(`Failed to capture screenshot for cell ${cellId}`);
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
144
140
|
return [cellId, ["image/png", dataUrl]] as [
|
|
145
141
|
CellId,
|
|
146
142
|
["image/png", string],
|