Add add streaming responses based on #107
This commit is contained in:
parent
fffe34c80c
commit
15272de1d4
14
src/app.scss
14
src/app.scss
|
@ -607,6 +607,20 @@ aside.menu.main-menu .menu-expanse {
|
||||||
border-top-left-radius: 0px !important;
|
border-top-left-radius: 0px !important;
|
||||||
border-bottom-left-radius: 0px !important;
|
border-bottom-left-radius: 0px !important;
|
||||||
}
|
}
|
||||||
|
.message.streaming .tool-drawer, .message.streaming .tool-drawer-mask {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
@keyframes cursor-blink {
|
||||||
|
0% {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.message.streaming .message-display p:last-of-type::after {
|
||||||
|
position: relative;
|
||||||
|
content: '❚';
|
||||||
|
animation: cursor-blink 1s steps(2) infinite;
|
||||||
|
}
|
||||||
|
|
||||||
.modal {
|
.modal {
|
||||||
z-index:100;
|
z-index:100;
|
||||||
|
|
|
@ -8,17 +8,23 @@
|
||||||
insertMessages,
|
insertMessages,
|
||||||
getChatSettingValueNullDefault,
|
getChatSettingValueNullDefault,
|
||||||
updateChatSettings,
|
updateChatSettings,
|
||||||
updateRunningTotal,
|
|
||||||
checkStateChange,
|
checkStateChange,
|
||||||
showSetChatSettings,
|
showSetChatSettings,
|
||||||
submitExitingPromptsNow
|
submitExitingPromptsNow,
|
||||||
|
|
||||||
|
deleteMessage
|
||||||
|
|
||||||
|
|
||||||
} from './Storage.svelte'
|
} from './Storage.svelte'
|
||||||
import { getRequestSettingList, defaultModel } from './Settings.svelte'
|
import { getRequestSettingList, defaultModel } from './Settings.svelte'
|
||||||
import {
|
import {
|
||||||
type Request,
|
type Request,
|
||||||
type Response,
|
|
||||||
type Message,
|
type Message,
|
||||||
type Chat
|
type Chat,
|
||||||
|
type ChatCompletionOpts,
|
||||||
|
|
||||||
|
type Usage
|
||||||
|
|
||||||
} from './Types.svelte'
|
} from './Types.svelte'
|
||||||
import Prompts from './Prompts.svelte'
|
import Prompts from './Prompts.svelte'
|
||||||
import Messages from './Messages.svelte'
|
import Messages from './Messages.svelte'
|
||||||
|
@ -37,11 +43,13 @@
|
||||||
// import { encode } from 'gpt-tokenizer'
|
// import { encode } from 'gpt-tokenizer'
|
||||||
import { v4 as uuidv4 } from 'uuid'
|
import { v4 as uuidv4 } from 'uuid'
|
||||||
import { countPromptTokens, getModelMaxTokens, getPrice } from './Stats.svelte'
|
import { countPromptTokens, getModelMaxTokens, getPrice } from './Stats.svelte'
|
||||||
import { autoGrowInputOnEvent, sizeTextElements } from './Util.svelte'
|
import { autoGrowInputOnEvent, scrollToMessage, sizeTextElements } from './Util.svelte'
|
||||||
import ChatSettingsModal from './ChatSettingsModal.svelte'
|
import ChatSettingsModal from './ChatSettingsModal.svelte'
|
||||||
import Footer from './Footer.svelte'
|
import Footer from './Footer.svelte'
|
||||||
import { openModal } from 'svelte-modals'
|
import { openModal } from 'svelte-modals'
|
||||||
import PromptInput from './PromptInput.svelte'
|
import PromptInput from './PromptInput.svelte'
|
||||||
|
import { ChatCompletionResponse } from './ChatCompletionResponse.svelte'
|
||||||
|
import { fetchEventSource } from '@microsoft/fetch-event-source'
|
||||||
|
|
||||||
// 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'
|
||||||
|
@ -52,8 +60,6 @@
|
||||||
let updating: boolean = false
|
let updating: boolean = false
|
||||||
let updatingMessage: string = ''
|
let updatingMessage: string = ''
|
||||||
let input: HTMLTextAreaElement
|
let input: HTMLTextAreaElement
|
||||||
// let settings: HTMLDivElement
|
|
||||||
// let chatNameSettings: HTMLFormElement
|
|
||||||
let recognition: any = null
|
let recognition: any = null
|
||||||
let recording = false
|
let recording = false
|
||||||
|
|
||||||
|
@ -141,20 +147,24 @@
|
||||||
// Scroll to the bottom of the chat on update
|
// Scroll to the bottom of the chat on update
|
||||||
const focusInput = () => {
|
const focusInput = () => {
|
||||||
input.focus()
|
input.focus()
|
||||||
setTimeout(() => document.querySelector('body')?.scrollIntoView({ behavior: 'smooth', block: 'end' }), 0)
|
setTimeout(() => scrollToBottom(), 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
const scrollToBottom = (instant:boolean = false) => {
|
||||||
|
document.querySelector('body')?.scrollIntoView({ behavior: (instant ? 'instant' : 'smooth') as any, block: 'end' })
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send API request
|
// Send API request
|
||||||
const sendRequest = async (messages: Message[], summaryTarget:number|undefined = undefined, withSummary:boolean = false): Promise<Response> => {
|
const sendRequest = async (messages: Message[], opts:ChatCompletionOpts): Promise<ChatCompletionResponse> => {
|
||||||
// Show updating bar
|
// Show updating bar
|
||||||
|
opts.chat = chat
|
||||||
|
const chatResponse = new ChatCompletionResponse(opts)
|
||||||
updating = true
|
updating = true
|
||||||
|
|
||||||
const model = chat.settings.model || defaultModel
|
const model = chat.settings.model || defaultModel
|
||||||
const maxTokens = getModelMaxTokens(model) // max tokens for model
|
const maxTokens = getModelMaxTokens(model) // max tokens for model
|
||||||
|
|
||||||
let response: Response
|
const messageFilter = (m:Message) => !m.suppress && m.role !== 'error' && m.content && !m.summarized
|
||||||
|
|
||||||
const messageFilter = (m) => !m.suppress && m.role !== 'error' && m.content && !m.summarized
|
|
||||||
|
|
||||||
// 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
|
||||||
let filtered = messages.filter(messageFilter)
|
let filtered = messages.filter(messageFilter)
|
||||||
|
@ -166,8 +176,8 @@
|
||||||
|
|
||||||
// console.log('Estimated',promptTokenCount,'prompt token for this request')
|
// console.log('Estimated',promptTokenCount,'prompt token for this request')
|
||||||
|
|
||||||
if (chatSettings.useSummarization &&
|
if (chatSettings.useSummarization && !opts.didSummary &&
|
||||||
!withSummary && !summaryTarget &&
|
!opts.summaryRequest && !opts.maxTokens &&
|
||||||
promptTokenCount > chatSettings.summaryThreshold) {
|
promptTokenCount > chatSettings.summaryThreshold) {
|
||||||
// Too many tokens -- well need to sumarize some past ones else we'll run out of space
|
// Too many tokens -- well need to sumarize some past ones else we'll run out of space
|
||||||
// Get a block of past prompts we'll summarize
|
// Get a block of past prompts we'll summarize
|
||||||
|
@ -239,35 +249,47 @@
|
||||||
summarizeReq.push(summaryMessage)
|
summarizeReq.push(summaryMessage)
|
||||||
summaryPromptSize = countPromptTokens(summarizeReq, model)
|
summaryPromptSize = countPromptTokens(summarizeReq, model)
|
||||||
|
|
||||||
|
const summaryResponse:Message = {
|
||||||
|
role: 'assistant',
|
||||||
|
content: '',
|
||||||
|
uuid: uuidv4(),
|
||||||
|
streaming: opts.streaming,
|
||||||
|
summary: []
|
||||||
|
}
|
||||||
|
summaryResponse.usage = {
|
||||||
|
prompt_tokens: 0
|
||||||
|
} as Usage
|
||||||
|
summaryResponse.model = model
|
||||||
|
|
||||||
|
// Insert summary prompt
|
||||||
|
insertMessages(chatId, endPrompt, [summaryResponse])
|
||||||
|
if (opts.streaming) setTimeout(() => scrollToMessage(summaryResponse.uuid, 150, true, true), 0)
|
||||||
|
|
||||||
// Wait for the summary completion
|
// Wait for the summary completion
|
||||||
updatingMessage = 'Building Summary...'
|
updatingMessage = 'Summarizing...'
|
||||||
const summary = await sendRequest(summarizeReq, summarySize)
|
const summary = await sendRequest(summarizeReq, {
|
||||||
if (summary.error) {
|
summaryRequest: true,
|
||||||
|
streaming: opts.streaming,
|
||||||
|
maxTokens: summarySize,
|
||||||
|
fillMessage: summaryResponse,
|
||||||
|
autoAddMessages: true,
|
||||||
|
onMessageChange: (m) => {
|
||||||
|
if (opts.streaming) scrollToMessage(summaryResponse.uuid, 150, true, true)
|
||||||
|
}
|
||||||
|
} as ChatCompletionOpts)
|
||||||
|
if (!summary.hasFinished()) await summary.promiseToFinish()
|
||||||
|
if (summary.hasError()) {
|
||||||
// Failed to some API issue. let the original caller handle it.
|
// Failed to some API issue. let the original caller handle it.
|
||||||
|
deleteMessage(chatId, summaryResponse.uuid)
|
||||||
return summary
|
return summary
|
||||||
} else {
|
} else {
|
||||||
// Get response
|
|
||||||
const summaryPromptContent: string = summary.choices.reduce((a, c) => {
|
|
||||||
if (a.length > c.message.content.length) return a
|
|
||||||
a = c.message.content
|
|
||||||
return a
|
|
||||||
}, '')
|
|
||||||
|
|
||||||
// Looks like we got our summarized messages.
|
// Looks like we got our summarized messages.
|
||||||
// get ids of messages we summarized
|
// get ids of messages we summarized
|
||||||
const summarizedIds = summarize.slice(pinTop + systemPad).map(m => m.uuid)
|
const summarizedIds = summarize.slice(pinTop + systemPad).map(m => m.uuid)
|
||||||
// Mark the new summaries as such
|
// Mark the new summaries as such
|
||||||
const summaryPrompt:Message = {
|
summaryResponse.summary = summarizedIds
|
||||||
role: 'assistant',
|
|
||||||
content: summaryPromptContent,
|
const summaryIds = [summaryResponse.uuid]
|
||||||
uuid: uuidv4(),
|
|
||||||
summary: summarizedIds,
|
|
||||||
usage: summary.usage,
|
|
||||||
model
|
|
||||||
}
|
|
||||||
const summaryIds = [summaryPrompt.uuid]
|
|
||||||
// Insert messages
|
|
||||||
insertMessages(chatId, endPrompt, [summaryPrompt])
|
|
||||||
// Disable the messages we summarized so they still show in history
|
// Disable the messages we summarized so they still show in history
|
||||||
summarize.forEach((m, i) => {
|
summarize.forEach((m, i) => {
|
||||||
if (i - systemPad >= pinTop) {
|
if (i - systemPad >= pinTop) {
|
||||||
|
@ -278,7 +300,8 @@
|
||||||
// Re-run request with summarized prompts
|
// Re-run request with summarized prompts
|
||||||
// return { error: { message: "End for now" } } as Response
|
// return { error: { message: "End for now" } } as Response
|
||||||
updatingMessage = 'Continuing...'
|
updatingMessage = 'Continuing...'
|
||||||
return await sendRequest(chat.messages, undefined, true)
|
opts.didSummary = true
|
||||||
|
return await sendRequest(chat.messages, opts)
|
||||||
}
|
}
|
||||||
} else if (!summaryPrompt) {
|
} else if (!summaryPrompt) {
|
||||||
addMessage(chatId, { role: 'error', content: 'Unable to summarize. No summary prompt defined.', uuid: uuidv4() })
|
addMessage(chatId, { role: 'error', content: 'Unable to summarize. No summary prompt defined.', uuid: uuidv4() })
|
||||||
|
@ -315,67 +338,79 @@
|
||||||
if (typeof setting.apiTransform === 'function') {
|
if (typeof setting.apiTransform === 'function') {
|
||||||
value = setting.apiTransform(chatId, setting, value)
|
value = setting.apiTransform(chatId, setting, value)
|
||||||
}
|
}
|
||||||
if (summaryTarget) {
|
if (opts.summaryRequest && opts.maxTokens) {
|
||||||
// requesting summary. do overrides
|
// requesting summary. do overrides
|
||||||
if (setting.key === 'max_tokens') value = summaryTarget // only as large as we need for summary
|
if (setting.key === 'max_tokens') value = opts.maxTokens // only as large as we need for summary
|
||||||
if (setting.key === 'n') value = 1 // never more than one completion
|
if (setting.key === 'n') value = 1 // never more than one completion for summary
|
||||||
|
}
|
||||||
|
if (opts.streaming) {
|
||||||
|
/*
|
||||||
|
Streaming goes insane with more than one completion.
|
||||||
|
Doesn't seem like there's any way to separate the jumbled mess of deltas for the
|
||||||
|
different completions.
|
||||||
|
*/
|
||||||
|
if (setting.key === 'n') value = 1
|
||||||
}
|
}
|
||||||
if (value !== null) acc[setting.key] = value
|
if (value !== null) acc[setting.key] = value
|
||||||
return acc
|
return acc
|
||||||
}, {})
|
}, {})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Not working yet: a way to get the response as a stream
|
request.stream = opts.streaming
|
||||||
/*
|
|
||||||
request.stream = true
|
|
||||||
await fetchEventSource(apiBase + '/v1/chat/completions', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
Authorization:
|
|
||||||
`Bearer ${$apiKeyStorage}`,
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
},
|
|
||||||
body: JSON.stringify(request),
|
|
||||||
onmessage (ev) {
|
|
||||||
const data = JSON.parse(ev.data)
|
|
||||||
console.log(data)
|
|
||||||
},
|
|
||||||
onerror (err) {
|
|
||||||
throw err
|
|
||||||
}
|
|
||||||
})
|
|
||||||
*/
|
|
||||||
|
|
||||||
response = await (
|
chatResponse.setPromptTokenCount(promptTokenCount)
|
||||||
await fetch(apiBase + '/v1/chat/completions', {
|
|
||||||
|
const fetchOptions = {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${$apiKeyStorage}`,
|
Authorization: `Bearer ${$apiKeyStorage}`,
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
},
|
},
|
||||||
body: JSON.stringify(request)
|
body: JSON.stringify(request)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (opts.streaming) {
|
||||||
|
fetchEventSource(apiBase + '/v1/chat/completions', {
|
||||||
|
...fetchOptions,
|
||||||
|
onmessage (ev) {
|
||||||
|
// Remove updating indicator
|
||||||
|
updating = false
|
||||||
|
updatingMessage = ''
|
||||||
|
if (!chatResponse.hasFinished()) {
|
||||||
|
// console.log('ev.data', ev.data)
|
||||||
|
if (ev.data === '[DONE]') {
|
||||||
|
// ?? anything to do when "[DONE]"?
|
||||||
|
} else {
|
||||||
|
const data = JSON.parse(ev.data)
|
||||||
|
// console.log('data', data)
|
||||||
|
window.requestAnimationFrame(() => { chatResponse.updateFromAsyncResponse(data) })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onerror (err) {
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
}).catch(err => {
|
||||||
|
chatResponse.updateFromError(err.message)
|
||||||
})
|
})
|
||||||
).json()
|
} else {
|
||||||
|
const response = await fetch(apiBase + '/v1/chat/completions', fetchOptions)
|
||||||
|
const json = await response.json()
|
||||||
|
|
||||||
|
// Remove updating indicator
|
||||||
|
updating = false
|
||||||
|
updatingMessage = ''
|
||||||
|
chatResponse.updateFromSyncResponse(json)
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
response = { error: { message: e.message } } as Response
|
chatResponse.updateFromError(e.message)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hide updating bar
|
// Hide updating bar
|
||||||
updating = false
|
updating = false
|
||||||
updatingMessage = ''
|
updatingMessage = ''
|
||||||
|
|
||||||
if (!response.error) {
|
return chatResponse
|
||||||
// Add response counts to usage totals
|
|
||||||
updateRunningTotal(chatId, response.usage, response.model)
|
|
||||||
// const completionTokenCount:number = response.choices.reduce((a, c) => {
|
|
||||||
// // unlike the prompts, token count of the completion is just the completion.
|
|
||||||
// a += encode(c.message.content).length
|
|
||||||
// return a
|
|
||||||
// }, 0)
|
|
||||||
// console.log('estimated response token count', completionTokenCount)
|
|
||||||
}
|
|
||||||
|
|
||||||
return response
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const addNewMessage = () => {
|
const addNewMessage = () => {
|
||||||
|
@ -397,6 +432,14 @@
|
||||||
focusInput()
|
focusInput()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const tts = (text:string, recorded:boolean) => {
|
||||||
|
// Use TTS to read the response, if query was recorded
|
||||||
|
if (recorded && 'SpeechSynthesisUtterance' in window) {
|
||||||
|
const utterance = new SpeechSynthesisUtterance(text)
|
||||||
|
window.speechSynthesis.speak(utterance)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const submitForm = async (recorded: boolean = false, skipInput: boolean = false): Promise<void> => {
|
const submitForm = async (recorded: boolean = false, skipInput: boolean = false): Promise<void> => {
|
||||||
// Compose the system prompt message if there are no messages yet - disabled for now
|
// Compose the system prompt message if there are no messages yet - disabled for now
|
||||||
if (updating) return
|
if (updating) return
|
||||||
|
@ -419,29 +462,18 @@
|
||||||
}
|
}
|
||||||
focusInput()
|
focusInput()
|
||||||
|
|
||||||
const response = await sendRequest(chat.messages)
|
const response = await sendRequest(chat.messages, {
|
||||||
|
chat,
|
||||||
if (response.error) {
|
autoAddMessages: true, // Auto-add and update messages in array
|
||||||
addMessage(chatId, {
|
streaming: chatSettings.stream,
|
||||||
role: 'error',
|
onMessageChange: (messages) => {
|
||||||
content: `Error: ${response.error.message}`,
|
scrollToBottom(true)
|
||||||
uuid: uuidv4()
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
response.choices.forEach((choice) => {
|
|
||||||
// Store usage and model in the message
|
|
||||||
choice.message.usage = response.usage
|
|
||||||
choice.message.model = response.model
|
|
||||||
|
|
||||||
// Remove whitespace around the message that the OpenAI API sometimes returns
|
|
||||||
choice.message.content = choice.message.content.trim()
|
|
||||||
addMessage(chatId, choice.message)
|
|
||||||
// Use TTS to read the response, if query was recorded
|
|
||||||
if (recorded && 'SpeechSynthesisUtterance' in window) {
|
|
||||||
const utterance = new SpeechSynthesisUtterance(choice.message.content)
|
|
||||||
window.speechSynthesis.speak(utterance)
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
await response.promiseToFinish()
|
||||||
|
const message = response.getMessages()[0]
|
||||||
|
if (message) {
|
||||||
|
tts(message.content, recorded)
|
||||||
}
|
}
|
||||||
focusInput()
|
focusInput()
|
||||||
}
|
}
|
||||||
|
@ -456,17 +488,22 @@
|
||||||
const suggestMessages = chat.messages.slice(0, 10) // limit to first 10 messages
|
const suggestMessages = chat.messages.slice(0, 10) // limit to first 10 messages
|
||||||
suggestMessages.push(suggestMessage)
|
suggestMessages.push(suggestMessage)
|
||||||
|
|
||||||
const response = await sendRequest(suggestMessages, 20)
|
const response = await sendRequest(suggestMessages, {
|
||||||
|
chat,
|
||||||
|
autoAddMessages: false,
|
||||||
|
streaming: false
|
||||||
|
})
|
||||||
|
await response.promiseToFinish()
|
||||||
|
|
||||||
if (response.error) {
|
if (response.hasError()) {
|
||||||
addMessage(chatId, {
|
addMessage(chatId, {
|
||||||
role: 'error',
|
role: 'error',
|
||||||
content: `Unable to get suggested name: ${response.error.message}`,
|
content: `Unable to get suggested name: ${response.getError()}`,
|
||||||
uuid: uuidv4()
|
uuid: uuidv4()
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
response.choices.forEach((choice) => {
|
response.getMessages().forEach(m => {
|
||||||
chat.name = choice.message.content
|
chat.name = m.content
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,132 @@
|
||||||
|
<script context="module" lang="ts">
|
||||||
|
// TODO: Integrate API calls
|
||||||
|
import { addMessage, updateRunningTotal } from './Storage.svelte'
|
||||||
|
import type { Chat, ChatCompletionOpts, Message, Response, Usage } from './Types.svelte'
|
||||||
|
import { encode } from 'gpt-tokenizer'
|
||||||
|
import { v4 as uuidv4 } from 'uuid'
|
||||||
|
|
||||||
|
export class ChatCompletionResponse {
|
||||||
|
constructor (opts: ChatCompletionOpts) {
|
||||||
|
this.opts = opts
|
||||||
|
this.chat = opts.chat
|
||||||
|
this.messages = []
|
||||||
|
if (opts.fillMessage) this.messages.push(opts.fillMessage)
|
||||||
|
if (opts.onMessageChange) this.messageChangeListeners.push(opts.onMessageChange)
|
||||||
|
}
|
||||||
|
|
||||||
|
private opts: ChatCompletionOpts
|
||||||
|
private chat: Chat
|
||||||
|
|
||||||
|
private messages: Message[]
|
||||||
|
|
||||||
|
private error: string
|
||||||
|
private didFinish: boolean
|
||||||
|
|
||||||
|
private finishResolver: (value: Message[]) => void
|
||||||
|
private errorResolver: (error: string) => void
|
||||||
|
private finishPromise = new Promise<Message[]>((resolve, reject) => {
|
||||||
|
this.finishResolver = resolve
|
||||||
|
this.errorResolver = reject
|
||||||
|
})
|
||||||
|
|
||||||
|
private promptTokenCount:number
|
||||||
|
private finished = false
|
||||||
|
private messageChangeListeners: ((m: Message[]) => void)[] = []
|
||||||
|
|
||||||
|
setPromptTokenCount (tokens:number) {
|
||||||
|
this.promptTokenCount = tokens
|
||||||
|
}
|
||||||
|
|
||||||
|
updateFromSyncResponse (response: Response) {
|
||||||
|
response.choices.forEach((choice, i) => {
|
||||||
|
const message = this.messages[i] || choice.message
|
||||||
|
message.content = choice.message.content
|
||||||
|
message.usage = response.usage
|
||||||
|
message.model = response.model
|
||||||
|
message.role = choice.message.role
|
||||||
|
this.messages[i] = message
|
||||||
|
if (this.opts.autoAddMessages) addMessage(this.chat.id, message)
|
||||||
|
})
|
||||||
|
this.notifyMessageChange()
|
||||||
|
this.finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
updateFromAsyncResponse (response: Response) {
|
||||||
|
let completionTokenCount = 0
|
||||||
|
response.choices.forEach((choice, i) => {
|
||||||
|
const message = this.messages[i] || {
|
||||||
|
role: 'assistant',
|
||||||
|
content: '',
|
||||||
|
uuid: uuidv4()
|
||||||
|
} as Message
|
||||||
|
choice.delta?.role && (message.role = choice.delta.role)
|
||||||
|
choice.delta?.content && (message.content += choice.delta.content)
|
||||||
|
completionTokenCount += encode(message.content).length
|
||||||
|
message.usage = response.usage || {
|
||||||
|
prompt_tokens: this.promptTokenCount
|
||||||
|
} as Usage
|
||||||
|
message.model = response.model
|
||||||
|
message.finish_reason = choice.finish_reason
|
||||||
|
message.streaming = choice.finish_reason === null
|
||||||
|
this.messages[i] = message
|
||||||
|
if (this.opts.autoAddMessages) addMessage(this.chat.id, message)
|
||||||
|
})
|
||||||
|
// total up the tokens
|
||||||
|
const totalTokens = this.promptTokenCount + completionTokenCount
|
||||||
|
this.messages.forEach(m => {
|
||||||
|
if (m.usage) {
|
||||||
|
m.usage.completion_tokens = completionTokenCount
|
||||||
|
m.usage.total_tokens = totalTokens
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const finished = !this.messages.find(m => m.streaming)
|
||||||
|
this.notifyMessageChange()
|
||||||
|
if (finished) this.finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
updateFromError (errorMessage: string): void {
|
||||||
|
this.error = errorMessage
|
||||||
|
if (this.opts.autoAddMessages) {
|
||||||
|
addMessage(this.chat.id, {
|
||||||
|
role: 'error',
|
||||||
|
content: `Error: ${errorMessage}`,
|
||||||
|
uuid: uuidv4()
|
||||||
|
} as Message)
|
||||||
|
}
|
||||||
|
this.notifyMessageChange()
|
||||||
|
this.finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
onMessageChange = (listener: (m: Message[]) => void): number =>
|
||||||
|
this.messageChangeListeners.push(listener)
|
||||||
|
|
||||||
|
promiseToFinish = (): Promise<Message[]> => this.finishPromise
|
||||||
|
|
||||||
|
hasFinished = (): boolean => this.finished
|
||||||
|
|
||||||
|
getError = (): string => this.error
|
||||||
|
hasError = (): boolean => !!this.error
|
||||||
|
getMessages = (): Message[] => this.messages
|
||||||
|
|
||||||
|
private notifyMessageChange (): void {
|
||||||
|
this.messageChangeListeners.forEach((listener) => {
|
||||||
|
listener(this.messages)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private finish = (): void => {
|
||||||
|
if (this.didFinish) return
|
||||||
|
this.didFinish = true
|
||||||
|
const message = this.messages[0]
|
||||||
|
if (message) {
|
||||||
|
updateRunningTotal(this.chat.id, message.usage as any, message.model as any)
|
||||||
|
}
|
||||||
|
this.finished = true
|
||||||
|
if (this.error) {
|
||||||
|
this.errorResolver(this.error)
|
||||||
|
} else {
|
||||||
|
this.finishResolver(this.messages)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
|
@ -7,7 +7,7 @@
|
||||||
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 } from '@fortawesome/free-solid-svg-icons/index'
|
import { faTrash, faDiagramPredecessor, faDiagramNext, faCircleCheck, faPaperPlane, faEye, faEyeSlash } from '@fortawesome/free-solid-svg-icons/index'
|
||||||
import { errorNotice, scrollIntoViewWithOffset } 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'
|
||||||
|
|
||||||
|
@ -43,7 +43,7 @@
|
||||||
})
|
})
|
||||||
|
|
||||||
const edit = () => {
|
const edit = () => {
|
||||||
if (noEdit) return
|
if (noEdit || message.streaming) return
|
||||||
editing = true
|
editing = true
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
const el = document.getElementById('edit-' + message.uuid)
|
const el = document.getElementById('edit-' + message.uuid)
|
||||||
|
@ -77,22 +77,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const scrollToMessage = (uuid:string | string[] | undefined) => {
|
|
||||||
if (Array.isArray(uuid)) {
|
|
||||||
uuid = uuid[0]
|
|
||||||
}
|
|
||||||
if (!uuid) {
|
|
||||||
console.error('Not a valid uuid', uuid)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const el = document.getElementById('message-' + uuid)
|
|
||||||
if (el) {
|
|
||||||
scrollIntoViewWithOffset(el, 80)
|
|
||||||
} else {
|
|
||||||
console.error("Can't find element with message ID", uuid)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Double click for mobile support
|
// Double click for mobile support
|
||||||
let lastTap: number = 0
|
let lastTap: number = 0
|
||||||
const editOnDoubleTap = () => {
|
const editOnDoubleTap = () => {
|
||||||
|
@ -146,7 +130,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
let waitingForTruncateConfirm:any = 0
|
let waitingForTruncateConfirm:any = 0
|
||||||
|
|
||||||
const checkTruncate = () => {
|
const checkTruncate = () => {
|
||||||
|
@ -195,6 +178,7 @@
|
||||||
class:summarized={message.summarized}
|
class:summarized={message.summarized}
|
||||||
class:suppress={message.suppress}
|
class:suppress={message.suppress}
|
||||||
class:editing={editing}
|
class:editing={editing}
|
||||||
|
class:streaming={message.streaming}
|
||||||
>
|
>
|
||||||
<div class="message-body content">
|
<div class="message-body content">
|
||||||
|
|
||||||
|
@ -210,6 +194,9 @@
|
||||||
on:touchend={editOnDoubleTap}
|
on:touchend={editOnDoubleTap}
|
||||||
on:dblclick|preventDefault={() => edit()}
|
on:dblclick|preventDefault={() => edit()}
|
||||||
>
|
>
|
||||||
|
{#if message.summary && !message.summary.length}
|
||||||
|
<p><b>Summarizing...</b></p>
|
||||||
|
{/if}
|
||||||
<SvelteMarkdown
|
<SvelteMarkdown
|
||||||
source={message.content}
|
source={message.content}
|
||||||
options={markdownOptions}
|
options={markdownOptions}
|
||||||
|
|
|
@ -60,7 +60,7 @@ const gptDefaults = {
|
||||||
temperature: 1,
|
temperature: 1,
|
||||||
top_p: 1,
|
top_p: 1,
|
||||||
n: 1,
|
n: 1,
|
||||||
stream: false,
|
stream: true,
|
||||||
stop: null,
|
stop: null,
|
||||||
max_tokens: 512,
|
max_tokens: 512,
|
||||||
presence_penalty: 0,
|
presence_penalty: 0,
|
||||||
|
@ -312,6 +312,12 @@ const chatSettingsList: ChatSetting[] = [
|
||||||
...summarySettings,
|
...summarySettings,
|
||||||
// ...responseAlterationSettings,
|
// ...responseAlterationSettings,
|
||||||
modelSetting,
|
modelSetting,
|
||||||
|
{
|
||||||
|
key: 'stream',
|
||||||
|
name: 'Stream Response',
|
||||||
|
title: 'Stream responses as they are generated.',
|
||||||
|
type: 'boolean'
|
||||||
|
},
|
||||||
{
|
{
|
||||||
key: 'temperature',
|
key: 'temperature',
|
||||||
name: 'Sampling Temperature',
|
name: 'Sampling Temperature',
|
||||||
|
|
|
@ -89,6 +89,7 @@
|
||||||
// make sure old chat messages have UUID
|
// make sure old chat messages have UUID
|
||||||
chat.messages.forEach((m) => {
|
chat.messages.forEach((m) => {
|
||||||
m.uuid = m.uuid || uuidv4()
|
m.uuid = m.uuid || uuidv4()
|
||||||
|
delete m.streaming
|
||||||
})
|
})
|
||||||
// Make sure the usage totals object is set
|
// Make sure the usage totals object is set
|
||||||
// (some earlier versions of this had different structures)
|
// (some earlier versions of this had different structures)
|
||||||
|
@ -163,7 +164,10 @@
|
||||||
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
|
||||||
if (!message.uuid) message.uuid = uuidv4()
|
if (!message.uuid) message.uuid = uuidv4()
|
||||||
|
if (chat.messages.indexOf(message) < 0) {
|
||||||
|
// Don't have message, add it
|
||||||
chat.messages.push(message)
|
chat.messages.push(message)
|
||||||
|
}
|
||||||
chatsStorage.set(chats)
|
chatsStorage.set(chats)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -27,6 +27,8 @@
|
||||||
summarized?: string[];
|
summarized?: string[];
|
||||||
summary?: string[];
|
summary?: string[];
|
||||||
suppress?: boolean;
|
suppress?: boolean;
|
||||||
|
finish_reason?: string;
|
||||||
|
streaming?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ResponseAlteration = {
|
export type ResponseAlteration = {
|
||||||
|
@ -88,6 +90,7 @@
|
||||||
index: number;
|
index: number;
|
||||||
message: Message;
|
message: Message;
|
||||||
finish_reason: string;
|
finish_reason: string;
|
||||||
|
delta: Message;
|
||||||
}[];
|
}[];
|
||||||
usage: Usage;
|
usage: Usage;
|
||||||
model: Model;
|
model: Model;
|
||||||
|
@ -111,6 +114,17 @@
|
||||||
}[];
|
}[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type ChatCompletionOpts = {
|
||||||
|
chat: Chat;
|
||||||
|
autoAddMessages: boolean;
|
||||||
|
maxTokens?:number;
|
||||||
|
summaryRequest?:boolean;
|
||||||
|
didSummary?:boolean;
|
||||||
|
streaming?:boolean;
|
||||||
|
onMessageChange?: (messages: Message[]) => void;
|
||||||
|
fillMessage?:Message,
|
||||||
|
};
|
||||||
|
|
||||||
export type GlobalSettings = {
|
export type GlobalSettings = {
|
||||||
profiles: Record<string, ChatSettings>;
|
profiles: Record<string, ChatSettings>;
|
||||||
lastProfile?: string;
|
lastProfile?: string;
|
||||||
|
|
|
@ -21,15 +21,41 @@
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|
||||||
export const scrollIntoViewWithOffset = (element:HTMLElement, offset:number) => {
|
export const scrollIntoViewWithOffset = (element:HTMLElement, offset:number, instant:boolean = false, bottom:boolean = false) => {
|
||||||
|
const behavior = instant ? 'instant' : 'smooth'
|
||||||
|
if (bottom) {
|
||||||
window.scrollTo({
|
window.scrollTo({
|
||||||
behavior: 'smooth',
|
behavior: behavior as any,
|
||||||
|
top:
|
||||||
|
(element.getBoundingClientRect().bottom) -
|
||||||
|
document.body.getBoundingClientRect().top - (window.innerHeight - offset)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
window.scrollTo({
|
||||||
|
behavior: behavior as any,
|
||||||
top:
|
top:
|
||||||
element.getBoundingClientRect().top -
|
element.getBoundingClientRect().top -
|
||||||
document.body.getBoundingClientRect().top -
|
document.body.getBoundingClientRect().top -
|
||||||
offset
|
offset
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const scrollToMessage = (uuid:string | string[] | undefined, offset:number = 60, instant:boolean = false, bottom:boolean = false) => {
|
||||||
|
if (Array.isArray(uuid)) {
|
||||||
|
uuid = uuid[0]
|
||||||
|
}
|
||||||
|
if (!uuid) {
|
||||||
|
console.error('Not a valid uuid', uuid)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const el = document.getElementById('message-' + uuid)
|
||||||
|
if (el) {
|
||||||
|
scrollIntoViewWithOffset(el, offset, instant, bottom)
|
||||||
|
} else {
|
||||||
|
console.error("Can't find element with message ID", uuid)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export const checkModalEsc = (event:KeyboardEvent|undefined):boolean|void => {
|
export const checkModalEsc = (event:KeyboardEvent|undefined):boolean|void => {
|
||||||
if (!event || event.key !== 'Escape') return
|
if (!event || event.key !== 'Escape') return
|
||||||
|
|
Loading…
Reference in New Issue