stock-ai-agent/frontend/js/app.js
2026-02-03 21:54:05 +08:00

426 lines
14 KiB
JavaScript
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.

// Vue 3 Application
const { createApp } = Vue;
createApp({
data() {
return {
messages: [],
userInput: '',
loading: false,
sessionId: null,
charts: {},
showImageModal: false,
modalImageUrl: '',
availableModels: [],
currentModel: null,
selectedModel: null
};
},
mounted() {
this.sessionId = this.generateSessionId();
this.autoResizeTextarea();
this.loadModels();
},
methods: {
async sendMessage() {
if (!this.userInput.trim() || this.loading) return;
const message = this.userInput.trim();
this.userInput = '';
// Add user message
this.messages.push({
role: 'user',
content: message,
timestamp: new Date()
});
this.$nextTick(() => {
this.scrollToBottom();
this.autoResizeTextarea();
});
this.loading = true;
try {
const response = await fetch('/api/chat/message', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
message: message,
session_id: this.sessionId
})
});
if (!response.ok) {
throw new Error('请求失败');
}
const data = await response.json();
// Add assistant message
const assistantMessage = {
role: 'assistant',
content: data.message,
timestamp: new Date(),
metadata: data.metadata
};
this.messages.push(assistantMessage);
// Render chart if needed
if (data.metadata && data.metadata.type === 'chart') {
this.$nextTick(() => {
const index = this.messages.length - 1;
this.renderChart(index, data.metadata.data);
});
}
this.$nextTick(() => {
this.scrollToBottom();
});
} catch (error) {
console.error('发送消息失败:', error);
this.messages.push({
role: 'assistant',
content: '抱歉,发送消息失败,请稍后重试。',
timestamp: new Date()
});
} finally {
this.loading = false;
}
},
sendExample(exampleText) {
// Set the example text to input and send
this.userInput = exampleText;
this.sendMessage();
},
renderMarkdown(content) {
if (!content) return '';
// Configure marked options
marked.setOptions({
breaks: true,
gfm: true,
headerIds: false,
mangle: false
});
return marked.parse(content);
},
renderChart(index, data) {
const chartId = `chart-${index}`;
const container = document.getElementById(chartId);
if (!container || !data.kline_data) return;
const chart = LightweightCharts.createChart(container, {
width: container.clientWidth,
height: 400,
layout: {
background: { color: '#000000' },
textColor: '#a0a0a0'
},
grid: {
vertLines: { color: '#1a1a1a' },
horzLines: { color: '#1a1a1a' }
},
timeScale: {
borderColor: '#333333',
timeVisible: true
},
rightPriceScale: {
borderColor: '#333333'
}
});
const candlestickSeries = chart.addCandlestickSeries({
upColor: '#00ff41',
downColor: '#ff0040',
borderVisible: false,
wickUpColor: '#00ff41',
wickDownColor: '#ff0040'
});
const klineData = data.kline_data.map(item => ({
time: item.trade_date,
open: item.open,
high: item.high,
low: item.low,
close: item.close
}));
candlestickSeries.setData(klineData);
if (data.volume_data) {
const volumeSeries = chart.addHistogramSeries({
color: '#00ff4140',
priceFormat: {
type: 'volume'
},
priceScaleId: ''
});
const volumeData = data.volume_data.map(item => ({
time: item.trade_date,
value: item.vol,
color: item.close >= item.open ? '#00ff4140' : '#ff004040'
}));
volumeSeries.setData(volumeData);
}
chart.timeScale().fitContent();
this.charts[chartId] = chart;
// Handle resize
window.addEventListener('resize', () => {
if (this.charts[chartId]) {
chart.applyOptions({ width: container.clientWidth });
}
});
},
scrollToBottom() {
const container = this.$refs.chatContainer;
if (container) {
setTimeout(() => {
container.scrollTop = container.scrollHeight;
}, 100);
}
},
autoResizeTextarea() {
const textarea = this.$refs.textarea;
if (textarea) {
textarea.style.height = 'auto';
textarea.style.height = Math.min(textarea.scrollHeight, 120) + 'px';
}
},
generateSessionId() {
return 'session_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
},
copyWechat() {
const wechatId = 'aaronlzhou';
// 使用现代的 Clipboard API
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(wechatId).then(() => {
this.showCopyNotification();
}).catch(err => {
console.error('复制失败:', err);
this.fallbackCopy(wechatId);
});
} else {
this.fallbackCopy(wechatId);
}
},
fallbackCopy(text) {
// 降级方案:使用传统方法
const textarea = document.createElement('textarea');
textarea.value = text;
textarea.style.position = 'fixed';
textarea.style.opacity = '0';
document.body.appendChild(textarea);
textarea.select();
try {
document.execCommand('copy');
this.showCopyNotification();
} catch (err) {
console.error('复制失败:', err);
}
document.body.removeChild(textarea);
},
showCopyNotification() {
// 创建临时提示
const notification = document.createElement('div');
notification.textContent = '已复制微信号';
notification.style.cssText = `
position: fixed;
bottom: 80px;
left: 50%;
transform: translateX(-50%);
background: #00ff41;
color: #000000;
padding: 8px 16px;
border-radius: 2px;
font-size: 13px;
font-weight: 500;
z-index: 10000;
animation: fadeInOut 2s ease;
`;
document.body.appendChild(notification);
setTimeout(() => {
document.body.removeChild(notification);
}, 2000);
},
copyMessage(content) {
// 移除HTML标签只保留纯文本
const tempDiv = document.createElement('div');
tempDiv.innerHTML = marked.parse(content);
const plainText = tempDiv.textContent || tempDiv.innerText;
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(plainText).then(() => {
this.showNotification('已复制内容');
}).catch(err => {
console.error('复制失败:', err);
this.fallbackCopy(plainText);
});
} else {
this.fallbackCopy(plainText);
}
},
async generateShareImage(content, index) {
try {
this.showNotification('正在生成分享图...');
// 创建临时容器
const container = document.createElement('div');
container.className = 'share-image-container';
container.style.left = '-9999px';
// 构建分享图内容
container.innerHTML = `
<div class="share-image-header">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none">
<path d="M3 13h8V3H3v10zm0 8h8v-6H3v6zm10 0h8V11h-8v10zm0-18v6h8V3h-8z" fill="#00ff41"/>
</svg>
<div class="share-image-logo">龙哥的 AI 金融智能体</div>
</div>
<div class="share-image-content">${marked.parse(content)}</div>
<div class="share-image-footer">
由 AI 智能分析生成 | 仅供参考,不构成投资建议
</div>
`;
document.body.appendChild(container);
// 等待渲染
await new Promise(resolve => setTimeout(resolve, 100));
// 生成图片
const canvas = await html2canvas(container, {
backgroundColor: '#0a0a0a',
scale: 2,
logging: false,
useCORS: true
});
// 移除临时容器
document.body.removeChild(container);
// 显示图片在模态框中
this.modalImageUrl = canvas.toDataURL('image/png');
this.showImageModal = true;
this.showNotification('长按图片可保存');
} catch (error) {
console.error('生成分享图失败:', error);
this.showNotification('生成失败,请重试');
}
},
closeImageModal() {
this.showImageModal = false;
this.modalImageUrl = '';
},
showNotification(text) {
const notification = document.createElement('div');
notification.textContent = text;
notification.style.cssText = `
position: fixed;
bottom: 80px;
left: 50%;
transform: translateX(-50%);
background: #00ff41;
color: #000000;
padding: 8px 16px;
border-radius: 2px;
font-size: 13px;
font-weight: 500;
z-index: 10000;
animation: fadeInOut 2s ease;
`;
document.body.appendChild(notification);
setTimeout(() => {
if (document.body.contains(notification)) {
document.body.removeChild(notification);
}
}, 2000);
},
async loadModels() {
try {
const response = await fetch('/api/llm/models');
const data = await response.json();
if (data.success) {
this.availableModels = data.models;
this.currentModel = data.current;
this.selectedModel = data.current ? data.current.provider : null;
}
} catch (error) {
console.error('加载模型列表失败:', error);
}
},
async switchModel() {
if (!this.selectedModel) return;
try {
const response = await fetch('/api/llm/switch', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
provider: this.selectedModel
})
});
const data = await response.json();
if (data.success) {
this.currentModel = data.current;
this.showNotification(`已切换到 ${data.current.name}`);
} else {
this.showNotification('切换失败');
}
} catch (error) {
console.error('切换模型失败:', error);
this.showNotification('切换失败');
}
}
},
watch: {
userInput() {
this.$nextTick(() => {
this.autoResizeTextarea();
});
}
}
}).mount('#app');