aboutsummaryrefslogtreecommitdiff
path: root/src/client/views
diff options
context:
space:
mode:
Diffstat (limited to 'src/client/views')
-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/scrapbook/ScrapbookBox.tsx231
-rw-r--r--src/client/views/nodes/scrapbook/ScrapbookPreset.tsx146
-rw-r--r--src/client/views/nodes/scrapbook/ScrapbookSettingsPanel.tsx60
5 files changed, 473 insertions, 1 deletions
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/scrapbook/ScrapbookBox.tsx b/src/client/views/nodes/scrapbook/ScrapbookBox.tsx
index 33761efc9..731715964 100644
--- a/src/client/views/nodes/scrapbook/ScrapbookBox.tsx
+++ b/src/client/views/nodes/scrapbook/ScrapbookBox.tsx
@@ -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}`;
@@ -120,13 +317,14 @@ export class ScrapbookBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
difference between passing a new List<Doc> versus just the raw array?
*/
- this.dataDoc[this.fieldKey] = new List<Doc>([placeholder, placeholder2, placeholder3, placeholder4]);
+ this.dataDoc[this.fieldKey] = this.dataDoc[this.fieldKey] ?? new List<Doc>([placeholder, placeholder2, placeholder3, placeholder4]);
}
}
componentDidMount() {
+ //this.initScrapbook(ScrapbookPresetType.Default);
this.setTitle();
}
@@ -222,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