diff --git a/packages/cli/src/services/insight/generators/TemplateRenderer.ts b/packages/cli/src/services/insight/generators/TemplateRenderer.ts
index 2b5d8262e..ac42e03e0 100644
--- a/packages/cli/src/services/insight/generators/TemplateRenderer.ts
+++ b/packages/cli/src/services/insight/generators/TemplateRenderer.ts
@@ -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';
diff --git a/packages/cli/src/services/insight/templates/insight-template.html b/packages/cli/src/services/insight/templates/insight-template.html
index 65bc01b46..a3506dcc7 100644
--- a/packages/cli/src/services/insight/templates/insight-template.html
+++ b/packages/cli/src/services/insight/templates/insight-template.html
@@ -25,27 +25,22 @@
-
-
-
+
+
+
+
+
+
+
-
diff --git a/packages/cli/src/services/insight/templates/scripts/insight-app.js b/packages/cli/src/services/insight/templates/scripts/insight-app.js
index 130a54bd4..e3f284250 100644
--- a/packages/cli/src/services/insight/templates/scripts/insight-app.js
+++ b/packages/cli/src/services/insight/templates/scripts/insight-app.js
@@ -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 (
+
+ No insight data available
+
+ );
+ }
-function initializeApp() {
- const insights = window.INSIGHT_DATA;
+ return (
+
+ );
+}
- 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 (
+
+ );
+}
+
+// Streak Card Component
+function StreakCard({ currentStreak, longestStreak, cardClass, captionClass }) {
+ return (
+
+
+
+
Current Streak
+
+ {currentStreak}
+
+ days
+
+
+
+
+ Longest {longestStreak}d
+
+
+
+ );
+}
+
+// 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 `
-
-
-
-
-
Current Streak
-
- ${insights.currentStreak}
-
- days
-
-
-
-
- Longest ${insights.longestStreak}d
-
-
-
-
-
-
-
Active Hours
-
- 24h
-
-
-
-
-
-
-
-
-
Work Session
-
-
-
- Longest
-
-
- ${insights.longestWorkDuration}m
-
-
-
-
- Date
-
-
- ${insights.longestWorkDate || '-'}
-
-
-
-
- Last Active
-
-
- ${insights.latestActiveTime || '-'}
-
-
-
-
-
-
-
-
-
Activity Heatmap
-
- Past year
-
-
-
-
-
-
-
-
Token Usage
-
-
-
- Input
-
-
- ${calculateTotalTokens(insights.tokenUsage, 'input').toLocaleString()}
-
-
-
-
- Output
-
-
- ${calculateTotalTokens(insights.tokenUsage, 'output').toLocaleString()}
-
-
-
-
- Total
-
-
- ${calculateTotalTokens(insights.tokenUsage, 'total').toLocaleString()}
-
-
-
-
-
-
-
-
-
Achievements
-
- ${insights.achievements.length} total
-
-
- ${insights.achievements.length === 0 ?
- '
No achievements yet. Keep coding!
' :
- `
- ${insights.achievements.map(achievement => `
-
-
- ${achievement.name}
-
-
- ${achievement.description}
-
-
- `).join('')}
-
`
- }
-
- `;
-}
-
-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 (
+
+
+
Active Hours
+
+ 24h
+
+
+
+
+
+
+ );
}
-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 (
+
+
Work Session
+
+
+
+ Longest
+
+
+ {longestWorkDuration}m
+
+
+
+
+ Date
+
+
+ {longestWorkDate || '-'}
+
+
+
+
+ Last Active
+
+
+ {latestActiveTime || '-'}
+
+
+
+
+ );
}
-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 (
+
+
+
Activity Heatmap
+ Past year
+
+
+
+ );
+}
- // 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 = `
+ );
}
-// 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 (
+
+
+
Token Usage
+
+
+
+
+
+
+
+ );
+}
+
+// Token Usage Card Component
+function TokenUsageCard({ label, value }) {
+ return (
+
+
+ {label}
+
+
{value}
+
+ );
+}
+
+// Achievements Section Component
+function AchievementsSection({ achievements }) {
+ const cardClass = 'glass-card p-6';
+ const sectionTitleClass =
+ 'text-lg font-semibold tracking-tight text-slate-900';
+
+ return (
+
+
+
Achievements
+
+ {achievements.length} total
+
+
+ {achievements.length === 0 ? (
+
+ No achievements yet. Keep coding!
+
+ ) : (
+
+ {achievements.map((achievement, index) => (
+
+ ))}
+
+ )}
+
+ );
+}
+
+// Achievement Item Component
+function AchievementItem({ achievement }) {
+ return (
+
+
+ {achievement.name}
+
+
{achievement.description}
+
+ );
+}
+
+// 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';
- });
-}
\ No newline at end of file
+ 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 (
+
+
+
+ );
+}
+
+// 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,
+ });
+}
diff --git a/packages/cli/src/services/insight/types/StaticInsightTypes.ts b/packages/cli/src/services/insight/types/StaticInsightTypes.ts
index f0786661d..c79589f17 100644
--- a/packages/cli/src/services/insight/types/StaticInsightTypes.ts
+++ b/packages/cli/src/services/insight/types/StaticInsightTypes.ts
@@ -48,4 +48,4 @@ export interface StaticInsightTemplateData {
data: InsightData;
scripts: string;
generatedTime: string;
-}
\ No newline at end of file
+}
diff --git a/scripts/copy_files.js b/scripts/copy_files.js
index 9f1d318e9..efaad7498 100644
--- a/scripts/copy_files.js
+++ b/scripts/copy_files.js
@@ -49,7 +49,10 @@ function copyFilesRecursive(source, target, rootSourceDir) {
const normalizedPath = relativePath.replace(/\\/g, '/');
const isLocaleJs =
ext === '.js' && normalizedPath.startsWith('i18n/locales/');
- if (extensionsToCopy.includes(ext) || isLocaleJs) {
+ const isInsightTemplate = normalizedPath.startsWith(
+ 'services/insight/templates/',
+ );
+ if (extensionsToCopy.includes(ext) || isLocaleJs || isInsightTemplate) {
fs.copyFileSync(sourcePath, targetPath);
}
}