Files
chatroom/resources/views/admin/ai-providers/index.blade.php

421 lines
21 KiB
PHP
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.
{{--
文件功能AI 厂商配置管理页面
提供 AI 厂商的完整 CRUD 管理:
- 列表展示所有配置(名称、模型、状态等)
- 新增/编辑厂商配置弹窗
- 启用/禁用切换、设为默认、删除
- 全局开关控制聊天机器人是否启用
@author ChatRoom Laravel
@version 1.0.0
--}}
@extends('admin.layouts.app')
@section('title', 'AI 厂商配置')
@section('content')
<div x-data="{
showForm: false,
editId: null,
form: {
provider: '',
name: '',
api_key: '',
api_endpoint: '',
model: '',
temperature: 0.3,
max_tokens: 2048,
sort_order: 0,
},
/**
* 打开新增弹窗,重置表单
*/
openNew() {
this.editId = null;
this.form = {
provider: '',
name: '',
api_key: '',
api_endpoint: '',
model: '',
temperature: 0.3,
max_tokens: 2048,
sort_order: 0,
};
this.showForm = true;
},
/**
* 打开编辑弹窗,填充现有数据
*/
openEdit(provider) {
this.editId = provider.id;
this.form = {
provider: provider.provider,
name: provider.name,
api_key: '', // 编辑时不回填 API Key已加密
api_endpoint: provider.api_endpoint,
model: provider.model,
temperature: provider.temperature,
max_tokens: provider.max_tokens,
sort_order: provider.sort_order,
};
this.showForm = true;
},
}">
{{-- 全局开关 + 高级设置 --}}
<div class="bg-white rounded-xl shadow-sm border border-gray-100 overflow-hidden mb-6">
<div class="p-6">
<!-- 顶部栏:开关和添加按钮 -->
<div class="flex items-center justify-between border-b border-gray-100 pb-4 mb-4">
<div class="flex items-center gap-4">
<h2 class="text-lg font-bold text-gray-800">🤖 AI 聊天机器人配置</h2>
<div class="flex items-center gap-2">
<span class="text-sm text-gray-500">大厅状态:</span>
<button id="chatbot-toggle-btn" onclick="toggleChatBot()"
class="relative inline-flex h-6 w-11 items-center rounded-full transition-colors {{ $chatbotEnabled ? 'bg-emerald-500' : 'bg-gray-300' }}">
<span
class="inline-block h-4 w-4 transform rounded-full bg-white transition-transform {{ $chatbotEnabled ? 'translate-x-6' : 'translate-x-1' }}"></span>
</button>
<span id="chatbot-status-text"
class="text-sm font-bold {{ $chatbotEnabled ? 'text-emerald-600' : 'text-gray-400' }}">
{{ $chatbotEnabled ? '已开启' : '已关闭' }}
</span>
</div>
</div>
<button x-on:click="openNew()"
class="px-4 py-2 bg-indigo-600 text-white rounded-md hover:bg-indigo-700 transition font-bold text-sm">
+ 添加 AI 厂商
</button>
</div>
<!-- 交互参数表单 -->
<form action="{{ route('admin.ai-providers.update-settings') }}" method="POST" class="flex flex-wrap items-end gap-6">
@csrf
<div>
<label class="block text-sm font-bold text-gray-700 mb-1">系统打赏单次最高金币(上限)</label>
<div class="relative">
<input type="number" name="chatbot_max_gold" value="{{ $chatbotMaxGold }}" min="1" required
class="w-64 border border-gray-300 rounded-md p-2 pl-3 pr-8 text-sm focus:ring-indigo-500 focus:border-indigo-500">
<span class="absolute right-3 top-2.5 text-gray-400 text-sm"></span>
</div>
</div>
<div>
<label class="block text-sm font-bold text-gray-700 mb-1">每人每日最高获取红包次数限制</label>
<div class="relative">
<input type="number" name="chatbot_max_daily_rewards" value="{{ $chatbotMaxDailyRewards }}" min="1" required
class="w-64 border border-gray-300 rounded-md p-2 pl-3 pr-8 text-sm focus:ring-indigo-500 focus:border-indigo-500">
<span class="absolute right-3 top-2.5 text-gray-400 text-sm"></span>
</div>
</div>
<button type="submit"
class="px-6 py-2 bg-slate-800 text-white rounded-md font-bold hover:bg-slate-900 text-sm transition h-[38px]">
保存运行参数
</button>
</form>
</div>
</div>
{{-- 厂商列表 --}}
<div class="bg-white rounded-xl shadow-sm border border-gray-100 overflow-hidden">
<table class="w-full text-sm">
<thead class="bg-gray-50 text-gray-600">
<tr>
<th class="px-6 py-3 text-left font-bold">厂商</th>
<th class="px-6 py-3 text-left font-bold">模型</th>
<th class="px-6 py-3 text-left font-bold">API 端点</th>
<th class="px-6 py-3 text-center font-bold">参数</th>
<th class="px-6 py-3 text-center font-bold">排序</th>
<th class="px-6 py-3 text-center font-bold">状态</th>
<th class="px-6 py-3 text-center font-bold">默认</th>
<th class="px-6 py-3 text-center font-bold">操作</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-100">
@forelse ($providers as $provider)
<tr class="hover:bg-gray-50 transition {{ !$provider->is_enabled ? 'opacity-50' : '' }}">
<td class="px-6 py-4">
<div class="font-bold text-gray-800">{{ $provider->name }}</div>
<div class="text-xs text-gray-400">{{ $provider->provider }}</div>
</td>
<td class="px-6 py-4">
<code class="bg-gray-100 px-2 py-1 rounded text-xs">{{ $provider->model }}</code>
</td>
<td class="px-6 py-4">
<span class="text-xs text-gray-500 truncate block max-w-[200px]"
title="{{ $provider->api_endpoint }}">
{{ $provider->api_endpoint }}
</span>
</td>
<td class="px-6 py-4 text-center text-xs text-gray-500">
T={{ $provider->temperature }} / {{ $provider->max_tokens }}
</td>
<td class="px-6 py-4 text-center text-gray-500">
{{ $provider->sort_order }}
</td>
<td class="px-6 py-4 text-center">
<button onclick="toggleProvider({{ $provider->id }}, this)"
class="px-3 py-1 rounded-full text-xs font-bold {{ $provider->is_enabled ? 'bg-emerald-100 text-emerald-700' : 'bg-gray-100 text-gray-500' }}">
{{ $provider->is_enabled ? '已启用' : '已禁用' }}
</button>
</td>
<td class="px-6 py-4 text-center">
@if ($provider->is_default)
<span class="px-3 py-1 rounded-full text-xs font-bold bg-amber-100 text-amber-700">
默认</span>
@else
<button onclick="setDefault({{ $provider->id }})"
class="px-3 py-1 rounded-full text-xs text-gray-400 hover:text-amber-600 hover:bg-amber-50 transition">
设为默认
</button>
@endif
</td>
<td class="px-6 py-4 text-center">
<div class="flex items-center justify-center gap-2">
<button
onclick="testConnection({{ $provider->id }}, '{{ addslashes($provider->name) }}')"
class="text-teal-600 hover:text-teal-800 text-xs font-bold"> 测试</button>
<button x-on:click="openEdit({{ $provider->toJson() }})"
class="text-indigo-600 hover:text-indigo-800 text-xs font-bold">编辑</button>
<form action="{{ route('admin.ai-providers.destroy', $provider->id) }}" method="POST"
onsubmit="return confirm('确定要删除 {{ $provider->name }} 吗?')">
@csrf
@method('DELETE')
<button type="submit"
class="text-red-500 hover:text-red-700 text-xs font-bold">删除</button>
</form>
</div>
</td>
</tr>
@empty
<tr>
<td colspan="8" class="px-6 py-12 text-center text-gray-400">
暂无 AI 厂商配置,请点击上方"添加 AI 厂商"按钮。
</td>
</tr>
@endforelse
</tbody>
</table>
</div>
{{-- 新增/编辑弹窗 --}}
<div x-show="showForm" x-cloak style="display: none;"
class="fixed inset-0 bg-black/50 flex items-center justify-center z-50" x-on:click.self="showForm = false">
<div class="bg-white rounded-xl shadow-xl w-[560px] max-h-[90vh] overflow-y-auto" x-transition>
<div class="p-6 border-b border-gray-100 flex justify-between items-center">
<h3 class="text-lg font-bold" x-text="editId ? '编辑 AI 厂商' : '添加 AI 厂商'"></h3>
<button x-on:click="showForm = false" class="text-gray-400 hover:text-gray-600 text-xl">&times;</button>
</div>
<form
:action="editId ? '{{ url('admin/ai-providers') }}/' + editId : '{{ route('admin.ai-providers.store') }}'"
method="POST" class="p-6 space-y-4">
@csrf
<template x-if="editId">
<input type="hidden" name="_method" value="PUT">
</template>
<div class="grid grid-cols-2 gap-4">
{{-- 厂商标识 --}}
<div>
<label class="block text-sm font-bold text-gray-700 mb-1">厂商标识 <span
class="text-red-500">*</span></label>
<input type="text" name="provider" x-model="form.provider" required
placeholder="deepseek, qwen, openai"
class="w-full border border-gray-300 rounded-md p-2 text-sm focus:ring-indigo-500 focus:border-indigo-500">
</div>
{{-- 显示名称 --}}
<div>
<label class="block text-sm font-bold text-gray-700 mb-1">显示名称 <span
class="text-red-500">*</span></label>
<input type="text" name="name" x-model="form.name" required placeholder="DeepSeek、通义千问"
class="w-full border border-gray-300 rounded-md p-2 text-sm focus:ring-indigo-500 focus:border-indigo-500">
</div>
</div>
{{-- API Key --}}
<div>
<label class="block text-sm font-bold text-gray-700 mb-1">
API Key
<span x-show="!editId" class="text-red-500">*</span>
<span x-show="editId" class="text-gray-400 font-normal">(留空表示不修改)</span>
</label>
<input type="password" name="api_key" x-model="form.api_key" :required="!editId"
placeholder="sk-xxxxxxxxxxxx"
class="w-full border border-gray-300 rounded-md p-2 text-sm focus:ring-indigo-500 focus:border-indigo-500 font-mono">
</div>
{{-- API 端点 --}}
<div>
<label class="block text-sm font-bold text-gray-700 mb-1">API 端点 <span
class="text-red-500">*</span></label>
<input type="url" name="api_endpoint" x-model="form.api_endpoint" required
placeholder="https://api.deepseek.com"
class="w-full border border-gray-300 rounded-md p-2 text-sm focus:ring-indigo-500 focus:border-indigo-500">
<p class="text-xs text-gray-400 mt-1">系统会自动拼接 /v1/chat/completions</p>
</div>
{{-- 模型 --}}
<div>
<label class="block text-sm font-bold text-gray-700 mb-1">模型名称 <span
class="text-red-500">*</span></label>
<input type="text" name="model" x-model="form.model" required
placeholder="deepseek-chat, qwen-turbo"
class="w-full border border-gray-300 rounded-md p-2 text-sm focus:ring-indigo-500 focus:border-indigo-500">
</div>
<div class="grid grid-cols-3 gap-4">
{{-- Temperature --}}
<div>
<label class="block text-sm font-bold text-gray-700 mb-1">Temperature</label>
<input type="number" name="temperature" x-model="form.temperature" step="0.1"
min="0" max="2"
class="w-full border border-gray-300 rounded-md p-2 text-sm focus:ring-indigo-500 focus:border-indigo-500">
</div>
{{-- Max Tokens --}}
<div>
<label class="block text-sm font-bold text-gray-700 mb-1">最大 Tokens</label>
<input type="number" name="max_tokens" x-model="form.max_tokens" min="100"
max="32000"
class="w-full border border-gray-300 rounded-md p-2 text-sm focus:ring-indigo-500 focus:border-indigo-500">
</div>
{{-- Sort Order --}}
<div>
<label class="block text-sm font-bold text-gray-700 mb-1">排序</label>
<input type="number" name="sort_order" x-model="form.sort_order" min="0"
class="w-full border border-gray-300 rounded-md p-2 text-sm focus:ring-indigo-500 focus:border-indigo-500">
<p class="text-xs text-gray-400 mt-1">故障转移时按此排序</p>
</div>
</div>
<div class="pt-4 border-t flex justify-end gap-3">
<button type="button" x-on:click="showForm = false"
class="px-4 py-2 text-gray-600 hover:bg-gray-100 rounded-md text-sm transition">取消</button>
<button type="submit"
class="px-6 py-2 bg-indigo-600 text-white rounded-md font-bold hover:bg-indigo-700 text-sm transition">
保存
</button>
</div>
</form>
</div>
</div>
</div>
<script>
/**
* 切换全局聊天机器人开关
*/
async function toggleChatBot() {
try {
const res = await fetch('{{ route('admin.ai-providers.toggle-chatbot') }}', {
method: 'POST',
headers: {
'X-CSRF-TOKEN': '{{ csrf_token() }}',
'Accept': 'application/json',
},
});
const data = await res.json();
if (data.status === 'success') {
// 更新按钮样式
const btn = document.getElementById('chatbot-toggle-btn');
const text = document.getElementById('chatbot-status-text');
if (data.enabled) {
btn.className = btn.className.replace('bg-gray-300', 'bg-emerald-500');
btn.firstElementChild.className = btn.firstElementChild.className.replace('translate-x-1',
'translate-x-6');
text.textContent = '已开启';
text.className = text.className.replace('text-gray-400', 'text-emerald-600');
} else {
btn.className = btn.className.replace('bg-emerald-500', 'bg-gray-300');
btn.firstElementChild.className = btn.firstElementChild.className.replace('translate-x-6',
'translate-x-1');
text.textContent = '已关闭';
text.className = text.className.replace('text-emerald-600', 'text-gray-400');
}
alert(data.message);
}
} catch (e) {
alert('操作失败:' + e.message);
}
}
/**
* 切换厂商启用/禁用状态
*/
async function toggleProvider(id, btn) {
try {
const res = await fetch('/admin/ai-providers/' + id + '/toggle', {
method: 'POST',
headers: {
'X-CSRF-TOKEN': '{{ csrf_token() }}',
'Accept': 'application/json',
},
});
const data = await res.json();
if (data.status === 'success') {
location.reload();
}
} catch (e) {
alert('操作失败:' + e.message);
}
}
/**
* 测试 AI 接口连通性
*/
async function testConnection(id, name) {
const btn = event.target;
const origText = btn.textContent;
btn.textContent = '测试中…';
btn.disabled = true;
try {
const res = await fetch('/admin/ai-providers/' + id + '/test', {
method: 'POST',
headers: {
'X-CSRF-TOKEN': '{{ csrf_token() }}',
'Accept': 'application/json',
},
});
const data = await res.json();
if (data.ok) {
alert(`✅ 「${name}」 连通成功!\n⤵ 响应耗时:${data.ms}ms包含冷启动\n🤖 模型回复:${data.message}`);
} else {
alert(`❌ 「${name}」 连通失败!\n耗时${data.ms}ms\n错误${data.message}`);
}
} catch (e) {
alert('请求异常:' + e.message);
} finally {
btn.textContent = origText;
btn.disabled = false;
}
}
/**
* 设为默认 AI 厂商
*/
async function setDefault(id) {
try {
const res = await fetch('/admin/ai-providers/' + id + '/default', {
method: 'POST',
headers: {
'X-CSRF-TOKEN': '{{ csrf_token() }}',
'Accept': 'application/json',
},
});
const data = await res.json();
if (data.status === 'success') {
location.reload();
}
} catch (e) {
alert('操作失败:' + e.message);
}
}
</script>
@endsection