| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314 |
- {% extends "base.html" %}
- {% block content %}
- <div class="flex h-screen overflow-hidden" id="ai-analysis-view">
- <!-- Sidebar -->
- {% include 'partials/sidebar.html' %}
- <!-- Main Content -->
- <div class="flex-1 flex flex-col min-w-0 overflow-hidden bg-gray-900/80 relative">
- <!-- Top Header -->
- <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">
- <button class="md:hidden text-gray-300 focus:outline-none" id="open-sidebar">
- <i class="fas fa-bars text-xl"></i>
- </button>
- <h1 class="text-lg md:text-2xl font-bold tech-title truncate ml-2">AI分析报告</h1>
- <div class="flex items-center space-x-4">
- <div class="flex items-center space-x-2">
- <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>
- <span class="hidden md:inline text-sm text-gray-300">{{ current_user.username }}</span>
- </div>
- </div>
- </header>
- <!-- Chat Area -->
- <main class="flex-1 overflow-y-auto p-4 md:p-6 scroll-smooth relative" id="chat-container">
- <div id="chat-messages" class="space-y-6 max-w-4xl mx-auto pb-20">
- <!-- Welcome Message -->
- <div class="flex justify-start">
- <div class="bg-gray-800 border border-gray-700 p-4 rounded-xl rounded-tl-none shadow-lg max-w-[90%]">
- <div class="flex items-center gap-2 mb-2">
- <i class="fas fa-robot text-blue-400"></i>
- <span class="text-sm font-bold text-gray-300">AI 分析助手</span>
- </div>
- <div class="text-gray-200 prose prose-invert max-w-none">
- <p>你好!我是你的AI分析助手。我可以帮你分析数据库中的采集数据,并生成报表。</p>
- <p>你可以问我:</p>
- <ul class="list-disc list-inside text-sm text-gray-400">
- <li>“统计一下最近采集的新闻来源分布”</li>
- <li>“分析一下关于‘人工智能’的深度采集内容摘要”</li>
- <li>“生成一个展示不同任务采集数量的柱状图”</li>
- </ul>
- </div>
- </div>
- </div>
- </div>
- </main>
- <!-- Input Area -->
- <div class="p-4 bg-gray-900/90 border-t border-gray-800 backdrop-blur-sm z-30">
- <div class="max-w-4xl mx-auto">
- <div class="relative">
- <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>
- <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">
- <i class="fas fa-paper-plane text-sm"></i>
- </button>
- </div>
- <div class="mt-2 text-xs text-center text-gray-500">
- AI生成内容仅供参考,请以实际数据为准
- </div>
- </div>
- </div>
- </div>
- </div>
- <!-- ECharts -->
- <script src="{{ url_for('static', filename='echarts/dist/echarts.min.js') }}"></script>
- <!-- Marked.js for Markdown rendering -->
- <script src="{{ url_for('static', filename='js/marked.min.js') }}"></script>
- <script>
- // Fallback for marked if CDN fails
- if (typeof marked === 'undefined') {
- console.warn('Marked.js failed to load. Using fallback parser.');
- window.marked = {
- parse: function(text) {
- // Very basic fallback
- if (!text) return '';
- let html = text
- .replace(/&/g, "&")
- .replace(/</g, "<")
- .replace(/>/g, ">")
- .replace(/```([\s\S]*?)```/g, function(match, code) { return '<pre class="bg-gray-900 p-2 rounded overflow-x-auto"><code>' + code + '</code></pre>'; })
- .replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>')
- .replace(/\*([^*]+)\*/g, '<em>$1</em>')
- .replace(/\n/g, '<br>');
- return html;
- }
- };
- }
- </script>
- <script>
- // Global variables
- let conversationHistory = [];
- let chatMessages, userInput, chatContainer;
- document.addEventListener('DOMContentLoaded', function() {
- console.log('AI Analysis script loaded');
- chatMessages = document.getElementById('chat-messages');
- userInput = document.getElementById('user-input');
- chatContainer = document.getElementById('chat-container');
- // Auto-resize textarea
- if (userInput) {
- userInput.addEventListener('keydown', function(e) {
- if (e.key === 'Enter' && !e.shiftKey) {
- e.preventDefault();
- sendMessage();
- }
- });
- }
- });
- function scrollToBottom() {
- if (chatContainer) {
- chatContainer.scrollTop = chatContainer.scrollHeight;
- }
- }
- // Make sendMessage global
- window.sendMessage = async function() {
- if (!userInput) return;
- const message = userInput.value.trim();
- if (!message) return;
- // Add User Message
- appendMessage('user', message);
- conversationHistory.push({ role: 'user', content: message });
-
- userInput.value = '';
- userInput.style.height = 'auto';
- // Add Assistant Placeholder
- const msgId = 'msg-' + Date.now();
- appendMessage('assistant', '<i class="fas fa-circle-notch fa-spin text-blue-400"></i> 思考中...', msgId);
- try {
- const response = await fetch('/ai/api/analysis/chat', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({
- message: message,
- history: conversationHistory
- })
- });
- const reader = response.body.getReader();
- const decoder = new TextDecoder();
- let assistantText = '';
- let isFirstChunk = true;
- let currentChartConfig = null;
- let chartBuffer = '';
-
- const $msgContent = $(`#${msgId} .content`);
- const $processLog = $(`#${msgId} .process-log`);
- const $chartsContainer = $(`#${msgId} .charts-container`);
- while (true) {
- const { done, value } = await reader.read();
- if (done) break;
- const chunk = decoder.decode(value, { stream: true });
- const lines = chunk.split('\n');
-
- for (const line of lines) {
- if (line.startsWith('data: ')) {
- const dataStr = line.slice(6).trim();
- if (dataStr === '[DONE]') continue;
- if (!dataStr) continue;
-
- try {
- const data = JSON.parse(dataStr);
-
- if (data.error) {
- $msgContent.html(`<span class="text-red-400">Error: ${data.error}</span>`);
- return;
- }
- // Handle Process/Thinking Events
- if (data.type === 'process') {
- $processLog.removeClass('hidden');
- $processLog.append(`
- <div class="flex items-center text-xs text-blue-300 bg-blue-900/20 p-2 rounded border border-blue-800/30">
- <i class="fas fa-circle-notch fa-spin mr-2 text-blue-400"></i>
- <span>${data.content}</span>
- </div>
- `);
- }
- // Handle Regular Content
- else if (data.content) {
- if (isFirstChunk) {
- $msgContent.empty();
- isFirstChunk = false;
- }
- assistantText += data.content;
-
- if (typeof marked !== 'undefined') {
- $msgContent.html(marked.parse(assistantText));
- } else {
- $msgContent.text(assistantText).css('white-space', 'pre-wrap');
- }
- }
- // Handle Charts
- if (data.chart) {
- const chartId = 'chart-' + Date.now();
- $chartsContainer.append(`<div id="${chartId}" class="w-full h-64 bg-gray-800 rounded-lg border border-gray-700"></div>`);
- const option = Array.isArray(data.chart) ? buildOptionFromList(data.chart) : data.chart;
- initChart(chartId, option);
- }
-
- scrollToBottom();
- } catch (e) {
- console.error('Error parsing SSE:', e);
- }
- }
- }
- }
-
- // Add complete assistant message to history
- if (assistantText) {
- conversationHistory.push({ role: 'assistant', content: assistantText });
- }
- } catch (error) {
- $(`#${msgId} .content`).html(`<span class="text-red-400">Connection Error: ${error.message}</span>`);
- }
- }
- function appendMessage(role, content, id = null) {
- if (!chatMessages) return;
- const div = document.createElement('div');
- div.className = `flex ${role === 'user' ? 'justify-end' : 'justify-start'}`;
- if (id) div.id = id;
-
- let innerHTML = '';
- if (role === 'user') {
- innerHTML = `
- <div class="bg-blue-600 text-white p-4 rounded-xl rounded-tr-none shadow-lg max-w-[90%]">
- <p>${escapeHtml(content)}</p>
- </div>
- `;
- } else {
- innerHTML = `
- <div class="bg-gray-800 border border-gray-700 p-4 rounded-xl rounded-tl-none shadow-lg max-w-[90%]">
- <div class="flex items-center gap-2 mb-2">
- <i class="fas fa-robot text-blue-400"></i>
- <span class="text-sm font-bold text-gray-300">AI 分析助手</span>
- </div>
- <!-- Process Log -->
- <div class="process-log space-y-2 mb-3 hidden bg-gray-900/50 p-3 rounded-lg border border-gray-700/50">
- <div class="text-xs text-gray-400 font-semibold mb-1 uppercase tracking-wider">Thinking Process</div>
- </div>
- <!-- Content -->
- <div class="content text-gray-200 prose prose-invert max-w-none min-h-[20px]">
- ${content}
- </div>
- <!-- Charts -->
- <div class="charts-container mt-4 space-y-4"></div>
- </div>
- `;
- }
- div.innerHTML = innerHTML;
- chatMessages.appendChild(div);
- scrollToBottom();
- }
- function escapeHtml(text) {
- const div = document.createElement('div');
- div.textContent = text;
- return div.innerHTML;
- }
- function initChart(domId, option) {
- const chartDom = document.getElementById(domId);
- if (!chartDom || typeof echarts === 'undefined') return;
- const myChart = echarts.init(chartDom, 'dark');
- option = option || {};
- option.backgroundColor = 'transparent';
- myChart.setOption(option, true);
- window.addEventListener('resize', function() { myChart.resize(); });
- }
- function buildOptionFromList(items) {
- if (!Array.isArray(items) || items.length === 0) return {};
- const keys = Object.keys(items[0] || {});
- let xField = null, yField = null;
- const xCandidates = ['source','name','title','label','date','time','category','key','keyword'];
- const yCandidates = ['count','value','num','total','amount','score'];
- for (const k of keys) { if (xCandidates.some(c => k.toLowerCase().includes(c))) { xField = k; break; } }
- for (const k of keys) { if (yCandidates.some(c => k.toLowerCase().includes(c))) { yField = k; break; } }
- if (!xField) xField = keys[0];
- if (!yField) yField = keys.find(k => k !== xField) || keys[0];
- const xData = items.map(it => it[xField]);
- const yData = items.map(it => it[yField]);
- return {
- title: { left: 'center', textStyle: { color: '#fff' } },
- tooltip: { trigger: 'item' },
- legend: { orient: 'vertical', left: 'left', textStyle: { color: '#ccc' } },
- xAxis: { type: 'category', data: xData, axisLabel: { color: '#ccc', rotate: xData.length > 5 ? 45 : 0 } },
- yAxis: { type: 'value', axisLabel: { color: '#ccc' }, splitLine: { lineStyle: { color: '#333' } } },
- grid: { bottom: '15%', containLabel: true },
- series: [{ data: yData, type: 'bar', itemStyle: { color: '#3b82f6' }, barMaxWidth: 50 }]
- };
- }
- // Sidebar toggle
- $(document).ready(function() {
- $('#open-sidebar').click(function() {
- $('#sidebar').toggleClass('-translate-x-full');
- });
- });
- </script>
- {% endblock %}
|