feat: add quick assistant settings panel and management functionality (#6201)

* feat: add quick assistant settings panel and management functionality

- Create QuickAssistantSettings component for UI
- Extend useAssistant hook with quick assistant controls
- Add settings button in ModelSettings page
- Implement temperature, context count, max tokens, and other parameters
- Connect settings to store via updateQuickAssistant action

Separate quick assistant preferences from default assistant settings for better customization.

* refactor(QuickAssistantSettings): remove maxTokens and refine UI layout

- Removed maxTokens related state, logic, and UI elements
- Simplified settings page by eliminating unused configuration
- Adjusted layout for Slider and InputNumber for better usability
- Removed fixed width from Modal to enable responsive behavior

* refactor(HomeWindow): optimize message building logic

- Removed redundant quickAssistant fetching logic
- Use `useQuickAssistant` hook directly for cleaner code
- Simplified message content concatenation method

* style(QuickAssistantSettings): Adjust spacing in settings page layout

Change the column width of sliders and input fields from 20/4 to 21/3 for a more reasonable layout
Also set the popup width to 800px to improve user experience

* feat(Quick Assistant): Add option to select assistant or model, and optimize Quick Assistant logic

- Added functionality to choose between using models or referencing other assistants
- Optimized model selection logic to automatically select based on settings
- Added relevant internationalization texts

* fix(HomeWindow): Dynamically display input box placeholder text based on quick assistant states

* refactor(QuickAssistant): remove the implement of the quick assistant feature and restructure related logic

- Remove code related to the quick assistant feature, including the useQuickAssistant hook, QuickAssistantSettings component, and associated store logic.
- Restructure the HomeWindow component to use default or specified assistants instead of the quick assistant functionality, simplifying the code structure.

* refactor(QuickAssistant): Remove custom default model for quick assistant and switch to default assistant

- Refactor quick assistant functionality, remove independent model settings, change to select via assistant ID
- Update multilingual translation text to match new features

* refactor(QuickAssistant): Remove quick assistant-related states and simplify logic

- Remove unused quick assistant states and toggle functionality, simplifying related logic
- Update multilingual files to match the new default model and assistant labels

* refactor(i18n): Unify translation keys for input field placeholders

Unify the placeholder translation keys from `model_empty` and `assistant_empty` into empty across different scenarios, streamlining code logic

* refactor(settings): simplify quick helper selection logic by directly using the preset helper

- Removed redundant helper filtering logic, directly using the preset helper as the quick helper
This commit is contained in:
jwcrystal 2025-06-16 18:13:35 +08:00 committed by GitHub
parent 9faa45b571
commit 477b5d2449
11 changed files with 192 additions and 65 deletions

View File

@ -15,7 +15,7 @@ import {
updateTopic,
updateTopics
} from '@renderer/store/assistants'
import { setDefaultModel, setQuickAssistantModel, setTopicNamingModel, setTranslateModel } from '@renderer/store/llm'
import { setDefaultModel, setTopicNamingModel, setTranslateModel } from '@renderer/store/llm'
import { Assistant, AssistantSettings, Model, Topic } from '@renderer/types'
import { useCallback, useMemo } from 'react'
@ -103,17 +103,15 @@ export function useDefaultAssistant() {
}
export function useDefaultModel() {
const { defaultModel, topicNamingModel, translateModel, quickAssistantModel } = useAppSelector((state) => state.llm)
const { defaultModel, topicNamingModel, translateModel } = useAppSelector((state) => state.llm)
const dispatch = useAppDispatch()
return {
defaultModel,
topicNamingModel,
translateModel,
quickAssistantModel,
setDefaultModel: (model: Model) => dispatch(setDefaultModel({ model })),
setTopicNamingModel: (model: Model) => dispatch(setTopicNamingModel({ model })),
setTranslateModel: (model: Model) => dispatch(setTranslateModel({ model })),
setQuickAssistantModel: (model: Model) => dispatch(setQuickAssistantModel({ model }))
setTranslateModel: (model: Model) => dispatch(setTranslateModel({ model }))
}
}

View File

@ -1592,6 +1592,10 @@
"models.translate_model_prompt_title": "Translate Model Prompt",
"models.quick_assistant_model": "Quick Assistant Model",
"models.quick_assistant_model_description": "Default model used by Quick Assistant",
"models.quick_assistant_selection": "Select Assistant",
"models.quick_assistant_default_tag": "Default",
"models.use_model": "Default Model",
"models.use_assistant": "Use Assistant",
"moresetting": "More Settings",
"moresetting.check.confirm": "Confirm Selection",
"moresetting.check.warn": "Please be cautious when selecting this option. Incorrect selection may cause the model to malfunction!",

View File

@ -1586,6 +1586,10 @@
"models.translate_model_prompt_title": "翻訳モデルのプロンプト",
"models.quick_assistant_model": "クイックアシスタントモデル",
"models.quick_assistant_model_description": "クイックアシスタントで使用されるデフォルトモデル",
"models.quick_assistant_selection": "アシスタントを選択します",
"models.quick_assistant_default_tag": "デフォルト",
"models.use_model": "デフォルトモデル",
"models.use_assistant": "アシスタントの活用",
"moresetting": "詳細設定",
"moresetting.check.confirm": "選択を確認",
"moresetting.check.warn": "このオプションを選択する際は慎重に行ってください。誤った選択はモデルの誤動作を引き起こす可能性があります!",

View File

@ -1586,6 +1586,10 @@
"models.translate_model_prompt_title": "Модель перевода",
"models.quick_assistant_model": "Модель быстрого помощника",
"models.quick_assistant_model_description": "Модель по умолчанию, используемая быстрым помощником",
"models.quick_assistant_selection": "Выберите помощника",
"models.quick_assistant_default_tag": "умолчанию",
"models.use_model": "модель по умолчанию",
"models.use_assistant": "Использование ассистентов",
"moresetting": "Дополнительные настройки",
"moresetting.check.confirm": "Подтвердить выбор",
"moresetting.check.warn": "Пожалуйста, будьте осторожны при выборе этой опции. Неправильный выбор может привести к сбою в работе модели!",

View File

@ -1592,6 +1592,10 @@
"models.translate_model_prompt_title": "翻译模型提示词",
"models.quick_assistant_model": "快捷助手模型",
"models.quick_assistant_model_description": "快捷助手使用的默认模型",
"models.quick_assistant_selection": "选择助手",
"models.quick_assistant_default_tag": "默认",
"models.use_model": "默认模型",
"models.use_assistant": "使用助手",
"moresetting": "更多设置",
"moresetting.check.confirm": "确认勾选",
"moresetting.check.warn": "请慎重勾选此选项,勾选错误会导致模型无法正常使用!!!",

View File

@ -1589,6 +1589,10 @@
"models.translate_model_prompt_title": "翻譯模型提示詞",
"models.quick_assistant_model": "快捷助手模型",
"models.quick_assistant_model_description": "快捷助手使用的預設模型",
"models.quick_assistant_selection": "選擇助手",
"models.quick_assistant_default_tag": "預設",
"models.use_model": "預設模型",
"models.use_assistant": "使用助手",
"moresetting": "更多設定",
"moresetting.check.confirm": "確認勾選",
"moresetting.check.warn": "請謹慎勾選此選項,勾選錯誤會導致模型無法正常使用!!!",
@ -1957,7 +1961,7 @@
},
"opacity": {
"title": "透明度",
"description": "設置視窗的默認透明度100%為完全不透明"
"description": "設置視窗的預設透明度100%為完全不透明"
}
},
"actions": {

View File

@ -1,37 +1,35 @@
import { RedoOutlined } from '@ant-design/icons'
import ModelAvatar from '@renderer/components/Avatar/ModelAvatar'
import { HStack } from '@renderer/components/Layout'
import PromptPopup from '@renderer/components/Popups/PromptPopup'
import { isEmbeddingModel } from '@renderer/config/models'
import { TRANSLATE_PROMPT } from '@renderer/config/prompts'
import { useTheme } from '@renderer/context/ThemeProvider'
import { useDefaultModel } from '@renderer/hooks/useAssistant'
import { useAssistants, useDefaultAssistant, useDefaultModel } from '@renderer/hooks/useAssistant'
import { useProviders } from '@renderer/hooks/useProvider'
import { useSettings } from '@renderer/hooks/useSettings'
import { getModelUniqId, hasModel } from '@renderer/services/ModelService'
import { useAppSelector } from '@renderer/store'
import { useAppDispatch } from '@renderer/store'
import { setQuickAssistantId } from '@renderer/store/llm'
import { setTranslateModelPrompt } from '@renderer/store/settings'
import { Model } from '@renderer/types'
import { Button, Select, Tooltip } from 'antd'
import { find, sortBy } from 'lodash'
import { FolderPen, Languages, MessageSquareMore, Rocket, Settings2 } from 'lucide-react'
import { CircleHelp, FolderPen, Languages, MessageSquareMore, Rocket, Settings2 } from 'lucide-react'
import { FC, useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
import { SettingContainer, SettingDescription, SettingGroup, SettingTitle } from '..'
import DefaultAssistantSettings from './DefaultAssistantSettings'
import TopicNamingModalPopup from './TopicNamingModalPopup'
const ModelSettings: FC = () => {
const {
defaultModel,
topicNamingModel,
translateModel,
quickAssistantModel,
setDefaultModel,
setTopicNamingModel,
setTranslateModel,
setQuickAssistantModel
} = useDefaultModel()
const { defaultModel, topicNamingModel, translateModel, setDefaultModel, setTopicNamingModel, setTranslateModel } =
useDefaultModel()
const { defaultAssistant } = useDefaultAssistant()
const { assistants } = useAssistants()
const { providers } = useProviders()
const allModels = providers.map((p) => p.models).flat()
const { theme } = useTheme()
@ -39,6 +37,7 @@ const ModelSettings: FC = () => {
const { translateModelPrompt } = useSettings()
const dispatch = useAppDispatch()
const { quickAssistantId } = useAppSelector((state) => state.llm)
const selectOptions = providers
.filter((p) => p.models.length > 0)
@ -68,11 +67,6 @@ const ModelSettings: FC = () => {
[translateModel]
)
const defaultQuickAssistantModel = useMemo(
() => (hasModel(quickAssistantModel) ? getModelUniqId(quickAssistantModel) : undefined),
[quickAssistantModel]
)
const onUpdateTranslateModel = async () => {
const prompt = await PromptPopup.show({
title: t('settings.models.translate_model_prompt_title'),
@ -163,27 +157,118 @@ const ModelSettings: FC = () => {
<SettingDescription>{t('settings.models.translate_model_description')}</SettingDescription>
</SettingGroup>
<SettingGroup theme={theme}>
<SettingTitle style={{ marginBottom: 12 }}>
<HStack alignItems="center" gap={10}>
<Rocket size={18} color="var(--color-text)" />
{t('settings.models.quick_assistant_model')}
</HStack>
</SettingTitle>
<HStack alignItems="center">
<Select
value={defaultQuickAssistantModel}
defaultValue={defaultQuickAssistantModel}
style={{ width: 360 }}
onChange={(value) => setQuickAssistantModel(find(allModels, JSON.parse(value)) as Model)}
options={selectOptions}
showSearch
placeholder={t('settings.models.empty')}
/>
<HStack alignItems="center" style={{ marginBottom: 12 }}>
<SettingTitle>
<HStack alignItems="center" gap={10}>
<Rocket size={18} color="var(--color-text)" />
{t('settings.models.quick_assistant_model')}
<Tooltip title={t('selection.settings.user_modal.model.tooltip')} arrow>
<QuestionIcon size={12} />
</Tooltip>
<Spacer />
</HStack>
<HStack alignItems="center" gap={0}>
<StyledButton
type={!quickAssistantId ? 'primary' : 'default'}
onClick={() => dispatch(setQuickAssistantId(null))}
selected={!quickAssistantId}>
{t('settings.models.use_model')}
</StyledButton>
<StyledButton
type={quickAssistantId ? 'primary' : 'default'}
onClick={() => {
dispatch(setQuickAssistantId(defaultAssistant.id))
}}
selected={!!quickAssistantId}>
{t('settings.models.use_assistant')}
</StyledButton>
</HStack>
</SettingTitle>
</HStack>
{!quickAssistantId ? null : (
<HStack alignItems="center" style={{ marginTop: 12 }}>
<Select
value={quickAssistantId}
style={{ width: 360 }}
onChange={(value) => dispatch(setQuickAssistantId(value))}
placeholder={t('settings.models.quick_assistant_selection')}>
{assistants.map((a) => (
<Select.Option key={a.id} value={a.id}>
<AssistantItem>
<ModelAvatar model={a.model || defaultModel} size={18} />
<AssistantName>{a.name}</AssistantName>
<Spacer />
{a.id === defaultAssistant.id && (
<DefaultTag isCurrent={true}>{t('settings.models.quick_assistant_default_tag')}</DefaultTag>
)}
</AssistantItem>
</Select.Option>
))}
</Select>
</HStack>
)}
<SettingDescription>{t('settings.models.quick_assistant_model_description')}</SettingDescription>
</SettingGroup>
</SettingContainer>
)
}
const QuestionIcon = styled(CircleHelp)`
cursor: pointer;
color: var(--color-text-3);
`
const StyledButton = styled(Button)<{ selected: boolean }>`
border-radius: ${(props) => (props.selected ? '6px' : '6px')};
z-index: ${(props) => (props.selected ? 1 : 0)};
min-width: 80px;
&:first-child {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
border-right-width: 0px; // No right border for the first button when not selected
}
&:last-child {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
border-left-width: 1px; // Ensure left border for the last button
}
// Override Ant Design's default hover and focus styles for a cleaner look
&:hover,
&:focus {
z-index: 1;
border-color: ${(props) => (props.selected ? 'var(--ant-primary-color)' : 'var(--ant-primary-color-hover)')};
box-shadow: ${(props) =>
props.selected ? '0 0 0 2px var(--ant-primary-color-outline)' : '0 0 0 2px var(--ant-primary-color-outline)'};
}
`
const AssistantItem = styled.div`
display: flex;
flex-direction: row;
align-items: center;
gap: 8px;
height: 28px;
`
const AssistantName = styled.span`
max-width: calc(100% - 60px);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
`
const Spacer = styled.div`
flex: 1;
`
const DefaultTag = styled.span<{ isCurrent: boolean }>`
color: ${(props) => (props.isCurrent ? 'var(--color-primary)' : 'var(--color-text-3)')};
font-size: 12px;
padding: 2px 4px;
border-radius: 4px;
`
export default ModelSettings

View File

@ -21,7 +21,7 @@ export interface LlmState {
defaultModel: Model
topicNamingModel: Model
translateModel: Model
quickAssistantModel: Model
quickAssistantId: string | null
settings: LlmSettings
}
@ -514,7 +514,7 @@ const initialState: LlmState = {
defaultModel: SYSTEM_MODELS.defaultModel[0],
topicNamingModel: SYSTEM_MODELS.defaultModel[1],
translateModel: SYSTEM_MODELS.defaultModel[2],
quickAssistantModel: SYSTEM_MODELS.defaultModel[3],
quickAssistantId: null,
providers: INITIAL_PROVIDERS,
settings: {
ollama: {
@ -621,8 +621,9 @@ const llmSlice = createSlice({
setTranslateModel: (state, action: PayloadAction<{ model: Model }>) => {
state.translateModel = action.payload.model
},
setQuickAssistantModel: (state, action: PayloadAction<{ model: Model }>) => {
state.quickAssistantModel = action.payload.model
setQuickAssistantId: (state, action: PayloadAction<string | null>) => {
state.quickAssistantId = action.payload
},
setOllamaKeepAliveTime: (state, action: PayloadAction<number>) => {
state.settings.ollama.keepAliveTime = action.payload
@ -661,7 +662,7 @@ export const {
setDefaultModel,
setTopicNamingModel,
setTranslateModel,
setQuickAssistantModel,
setQuickAssistantId,
setOllamaKeepAliveTime,
setLMStudioKeepAliveTime,
setGPUStackKeepAliveTime,

View File

@ -1462,8 +1462,6 @@ const migrateConfig = {
searchMessageShortcut.shortcut = [isMac ? 'Command' : 'Ctrl', 'Shift', 'F']
}
}
// Quick assistant model
state.llm.quickAssistantModel = state.llm.defaultModel || SYSTEM_MODELS.silicon[1]
return state
} catch (error) {
return state

View File

@ -1,5 +1,4 @@
import Scrollbar from '@renderer/components/Scrollbar'
import { getDefaultModel } from '@renderer/services/AssistantService'
import { Assistant } from '@renderer/types'
import { FC } from 'react'
import styled from 'styled-components'
@ -11,11 +10,9 @@ interface Props {
}
const ChatWindow: FC<Props> = ({ route, assistant }) => {
// const { defaultAssistant } = useDefaultAssistant()
return (
<Main className="bubble">
<Messages assistant={{ ...assistant, model: getDefaultModel() }} route={route} />
<Messages assistant={{ ...assistant }} route={route} />
</Main>
)
}

View File

@ -4,13 +4,13 @@ import { useDefaultAssistant, useDefaultModel } from '@renderer/hooks/useAssista
import { useSettings } from '@renderer/hooks/useSettings'
import i18n from '@renderer/i18n'
import { fetchChatCompletion } from '@renderer/services/ApiService'
import { getDefaultAssistant, getDefaultModel } from '@renderer/services/AssistantService'
import { getAssistantById } from '@renderer/services/AssistantService'
import { getAssistantMessage, getUserMessage } from '@renderer/services/MessagesService'
import store from '@renderer/store'
import store, { useAppSelector } from '@renderer/store'
import { upsertManyBlocks } from '@renderer/store/messageBlock'
import { updateOneBlock, upsertOneBlock } from '@renderer/store/messageBlock'
import { newMessagesActions } from '@renderer/store/newMessage'
import { ThemeMode } from '@renderer/types'
import { Assistant, ThemeMode } from '@renderer/types'
import { Chunk, ChunkType } from '@renderer/types/chunk'
import { AssistantMessageStatus } from '@renderer/types/newMessage'
import { MessageBlockStatus } from '@renderer/types/newMessage'
@ -37,14 +37,14 @@ const HomeWindow: FC = () => {
const [isFirstMessage, setIsFirstMessage] = useState(true)
const [clipboardText, setClipboardText] = useState('')
const [selectedText, setSelectedText] = useState('')
const [currentAssistant, setCurrentAssistant] = useState<Assistant>({} as Assistant)
const [text, setText] = useState('')
const [lastClipboardText, setLastClipboardText] = useState<string | null>(null)
const textChange = useState(() => {})[1]
const { defaultAssistant } = useDefaultAssistant()
const topic = defaultAssistant.topics[0]
const { defaultModel, quickAssistantModel } = useDefaultModel()
// 如果 quickAssistantModel 未設定,則使用 defaultModel
const model = quickAssistantModel || defaultModel
const { defaultModel } = useDefaultModel()
const model = currentAssistant.model || defaultModel
const { language, readClipboardAtStartup, windowStyle } = useSettings()
const { theme } = useTheme()
const { t } = useTranslation()
@ -54,6 +54,8 @@ const HomeWindow: FC = () => {
const content = isFirstMessage ? (referenceText === text ? text : `${referenceText}\n\n${text}`).trim() : text.trim()
const { quickAssistantId } = useAppSelector((state) => state.llm)
const readClipboard = useCallback(async () => {
if (!readClipboardAtStartup) return
@ -158,16 +160,36 @@ const HomeWindow: FC = () => {
setText(e.target.value)
}
useEffect(() => {
const defaultCurrentAssistant = {
...defaultAssistant,
model: defaultModel
}
if (quickAssistantId) {
// 獲取指定助手,如果不存在則使用默認助手
const assistantFromId = getAssistantById(quickAssistantId)
const currentAssistant = assistantFromId || defaultCurrentAssistant
// 如果助手本身沒有設定模型,則使用預設模型
if (!currentAssistant.model) {
currentAssistant.model = defaultModel
}
setCurrentAssistant(currentAssistant)
} else {
setCurrentAssistant(defaultCurrentAssistant)
}
}, [quickAssistantId, defaultAssistant, defaultModel])
const onSendMessage = useCallback(
async (prompt?: string) => {
if (isEmpty(content)) {
return
}
const topic = currentAssistant.topics[0]
const messageParams = {
role: 'user',
content: prompt ? `${prompt}\n\n${content}` : content,
assistant: defaultAssistant,
content: [prompt, content].filter(Boolean).join('\n\n'),
assistant: currentAssistant,
topic,
createdAt: dayjs().format('YYYY-MM-DD HH:mm:ss'),
status: 'success'
@ -178,7 +200,7 @@ const HomeWindow: FC = () => {
store.dispatch(newMessagesActions.addMessage({ topicId, message: userMessage }))
store.dispatch(upsertManyBlocks(blocks))
const assistant = getDefaultAssistant()
const assistant = currentAssistant
let blockId: string | null = null
let blockContent: string = ''
@ -187,7 +209,7 @@ const HomeWindow: FC = () => {
fetchChatCompletion({
messages: [userMessage],
assistant: { ...assistant, model: quickAssistantModel || getDefaultModel(), settings: { streamOutput: true } },
assistant: { ...assistant, settings: { streamOutput: true } },
onChunkReceived: (chunk: Chunk) => {
if (chunk.type === ChunkType.TEXT_DELTA) {
blockContent += chunk.text
@ -224,7 +246,7 @@ const HomeWindow: FC = () => {
setIsFirstMessage(false)
setText('') // ✅ 清除输入框内容
},
[content, defaultAssistant, topic, quickAssistantModel]
[content, currentAssistant, topic]
)
const clearClipboard = () => {
@ -277,7 +299,11 @@ const HomeWindow: FC = () => {
text={text}
model={model}
referenceText={referenceText}
placeholder={t('miniwindow.input.placeholder.empty', { model: model.name })}
placeholder={
quickAssistantId
? t('miniwindow.input.placeholder.empty', { model: currentAssistant.name })
: t('miniwindow.input.placeholder.empty', { model: model.name })
}
handleKeyDown={handleKeyDown}
handleChange={handleChange}
ref={inputBarRef}
@ -290,7 +316,7 @@ const HomeWindow: FC = () => {
<ClipboardPreview referenceText={referenceText} clearClipboard={clearClipboard} t={t} />
</div>
)}
<ChatWindow route={route} assistant={defaultAssistant} />
<ChatWindow route={route} assistant={currentAssistant ?? defaultAssistant} />
<Divider style={{ margin: '10px 0' }} />
<Footer route={route} onExit={() => setRoute('home')} />
</Container>
@ -316,7 +342,9 @@ const HomeWindow: FC = () => {
placeholder={
referenceText && route === 'home'
? t('miniwindow.input.placeholder.title')
: t('miniwindow.input.placeholder.empty', { model: model.name })
: quickAssistantId
? t('miniwindow.input.placeholder.empty', { model: currentAssistant.name })
: t('miniwindow.input.placeholder.empty', { model: model.name })
}
handleKeyDown={handleKeyDown}
handleChange={handleChange}