aboutsummaryrefslogtreecommitdiff
path: root/src/client/views
diff options
context:
space:
mode:
Diffstat (limited to 'src/client/views')
-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
19 files changed, 1581 insertions, 326 deletions
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>
);
}
+
}