aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/client/apis/gpt/GPT.ts33
-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.tsx35
-rw-r--r--src/client/views/nodes/DataVizBox/components/Chart.scss63
-rw-r--r--src/client/views/nodes/DataVizBox/components/TableBox.tsx201
-rw-r--r--src/client/views/nodes/formattedText/FormattedTextBox.tsx19
-rw-r--r--src/client/views/pdf/AnchorMenu.tsx15
-rw-r--r--src/client/views/pdf/GPTPopup/GPTPopup.tsx152
9 files changed, 433 insertions, 94 deletions
diff --git a/src/client/apis/gpt/GPT.ts b/src/client/apis/gpt/GPT.ts
index fb51278ae..30194f9f8 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,16 @@ 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." },
};
/**
@@ -25,7 +33,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 +42,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,
temperature: opts.temp,
- prompt: `${opts.prompt}${inputText}`,
+ messages,
});
- 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 +76,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..60c5fdba2 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,7 @@ import { LineChart } from './components/LineChart';
import { PieChart } from './components/PieChart';
import { TableBox } from './components/TableBox';
import { Checkbox } from '@mui/material';
+import { ContextMenu } from '../../ContextMenu';
export enum DataVizView {
TABLE = 'table',
@@ -43,6 +44,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,8 +402,28 @@ 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.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();
+ });
render() {
const scale = this._props.NativeDimScaling?.() || 1;
@@ -419,6 +441,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 +451,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..67e1c67bd 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 { undoable } 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,7 +163,8 @@ export class TableBox extends ObservableReactComponent<TableBoxProps> {
},
emptyFunction,
action(e => {
- if (e.shiftKey) {
+ 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);
@@ -170,6 +179,134 @@ export class TableBox extends ObservableReactComponent<TableBoxProps> {
);
};
+ /**
+ * These functions handle the filtering popup for when the "filter" button is pressed to select rows
+ */
+ filter = undoable((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 = ['', ''];
+ }, 'filter table');
+ @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 * 0.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 * 0.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 * 0.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 +320,39 @@ 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]}`}
@@ -227,17 +394,15 @@ export class TableBox extends ObservableReactComponent<TableBoxProps> {
: 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
+ 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 a2db2a1cc..43010b2ed 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';
@@ -983,10 +983,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
animateRes = (resIndex: number, newText: string) => {
if (resIndex < newText.length) {
const marks = this._editorView?.state.storedMarks ?? [];
- this._editorView?.dispatch(this._editorView.state.tr.setStoredMarks(marks).insertText(newText[resIndex]).setStoredMarks(marks));
- setTimeout(() => {
- this.animateRes(resIndex + 1, newText);
- }, 20);
+ this._editorView?.dispatch(this._editorView?.state.tr.insertText(newText[resIndex]).setStoredMarks(marks));
+ setTimeout(() => this.animateRes(resIndex + 1, newText), 20);
}
};
@@ -994,13 +992,16 @@ 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);
+ } else if (this._editorView) {
+ const { dispatch, state } = this._editorView;
+ // for no animation, use: dispatch(state.tr.insertText(res));
+ // for animted response starting at end of text, use:
+ dispatch(state.tr.setSelection(Selection.atEnd(state.doc)));
+ 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..29b1ca365 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,7 +10,7 @@ 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';
@@ -20,6 +20,7 @@ export enum GPTPopupMode {
SUMMARY,
EDIT,
IMAGE,
+ DATA,
}
interface GPTPopupProps {}
@@ -27,6 +28,7 @@ interface GPTPopupProps {}
@observer
export class GPTPopup extends ObservableReactComponent<GPTPopupProps> {
static Instance: GPTPopup;
+ @observable private chatMode: boolean = false;
@observable
public visible: boolean = false;
@@ -46,6 +48,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 +95,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
@@ -118,18 +135,46 @@ 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);
+ GPTPopup.Instance.setText(res || 'Something went wrong.');
+ } catch (err) {
+ console.error(err);
+ }
+ GPTPopup.Instance.setLoading(false);
+ }
+
/**
* Transfers the summarization text to a sidebar annotation text document.
*/
@@ -174,6 +219,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 +268,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 +285,6 @@ export class GPTPopup extends ObservableReactComponent<GPTPopupProps> {
}, 500);
},
]}
- //cursor={{ hideWhenDone: true }}
/>
) : (
this.text
@@ -266,7 +295,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 +317,65 @@ 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="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 +396,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>
);
}