Files
meshtastic_mqtt_server/meshmap_frontend/src/components/AdminLLMApi.vue
T

1095 lines
32 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import { computed, onMounted, ref } from 'vue'
import {
createLLMProvider,
deleteLLMProvider,
getLLMProviders,
getLLMToolRouter,
getLLMTopicConfig,
getLLMPrimaryConfig,
updateLLMProvider,
updateLLMToolRouter,
updateLLMTopicConfig,
updateLLMPrimaryConfig,
getAIServiceStatus,
restartAIService,
} from '../api'
import type { LLMPlatformRouter, LLMTopicConfig, LLMProvider, LLMPrimaryConfig, AIServiceStatus } from '../types'
const loading = ref(false)
const error = ref('')
const success = ref('')
const showWarning = ref(false)
// AI Service Status
const aiStatus = ref<AIServiceStatus>({ running: false, enabled: false, provider_count: 0 })
const restarting = ref(false)
// LLM Provider 相关
const providers = ref<LLMProvider[]>([])
const editingProvider = ref<LLMProvider | null>(null)
const showProviderForm = ref(false)
const isCreatingProvider = ref(false)
const providerForm = ref({
name: '',
active: true,
api_key: '',
base_url: 'https://ark.cn-beijing.volces.com/api/v3',
model: '',
timeout: 120,
context_window_tokens: 262144,
})
// Tool Router 相关
const toolRouter = ref<LLMPlatformRouter | null>(null)
const editingToolRouter = ref(false)
const toolRouterForm = ref({
enabled: true,
openai_name: '',
timeout: 30,
max_tokens: 512,
system_prompt: '',
})
// Topic Config 相关 - 话题选择配置
const topicConfig = ref<LLMTopicConfig | null>(null)
const editingTopicConfig = ref(false)
const topicConfigForm = ref({
enabled: false,
openai_name: '',
timeout: 30,
max_tokens: 512,
system_prompt: '',
})
// Primary AI Config 相关 - 主 AI 回复配置
const primaryConfig = ref<LLMPrimaryConfig | null>(null)
const editingPrimaryConfig = ref(false)
const primaryConfigForm = ref({
enabled: false,
provider_name: '',
timeout: 120,
max_tokens: 1024,
system_prompt: '',
enable_tool: false,
})
const activeProviders = computed(() => providers.value.filter((p) => p.active))
function clearSuccess() {
setTimeout(() => {
success.value = ''
}, 3000)
}
async function loadProviders() {
loading.value = true
error.value = ''
try {
const response = await getLLMProviders(true)
providers.value = response.items
} catch (err) {
error.value = err instanceof Error ? err.message : String(err)
} finally {
loading.value = false
}
}
async function loadToolRouter() {
try {
const response = await getLLMToolRouter()
toolRouter.value = response.item
} catch (err) {
// 如果不存在,使用默认值
console.warn('Tool router config not found, using defaults')
}
}
async function loadPrimaryConfig() {
try {
const response = await getLLMPrimaryConfig()
primaryConfig.value = response.item
} catch (err) {
// 如果不存在,使用默认值
console.warn('Primary AI config not found, using defaults')
}
}
async function loadTopicConfig() {
try {
const response = await getLLMTopicConfig()
topicConfig.value = response.item
} catch (err) {
// 如果不存在,使用默认值
console.warn('Topic config not found, using defaults')
}
}
function openCreateProvider() {
isCreatingProvider.value = true
providerForm.value = {
name: '',
active: true,
api_key: '',
base_url: 'https://ark.cn-beijing.volces.com/api/v3',
model: '',
timeout: 120,
context_window_tokens: 262144,
}
showProviderForm.value = true
error.value = ''
}
function openEditProvider(provider: LLMProvider) {
isCreatingProvider.value = false
providerForm.value = {
name: provider.name,
active: provider.active,
api_key: provider.api_key || '',
base_url: provider.base_url,
model: provider.model,
timeout: provider.timeout,
context_window_tokens: provider.context_window_tokens,
}
editingProvider.value = provider
showProviderForm.value = true
error.value = ''
}
function closeProviderForm() {
showProviderForm.value = false
editingProvider.value = null
}
async function saveProvider() {
if (!providerForm.value.name.trim()) {
error.value = '请输入配置名称'
return
}
if (!providerForm.value.base_url.trim()) {
error.value = '请输入 API 地址'
return
}
try {
let response: any
if (isCreatingProvider.value) {
response = await createLLMProvider(providerForm.value)
} else if (editingProvider.value) {
response = await updateLLMProvider(editingProvider.value.name, providerForm.value)
}
if (response.warning) {
success.value = response.warning
showWarning.value = true
} else {
success.value = isCreatingProvider.value ? '创建成功' : '更新成功'
showWarning.value = false
}
clearSuccess()
closeProviderForm()
await loadProviders()
} catch (err) {
error.value = err instanceof Error ? err.message : String(err)
}
}
async function confirmDeleteProvider(name: string) {
if (!confirm(`确定要删除配置 "${name}" 吗?此操作不可撤销。`)) {
return
}
try {
const response = await deleteLLMProvider(name)
if (response.warning) {
success.value = response.warning
showWarning.value = true
} else {
success.value = '删除成功'
showWarning.value = false
}
clearSuccess()
await loadProviders()
} catch (err) {
error.value = err instanceof Error ? err.message : String(err)
}
}
function openEditToolRouter() {
if (toolRouter.value) {
toolRouterForm.value = {
enabled: toolRouter.value.enabled,
openai_name: toolRouter.value.openai_name,
timeout: toolRouter.value.timeout,
max_tokens: toolRouter.value.max_tokens,
system_prompt: toolRouter.value.system_prompt,
}
}
editingToolRouter.value = true
}
function closeToolRouterForm() {
editingToolRouter.value = false
}
async function saveToolRouter() {
try {
await updateLLMToolRouter(toolRouterForm.value)
success.value = '更新成功'
clearSuccess()
closeToolRouterForm()
await loadToolRouter()
} catch (err) {
error.value = err instanceof Error ? err.message : String(err)
}
}
function openEditTopicConfig() {
if (topicConfig.value) {
topicConfigForm.value = {
enabled: topicConfig.value.enabled,
openai_name: topicConfig.value.openai_name,
timeout: topicConfig.value.timeout,
max_tokens: topicConfig.value.max_tokens,
system_prompt: topicConfig.value.system_prompt,
}
}
editingTopicConfig.value = true
}
function closeTopicConfigForm() {
editingTopicConfig.value = false
}
async function saveTopicConfig() {
try {
await updateLLMTopicConfig(topicConfigForm.value)
success.value = '更新成功'
clearSuccess()
closeTopicConfigForm()
await loadTopicConfig()
} catch (err) {
error.value = err instanceof Error ? err.message : String(err)
}
}
function openEditPrimaryConfig() {
if (primaryConfig.value) {
primaryConfigForm.value = {
enabled: primaryConfig.value.enabled,
provider_name: primaryConfig.value.provider_name,
timeout: primaryConfig.value.timeout,
max_tokens: primaryConfig.value.max_tokens,
system_prompt: primaryConfig.value.system_prompt,
enable_tool: primaryConfig.value.enable_tool,
}
}
editingPrimaryConfig.value = true
}
function closePrimaryConfigForm() {
editingPrimaryConfig.value = false
}
async function savePrimaryConfig() {
try {
await updateLLMPrimaryConfig(primaryConfigForm.value)
success.value = '更新成功'
clearSuccess()
closePrimaryConfigForm()
await loadPrimaryConfig()
} catch (err) {
error.value = err instanceof Error ? err.message : String(err)
}
}
async function loadAIStatus() {
try {
aiStatus.value = await getAIServiceStatus()
} catch (err) {
console.warn('Failed to load AI service status', err)
}
}
async function handleRestartAI() {
if (!confirm('确定要重启 AI 服务吗?')) return
restarting.value = true
try {
const result = await restartAIService()
aiStatus.value = result
success.value = result.message || 'AI 服务已重启'
clearSuccess()
} catch (err) {
error.value = err instanceof Error ? err.message : String(err)
} finally {
restarting.value = false
}
}
onMounted(() => {
loadAIStatus()
loadProviders()
loadToolRouter()
loadTopicConfig()
loadPrimaryConfig()
})
</script>
<template>
<section class="admin-dashboard">
<div class="panel">
<div class="ai-status-bar">
<span class="status-indicator" :class="{ running: aiStatus.running, stopped: !aiStatus.running }"></span>
<span class="status-text">
<template v-if="aiStatus.running">AI 服务运行中{{ aiStatus.provider_count }} 个提供商</template>
<template v-else>{{ aiStatus.message || 'AI 服务未运行' }}</template>
</span>
<button
class="admin-button"
:disabled="restarting"
@click="handleRestartAI"
>
{{ restarting ? '重启中...' : '重启 AI 服务' }}
</button>
</div>
<p v-if="error" class="error">{{ error }}</p>
<p v-if="success" :class="showWarning ? 'warning' : 'success'">{{ success }}</p>
</div>
<!-- LLM Provider 列表 -->
<div class="panel">
<div class="panel-header">
<div>
<p class="eyebrow">Providers</p>
<h2>AI 提供商配置</h2>
</div>
<button class="admin-button" @click="openCreateProvider">+ 添加配置</button>
</div>
<p class="section-desc">管理多个 LLM API 提供商配置支持不同模型和服务</p>
<div class="section-body">
<div v-if="loading" class="empty">加载中...</div>
<div v-else class="provider-grid">
<div v-for="provider in providers" :key="provider.name" class="provider-card">
<div class="provider-header">
<div class="provider-name">
<span class="status-badge" :class="{ active: provider.active, inactive: !provider.active }">
{{ provider.active ? '启用' : '停用' }}
</span>
<strong>{{ provider.name }}</strong>
</div>
<div class="provider-actions">
<button class="admin-button" @click="openEditProvider(provider)">编辑</button>
<button class="admin-button danger" @click="confirmDeleteProvider(provider.name)">删除</button>
</div>
</div>
<div class="provider-details">
<div class="detail-row">
<span class="detail-label">API 地址</span>
<span class="detail-value">{{ provider.base_url }}</span>
</div>
<div class="detail-row">
<span class="detail-label">模型</span>
<span class="detail-value">{{ provider.model || '-' }}</span>
</div>
<div class="detail-row">
<span class="detail-label">超时</span>
<span class="detail-value">{{ provider.timeout }} </span>
</div>
<div class="detail-row">
<span class="detail-label">上下文窗口</span>
<span class="detail-value">{{ provider.context_window_tokens.toLocaleString() }} tokens</span>
</div>
<div class="detail-row">
<span class="detail-label">API Key</span>
<span class="detail-value masked">{{ provider.api_key ? '••••••••••' : '-' }}</span>
</div>
</div>
</div>
<div v-if="providers.length === 0" class="empty">暂无配置点击上方按钮添加第一个 AI 提供商配置</div>
</div>
</div>
</div>
<!-- Tool Router 配置 -->
<div class="panel">
<div class="panel-header">
<div>
<p class="eyebrow">Tool Router</p>
<h2>工具路由配置</h2>
</div>
<button v-if="!editingToolRouter" class="admin-button" @click="openEditToolRouter">编辑配置</button>
</div>
<p class="section-desc">配置 LLM 工具调用的路由设置用于实现函数调用功能</p>
<div class="section-body">
<div v-if="editingToolRouter" class="tool-router-form">
<div class="form-group">
<label>
<input type="checkbox" v-model="toolRouterForm.enabled" />
启用工具路由
</label>
</div>
<div class="form-row">
<div class="form-group">
<label>使用的 AI 配置</label>
<select v-model="toolRouterForm.openai_name" class="form-input">
<option value="">请选择</option>
<option v-for="p in activeProviders" :key="p.name" :value="p.name">{{ p.name }}</option>
</select>
<p class="form-hint">选择用于工具调用的 AI 提供商配置</p>
</div>
</div>
<div class="form-row">
<div class="form-group">
<label>超时时间</label>
<input type="number" v-model.number="toolRouterForm.timeout" class="form-input" min="1" />
</div>
<div class="form-group">
<label>最大 Token </label>
<input type="number" v-model.number="toolRouterForm.max_tokens" class="form-input" min="1" />
</div>
</div>
<div class="form-group">
<label>系统提示词</label>
<textarea v-model="toolRouterForm.system_prompt" class="form-textarea" rows="6"></textarea>
<p class="form-hint">用于指导模型如何使用工具的系统提示词</p>
</div>
<div class="form-actions">
<button class="admin-button ghost" @click="closeToolRouterForm">取消</button>
<button class="admin-button" @click="saveToolRouter">保存</button>
</div>
</div>
<div v-else-if="toolRouter" class="tool-router-display">
<div class="router-status">
<span class="status-badge" :class="{ active: toolRouter.enabled, inactive: !toolRouter.enabled }">
{{ toolRouter.enabled ? '已启用' : '已停用' }}
</span>
</div>
<div class="router-details">
<div class="detail-row">
<span class="detail-label">使用的 AI 配置</span>
<span class="detail-value">{{ toolRouter.openai_name || '未设置' }}</span>
</div>
<div class="detail-row">
<span class="detail-label">超时时间</span>
<span class="detail-value">{{ toolRouter.timeout }} </span>
</div>
<div class="detail-row">
<span class="detail-label">最大 Token </span>
<span class="detail-value">{{ toolRouter.max_tokens }}</span>
</div>
<div class="detail-row full-width">
<span class="detail-label">系统提示词</span>
<pre class="detail-value system-prompt">{{ toolRouter.system_prompt }}</pre>
</div>
</div>
</div>
<div v-else class="empty">暂无工具路由配置点击上方按钮进行配置</div>
</div>
</div>
<!-- Topic Config 配置 - 话题选择配置 -->
<div class="panel">
<div class="panel-header">
<div>
<p class="eyebrow">Topic Filter</p>
<h2>话题选择配置</h2>
</div>
<button v-if="!editingTopicConfig" class="admin-button" @click="openEditTopicConfig">编辑配置</button>
</div>
<p class="section-desc">当工具路由未命中任何工具时由话题选择判断是否回复命中输出 REPLY才进入主回复否则丢弃不回复</p>
<div class="section-body">
<div v-if="editingTopicConfig" class="tool-router-form">
<div class="form-group">
<label>
<input type="checkbox" v-model="topicConfigForm.enabled" />
启用话题选择
</label>
<p class="form-hint">未启用时未命中工具的消息一律进入主回复不做过滤</p>
</div>
<div class="form-row">
<div class="form-group">
<label>使用的 AI 配置</label>
<select v-model="topicConfigForm.openai_name" class="form-input">
<option value="">请选择</option>
<option v-for="p in activeProviders" :key="p.name" :value="p.name">{{ p.name }}</option>
</select>
<p class="form-hint">选择用于话题判定的 AI 提供商配置留空则使用主回复配置</p>
</div>
</div>
<div class="form-row">
<div class="form-group">
<label>超时时间</label>
<input type="number" v-model.number="topicConfigForm.timeout" class="form-input" min="1" />
</div>
<div class="form-group">
<label>最大 Token </label>
<input type="number" v-model.number="topicConfigForm.max_tokens" class="form-input" min="1" />
</div>
</div>
<div class="form-group">
<label>系统提示词</label>
<textarea v-model="topicConfigForm.system_prompt" class="form-textarea" rows="6"></textarea>
<p class="form-hint">要求模型对应当回复的消息输出 REPLY否则输出 IGNORE</p>
</div>
<div class="form-actions">
<button class="admin-button ghost" @click="closeTopicConfigForm">取消</button>
<button class="admin-button" @click="saveTopicConfig">保存</button>
</div>
</div>
<div v-else-if="topicConfig" class="tool-router-display">
<div class="router-status">
<span class="status-badge" :class="{ active: topicConfig.enabled, inactive: !topicConfig.enabled }">
{{ topicConfig.enabled ? '已启用' : '已停用' }}
</span>
</div>
<div class="router-details">
<div class="detail-row">
<span class="detail-label">使用的 AI 配置</span>
<span class="detail-value">{{ topicConfig.openai_name || '未设置(使用主回复配置)' }}</span>
</div>
<div class="detail-row">
<span class="detail-label">超时时间</span>
<span class="detail-value">{{ topicConfig.timeout }} </span>
</div>
<div class="detail-row">
<span class="detail-label">最大 Token </span>
<span class="detail-value">{{ topicConfig.max_tokens }}</span>
</div>
<div class="detail-row full-width">
<span class="detail-label">系统提示词</span>
<pre class="detail-value system-prompt">{{ topicConfig.system_prompt }}</pre>
</div>
</div>
</div>
<div v-else class="empty">暂无话题选择配置点击上方按钮进行配置</div>
</div>
</div>
<!-- Primary AI Config 配置 - AI 回复配置 -->
<div class="panel">
<div class="panel-header">
<div>
<p class="eyebrow">Primary AI</p>
<h2> AI 回复配置</h2>
</div>
<button v-if="!editingPrimaryConfig" class="admin-button" @click="openEditPrimaryConfig">编辑配置</button>
</div>
<p class="section-desc">配置机器人自动回复消息的核心 AI 设置</p>
<div class="section-body">
<div v-if="editingPrimaryConfig" class="tool-router-form">
<div class="form-group">
<label>
<input type="checkbox" v-model="primaryConfigForm.enabled" />
启用 AI 自动回复
</label>
</div>
<div class="form-row">
<div class="form-group">
<label>使用的 AI 配置</label>
<select v-model="primaryConfigForm.provider_name" class="form-input">
<option value="">请选择</option>
<option v-for="p in activeProviders" :key="p.name" :value="p.name">{{ p.name }}</option>
</select>
<p class="form-hint">选择用于自动回复消息的 AI 提供商配置</p>
</div>
<div class="form-group">
<label>是否启用工具调用</label>
<select v-model="primaryConfigForm.enable_tool" class="form-input">
<option :value="false">不启用</option>
<option :value="true">启用</option>
</select>
<p class="form-hint">选择是否让 AI 在回复中调用工具如计算器等</p>
</div>
</div>
<div class="form-row">
<div class="form-group">
<label>超时时间</label>
<input type="number" v-model.number="primaryConfigForm.timeout" class="form-input" min="1" />
</div>
<div class="form-group">
<label>最大 Token </label>
<input type="number" v-model.number="primaryConfigForm.max_tokens" class="form-input" min="1" />
</div>
</div>
<div class="form-group">
<label>系统提示词</label>
<textarea v-model="primaryConfigForm.system_prompt" class="form-textarea" rows="6"></textarea>
<p class="form-hint">用于指导 AI 如何回复用户消息的系统提示词</p>
</div>
<div class="form-actions">
<button class="admin-button ghost" @click="closePrimaryConfigForm">取消</button>
<button class="admin-button" @click="savePrimaryConfig">保存</button>
</div>
</div>
<div v-else-if="primaryConfig" class="tool-router-display">
<div class="router-status">
<span class="status-badge" :class="{ active: primaryConfig.enabled, inactive: !primaryConfig.enabled }">
{{ primaryConfig.enabled ? '已启用' : '已停用' }}
</span>
</div>
<div class="router-details">
<div class="detail-row">
<span class="detail-label">使用的 AI 配置</span>
<span class="detail-value">{{ primaryConfig.provider_name || '未设置' }}</span>
</div>
<div class="detail-row">
<span class="detail-label">超时时间</span>
<span class="detail-value">{{ primaryConfig.timeout }} </span>
</div>
<div class="detail-row">
<span class="detail-label">最大 Token </span>
<span class="detail-value">{{ primaryConfig.max_tokens }}</span>
</div>
<div class="detail-row">
<span class="detail-label">工具调用</span>
<span class="detail-value">{{ primaryConfig.enable_tool ? '已启用' : '未启用' }}</span>
</div>
<div class="detail-row full-width">
<span class="detail-label">系统提示词</span>
<pre class="detail-value system-prompt">{{ primaryConfig.system_prompt }}</pre>
</div>
</div>
</div>
<div v-else class="empty">暂无主 AI 回复配置点击上方按钮进行配置</div>
</div>
</div>
<!-- Provider 表单弹窗 -->
<div v-if="showProviderForm" class="modal-overlay" @click.self="closeProviderForm">
<div class="modal-content">
<div class="modal-header">
<h3>{{ isCreatingProvider ? '添加 AI 提供商配置' : '编辑 AI 提供商配置' }}</h3>
<button class="modal-close" @click="closeProviderForm">×</button>
</div>
<div class="modal-body">
<div class="form-group">
<label>配置名称 *</label>
<input
type="text"
v-model="providerForm.name"
class="form-input"
:disabled="!isCreatingProvider"
placeholder="例如:volcengine-ark"
/>
<p class="form-hint">唯一标识此配置的名称创建后不可修改</p>
</div>
<div class="form-group">
<label>
<input type="checkbox" v-model="providerForm.active" />
启用此配置
</label>
</div>
<div class="form-group">
<label>API 地址 *</label>
<input type="text" v-model="providerForm.base_url" class="form-input" placeholder="https://api.example.com/v1" />
</div>
<div class="form-group">
<label>API Key *</label>
<input type="password" v-model="providerForm.api_key" class="form-input" placeholder="sk-..." />
</div>
<div class="form-group">
<label>模型名称</label>
<input type="text" v-model="providerForm.model" class="form-input" placeholder="例如:doubao-pro-32k" />
</div>
<div class="form-row">
<div class="form-group">
<label>超时时间</label>
<input type="number" v-model.number="providerForm.timeout" class="form-input" min="1" />
</div>
<div class="form-group">
<label>上下文窗口tokens</label>
<input type="number" v-model.number="providerForm.context_window_tokens" class="form-input" min="1" />
</div>
</div>
</div>
<div class="modal-footer">
<button class="admin-button ghost" @click="closeProviderForm">取消</button>
<button class="admin-button" @click="saveProvider">保存</button>
</div>
</div>
</div>
</section>
</template>
<style scoped>
.ai-status-bar {
display: flex;
align-items: center;
gap: 12px;
padding: 14px 16px;
}
.status-indicator {
width: 10px;
height: 10px;
border-radius: 50%;
flex-shrink: 0;
}
.status-indicator.running {
background: var(--color-success);
box-shadow: 0 0 6px color-mix(in srgb, var(--color-success) 60%, transparent);
}
.status-indicator.stopped {
background: var(--color-danger);
box-shadow: 0 0 6px color-mix(in srgb, var(--color-danger) 50%, transparent);
}
.status-text {
flex: 1;
color: var(--color-text);
font-size: 14px;
font-weight: 700;
}
.section-desc {
margin: 0;
padding: 12px 16px 0;
color: var(--color-muted);
font-size: 13px;
line-height: 1.6;
}
.section-body {
padding: 16px;
}
.provider-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(340px, 1fr));
gap: 12px;
}
.provider-card {
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
padding: 14px;
background: var(--color-surface-soft);
transition: border-color 0.16s ease, box-shadow 0.16s ease, transform 0.16s ease;
}
.provider-card:hover {
border-color: var(--color-primary);
box-shadow: var(--shadow-sm);
transform: translateY(-1px);
}
.provider-header {
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 8px;
margin-bottom: 10px;
padding-bottom: 10px;
border-bottom: 1px solid var(--color-border);
}
.provider-name {
display: flex;
align-items: center;
gap: 8px;
min-width: 0;
}
.provider-name strong {
color: var(--color-heading);
font-size: 15px;
overflow-wrap: anywhere;
}
.provider-actions {
display: flex;
gap: 8px;
}
.provider-details {
display: flex;
flex-direction: column;
gap: 6px;
}
.detail-row {
display: flex;
justify-content: space-between;
align-items: baseline;
gap: 12px;
}
.detail-row.full-width {
flex-direction: column;
align-items: flex-start;
}
.detail-label {
flex-shrink: 0;
color: var(--color-muted);
font-size: 12px;
font-weight: 700;
}
.detail-value {
color: var(--color-heading);
font-size: 13px;
word-break: break-all;
text-align: right;
}
.detail-value.masked {
font-family: var(--font-mono);
letter-spacing: 0.1em;
}
.status-badge {
display: inline-flex;
align-items: center;
border-radius: 999px;
padding: 4px 10px;
font-size: 12px;
font-weight: 700;
white-space: nowrap;
}
.status-badge.active {
color: color-mix(in srgb, var(--color-success) 70%, var(--color-heading));
background: var(--color-success-soft);
}
.status-badge.inactive {
color: color-mix(in srgb, var(--color-danger) 76%, var(--color-heading));
background: var(--color-danger-soft);
}
.tool-router-form,
.tool-router-display {
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
padding: 16px;
background: var(--color-surface-soft);
}
.router-status {
margin-bottom: 12px;
padding-bottom: 12px;
border-bottom: 1px solid var(--color-border);
}
.router-details {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 12px;
}
.router-details .detail-row {
flex-direction: column;
align-items: flex-start;
gap: 4px;
}
.router-details .detail-row .detail-value {
text-align: left;
}
.system-prompt {
width: 100%;
margin: 0;
white-space: pre-wrap;
word-break: break-word;
border: 1px solid var(--color-border);
border-radius: var(--radius-sm);
padding: 10px 12px;
color: var(--color-text);
background: var(--color-surface);
font-size: 13px;
line-height: 1.6;
}
.form-group {
margin-bottom: 14px;
}
.form-group label {
display: block;
margin-bottom: 6px;
color: var(--color-text);
font-size: 13px;
font-weight: 700;
}
.form-group label input[type='checkbox'] {
margin-right: 8px;
width: auto;
accent-color: var(--color-primary);
}
.form-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
}
.form-input,
.form-textarea {
width: 100%;
box-sizing: border-box;
border: 1px solid var(--color-border-strong);
border-radius: var(--radius-sm);
padding: 9px 12px;
color: var(--color-heading);
background: var(--color-surface);
font-size: 14px;
outline: none;
transition: border-color 0.16s ease, box-shadow 0.16s ease;
}
.form-input:focus,
.form-textarea:focus {
border-color: var(--color-primary);
box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-primary) 20%, transparent);
}
.form-input:disabled {
background: var(--color-surface-muted);
cursor: not-allowed;
}
.form-textarea {
resize: vertical;
min-height: 100px;
font-family: inherit;
}
.form-hint {
margin: 6px 0 0;
color: var(--color-muted);
font-size: 12px;
}
.form-actions {
display: flex;
justify-content: flex-end;
gap: 10px;
margin-top: 12px;
padding-top: 12px;
border-top: 1px solid var(--color-border);
}
.warning {
margin: 0 16px 12px;
border: 1px solid color-mix(in srgb, var(--color-warning) 36%, white);
border-radius: var(--radius-md);
padding: 10px 12px;
color: color-mix(in srgb, var(--color-warning) 72%, var(--color-heading));
background: var(--color-warning-soft);
}
/* 弹窗样式 */
.modal-overlay {
position: fixed;
inset: 0;
z-index: 1000;
display: flex;
align-items: center;
justify-content: center;
padding: 16px;
background: rgba(36, 41, 39, 0.45);
}
.modal-content {
width: 100%;
max-width: 550px;
max-height: 90vh;
overflow-y: auto;
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
background: var(--color-surface);
box-shadow: var(--shadow-floating);
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 14px 16px;
border-bottom: 1px solid var(--color-border);
background: color-mix(in srgb, var(--color-surface) 92%, var(--color-bg));
}
.modal-header h3 {
margin: 0;
color: var(--color-heading);
font-size: 16px;
}
.modal-close {
border: none;
border-radius: var(--radius-sm);
padding: 4px 8px;
color: var(--color-muted);
background: none;
font-size: 20px;
line-height: 1;
cursor: pointer;
transition: background-color 0.16s ease, color 0.16s ease;
}
.modal-close:hover {
color: var(--color-heading);
background: var(--color-surface-muted);
}
.modal-body {
padding: 16px;
}
.modal-footer {
display: flex;
justify-content: flex-end;
gap: 10px;
padding: 12px 16px 16px;
}
@media (max-width: 640px) {
.form-row {
grid-template-columns: 1fr;
}
.provider-grid {
grid-template-columns: 1fr;
}
}
</style>