mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-31 08:29:07 +08:00
feat(i18n): update localization files to include new API server controls and descriptions
- Added "Start" and "Stop" labels in English, Japanese, Russian, Chinese (Simplified and Traditional) locales. - Introduced "Authorization Header" title and descriptions for API key and port fields across all locales. - Removed deprecated API documentation unavailable messages for a cleaner user experience.
This commit is contained in:
parent
0cafaafdf2
commit
b629cd236d
@ -83,25 +83,28 @@
|
||||
"restart": {
|
||||
"button": "Restart",
|
||||
"tooltip": "Restart Server"
|
||||
}
|
||||
},
|
||||
"start": "Start",
|
||||
"stop": "Stop"
|
||||
},
|
||||
"authHeader": {
|
||||
"title": "Authorization Header"
|
||||
},
|
||||
"authHeaderText": "Use in Authorization header:",
|
||||
"configuration": "Configuration",
|
||||
"description": "Expose Cherry Studio's AI capabilities through OpenAI-compatible HTTP APIs",
|
||||
"documentation": {
|
||||
"title": "API Documentation",
|
||||
"unavailable": {
|
||||
"description": "Start the API server to view the interactive documentation",
|
||||
"title": "API Documentation Unavailable"
|
||||
}
|
||||
"title": "API Documentation"
|
||||
},
|
||||
"fields": {
|
||||
"apiKey": {
|
||||
"copyTooltip": "Copy API Key",
|
||||
"description": "Secure authentication token for API access",
|
||||
"label": "API Key",
|
||||
"placeholder": "API key will be auto-generated"
|
||||
},
|
||||
"port": {
|
||||
"description": "TCP port number for the HTTP server (1000-65535)",
|
||||
"helpText": "Stop server to change port",
|
||||
"label": "Port"
|
||||
},
|
||||
|
||||
@ -83,25 +83,28 @@
|
||||
"restart": {
|
||||
"button": "再起動",
|
||||
"tooltip": "サーバーを再起動"
|
||||
}
|
||||
},
|
||||
"start": "開始",
|
||||
"stop": "停止"
|
||||
},
|
||||
"authHeader": {
|
||||
"title": "認証ヘッダー"
|
||||
},
|
||||
"authHeaderText": "認証ヘッダーで使用:",
|
||||
"configuration": "設定",
|
||||
"description": "OpenAI 互換の HTTP API を通じて Cherry Studio の AI 機能を公開します",
|
||||
"documentation": {
|
||||
"title": "API ドキュメント",
|
||||
"unavailable": {
|
||||
"description": "インタラクティブドキュメントを表示するには API サーバーを開始してください",
|
||||
"title": "API ドキュメントが利用できません"
|
||||
}
|
||||
"title": "API ドキュメント"
|
||||
},
|
||||
"fields": {
|
||||
"apiKey": {
|
||||
"copyTooltip": "API キーをコピー",
|
||||
"description": "API アクセスのための安全な認証トークン",
|
||||
"label": "API キー",
|
||||
"placeholder": "API キーは自動生成されます"
|
||||
},
|
||||
"port": {
|
||||
"description": "HTTP サーバーの TCP ポート番号 (1000-65535)",
|
||||
"helpText": "ポートを変更するにはサーバーを停止してください",
|
||||
"label": "ポート"
|
||||
},
|
||||
@ -1575,7 +1578,7 @@
|
||||
"prompt_placeholder": "作成したい画像を説明します。例:夕日の湖畔、遠くに山々",
|
||||
"prompt_placeholder_edit": "画像の説明を入力します。テキスト描画には '二重引用符' を使用します",
|
||||
"prompt_placeholder_en": "「英語」の説明を入力します。Imagenは現在、英語のプロンプト語のみをサポートしています",
|
||||
"proxy_required": "打開代理並開啟”TUN模式“查看生成圖片或複製到瀏覽器開啟,後續會支持國內直連",
|
||||
"proxy_required": "打開代理並開啟TUN模式查看生成圖片或複製到瀏覽器開啟,後續會支持國內直連",
|
||||
"quality": "品質",
|
||||
"quality_options": {
|
||||
"auto": "自動",
|
||||
|
||||
@ -83,25 +83,28 @@
|
||||
"restart": {
|
||||
"button": "Перезапустить",
|
||||
"tooltip": "Перезапустить сервер"
|
||||
}
|
||||
},
|
||||
"start": "Запустить",
|
||||
"stop": "Остановить"
|
||||
},
|
||||
"authHeader": {
|
||||
"title": "Авторизация"
|
||||
},
|
||||
"authHeaderText": "Использовать в заголовке авторизации:",
|
||||
"configuration": "Конфигурация",
|
||||
"description": "Предоставляет возможности ИИ Cherry Studio через HTTP API, совместимые с OpenAI",
|
||||
"documentation": {
|
||||
"title": "Документация API",
|
||||
"unavailable": {
|
||||
"description": "Запустите API сервер для просмотра интерактивной документации",
|
||||
"title": "Документация API недоступна"
|
||||
}
|
||||
"title": "Документация API"
|
||||
},
|
||||
"fields": {
|
||||
"apiKey": {
|
||||
"copyTooltip": "Копировать API ключ",
|
||||
"description": "Безопасный токен для доступа к API",
|
||||
"label": "API Ключ",
|
||||
"placeholder": "API ключ будет сгенерирован автоматически"
|
||||
},
|
||||
"port": {
|
||||
"description": "TCP порт для HTTP сервера (1000-65535)",
|
||||
"helpText": "Остановите сервер для изменения порта",
|
||||
"label": "Порт"
|
||||
},
|
||||
|
||||
@ -83,25 +83,28 @@
|
||||
"restart": {
|
||||
"button": "重启",
|
||||
"tooltip": "重启服务器"
|
||||
}
|
||||
},
|
||||
"start": "启动",
|
||||
"stop": "停止"
|
||||
},
|
||||
"authHeader": {
|
||||
"title": "授权标头"
|
||||
},
|
||||
"authHeaderText": "在授权标头中使用:",
|
||||
"configuration": "配置",
|
||||
"description": "通过 OpenAI 兼容的 HTTP API 暴露 Cherry Studio 的 AI 功能",
|
||||
"documentation": {
|
||||
"title": "API 文档",
|
||||
"unavailable": {
|
||||
"description": "启动 API 服务器以查看交互式文档",
|
||||
"title": "API 文档不可用"
|
||||
}
|
||||
"title": "API 文档"
|
||||
},
|
||||
"fields": {
|
||||
"apiKey": {
|
||||
"copyTooltip": "复制 API 密钥",
|
||||
"description": "用于 API 访问的安全认证令牌",
|
||||
"label": "API 密钥",
|
||||
"placeholder": "API 密钥将自动生成"
|
||||
},
|
||||
"port": {
|
||||
"description": "HTTP 服务器的 TCP 端口号 (1000-65535)",
|
||||
"helpText": "停止服务器以更改端口",
|
||||
"label": "端口"
|
||||
},
|
||||
|
||||
@ -83,25 +83,28 @@
|
||||
"restart": {
|
||||
"button": "重新啟動",
|
||||
"tooltip": "重新啟動伺服器"
|
||||
}
|
||||
},
|
||||
"start": "啟動",
|
||||
"stop": "停止"
|
||||
},
|
||||
"authHeader": {
|
||||
"title": "授權標頭"
|
||||
},
|
||||
"authHeaderText": "在授權標頭中使用:",
|
||||
"configuration": "配置",
|
||||
"description": "透過 OpenAI 相容的 HTTP API 公開 Cherry Studio 的 AI 功能",
|
||||
"documentation": {
|
||||
"title": "API 文件",
|
||||
"unavailable": {
|
||||
"description": "啟動 API 伺服器以檢視互動式文件",
|
||||
"title": "API 文件無法使用"
|
||||
}
|
||||
"title": "API 文件"
|
||||
},
|
||||
"fields": {
|
||||
"apiKey": {
|
||||
"copyTooltip": "複製 API 金鑰",
|
||||
"description": "用於 API 訪問的安全認證令牌",
|
||||
"label": "API 金鑰",
|
||||
"placeholder": "API 金鑰將自動生成"
|
||||
},
|
||||
"port": {
|
||||
"description": "HTTP 伺服器的 TCP 連接埠 (1000-65535)",
|
||||
"helpText": "停止伺服器以變更連接埠",
|
||||
"label": "連接埠"
|
||||
},
|
||||
@ -1574,7 +1577,7 @@
|
||||
"prompt_enhancement_tip": "開啟後將提示重寫為詳細的、適合模型的版本",
|
||||
"prompt_placeholder": "描述你想建立的圖片,例如:一個寧靜的湖泊,夕陽西下,遠處是群山",
|
||||
"prompt_placeholder_edit": "輸入你的圖片描述,文本繪製用 ' 雙引號 ' 包裹",
|
||||
"prompt_placeholder_en": "輸入” 英文 “圖片描述,目前 Imagen 僅支持英文提示詞",
|
||||
"prompt_placeholder_en": "輸入英文圖片描述,目前 Imagen 僅支持英文提示詞",
|
||||
"proxy_required": "打開代理並開啟”TUN 模式 “查看生成圖片或複製到瀏覽器開啟,後續會支持國內直連",
|
||||
"quality": "品質",
|
||||
"quality_options": {
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
import { CopyOutlined, GlobalOutlined, ReloadOutlined } from '@ant-design/icons'
|
||||
import { useTheme } from '@renderer/context/ThemeProvider'
|
||||
import { loggerService } from '@renderer/services/LoggerService'
|
||||
import { RootState, useAppDispatch } from '@renderer/store'
|
||||
import { setApiServerApiKey, setApiServerPort } from '@renderer/store/settings'
|
||||
import { IpcChannel } from '@shared/IpcChannel'
|
||||
import { Button, Card, Input, Space, Switch, Tooltip, Typography } from 'antd'
|
||||
import { Button, Input, InputNumber, Tooltip, Typography } from 'antd'
|
||||
import { Copy, ExternalLink, Play, RotateCcw, Square } from 'lucide-react'
|
||||
import { FC, useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useSelector } from 'react-redux'
|
||||
@ -16,175 +16,13 @@ import { SettingContainer } from '..'
|
||||
const logger = loggerService.withContext('ApiServerSettings')
|
||||
const { Text, Title } = Typography
|
||||
|
||||
const ConfigCard = styled(Card)`
|
||||
margin-bottom: 24px;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
border: 1px solid var(--color-border);
|
||||
|
||||
.ant-card-head {
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
padding: 16px 24px;
|
||||
}
|
||||
|
||||
.ant-card-body {
|
||||
padding: 16px 20px;
|
||||
}
|
||||
`
|
||||
|
||||
const SectionHeader = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 12px;
|
||||
|
||||
h4 {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: var(--color-text-1);
|
||||
}
|
||||
`
|
||||
|
||||
const FieldLabel = styled.div`
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: var(--color-text-1);
|
||||
margin-bottom: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
`
|
||||
|
||||
const ActionButtonGroup = styled(Space)`
|
||||
.ant-btn {
|
||||
border-radius: 6px;
|
||||
font-weight: 500;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.ant-btn-primary {
|
||||
background: #1677ff;
|
||||
border-color: #1677ff;
|
||||
}
|
||||
|
||||
.ant-btn:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
`
|
||||
|
||||
const StyledInput = styled(Input)`
|
||||
border-radius: 6px;
|
||||
border: 1.5px solid var(--color-border);
|
||||
|
||||
&:focus,
|
||||
&:focus-within {
|
||||
border-color: #1677ff;
|
||||
box-shadow: 0 0 0 2px rgba(22, 119, 255, 0.1);
|
||||
}
|
||||
`
|
||||
|
||||
const ServerControlPanel = styled.div<{ status: boolean }>`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 12px 16px;
|
||||
border-radius: 8px;
|
||||
background: ${(props) =>
|
||||
props.status
|
||||
? 'linear-gradient(135deg, #f6ffed 0%, #f0f9ff 100%)'
|
||||
: 'linear-gradient(135deg, #fff2f0 0%, #fafafa 100%)'};
|
||||
border: 1px solid ${(props) => (props.status ? '#d9f7be' : '#ffd6d6')};
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
`
|
||||
|
||||
const StatusSection = styled.div<{ status: boolean }>`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
|
||||
.status-indicator {
|
||||
position: relative;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 50%;
|
||||
background: ${(props) => (props.status ? '#52c41a' : '#ff4d4f')};
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: -3px;
|
||||
border-radius: 50%;
|
||||
background: ${(props) => (props.status ? '#52c41a' : '#ff4d4f')};
|
||||
opacity: 0.2;
|
||||
animation: ${(props) => (props.status ? 'pulse 2s infinite' : 'none')};
|
||||
}
|
||||
}
|
||||
|
||||
.status-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.status-text {
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
color: ${(props) => (props.status ? '#52c41a' : '#ff4d4f')};
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.status-subtext {
|
||||
font-size: 12px;
|
||||
color: var(--color-text-3);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%,
|
||||
100% {
|
||||
transform: scale(1);
|
||||
opacity: 0.2;
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.5);
|
||||
opacity: 0.1;
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const ControlSection = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
|
||||
.restart-btn {
|
||||
opacity: 0;
|
||||
transform: translateX(10px);
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&.visible {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const ApiServerSettings: FC = () => {
|
||||
const { theme } = useTheme()
|
||||
const dispatch = useAppDispatch()
|
||||
const { t } = useTranslation()
|
||||
|
||||
// API Server state with proper defaults
|
||||
const apiServerConfig = useSelector((state: RootState) => {
|
||||
return state.settings.apiServer
|
||||
})
|
||||
const apiServerConfig = useSelector((state: RootState) => state.settings.apiServer)
|
||||
|
||||
const [apiServerRunning, setApiServerRunning] = useState(false)
|
||||
const [apiServerLoading, setApiServerLoading] = useState(false)
|
||||
@ -265,179 +103,320 @@ const ApiServerSettings: FC = () => {
|
||||
}
|
||||
}
|
||||
|
||||
const openApiDocs = () => {
|
||||
if (apiServerRunning) {
|
||||
window.open(`http://localhost:${apiServerConfig.port}/api-docs`, '_blank')
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<SettingContainer theme={theme}>
|
||||
<Container theme={theme}>
|
||||
{/* Header Section */}
|
||||
<div style={{ marginBottom: 32 }}>
|
||||
<Title level={3} style={{ margin: 0, marginBottom: 8 }}>
|
||||
{t('apiServer.title')}
|
||||
</Title>
|
||||
<Text type="secondary">{t('apiServer.description')}</Text>
|
||||
</div>
|
||||
|
||||
{/* Server Status & Configuration Card */}
|
||||
<ConfigCard
|
||||
title={
|
||||
<SectionHeader>
|
||||
<GlobalOutlined />
|
||||
<h4>{t('apiServer.configuration')}</h4>
|
||||
</SectionHeader>
|
||||
}>
|
||||
<Space direction="vertical" size={12} style={{ width: '100%' }}>
|
||||
{/* Server Control Panel */}
|
||||
<ServerControlPanel status={apiServerRunning}>
|
||||
<StatusSection status={apiServerRunning}>
|
||||
<div className="status-indicator" />
|
||||
<div className="status-content">
|
||||
<div className="status-text">
|
||||
{apiServerRunning ? t('apiServer.status.running') : t('apiServer.status.stopped')}
|
||||
</div>
|
||||
<div className="status-subtext">
|
||||
{apiServerRunning ? `http://localhost:${apiServerConfig.port}` : t('apiServer.fields.port.helpText')}
|
||||
</div>
|
||||
</div>
|
||||
</StatusSection>
|
||||
|
||||
<ControlSection>
|
||||
<Switch
|
||||
checked={apiServerRunning}
|
||||
loading={apiServerLoading}
|
||||
onChange={handleApiServerToggle}
|
||||
size="default"
|
||||
/>
|
||||
<Tooltip title={t('apiServer.actions.restart.tooltip')}>
|
||||
<Button
|
||||
icon={<ReloadOutlined />}
|
||||
onClick={handleApiServerRestart}
|
||||
loading={apiServerLoading}
|
||||
size="small"
|
||||
type="text"
|
||||
className={`restart-btn ${apiServerRunning ? 'visible' : ''}`}>
|
||||
{t('apiServer.actions.restart.button')}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</ControlSection>
|
||||
</ServerControlPanel>
|
||||
|
||||
{/* Configuration Fields */}
|
||||
<div style={{ display: 'grid', gap: '12px' }}>
|
||||
{/* Port Configuration */}
|
||||
{!apiServerRunning && (
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 12, flexWrap: 'wrap' }}>
|
||||
<FieldLabel style={{ minWidth: 50, margin: 0 }}>{t('apiServer.fields.port.label')}</FieldLabel>
|
||||
<StyledInput
|
||||
type="number"
|
||||
value={apiServerConfig.port}
|
||||
onChange={(e) => handlePortChange(e.target.value)}
|
||||
style={{ width: 100 }}
|
||||
min={1000}
|
||||
max={65535}
|
||||
disabled={apiServerRunning}
|
||||
placeholder="23333"
|
||||
size="small"
|
||||
/>
|
||||
{apiServerRunning && (
|
||||
<Text type="secondary" style={{ fontSize: 12 }}>
|
||||
{t('apiServer.fields.port.helpText')}
|
||||
</Text>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* API Key Configuration */}
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 12, flexWrap: 'wrap' }}>
|
||||
<FieldLabel style={{ minWidth: 50, margin: 0 }}>{t('apiServer.fields.apiKey.label')}</FieldLabel>
|
||||
<StyledInput
|
||||
value={apiServerConfig.apiKey}
|
||||
readOnly
|
||||
style={{ flex: 1, minWidth: 200, maxWidth: 300 }}
|
||||
placeholder={t('apiServer.fields.apiKey.placeholder')}
|
||||
disabled={apiServerRunning}
|
||||
size="small"
|
||||
/>
|
||||
<ActionButtonGroup>
|
||||
<Tooltip title={t('apiServer.fields.apiKey.copyTooltip')}>
|
||||
<Button icon={<CopyOutlined />} onClick={copyApiKey} disabled={!apiServerConfig.apiKey} size="small">
|
||||
{t('apiServer.actions.copy')}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
{!apiServerRunning && (
|
||||
<Button onClick={regenerateApiKey} disabled={apiServerRunning} size="small">
|
||||
{t('apiServer.actions.regenerate')}
|
||||
</Button>
|
||||
)}
|
||||
</ActionButtonGroup>
|
||||
</div>
|
||||
|
||||
{/* Authorization header info */}
|
||||
<Text type="secondary" style={{ fontSize: 11, lineHeight: 1.3 }}>
|
||||
{t('apiServer.authHeaderText')}{' '}
|
||||
<Text code style={{ fontSize: 11 }}>
|
||||
Bearer {apiServerConfig.apiKey || 'your-api-key'}
|
||||
</Text>
|
||||
</Text>
|
||||
</div>
|
||||
</Space>
|
||||
</ConfigCard>
|
||||
|
||||
{/* API Documentation Card */}
|
||||
<ConfigCard
|
||||
style={{
|
||||
flex: 1,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
marginBottom: 0
|
||||
}}
|
||||
styles={{
|
||||
body: {
|
||||
flex: 1,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
padding: 0
|
||||
}
|
||||
}}
|
||||
title={
|
||||
<SectionHeader>
|
||||
<h4>{t('apiServer.documentation.title')}</h4>
|
||||
</SectionHeader>
|
||||
}>
|
||||
{apiServerRunning ? (
|
||||
<iframe
|
||||
src={`http://localhost:${apiServerConfig.port}/api-docs`}
|
||||
style={{
|
||||
width: '100%',
|
||||
border: 'none',
|
||||
height: '100vh'
|
||||
}}
|
||||
title="API Documentation"
|
||||
sandbox="allow-scripts allow-forms"
|
||||
/>
|
||||
) : (
|
||||
<div
|
||||
style={{
|
||||
textAlign: 'center',
|
||||
padding: '60px 20px',
|
||||
color: 'var(--color-text-2)',
|
||||
background: 'var(--color-bg-2)',
|
||||
borderRadius: 8,
|
||||
border: '1px dashed var(--color-border)',
|
||||
flex: 1,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'center',
|
||||
margin: 16,
|
||||
height: '300px'
|
||||
}}>
|
||||
<GlobalOutlined style={{ fontSize: 48, marginBottom: 16, opacity: 0.3 }} />
|
||||
<div style={{ fontSize: 16, fontWeight: 500, marginBottom: 8 }}>
|
||||
{t('apiServer.documentation.unavailable.title')}
|
||||
</div>
|
||||
<div style={{ fontSize: 14 }}>{t('apiServer.documentation.unavailable.description')}</div>
|
||||
</div>
|
||||
<HeaderSection>
|
||||
<HeaderContent>
|
||||
<Title level={3} style={{ margin: 0, marginBottom: 8 }}>
|
||||
{t('apiServer.title')}
|
||||
</Title>
|
||||
<Text type="secondary">{t('apiServer.description')}</Text>
|
||||
</HeaderContent>
|
||||
{apiServerRunning && (
|
||||
<Button type="primary" icon={<ExternalLink size={14} />} onClick={openApiDocs}>
|
||||
{t('apiServer.documentation.title')}
|
||||
</Button>
|
||||
)}
|
||||
</ConfigCard>
|
||||
</SettingContainer>
|
||||
</HeaderSection>
|
||||
|
||||
{/* Server Control Panel with integrated configuration */}
|
||||
<ServerControlPanel $status={apiServerRunning}>
|
||||
<StatusSection>
|
||||
<StatusIndicator $status={apiServerRunning} />
|
||||
<StatusContent>
|
||||
<StatusText $status={apiServerRunning}>
|
||||
{apiServerRunning ? t('apiServer.status.running') : t('apiServer.status.stopped')}
|
||||
</StatusText>
|
||||
<StatusSubtext>
|
||||
{apiServerRunning ? `http://localhost:${apiServerConfig.port}` : t('apiServer.fields.port.description')}
|
||||
</StatusSubtext>
|
||||
</StatusContent>
|
||||
</StatusSection>
|
||||
|
||||
<ControlSection>
|
||||
{apiServerRunning && (
|
||||
<Tooltip title={t('apiServer.actions.restart.tooltip')}>
|
||||
<RestartButton
|
||||
$loading={apiServerLoading}
|
||||
onClick={apiServerLoading ? undefined : handleApiServerRestart}>
|
||||
<RotateCcw size={14} />
|
||||
<span>{t('apiServer.actions.restart.button')}</span>
|
||||
</RestartButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
{/* Port input when server is stopped */}
|
||||
{!apiServerRunning && (
|
||||
<StyledInputNumber
|
||||
value={apiServerConfig.port}
|
||||
onChange={(value) => handlePortChange(String(value || 23333))}
|
||||
min={1000}
|
||||
max={65535}
|
||||
disabled={apiServerRunning}
|
||||
placeholder="23333"
|
||||
size="middle"
|
||||
/>
|
||||
)}
|
||||
|
||||
<Tooltip title={apiServerRunning ? t('apiServer.actions.stop') : t('apiServer.actions.start')}>
|
||||
{apiServerRunning ? (
|
||||
<StopButton
|
||||
$loading={apiServerLoading}
|
||||
onClick={apiServerLoading ? undefined : () => handleApiServerToggle(false)}>
|
||||
<Square size={20} style={{ color: 'var(--color-status-error)' }} />
|
||||
</StopButton>
|
||||
) : (
|
||||
<StartButton
|
||||
$loading={apiServerLoading}
|
||||
onClick={apiServerLoading ? undefined : () => handleApiServerToggle(true)}>
|
||||
<Play size={20} style={{ color: 'var(--color-status-success)' }} />
|
||||
</StartButton>
|
||||
)}
|
||||
</Tooltip>
|
||||
</ControlSection>
|
||||
</ServerControlPanel>
|
||||
|
||||
{/* API Key Configuration */}
|
||||
<ConfigurationField>
|
||||
<FieldLabel>{t('apiServer.fields.apiKey.label')}</FieldLabel>
|
||||
<FieldDescription>{t('apiServer.fields.apiKey.description')}</FieldDescription>
|
||||
|
||||
<StyledInput
|
||||
value={apiServerConfig.apiKey}
|
||||
readOnly
|
||||
placeholder={t('apiServer.fields.apiKey.placeholder')}
|
||||
size="middle"
|
||||
suffix={
|
||||
<InputButtonContainer>
|
||||
{!apiServerRunning && (
|
||||
<RegenerateButton onClick={regenerateApiKey} disabled={apiServerRunning} type="link">
|
||||
{t('apiServer.actions.regenerate')}
|
||||
</RegenerateButton>
|
||||
)}
|
||||
<Tooltip title={t('apiServer.fields.apiKey.copyTooltip')}>
|
||||
<InputButton icon={<Copy size={14} />} onClick={copyApiKey} disabled={!apiServerConfig.apiKey} />
|
||||
</Tooltip>
|
||||
</InputButtonContainer>
|
||||
}
|
||||
/>
|
||||
|
||||
{/* Authorization header info */}
|
||||
<AuthHeaderSection>
|
||||
<FieldLabel>{t('apiServer.authHeader.title')}</FieldLabel>
|
||||
<StyledInput
|
||||
style={{ height: 38 }}
|
||||
value={`Authorization: Bearer ${apiServerConfig.apiKey || 'your-api-key'}`}
|
||||
readOnly
|
||||
size="middle"
|
||||
/>
|
||||
</AuthHeaderSection>
|
||||
</ConfigurationField>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
// Styled Components
|
||||
const Container = styled(SettingContainer)`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: calc(100vh - var(--navbar-height));
|
||||
`
|
||||
|
||||
const HeaderSection = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 24px;
|
||||
`
|
||||
|
||||
const HeaderContent = styled.div`
|
||||
flex: 1;
|
||||
`
|
||||
|
||||
const ServerControlPanel = styled.div<{ $status: boolean }>`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 16px 20px;
|
||||
border-radius: 8px;
|
||||
background: var(--color-background);
|
||||
border: 1px solid ${(props) => (props.$status ? 'var(--color-status-success)' : 'var(--color-border)')};
|
||||
transition: all 0.3s ease;
|
||||
margin-bottom: 16px;
|
||||
`
|
||||
|
||||
const StatusSection = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
`
|
||||
|
||||
const StatusIndicator = styled.div<{ $status: boolean }>`
|
||||
position: relative;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 50%;
|
||||
background: ${(props) => (props.$status ? 'var(--color-status-success)' : 'var(--color-status-error)')};
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: -3px;
|
||||
border-radius: 50%;
|
||||
background: ${(props) => (props.$status ? 'var(--color-status-success)' : 'var(--color-status-error)')};
|
||||
opacity: 0.2;
|
||||
animation: ${(props) => (props.$status ? 'pulse 2s infinite' : 'none')};
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%,
|
||||
100% {
|
||||
transform: scale(1);
|
||||
opacity: 0.2;
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.5);
|
||||
opacity: 0.1;
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const StatusContent = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
`
|
||||
|
||||
const StatusText = styled.div<{ $status: boolean }>`
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
color: ${(props) => (props.$status ? 'var(--color-status-success)' : 'var(--color-text-1)')};
|
||||
margin: 0;
|
||||
`
|
||||
|
||||
const StatusSubtext = styled.div`
|
||||
font-size: 12px;
|
||||
color: var(--color-text-3);
|
||||
margin: 0;
|
||||
`
|
||||
|
||||
const ControlSection = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
`
|
||||
|
||||
const RestartButton = styled.div<{ $loading: boolean }>`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
color: var(--color-text-2);
|
||||
cursor: ${(props) => (props.$loading ? 'not-allowed' : 'pointer')};
|
||||
opacity: ${(props) => (props.$loading ? 0.5 : 1)};
|
||||
font-size: 12px;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
color: ${(props) => (props.$loading ? 'var(--color-text-2)' : 'var(--color-primary)')};
|
||||
}
|
||||
`
|
||||
|
||||
const StyledInputNumber = styled(InputNumber)`
|
||||
width: 80px;
|
||||
border-radius: 6px;
|
||||
border: 1.5px solid var(--color-border);
|
||||
margin-right: 5px;
|
||||
`
|
||||
|
||||
const StartButton = styled.div<{ $loading: boolean }>`
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: ${(props) => (props.$loading ? 'not-allowed' : 'pointer')};
|
||||
opacity: ${(props) => (props.$loading ? 0.5 : 1)};
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
transform: ${(props) => (props.$loading ? 'scale(1)' : 'scale(1.1)')};
|
||||
}
|
||||
`
|
||||
|
||||
const StopButton = styled.div<{ $loading: boolean }>`
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: ${(props) => (props.$loading ? 'not-allowed' : 'pointer')};
|
||||
opacity: ${(props) => (props.$loading ? 0.5 : 1)};
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
transform: ${(props) => (props.$loading ? 'scale(1)' : 'scale(1.1)')};
|
||||
}
|
||||
`
|
||||
|
||||
const ConfigurationField = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
padding: 16px;
|
||||
background: var(--color-background);
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--color-border);
|
||||
`
|
||||
|
||||
const FieldLabel = styled.div`
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: var(--color-text-1);
|
||||
margin: 0;
|
||||
`
|
||||
|
||||
const FieldDescription = styled.div`
|
||||
font-size: 12px;
|
||||
color: var(--color-text-3);
|
||||
margin: 0;
|
||||
`
|
||||
|
||||
const StyledInput = styled(Input)`
|
||||
width: 100%;
|
||||
border-radius: 6px;
|
||||
border: 1.5px solid var(--color-border);
|
||||
`
|
||||
|
||||
const InputButtonContainer = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
`
|
||||
|
||||
const InputButton = styled(Button)`
|
||||
border: none;
|
||||
padding: 0 4px;
|
||||
background: transparent;
|
||||
`
|
||||
|
||||
const RegenerateButton = styled(Button)`
|
||||
padding: 0 4px;
|
||||
font-size: 12px;
|
||||
height: auto;
|
||||
line-height: 1;
|
||||
border: none;
|
||||
background: transparent;
|
||||
`
|
||||
|
||||
const AuthHeaderSection = styled.div`
|
||||
margin-top: 12px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
`
|
||||
|
||||
export default ApiServerSettings
|
||||
|
||||
@ -8,7 +8,6 @@ import {
|
||||
Info,
|
||||
MonitorCog,
|
||||
Package,
|
||||
PencilRuler,
|
||||
Rocket,
|
||||
Server,
|
||||
Settings2,
|
||||
@ -16,7 +15,6 @@ import {
|
||||
TextCursorInput,
|
||||
Zap
|
||||
} from 'lucide-react'
|
||||
// 导入useAppSelector
|
||||
import { FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Link, Route, Routes, useLocation } from 'react-router-dom'
|
||||
@ -91,12 +89,6 @@ const SettingsPage: FC = () => {
|
||||
{t('memory.title')}
|
||||
</MenuItem>
|
||||
</MenuItemLink>
|
||||
<MenuItemLink to="/settings/tool">
|
||||
<MenuItem className={isRoute('/settings/tool')}>
|
||||
<PencilRuler size={18} />
|
||||
{t('settings.tool.title')}
|
||||
</MenuItem>
|
||||
</MenuItemLink>
|
||||
<MenuItemLink to="/settings/shortcut">
|
||||
<MenuItem className={isRoute('/settings/shortcut')}>
|
||||
<Command size={18} />
|
||||
@ -219,7 +211,6 @@ const SettingContent = styled.div`
|
||||
display: flex;
|
||||
height: 100%;
|
||||
flex: 1;
|
||||
border-right: 0.5px solid var(--color-border);
|
||||
`
|
||||
|
||||
export default SettingsPage
|
||||
|
||||
Loading…
Reference in New Issue
Block a user