stock-ai-agent/frontend/index.html
2026-03-28 23:28:54 +08:00

1108 lines
36 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>TradusAI 金融智能体</title>
<!-- 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=DM+Sans:wght@300;400;500;600;700&family=DM+Serif+Display&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
<!-- Marked.js for Markdown rendering -->
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<!-- Styles -->
<link rel="stylesheet" href="/static/css/style.css?v=3">
<style>
/* ========================================
INDEX PAGE - ADVANCED STYLING
======================================== */
/* === ATMOSPHERIC BACKGROUND === */
body {
background: var(--bg-primary);
position: relative;
overflow: hidden;
}
body::before {
content: '';
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-image:
radial-gradient(circle at 15% 20%, rgba(102, 126, 234, 0.15) 0%, transparent 50%),
radial-gradient(circle at 85% 70%, rgba(118, 75, 162, 0.12) 0%, transparent 50%),
radial-gradient(circle at 50% 90%, rgba(0, 240, 255, 0.08) 0%, transparent 40%);
pointer-events: none;
z-index: 0;
animation: backgroundPulse 8s ease-in-out infinite;
}
@keyframes backgroundPulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.7; }
}
#app {
position: relative;
z-index: 1;
}
/* === GLASSMORPHISM HEADER === */
.header {
background: rgba(10, 14, 39, 0.85);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border-bottom: 1px solid rgba(0, 240, 255, 0.2);
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.4);
position: sticky;
top: 0;
z-index: 100;
animation: slideDown 0.5s ease-out;
}
@keyframes slideDown {
from {
opacity: 0;
transform: translateY(-20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.logo svg {
filter: drop-shadow(0 0 8px rgba(0, 240, 255, 0.5));
transition: filter 0.3s ease, transform 0.3s ease;
}
.logo:hover svg {
filter: drop-shadow(0 0 12px rgba(0, 240, 255, 0.8));
transform: rotate(5deg) scale(1.05);
}
.logo span {
background: linear-gradient(135deg, #00F0FF 0%, #00C9FF 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
font-weight: 600;
letter-spacing: 0.5px;
}
.model-selector {
background: rgba(26, 31, 58, 0.6);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 20px;
padding: 8px 16px;
backdrop-filter: blur(10px);
transition: all 0.3s ease;
}
.model-selector:hover {
background: rgba(26, 31, 58, 0.8);
border-color: rgba(0, 240, 255, 0.3);
box-shadow: 0 0 20px rgba(0, 240, 255, 0.2);
}
.model-selector svg {
color: var(--accent);
animation: rotate 4s linear infinite;
}
@keyframes rotate {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
.model-name {
font-family: 'JetBrains Mono', monospace;
font-size: 13px;
color: var(--text-primary);
}
.logout-btn {
background: rgba(255, 0, 64, 0.1);
border: 1px solid rgba(255, 0, 64, 0.3);
border-radius: 50%;
width: 36px;
height: 36px;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s ease;
position: relative;
overflow: hidden;
}
.logout-btn::before {
content: '';
position: absolute;
top: 50%;
left: 50%;
width: 0;
height: 0;
border-radius: 50%;
background: rgba(255, 0, 64, 0.3);
transition: all 0.4s ease;
transform: translate(-50%, -50%);
}
.logout-btn:hover::before {
width: 100%;
height: 100%;
}
.logout-btn:hover {
background: rgba(255, 0, 64, 0.2);
border-color: rgba(255, 0, 64, 0.5);
box-shadow: 0 0 16px rgba(255, 0, 64, 0.3);
transform: scale(1.05);
}
.logout-btn svg {
position: relative;
z-index: 1;
}
/* === WELCOME SCREEN === */
.welcome {
animation: fadeInUp 0.8s ease-out;
}
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(40px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.welcome-icon {
position: relative;
margin-bottom: 24px;
}
.welcome-icon::before {
content: '';
position: absolute;
top: 50%;
left: 50%;
width: 120px;
height: 120px;
background: radial-gradient(circle, rgba(0, 240, 255, 0.2) 0%, transparent 70%);
border-radius: 50%;
transform: translate(-50%, -50%);
animation: pulse 3s ease-in-out infinite;
z-index: -1;
}
@keyframes pulse {
0%, 100% {
opacity: 0.6;
transform: translate(-50%, -50%) scale(1);
}
50% {
opacity: 0.3;
transform: translate(-50%, -50%) scale(1.3);
}
}
.welcome-icon svg {
color: var(--accent);
filter: drop-shadow(0 0 16px rgba(0, 240, 255, 0.6));
animation: float 4s ease-in-out infinite;
}
@keyframes float {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-10px); }
}
.welcome h1 {
background: linear-gradient(135deg, #667EEA 0%, #764BA2 50%, #00F0FF 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
font-family: 'DM Serif Display', serif;
font-size: 52px;
margin-bottom: 16px;
position: relative;
display: inline-block;
}
.welcome h1::after {
content: '';
position: absolute;
bottom: -8px;
left: 0;
width: 100%;
height: 3px;
background: linear-gradient(90deg, #667EEA 0%, #764BA2 50%, #00F0FF 100%);
border-radius: 2px;
animation: shimmer 2s ease-in-out infinite;
}
@keyframes shimmer {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
.welcome-subtitle {
color: var(--text-secondary);
font-size: 18px;
margin-bottom: 48px;
letter-spacing: 1px;
animation: fadeIn 1s ease-out 0.3s backwards;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.guide-section {
animation: fadeIn 1s ease-out 0.6s backwards;
}
.example-queries {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
gap: 12px;
max-width: 900px;
margin: 0 auto 40px;
}
.example-btn {
background: rgba(26, 31, 58, 0.6);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 12px;
padding: 16px 20px;
color: var(--text-primary);
font-size: 14px;
cursor: pointer;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
position: relative;
overflow: hidden;
font-weight: 500;
}
.example-btn::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: linear-gradient(135deg, rgba(102, 126, 234, 0.2) 0%, rgba(118, 75, 162, 0.2) 100%);
opacity: 0;
transition: opacity 0.3s ease;
}
.example-btn::after {
content: '';
position: absolute;
top: 50%;
left: 50%;
width: 0;
height: 0;
border-radius: 50%;
background: rgba(0, 240, 255, 0.3);
transform: translate(-50%, -50%);
transition: width 0.4s ease, height 0.4s ease;
}
.example-btn:hover {
background: rgba(26, 31, 58, 0.9);
border-color: rgba(0, 240, 255, 0.4);
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4), 0 0 20px rgba(0, 240, 255, 0.3);
transform: translateY(-4px) scale(1.02);
}
.example-btn:hover::before {
opacity: 1;
}
.example-btn:active::after {
width: 300%;
height: 300%;
}
.welcome-footer {
margin-top: 48px;
animation: fadeIn 1s ease-out 0.9s backwards;
}
.welcome-footer p {
color: var(--text-tertiary);
font-size: 14px;
opacity: 0.8;
}
/* === MESSAGES AREA === */
.messages {
padding-bottom: 20px;
}
.message {
animation: messageSlideIn 0.4s ease-out;
}
@keyframes messageSlideIn {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.message.user .message-content {
background: linear-gradient(135deg, #667EEA 0%, #764BA2 100%);
box-shadow: 0 4px 16px rgba(102, 126, 234, 0.4);
position: relative;
overflow: hidden;
}
.message.user .message-content::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent 0%, rgba(255, 255, 255, 0.1) 50%, transparent 100%);
transform: translateX(-100%);
animation: shine 3s ease-in-out infinite;
}
@keyframes shine {
0%, 100% { transform: translateX(-100%); }
50% { transform: translateX(100%); }
}
.message.assistant .message-content {
background: rgba(26, 31, 58, 0.6);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.1);
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3);
transition: all 0.3s ease;
}
.message.assistant .message-content:hover {
background: rgba(26, 31, 58, 0.8);
border-color: rgba(0, 240, 255, 0.2);
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4), 0 0 20px rgba(0, 240, 255, 0.1);
transform: translateY(-2px);
}
.markdown {
line-height: 1.8;
}
.markdown h1, .markdown h2, .markdown h3 {
background: linear-gradient(135deg, #00F0FF 0%, #00C9FF 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
margin-top: 24px;
margin-bottom: 12px;
}
.markdown code {
background: rgba(0, 240, 255, 0.1);
border: 1px solid rgba(0, 240, 255, 0.2);
border-radius: 4px;
padding: 2px 8px;
font-family: 'JetBrains Mono', monospace;
font-size: 13px;
}
.message-actions {
margin-top: 16px;
padding-top: 12px;
border-top: 1px solid rgba(255, 255, 255, 0.1);
display: flex;
gap: 12px;
opacity: 0;
animation: fadeIn 0.3s ease-out 0.2s forwards;
}
.action-btn {
background: rgba(26, 31, 58, 0.6);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 8px;
padding: 8px 16px;
color: var(--text-secondary);
font-size: 13px;
cursor: pointer;
transition: all 0.3s ease;
display: flex;
align-items: center;
gap: 6px;
font-weight: 500;
}
.action-btn:hover {
background: rgba(0, 240, 255, 0.1);
border-color: rgba(0, 240, 255, 0.3);
color: var(--accent);
box-shadow: 0 0 16px rgba(0, 240, 255, 0.2);
transform: translateY(-2px);
}
.action-btn svg {
transition: transform 0.3s ease;
}
.action-btn:hover svg {
transform: scale(1.1);
}
/* === STREAMING INDICATOR === */
.streaming-indicator {
display: flex;
gap: 6px;
margin-top: 12px;
padding: 8px 0;
}
.dot {
width: 8px;
height: 8px;
background: var(--accent);
border-radius: 50%;
animation: bounce 1.4s ease-in-out infinite;
box-shadow: 0 0 8px rgba(0, 240, 255, 0.6);
}
.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 AREA === */
.input-container {
background: rgba(10, 14, 39, 0.8);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border-top: 1px solid rgba(0, 240, 255, 0.2);
padding: 20px;
box-shadow: 0 -4px 24px rgba(0, 0, 0, 0.4);
position: relative;
}
.input-container::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 1px;
background: linear-gradient(90deg, transparent 0%, #00F0FF 50%, transparent 100%);
animation: glowMove 3s ease-in-out infinite;
}
@keyframes glowMove {
0%, 100% { opacity: 0.5; }
50% { opacity: 1; }
}
.input-wrapper {
background: rgba(26, 31, 58, 0.6);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 16px;
padding: 12px 16px;
display: flex;
gap: 12px;
align-items: flex-end;
transition: all 0.3s ease;
position: relative;
}
.input-wrapper:focus-within {
background: rgba(26, 31, 58, 0.9);
border-color: rgba(0, 240, 255, 0.4);
box-shadow: 0 0 24px rgba(0, 240, 255, 0.2), inset 0 0 24px rgba(0, 240, 255, 0.05);
}
textarea {
background: transparent;
border: none;
color: var(--text-primary);
font-size: 15px;
resize: none;
flex: 1;
line-height: 1.5;
font-family: 'DM Sans', sans-serif;
}
textarea:focus {
outline: none;
}
textarea::placeholder {
color: var(--text-tertiary);
opacity: 0.6;
}
.send-btn {
background: linear-gradient(135deg, #00F0FF 0%, #00C9FF 100%);
border: none;
border-radius: 50%;
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
position: relative;
overflow: hidden;
flex-shrink: 0;
}
.send-btn::before {
content: '';
position: absolute;
top: 50%;
left: 50%;
width: 0;
height: 0;
border-radius: 50%;
background: rgba(255, 255, 255, 0.3);
transform: translate(-50%, -50%);
transition: width 0.4s ease, height 0.4s ease;
}
.send-btn:hover:not(:disabled) {
transform: scale(1.1);
box-shadow: 0 0 24px rgba(0, 240, 255, 0.5);
}
.send-btn:active:not(:disabled)::before {
width: 200%;
height: 200%;
}
.send-btn:disabled {
opacity: 0.4;
cursor: not-allowed;
}
.send-btn svg {
color: var(--bg-primary);
position: relative;
z-index: 1;
transition: transform 0.3s ease;
}
.send-btn:hover:not(:disabled) svg {
transform: translateX(2px) translateY(-2px);
}
.spinner {
width: 20px;
height: 20px;
border: 2px solid rgba(10, 14, 39, 0.3);
border-top-color: var(--bg-primary);
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.footer-info {
text-align: center;
margin-top: 12px;
}
.contact-link {
color: var(--text-tertiary);
font-size: 13px;
cursor: pointer;
transition: all 0.3s ease;
display: inline-block;
}
.contact-link:hover {
color: var(--accent);
text-shadow: 0 0 8px rgba(0, 240, 255, 0.6);
}
/* === MODALS === */
.image-modal, .contact-modal {
background: rgba(10, 14, 39, 0.95);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
animation: fadeIn 0.3s ease-out;
}
.image-modal-content, .contact-modal-content {
background: rgba(26, 31, 58, 0.95);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 16px;
padding: 32px;
box-shadow: 0 16px 64px rgba(0, 0, 0, 0.6), 0 0 40px rgba(0, 240, 255, 0.2);
animation: scaleIn 0.4s cubic-bezier(0.4, 0, 0.2, 1);
position: relative;
max-width: 90%;
}
@keyframes scaleIn {
from {
opacity: 0;
transform: scale(0.9) translateY(20px);
}
to {
opacity: 1;
transform: scale(1) translateY(0);
}
}
.modal-close-btn {
position: absolute;
top: 16px;
right: 16px;
background: rgba(255, 0, 64, 0.1);
border: 1px solid rgba(255, 0, 64, 0.3);
border-radius: 50%;
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.3s ease;
z-index: 10;
}
.modal-close-btn:hover {
background: rgba(255, 0, 64, 0.2);
border-color: rgba(255, 0, 64, 0.5);
box-shadow: 0 0 16px rgba(255, 0, 64, 0.3);
transform: rotate(90deg) scale(1.1);
}
.modal-close-btn svg {
color: #ff0040;
}
.modal-image {
border-radius: 12px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
max-width: 100%;
height: auto;
display: block;
}
.modal-hint, .contact-hint {
color: var(--text-tertiary);
font-size: 13px;
text-align: center;
margin-top: 16px;
opacity: 0.8;
}
.contact-title {
background: linear-gradient(135deg, #00F0FF 0%, #00C9FF 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
font-family: 'DM Serif Display', serif;
font-size: 28px;
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: rgba(0, 240, 255, 0.05);
border: 1px solid rgba(0, 240, 255, 0.2);
border-radius: 12px;
width: 100%;
max-width: 400px;
}
.contact-item svg {
color: var(--accent);
flex-shrink: 0;
}
.contact-item span {
color: var(--text-primary);
font-size: 15px;
}
.contact-item strong {
background: linear-gradient(135deg, #00F0FF 0%, #00C9FF 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
font-family: 'JetBrains Mono', monospace;
}
.copy-btn {
background: linear-gradient(135deg, rgba(102, 126, 234, 0.2) 0%, rgba(118, 75, 162, 0.2) 100%);
border: 1px solid rgba(0, 240, 255, 0.3);
border-radius: 12px;
padding: 12px 24px;
color: var(--accent);
font-size: 14px;
cursor: pointer;
transition: all 0.3s ease;
display: flex;
align-items: center;
gap: 8px;
font-weight: 500;
}
.copy-btn:hover {
background: linear-gradient(135deg, rgba(102, 126, 234, 0.3) 0%, rgba(118, 75, 162, 0.3) 100%);
border-color: rgba(0, 240, 255, 0.5);
box-shadow: 0 0 24px rgba(0, 240, 255, 0.3);
transform: translateY(-2px);
}
.copy-btn svg {
transition: transform 0.3s ease;
}
.copy-btn:hover svg {
transform: scale(1.1);
}
/* === RESPONSIVE === */
@media (max-width: 768px) {
.welcome h1 {
font-size: 36px;
}
.welcome-subtitle {
font-size: 16px;
}
.example-queries {
grid-template-columns: repeat(2, 1fr);
gap: 10px;
}
.example-btn {
padding: 12px 16px;
font-size: 13px;
}
.header {
padding: 12px 16px;
}
.logo span {
font-size: 14px;
}
.model-selector {
padding: 6px 12px;
}
.model-name {
font-size: 12px;
}
.input-wrapper {
padding: 10px 12px;
}
textarea {
font-size: 14px;
}
.send-btn {
width: 36px;
height: 36px;
}
.contact-item {
flex-direction: column;
align-items: flex-start;
padding: 12px;
}
.copy-btn {
width: 100%;
justify-content: center;
}
}
@media (max-width: 480px) {
.welcome h1 {
font-size: 28px;
}
.example-queries {
grid-template-columns: 1fr;
}
.header-right {
gap: 8px;
}
.model-selector {
display: none;
}
}
/* === CHART BOX === */
.chart-box {
margin-top: 16px;
background: rgba(10, 14, 39, 0.6);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 12px;
padding: 16px;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3);
}
.chart {
width: 100%;
height: 400px;
border-radius: 8px;
}
</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>TradusAI金融智能体</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 class="model-name">{{ currentModel.name }}</span>
</div>
<!-- Logout Button -->
<button class="logout-btn" @click="logout" title="登出" aria-label="登出">
<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="guide-section">
<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>
<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 for AI Messages (只在流式输出完成后显示) -->
<div v-if="!msg.streaming" class="message-actions">
<button class="action-btn" @click.stop="copyMessage(msg.content)" title="复制内容" aria-label="复制内容">
<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>
<span>复制</span>
</button>
<button class="action-btn" @click.stop="generateShareImage(msg.content, index)" title="生成分享图" aria-label="生成分享图">
<svg width="16" height="16" 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" role="dialog" aria-modal="true" aria-label="分享图预览">
<div class="image-modal-content" @click.stop>
<button class="modal-close-btn" @click="closeImageModal" title="关闭" aria-label="关闭分享图">
<svg width="24" height="24" 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" role="dialog" aria-modal="true" aria-label="联系作者">
<div class="contact-modal-content" @click.stop>
<button class="modal-close-btn" @click="showContactModal = false" title="关闭" aria-label="关闭联系方式">
<svg width="24" height="24" 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>