Compare commits
9 Commits
v1.3.11
...
feat/custo
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
92ba7089bb | ||
|
|
bca4fbe7de | ||
|
|
2b99d6066f | ||
|
|
9357bd6e0f | ||
|
|
c4e0744806 | ||
|
|
1b6cba454d | ||
|
|
77cd958d08 | ||
|
|
d8aac9ecb8 | ||
|
|
2758321821 |
86
.github/dependabot.yml
vendored
Normal file
86
.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,86 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "monthly"
|
||||
open-pull-requests-limit: 7
|
||||
target-branch: "main"
|
||||
commit-message:
|
||||
prefix: "chore"
|
||||
include: "scope"
|
||||
groups:
|
||||
# 核心框架
|
||||
core-framework:
|
||||
patterns:
|
||||
- "react"
|
||||
- "react-dom"
|
||||
- "electron"
|
||||
- "typescript"
|
||||
- "@types/react*"
|
||||
- "@types/node"
|
||||
update-types:
|
||||
- "minor"
|
||||
- "patch"
|
||||
|
||||
# Electron 生态和构建工具
|
||||
electron-build:
|
||||
patterns:
|
||||
- "electron-*"
|
||||
- "@electron*"
|
||||
- "vite"
|
||||
- "@vitejs/*"
|
||||
- "dotenv-cli"
|
||||
- "rollup-plugin-*"
|
||||
- "@swc/*"
|
||||
update-types:
|
||||
- "minor"
|
||||
- "patch"
|
||||
|
||||
# 测试工具
|
||||
testing-tools:
|
||||
patterns:
|
||||
- "vitest"
|
||||
- "@vitest/*"
|
||||
- "playwright"
|
||||
- "@playwright/*"
|
||||
- "eslint*"
|
||||
- "@eslint*"
|
||||
- "prettier"
|
||||
- "husky"
|
||||
- "lint-staged"
|
||||
update-types:
|
||||
- "minor"
|
||||
- "patch"
|
||||
|
||||
# CherryStudio 自定义包
|
||||
cherrystudio-packages:
|
||||
patterns:
|
||||
- "@cherrystudio/*"
|
||||
update-types:
|
||||
- "minor"
|
||||
- "patch"
|
||||
|
||||
# 兜底其他 dependencies
|
||||
other-dependencies:
|
||||
dependency-type: "production"
|
||||
|
||||
# 兜底其他 devDependencies
|
||||
other-dev-dependencies:
|
||||
dependency-type: "development"
|
||||
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
open-pull-requests-limit: 3
|
||||
commit-message:
|
||||
prefix: "ci"
|
||||
include: "scope"
|
||||
groups:
|
||||
github-actions:
|
||||
patterns:
|
||||
- "*"
|
||||
update-types:
|
||||
- "minor"
|
||||
- "patch"
|
||||
@@ -47,6 +47,7 @@ win:
|
||||
- target: portable
|
||||
signtoolOptions:
|
||||
sign: scripts/win-sign.js
|
||||
verifyUpdateCodeSignature: false
|
||||
nsis:
|
||||
artifactName: ${productName}-${version}-${arch}-setup.${ext}
|
||||
shortcutName: ${productName}
|
||||
|
||||
@@ -15,6 +15,8 @@ const BUN_PACKAGES = {
|
||||
'darwin-x64': 'bun-darwin-x64.zip',
|
||||
'win32-x64': 'bun-windows-x64.zip',
|
||||
'win32-x64-baseline': 'bun-windows-x64-baseline.zip',
|
||||
'win32-arm64': 'bun-windows-x64.zip',
|
||||
'win32-arm64-baseline': 'bun-windows-x64-baseline.zip',
|
||||
'linux-x64': 'bun-linux-x64.zip',
|
||||
'linux-x64-baseline': 'bun-linux-x64-baseline.zip',
|
||||
'linux-arm64': 'bun-linux-aarch64.zip',
|
||||
|
||||
@@ -386,7 +386,11 @@ class FileStorage {
|
||||
}
|
||||
}
|
||||
|
||||
public downloadFile = async (_: Electron.IpcMainInvokeEvent, url: string): Promise<FileType> => {
|
||||
public downloadFile = async (
|
||||
_: Electron.IpcMainInvokeEvent,
|
||||
url: string,
|
||||
isUseContentType?: boolean
|
||||
): Promise<FileType> => {
|
||||
try {
|
||||
const response = await fetch(url)
|
||||
if (!response.ok) {
|
||||
@@ -411,7 +415,7 @@ class FileStorage {
|
||||
}
|
||||
|
||||
// 如果文件名没有后缀,根据Content-Type添加后缀
|
||||
if (!filename.includes('.')) {
|
||||
if (isUseContentType || !filename.includes('.')) {
|
||||
const contentType = response.headers.get('Content-Type')
|
||||
const ext = this.getExtensionFromMimeType(contentType)
|
||||
filename += ext
|
||||
|
||||
@@ -450,6 +450,35 @@ class McpService {
|
||||
return cachedListTools(server)
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply default parameters to tool arguments
|
||||
*/
|
||||
private applyDefaultParameters(
|
||||
toolName: string,
|
||||
server: MCPServer,
|
||||
providedArgs: Record<string, any> = {}
|
||||
): Record<string, any> {
|
||||
const toolConfig = server.customToolConfigs?.find((config) => config.toolName === toolName)
|
||||
|
||||
if (!toolConfig) {
|
||||
return providedArgs
|
||||
}
|
||||
|
||||
const mergedArgs = { ...providedArgs }
|
||||
|
||||
toolConfig.parameters.forEach((paramConfig) => {
|
||||
if (
|
||||
paramConfig.defaultValue !== undefined &&
|
||||
paramConfig.defaultValue !== null &&
|
||||
paramConfig.defaultValue !== ''
|
||||
) {
|
||||
mergedArgs[paramConfig.name] = paramConfig.defaultValue
|
||||
}
|
||||
})
|
||||
|
||||
return mergedArgs
|
||||
}
|
||||
|
||||
/**
|
||||
* Call a tool on an MCP server
|
||||
*/
|
||||
@@ -466,8 +495,13 @@ class McpService {
|
||||
Logger.error('[MCP] args parse error', args)
|
||||
}
|
||||
}
|
||||
|
||||
Logger.info('[MCP] Calling with args:', server.name, name, args)
|
||||
const mergedArgs = this.applyDefaultParameters(name, server, args || {})
|
||||
Logger.info('[MCP] Calling with merged args:', server.name, name, mergedArgs)
|
||||
|
||||
const client = await this.initClient(server)
|
||||
const result = await client.callTool({ name, arguments: args }, undefined, {
|
||||
const result = await client.callTool({ name, arguments: mergedArgs }, undefined, {
|
||||
timeout: server.timeout ? server.timeout * 1000 : 60000 // Default timeout of 1 minute
|
||||
})
|
||||
return result as MCPCallToolResponse
|
||||
|
||||
@@ -74,7 +74,7 @@ const api = {
|
||||
selectFolder: () => ipcRenderer.invoke(IpcChannel.File_SelectFolder),
|
||||
saveImage: (name: string, data: string) => ipcRenderer.invoke(IpcChannel.File_SaveImage, name, data),
|
||||
base64Image: (fileId: string) => ipcRenderer.invoke(IpcChannel.File_Base64Image, fileId),
|
||||
download: (url: string) => ipcRenderer.invoke(IpcChannel.File_Download, url),
|
||||
download: (url: string, isUseContentType?: boolean) => ipcRenderer.invoke(IpcChannel.File_Download, url, isUseContentType),
|
||||
copy: (fileId: string, destPath: string) => ipcRenderer.invoke(IpcChannel.File_Copy, fileId, destPath),
|
||||
binaryImage: (fileId: string) => ipcRenderer.invoke(IpcChannel.File_BinaryImage, fileId),
|
||||
base64File: (fileId: string) => ipcRenderer.invoke(IpcChannel.File_Base64File, fileId),
|
||||
|
||||
BIN
src/renderer/src/assets/images/providers/DMXAPI-to-img.webp
Normal file
BIN
src/renderer/src/assets/images/providers/DMXAPI-to-img.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 254 KiB |
BIN
src/renderer/src/assets/images/providers/dmxapi-logo.webp
Normal file
BIN
src/renderer/src/assets/images/providers/dmxapi-logo.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.4 KiB |
@@ -60,6 +60,7 @@
|
||||
--assistants-width: 275px;
|
||||
--topic-list-width: 275px;
|
||||
--settings-width: 250px;
|
||||
--scrollbar-width: 5px;
|
||||
|
||||
--chat-background: #111111;
|
||||
--chat-background-user: #28b561;
|
||||
|
||||
@@ -5,8 +5,6 @@ import { FC, useEffect, useState } from 'react'
|
||||
import { useHotkeys } from 'react-hotkeys-hook'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import Scrollbar from '../Scrollbar'
|
||||
|
||||
interface Props {
|
||||
children: React.ReactNode
|
||||
activeAssistant: Assistant
|
||||
@@ -55,7 +53,8 @@ const FloatingSidebar: FC<Props> = ({
|
||||
forceToSeeAllTab={true}
|
||||
style={{
|
||||
background: 'transparent',
|
||||
border: 'none'
|
||||
border: 'none',
|
||||
maxHeight: maxHeight
|
||||
}}
|
||||
/>
|
||||
</PopoverContent>
|
||||
@@ -81,9 +80,8 @@ const FloatingSidebar: FC<Props> = ({
|
||||
)
|
||||
}
|
||||
|
||||
const PopoverContent = styled(Scrollbar)<{ maxHeight: number }>`
|
||||
const PopoverContent = styled.div<{ maxHeight: number }>`
|
||||
max-height: ${(props) => props.maxHeight}px;
|
||||
overflow-y: auto;
|
||||
`
|
||||
|
||||
export default FloatingSidebar
|
||||
|
||||
@@ -80,7 +80,11 @@ export const useChatContext = (activeTopic: Topic) => {
|
||||
(messageId: string, selected: boolean) => {
|
||||
dispatch(
|
||||
setSelectedMessageIds(
|
||||
selected ? [...selectedMessageIds, messageId] : selectedMessageIds.filter((id) => id !== messageId)
|
||||
selected
|
||||
? selectedMessageIds.includes(messageId)
|
||||
? selectedMessageIds
|
||||
: [...selectedMessageIds, messageId]
|
||||
: selectedMessageIds.filter((id) => id !== messageId)
|
||||
)
|
||||
)
|
||||
},
|
||||
|
||||
@@ -9,10 +9,12 @@ export function usePaintings() {
|
||||
const remix = useAppSelector((state) => state.paintings.remix)
|
||||
const edit = useAppSelector((state) => state.paintings.edit)
|
||||
const upscale = useAppSelector((state) => state.paintings.upscale)
|
||||
const DMXAPIPaintings = useAppSelector((state) => state.paintings.DMXAPIPaintings)
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
return {
|
||||
paintings,
|
||||
DMXAPIPaintings,
|
||||
persistentData: {
|
||||
generate,
|
||||
remix,
|
||||
|
||||
@@ -45,7 +45,10 @@
|
||||
"search.no_results": "No results found",
|
||||
"sorting.title": "Sorting",
|
||||
"settings": {
|
||||
"title": "Agent Setting"
|
||||
"title": "Agent Configuration",
|
||||
"subscription": {
|
||||
"title": "Agent Subscription Configuration"
|
||||
}
|
||||
},
|
||||
"tag.agent": "Agent",
|
||||
"tag.default": "Default",
|
||||
@@ -57,50 +60,50 @@
|
||||
"title": "Assistants",
|
||||
"abbr": "Assistants",
|
||||
"settings.title": "Assistant Settings",
|
||||
"clear.content": "Clearing the topic will delete all topics and files in the assistant. Are you sure you want to continue?",
|
||||
"clear.title": "Clear topics",
|
||||
"clear.content": "Clearing topics will delete all topics and files under the assistant. Are you sure you want to continue?",
|
||||
"clear.title": "Clear Topics",
|
||||
"copy.title": "Copy Assistant",
|
||||
"delete.content": "Deleting an assistant will delete all topics and files under the assistant. Are you sure you want to delete it?",
|
||||
"delete.content": "Deleting an assistant will delete all topics and files under that assistant. Are you sure you want to continue?",
|
||||
"delete.title": "Delete Assistant",
|
||||
"edit.title": "Edit Assistant",
|
||||
"save.success": "Saved successfully",
|
||||
"save.title": "Save to agent",
|
||||
"save.title": "Save to Agent",
|
||||
"icon.type": "Assistant Icon",
|
||||
"search": "Search assistants...",
|
||||
"search": "Search Assistants",
|
||||
"settings.mcp": "MCP Servers",
|
||||
"settings.mcp.enableFirst": "Please enable this server in MCP Settings first",
|
||||
"settings.mcp.title": "MCP Settings",
|
||||
"settings.mcp.noServersAvailable": "No MCP servers available. Please add a server in settings.",
|
||||
"settings.mcp.description": "Default enabled MCP servers",
|
||||
"settings.default_model": "Default Model",
|
||||
"settings.knowledge_base": "Knowledge Base Settings",
|
||||
"settings.mcp": "MCP Servers",
|
||||
"settings.mcp.enableFirst": "Enable this server in MCP settings first",
|
||||
"settings.mcp.title": "MCP Settings",
|
||||
"settings.mcp.noServersAvailable": "No MCP servers available. Add servers in settings",
|
||||
"settings.mcp.description": "Default enabled MCP servers",
|
||||
"settings.model": "Model Settings",
|
||||
"settings.preset_messages": "Preset Messages",
|
||||
"settings.prompt": "Prompt Settings",
|
||||
"settings.reasoning_effort": "Reasoning effort",
|
||||
"settings.reasoning_effort.off": "Off",
|
||||
"settings.reasoning_effort.high": "Think harder",
|
||||
"settings.reasoning_effort.low": "Think less",
|
||||
"settings.reasoning_effort.medium": "Think normally",
|
||||
"settings.reasoning_effort.default": "Default",
|
||||
"settings.more": "Assistant Settings",
|
||||
"settings.knowledge_base.recognition.tip": "The assistant will use the large model's intent recognition capability to determine whether to use the knowledge base for answering. This feature will depend on the model's capabilities",
|
||||
"settings.knowledge_base.recognition": "Use Knowledge Base",
|
||||
"settings.knowledge_base.recognition.off": "Force Search",
|
||||
"settings.knowledge_base.recognition.tip": "The agent will use the model's intent recognition ability to determine if it needs to call the knowledge base for an answer. This feature will depend on the model's capabilities.",
|
||||
"settings.knowledge_base.recognition": "Call Knowledge Base",
|
||||
"settings.knowledge_base.recognition.off": "Force Retrieval",
|
||||
"settings.knowledge_base.recognition.on": "Intent Recognition",
|
||||
"settings.tool_use_mode": "Tool Use Mode",
|
||||
"settings.tool_use_mode.function": "Function",
|
||||
"settings.tool_use_mode.prompt": "Prompt",
|
||||
"settings.model": "Model Settings",
|
||||
"settings.preset_messages": "Preset Messages",
|
||||
"settings.prompt": "Prompt Settings",
|
||||
"settings.reasoning_effort": "Chain of Thought Length",
|
||||
"settings.reasoning_effort.off": "Off",
|
||||
"settings.reasoning_effort.low": "Imagine",
|
||||
"settings.reasoning_effort.medium": "Consider",
|
||||
"settings.reasoning_effort.high": "Ponder",
|
||||
"settings.reasoning_effort.default": "Default",
|
||||
"settings.more": "Assistant Settings",
|
||||
"settings.regular_phrases": {
|
||||
"title": "Regular Phrase",
|
||||
"title": "Regular Phrases",
|
||||
"add": "Add Phrase",
|
||||
"edit": "Edit Phrase",
|
||||
"delete": "Delete Phrase",
|
||||
"deleteConfirm": "Are you sure to delete this phrase?",
|
||||
"deleteConfirm": "Are you sure you want to delete this phrase?",
|
||||
"titleLabel": "Title",
|
||||
"titlePlaceholder": "Enter title",
|
||||
"contentLabel": "Content",
|
||||
"contentPlaceholder": "Please enter phrase content, support using variables, and press Tab to quickly locate the variable to modify. For example: \nHelp me plan a route from ${from} to ${to}, and send it to ${email}."
|
||||
"contentPlaceholder": "Enter phrase content, supports variables, then press Tab to quickly navigate to variables for modification. E.g.:\nHelp me plan a route from ${from} to ${to}, then send to ${email}"
|
||||
}
|
||||
},
|
||||
"auth": {
|
||||
@@ -355,7 +358,7 @@
|
||||
"add": "Add",
|
||||
"advanced_settings": "Advanced Settings",
|
||||
"and": "and",
|
||||
"assistant": "Assistant",
|
||||
"assistant": "Agent",
|
||||
"avatar": "Avatar",
|
||||
"back": "Back",
|
||||
"cancel": "Cancel",
|
||||
@@ -376,9 +379,9 @@
|
||||
"edit": "Edit",
|
||||
"expand": "Expand",
|
||||
"collapse": "Collapse",
|
||||
"footnote": "Reference content",
|
||||
"footnotes": "References",
|
||||
"fullscreen": "Entered fullscreen mode. Press F11 to exit",
|
||||
"footnote": "Citation",
|
||||
"footnotes": "Citations",
|
||||
"fullscreen": "Entered fullscreen mode, press F11 to exit",
|
||||
"knowledge_base": "Knowledge Base",
|
||||
"language": "Language",
|
||||
"loading": "Loading...",
|
||||
@@ -392,7 +395,10 @@
|
||||
"regenerate": "Regenerate",
|
||||
"rename": "Rename",
|
||||
"reset": "Reset",
|
||||
"required": "REQUIRED",
|
||||
"allowed_values": "Allowed values",
|
||||
"save": "Save",
|
||||
"unsaved_changes": "You have unsaved changes",
|
||||
"search": "Search",
|
||||
"select": "Select",
|
||||
"selectedMessages": "Selected {{count}} messages",
|
||||
@@ -813,6 +819,7 @@
|
||||
"regenerate.confirm": "This will replace your existing generated images. Do you want to continue?",
|
||||
"seed": "Seed",
|
||||
"seed_tip": "The same seed and prompt can produce similar images",
|
||||
"seed_desc_tip": "The same seed and prompt can generate similar images, setting -1 will generate different results each time",
|
||||
"title": "Images",
|
||||
"magic_prompt_option": "Magic Prompt",
|
||||
"model": "Model Version",
|
||||
@@ -820,6 +827,7 @@
|
||||
"style_type": "Style",
|
||||
"rendering_speed": "Rendering Speed",
|
||||
"learn_more": "Learn More",
|
||||
"paint_course": "tutorial",
|
||||
"prompt_placeholder_edit": "Enter your image description, text drawing uses \"double quotes\" to wrap",
|
||||
"proxy_required": "Currently, you need to open a proxy to view the generated images, it will be supported in the future",
|
||||
"image_file_required": "Please upload an image first",
|
||||
@@ -885,7 +893,8 @@
|
||||
"number_images_tip": "Number of upscaled results to generate",
|
||||
"seed_tip": "Controls upscaling randomness",
|
||||
"magic_prompt_option_tip": "Intelligently enhances upscaling prompts"
|
||||
}
|
||||
},
|
||||
"text_desc_required": "Please enter image description first"
|
||||
},
|
||||
"prompts": {
|
||||
"explanation": "Explain this concept to me",
|
||||
@@ -1305,6 +1314,8 @@
|
||||
"stdio": "Standard Input/Output (stdio)",
|
||||
"inMemory": "Memory",
|
||||
"config_description": "Configure Model Context Protocol servers",
|
||||
"disable": "Disable MCP Server",
|
||||
"disable.description": "Do not enable MCP server functionality",
|
||||
"deleteError": "Failed to delete server",
|
||||
"deleteSuccess": "Server deleted successfully",
|
||||
"dependenciesInstall": "Install Dependencies",
|
||||
@@ -1368,7 +1379,13 @@
|
||||
"inputSchema": "Input Schema",
|
||||
"availableTools": "Available Tools",
|
||||
"noToolsAvailable": "No tools available",
|
||||
"loadError": "Get tools Error"
|
||||
"loadError": "Get tools Error",
|
||||
"configureDefaults": "Configure Default Parameters",
|
||||
"configureDefaultsDescription": "Configure default values for tool parameters. When enabled, these values will be automatically used if the AI model doesn't provide them.",
|
||||
"defaultValue": "Default Value",
|
||||
"configSaved": "Tool configuration saved successfully",
|
||||
"hasDefaults": "DEFAULTS",
|
||||
"hasDefaultTooltip": "This tool has configured default parameters."
|
||||
},
|
||||
"prompts": {
|
||||
"availablePrompts": "Available Prompts",
|
||||
@@ -1565,6 +1582,9 @@
|
||||
"rate_limit": "Rate limiting",
|
||||
"tooltip": "You need to log in to Github before using Github Copilot"
|
||||
},
|
||||
"dmxapi": {
|
||||
"select_platform": "Select the platform"
|
||||
},
|
||||
"delete.content": "Are you sure you want to delete this provider?",
|
||||
"delete.title": "Delete Provider",
|
||||
"docs_check": "Check",
|
||||
|
||||
@@ -50,7 +50,10 @@
|
||||
"tag.system": "システム",
|
||||
"title": "エージェント",
|
||||
"settings": {
|
||||
"title": "エージェント設定"
|
||||
"title": "エージェント設定",
|
||||
"subscription": {
|
||||
"title": "エージェントサブスクリプション設定"
|
||||
}
|
||||
}
|
||||
},
|
||||
"assistants": {
|
||||
@@ -406,7 +409,10 @@
|
||||
"pinyin.asc": "ピンインで昇順ソート",
|
||||
"pinyin.desc": "ピンインで降順ソート"
|
||||
},
|
||||
"no_results": "検索結果なし"
|
||||
"no_results": "検索結果なし",
|
||||
"required": "必須",
|
||||
"allowed_values": "許可された値",
|
||||
"unsaved_changes": "未保存の変更があります"
|
||||
},
|
||||
"docs": {
|
||||
"title": "ドキュメント"
|
||||
@@ -599,9 +605,8 @@
|
||||
"delete.confirm.content": "選択した{{count}}件のメッセージを削除しますか?",
|
||||
"delete.failed": "削除に失敗しました",
|
||||
"delete.success": "削除が成功しました",
|
||||
"error.chunk_overlap_too_large": "チャンクの重なりは、チャンクサイズを超えることはできません",
|
||||
"empty_url": "画像をダウンロードできません。プロンプトに不適切なコンテンツや禁止用語が含まれている可能性があります",
|
||||
"error.chunk_overlap_too_large": "チャンクのオーバーラップがチャンクサイズより大きくなることはできません",
|
||||
"empty_url": "画像をダウンロードできません。プロンプトに不適切なコンテンツや禁止用語が含まれている可能性があります",
|
||||
"error.dimension_too_large": "内容のサイズが大きすぎます",
|
||||
"error.enter.api.host": "APIホストを入力してください",
|
||||
"error.enter.api.key": "APIキーを入力してください",
|
||||
@@ -814,6 +819,7 @@
|
||||
"regenerate.confirm": "これにより、既存の生成画像が置き換えられます。続行しますか?",
|
||||
"seed": "シード",
|
||||
"seed_tip": "同じシードとプロンプトで似た画像を生成できます",
|
||||
"seed_desc_tip": "同じシードとプロンプトで類似した画像を生成できますが、-1 に設定すると毎回異なる結果が生成されます",
|
||||
"title": "画像",
|
||||
"magic_prompt_option": "プロンプト強化",
|
||||
"model": "モデルバージョン",
|
||||
@@ -821,6 +827,7 @@
|
||||
"style_type": "スタイル",
|
||||
"learn_more": "詳しくはこちら",
|
||||
"prompt_placeholder_edit": "画像の説明を入力します。テキスト描画には '二重引用符' を使用します",
|
||||
"paint_course": "チュートリアル",
|
||||
"proxy_required": "現在、プロキシを開く必要があります。これは、将来サポートされる予定です",
|
||||
"image_file_required": "画像を先にアップロードしてください",
|
||||
"image_file_retry": "画像を先にアップロードしてください",
|
||||
@@ -886,7 +893,8 @@
|
||||
"magic_prompt_option_tip": "拡大効果を向上させるための提示詞を最適化します"
|
||||
},
|
||||
"rendering_speed": "レンダリング速度",
|
||||
"translating": "翻訳中..."
|
||||
"translating": "翻訳中...",
|
||||
"text_desc_required": "画像の説明を先に入力してください"
|
||||
},
|
||||
"prompts": {
|
||||
"explanation": "この概念を説明してください",
|
||||
@@ -1302,6 +1310,8 @@
|
||||
"stdio": "標準入力/出力 (stdio)",
|
||||
"inMemory": "メモリ",
|
||||
"config_description": "モデルコンテキストプロトコルサーバーの設定",
|
||||
"disable": "MCPサーバーを無効にする",
|
||||
"disable.description": "MCP機能を有効にしない",
|
||||
"deleteError": "サーバーの削除に失敗しました",
|
||||
"deleteSuccess": "サーバーが正常に削除されました",
|
||||
"dependenciesInstall": "依存関係をインストール",
|
||||
@@ -1365,7 +1375,13 @@
|
||||
"inputSchema": "入力スキーマ",
|
||||
"availableTools": "利用可能なツール",
|
||||
"noToolsAvailable": "利用可能なツールなし",
|
||||
"loadError": "ツール取得エラー"
|
||||
"loadError": "ツール取得エラー",
|
||||
"configureDefaults": "デフォルトパラメーターを設定",
|
||||
"configureDefaultsDescription": "ツールパラメーターのデフォルト値を設定します。有効にすると、AIモデルが提供しない場合、これらの値が自動的に使用されます。",
|
||||
"defaultValue": "デフォルト値",
|
||||
"configSaved": "ツール設定が正常に保存されました",
|
||||
"hasDefaults": "デフォルトあり",
|
||||
"hasDefaultTooltip": "このツールには、設定済みのデフォルトパラメーターがあります。"
|
||||
},
|
||||
"prompts": {
|
||||
"availablePrompts": "利用可能なプロンプト",
|
||||
@@ -1553,6 +1569,9 @@
|
||||
"rate_limit": "レート制限",
|
||||
"tooltip": "Github Copilot を使用するには、まず Github にログインする必要があります。"
|
||||
},
|
||||
"dmxapi": {
|
||||
"select_platform": "プラットフォームを選択"
|
||||
},
|
||||
"delete.content": "このプロバイダーを削除してもよろしいですか?",
|
||||
"delete.title": "プロバイダーを削除",
|
||||
"docs_check": "チェック",
|
||||
|
||||
@@ -50,7 +50,10 @@
|
||||
"agent": "Экспорт агента"
|
||||
},
|
||||
"settings": {
|
||||
"title": "Настройки агента"
|
||||
"title": "Настройки агента",
|
||||
"subscription": {
|
||||
"title": "Конфигурация подписки агента"
|
||||
}
|
||||
}
|
||||
},
|
||||
"assistants": {
|
||||
@@ -406,7 +409,10 @@
|
||||
"pinyin.asc": "Сортировать по пиньинь (А-Я)",
|
||||
"pinyin.desc": "Сортировать по пиньинь (Я-А)"
|
||||
},
|
||||
"no_results": "Результатов не найдено"
|
||||
"no_results": "Результатов не найдено",
|
||||
"required": "Обязательно",
|
||||
"allowed_values": "Допустимые значения",
|
||||
"unsaved_changes": "У вас есть несохраненные изменения"
|
||||
},
|
||||
"docs": {
|
||||
"title": "Документация"
|
||||
@@ -813,6 +819,7 @@
|
||||
"regenerate.confirm": "Это заменит ваши существующие сгенерированные изображения. Хотите продолжить?",
|
||||
"seed": "Ключ генерации",
|
||||
"seed_tip": "Одинаковый ключ генерации и промпт могут производить похожие изображения",
|
||||
"seed_desc_tip": "Одинаковые сиды и промпты могут генерировать похожие изображения, установка -1 будет создавать разные результаты каждый раз",
|
||||
"title": "Изображения",
|
||||
"magic_prompt_option": "Улучшение промпта",
|
||||
"model": "Версия",
|
||||
@@ -821,6 +828,7 @@
|
||||
"rendering_speed": "Скорость рендеринга",
|
||||
"learn_more": "Узнать больше",
|
||||
"prompt_placeholder_edit": "Введите ваше описание изображения, текстовая отрисовка использует двойные кавычки для обертки",
|
||||
"paint_course": "Руководство / Учебник",
|
||||
"proxy_required": "Сейчас необходимо открыть прокси для просмотра сгенерированных изображений, в будущем будет поддерживаться прямое соединение",
|
||||
"image_file_required": "Пожалуйста, сначала загрузите изображение",
|
||||
"image_file_retry": "Пожалуйста, сначала загрузите изображение",
|
||||
@@ -886,7 +894,7 @@
|
||||
"seed_tip": "Контролирует случайный характер увеличения изображений для воспроизводимых результатов",
|
||||
"magic_prompt_option_tip": "Улучшает увеличение изображений с помощью интеллектуального оптимизирования промптов"
|
||||
},
|
||||
"rendering_speed": "Скорость рендеринга"
|
||||
"text_desc_required": "Пожалуйста, сначала введите описание изображения"
|
||||
},
|
||||
"prompts": {
|
||||
"explanation": "Объясните мне этот концепт",
|
||||
@@ -1302,6 +1310,8 @@
|
||||
"stdio": "Стандартный ввод/вывод (stdio)",
|
||||
"inMemory": "Память",
|
||||
"config_description": "Настройка серверов протокола контекста модели",
|
||||
"disable": "Отключить сервер MCP",
|
||||
"disable.description": "Не включать функциональность сервера MCP",
|
||||
"deleteError": "Не удалось удалить сервер",
|
||||
"deleteSuccess": "Сервер успешно удален",
|
||||
"dependenciesInstall": "Установить зависимости",
|
||||
@@ -1365,7 +1375,13 @@
|
||||
"inputSchema": "Схема ввода",
|
||||
"availableTools": "Доступные инструменты",
|
||||
"noToolsAvailable": "Нет доступных инструментов",
|
||||
"loadError": "Ошибка получения инструментов"
|
||||
"loadError": "Ошибка получения инструментов",
|
||||
"configureDefaults": "Настроить параметры по умолчанию",
|
||||
"configureDefaultsDescription": "Настройте значения по умолчанию для параметров инструмента. Когда эта функция включена, эти значения будут использоваться автоматически, если их не предоставит AI модель.",
|
||||
"defaultValue": "Значение по умолчанию",
|
||||
"configSaved": "Конфигурация инструмента успешно сохранена",
|
||||
"hasDefaults": "ЗНАЧЕНИЯ ПО УМОЛЧАНИЮ",
|
||||
"hasDefaultTooltip": "Для этого инструмента настроены параметры по умолчанию."
|
||||
},
|
||||
"prompts": {
|
||||
"availablePrompts": "Доступные подсказки",
|
||||
@@ -1553,6 +1569,9 @@
|
||||
"rate_limit": "Ограничение скорости",
|
||||
"tooltip": "Для использования Github Copilot необходимо сначала войти в Github."
|
||||
},
|
||||
"dmxapi": {
|
||||
"select_platform": "Выберите платформу"
|
||||
},
|
||||
"delete.content": "Вы уверены, что хотите удалить этот провайдер?",
|
||||
"delete.title": "Удалить провайдер",
|
||||
"docs_check": "Проверить",
|
||||
|
||||
@@ -50,7 +50,10 @@
|
||||
"tag.system": "系统",
|
||||
"title": "智能体",
|
||||
"settings": {
|
||||
"title": "智能体配置"
|
||||
"title": "智能体配置",
|
||||
"subscription": {
|
||||
"title": "智能体订阅配置"
|
||||
}
|
||||
}
|
||||
},
|
||||
"assistants": {
|
||||
@@ -392,7 +395,10 @@
|
||||
"regenerate": "重新生成",
|
||||
"rename": "重命名",
|
||||
"reset": "重置",
|
||||
"required": "必填",
|
||||
"allowed_values": "允许的值",
|
||||
"save": "保存",
|
||||
"unsaved_changes": "您有未保存的更改",
|
||||
"search": "搜索",
|
||||
"select": "选择",
|
||||
"selectedMessages": "选中 {{count}} 条消息",
|
||||
@@ -813,6 +819,7 @@
|
||||
"regenerate.confirm": "这将覆盖已生成的图片,是否继续?",
|
||||
"seed": "随机种子",
|
||||
"seed_tip": "相同的种子和提示词可以生成相似的图片",
|
||||
"seed_desc_tip": "相同的种子和提示词可以生成相似的图片,设置 -1 每次生成都不一样",
|
||||
"title": "图片",
|
||||
"magic_prompt_option": "提示词增强",
|
||||
"model": "版本",
|
||||
@@ -820,6 +827,7 @@
|
||||
"style_type": "风格",
|
||||
"rendering_speed": "渲染速度",
|
||||
"learn_more": "了解更多",
|
||||
"paint_course": "教程",
|
||||
"prompt_placeholder_edit": "输入你的图片描述,文本绘制用 \"双引号\" 包裹",
|
||||
"proxy_required": "目前需要打开代理才能查看生成图片,后续会支持国内直连",
|
||||
"image_file_required": "请先上传图片",
|
||||
@@ -885,7 +893,8 @@
|
||||
"number_images_tip": "生成的放大结果数量",
|
||||
"seed_tip": "控制放大结果的随机性",
|
||||
"magic_prompt_option_tip": "智能优化放大提示词"
|
||||
}
|
||||
},
|
||||
"text_desc_required": "请先输入图片描述"
|
||||
},
|
||||
"prompts": {
|
||||
"explanation": "帮我解释一下这个概念",
|
||||
@@ -1305,6 +1314,8 @@
|
||||
"stdio": "标准输入/输出 (stdio)",
|
||||
"inMemory": "内存",
|
||||
"config_description": "配置模型上下文协议服务器",
|
||||
"disable": "不使用 MCP 服务器",
|
||||
"disable.description": "不启用 MCP 服务功能",
|
||||
"deleteError": "删除服务器失败",
|
||||
"deleteSuccess": "服务器删除成功",
|
||||
"dependenciesInstall": "安装依赖项",
|
||||
@@ -1368,7 +1379,13 @@
|
||||
"inputSchema": "输入模式",
|
||||
"availableTools": "可用工具",
|
||||
"noToolsAvailable": "无可用工具",
|
||||
"loadError": "获取工具失败"
|
||||
"loadError": "获取工具失败",
|
||||
"configureDefaults": "配置默认参数",
|
||||
"configureDefaultsDescription": "配置工具参数的默认值。启用后,如果AI模型没有提供这些参数,将自动使用这些默认值。",
|
||||
"defaultValue": "默认值",
|
||||
"configSaved": "工具配置保存成功",
|
||||
"hasDefaults": "有默认值",
|
||||
"hasDefaultTooltip": "此工具已配置默认参数。"
|
||||
},
|
||||
"prompts": {
|
||||
"availablePrompts": "可用提示",
|
||||
@@ -1565,6 +1582,9 @@
|
||||
"rate_limit": "速率限制",
|
||||
"tooltip": "使用 Github Copilot 需要先登录 Github"
|
||||
},
|
||||
"dmxapi": {
|
||||
"select_platform": "选择平台"
|
||||
},
|
||||
"delete.content": "确定要删除此模型提供商吗?",
|
||||
"delete.title": "删除提供商",
|
||||
"docs_check": "查看",
|
||||
|
||||
@@ -50,7 +50,10 @@
|
||||
"tag.system": "系統",
|
||||
"title": "智慧代理人",
|
||||
"settings": {
|
||||
"title": "智慧代理人設定"
|
||||
"title": "智慧代理人設定",
|
||||
"subscription": {
|
||||
"title": "智慧代理人訂閱設定"
|
||||
}
|
||||
}
|
||||
},
|
||||
"assistants": {
|
||||
@@ -406,7 +409,10 @@
|
||||
"pinyin.asc": "按拼音升序",
|
||||
"pinyin.desc": "按拼音降序"
|
||||
},
|
||||
"no_results": "沒有結果"
|
||||
"no_results": "沒有結果",
|
||||
"required": "必填",
|
||||
"allowed_values": "允許的值",
|
||||
"unsaved_changes": "您有未儲存的變更"
|
||||
},
|
||||
"docs": {
|
||||
"title": "說明文件"
|
||||
@@ -594,12 +600,11 @@
|
||||
"citations": "引用內容",
|
||||
"copied": "已複製!",
|
||||
"copy.failed": "複製失敗",
|
||||
"copy.success": "已複製!",
|
||||
"copy.success": "複製成功",
|
||||
"delete.confirm.title": "刪除確認",
|
||||
"delete.confirm.content": "確認刪除選中的 {{count}} 條訊息嗎?",
|
||||
"delete.failed": "刪除失敗",
|
||||
"delete.success": "刪除成功",
|
||||
"copy.success": "複製成功",
|
||||
"empty_url": "無法下載圖片,可能是提示詞包含敏感內容或違禁詞彙",
|
||||
"error.chunk_overlap_too_large": "分段重疊不能大於分段大小",
|
||||
"error.dimension_too_large": "內容尺寸過大",
|
||||
@@ -814,6 +819,7 @@
|
||||
"regenerate.confirm": "這將覆蓋已生成的圖片,是否繼續?",
|
||||
"seed": "隨機種子",
|
||||
"seed_tip": "相同的種子和提示詞可以生成相似的圖片",
|
||||
"seed_desc_tip": "相同的種子和提示詞可以生成相似的圖片,設置 -1 每次生成都不一樣",
|
||||
"title": "繪圖",
|
||||
"magic_prompt_option": "提示詞增強",
|
||||
"model": "版本",
|
||||
@@ -821,6 +827,7 @@
|
||||
"style_type": "風格",
|
||||
"learn_more": "了解更多",
|
||||
"prompt_placeholder_edit": "輸入你的圖片描述,文本繪製用 '雙引號' 包裹",
|
||||
"paint_course": "教程",
|
||||
"proxy_required": "目前需要打開代理才能查看生成圖片,後續會支持國內直連",
|
||||
"image_file_required": "請先上傳圖片",
|
||||
"image_file_retry": "請重新上傳圖片",
|
||||
@@ -886,7 +893,8 @@
|
||||
"seed_tip": "控制放大結果的隨機性",
|
||||
"magic_prompt_option_tip": "智能優化放大提示詞"
|
||||
},
|
||||
"rendering_speed": "渲染速度"
|
||||
"rendering_speed": "渲染速度",
|
||||
"text_desc_required": "請先輸入圖片描述"
|
||||
},
|
||||
"prompts": {
|
||||
"explanation": "幫我解釋一下這個概念",
|
||||
@@ -1305,6 +1313,8 @@
|
||||
"stdio": "標準輸入/輸出 (stdio)",
|
||||
"inMemory": "記憶體",
|
||||
"config_description": "設定模型上下文協議伺服器",
|
||||
"disable": "不使用 MCP 伺服器",
|
||||
"disable.description": "不啟用 MCP 服務功能",
|
||||
"deleteError": "刪除伺服器失敗",
|
||||
"deleteSuccess": "伺服器刪除成功",
|
||||
"dependenciesInstall": "安裝相依套件",
|
||||
@@ -1368,7 +1378,13 @@
|
||||
"inputSchema": "輸入模式",
|
||||
"availableTools": "可用工具",
|
||||
"noToolsAvailable": "無可用工具",
|
||||
"loadError": "獲取工具失敗"
|
||||
"loadError": "獲取工具失敗",
|
||||
"configureDefaults": "配置預設參數",
|
||||
"configureDefaultsDescription": "配置工具參數的預設值。啟用後,如果 AI 模型未提供這些值,將自動使用。",
|
||||
"defaultValue": "預設值",
|
||||
"configSaved": "工具配置已成功保存",
|
||||
"hasDefaults": "預設值已配置",
|
||||
"hasDefaultTooltip": "此工具已配置預設參數。"
|
||||
},
|
||||
"prompts": {
|
||||
"availablePrompts": "可用提示",
|
||||
@@ -1556,6 +1572,9 @@
|
||||
"rate_limit": "速率限制",
|
||||
"tooltip": "使用 Github Copilot 需要先登入 Github"
|
||||
},
|
||||
"dmxapi": {
|
||||
"select_platform": "選擇平臺"
|
||||
},
|
||||
"delete.content": "確定要刪除此提供者嗎?",
|
||||
"delete.title": "刪除提供者",
|
||||
"docs_check": "檢查",
|
||||
|
||||
@@ -36,9 +36,9 @@ const Chat: FC<Props> = (props) => {
|
||||
|
||||
const maxWidth = useMemo(() => {
|
||||
const showRightTopics = showTopics && topicPosition === 'right'
|
||||
const minusAssistantsWidth = showAssistants ? '- var(--assistants-width)' : ''
|
||||
const minusRightTopicsWidth = showRightTopics ? '- var(--assistants-width)' : ''
|
||||
return `calc(100vw - var(--sidebar-width) ${minusAssistantsWidth} ${minusRightTopicsWidth} - 5px)`
|
||||
const minusAssistantsWidth = showAssistants ? `- var(--assistants-width) - var(--scrollbar-width)` : ''
|
||||
const minusRightTopicsWidth = showRightTopics ? `- var(--assistants-width) - var(--scrollbar-width)` : ''
|
||||
return `calc(100vw - var(--sidebar-width) ${minusAssistantsWidth} ${minusRightTopicsWidth})`
|
||||
}, [showAssistants, showTopics, topicPosition])
|
||||
|
||||
useHotkeys('esc', () => {
|
||||
|
||||
@@ -4,9 +4,8 @@ import { useMCPServers } from '@renderer/hooks/useMCPServers'
|
||||
import { EventEmitter } from '@renderer/services/EventService'
|
||||
import { Assistant, MCPPrompt, MCPResource, MCPServer } from '@renderer/types'
|
||||
import { Form, Input, Tooltip } from 'antd'
|
||||
import { Plus, SquareTerminal } from 'lucide-react'
|
||||
import { FC, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react'
|
||||
import React from 'react'
|
||||
import { CircleX, Plus, SquareTerminal } from 'lucide-react'
|
||||
import React, { FC, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useNavigate } from 'react-router'
|
||||
|
||||
@@ -132,9 +131,6 @@ const MCPToolsButton: FC<Props> = ({ ref, setInputValue, resizeTextArea, Toolbar
|
||||
() => activedMcpServers.filter((server) => mcpServers.some((s) => s.id === server.id)),
|
||||
[activedMcpServers, mcpServers]
|
||||
)
|
||||
|
||||
const buttonEnabled = assistantMcpServers.length > 0
|
||||
|
||||
const handleMcpServerSelect = useCallback(
|
||||
(server: MCPServer) => {
|
||||
if (assistantMcpServers.some((s) => s.id === server.id)) {
|
||||
@@ -156,6 +152,18 @@ const MCPToolsButton: FC<Props> = ({ ref, setInputValue, resizeTextArea, Toolbar
|
||||
return () => EventEmitter.off('mcp-server-select', handler)
|
||||
}, [])
|
||||
|
||||
const updateMcpEnabled = useCallback(
|
||||
(enabled: boolean) => {
|
||||
setTimeout(() => {
|
||||
updateAssistant({
|
||||
...assistant,
|
||||
mcpServers: enabled ? assistant.mcpServers || [] : []
|
||||
})
|
||||
}, 200)
|
||||
},
|
||||
[assistant, updateAssistant]
|
||||
)
|
||||
|
||||
const menuItems = useMemo(() => {
|
||||
const newList: QuickPanelListItem[] = activedMcpServers.map((server) => ({
|
||||
label: server.name,
|
||||
@@ -171,8 +179,16 @@ const MCPToolsButton: FC<Props> = ({ ref, setInputValue, resizeTextArea, Toolbar
|
||||
action: () => navigate('/settings/mcp')
|
||||
})
|
||||
|
||||
newList.unshift({
|
||||
label: t('common.close'),
|
||||
description: t('settings.mcp.disable.description'),
|
||||
icon: <CircleX />,
|
||||
isSelected: !(assistant.mcpServers && assistant.mcpServers.length > 0),
|
||||
action: () => updateMcpEnabled(false)
|
||||
})
|
||||
|
||||
return newList
|
||||
}, [activedMcpServers, t, assistantMcpServers, navigate])
|
||||
}, [activedMcpServers, t, assistant.mcpServers, assistantMcpServers, navigate, updateMcpEnabled])
|
||||
|
||||
const openQuickPanel = useCallback(() => {
|
||||
quickPanel.open({
|
||||
@@ -412,10 +428,9 @@ const MCPToolsButton: FC<Props> = ({ ref, setInputValue, resizeTextArea, Toolbar
|
||||
}, [activedMcpServers])
|
||||
|
||||
const openResourcesList = useCallback(async () => {
|
||||
const resources = resourcesList
|
||||
quickPanel.open({
|
||||
title: t('settings.mcp.title'),
|
||||
list: resources,
|
||||
list: resourcesList,
|
||||
symbol: 'mcp-resource',
|
||||
multiple: true
|
||||
})
|
||||
@@ -442,7 +457,10 @@ const MCPToolsButton: FC<Props> = ({ ref, setInputValue, resizeTextArea, Toolbar
|
||||
return (
|
||||
<Tooltip placement="top" title={t('settings.mcp.title')} arrow>
|
||||
<ToolbarButton type="text" onClick={handleOpenQuickPanel}>
|
||||
<SquareTerminal size={18} color={buttonEnabled ? 'var(--color-primary)' : 'var(--color-icon)'} />
|
||||
<SquareTerminal
|
||||
size={18}
|
||||
color={assistant.mcpServers && assistant.mcpServers.length > 0 ? 'var(--color-primary)' : 'var(--color-icon)'}
|
||||
/>
|
||||
</ToolbarButton>
|
||||
</Tooltip>
|
||||
)
|
||||
|
||||
@@ -6,7 +6,7 @@ import WebSearchService from '@renderer/services/WebSearchService'
|
||||
import { Assistant, WebSearchProvider } from '@renderer/types'
|
||||
import { hasObjectKey } from '@renderer/utils'
|
||||
import { Tooltip } from 'antd'
|
||||
import { Globe, Settings } from 'lucide-react'
|
||||
import { CircleX, Globe, Settings } from 'lucide-react'
|
||||
import { FC, memo, useCallback, useImperativeHandle, useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
@@ -85,9 +85,9 @@ const WebSearchButton: FC<Props> = ({ ref, assistant, ToolbarButton }) => {
|
||||
})
|
||||
|
||||
items.unshift({
|
||||
label: t('chat.input.web_search.no_web_search'),
|
||||
label: t('common.close'),
|
||||
description: t('chat.input.web_search.no_web_search.description'),
|
||||
icon: <Globe />,
|
||||
icon: <CircleX />,
|
||||
isSelected: !assistant.enableWebSearch && !assistant.webSearchProviderId,
|
||||
action: () => {
|
||||
updateSelectedWebSearchProvider(undefined)
|
||||
|
||||
@@ -163,7 +163,9 @@ const MainTextBlock: React.FC<Props> = ({ block, citationBlockId, role, mentions
|
||||
</Flex>
|
||||
)}
|
||||
{role === 'user' && !renderInputMessageAsMarkdown ? (
|
||||
<p style={{ marginBottom: 5, whiteSpace: 'pre-wrap' }}>{block.content}</p>
|
||||
<p className="markdown" style={{ marginBottom: 5 }}>
|
||||
{block.content}
|
||||
</p>
|
||||
) : (
|
||||
<Markdown block={{ ...block, content: ignoreToolUse }} />
|
||||
)}
|
||||
|
||||
@@ -2,11 +2,13 @@ import SvgSpinners180Ring from '@renderer/components/Icons/SvgSpinners180Ring'
|
||||
import Scrollbar from '@renderer/components/Scrollbar'
|
||||
import { LOAD_MORE_COUNT } from '@renderer/config/constant'
|
||||
import { useAssistant } from '@renderer/hooks/useAssistant'
|
||||
import { useChatContext } from '@renderer/hooks/useChatContext'
|
||||
import { useMessageOperations, useTopicMessages } from '@renderer/hooks/useMessageOperations'
|
||||
import useScrollPosition from '@renderer/hooks/useScrollPosition'
|
||||
import { useSettings } from '@renderer/hooks/useSettings'
|
||||
import { useShortcut } from '@renderer/hooks/useShortcuts'
|
||||
import { autoRenameTopic, getTopic } from '@renderer/hooks/useTopic'
|
||||
import SelectionBox from '@renderer/pages/home/Messages/SelectionBox'
|
||||
import { getDefaultTopic } from '@renderer/services/AssistantService'
|
||||
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
|
||||
import { getContextCount, getGroupedMessages, getUserMessage } from '@renderer/services/MessagesService'
|
||||
@@ -64,7 +66,7 @@ const Messages: React.FC<MessagesProps> = ({ assistant, topic, setActiveTopic, o
|
||||
const { displayCount, clearTopicMessages, deleteMessage, createTopicBranch } = useMessageOperations(topic)
|
||||
const messagesRef = useRef<Message[]>(messages)
|
||||
|
||||
// const { isMultiSelectMode, handleSelectMessage } = useChatContext(topic)
|
||||
const { isMultiSelectMode, handleSelectMessage } = useChatContext(topic)
|
||||
|
||||
useEffect(() => {
|
||||
messagesRef.current = messages
|
||||
@@ -86,9 +88,9 @@ const Messages: React.FC<MessagesProps> = ({ assistant, topic, setActiveTopic, o
|
||||
|
||||
const maxWidth = useMemo(() => {
|
||||
const showRightTopics = showTopics && topicPosition === 'right'
|
||||
const minusAssistantsWidth = showAssistants ? '- var(--assistants-width)' : ''
|
||||
const minusRightTopicsWidth = showRightTopics ? '- var(--assistants-width)' : ''
|
||||
return `calc(100vw - var(--sidebar-width) ${minusAssistantsWidth} ${minusRightTopicsWidth} - 5px)`
|
||||
const minusAssistantsWidth = showAssistants ? `- var(--assistants-width) - var(--scrollbar-width)` : ''
|
||||
const minusRightTopicsWidth = showRightTopics ? `- var(--assistants-width) - var(--scrollbar-width)` : ''
|
||||
return `calc(100vw - var(--sidebar-width) ${minusAssistantsWidth} ${minusRightTopicsWidth})`
|
||||
}, [showAssistants, showTopics, topicPosition])
|
||||
|
||||
const scrollToBottom = useCallback(() => {
|
||||
@@ -313,13 +315,13 @@ const Messages: React.FC<MessagesProps> = ({ assistant, topic, setActiveTopic, o
|
||||
</NarrowLayout>
|
||||
{messageNavigation === 'anchor' && <MessageAnchorLine messages={displayMessages} />}
|
||||
{messageNavigation === 'buttons' && <ChatNavigation containerId="messages" />}
|
||||
{/* TODO: 多选功能实现有问题,需要重新改改 */}
|
||||
{/* <SelectionBox
|
||||
|
||||
<SelectionBox
|
||||
isMultiSelectMode={isMultiSelectMode}
|
||||
scrollContainerRef={scrollContainerRef}
|
||||
messageElements={messageElements.current}
|
||||
handleSelectMessage={handleSelectMessage}
|
||||
/> */}
|
||||
/>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import styled from 'styled-components'
|
||||
|
||||
interface SelectionBoxProps {
|
||||
@@ -18,6 +18,8 @@ const SelectionBox: React.FC<SelectionBoxProps> = ({
|
||||
const [dragStart, setDragStart] = useState({ x: 0, y: 0 })
|
||||
const [dragCurrent, setDragCurrent] = useState({ x: 0, y: 0 })
|
||||
|
||||
const dragSelectedIds = useRef<Set<string>>(new Set())
|
||||
|
||||
useEffect(() => {
|
||||
if (!isMultiSelectMode) return
|
||||
|
||||
@@ -25,96 +27,90 @@ const SelectionBox: React.FC<SelectionBoxProps> = ({
|
||||
const container = scrollContainerRef.current!
|
||||
if (!container) return { x: 0, y: 0 }
|
||||
const rect = container.getBoundingClientRect()
|
||||
const x = e.clientX - rect.left + container.scrollLeft
|
||||
const y = e.clientY - rect.top + container.scrollTop
|
||||
return { x, y }
|
||||
return {
|
||||
x: e.clientX - rect.left + container.scrollLeft,
|
||||
y: e.clientY - rect.top + container.scrollTop
|
||||
}
|
||||
}
|
||||
|
||||
const handleMouseDown = (e: MouseEvent) => {
|
||||
if ((e.target as HTMLElement).closest('.ant-checkbox-wrapper')) return
|
||||
if ((e.target as HTMLElement).closest('.MessageFooter')) return
|
||||
|
||||
e.preventDefault()
|
||||
|
||||
setIsDragging(true)
|
||||
const pos = updateDragPos(e)
|
||||
setDragStart(pos)
|
||||
setDragCurrent(pos)
|
||||
dragSelectedIds.current.clear()
|
||||
document.body.classList.add('no-select')
|
||||
}
|
||||
|
||||
const handleMouseMove = (e: MouseEvent) => {
|
||||
if (!isDragging) return
|
||||
setDragCurrent(updateDragPos(e))
|
||||
const container = scrollContainerRef.current!
|
||||
if (container) {
|
||||
const { top, bottom } = container.getBoundingClientRect()
|
||||
const scrollSpeed = 15
|
||||
if (e.clientY < top + 50) {
|
||||
container.scrollBy(0, -scrollSpeed)
|
||||
} else if (e.clientY > bottom - 50) {
|
||||
container.scrollBy(0, scrollSpeed)
|
||||
|
||||
e.preventDefault()
|
||||
|
||||
const pos = updateDragPos(e)
|
||||
setDragCurrent(pos)
|
||||
|
||||
// 计算当前框选矩形
|
||||
const left = Math.min(dragStart.x, pos.x)
|
||||
const right = Math.max(dragStart.x, pos.x)
|
||||
const top = Math.min(dragStart.y, pos.y)
|
||||
const bottom = Math.max(dragStart.y, pos.y)
|
||||
|
||||
// 创建新选中的消息ID集合
|
||||
const newSelectedIds = new Set<string>()
|
||||
|
||||
messageElements.forEach((el, id) => {
|
||||
// 检查消息是否已被选中(不管是拖动选中还是手动选中)
|
||||
const checkbox = el.querySelector('input[type="checkbox"]') as HTMLInputElement | null
|
||||
const isAlreadySelected = checkbox?.checked || false
|
||||
|
||||
// 如果已经被记录为拖动选中,跳过
|
||||
if (dragSelectedIds.current.has(id)) return
|
||||
|
||||
const rect = el.getBoundingClientRect()
|
||||
const container = scrollContainerRef.current!
|
||||
const eTop = rect.top - container.getBoundingClientRect().top + container.scrollTop
|
||||
const eLeft = rect.left - container.getBoundingClientRect().left + container.scrollLeft
|
||||
const eBottom = eTop + rect.height
|
||||
const eRight = eLeft + rect.width
|
||||
|
||||
// 检查消息是否在当前选择框内
|
||||
const isInSelectionBox = !(eRight < left || eLeft > right || eBottom < top || eTop > bottom)
|
||||
|
||||
// 只有在选择框内且未被选中的消息才需要处理
|
||||
if (isInSelectionBox && !isAlreadySelected) {
|
||||
handleSelectMessage(id, true)
|
||||
dragSelectedIds.current.add(id)
|
||||
newSelectedIds.add(id)
|
||||
el.classList.add('selection-highlight')
|
||||
setTimeout(() => el.classList.remove('selection-highlight'), 300)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const handleMouseUp = () => {
|
||||
if (!isDragging) return
|
||||
|
||||
const left = Math.min(dragStart.x, dragCurrent.x)
|
||||
const right = Math.max(dragStart.x, dragCurrent.x)
|
||||
const top = Math.min(dragStart.y, dragCurrent.y)
|
||||
const bottom = Math.max(dragStart.y, dragCurrent.y)
|
||||
|
||||
const MIN_SELECTION_SIZE = 5
|
||||
const isValidSelection =
|
||||
Math.abs(right - left) > MIN_SELECTION_SIZE && Math.abs(bottom - top) > MIN_SELECTION_SIZE
|
||||
|
||||
if (isValidSelection) {
|
||||
messageElements.forEach((element, messageId) => {
|
||||
try {
|
||||
const rect = element.getBoundingClientRect()
|
||||
const container = scrollContainerRef.current!
|
||||
|
||||
const elementTop = rect.top - container.getBoundingClientRect().top + container.scrollTop
|
||||
const elementLeft = rect.left - container.getBoundingClientRect().left + container.scrollLeft
|
||||
const elementBottom = elementTop + rect.height
|
||||
const elementRight = elementLeft + rect.width
|
||||
|
||||
const isIntersecting = !(
|
||||
elementRight < left ||
|
||||
elementLeft > right ||
|
||||
elementBottom < top ||
|
||||
elementTop > bottom
|
||||
)
|
||||
|
||||
if (isIntersecting) {
|
||||
handleSelectMessage(messageId, true)
|
||||
element.classList.add('selection-highlight')
|
||||
setTimeout(() => element.classList.remove('selection-highlight'), 300)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error calculating element intersection:', error)
|
||||
}
|
||||
})
|
||||
}
|
||||
setIsDragging(false)
|
||||
document.body.classList.remove('no-select')
|
||||
}
|
||||
|
||||
const container = scrollContainerRef.current!
|
||||
if (container) {
|
||||
container.addEventListener('mousedown', handleMouseDown)
|
||||
window.addEventListener('mousemove', handleMouseMove)
|
||||
window.addEventListener('mouseup', handleMouseUp)
|
||||
}
|
||||
container?.addEventListener('mousedown', handleMouseDown)
|
||||
window.addEventListener('mousemove', handleMouseMove)
|
||||
window.addEventListener('mouseup', handleMouseUp)
|
||||
|
||||
return () => {
|
||||
if (container) {
|
||||
container.removeEventListener('mousedown', handleMouseDown)
|
||||
window.removeEventListener('mousemove', handleMouseMove)
|
||||
window.removeEventListener('mouseup', handleMouseUp)
|
||||
document.body.classList.remove('no-select')
|
||||
}
|
||||
container?.removeEventListener('mousedown', handleMouseDown)
|
||||
window.removeEventListener('mousemove', handleMouseMove)
|
||||
window.removeEventListener('mouseup', handleMouseUp)
|
||||
document.body.classList.remove('no-select')
|
||||
}
|
||||
}, [isMultiSelectMode, isDragging, dragStart, dragCurrent, handleSelectMessage, scrollContainerRef, messageElements])
|
||||
}, [isMultiSelectMode, isDragging, dragStart, scrollContainerRef, messageElements, handleSelectMessage])
|
||||
|
||||
if (!isDragging || !isMultiSelectMode) return null
|
||||
|
||||
|
||||
702
src/renderer/src/pages/paintings/DmxapiPage.tsx
Normal file
702
src/renderer/src/pages/paintings/DmxapiPage.tsx
Normal file
@@ -0,0 +1,702 @@
|
||||
import { PlusOutlined, RedoOutlined } from '@ant-design/icons'
|
||||
import DMXAPIToImg from '@renderer/assets/images/providers/DMXAPI-to-img.webp'
|
||||
import { Navbar, NavbarCenter, NavbarRight } from '@renderer/components/app/Navbar'
|
||||
import { VStack } from '@renderer/components/Layout'
|
||||
import Scrollbar from '@renderer/components/Scrollbar'
|
||||
import { isMac } from '@renderer/config/constant'
|
||||
import { getProviderLogo } from '@renderer/config/providers'
|
||||
import { useTheme } from '@renderer/context/ThemeProvider'
|
||||
import { usePaintings } from '@renderer/hooks/usePaintings'
|
||||
import { useAllProviders } from '@renderer/hooks/useProvider'
|
||||
import { useRuntime } from '@renderer/hooks/useRuntime'
|
||||
import { useSettings } from '@renderer/hooks/useSettings'
|
||||
import FileManager from '@renderer/services/FileManager'
|
||||
import { translateText } from '@renderer/services/TranslateService'
|
||||
import { useAppDispatch } from '@renderer/store'
|
||||
import { setGenerating } from '@renderer/store/runtime'
|
||||
import type { FileType, PaintingsState } from '@renderer/types'
|
||||
import { getErrorMessage, uuid } from '@renderer/utils'
|
||||
import { DmxapiPainting, PaintingAction } from '@types'
|
||||
import { Avatar, Button, Input, Radio, Select, Tooltip } from 'antd'
|
||||
import TextArea from 'antd/es/input/TextArea'
|
||||
import { Info } from 'lucide-react'
|
||||
import React, { FC } from 'react'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useLocation, useNavigate } from 'react-router-dom'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import SendMessageButton from '../home/Inputbar/SendMessageButton'
|
||||
import { SettingHelpLink, SettingTitle } from '../settings'
|
||||
import Artboard from './Artboard'
|
||||
import {
|
||||
COURSE_URL,
|
||||
DEFAULT_PAINTING,
|
||||
IMAGE_SIZES,
|
||||
STYLE_TYPE_OPTIONS,
|
||||
TEXT_TO_IMAGES_MODELS
|
||||
} from './config/DmxapiConfig'
|
||||
import PaintingsList from './PaintingsList'
|
||||
|
||||
const generateRandomSeed = () => Math.floor(Math.random() * 1000000).toString()
|
||||
|
||||
const DmxapiPage: FC<{ Options: string[] }> = ({ Options }) => {
|
||||
const [mode] = useState<keyof PaintingsState>('DMXAPIPaintings')
|
||||
const { DMXAPIPaintings, addPainting, removePainting, updatePainting } = usePaintings()
|
||||
const [painting, setPainting] = useState<DmxapiPainting>(DMXAPIPaintings?.[0] || DEFAULT_PAINTING)
|
||||
const { theme } = useTheme()
|
||||
const { t } = useTranslation()
|
||||
const providers = useAllProviders()
|
||||
const providerOptions = Options.map((option) => {
|
||||
const provider = providers.find((p) => p.id === option)
|
||||
return {
|
||||
label: t(`provider.${provider?.id}`),
|
||||
value: provider?.id
|
||||
}
|
||||
})
|
||||
|
||||
const dmxapiProvider = providers.find((p) => p.id === 'dmxapi')!
|
||||
|
||||
const [currentImageIndex, setCurrentImageIndex] = useState(0)
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [abortController, setAbortController] = useState<AbortController | null>(null)
|
||||
const dispatch = useAppDispatch()
|
||||
const { generating } = useRuntime()
|
||||
const navigate = useNavigate()
|
||||
const location = useLocation()
|
||||
|
||||
const getNewPainting = () => {
|
||||
return {
|
||||
...DEFAULT_PAINTING,
|
||||
id: uuid(),
|
||||
seed: generateRandomSeed()
|
||||
}
|
||||
}
|
||||
|
||||
const modelOptions = TEXT_TO_IMAGES_MODELS.map((model) => ({
|
||||
label: model.name,
|
||||
value: model.id
|
||||
}))
|
||||
|
||||
const textareaRef = useRef<any>(null)
|
||||
|
||||
const updatePaintingState = (updates: Partial<DmxapiPainting>) => {
|
||||
const updatedPainting = { ...painting, ...updates }
|
||||
setPainting(updatedPainting)
|
||||
updatePainting('DMXAPIPaintings', updatedPainting)
|
||||
}
|
||||
|
||||
const onSelectModel = (modelId: string) => {
|
||||
const model = TEXT_TO_IMAGES_MODELS.find((m) => m.id === modelId)
|
||||
if (model) {
|
||||
updatePaintingState({ model: modelId })
|
||||
}
|
||||
}
|
||||
|
||||
const onCancel = () => {
|
||||
abortController?.abort()
|
||||
}
|
||||
|
||||
const onSelectImageSize = (v: string) => {
|
||||
const size = IMAGE_SIZES.find((i) => i.value === v)
|
||||
size && updatePaintingState({ image_size: size.value, aspect_ratio: size.label })
|
||||
}
|
||||
|
||||
const onSelectStyleType = (v: string) => {
|
||||
if (v === painting.style_type) {
|
||||
updatePaintingState({ style_type: '' })
|
||||
} else {
|
||||
updatePaintingState({ style_type: v })
|
||||
}
|
||||
}
|
||||
|
||||
const onInputSeed = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const value = e.target.value
|
||||
// 允许空值或合法整数,且大于等于 -1
|
||||
if (value === '' || value === '-' || /^-?\d+$/.test(value)) {
|
||||
const numValue = parseInt(value, 10)
|
||||
|
||||
if (numValue >= -1 || value === '' || value === '-') {
|
||||
updatePaintingState({ seed: value })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 检查提供者状态函数
|
||||
const checkProviderStatus = () => {
|
||||
if (!dmxapiProvider.enabled) {
|
||||
throw new Error('error.provider_disabled')
|
||||
}
|
||||
|
||||
if (!dmxapiProvider.apiKey) {
|
||||
throw new Error('error.no_api_key')
|
||||
}
|
||||
|
||||
if (!painting.model) {
|
||||
throw new Error('error.missing_required_fields')
|
||||
}
|
||||
|
||||
if (!painting.prompt) {
|
||||
throw new Error('paintings.text_desc_required')
|
||||
}
|
||||
}
|
||||
|
||||
// 准备V1生成请求函数
|
||||
const prepareV1GenerateRequest = (prompt: string, painting: DmxapiPainting) => {
|
||||
const params = {
|
||||
prompt,
|
||||
model: painting.model,
|
||||
n: painting.n
|
||||
}
|
||||
|
||||
if (painting.aspect_ratio) {
|
||||
params['aspect_ratio'] = painting.aspect_ratio
|
||||
}
|
||||
|
||||
if (painting.image_size) {
|
||||
params['size'] = painting.image_size
|
||||
}
|
||||
|
||||
if (painting.seed) {
|
||||
if (Number(painting.seed) >= -1) {
|
||||
params['seed'] = Number(painting.seed)
|
||||
} else {
|
||||
params['seed'] = -1
|
||||
}
|
||||
}
|
||||
|
||||
if (painting.style_type) {
|
||||
params.prompt = prompt + ',风格:' + painting.style_type
|
||||
}
|
||||
|
||||
return {
|
||||
body: JSON.stringify(params),
|
||||
endpoint: `${dmxapiProvider.apiHost}/v1/images/generations`
|
||||
}
|
||||
}
|
||||
|
||||
// API请求函数
|
||||
const callApi = async (requestConfig: { endpoint: string; body: any }) => {
|
||||
const { endpoint, body } = requestConfig
|
||||
const headers = {}
|
||||
|
||||
// 如果是JSON数据,添加Content-Type头
|
||||
if (typeof body === 'string') {
|
||||
headers['Content-Type'] = 'application/json'
|
||||
headers['Authorization'] = `Bearer ${dmxapiProvider.apiKey}`
|
||||
headers['User-Agent'] = 'DMXAPI/1.0.0 (https://www.dmxapi.com)'
|
||||
headers['Accept'] = 'application/json'
|
||||
}
|
||||
|
||||
const response = await fetch(endpoint, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json()
|
||||
throw new Error(errorData.error?.message || '操作失败')
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
return data.data.map((item: { url: string }) => item.url)
|
||||
}
|
||||
|
||||
// 下载图像函数
|
||||
const downloadImages = async (urls: string[]) => {
|
||||
return Promise.all(
|
||||
urls.map(async (url) => {
|
||||
try {
|
||||
if (!url || url.trim() === '') {
|
||||
window.message.warning({
|
||||
content: t('message.empty_url'),
|
||||
key: 'empty-url-warning'
|
||||
})
|
||||
return null
|
||||
}
|
||||
return await window.api.file.download(url, true)
|
||||
} catch (error) {
|
||||
if (
|
||||
error instanceof Error &&
|
||||
(error.message.includes('Failed to parse URL') || error.message.includes('Invalid URL'))
|
||||
) {
|
||||
window.message.warning({
|
||||
content: t('message.empty_url'),
|
||||
key: 'empty-url-warning'
|
||||
})
|
||||
}
|
||||
return null
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
// 准备请求配置函数
|
||||
const prepareRequestConfig = (prompt: string, painting: PaintingAction) => {
|
||||
// 根据模式和模型版本返回不同的请求配置
|
||||
return prepareV1GenerateRequest(prompt, painting)
|
||||
}
|
||||
|
||||
const onGenerate = async () => {
|
||||
try {
|
||||
// 获取提示词
|
||||
const prompt = textareaRef.current?.resizableTextArea?.textArea?.value || ''
|
||||
updatePaintingState({ prompt })
|
||||
|
||||
// 检查提供者状态
|
||||
checkProviderStatus()
|
||||
|
||||
// 处理已有文件
|
||||
if (painting.files.length > 0) {
|
||||
const confirmed = await window.modal.confirm({
|
||||
content: t('paintings.regenerate.confirm'),
|
||||
centered: true
|
||||
})
|
||||
if (!confirmed) return
|
||||
await FileManager.deleteFiles(painting.files)
|
||||
}
|
||||
|
||||
// 设置请求状态
|
||||
const controller = new AbortController()
|
||||
setAbortController(controller)
|
||||
setIsLoading(true)
|
||||
dispatch(setGenerating(true))
|
||||
|
||||
// 准备请求配置
|
||||
const requestConfig = prepareRequestConfig(prompt, painting)
|
||||
|
||||
// 发送API请求
|
||||
const urls = await callApi(requestConfig)
|
||||
|
||||
// 下载图像
|
||||
if (urls.length > 0) {
|
||||
const downloadedFiles = await downloadImages(urls)
|
||||
const validFiles = downloadedFiles.filter((file): file is FileType => file !== null)
|
||||
|
||||
// 保存文件并更新状态
|
||||
await FileManager.addFiles(validFiles)
|
||||
updatePaintingState({ files: validFiles, urls })
|
||||
}
|
||||
} catch (error) {
|
||||
// 错误处理
|
||||
if (error instanceof Error && error.name !== 'AbortError') {
|
||||
window.modal.error({
|
||||
content:
|
||||
error.message.startsWith('paintings.') || error.message.startsWith('error.')
|
||||
? t(error.message)
|
||||
: getErrorMessage(error),
|
||||
centered: true
|
||||
})
|
||||
}
|
||||
} finally {
|
||||
// 清理状态
|
||||
setIsLoading(false)
|
||||
dispatch(setGenerating(false))
|
||||
setAbortController(null)
|
||||
}
|
||||
}
|
||||
|
||||
const nextImage = () => {
|
||||
setCurrentImageIndex((prev) => (prev + 1) % painting.files.length)
|
||||
}
|
||||
|
||||
const prevImage = () => {
|
||||
setCurrentImageIndex((prev) => (prev - 1 + painting.files.length) % painting.files.length)
|
||||
}
|
||||
|
||||
const onDeletePainting = (paintingToDelete: DmxapiPainting) => {
|
||||
if (paintingToDelete.id === painting.id) {
|
||||
const currentIndex = DMXAPIPaintings.findIndex((p) => p.id === paintingToDelete.id)
|
||||
|
||||
if (currentIndex > 0) {
|
||||
setPainting(DMXAPIPaintings[currentIndex - 1])
|
||||
} else if (DMXAPIPaintings.length > 1) {
|
||||
setPainting(DMXAPIPaintings[1])
|
||||
}
|
||||
}
|
||||
|
||||
removePainting(mode, paintingToDelete).then(() => {})
|
||||
}
|
||||
|
||||
const onSelectPainting = (newPainting: DmxapiPainting) => {
|
||||
if (generating) return
|
||||
setPainting(newPainting)
|
||||
setCurrentImageIndex(0)
|
||||
}
|
||||
|
||||
const { autoTranslateWithSpace } = useSettings()
|
||||
const [spaceClickCount, setSpaceClickCount] = useState(0)
|
||||
const [isTranslating, setIsTranslating] = useState(false)
|
||||
const spaceClickTimer = useRef<NodeJS.Timeout>(null)
|
||||
|
||||
const translate = async () => {
|
||||
if (isTranslating) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!painting.prompt) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
setIsTranslating(true)
|
||||
const translatedText = await translateText(painting.prompt, 'english')
|
||||
updatePaintingState({ prompt: translatedText })
|
||||
} catch (error) {
|
||||
console.error('Translation failed:', error)
|
||||
} finally {
|
||||
setIsTranslating(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleKeyDown = (event: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
if (autoTranslateWithSpace && event.key === ' ') {
|
||||
setSpaceClickCount((prev) => prev + 1)
|
||||
|
||||
if (spaceClickTimer.current) {
|
||||
clearTimeout(spaceClickTimer.current)
|
||||
}
|
||||
|
||||
spaceClickTimer.current = setTimeout(() => {
|
||||
setSpaceClickCount(0)
|
||||
}, 200)
|
||||
|
||||
if (spaceClickCount === 2) {
|
||||
setSpaceClickCount(0)
|
||||
setIsTranslating(true)
|
||||
translate().then(() => {})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handleProviderChange = (providerId: string) => {
|
||||
const routeName = location.pathname.split('/').pop()
|
||||
if (providerId !== routeName) {
|
||||
navigate('../' + providerId, { replace: true })
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!DMXAPIPaintings || DMXAPIPaintings.length === 0) {
|
||||
const newPainting = getNewPainting()
|
||||
addPainting('DMXAPIPaintings', newPainting)
|
||||
setPainting(newPainting)
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (spaceClickTimer.current) {
|
||||
clearTimeout(spaceClickTimer.current)
|
||||
}
|
||||
}
|
||||
}, [DMXAPIPaintings, DMXAPIPaintings.length, addPainting, mode])
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<Navbar>
|
||||
<NavbarCenter style={{ borderRight: 'none' }}>{t('paintings.title')}</NavbarCenter>
|
||||
{isMac && (
|
||||
<NavbarRight style={{ justifyContent: 'flex-end' }}>
|
||||
<Button
|
||||
size="small"
|
||||
className="nodrag"
|
||||
icon={<PlusOutlined />}
|
||||
onClick={() => setPainting(addPainting('DMXAPIPaintings', getNewPainting()))}>
|
||||
{t('paintings.button.new.image')}
|
||||
</Button>
|
||||
</NavbarRight>
|
||||
)}
|
||||
</Navbar>
|
||||
<ContentContainer id="content-container">
|
||||
<LeftContainer>
|
||||
<ProviderTitleContainer>
|
||||
<SettingTitle style={{ marginBottom: 5 }}>{t('common.provider')}</SettingTitle>
|
||||
<SettingHelpLink target="_blank" href={COURSE_URL}>
|
||||
{t('paintings.paint_course')}
|
||||
<ProviderLogo
|
||||
shape="square"
|
||||
src={getProviderLogo(dmxapiProvider.id)}
|
||||
size={16}
|
||||
style={{ marginLeft: 5 }}
|
||||
/>
|
||||
</SettingHelpLink>
|
||||
</ProviderTitleContainer>
|
||||
<Select value={providerOptions[2].value} onChange={handleProviderChange} style={{ marginBottom: 15 }}>
|
||||
{providerOptions.map((provider) => (
|
||||
<Select.Option value={provider.value} key={provider.value}>
|
||||
<SelectOptionContainer>
|
||||
<ProviderLogo shape="square" src={getProviderLogo(provider.value || '')} size={16} />
|
||||
{provider.label}
|
||||
</SelectOptionContainer>
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
<SettingTitle style={{ marginBottom: 5, marginTop: 15 }}>{t('common.model')}</SettingTitle>
|
||||
<Select value={painting.model} options={modelOptions} onChange={onSelectModel} />
|
||||
<SettingTitle style={{ marginBottom: 5, marginTop: 15 }}>{t('paintings.image.size')}</SettingTitle>
|
||||
<Radio.Group
|
||||
value={painting.image_size}
|
||||
onChange={(e) => onSelectImageSize(e.target.value)}
|
||||
style={{ display: 'flex' }}>
|
||||
{IMAGE_SIZES.map((size) => (
|
||||
<RadioButton value={size.value} key={size.value}>
|
||||
<VStack alignItems="center">
|
||||
<ImageSizeImage src={size.icon} theme={theme} />
|
||||
<span>{size.label}</span>
|
||||
</VStack>
|
||||
</RadioButton>
|
||||
))}
|
||||
</Radio.Group>
|
||||
|
||||
<SettingTitle style={{ marginBottom: 5, marginTop: 15 }}>
|
||||
{t('paintings.seed')}
|
||||
<Tooltip title={t('paintings.seed_desc_tip')}>
|
||||
<InfoIcon />
|
||||
</Tooltip>
|
||||
</SettingTitle>
|
||||
<Input
|
||||
value={painting.seed}
|
||||
pattern="[0-9]*"
|
||||
onChange={(e) => onInputSeed(e)}
|
||||
suffix={
|
||||
<RedoOutlined
|
||||
onClick={() => updatePaintingState({ seed: Math.floor(Math.random() * 1000000).toString() })}
|
||||
style={{ cursor: 'pointer', color: 'var(--color-text-2)' }}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
<SettingTitle style={{ marginBottom: 5, marginTop: 15 }}>{t('paintings.style_type')}</SettingTitle>
|
||||
<SliderContainer>
|
||||
<RadioTextBox>
|
||||
{STYLE_TYPE_OPTIONS.map((ele) => (
|
||||
<RadioTextItem
|
||||
key={ele.label}
|
||||
className={painting.style_type === ele.label ? 'selected' : ''}
|
||||
onClick={() => onSelectStyleType(ele.label)}>
|
||||
{ele.label}
|
||||
</RadioTextItem>
|
||||
))}
|
||||
</RadioTextBox>
|
||||
</SliderContainer>
|
||||
</LeftContainer>
|
||||
<MainContainer>
|
||||
{painting?.urls?.length > 0 || DMXAPIPaintings?.length > 1 ? (
|
||||
<Artboard
|
||||
painting={painting}
|
||||
isLoading={isLoading}
|
||||
currentImageIndex={currentImageIndex}
|
||||
onPrevImage={prevImage}
|
||||
onNextImage={nextImage}
|
||||
onCancel={onCancel}
|
||||
/>
|
||||
) : (
|
||||
<EmptyImgBox>
|
||||
<EmptyImg></EmptyImg>
|
||||
</EmptyImgBox>
|
||||
)}
|
||||
|
||||
<InputContainer>
|
||||
<Textarea
|
||||
ref={textareaRef}
|
||||
variant="borderless"
|
||||
disabled={isLoading}
|
||||
value={painting.prompt}
|
||||
spellCheck={false}
|
||||
onChange={(e) => updatePaintingState({ prompt: e.target.value })}
|
||||
placeholder={isTranslating ? t('paintings.translating') : t('paintings.prompt_placeholder')}
|
||||
onKeyDown={handleKeyDown}
|
||||
/>
|
||||
<Toolbar>
|
||||
<ToolbarMenu>
|
||||
<SendMessageButton sendMessage={onGenerate} disabled={isLoading} />
|
||||
</ToolbarMenu>
|
||||
</Toolbar>
|
||||
</InputContainer>
|
||||
</MainContainer>
|
||||
<PaintingsList
|
||||
namespace="DMXAPIPaintings"
|
||||
paintings={DMXAPIPaintings}
|
||||
selectedPainting={painting}
|
||||
onSelectPainting={onSelectPainting}
|
||||
onDeletePainting={onDeletePainting}
|
||||
onNewPainting={() => setPainting(addPainting('DMXAPIPaintings', getNewPainting()))}
|
||||
/>
|
||||
</ContentContainer>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
const Container = styled.div`
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
`
|
||||
|
||||
// 添加新的样式组件
|
||||
const ProviderTitleContainer = styled.div`
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 5px;
|
||||
`
|
||||
|
||||
const ProviderLogo = styled(Avatar)`
|
||||
border: 0.5px solid var(--color-border);
|
||||
`
|
||||
const SelectOptionContainer = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
`
|
||||
|
||||
const ContentContainer = styled.div`
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: row;
|
||||
height: 100%;
|
||||
background-color: var(--color-background);
|
||||
overflow: hidden;
|
||||
`
|
||||
|
||||
const LeftContainer = styled(Scrollbar)`
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
padding: 20px;
|
||||
background-color: var(--color-background);
|
||||
max-width: var(--assistants-width);
|
||||
border-right: 0.5px solid var(--color-border);
|
||||
`
|
||||
|
||||
const MainContainer = styled.div`
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
background-color: var(--color-background);
|
||||
`
|
||||
|
||||
const InputContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 95px;
|
||||
max-height: 95px;
|
||||
position: relative;
|
||||
border: 1px solid var(--color-border-soft);
|
||||
transition: all 0.3s ease;
|
||||
margin: 0 20px 15px 20px;
|
||||
border-radius: 10px;
|
||||
`
|
||||
|
||||
const Textarea = styled(TextArea)`
|
||||
padding: 10px;
|
||||
border-radius: 0;
|
||||
display: flex;
|
||||
flex: 1;
|
||||
resize: none !important;
|
||||
overflow: auto;
|
||||
width: auto;
|
||||
`
|
||||
|
||||
const Toolbar = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-end;
|
||||
padding: 0 8px;
|
||||
height: 40px;
|
||||
`
|
||||
|
||||
const ToolbarMenu = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
`
|
||||
|
||||
const ImageSizeImage = styled.img<{ theme: string }>`
|
||||
filter: ${({ theme }) => (theme === 'dark' ? 'invert(100%)' : 'none')};
|
||||
margin-top: 8px;
|
||||
`
|
||||
|
||||
const RadioButton = styled(Radio.Button)`
|
||||
width: 30px;
|
||||
height: 55px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
`
|
||||
|
||||
const InfoIcon = styled(Info)`
|
||||
margin-left: 5px;
|
||||
cursor: help;
|
||||
color: var(--color-text-2);
|
||||
opacity: 0.6;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
`
|
||||
|
||||
const SliderContainer = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
|
||||
.ant-slider {
|
||||
flex: 1;
|
||||
}
|
||||
`
|
||||
|
||||
const RadioTextBox = styled.div`
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
`
|
||||
|
||||
const RadioTextItem = styled.div`
|
||||
cursor: pointer;
|
||||
padding: 2px 6px;
|
||||
border-radius: 6px;
|
||||
transition: all 0.2s ease;
|
||||
border: 1px solid var(--color-border);
|
||||
|
||||
/* 默认状态 */
|
||||
background-color: var(--color-background);
|
||||
|
||||
/* 悬浮状态 */
|
||||
&:hover {
|
||||
background-color: var(--color-hover, #f0f0f0);
|
||||
}
|
||||
|
||||
/* 选中状态 - 需要添加selected类名 */
|
||||
&.selected {
|
||||
background-color: var(--color-primary, #1890ff);
|
||||
color: white;
|
||||
border: 1px solid var(--color-primary, #1890ff);
|
||||
}
|
||||
`
|
||||
|
||||
const EmptyImgBox = styled.div`
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
`
|
||||
|
||||
const EmptyImg = styled.div`
|
||||
width: 70vh;
|
||||
height: 70vh;
|
||||
background-size: 100% 100%;
|
||||
background-image: url(${DMXAPIToImg});
|
||||
`
|
||||
|
||||
export default DmxapiPage
|
||||
@@ -2,9 +2,10 @@ import { FC } from 'react'
|
||||
import { Route, Routes } from 'react-router-dom'
|
||||
|
||||
import AihubmixPage from './AihubmixPage'
|
||||
import DmxapiPage from './DmxapiPage'
|
||||
import SiliconPage from './PaintingsPage'
|
||||
|
||||
const Options = ['aihubmix', 'silicon']
|
||||
const Options = ['aihubmix', 'silicon', 'dmxapi']
|
||||
|
||||
const PaintingsRoutePage: FC = () => {
|
||||
return (
|
||||
@@ -12,6 +13,7 @@ const PaintingsRoutePage: FC = () => {
|
||||
<Route path="/" element={<AihubmixPage Options={Options} />} />
|
||||
<Route path="/aihubmix" element={<AihubmixPage Options={Options} />} />
|
||||
<Route path="/silicon" element={<SiliconPage Options={Options} />} />
|
||||
<Route path="/dmxapi" element={<DmxapiPage Options={Options} />} />
|
||||
</Routes>
|
||||
)
|
||||
}
|
||||
|
||||
94
src/renderer/src/pages/paintings/config/DmxapiConfig.ts
Normal file
94
src/renderer/src/pages/paintings/config/DmxapiConfig.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
import ImageSize1_1 from '@renderer/assets/images/paintings/image-size-1-1.svg'
|
||||
import ImageSize1_2 from '@renderer/assets/images/paintings/image-size-1-2.svg'
|
||||
import ImageSize3_2 from '@renderer/assets/images/paintings/image-size-3-2.svg'
|
||||
import ImageSize3_4 from '@renderer/assets/images/paintings/image-size-3-4.svg'
|
||||
import ImageSize9_16 from '@renderer/assets/images/paintings/image-size-9-16.svg'
|
||||
import ImageSize16_9 from '@renderer/assets/images/paintings/image-size-16-9.svg'
|
||||
import { uuid } from '@renderer/utils'
|
||||
import { DmxapiPainting } from '@types'
|
||||
|
||||
export const STYLE_TYPE_OPTIONS = [
|
||||
{ label: '吉卜力', value: '吉卜力' },
|
||||
{ label: '皮克斯', value: '皮克斯' },
|
||||
{ label: '绒线玩偶', value: '绒线玩偶' },
|
||||
{ label: '水彩画', value: '水彩画' },
|
||||
{ label: '卡通插画', value: '卡通插画' },
|
||||
{ label: '3D卡通', value: '3D卡通' },
|
||||
{ label: '日系动漫', value: '日系动漫' },
|
||||
{ label: '木雕', value: '木雕' },
|
||||
{ label: '唯美古风', value: '唯美古风' },
|
||||
{ label: '2.5D动画', value: '2.5D动画' },
|
||||
{ label: '清新日漫', value: '清新日漫' },
|
||||
{ label: '黏土', value: '黏土' },
|
||||
{ label: '小人书插画', value: '小人书插画' },
|
||||
{ label: '浮世绘', value: '浮世绘' },
|
||||
{ label: '毛毡', value: '毛毡' },
|
||||
{ label: '美式复古', value: '美式复古' },
|
||||
{ label: '赛博朋克', value: '赛博朋克' },
|
||||
{ label: '素描', value: '素描' },
|
||||
{ label: '莫奈花园', value: '莫奈花园' },
|
||||
{ label: '厚涂手绘', value: '厚涂手绘' },
|
||||
{ label: '扁平', value: '扁平' },
|
||||
{ label: '肌理', value: '肌理' },
|
||||
{ label: '像素艺术', value: '像素艺术' },
|
||||
{ label: '街头艺术', value: '街头艺术' },
|
||||
{ label: '迷幻', value: '迷幻' },
|
||||
{ label: '国风工笔', value: '国风工笔' },
|
||||
{ label: '巴洛克', value: '巴洛克' }
|
||||
]
|
||||
|
||||
export const TEXT_TO_IMAGES_MODELS = [
|
||||
{
|
||||
id: 'seedream-3.0',
|
||||
provider: 'DMXAPI',
|
||||
name: ' 即梦 seedream-3.0'
|
||||
}
|
||||
]
|
||||
|
||||
export const IMAGE_SIZES = [
|
||||
{
|
||||
label: '1:1',
|
||||
value: '1328x1328',
|
||||
icon: ImageSize1_1
|
||||
},
|
||||
{
|
||||
label: '1:2',
|
||||
value: '800x1600',
|
||||
icon: ImageSize1_2
|
||||
},
|
||||
{
|
||||
label: '3:2',
|
||||
value: '1584x1056',
|
||||
icon: ImageSize3_2
|
||||
},
|
||||
{
|
||||
label: '3:4',
|
||||
value: '1104x1472',
|
||||
icon: ImageSize3_4
|
||||
},
|
||||
{
|
||||
label: '16:9',
|
||||
value: '1664x936',
|
||||
icon: ImageSize16_9
|
||||
},
|
||||
{
|
||||
label: '9:16',
|
||||
value: '936x1664',
|
||||
icon: ImageSize9_16
|
||||
}
|
||||
]
|
||||
|
||||
export const COURSE_URL = 'http://seedream.dmxapi.cn/'
|
||||
|
||||
export const DEFAULT_PAINTING: DmxapiPainting = {
|
||||
id: uuid(),
|
||||
urls: [],
|
||||
files: [],
|
||||
prompt: '',
|
||||
image_size: '1328x1328',
|
||||
aspect_ratio: '1:1',
|
||||
n: 1,
|
||||
seed: '',
|
||||
style_type: '',
|
||||
model: TEXT_TO_IMAGES_MODELS[0].id
|
||||
}
|
||||
@@ -40,6 +40,7 @@ export type AihubmixMode = keyof PaintingsState
|
||||
export const createModeConfigs = (): Record<AihubmixMode, ConfigItem[]> => {
|
||||
return {
|
||||
paintings: [],
|
||||
DMXAPIPaintings: [],
|
||||
generate: [
|
||||
{ type: 'title', title: 'paintings.model', tooltip: 'paintings.generate.model_tip' },
|
||||
{
|
||||
|
||||
@@ -22,10 +22,7 @@ const AgentsSubscribeUrlSettings: FC = () => {
|
||||
|
||||
return (
|
||||
<SettingGroup theme={theme}>
|
||||
<SettingTitle>
|
||||
{t('agents.tag.agent')}
|
||||
{t('settings.websearch.subscribe_add')}
|
||||
</SettingTitle>
|
||||
<SettingTitle>{t('agents.settings.subscription.title')}</SettingTitle>
|
||||
<SettingDivider />
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('settings.websearch.subscribe_url')}</SettingRowTitle>
|
||||
|
||||
@@ -2,7 +2,8 @@ import { DeleteOutlined, SaveOutlined } from '@ant-design/icons'
|
||||
import { useTheme } from '@renderer/context/ThemeProvider'
|
||||
import { useMCPServer, useMCPServers } from '@renderer/hooks/useMCPServers'
|
||||
import MCPDescription from '@renderer/pages/settings/MCPSettings/McpDescription'
|
||||
import { MCPPrompt, MCPResource, MCPServer, MCPTool } from '@renderer/types'
|
||||
import { MCPPrompt, MCPResource, MCPServer, MCPTool, MCPToolConfig, MCPToolParameterConfig } from '@renderer/types'
|
||||
import { isEmpty } from '@renderer/utils'
|
||||
import { formatMcpError } from '@renderer/utils/error'
|
||||
import { Button, Flex, Form, Input, Radio, Select, Switch, Tabs } from 'antd'
|
||||
import TextArea from 'antd/es/input/TextArea'
|
||||
@@ -398,6 +399,42 @@ const McpSettings: React.FC = () => {
|
||||
[server, updateMCPServer]
|
||||
)
|
||||
|
||||
// Handle updating tool parameter configuration
|
||||
const handleUpdateToolConfig = useCallback(
|
||||
async (toolName: string, parameterConfig: MCPToolParameterConfig[]) => {
|
||||
let customToolConfigs = [...(server.customToolConfigs || [])]
|
||||
|
||||
const existingConfigIndex = customToolConfigs.findIndex((config) => config.toolName === toolName)
|
||||
|
||||
const newToolConfig: MCPToolConfig = {
|
||||
toolName,
|
||||
parameters: parameterConfig
|
||||
}
|
||||
|
||||
if (existingConfigIndex >= 0) {
|
||||
customToolConfigs[existingConfigIndex] = newToolConfig
|
||||
} else {
|
||||
customToolConfigs.push(newToolConfig)
|
||||
}
|
||||
|
||||
customToolConfigs = customToolConfigs.filter((config) =>
|
||||
config.parameters.some((param) => !isEmpty(param.defaultValue))
|
||||
)
|
||||
|
||||
const updatedServer = {
|
||||
...server,
|
||||
customToolConfigs
|
||||
}
|
||||
|
||||
updateMCPServer(updatedServer)
|
||||
window.message.success({
|
||||
content: t('settings.mcp.tools.configSaved'),
|
||||
key: 'mcp-tool-config'
|
||||
})
|
||||
},
|
||||
[server, updateMCPServer, t]
|
||||
)
|
||||
|
||||
const tabs = [
|
||||
{
|
||||
key: 'settings',
|
||||
@@ -592,7 +629,14 @@ const McpSettings: React.FC = () => {
|
||||
{
|
||||
key: 'tools',
|
||||
label: t('settings.mcp.tabs.tools'),
|
||||
children: <MCPToolsSection tools={tools} server={server} onToggleTool={handleToggleTool} />
|
||||
children: (
|
||||
<MCPToolsSection
|
||||
tools={tools}
|
||||
server={server}
|
||||
onToggleTool={handleToggleTool}
|
||||
onUpdateToolConfig={handleUpdateToolConfig}
|
||||
/>
|
||||
)
|
||||
},
|
||||
{
|
||||
key: 'prompts',
|
||||
|
||||
@@ -1,5 +1,21 @@
|
||||
import { MCPServer, MCPTool } from '@renderer/types'
|
||||
import { Badge, Collapse, Descriptions, Empty, Flex, Switch, Tag, Tooltip, Typography } from 'antd'
|
||||
import { CloseOutlined, SaveOutlined, SettingOutlined } from '@ant-design/icons'
|
||||
import { MCPServer, MCPTool, MCPToolParameterConfig } from '@renderer/types'
|
||||
import { isEmpty } from '@renderer/utils'
|
||||
import {
|
||||
Button,
|
||||
Collapse,
|
||||
Descriptions,
|
||||
Empty,
|
||||
Flex,
|
||||
Input,
|
||||
InputNumber,
|
||||
Space,
|
||||
Switch,
|
||||
Tag,
|
||||
Tooltip,
|
||||
Typography
|
||||
} from 'antd'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
|
||||
@@ -7,22 +23,211 @@ interface MCPToolsSectionProps {
|
||||
tools: MCPTool[]
|
||||
server: MCPServer
|
||||
onToggleTool: (tool: MCPTool, enabled: boolean) => void
|
||||
onUpdateToolConfig?: (toolName: string, config: MCPToolParameterConfig[]) => void
|
||||
}
|
||||
|
||||
const MCPToolsSection = ({ tools, server, onToggleTool }: MCPToolsSectionProps) => {
|
||||
const MCPToolsSection = ({ tools, server, onToggleTool, onUpdateToolConfig }: MCPToolsSectionProps) => {
|
||||
const { t } = useTranslation()
|
||||
const [editingToolName, setEditingToolName] = useState<string | null>(null)
|
||||
const [editableToolParams, setEditableToolParams] = useState<MCPToolParameterConfig[]>([])
|
||||
const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false)
|
||||
|
||||
// Effect to reset editable params when editingToolName changes or server config changes
|
||||
useEffect(() => {
|
||||
if (editingToolName) {
|
||||
const tool = tools.find((t) => t.name === editingToolName)
|
||||
if (tool) {
|
||||
initializeEditableParams(tool)
|
||||
} else {
|
||||
setEditingToolName(null)
|
||||
}
|
||||
} else {
|
||||
setEditableToolParams([])
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [editingToolName, server.customToolConfigs, tools])
|
||||
|
||||
// Check if a tool is enabled (not in the disabledTools array)
|
||||
const isToolEnabled = (tool: MCPTool) => {
|
||||
return !server.disabledTools?.includes(tool.name)
|
||||
}
|
||||
|
||||
// Handle tool toggle
|
||||
const handleToggle = (tool: MCPTool, checked: boolean) => {
|
||||
onToggleTool(tool, checked)
|
||||
}
|
||||
|
||||
// Render tool properties from the input schema
|
||||
const getToolConfig = (toolName: string): MCPToolParameterConfig[] => {
|
||||
const toolConfig = server.customToolConfigs?.find((config) => config.toolName === toolName)
|
||||
return toolConfig?.parameters || []
|
||||
}
|
||||
|
||||
const getDefaultValueForType = (type: string): any => {
|
||||
switch (type) {
|
||||
case 'string':
|
||||
return ''
|
||||
case 'number':
|
||||
return 0
|
||||
case 'boolean':
|
||||
return false
|
||||
case 'array':
|
||||
return []
|
||||
case 'object':
|
||||
return {}
|
||||
default:
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
const initializeEditableParams = (tool: MCPTool) => {
|
||||
const currentConfig = getToolConfig(tool.name)
|
||||
const initialConfig: MCPToolParameterConfig[] = []
|
||||
if (tool.inputSchema?.properties) {
|
||||
Object.entries(tool.inputSchema.properties).forEach(([paramName, paramDef]: [string, any]) => {
|
||||
const existingConfig = currentConfig.find((c) => c.name === paramName)
|
||||
initialConfig.push({
|
||||
name: paramName,
|
||||
defaultValue: existingConfig?.defaultValue ?? getDefaultValueForType(paramDef.type),
|
||||
description: paramDef.description || ''
|
||||
})
|
||||
})
|
||||
}
|
||||
setEditableToolParams(initialConfig)
|
||||
}
|
||||
|
||||
const handleEditToolParams = (tool: MCPTool) => {
|
||||
if (editingToolName === tool.name) {
|
||||
// If already editing this tool, cancel editing
|
||||
setEditingToolName(null)
|
||||
setEditableToolParams([])
|
||||
setHasUnsavedChanges(false)
|
||||
} else {
|
||||
setEditingToolName(tool.name)
|
||||
setHasUnsavedChanges(false)
|
||||
// initializeEditableParams will be called by the useEffect
|
||||
}
|
||||
}
|
||||
|
||||
const handleSaveToolParams = (toolName: string) => {
|
||||
if (onUpdateToolConfig) {
|
||||
onUpdateToolConfig(toolName, editableToolParams)
|
||||
}
|
||||
setEditingToolName(null)
|
||||
setEditableToolParams([])
|
||||
setHasUnsavedChanges(false)
|
||||
}
|
||||
|
||||
const handleCancelEditToolParams = () => {
|
||||
setEditingToolName(null)
|
||||
setEditableToolParams([])
|
||||
setHasUnsavedChanges(false)
|
||||
}
|
||||
|
||||
const updateParameterConfig = (index: number, field: keyof MCPToolParameterConfig, value: any) => {
|
||||
const newConfig = [...editableToolParams]
|
||||
newConfig[index] = { ...newConfig[index], [field]: value }
|
||||
setEditableToolParams(newConfig)
|
||||
setHasUnsavedChanges(true)
|
||||
}
|
||||
|
||||
const handleParameterBlur = (index: number, field: keyof MCPToolParameterConfig, value: any) => {
|
||||
updateParameterConfig(index, field, value)
|
||||
}
|
||||
|
||||
const renderParameterInput = (param: MCPToolParameterConfig, index: number, paramDef: any) => {
|
||||
const { type } = paramDef
|
||||
|
||||
switch (type) {
|
||||
case 'number':
|
||||
return (
|
||||
<InputNumber
|
||||
value={param.defaultValue}
|
||||
onChange={(value) => updateParameterConfig(index, 'defaultValue', value)}
|
||||
onBlur={(e) => {
|
||||
const value = e.target.value ? Number(e.target.value) : undefined
|
||||
handleParameterBlur(index, 'defaultValue', value)
|
||||
}}
|
||||
style={{ width: '100%' }}
|
||||
placeholder="Enter default number"
|
||||
/>
|
||||
)
|
||||
case 'boolean':
|
||||
return (
|
||||
<Switch
|
||||
checked={param.defaultValue}
|
||||
onChange={(checked) => {
|
||||
updateParameterConfig(index, 'defaultValue', checked)
|
||||
// Switch 组件没有 onBlur,直接在 onChange 中处理
|
||||
handleParameterBlur(index, 'defaultValue', checked)
|
||||
}}
|
||||
/>
|
||||
)
|
||||
case 'array':
|
||||
return (
|
||||
<Input.TextArea
|
||||
value={Array.isArray(param.defaultValue) ? param.defaultValue.join('\n') : ''}
|
||||
onChange={(e) => {
|
||||
const lines = e.target.value.split('\n').filter((line) => line.trim())
|
||||
updateParameterConfig(index, 'defaultValue', lines)
|
||||
}}
|
||||
onBlur={(e) => {
|
||||
const lines = e.target.value.split('\n').filter((line) => line.trim())
|
||||
handleParameterBlur(index, 'defaultValue', lines)
|
||||
}}
|
||||
placeholder="Enter array items (one per line)"
|
||||
rows={3}
|
||||
/>
|
||||
)
|
||||
default: // 'string' and 'object' (as JSON string for simplicity here)
|
||||
if (type === 'object') {
|
||||
return (
|
||||
<Input.TextArea
|
||||
value={
|
||||
typeof param.defaultValue === 'object'
|
||||
? JSON.stringify(param.defaultValue, null, 2)
|
||||
: param.defaultValue
|
||||
}
|
||||
onChange={(e) => {
|
||||
try {
|
||||
const val = e.target.value
|
||||
// Attempt to parse if it's meant to be an object, otherwise store as string
|
||||
if (val.trim().startsWith('{') || val.trim().startsWith('[')) {
|
||||
updateParameterConfig(index, 'defaultValue', JSON.parse(val))
|
||||
} else {
|
||||
updateParameterConfig(index, 'defaultValue', val)
|
||||
}
|
||||
} catch (error) {
|
||||
// If JSON is invalid while typing, keep the string value
|
||||
updateParameterConfig(index, 'defaultValue', e.target.value)
|
||||
}
|
||||
}}
|
||||
onBlur={(e) => {
|
||||
try {
|
||||
const val = e.target.value
|
||||
if (val.trim().startsWith('{') || val.trim().startsWith('[')) {
|
||||
handleParameterBlur(index, 'defaultValue', JSON.parse(val))
|
||||
} else {
|
||||
handleParameterBlur(index, 'defaultValue', val)
|
||||
}
|
||||
} catch (error) {
|
||||
// If JSON is invalid, keep the string value
|
||||
handleParameterBlur(index, 'defaultValue', e.target.value)
|
||||
}
|
||||
}}
|
||||
placeholder="Enter default JSON object or string"
|
||||
rows={3}
|
||||
/>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<Input
|
||||
value={param.defaultValue}
|
||||
onChange={(e) => updateParameterConfig(index, 'defaultValue', e.target.value)}
|
||||
onBlur={(e) => handleParameterBlur(index, 'defaultValue', e.target.value)}
|
||||
placeholder="Enter default value"
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const renderToolProperties = (tool: MCPTool) => {
|
||||
if (!tool.inputSchema?.properties) return null
|
||||
|
||||
@@ -43,52 +248,123 @@ const MCPToolsSection = ({ tools, server, onToggleTool }: MCPToolsSectionProps)
|
||||
}
|
||||
}
|
||||
|
||||
const toolConfig = getToolConfig(tool.name)
|
||||
const isEditingThisTool = editingToolName === tool.name
|
||||
|
||||
return (
|
||||
<div style={{ marginTop: 12 }}>
|
||||
<Typography.Title level={5}>{t('settings.mcp.tools.inputSchema')}:</Typography.Title>
|
||||
<Flex justify="space-between" align="center" style={{ marginBottom: 8 }}>
|
||||
<Typography.Title level={5} style={{ margin: 0 }}>
|
||||
{t('settings.mcp.tools.inputSchema')}:
|
||||
</Typography.Title>
|
||||
<Tooltip title={t('settings.mcp.tools.configureDefaults')}>
|
||||
<Button
|
||||
type={isEditingThisTool ? 'primary' : 'text'}
|
||||
icon={<SettingOutlined />}
|
||||
size="small"
|
||||
onClick={() => handleEditToolParams(tool)}
|
||||
/>
|
||||
</Tooltip>
|
||||
</Flex>
|
||||
<Descriptions bordered size="small" column={1} style={{ marginTop: 8 }}>
|
||||
{Object.entries(tool.inputSchema.properties).map(([key, prop]: [string, any]) => (
|
||||
<Descriptions.Item
|
||||
key={key}
|
||||
label={
|
||||
<Flex gap={4}>
|
||||
<Typography.Text strong>{key}</Typography.Text>
|
||||
{tool.inputSchema.required?.includes(key) && (
|
||||
<Tooltip title="Required field">
|
||||
<span style={{ color: '#f5222d' }}>*</span>
|
||||
</Tooltip>
|
||||
{Object.entries(tool.inputSchema.properties).map(([key, prop]: [string, any]) => {
|
||||
const paramDefFromSchema = tool.inputSchema?.properties?.[key] as any
|
||||
const currentParamSetting = toolConfig.find((c) => c.name === key)
|
||||
const hasDefaultValue = !isEmpty(currentParamSetting?.defaultValue)
|
||||
|
||||
const editableParam = isEditingThisTool ? editableToolParams.find((p) => p.name === key) : null
|
||||
|
||||
return (
|
||||
<Descriptions.Item
|
||||
key={key}
|
||||
label={
|
||||
<Flex gap={4} align="center">
|
||||
<Typography.Text strong>{key}</Typography.Text>
|
||||
{tool.inputSchema.required?.includes(key) && (
|
||||
<Tag color="red" style={{ margin: 0 }}>
|
||||
{t('common.required')}
|
||||
</Tag>
|
||||
)}
|
||||
{prop.type && (
|
||||
<Tag color={getTypeColor(prop.type)} style={{ margin: 0 }}>
|
||||
{prop.type}
|
||||
</Tag>
|
||||
)}
|
||||
</Flex>
|
||||
}>
|
||||
<Flex vertical gap={8}>
|
||||
{prop.description && (
|
||||
<Typography.Paragraph type="secondary" style={{ marginBottom: 0, fontSize: '13px' }}>
|
||||
{prop.description}
|
||||
</Typography.Paragraph>
|
||||
)}
|
||||
</Flex>
|
||||
}>
|
||||
<Flex vertical gap={4}>
|
||||
<Flex align="center" gap={8}>
|
||||
{prop.type && (
|
||||
// <Typography.Text type="secondary">{prop.type} </Typography.Text>
|
||||
<Badge
|
||||
color={getTypeColor(prop.type)}
|
||||
text={<Typography.Text type="secondary">{prop.type}</Typography.Text>}
|
||||
/>
|
||||
|
||||
{isEditingThisTool && editableParam && paramDefFromSchema ? (
|
||||
<Flex
|
||||
align="center"
|
||||
gap={8}
|
||||
style={{
|
||||
marginTop: 8,
|
||||
border: '1px solid var(--color-border-secondary)',
|
||||
borderRadius: 6,
|
||||
backgroundColor: 'var(--color-background)'
|
||||
}}>
|
||||
<Typography.Text style={{ fontWeight: 500, marginBottom: 0, flexShrink: 0 }}>
|
||||
{t('settings.mcp.tools.defaultValue')}:
|
||||
</Typography.Text>
|
||||
{renderParameterInput(
|
||||
editableParam,
|
||||
editableToolParams.findIndex((p) => p.name === key),
|
||||
paramDefFromSchema
|
||||
)}
|
||||
</Flex>
|
||||
) : (
|
||||
hasDefaultValue && (
|
||||
<div style={{ marginTop: 4 }}>
|
||||
<Typography.Text type="secondary">{t('settings.mcp.tools.defaultValue')}: </Typography.Text>
|
||||
<Tag color="geekblue">{JSON.stringify(currentParamSetting?.defaultValue)}</Tag>
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
</Flex>
|
||||
{prop.description && (
|
||||
<Typography.Paragraph type="secondary" style={{ marginBottom: 0, marginTop: 4 }}>
|
||||
{prop.description}
|
||||
</Typography.Paragraph>
|
||||
)}
|
||||
{prop.enum && (
|
||||
<div style={{ marginTop: 4 }}>
|
||||
<Typography.Text type="secondary">Allowed values: </Typography.Text>
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 4, marginTop: 4 }}>
|
||||
{prop.enum.map((value: string, idx: number) => (
|
||||
<Tag key={idx}>{value}</Tag>
|
||||
))}
|
||||
|
||||
{prop.enum && (
|
||||
<div style={{ marginTop: 4 }}>
|
||||
<Typography.Text type="secondary">{t('common.allowed_values')}: </Typography.Text>
|
||||
<Flex wrap="wrap" gap={4} style={{ marginTop: 4 }}>
|
||||
{prop.enum.map((value: string, idx: number) => (
|
||||
<Tag key={idx}>{value}</Tag>
|
||||
))}
|
||||
</Flex>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Flex>
|
||||
</Descriptions.Item>
|
||||
))}
|
||||
)}
|
||||
</Flex>
|
||||
</Descriptions.Item>
|
||||
)
|
||||
})}
|
||||
</Descriptions>
|
||||
{isEditingThisTool && (
|
||||
<Flex justify="space-between" align="center" style={{ marginTop: 16 }}>
|
||||
<Flex align="center" gap={8}>
|
||||
{hasUnsavedChanges && (
|
||||
<Typography.Text type="secondary" style={{ fontSize: '12px' }}>
|
||||
{t('common.unsaved_changes')}
|
||||
</Typography.Text>
|
||||
)}
|
||||
</Flex>
|
||||
<Flex gap={8}>
|
||||
<Button icon={<CloseOutlined />} onClick={handleCancelEditToolParams}>
|
||||
{t('common.cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<SaveOutlined />}
|
||||
onClick={() => handleSaveToolParams(tool.name)}
|
||||
disabled={!hasUnsavedChanges}>
|
||||
{t('common.save')}
|
||||
</Button>
|
||||
</Flex>
|
||||
</Flex>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -97,35 +373,65 @@ const MCPToolsSection = ({ tools, server, onToggleTool }: MCPToolsSectionProps)
|
||||
<Section>
|
||||
<SectionTitle>{t('settings.mcp.tools.availableTools')}</SectionTitle>
|
||||
{tools.length > 0 ? (
|
||||
<Collapse bordered={false} ghost>
|
||||
<Collapse bordered={false} ghost accordion>
|
||||
{tools.map((tool) => (
|
||||
<Collapse.Panel
|
||||
key={tool.id}
|
||||
header={
|
||||
<Flex justify="space-between" align="center" style={{ width: '100%' }}>
|
||||
<Flex vertical align="flex-start">
|
||||
<Flex vertical align="flex-start" style={{ flexGrow: 1, maxWidth: 'calc(100% - 60px)' }}>
|
||||
<Flex align="center" style={{ width: '100%' }}>
|
||||
<Typography.Text strong>{tool.name}</Typography.Text>
|
||||
<Typography.Text type="secondary" style={{ marginLeft: 8, fontSize: '12px' }}>
|
||||
{tool.id}
|
||||
<Typography.Text strong style={{ marginRight: 8 }}>
|
||||
{tool.name}
|
||||
</Typography.Text>
|
||||
<Typography.Text
|
||||
type="secondary"
|
||||
style={{
|
||||
fontSize: '12px',
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis'
|
||||
}}>
|
||||
({tool.id})
|
||||
</Typography.Text>
|
||||
{server.customToolConfigs
|
||||
?.find((c) => c.toolName === tool.name)
|
||||
?.parameters.some((p) => !isEmpty(p.defaultValue)) && (
|
||||
<Tooltip title={t('settings.mcp.tools.hasDefaultTooltip')}>
|
||||
<Tag color="blue" style={{ marginLeft: 'auto', marginRight: 8, flexShrink: 0 }}>
|
||||
{t('settings.mcp.tools.hasDefaults')}
|
||||
</Tag>
|
||||
</Tooltip>
|
||||
)}
|
||||
</Flex>
|
||||
{tool.description && (
|
||||
<Typography.Text type="secondary" style={{ fontSize: '13px', marginTop: 4 }}>
|
||||
{tool.description.length > 100 ? `${tool.description.substring(0, 100)}...` : tool.description}
|
||||
<Typography.Text
|
||||
type="secondary"
|
||||
style={{
|
||||
fontSize: '13px',
|
||||
marginTop: 4,
|
||||
width: '100%',
|
||||
whiteSpace: 'normal',
|
||||
wordBreak: 'break-word'
|
||||
}}>
|
||||
{tool.description.length > 150 ? `${tool.description.substring(0, 150)}...` : tool.description}
|
||||
</Typography.Text>
|
||||
)}
|
||||
</Flex>
|
||||
<Switch
|
||||
checked={isToolEnabled(tool)}
|
||||
onChange={(checked, event) => {
|
||||
event?.stopPropagation()
|
||||
handleToggle(tool, checked)
|
||||
}}
|
||||
/>
|
||||
<Space onClick={(e) => e.stopPropagation()} style={{ marginLeft: 10 }}>
|
||||
<Switch
|
||||
checked={isToolEnabled(tool)}
|
||||
onChange={(checked) => {
|
||||
handleToggle(tool, checked)
|
||||
}}
|
||||
/>
|
||||
</Space>
|
||||
</Flex>
|
||||
}>
|
||||
<SelectableContent>{renderToolProperties(tool)}</SelectableContent>
|
||||
<SelectableContent
|
||||
onClick={(e) => e.stopPropagation() /* Prevent collapse toggle when clicking content */}>
|
||||
{renderToolProperties(tool)}
|
||||
</SelectableContent>
|
||||
</Collapse.Panel>
|
||||
))}
|
||||
</Collapse>
|
||||
@@ -150,7 +456,7 @@ const SectionTitle = styled.h3`
|
||||
|
||||
const SelectableContent = styled.div`
|
||||
user-select: text;
|
||||
padding: 0 12px;
|
||||
padding: 0 12px 12px 12px;
|
||||
`
|
||||
|
||||
export default MCPToolsSection
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
import DmxapiLogo from '@renderer/assets/images/providers/dmxapi-logo.webp'
|
||||
import { useProvider } from '@renderer/hooks/useProvider'
|
||||
import { Provider } from '@renderer/types'
|
||||
import { Radio, RadioChangeEvent, Space } from 'antd'
|
||||
import { FC, useCallback, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import { SettingSubtitle } from '..'
|
||||
|
||||
interface DMXAPISettingsProps {
|
||||
provider: Provider
|
||||
setApiKey: (apiKey: string) => void
|
||||
}
|
||||
|
||||
// DMXAPI平台选项
|
||||
enum PlatformType {
|
||||
OFFICIAL = 'https://www.DMXAPI.cn',
|
||||
INTERNATIONAL = 'https://www.DMXAPI.com',
|
||||
OVERSEA = 'https://ssvip.DMXAPI.com'
|
||||
}
|
||||
|
||||
const PlatformOptions = [
|
||||
{
|
||||
label: 'www.DMXAPI.cn 人民币站',
|
||||
value: PlatformType.OFFICIAL,
|
||||
apiKeyWebsite: 'https://www.dmxapi.cn/register?aff=bwwY'
|
||||
},
|
||||
{
|
||||
label: 'www.DMXAPI.com 国际站',
|
||||
value: PlatformType.INTERNATIONAL,
|
||||
apiKeyWebsite: 'https://www.dmxapi.com/register'
|
||||
},
|
||||
{
|
||||
label: 'ssvip.DMXAPI.com 生产级商用站',
|
||||
value: PlatformType.OVERSEA,
|
||||
apiKeyWebsite: 'https://ssvip.dmxapi.com/register'
|
||||
}
|
||||
]
|
||||
|
||||
const DMXAPISettings: FC<DMXAPISettingsProps> = ({ provider: initialProvider }) => {
|
||||
const { provider, updateProvider } = useProvider(initialProvider.id)
|
||||
|
||||
const { t } = useTranslation()
|
||||
|
||||
// 获取当前选中的平台,如果没有设置则默认为官方平台
|
||||
const getCurrentPlatform = (): PlatformType => {
|
||||
if (!provider.apiHost) return PlatformType.OFFICIAL
|
||||
|
||||
if (provider.apiHost.includes('DMXAPI.com')) {
|
||||
return provider.apiHost.includes('ssvip') ? PlatformType.OVERSEA : PlatformType.INTERNATIONAL
|
||||
}
|
||||
|
||||
return PlatformType.OFFICIAL
|
||||
}
|
||||
|
||||
// 状态管理
|
||||
const [selectedPlatform, setSelectedPlatform] = useState<PlatformType>(getCurrentPlatform())
|
||||
|
||||
// 处理平台选择变更
|
||||
const handlePlatformChange = useCallback(
|
||||
(e: RadioChangeEvent) => {
|
||||
const platform = e.target.value as PlatformType
|
||||
setSelectedPlatform(platform)
|
||||
updateProvider({ ...provider, apiHost: platform })
|
||||
},
|
||||
[provider, updateProvider]
|
||||
)
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<Space direction="vertical" style={{ width: '100%' }}>
|
||||
<LogoContainer>
|
||||
<Logo src={DmxapiLogo}></Logo>
|
||||
</LogoContainer>
|
||||
|
||||
<SettingSubtitle style={{ marginTop: 5 }}>{t('settings.provider.dmxapi.select_platform')}</SettingSubtitle>
|
||||
<Radio.Group
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 8
|
||||
}}
|
||||
onChange={handlePlatformChange}
|
||||
value={selectedPlatform}
|
||||
options={PlatformOptions.map((option) => ({
|
||||
...option,
|
||||
label: (
|
||||
<span>
|
||||
{option.label}{' '}
|
||||
<a href={option.apiKeyWebsite} target="_blank" rel="noopener noreferrer">
|
||||
(获得 API密钥)
|
||||
</a>
|
||||
</span>
|
||||
)
|
||||
}))}></Radio.Group>
|
||||
</Space>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
// 样式组件
|
||||
const Container = styled.div`
|
||||
margin-top: 16px;
|
||||
margin-bottom: 30px;
|
||||
`
|
||||
|
||||
const LogoContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin-bottom: 30px;
|
||||
`
|
||||
|
||||
const Logo = styled.img`
|
||||
height: 70px;
|
||||
display: block;
|
||||
width: auto;
|
||||
`
|
||||
|
||||
export default DMXAPISettings
|
||||
@@ -32,6 +32,7 @@ import {
|
||||
SettingTitle
|
||||
} from '..'
|
||||
import ApiCheckPopup from './ApiCheckPopup'
|
||||
import DMXAPISettings from './DMXAPISettings'
|
||||
import GithubCopilotSettings from './GithubCopilotSettings'
|
||||
import GPUStackSettings from './GPUStackSettings'
|
||||
import HealthCheckPopup from './HealthCheckPopup'
|
||||
@@ -64,6 +65,8 @@ const ProviderSetting: FC<Props> = ({ provider: _provider }) => {
|
||||
|
||||
const isAzureOpenAI = provider.id === 'azure-openai' || provider.type === 'azure-openai'
|
||||
|
||||
const isDmxapi = provider.id === 'dmxapi'
|
||||
|
||||
const providerConfig = PROVIDER_CONFIG[provider.id]
|
||||
const officialWebsite = providerConfig?.websites?.official
|
||||
const apiKeyWebsite = providerConfig?.websites?.apiKey
|
||||
@@ -328,6 +331,7 @@ const ProviderSetting: FC<Props> = ({ provider: _provider }) => {
|
||||
/>
|
||||
)}
|
||||
{provider.id === 'openai' && <OpenAIAlert />}
|
||||
{isDmxapi && <DMXAPISettings provider={provider} setApiKey={setApiKey} />}
|
||||
<SettingSubtitle style={{ marginTop: 5 }}>{t('settings.provider.api_key')}</SettingSubtitle>
|
||||
<Space.Compact style={{ width: '100%', marginTop: 5 }}>
|
||||
<Input.Password
|
||||
@@ -358,35 +362,43 @@ const ProviderSetting: FC<Props> = ({ provider: _provider }) => {
|
||||
{apiKeyWebsite && (
|
||||
<SettingHelpTextRow style={{ justifyContent: 'space-between' }}>
|
||||
<HStack>
|
||||
<SettingHelpLink target="_blank" href={apiKeyWebsite}>
|
||||
{t('settings.provider.get_api_key')}
|
||||
</SettingHelpLink>
|
||||
{!isDmxapi && (
|
||||
<SettingHelpLink target="_blank" href={apiKeyWebsite}>
|
||||
{t('settings.provider.get_api_key')}
|
||||
</SettingHelpLink>
|
||||
)}
|
||||
</HStack>
|
||||
<SettingHelpText>{t('settings.provider.api_key.tip')}</SettingHelpText>
|
||||
</SettingHelpTextRow>
|
||||
)}
|
||||
<SettingSubtitle>{t('settings.provider.api_host')}</SettingSubtitle>
|
||||
<Space.Compact style={{ width: '100%', marginTop: 5 }}>
|
||||
<Input
|
||||
value={apiHost}
|
||||
placeholder={t('settings.provider.api_host')}
|
||||
onChange={(e) => setApiHost(e.target.value)}
|
||||
onBlur={onUpdateApiHost}
|
||||
/>
|
||||
{!isEmpty(configedApiHost) && apiHost !== configedApiHost && (
|
||||
<Button danger onClick={onReset}>
|
||||
{t('settings.provider.api.url.reset')}
|
||||
</Button>
|
||||
)}
|
||||
</Space.Compact>
|
||||
{isOpenAIProvider(provider) && (
|
||||
<SettingHelpTextRow style={{ justifyContent: 'space-between' }}>
|
||||
<SettingHelpText
|
||||
style={{ marginLeft: 6, marginRight: '1em', whiteSpace: 'break-spaces', wordBreak: 'break-all' }}>
|
||||
{hostPreview()}
|
||||
</SettingHelpText>
|
||||
<SettingHelpText style={{ minWidth: 'fit-content' }}>{t('settings.provider.api.url.tip')}</SettingHelpText>
|
||||
</SettingHelpTextRow>
|
||||
{!isDmxapi && (
|
||||
<>
|
||||
<SettingSubtitle>{t('settings.provider.api_host')}</SettingSubtitle>
|
||||
<Space.Compact style={{ width: '100%', marginTop: 5 }}>
|
||||
<Input
|
||||
value={apiHost}
|
||||
placeholder={t('settings.provider.api_host')}
|
||||
onChange={(e) => setApiHost(e.target.value)}
|
||||
onBlur={onUpdateApiHost}
|
||||
/>
|
||||
{!isEmpty(configedApiHost) && apiHost !== configedApiHost && (
|
||||
<Button danger onClick={onReset}>
|
||||
{t('settings.provider.api.url.reset')}
|
||||
</Button>
|
||||
)}
|
||||
</Space.Compact>
|
||||
{isOpenAIProvider(provider) && (
|
||||
<SettingHelpTextRow style={{ justifyContent: 'space-between' }}>
|
||||
<SettingHelpText
|
||||
style={{ marginLeft: 6, marginRight: '1em', whiteSpace: 'break-spaces', wordBreak: 'break-all' }}>
|
||||
{hostPreview()}
|
||||
</SettingHelpText>
|
||||
<SettingHelpText style={{ minWidth: 'fit-content' }}>
|
||||
{t('settings.provider.api.url.tip')}
|
||||
</SettingHelpText>
|
||||
</SettingHelpTextRow>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{isAzureOpenAI && (
|
||||
<>
|
||||
|
||||
@@ -46,7 +46,7 @@ const persistedReducer = persistReducer(
|
||||
{
|
||||
key: 'cherry-studio',
|
||||
storage,
|
||||
version: 106,
|
||||
version: 107,
|
||||
blacklist: ['runtime', 'messages', 'messageBlocks'],
|
||||
migrate
|
||||
},
|
||||
|
||||
@@ -1445,6 +1445,16 @@ const migrateConfig = {
|
||||
} catch (error) {
|
||||
return state
|
||||
}
|
||||
},
|
||||
'107': (state: RootState) => {
|
||||
try {
|
||||
if (state.paintings && !state.paintings.DMXAPIPaintings) {
|
||||
state.paintings.DMXAPIPaintings = []
|
||||
}
|
||||
return state
|
||||
} catch (error) {
|
||||
return state
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,8 @@ const initialState: PaintingsState = {
|
||||
generate: [],
|
||||
remix: [],
|
||||
edit: [],
|
||||
upscale: []
|
||||
upscale: [],
|
||||
DMXAPIPaintings: []
|
||||
}
|
||||
|
||||
const paintingsSlice = createSlice({
|
||||
|
||||
@@ -246,6 +246,16 @@ export interface ScalePainting extends PaintingParams {
|
||||
renderingSpeed?: string
|
||||
}
|
||||
|
||||
export interface DmxapiPainting extends PaintingParams {
|
||||
model?: string
|
||||
prompt?: string
|
||||
n?: number
|
||||
aspect_ratio?: string
|
||||
image_size?: string
|
||||
seed?: string
|
||||
style_type?: string
|
||||
}
|
||||
|
||||
export type PaintingAction = Partial<GeneratePainting & RemixPainting & EditPainting & ScalePainting> & PaintingParams
|
||||
|
||||
export interface PaintingsState {
|
||||
@@ -254,6 +264,7 @@ export interface PaintingsState {
|
||||
remix: Partial<RemixPainting> & PaintingParams[]
|
||||
edit: Partial<EditPainting> & PaintingParams[]
|
||||
upscale: Partial<ScalePainting> & PaintingParams[]
|
||||
DMXAPIPaintings: DmxapiPainting[]
|
||||
}
|
||||
|
||||
export type MinAppType = {
|
||||
@@ -507,6 +518,19 @@ export interface MCPConfigSample {
|
||||
env?: Record<string, string> | undefined
|
||||
}
|
||||
|
||||
// MCP工具自定义参数配置接口
|
||||
export interface MCPToolParameterConfig {
|
||||
name: string
|
||||
defaultValue: any
|
||||
description?: string
|
||||
}
|
||||
|
||||
// MCP工具配置接口
|
||||
export interface MCPToolConfig {
|
||||
toolName: string
|
||||
parameters: MCPToolParameterConfig[]
|
||||
}
|
||||
|
||||
export interface MCPServer {
|
||||
id: string
|
||||
name: string
|
||||
@@ -519,6 +543,7 @@ export interface MCPServer {
|
||||
env?: Record<string, string>
|
||||
isActive: boolean
|
||||
disabledTools?: string[] // List of tool names that are disabled for this server
|
||||
customToolConfigs?: MCPToolConfig[]
|
||||
configSample?: MCPConfigSample
|
||||
headers?: Record<string, string> // Custom headers to be sent with requests to this server
|
||||
searchKey?: string
|
||||
|
||||
@@ -4,6 +4,17 @@ import { ModalFuncProps } from 'antd/es/modal/interface'
|
||||
// @ts-ignore next-line`
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
|
||||
export function isEmpty(value: any) {
|
||||
return (
|
||||
value === null ||
|
||||
value === undefined ||
|
||||
value === '' ||
|
||||
(Array.isArray(value) && value.length === 0) ||
|
||||
(typeof value === 'object' && Object.keys(value).length === 0) ||
|
||||
(typeof value === 'number' && value === 0)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 异步执行一个函数。
|
||||
* @param fn 要执行的函数
|
||||
|
||||
Reference in New Issue
Block a user