import { action, makeObservable, observable, reaction, computed } from 'mobx'; import * as React from 'react'; import { Doc, DocListCast, StrListCast } from '../../../../fields/Doc'; import { List } from '../../../../fields/List'; import { emptyFunction } from '../../../../Utils'; import { Docs } from '../../../documents/Documents'; import { DocumentType } from '../../../documents/DocumentTypes'; import { CollectionView } from '../../collections/CollectionView'; import { ViewBoxAnnotatableComponent } from '../../DocComponent'; import { AspectRatioLimits } from '../../smartdraw/FireflyConstants'; import { DocumentView } from '../DocumentView'; import { FieldView, FieldViewProps } from '../FieldView'; import { DragManager } from '../../../util/DragManager'; import { RTFCast, StrCast, toList } from '../../../../fields/Types'; import { undoable } from '../../../util/UndoManager'; import ReactLoading from 'react-loading'; import { NumCast } from '../../../../fields/Types'; import { ScrapbookItemConfig, ScrapbookPreset } from './ScrapbookPreset'; import { ImageBox } from '../ImageBox'; import { FireflyImageDimensions } from '../../smartdraw/FireflyConstants'; import { SmartDrawHandler } from '../../smartdraw/SmartDrawHandler'; import { ImageCast } from '../../../../fields/Types'; import { SnappingManager } from '../../../util/SnappingManager'; import { IReactionDisposer } from 'mobx'; import { observer } from 'mobx-react'; import { runInAction } from 'mobx'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faRedoAlt } from '@fortawesome/free-solid-svg-icons'; import { getPresetNames, createPreset } from './ScrapbookPresetRegistry'; import './ScrapbookBox.scss'; export function buildPlaceholdersFromConfigs(configs: ScrapbookItemConfig[]): Doc[] { const placeholders: Doc[] = []; for (const cfg of configs) { if (cfg.children && cfg.children.length) { const childDocs = cfg.children.map(child => { const doc = Docs.Create.TextDocument("[placeholder] " + child.tag); doc.accepts_docType = child.type; doc.accepts_tagType = new List(child.acceptTags ?? [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; // Create a stacking document with the child placeholders 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 { const doc = Docs.Create.TextDocument("[placeholder] " + cfg.tag); doc.accepts_docType = cfg.type; doc.accepts_tagType = new List(cfg.acceptTags ?? [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); } } return placeholders; } export function slotRealDocIntoPlaceholders( realDoc: Doc, placeholders: Doc[] ): boolean { const realTags = new Set( StrListCast(realDoc.$tags_chat ?? new List()) .map(t => t.toLowerCase()) ); // Find placeholder with most matching tags let bestMatch: Doc | null = null; let maxMatches = 0; /* (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)))*/ placeholders.forEach(ph => { // 1) Enforce that placeholder.accepts_docType === realDoc.$type if (ph.accepts_docType !== realDoc.$type) { // Skip this placeholder entirely if types do not match. return; }; const phTagTypes = StrListCast(ph.accepts_tagType ?? new List()) .map(t => t.toLowerCase()); console.log({ realTags, phTagTypes }); const matches = phTagTypes.filter(tag => realTags.has(tag)); if (matches.length > maxMatches) { maxMatches = matches.length; bestMatch = ph; } }); if (bestMatch && maxMatches > 0) { setTimeout( undoable(() => { bestMatch!.proto = realDoc; }, 'Scrapbook add'), 0 ); return true; } return false; } // Scrapbook view: a container that lays out its child items in a template @observer export class ScrapbookBox extends ViewBoxAnnotatableComponent() { @observable selectedPreset = getPresetNames()[0]; @observable createdDate: string; @observable loading = false; @observable src = ''; @observable imgDoc: Doc | undefined; private _disposers: { [name: string]: IReactionDisposer } = {}; private imageBoxRef = React.createRef(); // @observable configs : ScrapbookItemConfig[] constructor(props: FieldViewProps) { super(props); makeObservable(this); const existingItems = DocListCast(this.dataDoc[this.fieldKey] as List); if (!existingItems || existingItems.length === 0) { // Only wire up reaction/setTitle if it's truly a brand-new, empty Scrapbook reaction( () => this.selectedPreset, presetName => this.initScrapbook(presetName), { fireImmediately: true } ); this.createdDate = this.getFormattedDate(); this.setTitle(); } else { // If items are already present, just preserve whatever was injected. // We still want `createdDate` set so that the UI title bar can show it if needed. 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.initScrapbook(ScrapbookPresetType.Default); //this.setLayout(ScrapbookPreset.Spotlight); } public static LayoutString(fieldStr: string) { return FieldView.LayoutString(ScrapbookBox, fieldStr); } getFormattedDate(): string { return new Date().toLocaleDateString(undefined, { year: 'numeric', month: 'short', day: 'numeric', }); } @action initScrapbook(name: string) { const configs = createPreset(name); // 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 placeholders = buildPlaceholdersFromConfigs(configs); // 3) commit them into the field this.dataDoc[this.fieldKey] = new List(placeholders); } @action setTitle() { const title = `Scrapbook - ${this.createdDate}`; if (this.dataDoc.title !== title) { this.dataDoc.title = title; if (!this.dataDoc[this.fieldKey]){ const image = Docs.Create.TextDocument('[placeholder] person image'); image.accepts_docType = DocumentType.IMG; image.accepts_tagType = 'PERSON' //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; placeholder._width = 250; placeholder._height = 200; placeholder.x = 0; placeholder.y = -100; //placeholder.overrideFields = new List(['x', 'y']); // shouldn't need to do this for layout fields since the placeholder already overrides its protos const summary = Docs.Create.TextDocument('[placeholder] long summary'); summary.accepts_docType = DocumentType.RTF; summary.accepts_tagType = 'lengthy description'; //summary.$tags_chat = new List(['lengthy description']); //we need to go back and set this const placeholder2 = new Doc(); placeholder2.proto = summary; placeholder2.original = summary; placeholder2.x = 0; placeholder2.y = 200; placeholder2._width = 250; //placeholder2.overrideFields = new List(['x', 'y', '_width']); // shouldn't need to do this for layout fields since the placeholder already overrides its protos const sidebar = Docs.Create.TextDocument('[placeholder] brief sidebar'); sidebar.accepts_docType = DocumentType.RTF; sidebar.accepts_tagType = 'title'; //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('[placeholder] landscape internal'); internalImg.accepts_docType = DocumentType.IMG; internalImg.accepts_tagType = 'LANDSCAPE' //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 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(); this.generateAiImageCorrect(); this._disposers.propagateResize = reaction( () => ({ w: this.layoutDoc._width, h: this.layoutDoc._height }), (dims, prev) => { // prev is undefined on the first run, so bail early if (!prev || !SnappingManager.ShiftKey || !this.imgDoc) return; // either guard the ref… const imageBox = this.imageBoxRef.current; if (!imageBox) return; // …or just hard-code the fieldKey if you know it’s always `"data"` const key = imageBox.props.fieldKey; runInAction(() => { if(!this.imgDoc){ return } // use prev.w/h (the *old* size) as your orig dims this.imgDoc[key + '_outpaintOriginalWidth'] = prev.w; this.imgDoc[key + '_outpaintOriginalHeight'] = prev.h; ;(this.imageBoxRef.current as any).layoutDoc._width = dims.w ;(this.imageBoxRef.current as any).layoutDoc._height = dims.h }); } ); } @action async generateAiImageCorrect(prompt?: string) { this.loading = true; try { // 1) Default to regenPrompt if none provided if (!prompt) prompt = this.regenPrompt; // 2) Measure the scrapbook’s current size const w = NumCast(this.layoutDoc._width, 1); const h = NumCast(this.layoutDoc._height, 1); const ratio = w / h; // 3) Pick the Firefly preset that best matches the aspect ratio let preset = FireflyImageDimensions.Square; if (ratio > AspectRatioLimits[FireflyImageDimensions.Widescreen]) { preset = FireflyImageDimensions.Widescreen; } else if (ratio > AspectRatioLimits[FireflyImageDimensions.Landscape]) { preset = FireflyImageDimensions.Landscape; } else if (ratio < AspectRatioLimits[FireflyImageDimensions.Portrait]) { preset = FireflyImageDimensions.Portrait; } // 4) Call exactly the same CreateWithFirefly that ImageBox uses const doc = await SmartDrawHandler.CreateWithFirefly(prompt, preset); if (doc instanceof Doc) { // 5) Hook it into your state this.imgDoc = doc; const imgField = ImageCast(doc.data); this.src = imgField?.url.href ?? ''; } else { alert('Failed to generate document.'); this.src = ''; } } catch (e) { alert(`Generation error: ${e}`); } finally { runInAction(() => { this.loading = false; }); } } childRejectDrop = (de: DragManager.DropEvent, subView?: DocumentView) => { return true; // disable dropping documents onto any child of the scrapbook. }; rejectDrop = (de: DragManager.DropEvent, subView?: DocumentView) => { // Test to see if the dropped doc is dropped on an acceptable location (anywerhe? on a specific box). // const draggedDocs = de.complete.docDragData?.draggedDocuments; return false; // allow all Docs to be dropped onto scrapbook -- let filterAddDocument make the final decision. }; filterAddDocument = (docIn: Doc | Doc[]) => { const docs = toList(docIn); //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) { return slotRealDocIntoPlaceholders( docs[0], allDocs, ) ? false : false; } return false; }; @computed get regenPrompt() { const slots = DocListCast(this.dataDoc[this.fieldKey]); const unwrap = (items: Doc[]): Doc[] => items.flatMap(d => d.$type === DocumentType.COL ? unwrap(DocListCast(d[Doc.LayoutDataKey(d)])) : [d] ); const allDocs: Doc[] = unwrap(slots); const internalTagsSet = new Set(); allDocs.forEach(doc => { const tags = StrListCast(doc.$tags_chat ?? new List()); tags.forEach(tag => {if (!tag.startsWith("ASPECT_")) { internalTagsSet.add(tag); } }); }); const internalTags = Array.from(internalTagsSet).join(', '); return internalTags ? `Create a new scrapbook background featuring: ${internalTags}` : 'A serene mountain landscape at sunrise, ultra-wide, pastel sky, abstract, scrapbook background'; } render() { return (
{this.loading && (
)} {this.src && this.imgDoc && ( )} {this._props.isContentActive() && (
)}
); } } Docs.Prototypes.TemplateMap.set(DocumentType.SCRAPBOOK, { layout: { view: ScrapbookBox, dataField: 'items' }, options: { acl: '', _height: 200, _xMargin: 10, _yMargin: 10, _layout_fitWidth: false, _layout_autoHeight: true, _layout_reflowVertical: true, _layout_reflowHorizontal: true, _freeform_fitContentsToBox: true, defaultDoubleClick: 'ignore', systemIcon: 'BsImages', }, });