diff options
Diffstat (limited to 'src/client/views/nodes')
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 |
