From 5edb53ef7deed2b9af8760ce4892c44ea7fd5488 Mon Sep 17 00:00:00 2001 From: kangfenmao Date: Tue, 6 Aug 2024 20:38:01 +0800 Subject: [PATCH] feat: add ollama settings --- src/renderer/src/components/TopView/index.tsx | 3 ++ src/renderer/src/hooks/useOllama.ts | 18 ++++++++++ src/renderer/src/hooks/useTopic.ts | 6 ++-- src/renderer/src/i18n/index.ts | 12 +++++++ .../home/components/SelectModelDropdown.tsx | 2 +- .../pages/home/components/input/Inputbar.tsx | 2 +- .../settings/components/ProviderSetting.tsx | 6 ++-- .../settings/providers/OllamaSettings.tsx | 35 +++++++++++++++++++ src/renderer/src/services/ProviderSDK.ts | 17 +++++++-- src/renderer/src/store/index.ts | 2 +- src/renderer/src/store/llm.ts | 20 +++++++++-- src/renderer/src/store/migrate.ts | 10 +++++- 12 files changed, 118 insertions(+), 15 deletions(-) create mode 100644 src/renderer/src/hooks/useOllama.ts create mode 100644 src/renderer/src/pages/settings/providers/OllamaSettings.tsx diff --git a/src/renderer/src/components/TopView/index.tsx b/src/renderer/src/components/TopView/index.tsx index ec5a1381d..6d2755e19 100644 --- a/src/renderer/src/components/TopView/index.tsx +++ b/src/renderer/src/components/TopView/index.tsx @@ -1,3 +1,4 @@ +import { useAppInit } from '@renderer/hooks/useAppInit' import { message, Modal } from 'antd' import React, { PropsWithChildren, useCallback, useEffect, useRef, useState } from 'react' @@ -30,6 +31,8 @@ const TopViewContainer: React.FC = ({ children }) => { const [messageApi, messageContextHolder] = message.useMessage() const [modal, modalContextHolder] = Modal.useModal() + useAppInit() + useEffect(() => { window.message = messageApi window.modal = modal diff --git a/src/renderer/src/hooks/useOllama.ts b/src/renderer/src/hooks/useOllama.ts new file mode 100644 index 000000000..379b05815 --- /dev/null +++ b/src/renderer/src/hooks/useOllama.ts @@ -0,0 +1,18 @@ +import store, { useAppSelector } from '@renderer/store' +import { setOllamaKeepAliveTime } from '@renderer/store/llm' +import { useDispatch } from 'react-redux' + +export function useOllamaSettings() { + const settings = useAppSelector((state) => state.llm.settings.ollama) + const dispatch = useDispatch() + + return { ...settings, setKeepAliveTime: (time: number) => dispatch(setOllamaKeepAliveTime(time)) } +} + +export function getOllamaSettings() { + return store.getState().llm.settings.ollama +} + +export function getOllamaKeepAliveTime() { + return store.getState().llm.settings.ollama.keepAliveTime + 'm' +} diff --git a/src/renderer/src/hooks/useTopic.ts b/src/renderer/src/hooks/useTopic.ts index cd4e49244..3bf2fa24d 100644 --- a/src/renderer/src/hooks/useTopic.ts +++ b/src/renderer/src/hooks/useTopic.ts @@ -2,12 +2,10 @@ import { Assistant, Topic } from '@renderer/types' import { find } from 'lodash' import { useEffect, useState } from 'react' -const activeTopicsMap = new Map() +let _activeTopic: Topic export function useActiveTopic(assistant: Assistant) { - const [activeTopic, setActiveTopic] = useState(activeTopicsMap.get(assistant.id) || assistant?.topics[0]) - - activeTopicsMap.set(assistant.id, activeTopic) + const [activeTopic, setActiveTopic] = useState(_activeTopic || assistant?.topics[0]) useEffect(() => { // activeTopic not in assistant.topics diff --git a/src/renderer/src/i18n/index.ts b/src/renderer/src/i18n/index.ts index 87431b38b..28b306f71 100644 --- a/src/renderer/src/i18n/index.ts +++ b/src/renderer/src/i18n/index.ts @@ -196,6 +196,12 @@ const resources = { italian: 'Italian', portuguese: 'Portuguese', arabic: 'Arabic' + }, + ollama: { + title: 'Ollama', + 'keep_alive_time.title': 'Keep Alive Time', + 'keep_alive_time.placeholder': 'Minutes', + 'keep_alive_time.description': 'The time in minutes to keep the connection alive, default is 5 minutes.' } } }, @@ -393,6 +399,12 @@ const resources = { italian: '意大利文', portuguese: '葡萄牙文', arabic: '阿拉伯文' + }, + ollama: { + title: 'Ollama', + 'keep_alive_time.title': '保持活跃时间', + 'keep_alive_time.placeholder': '分钟', + 'keep_alive_time.description': '对话后模型在内存中保持的时间(默认:5分钟)' } } } diff --git a/src/renderer/src/pages/home/components/SelectModelDropdown.tsx b/src/renderer/src/pages/home/components/SelectModelDropdown.tsx index 00043ec67..5621af976 100644 --- a/src/renderer/src/pages/home/components/SelectModelDropdown.tsx +++ b/src/renderer/src/pages/home/components/SelectModelDropdown.tsx @@ -40,7 +40,7 @@ const SelectModelDropdown: FC = ({ children, model, o menu={{ items, style: { maxHeight: '80vh', overflow: 'auto' } }} trigger={['click']} arrow - placement="bottomCenter" + placement="bottom" overlayClassName="chat-nav-dropdown" {...props}> {children} diff --git a/src/renderer/src/pages/home/components/input/Inputbar.tsx b/src/renderer/src/pages/home/components/input/Inputbar.tsx index b91a6909e..b57664d6c 100644 --- a/src/renderer/src/pages/home/components/input/Inputbar.tsx +++ b/src/renderer/src/pages/home/components/input/Inputbar.tsx @@ -187,7 +187,7 @@ const Inputbar: FC = ({ assistant, setActiveTopic }) => { {assistant?.settings?.contextCount ?? DEFAULT_CONEXTCOUNT} - ↑{`${inputTokenCount} / ${estimateTokenCount}`} + ↑ {`${inputTokenCount} / ${estimateTokenCount}`} )} diff --git a/src/renderer/src/pages/settings/components/ProviderSetting.tsx b/src/renderer/src/pages/settings/components/ProviderSetting.tsx index baa87d92f..2f2b5429d 100644 --- a/src/renderer/src/pages/settings/components/ProviderSetting.tsx +++ b/src/renderer/src/pages/settings/components/ProviderSetting.tsx @@ -19,6 +19,7 @@ import { FC, useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' import styled from 'styled-components' +import OllamSettings from '../providers/OllamaSettings' import { SettingContainer, SettingSubtitle, SettingTitle } from '.' import AddModelPopup from './AddModelPopup' import EditModelsPopup from './EditModelsPopup' @@ -126,6 +127,7 @@ const ProviderSetting: FC = ({ provider: _provider }) => { /> {apiEditable && } + {provider.id === 'ollama' && } {t('common.models')} {Object.keys(modelGroups).map((group) => ( @@ -182,14 +184,14 @@ const ModelListHeader = styled.div` align-items: center; ` -const HelpTextRow = styled.div` +export const HelpTextRow = styled.div` display: flex; flex-direction: row; align-items: center; padding: 5px 0; ` -const HelpText = styled.div` +export const HelpText = styled.div` font-size: 11px; color: var(--color-text); opacity: 0.4; diff --git a/src/renderer/src/pages/settings/providers/OllamaSettings.tsx b/src/renderer/src/pages/settings/providers/OllamaSettings.tsx new file mode 100644 index 000000000..3c3a9b4a8 --- /dev/null +++ b/src/renderer/src/pages/settings/providers/OllamaSettings.tsx @@ -0,0 +1,35 @@ +import { useOllamaSettings } from '@renderer/hooks/useOllama' +import { InputNumber } from 'antd' +import { FC, useState } from 'react' +import { useTranslation } from 'react-i18next' +import styled from 'styled-components' + +import { SettingSubtitle } from '../components' +import { HelpText, HelpTextRow } from '../components/ProviderSetting' + +const OllamSettings: FC = () => { + const { keepAliveTime, setKeepAliveTime } = useOllamaSettings() + const [keepAliveMinutes, setKeepAliveMinutes] = useState(keepAliveTime) + const { t } = useTranslation() + + return ( + + {t('ollama.keep_alive_time.title')} + setKeepAliveMinutes(Number(e))} + onBlur={() => setKeepAliveTime(keepAliveMinutes)} + suffix={t('ollama.keep_alive_time.placeholder')} + step={5} + /> + + {t('ollama.keep_alive_time.description')} + + + ) +} + +const Container = styled.div`` + +export default OllamSettings diff --git a/src/renderer/src/services/ProviderSDK.ts b/src/renderer/src/services/ProviderSDK.ts index b12419da5..4d317f995 100644 --- a/src/renderer/src/services/ProviderSDK.ts +++ b/src/renderer/src/services/ProviderSDK.ts @@ -1,5 +1,6 @@ import Anthropic from '@anthropic-ai/sdk' import { MessageCreateParamsNonStreaming, MessageParam } from '@anthropic-ai/sdk/resources' +import { getOllamaKeepAliveTime } from '@renderer/hooks/useOllama' import { Assistant, Message, Provider, Suggestion } from '@renderer/types' import { getAssistantSettings, removeQuotes } from '@renderer/utils' import { sum, takeRight } from 'lodash' @@ -26,6 +27,10 @@ export default class ProviderSDK { return this.provider.id === 'anthropic' } + private get keepAliveTime() { + return this.provider.id === 'ollama' ? getOllamaKeepAliveTime() : undefined + } + public async completions( messages: Message[], assistant: Assistant, @@ -61,11 +66,13 @@ export default class ProviderSDK { }) ) } else { + // @ts-ignore key is not typed const stream = await this.openaiSdk.chat.completions.create({ model: model.id, messages: [systemMessage, ...userMessages].filter(Boolean) as ChatCompletionMessageParam[], stream: true, - temperature: assistant?.settings?.temperature + temperature: assistant?.settings?.temperature, + keep_alive: this.keepAliveTime }) for await (const chunk of stream) { if (window.keyv.get(EVENT_NAMES.CHAT_COMPLETION_PAUSED)) break @@ -92,10 +99,12 @@ export default class ProviderSDK { }) return response.content[0].type === 'text' ? response.content[0].text : '' } else { + // @ts-ignore key is not typed const response = await this.openaiSdk.chat.completions.create({ model: model.id, messages: messages as ChatCompletionMessageParam[], - stream: false + stream: false, + keep_alive: this.keepAliveTime }) return response.choices[0].message?.content || '' } @@ -124,11 +133,13 @@ export default class ProviderSDK { return message.content[0].type === 'text' ? message.content[0].text : null } else { + // @ts-ignore key is not typed const response = await this.openaiSdk.chat.completions.create({ model: model.id, messages: [systemMessage, ...userMessages] as ChatCompletionMessageParam[], stream: false, - max_tokens: 50 + max_tokens: 50, + keep_alive: this.keepAliveTime }) return removeQuotes(response.choices[0].message?.content || '') diff --git a/src/renderer/src/store/index.ts b/src/renderer/src/store/index.ts index 9fd276998..535297277 100644 --- a/src/renderer/src/store/index.ts +++ b/src/renderer/src/store/index.ts @@ -22,7 +22,7 @@ const persistedReducer = persistReducer( { key: 'cherry-studio', storage, - version: 18, + version: 19, blacklist: ['runtime'], migrate }, diff --git a/src/renderer/src/store/llm.ts b/src/renderer/src/store/llm.ts index fdfd1effe..b2a848b2d 100644 --- a/src/renderer/src/store/llm.ts +++ b/src/renderer/src/store/llm.ts @@ -3,11 +3,18 @@ import { SYSTEM_MODELS } from '@renderer/config/models' import { Model, Provider } from '@renderer/types' import { uniqBy } from 'lodash' +type LlmSettings = { + ollama: { + keepAliveTime: number + } +} + export interface LlmState { providers: Provider[] defaultModel: Model topicNamingModel: Model translateModel: Model + settings: LlmSettings } const initialState: LlmState = { @@ -132,7 +139,12 @@ const initialState: LlmState = { isSystem: true, enabled: false } - ] + ], + settings: { + ollama: { + keepAliveTime: 0 + } + } } const settingsSlice = createSlice({ @@ -179,6 +191,9 @@ const settingsSlice = createSlice({ }, setTranslateModel: (state, action: PayloadAction<{ model: Model }>) => { state.translateModel = action.payload.model + }, + setOllamaKeepAliveTime: (state, action: PayloadAction) => { + state.settings.ollama.keepAliveTime = action.payload } } }) @@ -192,7 +207,8 @@ export const { removeModel, setDefaultModel, setTopicNamingModel, - setTranslateModel + setTranslateModel, + setOllamaKeepAliveTime } = settingsSlice.actions export default settingsSlice.reducer diff --git a/src/renderer/src/store/migrate.ts b/src/renderer/src/store/migrate.ts index 9ecf60e15..0d90d1e25 100644 --- a/src/renderer/src/store/migrate.ts +++ b/src/renderer/src/store/migrate.ts @@ -272,11 +272,19 @@ const migrateConfig = { } } }, - '18': (state: RootState) => { + '19': (state: RootState) => { return { ...state, agents: { agents: [] + }, + llm: { + ...state.llm, + settings: { + ollama: { + keepAliveTime: 5 + } + } } } }