This commit is contained in:
2025-07-05 22:32:51 +09:00
parent 55a78b2a02
commit 574e04fa19
21 changed files with 556 additions and 541 deletions

8
package-lock.json generated
View File

@@ -30,7 +30,6 @@
"flourite": "^1.3.0", "flourite": "^1.3.0",
"gpt-tokenizer": "^2.1.2", "gpt-tokenizer": "^2.1.2",
"katex": "^0.16.10", "katex": "^0.16.10",
"llama-tokenizer-js": "^1.2.2",
"postcss": "^8.4.32", "postcss": "^8.4.32",
"sass": "^1.77.6", "sass": "^1.77.6",
"stacking-order": "^2.0.0", "stacking-order": "^2.0.0",
@@ -3834,13 +3833,6 @@
"node": ">= 0.8.0" "node": ">= 0.8.0"
} }
}, },
"node_modules/llama-tokenizer-js": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/llama-tokenizer-js/-/llama-tokenizer-js-1.2.2.tgz",
"integrity": "sha512-Wmth393dc3odWU3IzARJ3r2oIfWgw9GdJ5Gm+hGhfECNO18UHLRqEFSf511jn4E9KcQGzuuKw4Wl08pHAemLAw==",
"dev": true,
"license": "MIT"
},
"node_modules/locate-path": { "node_modules/locate-path": {
"version": "6.0.0", "version": "6.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",

View File

@@ -36,7 +36,6 @@
"flourite": "^1.3.0", "flourite": "^1.3.0",
"gpt-tokenizer": "^2.1.2", "gpt-tokenizer": "^2.1.2",
"katex": "^0.16.10", "katex": "^0.16.10",
"llama-tokenizer-js": "^1.2.2",
"postcss": "^8.4.32", "postcss": "^8.4.32",
"sass": "^1.77.6", "sass": "^1.77.6",
"stacking-order": "^2.0.0", "stacking-order": "^2.0.0",

View File

@@ -7,7 +7,7 @@
import Home from './lib/Home.svelte' import Home from './lib/Home.svelte'
import Chat from './lib/Chat.svelte' import Chat from './lib/Chat.svelte'
import NewChat from './lib/NewChat.svelte' import NewChat from './lib/NewChat.svelte'
import { chatsStorage, setGlobalSettingValueByKey } from './lib/Storage.svelte' import { chatsStorage } from './lib/Storage.svelte'
import { Modals, closeModal } from 'svelte-modals' import { Modals, closeModal } from 'svelte-modals'
import { dispatchModalEsc, checkModalEsc } from './lib/Util.svelte' import { dispatchModalEsc, checkModalEsc } from './lib/Util.svelte'
import { set as setOpenAI } from './lib/providers/openai/util.svelte' import { set as setOpenAI } from './lib/providers/openai/util.svelte'
@@ -19,10 +19,6 @@
if (urlParams.has('key')) { if (urlParams.has('key')) {
setOpenAI({ apiKey: urlParams.get('key') as string }) setOpenAI({ apiKey: urlParams.get('key') as string })
} }
if (urlParams.has('petals')) {
console.log('enablePetals')
setGlobalSettingValueByKey('enablePetals', true)
}
// The definition of the routes with some conditions // The definition of the routes with some conditions
const routes = { const routes = {

View File

@@ -2,18 +2,16 @@
import { persisted } from 'svelte-local-storage-store' import { persisted } from 'svelte-local-storage-store'
import { get } from 'svelte/store' import { get } from 'svelte/store'
// 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 apiBaseStorage = persisted('apiBase', 'https://api.openai.com'); const apiBaseStorage = persisted('apiBase', 'https://api.openai.com')
const apiBase = get(apiBaseStorage) || 'https://api.openai.com'; const apiBase = get(apiBaseStorage) || '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 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'
const endpointEmbeddings = import.meta.env.VITE_ENDPOINT_EMBEDDINGS || '/v1/embeddings' const endpointEmbeddings = import.meta.env.VITE_ENDPOINT_EMBEDDINGS || '/v1/embeddings'
const petalsBase = import.meta.env.VITE_PEDALS_WEBSOCKET || 'wss://chat.petals.dev'
const endpointPetals = import.meta.env.VITE_PEDALS_WEBSOCKET || '/api/v2/generate'
export const setApiBase = (e: Record<string>) => { export const setApiBase = (e: string) => {
console.log(e); console.log(e)
apiBaseStorage.set(e || '') apiBaseStorage.set(e || '')
} }
export const getApiBase = ():string => apiBase export const getApiBase = ():string => apiBase
@@ -21,6 +19,4 @@
export const getEndpointGenerations = ():string => endpointGenerations export const getEndpointGenerations = ():string => endpointGenerations
export const getEndpointModels = ():string => endpointModels export const getEndpointModels = ():string => endpointModels
export const getEndpointEmbeddings = ():string => endpointEmbeddings export const getEndpointEmbeddings = ():string => endpointEmbeddings
export const getPetalsBase = ():string => petalsBase
export const getPetalsWebsocket = ():string => endpointPetals
</script> </script>

View File

@@ -51,15 +51,25 @@
let recording = false let recording = false
let lastSubmitRecorded = false let lastSubmitRecorded = false
$: chat = $chatsStorage.find((chat) => chat.id === chatId) as Chat // Optimize chat lookup to avoid expensive find() on every chats update
$: chatSettings = chat?.settings let chat: Chat
let chatSettings: ChatSettings
let showSettingsModal let showSettingsModal
let scDelay // Only update chat when chatId changes or when the specific chat is updated
$: {
const foundChat = $chatsStorage.find((c) => c.id === chatId)
if (foundChat && (!chat || chat.id !== foundChat.id || chat !== foundChat)) {
chat = foundChat
chatSettings = foundChat.settings
}
}
let scDelay: any
const onStateChange = (...args:any) => { const onStateChange = (...args:any) => {
if (!chat) return if (!chat) return
clearTimeout(scDelay) if (scDelay) clearTimeout(scDelay)
setTimeout(() => { scDelay = setTimeout(() => {
if (chat.startSession) { if (chat.startSession) {
restartProfile(chatId) restartProfile(chatId)
if (chat.startSession) { if (chat.startSession) {
@@ -101,6 +111,11 @@
onDestroy(async () => { onDestroy(async () => {
// clean up // clean up
// Clear timer to prevent memory leaks
if (scDelay) {
clearTimeout(scDelay)
scDelay = null
}
// abort any pending requests. // abort any pending requests.
chatRequest.controller.abort() chatRequest.controller.abort()
ttsStop() ttsStop()
@@ -286,10 +301,10 @@
chatRequest.updatingMessage = '' chatRequest.updatingMessage = ''
const userMessagesCount = chat.messages.filter(message => message.role === "user").length; const userMessagesCount = chat.messages.filter(message => message.role === 'user').length
const assiMessagesCount = chat.messages.filter(message => message.role === "assistant").length; const assiMessagesCount = chat.messages.filter(message => message.role === 'assistant').length
if (userMessagesCount == 3 && chat.name.startsWith("Chat ")) { if (userMessagesCount == 3 && chat.name.startsWith('Chat ')) {
suggestName(); suggestName()
} }
focusInput() focusInput()
@@ -305,7 +320,7 @@
const suggestMessages = $currentChatMessages.slice(0, 4) const suggestMessages = $currentChatMessages.slice(0, 4)
suggestMessages.push(suggestMessage) suggestMessages.push(suggestMessage)
const currentModel = chat.settings.model; const currentModel = chat.settings.model
// chat.settings.model = "gpt-4o"; // chat.settings.model = "gpt-4o";
chatRequest.updating = true chatRequest.updating = true
@@ -318,7 +333,7 @@
maxTokens: 30 maxTokens: 30
}) })
chat.settings.model = currentModel; chat.settings.model = currentModel
try { try {
await response.promiseToFinish() await response.promiseToFinish()

View File

@@ -157,74 +157,72 @@
reader.readAsText(image) reader.readAsText(image)
} }
function dumpLocalStorage(){ function dumpLocalStorage () {
try { try {
let storageObject = {}; const storageObject = {}
for (let i = 0; i < localStorage.length; i++) { for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i); const key = localStorage.key(i)
if (key) { if (key) {
storageObject[key] = localStorage.getItem(key); storageObject[key] = localStorage.getItem(key)
} }
} }
const dataStr = JSON.stringify(storageObject, null, 2); const dataStr = JSON.stringify(storageObject, null, 2)
const blob = new Blob([dataStr], { type: "application/json" }); const blob = new Blob([dataStr], { type: 'application/json' })
const url = URL.createObjectURL(blob); const url = URL.createObjectURL(blob)
const link = document.createElement("a"); const link = document.createElement('a')
link.href = url; link.href = url
const now = new Date(); const now = new Date()
const dateTimeStr = now.toISOString().replace(/:\d+\.\d+Z$/, '').replace(/-|:/g, '_'); const dateTimeStr = now.toISOString().replace(/:\d+\.\d+Z$/, '').replace(/-|:/g, '_')
link.download = `ChatGPT-web-${dateTimeStr}.json`; link.download = `ChatGPT-web-${dateTimeStr}.json`
document.body.appendChild(link); document.body.appendChild(link)
link.click(); link.click()
document.body.removeChild(link); document.body.removeChild(link)
URL.revokeObjectURL(url); URL.revokeObjectURL(url)
} catch (error) { } catch (error) {
console.error('Error dumping localStorage:', error); console.error('Error dumping localStorage:', error)
} }
} }
function loadLocalStorage() { function loadLocalStorage () {
var fileInput = document.createElement('input'); const fileInput = document.createElement('input')
fileInput.type = "file"; fileInput.type = 'file'
fileInput.addEventListener('change', function(e) { fileInput.addEventListener('change', function (e) {
var file = e.target.files[0]; const file = e.target.files[0]
if (file) { if (file) {
var reader = new FileReader(); const reader = new FileReader()
reader.onload = function(e) { reader.onload = function (e) {
var data = JSON.parse(e.target.result); const data = JSON.parse(e.target.result)
Object.keys(data).forEach(function(key) { Object.keys(data).forEach(function (key) {
localStorage.setItem(key, data[key]); localStorage.setItem(key, data[key])
}); })
window.location.reload(); window.location.reload()
};
reader.readAsText(file);
} }
}); reader.readAsText(file)
document.body.appendChild(fileInput); }
fileInput.click(); })
fileInput.remove(); document.body.appendChild(fileInput)
fileInput.click()
fileInput.remove()
} }
function backupLocalStorage() { function backupLocalStorage () {
try { try {
let storageObject = {}; const storageObject = {}
for (let i = 0; i < localStorage.length; i++) { for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i); const key = localStorage.key(i)
if (key) { if (key) {
storageObject[key] = localStorage.getItem(key); storageObject[key] = localStorage.getItem(key)
} }
} }
const dataStr = JSON.stringify(storageObject, null, 2); const dataStr = JSON.stringify(storageObject, null, 2)
const now = new Date(); const now = new Date()
const dateTimeStr = now.toISOString().replace(/:\d+\.\d+Z$/, '').replace(/-|:/g, '_'); const dateTimeStr = now.toISOString().replace(/:\d+\.\d+Z$/, '').replace(/-|:/g, '_')
localStorage.setItem(`prev-${dateTimeStr}`, dataStr); localStorage.setItem(`prev-${dateTimeStr}`, dataStr)
} catch (error) { } catch (error) {
console.error('Error backing up localStorage:', error); console.error('Error backing up localStorage:', error)
}
} }
}
</script> </script>

View File

@@ -196,7 +196,7 @@ export class ChatRequest {
if (value > maxAllowed || value < 1) value = null // if over max model, do not define max if (value > maxAllowed || value < 1) value = null // if over max model, do not define max
if (value) value = Math.floor(value) if (value) value = Math.floor(value)
if (modelDetail.reasoning == true) { if (modelDetail.reasoning == true) {
key = 'max_completion_tokens'; key = 'max_completion_tokens'
} }
} }
if (key === 'n') { if (key === 'n') {
@@ -351,12 +351,21 @@ export class ChatRequest {
* ************************************************************* * *************************************************************
*/ */
let promptSize = countPromptTokens(top.concat(rw), model, chat) + countPadding // Pre-calculate top tokens once to avoid repeated calculations
const topTokens = countPromptTokens(top, model, chat)
let rwTokens = countPromptTokens(rw, model, chat)
let promptSize = topTokens + rwTokens + countPadding
while (rw.length && rw.length > pinBottom && promptSize >= threshold) { while (rw.length && rw.length > pinBottom && promptSize >= threshold) {
const rolled = rw.shift() const rolled = rw.shift()
if (rolled) {
// Hide messages we're "rolling" // Hide messages we're "rolling"
if (rolled) rolled.suppress = true rolled.suppress = true
promptSize = countPromptTokens(top.concat(rw), model, chat) + countPadding // Subtract only the rolled message tokens instead of recalculating all
const rolledTokens = countMessageTokens(rolled, model, chat)
rwTokens -= rolledTokens
promptSize = topTokens + rwTokens + countPadding
}
} }
// Run a new request, now with the rolled messages hidden // Run a new request, now with the rolled messages hidden
return await _this.sendRequest(get(currentChatMessages), { return await _this.sendRequest(get(currentChatMessages), {
@@ -386,8 +395,11 @@ export class ChatRequest {
// the last prompt is a user prompt as that seems to work better for summaries // the last prompt is a user prompt as that seems to work better for summaries
while (rw.length > 2 && ((topSize + reductionPoolSize + promptSummarySize + maxSummaryTokens) >= maxTokens || while (rw.length > 2 && ((topSize + reductionPoolSize + promptSummarySize + maxSummaryTokens) >= maxTokens ||
(reductionPoolSize >= 100 && rw[rw.length - 1]?.role !== 'user'))) { (reductionPoolSize >= 100 && rw[rw.length - 1]?.role !== 'user'))) {
bottom.unshift(rw.pop() as Message) const removed = rw.pop() as Message
reductionPoolSize = countPromptTokens(rw, model, chat) bottom.unshift(removed)
// Optimize: subtract removed message tokens instead of recalculating all
const removedTokens = countMessageTokens(removed, model, chat)
reductionPoolSize -= removedTokens
maxSummaryTokens = getSS() maxSummaryTokens = getSS()
promptSummary = prepareSummaryPrompt(chatId, maxSummaryTokens) promptSummary = prepareSummaryPrompt(chatId, maxSummaryTokens)
summaryRequest.content = promptSummary summaryRequest.content = promptSummary

View File

@@ -18,7 +18,7 @@
export const codeBlockStyle: 'indented' | undefined = undefined export const codeBlockStyle: 'indented' | undefined = undefined
export let text: string export let text: string
let renderedMath: string | undefined; let renderedMath: string | undefined
// For copying code - reference: https://vyacheslavbasharov.com/blog/adding-click-to-copy-code-markdown-blog // For copying code - reference: https://vyacheslavbasharov.com/blog/adding-click-to-copy-code-markdown-blog
const copyFunction = (event) => { const copyFunction = (event) => {

View File

@@ -4,21 +4,21 @@
import renderMathInElement from 'katex/contrib/auto-render' import renderMathInElement from 'katex/contrib/auto-render'
let renderedMath: string | undefined let renderedMath: string | undefined
if ( raw.startsWith('`\\(') || raw.startsWith('`\\[') || raw.startsWith('`$') || raw.startsWith('`$$') ) { if (raw.startsWith('`\\(') || raw.startsWith('`\\[') || raw.startsWith('`$') || raw.startsWith('`$$')) {
let dummy = document.createElement("div") const dummy = document.createElement('div')
dummy.textContent = raw.replace(/`/g, '') dummy.textContent = raw.replace(/`/g, '')
renderMathInElement(dummy, { renderMathInElement(dummy, {
delimiters: [ delimiters: [
{left: '\\(', right: '\\)', display: false}, { left: '\\(', right: '\\)', display: false },
{left: '\\[', right: '\\]', display: true}, { left: '\\[', right: '\\]', display: true },
{left: '$', right: '$', display: false}, { left: '$', right: '$', display: false },
{left: '$$', right: '$$', display: true} { left: '$$', right: '$$', display: true }
], ],
throwOnError : false, throwOnError: false,
output: "html" output: 'html'
}) })
renderedMath = dummy.innerHTML; renderedMath = dummy.innerHTML
dummy.remove(); dummy.remove()
} }
</script> </script>

View File

@@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
import Code from './Code.svelte' import Code from './Code.svelte'
import Codespan from './Codespan.svelte' import Codespan from './Codespan.svelte'
import { afterUpdate, createEventDispatcher, onMount } from 'svelte' import { afterUpdate, createEventDispatcher, onMount, onDestroy } from 'svelte'
import { deleteMessage, deleteSummaryMessage, truncateFromMessage, submitExitingPromptsNow, continueMessage, updateMessages } from './Storage.svelte' import { deleteMessage, deleteSummaryMessage, truncateFromMessage, submitExitingPromptsNow, continueMessage, updateMessages } from './Storage.svelte'
import { getPrice } from './Stats.svelte' import { getPrice } from './Stats.svelte'
import SvelteMarkdown from 'svelte-markdown' import SvelteMarkdown from 'svelte-markdown'
@@ -13,7 +13,7 @@
import PromptConfirm from './PromptConfirm.svelte' import PromptConfirm from './PromptConfirm.svelte'
import { getImage } from './ImageStore.svelte' import { getImage } from './ImageStore.svelte'
import { getModelDetail } from './Models.svelte' import { getModelDetail } from './Models.svelte'
import renderMathInElement from "https://cdn.jsdelivr.net/npm/katex@0.16.22/dist/contrib/auto-render.mjs"; import renderMathInElement from 'https://cdn.jsdelivr.net/npm/katex@0.16.22/dist/contrib/auto-render.mjs'
export let message:Message export let message:Message
export let chatId:number export let chatId:number
@@ -177,22 +177,38 @@
} }
const takeReason = (msg) => { const takeReason = (msg) => {
if(isAssistant) { if (isAssistant) {
const regex = /<think>([\s\S]*?)<\/think>/; const regex = /<think>([\s\S]*?)<\/think>/
const match = msg.match(regex); const match = msg.match(regex)
if (match) { if (match) {
message.reason = match[1]; message.reason = match[1]
msg = msg.replace(regex, ''); msg = msg.replace(regex, '')
} }
} else { } else {
message.reason = ""; message.reason = ''
}
return msg
} }
return msg;
};
let waitingForTruncateConfirm:any = 0 let waitingForTruncateConfirm:any = 0
// Clean up timers to prevent memory leaks
onDestroy(() => {
if (dbnc) {
clearTimeout(dbnc)
dbnc = null
}
if (waitingForDeleteConfirm) {
clearTimeout(waitingForDeleteConfirm)
waitingForDeleteConfirm = null
}
if (waitingForTruncateConfirm) {
clearTimeout(waitingForTruncateConfirm)
waitingForTruncateConfirm = null
}
})
const checkTruncate = () => { const checkTruncate = () => {
clearTimeout(waitingForDeleteConfirm); waitingForDeleteConfirm = 0 clearTimeout(waitingForDeleteConfirm); waitingForDeleteConfirm = 0
if (!waitingForTruncateConfirm) { if (!waitingForTruncateConfirm) {
@@ -237,86 +253,86 @@
} }
const replaceLatexDelimiters = (text: string): string => { const replaceLatexDelimiters = (text: string): string => {
let result = ''; let result = ''
let i = 0; let i = 0
while (i < text.length) { while (i < text.length) {
// Check for display math: $$ ... $$ // Check for display math: $$ ... $$
if (text.startsWith('$$aaaaaaaa', i)) { if (text.startsWith('$$aaaaaaaa', i)) {
const endPos = text.indexOf('$$', i + 2); const endPos = text.indexOf('$$', i + 2)
if (endPos === -1) { if (endPos === -1) {
console.error(`LaTeX: Delimiter mismatch (missing $$) at position ${i}`); console.error(`LaTeX: Delimiter mismatch (missing $$) at position ${i}`)
result += text[i]; result += text[i]
i++; i++
} else { } else {
// Wrap in backticks for KaTeX // Wrap in backticks for KaTeX
result += `\`\\[${text.slice(i + 2, endPos)}\\]\``; result += `\`\\[${text.slice(i + 2, endPos)}\\]\``
i = endPos + 2; i = endPos + 2
} }
} }
// Check for inline math: $ ... $ // Check for inline math: $ ... $
else if (text.startsWith('$aaaaaaaaa', i)) { else if (text.startsWith('$aaaaaaaaa', i)) {
const endPos = text.indexOf('$', i + 1); const endPos = text.indexOf('$', i + 1)
if (endPos === -1) { if (endPos === -1) {
console.error(`LaTeX: Delimiter mismatch (missing $) at position ${i}`); console.error(`LaTeX: Delimiter mismatch (missing $) at position ${i}`)
result += text[i]; result += text[i]
i++; i++
} else { } else {
result += `\`$${text.slice(i + 1, endPos)}$\``; result += `\`$${text.slice(i + 1, endPos)}$\``
i = endPos + 1; i = endPos + 1
} }
} }
// Check for inline math: \(...\) // Check for inline math: \(...\)
else if (text.startsWith('\\(', i)) { else if (text.startsWith('\\(', i)) {
const endPos = text.indexOf('\\)', i + 2); const endPos = text.indexOf('\\)', i + 2)
if (endPos === -1) { if (endPos === -1) {
console.error(`LaTeX: Delimiter mismatch (missing \\)) at position ${i}`); console.error(`LaTeX: Delimiter mismatch (missing \\)) at position ${i}`)
result += text[i]; result += text[i]
i++; i++
} else { } else {
result += '`\\(' + text.slice(i + 2, endPos) + '\\)`'; result += '`\\(' + text.slice(i + 2, endPos) + '\\)`'
i = endPos + 2; i = endPos + 2
} }
} }
// Check for display math: \[...\] // Check for display math: \[...\]
else if (text.startsWith('\\[', i)) { else if (text.startsWith('\\[', i)) {
const endPos = text.indexOf('\\]', i + 2); const endPos = text.indexOf('\\]', i + 2)
if (endPos === -1) { if (endPos === -1) {
console.error(`LaTeX: Delimiter mismatch (missing \\]) at position ${i}`); console.error(`LaTeX: Delimiter mismatch (missing \\]) at position ${i}`)
result += text[i]; result += text[i]
i++; i++
} else { } else {
result += `\`\\[${text.slice(i + 2, endPos)}\\]\``; result += `\`\\[${text.slice(i + 2, endPos)}\\]\``
i = endPos + 2; i = endPos + 2
} }
} }
// Otherwise, just copy the current character (also handling backslash escapes) // Otherwise, just copy the current character (also handling backslash escapes)
else { else {
if (text.startsWith('\\(', i)) { if (text.startsWith('\\(', i)) {
result += '\\('; result += '\\('
i += 2; i += 2
} else if (text.startsWith('\\)', i)) { } else if (text.startsWith('\\)', i)) {
result += '\\)'; result += '\\)'
i += 2; i += 2
} else if (text.startsWith('\\[', i)) { } else if (text.startsWith('\\[', i)) {
result += '\\['; result += '\\['
i += 2; i += 2
} else if (text.startsWith('\\]', i)) { } else if (text.startsWith('\\]', i)) {
result += '\\]'; result += '\\]'
i += 2; i += 2
} else { } else {
result += text[i]; result += text[i]
i++; i++
} }
} }
} }
return result; return result
}; }
const renderMathMsg = () => { const renderMathMsg = () => {
displayMessage = replaceLatexDelimiters(message.content); displayMessage = replaceLatexDelimiters(message.content)
}; }
</script> </script>
@@ -349,7 +365,7 @@ const replaceLatexDelimiters = (text: string): string => {
<div <div
class="message-display" class="message-display"
on:touchend={editOnDoubleTap} on:touchend={editOnDoubleTap}
on:dblclick|preventDefault={() => {if(isUser){edit()}}} on:dblclick|preventDefault={() => { if (isUser) { edit() } }}
> >
{#if message.summary && !message.summary.length} {#if message.summary && !message.summary.length}
<p><b>Summarizing...</b></p> <p><b>Summarizing...</b></p>

View File

@@ -1,16 +1,14 @@
<script lang="ts"> <script lang="ts">
import { apiKeyStorage, globalStorage, lastChatId, getChat, started, setGlobalSettingValueByKey, checkStateChange } from './Storage.svelte' import { apiKeyStorage, lastChatId, getChat, started, checkStateChange } from './Storage.svelte'
import Footer from './Footer.svelte' import Footer from './Footer.svelte'
import { replace } from 'svelte-spa-router' import { replace } from 'svelte-spa-router'
import { afterUpdate, onMount } from 'svelte' import { afterUpdate, onMount } from 'svelte'
import { getPetalsBase, getPetalsWebsocket, getApiBase, setApiBase } from './ApiUtil.svelte' import { getApiBase, setApiBase } from './ApiUtil.svelte'
import { set as setOpenAI } from './providers/openai/util.svelte' import { set as setOpenAI } from './providers/openai/util.svelte'
import { hasActiveModels } from './Models.svelte' import { hasActiveModels } from './Models.svelte'
$: apiKey = $apiKeyStorage $: apiKey = $apiKeyStorage
let showPetalsSettings = $globalStorage.enablePetals
let pedalsEndpoint = $globalStorage.pedalsEndpoint
let hasModels = hasActiveModels() let hasModels = hasActiveModels()
onMount(() => { onMount(() => {
@@ -28,17 +26,9 @@ onMount(() => {
afterUpdate(() => { afterUpdate(() => {
hasModels = hasActiveModels() hasModels = hasActiveModels()
pedalsEndpoint = $globalStorage.pedalsEndpoint
$checkStateChange++ $checkStateChange++
}) })
const setPetalsEnabled = (event: Event) => {
const el = (event.target as HTMLInputElement)
setGlobalSettingValueByKey('enablePetals', !!el.checked)
showPetalsSettings = $globalStorage.enablePetals
hasModels = hasActiveModels()
}
</script> </script>
<section class="section"> <section class="section">
@@ -53,9 +43,6 @@ const setPetalsEnabled = (event: Event) => {
more than 10 million tokens per month. All messages are stored in your browser's local storage, so everything is more than 10 million tokens per month. All messages are stored in your browser's local storage, so everything is
<strong>private</strong>. You can also close the browser tab and come back later to continue the conversation. <strong>private</strong>. You can also close the browser tab and come back later to continue the conversation.
</p> </p>
<p>
As an alternative to OpenAI, you can also use Petals swarm as a free API option for open chat models like Llama 2.
</p>
<br> <br>
<style> <style>
.katex-version {display: none;} .katex-version {display: none;}
@@ -104,7 +91,6 @@ const setPetalsEnabled = (event: Event) => {
{#if !apiKey} {#if !apiKey}
<p class:is-danger={!hasModels} class:is-warning={!apiKey}> <p class:is-danger={!hasModels} class:is-warning={!apiKey}>
Please enter your <a target="_blank" href="https://platform.openai.com/account/api-keys">OpenAI API key</a> above to use Open AI's ChatGPT API. Please enter your <a target="_blank" href="https://platform.openai.com/account/api-keys">OpenAI API key</a> above to use Open AI's ChatGPT API.
At least one API must be enabled to use ChatGPT-web.
</p> </p>
{/if} {/if}
</div> </div>
@@ -117,10 +103,10 @@ const setPetalsEnabled = (event: Event) => {
class="field has-addons has-addons-right" class="field has-addons has-addons-right"
on:submit|preventDefault={(event) => { on:submit|preventDefault={(event) => {
if (event.target && event.target[0].value) { if (event.target && event.target[0].value) {
setApiBase(event.target[0].value); setApiBase(event.target[0].value)
} else { } else {
setApiBase("https://api.openai.com"); setApiBase('https://api.openai.com')
event.target[0].value = "https://api.openai.com"; event.target[0].value = 'https://api.openai.com'
} }
}} }}
> >
@@ -139,70 +125,6 @@ const setPetalsEnabled = (event: Event) => {
</form> </form>
</div> </div>
</article> </article>
<article class="message" class:is-danger={!hasModels} class:is-warning={!showPetalsSettings} class:is-info={showPetalsSettings}>
<div class="message-body">
<label class="label" for="enablePetals">
<input
type="checkbox"
class="checkbox"
id="enablePetals"
checked={!!$globalStorage.enablePetals}
on:click={setPetalsEnabled}
>
Use Petals API and Models (Llama 2)
</label>
{#if showPetalsSettings}
<p>Set Petals API Endpoint:</p>
<form
class="field has-addons has-addons-right"
on:submit|preventDefault={(event) => {
if (event.target && event.target[0].value) {
const v = event.target[0].value.trim()
const v2 = v.replace(/^https:/i, 'wss:').replace(/(^wss:\/\/[^/]+)\/*$/i, '$1' + getPetalsWebsocket())
setGlobalSettingValueByKey('pedalsEndpoint', v2)
event.target[0].value = v2
} else {
setGlobalSettingValueByKey('pedalsEndpoint', '')
}
}}
>
<p class="control is-expanded">
<input
aria-label="PetalsAPI Endpoint"
type="text"
class="input"
placeholder={getPetalsBase() + getPetalsWebsocket()}
value={$globalStorage.pedalsEndpoint || ''}
/>
</p>
<p class="control">
<button class="button is-info" type="submit">Save</button>
</p>
</form>
{#if !pedalsEndpoint}
<p class="help is-warning">
Please only use the default public API for testing. It's best to <a target="_blank" href="https://github.com/petals-infra/chat.petals.dev">configure a private endpoint</a> and enter it above for connection to the Petals swarm.
</p>
{/if}
<p class="my-4">
<a target="_blank" href="https://petals.dev/">Petals</a> lets you run large language models at home by connecting to a public swarm, BitTorrent-style, without hefty GPU requirements.
</p>
<p class="mb-4">
You are encouraged to <a target="_blank" href="https://github.com/bigscience-workshop/petals#connect-your-gpu-and-increase-petals-capacity">set up a Petals server to share your GPU resources</a> with the public swarm. Minimum requirements to contribute Llama 2 completions are a GTX&nbsp;1080&nbsp;8GB, but the larger/faster the better.
</p>
<p class="mb-4">
If you're receiving errors while using Petals, <a target="_blank" href="https://health.petals.dev/">check swarm health</a> and consider <a target="_blank" href="https://github.com/bigscience-workshop/petals#connect-your-gpu-and-increase-petals-capacity">adding your GPU to the swarm</a> to help.
</p>
<p class="help is-warning">
Because Petals uses a public swarm, <b>do not send sensitive information</b> when using Petals.
</p>
{/if}
</div>
</article>
{#if apiKey} {#if apiKey}
<article class="message is-info"> <article class="message is-info">
<div class="message-body"> <div class="message-body">

View File

@@ -10,10 +10,15 @@
$: chatSettings = chat.settings $: chatSettings = chat.settings
// Pre-compute filtered messages to avoid complex filtering in template
$: filteredMessages = messages.filter((message, i) => {
const isHiddenSummarized = (message.summarized) && $globalStorage.hideSummarized
const isHiddenSystemPrompt = i === 0 && message.role === 'system' && !chatSettings.useSystemPrompt
return !isHiddenSummarized && !isHiddenSystemPrompt
})
</script> </script>
{#each messages as message, i} {#each filteredMessages as message}
{#if !((message.summarized) && $globalStorage.hideSummarized) && !(i === 0 && message.role === 'system' && !chatSettings.useSystemPrompt)}
{#key message.uuid}<EditMessage bind:message={message} chatId={chatId} chat={chat} />{/key} {#key message.uuid}<EditMessage bind:message={message} chatId={chatId} chat={chat} />{/key}
{/if}
{/each} {/each}

View File

@@ -1,5 +1,5 @@
<script context="module" lang="ts"> <script context="module" lang="ts">
import { apiKeyStorage, globalStorage } from './Storage.svelte' import { apiKeyStorage } from './Storage.svelte'
import { get } from 'svelte/store' import { get } from 'svelte/store'
import type { ModelDetail, Model, SelectOption, Chat } from './Types.svelte' import type { ModelDetail, Model, SelectOption, Chat } from './Types.svelte'
import { mergeProfileFields } from './Profiles.svelte' import { mergeProfileFields } from './Profiles.svelte'
@@ -13,7 +13,7 @@ const unknownDetail = {
} as ModelDetail } as ModelDetail
export const supportedChatModels : Record<string, ModelDetail> = { export const supportedChatModels : Record<string, ModelDetail> = {
...openAiModels, ...openAiModels
// ...petalsModels // ...petalsModels
} }
@@ -144,8 +144,7 @@ export const countTokens = (model: Model, value: string): number => {
} }
export const hasActiveModels = (): boolean => { export const hasActiveModels = (): boolean => {
const globalSettings = get(globalStorage) || {} return !!get(apiKeyStorage)
return !!get(apiKeyStorage) || !!globalSettings.enablePetals
} }
export async function getChatModelOptions (): Promise<SelectOption[]> { export async function getChatModelOptions (): Promise<SelectOption[]> {
@@ -155,7 +154,7 @@ export async function getChatModelOptions (): Promise<SelectOption[]> {
const model = models[i] const model = models[i]
const modelDetail = getModelDetail(model) const modelDetail = getModelDetail(model)
await modelDetail.check(modelDetail) await modelDetail.check(modelDetail)
if(modelDetail.enabled){ if (modelDetail.enabled) {
result.push({ result.push({
value: model, value: model,
text: modelDetail.label || model, text: modelDetail.label || model,

View File

@@ -1,7 +1,6 @@
<script context="module" lang="ts"> <script context="module" lang="ts">
import { applyProfile } from './Profiles.svelte' import { applyProfile } from './Profiles.svelte'
import { get } from 'svelte/store' import { getChatSettings, getGlobalSettings, setGlobalSettingValueByKey } from './Storage.svelte'
import { apiKeyStorage, getChatSettings, getGlobalSettings, setGlobalSettingValueByKey } from './Storage.svelte'
import { faArrowDown91, faArrowDownAZ, faCheck, faThumbTack } from '@fortawesome/free-solid-svg-icons/index' import { faArrowDown91, faArrowDownAZ, faCheck, faThumbTack } from '@fortawesome/free-solid-svg-icons/index'
// Setting definitions // Setting definitions
@@ -21,10 +20,8 @@ import {
import { getModelDetail, getTokens } from './Models.svelte' import { getModelDetail, getTokens } from './Models.svelte'
const defaultModel:Model = 'gpt-4' const defaultModel:Model = 'gpt-4'
const defaultModelPetals:Model = 'stabilityai/StableBeluga2'
export const getDefaultModel = (): Model => { export const getDefaultModel = (): Model => {
if (!get(apiKeyStorage)) return defaultModelPetals
return defaultModel return defaultModel
} }
@@ -66,7 +63,7 @@ export const getExcludeFromProfile = () => {
return excludeFromProfile return excludeFromProfile
} }
const hideModelSetting = (chatId, setting) => { const hideModelSetting = (chatId: number, setting: ChatSetting) => {
return getModelDetail(getChatSettings(chatId).model).hideSetting(chatId, setting) return getModelDetail(getChatSettings(chatId).model).hideSetting(chatId, setting)
} }
@@ -134,9 +131,7 @@ export const globalDefaults: GlobalSettings = {
defaultProfile: 'default', defaultProfile: 'default',
hideSummarized: false, hideSummarized: false,
chatSort: 'created', chatSort: 'created',
openAICompletionEndpoint: '', openAICompletionEndpoint: ''
enablePetals: false,
pedalsEndpoint: ''
} }
const excludeFromProfile = { const excludeFromProfile = {
@@ -711,16 +706,6 @@ const globalSettingsList:GlobalSetting[] = [
key: 'openAICompletionEndpoint', key: 'openAICompletionEndpoint',
name: 'OpenAI Completions Endpoint', name: 'OpenAI Completions Endpoint',
type: 'text' type: 'text'
},
{
key: 'enablePetals',
name: 'Enable Petals APIs',
type: 'boolean'
},
{
key: 'pedalsEndpoint',
name: 'Petals API Endpoint',
type: 'text'
} }
] ]

View File

@@ -10,17 +10,33 @@
import { startNewChatWithWarning } from './Util.svelte' import { startNewChatWithWarning } from './Util.svelte'
import { chatSortOptions } from './Settings.svelte' import { chatSortOptions } from './Settings.svelte'
import { hasActiveModels } from './Models.svelte' import { hasActiveModels } from './Models.svelte'
import { onMount } from 'svelte'; import { onMount } from 'svelte'
// Cache sorted chats to avoid expensive sorting on every update
let sortedChats: Chat[] = []
let lastSortOption: any = null
let lastChatsLength = 0
$: sortedChats = $chatsStorage.sort(getChatSortOption().sortFn)
$: activeChatId = $params && $params.chatId ? parseInt($params.chatId) : undefined $: activeChatId = $params && $params.chatId ? parseInt($params.chatId) : undefined
let sortOption = getChatSortOption() let sortOption = getChatSortOption()
let hasModels = hasActiveModels() let hasModels = hasActiveModels()
// Only re-sort when sort option changes or chats are added/removed
$: {
const currentSortOption = getChatSortOption()
const chatsChanged = $chatsStorage.length !== lastChatsLength
const sortChanged = !lastSortOption || lastSortOption.value !== currentSortOption.value
if (sortChanged || chatsChanged) {
sortedChats = [...$chatsStorage].sort(currentSortOption.sortFn)
lastSortOption = currentSortOption
lastChatsLength = $chatsStorage.length
}
}
const onStateChange = (...args:any) => { const onStateChange = (...args:any) => {
sortOption = getChatSortOption() sortOption = getChatSortOption()
sortedChats = $chatsStorage.sort(sortOption.sortFn)
hasModels = hasActiveModels() hasModels = hasActiveModels()
} }
@@ -28,127 +44,124 @@
let showSortMenu = false let showSortMenu = false
async function uploadLocalStorage(uid = 19492){ async function uploadLocalStorage (uid = 19492) {
try { try {
let storageObject = {}; const storageObject = {}
for (let i = 0; i < localStorage.length; i++) { for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i); const key = localStorage.key(i)
if (key) { if (key) {
storageObject[key] = localStorage.getItem(key); storageObject[key] = localStorage.getItem(key)
} }
} }
const response = await fetch(`https://api.morgan.kr/localstore/${uid}`, { const response = await fetch(`https://api.morgan.kr/localstore/${uid}`, {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json'
}, },
body: JSON.stringify({data: storageObject}), body: JSON.stringify({ data: storageObject })
}); })
if (!response.ok) { if (!response.ok) {
throw new Error('Network response was not ok.'); throw new Error('Network response was not ok.')
} }
const data = await response.json(); const data = await response.json()
console.log(data) console.log(data)
console.log("Uploaded savedata."); console.log('Uploaded savedata.')
alert("Uploaded savedata."); alert('Uploaded savedata.')
return data.id; return data.id
} catch (error) { } catch (error) {
console.error('Error uploading localStorage:', error); console.error('Error uploading localStorage:', error)
} }
} }
async function fetchLocalStorage(){ async function fetchLocalStorage () {
if (!confirm("This will override all local data. Proceed?")) { if (!confirm('This will override all local data. Proceed?')) {
return; return
} }
try { try {
// dumpLocalStorage(); // dumpLocalStorage();
await uploadLocalStorage(99999); await uploadLocalStorage(99999)
const response = await fetch('https://api.morgan.kr/localstore/19492', { const response = await fetch('https://api.morgan.kr/localstore/19492', {
method: 'GET', method: 'GET'
}); })
if (!response.ok) { if (!response.ok) {
throw new Error('Network response was not ok.'); throw new Error('Network response was not ok.')
} }
const newData = await response.json(); const newData = await response.json()
localStorage.clear(); localStorage.clear()
Object.entries(newData).forEach(([key, value]) => { Object.entries(newData).forEach(([key, value]) => {
localStorage.setItem(key, value); localStorage.setItem(key, value)
}); })
console.log('Fetched savedata');
alert('Fetched savedata');
console.log('Fetched savedata')
alert('Fetched savedata')
} catch (error) { } catch (error) {
console.error('Error fetching localStorage:', error); console.error('Error fetching localStorage:', error)
alert(error); alert(error)
} }
} }
async function syncLocalStorage(){ async function syncLocalStorage () {
console.log("Syncing...") console.log('Syncing...')
uploadLocalStorage(); uploadLocalStorage()
localStorage.setItem('lastModified', new Date().toISOString()); localStorage.setItem('lastModified', new Date().toISOString())
} }
function dumpLocalStorage(){ function dumpLocalStorage () {
try { try {
let storageObject = {}; const storageObject = {}
for (let i = 0; i < localStorage.length; i++) { for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i); const key = localStorage.key(i)
if (key) { if (key) {
storageObject[key] = localStorage.getItem(key); storageObject[key] = localStorage.getItem(key)
} }
} }
const dataStr = JSON.stringify(storageObject, null, 2); const dataStr = JSON.stringify(storageObject, null, 2)
const blob = new Blob([dataStr], { type: "application/json" }); const blob = new Blob([dataStr], { type: 'application/json' })
const url = URL.createObjectURL(blob); const url = URL.createObjectURL(blob)
const link = document.createElement("a"); const link = document.createElement('a')
link.href = url; link.href = url
const now = new Date(); const now = new Date()
const dateTimeStr = now.toISOString().replace(/:\d+\.\d+Z$/, '').replace(/-|:/g, '_'); const dateTimeStr = now.toISOString().replace(/:\d+\.\d+Z$/, '').replace(/-|:/g, '_')
link.download = `ChatGPT-web-${dateTimeStr}.json`; link.download = `ChatGPT-web-${dateTimeStr}.json`
document.body.appendChild(link); document.body.appendChild(link)
link.click(); link.click()
document.body.removeChild(link); document.body.removeChild(link)
} catch (error) { } catch (error) {
console.error('Error dumping localStorage:', error); console.error('Error dumping localStorage:', error)
} }
} }
function loadLocalStorage() { function loadLocalStorage () {
var fileInput = document.createElement('input'); const fileInput = document.createElement('input')
fileInput.type = "file"; fileInput.type = 'file'
fileInput.addEventListener('change', function(e) { fileInput.addEventListener('change', function (e) {
var file = e.target.files[0]; const file = e.target.files[0]
if (file) { if (file) {
var reader = new FileReader(); const reader = new FileReader()
reader.onload = function(e) { reader.onload = function (e) {
var data = JSON.parse(e.target.result); const data = JSON.parse(e.target.result)
Object.keys(data).forEach(function(key) { Object.keys(data).forEach(function (key) {
localStorage.setItem(key, data[key]); localStorage.setItem(key, data[key])
}); })
window.location.reload(); window.location.reload()
};
reader.readAsText(file);
} }
}); reader.readAsText(file)
document.body.appendChild(fileInput); }
fileInput.click(); })
fileInput.remove(); document.body.appendChild(fileInput)
fileInput.click()
fileInput.remove()
} }
onMount(() => { onMount(() => {
// console.log('Downloading from server.'); // console.log('Downloading from server.');
// fetchLocalStorage(); // fetchLocalStorage();
}); })
// setInterval(syncLocalStorage, 10000); // setInterval(syncLocalStorage, 10000);
</script> </script>
@@ -198,12 +211,12 @@
</div> </div>
</div> </div>
<div class="is-left is-up ml-2"> <div class="is-left is-up ml-2">
<button class="button" aria-haspopup="true" on:click|preventDefault|stopPropagation={() => { loadLocalStorage(); }}> <button class="button" aria-haspopup="true" on:click|preventDefault|stopPropagation={() => { loadLocalStorage() }}>
<span class="icon"><Fa icon={faUpload}/></span> <span class="icon"><Fa icon={faUpload}/></span>
</button> </button>
</div> </div>
<div class="is-left is-up ml-2"> <div class="is-left is-up ml-2">
<button class="button" aria-haspopup="true" on:click|preventDefault|stopPropagation={() => { dumpLocalStorage(); }}> <button class="button" aria-haspopup="true" on:click|preventDefault|stopPropagation={() => { dumpLocalStorage() }}>
<span class="icon"><Fa icon={faDownload}/></span> <span class="icon"><Fa icon={faDownload}/></span>
</button> </button>
</div> </div>

View File

@@ -246,29 +246,43 @@
}, 10) }, 10)
} }
const signalChangeTimers: any = {} const signalChangeTimers = new Map<number, any>()
const setChatLastUse = (chatId: number, time: number) => { const setChatLastUse = (chatId: number, time: number) => {
clearTimeout(signalChangeTimers[chatId]) const existingTimer = signalChangeTimers.get(chatId)
signalChangeTimers[chatId] = setTimeout(() => { if (existingTimer) {
clearTimeout(existingTimer)
}
const timer = setTimeout(() => {
getChat(chatId).lastUse = time getChat(chatId).lastUse = time
saveChatStore() saveChatStore()
signalChangeTimers.delete(chatId)
}, 500) }, 500)
signalChangeTimers.set(chatId, timer)
} }
const setMessagesTimers: any = {} const setMessagesTimers = new Map<number, any>()
export const setMessages = (chatId: number, messages: Message[]) => { export const setMessages = (chatId: number, messages: Message[]) => {
if (get(currentChatId) === chatId) { if (get(currentChatId) === chatId) {
// update current message cache right away // update current message cache right away
currentChatMessages.set(messages) currentChatMessages.set(messages)
clearTimeout(setMessagesTimers[chatId]) const existingTimer = setMessagesTimers.get(chatId)
if (existingTimer) {
clearTimeout(existingTimer)
}
// delay expensive all chats update for a bit // delay expensive all chats update for a bit
setMessagesTimers[chatId] = setTimeout(() => { const timer = setTimeout(() => {
getChat(chatId).messages = messages getChat(chatId).messages = messages
saveChatStore() saveChatStore()
setChatLastUse(chatId, Date.now()) setChatLastUse(chatId, Date.now())
setMessagesTimers.delete(chatId)
}, 200) }, 200)
setMessagesTimers.set(chatId, timer)
} else { } else {
clearTimeout(setMessagesTimers[chatId]) const existingTimer = setMessagesTimers.get(chatId)
if (existingTimer) {
clearTimeout(existingTimer)
setMessagesTimers.delete(chatId)
}
getChat(chatId).messages = messages getChat(chatId).messages = messages
saveChatStore() saveChatStore()
setChatLastUse(chatId, Date.now()) setChatLastUse(chatId, Date.now())
@@ -279,6 +293,24 @@
setMessages(chatId, getMessages(chatId)) setMessages(chatId, getMessages(chatId))
} }
// Cleanup function to clear all timers and prevent memory leaks
export const clearAllTimers = () => {
if (setChatTimer) {
clearTimeout(setChatTimer)
setChatTimer = null
}
signalChangeTimers.forEach((timer) => {
clearTimeout(timer)
})
signalChangeTimers.clear()
setMessagesTimers.forEach((timer) => {
clearTimeout(timer)
})
setMessagesTimers.clear()
}
export const addError = (chatId: number, error: string) => { export const addError = (chatId: number, error: string) => {
addMessage(chatId, { content: error } as Message) addMessage(chatId, { content: error } as Message)
} }

View File

@@ -160,8 +160,6 @@ export type GlobalSettings = {
hideSummarized: boolean; hideSummarized: boolean;
chatSort: ChatSortOptions; chatSort: ChatSortOptions;
openAICompletionEndpoint: string; openAICompletionEndpoint: string;
enablePetals: boolean;
pedalsEndpoint: string;
}; };
type SettingNumber = { type SettingNumber = {

View File

@@ -6,10 +6,25 @@
import { replace } from 'svelte-spa-router' import { replace } from 'svelte-spa-router'
// import PromptConfirm from './PromptConfirm.svelte' // import PromptConfirm from './PromptConfirm.svelte'
import type { ChatSettings } from './Types.svelte' import type { ChatSettings } from './Types.svelte'
// Cache for auto-size elements to avoid expensive DOM queries
let cachedAutoSizeElements: HTMLTextAreaElement[] = []
let lastElementCount = 0
export const sizeTextElements = (force?: boolean) => { export const sizeTextElements = (force?: boolean) => {
const els = document.querySelectorAll('textarea.auto-size') // Only re-query if force is true or element count changed
for (let i:number = 0, l = els.length; i < l; i++) { const currentElements = document.querySelectorAll('textarea.auto-size')
autoGrowInput(els[i] as HTMLTextAreaElement, force) if (force || currentElements.length !== lastElementCount) {
cachedAutoSizeElements = Array.from(currentElements) as HTMLTextAreaElement[]
lastElementCount = currentElements.length
}
// Use cached elements for better performance
for (let i = 0, l = cachedAutoSizeElements.length; i < l; i++) {
const el = cachedAutoSizeElements[i]
// Check if element is still in DOM
if (document.contains(el)) {
autoGrowInput(el, force)
}
} }
} }

View File

@@ -1,15 +1,30 @@
<script context="module" lang="ts"> <script context="module" lang="ts">
import { getApiBase, getEndpointCompletions, getEndpointGenerations } from "../../ApiUtil.svelte"; import { getApiBase, getEndpointCompletions, getEndpointGenerations } from '../../ApiUtil.svelte'
import { countTokens } from "../../Models.svelte"; import { countTokens } from '../../Models.svelte'
import { countMessageTokens } from "../../Stats.svelte"; import { countMessageTokens } from '../../Stats.svelte'
import { globalStorage } from "../../Storage.svelte"; import { globalStorage } from '../../Storage.svelte'
import type { Chat, Message, Model, ModelDetail } from "../../Types.svelte"; import type { Chat, Message, Model, ModelDetail } from '../../Types.svelte'
import { chatRequest, imageRequest } from "./request.svelte"; import { chatRequest, imageRequest } from './request.svelte'
import { checkModel } from "./util.svelte"; import { checkModel } from './util.svelte'
import { encode } from "gpt-tokenizer"; // Lazy-load tokenizer to improve initial load time
import { get } from "svelte/store"; let encode: any = null
import chatModelsJson from './models.json';
// Simple token approximation for faster initial loads
const approximateTokens = (text: string): number[] => {
// Rough approximation: 1 token ≈ 4 characters for most text
return new Array(Math.ceil(text.length / 4)).fill(0)
}
const getTokenizer = async () => {
if (!encode) {
const tokenizer = await import('gpt-tokenizer')
encode = tokenizer.encode
}
return encode
}
import { get } from 'svelte/store'
import chatModelsJson from './models.json'
const hiddenSettings = { const hiddenSettings = {
startSequence: true, startSequence: true,
@@ -23,38 +38,45 @@
systemMessageStart: true, systemMessageStart: true,
systemMessageEnd: true, systemMessageEnd: true,
repetitionPenalty: true, repetitionPenalty: true,
holdSocket: true, holdSocket: true
// leadPrompt: true // leadPrompt: true
} as any; } as any
const chatModelBase = { const chatModelBase = {
type: "chat", type: 'chat',
help: 'Below are the settings that OpenAI allows to be changed for the API calls. See the <a target="_blank" href="https://platform.openai.com/docs/api-reference/chat/create">OpenAI API docs</a> for more details.', help: 'Below are the settings that OpenAI allows to be changed for the API calls. See the <a target="_blank" href="https://platform.openai.com/docs/api-reference/chat/create">OpenAI API docs</a> for more details.',
preFillMerge: (existingContent, newContent) => { preFillMerge: (existingContent, newContent) => {
if (existingContent && !newContent.match(/^('(t|ll|ve|m|d|re)[^a-z]|\s|[.,;:(_-{}*^%$#@!?+=~`[\]])/i)) { if (existingContent && !newContent.match(/^('(t|ll|ve|m|d|re)[^a-z]|\s|[.,;:(_-{}*^%$#@!?+=~`[\]])/i)) {
existingContent += " "; existingContent += ' '
} }
return existingContent; return existingContent
}, },
request: chatRequest, request: chatRequest,
check: checkModel, check: checkModel,
getTokens: (value) => encode(value), getTokens: (value) => {
// Use approximation for faster initial loads, actual tokenizer loads async
if (!encode) {
getTokenizer() // Start loading tokenizer for future use
return approximateTokens(value)
}
return encode(value)
},
getEndpoint: (model) => get(globalStorage).openAICompletionEndpoint || getApiBase() + getEndpointCompletions(), getEndpoint: (model) => get(globalStorage).openAICompletionEndpoint || getApiBase() + getEndpointCompletions(),
hideSetting: (chatId, setting) => !!hiddenSettings[setting.key], hideSetting: (chatId, setting) => !!hiddenSettings[setting.key],
countMessageTokens: (message: Message, model: Model, chat: Chat) => { countMessageTokens: (message: Message, model: Model, chat: Chat) => {
return countTokens(model, "## " + message.role + " ##:\r\n\r\n" + message.content + "\r\n\r\n\r\n"); return countTokens(model, '## ' + message.role + ' ##:\r\n\r\n' + message.content + '\r\n\r\n\r\n')
}, },
countPromptTokens: (prompts: Message[], model: Model, chat: Chat): number => { countPromptTokens: (prompts: Message[], model: Model, chat: Chat): number => {
return ( return (
prompts.reduce((a, m) => { prompts.reduce((a, m) => {
a += countMessageTokens(m, model, chat); a += countMessageTokens(m, model, chat)
return a; return a
}, 0) + 3 }, 0) + 3
); )
}, }
} as ModelDetail; } as ModelDetail
export const chatModels: Record<string, ModelDetail> = {}; export const chatModels: Record<string, ModelDetail> = {}
for (const [key, { prompt, completion, max, reasoning, alias }] of Object.entries(chatModelsJson)) { for (const [key, { prompt, completion, max, reasoning, alias }] of Object.entries(chatModelsJson)) {
chatModels[key] = { chatModels[key] = {
@@ -63,101 +85,101 @@
completion: completion / 1_000_000, completion: completion / 1_000_000,
max, max,
...(reasoning ? { reasoning } : {}), ...(reasoning ? { reasoning } : {}),
...(alias ? { alias } : {}), ...(alias ? { alias } : {})
}; }
} }
const imageModelBase = { const imageModelBase = {
type: "image", type: 'image',
prompt: 0.0, prompt: 0.0,
max: 1000, // 1000 char prompt, max max: 1000, // 1000 char prompt, max
request: imageRequest, request: imageRequest,
check: checkModel, check: checkModel,
getTokens: (value) => [0], getTokens: (value) => [0],
getEndpoint: (model) => getApiBase() + getEndpointGenerations(), getEndpoint: (model) => getApiBase() + getEndpointGenerations(),
hideSetting: (chatId, setting) => false, hideSetting: (chatId, setting) => false
} as ModelDetail; } as ModelDetail
export const imageModels: Record<string, ModelDetail> = { export const imageModels: Record<string, ModelDetail> = {
"dall-e-1024x1024": { 'dall-e-1024x1024': {
...imageModelBase, ...imageModelBase,
completion: 0.02, // $0.020 per image completion: 0.02, // $0.020 per image
opt: { opt: {
size: "1024x1024", size: '1024x1024'
}
}, },
}, 'dall-e-512x512': {
"dall-e-512x512": {
...imageModelBase, ...imageModelBase,
completion: 0.018, // $0.018 per image completion: 0.018, // $0.018 per image
opt: { opt: {
size: "512x512", size: '512x512'
}
}, },
}, 'dall-e-256x256': {
"dall-e-256x256": {
...imageModelBase, ...imageModelBase,
type: "image", type: 'image',
completion: 0.016, // $0.016 per image completion: 0.016, // $0.016 per image
opt: { opt: {
size: "256x256", size: '256x256'
}
}, },
}, 'dall-e-3-1024x1024': {
"dall-e-3-1024x1024": {
...imageModelBase, ...imageModelBase,
type: "image", type: 'image',
completion: 0.04, // $0.040 per image completion: 0.04, // $0.040 per image
opt: { opt: {
model: "dall-e-3", model: 'dall-e-3',
size: "1024x1024", size: '1024x1024'
}
}, },
}, 'dall-e-3-1024x1792-Portrait': {
"dall-e-3-1024x1792-Portrait": {
...imageModelBase, ...imageModelBase,
type: "image", type: 'image',
completion: 0.08, // $0.080 per image completion: 0.08, // $0.080 per image
opt: { opt: {
model: "dall-e-3", model: 'dall-e-3',
size: "1024x1792", size: '1024x1792'
}
}, },
}, 'dall-e-3-1792x1024-Landscape': {
"dall-e-3-1792x1024-Landscape": {
...imageModelBase, ...imageModelBase,
type: "image", type: 'image',
completion: 0.08, // $0.080 per image completion: 0.08, // $0.080 per image
opt: { opt: {
model: "dall-e-3", model: 'dall-e-3',
size: "1792x1024", size: '1792x1024'
}
}, },
}, 'dall-e-3-1024x1024-HD': {
"dall-e-3-1024x1024-HD": {
...imageModelBase, ...imageModelBase,
type: "image", type: 'image',
completion: 0.08, // $0.080 per image completion: 0.08, // $0.080 per image
opt: { opt: {
model: "dall-e-3", model: 'dall-e-3',
size: "1024x1024", size: '1024x1024',
quality: "hd", quality: 'hd'
}
}, },
}, 'dall-e-3-1024x1792-Portrait-HD': {
"dall-e-3-1024x1792-Portrait-HD": {
...imageModelBase, ...imageModelBase,
type: "image", type: 'image',
completion: 0.12, // $0.080 per image completion: 0.12, // $0.080 per image
opt: { opt: {
model: "dall-e-3", model: 'dall-e-3',
size: "1024x1792", size: '1024x1792',
quality: "hd", quality: 'hd'
}
}, },
}, 'dall-e-3-1792x1024-Landscape-HD': {
"dall-e-3-1792x1024-Landscape-HD": {
...imageModelBase, ...imageModelBase,
type: "image", type: 'image',
completion: 0.12, // $0.080 per image completion: 0.12, // $0.080 per image
opt: { opt: {
model: "dall-e-3", model: 'dall-e-3',
size: "1792x1024", size: '1792x1024',
quality: "hd", quality: 'hd'
}, }
}, }
}; }
</script> </script>

View File

@@ -33,8 +33,8 @@ export const chatRequest = async (
} }
if (modelDetail.stream === false) { if (modelDetail.stream === false) {
opts.streaming = false; opts.streaming = false
console.log("Disabled streaming on reasoning models."); console.log('Disabled streaming on reasoning models.')
} }
if (opts.streaming && !modelDetail.stream) { if (opts.streaming && !modelDetail.stream) {

View File

@@ -7,7 +7,7 @@ import purgecss from '@fullhuman/postcss-purgecss'
const plugins = [ const plugins = [
svelte(), svelte(),
dsv(), dsv()
// visualizer({ // visualizer({
// open: true, // open: true,
// gzipSize: true, // gzipSize: true,