stock-ai-agent/frontend/index.html
2026-04-22 11:03:24 +08:00

681 lines
23 KiB
HTML
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.

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>XClawAI 金融智能体</title>
<!-- Global Styles -->
<link rel="stylesheet" href="/static/css/style.css">
<!-- Fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@400;500;600;700&family=IBM+Plex+Mono:wght@400;500;600&display=swap" rel="stylesheet">
<!-- Marked.js for Markdown rendering -->
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<!-- Page-Specific Styles -->
<style>
#app {
height: 100vh;
display: flex;
flex-direction: column;
}
/* Header */
.header {
background: var(--panel-strong);
border-bottom: 1px solid var(--border);
padding: 16px 24px;
display: flex;
justify-content: space-between;
align-items: center;
backdrop-filter: blur(18px);
box-shadow: var(--shadow-soft);
}
.logo {
display: flex;
align-items: center;
gap: 12px;
font-size: 18px;
font-weight: 600;
color: var(--text-primary);
}
.logo svg {
color: var(--primary);
}
.header-right {
display: flex;
align-items: center;
gap: 16px;
}
.model-selector {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 16px;
background: rgba(126, 200, 255, 0.08);
border-radius: 14px;
border: 1px solid var(--border);
font-size: 13px;
color: var(--text-secondary);
}
.logout-btn {
width: 36px;
height: 36px;
display: flex;
align-items: center;
justify-content: center;
background: rgba(126, 200, 255, 0.08);
border: none;
border-radius: 12px;
color: var(--text-secondary);
cursor: pointer;
transition: all 0.2s;
border: 1px solid var(--border);
}
.logout-btn:hover {
background: var(--error-light);
color: var(--error);
}
/* Chat Container */
.chat-container {
flex: 1;
overflow-y: auto;
padding: 24px;
}
/* Welcome */
.welcome {
text-align: center;
padding: 80px 24px;
max-width: 800px;
margin: 0 auto;
}
.welcome-icon {
margin-bottom: 32px;
}
.welcome-icon svg {
width: 64px;
height: 64px;
color: var(--primary);
}
.welcome h1 {
font-size: 48px;
font-weight: 700;
color: var(--text-primary);
margin-bottom: 16px;
letter-spacing: -0.04em;
}
.welcome-subtitle {
font-size: 18px;
color: var(--text-secondary);
margin-bottom: 48px;
}
.example-queries {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
gap: 12px;
max-width: 700px;
margin: 0 auto 32px;
}
.example-btn {
padding: 14px 20px;
background: var(--bg-secondary);
border: 1px solid var(--border);
border-radius: 14px;
font-size: 14px;
color: var(--text-primary);
cursor: pointer;
transition: all 0.2s;
box-shadow: var(--shadow-soft);
}
.example-btn:hover {
background: rgba(126, 200, 255, 0.14);
border-color: var(--primary);
color: var(--primary);
}
.welcome-footer {
font-size: 14px;
color: var(--text-tertiary);
}
/* Messages */
.messages {
max-width: 800px;
margin: 0 auto;
}
.message {
margin-bottom: 24px;
}
.message-content {
padding: 16px 20px;
border-radius: 18px;
box-shadow: var(--shadow-soft);
backdrop-filter: blur(14px);
}
.message.user .message-content {
background: linear-gradient(135deg, rgba(126, 200, 255, 0.26), rgba(99, 230, 190, 0.18));
color: var(--text-primary);
margin-left: 48px;
border: 1px solid rgba(126, 200, 255, 0.22);
}
.message.assistant .message-content {
background: var(--bg-secondary);
border: 1px solid var(--border);
color: var(--text-primary);
margin-right: 48px;
}
.message-actions {
margin-top: 12px;
display: flex;
gap: 8px;
}
.action-btn {
padding: 6px 12px;
background: rgba(126, 200, 255, 0.08);
border: 1px solid var(--border);
border-radius: 12px;
font-size: 13px;
color: var(--text-secondary);
cursor: pointer;
display: flex;
align-items: center;
gap: 6px;
transition: all 0.2s;
}
.action-btn:hover {
background: rgba(126, 200, 255, 0.14);
color: var(--primary);
border-color: var(--primary);
}
/* Streaming Indicator */
.streaming-indicator {
display: flex;
gap: 4px;
padding: 12px 0;
}
.dot {
width: 6px;
height: 6px;
background: var(--primary);
border-radius: 50%;
animation: bounce 1.4s ease-in-out infinite;
}
.dot:nth-child(1) { animation-delay: 0s; }
.dot:nth-child(2) { animation-delay: 0.2s; }
.dot:nth-child(3) { animation-delay: 0.4s; }
@keyframes bounce {
0%, 80%, 100% { transform: scale(0.8); opacity: 0.5; }
40% { transform: scale(1.2); opacity: 1; }
}
/* Input */
.input-container {
border-top: 1px solid var(--border);
padding: 20px 24px;
background: var(--panel-strong);
backdrop-filter: blur(18px);
}
.input-wrapper {
max-width: 800px;
margin: 0 auto;
display: flex;
gap: 12px;
align-items: flex-end;
}
textarea {
flex: 1;
padding: 12px 16px;
background: rgba(10, 20, 31, 0.78);
border: 1px solid var(--border);
border-radius: 16px;
font-size: 15px;
font-family: inherit;
color: var(--text-primary);
resize: none;
transition: all 0.2s;
}
textarea:focus {
outline: none;
background: rgba(12, 24, 36, 0.92);
border-color: var(--primary);
}
textarea::placeholder {
color: var(--text-tertiary);
}
.send-btn {
width: 40px;
height: 40px;
background: linear-gradient(135deg, #8fd7ff, #63e6be);
border: none;
border-radius: 14px;
color: #071018;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s;
flex-shrink: 0;
box-shadow: 0 10px 24px rgba(49, 132, 189, 0.24);
}
.send-btn:hover:not(:disabled) {
transform: translateY(-1px);
}
.send-btn:disabled {
opacity: 0.4;
cursor: not-allowed;
}
.footer-info {
text-align: center;
margin-top: 12px;
font-size: 13px;
color: var(--text-tertiary);
}
.contact-link {
cursor: pointer;
color: var(--primary);
transition: all 0.2s;
}
.contact-link:hover {
text-decoration: underline;
}
/* Modals */
.image-modal, .contact-modal {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(4, 9, 14, 0.78);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
backdrop-filter: blur(10px);
}
.image-modal-content, .contact-modal-content {
background: var(--panel-strong);
border-radius: 20px;
padding: 32px;
max-width: 90%;
position: relative;
border: 1px solid var(--border);
box-shadow: var(--shadow);
}
.modal-close-btn {
position: absolute;
top: 16px;
right: 16px;
width: 32px;
height: 32px;
background: rgba(126, 200, 255, 0.08);
border: none;
border-radius: 12px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
color: var(--text-secondary);
transition: all 0.2s;
border: 1px solid var(--border);
}
.modal-close-btn:hover {
background: var(--error-light);
color: var(--error);
}
.contact-title {
font-size: 24px;
font-weight: 600;
margin-bottom: 24px;
text-align: center;
}
.contact-info {
display: flex;
flex-direction: column;
gap: 16px;
align-items: center;
}
.contact-item {
display: flex;
align-items: center;
gap: 12px;
padding: 16px;
background: var(--bg-secondary);
border-radius: 16px;
border: 1px solid var(--border);
width: 100%;
max-width: 400px;
}
.contact-item svg {
color: var(--primary);
}
.copy-btn {
padding: 12px 24px;
background: linear-gradient(135deg, #8fd7ff, #63e6be);
border: none;
border-radius: 14px;
color: #071018;
font-size: 14px;
font-weight: 500;
cursor: pointer;
display: flex;
align-items: center;
gap: 8px;
transition: all 0.2s;
}
.copy-btn:hover {
transform: translateY(-1px);
}
.modal-image {
border-radius: 16px;
max-width: 100%;
display: block;
}
.modal-hint, .contact-hint {
text-align: center;
color: var(--text-tertiary);
font-size: 13px;
margin-top: 16px;
}
/* Markdown */
.markdown {
line-height: 1.8;
}
.markdown h1, .markdown h2, .markdown h3 {
margin-top: 24px;
margin-bottom: 12px;
color: var(--text-primary);
}
.markdown code {
background: rgba(10, 20, 31, 0.82);
border: 1px solid var(--border);
border-radius: 10px;
padding: 2px 8px;
font-family: "IBM Plex Mono", monospace;
font-size: 13px;
}
.chart-box {
margin-top: 16px;
background: var(--bg-secondary);
border: 1px solid var(--border);
border-radius: 16px;
padding: 16px;
}
.chart {
width: 100%;
height: 400px;
border-radius: 14px;
}
/* Responsive */
@media (max-width: 768px) {
.welcome h1 {
font-size: 36px;
}
.welcome-subtitle {
font-size: 16px;
}
.example-queries {
grid-template-columns: repeat(2, 1fr);
}
.message.user .message-content {
margin-left: 0;
}
.message.assistant .message-content {
margin-right: 0;
}
}
</style>
</head>
<body>
<div id="app">
<!-- Main Container -->
<div class="container">
<!-- Header -->
<header class="header">
<div class="logo">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none">
<path d="M3 13h8V3H3v10zm0 8h8v-6H3v6zm10 0h8V11h-8v10zm0-18v6h8V3h-8z" fill="currentColor"/>
</svg>
<span>XClaw AI</span>
</div>
<div class="header-right">
<!-- Model Display -->
<div class="model-selector" v-if="currentModel">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="3"/>
<path d="M12 1v6m0 6v6M5.64 5.64l4.24 4.24m4.24 4.24l4.24 4.24M1 12h6m6 0h6M5.64 18.36l4.24-4.24m4.24-4.24l4.24-4.24"/>
</svg>
<span>{{ currentModel.name }}</span>
</div>
<!-- Logout Button -->
<button class="logout-btn" @click="logout" title="登出">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/>
<polyline points="16 17 21 12 16 7"/>
<line x1="21" y1="12" x2="9" y2="12"/>
</svg>
</button>
</div>
</header>
<!-- Chat Area -->
<div class="chat-container" ref="chatContainer">
<!-- Welcome Screen -->
<div v-if="messages.length === 0" class="welcome">
<div class="welcome-icon">
<svg width="60" height="60" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1">
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/>
</svg>
</div>
<h1>AI 金融智能体</h1>
<p class="welcome-subtitle">支持 A股 · 美股 · 港股 三大市场分析</p>
<div class="example-queries">
<button class="example-btn" @click="sendExample('分析贵州茅台')">分析贵州茅台</button>
<button class="example-btn" @click="sendExample('比亚迪怎么样')">比亚迪怎么样</button>
<button class="example-btn" @click="sendExample('分析特斯拉')">分析特斯拉</button>
<button class="example-btn" @click="sendExample('苹果股票')">苹果股票</button>
<button class="example-btn" @click="sendExample('港股腾讯')">港股腾讯</button>
<button class="example-btn" @click="sendExample('分析小米')">分析小米</button>
</div>
<div class="welcome-footer">
<p>💬 输入股票名称或代码,支持 A股/美股/港股</p>
</div>
</div>
<!-- Messages -->
<div v-else class="messages">
<div v-for="(msg, index) in messages" :key="index"
:class="['message', msg.role]">
<div class="message-content">
<div v-if="msg.role === 'user'" class="text">{{ msg.content }}</div>
<div v-else>
<div class="text markdown" v-html="renderMarkdown(msg.content)"></div>
<!-- Action Buttons -->
<div v-if="!msg.streaming" class="message-actions">
<button class="action-btn" @click.stop="copyMessage(msg.content)">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"/>
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/>
</svg>
<span>复制</span>
</button>
<button class="action-btn" @click.stop="generateShareImage(msg.content, index)">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"/>
<circle cx="8.5" cy="8.5" r="1.5"/>
<polyline points="21 15 16 10 5 21"/>
</svg>
<span>分享图</span>
</button>
</div>
<!-- Streaming Indicator -->
<div v-if="msg.streaming" class="streaming-indicator">
<span class="dot"></span>
<span class="dot"></span>
<span class="dot"></span>
</div>
<!-- Chart Display -->
<div v-if="msg.metadata && msg.metadata.type === 'chart'" class="chart-box">
<div :id="'chart-' + index" class="chart"></div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Input Area -->
<div class="input-container">
<div class="input-wrapper">
<textarea
v-model="userInput"
@keydown.enter.exact.prevent="sendMessage"
placeholder="输入股票名称或代码A股/美股/港股)..."
rows="1"
:disabled="loading"
ref="textarea"
></textarea>
<button
class="send-btn"
@click="sendMessage"
:disabled="loading || !userInput.trim()"
>
<svg v-if="!loading" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="22" y1="2" x2="11" y2="13"/>
<polygon points="22 2 15 22 11 13 2 9 22 2"/>
</svg>
<div v-else class="spinner"></div>
</button>
</div>
<!-- Footer Info -->
<div class="footer-info">
<span class="contact-link" @click="showContactModal = true">联系作者</span>
</div>
</div>
</div>
<!-- Image Modal -->
<div v-if="showImageModal" class="image-modal" @click="closeImageModal">
<div class="image-modal-content" @click.stop>
<button class="modal-close-btn" @click="closeImageModal">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="18" y1="6" x2="6" y2="18"/>
<line x1="6" y1="6" x2="18" y2="18"/>
</svg>
</button>
<img :src="modalImageUrl" alt="分享图" class="modal-image">
<p class="modal-hint">长按图片可保存到相册</p>
</div>
</div>
<!-- Contact Modal -->
<div v-if="showContactModal" class="contact-modal" @click="showContactModal = false">
<div class="contact-modal-content" @click.stop>
<button class="modal-close-btn" @click="showContactModal = false">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="18" y1="6" x2="6" y2="18"/>
<line x1="6" y1="6" x2="18" y2="18"/>
</svg>
</button>
<h3 class="contact-title">联系作者</h3>
<div class="contact-info">
<div class="contact-item">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/>
</svg>
<span>微信号:<strong>aaronlzhou</strong></span>
</div>
<button class="copy-btn" @click="copyWechat">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"/>
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/>
</svg>
复制微信号
</button>
</div>
<p class="contact-hint">欢迎交流讨论股票分析和AI技术</p>
</div>
</div>
</div>
<!-- Vue 3 -->
<script src="https://cdn.jsdelivr.net/npm/vue@3/dist/vue.global.js"></script>
<!-- Lightweight Charts -->
<script src="https://cdn.jsdelivr.net/npm/lightweight-charts/dist/lightweight-charts.standalone.production.js"></script>
<!-- html2canvas for generating share images -->
<script src="https://cdn.jsdelivr.net/npm/html2canvas@1.4.1/dist/html2canvas.min.js"></script>
<!-- App Script -->
<script src="/static/js/app.js?v=4"></script>
</body>
</html>