@marimo-team/frontend 0.14.18-dev24 → 0.14.18-dev26

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.
Files changed (117) hide show
  1. package/dist/assets/{ConnectedDataExplorerComponent-Dbu2xXc5.js → ConnectedDataExplorerComponent-CPwF1ckF.js} +1 -1
  2. package/dist/assets/{ImageComparisonComponent-BkZIjGHA.js → ImageComparisonComponent-BuFzvNj5.js} +1 -1
  3. package/dist/assets/{VegaLite-Ca7AXGyA.js → VegaLite-DI-xPLgU.js} +1 -1
  4. package/dist/assets/{_baseEach-H5Qk1V2B.js → _baseEach-BvIVAeE-.js} +1 -1
  5. package/dist/assets/_baseMap-DrbRr9s_.js +1 -0
  6. package/dist/assets/{_baseUniq-UAmxGez2.js → _baseUniq-Cz-AR5wG.js} +1 -1
  7. package/dist/assets/{_createAggregator-H-t5qYSG.js → _createAggregator-fFJIgkzg.js} +1 -1
  8. package/dist/assets/{any-language-editor-DUt-pIdy.js → any-language-editor-yovSkXXH.js} +1 -1
  9. package/dist/assets/{architectureDiagram-SUXI7LT5-DWP05q8_.js → architectureDiagram-SUXI7LT5-vhSKxFkz.js} +1 -1
  10. package/dist/assets/{blockDiagram-6J76NXCF-DB57b3LI.js → blockDiagram-6J76NXCF-D8FkmW5U.js} +1 -1
  11. package/dist/assets/{c4Diagram-6F6E4RAY-ByeAGPmY.js → c4Diagram-6F6E4RAY-DMz49WYv.js} +1 -1
  12. package/dist/assets/channel-C5D8_wdC.js +1 -0
  13. package/dist/assets/{chunk-353BL4L5-ngKqumF3.js → chunk-353BL4L5-C_h8QREg.js} +1 -1
  14. package/dist/assets/{chunk-67H74DCK-DTbVgB4A.js → chunk-67H74DCK-vQz9ujXD.js} +1 -1
  15. package/dist/assets/{chunk-AACKK3MU-t9Kf_p_V.js → chunk-AACKK3MU-CUxeSfqv.js} +1 -1
  16. package/dist/assets/{chunk-BFAMUDN2-B0dcgdMs.js → chunk-BFAMUDN2-CLGtBusC.js} +1 -1
  17. package/dist/assets/{chunk-E2GYISFI-C7n5SQvq.js → chunk-E2GYISFI-DGILtsZz.js} +1 -1
  18. package/dist/assets/{chunk-OW32GOEJ-DEvBFIh8.js → chunk-OW32GOEJ-D-HRiiaD.js} +1 -1
  19. package/dist/assets/{chunk-SKB7J2MH-DX71TF_n.js → chunk-SKB7J2MH-xs1kEiQZ.js} +1 -1
  20. package/dist/assets/{chunk-SZ463SBG-CO9fYjVe.js → chunk-SZ463SBG-DKY2KPhj.js} +1 -1
  21. package/dist/assets/{circle-play-B-ZWQLkW.js → circle-play-ub1A5yYA.js} +1 -1
  22. package/dist/assets/classDiagram-M3E45YP4-Bzxtjbca.js +1 -0
  23. package/dist/assets/classDiagram-v2-YAWTLIQI-Bzxtjbca.js +1 -0
  24. package/dist/assets/clone-DngVHtKT.js +1 -0
  25. package/dist/assets/{compile-lv6gzALt.js → compile-Eo2s_HHz.js} +1 -1
  26. package/dist/assets/{dagre-JOIXM2OF-B7ZtMRIJ.js → dagre-JOIXM2OF-BNvsGquX.js} +1 -1
  27. package/dist/assets/{data-grid-overlay-editor-DTAq6v9P.js → data-grid-overlay-editor-Vk_PgvbS.js} +1 -1
  28. package/dist/assets/{diagram-5UYTHUR4-CboeMF3G.js → diagram-5UYTHUR4-6gGyo4aQ.js} +1 -1
  29. package/dist/assets/{diagram-VMROVX33-BcnQcTFp.js → diagram-VMROVX33-Cr9VHqxg.js} +1 -1
  30. package/dist/assets/{diagram-ZTM2IBQH-CaFT3B8g.js → diagram-ZTM2IBQH-BFP88P3p.js} +1 -1
  31. package/dist/assets/{edit-page-CvWZ8wVZ.js → edit-page-Vrbmp55N.js} +53 -53
  32. package/dist/assets/{erDiagram-3M52JZNH-BFPc1-ng.js → erDiagram-3M52JZNH-C0U8FCGV.js} +1 -1
  33. package/dist/assets/{flowDiagram-KYDEHFYC-CAZBj4fC.js → flowDiagram-KYDEHFYC-3GT8PdlC.js} +1 -1
  34. package/dist/assets/{ganttDiagram-EK5VF46D-CUxffTMj.js → ganttDiagram-EK5VF46D-CrQQiHRJ.js} +1 -1
  35. package/dist/assets/{gitGraphDiagram-GW3U2K7C-Tx-ZfecV.js → gitGraphDiagram-GW3U2K7C-BuMcEd-g.js} +1 -1
  36. package/dist/assets/{glide-data-editor-BP8l5q3f.js → glide-data-editor-BB_hULql.js} +11 -11
  37. package/dist/assets/{graph-Bf6eoL3M.js → graph-B67OAr_1.js} +1 -1
  38. package/dist/assets/{home-page--ixzxF-w.js → home-page-D4saUb6P.js} +1 -1
  39. package/dist/assets/index-B3Q6PaCG.css +1 -0
  40. package/dist/assets/{index-g-JZ5Z-_.js → index-BCsh52ZA.js} +1 -1
  41. package/dist/assets/{index-DJ1FNShi.js → index-BUmTew_7.js} +1 -1
  42. package/dist/assets/{index-CmJm6kj5.js → index-BbaurO1n.js} +1 -1
  43. package/dist/assets/{index-D1vinr8C.js → index-BcNCs1Ec.js} +1 -1
  44. package/dist/assets/{index-Cv01rAR1.js → index-BcOm_dWX.js} +1 -1
  45. package/dist/assets/{index-dvYU8Vev.js → index-Bg-oxR2t.js} +94 -94
  46. package/dist/assets/{index-DleA5-JR.js → index-CT6WEyVV.js} +1 -1
  47. package/dist/assets/{index-kaWF-HKt.js → index-CWnyv0Mb.js} +1 -1
  48. package/dist/assets/{index-BMLuYuQT.js → index-CYZYUN8a.js} +1 -1
  49. package/dist/assets/{index-DupjaVjo.js → index-CfQw831p.js} +1 -1
  50. package/dist/assets/{index-C4iPAevr.js → index-CydDDozP.js} +1 -1
  51. package/dist/assets/{index-q4AwmgtF.js → index-D-aN-b2W.js} +1 -1
  52. package/dist/assets/{index-Uh_QGNQm.js → index-DFBulMSA.js} +1 -1
  53. package/dist/assets/{index-U0KSor5u.js → index-DeKrdrEq.js} +1 -1
  54. package/dist/assets/{index-Q73xW9dd.js → index-DqZ9_rHU.js} +1 -1
  55. package/dist/assets/{index-mvfeSH0B.js → index-Dtw1TXyN.js} +1 -1
  56. package/dist/assets/{index-B39Q37Jh.js → index-FrFiygIp.js} +1 -1
  57. package/dist/assets/{index-DhU7edG1.js → index-IDJxuXSR.js} +1 -1
  58. package/dist/assets/{index-CM8rF_ge.js → index-LcqVorg9.js} +1 -1
  59. package/dist/assets/infoDiagram-LHK5PUON-Bc9fde05.js +2 -0
  60. package/dist/assets/{journeyDiagram-EWQZEKCU-DBorgqR8.js → journeyDiagram-EWQZEKCU-CQkZyIS5.js} +1 -1
  61. package/dist/assets/{kanban-definition-ZSS6B67P-rOei7rdW.js → kanban-definition-ZSS6B67P-CEV1b6rk.js} +1 -1
  62. package/dist/assets/{layout-D5U1vfFv.js → layout-DAmvOShj.js} +1 -1
  63. package/dist/assets/{linear-BPaO6rYC.js → linear-ktW_rVCS.js} +1 -1
  64. package/dist/assets/links-C8gYI3jG.js +18 -0
  65. package/dist/assets/{mermaid-DuiiiGkf.js → mermaid-Dn9IUXi8.js} +4 -4
  66. package/dist/assets/{min-GM8d8p3k.js → min-BmsA5VGH.js} +1 -1
  67. package/dist/assets/{mindmap-definition-6CBA2TL7-DFNMYbU5.js → mindmap-definition-6CBA2TL7-B0yAK13C.js} +1 -1
  68. package/dist/assets/{number-overlay-editor-CcD_5O9P.js → number-overlay-editor-DbvoJDEj.js} +1 -1
  69. package/dist/assets/{pieDiagram-NIOCPIFQ-BAnWQSTZ.js → pieDiagram-NIOCPIFQ-BaPxK1u0.js} +1 -1
  70. package/dist/assets/{quadrantDiagram-2OG54O6I-CLzghZ4d.js → quadrantDiagram-2OG54O6I-PGZoquB4.js} +1 -1
  71. package/dist/assets/{react-plotly-qIomJONw.js → react-plotly-CzEXKtwr.js} +1 -1
  72. package/dist/assets/{requirementDiagram-QOLK2EJ7-ClKsOuLQ.js → requirementDiagram-QOLK2EJ7-BdGXi15P.js} +1 -1
  73. package/dist/assets/{run-page-B9ntqQci.js → run-page-BnZ5haku.js} +1 -1
  74. package/dist/assets/{sankeyDiagram-4UZDY2LN-CMUyusMd.js → sankeyDiagram-4UZDY2LN-Dk80vg6t.js} +1 -1
  75. package/dist/assets/{sequenceDiagram-SKLFT4DO-BUL7OokF.js → sequenceDiagram-SKLFT4DO-CqLVyCT3.js} +1 -1
  76. package/dist/assets/{slides-component-DxNxYl9E.js → slides-component-D_39bH-j.js} +1 -1
  77. package/dist/assets/{sortBy-Bo672N53.js → sortBy-CI9MGbno.js} +1 -1
  78. package/dist/assets/{stateDiagram-MI5ZYTHO-CK5D03xc.js → stateDiagram-MI5ZYTHO-C3tpbdm0.js} +1 -1
  79. package/dist/assets/stateDiagram-v2-5AN5P6BG-BXclnbG1.js +1 -0
  80. package/dist/assets/{storage-DCGJ86_2.js → storage-CxOI7c0Z.js} +3 -3
  81. package/dist/assets/{terminal-C9ZYVCQk.js → terminal-9Nyd6Apx.js} +1 -1
  82. package/dist/assets/{time-DUBsogDP.js → time-Do4-nT0Q.js} +1 -1
  83. package/dist/assets/{timeline-definition-MYPXXCX6-DlOzuKHL.js → timeline-definition-MYPXXCX6-CtllVQGu.js} +1 -1
  84. package/dist/assets/{tracing-DkB9iogQ.js → tracing-BWy-UTCz.js} +2 -2
  85. package/dist/assets/{trash-CuNNSzF1.js → trash-BPgHERlA.js} +1 -1
  86. package/dist/assets/{treemap-75Q7IDZK-DUsN_Z4F.js → treemap-75Q7IDZK-BXkwu4nl.js} +1 -1
  87. package/dist/assets/{vega-component-DByprFwW.js → vega-component-COxYVQBE.js} +1 -1
  88. package/dist/assets/{xychartDiagram-H2YORKM3-BaUqYAb6.js → xychartDiagram-H2YORKM3-DfzCBqlv.js} +1 -1
  89. package/dist/index.html +2 -2
  90. package/package.json +1 -1
  91. package/src/components/ai/ai-model-dropdown.tsx +288 -0
  92. package/src/components/ai/ai-provider-icon.tsx +7 -4
  93. package/src/components/app-config/ai-config.tsx +100 -76
  94. package/src/components/app-config/app-config-button.tsx +10 -1
  95. package/src/components/app-config/constants.ts +0 -34
  96. package/src/components/app-config/incorrect-model-id.tsx +4 -2
  97. package/src/components/app-config/user-config-form.tsx +12 -5
  98. package/src/components/chat/chat-panel.tsx +12 -26
  99. package/src/components/slides/slides.css +0 -1
  100. package/src/core/ai/__tests__/model-registry.test.ts +357 -0
  101. package/src/{utils/ai → core/ai/ids}/__tests__/ids.test.ts +2 -1
  102. package/src/{utils/ai → core/ai/ids}/ids.ts +18 -10
  103. package/src/core/ai/model-registry.ts +164 -0
  104. package/src/core/cells/cells.ts +1 -1
  105. package/src/core/cells/effects.ts +1 -1
  106. package/src/plugins/layout/carousel/CarouselPlugin.tsx +0 -2
  107. package/src/utils/__tests__/multi-map.test.ts +295 -0
  108. package/src/utils/multi-map.ts +71 -0
  109. package/dist/assets/_baseMap-lEtQfieX.js +0 -1
  110. package/dist/assets/channel-CJdgPvjM.js +0 -1
  111. package/dist/assets/classDiagram-M3E45YP4-Tb8oQ03C.js +0 -1
  112. package/dist/assets/classDiagram-v2-YAWTLIQI-Tb8oQ03C.js +0 -1
  113. package/dist/assets/clone-DygFoMzB.js +0 -1
  114. package/dist/assets/index-BlxPam9h.css +0 -1
  115. package/dist/assets/infoDiagram-LHK5PUON-Cz497oaY.js +0 -2
  116. package/dist/assets/links-Cxxlu7np.js +0 -17
  117. package/dist/assets/stateDiagram-v2-5AN5P6BG-Deqw4NDh.js +0 -1
@@ -0,0 +1,288 @@
1
+ /* Copyright 2024 Marimo. All rights reserved. */
2
+
3
+ import type { Role } from "@marimo-team/llm-info";
4
+ import { capitalize } from "lodash-es";
5
+ import { ChevronDownIcon, CircleHelpIcon } from "lucide-react";
6
+ import {
7
+ AiModelId,
8
+ isKnownAIProvider,
9
+ type ProviderId,
10
+ type QualifiedModelId,
11
+ } from "@/core/ai/ids/ids";
12
+ import { type AiModel, AiModelRegistry } from "@/core/ai/model-registry";
13
+ import { useResolvedMarimoConfig } from "@/core/config/config";
14
+ import {
15
+ DropdownMenu,
16
+ DropdownMenuContent,
17
+ DropdownMenuItem,
18
+ DropdownMenuPortal,
19
+ DropdownMenuSeparator,
20
+ DropdownMenuSub,
21
+ DropdownMenuSubContent,
22
+ DropdownMenuSubTrigger,
23
+ DropdownMenuTrigger,
24
+ } from "../ui/dropdown-menu";
25
+ import { AiProviderIcon } from "./ai-provider-icon";
26
+
27
+ interface AIModelDropdownProps {
28
+ value?: string;
29
+ placeholder?: string;
30
+ onSelect: (modelId: QualifiedModelId) => void;
31
+ triggerClassName?: string;
32
+ customDropdownContent?: React.ReactNode;
33
+ iconSize?: "medium" | "small";
34
+ showAddCustomModelDocs?: boolean;
35
+ forRole?: Role;
36
+ }
37
+
38
+ export const AIModelDropdown = ({
39
+ value,
40
+ placeholder,
41
+ onSelect,
42
+ triggerClassName,
43
+ customDropdownContent,
44
+ iconSize = "medium",
45
+ showAddCustomModelDocs = false,
46
+ forRole,
47
+ }: AIModelDropdownProps) => {
48
+ const currentValue = value ? AiModelId.parse(value) : undefined;
49
+
50
+ const selectModel = (modelId: QualifiedModelId) => {
51
+ onSelect(modelId);
52
+ };
53
+
54
+ const [marimoConfig] = useResolvedMarimoConfig();
55
+ const configModels = marimoConfig.ai?.models;
56
+
57
+ // Only include autocompleteModel if copilot is set to "custom"
58
+ const autocompleteModel =
59
+ marimoConfig.completion.copilot === "custom"
60
+ ? configModels?.autocomplete_model
61
+ : undefined;
62
+
63
+ const aiModelRegistry = AiModelRegistry.create({
64
+ // We add all the custom models and the models used in the editor.
65
+ // If they among the known models, they won't overwrite them.
66
+ customModels: [
67
+ ...(configModels?.custom_models ?? []),
68
+ configModels?.chat_model,
69
+ autocompleteModel,
70
+ configModels?.edit_model,
71
+ ].filter(Boolean),
72
+ displayedModels: configModels?.displayed_models,
73
+ });
74
+ const modelsByProvider = aiModelRegistry.getGroupedModelsByProvider();
75
+
76
+ const activeModel =
77
+ forRole === "autocomplete"
78
+ ? configModels?.autocomplete_model
79
+ : forRole === "chat"
80
+ ? configModels?.chat_model
81
+ : forRole === "edit"
82
+ ? configModels?.edit_model
83
+ : undefined;
84
+
85
+ const iconSizeClass = iconSize === "medium" ? "h-4 w-4" : "h-3 w-3";
86
+
87
+ const renderModelWithRole = (modelId: AiModelId, role: Role) => {
88
+ const maybeModelMatch = aiModelRegistry.getModel(modelId.id);
89
+
90
+ return (
91
+ <div className="flex items-center gap-2 w-full px-2 py-1">
92
+ <AiProviderIcon
93
+ provider={modelId.providerId}
94
+ className={iconSizeClass}
95
+ />
96
+ <div className="flex flex-col">
97
+ <span>{maybeModelMatch?.name || modelId.shortModelId}</span>
98
+ <span className="text-xs text-muted-foreground">{modelId.id}</span>
99
+ </div>
100
+
101
+ <div className="ml-auto flex gap-1">
102
+ <span
103
+ key={role}
104
+ className={`text-xs px-1.5 py-0.5 rounded font-medium ${getTagColour(role)}`}
105
+ >
106
+ {role}
107
+ </span>
108
+ </div>
109
+ </div>
110
+ );
111
+ };
112
+
113
+ return (
114
+ <DropdownMenu>
115
+ <DropdownMenuTrigger
116
+ className={`flex items-center justify-between px-2 py-0.5 border rounded-md
117
+ hover:bg-accent hover:text-accent-foreground ${triggerClassName}`}
118
+ >
119
+ <div className="flex items-center gap-2">
120
+ {currentValue ? (
121
+ <>
122
+ <AiProviderIcon
123
+ provider={currentValue.providerId}
124
+ className={iconSizeClass}
125
+ />
126
+ <span className="truncate">
127
+ {isKnownAIProvider(currentValue.providerId)
128
+ ? currentValue.shortModelId
129
+ : currentValue.id}
130
+ </span>
131
+ </>
132
+ ) : (
133
+ <span className="text-muted-foreground truncate">
134
+ {placeholder}
135
+ </span>
136
+ )}
137
+ </div>
138
+ <ChevronDownIcon className={`${iconSizeClass} ml-1`} />
139
+ </DropdownMenuTrigger>
140
+
141
+ <DropdownMenuContent className="w-[300px]">
142
+ {activeModel &&
143
+ forRole &&
144
+ renderModelWithRole(AiModelId.parse(activeModel), forRole)}
145
+ {activeModel && forRole && <DropdownMenuSeparator />}
146
+
147
+ {[...modelsByProvider.entries()].map(([provider, models]) => (
148
+ <ProviderDropdownContent
149
+ key={provider}
150
+ provider={provider}
151
+ onSelect={selectModel}
152
+ models={models}
153
+ iconSizeClass={iconSizeClass}
154
+ />
155
+ ))}
156
+
157
+ {customDropdownContent}
158
+
159
+ {showAddCustomModelDocs && (
160
+ <>
161
+ <DropdownMenuSeparator />
162
+ <DropdownMenuItem className="flex items-center gap-2">
163
+ <a
164
+ className="flex items-center gap-1"
165
+ href="https://docs.marimo.io/guides/editor_features/ai_completion/?h=models#other-ai-providers"
166
+ target="_blank"
167
+ rel="noreferrer"
168
+ >
169
+ <CircleHelpIcon className="h-3 w-3" />
170
+ <span>How to add a custom model</span>
171
+ </a>
172
+ </DropdownMenuItem>
173
+ </>
174
+ )}
175
+ </DropdownMenuContent>
176
+ </DropdownMenu>
177
+ );
178
+ };
179
+
180
+ const ProviderDropdownContent = ({
181
+ provider,
182
+ onSelect,
183
+ models,
184
+ customModelIcon,
185
+ iconSizeClass,
186
+ }: {
187
+ provider: ProviderId;
188
+ onSelect: (modelId: QualifiedModelId) => void;
189
+ models: AiModel[];
190
+ customModelIcon?: React.ReactNode;
191
+ iconSizeClass: string;
192
+ }) => {
193
+ const iconProvider = isKnownAIProvider(provider)
194
+ ? provider
195
+ : "openai-compatible";
196
+
197
+ const maybeProviderInfo = AiModelRegistry.getProviderInfo(provider);
198
+
199
+ if (models.length === 0) {
200
+ return null;
201
+ }
202
+
203
+ return (
204
+ <DropdownMenuSub>
205
+ <DropdownMenuSubTrigger>
206
+ <p className="flex items-center gap-2">
207
+ <AiProviderIcon provider={iconProvider} className={iconSizeClass} />
208
+ {getProviderLabel(provider)}
209
+ </p>
210
+ </DropdownMenuSubTrigger>
211
+ <DropdownMenuPortal>
212
+ <DropdownMenuSubContent
213
+ className="max-h-[40vh] overflow-y-auto"
214
+ alignOffset={-90}
215
+ >
216
+ {maybeProviderInfo && (
217
+ <>
218
+ <p className="text-sm text-muted-foreground p-2 max-w-[300px]">
219
+ {maybeProviderInfo.description}
220
+ <br />
221
+ </p>
222
+
223
+ <p className="text-sm text-muted-foreground p-2 pt-0">
224
+ You can find more information about this provider{" "}
225
+ <a
226
+ href={maybeProviderInfo.url}
227
+ target="_blank"
228
+ className="underline"
229
+ rel="noreferrer"
230
+ >
231
+ here
232
+ </a>
233
+ .
234
+ </p>
235
+ <DropdownMenuSeparator />
236
+ </>
237
+ )}
238
+ {models.map((model) => {
239
+ const qualifiedModelId =
240
+ `${provider}/${model.model}` as QualifiedModelId;
241
+ return (
242
+ <DropdownMenuItem
243
+ key={qualifiedModelId}
244
+ className="flex items-center gap-2"
245
+ onSelect={(e) => {
246
+ onSelect(qualifiedModelId);
247
+ }}
248
+ onClick={(e) => {
249
+ e.preventDefault();
250
+ onSelect(qualifiedModelId);
251
+ }}
252
+ >
253
+ <AiProviderIcon provider={iconProvider} className="h-4 w-4" />
254
+ <div className="pl-1 flex flex-col">
255
+ <span>{model.name}</span>
256
+ <span className="text-xs text-muted-foreground">
257
+ {model.model}
258
+ </span>
259
+ </div>
260
+ {model.custom && customModelIcon}
261
+ </DropdownMenuItem>
262
+ );
263
+ })}
264
+ </DropdownMenuSubContent>
265
+ </DropdownMenuPortal>
266
+ </DropdownMenuSub>
267
+ );
268
+ };
269
+
270
+ function getProviderLabel(provider: ProviderId): string {
271
+ const providerInfo = AiModelRegistry.getProviderInfo(provider);
272
+ if (providerInfo) {
273
+ return providerInfo.name;
274
+ }
275
+ return capitalize(provider);
276
+ }
277
+
278
+ function getTagColour(role: Role): string {
279
+ switch (role) {
280
+ case "chat":
281
+ return "bg-[var(--purple-3)] text-[var(--purple-11)]";
282
+ case "autocomplete":
283
+ return "bg-[var(--green-3)] text-[var(--green-11)]";
284
+ case "edit":
285
+ return "bg-[var(--blue-3)] text-[var(--blue-11)]";
286
+ }
287
+ return "bg-[var(--mauve-3)] text-[var(--mauve-11)]";
288
+ }
@@ -3,20 +3,23 @@
3
3
  import AnthropicIcon from "@marimo-team/llm-info/icons/anthropic.svg?inline";
4
4
  import BedrockIcon from "@marimo-team/llm-info/icons/aws.svg?inline";
5
5
  import AzureIcon from "@marimo-team/llm-info/icons/azure.svg?inline";
6
- import GoogleIcon from "@marimo-team/llm-info/icons/google.svg?inline";
6
+ import DeepseekIcon from "@marimo-team/llm-info/icons/deepseek.svg?inline";
7
+ import GeminiIcon from "@marimo-team/llm-info/icons/googlegemini.svg?inline";
7
8
  import OllamaIcon from "@marimo-team/llm-info/icons/ollama.svg?inline";
8
9
  import OpenAIIcon from "@marimo-team/llm-info/icons/openai.svg?inline";
9
10
  import { BotIcon } from "lucide-react";
10
11
  import * as React from "react";
12
+ import type { ProviderId } from "@/core/ai/ids/ids";
11
13
  import { cn } from "@/utils/cn";
12
14
 
13
- const icons = {
15
+ const icons: Record<ProviderId, string> = {
14
16
  openai: OpenAIIcon,
15
17
  anthropic: AnthropicIcon,
16
- google: GoogleIcon,
18
+ google: GeminiIcon,
17
19
  ollama: OllamaIcon,
18
20
  azure: AzureIcon,
19
21
  bedrock: BedrockIcon,
22
+ deepseek: DeepseekIcon,
20
23
  };
21
24
 
22
25
  export interface AiProviderIconProps
@@ -30,7 +33,7 @@ export const AiProviderIcon: React.FC<AiProviderIconProps> = ({
30
33
  className = "",
31
34
  ...props
32
35
  }) => {
33
- if (provider === "openai-compatible") {
36
+ if (provider === "openai-compatible" || !(provider in icons)) {
34
37
  return <BotIcon className={cn("h-4 w-4", className)} />;
35
38
  }
36
39
 
@@ -1,10 +1,12 @@
1
1
  /* Copyright 2024 Marimo. All rights reserved. */
2
2
 
3
- import React, { useId } from "react";
3
+ import { InfoIcon } from "lucide-react";
4
+ import React from "react";
4
5
  import type { FieldPath, UseFormReturn } from "react-hook-form";
5
6
  import {
6
7
  FormControl,
7
8
  FormDescription,
9
+ FormErrorsBanner,
8
10
  FormField,
9
11
  FormItem,
10
12
  FormLabel,
@@ -14,9 +16,12 @@ import { Input } from "@/components/ui/input";
14
16
  import { Kbd } from "@/components/ui/kbd";
15
17
  import { NativeSelect } from "@/components/ui/native-select";
16
18
  import { Textarea } from "@/components/ui/textarea";
19
+ import type { QualifiedModelId } from "@/core/ai/ids/ids";
17
20
  import { CopilotConfig } from "@/core/codemirror/copilot/copilot-config";
18
21
  import { DEFAULT_AI_MODEL, type UserConfig } from "@/core/config/config-schema";
19
22
  import { isWasm } from "@/core/wasm/utils";
23
+ import { Events } from "@/utils/events";
24
+ import { AIModelDropdown } from "../ai/ai-model-dropdown";
20
25
  import {
21
26
  AiProviderIcon,
22
27
  type AiProviderIconProps,
@@ -27,10 +32,12 @@ import {
27
32
  AccordionItem,
28
33
  AccordionTrigger,
29
34
  } from "../ui/accordion";
35
+ import { DropdownMenuSeparator } from "../ui/dropdown-menu";
30
36
  import { ExternalLink } from "../ui/links";
31
37
  import { Tabs, TabsContent, TabsList, TabsTrigger } from "../ui/tabs";
38
+ import { Tooltip } from "../ui/tooltip";
32
39
  import { SettingSubtitle } from "./common";
33
- import { AWS_REGIONS, KNOWN_AI_MODELS } from "./constants";
40
+ import { AWS_REGIONS } from "./constants";
34
41
  import { IncorrectModelId } from "./incorrect-model-id";
35
42
  import { IsOverridden } from "./is-overridden";
36
43
 
@@ -182,6 +189,7 @@ interface ModelSelectorProps {
182
189
  description?: React.ReactNode;
183
190
  disabled?: boolean;
184
191
  label: string;
192
+ onSubmit: (values: UserConfig) => Promise<void>;
185
193
  }
186
194
 
187
195
  export const ModelSelector: React.FC<ModelSelectorProps> = ({
@@ -193,42 +201,72 @@ export const ModelSelector: React.FC<ModelSelectorProps> = ({
193
201
  description,
194
202
  disabled = false,
195
203
  label,
204
+ onSubmit,
196
205
  }) => {
197
- const modelInputId = useId();
198
-
199
206
  return (
200
207
  <FormField
201
208
  control={form.control}
202
209
  name={name}
203
210
  disabled={disabled}
204
- render={({ field }) => (
205
- <div className="flex flex-col space-y-1">
211
+ render={({ field }) => {
212
+ const value = asStringOrUndefined(field.value);
213
+
214
+ const selectModel = (modelId: QualifiedModelId) => {
215
+ field.onChange(modelId);
216
+ onSubmit(form.getValues());
217
+ };
218
+
219
+ const renderFormItem = () => (
206
220
  <FormItem className={formItemClasses}>
207
221
  <FormLabel>{label}</FormLabel>
208
222
  <FormControl>
209
- <Input
210
- list={modelInputId}
211
- data-testid={testId}
212
- className="m-0 inline-flex"
223
+ <AIModelDropdown
224
+ value={value}
213
225
  placeholder={placeholder}
214
- {...field}
215
- value={asStringOrUndefined(field.value)}
226
+ onSelect={selectModel}
227
+ triggerClassName="text-sm"
228
+ customDropdownContent={
229
+ <>
230
+ <DropdownMenuSeparator />
231
+ <p className="px-2 py-1.5 text-sm text-muted-secondary flex items-center gap-1">
232
+ Enter a custom model
233
+ <Tooltip content="Models should include the provider prefix, e.g. 'openai/gpt-4o'">
234
+ <InfoIcon className="h-3 w-3" />
235
+ </Tooltip>
236
+ </p>
237
+ <div className="px-2 py-1">
238
+ <Input
239
+ data-testid={testId}
240
+ className="w-full border-border shadow-none focus-visible:shadow-xs"
241
+ placeholder={placeholder}
242
+ {...field}
243
+ value={asStringOrUndefined(field.value)}
244
+ onKeyDown={Events.stopPropagation()}
245
+ />
246
+ {value && (
247
+ <IncorrectModelId
248
+ value={value}
249
+ includeSuggestion={false}
250
+ />
251
+ )}
252
+ </div>
253
+ </>
254
+ }
216
255
  />
217
256
  </FormControl>
218
257
  <FormMessage />
219
- <IsOverridden userConfig={config} name={name} />
220
258
  </FormItem>
221
- <datalist id={modelInputId}>
222
- {KNOWN_AI_MODELS.map((model) => (
223
- <option value={model} key={model}>
224
- {model}
225
- </option>
226
- ))}
227
- </datalist>
228
- <IncorrectModelId value={asStringOrUndefined(field.value)} />
229
- {description && <FormDescription>{description}</FormDescription>}
230
- </div>
231
- )}
259
+ );
260
+
261
+ return (
262
+ <div className="flex flex-col space-y-1">
263
+ {renderFormItem()}
264
+ <IsOverridden userConfig={config} name={name} />
265
+ <IncorrectModelId value={value} />
266
+ {description && <FormDescription>{description}</FormDescription>}
267
+ </div>
268
+ );
269
+ }}
232
270
  />
233
271
  );
234
272
  };
@@ -297,6 +335,7 @@ export const ProviderSelect: React.FC<ProviderSelectProps> = ({
297
335
 
298
336
  const renderCopilotProvider = (
299
337
  form: UseFormReturn<UserConfig>,
338
+ onSubmit: (values: UserConfig) => Promise<void>,
300
339
  config: UserConfig,
301
340
  ) => {
302
341
  const copilot = form.getValues("completion.copilot");
@@ -331,27 +370,16 @@ const renderCopilotProvider = (
331
370
 
332
371
  if (copilot === "custom") {
333
372
  return (
334
- <>
335
- <p className="text-sm text-muted-secondary">
336
- Configure your custom AI completion provider with the following
337
- settings.
338
- </p>
339
- <ModelSelector
340
- label="Autocomplete Model"
341
- form={form}
342
- config={config}
343
- name="ai.models.autocomplete_model"
344
- placeholder="ollama/qwen2.5-coder:1.5b"
345
- testId="custom-model-input"
346
- description={
347
- <>
348
- Model to use for code completion when using a custom provider.
349
- Models should include the provider name and model name separated
350
- by a slash.
351
- </>
352
- }
353
- />
354
- </>
373
+ <ModelSelector
374
+ label="Autocomplete Model"
375
+ form={form}
376
+ config={config}
377
+ name="ai.models.autocomplete_model"
378
+ placeholder="ollama/qwen2.5-coder:1.5b"
379
+ testId="custom-model-input"
380
+ description="Model to use for code completion when using a custom provider."
381
+ onSubmit={onSubmit}
382
+ />
355
383
  );
356
384
  }
357
385
  };
@@ -363,6 +391,7 @@ const SettingGroup = ({ children }: { children: React.ReactNode }) => {
363
391
  export const AiCodeCompletionConfig: React.FC<AiConfigProps> = ({
364
392
  form,
365
393
  config,
394
+ onSubmit,
366
395
  }) => {
367
396
  return (
368
397
  <SettingGroup>
@@ -380,7 +409,7 @@ export const AiCodeCompletionConfig: React.FC<AiConfigProps> = ({
380
409
  testId="copilot-select"
381
410
  />
382
411
 
383
- {renderCopilotProvider(form, config)}
412
+ {renderCopilotProvider(form, onSubmit, config)}
384
413
  </SettingGroup>
385
414
  );
386
415
  };
@@ -645,17 +674,17 @@ export const AiProvidersConfig: React.FC<AiConfigProps> = ({
645
674
  );
646
675
  };
647
676
 
648
- export const AiAssistConfig: React.FC<AiConfigProps> = ({ form, config }) => {
677
+ export const AiAssistConfig: React.FC<AiConfigProps> = ({
678
+ form,
679
+ config,
680
+ onSubmit,
681
+ }) => {
649
682
  const isWasmRuntime = isWasm();
650
683
 
651
684
  return (
652
685
  <SettingGroup>
653
686
  <SettingSubtitle>AI Assistant</SettingSubtitle>
654
- <p className="text-sm text-muted-secondary">
655
- Use the Chat panel to talk to your codebase, or make edits using the{" "}
656
- <Kbd className="inline">Generate with AI</Kbd> button.
657
- </p>
658
-
687
+ <FormErrorsBanner />
659
688
  <ModelSelector
660
689
  label="Chat Model"
661
690
  form={form}
@@ -665,21 +694,10 @@ export const AiAssistConfig: React.FC<AiConfigProps> = ({ form, config }) => {
665
694
  testId="ai-chat-model-input"
666
695
  disabled={isWasmRuntime}
667
696
  description={
668
- <>
669
- <p>
670
- Model to use for chat conversations in the Chat panel. Models
671
- should include the provider name and model name separated by a
672
- slash. For example, "anthropic/claude-3-5-sonnet-latest" or
673
- "google/gemini-2.0-flash-exp".
674
- </p>
675
- <p className="pt-1">
676
- Depending on the provider, we will use the respective API key and
677
- additional configuration.
678
- </p>
679
- </>
697
+ <span>Model to use for chat conversations in the Chat panel.</span>
680
698
  }
699
+ onSubmit={onSubmit}
681
700
  />
682
-
683
701
  <ModelSelector
684
702
  label="Edit Model"
685
703
  form={form}
@@ -689,20 +707,26 @@ export const AiAssistConfig: React.FC<AiConfigProps> = ({ form, config }) => {
689
707
  testId="ai-edit-model-input"
690
708
  disabled={isWasmRuntime}
691
709
  description={
692
- <>
693
- <p>
694
- Model to use for code editing with the{" "}
695
- <Kbd className="inline">Generate with AI</Kbd> button. Models
696
- should include the provider name and model name separated by a
697
- slash.
698
- </p>
699
- <p className="pt-1">
700
- You can use a faster, cheaper model for edits if desired.
701
- </p>
702
- </>
710
+ <span>
711
+ Model to use for code editing with the{" "}
712
+ <Kbd className="inline">Generate with AI</Kbd> button.
713
+ </span>
703
714
  }
715
+ onSubmit={onSubmit}
704
716
  />
705
717
 
718
+ <ul className="bg-muted p-2 rounded-md list-disc space-y-1 pl-6">
719
+ <li className="text-xs text-muted-secondary">
720
+ Models should include the provider name and model name separated by a
721
+ slash. For example, "anthropic/claude-3-5-sonnet-latest" or
722
+ "google/gemini-2.0-flash-exp"
723
+ </li>
724
+ <li className="text-xs text-muted-secondary">
725
+ Depending on the provider, we will use the respective API key and
726
+ additional configuration.
727
+ </li>
728
+ </ul>
729
+
706
730
  <FormField
707
731
  control={form.control}
708
732
  name="ai.rules"
@@ -2,6 +2,7 @@
2
2
 
3
3
  import { useAtom } from "jotai";
4
4
  import { SettingsIcon } from "lucide-react";
5
+ import { VisuallyHidden } from "react-aria";
5
6
  import { AppConfigForm } from "@/components/app-config/app-config-form";
6
7
  import {
7
8
  Popover,
@@ -10,7 +11,12 @@ import {
10
11
  } from "@/components/ui/popover";
11
12
  import { Button as EditorButton } from "../editor/inputs/Inputs";
12
13
  import { Button } from "../ui/button";
13
- import { Dialog, DialogContent, DialogTrigger } from "../ui/dialog";
14
+ import {
15
+ Dialog,
16
+ DialogContent,
17
+ DialogTitle,
18
+ DialogTrigger,
19
+ } from "../ui/dialog";
14
20
  import { Tooltip } from "../ui/tooltip";
15
21
  import { settingDialogAtom } from "./state";
16
22
  import { UserConfigForm } from "./user-config-form";
@@ -46,6 +52,9 @@ export const ConfigButton: React.FC<Props> = ({
46
52
 
47
53
  const userSettingsDialog = (
48
54
  <DialogContent className="w-[80vw] h-[70vh] overflow-hidden sm:max-w-5xl top-[15vh] p-0">
55
+ <VisuallyHidden>
56
+ <DialogTitle>User settings</DialogTitle>
57
+ </VisuallyHidden>
49
58
  <UserConfigForm />
50
59
  </DialogContent>
51
60
  );
@@ -1,38 +1,4 @@
1
1
  /* Copyright 2024 Marimo. All rights reserved. */
2
- export const KNOWN_AI_MODELS = [
3
- // Anthropic
4
- "anthropic/claude-opus-4-1-20250805",
5
- "anthropic/claude-opus-4-20250514",
6
- "anthropic/claude-sonnet-4-20250514",
7
- "anthropic/claude-3-7-sonnet-latest",
8
- "anthropic/claude-3-5-sonnet-latest",
9
- "anthropic/claude-3-5-haiku-latest",
10
-
11
- // DeepSeek
12
- "deepseek/deepseek-v3",
13
- "deepseek/deepseek-r1",
14
-
15
- // Google
16
- "google/gemini-2.5-flash-preview-05-20",
17
- "google/gemini-2.5-pro-preview-06-05",
18
- "google/gemini-2.0-flash",
19
- "google/gemini-2.0-flash-lite",
20
-
21
- // OpenAI
22
- "openai/o3",
23
- "openai/o4-mini",
24
- "openai/gpt-4.5-preview",
25
- "openai/gpt-4.1",
26
- "openai/gpt-4o",
27
- "openai/gpt-3.5-turbo",
28
-
29
- // AWS Bedrock Models
30
- "bedrock/anthropic.claude-3-5-haiku-20241022-v1:0",
31
- "bedrock/anthropic.claude-3-5-sonnet-20241022-v2:0",
32
- "bedrock/anthropic.claude-3-7-sonnet-20250219-v1:0",
33
- "bedrock/meta.llama3-3-70b-instruct-v1:0",
34
- "bedrock/cohere.command-r-plus-v1",
35
- ] as const;
36
2
 
37
3
  /**
38
4
  * AWS regions where the Bedrock service is available