aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/client/apis/gpt/GPT.ts57
-rw-r--r--src/client/documents/DocumentTypes.ts3
-rw-r--r--src/client/documents/Documents.ts8
-rw-r--r--src/client/util/CurrentUserUtils.ts39
-rw-r--r--src/client/views/MainView.tsx5
-rw-r--r--src/client/views/collections/CollectionCardDeckView.scss84
-rw-r--r--src/client/views/collections/CollectionCardDeckView.tsx513
-rw-r--r--src/client/views/collections/CollectionView.tsx2
-rw-r--r--src/client/views/global/globalScripts.ts131
-rw-r--r--src/client/views/pdf/GPTPopup/GPTPopup.scss39
-rw-r--r--src/client/views/pdf/GPTPopup/GPTPopup.tsx101
11 files changed, 964 insertions, 18 deletions
diff --git a/src/client/apis/gpt/GPT.ts b/src/client/apis/gpt/GPT.ts
index 078ac3e55..6857246da 100644
--- a/src/client/apis/gpt/GPT.ts
+++ b/src/client/apis/gpt/GPT.ts
@@ -5,6 +5,8 @@ enum GPTCallType {
SUMMARY = 'summary',
COMPLETION = 'completion',
EDIT = 'edit',
+ SORT = 'sort',
+ DESCRIBE = 'describe',
MERMAID = 'mermaid',
DATA = 'data',
}
@@ -16,10 +18,6 @@ type GPTCallOpts = {
prompt: string;
};
-/**
- * Replace completions (deprecated) with chat
- */
-
const callTypeMap: { [type: string]: GPTCallOpts } = {
// newest model: gpt-4
summary: { model: 'gpt-3.5-turbo', maxTokens: 256, temp: 0.5, prompt: 'Summarize the text given in simpler terms.' },
@@ -37,6 +35,13 @@ const callTypeMap: { [type: string]: 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: {
+ model: 'gpt-4o',
+ maxTokens: 2048,
+ temp: 0.5,
+ prompt: "I'm going to give you a list of descriptions. Each one is seperated by ====== on either side. They will vary in length, so make sure to only seperate when you see ======. Sort them into lists by shared content. MAKE SURE EACH DESCRIPTOR IS IN ONLY ONE LIST. Generate only the list with each list seperated by ====== with the elements seperated by ~~~~~~. Try to do around 4 groups, but a little more or less is ok.",
+ },
+ describe: { model: 'gpt-4-vision-preview', maxTokens: 2048, temp: 0, prompt: 'Describe these images in 3-5 words' },
};
let lastCall = '';
@@ -53,7 +58,7 @@ const gptAPICall = async (inputTextIn: string, callType: GPTCallType, prompt?: a
if (lastCall === inputText) return lastResp;
try {
const configuration: ClientOptions = {
- apiKey: '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',
+ apiKey: process.env.OPENAI_KEY,
dangerouslyAllowBrowser: true,
};
lastCall = inputText;
@@ -69,6 +74,7 @@ const gptAPICall = async (inputTextIn: string, callType: GPTCallType, prompt?: a
model: opts.model,
messages: messages,
temperature: opts.temp,
+ max_tokens: opts.maxTokens,
});
lastResp = response.choices[0].message.content ?? '';
return lastResp;
@@ -78,6 +84,42 @@ const gptAPICall = async (inputTextIn: string, callType: GPTCallType, prompt?: a
}
};
+const gptImageLabel = async (imgUrl: string): Promise<string> => {
+ try {
+ const configuration: ClientOptions = {
+ apiKey: process.env.OPENAI_KEY,
+ dangerouslyAllowBrowser: true,
+ };
+
+ const openai = new OpenAI(configuration);
+ const response = await openai.chat.completions.create({
+ model: 'gpt-4o',
+ messages: [
+ {
+ role: 'user',
+ content: [
+ { type: 'text', text: 'Describe this image in 3-5 words' },
+ {
+ type: 'image_url',
+ image_url: {
+ url: `${imgUrl}`,
+ },
+ },
+ ],
+ },
+ ],
+ });
+ if (response.choices[0].message.content) {
+ return response.choices[0].message.content;
+ } else {
+ return ':(';
+ }
+ } catch (err) {
+ console.log(err);
+ return 'Error connecting with API';
+ }
+};
+
const gptImageCall = async (prompt: string, n?: number) => {
try {
const configuration: ClientOptions = {
@@ -91,11 +133,12 @@ const gptImageCall = async (prompt: string, n?: number) => {
n: n ?? 1,
size: '1024x1024',
});
- return response.data.map(data => data.url);
+ return response.data.map((data: any) => data.url);
+ // return response.data.data[0].url;
} catch (err) {
console.error(err);
}
return undefined;
};
-export { gptAPICall, gptImageCall, GPTCallType };
+export { gptAPICall, gptImageCall, gptImageLabel, GPTCallType };
diff --git a/src/client/documents/DocumentTypes.ts b/src/client/documents/DocumentTypes.ts
index 53ee87908..8f95068db 100644
--- a/src/client/documents/DocumentTypes.ts
+++ b/src/client/documents/DocumentTypes.ts
@@ -12,7 +12,7 @@ export enum DocumentType {
REC = 'recording',
PDF = 'pdf',
INK = 'ink',
- DIAGRAM='diagram',
+ DIAGRAM = 'diagram',
SCREENSHOT = 'screenshot',
FONTICON = 'fonticonbox',
SEARCH = 'search', // search query
@@ -62,4 +62,5 @@ export enum CollectionViewType {
StackedTimeline = 'stacked timeline',
NoteTaking = 'notetaking',
Calendar = 'calendar',
+ Card = 'card',
}
diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index 19b8448f1..689439432 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -476,6 +476,10 @@ export class DocumentOptions {
hoverBackgroundColor?: string; // background color of a label when hovered
userBackgroundColor?: STRt = new StrInfo('background color associated with a Dash user (seen in header fields of shared documents)');
userColor?: STRt = new StrInfo('color associated with a Dash user (seen in header fields of shared documents)');
+
+ cardSort?: STRt = new StrInfo('way cards are sorted in deck view');
+ cardSort_customField?: STRt = new StrInfo('field key used for sorting cards');
+ cardSort_visibleSortGroups?: List<number>; // which sorting values are being filtered (shown)
}
export const DocOptions = new DocumentOptions();
@@ -942,6 +946,10 @@ export namespace Docs {
return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { ...options, _type_collection: CollectionViewType.Carousel3D });
}
+ export function CardDeckDocument(documents: Array<Doc>, options: DocumentOptions, id?: string) {
+ return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { ...options, _type_collection: CollectionViewType.Card });
+ }
+
export function SchemaDocument(schemaHeaders: SchemaHeaderField[], documents: Array<Doc>, options: DocumentOptions) {
return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { schemaHeaders: new List(schemaHeaders), ...options, _type_collection: CollectionViewType.Schema });
}
diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts
index 92bfefc5f..4fd6df799 100644
--- a/src/client/util/CurrentUserUtils.ts
+++ b/src/client/util/CurrentUserUtils.ts
@@ -658,12 +658,45 @@ pie title Minerals in my tap water
{ title: "Center", icon: "align-center", toolTip: "Center Align Stack", btnType: ButtonType.ToggleButton, ignoreClick: true, expertMode: false, toolType:"center", funcs: {}, scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}'}}, // Only when floating document is selected in freeform
]
}
+ static cardTools(): Button[] {
+ return [
+ { title: "Time", icon:"hourglass-half", toolTip:"Sort by most recent document creation", btnType: ButtonType.ToggleButton, expertMode: false, toolType:"time", funcs: {}, scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}'}},
+ { title: "Type", icon:"eye", toolTip:"Sort by document type", btnType: ButtonType.ToggleButton, expertMode: false, toolType:"docType",funcs: {}, scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}'}},
+ { title: "Color", icon:"palette", toolTip:"Sort by document color", btnType: ButtonType.ToggleButton, expertMode: false, toolType:"color", funcs: {}, scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}'}},
+ ]
+ }
+ static labelTools(): Button[] {
+ return [
+ { title: "AI", icon:"robot", toolTip:"Add AI labels", btnType: ButtonType.ToggleButton, expertMode: false, toolType:"chat", funcs: {hidden:`showFreeform ("chat", true)`},scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}'}},
+ { title: "AIs", icon:"AI Sort", toolTip:"Filter AI labels", subMenu: this.cardGroupTools("chat"), expertMode: false, toolType:CollectionViewType.Card, funcs: {hidden:`!showFreeform("chat", true)`, linearView_IsOpen: `SelectionManager_selectedDocType(this.toolType, this.expertMode)`} },
+ { title: "Like", icon:"heart", toolTip:"Add Like labels", btnType: ButtonType.ToggleButton, expertMode: false, toolType:"like", funcs: {hidden:`showFreeform ("like", true)`},scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}'}},
+ { title: "Likes", icon:"Likes", toolTip:"Filter likes", width: 10, subMenu: this.cardGroupTools("heart"), expertMode: false, toolType:CollectionViewType.Card, funcs: {hidden:`!showFreeform("like", true)`, linearView_IsOpen: `SelectionManager_selectedDocType(this.toolType, this.expertMode)`} },
+ { title: "Star", icon:"star", toolTip:"Add Star labels", btnType: ButtonType.ToggleButton, expertMode: false, toolType:"star", funcs: {hidden:`showFreeform ("star", true)`},scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}'}},
+ { title: "Stars", icon:"Stars", toolTip:"Filter stars", width: 80, subMenu: this.cardGroupTools("star"), expertMode: false, toolType:CollectionViewType.Card, funcs: {hidden:`!showFreeform("star", true)`, linearView_IsOpen: `SelectionManager_selectedDocType(this.toolType, this.expertMode)`} },
+ { title: "Idea", icon:"satellite", toolTip:"Add Idea labels", btnType: ButtonType.ToggleButton, expertMode: false, toolType:"idea", funcs: {hidden:`showFreeform ("idea", true)`},scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}'}},
+ { title: "Ideas", icon:"Ideas", toolTip:"Filter ideas", width: 80, subMenu: this.cardGroupTools("satellite"), expertMode: false, toolType:CollectionViewType.Card, funcs: {hidden:`!showFreeform("idea", true)`, linearView_IsOpen: `SelectionManager_selectedDocType(this.toolType, this.expertMode)`} },
+ ]
+ }
+ static cardGroupTools(icon: string): Button[] {
+ return [
+ { title: "1", icon, toolTip:"Click to toggle visibility", btnType: ButtonType.ToggleButton, expertMode: false, toolType:"1", funcs: {hidden:`!cardHasLabel(this.toolType)`}, scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}'}},
+ { title: "2", icon, toolTip:"Click to toggle visibility", btnType: ButtonType.ToggleButton, expertMode: false, toolType:"2", funcs: {hidden:`!cardHasLabel(this.toolType)`}, scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}'}},
+ { title: "3", icon, toolTip:"Click to toggle visibility", btnType: ButtonType.ToggleButton, expertMode: false, toolType:"3", funcs: {hidden:`!cardHasLabel(this.toolType)`}, scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}'}},
+ { title: "4", icon, toolTip:"Click to toggle visibility", btnType: ButtonType.ToggleButton, expertMode: false, toolType:"4", funcs: {hidden:`!cardHasLabel(this.toolType)`}, scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}'}},
+ { title: "5", icon, toolTip:"Click to toggle visibility", btnType: ButtonType.ToggleButton, expertMode: false, toolType:"5", funcs: {hidden:`!cardHasLabel(this.toolType)`}, scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}'}},
+ { title: "6", icon, toolTip:"Click to toggle visibility", btnType: ButtonType.ToggleButton, expertMode: false, toolType:"6", funcs: {hidden:`!cardHasLabel(this.toolType)`}, scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}'}},
+ { title: "7", icon, toolTip:"Click to toggle visibility", btnType: ButtonType.ToggleButton, expertMode: false, toolType:"7", funcs: {hidden:`!cardHasLabel(this.toolType)`}, scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}'}},
+ ]
+ }
static viewTools(): Button[] {
return [
{ title: "Snap", icon: "th", toolTip: "Show Snap Lines", btnType: ButtonType.ToggleButton, ignoreClick: true, expertMode: false, toolType:"snaplines", funcs: {}, scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}'}}, // Only when floating document is selected in freeform
{ title: "Grid", icon: "border-all", toolTip: "Show Grid", btnType: ButtonType.ToggleButton, ignoreClick: true, expertMode: false, toolType:"grid", funcs: {}, scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}'}}, // Only when floating document is selected in freeform
{ title: "Fit All", icon: "object-group", toolTip: "Fit Docs to View (double click to make sticky)",btnType: ButtonType.ToggleButton, ignoreClick:true, expertMode: false, toolType:"viewAll", funcs: {}, scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}', onDoubleClick: '{ return showFreeform(this.toolType, _readOnly_, true);}'}}, // Only when floating document is selected in freeform
{ title: "Clusters", icon: "braille", toolTip: "Show Doc Clusters", btnType: ButtonType.ToggleButton, ignoreClick: true, expertMode: false, toolType:"clusters", funcs: {}, scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}'}}, // Only when floating document is selected in freeform
+ { title: "Cards", icon: "brain", toolTip: "Flashcards", btnType: ButtonType.ToggleButton, ignoreClick: true, expertMode: false, toolType:"flashcards", funcs: {}, scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}'}}, // Only when floating document is selected in freeform
+ { title: "Arrange", icon:"arrow-down-short-wide",toolTip:"Toggle Auto Arrange", btnType: ButtonType.ToggleButton, ignoreClick: true, expertMode: false, toolType:"arrange", funcs: {hidden: 'IsNoviceMode()'}, scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}'}}, // Only when floating document is selected in freeform
+
]
}
static textTools():Button[] {
@@ -739,8 +772,8 @@ pie title Minerals in my tap water
{ btnList: new List<string>([CollectionViewType.Freeform, CollectionViewType.Schema, CollectionViewType.Tree,
CollectionViewType.Stacking, CollectionViewType.Masonry, CollectionViewType.Multicolumn,
CollectionViewType.Multirow, CollectionViewType.Time, CollectionViewType.Carousel,
- CollectionViewType.Carousel3D, CollectionViewType.Linear, CollectionViewType.Map,
- CollectionViewType.Grid, CollectionViewType.NoteTaking]),
+ CollectionViewType.Carousel3D, CollectionViewType.Card, CollectionViewType.Linear, CollectionViewType.Map,
+ CollectionViewType.Grid, CollectionViewType.NoteTaking, ]),
title: "Perspective", toolTip: "View", btnType: ButtonType.DropdownList, ignoreClick: true, width: 100, scripts: { script: '{ return setView(value, _readOnly_); }'}},
{ title: "Pin", icon: "map-pin", toolTip: "Pin View to Trail", btnType: ButtonType.ClickButton, expertMode: false, width: 30, scripts: { onClick: 'pinWithView(altKey)'}, funcs: {hidden: "IsNoneSelected()"}},
{ title: "Header", icon: "heading", toolTip: "Doc Titlebar Color", btnType: ButtonType.ColorButton, expertMode: false, ignoreClick: true, scripts: { script: 'return setHeaderColor(value, _readOnly_)'} },
@@ -755,6 +788,8 @@ pie title Minerals in my tap water
{ title: "Doc", icon: "Doc", toolTip: "Freeform Doc tools", subMenu: CurrentUserUtils.freeTools(), expertMode: false, toolType:CollectionViewType.Freeform, funcs: {hidden: `!SelectedDocType(this.toolType, this.expertMode, true)`, linearView_IsOpen: `SelectedDocType(this.toolType, this.expertMode)`} }, // Always available
{ title: "View", icon: "View", toolTip: "View tools", subMenu: CurrentUserUtils.viewTools(), expertMode: false, toolType:CollectionViewType.Freeform, funcs: {hidden: `!SelectedDocType(this.toolType, this.expertMode)`, linearView_IsOpen: `SelectedDocType(this.toolType, this.expertMode)`} }, // Always available
{ title: "Stack", icon: "View", toolTip: "Stacking tools", subMenu: CurrentUserUtils.stackTools(), expertMode: false, toolType:CollectionViewType.Stacking, funcs: {hidden: `!SelectedDocType(this.toolType, this.expertMode)`, linearView_IsOpen: `SelectedDocType(this.toolType, this.expertMode)`} }, // Always available
+ { title: "Card", icon: "Sort", toolTip: "Card sort", subMenu: CurrentUserUtils.cardTools(), expertMode: false, toolType:CollectionViewType.Card, funcs: {hidden: `!SelectedDocType(this.toolType, this.expertMode)`, linearView_IsOpen: `SelectedDocType(this.toolType, this.expertMode)`} }, // Always available
+ { title: "Label", icon: "Label", toolTip: "Assign card labels", subMenu: CurrentUserUtils.labelTools(), expertMode: false, toolType:CollectionViewType.Card, funcs: {hidden: `!SelectedDocType(this.toolType, this.expertMode)`, linearView_IsOpen: `SelectedDocType(this.toolType, this.expertMode)`} }, // Always available
{ title: "Web", icon: "Web", toolTip: "Web functions", subMenu: CurrentUserUtils.webTools(), expertMode: false, toolType:DocumentType.WEB, funcs: {hidden: `!SelectedDocType(this.toolType, this.expertMode)`, linearView_IsOpen: `SelectedDocType(this.toolType, this.expertMode)`} }, // Only when Web is selected
{ title: "Video", icon: "Video", toolTip: "Video functions", subMenu: CurrentUserUtils.videoTools(), expertMode: false, toolType:DocumentType.VID, funcs: {hidden: `!SelectedDocType(this.toolType, this.expertMode)`, linearView_IsOpen: `SelectedDocType(this.toolType, this.expertMode)`} }, // Only when video is selected
{ title: "Image", icon: "Image", toolTip: "Image functions", subMenu: CurrentUserUtils.imageTools(), expertMode: false, toolType:DocumentType.IMG, funcs: {hidden: `!SelectedDocType(this.toolType, this.expertMode)`, linearView_IsOpen: `SelectedDocType(this.toolType, this.expertMode)`} }, // Only when image is selected
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index e4a18dcea..33c343176 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -538,6 +538,11 @@ export class MainView extends ObservableReactComponent<{}> {
fa.faZ,
fa.faArrowsUpToLine,
fa.faArrowsDownToLine,
+ fa.faPalette,
+ fa.faHourglassHalf,
+ fa.faRobot,
+ fa.faSatellite,
+ fa.faStar
]
);
}
diff --git a/src/client/views/collections/CollectionCardDeckView.scss b/src/client/views/collections/CollectionCardDeckView.scss
new file mode 100644
index 000000000..a089b248d
--- /dev/null
+++ b/src/client/views/collections/CollectionCardDeckView.scss
@@ -0,0 +1,84 @@
+@import '../global/globalCssVariables.module.scss';
+
+.collectionCardView-outer {
+ height: 100%;
+ width: 100%;
+ position: relative;
+ background-color: white;
+ overflow: hidden;
+}
+
+.card-wrapper {
+ display: grid;
+ grid-template-columns: repeat(10, 1fr);
+ // width: 100%;
+ transform-origin: top left;
+
+ position: absolute;
+ align-items: center;
+ justify-items: center;
+ justify-content: center;
+
+ transition: transform 0.3s cubic-bezier(0.455, 0.03, 0.515, 0.955);
+}
+
+.card-button-container {
+ display: flex;
+ padding: 3px;
+ // width: 300px;
+ background-color: rgb(218, 218, 218); /* Background color of the container */
+ border-radius: 50px; /* Rounds the corners of the container */
+ transform: translateY(75px);
+ // box-shadow: 0 4px 8px rgba(0,0,0,0.1); /* Optional: Adds shadow for depth */
+ align-items: center; /* Centers buttons vertically */
+ justify-content: start; /* Centers buttons horizontally */
+}
+
+button {
+ width: 35px;
+ height: 35px;
+ border-radius: 50%;
+ background-color: $dark-gray;
+ // border-color: $medium-blue;
+ margin: 5px; // transform: translateY(-50px);
+}
+
+// button:hover {
+// transform: translateY(-50px);
+// }
+
+// .card-wrapper::after {
+// content: "";
+// width: 100%; /* Forces wrapping */
+// }
+
+// .card-wrapper > .card-item:nth-child(10n)::after {
+// content: "";
+// width: 100%; /* Forces wrapping after every 10th item */
+// }
+
+// .card-row{
+// display: flex;
+// position: absolute;
+// align-items: center;
+// transition: transform 0.3s cubic-bezier(0.455, 0.03, 0.515, 0.955);
+
+// }
+
+.card-item-inactive,
+.card-item-active,
+.card-item {
+ position: relative;
+ transition: transform 0.5s ease-in-out;
+ display: flex;
+ flex-direction: column;
+}
+
+.card-item-inactive {
+ opacity: 0.5;
+}
+
+.card-item-active {
+ position: absolute;
+ z-index: 100;
+}
diff --git a/src/client/views/collections/CollectionCardDeckView.tsx b/src/client/views/collections/CollectionCardDeckView.tsx
new file mode 100644
index 000000000..5f8ddd5c1
--- /dev/null
+++ b/src/client/views/collections/CollectionCardDeckView.tsx
@@ -0,0 +1,513 @@
+import { IReactionDisposer, ObservableMap, action, computed, makeObservable, observable, reaction } from 'mobx';
+import { observer } from 'mobx-react';
+import * as React from 'react';
+import { DashColor, Utils, numberRange, returnFalse, returnZero } from '../../../Utils';
+import { Doc, NumListCast } from '../../../fields/Doc';
+import { DocData } from '../../../fields/DocSymbols';
+import { Id } from '../../../fields/FieldSymbols';
+import { BoolCast, Cast, DateCast, NumCast, RTFCast, ScriptCast, StrCast } from '../../../fields/Types';
+import { URLField } from '../../../fields/URLField';
+import { gptImageLabel } from '../../apis/gpt/GPT';
+import { DocumentType } from '../../documents/DocumentTypes';
+import { DragManager } from '../../util/DragManager';
+import { SelectionManager } from '../../util/SelectionManager';
+import { SnappingManager } from '../../util/SnappingManager';
+import { Transform } from '../../util/Transform';
+import { undoable } from '../../util/UndoManager';
+import { StyleProp } from '../StyleProvider';
+import { DocumentView } from '../nodes/DocumentView';
+import { GPTPopup, GPTPopupMode } from '../pdf/GPTPopup/GPTPopup';
+import './CollectionCardDeckView.scss';
+import { CollectionSubView } from './CollectionSubView';
+
+enum cardSortings {
+ Time = 'time',
+ Type = 'type',
+ Color = 'color',
+ Custom = 'custom',
+ None = '',
+}
+@observer
+export class CollectionCardView extends CollectionSubView() {
+ private _dropDisposer?: DragManager.DragDropDisposer;
+ private _childDocumentWidth = 600; // target width of a Doc...
+ private _disposers: { [key: string]: IReactionDisposer } = {};
+ private _textToDoc = new Map<string, Doc>();
+
+ @observable _forceChildXf = false;
+ @observable _isLoading = false;
+ @observable _hoveredNodeIndex = -1;
+ @observable _docRefs = new ObservableMap<Doc, DocumentView>();
+ @observable _maxRowCount = 10;
+
+ static getButtonGroup(groupFieldKey: 'chat' | 'star' | 'idea' | 'like', doc: Doc): number | undefined {
+ return Cast(doc[groupFieldKey], 'number', null);
+ }
+
+ static imageUrlToBase64 = async (imageUrl: string): Promise<string> => {
+ try {
+ const response = await fetch(imageUrl);
+ const blob = await response.blob();
+
+ return new Promise((resolve, reject) => {
+ const reader = new FileReader();
+ reader.readAsDataURL(blob);
+ reader.onloadend = () => resolve(reader.result as string);
+ reader.onerror = error => reject(error);
+ });
+ } catch (error) {
+ console.error('Error:', error);
+ throw error;
+ }
+ };
+
+ protected createDashEventsTarget = (ele: HTMLDivElement | null) => {
+ this._dropDisposer?.();
+ if (ele) {
+ this._dropDisposer = DragManager.MakeDropTarget(ele, this.onInternalDrop.bind(this), this.layoutDoc);
+ }
+ };
+
+ constructor(props: any) {
+ super(props);
+ makeObservable(this);
+ }
+
+ componentDidMount(): void {
+ this._disposers.sort = reaction(
+ () => ({ cardSort: this.cardSort, field: this.cardSort_customField }),
+ ({ cardSort, field }) => (cardSort === cardSortings.Custom && field === 'chat' ? this.openChatPopup() : GPTPopup.Instance.setVisible(false))
+ );
+ }
+
+ componentWillUnmount() {
+ Object.keys(this._disposers).forEach(key => this._disposers[key]?.());
+ this._dropDisposer?.();
+ }
+
+ @computed get cardSort_customField() {
+ return StrCast(this.Document.cardSort_customField) as any as 'chat' | 'star' | 'idea' | 'like';
+ }
+
+ @computed get cardSort() {
+ return StrCast(this.Document.cardSort) as any as cardSortings;
+ }
+ /**
+ * how much to scale down the contents of the view so that everything will fit
+ */
+ @computed get fitContentScale() {
+ const length = Math.min(this.childDocsWithoutLinks.length, this._maxRowCount);
+ return (this._childDocumentWidth * length) / this._props.PanelWidth();
+ }
+
+ @computed get translateWrapperX() {
+ let translate = 0;
+
+ if (this.inactiveDocs().length !== this.childDocsWithoutLinks.length && this.inactiveDocs().length < 10) {
+ translate += this.panelWidth() / 2;
+ }
+ return translate;
+ }
+
+ /**
+ * The child documents to be rendered-- either all of them except the Links or the docs in the currently active
+ * custom group
+ */
+ @computed get childDocsWithoutLinks() {
+ const regularDocs = this.childDocs.filter(l => l.type !== DocumentType.LINK);
+ const activeGroups = NumListCast(this.Document.cardSort_visibleSortGroups);
+
+ if (activeGroups.length > 0 && this.cardSort === cardSortings.Custom) {
+ return regularDocs.filter(doc => {
+ // Get the group number for the current index
+ const groupNumber = CollectionCardView.getButtonGroup(this.cardSort_customField, doc);
+ // Check if the group number is in the active groups
+ return groupNumber !== undefined && activeGroups.includes(groupNumber);
+ });
+ }
+
+ // Default return for non-custom cardSort or other cases, filtering out links
+ return regularDocs;
+ }
+
+ /**
+ * Determines the order in which the cards will be rendered depending on the current sort type
+ */
+ @computed get sortedDocs() {
+ return this.sort(this.childDocsWithoutLinks, this.cardSort, BoolCast(this.layoutDoc.sortDesc));
+ }
+
+ @action
+ setHoveredNodeIndex = (index: number) => {
+ if (!SelectionManager.IsSelected(this.childDocs[index])) {
+ this._hoveredNodeIndex = index;
+ }
+ };
+ /**
+ * Translates the hovered node to the center of the screen
+ * @param index
+ * @returns
+ */
+ translateHover = (index: number) => (this._hoveredNodeIndex === index && !SelectionManager.IsSelected(this.childDocs[index]) ? -50 : 0);
+
+ isSelected = (index: number) => SelectionManager.IsSelected(this.childDocs[index]);
+
+ /**
+ * Returns all the documents except the one that's currently selected
+ */
+ inactiveDocs = () => this.childDocsWithoutLinks.filter(d => !SelectionManager.IsSelected(d));
+
+ panelWidth = () => this._childDocumentWidth;
+ panelHeight = (layout: Doc) => () => (this.panelWidth() * NumCast(layout._height)) / NumCast(layout._width);
+ onChildDoubleClick = () => ScriptCast(this.layoutDoc.onChildDoubleClick);
+ isContentActive = () => this._props.isSelected() || this._props.isContentActive() || this._props.isAnyChildContentActive();
+ isChildContentActive = () => (this.isContentActive() ? true : false);
+
+ /**
+ * Returns the degree to rotate a card dependind on the amount of cards in their row and their index in said row
+ * @param amCards
+ * @param index
+ * @returns
+ */
+ rotate = (amCards: number, index: number) => {
+ const possRotate = -30 + index * (30 / ((amCards - (amCards % 2)) / 2));
+ const stepMag = Math.abs(-30 + (amCards / 2 - 1) * (30 / ((amCards - (amCards % 2)) / 2)));
+
+ if (amCards % 2 == 0 && possRotate == 0) {
+ return possRotate + Math.abs(-30 + (index - 1) * (30 / (amCards / 2)));
+ }
+ if (amCards % 2 == 0 && index > (amCards + 1) / 2) {
+ return possRotate + stepMag;
+ }
+
+ return possRotate;
+ };
+ /**
+ * Returns the degree to which a card should be translated in the y direction for the arch effect
+ */
+ translateY = (amCards: number, index: number, realIndex: number) => {
+ const evenOdd = amCards % 2;
+ const apex = (amCards - evenOdd) / 2;
+ const stepMag = 200 / ((amCards - evenOdd) / 2) + Math.abs((apex - index) * 25);
+
+ let rowOffset = 0;
+ if (realIndex > this._maxRowCount - 1) {
+ rowOffset = 400 * ((realIndex - (realIndex % this._maxRowCount)) / this._maxRowCount);
+ }
+ if (evenOdd == 1 || index < apex - 1) {
+ return Math.abs(stepMag * (apex - index)) - rowOffset;
+ }
+ if (index == apex || index == apex - 1) {
+ return 0 - rowOffset;
+ }
+
+ return Math.abs(stepMag * (apex - index - 1)) - rowOffset;
+ };
+
+ /**
+ * Translates the selected node to the middle fo the screen
+ * @param index
+ * @returns
+ */
+ translateSelected = (index: number): number => {
+ // if (this.isSelected(index)) {
+ const middleOfPanel = this._props.PanelWidth() / 2;
+ const scaledNodeWidth = this.panelWidth() * 1.25;
+
+ // Calculate the position of the node's left edge before scaling
+ const nodeLeftEdge = index * this.panelWidth();
+ // Find the center of the node after scaling
+ const scaledNodeCenter = nodeLeftEdge + scaledNodeWidth / 2;
+
+ // Calculate the translation needed to align the scaled node's center with the panel's center
+ const translation = middleOfPanel - scaledNodeCenter - scaledNodeWidth - scaledNodeWidth / 4;
+
+ return translation;
+ };
+
+ /**
+ * Called in the sortedDocsType method. Compares the cards' value in regards to the desired sort type-- earlier cards are move to the
+ * front, latter cards to the back
+ * @param docs
+ * @param sortType
+ * @param isDesc
+ * @returns
+ */
+ sort = (docs: Doc[], sortType: cardSortings, isDesc: boolean) => {
+ if (sortType === cardSortings.None) return docs;
+ docs.sort((docA, docB) => {
+ const [typeA, typeB] = (() => {
+ switch (sortType) {
+ case cardSortings.Time:
+ return [DateCast(docA.author_date)?.date ?? Date.now(),
+ DateCast(docB.author_date)?.date ?? Date.now()];
+ case cardSortings.Color:
+ return [DashColor(StrCast(docA.backgroundColor)).hsv().toString(), // If docA.type is undefined, use an empty string
+ DashColor(StrCast(docB.backgroundColor)).hsv().toString()]; // If docB.type is undefined, use an empty string
+ case cardSortings.Custom:
+ return [CollectionCardView.getButtonGroup(this.cardSort_customField, docA)??0,
+ CollectionCardView.getButtonGroup(this.cardSort_customField, docB)??0];
+ default: return [StrCast(docA.type), // If docA.type is undefined, use an empty string
+ StrCast(docB.type)]; // If docB.type is undefined, use an empty string
+ } // prettier-ignore
+ })();
+
+ const out = typeA < typeB ? -1 : typeA > typeB ? 1 : 0;
+ return isDesc ? -out : out; // Reverse the sort order if descending is true
+ });
+
+ return docs;
+ };
+
+ displayDoc = (doc: Doc, screenToLocalTransform: () => Transform) => {
+ return (
+ <DocumentView
+ {...this._props}
+ ref={action((r: DocumentView) => r?.ContentDiv && this._docRefs.set(doc, r))}
+ Document={doc}
+ NativeWidth={returnZero}
+ NativeHeight={returnZero}
+ layout_fitWidth={returnFalse}
+ onDoubleClickScript={this.onChildDoubleClick}
+ renderDepth={this._props.renderDepth + 1}
+ LayoutTemplate={this._props.childLayoutTemplate}
+ LayoutTemplateString={this._props.childLayoutString}
+ ScreenToLocalTransform={screenToLocalTransform} //makes sure the box wrapper thing is in the right spot
+ isContentActive={this.isChildContentActive}
+ isDocumentActive={this._props.childDocumentsActive?.() || this.Document._childDocumentsActive ? this._props.isDocumentActive : this.isContentActive}
+ PanelWidth={this.panelWidth}
+ PanelHeight={this.panelHeight(doc)}
+ />
+ );
+ };
+
+ /**
+ * Determines how many cards are in the row of a card at a specific index
+ * @param index
+ * @returns
+ */
+ overflowAmCardsCalc = (index: number) => {
+ if (this.inactiveDocs().length < this._maxRowCount) {
+ return this.inactiveDocs().length;
+ }
+ // 13 - 3 = 10
+ const totalCards = this.inactiveDocs().length;
+ // if 9 or less
+ if (index < totalCards - (totalCards % 10)) {
+ return this._maxRowCount;
+ }
+ //(3)
+ return totalCards % 10;
+ };
+ /**
+ * Determines the index a card is in in a row
+ * @param realIndex
+ * @returns
+ */
+ overflowIndexCalc = (realIndex: number) => realIndex % 10;
+ /**
+ * Translates the cards in the second rows and beyond over to the right
+ * @param realIndex
+ * @param calcIndex
+ * @param calcRowCards
+ * @returns
+ */
+ translateOverflowX = (realIndex: number, calcRowCards: number) => (realIndex < this._maxRowCount ? 0 : (10 - calcRowCards) * (this.panelWidth() / 2));
+
+ /**
+ * Determines how far to translate a card in the y direction depending on its index, whether or not its being hovered, or if it's selected
+ * @param isHovered
+ * @param isSelected
+ * @param realIndex
+ * @param amCards
+ * @param calcRowIndex
+ * @returns
+ */
+ calculateTranslateY = (isHovered: boolean, isSelected: boolean, realIndex: number, amCards: number, calcRowIndex: number) => {
+ if (isSelected) return 50 * this.fitContentScale;
+ const trans = isHovered ? this.translateHover(realIndex) : 0;
+ return trans + this.translateY(amCards, calcRowIndex, realIndex);
+ };
+
+ /**
+ * Toggles the buttons between on and off when creating custom sort groupings/changing those created by gpt
+ * @param childPairIndex
+ * @param buttonID
+ * @param doc
+ */
+ toggleButton = undoable((buttonID: number, doc: Doc) => this.cardSort_customField && (doc[this.cardSort_customField] = buttonID), 'toggle custom button');
+
+ /**
+ * A list of the text content of all the child docs. RTF documents will have just their text and pdf documents will have the first 50 words.
+ * Image documents are converted to bse64 and gpt generates a description for them. all other documents use their title. This string is
+ * inputted into the gpt prompt to sort everything together
+ * @returns
+ */
+ childPairStringList = () => {
+ const docToText = (doc: Doc) => {
+ switch (doc.type) {
+ case DocumentType.PDF: const words = StrCast(doc.text).split(/\s+/);
+ return words.slice(0, 50).join(' '); // first 50 words of pdf text
+ case DocumentType.IMG: return this.getImageDesc(doc);
+ case DocumentType.RTF: return StrCast(RTFCast(doc.text).Text);
+ default: return StrCast(doc.title);
+ } // prettier-ignore
+ };
+ const docTextPromises = this.childDocsWithoutLinks.map(async doc => {
+ const docText = (await docToText(doc)) ?? '';
+ this._textToDoc.set(docText.trim(), doc);
+ return `======${docText.replace(/\n/g, ' ').trim()}======`;
+ });
+ return Promise.all<string>(docTextPromises);
+ };
+
+ /**
+ * Calls the gpt API to generate descriptions for the images in the view
+ * @param image
+ * @returns
+ */
+ getImageDesc = async (image: Doc) => {
+ if (StrCast(image.description)) return StrCast(image.description); // Return existing description
+ const href = (image.data as URLField).url.href;
+ const hrefParts = href.split('.');
+ const hrefComplete = `${hrefParts[0]}_o.${hrefParts[1]}`;
+ try {
+ const hrefBase64 = await CollectionCardView.imageUrlToBase64(hrefComplete);
+ const response = await gptImageLabel(hrefBase64);
+ image[DocData].description = response.trim();
+ return response; // Return the response from gptImageLabel
+ } catch (error) {
+ console.log('bad things have happened');
+ }
+ return '';
+ };
+
+ /**
+ * Converts the gpt output into a hashmap that can be used for sorting. lists are seperated by ==== while elements within the list are seperated by ~~~~~~
+ * @param gptOutput
+ */
+ processGptOutput = (gptOutput: string) => {
+ // Split the string into individual list items
+ const listItems = gptOutput.split('======').filter(item => item.trim() !== '');
+ listItems.forEach((item, index) => {
+ // Split the item by '~~~~~~' to get all descriptors
+ const parts = item.split('~~~~~~').map(part => part.trim());
+
+ parts.forEach(part => {
+ // Find the corresponding Doc in the textToDoc map
+ const doc = this._textToDoc.get(part);
+ if (doc) {
+ doc.chat = index;
+ }
+ });
+ });
+ };
+ /**
+ * Opens up the chat popup and starts the process for smart sorting.
+ */
+ openChatPopup = async () => {
+ GPTPopup.Instance.setVisible(true);
+ GPTPopup.Instance.setMode(GPTPopupMode.SORT);
+ const sortDesc = await this.childPairStringList(); // Await the promise to get the string result
+ GPTPopup.Instance.setCardsDoneLoading(true); // Set dataDoneLoading to true after data is loaded
+ GPTPopup.Instance.setSortDesc(sortDesc.join());
+ GPTPopup.Instance.onSortComplete = (sortResult: string) => this.processGptOutput(sortResult);
+ };
+
+ /**
+ * Renders the buttons to customize sorting depending on which group the card belongs to and the amount of total groups
+ * @param childPairIndex
+ * @param doc
+ * @returns
+ */
+ renderButtons = (doc: Doc, cardSort: cardSortings) => {
+ if (cardSort !== cardSortings.Custom) return '';
+ const amButtons = Math.max(4, this.childDocs?.reduce((set, doc) => this.cardSort_customField && set.add(NumCast(doc[this.cardSort_customField])), new Set<number>()).size ?? 0);
+ const activeButtonIndex = CollectionCardView.getButtonGroup(this.cardSort_customField, doc);
+ const totalWidth = amButtons * 35 + amButtons * 2 * 5 + 6;
+ return (
+ <div className="card-button-container" style={{ width: `${totalWidth}px` }}>
+ {numberRange(amButtons).map(i => (
+ <button
+ key={i}
+ type="button"
+ style={{ backgroundColor: activeButtonIndex === i ? '#4476f7' : '#323232' }} //
+ onClick={() => this.toggleButton(i, doc)}
+ />
+ ))}
+ </div>
+ );
+ };
+ /**
+ * Actually renders all the cards
+ */
+ renderCards = () => {
+ const anySelected = this.childDocs.some(doc => SelectionManager.IsSelected(doc));
+ // Map sorted documents to their rendered components
+ return this.sortedDocs.map((doc, index) => {
+ const realIndex = this.sortedDocs.filter(sortDoc => !SelectionManager.IsSelected(sortDoc)).indexOf(doc);
+ const calcRowIndex = this.overflowIndexCalc(realIndex);
+ const amCards = this.overflowAmCardsCalc(realIndex);
+ const isSelected = SelectionManager.IsSelected(doc);
+
+ const childScreenToLocal = () => {
+ this._forceChildXf;
+ const dref = this._docRefs.get(doc);
+ const { translateX, translateY, scale } = Utils.GetScreenTransform(dref?.ContentDiv);
+ return new Transform(-translateX + (dref?.centeringX || 0) * scale,
+ -translateY + (dref?.centeringY || 0) * scale, 1)
+ .scale(1 / scale).rotate(!isSelected ? -this.rotate(amCards, calcRowIndex) : 0); // prettier-ignore
+ };
+
+ return (
+ <div
+ key={doc[Id]}
+ className={`card-item${isSelected ? '-active' : anySelected ? '-inactive' : ''}`}
+ onPointerUp={() => {
+ // this turns off documentDecorations during a transition, then turns them back on afterward.
+ SnappingManager.SetIsResizing(this.Document);
+ setTimeout(
+ action(() => {
+ SnappingManager.SetIsResizing(undefined);
+ this._forceChildXf = !this._forceChildXf;
+ }),
+ 700
+ );
+ }}
+ style={{
+ width: this.panelWidth(),
+ height: 'max-content', // this.panelHeight(childPair.layout)(),
+ transform: `translateY(${this.calculateTranslateY(this._hoveredNodeIndex === index, isSelected, realIndex, amCards, calcRowIndex)}px)
+ translateX(${isSelected ? this.translateSelected(calcRowIndex) : this.translateOverflowX(realIndex, amCards)}px)
+ rotate(${!isSelected ? this.rotate(amCards, calcRowIndex) : 0}deg)
+ scale(${isSelected ? 1.25 : 1})`,
+ }}
+ onMouseEnter={() => this.setHoveredNodeIndex(index)}>
+ {this.displayDoc(doc, childScreenToLocal)}
+ {this.renderButtons(doc, this.cardSort)}
+ </div>
+ );
+ });
+ };
+ render() {
+ return (
+ <div
+ className="collectionCardView-outer"
+ ref={this.createDashEventsTarget}
+ style={{
+ background: this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.BackgroundColor),
+ color: this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.Color),
+ }}>
+ <div
+ className="card-wrapper"
+ style={{
+ transform: ` scale(${1 / this.fitContentScale}) translateX(${this.translateWrapperX}px)`,
+ height: `${100 * this.fitContentScale}%`,
+ }}
+ onMouseLeave={() => this.setHoveredNodeIndex(-1)}>
+ {this.renderCards()}
+ </div>
+ </div>
+ );
+ }
+}
diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx
index b52c7c54c..5c304b4a9 100644
--- a/src/client/views/collections/CollectionView.tsx
+++ b/src/client/views/collections/CollectionView.tsx
@@ -33,6 +33,7 @@ import { CollectionLinearView } from './collectionLinear';
import { CollectionMulticolumnView } from './collectionMulticolumn/CollectionMulticolumnView';
import { CollectionMultirowView } from './collectionMulticolumn/CollectionMultirowView';
import { CollectionSchemaView } from './collectionSchema/CollectionSchemaView';
+import { CollectionCardView } from './CollectionCardDeckView';
@observer
export class CollectionView extends ViewBoxAnnotatableComponent<CollectionViewProps>() {
@@ -104,6 +105,7 @@ export class CollectionView extends ViewBoxAnnotatableComponent<CollectionViewPr
case CollectionViewType.Masonry: return <CollectionStackingView key="collview" {...props} />;
case CollectionViewType.Time: return <CollectionTimeView key="collview" {...props} />;
case CollectionViewType.Grid: return <CollectionGridView key="collview" {...props} />;
+ case CollectionViewType.Card: return <CollectionCardView key="collview" {...props} />;
case CollectionViewType.Freeform:
default: return <CollectionFreeFormView key="collview" {...props} />;
}
diff --git a/src/client/views/global/globalScripts.ts b/src/client/views/global/globalScripts.ts
index c595681b7..2b804c5d3 100644
--- a/src/client/views/global/globalScripts.ts
+++ b/src/client/views/global/globalScripts.ts
@@ -1,7 +1,21 @@
import { Colors } from 'browndash-components';
import { action, runInAction } from 'mobx';
import { aggregateBounds } from '../../../Utils';
-import { ActiveFillColor, ActiveInkColor, ActiveInkHideTextLabels, ActiveInkWidth, ActiveIsInkMask, Doc, Opt, SetActiveFillColor, SetActiveInkColor, SetActiveInkHideTextLabels, SetActiveInkWidth, SetActiveIsInkMask } from '../../../fields/Doc';
+import {
+ ActiveFillColor,
+ ActiveInkColor,
+ ActiveInkHideTextLabels,
+ ActiveInkWidth,
+ ActiveIsInkMask,
+ Doc,
+ DocListCast,
+ Opt,
+ SetActiveFillColor,
+ SetActiveInkColor,
+ SetActiveInkHideTextLabels,
+ SetActiveInkWidth,
+ SetActiveIsInkMask,
+} from '../../../fields/Doc';
import { DocData } from '../../../fields/DocSymbols';
import { InkTool } from '../../../fields/InkField';
import { BoolCast, Cast, NumCast, StrCast } from '../../../fields/Types';
@@ -20,6 +34,8 @@ import { ImageBox } from '../nodes/ImageBox';
import { VideoBox } from '../nodes/VideoBox';
import { WebBox } from '../nodes/WebBox';
import { RichTextMenu } from '../nodes/formattedText/RichTextMenu';
+import { NumListCast } from '../../../fields/Doc';
+import { List } from '../../../fields/List';
// import { InkTranscription } from '../InkTranscription';
@@ -120,7 +136,12 @@ ScriptingGlobals.add(function toggleOverlay(checkResult?: boolean) {
ScriptingGlobals.add(function showFreeform(attr: 'center' | 'grid' | 'snaplines' | 'clusters' | 'viewAll' | 'fitOnce', checkResult?: boolean, persist?: boolean) {
const selected = DocumentView.SelectedDocs().lastElement();
// prettier-ignore
- const map: Map<'center' |'grid' | 'snaplines' | 'clusters' | 'viewAll' | 'fitOnce', { waitForRender?: boolean, checkResult: (doc:Doc) => any; setDoc: (doc:Doc, dv:DocumentView) => void;}> = new Map([
+ const map: Map<'flashcards' | 'center' | 'grid' | 'snaplines' | 'clusters' | 'arrange' | 'viewAll' | 'fitOnce' | 'time' | 'docType' | 'color' | 'links' | 'like' | 'star' | 'idea' | 'chat' | '1' | '2' | '3' | '4',
+ {
+ waitForRender?: boolean;
+ checkResult: (doc: Doc) => any;
+ setDoc: (doc: Doc, dv: DocumentView) => void;
+ }> = new Map([
['grid', {
checkResult: (doc:Doc) => BoolCast(doc?._freeform_backgroundGrid, false),
setDoc: (doc:Doc) => { doc._freeform_backgroundGrid = !doc._freeform_backgroundGrid; },
@@ -130,8 +151,8 @@ ScriptingGlobals.add(function showFreeform(attr: 'center' | 'grid' | 'snaplines'
setDoc: (doc:Doc) => { doc._freeform_snapLines = !doc._freeform_snapLines; },
}],
['viewAll', {
- checkResult: (doc:Doc) => BoolCast(doc?._freeform_fitContentsToBox, false),
- setDoc: (doc:Doc,dv:DocumentView) => {
+ checkResult: (doc: Doc) => BoolCast(doc?._freeform_fitContentsToBox, false),
+ setDoc: (doc: Doc, dv: DocumentView) => {
if (persist) doc._freeform_fitContentsToBox = !doc._freeform_fitContentsToBox;
else if (doc._freeform_fitContentsToBox) doc._freeform_fitContentsToBox = undefined;
else (dv.ComponentView as CollectionFreeFormView)?.fitContentOnce();
@@ -146,7 +167,68 @@ ScriptingGlobals.add(function showFreeform(attr: 'center' | 'grid' | 'snaplines'
checkResult: (doc:Doc) => BoolCast(doc?._freeform_useClusters, false),
setDoc: (doc:Doc) => { doc._freeform_useClusters = !doc._freeform_useClusters; },
}],
- ]);
+ ['flashcards', {
+ checkResult: (doc: Doc) => BoolCast(Doc.UserDoc().defaultToFlashcards, false),
+ setDoc: (doc: Doc, dv: DocumentView) => Doc.UserDoc().defaultToFlashcards = !Doc.UserDoc().defaultToFlashcards,
+ }],
+ ['time', {
+ checkResult: (doc: Doc) => StrCast(doc?.cardSort) === "time",
+ setDoc: (doc: Doc, dv: DocumentView) => doc.cardSort = "time",
+ }],
+ ['docType', {
+ checkResult: (doc: Doc) => StrCast(doc?.cardSort) === "type",
+ setDoc: (doc: Doc, dv: DocumentView) => doc.cardSort = "type",
+ }],
+ ['color', {
+ checkResult: (doc: Doc) => StrCast(doc?.cardSort) === "color",
+ setDoc: (doc: Doc, dv: DocumentView) => doc.cardSort = "color",
+ }],
+ ['links', {
+ checkResult: (doc: Doc) => StrCast(doc?.cardSort) === "links",
+ setDoc: (doc: Doc, dv: DocumentView) => doc.cardSort = "links",
+ }],
+ ['like', {
+ checkResult: (doc: Doc) => StrCast(doc?.cardSort) === "custom" && StrCast(doc?.cardSort_customField) === "like",
+ setDoc: (doc: Doc, dv: DocumentView) => {
+ doc.cardSort = "custom";
+ doc.cardSort_customField = "like";
+ doc.cardSort_visibleSortGroups = new List<number>();
+ }
+ }],
+ ['star', {
+ checkResult: (doc: Doc) => StrCast(doc?.cardSort) === "custom" && StrCast(doc?.cardSort_customField) === "star",
+ setDoc: (doc: Doc, dv: DocumentView) => {
+ doc.cardSort = "custom";
+ doc.cardSort_customField = "star";
+ doc.cardSort_visibleSortGroups = new List<number>();
+ }
+ }],
+ ['idea', {
+ checkResult: (doc: Doc) => StrCast(doc?.cardSort) === "custom" && StrCast(doc?.cardSort_customField) === "idea",
+ setDoc: (doc: Doc, dv: DocumentView) => {
+ doc.cardSort = "custom";
+ doc.cardSort_customField = "idea";
+ doc.cardSort_visibleSortGroups = new List<number>();
+ }
+ }],
+ ['chat', {
+ checkResult: (doc: Doc) => StrCast(doc?.cardSort) === "custom" && StrCast(doc?.cardSort_customField) === "chat",
+ setDoc: (doc: Doc, dv: DocumentView) => {
+ doc.cardSort = "custom";
+ doc.cardSort_customField = "chat";
+ doc.cardSort_visibleSortGroups = new List<number>();
+ },
+ }],
+ ]);
+ for (let i = 0; i < 8; i++) {
+ map.set((i + 1 + '') as any, {
+ checkResult: (doc: Doc) => NumListCast(doc?.cardSort_visibleSortGroups).includes(i),
+ setDoc: (doc: Doc, dv: DocumentView) => {
+ const list = NumListCast(doc.cardSort_visibleSortGroups);
+ doc.cardSort_visibleSortGroups = new List<number>(list.includes(i) ? list.filter(d => d !== i) : [...list, i]);
+ },
+ });
+ }
if (checkResult) {
return map.get(attr)?.checkResult(selected);
@@ -157,6 +239,45 @@ ScriptingGlobals.add(function showFreeform(attr: 'center' | 'grid' | 'snaplines'
return undefined;
});
+ScriptingGlobals.add(function cardHasLabel(label: string) {
+ const selected = DocumentView.SelectedDocs().lastElement();
+ const labelNum = Number(label) - 1;
+ return labelNum < 4 || (selected && DocListCast(selected[Doc.LayoutFieldKey(selected)]).some(doc => doc[StrCast(selected.cardSort_customField)] == labelNum));
+}, '');
+
+// ScriptingGlobals.add(function setCardSortAttr(attr: 'time' | 'docType' | 'color', value: any, checkResult?: boolean) {
+// // const editorView = RichTextMenu.Instance?.TextView?.EditorView;
+// const selected = SelectionManager.Docs.lastElement();
+// // prettier-ignore
+// const map: Map<'time' | 'docType' | 'color', { waitForRender?: boolean, checkResult: (doc:Doc) => any; setDoc: (doc:Doc, dv:DocumentView) => void;}> = new Map([
+// ['time', {
+// checkResult: (doc:Doc) => StrCast(doc?.cardSort),
+// setDoc: (doc:Doc,dv:DocumentView) => doc.cardSort = "time",
+// }],
+// ['docType', {
+// checkResult: (doc:Doc) => StrCast(doc?.cardSort),
+// setDoc: (doc:Doc,dv:DocumentView) => doc.cardSort = "type",
+// }],
+// ['color', {
+// checkResult: (doc:Doc) => StrCast(doc?.cardSort),
+// setDoc: (doc:Doc,dv:DocumentView) => doc.cardSort = "color",
+// }],
+// // ['custom', {
+// // checkResult: () => RichTextMenu.Instance.textAlign,
+// // setDoc: () => value && editorView?.state ? RichTextMenu.Instance.align(editorView, editorView.dispatch, value):(Doc.UserDoc().textAlign = value),
+// // }]
+// // ,
+// ]);
+
+// if (checkResult) {
+// return map.get(attr)?.checkResult(selected);
+// }
+
+// console.log('hey')
+// SelectionManager.Views.map(dv => map.get(attr)?.setDoc(dv.layoutDoc, dv));
+// console.log('success')
+// });
+
// eslint-disable-next-line prefer-arrow-callback
ScriptingGlobals.add(function setFontAttr(attr: 'font' | 'fontColor' | 'highlight' | 'fontSize' | 'alignment', value: any, checkResult?: boolean) {
const editorView = RichTextMenu.Instance?.TextView?.EditorView;
diff --git a/src/client/views/pdf/GPTPopup/GPTPopup.scss b/src/client/views/pdf/GPTPopup/GPTPopup.scss
index 48659d0e7..6d8793f82 100644
--- a/src/client/views/pdf/GPTPopup/GPTPopup.scss
+++ b/src/client/views/pdf/GPTPopup/GPTPopup.scss
@@ -55,16 +55,29 @@ $highlightedText: #82e0ff;
overflow-y: auto;
}
- .btns-wrapper {
+ .btns-wrapper-gpt {
height: 50px;
display: flex;
- justify-content: space-between;
+ justify-content: center;
align-items: center;
+ transform: translateY(30px);
+
+
+ .searchBox-input{
+ transform: translateY(-15px);
+ height: 50px;
+ border-radius: 10px;
+ border-color: #5b97ff;
+ }
+
+
.summarizing {
display: flex;
align-items: center;
}
+
+
}
button {
@@ -111,6 +124,28 @@ $highlightedText: #82e0ff;
}
}
+.loading-spinner {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ height: 100px;
+ font-size: 20px;
+ font-weight: bold;
+ color: #666;
+}
+
+
+
+
+
+@keyframes spin {
+ to {
+ transform: rotate(360deg);
+ }
+}
+
+
+
.image-content-wrapper {
display: flex;
flex-direction: column;
diff --git a/src/client/views/pdf/GPTPopup/GPTPopup.tsx b/src/client/views/pdf/GPTPopup/GPTPopup.tsx
index 3f6c154bb..8bb2e2844 100644
--- a/src/client/views/pdf/GPTPopup/GPTPopup.tsx
+++ b/src/client/views/pdf/GPTPopup/GPTPopup.tsx
@@ -25,6 +25,7 @@ export enum GPTPopupMode {
EDIT,
IMAGE,
DATA,
+ SORT,
}
interface GPTPopupProps {}
@@ -102,6 +103,14 @@ export class GPTPopup extends ObservableReactComponent<GPTPopupProps> {
this.chatMode = false;
};
+ @observable
+ private sortDone: boolean = false; // this is so redundant but the og done variable was causing weird unknown problems and im just a girl
+
+ @action
+ public setSortDone = (done: boolean) => {
+ this.sortDone = done;
+ };
+
// change what can be a ref into a ref
@observable
private sidebarId: string = '';
@@ -124,11 +133,48 @@ export class GPTPopup extends ObservableReactComponent<GPTPopupProps> {
this.textAnchor = anchor;
};
+ @observable
+ public sortDesc: string = '';
+
+ @action public setSortDesc = (t: string) => {
+ this.sortDesc = t;
+ };
+
+ @observable onSortComplete?: (sortResult: string) => void;
+ @observable cardsDoneLoading = false;
+
+ @action setCardsDoneLoading(done: boolean) {
+ console.log(done + 'HI HIHI');
+ this.cardsDoneLoading = done;
+ }
+
public addDoc: (doc: Doc | Doc[], sidebarKey?: string | undefined) => boolean = () => false;
public createFilteredDoc: (axes?: any) => boolean = () => false;
public addToCollection: ((doc: Doc | Doc[], annotationKey?: string | undefined) => boolean) | undefined;
/**
+ * Sorts cards in the CollectionCardDeckView
+ */
+ generateSort = async () => {
+ this.setLoading(true);
+ this.setSortDone(false);
+
+ try {
+ const res = await gptAPICall(this.sortDesc, GPTCallType.SORT);
+ // Trigger the callback with the result
+ if (this.onSortComplete) {
+ this.onSortComplete(res || 'Something went wrong :(');
+ console.log(res);
+ }
+ } catch (err) {
+ console.error(err);
+ }
+
+ this.setLoading(false);
+ this.setSortDone(true);
+ };
+
+ /**
* Generates a Dalle image and uploads it to the server.
*/
generateImage = async () => {
@@ -267,6 +313,59 @@ export class GPTPopup extends ObservableReactComponent<GPTPopupProps> {
}
};
+ sortBox = () => (
+ <>
+ <div>
+ {this.heading('SORTING')}
+ {this.loading ? (
+ <div className="content-wrapper">
+ <div className="loading-spinner">
+ <ReactLoading type="spin" color={StrCast(Doc.UserDoc().userVariantColor)} height={30} width={30} />
+ <span>Loading...</span>
+ </div>
+ </div>
+ ) : (
+ <>
+ {!this.cardsDoneLoading ? (
+ <div className="content-wrapper">
+ <div className="loading-spinner">
+ <ReactLoading type="spin" color={StrCast(Doc.UserDoc().userVariantColor)} height={30} width={30} />
+ <span>Reading Cards...</span>
+ </div>
+ </div>
+ ) : (
+ !this.sortDone && (
+ <div className="btns-wrapper-gpt">
+ <Button
+ tooltip="Have ChatGPT sort your cards for you!"
+ text="Sort!"
+ onClick={this.generateSort}
+ color={StrCast(Doc.UserDoc().userVariantColor)}
+ type={Type.TERT}
+ style={{
+ width: '90%', // Almost as wide as the container
+ textAlign: 'center',
+ color: '#ffffff', // White text
+ fontSize: '16px', // Adjust font size as needed
+ }}
+ />
+ </div>
+ )
+ )}
+
+ {this.sortDone && (
+ <div>
+ <div className="content-wrapper">
+ <p>{this.text === 'Something went wrong :(' ? 'Something went wrong :(' : 'Sorting done! Feel free to move things around / regenerate :) !'}</p>
+ <IconButton tooltip="Generate Again" onClick={() => this.setSortDone(false)} icon={<FontAwesomeIcon icon="redo-alt" size="lg" />} color={StrCast(Doc.UserDoc().userVariantColor)} />
+ </div>
+ </div>
+ )}
+ </>
+ )}
+ </div>
+ </>
+ );
imageBox = () => (
<div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}>
{this.heading('GENERATED IMAGE')}
@@ -419,7 +518,7 @@ export class GPTPopup extends ObservableReactComponent<GPTPopupProps> {
render() {
return (
<div className="summary-box" style={{ display: this.visible ? 'flex' : 'none' }}>
- {this.mode === GPTPopupMode.SUMMARY ? this.summaryBox() : this.mode === GPTPopupMode.DATA ? this.dataAnalysisBox() : this.mode === GPTPopupMode.IMAGE ? this.imageBox() : null}
+ {this.mode === GPTPopupMode.SUMMARY ? this.summaryBox() : this.mode === GPTPopupMode.DATA ? this.dataAnalysisBox() : this.mode === GPTPopupMode.IMAGE ? this.imageBox() : this.mode === GPTPopupMode.SORT ? this.sortBox() : null}
</div>
);
}