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 工具的稳定性,支持长时间任务执行 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", "function_calling": "Tool",
"reasoning": "Reasoning", "reasoning": "Reasoning",
"rerank": "Reranker", "rerank": "Reranker",
"select": "Select Model Types", "select": "Model Types",
"text": "Text", "text": "Text",
"vision": "Vision", "vision": "Vision",
"websearch": "WebSearch" "websearch": "WebSearch"
@ -2716,6 +2716,8 @@
"jsonSaveError": "Failed to save JSON configuration.", "jsonSaveError": "Failed to save JSON configuration.",
"jsonSaveSuccess": "JSON configuration has been saved.", "jsonSaveSuccess": "JSON configuration has been saved.",
"logoUrl": "Logo URL", "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.", "missingDependencies": "is Missing, please install it to continue.",
"more": { "more": {
"awesome": "Curated MCP Server List", "awesome": "Curated MCP Server List",
@ -2962,8 +2964,8 @@
"tooltip": "Optional e.g. GPT-4" "tooltip": "Optional e.g. GPT-4"
}, },
"supported_text_delta": { "supported_text_delta": {
"label": "Incremental text output", "label": "Support incremental text output",
"tooltip": "When the model is not supported, close the button" "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", "api_key": "API Key",
@ -3019,7 +3021,6 @@
"provider_name": "Provider Name", "provider_name": "Provider Name",
"quick_assistant_default_tag": "Default", "quick_assistant_default_tag": "Default",
"quick_assistant_model": "Quick Assistant Model", "quick_assistant_model": "Quick Assistant Model",
"quick_assistant_model_description": "Default model used by Quick Assistant",
"quick_assistant_selection": "Select Assistant", "quick_assistant_selection": "Select Assistant",
"topic_naming_model": "Topic Naming Model", "topic_naming_model": "Topic Naming Model",
"topic_naming_model_description": "Model used when automatically naming a new topic", "topic_naming_model_description": "Model used when automatically naming a new topic",
@ -3337,7 +3338,7 @@
"title": "Pre Process", "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." "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": { "websearch": {
"apikey": "API key", "apikey": "API key",
"blacklist": "Blacklist", "blacklist": "Blacklist",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1463,7 +1463,7 @@
"function_calling": "Appel de fonction", "function_calling": "Appel de fonction",
"reasoning": "Raisonnement", "reasoning": "Raisonnement",
"rerank": "Reclasser", "rerank": "Reclasser",
"select": "Sélectionnez le type de modèle", "select": "Types de modèle",
"text": "Texte", "text": "Texte",
"vision": "Image", "vision": "Image",
"websearch": "Recherche web" "websearch": "Recherche web"
@ -3018,7 +3018,6 @@
"provider_name": "Nom du fournisseur", "provider_name": "Nom du fournisseur",
"quick_assistant_default_tag": "Par défaut", "quick_assistant_default_tag": "Par défaut",
"quick_assistant_model": "Modèle de l'assistant rapide", "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", "quick_assistant_selection": "Sélectionner l'assistant",
"topic_naming_model": "Modèle de renommage des sujets", "topic_naming_model": "Modèle de renommage des sujets",
"topic_naming_model_description": "Modèle utilisé pour le renommage automatique des nouveaux 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", "function_calling": "chamada de função",
"reasoning": "raciocínio", "reasoning": "raciocínio",
"rerank": "Reclassificar", "rerank": "Reclassificar",
"select": "selecione o tipo de modelo", "select": "Tipos de modelo",
"text": "texto", "text": "texto",
"vision": "imagem", "vision": "imagem",
"websearch": "Procurar na web" "websearch": "Procurar na web"
@ -3018,7 +3018,6 @@
"provider_name": "Nome do Provedor", "provider_name": "Nome do Provedor",
"quick_assistant_default_tag": "Padrão", "quick_assistant_default_tag": "Padrão",
"quick_assistant_model": "Modelo do Assistente Rápido", "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", "quick_assistant_selection": "Selecionar Assistente",
"topic_naming_model": "Modelo de nomenclatura de tópicos", "topic_naming_model": "Modelo de nomenclatura de tópicos",
"topic_naming_model_description": "Modelo usado para nomear tópicos automaticamente", "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 { ImportOutlined, PlusOutlined } from '@ant-design/icons'
import { Navbar, NavbarCenter } from '@renderer/components/app/Navbar' import { Navbar, NavbarCenter } from '@renderer/components/app/Navbar'
import CustomTag from '@renderer/components/CustomTag' import CustomTag from '@renderer/components/CustomTag'
import { HStack } from '@renderer/components/Layout'
import ListItem from '@renderer/components/ListItem' import ListItem from '@renderer/components/ListItem'
import Scrollbar from '@renderer/components/Scrollbar' import Scrollbar from '@renderer/components/Scrollbar'
import { useAgents } from '@renderer/hooks/useAgents' import { useAgents } from '@renderer/hooks/useAgents'
@ -213,11 +214,11 @@ const AgentsPage: FC = () => {
{getLocalizedGroupName(group)} {getLocalizedGroupName(group)}
</Flex> </Flex>
{ {
<div style={{ minWidth: 40, textAlign: 'center' }}> <HStack alignItems="center" justifyContent="center" style={{ minWidth: 40 }}>
<CustomTag color="#A0A0A0" size={8}> <CustomTag color="#A0A0A0" size={8}>
{agentGroups[group].length} {agentGroups[group].length}
</CustomTag> </CustomTag>
</div> </HStack>
} }
</Flex> </Flex>
} }

View File

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

View File

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

View File

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

View File

@ -633,8 +633,13 @@ const McpSettings: React.FC = () => {
</Form.Item> </Form.Item>
</> </>
)} )}
<Form.Item name="longRunning" label={t('settings.mcp.longRunning', 'Long Running')} valuePropName="checked"> <Form.Item
<Switch /> 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>
<Form.Item <Form.Item
name="timeout" name="timeout"

View File

@ -1,26 +1,22 @@
import { RedoOutlined } from '@ant-design/icons' import { RedoOutlined } from '@ant-design/icons'
import ModelAvatar from '@renderer/components/Avatar/ModelAvatar'
import { HStack } from '@renderer/components/Layout' import { HStack } from '@renderer/components/Layout'
import ModelSelector from '@renderer/components/ModelSelector' import ModelSelector from '@renderer/components/ModelSelector'
import PromptPopup from '@renderer/components/Popups/PromptPopup' import PromptPopup from '@renderer/components/Popups/PromptPopup'
import { isEmbeddingModel, isRerankModel, isTextToImageModel } from '@renderer/config/models' import { isEmbeddingModel, isRerankModel, isTextToImageModel } from '@renderer/config/models'
import { TRANSLATE_PROMPT } from '@renderer/config/prompts' import { TRANSLATE_PROMPT } from '@renderer/config/prompts'
import { useTheme } from '@renderer/context/ThemeProvider' 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 { useProviders } from '@renderer/hooks/useProvider'
import { useSettings } from '@renderer/hooks/useSettings' import { useSettings } from '@renderer/hooks/useSettings'
import { getModelUniqId, hasModel } from '@renderer/services/ModelService' import { getModelUniqId, hasModel } from '@renderer/services/ModelService'
import { useAppSelector } from '@renderer/store'
import { useAppDispatch } from '@renderer/store' import { useAppDispatch } from '@renderer/store'
import { setQuickAssistantId } from '@renderer/store/llm'
import { setTranslateModelPrompt } from '@renderer/store/settings' import { setTranslateModelPrompt } from '@renderer/store/settings'
import { Model } from '@renderer/types' import { Model } from '@renderer/types'
import { Button, Select, Tooltip } from 'antd' import { Button, Tooltip } from 'antd'
import { find } from 'lodash' 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 { FC, useCallback, useMemo } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
import { SettingContainer, SettingDescription, SettingGroup, SettingTitle } from '..' import { SettingContainer, SettingDescription, SettingGroup, SettingTitle } from '..'
import DefaultAssistantSettings from './DefaultAssistantSettings' import DefaultAssistantSettings from './DefaultAssistantSettings'
@ -29,8 +25,6 @@ import TopicNamingModalPopup from './TopicNamingModalPopup'
const ModelSettings: FC = () => { const ModelSettings: FC = () => {
const { defaultModel, topicNamingModel, translateModel, setDefaultModel, setTopicNamingModel, setTranslateModel } = const { defaultModel, topicNamingModel, translateModel, setDefaultModel, setTopicNamingModel, setTranslateModel } =
useDefaultModel() useDefaultModel()
const { defaultAssistant } = useDefaultAssistant()
const { assistants } = useAssistants()
const { providers } = useProviders() const { providers } = useProviders()
const allModels = providers.map((p) => p.models).flat() const allModels = providers.map((p) => p.models).flat()
const { theme } = useTheme() const { theme } = useTheme()
@ -38,7 +32,6 @@ const ModelSettings: FC = () => {
const { translateModelPrompt } = useSettings() const { translateModelPrompt } = useSettings()
const dispatch = useAppDispatch() const dispatch = useAppDispatch()
const { quickAssistantId } = useAppSelector((state) => state.llm)
const modelPredicate = useCallback( const modelPredicate = useCallback(
(m: Model) => !isEmbeddingModel(m) && !isRerankModel(m) && !isTextToImageModel(m), (m: Model) => !isEmbeddingModel(m) && !isRerankModel(m) && !isTextToImageModel(m),
@ -149,127 +142,8 @@ const ModelSettings: FC = () => {
</HStack> </HStack>
<SettingDescription>{t('settings.models.translate_model_description')}</SettingDescription> <SettingDescription>{t('settings.models.translate_model_description')}</SettingDescription>
</SettingGroup> </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> </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 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 { useDynamicLabelWidth } from '@renderer/hooks/useDynamicLabelWidth'
import { Model, ModelCapability, ModelType, Provider } from '@renderer/types' import { Model, ModelCapability, ModelType, Provider } from '@renderer/types'
import { getDefaultGroupName, getDifference, getUnion, uniqueObjectArray } from '@renderer/utils' 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 { cloneDeep } from 'lodash'
import { ChevronDown, ChevronUp, SaveIcon } from 'lucide-react' import { ChevronDown, ChevronUp, SaveIcon } from 'lucide-react'
import { FC, useEffect, useRef, useState } from 'react' import { FC, useEffect, useRef, useState } from 'react'
@ -22,12 +35,10 @@ interface ModelEditContentProps {
provider: Provider provider: Provider
model: Model model: Model
onUpdateModel: (model: Model) => void onUpdateModel: (model: Model) => void
open: boolean
onClose: () => void
} }
const symbols = ['$', '¥', '€', '£'] 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 [form] = Form.useForm()
const { t } = useTranslation() const { t } = useTranslation()
const [showMoreSettings, setShowMoreSettings] = useState(false) 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 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 onFinish = (values: any) => {
const finalCurrencySymbol = isCustomCurrency ? values.customCurrencySymbol : values.currencySymbol const finalCurrencySymbol = isCustomCurrency ? values.customCurrencySymbol : values.currencySymbol
const updatedModel: Model = { const updatedModel: Model = {
@ -58,13 +99,7 @@ const ModelEditContent: FC<ModelEditContentProps> = ({ provider, model, onUpdate
} }
onUpdateModel(updatedModel) onUpdateModel(updatedModel)
setShowMoreSettings(false) setShowMoreSettings(false)
onClose() props.onOk?.(undefined as any)
}
const handleClose = () => {
setShowMoreSettings(false)
setModelCapabilities(model.capabilities || [])
onClose()
} }
const currencyOptions = [ const currencyOptions = [
@ -107,120 +142,15 @@ const ModelEditContent: FC<ModelEditContentProps> = ({ provider, model, onUpdate
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [showMoreSettings]) }, [showMoreSettings])
return ( // 监听modelCapabilities变化自动保存但跳过初始化时的保存
<Modal useEffect(() => {
title={t('models.edit')} if (hasUserModified && showMoreSettings) {
open={open} autoSave()
onCancel={handleClose}
footer={null}
transitionName="animation-move-down"
centered
afterOpenChange={(visible) => {
if (visible) {
form.getFieldInstance('id')?.focus()
} else {
setShowMoreSettings(false)
} }
}}> // eslint-disable-next-line react-hooks/exhaustive-deps
<Form }, [modelCapabilities])
form={form}
labelCol={{ flex: provider.id === 'new-api' ? labelWidth : '110px' }} const ModelCapability = () => {
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>
{(() => {
const isDisabled = selectedTypes.includes('rerank') || selectedTypes.includes('embedding') const isDisabled = selectedTypes.includes('rerank') || selectedTypes.includes('embedding')
const isRerankDisabled = selectedTypes.includes('embedding') const isRerankDisabled = selectedTypes.includes('embedding')
@ -373,25 +303,151 @@ const ModelEditContent: FC<ModelEditContentProps> = ({ provider, model, onUpdate
</Flex> </Flex>
</div> </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 <Form.Item
name="supported_text_delta" name="supported_text_delta"
style={{ marginBottom: 10 }}
labelCol={{ flex: 1 }}
label={t('settings.models.add.supported_text_delta.label')} label={t('settings.models.add.supported_text_delta.label')}
tooltip={t('settings.models.add.supported_text_delta.tooltip')}> 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> </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 }}> <Form.Item name="currencySymbol" label={t('models.price.currency')} style={{ marginBottom: 10 }}>
<Select <Select
style={{ width: '100px' }} style={{ width: '100px' }}
options={currencyOptions} options={currencyOptions}
onChange={(value) => { onChange={(value) => {
if (value === 'custom') { if (value === 'custom') {
const customSymbol = form.getFieldValue('customCurrencySymbol') || ''
setIsCustomCurrency(true) setIsCustomCurrency(true)
setCurrencySymbol(form.getFieldValue('customCurrencySymbol') || '') setCurrencySymbol(customSymbol)
// 自动保存
autoSave({
isCustomCurrency: true,
currencySymbol: customSymbol
})
} else { } else {
setIsCustomCurrency(false) setIsCustomCurrency(false)
setCurrencySymbol(value) setCurrencySymbol(value)
// 自动保存
autoSave({
isCustomCurrency: false,
currencySymbol: value
})
} }
}} }}
dropdownMatchSelectWidth={false} dropdownMatchSelectWidth={false}
@ -409,12 +465,20 @@ const ModelEditContent: FC<ModelEditContentProps> = ({ provider, model, onUpdate
placeholder={t('models.price.custom_currency_placeholder')} placeholder={t('models.price.custom_currency_placeholder')}
defaultValue={model.pricing?.currencySymbol} defaultValue={model.pricing?.currencySymbol}
maxLength={5} 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>
)} )}
<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 <InputNumber
placeholder="0.00" placeholder="0.00"
defaultValue={model.pricing?.input_per_million_tokens} defaultValue={model.pricing?.input_per_million_tokens}
@ -423,9 +487,13 @@ const ModelEditContent: FC<ModelEditContentProps> = ({ provider, model, onUpdate
precision={2} precision={2}
style={{ width: '240px' }} style={{ width: '240px' }}
addonAfter={`${currencySymbol} / ${t('models.price.million_tokens')}`} addonAfter={`${currencySymbol} / ${t('models.price.million_tokens')}`}
onChange={() => {
// 自动保存
autoSave()
}}
/> />
</Form.Item> </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 <InputNumber
placeholder="0.00" placeholder="0.00"
defaultValue={model.pricing?.output_per_million_tokens} defaultValue={model.pricing?.output_per_million_tokens}
@ -434,6 +502,10 @@ const ModelEditContent: FC<ModelEditContentProps> = ({ provider, model, onUpdate
precision={2} precision={2}
style={{ width: '240px' }} style={{ width: '240px' }}
addonAfter={`${currencySymbol} / ${t('models.price.million_tokens')}`} addonAfter={`${currencySymbol} / ${t('models.price.million_tokens')}`}
onChange={() => {
// 自动保存
autoSave()
}}
/> />
</Form.Item> </Form.Item>
</div> </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 CustomTag from '@renderer/components/CustomTag'
import ExpandableText from '@renderer/components/ExpandableText' import ExpandableText from '@renderer/components/ExpandableText'
import ModelIdWithTags from '@renderer/components/ModelIdWithTags' import ModelIdWithTags from '@renderer/components/ModelIdWithTags'
import NewApiBatchAddModelPopup from '@renderer/components/ModelList/NewApiBatchAddModelPopup'
import { DynamicVirtualList } from '@renderer/components/VirtualList' import { DynamicVirtualList } from '@renderer/components/VirtualList'
import { getModelLogo } from '@renderer/config/models' import { getModelLogo } from '@renderer/config/models'
import FileItem from '@renderer/pages/files/FileItem' import FileItem from '@renderer/pages/files/FileItem'
import NewApiBatchAddModelPopup from '@renderer/pages/settings/ProviderSettings/ModelList/NewApiBatchAddModelPopup'
import { Model, Provider } from '@renderer/types' import { Model, Provider } from '@renderer/types'
import { Button, Flex, Tooltip } from 'antd' import { Button, Flex, Tooltip } from 'antd'
import { Avatar } from 'antd' import { Avatar } from 'antd'
@ -221,7 +221,7 @@ const GroupHeader = styled.div<{ isCollapsed: boolean }>`
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
padding: 0 13px; padding: 0 13px;
min-height: 35px; min-height: 38px;
color: var(--color-text); color: var(--color-text);
cursor: pointer; cursor: pointer;
` `

View File

@ -1,7 +1,5 @@
import { loggerService } from '@logger' import { loggerService } from '@logger'
import { LoadingIcon } from '@renderer/components/Icons' 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 { TopView } from '@renderer/components/TopView'
import { import {
groupQwenModels, groupQwenModels,
@ -15,6 +13,8 @@ import {
SYSTEM_MODELS SYSTEM_MODELS
} from '@renderer/config/models' } from '@renderer/config/models'
import { useProvider } from '@renderer/hooks/useProvider' 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 { fetchModels } from '@renderer/services/ApiService'
import { Model, Provider } from '@renderer/types' import { Model, Provider } from '@renderer/types'
import { filterModelsByKeywords, getDefaultGroupName, getFancyProviderName, isFreeModel } from '@renderer/utils' 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 { useTranslation } from 'react-i18next'
import styled from 'styled-components' import styled from 'styled-components'
import { HStack } from '../Layout' import { HStack } from '../../../../components/Layout'
import ManageModelsList from './ManageModelsList' import ManageModelsList from './ManageModelsList'
import { isModelInProvider, isValidNewApiModel } from './utils' import { isModelInProvider, isValidNewApiModel } from './utils'

View File

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

View File

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

View File

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

View File

@ -1,14 +1,18 @@
import { InfoCircleOutlined } from '@ant-design/icons' 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 { useTheme } from '@renderer/context/ThemeProvider'
import { useAssistants, useDefaultAssistant, useDefaultModel } from '@renderer/hooks/useAssistant'
import { useSettings } from '@renderer/hooks/useSettings' import { useSettings } from '@renderer/hooks/useSettings'
import { useAppDispatch } from '@renderer/store' import { useAppDispatch, useAppSelector } from '@renderer/store'
import { setQuickAssistantId } from '@renderer/store/llm'
import { import {
setClickTrayToShowQuickAssistant, setClickTrayToShowQuickAssistant,
setEnableQuickAssistant, setEnableQuickAssistant,
setReadClipboardAtStartup setReadClipboardAtStartup
} from '@renderer/store/settings' } from '@renderer/store/settings'
import HomeWindow from '@renderer/windows/mini/home/HomeWindow' 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 { FC } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import styled from 'styled-components' import styled from 'styled-components'
@ -20,6 +24,10 @@ const QuickAssistantSettings: FC = () => {
const { theme } = useTheme() const { theme } = useTheme()
const { enableQuickAssistant, clickTrayToShowQuickAssistant, setTray, readClipboardAtStartup } = useSettings() const { enableQuickAssistant, clickTrayToShowQuickAssistant, setTray, readClipboardAtStartup } = useSettings()
const dispatch = useAppDispatch() const dispatch = useAppDispatch()
const { assistants } = useAssistants()
const { quickAssistantId } = useAppSelector((state) => state.llm)
const { defaultAssistant } = useDefaultAssistant()
const { defaultModel } = useDefaultModel()
const handleEnableQuickAssistant = async (enable: boolean) => { const handleEnableQuickAssistant = async (enable: boolean) => {
dispatch(setEnableQuickAssistant(enable)) dispatch(setEnableQuickAssistant(enable))
@ -86,9 +94,69 @@ const QuickAssistantSettings: FC = () => {
</> </>
)} )}
</SettingGroup> </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 && ( {enableQuickAssistant && (
<AssistantContainer> <AssistantContainer>
<HomeWindow /> <HomeWindow draggable={false} />
</AssistantContainer> </AssistantContainer>
)} )}
</SettingContainer> </SettingContainer>
@ -105,4 +173,58 @@ const AssistantContainer = styled.div`
overflow: hidden; 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 export default QuickAssistantSettings

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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