style(ModelList): adjust GroupHeader height and update icon in ModelListItem

This commit is contained in:
kangfenmao 2025-08-04 19:03:29 +08:00
parent c9837eaa71
commit ce804ce02b
42 changed files with 801 additions and 552 deletions

View File

@ -128,3 +128,4 @@ releaseInfo:
内存泄漏修复:优化代码逻辑,解决内存泄漏问题,提升运行稳定性
嵌入模型简化:降低嵌入模型配置复杂度,提高易用性
MCP Tool 长时间运行:增强 MCP 工具的稳定性,支持长时间任务执行
设置页面优化:优化设置页面布局,提升用户体验

View File

@ -1,57 +0,0 @@
import ModelEditContent from '@renderer/components/ModelList/ModelEditContent'
import { TopView } from '@renderer/components/TopView'
import { Model, Provider } from '@renderer/types'
import React from 'react'
interface ShowParams {
provider: Provider
model: Model
}
interface Props extends ShowParams {
resolve: (data?: Model) => void
}
const PopupContainer: React.FC<Props> = ({ provider, model, resolve }) => {
const handleUpdateModel = (updatedModel: Model) => {
resolve(updatedModel)
}
const handleClose = () => {
resolve(undefined) // Resolve with no data on close
}
return (
<ModelEditContent
provider={provider}
model={model}
onUpdateModel={handleUpdateModel}
open={true} // Always open when rendered by TopView
onClose={handleClose}
key={model.id} // Ensure re-mount when model changes
/>
)
}
const TopViewKey = 'EditModelPopup'
export default class EditModelPopup {
static hide() {
TopView.hide(TopViewKey)
}
static show(props: ShowParams) {
return new Promise<Model | undefined>((resolve) => {
TopView.show(
<PopupContainer
{...props}
resolve={(v) => {
resolve(v)
this.hide()
}}
/>,
TopViewKey
)
})
}
}

View File

@ -1463,7 +1463,7 @@
"function_calling": "Tool",
"reasoning": "Reasoning",
"rerank": "Reranker",
"select": "Select Model Types",
"select": "Model Types",
"text": "Text",
"vision": "Vision",
"websearch": "WebSearch"
@ -2716,6 +2716,8 @@
"jsonSaveError": "Failed to save JSON configuration.",
"jsonSaveSuccess": "JSON configuration has been saved.",
"logoUrl": "Logo URL",
"longRunning": "Long Running Mode",
"longRunningTooltip": "When enabled, the server supports long-running tasks. When receiving progress notifications, the timeout will be reset and the maximum execution time will be extended to 10 minutes.",
"missingDependencies": "is Missing, please install it to continue.",
"more": {
"awesome": "Curated MCP Server List",
@ -2962,8 +2964,8 @@
"tooltip": "Optional e.g. GPT-4"
},
"supported_text_delta": {
"label": "Incremental text output",
"tooltip": "When the model is not supported, close the button"
"label": "Support incremental text output",
"tooltip": "The model returns text incrementally, rather than all at once. Enabled by default, if the model does not support it, please disable this option"
}
},
"api_key": "API Key",
@ -3019,7 +3021,6 @@
"provider_name": "Provider Name",
"quick_assistant_default_tag": "Default",
"quick_assistant_model": "Quick Assistant Model",
"quick_assistant_model_description": "Default model used by Quick Assistant",
"quick_assistant_selection": "Select Assistant",
"topic_naming_model": "Topic Naming Model",
"topic_naming_model_description": "Model used when automatically naming a new topic",
@ -3337,7 +3338,7 @@
"title": "Pre Process",
"tooltip": "In Settings -> Tools, set a document preprocessing service provider. Document preprocessing can effectively improve the retrieval performance of complex format documents and scanned documents."
},
"title": "Tools Settings",
"title": "Other Settings",
"websearch": {
"apikey": "API key",
"blacklist": "Blacklist",

View File

@ -1463,7 +1463,7 @@
"function_calling": "ツール",
"reasoning": "推論",
"rerank": "再順序付け",
"select": "モデルタイプを選択",
"select": "モデルタイプ",
"text": "テキスト",
"vision": "画像",
"websearch": "ウェブ検索"
@ -2716,6 +2716,8 @@
"jsonSaveError": "JSON設定の保存に失敗しました",
"jsonSaveSuccess": "JSON設定が保存されました。",
"logoUrl": "ロゴURL",
"longRunning": "長時間運行モード",
"longRunningTooltip": "このオプションを有効にすると、サーバーは長時間のタスクをサポートします。進行状況通知を受信すると、タイムアウトがリセットされ、最大実行時間が10分に延長されます。",
"missingDependencies": "が不足しています。続行するにはインストールしてください。",
"more": {
"awesome": "厳選された MCP サーバーリスト",
@ -2962,8 +2964,8 @@
"tooltip": "例GPT-4"
},
"supported_text_delta": {
"label": "インクリメンタルテキスト出力",
"tooltip": "モデルがサポートされていない場合は、ボタンを閉じます"
"label": "インクリメンタルテキスト出力のサポート",
"tooltip": "モデルがテキストをチャンクで返す場合、デフォルトで有効になっています。モデルがサポートしていない場合は、このオプションを無効にしてください"
}
},
"api_key": "API キー",
@ -3019,7 +3021,6 @@
"provider_name": "プロバイダー名",
"quick_assistant_default_tag": "デフォルト",
"quick_assistant_model": "クイックアシスタントモデル",
"quick_assistant_model_description": "クイックアシスタントで使用されるデフォルトモデル",
"quick_assistant_selection": "アシスタントを選択します",
"topic_naming_model": "トピック命名モデル",
"topic_naming_model_description": "新しいトピックを自動的に命名する際に使用されるモデル",
@ -3337,7 +3338,7 @@
"title": "前処理",
"tooltip": "設定 → ツールで、ドキュメント前処理サービスプロバイダーを設定します。ドキュメント前処理は、複雑な形式のドキュメントやスキャンされたドキュメントの検索性能を効果的に向上させます。"
},
"title": "ツール設定",
"title": "その他の設定",
"websearch": {
"apikey": "APIキー",
"blacklist": "ブラックリスト",

View File

@ -2716,6 +2716,8 @@
"jsonSaveError": "Не удалось сохранить конфигурацию JSON",
"jsonSaveSuccess": "JSON конфигурация сохранена",
"logoUrl": "URL логотипа",
"longRunning": "Длительный режим работы",
"longRunningTooltip": "Включив эту опцию, сервер будет поддерживать длительные задачи. При получении уведомлений о ходе выполнения будет сброшен тайм-аут и максимальное время выполнения будет увеличено до 10 минут.",
"missingDependencies": "отсутствует, пожалуйста, установите для продолжения.",
"more": {
"awesome": "Кураторский список серверов MCP",
@ -2962,8 +2964,8 @@
"tooltip": "Необязательно, например, GPT-4"
},
"supported_text_delta": {
"label": "Инкрементный текст вывод",
"tooltip": "Когда модель не поддерживается, закройте кнопку"
"label": "Поддержка инкрементного текстового вывода",
"tooltip": "Модель возвращает текст по частям, а не одним блоком, по умолчанию включено, если модель не поддерживает, закройте эту опцию"
}
},
"api_key": "API ключ",
@ -3019,7 +3021,6 @@
"provider_name": "Имя провайдера",
"quick_assistant_default_tag": "умолчанию",
"quick_assistant_model": "Модель быстрого помощника",
"quick_assistant_model_description": "Модель по умолчанию, используемая быстрым помощником",
"quick_assistant_selection": "Выберите помощника",
"topic_naming_model": "Модель именования топика",
"topic_naming_model_description": "Модель, используемая при автоматическом именовании нового топика",
@ -3337,7 +3338,7 @@
"title": "Предварительная обработка",
"tooltip": "В настройках (Настройки -> Инструменты) укажите поставщика услуги предварительной обработки документов. Предварительная обработка документов может значительно повысить эффективность поиска для документов сложных форматов и отсканированных документов."
},
"title": "Настройки инструментов",
"title": "Другие настройки",
"websearch": {
"apikey": "API ключ",
"blacklist": "Черный список",

View File

@ -1463,7 +1463,7 @@
"function_calling": "工具",
"reasoning": "推理",
"rerank": "重排",
"select": "选择模型类型",
"select": "模型类型",
"text": "文本",
"vision": "视觉",
"websearch": "联网"
@ -2716,6 +2716,8 @@
"jsonSaveError": "保存 JSON 配置失败",
"jsonSaveSuccess": "JSON 配置已保存",
"logoUrl": "标志网址",
"longRunning": "长时间运行模式",
"longRunningTooltip": "启用后服务器支持长时间任务接收到进度通知时会重置超时计时器并延长最大超时时间至10分钟",
"missingDependencies": "缺失,请安装它以继续",
"more": {
"awesome": "精选的 MCP 服务器列表",
@ -2962,8 +2964,8 @@
"tooltip": "例如 GPT-4"
},
"supported_text_delta": {
"label": "增量文本输出",
"tooltip": "当模型不支持的时候,将该按钮关闭"
"label": "支持增量文本输出",
"tooltip": "模型每次返回文本增量,而不是一次性返回所有文本,默认开启,如果模型不支持,请关闭"
}
},
"api_key": "API 密钥",
@ -3019,7 +3021,6 @@
"provider_name": "服务商名称",
"quick_assistant_default_tag": "默认",
"quick_assistant_model": "快捷助手模型",
"quick_assistant_model_description": "快捷助手使用的默认模型",
"quick_assistant_selection": "选择助手",
"topic_naming_model": "话题命名模型",
"topic_naming_model_description": "自动命名新话题时使用的模型",
@ -3337,7 +3338,7 @@
"title": "文档预处理",
"tooltip": "在设置 -> 工具中设置文档预处理服务商,文档预处理可以有效提升复杂格式文档与扫描版文档的检索效果"
},
"title": "工具设置",
"title": "其他设置",
"websearch": {
"apikey": "API 密钥",
"blacklist": "黑名单",

View File

@ -1463,7 +1463,7 @@
"function_calling": "工具",
"reasoning": "推理",
"rerank": "重排",
"select": "選擇模型類型",
"select": "模型類型",
"text": "文字",
"vision": "視覺",
"websearch": "網路搜尋"
@ -2716,6 +2716,8 @@
"jsonSaveError": "保存 JSON 配置失敗",
"jsonSaveSuccess": "JSON 配置已儲存",
"logoUrl": "標誌網址",
"longRunning": "長時間運行模式",
"longRunningTooltip": "啟用後伺服器支援長時間任務接收到進度通知時會重置超時計時器並延長最大超時時間至10分鐘",
"missingDependencies": "缺失,請安裝它以繼續",
"more": {
"awesome": "精選的 MCP 伺服器清單",
@ -2962,8 +2964,8 @@
"tooltip": "例如 GPT-4"
},
"supported_text_delta": {
"label": "增量文本輸出",
"tooltip": "當模型不支持的時候,將該按鈕關閉"
"label": "支持增量文本輸出",
"tooltip": "模型每次返回文本增量,而不是一次性返回所有文本,預設開啟,如果模型不支持,請關閉"
}
},
"api_key": "API 密鑰",
@ -3019,7 +3021,6 @@
"provider_name": "提供者名稱",
"quick_assistant_default_tag": "預設",
"quick_assistant_model": "快捷助手模型",
"quick_assistant_model_description": "快捷助手使用的預設模型",
"quick_assistant_selection": "選擇助手",
"topic_naming_model": "話題命名模型",
"topic_naming_model_description": "自動命名新話題時使用的模型",
@ -3337,7 +3338,7 @@
"title": "前置處理",
"tooltip": "在「設定」->「工具」中設定文件預處理服務供應商。文件預處理可有效提升複雜格式文件及掃描文件的檢索效能"
},
"title": "工具設定",
"title": "其他設定",
"websearch": {
"apikey": "API 金鑰",
"blacklist": "黑名單",

View File

@ -1463,7 +1463,7 @@
"function_calling": "κλήση συνάρτησης",
"reasoning": "λογική",
"rerank": "Τακτοποιώ",
"select": "Επιλέξτε τύπο μοντέλου",
"select": "Τύποι μοντέλου",
"text": "κείμενο",
"vision": "εικόνα",
"websearch": "δικτύωση"
@ -3018,7 +3018,6 @@
"provider_name": "Όνομα Παρόχου",
"quick_assistant_default_tag": "Προεπιλογή",
"quick_assistant_model": "Μοντέλο Γρήγορου Βοηθού",
"quick_assistant_model_description": "Προεπιλεγμένο μοντέλο που χρησιμοποιείται από το Γρήγορο Βοηθό",
"quick_assistant_selection": "Επιλογή Βοηθού",
"topic_naming_model": "Μοντέλο αναδόμησης θεμάτων",
"topic_naming_model_description": "Το μοντέλο που χρησιμοποιείται όταν αυτόματα ονομάζεται ένα νέο θέμα",

View File

@ -1463,7 +1463,7 @@
"function_calling": "Llamada a función",
"reasoning": "Razonamiento",
"rerank": "Reclasificar",
"select": "Seleccionar tipo de modelo",
"select": "Tipos de modelo",
"text": "Texto",
"vision": "Imagen",
"websearch": "Búsqueda en línea"
@ -3018,7 +3018,6 @@
"provider_name": "Nombre del proveedor",
"quick_assistant_default_tag": "Predeterminado",
"quick_assistant_model": "Modelo del asistente rápido",
"quick_assistant_model_description": "Modelo predeterminado utilizado por el asistente rápido",
"quick_assistant_selection": "Seleccionar asistente",
"topic_naming_model": "Modelo de nombramiento de temas",
"topic_naming_model_description": "Modelo utilizado para nombrar temas automáticamente",

View File

@ -1463,7 +1463,7 @@
"function_calling": "Appel de fonction",
"reasoning": "Raisonnement",
"rerank": "Reclasser",
"select": "Sélectionnez le type de modèle",
"select": "Types de modèle",
"text": "Texte",
"vision": "Image",
"websearch": "Recherche web"
@ -3018,7 +3018,6 @@
"provider_name": "Nom du fournisseur",
"quick_assistant_default_tag": "Par défaut",
"quick_assistant_model": "Modèle de l'assistant rapide",
"quick_assistant_model_description": "Modèle par défaut utilisé par l'assistant rapide",
"quick_assistant_selection": "Sélectionner l'assistant",
"topic_naming_model": "Modèle de renommage des sujets",
"topic_naming_model_description": "Modèle utilisé pour le renommage automatique des nouveaux sujets",

View File

@ -1463,7 +1463,7 @@
"function_calling": "chamada de função",
"reasoning": "raciocínio",
"rerank": "Reclassificar",
"select": "selecione o tipo de modelo",
"select": "Tipos de modelo",
"text": "texto",
"vision": "imagem",
"websearch": "Procurar na web"
@ -3018,7 +3018,6 @@
"provider_name": "Nome do Provedor",
"quick_assistant_default_tag": "Padrão",
"quick_assistant_model": "Modelo do Assistente Rápido",
"quick_assistant_model_description": "Modelo padrão usado pelo assistente rápido",
"quick_assistant_selection": "Selecionar Assistente",
"topic_naming_model": "Modelo de nomenclatura de tópicos",
"topic_naming_model_description": "Modelo usado para nomear tópicos automaticamente",

View File

@ -1,6 +1,7 @@
import { ImportOutlined, PlusOutlined } from '@ant-design/icons'
import { Navbar, NavbarCenter } from '@renderer/components/app/Navbar'
import CustomTag from '@renderer/components/CustomTag'
import { HStack } from '@renderer/components/Layout'
import ListItem from '@renderer/components/ListItem'
import Scrollbar from '@renderer/components/Scrollbar'
import { useAgents } from '@renderer/hooks/useAgents'
@ -213,11 +214,11 @@ const AgentsPage: FC = () => {
{getLocalizedGroupName(group)}
</Flex>
{
<div style={{ minWidth: 40, textAlign: 'center' }}>
<HStack alignItems="center" justifyContent="center" style={{ minWidth: 40 }}>
<CustomTag color="#A0A0A0" size={8}>
{agentGroups[group].length}
</CustomTag>
</div>
</HStack>
}
</Flex>
}

View File

@ -63,6 +63,7 @@ const FilesPage: FC = () => {
okText={t('common.confirm')}
cancelText={t('common.cancel')}
onConfirm={() => handleDelete(file.id, t)}
placement="left"
icon={<ExclamationCircleOutlined style={{ color: 'red' }} />}>
<Button type="text" danger icon={<DeleteIcon size={14} className="lucide-custom" />} />
</Popconfirm>

View File

@ -289,9 +289,12 @@ export const ItemHeader = styled.div`
align-items: center;
justify-content: space-between;
position: absolute;
top: calc(var(--navbar-height) + 14px);
right: 16px;
z-index: 1000;
top: calc(var(--navbar-height) + 12px);
[navbar-position='top'] & {
top: calc(var(--navbar-height) + 10px);
}
`
export const StatusIconWrapper = styled.div`

View File

@ -92,8 +92,8 @@ const LaunchpadPage: FC = () => {
<SectionTitle>{t('launchpad.minapps')}</SectionTitle>
<Grid>
{sortedMinapps.map((app) => (
<AppWrapper key={app.id} onClick={() => setTimeout(() => tabsService.closeTab('launchpad'), 350)}>
<App app={app} size={56} />
<AppWrapper key={app.id}>
<App app={app} size={56} onClick={() => setTimeout(() => tabsService.closeTab('launchpad'), 350)} />
</AppWrapper>
))}
</Grid>

View File

@ -633,8 +633,13 @@ const McpSettings: React.FC = () => {
</Form.Item>
</>
)}
<Form.Item name="longRunning" label={t('settings.mcp.longRunning', 'Long Running')} valuePropName="checked">
<Switch />
<Form.Item
name="longRunning"
label={t('settings.mcp.longRunning', 'Long Running')}
tooltip={t('settings.mcp.longRunningTooltip')}
layout="horizontal"
valuePropName="checked">
<Switch size="small" style={{ marginLeft: 10 }} />
</Form.Item>
<Form.Item
name="timeout"

View File

@ -1,26 +1,22 @@
import { RedoOutlined } from '@ant-design/icons'
import ModelAvatar from '@renderer/components/Avatar/ModelAvatar'
import { HStack } from '@renderer/components/Layout'
import ModelSelector from '@renderer/components/ModelSelector'
import PromptPopup from '@renderer/components/Popups/PromptPopup'
import { isEmbeddingModel, isRerankModel, isTextToImageModel } from '@renderer/config/models'
import { TRANSLATE_PROMPT } from '@renderer/config/prompts'
import { useTheme } from '@renderer/context/ThemeProvider'
import { useAssistants, useDefaultAssistant, useDefaultModel } from '@renderer/hooks/useAssistant'
import { 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 { Button, Tooltip } from 'antd'
import { find } from 'lodash'
import { CircleHelp, FolderPen, Languages, MessageSquareMore, Rocket, Settings2 } from 'lucide-react'
import { FolderPen, Languages, MessageSquareMore, Settings2 } from 'lucide-react'
import { FC, useCallback, useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
import { SettingContainer, SettingDescription, SettingGroup, SettingTitle } from '..'
import DefaultAssistantSettings from './DefaultAssistantSettings'
@ -29,8 +25,6 @@ import TopicNamingModalPopup from './TopicNamingModalPopup'
const ModelSettings: FC = () => {
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()
@ -38,7 +32,6 @@ const ModelSettings: FC = () => {
const { translateModelPrompt } = useSettings()
const dispatch = useAppDispatch()
const { quickAssistantId } = useAppSelector((state) => state.llm)
const modelPredicate = useCallback(
(m: Model) => !isEmbeddingModel(m) && !isRerankModel(m) && !isTextToImageModel(m),
@ -149,127 +142,8 @@ const ModelSettings: FC = () => {
</HStack>
<SettingDescription>{t('settings.models.translate_model_description')}</SettingDescription>
</SettingGroup>
<SettingGroup theme={theme}>
<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(''))}
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 || defaultAssistant.id}
style={{ width: 360 }}
onChange={(value) => dispatch(setQuickAssistantId(value))}
placeholder={t('settings.models.quick_assistant_selection')}>
<Select.Option key={defaultAssistant.id} value={defaultAssistant.id}>
<AssistantItem>
<ModelAvatar model={defaultAssistant.model || defaultModel} size={18} />
<AssistantName>{defaultAssistant.name}</AssistantName>
<Spacer />
<DefaultTag isCurrent={true}>{t('settings.models.quick_assistant_default_tag')}</DefaultTag>
</AssistantItem>
</Select.Option>
{assistants
.filter((a) => a.id !== defaultAssistant.id)
.map((a) => (
<Select.Option key={a.id} value={a.id}>
<AssistantItem>
<ModelAvatar model={a.model || defaultModel} size={18} />
<AssistantName>{a.name}</AssistantName>
<Spacer />
</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: 0; // 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

@ -0,0 +1,88 @@
import { TopView } from '@renderer/components/TopView'
import { useAssistants, useDefaultModel } from '@renderer/hooks/useAssistant'
import { useProvider } from '@renderer/hooks/useProvider'
import ModelEditContent from '@renderer/pages/settings/ProviderSettings/EditModelPopup/ModelEditContent'
import { useAppDispatch } from '@renderer/store'
import { setModel } from '@renderer/store/assistants'
import { Model, Provider } from '@renderer/types'
import React, { useCallback, useState } from 'react'
interface ShowParams {
provider: Provider
model: Model
}
interface Props extends ShowParams {
resolve: (data?: Model) => void
}
const PopupContainer: React.FC<Props> = ({ provider: _provider, model, resolve }) => {
const [open, setOpen] = useState(true)
const { provider, updateProvider, models } = useProvider(_provider.id)
const { assistants } = useAssistants()
const { defaultModel, setDefaultModel } = useDefaultModel()
const dispatch = useAppDispatch()
const onOk = () => {
setOpen(false)
}
const onCancel = () => {
setOpen(false)
}
const onClose = () => {
EditModelPopup.hide()
resolve(undefined)
}
const onUpdateModel = useCallback(
(updatedModel: Model) => {
const updatedModels = models.map((m) => (m.id === updatedModel.id ? updatedModel : m))
updateProvider({ models: updatedModels })
assistants.forEach((assistant) => {
if (assistant?.model?.id === updatedModel.id && assistant.model.provider === provider.id) {
dispatch(
setModel({
assistantId: assistant.id,
model: updatedModel
})
)
}
})
if (defaultModel?.id === updatedModel.id && defaultModel?.provider === provider.id) {
setDefaultModel(updatedModel)
}
},
[models, updateProvider, provider.id, assistants, defaultModel, dispatch, setDefaultModel]
)
return (
<ModelEditContent
provider={provider}
model={model}
open={open}
onOk={onOk}
onCancel={onCancel}
afterClose={onClose}
onUpdateModel={onUpdateModel}
/>
)
}
const TopViewKey = 'EditModelPopup'
export default class EditModelPopup {
static hide() {
TopView.hide(TopViewKey)
}
static show(props: ShowParams) {
return new Promise<Model | undefined>((resolve) => {
TopView.show(<PopupContainer {...props} resolve={resolve} />, TopViewKey)
})
}
}

View File

@ -11,7 +11,20 @@ import {
import { useDynamicLabelWidth } from '@renderer/hooks/useDynamicLabelWidth'
import { Model, ModelCapability, ModelType, Provider } from '@renderer/types'
import { getDefaultGroupName, getDifference, getUnion, uniqueObjectArray } from '@renderer/utils'
import { Button, Checkbox, Divider, Flex, Form, Input, InputNumber, message, Modal, Select, Switch } from 'antd'
import {
Button,
Checkbox,
Divider,
Flex,
Form,
Input,
InputNumber,
message,
Modal,
ModalProps,
Select,
Switch
} from 'antd'
import { cloneDeep } from 'lodash'
import { ChevronDown, ChevronUp, SaveIcon } from 'lucide-react'
import { FC, useEffect, useRef, useState } from 'react'
@ -22,12 +35,10 @@ interface ModelEditContentProps {
provider: Provider
model: Model
onUpdateModel: (model: Model) => void
open: boolean
onClose: () => void
}
const symbols = ['$', '¥', '€', '£']
const ModelEditContent: FC<ModelEditContentProps> = ({ provider, model, onUpdateModel, open, onClose }) => {
const ModelEditContent: FC<ModelEditContentProps & ModalProps> = ({ provider, model, onUpdateModel, ...props }) => {
const [form] = Form.useForm()
const { t } = useTranslation()
const [showMoreSettings, setShowMoreSettings] = useState(false)
@ -40,6 +51,36 @@ const ModelEditContent: FC<ModelEditContentProps> = ({ provider, model, onUpdate
const labelWidth = useDynamicLabelWidth([t('settings.models.add.endpoint_type.label')])
// 自动保存函数
const autoSave = (overrides?: {
capabilities?: ModelCapability[]
supported_text_delta?: boolean
currencySymbol?: string
isCustomCurrency?: boolean
}) => {
const formValues = form.getFieldsValue()
const currentIsCustomCurrency = overrides?.isCustomCurrency ?? isCustomCurrency
const currentCurrencySymbol = overrides?.currencySymbol ?? currencySymbol
const finalCurrencySymbol = currentIsCustomCurrency
? formValues.customCurrencySymbol || currentCurrencySymbol
: formValues.currencySymbol || currentCurrencySymbol || '$'
const updatedModel: Model = {
...model,
id: formValues.id || model.id,
name: formValues.name || model.name,
group: formValues.group || model.group,
endpoint_type: provider.id === 'new-api' ? formValues.endpointType : model.endpoint_type,
capabilities: overrides?.capabilities ?? modelCapabilities,
supported_text_delta: overrides?.supported_text_delta ?? supportedTextDelta,
pricing: {
input_per_million_tokens: Number(formValues.input_per_million_tokens) || 0,
output_per_million_tokens: Number(formValues.output_per_million_tokens) || 0,
currencySymbol: finalCurrencySymbol
}
}
onUpdateModel(updatedModel)
}
const onFinish = (values: any) => {
const finalCurrencySymbol = isCustomCurrency ? values.customCurrencySymbol : values.currencySymbol
const updatedModel: Model = {
@ -58,13 +99,7 @@ const ModelEditContent: FC<ModelEditContentProps> = ({ provider, model, onUpdate
}
onUpdateModel(updatedModel)
setShowMoreSettings(false)
onClose()
}
const handleClose = () => {
setShowMoreSettings(false)
setModelCapabilities(model.capabilities || [])
onClose()
props.onOk?.(undefined as any)
}
const currencyOptions = [
@ -107,21 +142,171 @@ const ModelEditContent: FC<ModelEditContentProps> = ({ provider, model, onUpdate
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [showMoreSettings])
return (
<Modal
title={t('models.edit')}
open={open}
onCancel={handleClose}
footer={null}
transitionName="animation-move-down"
centered
afterOpenChange={(visible) => {
if (visible) {
form.getFieldInstance('id')?.focus()
// 监听modelCapabilities变化自动保存但跳过初始化时的保存
useEffect(() => {
if (hasUserModified && showMoreSettings) {
autoSave()
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [modelCapabilities])
const ModelCapability = () => {
const isDisabled = selectedTypes.includes('rerank') || selectedTypes.includes('embedding')
const isRerankDisabled = selectedTypes.includes('embedding')
const isEmbeddingDisabled = selectedTypes.includes('rerank')
const showTypeConfirmModal = (newCapability: ModelCapability) => {
const onUpdateType = selectedTypes?.find((t) => t === newCapability.type)
window.modal.confirm({
title: t('settings.moresetting.warn'),
content: t('settings.moresetting.check.warn'),
okText: t('settings.moresetting.check.confirm'),
cancelText: t('common.cancel'),
okButtonProps: { danger: true },
cancelButtonProps: { type: 'primary' },
onOk: () => {
if (onUpdateType) {
const updatedModelCapabilities = modelCapabilities?.map((t) => {
if (t.type === newCapability.type) {
return { ...t, isUserSelected: true }
}
if (
((onUpdateType !== t.type && onUpdateType === 'rerank') ||
(onUpdateType === 'embedding' && onUpdateType !== t.type)) &&
t.isUserSelected !== false
) {
changedTypesRef.current.push(t.type)
return { ...t, isUserSelected: false }
}
return t
})
setModelCapabilities(uniqueObjectArray(updatedModelCapabilities as ModelCapability[]))
} else {
const updatedModelCapabilities = modelCapabilities?.map((t) => {
if (
((newCapability.type !== t.type && newCapability.type === 'rerank') ||
(newCapability.type === 'embedding' && newCapability.type !== t.type)) &&
t.isUserSelected !== false
) {
changedTypesRef.current.push(t.type)
return { ...t, isUserSelected: false }
}
if (newCapability.type === t.type) {
return { ...t, isUserSelected: true }
}
return t
})
updatedModelCapabilities.push(newCapability as any)
setModelCapabilities(uniqueObjectArray(updatedModelCapabilities as ModelCapability[]))
}
},
onCancel: () => {},
centered: true
})
}
const handleTypeChange = (types: string[]) => {
setHasUserModified(true) // 标记用户已进行修改
const diff = types.length > selectedTypes.length
if (diff) {
const newCapability = getDifference(types, selectedTypes) // checkbox的特性确保了newCapability只有一个元素
showTypeConfirmModal({
type: newCapability[0] as ModelType,
isUserSelected: true
})
} else {
const disabledTypes = getDifference(selectedTypes, types)
const onUpdateType = modelCapabilities?.find((t) => t.type === disabledTypes[0])
if (onUpdateType) {
const updatedTypes = modelCapabilities?.map((t) => {
if (t.type === disabledTypes[0]) {
return { ...t, isUserSelected: false }
}
if (
((onUpdateType !== t && onUpdateType.type === 'rerank') ||
(onUpdateType.type === 'embedding' && onUpdateType !== t)) &&
t.isUserSelected === false
) {
if (changedTypesRef.current.includes(t.type)) {
return { ...t, isUserSelected: true }
}
}
return t
})
setModelCapabilities(uniqueObjectArray(updatedTypes as ModelCapability[]))
} else {
setShowMoreSettings(false)
const updatedModelCapabilities = modelCapabilities?.map((t) => {
if (
(disabledTypes[0] === 'rerank' && t.type !== 'rerank') ||
(disabledTypes[0] === 'embedding' && t.type !== 'embedding' && t.isUserSelected === false)
) {
return { ...t, isUserSelected: true }
}
return t
})
updatedModelCapabilities.push({ type: disabledTypes[0] as ModelType, isUserSelected: false })
setModelCapabilities(uniqueObjectArray(updatedModelCapabilities as ModelCapability[]))
}
}}>
changedTypesRef.current.length = 0
}
}
const handleResetTypes = () => {
setModelCapabilities(originalModelCapabilities)
setHasUserModified(false) // 重置后清除修改标志
}
return (
<div>
<Flex justify="space-between" align="center" style={{ marginBottom: 8 }}>
<Checkbox.Group
value={selectedTypes}
onChange={handleTypeChange}
options={[
{
label: t('models.type.vision'),
value: 'vision',
disabled: isDisabled
},
{
label: t('models.type.websearch'),
value: 'web_search',
disabled: isDisabled
},
{
label: t('models.type.rerank'),
value: 'rerank',
disabled: isRerankDisabled
},
{
label: t('models.type.embedding'),
value: 'embedding',
disabled: isEmbeddingDisabled
},
{
label: t('models.type.reasoning'),
value: 'reasoning',
disabled: isDisabled
},
{
label: t('models.type.function_calling'),
value: 'function_calling',
disabled: isDisabled
}
]}
/>
{hasUserModified && (
<Button size="small" onClick={handleResetTypes}>
{t('common.reset')}
</Button>
)}
</Flex>
</div>
)
}
return (
<Modal title={t('models.edit')} footer={null} transitionName="animation-move-down" centered {...props}>
<Form
form={form}
labelCol={{ flex: provider.id === 'new-api' ? labelWidth : '110px' }}
@ -160,17 +345,18 @@ const ModelEditContent: FC<ModelEditContentProps> = ({ provider, model, onUpdate
form.setFieldValue('name', value)
form.setFieldValue('group', getDefaultGroupName(value))
}}
suffix={
<CopyIcon
size={14}
style={{ cursor: 'pointer' }}
onClick={() => {
const val = form.getFieldValue('name')
navigator.clipboard.writeText((val.id || model.id) as string)
message.success(t('message.copied'))
}}
/>
}
/>
<Button
onClick={() => {
//copy model id
const val = form.getFieldValue('name')
navigator.clipboard.writeText((val.id || model.id) as string)
message.success(t('message.copied'))
}}
icon={<CopyIcon size={16} />}>
{t('chat.topics.copy.title')}
</Button>
</Flex>
</Form.Item>
<Form.Item
@ -219,179 +405,49 @@ const ModelEditContent: FC<ModelEditContentProps> = ({ provider, model, onUpdate
{showMoreSettings && (
<div style={{ marginBottom: 8 }}>
<Divider style={{ margin: '16px 0 16px 0' }} />
<TypeTitle>{t('models.type.select')}:</TypeTitle>
{(() => {
const isDisabled = selectedTypes.includes('rerank') || selectedTypes.includes('embedding')
const isRerankDisabled = selectedTypes.includes('embedding')
const isEmbeddingDisabled = selectedTypes.includes('rerank')
const showTypeConfirmModal = (newCapability: ModelCapability) => {
const onUpdateType = selectedTypes?.find((t) => t === newCapability.type)
window.modal.confirm({
title: t('settings.moresetting.warn'),
content: t('settings.moresetting.check.warn'),
okText: t('settings.moresetting.check.confirm'),
cancelText: t('common.cancel'),
okButtonProps: { danger: true },
cancelButtonProps: { type: 'primary' },
onOk: () => {
if (onUpdateType) {
const updatedModelCapabilities = modelCapabilities?.map((t) => {
if (t.type === newCapability.type) {
return { ...t, isUserSelected: true }
}
if (
((onUpdateType !== t.type && onUpdateType === 'rerank') ||
(onUpdateType === 'embedding' && onUpdateType !== t.type)) &&
t.isUserSelected !== false
) {
changedTypesRef.current.push(t.type)
return { ...t, isUserSelected: false }
}
return t
})
setModelCapabilities(uniqueObjectArray(updatedModelCapabilities as ModelCapability[]))
} else {
const updatedModelCapabilities = modelCapabilities?.map((t) => {
if (
((newCapability.type !== t.type && newCapability.type === 'rerank') ||
(newCapability.type === 'embedding' && newCapability.type !== t.type)) &&
t.isUserSelected !== false
) {
changedTypesRef.current.push(t.type)
return { ...t, isUserSelected: false }
}
if (newCapability.type === t.type) {
return { ...t, isUserSelected: true }
}
return t
})
updatedModelCapabilities.push(newCapability as any)
setModelCapabilities(uniqueObjectArray(updatedModelCapabilities as ModelCapability[]))
}
},
onCancel: () => {},
centered: true
})
}
const handleTypeChange = (types: string[]) => {
setHasUserModified(true) // 标记用户已进行修改
const diff = types.length > selectedTypes.length
if (diff) {
const newCapability = getDifference(types, selectedTypes) // checkbox的特性确保了newCapability只有一个元素
showTypeConfirmModal({
type: newCapability[0] as ModelType,
isUserSelected: true
})
} else {
const disabledTypes = getDifference(selectedTypes, types)
const onUpdateType = modelCapabilities?.find((t) => t.type === disabledTypes[0])
if (onUpdateType) {
const updatedTypes = modelCapabilities?.map((t) => {
if (t.type === disabledTypes[0]) {
return { ...t, isUserSelected: false }
}
if (
((onUpdateType !== t && onUpdateType.type === 'rerank') ||
(onUpdateType.type === 'embedding' && onUpdateType !== t)) &&
t.isUserSelected === false
) {
if (changedTypesRef.current.includes(t.type)) {
return { ...t, isUserSelected: true }
}
}
return t
})
setModelCapabilities(uniqueObjectArray(updatedTypes as ModelCapability[]))
} else {
const updatedModelCapabilities = modelCapabilities?.map((t) => {
if (
(disabledTypes[0] === 'rerank' && t.type !== 'rerank') ||
(disabledTypes[0] === 'embedding' && t.type !== 'embedding' && t.isUserSelected === false)
) {
return { ...t, isUserSelected: true }
}
return t
})
updatedModelCapabilities.push({ type: disabledTypes[0] as ModelType, isUserSelected: false })
setModelCapabilities(uniqueObjectArray(updatedModelCapabilities as ModelCapability[]))
}
changedTypesRef.current.length = 0
}
}
const handleResetTypes = () => {
setModelCapabilities(originalModelCapabilities)
setHasUserModified(false) // 重置后清除修改标志
}
return (
<div>
<Flex justify="space-between" align="center" style={{ marginBottom: 8 }}>
<Checkbox.Group
value={selectedTypes}
onChange={handleTypeChange}
options={[
{
label: t('models.type.vision'),
value: 'vision',
disabled: isDisabled
},
{
label: t('models.type.websearch'),
value: 'web_search',
disabled: isDisabled
},
{
label: t('models.type.rerank'),
value: 'rerank',
disabled: isRerankDisabled
},
{
label: t('models.type.embedding'),
value: 'embedding',
disabled: isEmbeddingDisabled
},
{
label: t('models.type.reasoning'),
value: 'reasoning',
disabled: isDisabled
},
{
label: t('models.type.function_calling'),
value: 'function_calling',
disabled: isDisabled
}
]}
/>
{hasUserModified && (
<Button size="small" onClick={handleResetTypes}>
{t('common.reset')}
</Button>
)}
</Flex>
</div>
)
})()}
<TypeTitle>{t('models.type.select')}</TypeTitle>
<ModelCapability />
<Divider style={{ margin: '16px 0 12px 0' }} />
<Form.Item
name="supported_text_delta"
style={{ marginBottom: 10 }}
labelCol={{ flex: 1 }}
label={t('settings.models.add.supported_text_delta.label')}
tooltip={t('settings.models.add.supported_text_delta.tooltip')}>
<Switch checked={supportedTextDelta} onChange={(checked) => setSupportedTextDelta(checked)} />
<Switch
checked={supportedTextDelta}
style={{ marginLeft: 'auto' }}
size="small"
onChange={(checked) => {
setSupportedTextDelta(checked)
// 直接传递新值给autoSave
autoSave({ supported_text_delta: checked })
}}
/>
</Form.Item>
<TypeTitle>{t('models.price.price')}</TypeTitle>
<Divider style={{ margin: '12px 0 16px 0' }} />
<Form.Item name="currencySymbol" label={t('models.price.currency')} style={{ marginBottom: 10 }}>
<Select
style={{ width: '100px' }}
options={currencyOptions}
onChange={(value) => {
if (value === 'custom') {
const customSymbol = form.getFieldValue('customCurrencySymbol') || ''
setIsCustomCurrency(true)
setCurrencySymbol(form.getFieldValue('customCurrencySymbol') || '')
setCurrencySymbol(customSymbol)
// 自动保存
autoSave({
isCustomCurrency: true,
currencySymbol: customSymbol
})
} else {
setIsCustomCurrency(false)
setCurrencySymbol(value)
// 自动保存
autoSave({
isCustomCurrency: false,
currencySymbol: value
})
}
}}
dropdownMatchSelectWidth={false}
@ -409,12 +465,20 @@ const ModelEditContent: FC<ModelEditContentProps> = ({ provider, model, onUpdate
placeholder={t('models.price.custom_currency_placeholder')}
defaultValue={model.pricing?.currencySymbol}
maxLength={5}
onChange={(e) => setCurrencySymbol(e.target.value)}
onChange={(e) => {
const newValue = e.target.value
setCurrencySymbol(newValue)
// 自动保存
autoSave({
currencySymbol: newValue,
isCustomCurrency: true
})
}}
/>
</Form.Item>
)}
<Form.Item label={t('models.price.input')} name="input_per_million_tokens">
<Form.Item label={t('models.price.input')} style={{ marginBottom: 10 }} name="input_per_million_tokens">
<InputNumber
placeholder="0.00"
defaultValue={model.pricing?.input_per_million_tokens}
@ -423,9 +487,13 @@ const ModelEditContent: FC<ModelEditContentProps> = ({ provider, model, onUpdate
precision={2}
style={{ width: '240px' }}
addonAfter={`${currencySymbol} / ${t('models.price.million_tokens')}`}
onChange={() => {
// 自动保存
autoSave()
}}
/>
</Form.Item>
<Form.Item label={t('models.price.output')} name="output_per_million_tokens">
<Form.Item label={t('models.price.output')} style={{ marginBottom: 10 }} name="output_per_million_tokens">
<InputNumber
placeholder="0.00"
defaultValue={model.pricing?.output_per_million_tokens}
@ -434,6 +502,10 @@ const ModelEditContent: FC<ModelEditContentProps> = ({ provider, model, onUpdate
precision={2}
style={{ width: '240px' }}
addonAfter={`${currencySymbol} / ${t('models.price.million_tokens')}`}
onChange={() => {
// 自动保存
autoSave()
}}
/>
</Form.Item>
</div>

View File

@ -0,0 +1,182 @@
import { ModelCapability, ModelType } from '@renderer/types'
import { getDifference, uniqueObjectArray } from '@renderer/utils'
import { Button, Checkbox, Flex } from 'antd'
import { FC, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
interface ModelTypeSelectorProps {
modelCapabilities: ModelCapability[]
originalModelCapabilities: ModelCapability[]
selectedTypes: string[]
onCapabilitiesChange: (capabilities: ModelCapability[]) => void
onUserModified: (modified: boolean) => void
}
const ModelTypeSelector: FC<ModelTypeSelectorProps> = ({
modelCapabilities,
originalModelCapabilities,
selectedTypes,
onCapabilitiesChange,
onUserModified
}) => {
const { t } = useTranslation()
const [hasUserModified, setHasUserModified] = useState(false)
const changedTypesRef = useRef<string[]>([])
const isDisabled = selectedTypes.includes('rerank') || selectedTypes.includes('embedding')
const isRerankDisabled = selectedTypes.includes('embedding')
const isEmbeddingDisabled = selectedTypes.includes('rerank')
const showTypeConfirmModal = (newCapability: ModelCapability) => {
const onUpdateType = selectedTypes?.find((t) => t === newCapability.type)
window.modal.confirm({
title: t('settings.moresetting.warn'),
content: t('settings.moresetting.check.warn'),
okText: t('settings.moresetting.check.confirm'),
cancelText: t('common.cancel'),
okButtonProps: { danger: true },
cancelButtonProps: { type: 'primary' },
onOk: () => {
if (onUpdateType) {
const updatedModelCapabilities = modelCapabilities?.map((t) => {
if (t.type === newCapability.type) {
return { ...t, isUserSelected: true }
}
if (
((onUpdateType !== t.type && onUpdateType === 'rerank') ||
(onUpdateType === 'embedding' && onUpdateType !== t.type)) &&
t.isUserSelected !== false
) {
changedTypesRef.current.push(t.type)
return { ...t, isUserSelected: false }
}
return t
})
onCapabilitiesChange(uniqueObjectArray(updatedModelCapabilities as ModelCapability[]))
} else {
const updatedModelCapabilities = modelCapabilities?.map((t) => {
if (
((newCapability.type !== t.type && newCapability.type === 'rerank') ||
(newCapability.type === 'embedding' && newCapability.type !== t.type)) &&
t.isUserSelected !== false
) {
changedTypesRef.current.push(t.type)
return { ...t, isUserSelected: false }
}
if (newCapability.type === t.type) {
return { ...t, isUserSelected: true }
}
return t
})
updatedModelCapabilities.push(newCapability as any)
onCapabilitiesChange(uniqueObjectArray(updatedModelCapabilities as ModelCapability[]))
}
},
onCancel: () => {},
centered: true
})
}
const handleTypeChange = (types: string[]) => {
setHasUserModified(true)
onUserModified(true)
const diff = types.length > selectedTypes.length
if (diff) {
const newCapability = getDifference(types, selectedTypes) // checkbox的特性确保了newCapability只有一个元素
showTypeConfirmModal({
type: newCapability[0] as ModelType,
isUserSelected: true
})
} else {
const disabledTypes = getDifference(selectedTypes, types)
const onUpdateType = modelCapabilities?.find((t) => t.type === disabledTypes[0])
if (onUpdateType) {
const updatedTypes = modelCapabilities?.map((t) => {
if (t.type === disabledTypes[0]) {
return { ...t, isUserSelected: false }
}
if (
((onUpdateType !== t && onUpdateType.type === 'rerank') ||
(onUpdateType.type === 'embedding' && onUpdateType !== t)) &&
t.isUserSelected === false
) {
if (changedTypesRef.current.includes(t.type)) {
return { ...t, isUserSelected: true }
}
}
return t
})
onCapabilitiesChange(uniqueObjectArray(updatedTypes as ModelCapability[]))
} else {
const updatedModelCapabilities = modelCapabilities?.map((t) => {
if (
(disabledTypes[0] === 'rerank' && t.type !== 'rerank') ||
(disabledTypes[0] === 'embedding' && t.type !== 'embedding' && t.isUserSelected === false)
) {
return { ...t, isUserSelected: true }
}
return t
})
updatedModelCapabilities.push({ type: disabledTypes[0] as ModelType, isUserSelected: false })
onCapabilitiesChange(uniqueObjectArray(updatedModelCapabilities as ModelCapability[]))
}
changedTypesRef.current.length = 0
}
}
const handleResetTypes = () => {
onCapabilitiesChange(originalModelCapabilities)
setHasUserModified(false)
onUserModified(false)
}
return (
<div>
<Flex justify="space-between" align="center" style={{ marginBottom: 8 }}>
<Checkbox.Group
value={selectedTypes}
onChange={handleTypeChange}
options={[
{
label: t('models.type.vision'),
value: 'vision',
disabled: isDisabled
},
{
label: t('models.type.websearch'),
value: 'web_search',
disabled: isDisabled
},
{
label: t('models.type.rerank'),
value: 'rerank',
disabled: isRerankDisabled
},
{
label: t('models.type.embedding'),
value: 'embedding',
disabled: isEmbeddingDisabled
},
{
label: t('models.type.reasoning'),
value: 'reasoning',
disabled: isDisabled
},
{
label: t('models.type.function_calling'),
value: 'function_calling',
disabled: isDisabled
}
]}
/>
{hasUserModified && (
<Button size="small" onClick={handleResetTypes}>
{t('common.reset')}
</Button>
)}
</Flex>
</div>
)
}
export default ModelTypeSelector

View File

@ -1,10 +1,10 @@
import CustomTag from '@renderer/components/CustomTag'
import ExpandableText from '@renderer/components/ExpandableText'
import ModelIdWithTags from '@renderer/components/ModelIdWithTags'
import NewApiBatchAddModelPopup from '@renderer/components/ModelList/NewApiBatchAddModelPopup'
import { DynamicVirtualList } from '@renderer/components/VirtualList'
import { getModelLogo } from '@renderer/config/models'
import FileItem from '@renderer/pages/files/FileItem'
import NewApiBatchAddModelPopup from '@renderer/pages/settings/ProviderSettings/ModelList/NewApiBatchAddModelPopup'
import { Model, Provider } from '@renderer/types'
import { Button, Flex, Tooltip } from 'antd'
import { Avatar } from 'antd'
@ -221,7 +221,7 @@ const GroupHeader = styled.div<{ isCollapsed: boolean }>`
align-items: center;
justify-content: space-between;
padding: 0 13px;
min-height: 35px;
min-height: 38px;
color: var(--color-text);
cursor: pointer;
`

View File

@ -1,7 +1,5 @@
import { loggerService } from '@logger'
import { LoadingIcon } from '@renderer/components/Icons'
import NewApiAddModelPopup from '@renderer/components/ModelList/NewApiAddModelPopup'
import NewApiBatchAddModelPopup from '@renderer/components/ModelList/NewApiBatchAddModelPopup'
import { TopView } from '@renderer/components/TopView'
import {
groupQwenModels,
@ -15,6 +13,8 @@ import {
SYSTEM_MODELS
} from '@renderer/config/models'
import { useProvider } from '@renderer/hooks/useProvider'
import NewApiAddModelPopup from '@renderer/pages/settings/ProviderSettings/ModelList/NewApiAddModelPopup'
import NewApiBatchAddModelPopup from '@renderer/pages/settings/ProviderSettings/ModelList/NewApiBatchAddModelPopup'
import { fetchModels } from '@renderer/services/ApiService'
import { Model, Provider } from '@renderer/types'
import { filterModelsByKeywords, getDefaultGroupName, getFancyProviderName, isFreeModel } from '@renderer/utils'
@ -27,7 +27,7 @@ import { useCallback, useEffect, useMemo, useOptimistic, useRef, useState, useTr
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
import { HStack } from '../Layout'
import { HStack } from '../../../../components/Layout'
import ManageModelsList from './ManageModelsList'
import { isModelInProvider, isValidNewApiModel } from './utils'

View File

@ -2,17 +2,14 @@ import CollapsibleSearchBar from '@renderer/components/CollapsibleSearchBar'
import CustomTag from '@renderer/components/CustomTag'
import { LoadingIcon, StreamlineGoodHealthAndWellBeing } from '@renderer/components/Icons'
import { HStack } from '@renderer/components/Layout'
import AddModelPopup from '@renderer/components/ModelList/AddModelPopup'
import EditModelPopup from '@renderer/components/ModelList/EditModelPopup'
import ManageModelsPopup from '@renderer/components/ModelList/ManageModelsPopup'
import NewApiAddModelPopup from '@renderer/components/ModelList/NewApiAddModelPopup'
import { PROVIDER_CONFIG } from '@renderer/config/providers'
import { useAssistants, useDefaultModel } from '@renderer/hooks/useAssistant'
import { useProvider } from '@renderer/hooks/useProvider'
import { getProviderLabel } from '@renderer/i18n/label'
import { SettingHelpLink, SettingHelpText, SettingHelpTextRow, SettingSubtitle } from '@renderer/pages/settings'
import { useAppDispatch } from '@renderer/store'
import { setModel } from '@renderer/store/assistants'
import EditModelPopup from '@renderer/pages/settings/ProviderSettings/EditModelPopup/EditModelPopup'
import AddModelPopup from '@renderer/pages/settings/ProviderSettings/ModelList/AddModelPopup'
import ManageModelsPopup from '@renderer/pages/settings/ProviderSettings/ModelList/ManageModelsPopup'
import NewApiAddModelPopup from '@renderer/pages/settings/ProviderSettings/ModelList/NewApiAddModelPopup'
import { Model } from '@renderer/types'
import { filterModelsByKeywords } from '@renderer/utils'
import { Button, Empty, Flex, Spin, Tooltip } from 'antd'
@ -47,11 +44,8 @@ const calculateModelGroups = (models: Model[], searchText: string): ModelGroups
* CRUD
*/
const ModelList: React.FC<ModelListProps> = ({ providerId }) => {
const dispatch = useAppDispatch()
const { t } = useTranslation()
const { provider, updateProvider, models, removeModel } = useProvider(providerId)
const { assistants } = useAssistants()
const { defaultModel, setDefaultModel } = useDefaultModel()
const { provider, models, removeModel } = useProvider(providerId)
const providerConfig = PROVIDER_CONFIG[provider.id]
const docsWebsite = providerConfig?.websites?.docs
@ -99,40 +93,6 @@ const ModelList: React.FC<ModelListProps> = ({ providerId }) => {
}
}, [provider, t])
const onUpdateModel = useCallback(
(updatedModel: Model) => {
const updatedModels = models.map((m) => (m.id === updatedModel.id ? updatedModel : m))
updateProvider({ models: updatedModels })
assistants.forEach((assistant) => {
if (assistant?.model?.id === updatedModel.id && assistant.model.provider === provider.id) {
dispatch(
setModel({
assistantId: assistant.id,
model: updatedModel
})
)
}
})
if (defaultModel?.id === updatedModel.id && defaultModel?.provider === provider.id) {
setDefaultModel(updatedModel)
}
},
[models, updateProvider, provider.id, assistants, defaultModel, dispatch, setDefaultModel]
)
const onEditModel = useCallback(
async (model: Model) => {
const updatedModel = await EditModelPopup.show({ provider, model })
if (updatedModel) {
onUpdateModel(updatedModel)
}
},
[provider, onUpdateModel]
)
const isLoading = useMemo(() => displayedModelGroups === null, [displayedModelGroups])
return (
@ -170,7 +130,7 @@ const ModelList: React.FC<ModelListProps> = ({ providerId }) => {
modelStatuses={modelStatuses}
defaultOpen={i <= 5}
disabled={isHealthChecking}
onEditModel={onEditModel}
onEditModel={(model) => EditModelPopup.show({ provider, model })}
onRemoveModel={removeModel}
onRemoveGroup={() => displayedModelGroups[group].forEach((model) => removeModel(model))}
/>

View File

@ -1,5 +1,4 @@
import { type HealthResult, HealthStatusIndicator } from '@renderer/components/HealthStatusIndicator'
import { EditIcon } from '@renderer/components/Icons'
import { HStack } from '@renderer/components/Layout'
import ModelIdWithTags from '@renderer/components/ModelIdWithTags'
import { getModelLogo } from '@renderer/config/models'
@ -7,7 +6,7 @@ import { Model } from '@renderer/types'
import { ModelWithStatus } from '@renderer/types/healthCheck'
import { maskApiKey } from '@renderer/utils/api'
import { Avatar, Button, Tooltip } from 'antd'
import { Minus } from 'lucide-react'
import { Bolt, Minus } from 'lucide-react'
import React, { memo } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
@ -52,7 +51,7 @@ const ModelListItem: React.FC<ModelListItemProps> = ({ ref, model, modelStatus,
<HealthStatusIndicator results={healthResults} loading={isChecking} showLatency />
<HStack alignItems="center" gap={0}>
<Tooltip title={t('models.edit')} mouseLeaveDelay={0}>
<Button type="text" onClick={() => onEdit(model)} disabled={disabled} icon={<EditIcon size={14} />} />
<Button type="text" onClick={() => onEdit(model)} disabled={disabled} icon={<Bolt size={14} />} />
</Tooltip>
<Tooltip title={t('settings.models.manage.remove_model')} mouseLeaveDelay={0}>
<Button type="text" onClick={() => onRemove(model)} disabled={disabled} icon={<Minus size={14} />} />

View File

@ -1,8 +1,8 @@
export { default as EditModelPopup } from '../EditModelPopup/EditModelPopup'
export { default as ModelEditContent } from '../EditModelPopup/ModelEditContent'
export { default as AddModelPopup } from './AddModelPopup'
export { default as EditModelPopup } from './EditModelPopup'
export { default as HealthCheckPopup } from './HealthCheckPopup'
export { default as ManageModelsPopup } from './ManageModelsPopup'
export { default as ModelEditContent } from './ModelEditContent'
export { default as ModelList } from './ModelList'
export { default as NewApiAddModelPopup } from './NewApiAddModelPopup'
export { default as NewApiBatchAddModelPopup } from './NewApiBatchAddModelPopup'

View File

@ -1,13 +1,13 @@
import OpenAIAlert from '@renderer/components/Alert/OpenAIAlert'
import { LoadingIcon } from '@renderer/components/Icons'
import { HStack } from '@renderer/components/Layout'
import { ModelList } from '@renderer/components/ModelList'
import { ApiKeyListPopup } from '@renderer/components/Popups/ApiKeyListPopup'
import { isEmbeddingModel, isRerankModel } from '@renderer/config/models'
import { PROVIDER_CONFIG } from '@renderer/config/providers'
import { useTheme } from '@renderer/context/ThemeProvider'
import { useAllProviders, useProvider, useProviders } from '@renderer/hooks/useProvider'
import i18n from '@renderer/i18n'
import { ModelList } from '@renderer/pages/settings/ProviderSettings/ModelList'
import { checkApi } from '@renderer/services/ApiService'
import { isProviderSupportAuth } from '@renderer/services/ProviderService'
import { ApiKeyConnectivity, HealthStatus } from '@renderer/types/healthCheck'

View File

@ -1,14 +1,18 @@
import { InfoCircleOutlined } from '@ant-design/icons'
import ModelAvatar from '@renderer/components/Avatar/ModelAvatar'
import { HStack } from '@renderer/components/Layout'
import { useTheme } from '@renderer/context/ThemeProvider'
import { useAssistants, useDefaultAssistant, useDefaultModel } from '@renderer/hooks/useAssistant'
import { useSettings } from '@renderer/hooks/useSettings'
import { useAppDispatch } from '@renderer/store'
import { useAppDispatch, useAppSelector } from '@renderer/store'
import { setQuickAssistantId } from '@renderer/store/llm'
import {
setClickTrayToShowQuickAssistant,
setEnableQuickAssistant,
setReadClipboardAtStartup
} from '@renderer/store/settings'
import HomeWindow from '@renderer/windows/mini/home/HomeWindow'
import { Switch, Tooltip } from 'antd'
import { Button, Select, Switch, Tooltip } from 'antd'
import { FC } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
@ -20,6 +24,10 @@ const QuickAssistantSettings: FC = () => {
const { theme } = useTheme()
const { enableQuickAssistant, clickTrayToShowQuickAssistant, setTray, readClipboardAtStartup } = useSettings()
const dispatch = useAppDispatch()
const { assistants } = useAssistants()
const { quickAssistantId } = useAppSelector((state) => state.llm)
const { defaultAssistant } = useDefaultAssistant()
const { defaultModel } = useDefaultModel()
const handleEnableQuickAssistant = async (enable: boolean) => {
dispatch(setEnableQuickAssistant(enable))
@ -86,9 +94,69 @@ const QuickAssistantSettings: FC = () => {
</>
)}
</SettingGroup>
{enableQuickAssistant && (
<SettingGroup theme={theme}>
<HStack alignItems="center" justifyContent="space-between">
<HStack alignItems="center" gap={10}>
{t('settings.models.quick_assistant_model')}
<Tooltip title={t('selection.settings.user_modal.model.tooltip')} arrow>
<InfoCircleOutlined style={{ cursor: 'pointer' }} />
</Tooltip>
<Spacer />
</HStack>
<HStack alignItems="center" gap={10}>
{!quickAssistantId ? null : (
<HStack alignItems="center">
<Select
value={quickAssistantId || defaultAssistant.id}
style={{ width: 300, height: 34 }}
onChange={(value) => dispatch(setQuickAssistantId(value))}
placeholder={t('settings.models.quick_assistant_selection')}>
<Select.Option key={defaultAssistant.id} value={defaultAssistant.id}>
<AssistantItem>
<ModelAvatar model={defaultAssistant.model || defaultModel} size={18} />
<AssistantName>{defaultAssistant.name}</AssistantName>
<Spacer />
<DefaultTag isCurrent={true}>{t('settings.models.quick_assistant_default_tag')}</DefaultTag>
</AssistantItem>
</Select.Option>
{assistants
.filter((a) => a.id !== defaultAssistant.id)
.map((a) => (
<Select.Option key={a.id} value={a.id}>
<AssistantItem>
<ModelAvatar model={a.model || defaultModel} size={18} />
<AssistantName>{a.name}</AssistantName>
<Spacer />
</AssistantItem>
</Select.Option>
))}
</Select>
</HStack>
)}
<HStack alignItems="center" gap={0}>
<StyledButton
type={quickAssistantId ? 'primary' : 'default'}
onClick={() => {
dispatch(setQuickAssistantId(defaultAssistant.id))
}}
selected={!!quickAssistantId}>
{t('settings.models.use_assistant')}
</StyledButton>
<StyledButton
type={!quickAssistantId ? 'primary' : 'default'}
onClick={() => dispatch(setQuickAssistantId(''))}
selected={!quickAssistantId}>
{t('settings.models.use_model')}
</StyledButton>
</HStack>
</HStack>
</HStack>
</SettingGroup>
)}
{enableQuickAssistant && (
<AssistantContainer>
<HomeWindow />
<HomeWindow draggable={false} />
</AssistantContainer>
)}
</SettingContainer>
@ -105,4 +173,58 @@ const AssistantContainer = styled.div`
overflow: hidden;
`
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;
`
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: 0; // 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)'};
}
`
export default QuickAssistantSettings

View File

@ -91,7 +91,7 @@ const SelectionAssistantSettings: FC = () => {
return (
<SettingContainer theme={theme}>
<SettingGroup>
<SettingGroup theme={theme}>
<Row align="middle">
<SettingTitle>{t('selection.name')}</SettingTitle>
<Spacer />
@ -124,11 +124,9 @@ const SelectionAssistantSettings: FC = () => {
</SettingGroup>
{selectionEnabled && (
<>
<SettingGroup>
<SettingGroup theme={theme}>
<SettingTitle>{t('selection.settings.toolbar.title')}</SettingTitle>
<SettingDivider />
<SettingRow>
<SettingLabel>
<SettingRowTitle>
@ -167,9 +165,7 @@ const SelectionAssistantSettings: FC = () => {
</Tooltip>
</Radio.Group>
</SettingRow>
<SettingDivider />
<SettingRow>
<SettingLabel>
<SettingRowTitle>{t('selection.settings.toolbar.compact_mode.title')}</SettingRowTitle>
@ -179,11 +175,9 @@ const SelectionAssistantSettings: FC = () => {
</SettingRow>
</SettingGroup>
<SettingGroup>
<SettingGroup theme={theme}>
<SettingTitle>{t('selection.settings.window.title')}</SettingTitle>
<SettingDivider />
<SettingRow>
<SettingLabel>
<SettingRowTitle>{t('selection.settings.window.follow_toolbar.title')}</SettingRowTitle>
@ -191,9 +185,7 @@ const SelectionAssistantSettings: FC = () => {
</SettingLabel>
<Switch checked={isFollowToolbar} onChange={(checked) => setIsFollowToolbar(checked)} />
</SettingRow>
<SettingDivider />
<SettingRow>
<SettingLabel>
<SettingRowTitle>{t('selection.settings.window.remember_size.title')}</SettingRowTitle>
@ -201,9 +193,7 @@ const SelectionAssistantSettings: FC = () => {
</SettingLabel>
<Switch checked={isRemeberWinSize} onChange={(checked) => setIsRemeberWinSize(checked)} />
</SettingRow>
<SettingDivider />
<SettingRow>
<SettingLabel>
<SettingRowTitle>{t('selection.settings.window.auto_close.title')}</SettingRowTitle>
@ -211,9 +201,7 @@ const SelectionAssistantSettings: FC = () => {
</SettingLabel>
<Switch checked={isAutoClose} onChange={(checked) => setIsAutoClose(checked)} />
</SettingRow>
<SettingDivider />
<SettingRow>
<SettingLabel>
<SettingRowTitle>{t('selection.settings.window.auto_pin.title')}</SettingRowTitle>
@ -221,9 +209,7 @@ const SelectionAssistantSettings: FC = () => {
</SettingLabel>
<Switch checked={isAutoPin} onChange={(checked) => setIsAutoPin(checked)} />
</SettingRow>
<SettingDivider />
<SettingRow>
<SettingLabel>
<SettingRowTitle>{t('selection.settings.window.opacity.title')}</SettingRowTitle>
@ -245,11 +231,9 @@ const SelectionAssistantSettings: FC = () => {
<SelectionActionsList actionItems={actionItems} setActionItems={setActionItems} />
<SettingGroup>
<SettingGroup theme={theme}>
<SettingTitle>{t('selection.settings.advanced.title')}</SettingTitle>
<SettingDivider />
<SettingRow>
<SettingLabel>
<SettingRowTitle>{t('selection.settings.advanced.filter_mode.title')}</SettingRowTitle>
@ -277,7 +261,6 @@ const SelectionAssistantSettings: FC = () => {
{t('common.edit')}
</Button>
</SettingRow>
<SelectionFilterListModal
open={isFilterListModalOpen}
onClose={() => setIsFilterListModalOpen(false)}

View File

@ -1,4 +1,5 @@
import { DragDropContext } from '@hello-pangea/dnd'
import { useTheme } from '@renderer/context/ThemeProvider'
import { defaultActionItems } from '@renderer/store/selectionStore'
import type { ActionItem } from '@renderer/types/selectionTypes'
import SelectionToolbar from '@renderer/windows/selection/toolbar/SelectionToolbar'
@ -45,12 +46,14 @@ const SelectionActionsList: FC<SelectionActionsListProps> = ({ actionItems, setA
MAX_ENABLED_ITEMS
} = useActionItems(actionItems, setActionItems)
const { theme } = useTheme()
if (!actionItems || actionItems.length === 0) {
setActionItems(defaultActionItems)
}
return (
<SettingGroup>
<SettingGroup theme={theme}>
<SettingsActionsListHeader
customItemsCount={customItemsCount}
maxCustomItems={MAX_CUSTOM_ITEMS}

View File

@ -4,16 +4,15 @@ import {
Brain,
Cloud,
Command,
FolderCog,
HardDrive,
Info,
MonitorCog,
Package,
PencilRuler,
Rocket,
PictureInPicture2,
Settings2,
SquareTerminal,
TextCursorInput,
Zap
TextCursorInput
} from 'lucide-react'
import { FC } from 'react'
import { useTranslation } from 'react-i18next'
@ -28,7 +27,6 @@ import MCPSettings from './MCPSettings'
import MemorySettings from './MemorySettings'
import ProvidersList from './ProviderSettings'
import QuickAssistantSettings from './QuickAssistantSettings'
import QuickPhraseSettings from './QuickPhraseSettings'
import SelectionAssistantSettings from './SelectionAssistantSettings/SelectionAssistantSettings'
import ShortcutSettings from './ShortcutSettings'
import ToolSettings from './ToolSettings'
@ -70,6 +68,12 @@ const SettingsPage: FC = () => {
{t('settings.display.title')}
</MenuItem>
</MenuItemLink>
<MenuItemLink to="/settings/data">
<MenuItem className={isRoute('/settings/data')}>
<HardDrive size={18} />
{t('settings.data.title')}
</MenuItem>
</MenuItemLink>
<MenuItemLink to="/settings/mcp">
<MenuItem className={isRoute('/settings/mcp')}>
<SquareTerminal size={18} />
@ -82,21 +86,21 @@ const SettingsPage: FC = () => {
{t('memory.title')}
</MenuItem>
</MenuItemLink>
<MenuItemLink to="/settings/tool">
<MenuItem className={isRoute('/settings/tool')}>
<PencilRuler size={18} />
{t('settings.tool.title')}
</MenuItem>
</MenuItemLink>
<MenuItemLink to="/settings/shortcut">
<MenuItem className={isRoute('/settings/shortcut')}>
<Command size={18} />
{t('settings.shortcuts.title')}
</MenuItem>
</MenuItemLink>
<MenuItemLink to="/settings/tool">
<MenuItem className={isRoute('/settings/tool')}>
<FolderCog size={18} />
{t('settings.tool.title')}
</MenuItem>
</MenuItemLink>
<MenuItemLink to="/settings/quickAssistant">
<MenuItem className={isRoute('/settings/quickAssistant')}>
<Rocket size={18} />
<PictureInPicture2 size={18} />
{t('settings.quickAssistant.title')}
</MenuItem>
</MenuItemLink>
@ -106,18 +110,6 @@ const SettingsPage: FC = () => {
{t('selection.name')}
</MenuItem>
</MenuItemLink>
<MenuItemLink to="/settings/quickPhrase">
<MenuItem className={isRoute('/settings/quickPhrase')}>
<Zap size={18} />
{t('settings.quickPhrase.title')}
</MenuItem>
</MenuItemLink>
<MenuItemLink to="/settings/data">
<MenuItem className={isRoute('/settings/data')}>
<HardDrive size={18} />
{t('settings.data.title')}
</MenuItem>
</MenuItemLink>
<MenuItemLink to="/settings/about">
<MenuItem className={isRoute('/settings/about')}>
<Info size={18} />
@ -139,7 +131,6 @@ const SettingsPage: FC = () => {
<Route path="selectionAssistant" element={<SelectionAssistantSettings />} />
<Route path="data" element={<DataSettings />} />
<Route path="about" element={<AboutSettings />} />
<Route path="quickPhrase" element={<QuickPhraseSettings />} />
</Routes>
</SettingContent>
</ContentContainer>

View File

@ -1,42 +1,55 @@
import { GlobalOutlined } from '@ant-design/icons'
import { HStack } from '@renderer/components/Layout'
import ListItem from '@renderer/components/ListItem'
import { FileCode } from 'lucide-react'
import { FC, useState } from 'react'
import { FileCode, Zap } from 'lucide-react'
import { FC } from 'react'
import { useTranslation } from 'react-i18next'
import { Link, Route, Routes, useLocation } from 'react-router-dom'
import styled from 'styled-components'
import PreprocessSettings from './PreprocessSettings'
import QuickPhraseSettings from './QuickPhraseSettings'
import WebSearchSettings from './WebSearchSettings'
let _menu: string = 'web-search'
const ToolSettings: FC = () => {
const { t } = useTranslation()
const [menu, setMenu] = useState<string>(_menu)
const { pathname } = useLocation()
const menuItems = [
{ key: 'web-search', title: 'settings.tool.websearch.title', icon: <GlobalOutlined style={{ fontSize: 16 }} /> },
{ key: 'preprocess', title: 'settings.tool.preprocess.title', icon: <FileCode size={16} /> }
{ key: 'preprocess', title: 'settings.tool.preprocess.title', icon: <FileCode size={16} /> },
{ key: 'quick-phrase', title: 'settings.quickPhrase.title', icon: <Zap size={16} /> }
]
_menu = menu
const isActive = (key: string): boolean => {
const basePath = '/settings/tool'
if (key === 'web-search') {
return pathname === basePath || pathname === `${basePath}/` || pathname === `${basePath}/${key}`
}
return pathname === `${basePath}/${key}`
}
return (
<Container>
<MenuList>
{menuItems.map((item) => (
<ListItem
key={item.key}
title={t(item.title)}
active={menu === item.key}
onClick={() => setMenu(item.key)}
titleStyle={{ fontWeight: 500 }}
icon={item.icon}
/>
<Link key={item.key} to={`/settings/tool/${item.key}`} style={{ textDecoration: 'none', color: 'inherit' }}>
<ListItem
title={t(item.title)}
active={isActive(item.key)}
titleStyle={{ fontWeight: 500 }}
icon={item.icon}
/>
</Link>
))}
</MenuList>
{menu == 'web-search' && <WebSearchSettings />}
{menu == 'preprocess' && <PreprocessSettings />}
<ContentArea>
<Routes>
<Route path="/" element={<WebSearchSettings />} />
<Route path="/web-search" element={<WebSearchSettings />} />
<Route path="/preprocess" element={<PreprocessSettings />} />
<Route path="/quick-phrase" element={<QuickPhraseSettings />} />
</Routes>
</ContentArea>
</Container>
)
}
@ -44,6 +57,7 @@ const ToolSettings: FC = () => {
const Container = styled(HStack)`
flex: 1;
`
const MenuList = styled.div`
display: flex;
flex-direction: column;
@ -56,4 +70,10 @@ const MenuList = styled.div`
line-height: 16px;
}
`
const ContentArea = styled.div`
display: flex;
flex: 1;
height: 100%;
`
export default ToolSettings

View File

@ -381,7 +381,7 @@ export const initialState: SettingsState = {
// Developer mode
enableDeveloperMode: false,
// UI
navbarPosition: 'left',
navbarPosition: 'top',
// API Server
apiServer: {
enabled: false,

View File

@ -89,7 +89,7 @@ const MessageContentContainer = styled.div`
flex: 1;
flex-direction: column;
justify-content: space-between;
margin-top: 5px;
margin-top: 20px;
`
export default memo(MessageItem)

View File

@ -36,7 +36,7 @@ import InputBar from './components/InputBar'
const logger = loggerService.withContext('HomeWindow')
const HomeWindow: FC = () => {
const HomeWindow: FC<{ draggable?: boolean }> = ({ draggable = true }) => {
const { language, readClipboardAtStartup, windowStyle } = useSettings()
const { theme } = useTheme()
const { t } = useTranslation()
@ -487,7 +487,7 @@ const HomeWindow: FC = () => {
case 'summary':
case 'explanation':
return (
<Container style={{ backgroundColor }}>
<Container style={{ backgroundColor }} $draggable={draggable}>
{route === 'chat' && (
<>
<InputBar
@ -523,7 +523,7 @@ const HomeWindow: FC = () => {
case 'translate':
return (
<Container style={{ backgroundColor }}>
<Container style={{ backgroundColor }} $draggable={draggable}>
<TranslateWindow text={referenceText} />
<Divider style={{ margin: '10px 0' }} />
<Footer key="footer" {...baseFooterProps} />
@ -533,7 +533,7 @@ const HomeWindow: FC = () => {
// Home
default:
return (
<Container style={{ backgroundColor }}>
<Container style={{ backgroundColor }} $draggable={draggable}>
<InputBar
text={userInputText}
assistant={currentAssistant}
@ -566,13 +566,13 @@ const HomeWindow: FC = () => {
}
}
const Container = styled.div`
const Container = styled.div<{ $draggable: boolean }>`
display: flex;
flex: 1;
height: 100%;
width: 100%;
flex-direction: column;
-webkit-app-region: drag;
-webkit-app-region: ${({ $draggable }) => ($draggable ? 'drag' : 'no-drag')};
padding: 8px 10px;
`