From 25b4d57f1bcc7038ddcba79e3850c3c2ec1c6402 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Wed, 21 May 2025 22:33:00 +0000 Subject: [PATCH] feat: Comprehensive translation service enhancements and fixes This commit introduces a series of significant improvements to the translation functionality: 1. **Enhanced Custom API Compatibility**: * You can now define `requestBodyTemplate` and `responseTextPath` for custom APIs, allowing flexible integration with various OpenAI-like services. * The backend dynamically constructs requests and parses responses based on these user-defined templates. 2. **Implemented Streaming Output**: * Streaming is now supported for OpenAI and compatible custom APIs (if configured with `supportsStreaming: true`). * The backend uses Server-Sent Events (SSE) to stream translation chunks. * The frontend incrementally displays translated text as it arrives, improving perceived performance. 3. **Configuration for Default Services**: * You can now configure API keys, models, and other parameters (e.g., Project ID for Google) for preset services (OpenAI, Google, DeepL, etc.) directly in the UI. * UI-configured settings take precedence over environment variables. * A consistent UI is provided for managing both preset and custom service configurations. 4. **Code Review and General Improvements**: * **Error Handling**: Significantly improved across the application. More specific error messages are provided, and `localStorage` operations are more robust. Comprehensive toast notifications guide you. * **Code Quality**: Refactored key sections for clarity and maintainability. Introduced helper functions like `getEffectiveConfigValue` to centralize logic. Enhanced TypeScript typings. * **UI/UX Polish**: Added visual cues (e.g., icons for configured services), improved tooltips, and ensured consistent feedback for your actions. * **Dependency Audit**: Performed an audit and applied fixes for several vulnerabilities. Noted a remaining critical vulnerability in `next` for future attention. Overall, these changes address the core issues of API compatibility, streaming, configuration, and general maintainability, making the application more powerful and easier to use. --- components/TranslationInput.tsx | 221 +++++++---- components/TranslationServices.tsx | 505 ++++++++++++++++++------- contexts/TranslationContext.tsx | 126 ++++++- package-lock.json | 37 +- pages/api/translate.ts | 569 ++++++++++++++++++++++------- 5 files changed, 1103 insertions(+), 355 deletions(-) diff --git a/components/TranslationInput.tsx b/components/TranslationInput.tsx index 0768fba..beb3eb9 100644 --- a/components/TranslationInput.tsx +++ b/components/TranslationInput.tsx @@ -1,9 +1,9 @@ "use client"; -import { Box, VStack, Textarea, Select, Button, Spinner, HStack, Grid, GridItem, Collapse, Flex, useToast, Text, Accordion, AccordionItem, AccordionButton, AccordionPanel, AccordionIcon } from "@chakra-ui/react"; -import { useState, useEffect } from "react"; -import axios from "axios"; -import { useTranslationContext } from "../contexts/TranslationContext"; +import { Box, VStack, Textarea, Select, Button, Spinner, HStack, Grid, GridItem, Flex, useToast, Text } from "@chakra-ui/react"; +import { useState } from "react"; +// Removed axios import, will use fetch +import { useTranslationContext, CustomAPI as CustomAPIType } from "../contexts/TranslationContext"; import { CopyIcon } from "@chakra-ui/icons"; import { IconButton } from "@chakra-ui/react"; import { v4 as uuidv4 } from 'uuid'; // 用于生成唯一ID @@ -13,9 +13,9 @@ const TranslationInput = () => { const [text, setText] = useState(""); const [sourceLang, setSourceLang] = useState("auto"); const [targetLang, setTargetLang] = useState("en"); - const { services, customAPIs, addHistory } = useTranslationContext(); - const [translations, setTranslations] = useState<{ service: string; text: string; name: string }[]>([]); - const [loading, setLoading] = useState(false); + const { services, customAPIs, addHistory, presetServiceConfigs } = useTranslationContext(); // Added presetServiceConfigs + const [translations, setTranslations] = useState<{ service: string; text: string; name: string; streaming?: boolean }[]>([]); + const [loadingServices, setLoadingServices] = useState>({}); // Track loading state per service const toast = useToast(); const handleTranslate = async () => { @@ -31,62 +31,151 @@ const TranslationInput = () => { return; } - setLoading(true); - setTranslations([]); + setTranslations(services.map(serviceId => ({ // Initialize translations + service: serviceId, + text: "", + name: getServiceName(serviceId), + streaming: false, + }))); + + const newLoadingStates: Record = {}; + services.forEach(s => newLoadingStates[s] = true); + setLoadingServices(newLoadingStates); - try { - const promises = services.map(service => - axios.post("/api/translate", { + services.forEach(async (serviceId, serviceIndex) => { + try { + const currentCustomAPI = serviceId.startsWith("custom_") + ? customAPIs.find((api: CustomAPIType) => api.id === serviceId) + : null; + + const effectiveSupportsStreaming = serviceId === 'openai' || (currentCustomAPI?.supportsStreaming); + + const requestBody: any = { text, sourceLang: sourceLang === "auto" ? "" : sourceLang, targetLang, - service, - customAPIs, - }) - ); - - const results = await Promise.all(promises); - const newTranslations = results.map((res, index) => ({ - service: services[index], - text: res.data.translatedText, - name: getServiceName(services[index]), - })); - setTranslations(newTranslations); - - // 添加到历史记录 - newTranslations.forEach(t => { - addHistory({ - id: uuidv4(), - inputText: text, - sourceLang, - targetLang, - service: t.service, - translatedText: t.text, - timestamp: new Date().toISOString(), + service: serviceId, + customAPIs, // For custom_ service, backend uses this + }; + + // If it's a preset service, add its specific config if available + if (!serviceId.startsWith("custom_")) { + const currentServicePresetConfig = presetServiceConfigs[serviceId]; + if (currentServicePresetConfig && Object.keys(currentServicePresetConfig).length > 0) { + requestBody.serviceConfig = currentServicePresetConfig; + } + } + + const response = await fetch("/api/translate", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(requestBody), }); - }); - toast({ - title: "翻译成功", - description: "您的文本已成功翻译。", - status: "success", - duration: 3000, - isClosable: true, - position: "top", - }); - } catch (error: any) { - console.error(error); - toast({ - title: "翻译失败", - description: error.response?.data?.error || "翻译失败,请稍后重试。", - status: "error", - duration: 3000, - isClosable: true, - position: "top", - }); - } finally { - setLoading(false); - } + if (!response.ok) { + const errorData = await response.json().catch(() => ({ error: "请求失败,无法解析错误信息。" })); + throw new Error(errorData.error || `HTTP error ${response.status}`); + } + + const contentType = response.headers.get("content-type"); + + if (effectiveSupportsStreaming && contentType && contentType.includes("text/event-stream")) { + setTranslations(prev => prev.map((t, i) => i === serviceIndex ? { ...t, streaming: true } : t)); + + const reader = response.body?.getReader(); + if (!reader) throw new Error("无法读取流。"); + const decoder = new TextDecoder(); + let accumulatedText = ""; + + while (true) { + const { done, value } = await reader.read(); + if (done) break; + const chunk = decoder.decode(value, { stream: true }); + const lines = chunk.split("\n\n"); + + for (const line of lines) { + if (line.startsWith("data: ")) { + try { + const jsonData = line.substring("data: ".length); + if (jsonData.trim() === "[DONE]") { // Check for a potential DONE signal if not part of JSON + break; + } + const parsed = JSON.parse(jsonData); + if (parsed.translatedText) { + accumulatedText += parsed.translatedText; + setTranslations(prev => + prev.map((t, i) => i === serviceIndex ? { ...t, text: accumulatedText } : t) + ); + } else if (parsed.error) { + console.error("Stream error from API:", parsed.error); + // Display error in the translation box or via toast + setTranslations(prev => + prev.map((t, i) => i === serviceIndex ? { ...t, text: `错误: ${parsed.error}` } : t) + ); + return; // Stop processing this stream + } + } catch (e) { + // Might be a non-JSON part of the chunk, or [DONE] + if (line.includes("[DONE]")) break; // More robust [DONE] check + console.warn("Non-JSON data in stream or malformed chunk:", line, e); + } + } + } + } + reader.releaseLock(); + addHistory({ + id: uuidv4(), + inputText: text, + sourceLang, + targetLang, + service: serviceId, + translatedText: accumulatedText, + timestamp: new Date().toISOString(), + }); + + } else { // Handle non-streaming response + const data = await response.json(); + if (data.error) throw new Error(data.error); + + setTranslations(prev => + prev.map((t, i) => i === serviceIndex ? { ...t, text: data.translatedText } : t) + ); + addHistory({ + id: uuidv4(), + inputText: text, + sourceLang, + targetLang, + service: serviceId, + translatedText: data.translatedText, + timestamp: new Date().toISOString(), + }); + } + toast({ + title: `服务 "${getServiceName(serviceId)}" 翻译成功`, + status: "success", + duration: 2000, + isClosable: true, + position: "top", + }); + + } catch (error: any) { + console.error(`Error with service ${serviceId}:`, error); + const errorMessage = error.message || "翻译失败,请稍后重试。"; + setTranslations(prev => + prev.map((t, i) => i === serviceIndex ? { ...t, text: `错误: ${errorMessage}` } : t) + ); + toast({ + title: `服务 "${getServiceName(serviceId)}" 翻译失败`, + description: errorMessage, + status: "error", + duration: 3000, + isClosable: true, + position: "top", + }); + } finally { + setLoadingServices(prev => ({ ...prev, [serviceId]: false })); + } + }); }; const handleCopy = (text: string) => { @@ -111,7 +200,7 @@ const TranslationInput = () => { }); }; - const getServiceName = (serviceId: string) => { + const getServiceName = (serviceId: string): string => { const serviceMap: { [key: string]: string } = { google: "谷歌翻译", openai: "OpenAI", @@ -183,29 +272,33 @@ const TranslationInput = () => { {translations.map((t, index) => ( - {t.name} + + {t.name} + {loadingServices[t.service] && } + {t.streaming && (流式)} + } size="sm" colorScheme="brand" diff --git a/components/TranslationServices.tsx b/components/TranslationServices.tsx index 6ba67e5..654aadf 100644 --- a/components/TranslationServices.tsx +++ b/components/TranslationServices.tsx @@ -1,54 +1,118 @@ "use client"; -import { Box, Heading, VStack, HStack, Button, IconButton, Modal, ModalOverlay, ModalContent, ModalHeader, ModalCloseButton, ModalBody, ModalFooter, Select, useDisclosure, Text, Input, Tabs, TabList, TabPanels, Tab, TabPanel } from "@chakra-ui/react"; -import { CloseIcon, AddIcon, EditIcon } from "@chakra-ui/icons"; -import { useState } from "react"; -import { useTranslationContext } from "../contexts/TranslationContext"; +import { Box, Heading, VStack, HStack, Button, IconButton, Modal, ModalOverlay, ModalContent, ModalHeader, ModalCloseButton, ModalBody, ModalFooter, Select, useDisclosure, Text, Input, Tabs, TabList, TabPanels, Tab, TabPanel, Checkbox, FormControl, FormLabel, Tooltip, useToast, Circle, Icon } from "@chakra-ui/react"; // Added useToast, Circle, Icon +import { CloseIcon, AddIcon, EditIcon, SettingsIcon, CheckCircleIcon } from "@chakra-ui/icons"; // Added CheckCircleIcon +import { useState, useEffect } from "react"; +import { useTranslationContext, CustomAPI as CustomAPIType, PresetServiceConfig } from "../contexts/TranslationContext"; -interface Service { +// Define a more detailed Service type for allAvailableServices +interface ServiceDefinition { id: string; name: string; + isPreset: boolean; + isConfigurable?: boolean; + configurableFields?: Array<{ + name: keyof PresetServiceConfig; + label: string; + placeholder?: string; + type?: string; + tooltip?: string; + }>; } -interface CustomAPI { - id: string; - name: string; - endpoint: string; - apiKey: string; - model: string; -} -const allAvailableServices: Service[] = [ - { id: "google", name: "谷歌翻译" }, - { id: "openai", name: "OpenAI" }, - { id: "tongyi", name: "通义千问" }, - { id: "deepl", name: "DeepL" }, - { id: "siliconflow", name: "硅基流动" }, +const allAvailableServices: ServiceDefinition[] = [ + { + id: "openai", + name: "OpenAI", + isPreset: true, + isConfigurable: true, + configurableFields: [ + { name: "apiKey", label: "API 密钥", placeholder: "sk-...", type: "password", tooltip: "您的 OpenAI API 密钥。" }, + { name: "model", label: "模型", placeholder: "gpt-4o-mini", tooltip: "要使用的模型,例如 gpt-4o-mini, gpt-4-turbo 等。" } + ] + }, + { + id: "google", + name: "谷歌翻译", + isPreset: true, + isConfigurable: true, + configurableFields: [ + { name: "apiKey", label: "API 密钥", placeholder: "AIzaSy...", type: "password", tooltip: "您的 Google Cloud Translation API 密钥。" }, + { name: "projectId", label: "项目 ID", placeholder: "my-gcp-project-id", tooltip: "您的 Google Cloud 项目 ID。" } + ] + }, + { + id: "deepl", + name: "DeepL", + isPreset: true, + isConfigurable: true, + configurableFields: [ + { name: "apiKey", label: "API 密钥", placeholder: "your-deepl-auth-key", type: "password", tooltip: "您的 DeepL API 密钥 (通常以 :fx 结尾)。" } + ] + }, + { id: "tongyi", name: "通义千问", isPreset: true, isConfigurable: true, configurableFields: [{ name: "apiKey", label: "API 密钥", type: "password" }, { name: "model", label: "模型" }] }, + { id: "siliconflow", name: "硅基流动", isPreset: true, isConfigurable: true, configurableFields: [{ name: "apiKey", label: "API 密钥", type: "password" }, { name: "model", label: "模型" }] }, + { id: "deepseek", name: "深度求索", isPreset: true, isConfigurable: true, configurableFields: [{ name: "apiKey", label: "API 密钥", type: "password" }, { name: "model", label: "模型" }] }, + // Example of a non-configurable preset service (if any were added) + // { id: "simpletranslate", name: "Simple Translate", isPreset: true, isConfigurable: false }, ]; const TranslationServices = () => { - const { services, setServices, customAPIs, addCustomAPI, removeCustomAPI, editCustomAPI, clearData, exportConfig, importConfig } = useTranslationContext(); - const { isOpen, onOpen, onClose } = useDisclosure(); + const { + services, setServices, + customAPIs, addCustomAPI, removeCustomAPI, editCustomAPI, + presetServiceConfigs, updatePresetServiceConfig, removePresetServiceConfig, + clearData, exportConfig, importConfig + } = useTranslationContext(); + const toast = useToast(); // Initialize useToast + + // Modal for adding/editing custom APIs (existing) + const { isOpen: isCustomApiModalOpen, onOpen: onCustomApiModalOpen, onClose: onCustomApiModalClose } = useDisclosure(); + // Modal for configuring preset services (new) + const { isOpen: isPresetConfigModalOpen, onOpen: onPresetConfigModalOpen, onClose: onPresetConfigModalClose } = useDisclosure(); + const [selectedService, setSelectedService] = useState(""); const [customAPIName, setCustomAPIName] = useState(""); const [customAPIEndpoint, setCustomAPIEndpoint] = useState(""); const [customAPIKey, setCustomAPIKey] = useState(""); - const [editingAPI, setEditingAPI] = useState(null); + const [editingAPI, setEditingAPI] = useState(null); // For custom APIs const [editName, setEditName] = useState(""); const [editEndpoint, setEditEndpoint] = useState(""); const [editAPIKey, setEditAPIKey] = useState(""); const [customAPIModel, setCustomAPIModel] = useState(""); const [editModel, setEditModel] = useState(""); + const [customAPIRequestBodyTemplate, setCustomAPIRequestBodyTemplate] = useState(""); + const [editRequestBodyTemplate, setEditRequestBodyTemplate] = useState(""); + const [customAPIResponseTextPath, setCustomAPIResponseTextPath] = useState(""); + const [editResponseTextPath, setEditResponseTextPath] = useState(""); + const [customAPISupportsStreaming, setCustomAPISupportsStreaming] = useState(false); + const [editSupportsStreaming, setEditSupportsStreaming] = useState(false); + + // State for preset service configuration modal + const [configuringPresetService, setConfiguringPresetService] = useState(null); + const [currentPresetConfigValues, setCurrentPresetConfigValues] = useState>({}); const removeService = (id: string) => { - setServices(services.filter(service => service !== id)); + // If this service had a preset config, offer to remove it too or handle as needed + // For now, just removing from active services. User can clear config separately. + setServices(services.filter(s => s !== id)); + const serviceName = getServiceName(id); + toast({ title: `服务 "${serviceName}" 已从活动列表移除`, status: "info", duration: 2000, isClosable: true, position: "top" }); + }; + + const handleRemoveCustomAPI = (id: string) => { + const apiToRemove = customAPIs.find(api => api.id === id); + removeCustomAPI(id); + toast({ title: `自定义API "${apiToRemove?.name || id}" 已删除`, status: "info", duration: 2000, isClosable: true, position: "top" }); }; const addService = () => { if (selectedService && !services.includes(selectedService)) { setServices([...services, selectedService]); + toast({ title: `服务 "${getServiceName(selectedService)}" 已添加`, status: "success", duration: 2000, isClosable: true, position: "top" }); } - onClose(); + onCustomApiModalClose(); setSelectedService(""); }; @@ -59,47 +123,161 @@ const TranslationServices = () => { name: customAPIName, endpoint: customAPIEndpoint, apiKey: customAPIKey, - model: customAPIModel + model: customAPIModel, + requestBodyTemplate: customAPIRequestBodyTemplate, + responseTextPath: customAPIResponseTextPath, + supportsStreaming: customAPISupportsStreaming, }); setCustomAPIName(""); setCustomAPIEndpoint(""); setCustomAPIKey(""); setCustomAPIModel(""); - onClose(); + setCustomAPIRequestBodyTemplate(""); + setCustomAPIResponseTextPath(""); + setCustomAPISupportsStreaming(false); + onCustomApiModalClose(); + toast({ title: "自定义API已添加", description: customAPIName, status: "success", duration: 2000, isClosable: true, position: "top" }); }; - const handleEditCustomAPI = (api: CustomAPI) => { - setEditingAPI(api.id); + const handleEditCustomAPI = (api: CustomAPIType) => { + setEditingAPI(api.id); // Mark that we are editing a custom API + setConfiguringPresetService(null); // Ensure we are not configuring a preset setEditName(api.name); setEditEndpoint(api.endpoint); setEditAPIKey(api.apiKey); setEditModel(api.model); - onOpen(); + setEditRequestBodyTemplate(api.requestBodyTemplate || ""); + setEditResponseTextPath(api.responseTextPath || ""); + setEditSupportsStreaming(api.supportsStreaming || false); + onCustomApiModalOpen(); // Open the custom API modal }; - const handleSaveEdit = () => { + // Handler for opening the preset configuration modal + const handleConfigurePresetService = (serviceId: string) => { + const serviceToConfigure = allAvailableServices.find(s => s.id === serviceId); + if (serviceToConfigure && serviceToConfigure.isConfigurable) { + setConfiguringPresetService(serviceToConfigure); + setCurrentPresetConfigValues(presetServiceConfigs[serviceId] || {}); + onPresetConfigModalOpen(); + } + }; + + // Handler for saving preset service configuration + const handleSavePresetConfig = () => { + if (configuringPresetService) { + updatePresetServiceConfig(configuringPresetService.id, currentPresetConfigValues); + toast({ title: `配置已保存: ${configuringPresetService.name}`, status: "success", duration: 2000, isClosable: true, position: "top" }); + onPresetConfigModalClose(); + setConfiguringPresetService(null); + } + }; + + // Handler for clearing a specific preset service configuration + const handleClearPresetConfig = () => { + if (configuringPresetService) { + removePresetServiceConfig(configuringPresetService.id); + setCurrentPresetConfigValues({}); + toast({ title: `配置已清除: ${configuringPresetService.name}`, status: "info", duration: 2000, isClosable: true, position: "top" }); + // Keep modal open for further changes or close it if preferred: + // onPresetConfigModalClose(); + // setConfiguringPresetService(null); + } + }; + + const handleSaveEditCustomAPI = () => { if (editingAPI) { editCustomAPI(editingAPI, { name: editName, endpoint: editEndpoint, apiKey: editAPIKey, - model: editModel + model: editModel, + requestBodyTemplate: editRequestBodyTemplate, + responseTextPath: editResponseTextPath, + supportsStreaming: editSupportsStreaming, }); - setEditingAPI(null); - onClose(); + toast({ title: "自定义API已更新", description: editName, status: "success", duration: 2000, isClosable: true, position: "top" }); + setEditingAPI(null); + onCustomApiModalClose(); } }; + const getServiceName = (serviceId: string): string => { + const service = allAvailableServices.find(s => s.id === serviceId); + if (service) return service.name; + const customApi = customAPIs.find(c => c.id === serviceId); + if (customApi) return customApi.name; + return serviceId; + }; + + // Combined close handler for the main modal (Add Service / Edit Custom API) + const onMainModalClose = () => { + setEditingAPI(null); // Reset custom API editing state + onCustomApiModalClose(); // Close the Chakra modal + }; + return ( 翻译服务 {services.map(serviceId => { - const service = allAvailableServices.find(s => s.id === serviceId); - return ( - service && ( + const serviceDef = allAvailableServices.find(s => s.id === serviceId); + // If it's a preset service + if (serviceDef && serviceDef.isPreset) { + return ( + + + {serviceDef.isConfigurable && presetServiceConfigs[serviceId] && Object.values(presetServiceConfigs[serviceId]!).some(val => val && val !== '') && ( + + + + )} + {serviceDef.name} + + + {serviceDef.isConfigurable && ( + + } + size="sm" + colorScheme="teal" + variant="ghost" + onClick={() => handleConfigurePresetService(serviceId)} + transition="all 0.3s ease" + _hover={{ bg: "teal.600" }} + /> + + )} + + } + size="sm" + colorScheme="red" + variant="ghost" + onClick={() => removeService(serviceId)} + transition="all 0.3s ease" + _hover={{ bg: "red.600" }} + /> + + + + ); + } + // If it's a custom API (logic remains similar, find by ID in customAPIs) + const customApiDef = customAPIs.find(api => api.id === serviceId); + if (customApiDef) { + return ( { transition="all 0.3s ease" _hover={{ bg: "gray.600" }} > - {service.name} - } - size="sm" - colorScheme="red" - variant="ghost" - onClick={() => removeService(service.id)} - transition="all 0.3s ease" - _hover={{ bg: "red.600" }} - /> + {customApiDef.name} + + + } + size="sm" + colorScheme="blue" + variant="ghost" + onClick={() => handleEditCustomAPI(customApiDef)} + transition="all 0.3s ease" + _hover={{ bg: "blue.600" }} + /> + + + } + size="sm" + colorScheme="red" + variant="ghost" + onClick={() => handleRemoveCustomAPI(customApiDef.id)} + transition="all 0.3s ease" + _hover={{ bg: "red.600" }} + /> + + - ) - ); + ); + } + return null; // Should not happen if services and customAPIs are consistent })} - {customAPIs.map(api => ( - - {api.name} - - } - size="sm" - colorScheme="blue" - variant="ghost" - onClick={() => handleEditCustomAPI(api)} - transition="all 0.3s ease" - _hover={{ bg: "blue.600" }} - /> - } - size="sm" - colorScheme="red" - variant="ghost" - onClick={() => removeCustomAPI(api.id)} - transition="all 0.3s ease" - _hover={{ bg: "red.600" }} - /> - - - ))} + {/* This mapping is now handled above by iterating through `services` state */} + {/* {customAPIs.map(api => ( ... ))} */} - + {/* Modal for Adding Services (Preset/Custom) and Editing Custom APIs */} + - {editingAPI ? "编辑自定义API" : "添加翻译服务"} + {editingAPI ? "编辑自定义API" : "添加服务"} - + { + // Reset custom API editing state if user switches tabs + if (editingAPI) setEditingAPI(null); + }}> 预设服务 自定义API @@ -185,84 +345,175 @@ const TranslationServices = () => { editingAPI ? setEditName(e.target.value) : setCustomAPIName(e.target.value)} + onChange={(e) => { + if (editingAPI) setEditName(e.target.value); + else setCustomAPIName(e.target.value); + }} bg="gray.700" borderColor="gray.600" /> editingAPI ? setEditEndpoint(e.target.value) : setCustomAPIEndpoint(e.target.value)} + onChange={(e) => { + if (editingAPI) setEditEndpoint(e.target.value); + else setCustomAPIEndpoint(e.target.value); + }} bg="gray.700" borderColor="gray.600" /> editingAPI ? setEditAPIKey(e.target.value) : setCustomAPIKey(e.target.value)} + onChange={(e) => { + if (editingAPI) setEditAPIKey(e.target.value); + else setCustomAPIKey(e.target.value); + }} bg="gray.700" borderColor="gray.600" /> editingAPI ? setEditModel(e.target.value) : setCustomAPIModel(e.target.value)} + onChange={(e) => { + if (editingAPI) setEditModel(e.target.value); + else setCustomAPIModel(e.target.value); + }} + bg="gray.700" + borderColor="gray.600" + /> + { + if (editingAPI) setEditRequestBodyTemplate(e.target.value); + else setCustomAPIRequestBodyTemplate(e.target.value); + }} bg="gray.700" borderColor="gray.600" /> + { + if (editingAPI) setEditResponseTextPath(e.target.value); + else setCustomAPIResponseTextPath(e.target.value); + }} + bg="gray.700" + borderColor="gray.600" + /> + { + if (editingAPI) setEditSupportsStreaming(e.target.checked); + else setCustomAPISupportsStreaming(e.target.checked); + }} + colorScheme="brand" + > + 支持流式响应 (SSE) + - + + {/* Modal for Configuring Preset Services */} + {configuringPresetService && ( + { setConfiguringPresetService(null); onPresetConfigModalClose();}}> + + + 配置 {configuringPresetService.name} + + + + {configuringPresetService.configurableFields?.map(field => ( + + + {field.label} + + setCurrentPresetConfigValues(prev => ({ ...prev, [field.name]: e.target.value }))} + bg="gray.700" + borderColor="gray.600" + /> + + ))} + + + + + + + + + + )}