Background Image{' '}
-
+
}
diff --git a/web/shared/NavBar.tsx b/web/shared/NavBar.tsx
index 82ac7c834..c954a54de 100644
--- a/web/shared/NavBar.tsx
+++ b/web/shared/NavBar.tsx
@@ -6,8 +6,8 @@ import { isChatPageMemo } from './hooks'
const NavBar: Component = () => {
const chats = chatStore((s) => ({
- chat: s.active?.chat,
- char: s.active?.char,
+ chat: s.details[s.lastChatId]?.chat,
+ char: s.details[s.lastChatId]?.char,
loaded: s.detailLoaded,
opts: s.opts,
}))
diff --git a/web/shared/PresetSettings/Prompt.tsx b/web/shared/PresetSettings/Prompt.tsx
index 867470a61..48de8d6c0 100644
--- a/web/shared/PresetSettings/Prompt.tsx
+++ b/web/shared/PresetSettings/Prompt.tsx
@@ -15,7 +15,7 @@ import { PresetTabProps } from '/web/store/preset-context'
import Accordian from '../Accordian'
export const PromptSettings: Component
= (props) => {
- const character = chatStore((s) => s.active?.char)
+ const character = chatStore((s) => s.details[s.lastChatId]?.char)
const isChat = isChatPageMemo()
const jsonCharId = createMemo(() => {
diff --git a/web/shared/hooks.ts b/web/shared/hooks.ts
index f413846eb..505087f3b 100644
--- a/web/shared/hooks.ts
+++ b/web/shared/hooks.ts
@@ -124,7 +124,10 @@ export function useCharacterBg(src: 'layout' | 'page') {
const state = getStore('user')((s) => ({ ui: s.ui, background: s.background }))
const cfg = getStore('settings')((s) => ({ anonymize: s.anonymize }))
- const chat = getStore('chat')((s) => ({ active: s.active }))
+ const chat = getStore('chat')((s) => ({
+ lastChatId: s.lastChatId,
+ active: s.details[s.lastChatId || 'unknown'],
+ }))
const chars = getStore('character')((s) => ({ chatId: s.activeChatId, chars: s.chatChars }))
const bg = createMemo(() => {
diff --git a/web/shared/util.ts b/web/shared/util.ts
index 5fa68eae1..d3fbee9c8 100644
--- a/web/shared/util.ts
+++ b/web/shared/util.ts
@@ -15,6 +15,7 @@ import { v4 } from 'uuid'
import { getChatPreset } from '../pages/Chat/util'
import { extractReasoning } from '/common/reasoning'
import { getUserId, isLoggedIn } from '../store/api'
+import { debug } from '/common/debug'
const [css, hooks] = createHooks(recommended)
@@ -134,9 +135,13 @@ async function userCacheGet(key: string) {
if (!prop) return
const data = await getItem(prop)
- if (!data) return
+ if (!data) {
+ debug('cache')(`[${key}] miss`)
+ return
+ }
const json = JSON.parse(data)
+ debug('cache')(`[${key}] hit`)
return json
}
@@ -146,6 +151,7 @@ async function userCacheSet(key: string, data: any) {
const prop = getUserCacheKey(key)
if (!prop) return
+ debug('cache')(`[${key}] modified`)
await setItem(prop, JSON.stringify(data))
}
@@ -157,9 +163,10 @@ function getUserCacheKey(key: string) {
}
export function getUtterableText(msg: string) {
- const { active } = getStore('chat').getState()
+ const { details, lastChatId } = getStore('chat').getState()
const { ui, user } = getStore('user').getState()
const { presets } = getStore('presets').getState()
+ const active = details[lastChatId]
if (!active?.chat || !user) return
diff --git a/web/store/character.ts b/web/store/character.ts
index 88fffdbe6..3927b90d2 100644
--- a/web/store/character.ts
+++ b/web/store/character.ts
@@ -114,7 +114,7 @@ export const characterStore = createStore(
getStore('chat').setState({
allChats: (data.allChats || []).sort(sortDesc),
lastFetched: 0,
- lastChatId: null,
+ lastChatId: '',
})
// If we loaded cached chats/characters, forcibly get the latest after we've hydrated the cached data
diff --git a/web/store/chat.ts b/web/store/chat.ts
index 27d60510b..d3f86f002 100644
--- a/web/store/chat.ts
+++ b/web/store/chat.ts
@@ -3,7 +3,7 @@ import { getEncoder } from '../../common/tokenize'
import { AppSchema } from '../../common/types/schema'
import { EVENTS, events } from '../emitter'
import type { ChatModal } from '../pages/Chat/ChatOptions'
-import { clearDraft } from '../shared/hooks'
+import { clearDraft, isChatPage } from '../shared/hooks'
import { storage } from '../shared/util'
import { api } from './api'
import { createStore, getStore } from './create'
@@ -15,12 +15,19 @@ import { embedApi } from './embeddings'
import { msgStore } from './message'
import { subscribe } from './socket'
import { toastStore } from './toasts'
-import { inline, replace } from '/common/util'
+import { replace } from '/common/util'
export type AllChat = ChatData
+export type ChatDetail = {
+ chat: AppSchema.Chat & { background?: string }
+ char: AppSchema.Character
+ replyAs?: string
+ participantIds: string[]
+}
+
export type ChatState = {
- lastChatId: string | null
+ lastChatId: string
lastFetched: number
detailLoaded: boolean
@@ -28,12 +35,15 @@ export type ChatState = {
// All user chats a user owns or is a member of
allChats: AllChat[]
- active?: {
- chat: AppSchema.Chat
- char: AppSchema.Character
- replyAs?: string
- participantIds: string[]
- }
+ // active?: {
+ // chat: AppSchema.Chat
+ // char: AppSchema.Character
+ // replyAs?: string
+ // participantIds: string[]
+ // }
+
+ details: Record
+
chatProfiles: AppSchema.Profile[]
// chatBots: AppSchema.Character[]
// chatBotMap: Record
@@ -87,13 +97,13 @@ export type NewChat = {
const initState: ChatState = {
lastFetched: 0,
- lastChatId: null,
+ lastChatId: '',
detailLoaded: false,
detailLoading: false,
+ details: {},
allChats: [],
- active: undefined,
/** All profiles that have ever participated in the active chat */
chatProfiles: [],
@@ -120,10 +130,12 @@ const EDITING_KEY = 'chat-detail-settings'
export const chatStore = createStore('chat', {
lastFetched: 0,
- lastChatId: storage.localGetItem('lastChatId'),
+ lastChatId: '',
detailLoaded: false,
detailLoading: false,
+ details: {},
+
allChats: [],
chatProfiles: [],
memberIds: {},
@@ -171,19 +183,7 @@ export const chatStore = createStore('chat', {
}
}
},
- async *setChat({ active, allChats }, chatId: string, update: Partial) {
- const next = replace(chatId, allChats, update)
- yield { allChats: next }
- if (active?.chat._id !== chatId) return
-
- return {
- active: {
- ...active,
- chat: active?.chat._id === chatId ? Object.assign({}, active.chat, update) : active?.chat,
- },
- }
- },
async *openChat(
_,
id: string,
@@ -203,16 +203,6 @@ export const chatStore = createStore('chat', {
const res = await chatsApi.getChat(id)
yield { detailLoading: false }
- /**
- * We need to check the chat id changed while we were loading this chat.
- * If it changed, we need to abandon this payload
- */
- const currentId = get().lastChatId
- if (currentId !== id) {
- console.log(`[chat-state] abandoned chat id ${inline({ was: id, now: currentId })}`)
- return
- }
-
if (res.error) {
toastStore.error(`Failed to retrieve conversation: ${res.error}`)
opts?.onDone?.(false, undefined)
@@ -257,12 +247,12 @@ export const chatStore = createStore('chat', {
yield {
lastChatId: id,
- active: {
+ details: updateDetails(id, {
chat: res.result.chat,
char: res.result.character,
participantIds: res.result.active,
replyAs: isMultiChars ? undefined : res.result.character._id,
- },
+ }),
chatProfiles: res.result.members,
memberIds: res.result.members.reduce(toMemberKeys, {}),
detailLoaded: true,
@@ -274,19 +264,15 @@ export const chatStore = createStore('chat', {
})
}
},
- setAutoReplyAs({ active }, charId: string | undefined) {
- if (!active) return
+ setAutoReplyAs({ lastChatId }, charId: string | undefined) {
+ if (!lastChatId) return
return {
- active: { ...active, replyAs: charId },
+ details: updateDetails(lastChatId, { replyAs: charId }),
}
},
- async *updateChatScenarioStates({ active }, chatId: string, states: string[]) {
- if (!active || active.chat._id !== chatId) return
+ async *updateChatScenarioStates({}, chatId: string, states: string[]) {
yield {
- active: {
- ...active,
- chat: { ...active.chat, scenarioStates: states },
- },
+ details: updateChatDetail(chatId, { scenarioStates: states }),
}
const res = await chatsApi.editChat(chatId, {
scenarioStates: states,
@@ -298,16 +284,12 @@ export const chatStore = createStore('chat', {
}
},
- async *removeChatBackground({ active }) {
- if (!active) return
-
- await storage.removeItem(`chat-background-${active.chat._id}`)
+ async *removeChatBackground({ lastChatId }) {
+ if (!lastChatId) return
+ await storage.removeItem(`chat-background-${lastChatId}`)
yield {
- active: {
- ...active,
- chat: { ...active.chat, background: undefined },
- },
+ details: updateChatDetail(lastChatId, { background: undefined }),
}
},
@@ -315,35 +297,31 @@ export const chatStore = createStore('chat', {
return { msgVisibility: messageId ? { id: messageId } : undefined }
},
- async *editChatBackground({ active }, image: File) {
- if (!active) return
+ async *editChatBackground({ lastChatId }, image: File) {
+ if (!lastChatId) return
const base64 = await imageApi.getImageData(image)
if (!base64) return
- await storage.setItem(`chat-background-${active.chat._id}`, base64)
+ await storage.setItem(`chat-background-${lastChatId}`, base64)
yield {
- active: {
- ...active,
- chat: { ...active.chat, background: base64 },
- },
+ details: updateChatDetail(lastChatId, { background: base64 }),
}
},
- async *editLocalChatSettings({ active }, settings: any) {
- if (!active) return
-
- const id = `chat-settings-${active.chat._id}`
+ async *editLocalChatSettings({ lastChatId }, settings: any) {
+ if (!lastChatId) return
+ const id = `chat-settings-${lastChatId}`
const current = await storage.getItem(id).then((curr) => JSON.parse(curr || '{}'))
const next = { ...current, ...settings }
await storage.setItem(id, JSON.stringify(next))
- return { active: { ...active, chat: { ...active.chat, localSettings: next } } }
+ return { details: updateChatDetail(lastChatId, { localSettings: next }) }
},
async *editChat(
- { allChats, active },
+ { allChats },
id: string,
update: Partial,
opts?: { useOverrides?: boolean; quiet?: boolean; onSuccess?: () => void }
@@ -355,9 +333,6 @@ export const chatStore = createStore('chat', {
}
if (res.result) {
- res.result.background = active?.chat.background
- res.result.localSettings = active?.chat.localSettings
-
opts?.onSuccess?.()
if (!opts?.quiet) {
@@ -376,20 +351,22 @@ export const chatStore = createStore('chat', {
// }
// }
- if (active && active.chat._id === id) {
- yield {
- active: {
- ...active,
- chat: res.result!,
- char: active.char,
- participantIds: active.participantIds,
- },
- }
+ yield {
+ details: updateDetails(id, { chat: res.result }),
}
}
},
- async *assignChatPreset({ active }, chatId: string, preset: string, onSuccess?: () => void) {
+ async *setChat({ details, allChats }, chatId: string, update: Partial) {
+ const next = replace(chatId, allChats, update)
+ yield { allChats: next }
+
+ return {
+ details: updateChatDetail(chatId, update),
+ }
+ },
+
+ async *assignChatPreset({}, chatId: string, preset: string, onSuccess?: () => void) {
if (!preset) {
console.error(`Tried to assign undefined preset to chat`)
return
@@ -473,7 +450,7 @@ export const chatStore = createStore('chat', {
},
async *upsertTempCharacter(
- { active, allChats },
+ { allChats, details },
chatId: string,
char: Omit & {
_id?: string
@@ -481,16 +458,14 @@ export const chatStore = createStore('chat', {
onSuccess?: (char: AppSchema.Character) => void
) {
const res = await chatsApi.upsertTempCharacter(chatId, char)
+ const prev = details[chatId]?.chat
if (res.result) {
const char = res.result.char
onSuccess?.(char)
- if (active?.chat._id === chatId) {
+ if (prev) {
yield {
- active: {
- ...active,
- chat: Object.assign({}, replaceTemp(active.chat, char)),
- },
+ details: updateDetails(chatId, { chat: replaceTemp(prev, char) }),
}
}
@@ -515,18 +490,18 @@ export const chatStore = createStore('chat', {
}
},
- async *deleteChat({ active, allChats }, chatId: string, onSuccess?: Function) {
+ async *deleteChat({ allChats }, chatId: string, onSuccess?: Function) {
clearDraft(chatId)
const res = await chatsApi.deleteChat(chatId)
if (res.error) return toastStore.error(`Failed to delete chat: ${res.error}`)
if (res.result) {
embedApi.deleteChatCache(chatId)
toastStore.success('Successfully deleted chat')
- if (active?.chat._id === chatId) {
- yield { active: undefined }
- }
- yield { allChats: allChats.filter((ch) => ch._id !== chatId) }
+ yield {
+ allChats: allChats.filter((ch) => ch._id !== chatId),
+ details: removeChatDetail(chatId),
+ }
// if (char?.chats) {
// yield { char: { ...char, chats: char.chats.filter((ch) => ch._id !== chatId) } }
@@ -570,16 +545,21 @@ export const chatStore = createStore('chat', {
await api.get(`/chat/${chatId}/summary`)
},
- async computePrompt({ active }, msg: AppSchema.ChatMessage, perspective?: AppSchema.Character) {
- if (!active) return
+ async computePrompt(
+ { details },
+ msg: AppSchema.ChatMessage,
+ perspective?: AppSchema.Character
+ ) {
+ const detail = details[msg.chatId]
+ if (!detail) return
const { msgs, messageHistory } = msgStore.getState()
const entities = await getPromptEntities()
const encoder = await getEncoder()
- const replyAs = active.replyAs?.startsWith('temp-')
- ? entities.chat.tempCharacters![active.replyAs]
- : entities.characters[active.replyAs!] || active.char
+ const replyAs = detail.replyAs?.startsWith('temp-')
+ ? entities.chat.tempCharacters![detail.replyAs]
+ : entities.characters[detail.replyAs!] || detail.char
const resolvedScenario = resolveScenario(
entities.chat,
@@ -652,20 +632,9 @@ subscribe('profile-handle-changed', { userId: 'string', handle: 'string' }, (bod
})
subscribe('chat-deleted', { chatId: 'string' }, (body) => {
- const { allChats, active } = chatStore.getState()
- if (active?.chat._id === body.chatId) {
- chatStore.setState({ active: undefined })
- }
-
- {
- const next = allChats.filter((ch) => ch._id !== body.chatId)
- chatStore.setState({ allChats: next })
- }
-
- // if (char?.chats) {
- // const next = char.chats.filter((ch) => ch._id !== body.chatId)
- // chatStore.setState({ char: { ...char, chats: next } })
- // }
+ const { allChats } = chatStore.getState()
+ const next = allChats.filter((ch) => ch._id !== body.chatId)
+ chatStore.setState({ allChats: next, details: removeChatDetail(body.chatId) })
})
function sortDesc(left: { updatedAt: string }, right: { updatedAt: string }): number {
@@ -676,14 +645,16 @@ subscribe('member-removed', { memberId: 'string', chatId: 'string' }, (body) =>
const profile = getStore('user').getState().profile
if (!profile) return
- const { chatProfiles, active } = chatStore.getState()
-
- if (!active?.chat) return
- if (active.chat._id !== body.chatId) return
+ const { chatProfiles, details } = chatStore.getState()
+ const detail = details[body.chatId]
+ if (!detail) return
- const nextIds = active.participantIds.filter((id) => id !== body.memberId)
+ const nextIds = detail.participantIds.filter((id) => id !== body.memberId)
const nextProfiles = chatProfiles.filter((mem) => mem.userId !== body.memberId)
- chatStore.setState({ chatProfiles: nextProfiles, active: { ...active, participantIds: nextIds } })
+ chatStore.setState({
+ chatProfiles: nextProfiles,
+ details: updateDetails(body.chatId, { participantIds: nextIds }),
+ })
})
subscribe(
@@ -693,20 +664,21 @@ subscribe(
profile: { kind: 'any', userId: 'string', handle: 'string', _id: 'string', avatar: 'string?' },
},
(body) => {
- const { active, chatProfiles, memberIds } = chatStore.getState()
- if (!active || active.chat._id !== body.chatId) return
+ const { chatProfiles, memberIds, details } = chatStore.getState()
+ const detail = details[body.chatId]
+ if (!detail?.chat) return
const nextProfiles = chatProfiles.concat(body.profile)
const nextProfileMap = { ...memberIds, [body.profile.userId]: body.profile }
- const nextIds = active.participantIds.concat(body.profile.userId)
+ const nextIds = detail.participantIds.concat(body.profile.userId)
const nextChat = {
- ...active.chat,
- memberIds: active.chat.memberIds.concat(body.profile.userId),
+ ...detail.chat,
+ memberIds: detail.chat.memberIds.concat(body.profile.userId),
}
chatStore.setState({
chatProfiles: nextProfiles,
memberIds: nextProfileMap,
- active: { ...active, participantIds: nextIds, chat: nextChat },
+ details: updateDetails(body.chatId, { participantIds: nextIds, chat: nextChat }),
})
}
)
@@ -743,8 +715,9 @@ function getOptsCache(): ChatOptCache {
}
subscribe('chat-server-notification', { chatId: 'string', text: 'string' }, (body) => {
- const { active } = chatStore.getState()
- if (body.chatId !== active?.chat._id) return
+ const { lastChatId } = chatStore.getState()
+ if (!isChatPage()) return
+ if (body.chatId !== lastChatId) return
toastStore.warn(body.text)
})
@@ -752,7 +725,8 @@ subscribe(
'chat-character-added',
{ chatId: 'string', active: 'boolean?', character: 'any' },
(body) => {
- const { active, allChats } = chatStore.getState()
+ const { details, allChats } = chatStore.getState()
+ const detail = details[body.chatId]
const nextChats = allChats.map((chat) => {
if (chat._id !== body.chatId) return chat
@@ -764,21 +738,15 @@ subscribe(
chatStore.setState({ allChats: nextChats })
- if (!active || active.chat._id !== body.chatId) return
+ if (!detail) return
const nextActive = {
- ...(active.chat.characters || {}),
+ ...(detail.chat.characters || {}),
[body.character._id]: body.active ?? true,
}
chatStore.setState({
- active: {
- ...active,
- chat: {
- ...active.chat,
- characters: nextActive,
- },
- },
+ details: updateChatDetail(body.chatId, { characters: nextActive }),
})
events.emit(EVENTS.charAdded, body.character)
@@ -786,7 +754,8 @@ subscribe(
)
subscribe('chat-character-removed', { chatId: 'string', characterId: 'string' }, (body) => {
- const { active, allChats } = chatStore.getState()
+ const { details, allChats } = chatStore.getState()
+ const detail = details[body.chatId]
const nextChats = allChats.map((c) => {
if (c._id !== body.chatId) return c
@@ -797,17 +766,11 @@ subscribe('chat-character-removed', { chatId: 'string', characterId: 'string' },
})
chatStore.setState({ allChats: nextChats })
- if (!active || active.chat._id !== body.chatId) return
+ if (!detail) return
- const nextChatCharacters = { ...(active.chat.characters || {}), [body.characterId]: false }
+ const nextChatCharacters = { ...(detail.chat.characters || {}), [body.characterId]: false }
chatStore.setState({
- active: {
- ...active,
- chat: {
- ...active.chat,
- characters: nextChatCharacters,
- },
- },
+ details: updateChatDetail(body.chatId, { characters: nextChatCharacters }),
})
})
@@ -823,21 +786,18 @@ subscribe('service-prompt', { id: 'string', prompt: 'any' }, (body) => {
})
subscribe('chat-temp-character', { chatId: 'string', character: 'any' }, (body) => {
- const { active, allChats } = chatStore.getState()
+ const { details, allChats } = chatStore.getState()
+ const detail = details[body.chatId]
const nextChats = allChats.map((chat) => {
if (chat._id !== body.chatId) return chat
return replaceTemp(chat, body.character)
})
chatStore.setState({ allChats: nextChats })
-
- if (!active || active.chat._id !== body.chatId) return
+ if (!detail) return
chatStore.setState({
- active: {
- ...active,
- chat: replaceTemp(active.chat, body.character),
- },
+ details: updateDetails(body.chatId, { chat: replaceTemp(detail.chat, body.character) }),
})
})
@@ -850,3 +810,47 @@ function replaceTemp(chat: AppSchema.Chat, char: AppSchema.Character): AppSchema
tempCharacters: { ...temp },
}
}
+
+function removeChatDetail(id: string): Record {
+ const { details } = chatStore.getState()
+ const next = {
+ ...details,
+ [id]: undefined,
+ }
+
+ return next as Record
+}
+
+function updateChatDetail(
+ id: string,
+ update: Partial
+): Record {
+ const { details } = chatStore.getState()
+ const detail = details[id]
+ if (detail?.chat) return details
+
+ const next = {
+ ...details,
+ [id]: {
+ ...detail,
+ chat: { ...detail.chat, ...update },
+ },
+ }
+
+ return next
+}
+
+function updateDetails(id: string, update: Partial): Record {
+ const { details } = chatStore.getState()
+ const last = details[id] || {}
+
+ const next = {
+ ...details,
+ [id]: {
+ ...last,
+ ...update,
+ },
+ }
+
+ return next
+}
diff --git a/web/store/context.tsx b/web/store/context.tsx
index f1f6f9d78..0349ba7f7 100644
--- a/web/store/context.tsx
+++ b/web/store/context.tsx
@@ -2,7 +2,7 @@ import { JSX, createContext, createEffect, createMemo, useContext } from 'solid-
import { createStore } from 'solid-js/store'
import { characterStore } from './character'
import { settingStore } from './settings'
-import { chatStore } from './chat'
+import { ChatDetail, chatStore } from './chat'
import { AppSchema, UI } from '/common/types'
import { userStore } from './user'
import { toMap } from '../shared/util'
@@ -40,6 +40,7 @@ export type ContextState = {
impersonate?: AppSchema.Character
user?: AppSchema.User
profile?: AppSchema.Profile
+ active?: ChatDetail
chatProfiles?: AppSchema.Profile[]
flags: FeatureFlags
char?: AppSchema.Character
@@ -100,7 +101,7 @@ export function ContextProvider(props: { children: any }) {
impersonating: s.impersonating,
}))
const chats = chatStore((s) => ({
- active: s.active,
+ active: s.details[s.lastChatId || ''],
allChats: s.allChats,
lastChatId: s.lastChatId,
chatProfiles: s.chatProfiles,
@@ -197,6 +198,7 @@ export function ContextProvider(props: { children: any }) {
activeMap: toMap(activeBots()),
activeBots: activeBots(),
+ active: chats.active,
msgDeleting: msgs.deleting,
impersonate: chars.impersonating,
diff --git a/web/store/data/bot-generate.ts b/web/store/data/bot-generate.ts
index dcbd16920..9a1c48361 100644
--- a/web/store/data/bot-generate.ts
+++ b/web/store/data/bot-generate.ts
@@ -19,7 +19,7 @@ import { UserEmbed } from '/common/types/memory'
import { GenerateRequestV2 } from '/srv/adapter/type'
import { GenerateEntities, getPromptEntities, PromptEntities } from './common'
import { embedApi } from '../embeddings'
-import { ChatState } from '../chat'
+import { ChatDetail } from '../chat'
import { BUILTIN_FORMATS, replaceTags } from '/common/presets/templates'
import { getServiceTempConfig } from '/web/shared/adapter'
import { getActiveBots } from '/web/pages/Chat/util'
@@ -76,7 +76,8 @@ type ChatRequest = Awaited>
type StreamOpts = Exclude
async function streamResponse(opts: StreamOpts, onTick?: TickHandler) {
- const { active } = getStore('chat').getState()
+ const { details, lastChatId } = getStore('chat').getState()
+ const active = details[lastChatId]
if (!active) {
return localApi.error('No active chat. Try refreshing.')
}
@@ -143,6 +144,7 @@ async function streamResponse(opts: StreamOpts, onTick?: TickHandler) {
payload,
signal: opts.signal,
stop: stops,
+ chatId: req.request.chat._id,
// TODO: Re-enable multiplayer streaming
// broadcast: {
// type: 'chat',
@@ -358,7 +360,9 @@ async function buildChatRequest(opts: GenerateOpts) {
async function getActivePromptOptions(
opts: Exclude
) {
- const { active } = getStore('chat').getState()
+ const { details, lastChatId } = getStore('chat').getState()
+ const active = details[lastChatId]
+
const promptState = getStore('prompt').getState()
if (!active) {
@@ -410,7 +414,9 @@ type EventKind =
| 'send-event:ooc'
async function createActiveChatPrompt(opts: GenerateOpts) {
- const { active } = getStore('chat').getState()
+ const { details, lastChatId } = getStore('chat').getState()
+ const active = details[lastChatId]
+
const { ui } = getStore('user').getState()
const { templates } = getStore('presets').getState()
@@ -551,7 +557,7 @@ export type GenerateProps = {
async function getGenerateProps(
opts: GenerateOpts,
- active: NonNullable
+ active: NonNullable
): Promise {
const entities = await getPromptEntities()
diff --git a/web/store/data/common.ts b/web/store/data/common.ts
index 0aa6d279d..58a99ad5a 100644
--- a/web/store/data/common.ts
+++ b/web/store/data/common.ts
@@ -147,7 +147,9 @@ export function replaceUniversalTags(prompt: string, format?: ModelFormat) {
}
async function getGuestEntities() {
- const { active } = getStore('chat').getState()
+ const { details, lastChatId } = getStore('chat').getState()
+ const active = details[lastChatId]
+
if (!active) return
const { msgs, messageHistory, attachments } = getStore('messages').getState()
@@ -219,7 +221,8 @@ function getChatAttachments(
}
function getAuthedPromptEntities() {
- const { active, chatProfiles: members } = getStore('chat').getState()
+ const { details, chatProfiles: members, lastChatId } = getStore('chat').getState()
+ const active = details[lastChatId]
if (!active) return
const { profile, user } = getStore('user').getState()
@@ -267,7 +270,7 @@ function getAuthedPromptEntities() {
}
export function useActivePreset() {
- const chat = getStore('chat')((s) => ({ active: s.active }))
+ const chat = getStore('chat')((s) => ({ active: s.details[s.lastChatId] }))
const user = getStore('user')((s) => ({ user: s.user }))
const preset = createMemo(() => {
@@ -294,7 +297,8 @@ export function getActivePreset(
user?: AppSchema.User
): Partial | undefined {
if (!chat) {
- chat = getStore('chat').getState().active?.chat!
+ const { details, lastChatId } = getStore('chat').getState()
+ chat = details[lastChatId]?.chat
}
if (!user) {
diff --git a/web/store/data/image.ts b/web/store/data/image.ts
index 4ddcfbc12..f19382938 100644
--- a/web/store/data/image.ts
+++ b/web/store/data/image.ts
@@ -640,8 +640,9 @@ export async function createImageRequest(input: {
function getImageEntities(messageId?: string) {
const { user } = getStore('user').getState()
- const { active } = getStore('chat').getState()
+ const { details, lastChatId } = getStore('chat').getState()
const { graph } = getStore('messages').getState()
+ const active = details[lastChatId]
const isChat = isChatPage()
const message = messageId ? graph.tree[messageId]?.msg : undefined
diff --git a/web/store/data/inference.ts b/web/store/data/inference.ts
index 4e35b800a..4ec3c59bf 100644
--- a/web/store/data/inference.ts
+++ b/web/store/data/inference.ts
@@ -38,6 +38,7 @@ type InferenceOpts = {
settings?: Partial
overrides?: Partial
maxTokens?: number
+ chatId?: string
jsonSchema?: JsonField[]
stop?: string[]
@@ -77,6 +78,7 @@ const initState = (init?: {
})
export function inferenceHelper(init: {
+ chatId?: string
preset?: Partial
schema?: JsonField[]
onTick?: TickHandler
@@ -131,6 +133,7 @@ export function inferenceHelper(init: {
signal?: AbortController
prompt: string
image?: string
+ chatId?: string
preset?: Partial
schema?: JsonField[]
maxContext?: number
@@ -158,6 +161,7 @@ export function inferenceHelper(init: {
settings: preset,
jsonSchema: schema,
signal: opts.signal,
+ chatId: init.chatId || opts.chatId,
},
onTick
)
@@ -322,6 +326,7 @@ export async function inferenceStream(opts: InferenceOpts, onTick?: TickHandler)
jsonSchema: opts.jsonSchema,
settings: { ...preset, stream: true },
stop: opts.stop,
+ chatId: opts.chatId,
}
const provider = getProvider(settings?.providerId)
diff --git a/web/store/data/messages.ts b/web/store/data/messages.ts
index 388f67f4e..94edd068b 100644
--- a/web/store/data/messages.ts
+++ b/web/store/data/messages.ts
@@ -157,7 +157,8 @@ export async function getMessages(chatId: string, before: string) {
}
async function getActiveTemplateParts() {
- const { active } = chatStore.getState()
+ const { details, lastChatId } = chatStore.getState()
+ const active = details[lastChatId]
const signal = new AbortController()
const { parts, entities, props, lines } = await botGen.getActivePromptOptions({
diff --git a/web/store/data/presets.ts b/web/store/data/presets.ts
index 8232c27ce..da913497e 100644
--- a/web/store/data/presets.ts
+++ b/web/store/data/presets.ts
@@ -2,12 +2,14 @@ import { v4 } from 'uuid'
import { AppSchema } from '../../../common/types/schema'
import { api, isLoggedIn } from '../api'
import { loadItem, localApi } from './storage'
-import { now, replace } from '/common/util'
+import { deepClone, now, replace } from '/common/util'
import { joinUrl } from '/common/requests/util'
import { toastStore } from '../toasts'
import { storage } from '/web/shared/util'
import { getStore } from '../create'
import { getSafeProviderDetail } from '/common/providers'
+import { defaultPresets, getFallbackPreset } from '/common/presets'
+import { isDefaultPreset } from '/common/default-preset'
export type PresetUpdate = Omit
export type PresetCreate = PresetUpdate & { chatId?: string }
@@ -16,6 +18,7 @@ export type SubscriptionUpdate = Omit('/user/presets')
+ const res = await api.get<{
+ presets: AppSchema.UserGenPreset[]
+ templates: AppSchema.PromptTemplate[]
+ }>('/user/presets')
return res
}
const presets = await loadItem('presets')
- return localApi.result({ presets })
+ const templates = await loadItem('templates')
+ return localApi.result({ presets, templates })
}
export async function getPreset(id: string) {
@@ -54,6 +61,41 @@ export async function getPreset(id: string) {
return localApi.result(preset)
}
+async function getChatPreset(chatId: string) {
+ if (isLoggedIn()) {
+ const res = await api.get(`/user/presets/${chatId}/chat`)
+ return res
+ }
+
+ const chats = await loadItem('chats')
+ const chat = chats.find((ch) => ch._id === chatId)
+ if (!chat) {
+ return localApi.error(`Preset not found: Invalid chat`)
+ }
+
+ const presets = await loadItem('presets')
+
+ if (!chat.genPreset) {
+ const fallback = getFallbackPreset('agnaistic')
+ return localApi.result(fallback)
+ }
+
+ if (isDefaultPreset(chat.genPreset)) {
+ const fallback = { _id: chat.genPreset, ...deepClone(defaultPresets[chat.genPreset]) }
+ return localApi.result(fallback)
+ }
+
+ const preset = presets.find((p) => p._id === chat.genPreset)
+
+ if (!preset) {
+ toastStore.warn(`Preset not found: Using built-in`)
+ const fallback = getFallbackPreset('agnaistic')
+ return localApi.result(fallback)
+ }
+
+ return localApi.result(preset)
+}
+
export async function createPreset(preset: PresetUpdate) {
if (isLoggedIn()) {
const res = await api.post(`/user/presets`, preset)
diff --git a/web/store/embeddings/index.ts b/web/store/embeddings/index.ts
index e791ba4e9..fb7f13280 100644
--- a/web/store/embeddings/index.ts
+++ b/web/store/embeddings/index.ts
@@ -81,7 +81,8 @@ export const embedApi = {
setter({ embeds })
},
initSimiliary: (model: string) => {
- const chat = getStore('chat').getState().active?.chat
+ const { details, lastChatId } = getStore('chat').getState()
+ const chat = details[lastChatId]?.chat
// WIP: Only use small model on mobile
if (isMobile()) {
@@ -154,7 +155,8 @@ const handlers: {
return
}
const user = getStore('user').getState()
- const chat = getStore('chat').getState().active?.chat
+ const { details, lastChatId } = getStore('chat').getState()
+ const chat = details[lastChatId]?.chat
if (type === 'embed') {
post('initSimilarity', {
diff --git a/web/store/message.ts b/web/store/message.ts
index 0c7f85e51..d5f0d46be 100644
--- a/web/store/message.ts
+++ b/web/store/message.ts
@@ -769,7 +769,10 @@ function onCharacterMessageReceived(msg: AppSchema.ChatMessage) {
}
}
- eventStore.onCharacterMessageReceived(chatStore.getState().active?.chat!, messagesSinceLastEvent)
+ const { details, lastChatId } = chatStore.getState()
+ const active = details[lastChatId]
+
+ eventStore.onCharacterMessageReceived(active?.chat!, messagesSinceLastEvent)
}
function getMessageSpeechInfo(msg: AppSchema.ChatMessage, user: AppSchema.User | undefined) {
diff --git a/web/store/preset-context.tsx b/web/store/preset-context.tsx
index 2fd3d3c64..14a338cc6 100644
--- a/web/store/preset-context.tsx
+++ b/web/store/preset-context.tsx
@@ -3,7 +3,7 @@ import { AIAdapter, MODE_SETTINGS, PresetAISettings, ThirdPartyFormat } from '/c
import { AppSchema } from '/common/types'
import { SubscriptionModelOption } from '/common/types/presets'
import { agnaiPresets } from '/common/presets/agnaistic'
-import { createContext, useContext } from 'solid-js'
+import { createContext, createEffect, createSignal, on, useContext } from 'solid-js'
import { getStore } from '/web/store/create'
import { getPresetConnection, PresetConnection, ProviderDefinition } from '/common/providers'
import { defaultPresets, isDefaultPreset } from '/common/default-preset'
@@ -159,12 +159,16 @@ export function usePresetContext(opts?: { anonymous: boolean }) {
const cfg = settingStore((s) => ({ config: s.config }))
const user = userStore((s) => ({ user: s.user }))
const presets = presetStore((s) => ({ list: s.presets, loaded: s.presetsLoaded }))
+ const chats = chatStore((s) => ({ details: s.details, lastChatId: s.lastChatId }))
+
+ const [failedChatId, setFailedChatId] = createSignal('')
const [state, setState, context, setContext] = opts?.anonymous
? [...createStore(initPreset()), ...createStore(initContext())]
: useContext(PresetContext)
- const log = debug(`preset:${context.__}`)
+ const log = (...args: any[]) =>
+ debug(`preset[${state._id ? state._id.slice(0, 4) : '....'}]`).apply(null, args as any)
// Always clear the loading flag
if (!opts?.anonymous) {
@@ -182,12 +186,12 @@ export function usePresetContext(opts?: { anonymous: boolean }) {
const conn = getPresetConnection(state, list)
log(
- '[%s:%s] changing %s --> %s (%s)',
+ '[%s:%s] changing provider FROM:%s --> TO:%s (CONN:%s)',
context.__,
source,
- context.provider?._id,
- state.providerId,
- conn.provider?._id
+ context.provider?._id?.slice(0, 4) || 'nil',
+ state.providerId?.slice(0, 4) || 'nil',
+ conn.provider?._id?.slice(0, 4) || 'nil'
)
const subId =
conn.preset?.providerModels?.agnaistic || conn.preset?.registered?.agnaistic?.subscriptionId
@@ -208,6 +212,58 @@ export function usePresetContext(opts?: { anonymous: boolean }) {
setContext('hides', hides)
}
+ createEffect(
+ on(
+ () => [failedChatId(), chats.details],
+ () => {
+ if (!failedChatId()) return
+
+ const detail = chats.details[failedChatId()]
+
+ log('skipped post-failure (detail not ready yet)')
+ if (!detail?.chat) return
+
+ log('loading after failure (chat now ready)')
+ loadChat(detail.chat)
+ }
+ )
+ )
+
+ const loadChatId = async (chatId: string, knownPresetId?: string) => {
+ try {
+ if (knownPresetId) {
+ if (isDefaultPreset(knownPresetId)) {
+ await loadPresetId(knownPresetId)
+ return
+ }
+
+ const result = await loadPresetId(knownPresetId)
+ if (result) return
+ }
+
+ const result = await presetApi.getChatPreset(chatId)
+ if (result.result) {
+ load(result.result)
+ return
+ }
+
+ if (result.error) {
+ if (result.error === 'Resource not found') {
+ setFailedChatId(chatId)
+ } else {
+ toastStore.error(result.error)
+ }
+ }
+
+ const fallback = getFallbackPreset('agnaistic')
+ load(fallback)
+ } catch (ex) {
+ } finally {
+ log('loaded by chat-id')
+ loadModels()
+ }
+ }
+
const loadChat = async (chat: AppSchema.Chat, alert?: boolean) => {
const expectingUserPreset = !!chat.genPreset && !isDefaultPreset(chat.genPreset)
if (chat.genPreset && state._id === chat.genPreset) {
@@ -430,6 +486,7 @@ export function usePresetContext(opts?: { anonymous: boolean }) {
setState,
provider: changeProvider,
load: loadPresetId,
+ loadChatId,
loadChat,
clear,
upsert,
diff --git a/web/store/presets.ts b/web/store/presets.ts
index 94364778b..4a6d3494c 100644
--- a/web/store/presets.ts
+++ b/web/store/presets.ts
@@ -66,7 +66,8 @@ export const presetStore = createStore(
preset.thirdPartyKey = ''
}
}
- return { presets: res.result.presets, presetsLoaded: true }
+
+ return { presets: res.result.presets, templates: res.result.templates, presetsLoaded: true }
}
},
async *testConnection(
@@ -478,3 +479,5 @@ export async function getRemotePreset(presetId: string) {
return remote
}
+
+export async function getRemoteChatPreset(chatId: string) {}
diff --git a/web/store/response.ts b/web/store/response.ts
index 516f2ac46..b1fa7327d 100644
--- a/web/store/response.ts
+++ b/web/store/response.ts
@@ -196,7 +196,8 @@ export const responseStore = createStore(
return
}
- const { active } = getStore('chat').getState()
+ const { details, lastChatId } = getStore('chat').getState()
+ const active = details[lastChatId]
const replyingCharId = active?.replyAs || activeCharId
const signal = new AbortController()