From 64df08a9c284a54257b321f4fce42aded2c19f02 Mon Sep 17 00:00:00 2001 From: Ilya Bondar Date: Thu, 25 Jan 2024 16:54:01 +0100 Subject: [PATCH 001/202] fix gradient --- src/styles/globals.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/styles/globals.css b/src/styles/globals.css index c65e7faf7c..4f929fc813 100644 --- a/src/styles/globals.css +++ b/src/styles/globals.css @@ -123,7 +123,7 @@ pre:has(div.codeblock) { } .gradient-top-bottom { - @apply border-transparent bg-gradient-to-b from-transparent via-layer-1 via-30% to-layer-1; + @apply border-transparent bg-gradient-to-b from-transparent via-layer-1 via-[25px] to-layer-1; } } From f9c865e7818b7032a7dec9a62403ae6195872fd8 Mon Sep 17 00:00:00 2001 From: Ilya Bondar Date: Thu, 25 Jan 2024 18:51:07 +0100 Subject: [PATCH 002/202] move `api/files/bucket` -> `api/bucket` --- src/pages/api/{files => }/bucket.ts | 4 ++-- src/utils/app/data/data-service.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) rename src/pages/api/{files => }/bucket.ts (90%) diff --git a/src/pages/api/files/bucket.ts b/src/pages/api/bucket.ts similarity index 90% rename from src/pages/api/files/bucket.ts rename to src/pages/api/bucket.ts index b07135bada..a1cc05fd9a 100644 --- a/src/pages/api/files/bucket.ts +++ b/src/pages/api/bucket.ts @@ -2,14 +2,14 @@ import { NextApiRequest, NextApiResponse } from 'next'; import { getToken } from 'next-auth/jwt'; import { getServerSession } from 'next-auth/next'; -import { getApiHeaders } from '../../../utils/server/get-headers'; import { validateServerSession } from '@/src/utils/auth/session'; import { OpenAIError } from '@/src/utils/server'; +import { getApiHeaders } from '@/src/utils/server/get-headers'; import { logger } from '@/src/utils/server/logger'; import { errorsMessages } from '@/src/constants/errors'; -import { authOptions } from '../auth/[...nextauth]'; +import { authOptions } from '@/src/pages/api/auth/[...nextauth]'; import fetch from 'node-fetch'; diff --git a/src/utils/app/data/data-service.ts b/src/utils/app/data/data-service.ts index 803345c25d..072e414109 100644 --- a/src/utils/app/data/data-service.ts +++ b/src/utils/app/data/data-service.ts @@ -199,7 +199,7 @@ export class DataService { } public static getFilesBucket(): Observable<{ bucket: string }> { - return ApiStorage.request(`api/files/bucket`, { + return ApiStorage.request(`api/bucket`, { method: 'GET', headers: { 'Content-Type': 'application/json', From a21e9bfe667c5fb228fcc3f4d846caeb2feb75e3 Mon Sep 17 00:00:00 2001 From: Ilya Bondar Date: Fri, 26 Jan 2024 09:00:48 +0100 Subject: [PATCH 003/202] API refactoring --- src/components/Files/Download.tsx | 6 +++- .../[type]}/file/[...slug].ts | 2 +- .../api/{files => entities/[type]}/listing.ts | 16 +++++++--- src/store/files/files.epics.ts | 5 ++- src/utils/app/attachments.ts | 4 ++- src/utils/app/data/data-service.ts | 32 +++++++++++++------ src/utils/server/api.ts | 9 ++++++ 7 files changed, 55 insertions(+), 19 deletions(-) rename src/pages/api/{files => entities/[type]}/file/[...slug].ts (98%) rename src/pages/api/{files => entities/[type]}/listing.ts (76%) create mode 100644 src/utils/server/api.ts diff --git a/src/components/Files/Download.tsx b/src/components/Files/Download.tsx index a752ff2014..998cbcf37a 100644 --- a/src/components/Files/Download.tsx +++ b/src/components/Files/Download.tsx @@ -1,4 +1,5 @@ import { constructPath } from '@/src/utils/app/file'; +import { ApiKeys } from '@/src/utils/server/api'; import { DialFile } from '@/src/types/files'; import { CustomTriggerMenuRendererProps } from '@/src/types/menu'; @@ -14,7 +15,10 @@ export default function DownloadRenderer({ return ( diff --git a/src/pages/api/files/file/[...slug].ts b/src/pages/api/entities/[type]/file/[...slug].ts similarity index 98% rename from src/pages/api/files/file/[...slug].ts rename to src/pages/api/entities/[type]/file/[...slug].ts index d04cb5ce12..715ec2b63c 100644 --- a/src/pages/api/files/file/[...slug].ts +++ b/src/pages/api/entities/[type]/file/[...slug].ts @@ -9,7 +9,7 @@ import { logger } from '@/src/utils/server/logger'; import { errorsMessages } from '@/src/constants/errors'; -import { authOptions } from '../../auth/[...nextauth]'; +import { authOptions } from '../../../auth/[...nextauth]'; import fetch from 'node-fetch'; import { Readable } from 'stream'; diff --git a/src/pages/api/files/listing.ts b/src/pages/api/entities/[type]/listing.ts similarity index 76% rename from src/pages/api/files/listing.ts rename to src/pages/api/entities/[type]/listing.ts index 75e65c85a0..c14d88fa10 100644 --- a/src/pages/api/files/listing.ts +++ b/src/pages/api/entities/[type]/listing.ts @@ -2,9 +2,10 @@ import { NextApiRequest, NextApiResponse } from 'next'; import { getToken } from 'next-auth/jwt'; import { getServerSession } from 'next-auth/next'; -import { getApiHeaders } from '../../../utils/server/get-headers'; import { validateServerSession } from '@/src/utils/auth/session'; import { OpenAIError } from '@/src/utils/server'; +import { isValidEntityApiType } from '@/src/utils/server/api'; +import { getApiHeaders } from '@/src/utils/server/get-headers'; import { logger } from '@/src/utils/server/logger'; import { @@ -15,11 +16,16 @@ import { import { errorsMessages } from '@/src/constants/errors'; -import { authOptions } from '../auth/[...nextauth]'; +import { authOptions } from '@/src/pages/api/auth/[...nextauth]'; import fetch from 'node-fetch'; const handler = async (req: NextApiRequest, res: NextApiResponse) => { + const entityType = Array.isArray(req.query.type) ? '' : req.query.type; + if (!entityType || !isValidEntityApiType(entityType)) { + return res.status(500).json(errorsMessages.generalServer); + } + const session = await getServerSession(req, res, authOptions); const isSessionValid = validateServerSession(session, req, res); if (!isSessionValid) { @@ -39,9 +45,9 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => { const token = await getToken({ req }); - const url = `${process.env.DIAL_API_HOST}/v1/metadata/files/${bucket}${ - path && `/${encodeURI(path)}` - }/`; + const url = `${ + process.env.DIAL_API_HOST + }/v1/metadata/${entityType}/${bucket}${path && `/${encodeURI(path)}`}/`; const response = await fetch(url, { headers: getApiHeaders({ jwt: token?.access_token as string }), diff --git a/src/store/files/files.epics.ts b/src/store/files/files.epics.ts index c4bfbfcac2..be73ed2b02 100644 --- a/src/store/files/files.epics.ts +++ b/src/store/files/files.epics.ts @@ -17,6 +17,7 @@ import { combineEpics } from 'redux-observable'; import { DataService } from '@/src/utils/app/data/data-service'; import { triggerDownload } from '@/src/utils/app/file'; import { translate } from '@/src/utils/app/translation'; +import { ApiKeys } from '@/src/utils/server/api'; import { AppEpic } from '@/src/types/store'; @@ -278,7 +279,9 @@ const downloadFilesListEpic: AppEpic = (action$, state$) => tap(({ files }) => { files.forEach((file) => triggerDownload( - `api/files/file/${encodeURI(`${file.absolutePath}/${file.name}`)}`, + `api/entities/${ApiKeys.Files}/file/${encodeURI( + `${file.absolutePath}/${file.name}`, + )}`, file.name, ), ); diff --git a/src/utils/app/attachments.ts b/src/utils/app/attachments.ts index 5564362fa2..37bff99c62 100644 --- a/src/utils/app/attachments.ts +++ b/src/utils/app/attachments.ts @@ -1,12 +1,14 @@ import { Attachment } from '@/src/types/chat'; +import { ApiKeys } from '../server/api'; + export const getMappedAttachmentUrl = (url: string | undefined) => { if (!url) { return undefined; } return url.startsWith('//') || url.startsWith('http') ? url - : `api/files/file/${url}`; + : `api/entities/${ApiKeys.Files}/file/${url}`; }; export const getMappedAttachment = (attachment: Attachment): Attachment => { diff --git a/src/utils/app/data/data-service.ts b/src/utils/app/data/data-service.ts index 072e414109..65b8dd90de 100644 --- a/src/utils/app/data/data-service.ts +++ b/src/utils/app/data/data-service.ts @@ -18,6 +18,7 @@ import { Theme } from '@/src/types/themes'; import { SIDEBAR_MIN_WIDTH } from '@/src/constants/default-ui-settings'; +import { ApiKeys } from '../../server/api'; import { constructPath } from '../file'; import { ApiMockStorage } from './storages/api-mock-storage'; import { ApiStorage } from './storages/api-storage'; @@ -218,7 +219,7 @@ export class DataService { ); return ApiStorage.requestOld({ - url: `api/files/file/${resultPath}`, + url: `api/entities/${ApiKeys.Files}/file/${resultPath}`, method: 'PUT', async: true, body: formData, @@ -276,7 +277,9 @@ export class DataService { }); const resultQuery = query.toString(); - return ApiStorage.request(`api/files/listing?${resultQuery}`).pipe( + return ApiStorage.request( + `api/entities/${ApiKeys.Files}/listing?${resultQuery}`, + ).pipe( map((folders: BackendFileFolder[]) => { return folders.map((folder): FileFolderInterface => { const relativePath = folder.parentPath || undefined; @@ -285,7 +288,7 @@ export class DataService { id: constructPath(relativePath, folder.name), name: folder.name, type: FolderType.File, - absolutePath: constructPath('files', bucket, relativePath), + absolutePath: constructPath(ApiKeys.Files, bucket, relativePath), relativePath: relativePath, folderId: relativePath, serverSynced: true, @@ -298,12 +301,15 @@ export class DataService { public static removeFile(bucket: string, filePath: string): Observable { const resultPath = encodeURI(constructPath('files', bucket, filePath)); - return ApiStorage.request(`api/files/file/${resultPath}`, { - method: 'DELETE', - headers: { - 'Content-Type': 'application/json', + return ApiStorage.request( + `api/entities/${ApiKeys.Files}/file/${resultPath}`, + { + method: 'DELETE', + headers: { + 'Content-Type': 'application/json', + }, }, - }); + ); } public static getFiles( @@ -319,7 +325,9 @@ export class DataService { }); const resultQuery = query.toString(); - return ApiStorage.request(`api/files/listing?${resultQuery}`).pipe( + return ApiStorage.request( + `api/entities/${ApiKeys.Files}/listing?${resultQuery}`, + ).pipe( map((files: BackendFile[]) => { return files.map((file): DialFile => { const relativePath = file.parentPath || undefined; @@ -327,7 +335,11 @@ export class DataService { return { id: constructPath(relativePath, file.name), name: file.name, - absolutePath: constructPath('files', file.bucket, relativePath), + absolutePath: constructPath( + ApiKeys.Files, + file.bucket, + relativePath, + ), relativePath: relativePath, folderId: relativePath, contentLength: file.contentLength, diff --git a/src/utils/server/api.ts b/src/utils/server/api.ts new file mode 100644 index 0000000000..2c02f1f3b2 --- /dev/null +++ b/src/utils/server/api.ts @@ -0,0 +1,9 @@ +export enum ApiKeys { + Files = 'files', + Conversations = 'conversations', + Prompts = 'prompts', +} + +export const isValidEntityApiType = (apiKey: string) => { + return Object.values(ApiKeys).includes(apiKey as ApiKeys); +}; From 2d48586bc4badf4d0d99a8f968241f4bb087ac2e Mon Sep 17 00:00:00 2001 From: Ilya Bondar Date: Fri, 26 Jan 2024 09:06:40 +0100 Subject: [PATCH 004/202] refactoring --- src/pages/api/entities/[type]/file/[...slug].ts | 2 +- src/utils/app/attachments.ts | 4 ++-- src/utils/app/data/data-service.ts | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/pages/api/entities/[type]/file/[...slug].ts b/src/pages/api/entities/[type]/file/[...slug].ts index 715ec2b63c..a309868ac3 100644 --- a/src/pages/api/entities/[type]/file/[...slug].ts +++ b/src/pages/api/entities/[type]/file/[...slug].ts @@ -9,7 +9,7 @@ import { logger } from '@/src/utils/server/logger'; import { errorsMessages } from '@/src/constants/errors'; -import { authOptions } from '../../../auth/[...nextauth]'; +import { authOptions } from '@/src/pages/api/auth/[...nextauth]'; import fetch from 'node-fetch'; import { Readable } from 'stream'; diff --git a/src/utils/app/attachments.ts b/src/utils/app/attachments.ts index 37bff99c62..28065d5510 100644 --- a/src/utils/app/attachments.ts +++ b/src/utils/app/attachments.ts @@ -1,6 +1,6 @@ -import { Attachment } from '@/src/types/chat'; +import { ApiKeys } from '@/src/utils/server/api'; -import { ApiKeys } from '../server/api'; +import { Attachment } from '@/src/types/chat'; export const getMappedAttachmentUrl = (url: string | undefined) => { if (!url) { diff --git a/src/utils/app/data/data-service.ts b/src/utils/app/data/data-service.ts index 65b8dd90de..57f8217aba 100644 --- a/src/utils/app/data/data-service.ts +++ b/src/utils/app/data/data-service.ts @@ -2,6 +2,7 @@ import { Observable, map } from 'rxjs'; import { isSmallScreen } from '@/src/utils/app/mobile'; +import { ApiKeys } from '@/src/utils/server/api'; import { Conversation } from '@/src/types/chat'; import { @@ -18,7 +19,6 @@ import { Theme } from '@/src/types/themes'; import { SIDEBAR_MIN_WIDTH } from '@/src/constants/default-ui-settings'; -import { ApiKeys } from '../../server/api'; import { constructPath } from '../file'; import { ApiMockStorage } from './storages/api-mock-storage'; import { ApiStorage } from './storages/api-storage'; From a5ab094c124a10c881902dcb24a028773baebea0 Mon Sep 17 00:00:00 2001 From: Aliaksandr Kezik Date: Fri, 26 Jan 2024 09:34:11 +0100 Subject: [PATCH 005/202] url generating to getUrlFromSlugs method --- .../api/entities/[type]/file/[...slug].ts | 40 +++++++------------ 1 file changed, 15 insertions(+), 25 deletions(-) diff --git a/src/pages/api/entities/[type]/file/[...slug].ts b/src/pages/api/entities/[type]/file/[...slug].ts index a309868ac3..3cfebe03d9 100644 --- a/src/pages/api/entities/[type]/file/[...slug].ts +++ b/src/pages/api/entities/[type]/file/[...slug].ts @@ -14,6 +14,18 @@ import { authOptions } from '@/src/pages/api/auth/[...nextauth]'; import fetch from 'node-fetch'; import { Readable } from 'stream'; +const getUrlFromSlugs = (req: NextApiRequest) => { + const slugs = Array.isArray(req.query.slug) + ? req.query.slug + : [req.query.slug]; + + if (!slugs || slugs.length === 0) { + throw new OpenAIError('No file path provided', '', '', '400'); + } + + return `${process.env.DIAL_API_HOST}/v1/${encodeURI(slugs.join('/'))}`; +} + const handler = async (req: NextApiRequest, res: NextApiResponse) => { const session = await getServerSession(req, res, authOptions); const isSessionValid = validateServerSession(session, req, res); @@ -57,14 +69,7 @@ async function handlePutRequest( res: NextApiResponse, ) { const readable = Readable.from(req); - const slugs = Array.isArray(req.query.slug) - ? req.query.slug - : [req.query.slug]; - - if (!slugs || slugs.length === 0) { - throw new OpenAIError('No file path provided', '', '', '400'); - } - const url = `${process.env.DIAL_API_HOST}/v1/${encodeURI(slugs.join('/'))}`; + const url = getUrlFromSlugs(req); const proxyRes = await fetch(url, { method: 'PUT', headers: { @@ -92,14 +97,7 @@ async function handleGetRequest( token: JWT | null, res: NextApiResponse, ) { - const slugs = Array.isArray(req.query.slug) - ? req.query.slug - : [req.query.slug]; - - if (!slugs || slugs.length === 0) { - throw new OpenAIError('No file path provided', '', '', '400'); - } - const url = `${process.env.DIAL_API_HOST}/v1/${encodeURI(slugs.join('/'))}`; + const url = getUrlFromSlugs(req); const proxyRes = await fetch(url, { headers: getApiHeaders({ jwt: token?.access_token as string }), }); @@ -123,15 +121,7 @@ async function handleDeleteRequest( token: JWT | null, res: NextApiResponse, ) { - const slugs = Array.isArray(req.query.slug) - ? req.query.slug - : [req.query.slug]; - - if (!slugs || slugs.length === 0) { - throw new OpenAIError('No file path provided', '', '', '400'); - } - const url = `${process.env.DIAL_API_HOST}/v1/${encodeURI(slugs.join('/'))}`; - + const url = getUrlFromSlugs(req); const proxyRes = await fetch(url, { method: 'DELETE', headers: getApiHeaders({ jwt: token?.access_token as string }), From 26626cc9dc9d06a90d24775e8fc259126b299362 Mon Sep 17 00:00:00 2001 From: Ilya Bondar Date: Fri, 26 Jan 2024 09:53:24 +0100 Subject: [PATCH 006/202] remove "entities/" part --- src/components/Files/Download.tsx | 2 +- .../[type] => [entitytype]}/file/[...slug].ts | 0 .../[type] => [entitytype]}/listing.ts | 4 +++- src/store/files/files.epics.ts | 2 +- src/utils/app/attachments.ts | 2 +- src/utils/app/data/data-service.ts | 19 ++++++++----------- 6 files changed, 14 insertions(+), 15 deletions(-) rename src/pages/api/{entities/[type] => [entitytype]}/file/[...slug].ts (100%) rename src/pages/api/{entities/[type] => [entitytype]}/listing.ts (95%) diff --git a/src/components/Files/Download.tsx b/src/components/Files/Download.tsx index 998cbcf37a..38ffb79685 100644 --- a/src/components/Files/Download.tsx +++ b/src/components/Files/Download.tsx @@ -15,7 +15,7 @@ export default function DownloadRenderer({ return ( { - const entityType = Array.isArray(req.query.type) ? '' : req.query.type; + const entityType = Array.isArray(req.query.entitytype) + ? '' + : req.query.entitytype; if (!entityType || !isValidEntityApiType(entityType)) { return res.status(500).json(errorsMessages.generalServer); } diff --git a/src/store/files/files.epics.ts b/src/store/files/files.epics.ts index be73ed2b02..0a22098c7e 100644 --- a/src/store/files/files.epics.ts +++ b/src/store/files/files.epics.ts @@ -279,7 +279,7 @@ const downloadFilesListEpic: AppEpic = (action$, state$) => tap(({ files }) => { files.forEach((file) => triggerDownload( - `api/entities/${ApiKeys.Files}/file/${encodeURI( + `api/${ApiKeys.Files}/file/${encodeURI( `${file.absolutePath}/${file.name}`, )}`, file.name, diff --git a/src/utils/app/attachments.ts b/src/utils/app/attachments.ts index 28065d5510..5ac734bd64 100644 --- a/src/utils/app/attachments.ts +++ b/src/utils/app/attachments.ts @@ -8,7 +8,7 @@ export const getMappedAttachmentUrl = (url: string | undefined) => { } return url.startsWith('//') || url.startsWith('http') ? url - : `api/entities/${ApiKeys.Files}/file/${url}`; + : `api/${ApiKeys.Files}/file/${url}`; }; export const getMappedAttachment = (attachment: Attachment): Attachment => { diff --git a/src/utils/app/data/data-service.ts b/src/utils/app/data/data-service.ts index 57f8217aba..1d0be6ef25 100644 --- a/src/utils/app/data/data-service.ts +++ b/src/utils/app/data/data-service.ts @@ -219,7 +219,7 @@ export class DataService { ); return ApiStorage.requestOld({ - url: `api/entities/${ApiKeys.Files}/file/${resultPath}`, + url: `api/${ApiKeys.Files}/file/${resultPath}`, method: 'PUT', async: true, body: formData, @@ -278,7 +278,7 @@ export class DataService { const resultQuery = query.toString(); return ApiStorage.request( - `api/entities/${ApiKeys.Files}/listing?${resultQuery}`, + `api/${ApiKeys.Files}/listing?${resultQuery}`, ).pipe( map((folders: BackendFileFolder[]) => { return folders.map((folder): FileFolderInterface => { @@ -301,15 +301,12 @@ export class DataService { public static removeFile(bucket: string, filePath: string): Observable { const resultPath = encodeURI(constructPath('files', bucket, filePath)); - return ApiStorage.request( - `api/entities/${ApiKeys.Files}/file/${resultPath}`, - { - method: 'DELETE', - headers: { - 'Content-Type': 'application/json', - }, + return ApiStorage.request(`api/${ApiKeys.Files}/file/${resultPath}`, { + method: 'DELETE', + headers: { + 'Content-Type': 'application/json', }, - ); + }); } public static getFiles( @@ -326,7 +323,7 @@ export class DataService { const resultQuery = query.toString(); return ApiStorage.request( - `api/entities/${ApiKeys.Files}/listing?${resultQuery}`, + `api/${ApiKeys.Files}/listing?${resultQuery}`, ).pipe( map((files: BackendFile[]) => { return files.map((file): DialFile => { From b53d3af4fba5693b1e2162b909c1f54070b4be9c Mon Sep 17 00:00:00 2001 From: Ilya Bondar Date: Fri, 26 Jan 2024 09:59:46 +0100 Subject: [PATCH 007/202] remove "/file/" part --- src/components/Files/Download.tsx | 2 +- src/pages/api/[entitytype]/{file => }/[...slug].ts | 2 +- src/store/files/files.epics.ts | 2 +- src/utils/app/attachments.ts | 2 +- src/utils/app/data/data-service.ts | 4 ++-- 5 files changed, 6 insertions(+), 6 deletions(-) rename src/pages/api/[entitytype]/{file => }/[...slug].ts (99%) diff --git a/src/components/Files/Download.tsx b/src/components/Files/Download.tsx index 38ffb79685..9a5d2e5cbd 100644 --- a/src/components/Files/Download.tsx +++ b/src/components/Files/Download.tsx @@ -15,7 +15,7 @@ export default function DownloadRenderer({ return ( { } return `${process.env.DIAL_API_HOST}/v1/${encodeURI(slugs.join('/'))}`; -} +}; const handler = async (req: NextApiRequest, res: NextApiResponse) => { const session = await getServerSession(req, res, authOptions); diff --git a/src/store/files/files.epics.ts b/src/store/files/files.epics.ts index 0a22098c7e..69321737ce 100644 --- a/src/store/files/files.epics.ts +++ b/src/store/files/files.epics.ts @@ -279,7 +279,7 @@ const downloadFilesListEpic: AppEpic = (action$, state$) => tap(({ files }) => { files.forEach((file) => triggerDownload( - `api/${ApiKeys.Files}/file/${encodeURI( + `api/${ApiKeys.Files}/${encodeURI( `${file.absolutePath}/${file.name}`, )}`, file.name, diff --git a/src/utils/app/attachments.ts b/src/utils/app/attachments.ts index 5ac734bd64..222c6365cf 100644 --- a/src/utils/app/attachments.ts +++ b/src/utils/app/attachments.ts @@ -8,7 +8,7 @@ export const getMappedAttachmentUrl = (url: string | undefined) => { } return url.startsWith('//') || url.startsWith('http') ? url - : `api/${ApiKeys.Files}/file/${url}`; + : `api/${ApiKeys.Files}/${url}`; }; export const getMappedAttachment = (attachment: Attachment): Attachment => { diff --git a/src/utils/app/data/data-service.ts b/src/utils/app/data/data-service.ts index 1d0be6ef25..ec0cde4273 100644 --- a/src/utils/app/data/data-service.ts +++ b/src/utils/app/data/data-service.ts @@ -219,7 +219,7 @@ export class DataService { ); return ApiStorage.requestOld({ - url: `api/${ApiKeys.Files}/file/${resultPath}`, + url: `api/${ApiKeys.Files}/${resultPath}`, method: 'PUT', async: true, body: formData, @@ -301,7 +301,7 @@ export class DataService { public static removeFile(bucket: string, filePath: string): Observable { const resultPath = encodeURI(constructPath('files', bucket, filePath)); - return ApiStorage.request(`api/${ApiKeys.Files}/file/${resultPath}`, { + return ApiStorage.request(`api/${ApiKeys.Files}/${resultPath}`, { method: 'DELETE', headers: { 'Content-Type': 'application/json', From 4c34bc46209e60e5e05e1f0e1bf24d601df552f1 Mon Sep 17 00:00:00 2001 From: Ilya Bondar Date: Fri, 26 Jan 2024 11:06:34 +0100 Subject: [PATCH 008/202] util-methods for url/path --- package-lock.json | 119 ++++++++++++------------ src/pages/api/[entitytype]/[...slug].ts | 26 +++--- src/pages/api/[entitytype]/listing.ts | 9 +- src/utils/server/api.ts | 24 +++++ src/utils/server/index.ts | 15 +-- src/utils/server/types.ts | 13 +++ 6 files changed, 116 insertions(+), 90 deletions(-) create mode 100644 src/utils/server/types.ts diff --git a/package-lock.json b/package-lock.json index d8a1c49d24..1f0a8544ae 100644 --- a/package-lock.json +++ b/package-lock.json @@ -191,9 +191,9 @@ } }, "node_modules/@babel/core/node_modules/@babel/types": { - "version": "7.23.6", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.6.tgz", - "integrity": "sha512-+uarb83brBzPKN38NX1MkB6vb6+mwvR6amUulqAE7ccQw1pEl+bCia9TbdG1lsnFP7lZySvUn37CHyXQdfTwzg==", + "version": "7.23.9", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.9.tgz", + "integrity": "sha512-dQjSq/7HaSjRM43FFGnv5keM2HsxpmyV1PfaSVm0nzzjwwTmjOe6J4bC8e3+pTEIgHaHj+1ZlLThRJ2auc/w1Q==", "dev": true, "dependencies": { "@babel/helper-string-parser": "^7.23.4", @@ -239,9 +239,9 @@ } }, "node_modules/@babel/generator/node_modules/@babel/types": { - "version": "7.23.6", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.6.tgz", - "integrity": "sha512-+uarb83brBzPKN38NX1MkB6vb6+mwvR6amUulqAE7ccQw1pEl+bCia9TbdG1lsnFP7lZySvUn37CHyXQdfTwzg==", + "version": "7.23.9", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.9.tgz", + "integrity": "sha512-dQjSq/7HaSjRM43FFGnv5keM2HsxpmyV1PfaSVm0nzzjwwTmjOe6J4bC8e3+pTEIgHaHj+1ZlLThRJ2auc/w1Q==", "dev": true, "dependencies": { "@babel/helper-string-parser": "^7.23.4", @@ -264,9 +264,9 @@ } }, "node_modules/@babel/helper-annotate-as-pure/node_modules/@babel/types": { - "version": "7.23.6", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.6.tgz", - "integrity": "sha512-+uarb83brBzPKN38NX1MkB6vb6+mwvR6amUulqAE7ccQw1pEl+bCia9TbdG1lsnFP7lZySvUn37CHyXQdfTwzg==", + "version": "7.23.9", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.9.tgz", + "integrity": "sha512-dQjSq/7HaSjRM43FFGnv5keM2HsxpmyV1PfaSVm0nzzjwwTmjOe6J4bC8e3+pTEIgHaHj+1ZlLThRJ2auc/w1Q==", "dev": true, "dependencies": { "@babel/helper-string-parser": "^7.23.4", @@ -289,9 +289,9 @@ } }, "node_modules/@babel/helper-builder-binary-assignment-operator-visitor/node_modules/@babel/types": { - "version": "7.23.6", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.6.tgz", - "integrity": "sha512-+uarb83brBzPKN38NX1MkB6vb6+mwvR6amUulqAE7ccQw1pEl+bCia9TbdG1lsnFP7lZySvUn37CHyXQdfTwzg==", + "version": "7.23.9", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.9.tgz", + "integrity": "sha512-dQjSq/7HaSjRM43FFGnv5keM2HsxpmyV1PfaSVm0nzzjwwTmjOe6J4bC8e3+pTEIgHaHj+1ZlLThRJ2auc/w1Q==", "dev": true, "dependencies": { "@babel/helper-string-parser": "^7.23.4", @@ -476,9 +476,9 @@ } }, "node_modules/@babel/helper-member-expression-to-functions/node_modules/@babel/types": { - "version": "7.23.6", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.6.tgz", - "integrity": "sha512-+uarb83brBzPKN38NX1MkB6vb6+mwvR6amUulqAE7ccQw1pEl+bCia9TbdG1lsnFP7lZySvUn37CHyXQdfTwzg==", + "version": "7.23.9", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.9.tgz", + "integrity": "sha512-dQjSq/7HaSjRM43FFGnv5keM2HsxpmyV1PfaSVm0nzzjwwTmjOe6J4bC8e3+pTEIgHaHj+1ZlLThRJ2auc/w1Q==", "dev": true, "dependencies": { "@babel/helper-string-parser": "^7.23.4", @@ -501,9 +501,9 @@ } }, "node_modules/@babel/helper-module-imports/node_modules/@babel/types": { - "version": "7.23.6", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.6.tgz", - "integrity": "sha512-+uarb83brBzPKN38NX1MkB6vb6+mwvR6amUulqAE7ccQw1pEl+bCia9TbdG1lsnFP7lZySvUn37CHyXQdfTwzg==", + "version": "7.23.9", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.9.tgz", + "integrity": "sha512-dQjSq/7HaSjRM43FFGnv5keM2HsxpmyV1PfaSVm0nzzjwwTmjOe6J4bC8e3+pTEIgHaHj+1ZlLThRJ2auc/w1Q==", "dev": true, "dependencies": { "@babel/helper-string-parser": "^7.23.4", @@ -544,9 +544,9 @@ } }, "node_modules/@babel/helper-optimise-call-expression/node_modules/@babel/types": { - "version": "7.23.6", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.6.tgz", - "integrity": "sha512-+uarb83brBzPKN38NX1MkB6vb6+mwvR6amUulqAE7ccQw1pEl+bCia9TbdG1lsnFP7lZySvUn37CHyXQdfTwzg==", + "version": "7.23.9", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.9.tgz", + "integrity": "sha512-dQjSq/7HaSjRM43FFGnv5keM2HsxpmyV1PfaSVm0nzzjwwTmjOe6J4bC8e3+pTEIgHaHj+1ZlLThRJ2auc/w1Q==", "dev": true, "dependencies": { "@babel/helper-string-parser": "^7.23.4", @@ -609,9 +609,9 @@ } }, "node_modules/@babel/helper-simple-access/node_modules/@babel/types": { - "version": "7.23.6", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.6.tgz", - "integrity": "sha512-+uarb83brBzPKN38NX1MkB6vb6+mwvR6amUulqAE7ccQw1pEl+bCia9TbdG1lsnFP7lZySvUn37CHyXQdfTwzg==", + "version": "7.23.9", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.9.tgz", + "integrity": "sha512-dQjSq/7HaSjRM43FFGnv5keM2HsxpmyV1PfaSVm0nzzjwwTmjOe6J4bC8e3+pTEIgHaHj+1ZlLThRJ2auc/w1Q==", "dev": true, "dependencies": { "@babel/helper-string-parser": "^7.23.4", @@ -634,9 +634,9 @@ } }, "node_modules/@babel/helper-skip-transparent-expression-wrappers/node_modules/@babel/types": { - "version": "7.23.6", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.6.tgz", - "integrity": "sha512-+uarb83brBzPKN38NX1MkB6vb6+mwvR6amUulqAE7ccQw1pEl+bCia9TbdG1lsnFP7lZySvUn37CHyXQdfTwzg==", + "version": "7.23.9", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.9.tgz", + "integrity": "sha512-dQjSq/7HaSjRM43FFGnv5keM2HsxpmyV1PfaSVm0nzzjwwTmjOe6J4bC8e3+pTEIgHaHj+1ZlLThRJ2auc/w1Q==", "dev": true, "dependencies": { "@babel/helper-string-parser": "^7.23.4", @@ -709,9 +709,9 @@ } }, "node_modules/@babel/helper-wrap-function/node_modules/@babel/types": { - "version": "7.23.6", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.6.tgz", - "integrity": "sha512-+uarb83brBzPKN38NX1MkB6vb6+mwvR6amUulqAE7ccQw1pEl+bCia9TbdG1lsnFP7lZySvUn37CHyXQdfTwzg==", + "version": "7.23.9", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.9.tgz", + "integrity": "sha512-dQjSq/7HaSjRM43FFGnv5keM2HsxpmyV1PfaSVm0nzzjwwTmjOe6J4bC8e3+pTEIgHaHj+1ZlLThRJ2auc/w1Q==", "dev": true, "dependencies": { "@babel/helper-string-parser": "^7.23.4", @@ -737,9 +737,9 @@ } }, "node_modules/@babel/helpers/node_modules/@babel/types": { - "version": "7.23.6", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.6.tgz", - "integrity": "sha512-+uarb83brBzPKN38NX1MkB6vb6+mwvR6amUulqAE7ccQw1pEl+bCia9TbdG1lsnFP7lZySvUn37CHyXQdfTwzg==", + "version": "7.23.9", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.9.tgz", + "integrity": "sha512-dQjSq/7HaSjRM43FFGnv5keM2HsxpmyV1PfaSVm0nzzjwwTmjOe6J4bC8e3+pTEIgHaHj+1ZlLThRJ2auc/w1Q==", "dev": true, "dependencies": { "@babel/helper-string-parser": "^7.23.4", @@ -1731,9 +1731,9 @@ } }, "node_modules/@babel/plugin-transform-react-jsx/node_modules/@babel/types": { - "version": "7.23.6", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.6.tgz", - "integrity": "sha512-+uarb83brBzPKN38NX1MkB6vb6+mwvR6amUulqAE7ccQw1pEl+bCia9TbdG1lsnFP7lZySvUn37CHyXQdfTwzg==", + "version": "7.23.9", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.9.tgz", + "integrity": "sha512-dQjSq/7HaSjRM43FFGnv5keM2HsxpmyV1PfaSVm0nzzjwwTmjOe6J4bC8e3+pTEIgHaHj+1ZlLThRJ2auc/w1Q==", "dev": true, "dependencies": { "@babel/helper-string-parser": "^7.23.4", @@ -2029,9 +2029,9 @@ } }, "node_modules/@babel/preset-env/node_modules/@babel/types": { - "version": "7.23.6", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.6.tgz", - "integrity": "sha512-+uarb83brBzPKN38NX1MkB6vb6+mwvR6amUulqAE7ccQw1pEl+bCia9TbdG1lsnFP7lZySvUn37CHyXQdfTwzg==", + "version": "7.23.9", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.9.tgz", + "integrity": "sha512-dQjSq/7HaSjRM43FFGnv5keM2HsxpmyV1PfaSVm0nzzjwwTmjOe6J4bC8e3+pTEIgHaHj+1ZlLThRJ2auc/w1Q==", "dev": true, "dependencies": { "@babel/helper-string-parser": "^7.23.4", @@ -2131,9 +2131,9 @@ } }, "node_modules/@babel/template/node_modules/@babel/types": { - "version": "7.23.6", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.6.tgz", - "integrity": "sha512-+uarb83brBzPKN38NX1MkB6vb6+mwvR6amUulqAE7ccQw1pEl+bCia9TbdG1lsnFP7lZySvUn37CHyXQdfTwzg==", + "version": "7.23.9", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.9.tgz", + "integrity": "sha512-dQjSq/7HaSjRM43FFGnv5keM2HsxpmyV1PfaSVm0nzzjwwTmjOe6J4bC8e3+pTEIgHaHj+1ZlLThRJ2auc/w1Q==", "dev": true, "dependencies": { "@babel/helper-string-parser": "^7.23.4", @@ -2165,9 +2165,9 @@ } }, "node_modules/@babel/traverse/node_modules/@babel/types": { - "version": "7.23.6", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.6.tgz", - "integrity": "sha512-+uarb83brBzPKN38NX1MkB6vb6+mwvR6amUulqAE7ccQw1pEl+bCia9TbdG1lsnFP7lZySvUn37CHyXQdfTwzg==", + "version": "7.23.9", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.9.tgz", + "integrity": "sha512-dQjSq/7HaSjRM43FFGnv5keM2HsxpmyV1PfaSVm0nzzjwwTmjOe6J4bC8e3+pTEIgHaHj+1ZlLThRJ2auc/w1Q==", "dev": true, "dependencies": { "@babel/helper-string-parser": "^7.23.4", @@ -3856,9 +3856,9 @@ } }, "node_modules/@svgr/hast-util-to-babel-ast/node_modules/@babel/types": { - "version": "7.23.6", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.6.tgz", - "integrity": "sha512-+uarb83brBzPKN38NX1MkB6vb6+mwvR6amUulqAE7ccQw1pEl+bCia9TbdG1lsnFP7lZySvUn37CHyXQdfTwzg==", + "version": "7.23.9", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.9.tgz", + "integrity": "sha512-dQjSq/7HaSjRM43FFGnv5keM2HsxpmyV1PfaSVm0nzzjwwTmjOe6J4bC8e3+pTEIgHaHj+1ZlLThRJ2auc/w1Q==", "dev": true, "dependencies": { "@babel/helper-string-parser": "^7.23.4", @@ -4288,9 +4288,9 @@ } }, "node_modules/@types/babel__core/node_modules/@babel/types": { - "version": "7.23.6", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.6.tgz", - "integrity": "sha512-+uarb83brBzPKN38NX1MkB6vb6+mwvR6amUulqAE7ccQw1pEl+bCia9TbdG1lsnFP7lZySvUn37CHyXQdfTwzg==", + "version": "7.23.9", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.9.tgz", + "integrity": "sha512-dQjSq/7HaSjRM43FFGnv5keM2HsxpmyV1PfaSVm0nzzjwwTmjOe6J4bC8e3+pTEIgHaHj+1ZlLThRJ2auc/w1Q==", "dev": true, "dependencies": { "@babel/helper-string-parser": "^7.23.4", @@ -4330,9 +4330,9 @@ } }, "node_modules/@types/babel__traverse/node_modules/@babel/types": { - "version": "7.23.6", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.6.tgz", - "integrity": "sha512-+uarb83brBzPKN38NX1MkB6vb6+mwvR6amUulqAE7ccQw1pEl+bCia9TbdG1lsnFP7lZySvUn37CHyXQdfTwzg==", + "version": "7.23.9", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.9.tgz", + "integrity": "sha512-dQjSq/7HaSjRM43FFGnv5keM2HsxpmyV1PfaSVm0nzzjwwTmjOe6J4bC8e3+pTEIgHaHj+1ZlLThRJ2auc/w1Q==", "dev": true, "dependencies": { "@babel/helper-string-parser": "^7.23.4", @@ -9031,9 +9031,9 @@ } }, "node_modules/magicast/node_modules/@babel/types": { - "version": "7.23.6", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.6.tgz", - "integrity": "sha512-+uarb83brBzPKN38NX1MkB6vb6+mwvR6amUulqAE7ccQw1pEl+bCia9TbdG1lsnFP7lZySvUn37CHyXQdfTwzg==", + "version": "7.23.9", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.9.tgz", + "integrity": "sha512-dQjSq/7HaSjRM43FFGnv5keM2HsxpmyV1PfaSVm0nzzjwwTmjOe6J4bC8e3+pTEIgHaHj+1ZlLThRJ2auc/w1Q==", "dev": true, "dependencies": { "@babel/helper-string-parser": "^7.23.4", @@ -10872,8 +10872,9 @@ } }, "node_modules/property-information": { - "version": "6.4.0", - "license": "MIT", + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-6.4.1.tgz", + "integrity": "sha512-OHYtXfu5aI2sS2LWFSN5rgJjrQ4pCy8i1jubJLe2QvMF8JJ++HXTUIVWFLfXJoaOfvYYjk2SN8J2wFUWIGXT4w==", "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" diff --git a/src/pages/api/[entitytype]/[...slug].ts b/src/pages/api/[entitytype]/[...slug].ts index 1e77ab0b39..19ad6cff2a 100644 --- a/src/pages/api/[entitytype]/[...slug].ts +++ b/src/pages/api/[entitytype]/[...slug].ts @@ -4,6 +4,11 @@ import { JWT, getToken } from 'next-auth/jwt'; import { validateServerSession } from '@/src/utils/auth/session'; import { OpenAIError } from '@/src/utils/server'; +import { + getEntityTypeFromPath, + getEntityUrlFromSlugs, + isValidEntityApiType, +} from '@/src/utils/server/api'; import { getApiHeaders } from '@/src/utils/server/get-headers'; import { logger } from '@/src/utils/server/logger'; @@ -14,19 +19,12 @@ import { authOptions } from '@/src/pages/api/auth/[...nextauth]'; import fetch from 'node-fetch'; import { Readable } from 'stream'; -const getUrlFromSlugs = (req: NextApiRequest) => { - const slugs = Array.isArray(req.query.slug) - ? req.query.slug - : [req.query.slug]; - - if (!slugs || slugs.length === 0) { - throw new OpenAIError('No file path provided', '', '', '400'); +const handler = async (req: NextApiRequest, res: NextApiResponse) => { + const entityType = getEntityTypeFromPath(req); + if (!entityType || !isValidEntityApiType(entityType)) { + return res.status(500).json(errorsMessages.generalServer); } - return `${process.env.DIAL_API_HOST}/v1/${encodeURI(slugs.join('/'))}`; -}; - -const handler = async (req: NextApiRequest, res: NextApiResponse) => { const session = await getServerSession(req, res, authOptions); const isSessionValid = validateServerSession(session, req, res); const token = await getToken({ req }); @@ -69,7 +67,7 @@ async function handlePutRequest( res: NextApiResponse, ) { const readable = Readable.from(req); - const url = getUrlFromSlugs(req); + const url = getEntityUrlFromSlugs(process.env.DIAL_API_HOST, req); const proxyRes = await fetch(url, { method: 'PUT', headers: { @@ -97,7 +95,7 @@ async function handleGetRequest( token: JWT | null, res: NextApiResponse, ) { - const url = getUrlFromSlugs(req); + const url = getEntityUrlFromSlugs(process.env.DIAL_API_HOST, req); const proxyRes = await fetch(url, { headers: getApiHeaders({ jwt: token?.access_token as string }), }); @@ -121,7 +119,7 @@ async function handleDeleteRequest( token: JWT | null, res: NextApiResponse, ) { - const url = getUrlFromSlugs(req); + const url = getEntityUrlFromSlugs(process.env.DIAL_API_HOST, req); const proxyRes = await fetch(url, { method: 'DELETE', headers: getApiHeaders({ jwt: token?.access_token as string }), diff --git a/src/pages/api/[entitytype]/listing.ts b/src/pages/api/[entitytype]/listing.ts index 849729c336..9abdf43bb6 100644 --- a/src/pages/api/[entitytype]/listing.ts +++ b/src/pages/api/[entitytype]/listing.ts @@ -4,7 +4,10 @@ import { getServerSession } from 'next-auth/next'; import { validateServerSession } from '@/src/utils/auth/session'; import { OpenAIError } from '@/src/utils/server'; -import { isValidEntityApiType } from '@/src/utils/server/api'; +import { + getEntityTypeFromPath, + isValidEntityApiType, +} from '@/src/utils/server/api'; import { getApiHeaders } from '@/src/utils/server/get-headers'; import { logger } from '@/src/utils/server/logger'; @@ -21,9 +24,7 @@ import { authOptions } from '@/src/pages/api/auth/[...nextauth]'; import fetch from 'node-fetch'; const handler = async (req: NextApiRequest, res: NextApiResponse) => { - const entityType = Array.isArray(req.query.entitytype) - ? '' - : req.query.entitytype; + const entityType = getEntityTypeFromPath(req); if (!entityType || !isValidEntityApiType(entityType)) { return res.status(500).json(errorsMessages.generalServer); } diff --git a/src/utils/server/api.ts b/src/utils/server/api.ts index 2c02f1f3b2..5a1c94d5e6 100644 --- a/src/utils/server/api.ts +++ b/src/utils/server/api.ts @@ -1,3 +1,7 @@ +import { NextApiRequest } from 'next'; + +import { OpenAIError } from './types'; + export enum ApiKeys { Files = 'files', Conversations = 'conversations', @@ -7,3 +11,23 @@ export enum ApiKeys { export const isValidEntityApiType = (apiKey: string) => { return Object.values(ApiKeys).includes(apiKey as ApiKeys); }; + +export const getEntityTypeFromPath = (req: NextApiRequest) => { + return Array.isArray(req.query.entitytype) ? '' : req.query.entitytype; +}; + +export const getEntityUrlFromSlugs = ( + dialApiHost: string, + req: NextApiRequest, +) => { + const entityType = getEntityTypeFromPath(req); + const slugs = Array.isArray(req.query.slug) + ? req.query.slug + : [req.query.slug]; + + if (!slugs || slugs.length === 0) { + throw new OpenAIError(`No ${entityType} path provided`, '', '', '400'); + } + + return `${dialApiHost}/v1/${encodeURI(slugs.join('/'))}`; +}; diff --git a/src/utils/server/index.ts b/src/utils/server/index.ts index ad09a16357..e624809b70 100644 --- a/src/utils/server/index.ts +++ b/src/utils/server/index.ts @@ -14,6 +14,7 @@ import { import { errorsMessages } from '@/src/constants/errors'; import { getApiHeaders } from './get-headers'; +import { OpenAIError } from './types'; import { ParsedEvent, @@ -22,19 +23,7 @@ import { } from 'eventsource-parser'; import fetch from 'node-fetch'; -export class OpenAIError extends Error { - type: string; - param: string; - code: string; - - constructor(message: string, type: string, param: string, code: string) { - super(message); - this.name = 'OpenAIError'; - this.type = type; - this.param = param; - this.code = code; - } -} +export { OpenAIError }; interface OpenAIErrorResponse extends Response { error?: OpenAIError; diff --git a/src/utils/server/types.ts b/src/utils/server/types.ts new file mode 100644 index 0000000000..d0900451ff --- /dev/null +++ b/src/utils/server/types.ts @@ -0,0 +1,13 @@ +export class OpenAIError extends Error { + type: string; + param: string; + code: string; + + constructor(message: string, type: string, param: string, code: string) { + super(message); + this.name = 'OpenAIError'; + this.type = type; + this.param = param; + this.code = code; + } +} From c3400d14e43c4d49df42884def731f142c008e1d Mon Sep 17 00:00:00 2001 From: Ilya Bondar Date: Fri, 26 Jan 2024 12:12:01 +0100 Subject: [PATCH 009/202] if no filter --- src/pages/api/[entitytype]/listing.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/pages/api/[entitytype]/listing.ts b/src/pages/api/[entitytype]/listing.ts index 9abdf43bb6..71243bd7e8 100644 --- a/src/pages/api/[entitytype]/listing.ts +++ b/src/pages/api/[entitytype]/listing.ts @@ -2,9 +2,12 @@ import { NextApiRequest, NextApiResponse } from 'next'; import { getToken } from 'next-auth/jwt'; import { getServerSession } from 'next-auth/next'; + + import { validateServerSession } from '@/src/utils/auth/session'; import { OpenAIError } from '@/src/utils/server'; import { + ApiKeys, getEntityTypeFromPath, isValidEntityApiType, } from '@/src/utils/server/api'; @@ -62,9 +65,9 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => { } const json = (await response.json()) as BackendFileFolder; - let result: (BackendFileFolder | BackendFile)[] = []; - if (filter) { - result = (json.items || []).filter((item) => item.nodeType === filter); + let result: (BackendFileFolder | BackendFile)[] = json.items || []; + if (filter && entityType === ApiKeys.Files) { + result = result.filter((item) => item.nodeType === filter); } return res.status(200).send(result); From 858a2af1ea5c03414333cfda4af7e5f97463e925 Mon Sep 17 00:00:00 2001 From: Ilya Bondar Date: Fri, 26 Jan 2024 12:13:40 +0100 Subject: [PATCH 010/202] lint fix --- src/pages/api/[entitytype]/listing.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/pages/api/[entitytype]/listing.ts b/src/pages/api/[entitytype]/listing.ts index 71243bd7e8..29c6a4d9c6 100644 --- a/src/pages/api/[entitytype]/listing.ts +++ b/src/pages/api/[entitytype]/listing.ts @@ -2,8 +2,6 @@ import { NextApiRequest, NextApiResponse } from 'next'; import { getToken } from 'next-auth/jwt'; import { getServerSession } from 'next-auth/next'; - - import { validateServerSession } from '@/src/utils/auth/session'; import { OpenAIError } from '@/src/utils/server'; import { From 7baccda403cc9228c3236ab866f2922de5c6d1ca Mon Sep 17 00:00:00 2001 From: Aliaksandr Kezik Date: Fri, 26 Jan 2024 12:23:30 +0100 Subject: [PATCH 011/202] errorDuringEntityRequest -> errorDuringEntityRequest --- src/constants/errors.ts | 4 ++-- src/pages/api/[entitytype]/[...slug].ts | 4 +++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/constants/errors.ts b/src/constants/errors.ts index 5a48f9c958..d886f5f5c4 100644 --- a/src/constants/errors.ts +++ b/src/constants/errors.ts @@ -16,8 +16,8 @@ export const errorsMessages = { 'Server is taking to long to respond due to either poor internet connection or excessive load. Please check your internet connection and try again. You also may try different model.', customThemesConfigNotProvided: 'The custom config host url not provided. Please recheck application settings', - errorDuringFileRequest: - 'Error happened during file request. Please try again later.', + errorDuringEntityRequest: (entityType: string) => + `Error happened during ${entityType} request. Please try again later.`, errorGettingUserFileBucket: 'Error happened during getting file user bucket. Please reload the page to being able to load files.', noModelsAvailable: diff --git a/src/pages/api/[entitytype]/[...slug].ts b/src/pages/api/[entitytype]/[...slug].ts index 19ad6cff2a..1ca4745966 100644 --- a/src/pages/api/[entitytype]/[...slug].ts +++ b/src/pages/api/[entitytype]/[...slug].ts @@ -48,7 +48,9 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => { .status(parseInt(error.code, 10) || 500) .send(error.message || errorsMessages.generalServer); } - return res.status(500).send(errorsMessages.errorDuringFileRequest); + return res + .status(500) + .send(errorsMessages.errorDuringEntityRequest(entityType)); } }; From 2ba68a3658b6f1017742bec99b3f9f5691d8913e Mon Sep 17 00:00:00 2001 From: Ilya Bondar Date: Fri, 26 Jan 2024 13:21:42 +0100 Subject: [PATCH 012/202] handle 404 --- src/pages/api/[entitytype]/listing.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/pages/api/[entitytype]/listing.ts b/src/pages/api/[entitytype]/listing.ts index 29c6a4d9c6..38bedf527e 100644 --- a/src/pages/api/[entitytype]/listing.ts +++ b/src/pages/api/[entitytype]/listing.ts @@ -60,6 +60,8 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => { if (!response.ok) { const serverErrorMessage = await response.text(); throw new OpenAIError(serverErrorMessage, '', '', response.status + ''); + } else if(response.status === 404) { + return res.status(200).send([]); } const json = (await response.json()) as BackendFileFolder; From b3832ba4a2096aa1d46ffc0b7eee5dcc0f399110 Mon Sep 17 00:00:00 2001 From: Ilya Bondar Date: Fri, 26 Jan 2024 13:27:29 +0100 Subject: [PATCH 013/202] handle 404 --- src/pages/api/[entitytype]/listing.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/pages/api/[entitytype]/listing.ts b/src/pages/api/[entitytype]/listing.ts index 38bedf527e..2e4c572bcb 100644 --- a/src/pages/api/[entitytype]/listing.ts +++ b/src/pages/api/[entitytype]/listing.ts @@ -57,11 +57,11 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => { headers: getApiHeaders({ jwt: token?.access_token as string }), }); - if (!response.ok) { + if (response.status === 404) { + return res.status(200).send([]); + } else if (!response.ok) { const serverErrorMessage = await response.text(); throw new OpenAIError(serverErrorMessage, '', '', response.status + ''); - } else if(response.status === 404) { - return res.status(200).send([]); } const json = (await response.json()) as BackendFileFolder; From c839f5785bb0e0ce0800995790f3f24096d0e149 Mon Sep 17 00:00:00 2001 From: Ilya Bondar Date: Fri, 26 Jan 2024 14:26:59 +0100 Subject: [PATCH 014/202] StorageType --- README.md | 2 +- src/pages/index.tsx | 7 ++++++- src/store/settings/settings.reducers.ts | 4 ++-- src/types/storage.ts | 5 ++++- src/utils/app/data/data-service.ts | 10 +++------- 5 files changed, 16 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 064d98c293..100edf35fd 100644 --- a/README.md +++ b/README.md @@ -141,7 +141,7 @@ This project leverages environment variables for configuration. | `REQUEST_API_KEY_CODE` | No | Request API Key Code used when sending request api key info to Azure Functions API Host | Any string | | | `CODE_GENERATION_WARNING` | No | Warning text regarding code generation | Any string | | | `SHOW_TOKEN_SUB` | No | Show token sub in refresh login error logs | `true`, `false` | false | -| `STORAGE_TYPE` | No | Type of storage used for getting and saving information generated by user. Now supported only `browserStorage` | `browserStorage`, `api`,`apiMock` | `browserStorage` | +| `STORAGE_TYPE` | No | Type of storage used for getting and saving information generated by user. Now supported only `browserStorage` | `browserStorage`, `api` | `api` | | `KEEP_ALIVE_TIMEOUT` | No | Determines the maximum time in milliseconds in seconds that a connection may be idle before it is closed by the server. This is needed because infrastructure usually have default keep alive timeout 60 seconds and next server should have bigger value. Used only when running dockerfile. | Any number string | 61000 | The .env file contains environment variables that can be used to configure your app's settings and behavior. These values can be changed as needed to suit your specific requirements. diff --git a/src/pages/index.tsx b/src/pages/index.tsx index e8524dc58a..029786db15 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -13,6 +13,7 @@ import { delay } from '@/src/utils/auth/delay'; import { isServerSessionValid } from '@/src/utils/auth/session'; import { timeoutAsync } from '@/src/utils/auth/timeout-async'; +import { StorageType } from '../types/storage'; import { Translation } from '../types/translation'; import { Feature } from '@/src/types/features'; import { fallbackModelID } from '@/src/types/openai'; @@ -210,7 +211,11 @@ export const getServerSideProps: GetServerSideProps = async ({ packageJSON.version, ), isAuthDisabled, - storageType: process.env.STORAGE_TYPE || 'browserStorage', + storageType: Object.values(StorageType).includes( + process.env.STORAGE_TYPE as StorageType, + ) + ? (process.env.STORAGE_TYPE as StorageType) + : StorageType.BrowserStorage, //TODO: set API as default announcement: process.env.ANNOUNCEMENT_HTML_MESSAGE || '', themesHostDefined: !!process.env.THEMES_CONFIG_HOST, }; diff --git a/src/store/settings/settings.reducers.ts b/src/store/settings/settings.reducers.ts index f7fac75330..30eefef992 100644 --- a/src/store/settings/settings.reducers.ts +++ b/src/store/settings/settings.reducers.ts @@ -18,7 +18,7 @@ export interface SettingsState { defaultModelId: string | undefined; defaultRecentModelsIds: string[]; defaultRecentAddonsIds: string[]; - storageType: StorageType | string; + storageType: StorageType; themesHostDefined: boolean; } @@ -33,7 +33,7 @@ const initialState: SettingsState = { defaultModelId: undefined, defaultRecentModelsIds: [], defaultRecentAddonsIds: [], - storageType: 'browserStorage', + storageType: StorageType.BrowserStorage, themesHostDefined: false, }; diff --git a/src/types/storage.ts b/src/types/storage.ts index 1a5f98515e..22c3adf06c 100644 --- a/src/types/storage.ts +++ b/src/types/storage.ts @@ -4,7 +4,10 @@ import { Conversation } from './chat'; import { FolderInterface } from './folder'; import { Prompt } from './prompt'; -export type StorageType = 'browserStorage' | 'api' | 'apiMock'; +export enum StorageType { + BrowserStorage = 'browserStorage', + API = 'api', +} export enum UIStorageKeys { Prompts = 'prompts', diff --git a/src/utils/app/data/data-service.ts b/src/utils/app/data/data-service.ts index ec0cde4273..722783f4a0 100644 --- a/src/utils/app/data/data-service.ts +++ b/src/utils/app/data/data-service.ts @@ -14,13 +14,12 @@ import { } from '@/src/types/files'; import { FolderInterface, FolderType } from '@/src/types/folder'; import { Prompt } from '@/src/types/prompt'; -import { DialStorage, UIStorageKeys } from '@/src/types/storage'; +import { DialStorage, StorageType, UIStorageKeys } from '@/src/types/storage'; import { Theme } from '@/src/types/themes'; import { SIDEBAR_MIN_WIDTH } from '@/src/constants/default-ui-settings'; import { constructPath } from '../file'; -import { ApiMockStorage } from './storages/api-mock-storage'; import { ApiStorage } from './storages/api-storage'; import { BrowserStorage } from './storages/browser-storage'; @@ -357,13 +356,10 @@ export class DataService { private static setDataStorage(dataStorageType?: string): void { switch (dataStorageType) { - case 'api': + case StorageType.API: this.dataStorage = new ApiStorage(); break; - case 'apiMock': - this.dataStorage = new ApiMockStorage(); - break; - case 'browserStorage': + case StorageType.BrowserStorage: default: this.dataStorage = new BrowserStorage(); } From 474f7d64b0ae834bc975e20894e03c478f59147f Mon Sep 17 00:00:00 2001 From: Aliaksandr Kezik Date: Fri, 26 Jan 2024 17:02:42 +0100 Subject: [PATCH 015/202] add typing for backend conversations/prompts and refactor backend file typing --- src/pages/api/[entitytype]/listing.ts | 18 ++++++++++++----- src/types/common.ts | 28 +++++++++++++++++++++++++++ src/types/files.ts | 25 ++++++++---------------- 3 files changed, 49 insertions(+), 22 deletions(-) diff --git a/src/pages/api/[entitytype]/listing.ts b/src/pages/api/[entitytype]/listing.ts index 2e4c572bcb..d1a870948f 100644 --- a/src/pages/api/[entitytype]/listing.ts +++ b/src/pages/api/[entitytype]/listing.ts @@ -14,9 +14,10 @@ import { logger } from '@/src/utils/server/logger'; import { BackendDataNodeType, - BackendFile, - BackendFileFolder, -} from '@/src/types/files'; + BackendEntity, + BackendEntityFolder, +} from '@/src/types/common'; +import { BackendFile, BackendFileFolder } from '@/src/types/files'; import { errorsMessages } from '@/src/constants/errors'; @@ -64,8 +65,15 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => { throw new OpenAIError(serverErrorMessage, '', '', response.status + ''); } - const json = (await response.json()) as BackendFileFolder; - let result: (BackendFileFolder | BackendFile)[] = json.items || []; + const json = (await response.json()) as + | BackendFileFolder + | BackendEntityFolder; + let result: ( + | BackendFile + | BackendEntityFolder + | BackendEntity + | BackendFileFolder + )[] = json.items || []; if (filter && entityType === ApiKeys.Files) { result = result.filter((item) => item.nodeType === filter); } diff --git a/src/types/common.ts b/src/types/common.ts index 69cc92b339..c2d94b3182 100644 --- a/src/types/common.ts +++ b/src/types/common.ts @@ -12,6 +12,11 @@ export enum FeatureType { Prompt = 'prompt', } +export enum BackendDataNodeType { + ITEM = 'ITEM', + FOLDER = 'FOLDER', +} + export interface Entity { id: string; name: string; @@ -19,3 +24,26 @@ export interface Entity { } export interface ShareEntity extends Entity, ShareInterface {} + +export interface BackendDataEntity { + name: string; + nodeType: BackendDataNodeType; + resourceType: 'FILE' | 'PROMPTS' | 'CONVERSATIONS'; + bucket: string; + parentPath: string | null | undefined; +} + +export interface BackendEntity extends BackendDataEntity { + updateAt: number; + nodeType: BackendDataNodeType.ITEM; + url: string; +} + +export interface BackendFolder extends BackendDataEntity { + nodeType: BackendDataNodeType.FOLDER; + items: ItemType[]; +} + +export type BackendEntityFolder = BackendFolder< + BackendEntity | BackendEntityFolder +>; diff --git a/src/types/files.ts b/src/types/files.ts index 734d7ebb00..4343d383cd 100644 --- a/src/types/files.ts +++ b/src/types/files.ts @@ -1,3 +1,9 @@ +import { + BackendDataEntity, + BackendDataNodeType, + BackendFolder, +} from '@/src/types/common'; + import { FolderInterface } from './folder'; export type ImageMIMEType = 'image/jpeg' | 'image/png' | string; @@ -9,28 +15,13 @@ export type MIMEType = | ImageMIMEType | string; -export enum BackendDataNodeType { - ITEM = 'ITEM', - FOLDER = 'FOLDER', -} - -interface BackendDataEntity { - name: string; - nodeType: BackendDataNodeType; - resourceType: 'FILE'; // only 1 type for now - bucket: string; - parentPath: string | null | undefined; -} - export interface BackendFile extends BackendDataEntity { nodeType: BackendDataNodeType.ITEM; contentLength: number; contentType: MIMEType; } -export interface BackendFileFolder extends BackendDataEntity { - nodeType: BackendDataNodeType.FOLDER; - items: (BackendFile | BackendFileFolder)[]; -} + +export type BackendFileFolder = BackendFolder; export type DialFile = Omit< BackendFile, From 1db926dc59b5fd4485d33d9c60866d87fb86074b Mon Sep 17 00:00:00 2001 From: Aliaksandr Kezik Date: Fri, 26 Jan 2024 17:06:05 +0100 Subject: [PATCH 016/202] fix issue in data-service --- src/utils/app/data/data-service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/app/data/data-service.ts b/src/utils/app/data/data-service.ts index 722783f4a0..d0a52f370f 100644 --- a/src/utils/app/data/data-service.ts +++ b/src/utils/app/data/data-service.ts @@ -5,8 +5,8 @@ import { isSmallScreen } from '@/src/utils/app/mobile'; import { ApiKeys } from '@/src/utils/server/api'; import { Conversation } from '@/src/types/chat'; +import { BackendDataNodeType } from '@/src/types/common'; import { - BackendDataNodeType, BackendFile, BackendFileFolder, DialFile, From 4ebeb5091262cbdd19e113f54643eafb63ed6f21 Mon Sep 17 00:00:00 2001 From: Aliaksandr Kezik Date: Fri, 26 Jan 2024 17:28:27 +0100 Subject: [PATCH 017/202] add filterableEntityTypes for listing --- src/pages/api/[entitytype]/listing.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/pages/api/[entitytype]/listing.ts b/src/pages/api/[entitytype]/listing.ts index d1a870948f..6844d6a936 100644 --- a/src/pages/api/[entitytype]/listing.ts +++ b/src/pages/api/[entitytype]/listing.ts @@ -74,7 +74,9 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => { | BackendEntity | BackendFileFolder )[] = json.items || []; - if (filter && entityType === ApiKeys.Files) { + + const filterableEntityTypes: string[] = Object.values(ApiKeys); + if (filter && filterableEntityTypes.includes(entityType)) { result = result.filter((item) => item.nodeType === filter); } From 4763598c235c03c1734823ffa3af359a65f9bc02 Mon Sep 17 00:00:00 2001 From: Aliaksandr Kezik Date: Fri, 26 Jan 2024 17:49:04 +0100 Subject: [PATCH 018/202] updateAt -> updatedAt --- src/types/common.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/types/common.ts b/src/types/common.ts index c2d94b3182..236d8fa611 100644 --- a/src/types/common.ts +++ b/src/types/common.ts @@ -34,7 +34,7 @@ export interface BackendDataEntity { } export interface BackendEntity extends BackendDataEntity { - updateAt: number; + updatedAt: number; nodeType: BackendDataNodeType.ITEM; url: string; } From 4cfff00af85244f648be72e0ab6076374f02718c Mon Sep 17 00:00:00 2001 From: Ilya Bondar Date: Fri, 26 Jan 2024 18:04:37 +0100 Subject: [PATCH 019/202] generate/parse api keys --- src/types/chat.ts | 8 +++++++- src/types/prompt.ts | 4 +++- src/utils/server/api.ts | 15 +++++++++++++++ 3 files changed, 25 insertions(+), 2 deletions(-) diff --git a/src/types/chat.ts b/src/types/chat.ts index f791ad17c6..80078ea43a 100644 --- a/src/types/chat.ts +++ b/src/types/chat.ts @@ -1,4 +1,4 @@ -import { ShareEntity } from './common'; +import { Entity, ShareEntity } from './common'; import { MIMEType } from './files'; export interface Attachment { @@ -112,3 +112,9 @@ export interface ConversationsTemporarySettings { export interface ConversationEntityModel { id: string; } + +export interface ConversationInfo extends Entity { + modelId: string; + isPlayback?: boolean; + isReplay?: boolean; +} diff --git a/src/types/prompt.ts b/src/types/prompt.ts index 52a3d477a4..2dc60c4567 100644 --- a/src/types/prompt.ts +++ b/src/types/prompt.ts @@ -1,6 +1,8 @@ -import { ShareEntity } from './common'; +import { Entity, ShareEntity } from './common'; export interface Prompt extends ShareEntity { description?: string; content?: string; } + +export type PromptInfo = Entity; diff --git a/src/utils/server/api.ts b/src/utils/server/api.ts index 5a1c94d5e6..4c16054293 100644 --- a/src/utils/server/api.ts +++ b/src/utils/server/api.ts @@ -1,5 +1,8 @@ import { NextApiRequest } from 'next'; +import { Conversation } from '@/src/types/chat'; +import { Prompt } from '@/src/types/prompt'; + import { OpenAIError } from './types'; export enum ApiKeys { @@ -31,3 +34,15 @@ export const getEntityUrlFromSlugs = ( return `${dialApiHost}/v1/${encodeURI(slugs.join('/'))}`; }; + +const pathKeySeparator = '__'; + +export const getConversationKey = (conversation: Conversation) => { + return [conversation.id, conversation.model.id, conversation.name].join( + pathKeySeparator, + ); +}; + +export const getPromptKey = (prompt: Prompt) => { + return [prompt.id, prompt.name].join(pathKeySeparator); +}; From 26ac52c3731becc5d828973658c88fa3abca5a35 Mon Sep 17 00:00:00 2001 From: Ilya Bondar Date: Fri, 26 Jan 2024 18:07:00 +0100 Subject: [PATCH 020/202] setBucket --- src/store/files/files.epics.ts | 1 + src/types/storage.ts | 2 ++ src/utils/app/data/data-service.ts | 4 ++++ src/utils/app/data/storages/api-storage.ts | 4 ++++ src/utils/app/data/storages/browser-storage.ts | 3 +++ 5 files changed, 14 insertions(+) diff --git a/src/store/files/files.epics.ts b/src/store/files/files.epics.ts index 69321737ce..20cbbeb889 100644 --- a/src/store/files/files.epics.ts +++ b/src/store/files/files.epics.ts @@ -44,6 +44,7 @@ const getBucketEpic: AppEpic = (action$) => switchMap(() => { return DataService.getFilesBucket().pipe( map(({ bucket }) => { + DataService.setBucket(bucket); return FilesActions.setBucket({ bucket }); }), catchError((error) => { diff --git a/src/types/storage.ts b/src/types/storage.ts index 22c3adf06c..b9cdacedd5 100644 --- a/src/types/storage.ts +++ b/src/types/storage.ts @@ -26,6 +26,8 @@ export enum UIStorageKeys { TextOfClosedAnnouncement = 'textOfClosedAnnouncement', } export interface DialStorage { + setBucket(bucket: string): void; + getConversationsFolders(): Observable; setConversationsFolders(folders: FolderInterface[]): Observable; getPromptsFolders(): Observable; diff --git a/src/utils/app/data/data-service.ts b/src/utils/app/data/data-service.ts index 722783f4a0..55d686d75b 100644 --- a/src/utils/app/data/data-service.ts +++ b/src/utils/app/data/data-service.ts @@ -364,4 +364,8 @@ export class DataService { this.dataStorage = new BrowserStorage(); } } + + public static setBucket(bucket: string): void { + this.dataStorage.setBucket(bucket); + } } diff --git a/src/utils/app/data/storages/api-storage.ts b/src/utils/app/data/storages/api-storage.ts index 43bb59620d..bfb347ec93 100644 --- a/src/utils/app/data/storages/api-storage.ts +++ b/src/utils/app/data/storages/api-storage.ts @@ -7,6 +7,10 @@ import { Prompt } from '@/src/types/prompt'; import { DialStorage } from '@/src/types/storage'; export class ApiStorage implements DialStorage { + private bucket: string | undefined; + setBucket(bucket: string): void { + this.bucket = bucket; + } static request(url: string, options?: RequestInit) { return fromFetch(url, options).pipe( switchMap((response) => { diff --git a/src/utils/app/data/storages/browser-storage.ts b/src/utils/app/data/storages/browser-storage.ts index 27b116eae0..aa4928a193 100644 --- a/src/utils/app/data/storages/browser-storage.ts +++ b/src/utils/app/data/storages/browser-storage.ts @@ -14,6 +14,9 @@ import { cleanConversationHistory } from '../../clean'; import { isLocalStorageEnabled } from '../storage'; export class BrowserStorage implements DialStorage { + setBucket(_bucket: string): void { + return; + } private static storage: globalThis.Storage | undefined; public static init() { From 0ba112f359d830ba7b6c7f7fc963c02824605347 Mon Sep 17 00:00:00 2001 From: Ilya Bondar Date: Fri, 26 Jan 2024 20:40:29 +0100 Subject: [PATCH 021/202] refactoring --- src/pages/api/[entitytype]/listing.ts | 23 ++- src/store/files/files.epics.ts | 2 +- src/types/common.ts | 22 ++- src/types/files.ts | 11 +- src/utils/app/data/data-service.ts | 158 +----------------- src/utils/app/data/fileService.ts | 156 +++++++++++++++++ .../app/data/storages/api-mock-storage.ts | 3 + src/utils/server/api.ts | 69 +++++++- 8 files changed, 265 insertions(+), 179 deletions(-) create mode 100644 src/utils/app/data/fileService.ts diff --git a/src/pages/api/[entitytype]/listing.ts b/src/pages/api/[entitytype]/listing.ts index 6844d6a936..10be4dbed5 100644 --- a/src/pages/api/[entitytype]/listing.ts +++ b/src/pages/api/[entitytype]/listing.ts @@ -13,9 +13,9 @@ import { getApiHeaders } from '@/src/utils/server/get-headers'; import { logger } from '@/src/utils/server/logger'; import { + BackendChatEntity, + BackendChatFolder, BackendDataNodeType, - BackendEntity, - BackendEntityFolder, } from '@/src/types/common'; import { BackendFile, BackendFileFolder } from '@/src/types/files'; @@ -27,6 +27,8 @@ import fetch from 'node-fetch'; const handler = async (req: NextApiRequest, res: NextApiResponse) => { const entityType = getEntityTypeFromPath(req); + // eslint-disable-next-line no-console + console.log('------------------------>', entityType); if (!entityType || !isValidEntityApiType(entityType)) { return res.status(500).json(errorsMessages.generalServer); } @@ -58,6 +60,17 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => { headers: getApiHeaders({ jwt: token?.access_token as string }), }); + // eslint-disable-next-line no-console + console.log( + '------------------------>', + url, + '\r\n------->', + token?.access_token, + ); + + // eslint-disable-next-line no-console + console.log('------------------------>', response.status); + if (response.status === 404) { return res.status(200).send([]); } else if (!response.ok) { @@ -67,12 +80,12 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => { const json = (await response.json()) as | BackendFileFolder - | BackendEntityFolder; + | BackendChatFolder; let result: ( | BackendFile - | BackendEntityFolder - | BackendEntity | BackendFileFolder + | BackendChatEntity + | BackendChatFolder )[] = json.items || []; const filterableEntityTypes: string[] = Object.values(ApiKeys); diff --git a/src/store/files/files.epics.ts b/src/store/files/files.epics.ts index 20cbbeb889..57ff36a5c2 100644 --- a/src/store/files/files.epics.ts +++ b/src/store/files/files.epics.ts @@ -42,7 +42,7 @@ const getBucketEpic: AppEpic = (action$) => action$.pipe( filter(FilesActions.getBucket.match), switchMap(() => { - return DataService.getFilesBucket().pipe( + return DataService.getBucket().pipe( map(({ bucket }) => { DataService.setBucket(bucket); return FilesActions.setBucket({ bucket }); diff --git a/src/types/common.ts b/src/types/common.ts index 236d8fa611..d71f2c8c94 100644 --- a/src/types/common.ts +++ b/src/types/common.ts @@ -17,6 +17,12 @@ export enum BackendDataNodeType { FOLDER = 'FOLDER', } +export enum BackendResourceType { + FILE = 'FILE', + PROMPT = 'PROMPT', + CONVERSATION = 'CONVERSATION', +} + export interface Entity { id: string; name: string; @@ -27,16 +33,18 @@ export interface ShareEntity extends Entity, ShareInterface {} export interface BackendDataEntity { name: string; - nodeType: BackendDataNodeType; - resourceType: 'FILE' | 'PROMPTS' | 'CONVERSATIONS'; + resourceType: BackendResourceType; bucket: string; - parentPath: string | null | undefined; + parentPath?: string | null; + url: string; } export interface BackendEntity extends BackendDataEntity { - updatedAt: number; nodeType: BackendDataNodeType.ITEM; - url: string; +} + +export interface BackendChatEntity extends BackendEntity { + updatedAt: number; } export interface BackendFolder extends BackendDataEntity { @@ -44,6 +52,6 @@ export interface BackendFolder extends BackendDataEntity { items: ItemType[]; } -export type BackendEntityFolder = BackendFolder< - BackendEntity | BackendEntityFolder +export type BackendChatFolder = BackendFolder< + BackendChatEntity | BackendChatFolder >; diff --git a/src/types/files.ts b/src/types/files.ts index 4343d383cd..fef97c6660 100644 --- a/src/types/files.ts +++ b/src/types/files.ts @@ -1,8 +1,4 @@ -import { - BackendDataEntity, - BackendDataNodeType, - BackendFolder, -} from '@/src/types/common'; +import { BackendEntity, BackendFolder } from '@/src/types/common'; import { FolderInterface } from './folder'; @@ -15,8 +11,7 @@ export type MIMEType = | ImageMIMEType | string; -export interface BackendFile extends BackendDataEntity { - nodeType: BackendDataNodeType.ITEM; +export interface BackendFile extends BackendEntity { contentLength: number; contentType: MIMEType; } @@ -25,7 +20,7 @@ export type BackendFileFolder = BackendFolder; export type DialFile = Omit< BackendFile, - 'path' | 'nodeType' | 'resourceType' | 'bucket' | 'parentPath' + 'path' | 'nodeType' | 'resourceType' | 'bucket' | 'parentPath' | 'url' > & { // Combination of relative path and name id: string; diff --git a/src/utils/app/data/data-service.ts b/src/utils/app/data/data-service.ts index a0e6b5c3fb..a69d4cba86 100644 --- a/src/utils/app/data/data-service.ts +++ b/src/utils/app/data/data-service.ts @@ -1,29 +1,23 @@ /* eslint-disable no-restricted-globals */ import { Observable, map } from 'rxjs'; + + import { isSmallScreen } from '@/src/utils/app/mobile'; -import { ApiKeys } from '@/src/utils/server/api'; import { Conversation } from '@/src/types/chat'; -import { BackendDataNodeType } from '@/src/types/common'; -import { - BackendFile, - BackendFileFolder, - DialFile, - FileFolderInterface, -} from '@/src/types/files'; -import { FolderInterface, FolderType } from '@/src/types/folder'; +import { FolderInterface } from '@/src/types/folder'; import { Prompt } from '@/src/types/prompt'; import { DialStorage, StorageType, UIStorageKeys } from '@/src/types/storage'; import { Theme } from '@/src/types/themes'; import { SIDEBAR_MIN_WIDTH } from '@/src/constants/default-ui-settings'; -import { constructPath } from '../file'; +import { FileService } from './fileService'; import { ApiStorage } from './storages/api-storage'; import { BrowserStorage } from './storages/browser-storage'; -export class DataService { +export class DataService extends FileService { private static dataStorage: DialStorage; public static init(storageType?: string) { @@ -198,7 +192,7 @@ export class DataService { ); } - public static getFilesBucket(): Observable<{ bucket: string }> { + public static getBucket(): Observable<{ bucket: string }> { return ApiStorage.request(`api/bucket`, { method: 'GET', headers: { @@ -207,146 +201,6 @@ export class DataService { }); } - public static sendFile( - formData: FormData, - bucket: string, - relativePath: string | undefined, - fileName: string, - ): Observable<{ percent?: number; result?: DialFile }> { - const resultPath = encodeURI( - `files/${bucket}/${relativePath ? `${relativePath}/` : ''}${fileName}`, - ); - - return ApiStorage.requestOld({ - url: `api/${ApiKeys.Files}/${resultPath}`, - method: 'PUT', - async: true, - body: formData, - }).pipe( - map( - ({ - percent, - result, - }: { - percent?: number; - result?: unknown; - }): { percent?: number; result?: DialFile } => { - if (percent) { - return { percent }; - } - - if (!result) { - return {}; - } - - const typedResult = result as BackendFile; - const relativePath = typedResult.parentPath || undefined; - - return { - result: { - id: constructPath(relativePath, typedResult.name), - name: typedResult.name, - absolutePath: constructPath( - 'files', - typedResult.bucket, - relativePath, - ), - relativePath: relativePath, - folderId: relativePath, - contentLength: typedResult.contentLength, - contentType: typedResult.contentType, - serverSynced: true, - }, - }; - }, - ), - ); - } - - public static getFileFolders( - bucket: string, - parentPath?: string, - ): Observable { - const filter = BackendDataNodeType.FOLDER; - - const query = new URLSearchParams({ - filter, - bucket, - ...(parentPath && { path: parentPath }), - }); - const resultQuery = query.toString(); - - return ApiStorage.request( - `api/${ApiKeys.Files}/listing?${resultQuery}`, - ).pipe( - map((folders: BackendFileFolder[]) => { - return folders.map((folder): FileFolderInterface => { - const relativePath = folder.parentPath || undefined; - - return { - id: constructPath(relativePath, folder.name), - name: folder.name, - type: FolderType.File, - absolutePath: constructPath(ApiKeys.Files, bucket, relativePath), - relativePath: relativePath, - folderId: relativePath, - serverSynced: true, - }; - }); - }), - ); - } - - public static removeFile(bucket: string, filePath: string): Observable { - const resultPath = encodeURI(constructPath('files', bucket, filePath)); - - return ApiStorage.request(`api/${ApiKeys.Files}/${resultPath}`, { - method: 'DELETE', - headers: { - 'Content-Type': 'application/json', - }, - }); - } - - public static getFiles( - bucket: string, - parentPath?: string, - ): Observable { - const filter = BackendDataNodeType.ITEM; - - const query = new URLSearchParams({ - filter, - bucket, - ...(parentPath && { path: parentPath }), - }); - const resultQuery = query.toString(); - - return ApiStorage.request( - `api/${ApiKeys.Files}/listing?${resultQuery}`, - ).pipe( - map((files: BackendFile[]) => { - return files.map((file): DialFile => { - const relativePath = file.parentPath || undefined; - - return { - id: constructPath(relativePath, file.name), - name: file.name, - absolutePath: constructPath( - ApiKeys.Files, - file.bucket, - relativePath, - ), - relativePath: relativePath, - folderId: relativePath, - contentLength: file.contentLength, - contentType: file.contentType, - serverSynced: true, - }; - }); - }), - ); - } - private static getDataStorage(): DialStorage { if (!this.dataStorage) { this.setDataStorage(); diff --git a/src/utils/app/data/fileService.ts b/src/utils/app/data/fileService.ts new file mode 100644 index 0000000000..d296da3f08 --- /dev/null +++ b/src/utils/app/data/fileService.ts @@ -0,0 +1,156 @@ +import { Observable, map } from 'rxjs'; + +import { BackendDataNodeType } from '@/src/types/common'; +import { + BackendFile, + BackendFileFolder, + DialFile, + FileFolderInterface, +} from '@/src/types/files'; +import { FolderType } from '@/src/types/folder'; + +import { ApiKeys } from '../../server/api'; +import { constructPath } from '../file'; +import { ApiStorage } from './storages/api-storage'; + +export class FileService { + public static sendFile( + formData: FormData, + bucket: string, + relativePath: string | undefined, + fileName: string, + ): Observable<{ percent?: number; result?: DialFile }> { + const resultPath = encodeURI( + `files/${bucket}/${relativePath ? `${relativePath}/` : ''}${fileName}`, + ); + + return ApiStorage.requestOld({ + url: `api/${ApiKeys.Files}/${resultPath}`, + method: 'PUT', + async: true, + body: formData, + }).pipe( + map( + ({ + percent, + result, + }: { + percent?: number; + result?: unknown; + }): { percent?: number; result?: DialFile } => { + if (percent) { + return { percent }; + } + + if (!result) { + return {}; + } + + const typedResult = result as BackendFile; + const relativePath = typedResult.parentPath || undefined; + + return { + result: { + id: constructPath(relativePath, typedResult.name), + name: typedResult.name, + absolutePath: constructPath( + 'files', + typedResult.bucket, + relativePath, + ), + relativePath: relativePath, + folderId: relativePath, + contentLength: typedResult.contentLength, + contentType: typedResult.contentType, + serverSynced: true, + }, + }; + }, + ), + ); + } + + public static getFileFolders( + bucket: string, + parentPath?: string, + ): Observable { + const filter = BackendDataNodeType.FOLDER; + + const query = new URLSearchParams({ + filter, + bucket, + ...(parentPath && { path: parentPath }), + }); + const resultQuery = query.toString(); + + return ApiStorage.request( + `api/${ApiKeys.Files}/listing?${resultQuery}`, + ).pipe( + map((folders: BackendFileFolder[]) => { + return folders.map((folder): FileFolderInterface => { + const relativePath = folder.parentPath || undefined; + + return { + id: constructPath(relativePath, folder.name), + name: folder.name, + type: FolderType.File, + absolutePath: constructPath(ApiKeys.Files, bucket, relativePath), + relativePath: relativePath, + folderId: relativePath, + serverSynced: true, + }; + }); + }), + ); + } + + public static removeFile(bucket: string, filePath: string): Observable { + const resultPath = encodeURI(constructPath('files', bucket, filePath)); + + return ApiStorage.request(`api/${ApiKeys.Files}/${resultPath}`, { + method: 'DELETE', + headers: { + 'Content-Type': 'application/json', + }, + }); + } + + public static getFiles( + bucket: string, + parentPath?: string, + ): Observable { + const filter = BackendDataNodeType.ITEM; + + const query = new URLSearchParams({ + filter, + bucket, + ...(parentPath && { path: parentPath }), + }); + const resultQuery = query.toString(); + + return ApiStorage.request( + `api/${ApiKeys.Files}/listing?${resultQuery}`, + ).pipe( + map((files: BackendFile[]) => { + return files.map((file): DialFile => { + const relativePath = file.parentPath || undefined; + + return { + id: constructPath(relativePath, file.name), + name: file.name, + absolutePath: constructPath( + ApiKeys.Files, + file.bucket, + relativePath, + ), + relativePath: relativePath, + folderId: relativePath, + contentLength: file.contentLength, + contentType: file.contentType, + serverSynced: true, + }; + }); + }), + ); + } +} diff --git a/src/utils/app/data/storages/api-mock-storage.ts b/src/utils/app/data/storages/api-mock-storage.ts index f08cb42472..a49300a931 100644 --- a/src/utils/app/data/storages/api-mock-storage.ts +++ b/src/utils/app/data/storages/api-mock-storage.ts @@ -7,6 +7,9 @@ import { Prompt } from '@/src/types/prompt'; import { DialStorage } from '@/src/types/storage'; export class ApiMockStorage implements DialStorage { + setBucket(_bucket: string): void { + return; + } setConversationsFolders(_folders: FolderInterface[]): Observable { return of(undefined); } diff --git a/src/utils/server/api.ts b/src/utils/server/api.ts index 4c16054293..a3e1ae2691 100644 --- a/src/utils/server/api.ts +++ b/src/utils/server/api.ts @@ -1,7 +1,7 @@ import { NextApiRequest } from 'next'; -import { Conversation } from '@/src/types/chat'; -import { Prompt } from '@/src/types/prompt'; +import { Conversation, ConversationInfo } from '@/src/types/chat'; +import { Prompt, PromptInfo } from '@/src/types/prompt'; import { OpenAIError } from './types'; @@ -37,12 +37,69 @@ export const getEntityUrlFromSlugs = ( const pathKeySeparator = '__'; -export const getConversationKey = (conversation: Conversation) => { - return [conversation.id, conversation.model.id, conversation.name].join( +enum PseudoModel { + Replay = 'replay', + Playback = 'playback', +} + +const getModelApiIdFromConversation = (conversation: Conversation) => { + if (conversation.replay.isReplay) return PseudoModel.Replay; + if (conversation.playback?.isPlayback) return PseudoModel.Playback; + return conversation.model.id; +}; + +// Format key: {id:guid}__{modelId}__{name:base64} +export const getConversationApiKeyFromConversation = ( + conversation: Conversation, +) => { + return [ + conversation.id, + getModelApiIdFromConversation(conversation), + btoa(conversation.name), + ].join(pathKeySeparator); +}; + +// Format key: {id:guid}__{modelId}__{name:base64} +export const getConversationApiKeyFromConversationInfo = ( + conversation: ConversationInfo, +) => { + return [conversation.id, conversation.modelId, btoa(conversation.name)].join( pathKeySeparator, ); }; -export const getPromptKey = (prompt: Prompt) => { - return [prompt.id, prompt.name].join(pathKeySeparator); +// Format key: {id:guid}__{modelId}__{name:base64} +export const parseConversationApiKey = (apiKey: string): ConversationInfo => { + const parts = apiKey.split(pathKeySeparator); + + if (parts.length !== 3) throw new Error('Incorrect conversation key'); + + const [id, modelId, encodedName] = parts; + + return { + id, + modelId, + name: atob(encodedName), + isPlayback: modelId === PseudoModel.Playback, + isReplay: modelId === PseudoModel.Replay, + }; +}; + +// Format key: {id:guid}__{name:base64} +export const getPromptApiKey = (prompt: Prompt | PromptInfo) => { + return [prompt.id, btoa(prompt.name)].join(pathKeySeparator); +}; + +// Format key: {id:guid}__{name:base64} +export const parsePromptApiKey = (apiKey: string): PromptInfo => { + const parts = apiKey.split(pathKeySeparator); + + if (parts.length !== 2) throw new Error('Incorrect conversation key'); + + const [id, encodedName] = parts; + + return { + id, + name: atob(encodedName), + }; }; From c05a13c7341b5ccfa452596d830597dbbf8d5b6b Mon Sep 17 00:00:00 2001 From: Ilya Bondar Date: Fri, 26 Jan 2024 20:40:57 +0100 Subject: [PATCH 022/202] Update data-service.ts --- src/utils/app/data/data-service.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/utils/app/data/data-service.ts b/src/utils/app/data/data-service.ts index a69d4cba86..6bf262ca19 100644 --- a/src/utils/app/data/data-service.ts +++ b/src/utils/app/data/data-service.ts @@ -1,8 +1,6 @@ /* eslint-disable no-restricted-globals */ import { Observable, map } from 'rxjs'; - - import { isSmallScreen } from '@/src/utils/app/mobile'; import { Conversation } from '@/src/types/chat'; From 59f00d823ee58aaa3a0eed5ef149fbd41a9d8b61 Mon Sep 17 00:00:00 2001 From: Aliaksandr Kezik Date: Mon, 29 Jan 2024 14:08:26 +0100 Subject: [PATCH 023/202] move bucket receiving from file epic and reduser --- src/store/files/files.epics.ts | 98 +++++-------------- src/store/files/files.reducers.ts | 18 ---- src/store/settings/settings.epic.ts | 34 ++++++- src/types/storage.ts | 9 +- src/utils/app/data/data-service.ts | 15 ++- src/utils/app/data/fileService.ts | 29 +++--- src/utils/app/data/storages/api-storage.ts | 13 ++- .../app/data/storages/browser-storage.ts | 4 +- 8 files changed, 97 insertions(+), 123 deletions(-) diff --git a/src/store/files/files.epics.ts b/src/store/files/files.epics.ts index 57ff36a5c2..ed5678682c 100644 --- a/src/store/files/files.epics.ts +++ b/src/store/files/files.epics.ts @@ -21,60 +21,18 @@ import { ApiKeys } from '@/src/utils/server/api'; import { AppEpic } from '@/src/types/store'; -import { errorsMessages } from '@/src/constants/errors'; - import { UIActions } from '../ui/ui.reducers'; import { FilesActions, FilesSelectors } from './files.reducers'; -const initEpic: AppEpic = (action$) => - action$.pipe( - filter(FilesActions.init.match), - switchMap(() => { - const actions = []; - - actions.push(FilesActions.getBucket()); - - return concat(actions); - }), - ); - -const getBucketEpic: AppEpic = (action$) => - action$.pipe( - filter(FilesActions.getBucket.match), - switchMap(() => { - return DataService.getBucket().pipe( - map(({ bucket }) => { - DataService.setBucket(bucket); - return FilesActions.setBucket({ bucket }); - }), - catchError((error) => { - if (error.status === 401) { - window.location.assign('api/auth/signin'); - return EMPTY; - } else { - return of( - UIActions.showToast({ - message: errorsMessages.errorGettingUserFileBucket, - type: 'error', - }), - ); - } - }), - ); - }), - ); - -const uploadFileEpic: AppEpic = (action$, state$) => +const uploadFileEpic: AppEpic = (action$) => action$.pipe( filter(FilesActions.uploadFile.match), mergeMap(({ payload }) => { - const bucket = FilesSelectors.selectBucket(state$.value); const formData = new FormData(); formData.append('attachment', payload.fileContent, payload.name); return DataService.sendFile( formData, - bucket, payload.relativePath, payload.name, ).pipe( @@ -129,43 +87,35 @@ const reuploadFileEpic: AppEpic = (action$, state$) => }), ); -const getFilesEpic: AppEpic = (action$, state$) => +const getFilesEpic: AppEpic = (action$) => action$.pipe( filter(FilesActions.getFiles.match), - switchMap(({ payload }) => { - const bucket = FilesSelectors.selectBucket(state$.value); - - return DataService.getFiles(bucket, payload.path).pipe( - map((files) => { - return FilesActions.getFilesSuccess({ + switchMap(({ payload }) => + DataService.getFiles(payload.path).pipe( + map((files) => + FilesActions.getFilesSuccess({ relativePath: payload.path, files, - }); - }), - catchError(() => { - return of(FilesActions.getFilesFail()); - }), - ); - }), + }), + ), + catchError(() => of(FilesActions.getFilesFail())), + ), + ), ); -const getFileFoldersEpic: AppEpic = (action$, state$) => +const getFileFoldersEpic: AppEpic = (action$) => action$.pipe( filter(FilesActions.getFolders.match), - switchMap(({ payload }) => { - const bucket = FilesSelectors.selectBucket(state$.value); - - return DataService.getFileFolders(bucket, payload?.path).pipe( - map((folders) => { - return FilesActions.getFoldersSuccess({ + switchMap(({ payload }) => + DataService.getFileFolders(payload?.path).pipe( + map((folders) => + FilesActions.getFoldersSuccess({ folders, - }); - }), - catchError(() => { - return of(FilesActions.getFoldersFail()); - }), - ); - }), + }), + ), + catchError(() => of(FilesActions.getFoldersFail())), + ), + ), ); const getFilesWithFoldersEpic: AppEpic = (action$) => @@ -195,8 +145,6 @@ const removeFileEpic: AppEpic = (action$, state$) => action$.pipe( filter(FilesActions.removeFile.match), mergeMap(({ payload }) => { - const bucket = FilesSelectors.selectBucket(state$.value); - const file = FilesSelectors.selectFiles(state$.value).find( (file) => file.id === payload.fileId, ); @@ -216,7 +164,7 @@ const removeFileEpic: AppEpic = (action$, state$) => ); } - return DataService.removeFile(bucket, payload.fileId).pipe( + return DataService.removeFile(payload.fileId).pipe( map(() => { return FilesActions.removeFileSuccess({ fileId: payload.fileId, @@ -291,7 +239,6 @@ const downloadFilesListEpic: AppEpic = (action$, state$) => ); export const FilesEpics = combineEpics( - initEpic, uploadFileEpic, getFileFoldersEpic, getFilesEpic, @@ -302,6 +249,5 @@ export const FilesEpics = combineEpics( removeMultipleFilesEpic, downloadFilesListEpic, removeFileFailEpic, - getBucketEpic, unselectFilesEpic, ); diff --git a/src/store/files/files.reducers.ts b/src/store/files/files.reducers.ts index b51c5f2c13..dc8e40d0d8 100644 --- a/src/store/files/files.reducers.ts +++ b/src/store/files/files.reducers.ts @@ -15,7 +15,6 @@ type Status = undefined | 'LOADING' | 'LOADED' | 'FAILED'; export interface FilesState { files: DialFile[]; - bucket: string; selectedFilesIds: string[]; filesStatus: Status; @@ -27,7 +26,6 @@ export interface FilesState { const initialState: FilesState = { files: [], - bucket: '', filesStatus: undefined, selectedFilesIds: [], @@ -41,18 +39,6 @@ export const filesSlice = createSlice({ name: 'files', initialState, reducers: { - init: (state) => state, - getBucket: (state) => state, - setBucket: ( - state, - { - payload, - }: PayloadAction<{ - bucket: string; - }>, - ) => { - state.bucket = payload.bucket; - }, uploadFile: ( state, { @@ -353,9 +339,6 @@ const selectLoadingFolderId = createSelector([rootSelector], (state) => { const selectNewAddedFolderId = createSelector([rootSelector], (state) => { return state.newAddedFolderId; }); -const selectBucket = createSelector([rootSelector], (state) => { - return state.bucket; -}); const selectFoldersWithSearchTerm = createSelector( [selectFolders, (_state, searchTerm: string) => searchTerm], (folders, searchTerm) => { @@ -377,7 +360,6 @@ export const FilesSelectors = { selectLoadingFolderId, selectNewAddedFolderId, selectFilesByIds, - selectBucket, selectFoldersWithSearchTerm, }; diff --git a/src/store/settings/settings.epic.ts b/src/store/settings/settings.epic.ts index 33f692fa85..23c5aa0355 100644 --- a/src/store/settings/settings.epic.ts +++ b/src/store/settings/settings.epic.ts @@ -1,4 +1,14 @@ -import { concat, filter, first, of, switchMap, tap } from 'rxjs'; +import { + EMPTY, + catchError, + concat, + filter, + first, + map, + of, + switchMap, + tap, +} from 'rxjs'; import { combineEpics } from 'redux-observable'; @@ -6,10 +16,11 @@ import { DataService } from '@/src/utils/app/data/data-service'; import { AppEpic } from '@/src/types/store'; +import { errorsMessages } from '@/src/constants/errors'; + import { AddonsActions } from '../addons/addons.reducers'; import { AuthSelectors } from '../auth/auth.reducers'; import { ConversationsActions } from '../conversations/conversations.reducers'; -import { FilesActions } from '../files/files.reducers'; import { ModelsActions } from '../models/models.reducers'; import { PromptsActions } from '../prompts/prompts.reducers'; import { UIActions } from '../ui/ui.reducers'; @@ -31,6 +42,24 @@ const initEpic: AppEpic = (action$, state$) => return authStatus !== 'loading' && !shouldLogin; }), first(), + switchMap(() => + DataService.requestBucket().pipe( + map(({ bucket }) => DataService.setBucket(bucket)), + catchError((error) => { + if (error.status === 401) { + window.location.assign('api/auth/signin'); + return EMPTY; + } else { + return of( + UIActions.showToast({ + message: errorsMessages.errorGettingUserFileBucket, + type: 'error', + }), + ); + } + }), + ), + ), switchMap(() => concat( of(UIActions.init()), @@ -38,7 +67,6 @@ const initEpic: AppEpic = (action$, state$) => of(AddonsActions.init()), of(ConversationsActions.init()), of(PromptsActions.init()), - of(FilesActions.init()), ), ), ); diff --git a/src/types/storage.ts b/src/types/storage.ts index b9cdacedd5..6f3797963f 100644 --- a/src/types/storage.ts +++ b/src/types/storage.ts @@ -25,16 +25,21 @@ export enum UIStorageKeys { OpenedFoldersIds = 'openedFoldersIds', TextOfClosedAnnouncement = 'textOfClosedAnnouncement', } -export interface DialStorage { - setBucket(bucket: string): void; +export interface DialStorage { getConversationsFolders(): Observable; + setConversationsFolders(folders: FolderInterface[]): Observable; + getPromptsFolders(): Observable; + setPromptsFolders(folders: FolderInterface[]): Observable; getConversations(): Observable; + setConversations(conversations: Conversation[]): Observable; + getPrompts(): Observable; + setPrompts(prompts: Prompt[]): Observable; } diff --git a/src/utils/app/data/data-service.ts b/src/utils/app/data/data-service.ts index 6bf262ca19..fb1672b025 100644 --- a/src/utils/app/data/data-service.ts +++ b/src/utils/app/data/data-service.ts @@ -17,12 +17,17 @@ import { BrowserStorage } from './storages/browser-storage'; export class DataService extends FileService { private static dataStorage: DialStorage; + private static bucket: string; public static init(storageType?: string) { BrowserStorage.init(); this.setDataStorage(storageType); } + public static setBucket(bucket: string): void { + this.bucket = bucket; + } + public static getConversationsFolders(): Observable { return this.getDataStorage().getConversationsFolders(); } @@ -190,7 +195,7 @@ export class DataService extends FileService { ); } - public static getBucket(): Observable<{ bucket: string }> { + public static requestBucket(): Observable<{ bucket: string }> { return ApiStorage.request(`api/bucket`, { method: 'GET', headers: { @@ -199,6 +204,10 @@ export class DataService extends FileService { }); } + public static getBucket(): string { + return this.bucket; + } + private static getDataStorage(): DialStorage { if (!this.dataStorage) { this.setDataStorage(); @@ -216,8 +225,4 @@ export class DataService extends FileService { this.dataStorage = new BrowserStorage(); } } - - public static setBucket(bucket: string): void { - this.dataStorage.setBucket(bucket); - } } diff --git a/src/utils/app/data/fileService.ts b/src/utils/app/data/fileService.ts index d296da3f08..b06fab85a5 100644 --- a/src/utils/app/data/fileService.ts +++ b/src/utils/app/data/fileService.ts @@ -1,5 +1,7 @@ import { Observable, map } from 'rxjs'; +import { DataService } from '@/src/utils/app/data/data-service'; + import { BackendDataNodeType } from '@/src/types/common'; import { BackendFile, @@ -16,12 +18,13 @@ import { ApiStorage } from './storages/api-storage'; export class FileService { public static sendFile( formData: FormData, - bucket: string, relativePath: string | undefined, fileName: string, ): Observable<{ percent?: number; result?: DialFile }> { const resultPath = encodeURI( - `files/${bucket}/${relativePath ? `${relativePath}/` : ''}${fileName}`, + `files/${DataService.getBucket()}/${ + relativePath ? `${relativePath}/` : '' + }${fileName}`, ); return ApiStorage.requestOld({ @@ -71,14 +74,13 @@ export class FileService { } public static getFileFolders( - bucket: string, parentPath?: string, ): Observable { const filter = BackendDataNodeType.FOLDER; const query = new URLSearchParams({ filter, - bucket, + bucket: DataService.getBucket(), ...(parentPath && { path: parentPath }), }); const resultQuery = query.toString(); @@ -94,7 +96,11 @@ export class FileService { id: constructPath(relativePath, folder.name), name: folder.name, type: FolderType.File, - absolutePath: constructPath(ApiKeys.Files, bucket, relativePath), + absolutePath: constructPath( + ApiKeys.Files, + DataService.getBucket(), + relativePath, + ), relativePath: relativePath, folderId: relativePath, serverSynced: true, @@ -104,8 +110,10 @@ export class FileService { ); } - public static removeFile(bucket: string, filePath: string): Observable { - const resultPath = encodeURI(constructPath('files', bucket, filePath)); + public static removeFile(filePath: string): Observable { + const resultPath = encodeURI( + constructPath('files', DataService.getBucket(), filePath), + ); return ApiStorage.request(`api/${ApiKeys.Files}/${resultPath}`, { method: 'DELETE', @@ -115,15 +123,12 @@ export class FileService { }); } - public static getFiles( - bucket: string, - parentPath?: string, - ): Observable { + public static getFiles(parentPath?: string): Observable { const filter = BackendDataNodeType.ITEM; const query = new URLSearchParams({ filter, - bucket, + bucket: DataService.getBucket(), ...(parentPath && { path: parentPath }), }); const resultQuery = query.toString(); diff --git a/src/utils/app/data/storages/api-storage.ts b/src/utils/app/data/storages/api-storage.ts index bfb347ec93..8abee21040 100644 --- a/src/utils/app/data/storages/api-storage.ts +++ b/src/utils/app/data/storages/api-storage.ts @@ -7,10 +7,6 @@ import { Prompt } from '@/src/types/prompt'; import { DialStorage } from '@/src/types/storage'; export class ApiStorage implements DialStorage { - private bucket: string | undefined; - setBucket(bucket: string): void { - this.bucket = bucket; - } static request(url: string, options?: RequestInit) { return fromFetch(url, options).pipe( switchMap((response) => { @@ -22,6 +18,7 @@ export class ApiStorage implements DialStorage { }), ); } + static requestOld({ url, method, @@ -69,27 +66,35 @@ export class ApiStorage implements DialStorage { }; }); } + getConversationsFolders(): Observable { throw new Error('Method not implemented.'); } + setConversationsFolders(_folders: FolderInterface[]): Observable { throw new Error('Method not implemented.'); } + getPromptsFolders(): Observable { throw new Error('Method not implemented.'); } + setPromptsFolders(_folders: FolderInterface[]): Observable { throw new Error('Method not implemented.'); } + getConversations(): Observable { throw new Error('Method not implemented.'); } + setConversations(_conversations: Conversation[]): Observable { throw new Error('Method not implemented.'); } + getPrompts(): Observable { throw new Error('Method not implemented.'); } + setPrompts(_prompts: Prompt[]): Observable { throw new Error('Method not implemented.'); } diff --git a/src/utils/app/data/storages/browser-storage.ts b/src/utils/app/data/storages/browser-storage.ts index aa4928a193..e8b77458a4 100644 --- a/src/utils/app/data/storages/browser-storage.ts +++ b/src/utils/app/data/storages/browser-storage.ts @@ -14,9 +14,6 @@ import { cleanConversationHistory } from '../../clean'; import { isLocalStorageEnabled } from '../storage'; export class BrowserStorage implements DialStorage { - setBucket(_bucket: string): void { - return; - } private static storage: globalThis.Storage | undefined; public static init() { @@ -79,6 +76,7 @@ export class BrowserStorage implements DialStorage { ), ); } + setPromptsFolders(promptsFolders: FolderInterface[]): Observable { return BrowserStorage.getData(UIStorageKeys.Folders, []).pipe( map((items: FolderInterface[]) => From efc2bf64f559ff7ec6cc81cbe012acd901e2df7f Mon Sep 17 00:00:00 2001 From: Ilya Bondar Date: Mon, 29 Jan 2024 17:22:00 +0100 Subject: [PATCH 024/202] draft changes for storage --- src/types/chat.ts | 5 +- src/types/prompt.ts | 7 +- src/types/storage.ts | 17 ++- src/utils/app/data/storages/api-storage.ts | 136 +++++++++++++++--- .../app/data/storages/browser-storage.ts | 19 ++- src/utils/server/api.ts | 7 +- 6 files changed, 160 insertions(+), 31 deletions(-) diff --git a/src/types/chat.ts b/src/types/chat.ts index 80078ea43a..6c6d23d4f5 100644 --- a/src/types/chat.ts +++ b/src/types/chat.ts @@ -1,6 +1,7 @@ import { Entity, ShareEntity } from './common'; import { MIMEType } from './files'; + export interface Attachment { index?: number; type: MIMEType; @@ -68,7 +69,7 @@ export interface RateBody { value: boolean; } -export interface Conversation extends ShareEntity { +export interface Conversation extends ShareEntity, ConversationInfo { messages: Message[]; model: ConversationEntityModel; prompt: string; @@ -117,4 +118,6 @@ export interface ConversationInfo extends Entity { modelId: string; isPlayback?: boolean; isReplay?: boolean; + + uploaded?: boolean; } diff --git a/src/types/prompt.ts b/src/types/prompt.ts index 2dc60c4567..ca590d5cd7 100644 --- a/src/types/prompt.ts +++ b/src/types/prompt.ts @@ -1,8 +1,11 @@ import { Entity, ShareEntity } from './common'; -export interface Prompt extends ShareEntity { + +export interface Prompt extends ShareEntity, PromptInfo { description?: string; content?: string; } -export type PromptInfo = Entity; +export interface PromptInfo extends Entity { + uploaded?: boolean; +}; diff --git a/src/types/storage.ts b/src/types/storage.ts index 6f3797963f..1fcaa6538e 100644 --- a/src/types/storage.ts +++ b/src/types/storage.ts @@ -1,8 +1,10 @@ import { Observable } from 'rxjs'; -import { Conversation } from './chat'; +import { Conversation } from '@/src/types/chat'; + +import { ConversationInfo } from './chat'; import { FolderInterface } from './folder'; -import { Prompt } from './prompt'; +import { Prompt, PromptInfo } from './prompt'; export enum StorageType { BrowserStorage = 'browserStorage', @@ -35,11 +37,18 @@ export interface DialStorage { setPromptsFolders(folders: FolderInterface[]): Observable; - getConversations(): Observable; + getConversations(path?: string): Observable; + + getConversation( + info: ConversationInfo, + path?: string, + ): Observable; setConversations(conversations: Conversation[]): Observable; - getPrompts(): Observable; + getPrompts(path?: string): Observable; + + getPrompt(info: PromptInfo, path?: string): Observable; setPrompts(prompts: Prompt[]): Observable; } diff --git a/src/utils/app/data/storages/api-storage.ts b/src/utils/app/data/storages/api-storage.ts index 8abee21040..66f5266052 100644 --- a/src/utils/app/data/storages/api-storage.ts +++ b/src/utils/app/data/storages/api-storage.ts @@ -1,11 +1,23 @@ -import { Observable, from, switchMap, throwError } from 'rxjs'; +import { Observable, from, map, of, switchMap, throwError } from 'rxjs'; import { fromFetch } from 'rxjs/fetch'; -import { Conversation } from '@/src/types/chat'; +import { + ApiKeys, + getConversationApiKeyFromConversationInfo, + getPromptApiKey, + parseConversationApiKey, + parsePromptApiKey, +} from '@/src/utils/server/api'; + +import { Conversation, ConversationInfo } from '@/src/types/chat'; +import { BackendChatEntity, BackendDataNodeType } from '@/src/types/common'; import { FolderInterface } from '@/src/types/folder'; -import { Prompt } from '@/src/types/prompt'; +import { Prompt, PromptInfo } from '@/src/types/prompt'; import { DialStorage } from '@/src/types/storage'; +import { constructPath } from '../../file'; +import { DataService } from '../data-service'; + export class ApiStorage implements DialStorage { static request(url: string, options?: RequestInit) { return fromFetch(url, options).pipe( @@ -18,7 +30,6 @@ export class ApiStorage implements DialStorage { }), ); } - static requestOld({ url, method, @@ -66,36 +77,121 @@ export class ApiStorage implements DialStorage { }; }); } - getConversationsFolders(): Observable { - throw new Error('Method not implemented.'); + return of(); //TODO } - setConversationsFolders(_folders: FolderInterface[]): Observable { - throw new Error('Method not implemented.'); - } + const resultPath = encodeURI(constructPath(DataService.getBucket(), '')); - getPromptsFolders(): Observable { - throw new Error('Method not implemented.'); + const response = ApiStorage.request( + `api/${ApiKeys.Conversations}/${resultPath}`, + ); + + // eslint-disable-next-line no-console + console.log(response); + + return response; } + getPromptsFolders(): Observable { + const resultPath = encodeURI(constructPath(DataService.getBucket(), '')); + const response = ApiStorage.request(`api/${ApiKeys.Prompts}/${resultPath}`); + + // eslint-disable-next-line no-console + console.log(response); + + return response; + } setPromptsFolders(_folders: FolderInterface[]): Observable { - throw new Error('Method not implemented.'); + return of(); //TODO } + getConversations(path?: string): Observable { + const filter = BackendDataNodeType.ITEM; - getConversations(): Observable { - throw new Error('Method not implemented.'); + const query = new URLSearchParams({ + filter, + bucket: DataService.getBucket(), + ...(path && { path }), + }); + const resultQuery = query.toString(); + + return ApiStorage.request( + `api/${ApiKeys.Conversations}/listing?${resultQuery}`, + ).pipe( + map((conversations: BackendChatEntity[]) => { + return conversations.map((conversation): ConversationInfo => { + const relativePath = conversation.parentPath || undefined; + const conversationInfo = parseConversationApiKey(conversation.name); + + return { + ...conversationInfo, + folderId: relativePath, + }; + }); + }), + ); + } + getConversation( + info: ConversationInfo, + path?: string | undefined, + ): Observable { + const key = getConversationApiKeyFromConversationInfo(info); + return ApiStorage.request( + `api/${ApiKeys.Conversations}/${DataService.getBucket()}/${key}`, + ).pipe( + map((conversation: Conversation) => { + return { + ...info, + ...conversation, + uploaded: true, + }; + }), + ); } - setConversations(_conversations: Conversation[]): Observable { - throw new Error('Method not implemented.'); + return of(); //TODO } + getPrompts(path?: string): Observable { + const filter = BackendDataNodeType.ITEM; - getPrompts(): Observable { - throw new Error('Method not implemented.'); + const query = new URLSearchParams({ + filter, + bucket: DataService.getBucket(), + ...(path && { path }), + }); + const resultQuery = query.toString(); + + return ApiStorage.request( + `api/${ApiKeys.Prompts}/listing?${resultQuery}`, + ).pipe( + map((prompts: BackendChatEntity[]) => { + return prompts.map((prompt): PromptInfo => { + const relativePath = prompt.parentPath || undefined; + const promptInfo = parsePromptApiKey(prompt.name); + + return { + ...promptInfo, + folderId: relativePath, + }; + }); + }), + ); + } + getPrompt(info: PromptInfo, path?: string | undefined): Observable { + const key = getPromptApiKey(info); + return ApiStorage.request( + `api/${ApiKeys.Prompts}/${DataService.getBucket()}/${key}`, + ).pipe( + map((prompt: Prompt) => { + return { + ...info, + ...prompt, + uploaded: true, + }; + }), + ); } - setPrompts(_prompts: Prompt[]): Observable { - throw new Error('Method not implemented.'); + return of(); //TODO } } diff --git a/src/utils/app/data/storages/browser-storage.ts b/src/utils/app/data/storages/browser-storage.ts index e8b77458a4..e573b7f305 100644 --- a/src/utils/app/data/storages/browser-storage.ts +++ b/src/utils/app/data/storages/browser-storage.ts @@ -1,11 +1,15 @@ /* eslint-disable no-restricted-globals */ import toast from 'react-hot-toast'; + + import { Observable, map, of, switchMap, throwError } from 'rxjs'; -import { Conversation } from '@/src/types/chat'; + + +import { Conversation, ConversationInfo } from '@/src/types/chat'; import { FolderInterface, FolderType } from '@/src/types/folder'; -import { Prompt } from '@/src/types/prompt'; +import { Prompt, PromptInfo } from '@/src/types/prompt'; import { DialStorage, UIStorageKeys } from '@/src/types/storage'; import { errorsMessages } from '@/src/constants/errors'; @@ -14,6 +18,15 @@ import { cleanConversationHistory } from '../../clean'; import { isLocalStorageEnabled } from '../storage'; export class BrowserStorage implements DialStorage { + getConversation( + _info: ConversationInfo, + _path?: string | undefined, + ): Observable { + throw new Error('Method not implemented.'); + } + getPrompt(_info: PromptInfo, _path?: string | undefined): Observable { + throw new Error('Method not implemented.'); + } private static storage: globalThis.Storage | undefined; public static init() { @@ -100,7 +113,7 @@ export class BrowserStorage implements DialStorage { return of( value === null || value === undefined ? defaultValue - : JSON.parse(value), + : { ...JSON.parse(value), uploaded: true }, ); } catch (e: unknown) { console.error(e); diff --git a/src/utils/server/api.ts b/src/utils/server/api.ts index a3e1ae2691..ead741f4a7 100644 --- a/src/utils/server/api.ts +++ b/src/utils/server/api.ts @@ -1,10 +1,15 @@ import { NextApiRequest } from 'next'; + + import { Conversation, ConversationInfo } from '@/src/types/chat'; import { Prompt, PromptInfo } from '@/src/types/prompt'; + + import { OpenAIError } from './types'; + export enum ApiKeys { Files = 'files', Conversations = 'conversations', @@ -86,7 +91,7 @@ export const parseConversationApiKey = (apiKey: string): ConversationInfo => { }; // Format key: {id:guid}__{name:base64} -export const getPromptApiKey = (prompt: Prompt | PromptInfo) => { +export const getPromptApiKey = (prompt: PromptInfo) => { return [prompt.id, btoa(prompt.name)].join(pathKeySeparator); }; From 79653fe8e73e609c4204e6400645ab7cc1c84e1e Mon Sep 17 00:00:00 2001 From: Ilya Bondar Date: Mon, 29 Jan 2024 18:10:01 +0100 Subject: [PATCH 025/202] generic storage and refactoring --- src/types/chat.ts | 4 +--- src/types/prompt.ts | 3 +-- src/types/storage.ts | 12 ++++++++++++ src/utils/app/data/storages/api-storage.ts | 4 ++-- src/utils/app/data/storages/browser-storage.ts | 4 ---- src/utils/server/api.ts | 11 +++-------- 6 files changed, 19 insertions(+), 19 deletions(-) diff --git a/src/types/chat.ts b/src/types/chat.ts index 6c6d23d4f5..d16f754475 100644 --- a/src/types/chat.ts +++ b/src/types/chat.ts @@ -1,7 +1,6 @@ import { Entity, ShareEntity } from './common'; import { MIMEType } from './files'; - export interface Attachment { index?: number; type: MIMEType; @@ -71,7 +70,6 @@ export interface RateBody { export interface Conversation extends ShareEntity, ConversationInfo { messages: Message[]; - model: ConversationEntityModel; prompt: string; temperature: number; replay: Replay; @@ -115,7 +113,7 @@ export interface ConversationEntityModel { } export interface ConversationInfo extends Entity { - modelId: string; + model: ConversationEntityModel; isPlayback?: boolean; isReplay?: boolean; diff --git a/src/types/prompt.ts b/src/types/prompt.ts index ca590d5cd7..0ca2267ddf 100644 --- a/src/types/prompt.ts +++ b/src/types/prompt.ts @@ -1,6 +1,5 @@ import { Entity, ShareEntity } from './common'; - export interface Prompt extends ShareEntity, PromptInfo { description?: string; content?: string; @@ -8,4 +7,4 @@ export interface Prompt extends ShareEntity, PromptInfo { export interface PromptInfo extends Entity { uploaded?: boolean; -}; +} diff --git a/src/types/storage.ts b/src/types/storage.ts index 1fcaa6538e..455c7a2dda 100644 --- a/src/types/storage.ts +++ b/src/types/storage.ts @@ -28,6 +28,18 @@ export enum UIStorageKeys { TextOfClosedAnnouncement = 'textOfClosedAnnouncement', } +export interface EntityStorage { + getEntities(path?: string): EntityInfo[]; + + getEntity(info: EntityInfo): Entity; + + createEntity(info: EntityInfo): void; + + updateEntity(info: EntityInfo): void; + + deleteEntity(info: EntityInfo): void; +} + export interface DialStorage { getConversationsFolders(): Observable; diff --git a/src/utils/app/data/storages/api-storage.ts b/src/utils/app/data/storages/api-storage.ts index 66f5266052..41c40c6e3b 100644 --- a/src/utils/app/data/storages/api-storage.ts +++ b/src/utils/app/data/storages/api-storage.ts @@ -133,7 +133,7 @@ export class ApiStorage implements DialStorage { } getConversation( info: ConversationInfo, - path?: string | undefined, + _path?: string | undefined, ): Observable { const key = getConversationApiKeyFromConversationInfo(info); return ApiStorage.request( @@ -177,7 +177,7 @@ export class ApiStorage implements DialStorage { }), ); } - getPrompt(info: PromptInfo, path?: string | undefined): Observable { + getPrompt(info: PromptInfo, _path?: string | undefined): Observable { const key = getPromptApiKey(info); return ApiStorage.request( `api/${ApiKeys.Prompts}/${DataService.getBucket()}/${key}`, diff --git a/src/utils/app/data/storages/browser-storage.ts b/src/utils/app/data/storages/browser-storage.ts index e573b7f305..6d621737b2 100644 --- a/src/utils/app/data/storages/browser-storage.ts +++ b/src/utils/app/data/storages/browser-storage.ts @@ -1,12 +1,8 @@ /* eslint-disable no-restricted-globals */ import toast from 'react-hot-toast'; - - import { Observable, map, of, switchMap, throwError } from 'rxjs'; - - import { Conversation, ConversationInfo } from '@/src/types/chat'; import { FolderInterface, FolderType } from '@/src/types/folder'; import { Prompt, PromptInfo } from '@/src/types/prompt'; diff --git a/src/utils/server/api.ts b/src/utils/server/api.ts index ead741f4a7..a2a8e6b6e0 100644 --- a/src/utils/server/api.ts +++ b/src/utils/server/api.ts @@ -1,15 +1,10 @@ import { NextApiRequest } from 'next'; - - import { Conversation, ConversationInfo } from '@/src/types/chat'; -import { Prompt, PromptInfo } from '@/src/types/prompt'; - - +import { PromptInfo } from '@/src/types/prompt'; import { OpenAIError } from './types'; - export enum ApiKeys { Files = 'files', Conversations = 'conversations', @@ -68,7 +63,7 @@ export const getConversationApiKeyFromConversation = ( export const getConversationApiKeyFromConversationInfo = ( conversation: ConversationInfo, ) => { - return [conversation.id, conversation.modelId, btoa(conversation.name)].join( + return [conversation.id, conversation.model.id, btoa(conversation.name)].join( pathKeySeparator, ); }; @@ -83,7 +78,7 @@ export const parseConversationApiKey = (apiKey: string): ConversationInfo => { return { id, - modelId, + model: { id: modelId }, name: atob(encodedName), isPlayback: modelId === PseudoModel.Playback, isReplay: modelId === PseudoModel.Replay, From 5d4c4f8dbf1eed7b4ccc3636467e9fe7ea49df37 Mon Sep 17 00:00:00 2001 From: Ilya Bondar Date: Mon, 29 Jan 2024 18:37:59 +0100 Subject: [PATCH 026/202] generic storage --- src/types/storage.ts | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/src/types/storage.ts b/src/types/storage.ts index 455c7a2dda..f75f1c47c5 100644 --- a/src/types/storage.ts +++ b/src/types/storage.ts @@ -1,11 +1,16 @@ import { Observable } from 'rxjs'; + + import { Conversation } from '@/src/types/chat'; + + import { ConversationInfo } from './chat'; import { FolderInterface } from './folder'; import { Prompt, PromptInfo } from './prompt'; + export enum StorageType { BrowserStorage = 'browserStorage', API = 'api', @@ -29,15 +34,19 @@ export enum UIStorageKeys { } export interface EntityStorage { - getEntities(path?: string): EntityInfo[]; + getEntities(path: string): Observable; + + getEntity(info: EntityInfo): Observable; + + createEntity(info: EntityInfo): Observable; - getEntity(info: EntityInfo): Entity; + updateEntity(info: EntityInfo): Observable; - createEntity(info: EntityInfo): void; + deleteEntity(info: EntityInfo): Observable; - updateEntity(info: EntityInfo): void; + getKey(info: EntityInfo): string; - deleteEntity(info: EntityInfo): void; + parseKey(key: string): EntityInfo; } export interface DialStorage { From ac865c6b8ee591a7045629f75d016ac8e115cba6 Mon Sep 17 00:00:00 2001 From: Ilya Bondar Date: Mon, 29 Jan 2024 18:48:13 +0100 Subject: [PATCH 027/202] generic storage --- src/types/storage.ts | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/src/types/storage.ts b/src/types/storage.ts index f75f1c47c5..cb0abe0be9 100644 --- a/src/types/storage.ts +++ b/src/types/storage.ts @@ -1,16 +1,11 @@ import { Observable } from 'rxjs'; - - import { Conversation } from '@/src/types/chat'; - - import { ConversationInfo } from './chat'; import { FolderInterface } from './folder'; import { Prompt, PromptInfo } from './prompt'; - export enum StorageType { BrowserStorage = 'browserStorage', API = 'api', @@ -34,7 +29,7 @@ export enum UIStorageKeys { } export interface EntityStorage { - getEntities(path: string): Observable; + getEntities(path?: string): Observable; // listing with short information getEntity(info: EntityInfo): Observable; @@ -44,9 +39,11 @@ export interface EntityStorage { deleteEntity(info: EntityInfo): Observable; - getKey(info: EntityInfo): string; + getEntityKey(info: EntityInfo): string; + + parseEntityKey(key: string): EntityInfo; - parseKey(key: string): EntityInfo; + getStorageKey(): string; // e.g. ApiKeys or `conversationHistory`/`prompts` in case of localStorage } export interface DialStorage { From ea8663a879f43a9a44f71b35181405e23eaf64b5 Mon Sep 17 00:00:00 2001 From: Ilya Bondar Date: Mon, 29 Jan 2024 19:11:04 +0100 Subject: [PATCH 028/202] Update storage.ts --- src/types/storage.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/types/storage.ts b/src/types/storage.ts index cb0abe0be9..776bf9ded4 100644 --- a/src/types/storage.ts +++ b/src/types/storage.ts @@ -28,14 +28,14 @@ export enum UIStorageKeys { TextOfClosedAnnouncement = 'textOfClosedAnnouncement', } -export interface EntityStorage { +export interface EntityStorage { getEntities(path?: string): Observable; // listing with short information getEntity(info: EntityInfo): Observable; - createEntity(info: EntityInfo): Observable; + createEntity(entity: Entity): Observable; - updateEntity(info: EntityInfo): Observable; + updateEntity(entity: Entity): Observable; deleteEntity(info: EntityInfo): Observable; From 7d7abc6956fe7137fb249d6aaebf81ee09260fa7 Mon Sep 17 00:00:00 2001 From: Ilya Bondar Date: Mon, 29 Jan 2024 19:43:01 +0100 Subject: [PATCH 029/202] draft api --- src/utils/app/data/data-service.ts | 13 +- src/utils/app/data/fileService.ts | 15 +- .../app/data/storages/api-entity-storage.ts | 84 +++++++++ src/utils/app/data/storages/api-storage.ts | 171 ++---------------- .../data/storages/conversation-api-storage.ts | 24 +++ .../app/data/storages/prompt-api-storage.ts | 21 +++ src/utils/server/api.ts | 67 +++++++ 7 files changed, 219 insertions(+), 176 deletions(-) create mode 100644 src/utils/app/data/storages/api-entity-storage.ts create mode 100644 src/utils/app/data/storages/conversation-api-storage.ts create mode 100644 src/utils/app/data/storages/prompt-api-storage.ts diff --git a/src/utils/app/data/data-service.ts b/src/utils/app/data/data-service.ts index fb1672b025..a6a9227ef5 100644 --- a/src/utils/app/data/data-service.ts +++ b/src/utils/app/data/data-service.ts @@ -3,14 +3,15 @@ import { Observable, map } from 'rxjs'; import { isSmallScreen } from '@/src/utils/app/mobile'; -import { Conversation } from '@/src/types/chat'; +import { Conversation, ConversationInfo } from '@/src/types/chat'; import { FolderInterface } from '@/src/types/folder'; -import { Prompt } from '@/src/types/prompt'; +import { Prompt, PromptInfo } from '@/src/types/prompt'; import { DialStorage, StorageType, UIStorageKeys } from '@/src/types/storage'; import { Theme } from '@/src/types/themes'; import { SIDEBAR_MIN_WIDTH } from '@/src/constants/default-ui-settings'; +import { ApiUtils } from '../../server/api'; import { FileService } from './fileService'; import { ApiStorage } from './storages/api-storage'; import { BrowserStorage } from './storages/browser-storage'; @@ -46,7 +47,7 @@ export class DataService extends FileService { return this.getDataStorage().setPromptsFolders(folders); } - public static getPrompts(): Observable { + public static getPrompts(): Observable { return this.getDataStorage().getPrompts(); } @@ -54,7 +55,7 @@ export class DataService extends FileService { return this.getDataStorage().setPrompts(prompts); } - public static getConversations(): Observable { + public static getConversations(): Observable { return this.getDataStorage().getConversations(); } @@ -114,7 +115,7 @@ export class DataService extends FileService { } public static getAvailableThemes(): Observable { - return ApiStorage.request('api/themes/listing'); + return ApiUtils.request('api/themes/listing'); } public static getChatbarWidth(): Observable { @@ -196,7 +197,7 @@ export class DataService extends FileService { } public static requestBucket(): Observable<{ bucket: string }> { - return ApiStorage.request(`api/bucket`, { + return ApiUtils.request(`api/bucket`, { method: 'GET', headers: { 'Content-Type': 'application/json', diff --git a/src/utils/app/data/fileService.ts b/src/utils/app/data/fileService.ts index b06fab85a5..b1b0bdacf5 100644 --- a/src/utils/app/data/fileService.ts +++ b/src/utils/app/data/fileService.ts @@ -11,9 +11,8 @@ import { } from '@/src/types/files'; import { FolderType } from '@/src/types/folder'; -import { ApiKeys } from '../../server/api'; +import { ApiKeys, ApiUtils } from '../../server/api'; import { constructPath } from '../file'; -import { ApiStorage } from './storages/api-storage'; export class FileService { public static sendFile( @@ -27,7 +26,7 @@ export class FileService { }${fileName}`, ); - return ApiStorage.requestOld({ + return ApiUtils.requestOld({ url: `api/${ApiKeys.Files}/${resultPath}`, method: 'PUT', async: true, @@ -85,9 +84,7 @@ export class FileService { }); const resultQuery = query.toString(); - return ApiStorage.request( - `api/${ApiKeys.Files}/listing?${resultQuery}`, - ).pipe( + return ApiUtils.request(`api/${ApiKeys.Files}/listing?${resultQuery}`).pipe( map((folders: BackendFileFolder[]) => { return folders.map((folder): FileFolderInterface => { const relativePath = folder.parentPath || undefined; @@ -115,7 +112,7 @@ export class FileService { constructPath('files', DataService.getBucket(), filePath), ); - return ApiStorage.request(`api/${ApiKeys.Files}/${resultPath}`, { + return ApiUtils.request(`api/${ApiKeys.Files}/${resultPath}`, { method: 'DELETE', headers: { 'Content-Type': 'application/json', @@ -133,9 +130,7 @@ export class FileService { }); const resultQuery = query.toString(); - return ApiStorage.request( - `api/${ApiKeys.Files}/listing?${resultQuery}`, - ).pipe( + return ApiUtils.request(`api/${ApiKeys.Files}/listing?${resultQuery}`).pipe( map((files: BackendFile[]) => { return files.map((file): DialFile => { const relativePath = file.parentPath || undefined; diff --git a/src/utils/app/data/storages/api-entity-storage.ts b/src/utils/app/data/storages/api-entity-storage.ts new file mode 100644 index 0000000000..0b65af639c --- /dev/null +++ b/src/utils/app/data/storages/api-entity-storage.ts @@ -0,0 +1,84 @@ +import { Observable, map } from 'rxjs'; + +import { ApiUtils } from '@/src/utils/server/api'; + +import { BackendChatEntity, BackendDataNodeType } from '@/src/types/common'; +import { EntityStorage } from '@/src/types/storage'; + +import { DataService } from '../data-service'; + +export abstract class ApiEntityStorage + implements EntityStorage +{ + getEntities(path?: string | undefined): Observable { + const filter = BackendDataNodeType.ITEM; + + const query = new URLSearchParams({ + filter, + bucket: DataService.getBucket(), + ...(path && { path }), + }); + const resultQuery = query.toString(); + + return ApiUtils.request( + `api/${this.getStorageKey()}/listing?${resultQuery}`, + ).pipe( + map((conversations: BackendChatEntity[]) => { + return conversations.map((conversation): EntityInfo => { + const relativePath = conversation.parentPath || undefined; + const info = this.parseEntityKey(conversation.name); + + return { + ...info, + folderId: relativePath, + }; + }); + }), + ); + } + getEntity(info: EntityInfo): Observable { + const key = this.getEntityKey(info); + return ApiUtils.request( + `api/${this.getStorageKey()}/${DataService.getBucket()}/${key}`, + ).pipe( + map((entity: Entity) => { + return { + ...info, + ...entity, + uploaded: true, + }; + }), + ); + } + createEntity(entity: Entity): Observable { + const key = this.getEntityKey(entity); + return ApiUtils.request( + `api/${this.getStorageKey()}/${DataService.getBucket()}/${key}`, + { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(entity), + }, + ); + } + updateEntity(entity: Entity): Observable { + return this.createEntity(entity); + } + deleteEntity(info: EntityInfo): Observable { + const key = this.getEntityKey(info); + return ApiUtils.request( + `api/${this.getStorageKey()}/${DataService.getBucket()}/${key}`, + { + method: 'DELETE', + headers: { + 'Content-Type': 'application/json', + }, + }, + ); + } + abstract getEntityKey(info: EntityInfo): string; + abstract parseEntityKey(key: string): EntityInfo; + abstract getStorageKey(): string; +} diff --git a/src/utils/app/data/storages/api-storage.ts b/src/utils/app/data/storages/api-storage.ts index 41c40c6e3b..10412c3c03 100644 --- a/src/utils/app/data/storages/api-storage.ts +++ b/src/utils/app/data/storages/api-storage.ts @@ -1,195 +1,46 @@ -import { Observable, from, map, of, switchMap, throwError } from 'rxjs'; -import { fromFetch } from 'rxjs/fetch'; - -import { - ApiKeys, - getConversationApiKeyFromConversationInfo, - getPromptApiKey, - parseConversationApiKey, - parsePromptApiKey, -} from '@/src/utils/server/api'; +import { Observable, of } from 'rxjs'; import { Conversation, ConversationInfo } from '@/src/types/chat'; -import { BackendChatEntity, BackendDataNodeType } from '@/src/types/common'; import { FolderInterface } from '@/src/types/folder'; import { Prompt, PromptInfo } from '@/src/types/prompt'; import { DialStorage } from '@/src/types/storage'; -import { constructPath } from '../../file'; -import { DataService } from '../data-service'; +import { ConversationApiStorage } from './conversation-api-storage'; +import { PromptApiStorage } from './prompt-api-storage'; export class ApiStorage implements DialStorage { - static request(url: string, options?: RequestInit) { - return fromFetch(url, options).pipe( - switchMap((response) => { - if (!response.ok) { - return throwError(() => new Error(response.statusText)); - } - - return from(response.json()); - }), - ); - } - static requestOld({ - url, - method, - async, - body, - }: { - url: string | URL; - method: string; - async: boolean; - body: XMLHttpRequestBodyInit | Document | null | undefined; - }): Observable<{ percent?: number; result?: unknown }> { - return new Observable((observer) => { - const xhr = new XMLHttpRequest(); - - xhr.open(method, url, async); - xhr.responseType = 'json'; - - // Track upload progress - xhr.upload.onprogress = (event) => { - if (event.lengthComputable) { - const percentComplete = (event.loaded / event.total) * 100; - observer.next({ percent: Math.round(percentComplete) }); - } - }; - - // Handle response - xhr.onload = () => { - if (xhr.status === 200) { - observer.next({ result: xhr.response }); - observer.complete(); - } else { - observer.error('Request failed'); - } - }; + private conversationStorage = new ConversationApiStorage(); + private promptStorage = new PromptApiStorage(); - xhr.onerror = () => { - observer.error('Request failed'); - }; - - xhr.send(body); - - // Return cleanup function - return () => { - xhr.abort(); - }; - }); - } getConversationsFolders(): Observable { return of(); //TODO } setConversationsFolders(_folders: FolderInterface[]): Observable { - const resultPath = encodeURI(constructPath(DataService.getBucket(), '')); - - const response = ApiStorage.request( - `api/${ApiKeys.Conversations}/${resultPath}`, - ); - - // eslint-disable-next-line no-console - console.log(response); - - return response; + return of(); //TODO } getPromptsFolders(): Observable { - const resultPath = encodeURI(constructPath(DataService.getBucket(), '')); - - const response = ApiStorage.request(`api/${ApiKeys.Prompts}/${resultPath}`); - - // eslint-disable-next-line no-console - console.log(response); - - return response; + return of(); //TODO } setPromptsFolders(_folders: FolderInterface[]): Observable { return of(); //TODO } getConversations(path?: string): Observable { - const filter = BackendDataNodeType.ITEM; - - const query = new URLSearchParams({ - filter, - bucket: DataService.getBucket(), - ...(path && { path }), - }); - const resultQuery = query.toString(); - - return ApiStorage.request( - `api/${ApiKeys.Conversations}/listing?${resultQuery}`, - ).pipe( - map((conversations: BackendChatEntity[]) => { - return conversations.map((conversation): ConversationInfo => { - const relativePath = conversation.parentPath || undefined; - const conversationInfo = parseConversationApiKey(conversation.name); - - return { - ...conversationInfo, - folderId: relativePath, - }; - }); - }), - ); + return this.conversationStorage.getEntities(path); } getConversation( info: ConversationInfo, _path?: string | undefined, ): Observable { - const key = getConversationApiKeyFromConversationInfo(info); - return ApiStorage.request( - `api/${ApiKeys.Conversations}/${DataService.getBucket()}/${key}`, - ).pipe( - map((conversation: Conversation) => { - return { - ...info, - ...conversation, - uploaded: true, - }; - }), - ); + return this.conversationStorage.getEntity(info); } setConversations(_conversations: Conversation[]): Observable { return of(); //TODO } getPrompts(path?: string): Observable { - const filter = BackendDataNodeType.ITEM; - - const query = new URLSearchParams({ - filter, - bucket: DataService.getBucket(), - ...(path && { path }), - }); - const resultQuery = query.toString(); - - return ApiStorage.request( - `api/${ApiKeys.Prompts}/listing?${resultQuery}`, - ).pipe( - map((prompts: BackendChatEntity[]) => { - return prompts.map((prompt): PromptInfo => { - const relativePath = prompt.parentPath || undefined; - const promptInfo = parsePromptApiKey(prompt.name); - - return { - ...promptInfo, - folderId: relativePath, - }; - }); - }), - ); + return this.promptStorage.getEntities(path); } getPrompt(info: PromptInfo, _path?: string | undefined): Observable { - const key = getPromptApiKey(info); - return ApiStorage.request( - `api/${ApiKeys.Prompts}/${DataService.getBucket()}/${key}`, - ).pipe( - map((prompt: Prompt) => { - return { - ...info, - ...prompt, - uploaded: true, - }; - }), - ); + return this.promptStorage.getEntity(info); } setPrompts(_prompts: Prompt[]): Observable { return of(); //TODO diff --git a/src/utils/app/data/storages/conversation-api-storage.ts b/src/utils/app/data/storages/conversation-api-storage.ts new file mode 100644 index 0000000000..c89aca2141 --- /dev/null +++ b/src/utils/app/data/storages/conversation-api-storage.ts @@ -0,0 +1,24 @@ +import { + ApiKeys, + getConversationApiKeyFromConversationInfo, + parseConversationApiKey, +} from '@/src/utils/server/api'; + +import { Conversation, ConversationInfo } from '@/src/types/chat'; + +import { ApiEntityStorage } from './api-entity-storage'; + +export class ConversationApiStorage extends ApiEntityStorage< + ConversationInfo, + Conversation +> { + getEntityKey(info: ConversationInfo): string { + return getConversationApiKeyFromConversationInfo(info); + } + parseEntityKey(key: string): ConversationInfo { + return parseConversationApiKey(key); + } + getStorageKey(): string { + return ApiKeys.Conversations; + } +} diff --git a/src/utils/app/data/storages/prompt-api-storage.ts b/src/utils/app/data/storages/prompt-api-storage.ts new file mode 100644 index 0000000000..e829ded269 --- /dev/null +++ b/src/utils/app/data/storages/prompt-api-storage.ts @@ -0,0 +1,21 @@ +import { + ApiKeys, + getPromptApiKey, + parsePromptApiKey, +} from '@/src/utils/server/api'; + +import { Prompt, PromptInfo } from '@/src/types/prompt'; + +import { ApiEntityStorage } from './api-entity-storage'; + +export class PromptApiStorage extends ApiEntityStorage { + getEntityKey(info: PromptInfo): string { + return getPromptApiKey(info); + } + parseEntityKey(key: string): PromptInfo { + return parsePromptApiKey(key); + } + getStorageKey(): string { + return ApiKeys.Prompts; + } +} diff --git a/src/utils/server/api.ts b/src/utils/server/api.ts index a2a8e6b6e0..7a38f7c26b 100644 --- a/src/utils/server/api.ts +++ b/src/utils/server/api.ts @@ -1,5 +1,8 @@ import { NextApiRequest } from 'next'; +import { Observable, from, switchMap, throwError } from 'rxjs'; +import { fromFetch } from 'rxjs/fetch'; + import { Conversation, ConversationInfo } from '@/src/types/chat'; import { PromptInfo } from '@/src/types/prompt'; @@ -103,3 +106,67 @@ export const parsePromptApiKey = (apiKey: string): PromptInfo => { name: atob(encodedName), }; }; + +export class ApiUtils { + static request(url: string, options?: RequestInit) { + return fromFetch(url, options).pipe( + switchMap((response) => { + if (response.status === 404) { + return []; + } else if (!response.ok) { + return throwError(() => new Error(response.statusText)); + } + + return from(response.json()); + }), + ); + } + + static requestOld({ + url, + method, + async, + body, + }: { + url: string | URL; + method: string; + async: boolean; + body: XMLHttpRequestBodyInit | Document | null | undefined; + }): Observable<{ percent?: number; result?: unknown }> { + return new Observable((observer) => { + const xhr = new XMLHttpRequest(); + + xhr.open(method, url, async); + xhr.responseType = 'json'; + + // Track upload progress + xhr.upload.onprogress = (event) => { + if (event.lengthComputable) { + const percentComplete = (event.loaded / event.total) * 100; + observer.next({ percent: Math.round(percentComplete) }); + } + }; + + // Handle response + xhr.onload = () => { + if (xhr.status === 200) { + observer.next({ result: xhr.response }); + observer.complete(); + } else { + observer.error('Request failed'); + } + }; + + xhr.onerror = () => { + observer.error('Request failed'); + }; + + xhr.send(body); + + // Return cleanup function + return () => { + xhr.abort(); + }; + }); + } +} From 7b8bb680e617f67ed33a0cdab3c14e44ad619859 Mon Sep 17 00:00:00 2001 From: Aliaksandr Kezik Date: Mon, 29 Jan 2024 20:16:13 +0100 Subject: [PATCH 030/202] set entities draft --- src/types/common.ts | 21 ++ src/types/files.ts | 27 +- src/utils/app/data/storages/api-storage.ts | 273 +++++++++++++++++++-- 3 files changed, 290 insertions(+), 31 deletions(-) diff --git a/src/types/common.ts b/src/types/common.ts index d71f2c8c94..ed0f463e82 100644 --- a/src/types/common.ts +++ b/src/types/common.ts @@ -1,3 +1,5 @@ +import { BackendFile } from '@/src/types/files'; + import { ShareInterface } from './share'; export enum EntityType { @@ -55,3 +57,22 @@ export interface BackendFolder extends BackendDataEntity { export type BackendChatFolder = BackendFolder< BackendChatEntity | BackendChatFolder >; + +export interface BaseDialEntity { + // Combination of relative path and name + id: string; + // Only for files fetched uploaded to backend + // Same as relative path but has some absolute prefix like + absolutePath?: string; + relativePath?: string; + // Same as relative path, but needed for simplicity and backward compatibility + folderId?: string; + serverSynced?: boolean; + status?: 'UPLOADING' | 'FAILED'; +}; + +export type DialChatEntity = Omit< + BackendChatEntity, + 'path' | 'nodeType' | 'resourceType' | 'bucket' | 'parentPath' | 'url' +> & + BaseDialEntity; diff --git a/src/types/files.ts b/src/types/files.ts index fef97c6660..8543836a59 100644 --- a/src/types/files.ts +++ b/src/types/files.ts @@ -1,4 +1,8 @@ -import { BackendEntity, BackendFolder } from '@/src/types/common'; +import { + BackendEntity, + BackendFolder, + BaseDialEntity, +} from '@/src/types/common'; import { FolderInterface } from './folder'; @@ -21,21 +25,12 @@ export type BackendFileFolder = BackendFolder; export type DialFile = Omit< BackendFile, 'path' | 'nodeType' | 'resourceType' | 'bucket' | 'parentPath' | 'url' -> & { - // Combination of relative path and name - id: string; - // Only for files fetched uploaded to backend - // Same as relative path but has some absolute prefix like - absolutePath?: string; - relativePath?: string; - // Same as relative path, but needed for simplicity and backward compatibility - folderId?: string; - - status?: 'UPLOADING' | 'FAILED'; - percent?: number; - fileContent?: File; - serverSynced?: boolean; -}; +> & + BaseDialEntity & { + status?: 'UPLOADING' | 'FAILED'; + percent?: number; + fileContent?: File; + }; // For file folders folderId is relative path and id is relative path + '/' + name export type FileFolderInterface = FolderInterface & { diff --git a/src/utils/app/data/storages/api-storage.ts b/src/utils/app/data/storages/api-storage.ts index 10412c3c03..f73c6bd01f 100644 --- a/src/utils/app/data/storages/api-storage.ts +++ b/src/utils/app/data/storages/api-storage.ts @@ -1,48 +1,291 @@ -import { Observable, of } from 'rxjs'; +import { + Observable, + from, + map, + mergeMap, + of, + switchMap, + throwError, + toArray, +} from 'rxjs'; +import { fromFetch } from 'rxjs/fetch'; + +import { + ApiKeys, + getConversationApiKeyFromConversation, + getConversationApiKeyFromConversationInfo, + getPromptApiKey, + parseConversationApiKey, + parsePromptApiKey, +} from '@/src/utils/server/api'; import { Conversation, ConversationInfo } from '@/src/types/chat'; +import { + BackendChatEntity, + BackendDataNodeType, + DialChatEntity, +} from '@/src/types/common'; import { FolderInterface } from '@/src/types/folder'; import { Prompt, PromptInfo } from '@/src/types/prompt'; import { DialStorage } from '@/src/types/storage'; -import { ConversationApiStorage } from './conversation-api-storage'; -import { PromptApiStorage } from './prompt-api-storage'; +import { constructPath } from '../../file'; +import { DataService } from '../data-service'; export class ApiStorage implements DialStorage { - private conversationStorage = new ConversationApiStorage(); - private promptStorage = new PromptApiStorage(); + static request(url: string, options?: RequestInit) { + return fromFetch(url, options).pipe( + switchMap((response) => { + if (!response.ok) { + return throwError(() => new Error(response.statusText)); + } + + return from(response.json()); + }), + ); + } + + static requestOld({ + url, + method, + async, + body, + }: { + url: string | URL; + method: string; + async: boolean; + body: XMLHttpRequestBodyInit | Document | null | undefined; + }): Observable<{ percent?: number; result?: unknown }> { + return new Observable((observer) => { + const xhr = new XMLHttpRequest(); + + xhr.open(method, url, async); + xhr.responseType = 'json'; + + // Track upload progress + xhr.upload.onprogress = (event) => { + if (event.lengthComputable) { + const percentComplete = (event.loaded / event.total) * 100; + observer.next({ percent: Math.round(percentComplete) }); + } + }; + + // Handle response + xhr.onload = () => { + if (xhr.status === 200) { + observer.next({ result: xhr.response }); + observer.complete(); + } else { + observer.error('Request failed'); + } + }; + + xhr.onerror = () => { + observer.error('Request failed'); + }; + + xhr.send(body); + + // Return cleanup function + return () => { + xhr.abort(); + }; + }); + } + + static setData( + entity: Conversation | Prompt, + entityType: ApiKeys, + relativePath: string | undefined, + entityId: string, + ): Observable<{ result?: DialChatEntity }> { + const resultPath = encodeURI( + `${entityType}/${DataService.getBucket()}/${ + relativePath ? `${relativePath}/` : '' + }${entityId}`, + ); + + return ApiStorage.requestOld({ + url: `api/${entityType}/${resultPath}`, + method: 'PUT', + async: true, + body: JSON.stringify(entity), + }).pipe( + map(({ result }: { result?: unknown }): { result?: DialChatEntity } => { + if (!result) { + return {}; + } + + const typedResult = result as BackendChatEntity; + const relativePath = typedResult.parentPath || undefined; + + return { + result: { + id: constructPath(relativePath, typedResult.name), + name: typedResult.name, + absolutePath: constructPath( + entityType, + typedResult.bucket, + relativePath, + ), + relativePath: relativePath, + folderId: relativePath, + updatedAt: typedResult.updatedAt, + serverSynced: true, + }, + }; + }), + ); + } getConversationsFolders(): Observable { return of(); //TODO } + setConversationsFolders(_folders: FolderInterface[]): Observable { - return of(); //TODO + const resultPath = encodeURI(constructPath(DataService.getBucket(), '')); + + const response = ApiStorage.request( + `api/${ApiKeys.Conversations}/${resultPath}`, + ); + + // eslint-disable-next-line no-console + console.log(response); + + return response; } + getPromptsFolders(): Observable { - return of(); //TODO + const resultPath = encodeURI(constructPath(DataService.getBucket(), '')); + + const response = ApiStorage.request(`api/${ApiKeys.Prompts}/${resultPath}`); + + // eslint-disable-next-line no-console + console.log(response); + + return response; } + setPromptsFolders(_folders: FolderInterface[]): Observable { return of(); //TODO } + getConversations(path?: string): Observable { - return this.conversationStorage.getEntities(path); + const filter = BackendDataNodeType.ITEM; + + const query = new URLSearchParams({ + filter, + bucket: DataService.getBucket(), + ...(path && { path }), + }); + const resultQuery = query.toString(); + + return ApiStorage.request( + `api/${ApiKeys.Conversations}/listing?${resultQuery}`, + ).pipe( + map((conversations: BackendChatEntity[]) => { + return conversations.map((conversation): ConversationInfo => { + const relativePath = conversation.parentPath || undefined; + const conversationInfo = parseConversationApiKey(conversation.name); + + return { + ...conversationInfo, + folderId: relativePath, + }; + }); + }), + ); } + getConversation( info: ConversationInfo, _path?: string | undefined, ): Observable { - return this.conversationStorage.getEntity(info); + const key = getConversationApiKeyFromConversationInfo(info); + return ApiStorage.request( + `api/${ApiKeys.Conversations}/${DataService.getBucket()}/${key}`, + ).pipe( + map((conversation: Conversation) => { + return { + ...info, + ...conversation, + uploaded: true, + }; + }), + ); } - setConversations(_conversations: Conversation[]): Observable { - return of(); //TODO + + setConversations( + _conversations: Conversation[], + ): Observable<{ result?: DialChatEntity | undefined }[]> { + return from(_conversations).pipe( + mergeMap((conversation) => + ApiStorage.setData( + conversation, + ApiKeys.Conversations, + undefined, // TODO: define relative path + getConversationApiKeyFromConversation(conversation), + ), + ), + toArray(), + ); } + getPrompts(path?: string): Observable { - return this.promptStorage.getEntities(path); + const filter = BackendDataNodeType.ITEM; + + const query = new URLSearchParams({ + filter, + bucket: DataService.getBucket(), + ...(path && { path }), + }); + const resultQuery = query.toString(); + + return ApiStorage.request( + `api/${ApiKeys.Prompts}/listing?${resultQuery}`, + ).pipe( + map((prompts: BackendChatEntity[]) => { + return prompts.map((prompt): PromptInfo => { + const relativePath = prompt.parentPath || undefined; + const promptInfo = parsePromptApiKey(prompt.name); + + return { + ...promptInfo, + folderId: relativePath, + }; + }); + }), + ); } + getPrompt(info: PromptInfo, _path?: string | undefined): Observable { - return this.promptStorage.getEntity(info); + const key = getPromptApiKey(info); + return ApiStorage.request( + `api/${ApiKeys.Prompts}/${DataService.getBucket()}/${key}`, + ).pipe( + map((prompt: Prompt) => { + return { + ...info, + ...prompt, + uploaded: true, + }; + }), + ); } - setPrompts(_prompts: Prompt[]): Observable { - return of(); //TODO + + setPrompts( + _prompts: Prompt[], + ): Observable<{ result?: DialChatEntity | undefined }[]> { + return from(_prompts).pipe( + mergeMap((prompt) => + ApiStorage.setData( + prompt, + ApiKeys.Prompts, + undefined, // TODO: define relative path + prompt.name, + ), + ), + toArray(), + ); } } From 85e3d1b9d5d80f5e9fe479ffd887b9074bf8f8f4 Mon Sep 17 00:00:00 2001 From: Aliaksandr Kezik Date: Mon, 29 Jan 2024 20:16:55 +0100 Subject: [PATCH 031/202] remove status from DialFile --- src/types/files.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/types/files.ts b/src/types/files.ts index 8543836a59..7baa0d0e6c 100644 --- a/src/types/files.ts +++ b/src/types/files.ts @@ -27,7 +27,6 @@ export type DialFile = Omit< 'path' | 'nodeType' | 'resourceType' | 'bucket' | 'parentPath' | 'url' > & BaseDialEntity & { - status?: 'UPLOADING' | 'FAILED'; percent?: number; fileContent?: File; }; From 5762e3855d40e121d89f81150bc55b847a0679cc Mon Sep 17 00:00:00 2001 From: Aliaksandr Kezik Date: Mon, 29 Jan 2024 22:39:54 +0100 Subject: [PATCH 032/202] refactor with ConversationsApiStorage and PromptApiStorage --- src/utils/app/data/storages/api-storage.ts | 84 +++------------------- 1 file changed, 10 insertions(+), 74 deletions(-) diff --git a/src/utils/app/data/storages/api-storage.ts b/src/utils/app/data/storages/api-storage.ts index f73c6bd01f..1b1653103e 100644 --- a/src/utils/app/data/storages/api-storage.ts +++ b/src/utils/app/data/storages/api-storage.ts @@ -6,13 +6,13 @@ import { of, switchMap, throwError, - toArray, } from 'rxjs'; import { fromFetch } from 'rxjs/fetch'; +import { ConversationApiStorage } from '@/src/utils/app/data/storages/conversation-api-storage'; +import { PromptApiStorage } from '@/src/utils/app/data/storages/prompt-api-storage'; import { ApiKeys, - getConversationApiKeyFromConversation, getConversationApiKeyFromConversationInfo, getPromptApiKey, parseConversationApiKey, @@ -20,11 +20,7 @@ import { } from '@/src/utils/server/api'; import { Conversation, ConversationInfo } from '@/src/types/chat'; -import { - BackendChatEntity, - BackendDataNodeType, - DialChatEntity, -} from '@/src/types/common'; +import { BackendChatEntity, BackendDataNodeType } from '@/src/types/common'; import { FolderInterface } from '@/src/types/folder'; import { Prompt, PromptInfo } from '@/src/types/prompt'; import { DialStorage } from '@/src/types/storage'; @@ -33,6 +29,9 @@ import { constructPath } from '../../file'; import { DataService } from '../data-service'; export class ApiStorage implements DialStorage { + private _conversationApiStorage = new ConversationApiStorage(); + private _promptApiStorage = new PromptApiStorage(); + static request(url: string, options?: RequestInit) { return fromFetch(url, options).pipe( switchMap((response) => { @@ -93,51 +92,6 @@ export class ApiStorage implements DialStorage { }); } - static setData( - entity: Conversation | Prompt, - entityType: ApiKeys, - relativePath: string | undefined, - entityId: string, - ): Observable<{ result?: DialChatEntity }> { - const resultPath = encodeURI( - `${entityType}/${DataService.getBucket()}/${ - relativePath ? `${relativePath}/` : '' - }${entityId}`, - ); - - return ApiStorage.requestOld({ - url: `api/${entityType}/${resultPath}`, - method: 'PUT', - async: true, - body: JSON.stringify(entity), - }).pipe( - map(({ result }: { result?: unknown }): { result?: DialChatEntity } => { - if (!result) { - return {}; - } - - const typedResult = result as BackendChatEntity; - const relativePath = typedResult.parentPath || undefined; - - return { - result: { - id: constructPath(relativePath, typedResult.name), - name: typedResult.name, - absolutePath: constructPath( - entityType, - typedResult.bucket, - relativePath, - ), - relativePath: relativePath, - folderId: relativePath, - updatedAt: typedResult.updatedAt, - serverSynced: true, - }, - }; - }), - ); - } - getConversationsFolders(): Observable { return of(); //TODO } @@ -215,19 +169,11 @@ export class ApiStorage implements DialStorage { ); } - setConversations( - _conversations: Conversation[], - ): Observable<{ result?: DialChatEntity | undefined }[]> { + setConversations(_conversations: Conversation[]): Observable { return from(_conversations).pipe( mergeMap((conversation) => - ApiStorage.setData( - conversation, - ApiKeys.Conversations, - undefined, // TODO: define relative path - getConversationApiKeyFromConversation(conversation), - ), + this._conversationApiStorage.createEntity(conversation), ), - toArray(), ); } @@ -273,19 +219,9 @@ export class ApiStorage implements DialStorage { ); } - setPrompts( - _prompts: Prompt[], - ): Observable<{ result?: DialChatEntity | undefined }[]> { + setPrompts(_prompts: Prompt[]): Observable { return from(_prompts).pipe( - mergeMap((prompt) => - ApiStorage.setData( - prompt, - ApiKeys.Prompts, - undefined, // TODO: define relative path - prompt.name, - ), - ), - toArray(), + mergeMap((prompt) => this._promptApiStorage.createEntity(prompt)), ); } } From 57cfb39889da1794cf23496585ac79aae6c19ef6 Mon Sep 17 00:00:00 2001 From: Ilya Bondar Date: Tue, 30 Jan 2024 00:07:21 +0100 Subject: [PATCH 033/202] using stateful api --- .../conversations/conversations.epics.ts | 532 +++++++++--------- .../conversations/conversations.reducers.ts | 17 +- .../conversations/conversations.types.ts | 4 +- src/types/chat.ts | 4 +- src/types/common.ts | 4 +- src/types/prompt.ts | 4 +- src/types/storage.ts | 12 +- src/utils/app/clean.ts | 2 +- .../app/data/storages/api-entity-storage.ts | 22 +- src/utils/app/data/storages/api-storage.ts | 221 +------- .../app/data/storages/browser-storage.ts | 29 +- .../data/storages/conversation-api-storage.ts | 4 +- src/utils/app/import-export.ts | 10 +- src/utils/server/api.ts | 25 +- 14 files changed, 356 insertions(+), 534 deletions(-) diff --git a/src/store/conversations/conversations.epics.ts b/src/store/conversations/conversations.epics.ts index e51124eacd..ccd2df24fb 100644 --- a/src/store/conversations/conversations.epics.ts +++ b/src/store/conversations/conversations.epics.ts @@ -37,12 +37,6 @@ import { isSettingsChanged, } from '@/src/utils/app/conversation'; import { DataService } from '@/src/utils/app/data/data-service'; -import { renameAttachments } from '@/src/utils/app/file'; -import { - findRootFromItems, - getFolderIdByPath, - getTemporaryFoldersToPublish, -} from '@/src/utils/app/folders'; import { ImportConversationsResponse, exportConversation, @@ -68,7 +62,6 @@ import { import { EntityType } from '@/src/types/common'; import { AppEpic } from '@/src/types/store'; -import { resetShareEntity } from '@/src/constants/chat'; import { DEFAULT_CONVERSATION_NAME } from '@/src/constants/default-settings'; import { errorsMessages } from '@/src/constants/errors'; @@ -80,8 +73,6 @@ import { ConversationsSelectors, } from './conversations.reducers'; -import { v4 as uuidv4 } from 'uuid'; - const createNewConversationEpic: AppEpic = (action$, state$) => action$.pipe( filter(ConversationsActions.createNewConversations.match), @@ -205,6 +196,7 @@ const importConversationsEpic: AppEpic = (action$, state$) => const currentFolders = ConversationsSelectors.selectFolders(state$.value); const { history, folders, isError }: ImportConversationsResponse = importConversations(payload.data, { + //TODO: save in API currentConversations, currentFolders, }); @@ -334,7 +326,9 @@ const rateMessageEpic: AppEpic = (action$, state$) => }), ); } - const message = conversation.messages[payload.messageIndex]; + const message = (conversation as Conversation).messages[ + payload.messageIndex + ]; if (!message || !message.responseId) { return of( @@ -388,7 +382,7 @@ const updateMessageEpic: AppEpic = (action$, state$) => switchMap(({ conversations, payload }) => { const conversation = conversations.find( (conv) => conv.id === payload.conversationId, - ); + ) as Conversation; if (!conversation || !conversation.messages[payload.messageIndex]) { return EMPTY; } @@ -1395,260 +1389,260 @@ const initEpic: AppEpic = (action$) => ), ); -//TODO: added for development purpose - emulate immediate sharing with yourself -const shareFolderEpic: AppEpic = (action$, state$) => - action$.pipe( - filter(ConversationsActions.shareFolder.match), - map(({ payload }) => ({ - sharedFolderId: payload.id, - shareUniqueId: payload.shareUniqueId, - conversations: ConversationsSelectors.selectConversations(state$.value), - childFolders: ConversationsSelectors.selectChildAndCurrentFoldersIdsById( - state$.value, - payload.id, - ), - folders: ConversationsSelectors.selectFolders(state$.value), - })), - switchMap( - ({ - sharedFolderId, - shareUniqueId, - conversations, - childFolders, - folders, - }) => { - const mapping = new Map(); - childFolders.forEach((folderId) => mapping.set(folderId, uuidv4())); - const newFolders = folders - .filter(({ id }) => childFolders.has(id)) - .map(({ folderId, ...folder }) => ({ - ...folder, - id: mapping.get(folder.id), - originalId: folder.id, - folderId: - folder.id === sharedFolderId ? undefined : mapping.get(folderId), // show shared folder on root level - ...resetShareEntity, - sharedWithMe: folder.id === sharedFolderId || folder.sharedWithMe, - shareUniqueId: - folder.id === sharedFolderId ? shareUniqueId : undefined, - })); - - const sharedConversations = conversations - .filter( - (conversation) => - conversation.folderId && childFolders.has(conversation.folderId), - ) - .map(({ folderId, ...conversation }) => ({ - ...conversation, - ...resetShareEntity, - id: uuidv4(), - originalId: conversation.id, - folderId: mapping.get(folderId), - })); - - return concat( - of( - ConversationsActions.addConversations({ - conversations: sharedConversations, - }), - ), - of( - ConversationsActions.addFolders({ - folders: newFolders, - }), - ), - ); - }, - ), - ); - -//TODO: added for development purpose - emulate immediate sharing with yourself -const shareConversationEpic: AppEpic = (action$, state$) => - action$.pipe( - filter(ConversationsActions.shareConversation.match), - map(({ payload }) => ({ - sharedConversationId: payload.id, - shareUniqueId: payload.shareUniqueId, - conversations: ConversationsSelectors.selectConversations(state$.value), - })), - switchMap(({ sharedConversationId, shareUniqueId, conversations }) => { - const sharedConversations = conversations - .filter((conversation) => conversation.id === sharedConversationId) - .map(({ folderId: _, ...conversation }) => ({ - ...conversation, - ...resetShareEntity, - id: uuidv4(), - originalId: conversation.id, - folderId: undefined, // show on root level - sharedWithMe: true, - shareUniqueId, - })); - - return concat( - of( - ConversationsActions.addConversations({ - conversations: sharedConversations, - }), - ), - ); - }), - ); - -//TODO: added for development purpose - emulate immediate sharing with yourself -const publishFolderEpic: AppEpic = (action$, state$) => - action$.pipe( - filter(ConversationsActions.publishFolder.match), - map(({ payload }) => ({ - publishRequest: payload, - conversations: ConversationsSelectors.selectConversations(state$.value), - childFolders: ConversationsSelectors.selectChildAndCurrentFoldersIdsById( - state$.value, - payload.id, - ), - folders: ConversationsSelectors.selectFolders(state$.value), - publishedAndTemporaryFolders: - ConversationsSelectors.selectTemporaryAndFilteredFolders(state$.value), - })), - switchMap( - ({ - publishRequest, - conversations, - childFolders, - folders, - publishedAndTemporaryFolders, - }) => { - const mapping = new Map(); - childFolders.forEach((folderId) => mapping.set(folderId, uuidv4())); - const newFolders = folders - .filter(({ id }) => childFolders.has(id)) - .map(({ folderId, ...folder }) => ({ - ...folder, - ...resetShareEntity, - id: mapping.get(folder.id), - originalId: folder.id, - folderId: - folder.id === publishRequest.id - ? getFolderIdByPath( - publishRequest.path, - publishedAndTemporaryFolders, - ) - : mapping.get(folderId), - name: - folder.id === publishRequest.id - ? publishRequest.name - : folder.name, - publishedWithMe: true, - shareUniqueId: - folder.id === publishRequest.id - ? publishRequest.shareUniqueId - : folder.shareUniqueId, - publishVersion: - folder.id === publishRequest.id - ? publishRequest.version - : folder.publishVersion, - })); - - const rootFolder = findRootFromItems(newFolders); - const temporaryFolders = getTemporaryFoldersToPublish( - publishedAndTemporaryFolders, - rootFolder?.folderId, - publishRequest.version, - ); - - const sharedConversations = conversations - .filter( - (conversation) => - conversation.folderId && childFolders.has(conversation.folderId), - ) - .map(({ folderId, ...conversation }) => ({ - ...renameAttachments( - conversation, - folderId, - folders, - publishRequest.fileNameMapping, - ), - ...resetShareEntity, - id: uuidv4(), - originalId: conversation.id, - folderId: mapping.get(folderId), - })); - - return concat( - of( - ConversationsActions.addConversations({ - conversations: sharedConversations, - }), - ), - of( - ConversationsActions.addFolders({ - folders: [...temporaryFolders, ...newFolders], - }), - ), - of(ConversationsActions.deleteAllTemporaryFolders()), - ); - }, - ), - ); - -//TODO: added for development purpose - emulate immediate sharing with yourself -const publishConversationEpic: AppEpic = (action$, state$) => - action$.pipe( - filter(ConversationsActions.publishConversation.match), - map(({ payload }) => ({ - publishRequest: payload, - conversations: ConversationsSelectors.selectConversations(state$.value), - publishedAndTemporaryFolders: - ConversationsSelectors.selectTemporaryAndFilteredFolders(state$.value), - folders: ConversationsSelectors.selectFolders(state$.value), - })), - switchMap( - ({ - publishRequest, - conversations, - publishedAndTemporaryFolders, - folders, - }) => { - const sharedConversations = conversations - .filter((conversation) => conversation.id === publishRequest.id) - .map(({ folderId, ...conversation }) => ({ - ...renameAttachments( - conversation, - folderId, - folders, - publishRequest.fileNameMapping, - ), - ...resetShareEntity, - id: uuidv4(), - originalId: conversation.id, - folderId: getFolderIdByPath( - publishRequest.path, - publishedAndTemporaryFolders, - ), - publishedWithMe: true, - name: publishRequest.name, - shareUniqueId: publishRequest.shareUniqueId, - publishVersion: publishRequest.version, - })); - - const rootItem = findRootFromItems(sharedConversations); - const temporaryFolders = getTemporaryFoldersToPublish( - publishedAndTemporaryFolders, - rootItem?.folderId, - publishRequest.version, - ); - - return concat( - of(ConversationsActions.addFolders({ folders: temporaryFolders })), - of(ConversationsActions.deleteAllTemporaryFolders()), - of( - ConversationsActions.addConversations({ - conversations: sharedConversations, - }), - ), - ); - }, - ), - ); +// //TODO: added for development purpose - emulate immediate sharing with yourself +// const shareFolderEpic: AppEpic = (action$, state$) => +// action$.pipe( +// filter(ConversationsActions.shareFolder.match), +// map(({ payload }) => ({ +// sharedFolderId: payload.id, +// shareUniqueId: payload.shareUniqueId, +// conversations: ConversationsSelectors.selectConversations(state$.value), +// childFolders: ConversationsSelectors.selectChildAndCurrentFoldersIdsById( +// state$.value, +// payload.id, +// ), +// folders: ConversationsSelectors.selectFolders(state$.value), +// })), +// switchMap( +// ({ +// sharedFolderId, +// shareUniqueId, +// conversations, +// childFolders, +// folders, +// }) => { +// const mapping = new Map(); +// childFolders.forEach((folderId) => mapping.set(folderId, uuidv4())); +// const newFolders = folders +// .filter(({ id }) => childFolders.has(id)) +// .map(({ folderId, ...folder }) => ({ +// ...folder, +// id: mapping.get(folder.id), +// originalId: folder.id, +// folderId: +// folder.id === sharedFolderId ? undefined : mapping.get(folderId), // show shared folder on root level +// ...resetShareEntity, +// sharedWithMe: folder.id === sharedFolderId || folder.sharedWithMe, +// shareUniqueId: +// folder.id === sharedFolderId ? shareUniqueId : undefined, +// })); + +// const sharedConversations = conversations +// .filter( +// (conversation) => +// conversation.folderId && childFolders.has(conversation.folderId), +// ) +// .map(({ folderId, ...conversation }) => ({ +// ...conversation, +// ...resetShareEntity, +// id: uuidv4(), +// originalId: conversation.id, +// folderId: mapping.get(folderId), +// })); + +// return concat( +// of( +// ConversationsActions.addConversations({ +// conversations: sharedConversations, +// }), +// ), +// of( +// ConversationsActions.addFolders({ +// folders: newFolders, +// }), +// ), +// ); +// }, +// ), +// ); + +// //TODO: added for development purpose - emulate immediate sharing with yourself +// const shareConversationEpic: AppEpic = (action$, state$) => +// action$.pipe( +// filter(ConversationsActions.shareConversation.match), +// map(({ payload }) => ({ +// sharedConversationId: payload.id, +// shareUniqueId: payload.shareUniqueId, +// conversations: ConversationsSelectors.selectConversations(state$.value), +// })), +// switchMap(({ sharedConversationId, shareUniqueId, conversations }) => { +// const sharedConversations = conversations +// .filter((conversation) => conversation.id === sharedConversationId) +// .map(({ folderId: _, ...conversation }) => ({ +// ...conversation, +// ...resetShareEntity, +// id: uuidv4(), +// originalId: conversation.id, +// folderId: undefined, // show on root level +// sharedWithMe: true, +// shareUniqueId, +// })); + +// return concat( +// of( +// ConversationsActions.addConversations({ +// conversations: sharedConversations, +// }), +// ), +// ); +// }), +// ); + +// //TODO: added for development purpose - emulate immediate sharing with yourself +// const publishFolderEpic: AppEpic = (action$, state$) => +// action$.pipe( +// filter(ConversationsActions.publishFolder.match), +// map(({ payload }) => ({ +// publishRequest: payload, +// conversations: ConversationsSelectors.selectConversations(state$.value), +// childFolders: ConversationsSelectors.selectChildAndCurrentFoldersIdsById( +// state$.value, +// payload.id, +// ), +// folders: ConversationsSelectors.selectFolders(state$.value), +// publishedAndTemporaryFolders: +// ConversationsSelectors.selectTemporaryAndFilteredFolders(state$.value), +// })), +// switchMap( +// ({ +// publishRequest, +// conversations, +// childFolders, +// folders, +// publishedAndTemporaryFolders, +// }) => { +// const mapping = new Map(); +// childFolders.forEach((folderId) => mapping.set(folderId, uuidv4())); +// const newFolders = folders +// .filter(({ id }) => childFolders.has(id)) +// .map(({ folderId, ...folder }) => ({ +// ...folder, +// ...resetShareEntity, +// id: mapping.get(folder.id), +// originalId: folder.id, +// folderId: +// folder.id === publishRequest.id +// ? getFolderIdByPath( +// publishRequest.path, +// publishedAndTemporaryFolders, +// ) +// : mapping.get(folderId), +// name: +// folder.id === publishRequest.id +// ? publishRequest.name +// : folder.name, +// publishedWithMe: true, +// shareUniqueId: +// folder.id === publishRequest.id +// ? publishRequest.shareUniqueId +// : folder.shareUniqueId, +// publishVersion: +// folder.id === publishRequest.id +// ? publishRequest.version +// : folder.publishVersion, +// })); + +// const rootFolder = findRootFromItems(newFolders); +// const temporaryFolders = getTemporaryFoldersToPublish( +// publishedAndTemporaryFolders, +// rootFolder?.folderId, +// publishRequest.version, +// ); + +// const sharedConversations = conversations +// .filter( +// (conversation) => +// conversation.folderId && childFolders.has(conversation.folderId), +// ) +// .map(({ folderId, ...conversation }) => ({ +// ...renameAttachments( +// conversation, +// folderId, +// folders, +// publishRequest.fileNameMapping, +// ), +// ...resetShareEntity, +// id: uuidv4(), +// originalId: conversation.id, +// folderId: mapping.get(folderId), +// })); + +// return concat( +// of( +// ConversationsActions.addConversations({ +// conversations: sharedConversations, +// }), +// ), +// of( +// ConversationsActions.addFolders({ +// folders: [...temporaryFolders, ...newFolders], +// }), +// ), +// of(ConversationsActions.deleteAllTemporaryFolders()), +// ); +// }, +// ), +// ); + +// //TODO: added for development purpose - emulate immediate sharing with yourself +// const publishConversationEpic: AppEpic = (action$, state$) => +// action$.pipe( +// filter(ConversationsActions.publishConversation.match), +// map(({ payload }) => ({ +// publishRequest: payload, +// conversations: ConversationsSelectors.selectConversations(state$.value), +// publishedAndTemporaryFolders: +// ConversationsSelectors.selectTemporaryAndFilteredFolders(state$.value), +// folders: ConversationsSelectors.selectFolders(state$.value), +// })), +// switchMap( +// ({ +// publishRequest, +// conversations, +// publishedAndTemporaryFolders, +// folders, +// }) => { +// const sharedConversations = conversations +// .filter((conversation) => conversation.id === publishRequest.id) +// .map(({ folderId, ...conversation }) => ({ +// ...renameAttachments( +// conversation, +// folderId, +// folders, +// publishRequest.fileNameMapping, +// ), +// ...resetShareEntity, +// id: uuidv4(), +// originalId: conversation.id, +// folderId: getFolderIdByPath( +// publishRequest.path, +// publishedAndTemporaryFolders, +// ), +// publishedWithMe: true, +// name: publishRequest.name, +// shareUniqueId: publishRequest.shareUniqueId, +// publishVersion: publishRequest.version, +// })); + +// const rootItem = findRootFromItems(sharedConversations); +// const temporaryFolders = getTemporaryFoldersToPublish( +// publishedAndTemporaryFolders, +// rootItem?.folderId, +// publishRequest.version, +// ); + +// return concat( +// of(ConversationsActions.addFolders({ folders: temporaryFolders })), +// of(ConversationsActions.deleteAllTemporaryFolders()), +// of( +// ConversationsActions.addConversations({ +// conversations: sharedConversations, +// }), +// ), +// ); +// }, +// ), +// ); export const ConversationsEpics = combineEpics( initEpic, @@ -1684,8 +1678,8 @@ export const ConversationsEpics = combineEpics( playbackPrevMessageEpic, playbackCalncelEpic, - shareFolderEpic, - shareConversationEpic, - publishFolderEpic, - publishConversationEpic, + // shareFolderEpic, + // shareConversationEpic, + // publishFolderEpic, + // publishConversationEpic, ); diff --git a/src/store/conversations/conversations.reducers.ts b/src/store/conversations/conversations.reducers.ts index 2af61bd7ec..a75826ce4e 100644 --- a/src/store/conversations/conversations.reducers.ts +++ b/src/store/conversations/conversations.reducers.ts @@ -7,6 +7,7 @@ import { translate } from '@/src/utils/app/translation'; import { Conversation, ConversationEntityModel, + ConversationInfo, Message, Role, } from '@/src/types/chat'; @@ -105,7 +106,7 @@ export const conversationsSlice = createSlice({ }; }, ); - state.conversations = state.conversations.concat(newConversations); + state.conversations = state.conversations.concat(newConversations); // TODO: save in API state.selectedConversationsIds = newConversations.map(({ id }) => id); }, updateConversation: ( @@ -267,7 +268,7 @@ export const conversationsSlice = createSlice({ messagesStack: [], }, }; - state.conversations = state.conversations.concat(newConversation); + state.conversations = state.conversations.concat(newConversation); // TODO: save in API state.selectedConversationsIds = [newConversation.id]; }, createNewPlaybackConversation: ( @@ -303,7 +304,7 @@ export const conversationsSlice = createSlice({ replayAsIs: false, }, }; - state.conversations = state.conversations.concat(newConversation); + state.conversations = state.conversations.concat(newConversation); // TODO: save in API state.selectedConversationsIds = [newConversation.id]; }, duplicateConversation: ( @@ -323,7 +324,7 @@ export const conversationsSlice = createSlice({ id: uuidv4(), lastActivityDate: Date.now(), }; - state.conversations = state.conversations.concat(newConversation); + state.conversations = state.conversations.concat(newConversation); //TODO: save in API state.selectedConversationsIds = [newConversation.id]; }, duplicateSelectedConversations: (state) => { @@ -340,7 +341,7 @@ export const conversationsSlice = createSlice({ FeatureType.Chat, ) ) { - const newConversation: Conversation = { + const newConversation: ConversationInfo = { ...conversation, ...resetShareEntity, folderId: undefined, @@ -359,7 +360,7 @@ export const conversationsSlice = createSlice({ newSelectedIds.push(id); } }); - state.conversations = state.conversations.concat(newConversations); + state.conversations = state.conversations.concat(newConversations); // TODO: save in API state.selectedConversationsIds = newSelectedIds; }, exportConversations: (state) => state, @@ -372,7 +373,7 @@ export const conversationsSlice = createSlice({ { payload, }: PayloadAction<{ - conversations: Conversation[]; + conversations: ConversationInfo[]; folders: FolderInterface[]; }>, ) => { @@ -384,7 +385,7 @@ export const conversationsSlice = createSlice({ }, updateConversations: ( state, - { payload }: PayloadAction<{ conversations: Conversation[] }>, + { payload }: PayloadAction<{ conversations: ConversationInfo[] }>, ) => { state.conversations = payload.conversations; }, diff --git a/src/store/conversations/conversations.types.ts b/src/store/conversations/conversations.types.ts index e91b4d585e..4563877e9e 100644 --- a/src/store/conversations/conversations.types.ts +++ b/src/store/conversations/conversations.types.ts @@ -1,9 +1,9 @@ -import { Conversation } from '@/src/types/chat'; +import { ConversationInfo } from '@/src/types/chat'; import { FolderInterface } from '@/src/types/folder'; import { SearchFilters } from '@/src/types/search'; export interface ConversationsState { - conversations: Conversation[]; + conversations: ConversationInfo[]; selectedConversationsIds: string[]; folders: FolderInterface[]; temporaryFolders: FolderInterface[]; diff --git a/src/types/chat.ts b/src/types/chat.ts index d16f754475..f53185dcd8 100644 --- a/src/types/chat.ts +++ b/src/types/chat.ts @@ -78,7 +78,6 @@ export interface Conversation extends ShareEntity, ConversationInfo { // Addons selected by user clicks selectedAddons: string[]; assistantModelId?: string; - lastActivityDate?: number; isMessageStreaming: boolean; isNameChanged?: boolean; @@ -114,8 +113,7 @@ export interface ConversationEntityModel { export interface ConversationInfo extends Entity { model: ConversationEntityModel; + lastActivityDate?: number; isPlayback?: boolean; isReplay?: boolean; - - uploaded?: boolean; } diff --git a/src/types/common.ts b/src/types/common.ts index ed0f463e82..769fa48650 100644 --- a/src/types/common.ts +++ b/src/types/common.ts @@ -1,5 +1,3 @@ -import { BackendFile } from '@/src/types/files'; - import { ShareInterface } from './share'; export enum EntityType { @@ -69,7 +67,7 @@ export interface BaseDialEntity { folderId?: string; serverSynced?: boolean; status?: 'UPLOADING' | 'FAILED'; -}; +} export type DialChatEntity = Omit< BackendChatEntity, diff --git a/src/types/prompt.ts b/src/types/prompt.ts index 0ca2267ddf..aff7491590 100644 --- a/src/types/prompt.ts +++ b/src/types/prompt.ts @@ -5,6 +5,4 @@ export interface Prompt extends ShareEntity, PromptInfo { content?: string; } -export interface PromptInfo extends Entity { - uploaded?: boolean; -} +export type PromptInfo = Entity; diff --git a/src/types/storage.ts b/src/types/storage.ts index 776bf9ded4..2c0d8fda2d 100644 --- a/src/types/storage.ts +++ b/src/types/storage.ts @@ -28,7 +28,10 @@ export enum UIStorageKeys { TextOfClosedAnnouncement = 'textOfClosedAnnouncement', } -export interface EntityStorage { +export interface EntityStorage< + EntityInfo extends { folderId?: string }, + Entity extends EntityInfo, +> { getEntities(path?: string): Observable; // listing with short information getEntity(info: EntityInfo): Observable; @@ -57,16 +60,13 @@ export interface DialStorage { getConversations(path?: string): Observable; - getConversation( - info: ConversationInfo, - path?: string, - ): Observable; + getConversation(info: ConversationInfo): Observable; setConversations(conversations: Conversation[]): Observable; getPrompts(path?: string): Observable; - getPrompt(info: PromptInfo, path?: string): Observable; + getPrompt(info: PromptInfo): Observable; setPrompts(prompts: Prompt[]): Observable; } diff --git a/src/utils/app/clean.ts b/src/utils/app/clean.ts index 363360cda2..6a1528c49d 100644 --- a/src/utils/app/clean.ts +++ b/src/utils/app/clean.ts @@ -55,7 +55,7 @@ const migrateMessageAttachmentUrls = (message: Message): Message => { }; export const cleanConversationHistory = ( - history: Conversation[] | unknown, + history: Conversation[], ): Conversation[] => { // added model for each conversation (3/20/23) // added system prompt for each conversation (3/21/23) diff --git a/src/utils/app/data/storages/api-entity-storage.ts b/src/utils/app/data/storages/api-entity-storage.ts index 0b65af639c..8d0397d234 100644 --- a/src/utils/app/data/storages/api-entity-storage.ts +++ b/src/utils/app/data/storages/api-entity-storage.ts @@ -1,16 +1,18 @@ import { Observable, map } from 'rxjs'; -import { ApiUtils } from '@/src/utils/server/api'; +import { ApiUtils, getParentPath } from '@/src/utils/server/api'; import { BackendChatEntity, BackendDataNodeType } from '@/src/types/common'; import { EntityStorage } from '@/src/types/storage'; import { DataService } from '../data-service'; -export abstract class ApiEntityStorage - implements EntityStorage +export abstract class ApiEntityStorage< + EntityInfo extends { folderId?: string }, + Entity extends EntityInfo, +> implements EntityStorage { - getEntities(path?: string | undefined): Observable { + getEntities(path?: string): Observable { const filter = BackendDataNodeType.ITEM; const query = new URLSearchParams({ @@ -39,7 +41,9 @@ export abstract class ApiEntityStorage getEntity(info: EntityInfo): Observable { const key = this.getEntityKey(info); return ApiUtils.request( - `api/${this.getStorageKey()}/${DataService.getBucket()}/${key}`, + `api/${this.getStorageKey()}/${DataService.getBucket()}${getParentPath( + info.folderId, + )}/${key}`, ).pipe( map((entity: Entity) => { return { @@ -53,7 +57,9 @@ export abstract class ApiEntityStorage createEntity(entity: Entity): Observable { const key = this.getEntityKey(entity); return ApiUtils.request( - `api/${this.getStorageKey()}/${DataService.getBucket()}/${key}`, + `api/${this.getStorageKey()}/${DataService.getBucket()}${getParentPath( + entity.folderId, + )}/${key}`, { method: 'PUT', headers: { @@ -69,7 +75,9 @@ export abstract class ApiEntityStorage deleteEntity(info: EntityInfo): Observable { const key = this.getEntityKey(info); return ApiUtils.request( - `api/${this.getStorageKey()}/${DataService.getBucket()}/${key}`, + `api/${this.getStorageKey()}/${DataService.getBucket()}${getParentPath( + info.folderId, + )}/${key}`, { method: 'DELETE', headers: { diff --git a/src/utils/app/data/storages/api-storage.ts b/src/utils/app/data/storages/api-storage.ts index f73c6bd01f..a716f04b43 100644 --- a/src/utils/app/data/storages/api-storage.ts +++ b/src/utils/app/data/storages/api-storage.ts @@ -1,97 +1,21 @@ -import { - Observable, - from, - map, - mergeMap, - of, - switchMap, - throwError, - toArray, -} from 'rxjs'; -import { fromFetch } from 'rxjs/fetch'; +import { Observable, from, map, mergeMap, of } from 'rxjs'; -import { - ApiKeys, - getConversationApiKeyFromConversation, - getConversationApiKeyFromConversationInfo, - getPromptApiKey, - parseConversationApiKey, - parsePromptApiKey, -} from '@/src/utils/server/api'; +import { ApiKeys, ApiUtils } from '@/src/utils/server/api'; import { Conversation, ConversationInfo } from '@/src/types/chat'; -import { - BackendChatEntity, - BackendDataNodeType, - DialChatEntity, -} from '@/src/types/common'; +import { BackendChatEntity, DialChatEntity } from '@/src/types/common'; import { FolderInterface } from '@/src/types/folder'; import { Prompt, PromptInfo } from '@/src/types/prompt'; import { DialStorage } from '@/src/types/storage'; import { constructPath } from '../../file'; import { DataService } from '../data-service'; +import { ConversationApiStorage } from './conversation-api-storage'; +import { PromptApiStorage } from './prompt-api-storage'; export class ApiStorage implements DialStorage { - static request(url: string, options?: RequestInit) { - return fromFetch(url, options).pipe( - switchMap((response) => { - if (!response.ok) { - return throwError(() => new Error(response.statusText)); - } - - return from(response.json()); - }), - ); - } - - static requestOld({ - url, - method, - async, - body, - }: { - url: string | URL; - method: string; - async: boolean; - body: XMLHttpRequestBodyInit | Document | null | undefined; - }): Observable<{ percent?: number; result?: unknown }> { - return new Observable((observer) => { - const xhr = new XMLHttpRequest(); - - xhr.open(method, url, async); - xhr.responseType = 'json'; - - // Track upload progress - xhr.upload.onprogress = (event) => { - if (event.lengthComputable) { - const percentComplete = (event.loaded / event.total) * 100; - observer.next({ percent: Math.round(percentComplete) }); - } - }; - - // Handle response - xhr.onload = () => { - if (xhr.status === 200) { - observer.next({ result: xhr.response }); - observer.complete(); - } else { - observer.error('Request failed'); - } - }; - - xhr.onerror = () => { - observer.error('Request failed'); - }; - - xhr.send(body); - - // Return cleanup function - return () => { - xhr.abort(); - }; - }); - } + private conversationStorage = new ConversationApiStorage(); + private promptStorage = new PromptApiStorage(); static setData( entity: Conversation | Prompt, @@ -105,7 +29,7 @@ export class ApiStorage implements DialStorage { }${entityId}`, ); - return ApiStorage.requestOld({ + return ApiUtils.requestOld({ url: `api/${entityType}/${resultPath}`, method: 'PUT', async: true, @@ -143,27 +67,11 @@ export class ApiStorage implements DialStorage { } setConversationsFolders(_folders: FolderInterface[]): Observable { - const resultPath = encodeURI(constructPath(DataService.getBucket(), '')); - - const response = ApiStorage.request( - `api/${ApiKeys.Conversations}/${resultPath}`, - ); - - // eslint-disable-next-line no-console - console.log(response); - - return response; + return of(); //TODO } getPromptsFolders(): Observable { - const resultPath = encodeURI(constructPath(DataService.getBucket(), '')); - - const response = ApiStorage.request(`api/${ApiKeys.Prompts}/${resultPath}`); - - // eslint-disable-next-line no-console - console.log(response); - - return response; + return of(); //TODO } setPromptsFolders(_folders: FolderInterface[]): Observable { @@ -171,121 +79,32 @@ export class ApiStorage implements DialStorage { } getConversations(path?: string): Observable { - const filter = BackendDataNodeType.ITEM; - - const query = new URLSearchParams({ - filter, - bucket: DataService.getBucket(), - ...(path && { path }), - }); - const resultQuery = query.toString(); - - return ApiStorage.request( - `api/${ApiKeys.Conversations}/listing?${resultQuery}`, - ).pipe( - map((conversations: BackendChatEntity[]) => { - return conversations.map((conversation): ConversationInfo => { - const relativePath = conversation.parentPath || undefined; - const conversationInfo = parseConversationApiKey(conversation.name); - - return { - ...conversationInfo, - folderId: relativePath, - }; - }); - }), - ); + return this.conversationStorage.getEntities(path); } - getConversation( - info: ConversationInfo, - _path?: string | undefined, - ): Observable { - const key = getConversationApiKeyFromConversationInfo(info); - return ApiStorage.request( - `api/${ApiKeys.Conversations}/${DataService.getBucket()}/${key}`, - ).pipe( - map((conversation: Conversation) => { - return { - ...info, - ...conversation, - uploaded: true, - }; - }), - ); + getConversation(info: ConversationInfo): Observable { + return this.conversationStorage.getEntity(info); } - setConversations( - _conversations: Conversation[], - ): Observable<{ result?: DialChatEntity | undefined }[]> { + setConversations(_conversations: Conversation[]): Observable { return from(_conversations).pipe( mergeMap((conversation) => - ApiStorage.setData( - conversation, - ApiKeys.Conversations, - undefined, // TODO: define relative path - getConversationApiKeyFromConversation(conversation), - ), + this.conversationStorage.createEntity(conversation), ), - toArray(), ); } getPrompts(path?: string): Observable { - const filter = BackendDataNodeType.ITEM; - - const query = new URLSearchParams({ - filter, - bucket: DataService.getBucket(), - ...(path && { path }), - }); - const resultQuery = query.toString(); - - return ApiStorage.request( - `api/${ApiKeys.Prompts}/listing?${resultQuery}`, - ).pipe( - map((prompts: BackendChatEntity[]) => { - return prompts.map((prompt): PromptInfo => { - const relativePath = prompt.parentPath || undefined; - const promptInfo = parsePromptApiKey(prompt.name); - - return { - ...promptInfo, - folderId: relativePath, - }; - }); - }), - ); + return this.promptStorage.getEntities(path); } - getPrompt(info: PromptInfo, _path?: string | undefined): Observable { - const key = getPromptApiKey(info); - return ApiStorage.request( - `api/${ApiKeys.Prompts}/${DataService.getBucket()}/${key}`, - ).pipe( - map((prompt: Prompt) => { - return { - ...info, - ...prompt, - uploaded: true, - }; - }), - ); + getPrompt(info: PromptInfo): Observable { + return this.promptStorage.getEntity(info); } - setPrompts( - _prompts: Prompt[], - ): Observable<{ result?: DialChatEntity | undefined }[]> { + setPrompts(_prompts: Prompt[]): Observable { return from(_prompts).pipe( - mergeMap((prompt) => - ApiStorage.setData( - prompt, - ApiKeys.Prompts, - undefined, // TODO: define relative path - prompt.name, - ), - ), - toArray(), + mergeMap((prompt) => this.promptStorage.createEntity(prompt)), ); } } diff --git a/src/utils/app/data/storages/browser-storage.ts b/src/utils/app/data/storages/browser-storage.ts index 6d621737b2..49de6158f8 100644 --- a/src/utils/app/data/storages/browser-storage.ts +++ b/src/utils/app/data/storages/browser-storage.ts @@ -14,15 +14,6 @@ import { cleanConversationHistory } from '../../clean'; import { isLocalStorageEnabled } from '../storage'; export class BrowserStorage implements DialStorage { - getConversation( - _info: ConversationInfo, - _path?: string | undefined, - ): Observable { - throw new Error('Method not implemented.'); - } - getPrompt(_info: PromptInfo, _path?: string | undefined): Observable { - throw new Error('Method not implemented.'); - } private static storage: globalThis.Storage | undefined; public static init() { @@ -39,6 +30,17 @@ export class BrowserStorage implements DialStorage { ); } + getConversation(info: ConversationInfo): Observable { + return BrowserStorage.getData(UIStorageKeys.ConversationHistory, []).pipe( + map((conversations) => { + const conv = conversations.find( + (conv: Conversation) => conv.id === info.id, + ); + return conv ? cleanConversationHistory([conv])[0] : null; + }), + ); + } + setConversations(conversations: Conversation[]): Observable { return BrowserStorage.setData( UIStorageKeys.ConversationHistory, @@ -50,6 +52,15 @@ export class BrowserStorage implements DialStorage { return BrowserStorage.getData(UIStorageKeys.Prompts, []); } + getPrompt(info: PromptInfo): Observable { + return BrowserStorage.getData(UIStorageKeys.Prompts, []).pipe( + map( + (prompts: Prompt[]) => + prompts.find((prompt) => prompt.id === info.id) || null, + ), + ); + } + setPrompts(prompts: Prompt[]): Observable { return BrowserStorage.setData(UIStorageKeys.Prompts, prompts); } diff --git a/src/utils/app/data/storages/conversation-api-storage.ts b/src/utils/app/data/storages/conversation-api-storage.ts index c89aca2141..90a9185478 100644 --- a/src/utils/app/data/storages/conversation-api-storage.ts +++ b/src/utils/app/data/storages/conversation-api-storage.ts @@ -1,6 +1,6 @@ import { ApiKeys, - getConversationApiKeyFromConversationInfo, + getConversationApiKey, parseConversationApiKey, } from '@/src/utils/server/api'; @@ -13,7 +13,7 @@ export class ConversationApiStorage extends ApiEntityStorage< Conversation > { getEntityKey(info: ConversationInfo): string { - return getConversationApiKeyFromConversationInfo(info); + return getConversationApiKey(info); } parseEntityKey(key: string): ConversationInfo { return parseConversationApiKey(key); diff --git a/src/utils/app/import-export.ts b/src/utils/app/import-export.ts index 0a1273d4d4..e6415a12f6 100644 --- a/src/utils/app/import-export.ts +++ b/src/utils/app/import-export.ts @@ -1,4 +1,4 @@ -import { Conversation } from '@/src/types/chat'; +import { Conversation, ConversationInfo } from '@/src/types/chat'; import { ExportConversationsFormatV4, ExportFormatV1, @@ -48,7 +48,7 @@ export function cleanData(data: SupportedExportFormats): CleanDataResponse { if (isExportFormatV1(data)) { const cleanHistoryData: LatestExportFormat = { version: 4, - history: cleanConversationHistory(data), + history: cleanConversationHistory(data as unknown as Conversation[]), folders: [], prompts: [], }; @@ -190,7 +190,7 @@ export const exportPrompt = (prompt: Prompt, folders: FolderInterface[]) => { }; export interface ImportConversationsResponse { - history: Conversation[]; + history: ConversationInfo[]; folders: FolderInterface[]; isError: boolean; } @@ -200,13 +200,13 @@ export const importConversations = ( currentConversations, currentFolders, }: { - currentConversations: Conversation[]; + currentConversations: ConversationInfo[]; currentFolders: FolderInterface[]; }, ): ImportConversationsResponse => { const { history, folders, isError } = cleanData(importedData); - const newHistory: Conversation[] = [ + const newHistory: ConversationInfo[] = [ ...currentConversations, ...history, ].filter( diff --git a/src/utils/server/api.ts b/src/utils/server/api.ts index 7a38f7c26b..70a8a04c00 100644 --- a/src/utils/server/api.ts +++ b/src/utils/server/api.ts @@ -46,31 +46,23 @@ enum PseudoModel { } const getModelApiIdFromConversation = (conversation: Conversation) => { - if (conversation.replay.isReplay) return PseudoModel.Replay; - if (conversation.playback?.isPlayback) return PseudoModel.Playback; + if (conversation.replay?.isReplay || conversation.isReplay) + return PseudoModel.Replay; + if (conversation.playback?.isPlayback || conversation.isPlayback) + return PseudoModel.Playback; return conversation.model.id; }; // Format key: {id:guid}__{modelId}__{name:base64} -export const getConversationApiKeyFromConversation = ( - conversation: Conversation, -) => { +export const getConversationApiKey = (conversation: ConversationInfo) => { return [ conversation.id, - getModelApiIdFromConversation(conversation), + getModelApiIdFromConversation(conversation as Conversation), + conversation.lastActivityDate, btoa(conversation.name), ].join(pathKeySeparator); }; -// Format key: {id:guid}__{modelId}__{name:base64} -export const getConversationApiKeyFromConversationInfo = ( - conversation: ConversationInfo, -) => { - return [conversation.id, conversation.model.id, btoa(conversation.name)].join( - pathKeySeparator, - ); -}; - // Format key: {id:guid}__{modelId}__{name:base64} export const parseConversationApiKey = (apiKey: string): ConversationInfo => { const parts = apiKey.split(pathKeySeparator); @@ -170,3 +162,6 @@ export class ApiUtils { }); } } + +export const getParentPath = (parentPath?: string | null) => + parentPath ? `/${parentPath}` : ''; From 2029fba623b2bb89e8cda503f422a2108b89e96b Mon Sep 17 00:00:00 2001 From: Ilya Bondar Date: Tue, 30 Jan 2024 02:45:42 +0100 Subject: [PATCH 034/202] stateful API --- src/components/Chat/ChatCompareSelect.tsx | 6 +- src/components/Chatbar/Chatbar.tsx | 4 +- .../Chatbar/components/Conversation.tsx | 50 +++--- .../Chatbar/components/Conversations.tsx | 22 +-- src/components/Folder/Folder.tsx | 6 +- src/components/Promptbar/Promptbar.tsx | 4 +- .../conversations/conversations.epics.ts | 156 +++++++++++++++++- .../conversations/conversations.reducers.ts | 104 ++---------- src/types/storage.ts | 12 ++ src/utils/app/data/data-service.ts | 48 +++++- .../app/data/storages/api-mock-storage.ts | 28 +++- src/utils/app/data/storages/api-storage.ts | 25 ++- .../app/data/storages/browser-storage.ts | 62 +++++++ src/utils/server/api.ts | 9 +- 14 files changed, 382 insertions(+), 154 deletions(-) diff --git a/src/components/Chat/ChatCompareSelect.tsx b/src/components/Chat/ChatCompareSelect.tsx index 5e39282dae..b41c846da6 100644 --- a/src/components/Chat/ChatCompareSelect.tsx +++ b/src/components/Chat/ChatCompareSelect.tsx @@ -4,7 +4,7 @@ import { useTranslation } from 'next-i18next'; import { isMobile } from '@/src/utils/app/mobile'; -import { Conversation, Role } from '@/src/types/chat'; +import { Conversation, ConversationInfo, Role } from '@/src/types/chat'; import { FeatureType } from '@/src/types/common'; import { Translation } from '@/src/types/translation'; @@ -47,7 +47,7 @@ const Option = ({ item }: OptionProps) => { }; interface Props { - conversations: Conversation[]; + conversations: ConversationInfo[]; selectedConversations: Conversation[]; onConversationSelect: (conversation: Conversation) => void; } @@ -67,7 +67,7 @@ export const ChatCompareSelect = ({ if (selectedConversations.length === 1) { const selectedConversation = selectedConversations[0]; - const comparableConversations = conversations + const comparableConversations = (conversations as Conversation[]) // TODO: how to filter for comparison? .filter((conv) => !conv.replay.isReplay) .filter((conv) => { if (conv.id === selectedConversation.id) { diff --git a/src/components/Chatbar/Chatbar.tsx b/src/components/Chatbar/Chatbar.tsx index 93205a4bc1..eeed6675f1 100644 --- a/src/components/Chatbar/Chatbar.tsx +++ b/src/components/Chatbar/Chatbar.tsx @@ -4,7 +4,7 @@ import { useTranslation } from 'next-i18next'; import { MoveType } from '@/src/utils/app/move'; -import { Conversation } from '@/src/types/chat'; +import { ConversationInfo } from '@/src/types/chat'; import { FeatureType } from '@/src/types/common'; import { SearchFilters } from '@/src/types/search'; import { Translation } from '@/src/types/translation'; @@ -94,7 +94,7 @@ export const Chatbar = () => { ); return ( - + featureType={FeatureType.Chat} side="left" actionButtons={} diff --git a/src/components/Chatbar/components/Conversation.tsx b/src/components/Chatbar/components/Conversation.tsx index eeb3a8d3f4..be97bcd065 100644 --- a/src/components/Chatbar/components/Conversation.tsx +++ b/src/components/Chatbar/components/Conversation.tsx @@ -18,7 +18,7 @@ import { MoveType, getDragImage } from '@/src/utils/app/move'; import { defaultMyItemsFilters } from '@/src/utils/app/search'; import { isEntityOrParentsExternal } from '@/src/utils/app/share'; -import { Conversation } from '@/src/types/chat'; +import { ConversationInfo } from '@/src/types/chat'; import { FeatureType } from '@/src/types/common'; import { SharingType } from '@/src/types/share'; @@ -45,7 +45,7 @@ import { ModelIcon } from './ModelIcon'; import { v4 as uuidv4 } from 'uuid'; interface ViewProps { - conversation: Conversation; + conversation: ConversationInfo; isHighlited: boolean; } @@ -59,26 +59,25 @@ export function ConversationView({ conversation, isHighlited }: ViewProps) { isHighlighted={!!isHighlited} featureType={FeatureType.Chat} > - {conversation.replay.replayAsIs && ( + {conversation.isReplay && ( )} - {conversation.playback && conversation.playback.isPlayback && ( + {conversation.isPlayback && ( )} - {!conversation.replay.replayAsIs && - !conversation.playback?.isPlayback && ( - - )} + {!conversation.isReplay && !conversation.isPlayback && ( + + )}
{ const dismiss = useDismiss(context); const { getFloatingProps } = useInteractions([dismiss]); - const isEmptyConversation = conversation.messages.length === 0; + const isEmptyConversation = false; //conversation.messages.length === 0; //TODO: how check if empty? const handleRename = useCallback( - (conversation: Conversation) => { + (conversation: ConversationInfo) => { if (renameValue.trim().length > 0) { dispatch( ConversationsActions.updateConversation({ @@ -179,7 +178,7 @@ export const ConversationComponent = ({ item: conversation, level }: Props) => { ); const handleDragStart = useCallback( - (e: DragEvent, conversation: Conversation) => { + (e: DragEvent, conversation: ConversationInfo) => { if (e.dataTransfer && !isExternal) { e.dataTransfer.setDragImage(getDragImage(), 0, 0); e.dataTransfer.setData( @@ -378,26 +377,25 @@ export const ConversationComponent = ({ item: conversation, level }: Props) => { isHighlighted={isHighlighted} featureType={FeatureType.Chat} > - {conversation.replay.replayAsIs && ( + {conversation.isReplay && ( )} - {conversation.playback && conversation.playback.isPlayback && ( + {conversation.isPlayback && ( )} - {!conversation.replay.replayAsIs && - !conversation.playback?.isPlayback && ( - - )} + {!conversation.isReplay && !conversation.isPlayback && ( + + )} { if (convA.lastActivityDate && convB.lastActivityDate) { const dateA = convA.lastActivityDate; diff --git a/src/components/Folder/Folder.tsx b/src/components/Folder/Folder.tsx index bfa5af9653..c11745d9ff 100644 --- a/src/components/Folder/Folder.tsx +++ b/src/components/Folder/Folder.tsx @@ -32,11 +32,11 @@ import { import { doesEntityContainSearchItem } from '@/src/utils/app/search'; import { isEntityOrParentsExternal } from '@/src/utils/app/share'; -import { Conversation } from '@/src/types/chat'; +import { ConversationInfo } from '@/src/types/chat'; import { FeatureType } from '@/src/types/common'; import { DialFile } from '@/src/types/files'; import { FolderInterface } from '@/src/types/folder'; -import { Prompt } from '@/src/types/prompt'; +import { PromptInfo } from '@/src/types/prompt'; import { SharingType } from '@/src/types/share'; import { Translation } from '@/src/types/translation'; @@ -96,7 +96,7 @@ export interface FolderProps { withBorderHighlight?: boolean; } -const Folder = ({ +const Folder = ({ currentFolder, searchTerm, itemComponent, diff --git a/src/components/Promptbar/Promptbar.tsx b/src/components/Promptbar/Promptbar.tsx index d5ab25d585..466d19b166 100644 --- a/src/components/Promptbar/Promptbar.tsx +++ b/src/components/Promptbar/Promptbar.tsx @@ -5,7 +5,7 @@ import { useTranslation } from 'next-i18next'; import { MoveType } from '@/src/utils/app/move'; import { FeatureType } from '@/src/types/common'; -import { Prompt } from '@/src/types/prompt'; +import { PromptInfo } from '@/src/types/prompt'; import { SearchFilters } from '@/src/types/search'; import { Translation } from '@/src/types/translation'; @@ -79,7 +79,7 @@ const Promptbar = () => { ); return ( - + featureType={FeatureType.Prompt} side="right" isOpen={showPromptbar} diff --git a/src/store/conversations/conversations.epics.ts b/src/store/conversations/conversations.epics.ts index ccd2df24fb..31dc1a957f 100644 --- a/src/store/conversations/conversations.epics.ts +++ b/src/store/conversations/conversations.epics.ts @@ -37,6 +37,7 @@ import { isSettingsChanged, } from '@/src/utils/app/conversation'; import { DataService } from '@/src/utils/app/data/data-service'; +import { generateNextName } from '@/src/utils/app/folders'; import { ImportConversationsResponse, exportConversation, @@ -62,6 +63,7 @@ import { import { EntityType } from '@/src/types/common'; import { AppEpic } from '@/src/types/store'; +import { resetShareEntity } from '@/src/constants/chat'; import { DEFAULT_CONVERSATION_NAME } from '@/src/constants/default-settings'; import { errorsMessages } from '@/src/constants/errors'; @@ -72,6 +74,9 @@ import { ConversationsActions, ConversationsSelectors, } from './conversations.reducers'; +import { hasExternalParent } from './conversations.selectors'; + +import { v4 as uuidv4 } from 'uuid'; const createNewConversationEpic: AppEpic = (action$, state$) => action$.pipe( @@ -113,6 +118,146 @@ const createNewConversationEpic: AppEpic = (action$, state$) => }), ); +const createNewReplayConversationEpic: AppEpic = (action$, state$) => + action$.pipe( + filter(ConversationsActions.createNewReplayConversation.match), + switchMap(({ payload }) => + forkJoin({ + conversation: DataService.getConversation(payload.conversation), + }), + ), + switchMap(({ conversation }) => { + if (!conversation) return EMPTY; + + const newConversationName = `[Replay] ${conversation.name}`; + + const userMessages = conversation.messages.filter( + ({ role }) => role === Role.User, + ); + const newConversation: Conversation = { + ...conversation, + ...resetShareEntity, + folderId: hasExternalParent( + { conversations: state$.value }, + conversation.folderId, + ) + ? undefined + : conversation.folderId, + id: uuidv4(), + name: newConversationName, + messages: [], + lastActivityDate: Date.now(), + + replay: { + isReplay: true, + replayUserMessagesStack: userMessages, + activeReplayIndex: 0, + replayAsIs: true, + }, + + playback: { + isPlayback: false, + activePlaybackIndex: 0, + messagesStack: [], + }, + }; + + DataService.createConversation(newConversation); + + return of( + ConversationsActions.createNewConversationSuccess({ + newConversation, + }), + ); + }), + ); + +const createNewPlaybackConversationEpic: AppEpic = (action$, state$) => + action$.pipe( + filter(ConversationsActions.createNewPlaybackConversation.match), + switchMap(({ payload }) => + forkJoin({ + conversation: DataService.getConversation(payload.conversation), + }), + ), + switchMap(({ conversation }) => { + if (!conversation) return EMPTY; + + const newConversationName = `[Playback] ${conversation.name}`; + + const newConversation: Conversation = { + ...conversation, + ...resetShareEntity, + folderId: hasExternalParent( + { conversations: state$.value }, + conversation.folderId, + ) + ? undefined + : conversation.folderId, + id: uuidv4(), + name: newConversationName, + messages: [], + lastActivityDate: Date.now(), + + playback: { + messagesStack: conversation.messages, + activePlaybackIndex: 0, + isPlayback: true, + }, + + replay: { + isReplay: false, + replayUserMessagesStack: [], + activeReplayIndex: 0, + replayAsIs: false, + }, + }; + + DataService.createConversation(newConversation); + + return of( + ConversationsActions.createNewConversationSuccess({ + newConversation, + }), + ); + }), + ); + +const duplicateConversationEpic: AppEpic = (action$, state$) => + action$.pipe( + filter(ConversationsActions.duplicateConversation.match), + switchMap(({ payload }) => + forkJoin({ + conversation: DataService.getConversation(payload.conversation), + }), + ), + switchMap(({ conversation }) => { + if (!conversation) return EMPTY; + + const newConversation: Conversation = { + ...conversation, + ...resetShareEntity, + folderId: undefined, + name: generateNextName( + DEFAULT_CONVERSATION_NAME, + conversation.name, + state$.value.conversations, + 0, + ), + id: uuidv4(), + lastActivityDate: Date.now(), + }; + + DataService.createConversation(newConversation); + + return of( + ConversationsActions.createNewConversationSuccess({ + newConversation, + }), + ); + }), + ); + const createNewConversationSuccessEpic: AppEpic = (action$) => action$.pipe( filter(ConversationsActions.createNewConversations.match), @@ -181,7 +326,7 @@ const exportConversationsEpic: AppEpic = (action$, state$) => folders: ConversationsSelectors.selectFolders(state$.value), })), tap(({ conversations, folders }) => { - exportConversations(conversations, folders); + exportConversations(conversations as Conversation[], folders); // TODO: upload all conversations for export }), ignoreElements(), ); @@ -1103,9 +1248,8 @@ const selectConversationsEpic: AppEpic = (action$, state$) => ConversationsActions.selectConversations.match(action) || ConversationsActions.unselectConversations.match(action) || ConversationsActions.createNewConversationsSuccess.match(action) || - ConversationsActions.createNewReplayConversation.match(action) || + ConversationsActions.createNewConversationSuccess.match(action) || ConversationsActions.importConversationsSuccess.match(action) || - ConversationsActions.createNewPlaybackConversation.match(action) || ConversationsActions.deleteConversations.match(action) || ConversationsActions.addConversations.match(action) || ConversationsActions.duplicateConversation.match(action) || @@ -1134,12 +1278,10 @@ const saveConversationsEpic: AppEpic = (action$, state$) => filter( (action) => ConversationsActions.createNewConversationsSuccess.match(action) || - ConversationsActions.createNewReplayConversation.match(action) || ConversationsActions.updateConversation.match(action) || ConversationsActions.updateConversations.match(action) || ConversationsActions.importConversationsSuccess.match(action) || ConversationsActions.deleteConversations.match(action) || - ConversationsActions.createNewPlaybackConversation.match(action) || ConversationsActions.addConversations.match(action) || ConversationsActions.unpublishConversation.match(action) || ConversationsActions.duplicateConversation.match(action) || @@ -1678,6 +1820,10 @@ export const ConversationsEpics = combineEpics( playbackPrevMessageEpic, playbackCalncelEpic, + createNewReplayConversationEpic, + createNewPlaybackConversationEpic, + duplicateConversationEpic, + // shareFolderEpic, // shareConversationEpic, // publishFolderEpic, diff --git a/src/store/conversations/conversations.reducers.ts b/src/store/conversations/conversations.reducers.ts index a75826ce4e..85031a3d71 100644 --- a/src/store/conversations/conversations.reducers.ts +++ b/src/store/conversations/conversations.reducers.ts @@ -9,7 +9,6 @@ import { ConversationEntityModel, ConversationInfo, Message, - Role, } from '@/src/types/chat'; import { FeatureType } from '@/src/types/common'; import { SupportedExportFormats } from '@/src/types/export'; @@ -25,7 +24,6 @@ import { } from '@/src/constants/default-settings'; import { defaultReplay } from '@/src/constants/replay'; -import { hasExternalParent } from './conversations.selectors'; import { ConversationsState } from './conversations.types'; import { v4 as uuidv4 } from 'uuid'; @@ -234,99 +232,25 @@ export const conversationsSlice = createSlice({ }, createNewReplayConversation: ( state, - { payload }: PayloadAction<{ conversation: Conversation }>, + _action: PayloadAction<{ conversation: ConversationInfo }>, + ) => state, + createNewConversationSuccess: ( + state, + { + payload: { newConversation }, + }: PayloadAction<{ newConversation: Conversation }>, ) => { - const newConversationName = `[Replay] ${payload.conversation.name}`; - - const userMessages = payload.conversation.messages.filter( - ({ role }) => role === Role.User, - ); - const newConversation: Conversation = { - ...payload.conversation, - ...resetShareEntity, - folderId: hasExternalParent( - { conversations: state }, - payload.conversation.folderId, - ) - ? undefined - : payload.conversation.folderId, - id: uuidv4(), - name: newConversationName, - messages: [], - lastActivityDate: Date.now(), - - replay: { - isReplay: true, - replayUserMessagesStack: userMessages, - activeReplayIndex: 0, - replayAsIs: true, - }, - - playback: { - isPlayback: false, - activePlaybackIndex: 0, - messagesStack: [], - }, - }; - state.conversations = state.conversations.concat(newConversation); // TODO: save in API + state.conversations.concat(newConversation); state.selectedConversationsIds = [newConversation.id]; }, createNewPlaybackConversation: ( state, - { payload }: PayloadAction<{ conversation: Conversation }>, - ) => { - const newConversationName = `[Playback] ${payload.conversation.name}`; - - const newConversation: Conversation = { - ...payload.conversation, - ...resetShareEntity, - folderId: hasExternalParent( - { conversations: state }, - payload.conversation.folderId, - ) - ? undefined - : payload.conversation.folderId, - id: uuidv4(), - name: newConversationName, - messages: [], - lastActivityDate: Date.now(), - - playback: { - messagesStack: payload.conversation.messages, - activePlaybackIndex: 0, - isPlayback: true, - }, - - replay: { - isReplay: false, - replayUserMessagesStack: [], - activeReplayIndex: 0, - replayAsIs: false, - }, - }; - state.conversations = state.conversations.concat(newConversation); // TODO: save in API - state.selectedConversationsIds = [newConversation.id]; - }, + _action: PayloadAction<{ conversation: ConversationInfo }>, + ) => state, duplicateConversation: ( state, - { payload }: PayloadAction<{ conversation: Conversation }>, - ) => { - const newConversation: Conversation = { - ...payload.conversation, - ...resetShareEntity, - folderId: undefined, - name: generateNextName( - DEFAULT_CONVERSATION_NAME, - payload.conversation.name, - state.conversations, - 0, - ), - id: uuidv4(), - lastActivityDate: Date.now(), - }; - state.conversations = state.conversations.concat(newConversation); //TODO: save in API - state.selectedConversationsIds = [newConversation.id]; - }, + _action: PayloadAction<{ conversation: ConversationInfo }>, + ) => state, duplicateSelectedConversations: (state) => { const selectedIds = new Set(state.selectedConversationsIds); const newSelectedIds: string[] = []; @@ -341,8 +265,8 @@ export const conversationsSlice = createSlice({ FeatureType.Chat, ) ) { - const newConversation: ConversationInfo = { - ...conversation, + const newConversation: Conversation = { + ...(conversation as Conversation), ...resetShareEntity, folderId: undefined, name: generateNextName( diff --git a/src/types/storage.ts b/src/types/storage.ts index 2c0d8fda2d..8fae5f315b 100644 --- a/src/types/storage.ts +++ b/src/types/storage.ts @@ -62,11 +62,23 @@ export interface DialStorage { getConversation(info: ConversationInfo): Observable; + createConversation(conversation: Conversation): Observable; + + updateConversation(conversation: Conversation): Observable; + + deleteConversation(info: ConversationInfo): Observable; + setConversations(conversations: Conversation[]): Observable; getPrompts(path?: string): Observable; getPrompt(info: PromptInfo): Observable; + createPrompt(prompt: Prompt): Observable; + + updatePrompt(prompt: Prompt): Observable; + + deletePrompt(info: PromptInfo): Observable; + setPrompts(prompts: Prompt[]): Observable; } diff --git a/src/utils/app/data/data-service.ts b/src/utils/app/data/data-service.ts index a6a9227ef5..11c283c34e 100644 --- a/src/utils/app/data/data-service.ts +++ b/src/utils/app/data/data-service.ts @@ -47,16 +47,56 @@ export class DataService extends FileService { return this.getDataStorage().setPromptsFolders(folders); } - public static getPrompts(): Observable { - return this.getDataStorage().getPrompts(); + public static getPrompts(path?: string): Observable { + return this.getDataStorage().getPrompts(path); + } + + public static getPrompt(info: PromptInfo): Observable { + return this.getDataStorage().getPrompt(info); + } + + public static createConversation( + conversation: Conversation, + ): Observable { + return this.getDataStorage().createConversation(conversation); + } + + public static updateConversation( + conversation: Conversation, + ): Observable { + return this.getDataStorage().updateConversation(conversation); + } + + public static deleteConversation(info: ConversationInfo): Observable { + return this.getDataStorage().deleteConversation(info); } public static setPrompts(prompts: Prompt[]): Observable { return this.getDataStorage().setPrompts(prompts); } - public static getConversations(): Observable { - return this.getDataStorage().getConversations(); + public static getConversations( + path?: string, + ): Observable { + return this.getDataStorage().getConversations(path); + } + + public static getConversation( + info: ConversationInfo, + ): Observable { + return this.getDataStorage().getConversation(info); + } + + public static createPrompt(prompt: Prompt): Observable { + return this.getDataStorage().createPrompt(prompt); + } + + public static updatePrompt(prompt: Prompt): Observable { + return this.getDataStorage().updatePrompt(prompt); + } + + public static deletePrompt(info: PromptInfo): Observable { + return this.getDataStorage().deletePrompt(info); } public static setConversations( diff --git a/src/utils/app/data/storages/api-mock-storage.ts b/src/utils/app/data/storages/api-mock-storage.ts index a49300a931..84e4a9352d 100644 --- a/src/utils/app/data/storages/api-mock-storage.ts +++ b/src/utils/app/data/storages/api-mock-storage.ts @@ -1,12 +1,36 @@ import { Observable, of } from 'rxjs'; -import { Conversation } from '@/src/types/chat'; -import { EntityType } from '@/src/types/common'; +import { Conversation, ConversationInfo } from '@/src/types/chat'; +import { Entity, EntityType } from '@/src/types/common'; import { FolderInterface, FolderType } from '@/src/types/folder'; import { Prompt } from '@/src/types/prompt'; import { DialStorage } from '@/src/types/storage'; export class ApiMockStorage implements DialStorage { + getConversation(_info: ConversationInfo): Observable { + throw new Error('Method not implemented.'); + } + createConversation(_conversation: Conversation): Observable { + throw new Error('Method not implemented.'); + } + updateConversation(_conversation: Conversation): Observable { + throw new Error('Method not implemented.'); + } + deleteConversation(_info: ConversationInfo): Observable { + throw new Error('Method not implemented.'); + } + getPrompt(_info: Entity): Observable { + throw new Error('Method not implemented.'); + } + createPrompt(_prompt: Prompt): Observable { + throw new Error('Method not implemented.'); + } + updatePrompt(_prompt: Prompt): Observable { + throw new Error('Method not implemented.'); + } + deletePrompt(_info: Entity): Observable { + throw new Error('Method not implemented.'); + } setBucket(_bucket: string): void { return; } diff --git a/src/utils/app/data/storages/api-storage.ts b/src/utils/app/data/storages/api-storage.ts index 1c55c46035..0a86d12316 100644 --- a/src/utils/app/data/storages/api-storage.ts +++ b/src/utils/app/data/storages/api-storage.ts @@ -1,6 +1,7 @@ import { Observable, from, mergeMap, of } from 'rxjs'; import { Conversation, ConversationInfo } from '@/src/types/chat'; +import { Entity } from '@/src/types/common'; import { FolderInterface } from '@/src/types/folder'; import { Prompt, PromptInfo } from '@/src/types/prompt'; import { DialStorage } from '@/src/types/storage'; @@ -13,7 +14,7 @@ export class ApiStorage implements DialStorage { private _promptApiStorage = new PromptApiStorage(); getConversationsFolders(): Observable { - return of(); //TODO + return of([]); //TODO } setConversationsFolders(_folders: FolderInterface[]): Observable { @@ -21,7 +22,7 @@ export class ApiStorage implements DialStorage { } getPromptsFolders(): Observable { - return of(); //TODO + return of([]); //TODO } setPromptsFolders(_folders: FolderInterface[]): Observable { @@ -36,6 +37,16 @@ export class ApiStorage implements DialStorage { return this._conversationApiStorage.getEntity(info); } + createConversation(conversation: Conversation): Observable { + return this._conversationApiStorage.createEntity(conversation); + } + updateConversation(conversation: Conversation): Observable { + return this._conversationApiStorage.updateEntity(conversation); + } + deleteConversation(info: ConversationInfo): Observable { + return this._conversationApiStorage.deleteEntity(info); + } + setConversations(_conversations: Conversation[]): Observable { return from(_conversations).pipe( mergeMap((conversation) => @@ -52,6 +63,16 @@ export class ApiStorage implements DialStorage { return this._promptApiStorage.getEntity(info); } + createPrompt(prompt: Prompt): Observable { + return this._promptApiStorage.createEntity(prompt); + } + updatePrompt(prompt: Prompt): Observable { + return this._promptApiStorage.updateEntity(prompt); + } + deletePrompt(info: Entity): Observable { + return this._promptApiStorage.deleteEntity(info); + } + setPrompts(_prompts: Prompt[]): Observable { return from(_prompts).pipe( mergeMap((prompt) => this._promptApiStorage.createEntity(prompt)), diff --git a/src/utils/app/data/storages/browser-storage.ts b/src/utils/app/data/storages/browser-storage.ts index 49de6158f8..e318fbbdeb 100644 --- a/src/utils/app/data/storages/browser-storage.ts +++ b/src/utils/app/data/storages/browser-storage.ts @@ -4,6 +4,7 @@ import toast from 'react-hot-toast'; import { Observable, map, of, switchMap, throwError } from 'rxjs'; import { Conversation, ConversationInfo } from '@/src/types/chat'; +import { Entity } from '@/src/types/common'; import { FolderInterface, FolderType } from '@/src/types/folder'; import { Prompt, PromptInfo } from '@/src/types/prompt'; import { DialStorage, UIStorageKeys } from '@/src/types/storage'; @@ -41,6 +42,39 @@ export class BrowserStorage implements DialStorage { ); } + createConversation(conversation: Conversation): Observable { + return BrowserStorage.getData(UIStorageKeys.ConversationHistory, []).pipe( + map((conversations: Conversation[]) => { + BrowserStorage.setData(UIStorageKeys.ConversationHistory, [ + ...conversations, + conversation, + ]); + }), + ); + } + updateConversation(conversation: Conversation): Observable { + return BrowserStorage.getData(UIStorageKeys.ConversationHistory, []).pipe( + map((conversations: Conversation[]) => { + BrowserStorage.setData( + UIStorageKeys.ConversationHistory, + conversations.map((conv) => + conv.id === conversation.id ? conversation : conv, + ), + ); + }), + ); + } + deleteConversation(info: ConversationInfo): Observable { + return BrowserStorage.getData(UIStorageKeys.ConversationHistory, []).pipe( + map((conversations: Conversation[]) => { + BrowserStorage.setData( + UIStorageKeys.ConversationHistory, + conversations.filter((conv) => conv.id !== info.id), + ); + }), + ); + } + setConversations(conversations: Conversation[]): Observable { return BrowserStorage.setData( UIStorageKeys.ConversationHistory, @@ -61,6 +95,34 @@ export class BrowserStorage implements DialStorage { ); } + createPrompt(prompt: Prompt): Observable { + return BrowserStorage.getData(UIStorageKeys.Prompts, []).pipe( + map((prompts: Prompt[]) => { + BrowserStorage.setData(UIStorageKeys.Prompts, [...prompts, prompt]); + }), + ); + } + updatePrompt(prompt: Prompt): Observable { + return BrowserStorage.getData(UIStorageKeys.Prompts, []).pipe( + map((prompts: Prompt[]) => { + BrowserStorage.setData( + UIStorageKeys.Prompts, + prompts.map((item) => (prompt.id === item.id ? prompt : item)), + ); + }), + ); + } + deletePrompt(info: Entity): Observable { + return BrowserStorage.getData(UIStorageKeys.Prompts, []).pipe( + map((prompts: Prompt[]) => { + BrowserStorage.setData( + UIStorageKeys.Prompts, + prompts.filter((prompt) => prompt.id !== info.id), + ); + }), + ); + } + setPrompts(prompts: Prompt[]): Observable { return BrowserStorage.setData(UIStorageKeys.Prompts, prompts); } diff --git a/src/utils/server/api.ts b/src/utils/server/api.ts index 70a8a04c00..64703c8a91 100644 --- a/src/utils/server/api.ts +++ b/src/utils/server/api.ts @@ -56,9 +56,9 @@ const getModelApiIdFromConversation = (conversation: Conversation) => { // Format key: {id:guid}__{modelId}__{name:base64} export const getConversationApiKey = (conversation: ConversationInfo) => { return [ + conversation.lastActivityDate, conversation.id, getModelApiIdFromConversation(conversation as Conversation), - conversation.lastActivityDate, btoa(conversation.name), ].join(pathKeySeparator); }; @@ -67,14 +67,15 @@ export const getConversationApiKey = (conversation: ConversationInfo) => { export const parseConversationApiKey = (apiKey: string): ConversationInfo => { const parts = apiKey.split(pathKeySeparator); - if (parts.length !== 3) throw new Error('Incorrect conversation key'); + if (parts.length !== 4) throw new Error('Incorrect conversation key'); - const [id, modelId, encodedName] = parts; + const [lastActivityDate, id, modelId, encodedName] = parts; return { id, model: { id: modelId }, name: atob(encodedName), + lastActivityDate: parseInt(lastActivityDate), isPlayback: modelId === PseudoModel.Playback, isReplay: modelId === PseudoModel.Replay, }; @@ -89,7 +90,7 @@ export const getPromptApiKey = (prompt: PromptInfo) => { export const parsePromptApiKey = (apiKey: string): PromptInfo => { const parts = apiKey.split(pathKeySeparator); - if (parts.length !== 2) throw new Error('Incorrect conversation key'); + if (parts.length !== 2) throw new Error('Incorrect prompt key'); const [id, encodedName] = parts; From 792d85695dd0550d7dae3e1556a30a7b025054c5 Mon Sep 17 00:00:00 2001 From: Ilya Bondar Date: Tue, 30 Jan 2024 02:47:24 +0100 Subject: [PATCH 035/202] Update conversations.epics.ts --- src/store/conversations/conversations.epics.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/store/conversations/conversations.epics.ts b/src/store/conversations/conversations.epics.ts index 31dc1a957f..42a00f3992 100644 --- a/src/store/conversations/conversations.epics.ts +++ b/src/store/conversations/conversations.epics.ts @@ -1284,7 +1284,6 @@ const saveConversationsEpic: AppEpic = (action$, state$) => ConversationsActions.deleteConversations.match(action) || ConversationsActions.addConversations.match(action) || ConversationsActions.unpublishConversation.match(action) || - ConversationsActions.duplicateConversation.match(action) || ConversationsActions.duplicateSelectedConversations.match(action), ), map(() => ConversationsSelectors.selectConversations(state$.value)), From fc622344a81f4fe94598784414e8d3ec01fdf091 Mon Sep 17 00:00:00 2001 From: Ilya Bondar Date: Tue, 30 Jan 2024 09:03:37 +0100 Subject: [PATCH 036/202] fix getData --- src/utils/app/data/storages/browser-storage.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/app/data/storages/browser-storage.ts b/src/utils/app/data/storages/browser-storage.ts index e318fbbdeb..80c6308bbf 100644 --- a/src/utils/app/data/storages/browser-storage.ts +++ b/src/utils/app/data/storages/browser-storage.ts @@ -182,7 +182,7 @@ export class BrowserStorage implements DialStorage { return of( value === null || value === undefined ? defaultValue - : { ...JSON.parse(value), uploaded: true }, + : JSON.parse(value), ); } catch (e: unknown) { console.error(e); From ecb938300b6570e2f7ddacc865857912203500bd Mon Sep 17 00:00:00 2001 From: Ilya Bondar Date: Tue, 30 Jan 2024 09:58:36 +0100 Subject: [PATCH 037/202] fix listing --- .eslintrc.json | 2 +- .../conversations/conversations.epics.ts | 2 +- .../app/data/storages/api-entity-storage.ts | 6 ++++-- src/utils/server/api.ts | 19 +++++++++++-------- 4 files changed, 17 insertions(+), 12 deletions(-) diff --git a/.eslintrc.json b/.eslintrc.json index dc47c0db76..61e84605e9 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -24,7 +24,7 @@ "no-constant-condition": "off", "@typescript-eslint/no-unused-vars": [ "error", - { "argsIgnorePattern": "^_" } + { "argsIgnorePattern": "^_", "varsIgnorePattern": "^__" } ], "no-restricted-imports": ["error", { "paths": ["react-i18next"] }], "tailwindcss/no-custom-classname": "error", diff --git a/src/store/conversations/conversations.epics.ts b/src/store/conversations/conversations.epics.ts index 42a00f3992..cb318b551a 100644 --- a/src/store/conversations/conversations.epics.ts +++ b/src/store/conversations/conversations.epics.ts @@ -1288,7 +1288,7 @@ const saveConversationsEpic: AppEpic = (action$, state$) => ), map(() => ConversationsSelectors.selectConversations(state$.value)), switchMap((conversations) => { - return DataService.setConversations(conversations); + return of(conversations); //DataService.setConversations(conversations); }), ignoreElements(), ); diff --git a/src/utils/app/data/storages/api-entity-storage.ts b/src/utils/app/data/storages/api-entity-storage.ts index 8d0397d234..8da3c18fec 100644 --- a/src/utils/app/data/storages/api-entity-storage.ts +++ b/src/utils/app/data/storages/api-entity-storage.ts @@ -1,6 +1,6 @@ import { Observable, map } from 'rxjs'; -import { ApiUtils, getParentPath } from '@/src/utils/server/api'; +import { ApiUtils, combineApiKey, getParentPath } from '@/src/utils/server/api'; import { BackendChatEntity, BackendDataNodeType } from '@/src/types/common'; import { EntityStorage } from '@/src/types/storage'; @@ -28,7 +28,9 @@ export abstract class ApiEntityStorage< map((conversations: BackendChatEntity[]) => { return conversations.map((conversation): EntityInfo => { const relativePath = conversation.parentPath || undefined; - const info = this.parseEntityKey(conversation.name); + const info = this.parseEntityKey( + combineApiKey(conversation.updatedAt, conversation.name), + ); return { ...info, diff --git a/src/utils/server/api.ts b/src/utils/server/api.ts index 64703c8a91..2d4647fc84 100644 --- a/src/utils/server/api.ts +++ b/src/utils/server/api.ts @@ -40,6 +40,9 @@ export const getEntityUrlFromSlugs = ( const pathKeySeparator = '__'; +export const combineApiKey = (...args: (string | number)[]) => + args.join(pathKeySeparator); + enum PseudoModel { Replay = 'replay', Playback = 'playback', @@ -55,12 +58,12 @@ const getModelApiIdFromConversation = (conversation: Conversation) => { // Format key: {id:guid}__{modelId}__{name:base64} export const getConversationApiKey = (conversation: ConversationInfo) => { - return [ - conversation.lastActivityDate, + return combineApiKey( + conversation.lastActivityDate?.toString() || '', conversation.id, getModelApiIdFromConversation(conversation as Conversation), btoa(conversation.name), - ].join(pathKeySeparator); + ); }; // Format key: {id:guid}__{modelId}__{name:base64} @@ -69,13 +72,13 @@ export const parseConversationApiKey = (apiKey: string): ConversationInfo => { if (parts.length !== 4) throw new Error('Incorrect conversation key'); - const [lastActivityDate, id, modelId, encodedName] = parts; + const [updatedAt, id, modelId, encodedName] = parts; return { id, model: { id: modelId }, name: atob(encodedName), - lastActivityDate: parseInt(lastActivityDate), + lastActivityDate: parseInt(updatedAt), isPlayback: modelId === PseudoModel.Playback, isReplay: modelId === PseudoModel.Replay, }; @@ -83,16 +86,16 @@ export const parseConversationApiKey = (apiKey: string): ConversationInfo => { // Format key: {id:guid}__{name:base64} export const getPromptApiKey = (prompt: PromptInfo) => { - return [prompt.id, btoa(prompt.name)].join(pathKeySeparator); + return combineApiKey(prompt.id, btoa(prompt.name)); }; // Format key: {id:guid}__{name:base64} export const parsePromptApiKey = (apiKey: string): PromptInfo => { const parts = apiKey.split(pathKeySeparator); - if (parts.length !== 2) throw new Error('Incorrect prompt key'); + if (parts.length !== 3) throw new Error('Incorrect prompt key'); - const [id, encodedName] = parts; + const [__updatedAt, id, encodedName] = parts; return { id, From 8bf751e788501437212e81b816fbefbbf1674897 Mon Sep 17 00:00:00 2001 From: Ilya Bondar Date: Tue, 30 Jan 2024 10:26:41 +0100 Subject: [PATCH 038/202] fix search --- .../conversations/conversations.epics.ts | 12 +++------ .../conversations/conversations.selectors.ts | 27 ++++++++++++------- src/utils/app/search.ts | 21 +++++---------- 3 files changed, 27 insertions(+), 33 deletions(-) diff --git a/src/store/conversations/conversations.epics.ts b/src/store/conversations/conversations.epics.ts index cb318b551a..e57ce2861c 100644 --- a/src/store/conversations/conversations.epics.ts +++ b/src/store/conversations/conversations.epics.ts @@ -85,7 +85,7 @@ const createNewConversationEpic: AppEpic = (action$, state$) => names: payload.names, lastConversation: ConversationsSelectors.selectLastConversation( state$.value, - ), + ) as Conversation, //TODO: need to upload last conversation? })), switchMap(({ names, lastConversation }) => { return state$.pipe( @@ -93,13 +93,7 @@ const createNewConversationEpic: AppEpic = (action$, state$) => map((state) => ModelsSelectors.selectRecentModels(state)), filter((models) => models && models.length > 0), take(1), - map((recentModels) => ({ - lastConversation: ConversationsSelectors.selectLastConversation( - state$.value, - ), - recentModels: recentModels, - })), - switchMap(({ recentModels }) => { + switchMap((recentModels) => { const model = recentModels[0]; if (!model) { @@ -1288,7 +1282,7 @@ const saveConversationsEpic: AppEpic = (action$, state$) => ), map(() => ConversationsSelectors.selectConversations(state$.value)), switchMap((conversations) => { - return of(conversations); //DataService.setConversations(conversations); + return of(conversations); //DataService.setConversations(conversations); //TODO: fix saving conversations }), ignoreElements(), ); diff --git a/src/store/conversations/conversations.selectors.ts b/src/store/conversations/conversations.selectors.ts index b52f1e9d89..fbf56f51fd 100644 --- a/src/store/conversations/conversations.selectors.ts +++ b/src/store/conversations/conversations.selectors.ts @@ -19,7 +19,7 @@ import { isEntityOrParentsExternal, } from '@/src/utils/app/share'; -import { Conversation, Role } from '@/src/types/chat'; +import { Conversation, ConversationInfo, Role } from '@/src/types/chat'; import { EntityType, FeatureType } from '@/src/types/common'; import { DialFile } from '@/src/types/files'; import { EntityFilters, SearchFilters } from '@/src/types/search'; @@ -107,13 +107,13 @@ export const selectSectionFolders = createSelector( export const selectLastConversation = createSelector( [selectConversations], - (state): Conversation | undefined => { + (state): ConversationInfo | undefined => { return state[0]; }, ); export const selectConversation = createSelector( [selectConversations, (_state, id: string) => id], - (conversations, id): Conversation | undefined => { + (conversations, id): ConversationInfo | undefined => { return conversations.find((conv) => conv.id === id); }, ); @@ -399,13 +399,16 @@ export const isPublishConversationVersionUnique = createSelector( (_state: RootState, _entityId: string, version: string) => version, ], (state, entityId, version) => { - const conversation = selectConversation(state, entityId); + const conversation = selectConversation(state, entityId) as Conversation; // TODO: fix if (!conversation || conversation?.publishVersion === version) return false; - const conversations = selectConversations(state).filter( - (conv) => conv.originalId === entityId && conv.publishVersion === version, - ); + const conversations = selectConversations(state) + .map((conv) => conv as Conversation) // TODO: fix + .filter( + (conv) => + conv.originalId === entityId && conv.publishVersion === version, + ); if (conversations.length) return false; @@ -475,7 +478,10 @@ export const getAttachments = createSelector( const conversation = selectConversation(state, entityId); if (conversation) { return getUniqueAttachments( - getConversationAttachmentWithPath(conversation, folders), + getConversationAttachmentWithPath( + conversation as Conversation, //TODO: upload conversation + folders, + ), ); } else { const folderIds = new Set( @@ -490,7 +496,10 @@ export const getAttachments = createSelector( return getUniqueAttachments( conversations.flatMap((conv) => - getConversationAttachmentWithPath(conv, folders), + getConversationAttachmentWithPath( + conv as Conversation, //TODO: upload conversation + folders, + ), ), ); } diff --git a/src/utils/app/search.ts b/src/utils/app/search.ts index c1d5a44d2b..fe6ffa0182 100644 --- a/src/utils/app/search.ts +++ b/src/utils/app/search.ts @@ -1,34 +1,25 @@ -import { Conversation } from '@/src/types/chat'; +import { Conversation, ConversationInfo } from '@/src/types/chat'; import { DialFile } from '@/src/types/files'; import { FolderInterface } from '@/src/types/folder'; import { OpenAIEntityAddon, OpenAIEntityModel } from '@/src/types/openai'; -import { Prompt } from '@/src/types/prompt'; +import { Prompt, PromptInfo } from '@/src/types/prompt'; import { EntityFilter, EntityFilters, SearchFilters } from '@/src/types/search'; import { ShareInterface } from '@/src/types/share'; import { getChildAndCurrentFoldersIdsById } from './folders'; export const doesConversationContainSearchTerm = ( - conversation: Conversation, + conversation: ConversationInfo, searchTerm: string, ) => { - return [ - conversation.name, - ...conversation.messages.map((message) => message.content), - ] - .join(' ') - .toLowerCase() - .includes(searchTerm.toLowerCase()); + return conversation.name.toLowerCase().includes(searchTerm.toLowerCase()); }; export const doesPromptContainSearchTerm = ( - prompt: Prompt, + prompt: PromptInfo, searchTerm: string, ) => { - return [prompt.name, prompt.description, prompt.content] - .join(' ') - .toLowerCase() - .includes(searchTerm.toLowerCase()); + return prompt.name.toLowerCase().includes(searchTerm.toLowerCase()); }; export const doesFileContainSearchTerm = ( From 99c516c373c7579cab3b1fdf12bb2c6dc2487375 Mon Sep 17 00:00:00 2001 From: Ilya Bondar Date: Tue, 30 Jan 2024 11:01:37 +0100 Subject: [PATCH 039/202] fix url --- src/utils/server/api.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/server/api.ts b/src/utils/server/api.ts index 2d4647fc84..ac06156fd2 100644 --- a/src/utils/server/api.ts +++ b/src/utils/server/api.ts @@ -35,7 +35,7 @@ export const getEntityUrlFromSlugs = ( throw new OpenAIError(`No ${entityType} path provided`, '', '', '400'); } - return `${dialApiHost}/v1/${encodeURI(slugs.join('/'))}`; + return `${dialApiHost}/v1/${entityType}/${encodeURI(slugs.join('/'))}`; }; const pathKeySeparator = '__'; From c30d79f8285c9889fddbe3e971638cb3b8134444 Mon Sep 17 00:00:00 2001 From: Ilya Bondar Date: Tue, 30 Jan 2024 11:05:52 +0100 Subject: [PATCH 040/202] fix key --- src/utils/server/api.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/utils/server/api.ts b/src/utils/server/api.ts index ac06156fd2..d0c10aa328 100644 --- a/src/utils/server/api.ts +++ b/src/utils/server/api.ts @@ -59,7 +59,6 @@ const getModelApiIdFromConversation = (conversation: Conversation) => { // Format key: {id:guid}__{modelId}__{name:base64} export const getConversationApiKey = (conversation: ConversationInfo) => { return combineApiKey( - conversation.lastActivityDate?.toString() || '', conversation.id, getModelApiIdFromConversation(conversation as Conversation), btoa(conversation.name), From 9d33bd0bded5b2da5f4ee8ffd527e2b597ea4b65 Mon Sep 17 00:00:00 2001 From: Ilya Bondar Date: Tue, 30 Jan 2024 11:12:00 +0100 Subject: [PATCH 041/202] temporary solution for saving --- src/store/conversations/conversations.epics.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/store/conversations/conversations.epics.ts b/src/store/conversations/conversations.epics.ts index e57ce2861c..bd52c63cc1 100644 --- a/src/store/conversations/conversations.epics.ts +++ b/src/store/conversations/conversations.epics.ts @@ -1282,7 +1282,11 @@ const saveConversationsEpic: AppEpic = (action$, state$) => ), map(() => ConversationsSelectors.selectConversations(state$.value)), switchMap((conversations) => { - return of(conversations); //DataService.setConversations(conversations); //TODO: fix saving conversations + return DataService.setConversations( + (conversations as Conversation[]).filter( + (conv: Conversation) => !!conv.replay, //TODO: fix saving conversations + ), + ); }), ignoreElements(), ); From 7de060b4f3c77239e4dff893c821253cd5c31286 Mon Sep 17 00:00:00 2001 From: Ilya Bondar Date: Tue, 30 Jan 2024 18:05:47 +0100 Subject: [PATCH 042/202] loaders --- public/images/icons/loader.svg | 3 - src/components/Chat/ChatLoader.tsx | 28 ++++ src/components/Chat/MessageStage.tsx | 8 +- src/components/Common/Spinner.tsx | 15 +- .../Promptbar/components/PromptModal.tsx | 156 ++++++++++-------- src/components/Sidebar/Sidebar.tsx | 85 +++++----- src/pages/index.tsx | 5 +- tailwind.config.js | 3 + 8 files changed, 177 insertions(+), 126 deletions(-) delete mode 100644 public/images/icons/loader.svg create mode 100644 src/components/Chat/ChatLoader.tsx diff --git a/public/images/icons/loader.svg b/public/images/icons/loader.svg deleted file mode 100644 index 34eeb54b4b..0000000000 --- a/public/images/icons/loader.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/src/components/Chat/ChatLoader.tsx b/src/components/Chat/ChatLoader.tsx new file mode 100644 index 0000000000..f2e52704a3 --- /dev/null +++ b/src/components/Chat/ChatLoader.tsx @@ -0,0 +1,28 @@ +import classNames from 'classnames'; + +import { Spinner } from '../Common/Spinner'; + +interface Props { + size?: number; + slow?: boolean; + containerClassName?: string; + loaderClassName?: string; +} + +export default function ChatLoader({ + size = 45, + slow = true, + containerClassName, + loaderClassName, +}: Props) { + return ( +
+ +
+ ); +} diff --git a/src/components/Chat/MessageStage.tsx b/src/components/Chat/MessageStage.tsx index 5a867f8048..d558644290 100644 --- a/src/components/Chat/MessageStage.tsx +++ b/src/components/Chat/MessageStage.tsx @@ -13,7 +13,7 @@ import { ModelIcon } from '@/src/components/Chatbar/components/ModelIcon'; import ChevronDown from '../../../public/images/icons/chevron-down.svg'; import CircleCheck from '../../../public/images/icons/circle-check.svg'; -import Loader from '../../../public/images/icons/loader.svg'; +import { Spinner } from '../Common/Spinner'; import ChatMDComponent from '../Markdown/ChatMDComponent'; import { MessageAttachments } from './MessageAttachments'; @@ -37,11 +37,7 @@ const StageTitle = ({ isOpened, stage }: StageTitleProps) => { return (
{stage.status == null ? ( - + ) : stage.status === 'completed' ? ( { +export const Spinner = ({ size = 16, className = '', slow = false }: Props) => { return ( -
+ /> ); }; diff --git a/src/components/Promptbar/components/PromptModal.tsx b/src/components/Promptbar/components/PromptModal.tsx index 8405658ff5..6064612f85 100644 --- a/src/components/Promptbar/components/PromptModal.tsx +++ b/src/components/Promptbar/components/PromptModal.tsx @@ -18,6 +18,7 @@ import { onBlur } from '@/src/utils/app/style-helpers'; import { Prompt } from '@/src/types/prompt'; import { Translation } from '@/src/types/translation'; +import ChatLoader from '../../Chat/ChatLoader'; import EmptyRequiredInputMessage from '../../Common/EmptyRequiredInputMessage'; import Modal from '../../Common/Modal'; @@ -115,6 +116,8 @@ export const PromptModal: FC = ({ setName(prompt.name); }, [prompt.name]); + const uploaded = true; + return ( = ({ {t('Edit prompt')}
-
- - - -
- -
- -