aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/ClientUtils.ts34
-rw-r--r--src/client/apis/gpt/GPT.ts43
-rw-r--r--src/client/documents/Documents.ts10
-rw-r--r--src/client/util/CurrentUserUtils.ts73
-rw-r--r--src/client/util/Scripting.ts4
-rw-r--r--src/client/views/DocumentButtonBar.scss9
-rw-r--r--src/client/views/DocumentButtonBar.tsx68
-rw-r--r--src/client/views/DocumentDecorations.tsx7
-rw-r--r--src/client/views/FilterPanel.scss123
-rw-r--r--src/client/views/FilterPanel.tsx262
-rw-r--r--src/client/views/InkingStroke.tsx7
-rw-r--r--src/client/views/MainView.tsx6
-rw-r--r--src/client/views/PropertiesView.tsx2
-rw-r--r--src/client/views/StyleProvider.tsx11
-rw-r--r--src/client/views/collections/CollectionCardDeckView.scss56
-rw-r--r--src/client/views/collections/CollectionCardDeckView.tsx448
-rw-r--r--src/client/views/collections/CollectionCarouselView.tsx2
-rw-r--r--src/client/views/global/globalScripts.ts203
-rw-r--r--src/client/views/nodes/FontIconBox/FontIconBox.tsx2
-rw-r--r--src/client/views/nodes/IconTagBox.scss30
-rw-r--r--src/client/views/nodes/IconTagBox.tsx142
-rw-r--r--src/client/views/pdf/AnchorMenu.tsx2
-rw-r--r--src/client/views/pdf/GPTPopup/GPTPopup.scss100
-rw-r--r--src/client/views/pdf/GPTPopup/GPTPopup.tsx427
-rw-r--r--src/typings/index.d.ts2
25 files changed, 1710 insertions, 363 deletions
diff --git a/src/ClientUtils.ts b/src/ClientUtils.ts
index 01eda7e98..58648c5b6 100644
--- a/src/ClientUtils.ts
+++ b/src/ClientUtils.ts
@@ -234,6 +234,40 @@ export namespace ClientUtils {
return 'rgba(' + col.r + ',' + col.g + ',' + col.b + (col.a !== undefined ? ',' + col.a : '') + ')';
}
+ export function hexToHsv(hex: string): [number, number, number] {
+ if (!hex) return [0, 0, 0]; // Default to black if hex is not defined
+ const r = parseInt(hex.slice(1, 3), 16) / 255;
+ const g = parseInt(hex.slice(3, 5), 16) / 255;
+ const b = parseInt(hex.slice(5, 7), 16) / 255;
+ const max = Math.max(r, g, b),
+ min = Math.min(r, g, b);
+ const d = max - min;
+ let h: number;
+ const s = max === 0 ? 0 : d / max;
+ const v = max;
+
+ switch (max) {
+ case min:
+ h = 0;
+ break;
+ case r:
+ h = (g - b) / d + (g < b ? 6 : 0);
+ break;
+ case g:
+ h = (b - r) / d + 2;
+ break;
+ case b:
+ h = (r - g) / d + 4;
+ break;
+ default:
+ h = 0;
+ break;
+ }
+ h /= 6;
+ return [h, s, v];
+ };
+
+
export function HSLtoRGB(h: number, s: number, l: number) {
// Must be fractions of 1
// s /= 100;
diff --git a/src/client/apis/gpt/GPT.ts b/src/client/apis/gpt/GPT.ts
index 8dd3fd6e2..2657bf12e 100644
--- a/src/client/apis/gpt/GPT.ts
+++ b/src/client/apis/gpt/GPT.ts
@@ -12,6 +12,10 @@ enum GPTCallType {
DESCRIBE = 'describe',
MERMAID = 'mermaid',
DATA = 'data',
+ RUBRIC = 'rubric',
+ TYPE = 'type',
+ SUBSET = 'subset',
+ INFO = 'info'
}
type GPTCallOpts = {
@@ -26,6 +30,7 @@ const callTypeMap: { [type: string]: GPTCallOpts } = {
summary: { model: 'gpt-4-turbo', maxTokens: 256, temp: 0.5, prompt: 'Summarize the text given in simpler terms.' },
edit: { model: 'gpt-4-turbo', maxTokens: 256, temp: 0.5, prompt: 'Reword the text.' },
flashcard: { model: 'gpt-4-turbo', maxTokens: 512, temp: 0.5, prompt: 'Make flashcards out of this text with each question and answer labeled. Do not label each flashcard and do not include asterisks: ' },
+
completion: { model: 'gpt-4-turbo', maxTokens: 256, temp: 0.5, prompt: "You are a helpful assistant. Answer the user's prompt." },
mermaid: {
model: 'gpt-4-turbo',
@@ -42,8 +47,8 @@ const callTypeMap: { [type: string]: GPTCallOpts } = {
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.",
+ temp: 0.25,
+ prompt: "The user is going to give you a list of descriptions. Each one is separated by `======` on either side. Descriptions will vary in length, so make sure to only separate when you see `======`. Sort them by the user's specifications. Make sure each description is only in the list once. Each item should be separated by `======`. Immediately afterward, surrounded by `------` on BOTH SIDES, provide some insight into your reasoning for the way you sorted (and mention nothing about the formatting details given in this description). It is VERY important that you format it exactly as described, ensuring the proper number of `=` and `-` (6 of each) and NO commas"
},
describe: { model: 'gpt-4-vision-preview', maxTokens: 2048, temp: 0, prompt: 'Describe these images in 3-5 words' },
chatcard: { model: 'gpt-4-turbo', maxTokens: 512, temp: 0.5, prompt: 'Answer the following question as a short flashcard response. Do not include a label.' },
@@ -51,8 +56,38 @@ const callTypeMap: { [type: string]: GPTCallOpts } = {
model: 'gpt-4-turbo',
maxTokens: 1024,
temp: 0,
- prompt: 'List unique differences between the content of the UserAnswer and Rubric. Before each difference, label it and provide any additional information the UserAnswer missed and explain it in second person without separating it into UserAnswer and Rubric content and additional information. If there are no differences, say correct',
+ prompt: "BRIEFLY (<50 words) describe any differences between the rubric and the user's answer answer in second person. If there are no differences, say correct",
+ },
+
+ rubric: {
+ model: 'gpt-4-turbo',
+ maxTokens: 1024,
+ temp: 0,
+ prompt: "BRIEFLY (<25 words) provide a definition for the following term. It will be used as a rubric to evaluate the user's understanding of the topic",
+ },
+
+ type: {
+ model: 'gpt-4-turbo',
+ maxTokens: 1024,
+ temp: 0,
+ prompt: "I'm going to provide you with a question. Based on the question, is the user asking you to 1. Assigns docs with tags(like star / heart etc)/labels, 2. Filter docs, 3. Provide information about a specific doc 4. Provide a specific doc based on a question/information 5. Provide general information 6. Put cards in a specific order. Answer with only the number for 2-6. For number one, provide the number (1) and the appropriate tag",
},
+
+ subset: {
+ model: 'gpt-4-turbo',
+ maxTokens: 1024,
+ temp: 0,
+ prompt: "I'm going to give you a list of descriptions. Each one is separated by `======` on either side. Descriptions will vary in length, so make sure to only separate when you see `======`. Based on the question the user asks, provide a subset of the given descriptions that best matches the user's specifications. Make sure each description is only in the list once. Each item should be separated by `======`. Immediately afterward, surrounded by `------` on BOTH SIDES, provide some insight into your reasoning in the 2nd person (and mention nothing about the formatting details given in this description). It is VERY important that you format it exactly as described, ensuring the proper number of `=` and `-` (6 of each) and no commas"
+ },
+
+ info: {
+ model: 'gpt-4-turbo',
+ maxTokens: 1024,
+ temp: 0,
+ prompt: "Answer the user's question with a short (<100 word) response. If a particular document is selected I will provide that information (which may help with your response)"
+ },
+
+
};
let lastCall = '';
@@ -70,7 +105,7 @@ const gptAPICall = async (inputTextIn: string, callType: GPTCallType, prompt?: a
try {
lastCall = inputText;
- const usePrompt = prompt ? opts.prompt + prompt : opts.prompt;
+ const usePrompt = prompt ? prompt + opts.prompt : opts.prompt;
const messages: ChatCompletionMessageParam[] = [
{ role: 'system', content: usePrompt },
{ role: 'user', content: inputText },
diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index 89356072a..623a5251e 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -488,8 +488,14 @@ export class DocumentOptions {
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)
+ // cardSortForDropDown?: STRt = new StrInfo('needed for dropdown and i dont know why')
+ // cardSort_customField?: STRt = new StrInfo('field key used for sorting cards');
+ cardSort_activeIcons?: List<string>; //icons each card is tagges with
+ // cardSort_visibleSortGroups?: List<string>; // which sorting values are being filtered (shown)
+
+ // cardSort_visibleSortGroups?: List<number>; // which sorting values are being filtered (shown)
+ cardSort_isDesc?: BOOLt = new BoolInfo('whether the cards are sorted ascending or descending');
+ // test?: STRt = new StrInfo('testing for filtering')
}
export const DocOptions = new DocumentOptions();
diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts
index 2014c48e1..eee6be937 100644
--- a/src/client/util/CurrentUserUtils.ts
+++ b/src/client/util/CurrentUserUtils.ts
@@ -40,11 +40,12 @@ import { ColorScheme } from "./SettingsManager";
import { SnappingManager } from "./SnappingManager";
import { UndoManager } from "./UndoManager";
-interface Button {
+export interface Button {
// DocumentOptions fields a button can set
title?: string;
toolTip?: string;
icon?: string;
+ isSystem?: boolean;
btnType?: ButtonType;
numBtnMin?: number;
numBtnMax?: number;
@@ -673,30 +674,35 @@ pie title Minerals in my tap water
{ 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_);}'}},
+ { title: "Tags", icon:"bolt", toolTip:"Sort by document's tags", btnType: ButtonType.ToggleButton, expertMode: false, toolType:"tag", funcs: {}, scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}'}},
+ { title: "Pile", icon:"layer-group", toolTip:"View the cards as a pile in the free form view !", btnType: ButtonType.ClickButton, expertMode: false, toolType:"pile", funcs: {}, scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}'}},
+ { title: "Chat Popup", icon:"lightbulb", toolTip:"Toggle the chat popup's visibility!", width: 45 ,btnType: ButtonType.ToggleButton, expertMode: false, toolType:"toggle-chat", funcs: {}, scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}'} },
+
+ { title: "Sort", toolTip: "Manage sort order / lock status", icon: "sort" , btnType: ButtonType.MultiToggleButton, toolType:"alignment", ignoreClick: true,
+ subMenu: [
+ { title: "Ascending", toolTip: "Sort the cards in ascending order", btnType: ButtonType.ToggleButton, icon: "sort-up", toolType:"up", ignoreClick: true, scripts: {onClick: '{ return showFreeform(this.toolType, _readOnly_);}' }},
+ { title: "Descending", toolTip: "Sort the cards in descending order",btnType: ButtonType.ToggleButton, icon: "sort-down",toolType:"down",ignoreClick: true, 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: `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: `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: `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: `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 tagGroupTools(): Button[] {
+ if (!Doc.UserDoc().activeDashboard) {
+ Doc.UserDoc().myFilterHotKeyTitles = new List<string>(['Star', 'Heart', 'Bolt', 'Cloud']);
+
+ ['Star', 'Heart', 'Bolt', 'Cloud'].forEach(key => {
+ Doc.UserDoc()[key] = key.toLowerCase();
+ });
+}
+ // hack: if there's no dashboard, create default filters. otherwise, just make sure that the Options button is preserved
+ return (Doc.UserDoc().activeDashboard ? [] : [
+ { title: "Star", isSystem: false, icon: "star", toolTip:"Click to toggle the star group's visibility", btnType: ButtonType.ToggleButton, expertMode: false, toolType:"star", funcs: {}, scripts: { onClick: '{ return handleTags(this.toolType, _readOnly_);}'}},
+ { title: "Heart", isSystem: false,icon: "heart", toolTip:"Click to toggle the heart group's visibility", btnType: ButtonType.ToggleButton, expertMode: false, toolType:"heart", funcs: {}, scripts: { onClick: '{ return handleTags(this.toolType, _readOnly_);}'}},
+ { title: "Bolt", isSystem: false,icon: "bolt", toolTip:"Click to toggle the bolt group's visibility", btnType: ButtonType.ToggleButton, expertMode: false, toolType:"bolt", funcs: {}, scripts: { onClick: '{ return handleTags(this.toolType, _readOnly_);}'}},
+ { title: "Cloud", isSystem: false,icon: "cloud", toolTip:"Click to toggle the cloud group's visibility", btnType: ButtonType.ToggleButton, expertMode: false, toolType:"cloud", funcs: {}, scripts: { onClick: '{ return handleTags(this.toolType, _readOnly_);}'}},
+ ]).concat([
+ { title: "Options", isSystem: true,icon: "gear", toolTip:"Click to customize your filter panel", btnType: ButtonType.ClickButton, expertMode: false, toolType:"opts", funcs: {}, scripts: { onClick: '{ return handleTags(this.toolType, _readOnly_);}'}}
+ ])
}
static viewTools(): Button[] {
return [
@@ -799,25 +805,35 @@ pie title Minerals in my tap water
{ title: "Back", icon: "chevron-left", toolTip: "Prev Animation Frame", btnType: ButtonType.ClickButton, expertMode: true, toolType:CollectionViewType.Freeform, funcs: {hidden: '!SelectedDocType(this.toolType, this.expertMode)'}, width: 30, scripts: { onClick: 'prevKeyFrame(_readOnly_)'}},
{ title: "Num", icon:"", toolTip: "Frame # (click to toggle edit mode)",btnType: ButtonType.TextButton, expertMode: true, toolType:CollectionViewType.Freeform, funcs: {hidden: '!SelectedDocType(this.toolType, this.expertMode)', buttonText: 'selectedDocs()?.lastElement()?.currentFrame?.toString()'}, width: 20, scripts: { onClick: '{ return curKeyFrame(_readOnly_);}'}},
{ title: "Fwd", icon: "chevron-right", toolTip: "Next Animation Frame", btnType: ButtonType.ClickButton, expertMode: true, toolType:CollectionViewType.Freeform, funcs: {hidden: '!SelectedDocType(this.toolType, this.expertMode)'}, width: 30, scripts: { onClick: 'nextKeyFrame(_readOnly_)'}},
+
+
+
{ title: "Text", icon: "Text", toolTip: "Text functions", subMenu: CurrentUserUtils.textTools(), expertMode: false, toolType:DocumentType.RTF, funcs: { linearView_IsOpen: `SelectedDocType(this.toolType, this.expertMode)`} }, // Always available
{ title: "Ink", icon: "Ink", toolTip: "Ink functions", subMenu: CurrentUserUtils.inkTools(), expertMode: false, toolType:DocumentType.INK, funcs: {hidden: `IsExploreMode()`, linearView_IsOpen: `SelectedDocType(this.toolType, this.expertMode)`}, scripts: { onClick: 'setInkToolDefaults()'} }, // Always available
{ 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: "Card", icon: "Card", toolTip: "Card View Tools", 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: "Create", icon: "Create", 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
{ title: "Schema", icon: "Schema",linearBtnWidth:58,toolTip: "Schema functions",subMenu: CurrentUserUtils.schemaTools(),expertMode: false,toolType:CollectionViewType.Schema,funcs: {hidden: `!SelectedDocType(this.toolType, this.expertMode)`, linearView_IsOpen: `SelectedDocType(this.toolType, this.expertMode)`} }, // Only when Schema is selected
+ { title: "Filter", icon:"=", toolTip:"Filter cards by tags", btnType: ButtonType.MultiToggleButton, width: 150, ignoreClick: true,toolType:DocumentType.COL,
+ subMenu: this.tagGroupTools(), funcs: {hidden: '!SelectedDocType(this.toolType, this.expertMode)'},
+
+ },
];
+
}
/// initializes a context menu button for the top bar context menu
static setupContextMenuButton(params:Button, btnDoc?:Doc, btnContainer?:Doc) {
const reqdOpts:DocumentOptions = {
- ...OmitKeys(params, ["scripts", "funcs", "subMenu"]).omit as {[key:string]: string|undefined},
- color: Colors.WHITE, isSystem: true,
+ isSystem: true,
+ ...OmitKeys(params, ["scripts", "funcs", "subMenu"]).omit,
+ color: Colors.WHITE,
_nativeWidth: params.width ?? 30, _width: params.width ?? 30,
_height: 30, _nativeHeight: 30, linearBtnWidth: params.linearBtnWidth,
toolType: params.toolType, expertMode: params.expertMode,
@@ -986,6 +1002,7 @@ pie title Minerals in my tap water
doc.filterDocCount = 0;
doc.treeView_FreezeChildren = "remove|add";
doc.activePage = doc.activeDashboard === undefined ? 'home': doc.activePage;
+
this.setupLinkDocs(doc, linkDatabaseId);
this.setupSharedDocs(doc, sharingDocumentId); // sets up the right sidebar collection for mobile upload documents and sharing
this.setupDefaultIconTemplates(doc); // creates a set of icon templates triggered by the document deoration icon
@@ -1119,4 +1136,4 @@ ScriptingGlobals.add(function importDocument() { return CurrentUserUtils.import
// eslint-disable-next-line prefer-arrow-callback
ScriptingGlobals.add(function setInkToolDefaults() { Doc.ActiveTool = InkTool.None; });
// eslint-disable-next-line prefer-arrow-callback
-ScriptingGlobals.add(function getSharingDoc() { return Doc.SharingDoc() });
+ScriptingGlobals.add(function getSharingDoc() { return Doc.SharingDoc() }); \ No newline at end of file
diff --git a/src/client/util/Scripting.ts b/src/client/util/Scripting.ts
index c63d3d7cb..cb314e3f1 100644
--- a/src/client/util/Scripting.ts
+++ b/src/client/util/Scripting.ts
@@ -1,7 +1,7 @@
// export const ts = (window as any).ts;
// import * as typescriptlib from '!!raw-loader!../../../node_modules/typescript/lib/lib.d.ts'
// import * as typescriptes5 from '!!raw-loader!../../../node_modules/typescript/lib/lib.es5.d.ts'
-import typescriptlib from 'type_decls.d';
+// import typescriptlib from 'type_decls.d';
import * as ts from 'typescript';
import { Doc, FieldType } from '../../fields/Doc';
import { RefField } from '../../fields/RefField';
@@ -248,7 +248,7 @@ export function CompileScript(script: string, options: ScriptOptions = {}): Comp
const funcScript = `(function(${paramString})${reqTypes} { ${body} })`;
host.writeFile('file.ts', funcScript);
- if (typecheck) host.writeFile('node_modules/typescript/lib/lib.d.ts', typescriptlib);
+ // if (typecheck) host.writeFile('node_modules/typescript/lib/lib.d.ts', typescriptlib);
const program = ts.createProgram(['file.ts'], {}, host);
const testResult = program.emit();
const outputText = host.readFile('file.js');
diff --git a/src/client/views/DocumentButtonBar.scss b/src/client/views/DocumentButtonBar.scss
index 11614d627..ede277aae 100644
--- a/src/client/views/DocumentButtonBar.scss
+++ b/src/client/views/DocumentButtonBar.scss
@@ -29,6 +29,11 @@ $linkGap: 3px;
background: black;
height: 20px;
align-items: center;
+
+ .tags {
+ width: 40px;
+
+ }
}
.documentButtonBar-followTypes {
width: 20px;
@@ -153,6 +158,10 @@ $linkGap: 3px;
&:hover {
background-color: $black;
+
+ .documentButtonBar-pinTypes {
+ display: flex;
+ }
}
}
diff --git a/src/client/views/DocumentButtonBar.tsx b/src/client/views/DocumentButtonBar.tsx
index f14fd033b..ea859a1e5 100644
--- a/src/client/views/DocumentButtonBar.tsx
+++ b/src/client/views/DocumentButtonBar.tsx
@@ -265,24 +265,64 @@ export class DocumentButtonBar extends ObservableReactComponent<{ views: () => (
@computed
get keywordButton() {
+ const targetDoc = this.view0?.Document;
+
+ const metaBtn = (name: string, icon: IconProp) => {
+ const tooltip = `Toggle ${name}`;
+ return (
+ <Tooltip title={<div className="dash-tooltip">{tooltip}</div>}>
+ <div className="documentButtonBar-pinIcon">
+ <FontAwesomeIcon
+ className="documentdecorations-icon"
+ style={{ width: 20 }}
+ key={icon.toString()}
+ size="sm"
+ icon={icon}
+ onClick={e => {
+ // console.log('wtfff')
+ // name === 'tags' ??
+ if (name === 'tags'){
+ undoable(e => (targetDoc && (targetDoc[DocData].showIconTags = !targetDoc[DocData].showIconTags)), 'show icon tags')
+ } else {
+ undoable(e => {
+ const showing = DocumentView.Selected().some(dv => dv.layoutDoc._layout_showTags);
+ DocumentView.Selected().forEach(dv => {
+ dv.layoutDoc._layout_showTags = !showing;
+ if (e.shiftKey)
+ DocListCast(dv.Document[Doc.LayoutFieldKey(dv.Document) + '_annotations']).forEach(doc => {
+ if (doc.face) doc.hidden = showing;
+ });
+ });
+ }, 'show Doc tags')
+ }
+
+
+
+
+ }}
+ />
+ </div>
+ </Tooltip>
+ );
+ };
+
+
+
+
return !DocumentView.Selected().length ? null : (
- <Tooltip title={<div className="dash-keyword-button">Open keyword menu</div>}>
- <div
- className="documentButtonBar-icon"
- style={{ color: 'white' }}
- onClick={undoable(e => {
- const showing = DocumentView.Selected().some(dv => dv.layoutDoc._layout_showTags);
- DocumentView.Selected().forEach(dv => {
- dv.layoutDoc._layout_showTags = !showing;
- if (e.shiftKey)
- DocListCast(dv.Document[Doc.LayoutFieldKey(dv.Document) + '_annotations']).forEach(doc => {
- if (doc.face) doc.hidden = showing;
- });
- });
- }, 'show Doc tags')}>
+ <div className='documentButtonBar-icon'>
+ <div className="documentButtonBar-pinTypes" style = {{width: '40px'}}>
+ {metaBtn('tags', 'star')}
+ {metaBtn("keywords", 'id-card')}
+ </div>
+
+ <Tooltip title={<div className="dash-keyword-button">Open keyword / icon tag menu</div>}>
+ <div className="documentButtonBar-icon" style={{ color: 'white' }}>
<FontAwesomeIcon className="documentdecorations-icon" icon="tag" />
</div>
</Tooltip>
+ </div>
+
);
}
diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index 5e7908725..45ce681aa 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -708,6 +708,7 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora
const maxDist = Math.min((this.Bounds.r - this.Bounds.x) / 2, (this.Bounds.b - this.Bounds.y) / 2);
const radiusHandle = (borderRadius / docMax) * maxDist;
const radiusHandleLocation = Math.min(radiusHandle, maxDist);
+
const sharingMenu =
Doc.IsSharingEnabled && docShareMode ? (
@@ -770,6 +771,12 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora
const centery = hideTitle ? 0 : this._titleHeight;
const transformOrigin = `${50}% calc(50% + ${centery / 2}px)`;
const freeformDoc = DocumentView.Selected().some(v => CollectionFreeFormDocumentView.from(v));
+
+
+ const keyWordTrans = doc[DocData].showLabels ? NumCast(doc[DocData].keywordHeight) : 0
+ const tagTrans = doc[DocData].showIconTags ? NumCast(doc[DocData].tagHeight) : 0
+
+
return (
<div className="documentDecorations" style={{ display: this._showNothing && !freeformDoc ? 'none' : undefined }}>
<div
diff --git a/src/client/views/FilterPanel.scss b/src/client/views/FilterPanel.scss
index d6d2956aa..fbdebde0d 100644
--- a/src/client/views/FilterPanel.scss
+++ b/src/client/views/FilterPanel.scss
@@ -1,3 +1,4 @@
+
.filterBox-flyout {
display: block;
text-align: left;
@@ -228,6 +229,128 @@
vertical-align: middle;
}
+.filterHotKey-button {
+ pointer-events: auto;
+ // padding-right: 8px; //5px;
+ width: 100%; //width: 25px;
+ border-radius: 5px;
+ // margin-right: 20px;
+ margin-top: 8px;
+ border-color: #d3d3d3;
+ border-style: solid;
+ border-width: thin;
+ transition: all 0.3s ease-out;
+ display: flex;
+ flex-direction: row;
+ padding: 5px; /* Adjust the padding value as needed */
+
+
+ &:hover{
+ border-color: #e9e9e9;
+ background-color: #6d6c6c
+ }
+
+
+
+ // &.active {
+ // background-color: #6d6c6c;
+
+
+
+ // .icon-panel{
+ // display: flex
+ // }
+ // }
+
+ .hotKey-icon, .hotKey-close{
+ background-color: transparent;
+ border-radius: 10%;
+ padding: 5px;
+
+
+ &:hover{
+ background-color: #616060;
+ }
+ }
+
+ .hotKey-close{
+ right: 30px;
+ position: fixed;
+ padding-top: 10px;
+
+}
+
+ .hotkey-title{
+ top: 6px;
+ position: relative;
+ cursor: text;
+
+ }
+
+ .hotkey-title-input{
+ background-color: transparent;
+ border: none;
+ border-color: transparent;
+ outline: none;
+ cursor: text;
+
+ }
+}
+
+.hotKeyButtons {
+ position: relative;
+ width: 100%;
+ // margin-top: 3px;
+ // // grid-column: 1/4;
+ // width: 100%;
+ // height: auto;
+ // display: flex;
+ // // flex-direction: row;
+ // // flex-wrap: wrap;
+ // padding-bottom: 5.5px;
+}
+
+.hotKey-icon-button {
+ // pointer-events: auto; /* Re-enable pointer events for the buttons */
+
+ // width: 30px;
+ // height: 30px;
+ // border-color: $medium-blue;
+ background-color: transparent;
+ &.active{
+
+
+ }
+
+}
+
+.icon-panel {
+ // bottom: -14px;
+ position: absolute;
+ z-index: 10000;
+ // background-color: #616060;
+ // background-color: #323232;
+ border-color: black;
+ border-style: solid;
+ border-width: medium;
+ border-radius: 10%;
+ background-color: #323232;
+
+ .icon-panel-button{
+ background-color: #323232;
+ border-radius: 10%;
+
+
+ &:hover{
+ background-color:#7a7878
+ }
+ }
+
+
+
+}
+
+
// .sliderBox-outerDiv {
// width: 30%;// width: calc(100% - 14px); // 14px accounts for handles that are at the max value of the slider that would extend outside the box
// height: 40; // height: 100%;
diff --git a/src/client/views/FilterPanel.tsx b/src/client/views/FilterPanel.tsx
index 2f6d1fbaa..f3f447e22 100644
--- a/src/client/views/FilterPanel.tsx
+++ b/src/client/views/FilterPanel.tsx
@@ -11,12 +11,24 @@ import { List } from '../../fields/List';
import { RichTextField } from '../../fields/RichTextField';
import { SearchUtil } from '../util/SearchUtil';
import { SnappingManager } from '../util/SnappingManager';
-import { undoable } from '../util/UndoManager';
+import { undoable, undoBatch } from '../util/UndoManager';
import { FieldsDropdown } from './FieldsDropdown';
import './FilterPanel.scss';
import { DocumentView } from './nodes/DocumentView';
import { Handle, Tick, TooltipRail, Track } from './nodes/SliderBox-components';
import { ObservableReactComponent } from './ObservableReactComponent';
+import { Button } from '../util/CurrentUserUtils';
+import { ButtonType } from './nodes/FontIconBox/FontIconBox';
+import { DocCast } from '../../fields/Types';
+import { CurrentUserUtils } from '../util/CurrentUserUtils';
+import { StrCast } from '../../fields/Types';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { DocData } from '../../fields/DocSymbols';
+import { Tooltip } from '@mui/material';
+import { useLocalObservable } from 'mobx-react';
+import { useRef } from 'react';
+import { useEffect } from 'react';
+
interface filterProps {
Document: Doc;
@@ -25,10 +37,13 @@ interface filterProps {
@observer
export class FilterPanel extends ObservableReactComponent<filterProps> {
@observable _selectedFacetHeaders = new Set<string>();
+ public static Instance: FilterPanel;
+
constructor(props: filterProps) {
super(props);
makeObservable(this);
+ FilterPanel.Instance = this;
}
/**
@@ -211,12 +226,67 @@ export class FilterPanel extends ObservableReactComponent<filterProps> {
return nonNumbers / facetValues.length > 0.1 ? facetValues.sort() : facetValues.sort((n1: string, n2: string) => Number(n1) - Number(n2));
};
+ /**
+ * Allows users to add a filter hotkey to the properties panel. Will also update the multitoggle at the top menu and the
+ * icontags tht are displayed on the documents themselves
+ * @param hotKey tite of the new hotkey
+ */
+ addHotkey = (hotKey: string) => {
+ const buttons = DocCast(Doc.UserDoc().myContextMenuBtns);
+ const filter = DocCast(buttons.Filter);
+
+ const newKey: Button = {
+ title: hotKey,
+ icon: 'bolt',
+ toolTip: `Click to toggle the ${hotKey}'s group's visibility`,
+ btnType: ButtonType.ToggleButton,
+ expertMode: false,
+ toolType: 'bolt',
+ funcs: {},
+ scripts: { onClick: '{ return handleTags(this.toolType, _readOnly_);}' },
+ };
+
+ const currHotKeys = StrListCast(Doc.UserDoc().myFilterHotKeyTitles)
+
+ Doc.UserDoc().myFilterHotKeyTitles = new List<string>(currHotKeys.concat(hotKey))
+
+ Doc.UserDoc()[hotKey] = 'bolt'
+
+ const newBtn = CurrentUserUtils.setupContextMenuBtn(newKey, filter);
+ newBtn.isSystem = newBtn[DocData].isSystem = undefined;
+
+ const subDocs = DocListCast(filter.data)
+ const opts = subDocs[subDocs.length-1]
+ Doc.AddDocToList(filter, 'data', newBtn, opts, true);
+ }
+
+
+ /**
+ * Renders the newly formed hotkey icon buttons
+ * @returns the buttons to be rendered
+ */
+ hotKeyButtons = () => {
+ const selected = DocumentView.SelectedDocs().lastElement();
+ const hotKeys = StrListCast(Doc.UserDoc().myFilterHotKeyTitles);
+
+ // Selecting a button should make it so that the icon on the top filter panel becomes said icon
+ const buttons = hotKeys.map((hotKey, i) => (
+ <Tooltip key={hotKey} title={<div className="dash-tooltip">Click to customize this hotkey's icon</div>}>
+ <HotKeyIconButton hotKey={hotKey} selected = {selected}/>
+ </Tooltip>
+ ));
+
+ return buttons;
+ }
+
+ // @observable iconPanelMap: Map<string, number> = new Map();
+
render() {
return (
<div className="filterBox-treeView">
<div className="filterBox-select">
<div style={{ width: '100%' }}>
- <FieldsDropdown Document={this.Document} selectFunc={this.facetClick} showPlaceholder placeholder="add a filter" addedFields={['acl_Guest', LinkedTo]} />
+ <FieldsDropdown Document={this.Document} selectFunc={this.facetClick} showPlaceholder placeholder="add a filter" addedFields={['acl_Guest', LinkedTo, 'Star', 'Heart', 'Bolt', 'Cloud']} />
</div>
{/* THE FOLLOWING CODE SHOULD BE DEVELOPER FOR BOOLEAN EXPRESSION (AND / OR) */}
{/* <div className="filterBox-select-bool">
@@ -277,6 +347,15 @@ export class FilterPanel extends ObservableReactComponent<filterProps> {
)
)}
</div>
+ <div>
+ <div className="filterBox-select">
+ <div style={{ width: '100%' }}>
+ <FieldsDropdown Document={this.Document} selectFunc={this.addHotkey} showPlaceholder placeholder="add a hotkey" addedFields={['acl_Guest', LinkedTo]} />
+ </div>
+ </div>
+ </div>
+
+ <div>{this.hotKeyButtons()}</div>
</div>
);
}
@@ -385,3 +464,182 @@ export class FilterPanel extends ObservableReactComponent<filterProps> {
return undefined;
}
}
+
+
+
+interface HotKeyButtonProps {
+ hotKey: string;
+ selected?: Doc
+}
+
+/**
+ * Renders the buttons that correspond to each icon tag in the properties view. Allows users to change the icon,
+ * title, and delete.
+ */
+const HotKeyIconButton: React.FC<HotKeyButtonProps> = observer(({ hotKey, selected}) => {
+ const state = useLocalObservable(() => ({
+ isActive: false,
+ isEditing: false,
+ myHotKey: hotKey,
+
+ toggleActive() {
+ this.isActive = !this.isActive;
+ },
+ deactivate() {
+ this.isActive = false;
+ },
+ startEditing() {
+ this.isEditing = true;
+ },
+ stopEditing() {
+ this.isEditing = false;
+ },
+ setHotKey(newHotKey: string) {
+ this.myHotKey = newHotKey;
+ }
+ }));
+
+ const panelRef = useRef<HTMLDivElement>(null);
+ const inputRef = useRef<HTMLInputElement>(null);
+
+ const handleClick = () => {
+ state.toggleActive();
+ };
+
+ const hotKeys = StrListCast(Doc.UserDoc().myFilterHotKeyTitles)
+ const buttons = DocCast(Doc.UserDoc().myContextMenuBtns);
+ const filter = DocCast(buttons.Filter);
+
+ /**
+ * The doc of the button in the context menu that corresponds to the current hotkey
+ * @returns
+ */
+ const myHotKeyDoc = () => {
+ const hotKeyDocs = DocListCast(filter.data)
+ return hotKeyDocs.filter(k => StrCast(k.title) === hotKey)[0]
+
+ }
+
+ /**
+ * Removes a hotkey from list
+ */
+ const removeHotKey = () => {
+ Doc.RemoveDocFromList(filter, 'data', myHotKeyDoc());
+
+ }
+
+ /**
+ * Deselects if the user clicks outside the button
+ * @param event
+ */
+ const handleClickOutside = (event: MouseEvent) => {
+ if (panelRef.current && !panelRef.current.contains(event.target as Node)) {
+ state.deactivate();
+ if (state.isEditing) {
+ state.stopEditing();
+
+ updateFromInput()
+ }
+ }
+ };
+
+ /**
+ * Updates the list of hotkeys based on the users input. replaces the old title with the new one and then assigns this new
+ * hotkey with the current icon
+ */
+ const updateFromInput = undoable (() => {
+ const myDoc = myHotKeyDoc()
+ Doc.UserDoc().myFilterHotKeyTitles = new List<string>(hotKeys.map(k => k === hotKey ? state.myHotKey : k));
+ Doc.UserDoc()[state.myHotKey] = StrCast(Doc.UserDoc()[hotKey])
+ Doc.UserDoc()[hotKey] = ''
+ myDoc.title = state.myHotKey
+ myDoc.toolTip = `Click to toggle the ${state.myHotKey}'s group's visibility`
+ }, '')
+
+
+ useEffect(() => {
+ document.addEventListener('mousedown', handleClickOutside);
+ return () => {
+ document.removeEventListener('mousedown', handleClickOutside);
+ };
+ }, []);
+
+ const iconOpts = ['star', 'heart', 'bolt', 'satellite', 'palette', 'robot', 'lightbulb', 'highlighter', 'book', 'chalkboard' ];
+
+ /**
+ * Panel of icons the user can choose from to represent their tag
+ */
+ const iconPanel = iconOpts.map((icon, i) => (
+ <button key={i} onClick={undoable((e: React.MouseEvent) => {
+ e.stopPropagation;
+ Doc.UserDoc()[hotKey] = icon;
+ myHotKeyDoc().icon = icon
+ }, '')} className='icon-panel-button'>
+ <FontAwesomeIcon icon={icon as any} color = {SnappingManager.userColor}/>
+ </button>
+ ));
+
+ /**
+ * Actually renders the buttons
+ */
+
+ return (
+ <div className={`filterHotKey-button`}
+ onClick={(e) => {
+ e.stopPropagation();
+ state.startEditing();
+ setTimeout(() => inputRef.current?.focus(), 0);
+ }}
+ >
+ <div className={`hotKey-icon-button ${state.isActive ? 'active' : ''}`} ref={panelRef}>
+ <Tooltip title={<div className="dash-tooltip">Click to customize this hotkey's icon</div>}>
+ <button
+ type="button"
+ className='hotKey-icon'
+ onClick={(e: React.MouseEvent) => {
+ e.stopPropagation();
+ handleClick();
+ }}
+ >
+ <FontAwesomeIcon icon={Doc.UserDoc()[hotKey] as any} size="2xl" color={SnappingManager.userColor}/>
+ </button>
+ </Tooltip>
+ {state.isActive && (
+ <div className="icon-panel">
+ {iconPanel}
+ </div>
+ )}
+ </div>
+ {state.isEditing ? (
+ <input
+ ref={inputRef}
+ type="text"
+ value={state.myHotKey.toUpperCase()}
+ onChange={(e) => state.setHotKey(e.target.value)}
+ onBlur={() => {
+ state.stopEditing();
+ updateFromInput()
+ }}
+ onKeyDown={(e) => {
+ if (e.key === 'Enter') {
+ state.stopEditing();
+ updateFromInput()
+
+ }
+ }}
+ className='hotkey-title-input'
+ />
+ ) : (
+ <p className='hotkey-title'>{hotKey.toUpperCase()}</p>
+ )}
+ <button className='hotKey-close' onClick={(e: React.MouseEvent) => {
+ e.stopPropagation();
+ Doc.UserDoc().myFilterHotKeyTitles = new List<string>(hotKeys.filter(k => k !== hotKey));
+ removeHotKey()
+
+ }}>
+ <FontAwesomeIcon icon={'x' as any} color={SnappingManager.userColor}/>
+ </button>
+ </div>
+ );
+}) \ No newline at end of file
diff --git a/src/client/views/InkingStroke.tsx b/src/client/views/InkingStroke.tsx
index 2e82371cb..067b11310 100644
--- a/src/client/views/InkingStroke.tsx
+++ b/src/client/views/InkingStroke.tsx
@@ -450,6 +450,13 @@ export class InkingStroke extends ViewBoxAnnotatableComponent<FieldViewProps>()
}),
icon: 'paint-brush',
});
+ // cm?.addItem({
+ // description: 'Create a hotkey',
+ // event: action(() => {
+ // InkStrokeProperties.Instance._controlButton = !InkStrokeProperties.Instance._controlButton;
+ // }),
+ // icon: 'satellite',
+ // });
},
};
return (
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index 393abea53..74a788d94 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -87,7 +87,7 @@ export class MainView extends ObservableReactComponent<object> {
@observable private _windowWidth: number = 0;
@observable private _windowHeight: number = 0;
- @observable private _dashUIWidth: number = 0; // width of entire main dashboard region including left menu buttons and properties panel (but not including the dashboard selector button row)
+ @observable _dashUIWidth: number = 0; // width of entire main dashboard region including left menu buttons and properties panel (but not including the dashboard selector button row)
@observable private _dashUIHeight: number = 0; // height of entire main dashboard region including top menu buttons
@observable private _panelContent: string = 'none';
@observable private _sidebarContent: Doc = Doc.MyLeftSidebarPanel;
@@ -546,6 +546,10 @@ export class MainView extends ObservableReactComponent<object> {
fa.faRobot,
fa.faSatellite,
fa.faStar,
+ fa.faCloud,
+ fa.faBolt,
+ fa.faLightbulb,
+ fa.faX
]
);
}
diff --git a/src/client/views/PropertiesView.tsx b/src/client/views/PropertiesView.tsx
index daa8e1720..e940ba6f9 100644
--- a/src/client/views/PropertiesView.tsx
+++ b/src/client/views/PropertiesView.tsx
@@ -54,7 +54,7 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
private _widthUndo?: UndoManager.Batch;
// eslint-disable-next-line no-use-before-define
- public static Instance: PropertiesView | undefined;
+ public static Instance: PropertiesView;
constructor(props: PropertiesViewProps) {
super(props);
makeObservable(this);
diff --git a/src/client/views/StyleProvider.tsx b/src/client/views/StyleProvider.tsx
index 262f888fb..cda39b474 100644
--- a/src/client/views/StyleProvider.tsx
+++ b/src/client/views/StyleProvider.tsx
@@ -25,6 +25,8 @@ import { DocumentView, DocumentViewProps } from './nodes/DocumentView';
import { FieldViewProps } from './nodes/FieldView';
import { StyleProp } from './StyleProp';
import './StyleProvider.scss';
+import { IconTagBox } from './nodes/IconTagBox';
+import { DocData } from '../../fields/DocSymbols';
function toggleLockedPosition(doc: Doc) {
UndoManager.RunInBatch(() => Doc.toggleLockedPosition(doc), 'toggleBackground');
@@ -365,7 +367,13 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<FieldViewProps &
</Tooltip>
);
};
- const tags = () => props?.DocumentView?.() ? <TagsView Views={[props.DocumentView()]}/> : null;
+ const tags = () => props?.DocumentView?.() && CollectionFreeFormDocumentView.from(props.DocumentView()) ? <TagsView View={props.DocumentView()}/> : null;
+
+ const iconTags = () => {
+ if (doc && doc![DocData].showIconTags)
+ {return (<IconTagBox doc= {doc}></IconTagBox>)}
+ }
+
return (
<>
{paint()}
@@ -373,6 +381,7 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<FieldViewProps &
{filter()}
{audio()}
{tags()}
+ {iconTags()}
</>
);
}
diff --git a/src/client/views/collections/CollectionCardDeckView.scss b/src/client/views/collections/CollectionCardDeckView.scss
index cc797d0bd..1728f7aee 100644
--- a/src/client/views/collections/CollectionCardDeckView.scss
+++ b/src/client/views/collections/CollectionCardDeckView.scss
@@ -31,18 +31,45 @@
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 */
+// .card-button-container {
+// display: flex;
+// padding: 3px;
+// // width: 300px;
+// // height:100px;
+// pointer-events: none; /* This ensures the container does not capture hover events */
+
+// background-color: rgb(218, 218, 218); /* Background color of the container */
+// border-radius: 50px; /* Rounds the corners of the container */
+// transform: translateY(25px);
+// // 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 {
+// pointer-events: auto; /* Re-enable pointer events for the buttons */
+
+// width: 70px;
+// height: 70px;
+// border-radius: 50%;
+// background-color: $dark-gray;
+// // border-color: $medium-blue;
+// margin: 5px; // transform: translateY(-50px);
+// background-color: transparent;
+// }
+// }
+
+.no-card-span{
+ position: relative;
+ width: fit-content;
+ text-align: center;
+ font-size: 65px;
+
+
+
}
+
+
// button:hover {
// transform: translateY(-50px);
// }
@@ -74,11 +101,18 @@
flex-direction: column;
}
+// .card-item:hover {
+// box-shadow: 0 20px 20px $medium-blue;
+// transform: scale(1.05);
+
+
+// }
+
.card-item-inactive {
opacity: 0.5;
}
.card-item-active {
- position: absolute;
+ // position: absolute;
z-index: 100;
}
diff --git a/src/client/views/collections/CollectionCardDeckView.tsx b/src/client/views/collections/CollectionCardDeckView.tsx
index 28a769896..3d9f28a49 100644
--- a/src/client/views/collections/CollectionCardDeckView.tsx
+++ b/src/client/views/collections/CollectionCardDeckView.tsx
@@ -2,8 +2,8 @@ import { IReactionDisposer, ObservableMap, action, computed, makeObservable, obs
import { observer } from 'mobx-react';
import * as React from 'react';
import { ClientUtils, DashColor, returnFalse, returnZero } from '../../../ClientUtils';
-import { numberRange } from '../../../Utils';
-import { Doc, NumListCast } from '../../../fields/Doc';
+import { emptyFunction, numberRange } from '../../../Utils';
+import { Doc, NumListCast, StrListCast } from '../../../fields/Doc';
import { DocData } from '../../../fields/DocSymbols';
import { Id } from '../../../fields/FieldSymbols';
import { BoolCast, Cast, DateCast, NumCast, RTFCast, ScriptCast, StrCast } from '../../../fields/Types';
@@ -18,15 +18,30 @@ import { StyleProp } from '../StyleProp';
import { DocumentView } from '../nodes/DocumentView';
import { GPTPopup, GPTPopupMode } from '../pdf/GPTPopup/GPTPopup';
import './CollectionCardDeckView.scss';
-import { CollectionSubView, SubCollectionViewProps } from './CollectionSubView';
+import { CollectionSubView } from './CollectionSubView';
+import { dropActionType } from '../../util/DropActionTypes';
+import { DocCast } from '../../../fields/Types';
+import { SelectionManager } from '../../util/SelectionManager';
+
+
enum cardSortings {
Time = 'time',
Type = 'type',
Color = 'color',
Custom = 'custom',
+ Chat = 'chat',
+ Tag = 'tag',
None = '',
}
+
+/**
+ * New view type specifically for studying more dynamically. Allows you to reorder docs however you see fit, easily
+ * sort and filter using presets, and customize your experience with chat gpt.
+ *
+ * This file contains code as to how the docs are to be rendered (there place geographically and also in regards to sorting),
+ * and callback functions for the gpt popup
+ */
@observer
export class CollectionCardView extends CollectionSubView() {
private _dropDisposer?: DragManager.DragDropDisposer;
@@ -35,15 +50,14 @@ export class CollectionCardView extends CollectionSubView() {
private _textToDoc = new Map<string, Doc>();
@observable _forceChildXf = false;
- @observable _isLoading = false;
@observable _hoveredNodeIndex = -1;
@observable _docRefs = new ObservableMap<Doc, DocumentView>();
@observable _maxRowCount = 10;
+ @observable _docDraggedIndex: number = -1;
+ @observable _isACardBeingDragged: boolean = false;
+ @observable overIndex: number = -1;
- 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);
@@ -61,22 +75,46 @@ export class CollectionCardView extends CollectionSubView() {
}
};
- protected createDashEventsTarget = (ele: HTMLDivElement | null) => {
- this._dropDisposer?.();
- if (ele) {
- this._dropDisposer = DragManager.MakeDropTarget(ele, this.onInternalDrop.bind(this), this.layoutDoc);
- }
- };
-
- constructor(props: SubCollectionViewProps) {
+ constructor(props: any) {
super(props);
makeObservable(this);
+ this.setRegenerateCallback();
}
+ /**
+ * Callback to ensure gpt's text versions of the child docs are updated
+ */
+ setRegenerateCallback = () => {
+ GPTPopup.Instance.setRegenerateCallback(this.childPairStringListAndUpdateSortDesc);
+ }
+
+ /**
+ * update's gpt's doc-text list and initializes callbacks
+ */
+ @action
+ childPairStringListAndUpdateSortDesc = async () => {
+ const sortDesc = await this.childPairStringList(); // Await the promise to get the string result
+ GPTPopup.Instance.setSortDesc(sortDesc.join());
+ GPTPopup.Instance.onSortComplete = (sortResult: string, questionType: string, tag?: string) => this.processGptOutput(sortResult, questionType, tag);
+ GPTPopup.Instance.onQuizRandom = () => this.quizMode();
+ };
+
componentDidMount(): void {
+ this.Document.childFilters_boolean = 'OR';
+ this.childDocsWithoutLinks.forEach(c => {
+ c[DocData].showIconTags = true;
+ });
+
+ // Reaction to cardSort changes
this._disposers.sort = reaction(
- () => ({ cardSort: this.cardSort, field: this.cardSort_customField }),
- ({ cardSort, field }) => (cardSort === cardSortings.Custom && field === 'chat' ? this.openChatPopup() : GPTPopup.Instance.setVisible(false))
+ () => GPTPopup.Instance.visible,
+ isVis => {
+ if (isVis) {
+ this.openChatPopup();
+ } else {
+ this.Document.cardSort = (this.cardSort === cardSortings.Chat) ? '' : this.Document.cardSort;
+ }
+ }
);
}
@@ -92,6 +130,7 @@ export class CollectionCardView extends CollectionSubView() {
@computed get cardSort() {
return StrCast(this.Document.cardSort) as cardSortings;
}
+
/**
* how much to scale down the contents of the view so that everything will fit
*/
@@ -100,29 +139,18 @@ export class CollectionCardView extends CollectionSubView() {
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);
+ const activeGroups = StrListCast(this.Document.cardSort_visibleSortGroups);
- if (activeGroups.length > 0 && this.cardSort === cardSortings.Custom) {
+ if (activeGroups.length > 0) {
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);
+ const activeTags = StrListCast(doc.cardSort_activeIcons);
+ return activeTags !== undefined && activeTags.some(tag => activeGroups.includes(tag));
});
}
@@ -131,12 +159,22 @@ export class CollectionCardView extends CollectionSubView() {
}
/**
- * Determines the order in which the cards will be rendered depending on the current sort type
+ * When in quiz mode, randomly selects a document
*/
- @computed get sortedDocs() {
- return this.sort(this.childDocsWithoutLinks, this.cardSort, BoolCast(this.layoutDoc.sortDesc));
+ quizMode = () => {
+ const randomIndex = Math.floor(Math.random() * this.childDocs.length);
+ SelectionManager.DeselectAll();
+ DocumentView.SelectView(DocumentView.getDocumentView(this.childDocs[randomIndex]), false);
}
+ /**
+ * Number of rows of cards to be rendered
+ */
+ @computed get numRows() {
+ return Math.ceil(this.sortedDocs.length / 10);
+ }
+
+
@action
setHoveredNodeIndex = (index: number) => {
if (!DocumentView.SelectedDocs().includes(this.childDocs[index])) {
@@ -170,6 +208,8 @@ export class CollectionCardView extends CollectionSubView() {
* @returns
*/
rotate = (amCards: number, index: number) => {
+ if (amCards == 1) return 0;
+
const possRotate = -30 + index * (30 / ((amCards - (amCards % 2)) / 2));
const stepMag = Math.abs(-30 + (amCards / 2 - 1) * (30 / ((amCards - (amCards % 2)) / 2)));
@@ -205,27 +245,120 @@ export class CollectionCardView extends CollectionSubView() {
};
/**
- * Translates the selected node to the middle fo the screen
- * @param index
- * @returns
+ * When dragging a card, determines the index the card should be set to if dropped
+ * @param mouseX mouse's x location
+ * @param mouseY mouses' y location
+ * @returns the card's new index
+ */
+ findCardDropIndex = (mouseX: number, mouseY: number) => {
+ const amCardsTotal = this.sortedDocs.length;
+ let index = 0;
+ const cardWidth = amCardsTotal < this._maxRowCount ? this._props.PanelWidth() / amCardsTotal : this._props.PanelWidth() / this._maxRowCount;
+
+ // Calculate the adjusted X position accounting for the initial offset
+ let adjustedX = mouseX;
+
+ const amRows = Math.ceil(amCardsTotal / this._maxRowCount);
+ const rowHeight = this._props.PanelHeight() / amRows;
+ const currRow = Math.floor((mouseY - 100) / rowHeight); //rows start at 0
+
+ if (adjustedX < 0) {
+ return 0; // Before the first column
+ }
+
+ if (amCardsTotal < this._maxRowCount) {
+ index = Math.floor(adjustedX / cardWidth);
+ } else if (currRow != amRows - 1) {
+ index = Math.floor(adjustedX / cardWidth) + currRow * this._maxRowCount;
+ } else {
+ const rowAmCards = amCardsTotal - currRow * this._maxRowCount;
+ const offset = ((this._maxRowCount - rowAmCards) / 2) * cardWidth;
+ adjustedX = mouseX - offset;
+
+ index = Math.floor(adjustedX / cardWidth) + currRow * this._maxRowCount;
+ }
+ return index;
+ };
+
+ /**
+ * Checks to see if a card is being dragged and calls the appropriate methods if so
+ * @param e the current pointer event
*/
- 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;
+ @action
+ onPointerMove = (e: React.PointerEvent<HTMLDivElement>) => {
+ if (DragManager.docsBeingDragged.length != 0) {
+ this._isACardBeingDragged = true;
- // Calculate the translation needed to align the scaled node's center with the panel's center
- const translation = middleOfPanel - scaledNodeCenter - scaledNodeWidth - scaledNodeWidth / 4;
+ const newIndex = this.findCardDropIndex(e.clientX, e.clientY);
- return translation;
+ if (newIndex !== this._docDraggedIndex && newIndex != -1) {
+ this._docDraggedIndex = newIndex;
+ }
+ }
};
/**
+ * Resets all the doc dragging vairables once a card is dropped
+ * @param e
+ * @param de drop event
+ * @returns true if a card has been dropped, falls if not
+ */
+ onInternalDrop = undoable((e: Event, de: DragManager.DropEvent) => {
+ if (de.complete.docDragData) {
+ this._isACardBeingDragged = false;
+ this._docDraggedIndex = -1;
+ e.stopPropagation();
+ return true;
+ }
+ return false;
+ }, '' );
+
+ get sortedDocs() {
+ return this.sort(this.childDocsWithoutLinks, this.cardSort, BoolCast(this.Document.cardSort_isDesc), this._docDraggedIndex);
+ }
+
+
+ /**
+ * Used to determine how to sort cards based on tags. The lestmost tags are given lower values while cards to the right are
+ * given higher values. Decimals are used to determine placement for cards with multiple tags
+ * @param doc the doc whose value is being determined
+ * @returns its value based on its tags
+ */
+
+ tagValue = (doc: Doc) => {
+ const keys = StrListCast(Doc.UserDoc().myFilterHotKeyTitles);
+
+ const isTagActive = (buttonID: number) => {
+ return BoolCast(doc[StrCast(Doc.UserDoc()[keys[buttonID]])]);
+ };
+
+ let base = '';
+ let fraction = '';
+
+ for (let i = 0; i < keys.length; i++) {
+ if (isTagActive(i)) {
+ if (base === '') {
+ base = i.toString(); // First active tag becomes the base
+ } else {
+ fraction += i.toString(); // Subsequent active tags become part of the fraction
+ }
+ }
+ }
+
+ // If no tag was active, return 0 by default
+ if (base === '') {
+ return 0;
+ }
+
+ // Construct the final number by appending the fraction if it exists
+ const numberString = fraction ? `${base}.${fraction}` : base;
+
+ // Convert the result to a number and return
+ return Number(numberString);
+ }
+
+ /**
* 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
@@ -233,28 +366,39 @@ export class CollectionCardView extends CollectionSubView() {
* @param isDesc
* @returns
*/
- sort = (docs: Doc[], sortType: cardSortings, isDesc: boolean) => {
- if (sortType === cardSortings.None) return docs;
+ @action sort = (docs: Doc[], sortType: cardSortings, isDesc: boolean, dragIndex: number) => {
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
+ case cardSortings.Time:
+ return [DateCast(docA.author_date)?.date ?? Date.now(), DateCast(docB.author_date)?.date ?? Date.now()];
+ case cardSortings.Color:
+ return [ClientUtils.hexToHsv(StrCast(docA.backgroundColor)), ClientUtils.hexToHsv(StrCast(docB.backgroundColor))];
+ case cardSortings.Tag:
+ return [this.tagValue(docA) ?? 9999, this.tagValue(docB) ?? 9999];
+ case cardSortings.Chat:
+ return [NumCast(docA.chatIndex) ?? 9999, NumCast(docB.chatIndex) ?? 9999];
+
+ default:
+ return [StrCast(docA.type), StrCast(docB.type)];
+ }
})();
const out = typeA < typeB ? -1 : typeA > typeB ? 1 : 0;
- return isDesc ? -out : out; // Reverse the sort order if descending is true
+
+ if (isDesc) {
+ return out;
+ }
+
+ return -out;
});
+ if (dragIndex != -1) {
+ const draggedDoc = DragManager.docsBeingDragged[0];
+ const originalIndex = docs.findIndex(doc => doc === draggedDoc);
+
+ docs.splice(originalIndex, 1);
+ docs.splice(dragIndex, 0, draggedDoc);
+ }
return docs;
};
@@ -273,10 +417,12 @@ export class CollectionCardView extends CollectionSubView() {
LayoutTemplate={this._props.childLayoutTemplate}
LayoutTemplateString={this._props.childLayoutString}
ScreenToLocalTransform={screenToLocalTransform} // makes sure the box wrapper thing is in the right spot
- isContentActive={this.isChildContentActive}
+ isContentActive={emptyFunction}
isDocumentActive={this._props.childDocumentsActive?.() || this.Document._childDocumentsActive ? this._props.isDocumentActive : this.isContentActive}
PanelWidth={this.panelWidth}
PanelHeight={this.panelHeight(doc)}
+ dragAction={(this.Document.childDragAction ?? this._props.childDragAction) as dropActionType}
+ dontHideOnDrag
/>
);
@@ -286,11 +432,11 @@ export class CollectionCardView extends CollectionSubView() {
* @returns
*/
overflowAmCardsCalc = (index: number) => {
- if (this.inactiveDocs().length < this._maxRowCount) {
- return this.inactiveDocs().length;
+ if (this.sortedDocs.length < this._maxRowCount) {
+ return this.sortedDocs.length;
}
// 13 - 3 = 10
- const totalCards = this.inactiveDocs().length;
+ const totalCards = this.sortedDocs.length;
// if 9 or less
if (index < totalCards - (totalCards % 10)) {
return this._maxRowCount;
@@ -323,22 +469,17 @@ export class CollectionCardView extends CollectionSubView() {
* @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;
+ const rowHeight = (this._props.PanelHeight() * this.fitContentScale) / this.numRows;
+ const rowIndex = Math.trunc(realIndex / this._maxRowCount);
+ const rowToCenterShift = this.numRows / 2 - rowIndex;
+ if (isSelected) return rowToCenterShift * rowHeight - rowHeight / 2;
+ if (amCards == 1) return 50 * this.fitContentScale;
+ // const trans = isHovered ? this.translateHover(realIndex) : 0;
+ const trans = 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
@@ -355,6 +496,7 @@ export class CollectionCardView extends CollectionSubView() {
};
const docTextPromises = this.childDocsWithoutLinks.map(async doc => {
const docText = (await docToText(doc)) ?? '';
+ doc['gptInputText'] = docText;
this._textToDoc.set(docText.replace(/\n/g, ' ').trim(), doc);
return `======${docText.replace(/\n/g, ' ').trim()}======`;
});
@@ -377,78 +519,110 @@ export class CollectionCardView extends CollectionSubView() {
image[DocData].description = response.trim();
return response; // Return the response from gptImageLabel
} catch (error) {
- console.log('bad things have happened');
+ console.log(error);
}
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 ~~~~~~
+ * Processes gpt's output depending on the type of question the user asked. Converts gpt's string output to
+ * usable code
* @param gptOutput
*/
- processGptOutput = (gptOutput: string) => {
+ @action
+
+ processGptOutput = undoable((gptOutput: string, questionType: string, tag?: string) => {
// Split the string into individual list items
const listItems = gptOutput.split('======').filter(item => item.trim() !== '');
+
+ if (questionType === '2' || questionType === '4') {
+ this.childDocs.forEach(d => {
+ d['chatFilter'] = false;
+ });
+ }
+
+ if (questionType ==='6'){
+ this.Document.cardSort = 'chat'
+ }
+
+
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;
+ const normalizedItem = item.trim();
+ // find the corresponding Doc in the textToDoc map
+ const doc = this._textToDoc.get(normalizedItem);
+
+ if (doc) {
+ switch (questionType) {
+ case '6':
+ doc.chatIndex = index;
+ break;
+ case '1':
+ const allHotKeys = StrListCast(Doc.UserDoc().myFilterHotKeyTitles);
+
+ let myTag = '';
+
+ if (tag) {
+ for (let i = 0; i < allHotKeys.length; i++) {
+ if (tag.includes(allHotKeys[i])) {
+ myTag = StrCast(Doc.UserDoc()[allHotKeys[i]]);
+ break;
+ } else if (tag.includes(StrCast(Doc.UserDoc()[allHotKeys[i]]))) {
+ myTag = StrCast(Doc.UserDoc()[allHotKeys[i]]);
+ break;
+ }
+ }
+
+ if (myTag != '') {
+ doc[myTag] = true;
+ }
+ }
+ break;
+ case '2':
+ case '4':
+ doc['chatFilter'] = true;
+ Doc.setDocFilter(DocCast(this.Document.embedContainer), 'chatFilter', true, 'match');
+ break;
}
- });
+ } else {
+ console.warn(`No matching document found for item: ${normalizedItem}`);
+ }
});
- };
+ }, '');
+
+
/**
* 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.setMode(GPTPopupMode.CARD);
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);
+ await this.childPairStringListAndUpdateSortDesc()
};
/**
- * 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, d) => this.cardSort_customField && set.add(NumCast(d[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 => DocumentView.SelectedDocs().includes(doc));
+ const isEmpty = this.childDocsWithoutLinks.length === 0;
+ const isDesc = BoolCast(this.Document.cardSort_isDesc);
+
+ if (isEmpty) {
+ return (
+ <span className="no-card-span" style={{ width: ` ${this._props.PanelWidth()}px`, height: ` ${this._props.PanelHeight()}px` }}>
+ Sorry ! There are no cards in this group
+ </span>
+ );
+ }
+
// Map sorted documents to their rendered components
return this.sortedDocs.map((doc, index) => {
- const realIndex = this.sortedDocs.filter(sortDoc => !DocumentView.SelectedDocs().includes(sortDoc)).indexOf(doc);
+ const realIndex = this.sortedDocs.indexOf(doc);
const calcRowIndex = this.overflowIndexCalc(realIndex);
const amCards = this.overflowAmCardsCalc(realIndex);
const isSelected = DocumentView.SelectedDocs().includes(doc);
+ const isDragging = DragManager.docsBeingDragged.includes(doc);
const childScreenToLocal = () => {
this._forceChildXf;
@@ -459,6 +633,12 @@ export class CollectionCardView extends CollectionSubView() {
.scale(1 / scale).rotate(!isSelected ? -this.rotate(amCards, calcRowIndex) : 0); // prettier-ignore
};
+ const translateIfSelected = () => {
+ const indexInRow = index % this._maxRowCount;
+ const rowIndex = Math.trunc(index / this._maxRowCount);
+ const rowCenterIndex = Math.min(this._maxRowCount, this.sortedDocs.length - rowIndex * this._maxRowCount) / 2;
+ return (rowCenterIndex - indexInRow) * 100 - 50;
+ };
return (
<div
key={doc[Id]}
@@ -471,29 +651,34 @@ export class CollectionCardView extends CollectionSubView() {
SnappingManager.SetIsResizing(undefined);
this._forceChildXf = !this._forceChildXf;
}),
- 700
+ 900
);
}}
style={{
width: this.panelWidth(),
- height: 'max-content', // this.panelHeight(childPair.layout)(),
+ height: 'max-content',
transform: `translateY(${this.calculateTranslateY(this._hoveredNodeIndex === index, isSelected, realIndex, amCards, calcRowIndex)}px)
- translateX(${isSelected ? this.translateSelected(calcRowIndex) : this.translateOverflowX(realIndex, amCards)}px)
+ translateX(calc(${(isSelected ? translateIfSelected() : 0) + '% + ' + this.translateOverflowX(realIndex, amCards) + 'px'}))
rotate(${!isSelected ? this.rotate(amCards, calcRowIndex) : 0}deg)
- scale(${isSelected ? 1.25 : 1})`,
+ scale(${isSelected ? 2 : this._hoveredNodeIndex === index ? 1.05 : 1})`,
}}
onMouseEnter={() => this.setHoveredNodeIndex(index)}>
{this.displayDoc(doc, childScreenToLocal)}
- {this.renderButtons(doc, this.cardSort)}
</div>
);
});
};
+
render() {
+ const isEmpty = this.childDocsWithoutLinks.length === 0;
+ const transformValue = `scale(${1 / this.fitContentScale})`;
+ const heightValue = `${100 * this.fitContentScale}%`;
+
return (
<div
+ onPointerMove={e => this.onPointerMove(e)}
className="collectionCardView-outer"
- ref={this.createDashEventsTarget}
+ ref={(ele: HTMLDivElement | null) => this.createDashEventsTarget(ele)}
style={{
background: this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.BackgroundColor) as string,
color: this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.Color) as string,
@@ -501,8 +686,9 @@ export class CollectionCardView extends CollectionSubView() {
<div
className="card-wrapper"
style={{
- transform: ` scale(${1 / this.fitContentScale}) translateX(${this.translateWrapperX}px)`,
- height: `${100 * this.fitContentScale}%`,
+ ...(!isEmpty && { transform: transformValue }),
+ ...(!isEmpty && { height: heightValue }),
+ gridAutoRows: `${100 / this.numRows}%`,
}}
onMouseLeave={() => this.setHoveredNodeIndex(-1)}>
{this.renderCards()}
diff --git a/src/client/views/collections/CollectionCarouselView.tsx b/src/client/views/collections/CollectionCarouselView.tsx
index 4bec2d963..2f8d5adaf 100644
--- a/src/client/views/collections/CollectionCarouselView.tsx
+++ b/src/client/views/collections/CollectionCarouselView.tsx
@@ -30,7 +30,7 @@ enum practiceVal {
export class CollectionCarouselView extends CollectionSubView() {
private _dropDisposer?: DragManager.DragDropDisposer;
get practiceField() { return this.fieldKey + "_practice"; } // prettier-ignore
- get starField() { return this.fieldKey + "_star"; } // prettier-ignore
+ get starField() { return "star"; } // prettier-ignore
constructor(props: SubCollectionViewProps) {
super(props);
diff --git a/src/client/views/global/globalScripts.ts b/src/client/views/global/globalScripts.ts
index 934ed6c3e..53e10d44d 100644
--- a/src/client/views/global/globalScripts.ts
+++ b/src/client/views/global/globalScripts.ts
@@ -2,10 +2,9 @@
import { Colors } from 'browndash-components';
import { action, runInAction } from 'mobx';
import { aggregateBounds } from '../../../Utils';
-import { Doc, DocListCast, FieldType, NumListCast, Opt } from '../../../fields/Doc';
+import { Doc, DocListCast, Opt } from '../../../fields/Doc';
import { DocData } from '../../../fields/DocSymbols';
import { InkTool } from '../../../fields/InkField';
-import { List } from '../../../fields/List';
import { BoolCast, Cast, NumCast, StrCast } from '../../../fields/Types';
import { WebField } from '../../../fields/URLField';
import { Gestures } from '../../../pen-gestures/GestureTypes';
@@ -36,9 +35,16 @@ import { ImageBox } from '../nodes/ImageBox';
import { VideoBox } from '../nodes/VideoBox';
import { WebBox } from '../nodes/WebBox';
import { RichTextMenu } from '../nodes/formattedText/RichTextMenu';
-
+import { NumListCast, StrListCast } from '../../../fields/Doc';
+import { List } from '../../../fields/List';
+import { CollectionViewType } from '../../documents/DocumentTypes';
// import { InkTranscription } from '../InkTranscription';
-
+import { Docs } from '../../documents/Documents';
+import { CollectionSubView } from '../collections/CollectionSubView';
+import { GPTPopup, GPTPopupMode } from '../pdf/GPTPopup/GPTPopup';
+import { PropertiesView } from '../PropertiesView';
+import { MainView } from '../MainView';
+import { SnappingManager } from '../../util/SnappingManager';
// eslint-disable-next-line prefer-arrow-callback
ScriptingGlobals.add(function IsNoneSelected() {
return DocumentView.Selected().length <= 0;
@@ -135,20 +141,25 @@ ScriptingGlobals.add(function toggleOverlay(checkResult?: boolean) {
// eslint-disable-next-line prefer-arrow-callback
ScriptingGlobals.add(function showFreeform(attr: 'hcenter' | 'vcenter' | 'grid' | 'snaplines' | 'clusters' | 'viewAll' | 'fitOnce', checkResult?: boolean, persist?: boolean) {
const selected = DocumentView.SelectedDocs().lastElement();
+
+ function isAttrFiltered(attr: string) {
+ return StrListCast(selected._childFilters).some(filter => filter.includes(attr));
+ }
+
// prettier-ignore
- const map: Map<'flashcards' | 'hcenter' | 'vcenter' | 'grid' | 'snaplines' | 'clusters' | 'arrange' | 'viewAll' | 'fitOnce' | 'time' | 'docType' | 'color' | 'links' | 'like' | 'star' | 'idea' | 'chat' | '1' | '2' | '3' | '4',
+ const map: Map<'flashcards' | 'center' | 'grid' | 'snaplines' | 'clusters' | 'arrange' | 'viewAll' | 'fitOnce' | 'time' | 'docType' | 'color' | 'chat' | 'up' | 'down' | 'pile' | 'toggle-chat' | 'tag',
{
waitForRender?: boolean;
checkResult: (doc: Doc) => boolean;
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; },
+ checkResult: (doc: Doc) => BoolCast(doc?._freeform_backgroundGrid, false),
+ setDoc: (doc: Doc) => { doc._freeform_backgroundGrid = !doc._freeform_backgroundGrid; },
}],
['snaplines', {
- checkResult: (doc:Doc) => BoolCast(doc?._freeform_snapLines, false),
- setDoc: (doc:Doc) => { doc._freeform_snapLines = !doc._freeform_snapLines; },
+ checkResult: (doc: Doc) => BoolCast(doc?._freeform_snapLines, false),
+ setDoc: (doc: Doc) => { doc._freeform_snapLines = !doc._freeform_snapLines; },
}],
['viewAll', {
checkResult: (doc: Doc) => BoolCast(doc?._freeform_fitContentsToBox, false),
@@ -158,18 +169,14 @@ ScriptingGlobals.add(function showFreeform(attr: 'hcenter' | 'vcenter' | 'grid'
else (dv.ComponentView as CollectionFreeFormView)?.fitContentOnce();
},
}],
- ['vcenter', {
- checkResult: (doc:Doc) => !StrCast(doc?._layout_dontCenter).includes('y'),
- setDoc: (doc:Doc) => { doc._layout_dontCenter = StrCast(doc.layout_dontCenter).includes('y') ? StrCast(doc.layout_dontCenter).replace(/y/,"") : StrCast(doc.layout_dontCenter) + 'y'; },
- }],
- ['hcenter', {
- checkResult: (doc:Doc) => !StrCast(doc?._layout_dontCenter).includes('x'),
- setDoc: (doc:Doc) => { doc._layout_dontCenter = StrCast(doc.layout_dontCenter).includes('x') ? StrCast(doc.layout_dontCenter).replace(/x/,"") : 'x'+ StrCast(doc.layout_dontCenter); },
+ ['center', {
+ checkResult: (doc: Doc) => BoolCast(doc?._stacking_alignCenter, false),
+ setDoc: (doc: Doc) => { doc._stacking_alignCenter = !doc._stacking_alignCenter; },
}],
['clusters', {
waitForRender: true, // flags that undo batch should terminate after a re-render giving the script the chance to fire
- checkResult: (doc:Doc) => BoolCast(doc?._freeform_useClusters, false),
- setDoc: (doc:Doc) => { doc._freeform_useClusters = !doc._freeform_useClusters; },
+ 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),
@@ -177,111 +184,118 @@ ScriptingGlobals.add(function showFreeform(attr: 'hcenter' | 'vcenter' | 'grid'
}],
['time', {
checkResult: (doc: Doc) => StrCast(doc?.cardSort) === "time",
- setDoc: (doc: Doc, dv: DocumentView) => doc.cardSort = "time",
+ setDoc: (doc: Doc, dv: DocumentView) => doc.cardSort === "time" ? doc.cardSort = '' : doc.cardSort = 'time',
}],
['docType', {
checkResult: (doc: Doc) => StrCast(doc?.cardSort) === "type",
- setDoc: (doc: Doc, dv: DocumentView) => doc.cardSort = "type",
+ setDoc: (doc: Doc, dv: DocumentView) => doc.cardSort === "type" ? doc.cardSort = '' : doc.cardSort = 'type',
}],
['color', {
checkResult: (doc: Doc) => StrCast(doc?.cardSort) === "color",
- setDoc: (doc: Doc, dv: DocumentView) => doc.cardSort = "color",
+ setDoc: (doc: Doc, dv: DocumentView) => doc.cardSort === "color" ? doc.cardSort = '' : doc.cardSort = 'color',
}],
- ['links', {
- checkResult: (doc: Doc) => StrCast(doc?.cardSort) === "links",
- setDoc: (doc: Doc, dv: DocumentView) => doc.cardSort = "links",
+ ['tag', {
+ checkResult: (doc: Doc) => StrCast(doc?.cardSort) === "tag",
+ setDoc: (doc: Doc, dv: DocumentView) => doc.cardSort === "tag" ? doc.cardSort = '' : doc.cardSort = 'tag',
}],
- ['like', {
- checkResult: (doc: Doc) => StrCast(doc?.cardSort) === "custom" && StrCast(doc?.cardSort_customField) === "like",
+ ['up', {
+ checkResult: (doc: Doc) => BoolCast(!doc?.cardSort_isDesc),
setDoc: (doc: Doc, dv: DocumentView) => {
- doc.cardSort = "custom";
- doc.cardSort_customField = "like";
- doc.cardSort_visibleSortGroups = new List<number>();
- }
+ doc.cardSort_isDesc = false;
+ },
}],
- ['star', {
- checkResult: (doc: Doc) => StrCast(doc?.cardSort) === "custom" && StrCast(doc?.cardSort_customField) === "star",
+ ['down', {
+ checkResult: (doc: Doc) => BoolCast(doc?.cardSort_isDesc),
setDoc: (doc: Doc, dv: DocumentView) => {
- doc.cardSort = "custom";
- doc.cardSort_customField = "star";
- doc.cardSort_visibleSortGroups = new List<number>();
- }
+ doc.cardSort_isDesc = true;
+ },
}],
- ['idea', {
- checkResult: (doc: Doc) => StrCast(doc?.cardSort) === "custom" && StrCast(doc?.cardSort_customField) === "idea",
+ ['toggle-chat', {
+ checkResult: (doc: Doc) => GPTPopup.Instance.visible,
setDoc: (doc: Doc, dv: DocumentView) => {
- doc.cardSort = "custom";
- doc.cardSort_customField = "idea";
- doc.cardSort_visibleSortGroups = new List<number>();
- }
+ if (GPTPopup.Instance.visible){
+ doc.cardSort = ''
+ GPTPopup.Instance.setVisible(false);
+
+ } else {
+ GPTPopup.Instance.setVisible(true);
+ GPTPopup.Instance.setMode(GPTPopupMode.CARD);
+ GPTPopup.Instance.setCardsDoneLoading(true);
+
+ }
+
+
+ },
}],
- ['chat', {
- checkResult: (doc: Doc) => StrCast(doc?.cardSort) === "custom" && StrCast(doc?.cardSort_customField) === "chat",
+ ['pile', {
+ checkResult: (doc: Doc) => doc._type_collection == CollectionViewType.Freeform,
setDoc: (doc: Doc, dv: DocumentView) => {
- doc.cardSort = "custom";
- doc.cardSort_customField = "chat";
- doc.cardSort_visibleSortGroups = new List<number>();
+ doc._type_collection = CollectionViewType.Freeform;
+ const newCol = Docs.Create.CarouselDocument(DocListCast(doc[Doc.LayoutFieldKey(doc)]), {
+ _width: 250,
+ _height: 200,
+ _layout_fitWidth: false,
+ _layout_autoHeight: true,
+ });
+
+
+ const iconMap: { [key: number]: any } = {
+ 0: 'star',
+ 1: 'heart',
+ 2: 'cloud',
+ 3: 'bolt'
+ };
+
+ for (let i=0; i<4; i++){
+ if (isAttrFiltered(iconMap[i])){
+ newCol[iconMap[i]] = true
+ }
+ }
+
+ newCol && dv.ComponentView?.addDocument?.(newCol);
+ DocumentView.showDocument(newCol, { willZoomCentered: true })
+
},
}],
]);
- for (let i = 0; i < 8; i++) {
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- 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);
}
+
const batch = map.get(attr)?.waitForRender ? UndoManager.StartBatch('set freeform attribute') : { end: () => {} };
DocumentView.Selected().map(dv => map.get(attr)?.setDoc(dv.layoutDoc, dv));
setTimeout(() => batch.end(), 100);
return undefined;
});
-ScriptingGlobals.add(function cardHasLabel(label: string) {
+/**
+ * Applies a filter to the selected document (or, if the settins button is pressed, opens the filter panel)
+ */
+ScriptingGlobals.add(function handleTags(value?: any, checkResult?: boolean) {
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')
-// });
+
+ function isAttrFiltered(attr: string) {
+ return StrListCast(selected._childFilters).some(filter => filter.includes(attr));
+ }
+
+ if (checkResult) {
+ return value=== 'opts' ? PropertiesView.Instance?.openFilters : isAttrFiltered(value)
+ }
+
+ if (value != 'opts'){
+ isAttrFiltered(value) ? Doc.setDocFilter(selected, value, true, 'remove') : Doc.setDocFilter(selected, value, true, 'match');
+ }
+ else {
+ SnappingManager.PropertiesWidth < 5 && SnappingManager.SetPropertiesWidth(0);
+ SnappingManager.SetPropertiesWidth(MainView.Instance.propertiesWidth() < 15 ? Math.min(MainView.Instance._dashUIWidth - 50, 250) : 0);
+ PropertiesView.Instance?.CloseAll()
+ PropertiesView.Instance.openFilters = true
+ }
+
+
+ return undefined;
+ }, '');
// eslint-disable-next-line prefer-arrow-callback
ScriptingGlobals.add(function setFontAttr(attr: 'font' | 'fontColor' | 'highlight' | 'fontSize' | 'alignment', value: string | number, checkResult?: boolean) {
@@ -316,6 +330,7 @@ ScriptingGlobals.add(function setFontAttr(attr: 'font' | 'fontColor' | 'highligh
]);
if (checkResult) {
+ // console.log(map.get(attr)?.checkResult() + "font check result")
return map.get(attr)?.checkResult();
}
map.get(attr)?.setDoc?.();
diff --git a/src/client/views/nodes/FontIconBox/FontIconBox.tsx b/src/client/views/nodes/FontIconBox/FontIconBox.tsx
index 7a09ad9e2..0fe112634 100644
--- a/src/client/views/nodes/FontIconBox/FontIconBox.tsx
+++ b/src/client/views/nodes/FontIconBox/FontIconBox.tsx
@@ -192,7 +192,7 @@ export class FontIconBox extends ViewBoxBaseComponent<ButtonProps>() {
} else {
text = script?.script.run({ this: this.Document, value: '', _readOnly_: true }).result as string;
// text = StrCast((RichTextMenu.Instance?.TextView?.EditorView ? RichTextMenu.Instance : Doc.UserDoc()).fontFamily);
- getStyle = (val: string) => ({ fontFamily: val });
+ // getStyle = (val: string) => ({ fontFamily: val });
}
// Get items to place into the list
diff --git a/src/client/views/nodes/IconTagBox.scss b/src/client/views/nodes/IconTagBox.scss
new file mode 100644
index 000000000..8c0f92c90
--- /dev/null
+++ b/src/client/views/nodes/IconTagBox.scss
@@ -0,0 +1,30 @@
+@import '../global/globalCssVariables.module.scss';
+
+.card-button-container {
+ display: flex;
+ padding: 3px;
+ position: absolute;
+ // width: 300px;
+ // height:100px;
+ pointer-events: none; /* This ensures the container does not capture hover events */
+
+ background-color: rgb(218, 218, 218); /* Background color of the container */
+ border-radius: 50px; /* Rounds the corners of the container */
+ transform: translateY(25px);
+ // 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 {
+ pointer-events: auto; /* Re-enable pointer events for the buttons */
+ transform: translateY(-7.5px);
+
+ width: 30px;
+ height: 30px;
+ border-radius: 50%;
+ background-color: $dark-gray;
+ // border-color: $medium-blue;
+ margin: 5px; // transform: translateY(-50px);
+ background-color: transparent;
+ }
+} \ No newline at end of file
diff --git a/src/client/views/nodes/IconTagBox.tsx b/src/client/views/nodes/IconTagBox.tsx
new file mode 100644
index 000000000..370ce51d1
--- /dev/null
+++ b/src/client/views/nodes/IconTagBox.tsx
@@ -0,0 +1,142 @@
+import React from "react";
+import { observer } from "mobx-react";
+import { computed } from "mobx";
+import { ObservableReactComponent } from "../ObservableReactComponent";
+import { NumCast } from "../../../fields/Types";
+import { Doc } from "../../../fields/Doc";
+import { numberRange } from "../../../Utils";
+import { Tooltip } from "@mui/material";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { undoable } from "../../util/UndoManager";
+import { BoolCast } from "../../../fields/Types";
+import { DocCast } from "../../../fields/Types";
+import './IconTagBox.scss';
+import {DocData } from '../../../fields/DocSymbols';
+import { StrListCast } from "../../../fields/Doc";
+import { StrCast } from "../../../fields/Types";
+import { CollectionViewType } from "../../documents/DocumentTypes";
+import { SnappingManager } from "../../util/SnappingManager";
+import { MainView } from "../MainView";
+import { PropertiesView } from "../PropertiesView";
+
+
+export interface IconTagProps {
+ doc: Doc;
+}
+
+/**
+ * Renders the icon tags that rest under the document. The icons rendered are determined by the values of
+ * each icon in the userdoc.
+ */
+@observer
+export class IconTagBox extends ObservableReactComponent<IconTagProps> {
+ @computed
+ get currentScale() {
+ return NumCast((this._props.doc.embedContainer as Doc)?._freeform_scale, 1);
+
+ }
+
+ constructor(props: any) {
+ super(props);
+ }
+
+ componentDidUpdate(prevProps: Readonly<IconTagProps>): void {
+ this._props.doc[DocData].tagHeight = 36*this.currentScale;
+ }
+
+
+ /**
+ * Renders the buttons to customize sorting depending on which group the card belongs to and the amount of total groups
+ * @param doc
+ * @param cardSort
+ * @returns
+ */
+ renderButtons = (doc: Doc): JSX.Element | null => {
+ const amButtons = (StrListCast(Doc.UserDoc().myFilterHotKeyTitles).length) + 1
+
+ const keys = StrListCast(Doc.UserDoc().myFilterHotKeyTitles)
+
+ const totalWidth = (amButtons -1) * 35 + (amButtons -1) * 2 * 5 + 6;
+
+ const iconMap = (buttonID: number) => {
+ return StrCast(Doc.UserDoc()[keys[buttonID]])
+
+ };
+
+ const isCard = DocCast(this._props.doc.embedContainer).type_collection === CollectionViewType.Card
+
+
+
+ return (
+ <div
+ className="card-button-container"
+ style={{
+ transformOrigin: 'top left',
+ transform: `scale(${ isCard ? 2 : 0.6 / this.currentScale})
+ translateY(${doc[DocData].showLabels ? ((NumCast(doc[DocData].keywordHeight)*(1-this.currentScale))) : 0}px)
+ `,
+ width: `${totalWidth}px`,
+ fontSize: '50px'
+ }}
+ >
+ {numberRange(amButtons-1).map(i => (
+ <Tooltip key={i} title={<div className="dash-tooltip">Click to add/remove this card from the {iconMap(i)} group</div>}>
+ <button key = {i} type="button" onClick={() => this.toggleButton(doc, iconMap(i) )}>
+ {this.getButtonIcon(doc, iconMap(i))}
+ </button>
+ </Tooltip>
+ ))}
+ </div>
+ );
+ };
+
+ /**
+ * Opens the filter panel in the properties menu
+ */
+
+ openHotKeyMenu = () => {
+ SnappingManager.PropertiesWidth < 5 && SnappingManager.SetPropertiesWidth(0);
+ SnappingManager.SetPropertiesWidth(MainView.Instance.propertiesWidth() < 15 ? Math.min(MainView.Instance._dashUIWidth - 50, 250) : 0);
+
+ PropertiesView.Instance.CloseAll()
+ PropertiesView.Instance.openFilters = true
+ }
+
+ /**
+ * 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((doc: Doc, icon: string) => {
+ BoolCast(doc[icon]) ? doc[icon] = false : doc[icon] = true
+
+ }, 'toggle card tag');
+
+
+ /**
+ * Determines whether or not the given icon is active depending on the doc's data
+ * @param doc
+ * @param icon
+ * @returns
+ */
+ getButtonIcon = (doc: Doc, icon: any): JSX.Element => {
+ const isActive = doc[icon]
+ const color = isActive ? '#4476f7' : '#323232';
+
+ return <FontAwesomeIcon icon={icon} style={{ color , height: '30px', width: '30px'}} />;
+ };
+
+ render (){
+ return (
+ <>
+ {this.renderButtons(this._props.doc)}
+
+ </>
+ )
+
+ }
+
+}
+
+
diff --git a/src/client/views/pdf/AnchorMenu.tsx b/src/client/views/pdf/AnchorMenu.tsx
index 03585a8b7..f5f758ad2 100644
--- a/src/client/views/pdf/AnchorMenu.tsx
+++ b/src/client/views/pdf/AnchorMenu.tsx
@@ -133,7 +133,7 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
_layout_autoHeight: true,
});
- this.addToCollection?.(newCol);
+ this.addToCollection?.(newCol); //this._props.addDocument(newCol)
};
pointerDown = (e: React.PointerEvent) => {
diff --git a/src/client/views/pdf/GPTPopup/GPTPopup.scss b/src/client/views/pdf/GPTPopup/GPTPopup.scss
index 6d8793f82..1defd1a7f 100644
--- a/src/client/views/pdf/GPTPopup/GPTPopup.scss
+++ b/src/client/views/pdf/GPTPopup/GPTPopup.scss
@@ -7,10 +7,13 @@ $highlightedText: #82e0ff;
.summary-box {
position: fixed;
- bottom: 10px;
- right: 10px;
+ top: 115px;
+ left: 75px;
width: 250px;
+ height: 200px;
min-height: 200px;
+ min-width: 180px;
+
border-radius: 16px;
padding: 16px;
padding-bottom: 0;
@@ -21,6 +24,18 @@ $highlightedText: #82e0ff;
background-color: #ffffff;
box-shadow: 0 2px 5px #7474748d;
color: $textgrey;
+ resize: both; /* Allows resizing */
+ overflow: auto;
+
+ .resize-handle {
+ width: 10px;
+ height: 10px;
+ background: #ccc;
+ position: absolute;
+ right: 0;
+ bottom: 0;
+ cursor: se-resize;
+ }
.summary-heading {
display: flex;
@@ -51,25 +66,76 @@ $highlightedText: #82e0ff;
.content-wrapper {
padding-top: 10px;
min-height: 50px;
- max-height: 150px;
+ // max-height: 150px;
overflow-y: auto;
+ height: 100%
}
.btns-wrapper-gpt {
- height: 50px;
+ height: 100%;
display: flex;
justify-content: center;
align-items: center;
- transform: translateY(30px);
+ flex-direction: column;
+
+ .inputWrapper{
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ height: 60px;
+ position: absolute;
+ bottom: 0;
+ width: 100%;
+ background-color: white;
+
+ }
.searchBox-input{
- transform: translateY(-15px);
- height: 50px;
+ height: 40px;
border-radius: 10px;
+ position: absolute;
+ bottom: 10px;
border-color: #5b97ff;
+ width: 90%
}
+ .chat-wrapper {
+ display: flex;
+ flex-direction: column;
+ width: 100%;
+ max-height: calc(100vh - 80px); /* Height minus the input box and some padding */
+ overflow-y: auto;
+ padding-bottom: 60px; /* Space for the input */
+ }
+
+ .chat-bubbles {
+ margin-top: 20px;
+ display: flex;
+ flex-direction: column;
+ flex-grow: 1;
+ }
+
+ .chat-bubble {
+ padding: 10px;
+ margin-bottom: 10px;
+ border-radius: 10px;
+ max-width: 60%;
+ }
+
+ .user-message {
+ background-color: #283d53;
+ align-self: flex-end;
+ color: whitesmoke;
+ }
+
+ .chat-message {
+ background-color: #367ae7;
+ align-self: flex-start;
+ color:whitesmoke;
+ }
+
+
.summarizing {
@@ -78,16 +144,22 @@ $highlightedText: #82e0ff;
}
- }
- button {
- font-size: 9px;
- padding: 10px;
- color: #ffffff;
- background-color: $button;
- border-radius: 5px;
+
+
+
}
+ // button {
+ // font-size: 9px;
+ // padding: 10px;
+ // color: #ffffff;
+ // width: 100%;
+ // background-color: $button;
+ // border-radius: 5px;
+
+ // }
+
.text-btn {
&:hover {
background-color: $button;
diff --git a/src/client/views/pdf/GPTPopup/GPTPopup.tsx b/src/client/views/pdf/GPTPopup/GPTPopup.tsx
index a37e73e27..53bedbb65 100644
--- a/src/client/views/pdf/GPTPopup/GPTPopup.tsx
+++ b/src/client/views/pdf/GPTPopup/GPTPopup.tsx
@@ -3,7 +3,7 @@ import { Button, IconButton, Type } from 'browndash-components';
import { action, makeObservable, observable } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
-import { CgClose } from 'react-icons/cg';
+import { CgClose, CgPathBack, CgArrowLeftO, CgCornerUpLeft } from 'react-icons/cg';
import ReactLoading from 'react-loading';
import { TypeAnimation } from 'react-type-animation';
import { ClientUtils } from '../../../../ClientUtils';
@@ -18,6 +18,9 @@ import { AnchorMenu } from '../AnchorMenu';
import './GPTPopup.scss';
import { SettingsManager } from '../../../util/SettingsManager';
import { SnappingManager } from '../../../util/SnappingManager';
+import { DocumentView } from '../../nodes/DocumentView';
+import { DocCast } from '../../../../fields/Types';
+import { RTFCast } from '../../../../fields/Types';
export enum GPTPopupMode {
SUMMARY,
@@ -25,15 +28,31 @@ export enum GPTPopupMode {
IMAGE,
FLASHCARD,
DATA,
+ CARD,
SORT,
+ QUIZ
}
+export enum GPTQuizType {
+ CURRENT = 0,
+ CHOOSE = 1,
+ MULTIPLE = 2
+
+}
+
+
+
+
+
+
interface GPTPopupProps {}
@observer
export class GPTPopup extends ObservableReactComponent<GPTPopupProps> {
// eslint-disable-next-line no-use-before-define
static Instance: GPTPopup;
+ private messagesEndRef: React.RefObject<HTMLDivElement>;
+
@observable private chatMode: boolean = false;
private correlatedColumns: string[] = [];
@@ -140,7 +159,8 @@ export class GPTPopup extends ObservableReactComponent<GPTPopupProps> {
this.sortDesc = t;
};
- @observable onSortComplete?: (sortResult: string) => void;
+ @observable onSortComplete?: (sortResult: string, questionType: string, tag?: string) => void;
+ @observable onQuizRandom?: () => void;
@observable cardsDoneLoading = false;
@action setCardsDoneLoading(done: boolean) {
@@ -148,28 +168,178 @@ export class GPTPopup extends ObservableReactComponent<GPTPopupProps> {
this.cardsDoneLoading = done;
}
+ @observable sortRespText: string = ''
+
+ @action setSortRespText(resp: string) {
+ this.sortRespText = resp
+ }
+
+ @observable chatSortPrompt: string = ""
+
+ sortPromptChanged = action((e: React.ChangeEvent<HTMLInputElement>) => {
+ this.chatSortPrompt = e.target.value;
+ });
+
+ @observable quizAnswer: string = ""
+
+ quizAnswerChanged = action((e: React.ChangeEvent<HTMLInputElement>) => {
+ this.quizAnswer = e.target.value;
+ });
+
+ @observable conversationArray: string[] = ["Hi! In this pop up, you can ask ChatGPT questions about your documents and filter / sort them. "]
+
+
+ /**
+ * When the cards are in quiz mode in the card view, allows gpt to determine whether the user's answer was correct
+ * @returns
+ */
+ generateQuiz = async () => {
+ this.setLoading(true);
+ this.setSortDone(false);
+
+ const quizType = this.quizMode;
+
+ const selected = DocumentView.SelectedDocs().lastElement();
+
+
+ const questionText = 'Question: ' + StrCast(selected['gptInputText']);
+
+ if (StrCast(selected['gptRubric']) === '') {
+ const rubricText = 'Rubric: ' + await this.generateRubric(StrCast(selected['gptInputText']), selected)
+ }
+
+ const rubricText = 'Rubric: ' + (StrCast(selected['gptRubric']))
+ const queryText = questionText + ' UserAnswer: ' + this.quizAnswer + '. ' + 'Rubric' + rubricText;
+
+ try {
+ const res = await gptAPICall(queryText, GPTCallType.QUIZ);
+ if (!res) {
+ console.error('GPT call failed');
+ return;
+ }
+ console.log(res)
+ this.setQuizResp(res)
+ this.conversationArray.push(res)
+
+ this.setLoading(false);
+ this.setSortDone(true);
+
+ } catch (err) {
+ console.error('GPT call failed');
+ }
+
+
+ if (this.onQuizRandom){
+ this.onQuizRandom()
+ }
+
+ }
+
+ /**
+ * Generates a rubric by which to compare the user's answer to
+ * @param inputText user's answer
+ * @param doc the doc the user is providing info about
+ * @returns gpt's response
+ */
+ generateRubric = async (inputText: string, doc:Doc) => {
+ try {
+ const res = await gptAPICall(inputText, GPTCallType.RUBRIC);
+ doc['gptRubric']= res;
+ return res
+ } catch (err) {
+ console.error('GPT call failed');
+ }
+
+ }
+
+
+
+ @observable private regenerateCallback: (() => Promise<void>) | null = null;
+
+ /**
+ * Callback function that causes the card view to update the childpair string list
+ * @param callback
+ */
+ @action public setRegenerateCallback(callback: () => Promise<void>) {
+ this.regenerateCallback = callback;
+ }
+
+
+
+
public addDoc: (doc: Doc | Doc[], sidebarKey?: string | undefined) => boolean = () => false;
public createFilteredDoc: (axes?: string[]) => boolean = () => false;
public addToCollection: ((doc: Doc | Doc[], annotationKey?: string | undefined) => boolean) | undefined;
+ @observable quizRespText: string = ''
+
+ @action setQuizResp (resp: string) {
+ this.quizRespText = resp
+
+ }
+
/**
- * Sorts cards in the CollectionCardDeckView
+ * Generates a response to the user's question depending on the type of their question
*/
- generateSort = async () => {
+ generateCard = async () => {
+ console.log(this.chatSortPrompt + "USER PROMPT")
this.setLoading(true);
this.setSortDone(false);
+ if (this.regenerateCallback) {
+ await this.regenerateCallback();
+ }
+
try {
- const res = await gptAPICall(this.sortDesc, GPTCallType.SORT);
+ // const res = await gptAPICall(this.sortDesc, GPTCallType.SORT, this.chatSortPrompt);
+ const questionType = await gptAPICall(this.chatSortPrompt, GPTCallType.TYPE);
+ const questionNumber = questionType.split(' ')[0]
+ console.log(questionType)
+ let res = ''
+
+ switch (questionNumber) {
+ case '1':
+ case '2':
+ case '4':
+ res = await gptAPICall(this.sortDesc, GPTCallType.SUBSET, this.chatSortPrompt);
+ break
+ case '6':
+ res = await gptAPICall(this.sortDesc, GPTCallType.SORT, this.chatSortPrompt);
+ break
+ default:
+
+ const selected = DocumentView.SelectedDocs().lastElement();
+ const questionText = StrCast(selected!['gptInputText']);
+
+
+ res = await gptAPICall(questionText, GPTCallType.INFO, this.chatSortPrompt);
+ break
+ }
+
// Trigger the callback with the result
if (this.onSortComplete) {
- this.onSortComplete(res || 'Something went wrong :(');
+ this.onSortComplete(res || 'Something went wrong :(', questionNumber, questionType.split(' ').slice(1).join(' '));
+
+ let explanation = res
+
+ if (questionType != '5' && questionType != '3'){
+
+ // Extract explanation surrounded by ------ at the top or both at the top and bottom
+ const explanationMatch = res.match(/------\s*([\s\S]*?)\s*(?:------|$)/) || [];
+ explanation = explanationMatch[1] ? explanationMatch[1].trim() : 'No explanation found';
+ }
+
+ // Set the extracted explanation to sortRespText
+ this.setSortRespText(explanation);
+ this.conversationArray.push(this.sortRespText)
+ this.scrollToBottom()
+
console.log(res);
}
} catch (err) {
console.error(err);
}
-
+
this.setLoading(false);
this.setSortDone(true);
};
@@ -305,67 +475,167 @@ export class GPTPopup extends ObservableReactComponent<GPTPopupProps> {
super(props);
makeObservable(this);
GPTPopup.Instance = this;
+ this.messagesEndRef = React.createRef();
+
}
+
+
+ scrollToBottom = () => {
+ setTimeout(() => {
+ // Code to execute after 1 second (1000 ms)
+ if (this.messagesEndRef.current) {
+
+ this.messagesEndRef.current.scrollIntoView({ behavior: 'smooth', block: 'end' });
+ }
+
+ }, 50);
+ }
+
componentDidUpdate = () => {
if (this.loading) {
this.setDone(false);
}
};
+ @observable quizMode : GPTQuizType = GPTQuizType.CURRENT
+ @action setQuizMode (g: GPTQuizType) {
+ this.quizMode = g
+ }
+
+ cardMenu = () => (
+ <div className="btns-wrapper-gpt">
+ <Button
+ tooltip="Have ChatGPT sort, tag, define, or filter your cards for you!"
+ text="Modify/Sort Cards!"
+ onClick={() => this.setMode(GPTPopupMode.SORT)}
+ color={StrCast(Doc.UserDoc().userVariantColor)}
+ type={Type.TERT}
+ style={{
+ width: '100%',
+ height: '40%',
+ textAlign: 'center',
+ color: '#ffffff',
+ fontSize: '16px',
+ marginBottom: '10px'
+ }}
+ />
+ <Button
+ tooltip="Test your knowledge with ChatGPT!"
+ text="Quiz Cards!"
+ onClick={() => {
+ this.conversationArray = ['Define the selected card!']
+ this.setMode(GPTPopupMode.QUIZ)
+ if (this.onQuizRandom){
+ this.onQuizRandom()
+ }
+
+
+
+ }}
+ color={StrCast(Doc.UserDoc().userVariantColor)}
+ type={Type.TERT}
+ style={{
+ width: '100%',
+ textAlign: 'center',
+ color: '#ffffff',
+ fontSize: '16px',
+ height: '40%',
+
+ }}
+ />
+ </div>
+ )
+
+
+
+ handleKeyPress = async (e: React.KeyboardEvent, isSort: boolean) => {
+ if (e.key === 'Enter') {
+ e.stopPropagation();
+
+ if (isSort) {
+ this.conversationArray.push(this.chatSortPrompt);
+ await this.generateCard();
+ this.chatSortPrompt = ''
+
+ } else {
+ this.conversationArray.push(this.quizAnswer);
+ await this.generateQuiz();
+ this.quizAnswer = ''
+
+ }
+
+ this.scrollToBottom();
+ }
+ }
+
+ cardActual = (opt: GPTPopupMode) => {
+ const isSort = opt === GPTPopupMode.SORT
+ return (
+
+ <div className="btns-wrapper-gpt">
+ <div className="chat-wrapper">
+ <div className="chat-bubbles">
+ {this.conversationArray.map((message, index) => (
+ <div
+ key={index}
+ className={`chat-bubble ${index % 2 === 1 ? 'user-message' : 'chat-message'}`}
+ >
+ {message}
+ </div>
+ ))}
+ {(!this.cardsDoneLoading || this.loading) && (
+ <div className={`chat-bubble chat-message`}>
+ ...
+ </div>
+ )}
+ </div>
+
+ <div ref={this.messagesEndRef} style= {{height: '100px'}} />
+
+ </div>
+
+ <div className="inputWrapper">
+ <input
+ className="searchBox-input"
+ defaultValue=""
+ value={isSort ? this.chatSortPrompt : this.quizAnswer} // Controlled input
+ autoComplete="off"
+ onChange={isSort ? this.sortPromptChanged : this.quizAnswerChanged}
+ onKeyDown={(e) => {
+ this.handleKeyPress(e, isSort)
+
+
+ }
+ }
+ type="text"
+ placeholder={`${isSort ? 'Have ChatGPT sort, tag, define, or filter your cards for you!' : 'Define the selected card!'}`}
+ />
+ </div>
+ </div>
+ );
+ };
+
sortBox = () => (
- <>
- <div>
- {this.heading('SORTING')}
- {this.loading ? (
+ <div style = {{height: '80%'}}>
+ {this.heading(this.mode === GPTPopupMode.SORT ? 'SORTING' : 'QUIZ')}
+ <>
+ {!this.cardsDoneLoading? (
<div className="content-wrapper">
<div className="loading-spinner">
<ReactLoading type="spin" color={StrCast(Doc.UserDoc().userVariantColor)} height={30} width={30} />
- <span>Loading...</span>
+ {this.loading ? <span>Loading...</span> : <span>Reading Cards...</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>
- )}
- </>
+ (this.mode === GPTPopupMode.CARD ? this.cardMenu() : this.cardActual(this.mode)) // Call the functions to render JSX
)}
- </div>
- </>
+ </>
+ </div>
);
+
+
+
imageBox = () => (
<div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}>
{this.heading('GENERATED IMAGE')}
@@ -511,15 +781,64 @@ export class GPTPopup extends ObservableReactComponent<GPTPopupProps> {
heading = (headingText: string) => (
<div className="summary-heading">
<label className="summary-text">{headingText}</label>
- {this.loading ? <ReactLoading type="spin" color="#bcbcbc" width={14} height={14} /> : <IconButton color={StrCast(SettingsManager.userVariantColor)} tooltip="close" icon={<CgClose size="16px" />} onClick={() => this.setVisible(false)} />}
+ {this.loading ? (
+ <ReactLoading type="spin" color="#bcbcbc" width={14} height={14} />
+ ) : (
+ <>
+ {(this.mode === GPTPopupMode.SORT || this.mode === GPTPopupMode.QUIZ) && (
+ <IconButton
+ color={StrCast(SettingsManager.userVariantColor)}
+ tooltip="back"
+ icon={<CgCornerUpLeft size="16px" />}
+ onClick={() => this.mode = GPTPopupMode.CARD}
+ style = {{right: '50px', position: 'absolute'}}
+ />
+ )}
+ <IconButton
+ color={StrCast(SettingsManager.userVariantColor)}
+ tooltip="close"
+ icon={<CgClose size="16px" />}
+ onClick={() => {
+ this.setVisible(false)}}
+ />
+
+ </>
+ )}
</div>
);
+
render() {
+ let content;
+
+ switch (this.mode) {
+ case GPTPopupMode.SUMMARY:
+ content = this.summaryBox();
+ break;
+ case GPTPopupMode.DATA:
+ content = this.dataAnalysisBox();
+ break;
+ case GPTPopupMode.IMAGE:
+ content = this.imageBox();
+ break;
+ case GPTPopupMode.SORT:
+ case GPTPopupMode.CARD:
+ case GPTPopupMode.QUIZ:
+ content = this.sortBox();
+ break;
+ default:
+ content = null;
+ }
+
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() : this.mode === GPTPopupMode.SORT ? this.sortBox() : null}
+ <div
+ className="summary-box"
+ style={{ display: this.visible ? 'flex' : 'none' }}
+ >
+ {content}
+ <div className="resize-handle" />
</div>
);
}
+
}
diff --git a/src/typings/index.d.ts b/src/typings/index.d.ts
index bee79a38d..fea88cdcb 100644
--- a/src/typings/index.d.ts
+++ b/src/typings/index.d.ts
@@ -12,7 +12,7 @@ declare module 'fit-curve';
declare module 'iink-js';
declare module 'pdfjs-dist/web/pdf_viewer';
declare module 'react-jsx-parser';
-declare module 'type_decls.d';
+// declare module 'type_decls.d';
declare module '@react-pdf/renderer' {
import * as React from 'react';