Merge pull request #173 from Webifi/main
Add image generation, new gpt-3.5 models and fix #172
This commit is contained in:
commit
f5501f3637
|
@ -22,6 +22,7 @@
|
||||||
"bulma": "^0.9.4",
|
"bulma": "^0.9.4",
|
||||||
"bulma-prefers-dark": "^0.1.0-beta.1",
|
"bulma-prefers-dark": "^0.1.0-beta.1",
|
||||||
"copy-to-clipboard": "^3.3.3",
|
"copy-to-clipboard": "^3.3.3",
|
||||||
|
"dexie": "^4.0.1-alpha.20",
|
||||||
"eslint-config-standard-with-typescript": "^35.0.0",
|
"eslint-config-standard-with-typescript": "^35.0.0",
|
||||||
"eslint-plugin-svelte3": "^4.0.0",
|
"eslint-plugin-svelte3": "^4.0.0",
|
||||||
"flourite": "^1.2.3",
|
"flourite": "^1.2.3",
|
||||||
|
@ -1601,6 +1602,12 @@
|
||||||
"node": ">=8"
|
"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": {
|
"node_modules/dir-glob": {
|
||||||
"version": "3.0.1",
|
"version": "3.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz",
|
||||||
|
|
|
@ -28,6 +28,7 @@
|
||||||
"bulma": "^0.9.4",
|
"bulma": "^0.9.4",
|
||||||
"bulma-prefers-dark": "^0.1.0-beta.1",
|
"bulma-prefers-dark": "^0.1.0-beta.1",
|
||||||
"copy-to-clipboard": "^3.3.3",
|
"copy-to-clipboard": "^3.3.3",
|
||||||
|
"dexie": "^4.0.1-alpha.20",
|
||||||
"eslint-config-standard-with-typescript": "^35.0.0",
|
"eslint-config-standard-with-typescript": "^35.0.0",
|
||||||
"eslint-plugin-svelte3": "^4.0.0",
|
"eslint-plugin-svelte3": "^4.0.0",
|
||||||
"flourite": "^1.2.3",
|
"flourite": "^1.2.3",
|
||||||
|
|
|
@ -245,8 +245,6 @@ $modal-background-background-color-dark: rgba($dark, 0.86) !default; // remove t
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/* Loading chat messages */
|
/* Loading chat messages */
|
||||||
.is-loading {
|
.is-loading {
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
|
@ -462,6 +460,10 @@ aside.menu.main-menu .menu-expanse {
|
||||||
.control.send .button {
|
.control.send .button {
|
||||||
width: 60px;
|
width: 60px;
|
||||||
}
|
}
|
||||||
|
textarea {
|
||||||
|
max-height: calc(100vh - (var(--chatContentPaddingBottom) + var(--runningTotalLineHeight) * var(--running-totals))) !important;
|
||||||
|
min-height: 38px !important;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media only screen and (max-width: 768px) {
|
@media only screen and (max-width: 768px) {
|
||||||
|
|
|
@ -2,9 +2,11 @@
|
||||||
// This makes it possible to override the OpenAI API base URL in the .env file
|
// 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 apiBase = import.meta.env.VITE_API_BASE || 'https://api.openai.com'
|
||||||
const endpointCompletions = import.meta.env.VITE_ENDPOINT_COMPLETIONS || '/v1/chat/completions'
|
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'
|
const endpointModels = import.meta.env.VITE_ENDPOINT_MODELS || '/v1/models'
|
||||||
|
|
||||||
export const getApiBase = ():string => apiBase
|
export const getApiBase = ():string => apiBase
|
||||||
export const getEndpointCompletions = ():string => endpointCompletions
|
export const getEndpointCompletions = ():string => endpointCompletions
|
||||||
|
export const getEndpointGenerations = ():string => endpointGenerations
|
||||||
export const getEndpointModels = ():string => endpointModels
|
export const getEndpointModels = ():string => endpointModels
|
||||||
</script>
|
</script>
|
|
@ -1,7 +1,8 @@
|
||||||
<script context="module" lang="ts">
|
<script context="module" lang="ts">
|
||||||
|
import { setImage } from './ImageStore.svelte'
|
||||||
// TODO: Integrate API calls
|
// TODO: Integrate API calls
|
||||||
import { addMessage, getLatestKnownModel, saveChatStore, setLatestKnownModel, subtractRunningTotal, updateRunningTotal } from './Storage.svelte'
|
import { addMessage, getLatestKnownModel, saveChatStore, setLatestKnownModel, subtractRunningTotal, updateRunningTotal } from './Storage.svelte'
|
||||||
import type { Chat, ChatCompletionOpts, Message, Model, Response, Usage } from './Types.svelte'
|
import type { Chat, ChatCompletionOpts, ChatImage, Message, Model, Response, ResponseImage, Usage } from './Types.svelte'
|
||||||
import { encode } from 'gpt-tokenizer'
|
import { encode } from 'gpt-tokenizer'
|
||||||
import { v4 as uuidv4 } from 'uuid'
|
import { v4 as uuidv4 } from 'uuid'
|
||||||
|
|
||||||
|
@ -64,6 +65,29 @@ export class ChatCompletionResponse {
|
||||||
this.promptTokenCount = tokens
|
this.promptTokenCount = tokens
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async updateImageFromSyncResponse (response: ResponseImage, prompt: string, model: Model) {
|
||||||
|
this.setModel(model)
|
||||||
|
for (let i = 0; i < response.data.length; i++) {
|
||||||
|
const d = response.data[i]
|
||||||
|
const message = {
|
||||||
|
role: 'image',
|
||||||
|
uuid: uuidv4(),
|
||||||
|
content: prompt,
|
||||||
|
image: await setImage(this.chat.id, { b64image: d.b64_json } as ChatImage),
|
||||||
|
model,
|
||||||
|
usage: {
|
||||||
|
prompt_tokens: 0,
|
||||||
|
completion_tokens: 1,
|
||||||
|
total_tokens: 1
|
||||||
|
} as Usage
|
||||||
|
} as Message
|
||||||
|
this.messages[i] = message
|
||||||
|
if (this.opts.autoAddMessages) addMessage(this.chat.id, message)
|
||||||
|
}
|
||||||
|
this.notifyMessageChange()
|
||||||
|
this.finish()
|
||||||
|
}
|
||||||
|
|
||||||
updateFromSyncResponse (response: Response) {
|
updateFromSyncResponse (response: Response) {
|
||||||
this.setModel(response.model)
|
this.setModel(response.model)
|
||||||
response.choices.forEach((choice, i) => {
|
response.choices.forEach((choice, i) => {
|
||||||
|
|
|
@ -2,12 +2,12 @@
|
||||||
import { ChatCompletionResponse } from './ChatCompletionResponse.svelte'
|
import { ChatCompletionResponse } from './ChatCompletionResponse.svelte'
|
||||||
import { mergeProfileFields, prepareSummaryPrompt } from './Profiles.svelte'
|
import { mergeProfileFields, prepareSummaryPrompt } from './Profiles.svelte'
|
||||||
import { countMessageTokens, countPromptTokens, getModelMaxTokens } from './Stats.svelte'
|
import { countMessageTokens, countPromptTokens, getModelMaxTokens } from './Stats.svelte'
|
||||||
import type { Chat, ChatCompletionOpts, ChatSettings, Message, Model, Request } from './Types.svelte'
|
import type { Chat, ChatCompletionOpts, ChatSettings, Message, Model, Request, RequestImageGeneration } from './Types.svelte'
|
||||||
import { deleteMessage, getChatSettingValueNullDefault, insertMessages, saveChatStore, getApiKey, addError } from './Storage.svelte'
|
import { deleteMessage, getChatSettingValueNullDefault, insertMessages, saveChatStore, getApiKey, addError } from './Storage.svelte'
|
||||||
import { scrollToBottom, scrollToMessage } from './Util.svelte'
|
import { scrollToBottom, scrollToMessage } from './Util.svelte'
|
||||||
import { getRequestSettingList, defaultModel } from './Settings.svelte'
|
import { getRequestSettingList, defaultModel } from './Settings.svelte'
|
||||||
import { EventStreamContentType, fetchEventSource } from '@microsoft/fetch-event-source'
|
import { EventStreamContentType, fetchEventSource } from '@microsoft/fetch-event-source'
|
||||||
import { getApiBase, getEndpointCompletions } from './ApiUtil.svelte'
|
import { getApiBase, getEndpointCompletions, getEndpointGenerations } from './ApiUtil.svelte'
|
||||||
import { v4 as uuidv4 } from 'uuid'
|
import { v4 as uuidv4 } from 'uuid'
|
||||||
|
|
||||||
export class ChatRequest {
|
export class ChatRequest {
|
||||||
|
@ -26,6 +26,66 @@ export class ChatRequest {
|
||||||
this.chat = chat
|
this.chat = chat
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Common error handler
|
||||||
|
async handleError (response) {
|
||||||
|
let errorResponse
|
||||||
|
try {
|
||||||
|
const errObj = await response.json()
|
||||||
|
errorResponse = errObj?.error?.message || errObj?.error?.code
|
||||||
|
if (!errorResponse && response.choices && response.choices[0]) {
|
||||||
|
errorResponse = response.choices[0]?.message?.content
|
||||||
|
}
|
||||||
|
errorResponse = errorResponse || 'Unexpected Response'
|
||||||
|
} catch (e) {
|
||||||
|
errorResponse = 'Unknown Response'
|
||||||
|
}
|
||||||
|
throw new Error(`${response.status} - ${errorResponse}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
async imageRequest (message: Message, prompt: string, count:number, messages: Message[], opts: ChatCompletionOpts, overrides: ChatSettings = {} as ChatSettings): Promise<ChatCompletionResponse> {
|
||||||
|
const _this = this
|
||||||
|
count = count || 1
|
||||||
|
_this.updating = true
|
||||||
|
_this.updatingMessage = 'Generating Image...'
|
||||||
|
const signal = _this.controller.signal
|
||||||
|
const size = this.chat.settings.imageGenerationSize
|
||||||
|
const request: RequestImageGeneration = {
|
||||||
|
prompt,
|
||||||
|
response_format: 'b64_json',
|
||||||
|
size,
|
||||||
|
n: count
|
||||||
|
}
|
||||||
|
const fetchOptions = {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${getApiKey()}`,
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify(request),
|
||||||
|
signal
|
||||||
|
}
|
||||||
|
const chatResponse = new ChatCompletionResponse(opts)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(getApiBase() + getEndpointGenerations(), fetchOptions)
|
||||||
|
if (!response.ok) {
|
||||||
|
await _this.handleError(response)
|
||||||
|
} else {
|
||||||
|
const json = await response.json()
|
||||||
|
// Remove updating indicator
|
||||||
|
_this.updating = false
|
||||||
|
_this.updatingMessage = ''
|
||||||
|
// console.log('image json', json, json?.data[0])
|
||||||
|
chatResponse.updateImageFromSyncResponse(json, prompt, 'dall-e-' + size)
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
chatResponse.updateFromError(e)
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
message.suppress = true
|
||||||
|
return chatResponse
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Send API request
|
* Send API request
|
||||||
* @param messages
|
* @param messages
|
||||||
|
@ -38,11 +98,33 @@ export class ChatRequest {
|
||||||
const chat = _this.chat
|
const chat = _this.chat
|
||||||
const chatSettings = _this.chat.settings
|
const chatSettings = _this.chat.settings
|
||||||
const chatId = chat.id
|
const chatId = chat.id
|
||||||
|
const imagePromptDetect = /^\s*(please|can\s+you|will\s+you)*\s*(give|generate|create|show|build|design)\s+(me)*\s*(an|a|set|a\s+set\s+of)*\s*([0-9]+|one|two|three|four)*\s+(image|photo|picture|pic)s*\s*(for\s+me)*\s*(of|[^a-z0-9]+|about|that\s+has|showing|with|having|depicting)\s+[^a-z0-9]*(.*)$/i
|
||||||
opts.chat = chat
|
opts.chat = chat
|
||||||
_this.updating = true
|
_this.updating = true
|
||||||
|
|
||||||
|
const lastMessage = messages[messages.length - 1]
|
||||||
|
|
||||||
|
if (chatSettings.imageGenerationSize && !opts.didSummary && !opts.summaryRequest && lastMessage?.role === 'user') {
|
||||||
|
const im = lastMessage.content.match(imagePromptDetect)
|
||||||
|
if (im) {
|
||||||
|
// console.log('image prompt request', im)
|
||||||
|
let n = parseInt((im[5] || '').toLowerCase().trim()
|
||||||
|
.replace(/one/ig, '1')
|
||||||
|
.replace(/two/ig, '2')
|
||||||
|
.replace(/three/ig, '3')
|
||||||
|
.replace(/four/ig, '4')
|
||||||
|
)
|
||||||
|
if (isNaN(n)) n = 1
|
||||||
|
n = Math.min(Math.max(1, n), 4)
|
||||||
|
return await this.imageRequest(lastMessage, im[9], n, messages, opts, overrides)
|
||||||
|
// throw new Error('Image prompt:' + im[7])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Submit only the role and content of the messages, provide the previous messages as well for context
|
// Submit only the role and content of the messages, provide the previous messages as well for context
|
||||||
const messageFilter = (m:Message) => !m.suppress && m.role !== 'error' && m.content && !m.summarized
|
const messageFilter = (m:Message) => !m.suppress &&
|
||||||
|
['user', 'assistant', 'system'].includes(m.role) &&
|
||||||
|
m.content && !m.summarized
|
||||||
const filtered = messages.filter(messageFilter)
|
const filtered = messages.filter(messageFilter)
|
||||||
|
|
||||||
// If we're doing continuous chat, do it
|
// If we're doing continuous chat, do it
|
||||||
|
@ -110,22 +192,6 @@ export class ChatRequest {
|
||||||
signal
|
signal
|
||||||
}
|
}
|
||||||
|
|
||||||
// Common error handler
|
|
||||||
const handleError = async (response) => {
|
|
||||||
let errorResponse
|
|
||||||
try {
|
|
||||||
const errObj = await response.json()
|
|
||||||
errorResponse = errObj?.error?.message || errObj?.error?.code
|
|
||||||
if (!errorResponse && response.choices && response.choices[0]) {
|
|
||||||
errorResponse = response.choices[0]?.message?.content
|
|
||||||
}
|
|
||||||
errorResponse = errorResponse || 'Unexpected Response'
|
|
||||||
} catch (e) {
|
|
||||||
errorResponse = 'Unknown Response'
|
|
||||||
}
|
|
||||||
throw new Error(`${response.status} - ${errorResponse}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
// fetchEventSource doesn't seem to throw on abort,
|
// fetchEventSource doesn't seem to throw on abort,
|
||||||
// so we deal with it ourselves
|
// so we deal with it ourselves
|
||||||
const abortListener = (e:Event) => {
|
const abortListener = (e:Event) => {
|
||||||
|
@ -174,7 +240,7 @@ export class ChatRequest {
|
||||||
// everything's good
|
// everything's good
|
||||||
} else {
|
} else {
|
||||||
// client-side errors are usually non-retriable:
|
// client-side errors are usually non-retriable:
|
||||||
await handleError(response)
|
await _this.handleError(response)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}).catch(err => {
|
}).catch(err => {
|
||||||
|
@ -187,7 +253,7 @@ export class ChatRequest {
|
||||||
*/
|
*/
|
||||||
const response = await fetch(getApiBase() + getEndpointCompletions(), fetchOptions)
|
const response = await fetch(getApiBase() + getEndpointCompletions(), fetchOptions)
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
await handleError(response)
|
await _this.handleError(response)
|
||||||
} else {
|
} else {
|
||||||
const json = await response.json()
|
const json = await response.json()
|
||||||
// Remove updating indicator
|
// Remove updating indicator
|
||||||
|
|
|
@ -6,10 +6,11 @@
|
||||||
import SvelteMarkdown from 'svelte-markdown'
|
import SvelteMarkdown from 'svelte-markdown'
|
||||||
import type { Message, Model, Chat } from './Types.svelte'
|
import type { Message, Model, Chat } from './Types.svelte'
|
||||||
import Fa from 'svelte-fa/src/fa.svelte'
|
import Fa from 'svelte-fa/src/fa.svelte'
|
||||||
import { faTrash, faDiagramPredecessor, faDiagramNext, faCircleCheck, faPaperPlane, faEye, faEyeSlash, faEllipsis } from '@fortawesome/free-solid-svg-icons/index'
|
import { faTrash, faDiagramPredecessor, faDiagramNext, faCircleCheck, faPaperPlane, faEye, faEyeSlash, faEllipsis, faDownload } from '@fortawesome/free-solid-svg-icons/index'
|
||||||
import { errorNotice, scrollToMessage } from './Util.svelte'
|
import { errorNotice, scrollToMessage } from './Util.svelte'
|
||||||
import { openModal } from 'svelte-modals'
|
import { openModal } from 'svelte-modals'
|
||||||
import PromptConfirm from './PromptConfirm.svelte'
|
import PromptConfirm from './PromptConfirm.svelte'
|
||||||
|
import { getImage } from './ImageStore.svelte'
|
||||||
|
|
||||||
export let message:Message
|
export let message:Message
|
||||||
export let chatId:number
|
export let chatId:number
|
||||||
|
@ -22,6 +23,7 @@
|
||||||
const isSystem = message.role === 'system'
|
const isSystem = message.role === 'system'
|
||||||
const isUser = message.role === 'user'
|
const isUser = message.role === 'user'
|
||||||
const isAssistant = message.role === 'assistant'
|
const isAssistant = message.role === 'assistant'
|
||||||
|
const isImage = message.role === 'image'
|
||||||
|
|
||||||
// Marked options
|
// Marked options
|
||||||
const markdownOptions = {
|
const markdownOptions = {
|
||||||
|
@ -34,9 +36,15 @@
|
||||||
let editing = false
|
let editing = false
|
||||||
let original:string
|
let original:string
|
||||||
let defaultModel:Model
|
let defaultModel:Model
|
||||||
|
let imageUrl:string
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
defaultModel = chatSettings.model
|
defaultModel = chatSettings.model
|
||||||
|
if (message?.image) {
|
||||||
|
getImage(message.image.id).then(i => {
|
||||||
|
imageUrl = 'data:image/png;base64, ' + i.b64image
|
||||||
|
})
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const edit = () => {
|
const edit = () => {
|
||||||
|
@ -168,17 +176,28 @@
|
||||||
saveChatStore()
|
saveChatStore()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const downloadImage = () => {
|
||||||
|
const filename = (message?.content || `${chat.name}-image-${message?.image?.id}`)
|
||||||
|
.replace(/([^a-z0-9- ]|\.)+/gi, '_').trim().slice(0, 80)
|
||||||
|
const a = document.createElement('a')
|
||||||
|
a.download = `${filename}.png`
|
||||||
|
a.href = imageUrl
|
||||||
|
document.body.appendChild(a)
|
||||||
|
a.click()
|
||||||
|
document.body.removeChild(a)
|
||||||
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<article
|
<article
|
||||||
id="{'message-' + message.uuid}"
|
id="{'message-' + message.uuid}"
|
||||||
class="message chat-message"
|
class="message chat-message"
|
||||||
class:is-info={isUser}
|
class:is-info={isUser}
|
||||||
class:is-success={isAssistant}
|
class:is-success={isAssistant || isImage}
|
||||||
class:is-warning={isSystem}
|
class:is-warning={isSystem}
|
||||||
class:is-danger={isError}
|
class:is-danger={isError}
|
||||||
class:user-message={isUser || isSystem}
|
class:user-message={isUser || isSystem}
|
||||||
class:assistant-message={isError || isAssistant}
|
class:assistant-message={isError || isAssistant || isImage}
|
||||||
class:summarized={message.summarized}
|
class:summarized={message.summarized}
|
||||||
class:suppress={message.suppress}
|
class:suppress={message.suppress}
|
||||||
class:editing={editing}
|
class:editing={editing}
|
||||||
|
@ -192,6 +211,9 @@
|
||||||
<div id={'edit-' + message.uuid} class="message-editor" bind:innerText={message.content} contenteditable
|
<div id={'edit-' + message.uuid} class="message-editor" bind:innerText={message.content} contenteditable
|
||||||
on:input={update} on:blur={exit} />
|
on:input={update} on:blur={exit} />
|
||||||
</form>
|
</form>
|
||||||
|
{#if imageUrl}
|
||||||
|
<img src={imageUrl} alt="">
|
||||||
|
{/if}
|
||||||
{:else}
|
{:else}
|
||||||
<div
|
<div
|
||||||
class="message-display"
|
class="message-display"
|
||||||
|
@ -207,6 +229,9 @@
|
||||||
options={markdownOptions}
|
options={markdownOptions}
|
||||||
renderers={{ code: Code, html: Code }}
|
renderers={{ code: Code, html: Code }}
|
||||||
/>
|
/>
|
||||||
|
{#if imageUrl}
|
||||||
|
<img src={imageUrl} alt="">
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
{#if isSystem}
|
{#if isSystem}
|
||||||
|
@ -261,7 +286,7 @@
|
||||||
<a
|
<a
|
||||||
href={'#'}
|
href={'#'}
|
||||||
title="Delete this message"
|
title="Delete this message"
|
||||||
class=" msg-delete button is-small"
|
class="msg-delete button is-small"
|
||||||
on:click|preventDefault={() => {
|
on:click|preventDefault={() => {
|
||||||
checkDelete()
|
checkDelete()
|
||||||
}}
|
}}
|
||||||
|
@ -273,11 +298,11 @@
|
||||||
{/if}
|
{/if}
|
||||||
</a>
|
</a>
|
||||||
{/if}
|
{/if}
|
||||||
{#if !message.summarized && !isError}
|
{#if !isImage && !message.summarized && !isError}
|
||||||
<a
|
<a
|
||||||
href={'#'}
|
href={'#'}
|
||||||
title="Truncate from here and send"
|
title="Truncate from here and send"
|
||||||
class=" msg-truncate button is-small"
|
class="msg-truncate button is-small"
|
||||||
on:click|preventDefault={() => {
|
on:click|preventDefault={() => {
|
||||||
checkTruncate()
|
checkTruncate()
|
||||||
}}
|
}}
|
||||||
|
@ -289,11 +314,11 @@
|
||||||
{/if}
|
{/if}
|
||||||
</a>
|
</a>
|
||||||
{/if}
|
{/if}
|
||||||
{#if !message.summarized && !isSystem && !isError}
|
{#if !isImage && !message.summarized && !isSystem && !isError}
|
||||||
<a
|
<a
|
||||||
href={'#'}
|
href={'#'}
|
||||||
title={(message.suppress ? 'Uns' : 'S') + 'uppress message from submission'}
|
title={(message.suppress ? 'Uns' : 'S') + 'uppress message from submission'}
|
||||||
class=" msg-supress button is-small"
|
class="msg-supress button is-small"
|
||||||
on:click|preventDefault={() => {
|
on:click|preventDefault={() => {
|
||||||
setSuppress(!message.suppress)
|
setSuppress(!message.suppress)
|
||||||
}}
|
}}
|
||||||
|
@ -305,6 +330,18 @@
|
||||||
{/if}
|
{/if}
|
||||||
</a>
|
</a>
|
||||||
{/if}
|
{/if}
|
||||||
|
{#if imageUrl}
|
||||||
|
<a
|
||||||
|
href={'#'}
|
||||||
|
title="Download Image"
|
||||||
|
class="msg-image button is-small"
|
||||||
|
on:click|preventDefault={() => {
|
||||||
|
downloadImage()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span class="icon"><Fa icon={faDownload} /></span>
|
||||||
|
</a>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
<script context="module" lang="ts">
|
<script context="module" lang="ts">
|
||||||
import { get } from 'svelte/store'
|
import { get } from 'svelte/store'
|
||||||
import type { Chat } from './Types.svelte'
|
import type { Chat } from './Types.svelte'
|
||||||
import { chatsStorage } from './Storage.svelte'
|
import { chatsStorage, getChat } from './Storage.svelte'
|
||||||
import { getExcludeFromProfile } from './Settings.svelte'
|
import { getExcludeFromProfile } from './Settings.svelte'
|
||||||
|
import { getImage } from './ImageStore.svelte'
|
||||||
|
|
||||||
export const exportAsMarkdown = (chatId: number) => {
|
export const exportAsMarkdown = (chatId: number) => {
|
||||||
const chats = get(chatsStorage)
|
const chats = get(chatsStorage)
|
||||||
|
@ -27,9 +28,13 @@
|
||||||
document.body.removeChild(a)
|
document.body.removeChild(a)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const exportChatAsJSON = (chatId: number) => {
|
export const exportChatAsJSON = async (chatId: number) => {
|
||||||
const chats = get(chatsStorage)
|
const chat = JSON.parse(JSON.stringify(getChat(chatId))) as Chat
|
||||||
const chat = chats.find((chat) => chat.id === chatId) as Chat
|
for (let i = 0; i < chat.messages.length; i++) {
|
||||||
|
// Pull images out of indexedDB store for JSON download
|
||||||
|
const m = chat.messages[i]
|
||||||
|
if (m.image) m.image = await getImage(m.image.id)
|
||||||
|
}
|
||||||
const exportContent = JSON.stringify(chat)
|
const exportContent = JSON.stringify(chat)
|
||||||
const blob = new Blob([exportContent], { type: 'text/json' })
|
const blob = new Blob([exportContent], { type: 'text/json' })
|
||||||
const url = URL.createObjectURL(blob)
|
const url = URL.createObjectURL(blob)
|
||||||
|
|
|
@ -0,0 +1,71 @@
|
||||||
|
<script context="module" lang="ts">
|
||||||
|
import Dexie, { type Table } from 'dexie'
|
||||||
|
import type { ChatImage } from './Types.svelte'
|
||||||
|
import { v4 as uuidv4 } from 'uuid'
|
||||||
|
|
||||||
|
let _hasIndexedDb = !!window.indexedDB
|
||||||
|
const dbCheck = _hasIndexedDb && window.indexedDB.open('test')
|
||||||
|
if (_hasIndexedDb) dbCheck.onerror = () => { _hasIndexedDb = false }
|
||||||
|
|
||||||
|
const imageCache: Record<string, ChatImage> = {}
|
||||||
|
|
||||||
|
class ChatImageStore extends Dexie {
|
||||||
|
images!: Table<ChatImage>
|
||||||
|
constructor () {
|
||||||
|
super('chatImageStore')
|
||||||
|
this.version(1).stores({
|
||||||
|
images: 'id' // Primary key and indexed props
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const imageDb = new ChatImageStore()
|
||||||
|
|
||||||
|
export const hasIndexedDb = () => _hasIndexedDb
|
||||||
|
|
||||||
|
export const getImage = async (uuid:string): Promise<ChatImage> => {
|
||||||
|
let image = imageCache[uuid]
|
||||||
|
if (image || !_hasIndexedDb) return image
|
||||||
|
image = await imageDb.images.get(uuid) as any
|
||||||
|
imageCache[uuid] = image
|
||||||
|
return image
|
||||||
|
}
|
||||||
|
|
||||||
|
export const deleteImage = async (chatId:number, uuid:string): Promise<void> => {
|
||||||
|
const cached = imageCache[uuid]
|
||||||
|
if (cached) cached.chats = cached.chats?.filter(c => c !== chatId)
|
||||||
|
if (!cached?.chats?.length) delete imageCache[uuid]
|
||||||
|
if (_hasIndexedDb) {
|
||||||
|
const stored:ChatImage = await imageDb.images.get({ id: uuid }) as any
|
||||||
|
if (stored) stored.chats = stored.chats?.filter(c => c !== chatId)
|
||||||
|
if (!stored?.chats?.length) {
|
||||||
|
imageDb.images.delete(uuid)
|
||||||
|
} else if (stored) {
|
||||||
|
await setImage(chatId, stored)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const setImage = async (chatId:number, image:ChatImage): Promise<ChatImage> => {
|
||||||
|
image.id = image.id || uuidv4()
|
||||||
|
let current: ChatImage
|
||||||
|
if (_hasIndexedDb) {
|
||||||
|
current = await imageDb.images.get({ id: image.id }) as any
|
||||||
|
} else {
|
||||||
|
current = imageCache[image.id]
|
||||||
|
}
|
||||||
|
current = current || image
|
||||||
|
current.chats = current.chats || []
|
||||||
|
if (!(chatId in current.chats)) current.chats.push(chatId)
|
||||||
|
imageCache[current.id] = current
|
||||||
|
if (_hasIndexedDb) {
|
||||||
|
imageDb.images.put(current, current.id)
|
||||||
|
}
|
||||||
|
const clone = JSON.parse(JSON.stringify(current))
|
||||||
|
// Return a copy without the payload so local storage doesn't get clobbered
|
||||||
|
delete clone.b64image
|
||||||
|
delete clone.chats
|
||||||
|
return clone
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
|
@ -14,10 +14,38 @@ const modelDetails : Record<string, ModelDetail> = {
|
||||||
completion: 0.00006, // $0.06 per 1000 tokens completion
|
completion: 0.00006, // $0.06 per 1000 tokens completion
|
||||||
max: 8192 // 8k max token buffer
|
max: 8192 // 8k max token buffer
|
||||||
},
|
},
|
||||||
|
'gpt-3.5-turbo-0613': {
|
||||||
|
prompt: 0.0000015, // $0.0015 per 1000 tokens prompt
|
||||||
|
completion: 0.0000015, // $0.0015 per 1000 tokens completion
|
||||||
|
max: 4096 // 4k max token buffer
|
||||||
|
},
|
||||||
'gpt-3.5': {
|
'gpt-3.5': {
|
||||||
prompt: 0.000002, // $0.002 per 1000 tokens prompt
|
prompt: 0.000002, // $0.002 per 1000 tokens prompt
|
||||||
completion: 0.000002, // $0.002 per 1000 tokens completion
|
completion: 0.000002, // $0.002 per 1000 tokens completion
|
||||||
max: 4096 // 4k max token buffer
|
max: 4096 // 4k max token buffer
|
||||||
|
},
|
||||||
|
'gpt-3.5-turbo-16k': {
|
||||||
|
prompt: 0.000003, // $0.003 per 1000 tokens prompt
|
||||||
|
completion: 0.000004, // $0.004 per 1000 tokens completion
|
||||||
|
max: 16384 // 16k max token buffer
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const imageModels : Record<string, ModelDetail> = {
|
||||||
|
'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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -35,15 +63,18 @@ export const supportedModels : Record<string, ModelDetail> = {
|
||||||
'gpt-4-32k': modelDetails['gpt-4-32k'],
|
'gpt-4-32k': modelDetails['gpt-4-32k'],
|
||||||
'gpt-4-32k-0314': modelDetails['gpt-4-32k'],
|
'gpt-4-32k-0314': modelDetails['gpt-4-32k'],
|
||||||
'gpt-3.5-turbo': modelDetails['gpt-3.5'],
|
'gpt-3.5-turbo': modelDetails['gpt-3.5'],
|
||||||
'gpt-3.5-turbo-0301': modelDetails['gpt-3.5']
|
'gpt-3.5-turbo-16k': modelDetails['gpt-3.5-turbo-16k'],
|
||||||
|
'gpt-3.5-turbo-0301': modelDetails['gpt-3.5'],
|
||||||
|
'gpt-3.5-turbo-0613': modelDetails['gpt-3.5-turbo-0613']
|
||||||
}
|
}
|
||||||
|
|
||||||
const lookupList = {
|
const lookupList = {
|
||||||
|
...imageModels,
|
||||||
...modelDetails,
|
...modelDetails,
|
||||||
...supportedModels
|
...supportedModels
|
||||||
}
|
}
|
||||||
|
|
||||||
export const supportedModelKeys = Object.keys(supportedModels)
|
export const supportedModelKeys = Object.keys({ ...supportedModels, ...imageModels })
|
||||||
|
|
||||||
const tpCache : Record<string, ModelDetail> = {}
|
const tpCache : Record<string, ModelDetail> = {}
|
||||||
|
|
||||||
|
|
|
@ -86,6 +86,7 @@ const defaults:ChatSettings = {
|
||||||
autoStartSession: false,
|
autoStartSession: false,
|
||||||
trainingPrompts: [],
|
trainingPrompts: [],
|
||||||
hiddenPromptPrefix: '',
|
hiddenPromptPrefix: '',
|
||||||
|
imageGenerationSize: '',
|
||||||
// useResponseAlteration: false,
|
// useResponseAlteration: false,
|
||||||
// responseAlterations: [],
|
// responseAlterations: [],
|
||||||
isDirty: false
|
isDirty: false
|
||||||
|
@ -97,6 +98,12 @@ const excludeFromProfile = {
|
||||||
isDirty: true
|
isDirty: true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const imageGenerationSizes = [
|
||||||
|
'1024x1024', '512x512', '256x256'
|
||||||
|
]
|
||||||
|
|
||||||
|
export const imageGenerationSizeTypes = ['', ...imageGenerationSizes]
|
||||||
|
|
||||||
const profileSetting: ChatSetting & SettingSelect = {
|
const profileSetting: ChatSetting & SettingSelect = {
|
||||||
key: 'profile',
|
key: 'profile',
|
||||||
name: 'Profile',
|
name: 'Profile',
|
||||||
|
@ -269,6 +276,18 @@ const summarySettings: ChatSetting[] = [
|
||||||
placeholder: 'Enter a prompt that will be used to summarize past prompts here.',
|
placeholder: 'Enter a prompt that will be used to summarize past prompts here.',
|
||||||
type: 'textarea',
|
type: 'textarea',
|
||||||
hide: (chatId) => getChatSettings(chatId).continuousChat !== 'summary'
|
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 } })
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
@ -6,7 +6,10 @@
|
||||||
import { v4 as uuidv4 } from 'uuid'
|
import { v4 as uuidv4 } from 'uuid'
|
||||||
import { getProfile, getProfiles, isStaticProfile, newNameForProfile, restartProfile } from './Profiles.svelte'
|
import { getProfile, getProfiles, isStaticProfile, newNameForProfile, restartProfile } from './Profiles.svelte'
|
||||||
import { errorNotice } from './Util.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 chatsStorage = persisted('chats', [] as Chat[])
|
||||||
export const latestModelMap = persisted('latestModelMap', {} as Record<Model, Model>) // What was returned when a model was requested
|
export const latestModelMap = persisted('latestModelMap', {} as Record<Model, Model>) // What was returned when a model was requested
|
||||||
export const globalStorage = persisted('global', {} as GlobalSettings)
|
export const globalStorage = persisted('global', {} as GlobalSettings)
|
||||||
|
@ -53,7 +56,7 @@
|
||||||
return chatId
|
return chatId
|
||||||
}
|
}
|
||||||
|
|
||||||
export const addChatFromJSON = (json: string): number => {
|
export const addChatFromJSON = async (json: string): Promise<number> => {
|
||||||
const chats = get(chatsStorage)
|
const chats = get(chatsStorage)
|
||||||
|
|
||||||
// Find the max chatId
|
// Find the max chatId
|
||||||
|
@ -73,6 +76,10 @@
|
||||||
|
|
||||||
chat.id = chatId
|
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
|
// Add a new chat
|
||||||
chats.push(chat)
|
chats.push(chat)
|
||||||
chatsStorage.set(chats)
|
chatsStorage.set(chats)
|
||||||
|
@ -154,7 +161,10 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
export const clearChats = () => {
|
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 = () => {
|
export const saveChatStore = () => {
|
||||||
const chats = get(chatsStorage)
|
const chats = get(chatsStorage)
|
||||||
|
@ -268,13 +278,16 @@
|
||||||
const chat = chats.find((chat) => chat.id === chatId) as Chat
|
const chat = chats.find((chat) => chat.id === chatId) as Chat
|
||||||
const index = chat.messages.findIndex((m) => m.uuid === uuid)
|
const index = chat.messages.findIndex((m) => m.uuid === uuid)
|
||||||
const message = getMessage(chat, uuid)
|
const message = getMessage(chat, uuid)
|
||||||
if (message && message.summarized) throw new Error('Unable to delete summarized message')
|
if (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?.summary) throw new Error('Unable to directly delete message summary')
|
||||||
// const found = chat.messages.filter((m) => m.uuid === uuid)
|
// const found = chat.messages.filter((m) => m.uuid === uuid)
|
||||||
if (index < 0) {
|
if (index < 0) {
|
||||||
console.error(`Unable to find and delete message with ID: ${uuid}`)
|
console.error(`Unable to find and delete message with ID: ${uuid}`)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if (message?.image) {
|
||||||
|
deleteImage(chatId, message.image.id)
|
||||||
|
}
|
||||||
// console.warn(`Deleting message with ID: ${uuid}`, found, index)
|
// console.warn(`Deleting message with ID: ${uuid}`, found, index)
|
||||||
chat.messages.splice(index, 1) // remove item
|
chat.messages.splice(index, 1) // remove item
|
||||||
chatsStorage.set(chats)
|
chatsStorage.set(chats)
|
||||||
|
@ -303,10 +316,21 @@
|
||||||
|
|
||||||
export const deleteChat = (chatId: number) => {
|
export const deleteChat = (chatId: number) => {
|
||||||
const chats = get(chatsStorage)
|
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))
|
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 chats = get(chatsStorage)
|
||||||
const chat = chats.find((chat) => chat.id === chatId) as Chat
|
const chat = chats.find((chat) => chat.id === chatId) as Chat
|
||||||
const nameMap = chats.reduce((a, chat) => { a[chat.name] = chat; return a }, {})
|
const nameMap = chats.reduce((a, chat) => { a[chat.name] = chat; return a }, {})
|
||||||
|
@ -323,6 +347,8 @@
|
||||||
// Set new name
|
// Set new name
|
||||||
chatCopy.name = cname
|
chatCopy.name = cname
|
||||||
|
|
||||||
|
await updateChatImages(chatId, chatCopy)
|
||||||
|
|
||||||
// Add a new chat
|
// Add a new chat
|
||||||
chats.push(chatCopy)
|
chats.push(chatCopy)
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,11 @@
|
||||||
<script context="module" lang="ts">
|
<script context="module" lang="ts">
|
||||||
import type { supportedModelKeys } from './Models.svelte'
|
import { supportedModelKeys } from './Models.svelte'
|
||||||
|
import { imageGenerationSizeTypes } from './Settings.svelte'
|
||||||
|
|
||||||
export type Model = typeof supportedModelKeys[number];
|
export type Model = typeof supportedModelKeys[number];
|
||||||
|
|
||||||
|
export type ImageGenerationSizes = typeof imageGenerationSizeTypes[number];
|
||||||
|
|
||||||
export type ModelDetail = {
|
export type ModelDetail = {
|
||||||
prompt: number;
|
prompt: number;
|
||||||
completion: number;
|
completion: number;
|
||||||
|
@ -15,8 +18,14 @@
|
||||||
total_tokens: number;
|
total_tokens: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export interface ChatImage {
|
||||||
|
id: string;
|
||||||
|
b64image: string;
|
||||||
|
chats: number[];
|
||||||
|
}
|
||||||
|
|
||||||
export type Message = {
|
export type Message = {
|
||||||
role: 'user' | 'assistant' | 'system' | 'error';
|
role: 'user' | 'assistant' | 'system' | 'error' | 'image';
|
||||||
content: string;
|
content: string;
|
||||||
uuid: string;
|
uuid: string;
|
||||||
usage?: Usage;
|
usage?: Usage;
|
||||||
|
@ -27,6 +36,7 @@
|
||||||
suppress?: boolean;
|
suppress?: boolean;
|
||||||
finish_reason?: string;
|
finish_reason?: string;
|
||||||
streaming?: boolean;
|
streaming?: boolean;
|
||||||
|
image?: ChatImage;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ResponseAlteration = {
|
export type ResponseAlteration = {
|
||||||
|
@ -35,6 +45,23 @@
|
||||||
replace: string;
|
replace: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type ResponseImageDetail = {
|
||||||
|
url: string;
|
||||||
|
b64_json: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ResponseImage = {
|
||||||
|
created: number;
|
||||||
|
data: ResponseImageDetail[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export type RequestImageGeneration = {
|
||||||
|
prompt: string;
|
||||||
|
n?: number;
|
||||||
|
size?: ImageGenerationSizes;
|
||||||
|
response_format?: keyof ResponseImageDetail;
|
||||||
|
}
|
||||||
|
|
||||||
export type Request = {
|
export type Request = {
|
||||||
model: Model;
|
model: Model;
|
||||||
messages?: Message[];
|
messages?: Message[];
|
||||||
|
@ -66,6 +93,7 @@
|
||||||
systemPrompt: string;
|
systemPrompt: string;
|
||||||
autoStartSession: boolean;
|
autoStartSession: boolean;
|
||||||
hiddenPromptPrefix: string;
|
hiddenPromptPrefix: string;
|
||||||
|
imageGenerationSize: ImageGenerationSizes;
|
||||||
trainingPrompts?: Message[];
|
trainingPrompts?: Message[];
|
||||||
useResponseAlteration?: boolean;
|
useResponseAlteration?: boolean;
|
||||||
responseAlterations?: ResponseAlteration[];
|
responseAlterations?: ResponseAlteration[];
|
||||||
|
|
|
@ -21,6 +21,13 @@
|
||||||
const anyEl = el as any // Oh how I hate typescript. All the markup of Java with no real payoff..
|
const anyEl = el as any // Oh how I hate typescript. All the markup of Java with no real payoff..
|
||||||
if (!anyEl.__didAutoGrow) el.style.height = '38px' // don't use "auto" here. Firefox will over-size.
|
if (!anyEl.__didAutoGrow) el.style.height = '38px' // don't use "auto" here. Firefox will over-size.
|
||||||
el.style.height = el.scrollHeight + 'px'
|
el.style.height = el.scrollHeight + 'px'
|
||||||
|
setTimeout(() => {
|
||||||
|
if (el.scrollHeight > el.getBoundingClientRect().height + 5) {
|
||||||
|
el.style.overflowY = 'auto'
|
||||||
|
} else {
|
||||||
|
el.style.overflowY = ''
|
||||||
|
}
|
||||||
|
}, 0)
|
||||||
anyEl.__didAutoGrow = true // don't resize this one again unless it's via an event
|
anyEl.__didAutoGrow = true // don't resize this one again unless it's via an event
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue