ai_management.html 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423
  1. {% extends "base.html" %}
  2. {% block content %}
  3. <div class="flex h-screen overflow-hidden" id="ai-view">
  4. <!-- Sidebar -->
  5. {% include 'partials/sidebar.html' %}
  6. <!-- Main Content -->
  7. <div class="flex-1 flex flex-col min-w-0 overflow-hidden bg-gray-900/80 relative">
  8. <!-- Top Header -->
  9. <header class="h-16 flex items-center justify-between px-6 z-20 bg-gray-900/80 backdrop-blur-md border-b border-blue-900/30">
  10. <button class="md:hidden text-gray-300 focus:outline-none" id="open-sidebar">
  11. <i class="fas fa-bars text-xl"></i>
  12. </button>
  13. <h1 class="text-lg md:text-2xl font-bold tech-title truncate ml-2">AI模型管理</h1>
  14. <div class="flex items-center space-x-4">
  15. <div class="flex items-center space-x-2">
  16. <div class="w-8 h-8 rounded-full bg-blue-500 flex items-center justify-center text-white font-bold border border-cyan-400 shadow-md">A</div>
  17. <span class="hidden md:inline text-sm text-gray-300">{{ current_user.username }}</span>
  18. </div>
  19. </div>
  20. </header>
  21. <!-- Main Scrollable Area -->
  22. <main class="flex-1 overflow-y-auto p-4 md:p-6 scroll-smooth">
  23. <!-- Toolbar -->
  24. <div class="mb-6 flex justify-end">
  25. <button onclick="openEditModal()" class="bg-blue-600 hover:bg-blue-500 text-white px-4 py-2 rounded-lg shadow-lg flex items-center gap-2 transition-all">
  26. <i class="fas fa-plus"></i> 接入新模型
  27. </button>
  28. </div>
  29. <!-- Model Grid -->
  30. <div id="model-grid" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
  31. <!-- Cards injected here -->
  32. </div>
  33. </main>
  34. </div>
  35. </div>
  36. <!-- Edit/Add Modal -->
  37. <div id="edit-modal" class="fixed inset-0 z-50 hidden">
  38. <div class="absolute inset-0 bg-black/70 backdrop-blur-sm" onclick="closeEditModal()"></div>
  39. <div class="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 bg-gray-800 rounded-xl shadow-2xl border border-gray-700 w-full max-w-lg p-6 animate-fade-in-up">
  40. <h3 class="text-xl font-bold text-white mb-4" id="modal-title">接入模型</h3>
  41. <form id="model-form" class="space-y-4">
  42. <input type="hidden" id="model-id">
  43. <div>
  44. <label class="block text-gray-400 text-sm mb-1">模型别名</label>
  45. <input type="text" id="model-name" class="w-full bg-gray-700 text-white rounded p-2 border border-gray-600 focus:border-blue-500 focus:outline-none" required placeholder="例如:SiliconFlow Qwen">
  46. </div>
  47. <div>
  48. <label class="block text-gray-400 text-sm mb-1">API接口地址 (Base URL)</label>
  49. <input type="text" id="api-base" class="w-full bg-gray-700 text-white rounded p-2 border border-gray-600 focus:border-blue-500 focus:outline-none" required placeholder="https://api.siliconflow.cn/v1/">
  50. </div>
  51. <div>
  52. <label class="block text-gray-400 text-sm mb-1">API密钥 (API Key)</label>
  53. <input type="password" id="api-key" class="w-full bg-gray-700 text-white rounded p-2 border border-gray-600 focus:border-blue-500 focus:outline-none" required>
  54. </div>
  55. <div>
  56. <label class="block text-gray-400 text-sm mb-1">模型ID (Model Name)</label>
  57. <input type="text" id="real-model-name" class="w-full bg-gray-700 text-white rounded p-2 border border-gray-600 focus:border-blue-500 focus:outline-none" required placeholder="Qwen/Qwen3-Next-80B-A3B-Instruct">
  58. </div>
  59. <div>
  60. <label class="block text-gray-400 text-sm mb-1">系统提示词 (System Prompt)</label>
  61. <textarea id="system-prompt" rows="3" class="w-full bg-gray-700 text-white rounded p-2 border border-gray-600 focus:border-blue-500 focus:outline-none" placeholder="You are a helpful assistant."></textarea>
  62. </div>
  63. <div class="flex justify-end gap-3 mt-6">
  64. <button type="button" onclick="closeEditModal()" class="px-4 py-2 rounded text-gray-400 hover:text-white hover:bg-gray-700 transition-colors">取消</button>
  65. <button type="submit" class="px-4 py-2 rounded bg-blue-600 text-white hover:bg-blue-700 transition-colors shadow-lg">保存</button>
  66. </div>
  67. </form>
  68. </div>
  69. </div>
  70. <!-- Test Chat Modal -->
  71. <div id="test-modal" class="fixed inset-0 z-50 hidden">
  72. <div class="absolute inset-0 bg-black/70 backdrop-blur-sm" onclick="closeTestModal()"></div>
  73. <div class="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 bg-gray-800 rounded-xl shadow-2xl border border-gray-700 w-full max-w-2xl h-[600px] flex flex-col animate-fade-in-up">
  74. <div class="p-4 border-b border-gray-700 flex justify-between items-center bg-gray-800/50 rounded-t-xl">
  75. <div>
  76. <h3 class="text-lg font-bold text-white">模型测试</h3>
  77. <span class="text-xs text-green-400 flex items-center gap-1"><i class="fas fa-circle text-[8px]"></i> <span id="test-model-name">目标模型</span></span>
  78. </div>
  79. <button onclick="closeTestModal()" class="text-gray-400 hover:text-white"><i class="fas fa-times"></i></button>
  80. </div>
  81. <!-- Chat Area -->
  82. <div id="chat-box" class="flex-1 overflow-y-auto p-4 space-y-4 bg-gray-900/50">
  83. <div class="flex justify-start">
  84. <div class="bg-gray-700 text-gray-200 p-3 rounded-lg rounded-tl-none max-w-[80%]">
  85. <p>你好!我是该模型的测试助手,请发送消息开始测试连接。</p>
  86. </div>
  87. </div>
  88. </div>
  89. <!-- Input Area -->
  90. <div class="p-4 bg-gray-800 border-t border-gray-700">
  91. <div class="flex gap-2">
  92. <input type="text" id="test-input" class="flex-1 bg-gray-700 text-white rounded p-2 border border-gray-600 focus:border-blue-500 focus:outline-none" placeholder="输入测试消息..." onkeypress="if(event.keyCode==13) sendTestMessage()">
  93. <button onclick="sendTestMessage()" id="send-btn" class="bg-blue-600 hover:bg-blue-500 text-white px-4 rounded transition-colors">
  94. <i class="fas fa-paper-plane"></i>
  95. </button>
  96. </div>
  97. <div class="mt-2 text-xs text-gray-500 flex justify-between">
  98. <span>本次消耗: <span id="last-tokens" class="text-blue-400">0</span> tokens</span>
  99. <span>总消耗: <span id="total-tokens-display" class="text-purple-400">0</span> tokens</span>
  100. </div>
  101. </div>
  102. </div>
  103. </div>
  104. <!-- Toast -->
  105. <div id="toast" class="fixed top-5 left-1/2 transform -translate-x-1/2 z-50 hidden transition-all duration-300">
  106. <div class="bg-gray-800 border-l-4 border-blue-500 text-white px-6 py-3 rounded shadow-lg flex items-center gap-3">
  107. <i class="fas fa-info-circle text-blue-400" id="toast-icon"></i>
  108. <span id="toast-message">系统通知</span>
  109. </div>
  110. </div>
  111. <script>
  112. let currentTestModelId = null;
  113. $(document).ready(function() {
  114. loadModels();
  115. $('#open-sidebar').click(function() {
  116. $('#sidebar').toggleClass('-translate-x-full');
  117. });
  118. $('#model-form').submit(function(e) {
  119. e.preventDefault();
  120. saveModel();
  121. });
  122. });
  123. function loadModels() {
  124. $.get('/ai/api/list', function(response) {
  125. const grid = $('#model-grid');
  126. grid.empty();
  127. if (response.items.length === 0) {
  128. grid.html('<div class="col-span-full text-center text-gray-500 py-10">暂无模型接入</div>');
  129. return;
  130. }
  131. response.items.forEach(item => {
  132. grid.append(`
  133. <div class="bg-gray-800 rounded-xl border border-gray-700 p-6 hover:border-blue-500/50 transition-all group relative overflow-hidden">
  134. <div class="absolute top-0 right-0 p-4 opacity-10 group-hover:opacity-20 transition-opacity">
  135. <i class="fas fa-brain text-6xl text-blue-500"></i>
  136. </div>
  137. <div class="flex justify-between items-start mb-4 relative z-10">
  138. <div>
  139. <h3 class="text-lg font-bold text-white mb-1">${item.name}</h3>
  140. <p class="text-xs text-gray-400 font-mono bg-gray-900/50 px-2 py-1 rounded inline-block">${item.model_name}</p>
  141. </div>
  142. <div class="flex items-center">
  143. <label class="relative inline-flex items-center cursor-pointer">
  144. <input type="checkbox" class="sr-only peer" onchange="toggleModel(${item.id})" ${item.is_active ? 'checked' : ''}>
  145. <div class="w-9 h-5 bg-gray-600 peer-focus:outline-none peer-focus:ring-2 peer-focus:ring-blue-800 rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-4 after:w-4 after:transition-all peer-checked:bg-blue-600"></div>
  146. </label>
  147. </div>
  148. </div>
  149. <div class="space-y-3 mb-6 relative z-10">
  150. <div class="flex items-center justify-between text-sm">
  151. <span class="text-gray-400">服务提供商</span>
  152. <span class="text-gray-300">${item.provider}</span>
  153. </div>
  154. <div class="flex items-center justify-between text-sm">
  155. <span class="text-gray-400">Token消耗</span>
  156. <span class="text-purple-400 font-mono font-bold">${item.total_tokens.toLocaleString()}</span>
  157. </div>
  158. <div class="flex items-center justify-between text-sm">
  159. <span class="text-gray-400">接口状态</span>
  160. <span class="text-green-400"><i class="fas fa-check-circle"></i> 已配置</span>
  161. </div>
  162. </div>
  163. <div class="flex gap-2 relative z-10 pt-4 border-t border-gray-700">
  164. <button onclick="openTestModal(${item.id}, '${item.name}', ${item.total_tokens})" class="flex-1 bg-blue-600/20 hover:bg-blue-600 text-blue-400 hover:text-white py-2 rounded transition-colors text-sm font-medium">
  165. <i class="fas fa-comments mr-1"></i> 测试对话
  166. </button>
  167. <button onclick='openEditModal(${JSON.stringify(item)})' class="bg-gray-700 hover:bg-gray-600 text-gray-300 py-2 px-3 rounded transition-colors" title="编辑">
  168. <i class="fas fa-edit"></i>
  169. </button>
  170. <button onclick="deleteModel(${item.id})" class="bg-red-900/20 hover:bg-red-600 text-red-400 hover:text-white py-2 px-3 rounded transition-colors" title="删除">
  171. <i class="fas fa-trash-alt"></i>
  172. </button>
  173. </div>
  174. </div>
  175. `);
  176. });
  177. });
  178. }
  179. function openEditModal(model = null) {
  180. if (model) {
  181. $('#modal-title').text('编辑模型');
  182. $('#model-id').val(model.id);
  183. $('#model-name').val(model.name);
  184. $('#api-base').val(model.api_base);
  185. $('#api-key').val(model.api_key); // Note: Should handle security better
  186. $('#real-model-name').val(model.model_name);
  187. $('#system-prompt').val(model.system_prompt);
  188. } else {
  189. $('#modal-title').text('接入新模型');
  190. $('#model-form')[0].reset();
  191. $('#model-id').val('');
  192. }
  193. $('#edit-modal').removeClass('hidden');
  194. }
  195. function closeEditModal() {
  196. $('#edit-modal').addClass('hidden');
  197. }
  198. function saveModel() {
  199. const data = {
  200. id: $('#model-id').val(),
  201. name: $('#model-name').val(),
  202. api_base: $('#api-base').val(),
  203. api_key: $('#api-key').val(),
  204. model_name: $('#real-model-name').val(),
  205. system_prompt: $('#system-prompt').val()
  206. };
  207. $.ajax({
  208. url: '/ai/api/save',
  209. method: 'POST',
  210. contentType: 'application/json',
  211. data: JSON.stringify(data),
  212. success: function() {
  213. showToast('保存成功', 'success');
  214. closeEditModal();
  215. loadModels();
  216. },
  217. error: function(xhr) {
  218. showToast('保存失败: ' + (xhr.responseJSON?.error || 'Unknown error'), 'error');
  219. }
  220. });
  221. }
  222. function deleteModel(id) {
  223. if (!confirm('确定要删除该模型配置吗?')) return;
  224. $.ajax({
  225. url: '/ai/api/delete',
  226. method: 'POST',
  227. contentType: 'application/json',
  228. data: JSON.stringify({id: id}),
  229. success: function() {
  230. showToast('删除成功', 'success');
  231. loadModels();
  232. },
  233. error: function(xhr) {
  234. showToast('删除失败', 'error');
  235. }
  236. });
  237. }
  238. function toggleModel(id) {
  239. $.ajax({
  240. url: '/ai/api/toggle',
  241. method: 'POST',
  242. contentType: 'application/json',
  243. data: JSON.stringify({id: id}),
  244. success: function() {
  245. // Optionally show toast
  246. }
  247. });
  248. }
  249. // --- Test Chat Logic ---
  250. function openTestModal(id, name, totalTokens) {
  251. currentTestModelId = id;
  252. $('#test-model-name').text(name);
  253. $('#total-tokens-display').text(totalTokens.toLocaleString());
  254. $('#last-tokens').text('0');
  255. $('#chat-box').html(`
  256. <div class="flex justify-start">
  257. <div class="bg-gray-700 text-gray-200 p-3 rounded-lg rounded-tl-none max-w-[80%]">
  258. <p>你好!我是该模型的测试助手,请发送消息开始测试连接。</p>
  259. </div>
  260. </div>
  261. `);
  262. $('#test-modal').removeClass('hidden');
  263. $('#test-input').focus();
  264. }
  265. function closeTestModal() {
  266. $('#test-modal').addClass('hidden');
  267. currentTestModelId = null;
  268. }
  269. async function sendTestMessage() {
  270. const input = $('#test-input');
  271. const msg = input.val().trim();
  272. if (!msg) return;
  273. // Append User Message
  274. $('#chat-box').append(`
  275. <div class="flex justify-end animate-fade-in-up">
  276. <div class="bg-blue-600 text-white p-3 rounded-lg rounded-tr-none max-w-[80%]">
  277. <p>${escapeHtml(msg)}</p>
  278. </div>
  279. </div>
  280. `);
  281. input.val('');
  282. scrollToBottom();
  283. // Prepare Assistant Message Container
  284. const msgId = 'msg-' + Date.now();
  285. $('#chat-box').append(`
  286. <div class="flex justify-start animate-fade-in-up">
  287. <div class="bg-gray-700 text-gray-200 p-3 rounded-lg rounded-tl-none max-w-[80%]">
  288. <p id="${msgId}" class="whitespace-pre-wrap"><i class="fas fa-circle-notch fa-spin text-blue-400"></i></p>
  289. </div>
  290. </div>
  291. `);
  292. scrollToBottom();
  293. try {
  294. const response = await fetch('/ai/api/test', {
  295. method: 'POST',
  296. headers: { 'Content-Type': 'application/json' },
  297. body: JSON.stringify({
  298. id: currentTestModelId,
  299. message: msg
  300. })
  301. });
  302. const reader = response.body.getReader();
  303. const decoder = new TextDecoder();
  304. let assistantText = '';
  305. let isFirstChunk = true;
  306. const $msgContent = $(`#${msgId}`);
  307. while (true) {
  308. const { done, value } = await reader.read();
  309. if (done) break;
  310. const chunk = decoder.decode(value, { stream: true });
  311. const lines = chunk.split('\n');
  312. for (const line of lines) {
  313. if (line.startsWith('data: ')) {
  314. const dataStr = line.slice(6).trim();
  315. if (dataStr === '[DONE]') continue;
  316. if (!dataStr) continue;
  317. try {
  318. const data = JSON.parse(dataStr);
  319. if (data.error) {
  320. $msgContent.html(`<span class="text-red-400">Error: ${data.error}</span>`);
  321. return;
  322. }
  323. if (data.content) {
  324. if (isFirstChunk) {
  325. $msgContent.empty(); // Remove spinner
  326. isFirstChunk = false;
  327. }
  328. assistantText += data.content;
  329. $msgContent.text(assistantText);
  330. scrollToBottom();
  331. }
  332. if (data.usage) {
  333. $('#last-tokens').text(data.usage.total);
  334. $('#total-tokens-display').text(data.usage.total_aggregated.toLocaleString());
  335. loadModels();
  336. }
  337. } catch (e) {
  338. console.error('Error parsing SSE:', e);
  339. }
  340. }
  341. }
  342. }
  343. } catch (error) {
  344. $(`#${msgId}`).html(`<span class="text-red-400">Connection Error: ${error.message}</span>`);
  345. }
  346. }
  347. function scrollToBottom() {
  348. const chatBox = document.getElementById('chat-box');
  349. chatBox.scrollTop = chatBox.scrollHeight;
  350. }
  351. function escapeHtml(text) {
  352. const div = document.createElement('div');
  353. div.textContent = text;
  354. return div.innerHTML;
  355. }
  356. function showToast(msg, type = 'info') {
  357. const toast = $('#toast');
  358. const icon = $('#toast-icon');
  359. const text = $('#toast-message');
  360. const container = toast.find('div');
  361. text.text(msg);
  362. toast.removeClass('hidden').addClass('flex');
  363. container.removeClass('border-blue-500 border-red-500 border-green-500');
  364. icon.removeClass('text-blue-400 text-red-400 text-green-400 fa-info-circle fa-check-circle fa-exclamation-circle');
  365. if (type === 'success') {
  366. container.addClass('border-green-500');
  367. icon.addClass('text-green-400 fa-check-circle');
  368. } else if (type === 'error') {
  369. container.addClass('border-red-500');
  370. icon.addClass('text-red-400 fa-exclamation-circle');
  371. } else {
  372. container.addClass('border-blue-500');
  373. icon.addClass('text-blue-400 fa-info-circle');
  374. }
  375. setTimeout(() => {
  376. toast.addClass('hidden').removeClass('flex');
  377. }, 3000);
  378. }
  379. </script>
  380. {% endblock %}