Skip to content

Commit

Permalink
fix: correct streaming interruptions
Browse files Browse the repository at this point in the history
  • Loading branch information
henrycunh committed May 7, 2023
1 parent 522782c commit f4cec0d
Show file tree
Hide file tree
Showing 15 changed files with 242 additions and 50 deletions.
2 changes: 1 addition & 1 deletion app.vue
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ useHead({
</script>

<template>
<div relative h-100dvh overflow-hidden>
<div relative h-100vh overflow-hidden>
<NuxtLayout>
<NuxtPage h-full />
</NuxtLayout>
Expand Down
14 changes: 12 additions & 2 deletions components/app-chat/header.vue
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ defineProps<{
const { currentConversation, updateConversation } = useConversations()
const { isDetaEnabled } = useDeta()
const { isOnSharePage } = useSession()
const { personaList } = usePersona()
const conversationTitle = ref<string>(currentConversation.value?.title || '')
const isEditingTitle = ref(false)
Expand Down Expand Up @@ -43,6 +44,15 @@ function onFavoriteConversation() {
},
})
}
const currentPersona = computed(() => {
if (!currentConversation.value) {
return null
}
return personaList.value.find(
persona => persona.id === currentConversation.value?.metadata?.personaId,
) || personaList.value[0]
})
</script>

<template>
Expand Down Expand Up @@ -106,8 +116,8 @@ function onFavoriteConversation() {
>
Share
</UButton>
<!-- TODO: Implement conversation settings -->
<!-- <UButton ml-auto icon="i-tabler-settings text-5" /> -->
</div>

<div />
</div>
</template>
3 changes: 2 additions & 1 deletion components/app-chat/history-container.vue
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,15 @@ const chatScroll = useScroll(container, {
if (event) {
// Detect if the user is at the bottom of the chat
const el = (event.target as HTMLElement)
if (el.scrollTop + el.clientHeight >= el.scrollHeight) {
if (el.scrollTop + el.clientHeight >= el.scrollHeight - 200) {
lockToBottom.value = true
}
else {
lockToBottom.value = false
}
}
},
behavior: 'smooth',
})
function getScrollHeight() {
Expand Down
29 changes: 26 additions & 3 deletions components/app-navbar/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,22 @@ const sidebarItems = [
{
path: '/chat',
icon: 'i-tabler-messages',
title: 'Chat',
},
{
path: '/history',
icon: 'i-tabler-history',
title: 'History',
},
// {
// path: '/personas',
// icon: 'i-tabler-users',
// title: 'Personas',
// },
{
path: '/settings',
icon: 'i-tabler-settings',
title: 'Settings',
},
]
const navigationBarPositionClasses = computed(() => {
Expand Down Expand Up @@ -48,6 +56,16 @@ const activeOverlayClasses = computed(() => {
const isCurrentRoute = (item: typeof sidebarItems[0]) => {
return useRoute().path.startsWith(item.path)
}
const tooltipPlacement = computed(() => {
const positionMapping = {
top: 'bottom',
bottom: 'top',
right: 'left',
left: 'right',
} as const
return positionMapping[navigationBarPosition.value]
})
</script>

<template>
Expand All @@ -59,10 +77,15 @@ const isCurrentRoute = (item: typeof sidebarItems[0]) => {
<div
v-for="item in sidebarItems"
:key="item.path"
p-4 text-color
v-tooltip="{
content: item.title,
placement: tooltipPlacement,
}"
p-4
text-color
cursor-pointer
relative
flex items-center justify-center
relative flex items-center
justify-center
transition-all
group
:class="itemClasses"
Expand Down
29 changes: 21 additions & 8 deletions composables/conversation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { Configuration, OpenAIApi } from 'openai'
import pLimit from 'p-limit'
import { encode } from 'gpt-token-utils'
import type { types } from '~~/utils/types'
import trimIndent from '~~/utils/string'

const MaxTokensPerModel = {
'gpt-4': 8180,
Expand Down Expand Up @@ -204,7 +205,7 @@ export const useConversations = () => {

const assistantMessageList = (fromConversation.messages || []).filter((message: ChatMessage) => message.role === 'assistant')
const lastAssistantMessage = assistantMessageList[assistantMessageList.length - 1]

console.log('lastAssistantMessage', lastAssistantMessage)
const userMessage = {
id: nanoid(),
role: 'user' as const,
Expand All @@ -231,12 +232,14 @@ export const useConversations = () => {
// }

let messageList: any[] = getMessageChain(fromConversation.messages, userMessage)
if (fromConversation.systemMessage) {
messageList = [
{ role: 'system', text: fromConversation.systemMessage, id: 'system-message' },
...messageList,
]
}
messageList = [
{
role: 'system',
text: fromConversation.systemMessage || getDefaultSystemMessage(),
id: 'system-message',
},
...messageList,
]
messageList = messageList.map(message => ({
role: message.role,
content: message.text,
Expand Down Expand Up @@ -264,12 +267,13 @@ export const useConversations = () => {
const getTokenCount = () => encode(messageList.map(message => message.content).join('\n\n')).length
let lastTokenCount = getTokenCount()
if (getTokenCount() > MaxTokensPerModel[modelUsed.value]) {
logger.info('Message is too long, removing messages...', { lastTokenCount, MaxTokensPerModel, modelUsed })
// Remove the first message that is not a system message
// until the token count is less than the max or there are no more messages
while (messageList.length > 1 && lastTokenCount > MaxTokensPerModel[modelUsed.value]) {
messageList = [
messageList[0],
...messageList.filter(({ role }) => role !== 'system').slice(1),
...(messageList.filter(({ role }) => role !== 'system').slice(1)),
]
lastTokenCount = getTokenCount()
}
Expand Down Expand Up @@ -493,3 +497,12 @@ function getMessageChain(messages: ChatMessage[], message: ChatMessage): ChatMes
}
return [...getMessageChain(messages, parentMessage), message]
}

function getDefaultSystemMessage() {
const currentDate = new Date().toISOString().split('T')[0]
return trimIndent(`
You are ChatGPT, a large language model trained by OpenAI. Answer as concisely as possible.
Knowledge cutoff: 2021-09-01
Current date: ${currentDate}
`)
}
3 changes: 2 additions & 1 deletion composables/idb.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,10 @@ export function useIDB(options?: { disableStorage: boolean }) {
if (isStorageAvailable() && process.client) {
const db = new Dexie('gepeto')

db.version(1).stores({
db.version(2).stores({
knowledge: 'id, title, type, sections, metadata, updatedAt, createdAt',
conversations: 'id, title, messages, metadata, createdAt, updatedAt',
personas: 'id, title, instructions',
})

return db
Expand Down
46 changes: 14 additions & 32 deletions composables/language-model.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Configuration, OpenAIApi } from 'openai'
import type { NitroFetchOptions } from 'nitropack'
import { nanoid } from 'nanoid'
import { streamOpenAIResponse } from '~~/utils/fetch-sse'

export function useLanguageModel() {
const { apiKey } = useSettings()
Expand Down Expand Up @@ -93,42 +94,23 @@ export function useLanguageModel() {
return result
}
else {
const stream = (response as ReadableStream)
const reader = stream.getReader()
const decoder = new TextDecoder()
while (true) {
const { done, value } = await reader.read()
if (done) {
break
for await (const data of streamOpenAIResponse(response)) {
if (data.id) {
result.id = data.id
}
const decoded = decoder.decode(value)
const decodedData = decoded.split('data: ').map(s => s.trim()).filter(content => content.length > 0 && content !== '[DONE]')
try {
const chunks = decodedData.map(data => JSON.parse(data)).filter(data => data.choices?.[0]?.delta?.content)
const [parsed] = chunks
if (!parsed) {
continue
if (data?.choices?.length) {
const delta = data.choices[0].delta
result.delta = delta.content
if (delta?.content) {
result.text += delta.content
}
if (parsed.id) {
result.id = parsed.id
}
if (parsed?.choices?.length) {
const delta = parsed.choices[0].delta
result.delta = delta.content
if (delta?.content) {
result.text += delta.content
}
result.detail = parsed
if (delta.role) {
result.role = delta.role
}
}
if (onProgress) {
await onProgress(result)
result.detail = data
if (delta.role) {
result.role = delta.role
}
}
catch (e) {
console.log(e, decoded, decodedData)
if (onProgress) {
await onProgress(result)
}
}
return result
Expand Down
55 changes: 55 additions & 0 deletions composables/persona.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { nanoid } from 'nanoid'
import type { types } from '~~/utils/types'

export function usePersona() {
const db = useIDB()

const personaList = useState<types.Persona[]>('personaList', () => [])

async function initPersonaList() {
personaList.value = await db.table('personas').toArray()
if (!personaList.value.length) {
await createPersona({
id: nanoid(),
title: 'Golem',
instructions: 'You are Golem, a large language model based assistant. Answer as concisely as possible.',
})
}
}

async function createPersona(persona: types.Persona) {
const newPersona = {
...persona,
id: nanoid(),
}
await db.table('personas').add(newPersona)
personaList.value.push(newPersona)
await updatePersonaList()
}

async function deletePersona(personaId: string) {
await db.table('personas').delete(personaId)
personaList.value = personaList.value.filter(p => p.id !== personaId)
await updatePersonaList()
}

async function updatePersona(personaId: string, update: Partial<types.Persona>) {
const persona = personaList.value.find(p => p.id === personaId)
if (persona) {
await db.table('personas').put({ ...persona, ...update })
}
await updatePersonaList()
}

async function updatePersonaList() {
personaList.value = await db.table('personas').toArray()
}

return {
initPersonaList,
personaList,
createPersona,
deletePersona,
updatePersona,
}
}
8 changes: 8 additions & 0 deletions composables/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,14 @@ export async function useSetup(options?: { disableStorage: boolean; embedded?: b
switchConversation,
} = useConversations()

const { initPersonaList, personaList } = usePersona()

watchEffect(async () => {
if (personaList.value.length === 0) {
await initPersonaList()
}
})

const { updateKnowledgeList } = useKnowledge()

await updateKnowledgeList()
Expand Down
2 changes: 1 addition & 1 deletion layouts/default.vue
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ useSetup()
<div
relative
class="2xl:max-w-1366px 2xl:mx-auto"
h-100dvh
h-screen
>
<AppNavbar
v-if="!isMobile"
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
"cross-fetch": "^3.1.5",
"deta": "^1.1.0",
"dexie": "^3.2.3",
"eventsource-parser": "^1.0.0",
"floating-vue": "2.0.0-beta.20",
"fuse.js": "^6.6.2",
"gpt-token-utils": "^1.2.0",
Expand Down
34 changes: 34 additions & 0 deletions pages/personas.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<script lang="ts" setup>
const { personaList } = usePersona()
function onCreatePersona() {}
</script>

<template>
<div p-4>
<div
font-bold font-title
text-14px sm:text-22px
text-color mb-2 flex items-center
>
<div>
Personas
</div>
</div>
<div
max-h-100 overflow-y-auto overflow-x-hidden w-full p-2
rounded-2 mt-6
class="light:bg-gray-1/50 dark:bg-dark-3 dark:shadow-dark" shadow shadow-inset
>
{{ personaList }}
</div>
<div flex items-center children:grow gap-3 mt-2>
<UButton
secondary icon="i-tabler-plus"
@click="onCreatePersona"
>
New persona
</UButton>
</div>
</div>
</template>
Loading

0 comments on commit f4cec0d

Please sign in to comment.