feat(insight): update insight template and app to React, enhance export functionality

This commit is contained in:
DragonnZhang 2026-01-23 20:06:06 +08:00
parent 2931e75a17
commit 6cb0bb078c
5 changed files with 472 additions and 329 deletions

View file

@ -5,8 +5,7 @@
*/
import fs from 'fs/promises';
import path from 'path';
import { dirname } from 'path';
import path, { dirname } from 'path';
import { fileURLToPath } from 'url';
import type { InsightData } from '../types/StaticInsightTypes.js';

View file

@ -25,27 +25,22 @@
</p>
</header>
<div class="mt-6 flex justify-center">
<button
id="export-btn"
class="group inline-flex items-center gap-2 rounded-full bg-slate-900 px-5 py-3 text-sm font-semibold text-white shadow-soft transition focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-slate-400 hover:-translate-y-[1px] hover:shadow-lg active:translate-y-[1px]"
onclick="handleExport()"
>
Export as Image
<span class="text-slate-200 transition group-hover:translate-x-0.5">
</span>
</button>
</div>
<!-- React App Mount Point -->
<div id="react-root"></div>
</div>
</div>
<!-- React CDN -->
<script crossorigin src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<!-- CDN Libraries -->
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.js"></script>
<script src="https://cdn.jsdelivr.net/npm/html2canvas@1.4.1/dist/html2canvas.min.js"></script>
<!-- Application Data -->
<script>
<script type="text/babel">
window.INSIGHT_DATA = {{DATA_PLACEHOLDER}};
{{SCRIPTS_PLACEHOLDER}}
</script>

View file

@ -1,284 +1,295 @@
// Native JavaScript implementation of the insight app
// This replaces the React-based App.tsx functionality
/* eslint-disable react/prop-types */
/* eslint-disable no-undef */
// React-based implementation of the insight app
// Converts the vanilla JavaScript implementation to React
let hourChartInstance = null;
const { useState, useRef, useEffect } = React;
// Initialize the app when DOM is loaded
document.addEventListener('DOMContentLoaded', function() {
initializeApp();
});
// Main App Component
function InsightApp({ data }) {
if (!data) {
return (
<div className="text-center text-slate-600">
No insight data available
</div>
);
}
function initializeApp() {
const insights = window.INSIGHT_DATA;
return (
<div>
<DashboardCards insights={data} />
<HeatmapSection heatmap={data.heatmap} />
<TokenUsageSection tokenUsage={data.tokenUsage} />
<AchievementsSection achievements={data.achievements} />
<ExportButton />
</div>
);
}
if (!insights) {
showError('No insight data available');
return;
// Dashboard Cards Component
function DashboardCards({ insights }) {
const cardClass = 'glass-card p-6';
const sectionTitleClass =
'text-lg font-semibold tracking-tight text-slate-900';
const captionClass = 'text-sm font-medium text-slate-500';
return (
<div className="grid gap-4 md:grid-cols-3 md:gap-6">
<StreakCard
currentStreak={insights.currentStreak}
longestStreak={insights.longestStreak}
cardClass={cardClass}
captionClass={captionClass}
/>
<ActiveHoursChart
activeHours={insights.activeHours}
cardClass={cardClass}
sectionTitleClass={sectionTitleClass}
/>
<WorkSessionCard
longestWorkDuration={insights.longestWorkDuration}
longestWorkDate={insights.longestWorkDate}
latestActiveTime={insights.latestActiveTime}
cardClass={cardClass}
sectionTitleClass={sectionTitleClass}
/>
</div>
);
}
// Streak Card Component
function StreakCard({ currentStreak, longestStreak, cardClass, captionClass }) {
return (
<div className={`${cardClass} h-full`}>
<div className="flex items-start justify-between">
<div>
<p className={captionClass}>Current Streak</p>
<p className="mt-1 text-4xl font-bold text-slate-900">
{currentStreak}
<span className="ml-2 text-base font-semibold text-slate-500">
days
</span>
</p>
</div>
<span className="rounded-full bg-emerald-50 px-4 py-2 text-sm font-semibold text-emerald-700">
Longest {longestStreak}d
</span>
</div>
</div>
);
}
// Active Hours Chart Component
function ActiveHoursChart({ activeHours, cardClass, sectionTitleClass }) {
const chartRef = useRef(null);
const chartInstance = useRef(null);
useEffect(() => {
if (chartInstance.current) {
chartInstance.current.destroy();
}
// Create the main content
createInsightContent(insights);
// Initialize charts
initializeHourChart(insights);
// Initialize heatmap
initializeHeatmap(insights);
}
function createInsightContent(insights) {
const container = document.getElementById('container');
const contentDiv = container.querySelector('.mx-auto');
// Find the header and content placeholder
const header = contentDiv.querySelector('header');
const contentPlaceholder = contentDiv.querySelector('[data-placeholder="content"]');
// If placeholder doesn't exist, create content after header
if (!contentPlaceholder) {
const content = createMainContent(insights);
header.insertAdjacentHTML('afterend', content);
}
}
function createMainContent(insights) {
const cardClass = 'glass-card p-6';
const sectionTitleClass = 'text-lg font-semibold tracking-tight text-slate-900';
const captionClass = 'text-sm font-medium text-slate-500';
return `
<div class="grid gap-4 md:grid-cols-3 md:gap-6">
<div class="${cardClass} h-full">
<div class="flex items-start justify-between">
<div>
<p class="${captionClass}">Current Streak</p>
<p class="mt-1 text-4xl font-bold text-slate-900">
${insights.currentStreak}
<span class="ml-2 text-base font-semibold text-slate-500">
days
</span>
</p>
</div>
<span class="rounded-full bg-emerald-50 px-4 py-2 text-sm font-semibold text-emerald-700">
Longest ${insights.longestStreak}d
</span>
</div>
</div>
<div class="${cardClass} h-full">
<div class="flex items-center justify-between">
<h3 class="${sectionTitleClass}">Active Hours</h3>
<span class="rounded-full bg-slate-100 px-3 py-1 text-xs font-semibold text-slate-600">
24h
</span>
</div>
<div class="mt-4 h-56 w-full">
<canvas id="hour-chart"></canvas>
</div>
</div>
<div class="${cardClass} h-full space-y-3">
<h3 class="${sectionTitleClass}">Work Session</h3>
<div class="grid grid-cols-2 gap-3 text-sm text-slate-700">
<div class="rounded-xl bg-slate-50 px-3 py-2">
<p class="text-xs font-semibold uppercase tracking-wide text-slate-500">
Longest
</p>
<p class="mt-1 text-lg font-semibold text-slate-900">
${insights.longestWorkDuration}m
</p>
</div>
<div class="rounded-xl bg-slate-50 px-3 py-2">
<p class="text-xs font-semibold uppercase tracking-wide text-slate-500">
Date
</p>
<p class="mt-1 text-lg font-semibold text-slate-900">
${insights.longestWorkDate || '-'}
</p>
</div>
<div class="col-span-2 rounded-xl bg-slate-50 px-3 py-2">
<p class="text-xs font-semibold uppercase tracking-wide text-slate-500">
Last Active
</p>
<p class="mt-1 text-lg font-semibold text-slate-900">
${insights.latestActiveTime || '-'}
</p>
</div>
</div>
</div>
</div>
<div class="${cardClass} mt-4 space-y-4 md:mt-6">
<div class="flex items-center justify-between">
<h3 class="${sectionTitleClass}">Activity Heatmap</h3>
<span class="text-xs font-semibold text-slate-500">
Past year
</span>
</div>
<div class="heatmap-container">
<div id="heatmap" class="min-w-[720px] rounded-xl border border-slate-100 bg-white/70 p-4 shadow-inner shadow-slate-100">
<!-- Heatmap will be inserted here -->
</div>
</div>
</div>
<div class="${cardClass} mt-4 md:mt-6">
<div class="space-y-3">
<h3 class="${sectionTitleClass}">Token Usage</h3>
<div class="grid grid-cols-3 gap-3">
<div class="rounded-xl bg-slate-50 px-4 py-3">
<p class="text-xs font-semibold uppercase tracking-wide text-slate-500">
Input
</p>
<p class="mt-1 text-2xl font-bold text-slate-900">
${calculateTotalTokens(insights.tokenUsage, 'input').toLocaleString()}
</p>
</div>
<div class="rounded-xl bg-slate-50 px-4 py-3">
<p class="text-xs font-semibold uppercase tracking-wide text-slate-500">
Output
</p>
<p class="mt-1 text-2xl font-bold text-slate-900">
${calculateTotalTokens(insights.tokenUsage, 'output').toLocaleString()}
</p>
</div>
<div class="rounded-xl bg-slate-50 px-4 py-3">
<p class="text-xs font-semibold uppercase tracking-wide text-slate-500">
Total
</p>
<p class="mt-1 text-2xl font-bold text-slate-900">
${calculateTotalTokens(insights.tokenUsage, 'total').toLocaleString()}
</p>
</div>
</div>
</div>
</div>
<div class="${cardClass} mt-4 space-y-4 md:mt-6">
<div class="flex items-center justify-between">
<h3 class="${sectionTitleClass}">Achievements</h3>
<span class="text-xs font-semibold text-slate-500">
${insights.achievements.length} total
</span>
</div>
${insights.achievements.length === 0 ?
'<p class="text-sm text-slate-600">No achievements yet. Keep coding!</p>' :
`<div class="divide-y divide-slate-200">
${insights.achievements.map(achievement => `
<div class="flex flex-col gap-1 py-3 text-left">
<span class="text-base font-semibold text-slate-900">
${achievement.name}
</span>
<p class="text-sm text-slate-600">
${achievement.description}
</p>
</div>
`).join('')}
</div>`
}
</div>
`;
}
function calculateTotalTokens(tokenUsage, type) {
return Object.values(tokenUsage).reduce((acc, usage) => acc + usage[type], 0);
}
function initializeHourChart(insights) {
const canvas = document.getElementById('hour-chart');
const canvas = chartRef.current;
if (!canvas || !window.Chart) return;
// Destroy existing chart if it exists
if (hourChartInstance) {
hourChartInstance.destroy();
}
const labels = Array.from({ length: 24 }, (_, i) => `${i}:00`);
const data = labels.map((_, i) => insights.activeHours[i] || 0);
const data = labels.map((_, i) => activeHours[i] || 0);
const ctx = canvas.getContext('2d');
if (!ctx) return;
hourChartInstance = new Chart(ctx, {
type: 'bar',
data: {
labels,
datasets: [
{
label: 'Activity per Hour',
data,
backgroundColor: 'rgba(52, 152, 219, 0.7)',
borderColor: 'rgba(52, 152, 219, 1)',
borderWidth: 1,
},
],
chartInstance.current = new Chart(ctx, {
type: 'bar',
data: {
labels,
datasets: [
{
label: 'Activity per Hour',
data,
backgroundColor: 'rgba(52, 152, 219, 0.7)',
borderColor: 'rgba(52, 152, 219, 1)',
borderWidth: 1,
},
],
},
options: {
indexAxis: 'y',
responsive: true,
maintainAspectRatio: false,
scales: {
x: {
beginAtZero: true,
},
},
options: {
indexAxis: 'y',
responsive: true,
maintainAspectRatio: false,
scales: {
x: {
beginAtZero: true,
},
},
plugins: {
legend: {
display: false,
},
},
plugins: {
legend: {
display: false,
},
},
},
});
return () => {
if (chartInstance.current) {
chartInstance.current.destroy();
}
};
}, [activeHours]);
return (
<div className={`${cardClass} h-full`}>
<div className="flex items-center justify-between">
<h3 className={sectionTitleClass}>Active Hours</h3>
<span className="rounded-full bg-slate-100 px-3 py-1 text-xs font-semibold text-slate-600">
24h
</span>
</div>
<div className="mt-4 h-56 w-full">
<canvas ref={chartRef} className="w-full h-56" />
</div>
</div>
);
}
function initializeHeatmap(insights) {
const heatmapContainer = document.getElementById('heatmap');
if (!heatmapContainer) return;
// Create a simple SVG heatmap
const svg = createHeatmapSVG(insights.heatmap);
heatmapContainer.innerHTML = svg;
// Work Session Card Component
function WorkSessionCard({
longestWorkDuration,
longestWorkDate,
latestActiveTime,
cardClass,
sectionTitleClass,
}) {
return (
<div className={`${cardClass} h-full space-y-3`}>
<h3 className={sectionTitleClass}>Work Session</h3>
<div className="grid grid-cols-2 gap-3 text-sm text-slate-700">
<div className="rounded-xl bg-slate-50 px-3 py-2">
<p className="text-xs font-semibold uppercase tracking-wide text-slate-500">
Longest
</p>
<p className="mt-1 text-lg font-semibold text-slate-900">
{longestWorkDuration}m
</p>
</div>
<div className="rounded-xl bg-slate-50 px-3 py-2">
<p className="text-xs font-semibold uppercase tracking-wide text-slate-500">
Date
</p>
<p className="mt-1 text-lg font-semibold text-slate-900">
{longestWorkDate || '-'}
</p>
</div>
<div className="col-span-2 rounded-xl bg-slate-50 px-3 py-2">
<p className="text-xs font-semibold uppercase tracking-wide text-slate-500">
Last Active
</p>
<p className="mt-1 text-lg font-semibold text-slate-900">
{latestActiveTime || '-'}
</p>
</div>
</div>
</div>
);
}
function createHeatmapSVG(heatmapData) {
const width = 1000;
const height = 150;
const cellSize = 14;
const cellPadding = 2;
// Heatmap Section Component
function HeatmapSection({ heatmap }) {
const cardClass = 'glass-card p-6';
const sectionTitleClass =
'text-lg font-semibold tracking-tight text-slate-900';
const today = new Date();
const oneYearAgo = new Date(today);
oneYearAgo.setFullYear(today.getFullYear() - 1);
return (
<div className={`${cardClass} mt-4 space-y-4 md:mt-6`}>
<div className="flex items-center justify-between">
<h3 className={sectionTitleClass}>Activity Heatmap</h3>
<span className="text-xs font-semibold text-slate-500">Past year</span>
</div>
<div className="heatmap-container">
<div className="min-w-[720px] rounded-xl border border-slate-100 bg-white/70 p-4 shadow-inner shadow-slate-100">
<ActivityHeatmap heatmapData={heatmap} />
</div>
</div>
</div>
);
}
// Generate all dates for the past year
const dates = [];
const currentDate = new Date(oneYearAgo);
while (currentDate <= today) {
dates.push(new Date(currentDate));
currentDate.setDate(currentDate.getDate() + 1);
// Activity Heatmap Component
function ActivityHeatmap({ heatmapData }) {
const width = 1000;
const height = 150;
const cellSize = 14;
const cellPadding = 2;
const today = new Date();
const oneYearAgo = new Date(today);
oneYearAgo.setFullYear(today.getFullYear() - 1);
// Generate all dates for the past year
const dates = [];
const currentDate = new Date(oneYearAgo);
while (currentDate <= today) {
dates.push(new Date(currentDate));
currentDate.setDate(currentDate.getDate() + 1);
}
const colorLevels = [0, 2, 4, 10, 20];
const colors = ['#e2e8f0', '#a5d8ff', '#74c0fc', '#339af0', '#1c7ed6'];
function getColor(value) {
if (value === 0) return colors[0];
for (let i = colorLevels.length - 1; i >= 1; i--) {
if (value >= colorLevels[i]) return colors[i];
}
return colors[1];
}
// Calculate max value for color scaling
const maxValue = Math.max(...Object.values(heatmapData));
const colorLevels = [0, 2, 4, 10, 20];
const colors = ['#e2e8f0', '#a5d8ff', '#74c0fc', '#339af0', '#1c7ed6'];
const weeksInYear = Math.ceil(dates.length / 7);
const startX = 50;
const startY = 20;
function getColor(value) {
if (value === 0) return colors[0];
for (let i = colorLevels.length - 1; i >= 1; i--) {
if (value >= colorLevels[i]) return colors[i];
}
return colors[1];
const months = [
'Jan',
'Feb',
'Mar',
'Apr',
'May',
'Jun',
'Jul',
'Aug',
'Sep',
'Oct',
'Nov',
'Dec',
];
// Generate month labels
const monthLabels = [];
let currentMonth = oneYearAgo.getMonth();
let monthX = startX;
for (let week = 0; week < weeksInYear; week++) {
const weekDate = new Date(oneYearAgo);
weekDate.setDate(weekDate.getDate() + week * 7);
if (weekDate.getMonth() !== currentMonth) {
currentMonth = weekDate.getMonth();
monthLabels.push({
x: monthX,
text: months[currentMonth],
});
monthX = startX + week * (cellSize + cellPadding);
}
}
let svg = `<svg class="heatmap-svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}">`;
// Calculate grid dimensions
const weeksInYear = Math.ceil(dates.length / 7);
const startX = 50;
const startY = 20;
dates.forEach((date, index) => {
return (
<svg
className="heatmap-svg"
width={width}
height={height}
viewBox={`0 0 ${width} ${height}`}
>
{/* Render heatmap cells */}
{dates.map((date, index) => {
const week = Math.floor(index / 7);
const day = index % 7;
@ -289,76 +300,211 @@ function createHeatmapSVG(heatmapData) {
const value = heatmapData[dateKey] || 0;
const color = getColor(value);
svg += `<rect class="heatmap-day"
x="${x}" y="${y}"
width="${cellSize}" height="${cellSize}"
rx="2"
fill="${color}"
data-date="${dateKey}"
data-count="${value}">
<title>${dateKey}: ${value} activities</title>
</rect>`;
});
return (
<rect
key={dateKey}
className="heatmap-day"
x={x}
y={y}
width={cellSize}
height={cellSize}
rx="2"
fill={color}
data-date={dateKey}
data-count={value}
>
<title>
{dateKey}: {value} activities
</title>
</rect>
);
})}
// Add month labels
const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
let currentMonth = oneYearAgo.getMonth();
let monthX = startX;
{/* Render month labels */}
{monthLabels.map((label, index) => (
<text key={index} x={label.x} y="15" fontSize="12" fill="#64748b">
{label.text}
</text>
))}
for (let week = 0; week < weeksInYear; week++) {
const weekDate = new Date(oneYearAgo);
weekDate.setDate(weekDate.getDate() + week * 7);
if (weekDate.getMonth() !== currentMonth) {
currentMonth = weekDate.getMonth();
svg += `<text x="${monthX}" y="15" font-size="12" fill="#64748b">${months[currentMonth]}</text>`;
monthX = startX + week * (cellSize + cellPadding);
}
}
// Add legend
const legendY = height - 30;
svg += '<text x="' + startX + '" y="' + (legendY - 10) + '" font-size="12" fill="#64748b">Less</text>';
colors.forEach((color, index) => {
{/* Render legend */}
<text x={startX} y={height - 40} fontSize="12" fill="#64748b">
Less
</text>
{colors.map((color, index) => {
const legendX = startX + 40 + index * (cellSize + 2);
svg += `<rect x="${legendX}" y="${legendY}" width="10" height="10" rx="2" fill="${color}"></rect>`;
});
svg += '<text x="' + (startX + 40 + colors.length * (cellSize + 2) + 5) + '" y="' + (legendY + 9) + '" font-size="12" fill="#64748b">More</text>';
svg += '</svg>';
return svg;
return (
<rect
key={index}
x={legendX}
y={height - 30}
width="10"
height="10"
rx="2"
fill={color}
/>
);
})}
<text
x={startX + 40 + colors.length * (cellSize + 2) + 5}
y={height - 21}
fontSize="12"
fill="#64748b"
>
More
</text>
</svg>
);
}
// Export functionality
function handleExport() {
// Token Usage Section Component
function TokenUsageSection({ tokenUsage }) {
const cardClass = 'glass-card p-6';
const sectionTitleClass =
'text-lg font-semibold tracking-tight text-slate-900';
function calculateTotalTokens(tokenUsage, type) {
return Object.values(tokenUsage).reduce(
(acc, usage) => acc + usage[type],
0,
);
}
return (
<div className={`${cardClass} mt-4 md:mt-6`}>
<div className="space-y-3">
<h3 className={sectionTitleClass}>Token Usage</h3>
<div className="grid grid-cols-3 gap-3">
<TokenUsageCard
label="Input"
value={calculateTotalTokens(tokenUsage, 'input').toLocaleString()}
/>
<TokenUsageCard
label="Output"
value={calculateTotalTokens(tokenUsage, 'output').toLocaleString()}
/>
<TokenUsageCard
label="Total"
value={calculateTotalTokens(tokenUsage, 'total').toLocaleString()}
/>
</div>
</div>
</div>
);
}
// Token Usage Card Component
function TokenUsageCard({ label, value }) {
return (
<div className="rounded-xl bg-slate-50 px-4 py-3">
<p className="text-xs font-semibold uppercase tracking-wide text-slate-500">
{label}
</p>
<p className="mt-1 text-2xl font-bold text-slate-900">{value}</p>
</div>
);
}
// Achievements Section Component
function AchievementsSection({ achievements }) {
const cardClass = 'glass-card p-6';
const sectionTitleClass =
'text-lg font-semibold tracking-tight text-slate-900';
return (
<div className={`${cardClass} mt-4 space-y-4 md:mt-6`}>
<div className="flex items-center justify-between">
<h3 className={sectionTitleClass}>Achievements</h3>
<span className="text-xs font-semibold text-slate-500">
{achievements.length} total
</span>
</div>
{achievements.length === 0 ? (
<p className="text-sm text-slate-600">
No achievements yet. Keep coding!
</p>
) : (
<div className="divide-y divide-slate-200">
{achievements.map((achievement, index) => (
<AchievementItem key={index} achievement={achievement} />
))}
</div>
)}
</div>
);
}
// Achievement Item Component
function AchievementItem({ achievement }) {
return (
<div className="flex flex-col gap-1 py-3 text-left">
<span className="text-base font-semibold text-slate-900">
{achievement.name}
</span>
<p className="text-sm text-slate-600">{achievement.description}</p>
</div>
);
}
// Export Button Component
function ExportButton() {
const [isExporting, setIsExporting] = useState(false);
const handleExport = async () => {
const container = document.getElementById('container');
const button = document.getElementById('export-btn');
if (!container || !window.html2canvas) {
alert('Export functionality is not available.');
return;
alert('Export functionality is not available.');
return;
}
button.style.display = 'none';
setIsExporting(true);
html2canvas(container, {
try {
const canvas = await html2canvas(container, {
scale: 2,
useCORS: true,
logging: false,
}).then(function(canvas) {
const imgData = canvas.toDataURL('image/png');
const link = document.createElement('a');
link.href = imgData;
link.download = `qwen-insights-${new Date().toISOString().slice(0, 10)}.png`;
link.click();
});
button.style.display = 'block';
}).catch(function(error) {
console.error('Error capturing image:', error);
alert('Failed to export image. Please try again.');
button.style.display = 'block';
});
}
const imgData = canvas.toDataURL('image/png');
const link = document.createElement('a');
link.href = imgData;
link.download = `qwen-insights-${new Date().toISOString().slice(0, 10)}.png`;
link.click();
} catch (error) {
console.error('Export error:', error);
alert('Failed to export image. Please try again.');
} finally {
setIsExporting(false);
}
};
return (
<div className="mt-6 flex justify-center">
<button
onClick={handleExport}
disabled={isExporting}
className="group inline-flex items-center gap-2 rounded-full bg-slate-900 px-5 py-3 text-sm font-semibold text-white shadow-soft transition focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-slate-400 hover:-translate-y-[1px] hover:shadow-lg active:translate-y-[1px] disabled:opacity-50"
>
{isExporting ? 'Exporting...' : 'Export as Image'}
<span className="text-slate-200 transition group-hover:translate-x-0.5">
</span>
</button>
</div>
);
}
// App Initialization - Mount React app when DOM is ready
const container = document.getElementById('react-root');
if (container && window.INSIGHT_DATA && window.ReactDOM) {
const root = ReactDOM.createRoot(container);
root.render(React.createElement(InsightApp, { data: window.INSIGHT_DATA }));
} else {
console.error('Failed to mount React app:', {
container: !!container,
data: !!window.INSIGHT_DATA,
ReactDOM: !!window.ReactDOM,
});
}

View file

@ -48,4 +48,4 @@ export interface StaticInsightTemplateData {
data: InsightData;
scripts: string;
generatedTime: string;
}
}