mirror of
https://github.com/chathub-dev/chathub.git
synced 2025-09-26 20:31:18 +08:00
Refactor bot instance creation
This commit is contained in:
@@ -88,52 +88,24 @@ export abstract class AbstractBot {
|
||||
abstract resetConversation(): void
|
||||
}
|
||||
|
||||
class DummyBot extends AbstractBot {
|
||||
async doSendMessage(_params: SendMessageParams) {
|
||||
// dummy
|
||||
}
|
||||
resetConversation() {
|
||||
// dummy
|
||||
}
|
||||
get name() {
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
export abstract class AsyncAbstractBot extends AbstractBot {
|
||||
#bot: AbstractBot
|
||||
#initializeError?: Error
|
||||
|
||||
constructor() {
|
||||
export abstract class DelegatedBot extends AbstractBot {
|
||||
constructor(private bot: AbstractBot) {
|
||||
super()
|
||||
this.#bot = new DummyBot()
|
||||
this.initializeBot()
|
||||
.then((bot) => {
|
||||
this.#bot = bot
|
||||
})
|
||||
.catch((err) => {
|
||||
this.#initializeError = err
|
||||
})
|
||||
}
|
||||
|
||||
abstract initializeBot(): Promise<AbstractBot>
|
||||
|
||||
doSendMessage(params: SendMessageParams) {
|
||||
if (this.#bot instanceof DummyBot && this.#initializeError) {
|
||||
throw this.#initializeError
|
||||
}
|
||||
return this.#bot.doSendMessage(params)
|
||||
return this.bot.doSendMessage(params)
|
||||
}
|
||||
|
||||
resetConversation() {
|
||||
return this.#bot.resetConversation()
|
||||
return this.bot.resetConversation()
|
||||
}
|
||||
|
||||
get name() {
|
||||
return this.#bot.name
|
||||
return this.bot.name
|
||||
}
|
||||
|
||||
get supportsImageInput() {
|
||||
return this.#bot.supportsImageInput
|
||||
return this.bot.supportsImageInput
|
||||
}
|
||||
}
|
||||
|
@@ -1,49 +1,54 @@
|
||||
import { ChatGPTMode, getUserConfig } from '~/services/user-config'
|
||||
import * as agent from '~services/agent'
|
||||
import { ChatError, ErrorCode } from '~utils/errors'
|
||||
import { AsyncAbstractBot, MessageParams } from '../abstract-bot'
|
||||
import { DelegatedBot, MessageParams } from '../abstract-bot'
|
||||
import { ChatGPTApiBot } from '../chatgpt-api'
|
||||
import { ChatGPTAzureApiBot } from '../chatgpt-azure'
|
||||
import { ChatGPTWebBot } from '../chatgpt-webapp'
|
||||
import { PoeWebBot } from '../poe'
|
||||
import { OpenRouterBot } from '../openrouter'
|
||||
import { PoeWebBot } from '../poe'
|
||||
|
||||
export class ChatGPTBot extends AsyncAbstractBot {
|
||||
async initializeBot() {
|
||||
const { chatgptMode, ...config } = await getUserConfig()
|
||||
if (chatgptMode === ChatGPTMode.API) {
|
||||
if (!config.openaiApiKey) {
|
||||
throw new ChatError('OpenAI API key not set', ErrorCode.API_KEY_NOT_SET)
|
||||
}
|
||||
return new ChatGPTApiBot({
|
||||
openaiApiKey: config.openaiApiKey,
|
||||
openaiApiHost: config.openaiApiHost,
|
||||
chatgptApiModel: config.chatgptApiModel,
|
||||
chatgptApiTemperature: config.chatgptApiTemperature,
|
||||
chatgptApiSystemMessage: config.chatgptApiSystemMessage,
|
||||
})
|
||||
async function initializeBot() {
|
||||
const { chatgptMode, ...config } = await getUserConfig()
|
||||
if (chatgptMode === ChatGPTMode.API) {
|
||||
if (!config.openaiApiKey) {
|
||||
throw new ChatError('OpenAI API key not set', ErrorCode.API_KEY_NOT_SET)
|
||||
}
|
||||
if (chatgptMode === ChatGPTMode.Azure) {
|
||||
if (!config.azureOpenAIApiInstanceName || !config.azureOpenAIApiDeploymentName || !config.azureOpenAIApiKey) {
|
||||
throw new Error('Please check your Azure OpenAI API configuration')
|
||||
}
|
||||
return new ChatGPTAzureApiBot({
|
||||
azureOpenAIApiKey: config.azureOpenAIApiKey,
|
||||
azureOpenAIApiDeploymentName: config.azureOpenAIApiDeploymentName,
|
||||
azureOpenAIApiInstanceName: config.azureOpenAIApiInstanceName,
|
||||
})
|
||||
return new ChatGPTApiBot({
|
||||
openaiApiKey: config.openaiApiKey,
|
||||
openaiApiHost: config.openaiApiHost,
|
||||
chatgptApiModel: config.chatgptApiModel,
|
||||
chatgptApiTemperature: config.chatgptApiTemperature,
|
||||
chatgptApiSystemMessage: config.chatgptApiSystemMessage,
|
||||
})
|
||||
}
|
||||
if (chatgptMode === ChatGPTMode.Azure) {
|
||||
if (!config.azureOpenAIApiInstanceName || !config.azureOpenAIApiDeploymentName || !config.azureOpenAIApiKey) {
|
||||
throw new Error('Please check your Azure OpenAI API configuration')
|
||||
}
|
||||
if (chatgptMode === ChatGPTMode.Poe) {
|
||||
return new PoeWebBot(config.chatgptPoeModelName)
|
||||
return new ChatGPTAzureApiBot({
|
||||
azureOpenAIApiKey: config.azureOpenAIApiKey,
|
||||
azureOpenAIApiDeploymentName: config.azureOpenAIApiDeploymentName,
|
||||
azureOpenAIApiInstanceName: config.azureOpenAIApiInstanceName,
|
||||
})
|
||||
}
|
||||
if (chatgptMode === ChatGPTMode.Poe) {
|
||||
return new PoeWebBot(config.chatgptPoeModelName)
|
||||
}
|
||||
if (chatgptMode === ChatGPTMode.OpenRouter) {
|
||||
if (!config.openrouterApiKey) {
|
||||
throw new ChatError('OpenRouter API key not set', ErrorCode.API_KEY_NOT_SET)
|
||||
}
|
||||
if (chatgptMode === ChatGPTMode.OpenRouter) {
|
||||
if (!config.openrouterApiKey) {
|
||||
throw new ChatError('OpenRouter API key not set', ErrorCode.API_KEY_NOT_SET)
|
||||
}
|
||||
const model = `openai/${config.openrouterOpenAIModel}`
|
||||
return new OpenRouterBot({ apiKey: config.openrouterApiKey, model })
|
||||
}
|
||||
return new ChatGPTWebBot(config.chatgptWebappModelName)
|
||||
const model = `openai/${config.openrouterOpenAIModel}`
|
||||
return new OpenRouterBot({ apiKey: config.openrouterApiKey, model })
|
||||
}
|
||||
return new ChatGPTWebBot(config.chatgptWebappModelName)
|
||||
}
|
||||
|
||||
export class ChatGPTBot extends DelegatedBot {
|
||||
static async initialize() {
|
||||
const bot = await initializeBot()
|
||||
return new ChatGPTBot(bot)
|
||||
}
|
||||
|
||||
async sendMessage(params: MessageParams) {
|
||||
|
@@ -1,35 +1,40 @@
|
||||
import { ClaudeMode, getUserConfig } from '~/services/user-config'
|
||||
import * as agent from '~services/agent'
|
||||
import { AsyncAbstractBot, MessageParams } from '../abstract-bot'
|
||||
import { ChatError, ErrorCode } from '~utils/errors'
|
||||
import { DelegatedBot, MessageParams } from '../abstract-bot'
|
||||
import { ClaudeApiBot } from '../claude-api'
|
||||
import { ClaudeWebBot } from '../claude-web'
|
||||
import { PoeWebBot } from '../poe'
|
||||
import { ChatError, ErrorCode } from '~utils/errors'
|
||||
import { OpenRouterBot } from '../openrouter'
|
||||
import { PoeWebBot } from '../poe'
|
||||
|
||||
export class ClaudeBot extends AsyncAbstractBot {
|
||||
async initializeBot() {
|
||||
const { claudeMode, ...config } = await getUserConfig()
|
||||
if (claudeMode === ClaudeMode.API) {
|
||||
if (!config.claudeApiKey) {
|
||||
throw new Error('Claude API key missing')
|
||||
}
|
||||
return new ClaudeApiBot({
|
||||
claudeApiKey: config.claudeApiKey,
|
||||
claudeApiModel: config.claudeApiModel,
|
||||
})
|
||||
async function initializeBot() {
|
||||
const { claudeMode, ...config } = await getUserConfig()
|
||||
if (claudeMode === ClaudeMode.API) {
|
||||
if (!config.claudeApiKey) {
|
||||
throw new Error('Claude API key missing')
|
||||
}
|
||||
if (claudeMode === ClaudeMode.Webapp) {
|
||||
return new ClaudeWebBot()
|
||||
return new ClaudeApiBot({
|
||||
claudeApiKey: config.claudeApiKey,
|
||||
claudeApiModel: config.claudeApiModel,
|
||||
})
|
||||
}
|
||||
if (claudeMode === ClaudeMode.Webapp) {
|
||||
return new ClaudeWebBot()
|
||||
}
|
||||
if (claudeMode === ClaudeMode.OpenRouter) {
|
||||
if (!config.openrouterApiKey) {
|
||||
throw new ChatError('OpenRouter API key not set', ErrorCode.API_KEY_NOT_SET)
|
||||
}
|
||||
if (claudeMode === ClaudeMode.OpenRouter) {
|
||||
if (!config.openrouterApiKey) {
|
||||
throw new ChatError('OpenRouter API key not set', ErrorCode.API_KEY_NOT_SET)
|
||||
}
|
||||
const model = `anthropic/${config.openrouterClaudeModel}`
|
||||
return new OpenRouterBot({ apiKey: config.openrouterApiKey, model })
|
||||
}
|
||||
return new PoeWebBot(config.poeModel)
|
||||
const model = `anthropic/${config.openrouterClaudeModel}`
|
||||
return new OpenRouterBot({ apiKey: config.openrouterApiKey, model })
|
||||
}
|
||||
return new PoeWebBot(config.poeModel)
|
||||
}
|
||||
|
||||
export class ClaudeBot extends DelegatedBot {
|
||||
static async initialize() {
|
||||
const bot = await initializeBot()
|
||||
return new ClaudeBot(bot)
|
||||
}
|
||||
|
||||
async sendMessage(params: MessageParams) {
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import { GoogleGenerativeAI, ChatSession } from '@google/generative-ai'
|
||||
import { AbstractBot, AsyncAbstractBot, SendMessageParams } from '../abstract-bot'
|
||||
import { ChatSession, GoogleGenerativeAI } from '@google/generative-ai'
|
||||
import { getUserConfig } from '~services/user-config'
|
||||
import { AbstractBot, SendMessageParams } from '../abstract-bot'
|
||||
|
||||
interface ConversationContext {
|
||||
chatSession: ChatSession
|
||||
@@ -47,8 +47,8 @@ export class GeminiApiBot extends AbstractBot {
|
||||
}
|
||||
}
|
||||
|
||||
export class GeminiBot extends AsyncAbstractBot {
|
||||
async initializeBot() {
|
||||
export class GeminiBot {
|
||||
static async initialize() {
|
||||
const { geminiApiKey } = await getUserConfig()
|
||||
if (!geminiApiKey) {
|
||||
throw new Error('Gemini API key missing')
|
||||
|
@@ -31,16 +31,17 @@ export type BotId =
|
||||
| 'grok'
|
||||
| 'gemini'
|
||||
|
||||
export function createBotInstance(botId: BotId) {
|
||||
export async function createBotInstance(botId: BotId) {
|
||||
console.debug('createBotInstance', botId)
|
||||
switch (botId) {
|
||||
case 'chatgpt':
|
||||
return new ChatGPTBot()
|
||||
return ChatGPTBot.initialize()
|
||||
case 'bing':
|
||||
return new BingWebBot()
|
||||
case 'bard':
|
||||
return new BardBot()
|
||||
case 'claude':
|
||||
return new ClaudeBot()
|
||||
return ClaudeBot.initialize()
|
||||
case 'xunfei':
|
||||
return new XunfeiBot()
|
||||
case 'vicuna':
|
||||
@@ -64,12 +65,10 @@ export function createBotInstance(botId: BotId) {
|
||||
case 'baichuan':
|
||||
return new BaichuanWebBot()
|
||||
case 'perplexity':
|
||||
return new PerplexityBot()
|
||||
return PerplexityBot.initialize()
|
||||
case 'grok':
|
||||
return new GrokWebBot()
|
||||
case 'gemini':
|
||||
return new GeminiBot()
|
||||
return GeminiBot.initialize()
|
||||
}
|
||||
}
|
||||
|
||||
export type BotInstance = ReturnType<typeof createBotInstance>
|
||||
|
@@ -1,10 +1,9 @@
|
||||
import { PerplexityMode, getUserConfig } from '~/services/user-config'
|
||||
import { AsyncAbstractBot } from '../abstract-bot'
|
||||
import { PerplexityApiBot } from '../perplexity-api'
|
||||
import { PerplexityLabsBot } from '../perplexity-web'
|
||||
|
||||
export class PerplexityBot extends AsyncAbstractBot {
|
||||
async initializeBot() {
|
||||
export class PerplexityBot {
|
||||
static async initialize() {
|
||||
const { perplexityMode, ...config } = await getUserConfig()
|
||||
if (perplexityMode === PerplexityMode.API) {
|
||||
if (!config.perplexityApiKey) {
|
||||
|
@@ -5,11 +5,12 @@ import clearIcon from '~/assets/icons/clear.svg'
|
||||
import historyIcon from '~/assets/icons/history.svg'
|
||||
import shareIcon from '~/assets/icons/share.svg'
|
||||
import { cx } from '~/utils'
|
||||
import { AbstractBot } from '~app/bots/abstract-bot'
|
||||
import { CHATBOTS } from '~app/consts'
|
||||
import { ConversationContext, ConversationContextValue } from '~app/context'
|
||||
import { trackEvent } from '~app/plausible'
|
||||
import { ChatMessageModel } from '~types'
|
||||
import { BotId, BotInstance } from '../../bots'
|
||||
import { BotId } from '../../bots'
|
||||
import Button from '../Button'
|
||||
import HistoryDialog from '../History/Dialog'
|
||||
import ShareDialog from '../Share/Dialog'
|
||||
@@ -21,7 +22,7 @@ import WebAccessCheckbox from './WebAccessCheckbox'
|
||||
|
||||
interface Props {
|
||||
botId: BotId
|
||||
bot: BotInstance
|
||||
bot: AbstractBot
|
||||
messages: ChatMessageModel[]
|
||||
onUserSendMessage: (input: string, image?: File) => void
|
||||
resetConversation: () => void
|
||||
|
@@ -1,5 +1,6 @@
|
||||
import { useAtom } from 'jotai'
|
||||
import { useCallback, useEffect, useMemo } from 'react'
|
||||
import useSWRImmutable from 'swr/immutable'
|
||||
import { trackEvent } from '~app/plausible'
|
||||
import { chatFamily } from '~app/state'
|
||||
import { compressImageFile } from '~app/utils/image-compression'
|
||||
@@ -7,11 +8,12 @@ import { setConversationMessages } from '~services/chat-history'
|
||||
import { ChatMessageModel } from '~types'
|
||||
import { uuid } from '~utils'
|
||||
import { ChatError } from '~utils/errors'
|
||||
import { BotId } from '../bots'
|
||||
import { BotId, createBotInstance } from '../bots'
|
||||
|
||||
export function useChat(botId: BotId) {
|
||||
const chatAtom = useMemo(() => chatFamily({ botId }), [botId])
|
||||
const [chatState, setChatState] = useAtom(chatAtom)
|
||||
const { data: bot } = useSWRImmutable(['bot', botId], () => createBotInstance(botId), { suspense: true })
|
||||
|
||||
const updateMessage = useCallback(
|
||||
(messageId: string, updater: (message: ChatMessageModel) => void) => {
|
||||
@@ -27,7 +29,7 @@ export function useChat(botId: BotId) {
|
||||
|
||||
const sendMessage = useCallback(
|
||||
async (input: string, image?: File) => {
|
||||
trackEvent('send_message', { botId, withImage: !!image, name: chatState.bot.name })
|
||||
trackEvent('send_message', { botId, withImage: !!image, name: bot.name })
|
||||
|
||||
const botMessageId = uuid()
|
||||
setChatState((draft) => {
|
||||
@@ -48,7 +50,7 @@ export function useChat(botId: BotId) {
|
||||
compressedImage = await compressImageFile(image)
|
||||
}
|
||||
|
||||
const resp = await chatState.bot.sendMessage({
|
||||
const resp = await bot.sendMessage({
|
||||
prompt: input,
|
||||
image: compressedImage,
|
||||
signal: abortController.signal,
|
||||
@@ -80,18 +82,18 @@ export function useChat(botId: BotId) {
|
||||
draft.generatingMessageId = ''
|
||||
})
|
||||
},
|
||||
[botId, chatState.bot, setChatState, updateMessage],
|
||||
[bot, botId, setChatState, updateMessage],
|
||||
)
|
||||
|
||||
const resetConversation = useCallback(() => {
|
||||
chatState.bot.resetConversation()
|
||||
bot.resetConversation()
|
||||
setChatState((draft) => {
|
||||
draft.abortController = undefined
|
||||
draft.generatingMessageId = ''
|
||||
draft.messages = []
|
||||
draft.conversationId = uuid()
|
||||
})
|
||||
}, [chatState.bot, setChatState])
|
||||
}, [bot, setChatState])
|
||||
|
||||
const stopGenerating = useCallback(() => {
|
||||
chatState.abortController?.abort()
|
||||
@@ -116,22 +118,14 @@ export function useChat(botId: BotId) {
|
||||
const chat = useMemo(
|
||||
() => ({
|
||||
botId,
|
||||
bot: chatState.bot,
|
||||
bot,
|
||||
messages: chatState.messages,
|
||||
sendMessage,
|
||||
resetConversation,
|
||||
generating: !!chatState.generatingMessageId,
|
||||
stopGenerating,
|
||||
}),
|
||||
[
|
||||
botId,
|
||||
chatState.bot,
|
||||
chatState.generatingMessageId,
|
||||
chatState.messages,
|
||||
resetConversation,
|
||||
sendMessage,
|
||||
stopGenerating,
|
||||
],
|
||||
[botId, bot, chatState.generatingMessageId, chatState.messages, resetConversation, sendMessage, stopGenerating],
|
||||
)
|
||||
|
||||
return chat
|
||||
|
@@ -18,10 +18,18 @@ import ConversationPanel from '../components/Chat/ConversationPanel'
|
||||
const DEFAULT_BOTS: BotId[] = Object.keys(CHATBOTS).slice(0, 6) as BotId[]
|
||||
|
||||
const layoutAtom = atomWithStorage<Layout>('multiPanelLayout', 2, undefined, { getOnInit: true })
|
||||
const twoPanelBotsAtom = atomWithStorage<BotId[]>('multiPanelBots:2', DEFAULT_BOTS.slice(0, 2))
|
||||
const threePanelBotsAtom = atomWithStorage<BotId[]>('multiPanelBots:3', DEFAULT_BOTS.slice(0, 3))
|
||||
const fourPanelBotsAtom = atomWithStorage<BotId[]>('multiPanelBots:4', DEFAULT_BOTS.slice(0, 4))
|
||||
const sixPanelBotsAtom = atomWithStorage<BotId[]>('multiPanelBots:6', DEFAULT_BOTS.slice(0, 6))
|
||||
const twoPanelBotsAtom = atomWithStorage<BotId[]>('multiPanelBots:2', DEFAULT_BOTS.slice(0, 2), undefined, {
|
||||
getOnInit: true,
|
||||
})
|
||||
const threePanelBotsAtom = atomWithStorage<BotId[]>('multiPanelBots:3', DEFAULT_BOTS.slice(0, 3), undefined, {
|
||||
getOnInit: true,
|
||||
})
|
||||
const fourPanelBotsAtom = atomWithStorage<BotId[]>('multiPanelBots:4', DEFAULT_BOTS.slice(0, 4), undefined, {
|
||||
getOnInit: true,
|
||||
})
|
||||
const sixPanelBotsAtom = atomWithStorage<BotId[]>('multiPanelBots:6', DEFAULT_BOTS.slice(0, 6), undefined, {
|
||||
getOnInit: true,
|
||||
})
|
||||
|
||||
function replaceDeprecatedBots(bots: BotId[]): BotId[] {
|
||||
return bots.map((bot) => {
|
||||
@@ -204,7 +212,7 @@ const MultiBotChatPanel: FC = () => {
|
||||
|
||||
const MultiBotChatPanelPage: FC = () => {
|
||||
return (
|
||||
<Suspense>
|
||||
<Suspense fallback={<div className="bg-primary-background w-full h-full rounded-2xl"></div>}>
|
||||
<MultiBotChatPanel />
|
||||
</Suspense>
|
||||
)
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { FC } from 'react'
|
||||
import { FC, Suspense } from 'react'
|
||||
import { useChat } from '~app/hooks/use-chat'
|
||||
import { BotId } from '../bots'
|
||||
import ConversationPanel from '../components/Chat/ConversationPanel'
|
||||
@@ -24,4 +24,12 @@ const SingleBotChatPanel: FC<Props> = ({ botId }) => {
|
||||
)
|
||||
}
|
||||
|
||||
export default SingleBotChatPanel
|
||||
const SingleBotChatPanelPage: FC<Props> = ({ botId }) => {
|
||||
return (
|
||||
<Suspense fallback={<div className="bg-primary-background w-full h-full rounded-2xl"></div>}>
|
||||
<SingleBotChatPanel botId={botId} />
|
||||
</Suspense>
|
||||
)
|
||||
}
|
||||
|
||||
export default SingleBotChatPanelPage
|
||||
|
@@ -4,7 +4,7 @@ import Layout from './components/Layout'
|
||||
import MultiBotChatPanel from './pages/MultiBotChatPanel'
|
||||
import PremiumPage from './pages/PremiumPage'
|
||||
import SettingPage from './pages/SettingPage'
|
||||
import SingleBotChatPanel from './pages/SingleBotChatPanel'
|
||||
import SingleBotChatPanelPage from './pages/SingleBotChatPanel'
|
||||
|
||||
const rootRoute = new RootRoute()
|
||||
|
||||
@@ -22,7 +22,7 @@ const indexRoute = new Route({
|
||||
|
||||
function ChatRoute() {
|
||||
const { botId } = useParams({ from: chatRoute.id })
|
||||
return <SingleBotChatPanel botId={botId as BotId} />
|
||||
return <SingleBotChatPanelPage botId={botId as BotId} />
|
||||
}
|
||||
|
||||
const chatRoute = new Route({
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import { atom } from 'jotai'
|
||||
import { atomWithImmer } from 'jotai-immer'
|
||||
import { atomFamily, atomWithStorage } from 'jotai/utils'
|
||||
import { BotId, createBotInstance } from '~app/bots'
|
||||
import { BotId } from '~app/bots'
|
||||
import { FeatureId } from '~app/components/Premium/FeatureList'
|
||||
import { getDefaultThemeColor } from '~app/utils/color-scheme'
|
||||
import { Campaign } from '~services/server-api'
|
||||
@@ -14,7 +14,6 @@ export const chatFamily = atomFamily(
|
||||
(param: Param) => {
|
||||
return atomWithImmer({
|
||||
botId: param.botId,
|
||||
bot: createBotInstance(param.botId),
|
||||
messages: [] as ChatMessageModel[],
|
||||
generatingMessageId: '',
|
||||
abortController: undefined as AbortController | undefined,
|
||||
|
Reference in New Issue
Block a user