From 6900008b1bb89cca1eab7b95f17ee33fa335282f Mon Sep 17 00:00:00 2001 From: sharkiecodes Date: Mon, 28 Apr 2025 20:18:26 -0400 Subject: adding autotagging --- src/client/views/ViewBoxInterface.ts | 1 + .../collections/collectionFreeForm/ImageLabelBox.tsx | 4 ++-- src/client/views/nodes/ImageBox.tsx | 7 +++++++ src/client/views/nodes/scrapbook/ScrapbookBox.tsx | 16 +++++++++++++++- src/client/views/search/FaceRecognitionHandler.tsx | 16 +++++++++++----- 5 files changed, 36 insertions(+), 8 deletions(-) (limited to 'src') diff --git a/src/client/views/ViewBoxInterface.ts b/src/client/views/ViewBoxInterface.ts index 0ddac8914..5726a78f3 100644 --- a/src/client/views/ViewBoxInterface.ts +++ b/src/client/views/ViewBoxInterface.ts @@ -24,6 +24,7 @@ export abstract class ViewBoxInterface

extends ObservableReactComponent 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; // 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..28d3ccd48 100644 --- a/src/client/views/collections/collectionFreeForm/ImageLabelBox.tsx +++ b/src/client/views/collections/collectionFreeForm/ImageLabelBox.tsx @@ -158,7 +158,7 @@ export class ImageLabelBox extends ViewBoxBaseComponent() { this._currentLabel = e.target.value; }); - classifyImagesInBox = async () => { + classifyImagesInBox = async (prompt? : string) => { this.startLoading(); // Converts the images into a Base64 format, afterwhich the information is sent to GPT to label them. @@ -168,7 +168,7 @@ export class ImageLabelBox extends ViewBoxBaseComponent() { 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/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx index 999d7089b..38f0390db 100644 --- a/src/client/views/nodes/ImageBox.tsx +++ b/src/client/views/nodes/ImageBox.tsx @@ -119,6 +119,13 @@ export class ImageBox extends ViewBoxAnnotatableComponent() { this._dropDisposer?.(); ele && (this._dropDisposer = DragManager.MakeDropTarget(ele, this.drop.bind(this), this.Document)); }; + + autoTag = () => { + //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/scrapbook/ScrapbookBox.tsx b/src/client/views/nodes/scrapbook/ScrapbookBox.tsx index 6cfe9a62c..55d0df585 100644 --- a/src/client/views/nodes/scrapbook/ScrapbookBox.tsx +++ b/src/client/views/nodes/scrapbook/ScrapbookBox.tsx @@ -68,7 +68,21 @@ export class ScrapbookBox extends ViewBoxAnnotatableComponent() 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 - this.dataDoc[this.fieldKey] = new List([placeholder, placeholder2]); + + const sidebar = Docs.Create.TextDocument('sidebar'); + sidebar.accepts_docType = DocumentType.RTF; + sidebar.accepts_textType = 'sidebar'; + const placeholder3 = new Doc(); + placeholder3.proto = sidebar; + placeholder3.original = sidebar; + placeholder3.x = 280; + placeholder3.y = -50; + placeholder3._width = 50; + placeholder3._height = 200; + + this.dataDoc[this.fieldKey] = new List([placeholder, placeholder2, placeholder3]); + + } } diff --git a/src/client/views/search/FaceRecognitionHandler.tsx b/src/client/views/search/FaceRecognitionHandler.tsx index cb837e3ab..436cca8cf 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 && imgDoc.type == 'text' + && 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. -- cgit v1.2.3-70-g09d2 From 2a36216359054532084be24ab9034cd08c1c8798 Mon Sep 17 00:00:00 2001 From: sharkiecodes Date: Mon, 28 Apr 2025 21:13:31 -0400 Subject: working on autotagging --- src/client/views/collections/collectionFreeForm/ImageLabelBox.tsx | 6 ++++-- src/client/views/nodes/ImageBox.tsx | 6 +++++- 2 files changed, 9 insertions(+), 3 deletions(-) (limited to 'src') diff --git a/src/client/views/collections/collectionFreeForm/ImageLabelBox.tsx b/src/client/views/collections/collectionFreeForm/ImageLabelBox.tsx index 28d3ccd48..bf674c025 100644 --- a/src/client/views/collections/collectionFreeForm/ImageLabelBox.tsx +++ b/src/client/views/collections/collectionFreeForm/ImageLabelBox.tsx @@ -158,12 +158,14 @@ export class ImageLabelBox extends ViewBoxBaseComponent() { this._currentLabel = e.target.value; }); - classifyImagesInBox = async (prompt? : string) => { + classifyImagesInBox = async (selectedImages? : Doc[], prompt? : string) => { this.startLoading(); + 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 => diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx index 87168d17d..6e6ca3f73 100644 --- a/src/client/views/nodes/ImageBox.tsx +++ b/src/client/views/nodes/ImageBox.tsx @@ -46,6 +46,7 @@ import { FieldView, FieldViewProps } from './FieldView'; import { FocusViewOptions } from './FocusViewOptions'; import './ImageBox.scss'; import { OpenWhere } from './OpenWhere'; +import { ImageLabelBox } from '../collections/collectionFreeForm/ImageLabelBox'; const DefaultPath = '/assets/unknown-file-icon-hi.png'; export class ImageEditorData { @@ -120,7 +121,10 @@ export class ImageBox extends ViewBoxAnnotatableComponent() { ele && (this._dropDisposer = DragManager.MakeDropTarget(ele, this.drop.bind(this), this.Document)); }; - autoTag = () => { + autoTag = async () => { + ImageLabelBox.Instance.classifyImagesInBox([this.Document], "Classify this image as a PERSON or LANDSCAPE. You may only respond with one of these two options"); + + //Doc.getDescription(this.Document).then(desc => this.desc = desc) } -- cgit v1.2.3-70-g09d2 From 609ec6d37b5abf94f3ab84784544ebe47804cdf5 Mon Sep 17 00:00:00 2001 From: sharkiecodes Date: Tue, 29 Apr 2025 23:01:11 -0400 Subject: Autotag --- src/client/apis/gpt/GPT.ts | 35 +++++++++++---- .../collectionFreeForm/ImageLabelBox.tsx | 2 +- src/client/views/nodes/ImageBox.tsx | 48 ++++++++++++++++++--- .../views/nodes/formattedText/FormattedTextBox.tsx | 18 ++++++++ src/client/views/nodes/scrapbook/ScrapbookBox.tsx | 50 ++++++++++++++++++++-- src/client/views/search/FaceRecognitionHandler.tsx | 4 +- 6 files changed, 137 insertions(+), 20 deletions(-) (limited to 'src') diff --git a/src/client/apis/gpt/GPT.ts b/src/client/apis/gpt/GPT.ts index 9cb47995c..1956fef0c 100644 --- a/src/client/apis/gpt/GPT.ts +++ b/src/client/apis/gpt/GPT.ts @@ -11,6 +11,10 @@ export enum GPTDocCommand { export const DescriptionSeperator = '======'; export const DocSeperator = '------'; +export enum TextClassifications { + Title = 'title', //a few words + Caption = 'caption', //few sentences + LengthyDescription = 'lengthy description' } enum GPTCallType { SUMMARY = 'summary', @@ -36,6 +40,7 @@ enum GPTCallType { SUBSETDOCS = 'subset_docs', // select a subset of documents based on their descriptions DOCINFO = 'doc_info', // provide information about a document SORTDOCS = 'sort_docs', + CLASSIFYTEXT = 'classify_text', // classify text into one of the three categories: title, caption, lengthy description } type GPTCallOpts = { @@ -48,6 +53,23 @@ type GPTCallOpts = { const callTypeMap: { [type in GPTCallType]: GPTCallOpts } = { // newest model: gpt-4 summary: { model: 'gpt-4-turbo', maxTokens: 256, temp: 0.5, prompt: 'Summarize the text given in simpler terms.' }, + + + sort_docs: { + model: 'gpt-4o', + maxTokens: 2048, + temp: 0.25, + prompt: + `The user is going to give you a list of descriptions. + Each one is separated by '${DescriptionSeperator}' on either side. + Descriptions will vary in length, so make sure to only separate when you see '${DescriptionSeperator}'. + Sort them by the user's specifications. + Make sure each description is only in the list once. Each item should be separated by '${DescriptionSeperator}'. + Immediately afterward, surrounded by '${DocSeperator}' on BOTH SIDES, provide some insight into your reasoning for the way you sorted (and mention nothing about the formatting details given in this description). + It is VERY important that you format it exactly as described, ensuring the proper number of '${DescriptionSeperator[0]}' and '${DocSeperator[0]}' (${DescriptionSeperator.length} of each) and NO commas`, + }, + + edit: { model: 'gpt-4-turbo', maxTokens: 256, temp: 0.5, prompt: 'Reword the text.' }, stack: { model: 'gpt-4o', @@ -69,17 +91,14 @@ const callTypeMap: { [type in GPTCallType]: GPTCallOpts } = { temp: 0.5, prompt: "You are a helpful resarch assistant. Analyze the user's data to find meaningful patterns and/or correlation. Please only return a JSON with a correlation column 1 propert, a correlation column 2 property, and an analysis property. ", }, - sort_docs: { + //new + classify_text: { model: 'gpt-4o', maxTokens: 2048, temp: 0.25, - prompt: `The user is going to give you a list of descriptions. - Each one is separated by '${DescriptionSeperator}' on either side. - Descriptions will vary in length, so make sure to only separate when you see '${DescriptionSeperator}'. - Sort them by the user's specifications. - Make sure each description is only in the list once. Each item should be separated by '${DescriptionSeperator}'. - Immediately afterward, surrounded by '${DocSeperator}' on BOTH SIDES, provide some insight into your reasoning for the way you sorted (and mention nothing about the formatting details given in this description). - It is VERY important that you format it exactly as described, ensuring the proper number of '${DescriptionSeperator[0]}' and '${DocSeperator[0]}' (${DescriptionSeperator.length} of each) and NO commas`, + prompt: `Based on the content of the the text, classify it into the + most appropriate category: '${TextClassifications.Title}', '${TextClassifications.Caption}', or '${TextClassifications.LengthyDescription}'. Output exclusively the classification in your response. + ` }, describe: { model: 'gpt-4-vision-preview', maxTokens: 2048, temp: 0, prompt: 'Describe these images in 3-5 words' }, flashcard: { diff --git a/src/client/views/collections/collectionFreeForm/ImageLabelBox.tsx b/src/client/views/collections/collectionFreeForm/ImageLabelBox.tsx index bf674c025..038b1c6f9 100644 --- a/src/client/views/collections/collectionFreeForm/ImageLabelBox.tsx +++ b/src/client/views/collections/collectionFreeForm/ImageLabelBox.tsx @@ -160,7 +160,7 @@ export class ImageLabelBox extends ViewBoxBaseComponent() { 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. diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx index 6e6ca3f73..0b6814c01 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'; @@ -46,6 +47,7 @@ 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'; @@ -122,11 +124,47 @@ export class ImageBox extends ViewBoxAnnotatableComponent() { }; autoTag = async () => { - ImageLabelBox.Instance.classifyImagesInBox([this.Document], "Classify this image as a PERSON or LANDSCAPE. You may only respond with one of these two options"); - - //Doc.getDescription(this.Document).then(desc => this.desc = desc) + 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(); + (this.Document.$tags_chat as List).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) + diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx index 98e461a52..f4cbbcc9e 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 { + this.Document.$tags_chat = new List(); + 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).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); @@ -1236,6 +1245,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent Doc.IsSearchMatch(this.Document), @@ -1269,6 +1279,14 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent ({ 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 55d0df585..39729a1c5 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'; @@ -100,20 +100,62 @@ export class ScrapbookBox extends ViewBoxAnnotatableComponent() }; 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[] = placeholder.map(doc => + new Set(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 => + StrListCast(ph.$tags_chat).some(tag => targetTags.has(tag)) + ); + if (match) { + match.proto = docs[0]; + } + + /*const chosenPlaceholder = placeholder.find(d => + pl = new Set(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; diff --git a/src/client/views/search/FaceRecognitionHandler.tsx b/src/client/views/search/FaceRecognitionHandler.tsx index 60744588f..256e68afd 100644 --- a/src/client/views/search/FaceRecognitionHandler.tsx +++ b/src/client/views/search/FaceRecognitionHandler.tsx @@ -210,8 +210,8 @@ export class FaceRecognitionHandler { } else if (imgDoc.type === DocumentType.LOADING && !imgDoc.loadingError) { setTimeout(() => this.classifyFacesInImage(imgDocView), 1000); } else { - reaction(() => ({sel:imgDocView.isSelected()}), ({sel}) => !sel && imgDoc.type == 'text' - && imgDocView.ComponentView?.autoTag?.(), {fireImmediately: true} + 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])) { -- cgit v1.2.3-70-g09d2 From a5f0a1658319901bef0f386a2e70a5e421dfc3b9 Mon Sep 17 00:00:00 2001 From: sharkiecodes Date: Tue, 29 Apr 2025 23:17:59 -0400 Subject: imageboxes and rich text boxes are classified automatically by GPT and store these tags in tags_chat. When ambiguous (there exist multiple slots of the same type), Scrapbook can determine where to place docs through tags --- src/client/views/nodes/scrapbook/ScrapbookBox.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/src/client/views/nodes/scrapbook/ScrapbookBox.tsx b/src/client/views/nodes/scrapbook/ScrapbookBox.tsx index 39729a1c5..dd3eed6e4 100644 --- a/src/client/views/nodes/scrapbook/ScrapbookBox.tsx +++ b/src/client/views/nodes/scrapbook/ScrapbookBox.tsx @@ -57,10 +57,11 @@ export class ScrapbookBox extends ViewBoxAnnotatableComponent() 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('summary'); summary.accepts_docType = DocumentType.RTF; summary.accepts_textType = 'one line'; + //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; -- cgit v1.2.3-70-g09d2 From 675408d8358f1373d7892d2db3e6ecb06c098948 Mon Sep 17 00:00:00 2001 From: sharkiecodes Date: Wed, 30 Apr 2025 00:10:06 -0400 Subject: small updates --- src/client/views/nodes/scrapbook/ScrapbookBox.tsx | 26 ++++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) (limited to 'src') diff --git a/src/client/views/nodes/scrapbook/ScrapbookBox.tsx b/src/client/views/nodes/scrapbook/ScrapbookBox.tsx index dd3eed6e4..524925132 100644 --- a/src/client/views/nodes/scrapbook/ScrapbookBox.tsx +++ b/src/client/views/nodes/scrapbook/ScrapbookBox.tsx @@ -49,6 +49,7 @@ export class ScrapbookBox extends ViewBoxAnnotatableComponent() 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; @@ -60,7 +61,7 @@ export class ScrapbookBox extends ViewBoxAnnotatableComponent() 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(['lengthy description']); //we need to go back and set this const placeholder2 = new Doc(); placeholder2.proto = summary; @@ -72,7 +73,7 @@ export class ScrapbookBox extends ViewBoxAnnotatableComponent() const sidebar = Docs.Create.TextDocument('sidebar'); sidebar.accepts_docType = DocumentType.RTF; - sidebar.accepts_textType = 'sidebar'; + sidebar.accepts_tagType = 'lengthy description'; //accepts_textType = 'lengthy description' const placeholder3 = new Doc(); placeholder3.proto = sidebar; placeholder3.original = sidebar; @@ -81,6 +82,23 @@ export class ScrapbookBox extends ViewBoxAnnotatableComponent() placeholder3._width = 50; placeholder3._height = 200; + const collection = Docs.Create.ScrapbookDocument([]); + /*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]); @@ -144,7 +162,9 @@ export class ScrapbookBox extends ViewBoxAnnotatableComponent() // find the first placeholder that shares *any* tag const match = placeholder.find(ph => - StrListCast(ph.$tags_chat).some(tag => targetTags.has(tag)) + 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]; -- cgit v1.2.3-70-g09d2 From 6fe7caab9bff456eaca0096884feb5b7f6c9f6e5 Mon Sep 17 00:00:00 2001 From: sharkiecodes Date: Wed, 30 Apr 2025 18:25:42 -0400 Subject: fixes --- src/client/views/nodes/scrapbook/ScrapbookBox.tsx | 25 +++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) (limited to 'src') diff --git a/src/client/views/nodes/scrapbook/ScrapbookBox.tsx b/src/client/views/nodes/scrapbook/ScrapbookBox.tsx index 524925132..6d5ad031c 100644 --- a/src/client/views/nodes/scrapbook/ScrapbookBox.tsx +++ b/src/client/views/nodes/scrapbook/ScrapbookBox.tsx @@ -82,7 +82,28 @@ export class ScrapbookBox extends ViewBoxAnnotatableComponent() placeholder3._width = 50; placeholder3._height = 200; - const collection = Docs.Create.ScrapbookDocument([]); + + + 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 = image; + placeholder5.original = image; + 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: @@ -99,7 +120,7 @@ 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]); + this.dataDoc[this.fieldKey] = new List([placeholder, placeholder2, placeholder3, placeholder4]); } -- cgit v1.2.3-70-g09d2 From 37cdff4fb2383953f39cb0fee384a4779cd9fa7b Mon Sep 17 00:00:00 2001 From: sharkiecodes Date: Wed, 30 Apr 2025 19:50:21 -0400 Subject: fixes --- src/client/views/nodes/scrapbook/ScrapbookBox.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'src') diff --git a/src/client/views/nodes/scrapbook/ScrapbookBox.tsx b/src/client/views/nodes/scrapbook/ScrapbookBox.tsx index 6d5ad031c..33761efc9 100644 --- a/src/client/views/nodes/scrapbook/ScrapbookBox.tsx +++ b/src/client/views/nodes/scrapbook/ScrapbookBox.tsx @@ -88,8 +88,8 @@ export class ScrapbookBox extends ViewBoxAnnotatableComponent() 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 = image; - placeholder5.original = image; + placeholder5.proto = internalImg; + placeholder5.original = internalImg; placeholder5._width = 50; placeholder5._height = 100; placeholder5.x = 0; -- cgit v1.2.3-70-g09d2 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 From a2195058135bfac873aba2a5e8677baf8d90b201 Mon Sep 17 00:00:00 2001 From: bobzel Date: Fri, 9 May 2025 23:09:35 -0400 Subject: fixed reaction exception from accessing props and not _props --- src/client/views/nodes/formattedText/FormattedTextBox.tsx | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) (limited to 'src') diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx index 97049d0eb..10becc00b 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx +++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx @@ -307,11 +307,10 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent { this.Document.$tags_chat = new List(); - 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).push(desc)); + 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).push(desc)); this.Document._layout_showTags = true; - //or... then(desc => this.Document.$tags_chat = desc); - } + //or... then(desc => this.Document.$tags_chat = desc); + }; leafText = (node: Node) => { if (node.type === this.EditorView?.state.schema.nodes.dashField) { @@ -1246,7 +1245,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent Doc.IsSearchMatch(this.Document), @@ -1281,13 +1279,13 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent ({ title: this.Document.title, sel: this.props.isSelected() }), + () => ({ title: this.Document.title, sel: this._props.isSelected() }), action(() => { this.autoTag(); }), { fireImmediately: true } ); - + if (!this._props.dontRegisterView) { this._disposers.record = reaction( () => this.recordingDictation, -- cgit v1.2.3-70-g09d2 From 67450b443b70099ce51a8db2872b2c04e09b4558 Mon Sep 17 00:00:00 2001 From: sharkiecodes Date: Sun, 11 May 2025 21:28:05 -0400 Subject: scrapbook backgrounds --- src/client/apis/gpt/GPT.ts | 1 + src/client/views/nodes/scrapbook/ScrapbookBox.tsx | 100 ++++++++++++++++++---- 2 files changed, 85 insertions(+), 16 deletions(-) (limited to 'src') diff --git a/src/client/apis/gpt/GPT.ts b/src/client/apis/gpt/GPT.ts index 693b4f901..4dd30f8b3 100644 --- a/src/client/apis/gpt/GPT.ts +++ b/src/client/apis/gpt/GPT.ts @@ -30,6 +30,7 @@ enum GPTCallType { DRAW = 'draw', COLOR = 'color', TEMPLATE = 'template', + SCRAPBOOK = 'scrapbook', VIZSUM = 'vizsum', VIZSUM2 = 'vizsum2', FILL = 'fill', diff --git a/src/client/views/nodes/scrapbook/ScrapbookBox.tsx b/src/client/views/nodes/scrapbook/ScrapbookBox.tsx index 731715964..ced2df6c5 100644 --- a/src/client/views/nodes/scrapbook/ScrapbookBox.tsx +++ b/src/client/views/nodes/scrapbook/ScrapbookBox.tsx @@ -12,7 +12,13 @@ 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 { ScrapbookItemConfig, ScrapbookPreset } from './ScrapbookPreset'; +import { ImageBox } from '../ImageBox'; +import { FireflyImageDimensions } from '../../smartdraw/FireflyConstants'; +import { SmartDrawHandler } from '../../smartdraw/SmartDrawHandler'; +import { ImageCast } from '../../../../fields/Types'; +import { lengthToDegrees } from '@turf/turf'; enum ScrapbookPresetType { Classic = 'Classic', @@ -23,16 +29,24 @@ enum ScrapbookPresetType { // Scrapbook view: a container that lays out its child items in a grid/template export class ScrapbookBox extends ViewBoxAnnotatableComponent() { + + + state = { + loading: false, + src: '', + }; + + @observable createdDate: string; - @observable configs : ScrapbookItemConfig[] + // @observable configs : ScrapbookItemConfig[] constructor(props: FieldViewProps) { super(props); makeObservable(this); this.createdDate = this.getFormattedDate(); - this.configs = - ScrapbookPreset.createPreset(presetType); + //this.configs = + //ScrapbookPreset.createPreset(presetType); // ensure we always have a List in dataDoc['items'] if (!this.dataDoc[this.fieldKey]) { @@ -244,9 +258,9 @@ export class ScrapbookBox extends ViewBoxAnnotatableComponent() if (this.dataDoc.title !== title) { this.dataDoc.title = title; - const image = Docs.Create.TextDocument('image'); + const image = Docs.Create.TextDocument('person 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 + 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; @@ -256,9 +270,9 @@ export class ScrapbookBox extends ViewBoxAnnotatableComponent() 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('summary'); + const summary = Docs.Create.TextDocument('long summary'); summary.accepts_docType = DocumentType.RTF; - summary.accepts_tagType = 'caption'; + 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; @@ -268,9 +282,9 @@ export class ScrapbookBox extends ViewBoxAnnotatableComponent() 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('sidebar'); + const sidebar = Docs.Create.TextDocument('brief sidebar'); sidebar.accepts_docType = DocumentType.RTF; - sidebar.accepts_tagType = 'lengthy description'; //accepts_textType = 'lengthy description' + sidebar.accepts_tagType = 'title'; //accepts_textType = 'lengthy description' const placeholder3 = new Doc(); placeholder3.proto = sidebar; placeholder3.original = sidebar; @@ -281,9 +295,9 @@ export class ScrapbookBox extends ViewBoxAnnotatableComponent() - const internalImg = Docs.Create.TextDocument('image internal'); + const internalImg = Docs.Create.TextDocument('landscape 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 + 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; @@ -316,16 +330,43 @@ export class ScrapbookBox extends ViewBoxAnnotatableComponent() /*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]); + //this.dataDoc[this.fieldKey] = this.dataDoc[this.fieldKey] ?? new List([placeholder, placeholder2, placeholder3, placeholder4]); } } + + async generateAiImage() { + this.setState({ loading: true }); + + const prompt = 'A serene mountain landscape at sunrise, ultra-wide, pastel sky, abstract, scrapbook background'; + //'A serene mountain landscape at sunrise, ultra-wide, pastel sky'; + const dimensions = FireflyImageDimensions.Square; // or whichever suits your scenario + + SmartDrawHandler.CreateWithFirefly(prompt, dimensions) + .then(action(doc => { + if (doc instanceof Doc) { + const imgField = ImageCast(doc.data); + this.setState({ src: imgField?.url.href }); + } else { + alert('Failed to generate the image.'); + } + })) + .catch(e => { + alert(`Image generation error: ${e}`); + }) + .finally(() => { + this.setState({ loading: false }); + }); + } componentDidMount() { //this.initScrapbook(ScrapbookPresetType.Default); this.setTitle(); + this.generateAiImage(); + } childRejectDrop = (de: DragManager.DropEvent, subView?: DocumentView) => { @@ -405,8 +446,33 @@ export class ScrapbookBox extends ViewBoxAnnotatableComponent() }; render() { + const { loading, src } = this.state; + return ( -
+
+ {loading && ( +
+ +
+ )} + {loading && ( +
+ +
+ )} + {/* Render AI-generated background */} + {src && ( + + )} + () -function extractScrapbookConfigs(docs: Doc[]): ScrapbookItemConfig[] { - return docs.map(doc => extractConfig(doc)); -} +//function extractScrapbookConfigs(docs: Doc[]): ScrapbookItemConfig[] { + //return docs.map(doc => extractConfig(doc)); +//} // function extractConfig(doc: Doc): ScrapbookItemConfig { // const layoutKey = Doc.LayoutDataKey(doc); @@ -452,6 +518,8 @@ function extractScrapbookConfigs(docs: Doc[]): ScrapbookItemConfig[] { // } // Register scrapbook + + Docs.Prototypes.TemplateMap.set(DocumentType.SCRAPBOOK, { layout: { view: ScrapbookBox, dataField: 'items' }, options: { -- cgit v1.2.3-70-g09d2 From 64a9a1a982ec3f11adfed68cc0f18eb61059aff8 Mon Sep 17 00:00:00 2001 From: sharkiecodes Date: Sun, 11 May 2025 22:58:07 -0400 Subject: integrated outpainting with scrapbooks --- src/client/views/DocumentDecorations.tsx | 7 +- src/client/views/nodes/ImageBox.tsx | 1 - src/client/views/nodes/scrapbook/ScrapbookBox.tsx | 136 ++++++++++++++++++---- 3 files changed, 118 insertions(+), 26 deletions(-) (limited to 'src') diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx index ab665e984..94e5e662c 100644 --- a/src/client/views/DocumentDecorations.tsx +++ b/src/client/views/DocumentDecorations.tsx @@ -36,6 +36,7 @@ import { ImageBox } from './nodes/ImageBox'; import { OpenWhere, OpenWhereMod } from './nodes/OpenWhere'; import { FormattedTextBox } from './nodes/formattedText/FormattedTextBox'; import { TagsView } from './TagsView'; +import { ScrapbookBox } from './nodes/scrapbook/ScrapbookBox'; interface DocumentDecorationsProps { PanelWidth: number; @@ -446,7 +447,7 @@ export class DocumentDecorations extends ObservableReactComponent { SnappingManager.SetIsResizing(DocumentView.Selected().lastElement()?.Document[Id]); // turns off pointer events on things like youtube videos and web pages so that dragging doesn't get "stuck" when cursor moves over them DocumentView.Selected() - .filter(dv => e.shiftKey && dv.ComponentView instanceof ImageBox) + .filter(dv => e.shiftKey && dv.ComponentView instanceof ImageBox || dv.ComponentView instanceof ScrapbookBox) .forEach(dv => { dv.Document[dv.ComponentView!.fieldKey + '_outpaintOriginalWidth'] = NumCast(dv.Document._width); dv.Document[dv.ComponentView!.fieldKey + '_outpaintOriginalHeight'] = NumCast(dv.Document._height); @@ -502,7 +503,7 @@ export class DocumentDecorations extends ObservableReactComponent dv.ComponentView instanceof ImageBox) : []; + const outpainted = e.shiftKey ? DocumentView.Selected().filter(dv => dv.ComponentView instanceof ImageBox || dv.ComponentView instanceof ScrapbookBox) : []; const notOutpainted = e.shiftKey ? DocumentView.Selected().filter(dv => !outpainted.includes(dv)) : DocumentView.Selected(); // Special handling for shift-drag resize (outpainting of Images by resizing without scaling content - fill in with firefly GAI) @@ -765,7 +766,7 @@ export class DocumentDecorations extends ObservableReactComponent() { - state = { - loading: false, - src: '', - }; - - @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) { @@ -337,7 +342,7 @@ export class ScrapbookBox extends ViewBoxAnnotatableComponent() } } - + async generateAiImage() { this.setState({ loading: true }); @@ -365,10 +370,98 @@ export class ScrapbookBox extends ViewBoxAnnotatableComponent() componentDidMount() { //this.initScrapbook(ScrapbookPresetType.Default); this.setTitle(); - this.generateAiImage(); + 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 + + // tell the imageDoc to resize itself to the *new* scrapbook size + //this.imgDoc._width = dims.w; + //this.imgDoc._height = dims.h; + //Doc.SetNativeWidth(this.imgDoc, dims.w); + //Doc.SetNativeHeight(this.imgDoc, dims.h); + }); + } + ); + /* + this._disposers.propagateResize = reaction( + () => ({ w: this.layoutDoc._width, h: this.layoutDoc._height }), + ({ w, h }, prev) => { + // only when shift is held (i.e. outpaint mode) + if (SnappingManager.ShiftKey && this.imgDoc) { + const key = this.imageBoxRef.current!.props.fieldKey; // “data” + // record original size on the *image* doc: + this.imgDoc[key + '_outpaintOriginalWidth'] = this.imgDoc._width; + this.imgDoc[key + '_outpaintOriginalHeight'] = this.imgDoc._height; + } + } + );*/ + + + // this._disposers.outpaint = reaction( + // () => this.imgDoc?.[this.imgDoc.fieldKey + '_outpaintOriginalWidth'], + // originalWidth => { + // if (originalWidth !== undefined && !SnappingManager.ShiftKey) { + // this.imageBoxRef.current?.openOutpaintPrompt(); // ✅ CORRECT! + // } + // } + // ); } + async generateAiImageCorrect() { + this.loading = true; + + const prompt = 'A serene mountain landscape at sunrise, ultra-wide, pastel sky, abstract, scrapbook background'; + const dimensions = FireflyImageDimensions.Square; + + SmartDrawHandler.CreateWithFirefly(prompt, dimensions) + .then(action(doc => { + if (doc instanceof Doc) { + const imgField = ImageCast(doc.data); + if (imgField?.url.href) { + this.src = imgField.url.href; + const url = new ImageField(this.src); + this.imgDoc = Docs.Create.ImageDocument(url, { title: 'Generated Background', _width: 1792, _height: 2304, + _nativeWidth: 1792, _nativeHeight: 1792 + }, ); + } else { + alert('Image URL missing.'); + this.src = ''; + } + + } else { + alert('Failed to generate document.'); + } + })) + .catch(e => { + alert(`Generation error: ${e}`); + }) + .finally(action(() => { + this.loading = false; + })); + } + childRejectDrop = (de: DragManager.DropEvent, subView?: DocumentView) => { return true; // disable dropping documents onto any child of the scrapbook. }; @@ -446,18 +539,18 @@ export class ScrapbookBox extends ViewBoxAnnotatableComponent() }; render() { - const { loading, src } = this.state; + return (
- {loading && ( + {this.loading && (
)} - {loading && ( + {this.loading && (
@@ -465,15 +558,14 @@ export class ScrapbookBox extends ViewBoxAnnotatableComponent()
)} {/* Render AI-generated background */} - {src && ( - - )} - - + )} Date: Sun, 11 May 2025 23:28:12 -0400 Subject: outpainting scrapbooks --- src/client/views/nodes/ImageBox.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx index 0eb74740f..89fa9942d 100644 --- a/src/client/views/nodes/ImageBox.tsx +++ b/src/client/views/nodes/ImageBox.tsx @@ -135,7 +135,10 @@ export class ImageBox extends ViewBoxAnnotatableComponent() { // 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.' + `Classify this image as PERSON or LANDSCAPE. You may only respond with one of these two options. Then + provide five additional descriptive tags to describe the image for a total of 6 words outputted, + delimited by spaces and commas. Then add one final summary tag (separated by underscores) + that describes the image.` ); // 4) normalize and prefix -- cgit v1.2.3-70-g09d2 From a5744b79733654cdaa4ed732b48e2d86f570e31d Mon Sep 17 00:00:00 2001 From: sharkiecodes Date: Mon, 12 May 2025 00:43:44 -0400 Subject: scrapbooks with tags --- src/client/views/nodes/ImageBox.tsx | 7 ++- src/client/views/nodes/scrapbook/ScrapbookBox.tsx | 66 +++++++++++++++++++++-- 2 files changed, 66 insertions(+), 7 deletions(-) (limited to 'src') diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx index 89fa9942d..bd612d04f 100644 --- a/src/client/views/nodes/ImageBox.tsx +++ b/src/client/views/nodes/ImageBox.tsx @@ -137,7 +137,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent() { base64, `Classify this image as PERSON or LANDSCAPE. You may only respond with one of these two options. Then provide five additional descriptive tags to describe the image for a total of 6 words outputted, - delimited by spaces and commas. Then add one final summary tag (separated by underscores) + delimited by spaces. For example: "LANDSCAPE BUNNY NATURE FOREST PEACEFUL OUTDOORS". Then add one final lengthier summary tag (separated by underscores) that describes the image.` ); @@ -148,8 +148,11 @@ export class ImageBox extends ViewBoxAnnotatableComponent() { // 5) stash it on the Doc // overwrite any old tags so re-runs still work + const tokens = label.split(/\s+/); this.Document.$tags_chat = new List(); - (this.Document.$tags_chat as List).push(label); + tokens.forEach(tok => { + (this.Document.$tags_chat as List).push(tok)}); + //!!! changed may 11 (this.Document.$tags_chat as List).push(label); // 6) flip on “show tags” in the layout // (same flag that ImageLabelBox.toggleDisplayInformation uses) diff --git a/src/client/views/nodes/scrapbook/ScrapbookBox.tsx b/src/client/views/nodes/scrapbook/ScrapbookBox.tsx index ad3bfa7ad..9ffc13e6e 100644 --- a/src/client/views/nodes/scrapbook/ScrapbookBox.tsx +++ b/src/client/views/nodes/scrapbook/ScrapbookBox.tsx @@ -23,6 +23,8 @@ import { IReactionDisposer } from 'mobx'; import { observer } from 'mobx-react'; import { ImageField } from '../../../../fields/URLField'; import { runInAction } from 'mobx'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { faRedoAlt } from '@fortawesome/free-solid-svg-icons'; enum ScrapbookPresetType { Classic = 'Classic', @@ -429,10 +431,12 @@ export class ScrapbookBox extends ViewBoxAnnotatableComponent() } - async generateAiImageCorrect() { + async generateAiImageCorrect(prompt? : string) { this.loading = true; - - const prompt = 'A serene mountain landscape at sunrise, ultra-wide, pastel sky, abstract, scrapbook background'; + + if(!prompt){ + prompt = 'A serene mountain landscape at sunrise, ultra-wide, pastel sky, abstract, scrapbook background'; + } const dimensions = FireflyImageDimensions.Square; SmartDrawHandler.CreateWithFirefly(prompt, dimensions) @@ -538,8 +542,31 @@ export class ScrapbookBox extends ViewBoxAnnotatableComponent() return false; }; + render() { + /*const internalTags = DocListCast(this.dataDoc[this.fieldKey]) + .flatMap(ph => StrListCast(ph.$tags_chat)) + .join(' ');*/ + 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); + const internalTags = '' + + const regenPrompt = internalTags + ? `Create a new background using these tags: ${internalTags}` + : 'A serene mountain landscape at sunrise, ultra-wide, pastel sky, abstract, scrapbook background'; + + return (
@@ -564,8 +591,37 @@ export class ScrapbookBox extends ViewBoxAnnotatableComponent() {...this._props} Document={this.imgDoc} fieldKey="data" - /> - )} )} + + {this._props.isContentActive() && ( +
e.stopPropagation()} + > + +
+ )} + + Date: Thu, 22 May 2025 20:04:56 -0400 Subject: reactive backgrounds --- src/client/views/nodes/scrapbook/ScrapbookBox.tsx | 81 +++++++++++++---------- 1 file changed, 47 insertions(+), 34 deletions(-) (limited to 'src') diff --git a/src/client/views/nodes/scrapbook/ScrapbookBox.tsx b/src/client/views/nodes/scrapbook/ScrapbookBox.tsx index 9ffc13e6e..593c0ac63 100644 --- a/src/client/views/nodes/scrapbook/ScrapbookBox.tsx +++ b/src/client/views/nodes/scrapbook/ScrapbookBox.tsx @@ -1,4 +1,4 @@ -import { action, makeObservable, observable, reaction } from 'mobx'; +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'; @@ -438,7 +438,6 @@ export class ScrapbookBox extends ViewBoxAnnotatableComponent() prompt = 'A serene mountain landscape at sunrise, ultra-wide, pastel sky, abstract, scrapbook background'; } const dimensions = FireflyImageDimensions.Square; - SmartDrawHandler.CreateWithFirefly(prompt, dimensions) .then(action(doc => { if (doc instanceof Doc) { @@ -542,31 +541,37 @@ export class ScrapbookBox extends ViewBoxAnnotatableComponent() 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 => 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() { /*const internalTags = DocListCast(this.dataDoc[this.fieldKey]) .flatMap(ph => StrListCast(ph.$tags_chat)) .join(' ');*/ - 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); - const internalTags = '' - - const regenPrompt = internalTags - ? `Create a new background using these tags: ${internalTags}` - : 'A serene mountain landscape at sunrise, ultra-wide, pastel sky, abstract, scrapbook background'; - - return (
@@ -604,20 +609,28 @@ export class ScrapbookBox extends ViewBoxAnnotatableComponent() }} onPointerDown={e => e.stopPropagation()} > - +
)} -- cgit v1.2.3-70-g09d2 From 3488e9b8b1bec330f071f9e0ee8c49a3bc00b369 Mon Sep 17 00:00:00 2001 From: sharkiecodes Date: Thu, 22 May 2025 20:59:15 -0400 Subject: regen of backgrounds adapts to outpainted size --- src/client/views/nodes/scrapbook/ScrapbookBox.tsx | 69 ++++++++++++++--------- 1 file changed, 42 insertions(+), 27 deletions(-) (limited to 'src') diff --git a/src/client/views/nodes/scrapbook/ScrapbookBox.tsx b/src/client/views/nodes/scrapbook/ScrapbookBox.tsx index 593c0ac63..d67f9b7a2 100644 --- a/src/client/views/nodes/scrapbook/ScrapbookBox.tsx +++ b/src/client/views/nodes/scrapbook/ScrapbookBox.tsx @@ -3,16 +3,19 @@ import * as React from 'react'; import { Doc, DocListCast, StrListCast } from '../../../../fields/Doc'; import { List } from '../../../../fields/List'; import { emptyFunction } from '../../../../Utils'; +import axios from 'axios'; 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'; @@ -431,39 +434,49 @@ export class ScrapbookBox extends ViewBoxAnnotatableComponent() } - async generateAiImageCorrect(prompt? : string) { - this.loading = true; - - if(!prompt){ - prompt = 'A serene mountain landscape at sunrise, ultra-wide, pastel sky, abstract, scrapbook background'; - } - const dimensions = FireflyImageDimensions.Square; - SmartDrawHandler.CreateWithFirefly(prompt, dimensions) - .then(action(doc => { + + @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); - if (imgField?.url.href) { - this.src = imgField.url.href; - const url = new ImageField(this.src); - this.imgDoc = Docs.Create.ImageDocument(url, { title: 'Generated Background', _width: 1792, _height: 2304, - _nativeWidth: 1792, _nativeHeight: 1792 - }, ); - } else { - alert('Image URL missing.'); - this.src = ''; - } - + this.src = imgField?.url.href ?? ''; } else { alert('Failed to generate document.'); + this.src = ''; } - })) - .catch(e => { + } catch (e) { alert(`Generation error: ${e}`); - }) - .finally(action(() => { - this.loading = false; - })); - } + } finally { + runInAction(() => { + this.loading = false; + }); + } + } childRejectDrop = (de: DragManager.DropEvent, subView?: DocumentView) => { return true; // disable dropping documents onto any child of the scrapbook. @@ -541,6 +554,8 @@ export class ScrapbookBox extends ViewBoxAnnotatableComponent() return false; }; + + @computed get regenPrompt() { const slots = DocListCast(this.dataDoc[this.fieldKey]); -- cgit v1.2.3-70-g09d2 From c1f4a60b0016242a9097357074730f0cc9c151ba Mon Sep 17 00:00:00 2001 From: sharkiecodes Date: Thu, 22 May 2025 23:48:35 -0400 Subject: presets --- .../views/nodes/scrapbook/EmbeddedDocView.tsx | 52 ---------------------- src/client/views/nodes/scrapbook/ScrapbookBox.tsx | 25 +++++++++-- .../views/nodes/scrapbook/ScrapbookContent.tsx | 23 ---------- .../views/nodes/scrapbook/ScrapbookPreset.tsx | 17 ++++++- .../nodes/scrapbook/ScrapbookPresetRegistry.ts | 40 +++++++++++++++++ 5 files changed, 78 insertions(+), 79 deletions(-) delete mode 100644 src/client/views/nodes/scrapbook/EmbeddedDocView.tsx delete mode 100644 src/client/views/nodes/scrapbook/ScrapbookContent.tsx create mode 100644 src/client/views/nodes/scrapbook/ScrapbookPresetRegistry.ts (limited to 'src') diff --git a/src/client/views/nodes/scrapbook/EmbeddedDocView.tsx b/src/client/views/nodes/scrapbook/EmbeddedDocView.tsx deleted file mode 100644 index e99bf67c7..000000000 --- a/src/client/views/nodes/scrapbook/EmbeddedDocView.tsx +++ /dev/null @@ -1,52 +0,0 @@ -//IGNORE FOR NOW, CURRENTLY NOT USED IN SCRAPBOOK IMPLEMENTATION -import * as React from "react"; -import { observer } from "mobx-react"; -import { Doc } from "../../../../fields/Doc"; -import { DocumentView } from "../DocumentView"; -import { Transform } from "../../../util/Transform"; - -interface EmbeddedDocViewProps { - doc: Doc; - width?: number; - height?: number; - slotId?: string; -} - -@observer -export class EmbeddedDocView extends React.Component { - render() { - const { doc, width = 300, height = 200, slotId } = this.props; - - // Use either an existing embedding or create one - let docToDisplay = doc; - - // If we need an embedding, create or use one - if (!docToDisplay.isEmbedding) { - docToDisplay = Doc.BestEmbedding(doc) || Doc.MakeEmbedding(doc); - // Set the container to the slot's ID so we can track it - if (slotId) { - docToDisplay.embedContainer = `scrapbook-slot-${slotId}`; - } - } - - return ( - width} - NativeHeight={() => height} - PanelWidth={() => width} - PanelHeight={() => height} - // Required state functions - isContentActive={() => true} - childFilters={() => []} - ScreenToLocalTransform={() => new Transform()} - // Display options - hideDeleteButton={true} - hideDecorations={true} - hideResizeHandles={true} - /> - ); - } -} \ No newline at end of file diff --git a/src/client/views/nodes/scrapbook/ScrapbookBox.tsx b/src/client/views/nodes/scrapbook/ScrapbookBox.tsx index d67f9b7a2..391dcb83d 100644 --- a/src/client/views/nodes/scrapbook/ScrapbookBox.tsx +++ b/src/client/views/nodes/scrapbook/ScrapbookBox.tsx @@ -28,6 +28,8 @@ import { ImageField } from '../../../../fields/URLField'; import { runInAction } from 'mobx'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faRedoAlt } from '@fortawesome/free-solid-svg-icons'; +import { getPresetNames, createPreset } from './ScrapbookPresetRegistry'; + enum ScrapbookPresetType { Classic = 'Classic', @@ -39,7 +41,7 @@ enum ScrapbookPresetType { // Scrapbook view: a container that lays out its child items in a grid/template @observer export class ScrapbookBox extends ViewBoxAnnotatableComponent() { - + @observable selectedPreset = getPresetNames()[0]; @observable createdDate: string; @observable loading = false; @@ -53,6 +55,12 @@ export class ScrapbookBox extends ViewBoxAnnotatableComponent() constructor(props: FieldViewProps) { super(props); makeObservable(this); + // whenever the preset changes, rebuild the layout + reaction( + () => this.selectedPreset, + presetName => this.initScrapbook(presetName), + { fireImmediately: true } + ); this.createdDate = this.getFormattedDate(); //this.configs = @@ -82,7 +90,8 @@ export class ScrapbookBox extends ViewBoxAnnotatableComponent() @action - initScrapbook(presetType: ScrapbookPresetType) { + initScrapbook(name: string) { + const configs = createPreset(name); // 1) ensure title is set const title = `Scrapbook - ${this.createdDate}`; if (this.dataDoc.title !== title) { @@ -90,7 +99,7 @@ export class ScrapbookBox extends ViewBoxAnnotatableComponent() } // 2) build placeholders from the preset - const configs = ScrapbookPreset.createPreset(presetType); + const placeholders: Doc[] = []; for (const cfg of configs) { @@ -613,6 +622,16 @@ export class ScrapbookBox extends ViewBoxAnnotatableComponent() fieldKey="data" />)} + + {this._props.isContentActive() && (
= observer(({ doc }) => { - // If doc.title or doc.content are not plain strings, convert them. - const titleText = doc.title ? doc.title.toString() : "Untitled"; - const contentText = doc.content ? doc.content.toString() : "No content available."; - - return ( -
-

{titleText}

-

{contentText}

-
- ); -}); diff --git a/src/client/views/nodes/scrapbook/ScrapbookPreset.tsx b/src/client/views/nodes/scrapbook/ScrapbookPreset.tsx index 3cae4382b..fc69552c0 100644 --- a/src/client/views/nodes/scrapbook/ScrapbookPreset.tsx +++ b/src/client/views/nodes/scrapbook/ScrapbookPreset.tsx @@ -5,6 +5,7 @@ export enum ScrapbookPresetType { Classic = 'Classic', Collage = 'Collage', Spotlight = 'Spotlight', + Gallery = 'Gallery' } export interface ScrapbookItemConfig { @@ -35,7 +36,9 @@ export class ScrapbookPreset { case ScrapbookPresetType.Spotlight: return ScrapbookPreset.createSpotlightPreset(); case ScrapbookPresetType.Default: - return ScrapbookPreset.createDefaultPreset(); + return ScrapbookPreset.createDefaultPreset(); + case ScrapbookPresetType.Gallery: + return ScrapbookPreset.createGalleryPreset(); default: throw new Error(`Unknown preset type: ${presetType}`); } @@ -66,6 +69,18 @@ export class ScrapbookPreset { ]; } + private static createGalleryPreset(): ScrapbookItemConfig[] { + return [ + { type: DocumentType.IMG, tag: 'Gallery 1', acceptTag: 'LANDSCAPE', x: -150, y: -150, width: 150, height: 150 }, + { type: DocumentType.IMG, tag: 'Gallery 2', acceptTag: 'LANDSCAPE', x: 0, y: -150, width: 150, height: 150 }, + { type: DocumentType.IMG, tag: 'Gallery 3', acceptTag: 'LANDSCAPE', x: 150, y: -150, width: 150, height: 150 }, + { type: DocumentType.IMG, tag: 'Gallery 4', acceptTag: 'LANDSCAPE', x: -150, y: 0, width: 150, height: 150 }, + { type: DocumentType.IMG, tag: 'Gallery 5', acceptTag: 'LANDSCAPE', x: 0, y: 0, width: 150, height: 150 }, + { type: DocumentType.IMG, tag: 'Gallery 6', acceptTag: 'LANDSCAPE', x: 150, y: 0, width: 150, height: 150 }, + ]; + } + + private static createDefaultPreset(): ScrapbookItemConfig[] { return [ { type: DocumentType.IMG, diff --git a/src/client/views/nodes/scrapbook/ScrapbookPresetRegistry.ts b/src/client/views/nodes/scrapbook/ScrapbookPresetRegistry.ts new file mode 100644 index 000000000..f7ddd70ab --- /dev/null +++ b/src/client/views/nodes/scrapbook/ScrapbookPresetRegistry.ts @@ -0,0 +1,40 @@ +import { ScrapbookItemConfig } from './ScrapbookPreset'; +import { ScrapbookPresetType } from './ScrapbookPreset'; + +type PresetGenerator = () => ScrapbookItemConfig[]; + +// Internal map of preset name to generator +const presetRegistry = new Map(); + +/** + * Register a new scrapbook preset under the given name. + */ +export function registerPreset(name: string, gen: PresetGenerator) { + presetRegistry.set(name, gen); +} + +/** + * List all registered preset names. + */ +export function getPresetNames(): string[] { + return Array.from(presetRegistry.keys()); +} + +/** + * Create the config array for the named preset. + */ +export function createPreset(name: string): ScrapbookItemConfig[] { + const gen = presetRegistry.get(name); + if (!gen) throw new Error(`Unknown scrapbook preset: ${name}`); + return gen(); +} + +// ------------------------ +// Register built-in presets +import { ScrapbookPreset } from './ScrapbookPreset'; + +registerPreset(ScrapbookPresetType.Classic, () => ScrapbookPreset.createPreset(ScrapbookPresetType.Classic)); +registerPreset(ScrapbookPresetType.Collage, () => ScrapbookPreset.createPreset(ScrapbookPresetType.Collage)); +registerPreset(ScrapbookPresetType.Spotlight, () => ScrapbookPreset.createPreset(ScrapbookPresetType.Spotlight)); +registerPreset(ScrapbookPresetType.Default, () => ScrapbookPreset.createPreset(ScrapbookPresetType.Default)); +registerPreset(ScrapbookPresetType.Gallery, () => ScrapbookPreset.createPreset(ScrapbookPresetType.Gallery)); -- cgit v1.2.3-70-g09d2 From 7626527799c0606fa9c4fd4d26a19189dc7e7a0e Mon Sep 17 00:00:00 2001 From: sharkiecodes Date: Sun, 1 Jun 2025 20:24:04 -0400 Subject: reactive backgrounds, tagging of pdfs, group-select and suggested templates, text box content influences backgrounds --- src/client/apis/gpt/GPT.ts | 41 +++- .../collections/collectionFreeForm/MarqueeView.tsx | 70 ++++-- src/client/views/nodes/ImageBox.tsx | 2 +- src/client/views/nodes/PDFBox.tsx | 44 ++++ src/client/views/nodes/VideoBox.tsx | 51 ++++ .../views/nodes/formattedText/FormattedTextBox.tsx | 52 +++- .../views/nodes/scrapbook/AIPresetGenerator.ts | 31 +++ src/client/views/nodes/scrapbook/ScrapbookBox.tsx | 264 ++++++--------------- .../views/nodes/scrapbook/ScrapbookPreset.tsx | 8 +- .../nodes/scrapbook/ScrapbookPresetRegistry.ts | 3 + 10 files changed, 347 insertions(+), 219 deletions(-) create mode 100644 src/client/views/nodes/scrapbook/AIPresetGenerator.ts (limited to 'src') diff --git a/src/client/apis/gpt/GPT.ts b/src/client/apis/gpt/GPT.ts index 4dd30f8b3..03fce21f7 100644 --- a/src/client/apis/gpt/GPT.ts +++ b/src/client/apis/gpt/GPT.ts @@ -30,7 +30,6 @@ enum GPTCallType { DRAW = 'draw', COLOR = 'color', TEMPLATE = 'template', - SCRAPBOOK = 'scrapbook', VIZSUM = 'vizsum', VIZSUM2 = 'vizsum2', FILL = 'fill', @@ -41,7 +40,9 @@ enum GPTCallType { SUBSETDOCS = 'subset_docs', // select a subset of documents based on their descriptions DOCINFO = 'doc_info', // provide information about a document SORTDOCS = 'sort_docs', - CLASSIFYTEXT = 'classify_text', // classify text into one of the three categories: title, caption, lengthy description + CLASSIFYTEXTMINIMAL = 'classify_text_minimal', // classify text into one of the three categories: title, caption, lengthy description + CLASSIFYTEXTFULL = 'classify_text_full', //tags pdf content + GENERATESCRAPBOOK = 'generate_scrapbook' } type GPTCallOpts = { @@ -93,7 +94,7 @@ const callTypeMap: { [type in GPTCallType]: GPTCallOpts } = { prompt: "You are a helpful resarch assistant. Analyze the user's data to find meaningful patterns and/or correlation. Please only return a JSON with a correlation column 1 propert, a correlation column 2 property, and an analysis property. ", }, //new - classify_text: { + classify_text_minimal: { model: 'gpt-4o', maxTokens: 2048, temp: 0.25, @@ -101,6 +102,13 @@ const callTypeMap: { [type in GPTCallType]: GPTCallOpts } = { most appropriate category: '${TextClassifications.Title}', '${TextClassifications.Caption}', or '${TextClassifications.LengthyDescription}'. Output exclusively the classification in your response. ` }, + classify_text_full: { + model: 'gpt-4o', + maxTokens: 2048, + temp: 0.25, + prompt: `Based on the content of the text, provide six descriptive tags (single words) separated by spaces. + Finally, include a seventh more detailed summary phrase using underscores.` + }, describe: { model: 'gpt-4-vision-preview', maxTokens: 2048, temp: 0, prompt: 'Describe these images in 3-5 words' }, flashcard: { model: 'gpt-4-turbo', @@ -160,12 +168,29 @@ const callTypeMap: { [type in GPTCallType]: GPTCallOpts } = { 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, + generate_scrapbook: { + model: 'gpt-4o', + maxTokens: 2048, 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:', - }, + prompt: `Generate an aesthetically pleasing scrapbook layout preset based on these items. + Return your response as JSON in the format: + [{ + "type": DocumentType.RTF or DocumentType.IMG or DocumentType.PDF + "tag": a singular tag summarizing the document + "x": number, + "y": number, + "width": number, + "height": number + }, ...] + If there are mutliple documents, you may include + "children": [ + { type: + tag: + x: , y: , width: , height: + } + ] ` + + }, command_type: { model: 'gpt-4-turbo', diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx index f5e699d3e..0b91d628b 100644 --- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx +++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx @@ -28,6 +28,8 @@ 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 './MarqueeView.scss'; interface MarqueeViewProps { @@ -519,35 +521,71 @@ export class MarqueeView extends ObservableReactComponent { + + const selectedDocs = this.marqueeSelect(false); + if (!selectedDocs.length) return; - @undoBatch - generateScrapbook = action(() => { - let docs = new Array(); + const descriptors: DocumentDescriptor[] = selectedDocs.map(doc => ({ + type: typeof doc.$type === 'string' ? doc.$type : 'UNKNOWN', + tags: (() => { + const internalTagsSet = new Set(); + StrListCast(doc.$tags_chat ?? new List()).forEach(tag => { + internalTagsSet.add(tag); + }); + return Array.from(internalTagsSet); + })() + })); + + + const aiPreset = await requestAiGeneratedPreset(descriptors); + if (!aiPreset.length) { + alert("Failed to generate preset"); + return; + } + + const scrapbookPlaceholders: Doc[] = aiPreset.map(cfg => { + const placeholderDoc = Docs.Create.TextDocument(cfg.tag); + placeholderDoc.accepts_docType = cfg.type as DocumentType; + placeholderDoc.accepts_tagType = cfg.acceptTag ?? 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' + }); 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', - }); + + this._props.addDocument?.(scrapbook); + selectedDocs.forEach(doc => this._props.removeDocument?.(doc)); 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); + this.hideMarquee(); }); diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx index bd612d04f..9067f7e0c 100644 --- a/src/client/views/nodes/ImageBox.tsx +++ b/src/client/views/nodes/ImageBox.tsx @@ -132,7 +132,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent() { 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 + // 3) ask GPT for labels 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. Then diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx index 55e6d5596..282b06215 100644 --- a/src/client/views/nodes/PDFBox.tsx +++ b/src/client/views/nodes/PDFBox.tsx @@ -34,6 +34,9 @@ import { ImageBox } from './ImageBox'; import { OpenWhere } from './OpenWhere'; import './PDFBox.scss'; import { CreateImage } from './WebBoxRenderer'; +import { gptAPICall } from '../../apis/gpt/GPT'; +import { List } from '../../../fields/List'; +import { GPTCallType } from '../../apis/gpt/GPT'; @observer export class PDFBox extends ViewBoxAnnotatableComponent() { @@ -76,6 +79,47 @@ export class PDFBox extends ViewBoxAnnotatableComponent() { } } + autoTag = async () => { + try { + if (!this._pdf) { + throw new Error('PDF not loaded'); + } + + // 1) Extract text from the first few pages (e.g., first 2 pages) + const maxPages = Math.min(2, this._pdf.numPages); + let textContent = ''; + for (let pageNum = 1; pageNum <= maxPages; pageNum++) { + const page = await this._pdf.getPage(pageNum); + const text = await page.getTextContent(); + const pageText = text.items.map(item => ('str' in item ? item.str : '')).join(' '); + textContent += ` ${pageText}`; + } + + if (!textContent.trim()) { + throw new Error('No text found in PDF'); + } + + // 2) Ask GPT to classify and provide descriptive tags + const raw = await gptAPICall( + `"${textContent.trim().slice(0, 2000)}"`, + GPTCallType.CLASSIFYTEXTFULL + ); + + // 3) Normalize and store the labels + const label = raw.trim().toUpperCase(); + + const tokens = label.split(/\s+/); + this.Document.$tags_chat = new List(); + tokens.forEach(tok => (this.Document.$tags_chat as List).push(tok)); + + // 4) Show tags in layout + this.Document._layout_showTags = true; + + } catch (err) { + console.error('PDF autoTag failed:', err); + } +}; + replaceCanvases = (oldDiv: HTMLElement, newDiv: HTMLElement) => { if (oldDiv.childNodes) { for (let i = 0; i < oldDiv.childNodes.length; i++) { diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx index fa099178c..0e7afbab1 100644 --- a/src/client/views/nodes/VideoBox.tsx +++ b/src/client/views/nodes/VideoBox.tsx @@ -30,6 +30,7 @@ import { StyleProp } from '../StyleProp'; import { DocumentView } from './DocumentView'; import { FieldView, FieldViewProps } from './FieldView'; import { FocusViewOptions } from './FocusViewOptions'; +import { gptImageLabel } from '../../apis/gpt/GPT'; import './VideoBox.scss'; /** @@ -109,6 +110,56 @@ export class VideoBox extends ViewBoxAnnotatableComponent() { return this._videoRef; } + + autoTag = async () => { + try { + if (!this.player) throw new Error('Video element not available.'); + + // 1) Extract a frame at the video's midpoint + const videoDuration = this.player.duration; + const snapshotTime = videoDuration / 2; + + // Seek the video element to the midpoint + await new Promise((resolve, reject) => { + const onSeeked = () => { + this.player!.removeEventListener('seeked', onSeeked); + resolve(); + }; + this.player!.addEventListener('seeked', onSeeked); + this.player!.currentTime = snapshotTime; + }); + + // 2) Draw the frame onto a canvas and get a base64 representation + const canvas = document.createElement('canvas'); + canvas.width = this.player.videoWidth; + canvas.height = this.player.videoHeight; + const ctx = canvas.getContext('2d'); + if (!ctx) throw new Error('Failed to create canvas context.'); + ctx.drawImage(this.player, 0, 0, canvas.width, canvas.height); + const base64Image = canvas.toDataURL('image/png'); + + // 3) Send the image data to GPT for classification and descriptive tags + const raw = await gptImageLabel( + base64Image, + `Classify this video frame as either a PERSON or LANDSCAPE. + Then provide five additional descriptive tags (single words) separated by spaces. + Finally, add one detailed summary phrase using underscores.` + ); + + // 4) Normalize and store labels in the Document's tags + const label = raw.trim().toUpperCase(); + const tokens = label.split(/\s+/); + this.Document.$tags_chat = new List(); + tokens.forEach(tok => (this.Document.$tags_chat as List).push(tok)); + + // 5) Turn on tag display in layout + this.Document._layout_showTags = true; + + } catch (err) { + console.error('Video autoTag failed:', err); + } +}; + componentDidMount() { this.unmounting = false; this._props.setContentViewBox?.(this); // this tells the DocumentView that this VideoBox is the "content" of the document. this allows the DocumentView to indirectly call getAnchor() on the VideoBox when making a link. diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx index 10becc00b..04a14a15f 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx +++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx @@ -8,6 +8,7 @@ import { baseKeymap, selectAll, splitBlock } from 'prosemirror-commands'; import { history } from 'prosemirror-history'; import { inputRules } from 'prosemirror-inputrules'; import { keymap } from 'prosemirror-keymap'; +import { runInAction } from 'mobx'; import { Fragment, Mark, Node, Slice } from 'prosemirror-model'; import { EditorState, NodeSelection, Plugin, Selection, TextSelection, Transaction } from 'prosemirror-state'; import { EditorView, NodeViewConstructor } from 'prosemirror-view'; @@ -305,12 +306,53 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent { + autoTag = async () => { + + const layoutKey = Doc.LayoutDataKey(this.Document); + const rawText = RTFCast(this.Document[layoutKey])?.Text ?? StrCast(this.Document[layoutKey]); + + const callType = rawText.includes("[placeholder]") + ? GPTCallType.CLASSIFYTEXTMINIMAL + : GPTCallType.CLASSIFYTEXTFULL; + + gptAPICall(rawText, callType).then(desc => { + runInAction(() => { + // Clear existing tags + this.Document.$tags_chat = new List(); + + // Split GPT response into tokens and push individually + const tokens = desc.trim().split(/\s+/); + tokens.forEach(tok => { + (this.Document.$tags_chat as List).push(tok); + }); + + this.Document._layout_showTags = true; + }); + }); + /*this.Document.$tags_chat = new List(); + gptAPICall(RTFCast(this.Document[Doc.LayoutDataKey(this.Document)])?.Text ?? StrCast(this.Document[Doc.LayoutDataKey(this.Document)]), GPTCallType.CLASSIFYTEXTMINIMAL).then(desc => (this.Document.$tags_chat as List).push(desc)); + this.Document._layout_showTags = true;*/ + + + // 2) grab whatever’s actually in the field (either RTF or plain string) +/* + const rawText = RTFCast(this.Document[Doc.LayoutDataKey(this.Document)])?.Text ?? StrCast(this.Document[Doc.LayoutDataKey(this.Document)]) + + // 3) pick minimal vs. full classification based on "[placeholder]" substring + if (rawText.includes("[placeholder]")) { this.Document.$tags_chat = new List(); - 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).push(desc)); - this.Document._layout_showTags = true; - //or... then(desc => this.Document.$tags_chat = desc); - }; + gptAPICall(RTFCast(this.Document[Doc.LayoutDataKey(this.Document)])?.Text ?? StrCast(this.Document[Doc.LayoutDataKey(this.Document)]), GPTCallType.CLASSIFYTEXTMINIMAL).then(desc => { + (this.Document.$tags_chat as List).push(desc); + }); + } else { + this.Document.$tags_chat = new List(); + gptAPICall(RTFCast(this.Document[Doc.LayoutDataKey(this.Document)])?.Text ?? StrCast(this.Document[Doc.LayoutDataKey(this.Document)]), GPTCallType.CLASSIFYTEXTFULL).then(desc => { + (this.Document.$tags_chat as List).push(desc); + })}; + // 4) make sure the UI will show tags + this.Document._layout_showTags = true;*/ + +}; leafText = (node: Node) => { if (node.type === this.EditorView?.state.schema.nodes.dashField) { diff --git a/src/client/views/nodes/scrapbook/AIPresetGenerator.ts b/src/client/views/nodes/scrapbook/AIPresetGenerator.ts new file mode 100644 index 000000000..1f159222b --- /dev/null +++ b/src/client/views/nodes/scrapbook/AIPresetGenerator.ts @@ -0,0 +1,31 @@ +import { ScrapbookItemConfig } from './ScrapbookPreset'; +import { GPTCallType, gptAPICall } from '../../../apis/gpt/GPT'; + +// Represents the descriptor for each document +export interface DocumentDescriptor { + type: string; + tags: string[]; +} + +// Main function to request AI-generated presets +export async function requestAiGeneratedPreset(descriptors: DocumentDescriptor[]): Promise { + const prompt = createPrompt(descriptors); + let aiResponse = await gptAPICall(prompt, GPTCallType.GENERATESCRAPBOOK); + // Strip out ```json and ``` if the model wrapped its answer in fences + aiResponse = aiResponse + .trim() + .replace(/^```(?:json)?\s*/, "") // remove leading ``` or ```json + .replace(/\s*```$/, ""); // remove trailing ``` + const parsedPreset = JSON.parse(aiResponse) as ScrapbookItemConfig[]; + return parsedPreset; +} + +// Helper to generate prompt text for AI +function createPrompt(descriptors: DocumentDescriptor[]): string { + let prompt = ""; + descriptors.forEach((desc, index) => { + prompt += `${index + 1}. Type: ${desc.type}, Tags: ${desc.tags.join(', ')}\n`; + }); + + return prompt; +} diff --git a/src/client/views/nodes/scrapbook/ScrapbookBox.tsx b/src/client/views/nodes/scrapbook/ScrapbookBox.tsx index 391dcb83d..5dd02295c 100644 --- a/src/client/views/nodes/scrapbook/ScrapbookBox.tsx +++ b/src/client/views/nodes/scrapbook/ScrapbookBox.tsx @@ -24,20 +24,72 @@ import { ImageCast } from '../../../../fields/Types'; import { SnappingManager } from '../../../util/SnappingManager'; import { IReactionDisposer } from 'mobx'; import { observer } from 'mobx-react'; -import { ImageField } from '../../../../fields/URLField'; import { runInAction } from 'mobx'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faRedoAlt } from '@fortawesome/free-solid-svg-icons'; import { getPresetNames, createPreset } from './ScrapbookPresetRegistry'; -enum ScrapbookPresetType { - Classic = 'Classic', - Default = 'Default', - Collage = 'Collage', - Spotlight = 'Spotlight', -} +export function buildPlaceholdersFromConfigs(configs: ScrapbookItemConfig[]): Doc[] { + const placeholders: Doc[] = []; + + for (const cfg of configs) { + if (cfg.children && cfg.children.length) { + // --- nested container --- + const childDocs = cfg.children.map(child => { + const doc = Docs.Create.TextDocument("[placeholder] " + child.tag); + doc.accepts_docType = child.type; + + 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; + }); + + // wrap those children in a stacking container + 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("[placeholder] " + 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); + } + } + return placeholders; +} // Scrapbook view: a container that lays out its child items in a grid/template @observer export class ScrapbookBox extends ViewBoxAnnotatableComponent() { @@ -55,14 +107,22 @@ export class ScrapbookBox extends ViewBoxAnnotatableComponent() constructor(props: FieldViewProps) { super(props); makeObservable(this); - // whenever the preset changes, rebuild the layout - reaction( + 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.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); @@ -70,9 +130,7 @@ export class ScrapbookBox extends ViewBoxAnnotatableComponent() 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); } @@ -99,175 +157,11 @@ export class ScrapbookBox extends ViewBoxAnnotatableComponent() } // 2) build placeholders from the preset + const placeholders = buildPlaceholdersFromConfigs(configs); - 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); - } @@ -276,8 +170,8 @@ export class ScrapbookBox extends ViewBoxAnnotatableComponent() const title = `Scrapbook - ${this.createdDate}`; if (this.dataDoc.title !== title) { this.dataDoc.title = title; - - const image = Docs.Create.TextDocument('person image'); + 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(); @@ -289,7 +183,7 @@ export class ScrapbookBox extends ViewBoxAnnotatableComponent() 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('long summary'); + 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 @@ -301,7 +195,7 @@ export class ScrapbookBox extends ViewBoxAnnotatableComponent() 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('brief sidebar'); + 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(); @@ -314,7 +208,7 @@ export class ScrapbookBox extends ViewBoxAnnotatableComponent() - const internalImg = Docs.Create.TextDocument('landscape internal'); + 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(); @@ -350,7 +244,7 @@ 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]); diff --git a/src/client/views/nodes/scrapbook/ScrapbookPreset.tsx b/src/client/views/nodes/scrapbook/ScrapbookPreset.tsx index fc69552c0..87821c7bf 100644 --- a/src/client/views/nodes/scrapbook/ScrapbookPreset.tsx +++ b/src/client/views/nodes/scrapbook/ScrapbookPreset.tsx @@ -47,22 +47,22 @@ export class ScrapbookPreset { private static createClassicPreset(): ScrapbookItemConfig[] { return [ { type: DocumentType.IMG, - tag: 'LANDSCAPE', + tag: '[placeholder] LANDSCAPE', acceptTag: 'LANDSCAPE', x: 0, y: -100, width: 250, height: 200 }, { type: DocumentType.RTF, - tag: 'caption', + tag: '[placeholder] caption', acceptTag: 'caption', x: 0, y: 200, width: 250, height: 50 }, { type: DocumentType.RTF, - tag: 'lengthy description', + tag: '[placeholder] lengthy description', acceptTag: 'lengthy description', x: 280, y: -50, width: 50, height: 200 }, { type: DocumentType.IMG, - tag: 'PERSON', + tag: '[placeholder] PERSON', acceptTag: 'PERSON', x: -200, y: -100, width: 100, height: 200 }, diff --git a/src/client/views/nodes/scrapbook/ScrapbookPresetRegistry.ts b/src/client/views/nodes/scrapbook/ScrapbookPresetRegistry.ts index f7ddd70ab..d6fd3620c 100644 --- a/src/client/views/nodes/scrapbook/ScrapbookPresetRegistry.ts +++ b/src/client/views/nodes/scrapbook/ScrapbookPresetRegistry.ts @@ -6,6 +6,9 @@ type PresetGenerator = () => ScrapbookItemConfig[]; // Internal map of preset name to generator const presetRegistry = new Map(); + + + /** * Register a new scrapbook preset under the given name. */ -- cgit v1.2.3-70-g09d2 From 42b35b687f081e579cbec524426105df3ac695ef Mon Sep 17 00:00:00 2001 From: sharkiecodes Date: Thu, 5 Jun 2025 22:48:33 -0400 Subject: attempting marqueeview gen of multiple scrapbooks --- src/client/apis/gpt/GPT.ts | 15 +- .../collectionFreeForm/MarqueeView.scss | 2 +- .../collections/collectionFreeForm/MarqueeView.tsx | 191 +++++++++++++++++++-- src/client/views/nodes/ImageBox.tsx | 6 + src/client/views/nodes/VideoBox.tsx | 3 +- src/client/views/nodes/scrapbook/ScrapbookBox.tsx | 165 +++++++++--------- .../views/nodes/scrapbook/ScrapbookPicker.scss | 40 +++++ .../views/nodes/scrapbook/ScrapbookPicker.tsx | 84 +++++++++ .../views/nodes/scrapbook/ScrapbookPreset.tsx | 44 ++--- .../views/nodes/scrapbook/scrapbookleftover.ts | 46 +++++ 10 files changed, 474 insertions(+), 122 deletions(-) create mode 100644 src/client/views/nodes/scrapbook/ScrapbookPicker.scss create mode 100644 src/client/views/nodes/scrapbook/ScrapbookPicker.tsx create mode 100644 src/client/views/nodes/scrapbook/scrapbookleftover.ts (limited to 'src') diff --git a/src/client/apis/gpt/GPT.ts b/src/client/apis/gpt/GPT.ts index 03fce21f7..4642d79eb 100644 --- a/src/client/apis/gpt/GPT.ts +++ b/src/client/apis/gpt/GPT.ts @@ -14,7 +14,7 @@ export const DocSeperator = '------'; export enum TextClassifications { Title = 'title', //a few words Caption = 'caption', //few sentences - LengthyDescription = 'lengthy description' } + LengthyDescription = 'lengthy' } enum GPTCallType { SUMMARY = 'summary', @@ -106,8 +106,10 @@ const callTypeMap: { [type in GPTCallType]: GPTCallOpts } = { model: 'gpt-4o', maxTokens: 2048, temp: 0.25, - prompt: `Based on the content of the text, provide six descriptive tags (single words) separated by spaces. - Finally, include a seventh more detailed summary phrase using underscores.` + prompt: `Based on the content of the the text, classify it into the + most appropriate category: '${TextClassifications.Title}', '${TextClassifications.Caption}', or '${TextClassifications.LengthyDescription}'. + Then provide five more descriptive tags (single words) separated by spaces. + Finally, include a more detailed summary phrase tag using underscores, for a total of seven tags.` }, describe: { model: 'gpt-4-vision-preview', maxTokens: 2048, temp: 0, prompt: 'Describe these images in 3-5 words' }, flashcard: { @@ -175,14 +177,15 @@ const callTypeMap: { [type in GPTCallType]: GPTCallOpts } = { prompt: `Generate an aesthetically pleasing scrapbook layout preset based on these items. Return your response as JSON in the format: [{ - "type": DocumentType.RTF or DocumentType.IMG or DocumentType.PDF + "type": rich text or image or pdf or video or collection "tag": a singular tag summarizing the document + "acceptTags": [a list of all relevant tags that this document accepts, like ['PERSON', 'LANDSCAPE']] "x": number, "y": number, - "width": number, + "width": number, **note: if it is in an image, please respect existing aspect ratio if it is provided "height": number }, ...] - If there are mutliple documents, you may include + If there are mutliple documents and you wish to nest documents into a collection for aesthetic purposes, you may include "children": [ { type: tag: diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.scss b/src/client/views/collections/collectionFreeForm/MarqueeView.scss index 7c9d0f6e1..b514b0911 100644 --- a/src/client/views/collections/collectionFreeForm/MarqueeView.scss +++ b/src/client/views/collections/collectionFreeForm/MarqueeView.scss @@ -28,4 +28,4 @@ .marquee-legend::after { content: 'Press 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 0b91d628b..05d4cd81d 100644 --- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx +++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx @@ -22,6 +22,7 @@ import { ObservableReactComponent } from '../../ObservableReactComponent'; import { MarqueeViewBounds } from '../../PinFuncs'; import { PreviewCursor } from '../../PreviewCursor'; import { DocumentView } from '../../nodes/DocumentView'; +import { OverlayDisposer } from '../../OverlayView'; import { OpenWhere } from '../../nodes/OpenWhere'; import { pasteImageBitmap } from '../../nodes/WebBoxRenderer'; import { FormattedTextBox } from '../../nodes/formattedText/FormattedTextBox'; @@ -30,8 +31,15 @@ import { ImageLabelBoxData } from './ImageLabelBox'; import { MarqueeOptionsMenu } from './MarqueeOptionsMenu'; import { StrListCast } from '../../../../fields/Doc'; import { requestAiGeneratedPreset, DocumentDescriptor } from '../../nodes/scrapbook/AIPresetGenerator'; +import { ScrapbookItemConfig } from '../../nodes/scrapbook/ScrapbookPreset'; +import { OverlayView } from '../../OverlayView'; +import { runInAction } from 'mobx'; +import { ScrapbookPicker } from '../../nodes/scrapbook/ScrapbookPicker'; +import { buildPlaceholdersFromConfigs, slotRealDocIntoPlaceholders } from '../../nodes/scrapbook/ScrapbookBox'; import './MarqueeView.scss'; +import { build } from 'xregexp'; + interface MarqueeViewProps { Doc: Doc; getContainerTransform: () => Transform; @@ -78,6 +86,16 @@ export class MarqueeView extends ObservableReactComponent
+ ); + } +} -// Register scrapbook Docs.Prototypes.TemplateMap.set(DocumentType.SCRAPBOOK, { diff --git a/src/client/views/nodes/scrapbook/ScrapbookSettingsPanel.tsx b/src/client/views/nodes/scrapbook/ScrapbookSettingsPanel.tsx deleted file mode 100644 index 5808ab4d1..000000000 --- a/src/client/views/nodes/scrapbook/ScrapbookSettingsPanel.tsx +++ /dev/null @@ -1,60 +0,0 @@ -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 diff --git a/src/client/views/nodes/scrapbook/ScrapbookSlot.scss b/src/client/views/nodes/scrapbook/ScrapbookSlot.scss deleted file mode 100644 index ae647ad36..000000000 --- a/src/client/views/nodes/scrapbook/ScrapbookSlot.scss +++ /dev/null @@ -1,85 +0,0 @@ -//IGNORE FOR NOW, CURRENTLY NOT USED IN SCRAPBOOK IMPLEMENTATION -.scrapbook-slot { - position: absolute; - background-color: rgba(245, 245, 245, 0.7); - border: 2px dashed #ccc; - border-radius: 5px; - box-sizing: border-box; - transition: all 0.2s ease; - overflow: hidden; - - &.scrapbook-slot-over { - border-color: #4a90e2; - background-color: rgba(74, 144, 226, 0.1); - } - - &.scrapbook-slot-filled { - border-style: solid; - border-color: rgba(0, 0, 0, 0.1); - background-color: transparent; - - &.scrapbook-slot-over { - border-color: #4a90e2; - background-color: rgba(74, 144, 226, 0.1); - } - } - - .scrapbook-slot-empty { - display: flex; - align-items: center; - justify-content: center; - width: 100%; - height: 100%; - } - - .scrapbook-slot-placeholder { - text-align: center; - color: #888; - } - - .scrapbook-slot-title { - font-weight: bold; - margin-bottom: 5px; - } - - .scrapbook-slot-instruction { - font-size: 0.9em; - font-style: italic; - } - - .scrapbook-slot-content { - width: 100%; - height: 100%; - position: relative; - } - - .scrapbook-slot-controls { - position: absolute; - top: 5px; - right: 5px; - z-index: 10; - opacity: 0; - transition: opacity 0.2s ease; - - .scrapbook-slot-remove-btn { - background-color: rgba(255, 255, 255, 0.8); - border: 1px solid #ccc; - border-radius: 50%; - width: 20px; - height: 20px; - display: flex; - align-items: center; - justify-content: center; - cursor: pointer; - font-size: 10px; - - &:hover { - background-color: rgba(255, 0, 0, 0.1); - } - } - } - - &:hover .scrapbook-slot-controls { - opacity: 1; - } -} \ No newline at end of file diff --git a/src/client/views/nodes/scrapbook/ScrapbookSlot.tsx b/src/client/views/nodes/scrapbook/ScrapbookSlot.tsx deleted file mode 100644 index 2c8f93778..000000000 --- a/src/client/views/nodes/scrapbook/ScrapbookSlot.tsx +++ /dev/null @@ -1,28 +0,0 @@ - -//IGNORE FOR NOW, CURRENTLY NOT USED IN SCRAPBOOK IMPLEMENTATION -export interface SlotDefinition { - id: string; - x: number; y: number; - defaultWidth: number; - defaultHeight: number; - } - - export interface SlotContentMap { - slotId: string; - docId?: string; - } - - export interface ScrapbookConfig { - slots: SlotDefinition[]; - contents?: SlotContentMap[]; - } - - export const DEFAULT_SCRAPBOOK_CONFIG: ScrapbookConfig = { - slots: [ - { id: "slot1", x: 10, y: 10, defaultWidth: 180, defaultHeight: 120 }, - { id: "slot2", x: 200, y: 10, defaultWidth: 180, defaultHeight: 120 }, - // …etc - ], - contents: [] - }; - \ No newline at end of file diff --git a/src/client/views/nodes/scrapbook/ScrapbookSlotTypes.ts b/src/client/views/nodes/scrapbook/ScrapbookSlotTypes.ts deleted file mode 100644 index 686917d9a..000000000 --- a/src/client/views/nodes/scrapbook/ScrapbookSlotTypes.ts +++ /dev/null @@ -1,25 +0,0 @@ -// ScrapbookSlotTypes.ts -export interface SlotDefinition { - id: string; - title: string; - x: number; - y: number; - defaultWidth: number; - defaultHeight: number; - } - - export interface ScrapbookConfig { - slots: SlotDefinition[]; - contents?: { slotId: string; docId: string }[]; - } - - // give it three slots by default: - export const DEFAULT_SCRAPBOOK_CONFIG: ScrapbookConfig = { - slots: [ - { id: "main", title: "Main Content", x: 20, y: 20, defaultWidth: 360, defaultHeight: 200 }, - { id: "notes", title: "Notes", x: 20, y: 240, defaultWidth: 360, defaultHeight: 160 }, - { id: "resources", title: "Resources", x: 400, y: 20, defaultWidth: 320, defaultHeight: 380 }, - ], - contents: [], - }; - \ No newline at end of file diff --git a/src/client/views/nodes/scrapbook/scrapbookleftover.ts b/src/client/views/nodes/scrapbook/scrapbookleftover.ts index 2f381ab95..ee7a858e4 100644 --- a/src/client/views/nodes/scrapbook/scrapbookleftover.ts +++ b/src/client/views/nodes/scrapbook/scrapbookleftover.ts @@ -1,8 +1,4 @@ - - - - - //DocListCast(this.Document.items).map(doc => DocListCast(doc[Doc.LayoutDataKey(doc)]) + //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*/ -- cgit v1.2.3-70-g09d2 From cea108a2f0b4a64fedb523a415a1d586ca8d126d Mon Sep 17 00:00:00 2001 From: sharkiecodes Date: Fri, 6 Jun 2025 11:10:14 -0400 Subject: clean-up p2 --- .../views/nodes/scrapbook/scrapbookleftover.ts | 42 ---------------------- 1 file changed, 42 deletions(-) delete mode 100644 src/client/views/nodes/scrapbook/scrapbookleftover.ts (limited to 'src') diff --git a/src/client/views/nodes/scrapbook/scrapbookleftover.ts b/src/client/views/nodes/scrapbook/scrapbookleftover.ts deleted file mode 100644 index ee7a858e4..000000000 --- a/src/client/views/nodes/scrapbook/scrapbookleftover.ts +++ /dev/null @@ -1,42 +0,0 @@ - //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[] = placeholder.map(doc => - new Set(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 - - // 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(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; - } - } - return false; - }; \ No newline at end of file -- cgit v1.2.3-70-g09d2 From 6807ed433befd3f90f2dfc4ca359cd9ee8a68e32 Mon Sep 17 00:00:00 2001 From: sharkiecodes Date: Fri, 6 Jun 2025 11:10:43 -0400 Subject: temp cleanup --- .../views/nodes/scrapbook/ScrapbookPicker.scss | 40 ----------- .../views/nodes/scrapbook/ScrapbookPicker.tsx | 84 ---------------------- 2 files changed, 124 deletions(-) delete mode 100644 src/client/views/nodes/scrapbook/ScrapbookPicker.scss delete mode 100644 src/client/views/nodes/scrapbook/ScrapbookPicker.tsx (limited to 'src') diff --git a/src/client/views/nodes/scrapbook/ScrapbookPicker.scss b/src/client/views/nodes/scrapbook/ScrapbookPicker.scss deleted file mode 100644 index 237274433..000000000 --- a/src/client/views/nodes/scrapbook/ScrapbookPicker.scss +++ /dev/null @@ -1,40 +0,0 @@ -/* ScrapbookPicker.scss */ - -.scrapbook-picker-popup { - background: rgba(255, 0, 0, 0.5); /* semi-transparent red */ - position: absolute; /* ← make it float */ - z-index: 10000; /* so it sits above the overlay‐window’s background */ - border: 1px solid #ccc; - border-radius: 4px; - box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15); - padding: 8px; - min-width: 200px; /* at least give it some size */ -} - -.scrapbook-picker-close { - position: absolute; - top: 4px; - right: 8px; - cursor: pointer; - font-size: 14px; -} - -.scrapbook-picker-thumbnails { - margin-top: 24px; /* room under the close button */ - display: flex; - flex-wrap: wrap; - gap: 8px; -} - -.scrapbook-picker-thumb { - cursor: pointer; - border: 1px solid #ddd; - border-radius: 2px; - padding: 4px; - background: #f9f9f9; -} - -.scrapbook-picker-thumb-inner { - font-size: 12px; - text-align: center; -} diff --git a/src/client/views/nodes/scrapbook/ScrapbookPicker.tsx b/src/client/views/nodes/scrapbook/ScrapbookPicker.tsx deleted file mode 100644 index 6054cb98d..000000000 --- a/src/client/views/nodes/scrapbook/ScrapbookPicker.tsx +++ /dev/null @@ -1,84 +0,0 @@ -// src/client/views/nodes/scrapbook/ScrapbookPicker.tsx -import * as React from 'react'; -import { observer } from 'mobx-react'; -import { Doc } from '../../../../fields/Doc'; -import { Id } from '../../../../fields/FieldSymbols'; -import { StrCast } from '../../../../fields/Types'; -import './ScrapbookPicker.scss'; - -export interface ScrapbookPickerProps { - choices: Doc[]; - x: number; - y: number; - onSelect: (index: number) => void; - onCancel: () => void; -} - -/** - * A floating popup that shows N “temporary” Scrapbook documents. - * When the user clicks one thumbnail, we call onSelect(i). - * When the user clicks × or outside, we call onCancel(). - * - * This component itself does not control its own visibility; MarqueeView / OverlayView will mount/unmount it. - */ -@observer -export class ScrapbookPicker extends React.Component { - containerRef = React.createRef(); - - // Close when user clicks outside the popup - handleClickOutside = (e: MouseEvent) => { - if ( - this.containerRef.current && - !this.containerRef.current.contains(e.target as Node) - ) { - this.props.onCancel(); - } - }; - - componentDidMount() { - document.addEventListener('mousedown', this.handleClickOutside); - } - componentWillUnmount() { - document.removeEventListener('mousedown', this.handleClickOutside); - } - - render() { - const { choices, x, y, onSelect, onCancel } = this.props; - return ( -
- {/* close icon */} -
- × -
-
- {choices.map((doc, i) => { - // We simply show a small thumbnail representation of each temp scrapbook - // You could replace this with DocumentThumbnail or a custom mini‐preview. - return ( -
onSelect(i)} - > - {/* - For a minimal example, use the document’s title or ID as a placeholder. - In a real version, you might render a proper thumbnail/view of doc. - */} -
- {StrCast(doc.title)} -
-
- ); - })} -
-
- ); - } -} -- cgit v1.2.3-70-g09d2 From 5493911505ddca32e32b7ca2dfb9545766b84a9b Mon Sep 17 00:00:00 2001 From: sharkiecodes Date: Mon, 9 Jun 2025 12:08:48 -0400 Subject: scrapbooks commit --- src/client/views/nodes/scrapbook/ScrapbookBox.tsx | 33 +++++++++-------------- 1 file changed, 12 insertions(+), 21 deletions(-) (limited to 'src') diff --git a/src/client/views/nodes/scrapbook/ScrapbookBox.tsx b/src/client/views/nodes/scrapbook/ScrapbookBox.tsx index e910f2fa7..d1d357d4c 100644 --- a/src/client/views/nodes/scrapbook/ScrapbookBox.tsx +++ b/src/client/views/nodes/scrapbook/ScrapbookBox.tsx @@ -11,11 +11,11 @@ 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 { 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 { ScrapbookItemConfig } from './ScrapbookPreset'; import { ImageBox } from '../ImageBox'; import { FireflyImageDimensions } from '../../smartdraw/FireflyConstants'; import { SmartDrawHandler } from '../../smartdraw/SmartDrawHandler'; @@ -104,7 +104,6 @@ export function slotRealDocIntoPlaceholders( 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; @@ -146,7 +145,6 @@ export class ScrapbookBox extends ViewBoxAnnotatableComponent() private _disposers: { [name: string]: IReactionDisposer } = {}; private imageBoxRef = React.createRef(); - // @observable configs : ScrapbookItemConfig[] constructor(props: FieldViewProps) { super(props); @@ -167,15 +165,13 @@ export class ScrapbookBox extends ViewBoxAnnotatableComponent() // 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) { @@ -217,7 +213,7 @@ export class ScrapbookBox extends ViewBoxAnnotatableComponent() 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 + image.accepts_tagType = 'PERSON' const placeholder = new Doc(); placeholder.proto = image; placeholder.original = image; @@ -225,23 +221,22 @@ export class ScrapbookBox extends ViewBoxAnnotatableComponent() 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' + sidebar.accepts_tagType = 'title'; const placeholder3 = new Doc(); placeholder3.proto = sidebar; placeholder3.original = sidebar; @@ -254,7 +249,7 @@ export class ScrapbookBox extends ViewBoxAnnotatableComponent() 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 + internalImg.accepts_tagType = 'LANDSCAPE' const placeholder5 = new Doc(); placeholder5.proto = internalImg; placeholder5.original = internalImg; @@ -282,23 +277,19 @@ export class ScrapbookBox extends ViewBoxAnnotatableComponent() 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.generateAiImage(); this._disposers.propagateResize = reaction( () => ({ w: this.layoutDoc._width, h: this.layoutDoc._height }), @@ -330,7 +321,7 @@ export class ScrapbookBox extends ViewBoxAnnotatableComponent() @action - async generateAiImageCorrect(prompt?: string) { + async generateAiImage(prompt?: string) { this.loading = true; try { // 1) Default to regenPrompt if none provided @@ -475,7 +466,7 @@ export class ScrapbookBox extends ViewBoxAnnotatableComponent() -
- )} + 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.BackgroundDoc && } +
+ +
+ } onClick={() => !this._loading && this.generateAiImage(this.regenPrompt)} /> +
+
+ + +
+ ); + } } - - Docs.Prototypes.TemplateMap.set(DocumentType.SCRAPBOOK, { layout: { view: ScrapbookBox, dataField: 'items' }, options: { diff --git a/src/client/views/nodes/scrapbook/ScrapbookPreset.tsx b/src/client/views/nodes/scrapbook/ScrapbookPreset.tsx index 706b9dafd..fe33741af 100644 --- a/src/client/views/nodes/scrapbook/ScrapbookPreset.tsx +++ b/src/client/views/nodes/scrapbook/ScrapbookPreset.tsx @@ -1,23 +1,22 @@ import { DocumentType } from '../../../documents/DocumentTypes'; export enum ScrapbookPresetType { - Default = 'Default', - Classic = 'Classic', + Default = 'Default', + Classic = 'Classic', None = 'Select Template', - Collage = 'Collage', + Collage = 'Collage', Spotlight = 'Spotlight', - Gallery = 'Gallery' + Gallery = 'Gallery', } export interface ScrapbookItemConfig { - type: DocumentType; - /** text shown in the placeholder bubble */ - tag: string; - /** what this slot actually accepts (defaults to `tag`) */ - acceptTags?: string[]; - x: number; y: number; + + message?: string; // optional text to display instead of [placeholder] + acceptTags[0] + type?: DocumentType; + /** what this slot actually accepts (defaults to `tag`) */ + acceptTags?: string[]; /** the frame this placeholder occupies */ width?: number; height?: number; @@ -30,147 +29,65 @@ export interface ScrapbookItemConfig { export class ScrapbookPreset { static createPreset(presetType: ScrapbookPresetType): ScrapbookItemConfig[] { switch (presetType) { - case ScrapbookPresetType.None: - return ScrapbookPreset.createNonePreset(); - case ScrapbookPresetType.Classic: - return ScrapbookPreset.createClassicPreset(); - case ScrapbookPresetType.Collage: - return ScrapbookPreset.createCollagePreset(); - case ScrapbookPresetType.Spotlight: - return ScrapbookPreset.createSpotlightPreset(); - case ScrapbookPresetType.Default: - return ScrapbookPreset.createDefaultPreset(); - case ScrapbookPresetType.Gallery: - return ScrapbookPreset.createGalleryPreset(); + case ScrapbookPresetType.None: return ScrapbookPreset.createNonePreset(); + case ScrapbookPresetType.Classic: return ScrapbookPreset.createClassicPreset(); + case ScrapbookPresetType.Collage: return ScrapbookPreset.createCollagePreset(); + case ScrapbookPresetType.Spotlight: return ScrapbookPreset.createSpotlightPreset(); + case ScrapbookPresetType.Default: return ScrapbookPreset.createDefaultPreset(); + case ScrapbookPresetType.Gallery: return ScrapbookPreset.createGalleryPreset(); default: throw new Error(`Unknown preset type: ${presetType}`); - } + } // prettier-ignore } - private static createNonePreset(): ScrapbookItemConfig[] { - return [ - - { type: DocumentType.RTF, - tag: 'To create a scrapbook from existing documents, marquee select. For existing scrapbook arrangements, select a preset from the dropdown.', - acceptTags: ['n/a'], - x: 0, y: 0, width: 250, height: 200 - }, - - ]; + private static createNonePreset(): ScrapbookItemConfig[] { + return [{ message: 'To create a scrapbook from existing documents, marquee select. For existing scrapbook arrangements, select a preset from the dropdown.', type: DocumentType.RTF, acceptTags: [], x: 0, y: 0, width: 250, height: 200 }]; } private static createClassicPreset(): ScrapbookItemConfig[] { return [ - { type: DocumentType.IMG, - tag: '[placeholder] LANDSCAPE', - acceptTags: ['LANDSCAPE'], - x: 0, y: -100, width: 250, height: 200 - }, - { type: DocumentType.RTF, - tag: '[placeholder] caption', - acceptTags: ['sentence'], - x: 0, y: 200, width: 250, height: 50 - }, - { type: DocumentType.RTF, - tag: '[placeholder] lengthy description', - acceptTags: ['paragraphs'], - x: 280, y: -50, width: 50, height: 200 - }, - { type: DocumentType.IMG, - tag: '[placeholder] PERSON', - acceptTags: ['PERSON'], - x: -200, y: -100, width: 100, height: 200 - }, + { type: DocumentType.IMG, message: '[placeholder] LANDSCAPE', acceptTags: ['LANDSCAPE'], x: 0, y: -100, width: 250, height: 200 }, + { type: DocumentType.RTF, message: '[placeholder] caption', acceptTags: ['sentence'], x: 0, y: 200, width: 250, height: 50 }, + { type: DocumentType.RTF, message: '[placeholder] lengthy description', acceptTags: ['paragraphs'], x: 280, y: -50, width: 50, height: 200 }, + { type: DocumentType.IMG, message: '[placeholder] PERSON', acceptTags: ['PERSON'], x: -200, y: -100, width: 100, height: 200 }, ]; } private static createGalleryPreset(): ScrapbookItemConfig[] { return [ - { type: DocumentType.IMG, tag: 'Gallery 1', acceptTags: ['LANDSCAPE'], x: -150, y: -150, width: 150, height: 150 }, - { type: DocumentType.IMG, tag: 'Gallery 2', acceptTags: ['LANDSCAPE'], x: 0, y: -150, width: 150, height: 150 }, - { type: DocumentType.IMG, tag: 'Gallery 3', acceptTags: ['LANDSCAPE'], x: 150, y: -150, width: 150, height: 150 }, - { type: DocumentType.IMG, tag: 'Gallery 4', acceptTags: ['LANDSCAPE'], x: -150, y: 0, width: 150, height: 150 }, - { type: DocumentType.IMG, tag: 'Gallery 5', acceptTags: ['LANDSCAPE'], x: 0, y: 0, width: 150, height: 150 }, - { type: DocumentType.IMG, tag: 'Gallery 6', acceptTags: ['LANDSCAPE'], x: 150, y: 0, width: 150, height: 150 }, + { type: DocumentType.IMG, message: 'Gallery 1', acceptTags: ['LANDSCAPE'], x: -150, y: -150, width: 150, height: 150 }, + { type: DocumentType.IMG, message: 'Gallery 2', acceptTags: ['LANDSCAPE'], x: 0, y: -150, width: 150, height: 150 }, + { type: DocumentType.IMG, message: 'Gallery 3', acceptTags: ['LANDSCAPE'], x: 150, y: -150, width: 150, height: 150 }, + { type: DocumentType.IMG, message: 'Gallery 4', acceptTags: ['LANDSCAPE'], x: -150, y: 0, width: 150, height: 150 }, + { type: DocumentType.IMG, message: 'Gallery 5', acceptTags: ['LANDSCAPE'], x: 0, y: 0, width: 150, height: 150 }, + { type: DocumentType.IMG, message: 'Gallery 6', acceptTags: ['LANDSCAPE'], x: 150, y: 0, width: 150, height: 150 }, ]; - } - + } private static createDefaultPreset(): ScrapbookItemConfig[] { return [ - { type: DocumentType.IMG, - tag: 'image', - acceptTags: ['LANDSCAPE'], - x: 0, y: -100, width: 250, height: 200 - }, - { type: DocumentType.RTF, - tag: 'summary', - acceptTags: ['sentence'], - x: 0, y: 200, width: 250 - }, - { type: DocumentType.RTF, - tag: 'sidebar', - acceptTags: ['paragraphs'], - 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', - acceptTags: ['PERSON'], - x: 0, y: 0, width: 50, height: 100 - } - ] - } - ]; + { type: DocumentType.IMG, message: 'image', acceptTags: ['LANDSCAPE'], x: 0, y: -100, width: 250, height: 200 }, + { type: DocumentType.RTF, message: 'summary', acceptTags: ['sentence'], x: 0, y: 200, width: 250 }, + { type: DocumentType.RTF, message: 'sidebar', acceptTags: ['paragraphs'], x: 280, y: -50, width: 50, height: 200 }, + { containerWidth: 300, containerHeight: 300, x: -200, y: -100, width: 100, height: 200, + children: [{ type: DocumentType.IMG, message: 'image internal', acceptTags: ['PERSON'], x: 0, y: 0, width: 50, height: 100 }], }, + ]; // prettier-ignore } private static createCollagePreset(): ScrapbookItemConfig[] { return [ - { type: DocumentType.IMG, - tag: 'LANDSCAPE', - acceptTags: ['LANDSCAPE'], - x: -150, y: -150, width: 150, height: 150 - }, - { type: DocumentType.IMG, - tag: 'PERSON', - acceptTags: ['PERSON'], - x: 0, y: -150, width: 150, height: 150 - }, - { type: DocumentType.RTF, - tag: 'caption', - acceptTags: ['sentence'], - x: -150, y: 0, width: 300, height: 100 - }, - { type: DocumentType.RTF, - tag: 'lengthy description', - acceptTags: ['paragraphs'], - x: 0, y: 100, width: 300, height: 100 - } - ]; + { type: DocumentType.IMG, message: 'LANDSCAPE', acceptTags: ['LANDSCAPE'], x: -150, y: -150, width: 150, height: 150 }, + { type: DocumentType.IMG, message: 'PERSON', acceptTags: ['PERSON'], x: 0, y: -150, width: 150, height: 150 }, + { type: DocumentType.RTF, message: 'caption', acceptTags: ['sentence'], x: -150, y: 0, width: 300, height: 100 }, + { type: DocumentType.RTF, message: 'lengthy description', acceptTags: ['paragraphs'], x: 0, y: 100, width: 300, height: 100 }, + ]; // prettier-ignore } private static createSpotlightPreset(): ScrapbookItemConfig[] { return [ - { type: DocumentType.RTF, - tag: 'title', - acceptTags: ['word'], - x: 0, y: -180, width: 300, height: 40 - }, - { type: DocumentType.IMG, - tag: 'LANDSCAPE', - acceptTags: ['LANDSCAPE'], - x: 0, y: 0, width: 300, height: 200 - }, - { type: DocumentType.RTF, - tag: 'caption', - acceptTags: ['sentence'], - x: 0, y: 230, width: 300, height: 50 - } + { type: DocumentType.RTF, message: 'title', acceptTags: ['word'], x: 0, y: -180, width: 300, height: 40 }, + { type: DocumentType.IMG, message: 'LANDSCAPE', acceptTags: ['LANDSCAPE'], x: 0, y: 0, width: 300, height: 200 }, + { type: DocumentType.RTF, message: 'caption', acceptTags: ['sentence'], x: 0, y: 230, width: 300, height: 50 }, ]; } } diff --git a/src/client/views/nodes/scrapbook/ScrapbookPresetRegistry.ts b/src/client/views/nodes/scrapbook/ScrapbookPresetRegistry.ts index c6d67ab73..3a2189d00 100644 --- a/src/client/views/nodes/scrapbook/ScrapbookPresetRegistry.ts +++ b/src/client/views/nodes/scrapbook/ScrapbookPresetRegistry.ts @@ -6,39 +6,31 @@ type PresetGenerator = () => ScrapbookItemConfig[]; // Internal map of preset name to generator const presetRegistry = new Map(); - - - /** * Register a new scrapbook preset under the given name. */ export function registerPreset(name: string, gen: PresetGenerator) { - presetRegistry.set(name, gen); + presetRegistry.set(name, gen); } /** * List all registered preset names. */ export function getPresetNames(): string[] { - return Array.from(presetRegistry.keys()); + return Array.from(presetRegistry.keys()); } /** * Create the config array for the named preset. */ export function createPreset(name: string): ScrapbookItemConfig[] { - const gen = presetRegistry.get(name); - if (!gen) throw new Error(`Unknown scrapbook preset: ${name}`); - return gen(); + const gen = presetRegistry.get(name); + if (!gen) throw new Error(`Unknown scrapbook preset: ${name}`); + return gen(); } // ------------------------ // Register built-in presets import { ScrapbookPreset } from './ScrapbookPreset'; -registerPreset(ScrapbookPresetType.None, () => ScrapbookPreset.createPreset(ScrapbookPresetType.None)); -registerPreset(ScrapbookPresetType.Classic, () => ScrapbookPreset.createPreset(ScrapbookPresetType.Classic)); -registerPreset(ScrapbookPresetType.Collage, () => ScrapbookPreset.createPreset(ScrapbookPresetType.Collage)); -registerPreset(ScrapbookPresetType.Spotlight, () => ScrapbookPreset.createPreset(ScrapbookPresetType.Spotlight)); -registerPreset(ScrapbookPresetType.Default, () => ScrapbookPreset.createPreset(ScrapbookPresetType.Default)); -registerPreset(ScrapbookPresetType.Gallery, () => ScrapbookPreset.createPreset(ScrapbookPresetType.Gallery)); +Object.keys(ScrapbookPresetType).forEach(key => registerPreset(key, () => ScrapbookPreset.createPreset(key as ScrapbookPresetType))); // pretter-ignore -- cgit v1.2.3-70-g09d2 From bf33580a66c1f8ce87e85bea701415788a887401 Mon Sep 17 00:00:00 2001 From: bobzel Date: Wed, 11 Jun 2025 11:50:45 -0400 Subject: change how autoTag is triggered for images to not use a reaction --- src/client/views/nodes/ImageBox.tsx | 83 +++++++++------------- src/client/views/search/FaceRecognitionHandler.tsx | 5 +- 2 files changed, 34 insertions(+), 54 deletions(-) (limited to 'src') diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx index 1e16bbfc9..d7e21b0a6 100644 --- a/src/client/views/nodes/ImageBox.tsx +++ b/src/client/views/nodes/ImageBox.tsx @@ -142,56 +142,39 @@ export class ImageBox extends ViewBoxAnnotatableComponent() { }; 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 labels 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. Then - provide five additional descriptive tags to describe the image for a total of 6 words outputted, - delimited by spaces. For example: "LANDSCAPE BUNNY NATURE FOREST PEACEFUL OUTDOORS". Then add one final lengthier summary tag (separated by underscores) - that describes the image.` - ); - - const { nativeWidth, nativeHeight } = this.nativeSize; - const aspectRatio = nativeWidth && nativeHeight - ? (nativeWidth / nativeHeight).toFixed(2) - : '1.00'; - - // 4) normalize and prefix - const label = raw - .trim() - .toUpperCase() - - // 5) stash it on the Doc - // overwrite any old tags so re-runs still work - const tokens = label.split(/\s+/); - this.Document.$tags_chat = new List(); - tokens.forEach(tok => { - (this.Document.$tags_chat as List).push(tok)}); - (this.Document.$tags_chat as List).push(`ASPECT_${aspectRatio}`); - - // 6) flip on “show tags” in the layout - // (same flag that ImageLabelBox.toggleDisplayInformation uses) - this.Document._layout_showTags = true; - - } catch (err) { - console.error('autoTag failed:', err); - } finally { - } - }; - - - + if (this.Document.$tags_chat) return; + 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 labels one label: PERSON or LANDSCAPE + const label = await gptImageLabel( + base64, + `Classify this image as PERSON or LANDSCAPE. You may only respond with one of these two options. + Then provide five additional descriptive tags to describe the image for a total of 6 words outputted, delimited by spaces. + For example: "LANDSCAPE BUNNY NATURE FOREST PEACEFUL OUTDOORS". + Then add one final lengthier summary tag (separated by underscores) that describes the image.` + ).then(raw => raw.trim().toUpperCase()); + + const { nativeWidth, nativeHeight } = this.nativeSize; + const aspectRatio = ((nativeWidth || 1) / (nativeHeight || 1)).toFixed(2); + + // 5) stash it on the Doc + // overwrite any old tags so re-runs still work + this.Document.$tags_chat = new List([...label.split(/\s+/), `ASPECT_${aspectRatio}`]); + + // 6) flip on “show tags” in the layout + this.Document._layout_showTags = true; + } catch (err) { + console.error('autoTag failed:', err); + } + }; getAnchor = (addAsAnnotation: boolean, pinProps?: PinProps) => { const visibleAnchor = this._getAnchor?.(this._savedAnnotations, true); // use marquee anchor, otherwise, save zoom/pan as anchor diff --git a/src/client/views/search/FaceRecognitionHandler.tsx b/src/client/views/search/FaceRecognitionHandler.tsx index 256e68afd..dac91b89a 100644 --- a/src/client/views/search/FaceRecognitionHandler.tsx +++ b/src/client/views/search/FaceRecognitionHandler.tsx @@ -9,7 +9,6 @@ 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'; /** @@ -210,9 +209,7 @@ export class FaceRecognitionHandler { } else if (imgDoc.type === DocumentType.LOADING && !imgDoc.loadingError) { setTimeout(() => this.classifyFacesInImage(imgDocView), 1000); } else { - reaction(() => ({sel:imgDocView.isSelected()}), ({sel}) => !sel && - imgDocView.ComponentView?.autoTag?.(), {fireImmediately: true} - ) + imgDocView.ComponentView?.autoTag?.(); 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. -- cgit v1.2.3-70-g09d2 From 6c011f502118e5246aabd29a30494c669d917fb1 Mon Sep 17 00:00:00 2001 From: bobzel Date: Wed, 11 Jun 2025 12:15:39 -0400 Subject: don't autoTag text docs until they're dropped onto a scrapbook. don't re-tag a text box that has not changed. --- .../collections/collectionFreeForm/MarqueeView.tsx | 8 ++------ .../views/nodes/formattedText/FormattedTextBox.tsx | 23 +++++++++++----------- src/client/views/nodes/scrapbook/ScrapbookBox.tsx | 5 ++++- src/client/views/search/FaceRecognitionHandler.tsx | 2 +- 4 files changed, 19 insertions(+), 19 deletions(-) (limited to 'src') diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx index 12515a72c..7a456c46f 100644 --- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx +++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx @@ -16,7 +16,7 @@ import { DocumentType } from '../../../documents/DocumentTypes'; import { Docs, DocumentOptions } from '../../../documents/Documents'; import { SnappingManager, freeformScrollMode } from '../../../util/SnappingManager'; import { Transform } from '../../../util/Transform'; -import { UndoManager, undoBatch, undoable } from '../../../util/UndoManager'; +import { UndoManager, undoBatch } from '../../../util/UndoManager'; import { ContextMenu } from '../../ContextMenu'; import { ObservableReactComponent } from '../../ObservableReactComponent'; import { MarqueeViewBounds } from '../../PinFuncs'; @@ -33,8 +33,6 @@ import { requestAiGeneratedPreset, DocumentDescriptor } from '../../nodes/scrapb import { buildPlaceholdersFromConfigs, slotRealDocIntoPlaceholders } from '../../nodes/scrapbook/ScrapbookBox'; import './MarqueeView.scss'; -import { build } from 'xregexp'; - interface MarqueeViewProps { Doc: Doc; getContainerTransform: () => Transform; @@ -589,9 +587,7 @@ export class MarqueeView extends ObservableReactComponent { - slotRealDocIntoPlaceholders(realDoc, allPlaceholders); - }); + selectedDocs.forEach(realDoc => slotRealDocIntoPlaceholders(realDoc, allPlaceholders)); const selected = this.marqueeSelect(false).map(d => { this._props.removeDocument?.(d); diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx index 1768eb08d..d700ce9f8 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx +++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx @@ -310,15 +310,17 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent { const rawText = RTFCast(this.Document[this.fieldKey])?.Text ?? StrCast(this.Document[this.fieldKey]); - const callType = rawText.includes('[placeholder]') ? GPTCallType.CLASSIFYTEXTMINIMAL : GPTCallType.CLASSIFYTEXTFULL; - - gptAPICall(rawText, callType).then( - action(desc => { - // Split GPT response into tokens and push individually & clear existing tags - this.Document.$tags_chat = new List(desc.trim().split(/\s+/)); - this.Document._layout_showTags = true; - }) - ); + if (rawText && !this.Document.$tags_chat) { + const callType = rawText.includes('[placeholder]') ? GPTCallType.CLASSIFYTEXTMINIMAL : GPTCallType.CLASSIFYTEXTFULL; + + gptAPICall(rawText, callType).then( + action(desc => { + // Split GPT response into tokens and push individually & clear existing tags + this.Document.$tags_chat = new List(desc.trim().split(/\s+/)); + this.Document._layout_showTags = true; + }) + ); + } }; leafText = (node: Node) => { @@ -382,6 +384,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent ({ title: this.Document.title, sel: this._props.isSelected() }), 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 fcb82a6ba..94522c979 100644 --- a/src/client/views/nodes/scrapbook/ScrapbookBox.tsx +++ b/src/client/views/nodes/scrapbook/ScrapbookBox.tsx @@ -57,7 +57,10 @@ export function buildPlaceholdersFromConfigs(configs: ScrapbookItemConfig[]) { return createMessagePlaceholder(cfg); }); } -export function slotRealDocIntoPlaceholders(realDoc: Doc, placeholders: Doc[]): boolean { +export async function slotRealDocIntoPlaceholders(realDoc: Doc, placeholders: Doc[]) { + if (!realDoc.$tags_chart) { + await DocumentView.getFirstDocumentView(realDoc)?.ComponentView?.autoTag?.(); + } const realTags = new Set(StrListCast(realDoc.$tags_chat).map(t => t.toLowerCase?.() ?? '')); // Find placeholder with most matching tags diff --git a/src/client/views/search/FaceRecognitionHandler.tsx b/src/client/views/search/FaceRecognitionHandler.tsx index dac91b89a..84404d65a 100644 --- a/src/client/views/search/FaceRecognitionHandler.tsx +++ b/src/client/views/search/FaceRecognitionHandler.tsx @@ -209,9 +209,9 @@ export class FaceRecognitionHandler { } else if (imgDoc.type === DocumentType.LOADING && !imgDoc.loadingError) { setTimeout(() => this.classifyFacesInImage(imgDocView), 1000); } else { - imgDocView.ComponentView?.autoTag?.(); const imgUrl = ImageCast(imgDoc[Doc.LayoutDataKey(imgDoc)]); if (imgUrl && !DocListCast(Doc.MyFaceCollection?.examinedFaceDocs).includes(imgDoc[DocData])) { + imgDocView.ComponentView?.autoTag?.(); // only examine Docs that have an image and that haven't already been examined. Doc.MyFaceCollection && Doc.AddDocToList(Doc.MyFaceCollection, 'examinedFaceDocs', imgDoc[DocData]); FaceRecognitionHandler.loadImage(imgUrl).then( -- cgit v1.2.3-70-g09d2 From 3df4e67c42e431603d60f00d0a91867df7d0e2a6 Mon Sep 17 00:00:00 2001 From: bobzel Date: Wed, 11 Jun 2025 12:36:08 -0400 Subject: don't recompute tags_chat for PDF and videos. Don't block within loop for getting pdf text. --- src/client/views/nodes/PDFBox.tsx | 63 +++++++++++--------------- src/client/views/nodes/VideoBox.tsx | 89 +++++++++++++++++-------------------- 2 files changed, 68 insertions(+), 84 deletions(-) (limited to 'src') diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx index a0c7d8d22..5501f0a31 100644 --- a/src/client/views/nodes/PDFBox.tsx +++ b/src/client/views/nodes/PDFBox.tsx @@ -81,46 +81,35 @@ export class PDFBox extends ViewBoxAnnotatableComponent() { } } - autoTag = async () => { - try { - if (!this._pdf) { - throw new Error('PDF not loaded'); - } - - // 1) Extract text from the first few pages (e.g., first 2 pages) - const maxPages = Math.min(2, this._pdf.numPages); - let textContent = ''; - for (let pageNum = 1; pageNum <= maxPages; pageNum++) { - const page = await this._pdf.getPage(pageNum); - const text = await page.getTextContent(); - const pageText = text.items.map(item => ('str' in item ? item.str : '')).join(' '); - textContent += ` ${pageText}`; - } - - if (!textContent.trim()) { - throw new Error('No text found in PDF'); - } - - // 2) Ask GPT to classify and provide descriptive tags - const raw = await gptAPICall( - `"${textContent.trim().slice(0, 2000)}"`, - GPTCallType.CLASSIFYTEXTFULL - ); - - // 3) Normalize and store the labels - const label = raw.trim().toUpperCase(); + autoTag = async () => { + if (!this.Document.$tags_chat && this._pdf) { + if (!this.dataDoc.text) { + // 1) Extract text from the first few pages (e.g., first 2 pages) + const maxPages = Math.min(2, this._pdf.numPages); + const promises: Promise[] = []; + for (let pageNum = 1; pageNum <= maxPages; pageNum++) { + promises.push( + this._pdf + .getPage(pageNum) + .then(page => page.getTextContent()) + .then(content => content.items.map(item => ('str' in item ? item.str : '')).join(' ')) + ); + } + this.dataDoc.text = (await Promise.all(promises)).join(' '); + } - const tokens = label.split(/\s+/); - this.Document.$tags_chat = new List(); - tokens.forEach(tok => (this.Document.$tags_chat as List).push(tok)); + const text = StrCast(this.dataDoc.text).trim().slice(0, 2000); + if (text) { + // 2) Ask GPT to classify and provide descriptive tags, then normalize the results + const label = await gptAPICall(`"${text}"`, GPTCallType.CLASSIFYTEXTFULL).then(raw => raw.trim().toUpperCase()); - // 4) Show tags in layout - this.Document._layout_showTags = true; + this.Document.$tags_chat = new List(label.split(/\s+/)); - } catch (err) { - console.error('PDF autoTag failed:', err); - } -}; + // 4) Show tags in layout + this.Document._layout_showTags = true; + } + } + }; replaceCanvases = (oldDiv: HTMLElement, newDiv: HTMLElement) => { if (oldDiv.childNodes) { diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx index 4d85b4942..f994bdbb5 100644 --- a/src/client/views/nodes/VideoBox.tsx +++ b/src/client/views/nodes/VideoBox.tsx @@ -110,56 +110,51 @@ export class VideoBox extends ViewBoxAnnotatableComponent() { return this._videoRef; } + autoTag = async () => { + if (this.Document.$tags_chat) return; + try { + if (!this.player) throw new Error('Video element not available.'); + + // 1) Extract a frame at the video's midpoint + const videoDuration = this.player.duration; + const snapshotTime = videoDuration / 2; + + // Seek the video element to the midpoint + await new Promise(resolve => { + const onSeeked = () => { + this.player!.removeEventListener('seeked', onSeeked); + resolve(); + }; + this.player!.addEventListener('seeked', onSeeked); + this.player!.currentTime = snapshotTime; + }); - autoTag = async () => { - try { - if (!this.player) throw new Error('Video element not available.'); - - // 1) Extract a frame at the video's midpoint - const videoDuration = this.player.duration; - const snapshotTime = videoDuration / 2; - - // Seek the video element to the midpoint - await new Promise((resolve, reject) => { - const onSeeked = () => { - this.player!.removeEventListener('seeked', onSeeked); - resolve(); - }; - this.player!.addEventListener('seeked', onSeeked); - this.player!.currentTime = snapshotTime; - }); - - // 2) Draw the frame onto a canvas and get a base64 representation - const canvas = document.createElement('canvas'); - canvas.width = this.player.videoWidth; - canvas.height = this.player.videoHeight; - const ctx = canvas.getContext('2d'); - if (!ctx) throw new Error('Failed to create canvas context.'); - ctx.drawImage(this.player, 0, 0, canvas.width, canvas.height); - const base64Image = canvas.toDataURL('image/png'); - - // 3) Send the image data to GPT for classification and descriptive tags - const raw = await gptImageLabel( - base64Image, - `Classify this video frame as either a PERSON or LANDSCAPE. + // 2) Draw the frame onto a canvas and get a base64 representation + const canvas = document.createElement('canvas'); + canvas.width = this.player.videoWidth; + canvas.height = this.player.videoHeight; + const ctx = canvas.getContext('2d'); + if (!ctx) throw new Error('Failed to create canvas context.'); + ctx.drawImage(this.player, 0, 0, canvas.width, canvas.height); + const base64Image = canvas.toDataURL('image/png'); + + // 3) Send the image data to GPT for classification and descriptive tags + const label = await gptImageLabel( + base64Image, + `Classify this video frame as either a PERSON or LANDSCAPE. Then provide five additional descriptive tags (single words) separated by spaces. Finally, add one detailed summary phrase using underscores.` - ); - - // 4) Normalize and store labels in the Document's tags - const label = raw.trim().toUpperCase(); - const tokens = label.split(/\s+/); - this.Document.$tags_chat = new List(); - tokens.forEach(tok => (this.Document.$tags_chat as List).push(tok)); - const aspect = this.player!.videoWidth / (this.player!.videoHeight || 1); - (this.Document.$tags_chat as List).push(`ASPECT_${aspect}`); - // 5) Turn on tag display in layout - this.Document._layout_showTags = true; - - } catch (err) { - console.error('Video autoTag failed:', err); - } -}; + ).then(raw => raw.trim().toUpperCase()); + + // 4) Normalize and store labels in the Document's tags + const aspect = this.player!.videoWidth / (this.player!.videoHeight || 1); + this.Document.$tags_chat = new List([...label.split(/\s+/), `ASPECT_${aspect}`]); + // 5) Turn on tag display in layout + this.Document._layout_showTags = true; + } catch (err) { + console.error('Video autoTag failed:', err); + } + }; componentDidMount() { this.unmounting = false; -- cgit v1.2.3-70-g09d2 From b4db1e2467337468139d0e92ef94799c4143a0fc Mon Sep 17 00:00:00 2001 From: bobzel Date: Wed, 11 Jun 2025 12:43:16 -0400 Subject: added outpaintable method to streamline determining which docs can be outpainted. --- src/client/views/DocumentDecorations.tsx | 8 ++++---- src/client/views/ViewBoxInterface.ts | 1 + src/client/views/nodes/ImageBox.tsx | 10 ++++------ src/client/views/nodes/scrapbook/ScrapbookBox.tsx | 2 ++ 4 files changed, 11 insertions(+), 10 deletions(-) (limited to 'src') diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx index f36312056..383960444 100644 --- a/src/client/views/DocumentDecorations.tsx +++ b/src/client/views/DocumentDecorations.tsx @@ -1,7 +1,7 @@ +import { IconButton } from '@dash/components'; import { IconProp } from '@fortawesome/fontawesome-svg-core'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Tooltip } from '@mui/material'; -import { IconButton } from '@dash/components'; import { action, computed, makeObservable, observable, runInAction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; @@ -27,6 +27,7 @@ import './DocumentDecorations.scss'; import { InkStrokeProperties } from './InkStrokeProperties'; import { InkingStroke } from './InkingStroke'; import { ObservableReactComponent } from './ObservableReactComponent'; +import { TagsView } from './TagsView'; import { CollectionDockingView } from './collections/CollectionDockingView'; import { CollectionFreeFormView } from './collections/collectionFreeForm'; import { Colors } from './global/globalEnums'; @@ -35,7 +36,6 @@ import { DocumentView } from './nodes/DocumentView'; import { ImageBox } from './nodes/ImageBox'; import { OpenWhere, OpenWhereMod } from './nodes/OpenWhere'; import { FormattedTextBox } from './nodes/formattedText/FormattedTextBox'; -import { TagsView } from './TagsView'; import { ScrapbookBox } from './nodes/scrapbook/ScrapbookBox'; interface DocumentDecorationsProps { @@ -431,7 +431,7 @@ export class DocumentDecorations extends ObservableReactComponent { SnappingManager.SetIsResizing(DocumentView.Selected().lastElement()?.Document[Id]); // turns off pointer events on things like youtube videos and web pages so that dragging doesn't get "stuck" when cursor moves over them DocumentView.Selected() - .filter(dv => e.shiftKey && dv.ComponentView instanceof ImageBox || dv.ComponentView instanceof ScrapbookBox) + .filter(dv => e.shiftKey && dv.ComponentView?.isOutpaintable?.()) .forEach(dv => { dv.Document[dv.ComponentView!.fieldKey + '_outpaintOriginalWidth'] = NumCast(dv.Document._width); dv.Document[dv.ComponentView!.fieldKey + '_outpaintOriginalHeight'] = NumCast(dv.Document._height); @@ -487,7 +487,7 @@ export class DocumentDecorations extends ObservableReactComponent dv.ComponentView instanceof ImageBox || dv.ComponentView instanceof ScrapbookBox) : []; + const outpainted = e.shiftKey ? DocumentView.Selected().filter(dv => dv.ComponentView?.isOutpaintable?.()) : []; const notOutpainted = e.shiftKey ? DocumentView.Selected().filter(dv => !outpainted.includes(dv)) : DocumentView.Selected(); // Special handling for shift-drag resize (outpainting of Images by resizing without scaling content - fill in with firefly GAI) diff --git a/src/client/views/ViewBoxInterface.ts b/src/client/views/ViewBoxInterface.ts index b532dfe35..83e395867 100644 --- a/src/client/views/ViewBoxInterface.ts +++ b/src/client/views/ViewBoxInterface.ts @@ -25,6 +25,7 @@ export abstract class ViewBoxInterface

extends ObservableReactComponent Doc[]; docEditorView?: () => void; autoTag?: () => void; // auto tag the document + isOutpaintable?: () => boolean; // can document be resized and outpainted showSmartDraw?: (x: number, y: number, regenerate?: boolean) => void; updateIcon?: (usePanelDimensions?: boolean) => Promise; // 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/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx index d7e21b0a6..a99421f6d 100644 --- a/src/client/views/nodes/ImageBox.tsx +++ b/src/client/views/nodes/ImageBox.tsx @@ -262,9 +262,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent() { } }; - handleSelection = async (selection: string) => { - this._searchInput = selection; - }; + handleSelection = async (selection: string) => (this._searchInput = selection); drop = undoable( action((e: Event, de: DragManager.DropEvent) => { @@ -429,9 +427,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent() { }; @action - handlePromptChange = (val: string | number) => { - this._outpaintPromptInput = '' + val; - }; + handlePromptChange = (val: string | number) => (this._outpaintPromptInput = '' + val); @action submitOutpaintPrompt = () => { @@ -532,6 +528,8 @@ export class ImageBox extends ViewBoxAnnotatableComponent() { return this._props.PanelWidth() / this._props.PanelHeight() < this.nativeSize.nativeWidth / this.nativeSize.nativeHeight; } + isOutpaintable = () => true; + componentUI = (/* boundsLeft: number, boundsTop: number*/) => !this._showOutpaintPrompt ? null : (

() ); } + isOutpaintable = () => true; + @action generateAiImage = (prompt: string) => { this._loading = true; -- cgit v1.2.3-70-g09d2 From 05845fa3e5a6d71a58845d058a15233ccf6d72c0 Mon Sep 17 00:00:00 2001 From: bobzel Date: Wed, 11 Jun 2025 13:01:44 -0400 Subject: refactor unwrapPlaceholder duplicated code to DocUtils. --- src/client/documents/DocUtils.ts | 9 +++++++++ .../collections/collectionFreeForm/ImageLabelBox.tsx | 7 +++---- .../collections/collectionFreeForm/MarqueeView.tsx | 19 +++++-------------- src/client/views/nodes/scrapbook/ScrapbookBox.tsx | 12 +++--------- 4 files changed, 20 insertions(+), 27 deletions(-) (limited to 'src') diff --git a/src/client/documents/DocUtils.ts b/src/client/documents/DocUtils.ts index dee929c89..9704867d5 100644 --- a/src/client/documents/DocUtils.ts +++ b/src/client/documents/DocUtils.ts @@ -587,6 +587,15 @@ export namespace DocUtils { doc.onClick = FollowLinkScript(); } + /** + * iterate through all items and their children and return a flat list of leaf placeholder content Docs + * @param items + * @returns list of placeholder content Docs + */ + export function unwrapPlaceholders(items: Doc[]): Doc[] { + return items.flatMap(d => (d.$type === DocumentType.COL ? unwrapPlaceholders(DocListCast(d[Doc.LayoutDataKey(d)])) : [d])); + } + export function LeavePushpin(doc: Doc, annotationField: string) { if (doc.followLinkToggle) return undefined; const context = Cast(doc.embedContainer, Doc, null) ?? Cast(doc.annotationOn, Doc, null); diff --git a/src/client/views/collections/collectionFreeForm/ImageLabelBox.tsx b/src/client/views/collections/collectionFreeForm/ImageLabelBox.tsx index 038b1c6f9..e3a3f9b05 100644 --- a/src/client/views/collections/collectionFreeForm/ImageLabelBox.tsx +++ b/src/client/views/collections/collectionFreeForm/ImageLabelBox.tsx @@ -158,10 +158,9 @@ export class ImageLabelBox extends ViewBoxBaseComponent() { this._currentLabel = e.target.value; }); - classifyImagesInBox = async (selectedImages? : Doc[], prompt? : string) => { + classifyImagesInBox = async () => { this.startLoading(); - alert('Classifying images...'); - selectedImages ??= this._selectedImages; + const selectedImages = this._selectedImages; // Converts the images into a Base64 format, afterwhich the information is sent to GPT to label them. @@ -170,7 +169,7 @@ export class ImageLabelBox extends ViewBoxBaseComponent() { const url = ImageCastWithSuffix(doc[Doc.LayoutDataKey(doc)], '_o') ?? ''; return imageUrlToBase64(url).then(hrefBase64 => !hrefBase64 ? undefined : - gptImageLabel(hrefBase64, prompt ?? '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/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx index 7a456c46f..128606675 100644 --- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx +++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx @@ -527,17 +527,11 @@ export class MarqueeView extends ObservableReactComponent ({ + getAiPresetsDescriptors = (): DocumentDescriptor[] => + this.marqueeSelect(false).map(doc => ({ type: typeof doc.$type === 'string' ? doc.$type : 'UNKNOWN', - tags: (() => { - const s = new Set(); - StrListCast(doc.$tags_chat ?? new List()).forEach(t => s.add(t)); - return Array.from(s); - })(), + tags: Array.from(new Set(StrListCast(doc.$tags_chat))), })); - } generateScrapbook = action(async () => { const selectedDocs = this.marqueeSelect(false); @@ -582,14 +576,12 @@ export class MarqueeView extends ObservableReactComponent items.flatMap(d => (d.$type === DocumentType.COL ? unwrap(DocListCast(d[Doc.LayoutDataKey(d)])) : [d])); - const allPlaceholders = unwrap(flatPl); + const allPlaceholders = DocUtils.unwrapPlaceholders(scrapbookPlaceholders); // 4) Slot each selectedDocs[i] into the first matching placeholder selectedDocs.forEach(realDoc => slotRealDocIntoPlaceholders(realDoc, allPlaceholders)); - const selected = this.marqueeSelect(false).map(d => { + 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; @@ -597,7 +589,6 @@ export class MarqueeView extends ObservableReactComponent this._props.removeDocument?.(doc)); 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' }); diff --git a/src/client/views/nodes/scrapbook/ScrapbookBox.tsx b/src/client/views/nodes/scrapbook/ScrapbookBox.tsx index 2db76c76f..ff757af88 100644 --- a/src/client/views/nodes/scrapbook/ScrapbookBox.tsx +++ b/src/client/views/nodes/scrapbook/ScrapbookBox.tsx @@ -23,6 +23,7 @@ import './ScrapbookBox.scss'; import { ScrapbookItemConfig } from './ScrapbookPreset'; import { createPreset, getPresetNames } from './ScrapbookPresetRegistry'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { DocUtils } from '../../../documents/DocUtils'; function createPlaceholder(cfg: ScrapbookItemConfig, doc: Doc) { const placeholder = new Doc(); @@ -206,13 +207,6 @@ export class ScrapbookBox extends ViewBoxAnnotatableComponent() // eslint-disable-next-line @typescript-eslint/no-unused-vars rejectDrop = (de: DragManager.DropEvent, subView?: DocumentView) => false; // allow all Docs to be dropped onto scrapbook -- let filterAddDocument make the final decision. - /** - * iterate through all items and their children and return a flat list of leaf placeholder content Docs - * @param items - * @returns list of placeholder content Docs - */ - unwrapPlaceholders = (items: Doc[]): Doc[] => items.flatMap(d => (d.$type === DocumentType.COL ? this.unwrapPlaceholders(DocListCast(d[Doc.LayoutDataKey(d)])) : [d])); - /** * Filter function to determine if a document can be added to the scrapbook. * This checks if the document matches any of the placeholder slots in the scrapbook. @@ -220,12 +214,12 @@ export class ScrapbookBox extends ViewBoxAnnotatableComponent() * @returns true if the document can be added, false otherwise. */ filterAddDocument = (docs: Doc | Doc[]) => { - toList(docs).forEach(doc => slotRealDocIntoPlaceholders(doc, this.unwrapPlaceholders(this.ScrapbookLayoutDocs))); + toList(docs).forEach(doc => slotRealDocIntoPlaceholders(doc, DocUtils.unwrapPlaceholders(this.ScrapbookLayoutDocs))); return false; }; @computed get regenPrompt() { - const allDocs = this.unwrapPlaceholders(this.ScrapbookLayoutDocs); // find all non-collections in scrapbook (e.g., placeholder content docs) + const allDocs = DocUtils.unwrapPlaceholders(this.ScrapbookLayoutDocs); // find all non-collections in scrapbook (e.g., placeholder content docs) const internalTagsSet = new Set(allDocs.flatMap(doc => StrListCast(doc.$tags_chat).filter(tag => !tag.startsWith?.('ASPECT_')))); const internalTags = Array.from(internalTagsSet).join(', '); -- cgit v1.2.3-70-g09d2 From c39bebac486261df011b692c1a14138c24eb5c71 Mon Sep 17 00:00:00 2001 From: sharkiecodes Date: Thu, 12 Jun 2025 12:11:11 -0400 Subject: Update ScrapbookPreset.tsx fixing presets --- .../views/nodes/scrapbook/ScrapbookPreset.tsx | 38 +++++++++++----------- 1 file changed, 19 insertions(+), 19 deletions(-) (limited to 'src') diff --git a/src/client/views/nodes/scrapbook/ScrapbookPreset.tsx b/src/client/views/nodes/scrapbook/ScrapbookPreset.tsx index fe33741af..f15fc8172 100644 --- a/src/client/views/nodes/scrapbook/ScrapbookPreset.tsx +++ b/src/client/views/nodes/scrapbook/ScrapbookPreset.tsx @@ -46,48 +46,48 @@ export class ScrapbookPreset { private static createClassicPreset(): ScrapbookItemConfig[] { return [ - { type: DocumentType.IMG, message: '[placeholder] LANDSCAPE', acceptTags: ['LANDSCAPE'], x: 0, y: -100, width: 250, height: 200 }, + { type: DocumentType.IMG, message: '[placeholder] landscape', acceptTags: ['LANDSCAPE'], x: 0, y: -100, width: 250, height: 200 }, { type: DocumentType.RTF, message: '[placeholder] caption', acceptTags: ['sentence'], x: 0, y: 200, width: 250, height: 50 }, { type: DocumentType.RTF, message: '[placeholder] lengthy description', acceptTags: ['paragraphs'], x: 280, y: -50, width: 50, height: 200 }, - { type: DocumentType.IMG, message: '[placeholder] PERSON', acceptTags: ['PERSON'], x: -200, y: -100, width: 100, height: 200 }, + { type: DocumentType.IMG, message: '[placeholder] person', acceptTags: ['PERSON'], x: -200, y: -100, width: 100, height: 200 }, ]; } private static createGalleryPreset(): ScrapbookItemConfig[] { return [ - { type: DocumentType.IMG, message: 'Gallery 1', acceptTags: ['LANDSCAPE'], x: -150, y: -150, width: 150, height: 150 }, - { type: DocumentType.IMG, message: 'Gallery 2', acceptTags: ['LANDSCAPE'], x: 0, y: -150, width: 150, height: 150 }, - { type: DocumentType.IMG, message: 'Gallery 3', acceptTags: ['LANDSCAPE'], x: 150, y: -150, width: 150, height: 150 }, - { type: DocumentType.IMG, message: 'Gallery 4', acceptTags: ['LANDSCAPE'], x: -150, y: 0, width: 150, height: 150 }, - { type: DocumentType.IMG, message: 'Gallery 5', acceptTags: ['LANDSCAPE'], x: 0, y: 0, width: 150, height: 150 }, - { type: DocumentType.IMG, message: 'Gallery 6', acceptTags: ['LANDSCAPE'], x: 150, y: 0, width: 150, height: 150 }, + { type: DocumentType.IMG, message: 'Gallery 1 ', acceptTags: ['PERSON'], x: -150, y: -150, width: 150, height: 150 }, + { type: DocumentType.IMG, message: 'Gallery 2', acceptTags: ['PERSON'], x: 0, y: -150, width: 150, height: 150 }, + { type: DocumentType.IMG, message: 'Gallery 3', acceptTags: ['PERSON'], x: 150, y: -150, width: 150, height: 150 }, + { type: DocumentType.IMG, message: 'Gallery 4', acceptTags: ['PERSON'], x: -150, y: 0, width: 150, height: 150 }, + { type: DocumentType.IMG, message: 'Gallery 5', acceptTags: ['PERSON'], x: 0, y: 0, width: 150, height: 150 }, + { type: DocumentType.IMG, message: 'Gallery 6', acceptTags: ['PERSON'], x: 150, y: 0, width: 150, height: 150 }, ]; } private static createDefaultPreset(): ScrapbookItemConfig[] { return [ - { type: DocumentType.IMG, message: 'image', acceptTags: ['LANDSCAPE'], x: 0, y: -100, width: 250, height: 200 }, - { type: DocumentType.RTF, message: 'summary', acceptTags: ['sentence'], x: 0, y: 200, width: 250 }, - { type: DocumentType.RTF, message: 'sidebar', acceptTags: ['paragraphs'], x: 280, y: -50, width: 50, height: 200 }, + { type: DocumentType.IMG, message: 'drop a landscape image', acceptTags: ['LANDSCAPE'], x: 44, y: -50, width: 200, height: 120 }, + { type: DocumentType.RTF, message: 'summary text', acceptTags: ['sentence'], x: 0, y: 200, width: 345 }, + { type: DocumentType.RTF, message: 'sidebar text', acceptTags: ['paragraphs'], x: 250, y: -50, width: 100, height: 200 }, { containerWidth: 300, containerHeight: 300, x: -200, y: -100, width: 100, height: 200, - children: [{ type: DocumentType.IMG, message: 'image internal', acceptTags: ['PERSON'], x: 0, y: 0, width: 50, height: 100 }], }, + children: [{ type: DocumentType.IMG, message: 'drop a person image', acceptTags: ['PERSON'], x: 0, y: 0, width: 50, height: 100 }], }, ]; // prettier-ignore } private static createCollagePreset(): ScrapbookItemConfig[] { return [ - { type: DocumentType.IMG, message: 'LANDSCAPE', acceptTags: ['LANDSCAPE'], x: -150, y: -150, width: 150, height: 150 }, - { type: DocumentType.IMG, message: 'PERSON', acceptTags: ['PERSON'], x: 0, y: -150, width: 150, height: 150 }, - { type: DocumentType.RTF, message: 'caption', acceptTags: ['sentence'], x: -150, y: 0, width: 300, height: 100 }, - { type: DocumentType.RTF, message: 'lengthy description', acceptTags: ['paragraphs'], x: 0, y: 100, width: 300, height: 100 }, + { type: DocumentType.IMG, message: 'landscape image', acceptTags: ['LANDSCAPE'], x: -174, y: 100, width: 160, height: 150 }, + { type: DocumentType.IMG, message: 'person image', acceptTags: ['PERSON'], x: 0, y: 100, width: 150, height: 150 }, + { type: DocumentType.RTF, message: 'caption', acceptTags: ['sentence'], x: -170, y: 40, width: 150, height: 40 }, + { type: DocumentType.RTF, message: 'lengthy description', acceptTags: ['paragraphs'], x: -180, y: -60, width: 350, height: 100 }, ]; // prettier-ignore } private static createSpotlightPreset(): ScrapbookItemConfig[] { return [ - { type: DocumentType.RTF, message: 'title', acceptTags: ['word'], x: 0, y: -180, width: 300, height: 40 }, - { type: DocumentType.IMG, message: 'LANDSCAPE', acceptTags: ['LANDSCAPE'], x: 0, y: 0, width: 300, height: 200 }, - { type: DocumentType.RTF, message: 'caption', acceptTags: ['sentence'], x: 0, y: 230, width: 300, height: 50 }, + { type: DocumentType.RTF, message: 'title text', acceptTags: ['word'], x: 0, y: -30, width: 300, height: 40 }, + { type: DocumentType.IMG, message: 'drop a landscape image', acceptTags: ['LANDSCAPE'], x: 0, y: 20, width: 300, height: 200 }, + { type: DocumentType.RTF, message: 'caption text', acceptTags: ['sentence'], x: 0, y: 230, width: 300, height: 50 }, ]; } } -- cgit v1.2.3-70-g09d2 From 2c947f1ba7785ccbb705c0dce7e6bd64821da99d Mon Sep 17 00:00:00 2001 From: sharkiecodes Date: Thu, 12 Jun 2025 12:39:47 -0400 Subject: Update ScrapbookPreset.tsx cleaned up presets --- .../views/nodes/scrapbook/ScrapbookPreset.tsx | 23 +++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) (limited to 'src') diff --git a/src/client/views/nodes/scrapbook/ScrapbookPreset.tsx b/src/client/views/nodes/scrapbook/ScrapbookPreset.tsx index f15fc8172..a3405083b 100644 --- a/src/client/views/nodes/scrapbook/ScrapbookPreset.tsx +++ b/src/client/views/nodes/scrapbook/ScrapbookPreset.tsx @@ -1,12 +1,12 @@ import { DocumentType } from '../../../documents/DocumentTypes'; export enum ScrapbookPresetType { - Default = 'Default', - Classic = 'Classic', - None = 'Select Template', + None = 'None', Collage = 'Collage', Spotlight = 'Spotlight', Gallery = 'Gallery', + Default = 'Default', + Classic = 'Classic', } export interface ScrapbookItemConfig { @@ -47,15 +47,15 @@ export class ScrapbookPreset { private static createClassicPreset(): ScrapbookItemConfig[] { return [ { type: DocumentType.IMG, message: '[placeholder] landscape', acceptTags: ['LANDSCAPE'], x: 0, y: -100, width: 250, height: 200 }, - { type: DocumentType.RTF, message: '[placeholder] caption', acceptTags: ['sentence'], x: 0, y: 200, width: 250, height: 50 }, - { type: DocumentType.RTF, message: '[placeholder] lengthy description', acceptTags: ['paragraphs'], x: 280, y: -50, width: 50, height: 200 }, - { type: DocumentType.IMG, message: '[placeholder] person', acceptTags: ['PERSON'], x: -200, y: -100, width: 100, height: 200 }, + { type: DocumentType.RTF, message: '[placeholder] lengthy caption', acceptTags: ['paragraphs'], x: 0, y: 138, width: 250, height: 172 }, + { type: DocumentType.RTF, message: '[placeholder] brief description', acceptTags: ['sentence'], x: 280, y: -50, width: 50, height: 200 }, + { type: DocumentType.IMG, message: '[placeholder] person', acceptTags: ['PERSON'], x: -200, y: -100, width: 167, height: 200 }, ]; } private static createGalleryPreset(): ScrapbookItemConfig[] { return [ - { type: DocumentType.IMG, message: 'Gallery 1 ', acceptTags: ['PERSON'], x: -150, y: -150, width: 150, height: 150 }, + { type: DocumentType.IMG, message: 'Gallery 1 ', acceptTags: ['PERSON'], x: -150, y: -150, width: 150, height: 150 }, { type: DocumentType.IMG, message: 'Gallery 2', acceptTags: ['PERSON'], x: 0, y: -150, width: 150, height: 150 }, { type: DocumentType.IMG, message: 'Gallery 3', acceptTags: ['PERSON'], x: 150, y: -150, width: 150, height: 150 }, { type: DocumentType.IMG, message: 'Gallery 4', acceptTags: ['PERSON'], x: -150, y: 0, width: 150, height: 150 }, @@ -67,10 +67,10 @@ export class ScrapbookPreset { private static createDefaultPreset(): ScrapbookItemConfig[] { return [ { type: DocumentType.IMG, message: 'drop a landscape image', acceptTags: ['LANDSCAPE'], x: 44, y: -50, width: 200, height: 120 }, - { type: DocumentType.RTF, message: 'summary text', acceptTags: ['sentence'], x: 0, y: 200, width: 345 }, + { type: DocumentType.PDF, message: 'summary pdf', acceptTags: ['word', 'sentence', 'paragraphs'], x: 45, y: 93, width: 184, height: 273 }, { type: DocumentType.RTF, message: 'sidebar text', acceptTags: ['paragraphs'], x: 250, y: -50, width: 100, height: 200 }, - { containerWidth: 300, containerHeight: 300, x: -200, y: -100, width: 100, height: 200, - children: [{ type: DocumentType.IMG, message: 'drop a person image', acceptTags: ['PERSON'], x: 0, y: 0, width: 50, height: 100 }], }, + { containerWidth: 200, containerHeight: 425, x: -171, y: -54, width: 200, height: 425, + children: [{ type: DocumentType.IMG, message: 'drop a person image', acceptTags: ['PERSON'], x: -350, y: 200, width: 162, height: 137 }], }, ]; // prettier-ignore } @@ -78,7 +78,8 @@ export class ScrapbookPreset { return [ { type: DocumentType.IMG, message: 'landscape image', acceptTags: ['LANDSCAPE'], x: -174, y: 100, width: 160, height: 150 }, { type: DocumentType.IMG, message: 'person image', acceptTags: ['PERSON'], x: 0, y: 100, width: 150, height: 150 }, - { type: DocumentType.RTF, message: 'caption', acceptTags: ['sentence'], x: -170, y: 40, width: 150, height: 40 }, + { type: DocumentType.RTF, message: 'caption', acceptTags: ['sentence'], x: -174, y: 50, width: 150, height: 40 }, + { type: DocumentType.RTF, message: 'caption', acceptTags: ['sentence'], x: 0, y: 50, width: 150, height: 40 }, { type: DocumentType.RTF, message: 'lengthy description', acceptTags: ['paragraphs'], x: -180, y: -60, width: 350, height: 100 }, ]; // prettier-ignore } -- cgit v1.2.3-70-g09d2 From dd84952de3381049aca6742054f28bf502df2e16 Mon Sep 17 00:00:00 2001 From: bobzel Date: Thu, 19 Jun 2025 15:21:53 -0400 Subject: removing references to Box types from documentdecorations - added showBorderRounding to viewBoxInterface. --- src/client/views/ViewBoxInterface.ts | 1 + .../views/collections/collectionFreeForm/CollectionFreeFormView.tsx | 3 ++- src/client/views/nodes/ImageBox.tsx | 3 ++- src/client/views/nodes/formattedText/FormattedTextBox.tsx | 3 ++- src/client/views/nodes/scrapbook/ScrapbookBox.tsx | 4 +++- 5 files changed, 10 insertions(+), 4 deletions(-) (limited to 'src') diff --git a/src/client/views/ViewBoxInterface.ts b/src/client/views/ViewBoxInterface.ts index 83e395867..514dc4ae8 100644 --- a/src/client/views/ViewBoxInterface.ts +++ b/src/client/views/ViewBoxInterface.ts @@ -26,6 +26,7 @@ export abstract class ViewBoxInterface

extends ObservableReactComponent void; autoTag?: () => void; // auto tag the document isOutpaintable?: () => boolean; // can document be resized and outpainted + showBorderRounding?: () => boolean; // can document borders be rounded showSmartDraw?: (x: number, y: number, regenerate?: boolean) => void; updateIcon?: (usePanelDimensions?: boolean) => Promise; // 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/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index 3571dab1a..32ace463d 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'; @@ -2065,6 +2065,7 @@ export class CollectionFreeFormView extends CollectionSubView !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 diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx index 356f9e13f..4d8d486b1 100644 --- a/src/client/views/nodes/ImageBox.tsx +++ b/src/client/views/nodes/ImageBox.tsx @@ -8,7 +8,7 @@ import { extname } from 'path'; import * as React from 'react'; import { AiOutlineSend } from 'react-icons/ai'; import ReactLoading from 'react-loading'; -import { ClientUtils, imageUrlToBase64, DashColor, returnEmptyString, returnFalse, returnOne, returnZero, setupMoveUpEvents, UpdateIcon } from '../../../ClientUtils'; +import { ClientUtils, imageUrlToBase64, DashColor, returnEmptyString, returnFalse, returnOne, returnZero, setupMoveUpEvents, UpdateIcon, returnTrue } from '../../../ClientUtils'; import { Doc, DocListCast, Opt } from '../../../fields/Doc'; import { DocData } from '../../../fields/DocSymbols'; import { Id } from '../../../fields/FieldSymbols'; @@ -994,6 +994,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent() { return { width, height }; }; savedAnnotations = () => this._savedAnnotations; + showBorderRounding = returnTrue; rejectDrop = (de: DragManager.DropEvent, subView?: DocumentView | undefined) => (this.dataDoc[this.fieldKey] === undefined ? true : (this._props.rejectDrop?.(de, subView) ?? false)); render() { TraceMobx(); diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx index d700ce9f8..07cb795f1 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx +++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx @@ -13,7 +13,7 @@ import { EditorState, NodeSelection, Plugin, Selection, TextSelection, Transacti import { EditorView, NodeViewConstructor } from 'prosemirror-view'; import * as React from 'react'; import { BsMarkdownFill } from 'react-icons/bs'; -import { addStyleSheet, addStyleSheetRule, clearStyleSheetRules, ClientUtils, DivWidth, removeStyleSheet, returnFalse, returnZero, setupMoveUpEvents, simMouseEvent, smoothScroll, StopEvent } from '../../../../ClientUtils'; +import { addStyleSheet, addStyleSheetRule, clearStyleSheetRules, ClientUtils, DivWidth, removeStyleSheet, returnFalse, returnTrue, returnZero, setupMoveUpEvents, simMouseEvent, smoothScroll, StopEvent } from '../../../../ClientUtils'; import { DateField } from '../../../../fields/DateField'; import { CreateLinkToActiveAudio, Doc, DocListCast, Field, FieldType, Opt, StrListCast } from '../../../../fields/Doc'; import { AclAdmin, AclAugment, AclEdit, AclSelfEdit, DocCss, ForceServerWrite, UpdatingFromServer } from '../../../../fields/DocSymbols'; @@ -1088,6 +1088,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent { if (DocListCast(this.dataDoc[this.sidebarKey]).find(anno => Doc.AreProtosEqual(doc.layout_unrendered ? DocCast(doc.annotationOn) : doc, anno))) { return SidebarAnnos.getView(this._sidebarRef.current, this.SidebarShown, () => this.toggleSidebar(false), doc, options); diff --git a/src/client/views/nodes/scrapbook/ScrapbookBox.tsx b/src/client/views/nodes/scrapbook/ScrapbookBox.tsx index ff757af88..d0ae6194f 100644 --- a/src/client/views/nodes/scrapbook/ScrapbookBox.tsx +++ b/src/client/views/nodes/scrapbook/ScrapbookBox.tsx @@ -24,6 +24,7 @@ import { ScrapbookItemConfig } from './ScrapbookPreset'; import { createPreset, getPresetNames } from './ScrapbookPresetRegistry'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { DocUtils } from '../../../documents/DocUtils'; +import { returnTrue } from '../../../../ClientUtils'; function createPlaceholder(cfg: ScrapbookItemConfig, doc: Doc) { const placeholder = new Doc(); @@ -175,7 +176,8 @@ export class ScrapbookBox extends ViewBoxAnnotatableComponent() ); } - isOutpaintable = () => true; + isOutpaintable = returnTrue; + showBorderRounding = returnTrue; @action generateAiImage = (prompt: string) => { -- cgit v1.2.3-70-g09d2 From bb7315dab4ba6da444e6a73820aed86bbdb82522 Mon Sep 17 00:00:00 2001 From: bobzel Date: Thu, 19 Jun 2025 15:22:49 -0400 Subject: from last --- src/client/views/DocumentDecorations.tsx | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) (limited to 'src') diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx index 9172a3734..7a9f6c514 100644 --- a/src/client/views/DocumentDecorations.tsx +++ b/src/client/views/DocumentDecorations.tsx @@ -33,10 +33,7 @@ import { CollectionFreeFormView } from './collections/collectionFreeForm'; import { Colors } from './global/globalEnums'; import { CollectionFreeFormDocumentView } from './nodes/CollectionFreeFormDocumentView'; import { DocumentView } from './nodes/DocumentView'; -import { ImageBox } from './nodes/ImageBox'; import { OpenWhere, OpenWhereMod } from './nodes/OpenWhere'; -import { FormattedTextBox } from './nodes/formattedText/FormattedTextBox'; -import { ScrapbookBox } from './nodes/scrapbook/ScrapbookBox'; interface DocumentDecorationsProps { PanelWidth: number; @@ -751,7 +748,7 @@ export class DocumentDecorations extends ObservableReactComponent Date: Thu, 19 Jun 2025 15:30:38 -0400 Subject: from last --- src/client/views/collections/collectionFreeForm/MarqueeView.tsx | 4 ++-- src/client/views/nodes/ImageBox.tsx | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) (limited to 'src') diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx index 128606675..ff78b332a 100644 --- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx +++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx @@ -589,8 +589,8 @@ export class MarqueeView extends ObservableReactComponent() { } }; - handleSelection = async (selection: string) => (this._searchInput = selection); + handleSelection = (selection: string) => (this._searchInput = selection); drop = undoable( action((e: Event, de: DragManager.DropEvent) => { -- cgit v1.2.3-70-g09d2