diff options
Diffstat (limited to 'src/client/apis')
-rw-r--r-- | src/client/apis/GoogleAuthenticationManager.tsx | 119 | ||||
-rw-r--r-- | src/client/apis/google_docs/GoogleApiClientUtils.ts | 164 | ||||
-rw-r--r-- | src/client/apis/google_docs/GooglePhotosClientUtils.ts | 91 | ||||
-rw-r--r-- | src/client/apis/gpt/GPT.ts | 18 | ||||
-rw-r--r-- | src/client/apis/youtube/YoutubeBox.scss | 126 | ||||
-rw-r--r-- | src/client/apis/youtube/YoutubeBox.tsx | 369 |
6 files changed, 196 insertions, 691 deletions
diff --git a/src/client/apis/GoogleAuthenticationManager.tsx b/src/client/apis/GoogleAuthenticationManager.tsx index 855f48f7e..5269f763b 100644 --- a/src/client/apis/GoogleAuthenticationManager.tsx +++ b/src/client/apis/GoogleAuthenticationManager.tsx @@ -1,14 +1,14 @@ -import { action, IReactionDisposer, observable, reaction, runInAction } from "mobx"; -import { observer } from "mobx-react"; -import * as React from "react"; -import { Opt } from "../../fields/Doc"; -import { Networking } from "../Network"; -import { ScriptingGlobals } from "../util/ScriptingGlobals"; -import { MainViewModal } from "../views/MainViewModal"; -import "./GoogleAuthenticationManager.scss"; +import { action, IReactionDisposer, observable, reaction, runInAction } from 'mobx'; +import { observer } from 'mobx-react'; +import * as React from 'react'; +import { Opt } from '../../fields/Doc'; +import { Networking } from '../Network'; +import { ScriptingGlobals } from '../util/ScriptingGlobals'; +import { MainViewModal } from '../views/MainViewModal'; +import './GoogleAuthenticationManager.scss'; -const AuthenticationUrl = "https://accounts.google.com/o/oauth2/v2/auth"; -const prompt = "Paste authorization code here..."; +const AuthenticationUrl = 'https://accounts.google.com/o/oauth2/v2/auth'; +const prompt = 'Paste authorization code here...'; @observer export class GoogleAuthenticationManager extends React.Component<{}> { @@ -23,11 +23,11 @@ export class GoogleAuthenticationManager extends React.Component<{}> { private disposer: Opt<IReactionDisposer>; private set isOpen(value: boolean) { - runInAction(() => this.openState = value); + runInAction(() => (this.openState = value)); } private set shouldShowPasteTarget(value: boolean) { - runInAction(() => this.showPasteTargetState = value); + runInAction(() => (this.showPasteTargetState = value)); } public cancel() { @@ -35,7 +35,7 @@ export class GoogleAuthenticationManager extends React.Component<{}> { } public fetchOrGenerateAccessToken = async (displayIfFound = false) => { - let response: any = await Networking.FetchFromServer("/readGoogleAccessToken"); + let response: any = await Networking.FetchFromServer('/readGoogleAccessToken'); // if this is an authentication url, activate the UI to register the new access token if (new RegExp(AuthenticationUrl).test(response)) { this.isOpen = true; @@ -47,7 +47,7 @@ export class GoogleAuthenticationManager extends React.Component<{}> { async authenticationCode => { if (authenticationCode && /\d{1}\/[\w-]{55}/.test(authenticationCode)) { this.disposer?.(); - const response = await Networking.PostToServer("/writeGoogleAccessToken", { authenticationCode }); + const response = await Networking.PostToServer('/writeGoogleAccessToken', { authenticationCode }); runInAction(() => { this.success = true; this.credentials = response; @@ -71,7 +71,7 @@ export class GoogleAuthenticationManager extends React.Component<{}> { this.isOpen = true; } return response.access_token; - } + }; resetState = action((visibleForMS: number = 3000, fadesOutInMS: number = 500) => { if (!visibleForMS && !fadesOutInMS) { @@ -89,14 +89,20 @@ export class GoogleAuthenticationManager extends React.Component<{}> { this.displayLauncher = false; this.shouldShowPasteTarget = false; if (visibleForMS > 0 && fadesOutInMS > 0) { - setTimeout(action(() => { - this.isOpen = false; - setTimeout(action(() => { - this.success = undefined; - this.displayLauncher = true; - this.credentials = undefined; - }), fadesOutInMS); - }), visibleForMS); + setTimeout( + action(() => { + this.isOpen = false; + setTimeout( + action(() => { + this.success = undefined; + this.displayLauncher = true; + this.credentials = undefined; + }), + fadesOutInMS + ); + }), + visibleForMS + ); } }); @@ -108,61 +114,44 @@ export class GoogleAuthenticationManager extends React.Component<{}> { private get renderPrompt() { return ( <div className={'authorize-container'}> - - {this.displayLauncher ? <button - className={"dispatch"} - onClick={() => { - window.open(this.authenticationLink); - setTimeout(() => this.shouldShowPasteTarget = true, 500); - }} - style={{ marginBottom: this.showPasteTargetState ? 15 : 0 }} - >Authorize a Google account...</button> : (null)} - {this.showPasteTargetState ? <input - className={'paste-target'} - onChange={action(e => this.authenticationCode = e.currentTarget.value)} - placeholder={prompt} - /> : (null)} - {this.credentials ? + {this.displayLauncher ? ( + <button + className={'dispatch'} + onClick={() => { + window.open(this.authenticationLink); + setTimeout(() => (this.shouldShowPasteTarget = true), 500); + }} + style={{ marginBottom: this.showPasteTargetState ? 15 : 0 }}> + Authorize a Google account... + </button> + ) : null} + {this.showPasteTargetState ? <input className={'paste-target'} onChange={action(e => (this.authenticationCode = e.currentTarget.value))} placeholder={prompt} /> : null} + {this.credentials ? ( <> - <img - className={'avatar'} - src={this.credentials.userInfo.picture} - /> - <span - className={'welcome'} - >Welcome to Dash, {this.credentials.userInfo.name} - </span> + <img className={'avatar'} src={this.credentials.userInfo.picture} /> + <span className={'welcome'}>Welcome to Dash, {this.credentials.userInfo.name}</span> <div className={'disconnect'} onClick={async () => { - await Networking.FetchFromServer("/revokeGoogleAccessToken"); + await Networking.FetchFromServer('/revokeGoogleAccessToken'); this.resetState(0, 0); - }} - >Disconnect Account</div> - </> : (null)} + }}> + Disconnect Account + </div> + </> + ) : null} </div> ); } private get dialogueBoxStyle() { - const borderColor = this.success === undefined ? "black" : this.success ? "green" : "red"; - return { borderColor, transition: "0.2s borderColor ease", zIndex: 1002 }; + const borderColor = this.success === undefined ? 'black' : this.success ? 'green' : 'red'; + return { borderColor, transition: '0.2s borderColor ease', zIndex: 1002 }; } render() { - return ( - <MainViewModal - isDisplayed={this.openState} - interactive={true} - contents={this.renderPrompt} - // overlayDisplayedOpacity={0.9} - dialogueBoxStyle={this.dialogueBoxStyle} - overlayStyle={{ zIndex: 1001 }} - closeOnExternalClick={action(() => this.isOpen = false)} - /> - ); + return <MainViewModal isDisplayed={this.openState} interactive={true} contents={this.renderPrompt} dialogueBoxStyle={this.dialogueBoxStyle} overlayStyle={{ zIndex: 1001 }} closeOnExternalClick={action(() => (this.isOpen = false))} />; } - } -ScriptingGlobals.add("GoogleAuthenticationManager", GoogleAuthenticationManager);
\ No newline at end of file +ScriptingGlobals.add('GoogleAuthenticationManager', GoogleAuthenticationManager); diff --git a/src/client/apis/google_docs/GoogleApiClientUtils.ts b/src/client/apis/google_docs/GoogleApiClientUtils.ts index c8f381cc0..0b303eacf 100644 --- a/src/client/apis/google_docs/GoogleApiClientUtils.ts +++ b/src/client/apis/google_docs/GoogleApiClientUtils.ts @@ -1,45 +1,46 @@ -import { docs_v1 } from "googleapis"; -import { Opt } from "../../../fields/Doc"; -import { isArray } from "util"; -import { EditorState } from "prosemirror-state"; -import { Networking } from "../../Network"; +/* eslint-disable no-restricted-syntax */ +/* eslint-disable no-use-before-define */ +import { docs_v1 as docsV1 } from 'googleapis'; +// eslint-disable-next-line node/no-deprecated-api +import { isArray } from 'util'; +import { EditorState } from 'prosemirror-state'; +import { Opt } from '../../../fields/Doc'; +import { Networking } from '../../Network'; -export const Pulls = "googleDocsPullCount"; -export const Pushes = "googleDocsPushCount"; +export const Pulls = 'googleDocsPullCount'; +export const Pushes = 'googleDocsPushCount'; export namespace GoogleApiClientUtils { - export enum Actions { - Create = "create", - Retrieve = "retrieve", - Update = "update" + Create = 'create', + Retrieve = 'retrieve', + Update = 'update', } export namespace Docs { - - export type RetrievalResult = Opt<docs_v1.Schema$Document>; - export type UpdateResult = Opt<docs_v1.Schema$BatchUpdateDocumentResponse>; + export type RetrievalResult = Opt<docsV1.Schema$Document>; + export type UpdateResult = Opt<docsV1.Schema$BatchUpdateDocumentResponse>; export interface UpdateOptions { documentId: DocumentId; - requests: docs_v1.Schema$Request[]; + requests: docsV1.Schema$Request[]; } export enum WriteMode { Insert, - Replace + Replace, } export type DocumentId = string; export type Reference = DocumentId | CreateOptions; export interface Content { text: string | string[]; - requests: docs_v1.Schema$Request[]; + requests: docsV1.Schema$Request[]; } export type IdHandler = (id: DocumentId) => any; export type CreationResult = Opt<DocumentId>; - export type ReadLinesResult = Opt<{ title?: string, bodyLines?: string[] }>; - export type ReadResult = { title: string, body: string }; + export type ReadLinesResult = Opt<{ title?: string; bodyLines?: string[] }>; + export type ReadResult = { title: string; body: string }; export interface ImportResult { title: string; text: string; @@ -67,23 +68,23 @@ export namespace GoogleApiClientUtils { } /** - * After following the authentication routine, which connects this API call to the current signed in account - * and grants the appropriate permissions, this function programmatically creates an arbitrary Google Doc which - * should appear in the user's Google Doc library instantaneously. - * - * @param options the title to assign to the new document, and the information necessary - * to store the new documentId returned from the creation process - * @returns the documentId of the newly generated document, or undefined if the creation process fails. - */ + * After following the authentication routine, which connects this API call to the current signed in account + * and grants the appropriate permissions, this function programmatically creates an arbitrary Google Doc which + * should appear in the user's Google Doc library instantaneously. + * + * @param options the title to assign to the new document, and the information necessary + * to store the new documentId returned from the creation process + * @returns the documentId of the newly generated document, or undefined if the creation process fails. + */ export const create = async (options: CreateOptions): Promise<CreationResult> => { const path = `/googleDocs/Documents/${Actions.Create}`; const parameters = { requestBody: { - title: options.title || `Dash Export (${new Date().toDateString()})` - } + title: options.title || `Dash Export (${new Date().toDateString()})`, + }, }; try { - const schema: docs_v1.Schema$Document = await Networking.PostToServer(path, parameters); + const schema: docsV1.Schema$Document = await Networking.PostToServer(path, parameters); return schema.documentId === null ? undefined : schema.documentId; } catch { return undefined; @@ -91,19 +92,25 @@ export namespace GoogleApiClientUtils { }; export namespace Utils { - - export type ExtractResult = { text: string, paragraphs: DeconstructedParagraph[] }; - export const extractText = (document: docs_v1.Schema$Document, removeNewlines = false): ExtractResult => { + export type ExtractResult = { text: string; paragraphs: DeconstructedParagraph[] }; + export const extractText = (document: docsV1.Schema$Document, removeNewlines = false): ExtractResult => { const paragraphs = extractParagraphs(document); - let text = paragraphs.map(paragraph => paragraph.contents.filter(content => !("inlineObjectId" in content)).map(run => (run as docs_v1.Schema$TextRun).content).join("")).join(""); + let text = paragraphs + .map(paragraph => + paragraph.contents + .filter(content => !('inlineObjectId' in content)) + .map(run => (run as docsV1.Schema$TextRun).content) + .join('') + ) + .join(''); text = text.substring(0, text.length - 1); - removeNewlines && text.replace(/\n/g, ""); + removeNewlines && text.replace(/\n/g, ''); return { text, paragraphs }; }; - export type ContentArray = (docs_v1.Schema$TextRun | docs_v1.Schema$InlineObjectElement)[]; - export type DeconstructedParagraph = { contents: ContentArray, bullet: Opt<number> }; - const extractParagraphs = (document: docs_v1.Schema$Document, filterEmpty = true): DeconstructedParagraph[] => { + export type ContentArray = (docsV1.Schema$TextRun | docsV1.Schema$InlineObjectElement)[]; + export type DeconstructedParagraph = { contents: ContentArray; bullet: Opt<number> }; + const extractParagraphs = (document: docsV1.Schema$Document, filterEmpty = true): DeconstructedParagraph[] => { const fragments: DeconstructedParagraph[] = []; if (document.body && document.body.content) { for (const element of document.body.content) { @@ -132,7 +139,7 @@ export namespace GoogleApiClientUtils { return fragments; }; - export const endOf = (schema: docs_v1.Schema$Document): number | undefined => { + export const endOf = (schema: docsV1.Schema$Document): number | undefined => { if (schema.body && schema.body.content) { const paragraphs = schema.body.content.filter(el => el.paragraph); if (paragraphs.length) { @@ -146,10 +153,10 @@ export namespace GoogleApiClientUtils { } } } + return undefined; }; - export const initialize = async (reference: Reference) => typeof reference === "string" ? reference : create(reference); - + export const initialize = async (reference: Reference) => (typeof reference === 'string' ? reference : create(reference)); } export const retrieve = async (options: RetrieveOptions): Promise<RetrievalResult> => { @@ -168,8 +175,8 @@ export namespace GoogleApiClientUtils { const parameters = { documentId: options.documentId, requestBody: { - requests: options.requests - } + requests: options.requests, + }, }; try { const replies: UpdateResult = await Networking.PostToServer(path, parameters); @@ -179,83 +186,84 @@ export namespace GoogleApiClientUtils { } }; - export const read = async (options: ReadOptions): Promise<Opt<ReadResult>> => { - return retrieve({ documentId: options.documentId }).then(document => { + export const read = async (options: ReadOptions): Promise<Opt<ReadResult>> => + retrieve({ documentId: options.documentId }).then(document => { if (document) { const title = document.title!; const body = Utils.extractText(document, options.removeNewlines).text; return { title, body }; } + return undefined; }); - }; - export const readLines = async (options: ReadOptions): Promise<Opt<ReadLinesResult>> => { - return retrieve({ documentId: options.documentId }).then(document => { + export const readLines = async (options: ReadOptions): Promise<Opt<ReadLinesResult>> => + retrieve({ documentId: options.documentId }).then(document => { if (document) { - const title = document.title; - let bodyLines = Utils.extractText(document).text.split("\n"); + const { title } = document; + let bodyLines = Utils.extractText(document).text.split('\n'); options.removeNewlines && (bodyLines = bodyLines.filter(line => line.length)); - return { title: title ?? "", bodyLines }; + return { title: title ?? '', bodyLines }; } + return undefined; }); - }; export const setStyle = async (options: UpdateOptions) => { const replies: any = await update({ documentId: options.documentId, - requests: options.requests + requests: options.requests, }); - if ("errors" in replies) { - console.log("Write operation failed:"); + if ('errors' in replies) { + console.log('Write operation failed:'); console.log(replies.errors.map((error: any) => error.message)); } return replies; }; export const write = async (options: WriteOptions): Promise<UpdateResult> => { - const requests: docs_v1.Schema$Request[] = []; + const requests: docsV1.Schema$Request[] = []; const documentId = await Utils.initialize(options.reference); if (!documentId) { return undefined; } - let index = options.index; - const mode = options.mode; + let { index } = options; + const { mode } = options; if (!(index && mode === WriteMode.Insert)) { const schema = await retrieve({ documentId }); + // eslint-disable-next-line no-cond-assign if (!schema || !(index = Utils.endOf(schema))) { return undefined; } } if (mode === WriteMode.Replace) { - index > 1 && requests.push({ - deleteContentRange: { - range: { - startIndex: 1, - endIndex: index - } - } - }); + index > 1 && + requests.push({ + deleteContentRange: { + range: { + startIndex: 1, + endIndex: index, + }, + }, + }); index = 1; } - const text = options.content.text; - text.length && requests.push({ - insertText: { - text: isArray(text) ? text.join("\n") : text, - location: { index } - } - }); + const { text } = options.content; + text.length && + requests.push({ + insertText: { + text: isArray(text) ? text.join('\n') : text, + location: { index }, + }, + }); if (!requests.length) { return undefined; } requests.push(...options.content.requests); const replies: any = await update({ documentId, requests }); - if ("errors" in replies) { - console.log("Write operation failed:"); + if ('errors' in replies) { + console.log('Write operation failed:'); console.log(replies.errors.map((error: any) => error.message)); } return replies; }; - } - -}
\ No newline at end of file +} diff --git a/src/client/apis/google_docs/GooglePhotosClientUtils.ts b/src/client/apis/google_docs/GooglePhotosClientUtils.ts index e8fd8fb8a..fdc185a8e 100644 --- a/src/client/apis/google_docs/GooglePhotosClientUtils.ts +++ b/src/client/apis/google_docs/GooglePhotosClientUtils.ts @@ -1,18 +1,19 @@ +/* eslint-disable no-use-before-define */ +import Photos = require('googlephotos'); import { AssertionError } from 'assert'; import { EditorState } from 'prosemirror-state'; +import { ClientUtils } from '../../../ClientUtils'; import { Doc, DocListCastAsync, Opt } from '../../../fields/Doc'; import { Id } from '../../../fields/FieldSymbols'; import { RichTextField } from '../../../fields/RichTextField'; import { RichTextUtils } from '../../../fields/RichTextUtils'; -import { Cast, StrCast } from '../../../fields/Types'; -import { ImageField } from '../../../fields/URLField'; +import { Cast, ImageCast, StrCast } from '../../../fields/Types'; import { MediaItem, NewMediaItemResult } from '../../../server/apis/google/SharedTypes'; -import { Utils } from '../../../Utils'; -import { Docs, DocumentOptions, DocUtils } from '../../documents/Documents'; import { Networking } from '../../Network'; +import { Docs, DocumentOptions } from '../../documents/Documents'; +import { DocUtils } from '../../documents/DocUtils'; import { FormattedTextBox } from '../../views/nodes/formattedText/FormattedTextBox'; import { GoogleAuthenticationManager } from '../GoogleAuthenticationManager'; -import Photos = require('googlephotos'); export namespace GooglePhotos { const endpoint = async () => new Photos(await GoogleAuthenticationManager.Instance.fetchOrGenerateAccessToken()); @@ -76,17 +77,16 @@ export namespace GooglePhotos { export const CollectionToAlbum = async (options: AlbumCreationOptions): Promise<Opt<AlbumCreationResult>> => { const { collection, title, descriptionKey, tag } = options; const dataDocument = Doc.GetProto(collection); - const images = ((await DocListCastAsync(dataDocument.data)) || []).filter(doc => Cast(doc.data, ImageField)); + const images = ((await DocListCastAsync(dataDocument.data)) || []).filter(doc => ImageCast(doc.data)); if (!images || !images.length) { return undefined; } - const resolved = title ? title : StrCast(collection.title) || `Dash Collection (${collection[Id]}`; + const resolved = title || StrCast(collection.title) || `Dash Collection (${collection[Id]}`; const { id, productUrl } = await Create.Album(resolved); const response = await Transactions.UploadImages(images, { id }, descriptionKey); if (response) { const { results, failed } = response; - let index: Opt<number>; - while ((index = failed.pop()) !== undefined) { + for (let index = failed.pop(); index !== undefined; index = failed.pop()) { Doc.RemoveDocFromList(dataDocument, 'data', images.splice(index, 1)[0]); } const mediaItems: MediaItem[] = results.map(item => item.mediaItem); @@ -97,13 +97,12 @@ export namespace GooglePhotos { for (let i = 0; i < images.length; i++) { const image = Doc.GetProto(images[i]); const mediaItem = mediaItems[i]; - if (!mediaItem) { - continue; + if (mediaItem) { + image.googlePhotosId = mediaItem.id; + image.googlePhotosAlbumUrl = productUrl; + image.googlePhotosUrl = mediaItem.productUrl || mediaItem.baseUrl; + idMapping[mediaItem.id] = image; } - image.googlePhotosId = mediaItem.id; - image.googlePhotosAlbumUrl = productUrl; - image.googlePhotosUrl = mediaItem.productUrl || mediaItem.baseUrl; - idMapping[mediaItem.id] = image; } collection.googlePhotosAlbumUrl = productUrl; collection.googlePhotosIdMapping = idMapping; @@ -111,9 +110,10 @@ export namespace GooglePhotos { await Query.TagChildImages(collection); } collection.albumId = id; - Transactions.AddTextEnrichment(collection, `Find me at ${Utils.prepend(`/doc/${collection[Id]}?sharing=true`)}`); + Transactions.AddTextEnrichment(collection, `Find me at ${ClientUtils.prepend(`/doc/${collection[Id]}?sharing=true`)}`); return { albumId: id, mediaItems }; } + return undefined; }; } @@ -124,7 +124,7 @@ export namespace GooglePhotos { await GoogleAuthenticationManager.Instance.fetchOrGenerateAccessToken(); const response = await Query.ContentSearch(requested); const uploads = await Transactions.WriteMediaItemsToServer(response); - const children = uploads.map((upload: Transactions.UploadInformation) => Docs.Create.ImageDocument(Utils.fileUrl(upload.fileNames.clean) /*, {"data_contentSize":upload.contentSize}*/)); + const children = uploads.map((upload: Transactions.UploadInformation) => Docs.Create.ImageDocument(ClientUtils.fileUrl(upload.fileNames.clean) /* , {"data_contentSize":upload.contentSize} */)); const options = { _width: 500, _height: 500 }; return constructor(children, options); }; @@ -144,7 +144,7 @@ export namespace GooglePhotos { const images = (await DocListCastAsync(collection.data))!.map(Doc.GetProto); images?.forEach(image => tagMapping.set(image[Id], ContentCategories.NONE)); const values = Object.values(ContentCategories).filter(value => value !== ContentCategories.NONE); - for (const value of values) { + values.forEach(async value => { const searched = (await ContentSearch({ included: [value] }))?.mediaItems?.map(({ id }) => id); searched?.forEach(async id => { const image = await Cast(idMapping[id], Doc); @@ -154,7 +154,7 @@ export namespace GooglePhotos { !tags?.includes(value) && tagMapping.set(key, tags + delimiter + value); } }); - } + }); images?.forEach(image => { const concatenated = tagMapping.get(image[Id])!; const tags = concatenated.split(delimiter); @@ -200,9 +200,10 @@ export namespace GooglePhotos { export const AlbumSearch = async (albumId: string, pageSize = 100): Promise<MediaItem[]> => { const photos = await endpoint(); const mediaItems: MediaItem[] = []; - let nextPageTokenStored: Opt<string> = undefined; + let nextPageTokenStored: Opt<string>; const found = 0; do { + // eslint-disable-next-line no-await-in-loop const response: any = await photos.mediaItems.search(albumId, pageSize, nextPageTokenStored); mediaItems.push(...response.mediaItems); nextPageTokenStored = response.nextPageToken; @@ -222,7 +223,7 @@ export namespace GooglePhotos { excluded.length && excluded.forEach(category => contentFilter.addExcludedContentCategories(category)); filters.setContentFilter(contentFilter); - const date = options.date; + const { date } = options; if (date) { const dateFilter = new photos.DateFilter(); if (date instanceof Date) { @@ -240,15 +241,11 @@ export namespace GooglePhotos { }); }; - export const GetImage = async (mediaItemId: string): Promise<Transactions.MediaItem> => { - return (await endpoint()).mediaItems.get(mediaItemId); - }; + export const GetImage = async (mediaItemId: string): Promise<Transactions.MediaItem> => (await endpoint()).mediaItems.get(mediaItemId); } namespace Create { - export const Album = async (title: string) => { - return (await endpoint()).albums.create(title); - }; + export const Album = async (title: string) => (await endpoint()).albums.create(title); } export namespace Transactions { @@ -278,6 +275,7 @@ export namespace GooglePhotos { return enrichmentItem.id; } } + return undefined; }; export const WriteMediaItemsToServer = async (body: { mediaItems: any[] }): Promise<UploadInformation[]> => { @@ -291,9 +289,12 @@ export namespace GooglePhotos { return undefined; } const baseUrls: string[] = await Promise.all( - response.results.map(item => { - return new Promise<string>(resolve => Query.GetImage(item.mediaItem.id).then(item => resolve(item.baseUrl))); - }) + response.results.map( + item => + new Promise<string>(resolve => { + Query.GetImage(item.mediaItem.id).then(itm => resolve(itm.baseUrl)); + }) + ) ); return baseUrls; }; @@ -303,31 +304,29 @@ export namespace GooglePhotos { failed: number[]; } - export const UploadImages = async (sources: Doc[], album?: AlbumReference, descriptionKey = 'caption'): Promise<Opt<ImageUploadResults>> => { + export const UploadImages = async (sources: Doc[], albumIn?: AlbumReference, descriptionKey = 'caption'): Promise<Opt<ImageUploadResults>> => { await GoogleAuthenticationManager.Instance.fetchOrGenerateAccessToken(); - if (album && 'title' in album) { - album = await Create.Album(album.title); - } + const album = albumIn && 'title' in albumIn ? await Create.Album(albumIn.title) : albumIn; const media: MediaInput[] = []; - for (const source of sources) { - const data = Cast(Doc.GetProto(source).data, ImageField); - if (!data) { - return; - } - const url = data.url.href; - const target = Doc.MakeEmbedding(source); - const description = parseDescription(target, descriptionKey); - await DocUtils.makeCustomViewClicked(target, Docs.Create.FreeformDocument); - media.push({ url, description }); - } + sources + .filter(source => ImageCast(Doc.GetProto(source).data)) + .forEach(async source => { + const data = ImageCast(Doc.GetProto(source).data); + const url = data.url.href; + const target = Doc.MakeEmbedding(source); + const description = parseDescription(target, descriptionKey); + await DocUtils.makeCustomViewClicked(target, Docs.Create.FreeformDocument); + media.push({ url, description }); + }); if (media.length) { const results = await Networking.PostToServer('/googlePhotosMediaPost', { media, album }); return results; } + return undefined; }; const parseDescription = (document: Doc, descriptionKey: string) => { - let description: string = Utils.prepend(`/doc/${document[Id]}?sharing=true`); + let description: string = ClientUtils.prepend(`/doc/${document[Id]}?sharing=true`); const target = document[descriptionKey]; if (typeof target === 'string') { description = target; diff --git a/src/client/apis/gpt/GPT.ts b/src/client/apis/gpt/GPT.ts index 30194f9f8..8f58ec364 100644 --- a/src/client/apis/gpt/GPT.ts +++ b/src/client/apis/gpt/GPT.ts @@ -27,24 +27,28 @@ const callTypeMap: { [type: string]: GPTCallOpts } = { data: { model: 'gpt-3.5-turbo', maxTokens: 256, temp: 0.5, prompt: "You are a helpful resarch assistant. Analyze the user's data to find meaningful patterns and/or correlation. Please keep your response short and to the point." }, }; +let lastCall = ''; +let lastResp = ''; /** * Calls the OpenAI API. * * @param inputText Text to process * @returns AI Output */ -const gptAPICall = async (inputText: string, callType: GPTCallType, prompt?: any) => { - if (callType === GPTCallType.SUMMARY) inputText += '.'; +const gptAPICall = async (inputTextIn: string, callType: GPTCallType, prompt?: any) => { + const inputText = callType === GPTCallType.SUMMARY ? inputTextIn + '.' : inputTextIn; const opts: GPTCallOpts = callTypeMap[callType]; + if (lastCall === inputText) return lastResp; try { const configuration: ClientOptions = { apiKey: process.env.OPENAI_KEY, dangerouslyAllowBrowser: true, }; + lastCall = inputText; const openai = new OpenAI(configuration); - let usePrompt = prompt ? opts.prompt + prompt : opts.prompt; - let messages: ChatCompletionMessageParam[] = [ + const usePrompt = prompt ? opts.prompt + prompt : opts.prompt; + const messages: ChatCompletionMessageParam[] = [ { role: 'system', content: usePrompt }, { role: 'user', content: inputText }, ]; @@ -55,8 +59,8 @@ const gptAPICall = async (inputText: string, callType: GPTCallType, prompt?: any temperature: opts.temp, messages, }); - const content = response.choices[0].message.content; - return content; + lastResp = response.choices[0].message.content ?? ''; + return lastResp; } catch (err) { console.log(err); return 'Error connecting with API.'; @@ -79,8 +83,8 @@ const gptImageCall = async (prompt: string, n?: number) => { return response.data.map(data => data.url); } catch (err) { console.error(err); - return; } + return undefined; }; export { gptAPICall, gptImageCall, GPTCallType }; diff --git a/src/client/apis/youtube/YoutubeBox.scss b/src/client/apis/youtube/YoutubeBox.scss deleted file mode 100644 index eabdbb1ac..000000000 --- a/src/client/apis/youtube/YoutubeBox.scss +++ /dev/null @@ -1,126 +0,0 @@ -.youtubeBox-cont { - ul { - list-style-type: none; - padding-inline-start: 10px; - } - - - li { - margin: 4px; - display: inline-flex; - } - - li:hover { - cursor: pointer; - opacity: 0.8; - } - - .search_wrapper { - width: 100%; - display: inline-flex; - height: 175px; - - .video_duration { - // margin: 0; - // padding: 0; - border: 0; - background: transparent; - display: inline-block; - position: relative; - bottom: 25px; - left: 85%; - margin: 4px; - color: #FFFFFF; - background-color: rgba(0, 0, 0, 0.80); - padding: 2px 4px; - border-radius: 2px; - letter-spacing: .5px; - font-size: 1.2rem; - font-weight: 500; - line-height: 1.2rem; - - } - - .textual_info { - font-family: Arial, Helvetica, sans-serif; - - .videoTitle { - margin-left: 4px; - // display: inline-block; - color: #0D0D0D; - -webkit-line-clamp: 2; - display: block; - max-height: 4.8rem; - overflow: hidden; - font-size: 1.8rem; - font-weight: 400; - line-height: 2.4rem; - -webkit-box-orient: vertical; - text-overflow: ellipsis; - white-space: normal; - display: -webkit-box; - } - - .channelName { - color: #606060; - margin-left: 4px; - font-size: 1.3rem; - font-weight: 400; - line-height: 1.8rem; - text-transform: none; - margin-top: 0px; - display: inline-block; - } - - .video_description { - margin-left: 4px; - // font-size: 12px; - color: #606060; - padding-top: 8px; - margin-bottom: 8px; - display: block; - line-height: 1.8rem; - max-height: 4.2rem; - overflow: hidden; - font-size: 1.3rem; - font-weight: 400; - text-transform: none; - } - - .publish_time { - //display: inline-block; - margin-left: 8px; - padding: 0; - border: 0; - background: transparent; - color: #606060; - max-width: 100%; - line-height: 1.8rem; - max-height: 3.6rem; - overflow: hidden; - font-size: 1.3rem; - font-weight: 400; - text-transform: none; - } - - .viewCount { - - margin-left: 8px; - padding: 0; - border: 0; - background: transparent; - color: #606060; - max-width: 100%; - line-height: 1.8rem; - max-height: 3.6rem; - overflow: hidden; - font-size: 1.3rem; - font-weight: 400; - text-transform: none; - } - - - - } - } -}
\ No newline at end of file diff --git a/src/client/apis/youtube/YoutubeBox.tsx b/src/client/apis/youtube/YoutubeBox.tsx deleted file mode 100644 index d3a15cd84..000000000 --- a/src/client/apis/youtube/YoutubeBox.tsx +++ /dev/null @@ -1,369 +0,0 @@ -import { action, observable, runInAction } from 'mobx'; -import { observer } from 'mobx-react'; -import { Doc, DocListCastAsync } from '../../../fields/Doc'; -import { InkTool } from '../../../fields/InkField'; -import { Cast, NumCast, StrCast } from '../../../fields/Types'; -import { Utils } from '../../../Utils'; -import { DocServer } from '../../DocServer'; -import { Docs } from '../../documents/Documents'; -import { DocumentView } from '../../views/nodes/DocumentView'; -import { FieldView, FieldViewProps } from '../../views/nodes/FieldView'; -import '../../views/nodes/WebBox.scss'; -import './YoutubeBox.scss'; -import * as React from 'react'; -import { SnappingManager } from '../../util/SnappingManager'; - -interface VideoTemplate { - thumbnailUrl: string; - videoTitle: string; - videoId: string; - duration: string; - channelTitle: string; - viewCount: string; - publishDate: string; - videoDescription: string; -} - -/** - * This class models the youtube search document that can be dropped on to canvas. - */ -@observer -export class YoutubeBox extends React.Component<FieldViewProps> { - @observable YoutubeSearchElement: HTMLInputElement | undefined; - @observable searchResultsFound: boolean = false; - @observable searchResults: any[] = []; - @observable videoClicked: boolean = false; - @observable selectedVideoUrl: string = ''; - @observable lisOfBackUp: JSX.Element[] = []; - @observable videoIds: string | undefined; - @observable videoDetails: any[] = []; - @observable curVideoTemplates: VideoTemplate[] = []; - - public static LayoutString(fieldKey: string) { - return FieldView.LayoutString(YoutubeBox, fieldKey); - } - - /** - * When component mounts, last search's results are laoded in based on the back up stored - * in the document of the props. - */ - async componentDidMount() { - //DocServer.getYoutubeChannels(); - const castedSearchBackUp = Cast(this.props.Document.cachedSearchResults, Doc); - const awaitedBackUp = await castedSearchBackUp; - const castedDetailBackUp = Cast(this.props.Document.cachedDetails, Doc); - const awaitedDetails = await castedDetailBackUp; - - if (awaitedBackUp) { - const jsonList = await DocListCastAsync(awaitedBackUp.json); - const jsonDetailList = await DocListCastAsync(awaitedDetails!.json); - - if (jsonList!.length !== 0) { - runInAction(() => (this.searchResultsFound = true)); - let index = 0; - //getting the necessary information from backUps and building templates that will be used to map in render - for (const video of jsonList!) { - const videoId = await Cast(video.id, Doc); - const id = StrCast(videoId!.videoId); - const snippet = await Cast(video.snippet, Doc); - const videoTitle = this.filterYoutubeTitleResult(StrCast(snippet!.title)); - const thumbnail = await Cast(snippet!.thumbnails, Doc); - const thumbnailMedium = await Cast(thumbnail!.medium, Doc); - const thumbnailUrl = StrCast(thumbnailMedium!.url); - const videoDescription = StrCast(snippet!.description); - const pusblishDate = this.roundPublishTime(StrCast(snippet!.publishedAt))!; - const channelTitle = StrCast(snippet!.channelTitle); - let duration: string = ''; - let viewCount: string = ''; - if (jsonDetailList!.length !== 0) { - const contentDetails = await Cast(jsonDetailList![index].contentDetails, Doc); - const statistics = await Cast(jsonDetailList![index].statistics, Doc); - duration = this.convertIsoTimeToDuration(StrCast(contentDetails!.duration)); - viewCount = this.abbreviateViewCount(parseInt(StrCast(statistics!.viewCount)))!; - } - index = index + 1; - const newTemplate: VideoTemplate = { - videoId: id, - videoTitle: videoTitle, - thumbnailUrl: thumbnailUrl, - publishDate: pusblishDate, - channelTitle: channelTitle, - videoDescription: videoDescription, - duration: duration, - viewCount: viewCount, - }; - runInAction(() => this.curVideoTemplates.push(newTemplate)); - } - } - } - } - - _ignore = 0; - onPreWheel = (e: React.WheelEvent) => { - this._ignore = e.timeStamp; - }; - onPrePointer = (e: React.PointerEvent) => { - this._ignore = e.timeStamp; - }; - onPostPointer = (e: React.PointerEvent) => { - if (this._ignore !== e.timeStamp) { - e.stopPropagation(); - } - }; - onPostWheel = (e: React.WheelEvent) => { - if (this._ignore !== e.timeStamp) { - e.stopPropagation(); - } - }; - - /** - * Function that submits the title entered by user on enter press. - */ - onEnterKeyDown = (e: React.KeyboardEvent) => { - if (e.keyCode === 13) { - const submittedTitle = this.YoutubeSearchElement!.value; - this.YoutubeSearchElement!.value = ''; - this.YoutubeSearchElement!.blur(); - DocServer.getYoutubeVideos(submittedTitle, this.processesVideoResults); - } - }; - - /** - * The callback that is passed in to server, which functions as a way to - * get videos that is returned by search. It also makes a call to server - * to get details for the videos found. - */ - @action - processesVideoResults = (videos: any[]) => { - this.searchResults = videos; - if (this.searchResults.length > 0) { - this.searchResultsFound = true; - this.videoIds = ''; - videos.forEach(video => { - if (this.videoIds === '') { - this.videoIds = video.id.videoId; - } else { - this.videoIds = this.videoIds! + ', ' + video.id.videoId; - } - }); - //Asking for details that include duration and viewCount from server for videoIds - DocServer.getYoutubeVideoDetails(this.videoIds, this.processVideoDetails); - this.backUpSearchResults(videos); - if (this.videoClicked) { - this.videoClicked = false; - } - } - }; - - /** - * The callback that is given to server to process and receive returned details about the videos. - */ - @action - processVideoDetails = (videoDetails: any[]) => { - this.videoDetails = videoDetails; - this.props.Document.cachedDetails = Doc.Get.FromJson({ data: videoDetails, title: 'detailBackUp' }); - }; - - /** - * The function that stores the search results in the props document. - */ - backUpSearchResults = (videos: any[]) => { - this.props.Document.cachedSearchResults = Doc.Get.FromJson({ data: videos, title: 'videosBackUp' }); - }; - - /** - * The function that filters out escaped characters returned by the api - * in the title of the videos. - */ - filterYoutubeTitleResult = (resultTitle: string) => { - let processedTitle: string = resultTitle.replace(/&/g, '&'); //.ReplaceAll("&", "&"); - processedTitle = processedTitle.replace(/"'/g, "'"); - processedTitle = processedTitle.replace(/"/g, '"'); - return processedTitle; - }; - - /** - * The function that converts ISO date, which is passed in, to normal date and finds the - * difference between today's date and that date, in terms of "ago" to imitate youtube. - */ - roundPublishTime = (publishTime: string) => { - const date = new Date(publishTime).getTime(); - const curDate = new Date().getTime(); - const timeDif = curDate - date; - const totalSeconds = timeDif / 1000; - const totalMin = totalSeconds / 60; - const totalHours = totalMin / 60; - const totalDays = totalHours / 24; - const totalMonths = totalDays / 30.417; - const totalYears = totalMonths / 12; - - const truncYears = Math.trunc(totalYears); - const truncMonths = Math.trunc(totalMonths); - const truncDays = Math.trunc(totalDays); - const truncHours = Math.trunc(totalHours); - const truncMin = Math.trunc(totalMin); - const truncSec = Math.trunc(totalSeconds); - - let pluralCase = ''; - - if (truncYears !== 0) { - truncYears > 1 ? (pluralCase = 's') : (pluralCase = ''); - return truncYears + ' year' + pluralCase + ' ago'; - } else if (truncMonths !== 0) { - truncMonths > 1 ? (pluralCase = 's') : (pluralCase = ''); - return truncMonths + ' month' + pluralCase + ' ago'; - } else if (truncDays !== 0) { - truncDays > 1 ? (pluralCase = 's') : (pluralCase = ''); - return truncDays + ' day' + pluralCase + ' ago'; - } else if (truncHours !== 0) { - truncHours > 1 ? (pluralCase = 's') : (pluralCase = ''); - return truncHours + ' hour' + pluralCase + ' ago'; - } else if (truncMin !== 0) { - truncMin > 1 ? (pluralCase = 's') : (pluralCase = ''); - return truncMin + ' minute' + pluralCase + ' ago'; - } else if (truncSec !== 0) { - truncSec > 1 ? (pluralCase = 's') : (pluralCase = ''); - return truncSec + ' second' + pluralCase + ' ago'; - } - }; - - /** - * The function that converts the passed in ISO time to normal duration time. - */ - convertIsoTimeToDuration = (isoDur: string) => { - const convertedTime = isoDur.replace(/D|H|M/g, ':').replace(/P|T|S/g, '').split(':'); - - if (1 === convertedTime.length) { - 2 !== convertedTime[0].length && (convertedTime[0] = '0' + convertedTime[0]), (convertedTime[0] = '0:' + convertedTime[0]); - } else { - for (var r = 1, l = convertedTime.length - 1; l >= r; r++) { - 2 !== convertedTime[r].length && (convertedTime[r] = '0' + convertedTime[r]); - } - } - - return convertedTime.join(':'); - }; - - /** - * The function that rounds the viewCount to the nearest - * thousand, million or billion, given a viewCount number. - */ - abbreviateViewCount = (viewCount: number) => { - if (viewCount < 1000) { - return viewCount.toString(); - } else if (viewCount >= 1000 && viewCount < 1000000) { - return Math.trunc(viewCount / 1000) + 'K'; - } else if (viewCount >= 1000000 && viewCount < 1000000000) { - return Math.trunc(viewCount / 1000000) + 'M'; - } else if (viewCount >= 1000000000) { - return Math.trunc(viewCount / 1000000000) + 'B'; - } - }; - - /** - * The function that is called to decide on what'll be rendered by the component. - * It renders search Results if found. If user didn't do a new search, it renders from the videoTemplates - * generated by the backUps. If none present, renders nothing. - */ - renderSearchResultsOrVideo = () => { - if (this.searchResultsFound) { - if (this.searchResults.length !== 0) { - return ( - <ul> - {this.searchResults.map((video, index) => { - const filteredTitle = this.filterYoutubeTitleResult(video.snippet.title); - const channelTitle = video.snippet.channelTitle; - const videoDescription = video.snippet.description; - const pusblishDate = this.roundPublishTime(video.snippet.publishedAt); - let duration; - let viewCount; - if (this.videoDetails.length !== 0) { - duration = this.convertIsoTimeToDuration(this.videoDetails[index].contentDetails.duration); - viewCount = this.abbreviateViewCount(this.videoDetails[index].statistics.viewCount); - } - - return ( - <li onClick={() => this.embedVideoOnClick(video.id.videoId, filteredTitle)} key={Utils.GenerateGuid()}> - <div className="search_wrapper"> - <div style={{ backgroundColor: 'yellow' }}> - <img src={video.snippet.thumbnails.medium.url} /> - <span className="video_duration">{duration}</span> - </div> - <div className="textual_info"> - <span className="videoTitle">{filteredTitle}</span> - <span className="channelName">{channelTitle}</span> - <span className="viewCount">{viewCount}</span> - <span className="publish_time">{pusblishDate}</span> - <p className="video_description">{videoDescription}</p> - </div> - </div> - </li> - ); - })} - </ul> - ); - } else if (this.curVideoTemplates.length !== 0) { - return ( - <ul> - {this.curVideoTemplates.map((video: VideoTemplate) => { - return ( - <li onClick={() => this.embedVideoOnClick(video.videoId, video.videoTitle)} key={Utils.GenerateGuid()}> - <div className="search_wrapper"> - <div style={{ backgroundColor: 'yellow' }}> - <img src={video.thumbnailUrl} /> - <span className="video_duration">{video.duration}</span> - </div> - <div className="textual_info"> - <span className="videoTitle">{video.videoTitle}</span> - <span className="channelName">{video.channelTitle}</span> - <span className="viewCount">{video.viewCount}</span> - <span className="publish_time">{video.publishDate}</span> - <p className="video_description">{video.videoDescription}</p> - </div> - </div> - </li> - ); - })} - </ul> - ); - } - } else { - return null; - } - }; - - /** - * Given a videoId and title, creates a new youtube embedded url, and uses that - * to create a new video document. - */ - @action - embedVideoOnClick = (videoId: string, filteredTitle: string) => { - const embeddedUrl = 'https://www.youtube.com/embed/' + videoId; - this.selectedVideoUrl = embeddedUrl; - const addFunction = this.props.addDocument!; - const newVideoX = NumCast(this.props.Document.x); - const newVideoY = NumCast(this.props.Document.y) + NumCast(this.props.Document.height); - - addFunction(Docs.Create.VideoDocument(embeddedUrl, { title: filteredTitle, _width: 400, _height: 315, x: newVideoX, y: newVideoY })); - this.videoClicked = true; - }; - - render() { - const content = ( - <div className="youtubeBox-cont" style={{ width: '100%', height: '100%', position: 'absolute' }} onWheel={this.onPostWheel} onPointerDown={this.onPostPointer} onPointerMove={this.onPostPointer} onPointerUp={this.onPostPointer}> - <input type="text" placeholder="Search for a video" onKeyDown={this.onEnterKeyDown} style={{ height: 40, width: '100%', border: '1px solid black', padding: 5, textAlign: 'center' }} ref={e => (this.YoutubeSearchElement = e!)} /> - {this.renderSearchResultsOrVideo()} - </div> - ); - - const frozen = !this.props.isSelected() || SnappingManager.IsResizing; - - const classname = 'webBox-cont' + (this.props.isSelected() && Doc.ActiveTool === InkTool.None && !SnappingManager.IsResizing ? '-interactive' : ''); - return ( - <> - <div className={classname}>{content}</div> - {!frozen ? null : <div className="webBox-overlay" onWheel={this.onPreWheel} onPointerDown={this.onPrePointer} onPointerMove={this.onPrePointer} onPointerUp={this.onPrePointer} />} - </> - ); - } -} |