diff --git a/src/modules/dblobs/dblobs.db.ts b/src/modules/dblobs/dblobs.db.ts index 245a3b4b6..e82694a64 100644 --- a/src/modules/dblobs/dblobs.db.ts +++ b/src/modules/dblobs/dblobs.db.ts @@ -1,6 +1,6 @@ import Dexie from 'dexie'; -import { DBlobAudioItem, dBlobCacheT256, DBlobDBItem, DBlobId, DBlobImageItem, DBlobItem, DBlobMetaDataType, DBlobMimeType } from './dblobs.types'; +import { DBlobAsset, DBlobAssetType, DBlobAudioAsset, DBlobDBAsset, DBlobId, DBlobImageAsset, DBlobMimeType } from './dblobs.types'; import { resizeBase64ImageIfNeeded } from '~/common/util/imageUtils'; @@ -12,14 +12,15 @@ import { resizeBase64ImageIfNeeded } from '~/common/util/imageUtils'; * - indexedDB.deleteDatabase('NAME').onsuccess = console.log; */ class BigAgiDB extends Dexie { - largeAssets!: Dexie.Table; + largeAssets!: Dexie.Table; constructor() { super('Big-AGI'); this.version(1).stores({ - // Index common properties - largeAssets: 'id, uId, wId, cId, sId, [cId+sId], type, [type+cId+sId], data.mimeType, origin.ot, origin.source, createdAt, updatedAt', + // Index common properties (and compound indexes) + largeAssets: 'id, contextId, scopeId, [contextId+scopeId], type, [type+contextId+scopeId], data.mimeType, origin.ot, origin.source, createdAt, updatedAt', }); + // Note: can re-add wId and uId in version 2 if needed } } @@ -38,20 +39,17 @@ const assetsTable = _db.largeAssets; // CRUD -const DEFAULT_USER_ID = '1'; -const DEFAULT_WORKSPACE_ID = '1'; - -export async function addDBlobItem(item: T, cId: DBlobDBItem['cId'], sId: DBlobDBItem['sId']): Promise { +export async function addDBlobItem(item: T, contextId: DBlobDBAsset['contextId'], scopeId: DBlobDBAsset['scopeId']): Promise { // Auto-Thumbnail: when adding an image, generate a thumbnail-256 cache level - if (item.type === DBlobMetaDataType.IMAGE) { - if (!item.cache?.[dBlobCacheT256]) { - const imageItem = item as DBlobImageItem; + if (item.type === 'image') { + if (!item.cache?.thumb256) { + const imageItem = item as DBlobImageAsset; const resizedDataForCache = await resizeBase64ImageIfNeeded(imageItem.data.mimeType, imageItem.data.base64, 'thumbnail-256', DBlobMimeType.IMG_WEBP, 0.9) .catch((error: any) => console.error('addDBlobItem: Error resizing image', error)); if (resizedDataForCache) { - item.cache[dBlobCacheT256] = { + item.cache.thumb256 = { base64: resizedDataForCache.base64, mimeType: DBlobMimeType.IMG_WEBP, }; @@ -63,10 +61,8 @@ export async function addDBlobItem(item: T, cId: DBlobDBIte // returns the id of the added item return await assetsTable.add({ ...item, - uId: DEFAULT_USER_ID, - wId: DEFAULT_WORKSPACE_ID, - cId, - sId, + contextId, + scopeId, }); } catch (error) { console.error('addDBlobItem: Error adding item', error); @@ -78,25 +74,25 @@ export async function getDBlobItemIDs() { return assetsTable.toCollection().primaryKeys(); } -export async function getItemById(id: DBlobId) { +export async function getItemById(id: DBlobId) { return await assetsTable.get(id) as T | undefined; } -export async function getDBlobItemsByType(type: T['type']) { +export async function getDBlobItemsByType(type: T['type']) { return await assetsTable.where({ type }).toArray() as unknown as T[]; } -export async function getDBlobItemsByTypeCIdSid(type: T['type'], cId: DBlobDBItem['cId'], sId: DBlobDBItem['sId']) { - const items = await assetsTable.where({ type, cId, sId }).sortBy('createdAt'); +export async function getDBlobItemsByTypeContextIdScopeId(type: T['type'], contextId: DBlobDBAsset['contextId'], scopeId: DBlobDBAsset['scopeId']) { + const items = await assetsTable.where({ type, contextId, scopeId }).sortBy('createdAt'); return items.reverse() as unknown as T[]; } -export async function getItemsByMimeType(mimeType: T['data']['mimeType']) { +export async function getItemsByMimeType(mimeType: T['data']['mimeType']) { return await assetsTable.where('data.mimeType').equals(mimeType).toArray() as unknown as T[]; } -export async function updateDBlobItem(id: DBlobId, updates: Partial) { +export async function updateDBlobItem(id: DBlobId, updates: Partial) { return assetsTable.update(id, updates); } @@ -108,14 +104,14 @@ export async function deleteDBlobItems(ids: DBlobId[]) { return assetsTable.bulkDelete(ids); } -export async function deleteAllDBlobsInScopeId(cId: DBlobDBItem['cId'], sId: DBlobDBItem['sId']) { - return assetsTable.where({ cId, sId }).delete(); +export async function deleteAllDBlobsInScopeId(contextId: DBlobDBAsset['contextId'], scopeId: DBlobDBAsset['scopeId']) { + return assetsTable.where({ contextId, scopeId }).delete(); } // Specific item types async function getImageItemById(id: DBlobId) { - return await getItemById(id); + return await getItemById(id); } export async function getImageDataURLById(id: DBlobId) { @@ -139,18 +135,18 @@ export async function getImageBlobURLById(id: DBlobId) { } // Example usage: -async function getAllImages(): Promise { - return await getDBlobItemsByType(DBlobMetaDataType.IMAGE); +async function getAllImages(): Promise { + return await getDBlobItemsByType(DBlobAssetType.IMAGE); } -async function getAllAudio(): Promise { - return await getDBlobItemsByType(DBlobMetaDataType.AUDIO); +async function getAllAudio(): Promise { + return await getDBlobItemsByType(DBlobAssetType.AUDIO); } async function getHighResImages() { return await assetsTable .where('data.mimeType') .startsWith('image/') - .and(item => (item as DBlobImageItem).metadata.width > 1920) - .toArray() as DBlobImageItem[]; + .and(item => (item as DBlobImageAsset).metadata.width > 1920) + .toArray() as DBlobImageAsset[]; } diff --git a/src/modules/dblobs/dblobs.hooks.ts b/src/modules/dblobs/dblobs.hooks.ts index 557720af8..268bbe149 100644 --- a/src/modules/dblobs/dblobs.hooks.ts +++ b/src/modules/dblobs/dblobs.hooks.ts @@ -1,14 +1,14 @@ import { useLiveQuery } from 'dexie-react-hooks'; -import type { DBlobDBItem, DBlobItem } from './dblobs.types'; -import { addDBlobItem, deleteDBlobItem, getDBlobItemsByTypeCIdSid, getItemById, updateDBlobItem } from './dblobs.db'; +import type { DBlobAsset, DBlobDBAsset } from './dblobs.types'; +import { addDBlobItem, deleteDBlobItem, getDBlobItemsByTypeContextIdScopeId, getItemById, updateDBlobItem } from './dblobs.db'; -// export function useDBlobItems(type: T['type']): [T[] | undefined, (item: T, cId: DBlobDBItem['cId'], sId: DBlobDBItem['sId']) => Promise, (id: string) => Promise] { +// export function useDBlobItems(type: T['type']): [T[] | undefined, (item: T, contextId: DBlobDBItem['contextId'], scopeId: DBlobDBItem['scopeId']) => Promise, (id: string) => Promise] { // const items = useLiveQuery(() => getDBlobItemsByType(type), [type]); // -// const addDBlobItemHandler = async (item: T, cId: DBlobDBItem['cId'], sId: DBlobDBItem['sId']) => { -// await addDBlobItem(item, cId, sId); +// const addDBlobItemHandler = async (item: T, contextId: DBlobDBItem['contextId'], scopeId: DBlobDBItem['scopeId']) => { +// await addDBlobItem(item, contextId, scopeId); // }; // // const deleteDBlobItemHandler = async (id: string) => { @@ -18,14 +18,14 @@ import { addDBlobItem, deleteDBlobItem, getDBlobItemsByTypeCIdSid, getItemById, // return [items, addDBlobItemHandler, deleteDBlobItemHandler]; // } -export function useDBlobItemsByTypeCIdSId(type: T['type'], cId: DBlobDBItem['cId'], sId: DBlobDBItem['sId']): [T[] | undefined, (item: T, cId: DBlobDBItem['cId'], sId: DBlobDBItem['sId']) => Promise, (id: string) => Promise] { +export function useDBlobItemsByTypeContextIdScopeId(type: T['type'], contextId: DBlobDBAsset['contextId'], scopeId: DBlobDBAsset['scopeId']): [T[] | undefined, (item: T, contextId: DBlobDBAsset['contextId'], scopeId: DBlobDBAsset['scopeId']) => Promise, (id: string) => Promise] { const items = useLiveQuery( - () => getDBlobItemsByTypeCIdSid(type, cId, sId), - [type, cId, sId], + () => getDBlobItemsByTypeContextIdScopeId(type, contextId, scopeId), + [type, contextId, scopeId], ); - const addDBlobItemHandler = async (item: T, cId: DBlobDBItem['cId'], sId: DBlobDBItem['sId']) => { - await addDBlobItem(item, cId, sId); + const addDBlobItemHandler = async (item: T, contextId: DBlobDBAsset['contextId'], scopeId: DBlobDBAsset['scopeId']) => { + await addDBlobItem(item, contextId, scopeId); }; const deleteDBlobItemHandler = async (id: string) => { @@ -36,7 +36,7 @@ export function useDBlobItemsByTypeCIdSId(type: T['type'], } -export function useDBlobItem(id: string): [T | undefined, (updates: Partial) => Promise] { +export function useDBlobItem(id: string): [T | undefined, (updates: Partial) => Promise] { const item = useLiveQuery( () => getItemById(id), [id], diff --git a/src/modules/dblobs/dblobs.types.ts b/src/modules/dblobs/dblobs.types.ts index e61599ff7..f0eaecee6 100644 --- a/src/modules/dblobs/dblobs.types.ts +++ b/src/modules/dblobs/dblobs.types.ts @@ -1,17 +1,72 @@ import { nanoid } from 'nanoid'; -// Blob +// DB - Assets -export enum DBlobMimeType { - IMG_PNG = 'image/png', IMG_JPEG = 'image/jpeg', IMG_WEBP = 'image/webp', - AUDIO_MPEG = 'audio/mpeg', AUDIO_WAV = 'audio/wav', - // VIDEO_MP4 = 'video/mp4', - // DOCUMENT_PDF = 'application/pdf', DOCUMENT_PLAIN = 'text/plain', DOCUMENT_HTML = 'text/html', - // ... +/** + * This is the asset when stored/loaded in the DB. Carries some more context out of band from the asset itself. + */ +export type DBlobDBAsset = { + contextId: 'global'; + scopeId: 'app-chat' | 'app-draw' | 'attachment-drafts'; +} & DBlobAsset; + + +// Assets + +export type DBlobAsset = DBlobImageAsset | DBlobAudioAsset; // | DBlobVideoAsset | DBlobDocumentAsset | DBlobTextAsset; + +export type DBlobImageAsset = DBlobAssetImplV1< + /* type: */ DBlobAssetType.IMAGE, + /* data: */ DBlobMimeType.IMG_PNG | DBlobMimeType.IMG_JPEG | DBlobMimeType.IMG_WEBP, + /* metadata: */ ImageAssetMetadata +>; + +export type DBlobAudioAsset = DBlobAssetImplV1< + /* type: */ DBlobAssetType.AUDIO, + /* data: */ DBlobMimeType.AUDIO_MPEG | DBlobMimeType.AUDIO_WAV, + /* metadata: */ AudioAssetMetadata +>; + +// type DBlobVideoAsset = DBlobAssetImplV1; +// type DBlobDocumentAsset = DBlobAssetImplV1; +// type DBlobTextAsset = DBlobAssetImplV1; + + +// DB - Asset Generic Type + +interface DBlobAssetImplV1> { + id: DBlobId; // Unique identifier + type: TType; // Type of asset, used for discrimination + + label: string; // Textual representation + data: DBlobAssetData; // Original data as a BlobData object + origin: DBlobAssetOrigin; // Source of the data (e.g., "upload", "generated") + + createdAt: Date; // Creation date + updatedAt: Date; // Last updated date + + metadata: TMeta; // Flexible metadata for specific .type(s) + // cache: Record>; // Cached conversions as BlobData objects + cache: { + thumb256?: DBlobAssetData; // Cache for the thumbnail-256 conversion + }; } -interface DBlobData { +export type DBlobId = string; + +export enum DBlobAssetType { + IMAGE = 'image', + AUDIO = 'audio', + // VIDEO = 'video', + // DOCUMENT = 'document', + // EGO = 'ego', +} + + +// Asset Data + +interface DBlobAssetData { mimeType: M; base64: string; // Base64 encoded content (not a data URL) // NOTE: the data url will be "data:${mimeType};base64,${base64}" @@ -20,8 +75,17 @@ interface DBlobData { altData?: string; // Alternative data for the input (optional) } +export enum DBlobMimeType { + IMG_PNG = 'image/png', IMG_JPEG = 'image/jpeg', IMG_WEBP = 'image/webp', + AUDIO_MPEG = 'audio/mpeg', AUDIO_WAV = 'audio/wav', + // VIDEO_MP4 = 'video/mp4', + // DOCUMENT_PDF = 'application/pdf', DOCUMENT_PLAIN = 'text/plain', DOCUMENT_HTML = 'text/html', +} -// Item Origin + +// Asset Origin + +type DBlobAssetOrigin = UserOrigin | GeneratedOrigin; interface UserOrigin { ot: 'user'; @@ -76,58 +140,10 @@ interface EgoOrigin { messageId?: string; // ID of the message (optional) }*/ -// Union type for ItemDataOrigin -type ItemDataOrigin = UserOrigin | GeneratedOrigin; +// Asset Metadata -// Item Base type - -interface DBlobBase> { - id: DBlobId; // Unique identifier - type: TType; // Type of item, used for discrimination - - label: string; // Textual representation - data: DBlobData; // Original data as a BlobData object - origin: ItemDataOrigin; // Source of the data (e.g., "upload", "generated") - - createdAt: Date; // Creation date - updatedAt: Date; // Last updated date - - metadata: TMeta; // Flexible metadata for specific .type(s) - cache: Record>; // Cached conversions as BlobData objects -} - -export type DBlobId = string; - -export function createDBlobBase>(type: TType, label: string, data: DBlobData, origin: ItemDataOrigin, metadata: TMeta): DBlobBase { - const creationDate = new Date(); - return { - id: nanoid(), - type, - label, - data, - origin, - createdAt: creationDate, - updatedAt: creationDate, - metadata, - cache: {}, - }; -} - -export const dBlobCacheT256 = 'thumbnail-256'; - - -// Item Specialization - -export enum DBlobMetaDataType { - IMAGE = 'image', - AUDIO = 'audio', - // VIDEO = 'video', - // DOCUMENT = 'document', - // EGO = 'ego', -} - -interface ImageMetadata { +interface ImageAssetMetadata { width: number; height: number; averageColor?: string; // Average html color of the image (optional) @@ -136,7 +152,7 @@ interface ImageMetadata { description?: string; // Description of the image (optional) } -interface AudioMetadata { +interface AudioAssetMetadata { duration: number; // Duration in seconds sampleRate: number; // Sample rate of the audio bitrate?: number; // Bitrate of the audio (optional) @@ -167,25 +183,29 @@ interface DocumentMetadata { }*/ -// Item Data - -export type DBlobImageItem = DBlobBase; -export type DBlobAudioItem = DBlobBase; -// type DBlobVideoItem = DBlobBase; -// type DBlobDocumentItem = DBlobBase; -// type DBlobTextItem = DBlobBase; - - // DB Item Data -export type DBlobItem = DBlobImageItem | DBlobAudioItem; // | DBlobVideoItem | DBlobDocumentItem | DBlobTextItem | DBlobEgoItem; - -export const createDBlobImageItem = (label: string, data: DBlobImageItem['data'], origin: ItemDataOrigin, metadata: ImageMetadata): DBlobImageItem => - createDBlobBase(DBlobMetaDataType.IMAGE, label, data, origin, metadata); - -export type DBlobDBItem = DBlobItem & { - uId: '1'; - wId: '1'; - cId: 'global'; - sId: 'app-chat' | 'app-draw' | 'attachment-drafts'; // scope ID +export function createDBlobImageAsset(label: string, data: DBlobImageAsset['data'], origin: DBlobAssetOrigin, metadata: ImageAssetMetadata): DBlobImageAsset { + return _createAssetImpl(DBlobAssetType.IMAGE, label, data, origin, metadata); +} + +function _createAssetImpl>( + type: TType, + label: string, + data: DBlobAssetData, + origin: DBlobAssetOrigin, + metadata: TMeta, +): DBlobAssetImplV1 { + const creationDate = new Date(); + return { + id: nanoid(), + type, + label, + data, + origin, + createdAt: creationDate, + updatedAt: creationDate, + metadata, + cache: {}, + }; }