feat: add ollama settings
This commit is contained in:
parent
167988927b
commit
5edb53ef7d
@ -1,3 +1,4 @@
|
|||||||
|
import { useAppInit } from '@renderer/hooks/useAppInit'
|
||||||
import { message, Modal } from 'antd'
|
import { message, Modal } from 'antd'
|
||||||
import React, { PropsWithChildren, useCallback, useEffect, useRef, useState } from 'react'
|
import React, { PropsWithChildren, useCallback, useEffect, useRef, useState } from 'react'
|
||||||
|
|
||||||
@ -30,6 +31,8 @@ const TopViewContainer: React.FC<Props> = ({ children }) => {
|
|||||||
const [messageApi, messageContextHolder] = message.useMessage()
|
const [messageApi, messageContextHolder] = message.useMessage()
|
||||||
const [modal, modalContextHolder] = Modal.useModal()
|
const [modal, modalContextHolder] = Modal.useModal()
|
||||||
|
|
||||||
|
useAppInit()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
window.message = messageApi
|
window.message = messageApi
|
||||||
window.modal = modal
|
window.modal = modal
|
||||||
|
|||||||
18
src/renderer/src/hooks/useOllama.ts
Normal file
18
src/renderer/src/hooks/useOllama.ts
Normal file
@ -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'
|
||||||
|
}
|
||||||
@ -2,12 +2,10 @@ import { Assistant, Topic } from '@renderer/types'
|
|||||||
import { find } from 'lodash'
|
import { find } from 'lodash'
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
|
|
||||||
const activeTopicsMap = new Map<string, Topic>()
|
let _activeTopic: Topic
|
||||||
|
|
||||||
export function useActiveTopic(assistant: Assistant) {
|
export function useActiveTopic(assistant: Assistant) {
|
||||||
const [activeTopic, setActiveTopic] = useState(activeTopicsMap.get(assistant.id) || assistant?.topics[0])
|
const [activeTopic, setActiveTopic] = useState(_activeTopic || assistant?.topics[0])
|
||||||
|
|
||||||
activeTopicsMap.set(assistant.id, activeTopic)
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// activeTopic not in assistant.topics
|
// activeTopic not in assistant.topics
|
||||||
|
|||||||
@ -196,6 +196,12 @@ const resources = {
|
|||||||
italian: 'Italian',
|
italian: 'Italian',
|
||||||
portuguese: 'Portuguese',
|
portuguese: 'Portuguese',
|
||||||
arabic: 'Arabic'
|
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: '意大利文',
|
italian: '意大利文',
|
||||||
portuguese: '葡萄牙文',
|
portuguese: '葡萄牙文',
|
||||||
arabic: '阿拉伯文'
|
arabic: '阿拉伯文'
|
||||||
|
},
|
||||||
|
ollama: {
|
||||||
|
title: 'Ollama',
|
||||||
|
'keep_alive_time.title': '保持活跃时间',
|
||||||
|
'keep_alive_time.placeholder': '分钟',
|
||||||
|
'keep_alive_time.description': '对话后模型在内存中保持的时间(默认:5分钟)'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -40,7 +40,7 @@ const SelectModelDropdown: FC<Props & PropsWithChildren> = ({ children, model, o
|
|||||||
menu={{ items, style: { maxHeight: '80vh', overflow: 'auto' } }}
|
menu={{ items, style: { maxHeight: '80vh', overflow: 'auto' } }}
|
||||||
trigger={['click']}
|
trigger={['click']}
|
||||||
arrow
|
arrow
|
||||||
placement="bottomCenter"
|
placement="bottom"
|
||||||
overlayClassName="chat-nav-dropdown"
|
overlayClassName="chat-nav-dropdown"
|
||||||
{...props}>
|
{...props}>
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
@ -187,7 +187,7 @@ const Inputbar: FC<Props> = ({ assistant, setActiveTopic }) => {
|
|||||||
<Tag style={{ cursor: 'pointer' }}>{assistant?.settings?.contextCount ?? DEFAULT_CONEXTCOUNT}</Tag>
|
<Tag style={{ cursor: 'pointer' }}>{assistant?.settings?.contextCount ?? DEFAULT_CONEXTCOUNT}</Tag>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip title={t('chat.input.estimated_tokens.tip')}>
|
<Tooltip title={t('chat.input.estimated_tokens.tip')}>
|
||||||
<Tag style={{ cursor: 'pointer' }}>↑{`${inputTokenCount} / ${estimateTokenCount}`}</Tag>
|
<Tag style={{ cursor: 'pointer' }}>↑ {`${inputTokenCount} / ${estimateTokenCount}`}</Tag>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</TextCount>
|
</TextCount>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -19,6 +19,7 @@ import { FC, useEffect, useState } from 'react'
|
|||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
|
|
||||||
|
import OllamSettings from '../providers/OllamaSettings'
|
||||||
import { SettingContainer, SettingSubtitle, SettingTitle } from '.'
|
import { SettingContainer, SettingSubtitle, SettingTitle } from '.'
|
||||||
import AddModelPopup from './AddModelPopup'
|
import AddModelPopup from './AddModelPopup'
|
||||||
import EditModelsPopup from './EditModelsPopup'
|
import EditModelsPopup from './EditModelsPopup'
|
||||||
@ -126,6 +127,7 @@ const ProviderSetting: FC<Props> = ({ provider: _provider }) => {
|
|||||||
/>
|
/>
|
||||||
{apiEditable && <Button onClick={onReset}>{t('settings.provider.api.url.reset')}</Button>}
|
{apiEditable && <Button onClick={onReset}>{t('settings.provider.api.url.reset')}</Button>}
|
||||||
</Space.Compact>
|
</Space.Compact>
|
||||||
|
{provider.id === 'ollama' && <OllamSettings />}
|
||||||
<SettingSubtitle>{t('common.models')}</SettingSubtitle>
|
<SettingSubtitle>{t('common.models')}</SettingSubtitle>
|
||||||
{Object.keys(modelGroups).map((group) => (
|
{Object.keys(modelGroups).map((group) => (
|
||||||
<Card key={group} type="inner" title={group} style={{ marginBottom: '10px' }} size="small">
|
<Card key={group} type="inner" title={group} style={{ marginBottom: '10px' }} size="small">
|
||||||
@ -182,14 +184,14 @@ const ModelListHeader = styled.div`
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
`
|
`
|
||||||
|
|
||||||
const HelpTextRow = styled.div`
|
export const HelpTextRow = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 5px 0;
|
padding: 5px 0;
|
||||||
`
|
`
|
||||||
|
|
||||||
const HelpText = styled.div`
|
export const HelpText = styled.div`
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
color: var(--color-text);
|
color: var(--color-text);
|
||||||
opacity: 0.4;
|
opacity: 0.4;
|
||||||
|
|||||||
35
src/renderer/src/pages/settings/providers/OllamaSettings.tsx
Normal file
35
src/renderer/src/pages/settings/providers/OllamaSettings.tsx
Normal file
@ -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 (
|
||||||
|
<Container>
|
||||||
|
<SettingSubtitle>{t('ollama.keep_alive_time.title')}</SettingSubtitle>
|
||||||
|
<InputNumber
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
value={keepAliveMinutes}
|
||||||
|
onChange={(e) => setKeepAliveMinutes(Number(e))}
|
||||||
|
onBlur={() => setKeepAliveTime(keepAliveMinutes)}
|
||||||
|
suffix={t('ollama.keep_alive_time.placeholder')}
|
||||||
|
step={5}
|
||||||
|
/>
|
||||||
|
<HelpTextRow>
|
||||||
|
<HelpText>{t('ollama.keep_alive_time.description')}</HelpText>
|
||||||
|
</HelpTextRow>
|
||||||
|
</Container>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const Container = styled.div``
|
||||||
|
|
||||||
|
export default OllamSettings
|
||||||
@ -1,5 +1,6 @@
|
|||||||
import Anthropic from '@anthropic-ai/sdk'
|
import Anthropic from '@anthropic-ai/sdk'
|
||||||
import { MessageCreateParamsNonStreaming, MessageParam } from '@anthropic-ai/sdk/resources'
|
import { MessageCreateParamsNonStreaming, MessageParam } from '@anthropic-ai/sdk/resources'
|
||||||
|
import { getOllamaKeepAliveTime } from '@renderer/hooks/useOllama'
|
||||||
import { Assistant, Message, Provider, Suggestion } from '@renderer/types'
|
import { Assistant, Message, Provider, Suggestion } from '@renderer/types'
|
||||||
import { getAssistantSettings, removeQuotes } from '@renderer/utils'
|
import { getAssistantSettings, removeQuotes } from '@renderer/utils'
|
||||||
import { sum, takeRight } from 'lodash'
|
import { sum, takeRight } from 'lodash'
|
||||||
@ -26,6 +27,10 @@ export default class ProviderSDK {
|
|||||||
return this.provider.id === 'anthropic'
|
return this.provider.id === 'anthropic'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private get keepAliveTime() {
|
||||||
|
return this.provider.id === 'ollama' ? getOllamaKeepAliveTime() : undefined
|
||||||
|
}
|
||||||
|
|
||||||
public async completions(
|
public async completions(
|
||||||
messages: Message[],
|
messages: Message[],
|
||||||
assistant: Assistant,
|
assistant: Assistant,
|
||||||
@ -61,11 +66,13 @@ export default class ProviderSDK {
|
|||||||
})
|
})
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
|
// @ts-ignore key is not typed
|
||||||
const stream = await this.openaiSdk.chat.completions.create({
|
const stream = await this.openaiSdk.chat.completions.create({
|
||||||
model: model.id,
|
model: model.id,
|
||||||
messages: [systemMessage, ...userMessages].filter(Boolean) as ChatCompletionMessageParam[],
|
messages: [systemMessage, ...userMessages].filter(Boolean) as ChatCompletionMessageParam[],
|
||||||
stream: true,
|
stream: true,
|
||||||
temperature: assistant?.settings?.temperature
|
temperature: assistant?.settings?.temperature,
|
||||||
|
keep_alive: this.keepAliveTime
|
||||||
})
|
})
|
||||||
for await (const chunk of stream) {
|
for await (const chunk of stream) {
|
||||||
if (window.keyv.get(EVENT_NAMES.CHAT_COMPLETION_PAUSED)) break
|
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 : ''
|
return response.content[0].type === 'text' ? response.content[0].text : ''
|
||||||
} else {
|
} else {
|
||||||
|
// @ts-ignore key is not typed
|
||||||
const response = await this.openaiSdk.chat.completions.create({
|
const response = await this.openaiSdk.chat.completions.create({
|
||||||
model: model.id,
|
model: model.id,
|
||||||
messages: messages as ChatCompletionMessageParam[],
|
messages: messages as ChatCompletionMessageParam[],
|
||||||
stream: false
|
stream: false,
|
||||||
|
keep_alive: this.keepAliveTime
|
||||||
})
|
})
|
||||||
return response.choices[0].message?.content || ''
|
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
|
return message.content[0].type === 'text' ? message.content[0].text : null
|
||||||
} else {
|
} else {
|
||||||
|
// @ts-ignore key is not typed
|
||||||
const response = await this.openaiSdk.chat.completions.create({
|
const response = await this.openaiSdk.chat.completions.create({
|
||||||
model: model.id,
|
model: model.id,
|
||||||
messages: [systemMessage, ...userMessages] as ChatCompletionMessageParam[],
|
messages: [systemMessage, ...userMessages] as ChatCompletionMessageParam[],
|
||||||
stream: false,
|
stream: false,
|
||||||
max_tokens: 50
|
max_tokens: 50,
|
||||||
|
keep_alive: this.keepAliveTime
|
||||||
})
|
})
|
||||||
|
|
||||||
return removeQuotes(response.choices[0].message?.content || '')
|
return removeQuotes(response.choices[0].message?.content || '')
|
||||||
|
|||||||
@ -22,7 +22,7 @@ const persistedReducer = persistReducer(
|
|||||||
{
|
{
|
||||||
key: 'cherry-studio',
|
key: 'cherry-studio',
|
||||||
storage,
|
storage,
|
||||||
version: 18,
|
version: 19,
|
||||||
blacklist: ['runtime'],
|
blacklist: ['runtime'],
|
||||||
migrate
|
migrate
|
||||||
},
|
},
|
||||||
|
|||||||
@ -3,11 +3,18 @@ import { SYSTEM_MODELS } from '@renderer/config/models'
|
|||||||
import { Model, Provider } from '@renderer/types'
|
import { Model, Provider } from '@renderer/types'
|
||||||
import { uniqBy } from 'lodash'
|
import { uniqBy } from 'lodash'
|
||||||
|
|
||||||
|
type LlmSettings = {
|
||||||
|
ollama: {
|
||||||
|
keepAliveTime: number
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export interface LlmState {
|
export interface LlmState {
|
||||||
providers: Provider[]
|
providers: Provider[]
|
||||||
defaultModel: Model
|
defaultModel: Model
|
||||||
topicNamingModel: Model
|
topicNamingModel: Model
|
||||||
translateModel: Model
|
translateModel: Model
|
||||||
|
settings: LlmSettings
|
||||||
}
|
}
|
||||||
|
|
||||||
const initialState: LlmState = {
|
const initialState: LlmState = {
|
||||||
@ -132,7 +139,12 @@ const initialState: LlmState = {
|
|||||||
isSystem: true,
|
isSystem: true,
|
||||||
enabled: false
|
enabled: false
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
|
settings: {
|
||||||
|
ollama: {
|
||||||
|
keepAliveTime: 0
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const settingsSlice = createSlice({
|
const settingsSlice = createSlice({
|
||||||
@ -179,6 +191,9 @@ const settingsSlice = createSlice({
|
|||||||
},
|
},
|
||||||
setTranslateModel: (state, action: PayloadAction<{ model: Model }>) => {
|
setTranslateModel: (state, action: PayloadAction<{ model: Model }>) => {
|
||||||
state.translateModel = action.payload.model
|
state.translateModel = action.payload.model
|
||||||
|
},
|
||||||
|
setOllamaKeepAliveTime: (state, action: PayloadAction<number>) => {
|
||||||
|
state.settings.ollama.keepAliveTime = action.payload
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -192,7 +207,8 @@ export const {
|
|||||||
removeModel,
|
removeModel,
|
||||||
setDefaultModel,
|
setDefaultModel,
|
||||||
setTopicNamingModel,
|
setTopicNamingModel,
|
||||||
setTranslateModel
|
setTranslateModel,
|
||||||
|
setOllamaKeepAliveTime
|
||||||
} = settingsSlice.actions
|
} = settingsSlice.actions
|
||||||
|
|
||||||
export default settingsSlice.reducer
|
export default settingsSlice.reducer
|
||||||
|
|||||||
@ -272,11 +272,19 @@ const migrateConfig = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
'18': (state: RootState) => {
|
'19': (state: RootState) => {
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
agents: {
|
agents: {
|
||||||
agents: []
|
agents: []
|
||||||
|
},
|
||||||
|
llm: {
|
||||||
|
...state.llm,
|
||||||
|
settings: {
|
||||||
|
ollama: {
|
||||||
|
keepAliveTime: 5
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user