@marimo-team/islands 0.15.2 → 0.15.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/{ConnectedDataExplorerComponent-C39nQwtD.js → ConnectedDataExplorerComponent-DfvW3rBn.js} +323 -328
- package/dist/{ImageComparisonComponent-BhkiyswP.js → ImageComparisonComponent-XaJshw7d.js} +13 -13
- package/dist/{_baseUniq-DdHL34FO.js → _baseUniq-dN9WKF9m.js} +67 -67
- package/dist/any-language-editor-CpFniVi-.js +27 -0
- package/dist/{arc-BXrety1g.js → arc-BOhn-m2C.js} +1 -1
- package/dist/{architectureDiagram-KFL7JDKH-BMy6ywCF.js → architectureDiagram-W76B3OCA-Bpg85ZKv.js} +144 -144
- package/dist/assets/{worker-COGufAQn.js → worker-Y-Q4G-N2.js} +30 -26
- package/dist/asterisk-DS281yxp.js +271 -0
- package/dist/{blockDiagram-ZYB65J3Q-DYT2-nlI.js → blockDiagram-QIGZ2CNN-DS1kOHlW.js} +10 -10
- package/dist/{c4Diagram-AAMF2YG6-ZiQzioe6.js → c4Diagram-FPNF74CW-CyRVKssw.js} +8 -8
- package/dist/{channel-CeuXqUAU.js → channel-BilGXox7.js} +1 -1
- package/dist/{chunk-ANTBXLJU-BvYnIrdq.js → chunk-4BX2VUAB-CZR39zCO.js} +1 -1
- package/dist/{chunk-WVR4S24B-DXj8yaUk.js → chunk-55IACEB6-BIH-MYov.js} +1 -1
- package/dist/{chunk-GLLZNHP4-CyFsosAe.js → chunk-FMBD7UC4-4PZXFZE8.js} +1 -1
- package/dist/{chunk-JBRWN2VN-DA_EEhy2.js → chunk-K7UQS3LO-CEvWKznk.js} +117 -117
- package/dist/{chunk-NRVI72HA-BYx2jMlI.js → chunk-QN33PNHL-D5LO5Jq_.js} +1 -1
- package/dist/{chunk-FHKO5MBM-DfCztBk8.js → chunk-QZHKN3VN-6gwUonWI.js} +1 -1
- package/dist/{chunk-LXBSTHXV-Se7vdY6J.js → chunk-TVAH2DTR-3gm06QdU.js} +7 -7
- package/dist/{chunk-OMD6QJNC-CqgcPMgL.js → chunk-TZMSLE5B-Cm8Iy9bO.js} +1 -1
- package/dist/{classDiagram-v2-QTMF73CY-B19A3G1l.js → classDiagram-KNZD7YFC-DC529O_z.js} +2 -2
- package/dist/{classDiagram-3BZAVTQC-B19A3G1l.js → classDiagram-v2-RKCZMP56-DC529O_z.js} +2 -2
- package/dist/{clone-78au0tn1.js → clone-CLoRX3j6.js} +1 -1
- package/dist/cose-bilkent-S5V4N54A-qf5DlS6Y.js +2609 -0
- package/dist/{cytoscape.esm-BYnVVhJX.js → cytoscape.esm-DfdJODL8.js} +34 -34
- package/dist/{dagre-2BBEFEWP-BfEn3ZUV.js → dagre-5GWH7T2D-Ceocls0m.js} +6 -6
- package/dist/{data-grid-overlay-editor-CH_qLkV2.js → data-grid-overlay-editor-AqDS_UKe.js} +11 -11
- package/dist/{diagram-4IRLE6MV-CL8xidnG.js → diagram-N5W7TBWH-CP66oSiv.js} +59 -60
- package/dist/{diagram-RP2FKANI-B1BPcUew.js → diagram-QEK2KX5R-_YD4kxxi.js} +15 -15
- package/dist/{diagram-GUPCWM2R-CZ5cfqlq.js → diagram-S2PKOQOG-Cnj8T-OP.js} +10 -10
- package/dist/dockerfile-Cm8cRYCN.js +194 -0
- package/dist/ebnf-DUPDuY4r.js +78 -0
- package/dist/{erDiagram-HZWUO2LU-BEAIww50.js → erDiagram-AWTI2OKA-CGnvoHx6.js} +8 -8
- package/dist/fcl-CPC2WYrI.js +103 -0
- package/dist/{flowDiagram-THRYKUMA-Czs2UAI2.js → flowDiagram-PVAE7QVJ-DG-pr9R9.js} +9 -9
- package/dist/{ganttDiagram-WV7ZQ7D5-ByYIAVFO.js → ganttDiagram-OWAHRB6G-JmChtxvn.js} +34 -34
- package/dist/{gitGraphDiagram-OJR772UL-BcpDsiyB.js → gitGraphDiagram-NY62KEGX-D8wLfOPd.js} +4 -4
- package/dist/{glide-data-editor-CmN6FVyi.js → glide-data-editor-9nC3iCIZ.js} +33 -33
- package/dist/{graph-77W6heli.js → graph-CoRe7vAN.js} +3 -3
- package/dist/http-D9LttvKF.js +44 -0
- package/dist/{index-Bfk9dnyS.js → index-6qYeHHjQ.js} +33090 -32892
- package/dist/{index-BOojn38D.js → index-BpzLh4Qe.js} +7711 -7711
- package/dist/{index-CmozKMxx.js → index-BthgsgYX.js} +6 -6
- package/dist/{index-pBmAzQJl.js → index-MCx5v1x0.js} +2 -2
- package/dist/index-jkm77Jrz.js +98 -0
- package/dist/{infoDiagram-6WOFNB3A-CfzLHHVP.js → infoDiagram-STP46IZ2-BlXxvOrR.js} +2 -2
- package/dist/{journeyDiagram-FFXJYRFH-ndAcpkGn.js → journeyDiagram-BIP6EPQ6-CNRYs_Fc.js} +24 -26
- package/dist/{kanban-definition-KOZQBZVT-DcQYzNvc.js → kanban-definition-6OIFK2YF-B9HeMAuP.js} +14 -14
- package/dist/{layout-XySVHJgD.js → layout-m2vOUiW1.js} +81 -81
- package/dist/{linear-PbooOqg7.js → linear-DU6Q5CX3.js} +35 -35
- package/dist/{main-B5yML0bw.js → main-BD2KGFpU.js} +74594 -68034
- package/dist/main.js +1 -1
- package/dist/{mermaid-Cg5IX6Nv.js → mermaid-HVCtvbyx.js} +6160 -7493
- package/dist/min-DcGMA4e_.js +80 -0
- package/dist/mindmap-definition-Q6HEUPPD-BW8UmIDQ.js +785 -0
- package/dist/nginx-zDPm3Z74.js +89 -0
- package/dist/{number-overlay-editor-DUhfZqtP.js → number-overlay-editor-D8Hl0Syo.js} +19 -19
- package/dist/{pieDiagram-DBDJKBY4-DTOlNsja.js → pieDiagram-ADFJNKIX-Bg-3zg5U.js} +17 -17
- package/dist/{quadrantDiagram-YPSRARAO-BX2d8VS-.js → quadrantDiagram-LMRXKWRM-BO4IG6Yz.js} +6 -6
- package/dist/{react-plotly-Dcyw-3Sa.js → react-plotly-dkvHVuRb.js} +3577 -3577
- package/dist/{requirementDiagram-EGVEC5DT-D1T5u-wG.js → requirementDiagram-4UW4RH46-5sdTguSM.js} +7 -7
- package/dist/{sankeyDiagram-HRAUVNP4-G6xDfnp-.js → sankeyDiagram-GR3RE2ED-Buhlv9OI.js} +5 -5
- package/dist/sequenceDiagram-C3RYC4MD-C3qsM2UP.js +2519 -0
- package/dist/{slides-component-BJLlPJSr.js → slides-component-D209A0-s.js} +66 -66
- package/dist/solr-BNlsLglM.js +41 -0
- package/dist/spreadsheet-C-cy4P5N.js +49 -0
- package/dist/{stateDiagram-UUKSUZ4H-CYXbjaom.js → stateDiagram-KXAO66HF-CopJ7G6P.js} +5 -5
- package/dist/{stateDiagram-v2-EYPG3UTE-Br1HYKT6.js → stateDiagram-v2-UMBNRL4Z-CejL8AKf.js} +2 -2
- package/dist/style.css +1 -1
- package/dist/tiddlywiki-5wqsXtSk.js +155 -0
- package/dist/tiki-__Kn3CeS.js +181 -0
- package/dist/{time-B9SZnSen.js → time-BwSBitlN.js} +58 -58
- package/dist/{timeline-definition-3HZDQTIS-DeK_ZRD0.js → timeline-definition-XQNQX7LJ-DzMNTX-C.js} +10 -12
- package/dist/{timer-BYwnU4DF.js → timer-B0-z63CM.js} +16 -16
- package/dist/{treemap-75Q7IDZK-CKP4vV_0.js → treemap-75Q7IDZK-zeJG07dk.js} +14 -14
- package/dist/{vega-component-CpgdqX2d.js → vega-component-CUkiTayd.js} +30 -30
- package/dist/{xychartDiagram-FDP5SA34-AMEPsx_R.js → xychartDiagram-6GGTOJPD-DiENNXMS.js} +7 -7
- package/package.json +39 -39
- package/src/__mocks__/notebook.ts +3 -0
- package/src/__mocks__/requests.ts +3 -0
- package/src/__tests__/__snapshots__/CellStatus.test.tsx.snap +12 -12
- package/src/__tests__/chat-utils.test.ts +26 -211
- package/src/components/ai/ai-model-dropdown.tsx +25 -9
- package/src/components/app-config/ai-config.tsx +7 -0
- package/src/components/chat/chat-components.tsx +71 -0
- package/src/components/chat/chat-panel.tsx +481 -291
- package/src/components/chat/chat-utils.ts +50 -0
- package/src/components/chat/markdown-renderer.tsx +3 -7
- package/src/components/chat/tool-call-accordion.tsx +5 -5
- package/src/components/datasources/__tests__/utils.test.ts +6 -0
- package/src/components/datasources/column-preview.tsx +1 -3
- package/src/components/editor/actions/useNotebookActions.tsx +1 -1
- package/src/components/editor/ai/add-cell-with-ai.tsx +20 -15
- package/src/components/editor/ai/ai-completion-editor.tsx +22 -3
- package/src/components/editor/ai/completion-handlers.tsx +2 -4
- package/src/components/editor/ai/completion-utils.ts +85 -11
- package/src/components/editor/alerts/startup-logs-alert.tsx +72 -0
- package/src/components/editor/chrome/panels/datasources-panel.tsx +3 -1
- package/src/components/editor/chrome/panels/dependency-graph-panel.tsx +3 -1
- package/src/components/editor/chrome/panels/documentation-panel.tsx +3 -1
- package/src/components/editor/chrome/panels/error-panel.tsx +3 -1
- package/src/components/editor/chrome/panels/file-explorer-panel.tsx +3 -1
- package/src/components/editor/chrome/panels/logs-panel.tsx +3 -1
- package/src/components/editor/chrome/panels/outline-panel.tsx +3 -1
- package/src/components/editor/chrome/panels/packages-panel.tsx +4 -2
- package/src/components/editor/chrome/panels/scratchpad-panel.tsx +3 -1
- package/src/components/editor/chrome/panels/secrets-panel.tsx +3 -1
- package/src/components/editor/chrome/panels/snippets-panel.tsx +3 -1
- package/src/components/editor/chrome/panels/tracing-panel.tsx +3 -1
- package/src/components/editor/chrome/panels/variable-panel.tsx +3 -1
- package/src/components/editor/chrome/wrapper/app-chrome.tsx +38 -28
- package/src/components/editor/controls/command-palette-button.tsx +1 -1
- package/src/components/editor/controls/command-palette.tsx +5 -4
- package/src/components/editor/controls/state.ts +4 -0
- package/src/components/editor/package-alert.tsx +108 -58
- package/src/components/editor/renderers/CellArray.tsx +2 -0
- package/src/components/editor/renderers/vertical-layout/vertical-layout.tsx +0 -1
- package/src/components/pages/edit-page.tsx +7 -3
- package/src/core/ai/chat-utils.ts +26 -43
- package/src/core/ai/config.ts +1 -1
- package/src/core/ai/context/__tests__/registry.test.ts +277 -3
- package/src/core/ai/context/context.ts +11 -1
- package/src/core/ai/context/providers/__tests__/cell-output.test.ts +378 -0
- package/src/core/ai/context/providers/__tests__/error.test.ts +3 -2
- package/src/core/ai/context/providers/__tests__/file.test.ts +119 -0
- package/src/core/ai/context/providers/cell-output.ts +349 -0
- package/src/core/ai/context/providers/common.ts +5 -1
- package/src/core/ai/context/providers/file.ts +287 -0
- package/src/core/ai/context/registry.ts +79 -0
- package/src/core/ai/state.ts +22 -5
- package/src/core/alerts/state.ts +71 -3
- package/src/core/cells/cell.ts +2 -2
- package/src/core/cells/cells.ts +1 -1
- package/src/core/cells/logs.ts +1 -1
- package/src/core/cells/runs.ts +6 -5
- package/src/core/codemirror/ai/resources.ts +47 -5
- package/src/core/codemirror/ai/state.ts +12 -0
- package/src/core/codemirror/language/__tests__/sql.test.ts +45 -0
- package/src/core/codemirror/markdown/__tests__/commands.test.ts +1 -0
- package/src/core/codemirror/theme/dark.ts +1 -1
- package/src/core/config/capabilities.ts +1 -1
- package/src/core/datasets/__tests__/data-source.test.ts +24 -0
- package/src/core/errors/__tests__/errors.test.ts +2 -0
- package/src/core/islands/bridge.ts +1 -0
- package/src/core/islands/main.ts +1 -0
- package/src/core/kernel/messages.ts +12 -6
- package/src/core/layout/layout.ts +3 -3
- package/src/core/network/requests-network.ts +8 -0
- package/src/core/network/requests-static.ts +1 -0
- package/src/core/network/requests-toasting.ts +1 -0
- package/src/core/network/types.ts +4 -1
- package/src/core/wasm/bridge.ts +18 -2
- package/src/core/wasm/worker/bootstrap.ts +3 -1
- package/src/core/wasm/worker/getMarimoWheel.ts +3 -8
- package/src/core/wasm/worker/types.ts +3 -0
- package/src/core/websocket/useMarimoWebSocket.tsx +7 -1
- package/src/css/app/Cell.css +42 -21
- package/src/css/app/codemirror.css +5 -1
- package/src/css/globals.css +3 -0
- package/src/css/md.css +1 -1
- package/src/plugins/impl/MicrophonePlugin.tsx +2 -2
- package/src/plugins/impl/chat/ChatPlugin.tsx +2 -9
- package/src/plugins/impl/chat/chat-ui.tsx +129 -110
- package/src/plugins/impl/chat/types.ts +5 -8
- package/src/plugins/impl/code/__tests__/language.test.ts +15 -0
- package/src/plugins/impl/code/any-language-editor.tsx +11 -8
- package/src/plugins/layout/MimeRenderPlugin.tsx +3 -6
- package/src/stories/cell.stories.tsx +6 -0
- package/src/stories/layout/vertical/one-column.stories.tsx +215 -0
- package/src/theme/useTheme.ts +11 -6
- package/src/utils/__tests__/blob.test.ts +37 -0
- package/src/utils/arrays.ts +13 -0
- package/src/utils/fileToBase64.ts +21 -6
- package/src/utils/json/base64.ts +5 -2
- package/src/utils/numbers.ts +9 -7
- package/dist/any-language-editor-DC5170DQ.js +0 -45
- package/dist/asn1-jKiBa2Ya.js +0 -95
- package/dist/clojure-CCKyeQKf.js +0 -800
- package/dist/css-BkF-NPzE.js +0 -1553
- package/dist/index-5ZH_qS8j.js +0 -288
- package/dist/index-U4yn89qO.js +0 -341
- package/dist/javascript-C2yteZeJ.js +0 -691
- package/dist/min-DS5Jz-hg.js +0 -80
- package/dist/mindmap-definition-LNHGMQRG-0aOVaMR8.js +0 -3234
- package/dist/mllike-BSnXJBGA.js +0 -272
- package/dist/pug-CwAQJzGR.js +0 -248
- package/dist/python-BkR3uSy8.js +0 -313
- package/dist/rpm-IznJm2Xc.js +0 -57
- package/dist/sequenceDiagram-WFGC7UMF-DMhHzllb.js +0 -2284
- package/dist/ttcn-cfg-Bac_acMi.js +0 -88
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
/* Copyright 2024 Marimo. All rights reserved. */
|
|
2
|
+
|
|
3
|
+
import type { Completion } from "@codemirror/autocomplete";
|
|
4
|
+
import type { FileUIPart } from "ai";
|
|
5
|
+
import { toPng } from "html-to-image";
|
|
6
|
+
import { notebookAtom } from "@/core/cells/cells";
|
|
7
|
+
import { type CellId, CellOutputId } from "@/core/cells/ids";
|
|
8
|
+
import { displayCellName } from "@/core/cells/names";
|
|
9
|
+
import { isOutputEmpty } from "@/core/cells/outputs";
|
|
10
|
+
import type { OutputMessage } from "@/core/kernel/messages";
|
|
11
|
+
import type { JotaiStore } from "@/core/state/jotai";
|
|
12
|
+
import { Logger } from "@/utils/Logger";
|
|
13
|
+
import { type AIContextItem, AIContextProvider } from "../registry";
|
|
14
|
+
import { contextToXml } from "../utils";
|
|
15
|
+
import { Boosts } from "./common";
|
|
16
|
+
|
|
17
|
+
export interface CellOutputContextItem extends AIContextItem {
|
|
18
|
+
type: "cell-output";
|
|
19
|
+
data: {
|
|
20
|
+
cellId: CellId;
|
|
21
|
+
cellName: string;
|
|
22
|
+
cellCode: string;
|
|
23
|
+
output: OutputMessage;
|
|
24
|
+
outputType: "text" | "media";
|
|
25
|
+
processedContent?: string;
|
|
26
|
+
imageUrl?: string;
|
|
27
|
+
shouldDownloadImage?: boolean;
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function isMediaMimetype(
|
|
32
|
+
mimetype: OutputMessage["mimetype"] | undefined,
|
|
33
|
+
htmlString: string,
|
|
34
|
+
): boolean {
|
|
35
|
+
if (!mimetype) {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const mediaPrefixes = ["image/", "video/", "audio/", "application/pdf"];
|
|
40
|
+
if (mediaPrefixes.some((prefix) => mimetype.startsWith(prefix))) {
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
const mediaIncludes = ["svg", "vega"];
|
|
44
|
+
if (mediaIncludes.some((include) => mimetype.includes(include))) {
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// If it is HTML, we need to check if it contains a media tag
|
|
49
|
+
if (mimetype === "text/html") {
|
|
50
|
+
const mediaTags = [
|
|
51
|
+
"<img",
|
|
52
|
+
"<video",
|
|
53
|
+
"<audio",
|
|
54
|
+
"<iframe",
|
|
55
|
+
"<canvas",
|
|
56
|
+
"<svg",
|
|
57
|
+
"<marimo-ui-element",
|
|
58
|
+
];
|
|
59
|
+
if (mediaTags.some((tag) => htmlString.includes(tag))) {
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function parseHtmlContent(htmlString: string): string {
|
|
68
|
+
try {
|
|
69
|
+
// Create a temporary DOM element to parse HTML
|
|
70
|
+
const tempDiv = document.createElement("div");
|
|
71
|
+
tempDiv.innerHTML = htmlString;
|
|
72
|
+
|
|
73
|
+
// Extract text content, removing HTML tags
|
|
74
|
+
const textContent = tempDiv.textContent || tempDiv.innerText || "";
|
|
75
|
+
|
|
76
|
+
// Clean up extra whitespace
|
|
77
|
+
return textContent.replaceAll(/\s+/g, " ").trim();
|
|
78
|
+
} catch (error) {
|
|
79
|
+
Logger.error("Error parsing HTML content:", error);
|
|
80
|
+
// If parsing fails, return the original string
|
|
81
|
+
return htmlString;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export class CellOutputContextProvider extends AIContextProvider<CellOutputContextItem> {
|
|
86
|
+
readonly title = "Cell Outputs";
|
|
87
|
+
readonly mentionPrefix = "@";
|
|
88
|
+
readonly contextType = "cell-output";
|
|
89
|
+
|
|
90
|
+
constructor(private store: JotaiStore) {
|
|
91
|
+
super();
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
getItems(): CellOutputContextItem[] {
|
|
95
|
+
const notebook = this.store.get(notebookAtom);
|
|
96
|
+
const items: CellOutputContextItem[] = [];
|
|
97
|
+
|
|
98
|
+
for (const [cellIndex, cellId] of notebook.cellIds.inOrderIds.entries()) {
|
|
99
|
+
const cellRuntime = notebook.cellRuntime[cellId];
|
|
100
|
+
|
|
101
|
+
// Filter to only cells with output
|
|
102
|
+
if (!cellRuntime?.output || isOutputEmpty(cellRuntime.output)) {
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const cellData = notebook.cellData[cellId];
|
|
107
|
+
const output = cellRuntime.output;
|
|
108
|
+
const mimetype = output.mimetype;
|
|
109
|
+
|
|
110
|
+
// Determine output type
|
|
111
|
+
const isMedia = isMediaMimetype(mimetype, String(output.data));
|
|
112
|
+
const outputType = isMedia ? "media" : "text";
|
|
113
|
+
|
|
114
|
+
const cellName = displayCellName(cellData.name, cellIndex);
|
|
115
|
+
|
|
116
|
+
let processedContent: string | undefined;
|
|
117
|
+
let imageUrl: string | undefined;
|
|
118
|
+
let shouldDownloadImage = false;
|
|
119
|
+
|
|
120
|
+
// Process text content
|
|
121
|
+
if (outputType === "text" && typeof output.data === "string") {
|
|
122
|
+
processedContent =
|
|
123
|
+
mimetype === "text/html"
|
|
124
|
+
? parseHtmlContent(output.data)
|
|
125
|
+
: output.data;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Process media content - for now, we'll just note that it's media
|
|
129
|
+
if (outputType === "media") {
|
|
130
|
+
if (
|
|
131
|
+
typeof output.data === "string" &&
|
|
132
|
+
output.data.startsWith("data:")
|
|
133
|
+
) {
|
|
134
|
+
imageUrl = output.data; // Data URL
|
|
135
|
+
} else if (
|
|
136
|
+
typeof output.data === "string" &&
|
|
137
|
+
output.data.startsWith("http")
|
|
138
|
+
) {
|
|
139
|
+
imageUrl = output.data; // External URL
|
|
140
|
+
} else {
|
|
141
|
+
// Download the image from the DOM element
|
|
142
|
+
shouldDownloadImage = true;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
items.push({
|
|
147
|
+
uri: this.asURI(cellId),
|
|
148
|
+
name: cellName,
|
|
149
|
+
type: this.contextType,
|
|
150
|
+
description: `Cell output (${mimetype || "unknown"})`,
|
|
151
|
+
data: {
|
|
152
|
+
cellId,
|
|
153
|
+
cellName,
|
|
154
|
+
cellCode: cellData?.code || "",
|
|
155
|
+
output,
|
|
156
|
+
outputType,
|
|
157
|
+
processedContent,
|
|
158
|
+
imageUrl,
|
|
159
|
+
shouldDownloadImage,
|
|
160
|
+
},
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return items;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
formatCompletion(item: CellOutputContextItem): Completion {
|
|
168
|
+
const { data } = item;
|
|
169
|
+
return {
|
|
170
|
+
label: `@${data.cellName}`,
|
|
171
|
+
displayLabel: data.cellName,
|
|
172
|
+
detail: `${data.outputType} output`,
|
|
173
|
+
boost: Boosts.CELL_OUTPUT,
|
|
174
|
+
type: this.contextType,
|
|
175
|
+
section: "Cell Output",
|
|
176
|
+
apply: `@${data.cellName}`,
|
|
177
|
+
info: () => {
|
|
178
|
+
const infoContainer = document.createElement("div");
|
|
179
|
+
infoContainer.classList.add(
|
|
180
|
+
"mo-cm-tooltip",
|
|
181
|
+
"docs-documentation",
|
|
182
|
+
"min-w-[300px]",
|
|
183
|
+
"max-w-[500px]",
|
|
184
|
+
"flex",
|
|
185
|
+
"flex-col",
|
|
186
|
+
"gap-2",
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
const headerDiv = document.createElement("div");
|
|
190
|
+
headerDiv.classList.add("flex", "flex-col", "gap-1");
|
|
191
|
+
|
|
192
|
+
const nameDiv = document.createElement("div");
|
|
193
|
+
nameDiv.classList.add("font-bold", "text-base");
|
|
194
|
+
nameDiv.textContent = data.cellName;
|
|
195
|
+
headerDiv.append(nameDiv);
|
|
196
|
+
|
|
197
|
+
const descriptionDiv = document.createElement("div");
|
|
198
|
+
descriptionDiv.classList.add("text-sm", "text-muted-foreground");
|
|
199
|
+
headerDiv.append(descriptionDiv);
|
|
200
|
+
|
|
201
|
+
infoContainer.append(headerDiv);
|
|
202
|
+
|
|
203
|
+
// Show cell code preview
|
|
204
|
+
if (data.cellCode) {
|
|
205
|
+
const codeHeaderDiv = document.createElement("div");
|
|
206
|
+
codeHeaderDiv.classList.add(
|
|
207
|
+
"text-xs",
|
|
208
|
+
"font-medium",
|
|
209
|
+
"text-muted-foreground",
|
|
210
|
+
);
|
|
211
|
+
codeHeaderDiv.textContent = "Code:";
|
|
212
|
+
infoContainer.append(codeHeaderDiv);
|
|
213
|
+
|
|
214
|
+
const codeDiv = document.createElement("div");
|
|
215
|
+
codeDiv.classList.add(
|
|
216
|
+
"text-xs",
|
|
217
|
+
"font-mono",
|
|
218
|
+
"bg-muted",
|
|
219
|
+
"p-2",
|
|
220
|
+
"rounded",
|
|
221
|
+
"max-h-20",
|
|
222
|
+
"overflow-y-auto",
|
|
223
|
+
);
|
|
224
|
+
codeDiv.textContent =
|
|
225
|
+
data.cellCode.slice(0, 200) +
|
|
226
|
+
(data.cellCode.length > 200 ? "..." : "");
|
|
227
|
+
infoContainer.append(codeDiv);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Show output preview
|
|
231
|
+
if (data.processedContent) {
|
|
232
|
+
const outputHeaderDiv = document.createElement("div");
|
|
233
|
+
outputHeaderDiv.classList.add(
|
|
234
|
+
"text-xs",
|
|
235
|
+
"font-medium",
|
|
236
|
+
"text-muted-foreground",
|
|
237
|
+
"mt-2",
|
|
238
|
+
);
|
|
239
|
+
outputHeaderDiv.textContent = "Output Preview:";
|
|
240
|
+
infoContainer.append(outputHeaderDiv);
|
|
241
|
+
|
|
242
|
+
const outputDiv = document.createElement("div");
|
|
243
|
+
outputDiv.classList.add(
|
|
244
|
+
"text-xs",
|
|
245
|
+
"bg-muted",
|
|
246
|
+
"p-2",
|
|
247
|
+
"rounded",
|
|
248
|
+
"max-h-24",
|
|
249
|
+
"overflow-y-auto",
|
|
250
|
+
"mb-2",
|
|
251
|
+
);
|
|
252
|
+
outputDiv.textContent =
|
|
253
|
+
data.processedContent.slice(0, 300) +
|
|
254
|
+
(data.processedContent.length > 300 ? "..." : "");
|
|
255
|
+
infoContainer.append(outputDiv);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (data.outputType === "media") {
|
|
259
|
+
const mediaDiv = document.createElement("div");
|
|
260
|
+
mediaDiv.classList.add(
|
|
261
|
+
"text-xs",
|
|
262
|
+
"text-muted-foreground",
|
|
263
|
+
"italic",
|
|
264
|
+
"mb-2",
|
|
265
|
+
);
|
|
266
|
+
mediaDiv.textContent =
|
|
267
|
+
"Contains media content (image, SVG, or canvas)";
|
|
268
|
+
infoContainer.append(mediaDiv);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
return infoContainer;
|
|
272
|
+
},
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
formatContext(item: CellOutputContextItem): string {
|
|
277
|
+
const { data } = item;
|
|
278
|
+
|
|
279
|
+
const contextData = {
|
|
280
|
+
name: data.cellName,
|
|
281
|
+
cellId: data.cellId,
|
|
282
|
+
outputType: data.outputType,
|
|
283
|
+
mimetype: data.output.mimetype,
|
|
284
|
+
} as const;
|
|
285
|
+
|
|
286
|
+
let details = `Cell Code:\n${data.cellCode}\n\n`;
|
|
287
|
+
|
|
288
|
+
if (data.outputType === "text" && data.processedContent) {
|
|
289
|
+
details += `Output:\n${data.processedContent}`;
|
|
290
|
+
} else if (data.outputType === "media") {
|
|
291
|
+
details += `Media Output: Contains ${data.output.mimetype} content`;
|
|
292
|
+
if (data.imageUrl) {
|
|
293
|
+
details += `\nImage URL: ${data.imageUrl}`;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
return contextToXml({
|
|
298
|
+
type: this.contextType,
|
|
299
|
+
data: contextData,
|
|
300
|
+
details,
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/** Get attachments for cell output items that have shouldDownloadImage=true */
|
|
305
|
+
override async getAttachments(
|
|
306
|
+
items: CellOutputContextItem[],
|
|
307
|
+
): Promise<FileUIPart[]> {
|
|
308
|
+
// Filter items that need image downloading
|
|
309
|
+
const itemsNeedingDownload = items.filter(
|
|
310
|
+
(item) =>
|
|
311
|
+
item.data.shouldDownloadImage && item.data.outputType === "media",
|
|
312
|
+
);
|
|
313
|
+
|
|
314
|
+
if (itemsNeedingDownload.length === 0) {
|
|
315
|
+
return [];
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Prepare download requests
|
|
319
|
+
const downloadRequests = itemsNeedingDownload.flatMap((item) => {
|
|
320
|
+
const outputElement = document.getElementById(
|
|
321
|
+
CellOutputId.create(item.data.cellId),
|
|
322
|
+
);
|
|
323
|
+
if (!outputElement) {
|
|
324
|
+
Logger.warn(`Output element not found for cell ${item.data.cellId}`);
|
|
325
|
+
return [];
|
|
326
|
+
}
|
|
327
|
+
return {
|
|
328
|
+
cellId: item.data.cellId,
|
|
329
|
+
cellName: item.data.cellName,
|
|
330
|
+
mimetype: item.data.output.mimetype,
|
|
331
|
+
element: outputElement,
|
|
332
|
+
};
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
try {
|
|
336
|
+
return await Promise.all(
|
|
337
|
+
downloadRequests.map(async (item) => ({
|
|
338
|
+
type: "file",
|
|
339
|
+
filename: `${item.cellName}-output-screenshot`,
|
|
340
|
+
mediaType: "image/png",
|
|
341
|
+
url: await toPng(item.element),
|
|
342
|
+
})),
|
|
343
|
+
);
|
|
344
|
+
} catch (error) {
|
|
345
|
+
Logger.error("Error downloading cell output images:", error);
|
|
346
|
+
return [];
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
/* Copyright 2024 Marimo. All rights reserved. */
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
type Completion,
|
|
5
|
+
type CompletionContext,
|
|
6
|
+
type CompletionResult,
|
|
7
|
+
type CompletionSource,
|
|
8
|
+
closeCompletion,
|
|
9
|
+
} from "@codemirror/autocomplete";
|
|
10
|
+
import { toast } from "@/components/ui/use-toast";
|
|
11
|
+
import { contextCallbacks } from "@/core/codemirror/ai/state";
|
|
12
|
+
import type { EditRequests, FileInfo, RunRequests } from "@/core/network/types";
|
|
13
|
+
import { deserializeBlob } from "@/utils/blob";
|
|
14
|
+
import { type Base64String, base64ToDataURL } from "@/utils/json/base64";
|
|
15
|
+
import { Logger } from "@/utils/Logger";
|
|
16
|
+
import { type AIContextItem, AIContextProvider } from "../registry";
|
|
17
|
+
import { contextToXml } from "../utils";
|
|
18
|
+
import { Boosts } from "./common";
|
|
19
|
+
export interface FileContextItem extends AIContextItem {
|
|
20
|
+
type: "file";
|
|
21
|
+
data: {
|
|
22
|
+
path: string;
|
|
23
|
+
isDirectory: boolean;
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface FileSearchConfig {
|
|
28
|
+
maxDepth: number;
|
|
29
|
+
maxResults: number;
|
|
30
|
+
defaultResultsLimit: number;
|
|
31
|
+
includeDirectories: boolean;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const DEFAULT_FILE_SEARCH_CONFIG: FileSearchConfig = {
|
|
35
|
+
maxDepth: 3,
|
|
36
|
+
maxResults: 20,
|
|
37
|
+
defaultResultsLimit: 5,
|
|
38
|
+
includeDirectories: false,
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export class FileContextProvider extends AIContextProvider<FileContextItem> {
|
|
42
|
+
readonly title = "Files";
|
|
43
|
+
readonly mentionPrefix = "#";
|
|
44
|
+
readonly contextType = "file";
|
|
45
|
+
|
|
46
|
+
constructor(
|
|
47
|
+
private apiRequests: EditRequests & RunRequests,
|
|
48
|
+
private config: FileSearchConfig = DEFAULT_FILE_SEARCH_CONFIG,
|
|
49
|
+
) {
|
|
50
|
+
super();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Create a dynamic completion source for file mentions
|
|
55
|
+
* This bypasses the standard registry system to enable dynamic searching
|
|
56
|
+
*/
|
|
57
|
+
createCompletionSource(): CompletionSource {
|
|
58
|
+
return async (
|
|
59
|
+
context: CompletionContext,
|
|
60
|
+
): Promise<CompletionResult | null> => {
|
|
61
|
+
// Look for # followed by any characters (including dots, slashes, etc.)
|
|
62
|
+
const match = context.matchBefore(/#[^\s#]*/);
|
|
63
|
+
if (!match) {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const matchText = match.text;
|
|
68
|
+
if (!matchText.startsWith("#")) {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const searchQuery = matchText.slice(1); // Remove the #
|
|
73
|
+
if (searchQuery.length === 0) {
|
|
74
|
+
// Show some popular files/directories even with no query
|
|
75
|
+
return this.getDefaultCompletions(match);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
try {
|
|
79
|
+
const files = (await this.searchFiles(searchQuery)) || [];
|
|
80
|
+
const completions = files.map((file: FileInfo) => {
|
|
81
|
+
const item: FileContextItem = {
|
|
82
|
+
uri: this.asURI(file.path),
|
|
83
|
+
name: file.name,
|
|
84
|
+
type: this.contextType,
|
|
85
|
+
description: file.isDirectory ? "Directory" : "File",
|
|
86
|
+
data: {
|
|
87
|
+
path: file.path,
|
|
88
|
+
isDirectory: file.isDirectory,
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
return this.formatCompletion(item);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
return {
|
|
95
|
+
from: match.from,
|
|
96
|
+
options: completions,
|
|
97
|
+
};
|
|
98
|
+
} catch (error) {
|
|
99
|
+
Logger.error("Failed to search files:", error);
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
private searchFiles = async (
|
|
106
|
+
query: string,
|
|
107
|
+
options: Partial<FileSearchConfig> = {},
|
|
108
|
+
): Promise<FileInfo[]> => {
|
|
109
|
+
const { maxDepth, maxResults, includeDirectories } = {
|
|
110
|
+
...this.config,
|
|
111
|
+
...options,
|
|
112
|
+
};
|
|
113
|
+
const response = await this.apiRequests.sendSearchFiles({
|
|
114
|
+
query,
|
|
115
|
+
includeFiles: true,
|
|
116
|
+
includeDirectories: includeDirectories,
|
|
117
|
+
depth: maxDepth,
|
|
118
|
+
limit: maxResults,
|
|
119
|
+
});
|
|
120
|
+
return response.files;
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
private async getDefaultCompletions(match: {
|
|
124
|
+
from: number;
|
|
125
|
+
}): Promise<CompletionResult | null> {
|
|
126
|
+
try {
|
|
127
|
+
// Show common file types when no specific query is given
|
|
128
|
+
// Use broad searches for common file types
|
|
129
|
+
const searches = ["py", "md", "csv"];
|
|
130
|
+
|
|
131
|
+
// Try the first search that returns results
|
|
132
|
+
for (const search of searches) {
|
|
133
|
+
try {
|
|
134
|
+
const files = await this.searchFiles(search, {
|
|
135
|
+
maxDepth: 1,
|
|
136
|
+
maxResults: 5,
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
if (files && files.length > 0) {
|
|
140
|
+
const completions = files.map((file) => {
|
|
141
|
+
const item: FileContextItem = {
|
|
142
|
+
uri: this.asURI(file.path),
|
|
143
|
+
name: file.name,
|
|
144
|
+
type: this.contextType,
|
|
145
|
+
description: file.isDirectory ? "Directory" : "File",
|
|
146
|
+
data: {
|
|
147
|
+
path: file.path,
|
|
148
|
+
isDirectory: file.isDirectory,
|
|
149
|
+
},
|
|
150
|
+
};
|
|
151
|
+
return this.formatCompletion(item);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
return {
|
|
155
|
+
from: match.from,
|
|
156
|
+
options: completions,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
} catch (error) {
|
|
160
|
+
Logger.error("Failed to get default file completions:", error);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// If no searches return results, return empty
|
|
165
|
+
return {
|
|
166
|
+
from: match.from,
|
|
167
|
+
options: [],
|
|
168
|
+
};
|
|
169
|
+
} catch (error) {
|
|
170
|
+
Logger.error("Failed to get default file completions:", error);
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
getItems(): FileContextItem[] {
|
|
176
|
+
// Files are fetched dynamically, so return empty array
|
|
177
|
+
// This provider relies on dynamic fetching via createCompletionSource()
|
|
178
|
+
return [];
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
formatCompletion(item: FileContextItem): Completion {
|
|
182
|
+
const { data, name } = item;
|
|
183
|
+
const icon = data.isDirectory ? "📁" : "📄";
|
|
184
|
+
|
|
185
|
+
return {
|
|
186
|
+
...this.createBasicCompletion(item),
|
|
187
|
+
type: "file",
|
|
188
|
+
section: "File",
|
|
189
|
+
boost: data.isDirectory ? Boosts.MEDIUM : Boosts.LOW,
|
|
190
|
+
detail: data.path,
|
|
191
|
+
displayLabel: `${icon} ${name}`,
|
|
192
|
+
apply: async (view, completion, from, to) => {
|
|
193
|
+
// First try to add the file as an attachment, if the callback is provided
|
|
194
|
+
// otherwise add it to the prompt
|
|
195
|
+
const addAttachment = view.state.facet(contextCallbacks)?.addAttachment;
|
|
196
|
+
if (!addAttachment) {
|
|
197
|
+
Logger.warn("No addAttachment callback provided");
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const fileDetails = await this.apiRequests
|
|
202
|
+
.sendFileDetails({ path: data.path })
|
|
203
|
+
.catch((error) => {
|
|
204
|
+
toast({
|
|
205
|
+
title: "Failed to get file details",
|
|
206
|
+
description: error.message,
|
|
207
|
+
});
|
|
208
|
+
return null;
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
if (!fileDetails) {
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const mimeType = fileDetails.mimeType || "text/plain";
|
|
216
|
+
|
|
217
|
+
// Handle binary vs text files
|
|
218
|
+
let blob: Blob;
|
|
219
|
+
if (
|
|
220
|
+
mimeType.startsWith("text/") ||
|
|
221
|
+
mimeType.includes("json") ||
|
|
222
|
+
mimeType.includes("xml")
|
|
223
|
+
) {
|
|
224
|
+
// Text files - create blob directly from contents
|
|
225
|
+
blob = new Blob([fileDetails.contents || ""], { type: mimeType });
|
|
226
|
+
} else {
|
|
227
|
+
// Binary files - use blob utility to decode base64
|
|
228
|
+
if (fileDetails.contents) {
|
|
229
|
+
try {
|
|
230
|
+
// Create data URL using utility and deserialize blob
|
|
231
|
+
const dataURL = base64ToDataURL(
|
|
232
|
+
fileDetails.contents as Base64String,
|
|
233
|
+
mimeType,
|
|
234
|
+
);
|
|
235
|
+
blob = await deserializeBlob(dataURL);
|
|
236
|
+
} catch {
|
|
237
|
+
// Fallback to treating as text
|
|
238
|
+
blob = new Blob([fileDetails.contents], { type: mimeType });
|
|
239
|
+
}
|
|
240
|
+
} else {
|
|
241
|
+
blob = new Blob([""], { type: mimeType });
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const file = new File([blob], name, { type: mimeType });
|
|
246
|
+
addAttachment(file);
|
|
247
|
+
|
|
248
|
+
// Close completion and delete the entire mention text (from # to cursor)
|
|
249
|
+
view.dispatch({
|
|
250
|
+
changes: { from, to, insert: "" },
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
closeCompletion(view);
|
|
254
|
+
},
|
|
255
|
+
info: () => {
|
|
256
|
+
const element = document.createElement("div");
|
|
257
|
+
element.classList.add("flex", "flex-col", "gap-1", "p-2");
|
|
258
|
+
|
|
259
|
+
const title = document.createElement("div");
|
|
260
|
+
title.classList.add("font-bold");
|
|
261
|
+
title.textContent = name;
|
|
262
|
+
element.append(title);
|
|
263
|
+
|
|
264
|
+
const path = document.createElement("div");
|
|
265
|
+
path.classList.add("text-xs", "text-muted-foreground");
|
|
266
|
+
path.textContent = data.path;
|
|
267
|
+
element.append(path);
|
|
268
|
+
return element;
|
|
269
|
+
},
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
formatContext(item: FileContextItem): string {
|
|
274
|
+
const { data, name } = item;
|
|
275
|
+
return contextToXml({
|
|
276
|
+
type: this.contextType,
|
|
277
|
+
data: {
|
|
278
|
+
name: name,
|
|
279
|
+
path: data.path,
|
|
280
|
+
isDirectory: data.isDirectory,
|
|
281
|
+
},
|
|
282
|
+
details: data.isDirectory
|
|
283
|
+
? "Directory containing files and subdirectories"
|
|
284
|
+
: "File",
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
}
|