mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-19 22:52:08 +08:00
style(ModelList): adjust GroupHeader height and update icon in ModelListItem
This commit is contained in:
parent
c9837eaa71
commit
ce804ce02b
@ -128,3 +128,4 @@ releaseInfo:
|
||||
内存泄漏修复:优化代码逻辑,解决内存泄漏问题,提升运行稳定性
|
||||
嵌入模型简化:降低嵌入模型配置复杂度,提高易用性
|
||||
MCP Tool 长时间运行:增强 MCP 工具的稳定性,支持长时间任务执行
|
||||
设置页面优化:优化设置页面布局,提升用户体验
|
||||
|
||||
@ -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
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -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",
|
||||
|
||||
@ -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": "ブラックリスト",
|
||||
|
||||
@ -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": "Черный список",
|
||||
|
||||
@ -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": "黑名单",
|
||||
|
||||
@ -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": "黑名單",
|
||||
|
||||
@ -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": "Το μοντέλο που χρησιμοποιείται όταν αυτόματα ονομάζεται ένα νέο θέμα",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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>
|
||||
}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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`
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -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,120 +142,15 @@ 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()
|
||||
} else {
|
||||
setShowMoreSettings(false)
|
||||
// 监听modelCapabilities变化,自动保存(但跳过初始化时的保存)
|
||||
useEffect(() => {
|
||||
if (hasUserModified && showMoreSettings) {
|
||||
autoSave()
|
||||
}
|
||||
}}>
|
||||
<Form
|
||||
form={form}
|
||||
labelCol={{ flex: provider.id === 'new-api' ? labelWidth : '110px' }}
|
||||
labelAlign="left"
|
||||
colon={false}
|
||||
style={{ marginTop: 15 }}
|
||||
initialValues={{
|
||||
id: model.id,
|
||||
name: model.name,
|
||||
group: model.group,
|
||||
endpointType: model.endpoint_type,
|
||||
input_per_million_tokens: model.pricing?.input_per_million_tokens ?? 0,
|
||||
output_per_million_tokens: model.pricing?.output_per_million_tokens ?? 0,
|
||||
currencySymbol: symbols.includes(model.pricing?.currencySymbol || '$')
|
||||
? model.pricing?.currencySymbol || '$'
|
||||
: 'custom',
|
||||
customCurrencySymbol: symbols.includes(model.pricing?.currencySymbol || '$')
|
||||
? ''
|
||||
: model.pricing?.currencySymbol || ''
|
||||
}}
|
||||
onFinish={onFinish}>
|
||||
<Form.Item
|
||||
name="id"
|
||||
label={t('settings.models.add.model_id.label')}
|
||||
tooltip={t('settings.models.add.model_id.tooltip')}
|
||||
rules={[{ required: true }]}>
|
||||
<Flex justify="space-between" gap={5}>
|
||||
<Input
|
||||
placeholder={t('settings.models.add.model_id.placeholder')}
|
||||
spellCheck={false}
|
||||
maxLength={200}
|
||||
disabled={true}
|
||||
value={model.id}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value
|
||||
form.setFieldValue('name', value)
|
||||
form.setFieldValue('group', getDefaultGroupName(value))
|
||||
}}
|
||||
/>
|
||||
<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
|
||||
name="name"
|
||||
label={t('settings.models.add.model_name.label')}
|
||||
tooltip={t('settings.models.add.model_name.tooltip')}>
|
||||
<Input placeholder={t('settings.models.add.model_name.placeholder')} spellCheck={false} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="group"
|
||||
label={t('settings.models.add.group_name.label')}
|
||||
tooltip={t('settings.models.add.group_name.tooltip')}>
|
||||
<Input placeholder={t('settings.models.add.group_name.placeholder')} spellCheck={false} />
|
||||
</Form.Item>
|
||||
{provider.id === 'new-api' && (
|
||||
<Form.Item
|
||||
name="endpointType"
|
||||
label={t('settings.models.add.endpoint_type.label')}
|
||||
tooltip={t('settings.models.add.endpoint_type.tooltip')}
|
||||
rules={[{ required: true, message: t('settings.models.add.endpoint_type.required') }]}>
|
||||
<Select placeholder={t('settings.models.add.endpoint_type.placeholder')}>
|
||||
{endpointTypeOptions.map((opt) => (
|
||||
<Select.Option key={opt.value} value={opt.value}>
|
||||
{t(opt.label)}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
)}
|
||||
<Form.Item style={{ marginBottom: 8, textAlign: 'center' }}>
|
||||
<Flex justify="space-between" align="center" style={{ position: 'relative' }}>
|
||||
<Button
|
||||
color="default"
|
||||
variant="filled"
|
||||
icon={showMoreSettings ? <ChevronUp size={16} /> : <ChevronDown size={16} />}
|
||||
iconPosition="end"
|
||||
onClick={() => setShowMoreSettings(!showMoreSettings)}
|
||||
style={{ color: 'var(--color-text-3)' }}>
|
||||
{t('settings.moresetting.label')}
|
||||
</Button>
|
||||
<Button type="primary" htmlType="submit" icon={<SaveIcon size={16} />}>
|
||||
{t('common.save')}
|
||||
</Button>
|
||||
</Flex>
|
||||
</Form.Item>
|
||||
{showMoreSettings && (
|
||||
<div style={{ marginBottom: 8 }}>
|
||||
<Divider style={{ margin: '16px 0 16px 0' }} />
|
||||
<TypeTitle>{t('models.type.select')}:</TypeTitle>
|
||||
{(() => {
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [modelCapabilities])
|
||||
|
||||
const ModelCapability = () => {
|
||||
const isDisabled = selectedTypes.includes('rerank') || selectedTypes.includes('embedding')
|
||||
|
||||
const isRerankDisabled = selectedTypes.includes('embedding')
|
||||
@ -373,25 +303,151 @@ const ModelEditContent: FC<ModelEditContentProps> = ({ provider, model, onUpdate
|
||||
</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' }}
|
||||
labelAlign="left"
|
||||
colon={false}
|
||||
style={{ marginTop: 15 }}
|
||||
initialValues={{
|
||||
id: model.id,
|
||||
name: model.name,
|
||||
group: model.group,
|
||||
endpointType: model.endpoint_type,
|
||||
input_per_million_tokens: model.pricing?.input_per_million_tokens ?? 0,
|
||||
output_per_million_tokens: model.pricing?.output_per_million_tokens ?? 0,
|
||||
currencySymbol: symbols.includes(model.pricing?.currencySymbol || '$')
|
||||
? model.pricing?.currencySymbol || '$'
|
||||
: 'custom',
|
||||
customCurrencySymbol: symbols.includes(model.pricing?.currencySymbol || '$')
|
||||
? ''
|
||||
: model.pricing?.currencySymbol || ''
|
||||
}}
|
||||
onFinish={onFinish}>
|
||||
<Form.Item
|
||||
name="id"
|
||||
label={t('settings.models.add.model_id.label')}
|
||||
tooltip={t('settings.models.add.model_id.tooltip')}
|
||||
rules={[{ required: true }]}>
|
||||
<Flex justify="space-between" gap={5}>
|
||||
<Input
|
||||
placeholder={t('settings.models.add.model_id.placeholder')}
|
||||
spellCheck={false}
|
||||
maxLength={200}
|
||||
disabled={true}
|
||||
value={model.id}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value
|
||||
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'))
|
||||
}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</Flex>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="name"
|
||||
label={t('settings.models.add.model_name.label')}
|
||||
tooltip={t('settings.models.add.model_name.tooltip')}>
|
||||
<Input placeholder={t('settings.models.add.model_name.placeholder')} spellCheck={false} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="group"
|
||||
label={t('settings.models.add.group_name.label')}
|
||||
tooltip={t('settings.models.add.group_name.tooltip')}>
|
||||
<Input placeholder={t('settings.models.add.group_name.placeholder')} spellCheck={false} />
|
||||
</Form.Item>
|
||||
{provider.id === 'new-api' && (
|
||||
<Form.Item
|
||||
name="endpointType"
|
||||
label={t('settings.models.add.endpoint_type.label')}
|
||||
tooltip={t('settings.models.add.endpoint_type.tooltip')}
|
||||
rules={[{ required: true, message: t('settings.models.add.endpoint_type.required') }]}>
|
||||
<Select placeholder={t('settings.models.add.endpoint_type.placeholder')}>
|
||||
{endpointTypeOptions.map((opt) => (
|
||||
<Select.Option key={opt.value} value={opt.value}>
|
||||
{t(opt.label)}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
)}
|
||||
<Form.Item style={{ marginBottom: 8, textAlign: 'center' }}>
|
||||
<Flex justify="space-between" align="center" style={{ position: 'relative' }}>
|
||||
<Button
|
||||
color="default"
|
||||
variant="filled"
|
||||
icon={showMoreSettings ? <ChevronUp size={16} /> : <ChevronDown size={16} />}
|
||||
iconPosition="end"
|
||||
onClick={() => setShowMoreSettings(!showMoreSettings)}
|
||||
style={{ color: 'var(--color-text-3)' }}>
|
||||
{t('settings.moresetting.label')}
|
||||
</Button>
|
||||
<Button type="primary" htmlType="submit" icon={<SaveIcon size={16} />}>
|
||||
{t('common.save')}
|
||||
</Button>
|
||||
</Flex>
|
||||
</Form.Item>
|
||||
{showMoreSettings && (
|
||||
<div style={{ marginBottom: 8 }}>
|
||||
<Divider style={{ margin: '16px 0 16px 0' }} />
|
||||
<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>
|
||||
@ -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
|
||||
@ -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;
|
||||
`
|
||||
@ -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'
|
||||
|
||||
@ -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))}
|
||||
/>
|
||||
@ -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} />} />
|
||||
@ -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'
|
||||
@ -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'
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)}
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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) => (
|
||||
<Link key={item.key} to={`/settings/tool/${item.key}`} style={{ textDecoration: 'none', color: 'inherit' }}>
|
||||
<ListItem
|
||||
key={item.key}
|
||||
title={t(item.title)}
|
||||
active={menu === item.key}
|
||||
onClick={() => setMenu(item.key)}
|
||||
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
|
||||
|
||||
@ -381,7 +381,7 @@ export const initialState: SettingsState = {
|
||||
// Developer mode
|
||||
enableDeveloperMode: false,
|
||||
// UI
|
||||
navbarPosition: 'left',
|
||||
navbarPosition: 'top',
|
||||
// API Server
|
||||
apiServer: {
|
||||
enabled: false,
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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;
|
||||
`
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user