aboutsummaryrefslogtreecommitdiff
path: root/src/client/views/nodes
diff options
context:
space:
mode:
Diffstat (limited to 'src/client/views/nodes')
-rw-r--r--src/client/views/nodes/DataVizBox/DataVizBox.tsx16
-rw-r--r--src/client/views/nodes/DataVizBox/SchemaCSVPopUp.tsx6
-rw-r--r--src/client/views/nodes/DataVizBox/components/Histogram.tsx17
-rw-r--r--src/client/views/nodes/DataVizBox/components/LineChart.tsx25
-rw-r--r--src/client/views/nodes/DataVizBox/components/TableBox.tsx2
-rw-r--r--src/client/views/nodes/EquationBox.tsx1
-rw-r--r--src/client/views/nodes/FieldView.tsx9
-rw-r--r--src/client/views/nodes/FontIconBox/FontIconBox.tsx9
-rw-r--r--src/client/views/nodes/FunctionPlotBox.tsx11
-rw-r--r--src/client/views/nodes/ImageBox.scss36
-rw-r--r--src/client/views/nodes/ImageBox.tsx371
-rw-r--r--src/client/views/nodes/LinkBox.tsx1
-rw-r--r--src/client/views/nodes/MapBox/DirectionsAnchorMenu.tsx2
-rw-r--r--src/client/views/nodes/calendarBox/CalendarBox.tsx58
-rw-r--r--src/client/views/nodes/chatbot/chatboxcomponents/ChatBox.tsx20
-rw-r--r--src/client/views/nodes/chatbot/tools/GetDocsTool.ts5
-rw-r--r--src/client/views/nodes/chatbot/vectorstore/Vectorstore.ts15
-rw-r--r--src/client/views/nodes/formattedText/DailyJournal.tsx2
-rw-r--r--src/client/views/nodes/formattedText/FormattedTextBox.tsx65
-rw-r--r--src/client/views/nodes/formattedText/RichTextRules.ts3
-rw-r--r--src/client/views/nodes/imageEditor/imageMeshTool/imageMeshToolButton.tsx15
-rw-r--r--src/client/views/nodes/scrapbook/EmbeddedDocView.tsx52
-rw-r--r--src/client/views/nodes/scrapbook/ScrapbookBox.tsx143
-rw-r--r--src/client/views/nodes/scrapbook/ScrapbookContent.tsx23
-rw-r--r--src/client/views/nodes/scrapbook/ScrapbookSlot.scss85
-rw-r--r--src/client/views/nodes/scrapbook/ScrapbookSlot.tsx28
-rw-r--r--src/client/views/nodes/scrapbook/ScrapbookSlotTypes.ts25
27 files changed, 819 insertions, 226 deletions
diff --git a/src/client/views/nodes/DataVizBox/DataVizBox.tsx b/src/client/views/nodes/DataVizBox/DataVizBox.tsx
index c80e8ebc1..9369ff98a 100644
--- a/src/client/views/nodes/DataVizBox/DataVizBox.tsx
+++ b/src/client/views/nodes/DataVizBox/DataVizBox.tsx
@@ -108,7 +108,7 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
// all CSV records in the dataset (that aren't an empty row)
@computed.struct get records() {
try {
- const records = DataVizBox.dataset.get(CsvCast(this.dataDoc[this.fieldKey]).url.href);
+ const records = DataVizBox.dataset.get(CsvCast(this.dataDoc[this.fieldKey])?.url.href ?? '');
this._urlError = false;
return records?.filter(record => Object.keys(record).some(key => record[key])) ?? [];
} catch {
@@ -344,7 +344,7 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
componentDidMount() {
this._props.setContentViewBox?.(this);
if (!this._urlError) {
- if (!DataVizBox.dataset.has(CsvCast(this.dataDoc[this.fieldKey]).url.href)) this.fetchData();
+ if (!DataVizBox.dataset.has(CsvCast(this.dataDoc[this.fieldKey])?.url.href ?? '')) this.fetchData();
}
this._disposers.datavis = reaction(
() => {
@@ -386,8 +386,8 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
this.layoutDoc._dataViz_schemaOG = loading;
}
const ogDoc = this.layoutDoc._dataViz_schemaOG as Doc;
- const ogHref = CsvCast(ogDoc[this.fieldKey]) ? CsvCast(ogDoc[this.fieldKey]).url.href : undefined;
- const { href } = CsvCast(this.Document[this.fieldKey]).url;
+ const ogHref = CsvCast(ogDoc[this.fieldKey]) ? CsvCast(ogDoc[this.fieldKey])!.url.href : undefined;
+ const { href } = CsvCast(this.Document[this.fieldKey])?.url ?? { href: '' };
if (ogHref && !DataVizBox.datasetSchemaOG.has(href)) {
// sets original dataset to the var
const lastRow = current.pop();
@@ -399,7 +399,7 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
},
current => {
if (current) {
- const { href } = CsvCast(this.Document[this.fieldKey]).url;
+ const { href } = CsvCast(this.Document[this.fieldKey])?.url ?? { href: '' };
if (this.layoutDoc.dataViz_schemaLive) DataVizBox.dataset.set(href, current);
else DataVizBox.dataset.set(href, DataVizBox.datasetSchemaOG.get(href)!);
}
@@ -414,9 +414,9 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
fetchData = () => {
if (!this.Document.dataViz_asSchema) {
- DataVizBox.dataset.set(CsvCast(this.dataDoc[this.fieldKey]).url.href, []); // assign temporary dataset as a lock to prevent duplicate server requests
+ DataVizBox.dataset.set(CsvCast(this.dataDoc[this.fieldKey])?.url.href ?? '', []); // assign temporary dataset as a lock to prevent duplicate server requests
fetch('/csvData?uri=' + (this.dataUrl?.url.href ?? '')) //
- .then(res => res.json().then(action(jsonRes => !jsonRes.errno && DataVizBox.dataset.set(CsvCast(this.dataDoc[this.fieldKey]).url.href, jsonRes))));
+ .then(res => res.json().then(action(jsonRes => !jsonRes.errno && DataVizBox.dataset.set(CsvCast(this.dataDoc[this.fieldKey])?.url.href ?? '', jsonRes))));
}
};
@@ -523,7 +523,7 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
GPTPopup.Instance.createFilteredDoc = this.createFilteredDoc;
GPTPopup.Instance.setDataJson('');
GPTPopup.Instance.setMode(GPTPopupMode.DATA);
- const csvdata = DataVizBox.dataset.get(CsvCast(this.dataDoc[this.fieldKey]).url.href);
+ const csvdata = DataVizBox.dataset.get(CsvCast(this.dataDoc[this.fieldKey])?.url.href ?? '');
GPTPopup.Instance.setDataJson(JSON.stringify(csvdata));
GPTPopup.Instance.generateDataAnalysis();
});
diff --git a/src/client/views/nodes/DataVizBox/SchemaCSVPopUp.tsx b/src/client/views/nodes/DataVizBox/SchemaCSVPopUp.tsx
index 8ae29a88c..1e2a95d31 100644
--- a/src/client/views/nodes/DataVizBox/SchemaCSVPopUp.tsx
+++ b/src/client/views/nodes/DataVizBox/SchemaCSVPopUp.tsx
@@ -11,10 +11,8 @@ import { DragManager } from '../../../util/DragManager';
import { DocumentView } from '../DocumentView';
import './SchemaCSVPopUp.scss';
-interface SchemaCSVPopUpProps {}
-
@observer
-export class SchemaCSVPopUp extends React.Component<SchemaCSVPopUpProps> {
+export class SchemaCSVPopUp extends React.Component<object> {
// eslint-disable-next-line no-use-before-define
static Instance: SchemaCSVPopUp;
@@ -23,7 +21,7 @@ export class SchemaCSVPopUp extends React.Component<SchemaCSVPopUpProps> {
@observable public target: Doc | undefined = undefined;
@observable public visible: boolean = false;
- constructor(props: SchemaCSVPopUpProps) {
+ constructor(props: object) {
super(props);
makeObservable(this);
SchemaCSVPopUp.Instance = this;
diff --git a/src/client/views/nodes/DataVizBox/components/Histogram.tsx b/src/client/views/nodes/DataVizBox/components/Histogram.tsx
index 5450d03b1..a7c4a00b0 100644
--- a/src/client/views/nodes/DataVizBox/components/Histogram.tsx
+++ b/src/client/views/nodes/DataVizBox/components/Histogram.tsx
@@ -5,10 +5,9 @@ import { IReactionDisposer, action, computed, makeObservable, observable, reacti
import { observer } from 'mobx-react';
import * as React from 'react';
import { FaFillDrip } from 'react-icons/fa';
-import { Doc, NumListCast } from '../../../../../fields/Doc';
+import { Doc, NumListCast, StrListCast } from '../../../../../fields/Doc';
import { List } from '../../../../../fields/List';
-import { listSpec } from '../../../../../fields/Schema';
-import { Cast, DocCast, StrCast } from '../../../../../fields/Types';
+import { DocCast, StrCast } from '../../../../../fields/Types';
import { Docs } from '../../../../documents/Documents';
import { undoable } from '../../../../util/UndoManager';
import { ObservableReactComponent } from '../../../ObservableReactComponent';
@@ -108,13 +107,13 @@ export class Histogram extends ObservableReactComponent<HistogramProps> {
}
@computed get defaultBarColor() {
- return Cast(this.layoutDoc.dataViz_histogram_defaultColor, 'string', '#69b3a2');
+ return StrCast(this.layoutDoc.dataViz_histogram_defaultColor, '#69b3a2')!;
}
@computed get barColors() {
- return Cast(this.layoutDoc.dataViz_histogram_barColors, listSpec('string'), null);
+ return StrListCast(this.layoutDoc.dataViz_histogram_barColors);
}
@computed get selectedBins() {
- return Cast(this.layoutDoc.dataViz_histogram_selectedBins, listSpec('number'), null);
+ return NumListCast(this.layoutDoc.dataViz_histogram_selectedBins);
}
@computed get rangeVals(): { xMin?: number; xMax?: number; yMin?: number; yMax?: number } {
@@ -129,7 +128,7 @@ export class Histogram extends ObservableReactComponent<HistogramProps> {
// restore selected bars
this._histogramSvg?.selectAll('rect').attr('class', dIn => {
const d = dIn as HistogramData;
- if (this.selectedBins.some(selBin => d[0] === selBin)) {
+ if (this.selectedBins?.some(selBin => d[0] === selBin)) {
this._selectedBars.push(d);
return 'histogram-bar hover';
}
@@ -199,10 +198,10 @@ export class Histogram extends ObservableReactComponent<HistogramProps> {
const alreadySelected = this._selectedBars.findIndex(eachData => !Object.keys(d).some(key => d[key] !== eachData[key]));
if (alreadySelected !== -1) {
this._selectedBars.splice(alreadySelected, 1);
- this.selectedBins.splice(alreadySelected, 1);
+ this.selectedBins?.splice(alreadySelected, 1);
} else {
this._selectedBars.push(d);
- this.selectedBins.push(d[0] as number);
+ this.selectedBins?.push(d[0] as number);
}
const showSelectedLabel = (dataset: HistogramData[]) => {
const datum = dataset.lastElement();
diff --git a/src/client/views/nodes/DataVizBox/components/LineChart.tsx b/src/client/views/nodes/DataVizBox/components/LineChart.tsx
index 6b047546c..80fadf178 100644
--- a/src/client/views/nodes/DataVizBox/components/LineChart.tsx
+++ b/src/client/views/nodes/DataVizBox/components/LineChart.tsx
@@ -26,7 +26,7 @@ export interface LineChartProps {
layoutDoc: Doc;
axes: string[];
titleCol: string;
- records: { [key: string]: any }[];
+ records: { [key: string]: string }[];
width: number;
height: number;
dataDoc: Doc;
@@ -47,7 +47,7 @@ export class LineChart extends ObservableReactComponent<LineChartProps> {
@observable _currSelected: DataPoint | undefined = undefined;
// TODO: nda - some sort of mapping that keeps track of the annotated points so we can easily remove when annotations list updates
- constructor(props: any) {
+ constructor(props: LineChartProps) {
super(props);
makeObservable(this);
}
@@ -79,7 +79,7 @@ export class LineChart extends ObservableReactComponent<LineChartProps> {
@computed get incomingHighlited() {
// return selected x and y axes
// otherwise, use the selection of whatever is linked to us
- const incomingVizBox = DocumentView.getFirstDocumentView(this.parentViz)?.ComponentView as DataVizBox;
+ const incomingVizBox = this.parentViz && (DocumentView.getFirstDocumentView(this.parentViz)?.ComponentView as DataVizBox);
const highlitedRowIds = NumListCast(incomingVizBox?.layoutDoc?.dataViz_highlitedRows);
return this._tableData.filter((record, i) => highlitedRowIds.includes(this._tableDataIds[i])); // get all the datapoints they have selected field set by incoming anchor
}
@@ -170,8 +170,8 @@ export class LineChart extends ObservableReactComponent<LineChartProps> {
}
});
if (!ptWasSelected) {
- selectedDatapoints.push(d.x + ',' + d.y);
- this._currSelected = selectedDatapoints.length > 1 ? undefined : d;
+ selectedDatapoints?.push(d.x + ',' + d.y);
+ this._currSelected = (selectedDatapoints?.length ?? 0 > 1) ? undefined : d;
}
// for filtering child dataviz docs
@@ -190,7 +190,14 @@ export class LineChart extends ObservableReactComponent<LineChartProps> {
}
}
- drawDataPoints(data: DataPoint[], idx: number, xScale: d3.ScaleLinear<number, number, never>, yScale: d3.ScaleLinear<number, number, never>, higlightFocusPt: any, tooltip: any) {
+ drawDataPoints(
+ data: DataPoint[], //
+ idx: number,
+ xScale: d3.ScaleLinear<number, number, never>,
+ yScale: d3.ScaleLinear<number, number, never>,
+ higlightFocusPt: d3.Selection<SVGGElement, unknown, null, undefined>,
+ tooltip: d3.Selection<HTMLDivElement, unknown, null, undefined>
+ ) {
if (this._lineChartSvg) {
const circleClass = '.circle-' + idx;
this._lineChartSvg
@@ -211,7 +218,7 @@ export class LineChart extends ObservableReactComponent<LineChartProps> {
.on('mouseleave', () => {
tooltip?.transition().duration(300).style('opacity', 0);
})
- .on('click', (e: any) => {
+ .on('click', e => {
const d0 = { x: Number(e.target.getAttribute('data-x')), y: Number(e.target.getAttribute('data-y')) };
// find .circle-d1 with data-x = d0.x and data-y = d0.y
this.setCurrSelected(d0);
@@ -220,7 +227,7 @@ export class LineChart extends ObservableReactComponent<LineChartProps> {
}
}
- drawChart = (dataSet: any[][], rangeVals: { xMin?: number; xMax?: number; yMin?: number; yMax?: number }, width: number, height: number) => {
+ drawChart = (dataSet: { x: number; y: number }[][], rangeVals: { xMin?: number; xMax?: number; yMin?: number; yMax?: number }, width: number, height: number) => {
// clearing tooltip and the current chart
d3.select(this._lineChartRef).select('svg').remove();
d3.select(this._lineChartRef).select('.tooltip').remove();
@@ -277,7 +284,7 @@ export class LineChart extends ObservableReactComponent<LineChartProps> {
svg.append('path').attr('stroke', 'red');
// legend
- const color: any = d3.scaleOrdinal().range(['black', 'blue']).domain([this._props.axes[1], this._props.axes[2]]);
+ const color = d3.scaleOrdinal().range(['black', 'blue']).domain([this._props.axes[1], this._props.axes[2]]);
svg.selectAll('mydots')
.data([this._props.axes[1], this._props.axes[2]])
.enter()
diff --git a/src/client/views/nodes/DataVizBox/components/TableBox.tsx b/src/client/views/nodes/DataVizBox/components/TableBox.tsx
index b73123691..ad2731109 100644
--- a/src/client/views/nodes/DataVizBox/components/TableBox.tsx
+++ b/src/client/views/nodes/DataVizBox/components/TableBox.tsx
@@ -128,7 +128,7 @@ export class TableBox extends ObservableReactComponent<TableBoxProps> {
selected.splice(selected.indexOf(rowId), 1);
} else selected?.push(rowId);
e.stopPropagation();
- this.hasRowsToFilter = selected.length > 0;
+ this.hasRowsToFilter = (selected?.length ?? 0) > 0;
};
columnPointerDown = (e: React.PointerEvent, col: string) => {
diff --git a/src/client/views/nodes/EquationBox.tsx b/src/client/views/nodes/EquationBox.tsx
index dcc6e27ed..3cacb6692 100644
--- a/src/client/views/nodes/EquationBox.tsx
+++ b/src/client/views/nodes/EquationBox.tsx
@@ -1,7 +1,6 @@
import { action, computed, makeObservable, reaction } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
-import { Doc } from '../../../fields/Doc';
import { NumCast, StrCast } from '../../../fields/Types';
import { TraceMobx } from '../../../fields/util';
import { DocUtils } from '../../documents/DocUtils';
diff --git a/src/client/views/nodes/FieldView.tsx b/src/client/views/nodes/FieldView.tsx
index de49f502f..a6872f8dc 100644
--- a/src/client/views/nodes/FieldView.tsx
+++ b/src/client/views/nodes/FieldView.tsx
@@ -6,16 +6,17 @@ import { DateField } from '../../../fields/DateField';
import { Doc, Field, FieldType, Opt } from '../../../fields/Doc';
import { List } from '../../../fields/List';
import { ScriptField } from '../../../fields/ScriptField';
+import { WebField } from '../../../fields/URLField';
+import { DragManager } from '../../util/DragManager';
import { dropActionType } from '../../util/DropActionTypes';
import { Transform } from '../../util/Transform';
+import { ContextMenuProps } from '../ContextMenuItem';
import { PinProps } from '../PinFuncs';
import { ViewBoxInterface } from '../ViewBoxInterface';
import { DocumentView } from './DocumentView';
import { FocusViewOptions } from './FocusViewOptions';
-import { OpenWhere } from './OpenWhere';
-import { WebField } from '../../../fields/URLField';
-import { ContextMenuProps } from '../ContextMenuItem';
import { FormattedTextBox } from './formattedText/FormattedTextBox';
+import { OpenWhere } from './OpenWhere';
export type FocusFuncType = (doc: Doc, options: FocusViewOptions) => Opt<number>;
export type StyleProviderFuncType = (
@@ -68,7 +69,7 @@ export interface FieldViewSharedProps {
isGroupActive?: () => string | undefined; // is this document part of a group that is active
// eslint-disable-next-line no-use-before-define
setContentViewBox?: (view: ViewBoxInterface<FieldViewProps>) => void; // called by rendered field's viewBox so that DocumentView can make direct calls to the viewBox
-
+ rejectDrop?: (de: DragManager.DropEvent, subView?: DocumentView) => boolean; // whether a document drop is rejected
PanelWidth: () => number;
PanelHeight: () => number;
isDocumentActive?: () => boolean | undefined; // whether a document should handle pointer events
diff --git a/src/client/views/nodes/FontIconBox/FontIconBox.tsx b/src/client/views/nodes/FontIconBox/FontIconBox.tsx
index 3190757e2..a3167ee06 100644
--- a/src/client/views/nodes/FontIconBox/FontIconBox.tsx
+++ b/src/client/views/nodes/FontIconBox/FontIconBox.tsx
@@ -117,10 +117,10 @@ export class FontIconBox extends ViewBoxBaseComponent<ButtonProps>() {
default: type = 'slider';
break;
} // prettier-ignore
- const numScript = (value?: number) => ScriptCast(this.Document.script).script.run({ this: this.Document, value, _readOnly_: value === undefined });
+ const numScript = (value?: number) => ScriptCast(this.Document.script)?.script.run({ this: this.Document, value, _readOnly_: value === undefined });
const color = this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.Color) as string;
// Script for checking the outcome of the toggle
- const checkResult = Number(Number(numScript().result ?? 0).toPrecision(NumCast(this.dataDoc.numPrecision, 3)));
+ const checkResult = Number(Number(numScript()?.result ?? 0).toPrecision(NumCast(this.dataDoc.numPrecision, 3)));
return (
<NumberDropdown
@@ -207,6 +207,7 @@ export class FontIconBox extends ViewBoxBaseComponent<ButtonProps>() {
*/
@computed get dropdownListButton() {
const script = ScriptCast(this.Document.script);
+ if (!script) return null;
const selectedFunc = () => script?.script.run({ this: this.Document, value: '', _readOnly_: true }).result as string;
const { buttonList, selectedVal, getStyle, jsx, toolTip } = (() => {
switch (this.Document.title) {
@@ -291,7 +292,7 @@ export class FontIconBox extends ViewBoxBaseComponent<ButtonProps>() {
const color = this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.Color) as string;
const background = this._props.styleProvider?.(this.Document, this._props, StyleProp.BackgroundColor) as string;
const items = DocListCast(this.dataDoc.data);
- const selectedItems = items.filter(itemDoc => ScriptCast(itemDoc.onClick).script.run({ this: itemDoc, value: undefined, _readOnly_: true }).result).map(item => StrCast(item.toolType));
+ const selectedItems = items.filter(itemDoc => ScriptCast(itemDoc.onClick)?.script.run({ this: itemDoc, value: undefined, _readOnly_: true }).result).map(item => StrCast(item.toolType));
return (
<MultiToggle
@@ -316,7 +317,7 @@ export class FontIconBox extends ViewBoxBaseComponent<ButtonProps>() {
// it would be better to pas the 'added' flag to the callback script, but our script generator from currentUserUtils makes it hard to define
// arbitrary parameter variables (but it could be done as a special case or with additional effort when creating the sript)
const itemsChanged = items.filter(item => (val instanceof Array ? val.includes(item.toolType as string | number) : item.toolType === val));
- itemsChanged.forEach(itemDoc => ScriptCast(itemDoc.onClick).script.run({ this: itemDoc, _added_: added, value: toggleStatus, itemDoc, _readOnly_: false }));
+ itemsChanged.forEach(itemDoc => ScriptCast(itemDoc.onClick)?.script.run({ this: itemDoc, _added_: added, value: toggleStatus, itemDoc, _readOnly_: false }));
}}
/>
);
diff --git a/src/client/views/nodes/FunctionPlotBox.tsx b/src/client/views/nodes/FunctionPlotBox.tsx
index 91c351895..8e4b64851 100644
--- a/src/client/views/nodes/FunctionPlotBox.tsx
+++ b/src/client/views/nodes/FunctionPlotBox.tsx
@@ -2,10 +2,11 @@ import functionPlot, { Chart } from 'function-plot';
import { action, computed, makeObservable, reaction } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
-import { Doc, DocListCast } from '../../../fields/Doc';
+import { returnFalse, setupMoveUpEvents } from '../../../ClientUtils';
+import { emptyFunction } from '../../../Utils';
+import { Doc, DocListCast, NumListCast } from '../../../fields/Doc';
import { List } from '../../../fields/List';
-import { listSpec } from '../../../fields/Schema';
-import { Cast, StrCast } from '../../../fields/Types';
+import { StrCast } from '../../../fields/Types';
import { TraceMobx } from '../../../fields/util';
import { DocUtils } from '../../documents/DocUtils';
import { DocumentType } from '../../documents/DocumentTypes';
@@ -15,8 +16,6 @@ import { undoBatch } from '../../util/UndoManager';
import { ViewBoxAnnotatableComponent } from '../DocComponent';
import { PinDocView, PinProps } from '../PinFuncs';
import { FieldView, FieldViewProps } from './FieldView';
-import { returnFalse, setupMoveUpEvents } from '../../../ClientUtils';
-import { emptyFunction } from '../../../Utils';
@observer
export class FunctionPlotBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
@@ -76,7 +75,7 @@ export class FunctionPlotBox extends ViewBoxAnnotatableComponent<FieldViewProps>
this._plotEle = ele || this._plotEle;
const width = this._props.PanelWidth();
const height = this._props.PanelHeight();
- const xrange = Cast(this.layoutDoc.xRange, listSpec('number'), [-10, 10]);
+ const xrange = NumListCast(this.layoutDoc.xRange, [-10, 10])!;
try {
this._plotEle?.children.length && this._plotEle.removeChild(this._plotEle.children[0]);
this._plot = functionPlot({
diff --git a/src/client/views/nodes/ImageBox.scss b/src/client/views/nodes/ImageBox.scss
index 4a6e8eb49..ac1a6ece9 100644
--- a/src/client/views/nodes/ImageBox.scss
+++ b/src/client/views/nodes/ImageBox.scss
@@ -106,7 +106,7 @@
height: 100%;
img {
object-fit: contain;
- height: 100%;
+ height: fit-content;
}
.imageBox-fadeBlocker,
@@ -241,3 +241,37 @@
color: black;
}
}
+.imageBox-regenerate-dialog {
+ position: fixed;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ background: white;
+ padding: 20px;
+ border-radius: 8px;
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
+ z-index: 10000;
+
+ h3 {
+ margin-top: 0;
+ }
+
+ input {
+ width: 300px;
+ padding: 8px;
+ margin-bottom: 10px;
+ }
+
+ .buttons {
+ display: flex;
+ justify-content: flex-end;
+ gap: 10px;
+
+ .generate-btn {
+ background: #0078d4;
+ color: white;
+ border: none;
+ padding: 8px 16px;
+ }
+ }
+}
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index 0c475b7bb..31a135fa7 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -1,8 +1,8 @@
-import { Button, Colors, Size, Type } from '@dash/components';
+import { Button, Colors, EditableText, Size, Type } from '@dash/components';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { Slider, Tooltip } from '@mui/material';
import axios from 'axios';
-import { action, computed, IReactionDisposer, makeObservable, observable, ObservableMap, reaction } from 'mobx';
+import { action, computed, IReactionDisposer, makeObservable, observable, ObservableMap, reaction, runInAction } from 'mobx';
import { observer } from 'mobx-react';
import { extname } from 'path';
import * as React from 'react';
@@ -13,11 +13,12 @@ import { Doc, DocListCast, Opt } from '../../../fields/Doc';
import { DocData } from '../../../fields/DocSymbols';
import { Id } from '../../../fields/FieldSymbols';
import { InkTool } from '../../../fields/InkField';
+import { List } from '../../../fields/List';
import { ObjectField } from '../../../fields/ObjectField';
+import { ComputedField } from '../../../fields/ScriptField';
import { Cast, DocCast, ImageCast, NumCast, RTFCast, StrCast } from '../../../fields/Types';
import { ImageField } from '../../../fields/URLField';
import { TraceMobx } from '../../../fields/util';
-import { Upload } from '../../../server/SharedMediaTypes';
import { emptyFunction } from '../../../Utils';
import { Docs } from '../../documents/Documents';
import { DocumentType } from '../../documents/DocumentTypes';
@@ -26,7 +27,7 @@ import { Networking } from '../../Network';
import { DragManager } from '../../util/DragManager';
import { SettingsManager } from '../../util/SettingsManager';
import { SnappingManager } from '../../util/SnappingManager';
-import { undoable, undoBatch } from '../../util/UndoManager';
+import { undoable, undoBatch, UndoManager } from '../../util/UndoManager';
import { CollectionFreeFormView } from '../collections/collectionFreeForm/CollectionFreeFormView';
import { ContextMenu } from '../ContextMenu';
import { ContextMenuProps } from '../ContextMenuItem';
@@ -45,7 +46,6 @@ import { FieldView, FieldViewProps } from './FieldView';
import { FocusViewOptions } from './FocusViewOptions';
import './ImageBox.scss';
import { OpenWhere } from './OpenWhere';
-import { ComputedField } from '../../../fields/ScriptField';
const DefaultPath = '/assets/unknown-file-icon-hi.png';
export class ImageEditorData {
@@ -104,6 +104,10 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
@observable private _regenerateLoading = false;
@observable private _prevImgs: FireflyImageData[] = StrCast(this.Document.ai_firefly_history) ? JSON.parse(StrCast(this.Document.ai_firefly_history)) : [];
+ // Add these observable properties to the ImageBox class
+ @observable private _outpaintingInProgress = false;
+ @observable private _outpaintingPrompt = '';
+
constructor(props: FieldViewProps) {
super(props);
makeObservable(this);
@@ -166,6 +170,11 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
},
{ fireImmediately: true }
);
+ this._disposers.outpaint = reaction(
+ () => this.Document[this.fieldKey + '_outpaintOriginalWidth'] !== undefined && !SnappingManager.ShiftKey,
+ complete => complete && this.openOutpaintPrompt(),
+ { fireImmediately: true }
+ );
}
componentWillUnmount() {
@@ -200,7 +209,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
drop = undoable(
action((e: Event, de: DragManager.DropEvent) => {
- if (de.complete.docDragData) {
+ if (de.complete.docDragData && !this._props.rejectDrop?.(de, this.DocumentView?.())) {
let added: boolean | undefined;
const hitDropTarget = (ele: HTMLElement, dropTarget: HTMLDivElement | null): boolean => {
if (!ele) return false;
@@ -294,7 +303,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
const anchy = NumCast(cropping.y);
const anchw = NumCast(cropping._width);
const anchh = NumCast(cropping._height);
- const viewScale = NumCast(this.dataDoc[this.fieldKey + '_nativeHeight']) / anchh;
+ const viewScale = NumCast(this.dataDoc[this.fieldKey + '_nativeWidth']) / anchw;
cropping.x = NumCast(this.Document.x) + NumCast(this.layoutDoc._width);
cropping.y = NumCast(this.Document.y);
cropping.onClick = undefined;
@@ -339,6 +348,142 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
}
});
+ @observable _showOutpaintPrompt: boolean = false;
+ @observable _outpaintPromptInput: string = 'Extend this image naturally with matching content';
+
+ @action
+ openOutpaintPrompt = () => {
+ this._showOutpaintPrompt = true;
+ };
+
+ @action
+ closeOutpaintPrompt = () => {
+ this._showOutpaintPrompt = false;
+ };
+
+ @action
+ handlePromptChange = (val: string | number) => {
+ this._outpaintPromptInput = '' + val;
+ };
+
+ @action
+ submitOutpaintPrompt = () => {
+ this.closeOutpaintPrompt();
+ this.processOutpaintingWithPrompt(this._outpaintPromptInput);
+ };
+
+ @action
+ processOutpaintingWithPrompt = async (customPrompt: string) => {
+ const field = Cast(this.dataDoc[this.fieldKey], ImageField);
+ if (!field) return;
+
+ const origWidth = NumCast(this.Document[this.fieldKey + '_outpaintOriginalWidth']);
+ const origHeight = NumCast(this.Document[this.fieldKey + '_outpaintOriginalHeight']);
+
+ // Set flag that outpainting is in progress
+ this._outpaintingInProgress = true;
+
+ // Revert dimensions if prompt is blank (acts like Cancel)
+ if (!customPrompt) {
+ this.Document._width = origWidth;
+ this.Document._height = origHeight;
+ this._outpaintingInProgress = false;
+ return;
+ }
+
+ try {
+ const currentPath = this.choosePath(field.url);
+ const newWidth = NumCast(this.Document._width);
+ const newHeight = NumCast(this.Document._height);
+
+ // Optional: add loading indicator
+ const loadingOverlay = document.createElement('div');
+ loadingOverlay.style.position = 'absolute';
+ loadingOverlay.style.top = '0';
+ loadingOverlay.style.left = '0';
+ loadingOverlay.style.width = '100%';
+ loadingOverlay.style.height = '100%';
+ loadingOverlay.style.background = 'rgba(0,0,0,0.5)';
+ loadingOverlay.style.display = 'flex';
+ loadingOverlay.style.justifyContent = 'center';
+ loadingOverlay.style.alignItems = 'center';
+ loadingOverlay.innerHTML = '<div style="color: white; font-size: 16px;">Generating outpainted image...</div>';
+ this._mainCont?.appendChild(loadingOverlay);
+
+ const response = await Networking.PostToServer('/outpaintImage', {
+ imageUrl: currentPath,
+ prompt: customPrompt,
+ originalDimensions: { width: Math.min(newWidth, origWidth), height: Math.min(newHeight, origHeight) },
+ newDimensions: { width: newWidth, height: newHeight },
+ });
+
+ const error = ('error' in response && (response.error as string)) || '';
+ if (error.includes('Dropbox') && confirm('Outpaint image failed. Try authorizing DropBox?\r\n' + error.replace(/^[^"]*/, ''))) {
+ DrawingFillHandler.authorizeDropbox();
+ } else {
+ const batch = UndoManager.StartBatch('outpaint image');
+ if (response && typeof response === 'object' && 'url' in response && typeof response.url === 'string') {
+ if (!this.dataDoc[this.fieldKey + '_alternates']) {
+ this.dataDoc[this.fieldKey + '_alternates'] = new List<Doc>();
+ }
+
+ const originalDoc = Docs.Create.ImageDocument(field.url.href, {
+ title: `Original: ${this.Document.title}`,
+ _nativeWidth: Doc.NativeWidth(this.dataDoc),
+ _nativeHeight: Doc.NativeHeight(this.dataDoc),
+ });
+
+ Doc.AddDocToList(this.dataDoc, this.fieldKey + '_alternates', originalDoc);
+
+ // Replace with new outpainted image
+ this.dataDoc[this.fieldKey] = new ImageField(response.url);
+
+ Doc.SetNativeWidth(this.dataDoc, newWidth);
+ Doc.SetNativeHeight(this.dataDoc, newHeight);
+
+ this.Document.$ai = true;
+ this.Document.$ai_outpainted = true;
+ this.Document.$ai_outpaint_prompt = customPrompt;
+ this.Document[this.fieldKey + '_outpaintOriginalWidth'] = undefined;
+ this.Document[this.fieldKey + '_outpaintOriginalHeight'] = undefined;
+ } else {
+ this.Document._width = origWidth;
+ this.Document._height = origHeight;
+ alert('Failed to receive a valid image URL from server.');
+ }
+ batch.end();
+ }
+
+ this._mainCont?.removeChild(loadingOverlay);
+ } catch (error) {
+ console.error('Error during outpainting:', error);
+ this.Document._width = origWidth;
+ this.Document._height = origHeight;
+ alert('An error occurred while outpainting. Please try again.');
+ } finally {
+ runInAction(() => (this._outpaintingInProgress = false));
+ }
+ };
+
+ componentUI = () =>
+ !this._showOutpaintPrompt ? null : (
+ <div key="imageBox-componentui" className="imageBox-regenerate-dialog" style={{ backgroundColor: SettingsManager.userBackgroundColor, color: SettingsManager.userColor }}>
+ <h3>Outpaint Image</h3>
+ <EditableText
+ placeholder="Enter a prompt for extending the image:"
+ setVal={val => this.handlePromptChange(val)}
+ val={this._outpaintPromptInput}
+ type={Type.TERT}
+ color={SettingsManager.userColor}
+ background={SettingsManager.userBackgroundColor}
+ />
+ <div className="buttons">
+ <Button text="Cancel" type={Type.TERT} onClick={this.closeOutpaintPrompt} color={SnappingManager.userColor} background={SnappingManager.userVariantColor} />
+ <Button text="Generate" type={Type.TERT} onClick={this.submitOutpaintPrompt} color={SnappingManager.userColor} background={SnappingManager.userVariantColor} />
+ </div>
+ </div>
+ );
+
specificContextMenu = (): void => {
const field = Cast(this.dataDoc[this.fieldKey], ImageField);
if (field) {
@@ -346,9 +491,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
funcs.push({ description: 'Rotate Clockwise 90', event: this.rotate, icon: 'redo-alt' });
funcs.push({ description: `Show ${this.layoutDoc._showFullRes ? 'Dynamic Res' : 'Full Res'}`, event: this.resolution, icon: 'expand' });
funcs.push({ description: 'Set Native Pixel Size', event: this.setNativeSize, icon: 'expand-arrows-alt' });
- funcs.push({
- description: 'GetImageText',
- event: () => {
+ funcs.push({ description: 'GetImageText', event: () => {
Networking.PostToServer('/queryFireflyImageText', {
file: (file => {
const ext = file ? extname(file) : '';
@@ -357,25 +500,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
}).then(text => alert(text));
},
icon: 'expand-arrows-alt',
- });
- funcs.push({
- description: 'Expand Image',
- event: () => {
- Networking.PostToServer('/expandImage', {
- prompt: 'sunny skies',
- file: (file => {
- const ext = file ? extname(file) : '';
- return file?.replace(ext, (this._error ? '_o' : this._curSuffix) + ext);
- })(ImageCast(this.Document[Doc.LayoutDataKey(this.Document)])?.url.href),
- }).then(res => {
- const info = res as Upload.ImageInformation;
- const img = Docs.Create.ImageDocument(info.accessPaths.agnostic.client, { title: 'expand:' + this.Document.title });
- DocUtils.assignImageInfo(info, img);
- this._props.addDocTab(img, OpenWhere.addRight);
- });
- },
- icon: 'expand-arrows-alt',
- });
+ }); // prettier-ignore
funcs.push({ description: 'Copy path', event: () => ClientUtils.CopyText(this.choosePath(field.url)), icon: 'copy' });
funcs.push({ description: 'Open Image Editor', event: this.docEditorView, icon: 'pencil-alt' });
this.layoutDoc.ai &&
@@ -396,6 +521,22 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
event: action(undoable(async () => await StickerPalette.addToPalette(this.Document), 'save to palette')),
icon: this.Document.savedAsSticker ? 'clipboard-check' : 'file-arrow-down',
});
+ // Add new outpainting option
+ funcs.push({ description: 'Outpaint Image', event: () => this.openOutpaintPrompt(), icon: 'brush' });
+
+ // Add outpainting history option if the image was outpainted
+ this.Document.ai_outpainted &&
+ funcs.push({
+ description: 'View Original Image',
+ event: action(() => {
+ const alternates = DocListCast(this.dataDoc[this.fieldKey + '_alternates']);
+ if (alternates && alternates.length) {
+ // Toggle to show the original image
+ this.layoutDoc[this.fieldKey + '_usePath'] = 'alternate';
+ }
+ }),
+ icon: 'image',
+ });
ContextMenu.Instance?.addItem({ description: 'Options...', subitems: funcs, icon: 'asterisk' });
}
};
@@ -432,7 +573,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
const ext = extname(url.href);
return url.href.replace(ext, (this._error ? '_o' : this._curSuffix) + ext);
};
- getScrollHeight = () => (this._props.fitWidth?.(this.Document) !== false && NumCast(this.layoutDoc._freeform_scale, 1) === NumCast(this.dataDoc._freeform_scaleMin, 1) ? this.nativeSize.nativeHeight : undefined);
+ getScrollHeight = () => (this._props.fitWidth?.(this.Document) !== false && NumCast(this.layoutDoc._freeform_scale, 1) === NumCast(this.dataDoc.freeform_scaleMin, 1) ? this.nativeSize.nativeHeight : undefined);
@computed get usingAlternate() {
const usePath = StrCast(this.Document[this.fieldKey + '_usePath']);
@@ -583,7 +724,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
ref={action((r: HTMLImageElement | null) => (this.imageRef = r))}
key="paths"
src={srcpath}
- style={{ transform, transformOrigin }}
+ style={{ transform, transformOrigin, height: this.Document[this.fieldKey + '_outpaintOriginalWidth'] !== undefined ? '100%' : undefined }}
onError={action(e => (this._error = e.toString()))}
draggable={false}
width={nativeWidth}
@@ -752,82 +893,90 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
const borderRad = this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.BorderRounding) as string;
const borderRadius = borderRad?.includes('px') ? `${Number(borderRad.split('px')[0]) / (this._props.NativeDimScaling?.() || 1)}px` : borderRad;
return (
- <div
- className="imageBox"
- onContextMenu={this.specificContextMenu}
- ref={this.createDropTarget}
- onScroll={action(() => {
- if (!this._forcedScroll) {
- if (this.layoutDoc._layout_scrollTop || this._mainCont?.scrollTop) {
- this._ignoreScroll = true;
- this.layoutDoc._layout_scrollTop = this._mainCont?.scrollTop;
- this._ignoreScroll = false;
+ <>
+ <div
+ className="imageBox"
+ onContextMenu={this.specificContextMenu}
+ ref={this.createDropTarget}
+ onScroll={action(() => {
+ if (!this._forcedScroll) {
+ if (this.layoutDoc._layout_scrollTop || this._mainCont?.scrollTop) {
+ this._ignoreScroll = true;
+ this.layoutDoc._layout_scrollTop = this._mainCont?.scrollTop;
+ this._ignoreScroll = false;
+ }
}
- }
- })}
- style={{
- width: this._props.PanelWidth() ? undefined : `100%`,
- height: this._props.PanelHeight() ? undefined : `100%`,
- pointerEvents: this.layoutDoc._lockedPosition ? 'none' : undefined,
- borderRadius,
- overflow: this.layoutDoc.layout_fitWidth || this._props.fitWidth?.(this.Document) ? 'auto' : 'hidden',
- }}>
- <CollectionFreeFormView
- ref={this._ffref}
- {...this._props}
- Document={this.Document}
- setContentViewBox={emptyFunction}
- NativeWidth={returnZero}
- NativeHeight={returnZero}
- renderDepth={this._props.renderDepth + 1}
- fieldKey={this.annotationKey}
- styleProvider={this._props.styleProvider}
- isAnnotationOverlay
- annotationLayerHostsContent
- PanelWidth={this._props.PanelWidth}
- PanelHeight={this._props.PanelHeight}
- ScreenToLocalTransform={this.screenToLocalTransform}
- select={emptyFunction}
- focus={this.focus}
- getScrollHeight={this.getScrollHeight}
- NativeDimScaling={returnOne}
- isAnyChildContentActive={returnFalse}
- whenChildContentsActiveChanged={this.whenChildContentsActiveChanged}
- removeDocument={this.removeDocument}
- moveDocument={this.moveDocument}
- addDocument={this.addDocument}>
- {this.content}
- </CollectionFreeFormView>
- {this.Loading ? (
- <div className="loading-spinner" style={{ position: 'absolute' }}>
- <ReactLoading type="spin" height={50} width={50} color={'blue'} />
- </div>
- ) : null}
- {this.regenerateImageIcon}
- {this.overlayImageIcon}
- {this.annotationLayer}
- {!this._mainCont || !this.DocumentView || !this._annotationLayer.current ? null : (
- <MarqueeAnnotator
+ })}
+ style={{
+ width: this._props.PanelWidth() ? undefined : `100%`,
+ height: this._props.PanelHeight() ? undefined : `100%`,
+ pointerEvents: this.layoutDoc._lockedPosition ? 'none' : undefined,
+ borderRadius,
+ overflow: this.layoutDoc.layout_fitWidth || this._props.fitWidth?.(this.Document) ? 'auto' : 'hidden',
+ }}>
+ <CollectionFreeFormView
+ ref={this._ffref}
+ {...this._props}
Document={this.Document}
- ref={this.marqueeref}
- scrollTop={0}
- annotationLayerScrollTop={0}
- scaling={returnOne}
- annotationLayerScaling={this._props.NativeDimScaling}
- screenTransform={this.DocumentView().screenToViewTransform}
- docView={this.DocumentView}
- addDocument={this.addDocument}
- finishMarquee={this.finishMarquee}
- savedAnnotations={this.savedAnnotations}
- selectionText={returnEmptyString}
- annotationLayer={this._annotationLayer.current}
- marqueeContainer={this._mainCont}
- highlightDragSrcColor=""
- anchorMenuCrop={this.crop}
- // anchorMenuFlashcard={() => this.getImageDesc()}
- />
- )}
- </div>
+ setContentViewBox={emptyFunction}
+ NativeWidth={returnZero}
+ NativeHeight={returnZero}
+ renderDepth={this._props.renderDepth + 1}
+ fieldKey={this.annotationKey}
+ styleProvider={this._props.styleProvider}
+ isAnnotationOverlay
+ annotationLayerHostsContent
+ PanelWidth={this._props.PanelWidth}
+ PanelHeight={this._props.PanelHeight}
+ ScreenToLocalTransform={this.screenToLocalTransform}
+ select={emptyFunction}
+ focus={this.focus}
+ rejectDrop={this._props.rejectDrop}
+ getScrollHeight={this.getScrollHeight}
+ NativeDimScaling={returnOne}
+ isAnyChildContentActive={returnFalse}
+ whenChildContentsActiveChanged={this.whenChildContentsActiveChanged}
+ removeDocument={this.removeDocument}
+ moveDocument={this.moveDocument}
+ addDocument={this.addDocument}>
+ {this.content}
+ </CollectionFreeFormView>
+ {this.Loading ? (
+ <div className="loading-spinner" style={{ position: 'absolute' }}>
+ <ReactLoading type="spin" height={50} width={50} color={'blue'} />
+ </div>
+ ) : null}
+ {this.regenerateImageIcon}
+ {this.overlayImageIcon}
+ {this.annotationLayer}
+ {!this._mainCont || !this.DocumentView || !this._annotationLayer.current ? null : (
+ <MarqueeAnnotator
+ Document={this.Document}
+ ref={this.marqueeref}
+ scrollTop={0}
+ annotationLayerScrollTop={0}
+ scaling={returnOne}
+ annotationLayerScaling={this._props.NativeDimScaling}
+ screenTransform={this.DocumentView().screenToViewTransform}
+ docView={this.DocumentView}
+ addDocument={this.addDocument}
+ finishMarquee={this.finishMarquee}
+ savedAnnotations={this.savedAnnotations}
+ selectionText={returnEmptyString}
+ annotationLayer={this._annotationLayer.current}
+ marqueeContainer={this._mainCont}
+ highlightDragSrcColor=""
+ anchorMenuCrop={this.crop}
+ // anchorMenuFlashcard={() => this.getImageDesc()}
+ />
+ )}
+ {this._outpaintingInProgress && (
+ <div className="imageBox-outpaintingSpinner">
+ <ReactLoading type="spin" color="#666" height={60} width={60} />
+ </div>
+ )}
+ </div>
+ </>
);
}
@@ -840,10 +989,14 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
const file = input.files?.[0];
if (file) {
const disposer = OverlayView.ShowSpinner();
- DocUtils.uploadFileToDoc(file, {}, this.Document).then(doc => {
- disposer();
- doc && (doc.height = undefined);
- });
+ const [{ result }] = await Networking.UploadFilesToServer({ file });
+ if (result instanceof Error) {
+ alert('Error uploading files - possibly due to unsupported file types');
+ } else {
+ this.dataDoc[this.fieldKey] = new ImageField(result.accessPaths.agnostic.client);
+ !(result instanceof Error) && DocUtils.assignImageInfo(result, this.dataDoc);
+ }
+ disposer();
} else {
console.log('No file selected');
}
diff --git a/src/client/views/nodes/LinkBox.tsx b/src/client/views/nodes/LinkBox.tsx
index 8bf65b637..78c8a686c 100644
--- a/src/client/views/nodes/LinkBox.tsx
+++ b/src/client/views/nodes/LinkBox.tsx
@@ -128,6 +128,7 @@ export class LinkBox extends ViewBoxBaseComponent<FieldViewProps>() {
const getAnchor = (field: FieldResult): Element[] => {
const docField = DocCast(field);
const doc = docField?.layout_unrendered ? DocCast(docField.annotationOn, docField) : docField;
+ if (!doc) return [];
const ele = document.getElementById(DocumentView.UniquifyId(DocumentView.LightboxContains(this.DocumentView?.()), doc[Id]));
if (ele?.className === 'linkBox-label') foundParent = true;
if (ele?.getBoundingClientRect().width) return [ele];
diff --git a/src/client/views/nodes/MapBox/DirectionsAnchorMenu.tsx b/src/client/views/nodes/MapBox/DirectionsAnchorMenu.tsx
index 8784a709a..8bb7ddd9f 100644
--- a/src/client/views/nodes/MapBox/DirectionsAnchorMenu.tsx
+++ b/src/client/views/nodes/MapBox/DirectionsAnchorMenu.tsx
@@ -43,7 +43,7 @@ export class DirectionsAnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
return this._left > 0;
}
- constructor(props: Readonly<{}>) {
+ constructor(props: AntimodeMenuProps) {
super(props);
DirectionsAnchorMenu.Instance = this;
diff --git a/src/client/views/nodes/calendarBox/CalendarBox.tsx b/src/client/views/nodes/calendarBox/CalendarBox.tsx
index 4ee2413be..84fa25dba 100644
--- a/src/client/views/nodes/calendarBox/CalendarBox.tsx
+++ b/src/client/views/nodes/calendarBox/CalendarBox.tsx
@@ -115,6 +115,7 @@ export class CalendarBox extends CollectionSubView() {
return 'red';
};
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
internalDocDrop = (e: Event, de: DragManager.DropEvent, docDragData: DragManager.DocumentDragData) => {
if (!super.onInternalDrop(e, de)) return false;
de.complete.docDragData?.droppedDocuments.forEach(doc => {
@@ -153,7 +154,6 @@ export class CalendarBox extends CollectionSubView() {
doc.startTime = new DateField(startDate);
doc.endTime = new DateField(endDate);
}
-
};
handleEventClick = (arg: EventClickArg) => {
@@ -201,40 +201,36 @@ export class CalendarBox extends CollectionSubView() {
eventDrop: this.handleEventDrop,
eventResize: this.handleEventDrop,
eventDidMount: arg => {
+ const doc = DocServer.GetCachedRefField(arg.event._def.groupId ?? '');
+ if (!doc) return;
- const doc = DocServer.GetCachedRefField(arg.event._def.groupId ?? '');
- if (!doc) return;
-
- if (doc.type === 'task') {
- const checkButton = document.createElement('button');
- checkButton.innerText = doc.completed ? '✅' : '⬜';
- checkButton.style.position = 'absolute';
- checkButton.style.right = '5px';
- checkButton.style.top = '50%';
- checkButton.style.transform = 'translateY(-50%)';
- checkButton.style.background = 'transparent';
- checkButton.style.border = 'none';
- checkButton.style.cursor = 'pointer';
- checkButton.style.fontSize = '18px';
- checkButton.style.zIndex = '1000';
- checkButton.style.padding = '0';
- checkButton.style.margin = '0';
-
- checkButton.onclick = ev => {
- ev.stopPropagation();
- doc.completed = !doc.completed;
- this._calendar?.refetchEvents();
- };
-
- // Make sure the parent box is positioned relative
- arg.el.style.position = 'relative';
- arg.el.appendChild(checkButton);
- }
-
- // (keep your other pointerup/contextmenu handlers here)
+ if (doc.type === 'task') {
+ const checkButton = document.createElement('button');
+ checkButton.innerText = doc.completed ? '✅' : '⬜';
+ checkButton.style.position = 'absolute';
+ checkButton.style.right = '5px';
+ checkButton.style.top = '50%';
+ checkButton.style.transform = 'translateY(-50%)';
+ checkButton.style.background = 'transparent';
+ checkButton.style.border = 'none';
+ checkButton.style.cursor = 'pointer';
+ checkButton.style.fontSize = '18px';
+ checkButton.style.zIndex = '1000';
+ checkButton.style.padding = '0';
+ checkButton.style.margin = '0';
+ checkButton.onclick = ev => {
+ ev.stopPropagation();
+ doc.completed = !doc.completed;
+ this._calendar?.refetchEvents();
+ };
+ // Make sure the parent box is positioned relative
+ arg.el.style.position = 'relative';
+ arg.el.appendChild(checkButton);
+ }
+ // (keep your other pointerup/contextmenu handlers here)
arg.el.addEventListener('pointerdown', ev => {
ev.button && ev.stopPropagation();
diff --git a/src/client/views/nodes/chatbot/chatboxcomponents/ChatBox.tsx b/src/client/views/nodes/chatbot/chatboxcomponents/ChatBox.tsx
index 528bcd05a..6c3da8977 100644
--- a/src/client/views/nodes/chatbot/chatboxcomponents/ChatBox.tsx
+++ b/src/client/views/nodes/chatbot/chatboxcomponents/ChatBox.tsx
@@ -196,7 +196,7 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
// Add CSV details to linked files
this._linked_csv_files.push({
- filename: CsvCast(newLinkedDoc.data).url.pathname,
+ filename: CsvCast(newLinkedDoc.data)?.url.pathname ?? '',
id: csvId,
text: csvData,
});
@@ -634,6 +634,7 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
};
getDirectMatchingSegmentStart = (doc: Doc, citationText: string, indexesOfSegments: string[]): number => {
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
const originalSegments = JSON.parse(StrCast(doc.original_segments!)).map((segment: any, index: number) => ({
index: index.toString(),
text: segment.text,
@@ -877,7 +878,8 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
return LinkManager.Instance.getAllRelatedLinks(this.Document)
.map(d => DocCast(LinkManager.getOppositeAnchor(d, this.Document)))
.map(d => DocCast(d?.annotationOn, d))
- .filter(d => d);
+ .filter(d => d)
+ .map(d => d!);
}
/**
@@ -889,6 +891,7 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
.map(d => DocCast(LinkManager.getOppositeAnchor(d, this.Document)))
.map(d => DocCast(d?.annotationOn, d))
.filter(d => d)
+ .map(d => d!)
.filter(d => {
console.log(d.ai_doc_id);
return d.ai_doc_id;
@@ -905,15 +908,14 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
LinkManager.Instance.getAllRelatedLinks(this.Document)
.map(d => DocCast(LinkManager.getOppositeAnchor(d, this.Document)))
.map(d => DocCast(d?.annotationOn, d))
- .filter(d => d)
- .filter(d => d.summary)
+ .filter(d => d?.summary)
.map((doc, index) => {
- if (PDFCast(doc.data)) {
- return `<summary file_name="${PDFCast(doc.data).url.pathname}" applicable_tools=["rag"]>${doc.summary}</summary>`;
- } else if (CsvCast(doc.data)) {
- return `<summary file_name="${CsvCast(doc.data).url.pathname}" applicable_tools=["dataAnalysis"]>${doc.summary}</summary>`;
+ if (PDFCast(doc?.data)) {
+ return `<summary file_name="${PDFCast(doc!.data)!.url.pathname}" applicable_tools=["rag"]>${doc!.summary}</summary>`;
+ } else if (CsvCast(doc?.data)) {
+ return `<summary file_name="${CsvCast(doc!.data)!.url.pathname}" applicable_tools=["dataAnalysis"]>${doc!.summary}</summary>`;
} else {
- return `${index + 1}) ${doc.summary}`;
+ return `${index + 1}) ${doc?.summary}`;
}
})
.join('\n') + '\n'
diff --git a/src/client/views/nodes/chatbot/tools/GetDocsTool.ts b/src/client/views/nodes/chatbot/tools/GetDocsTool.ts
index 05482a66e..42a7747d3 100644
--- a/src/client/views/nodes/chatbot/tools/GetDocsTool.ts
+++ b/src/client/views/nodes/chatbot/tools/GetDocsTool.ts
@@ -40,7 +40,10 @@ export class GetDocsTool extends BaseTool<GetDocsToolParamsType> {
}
async execute(args: ParametersType<GetDocsToolParamsType>): Promise<Observation[]> {
- const docs = args.document_ids.map(doc_id => DocCast(DocServer.GetCachedRefField(doc_id)));
+ const docs = args.document_ids
+ .map(doc_id => DocCast(DocServer.GetCachedRefField(doc_id)))
+ .filter(d => d)
+ .map(d => d!);
const collection = Docs.Create.FreeformDocument(docs, { title: args.title });
this._docView._props.addDocTab(collection, OpenWhere.addRight);
return [{ type: 'text', text: `Collection created in Dash called ${args.title}` }];
diff --git a/src/client/views/nodes/chatbot/vectorstore/Vectorstore.ts b/src/client/views/nodes/chatbot/vectorstore/Vectorstore.ts
index c1915a398..6d524e40f 100644
--- a/src/client/views/nodes/chatbot/vectorstore/Vectorstore.ts
+++ b/src/client/views/nodes/chatbot/vectorstore/Vectorstore.ts
@@ -15,7 +15,6 @@ import { Networking } from '../../../../Network';
import { AI_Document, CHUNK_TYPE, RAGChunk } from '../types/types';
import OpenAI from 'openai';
import { Embedding } from 'openai/resources';
-import { PineconeEnvironmentVarsNotSupportedError } from '@pinecone-database/pinecone/dist/errors';
dotenv.config();
@@ -101,7 +100,7 @@ export class Vectorstore {
} else {
// Start processing the document.
doc.ai_document_status = 'PROGRESS';
- const local_file_path: string = CsvCast(doc.data)?.url?.pathname ?? PDFCast(doc.data)?.url?.pathname ?? VideoCast(doc.data)?.url?.pathname ?? AudioCast(doc.data)?.url?.pathname;
+ const local_file_path = CsvCast(doc.data)?.url?.pathname ?? PDFCast(doc.data)?.url?.pathname ?? VideoCast(doc.data)?.url?.pathname ?? AudioCast(doc.data)?.url?.pathname;
if (!local_file_path) {
console.log('Invalid file path.');
@@ -112,13 +111,13 @@ export class Vectorstore {
let result: AI_Document & { doc_id: string };
if (isAudioOrVideo) {
console.log('Processing media file...');
- const response = await Networking.PostToServer('/processMediaFile', { fileName: path.basename(local_file_path) });
+ const response = (await Networking.PostToServer('/processMediaFile', { fileName: path.basename(local_file_path) })) as { [key: string]: unknown };
const segmentedTranscript = response.condensed;
console.log(segmentedTranscript);
- const summary = response.summary;
+ const summary = response.summary as string;
doc.summary = summary;
// Generate embeddings for each chunk
- const texts = segmentedTranscript.map((chunk: any) => chunk.text);
+ const texts = (segmentedTranscript as { text: string }[])?.map(chunk => chunk.text);
try {
const embeddingsResponse = await this.openai.embeddings.create({
@@ -138,7 +137,7 @@ export class Vectorstore {
file_name: local_file_path,
num_pages: 0,
summary: '',
- chunks: segmentedTranscript.map((chunk: any, index: number) => ({
+ chunks: (segmentedTranscript as { text: string; start: number; end: number; indexes: string[] }[]).map((chunk, index) => ({
id: uuidv4(),
values: (embeddingsResponse.data as Embedding[])[index].embedding, // Assign embedding
metadata: {
@@ -173,7 +172,7 @@ export class Vectorstore {
} else {
// Existing document processing logic remains unchanged
console.log('Processing regular document...');
- const { jobId } = await Networking.PostToServer('/createDocument', { file_path: local_file_path });
+ const { jobId } = (await Networking.PostToServer('/createDocument', { file_path: local_file_path })) as { jobId: string };
while (true) {
await new Promise(resolve => setTimeout(resolve, 2000));
@@ -297,7 +296,7 @@ export class Vectorstore {
encoding_format: 'float',
});
- let queryEmbedding = queryEmbeddingResponse.data[0].embedding;
+ const queryEmbedding = queryEmbeddingResponse.data[0].embedding;
// Extract the embedding from the response.
diff --git a/src/client/views/nodes/formattedText/DailyJournal.tsx b/src/client/views/nodes/formattedText/DailyJournal.tsx
index e4127fba3..d6d30dc13 100644
--- a/src/client/views/nodes/formattedText/DailyJournal.tsx
+++ b/src/client/views/nodes/formattedText/DailyJournal.tsx
@@ -206,7 +206,7 @@ export class DailyJournal extends ViewBoxAnnotatableComponent<FieldViewProps>()
// Check if doc or selection changed
if (!prevState.doc.eq(state.doc) || !prevState.selection.eq(state.selection)) {
- let found = false;
+ const found = false;
const textToRemove = this.predictiveText;
state.doc.descendants((node, pos) => {
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index dc23a695d..d6fa3172d 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -586,7 +586,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
return true;
}
const dragData = de.complete.docDragData;
- if (dragData) {
+ if (dragData && !this._props.rejectDrop?.(de, this.DocumentView?.())) {
const layoutProto = DocCast(this.layoutDoc.proto);
const dataDoc = layoutProto && Doc.IsDelegateField(layoutProto, this.fieldKey) ? layoutProto : this.dataDoc;
const effectiveAcl = GetEffectiveAcl(dataDoc);
@@ -1149,7 +1149,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
// Since we also monitor all component height changes, this will update the document's height.
resetNativeHeight = action((scrollHeight: number) => {
this.layoutDoc['_' + this.fieldKey + '_height'] = scrollHeight;
- if (!this.layoutDoc.isTemplateForField) this.layoutDoc._nativeHeight = scrollHeight;
+ if (!this.layoutDoc.isTemplateForField && NumCast(this.layoutDoc._nativeHeight)) this.layoutDoc._nativeHeight = scrollHeight;
});
addPlugin = (plugin: Plugin) => {
@@ -1344,8 +1344,62 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
return text;
};
- handlePaste = (view: EditorView, event: Event /* , slice: Slice */): boolean => {
- const pdfAnchorId = (event as ClipboardEvent).clipboardData?.getData('dash/pdfAnchor');
+ handlePaste = (view: EditorView, event: ClipboardEvent /* , slice: Slice */): boolean => {
+ return this.doPaste(view, event.clipboardData);
+ };
+ doPaste = (view: EditorView, data: DataTransfer | null) => {
+ const html = data?.getData('text/html');
+ const pdfAnchorId = data?.getData('dash/pdfAnchor');
+ if (html && !pdfAnchorId) {
+ const replaceDivsWithParagraphs = (expr: string) => {
+ // Create a temporary DOM container
+ const container = document.createElement('div');
+ container.innerHTML = expr;
+
+ // Recursive function to process all divs
+ function processDivs(node: HTMLElement) {
+ // Get all div elements in the current node (live collection)
+ const divs = node.getElementsByTagName('div');
+
+ // We need to convert to array because we'll be modifying the DOM
+ const divsArray = Array.from(divs);
+
+ for (const div of divsArray) {
+ // Create replacement paragraph
+ const p = document.createElement('p');
+
+ // Copy all attributes
+ for (const attr of div.attributes) {
+ p.setAttribute(attr.name, attr.value);
+ }
+
+ // Move all child nodes
+ while (div.firstChild) {
+ p.appendChild(div.firstChild);
+ }
+
+ // Replace the div with the paragraph
+ div.parentNode?.replaceChild(p, div);
+
+ // Process any nested divs that were moved into the new paragraph
+ processDivs(p);
+ }
+ }
+
+ // Start processing from the container
+ processDivs(container);
+
+ return container.innerHTML;
+ };
+ const fixedHTML = replaceDivsWithParagraphs(html);
+ // .replace(/<div\b([^>]*)>(.*?)<\/div>/g, '<p$1>$2</p>'); // prettier-ignore
+ this._inDrop = true;
+ view.pasteHTML(html.split('<p').length < 2 ? fixedHTML : html);
+ this._inDrop = false;
+
+ return true;
+ }
+
return !!(pdfAnchorId && this.addPdfReference(pdfAnchorId));
};
@@ -1485,9 +1539,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
}
if (this._props.isContentActive()) this.prepareForTyping();
if (this.EditorView && FormattedTextBox.PasteOnLoad) {
- const pdfAnchorId = FormattedTextBox.PasteOnLoad.clipboardData?.getData('dash/pdfAnchor');
+ this.doPaste(this.EditorView, FormattedTextBox.PasteOnLoad.clipboardData);
FormattedTextBox.PasteOnLoad = undefined;
- pdfAnchorId && this.addPdfReference(pdfAnchorId);
}
if (this._props.autoFocus) setTimeout(() => this.EditorView!.focus()); // not sure why setTimeout is needed but editing dashFieldView's doesn't work without it.
}
diff --git a/src/client/views/nodes/formattedText/RichTextRules.ts b/src/client/views/nodes/formattedText/RichTextRules.ts
index 26ccf6931..f26a75fe4 100644
--- a/src/client/views/nodes/formattedText/RichTextRules.ts
+++ b/src/client/views/nodes/formattedText/RichTextRules.ts
@@ -349,7 +349,8 @@ export class RichTextRules {
let count = 0; // ignore first return value which will be the notation that chat is pending a result
Doc.SetField(this.Document, '', match[2], false, (gptval: FieldResult) => {
if (count) {
- const tr = this.TextBox.EditorView?.state.tr.insertText(' ' + (gptval as string));
+ this.TextBox.EditorView?.pasteText(' ' + (gptval as string), undefined);
+ const tr = this.TextBox.EditorView?.state.tr; //.insertText(' ' + (gptval as string));
tr && this.TextBox.EditorView?.dispatch(tr.setSelection(new TextSelection(tr.doc.resolve(end + 2), tr.doc.resolve(end + 2 + (gptval as string).length))));
RichTextMenu.Instance?.elideSelection(this.TextBox.EditorView?.state, true);
}
diff --git a/src/client/views/nodes/imageEditor/imageMeshTool/imageMeshToolButton.tsx b/src/client/views/nodes/imageEditor/imageMeshTool/imageMeshToolButton.tsx
index eb68410b0..e580c7070 100644
--- a/src/client/views/nodes/imageEditor/imageMeshTool/imageMeshToolButton.tsx
+++ b/src/client/views/nodes/imageEditor/imageMeshTool/imageMeshToolButton.tsx
@@ -13,20 +13,11 @@ interface ButtonContainerProps {
btnText: string;
imageWidth: number;
imageHeight: number;
- gridXSize: number; // X subdivisions
- gridYSize: number; // Y subdivisions
+ gridXSize: number; // X subdivisions
+ gridYSize: number; // Y subdivisions
}
-export function MeshTransformButton({
- loading,
- onClick: startMeshTransform,
- onReset,
- btnText,
- imageWidth,
- imageHeight,
- gridXSize,
- gridYSize
-}: ButtonContainerProps) {
+export function MeshTransformButton({ loading, onClick, onReset, btnText, imageWidth, imageHeight, gridXSize, gridYSize }: ButtonContainerProps) {
const [showGrid, setShowGrid] = React.useState(false);
const [isGridInteractive, setIsGridInteractive] = React.useState(false); // Controls the dragging of control points
const imageRef = React.useRef<HTMLImageElement>(null); // Reference to the image element
diff --git a/src/client/views/nodes/scrapbook/EmbeddedDocView.tsx b/src/client/views/nodes/scrapbook/EmbeddedDocView.tsx
new file mode 100644
index 000000000..e99bf67c7
--- /dev/null
+++ b/src/client/views/nodes/scrapbook/EmbeddedDocView.tsx
@@ -0,0 +1,52 @@
+//IGNORE FOR NOW, CURRENTLY NOT USED IN SCRAPBOOK IMPLEMENTATION
+import * as React from "react";
+import { observer } from "mobx-react";
+import { Doc } from "../../../../fields/Doc";
+import { DocumentView } from "../DocumentView";
+import { Transform } from "../../../util/Transform";
+
+interface EmbeddedDocViewProps {
+ doc: Doc;
+ width?: number;
+ height?: number;
+ slotId?: string;
+}
+
+@observer
+export class EmbeddedDocView extends React.Component<EmbeddedDocViewProps> {
+ render() {
+ const { doc, width = 300, height = 200, slotId } = this.props;
+
+ // Use either an existing embedding or create one
+ let docToDisplay = doc;
+
+ // If we need an embedding, create or use one
+ if (!docToDisplay.isEmbedding) {
+ docToDisplay = Doc.BestEmbedding(doc) || Doc.MakeEmbedding(doc);
+ // Set the container to the slot's ID so we can track it
+ if (slotId) {
+ docToDisplay.embedContainer = `scrapbook-slot-${slotId}`;
+ }
+ }
+
+ return (
+ <DocumentView
+ Document={docToDisplay}
+ renderDepth={0}
+ // Required sizing functions
+ NativeWidth={() => width}
+ NativeHeight={() => height}
+ PanelWidth={() => width}
+ PanelHeight={() => height}
+ // Required state functions
+ isContentActive={() => true}
+ childFilters={() => []}
+ ScreenToLocalTransform={() => new Transform()}
+ // Display options
+ hideDeleteButton={true}
+ hideDecorations={true}
+ hideResizeHandles={true}
+ />
+ );
+ }
+} \ No newline at end of file
diff --git a/src/client/views/nodes/scrapbook/ScrapbookBox.tsx b/src/client/views/nodes/scrapbook/ScrapbookBox.tsx
new file mode 100644
index 000000000..6cfe9a62c
--- /dev/null
+++ b/src/client/views/nodes/scrapbook/ScrapbookBox.tsx
@@ -0,0 +1,143 @@
+import { action, makeObservable, observable } from 'mobx';
+import * as React from 'react';
+import { Doc, DocListCast } from '../../../../fields/Doc';
+import { List } from '../../../../fields/List';
+import { emptyFunction } from '../../../../Utils';
+import { Docs } from '../../../documents/Documents';
+import { DocumentType } from '../../../documents/DocumentTypes';
+import { CollectionView } from '../../collections/CollectionView';
+import { ViewBoxAnnotatableComponent } from '../../DocComponent';
+import { DocumentView } from '../DocumentView';
+import { FieldView, FieldViewProps } from '../FieldView';
+import { DragManager } from '../../../util/DragManager';
+import { RTFCast, StrCast, toList } from '../../../../fields/Types';
+import { undoable } from '../../../util/UndoManager';
+// Scrapbook view: a container that lays out its child items in a grid/template
+export class ScrapbookBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
+ @observable createdDate: string;
+
+ constructor(props: FieldViewProps) {
+ super(props);
+ makeObservable(this);
+ this.createdDate = this.getFormattedDate();
+
+ // ensure we always have a List<Doc> in dataDoc['items']
+ if (!this.dataDoc[this.fieldKey]) {
+ this.dataDoc[this.fieldKey] = new List<Doc>();
+ }
+ this.createdDate = this.getFormattedDate();
+ this.setTitle();
+ }
+
+ public static LayoutString(fieldStr: string) {
+ return FieldView.LayoutString(ScrapbookBox, fieldStr);
+ }
+
+ getFormattedDate(): string {
+ return new Date().toLocaleDateString(undefined, {
+ year: 'numeric',
+ month: 'short',
+ day: 'numeric',
+ });
+ }
+
+ @action
+ setTitle() {
+ const title = `Scrapbook - ${this.createdDate}`;
+ if (this.dataDoc.title !== title) {
+ this.dataDoc.title = title;
+
+ const image = Docs.Create.TextDocument('image');
+ image.accepts_docType = DocumentType.IMG;
+ const placeholder = new Doc();
+ placeholder.proto = image;
+ placeholder.original = image;
+ placeholder._width = 250;
+ placeholder._height = 200;
+ placeholder.x = 0;
+ placeholder.y = -100;
+ //placeholder.overrideFields = new List<string>(['x', 'y']); // shouldn't need to do this for layout fields since the placeholder already overrides its protos
+
+ const summary = Docs.Create.TextDocument('summary');
+ summary.accepts_docType = DocumentType.RTF;
+ summary.accepts_textType = 'one line';
+ const placeholder2 = new Doc();
+ placeholder2.proto = summary;
+ placeholder2.original = summary;
+ placeholder2.x = 0;
+ placeholder2.y = 200;
+ placeholder2._width = 250;
+ //placeholder2.overrideFields = new List<string>(['x', 'y', '_width']); // shouldn't need to do this for layout fields since the placeholder already overrides its protos
+ this.dataDoc[this.fieldKey] = new List<Doc>([placeholder, placeholder2]);
+ }
+ }
+
+ componentDidMount() {
+ this.setTitle();
+ }
+
+ childRejectDrop = (de: DragManager.DropEvent, subView?: DocumentView) => {
+ return true; // disable dropping documents onto any child of the scrapbook.
+ };
+ rejectDrop = (de: DragManager.DropEvent, subView?: DocumentView) => {
+ // Test to see if the dropped doc is dropped on an acceptable location (anywerhe? on a specific box).
+ // const draggedDocs = de.complete.docDragData?.draggedDocuments;
+ return false; // allow all Docs to be dropped onto scrapbook -- let filterAddDocument make the final decision.
+ };
+
+ filterAddDocument = (docIn: Doc | Doc[]) => {
+ const docs = toList(docIn);
+ if (docs?.length === 1) {
+ const placeholder = DocListCast(this.dataDoc[this.fieldKey]).find(d =>
+ (d.accepts_docType === docs[0].$type || // match fields based on type, or by analyzing content .. simple example of matching text in placeholder to dropped doc's type
+ RTFCast(d[Doc.LayoutDataKey(d)])?.Text.includes(StrCast(docs[0].$type)))
+ ); // prettier-ignore
+
+ if (placeholder) {
+ // ugh. we have to tell the underlying view not to add the Doc so that we can add it where we want it.
+ // However, returning 'false' triggers an undo. so this settimeout is needed to make the assignment happen after the undo.
+ setTimeout(
+ undoable(() => {
+ //StrListCast(placeholder.overrideFields).map(field => (docs[0][field] = placeholder[field])); // // shouldn't need to do this for layout fields since the placeholder already overrides its protos
+ placeholder.proto = docs[0];
+ }, 'Scrapbook add')
+ );
+ return false;
+ }
+ }
+ return false;
+ };
+
+ render() {
+ return (
+ <div style={{ background: 'beige', width: '100%', height: '100%' }}>
+ <CollectionView
+ {...this._props} //
+ setContentViewBox={emptyFunction}
+ rejectDrop={this.rejectDrop}
+ childRejectDrop={this.childRejectDrop}
+ filterAddDocument={this.filterAddDocument}
+ />
+ {/* <div style={{ border: '1px black', borderStyle: 'dotted', position: 'absolute', top: '50%', width: '100%', textAlign: 'center' }}>Drop an image here</div> */}
+ </div>
+ );
+ }
+}
+
+// Register scrapbook
+Docs.Prototypes.TemplateMap.set(DocumentType.SCRAPBOOK, {
+ layout: { view: ScrapbookBox, dataField: 'items' },
+ options: {
+ acl: '',
+ _height: 200,
+ _xMargin: 10,
+ _yMargin: 10,
+ _layout_fitWidth: false,
+ _layout_autoHeight: true,
+ _layout_reflowVertical: true,
+ _layout_reflowHorizontal: true,
+ _freeform_fitContentsToBox: true,
+ defaultDoubleClick: 'ignore',
+ systemIcon: 'BsImages',
+ },
+});
diff --git a/src/client/views/nodes/scrapbook/ScrapbookContent.tsx b/src/client/views/nodes/scrapbook/ScrapbookContent.tsx
new file mode 100644
index 000000000..ad1d308e8
--- /dev/null
+++ b/src/client/views/nodes/scrapbook/ScrapbookContent.tsx
@@ -0,0 +1,23 @@
+import React from "react";
+import { observer } from "mobx-react-lite";
+// Import the Doc type from your actual module.
+import { Doc } from "../../../../fields/Doc";
+
+export interface ScrapbookContentProps {
+ doc: Doc;
+}
+
+// A simple view that displays a document's title and content.
+// Adjust how you extract the text if your Doc fields are objects.
+export const ScrapbookContent: React.FC<ScrapbookContentProps> = observer(({ doc }) => {
+ // If doc.title or doc.content are not plain strings, convert them.
+ const titleText = doc.title ? doc.title.toString() : "Untitled";
+ const contentText = doc.content ? doc.content.toString() : "No content available.";
+
+ return (
+ <div className="scrapbook-content">
+ <h3>{titleText}</h3>
+ <p>{contentText}</p>
+ </div>
+ );
+});
diff --git a/src/client/views/nodes/scrapbook/ScrapbookSlot.scss b/src/client/views/nodes/scrapbook/ScrapbookSlot.scss
new file mode 100644
index 000000000..ae647ad36
--- /dev/null
+++ b/src/client/views/nodes/scrapbook/ScrapbookSlot.scss
@@ -0,0 +1,85 @@
+//IGNORE FOR NOW, CURRENTLY NOT USED IN SCRAPBOOK IMPLEMENTATION
+.scrapbook-slot {
+ position: absolute;
+ background-color: rgba(245, 245, 245, 0.7);
+ border: 2px dashed #ccc;
+ border-radius: 5px;
+ box-sizing: border-box;
+ transition: all 0.2s ease;
+ overflow: hidden;
+
+ &.scrapbook-slot-over {
+ border-color: #4a90e2;
+ background-color: rgba(74, 144, 226, 0.1);
+ }
+
+ &.scrapbook-slot-filled {
+ border-style: solid;
+ border-color: rgba(0, 0, 0, 0.1);
+ background-color: transparent;
+
+ &.scrapbook-slot-over {
+ border-color: #4a90e2;
+ background-color: rgba(74, 144, 226, 0.1);
+ }
+ }
+
+ .scrapbook-slot-empty {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 100%;
+ height: 100%;
+ }
+
+ .scrapbook-slot-placeholder {
+ text-align: center;
+ color: #888;
+ }
+
+ .scrapbook-slot-title {
+ font-weight: bold;
+ margin-bottom: 5px;
+ }
+
+ .scrapbook-slot-instruction {
+ font-size: 0.9em;
+ font-style: italic;
+ }
+
+ .scrapbook-slot-content {
+ width: 100%;
+ height: 100%;
+ position: relative;
+ }
+
+ .scrapbook-slot-controls {
+ position: absolute;
+ top: 5px;
+ right: 5px;
+ z-index: 10;
+ opacity: 0;
+ transition: opacity 0.2s ease;
+
+ .scrapbook-slot-remove-btn {
+ background-color: rgba(255, 255, 255, 0.8);
+ border: 1px solid #ccc;
+ border-radius: 50%;
+ width: 20px;
+ height: 20px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ cursor: pointer;
+ font-size: 10px;
+
+ &:hover {
+ background-color: rgba(255, 0, 0, 0.1);
+ }
+ }
+ }
+
+ &:hover .scrapbook-slot-controls {
+ opacity: 1;
+ }
+} \ No newline at end of file
diff --git a/src/client/views/nodes/scrapbook/ScrapbookSlot.tsx b/src/client/views/nodes/scrapbook/ScrapbookSlot.tsx
new file mode 100644
index 000000000..2c8f93778
--- /dev/null
+++ b/src/client/views/nodes/scrapbook/ScrapbookSlot.tsx
@@ -0,0 +1,28 @@
+
+//IGNORE FOR NOW, CURRENTLY NOT USED IN SCRAPBOOK IMPLEMENTATION
+export interface SlotDefinition {
+ id: string;
+ x: number; y: number;
+ defaultWidth: number;
+ defaultHeight: number;
+ }
+
+ export interface SlotContentMap {
+ slotId: string;
+ docId?: string;
+ }
+
+ export interface ScrapbookConfig {
+ slots: SlotDefinition[];
+ contents?: SlotContentMap[];
+ }
+
+ export const DEFAULT_SCRAPBOOK_CONFIG: ScrapbookConfig = {
+ slots: [
+ { id: "slot1", x: 10, y: 10, defaultWidth: 180, defaultHeight: 120 },
+ { id: "slot2", x: 200, y: 10, defaultWidth: 180, defaultHeight: 120 },
+ // …etc
+ ],
+ contents: []
+ };
+ \ No newline at end of file
diff --git a/src/client/views/nodes/scrapbook/ScrapbookSlotTypes.ts b/src/client/views/nodes/scrapbook/ScrapbookSlotTypes.ts
new file mode 100644
index 000000000..686917d9a
--- /dev/null
+++ b/src/client/views/nodes/scrapbook/ScrapbookSlotTypes.ts
@@ -0,0 +1,25 @@
+// ScrapbookSlotTypes.ts
+export interface SlotDefinition {
+ id: string;
+ title: string;
+ x: number;
+ y: number;
+ defaultWidth: number;
+ defaultHeight: number;
+ }
+
+ export interface ScrapbookConfig {
+ slots: SlotDefinition[];
+ contents?: { slotId: string; docId: string }[];
+ }
+
+ // give it three slots by default:
+ export const DEFAULT_SCRAPBOOK_CONFIG: ScrapbookConfig = {
+ slots: [
+ { id: "main", title: "Main Content", x: 20, y: 20, defaultWidth: 360, defaultHeight: 200 },
+ { id: "notes", title: "Notes", x: 20, y: 240, defaultWidth: 360, defaultHeight: 160 },
+ { id: "resources", title: "Resources", x: 400, y: 20, defaultWidth: 320, defaultHeight: 380 },
+ ],
+ contents: [],
+ };
+ \ No newline at end of file