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 (