aboutsummaryrefslogtreecommitdiff
path: root/src/client/views/collections/collectionFreeForm
diff options
context:
space:
mode:
authorJoanne <zehan_ding@brown.edu>2025-06-20 10:18:38 -0400
committerJoanne <zehan_ding@brown.edu>2025-06-20 10:18:38 -0400
commit61787b3c1cf53c0230f6142bee0df30c65971012 (patch)
tree36c43ca031722b9a92f3f344288c8f6cf7fff5e1 /src/client/views/collections/collectionFreeForm
parent2aa2c26b95a539d220e46b20cdfbef6ae39d6c43 (diff)
parente7a96fa043cfc9c3c426e09bbef42c8df88a45f6 (diff)
Merge branch 'master' of https://github.com/brown-dash/Dash-Web into joanne-tutorialagent
Diffstat (limited to 'src/client/views/collections/collectionFreeForm')
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormRemoteCursors.scss2
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss32
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx52
-rw-r--r--src/client/views/collections/collectionFreeForm/FaceCollectionBox.scss14
-rw-r--r--src/client/views/collections/collectionFreeForm/ImageLabelBox.tsx5
-rw-r--r--src/client/views/collections/collectionFreeForm/MarqueeOptionsMenu.tsx2
-rw-r--r--src/client/views/collections/collectionFreeForm/MarqueeView.scss8
-rw-r--r--src/client/views/collections/collectionFreeForm/MarqueeView.tsx126
8 files changed, 166 insertions, 75 deletions
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormRemoteCursors.scss b/src/client/views/collections/collectionFreeForm/CollectionFreeFormRemoteCursors.scss
index 7951aff65..32cf3586f 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormRemoteCursors.scss
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormRemoteCursors.scss
@@ -18,5 +18,5 @@
color: black;
// fontStyle: "italic",
margin-left: -12;
- margin-top: 4;
+ margin-top: 4px;
}
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss
index 6c47a71b0..ac1ef7d65 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss
@@ -2,8 +2,8 @@
.collectionfreeformview-none {
position: inherit;
- top: 0;
- left: 0;
+ top: 0px;
+ left: 0px;
width: 100%;
height: 100%;
transform-origin: left top;
@@ -12,10 +12,10 @@
border-radius: inherit;
}
.collectionFreeForm-groupDropper {
- width: 10000;
- height: 10000;
- left: -5000;
- top: -5000;
+ width: 10000px;
+ height: 10000px;
+ left: -5000px;
+ top: -5000px;
position: absolute;
background: transparent;
pointer-events: all;
@@ -24,8 +24,8 @@
.collectionfreeformview-grid {
transform-origin: top left;
position: absolute;
- top: 0;
- left: 0;
+ top: 0px;
+ left: 0px;
pointer-events: none;
}
@@ -219,8 +219,8 @@
border-radius: inherit;
box-sizing: border-box;
position: absolute;
- top: 0;
- left: 0;
+ top: 0px;
+ left: 0px;
width: 100%;
height: 100%;
align-items: center;
@@ -264,7 +264,7 @@
.collectionFreeform-infoUI {
position: absolute;
display: block;
- top: 0;
+ top: 0px;
color: white;
background-color: #5075ef;
@@ -275,19 +275,19 @@
padding: 10px;
.collectionFreeform-infoUI-close {
position: absolute;
- top: -10;
- left: -10;
+ top: -10px;
+ left: -10px;
}
.collectionFreeform-infoUI-msg {
position: relative;
- max-width: 500;
- margin: 10;
+ max-width: 500px;
+ margin: 10px;
}
.collectionFreeform-infoUI-button {
border-radius: 50px;
font-size: 12px;
- padding: 6;
+ padding: 6px;
position: relative;
}
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 5bbe93a90..8f9b132e8 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -8,7 +8,7 @@ import { computedFn } from 'mobx-utils';
import * as React from 'react';
import { AiOutlineSend } from 'react-icons/ai';
import ReactLoading from 'react-loading';
-import { ClientUtils, DashColor, lightOrDark, OmitKeys, returnFalse, returnZero, setupMoveUpEvents, UpdateIcon } from '../../../../ClientUtils';
+import { ClientUtils, DashColor, lightOrDark, OmitKeys, returnFalse, returnTrue, returnZero, setupMoveUpEvents, UpdateIcon } from '../../../../ClientUtils';
import { DateField } from '../../../../fields/DateField';
import { Doc, DocListCast, Field, FieldType, Opt, StrListCast } from '../../../../fields/Doc';
import { DocData, DocLayout, Height, Width } from '../../../../fields/DocSymbols';
@@ -178,7 +178,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
return renderableEles;
}
@computed get fitContentsToBox() {
- return (this._props.fitContentsToBox?.() || this.Document._freeform_fitContentsToBox) && !this.isAnnotationOverlay;
+ return (this._props.fitContentsToBox?.() || this.Document._freeform_fitContentsToBox || this.Document.freeform_isGroup) && !this.isAnnotationOverlay;
}
@computed get nativeWidth() {
return this._props.NativeWidth?.() || Doc.NativeWidth(this.Document);
@@ -344,7 +344,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
*/
focusOnPoint = (options: FocusViewOptions) => {
const { pointFocus, zoomTime, didMove } = options;
- if (!this.Document.isGroup && pointFocus && !didMove) {
+ if (!this.Document.freeform_isGroup && pointFocus && !didMove) {
const dfltScale = this.isAnnotationOverlay ? 1 : 0.25;
if (this.layoutDoc[this.scaleFieldKey] !== dfltScale) {
this.zoomSmoothlyAboutPt(this.screenToFreeformContentsXf.transformPoint(pointFocus.X, pointFocus.Y), dfltScale, zoomTime);
@@ -382,7 +382,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
* @returns
*/
focus = (anchor: Doc, options: FocusViewOptions) => {
- if (anchor.isGroup && !options.docTransform && options.contextPath?.length) {
+ if (Doc.IsFreeformGroup(anchor) && !options.docTransform && options.contextPath?.length) {
// don't focus on group if there's a context path because we're about to focus on a group item
// which will override any group focus. (If we allowed the group to focus, it would mark didMove even if there were no net movement)
return undefined;
@@ -397,7 +397,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
}
const xfToCollection = options?.docTransform ?? Transform.Identity();
const savedState = { panX: NumCast(this.Document[this.panXFieldKey]), panY: NumCast(this.Document[this.panYFieldKey]), scale: options?.willZoomCentered ? this.Document[this.scaleFieldKey] : undefined };
- const cantTransform = this.fitContentsToBox || ((this.Document.isGroup || this.layoutDoc._lockedTransform) && !DocumentView.LightboxDoc());
+ const cantTransform = this.fitContentsToBox || ((this.Document.freeform_isGroup || this.layoutDoc._lockedTransform) && !DocumentView.LightboxDoc());
const { panX, panY, scale } = cantTransform || (!options.willPan && !options.willZoomCentered) ? savedState : this.calculatePanIntoView(anchor, xfToCollection, options?.willZoomCentered ? (options?.zoomScale ?? 0.75) : undefined);
// focus on the document in the collection
@@ -516,7 +516,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
this._downTime = Date.now();
const scrollMode = e.altKey ? (Doc.UserDoc().freeformScrollMode === freeformScrollMode.Pan ? freeformScrollMode.Zoom : freeformScrollMode.Pan) : Doc.UserDoc().freeformScrollMode;
if (e.button === 0 && (!(e.ctrlKey && !e.metaKey) || scrollMode !== freeformScrollMode.Pan) && this._props.isContentActive()) {
- if (!this.Document.isGroup) {
+ if (!this.Document.freeform_isGroup) {
// group freeforms don't pan when dragged -- instead let the event go through to allow the group itself to drag
// prettier-ignore
const hit = this._clusters.handlePointerDown(this.screenToFreeformContentsXf.transformPoint(e.clientX, e.clientY));
@@ -1273,7 +1273,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
@action
zoom = (pointX: number, pointY: number, deltaY: number): void => {
- if (this.Document.isGroup || this.Document[(this._props.viewField ?? '_') + 'freeform_noZoom']) return;
+ if (this.Document.freeform_isGroup || this.Document[(this._props.viewField ?? '_') + 'freeform_noZoom']) return;
let deltaScale = deltaY > 0 ? 1 / 1.05 : 1.05;
if (deltaScale < 0) deltaScale = -deltaScale;
const [x, y] = this.screenToFreeformContentsXf.transformPoint(pointX, pointY);
@@ -1300,7 +1300,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
@action
onPointerWheel = (e: React.WheelEvent): void => {
- if (this.Document.isGroup || !this.isContentActive()) return; // group style collections neither pan nor zoom
+ if (this.Document.freeform_isGroup || !this.isContentActive()) return; // group style collections neither pan nor zoom
SnappingManager.TriggerUserPanned();
if (this.layoutDoc._Transform || this.Document.treeView_OutlineMode === TreeViewType.outline) return;
e.stopPropagation();
@@ -1424,7 +1424,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
@action
zoomSmoothlyAboutPt(docpt: number[], scale: number, transitionTime = 500) {
- if (this.Document.isGroup) return;
+ if (this.Document.freeform_isGroup) return;
this.setPanZoomTransition(transitionTime);
const screenXY = this.screenToFreeformContentsXf.inverse().transformPoint(docpt[0], docpt[1]);
this.layoutDoc[this.scaleFieldKey] = scale;
@@ -1507,7 +1507,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
removeDocument = (docs: Doc | Doc[], annotationKey?: string | undefined) => {
const ret = !!this._props.removeDocument?.(docs, annotationKey);
// if this is a group and we have fewer than 2 Docs, then just promote what's left to our parent and get rid of the group.
- if (ret && DocListCast(this.dataDoc[annotationKey ?? this.fieldKey]).length < 2 && this.Document.isGroup) {
+ if (ret && DocListCast(this.dataDoc[annotationKey ?? this.fieldKey]).length < 2 && this.Document.freeform_isGroup) {
this.promoteCollection();
}
return ret;
@@ -1550,7 +1550,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
searchFilterDocs={this.searchFilterDocs}
isDocumentActive={childLayout.pointerEvents === 'none' ? returnFalse : this._props.childDocumentsActive?.() ? this._props.isDocumentActive : this.isContentActive}
isContentActive={this.childContentsActive}
- focus={this.Document.isGroup ? this.groupFocus : this.isAnnotationOverlay ? this._props.focus : this.focus}
+ focus={this.Document.freeform_isGroup ? this.groupFocus : this.isAnnotationOverlay ? this._props.focus : this.focus}
addDocTab={this.addDocTab}
addDocument={this._props.addDocument}
removeDocument={this.removeDocument}
@@ -1735,15 +1735,16 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
});
PinDocView(
anchor,
- { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ? { ...pinProps.pinData, poslayoutview: pinProps.pinData.dataview } : {}), pannable: !this.Document.isGroup, collectionType: true, filters: true } },
+ { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ? { ...pinProps.pinData, poslayoutview: pinProps.pinData.dataview } : {}), pannable: !this.Document.freeform_isGroup, collectionType: true, filters: true } },
this.Document
);
if (addAsAnnotation) {
- if (Cast(this.dataDoc[this._props.fieldKey + '_annotations'], listSpec(Doc), null) !== undefined) {
- Cast(this.dataDoc[this._props.fieldKey + '_annotations'], listSpec(Doc), [])?.push(anchor);
+ const fieldKey = this._props.isAnnotationOverlay ? this._props.fieldKey : this._props.fieldKey + '_annotations';
+ if (Cast(this.dataDoc[fieldKey], listSpec(Doc), null) !== undefined) {
+ Cast(this.dataDoc[fieldKey], listSpec(Doc), [])?.push(anchor);
} else {
- this.dataDoc[this._props.fieldKey + '_annotations'] = new List<Doc>([anchor]);
+ this.dataDoc[fieldKey] = new List<Doc>([anchor]);
}
}
return anchor;
@@ -1806,7 +1807,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
this._firstRender = false;
this._disposers.groupBounds = reaction(
() => {
- if (this.Document.isGroup && this.childDocs.length === this.childDocList?.length) {
+ if (this.Document.freeform_isGroup && this.childDocs.length === this.childDocList?.length) {
const clist = this.childDocs.map(cd => ({ x: NumCast(cd.x), y: NumCast(cd.y), width: NumCast(cd._width), height: NumCast(cd._height) }));
return aggregateBounds(clist, NumCast(this.layoutDoc._xMargin), NumCast(this.layoutDoc._yMargin));
}
@@ -1968,8 +1969,8 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
const appearance = ContextMenu.Instance.findByDescription('Appearance...');
const appearanceItems = appearance?.subitems ?? [];
- !this.Document.isGroup && appearanceItems.push({ description: 'Reset View', event: this.resetView, icon: 'compress-arrows-alt' });
- !this.Document.isGroup && appearanceItems.push({ description: 'Toggle Auto Reset View', event: this.toggleResetView, icon: 'compress-arrows-alt' });
+ !this.Document.freeform_isGroup && appearanceItems.push({ description: 'Reset View', event: this.resetView, icon: 'compress-arrows-alt' });
+ !this.Document.freeform_isGroup && appearanceItems.push({ description: 'Toggle Auto Reset View', event: this.toggleResetView, icon: 'compress-arrows-alt' });
if (this._props.setContentViewBox === emptyFunction) {
!appearance && ContextMenu.Instance.addItem({ description: 'Appearance...', subitems: appearanceItems, icon: 'eye' });
return;
@@ -1987,7 +1988,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
!Doc.noviceMode && appearanceItems.push({ description: `update icon`, event: () => this.updateIcon(), icon: 'compress-arrows-alt' });
this._props.renderDepth && appearanceItems.push({ description: 'Ungroup collection', event: this.promoteCollection, icon: 'table' });
- this.Document.isGroup && this.Document.transcription && appearanceItems.push({ description: 'Ink to text', event: this.transcribeStrokes, icon: 'font' });
+ this.Document.freeform_isGroup && this.Document.transcription && appearanceItems.push({ description: 'Ink to text', event: this.transcribeStrokes, icon: 'font' });
!Doc.noviceMode ? appearanceItems.push({ description: 'Arrange contents in grid', event: this.layoutDocsInGrid, icon: 'table' }) : null;
!Doc.noviceMode ? appearanceItems.push({ description: (this.Document._freeform_useClusters ? 'Hide' : 'Show') + ' Clusters', event: () => this._clusters.updateClusters(!this.Document._freeform_useClusters), icon: 'braille' }) : null;
@@ -2046,7 +2047,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
};
transcribeStrokes = undoable(() => {
- if (this.Document.isGroup && this.Document.transcription) {
+ if (this.Document.freeform_isGroup && this.Document.transcription) {
const text = StrCast(this.Document.transcription);
const lines = text.split('\n');
const height = 30 + 15 * lines.length;
@@ -2064,7 +2065,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
dragStarting = (snapToDraggedDoc: boolean = false, showGroupDragTarget: boolean = true, visited = new Set<Doc>()) => {
if (visited.has(this.Document)) return;
visited.add(this.Document);
- showGroupDragTarget && (this.GroupChildDrag = BoolCast(this.Document.isGroup));
+ showGroupDragTarget && (this.GroupChildDrag = BoolCast(this.Document.freeform_isGroup));
const activeDocs = this.getActiveDocuments();
const size = this.screenToFreeformContentsXf.transformDirection(this._props.PanelWidth(), this._props.PanelHeight());
const selRect = { left: this.panX() - size[0] / 2, top: this.panY() - size[1] / 2, width: size[0], height: size[1] };
@@ -2072,13 +2073,15 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
const isDocInView = (doc: Doc, rect: { left: number; top: number; width: number; height: number }) => intersectRect(docDims(doc), rect);
const snappableDocs = activeDocs.filter(doc => doc.z === undefined && isDocInView(doc, selRect)); // first see if there are any foreground docs to snap to
- activeDocs.filter(doc => doc.isGroup && SnappingManager.IsResizing !== doc[Id] && !DragManager.docsBeingDragged.includes(doc)).forEach(doc => DocumentView.getDocumentView(doc)?.ComponentView?.dragStarting?.(snapToDraggedDoc, false, visited));
+ activeDocs
+ .filter(doc => Doc.IsFreeformGroup(doc) && SnappingManager.IsResizing !== doc[Id] && !DragManager.docsBeingDragged.includes(doc))
+ .forEach(doc => DocumentView.getDocumentView(doc)?.ComponentView?.dragStarting?.(snapToDraggedDoc, false, visited));
const horizLines: number[] = [];
const vertLines: number[] = [];
const invXf = this.screenToFreeformContentsXf.inverse();
snappableDocs
- .filter(doc => !doc.isGroup && (snapToDraggedDoc || (SnappingManager.IsResizing !== doc[Id] && !DragManager.docsBeingDragged.includes(doc))))
+ .filter(doc => !Doc.IsFreeformGroup(doc) && (snapToDraggedDoc || (SnappingManager.IsResizing !== doc[Id] && !DragManager.docsBeingDragged.includes(doc))))
.forEach(doc => {
const { left, top, width, height } = docDims(doc);
const topLeftInScreen = invXf.transformPoint(left, top);
@@ -2102,6 +2105,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
}
this.childDocs.some(doc => !this._renderCutoffData.get(doc[Id])) && setTimeout(this.incrementalRender, 1);
});
+ showBorderRounding = returnTrue;
showPresPaths = () => SnappingManager.ShowPresPaths;
brushedView = () => this._brushedView;
gridColor = () => DashColor(lightOrDark(this.backgroundColor)).fade(0.5).toString(); // prettier-ignore
@@ -2170,7 +2174,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
{...this._props}
ref={this._marqueeViewRef}
Doc={this.Document}
- ungroup={this.Document.isGroup ? this.promoteCollection : undefined}
+ ungroup={this.Document.freeform_isGroup ? this.promoteCollection : undefined}
nudge={this.isAnnotationOverlay || this._props.renderDepth > 0 ? undefined : this.nudge}
addDocTab={this.addDocTab}
slowLoadDocuments={this.slowLoadDocuments}
diff --git a/src/client/views/collections/collectionFreeForm/FaceCollectionBox.scss b/src/client/views/collections/collectionFreeForm/FaceCollectionBox.scss
index 0a001d84c..d0685e419 100644
--- a/src/client/views/collections/collectionFreeForm/FaceCollectionBox.scss
+++ b/src/client/views/collections/collectionFreeForm/FaceCollectionBox.scss
@@ -2,7 +2,7 @@
display: flex;
height: max-content;
flex-direction: column;
- top: 0;
+ top: 0px;
position: absolute;
width: 100%;
height: 100%;
@@ -31,9 +31,9 @@
}
.face-document-top {
position: relative;
- top: 0;
+ top: 0px;
width: 100%;
- left: 0;
+ left: 0px;
}
.face-document-image-container {
@@ -69,8 +69,8 @@
.remove-item {
position: absolute;
- bottom: -5;
- right: -5;
+ bottom: -5px;
+ right: -5px;
background-color: rgba(0, 0, 0, 0.5); // Optional: to add a background behind the icon for better visibility
border-radius: 30%;
width: 10px; // Adjust size as needed
@@ -98,7 +98,7 @@
.faceCollectionBox {
width: 100%;
height: 100%;
- top: 0;
- left: 0;
+ top: 0px;
+ left: 0px;
position: absolute;
}
diff --git a/src/client/views/collections/collectionFreeForm/ImageLabelBox.tsx b/src/client/views/collections/collectionFreeForm/ImageLabelBox.tsx
index ff9fb14e7..e3a3f9b05 100644
--- a/src/client/views/collections/collectionFreeForm/ImageLabelBox.tsx
+++ b/src/client/views/collections/collectionFreeForm/ImageLabelBox.tsx
@@ -160,15 +160,16 @@ export class ImageLabelBox extends ViewBoxBaseComponent<FieldViewProps>() {
classifyImagesInBox = async () => {
this.startLoading();
+ const selectedImages = this._selectedImages;
// Converts the images into a Base64 format, afterwhich the information is sent to GPT to label them.
- const imageInfos = this._selectedImages.map(async doc => {
+ const imageInfos = selectedImages.map(async doc => {
if (!doc.$tags_chat) {
const url = ImageCastWithSuffix(doc[Doc.LayoutDataKey(doc)], '_o') ?? '';
return imageUrlToBase64(url).then(hrefBase64 =>
!hrefBase64 ? undefined :
- gptImageLabel(hrefBase64,'Give three labels to describe this image.').then(labels =>
+ gptImageLabel(hrefBase64, 'Give three labels to describe this image.').then(labels =>
({ doc, labels }))) ; // prettier-ignore
}
});
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeOptionsMenu.tsx b/src/client/views/collections/collectionFreeForm/MarqueeOptionsMenu.tsx
index abd828945..2ec59e5d5 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeOptionsMenu.tsx
+++ b/src/client/views/collections/collectionFreeForm/MarqueeOptionsMenu.tsx
@@ -16,6 +16,7 @@ export class MarqueeOptionsMenu extends AntimodeMenu<AntimodeMenuProps> {
public createCollection: (e: KeyboardEvent | React.PointerEvent | undefined, group?: boolean, selection?: Doc[]) => Doc | void = unimplementedFunction;
public delete: (e: KeyboardEvent | React.PointerEvent | undefined) => void = unimplementedFunction;
public summarize: (e: KeyboardEvent | React.PointerEvent | undefined) => void = unimplementedFunction;
+ public generateScrapbook: (e: KeyboardEvent | React.PointerEvent | undefined) => void = unimplementedFunction;
public showMarquee: () => void = unimplementedFunction;
public hideMarquee: () => void = unimplementedFunction;
public pinWithView: (e: KeyboardEvent | React.PointerEvent | undefined) => void = unimplementedFunction;
@@ -38,6 +39,7 @@ export class MarqueeOptionsMenu extends AntimodeMenu<AntimodeMenuProps> {
<IconButton tooltip="Create a Collection" onPointerDown={this.createCollection} icon={<FontAwesomeIcon icon="object-group" />} color={this.userColor} />
<IconButton tooltip="Create a Grouping" onPointerDown={e => this.createCollection(e, true)} icon={<FontAwesomeIcon icon="layer-group" />} color={this.userColor} />
<IconButton tooltip="Summarize Documents" onPointerDown={this.summarize} icon={<FontAwesomeIcon icon="compress-arrows-alt" />} color={this.userColor} />
+ <IconButton tooltip="Generate Scrapbook" onPointerDown={this.generateScrapbook} icon={<FontAwesomeIcon icon="palette" />} color={this.userColor} />
<IconButton tooltip="Delete Documents" onPointerDown={this.delete} icon={<FontAwesomeIcon icon="trash-alt" />} color={this.userColor} />
<IconButton tooltip="Pin selected region" onPointerDown={this.pinWithView} icon={<FontAwesomeIcon icon="map-pin" />} color={this.userColor} />
<IconButton tooltip="Classify and Sort Images" onPointerDown={this.classifyImages} icon={<FontAwesomeIcon icon="object-group" />} color={this.userColor} />
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.scss b/src/client/views/collections/collectionFreeForm/MarqueeView.scss
index 7c9d0f6e1..135f4deac 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeView.scss
+++ b/src/client/views/collections/collectionFreeForm/MarqueeView.scss
@@ -1,7 +1,7 @@
.marqueeView {
position: inherit;
- top: 0;
- left: 0;
+ top: 0px;
+ left: 0px;
width: 100%;
height: 100%;
overflow: hidden;
@@ -20,7 +20,7 @@
pointer-events: none;
.marquee-legend {
bottom: -18px;
- left: 0;
+ left: 0px;
position: absolute;
font-size: 9;
white-space: nowrap;
@@ -28,4 +28,4 @@
.marquee-legend::after {
content: 'Press <space> for lasso';
}
-}
+} \ No newline at end of file
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
index 3cc7c0f2d..ff78b332a 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
+++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
@@ -28,6 +28,9 @@ import { FormattedTextBox } from '../../nodes/formattedText/FormattedTextBox';
import { SubCollectionViewProps } from '../CollectionSubView';
import { ImageLabelBoxData } from './ImageLabelBox';
import { MarqueeOptionsMenu } from './MarqueeOptionsMenu';
+import { StrListCast } from '../../../../fields/Doc';
+import { requestAiGeneratedPreset, DocumentDescriptor } from '../../nodes/scrapbook/AIPresetGenerator';
+import { buildPlaceholdersFromConfigs, slotRealDocIntoPlaceholders } from '../../nodes/scrapbook/ScrapbookBox';
import './MarqueeView.scss';
interface MarqueeViewProps {
@@ -76,6 +79,11 @@ export class MarqueeView extends ObservableReactComponent<SubCollectionViewProps
@observable _labelsVisibile: boolean = false;
@observable _lassoPts: [number, number][] = [];
@observable _lassoFreehand: boolean = false;
+ // ─── New Observables for “Pick 1 of N AI Scrapbook” ───
+ @observable aiChoices: Doc[] = []; // temporary hidden Scrapbook docs
+ @observable pickerX = 0; // popup x coordinate
+ @observable pickerY = 0; // popup y coordinate
+ @observable pickerVisible = false; // show/hide ScrapbookPicker
@computed get Transform() {
return this._props.getTransform();
@@ -190,6 +198,7 @@ export class MarqueeView extends ObservableReactComponent<SubCollectionViewProps
FormattedTextBox.SelectOnLoadChar = Doc.UserDoc().defaultTextLayout && !this._props.childLayoutString ? e.key : '';
FormattedTextBox.LiveTextUndo = UndoManager.StartBatch('type new note');
this._props.addLiveTextDocument(DocUtils.GetNewTextDoc('-typed text-', x, y, 200, 100));
+ setTimeout(() => FormattedTextBox.LiveTextUndo?.end(), 100);
e.stopPropagation();
}
};
@@ -275,6 +284,7 @@ export class MarqueeView extends ObservableReactComponent<SubCollectionViewProps
MarqueeOptionsMenu.Instance.createCollection = this.collection;
MarqueeOptionsMenu.Instance.delete = this.delete;
MarqueeOptionsMenu.Instance.summarize = this.summary;
+ MarqueeOptionsMenu.Instance.generateScrapbook = this.generateScrapbook;
MarqueeOptionsMenu.Instance.showMarquee = this.showMarquee;
MarqueeOptionsMenu.Instance.hideMarquee = this.hideMarquee;
MarqueeOptionsMenu.Instance.jumpTo(e.clientX, e.clientY);
@@ -372,7 +382,7 @@ export class MarqueeView extends ObservableReactComponent<SubCollectionViewProps
? creator(selected, { title: 'nested stack' })
: ((doc: Doc) => {
doc.$data = new List<Doc>(selected);
- doc.$isGroup = makeGroup;
+ doc.$freeform_isGroup = makeGroup;
doc.$title = makeGroup ? 'grouping' : 'nested freeform';
doc._freeform_panX = doc._freeform_panY = 0;
return doc;
@@ -508,7 +518,7 @@ export class MarqueeView extends ObservableReactComponent<SubCollectionViewProps
_layout_showSidebar: true,
title: 'overview',
});
- const portal = Docs.Create.FreeformDocument(selected, { title: 'summarized documents', x: this.Bounds.left + 200, y: this.Bounds.top, isGroup: true, backgroundColor: 'transparent' });
+ const portal = Docs.Create.FreeformDocument(selected, { title: 'summarized documents', x: this.Bounds.left + 200, y: this.Bounds.top, freeform_isGroup: true, backgroundColor: 'transparent' });
DocUtils.MakeLink(summary, portal, { link_relationship: 'summary of:summarized by' });
portal.hidden = true;
@@ -517,6 +527,77 @@ export class MarqueeView extends ObservableReactComponent<SubCollectionViewProps
MarqueeOptionsMenu.Instance.fadeOut(true);
});
+ getAiPresetsDescriptors = (): DocumentDescriptor[] =>
+ this.marqueeSelect(false).map(doc => ({
+ type: typeof doc.$type === 'string' ? doc.$type : 'UNKNOWN',
+ tags: Array.from(new Set(StrListCast(doc.$tags_chat))),
+ }));
+
+ generateScrapbook = action(async () => {
+ const selectedDocs = this.marqueeSelect(false);
+ if (!selectedDocs.length) return;
+
+ const descriptors = this.getAiPresetsDescriptors();
+ if (descriptors.length === 0) {
+ alert('No documents selected to generate a scrapbook from!');
+ return;
+ }
+
+ const aiPreset = await requestAiGeneratedPreset(descriptors);
+ if (!aiPreset.length) {
+ alert('Failed to generate preset');
+ return;
+ }
+ const scrapbookPlaceholders: Doc[] = buildPlaceholdersFromConfigs(aiPreset);
+ /*
+ const scrapbookPlaceholders: Doc[] = aiPreset.map(cfg => {
+ const placeholderDoc = Docs.Create.TextDocument(cfg.tag);
+ placeholderDoc.placeholder_docType = cfg.type as DocumentType;
+ placeholderDoc.placeholder_acceptTags = new List<string>(cfg.acceptTags ?? [cfg.tag]);
+
+ const placeholder = new Doc();
+ placeholder.proto = placeholderDoc;
+ placeholder.original = placeholderDoc;
+ placeholder.x = cfg.x;
+ placeholder.y = cfg.y;
+ if (cfg.width != null) placeholder._width = cfg.width;
+ if (cfg.height != null) placeholder._height = cfg.height;
+
+ return placeholder;
+ });*/
+
+ const scrapbook = Docs.Create.ScrapbookDocument(scrapbookPlaceholders, {
+ backgroundColor: '#e2ad32',
+ x: this.Bounds.left,
+ y: this.Bounds.top,
+ _width: 500,
+ _height: 500,
+ title: 'AI-generated Scrapbook',
+ });
+
+ // 3) Now grab that new scrapbook’s flat placeholders
+ const allPlaceholders = DocUtils.unwrapPlaceholders(scrapbookPlaceholders);
+
+ // 4) Slot each selectedDocs[i] into the first matching placeholder
+ selectedDocs.forEach(realDoc => slotRealDocIntoPlaceholders(realDoc, allPlaceholders));
+
+ const selected = selectedDocs.map(d => {
+ this._props.removeDocument?.(d);
+ d.x = NumCast(d.x) - this.Bounds.left;
+ d.y = NumCast(d.y) - this.Bounds.top;
+ return d;
+ });
+
+ this._props.addDocument?.(scrapbook);
+ const portal = Docs.Create.FreeformDocument(selected, { title: 'docs in scrapbook', x: this.Bounds.left + 200, y: this.Bounds.top, isGroup: true, backgroundColor: 'transparent' });
+ DocUtils.MakeLink(scrapbook, portal, { link_relationship: 'scrapbook of:in scrapbook' });
+
+ portal.hidden = true;
+ this._props.addDocument?.(portal);
+ MarqueeOptionsMenu.Instance.fadeOut(true);
+ this.hideMarquee();
+ });
+
@action
marqueeCommand = (e: KeyboardEvent) => {
const ee = e as unknown as KeyboardEvent & { propagationIsStopped?: boolean };
@@ -538,6 +619,7 @@ export class MarqueeView extends ObservableReactComponent<SubCollectionViewProps
if (e.key === 'g') this.collection(e, true);
if (e.key === 'c' || e.key === 't') this.collection(e);
if (e.key === 's' || e.key === 'S') this.summary();
+ if (e.key === 'g' || e.key === 'G') this.generateScrapbook(); // ← scrapbook shortcut
if (e.key === 'p') this.pileup();
this.cleanupInteractions(false);
}
@@ -682,25 +764,27 @@ export class MarqueeView extends ObservableReactComponent<SubCollectionViewProps
};
render() {
return (
- <div
- className="marqueeView"
- ref={r => {
- r?.addEventListener('dashDragMovePause', this.onDragMovePause as EventListenerOrEventListenerObject);
- this.MarqueeRef = r;
- }}
- style={{
- overflow: StrCast(this._props.Document._overflow),
- cursor: Doc.ActiveTool === InkTool.Ink || this._visible ? 'crosshair' : 'pointer',
- }}
- onDragOver={e => e.preventDefault()}
- onScroll={e => {
- e.currentTarget.scrollTop = e.currentTarget.scrollLeft = 0;
- }}
- onClick={this.onClick}
- onPointerDown={this.onPointerDown}>
- {this._visible ? this.marqueeDiv : null}
- {this.props.children}
- </div>
+ <>
+ <div
+ className="marqueeView"
+ ref={r => {
+ r?.addEventListener('dashDragMovePause', this.onDragMovePause as EventListenerOrEventListenerObject);
+ this.MarqueeRef = r;
+ }}
+ style={{
+ overflow: StrCast(this._props.Document._overflow),
+ cursor: Doc.ActiveTool === InkTool.Ink || this._visible ? 'crosshair' : 'pointer',
+ }}
+ onDragOver={e => e.preventDefault()}
+ onScroll={e => {
+ e.currentTarget.scrollTop = e.currentTarget.scrollLeft = 0;
+ }}
+ onClick={this.onClick}
+ onPointerDown={this.onPointerDown}>
+ {this._visible ? this.marqueeDiv : null}
+ {this.props.children}
+ </div>
+ </>
);
}
}