diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/client/apis/gpt/GPT.ts | 36 | ||||
-rw-r--r-- | src/client/views/PropertiesView.tsx | 45 | ||||
-rw-r--r-- | src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx | 32 | ||||
-rw-r--r-- | src/client/views/nodes/formattedText/FormattedTextBox.tsx | 1 | ||||
-rw-r--r-- | src/client/views/nodes/trails/PresBox.scss | 25 | ||||
-rw-r--r-- | src/client/views/nodes/trails/PresBox.tsx | 132 | ||||
-rw-r--r-- | src/client/views/nodes/trails/PresElementBox.tsx | 37 | ||||
-rw-r--r-- | src/client/views/pdf/GPTPopup/GPTPopup.scss | 4 | ||||
-rw-r--r-- | src/client/views/pdf/GPTPopup/GPTPopup.tsx | 4 |
9 files changed, 285 insertions, 31 deletions
diff --git a/src/client/apis/gpt/GPT.ts b/src/client/apis/gpt/GPT.ts index 6bde7989b..66689f2f6 100644 --- a/src/client/apis/gpt/GPT.ts +++ b/src/client/apis/gpt/GPT.ts @@ -19,6 +19,32 @@ const callTypeMap: { [type: string]: GPTCallOpts } = { completion: { model: 'text-davinci-003', maxTokens: 256, temp: 0.5, prompt: '' }, }; +const configuration = new Configuration({ + apiKey: process.env.OPENAI_KEY, +}); +const openai = new OpenAIApi(configuration); + +const gptTrailSlideCustomization = async (inputText: string) => { + const prompt = + 'We are adding customization to a slide in a presentation. Given a natural language input, translate it into a json with the required fields: [title, presentation_transition, presentation_effect, and config_zoom]. title is the title/name of the slide. presentation_transition is a number in milliseconds for how long it should take to transition to a slide. presentation_effect is an effect applied to the slide when we transition to it. Its only possible values are: [None, Fade in, Flip, Rotate, Bounce, Roll]. config zoom is a number from 0 to 1.0 indicating the percentage we should be zooming into the slide. If the input does not contain info a specific key, please set their value to null. Please only return the json with these keys and their values.'; + + try { + const response = await openai.createChatCompletion({ + model: 'gpt-3.5-turbo', + messages: [ + { role: 'system', content: prompt }, + { role: 'user', content: inputText }, + ], + temperature: 0.1, + max_tokens: 1000, + }); + return response.data.choices[0].message?.content; + } catch (err) { + console.log(err); + return 'Error connecting with API.'; + } +}; + /** * Calls the OpenAI API. * @@ -29,10 +55,6 @@ const gptAPICall = async (inputText: string, callType: GPTCallType) => { if (callType === GPTCallType.SUMMARY) inputText += '.'; const opts: GPTCallOpts = callTypeMap[callType]; try { - const configuration = new Configuration({ - apiKey: process.env.OPENAI_KEY, - }); - const openai = new OpenAIApi(configuration); const response = await openai.createCompletion({ model: opts.model, max_tokens: opts.maxTokens, @@ -48,10 +70,6 @@ const gptAPICall = async (inputText: string, callType: GPTCallType) => { const gptImageCall = async (prompt: string, n?: number) => { try { - const configuration = new Configuration({ - apiKey: process.env.OPENAI_KEY, - }); - const openai = new OpenAIApi(configuration); const response = await openai.createImage({ prompt: prompt, n: n ?? 1, @@ -65,4 +83,4 @@ const gptImageCall = async (prompt: string, n?: number) => { } }; -export { gptAPICall, gptImageCall, GPTCallType }; +export { gptAPICall, gptImageCall, gptTrailSlideCustomization, GPTCallType }; diff --git a/src/client/views/PropertiesView.tsx b/src/client/views/PropertiesView.tsx index 04a948a27..984906ed0 100644 --- a/src/client/views/PropertiesView.tsx +++ b/src/client/views/PropertiesView.tsx @@ -3,7 +3,7 @@ import { IconLookup } from '@fortawesome/fontawesome-svg-core'; import { faAnchor, faArrowRight, faWindowMaximize } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Checkbox, Tooltip } from '@material-ui/core'; -import { Colors, EditableText, IconButton, NumberInput, Size, Slider, Type } from 'browndash-components'; +import { ColorPicker, Colors, EditableText, IconButton, NumberInput, Size, Slider, Type } from 'browndash-components'; import { concat } from 'lodash'; import { action, computed, IReactionDisposer, observable, reaction, trace } from 'mobx'; import { observer } from 'mobx-react'; @@ -39,6 +39,7 @@ import { PropertiesDocContextSelector } from './PropertiesDocContextSelector'; import { PropertiesSection } from './PropertiesSection'; import './PropertiesView.scss'; import { DefaultStyleProvider } from './StyleProvider'; +import { FaFillDrip } from 'react-icons/fa'; const _global = (window /* browser */ || global) /* node */ as any; interface PropertiesViewProps { @@ -1250,10 +1251,23 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { 'change link relationship' ); + handleColorChange = undoable( + action((value: string) => { + if (LinkManager.currentLink && this.selectedDoc) { + this.setColorValue(value); + } + }), + 'change link color' + ); + @undoBatch setDescripValue = action((value: string) => { if (LinkManager.currentLink) { Doc.GetProto(LinkManager.currentLink).link_description = value; + + if (LinkManager.currentLink.show_description === undefined) { + LinkManager.currentLink.show_description = !LinkManager.currentLink.show_description; + } } }); @@ -1299,6 +1313,13 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { } }); + @undoBatch + setColorValue = action((value: string) => { + if (LinkManager.currentLink) { + Doc.GetProto(LinkManager.currentLink).link_color = value; + } + }); + changeFollowBehavior = undoable((loc: Opt<string>) => this.sourceAnchor && (this.sourceAnchor.followLinkLocation = loc), 'change follow behavior'); @undoBatch @@ -1467,6 +1488,28 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { <FontAwesomeIcon className="fa-icon" icon={faArrowRight as IconLookup} size="lg" /> </button> </div> + <div className="propertiesView-input inline" style={{ marginLeft: 10 }}> + <p>Show description</p> + <button + style={{ background: !LinkManager.currentLink?.show_description ? '' : '#4476f7', borderRadius: 3 }} + onPointerDown={e => this.toggleLinkProp(e, 'show_description')} + onClick={e => e.stopPropagation()} + className="propertiesButton"> + <FontAwesomeIcon className="fa-icon" icon={faArrowRight as IconLookup} size="lg" /> + </button> + </div> + <div className="propertiesView-input inline" style={{ marginLeft: 10 }}> + <p>Link color</p> + <ColorPicker + tooltip={'User Color'} // + color={SettingsManager.userColor} + type={Type.SEC} + icon={<FaFillDrip />} + selectedColor={LinkManager.currentLink?.link_color ? StrCast(LinkManager.currentLink?.link_color) : '#449ef7'} + setSelectedColor={this.handleColorChange} + setFinalColor={this.handleColorChange} + /> + </div> </div> {!hasSelectedAnchor ? null : ( <div className="propertiesView-section"> diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx index 24a758d8c..470ff9527 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx @@ -239,11 +239,11 @@ export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFo textX, textY, // fully connected - // pt1, - // pt2, + // pt1, + // pt2, // this code adds space between links - pt1: link.link_displayArrow ? [pt1[0] + pt1normalized[0] * 3*NumCast(link.link_displayArrow_scale, 4), pt1[1] + pt1normalized[1] * 3*NumCast(link.link_displayArrow_scale, 3)] : pt1, - pt2: link.link_displayArrow ? [pt2[0] + pt2normalized[0] * 3*NumCast(link.link_displayArrow_scale, 4), pt2[1] + pt2normalized[1] * 3*NumCast(link.link_displayArrow_scale, 3)] : pt2, + pt1: link.link_displayArrow ? [pt1[0] + pt1normalized[0] * 3 * NumCast(link.link_displayArrow_scale, 4), pt1[1] + pt1normalized[1] * 3 * NumCast(link.link_displayArrow_scale, 3)] : pt1, + pt2: link.link_displayArrow ? [pt2[0] + pt2normalized[0] * 3 * NumCast(link.link_displayArrow_scale, 4), pt2[1] + pt2normalized[1] * 3 * NumCast(link.link_displayArrow_scale, 3)] : pt2, }; } @@ -262,19 +262,20 @@ export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFo const linkSize = Doc.noviceMode || currRelationshipIndex === -1 || currRelationshipIndex >= linkRelationshipSizes.length ? -1 : linkRelationshipSizes[currRelationshipIndex]; //access stroke color using index of the relationship in the color list (default black) - const stroke = currRelationshipIndex === -1 || currRelationshipIndex >= linkColorList.length ? StrCast(link._backgroundColor, 'black') : linkColorList[currRelationshipIndex]; + // const stroke = currRelationshipIndex === -1 || currRelationshipIndex >= linkColorList.length ? StrCast(link._backgroundColor, 'black') : linkColorList[currRelationshipIndex]; + const stroke = link.link_color ? Field.toString(link.link_color as any as Field) : '#449ef7'; // const hexStroke = this.rgbToHex(stroke) //calculate stroke width/thickness based on the relative importance of the relationshipship (i.e. how many links the relationship has) //thickness varies linearly from 3px to 12px for increasing link count const strokeWidth = linkSize === -1 ? '3px' : Math.floor(2 + 10 * (linkSize / Math.max(...linkRelationshipSizes))) + 'px'; - const arrowScale = NumCast(link.link_displayArrow_scale, 3) + const arrowScale = NumCast(link.link_displayArrow_scale, 3); return link.opacity === 0 || !a.width || !b.width || (!(Doc.UserDoc().showLinkLines || link.link_displayLine) && !aActive && !bActive) ? null : ( <> <defs> - <marker id={`${link[Id] + 'arrowhead'}`} markerWidth={`${4*arrowScale}`} markerHeight={`${3*arrowScale}`} refX="0" refY={`${1.5*arrowScale}`} orient="auto"> - <polygon points={`0 0, ${3*arrowScale} ${1.5*arrowScale}, 0 ${3*arrowScale}`} fill={stroke} /> + <marker id={`${link[Id] + 'arrowhead'}`} markerWidth={`${4 * arrowScale}`} markerHeight={`${3 * arrowScale}`} refX="0" refY={`${1.5 * arrowScale}`} orient="auto"> + <polygon points={`0 0, ${3 * arrowScale} ${1.5 * arrowScale}, 0 ${3 * arrowScale}`} fill={stroke} /> </marker> <filter id="outline"> <feMorphology in="SourceAlpha" result="expanded" operator="dilate" radius="1" /> @@ -301,13 +302,14 @@ export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFo d={`M ${pt1[0]} ${pt1[1]} C ${pt1[0] + pt1norm[0]} ${pt1[1] + pt1norm[1]}, ${pt2[0] + pt2norm[0]} ${pt2[1] + pt2norm[1]}, ${pt2[0]} ${pt2[1]}`} markerEnd={link.link_displayArrow ? `url(#${link[Id] + 'arrowhead'})` : ''} /> - {textX === undefined || !linkDescription ? null : ( - <text filter={`url(#${link[Id] + 'background'})`} className="collectionfreeformlinkview-linkText" x={textX} y={textY} onPointerDown={this.pointerDown}> - <tspan> </tspan> - <tspan dy="2">{linkDescription.substring(0, 50) + (linkDescription.length > 50 ? '...' : '')}</tspan> - <tspan dy="2"> </tspan> - </text> - )} + {link.show_description && + (textX === undefined || !linkDescription ? null : ( + <text filter={`url(#${link[Id] + 'background'})`} className="collectionfreeformlinkview-linkText" x={textX} y={textY} onPointerDown={this.pointerDown}> + <tspan> </tspan> + <tspan dy="2">{linkDescription.substring(0, 50) + (linkDescription.length > 50 ? '...' : '')}</tspan> + <tspan dy="2"> </tspan> + </text> + ))} </> ); } diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx index fcdfbdf2a..46cd6db8f 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx +++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx @@ -966,6 +966,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps }; breakupDictation = () => { + console.log('breakup'); if (this._editorView && this._recordingDictation) { this.stopDictation(true); this._break = true; diff --git a/src/client/views/nodes/trails/PresBox.scss b/src/client/views/nodes/trails/PresBox.scss index 0b51813a5..370b155fd 100644 --- a/src/client/views/nodes/trails/PresBox.scss +++ b/src/client/views/nodes/trails/PresBox.scss @@ -15,6 +15,31 @@ //overflow: hidden; transition: 0.7s opacity ease; + .presBox-chatbox { + position: fixed; + bottom: 8px; + left: 8px; + width: calc(100% - 16px); + min-height: 100px; + border-radius: 16px; + padding: 16px; + gap: 8px; + z-index: 999; + display: flex; + flex-direction: column; + justify-content: space-between; + background-color: #ffffff; + box-shadow: 0 2px 5px #7474748d; + + .pres-chatbox { + outline: none; + border: none; + font-size: 16px; + letter-spacing: 1px; + color: #404040; + } + } + .presBox-listCont { position: relative; height: calc(100% - 67px); diff --git a/src/client/views/nodes/trails/PresBox.tsx b/src/client/views/nodes/trails/PresBox.tsx index 383b400c8..051a1fbfd 100644 --- a/src/client/views/nodes/trails/PresBox.tsx +++ b/src/client/views/nodes/trails/PresBox.tsx @@ -37,7 +37,12 @@ import { FieldView, FieldViewProps } from '../FieldView'; import { ScriptingBox } from '../ScriptingBox'; import './PresBox.scss'; import { PresEffect, PresEffectDirection, PresMovement, PresStatus } from './PresEnums'; -const { Howl } = require('howler'); +import { IconButton, Type } from 'browndash-components'; +import { AiOutlineSend } from 'react-icons/ai'; +import { gptTrailSlideCustomization } from '../../../apis/gpt/GPT'; +import { DictationManager } from '../../../util/DictationManager'; +import { BiMicrophone, BiX } from 'react-icons/bi'; +import TextareaAutosize from 'react-textarea-autosize'; export interface pinDataTypes { scrollable?: boolean; @@ -105,7 +110,30 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { @observable _treeViewMap: Map<Doc, number> = new Map(); @observable _presKeyEvents: boolean = false; @observable _forceKeyEvents: boolean = false; - @computed get isTreeOrStack() { + + // GPT + @observable chatActive: boolean = false; + @observable chatInput: string = ''; + public slideToModify: Doc | null = null; + @observable isRecording: boolean = false; + + @action + setChatInput = (input: string) => { + this.chatInput = input; + }; + + @action + public setChatActive = (active: boolean) => { + this.chatActive = active; + }; + + @action + public setIsRecording = (isRecording: boolean) => { + this.isRecording = isRecording; + }; + + @computed + get isTreeOrStack() { return [CollectionViewType.Tree, CollectionViewType.Stacking].includes(StrCast(this.layoutDoc._type_collection) as any); } @computed get isTree() { @@ -241,6 +269,46 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { } }; + // GPT + + recordDictation = () => { + DictationManager.Controls.listen({ + interimHandler: this.setDictationContent, + continuous: { indefinite: false }, + }).then(results => { + if (results && [DictationManager.Controls.Infringed].includes(results)) { + DictationManager.Controls.stop(); + } + }); + }; + stopDictation = (abort: boolean) => DictationManager.Controls.stop(!abort); + + setDictationContent = (value: string) => { + this.setChatInput(value); + }; + + @action + customizeWithGPT = async (input: string) => { + // const testInput = 'change title to Customized Slide, transition for 2.3s with fade in effect'; + if (!this.slideToModify) return; + + try { + const res = await gptTrailSlideCustomization(input); + if (typeof res === 'string') { + const resObj = JSON.parse(res); + console.log('Result ', resObj); + // this.activeItem + for (let key in resObj) { + if (resObj[key]) { + this.slideToModify[key] = resObj[key]; + } + } + } + } catch (err) { + console.error(err); + } + }; + //TODO: al: it seems currently that tempMedia doesn't stop onslidechange after clicking the button; the time the tempmedia stop depends on the start & end time // TODO: to handle child slides (entering into subtrail and exiting), also the next() and back() functions // No more frames in current doc and next slide is defined, therefore move to next slide @@ -2661,7 +2729,6 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { /> ) : null} </div> - {/* { // if the document type is a presentation, then the collection stacking view has a "+ new slide" button at the bottom of the stack <Tooltip title={<div className="dash-tooltip">{'Click on document to pin to presentaiton or make a marquee selection to pin your desired view'}</div>}> @@ -2671,6 +2738,65 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { </Tooltip> } */} </div> + {/* presbox chatbox */} + {this.chatActive && ( + <div className="presBox-chatbox"> + <div style={{ alignSelf: 'flex-end' }}> + <IconButton + type={Type.PRIM} + color={StrCast(Doc.UserDoc().userVariantColor)} + tooltip="Close" + icon={<BiX size={'16px'} />} + onClick={() => { + this.setChatActive(false); + }} + /> + </div> + <TextareaAutosize + minRows={3} + placeholder="Customize..." + className="pres-chatbox" + autoFocus={true} + value={this.chatInput} + onChange={e => { + this.setChatInput(e.target.value); + }} + onSelect={e => { + this.stopDictation(true); + }} + onKeyDown={e => e.stopPropagation()} + onKeyPress={e => e.stopPropagation()} + onPointerDown={e => e.stopPropagation()} + onClick={e => e.stopPropagation()} + onPointerUp={e => e.stopPropagation()} + /> + {/* <input className="chatbox" placeholder="Customize..." value={this.chatInput} onChange={e => this.setChatInput(e.target.value)} /> */} + <div style={{ alignSelf: 'flex-end', display: 'flex', gap: '8px' }}> + <IconButton + type={Type.TERT} + color={this.isRecording ? 'red' : StrCast(Doc.UserDoc().userVariantColor)} + tooltip="Record" + icon={<BiMicrophone size={'16px'} />} + onClick={() => { + if (!this.isRecording) { + this.recordDictation(); + } else { + this.stopDictation(true); + } + }} + /> + <IconButton + type={Type.TERT} + color={StrCast(Doc.UserDoc().userVariantColor)} + tooltip="Send" + icon={<AiOutlineSend size={'16px'} />} + onClick={() => { + this.customizeWithGPT(this.chatInput); + }} + /> + </div> + </div> + )} </div> ); } diff --git a/src/client/views/nodes/trails/PresElementBox.tsx b/src/client/views/nodes/trails/PresElementBox.tsx index 82ed9e8d5..482d38f59 100644 --- a/src/client/views/nodes/trails/PresElementBox.tsx +++ b/src/client/views/nodes/trails/PresElementBox.tsx @@ -28,6 +28,8 @@ import React = require('react'); import { TreeView } from '../../collections/TreeView'; import { BranchingTrailManager } from '../../../util/BranchingTrailManager'; import { MultiToggle, Type } from 'browndash-components'; +import { gptTrailSlideCustomization } from '../../../apis/gpt/GPT'; +import { DictationManager } from '../../../util/DictationManager'; /** * This class models the view a document added to presentation will have in the presentation. * It involves some functionality for its buttons and options. @@ -434,6 +436,27 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { return width; } + // GPT + + @action + customizeWithGPT = async (input: string) => { + const testInput = 'change title to Customized Slide, transition for 2.3s with fade in effect'; + + try { + const res = await gptTrailSlideCustomization(testInput); + if (typeof res === 'string') { + const resObj = JSON.parse(res); + console.log('Result ', resObj); + + for (let key in resObj) { + this.rootDoc[key] = resObj[key]; + } + } + } catch (err) { + console.error(err); + } + }; + @computed get presButtons() { const presBox = this.presBox; const presBoxColor = StrCast(presBox?._backgroundColor); @@ -537,6 +560,20 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { </div> </Tooltip> ); + items.push( + <Tooltip key="customize" title={<div className="dash-tooltip">Customize</div>}> + <div + className={'slideButton'} + onClick={() => { + PresBox.Instance.setChatActive(true); + PresBox.Instance.slideToModify = this.rootDoc; + PresBox.Instance.recordDictation(); + // this.customizeWithGPT(''); + }}> + <FontAwesomeIcon icon={'message'} onPointerDown={e => e.stopPropagation()} /> + </div> + </Tooltip> + ); return items; } diff --git a/src/client/views/pdf/GPTPopup/GPTPopup.scss b/src/client/views/pdf/GPTPopup/GPTPopup.scss index 5d966395c..48659d0e7 100644 --- a/src/client/views/pdf/GPTPopup/GPTPopup.scss +++ b/src/client/views/pdf/GPTPopup/GPTPopup.scss @@ -11,8 +11,8 @@ $highlightedText: #82e0ff; right: 10px; width: 250px; min-height: 200px; - border-radius: 15px; - padding: 15px; + border-radius: 16px; + padding: 16px; padding-bottom: 0; z-index: 999; display: flex; diff --git a/src/client/views/pdf/GPTPopup/GPTPopup.tsx b/src/client/views/pdf/GPTPopup/GPTPopup.tsx index 038c45582..9b754588a 100644 --- a/src/client/views/pdf/GPTPopup/GPTPopup.tsx +++ b/src/client/views/pdf/GPTPopup/GPTPopup.tsx @@ -118,13 +118,15 @@ export class GPTPopup extends React.Component<GPTPopupProps> { try { let image_urls = await gptImageCall(this.imgDesc); if (image_urls && image_urls[0]) { + // need to fix this const [result] = await Networking.PostToServer('/uploadRemoteImage', { sources: [image_urls[0]] }); + console.log('Result', result); + console.log('Client', result.accessPaths.agnostic.client); const source = Utils.prepend(result.accessPaths.agnostic.client); this.setImgUrls([[image_urls[0], source]]); } } catch (err) { console.log(err); - return ''; } GPTPopup.Instance.setLoading(false); }; |