diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/client/apis/gpt/GPT.ts | 57 | ||||
-rw-r--r-- | src/client/documents/DocumentTypes.ts | 3 | ||||
-rw-r--r-- | src/client/documents/Documents.ts | 8 | ||||
-rw-r--r-- | src/client/util/CurrentUserUtils.ts | 39 | ||||
-rw-r--r-- | src/client/views/MainView.tsx | 5 | ||||
-rw-r--r-- | src/client/views/collections/CollectionCardDeckView.scss | 84 | ||||
-rw-r--r-- | src/client/views/collections/CollectionCardDeckView.tsx | 513 | ||||
-rw-r--r-- | src/client/views/collections/CollectionView.tsx | 2 | ||||
-rw-r--r-- | src/client/views/global/globalScripts.ts | 131 | ||||
-rw-r--r-- | src/client/views/pdf/GPTPopup/GPTPopup.scss | 39 | ||||
-rw-r--r-- | src/client/views/pdf/GPTPopup/GPTPopup.tsx | 101 |
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> ); } |