From 7424742ed2c85b9a5be95e56d28c810d0694a0d4 Mon Sep 17 00:00:00 2001 From: Webifi Date: Wed, 14 Jun 2023 00:34:24 -0500 Subject: [PATCH] Add DALL-E image generation --- package-lock.json | 7 ++ package.json | 1 + src/lib/ApiUtil.svelte | 2 + src/lib/ChatCompletionResponse.svelte | 26 ++++++- src/lib/ChatRequest.svelte | 108 +++++++++++++++++++++----- src/lib/EditMessage.svelte | 53 +++++++++++-- src/lib/Export.svelte | 13 +++- src/lib/ImageStore.svelte | 71 +++++++++++++++++ src/lib/Models.svelte | 21 ++++- src/lib/Settings.svelte | 19 +++++ src/lib/Storage.svelte | 36 +++++++-- src/lib/Types.svelte | 32 +++++++- 12 files changed, 347 insertions(+), 42 deletions(-) create mode 100644 src/lib/ImageStore.svelte diff --git a/package-lock.json b/package-lock.json index 2f715e4..e93028b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,6 +22,7 @@ "bulma": "^0.9.4", "bulma-prefers-dark": "^0.1.0-beta.1", "copy-to-clipboard": "^3.3.3", + "dexie": "^4.0.1-alpha.20", "eslint-config-standard-with-typescript": "^35.0.0", "eslint-plugin-svelte3": "^4.0.0", "flourite": "^1.2.3", @@ -1601,6 +1602,12 @@ "node": ">=8" } }, + "node_modules/dexie": { + "version": "4.0.1-alpha.20", + "resolved": "https://registry.npmjs.org/dexie/-/dexie-4.0.1-alpha.20.tgz", + "integrity": "sha512-q/nMsCQiTWTmnw11aseJLfAsGQ/9t05sjWltgw1/r2TbfnIkmfjdTt8ATSIwmtKXuSznEZ5czazvL5LO5rR+6w==", + "dev": true + }, "node_modules/dir-glob": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", diff --git a/package.json b/package.json index b08903e..2eba101 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "bulma": "^0.9.4", "bulma-prefers-dark": "^0.1.0-beta.1", "copy-to-clipboard": "^3.3.3", + "dexie": "^4.0.1-alpha.20", "eslint-config-standard-with-typescript": "^35.0.0", "eslint-plugin-svelte3": "^4.0.0", "flourite": "^1.2.3", diff --git a/src/lib/ApiUtil.svelte b/src/lib/ApiUtil.svelte index 7beff67..01b41ff 100644 --- a/src/lib/ApiUtil.svelte +++ b/src/lib/ApiUtil.svelte @@ -2,9 +2,11 @@ // This makes it possible to override the OpenAI API base URL in the .env file const apiBase = import.meta.env.VITE_API_BASE || 'https://api.openai.com' const endpointCompletions = import.meta.env.VITE_ENDPOINT_COMPLETIONS || '/v1/chat/completions' + const endpointGenerations = import.meta.env.VITE_ENDPOINT_GENERATIONS || '/v1/images/generations' const endpointModels = import.meta.env.VITE_ENDPOINT_MODELS || '/v1/models' export const getApiBase = ():string => apiBase export const getEndpointCompletions = ():string => endpointCompletions + export const getEndpointGenerations = ():string => endpointGenerations export const getEndpointModels = ():string => endpointModels \ No newline at end of file diff --git a/src/lib/ChatCompletionResponse.svelte b/src/lib/ChatCompletionResponse.svelte index e2afb6d..986784a 100644 --- a/src/lib/ChatCompletionResponse.svelte +++ b/src/lib/ChatCompletionResponse.svelte @@ -1,7 +1,8 @@
+ {#if imageUrl} + + {/if} {:else}
+ {#if imageUrl} + + {/if}
{/if} {#if isSystem} @@ -261,7 +286,7 @@ { checkDelete() }} @@ -273,11 +298,11 @@ {/if} {/if} - {#if !message.summarized && !isError} + {#if !isImage && !message.summarized && !isError} { checkTruncate() }} @@ -289,11 +314,11 @@ {/if} {/if} - {#if !message.summarized && !isSystem && !isError} + {#if !isImage && !message.summarized && !isSystem && !isError} { setSuppress(!message.suppress) }} @@ -305,6 +330,18 @@ {/if} {/if} + {#if imageUrl} + { + downloadImage() + }} + > + + + {/if} diff --git a/src/lib/Export.svelte b/src/lib/Export.svelte index 7050fd2..e7fa582 100644 --- a/src/lib/Export.svelte +++ b/src/lib/Export.svelte @@ -1,8 +1,9 @@ \ No newline at end of file diff --git a/src/lib/Models.svelte b/src/lib/Models.svelte index 4d36ea2..a3953c8 100644 --- a/src/lib/Models.svelte +++ b/src/lib/Models.svelte @@ -31,6 +31,24 @@ const modelDetails : Record = { } } +const imageModels : Record = { + 'dall-e-1024x1024': { + prompt: 0.00, + completion: 0.020, // $0.020 per image + max: 1000 // 1000 char prompt, max + }, + 'dall-e-512x512': { + prompt: 0.00, + completion: 0.018, // $0.018 per image + max: 1000 // 1000 char prompt, max + }, + 'dall-e-256x256': { + prompt: 0.00, + completion: 0.016, // $0.016 per image + max: 1000 // 1000 char prompt, max + } +} + const unknownDetail = { prompt: 0, completion: 0, @@ -51,11 +69,12 @@ export const supportedModels : Record = { } const lookupList = { + ...imageModels, ...modelDetails, ...supportedModels } -export const supportedModelKeys = Object.keys(supportedModels) +export const supportedModelKeys = Object.keys({ ...supportedModels, ...imageModels }) const tpCache : Record = {} diff --git a/src/lib/Settings.svelte b/src/lib/Settings.svelte index b3dc4b9..d732417 100644 --- a/src/lib/Settings.svelte +++ b/src/lib/Settings.svelte @@ -86,6 +86,7 @@ const defaults:ChatSettings = { autoStartSession: false, trainingPrompts: [], hiddenPromptPrefix: '', + imageGenerationSize: '', // useResponseAlteration: false, // responseAlterations: [], isDirty: false @@ -97,6 +98,12 @@ const excludeFromProfile = { isDirty: true } +export const imageGenerationSizes = [ + '1024x1024', '512x512', '256x256' +] + +export const imageGenerationSizeTypes = ['', ...imageGenerationSizes] + const profileSetting: ChatSetting & SettingSelect = { key: 'profile', name: 'Profile', @@ -269,6 +276,18 @@ const summarySettings: ChatSetting[] = [ placeholder: 'Enter a prompt that will be used to summarize past prompts here.', type: 'textarea', hide: (chatId) => getChatSettings(chatId).continuousChat !== 'summary' + }, + { + key: 'imageGenerationSize', + name: 'Image Generation Size', + header: 'Image Generation', + headerClass: 'is-info', + title: 'Prompt an image with: show me an image of ...', + type: 'select', + options: [ + { value: '', text: 'OFF - Disable Image Generation' }, + ...imageGenerationSizes.map(s => { return { value: s, text: s } }) + ] } ] diff --git a/src/lib/Storage.svelte b/src/lib/Storage.svelte index 80eafe0..967eca0 100644 --- a/src/lib/Storage.svelte +++ b/src/lib/Storage.svelte @@ -6,7 +6,10 @@ import { v4 as uuidv4 } from 'uuid' import { getProfile, getProfiles, isStaticProfile, newNameForProfile, restartProfile } from './Profiles.svelte' import { errorNotice } from './Util.svelte' + import { deleteImage, setImage } from './ImageStore.svelte' + // TODO: move chatsStorage to indexedDB with localStorage as a fallback for private browsing. + // Enough long chats will overflow localStorage. export const chatsStorage = persisted('chats', [] as Chat[]) export const latestModelMap = persisted('latestModelMap', {} as Record) // What was returned when a model was requested export const globalStorage = persisted('global', {} as GlobalSettings) @@ -53,7 +56,7 @@ return chatId } - export const addChatFromJSON = (json: string): number => { + export const addChatFromJSON = async (json: string): Promise => { const chats = get(chatsStorage) // Find the max chatId @@ -73,6 +76,10 @@ chat.id = chatId + // Make sure images are moved to indexedDB store, + // else they would clobber local storage + await updateChatImages(chatId, chat) + // Add a new chat chats.push(chat) chatsStorage.set(chats) @@ -154,7 +161,10 @@ } export const clearChats = () => { - chatsStorage.set([]) + const chats = get(chatsStorage) + chats.forEach(c => deleteChat(c.id)) // make sure images are removed + // TODO: add a clear images option to make this faster + // chatsStorage.set([]) } export const saveChatStore = () => { const chats = get(chatsStorage) @@ -268,13 +278,16 @@ const chat = chats.find((chat) => chat.id === chatId) as Chat const index = chat.messages.findIndex((m) => m.uuid === uuid) const message = getMessage(chat, uuid) - if (message && message.summarized) throw new Error('Unable to delete summarized message') - if (message && message.summary) throw new Error('Unable to directly delete message summary') + if (message?.summarized) throw new Error('Unable to delete summarized message') + if (message?.summary) throw new Error('Unable to directly delete message summary') // const found = chat.messages.filter((m) => m.uuid === uuid) if (index < 0) { console.error(`Unable to find and delete message with ID: ${uuid}`) return } + if (message?.image) { + deleteImage(chatId, message.image.id) + } // console.warn(`Deleting message with ID: ${uuid}`, found, index) chat.messages.splice(index, 1) // remove item chatsStorage.set(chats) @@ -303,10 +316,21 @@ export const deleteChat = (chatId: number) => { const chats = get(chatsStorage) + const chat = getChat(chatId) + chat?.messages?.forEach(m => { + if (m.image) deleteImage(chatId, m.image.id) + }) chatsStorage.set(chats.filter((chat) => chat.id !== chatId)) } - export const copyChat = (chatId: number) => { + export const updateChatImages = async (chatId: number, chat: Chat) => { + for (let i = 0; i < chat.messages.length; i++) { + const m = chat.messages[i] + if (m.image) m.image = await setImage(chatId, m.image) + } + } + + export const copyChat = async (chatId: number) => { const chats = get(chatsStorage) const chat = chats.find((chat) => chat.id === chatId) as Chat const nameMap = chats.reduce((a, chat) => { a[chat.name] = chat; return a }, {}) @@ -323,6 +347,8 @@ // Set new name chatCopy.name = cname + await updateChatImages(chatId, chatCopy) + // Add a new chat chats.push(chatCopy) diff --git a/src/lib/Types.svelte b/src/lib/Types.svelte index 246eedf..f944556 100644 --- a/src/lib/Types.svelte +++ b/src/lib/Types.svelte @@ -1,8 +1,11 @@