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.tsx59
-rw-r--r--src/client/views/DocumentDecorations.tsx23
-rw-r--r--src/client/views/FilterPanel.scss92
-rw-r--r--src/client/views/FilterPanel.tsx229
-rw-r--r--src/client/views/InkingStroke.tsx5
-rw-r--r--src/client/views/MainView.tsx4
-rw-r--r--src/client/views/PropertiesView.tsx4
-rw-r--r--src/client/views/StyleProvider.tsx9
-rw-r--r--src/client/views/TagsView.tsx122
-rw-r--r--src/client/views/collections/CollectionCardDeckView.scss46
-rw-r--r--src/client/views/collections/CollectionCardDeckView.tsx547
-rw-r--r--src/client/views/collections/CollectionCarousel3DView.tsx35
-rw-r--r--src/client/views/collections/CollectionCarouselView.scss3
-rw-r--r--src/client/views/collections/CollectionCarouselView.tsx120
-rw-r--r--src/client/views/collections/CollectionNoteTakingView.tsx1
-rw-r--r--src/client/views/collections/CollectionStackingView.tsx3
-rw-r--r--src/client/views/collections/CollectionSubView.tsx6
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx6
-rw-r--r--src/client/views/global/globalScripts.ts193
-rw-r--r--src/client/views/linking/LinkMenuItem.tsx2
-rw-r--r--src/client/views/nodes/ComparisonBox.tsx7
-rw-r--r--src/client/views/nodes/DocumentContentsView.tsx1
-rw-r--r--src/client/views/nodes/DocumentView.tsx6
-rw-r--r--src/client/views/nodes/FieldView.tsx4
-rw-r--r--src/client/views/nodes/FontIconBox/FontIconBox.tsx21
-rw-r--r--src/client/views/nodes/IconTagBox.scss26
-rw-r--r--src/client/views/nodes/IconTagBox.tsx92
-rw-r--r--src/client/views/nodes/ImageBox.tsx3
-rw-r--r--src/client/views/nodes/formattedText/FormattedTextBox.tsx16
-rw-r--r--src/client/views/nodes/formattedText/RichTextRules.ts2
-rw-r--r--src/client/views/pdf/GPTPopup/GPTPopup.scss92
-rw-r--r--src/client/views/pdf/GPTPopup/GPTPopup.tsx375
-rw-r--r--src/client/views/search/FaceRecognitionHandler.tsx3
34 files changed, 1617 insertions, 549 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..32bf67df1 100644
--- a/src/client/views/DocumentButtonBar.tsx
+++ b/src/client/views/DocumentButtonBar.tsx
@@ -28,7 +28,6 @@ import { DocumentLinksButton } from './nodes/DocumentLinksButton';
import { DocumentView } from './nodes/DocumentView';
import { OpenWhere } from './nodes/OpenWhere';
import { DashFieldView } from './nodes/formattedText/DashFieldView';
-import { DocData } from '../../fields/DocSymbols';
@observer
export class DocumentButtonBar extends ObservableReactComponent<{ views: () => (DocumentView | undefined)[]; stack?: unknown }> {
@@ -264,28 +263,57 @@ export class DocumentButtonBar extends ObservableReactComponent<{ views: () => (
}
@computed
- get keywordButton() {
- return !DocumentView.Selected().length ? null : (
- <Tooltip title={<div className="dash-keyword-button">Open keyword menu</div>}>
+ get calendarButton() {
+ const targetDoc = this.view0?.Document;
+ return !targetDoc ? null : (
+ <Tooltip title={<div className="dash-calendar-button">Open calendar 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')}>
- <FontAwesomeIcon className="documentdecorations-icon" icon="tag" />
+ onClick={() => {
+ CalendarManager.Instance.open(this.view0, targetDoc);
+ }}>
+ <FontAwesomeIcon className="documentdecorations-icon" icon={faCalendarDays as IconLookup} />
</div>
</Tooltip>
);
}
+ /**
+ * Allows for both the keywords and the icon tags to be shown using a quasi- multitoggle
+ */
+ @computed
+ get keywordButton() {
+ const targetDoc = this.view0?.Document;
+
+ return !targetDoc ? null : (
+ <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 menu</div>}>
+ <div
+ className="documentButtonBar-icon"
+ style={{ color: 'white' }}
+ onClick={undoable(e => {
+ const showing = DocumentView.Selected().some(dv => dv.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')}>
+ <FontAwesomeIcon className="documentdecorations-icon" icon="tag" />
+ </div>
+ </Tooltip>
+ </div>
+ );
+ }
+
@observable _isRecording = false;
_stopFunc: () => void = emptyFunction;
@computed
@@ -455,6 +483,7 @@ export class DocumentButtonBar extends ObservableReactComponent<{ views: () => (
{!DocumentView.Selected().some(v => v.allLinks.length) ? null : <div className="documentButtonBar-button">{this.followLinkButton}</div>}
<div className="documentButtonBar-button">{this.pinButton}</div>
<div className="documentButtonBar-button">{this.recordButton}</div>
+ <div className="documentButtonBar-button">{this.calendarButton}</div>
<div className="documentButtonBar-button">{this.keywordButton}</div>
{!Doc.UserDoc().documentLinksButton_fullMenu ? null : <div className="documentButtonBar-button">{this.shareButton}</div>}
<div className="documentButtonBar-button">{this.menuButton}</div>
diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index 5e7908725..34b05da56 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -2,7 +2,7 @@ import { IconProp } from '@fortawesome/fontawesome-svg-core';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { Tooltip } from '@mui/material';
import { IconButton } from 'browndash-components';
-import { action, computed, makeObservable, observable, runInAction } from 'mobx';
+import { action, computed, makeObservable, observable, runInAction, trace } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
import { FaUndo } from 'react-icons/fa';
@@ -36,6 +36,7 @@ import { ImageBox } from './nodes/ImageBox';
import { OpenWhere, OpenWhereMod } from './nodes/OpenWhere';
import { FormattedTextBox } from './nodes/formattedText/FormattedTextBox';
import { TagsView } from './TagsView';
+import { setTime } from 'react-datepicker/dist/date_utils';
interface DocumentDecorationsProps {
PanelWidth: number;
@@ -59,6 +60,7 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora
private _interactionLock?: boolean;
@observable _showNothing = true;
+ @observable private _forceRender = 0
@observable private _accumulatedTitle = '';
@observable private _titleControlString: string = '$title';
@observable private _editingTitle = false;
@@ -340,7 +342,7 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora
setRotateCenter = (seldocview: DocumentView, rotCenter: number[]) => {
const selDoc = seldocview.Document;
- const newloccentern = seldocview.screenToContentsTransform().transformPoint(rotCenter[0], rotCenter[1]);
+ const newloccentern = seldocview.screenToViewTransform().transformPoint(rotCenter[0], rotCenter[1]);
const newlocenter = [newloccentern[0] - NumCast(seldocview.layoutDoc._width) / 2, newloccentern[1] - NumCast(seldocview.layoutDoc._height) / 2];
const final = Utils.rotPt(newlocenter[0], newlocenter[1], -(NumCast(seldocview.Document._rotation) / 180) * Math.PI);
selDoc._rotation_centerX = final.x / NumCast(seldocview.layoutDoc._width);
@@ -620,8 +622,7 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora
});
};
- @computed
- get selectionTitle(): string {
+ @computed get selectionTitle(): string {
if (DocumentView.Selected().length === 1) {
const selected = DocumentView.Selected()[0];
if (this._titleControlString.startsWith('$')) {
@@ -635,15 +636,16 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora
@computed get rotCenter() {
const lastView = DocumentView.Selected().lastElement();
if (lastView) {
- const invXf = lastView.screenToContentsTransform().inverse();
+ const invXf = lastView.screenToViewTransform().inverse();
const seldoc = lastView.layoutDoc;
const loccenter = Utils.rotPt(NumCast(seldoc._rotation_centerX) * NumCast(seldoc._width), NumCast(seldoc._rotation_centerY) * NumCast(seldoc._height), invXf.Rotate);
return invXf.transformPoint(loccenter.x + NumCast(seldoc._width) / 2, loccenter.y + NumCast(seldoc._height) / 2);
}
return this._rotCenter;
}
-
+;
render() {
+ this._forceRender;
const { b, r, x, y } = this.Bounds;
const seldocview = DocumentView.Selected().lastElement();
if (SnappingManager.IsDragging || r - x < 1 || x === Number.MAX_VALUE || !seldocview || this._hidden || isNaN(r) || isNaN(b) || isNaN(x) || isNaN(y)) {
@@ -656,6 +658,11 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora
return null;
}
+ if (seldocview && !seldocview?.ContentDiv?.getBoundingClientRect().width) {
+ setTimeout(action(() => this._forceRender++)); // if the selected Doc has no width, then assume it's stil being layed out and try to render again later.
+ return null;
+ }
+
// sharing
const acl = GetEffectiveAcl(!this._showLayoutAcl ? Doc.GetProto(seldocview.Document) : seldocview.Document);
const docShareMode = HierarchyMapping.get(acl)!.name;
@@ -837,7 +844,7 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora
<div
className="link-button-container"
style={{
- top: DocumentView.Selected().length > 1 ? 0 : `${seldocview.Document._layout_showTags ? 4 + seldocview.TagPanelHeight : 4}px`,
+ top: DocumentView.Selected().length > 1 ? 0 : `${seldocview.showTags ? 4 + seldocview.TagPanelHeight : 4}px`,
transform: `translate(${-this._resizeBorderWidth / 2 + 10}px, ${this._resizeBorderWidth + bounds.b - bounds.y + this._titleHeight}px) `,
}}>
<DocumentButtonBar views={() => DocumentView.Selected()} />
@@ -846,7 +853,7 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora
<div
className="documentDecorations-tagsView"
style={{
- top: `${seldocview.Document._layout_showTags ? 4 + seldocview.TagPanelHeight : 4}px`,
+ top: `${seldocview.showTags ? 4 + seldocview.TagPanelHeight : 4}px`,
transform: `translate(${-this._resizeBorderWidth / 2 + 10}px, ${this._resizeBorderWidth + bounds.b - bounds.y + this._titleHeight}px) `,
}}>
{DocumentView.Selected().length > 1 ? <TagsView Views={DocumentView.Selected()} /> : null}
diff --git a/src/client/views/FilterPanel.scss b/src/client/views/FilterPanel.scss
index d6d2956aa..508b1ee1f 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,97 @@
vertical-align: middle;
}
+.filterHotKey-button {
+ pointer-events: auto;
+ width: 100%; //width: 25px;
+ border-radius: 5px;
+ 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;
+
+
+ &:hover{
+ border-color: #e9e9e9;
+ background-color: #6d6c6c
+ }
+
+ .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%;
+
+}
+
+.hotKey-icon-button {
+
+ background-color: transparent;
+
+
+}
+
+.icon-panel {
+ position: absolute;
+ z-index: 10000;
+ 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..e34b66963 100644
--- a/src/client/views/FilterPanel.tsx
+++ b/src/client/views/FilterPanel.tsx
@@ -1,22 +1,161 @@
-/* eslint-disable react/jsx-props-no-spreading */
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { Tooltip } from '@mui/material';
import { action, computed, makeObservable, observable, ObservableMap } from 'mobx';
-import { observer } from 'mobx-react';
+import { observer, useLocalObservable } from 'mobx-react';
import * as React from 'react';
+import { useEffect, useRef } from 'react';
import { Handles, Rail, Slider, Ticks, Tracks } from 'react-compound-slider';
import { AiOutlineMinusSquare, AiOutlinePlusSquare } from 'react-icons/ai';
import { CiCircleRemove } from 'react-icons/ci';
import { Doc, DocListCast, Field, FieldType, LinkedTo, StrListCast } from '../../fields/Doc';
+import { DocData } from '../../fields/DocSymbols';
import { Id } from '../../fields/FieldSymbols';
import { List } from '../../fields/List';
import { RichTextField } from '../../fields/RichTextField';
+import { DocCast, StrCast } from '../../fields/Types';
+import { Button, CurrentUserUtils } from '../util/CurrentUserUtils';
import { SearchUtil } from '../util/SearchUtil';
import { SnappingManager } from '../util/SnappingManager';
import { undoable } from '../util/UndoManager';
import { FieldsDropdown } from './FieldsDropdown';
import './FilterPanel.scss';
import { DocumentView } from './nodes/DocumentView';
+import { ButtonType } from './nodes/FontIconBox/FontIconBox';
import { Handle, Tick, TooltipRail, Track } from './nodes/SliderBox-components';
import { ObservableReactComponent } from './ObservableReactComponent';
+import { IconProp } from '@fortawesome/fontawesome-svg-core';
+
+interface HotKeyButtonProps {
+ hotKey: Doc;
+ 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.title = newHotKey; },
+ })); // prettier-ignore
+
+ const panelRef = useRef<HTMLDivElement>(null);
+ const inputRef = useRef<HTMLInputElement>(null);
+
+ const handleClick = () => state.toggleActive();
+
+ /**
+ * 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(() => {
+ hotKey.title = StrCast(state.myHotKey.title);
+ hotKey.toolTip = `Click to toggle the ${StrCast(hotKey.title)}'s group's visibility`;
+ }, '');
+
+ /**
+ * 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();
+ }
+ }
+ };
+
+ useEffect(() => {
+ document.addEventListener('mousedown', handleClickOutside);
+ return () => document.removeEventListener('mousedown', handleClickOutside);
+ }, []);
+
+ const iconOpts = ['star', 'heart', 'bolt', 'satellite', 'palette', 'robot', 'lightbulb', 'highlighter', 'book', 'chalkboard'] as IconProp[];
+
+ /**
+ * Panel of icons the user can choose from to represent their tag
+ */
+ const iconPanel = iconOpts.map(icon => (
+ <button
+ key={icon.toString()}
+ onClick={undoable(e => {
+ e.stopPropagation;
+ hotKey[DocData].icon = icon.toString();
+ }, '')}
+ className="icon-panel-button">
+ <FontAwesomeIcon icon={icon} 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&apos;s icon</div>}>
+ <button
+ type="button"
+ className="hotKey-icon"
+ onClick={(e: React.MouseEvent) => {
+ e.stopPropagation();
+ handleClick();
+ }}>
+ <FontAwesomeIcon icon={hotKey.icon as IconProp} size="2xl" color={SnappingManager.userColor} />
+ </button>
+ </Tooltip>
+ {state.isActive && <div className="icon-panel">{iconPanel}</div>}
+ </div>
+ {state.isEditing ? (
+ <input
+ ref={inputRef}
+ type="text"
+ value={StrCast(state.myHotKey.title).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">{StrCast(hotKey.title).toUpperCase()}</p>
+ )}
+ <button
+ className="hotKey-close"
+ onClick={(e: React.MouseEvent) => {
+ e.stopPropagation();
+ Doc.RemFromFilterHotKeys(hotKey);
+ }}>
+ <FontAwesomeIcon icon={'x' as IconProp} color={SnappingManager.userColor} />
+ </button>
+ </div>
+ );
+});
interface filterProps {
Document: Doc;
@@ -24,13 +163,16 @@ interface filterProps {
@observer
export class FilterPanel extends ObservableReactComponent<filterProps> {
- @observable _selectedFacetHeaders = new Set<string>();
+ // eslint-disable-next-line no-use-before-define
+ public static Instance: FilterPanel;
constructor(props: filterProps) {
super(props);
makeObservable(this);
+ FilterPanel.Instance = this;
}
+ @observable _selectedFacetHeaders = new Set<string>();
/**
* @returns the relevant doc according to the value of FilterBox._filterScope i.e. either the Current Dashboard or the Current Collection
*/
@@ -129,7 +271,7 @@ export class FilterPanel extends ObservableReactComponent<filterProps> {
@computed get activeRenderedFacetInfos() {
return new Set(
Array.from(new Set(Array.from(this._selectedFacetHeaders).concat(this.activeFacetHeaders))).map(facetHeader => {
- const facetValues = FilterPanel.gatherFieldValues(this.targetDocChildren, facetHeader, StrListCast(this.Document.childFilters));
+ const facetValues = facetHeader.startsWith('#') ? { strings: [] } : FilterPanel.gatherFieldValues(this.targetDocChildren, facetHeader, StrListCast(this.Document.childFilters));
let nonNumbers = 0;
let minVal = Number.MAX_VALUE;
@@ -147,7 +289,10 @@ export class FilterPanel extends ObservableReactComponent<filterProps> {
if (facetHeader === 'text') {
return { facetHeader, renderType: 'text' };
}
- if (facetHeader !== 'tags' && nonNumbers / facetValues.strings.length < 0.1) {
+ if (facetHeader.startsWith('#')) {
+ return { facetHeader, renderType: 'togglebox' };
+ }
+ if (facetHeader !== 'tags' && !facetHeader.startsWith('#') && nonNumbers / facetValues.strings.length < 0.1) {
const extendedMinVal = minVal - Math.min(1, Math.floor(Math.abs(maxVal - minVal) * 0.1));
const extendedMaxVal = Math.max(minVal + 1, maxVal + Math.min(1, Math.ceil(Math.abs(maxVal - minVal) * 0.05)));
const ranged: number[] | undefined = Doc.readDocRangeFilter(this.Document, facetHeader); // not the filter range, but the zooomed in range on the filter
@@ -211,12 +356,59 @@ 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 title = hotKey.startsWith('#') ? hotKey.substring(1) : hotKey;
+
+ const newKey: Button = {
+ title,
+ icon: 'question',
+ toolTip: `Click to toggle the ${title}'s group's visibility`,
+ btnType: ButtonType.ToggleButton,
+ expertMode: false,
+ toolType: '#' + title,
+ funcs: {},
+ scripts: { onClick: '{ return handleTags(this.toolType, _readOnly_);}' },
+ };
+
+ const newBtn = CurrentUserUtils.setupContextMenuBtn(newKey, filter);
+ newBtn.isSystem = newBtn[DocData].isSystem = undefined;
+
+ Doc.AddToFilterHotKeys(newBtn);
+ };
+
+ /**
+ * Renders the newly formed hotkey icon buttons
+ * @returns the buttons to be rendered
+ */
+ hotKeyButtons = () => {
+ const selected = DocumentView.SelectedDocs().lastElement();
+ const hotKeys = Doc.MyFilterHotKeys;
+
+ // Selecting a button should make it so that the icon on the top filter panel becomes said icon
+ const buttons = hotKeys.map(hotKey => (
+ <Tooltip key={StrCast(hotKey.title)} title={<div className="dash-tooltip">Click to customize this hotkey&apos;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 +469,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>
);
}
@@ -317,6 +518,22 @@ export class FilterPanel extends ObservableReactComponent<filterProps> {
</div>
);
});
+ case 'togglebox':
+ return (
+ <div>
+ <input
+ style={{ width: 20, marginLeft: 20 }}
+ checked={['check', 'exists'].includes(
+ StrListCast(this.Document._childFilters)
+ .find(filter => filter.split(Doc.FilterSep)[0] === 'tags' && filter.split(Doc.FilterSep)[1] === facetHeader)
+ ?.split(Doc.FilterSep)[2] ?? ''
+ )}
+ type={'checkbox'}
+ onChange={undoable(e => Doc.setDocFilter(this.Document, 'tags', facetHeader, e.target.checked ? 'check' : 'remove'), 'set filter')}
+ />
+ -set-
+ </div>
+ );
case 'range':
{
diff --git a/src/client/views/InkingStroke.tsx b/src/client/views/InkingStroke.tsx
index 2e82371cb..498042938 100644
--- a/src/client/views/InkingStroke.tsx
+++ b/src/client/views/InkingStroke.tsx
@@ -331,9 +331,10 @@ export class InkingStroke extends ViewBoxAnnotatableComponent<FieldViewProps>()
1,
1,
'' as Gestures,
- 'none',
+ 'all',
1.0,
- false
+ false,
+ this.onPointerDown
)}
<InkControlPtHandles inkView={this} inkDoc={inkDoc} inkCtrlPoints={inkData} screenCtrlPoints={this.screenCtrlPts} nearestScreenPt={this.nearestScreenPt} screenSpaceLineWidth={screenSpaceCenterlineStrokeWidth} />
<InkTangentHandles inkView={this} inkDoc={inkDoc} screenCtrlPoints={this.screenCtrlPts} screenSpaceLineWidth={screenSpaceCenterlineStrokeWidth} />
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index 389207893..c852f9a2a 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -547,6 +547,10 @@ export class MainView extends ObservableReactComponent<object> {
fa.faSatellite,
fa.faStar,
fa.faFilePen,
+ fa.faCloud,
+ fa.faBolt,
+ fa.faLightbulb,
+ fa.faX,
]
);
}
diff --git a/src/client/views/PropertiesView.tsx b/src/client/views/PropertiesView.tsx
index f42838086..b9f928cba 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);
@@ -1178,7 +1178,7 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
@computed get filtersSubMenu() {
return (
// prettier-ignore
- <PropertiesSection title="Filters" isOpen={this.openFilters} setIsOpen={bool => { this.openFilters = bool; }} onDoubleClick={this.CloseAll}>
+ <PropertiesSection title="Filters" isOpen={this.openFilters} setIsOpen={action(bool => { this.openFilters = bool; })} onDoubleClick={this.CloseAll}>
<div className="propertiesView-content filters" style={{ position: 'relative', height: 'auto' }}>
<FilterPanel Document={this.selectedDoc ?? Doc.ActiveDashboard!} />
</div>
diff --git a/src/client/views/StyleProvider.tsx b/src/client/views/StyleProvider.tsx
index 262f888fb..16f6aa40b 100644
--- a/src/client/views/StyleProvider.tsx
+++ b/src/client/views/StyleProvider.tsx
@@ -19,12 +19,11 @@ import { SnappingManager } from '../util/SnappingManager';
import { undoable, UndoManager } from '../util/UndoManager';
import { TreeSort } from './collections/TreeSort';
import { Colors } from './global/globalEnums';
-import { TagsView } from './TagsView';
-import { CollectionFreeFormDocumentView } from './nodes/CollectionFreeFormDocumentView';
import { DocumentView, DocumentViewProps } from './nodes/DocumentView';
import { FieldViewProps } from './nodes/FieldView';
import { StyleProp } from './StyleProp';
import './StyleProvider.scss';
+import { TagsView } from './TagsView';
function toggleLockedPosition(doc: Doc) {
UndoManager.RunInBatch(() => Doc.toggleLockedPosition(doc), 'toggleBackground');
@@ -319,13 +318,12 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<FieldViewProps &
: childFilters?.().filter(f => ClientUtils.IsRecursiveFilter(f) && f !== ClientUtils.noDragDocsFilter).length || childFiltersByRanges?.().length
? 'orange' // 'inheritsFilter'
: undefined;
- return !showFilterIcon ? null : (
+ return !showFilterIcon || props?.hideFilterStatus ? null : (
<div className="styleProvider-filter">
<Dropdown
type={Type.TERT}
dropdownType={DropdownType.CLICK}
fillWidth
- // eslint-disable-next-line react/no-unstable-nested-components
iconProvider={() => <div className='styleProvider-filterShift'><FaFilter/></div>}
closeOnSelect
setSelectedVal={((dvValue: unknown) => {
@@ -365,7 +363,8 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<FieldViewProps &
</Tooltip>
);
};
- const tags = () => props?.DocumentView?.() ? <TagsView Views={[props.DocumentView()]}/> : null;
+ const tags = () => docView?.() ? <TagsView Views={[docView?.()]}/> : null;
+
return (
<>
{paint()}
diff --git a/src/client/views/TagsView.tsx b/src/client/views/TagsView.tsx
index be2c28185..f44fd1d03 100644
--- a/src/client/views/TagsView.tsx
+++ b/src/client/views/TagsView.tsx
@@ -9,7 +9,7 @@ import { emptyFunction } from '../../Utils';
import { Doc, DocListCast, Field, Opt, StrListCast } from '../../fields/Doc';
import { DocData } from '../../fields/DocSymbols';
import { List } from '../../fields/List';
-import { DocCast, StrCast } from '../../fields/Types';
+import { DocCast, NumCast, StrCast } from '../../fields/Types';
import { DocumentType } from '../documents/DocumentTypes';
import { DragManager } from '../util/DragManager';
import { SnappingManager } from '../util/SnappingManager';
@@ -18,6 +18,8 @@ import { ObservableReactComponent } from './ObservableReactComponent';
import './TagsView.scss';
import { DocumentView } from './nodes/DocumentView';
import { FaceRecognitionHandler } from './search/FaceRecognitionHandler';
+import { IconTagBox } from './nodes/IconTagBox';
+import { Id } from '../../fields/FieldSymbols';
/**
* The TagsView is a metadata input/display panel shown at the bottom of a DocumentView in a freeform collection.
@@ -27,8 +29,8 @@ import { FaceRecognitionHandler } from './search/FaceRecognitionHandler';
* the user can drag them off in order to display a collection of all documents that share the tag value.
*
* The tags that are added using the panel are the same as the #tags that can entered in a text Doc.
- * Note that tags starting with #@ display a metadata key/value pair instead of the tag itself.
- * e.g., '#@author' shows the document author
+ * Note that tags starting with @ display a metadata key/value pair instead of the tag itself.
+ * e.g., '@author' shows the document author
*
*/
@@ -59,7 +61,7 @@ export class TagItem extends ObservableReactComponent<TagItemProps> {
* @param tag tag string
* @returns tag collection Doc or undefined
*/
- public static findTagCollectionDoc = (tag: String) => TagItem.AllTagCollectionDocs.find(doc => doc.title === tag);
+ public static findTagCollectionDoc = (tag: string) => TagItem.AllTagCollectionDocs.find(doc => doc.title === tag);
/**
* Creates a Doc that collects Docs with the specified tag / value
@@ -82,6 +84,9 @@ export class TagItem extends ObservableReactComponent<TagItemProps> {
*/
public static allDocsWithTag = (tag: string) => DocListCast(TagItem.findTagCollectionDoc(tag)?.[DocData].docs);
+ public static docHasTag = (doc: Doc, tag: string) => {
+ return StrListCast(doc?.tags).includes(tag);
+ };
/**
* Adds a tag to the metadata of this document and adds the Doc to the corresponding tag collection Doc (or creates it)
* @param tag tag string
@@ -148,7 +153,7 @@ export class TagItem extends ObservableReactComponent<TagItemProps> {
private _ref: React.RefObject<HTMLDivElement>;
- constructor(props: any) {
+ constructor(props: TagItemProps) {
super(props);
makeObservable(this);
this._ref = React.createRef();
@@ -202,7 +207,10 @@ export class TagItem extends ObservableReactComponent<TagItemProps> {
return false;
},
returnFalse,
- emptyFunction
+ clickEv => {
+ clickEv.stopPropagation();
+ this._props.setToEditing();
+ }
);
e.preventDefault();
};
@@ -213,19 +221,18 @@ export class TagItem extends ObservableReactComponent<TagItemProps> {
render() {
this._props.tagDoc && setTimeout(() => this._props.docs.forEach(doc => TagItem.addTagToDoc(doc, this._props.tag))); // bcz: hack to make sure that Docs are added to their tag Doc collection since metadata can get set anywhere without a guard triggering an add to the collection
- const tag = this._props.tag.replace(/^#/, '');
- const metadata = tag.startsWith('@') ? tag.replace(/^@/, '') : '';
+ const metadata = this._props.tag.startsWith('@') ? this._props.tag.replace(/^@/, '') : '';
return (
- <div className={'tagItem' + (!this._props.tagDoc ? ' faceItem' : '')} onClick={this._props.setToEditing} onPointerDown={this.handleDragStart} ref={this._ref}>
+ <div className={'tagItem' + (!this._props.tagDoc ? ' faceItem' : '')} onPointerDown={this.handleDragStart} ref={this._ref}>
{metadata ? (
<span>
- <b style={{ fontSize: 'smaller' }}>{tag}&nbsp;</b>
+ <b style={{ fontSize: 'smaller' }}>{'@' + metadata}&nbsp;</b>
{typeof this.doc[metadata] === 'boolean' ? (
<input
type="checkbox"
onClick={e => e.stopPropagation()}
onPointerDown={e => e.stopPropagation()}
- onChange={undoable(e => (this.doc[metadata] = !this.doc[metadata]), 'metadata toggle')}
+ onChange={undoable(() => (this.doc[metadata] = !this.doc[metadata]), 'metadata toggle')}
checked={this.doc[metadata] as boolean}
/>
) : (
@@ -233,7 +240,7 @@ export class TagItem extends ObservableReactComponent<TagItemProps> {
)}
</span>
) : (
- tag
+ this._props.tag
)}
{this.props.showRemoveUI && this._props.tagDoc && (
<IconButton
@@ -257,7 +264,7 @@ interface TagViewProps {
*/
@observer
export class TagsView extends ObservableReactComponent<TagViewProps> {
- constructor(props: any) {
+ constructor(props: TagViewProps) {
super(props);
makeObservable(this);
}
@@ -271,7 +278,7 @@ export class TagsView extends ObservableReactComponent<TagViewProps> {
componentDidMount() {
this._heightDisposer = reaction(
() => this.View.screenToContentsTransform(),
- xf => {
+ () => {
this._panelHeightDirty = this._panelHeightDirty + 1;
}
);
@@ -284,11 +291,17 @@ export class TagsView extends ObservableReactComponent<TagViewProps> {
return this._props.Views.lastElement();
}
+ // x: 1 => 1/vs 0 => 1 1/(vs - (1-x)*(vs-1))
@computed get currentScale() {
- return this._props.Views.length > 1 ? 1 : Math.max(1, 1 / this.View.screenToLocalScale());
+ if (this._props.Views.length > 1) return 1;
+ const x = NumCast(this.View.Document.height) / this.View.screenToContentsTransform().Scale / 80;
+ const xscale = x >= 1 ? 0 : 1 / (1 + x * (this.View.screenToLocalScale() - 1)); //docheight / this.View.screenToContentsTransform().Scale / 35 / this.View.screenToLocalScale() - ;
+ const y = NumCast(this.View.Document.width) / this.View.screenToContentsTransform().Scale / 200;
+ const yscale = y >= 1 ? 0 : 1 / (1 + y * (this.View.screenToLocalScale() - 1)); //docheight / this.View.screenToContentsTransform().Scale / 35 / this.View.screenToLocalScale() - ;
+ return Math.max(xscale, yscale, 1 / this.View.screenToLocalScale());
}
@computed get isEditing() {
- return this._isEditing && (this._props.Views.length > 1 || DocumentView.SelectedDocs().includes(this.View.Document));
+ return this._isEditing && (this._props.Views.length > 1 || (DocumentView.Selected().length === 1 && DocumentView.Selected().includes(this.View)));
}
/**
@@ -312,7 +325,7 @@ export class TagsView extends ObservableReactComponent<TagViewProps> {
const submittedLabel = tag.trim().replace(/^#/, '').split(':');
if (submittedLabel[0]) {
this._props.Views.forEach(view => {
- TagItem.addTagToDoc(view.Document, '#' + submittedLabel[0]);
+ TagItem.addTagToDoc(view.Document, (submittedLabel[0].startsWith('@') ? '' : '#') + submittedLabel[0]);
if (submittedLabel.length > 1) Doc.SetField(view.Document, submittedLabel[0].replace(/^@/, ''), ':' + submittedLabel[1]);
});
}
@@ -336,11 +349,12 @@ export class TagsView extends ObservableReactComponent<TagViewProps> {
);
this._panelHeightDirty;
- return this.View.ComponentView?.isUnstyledView?.() || (!this.View.Document._layout_showTags && this._props.Views.length === 1) ? null : (
+ return this.View.ComponentView?.isUnstyledView?.() || (!this.View.showTags && this._props.Views.length === 1) ? null : (
<div
className="tagsView-container"
ref={r => r && new ResizeObserver(action(() => this._props.Views.length === 1 && (this.View.TagPanelHeight = Math.max(0, (r?.getBoundingClientRect().height ?? 0) - this.InsetDist)))).observe(r)}
style={{
+ display: SnappingManager.IsResizing === this.View.Document[Id] ? 'none' : undefined,
transformOrigin: 'top left',
maxWidth: `${100 * this.currentScale}%`,
width: 'max-content',
@@ -352,21 +366,34 @@ export class TagsView extends ObservableReactComponent<TagViewProps> {
}}>
<div className="tagsView-content" style={{ width: '100%' }}>
<div className="tagsView-list">
- {!tagsList.size && !facesList.size ? null : ( //
- <IconButton style={{ width: '8px' }} tooltip="Close Menu" onPointerDown={() => this.setToEditing(!this._isEditing)} icon={<FontAwesomeIcon icon={this._isEditing ? 'chevron-up' : 'chevron-down'} size="sm" />} />
- )}
- {Array.from(tagsList).map((tag, i) => (
- <TagItem
- key={i}
- docs={this._props.Views.map(view => view.Document)}
- tag={tag}
- tagDoc={TagItem.findTagCollectionDoc(tag) ?? TagItem.createTagCollectionDoc(tag)}
- setToEditing={this.setToEditing}
- showRemoveUI={this.isEditing}
+ {this._props.Views.length === 1 && !this.View.showTags ? null : ( //
+ <IconButton
+ style={{ width: '8px' }}
+ tooltip="Close Menu"
+ onPointerDown={e =>
+ setupMoveUpEvents(this, e, returnFalse, emptyFunction, upEv => {
+ this.setToEditing(!this._isEditing);
+ upEv.stopPropagation();
+ })
+ }
+ icon={<FontAwesomeIcon icon={this._isEditing ? 'chevron-up' : 'chevron-down'} size="sm" />}
/>
- ))}
- {Array.from(facesList).map((tag, i) => (
- <TagItem key={i} docs={this._props.Views.map(view => view.Document)} tag={tag} tagDoc={undefined} setToEditing={this.setToEditing} showRemoveUI={this.isEditing} />
+ )}
+ <IconTagBox Views={this._props.Views} IsEditing={this._isEditing} />
+ {Array.from(tagsList)
+ .filter(tag => (tag.startsWith('#') || tag.startsWith('@')) && !Doc.MyFilterHotKeys.some(key => key.toolType === tag))
+ .map(tag => (
+ <TagItem
+ key={tag}
+ docs={this._props.Views.map(view => view.Document)}
+ tag={tag}
+ tagDoc={TagItem.findTagCollectionDoc(tag) ?? TagItem.createTagCollectionDoc(tag)}
+ setToEditing={this.setToEditing}
+ showRemoveUI={this.isEditing}
+ />
+ ))}
+ {Array.from(facesList).map(tag => (
+ <TagItem key={tag} docs={this._props.Views.map(view => view.Document)} tag={tag} tagDoc={undefined} setToEditing={this.setToEditing} showRemoveUI={this.isEditing} />
))}
</div>
{this.isEditing ? (
@@ -388,9 +415,9 @@ export class TagsView extends ObservableReactComponent<TagViewProps> {
/>
</div>
<div className="tagsView-suggestions-box">
- {TagItem.AllTagCollectionDocs.map((doc, i) => {
- const tag = StrCast(doc.title);
- return (
+ {TagItem.AllTagCollectionDocs.map(doc => StrCast(doc.title))
+ .filter(tag => (tag.startsWith('#') || tag.startsWith('@')) && !Doc.MyFilterHotKeys.some(key => key.toolType === tag))
+ .map(tag => (
<Button
style={{ margin: '2px 2px', border: '1px solid black', backgroundColor: 'lightblue', color: 'black' }}
text={tag}
@@ -399,20 +426,17 @@ export class TagsView extends ObservableReactComponent<TagViewProps> {
onClick={() => this.submitTag(tag)}
key={tag}
/>
- );
- })}
- {Array.from(chatTagsList).map(tag => {
- return (
- <Button
- style={{ margin: '2px 2px', border: '1px solid black', backgroundColor: 'lightpink', color: 'black' }}
- text={tag}
- color={SnappingManager.userVariantColor}
- tooltip="Add existing tag"
- onClick={() => this.submitTag(tag)}
- key={tag}
- />
- );
- })}
+ ))}
+ {Array.from(chatTagsList).map(tag => (
+ <Button
+ style={{ margin: '2px 2px', border: '1px solid black', backgroundColor: 'lightpink', color: 'black' }}
+ text={tag}
+ color={SnappingManager.userVariantColor}
+ tooltip="Add existing tag"
+ onClick={() => this.submitTag(tag)}
+ key={tag}
+ />
+ ))}
</div>
</div>
) : null}
diff --git a/src/client/views/collections/CollectionCardDeckView.scss b/src/client/views/collections/CollectionCardDeckView.scss
index cc797d0bd..d1731c244 100644
--- a/src/client/views/collections/CollectionCardDeckView.scss
+++ b/src/client/views/collections/CollectionCardDeckView.scss
@@ -8,19 +8,13 @@
overflow: hidden;
button {
- width: 35px;
- height: 35px;
border-radius: 50%;
- background-color: $dark-gray;
- // border-color: $medium-blue;
- margin: 5px; // transform: translateY(-50px);
}
}
.card-wrapper {
display: grid;
grid-template-columns: repeat(10, 1fr);
- // width: 100%;
transform-origin: top left;
position: absolute;
@@ -31,45 +25,18 @@
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 */
+.no-card-span {
+ position: relative;
+ width: fit-content;
+ text-align: center;
+ font-size: 65px;
}
-// button:hover {
-// transform: translateY(-50px);
-// }
-
-// .card-wrapper::after {
-// content: "";
-// width: 100%; /* Forces wrapping */
-// }
-
-// .card-wrapper > .card-item:nth-child(10n)::after {
-// content: "";
-// width: 100%; /* Forces wrapping after every 10th item */
-// }
-
-// .card-row{
-// display: flex;
-// position: absolute;
-// align-items: center;
-// transition: transform 0.3s cubic-bezier(0.455, 0.03, 0.515, 0.955);
-
-// }
-
.card-item-inactive,
.card-item-active,
.card-item {
position: relative;
- transition: transform 0.5s ease-in-out;
+ transition: transform 0.3s ease-in-out;
display: flex;
flex-direction: column;
}
@@ -79,6 +46,5 @@
}
.card-item-active {
- position: absolute;
z-index: 100;
}
diff --git a/src/client/views/collections/CollectionCardDeckView.tsx b/src/client/views/collections/CollectionCardDeckView.tsx
index 37b1adeff..66014a1e0 100644
--- a/src/client/views/collections/CollectionCardDeckView.tsx
+++ b/src/client/views/collections/CollectionCardDeckView.tsx
@@ -1,20 +1,24 @@
-import { IReactionDisposer, ObservableMap, action, computed, makeObservable, observable, reaction } from 'mobx';
+import { IReactionDisposer, ObservableMap, action, computed, makeObservable, observable, reaction, trace } from 'mobx';
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 } from '../../../Utils';
+import { Doc } from '../../../fields/Doc';
import { DocData } from '../../../fields/DocSymbols';
import { Id } from '../../../fields/FieldSymbols';
-import { BoolCast, Cast, DateCast, NumCast, RTFCast, ScriptCast, StrCast } from '../../../fields/Types';
+import { List } from '../../../fields/List';
+import { BoolCast, DateCast, DocCast, NumCast, RTFCast, ScriptCast, StrCast } from '../../../fields/Types';
import { URLField } from '../../../fields/URLField';
import { gptImageLabel } from '../../apis/gpt/GPT';
import { DocumentType } from '../../documents/DocumentTypes';
import { DragManager } from '../../util/DragManager';
+import { dropActionType } from '../../util/DropActionTypes';
+import { SelectionManager } from '../../util/SelectionManager';
import { SnappingManager } from '../../util/SnappingManager';
import { Transform } from '../../util/Transform';
import { undoable } from '../../util/UndoManager';
import { StyleProp } from '../StyleProp';
+import { TagItem } from '../TagsView';
import { DocumentView } from '../nodes/DocumentView';
import { GPTPopup, GPTPopupMode } from '../pdf/GPTPopup/GPTPopup';
import './CollectionCardDeckView.scss';
@@ -25,24 +29,31 @@ enum cardSortings {
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;
- private _childDocumentWidth = 600; // target width of a Doc...
private _disposers: { [key: string]: IReactionDisposer } = {};
private _textToDoc = new Map<string, Doc>();
+ private _dropped = false; // indicate when a card doc has just moved;
- @observable _forceChildXf = false;
- @observable _isLoading = false;
+ @observable _forceChildXf = 0;
@observable _hoveredNodeIndex = -1;
@observable _docRefs = new ObservableMap<Doc, DocumentView>();
@observable _maxRowCount = 10;
-
- static getButtonGroup(groupFieldKey: 'chat' | 'star' | 'idea' | 'like', doc: Doc): number | undefined {
- return Cast(doc[groupFieldKey], 'number', null);
- }
+ @observable _docDraggedIndex: number = -1;
+ @observable overIndex: number = -1;
static imageUrlToBase64 = async (imageUrl: string): Promise<string> => {
try {
@@ -61,22 +72,56 @@ export class CollectionCardView extends CollectionSubView() {
}
};
+ constructor(props: SubCollectionViewProps) {
+ super(props);
+ makeObservable(this);
+ this.setRegenerateCallback();
+ }
protected createDashEventsTarget = (ele: HTMLDivElement | null) => {
this._dropDisposer?.();
if (ele) {
this._dropDisposer = DragManager.MakeDropTarget(ele, this.onInternalDrop.bind(this), this.layoutDoc);
}
};
+ /**
+ * Callback to ensure gpt's text versions of the child docs are updated
+ */
+ setRegenerateCallback = () => GPTPopup.Instance.setRegenerateCallback(this.childPairStringListAndUpdateSortDesc);
- constructor(props: SubCollectionViewProps) {
- super(props);
- makeObservable(this);
- }
+ /**
+ * 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 {
+ componentDidMount() {
+ this._props.setContentViewBox?.(this);
+ // 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;
+ }
+ }
+ );
+ // if card deck moves, then the child doc views are hidden so their screen to local transforms will return empty rectangles
+ // when inquired from the dom (below in childScreenToLocal). When the doc is actually renders, we need to act like the
+ // dash data just changed and trigger a React involidation with the correct data (read from the dom).
+ this._disposers.child = reaction(
+ () => [this.Document.x, this.Document.y],
+ () => {
+ if (!Array.from(this._docRefs.values()).every(dv => dv.ContentDiv?.getBoundingClientRect().width)) {
+ setTimeout(action(() => this._forceChildXf++));
+ }
+ }
);
}
@@ -85,56 +130,40 @@ export class CollectionCardView extends CollectionSubView() {
this._dropDisposer?.();
}
- @computed get cardSort_customField() {
- return StrCast(this.Document.cardSort_customField) as 'chat' | 'star' | 'idea' | 'like';
- }
-
@computed get cardSort() {
return StrCast(this.Document.cardSort) as cardSortings;
}
+
+ /**
+ * 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() {
+ return this.childDocs.filter(l => l.type !== DocumentType.LINK);
+ }
+
/**
* how much to scale down the contents of the view so that everything will fit
*/
@computed get fitContentScale() {
const length = Math.min(this.childDocsWithoutLinks.length, this._maxRowCount);
- return (this._childDocumentWidth * length) / this._props.PanelWidth();
- }
-
- @computed get translateWrapperX() {
- let translate = 0;
-
- if (this.inactiveDocs().length !== this.childDocsWithoutLinks.length && this.inactiveDocs().length < 10) {
- translate += this.panelWidth() / 2;
- }
- return translate;
+ return (this.childPanelWidth() * length) / this._props.PanelWidth();
}
/**
- * The child documents to be rendered-- either all of them except the Links or the docs in the currently active
- * custom group
+ * When in quiz mode, randomly selects a document
*/
- @computed get childDocsWithoutLinks() {
- const regularDocs = this.childDocs.filter(l => l.type !== DocumentType.LINK);
- const activeGroups = NumListCast(this.Document.cardSort_visibleSortGroups);
-
- if (activeGroups.length > 0 && this.cardSort === cardSortings.Custom) {
- return regularDocs.filter(doc => {
- // Get the group number for the current index
- const groupNumber = CollectionCardView.getButtonGroup(this.cardSort_customField, doc);
- // Check if the group number is in the active groups
- return groupNumber !== undefined && activeGroups.includes(groupNumber);
- });
- }
-
- // Default return for non-custom cardSort or other cases, filtering out links
- return regularDocs;
- }
+ quizMode = () => {
+ const randomIndex = Math.floor(Math.random() * this.childDocs.length);
+ SelectionManager.DeselectAll();
+ DocumentView.SelectView(DocumentView.getDocumentView(this.childDocs[randomIndex]), false);
+ };
/**
- * Determines the order in which the cards will be rendered depending on the current sort type
+ * Number of rows of cards to be rendered
*/
- @computed get sortedDocs() {
- return this.sort(this.childDocsWithoutLinks, this.cardSort, BoolCast(this.layoutDoc.sortDesc));
+ @computed get numRows() {
+ return Math.ceil(this.sortedDocs.length / this._maxRowCount);
}
@action
@@ -157,8 +186,8 @@ export class CollectionCardView extends CollectionSubView() {
*/
inactiveDocs = () => this.childDocsWithoutLinks.filter(d => !DocumentView.SelectedDocs().includes(d));
- panelWidth = () => this._childDocumentWidth;
- panelHeight = (layout: Doc) => () => (this.panelWidth() * NumCast(layout._height)) / NumCast(layout._width);
+ childPanelWidth = () => NumCast(this.layoutDoc.childPanelWidth, this._props.PanelWidth() / 2);
+ childPanelHeight = () => this._props.PanelHeight() * this.fitContentScale;
onChildDoubleClick = () => ScriptCast(this.layoutDoc.onChildDoubleClick);
isContentActive = () => this._props.isSelected() || this._props.isContentActive() || this._props.isAnyChildContentActive();
isChildContentActive = () => !!this.isContentActive();
@@ -170,6 +199,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)));
@@ -188,11 +219,12 @@ export class CollectionCardView extends CollectionSubView() {
translateY = (amCards: number, index: number, realIndex: number) => {
const evenOdd = amCards % 2;
const apex = (amCards - evenOdd) / 2;
- const stepMag = 200 / ((amCards - evenOdd) / 2) + Math.abs((apex - index) * 25);
+ const Magnitude = this.childPanelWidth() / 2; // 400
+ const stepMag = Magnitude / 2 / ((amCards - evenOdd) / 2) + Math.abs((apex - index) * 25);
let rowOffset = 0;
if (realIndex > this._maxRowCount - 1) {
- rowOffset = 400 * ((realIndex - (realIndex % this._maxRowCount)) / this._maxRowCount);
+ rowOffset = Magnitude * ((realIndex - (realIndex % this._maxRowCount)) / this._maxRowCount);
}
if (evenOdd === 1 || index < apex - 1) {
return Math.abs(stepMag * (apex - index)) - rowOffset;
@@ -205,27 +237,108 @@ 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
*/
- translateSelected = (index: number): number => {
- // if (this.isSelected(index)) {
- const middleOfPanel = this._props.PanelWidth() / 2;
- const scaledNodeWidth = this.panelWidth() * 1.25;
+ 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
- // 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;
+ if (adjustedX < 0) {
+ return 0; // Before the first column
+ }
- // Calculate the translation needed to align the scaled node's center with the panel's center
- const translation = middleOfPanel - scaledNodeCenter - scaledNodeWidth - scaledNodeWidth / 4;
+ 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;
- return translation;
+ 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
+ */
+
+ @action
+ onPointerMove = (x: number, y: number) => {
+ this._docDraggedIndex = DragManager.docsBeingDragged.length ? this.findCardDropIndex(x, y) : -1;
+ };
+
+ /**
+ * Handles external drop of images/PDFs etc from outside Dash.
+ */
+ onExternalDrop = async (e: React.DragEvent): Promise<void> => {
+ super.onExternalDrop(e, {});
+ };
+
+ /**
+ * 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(
+ action((e: Event, de: DragManager.DropEvent) => {
+ if (de.complete.docDragData) {
+ const dragIndex = this._docDraggedIndex;
+ const draggedDoc = DragManager.docsBeingDragged[0];
+ if (dragIndex > -1 && draggedDoc) {
+ this._docDraggedIndex = -1;
+ const sorted = this.sortedDocs;
+ const originalIndex = sorted.findIndex(doc => doc === draggedDoc);
+
+ this.Document.cardSort = '';
+ originalIndex !== -1 && sorted.splice(originalIndex, 1);
+ sorted.splice(dragIndex, 0, draggedDoc);
+ if (de.complete.docDragData.removeDocument?.(draggedDoc)) {
+ this.dataDoc[this.fieldKey] = new List<Doc>(sorted);
+ }
+ this._dropped = true;
+ }
+ e.stopPropagation();
+ return true;
+ }
+ return false;
+ }),
+ ''
+ );
+
+ @computed 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) =>
+ Doc.MyFilterHotKeys.map((key, i) => ({ has: TagItem.docHasTag(doc, StrCast(key.toolType)), i }))
+ .filter(({ has }) => has)
+ .map(({ i }) => i)
+ .join('.');
+
+ /**
* 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,37 +346,46 @@ export class CollectionCardView extends CollectionSubView() {
* @param isDesc
* @returns
*/
- sort = (docs: Doc[], sortType: cardSortings, isDesc: boolean) => {
- if (sortType === cardSortings.None) return docs;
- docs.sort((docA, docB) => {
- const [typeA, typeB] = (() => {
- switch (sortType) {
- case cardSortings.Time:
- return [DateCast(docA.author_date)?.date ?? Date.now(),
- DateCast(docB.author_date)?.date ?? Date.now()];
- case cardSortings.Color:
- return [DashColor(StrCast(docA.backgroundColor)).hsv().toString(), // If docA.type is undefined, use an empty string
- DashColor(StrCast(docB.backgroundColor)).hsv().toString()]; // If docB.type is undefined, use an empty string
- case cardSortings.Custom:
- return [CollectionCardView.getButtonGroup(this.cardSort_customField, docA)??0,
- CollectionCardView.getButtonGroup(this.cardSort_customField, docB)??0];
- default: return [StrCast(docA.type), // If docA.type is undefined, use an empty string
- StrCast(docB.type)]; // If docB.type is undefined, use an empty string
- } // prettier-ignore
- })();
-
- const out = typeA < typeB ? -1 : typeA > typeB ? 1 : 0;
- return isDesc ? -out : out; // Reverse the sort order if descending is true
- });
+ sort = (docsIn: Doc[], sortType: cardSortings, isDesc: boolean, dragIndex: number) => {
+ const docs = docsIn.slice(); // need make new object list since sort() modifies the incoming list which confuses mobx caching
+ sortType &&
+ 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: {
+ const d1 = DashColor(StrCast(docA.backgroundColor));
+ const d2 = DashColor(StrCast(docB.backgroundColor));
+ return [d1.hsv().hue(), d2.hsv().hue()];
+ }
+ 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;
+ });
+ if (dragIndex !== -1) {
+ const draggedDoc = DragManager.docsBeingDragged[0];
+ const originalIndex = docs.findIndex(doc => doc === draggedDoc);
+
+ originalIndex !== -1 && docs.splice(originalIndex, 1);
+ draggedDoc && docs.splice(dragIndex, 0, draggedDoc);
+ }
return docs;
};
displayDoc = (doc: Doc, screenToLocalTransform: () => Transform) => (
<DocumentView
- // eslint-disable-next-line react/jsx-props-no-spreading
{...this._props}
- ref={action((r: DocumentView) => r?.ContentDiv && this._docRefs.set(doc, r))}
+ ref={action((r: DocumentView) => (!r?.ContentDiv ? this._docRefs.delete(doc) : this._docRefs.set(doc, r)))}
Document={doc}
NativeWidth={returnZero}
NativeHeight={returnZero}
@@ -272,11 +394,16 @@ export class CollectionCardView extends CollectionSubView() {
renderDepth={this._props.renderDepth + 1}
LayoutTemplate={this._props.childLayoutTemplate}
LayoutTemplateString={this._props.childLayoutString}
+ containerViewPath={this.childContainerViewPath}
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)}
+ PanelWidth={this.childPanelWidth}
+ PanelHeight={this.childPanelHeight}
+ dontCenter="y" // Don't center it vertically, because the grid it's in is already doing that and we don't want to do it twice.
+ dragAction={(this.Document.childDragAction ?? this._props.childDragAction) as dropActionType}
+ showTags={true}
+ dontHideOnDrag
/>
);
@@ -286,24 +413,24 @@ 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)) {
+ if (index < totalCards - (totalCards % this._maxRowCount)) {
return this._maxRowCount;
}
// (3)
- return totalCards % 10;
+ return totalCards % this._maxRowCount;
};
/**
* Determines the index a card is in in a row
* @param realIndex
* @returns
*/
- overflowIndexCalc = (realIndex: number) => realIndex % 10;
+ overflowIndexCalc = (realIndex: number) => realIndex % this._maxRowCount;
/**
* Translates the cards in the second rows and beyond over to the right
* @param realIndex
@@ -311,7 +438,7 @@ export class CollectionCardView extends CollectionSubView() {
* @param calcRowCards
* @returns
*/
- translateOverflowX = (realIndex: number, calcRowCards: number) => (realIndex < this._maxRowCount ? 0 : (10 - calcRowCards) * (this.panelWidth() / 2));
+ translateOverflowX = (realIndex: number, calcRowCards: number) => (realIndex < this._maxRowCount ? 0 : (this._maxRowCount - calcRowCards) * (this.childPanelWidth() / 2));
/**
* Determines how far to translate a card in the y direction depending on its index, whether or not its being hovered, or if it's selected
@@ -323,22 +450,15 @@ 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;
- return trans + this.translateY(amCards, calcRowIndex, realIndex);
+ 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;
+ return 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 +475,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,81 +498,113 @@ 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 = Doc.MyFilterHotKeys;
+
+ let myTag = '';
+
+ if (tag) {
+ for (let i = 0; i < allHotKeys.length; i++) {
+ // bcz: CHECK THIS CODE OUT -- SOMETHING CHANGED
+ const keyTag = StrCast(allHotKeys[i].toolType);
+ if (tag.includes(keyTag)) {
+ myTag = keyTag;
+ 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 = () => {
+ @computed get renderCards() {
+ const sortedDocs = this.sortedDocs;
const anySelected = this.childDocs.some(doc => DocumentView.SelectedDocs().includes(doc));
+ const isEmpty = this.childDocsWithoutLinks.length === 0;
+
+ 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);
+ return sortedDocs.map((doc, index) => {
+ const realIndex = sortedDocs.indexOf(doc);
const calcRowIndex = this.overflowIndexCalc(realIndex);
const amCards = this.overflowAmCardsCalc(realIndex);
- const isSelected = DocumentView.SelectedDocs().includes(doc);
+ const view = DocumentView.getDocumentView(doc, this.DocumentView?.());
+ const isSelected = view?.ComponentView?.isAnyChildContentActive?.() || view?.IsSelected ? true : false;
const childScreenToLocal = () => {
+ // need to explicitly trigger an invalidation since we're reading everything from the Dom
this._forceChildXf;
+ this._props.ScreenToLocalTransform();
+
const dref = this._docRefs.get(doc);
const { translateX, translateY, scale } = ClientUtils.GetScreenTransform(dref?.ContentDiv);
return new Transform(-translateX + (dref?.centeringX || 0) * scale,
@@ -459,41 +612,62 @@ 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, sortedDocs.length - rowIndex * this._maxRowCount) / 2;
+ return (rowCenterIndex - indexInRow) * 100 - 50;
+ };
+ const aspect = NumCast(doc.height) / NumCast(doc.width, 1);
+ const vscale = Math.max(1,Math.min((this._props.PanelHeight() * 0.95 * this.fitContentScale) / (aspect * this.childPanelWidth()),
+ (this._props.PanelHeight() - 80) / (aspect * (this._props.PanelWidth() / 10)))); // prettier-ignore
+ const hscale = Math.min(this.sortedDocs.length, this._maxRowCount) / 2; // bcz: hack - the grid is divided evenly into maxRowCount cells, so the max scaling would be maxRowCount -- but making things that wide is ugly, so cap it off at half the window size
return (
<div
key={doc[Id]}
className={`card-item${isSelected ? '-active' : anySelected ? '-inactive' : ''}`}
- onPointerUp={() => {
- // this turns off documentDecorations during a transition, then turns them back on afterward.
- SnappingManager.SetIsResizing(this.Document[Id]);
- setTimeout(
- action(() => {
- SnappingManager.SetIsResizing(undefined);
- this._forceChildXf = !this._forceChildXf;
- }),
- 700
- );
- }}
+ onPointerUp={action(() => {
+ // if a card doc has just moved, or a card is selected and in front, then ignore this event
+ if (DocumentView.SelectedDocs().includes(doc) || this._dropped) {
+ this._dropped = false;
+ } else {
+ // otherwise, turn off documentDecorations becase we're in a selection transition and want to avoid artifacts.
+ // Turn them back on when the animation has completed and the render and backend structures are in synch
+ SnappingManager.SetIsResizing(doc[Id]);
+ setTimeout(
+ action(() => {
+ SnappingManager.SetIsResizing(undefined);
+ this._forceChildXf++;
+ }),
+ 600
+ );
+ }
+ })}
style={{
- width: this.panelWidth(),
- height: 'max-content', // this.panelHeight(childPair.layout)(),
+ width: this.childPanelWidth(),
+ 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})`,
- }}
- onMouseEnter={() => this.setHoveredNodeIndex(index)}>
+ scale(${isSelected ? `${Math.min(hscale, vscale) * 100}%` : this._hoveredNodeIndex === index ? 1.05 : 1})`,
+ }} // prettier-ignore
+ onPointerEnter={() => !SnappingManager.IsDragging && this.setHoveredNodeIndex(index)}>
{this.displayDoc(doc, childScreenToLocal)}
- {this.renderButtons(doc, this.cardSort)}
</div>
);
});
- };
+ }
+
render() {
+ const isEmpty = this.childDocsWithoutLinks.length === 0;
+
return (
<div
className="collectionCardView-outer"
- ref={this.createDashEventsTarget}
+ ref={(ele: HTMLDivElement | null) => this.createDashEventsTarget(ele)}
+ onPointerLeave={action(() => (this._docDraggedIndex = -1))}
+ onPointerMove={e => this.onPointerMove(...this._props.ScreenToLocalTransform().transformPoint(e.clientX, e.clientY))}
+ onDrop={this.onExternalDrop.bind(this)}
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,11 +675,12 @@ 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: `scale(${1 / this.fitContentScale})` }),
+ ...(!isEmpty && { height: `${100 * this.fitContentScale}%` }),
+ gridAutoRows: `${100 / this.numRows}%`,
}}
onMouseLeave={() => this.setHoveredNodeIndex(-1)}>
- {this.renderCards()}
+ {this.renderCards}
</div>
</div>
);
diff --git a/src/client/views/collections/CollectionCarousel3DView.tsx b/src/client/views/collections/CollectionCarousel3DView.tsx
index 54cc02825..139aebb02 100644
--- a/src/client/views/collections/CollectionCarousel3DView.tsx
+++ b/src/client/views/collections/CollectionCarousel3DView.tsx
@@ -14,6 +14,7 @@ import { DocumentView } from '../nodes/DocumentView';
import { FocusViewOptions } from '../nodes/FocusViewOptions';
import './CollectionCarousel3DView.scss';
import { CollectionSubView, SubCollectionViewProps } from './CollectionSubView';
+import { Transform } from '../../util/Transform';
// eslint-disable-next-line @typescript-eslint/no-var-requires
const { CAROUSEL3D_CENTER_SCALE, CAROUSEL3D_SIDE_SCALE, CAROUSEL3D_TOP } = require('../global/globalCssVariables.module.scss');
@@ -46,15 +47,32 @@ export class CollectionCarousel3DView extends CollectionSubView() {
}
centerScale = Number(CAROUSEL3D_CENTER_SCALE);
+ sideScale = Number(CAROUSEL3D_SIDE_SCALE);
panelWidth = () => this._props.PanelWidth() / 3;
- panelHeight = () => this._props.PanelHeight() * Number(CAROUSEL3D_SIDE_SCALE);
+ panelHeight = () => this._props.PanelHeight() * this.sideScale;
onChildDoubleClick = () => ScriptCast(this.layoutDoc.onChildDoubleClick);
isContentActive = () => this._props.isSelected() || this._props.isContentActive() || this._props.isAnyChildContentActive();
isChildContentActive = () => !!this.isContentActive();
- childScreenToLocal = () =>
- this._props // document's left is the panel shifted by the doc's index * panelWidth/#docs. But it scales by centerScale around its center, so it's left moves left by the distance of the left from the center (panelwidth/2) * the scale delta (centerScale-1)
- .ScreenToLocalTransform() // the top behaves the same way ecept it's shifted by the 'top' amount specified for the panel in css and then by the scale factor.
- .translate(-this.panelWidth() + ((this.centerScale - 1) * this.panelWidth()) / 2, -((Number(CAROUSEL3D_TOP) / 100) * this._props.PanelHeight()) + ((this.centerScale - 1) * this.panelHeight()) / 2)
+ childScreenLeftToLocal = () =>
+ this._props
+ .ScreenToLocalTransform()
+ .scale(this._props.NativeDimScaling?.() || 1)
+ .translate(-(this.panelWidth() - this.panelWidth() * this.sideScale) / 2, -(this.panelHeight() - this.panelHeight() * this.sideScale) / 2 - (Number(CAROUSEL3D_TOP) / 100) * this._props.PanelHeight())
+ .scale(1 / this.sideScale);
+ childScreenRightToLocal = () =>
+ this._props
+ .ScreenToLocalTransform()
+ .scale(this._props.NativeDimScaling?.() || 1)
+ .translate(-2 * this.panelWidth() - (this.panelWidth() - this.panelWidth() * this.sideScale) / 2, -(this.panelHeight() - this.panelHeight() * this.sideScale) / 2 - (Number(CAROUSEL3D_TOP) / 100) * this._props.PanelHeight())
+ .scale(1 / this.sideScale);
+ childCenterScreenToLocal = () =>
+ this._props
+ .ScreenToLocalTransform()
+ .scale(this._props.NativeDimScaling?.() || 1)
+ .translate(
+ -this.panelWidth() + ((this.centerScale - 1) * this.panelWidth()) / 2, // Focused Doc is shifted right by 1/3 panel width then left by increased size percent of center * 1/2 * panel width / 3
+ -((Number(CAROUSEL3D_TOP) / 100) * this._props.PanelHeight()) + ((this.centerScale - 1) * this.panelHeight()) / 2
+ ) // top is top margin % of panelHeight - increased size percent of center * panelHeight / 2
.scale(1 / this.centerScale);
focus = (anchor: Doc, options: FocusViewOptions): Opt<number> => {
@@ -66,9 +84,10 @@ export class CollectionCarousel3DView extends CollectionSubView() {
index !== -1 && (this.layoutDoc._carousel_index = index);
return undefined;
};
+
@computed get content() {
const currentIndex = NumCast(this.layoutDoc._carousel_index);
- const displayDoc = (childPair: { layout: Doc; data: Doc }) => (
+ const displayDoc = (childPair: { layout: Doc; data: Doc }, dxf: () => Transform) => (
<DocumentView
// eslint-disable-next-line react/jsx-props-no-spreading
{...this._props}
@@ -84,7 +103,7 @@ export class CollectionCarousel3DView extends CollectionSubView() {
LayoutTemplate={this._props.childLayoutTemplate}
LayoutTemplateString={this._props.childLayoutString}
focus={this.focus}
- ScreenToLocalTransform={this.childScreenToLocal}
+ ScreenToLocalTransform={dxf}
isContentActive={this.isChildContentActive}
isDocumentActive={this._props.childDocumentsActive?.() || this.Document._childDocumentsActive ? this._props.isDocumentActive : this.isContentActive}
PanelWidth={this.panelWidth}
@@ -94,7 +113,7 @@ export class CollectionCarousel3DView extends CollectionSubView() {
return this.carouselItems.map((childPair, index) => (
<div key={childPair.layout[Id]} className={`collectionCarousel3DView-item${index === currentIndex ? '-active' : ''} ${index}`} style={{ width: this.panelWidth() }}>
- {displayDoc(childPair)}
+ {displayDoc(childPair, index < currentIndex ? this.childScreenLeftToLocal : index === currentIndex ? this.childCenterScreenToLocal : this.childScreenRightToLocal)}
</div>
));
}
diff --git a/src/client/views/collections/CollectionCarouselView.scss b/src/client/views/collections/CollectionCarouselView.scss
index c40f471d6..af617254a 100644
--- a/src/client/views/collections/CollectionCarouselView.scss
+++ b/src/client/views/collections/CollectionCarouselView.scss
@@ -85,7 +85,6 @@
.carouselView-quiz {
position: relative;
display: flex;
- flex-direction: column;
height: 20px;
align-items: center;
margin: auto;
@@ -127,7 +126,7 @@
align-items: center;
display: flex;
top: 2px;
- right: 2px;
+ left: 2px;
width: 30;
border-radius: 5px;
color: rgba(255, 255, 255, 0.5);
diff --git a/src/client/views/collections/CollectionCarouselView.tsx b/src/client/views/collections/CollectionCarouselView.tsx
index 04a0320fc..108cdbdb4 100644
--- a/src/client/views/collections/CollectionCarouselView.tsx
+++ b/src/client/views/collections/CollectionCarouselView.tsx
@@ -1,11 +1,10 @@
-/* eslint-disable react/jsx-props-no-spreading */
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { Tooltip } from '@mui/material';
import { IReactionDisposer, action, computed, makeObservable, observable, reaction } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
import { StopEvent, returnOne, returnZero } from '../../../ClientUtils';
-import { Doc, Opt } from '../../../fields/Doc';
+import { Doc, DocListCast, Opt } from '../../../fields/Doc';
import { BoolCast, DocCast, NumCast, ScriptCast, StrCast } from '../../../fields/Types';
import { DocumentType } from '../../documents/DocumentTypes';
import { Docs } from '../../documents/Documents';
@@ -16,6 +15,8 @@ import { FieldViewProps } from '../nodes/FieldView';
import { FormattedTextBox } from '../nodes/formattedText/FormattedTextBox';
import './CollectionCarouselView.scss';
import { CollectionSubView, SubCollectionViewProps } from './CollectionSubView';
+import { TagItem } from '../TagsView';
+import { tickStep } from 'd3';
enum cardMode {
STAR = 'star',
@@ -34,7 +35,7 @@ export class CollectionCarouselView extends CollectionSubView() {
private _dropDisposer?: DragManager.DragDropDisposer;
get practiceField() { return this.fieldKey + "_practice"; } // prettier-ignore
get sideField() { return "_" + this.fieldKey + "_usePath"; } // prettier-ignore
- get starField() { return "star"; } // prettier-ignore
+ get starField() { return "#star"; } // prettier-ignore
_fadeTimer: NodeJS.Timeout | undefined;
@@ -63,7 +64,7 @@ export class CollectionCarouselView extends CollectionSubView() {
@computed get practiceMessage() {
const cardCount = this.carouselItems.length;
if (this.practiceMode) {
- if (!Doc.hasDocFilter(this.layoutDoc, 'star') && !cardCount) {
+ if (!Doc.hasDocFilter(this.layoutDoc, 'tags', Doc.FilterAny) && !cardCount) {
return 'Finished! Click here to view all flashcards.';
}
}
@@ -73,8 +74,8 @@ export class CollectionCarouselView extends CollectionSubView() {
@computed get filterMessage() {
const cardCount = this.carouselItems.length;
if (!this.practiceMessage) {
- if (Doc.hasDocFilter(this.layoutDoc, 'star') && !cardCount) {
- return 'No starred items. Click here to view all flash cards.';
+ if (Doc.hasDocFilter(this.layoutDoc, 'tags', Doc.FilterAny) && !cardCount) {
+ return 'No tagged items. Click here to view all flash cards.';
}
if (this.practiceMode) {
if (!cardCount) return 'No flashcards to show! Click here to leave practice mode';
@@ -120,7 +121,10 @@ export class CollectionCarouselView extends CollectionSubView() {
toggleStar = (e: React.MouseEvent) => {
e.stopPropagation();
const curDoc = this.carouselItems[this.carouselIndex];
- curDoc && (curDoc[this.starField] = curDoc[this.starField] ? undefined : true);
+ if (curDoc) {
+ if (TagItem.docHasTag(curDoc, this.starField)) TagItem.removeTagFromDoc(curDoc, this.starField);
+ else TagItem.addTagToDoc(curDoc, this.starField);
+ }
};
/*
@@ -165,6 +169,8 @@ export class CollectionCarouselView extends CollectionSubView() {
? false
: undefined;
+ childScreenToLocalXf = () => this._props.ScreenToLocalTransform().scale(this._props.NativeDimScaling?.() || 1);
+
renderDoc = (doc: Doc, showCaptions: boolean, overlayFunc?: (r: DocumentView | null) => void) => {
return (
<DocumentView
@@ -174,6 +180,8 @@ export class CollectionCarouselView extends CollectionSubView() {
NativeWidth={returnZero}
NativeHeight={returnZero}
fitWidth={undefined}
+ showTags={true}
+ hideFilterStatus={true}
containerViewPath={this.childContainerViewPath}
setContentViewBox={undefined}
onDoubleClickScript={this.onContentDoubleClick}
@@ -261,11 +269,11 @@ export class CollectionCarouselView extends CollectionSubView() {
return (
<>
<div>
- <Tooltip title="star">
- <div key="star" className="carouselView-star" onClick={this.toggleStar}>
- <FontAwesomeIcon icon="star" color={this.carouselItems[this.carouselIndex]?.[this.starField] ? 'yellow' : 'gray'} size="1x" />
+ {/* <Tooltip title="add new flashcard to pile">
+ <div key="add" className="carouselView-add" onClick={this.addFlashcard}>
+ <FontAwesomeIcon icon="plus" color={this.carouselItems?.[NumCast(this.layoutDoc._carousel_index)].layout[this.starField] ? 'yellow' : 'gray'} size="1x" />
</div>
- </Tooltip>
+ </Tooltip> */}
</div>
<div key="back" className="carouselView-back" onClick={this.goback}>
<FontAwesomeIcon icon="chevron-left" size="2x" />
@@ -292,18 +300,48 @@ export class CollectionCarouselView extends CollectionSubView() {
}
togglePracticeMode = (mode: practiceMode) => this.setPracticeMode(mode === this.practiceMode ? undefined : mode);
- toggleFilterMode = () => Doc.setDocFilter(this.Document, 'star', true, 'match', true);
- setColor = (mode: practiceMode | cardMode, which: string) => { return which === mode ? 'white' : 'light gray'}; //prettier-ignore
+ toggleFilterMode = () => Doc.setDocFilter(this.Document, 'tags', this.starField, 'check', true);
+ setColor = (mode: practiceMode | cardMode, which: string) => (which === mode ? 'white' : 'light gray');
+ @computed get filterDoc() {
+ return DocListCast(Doc.MyContextMenuBtns.data).find(doc => doc.title === 'Filter');
+ }
+ filterHeight = () => NumCast(this.filterDoc?.height);
+ filterWidth = () => (!this.filterDoc ? 1 : (this.filterHeight() * NumCast(this.filterDoc._width)) / NumCast(this.filterDoc._height));
@computed get menu() {
const curDoc = this.carouselItems?.[this.carouselIndex];
return (
<div className="carouselView-menu">
- <Tooltip title={Doc.hasDocFilter(this.Document, 'star') ? 'Show all cards' : 'Show only starred cards'}>
- <div key="back" className="carouselView-starFilter" onClick={e => this.toggleFilterMode()}>
- <FontAwesomeIcon icon="filter" color={Doc.hasDocFilter(this.Document, 'star') ? 'white' : 'lightgray'} size="1x" />
+ {!this.filterDoc ? null : (
+ <div style={{ height: this.filterHeight(), width: this.filterHeight() }}>
+ <DocumentView
+ {...this._props}
+ Document={this.filterDoc}
+ TemplateDataDocument={undefined}
+ LayoutTemplate={this._props.childLayoutTemplate}
+ LayoutTemplateString={this._props.childLayoutString}
+ renderDepth={this._props.renderDepth + 1}
+ NativeWidth={returnZero}
+ NativeHeight={returnZero}
+ fitWidth={undefined}
+ showTags={false}
+ hideFilterStatus={true}
+ containerViewPath={this.childContainerViewPath}
+ setContentViewBox={undefined}
+ onDoubleClickScript={this.onContentDoubleClick}
+ onClickScript={this.onContentClick}
+ isDocumentActive={this._props.childDocumentsActive?.() ? this._props.isDocumentActive : this._props.isContentActive}
+ isContentActive={this.isChildContentActive}
+ hideCaptions={true}
+ childFilters={this.childDocFilters}
+ hideDecorations={BoolCast(this.layoutDoc.layout_hideDecorations)}
+ addDocument={this._props.addDocument}
+ ScreenToLocalTransform={this.contentScreentToLocalXf}
+ PanelWidth={this.filterWidth}
+ PanelHeight={this.filterHeight}
+ />
</div>
- </Tooltip>
+ )}
<div className="carouselView-practiceModes" style={{ display: BoolCast(curDoc?._layout_isFlashcard) ? undefined : 'none' }}>
<Tooltip title="Practice flashcards using GPT">
<div key="back" className="carouselView-quiz" onClick={e => this.togglePracticeMode(practiceMode.QUIZ)}>
@@ -322,27 +360,33 @@ export class CollectionCarouselView extends CollectionSubView() {
render() {
return (
- <div
- className="collectionCarouselView-outer"
- ref={this.createDashEventsTarget}
- 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,
- }}>
- {!this.practiceMessage && !this.filterMessage ? (
- this.content
- ) : (
- <p
- className="message"
- onClick={e => {
- if (this.filterMessage || this.practiceMessage) {
- this.setPracticeMode(undefined);
- Doc.setDocFilter(this.layoutDoc, 'star', undefined, 'remove');
- }
- }}>
- {this.filterMessage || this.practiceMessage}
- </p>
- )}
+ <div>
+ <div
+ className="collectionCarouselView-outer"
+ ref={this.createDashEventsTarget}
+ 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,
+ width: `calc(100% - ${NumCast(this.layoutDoc._xMargin)}px)`,
+ height: `calc(100% - ${NumCast(this.layoutDoc._yMargin)}px)`,
+ left: NumCast(this.layoutDoc._xMargin),
+ top: NumCast(this.layoutDoc._yMargin),
+ }}>
+ {!this.practiceMessage && !this.filterMessage ? (
+ this.content
+ ) : (
+ <p
+ className="message"
+ onClick={e => {
+ if (this.filterMessage || this.practiceMessage) {
+ this.setPracticeMode(undefined);
+ Doc.setDocFilter(this.layoutDoc, 'tags', Doc.FilterAny, 'remove');
+ }
+ }}>
+ {this.filterMessage || this.practiceMessage}
+ </p>
+ )}
+ </div>
{!this.Document._chromeHidden ? this.menu : null}
{!this.Document._chromeHidden ? this.buttons : null}
</div>
diff --git a/src/client/views/collections/CollectionNoteTakingView.tsx b/src/client/views/collections/CollectionNoteTakingView.tsx
index e1f0a3e41..ac8e37358 100644
--- a/src/client/views/collections/CollectionNoteTakingView.tsx
+++ b/src/client/views/collections/CollectionNoteTakingView.tsx
@@ -653,7 +653,6 @@ export class CollectionNoteTakingView extends CollectionSubView() {
const sections = Array.from(this.Sections.entries());
return sections.reduce((list, sec, i) => {
list.push(this.sectionNoteTaking(sec[0], sec[1]));
- // eslint-disable-next-line react/no-array-index-key
i !== sections.length - 1 && list.push(<CollectionNoteTakingViewDivider key={`divider${i}`} isContentActive={this.isContentActive} index={i} setColumnStartXCoords={this.setColumnStartXCoords} xMargin={this.xMargin} />);
return list;
}, [] as JSX.Element[]);
diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx
index e97ee713e..1ac0b6d70 100644
--- a/src/client/views/collections/CollectionStackingView.tsx
+++ b/src/client/views/collections/CollectionStackingView.tsx
@@ -1,4 +1,3 @@
-/* eslint-disable react/jsx-props-no-spreading */
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import * as CSS from 'csstype';
import { action, computed, IReactionDisposer, makeObservable, observable, ObservableMap, reaction, runInAction } from 'mobx';
@@ -540,7 +539,6 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
if (this.pivotField) {
const types = docList.length ? docList.map(d => typeof d[key]) : this.filteredChildren.map(d => typeof d[key]);
if (types.map((i, idx) => types.indexOf(i) === idx).length === 1) {
- // eslint-disable-next-line prefer-destructuring
type = types[0];
}
}
@@ -577,7 +575,6 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
let type: 'string' | 'number' | 'bigint' | 'boolean' | 'symbol' | 'undefined' | 'object' | 'function' | undefined;
const types = docList.length ? docList.map(d => typeof d[key]) : this.filteredChildren.map(d => typeof d[key]);
if (types.map((i, idx) => types.indexOf(i) === idx).length === 1) {
- // eslint-disable-next-line prefer-destructuring
type = types[0];
}
const rows = () => (!this.isStackingView ? 1 : Math.max(1, Math.min(docList.length, Math.floor((this._props.PanelWidth() - 2 * this.xMargin) / (this.columnWidth + this.gridGap)))));
diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx
index 6aca8f2ca..5d32482c3 100644
--- a/src/client/views/collections/CollectionSubView.tsx
+++ b/src/client/views/collections/CollectionSubView.tsx
@@ -122,6 +122,9 @@ export function CollectionSubView<X>() {
);
return validPairs.map(({ data, layout }) => ({ data: data as Doc, layout: layout! })); // this mapping is a bit of a hack to coerce types
}
+ /**
+ * This is the raw, stored list of children on a collection. If you modify this list, the database will be updated
+ */
@computed get childDocList() {
return Cast(this.dataField, listSpec(Doc));
}
@@ -218,7 +221,6 @@ export function CollectionSubView<X>() {
if (!cursors) {
proto.cursors = cursors = new List<CursorField>();
}
- // eslint-disable-next-line no-cond-assign
if (cursors.length > 0 && (ind = cursors.findIndex(entry => entry.data.metadata.id === id)) > -1) {
cursors[ind].setPosition(pos);
} else {
@@ -251,7 +253,7 @@ export function CollectionSubView<X>() {
protected onInternalDrop(e: Event, de: DragManager.DropEvent): boolean {
const { docDragData } = de.complete;
- if (docDragData) {
+ if (docDragData && !docDragData.draggedDocuments.includes(this.Document)) {
let added;
const dropAction = docDragData.dropAction || docDragData.userDropAction;
const targetDocments = DocListCast(this.dataDoc[this._props.fieldKey]);
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index dbf781e63..f106eba26 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -1,4 +1,3 @@
-/* eslint-disable react/jsx-props-no-spreading */
import { Bezier } from 'bezier-js';
import { Colors } from 'browndash-components';
import { Property } from 'csstype';
@@ -1211,7 +1210,6 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
for (let j = 0; j < otherCtrlPts.length - 3; j += 4) {
const neighboringSegment = i === j || i === j - 4 || i === j + 4;
// Ensuring that the curve intersected by the eraser is not checked for further ink intersections.
- // eslint-disable-next-line no-continue
if (ink?.Document === otherInk.Document && neighboringSegment) continue;
const otherCurve = new Bezier(otherCtrlPts.slice(j, j + 4).map(p => ({ x: p.X, y: p.Y })));
@@ -1481,8 +1479,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
const childData = entry.pair.data;
return (
<CollectionFreeFormDocumentView
- // eslint-disable-next-line react/jsx-props-no-spreading, @typescript-eslint/no-explicit-any
- {...(OmitKeys(entry, ['replica', 'pair']).omit as any)}
+ {...(OmitKeys(entry, ['replica', 'pair']).omit as { x: number; y: number; z: number; width: number; height: number })}
key={childLayout[Id] + (entry.replica || '')}
Document={childLayout}
reactParent={this}
@@ -1786,7 +1783,6 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
if (!code.includes('dashDiv')) {
const script = CompileScript(code, { params: { docView: 'any' }, typecheck: false, editable: true });
if (script.compiled) script.run({ this: this.DocumentView?.() });
- // eslint-disable-next-line no-eval
} else code && !first && eval?.(code);
},
{ fireImmediately: true }
diff --git a/src/client/views/global/globalScripts.ts b/src/client/views/global/globalScripts.ts
index 934ed6c3e..47cc07ce1 100644
--- a/src/client/views/global/globalScripts.ts
+++ b/src/client/views/global/globalScripts.ts
@@ -2,19 +2,22 @@
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, StrListCast } 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';
-import { DocumentType } from '../../documents/DocumentTypes';
+import { CollectionViewType, DocumentType } from '../../documents/DocumentTypes';
+import { Docs } from '../../documents/Documents';
import { LinkManager } from '../../util/LinkManager';
import { ScriptingGlobals } from '../../util/ScriptingGlobals';
+import { SnappingManager } from '../../util/SnappingManager';
import { UndoManager, undoable } from '../../util/UndoManager';
import { GestureOverlay } from '../GestureOverlay';
import { InkingStroke } from '../InkingStroke';
+import { MainView } from '../MainView';
+import { PropertiesView } from '../PropertiesView';
import { CollectionFreeFormView } from '../collections/collectionFreeForm';
import { CollectionFreeFormDocumentView } from '../nodes/CollectionFreeFormDocumentView';
import {
@@ -36,8 +39,7 @@ import { ImageBox } from '../nodes/ImageBox';
import { VideoBox } from '../nodes/VideoBox';
import { WebBox } from '../nodes/WebBox';
import { RichTextMenu } from '../nodes/formattedText/RichTextMenu';
-
-// import { InkTranscription } from '../InkTranscription';
+import { GPTPopup, GPTPopupMode } from '../pdf/GPTPopup/GPTPopup';
// eslint-disable-next-line prefer-arrow-callback
ScriptingGlobals.add(function IsNoneSelected() {
@@ -135,20 +137,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(attribute: string) {
+ return StrListCast(selected._childFilters).some(filter => filter.includes(attribute));
+ }
+
// 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' | 'hcenter' | 'vcenter' | 'grid' | 'snaplines' | 'clusters' | '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),
@@ -168,120 +175,128 @@ ScriptingGlobals.add(function showFreeform(attr: 'hcenter' | 'vcenter' | 'grid'
}],
['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),
- setDoc: (doc: Doc, dv: DocumentView) => Doc.UserDoc().defaultToFlashcards = !Doc.UserDoc().defaultToFlashcards,
+ setDoc: (doc: Doc, dv: DocumentView) => { Doc.UserDoc().defaultToFlashcards = !Doc.UserDoc().defaultToFlashcards}, // prettier-ignore
}],
['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'}, // prettier-ignore
}],
['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'}, // prettier-ignore
}],
['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'}, // prettier-ignore
}],
- ['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'}, // prettier-ignore
}],
- ['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]: string } = {
+ 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 (or removes) a filter to the selected document for the specified tag
+ * NOTE: this also opens the filter panel if the settings button is clicked (probably should be a different function)
+ */
+// eslint-disable-next-line prefer-arrow-callback
+ScriptingGlobals.add(function setTagFilter(tag: string, added: boolean, 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));
-}, '');
+ const isOptions = tag === '-opts-';
-// 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')
-// });
+ if (checkResult) {
+ return isOptions
+ ? false
+ : StrListCast(selected._childFilters) // check all filters for one that filters tags:value where value is the tag's name
+ .map(filter => filter.split(Doc.FilterSep))
+ .some(([key, val]) => key === 'tags' && val === tag);
+ }
+
+ if (!isOptions) {
+ added ? Doc.setDocFilter(selected, 'tags', tag, 'check') : Doc.setDocFilter(selected, 'tags', tag, 'remove');
+ } else {
+ SnappingManager.PropertiesWidth < 5 && SnappingManager.SetPropertiesWidth(0);
+ SnappingManager.SetPropertiesWidth(MainView.Instance.propertiesWidth() < 15 ? 250 : 0);
+ PropertiesView.Instance?.CloseAll();
+ runInAction(() => (PropertiesView.Instance.openFilters = SnappingManager.PropertiesWidth > 5));
+ }
+
+ 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 +331,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?.();
@@ -476,6 +492,7 @@ function setActiveTool(tool: InkTool | Gestures, keepPrim: boolean, checkResult?
ScriptingGlobals.add(setActiveTool, 'sets the active ink tool mode');
+// eslint-disable-next-line prefer-arrow-callback
ScriptingGlobals.add(function activeEraserTool() {
return StrCast(Doc.UserDoc().activeEraserTool, InkTool.StrokeEraser);
}, 'returns the current eraser tool');
diff --git a/src/client/views/linking/LinkMenuItem.tsx b/src/client/views/linking/LinkMenuItem.tsx
index f54d8311d..b24fca8e2 100644
--- a/src/client/views/linking/LinkMenuItem.tsx
+++ b/src/client/views/linking/LinkMenuItem.tsx
@@ -90,7 +90,7 @@ export class LinkMenuItem extends ObservableReactComponent<LinkMenuItemProps> {
moveEv => {
const dragData = new DragManager.DocumentDragData([this._props.linkDoc], dropActionType.embed);
dragData.dropPropertiesToRemove = ['hidden'];
- DragManager.StartDocumentDrag([this._editRef.current!], dragData, moveEv.x, moveEv.y);
+ DragManager.StartDocumentDrag([this._editRef.current!], dragData, moveEv.x, moveEv.y, undefined, e => (this._props.linkDoc._layout_isSvg = true));
return true;
},
emptyFunction,
diff --git a/src/client/views/nodes/ComparisonBox.tsx b/src/client/views/nodes/ComparisonBox.tsx
index df127f0d5..8fbcb74cb 100644
--- a/src/client/views/nodes/ComparisonBox.tsx
+++ b/src/client/views/nodes/ComparisonBox.tsx
@@ -122,7 +122,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
<FontAwesomeIcon icon="lightbulb" size="xl" />
</div>
</Tooltip>
- {DocCast(this.Document.embedContainer).type_collection === 'carousel' ? null : (
+ {DocCast(this.Document.embedContainer)?.type_collection === CollectionViewType.Carousel ? null : (
<div>
<Tooltip title={<div>Create a flashcard pile</div>}>
<div style={{ position: 'absolute', bottom: '3px', right: '74px', cursor: 'pointer' }} onPointerDown={e => this.createFlashcardPile([this.Document], false)}>
@@ -697,6 +697,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
<DocumentView
// eslint-disable-next-line react/jsx-props-no-spreading
{...this._props}
+ showTags={undefined}
fitWidth={undefined}
ignoreUsePath={layoutString ? true : undefined}
renderDepth={this.props.renderDepth + 1}
@@ -742,13 +743,13 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
// add text box to each side when comparison box is first created
if (!this.dataDoc[this.fieldKey + '_0'] && !this._isEmpty) {
const dataSplit = StrCast(this.dataDoc.data).includes('Keyword: ') ? StrCast(this.dataDoc.data).split('Keyword: ') : StrCast(this.dataDoc.data).split('Answer: ');
- const newDoc = Docs.Create.TextDocument(dataSplit[1]);
+ const newDoc = Docs.Create.TextDocument(dataSplit[1], { _layout_autoHeight: true });
this.addDoc(newDoc, this.fieldKey + '_0');
}
if (!this.dataDoc[this.fieldKey + '_1'] && !this._isEmpty) {
const dataSplit = StrCast(this.dataDoc.data).includes('Keyword: ') ? StrCast(this.dataDoc.data).split('Keyword: ') : StrCast(this.dataDoc.data).split('Answer: ');
- const newDoc = Docs.Create.TextDocument(dataSplit[0]);
+ const newDoc = Docs.Create.TextDocument(dataSplit[0], { _layout_autoHeight: true });
this.addDoc(newDoc, this.fieldKey + '_1');
}
diff --git a/src/client/views/nodes/DocumentContentsView.tsx b/src/client/views/nodes/DocumentContentsView.tsx
index afc160297..9aa000ba7 100644
--- a/src/client/views/nodes/DocumentContentsView.tsx
+++ b/src/client/views/nodes/DocumentContentsView.tsx
@@ -129,6 +129,7 @@ export class DocumentContentsView extends ObservableReactComponent<DocumentConte
'childContentPointerEvents',
'LayoutTemplateString',
'LayoutTemplate',
+ 'showTags',
'layoutFieldKey',
'dontCenter',
'DataTransition',
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 351fdce79..b85cb22bb 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -67,6 +67,8 @@ export interface DocumentViewProps extends FieldViewSharedProps {
hideCaptions?: boolean;
contentPointerEvents?: Property.PointerEvents | undefined; // pointer events allowed for content of a document view. eg. set to "none" in menuSidebar for sharedDocs so that you can select a document, but not interact with its contents
dontCenter?: 'x' | 'y' | 'xy';
+ showTags?: boolean;
+ hideFilterStatus?: boolean;
childHideDecorationTitle?: boolean;
childHideResizeHandles?: boolean;
childDragAction?: dropActionType; // allows child documents to be dragged out of collection without holding the embedKey or dragging the doc decorations title bar.
@@ -1128,6 +1130,10 @@ export class DocumentView extends DocComponent<DocumentViewProps>() {
@observable public static CurrentlyPlaying: DocumentView[] = []; // audio or video media views that are currently playing
@observable public TagPanelHeight = 0;
+ @computed get showTags() {
+ return this.Document._layout_showTags || this._props.showTags;
+ }
+
@computed private get shouldNotScale() {
return (this.layout_fitWidth && !this.nativeWidth) || this.ComponentView?.isUnstyledView?.();
}
diff --git a/src/client/views/nodes/FieldView.tsx b/src/client/views/nodes/FieldView.tsx
index b0033ef39..7b652dded 100644
--- a/src/client/views/nodes/FieldView.tsx
+++ b/src/client/views/nodes/FieldView.tsx
@@ -1,5 +1,3 @@
-/* eslint-disable react/no-unused-prop-types */
-/* eslint-disable react/require-default-props */
import { Property } from 'csstype';
import { computed } from 'mobx';
import { observer } from 'mobx-react';
@@ -21,6 +19,7 @@ export type FocusFuncType = (doc: Doc, options: FocusViewOptions) => Opt<number>
// eslint-disable-next-line no-use-before-define
export type StyleProviderFuncType = (
doc: Opt<Doc>,
+ // eslint-disable-next-line no-use-before-define
props: Opt<FieldViewProps>,
property: string
) =>
@@ -65,6 +64,7 @@ export interface FieldViewSharedProps {
containerViewPath?: () => DocumentView[];
fitContentsToBox?: () => boolean; // used by freeformview to fit its contents to its panel. corresponds to _freeform_fitContentsToBox property on a Document
isGroupActive?: () => string | undefined; // is this document part of a group that is active
+ // eslint-disable-next-line no-use-before-define
setContentViewBox?: (view: ViewBoxInterface<FieldViewProps>) => void; // called by rendered field's viewBox so that DocumentView can make direct calls to the viewBox
PanelWidth: () => number;
PanelHeight: () => number;
diff --git a/src/client/views/nodes/FontIconBox/FontIconBox.tsx b/src/client/views/nodes/FontIconBox/FontIconBox.tsx
index 7a09ad9e2..cb0c4d188 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 });
+ if (this.Document.title === 'Font') getStyle = (val: string) => ({ fontFamily: val }); // bcz: major hack to style the font dropdown items --- needs to become part of the dropdown's metadata
}
// Get items to place into the list
@@ -266,26 +266,31 @@ export class FontIconBox extends ViewBoxBaseComponent<ButtonProps>() {
// Colors
const color = this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.Color) as string;
const items = DocListCast(this.dataDoc.data);
- const multiDoc = this.Document;
+ const selectedItems = items.filter(itemDoc => ScriptCast(itemDoc.onClick).script.run({ this: itemDoc, value: undefined, _readOnly_: true }).result).map(item => StrCast(item.toolType));
return (
<MultiToggle
tooltip={`Toggle ${tooltip}`}
type={Type.PRIM}
color={color}
- onPointerDown={e => script && !toggleStatus && setupMoveUpEvents(this, e, returnFalse, emptyFunction, () => script.run({ this: multiDoc, value: undefined, _readOnly_: false }))}
+ multiSelect={true}
+ onPointerDown={e => script && !toggleStatus && setupMoveUpEvents(this, e, returnFalse, emptyFunction, () => script.run({ this: this.Document, value: undefined, _readOnly_: false }))}
isToggle={script ? true : false}
toggleStatus={toggleStatus}
//background={SnappingManager.userBackgroundColor}
label={this.label}
- items={DocListCast(this.dataDoc.data).map(item => ({
+ items={items.map(item => ({
icon: <FontAwesomeIcon className={`fontIconBox-icon-${this.type}`} icon={StrCast(item.icon) as IconProp} color={color} />,
tooltip: StrCast(item.toolTip),
val: StrCast(item.toolType),
}))}
- selectedVal={StrCast(items.find(itemDoc => ScriptCast(itemDoc.onClick).script.run({ this: itemDoc, value: undefined, _readOnly_: true }).result)?.toolType ?? StrCast(multiDoc.toolType))}
- setSelectedVal={(val: string | number) => {
- const itemDoc = items.find(item => item.toolType === val);
- itemDoc && ScriptCast(itemDoc.onClick).script.run({ this: itemDoc, value: val, _readOnly_: false });
+ selectedItems={selectedItems}
+ onSelectionChange={(val: (string | number) | (string | number)[], added: boolean) => {
+ // note: the multitoggle is telling us whether the selection was toggled on or off, but we ignore this since we know the state of all the buttons
+ // and control it through the selectedItems prop. Therefore, the callback script will have to re-determine the toggle information.
+ // it would be better to pas the 'added' flag to the callback script, but our script generator from currentUserUtils makes it hard to define
+ // arbitrary parameter variables (but it could be done as a special case or with additional effort when creating the sript)
+ const itemsChanged = items.filter(item => (val instanceof Array ? val.includes(item.toolType as string | number) : item.toolType === val));
+ itemsChanged.forEach(itemDoc => ScriptCast(itemDoc.onClick).script.run({ this: itemDoc, _added_: added, itemDoc, _readOnly_: false }));
}}
/>
);
diff --git a/src/client/views/nodes/IconTagBox.scss b/src/client/views/nodes/IconTagBox.scss
new file mode 100644
index 000000000..90cc06092
--- /dev/null
+++ b/src/client/views/nodes/IconTagBox.scss
@@ -0,0 +1,26 @@
+@import '../global/globalCssVariables.module.scss';
+
+.card-button-container {
+ display: flex;
+ position: relative;
+ pointer-events: none;
+ background-color: rgb(218, 218, 218);
+ border-radius: 50px;
+ align-items: center;
+ gap: 5px;
+ padding-left: 5px;
+ padding-right: 5px;
+ padding-top: 2px;
+ padding-bottom: 2px;
+
+ button {
+ pointer-events: auto;
+ width: 20px;
+ height: 20px;
+ margin: auto;
+ padding: 0;
+ border-radius: 50%;
+ background-color: $dark-gray;
+ background-color: transparent;
+ }
+}
diff --git a/src/client/views/nodes/IconTagBox.tsx b/src/client/views/nodes/IconTagBox.tsx
new file mode 100644
index 000000000..ddabd61e1
--- /dev/null
+++ b/src/client/views/nodes/IconTagBox.tsx
@@ -0,0 +1,92 @@
+import { IconProp } from '@fortawesome/fontawesome-svg-core';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { Tooltip } from '@mui/material';
+import { computed, makeObservable } from 'mobx';
+import { observer } from 'mobx-react';
+import React from 'react';
+import { returnFalse, setupMoveUpEvents } from '../../../ClientUtils';
+import { emptyFunction } from '../../../Utils';
+import { Doc } from '../../../fields/Doc';
+import { StrCast } from '../../../fields/Types';
+import { undoable } from '../../util/UndoManager';
+import { ObservableReactComponent } from '../ObservableReactComponent';
+import { TagItem } from '../TagsView';
+import { DocumentView } from './DocumentView';
+import './IconTagBox.scss';
+
+export interface IconTagProps {
+ Views: DocumentView[];
+ IsEditing: boolean;
+}
+
+/**
+ * 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> {
+ constructor(props: IconTagProps) {
+ super(props);
+ makeObservable(this);
+ }
+
+ @computed get View() { return this._props.Views.lastElement(); } // prettier-ignore
+ @computed get currentScale() { return this.View?.screenToLocalScale(); } // prettier-ignore
+
+ /**
+ * Sets or removes the specified tag
+ * @param tag tag name (should begin with '#')
+ * @param state flag to add or remove the metadata
+ */
+ setIconTag = undoable((tag: string, state: boolean) => {
+ this._props.Views.forEach(view => {
+ state && TagItem.addTagToDoc(view.dataDoc, tag);
+ !state && TagItem.removeTagFromDoc(view.dataDoc, tag);
+ });
+ }, 'toggle card tag');
+
+ /**
+ * Returns a renderable version of the button Doc that is colorized to indicate
+ * whether the doc has the associated tag set on it or not.
+ * @param doc doc to test
+ * @param key metadata icon button
+ * @returns an icon for the metdata button
+ */
+ getButtonIcon = (doc: Doc, key: Doc): JSX.Element => {
+ const icon = StrCast(key.icon) as IconProp;
+ const tag = StrCast(key.toolType);
+ const isActive = TagItem.docHasTag(doc, tag);
+ const color = isActive ? '#4476f7' : '#323232'; // TODO should use theme colors
+
+ return <FontAwesomeIcon icon={icon} style={{ color, height: '20px', width: '20px' }} />;
+ };
+
+ /**
+ * Renders the buttons to customize sorting depending on which group the card belongs to and the amount of total groups
+ */
+ render() {
+ const buttons = Doc.MyFilterHotKeys
+ .map(key => ({ key, tag: StrCast(key.toolType) }))
+ .filter(({ tag }) => this._props.IsEditing || TagItem.docHasTag(this.View.Document, tag) || (DocumentView.Selected().length === 1 && this.View.IsSelected))
+ .map(({ key, tag }) => (
+ <Tooltip key={tag} title={<div className="dash-tooltip">Click to add/remove this card from the {tag} group</div>}>
+ <button
+ type="button"
+ onPointerDown={e =>
+ setupMoveUpEvents(this, e, returnFalse, emptyFunction, clickEv => {
+ this.setIconTag(tag, !TagItem.docHasTag(this.View.Document, tag));
+ clickEv.stopPropagation();
+ })
+ }>
+ {this.getButtonIcon(this.View.Document, key)}
+ </button>
+ </Tooltip>
+ )); // prettier-ignore
+
+ return !buttons.length ? null : (
+ <div className="card-button-container" style={{ fontSize: '50px' }}>
+ {buttons}
+ </div>
+ );
+ }
+}
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index 45301247b..91e51d24e 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -693,7 +693,8 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
TraceMobx();
const backColor = DashColor((this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.BackgroundColor) as string) ?? Colors.WHITE);
- const backAlpha = backColor.red() === 0 && backColor.green() === 0 && backColor.blue() === 0 ? backColor.alpha() : 1;
+ // allow use case where the image is transparent when the alpha value is to smallest possible value from UI (alpha = 1 out of 255)
+ const backAlpha = backColor.alpha() < 0.015 && backColor.alpha() > 0 ? backColor.alpha() : 1;
const srcpath = this.layoutDoc.hideImage ? '' : this.paths[0];
const fadepath = this.layoutDoc.hideImage ? '' : this.paths.lastElement();
const { nativeWidth, nativeHeight /* , nativeOrientation */ } = this.nativeSize;
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index a281479f1..07a4d1093 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -213,9 +213,22 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
return anchor;
};
+ gptPDFFlashcards = async () => {
+ const queryText = window.getSelection()?.toString() ?? '';
+ try {
+ if (queryText) {
+ const res = await gptAPICall(queryText, GPTCallType.FLASHCARD);
+ AnchorMenu.Instance.transferToFlashcard(res || 'Something went wrong', NumCast(this.layoutDoc.x), NumCast(this.layoutDoc.y));
+ }
+ } catch (err) {
+ console.error(err);
+ }
+ };
+
@action
setupAnchorMenu = () => {
AnchorMenu.Instance.Status = 'marquee';
+ AnchorMenu.Instance.gptFlashcards = this.gptPDFFlashcards;
AnchorMenu.Instance.OnClick = () => {
!this.layoutDoc.layout_showSidebar && this.toggleSidebar();
setTimeout(() => this._sidebarRef.current?.anchorMenuClick(this.makeLinkAnchor(undefined, OpenWhere.addRight, undefined, 'Anchored Selection', true))); // give time for sidebarRef to be created
@@ -277,6 +290,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
ele.append(contents);
}
this._selectionHTML = ele?.innerHTML;
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
} catch (e) {
/* empty */
}
@@ -564,7 +578,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
const draggedDoc = dragData.droppedDocuments.lastElement();
let added: Opt<boolean>;
const dropAction = dragData.dropAction || dragData.userDropAction;
- if ([AclEdit, AclAdmin, AclSelfEdit].includes(effectiveAcl)) {
+ if ([AclEdit, AclAdmin, AclSelfEdit].includes(effectiveAcl) && !dragData.draggedDocuments.includes(this.Document)) {
// replace text contents when dragging with Alt
if (de.altKey) {
const fieldKey = Doc.LayoutFieldKey(draggedDoc);
diff --git a/src/client/views/nodes/formattedText/RichTextRules.ts b/src/client/views/nodes/formattedText/RichTextRules.ts
index 0ef67b4be..f58434906 100644
--- a/src/client/views/nodes/formattedText/RichTextRules.ts
+++ b/src/client/views/nodes/formattedText/RichTextRules.ts
@@ -406,7 +406,7 @@ export class RichTextRules {
this.Document[DocData].tags = new List<string>(tags);
this.Document._layout_showTags = true;
}
- const fieldView = state.schema.nodes.dashField.create({ fieldKey: '#' + tag });
+ const fieldView = state.schema.nodes.dashField.create({ fieldKey: tag.startsWith('@') ? tag.replace(/^@/, '') : '#' + tag });
return state.tr
.setSelection(new TextSelection(state.doc.resolve(start), state.doc.resolve(end)))
.replaceSelectionWith(fieldView, true)
diff --git a/src/client/views/pdf/GPTPopup/GPTPopup.scss b/src/client/views/pdf/GPTPopup/GPTPopup.scss
index 6d8793f82..0247dc10c 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);
+ overflow-y: auto;
+ padding-bottom: 60px;
+ }
+
+ .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,14 @@ $highlightedText: #82e0ff;
}
- }
- button {
- font-size: 9px;
- padding: 10px;
- color: #ffffff;
- 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..d5f5f620c 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, CgCornerUpLeft } from 'react-icons/cg';
import ReactLoading from 'react-loading';
import { TypeAnimation } from 'react-type-animation';
import { ClientUtils } from '../../../../ClientUtils';
@@ -11,13 +11,14 @@ import { Doc } from '../../../../fields/Doc';
import { NumCast, StrCast } from '../../../../fields/Types';
import { Networking } from '../../../Network';
import { GPTCallType, gptAPICall, gptImageCall } from '../../../apis/gpt/GPT';
-import { Docs } from '../../../documents/Documents';
import { DocUtils } from '../../../documents/DocUtils';
+import { Docs } from '../../../documents/Documents';
+import { SettingsManager } from '../../../util/SettingsManager';
+import { SnappingManager } from '../../../util/SnappingManager';
import { ObservableReactComponent } from '../../ObservableReactComponent';
+import { DocumentView } from '../../nodes/DocumentView';
import { AnchorMenu } from '../AnchorMenu';
import './GPTPopup.scss';
-import { SettingsManager } from '../../../util/SettingsManager';
-import { SnappingManager } from '../../../util/SnappingManager';
export enum GPTPopupMode {
SUMMARY,
@@ -25,7 +26,15 @@ export enum GPTPopupMode {
IMAGE,
FLASHCARD,
DATA,
+ CARD,
SORT,
+ QUIZ,
+}
+
+export enum GPTQuizType {
+ CURRENT = 0,
+ CHOOSE = 1,
+ MULTIPLE = 2,
}
interface GPTPopupProps {}
@@ -34,6 +43,8 @@ interface GPTPopupProps {}
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[] = [];
@@ -63,7 +74,7 @@ export class GPTPopup extends ObservableReactComponent<GPTPopupProps> {
};
@observable
public dataJson: string = '';
- public dataChatPrompt: string | null = null;
+ public dataChatPrompt: string | undefined = undefined;
@action
public setDataJson = (text: string) => {
if (text === '') this.dataChatPrompt = '';
@@ -140,7 +151,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,22 +160,157 @@ 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) {
@@ -305,67 +452,143 @@ 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.mode === GPTPopupMode.CARD ? (
+ this.cardMenu()
) : (
- <>
- {!this.cardsDoneLoading ? (
- <div className="content-wrapper">
- <div className="loading-spinner">
- <ReactLoading type="spin" color={StrCast(Doc.UserDoc().userVariantColor)} height={30} width={30} />
- <span>Reading Cards...</span>
- </div>
- </div>
- ) : (
- !this.sortDone && (
- <div className="btns-wrapper-gpt">
- <Button
- tooltip="Have ChatGPT sort your cards for you!"
- text="Sort!"
- onClick={this.generateSort}
- color={StrCast(Doc.UserDoc().userVariantColor)}
- type={Type.TERT}
- style={{
- width: '90%', // Almost as wide as the container
- textAlign: 'center',
- color: '#ffffff', // White text
- fontSize: '16px', // Adjust font size as needed
- }}
- />
- </div>
- )
- )}
-
- {this.sortDone && (
- <div>
- <div className="content-wrapper">
- <p>{this.text === 'Something went wrong :(' ? 'Something went wrong :(' : 'Sorting done! Feel free to move things around / regenerate :) !'}</p>
- <IconButton tooltip="Generate Again" onClick={() => this.setSortDone(false)} icon={<FontAwesomeIcon icon="redo-alt" size="lg" />} color={StrCast(Doc.UserDoc().userVariantColor)} />
- </div>
- </div>
- )}
- </>
- )}
- </div>
- </>
+ this.cardActual(this.mode)
+ ) // Call the functions to render JSX
+ }
+ </>
+ </div>
);
+
imageBox = () => (
<div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}>
{this.heading('GENERATED IMAGE')}
@@ -511,14 +734,52 @@ 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}
+ {content}
+ <div className="resize-handle" />
</div>
);
}
diff --git a/src/client/views/search/FaceRecognitionHandler.tsx b/src/client/views/search/FaceRecognitionHandler.tsx
index c507e54b6..6f70e96ab 100644
--- a/src/client/views/search/FaceRecognitionHandler.tsx
+++ b/src/client/views/search/FaceRecognitionHandler.tsx
@@ -30,6 +30,7 @@ import { DocumentManager } from '../../util/DocumentManager';
* face_annos - a list of face annotations, where each anno has
*/
export class FaceRecognitionHandler {
+ // eslint-disable-next-line no-use-before-define
static _instance: FaceRecognitionHandler;
private _apiModelReady = false;
private _pendingAPIModelReadyDocs: Doc[] = [];
@@ -221,7 +222,7 @@ export class FaceRecognitionHandler {
const annos = [] as Doc[];
const scale = NumCast(imgDoc.data_nativeWidth) / img.width;
const showTags= imgDocFaceDescriptions.length > 1;
- imgDocFaceDescriptions.forEach((fd, i) => {
+ imgDocFaceDescriptions.forEach(fd => {
const faceDescriptor = new List<number>(Array.from(fd.descriptor));
const matchedUniqueFace = this.findMatchingFaceDoc(fd.descriptor) ?? this.createUniqueFaceDoc(activeDashboard);
const faceAnno = Docs.Create.FreeformDocument([], {