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 = ``; - - // Calculate grid dimensions - const weeksInYear = Math.ceil(dates.length / 7); - const startX = 50; - const startY = 20; - - dates.forEach((date, index) => { + return ( + + {/* 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 += ` - ${dateKey}: ${value} activities - `; - }); + return ( + + + {dateKey}: {value} activities + + + ); + })} - // 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) => ( + + {label.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 += `${months[currentMonth]}`; - monthX = startX + week * (cellSize + cellPadding); - } - } - - // Add legend - const legendY = height - 30; - svg += 'Less'; - - colors.forEach((color, index) => { + {/* Render legend */} + + Less + + {colors.map((color, index) => { const legendX = startX + 40 + index * (cellSize + 2); - svg += ``; - }); - - svg += 'More'; - - svg += ''; - return svg; + return ( + + ); + })} + + More + + + ); } -// 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); } }