feat: Add Retro Futurism report templates and components

This commit is contained in:
Saramanda9988
2025-12-31 13:56:35 +08:00
committed by Helian Nuits
parent 4ca8180933
commit 59b82a29c9
7 changed files with 2082 additions and 0 deletions
@@ -0,0 +1,15 @@
{% if chart_data %}
{% for item in chart_data %}
<div class="activity-row">
<div class="activity-hour">{{ "%02d"|format(item.hour) }}:00</div>
<div class="activity-meter">
<div class="activity-bar" style="--pct: {{ "%.2f"|format(item.percentage) }}%;"></div>
</div>
<div class="activity-count">{{ item.count }}</div>
</div>
{% endfor %}
{% else %}
<div class="activity-row">
<div class="topic-meta" style="grid-column: span 3;">NO_DATA_AVAILABLE</div>
</div>
{% endif %}
@@ -0,0 +1,735 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>Retro Futurism Daily Export</title>
<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=Oswald:wght@400;600&family=JetBrains+Mono:wght@400;600&family=Noto+Sans+SC:wght@400;500&display=swap"
rel="stylesheet">
<style>
@font-face {
font-family: "Oswald-Fallback";
src: local("Impact"), local("Arial Black"), local("Helvetica Neue Bold");
font-weight: 400 700;
}
@font-face {
font-family: "JetBrains-Fallback";
src: local("Consolas"), local("Monaco"), local("Courier New");
font-weight: 400 700;
}
@font-face {
font-family: "NotoSans-Fallback";
src: local("Microsoft YaHei"), local("PingFang SC"), local("Hiragino Sans GB"), local("SimHei");
font-weight: 400 700;
}
:root {
/* Palette */
--c-bg: #F5EDDC;
--c-text: #181818;
--c-accent: #B2653B;
--c-dec-1: #5C7F71;
--c-dec-2: #802520;
--c-dec-3: #BA8530;
--c-grid: rgba(24, 24, 24, 0.08);
/* Fonts */
--font-display: "Oswald", "Oswald-Fallback", Impact, "Arial Black", sans-serif;
--font-mono: "JetBrains Mono", "JetBrains-Fallback", Consolas, Monaco, "Courier New", monospace;
--font-body: "Noto Sans SC", "NotoSans-Fallback", "Microsoft YaHei", "PingFang SC", sans-serif;
}
* {
box-sizing: border-box;
}
body {
margin: 0;
background-color: var(--c-bg);
color: var(--c-text);
font-family: var(--font-body);
/* Grid Background */
background-image:
linear-gradient(var(--c-grid) 1px, transparent 1px),
linear-gradient(90deg, var(--c-grid) 1px, transparent 1px);
background-size: 40px 40px;
min-height: 100vh;
overflow-x: hidden;
}
/* Layout Container */
.layout {
max-width: 1200px;
margin: 0 auto;
padding: 60px 40px;
position: relative;
border-left: 1px solid var(--c-text);
border-right: 1px solid var(--c-text);
background:
linear-gradient(90deg, transparent 0%, transparent 98%, var(--c-accent) 98%, var(--c-accent) 100%);
background-size: 100% 100%;
min-height: 100vh;
}
/* Decorative Color Bar */
.color-bar {
position: absolute;
top: 60px;
left: 0;
display: flex;
flex-direction: column;
gap: 0;
z-index: 10;
}
.c-block {
width: 40px;
height: 10px;
}
.cb-1 { background: var(--c-dec-1); }
.cb-2 { background: var(--c-dec-2); }
.cb-3 { background: var(--c-dec-3); }
/* Header */
header {
margin-bottom: 60px;
position: relative;
}
.header-meta {
font-family: var(--font-mono);
font-size: 0.75rem;
color: var(--c-text);
opacity: 0.65;
margin-bottom: 12px;
letter-spacing: 0.15rem;
text-transform: uppercase;
}
h1 {
font-family: var(--font-display);
font-size: 4.5rem;
line-height: 0.9;
margin: 0 0 20px;
text-transform: uppercase;
letter-spacing: -0.1rem;
color: var(--c-text);
}
h1 .highlight {
color: var(--c-accent);
}
.header-brief {
max-width: 500px;
color: rgba(24, 24, 24, 0.7);
line-height: 1.6;
margin-bottom: 24px;
}
.header-meta-row {
display: flex;
gap: 30px;
}
.meta-block {
padding-left: 16px;
border-left: 3px solid var(--c-accent);
}
.meta-label {
font-family: var(--font-mono);
font-size: 0.7rem;
letter-spacing: 0.15rem;
text-transform: uppercase;
color: rgba(24, 24, 24, 0.5);
}
.meta-value {
display: block;
margin-top: 4px;
color: var(--c-text);
font-family: var(--font-mono);
font-size: 0.85rem;
}
/* CRT Console */
.crt-console {
background: var(--c-text);
color: var(--c-bg);
padding: 20px 24px;
font-family: var(--font-mono);
font-size: 0.85rem;
line-height: 1.6;
position: relative;
overflow: hidden;
grid-column: span 12;
}
.crt-console::before {
content: "CRT_CHANNEL // PEAK_ACTIVITY";
display: block;
border-bottom: 1px solid #444;
padding-bottom: 8px;
margin-bottom: 12px;
color: var(--c-accent);
font-size: 0.65rem;
letter-spacing: 0.2rem;
}
.crt-label {
font-size: 0.7rem;
letter-spacing: 0.2rem;
text-transform: uppercase;
color: rgba(245, 237, 220, 0.7);
}
.crt-value {
font-family: var(--font-display);
font-size: 2.5rem;
color: var(--c-accent);
margin: 8px 0;
}
.crt-sub {
font-size: 0.8rem;
color: rgba(245, 237, 220, 0.8);
}
/* Grid System */
.grid-container {
display: grid;
grid-template-columns: repeat(12, 1fr);
gap: 20px;
margin-bottom: 60px;
}
/* Section Labels */
.section-label {
grid-column: span 12;
font-family: var(--font-display);
font-size: 1.8rem;
text-transform: uppercase;
border-bottom: 2px solid var(--c-text);
padding-bottom: 8px;
margin-bottom: 20px;
display: flex;
justify-content: space-between;
align-items: flex-end;
letter-spacing: 0.1rem;
}
.section-label span {
background: var(--c-text);
color: var(--c-bg);
padding: 2px 10px;
font-size: 0.75rem;
font-family: var(--font-mono);
vertical-align: middle;
letter-spacing: 0.15rem;
}
/* Stat Box */
.stat-box {
grid-column: span 3;
border: 1px solid var(--c-text);
padding: 20px;
position: relative;
background: rgba(255, 255, 255, 0.5);
}
.stat-box::before {
content: '';
position: absolute;
top: 4px;
left: 4px;
right: 4px;
bottom: 4px;
border: 1px solid var(--c-grid);
pointer-events: none;
}
.stat-label {
font-family: var(--font-mono);
font-size: 0.75rem;
text-transform: uppercase;
color: var(--c-dec-1);
margin-bottom: 10px;
display: block;
letter-spacing: 0.15rem;
}
.stat-value {
font-family: var(--font-display);
font-size: 3rem;
color: var(--c-accent);
line-height: 1;
}
.stat-note {
font-size: 0.8rem;
color: rgba(24, 24, 24, 0.6);
margin-top: 8px;
}
/* Content Box (for topics, titles, quotes) */
.content-box {
grid-column: span 12;
border: 1px solid var(--c-text);
padding: 24px;
background: #fff;
position: relative;
}
.content-box::before {
content: '';
position: absolute;
top: 6px;
left: 6px;
right: 6px;
bottom: 6px;
border: 1px solid var(--c-grid);
pointer-events: none;
}
/* Activity Chart */
.activity-wrapper {
font-family: var(--font-mono);
font-size: 0.8rem;
}
.activity-row {
display: grid;
grid-template-columns: 60px 1fr 50px;
align-items: center;
gap: 12px;
padding: 6px 0;
border-bottom: 1px solid var(--c-grid);
}
.activity-row:last-child {
border-bottom: none;
}
.activity-hour {
letter-spacing: 0.1rem;
color: rgba(24, 24, 24, 0.6);
}
.activity-meter {
background: rgba(24, 24, 24, 0.08);
height: 8px;
border-radius: 2px;
overflow: hidden;
}
.activity-bar {
height: 100%;
width: var(--pct, 10%);
background: var(--c-accent);
}
.activity-count {
text-align: right;
color: rgba(24, 24, 24, 0.6);
}
/* Topic List */
.topic-list {
display: flex;
flex-direction: column;
gap: 20px;
}
.topic-item {
display: grid;
grid-template-columns: 50px 1fr;
gap: 20px;
padding-bottom: 16px;
border-bottom: 1px solid var(--c-grid);
}
.topic-item:last-child {
border-bottom: none;
padding-bottom: 0;
}
.topic-index {
font-family: var(--font-mono);
font-size: 1rem;
letter-spacing: 0.2rem;
color: rgba(24, 24, 24, 0.3);
}
.topic-title {
margin: 0 0 6px;
font-size: 1.1rem;
letter-spacing: 0.05rem;
color: var(--c-text);
}
.topic-meta {
font-family: var(--font-mono);
font-size: 0.7rem;
letter-spacing: 0.15rem;
color: rgba(24, 24, 24, 0.5);
margin-bottom: 8px;
}
.topic-detail {
color: rgba(24, 24, 24, 0.7);
line-height: 1.6;
font-size: 0.9rem;
}
/* User Title Grid */
.title-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 20px;
}
.title-item {
border: 1px solid var(--c-text);
padding: 18px;
position: relative;
background: rgba(255, 255, 255, 0.5);
}
.title-item::before {
content: '';
position: absolute;
top: 4px;
left: 4px;
width: 24px;
height: 4px;
background: var(--c-dec-1);
}
.title-item::after {
content: '';
position: absolute;
top: 4px;
left: 32px;
width: 24px;
height: 4px;
background: var(--c-dec-2);
}
.title-header {
display: flex;
align-items: center;
gap: 14px;
margin-bottom: 12px;
}
.title-avatar {
width: 52px;
height: 52px;
border-radius: 50%;
overflow: hidden;
border: 1px solid var(--c-text);
background: rgba(24, 24, 24, 0.08);
flex-shrink: 0;
}
.title-avatar img {
width: 100%;
height: 100%;
object-fit: cover;
filter: grayscale(50%) contrast(110%);
}
.title-avatar-fallback {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
font-family: var(--font-mono);
font-size: 0.7rem;
letter-spacing: 0.15rem;
color: rgba(24, 24, 24, 0.5);
}
.title-name {
font-weight: 600;
font-size: 1rem;
}
.title-mbti {
font-family: var(--font-mono);
font-size: 0.65rem;
letter-spacing: 0.15rem;
background: rgba(24, 24, 24, 0.08);
padding: 2px 8px;
margin-left: 8px;
}
.title-role {
font-family: var(--font-mono);
letter-spacing: 0.2rem;
text-transform: uppercase;
color: var(--c-accent);
margin: 8px 0;
font-size: 0.8rem;
}
.title-reason {
margin: 0;
font-size: 0.85rem;
color: rgba(24, 24, 24, 0.7);
line-height: 1.5;
}
/* Quote List */
.quote-list {
display: flex;
flex-direction: column;
gap: 16px;
}
.quote-item {
border: 1px dashed var(--c-text);
padding: 20px;
position: relative;
background: rgba(255, 255, 255, 0.3);
}
.quote-item::before {
content: '"';
font-family: var(--font-display);
font-size: 2rem;
color: var(--c-accent);
opacity: 0.5;
line-height: 1;
}
.quote-content {
font-size: 1rem;
color: var(--c-text);
margin: 8px 0;
line-height: 1.6;
}
.quote-author {
font-family: var(--font-mono);
font-size: 0.75rem;
letter-spacing: 0.15rem;
color: rgba(24, 24, 24, 0.6);
}
.quote-note {
font-size: 0.8rem;
color: rgba(24, 24, 24, 0.7);
margin-top: 8px;
}
/* Token Strip */
.token-strip {
grid-column: span 12;
border-top: 2px solid var(--c-text);
padding-top: 16px;
display: flex;
gap: 40px;
font-family: var(--font-mono);
font-size: 0.75rem;
letter-spacing: 0.1rem;
}
.token-label {
color: rgba(24, 24, 24, 0.5);
text-transform: uppercase;
}
/* Footer */
footer {
margin-top: 60px;
border-top: 3px solid var(--c-text);
padding-top: 16px;
display: flex;
justify-content: space-between;
font-family: var(--font-mono);
font-size: 0.7rem;
text-transform: uppercase;
letter-spacing: 0.15rem;
color: rgba(24, 24, 24, 0.6);
}
/* CRT Overlay */
.crt-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: linear-gradient(rgba(18, 16, 16, 0) 50%, rgba(0, 0, 0, 0.03) 50%);
background-size: 100% 4px;
pointer-events: none;
z-index: 999;
}
/* Responsive */
@media (max-width: 768px) {
.layout {
padding: 40px 24px;
}
h1 {
font-size: 3rem;
}
.stat-box {
grid-column: span 6;
}
.title-grid {
grid-template-columns: 1fr;
}
.header-meta-row {
flex-direction: column;
gap: 16px;
}
}
</style>
</head>
<body>
<div class="crt-overlay"></div>
<div class="layout">
<!-- Decorative Bars -->
<div class="color-bar">
<div class="c-block cb-1"></div>
<div class="c-block cb-2"></div>
<div class="c-block cb-3"></div>
</div>
<header>
<div class="header-meta">System Report // {{current_date}}</div>
<h1>Daily<br><span class="highlight">Analysis</span><br>Export</h1>
<p class="header-brief">
QQ群日常活动分析报告,基于消息统计与AI分析生成
</p>
<div class="header-meta-row">
<div class="meta-block">
<div class="meta-label">Generated At</div>
<span class="meta-value">{{current_datetime}}</span>
</div>
<div class="meta-block">
<div class="meta-label">Peak Window</div>
<span class="meta-value">{{most_active_period}}</span>
</div>
</div>
</header>
<!-- Stats Grid -->
<div class="grid-container">
<div class="stat-box">
<span class="stat-label">Messages</span>
<div class="stat-value">{{message_count}}</div>
<div class="stat-note">累计消息数量</div>
</div>
<div class="stat-box">
<span class="stat-label">Participants</span>
<div class="stat-value">{{participant_count}}</div>
<div class="stat-note">参与者计数</div>
</div>
<div class="stat-box">
<span class="stat-label">Characters</span>
<div class="stat-value">{{total_characters}}</div>
<div class="stat-note">文本总字数</div>
</div>
<div class="stat-box">
<span class="stat-label">Emoji</span>
<div class="stat-value">{{emoji_count}}</div>
<div class="stat-note">表情符号统计</div>
</div>
</div>
<!-- CRT Console -->
<div class="grid-container">
<div class="crt-console">
<div class="crt-label">Peak Activity Window</div>
<div class="crt-value">{{most_active_period}}</div>
<div class="crt-sub">活跃序列 SCAN // 消息流强度 {{message_count}}</div>
</div>
</div>
<!-- Hourly Activity -->
<div class="section-label">
Hourly Broadcast <span>ACTIVITY_SPECTRUM</span>
</div>
<div class="grid-container">
<div class="content-box">
<div class="activity-wrapper">
{{hourly_chart_html}}
</div>
</div>
</div>
<!-- Topics -->
<div class="section-label">
Thread Matrix <span>TOPICS_MODULE</span>
</div>
<div class="grid-container">
<div class="content-box">
<div class="topic-list">
{{topics_html}}
</div>
</div>
</div>
<!-- Titles -->
<div class="section-label">
Operator Registry <span>TITLES_MODULE</span>
</div>
<div class="grid-container">
<div class="content-box">
<div class="title-grid">
{{titles_html}}
</div>
</div>
</div>
<!-- Quotes -->
<div class="section-label">
Golden Lines <span>QUOTES_MODULE</span>
</div>
<div class="grid-container">
<div class="content-box">
<div class="quote-list">
{{quotes_html}}
</div>
</div>
</div>
<!-- Token Strip -->
<div class="grid-container">
<div class="token-strip">
<div><span class="token-label">Total Tokens //</span> {{total_tokens}}</div>
<div><span class="token-label">Prompt //</span> {{prompt_tokens}}</div>
<div><span class="token-label">Completion //</span> {{completion_tokens}}</div>
</div>
</div>
<footer>
<div>CRT_EXPORT_CHANNEL // QQ群日常分析</div>
<div>END_OF_LINE_</div>
</footer>
</div>
</body>
</html>
@@ -0,0 +1,727 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>Retro Futurism Daily Export</title>
<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=Oswald:wght@400;600&family=JetBrains+Mono:wght@400;600&family=Noto+Sans+SC:wght@400;500&display=swap"
rel="stylesheet">
<style>
@font-face {
font-family: "Oswald-Fallback";
src: local("Impact"), local("Arial Black"), local("Helvetica Neue Bold");
font-weight: 400 700;
}
@font-face {
font-family: "JetBrains-Fallback";
src: local("Consolas"), local("Monaco"), local("Courier New");
font-weight: 400 700;
}
@font-face {
font-family: "NotoSans-Fallback";
src: local("Microsoft YaHei"), local("PingFang SC"), local("Hiragino Sans GB"), local("SimHei");
font-weight: 400 700;
}
:root {
--c-main: #B2653B;
--c-bg: #F5EDDC;
--c-dark: #181818;
--c-grid: rgba(186, 133, 48, 0.2);
--c-accent-1: #5C7F71;
--c-accent-2: #802520;
--c-accent-3: #BA8530;
--font-display: "Oswald", "Oswald-Fallback", Impact, "Arial Black", sans-serif;
--font-body: "Noto Sans SC", "NotoSans-Fallback", "Microsoft YaHei", "PingFang SC", sans-serif;
--font-mono: "JetBrains Mono", "JetBrains-Fallback", Consolas, Monaco, "Courier New", monospace;
}
* {
box-sizing: border-box;
}
body {
margin: 0;
background: linear-gradient(180deg, #f8f2e5, #efe1ca 65%, #fbf5ea);
font-family: var(--font-body);
color: var(--c-dark);
min-height: 100vh;
}
body::before {
content: "";
position: fixed;
inset: 0;
background-image:
linear-gradient(var(--c-grid) 1px, transparent 1px),
linear-gradient(90deg, var(--c-grid) 1px, transparent 1px);
background-size: 72px 72px;
opacity: 0.4;
pointer-events: none;
}
body::after {
content: "";
position: fixed;
inset: 0;
background:
radial-gradient(circle at 12% 10%, rgba(92, 127, 113, 0.25), transparent 45%),
radial-gradient(circle at 80% 15%, rgba(128, 37, 32, 0.2), transparent 40%),
linear-gradient(90deg, rgba(178, 101, 59, 0.08), transparent 55%);
opacity: 0.7;
pointer-events: none;
}
.scanlines {
position: fixed;
inset: 0;
pointer-events: none;
background-image: linear-gradient(rgba(24, 24, 24, 0.08) 50%, transparent 50%);
background-size: 100% 5px;
mix-blend-mode: multiply;
opacity: 0.35;
}
.report-shell {
position: relative;
width: min(1100px, calc(100% - 48px));
margin: 48px auto 80px;
background: linear-gradient(180deg, rgba(255, 255, 255, 0.95), rgba(246, 232, 210, 0.98));
border-left: 16px solid var(--c-main);
border-right: 2px solid rgba(24, 24, 24, 0.08);
box-shadow: 0 40px 80px rgba(0, 0, 0, 0.25);
overflow: hidden;
padding-bottom: 60px;
}
.report-shell::before {
content: "";
position: absolute;
top: 0;
right: -50px;
width: 180px;
height: 100%;
background: linear-gradient(180deg, rgba(24, 24, 24, 0), rgba(178, 101, 59, 0.1), rgba(24, 24, 24, 0));
}
.color-strip {
position: absolute;
top: 32px;
left: -40px;
display: flex;
flex-direction: column;
gap: 4px;
}
.color-strip span {
width: 120px;
height: 6px;
}
.strip-1 {
background: var(--c-accent-1);
}
.strip-2 {
background: var(--c-accent-2);
}
.strip-3 {
background: var(--c-accent-3);
}
header.hero {
display: grid;
grid-template-columns: 2.2fr 1fr;
gap: 40px;
padding: 80px 64px 40px 64px;
}
.hero-label {
font-family: var(--font-mono);
letter-spacing: 0.3rem;
font-size: 0.78rem;
text-transform: uppercase;
color: rgba(24, 24, 24, 0.65);
margin-bottom: 14px;
}
.hero-title {
font-family: var(--font-display);
font-size: clamp(3rem, 6vw, 4.8rem);
text-transform: uppercase;
line-height: 0.9;
margin: 0 0 18px;
letter-spacing: 0.1rem;
}
.hero-title .accent {
color: var(--c-main);
}
.hero-brief {
max-width: 460px;
color: rgba(24, 24, 24, 0.7);
line-height: 1.6;
margin: 0;
}
.hero-meta {
display: flex;
gap: 24px;
margin-top: 28px;
font-family: var(--font-mono);
}
.hero-meta .meta-block {
border-left: 4px solid var(--c-main);
padding-left: 14px;
}
.meta-label {
font-size: 0.72rem;
letter-spacing: 0.2rem;
text-transform: uppercase;
color: rgba(24, 24, 24, 0.5);
}
.meta-value {
display: block;
margin-top: 4px;
color: var(--c-dark);
}
.crt-console {
background: #181818;
color: var(--c-bg);
padding: 24px;
border-radius: 8px;
border: 1px solid rgba(245, 237, 220, 0.25);
box-shadow: inset 0 0 25px rgba(0, 0, 0, 0.5);
min-height: 280px;
position: relative;
}
.crt-console::before {
content: "CRT CHANNEL";
position: absolute;
top: 12px;
right: 16px;
font-family: var(--font-mono);
letter-spacing: 0.22rem;
font-size: 0.68rem;
color: rgba(245, 237, 220, 0.6);
}
.crt-label {
font-family: var(--font-mono);
font-size: 0.78rem;
letter-spacing: 0.28rem;
text-transform: uppercase;
color: rgba(245, 237, 220, 0.7);
}
.crt-value {
font-family: var(--font-display);
font-size: 3rem;
color: var(--c-main);
margin: 12px 0;
}
.crt-sub {
font-size: 0.9rem;
color: rgba(245, 237, 220, 0.8);
margin-bottom: 4px;
}
.section-shell {
padding: 0 64px;
}
.stat-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: 18px;
margin-bottom: 18px;
}
.stat-card {
background: rgba(255, 255, 255, 0.9);
border: 1px solid rgba(24, 24, 24, 0.12);
padding: 20px;
box-shadow: inset 0 0 0 1px rgba(24, 24, 24, 0.05);
min-height: 140px;
position: relative;
}
.stat-card::after {
content: "";
position: absolute;
inset: 18px;
border: 1px dashed rgba(24, 24, 24, 0.08);
pointer-events: none;
}
.stat-label {
font-family: var(--font-mono);
font-size: 0.78rem;
letter-spacing: 0.2rem;
text-transform: uppercase;
color: rgba(24, 24, 24, 0.55);
}
.stat-value {
font-family: var(--font-display);
font-size: 2.8rem;
line-height: 1;
color: var(--c-main);
margin: 16px 0 8px;
}
.stat-note {
font-size: 0.9rem;
color: rgba(24, 24, 24, 0.7);
}
.module {
margin: 36px 0;
padding: 32px 32px 40px;
border-left: 5px solid rgba(178, 101, 59, 0.6);
background: linear-gradient(180deg, rgba(255, 255, 255, 0.95), rgba(245, 237, 220, 0.85));
box-shadow: 0 12px 30px rgba(0, 0, 0, 0.08);
position: relative;
}
.module::after {
content: "";
position: absolute;
inset: 16px;
border: 1px solid rgba(24, 24, 24, 0.06);
pointer-events: none;
}
.module-heading {
display: flex;
justify-content: space-between;
align-items: flex-end;
margin-bottom: 18px;
}
.module-kicker {
font-family: var(--font-mono);
font-size: 0.75rem;
letter-spacing: 0.3rem;
text-transform: uppercase;
color: rgba(24, 24, 24, 0.55);
margin-bottom: 6px;
}
.module-title {
margin: 0;
font-family: var(--font-display);
font-size: 2.2rem;
letter-spacing: 0.1rem;
}
.module-tag {
font-family: var(--font-mono);
font-size: 0.75rem;
letter-spacing: 0.3rem;
background: var(--c-dark);
color: var(--c-bg);
padding: 6px 14px;
border-radius: 30px;
}
.activity-panel {
background: #fff5eb;
padding: 20px;
border: 1px solid rgba(24, 24, 24, 0.08);
}
.activity-board {
display: flex;
flex-direction: column;
gap: 12px;
}
.activity-row {
display: grid;
grid-template-columns: 80px 1fr 70px;
align-items: center;
gap: 12px;
font-family: var(--font-mono);
color: rgba(24, 24, 24, 0.6);
}
.activity-hour {
letter-spacing: 0.1rem;
}
.activity-meter {
background: rgba(24, 24, 24, 0.08);
height: 10px;
border-radius: 20px;
overflow: hidden;
border: 1px solid rgba(24, 24, 24, 0.1);
}
.activity-bar {
height: 100%;
width: var(--pct, 10%);
background: linear-gradient(90deg, rgba(178, 101, 59, 0.8), rgba(178, 101, 59, 0.3));
position: relative;
}
.activity-count {
text-align: right;
}
.topic-stack {
display: flex;
flex-direction: column;
gap: 18px;
margin-top: 12px;
}
.topic-card {
display: grid;
grid-template-columns: 90px 1fr;
gap: 20px;
padding-bottom: 18px;
border-bottom: 1px solid rgba(24, 24, 24, 0.08);
}
.topic-index {
font-family: var(--font-mono);
font-size: 1.2rem;
letter-spacing: 0.35rem;
color: rgba(24, 24, 24, 0.35);
}
.topic-title {
margin: 0;
font-size: 1.2rem;
letter-spacing: 0.04rem;
color: var(--c-dark);
}
.topic-meta {
font-family: var(--font-mono);
font-size: 0.8rem;
letter-spacing: 0.16rem;
color: rgba(24, 24, 24, 0.5);
margin: 6px 0;
}
.topic-detail {
color: rgba(24, 24, 24, 0.75);
line-height: 1.6;
}
.title-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
gap: 18px;
margin-top: 20px;
}
.title-card {
display: grid;
grid-template-columns: 72px 1fr;
column-gap: 18px;
padding: 18px;
border: 1px solid rgba(24, 24, 24, 0.08);
background: linear-gradient(180deg, rgba(255, 255, 255, 0.95), rgba(245, 237, 220, 0.6));
position: relative;
}
.title-card::before,
.title-card::after {
content: "";
position: absolute;
top: 12px;
height: 4px;
border-radius: 2px;
}
.title-card::before {
left: 12px;
width: 32px;
background: var(--c-accent-1);
}
.title-card::after {
left: 48px;
width: 32px;
background: var(--c-accent-2);
}
.title-avatar {
width: 72px;
height: 72px;
border-radius: 50%;
overflow: hidden;
border: 1px solid rgba(24, 24, 24, 0.12);
background: rgba(24, 24, 24, 0.08);
grid-row: 1 / span 3;
align-self: center;
}
.title-avatar img {
width: 100%;
height: 100%;
object-fit: cover;
filter: grayscale(35%) contrast(110%);
}
.title-avatar-fallback {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
font-family: var(--font-mono);
font-size: 0.78rem;
letter-spacing: 0.25rem;
color: rgba(24, 24, 24, 0.5);
}
.title-header {
display: flex;
justify-content: space-between;
align-items: center;
grid-column: 2;
}
.title-name {
font-weight: 600;
}
.title-mbti {
font-family: var(--font-mono);
font-size: 0.75rem;
letter-spacing: 0.18rem;
background: rgba(24, 24, 24, 0.08);
padding: 2px 10px;
}
.title-role {
grid-column: 2;
font-family: var(--font-mono);
letter-spacing: 0.3rem;
text-transform: uppercase;
color: var(--c-main);
margin: 8px 0;
}
.title-reason {
grid-column: 2;
margin: 0;
font-size: 0.92rem;
color: rgba(24, 24, 24, 0.75);
line-height: 1.5;
}
.quote-stack {
display: flex;
flex-direction: column;
gap: 18px;
margin-top: 16px;
}
.quote-card {
padding: 24px;
border: 1px dashed rgba(24, 24, 24, 0.15);
background: #fff8ee;
position: relative;
}
.quote-card::after {
content: "";
position: absolute;
top: -6px;
right: 40px;
width: 90px;
height: 6px;
background: var(--c-dark);
}
.quote-glyph {
font-size: 2.5rem;
color: rgba(178, 101, 59, 0.4);
margin: 0;
}
.quote-content {
font-size: 1.05rem;
color: var(--c-dark);
margin: 12px 0;
line-height: 1.6;
}
.quote-author {
font-family: var(--font-mono);
font-size: 0.85rem;
letter-spacing: 0.2rem;
color: rgba(24, 24, 24, 0.6);
}
.quote-note {
font-size: 0.9rem;
color: rgba(24, 24, 24, 0.7);
margin-top: 10px;
}
.token-strip {
padding: 0 64px;
margin-top: 20px;
}
.token-panel {
border-top: 1px solid rgba(24, 24, 24, 0.15);
padding: 16px 0 8px;
display: flex;
gap: 30px;
font-family: var(--font-mono);
letter-spacing: 0.15rem;
}
footer {
padding: 0 64px;
margin-top: 28px;
font-family: var(--font-mono);
font-size: 0.78rem;
letter-spacing: 0.25rem;
display: flex;
justify-content: space-between;
color: rgba(24, 24, 24, 0.65);
}
@media (max-width: 900px) {
header.hero {
grid-template-columns: 1fr;
}
.report-shell {
width: calc(100% - 24px);
margin: 24px auto 60px;
}
.section-shell,
.token-strip {
padding: 0 32px;
}
.module {
padding: 24px;
}
.stat-grid {
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
}
}
</style>
</head>
<body>
<div class="scanlines"></div>
<main class="report-shell">
<div class="color-strip">
<span class="strip-1"></span>
<span class="strip-2"></span>
<span class="strip-3"></span>
</div>
<header class="hero">
<div>
<div class="hero-label">Instruction Catalogue // Retro Futurism Export</div>
<h1 class="hero-title">Left <span class="accent">Aligned</span><br>Daily Report</h1>
<p class="hero-brief">
利用网格与留白塑造复古终端的秩序感,配合 #B2653B 主色的橙铜质感,
打造一份兼容当代阅读体验的指令式日报。
</p>
<div class="hero-meta">
<div class="meta-block">
<div class="meta-label">Report Date</div>
<span class="meta-value">{{current_date}}</span>
</div>
<div class="meta-block">
<div class="meta-label">Generated At</div>
<span class="meta-value">{{current_datetime}}</span>
</div>
</div>
</div>
<div class="crt-console">
<div class="crt-label">Peak Window</div>
<div class="crt-value">{{most_active_period}}</div>
<div class="crt-sub">活跃序列 SCAN</div>
<div class="crt-sub">消息流强度 {{message_count}}</div>
</div>
</header>
<section class="section-shell">
<div class="stat-grid">
<article class="stat-card">
<div class="stat-label">Messages</div>
<div class="stat-value">{{message_count}}</div>
<div class="stat-note">累计消息数量</div>
</article>
<article class="stat-card">
<div class="stat-label">Participants</div>
<div class="stat-value">{{participant_count}}</div>
<div class="stat-note">参与者计数</div>
</article>
<article class="stat-card">
<div class="stat-label">Characters</div>
<div class="stat-value">{{total_characters}}</div>
<div class="stat-note">文本总字数</div>
</article>
<article class="stat-card">
<div class="stat-label">Emoji Count</div>
<div class="stat-value">{{emoji_count}}</div>
<div class="stat-note">表情符号统计</div>
</article>
</div>
</section>
<section class="section-shell module module-activity">
<div class="module-heading">
<div>
<div class="module-kicker">Activity Spectrum</div>
<h2 class="module-title">Hourly Broadcast</h2>
</div>
<div class="module-tag">GRID-01</div>
</div>
<div class="activity-panel">
{{hourly_chart_html}}
</div>
</section>
<div class="section-shell">
{{topics_html}}
{{titles_html}}
{{quotes_html}}
</div>
<section class="token-strip">
<div class="module-kicker">Token Monitor</div>
<div class="token-panel">
<div>Total Tokens // {{total_tokens}}</div>
<div>Prompt Tokens // {{prompt_tokens}}</div>
<div>Completion Tokens // {{completion_tokens}}</div>
</div>
</section>
<footer>
<span>CRT EXPORT CHANNEL</span>
<span>END OF LINE</span>
</footer>
</main>
</body>
</html>
@@ -0,0 +1,10 @@
{% if quotes %}
{% for quote in quotes %}
<article class="quote-item">
<div class="quote-glyph">"</div>
<p class="quote-content">{{ quote.content }}</p>
<div class="quote-author">— {{ quote.sender }}</div>
<p class="quote-note">{{ quote.reason }}</p>
</article>
{% endfor %}
{% endif %}
@@ -0,0 +1,12 @@
{% if topics %}
{% for topic in topics %}
<article class="topic-item">
<div class="topic-index">{{ "%02d"|format(topic.index) }}</div>
<div>
<h3 class="topic-title">{{ topic.topic.topic }}</h3>
<p class="topic-meta">PARTICIPANTS: {{ topic.contributors }}</p>
<p class="topic-detail">{{ topic.topic.detail }}</p>
</div>
</article>
{% endfor %}
{% endif %}
@@ -0,0 +1,21 @@
{% if titles %}
{% for title in titles %}
<article class="title-item">
<div class="title-header">
<div class="title-avatar">
{% if title.avatar_data %}
<img src="{{ title.avatar_data }}" alt="{{ title.name }}">
{% else %}
<div class="title-avatar-fallback">CRT</div>
{% endif %}
</div>
<div>
<span class="title-name">{{ title.name }}</span>
<span class="title-mbti">{{ title.mbti }}</span>
</div>
</div>
<div class="title-role">{{ title.title }}</div>
<p class="title-reason">{{ title.reason }}</p>
</article>
{% endfor %}
{% endif %}
@@ -0,0 +1,562 @@
<!--
@name: 复古未来主义报告
@description: 结合复古工业与未来终端风格的统计报告
@author: LunaRain_079
@version: 1.0.0
-->
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>LUNABOX TERMINAL REPORT</title>
<!-- Chart.js CDN -->
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js"></script>
<!-- Fonts - Using system fonts as fallback for export compatibility -->
<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=JetBrains+Mono:wght@400;700&family=Oswald:wght@400;700&family=Noto+Sans+SC:wght@400;700&display=swap" rel="stylesheet">
<style>
/* Embedded font fallbacks for export */
@font-face {
font-family: 'Oswald-Fallback';
src: local('Impact'), local('Arial Black'), local('Helvetica Neue Bold');
font-weight: 400 700;
}
@font-face {
font-family: 'JetBrains-Fallback';
src: local('Consolas'), local('Monaco'), local('Courier New');
font-weight: 400 700;
}
@font-face {
font-family: 'NotoSans-Fallback';
src: local('Microsoft YaHei'), local('PingFang SC'), local('Hiragino Sans GB'), local('SimHei');
font-weight: 400 700;
}
:root {
/* Palette */
--c-bg: #F5EDDC;
--c-text: #181818;
--c-accent: #B2653B; /* Orange */
--c-dec-1: #5C7F71; /* Green */
--c-dec-2: #802520; /* Red */
--c-dec-3: #BA8530; /* Gold */
--c-grid: rgba(24, 24, 24, 0.1);
/* Fonts - with system fallbacks for export */
--font-display: 'Oswald', 'Oswald-Fallback', Impact, 'Arial Black', sans-serif;
--font-mono: 'JetBrains Mono', 'JetBrains-Fallback', Consolas, Monaco, 'Courier New', monospace;
--font-body: 'Noto Sans SC', 'NotoSans-Fallback', 'Microsoft YaHei', 'PingFang SC', sans-serif;
}
* {
box-sizing: border-box;
}
body {
margin: 0;
padding: 0;
background-color: var(--c-bg);
color: var(--c-text);
font-family: var(--font-body);
/* Grid Background */
background-image:
linear-gradient(var(--c-grid) 1px, transparent 1px),
linear-gradient(90deg, var(--c-grid) 1px, transparent 1px);
background-size: 40px 40px;
min-height: 100vh;
overflow-x: hidden;
}
/* Layout Container */
.layout {
max-width: 1200px;
margin: 0 auto;
padding: 60px 40px;
position: relative;
border-left: 1px solid var(--c-text);
border-right: 1px solid var(--c-text);
background:
linear-gradient(90deg, transparent 0%, transparent 98%, var(--c-accent) 98%, var(--c-accent) 100%);
background-size: 100% 100%;
min-height: 100vh;
}
/* Decorative Color Bar */
.color-bar {
position: absolute;
top: 60px;
left: -20px;
display: flex;
flex-direction: column;
gap: 0;
z-index: 10;
}
.c-block {
width: 60px;
height: 12px;
}
.cb-1 { background: var(--c-dec-1); }
.cb-2 { background: var(--c-dec-2); }
.cb-3 { background: var(--c-dec-3); }
/* Header */
header {
margin-bottom: 80px;
position: relative;
}
.header-meta {
font-family: var(--font-mono);
font-size: 0.8rem;
color: var(--c-text);
opacity: 0.7;
margin-bottom: 10px;
letter-spacing: 1px;
text-transform: uppercase;
}
h1 {
font-family: var(--font-display);
font-size: 5rem;
line-height: 0.9;
margin: 0;
text-transform: uppercase;
letter-spacing: -2px;
color: var(--c-text);
}
h1 .highlight {
color: var(--c-accent);
}
.date-range {
font-family: var(--font-mono);
font-size: 1.2rem;
margin-top: 20px;
padding-left: 20px;
border-left: 4px solid var(--c-accent);
display: inline-block;
}
/* Grid System */
.grid-container {
display: grid;
grid-template-columns: repeat(12, 1fr);
gap: 20px;
margin-bottom: 60px;
}
/* Section Titles */
.section-label {
grid-column: span 12;
font-family: var(--font-display);
font-size: 2rem;
text-transform: uppercase;
border-bottom: 2px solid var(--c-text);
padding-bottom: 10px;
margin-bottom: 20px;
display: flex;
justify-content: space-between;
align-items: flex-end;
}
.section-label span {
background: var(--c-text);
color: var(--c-bg);
padding: 2px 10px;
font-size: 1rem;
font-family: var(--font-mono);
vertical-align: middle;
}
/* Stats Big Number */
.stat-box {
grid-column: span 4;
border: 1px solid var(--c-text);
padding: 20px;
position: relative;
background: rgba(255, 255, 255, 0.5);
}
.stat-box::before {
content: '';
position: absolute;
top: 4px; left: 4px; right: 4px; bottom: 4px;
border: 1px solid var(--c-grid);
pointer-events: none;
}
.stat-label {
font-family: var(--font-mono);
font-size: 0.9rem;
text-transform: uppercase;
color: var(--c-dec-1);
margin-bottom: 10px;
display: block;
}
.stat-value {
font-family: var(--font-display);
font-size: 4rem;
color: var(--c-accent);
line-height: 1;
}
/* AI Summary - Terminal Style */
.ai-terminal {
grid-column: span 8;
background: var(--c-text);
color: var(--c-bg);
padding: 20px;
font-family: var(--font-mono);
font-size: 0.9rem;
line-height: 1.6;
position: relative;
overflow: hidden;
}
.ai-terminal::before {
content: "AI_ANALYSIS_MODULE // v.2.0";
display: block;
border-bottom: 1px solid #444;
padding-bottom: 10px;
margin-bottom: 15px;
color: var(--c-accent);
font-size: 0.7rem;
}
.cursor {
display: inline-block;
width: 8px;
height: 14px;
background: var(--c-accent);
animation: blink 1s infinite;
}
@keyframes blink { 0%, 100% { opacity: 1; } 50% { opacity: 0; } }
/* Charts */
.chart-wrapper {
grid-column: span 12;
border: 1px solid var(--c-text);
padding: 20px;
background: #fff;
position: relative;
}
.chart-wrapper.half {
grid-column: span 6;
}
.chart-deco {
position: absolute;
top: -10px;
right: 20px;
background: var(--c-bg);
padding: 0 10px;
font-family: var(--font-mono);
font-size: 0.8rem;
border: 1px solid var(--c-text);
}
/* Leaderboard - Asymmetrical List */
.game-list {
grid-column: span 12;
display: flex;
flex-direction: column;
gap: 15px;
}
.game-item {
display: grid;
grid-template-columns: 60px 80px 1fr 150px;
align-items: center;
gap: 20px;
border-bottom: 1px solid var(--c-grid);
padding-bottom: 10px;
}
.game-cover {
width: 80px;
height: 106px; /* 3:4 aspect ratio */
background: #000;
border: 1px solid var(--c-text);
overflow: hidden;
position: relative;
}
.game-cover img {
width: 100%;
height: 100%;
object-fit: cover;
filter: grayscale(100%) contrast(120%); /* Retro B&W look */
transition: filter 0.3s;
}
.game-item:hover .game-cover img {
filter: grayscale(0%) contrast(100%);
}
.game-rank {
font-family: var(--font-display);
font-size: 2rem;
color: var(--c-text);
opacity: 0.3;
}
.game-item:nth-child(1) .game-rank { color: var(--c-dec-3); opacity: 1; }
.game-item:nth-child(2) .game-rank { color: var(--c-dec-1); opacity: 1; }
.game-item:nth-child(3) .game-rank { color: var(--c-dec-2); opacity: 1; }
.game-info h3 {
margin: 0;
font-family: var(--font-body);
font-size: 1.2rem;
font-weight: 700;
}
.game-time {
font-family: var(--font-mono);
text-align: right;
color: var(--c-accent);
font-weight: bold;
}
/* Footer */
footer {
margin-top: 80px;
border-top: 4px solid var(--c-text);
padding-top: 20px;
display: flex;
justify-content: space-between;
font-family: var(--font-mono);
font-size: 0.8rem;
text-transform: uppercase;
}
/* Responsive */
@media (max-width: 768px) {
.grid-container { display: flex; flex-direction: column; }
.chart-wrapper.half { grid-column: span 12; }
h1 { font-size: 3rem; }
}
/* CRT Scanline Effect Overlay (Optional, subtle) */
.crt-overlay {
position: fixed;
top: 0; left: 0; width: 100%; height: 100%;
background: linear-gradient(rgba(18, 16, 16, 0) 50%, rgba(0, 0, 0, 0.05) 50%);
background-size: 100% 4px;
pointer-events: none;
z-index: 999;
}
</style>
</head>
<body>
<div class="crt-overlay"></div>
<div class="layout">
<!-- Decorative Bars -->
<div class="color-bar">
<div class="c-block cb-1"></div>
<div class="c-block cb-2"></div>
<div class="c-block cb-3"></div>
</div>
<header>
<div class="header-meta">System Report // {{.ExportTime}}</div>
<h1>Luna<span class="highlight">Box</span><br>Instruction<br>Catalogue</h1>
<div class="date-range">
PERIOD: {{.StartDate}} >> {{.EndDate}}
</div>
</header>
<div class="grid-container">
<!-- Stats -->
<div class="stat-box" style="grid-column: span 3;">
<span class="stat-label">Total Sessions</span>
<div class="stat-value">{{.TotalPlayCount}}</div>
</div>
<div class="stat-box" style="grid-column: span 3;">
<span class="stat-label">Total Duration</span>
<div class="stat-value" style="font-size: 2.5rem; margin-top: 10px;">{{.TotalPlayTimeStr}}</div>
</div>
</div>
<!-- AI Summary -->
{{if .AISummary}}
<div class="ai-terminal" style="grid-column: span 6;">
{{.AISummary}}<span class="cursor"></span>
</div>
{{end}}
<!-- Charts -->
{{if .ChartLabels}}
<div class="section-label">
Analytics <span>DATA_VISUALIZATION</span>
</div>
<div class="grid-container">
<div class="chart-wrapper">
<div class="chart-deco">FIGURE_01: TIME_SERIES</div>
<canvas id="trendChart" height="300"></canvas>
</div>
</div>
{{end}}
{{if .GameTrendData}}
<div class="grid-container">
<div class="chart-wrapper">
<div class="chart-deco">FIGURE_02: GAME_TRENDS</div>
<canvas id="gameTrendChart" height="300"></canvas>
</div>
</div>
{{end}}
<!-- Leaderboard -->
{{if .Leaderboard}}
<div class="section-label">
Top Records <span>RANKING_MODULE</span>
</div>
<div class="grid-container">
<div class="game-list">
{{range $index, $game := .Leaderboard}}
<div class="game-item">
<div class="game-rank">#{{add $index 1}}</div>
<div class="game-cover">
{{if $game.CoverBase64}}
<img src="{{$game.CoverBase64}}" alt="{{$game.GameName}}">
{{else if $game.CoverURL}}
<img src="{{$game.CoverURL}}" alt="{{$game.GameName}}">
{{else}}
<div style="width:100%;height:100%;background:#333;display:flex;align-items:center;justify-content:center;color:#555;font-size:0.8rem;">NO_IMG</div>
{{end}}
</div>
<div class="game-info">
<h3>{{$game.GameName}}</h3>
</div>
<div class="game-time">{{$game.DurationStr}}</div>
</div>
{{end}}
</div>
</div>
{{end}}
<footer>
<div>Generated by {{.AppName}} v{{.AppVersion}}</div>
<div>END OF LINE_</div>
</footer>
</div>
<!-- Data Injection -->
<script id="chart-data-config" type="application/json">
{
"labels": {{ .ChartLabels | safeJS }},
"data": {{ .ChartData | safeJS }},
"gameTrend": {{ .GameTrendData | safeJS }}
}
</script>
<script>
// Chart Configuration
Chart.defaults.font.family = "'JetBrains Mono', monospace";
Chart.defaults.color = '#181818';
const commonOptions = {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
labels: {
usePointStyle: true,
boxWidth: 10
}
}
},
scales: {
x: {
grid: {
color: 'rgba(24, 24, 24, 0.1)',
tickLength: 0
}
},
y: {
grid: {
color: 'rgba(24, 24, 24, 0.1)',
borderDash: [4, 4]
},
beginAtZero: true
}
}
};
// Load Data
let config = {};
try {
config = JSON.parse(document.getElementById('chart-data-config').textContent);
} catch (e) { console.error(e); }
// Render Trend Chart
if (config.labels && config.data) {
const ctx = document.getElementById('trendChart');
if (ctx) {
new Chart(ctx, {
type: 'line',
data: {
labels: config.labels,
datasets: [{
label: 'PLAY_TIME (Hours)',
data: config.data,
borderColor: '#B2653B',
backgroundColor: 'rgba(178, 101, 59, 0.1)',
borderWidth: 2,
pointBackgroundColor: '#F5EDDC',
pointBorderColor: '#B2653B',
pointRadius: 4,
pointHoverRadius: 6,
fill: true,
tension: 0 // Sharp lines for retro feel
}]
},
options: commonOptions
});
}
}
// Render Game Distribution
if (config.gameTrend) {
const ctx2 = document.getElementById('gameTrendChart');
if (ctx2) {
// Palette for multiple items
const palette = ['#B2653B', '#5C7F71', '#802520', '#BA8530', '#181818'];
// Map data to datasets
const datasets = config.gameTrend.map((ds, index) => {
const color = palette[index % palette.length];
return {
label: ds.label, // Game Name
data: ds.data, // Array of hours
borderColor: color,
backgroundColor: color,
borderWidth: 2,
pointBackgroundColor: '#F5EDDC',
pointBorderColor: color,
pointRadius: 4,
tension: 0 // Sharp lines
};
});
new Chart(ctx2, {
type: 'line',
data: {
labels: config.labels, // Date labels
datasets: datasets
},
options: commonOptions
});
}
}
</script>
</body>
</html>