ai_analysis.html 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314
  1. {% extends "base.html" %}
  2. {% block content %}
  3. <div class="flex h-screen overflow-hidden" id="ai-analysis-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. <!-- Chat Area -->
  22. <main class="flex-1 overflow-y-auto p-4 md:p-6 scroll-smooth relative" id="chat-container">
  23. <div id="chat-messages" class="space-y-6 max-w-4xl mx-auto pb-20">
  24. <!-- Welcome Message -->
  25. <div class="flex justify-start">
  26. <div class="bg-gray-800 border border-gray-700 p-4 rounded-xl rounded-tl-none shadow-lg max-w-[90%]">
  27. <div class="flex items-center gap-2 mb-2">
  28. <i class="fas fa-robot text-blue-400"></i>
  29. <span class="text-sm font-bold text-gray-300">AI 分析助手</span>
  30. </div>
  31. <div class="text-gray-200 prose prose-invert max-w-none">
  32. <p>你好!我是你的AI分析助手。我可以帮你分析数据库中的采集数据,并生成报表。</p>
  33. <p>你可以问我:</p>
  34. <ul class="list-disc list-inside text-sm text-gray-400">
  35. <li>“统计一下最近采集的新闻来源分布”</li>
  36. <li>“分析一下关于‘人工智能’的深度采集内容摘要”</li>
  37. <li>“生成一个展示不同任务采集数量的柱状图”</li>
  38. </ul>
  39. </div>
  40. </div>
  41. </div>
  42. </div>
  43. </main>
  44. <!-- Input Area -->
  45. <div class="p-4 bg-gray-900/90 border-t border-gray-800 backdrop-blur-sm z-30">
  46. <div class="max-w-4xl mx-auto">
  47. <div class="relative">
  48. <textarea id="user-input" rows="1" class="w-full bg-gray-800 text-white rounded-xl pl-4 pr-12 py-3 border border-gray-700 focus:border-blue-500 focus:ring-1 focus:ring-blue-500 focus:outline-none resize-none overflow-hidden" placeholder="输入你的分析需求..." oninput="this.style.height = ''; this.style.height = this.scrollHeight + 'px'"></textarea>
  49. <button onclick="sendMessage()" id="send-btn" class="absolute right-2 bottom-2 bg-blue-600 hover:bg-blue-500 text-white w-8 h-8 rounded-lg flex items-center justify-center transition-colors shadow-lg">
  50. <i class="fas fa-paper-plane text-sm"></i>
  51. </button>
  52. </div>
  53. <div class="mt-2 text-xs text-center text-gray-500">
  54. AI生成内容仅供参考,请以实际数据为准
  55. </div>
  56. </div>
  57. </div>
  58. </div>
  59. </div>
  60. <!-- ECharts -->
  61. <script src="{{ url_for('static', filename='echarts/dist/echarts.min.js') }}"></script>
  62. <!-- Marked.js for Markdown rendering -->
  63. <script src="{{ url_for('static', filename='js/marked.min.js') }}"></script>
  64. <script>
  65. // Fallback for marked if CDN fails
  66. if (typeof marked === 'undefined') {
  67. console.warn('Marked.js failed to load. Using fallback parser.');
  68. window.marked = {
  69. parse: function(text) {
  70. // Very basic fallback
  71. if (!text) return '';
  72. let html = text
  73. .replace(/&/g, "&amp;")
  74. .replace(/</g, "&lt;")
  75. .replace(/>/g, "&gt;")
  76. .replace(/```([\s\S]*?)```/g, function(match, code) { return '<pre class="bg-gray-900 p-2 rounded overflow-x-auto"><code>' + code + '</code></pre>'; })
  77. .replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>')
  78. .replace(/\*([^*]+)\*/g, '<em>$1</em>')
  79. .replace(/\n/g, '<br>');
  80. return html;
  81. }
  82. };
  83. }
  84. </script>
  85. <script>
  86. // Global variables
  87. let conversationHistory = [];
  88. let chatMessages, userInput, chatContainer;
  89. document.addEventListener('DOMContentLoaded', function() {
  90. console.log('AI Analysis script loaded');
  91. chatMessages = document.getElementById('chat-messages');
  92. userInput = document.getElementById('user-input');
  93. chatContainer = document.getElementById('chat-container');
  94. // Auto-resize textarea
  95. if (userInput) {
  96. userInput.addEventListener('keydown', function(e) {
  97. if (e.key === 'Enter' && !e.shiftKey) {
  98. e.preventDefault();
  99. sendMessage();
  100. }
  101. });
  102. }
  103. });
  104. function scrollToBottom() {
  105. if (chatContainer) {
  106. chatContainer.scrollTop = chatContainer.scrollHeight;
  107. }
  108. }
  109. // Make sendMessage global
  110. window.sendMessage = async function() {
  111. if (!userInput) return;
  112. const message = userInput.value.trim();
  113. if (!message) return;
  114. // Add User Message
  115. appendMessage('user', message);
  116. conversationHistory.push({ role: 'user', content: message });
  117. userInput.value = '';
  118. userInput.style.height = 'auto';
  119. // Add Assistant Placeholder
  120. const msgId = 'msg-' + Date.now();
  121. appendMessage('assistant', '<i class="fas fa-circle-notch fa-spin text-blue-400"></i> 思考中...', msgId);
  122. try {
  123. const response = await fetch('/ai/api/analysis/chat', {
  124. method: 'POST',
  125. headers: { 'Content-Type': 'application/json' },
  126. body: JSON.stringify({
  127. message: message,
  128. history: conversationHistory
  129. })
  130. });
  131. const reader = response.body.getReader();
  132. const decoder = new TextDecoder();
  133. let assistantText = '';
  134. let isFirstChunk = true;
  135. let currentChartConfig = null;
  136. let chartBuffer = '';
  137. const $msgContent = $(`#${msgId} .content`);
  138. const $processLog = $(`#${msgId} .process-log`);
  139. const $chartsContainer = $(`#${msgId} .charts-container`);
  140. while (true) {
  141. const { done, value } = await reader.read();
  142. if (done) break;
  143. const chunk = decoder.decode(value, { stream: true });
  144. const lines = chunk.split('\n');
  145. for (const line of lines) {
  146. if (line.startsWith('data: ')) {
  147. const dataStr = line.slice(6).trim();
  148. if (dataStr === '[DONE]') continue;
  149. if (!dataStr) continue;
  150. try {
  151. const data = JSON.parse(dataStr);
  152. if (data.error) {
  153. $msgContent.html(`<span class="text-red-400">Error: ${data.error}</span>`);
  154. return;
  155. }
  156. // Handle Process/Thinking Events
  157. if (data.type === 'process') {
  158. $processLog.removeClass('hidden');
  159. $processLog.append(`
  160. <div class="flex items-center text-xs text-blue-300 bg-blue-900/20 p-2 rounded border border-blue-800/30">
  161. <i class="fas fa-circle-notch fa-spin mr-2 text-blue-400"></i>
  162. <span>${data.content}</span>
  163. </div>
  164. `);
  165. }
  166. // Handle Regular Content
  167. else if (data.content) {
  168. if (isFirstChunk) {
  169. $msgContent.empty();
  170. isFirstChunk = false;
  171. }
  172. assistantText += data.content;
  173. if (typeof marked !== 'undefined') {
  174. $msgContent.html(marked.parse(assistantText));
  175. } else {
  176. $msgContent.text(assistantText).css('white-space', 'pre-wrap');
  177. }
  178. }
  179. // Handle Charts
  180. if (data.chart) {
  181. const chartId = 'chart-' + Date.now();
  182. $chartsContainer.append(`<div id="${chartId}" class="w-full h-64 bg-gray-800 rounded-lg border border-gray-700"></div>`);
  183. const option = Array.isArray(data.chart) ? buildOptionFromList(data.chart) : data.chart;
  184. initChart(chartId, option);
  185. }
  186. scrollToBottom();
  187. } catch (e) {
  188. console.error('Error parsing SSE:', e);
  189. }
  190. }
  191. }
  192. }
  193. // Add complete assistant message to history
  194. if (assistantText) {
  195. conversationHistory.push({ role: 'assistant', content: assistantText });
  196. }
  197. } catch (error) {
  198. $(`#${msgId} .content`).html(`<span class="text-red-400">Connection Error: ${error.message}</span>`);
  199. }
  200. }
  201. function appendMessage(role, content, id = null) {
  202. if (!chatMessages) return;
  203. const div = document.createElement('div');
  204. div.className = `flex ${role === 'user' ? 'justify-end' : 'justify-start'}`;
  205. if (id) div.id = id;
  206. let innerHTML = '';
  207. if (role === 'user') {
  208. innerHTML = `
  209. <div class="bg-blue-600 text-white p-4 rounded-xl rounded-tr-none shadow-lg max-w-[90%]">
  210. <p>${escapeHtml(content)}</p>
  211. </div>
  212. `;
  213. } else {
  214. innerHTML = `
  215. <div class="bg-gray-800 border border-gray-700 p-4 rounded-xl rounded-tl-none shadow-lg max-w-[90%]">
  216. <div class="flex items-center gap-2 mb-2">
  217. <i class="fas fa-robot text-blue-400"></i>
  218. <span class="text-sm font-bold text-gray-300">AI 分析助手</span>
  219. </div>
  220. <!-- Process Log -->
  221. <div class="process-log space-y-2 mb-3 hidden bg-gray-900/50 p-3 rounded-lg border border-gray-700/50">
  222. <div class="text-xs text-gray-400 font-semibold mb-1 uppercase tracking-wider">Thinking Process</div>
  223. </div>
  224. <!-- Content -->
  225. <div class="content text-gray-200 prose prose-invert max-w-none min-h-[20px]">
  226. ${content}
  227. </div>
  228. <!-- Charts -->
  229. <div class="charts-container mt-4 space-y-4"></div>
  230. </div>
  231. `;
  232. }
  233. div.innerHTML = innerHTML;
  234. chatMessages.appendChild(div);
  235. scrollToBottom();
  236. }
  237. function escapeHtml(text) {
  238. const div = document.createElement('div');
  239. div.textContent = text;
  240. return div.innerHTML;
  241. }
  242. function initChart(domId, option) {
  243. const chartDom = document.getElementById(domId);
  244. if (!chartDom || typeof echarts === 'undefined') return;
  245. const myChart = echarts.init(chartDom, 'dark');
  246. option = option || {};
  247. option.backgroundColor = 'transparent';
  248. myChart.setOption(option, true);
  249. window.addEventListener('resize', function() { myChart.resize(); });
  250. }
  251. function buildOptionFromList(items) {
  252. if (!Array.isArray(items) || items.length === 0) return {};
  253. const keys = Object.keys(items[0] || {});
  254. let xField = null, yField = null;
  255. const xCandidates = ['source','name','title','label','date','time','category','key','keyword'];
  256. const yCandidates = ['count','value','num','total','amount','score'];
  257. for (const k of keys) { if (xCandidates.some(c => k.toLowerCase().includes(c))) { xField = k; break; } }
  258. for (const k of keys) { if (yCandidates.some(c => k.toLowerCase().includes(c))) { yField = k; break; } }
  259. if (!xField) xField = keys[0];
  260. if (!yField) yField = keys.find(k => k !== xField) || keys[0];
  261. const xData = items.map(it => it[xField]);
  262. const yData = items.map(it => it[yField]);
  263. return {
  264. title: { left: 'center', textStyle: { color: '#fff' } },
  265. tooltip: { trigger: 'item' },
  266. legend: { orient: 'vertical', left: 'left', textStyle: { color: '#ccc' } },
  267. xAxis: { type: 'category', data: xData, axisLabel: { color: '#ccc', rotate: xData.length > 5 ? 45 : 0 } },
  268. yAxis: { type: 'value', axisLabel: { color: '#ccc' }, splitLine: { lineStyle: { color: '#333' } } },
  269. grid: { bottom: '15%', containLabel: true },
  270. series: [{ data: yData, type: 'bar', itemStyle: { color: '#3b82f6' }, barMaxWidth: 50 }]
  271. };
  272. }
  273. // Sidebar toggle
  274. $(document).ready(function() {
  275. $('#open-sidebar').click(function() {
  276. $('#sidebar').toggleClass('-translate-x-full');
  277. });
  278. });
  279. </script>
  280. {% endblock %}