aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/client/apis/gpt/GPT.ts15
-rw-r--r--src/client/views/collections/collectionFreeForm/MarqueeView.scss2
-rw-r--r--src/client/views/collections/collectionFreeForm/MarqueeView.tsx191
-rw-r--r--src/client/views/nodes/ImageBox.tsx6
-rw-r--r--src/client/views/nodes/VideoBox.tsx3
-rw-r--r--src/client/views/nodes/scrapbook/ScrapbookBox.tsx165
-rw-r--r--src/client/views/nodes/scrapbook/ScrapbookPicker.scss40
-rw-r--r--src/client/views/nodes/scrapbook/ScrapbookPicker.tsx84
-rw-r--r--src/client/views/nodes/scrapbook/ScrapbookPreset.tsx44
-rw-r--r--src/client/views/nodes/scrapbook/scrapbookleftover.ts46
10 files changed, 474 insertions, 122 deletions
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 <space> 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<SubCollectionViewProps
@observable _labelsVisibile: boolean = false;
@observable _lassoPts: [number, number][] = [];
@observable _lassoFreehand: boolean = false;
+ // ─── New Observables for “Pick 1 of N AI Scrapbook” ───
+ @observable aiChoices: Doc[] = []; // temporary hidden Scrapbook docs
+ @observable pickerX = 0; // popup x coordinate
+ @observable pickerY = 0; // popup y coordinate
+ @observable pickerVisible = false; // show/hide ScrapbookPicker
+ // This will hold the “disposer” function returned by addWindow()
+ private _overlayDisposer: OverlayDisposer | null = null;
+
+
+
@computed get Transform() {
return this._props.getTransform();
@@ -277,7 +295,7 @@ export class MarqueeView extends ObservableReactComponent<SubCollectionViewProps
MarqueeOptionsMenu.Instance.createCollection = this.collection;
MarqueeOptionsMenu.Instance.delete = this.delete;
MarqueeOptionsMenu.Instance.summarize = this.summary;
- MarqueeOptionsMenu.Instance.generateScrapbook = this.generateScrapbook;
+ MarqueeOptionsMenu.Instance.generateScrapbook = this.generateAiScrapbooks;
MarqueeOptionsMenu.Instance.showMarquee = this.showMarquee;
MarqueeOptionsMenu.Instance.hideMarquee = this.hideMarquee;
MarqueeOptionsMenu.Instance.jumpTo(e.clientX, e.clientY);
@@ -521,6 +539,17 @@ export class MarqueeView extends ObservableReactComponent<SubCollectionViewProps
});
+ getAiPresetsDescriptors(): DocumentDescriptor[] {
+ const selected = this.marqueeSelect(false);
+ return selected.map((doc) => ({
+ type: typeof doc.$type === 'string' ? doc.$type : 'UNKNOWN',
+ tags: (() => {
+ const s = new Set<string>();
+ StrListCast(doc.$tags_chat ?? new List<string>()).forEach((t) => s.add(t));
+ return Array.from(s);
+ })(),
+ }));
+ }
generateScrapbook = action(async () => {
@@ -528,28 +557,23 @@ export class MarqueeView extends ObservableReactComponent<SubCollectionViewProps
const selectedDocs = this.marqueeSelect(false);
if (!selectedDocs.length) return;
- const descriptors: DocumentDescriptor[] = selectedDocs.map(doc => ({
- type: typeof doc.$type === 'string' ? doc.$type : 'UNKNOWN',
- tags: (() => {
- const internalTagsSet = new Set<string>();
- StrListCast(doc.$tags_chat ?? new List<string>()).forEach(tag => {
- internalTagsSet.add(tag);
- });
- return Array.from(internalTagsSet);
- })()
- }));
-
+ const descriptors = this.getAiPresetsDescriptors();
+ if (descriptors.length === 0) {
+ alert('No documents selected to generate a scrapbook from!');
+ return;
+ }
const aiPreset = await requestAiGeneratedPreset(descriptors);
if (!aiPreset.length) {
alert("Failed to generate preset");
return;
}
-
+ const scrapbookPlaceholders: Doc[] = buildPlaceholdersFromConfigs(aiPreset);
+ /*
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;
+ placeholderDoc.accepts_tagType = new List<string>(cfg.acceptTags ?? [cfg.tag]);
const placeholder = new Doc();
placeholder.proto = placeholderDoc;
@@ -560,7 +584,7 @@ export class MarqueeView extends ObservableReactComponent<SubCollectionViewProps
if (cfg.height != null) placeholder._height = cfg.height;
return placeholder;
- });
+ });*/
const scrapbook = Docs.Create.ScrapbookDocument(scrapbookPlaceholders, {
backgroundColor: '#e2ad32',
@@ -570,6 +594,25 @@ export class MarqueeView extends ObservableReactComponent<SubCollectionViewProps
_height: 500,
title: 'AI-generated Scrapbook'
});
+
+
+
+ // 3) Now grab that new scrapbook’s flat placeholders
+ const flatPl = DocListCast(scrapbook[Doc.LayoutDataKey(scrapbook)]) as Doc[];
+ const unwrap = (items: Doc[]): Doc[] =>
+ items.flatMap(d =>
+ d.$type === DocumentType.COL
+ ? unwrap(DocListCast(d[Doc.LayoutDataKey(d)]))
+ : [d]
+ );
+ const allPlaceholders = unwrap(flatPl);
+
+ // 4) Slot each selectedDocs[i] into the first matching placeholder
+ selectedDocs.forEach(realDoc => {
+ slotRealDocIntoPlaceholders(realDoc, allPlaceholders
+ );
+ });
+
const selected = this.marqueeSelect(false).map(d => {
this._props.removeDocument?.(d);
d.x = NumCast(d.x) - this.Bounds.left;
@@ -589,6 +632,102 @@ export class MarqueeView extends ObservableReactComponent<SubCollectionViewProps
});
+
+
+ /** Called when the user clicks one of the N thumbnails */
+ @action
+ onScrapbookChoice(idx: number) {
+ const chosenDoc = this.aiChoices[idx];
+ if (!chosenDoc) return;
+
+ // 1) Move chosenDoc from off‐screen to the marquee area
+ const bounds = this.Bounds;
+ chosenDoc.x = bounds.left;
+ chosenDoc.y = bounds.top;
+ chosenDoc._width = NumCast(bounds.width || chosenDoc._width);
+ chosenDoc._height = NumCast(bounds.height || chosenDoc._height);
+ chosenDoc.$title = 'AI‐chosen scrapbook';
+
+ // 2) Remove the other temp docs
+ this.aiChoices.forEach((doc, i) => {
+ if (i !== idx) {
+ this.props.removeDocument?.(doc);
+ }
+ });
+
+ // 3) Clear state and close the popup
+ this.aiChoices = [];
+ this.pickerVisible = false;
+ }
+
+ /** Called when user clicks outside or the “×” in ScrapbookPicker */
+ @action
+ cancelScrapbookChoice() {
+ // 1) Remove all temp scrapbooks
+ this.aiChoices.forEach((doc) => {
+ this.props.removeDocument?.(doc);
+ });
+
+ // 2) Clear array and hide popup
+ this.aiChoices = [];
+ this.pickerVisible = false;
+ }
+ @action
+ generateAiScrapbooks = async () => {
+ const n = 3; // Number of AI scrapbook presets
+ const descriptors = this.getAiPresetsDescriptors();
+ if (descriptors.length === 0) {
+ alert('No documents selected to generate a scrapbook from!');
+ return;
+ }
+
+ // 1) Start N parallel requests for JSON configs
+ const calls: Promise<ScrapbookItemConfig[]>[] = [];
+ for (let i = 0; i < n; i++) {
+ calls.push(requestAiGeneratedPreset(descriptors));
+ }
+
+ // 2) Optionally show a “spinner” overlay in‐line here if you like…
+ // (But for brevity, let’s omit that.)
+
+ try {
+ const allConfigsArrays = await Promise.all(calls);
+
+ runInAction(() => {
+ // 3) For each returned config array, build placeholders and create _hidden_ Scrapbook doc
+ this.aiChoices = allConfigsArrays.map((cfgArr, i) => {
+ const placeholders = buildPlaceholdersFromConfigs(cfgArr);
+ // Off‐screen ScrapbookDocument:
+ const tempSb: Doc = Docs.Create.ScrapbookDocument(placeholders, {
+ title: `AI Preset (temp ${i + 1})`,
+ x: -10000, // far off screen
+ y: -10000,
+ _width: 600,
+ _height: 600,
+ backgroundColor: 'transparent',
+ _layout_showFlash: false, // keep it hidden until chosen
+ });
+ this.props.addDocument?.(tempSb);
+ tempSb.$tags_chat = new List<string>(['@ai_preset']);
+ return tempSb;
+ });
+
+ // 4) Compute where to show the popup in screen coords
+ const bounds = this.Bounds;
+ const screenTopLeft = this._props
+ .getContainerTransform()
+ .transformPoint(bounds.left, bounds.top);
+ this.pickerX = screenTopLeft[0] + 20;
+ this.pickerY = screenTopLeft[1] + 20;
+ this.pickerVisible = true;
+ });
+ } catch (err) {
+ console.error('Error generating AI scrapbooks:', err);
+ alert('Failed to generate presets. Please try again.');
+ }
+ };
+
+
@action
marqueeCommand = (e: KeyboardEvent) => {
const ee = e as unknown as KeyboardEvent & { propagationIsStopped?: boolean };
@@ -755,6 +894,7 @@ export class MarqueeView extends ObservableReactComponent<SubCollectionViewProps
};
render() {
return (
+ <>
<div
className="marqueeView"
ref={r => {
@@ -774,6 +914,27 @@ export class MarqueeView extends ObservableReactComponent<SubCollectionViewProps
{this._visible ? this.marqueeDiv : null}
{this.props.children}
</div>
+ {this.pickerVisible && (
+ <div
+ className="marqueeView-scrapbook-overlay"
+ style={{
+ position: 'absolute',
+ top: this.pickerY,
+ left: this.pickerX,
+ zIndex: 2000, // just ensure it floats above
+ }}
+ >
+ <ScrapbookPicker
+ choices={this.aiChoices}
+ x={this.pickerX}
+ y={this.pickerY}
+ onSelect={(i) => this.onScrapbookChoice(i)}
+ onCancel={() => this.cancelScrapbookChoice()}
+ />
+ </div>
+)}
+ </>
+
);
}
}
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index 9067f7e0c..2473f1c0a 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -141,6 +141,11 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
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()
@@ -152,6 +157,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
this.Document.$tags_chat = new List<string>();
tokens.forEach(tok => {
(this.Document.$tags_chat as List<string>).push(tok)});
+ (this.Document.$tags_chat as List<string>).push(`ASPECT_${aspectRatio}`);
//!!! changed may 11 (this.Document.$tags_chat as List<string>).push(label);
// 6) flip on “show tags” in the layout
diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx
index 0e7afbab1..404be6a1b 100644
--- a/src/client/views/nodes/VideoBox.tsx
+++ b/src/client/views/nodes/VideoBox.tsx
@@ -151,7 +151,8 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
const tokens = label.split(/\s+/);
this.Document.$tags_chat = new List<string>();
tokens.forEach(tok => (this.Document.$tags_chat as List<string>).push(tok));
-
+ const aspect = this.player!.videoWidth / (this.player!.videoHeight || 1);
+ (this.Document.$tags_chat as List<string>).push(`ASPECT_${aspect}`);
// 5) Turn on tag display in layout
this.Document._layout_showTags = true;
diff --git a/src/client/views/nodes/scrapbook/ScrapbookBox.tsx b/src/client/views/nodes/scrapbook/ScrapbookBox.tsx
index 5dd02295c..eb997024b 100644
--- a/src/client/views/nodes/scrapbook/ScrapbookBox.tsx
+++ b/src/client/views/nodes/scrapbook/ScrapbookBox.tsx
@@ -3,7 +3,6 @@ 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';
@@ -35,54 +34,51 @@ export function buildPlaceholdersFromConfigs(configs: ScrapbookItemConfig[]): Do
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;
+ doc.accepts_tagType = new List<string>(child.acceptTags ?? [child.tag]);
const ph = new Doc();
- ph.proto = doc;
+ ph.proto = doc;
ph.original = doc;
- ph.x = child.x;
- ph.y = child.y;
- if (child.width != null) ph._width = child.width;
+ 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
- }
- );
+ // Create a stacking document with the child placeholders
+ const containerProto = Docs.Create.StackingDocument(childDocs, {
+ ...(protoW != null ? { _width: protoW } : {}),
+ ...(protoH != null ? { _height: protoH } : {}),
+ title: cfg.tag,
+ });
const ph = new Doc();
- ph.proto = containerProto;
+ ph.proto = containerProto;
ph.original = containerProto;
- ph.x = cfg.x;
- ph.y = cfg.y;
- if (cfg.width != null) ph._width = cfg.width;
+ 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 ---
+ }
+
+ else {
const doc = Docs.Create.TextDocument("[placeholder] " + cfg.tag);
doc.accepts_docType = cfg.type;
- doc.accepts_tagType = cfg.acceptTag ?? cfg.tag;
+ doc.accepts_tagType = new List<string>(cfg.acceptTags ?? [cfg.tag]);
const ph = new Doc();
- ph.proto = doc;
+ ph.proto = doc;
ph.original = doc;
- ph.x = cfg.x;
- ph.y = cfg.y;
- if (cfg.width != null) ph._width = cfg.width;
+ 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);
}
@@ -90,7 +86,54 @@ export function buildPlaceholdersFromConfigs(configs: ScrapbookItemConfig[]): Do
return placeholders;
}
-// Scrapbook view: a container that lays out its child items in a grid/template
+export function slotRealDocIntoPlaceholders(
+ realDoc: Doc,
+ placeholders: Doc[]
+): boolean {
+ const realTags = new Set<string>(
+ StrListCast(realDoc.$tags_chat ?? new List<string>())
+ .map(t => t.toLowerCase())
+);
+
+ // Find placeholder with most matching tags
+ let bestMatch: Doc | null = null;
+ let maxMatches = 0;
+/*
+ (d.accepts_docType === docs[0].$type || // match fields based on type, or by analyzing content .. simple example of matching text in placeholder to dropped doc's type
+ RTFCast(d[Doc.LayoutDataKey(d)])?.Text.includes(StrCast(docs[0].$type)))*/
+
+ placeholders.forEach(ph => {
+ // 1) Enforce that placeholder.accepts_docType === realDoc.$type
+ if (ph.accepts_docType !== realDoc.$type) {
+ // Skip this placeholder entirely if types do not match.
+ return;
+ };
+ const phTagTypes = StrListCast(ph.accepts_tagType ?? new List<string>())
+ .map(t => t.toLowerCase());
+ console.log({ realTags, phTagTypes });
+ const matches = phTagTypes.filter(tag => realTags.has(tag));
+
+ if (matches.length > maxMatches) {
+ maxMatches = matches.length;
+ bestMatch = ph;
+ }
+
+ });
+
+ if (bestMatch && maxMatches > 0) {
+ setTimeout(
+ undoable(() => {
+ bestMatch!.proto = realDoc;
+ }, 'Scrapbook add'),
+ 0
+ );
+ return true;
+ }
+
+ return false;
+}
+
+// Scrapbook view: a container that lays out its child items in a template
@observer
export class ScrapbookBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
@observable selectedPreset = getPresetNames()[0];
@@ -406,56 +449,19 @@ export class ScrapbookBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
// 3) produce a flat list of every doc, unwrapping any number of nested COLs
const allDocs: Doc[] = unwrap(slots);
-
+ if (docs?.length === 1) {
+ return slotRealDocIntoPlaceholders(
+ docs[0],
+ allDocs,
+ )
+ ? false
+ : false;
+ }
- if (docs?.length === 1) {
- const placeholder = allDocs.filter(d =>
+ return false;
+};
+
- (d.accepts_docType === docs[0].$type || // match fields based on type, or by analyzing content .. simple example of matching text in placeholder to dropped doc's type
- RTFCast(d[Doc.LayoutDataKey(d)])?.Text.includes(StrCast(docs[0].$type)))
- ); // prettier-ignore
-
- //DocListCast(this.Document.items).map(doc => DocListCast(doc[Doc.LayoutDataKey(doc)])
-
- if (placeholder) {
- /**Look at the autotags and see what matches*RTFCast(d[Doc.LayoutDataKey(d)])?.Text*/
- // ugh. we have to tell the underlying view not to add the Doc so that we can add it where we want it.
- // However, returning 'false' triggers an undo. so this settimeout is needed to make the assignment happen after the undo.
- setTimeout(
- undoable(() => {
-
- const slotTagsList: Set<string>[] = placeholder.map(doc =>
- new Set<string>(StrListCast(doc.$tags_chat))
- );
- // turn docs[0].$tags_chat into a Set
- const targetTags = new Set(StrListCast(docs[0].$tags_chat));
-
- //StrListCast(placeholder.overrideFields).map(field => (docs[0][field] = placeholder[field])); // // shouldn't need to do this for layout fields since the placeholder already overrides its protos
-
- // find the first placeholder that shares *any* tag
- const match = placeholder.find(ph =>
- ph.accepts_tagType != null && // make sure it actually has one
- targetTags.has(StrCast(ph.accepts_tagType)) // test membership in the Set
- //StrListCast(ph.$tags_chat).some(tag => targetTags.has(tag))
- );
- if (match) {
- match.proto = docs[0];
- }
-
- /*const chosenPlaceholder = placeholder.find(d =>
- pl = new Set<string>(StrListCast(d.$tags_chat)
-
- d.$tags_chat && d.$tags_chat[0].equals(docs[0].$tags_chat)); //why [0]
- if (chosenPlaceholder){
- chosenPlaceholder.proto = docs[0];}*/
- //excess if statement??
- }, 'Scrapbook add')
- );
- return false;
- }
- }
- return false;
- };
@@ -474,10 +480,15 @@ export class ScrapbookBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
allDocs.forEach(doc => {
const tags = StrListCast(doc.$tags_chat ?? new List<string>());
- tags.forEach(tag => internalTagsSet.add(tag));
+ tags.forEach(tag =>
+ {if (!tag.startsWith("ASPECT_")) {
+ internalTagsSet.add(tag);
+ }
+ });
});
const internalTags = Array.from(internalTagsSet).join(', ');
+
return internalTags
? `Create a new scrapbook background featuring: ${internalTags}`
diff --git a/src/client/views/nodes/scrapbook/ScrapbookPicker.scss b/src/client/views/nodes/scrapbook/ScrapbookPicker.scss
new file mode 100644
index 000000000..237274433
--- /dev/null
+++ b/src/client/views/nodes/scrapbook/ScrapbookPicker.scss
@@ -0,0 +1,40 @@
+/* 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
new file mode 100644
index 000000000..6054cb98d
--- /dev/null
+++ b/src/client/views/nodes/scrapbook/ScrapbookPicker.tsx
@@ -0,0 +1,84 @@
+// 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<ScrapbookPickerProps> {
+ containerRef = React.createRef<HTMLDivElement>();
+
+ // 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 (
+ <div
+ className="scrapbook-picker-popup"
+ ref={this.containerRef}
+ style={{
+ top: `${y}px`,
+ left: `${x}px`,
+ }}
+ >
+ {/* close icon */}
+ <div className="scrapbook-picker-close" onClick={onCancel}>
+ ×
+ </div>
+ <div className="scrapbook-picker-thumbnails">
+ {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 (
+ <div
+ key={`${doc[Id]}_${i}`}
+ className="scrapbook-picker-thumb"
+ onClick={() => 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.
+ */}
+ <div className="scrapbook-picker-thumb-inner">
+ {StrCast(doc.title)}
+ </div>
+ </div>
+ );
+ })}
+ </div>
+ </div>
+ );
+ }
+}
diff --git a/src/client/views/nodes/scrapbook/ScrapbookPreset.tsx b/src/client/views/nodes/scrapbook/ScrapbookPreset.tsx
index 87821c7bf..96a8e9b5f 100644
--- a/src/client/views/nodes/scrapbook/ScrapbookPreset.tsx
+++ b/src/client/views/nodes/scrapbook/ScrapbookPreset.tsx
@@ -13,7 +13,7 @@ export interface ScrapbookItemConfig {
/** text shown in the placeholder bubble */
tag: string;
/** what this slot actually accepts (defaults to `tag`) */
- acceptTag?: string;
+ acceptTags?: string[];
x: number;
y: number;
@@ -48,22 +48,22 @@ export class ScrapbookPreset {
return [
{ type: DocumentType.IMG,
tag: '[placeholder] LANDSCAPE',
- acceptTag: 'LANDSCAPE',
+ acceptTags: ['LANDSCAPE'],
x: 0, y: -100, width: 250, height: 200
},
{ type: DocumentType.RTF,
tag: '[placeholder] caption',
- acceptTag: 'caption',
+ acceptTags: ['caption'],
x: 0, y: 200, width: 250, height: 50
},
{ type: DocumentType.RTF,
tag: '[placeholder] lengthy description',
- acceptTag: 'lengthy description',
+ acceptTags: ['lengthy description'],
x: 280, y: -50, width: 50, height: 200
},
{ type: DocumentType.IMG,
tag: '[placeholder] PERSON',
- acceptTag: 'PERSON',
+ acceptTags: ['PERSON'],
x: -200, y: -100, width: 100, height: 200
},
];
@@ -71,12 +71,12 @@ 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 },
+ { 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 },
];
}
@@ -85,17 +85,17 @@ export class ScrapbookPreset {
return [
{ type: DocumentType.IMG,
tag: 'image',
- acceptTag: 'LANDSCAPE',
+ acceptTags: ['LANDSCAPE'],
x: 0, y: -100, width: 250, height: 200
},
{ type: DocumentType.RTF,
tag: 'summary',
- acceptTag: 'caption',
+ acceptTags: ['caption'],
x: 0, y: 200, width: 250
},
{ type: DocumentType.RTF,
tag: 'sidebar',
- acceptTag: 'lengthy description',
+ acceptTags: ['lengthy description'],
x: 280, y: -50, width: 50, height: 200
},
{
@@ -106,7 +106,7 @@ export class ScrapbookPreset {
children: [
{ type: DocumentType.IMG,
tag: 'image internal',
- acceptTag: 'PERSON',
+ acceptTags: ['PERSON'],
x: 0, y: 0, width: 50, height: 100
}
]
@@ -118,22 +118,22 @@ export class ScrapbookPreset {
return [
{ type: DocumentType.IMG,
tag: 'LANDSCAPE',
- acceptTag: 'LANDSCAPE',
+ acceptTags: ['LANDSCAPE'],
x: -150, y: -150, width: 150, height: 150
},
{ type: DocumentType.IMG,
tag: 'PERSON',
- acceptTag: 'PERSON',
+ acceptTags: ['PERSON'],
x: 0, y: -150, width: 150, height: 150
},
{ type: DocumentType.RTF,
tag: 'caption',
- acceptTag: 'caption',
+ acceptTags: ['caption'],
x: -150, y: 0, width: 300, height: 100
},
{ type: DocumentType.RTF,
tag: 'lengthy description',
- acceptTag: 'lengthy description',
+ acceptTags: ['lengthy description'],
x: 0, y: 100, width: 300, height: 100
}
];
@@ -143,17 +143,17 @@ export class ScrapbookPreset {
return [
{ type: DocumentType.RTF,
tag: 'title',
- acceptTag: 'title',
+ acceptTags: ['title'],
x: 0, y: -180, width: 300, height: 40
},
{ type: DocumentType.IMG,
tag: 'LANDSCAPE',
- acceptTag: 'LANDSCAPE',
+ acceptTags: ['LANDSCAPE'],
x: 0, y: 0, width: 300, height: 200
},
{ type: DocumentType.RTF,
tag: 'caption',
- acceptTag: 'caption',
+ acceptTags: ['caption'],
x: 0, y: 230, width: 300, height: 50
}
];
diff --git a/src/client/views/nodes/scrapbook/scrapbookleftover.ts b/src/client/views/nodes/scrapbook/scrapbookleftover.ts
new file mode 100644
index 000000000..2f381ab95
--- /dev/null
+++ b/src/client/views/nodes/scrapbook/scrapbookleftover.ts
@@ -0,0 +1,46 @@
+
+
+
+
+ //DocListCast(this.Document.items).map(doc => DocListCast(doc[Doc.LayoutDataKey(doc)])
+
+ if (placeholder) {
+ /**Look at the autotags and see what matches*RTFCast(d[Doc.LayoutDataKey(d)])?.Text*/
+ // ugh. we have to tell the underlying view not to add the Doc so that we can add it where we want it.
+ // However, returning 'false' triggers an undo. so this settimeout is needed to make the assignment happen after the undo.
+ setTimeout(
+ undoable(() => {
+
+ const slotTagsList: Set<string>[] = placeholder.map(doc =>
+ new Set<string>(StrListCast(doc.$tags_chat))
+ );
+ // turn docs[0].$tags_chat into a Set
+ const targetTags = new Set(StrListCast(docs[0].$tags_chat));
+
+ //StrListCast(placeholder.overrideFields).map(field => (docs[0][field] = placeholder[field]));
+ // // // shouldn't need to do this for layout fields since the placeholder already overrides its protos
+
+ // find the first placeholder that shares *any* tag
+ const match = placeholder.find(ph =>
+ ph.accepts_tagType != null && // make sure it actually has one
+ targetTags.has(StrCast(ph.accepts_tagType)) // test membership in the Set
+ //StrListCast(ph.$tags_chat).some(tag => targetTags.has(tag))
+ );
+ if (match) {
+ match.proto = docs[0];
+ }
+
+ /*const chosenPlaceholder = placeholder.find(d =>
+ pl = new Set<string>(StrListCast(d.$tags_chat)
+
+ d.$tags_chat && d.$tags_chat[0].equals(docs[0].$tags_chat)); //why [0]
+ if (chosenPlaceholder){
+ chosenPlaceholder.proto = docs[0];}*/
+ //excess if statement??
+ }, 'Scrapbook add')
+ );
+ return false;
+ }
+ }
+ return false;
+ }; \ No newline at end of file