diff --git a/packages/cli/assets/insight/build.mjs b/packages/cli/assets/insight/build.mjs new file mode 100644 index 000000000..11de26102 --- /dev/null +++ b/packages/cli/assets/insight/build.mjs @@ -0,0 +1,63 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +/* eslint-disable no-undef */ +import { writeFile, readFile } from 'node:fs/promises'; +import { fileURLToPath } from 'node:url'; +import { dirname, join } from 'node:path'; +import { build } from 'vite'; + +const assetsDir = dirname(fileURLToPath(import.meta.url)); +const distDir = join(assetsDir, 'dist'); + +const templateModulePath = join( + assetsDir, + '..', + '..', + 'src', + 'services', + 'insight', + 'templates', + 'insightTemplate.ts', +); + +console.log('Building insight assets with Vite...'); +await build(); + +console.log('Reading generated files...'); +let jsContent = ''; +let cssContent = ''; + +try { + jsContent = await readFile(join(distDir, 'main.js'), 'utf-8'); +} catch (e) { + console.error('Failed to read main.js from dist'); + throw e; +} + +try { + // Try style.css first (standard Vite lib mode output) + cssContent = await readFile(join(distDir, 'style.css'), 'utf-8'); +} catch (e) { + try { + // Try main.css (if configured via assetFileNames) + cssContent = await readFile(join(distDir, 'main.css'), 'utf-8'); + } catch (e2) { + console.warn( + 'No CSS file found in dist (style.css or main.css). Using empty string.', + ); + } +} + +const templateModule = `/** + * @license + * Copyright 2025 Qwen Team + * SPDX-License-Identifier: Apache-2.0 + * + * This file is code-generated; do not edit manually. + */ + +export const INSIGHT_JS = ${JSON.stringify(jsContent.trim())}; +export const INSIGHT_CSS = ${JSON.stringify(cssContent.trim())}; +`; + +await writeFile(templateModulePath, templateModule); +console.log(`Successfully generated ${templateModulePath}`); diff --git a/packages/cli/assets/insight/package.json b/packages/cli/assets/insight/package.json new file mode 100644 index 000000000..53103e99b --- /dev/null +++ b/packages/cli/assets/insight/package.json @@ -0,0 +1,19 @@ +{ + "name": "@qwen-code/cli-insight", + "private": true, + "type": "module", + "scripts": { + "build": "node build.mjs" + }, + "dependencies": {}, + "devDependencies": { + "@tailwindcss/postcss": "^4.1.18", + "@types/react": "^18.2.0", + "@types/react-dom": "^18.2.0", + "@vitejs/plugin-react": "^4.2.0", + "autoprefixer": "^10.4.24", + "postcss": "^8.5.6", + "tailwindcss": "^4.1.18", + "vite": "^5.0.0" + } +} diff --git a/packages/cli/assets/insight/postcss.config.js b/packages/cli/assets/insight/postcss.config.js new file mode 100644 index 000000000..51a6e4e62 --- /dev/null +++ b/packages/cli/assets/insight/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + '@tailwindcss/postcss': {}, + autoprefixer: {}, + }, +}; diff --git a/packages/cli/src/services/insight/templates/scripts/components/App.js b/packages/cli/assets/insight/src/App.tsx similarity index 76% rename from packages/cli/src/services/insight/templates/scripts/components/App.js rename to packages/cli/assets/insight/src/App.tsx index 2960c270b..47c00725c 100644 --- a/packages/cli/src/services/insight/templates/scripts/components/App.js +++ b/packages/cli/assets/insight/src/App.tsx @@ -1,9 +1,24 @@ -/* eslint-disable react/jsx-no-undef */ -/* eslint-disable react/prop-types */ -/* eslint-disable no-undef */ +import { useState } from 'react'; +import ReactDOM from 'react-dom/client'; +import { Header, StatsRow } from './Header'; +import { + AtAGlance, + NavToc, + ProjectAreas, + InteractionStyle, + ImpressiveWorkflows, + FrictionPoints, + Improvements, + FutureOpportunities, + MemorableMoment, +} from './Qualitative'; +import './styles.css'; +import { InsightData } from './types'; +// eslint-disable-next-line @typescript-eslint/no-unused-vars +import React from 'react'; // Main App Component -function InsightApp({ data }) { +function InsightApp({ data }: { data: InsightData }) { if (!data) { return (
@@ -17,9 +32,10 @@ function InsightApp({ data }) { let dateRangeStr = ''; if (heatmapKeys.length > 0) { const dates = heatmapKeys.map((d) => new Date(d)); - const minDate = new Date(Math.min(...dates)); - const maxDate = new Date(Math.max(...dates)); - const formatDate = (d) => d.toISOString().split('T')[0]; + const timestamps = dates.map((d) => d.getTime()); + const minDate = new Date(Math.min(...timestamps)); + const maxDate = new Date(Math.max(...timestamps)); + const formatDate = (d: Date) => d.toISOString().split('T')[0]; dateRangeStr = `${formatDate(minDate)} to ${formatDate(maxDate)}`; } @@ -56,8 +72,8 @@ function InsightApp({ data }) { <> ); } else { console.error('Failed to mount React app:', { container: !!container, data: !!window.INSIGHT_DATA, - ReactDOM: !!window.ReactDOM, + ReactDOM: !!ReactDOM, }); } diff --git a/packages/cli/src/services/insight/templates/scripts/components/Charts.js b/packages/cli/assets/insight/src/Charts.tsx similarity index 88% rename from packages/cli/src/services/insight/templates/scripts/components/Charts.js rename to packages/cli/assets/insight/src/Charts.tsx index 836e072b8..51cfd66af 100644 --- a/packages/cli/src/services/insight/templates/scripts/components/Charts.js +++ b/packages/cli/assets/insight/src/Charts.tsx @@ -1,13 +1,16 @@ -/* eslint-disable @typescript-eslint/no-unused-vars */ -/* eslint-disable react/prop-types */ -/* eslint-disable no-undef */ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { InsightData } from './types'; + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +import React, { useRef, useEffect } from 'react'; +const Chart = (window as any).Chart; // ----------------------------------------------------------------------------- // Existing Components // ----------------------------------------------------------------------------- // Dashboard Cards Component -function DashboardCards({ insights }) { +export function DashboardCards({ insights }: { insights: InsightData }) { const cardClass = 'glass-card p-6'; const sectionTitleClass = 'text-lg font-semibold tracking-tight text-slate-900'; @@ -38,7 +41,17 @@ function DashboardCards({ insights }) { } // Streak Card Component -function StreakCard({ currentStreak, longestStreak, cardClass, captionClass }) { +export function StreakCard({ + currentStreak, + longestStreak, + cardClass, + captionClass, +}: { + currentStreak: number; + longestStreak: number; + cardClass: string; + captionClass: string; +}) { return (
@@ -60,9 +73,17 @@ function StreakCard({ currentStreak, longestStreak, cardClass, captionClass }) { } // Active Hours Chart Component -function ActiveHoursChart({ activeHours, cardClass, sectionTitleClass }) { - const chartRef = useRef(null); - const chartInstance = useRef(null); +export function ActiveHoursChart({ + activeHours, + cardClass, + sectionTitleClass, +}: { + activeHours: Record; + cardClass: string; + sectionTitleClass: string; +}) { + const chartRef = useRef(null); + const chartInstance = useRef(null); useEffect(() => { if (chartInstance.current) { @@ -138,6 +159,12 @@ function WorkSessionCard({ latestActiveTime, cardClass, sectionTitleClass, +}: { + longestWorkDuration: number; + longestWorkDate: string | null; + latestActiveTime: string | null; + cardClass: string; + sectionTitleClass: string; }) { return (
@@ -173,7 +200,11 @@ function WorkSessionCard({ } // Heatmap Section Component -function HeatmapSection({ heatmap }) { +export function HeatmapSection({ + heatmap, +}: { + heatmap: Record; +}) { const cardClass = 'glass-card p-6'; const sectionTitleClass = 'text-lg font-semibold tracking-tight text-slate-900'; @@ -194,7 +225,11 @@ function HeatmapSection({ heatmap }) { } // Activity Heatmap Component -function ActivityHeatmap({ heatmapData }) { +function ActivityHeatmap({ + heatmapData, +}: { + heatmapData: Record; +}) { const width = 1000; const height = 150; const cellSize = 14; @@ -215,7 +250,7 @@ function ActivityHeatmap({ heatmapData }) { const colorLevels = [0, 2, 4, 10, 20]; const colors = ['#e2e8f0', '#a5d8ff', '#74c0fc', '#339af0', '#1c7ed6']; - function getColor(value) { + function getColor(value: number) { if (value === 0) return colors[0]; for (let i = colorLevels.length - 1; i >= 1; i--) { if (value >= colorLevels[i]) return colors[i]; diff --git a/packages/cli/src/services/insight/templates/scripts/components/utils.js b/packages/cli/assets/insight/src/Components.tsx similarity index 72% rename from packages/cli/src/services/insight/templates/scripts/components/utils.js rename to packages/cli/assets/insight/src/Components.tsx index 75d371a4b..e64df68fa 100644 --- a/packages/cli/src/services/insight/templates/scripts/components/utils.js +++ b/packages/cli/assets/insight/src/Components.tsx @@ -1,11 +1,9 @@ -/* eslint-disable @typescript-eslint/no-unused-vars */ -/* eslint-disable react/prop-types */ -/* eslint-disable no-undef */ - -const { useState, useRef, useEffect } = React; +// eslint-disable-next-line @typescript-eslint/no-unused-vars +import React from 'react'; +import { useState } from 'react'; // Simple Markdown Parser Component -function MarkdownText({ children }) { +export function MarkdownText({ children }: { children: string }) { if (!children || typeof children !== 'string') return children; // Split by bold markers (**text**) @@ -23,7 +21,13 @@ function MarkdownText({ children }) { ); } -function CopyButton({ text, label = 'Copy' }) { +export function CopyButton({ + text, + label = 'Copy', +}: { + text: string; + label?: string; +}) { const [copied, setCopied] = useState(false); const handleCopy = () => { diff --git a/packages/cli/src/services/insight/templates/scripts/components/Header.js b/packages/cli/assets/insight/src/Header.tsx similarity index 77% rename from packages/cli/src/services/insight/templates/scripts/components/Header.js rename to packages/cli/assets/insight/src/Header.tsx index 4f4fb1816..300b720c4 100644 --- a/packages/cli/src/services/insight/templates/scripts/components/Header.js +++ b/packages/cli/assets/insight/src/Header.tsx @@ -1,8 +1,15 @@ -/* eslint-disable @typescript-eslint/no-unused-vars */ -/* eslint-disable react/prop-types */ +// eslint-disable-next-line @typescript-eslint/no-unused-vars +import React from 'react'; +import { InsightData } from './types'; // Header Component -function Header({ data, dateRangeStr }) { +export function Header({ + data, + dateRangeStr, +}: { + data: InsightData; + dateRangeStr: string; +}) { const { totalMessages, totalSessions } = data; return ( @@ -20,7 +27,7 @@ function Header({ data, dateRangeStr }) { ); } -function StatsRow({ data }) { +export function StatsRow({ data }: { data: InsightData }) { const { totalMessages = 0, totalLinesAdded = 0, @@ -34,9 +41,10 @@ function StatsRow({ data }) { let daysSpan = 0; if (heatmapKeys.length > 0) { const dates = heatmapKeys.map((d) => new Date(d)); - const minDate = new Date(Math.min(...dates)); - const maxDate = new Date(Math.max(...dates)); - const diffTime = Math.abs(maxDate - minDate); + const timestamps = dates.map((d) => d.getTime()); + const minDate = new Date(Math.min(...timestamps)); + const maxDate = new Date(Math.max(...timestamps)); + const diffTime = Math.abs(maxDate.getTime() - minDate.getTime()); daysSpan = Math.ceil(diffTime / (1000 * 60 * 60 * 24)) + 1; } diff --git a/packages/cli/src/services/insight/templates/scripts/components/Qualitative.js b/packages/cli/assets/insight/src/Qualitative.tsx similarity index 91% rename from packages/cli/src/services/insight/templates/scripts/components/Qualitative.js rename to packages/cli/assets/insight/src/Qualitative.tsx index de2a44b2a..f9b3500e0 100644 --- a/packages/cli/src/services/insight/templates/scripts/components/Qualitative.js +++ b/packages/cli/assets/insight/src/Qualitative.tsx @@ -1,13 +1,16 @@ -/* eslint-disable react/jsx-no-undef */ -/* eslint-disable @typescript-eslint/no-unused-vars */ -/* eslint-disable react/prop-types */ -/* eslint-disable no-undef */ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { useState } from 'react'; +import { DashboardCards, HeatmapSection } from './Charts'; +import { InsightData, QualitativeData } from './types'; +import { CopyButton, MarkdownText } from './Components'; +// eslint-disable-next-line @typescript-eslint/no-unused-vars +import React from 'react'; // ----------------------------------------------------------------------------- // Qualitative Insight Components // ----------------------------------------------------------------------------- -function AtAGlance({ qualitative }) { +export function AtAGlance({ qualitative }: { qualitative: QualitativeData }) { const { atAGlance } = qualitative; if (!atAGlance) return null; @@ -48,7 +51,7 @@ function AtAGlance({ qualitative }) { ); } -function NavToc() { +export function NavToc() { return (