aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/client/apis/gpt/GPT.ts37
-rw-r--r--src/client/views/PropertiesView.scss5
-rw-r--r--src/client/views/nodes/DataVizBox/DataVizBox.scss4
-rw-r--r--src/client/views/nodes/DataVizBox/DataVizBox.tsx52
-rw-r--r--src/client/views/nodes/DataVizBox/components/Chart.scss63
-rw-r--r--src/client/views/nodes/DataVizBox/components/TableBox.tsx160
-rw-r--r--src/client/views/nodes/formattedText/FormattedTextBox.tsx20
-rw-r--r--src/client/views/pdf/AnchorMenu.tsx15
-rw-r--r--src/client/views/pdf/GPTPopup/GPTPopup.tsx169
9 files changed, 425 insertions, 100 deletions
diff --git a/src/client/apis/gpt/GPT.ts b/src/client/apis/gpt/GPT.ts
index fb51278ae..2de87586b 100644
--- a/src/client/apis/gpt/GPT.ts
+++ b/src/client/apis/gpt/GPT.ts
@@ -1,9 +1,11 @@
import { ClientOptions, OpenAI } from 'openai';
+import { ChatCompletionMessageParam } from 'openai/resources';
enum GPTCallType {
SUMMARY = 'summary',
COMPLETION = 'completion',
EDIT = 'edit',
+ DATA = 'data',
}
type GPTCallOpts = {
@@ -13,10 +15,18 @@ type GPTCallOpts = {
prompt: string;
};
+/**
+ * Replace completions (deprecated) with chat
+ */
+
const callTypeMap: { [type: string]: GPTCallOpts } = {
- summary: { model: 'gpt-3.5-turbo-instruct', maxTokens: 256, temp: 0.5, prompt: 'Summarize this text in simpler terms: ' },
- edit: { model: 'gpt-3.5-turbo-instruct', maxTokens: 256, temp: 0.5, prompt: 'Reword this: ' },
- completion: { model: 'gpt-3.5-turbo-instruct', maxTokens: 256, temp: 0.5, prompt: '' },
+ // newest model: gpt-4
+ summary: { model: 'gpt-3.5-turbo', maxTokens: 256, temp: 0.5, prompt: 'Summarize the text given in simpler terms.' },
+ edit: { model: 'gpt-3.5-turbo', maxTokens: 256, temp: 0.5, prompt: 'Reword the text.' },
+ completion: { model: 'gpt-3.5-turbo', maxTokens: 256, temp: 0.5, prompt: "You are a helpful assistant. Answer the user's prompt." },
+ // data: { model: 'gpt-3.5-turbo', maxTokens: 256, temp: 0.5, prompt: "You are a helpful resarch assistant. Analyze the user's data to find meaningful patterns and/or correlation. Please keep your response short and to the point." },
+ data: { model: 'gpt-3.5-turbo', maxTokens: 256, temp: 0.5, prompt: "You are a helpful resarch assistant. Analyze the user's data to find meaningful patterns and/or correlation. Please only return a JSON with a correlation column 1 propert, a correlation column 2 property, and an analysis property. " },
+
};
/**
@@ -25,7 +35,7 @@ const callTypeMap: { [type: string]: GPTCallOpts } = {
* @param inputText Text to process
* @returns AI Output
*/
-const gptAPICall = async (inputText: string, callType: GPTCallType) => {
+const gptAPICall = async (inputText: string, callType: GPTCallType, prompt?: any) => {
if (callType === GPTCallType.SUMMARY) inputText += '.';
const opts: GPTCallOpts = callTypeMap[callType];
try {
@@ -34,13 +44,21 @@ const gptAPICall = async (inputText: string, callType: GPTCallType) => {
dangerouslyAllowBrowser: true,
};
const openai = new OpenAI(configuration);
- const response = await openai.completions.create({
+
+ let usePrompt = prompt? opts.prompt+prompt: opts.prompt;
+ let messages: ChatCompletionMessageParam[] = [
+ { role: 'system', content: usePrompt },
+ { role: 'user', content: inputText },
+ ];
+
+ const response = await openai.chat.completions.create({
model: opts.model,
- max_tokens: opts.maxTokens,
+ messages: messages,
temperature: opts.temp,
- prompt: `${opts.prompt}${inputText}`,
+ max_tokens: opts.maxTokens,
});
- return response.choices[0].text;
+ const content = response.choices[0].message.content;
+ return content;
} catch (err) {
console.log(err);
return 'Error connecting with API.';
@@ -60,8 +78,7 @@ const gptImageCall = async (prompt: string, n?: number) => {
n: n ?? 1,
size: '1024x1024',
});
- return response.data.map((data: any) => data.url);
- // return response.data.data[0].url;
+ return response.data.map(data => data.url);
} catch (err) {
console.error(err);
return;
diff --git a/src/client/views/PropertiesView.scss b/src/client/views/PropertiesView.scss
index 8581bdf73..476b46905 100644
--- a/src/client/views/PropertiesView.scss
+++ b/src/client/views/PropertiesView.scss
@@ -227,14 +227,15 @@
font-weight: bold;
width: 95px;
overflow-x: hidden;
- display: inline-block;
text-overflow: ellipsis;
white-space: nowrap;
+ display: flex;
+ align-items: center;
}
.propertiesView-sharingTable-item-permission {
display: flex;
- align-items: flex-end;
+ align-items: center;
text-align: right;
margin-left: auto;
margin-right: -12px;
diff --git a/src/client/views/nodes/DataVizBox/DataVizBox.scss b/src/client/views/nodes/DataVizBox/DataVizBox.scss
index 6b5738790..e9a346fbe 100644
--- a/src/client/views/nodes/DataVizBox/DataVizBox.scss
+++ b/src/client/views/nodes/DataVizBox/DataVizBox.scss
@@ -32,6 +32,10 @@
.liveSchema-checkBox {
margin-bottom: -35px;
}
+
+ .displaySchemaLive {
+ margin-bottom: 20px;
+ }
.dataviz-sidebar {
position: absolute;
diff --git a/src/client/views/nodes/DataVizBox/DataVizBox.tsx b/src/client/views/nodes/DataVizBox/DataVizBox.tsx
index 22f1f7b79..dbba9c7f3 100644
--- a/src/client/views/nodes/DataVizBox/DataVizBox.tsx
+++ b/src/client/views/nodes/DataVizBox/DataVizBox.tsx
@@ -18,7 +18,7 @@ import { ViewBoxAnnotatableComponent, ViewBoxInterface } from '../../DocComponen
import { MarqueeAnnotator } from '../../MarqueeAnnotator';
import { SidebarAnnos } from '../../SidebarAnnos';
import { AnchorMenu } from '../../pdf/AnchorMenu';
-import { GPTPopup } from '../../pdf/GPTPopup/GPTPopup';
+import { GPTPopup, GPTPopupMode } from '../../pdf/GPTPopup/GPTPopup';
import { DocumentView } from '../DocumentView';
import { FocusViewOptions, FieldView, FieldViewProps } from '../FieldView';
import { PinProps } from '../trails';
@@ -28,6 +28,8 @@ import { LineChart } from './components/LineChart';
import { PieChart } from './components/PieChart';
import { TableBox } from './components/TableBox';
import { Checkbox } from '@mui/material';
+import { ContextMenu } from '../../ContextMenu';
+import { DragManager } from '../../../util/DragManager';
export enum DataVizView {
TABLE = 'table',
@@ -43,6 +45,7 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() im
private _annotationLayer: React.RefObject<HTMLDivElement> = React.createRef();
private _disposers: { [name: string]: IReactionDisposer } = {};
anchorMenuClick?: () => undefined | ((anchor: Doc) => void);
+ sidebarAddDoc: ((doc: Doc | Doc[], sidebarKey?: string | undefined) => boolean) | undefined;
crop: ((region: Doc | undefined, addCrop?: boolean) => Doc | undefined) | undefined;
@observable _marqueeing: number[] | undefined = undefined;
@observable _savedAnnotations = new ObservableMap<number, HTMLDivElement[]>();
@@ -400,7 +403,45 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() im
@action
changeLiveSchemaCheckbox = () => {
- this.layoutDoc.dataViz_schemaLive = !this.layoutDoc.dataViz_schemaLive;
+ this.layoutDoc.dataViz_schemaLive = !this.layoutDoc.dataViz_schemaLive
+ }
+
+ specificContextMenu = (e: React.MouseEvent): void => {
+ const cm = ContextMenu.Instance;
+ const options = cm.findByDescription('Options...');
+ const optionItems = options && 'subitems' in options ? options.subitems : [];
+ optionItems.push({ description: `Analyze with AI`, event: () => this.askGPT(), icon: 'lightbulb' });
+ !options && cm.addItem({ description: 'Options...', subitems: optionItems, icon: 'eye' });
+ }
+
+
+ askGPT = action(async () => {
+ GPTPopup.Instance.setSidebarId('data_sidebar');
+ GPTPopup.Instance.addDoc = this.sidebarAddDocument;
+ GPTPopup.Instance.createFilteredDoc = this.createFilteredDoc;
+ GPTPopup.Instance.setDataJson("");
+ GPTPopup.Instance.setMode(GPTPopupMode.DATA);
+ let data = DataVizBox.dataset.get(CsvCast(this.dataDoc[this.fieldKey]).url.href);
+ let input = JSON.stringify(data);
+ GPTPopup.Instance.setDataJson(input);
+ GPTPopup.Instance.generateDataAnalysis();
+ });
+
+ createFilteredDoc = (axes?: any, type?: DataVizView) => {
+
+ const embedding = Doc.MakeEmbedding(this.Document!);
+ embedding._dataViz = DataVizView.HISTOGRAM;
+ embedding._dataViz_axes = new List<string>(axes);
+ embedding._dataViz_parentViz = this.Document;
+ embedding.histogramBarColors = Field.Copy(this.layoutDoc.histogramBarColors);
+ embedding.defaultHistogramColor = this.layoutDoc.defaultHistogramColor;
+ embedding.pieSliceColors = Field.Copy(this.layoutDoc.pieSliceColors);
+ this._props.addDocument?.(embedding);
+ embedding._dataViz_axes = new List<string>(axes)
+ this.layoutDoc.dataViz_selectedRows = new List<number>(this.records.map((rec, i) => i))
+ embedding.x = Number(embedding.x) + 100.0;
+
+ return true;
};
render() {
@@ -419,6 +460,7 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() im
transform: `scale(${scale})`,
position: 'absolute',
}}
+ onContextMenu={this.specificContextMenu}
onWheel={e => e.stopPropagation()}
ref={this._mainCont}>
<div className="datatype-button">
@@ -428,11 +470,13 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() im
<Toggle text={'PIE CHART'} toggleType={ToggleType.BUTTON} type={Type.SEC} color={'black'} onClick={e => (this.layoutDoc._dataViz = DataVizView.PIECHART)} toggleStatus={this.layoutDoc._dataViz == -DataVizView.PIECHART} />
</div>
- {this.layoutDoc && this.layoutDoc.dataViz_asSchema ? (
- <div className={'liveSchema-checkBox'} style={{ width: this._props.width }}>
+ {(this.layoutDoc && this.layoutDoc.dataViz_asSchema)?(
+ <div className={'displaySchemaLive'}>
+ <div className={'liveSchema-checkBox'} style={{ width: this._props.width }}>
<Checkbox color="primary" onChange={this.changeLiveSchemaCheckbox} checked={this.layoutDoc.dataViz_schemaLive as boolean} />
Display Live Updates to Canvas
</div>
+ </div>
) : null}
{this.renderVizView}
diff --git a/src/client/views/nodes/DataVizBox/components/Chart.scss b/src/client/views/nodes/DataVizBox/components/Chart.scss
index 41ce637ac..cf0007cfd 100644
--- a/src/client/views/nodes/DataVizBox/components/Chart.scss
+++ b/src/client/views/nodes/DataVizBox/components/Chart.scss
@@ -120,11 +120,62 @@
}
}
}
-.selectAll-buttons {
- display: flex;
- flex-direction: row;
- justify-content: flex-end;
+.tableBox-selectButtons {
margin-top: 5px;
- margin-right: 10px;
- float: right;
+ margin-left: 25px;
+ display: inline-block;
+ padding: 2px;
+ .tableBox-selectTitle {
+ display: inline-flex;
+ flex-direction: row;
+ }
+ .tableBox-filtering {
+ display: flex;
+ flex-direction: row;
+ float: right;
+ margin-right: 10px;
+ .tableBox-filterAll {
+ min-width: 75px;
+ }
+ }
+}
+
+.tableBox-filterPopup {
+ background: $light-gray;
+ position: absolute;
+ min-width: 235px;
+ top: 60px;
+ display: flex;
+ flex-direction: column;
+ align-items: flex-start;
+ z-index: 2;
+ padding: 7px;
+ border-radius: 5px;
+ margin: 3px;
+ .tableBox-filterPopup-selectColumn {
+ margin-top: 5px;
+ flex-direction: row;
+ .tableBox-filterPopup-selectColumn-each {
+ margin-left: 25px;
+ border-radius: 3px;
+ background: $light-gray;
+ }
+ }
+ .tableBox-filterPopup-setValue {
+ margin-top: 5px;
+ display: flex;
+ flex-direction: row;
+ .tableBox-filterPopup-setValue-each {
+ margin-right: 5px;
+ border-radius: 3px;
+ background: $light-gray;
+ }
+ .tableBox-filterPopup-setValue-input {
+ margin: 5px;
+ }
+ }
+ .tableBox-filterPopup-setFilter {
+ margin-top: 5px;
+ align-self: center;
+ }
}
diff --git a/src/client/views/nodes/DataVizBox/components/TableBox.tsx b/src/client/views/nodes/DataVizBox/components/TableBox.tsx
index 53d1869d9..558847a03 100644
--- a/src/client/views/nodes/DataVizBox/components/TableBox.tsx
+++ b/src/client/views/nodes/DataVizBox/components/TableBox.tsx
@@ -12,6 +12,7 @@ import { ObservableReactComponent } from '../../../ObservableReactComponent';
import { DocumentView } from '../../DocumentView';
import { DataVizView } from '../DataVizBox';
import './Chart.scss';
+import { undoBatch } from '../../../../util/UndoManager';
const { DATA_VIZ_TABLE_ROW_HEIGHT } = require('../../../global/globalCssVariables.module.scss'); // prettier-ignore
interface TableBoxProps {
Document: Doc;
@@ -37,6 +38,13 @@ export class TableBox extends ObservableReactComponent<TableBoxProps> {
_inputChangedDisposer?: IReactionDisposer;
_containerRef: HTMLDivElement | null = null;
+ @observable settingTitle: boolean = false; // true when setting a title column
+ @observable hasRowsToFilter: boolean = false; // true when any rows are selected
+ @observable filtering: boolean = false; // true when the filtering menu is open
+ @observable filteringColumn: any = ""; // column to filter
+ @observable filteringType: string = "Value"; // "Value" or "Range"
+ filteringVal: any[] = ["", ""]; // value or range to filter the column with
+
@observable _scrollTop = -1;
@observable _tableHeight = 0;
@observable _tableContainerHeight = 0;
@@ -49,6 +57,8 @@ export class TableBox extends ObservableReactComponent<TableBoxProps> {
// if the tableData changes (ie., when records are selected by the parent (input) visulization),
// then we need to remove any selected rows that are no longer part of the visualized dataset.
this._inputChangedDisposer = reaction(() => this._tableData.slice(), this.filterSelectedRowsDown, { fireImmediately: true });
+ const selected = NumListCast(this._props.layoutDoc.dataViz_selectedRows);
+ if (selected.length>0) this.hasRowsToFilter = true;
this.handleScroll();
}
componentWillUnmount() {
@@ -64,9 +74,6 @@ export class TableBox extends ObservableReactComponent<TableBoxProps> {
@computed get parentViz() {
return DocCast(this._props.Document.dataViz_parentViz);
- // return LinkManager.Instance.getAllRelatedLinks(this._props.Document) // out of all links
- // .filter(link => link.link_anchor_1 == this._props.Document.dataViz_parentViz) // get links where this chart doc is the target of the link
- // .map(link => DocCast(link.link_anchor_1)); // then return the source of the link
}
@computed get columns() {
@@ -115,6 +122,7 @@ export class TableBox extends ObservableReactComponent<TableBoxProps> {
} else selected?.push(rowId);
}
e.stopPropagation();
+ this.hasRowsToFilter = (selected.length>0)? true : false;
};
columnPointerDown = (e: React.PointerEvent, col: string) => {
@@ -155,8 +163,9 @@ export class TableBox extends ObservableReactComponent<TableBoxProps> {
},
emptyFunction,
action(e => {
- if (e.shiftKey) {
- if (this._props.titleCol == col) this._props.titleCol = '';
+ if (e.shiftKey || this.settingTitle){
+ if (this.settingTitle) this.settingTitle = false;
+ if (this._props.titleCol == col) this._props.titleCol = "";
else this._props.titleCol = col;
this._props.selectTitleCol(this._props.titleCol);
} else {
@@ -170,6 +179,101 @@ export class TableBox extends ObservableReactComponent<TableBoxProps> {
);
};
+ /**
+ * These functions handle the filtering popup for when the "filter" button is pressed to select rows
+ */
+ @undoBatch
+ filter = (e: any) => {
+ var start: any;
+ var end: any;
+ if (this.filteringType=="Range"){
+ start = (this.filteringVal[0] as Number)? Number(this.filteringVal[0]): this.filteringVal[0]
+ end = (this.filteringVal[1] as Number)? Number(this.filteringVal[1]): this.filteringVal[0]
+ }
+
+ this._tableDataIds.forEach(rowID => {
+ if (this.filteringType=="Value"){
+ if (this._props.records[rowID][this.filteringColumn]==this.filteringVal[0]) {
+ if (!NumListCast(this._props.layoutDoc.dataViz_selectedRows).includes(rowID)) {
+ this.tableRowClick(e, rowID);
+ }
+ }
+ }
+ else {
+ let compare = this._props.records[rowID][this.filteringColumn]
+ if (compare as Number) compare = Number(compare)
+ if (start<=compare && compare<=end){
+ if (!NumListCast(this._props.layoutDoc.dataViz_selectedRows).includes(rowID)) {
+ this.tableRowClick(e, rowID);
+ }
+ }
+ }
+ })
+ this.filtering = false;
+ this.filteringColumn = "";
+ this.filteringVal = ["", ""];
+ }
+ @action
+ setFilterColumn = (e:any) => {
+ this.filteringColumn = e.currentTarget.value;
+ }
+ @action
+ setFilterType = (e:any) => {
+ this.filteringType = e.currentTarget.value;
+ }
+ changeFilterValue = action((e: React.ChangeEvent<HTMLInputElement>) => {
+ this.filteringVal[0] = e.target.value;
+ });
+ changeFilterRange0 = action((e: React.ChangeEvent<HTMLInputElement>) => {
+ this.filteringVal[0] = e.target.value;
+ });
+ changeFilterRange1 = action((e: React.ChangeEvent<HTMLInputElement>) => {
+ this.filteringVal[1] = e.target.value;
+ });
+ @computed get renderFiltering() {
+ if (this.filteringColumn==="") this.filteringColumn = this.columns[0];
+ return (
+ <div className="tableBox-filterPopup" style={{right: this._props.width*.05}}>
+ <div className="tableBox-filterPopup-selectColumn">
+ Column:
+ <select className="tableBox-filterPopup-selectColumn-each" value={this.filteringColumn!=""? this.filteringColumn : this.columns[0]} onChange={e => this.setFilterColumn(e)}>
+ {this.columns.map(column => (
+ <option className="" key={column} value={column}> {column} </option>
+ ))}
+ </select>
+ </div>
+ <div className="tableBox-filterPopup-setValue">
+ <select className="tableBox-filterPopup-setValue-each" value={this.filteringType} onChange={e => this.setFilterType(e)}>
+ <option className="" key={"Value"} value={"Value"}> {"Value"} </option>
+ <option className="" key={"Range"} value={"Range"}> {"Range"} </option>
+ </select>
+ :
+ {this.filteringType=="Value"?
+ <input className="tableBox-filterPopup-setValue-input" defaultValue="" autoComplete="off"
+ onChange={this.changeFilterValue} onKeyDown={e => {e.stopPropagation();}}
+ type="text" placeholder="" id="search-input"
+ />
+ :
+ <div>
+ <input className="tableBox-filterPopup-setValue-input" defaultValue="" autoComplete="off"
+ onChange={this.changeFilterRange0} onKeyDown={e => {e.stopPropagation();}}
+ type="text" placeholder="" id="search-input" style={{width: this._props.width*.15}}
+ />
+ to
+ <input className="tableBox-filterPopup-setValue-input" defaultValue="" autoComplete="off"
+ onChange={this.changeFilterRange1} onKeyDown={e => {e.stopPropagation();}}
+ type="text" placeholder="" id="search-input" style={{width: this._props.width*.15}}
+ />
+ </div>
+ }
+ </div>
+ <div className="tableBox-filterPopup-setFilter">
+ <Button onClick={action((e) => this.filter(e))} text="Set Filter" type={Type.SEC} color={'black'} />
+ </div>
+ </div>
+ )
+ }
+
render() {
if (this._tableData.length > 0) {
return (
@@ -183,9 +287,23 @@ export class TableBox extends ObservableReactComponent<TableBoxProps> {
this._props.layoutDoc.dataViz_selectedRows = new List<number>(this._tableDataIds);
}
}}>
- <div className="selectAll-buttons">
- <Button onClick={action(() => (this._props.layoutDoc.dataViz_selectedRows = new List<number>(this._tableDataIds)))} text="Select All" type={Type.SEC} color={'black'} />
- <Button onClick={action(() => (this._props.layoutDoc.dataViz_selectedRows = new List<number>()))} text="Deselect All" type={Type.SEC} color={'black'} />
+ <div className="tableBox-selectButtons">
+ <div className="tableBox-selectTitle">
+ <Button onClick={action(() => (this.settingTitle = !this.settingTitle))} text="Select Title Column" type={Type.SEC} color={'black'} />
+ </div>
+ <div className="tableBox-filtering">
+ {this.filtering? this.renderFiltering : null}
+ <Button onClick={action(() => (this.filtering = !this.filtering))} text="Filter" type={Type.SEC} color={'black'} />
+ <div className="tableBox-filterAll">
+ {this.hasRowsToFilter? <Button onClick={action(() => {
+ this._props.layoutDoc.dataViz_selectedRows = new List<number>();
+ this.hasRowsToFilter = false; })} text="Deselect All" type={Type.SEC} color={'black'} tooltip="Select rows to be displayed in any DataViz boxes dragged off of this one." />
+ : <Button onClick={action(() => {
+ this._props.layoutDoc.dataViz_selectedRows = new List<number>(this._tableDataIds)
+ this.hasRowsToFilter = true; })} text="Select All" type={Type.SEC} color={'black'} tooltip="Select rows to be displayed in any DataViz boxes dragged off of this one." />
+ }
+ </div>
+ </div>
</div>
<div
className={`tableBox-container ${this.columns[0]}`}
@@ -219,25 +337,13 @@ export class TableBox extends ObservableReactComponent<TableBoxProps> {
<th
key={this.columns.indexOf(col)}
style={{
- color:
- this._props.axes.slice().reverse().lastElement() === col
- ? 'darkgreen'
- : this._props.axes.length > 2 && this._props.axes.lastElement() === col
- ? 'darkred'
- : this._props.axes.lastElement() === col || (this._props.axes.length > 2 && this._props.axes[1] == col)
- ? 'darkblue'
- : undefined,
- background:
- this._props.axes.slice().reverse().lastElement() === col
- ? '#E3fbdb'
- : this._props.axes.length > 2 && this._props.axes.lastElement() === col
- ? '#Fbdbdb'
- : this._props.axes.lastElement() === col || (this._props.axes.length > 2 && this._props.axes[1] == col)
- ? '#c6ebf7'
- : undefined,
- // blue: #ADD8E6
- // green: #E3fbdb
- // red: #Fbdbdb
+ color: this._props.axes.slice().reverse().lastElement() === col ? 'darkgreen'
+ : (this._props.axes.length>2 && this._props.axes.lastElement() === col) ? 'darkred'
+ : (this._props.axes.lastElement()===col || (this._props.axes.length>2 && this._props.axes[1]==col))? 'darkblue' : undefined,
+ background: this.settingTitle? 'lightgrey'
+ : this._props.axes.slice().reverse().lastElement() === col ? '#E3fbdb'
+ : (this._props.axes.length>2 && this._props.axes.lastElement() === col) ? '#Fbdbdb'
+ : (this._props.axes.lastElement()===col || (this._props.axes.length>2 && this._props.axes[1]==col))? '#c6ebf7' : undefined,
fontWeight: 'bolder',
border: '3px solid black',
}}
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index 5c59f7f60..a53ac8f36 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -8,7 +8,7 @@ import { history } from 'prosemirror-history';
import { inputRules } from 'prosemirror-inputrules';
import { keymap } from 'prosemirror-keymap';
import { Fragment, Mark, Node, Slice } from 'prosemirror-model';
-import { EditorState, NodeSelection, Plugin, TextSelection, Transaction } from 'prosemirror-state';
+import { EditorState, NodeSelection, Plugin, Selection, TextSelection, Transaction } from 'prosemirror-state';
import { EditorView } from 'prosemirror-view';
import * as React from 'react';
import { BsMarkdownFill } from 'react-icons/bs';
@@ -981,9 +981,10 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
};
animateRes = (resIndex: number, newText: string) => {
+ if (!this._editorView) return;
if (resIndex < newText.length) {
- const marks = this._editorView?.state.storedMarks ?? [];
- this._editorView?.dispatch(this._editorView.state.tr.setStoredMarks(marks).insertText(newText[resIndex]).setStoredMarks(marks));
+ const marks = this._editorView.state.storedMarks ?? [];
+ this._editorView.dispatch(this._editorView.state.tr.insertText(newText[resIndex]).setStoredMarks(marks));
setTimeout(() => {
this.animateRes(resIndex + 1, newText);
}, 20);
@@ -994,13 +995,20 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
try {
let res = await gptAPICall((this.dataDoc.text as RichTextField)?.Text, GPTCallType.COMPLETION);
if (!res) {
- console.error('GPT call failed');
this.animateRes(0, 'Something went wrong.');
} else {
- this.animateRes(0, res);
+ if (!this._editorView) return;
+ // No animation
+ // this._editorView.dispatch(this._editorView.state.tr.insertText(res));
+
+ // Animation
+ // Set selection at end
+ const sel = Selection.atEnd(this._editorView.state.doc);
+ this._editorView.dispatch(this._editorView.state.tr.setSelection(sel));
+ this.animateRes(0, '\n\n' + res);
}
} catch (err) {
- console.error('GPT call failed');
+ console.error(err);
this.animateRes(0, 'Something went wrong.');
}
});
diff --git a/src/client/views/pdf/AnchorMenu.tsx b/src/client/views/pdf/AnchorMenu.tsx
index 59f191af0..0b3ba81d3 100644
--- a/src/client/views/pdf/AnchorMenu.tsx
+++ b/src/client/views/pdf/AnchorMenu.tsx
@@ -6,7 +6,6 @@ import * as React from 'react';
import { ColorResult } from 'react-color';
import { Utils, returnFalse, setupMoveUpEvents, unimplementedFunction } from '../../../Utils';
import { Doc, Opt } from '../../../fields/Doc';
-import { GPTCallType, gptAPICall } from '../../apis/gpt/GPT';
import { DocumentType } from '../../documents/DocumentTypes';
import { SelectionManager } from '../../util/SelectionManager';
import { SettingsManager } from '../../util/SettingsManager';
@@ -73,18 +72,8 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
* @param e pointer down event
*/
gptSummarize = async (e: React.PointerEvent) => {
- // move this logic to gptpopup, need to implement generate again
- GPTPopup.Instance.setVisible(true);
- GPTPopup.Instance.setMode(GPTPopupMode.SUMMARY);
- GPTPopup.Instance.setLoading(true);
-
- try {
- const res = await gptAPICall(this.selectedText, GPTCallType.SUMMARY);
- GPTPopup.Instance.setText(res || 'Something went wrong.');
- } catch (err) {
- console.error(err);
- }
- GPTPopup.Instance.setLoading(false);
+ GPTPopup.Instance?.setSelectedText(this.selectedText);
+ GPTPopup.Instance.generateSummary();
};
pointerDown = (e: React.PointerEvent) => {
diff --git a/src/client/views/pdf/GPTPopup/GPTPopup.tsx b/src/client/views/pdf/GPTPopup/GPTPopup.tsx
index da8a88803..686ef9c28 100644
--- a/src/client/views/pdf/GPTPopup/GPTPopup.tsx
+++ b/src/client/views/pdf/GPTPopup/GPTPopup.tsx
@@ -1,5 +1,5 @@
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { Button, IconButton, Type } from 'browndash-components';
+import { Button, EditableText, IconButton, Size, Type } from 'browndash-components';
import { action, makeObservable, observable } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
@@ -10,16 +10,18 @@ import { Utils } from '../../../../Utils';
import { Doc } from '../../../../fields/Doc';
import { NumCast, StrCast } from '../../../../fields/Types';
import { Networking } from '../../../Network';
-import { gptImageCall } from '../../../apis/gpt/GPT';
+import { GPTCallType, gptAPICall, gptImageCall } from '../../../apis/gpt/GPT';
import { DocUtils, Docs } from '../../../documents/Documents';
import { ObservableReactComponent } from '../../ObservableReactComponent';
import { AnchorMenu } from '../AnchorMenu';
import './GPTPopup.scss';
+import { DataVizView } from '../../nodes/DataVizBox/DataVizBox';
export enum GPTPopupMode {
SUMMARY,
EDIT,
IMAGE,
+ DATA,
}
interface GPTPopupProps {}
@@ -27,6 +29,8 @@ interface GPTPopupProps {}
@observer
export class GPTPopup extends ObservableReactComponent<GPTPopupProps> {
static Instance: GPTPopup;
+ @observable private chatMode: boolean = false;
+ private correlatedColumns: string[] = []
@observable
public visible: boolean = false;
@@ -46,6 +50,20 @@ export class GPTPopup extends ObservableReactComponent<GPTPopupProps> {
public setText = (text: string) => {
this.text = text;
};
+ @observable
+ public selectedText: string = '';
+ @action
+ public setSelectedText = (text: string) => {
+ this.selectedText = text;
+ };
+ @observable
+ public dataJson: string = '';
+ public dataChatPrompt: string | null = null;
+ @action
+ public setDataJson = (text: string) => {
+ if (text=="") this.dataChatPrompt = "";
+ this.dataJson = text;
+ };
@observable
public imgDesc: string = '';
@@ -79,6 +97,7 @@ export class GPTPopup extends ObservableReactComponent<GPTPopupProps> {
@action
public setDone = (done: boolean) => {
this.done = done;
+ this.chatMode = false;
};
// change what can be a ref into a ref
@@ -104,6 +123,7 @@ export class GPTPopup extends ObservableReactComponent<GPTPopupProps> {
};
public addDoc: (doc: Doc | Doc[], sidebarKey?: string | undefined) => boolean = () => false;
+ public createFilteredDoc: (axes?: any, type?: DataVizView) => boolean = () => false;
public addToCollection: ((doc: Doc | Doc[], annotationKey?: string | undefined) => boolean) | undefined;
/**
@@ -118,18 +138,52 @@ export class GPTPopup extends ObservableReactComponent<GPTPopupProps> {
try {
let image_urls = await gptImageCall(this.imgDesc);
+ console.log('Image urls: ', image_urls);
if (image_urls && image_urls[0]) {
const [result] = await Networking.PostToServer('/uploadRemoteImage', { sources: [image_urls[0]] });
+ console.log('Upload result: ', result);
const source = Utils.prepend(result.accessPaths.agnostic.client);
+ console.log('Upload source: ', source);
this.setImgUrls([[image_urls[0], source]]);
}
} catch (err) {
- console.log(err);
- return '';
+ console.error(err);
}
this.setLoading(false);
};
+ generateSummary = async () => {
+ GPTPopup.Instance.setVisible(true);
+ GPTPopup.Instance.setMode(GPTPopupMode.SUMMARY);
+ GPTPopup.Instance.setLoading(true);
+
+ try {
+ const res = await gptAPICall(this.selectedText, GPTCallType.SUMMARY);
+ GPTPopup.Instance.setText(res || 'Something went wrong.');
+ } catch (err) {
+ console.error(err);
+ }
+ GPTPopup.Instance.setLoading(false);
+ }
+
+ generateDataAnalysis = async () => {
+ GPTPopup.Instance.setVisible(true);
+ GPTPopup.Instance.setLoading(true);
+ try {
+ let res = await gptAPICall(this.dataJson, GPTCallType.DATA, this.dataChatPrompt);
+ console.log(res)
+ let json = JSON.parse(res! as string);
+ const keys = Object.keys(json)
+ this.correlatedColumns = []
+ this.correlatedColumns.push(json[keys[0]])
+ this.correlatedColumns.push(json[keys[1]])
+ GPTPopup.Instance.setText(json[keys[2]] || 'Something went wrong.');
+ } catch (err) {
+ console.error(err);
+ }
+ GPTPopup.Instance.setLoading(false);
+ }
+
/**
* Transfers the summarization text to a sidebar annotation text document.
*/
@@ -150,6 +204,13 @@ export class GPTPopup extends ObservableReactComponent<GPTPopupProps> {
};
/**
+ * Creates a histogram to show the correlation relationship that was found
+ */
+ private createVisualization = () => {
+ this.createFilteredDoc(this.correlatedColumns);
+ };
+
+ /**
* Transfers the image urls to actual image docs
*/
private transferToImage = (source: string) => {
@@ -174,6 +235,16 @@ export class GPTPopup extends ObservableReactComponent<GPTPopupProps> {
DocUtils.MakeLink(textAnchor, newDoc, { link_relationship: 'Image Prompt' });
};
+ /**
+ * Creates a chatbox for analyzing data so that users can ask specific questions.
+ */
+ private chatWithAI = () => {
+ this.chatMode = true;
+ }
+ dataPromptChanged = action((e: React.ChangeEvent<HTMLInputElement>) => {
+ this.dataChatPrompt = e.target.value;
+ });
+
private getPreviewUrl = (source: string) => source.split('.').join('_m.');
constructor(props: GPTPopupProps) {
@@ -213,31 +284,6 @@ export class GPTPopup extends ObservableReactComponent<GPTPopupProps> {
);
};
- data = () => {
- return (
- <div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}>
- {this.heading('GENERATED IMAGE')}
- <div className="image-content-wrapper">
- {this.imgUrls.map(rawSrc => (
- <div className="img-wrapper">
- <div className="img-container">
- <img key={rawSrc[0]} src={rawSrc[0]} width={150} height={150} alt="dalle generation" />
- </div>
- <div className="btn-container">
- <Button text="Save Image" onClick={() => this.transferToImage(rawSrc[1])} color={StrCast(Doc.UserDoc().userColor)} type={Type.TERT} />
- </div>
- </div>
- ))}
- </div>
- {!this.loading && (
- <>
- <IconButton tooltip="Generate Again" onClick={this.generateImage} icon={<FontAwesomeIcon icon="redo-alt" size="lg" />} color={StrCast(Doc.UserDoc().userVariantColor)} />
- </>
- )}
- </div>
- );
- };
-
summaryBox = () => (
<>
<div>
@@ -255,7 +301,6 @@ export class GPTPopup extends ObservableReactComponent<GPTPopupProps> {
}, 500);
},
]}
- //cursor={{ hideWhenDone: true }}
/>
) : (
this.text
@@ -266,7 +311,7 @@ export class GPTPopup extends ObservableReactComponent<GPTPopupProps> {
<div className="btns-wrapper">
{this.done ? (
<>
- <IconButton tooltip="Generate Again" onClick={this.callSummaryApi} icon={<FontAwesomeIcon icon="redo-alt" size="lg" />} color={StrCast(Doc.UserDoc().userVariantColor)} />
+ <IconButton tooltip="Generate Again" onClick={this.generateSummary} icon={<FontAwesomeIcon icon="redo-alt" size="lg" />} color={StrCast(Doc.UserDoc().userVariantColor)} />
<Button tooltip="Transfer to text" text="Transfer To Text" onClick={this.transferToText} color={StrCast(Doc.UserDoc().userVariantColor)} type={Type.TERT} />
</>
) : (
@@ -288,6 +333,66 @@ export class GPTPopup extends ObservableReactComponent<GPTPopupProps> {
</>
);
+ dataAnalysisBox = () => (
+ <>
+ <div>
+ {this.heading("ANALYSIS")}
+ <div className="content-wrapper">
+ {!this.loading &&
+ (!this.done ? (
+ <TypeAnimation
+ speed={50}
+ sequence={[
+ this.text,
+ () => {
+ setTimeout(() => {
+ this.setDone(true);
+ }, 500);
+ },
+ ]}
+ />
+ ) : (
+ this.text
+ ))}
+ </div>
+ </div>
+ {!this.loading && (
+ <div className="btns-wrapper">
+ {this.done?
+ this.chatMode?(
+ <input
+ defaultValue=""
+ autoComplete="off"
+ onChange={this.dataPromptChanged}
+ onKeyDown={e => {
+ e.key === 'Enter' ? this.generateDataAnalysis() : null;
+ e.stopPropagation();
+ }}
+ type="text"
+ placeholder="Ask GPT a question about the data..."
+ id="search-input"
+ className="searchBox-input"
+ style={{width: "100%"}}
+ />
+ )
+ :(
+ <>
+ <Button tooltip="Transfer to text" text="Transfer To Text" onClick={this.transferToText} color={StrCast(Doc.UserDoc().userVariantColor)} type={Type.TERT} />
+ <Button tooltip="Create a graph to visualize the correlation results" text="Visualize" onClick={this.createVisualization} color={StrCast(Doc.UserDoc().userVariantColor)} type={Type.TERT} />
+ <Button tooltip="Chat with AI" text="Chat with AI" onClick={this.chatWithAI} color={StrCast(Doc.UserDoc().userVariantColor)} type={Type.TERT} />
+ </>
+ ) : (
+ <div className="summarizing">
+ <span>Summarizing</span>
+ <ReactLoading type="bubbles" color="#bcbcbc" width={20} height={20} />
+ <Button text="Stop Animation" onClick={() => {this.setDone(true);}} color={StrCast(Doc.UserDoc().userVariantColor)} type={Type.TERT}/>
+ </div>
+ )}
+ </div>
+ )}
+ </>
+ );
+
aiWarning = () =>
this.done ? (
<div className="ai-warning">
@@ -308,7 +413,7 @@ export class GPTPopup extends ObservableReactComponent<GPTPopupProps> {
render() {
return (
<div className="summary-box" style={{ display: this.visible ? 'flex' : 'none' }}>
- {this.mode === GPTPopupMode.SUMMARY ? this.summaryBox() : this.mode === GPTPopupMode.IMAGE ? this.imageBox() : <></>}
+ {this.mode === GPTPopupMode.SUMMARY? this.summaryBox() : this.mode === GPTPopupMode.DATA? this.dataAnalysisBox() : this.mode === GPTPopupMode.IMAGE ? this.imageBox() : <></>}
</div>
);
}