From fec7f7dd736af3296a2df52acd07f3eadb8123c3 Mon Sep 17 00:00:00 2001 From: sharkiecodes Date: Thu, 1 May 2025 00:44:14 -0400 Subject: marquee selection --- src/client/apis/gpt/GPT.ts | 9 + .../collectionFreeForm/MarqueeOptionsMenu.tsx | 2 + .../collections/collectionFreeForm/MarqueeView.tsx | 35 ++++ src/client/views/nodes/scrapbook/ScrapbookBox.tsx | 231 ++++++++++++++++++++- .../views/nodes/scrapbook/ScrapbookPreset.tsx | 146 +++++++++++++ .../nodes/scrapbook/ScrapbookSettingsPanel.tsx | 60 ++++++ 6 files changed, 482 insertions(+), 1 deletion(-) create mode 100644 src/client/views/nodes/scrapbook/ScrapbookPreset.tsx create mode 100644 src/client/views/nodes/scrapbook/ScrapbookSettingsPanel.tsx (limited to 'src') diff --git a/src/client/apis/gpt/GPT.ts b/src/client/apis/gpt/GPT.ts index 1956fef0c..693b4f901 100644 --- a/src/client/apis/gpt/GPT.ts +++ b/src/client/apis/gpt/GPT.ts @@ -126,6 +126,7 @@ const callTypeMap: { [type in GPTCallType]: GPTCallOpts } = { temp: 0.5, prompt: 'You will be given a list of field descriptions for one or more templates in the format {field #0: “description”}{field #1: “description”}{...}, and a list of column descriptions in the format {“title”: “description”}{...}. Your job is to match columns with fields based on their descriptions. Your output should be in the following JSON format: {“template_title”:{“#”: “title”, “#”: “title”, “#”: “title” …}, “template_title”:{“#”: “title”, “#”: “title”, “#”: “title” …}} where “template_title” is the templates title as specified in the description provided, # represents the field # and “title” the title of the column assigned to it. A filled out example might look like {“fivefield2”:{“0”:”Name”, “1”:”Image”, “2”:”Caption”, “3”:”Position”, “4”:”Stats”}, “fivefield3”:{0:”Image”, 1:”Name”, 2:”Caption”, 3:”Stats”, 4:”Position”}. Include one object for each template. IT IS VERY IMPORTANT THAT YOU ONLY INCLUDE TEXT IN THE FORMAT ABOVE, WITH NO ADDITIONS WHATSOEVER. Do not include extraneous ‘#’ characters, ‘column’ for columns, or ‘template’ for templates: ONLY THE TITLES AND NUMBERS. There should never be one column assigned to more than one field (ie. if the “name” column is assigned to field 1, it can’t be assigned to any other fields) . Do this for each template whose fields are described. The descriptions are as follows:', }, + vizsum: { model: 'gpt-4-turbo', maxTokens: 512, @@ -157,6 +158,14 @@ const callTypeMap: { [type in GPTCallType]: GPTCallOpts } = { temp: 0.5, prompt: 'You will be coloring drawings. You will be given what the drawing is, then a list of descriptions for parts of the drawing. Based on each description, respond with the stroke and fill color that it should be. Follow the rules: 1. Avoid using black for stroke color 2. Make the stroke color 1-3 shades darker than the fill color 3. Use the same colors when possible. Format as {#abcdef #abcdef}, making sure theres a color for each description, and do not include any additional text.', }, + + scrapbook: { + model: 'gpt-4-turbo', + maxTokens: 512, + temp: 0.5, + prompt: 'You will be given a list of field descriptions for one or more templates in the format {field #0: “description”}{field #1: “description”}{...}, and a list of column descriptions in the format {“title”: “description”}{...}. Your job is to match columns with fields based on their descriptions. Your output should be in the following JSON format: {“template_title”:{“#”: “title”, “#”: “title”, “#”: “title” …}, “template_title”:{“#”: “title”, “#”: “title”, “#”: “title” …}} where “template_title” is the templates title as specified in the description provided, # represents the field # and “title” the title of the column assigned to it. A filled out example might look like {“fivefield2”:{“0”:”Name”, “1”:”Image”, “2”:”Caption”, “3”:”Position”, “4”:”Stats”}, “fivefield3”:{0:”Image”, 1:”Name”, 2:”Caption”, 3:”Stats”, 4:”Position”}. Include one object for each template. IT IS VERY IMPORTANT THAT YOU ONLY INCLUDE TEXT IN THE FORMAT ABOVE, WITH NO ADDITIONS WHATSOEVER. Do not include extraneous ‘#’ characters, ‘column’ for columns, or ‘template’ for templates: ONLY THE TITLES AND NUMBERS. There should never be one column assigned to more than one field (ie. if the “name” column is assigned to field 1, it can’t be assigned to any other fields) . Do this for each template whose fields are described. The descriptions are as follows:', + }, + command_type: { model: 'gpt-4-turbo', maxTokens: 1024, 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 { 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 { } color={this.userColor} /> this.createCollection(e, true)} icon={} color={this.userColor} /> } color={this.userColor} /> + } color={this.userColor} /> } color={this.userColor} /> } color={this.userColor} /> } 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 { + let docs = new Array(); + 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() { @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 in dataDoc['items'] if (!this.dataDoc[this.fieldKey]) { this.dataDoc[this.fieldKey] = new List(); } 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() }); } + + @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(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(''); + internalImg.accepts_docType = DocumentType.IMG; + internalImg.accepts_tagType = 'PERSON'; + const phInternal = makePlaceholder(internalImg, -100, -120, 120, 160); + + const landscapeImg = Docs.Create.TextDocument(''); + 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(placeholders); + } + + + @action setTitle() { const title = `Scrapbook - ${this.createdDate}`; @@ -120,13 +317,14 @@ export class ScrapbookBox extends ViewBoxAnnotatableComponent() difference between passing a new List versus just the raw array? */ - this.dataDoc[this.fieldKey] = new List([placeholder, placeholder2, placeholder3, placeholder4]); + this.dataDoc[this.fieldKey] = this.dataDoc[this.fieldKey] ?? new List([placeholder, placeholder2, placeholder3, placeholder4]); } } componentDidMount() { + //this.initScrapbook(ScrapbookPresetType.Default); this.setTitle(); } @@ -222,6 +420,37 @@ export class ScrapbookBox extends ViewBoxAnnotatableComponent() } } + + +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 ( +
+ +
+ ); + } +} \ No newline at end of file -- cgit v1.2.3-70-g09d2