@oceanum/oceanumlab 4.6.0
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/LICENSE +29 -0
- package/README.md +116 -0
- package/lib/DatameshUI.d.ts +5 -0
- package/lib/DatameshUI.js +8 -0
- package/lib/DatameshUI.js.map +1 -0
- package/lib/DatameshWidget.d.ts +59 -0
- package/lib/DatameshWidget.js +573 -0
- package/lib/DatameshWidget.js.map +1 -0
- package/lib/DatasourceItem.d.ts +12 -0
- package/lib/DatasourceItem.js +47 -0
- package/lib/DatasourceItem.js.map +1 -0
- package/lib/aiLoop.d.ts +60 -0
- package/lib/aiLoop.js +85 -0
- package/lib/aiLoop.js.map +1 -0
- package/lib/chatRouter.d.ts +64 -0
- package/lib/chatRouter.js +225 -0
- package/lib/chatRouter.js.map +1 -0
- package/lib/constants.d.ts +14 -0
- package/lib/constants.js +15 -0
- package/lib/constants.js.map +1 -0
- package/lib/conversationPin.d.ts +68 -0
- package/lib/conversationPin.js +120 -0
- package/lib/conversationPin.js.map +1 -0
- package/lib/handler.d.ts +8 -0
- package/lib/handler.js +36 -0
- package/lib/handler.js.map +1 -0
- package/lib/index.d.ts +16 -0
- package/lib/index.js +273 -0
- package/lib/index.js.map +1 -0
- package/lib/kernelHandoff.d.ts +61 -0
- package/lib/kernelHandoff.js +236 -0
- package/lib/kernelHandoff.js.map +1 -0
- package/lib/notebookContext.d.ts +48 -0
- package/lib/notebookContext.js +118 -0
- package/lib/notebookContext.js.map +1 -0
- package/lib/notebookHost.d.ts +6 -0
- package/lib/notebookHost.js +72 -0
- package/lib/notebookHost.js.map +1 -0
- package/lib/notebookRun.d.ts +37 -0
- package/lib/notebookRun.js +70 -0
- package/lib/notebookRun.js.map +1 -0
- package/lib/progress.d.ts +43 -0
- package/lib/progress.js +103 -0
- package/lib/progress.js.map +1 -0
- package/lib/sse.d.ts +26 -0
- package/lib/sse.js +112 -0
- package/lib/sse.js.map +1 -0
- package/package.json +151 -0
- package/schema/datamesh-connect.json +35 -0
- package/src/DatameshUI.tsx +15 -0
- package/src/DatameshWidget.tsx +909 -0
- package/src/DatasourceItem.tsx +124 -0
- package/src/__tests__/aiLoop.spec.ts +304 -0
- package/src/__tests__/chatRouter.spec.ts +315 -0
- package/src/__tests__/conversationPin.spec.ts +236 -0
- package/src/__tests__/kernelHandoff.spec.ts +106 -0
- package/src/__tests__/notebookContext.spec.ts +97 -0
- package/src/__tests__/notebookHost.spec.ts +195 -0
- package/src/__tests__/notebookRun.spec.ts +95 -0
- package/src/__tests__/notebookSnapshot.spec.ts +62 -0
- package/src/__tests__/oceanumlab.spec.ts +9 -0
- package/src/__tests__/sse.spec.ts +304 -0
- package/src/aiLoop.ts +140 -0
- package/src/chatRouter.ts +329 -0
- package/src/constants.ts +16 -0
- package/src/conversationPin.ts +148 -0
- package/src/global.d.ts +5 -0
- package/src/handler.ts +46 -0
- package/src/index.ts +327 -0
- package/src/kernelHandoff.ts +284 -0
- package/src/notebookContext.ts +144 -0
- package/src/notebookHost.ts +97 -0
- package/src/notebookRun.ts +104 -0
- package/src/progress.ts +121 -0
- package/src/sse.ts +127 -0
- package/src/svg.d.ts +4 -0
- package/style/base.css +5 -0
- package/style/icons/oceanum-dark.svg +1 -0
- package/style/icons/oceanum-light.svg +1 -0
- package/style/icons/oceanum.png +0 -0
- package/style/icons/oceanum.svg +1 -0
- package/style/index.css +477 -0
- package/style/index.js +1 -0
|
@@ -0,0 +1,909 @@
|
|
|
1
|
+
import { JupyterFrontEnd } from '@jupyterlab/application';
|
|
2
|
+
import {
|
|
3
|
+
Dialog,
|
|
4
|
+
ReactWidget,
|
|
5
|
+
UseSignal,
|
|
6
|
+
showDialog
|
|
7
|
+
} from '@jupyterlab/apputils';
|
|
8
|
+
import { CodeCell } from '@jupyterlab/cells';
|
|
9
|
+
import { CodeEditor } from '@jupyterlab/codeeditor';
|
|
10
|
+
import * as nbformat from '@jupyterlab/nbformat';
|
|
11
|
+
import { Notebook, NotebookPanel } from '@jupyterlab/notebook';
|
|
12
|
+
import { CodeCellModel } from '@jupyterlab/cells';
|
|
13
|
+
import { LabIcon, addIcon } from '@jupyterlab/ui-components';
|
|
14
|
+
import { CommandRegistry } from '@lumino/commands';
|
|
15
|
+
import { MimeData, ReadonlyJSONArray } from '@lumino/coreutils';
|
|
16
|
+
import { Drag } from '@lumino/dragdrop';
|
|
17
|
+
import { Signal } from '@lumino/signaling';
|
|
18
|
+
import { Widget } from '@lumino/widgets';
|
|
19
|
+
|
|
20
|
+
import React from 'react';
|
|
21
|
+
import { marked } from 'marked';
|
|
22
|
+
|
|
23
|
+
import { DatasourceItem } from './DatasourceItem';
|
|
24
|
+
import { OCEANUM_AI_BACKEND_URL } from './constants';
|
|
25
|
+
import { describeProgress, onProgress, type Progress } from './progress';
|
|
26
|
+
|
|
27
|
+
// Configure marked for safe rendering
|
|
28
|
+
marked.setOptions({
|
|
29
|
+
breaks: true,
|
|
30
|
+
gfm: true
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Renders markdown content safely using dangerouslySetInnerHTML.
|
|
35
|
+
*/
|
|
36
|
+
function MarkdownContent({ content }: { content: string }): React.ReactElement {
|
|
37
|
+
const html = React.useMemo(() => {
|
|
38
|
+
try {
|
|
39
|
+
return marked.parse(content) as string;
|
|
40
|
+
} catch {
|
|
41
|
+
return content;
|
|
42
|
+
}
|
|
43
|
+
}, [content]);
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<div
|
|
47
|
+
className="oceanum-ai-chat-content oceanum-ai-markdown"
|
|
48
|
+
dangerouslySetInnerHTML={{ __html: html }}
|
|
49
|
+
/>
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const JUPYTER_CELL_MIME = 'application/vnd.jupyter.cells';
|
|
54
|
+
|
|
55
|
+
const DRAG_IMAGE = new Image();
|
|
56
|
+
DRAG_IMAGE.src =
|
|
57
|
+
'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIzMiIgaGVpZ2h0PSIzMiIgZmlsbD0iIzAwMDAwMCIgdmlld0JveD0iMCAwIDI1NiAyNTYiPjxwYXRoIGQ9Ik02OS4xMiw5NC4xNSwyOC41LDEyOGw0MC42MiwzMy44NWE4LDgsMCwxLDEtMTAuMjQsMTIuMjlsLTQ4LTQwYTgsOCwwLDAsMSwwLTEyLjI5bDQ4LTQwYTgsOCwwLDAsMSwxMC4yNCwxMi4zWm0xNzYsMjcuNy00OC00MGE4LDgsMCwxLDAtMTAuMjQsMTIuM0wyMjcuNSwxMjhsLTQwLjYyLDMzLjg1YTgsOCwwLDEsMCwxMC4yNCwxMi4yOWw0OC00MGE4LDgsMCwwLDAsMC0xMi4yOVpNMTYyLjczLDMyLjQ4YTgsOCwwLDAsMC0xMC4yNSw0Ljc5bC02NCwxNzZhOCw4LDAsMCwwLDQuNzksMTAuMjZBOC4xNCw4LjE0LDAsMCwwLDk2LDIyNGE4LDgsMCwwLDAsNy41Mi01LjI3bDY0LTE3NkE4LDgsMCwwLDAsMTYyLjczLDMyLjQ4WiI+PC9wYXRoPjwvc3ZnPg==';
|
|
58
|
+
|
|
59
|
+
const datameshToken = (notebook: Notebook): string => {
|
|
60
|
+
const datameshTokenInjected =
|
|
61
|
+
notebook &&
|
|
62
|
+
notebook.widgets.find(cell => {
|
|
63
|
+
if (cell.editor) {
|
|
64
|
+
const line = cell.editor.getLine(0);
|
|
65
|
+
if (line && line.indexOf('DATAMESH_TOKEN=') >= 0) {
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
if (!datameshTokenInjected) {
|
|
71
|
+
return `DATAMESH_TOKEN='${window.datameshToken}';`;
|
|
72
|
+
} else {
|
|
73
|
+
return '';
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const datasourceCode = (
|
|
78
|
+
datasource: IDatasource,
|
|
79
|
+
notebook: Notebook,
|
|
80
|
+
icell: number
|
|
81
|
+
): string => {
|
|
82
|
+
const datameshImport =
|
|
83
|
+
notebook &&
|
|
84
|
+
notebook.widgets.find(cell => {
|
|
85
|
+
if (cell.editor) {
|
|
86
|
+
let found = false;
|
|
87
|
+
for (let i = 0; i < icell; i++) {
|
|
88
|
+
const line = cell.editor.getLine(i);
|
|
89
|
+
if (
|
|
90
|
+
line &&
|
|
91
|
+
line.indexOf('from oceanum.datamesh import Connector') >= 0
|
|
92
|
+
) {
|
|
93
|
+
found = true;
|
|
94
|
+
break;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return found;
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
let datasourceStr = (datasource.label || datasource.datasource).replace(
|
|
101
|
+
/[\s-.]/g,
|
|
102
|
+
'_'
|
|
103
|
+
);
|
|
104
|
+
const tokenString = window.injectToken ? 'token=DATAMESH_TOKEN' : '';
|
|
105
|
+
if (
|
|
106
|
+
datasource.variables ||
|
|
107
|
+
datasource.geofilter ||
|
|
108
|
+
datasource.timefilter ||
|
|
109
|
+
datasource.spatialref
|
|
110
|
+
) {
|
|
111
|
+
datasourceStr += `=datamesh.query(${JSON.stringify(
|
|
112
|
+
datasource,
|
|
113
|
+
null,
|
|
114
|
+
' '
|
|
115
|
+
).replace(/null/g, 'None')})`;
|
|
116
|
+
} else {
|
|
117
|
+
datasourceStr += `=datamesh.load_datasource('${datasource.datasource}')`;
|
|
118
|
+
}
|
|
119
|
+
if (!datameshImport) {
|
|
120
|
+
datasourceStr =
|
|
121
|
+
'from oceanum.datamesh import Connector' +
|
|
122
|
+
'\n' +
|
|
123
|
+
'#Put your datamesh token in the Jupyterlab settings, or as argument in the constructor below' +
|
|
124
|
+
'\n' +
|
|
125
|
+
`datamesh=Connector(${tokenString})` +
|
|
126
|
+
'\n' +
|
|
127
|
+
datasourceStr;
|
|
128
|
+
}
|
|
129
|
+
return datasourceStr;
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
export interface IDatasource {
|
|
133
|
+
id: string;
|
|
134
|
+
label: string;
|
|
135
|
+
datasource: string;
|
|
136
|
+
description: string;
|
|
137
|
+
variables?: string[];
|
|
138
|
+
geofilter?: Record<string, any>;
|
|
139
|
+
timefilter: any;
|
|
140
|
+
spatialref: string;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export interface IWorkspaceSpec {
|
|
144
|
+
id: string;
|
|
145
|
+
name: string;
|
|
146
|
+
data: IDatasource[];
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export interface IDatameshWorkspaceProps {
|
|
150
|
+
spec: IWorkspaceSpec;
|
|
151
|
+
openDatameshUI: (args: any) => void;
|
|
152
|
+
getCurrentWidget: () => Widget;
|
|
153
|
+
shell: JupyterFrontEnd.IShell;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export interface IDatasourceActionButton {
|
|
157
|
+
title: string;
|
|
158
|
+
icon: LabIcon;
|
|
159
|
+
feedback?: string;
|
|
160
|
+
onClick: () => void;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
class DatameshWorkspaceDisplay extends React.Component<IDatameshWorkspaceProps> {
|
|
164
|
+
constructor(props: IDatameshWorkspaceProps) {
|
|
165
|
+
super(props);
|
|
166
|
+
this._drag = null;
|
|
167
|
+
this._dragData = null;
|
|
168
|
+
this.handleDragMove = this.handleDragMove.bind(this);
|
|
169
|
+
this._evtMouseUp = this._evtMouseUp.bind(this);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
private _drag: Drag | null;
|
|
173
|
+
private _dragData: {
|
|
174
|
+
pressX: number;
|
|
175
|
+
pressY: number;
|
|
176
|
+
dragImage: HTMLElement;
|
|
177
|
+
} | null;
|
|
178
|
+
|
|
179
|
+
render(): React.ReactElement {
|
|
180
|
+
return (
|
|
181
|
+
<div className={'datamesh-workspace-display'}>
|
|
182
|
+
{this.props.spec && (
|
|
183
|
+
<div>
|
|
184
|
+
<span className="datamesh-workspace-name">
|
|
185
|
+
{this.props.spec.name}
|
|
186
|
+
</span>
|
|
187
|
+
<hr></hr>
|
|
188
|
+
{this.props.spec.data.map(datasource => (
|
|
189
|
+
// Render display of a code datasource
|
|
190
|
+
<div
|
|
191
|
+
key={datasource.id}
|
|
192
|
+
data-item-id={datasource.id}
|
|
193
|
+
className={'datasource-item'}
|
|
194
|
+
>
|
|
195
|
+
<DatasourceItem
|
|
196
|
+
datasource={datasource}
|
|
197
|
+
insertDatasource={this.insertDatameshConnect}
|
|
198
|
+
onMouseDown={(event: any): void => {
|
|
199
|
+
this.handleDragSnippet(event, datasource);
|
|
200
|
+
}}
|
|
201
|
+
/>
|
|
202
|
+
</div>
|
|
203
|
+
))}
|
|
204
|
+
</div>
|
|
205
|
+
)}
|
|
206
|
+
</div>
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
private injectToken = (notebookWidget: NotebookPanel): void => {
|
|
211
|
+
const notebookContent = notebookWidget.content as Notebook;
|
|
212
|
+
const datameshTokenInject = datameshToken(notebookContent);
|
|
213
|
+
//const codeCell=new CodeCellModel({});
|
|
214
|
+
if (datameshTokenInject) {
|
|
215
|
+
// const tokenCell = notebookContent.contentFactory.createCodeCell({
|
|
216
|
+
// model: codeCell,
|
|
217
|
+
// rendermime: notebookContent.rendermime,
|
|
218
|
+
// contentFactory: notebookContent.contentFactory,
|
|
219
|
+
// })
|
|
220
|
+
// tokenCell.model.sharedModel.setSource(datameshTokenInject);
|
|
221
|
+
notebookContent.model?.sharedModel.insertCell(0, {
|
|
222
|
+
cell_type: 'code',
|
|
223
|
+
source: datameshTokenInject,
|
|
224
|
+
metadata: {}
|
|
225
|
+
});
|
|
226
|
+
// CodeCell.execute(
|
|
227
|
+
// notebookContent.widgets[0] as CodeCell,
|
|
228
|
+
// notebookWidget.sessionContext
|
|
229
|
+
// );
|
|
230
|
+
}
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
// Handle code datasource insert into an editor
|
|
234
|
+
private insertDatameshConnect = async (
|
|
235
|
+
datasource: IDatasource
|
|
236
|
+
): Promise<void> => {
|
|
237
|
+
const widget: Widget = this.props.getCurrentWidget();
|
|
238
|
+
if (widget instanceof NotebookPanel) {
|
|
239
|
+
const notebookWidget = widget as NotebookPanel;
|
|
240
|
+
const notebookContent = notebookWidget.content as Notebook;
|
|
241
|
+
const notebookCell = notebookContent.activeCell;
|
|
242
|
+
const notebookCellIndex = notebookContent.activeCellIndex;
|
|
243
|
+
const notebookCellEditor = notebookCell.editor;
|
|
244
|
+
|
|
245
|
+
const datasourceStr = datasourceCode(
|
|
246
|
+
datasource,
|
|
247
|
+
notebookContent,
|
|
248
|
+
notebookCellIndex
|
|
249
|
+
);
|
|
250
|
+
if (notebookCellEditor) {
|
|
251
|
+
if (notebookCell instanceof CodeCell) {
|
|
252
|
+
this.verifyLanguageAndInsert(
|
|
253
|
+
datasourceStr,
|
|
254
|
+
'python',
|
|
255
|
+
notebookCellEditor
|
|
256
|
+
);
|
|
257
|
+
} else {
|
|
258
|
+
notebookCellEditor.replaceSelection(datasourceStr);
|
|
259
|
+
}
|
|
260
|
+
if (window.injectToken) {
|
|
261
|
+
this.injectToken(notebookWidget);
|
|
262
|
+
}
|
|
263
|
+
} else {
|
|
264
|
+
this.showErrDialog(
|
|
265
|
+
'Datamesh datasource insert failed: Please select code cell'
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
// Handle language compatibility between code datasource and editor
|
|
272
|
+
private verifyLanguageAndInsert = async (
|
|
273
|
+
datasourceStr: string,
|
|
274
|
+
editorLanguage: string,
|
|
275
|
+
editor: CodeEditor.IEditor
|
|
276
|
+
): Promise<void> => {
|
|
277
|
+
if (editor && editorLanguage && 'python' !== editorLanguage.toLowerCase()) {
|
|
278
|
+
const result = await this.showWarnDialog(editorLanguage);
|
|
279
|
+
if (result.button.accept) {
|
|
280
|
+
editor.replaceSelection(datasourceStr);
|
|
281
|
+
}
|
|
282
|
+
} else {
|
|
283
|
+
// Language match or editorLanguage is unavailable
|
|
284
|
+
editor?.replaceSelection(datasourceStr);
|
|
285
|
+
}
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
// Display warning dialog when inserting a code datasource incompatible with editor's language
|
|
289
|
+
private showWarnDialog = async (
|
|
290
|
+
editorLanguage: string
|
|
291
|
+
): Promise<Dialog.IResult<string>> => {
|
|
292
|
+
return showDialog({
|
|
293
|
+
title: 'Warning',
|
|
294
|
+
body: `Datamesh connector is incompatible with ${editorLanguage}. Continue?`,
|
|
295
|
+
buttons: [Dialog.cancelButton(), Dialog.okButton()]
|
|
296
|
+
});
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
// Display error dialog when inserting a code datasource into unsupported widget (i.e. not an editor)
|
|
300
|
+
private showErrDialog = (errMsg: string): Promise<Dialog.IResult<string>> => {
|
|
301
|
+
return showDialog({
|
|
302
|
+
title: 'Error',
|
|
303
|
+
body: errMsg,
|
|
304
|
+
buttons: [Dialog.okButton()]
|
|
305
|
+
});
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
// Initial setup to handle dragging a code datasource
|
|
309
|
+
private handleDragSnippet(
|
|
310
|
+
event: React.MouseEvent<HTMLDivElement, MouseEvent>,
|
|
311
|
+
datasource: IDatasource
|
|
312
|
+
): void {
|
|
313
|
+
const { button } = event;
|
|
314
|
+
|
|
315
|
+
// do nothing if left mouse button is clicked
|
|
316
|
+
if (button !== 0) {
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
this._dragData = {
|
|
321
|
+
pressX: event.clientX,
|
|
322
|
+
pressY: event.clientY,
|
|
323
|
+
dragImage: DRAG_IMAGE
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
const mouseUpListener = (event: MouseEvent): void => {
|
|
327
|
+
this._evtMouseUp(event, datasource, mouseMoveListener);
|
|
328
|
+
};
|
|
329
|
+
const mouseMoveListener = (event: MouseEvent): void => {
|
|
330
|
+
this.handleDragMove(
|
|
331
|
+
event,
|
|
332
|
+
datasource,
|
|
333
|
+
mouseMoveListener,
|
|
334
|
+
mouseUpListener
|
|
335
|
+
);
|
|
336
|
+
};
|
|
337
|
+
|
|
338
|
+
const target = event.target as HTMLElement;
|
|
339
|
+
target.addEventListener('mouseup', mouseUpListener, {
|
|
340
|
+
once: true,
|
|
341
|
+
capture: true
|
|
342
|
+
});
|
|
343
|
+
target.addEventListener('mousemove', mouseMoveListener, true);
|
|
344
|
+
|
|
345
|
+
// since a browser has its own drag'n'drop support for images and some other elements.
|
|
346
|
+
target.ondragstart = (): boolean => false;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
private _evtMouseUp(
|
|
350
|
+
event: MouseEvent,
|
|
351
|
+
datasource: IDatasource,
|
|
352
|
+
mouseMoveListener: (event: MouseEvent) => void
|
|
353
|
+
): void {
|
|
354
|
+
event.preventDefault();
|
|
355
|
+
event.stopPropagation();
|
|
356
|
+
|
|
357
|
+
const target = event.target as HTMLElement;
|
|
358
|
+
target.removeEventListener('mousemove', mouseMoveListener, true);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
private handleDragMove(
|
|
362
|
+
event: MouseEvent,
|
|
363
|
+
datasource: IDatasource,
|
|
364
|
+
mouseMoveListener: (event: MouseEvent) => void,
|
|
365
|
+
mouseUpListener: (event: MouseEvent) => void
|
|
366
|
+
): void {
|
|
367
|
+
event.preventDefault();
|
|
368
|
+
event.stopPropagation();
|
|
369
|
+
|
|
370
|
+
const data = this._dragData;
|
|
371
|
+
|
|
372
|
+
if (
|
|
373
|
+
data &&
|
|
374
|
+
this.shouldStartDrag(
|
|
375
|
+
data.pressX,
|
|
376
|
+
data.pressY,
|
|
377
|
+
event.clientX,
|
|
378
|
+
event.clientY
|
|
379
|
+
)
|
|
380
|
+
) {
|
|
381
|
+
// Create drag image
|
|
382
|
+
const element = document.createElement('div');
|
|
383
|
+
element.innerHTML = datasource.description;
|
|
384
|
+
element.classList.add('datasource-drag-image');
|
|
385
|
+
data.dragImage = element;
|
|
386
|
+
|
|
387
|
+
// Remove mouse listeners and start the drag.
|
|
388
|
+
const target = event.target as HTMLElement;
|
|
389
|
+
target.removeEventListener('mousemove', mouseMoveListener, true);
|
|
390
|
+
target.removeEventListener('mouseup', mouseUpListener, true);
|
|
391
|
+
|
|
392
|
+
void this.startDrag(
|
|
393
|
+
data.dragImage,
|
|
394
|
+
datasource,
|
|
395
|
+
event.clientX,
|
|
396
|
+
event.clientY
|
|
397
|
+
);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* Detect if a drag event should be started. This is down if the
|
|
403
|
+
* mouse is moved beyond a certain distance (DRAG_THRESHOLD).
|
|
404
|
+
*
|
|
405
|
+
* @param prevX - X Coordinate of the mouse pointer during the mousedown event
|
|
406
|
+
* @param prevY - Y Coordinate of the mouse pointer during the mousedown event
|
|
407
|
+
* @param nextX - Current X Coordinate of the mouse pointer
|
|
408
|
+
* @param nextY - Current Y Coordinate of the mouse pointer
|
|
409
|
+
*/
|
|
410
|
+
private shouldStartDrag(
|
|
411
|
+
prevX: number,
|
|
412
|
+
prevY: number,
|
|
413
|
+
nextX: number,
|
|
414
|
+
nextY: number
|
|
415
|
+
): boolean {
|
|
416
|
+
const dx = Math.abs(nextX - prevX);
|
|
417
|
+
const dy = Math.abs(nextY - prevY);
|
|
418
|
+
return dx >= 0 || dy >= 5;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
private async startDrag(
|
|
422
|
+
dragImage: HTMLElement,
|
|
423
|
+
datasource: IDatasource,
|
|
424
|
+
clientX: number,
|
|
425
|
+
clientY: number
|
|
426
|
+
): Promise<void> {
|
|
427
|
+
const notebookPanel: NotebookPanel =
|
|
428
|
+
this.props.getCurrentWidget() as NotebookPanel;
|
|
429
|
+
const notebookContent = notebookPanel.content as Notebook;
|
|
430
|
+
const codeCell = new CodeCellModel({});
|
|
431
|
+
const cell = notebookContent.contentFactory.createCodeCell({
|
|
432
|
+
model: codeCell,
|
|
433
|
+
rendermime: notebookContent.rendermime,
|
|
434
|
+
contentFactory: notebookContent.contentFactory
|
|
435
|
+
});
|
|
436
|
+
let content = datasourceCode(datasource, notebookContent, 0);
|
|
437
|
+
if (window.injectToken) {
|
|
438
|
+
const notebookPanel = this.props.getCurrentWidget() as NotebookPanel;
|
|
439
|
+
content = datameshToken(notebookPanel.content) + '\n' + content;
|
|
440
|
+
}
|
|
441
|
+
cell.model.sharedModel.setSource(content);
|
|
442
|
+
|
|
443
|
+
this._drag = new Drag({
|
|
444
|
+
mimeData: new MimeData(),
|
|
445
|
+
dragImage: dragImage,
|
|
446
|
+
supportedActions: 'copy-move',
|
|
447
|
+
proposedAction: 'copy',
|
|
448
|
+
source: this
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
const selected: nbformat.ICell[] = [cell.model.toJSON()];
|
|
452
|
+
this._drag.mimeData.setData(JUPYTER_CELL_MIME, selected);
|
|
453
|
+
this._drag.mimeData.setData('text/plain', datasource.description);
|
|
454
|
+
|
|
455
|
+
return this._drag.start(clientX, clientY).then(() => {
|
|
456
|
+
this._drag = null;
|
|
457
|
+
this._dragData = null;
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/**
|
|
463
|
+
* Shows a message prompting user to configure their Datamesh token.
|
|
464
|
+
* Only renders when no token is set.
|
|
465
|
+
*/
|
|
466
|
+
function TokenConfigMessage({
|
|
467
|
+
commands
|
|
468
|
+
}: {
|
|
469
|
+
commands: CommandRegistry;
|
|
470
|
+
}): React.ReactElement | null {
|
|
471
|
+
const [hasToken, setHasToken] = React.useState(!!window.datameshToken);
|
|
472
|
+
|
|
473
|
+
React.useEffect(() => {
|
|
474
|
+
const checkToken = () => setHasToken(!!window.datameshToken);
|
|
475
|
+
const interval = setInterval(checkToken, 1000);
|
|
476
|
+
return () => clearInterval(interval);
|
|
477
|
+
}, []);
|
|
478
|
+
|
|
479
|
+
if (hasToken) {
|
|
480
|
+
return null;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
return (
|
|
484
|
+
<div className="oceanum-token-config">
|
|
485
|
+
Set your{' '}
|
|
486
|
+
<a
|
|
487
|
+
onClick={() =>
|
|
488
|
+
commands.execute('settingeditor:open', { query: 'Oceanum' })
|
|
489
|
+
}
|
|
490
|
+
>
|
|
491
|
+
Datamesh token
|
|
492
|
+
</a>{' '}
|
|
493
|
+
to enable Oceanum.oi services{' '}
|
|
494
|
+
<a
|
|
495
|
+
href="https://home.oceanum.io/account"
|
|
496
|
+
target="_blank"
|
|
497
|
+
rel="noopener noreferrer"
|
|
498
|
+
>
|
|
499
|
+
Get token here
|
|
500
|
+
</a>
|
|
501
|
+
</div>
|
|
502
|
+
);
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
interface IChatMessage {
|
|
506
|
+
role: 'user' | 'assistant';
|
|
507
|
+
content: string;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
interface IAIChatPanelProps {
|
|
511
|
+
commands: CommandRegistry;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
function AIChatPanel({
|
|
515
|
+
commands
|
|
516
|
+
}: IAIChatPanelProps): React.ReactElement | null {
|
|
517
|
+
const [messages, setMessages] = React.useState<IChatMessage[]>([]);
|
|
518
|
+
const [input, setInput] = React.useState('');
|
|
519
|
+
const [loading, setLoading] = React.useState(false);
|
|
520
|
+
const [progress, setProgress] = React.useState<Progress | null>(null);
|
|
521
|
+
|
|
522
|
+
// What the agent is doing, as the backend reports it. A `/api/chat` call
|
|
523
|
+
// takes around fifty seconds and used to spend all of them showing the same
|
|
524
|
+
// three words, which read as a hang rather than as work.
|
|
525
|
+
React.useEffect(() => onProgress(setProgress), []);
|
|
526
|
+
const [error, setError] = React.useState<string | null>(null);
|
|
527
|
+
const messagesEndRef = React.useRef<HTMLDivElement>(null);
|
|
528
|
+
|
|
529
|
+
// Chat history navigation
|
|
530
|
+
const [history, setHistory] = React.useState<string[]>([]);
|
|
531
|
+
const [historyIndex, setHistoryIndex] = React.useState(-1);
|
|
532
|
+
const [tempInput, setTempInput] = React.useState('');
|
|
533
|
+
|
|
534
|
+
// Track the actual token value to detect changes
|
|
535
|
+
const [token, setToken] = React.useState(window.datameshToken || '');
|
|
536
|
+
const hasToken = !!token;
|
|
537
|
+
|
|
538
|
+
// Check if code capability is enabled (null = loading/unknown)
|
|
539
|
+
const [codeEnabled, setCodeEnabled] = React.useState<boolean | null>(null);
|
|
540
|
+
|
|
541
|
+
// Re-check token periodically (settings may change)
|
|
542
|
+
React.useEffect(() => {
|
|
543
|
+
const checkToken = () => {
|
|
544
|
+
const currentToken = window.datameshToken || '';
|
|
545
|
+
if (currentToken !== token) {
|
|
546
|
+
setToken(currentToken);
|
|
547
|
+
}
|
|
548
|
+
};
|
|
549
|
+
const interval = setInterval(checkToken, 1000);
|
|
550
|
+
return () => clearInterval(interval);
|
|
551
|
+
}, [token]);
|
|
552
|
+
|
|
553
|
+
// Fetch capabilities when token changes
|
|
554
|
+
React.useEffect(() => {
|
|
555
|
+
if (!token) {
|
|
556
|
+
setCodeEnabled(null);
|
|
557
|
+
return;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
const fetchCapabilities = async () => {
|
|
561
|
+
try {
|
|
562
|
+
const response = await fetch(
|
|
563
|
+
`${OCEANUM_AI_BACKEND_URL}/api/capabilities`,
|
|
564
|
+
{
|
|
565
|
+
headers: {
|
|
566
|
+
'X-Datamesh-Token': token
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
);
|
|
570
|
+
if (response.ok) {
|
|
571
|
+
const data = await response.json();
|
|
572
|
+
setCodeEnabled(data.code === true);
|
|
573
|
+
} else {
|
|
574
|
+
setCodeEnabled(false);
|
|
575
|
+
}
|
|
576
|
+
} catch {
|
|
577
|
+
setCodeEnabled(false);
|
|
578
|
+
}
|
|
579
|
+
};
|
|
580
|
+
|
|
581
|
+
void fetchCapabilities();
|
|
582
|
+
}, [token]);
|
|
583
|
+
|
|
584
|
+
// Auto-scroll to bottom when messages change
|
|
585
|
+
React.useEffect(() => {
|
|
586
|
+
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
|
587
|
+
}, [messages, loading]);
|
|
588
|
+
|
|
589
|
+
// The notebook this conversation is pinned to: undefined until it starts,
|
|
590
|
+
// null when it has none. See 'oceanum-ai:chat-context' in index.ts.
|
|
591
|
+
const [context, setContext] = React.useState<string | null | undefined>(
|
|
592
|
+
undefined
|
|
593
|
+
);
|
|
594
|
+
// Bumped by New chat. A request keeps the value it started with, and drops
|
|
595
|
+
// its result once the value has moved on: the run New chat stopped resolves
|
|
596
|
+
// AFTER the reset -- with "Stopped." or a late answer -- and must neither
|
|
597
|
+
// land in the new conversation nor end `loading` under a newer request.
|
|
598
|
+
const conversation = React.useRef(0);
|
|
599
|
+
|
|
600
|
+
const handleSubmit = async (): Promise<void> => {
|
|
601
|
+
const prompt = input.trim();
|
|
602
|
+
if (!prompt || loading) {
|
|
603
|
+
return;
|
|
604
|
+
}
|
|
605
|
+
const mine = conversation.current;
|
|
606
|
+
|
|
607
|
+
// Add to history
|
|
608
|
+
setHistory(prev => [...prev, prompt]);
|
|
609
|
+
setHistoryIndex(-1);
|
|
610
|
+
setTempInput('');
|
|
611
|
+
|
|
612
|
+
setInput('');
|
|
613
|
+
setError(null);
|
|
614
|
+
setMessages(prev => [...prev, { role: 'user', content: prompt }]);
|
|
615
|
+
setProgress(null);
|
|
616
|
+
setLoading(true);
|
|
617
|
+
|
|
618
|
+
try {
|
|
619
|
+
// Starts the conversation if this is its first message, and reports
|
|
620
|
+
// its notebook's name either way.
|
|
621
|
+
const pinned = await commands.execute('oceanum-ai:chat-context');
|
|
622
|
+
if (mine !== conversation.current) {
|
|
623
|
+
return;
|
|
624
|
+
}
|
|
625
|
+
setContext((pinned as string | null) ?? null);
|
|
626
|
+
// Pass current chat history (before adding the new user message)
|
|
627
|
+
// The user message was already added to state, so we use the messages array directly
|
|
628
|
+
const result = await commands.execute('oceanum-ai:submit-prompt', {
|
|
629
|
+
prompt,
|
|
630
|
+
chatHistory: messages as unknown as ReadonlyJSONArray
|
|
631
|
+
});
|
|
632
|
+
if (mine !== conversation.current) {
|
|
633
|
+
return;
|
|
634
|
+
}
|
|
635
|
+
// Placing the answer can have put it in a new notebook (the old one was
|
|
636
|
+
// deleted), so the line is read again rather than left as it was.
|
|
637
|
+
const after = await commands.execute('oceanum-ai:chat-context');
|
|
638
|
+
if (mine !== conversation.current) {
|
|
639
|
+
return;
|
|
640
|
+
}
|
|
641
|
+
setContext((after as string | null) ?? null);
|
|
642
|
+
const explanation = (result as string) ?? '';
|
|
643
|
+
setMessages(prev => [
|
|
644
|
+
...prev,
|
|
645
|
+
{ role: 'assistant', content: explanation }
|
|
646
|
+
]);
|
|
647
|
+
} catch (err: any) {
|
|
648
|
+
if (mine === conversation.current) {
|
|
649
|
+
setError(err?.message ?? 'An error occurred');
|
|
650
|
+
}
|
|
651
|
+
} finally {
|
|
652
|
+
if (mine === conversation.current) {
|
|
653
|
+
setLoading(false);
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
};
|
|
657
|
+
|
|
658
|
+
// Clear the conversation and start another, pinned to whichever notebook is
|
|
659
|
+
// the active tab now. The prompt history (up/down arrow) is kept: it is
|
|
660
|
+
// input recall, not conversation.
|
|
661
|
+
const handleNewChat = async (): Promise<void> => {
|
|
662
|
+
conversation.current += 1;
|
|
663
|
+
const mine = conversation.current;
|
|
664
|
+
setMessages([]);
|
|
665
|
+
setInput('');
|
|
666
|
+
setError(null);
|
|
667
|
+
setLoading(false);
|
|
668
|
+
// The old run's last phase belongs to the conversation thrown away.
|
|
669
|
+
setProgress(null);
|
|
670
|
+
setHistoryIndex(-1);
|
|
671
|
+
setTempInput('');
|
|
672
|
+
try {
|
|
673
|
+
const pinned = await commands.execute('oceanum-ai:new-chat');
|
|
674
|
+
if (mine === conversation.current) {
|
|
675
|
+
setContext((pinned as string | null) ?? null);
|
|
676
|
+
}
|
|
677
|
+
} catch (err: any) {
|
|
678
|
+
// The chat commands register once settings have loaded, so a click
|
|
679
|
+
// before that has nothing to reach. Say so, rather than leave the old
|
|
680
|
+
// conversation's "Context:" line under a cleared chat.
|
|
681
|
+
if (mine === conversation.current) {
|
|
682
|
+
setContext(undefined);
|
|
683
|
+
setError(err?.message ?? 'Oceanum AI is not ready yet.');
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
};
|
|
687
|
+
|
|
688
|
+
const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>): void => {
|
|
689
|
+
if (e.key === 'Enter' && !e.shiftKey) {
|
|
690
|
+
e.preventDefault();
|
|
691
|
+
void handleSubmit();
|
|
692
|
+
return;
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
// History navigation with up/down arrows
|
|
696
|
+
if (e.key === 'ArrowUp' && history.length > 0) {
|
|
697
|
+
e.preventDefault();
|
|
698
|
+
if (historyIndex === -1) {
|
|
699
|
+
// Save current input before navigating
|
|
700
|
+
setTempInput(input);
|
|
701
|
+
setHistoryIndex(history.length - 1);
|
|
702
|
+
setInput(history[history.length - 1]);
|
|
703
|
+
} else if (historyIndex > 0) {
|
|
704
|
+
setHistoryIndex(historyIndex - 1);
|
|
705
|
+
setInput(history[historyIndex - 1]);
|
|
706
|
+
}
|
|
707
|
+
return;
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
if (e.key === 'ArrowDown' && historyIndex !== -1) {
|
|
711
|
+
e.preventDefault();
|
|
712
|
+
if (historyIndex < history.length - 1) {
|
|
713
|
+
setHistoryIndex(historyIndex + 1);
|
|
714
|
+
setInput(history[historyIndex + 1]);
|
|
715
|
+
} else {
|
|
716
|
+
// Return to current input
|
|
717
|
+
setHistoryIndex(-1);
|
|
718
|
+
setInput(tempInput);
|
|
719
|
+
}
|
|
720
|
+
return;
|
|
721
|
+
}
|
|
722
|
+
};
|
|
723
|
+
|
|
724
|
+
// Hide AI panel if no token or code capability is not enabled
|
|
725
|
+
if (!hasToken || codeEnabled === false) {
|
|
726
|
+
return null;
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
return (
|
|
730
|
+
<div className="oceanum-ai-chat">
|
|
731
|
+
<div className="oceanum-ai-chat-header">
|
|
732
|
+
<span className="oceanum-ai-chat-title">Oceanum AI</span>
|
|
733
|
+
<button
|
|
734
|
+
className="jp-mod-styled oceanum-ai-chat-new"
|
|
735
|
+
onClick={() => void handleNewChat()}
|
|
736
|
+
title="Clear this conversation and start a new one, with the active notebook as its context"
|
|
737
|
+
>
|
|
738
|
+
New chat
|
|
739
|
+
</button>
|
|
740
|
+
</div>
|
|
741
|
+
{context !== undefined && (
|
|
742
|
+
<div className="oceanum-ai-chat-context" title={context ?? undefined}>
|
|
743
|
+
{context === null ? 'No notebook in context' : `Context: ${context}`}
|
|
744
|
+
</div>
|
|
745
|
+
)}
|
|
746
|
+
<div className="oceanum-ai-chat-messages">
|
|
747
|
+
{messages.length === 0 && (
|
|
748
|
+
<div className="oceanum-text-empty">
|
|
749
|
+
Ask Oceanum AI to query and analyse data from Datamesh.
|
|
750
|
+
<br></br>
|
|
751
|
+
Answers go into this chat's notebook: the one in the active tab
|
|
752
|
+
when the chat starts, or a new one.
|
|
753
|
+
</div>
|
|
754
|
+
)}
|
|
755
|
+
{messages.map((msg, i) => (
|
|
756
|
+
<div
|
|
757
|
+
key={i}
|
|
758
|
+
className={`oceanum-ai-chat-message oceanum-ai-chat-message--${msg.role}`}
|
|
759
|
+
>
|
|
760
|
+
<span className="oceanum-ai-chat-role">
|
|
761
|
+
{msg.role === 'user' ? 'You' : 'AI'}
|
|
762
|
+
</span>
|
|
763
|
+
{msg.role === 'user' ? (
|
|
764
|
+
<pre className="oceanum-ai-chat-content">{msg.content}</pre>
|
|
765
|
+
) : (
|
|
766
|
+
<MarkdownContent content={msg.content} />
|
|
767
|
+
)}
|
|
768
|
+
</div>
|
|
769
|
+
))}
|
|
770
|
+
{loading && (
|
|
771
|
+
<div className="oceanum-ai-chat-message oceanum-ai-chat-message--assistant">
|
|
772
|
+
<span className="oceanum-ai-chat-role">AI</span>
|
|
773
|
+
<span className="oceanum-ai-chat-loading">
|
|
774
|
+
{describeProgress(progress)}
|
|
775
|
+
</span>
|
|
776
|
+
</div>
|
|
777
|
+
)}
|
|
778
|
+
{error && <div className="oceanum-ai-chat-error">{error}</div>}
|
|
779
|
+
<div ref={messagesEndRef} />
|
|
780
|
+
</div>
|
|
781
|
+
<div className="oceanum-ai-chat-input-area">
|
|
782
|
+
<textarea
|
|
783
|
+
className="oceanum-ai-chat-input"
|
|
784
|
+
rows={3}
|
|
785
|
+
placeholder="Ask Oceanum AI… (Enter to send, Shift+Enter for newline)"
|
|
786
|
+
value={input}
|
|
787
|
+
onChange={e => setInput(e.target.value)}
|
|
788
|
+
onKeyDown={handleKeyDown}
|
|
789
|
+
disabled={loading}
|
|
790
|
+
/>
|
|
791
|
+
<button
|
|
792
|
+
className="jp-mod-styled oceanum-ai-chat-send"
|
|
793
|
+
onClick={() =>
|
|
794
|
+
void (loading
|
|
795
|
+
? commands.execute('oceanum-ai:stop')
|
|
796
|
+
: handleSubmit())
|
|
797
|
+
}
|
|
798
|
+
disabled={!loading && !input.trim()}
|
|
799
|
+
title={loading ? 'Stop the current response' : undefined}
|
|
800
|
+
>
|
|
801
|
+
{loading ? 'Stop' : 'Send'}
|
|
802
|
+
</button>
|
|
803
|
+
</div>
|
|
804
|
+
</div>
|
|
805
|
+
);
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
/**
|
|
809
|
+
* DatameshConnectWidget props.
|
|
810
|
+
*/
|
|
811
|
+
export interface IDatameshWidgetProps {
|
|
812
|
+
app: JupyterFrontEnd;
|
|
813
|
+
name: string;
|
|
814
|
+
icon: LabIcon;
|
|
815
|
+
openDatameshUI: any;
|
|
816
|
+
commands: CommandRegistry;
|
|
817
|
+
getCurrentWidget: () => Widget;
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
/**
|
|
821
|
+
* A widget for Datamesh Connections.
|
|
822
|
+
*/
|
|
823
|
+
export class DatameshConnectWidget extends ReactWidget {
|
|
824
|
+
props: IDatameshWidgetProps;
|
|
825
|
+
renderSignal: Signal<this, any>;
|
|
826
|
+
icon: LabIcon;
|
|
827
|
+
openDatameshUI: any;
|
|
828
|
+
datameshWorkspaceSpec: IWorkspaceSpec | null = null;
|
|
829
|
+
|
|
830
|
+
constructor(props: IDatameshWidgetProps) {
|
|
831
|
+
super();
|
|
832
|
+
this.props = props;
|
|
833
|
+
this.renderSignal = new Signal<this, any>(this);
|
|
834
|
+
this.renderDisplay = this.renderDisplay.bind(this);
|
|
835
|
+
|
|
836
|
+
window.addEventListener(
|
|
837
|
+
'message',
|
|
838
|
+
this.receiveIFrameMessage.bind(this),
|
|
839
|
+
false
|
|
840
|
+
);
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
receiveIFrameMessage(event: MessageEvent): void {
|
|
844
|
+
if (event.data && event.data.action === 'workspace-modify') {
|
|
845
|
+
this.datameshWorkspaceSpec = {
|
|
846
|
+
id: event.data.id,
|
|
847
|
+
name: event.data.name,
|
|
848
|
+
data: event.data.data
|
|
849
|
+
};
|
|
850
|
+
this.renderSignal.emit(this.datameshWorkspaceSpec);
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
renderDisplay(datameshWorkspace: IWorkspaceSpec): React.ReactElement {
|
|
855
|
+
return (
|
|
856
|
+
<>
|
|
857
|
+
{this.datameshWorkspaceSpec ? (
|
|
858
|
+
<DatameshWorkspaceDisplay
|
|
859
|
+
spec={datameshWorkspace}
|
|
860
|
+
openDatameshUI={this.props.openDatameshUI}
|
|
861
|
+
getCurrentWidget={this.props.getCurrentWidget}
|
|
862
|
+
shell={this.props.app.shell}
|
|
863
|
+
/>
|
|
864
|
+
) : (
|
|
865
|
+
<div className="oceanum-text-empty">
|
|
866
|
+
<a onClick={this.props.openDatameshUI}>Open</a> the Oceanum Datamesh
|
|
867
|
+
UI to add datasources.
|
|
868
|
+
</div>
|
|
869
|
+
)}
|
|
870
|
+
</>
|
|
871
|
+
);
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
render(): React.ReactElement {
|
|
875
|
+
return (
|
|
876
|
+
<div className="datamesh-connect">
|
|
877
|
+
<header className="oceanum-sidebar-header">
|
|
878
|
+
<this.props.icon.react
|
|
879
|
+
tag="span"
|
|
880
|
+
width="auto"
|
|
881
|
+
height="20px"
|
|
882
|
+
verticalAlign="middle"
|
|
883
|
+
/>
|
|
884
|
+
<span className="oceanum-sidebar-title">Oceanum.io</span>
|
|
885
|
+
</header>
|
|
886
|
+
<div className="datamesh-workspace-header">
|
|
887
|
+
<span>Datamesh Workspace</span>
|
|
888
|
+
<div
|
|
889
|
+
className="open-datamesh-ui"
|
|
890
|
+
onClick={this.props.openDatameshUI}
|
|
891
|
+
title="Open Datamesh UI"
|
|
892
|
+
>
|
|
893
|
+
{<addIcon.react height="20px" verticalAlign="middle" />}
|
|
894
|
+
</div>
|
|
895
|
+
</div>
|
|
896
|
+
<div className="datamesh-connect-workspace">
|
|
897
|
+
<UseSignal signal={this.renderSignal} initialArgs={null}>
|
|
898
|
+
{(_, datameshWorkspace): React.ReactElement =>
|
|
899
|
+
this.renderDisplay(datameshWorkspace)
|
|
900
|
+
}
|
|
901
|
+
</UseSignal>
|
|
902
|
+
<TokenConfigMessage commands={this.props.commands} />
|
|
903
|
+
</div>
|
|
904
|
+
<div className="datamesh-connect-divider" />
|
|
905
|
+
<AIChatPanel commands={this.props.commands} />
|
|
906
|
+
</div>
|
|
907
|
+
);
|
|
908
|
+
}
|
|
909
|
+
}
|