diff options
55 files changed, 1970 insertions, 187 deletions
diff --git a/package-lock.json b/package-lock.json index 4da0600b0..b1cb63540 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4497,9 +4497,9 @@ } }, "browndash-components": { - "version": "0.0.28", - "resolved": "https://registry.npmjs.org/browndash-components/-/browndash-components-0.0.28.tgz", - "integrity": "sha512-pg8qAmaILk6GpUfrdDub6Bg5+LD7547UDzw44yntKsNQC8FDNE9KkDa1ShH5yAn+cwKuTMa/a9Q9yZxMtm94bQ==", + "version": "0.0.35", + "resolved": "https://registry.npmjs.org/browndash-components/-/browndash-components-0.0.35.tgz", + "integrity": "sha512-xHScZmY6y7s3RH7N4rQ4oEu0iAeeP4FbR2Nv289JREThcqiHQqII7vV4rF0tiL9jJepwGLaSIO3k45kUhm62ZQ==", "requires": { "color": "^4.2.3", "react-color": "^2.19.3", @@ -5955,16 +5955,6 @@ "integrity": "sha1-WW6WmP0MgOEgOMK4LW6xs1tiJNk=", "dev": true }, - "d": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/d/-/d-1.0.1.tgz", - "integrity": "sha512-m62ShEObQ39CfralilEQRjH6oAMtNCV1xJyEx5LpRYUVN+EviphDgUc/F3hnYbADmkiNs67Y+3ylmlG7Lnu+FA==", - "dev": true, - "requires": { - "es5-ext": "^0.10.50", - "type": "^1.0.1" - } - }, "d3": { "version": "7.8.4", "resolved": "https://registry.npmjs.org/d3/-/d3-7.8.4.tgz", @@ -7345,28 +7335,6 @@ "is-symbol": "^1.0.2" } }, - "es5-ext": { - "version": "0.10.62", - "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.62.tgz", - "integrity": "sha512-BHLqn0klhEpnOKSrzn/Xsz2UIW8j+cGmo9JLzr8BiUapV8hPL9+FliFqjwr9ngW7jWdnxv6eO+/LqyhJVqgrjA==", - "dev": true, - "requires": { - "es6-iterator": "^2.0.3", - "es6-symbol": "^3.1.3", - "next-tick": "^1.1.0" - } - }, - "es6-iterator": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz", - "integrity": "sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g==", - "dev": true, - "requires": { - "d": "1", - "es5-ext": "^0.10.35", - "es6-symbol": "^3.1.1" - } - }, "es6-promise": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-3.2.1.tgz", @@ -7378,7 +7346,6 @@ "integrity": "sha512-NJ6Yn3FuDinBaBRWl/q5X/s4koRHBrgKAu+yGI6JCBeiu3qrcbJhwT2GeR/EXVfylRk8dpQVJoLEFhK+Mu31NA==", "dev": true, "requires": { - "d": "^1.0.1", "ext": "^1.1.2" } }, @@ -23051,12 +23018,6 @@ "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=" }, - "type": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/type/-/type-1.2.0.tgz", - "integrity": "sha512-+5nt5AAniqsCnu2cEQQdpzCAh33kVx8n0VoFidKpB1dVVLAN/F+bgVOqOJqOnEnrhp222clB5p3vUlD+1QAnfg==", - "dev": true - }, "type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", diff --git a/package.json b/package.json index 6cd271c96..da3097bf4 100644 --- a/package.json +++ b/package.json @@ -177,7 +177,7 @@ "body-parser": "^1.19.2", "bootstrap": "^4.6.1", "brotli": "^1.3.3", - "browndash-components": "^0.0.28", + "browndash-components": "0.0.35", "browser-assert": "^1.2.1", "bson": "^4.6.1", "canvas": "^2.9.3", diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts index 11a8dcaf6..4c5fea260 100644 --- a/src/client/util/CurrentUserUtils.ts +++ b/src/client/util/CurrentUserUtils.ts @@ -693,13 +693,13 @@ export class CurrentUserUtils { CollectionViewType.Carousel3D, CollectionViewType.Linear, CollectionViewType.Map, CollectionViewType.Grid, CollectionViewType.NoteTaking]), title: "Perspective", toolTip: "View", btnType: ButtonType.DropdownList, ignoreClick: true, width: 100, scripts: { script: 'setView(value, _readOnly_)'}}, - { title: "Pin", icon: "map-pin", toolTip: "Pin View to Trail", btnType: ButtonType.ClickButton, expertMode: false, width: 20, scripts: { onClick: 'pinWithView(altKey)'}}, - { title: "Fill", icon: "fill-drip", toolTip: "Background Fill Color",btnType: ButtonType.ColorButton, expertMode: false, ignoreClick: true, width: 20, scripts: { script: 'return setBackgroundColor(value, _readOnly_)'}}, // Only when a document is selected + { title: "Pin", icon: "map-pin", toolTip: "Pin View to Trail", btnType: ButtonType.ClickButton, expertMode: false, width: 30, scripts: { onClick: 'pinWithView(altKey)'}}, + { title: "Fill", icon: "fill-drip", toolTip: "Background Fill Color",btnType: ButtonType.ColorButton, expertMode: false, ignoreClick: true, width: 30, scripts: { script: 'return setBackgroundColor(value, _readOnly_)'}}, // Only when a document is selected { title: "Header", icon: "heading", toolTip: "Header Color", btnType: ButtonType.ColorButton, expertMode: false, ignoreClick: true, scripts: { script: 'return setHeaderColor(value, _readOnly_)'}}, { title: "Overlay", icon: "layer-group", toolTip: "Overlay", btnType: ButtonType.ToggleButton, expertMode: false, toolType:CollectionViewType.Freeform, funcs: {hidden: '!SelectionManager_selectedDocType(self.toolType, self.expertMode, true)'}, scripts: { onClick: 'toggleOverlay(_readOnly_)'}}, // Only when floating document is selected in freeform - { title: "Back", icon: "chevron-left", toolTip: "Prev Animation Frame", btnType: ButtonType.ClickButton, expertMode: true, toolType:CollectionViewType.Freeform, funcs: {hidden: '!SelectionManager_selectedDocType(self.toolType, self.expertMode)'}, width: 20, scripts: { onClick: 'prevKeyFrame(_readOnly_)'}}, + { title: "Back", icon: "chevron-left", toolTip: "Prev Animation Frame", btnType: ButtonType.ClickButton, expertMode: true, toolType:CollectionViewType.Freeform, funcs: {hidden: '!SelectionManager_selectedDocType(self.toolType, self.expertMode)'}, width: 30, scripts: { onClick: 'prevKeyFrame(_readOnly_)'}}, { title: "Num", icon:"",toolTip: "Frame Number (click to toggle edit mode)",btnType: ButtonType.TextButton, expertMode: true, toolType:CollectionViewType.Freeform, funcs: {hidden: '!SelectionManager_selectedDocType(self.toolType, self.expertMode)', buttonText: 'selectedDocs()?.lastElement()?.currentFrame?.toString()'}, width: 20, scripts: { onClick: '{ return curKeyFrame(_readOnly_);}'}}, - { title: "Fwd", icon: "chevron-right", toolTip: "Next Animation Frame", btnType: ButtonType.ClickButton, expertMode: true, toolType:CollectionViewType.Freeform, funcs: {hidden: '!SelectionManager_selectedDocType(self.toolType, self.expertMode)'}, width: 20, scripts: { onClick: 'nextKeyFrame(_readOnly_)'}}, + { title: "Fwd", icon: "chevron-right", toolTip: "Next Animation Frame", btnType: ButtonType.ClickButton, expertMode: true, toolType:CollectionViewType.Freeform, funcs: {hidden: '!SelectionManager_selectedDocType(self.toolType, self.expertMode)'}, width: 30, scripts: { onClick: 'nextKeyFrame(_readOnly_)'}}, { title: "Text", icon: "Text", toolTip: "Text functions", subMenu: CurrentUserUtils.textTools(), expertMode: false, toolType:DocumentType.RTF, funcs: { linearView_IsExpanded: `SelectionManager_selectedDocType(self.toolType, self.expertMode)`} }, // Always available { title: "Ink", icon: "Ink", toolTip: "Ink functions", subMenu: CurrentUserUtils.inkTools(), expertMode: false, toolType:DocumentType.INK, funcs: { linearView_IsExpanded: `SelectionManager_selectedDocType(self.toolType, self.expertMode)`}, scripts: { onClick: 'setInkToolDefaults()'} }, // Always available { title: "Doc", icon: "Doc", toolTip: "Freeform Doc tools", subMenu: CurrentUserUtils.freeTools(), expertMode: false, toolType:CollectionViewType.Freeform, funcs: {hidden: `!SelectionManager_selectedDocType(self.toolType, self.expertMode, true)`, linearView_IsExpanded: `SelectionManager_selectedDocType(self.toolType, self.expertMode)`} }, // Always available diff --git a/src/client/views/DashboardView.tsx b/src/client/views/DashboardView.tsx index 6aae302ac..d91c17545 100644 --- a/src/client/views/DashboardView.tsx +++ b/src/client/views/DashboardView.tsx @@ -1,5 +1,5 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { Button, ColorPicker, FontSize, IconButton, Size } from 'browndash-components'; +import { Button, ColorPicker, FontSize, IconButton, Size, Type } from 'browndash-components'; import { action, computed, observable } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; @@ -155,7 +155,7 @@ export class DashboardView extends React.Component { <div className="dashboard-view"> <div className="left-menu"> <div className="new-dashboard-button"> - <Button icon={<FaPlus />} size={Size.MEDIUM} text="New" onClick={() => this.setNewDashboardName('')} /> + <Button icon={<FaPlus />} size={Size.LARGE} text="New" type={Type.SEC} onClick={() => this.setNewDashboardName('')} /> </div> <div className={`text-button ${this.selectedDashboardGroup === DashboardGroup.MyDashboards && 'selected'}`} onClick={() => this.selectDashboardGroup(DashboardGroup.MyDashboards)}> My Dashboards diff --git a/src/client/views/Main.scss b/src/client/views/Main.scss index c7a7614ac..a403a10e3 100644 --- a/src/client/views/Main.scss +++ b/src/client/views/Main.scss @@ -11,6 +11,7 @@ body { height: 100%; overflow: hidden; font-family: $sans-serif; + font-size: $body-text; margin: 0; position: absolute; top: 0; diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx index ab2e0f7c5..ec8f5989f 100644 --- a/src/client/views/MainView.tsx +++ b/src/client/views/MainView.tsx @@ -49,6 +49,7 @@ import { InkTranscription } from './InkTranscription'; import { LightboxView } from './LightboxView'; import { LinkMenu } from './linking/LinkMenu'; import './MainView.scss'; +import { NewLightboxView } from './newlightbox/NewLightboxView'; import { AudioBox } from './nodes/AudioBox'; import { DocumentLinksButton } from './nodes/DocumentLinksButton'; import { DocumentView, DocumentViewInternal, OpenWhere, OpenWhereMod } from './nodes/DocumentView'; @@ -1018,6 +1019,7 @@ export class MainView extends React.Component { <InkTranscription /> {this.snapLines} <LightboxView key="lightbox" PanelWidth={this._windowWidth} PanelHeight={this._windowHeight} maxBorder={[200, 50]} /> + <NewLightboxView key="newLightbox" PanelWidth={this._windowWidth} PanelHeight={this._windowHeight} maxBorder={[200, 50]} /> </div> ); } diff --git a/src/client/views/StyleProvider.tsx b/src/client/views/StyleProvider.tsx index 330ccc583..0af9bfed2 100644 --- a/src/client/views/StyleProvider.tsx +++ b/src/client/views/StyleProvider.tsx @@ -116,6 +116,7 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<DocumentViewProps allSorts[TreeSort.None] = { color: 'darkgray', label: '\u00A0\u00A0\u00A0' }; return allSorts; case StyleProp.Highlighting: + if (doc && (Doc.IsSystem(doc) || doc.type === DocumentType.FONTICON)) return undefined; if (doc && !doc.layout_disableBrushing && !props?.disableBrushing) { const selected = SelectionManager.Views().some(dv => dv.rootDoc === doc); const highlightIndex = Doc.isBrushedHighlightedDegree(doc) || (selected ? Doc.DocBrushStatus.selfBrushed : 0); @@ -221,8 +222,8 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<DocumentViewProps if (StrCast(Doc.LayoutField(doc)).includes(SliderBox.name)) break; docColor = docColor || (Doc.IsSystem(doc) ? darkScheme() - ? Colors.DARK_GRAY - : Colors.LIGHT_GRAY // system docs (seen in treeView) get a grayish background + ? undefined + : undefined // system docs (seen in treeView) get a grayish background : doc.annotationOn ? '#00000010' // faint interior for collections on PDFs, images, etc : doc?._isGroup diff --git a/src/client/views/collections/CollectionTreeView.scss b/src/client/views/collections/CollectionTreeView.scss index 273b08247..2bf649caf 100644 --- a/src/client/views/collections/CollectionTreeView.scss +++ b/src/client/views/collections/CollectionTreeView.scss @@ -13,7 +13,7 @@ width: 100%; position: relative; top: 0; - background: $light-gray; + // background: $light-gray; font-size: 13px; overflow: auto; user-select: none; diff --git a/src/client/views/collections/collectionLinear/CollectionLinearView.scss b/src/client/views/collections/collectionLinear/CollectionLinearView.scss index 3e3709827..6b3318bf3 100644 --- a/src/client/views/collections/collectionLinear/CollectionLinearView.scss +++ b/src/client/views/collections/collectionLinear/CollectionLinearView.scss @@ -17,12 +17,7 @@ .collectionLinearView-menuOpener { user-select: none; } - - &.true { - border-left: $standard-border; - background-color: $medium-blue-alt; - } - + > input:not(:checked) ~ &.true { background-color: transparent; } diff --git a/src/client/views/global/globalCssVariables.scss b/src/client/views/global/globalCssVariables.scss index 422dae15b..7b2ac5713 100644 --- a/src/client/views/global/globalCssVariables.scss +++ b/src/client/views/global/globalCssVariables.scss @@ -36,8 +36,8 @@ $icon-size: 28px; // fonts $sans-serif: 'Roboto', sans-serif; $large-header: 16px; -$body-text: 12px; -$small-text: 9px; +$body-text: 13px; +$small-text: 10px; // $sans-serif: "Roboto Slab", sans-serif; // misc values diff --git a/src/client/views/newlightbox/ButtonMenu/ButtonMenu.scss b/src/client/views/newlightbox/ButtonMenu/ButtonMenu.scss new file mode 100644 index 000000000..74fbfbb2c --- /dev/null +++ b/src/client/views/newlightbox/ButtonMenu/ButtonMenu.scss @@ -0,0 +1,15 @@ +@import '../NewLightboxStyles.scss'; + +.newLightboxButtonMeny-container { + width: 100vw; + height: 100vh; + + &.dark { + background: $black; + } + + &.light, + &.default { + background: $white; + } +}
\ No newline at end of file diff --git a/src/client/views/newlightbox/ButtonMenu/ButtonMenu.tsx b/src/client/views/newlightbox/ButtonMenu/ButtonMenu.tsx new file mode 100644 index 000000000..0ede75407 --- /dev/null +++ b/src/client/views/newlightbox/ButtonMenu/ButtonMenu.tsx @@ -0,0 +1,53 @@ +import './ButtonMenu.scss'; +import * as React from 'react'; +import { IButtonMenu } from "./utils"; +import { NewLightboxView } from '../NewLightboxView'; +import { SelectionManager } from '../../../util/SelectionManager'; +import { CollectionDockingView } from '../../collections/CollectionDockingView'; +import { OpenWhereMod } from '../../nodes/DocumentView'; +import { Doc } from '../../../../fields/Doc'; +import { InkTool } from '../../../../fields/InkField'; +import { MainView } from '../../MainView'; +import { action } from 'mobx'; + +export const ButtonMenu = (props: IButtonMenu) => { + + return <div className={`newLightboxButtonMenu-container`}> + <div + className="newLightboxView-navBtn" + title="toggle fit width" + onClick={e => { + e.stopPropagation(); + NewLightboxView.NewLightboxDoc!._fitWidth = !NewLightboxView.NewLightboxDoc!._fitWidth; + }}> + </div> + <div + className="newLightboxView-tabBtn" + title="open in tab" + onClick={e => { + e.stopPropagation(); + CollectionDockingView.AddSplit(NewLightboxView.NewLightboxDoc || NewLightboxView.NewLightboxDoc!, OpenWhereMod.none); + SelectionManager.DeselectAll(); + NewLightboxView.SetNewLightboxDoc(undefined); + }}> + </div> + <div + className="newLightboxView-penBtn" + title="toggle pen annotation" + style={{ background: Doc.ActiveTool === InkTool.Pen ? 'white' : undefined }} + onClick={e => { + e.stopPropagation(); + Doc.ActiveTool = Doc.ActiveTool === InkTool.Pen ? InkTool.None : InkTool.Pen; + }}> + </div> + <div + className="newLightboxView-exploreBtn" + title="toggle explore mode to navigate among documents only" + style={{ background: MainView.Instance._exploreMode ? 'white' : undefined }} + onClick={action(e => { + e.stopPropagation(); + MainView.Instance._exploreMode = !MainView.Instance._exploreMode; + })}> + </div> + </div> +}
\ No newline at end of file diff --git a/src/client/views/newlightbox/ButtonMenu/index.ts b/src/client/views/newlightbox/ButtonMenu/index.ts new file mode 100644 index 000000000..f53a8c729 --- /dev/null +++ b/src/client/views/newlightbox/ButtonMenu/index.ts @@ -0,0 +1 @@ +export * from './ButtonMenu'
\ No newline at end of file diff --git a/src/client/views/newlightbox/ButtonMenu/utils.ts b/src/client/views/newlightbox/ButtonMenu/utils.ts new file mode 100644 index 000000000..096ea87ad --- /dev/null +++ b/src/client/views/newlightbox/ButtonMenu/utils.ts @@ -0,0 +1,3 @@ +export interface IButtonMenu { + +}
\ No newline at end of file diff --git a/src/client/views/newlightbox/ExploreView/ExploreView.scss b/src/client/views/newlightbox/ExploreView/ExploreView.scss new file mode 100644 index 000000000..5a8ab2f87 --- /dev/null +++ b/src/client/views/newlightbox/ExploreView/ExploreView.scss @@ -0,0 +1,44 @@ +@import '../NewLightboxStyles.scss'; + +.exploreView-container { + width: 100%; + height: 100%; + border-radius: 20px; + position: relative; + // transform: scale(1); + background: $gray-l1; + border-top: $standard-border; + border-color: $gray-l2; + border-radius: 0px 0px 20px 20px; + transform-origin: 50% 50%; + overflow: hidden; + + &.dark { + background: $black; + } + + &.light, + &.default { + background: $gray-l1; + } + + .exploreView-doc { + width: 60px; + height: 80px; + position: absolute; + background: $blue-l2; + // opacity: 0.8; + transform-origin: 50% 50%; + transform: translate(-50%, -50%) scale(1); + cursor: pointer; + transition: 0.2s ease; + overflow: hidden; + font-size: 9px; + padding: 10px; + border-radius: 5px; + + &:hover { + transform: translate(calc(-50% * 1.125), calc(-50% * 1.125)) scale(1.5); + } + } +}
\ No newline at end of file diff --git a/src/client/views/newlightbox/ExploreView/ExploreView.tsx b/src/client/views/newlightbox/ExploreView/ExploreView.tsx new file mode 100644 index 000000000..855bfd9e2 --- /dev/null +++ b/src/client/views/newlightbox/ExploreView/ExploreView.tsx @@ -0,0 +1,30 @@ +import './ExploreView.scss'; +import { IBounds, IExploreView, emptyBounds } from "./utils"; +import { IRecommendation } from "../components"; +import * as React from 'react'; +import { NewLightboxView } from '../NewLightboxView'; +import { StrCast } from '../../../../fields/Types'; + + + +export const ExploreView = (props: IExploreView) => { + const { recs, bounds=emptyBounds } = props + + return <div className={`exploreView-container`}> + {recs && recs.map((rec) => { + console.log(rec.embedding, bounds) + const x_bound: number = Math.max(Math.abs(bounds.max_x), Math.abs(bounds.min_x)) + const y_bound: number = Math.max(Math.abs(bounds.max_y), Math.abs(bounds.min_y)) + console.log(x_bound, y_bound) + if (rec.embedding) { + const x = (rec.embedding.x / x_bound) * 50; + const y = (rec.embedding.y / y_bound) * 50; + console.log(x, y) + return <div className={`exploreView-doc`} onClick={() => {}} style={{top: `calc(50% + ${y}%)`, left: `calc(50% + ${x}%)`}}> + {rec.title} + </div> + } else return (null) + })} + <div className={`exploreView-doc`} style={{top: `calc(50% + ${0}%)`, left: `calc(50% + ${0}%)`, background: '#073763', color: 'white'}}>{StrCast(NewLightboxView.NewLightboxDoc?.title)}</div> + </div> +}
\ No newline at end of file diff --git a/src/client/views/newlightbox/ExploreView/index.ts b/src/client/views/newlightbox/ExploreView/index.ts new file mode 100644 index 000000000..bf94eedcd --- /dev/null +++ b/src/client/views/newlightbox/ExploreView/index.ts @@ -0,0 +1 @@ +export * from './ExploreView'
\ No newline at end of file diff --git a/src/client/views/newlightbox/ExploreView/utils.ts b/src/client/views/newlightbox/ExploreView/utils.ts new file mode 100644 index 000000000..7d9cf226d --- /dev/null +++ b/src/client/views/newlightbox/ExploreView/utils.ts @@ -0,0 +1,20 @@ +import { IRecommendation } from "../components"; + +export interface IExploreView { + recs?: IRecommendation[], + bounds?: IBounds +} + +export const emptyBounds = { + max_x: 0, + max_y: 0, + min_x: 0, + min_y: 0 +} + +export interface IBounds { + max_x: number, + max_y: number, + min_x: number, + min_y: number +}
\ No newline at end of file diff --git a/src/client/views/newlightbox/Header/LightboxHeader.scss b/src/client/views/newlightbox/Header/LightboxHeader.scss new file mode 100644 index 000000000..a9e60ea98 --- /dev/null +++ b/src/client/views/newlightbox/Header/LightboxHeader.scss @@ -0,0 +1,71 @@ +@import '../NewLightboxStyles.scss'; + +.newLightboxHeader-container { + width: 100%; + height: 100%; + background: $gray-l1; + border-radius: 20px 20px 0px 0px; + padding: 20px; + display: grid; + grid-template-columns: 70% 30%; + grid-template-rows: 50% 50%; + + .title-container, + .type-container { + display: flex; + flex-direction: row; + gap: 5px; + justify-content: flex-start; + align-items: center; + } + + .title-container { + grid-column: 1; + grid-row: 1; + } + + .type-container { + grid-column: 1; + grid-row: 2; + .type { + padding: 2px 7px !important; + background: $gray-l2; + } + } + + .lb-label { + color: $gray-l3; + font-weight: $h1-weight; + } + + .lb-button { + border: solid 1.5px black; + padding: 3px 5px; + cursor: pointer; + display: flex; + flex-direction: row; + justify-content: space-evenly; + align-items: center; + transition: 0.2s ease; + gap: 5px; + font-size: $body-size; + height: fit-content; + + &:hover { + background: $gray-l2; + } + + &.true { + background: $blue-l1; + } + } + + &.dark { + background: $black; + } + + &.light, + &.default { + background: $white; + } +}
\ No newline at end of file diff --git a/src/client/views/newlightbox/Header/LightboxHeader.tsx b/src/client/views/newlightbox/Header/LightboxHeader.tsx new file mode 100644 index 000000000..a272ce294 --- /dev/null +++ b/src/client/views/newlightbox/Header/LightboxHeader.tsx @@ -0,0 +1,62 @@ +import './LightboxHeader.scss'; +import * as React from 'react'; +import { INewLightboxHeader } from "./utils"; +import { NewLightboxView } from '../NewLightboxView'; +import { StrCast } from '../../../../fields/Types'; +import { EditableText } from '../components/EditableText'; +import { getType } from '../utils'; +import { Button, IconButton, Size, Type } from 'browndash-components'; +import { MdExplore, MdTravelExplore } from 'react-icons/md' +import { BsBookmark, BsBookmarkFill } from 'react-icons/bs' +import { Doc } from '../../../../fields/Doc'; +import { LightboxView } from '../../LightboxView'; +import { Colors } from '../../global/globalEnums'; + + +export const NewLightboxHeader = (props: INewLightboxHeader) => { + const {height = 100, width} = props; + const [doc, setDoc] = React.useState<Doc | undefined>(LightboxView.LightboxDoc) + const [editing, setEditing] = React.useState<boolean>(false) + const [title, setTitle] = React.useState<JSX.Element | null>( + (null) + ) + React.useEffect(() => { + let lbDoc = LightboxView.LightboxDoc + setDoc(lbDoc) + if (lbDoc) { + setTitle( + <EditableText + editing={editing} + text={StrCast(lbDoc.title)} + onEdit={(newText: string) => { + if(lbDoc) lbDoc.title = newText; + }} + setEditing={setEditing} + />) + } + }, [LightboxView.LightboxDoc]) + + const [saved, setSaved] = React.useState<boolean>(false) + + if (!doc) return null + else return <div className={`newLightboxHeader-container`} onPointerDown={(e) => e.stopPropagation()} style={{ minHeight: height, height: height, width: width }}> + <div className={`title-container`}> + <div className={`lb-label`}>Title</div> + {title} + </div> + <div className={`type-container`}> + <div className={`lb-label`}>Type</div> + <div className={`type`}>{getType(StrCast(doc.type))}</div> + </div> + <div style={{gridColumn: 2, gridRow: 1, height: '100%', display: 'flex', justifyContent: 'flex-end', alignItems: 'center'}}> + <IconButton size={Size.XSMALL} onClick={() => setSaved(!saved)} color={Colors.DARK_GRAY} icon={saved ? <BsBookmarkFill/> : <BsBookmark/>}/> + <IconButton size={Size.XSMALL} onClick={() => setSaved(!saved)} color={Colors.DARK_GRAY} icon={saved ? <BsBookmarkFill/> : <BsBookmark/>}/> + </div> + <div style={{gridColumn: 2, gridRow: 2, height: '100%', display: 'flex', justifyContent: 'flex-end', alignItems: 'center'}}> + <Button onClick={() => { + console.log(NewLightboxView.ExploreMode) + NewLightboxView.SetExploreMode(!NewLightboxView.ExploreMode) + }} size={Size.XSMALL} color={Colors.DARK_GRAY} type={Type.SEC} text={"t-SNE 2D Embeddings"} icon={<MdTravelExplore/>}/> + </div> + </div> +}
\ No newline at end of file diff --git a/src/client/views/newlightbox/Header/index.ts b/src/client/views/newlightbox/Header/index.ts new file mode 100644 index 000000000..090677c16 --- /dev/null +++ b/src/client/views/newlightbox/Header/index.ts @@ -0,0 +1 @@ +export * from './LightboxHeader'
\ No newline at end of file diff --git a/src/client/views/newlightbox/Header/utils.ts b/src/client/views/newlightbox/Header/utils.ts new file mode 100644 index 000000000..22e0487c2 --- /dev/null +++ b/src/client/views/newlightbox/Header/utils.ts @@ -0,0 +1,4 @@ +export interface INewLightboxHeader { + height?: number + width?: number +}
\ No newline at end of file diff --git a/src/client/views/newlightbox/NewLightboxStyles.scss b/src/client/views/newlightbox/NewLightboxStyles.scss new file mode 100644 index 000000000..ff4a6c971 --- /dev/null +++ b/src/client/views/newlightbox/NewLightboxStyles.scss @@ -0,0 +1,73 @@ +$white: white; +$black: black; + +// gray +$gray-l1: rgba(230, 230, 230, 1); +$gray-l2: rgb(201, 201, 201); +$gray-l3: rgba(87, 87, 87, 1); + +// blue +$blue-l1: #cfe2f3; +$blue-l2: #6fa8dc; +$blue-l3: #0b5394; +$blue-l4: #073763; + +// view backgrounds +$background-dm: black; +$background-lm: white; +$header-dm: $gray-l3; +$header-lm: $gray-l1; + +// border +$standard-border: solid 2px; + +// standard shadow + + +$text-color-dm: $gray-l1; +$text-color-lm: $gray-l3; + + +// text / font +$title-size: 2rem; +$title-weight: 700; + +$h1-size: 15px; +$h1-weight: 700; + +$h2-size: 13px; +$h2-weight: 600; + + +$body-size: 10px; +$body-weight: 400; + +// header +$header-height: 40px; + +@keyframes skeleton-loading-l3 { + 0% { + background-color: rgba(128, 128, 128, 1); + } + 100% { + background-color: rgba(128, 128, 128, 0.5); + } +} + +@keyframes skeleton-loading-l2 { + 0% { + background-color: rgba(182, 182, 182, 1); + } + 100% { + background-color: rgba(182, 182, 182, 0.5); + } +} + +@keyframes skeleton-loading-l1 { + 0% { + background-color: rgba(230, 230, 230, 1); + } + 100% { + background-color: rgba(230, 230, 230, 0.5); + } +}
\ No newline at end of file diff --git a/src/client/views/newlightbox/NewLightboxView.scss b/src/client/views/newlightbox/NewLightboxView.scss new file mode 100644 index 000000000..76c34bcf9 --- /dev/null +++ b/src/client/views/newlightbox/NewLightboxView.scss @@ -0,0 +1,34 @@ +@import './NewLightboxStyles.scss'; + +.newLightboxView-frame { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: #474545bb; + backdrop-filter: blur(4px); + z-index: 1000; + + .app-document { + width: 100%; + height: 100%; + display: grid; + } + + .explore { + width: 100%; + height: 100%; + display: grid; + } + + .newLightboxView-contents { + position: relative; + display: flex; + flex-direction: column; + + .newLightboxView-doc { + position: relative; + } + } +} diff --git a/src/client/views/newlightbox/NewLightboxView.tsx b/src/client/views/newlightbox/NewLightboxView.tsx new file mode 100644 index 000000000..c5e98da86 --- /dev/null +++ b/src/client/views/newlightbox/NewLightboxView.tsx @@ -0,0 +1,388 @@ +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { action, computed, observable } from 'mobx'; +import { observer } from 'mobx-react'; +import * as React from 'react'; +import { emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnTrue } from '../../../Utils'; +import { Doc, DocListCast, Opt, StrListCast } from '../../../fields/Doc'; +import { InkTool } from '../../../fields/InkField'; +import { Cast, NumCast, StrCast } from '../../../fields/Types'; +import { DocUtils } from '../../documents/Documents'; +import { DocumentManager } from '../../util/DocumentManager'; +import { LinkManager } from '../../util/LinkManager'; +import { SelectionManager } from '../../util/SelectionManager'; +import { Transform } from '../../util/Transform'; +import { GestureOverlay } from '../GestureOverlay'; +import { MainView } from '../MainView'; +import { DefaultStyleProvider } from '../StyleProvider'; +import { CollectionStackedTimeline } from '../collections/CollectionStackedTimeline'; +import { TabDocView } from '../collections/TabDocView'; +import { DocumentView, OpenWhere } from '../nodes/DocumentView'; +import { ExploreView } from './ExploreView'; +import { IBounds, emptyBounds } from './ExploreView/utils'; +import { NewLightboxHeader } from './Header'; +import './NewLightboxView.scss'; +import { RecommendationList } from './RecommendationList'; +import { IRecommendation } from './components'; +import { fetchKeywords, fetchRecommendations } from './utils'; +import { List } from '../../../fields/List'; +import { LightboxView } from '../LightboxView'; + +enum LightboxStatus { + RECOMMENDATIONS = "recommendations", + ANNOTATIONS = "annotations", + NONE = "none" +} + +interface LightboxViewProps { + PanelWidth: number; + PanelHeight: number; + maxBorder: number[]; +} + +type LightboxSavedState = { + panX: Opt<number>; + panY: Opt<number>; + scale: Opt<number>; + scrollTop: Opt<number>; + layout_fieldKey: Opt<string>; +}; +@observer +export class NewLightboxView extends React.Component<LightboxViewProps> { + @computed public static get LightboxDoc() { + return this._doc; + } + private static LightboxDocTemplate = () => NewLightboxView._layoutTemplate; + @observable private static _layoutTemplate: Opt<Doc>; + @observable private static _layoutTemplateString: Opt<string>; + @observable private static _doc: Opt<Doc>; + @observable private static _docTarget: Opt<Doc>; + @observable private static _docFilters: string[] = []; // filters + private static _savedState: Opt<LightboxSavedState>; + private static _history: Opt<{ doc: Doc; target?: Doc }[]> = []; + @observable private static _future: Opt<Doc[]> = []; + @observable private static _docView: Opt<DocumentView>; + + // keywords + @observable private static _keywords: string[] = [] + @action public static SetKeywords(kw: string[]) { + this._keywords = kw + } + @computed public static get Keywords() { + return this._keywords + } + + // query + @observable private static _query: string = '' + @action public static SetQuery(query: string) { + this._query = query + } + @computed public static get Query() { + return this._query + } + + // keywords + @observable private static _recs: IRecommendation[] = [] + @action public static SetRecs(recs: IRecommendation[]) { + this._recs = recs + } + @computed public static get Recs() { + return this._recs + } + + // bounds + @observable private static _bounds: IBounds = emptyBounds; + @action public static SetBounds(bounds: IBounds) { + this._bounds = bounds; + } + @computed public static get Bounds() { + return this._bounds; + } + + // explore + @observable private static _explore: Opt<boolean> = false; + @action public static SetExploreMode(status: Opt<boolean>) { + this._explore = status; + } + @computed public static get ExploreMode() { + return this._explore; + } + + // newLightbox sidebar status + @observable private static _sidebarStatus: Opt<string> = ""; + @action public static SetSidebarStatus(sidebarStatus: Opt<string>) { + this._sidebarStatus = sidebarStatus; + } + @computed public static get SidebarStatus() { + return this._sidebarStatus; + } + + static path: { doc: Opt<Doc>; target: Opt<Doc>; history: Opt<{ doc: Doc; target?: Doc }[]>; future: Opt<Doc[]>; saved: Opt<LightboxSavedState> }[] = []; + @action public static SetNewLightboxDoc(doc: Opt<Doc>, target?: Doc, future?: Doc[], layoutTemplate?: Doc | string) { + if (this.LightboxDoc && this.LightboxDoc !== doc && this._savedState) { + if (this._savedState.panX !== undefined) this.LightboxDoc._freeform_panX = this._savedState.panX; + if (this._savedState.panY !== undefined) this.LightboxDoc._freeform_panY = this._savedState.panY; + if (this._savedState.scrollTop !== undefined) this.LightboxDoc._layout_scrollTop = this._savedState.scrollTop; + if (this._savedState.scale !== undefined) this.LightboxDoc._freeform_scale = this._savedState.scale; + this.LightboxDoc.layout_fieldKey = this._savedState.layout_fieldKey; + } + if (!doc) { + this._docFilters && (this._docFilters.length = 0); + this._future = this._history = []; + Doc.ActiveTool = InkTool.None; + MainView.Instance._exploreMode = false; + } else { + const l = DocUtils.MakeLinkToActiveAudio(() => doc).lastElement(); + l && (Cast(l.link_anchor_2, Doc, null).backgroundColor = 'lightgreen'); + CollectionStackedTimeline.CurrentlyPlaying?.forEach(dv => dv.ComponentView?.Pause?.()); + //TabDocView.PinDoc(doc, { hidePresBox: true }); + this._history ? this._history.push({ doc, target }) : (this._history = [{ doc, target }]); + if (doc !== LightboxView.LightboxDoc) { + this._savedState = { + layout_fieldKey: StrCast(doc.layout_fieldKey), + panX: Cast(doc.freeform_panX, 'number', null), + panY: Cast(doc.freeform_panY, 'number', null), + scale: Cast(doc.freeform_scale, 'number', null), + scrollTop: Cast(doc.layout_scrollTop, 'number', null), + }; + } + } + if (future) { + this._future = [ + ...(this._future ?? []), + ...(this.LightboxDoc ? [this.LightboxDoc] : []), + ...future + .slice() + .sort((a, b) => NumCast(b._timecodeToShow) - NumCast(a._timecodeToShow)) + .sort((a, b) => LinkManager.Links(a).length - LinkManager.Links(b).length), + ]; + } + this._doc = doc; + this._layoutTemplate = layoutTemplate instanceof Doc ? layoutTemplate : undefined; + if (doc && (typeof layoutTemplate === 'string' ? layoutTemplate : undefined)) { + doc.layout_fieldKey = layoutTemplate; + } + this._docTarget = target || doc; + + return true; + } + public static IsNewLightboxDocView(path: DocumentView[]) { + return (path ?? []).includes(this._docView!); + } + @computed get leftBorder() { + return Math.min(this.props.PanelWidth / 4, this.props.maxBorder[0]); + } + @computed get topBorder() { + return Math.min(this.props.PanelHeight / 4, this.props.maxBorder[1]); + } + newLightboxWidth = () => this.props.PanelWidth - 420; + newLightboxHeight = () => this.props.PanelHeight - 140; + newLightboxScreenToLocal = () => new Transform(-this.leftBorder, -this.topBorder, 1); + navBtn = (left: Opt<string | number>, bottom: Opt<number>, top: number, icon: string, display: () => string, click: (e: React.MouseEvent) => void, color?: string) => { + return ( + <div + className="newLightboxView-navBtn-frame" + style={{ + display: display(), + left, + width: bottom !== undefined ? undefined : Math.min(this.props.PanelWidth / 4, this.props.maxBorder[0]), + bottom, + }}> + <div className="newLightboxView-navBtn" title={color} style={{ top, color: color ? 'red' : 'white', background: color ? 'white' : undefined }} onClick={click}> + <div style={{ height: 10 }}>{color}</div> + <FontAwesomeIcon icon={icon as any} size="3x" /> + </div> + </div> + ); + }; + public static GetSavedState(doc: Doc) { + return this.LightboxDoc === doc && this._savedState ? this._savedState : undefined; + } + + // adds a cookie to the newLightbox view - the cookie becomes part of a filter which will display any documents whose cookie metadata field matches this cookie + @action + public static SetCookie(cookie: string) { + if (this.LightboxDoc && cookie) { + this._docFilters = (f => (this._docFilters ? [this._docFilters.push(f) as any, this._docFilters][1] : [f]))(`cookies:${cookie}:provide`); + } + } + public static AddDocTab = (doc: Doc, location: OpenWhere, layoutTemplate?: Doc | string) => { + SelectionManager.DeselectAll(); + return NewLightboxView.SetNewLightboxDoc( + doc, + undefined, + [...DocListCast(doc[Doc.LayoutFieldKey(doc)]), ...DocListCast(doc[Doc.LayoutFieldKey(doc) + '_annotations']).filter(anno => anno.annotationOn !== doc), ...(NewLightboxView._future ?? [])].sort( + (a: Doc, b: Doc) => NumCast(b._timecodeToShow) - NumCast(a._timecodeToShow) + ), + layoutTemplate + ); + }; + docFilters = () => NewLightboxView._docFilters || []; + addDocTab = NewLightboxView.AddDocTab; + @action public static Next() { + const doc = NewLightboxView._doc!; + const target = (NewLightboxView._docTarget = this._future?.pop()); + const targetDocView = target && DocumentManager.Instance.getLightboxDocumentView(target); + if (targetDocView && target) { + const l = DocUtils.MakeLinkToActiveAudio(() => targetDocView.ComponentView?.getAnchor?.(true) || target).lastElement(); + l && (Cast(l.link_anchor_2, Doc, null).backgroundColor = 'lightgreen'); + DocumentManager.Instance.showDocument(target, { willZoomCentered: true, zoomScale: 0.9 }); + if (NewLightboxView._history?.lastElement().target !== target) NewLightboxView._history?.push({ doc, target }); + } else { + if (!target && NewLightboxView.path.length) { + const saved = NewLightboxView._savedState; + if (LightboxView.LightboxDoc && saved) { + LightboxView.LightboxDoc._freeform_panX = saved.panX; + LightboxView.LightboxDoc._freeform_panY = saved.panY; + LightboxView.LightboxDoc._freeform_scale = saved.scale; + LightboxView.LightboxDoc._layout_scrollTop = saved.scrollTop; + } + const pop = NewLightboxView.path.pop(); + if (pop) { + NewLightboxView._doc = pop.doc; + NewLightboxView._docTarget = pop.target; + NewLightboxView._future = pop.future; + NewLightboxView._history = pop.history; + NewLightboxView._savedState = pop.saved; + } + } else { + NewLightboxView.SetNewLightboxDoc(target); + } + } + } + + @action public static Previous() { + const previous = NewLightboxView._history?.pop(); + if (!previous || !NewLightboxView._history?.length) { + NewLightboxView.SetNewLightboxDoc(undefined); + return; + } + const { doc, target } = NewLightboxView._history?.lastElement(); + const docView = DocumentManager.Instance.getLightboxDocumentView(target || doc); + if (docView) { + NewLightboxView._docTarget = target; + target && DocumentManager.Instance.showDocument(target, { willZoomCentered: true, zoomScale: 0.9 }); + } else { + NewLightboxView.SetNewLightboxDoc(doc, target); + } + if (NewLightboxView._future?.lastElement() !== previous.target || previous.doc) NewLightboxView._future?.push(previous.target || previous.doc); + } + @action + stepInto = () => { + NewLightboxView.path.push({ + doc: LightboxView.LightboxDoc, + target: NewLightboxView._docTarget, + future: NewLightboxView._future, + history: NewLightboxView._history, + saved: NewLightboxView._savedState, + }); + const coll = NewLightboxView._docTarget; + if (coll) { + const fieldKey = Doc.LayoutFieldKey(coll); + const contents = [...DocListCast(coll[fieldKey]), ...DocListCast(coll[fieldKey + '_annotations'])]; + const links = LinkManager.Links(coll) + .map(link => LinkManager.getOppositeAnchor(link, coll)) + .filter(doc => doc) + .map(doc => doc!); + NewLightboxView.SetNewLightboxDoc(coll, undefined, contents.length ? contents : links); + } + }; + + @computed + get documentView() { + if (!LightboxView.LightboxDoc) return null + else return (<GestureOverlay isActive={true}> + <DocumentView + ref={action((r: DocumentView | null) => (NewLightboxView._docView = r !== null ? r : undefined))} + Document={LightboxView.LightboxDoc} + DataDoc={undefined} + PanelWidth={this.newLightboxWidth} + PanelHeight={this.newLightboxHeight} + LayoutTemplate={NewLightboxView.LightboxDocTemplate} + isDocumentActive={returnTrue} // without this being true, sidebar annotations need to be activated before text can be selected. + isContentActive={returnTrue} + styleProvider={DefaultStyleProvider} + ScreenToLocalTransform={this.newLightboxScreenToLocal} + renderDepth={0} + rootSelected={returnTrue} + docViewPath={returnEmptyDoclist} + docFilters={this.docFilters} + docRangeFilters={returnEmptyFilter} + searchFilterDocs={returnEmptyDoclist} + addDocument={undefined} + removeDocument={undefined} + whenChildContentsActiveChanged={emptyFunction} + addDocTab={this.addDocTab} + pinToPres={TabDocView.PinDoc} + bringToFront={emptyFunction} + onBrowseClick={MainView.Instance.exploreMode} + focus={emptyFunction} + /> + </GestureOverlay>) + } + + future = () => NewLightboxView._future; + render() { + let newLightboxHeaderHeight = 100; + let downx = 0, + downy = 0; + return !LightboxView.LightboxDoc ? null : ( + <div + className="newLightboxView-frame" + onPointerDown={e => { + downx = e.clientX; + downy = e.clientY; + }} + onClick={e => { + if (Math.abs(downx - e.clientX) < 4 && Math.abs(downy - e.clientY) < 4) { + NewLightboxView.SetNewLightboxDoc(undefined); + } + }}> + <div className={`app-document`} style={{gridTemplateColumns: `calc(100% - 400px) 400px`}}> + <div + className="newLightboxView-contents" + style={{ + top: 20, + left: 20, + width: this.newLightboxWidth(), + height: this.newLightboxHeight() - 40, + }}> + <NewLightboxHeader height={newLightboxHeaderHeight} width={this.newLightboxWidth()} /> + {!NewLightboxView._explore ? + <div className="newLightboxView-doc" style={{height: this.newLightboxHeight()}}> + {this.documentView} + </div> + : + <div className={`explore`}> + <ExploreView recs={NewLightboxView.Recs} bounds={NewLightboxView.Bounds}/> + </div> + } + </div> + <RecommendationList keywords={NewLightboxView.Keywords}/> + </div> + + </div> + ); + } +} +interface NewLightboxTourBtnProps { + navBtn: (left: Opt<string | number>, bottom: Opt<number>, top: number, icon: string, display: () => string, click: (e: React.MouseEvent) => void, color?: string) => JSX.Element; + future: () => Opt<Doc[]>; + stepInto: () => void; +} +@observer +export class NewLightboxTourBtn extends React.Component<NewLightboxTourBtnProps> { + render() { + return this.props.navBtn( + '50%', + 0, + 0, + 'chevron-down', + () => (LightboxView.LightboxDoc /*&& this.props.future()?.length*/ ? '' : 'none'), + e => { + e.stopPropagation(); + this.props.stepInto(); + }, + '' + ); + } +} diff --git a/src/client/views/newlightbox/RecommendationList/RecommendationList.scss b/src/client/views/newlightbox/RecommendationList/RecommendationList.scss new file mode 100644 index 000000000..40dd47e47 --- /dev/null +++ b/src/client/views/newlightbox/RecommendationList/RecommendationList.scss @@ -0,0 +1,117 @@ +@import '../NewLightboxStyles.scss'; + +.recommendationlist-container { + height: calc(100% - 40px); + margin: 20px; + border-radius: 20px; + overflow-y: scroll; + + .recommendations { + height: fit-content; + padding: 20px; + display: flex; + flex-direction: column; + gap: 20px; + background: $gray-l1; + border-radius: 0px 0px 20px 20px; + } + + .header { + top: 0px; + position: sticky; + background: $gray-l1; + border-bottom: $standard-border; + border-color: $gray-l2; + display: flex; + flex-direction: column; + justify-content: flex-start; + align-items: flex-start; + border-radius: 20px 20px 0px 0px; + padding: 20px; + z-index: 2; + gap: 10px; + color: $text-color-lm; + + .lb-label { + color: $gray-l3; + font-weight: $h1-weight; + font-size: $body-size; + } + + .lb-caret { + display: flex; + flex-direction: row; + justify-content: flex-end; + align-items: center; + gap: 5px; + cursor: pointer; + width: 100%; + user-select: none; + font-size: $body-size; + } + + .more { + width: 100%; + } + + &.dark { + color: $text-color-dm; + } + + .title { + height: 30px; + min-height: 30px; + font-size: $h1-size; + font-weight: $h1-weight; + text-align: left; + display: flex; + justify-content: space-between; + align-items: center; + width: 100%; + } + + .keywords { + display: flex; + flex-flow: row wrap; + gap: 5px; + + .keyword-input { + padding: 3px 7px; + background: $gray-l2; + outline: none; + border: none; + height: 21.5px; + color: $text-color-lm; + } + + .keyword { + padding: 3px 7px; + width: fit-content; + background: $gray-l2; + display: flex; + justify-content: center; + align-items: center; + flex-direction: row; + gap: 10px; + font-size: $body-size; + font-weight: $body-weight; + + &.loading { + animation: skeleton-loading-l2 1s linear infinite alternate; + min-width: 70px; + height: 21.5px; + } + } + + } + } + + &.dark { + background: $black; + } + + &.light, + &.default { + background: $gray-l1; + } +}
\ No newline at end of file diff --git a/src/client/views/newlightbox/RecommendationList/RecommendationList.tsx b/src/client/views/newlightbox/RecommendationList/RecommendationList.tsx new file mode 100644 index 000000000..9f3c32e4e --- /dev/null +++ b/src/client/views/newlightbox/RecommendationList/RecommendationList.tsx @@ -0,0 +1,196 @@ +import { GrClose } from 'react-icons/gr'; +import { IRecommendation, Recommendation } from "../components"; +import './RecommendationList.scss'; +import * as React from 'react'; +import { IRecommendationList } from "./utils"; +import { NewLightboxView } from '../NewLightboxView'; +import { DocCast, StrCast } from '../../../../fields/Types'; +import { FaCaretDown, FaCaretUp } from 'react-icons/fa'; +import { Doc, DocListCast, StrListCast } from '../../../../fields/Doc'; +import { IDocRequest, fetchKeywords, fetchRecommendations } from '../utils'; +import { IBounds } from '../ExploreView/utils'; +import { List } from '../../../../fields/List'; +import { Id } from '../../../../fields/FieldSymbols'; +import { LightboxView } from '../../LightboxView'; +import { IconButton, Size, Type } from 'browndash-components'; +import { Colors } from '../../global/globalEnums'; + +export const RecommendationList = (props: IRecommendationList) => { + const {loading, keywords} = props + const [loadingKeywords, setLoadingKeywords] = React.useState<boolean>(true) + const [showMore, setShowMore] = React.useState<boolean>(false) + const [keywordsLoc, setKeywordsLoc] = React.useState<string[]>([]) + const [update, setUpdate] = React.useState<boolean>(true) + const initialRecs: IRecommendation[] = [ + {loading: true}, + {loading: true}, + {loading: true}, + {loading: true}, + {loading: true} + ]; + const [recs, setRecs] = React.useState<IRecommendation[]>(initialRecs) + + React.useEffect(() => { + const getKeywords = async () => { + let text = StrCast(LightboxView.LightboxDoc?.text) + console.log('[1] fetching keywords') + const response = await fetchKeywords(text, 5, true) + console.log('[2] response:', response) + const kw = response.keywords; + console.log(kw); + NewLightboxView.SetKeywords(kw); + if (LightboxView.LightboxDoc) { + console.log('setting keywords on doc') + LightboxView.LightboxDoc.keywords = new List<string>(kw); + setKeywordsLoc(NewLightboxView.Keywords); + } + setLoadingKeywords(false) + } + let keywordsList = StrListCast(LightboxView.LightboxDoc!.keywords) + if (!keywordsList || keywordsList.length < 2) { + setLoadingKeywords(true) + getKeywords() + setUpdate(!update) + } else { + setKeywordsLoc(keywordsList) + setLoadingKeywords(false) + setUpdate(!update) + } + }, [NewLightboxView.LightboxDoc]) + + // terms: vannevar bush, information spaces, + React.useEffect(() => { + const getRecommendations = async () => { + console.log('fetching recommendations') + let query = 'undefined' + if (keywordsLoc) query = keywordsLoc.join(',') + let src = StrCast(NewLightboxView.LightboxDoc?.text) + let dashDocs:IDocRequest[] = []; + // get linked docs + let linkedDocs = DocListCast(NewLightboxView.LightboxDoc?.links) + console.log("linked docs", linkedDocs) + // get context docs (docs that are also in the collection) + // let contextDocs: Doc[] = DocListCast(DocCast(LightboxView.LightboxDoc?.context).data) + // let docId = LightboxView.LightboxDoc && LightboxView.LightboxDoc[Id] + // console.log("context docs", contextDocs) + // contextDocs.forEach((doc: Doc) => { + // if (docId !== doc[Id]){ + // dashDocs.push({ + // title: StrCast(doc.title), + // text: StrCast(doc.text), + // id: doc[Id], + // type: StrCast(doc.type) + // }) + // } + // }) + console.log("dash docs", dashDocs) + if (query !== undefined) { + const response = await fetchRecommendations(src, query, [], true) + const num_recs = response.num_recommendations + const recs = response.recommendations + const keywords = response.keywords + const response_bounds: IBounds = { + max_x: response.max_x, + max_y: response.max_y, + min_x: response.min_x, + min_y: response.min_y + } + // if (NewLightboxView.NewLightboxDoc) { + // NewLightboxView.NewLightboxDoc.keywords = new List<string>(keywords); + // setKeywordsLoc(NewLightboxView.Keywords); + // } + // console.log(response_bounds) + NewLightboxView.SetBounds(response_bounds) + const recommendations: IRecommendation[] = []; + for (const key in recs) { + console.log(key) + const title = recs[key].title; + const url = recs[key].url + const type = recs[key].type + const text = recs[key].text + const transcript = recs[key].transcript + const previewUrl = recs[key].previewUrl + const embedding = recs[key].embedding + const distance = recs[key].distance + const source = recs[key].source + const related_concepts = recs[key].related_concepts + const docId = recs[key].doc_id + related_concepts.length >= 1 && recommendations.push({ + title: title, + data: url, + type: type, + text: text, + transcript: transcript, + previewUrl: previewUrl, + embedding: embedding, + distance: Math.round(distance * 100) / 100, + source: source, + related_concepts: related_concepts, + docId: docId + }) + } + recommendations.sort((a, b) => { + if (a.distance && b.distance) { + return a.distance - b.distance + } else return 0 + }) + console.log("[rec]: ", recommendations) + NewLightboxView.SetRecs(recommendations) + setRecs(recommendations) + } + } + getRecommendations(); + }, [update]) + + + + return <div className={`recommendationlist-container`} onPointerDown={(e) => {e.stopPropagation()}}> + <div className={`header`}> + <div className={`title`}> + Recommendations + </div> + {NewLightboxView.LightboxDoc && <div style={{fontSize: 10}}> + The recommendations are produced based on the text in the document <b><u>{StrCast(NewLightboxView.LightboxDoc.title)}</u></b>. The following keywords are used to fetch the recommendations. + </div>} + <div className={`lb-label`}>Keywords</div> + {loadingKeywords ? <div className={`keywords`}> + <div className={`keyword ${loadingKeywords && 'loading'}`}/> + <div className={`keyword ${loadingKeywords && 'loading'}`}/> + <div className={`keyword ${loadingKeywords && 'loading'}`}/> + <div className={`keyword ${loadingKeywords && 'loading'}`}/> + </div> + : + <div className={`keywords`}> + {keywordsLoc && keywordsLoc.map((word, ind) => { + return <div className={`keyword`}> + {word} + <IconButton type={Type.PRIM} size={Size.XSMALL} color={Colors.DARK_GRAY} icon={<GrClose/>} onClick={() => { + let kw = keywordsLoc + kw.splice(ind) + NewLightboxView.SetKeywords(kw) + }}/> + </div> + })} + </div> + } + {!showMore ? + <div className={`lb-caret`} onClick={() => {setShowMore(true)}}> + More <FaCaretDown/> + </div> + : + <div className={`more`}> + <div className={`lb-caret`} onClick={() => {setShowMore(false)}}> + Less <FaCaretUp/> + </div> + <div className={`lb-label`}>Type</div> + <div className={`lb-label`}>Sources</div> + </div> + } + </div> + <div className={`recommendations`}> + {recs && recs.map((rec: IRecommendation) => { + return <Recommendation {...rec} /> + })} + </div> + </div> +}
\ No newline at end of file diff --git a/src/client/views/newlightbox/RecommendationList/index.ts b/src/client/views/newlightbox/RecommendationList/index.ts new file mode 100644 index 000000000..f4555c1f2 --- /dev/null +++ b/src/client/views/newlightbox/RecommendationList/index.ts @@ -0,0 +1 @@ +export * from './RecommendationList'
\ No newline at end of file diff --git a/src/client/views/newlightbox/RecommendationList/utils.ts b/src/client/views/newlightbox/RecommendationList/utils.ts new file mode 100644 index 000000000..cdfff3258 --- /dev/null +++ b/src/client/views/newlightbox/RecommendationList/utils.ts @@ -0,0 +1,9 @@ +import { IRecommendation } from "../components"; + +export interface IRecommendationList { + loading?: boolean, + keywords?: string[], + recs?: IRecommendation[] + getRecs?: any +} + diff --git a/src/client/views/newlightbox/components/EditableText/EditableText.scss b/src/client/views/newlightbox/components/EditableText/EditableText.scss new file mode 100644 index 000000000..7828538ab --- /dev/null +++ b/src/client/views/newlightbox/components/EditableText/EditableText.scss @@ -0,0 +1,34 @@ +@import '../../NewLightboxStyles.scss'; + +.lb-editableText, +.lb-displayText { + padding: 4px 7px !important; + border: $standard-border !important; + border-color: $gray-l2 !important; +} + +.lb-editableText { + -webkit-appearance: none; + overflow: hidden; + font-size: inherit; + border: none; + outline: none; + width: 100%; + margin: 0px; + padding: 0px; + box-shadow: none !important; + background: none; + + &:focus { + outline: none; + background-color: $blue-l1; + } +} + +.lb-displayText { + cursor: text !important; + width: 100%; + display: flex; + align-items: center; + font-size: inherit; +}
\ No newline at end of file diff --git a/src/client/views/newlightbox/components/EditableText/EditableText.tsx b/src/client/views/newlightbox/components/EditableText/EditableText.tsx new file mode 100644 index 000000000..e9e7ca264 --- /dev/null +++ b/src/client/views/newlightbox/components/EditableText/EditableText.tsx @@ -0,0 +1,65 @@ +import * as React from 'react' +import './EditableText.scss' +import { Size } from 'browndash-components' + +export interface IEditableTextProps { + text: string + placeholder?: string + editing: boolean + onEdit: (newText: string) => void + setEditing: (editing: boolean) => void + backgroundColor?: string + size?: Size + height?: number +} + +/** + * Editable Text is used for inline renaming of some text. + * It appears as normal UI text but transforms into a text input field when the user clicks on or focuses it. + * @param props + * @returns + */ +export const EditableText = (props: IEditableTextProps) => { + const { + editing, + height, + size, + text, + onEdit, + setEditing, + backgroundColor, + placeholder, + } = props + + const handleOnChange = (event: React.ChangeEvent<HTMLInputElement>) => { + onEdit(event.target.value) + } + + return editing ? ( + <input + style={{ background: backgroundColor, height: height }} + placeholder={placeholder} + size={1} + className="lb-editableText" + autoFocus + onChange={handleOnChange} + onBlur={() => setEditing(false)} + defaultValue={text} + ></input> + ) : ( + <input + style={{ background: backgroundColor, height: height }} + placeholder={placeholder} + size={1} + className="lb-editableText" + autoFocus + onChange={handleOnChange} + onBlur={() => setEditing(false)} + defaultValue={text} + ></input> + // <div className="lb-displayText" onClick={(e) => { + // e.stopPropagation() + // setEditing(true) + // }}>{text}</div> + ) +} diff --git a/src/client/views/newlightbox/components/EditableText/index.ts b/src/client/views/newlightbox/components/EditableText/index.ts new file mode 100644 index 000000000..e3367b175 --- /dev/null +++ b/src/client/views/newlightbox/components/EditableText/index.ts @@ -0,0 +1 @@ +export * from './EditableText' diff --git a/src/client/views/newlightbox/components/Recommendation/Recommendation.scss b/src/client/views/newlightbox/components/Recommendation/Recommendation.scss new file mode 100644 index 000000000..c86c63ba0 --- /dev/null +++ b/src/client/views/newlightbox/components/Recommendation/Recommendation.scss @@ -0,0 +1,176 @@ +@import '../../NewLightboxStyles.scss'; + +.recommendation-container { + width: 100%; + height: fit-content; + min-height: 180px; + border-radius: 20px; + display: grid; + grid-template-columns: 0% 100%; + grid-template-rows: auto auto auto auto auto; + gap: 5px 0px; + padding: 10px; + cursor: pointer; + transition: 0.2s ease; + border: $standard-border; + border-color: $gray-l2; + background: white; + + &:hover { + // background: white !important; + transform: scale(1.02); + z-index: 0; + + .title { + text-decoration: underline; + } + } + + &.previewUrl { + grid-template-columns: calc(30% - 10px) 70%; + grid-template-rows: auto auto auto auto auto; + gap: 5px 10px; + } + + &.loading { + animation: skeleton-loading-l2 1s linear infinite alternate; + border: none; + grid-template-columns: calc(30% - 10px) 70%; + grid-template-rows: auto auto auto auto auto; + gap: 5px 10px; + + .image-container, + .title, + .info, + .source, + .explainer, + .hide-rec { + animation: skeleton-loading-l3 1s linear infinite alternate; + } + + .title { + border-radius: 20px; + } + } + + .distance-container, + .type-container, + .source-container { + display: flex; + flex-direction: row; + justify-content: flex-start; + align-items: center; + gap: 5px; + } + + .image-container { + grid-row: 2/5; + grid-column: 1; + border-radius: 20px; + overflow: hidden; + + .image { + width: 100%; + height: 100%; + object-fit: cover; + } + } + + .title { + grid-row: 1; + grid-column: 1/3; + border-radius: 20px; + font-size: $h2-size; + font-weight: $h2-weight; + overflow: hidden; + border-radius: 0px; + min-height: 30px; + } + + .info { + grid-row: 2; + grid-column: 2; + border-radius: 20px; + display: flex; + flex-direction: row; + gap: 5px; + font-size: $body-size; + + .lb-type { + padding: 2px 7px !important; + background: $gray-l2; + } + } + + .lb-label { + color: $gray-l3; + font-weight: $h1-weight; + font-size: $body-size; + } + + .source { + grid-row: 3; + grid-column: 2; + border-radius: 20px; + font-size: $body-size; + display: flex; + justify-content: flex-start; + align-items: center; + + .lb-source { + padding: 2px 7px !important; + background: $gray-l2; + border-radius: 10px; + white-space: nowrap; + max-width: 130px; + text-overflow: ellipsis; + overflow: hidden; + } + } + + .explainer { + grid-row: 4; + grid-column: 2; + border-radius: 20px; + font-size: 10px; + width: 100%; + background: $blue-l1; + border-radius: 0; + padding: 10px; + + .concepts-container { + display: flex; + flex-flow: row wrap; + margin-top: 3px; + gap: 3px; + .concept { + padding: 2px 7px !important; + background: $gray-l2; + } + } + } + + .hide-rec { + grid-row: 5; + grid-column: 2; + border-radius: 20px; + font-size: $body-size; + display: flex; + align-items: center; + margin-top: 5px; + gap: 5px; + justify-content: flex-end; + text-transform: underline; + } + + &.dark { + background: $black; + border-color: $white; + } + + &.light, + &.default { + background: $white; + border-color: $white; + } +}
\ No newline at end of file diff --git a/src/client/views/newlightbox/components/Recommendation/Recommendation.tsx b/src/client/views/newlightbox/components/Recommendation/Recommendation.tsx new file mode 100644 index 000000000..c0d357ad5 --- /dev/null +++ b/src/client/views/newlightbox/components/Recommendation/Recommendation.tsx @@ -0,0 +1,90 @@ +import * as React from 'react'; +import { IRecommendation } from "./utils"; +import './Recommendation.scss'; +import { getType } from '../../utils'; +import { FaEyeSlash } from 'react-icons/fa'; +import { NewLightboxView } from '../../NewLightboxView'; +import { DocumentManager } from '../../../../util/DocumentManager'; +import { Doc } from '../../../../../fields/Doc'; +import { Docs } from '../../../../documents/Documents'; + +export const Recommendation = (props: IRecommendation) => { + const {title, data, type, text, transcript, loading, source, previewUrl, related_concepts, distance, docId} = props + + return <div className={`recommendation-container ${loading && 'loading'} ${previewUrl && 'previewUrl'}`} onClick={() => { + let doc: Doc | null = null; + if (source == "Dash" && docId) { + const docView = DocumentManager.Instance.getDocumentViewById(docId) + if (docView) { + doc = docView.rootDoc; + } + } else if (data) { + console.log(data, type) + switch(type) { + case "YouTube": + console.log('create ', type, 'document') + doc = Docs.Create.VideoDocument(data, { title: title, _width: 400, _height: 315, transcript: transcript }) + break; + case "Video": + console.log('create ', type, 'document') + doc = Docs.Create.VideoDocument(data, { title: title, _width: 400, _height: 315, transcript: transcript }) + break; + case "Webpage": + console.log('create ', type, 'document') + doc = Docs.Create.WebDocument(data, { title: title, text: text }) + break; + case "HTML": + console.log('create ', type, 'document') + doc = Docs.Create.WebDocument(data, { title: title, text: text }) + break; + case "Text": + console.log('create ', type, 'document') + doc = Docs.Create.TextDocument(data, { title: title, text: text }) + break; + case "PDF": + console.log('create ', type, 'document') + doc = Docs.Create.PdfDocument(data, { title: title, text: text }) + break; + } + } + if (doc !== null) NewLightboxView.SetNewLightboxDoc(doc) + }}> + {loading ? + <div className={`image-container`}> + </div> + : + previewUrl ? <div className={`image-container`}> + {<img className={`image`} src={previewUrl}></img>} + </div> + : null + } + <div className={`title`}>{title}</div> + <div className={`info`}> + {!loading && <div className={`type-container`}> + <div className={`lb-label`}>Type</div><div className={`lb-type`}>{getType(type!)}</div> + </div>} + {!loading && <div className={`distance-container`}> + <div className={`lb-label`}>Distance</div><div className={`lb-distance`}>{distance}</div> + </div>} + </div> + <div className={`source`}> + {!loading && <div className={`source-container`}> + <div className={`lb-label`}>Source</div><div className={`lb-source`}>{source}</div> + </div>} + </div> + <div className={`explainer`}> + {!loading && + <div> + You are seeing this recommendation because this document also explores + <div className={`concepts-container`}> + {related_concepts?.map((val) => { + return <div className={'concept'}>{val}</div> + })} + </div> + </div>} + </div> + <div className={`hide-rec`}> + {!loading && <><div>Hide Recommendation</div><div style={{fontSize: 15, paddingRight: 5}}><FaEyeSlash/></div></>} + </div> + </div> +}
\ No newline at end of file diff --git a/src/client/views/newlightbox/components/Recommendation/index.ts b/src/client/views/newlightbox/components/Recommendation/index.ts new file mode 100644 index 000000000..12ebf9d6e --- /dev/null +++ b/src/client/views/newlightbox/components/Recommendation/index.ts @@ -0,0 +1,2 @@ +export * from './utils' +export * from './Recommendation'
\ No newline at end of file diff --git a/src/client/views/newlightbox/components/Recommendation/utils.ts b/src/client/views/newlightbox/components/Recommendation/utils.ts new file mode 100644 index 000000000..796ce0eb0 --- /dev/null +++ b/src/client/views/newlightbox/components/Recommendation/utils.ts @@ -0,0 +1,23 @@ +import { DocumentType } from "../../../../documents/DocumentTypes" + +export interface IRecommendation { + loading?: boolean + type?: DocumentType | string, + data?: string, + title?: string, + text?: string, + source?: string, + previewUrl?: string, + transcript?: { + text: string, + start: number, + duration: number + }[], + embedding?: { + x: number, + y: number + }, + distance?: number, + related_concepts?: string[], + docId?: string +}
\ No newline at end of file diff --git a/src/client/views/newlightbox/components/SkeletonDoc/SkeletonDoc.scss b/src/client/views/newlightbox/components/SkeletonDoc/SkeletonDoc.scss new file mode 100644 index 000000000..e541e3f3c --- /dev/null +++ b/src/client/views/newlightbox/components/SkeletonDoc/SkeletonDoc.scss @@ -0,0 +1,82 @@ +@import '../../NewLightboxStyles.scss'; + +.skeletonDoc-container { + display: flex; + flex-direction: column; + height: calc(100% - 40px); + margin: 20px; + gap: 20px; + + .header { + width: calc(100% - 20px); + height: 80px; + background: $gray-l2; + animation: skeleton-loading-l2 1s linear infinite alternate; + display: grid; + grid-template-rows: 60% 40%; + padding: 10px; + grid-template-columns: auto auto auto auto; + border-radius: 20px; + + .title { + grid-row: 1; + grid-column: 1 / 5; + display: flex; + width: fit-content; + height: 100%; + min-width: 500px; + font-size: $title-size; + animation: skeleton-loading-l3 1s linear infinite alternate; + border-radius: 20px; + } + + .type { + display: flex; + padding: 3px 7px; + width: fit-content; + height: fit-content; + margin-top: 8px; + min-height: 15px; + min-width: 60px; + grid-row: 2; + grid-column: 1; + animation: skeleton-loading-l3 1s linear infinite alternate; + border-radius: 20px; + } + + .buttons-container { + grid-row: 1 / 3; + grid-column: 5; + display: flex; + justify-content: flex-end; + align-items: center; + gap: 10px; + + .button { + width: 50px; + height: 50px; + border-radius: 100%; + animation: skeleton-loading-l3 1s linear infinite alternate; + } + } + + } + + .content { + width: 100%; + flex: 1; + -webkit-flex: 1; /* Chrome */ + background: $gray-l2; + animation: skeleton-loading-l2 1s linear infinite alternate; + border-radius: 20px; + } + + // &.dark { + // background: $black; + // } + + // &.light, + // &.default { + // background: $white; + // } +}
\ No newline at end of file diff --git a/src/client/views/newlightbox/components/SkeletonDoc/SkeletonDoc.tsx b/src/client/views/newlightbox/components/SkeletonDoc/SkeletonDoc.tsx new file mode 100644 index 000000000..50cee893f --- /dev/null +++ b/src/client/views/newlightbox/components/SkeletonDoc/SkeletonDoc.tsx @@ -0,0 +1,22 @@ +import './SkeletonDoc.scss'; +import { ISkeletonDoc } from "./utils"; +import * as React from 'react'; + +export const SkeletonDoc = (props: ISkeletonDoc) => { + const { type, data } = props + + return <div className={`skeletonDoc-container`}> + <div className={`header`}> + <div className={`title`}></div> + <div className={`type`}></div> + <div className={`tags`}></div> + <div className={`buttons-container`}> + <div className={`button`}></div> + <div className={`button`}></div> + </div> + </div> + <div className={`content`}> + {data} + </div> + </div> +}
\ No newline at end of file diff --git a/src/client/views/newlightbox/components/SkeletonDoc/index.ts b/src/client/views/newlightbox/components/SkeletonDoc/index.ts new file mode 100644 index 000000000..396b7272b --- /dev/null +++ b/src/client/views/newlightbox/components/SkeletonDoc/index.ts @@ -0,0 +1 @@ +export * from './SkeletonDoc'
\ No newline at end of file diff --git a/src/client/views/newlightbox/components/SkeletonDoc/utils.ts b/src/client/views/newlightbox/components/SkeletonDoc/utils.ts new file mode 100644 index 000000000..81c32c328 --- /dev/null +++ b/src/client/views/newlightbox/components/SkeletonDoc/utils.ts @@ -0,0 +1,5 @@ +import { IRecommendation } from "../Recommendation"; + +export interface ISkeletonDoc extends IRecommendation { + +}
\ No newline at end of file diff --git a/src/client/views/newlightbox/components/Template/Template.scss b/src/client/views/newlightbox/components/Template/Template.scss new file mode 100644 index 000000000..5b72ddaf9 --- /dev/null +++ b/src/client/views/newlightbox/components/Template/Template.scss @@ -0,0 +1,15 @@ +@import '../../NewLightboxStyles.scss'; + +.template-container { + width: 100vw; + height: 100vh; + + &.dark { + background: $black; + } + + &.light, + &.default { + background: $white; + } +}
\ No newline at end of file diff --git a/src/client/views/newlightbox/components/Template/Template.tsx b/src/client/views/newlightbox/components/Template/Template.tsx new file mode 100644 index 000000000..9c6f0f59c --- /dev/null +++ b/src/client/views/newlightbox/components/Template/Template.tsx @@ -0,0 +1,10 @@ +import './Template.scss'; +import * as React from 'react'; +import { ITemplate } from "./utils"; + +export const Template = (props: ITemplate) => { + + return <div className={`template-container`}> + + </div> +}
\ No newline at end of file diff --git a/src/client/views/newlightbox/components/Template/index.ts b/src/client/views/newlightbox/components/Template/index.ts new file mode 100644 index 000000000..36b5f3f46 --- /dev/null +++ b/src/client/views/newlightbox/components/Template/index.ts @@ -0,0 +1 @@ +export * from './Template'
\ No newline at end of file diff --git a/src/client/views/newlightbox/components/Template/utils.ts b/src/client/views/newlightbox/components/Template/utils.ts new file mode 100644 index 000000000..965e653ec --- /dev/null +++ b/src/client/views/newlightbox/components/Template/utils.ts @@ -0,0 +1,3 @@ +export interface ITemplate { + +}
\ No newline at end of file diff --git a/src/client/views/newlightbox/components/index.ts b/src/client/views/newlightbox/components/index.ts new file mode 100644 index 000000000..3f9128690 --- /dev/null +++ b/src/client/views/newlightbox/components/index.ts @@ -0,0 +1,3 @@ +export * from './Template' +export * from './Recommendation' +export * from './SkeletonDoc'
\ No newline at end of file diff --git a/src/client/views/newlightbox/utils.ts b/src/client/views/newlightbox/utils.ts new file mode 100644 index 000000000..6016abca4 --- /dev/null +++ b/src/client/views/newlightbox/utils.ts @@ -0,0 +1,121 @@ +import { DocumentType } from "../../documents/DocumentTypes"; +import { IRecommendation } from "./components"; + +export interface IDocRequest { + id: string, + title: string, + text: string, + type: string +} + +export const fetchRecommendations = async (src: string, query: string, docs?: IDocRequest[], dummy?: boolean) => { + console.log("[rec] making request") + if (dummy) { + return { + "recommendations": dummyRecs, + "keywords": dummyKeywords, + "num_recommendations": 4, + "max_x": 100, + "max_y": 100, + "min_x": 0, + "min_y": 0 + + }; + } + const response = await fetch('http://127.0.0.1:8000/recommend', { + method: 'POST', + headers: { + 'Accept': 'application/json', + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + "src": src, + "query": query, + "docs": docs + }) + }) + const data = await response.json(); + + return data; +} + +export const fetchKeywords = async (text: string, n: number, dummy?: boolean) => { + console.log("[fetchKeywords]") + if (dummy) { + return { + "keywords": dummyKeywords + }; + } + const response = await fetch('http://127.0.0.1:8000/keywords', { + method: 'POST', + headers: { + 'Accept': 'application/json', + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + "text": text, + "n": n + }) + }) + const data = await response.json() + return data; +} + +export const getType = (type: DocumentType | string) => { + switch(type) { + case DocumentType.AUDIO: + return "Audio" + case DocumentType.VID: + return "Video" + case DocumentType.PDF: + return "PDF" + case DocumentType.WEB: + return "Webpage" + case "YouTube": + return "Video" + case "HTML": + return "Webpage" + default: + return "Unknown: " + type + } +} + +const dummyRecs = { + "a": { + title: 'Vannevar Bush - American Engineer', + previewUrl: 'https://cdn.britannica.com/98/23598-004-1E6A382E/Vannevar-Bush-Differential-Analyzer-1935.jpg', + type: 'web', + distance: 2.3, + source: 'www.britannica.com', + related_concepts: ['vannevar bush', 'knowledge'], + embedding: { + x: 0, + y: 0 + } + }, + "b": { + title: "From Memex to hypertext: Vannevar Bush and the mind's machine", + type: 'pdf', + distance: 5.4, + source: 'Google Scholar', + related_concepts: ['memex', 'vannevar bush', 'hypertext'], + }, + "c": { + title: 'How the hyperlink changed everything | Small Thing Big Idea, a TED series', + previewUrl: 'https://pi.tedcdn.com/r/talkstar-photos.s3.amazonaws.com/uploads/b17d043f-2642-4117-a913-52204505513f/MargaretGouldStewart_2018V-embed.jpg?u%5Br%5D=2&u%5Bs%5D=0.5&u%5Ba%5D=0.8&u%5Bt%5D=0.03&quality=82w=640', + type: 'youtube', + distance: 5.3, + source: 'www.youtube.com', + related_concepts: ['User Control', 'Explanations'] + }, + "d": { + title: 'Recommender Systems: Behind the Scenes of Machine Learning-Based Personalization', + previewUrl: 'https://sloanreview.mit.edu/wp-content/uploads/2018/10/MAG-Ransbotham-Ratings-Recommendations-1200X627-1200x627.jpg', + type: 'pdf', + distance: 9.3, + source: 'www.altexsoft.com', + related_concepts: ['User Control', 'Explanations'] + } +} + +const dummyKeywords = ['user control', 'vannevar bush', 'hypermedia', 'hypertext']
\ No newline at end of file diff --git a/src/client/views/nodes/DocumentView.scss b/src/client/views/nodes/DocumentView.scss index f1627e1e1..b25540dd3 100644 --- a/src/client/views/nodes/DocumentView.scss +++ b/src/client/views/nodes/DocumentView.scss @@ -115,7 +115,10 @@ width: 100%; height: 100%; transition: inherit; - + display: flex; + justify-content: center; + align-items: center; + .sharingIndicator { height: 30px; width: 30px; diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index b6f1626f8..6b37dc419 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -1232,7 +1232,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps const renderDoc = this.renderDoc({ borderRadius: this.borderRounding, outline: highlighting && !this.borderRounding && !highlighting.highlightStroke ? `${highlighting.highlightColor} ${highlighting.highlightStyle} ${highlighting.highlightIndex}px` : 'solid 0px', - border: highlighting && this.borderRounding && highlighting.highlightStyle === 'dashed' ? `${highlighting.highlightStyle} ${highlighting.highlightColor} ${highlighting.highlightIndex}px` : undefined, + border: highlighting && this.borderRounding && highlighting.highlightStyle === 'dashed' ? `${highlighting.highlightStyle} ${highlighting.highlightColor} ${highlighting.highlightIndex}px` : undefined, boxShadow, clipPath: borderPath?.clipPath, }); diff --git a/src/client/views/nodes/button/FontIconBox.tsx b/src/client/views/nodes/button/FontIconBox.tsx index 5bba51ec8..e2fe0bcf1 100644 --- a/src/client/views/nodes/button/FontIconBox.tsx +++ b/src/client/views/nodes/button/FontIconBox.tsx @@ -1,6 +1,7 @@ import { IconProp } from '@fortawesome/fontawesome-svg-core'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Tooltip } from '@material-ui/core'; +import { Button, ColorPicker, Dropdown, DropdownType, IconButton, IListItemProps, OrientationType, Size, Toggle, ToggleType, Type } from 'browndash-components'; import { action, computed, observable, runInAction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; @@ -33,6 +34,7 @@ import { OpenWhere } from '../DocumentView'; import { RichTextMenu } from '../formattedText/RichTextMenu'; import { WebBox } from '../WebBox'; import { FontIconBadge } from './FontIconBadge'; +import * as fa from 'react-icons/fa' import './FontIconBox.scss'; export enum ButtonType { @@ -309,19 +311,24 @@ export class FontIconBox extends DocComponent<ButtonProps>() { } // Get items to place into the list - const list = this.buttonList + const list: IListItemProps[] = this.buttonList .filter(value => !Doc.noviceMode || !noviceList.length || noviceList.includes(value)) .map(value => ( - <div - className="list-item" - key={value} - style={{ - fontFamily: script.script.originalScript.startsWith('{ return setFont') ? value : undefined, - backgroundColor: value === text ? Colors.LIGHT_BLUE : undefined, - }} - onClick={undoable(() => script.script.run({ self: this.rootDoc, value }), value)}> - {value[0].toUpperCase() + value.slice(1)} - </div> + { + text: value, + shortcut: '#', + icon: <fa.FaCaretUp/> + } + // <div + // className="list-item" + // key={value} + // style={{ + // fontFamily: script.script.originalScript.startsWith('{ return setFont') ? value : undefined, + // backgroundColor: value === text ? Colors.LIGHT_BLUE : undefined, + // }} + // onClick={undoable(() => script.script.run({ self: this.rootDoc, value }), value)}> + // {value[0].toUpperCase() + value.slice(1)} + // </div> )); const label = @@ -332,6 +339,10 @@ export class FontIconBox extends DocComponent<ButtonProps>() { ); return ( + <Dropdown type={Type.PRIM} dropdownType={DropdownType.CLICK} items={list} location={OrientationType.LEFT}/> + ) + + return ( <div className={`menuButton ${this.type} ${active}`} style={{ backgroundColor: this.rootDoc.dropDownOpen ? Colors.MEDIUM_BLUE : backgroundColor, color: color, display: dropdown ? undefined : 'flex' }} @@ -401,6 +412,15 @@ export class FontIconBox extends DocComponent<ButtonProps>() { {this.label} </div> ); + + return ( + <ColorPicker onChange={(e) => { + this.colorPickerClosed = !this.colorPickerClosed; + this.noTooltip = !this.colorPickerClosed; + setTimeout(() => Doc.UnBrushAllDocs()); + e.stopPropagation(); + }}/> + ) return ( <div @@ -453,6 +473,10 @@ export class FontIconBox extends DocComponent<ButtonProps>() { {this.label} </div> ); + console.log("switchToggle", switchToggle, this.rootDoc.title) + return ( + <Toggle toggleType={ToggleType.BUTTON} type={Type.PRIM} toggleStatus={switchToggle} text={buttonText} color={color} icon={this.Icon(color)!} label={this.label}/> + ) if (switchToggle) { return ( @@ -480,6 +504,11 @@ export class FontIconBox extends DocComponent<ButtonProps>() { @computed get defaultButton() { const color = this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.Color); const backgroundColor = this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.BackgroundColor); + + return ( + <IconButton icon={this.Icon(color)!} label={this.label}/> + ) + return ( <div className={`menuButton ${this.type}`} onContextMenu={this.specificContextMenu} style={{ backgroundColor: 'transparent', borderBottomLeftRadius: this.dropdown ? 0 : undefined }}> <div className="menuButton-wrap"> @@ -548,18 +577,20 @@ export class FontIconBox extends DocComponent<ButtonProps>() { break; case ButtonType.ClickButton: case ButtonType.ToolButton: button = ( - <div className={`menuButton ${this.type + (FontIconBox.GetShowLabels() ? 'Label' : '')}`} style={{ backgroundColor, color, opacity: 1 }}> - {this.Icon(color)} - {label()} - </div> + <IconButton color={color} icon={this.Icon(color)!} label={this.label}/> + // <div className={`menuButton ${this.type + (FontIconBox.GetShowLabels() ? 'Label' : '')}`} style={{ backgroundColor, color, opacity: 1 }}> + // {this.Icon(color)} + // {label()} + // </div> ); break; case ButtonType.MenuButton: button = ( - <div className={`menuButton ${this.type}`} style={{ color, backgroundColor }}> - {this.Icon(color)} - {label(true)} - <FontIconBadge value={Cast(this.Document.badgeValue, 'string', null)} /> - </div> + <IconButton size={Size.LARGE} color={color} icon={this.Icon(color)!} label={this.label}/> + // <div className={`menuButton ${this.type}`} style={{ color, backgroundColor }}> + // {this.Icon(color)} + // {label(true)} + // <FontIconBadge value={Cast(this.Document.badgeValue, 'string', null)} /> + // </div> ); break; } diff --git a/src/client/views/nodes/button/colorDropdown/ColorDropdown.tsx b/src/client/views/nodes/button/colorDropdown/ColorDropdown.tsx deleted file mode 100644 index 74c3c563c..000000000 --- a/src/client/views/nodes/button/colorDropdown/ColorDropdown.tsx +++ /dev/null @@ -1,77 +0,0 @@ -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import React, { Component } from 'react'; -import { BoolCast, StrCast } from '../../../../../fields/Types'; -import { IButtonProps } from '../ButtonInterface'; -import { ColorState, SketchPicker } from 'react-color'; -import { ScriptField } from '../../../../../fields/ScriptField'; -import { Doc } from '../../../../../fields/Doc'; -import { FontIconBox } from '../FontIconBox'; - -export class ColorDropdown extends Component<IButtonProps> { - render() { - const active: string = StrCast(this.props.rootDoc.dropDownOpen); - - const script: string = StrCast(this.props.rootDoc.script); - const scriptCheck: string = script + '(undefined, true)'; - const boolResult = ScriptField.MakeScript(scriptCheck)?.script.run().result; - - const stroke: boolean = false; - // if (script === "setStrokeColor") { - // stroke = true; - // const checkWidth = ScriptField.MakeScript("setStrokeWidth(0, true)")?.script.run().result; - // const width = 20 + (checkWidth / 100) * 70; - // const height = 20 + (checkWidth / 100) * 70; - // strokeIcon = (<div style={{ borderRadius: "100%", width: width + '%', height: height + '%', backgroundColor: boolResult ? boolResult : "#FFFFFF" }} />); - // } - - const colorOptions: string[] = ['#D0021B', '#F5A623', '#F8E71C', '#8B572A', '#7ED321', '#417505', '#9013FE', '#4A90E2', '#50E3C2', '#B8E986', '#000000', '#4A4A4A', '#9B9B9B', '#FFFFFF', '#f1efeb']; - - const colorBox = (func: (color: ColorState) => void) => <SketchPicker disableAlpha={!stroke} onChange={func} color={boolResult ? boolResult : '#FFFFFF'} presetColors={colorOptions} />; - const label = - !this.props.label || !FontIconBox.GetShowLabels() ? null : ( - <div className="fontIconBox-label" style={{ color: this.props.color, backgroundColor: this.props.backgroundColor, position: 'absolute' }}> - {this.props.label} - </div> - ); - - const dropdownCaret = ( - <div className="menuButton-dropDown" style={{ borderBottomRightRadius: active ? 0 : undefined }}> - <FontAwesomeIcon icon={'caret-down'} color={this.props.color} size="sm" /> - </div> - ); - - const click = (value: ColorState) => { - const hex: string = value.hex; - const s = ScriptField.MakeScript(script + '("' + hex + '", false)'); - if (s) { - s.script.run().result; - } - }; - return ( - <div - className={`menuButton ${this.props.type} ${active}`} - style={{ color: this.props.color, borderBottomLeftRadius: active ? 0 : undefined }} - onClick={() => (this.props.rootDoc.dropDownOpen = !this.props.rootDoc.dropDownOpen)} - onPointerDown={e => e.stopPropagation()}> - <FontAwesomeIcon className={`fontIconBox-icon-${this.props.type}`} icon={this.props.icon} color={this.props.color} /> - <div className="colorButton-color" style={{ backgroundColor: boolResult ? boolResult : '#FFFFFF' }} /> - {label} - {/* {dropdownCaret} */} - {this.props.rootDoc.dropDownOpen ? ( - <div> - <div className="menuButton-dropdownBox" onPointerDown={e => e.stopPropagation()} onClick={e => e.stopPropagation()}> - {colorBox(click)} - </div> - <div - className="dropbox-background" - onClick={e => { - e.stopPropagation(); - this.props.rootDoc.dropDownOpen = false; - }} - /> - </div> - ) : null} - </div> - ); - } -} diff --git a/src/client/views/nodes/button/colorDropdown/index.ts b/src/client/views/nodes/button/colorDropdown/index.ts deleted file mode 100644 index 1147d6457..000000000 --- a/src/client/views/nodes/button/colorDropdown/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './ColorDropdown';
\ No newline at end of file diff --git a/src/client/views/search/SearchBox.scss b/src/client/views/search/SearchBox.scss index e8865b918..5516cf205 100644 --- a/src/client/views/search/SearchBox.scss +++ b/src/client/views/search/SearchBox.scss @@ -9,16 +9,21 @@ background: none; z-index: 1000; padding: 0px; + overflow: scroll; cursor: default; .searchBox-bar { width: 100%; - height: 35px; + height: fit-content; display: flex; justify-content: center; align-items: center; background-color: none; padding: 5px; + top: 0px; + position: sticky; + background: $light-gray; + border-bottom: $standard-border; .searchBox-type { display: block; @@ -42,34 +47,66 @@ } } - .searchBox-results-container { + .section-header { + + .section-title { + font-size: $body-text; + font-weight: 600; + } + + .section-subtitle { + display: flex; + color: $light-gray; + } + + padding: 5px 10px; + display: flex; + flex-direction: column; + gap: 3px; + background: $medium-blue; + color: white; + } + + .searchBox-recommendations-container { display: flex; flex-direction: column; width: 100%; - height: 100%; + height: fit-content; justify-content: "center"; - - .searchBox-results-count { + + .searchBox-recommendations-view { + margin-top: 10px; display: flex; - color: gray; - margin-left: 5px; + width: 100%; + height: fit-content; + flex-direction: column; + gap: 10px; + padding: 0px 10px; + + } + } + + .searchBox-results-container { + display: flex; + flex-direction: column; + width: 100%; + height: fit-content; + justify-content: "center"; - .searchBox-results-scroll-view { - margin-top: 10px; + .searchBox-results-view { display: inline-block; width: 100%; - height: calc(100% - 55px); - overflow-y: scroll; + height: fit-content; .searchBox-results-scroll-view-result { display: inline-block; vertical-align: middle; width: 100%; - height: 50px; + height: fit-content; cursor: pointer; font-size: 15px; - padding: 11px; + padding: 10px; &.searchBox-results-scroll-view-result-selected { background: #999; @@ -81,6 +118,8 @@ width: calc(100% - 45px); text-align: left; overflow: hidden; + max-height: 2.4em; + line-height: 1.2em; text-overflow: ellipsis; } diff --git a/src/client/views/search/SearchBox.tsx b/src/client/views/search/SearchBox.tsx index d13c09443..12c25ca09 100644 --- a/src/client/views/search/SearchBox.tsx +++ b/src/client/views/search/SearchBox.tsx @@ -15,6 +15,8 @@ import { CollectionDockingView } from '../collections/CollectionDockingView'; import { ViewBoxBaseComponent } from '../DocComponent'; import { FieldView, FieldViewProps } from '../nodes/FieldView'; import './SearchBox.scss'; +import { fetchRecommendations } from '../newlightbox/utils'; +import { IRecommendation, Recommendation } from '../newlightbox/components'; const DAMPENING_FACTOR = 0.9; const MAX_ITERATIONS = 25; @@ -43,6 +45,7 @@ export class SearchBox extends ViewBoxBaseComponent<SearchBoxProps>() { @observable _searchString = ''; @observable _docTypeString = 'all'; @observable _results: Map<Doc, string[]> = new Map<Doc, string[]>(); + @observable _recommendations: IRecommendation[] = []; @observable _pageRanks: Map<Doc, number> = new Map<Doc, number>(); @observable _linkedDocsOut: Map<Doc, Set<Doc>> = new Map<Doc, Set<Doc>>(); @observable _linkedDocsIn: Map<Doc, Set<Doc>> = new Map<Doc, Set<Doc>>(); @@ -394,6 +397,38 @@ export class SearchBox extends ViewBoxBaseComponent<SearchBoxProps>() { if (query) { this.searchCollection(query); + const response = await fetchRecommendations('', query, [], true) + const recs = response.recommendations + const recommendations:IRecommendation[] = [] + for (const key in recs) { + const title = recs[key].title; + console.log(title); + const url = recs[key].url + const type = recs[key].type + const text = recs[key].text + const transcript = recs[key].transcript + const previewUrl = recs[key].previewUrl + const embedding = recs[key].embedding + const distance = recs[key].distance + const source = recs[key].source + const related_concepts = recs[key].related_concepts + const docId = recs[key].doc_id + recommendations.push({ + title: title, + data: url, + type: type, + text: text, + transcript: transcript, + previewUrl: previewUrl, + embedding: embedding, + distance: Math.round(distance * 100) / 100, + source: source, + related_concepts: related_concepts, + docId: docId + }) + } + const setRecommendations = action(() => this._recommendations = recommendations) + setRecommendations() } }; @@ -488,6 +523,10 @@ export class SearchBox extends ViewBoxBaseComponent<SearchBoxProps>() { } }); + const recommendationsJSX: JSX.Element[] = this._recommendations.map((props) => ( + <Recommendation {...props}/> + )) + return ( <div style={{ pointerEvents: 'all' }} className="searchBox-container"> <div className="searchBox-bar"> @@ -512,10 +551,20 @@ export class SearchBox extends ViewBoxBaseComponent<SearchBoxProps>() { ref={this._inputRef} /> </div> - <div className="searchBox-results-container"> - <div className="searchBox-results-count">{`${validResults}` + ' result' + (validResults === 1 ? '' : 's')}</div> - <div className="searchBox-results-scroll-view">{resultsJSX}</div> - </div> + {resultsJSX.length > 0 && <div className="searchBox-results-container"> + <div className="section-header"> + <div className="section-title">Results</div> + <div className="section-subtitle">{`${validResults}` + ' result' + (validResults === 1 ? '' : 's')}</div> + </div> + <div className="searchBox-results-view">{resultsJSX}</div> + </div>} + {recommendationsJSX.length > 0 && <div className="searchBox-recommendations-container"> + <div className="section-header"> + <div className="section-title">Recommendations</div> + <div className="section-subtitle">{`${validResults}` + ' result' + (validResults === 1 ? '' : 's')}</div> + </div> + <div className="searchBox-recommendations-view">{recommendationsJSX}</div> + </div>} </div> ); } diff --git a/src/client/views/topbar/TopBar.tsx b/src/client/views/topbar/TopBar.tsx index 20cf563c1..1d9c25e3c 100644 --- a/src/client/views/topbar/TopBar.tsx +++ b/src/client/views/topbar/TopBar.tsx @@ -1,7 +1,7 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { Tooltip } from '@mui/material'; -import { Button, FontSize, IconButton, Size } from 'browndash-components'; +import { Button, IconButton, FontSize, Size, Type } from 'browndash-components'; import { action, computed, observable, reaction } from 'mobx'; +import { Tooltip } from '@mui/material'; import { observer } from 'mobx-react'; import * as React from 'react'; import { FaBug, FaCamera, FaStamp } from 'react-icons/fa'; @@ -98,6 +98,7 @@ export class TopBar extends React.Component { tooltip="Browsing mode for directly navigating to documents" size={Size.SMALL} color={'white'} + style={{fontWeight: 700, fontSize: '1rem'}} onClick={(e: React.MouseEvent) => { const dashView = Doc.ActiveDashboard && DocumentManager.Instance.getDocumentView(Doc.ActiveDashboard); ContextMenu.Instance.addItem({ description: 'Open Dashboard View', event: this.navigateToHome, icon: 'edit' }); @@ -113,13 +114,6 @@ export class TopBar extends React.Component { dashView?.showContextMenu(e.clientX + 20, e.clientY + 30); }} /> - <Button - text={GetEffectiveAcl(Doc.GetProto(Doc.ActiveDashboard)) === AclAdmin ? 'Share' : 'View Original'} - onClick={() => { - SharingManager.Instance.open(undefined, Doc.ActiveDashboard); - }} - size={Size.SMALL} - /> {!Doc.noviceMode && ( <IconButton tooltip="Work on a copy of the dashboard layout" @@ -145,6 +139,14 @@ export class TopBar extends React.Component { @computed get topbarRight() { return ( <div className="topbar-right"> + {Doc.ActiveDashboard && <Button + text={GetEffectiveAcl(Doc.GetProto(Doc.ActiveDashboard)) === AclAdmin ? 'Share' : 'View Original'} + type={Type.TERT} + onClick={() => { + SharingManager.Instance.open(undefined, Doc.ActiveDashboard); + }} + size={Size.SMALL} + />} <IconButton size={Size.SMALL} color={Colors.LIGHT_GRAY} onClick={ServerStats.Instance.open} icon={<FaStamp />} /> <IconButton size={Size.SMALL} color={Colors.LIGHT_GRAY} onClick={ReportManager.Instance.open} icon={<FaBug />} /> <IconButton size={Size.SMALL} color={Colors.LIGHT_GRAY} onClick={() => window.open('https://brown-dash.github.io/Dash-Documentation/', '_blank')} icon={<FontAwesomeIcon icon="question-circle" />} /> diff --git a/src/server/server_Initialization.ts b/src/server/server_Initialization.ts index 805da1d43..15088ddb2 100644 --- a/src/server/server_Initialization.ts +++ b/src/server/server_Initialization.ts @@ -34,7 +34,7 @@ const compiler = webpack(config); export type RouteSetter = (server: RouteManager) => void; //export let disconnect: Function; -export let resolvedPorts: { server: number; socket: number } = { server: 1050, socket: 4321 }; +export let resolvedPorts: { server: number; socket: number } = { server: 3000, socket: 4321 }; export let resolvedServerUrl: string; export default async function InitializeServer(routeSetter: RouteSetter) { |