feat: add streaming output options #93

This commit is contained in:
kangfenmao 2024-09-25 11:23:45 +08:00
parent bf2005676d
commit dd60dfa872
10 changed files with 85 additions and 30 deletions

View File

@ -41,7 +41,7 @@
--color-hover: rgba(40, 40, 40, 1); --color-hover: rgba(40, 40, 40, 1);
--color-active: rgba(55, 55, 55, 1); --color-active: rgba(55, 55, 55, 1);
--navbar-background-mac: rgba(30, 30, 30, 0.4); --navbar-background-mac: rgba(30, 30, 30, 0.6);
--navbar-background: rgba(30, 30, 30); --navbar-background: rgba(30, 30, 30);
--navbar-height: 40px; --navbar-height: 40px;
@ -92,7 +92,7 @@ body[theme-mode='light'] {
--color-hover: var(--color-white-mute); --color-hover: var(--color-white-mute);
--color-active: var(--color-white-soft); --color-active: var(--color-white-soft);
--navbar-background-mac: rgba(255, 255, 255, 0.4); --navbar-background-mac: rgba(255, 255, 255, 0.6);
--navbar-background: rgba(255, 255, 255); --navbar-background: rgba(255, 255, 255);
} }

View File

@ -116,6 +116,9 @@ const resources = {
abbr: 'Assistant', abbr: 'Assistant',
search: 'Search assistants...' search: 'Search assistants...'
}, },
model: {
stream_output: 'Stream Output'
},
files: { files: {
title: 'Files', title: 'Files',
file: 'File', file: 'File',
@ -398,6 +401,9 @@ const resources = {
abbr: '助手', abbr: '助手',
search: '搜索助手' search: '搜索助手'
}, },
model: {
stream_output: '流式输出'
},
files: { files: {
title: '文件', title: '文件',
file: '文件', file: '文件',

View File

@ -27,10 +27,11 @@ interface Props {
message: Message message: Message
index?: number index?: number
total?: number total?: number
lastMessage?: boolean
onDeleteMessage?: (message: Message) => void onDeleteMessage?: (message: Message) => void
} }
const MessageItem: FC<Props> = ({ message, index, onDeleteMessage }) => { const MessageItem: FC<Props> = ({ message, index, lastMessage, onDeleteMessage }) => {
const avatar = useAvatar() const avatar = useAvatar()
const { t } = useTranslation() const { t } = useTranslation()
const { assistant, setModel } = useAssistant(message.assistantId) const { assistant, setModel } = useAssistant(message.assistantId)
@ -38,7 +39,7 @@ const MessageItem: FC<Props> = ({ message, index, onDeleteMessage }) => {
const { userName, showMessageDivider, messageFont, fontSize } = useSettings() const { userName, showMessageDivider, messageFont, fontSize } = useSettings()
const { theme } = useTheme() const { theme } = useTheme()
const isLastMessage = index === 0 const isLastMessage = lastMessage || index === 0
const isAssistantMessage = message.role === 'assistant' const isAssistantMessage = message.role === 'assistant'
const getUserName = useCallback(() => { const getUserName = useCallback(() => {
@ -106,18 +107,20 @@ const MessageItem: FC<Props> = ({ message, index, onDeleteMessage }) => {
</MessageHeader> </MessageHeader>
<MessageContentContainer style={{ fontFamily, fontSize }}> <MessageContentContainer style={{ fontFamily, fontSize }}>
<MessageContent message={message} /> <MessageContent message={message} />
<MessageFooter style={{ border: messageBorder, flexDirection: isLastMessage ? 'row-reverse' : undefined }}> {!lastMessage && (
<MessgeTokens message={message} /> <MessageFooter style={{ border: messageBorder, flexDirection: isLastMessage ? 'row-reverse' : undefined }}>
<MessageMenubar <MessgeTokens message={message} />
message={message} <MessageMenubar
model={model} message={message}
index={index} model={model}
isLastMessage={isLastMessage} index={index}
isAssistantMessage={isAssistantMessage} isLastMessage={isLastMessage}
setModel={setModel} isAssistantMessage={isAssistantMessage}
onDeleteMessage={onDeleteMessage} setModel={setModel}
/> onDeleteMessage={onDeleteMessage}
</MessageFooter> />
</MessageFooter>
)}
</MessageContentContainer> </MessageContentContainer>
</MessageContainer> </MessageContainer>
) )

View File

@ -189,7 +189,7 @@ const Messages: FC<Props> = ({ assistant, topic, setActiveTopic }) => {
return ( return (
<Container id="messages" key={assistant.id} ref={containerRef}> <Container id="messages" key={assistant.id} ref={containerRef}>
<Suggestions assistant={assistant} messages={messages} lastMessage={lastMessage} /> <Suggestions assistant={assistant} messages={messages} lastMessage={lastMessage} />
{lastMessage && <MessageItem key={lastMessage.id} message={lastMessage} />} {lastMessage && <MessageItem key={lastMessage.id} message={lastMessage} lastMessage />}
{reverse([...messages]).map((message, index) => ( {reverse([...messages]).map((message, index) => (
<MessageItem key={message.id} message={message} index={index} onDeleteMessage={onDeleteMessage} /> <MessageItem key={message.id} message={message} index={index} onDeleteMessage={onDeleteMessage} />
))} ))}

View File

@ -29,6 +29,7 @@ const SettingsTab: FC<Props> = (props) => {
const [contextCount, setConextCount] = useState(assistant?.settings?.contextCount ?? DEFAULT_CONEXTCOUNT) const [contextCount, setConextCount] = useState(assistant?.settings?.contextCount ?? DEFAULT_CONEXTCOUNT)
const [enableMaxTokens, setEnableMaxTokens] = useState(assistant?.settings?.enableMaxTokens ?? false) const [enableMaxTokens, setEnableMaxTokens] = useState(assistant?.settings?.enableMaxTokens ?? false)
const [maxTokens, setMaxTokens] = useState(assistant?.settings?.maxTokens ?? 0) const [maxTokens, setMaxTokens] = useState(assistant?.settings?.maxTokens ?? 0)
const [streamOutput, setStreamOutput] = useState(assistant?.settings?.streamOutput ?? true)
const [fontSizeValue, setFontSizeValue] = useState(fontSize) const [fontSizeValue, setFontSizeValue] = useState(fontSize)
const { t } = useTranslation() const { t } = useTranslation()
@ -48,7 +49,8 @@ const SettingsTab: FC<Props> = (props) => {
temperature: settings.temperature ?? temperature, temperature: settings.temperature ?? temperature,
contextCount: settings.contextCount ?? contextCount, contextCount: settings.contextCount ?? contextCount,
enableMaxTokens: settings.enableMaxTokens ?? enableMaxTokens, enableMaxTokens: settings.enableMaxTokens ?? enableMaxTokens,
maxTokens: settings.maxTokens ?? maxTokens maxTokens: settings.maxTokens ?? maxTokens,
streamOutput: settings.streamOutput ?? streamOutput
}) })
} }
@ -80,7 +82,8 @@ const SettingsTab: FC<Props> = (props) => {
temperature: DEFAULT_TEMPERATURE, temperature: DEFAULT_TEMPERATURE,
contextCount: DEFAULT_CONEXTCOUNT, contextCount: DEFAULT_CONEXTCOUNT,
enableMaxTokens: false, enableMaxTokens: false,
maxTokens: DEFAULT_MAX_TOKENS maxTokens: DEFAULT_MAX_TOKENS,
streamOutput: true
} }
}) })
} }
@ -90,6 +93,7 @@ const SettingsTab: FC<Props> = (props) => {
setConextCount(assistant?.settings?.contextCount ?? DEFAULT_CONEXTCOUNT) setConextCount(assistant?.settings?.contextCount ?? DEFAULT_CONEXTCOUNT)
setEnableMaxTokens(assistant?.settings?.enableMaxTokens ?? false) setEnableMaxTokens(assistant?.settings?.enableMaxTokens ?? false)
setMaxTokens(assistant?.settings?.maxTokens ?? DEFAULT_MAX_TOKENS) setMaxTokens(assistant?.settings?.maxTokens ?? DEFAULT_MAX_TOKENS)
setStreamOutput(assistant?.settings?.streamOutput ?? true)
}, [assistant]) }, [assistant])
return ( return (
@ -137,6 +141,18 @@ const SettingsTab: FC<Props> = (props) => {
/> />
</Col> </Col>
</Row> </Row>
<SettingRow>
<SettingRowTitleSmall>{t('model.stream_output')}</SettingRowTitleSmall>
<Switch
size="small"
checked={streamOutput}
onChange={(checked) => {
setStreamOutput(checked)
onUpdateAssistantSettings({ streamOutput: checked })
}}
/>
</SettingRow>
<SettingDivider />
<Row align="middle" justify="space-between"> <Row align="middle" justify="space-between">
<HStack alignItems="center"> <HStack alignItems="center">
<Label>{t('chat.settings.max_tokens')}</Label> <Label>{t('chat.settings.max_tokens')}</Label>

View File

@ -55,7 +55,7 @@ export default class AnthropicProvider extends BaseProvider {
public async completions({ messages, assistant, onChunk, onFilterMessages }: CompletionsParams) { public async completions({ messages, assistant, onChunk, onFilterMessages }: CompletionsParams) {
const defaultModel = getDefaultModel() const defaultModel = getDefaultModel()
const model = assistant.model || defaultModel const model = assistant.model || defaultModel
const { contextCount, maxTokens } = getAssistantSettings(assistant) const { contextCount, maxTokens, streamOutput } = getAssistantSettings(assistant)
const userMessagesParams: MessageParam[] = [] const userMessagesParams: MessageParam[] = []
const _messages = filterMessages(filterContextMessages(takeRight(messages, contextCount + 2))) const _messages = filterMessages(filterContextMessages(takeRight(messages, contextCount + 2)))
@ -72,6 +72,21 @@ export default class AnthropicProvider extends BaseProvider {
userMessages.shift() userMessages.shift()
} }
if (!streamOutput) {
const message = await this.sdk.messages.create({
model: model.id,
messages: userMessages,
max_tokens: maxTokens || DEFAULT_MAX_TOKENS,
temperature: assistant?.settings?.temperature,
system: assistant.prompt,
stream: false
})
return onChunk({
text: message.content[0].type === 'text' ? message.content[0].text : '',
usage: message.usage
})
}
return new Promise<void>((resolve, reject) => { return new Promise<void>((resolve, reject) => {
const stream = this.sdk.messages const stream = this.sdk.messages
.stream({ .stream({

View File

@ -57,7 +57,7 @@ export default class GeminiProvider extends BaseProvider {
public async completions({ messages, assistant, onChunk, onFilterMessages }: CompletionsParams) { public async completions({ messages, assistant, onChunk, onFilterMessages }: CompletionsParams) {
const defaultModel = getDefaultModel() const defaultModel = getDefaultModel()
const model = assistant.model || defaultModel const model = assistant.model || defaultModel
const { contextCount, maxTokens } = getAssistantSettings(assistant) const { contextCount, maxTokens, streamOutput } = getAssistantSettings(assistant)
const userMessages = filterMessages(filterContextMessages(takeRight(messages, contextCount + 1))) const userMessages = filterMessages(filterContextMessages(takeRight(messages, contextCount + 1)))
onFilterMessages(userMessages) onFilterMessages(userMessages)
@ -78,19 +78,32 @@ export default class GeminiProvider extends BaseProvider {
temperature: assistant?.settings?.temperature temperature: assistant?.settings?.temperature
}, },
safetySettings: [ safetySettings: [
{ category: HarmCategory.HARM_CATEGORY_HATE_SPEECH, threshold: HarmBlockThreshold.BLOCK_ONLY_HIGH }, { category: HarmCategory.HARM_CATEGORY_HATE_SPEECH, threshold: HarmBlockThreshold.BLOCK_NONE },
{ {
category: HarmCategory.HARM_CATEGORY_SEXUALLY_EXPLICIT, category: HarmCategory.HARM_CATEGORY_SEXUALLY_EXPLICIT,
threshold: HarmBlockThreshold.BLOCK_ONLY_HIGH threshold: HarmBlockThreshold.BLOCK_NONE
}, },
{ category: HarmCategory.HARM_CATEGORY_HARASSMENT, threshold: HarmBlockThreshold.BLOCK_ONLY_HIGH }, { category: HarmCategory.HARM_CATEGORY_HARASSMENT, threshold: HarmBlockThreshold.BLOCK_NONE },
{ category: HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT, threshold: HarmBlockThreshold.BLOCK_ONLY_HIGH } { category: HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT, threshold: HarmBlockThreshold.BLOCK_NONE }
] ]
}) })
const chat = geminiModel.startChat({ history }) const chat = geminiModel.startChat({ history })
const messageContents = await this.getMessageContents(userLastMessage!) const messageContents = await this.getMessageContents(userLastMessage!)
if (!streamOutput) {
const { response } = await chat.sendMessage(messageContents.parts)
onChunk({
text: response.candidates?.[0].content.parts[0].text,
usage: {
prompt_tokens: response.usageMetadata?.promptTokenCount || 0,
completion_tokens: response.usageMetadata?.candidatesTokenCount || 0,
total_tokens: response.usageMetadata?.totalTokenCount || 0
}
})
return
}
const userMessagesStream = await chat.sendMessageStream(messageContents.parts) const userMessagesStream = await chat.sendMessageStream(messageContents.parts)
for await (const chunk of userMessagesStream.stream) { for await (const chunk of userMessagesStream.stream) {

View File

@ -28,7 +28,7 @@ export default class OpenAIProvider extends BaseProvider {
} }
private isSupportStreamOutput(modelId: string): boolean { private isSupportStreamOutput(modelId: string): boolean {
if (this.provider.id === 'openai' && modelId.includes('o1-')) { if (modelId.includes('o1-')) {
return false return false
} }
return true return true
@ -112,7 +112,7 @@ export default class OpenAIProvider extends BaseProvider {
async completions({ messages, assistant, onChunk, onFilterMessages }: CompletionsParams): Promise<void> { async completions({ messages, assistant, onChunk, onFilterMessages }: CompletionsParams): Promise<void> {
const defaultModel = getDefaultModel() const defaultModel = getDefaultModel()
const model = assistant.model || defaultModel const model = assistant.model || defaultModel
const { contextCount, maxTokens } = getAssistantSettings(assistant) const { contextCount, maxTokens, streamOutput } = getAssistantSettings(assistant)
const systemMessage = assistant.prompt ? { role: 'system', content: assistant.prompt } : undefined const systemMessage = assistant.prompt ? { role: 'system', content: assistant.prompt } : undefined
const userMessages: ChatCompletionMessageParam[] = [] const userMessages: ChatCompletionMessageParam[] = []
@ -124,7 +124,7 @@ export default class OpenAIProvider extends BaseProvider {
userMessages.push(await this.getMessageParam(message, model)) userMessages.push(await this.getMessageParam(message, model))
} }
const isSupportStreamOutput = this.isSupportStreamOutput(model.id) const isSupportStreamOutput = streamOutput && this.isSupportStreamOutput(model.id)
// @ts-ignore key is not typed // @ts-ignore key is not typed
const stream = await this.sdk.chat.completions.create({ const stream = await this.sdk.chat.completions.create({

View File

@ -80,7 +80,8 @@ export const getAssistantSettings = (assistant: Assistant): AssistantSettings =>
contextCount: contextCount === 20 ? 100000 : contextCount, contextCount: contextCount === 20 ? 100000 : contextCount,
temperature: assistant?.settings?.temperature ?? DEFAULT_TEMPERATURE, temperature: assistant?.settings?.temperature ?? DEFAULT_TEMPERATURE,
enableMaxTokens: assistant?.settings?.enableMaxTokens ?? false, enableMaxTokens: assistant?.settings?.enableMaxTokens ?? false,
maxTokens: getAssistantMaxTokens() maxTokens: getAssistantMaxTokens(),
streamOutput: assistant?.settings?.streamOutput ?? true
} }
} }

View File

@ -16,6 +16,7 @@ export type AssistantSettings = {
temperature: number temperature: number
maxTokens: number | undefined maxTokens: number | undefined
enableMaxTokens: boolean enableMaxTokens: boolean
streamOutput: boolean
} }
export type Message = { export type Message = {