aboutsummaryrefslogtreecommitdiff
path: root/src/client/views
diff options
context:
space:
mode:
Diffstat (limited to 'src/client/views')
-rw-r--r--src/client/views/ViewBoxInterface.ts1
-rw-r--r--src/client/views/collections/collectionFreeForm/ImageLabelBox.tsx8
-rw-r--r--src/client/views/collections/collectionFreeForm/MarqueeOptionsMenu.tsx2
-rw-r--r--src/client/views/collections/collectionFreeForm/MarqueeView.tsx35
-rw-r--r--src/client/views/nodes/ImageBox.tsx53
-rw-r--r--src/client/views/nodes/formattedText/FormattedTextBox.tsx18
-rw-r--r--src/client/views/nodes/scrapbook/ScrapbookBox.tsx341
-rw-r--r--src/client/views/nodes/scrapbook/ScrapbookPreset.tsx146
-rw-r--r--src/client/views/nodes/scrapbook/ScrapbookSettingsPanel.tsx60
-rw-r--r--src/client/views/search/FaceRecognitionHandler.tsx16
10 files changed, 663 insertions, 17 deletions
diff --git a/src/client/views/ViewBoxInterface.ts b/src/client/views/ViewBoxInterface.ts
index d8dab8e89..b532dfe35 100644
--- a/src/client/views/ViewBoxInterface.ts
+++ b/src/client/views/ViewBoxInterface.ts
@@ -24,6 +24,7 @@ export abstract class ViewBoxInterface<P> extends ObservableReactComponent<React
promoteCollection?: () => void; // moves contents of collection to parent
hasChildDocs?: () => Doc[];
docEditorView?: () => void;
+ autoTag?: () => void; // auto tag the document
showSmartDraw?: (x: number, y: number, regenerate?: boolean) => void;
updateIcon?: (usePanelDimensions?: boolean) => Promise<void>; // updates the icon representation of the document
getAnchor?: (addAsAnnotation: boolean, pinData?: PinProps) => Doc; // returns an Anchor Doc that represents the current state of the doc's componentview (e.g., the current playhead location of a an audio/video box)
diff --git a/src/client/views/collections/collectionFreeForm/ImageLabelBox.tsx b/src/client/views/collections/collectionFreeForm/ImageLabelBox.tsx
index ff9fb14e7..038b1c6f9 100644
--- a/src/client/views/collections/collectionFreeForm/ImageLabelBox.tsx
+++ b/src/client/views/collections/collectionFreeForm/ImageLabelBox.tsx
@@ -158,17 +158,19 @@ export class ImageLabelBox extends ViewBoxBaseComponent<FieldViewProps>() {
this._currentLabel = e.target.value;
});
- classifyImagesInBox = async () => {
+ classifyImagesInBox = async (selectedImages? : Doc[], prompt? : string) => {
this.startLoading();
+ alert('Classifying images...');
+ 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, prompt ?? '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.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
index 3cc7c0f2d..f5e699d3e 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
+++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
@@ -275,6 +275,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);
@@ -517,6 +518,39 @@ export class MarqueeView extends ObservableReactComponent<SubCollectionViewProps
MarqueeOptionsMenu.Instance.fadeOut(true);
});
+
+
+ @undoBatch
+ generateScrapbook = action(() => {
+ let docs = new Array<Doc>();
+ const selected = this.marqueeSelect(false).map(d => {
+ this._props.removeDocument?.(d);
+ d.x = NumCast(d.x) - this.Bounds.left;
+ d.y = NumCast(d.y) - this.Bounds.top;
+ docs.push(d);
+ return d;
+ });
+ const scrapbook = Docs.Create.ScrapbookDocument(docs, {
+ backgroundColor: '#e2ad32',
+ x: this.Bounds.left,
+ y: this.Bounds.top,
+ followLinkToggle: true,
+ _width: 200,
+ _height: 200,
+ _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' });
+ DocUtils.MakeLink(scrapbook, portal, { link_relationship: 'summary of:summarized by' });
+
+ portal.hidden = true;
+ this._props.addDocument?.(portal);
+ //this._props.addLiveTextDocument(summary);
+ this._props.addDocument?.(scrapbook);
+ MarqueeOptionsMenu.Instance.fadeOut(true);
+ });
+
+
@action
marqueeCommand = (e: KeyboardEvent) => {
const ee = e as unknown as KeyboardEvent & { propagationIsStopped?: boolean };
@@ -538,6 +572,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);
}
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index f7ad5c7e2..9d459d7eb 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -7,8 +7,9 @@ import { observer } from 'mobx-react';
import { extname } from 'path';
import * as React from 'react';
import { AiOutlineSend } from 'react-icons/ai';
+import { ImageLabelBoxData } from '../collections/collectionFreeForm/ImageLabelBox';
import ReactLoading from 'react-loading';
-import { ClientUtils, DashColor, returnEmptyString, returnFalse, returnOne, returnZero, setupMoveUpEvents, UpdateIcon } from '../../../ClientUtils';
+import { ClientUtils, imageUrlToBase64, DashColor, returnEmptyString, returnFalse, returnOne, returnZero, setupMoveUpEvents, UpdateIcon } from '../../../ClientUtils';
import { Doc, DocListCast, Opt } from '../../../fields/Doc';
import { DocData } from '../../../fields/DocSymbols';
import { Id } from '../../../fields/FieldSymbols';
@@ -16,7 +17,7 @@ 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 { Cast, DocCast, ImageCast, NumCast, RTFCast, StrCast, ImageCastWithSuffix } from '../../../fields/Types';
import { ImageField } from '../../../fields/URLField';
import { TraceMobx } from '../../../fields/util';
import { emptyFunction } from '../../../Utils';
@@ -45,6 +46,8 @@ import { FieldView, FieldViewProps } from './FieldView';
import { FocusViewOptions } from './FocusViewOptions';
import './ImageBox.scss';
import { OpenWhere } from './OpenWhere';
+import { gptImageLabel } from '../../apis/gpt/GPT';
+import { ImageLabelBox } from '../collections/collectionFreeForm/ImageLabelBox';
const DefaultPath = '/assets/unknown-file-icon-hi.png';
export class ImageEditorData {
@@ -117,6 +120,52 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
this._dropDisposer?.();
ele && (this._dropDisposer = DragManager.MakeDropTarget(ele, this.drop.bind(this), this.Document));
};
+
+ autoTag = async () => {
+
+ try {
+ // 1) grab the full-size URL
+ const layoutKey = Doc.LayoutDataKey(this.Document);
+ const url = ImageCastWithSuffix(this.Document[layoutKey], '_o') ?? '';
+ if (!url) throw new Error('No image URL found');
+
+ // 2) convert to base64
+ const base64 = await imageUrlToBase64(url);
+ if (!base64) throw new Error('Failed to load image data');
+
+ // 3) ask GPT for exactly one label: PERSON or LANDSCAPE
+ const raw = await gptImageLabel(
+ base64,
+ 'Classify this image as PERSON or LANDSCAPE. You may only respond with one of these two options.'
+ );
+
+ // 4) normalize and prefix
+ const label = raw
+ .trim()
+ .toUpperCase()
+
+ // 5) stash it on the Doc
+ // overwrite any old tags so re-runs still work
+ this.Document.$tags_chat = new List<string>();
+ (this.Document.$tags_chat as List<string>).push(label);
+
+ // 6) flip on “show tags” in the layout
+ // (same flag that ImageLabelBox.toggleDisplayInformation uses)
+ //note to self: What if i used my own field (ex: Document.$auto_description or something
+ //Would i still have to toggle it on for it to show in the metadata?
+ this.Document._layout_showTags = true;
+
+ } catch (err) {
+ console.error('autoTag failed:', err);
+ } finally {
+ }
+ };
+
+ //Doc.getDescription(this.Document).then(desc => this.desc = desc)
+
+
+
+
getAnchor = (addAsAnnotation: boolean, pinProps?: PinProps) => {
const visibleAnchor = this._getAnchor?.(this._savedAnnotations, true); // use marquee anchor, otherwise, save zoom/pan as anchor
const anchor =
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index 57720baae..97049d0eb 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -64,6 +64,7 @@ import { removeMarkWithAttrs } from './prosemirrorPatches';
import { RichTextMenu, RichTextMenuPlugin } from './RichTextMenu';
import { RichTextRules } from './RichTextRules';
import { schema } from './schema_rts';
+import { tickStep } from 'd3';
// import * as applyDevTools from 'prosemirror-dev-tools';
export interface FormattedTextBoxProps extends FieldViewProps {
@@ -304,6 +305,14 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
}
};
+ autoTag = async () => {
+ this.Document.$tags_chat = new List<string>();
+ gptAPICall(RTFCast(this.Document[Doc.LayoutDataKey(this.Document)])?.Text ?? StrCast(this.Document[Doc.LayoutDataKey(this.Document)]),
+ GPTCallType.CLASSIFYTEXT).then(desc => (this.Document.$tags_chat as List<string>).push(desc));
+ this.Document._layout_showTags = true;
+ //or... then(desc => this.Document.$tags_chat = desc);
+ }
+
leafText = (node: Node) => {
if (node.type === this.EditorView?.state.schema.nodes.dashField) {
const refDoc = !node.attrs.docId ? this.rootDoc : (DocServer.GetCachedRefField(node.attrs.docId as string) as Doc);
@@ -1237,6 +1246,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
},
{ fireImmediately: true }
);
+
this._disposers.search = reaction(
() => Doc.IsSearchMatch(this.Document),
@@ -1270,6 +1280,14 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
{ fireImmediately: true }
);
+ this._disposers.tagger = reaction(
+ () => ({ title: this.Document.title, sel: this.props.isSelected() }),
+ action(() => {
+ this.autoTag();
+ }),
+ { fireImmediately: true }
+ );
+
if (!this._props.dontRegisterView) {
this._disposers.record = reaction(
() => this.recordingDictation,
diff --git a/src/client/views/nodes/scrapbook/ScrapbookBox.tsx b/src/client/views/nodes/scrapbook/ScrapbookBox.tsx
index 6cfe9a62c..731715964 100644
--- a/src/client/views/nodes/scrapbook/ScrapbookBox.tsx
+++ b/src/client/views/nodes/scrapbook/ScrapbookBox.tsx
@@ -1,6 +1,6 @@
import { action, makeObservable, observable } from 'mobx';
import * as React from 'react';
-import { Doc, DocListCast } from '../../../../fields/Doc';
+import { Doc, DocListCast, StrListCast } from '../../../../fields/Doc';
import { List } from '../../../../fields/List';
import { emptyFunction } from '../../../../Utils';
import { Docs } from '../../../documents/Documents';
@@ -12,21 +12,36 @@ import { FieldView, FieldViewProps } from '../FieldView';
import { DragManager } from '../../../util/DragManager';
import { RTFCast, StrCast, toList } from '../../../../fields/Types';
import { undoable } from '../../../util/UndoManager';
+import { ScrapbookItemConfig, ScrapbookPreset } from './ScrapbookPreset';
+
+enum ScrapbookPresetType {
+ Classic = 'Classic',
+ Default = 'Default',
+ Collage = 'Collage',
+ Spotlight = 'Spotlight',
+}
+
// Scrapbook view: a container that lays out its child items in a grid/template
export class ScrapbookBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
@observable createdDate: string;
+ @observable configs : ScrapbookItemConfig[]
constructor(props: FieldViewProps) {
super(props);
makeObservable(this);
this.createdDate = this.getFormattedDate();
+ this.configs =
+ ScrapbookPreset.createPreset(presetType);
+
// 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.initScrapbook(ScrapbookPresetType.Default);
this.setTitle();
+ //this.setLayout(ScrapbookPreset.Spotlight);
}
public static LayoutString(fieldStr: string) {
@@ -41,6 +56,188 @@ export class ScrapbookBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
});
}
+
+ @action
+ initScrapbook(presetType: ScrapbookPresetType) {
+ // 1) ensure title is set
+ const title = `Scrapbook - ${this.createdDate}`;
+ if (this.dataDoc.title !== title) {
+ this.dataDoc.title = title;
+ }
+
+ // 2) build placeholders from the preset
+ const configs = ScrapbookPreset.createPreset(presetType);
+ const placeholders: Doc[] = [];
+
+ for (const cfg of configs) {
+ if (cfg.children) {
+ // --- nested container ---
+ const childDocs = cfg.children.map(child => {
+ const doc = Docs.Create.TextDocument(child.tag);
+ doc.accepts_docType = child.type;
+ doc.accepts_tagType = child.acceptTag ?? child.tag;
+
+ const ph = new Doc();
+ ph.proto = doc;
+ ph.original = doc;
+ ph.x = child.x;
+ ph.y = child.y;
+ if (child.width != null) ph._width = child.width;
+ if (child.height != null) ph._height = child.height;
+ return ph;
+ });
+
+ const protoW = cfg.containerWidth ?? cfg.width;
+ const protoH = cfg.containerHeight ?? cfg.height;
+ const containerProto = Docs.Create.StackingDocument(
+ childDocs,
+ {
+ ...(protoW != null ? { _width: protoW } : {}),
+ ...(protoH != null ? { _height: protoH } : {}),
+ title: cfg.tag
+ }
+ );
+
+ const ph = new Doc();
+ ph.proto = containerProto;
+ ph.original = containerProto;
+ ph.x = cfg.x;
+ ph.y = cfg.y;
+ if (cfg.width != null) ph._width = cfg.width;
+ if (cfg.height != null) ph._height = cfg.height;
+ placeholders.push(ph);
+
+ } else {
+ // --- flat placeholder ---
+ const doc = Docs.Create.TextDocument(cfg.tag);
+ doc.accepts_docType = cfg.type;
+ doc.accepts_tagType = cfg.acceptTag ?? cfg.tag;
+
+ const ph = new Doc();
+ ph.proto = doc;
+ ph.original = doc;
+ ph.x = cfg.x;
+ ph.y = cfg.y;
+ if (cfg.width != null) ph._width = cfg.width;
+ if (cfg.height != null) ph._height = cfg.height;
+ placeholders.push(ph);
+ }
+ }
+
+ // 3) commit them into the field
+ this.dataDoc[this.fieldKey] = new List<Doc>(placeholders);
+ }
+ @action
+ //INACTIVE VER ignore!! not in use rn, implementation ver 1
+ setLayout(preset: ScrapbookPreset) {
+ // helper to wrap a TextDocument proto in a Doc with positioning
+ function makePlaceholder(
+ proto: Doc, x: number, y: number,
+ width: number, height: number
+ ): Doc {
+ const d = new Doc();
+ d.proto = proto;
+ d.original = proto;
+ d.x = x;
+ d.y = y;
+ d._width = width;
+ d._height = height;
+ return d;
+ }
+
+ let placeholders: Doc[];
+
+ switch (preset) {
+ case ScrapbookPresetType.Classic:
+ // One large landscape image on top, caption below, sidebar at right
+ const imgClassic = Docs.Create.TextDocument('image');
+ imgClassic.accepts_docType = DocumentType.IMG;
+ imgClassic.accepts_tagType = 'LANDSCAPE';
+ const phImageClassic = makePlaceholder(imgClassic, 0, -120, 300, 180);
+
+ const captionClassic = Docs.Create.TextDocument('caption');
+ captionClassic.accepts_docType = DocumentType.RTF;
+ captionClassic.accepts_tagType = 'caption';
+ const phCaptionClassic = makePlaceholder(captionClassic, 0, 80, 300, 60);
+
+ const sidebarClassic = Docs.Create.TextDocument('sidebar');
+ sidebarClassic.accepts_docType = DocumentType.RTF;
+ sidebarClassic.accepts_tagType = 'lengthy description';
+ const phSidebarClassic = makePlaceholder(sidebarClassic, 320, -50, 80, 200);
+
+ placeholders = [phImageClassic, phCaptionClassic, phSidebarClassic];
+ break;
+
+ case ScrapbookPresetType.Collage:
+ // Grid of four person images, small captions under each
+ const personDocs: Doc[] = [];
+ for (let i = 0; i < 4; i++) {
+ const img = Docs.Create.TextDocument(`person ${i+1}`);
+ img.accepts_docType = DocumentType.IMG;
+ img.accepts_tagType = 'PERSON';
+ // position in 2x2 grid
+ const x = (i % 2) * 160 - 80;
+ const y = Math.floor(i / 2) * 160 - 80;
+ personDocs.push(makePlaceholder(img, x, y, 150, 120));
+
+ const cap = Docs.Create.TextDocument(`caption ${i+1}`);
+ cap.accepts_docType = DocumentType.RTF;
+ cap.accepts_tagType = 'caption';
+ personDocs.push(makePlaceholder(cap, x, y + 70, 150, 30));
+ }
+ placeholders = personDocs;
+ break;
+
+ case ScrapbookPresetType.Spotlight:
+ // Full-width title, then a stacking of an internal person image + landscape, then description
+ const titleSpot = Docs.Create.TextDocument('title');
+ titleSpot.accepts_docType = DocumentType.RTF;
+ titleSpot.accepts_tagType = 'title';
+ const phTitleSpot = makePlaceholder(titleSpot, 0, -180, 400, 60);
+
+ const internalImg = Docs.Create.TextDocument('<person>');
+ internalImg.accepts_docType = DocumentType.IMG;
+ internalImg.accepts_tagType = 'PERSON';
+ const phInternal = makePlaceholder(internalImg, -100, -120, 120, 160);
+
+ const landscapeImg = Docs.Create.TextDocument('<landscape>');
+ landscapeImg.accepts_docType = DocumentType.IMG;
+ landscapeImg.accepts_tagType = 'LANDSCAPE';
+ const phLandscape = makePlaceholder(landscapeImg, 50, 0, 200, 160);
+
+ const stack = Docs.Create.StackingDocument(
+ [phInternal, phLandscape],
+ { _width: 360, _height: 180, title: 'spotlight stack' }
+ );
+ const phStack = (() => {
+ const d = new Doc();
+ d.proto = stack;
+ d.original = stack;
+ d.x = 8;
+ d.y = -84;
+ d._width = 360;
+ d._height = 180;
+ return d;
+ })();
+
+ const descSpot = Docs.Create.TextDocument('description');
+ descSpot.accepts_docType = DocumentType.RTF;
+ descSpot.accepts_tagType = 'lengthy description';
+ const phDescSpot = makePlaceholder(descSpot, 0, 140, 400, 100);
+
+ placeholders = [phTitleSpot, phStack, phDescSpot];
+ break;
+
+ default:
+ placeholders = [];
+ }
+
+ // finally assign into the dataDoc
+ this.dataDoc[this.fieldKey] = new List<Doc>(placeholders);
+ }
+
+
+
@action
setTitle() {
const title = `Scrapbook - ${this.createdDate}`;
@@ -49,6 +246,7 @@ export class ScrapbookBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
const image = Docs.Create.TextDocument('image');
image.accepts_docType = DocumentType.IMG;
+ image.accepts_tagType = 'LANDSCAPE' //should i be writing fields on this doc? clarify diff between this and proto, original
const placeholder = new Doc();
placeholder.proto = image;
placeholder.original = image;
@@ -57,10 +255,11 @@ export class ScrapbookBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
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';
+ summary.accepts_tagType = 'caption';
+ //summary.$tags_chat = new List<string>(['lengthy description']); //we need to go back and set this
const placeholder2 = new Doc();
placeholder2.proto = summary;
placeholder2.original = summary;
@@ -68,11 +267,64 @@ export class ScrapbookBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
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]);
+
+ const sidebar = Docs.Create.TextDocument('sidebar');
+ sidebar.accepts_docType = DocumentType.RTF;
+ sidebar.accepts_tagType = 'lengthy description'; //accepts_textType = 'lengthy description'
+ const placeholder3 = new Doc();
+ placeholder3.proto = sidebar;
+ placeholder3.original = sidebar;
+ placeholder3.x = 280;
+ placeholder3.y = -50;
+ placeholder3._width = 50;
+ placeholder3._height = 200;
+
+
+
+ const internalImg = Docs.Create.TextDocument('image internal');
+ internalImg.accepts_docType = DocumentType.IMG;
+ internalImg.accepts_tagType = 'PERSON' //should i be writing fields on this doc? clarify diff between this and proto, original
+ const placeholder5 = new Doc();
+ placeholder5.proto = internalImg;
+ placeholder5.original = internalImg;
+ placeholder5._width = 50;
+ placeholder5._height = 100;
+ placeholder5.x = 0;
+ placeholder5.y = -100;
+
+ const collection = Docs.Create.StackingDocument([placeholder5], { _width: 300, _height: 300, title: "internal coll" });
+ //collection.accepts_docType = DocumentType.COL; don't mark this field
+ const placeholder4 = new Doc();
+ placeholder4.proto = collection;
+ placeholder4.original = collection;
+ placeholder4.x = -200;
+ placeholder4.y = -100;
+ placeholder4._width = 100;
+ placeholder4._height = 200;
+ /*note-to-self
+ would doing:
+
+ const collection = Docs.Create.ScrapbookDocument([placeholder, placeholder2, placeholder3]);
+ create issues with references to the same object?*/
+
+ /*note-to-self
+ Should we consider that there are more collections than just COL type collections?
+ when spreading*/
+
+
+
+ /*note-to-self
+ difference between passing a new List<Doc> versus just the raw array?
+ */
+
+ this.dataDoc[this.fieldKey] = this.dataDoc[this.fieldKey] ?? new List<Doc>([placeholder, placeholder2, placeholder3, placeholder4]);
+
+
}
}
componentDidMount() {
+ //this.initScrapbook(ScrapbookPresetType.Default);
this.setTitle();
}
@@ -86,20 +338,64 @@ export class ScrapbookBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
};
filterAddDocument = (docIn: Doc | Doc[]) => {
- const docs = toList(docIn);
+ const docs = toList(docIn); //The docs being added to the scrapbook
+
+ // 1) Grab all template slots:
+ const slots = DocListCast(this.dataDoc[this.fieldKey]);
+
+ // 2) recursive unwrap:
+ const unwrap = (items: Doc[]): Doc[] =>
+ items.flatMap(d =>
+ d.$type === DocumentType.COL
+ ? unwrap(DocListCast(d[Doc.LayoutDataKey(d)]))
+ : [d]
+ );
+
+ // 3) produce a flat list of every doc, unwrapping any number of nested COLs
+ const allDocs: Doc[] = unwrap(slots);
+
+
if (docs?.length === 1) {
- const placeholder = DocListCast(this.dataDoc[this.fieldKey]).find(d =>
+ const placeholder = allDocs.filter(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
+ //DocListCast(this.Document.items).map(doc => DocListCast(doc[Doc.LayoutDataKey(doc)])
+
if (placeholder) {
+ /**Look at the autotags and see what matches*RTFCast(d[Doc.LayoutDataKey(d)])?.Text*/
// 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(() => {
+
+ const slotTagsList: Set<string>[] = placeholder.map(doc =>
+ new Set<string>(StrListCast(doc.$tags_chat))
+ );
+ // turn docs[0].$tags_chat into a Set
+ const targetTags = new Set(StrListCast(docs[0].$tags_chat));
+
//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];
+
+ // find the first placeholder that shares *any* tag
+ const match = placeholder.find(ph =>
+ ph.accepts_tagType != null && // make sure it actually has one
+ targetTags.has(StrCast(ph.accepts_tagType)) // test membership in the Set
+ //StrListCast(ph.$tags_chat).some(tag => targetTags.has(tag))
+ );
+ if (match) {
+ match.proto = docs[0];
+ }
+
+ /*const chosenPlaceholder = placeholder.find(d =>
+ pl = new Set<string>(StrListCast(d.$tags_chat)
+
+ d.$tags_chat && d.$tags_chat[0].equals(docs[0].$tags_chat)); //why [0]
+ if (chosenPlaceholder){
+ chosenPlaceholder.proto = docs[0];}*/
+ //excess if statement??
}, 'Scrapbook add')
);
return false;
@@ -124,6 +420,37 @@ export class ScrapbookBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
}
}
+
+
+function extractScrapbookConfigs(docs: Doc[]): ScrapbookItemConfig[] {
+ return docs.map(doc => extractConfig(doc));
+}
+
+// function extractConfig(doc: Doc): ScrapbookItemConfig {
+// const layoutKey = Doc.LayoutDataKey(doc);
+// const childDocs = doc[layoutKey] ? DocListCast(doc[layoutKey]) : [];
+
+// const isContainer = childDocs.length > 0;
+
+// const cfg: ScrapbookItemConfig = {
+// type: isContainer ? DocumentType.COL : doc.$type,
+// tag:
+// acceptTag: doc.accepts_tagType,
+// x: doc.x || 0,
+// y: doc.y || 0,
+// width: doc._width,
+// height: doc._height,
+// };
+
+// if (isContainer) {
+// cfg.containerWidth = doc.proto._width;
+// cfg.containerHeight = doc.proto._height;
+// cfg.children = childDocs.map(child => extractConfig(child));
+// }
+
+// return cfg;
+// }
+
// Register scrapbook
Docs.Prototypes.TemplateMap.set(DocumentType.SCRAPBOOK, {
layout: { view: ScrapbookBox, dataField: 'items' },
diff --git a/src/client/views/nodes/scrapbook/ScrapbookPreset.tsx b/src/client/views/nodes/scrapbook/ScrapbookPreset.tsx
new file mode 100644
index 000000000..3cae4382b
--- /dev/null
+++ b/src/client/views/nodes/scrapbook/ScrapbookPreset.tsx
@@ -0,0 +1,146 @@
+import { DocumentType } from '../../../documents/DocumentTypes';
+
+export enum ScrapbookPresetType {
+ Default = 'Default',
+ Classic = 'Classic',
+ Collage = 'Collage',
+ Spotlight = 'Spotlight',
+}
+
+export interface ScrapbookItemConfig {
+ type: DocumentType;
+ /** text shown in the placeholder bubble */
+ tag: string;
+ /** what this slot actually accepts (defaults to `tag`) */
+ acceptTag?: string;
+
+ x: number;
+ y: number;
+ /** the frame this placeholder occupies */
+ width?: number;
+ height?: number;
+ /** if this is a container with children, use these for the proto’s own size */
+ containerWidth?: number;
+ containerHeight?: number;
+ children?: ScrapbookItemConfig[];
+}
+
+export class ScrapbookPreset {
+ static createPreset(presetType: ScrapbookPresetType): ScrapbookItemConfig[] {
+ switch (presetType) {
+ case ScrapbookPresetType.Classic:
+ return ScrapbookPreset.createClassicPreset();
+ case ScrapbookPresetType.Collage:
+ return ScrapbookPreset.createCollagePreset();
+ case ScrapbookPresetType.Spotlight:
+ return ScrapbookPreset.createSpotlightPreset();
+ case ScrapbookPresetType.Default:
+ return ScrapbookPreset.createDefaultPreset();
+ default:
+ throw new Error(`Unknown preset type: ${presetType}`);
+ }
+ }
+
+ private static createClassicPreset(): ScrapbookItemConfig[] {
+ return [
+ { type: DocumentType.IMG,
+ tag: 'LANDSCAPE',
+ acceptTag: 'LANDSCAPE',
+ x: 0, y: -100, width: 250, height: 200
+ },
+ { type: DocumentType.RTF,
+ tag: 'caption',
+ acceptTag: 'caption',
+ x: 0, y: 200, width: 250, height: 50
+ },
+ { type: DocumentType.RTF,
+ tag: 'lengthy description',
+ acceptTag: 'lengthy description',
+ x: 280, y: -50, width: 50, height: 200
+ },
+ { type: DocumentType.IMG,
+ tag: 'PERSON',
+ acceptTag: 'PERSON',
+ x: -200, y: -100, width: 100, height: 200
+ },
+ ];
+ }
+
+ private static createDefaultPreset(): ScrapbookItemConfig[] {
+ return [
+ { type: DocumentType.IMG,
+ tag: 'image',
+ acceptTag: 'LANDSCAPE',
+ x: 0, y: -100, width: 250, height: 200
+ },
+ { type: DocumentType.RTF,
+ tag: 'summary',
+ acceptTag: 'caption',
+ x: 0, y: 200, width: 250
+ },
+ { type: DocumentType.RTF,
+ tag: 'sidebar',
+ acceptTag: 'lengthy description',
+ x: 280, y: -50, width: 50, height: 200
+ },
+ {
+ type: DocumentType.COL,
+ tag: 'internal coll',
+ x: -200, y: -100, width: 100, height: 200,
+ containerWidth: 300, containerHeight: 300,
+ children: [
+ { type: DocumentType.IMG,
+ tag: 'image internal',
+ acceptTag: 'PERSON',
+ x: 0, y: 0, width: 50, height: 100
+ }
+ ]
+ }
+ ];
+ }
+
+ private static createCollagePreset(): ScrapbookItemConfig[] {
+ return [
+ { type: DocumentType.IMG,
+ tag: 'LANDSCAPE',
+ acceptTag: 'LANDSCAPE',
+ x: -150, y: -150, width: 150, height: 150
+ },
+ { type: DocumentType.IMG,
+ tag: 'PERSON',
+ acceptTag: 'PERSON',
+ x: 0, y: -150, width: 150, height: 150
+ },
+ { type: DocumentType.RTF,
+ tag: 'caption',
+ acceptTag: 'caption',
+ x: -150, y: 0, width: 300, height: 100
+ },
+ { type: DocumentType.RTF,
+ tag: 'lengthy description',
+ acceptTag: 'lengthy description',
+ x: 0, y: 100, width: 300, height: 100
+ }
+ ];
+ }
+
+ private static createSpotlightPreset(): ScrapbookItemConfig[] {
+ return [
+ { type: DocumentType.RTF,
+ tag: 'title',
+ acceptTag: 'title',
+ x: 0, y: -180, width: 300, height: 40
+ },
+ { type: DocumentType.IMG,
+ tag: 'LANDSCAPE',
+ acceptTag: 'LANDSCAPE',
+ x: 0, y: 0, width: 300, height: 200
+ },
+ { type: DocumentType.RTF,
+ tag: 'caption',
+ acceptTag: 'caption',
+ x: 0, y: 230, width: 300, height: 50
+ }
+ ];
+ }
+}
diff --git a/src/client/views/nodes/scrapbook/ScrapbookSettingsPanel.tsx b/src/client/views/nodes/scrapbook/ScrapbookSettingsPanel.tsx
new file mode 100644
index 000000000..5808ab4d1
--- /dev/null
+++ b/src/client/views/nodes/scrapbook/ScrapbookSettingsPanel.tsx
@@ -0,0 +1,60 @@
+import React from 'react';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { faRedoAlt } from '@fortawesome/free-solid-svg-icons';
+import { action } from 'mobx';
+
+export default class ScrapbookSettingsPanel extends React.Component {
+
+ constructor(props) {
+ super(props);
+ this.state = { regenerating: false };
+ }
+
+ regenerateScrapbook = async () => {
+ this.setState({ regenerating: true });
+ try {
+ // Example API call or method invoking ChatGPT for JSON
+ const newLayout = await fetch('/api/generate-scrapbook-layout', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ currentLayout: this.props.currentLayout })
+ }).then(res => res.json());
+
+ action(() => {
+ // Apply new layout
+ this.props.applyNewLayout(newLayout);
+ })();
+ } catch (err) {
+ console.error('Failed to regenerate layout:', err);
+ } finally {
+ this.setState({ regenerating: false });
+ }
+ };
+
+ render() {
+ const { regenerating } = this.state;
+
+ return (
+ <div className="scrapbook-settings-panel" style={{ display: 'flex', alignItems: 'center', padding: '8px', backgroundColor: '#f0f0f0', borderRadius: '8px', boxShadow: '0 4px 8px rgba(0,0,0,0.1)' }}>
+ <button
+ className="regenerate-scrapbook-btn"
+ title="Regenerate Scrapbook"
+ onClick={this.regenerateScrapbook}
+ disabled={regenerating}
+ style={{
+ padding: '8px 12px',
+ background: regenerating ? '#ccc' : '#007bff',
+ color: 'white',
+ border: 'none',
+ borderRadius: '4px',
+ cursor: regenerating ? 'default' : 'pointer',
+ display: 'flex',
+ alignItems: 'center'
+ }}>
+ <FontAwesomeIcon icon={faRedoAlt} style={{ marginRight: '6px' }} />
+ {regenerating ? 'Regenerating...' : 'Regenerate Scrapbook'}
+ </button>
+ </div>
+ );
+ }
+} \ No newline at end of file
diff --git a/src/client/views/search/FaceRecognitionHandler.tsx b/src/client/views/search/FaceRecognitionHandler.tsx
index 3ad5bc844..256e68afd 100644
--- a/src/client/views/search/FaceRecognitionHandler.tsx
+++ b/src/client/views/search/FaceRecognitionHandler.tsx
@@ -9,6 +9,8 @@ import { ImageField } from '../../../fields/URLField';
import { DocumentType } from '../../documents/DocumentTypes';
import { Docs } from '../../documents/Documents';
import { DocumentManager } from '../../util/DocumentManager';
+import { reaction } from 'mobx';
+import { DocumentView } from '../nodes/DocumentView';
/**
* A singleton class that handles face recognition and manages face Doc collections for each face found.
@@ -33,7 +35,7 @@ export class FaceRecognitionHandler {
// eslint-disable-next-line no-use-before-define
static _instance: FaceRecognitionHandler;
private _apiModelReady = false;
- private _pendingAPIModelReadyDocs: Doc[] = [];
+ private _pendingAPIModelReadyDocs: DocumentView[] = [];
public static get Instance() {
return FaceRecognitionHandler._instance ?? new FaceRecognitionHandler();
@@ -126,7 +128,7 @@ export class FaceRecognitionHandler {
constructor() {
FaceRecognitionHandler._instance = this;
this.loadAPIModels().then(() => this._pendingAPIModelReadyDocs.forEach(this.classifyFacesInImage));
- DocumentManager.Instance.AddAnyViewRenderedCB(dv => FaceRecognitionHandler.Instance.classifyFacesInImage(dv.Document));
+ DocumentManager.Instance.AddAnyViewRenderedCB(dv => FaceRecognitionHandler.Instance.classifyFacesInImage(dv));
}
/**
@@ -199,14 +201,18 @@ export class FaceRecognitionHandler {
* match them to existing unique faces, otherwise new unique face(s) are created.
* @param imgDoc The document being analyzed.
*/
- private classifyFacesInImage = async (imgDoc: Doc) => {
+ private classifyFacesInImage = async (imgDocView: DocumentView) => {
+ const imgDoc = imgDocView.Document;
if (!Doc.UserDoc().recognizeFaceImages) return;
const activeDashboard = Doc.ActiveDashboard;
if (!this._apiModelReady || !activeDashboard) {
- this._pendingAPIModelReadyDocs.push(imgDoc);
+ this._pendingAPIModelReadyDocs.push(imgDocView);
} else if (imgDoc.type === DocumentType.LOADING && !imgDoc.loadingError) {
- setTimeout(() => this.classifyFacesInImage(imgDoc), 1000);
+ setTimeout(() => this.classifyFacesInImage(imgDocView), 1000);
} else {
+ reaction(() => ({sel:imgDocView.isSelected()}), ({sel}) => !sel &&
+ imgDocView.ComponentView?.autoTag?.(), {fireImmediately: true}
+ )
const imgUrl = ImageCast(imgDoc[Doc.LayoutDataKey(imgDoc)]);
if (imgUrl && !DocListCast(Doc.MyFaceCollection?.examinedFaceDocs).includes(imgDoc[DocData])) {
// only examine Docs that have an image and that haven't already been examined.