feat(insight): Refactor code structure for improved readability and maintainability

This commit is contained in:
DragonnZhang 2026-02-09 19:00:57 +08:00
parent 2edce464ae
commit e66c203cb0
18 changed files with 423 additions and 229 deletions

View file

@ -6,7 +6,10 @@
import fs from 'fs/promises';
import path from 'path';
import { read as readJsonlFile } from '@qwen-code/qwen-code-core';
import {
read as readJsonlFile,
createDebugLogger,
} from '@qwen-code/qwen-code-core';
import pLimit from 'p-limit';
import type { Config, ChatRecord } from '@qwen-code/qwen-code-core';
import type {
@ -40,6 +43,8 @@ import {
ANALYSIS_PROMPT,
} from '../prompts/InsightPrompts.js';
const logger = createDebugLogger('DataProcessor');
export class DataProcessor {
constructor(private config: Config) {}
@ -194,7 +199,7 @@ export class DataProcessor {
session_id: records[0].sessionId,
};
} catch (error) {
console.error(
logger.error(
`Failed to analyze session ${records[0]?.sessionId}:`,
error,
);
@ -362,7 +367,7 @@ export class DataProcessor {
return undefined;
}
console.log('Generating qualitative insights...');
logger.info('Generating qualitative insights...');
const commonData = this.prepareCommonPromptData(metrics, facets);
@ -380,7 +385,7 @@ export class DataProcessor {
});
return result as T;
} catch (error) {
console.error('Failed to generate insight:', error);
logger.error('Failed to generate insight:', error);
throw error;
}
};
@ -620,39 +625,6 @@ export class DataProcessor {
),
]);
console.log(
'🚀 ~ DataProcessor ~ generateQualitativeInsights ~ impressiveWorkflows:',
impressiveWorkflows,
);
console.log(
'🚀 ~ DataProcessor ~ generateQualitativeInsights ~ atAGlance:',
atAGlance,
);
console.log(
'🚀 ~ DataProcessor ~ generateQualitativeInsights ~ interactionStyle:',
interactionStyle,
);
console.log(
'🚀 ~ DataProcessor ~ generateQualitativeInsights ~ improvements:',
improvements,
);
console.log(
'🚀 ~ DataProcessor ~ generateQualitativeInsights ~ memorableMoment:',
memorableMoment,
);
console.log(
'🚀 ~ DataProcessor ~ generateQualitativeInsights ~ frictionPoints:',
frictionPoints,
);
console.log(
'🚀 ~ DataProcessor ~ generateQualitativeInsights ~ futureOpportunities:',
futureOpportunities,
);
console.log(
'🚀 ~ DataProcessor ~ generateQualitativeInsights ~ projectAreas:',
projectAreas,
);
return {
impressiveWorkflows,
projectAreas,
@ -664,7 +636,7 @@ export class DataProcessor {
atAGlance,
};
} catch (e) {
console.error('Error generating qualitative insights:', e);
logger.error('Error generating qualitative insights:', e);
return undefined;
}
}
@ -783,12 +755,12 @@ None captured`;
const fileStats = await fs.stat(filePath);
allChatFiles.push({ path: filePath, mtime: fileStats.mtimeMs });
} catch (e) {
console.error(`Failed to stat file ${filePath}:`, e);
logger.error(`Failed to stat file ${filePath}:`, e);
}
}
} catch (error) {
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') {
console.log(
logger.error(
`Error reading chats directory for project ${projectDir}: ${error}`,
);
}
@ -800,9 +772,9 @@ None captured`;
} catch (error) {
if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
// Base directory doesn't exist, return empty
console.log(`Base directory does not exist: ${baseDir}`);
logger.info(`Base directory does not exist: ${baseDir}`);
} else {
console.log(`Error reading base directory: ${error}`);
logger.error(`Error reading base directory: ${error}`);
}
}
@ -898,7 +870,7 @@ None captured`;
}
}
} catch (error) {
console.error(
logger.error(
`Failed to process metrics for file ${fileInfo.path}:`,
error,
);
@ -994,10 +966,10 @@ None captured`;
.sort((a, b) => b.mtime - a.mtime)
.slice(0, 50);
console.log(`Analyzing ${recentFiles.length} recent sessions with LLM...`);
logger.info(`Analyzing ${recentFiles.length} recent sessions with LLM...`);
// Create a limit function with concurrency of 4 to avoid 429 errors
const limit = pLimit(4);
const limit = pLimit(2);
let completed = 0;
const total = recentFiles.length;
@ -1036,7 +1008,7 @@ None captured`;
} catch (readError) {
// File doesn't exist or is invalid, proceed to analyze
if ((readError as NodeJS.ErrnoException).code !== 'ENOENT') {
console.warn(
logger.warn(
`Failed to read existing facet for ${sessionId}, regenerating:`,
readError,
);
@ -1059,7 +1031,7 @@ None captured`;
'utf-8',
);
} catch (writeError) {
console.error(
logger.error(
`Failed to write facet file for session ${facet.session_id}:`,
writeError,
);
@ -1074,7 +1046,7 @@ None captured`;
return facet;
} catch (e) {
console.error(`Error analyzing session file ${fileInfo.path}:`, e);
logger.error(`Error analyzing session file ${fileInfo.path}:`, e);
completed++;
if (onProgress) {
const percent = 20 + Math.round((completed / total) * 60);

View file

@ -37,33 +37,25 @@ export class StaticInsightGenerator {
baseDir: string,
onProgress?: InsightProgressCallback,
): Promise<string> {
try {
// Ensure output directory exists
const outputDir = await this.ensureOutputDirectory();
const facetsDir = path.join(outputDir, 'facets');
await fs.mkdir(facetsDir, { recursive: true });
// Ensure output directory exists
const outputDir = await this.ensureOutputDirectory();
const facetsDir = path.join(outputDir, 'facets');
await fs.mkdir(facetsDir, { recursive: true });
// Process data
console.log('Processing insight data...');
const insights: InsightData = await this.dataProcessor.generateInsights(
baseDir,
facetsDir,
onProgress,
);
// Process data
const insights: InsightData = await this.dataProcessor.generateInsights(
baseDir,
facetsDir,
onProgress,
);
// Render HTML
console.log('Rendering HTML template...');
const html = await this.templateRenderer.renderInsightHTML(insights);
// Render HTML
const html = await this.templateRenderer.renderInsightHTML(insights);
const outputPath = path.join(outputDir, 'insight.html');
const outputPath = path.join(outputDir, 'insight.html');
// Write the HTML file
console.log(`Writing HTML file to: ${outputPath}`);
await fs.writeFile(outputPath, html, 'utf-8');
return outputPath;
} catch (error) {
console.log(`Error generating static insight: ${error}`);
throw error;
}
// Write the HTML file
await fs.writeFile(outputPath, html, 'utf-8');
return outputPath;
}
}

View file

@ -4,80 +4,48 @@
* SPDX-License-Identifier: Apache-2.0
*/
import fs from 'fs/promises';
import { existsSync } from 'fs';
import path, { dirname } from 'path';
import { fileURLToPath } from 'url';
import { INSIGHT_JS, INSIGHT_CSS } from '../templates/insightTemplate.js';
import type { InsightData } from '../types/StaticInsightTypes.js';
export class TemplateRenderer {
private templateDir: string;
constructor() {
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
// In bundled version (dist/cli.js), __dirname is dist/, templates at dist/templates/
// In development (dist/src/services/insight/generators/), templates at dist/src/services/insight/templates/
const bundledTemplatePath = path.join(__dirname, 'templates');
const devTemplatePath = path.join(__dirname, '..', 'templates');
// Try bundled path first (for production), fall back to dev path
try {
// Check if bundled templates exist
if (existsSync(bundledTemplatePath)) {
this.templateDir = bundledTemplatePath;
} else {
this.templateDir = devTemplatePath;
}
} catch {
// If check fails, use dev path as fallback
this.templateDir = devTemplatePath;
}
}
// Load template files
private async loadTemplate(): Promise<string> {
const templatePath = path.join(this.templateDir, 'insight-template.html');
return await fs.readFile(templatePath, 'utf-8');
}
private async loadStyles(): Promise<string> {
const stylesPath = path.join(this.templateDir, 'styles', 'base.css');
return await fs.readFile(stylesPath, 'utf-8');
}
private async loadScripts(): Promise<string> {
const componentsDir = path.join(this.templateDir, 'scripts', 'components');
const componentFiles = [
'utils.js',
'Header.js',
'Qualitative.js',
'Charts.js',
'App.js',
];
const scripts = await Promise.all(
componentFiles.map((file) =>
fs.readFile(path.join(componentsDir, file), 'utf-8'),
),
);
return scripts.join('\n\n');
}
// Render the complete HTML file
async renderInsightHTML(insights: InsightData): Promise<string> {
const template = await this.loadTemplate();
const styles = await this.loadStyles();
const scripts = await this.loadScripts();
const html = `<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Qwen Code Insights</title>
<style>
${INSIGHT_CSS}
</style>
</head>
<body>
<div class="min-h-screen" id="container">
<div class="mx-auto max-w-6xl px-6 py-10 md:py-12">
<div id="react-root"></div>
</div>
</div>
// Replace all placeholders
let html = template;
html = html.replace('{{STYLES_PLACEHOLDER}}', styles);
html = html.replace('{{DATA_PLACEHOLDER}}', JSON.stringify(insights));
html = html.replace('{{SCRIPTS_PLACEHOLDER}}', scripts);
<!-- 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>
<!-- 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>
window.INSIGHT_DATA = ${JSON.stringify(insights)};
</script>
<!-- App Script -->
<script>
${INSIGHT_JS}
</script>
</body>
</html>`;
return html;
}

View file

@ -1,34 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Qwen Code Insights</title>
<style>
{{STYLES_PLACEHOLDER}}
</style>
</head>
<body>
<div class="min-h-screen" id="container">
<div class="mx-auto max-w-6xl px-6 py-10 md:py-12">
<!-- 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 type="text/babel">
window.INSIGHT_DATA = {{DATA_PLACEHOLDER}};
{{SCRIPTS_PLACEHOLDER}}
</script>
</body>
</html>

File diff suppressed because one or more lines are too long

View file

@ -1,139 +0,0 @@
/* eslint-disable react/jsx-no-undef */
/* eslint-disable react/prop-types */
/* eslint-disable no-undef */
// Main App Component
function InsightApp({ data }) {
if (!data) {
return (
<div className="text-center text-slate-600">
No insight data available
</div>
);
}
// Calculate date range
const heatmapKeys = Object.keys(data.heatmap || {});
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];
dateRangeStr = `${formatDate(minDate)} to ${formatDate(maxDate)}`;
}
return (
<div>
<Header data={data} dateRangeStr={dateRangeStr} />
{data.qualitative && (
<>
<AtAGlance qualitative={data.qualitative} />
<NavToc />
</>
)}
<StatsRow data={data} />
{data.qualitative && (
<>
<ProjectAreas
qualitative={data.qualitative}
topGoals={data.topGoals}
topTools={data.topTools}
/>
</>
)}
{data.qualitative && (
<>
<InteractionStyle qualitative={data.qualitative} insights={data} />
</>
)}
{data.qualitative && (
<>
<ImpressiveWorkflows
qualitative={data.qualitative}
primarySuccess={data.primarySuccess}
outcomes={data.outcomes}
/>
<FrictionPoints
qualitative={data.qualitative}
satisfaction={data.satisfaction}
friction={data.friction}
/>
<Improvements qualitative={data.qualitative} />
<FutureOpportunities qualitative={data.qualitative} />
<MemorableMoment qualitative={data.qualitative} />
</>
)}
<ExportButton />
</div>
);
}
// Export Button Component
function ExportButton() {
const [isExporting, setIsExporting] = useState(false);
const handleExport = async () => {
const container = document.getElementById('container');
if (!container || !window.html2canvas) {
alert('Export functionality is not available.');
return;
}
setIsExporting(true);
try {
const canvas = await html2canvas(container, {
scale: 2,
useCORS: true,
logging: false,
});
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

@ -1,339 +0,0 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
/* eslint-disable react/prop-types */
/* eslint-disable no-undef */
// -----------------------------------------------------------------------------
// Existing Components
// -----------------------------------------------------------------------------
// 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();
}
const canvas = chartRef.current;
if (!canvas || !window.Chart) return;
const labels = Array.from({ length: 24 }, (_, i) => `${i}:00`);
const data = labels.map((_, i) => activeHours[i] || 0);
const ctx = canvas.getContext('2d');
if (!ctx) return;
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,
},
},
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>
);
}
// 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>
);
}
// Heatmap Section Component
function HeatmapSection({ heatmap }) {
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}>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>
);
}
// 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];
}
const weeksInYear = Math.ceil(dates.length / 7);
const startX = 50;
const startY = 20;
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);
}
}
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;
const x = startX + week * (cellSize + cellPadding);
const y = startY + day * (cellSize + cellPadding);
const dateKey = date.toISOString().split('T')[0];
const value = heatmapData[dateKey] || 0;
const color = getColor(value);
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>
);
})}
{/* Render month labels */}
{monthLabels.map((label, index) => (
<text key={index} x={label.x} y="15" fontSize="12" fill="#64748b">
{label.text}
</text>
))}
{/* 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);
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"
width={cellSize}
>
More
</text>
</svg>
);
}

View file

@ -1,71 +0,0 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
/* eslint-disable react/prop-types */
// Header Component
function Header({ data, dateRangeStr }) {
const { totalMessages, totalSessions } = data;
return (
<header className="mb-8 space-y-3 text-center">
<h1 className="text-3xl font-semibold text-slate-900 md:text-4xl">
Qwen Code Insights
</h1>
<p className="text-sm text-slate-600">
{totalMessages
? `${totalMessages} messages across ${totalSessions} sessions`
: 'Your personalized coding journey and patterns'}
{dateRangeStr && ` | ${dateRangeStr}`}
</p>
</header>
);
}
function StatsRow({ data }) {
const {
totalMessages = 0,
totalLinesAdded = 0,
totalLinesRemoved = 0,
totalFiles = 0,
// totalSessions = 0,
// totalHours = 0,
} = data;
const heatmapKeys = Object.keys(data.heatmap || {});
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);
daysSpan = Math.ceil(diffTime / (1000 * 60 * 60 * 24)) + 1;
}
const msgsPerDay = daysSpan > 0 ? Math.round(totalMessages / daysSpan) : 0;
return (
<div className="stats-row">
<div className="stat">
<div className="stat-value">{totalMessages}</div>
<div className="stat-label">Messages</div>
</div>
<div className="stat">
<div className="stat-value">
+{totalLinesAdded}/-{totalLinesRemoved}
</div>
<div className="stat-label">Lines</div>
</div>
<div className="stat">
<div className="stat-value">{totalFiles}</div>
<div className="stat-label">Files</div>
</div>
<div className="stat">
<div className="stat-value">{daysSpan}</div>
<div className="stat-label">Days</div>
</div>
<div className="stat">
<div className="stat-value">{msgsPerDay}</div>
<div className="stat-label">Msgs/Day</div>
</div>
</div>
);
}

View file

@ -1,659 +0,0 @@
/* eslint-disable react/jsx-no-undef */
/* eslint-disable @typescript-eslint/no-unused-vars */
/* eslint-disable react/prop-types */
/* eslint-disable no-undef */
// -----------------------------------------------------------------------------
// Qualitative Insight Components
// -----------------------------------------------------------------------------
function AtAGlance({ qualitative }) {
const { atAGlance } = qualitative;
if (!atAGlance) return null;
return (
<div className="at-a-glance">
<div className="glance-title">At a Glance</div>
<div className="glance-sections">
<div className="glance-section">
<strong>What&apos;s working:</strong>{' '}
<MarkdownText>{atAGlance.whats_working}</MarkdownText>
<a href="#section-wins" className="see-more">
Impressive Things You Did
</a>
</div>
<div className="glance-section">
<strong>What&apos;s hindering you:</strong>{' '}
<MarkdownText>{atAGlance.whats_hindering}</MarkdownText>
<a href="#section-friction" className="see-more">
Where Things Go Wrong
</a>
</div>
<div className="glance-section">
<strong>Quick wins to try:</strong>{' '}
<MarkdownText>{atAGlance.quick_wins}</MarkdownText>
<a href="#section-features" className="see-more">
Features to Try
</a>
</div>
<div className="glance-section">
<strong>Ambitious workflows:</strong>{' '}
<MarkdownText>{atAGlance.ambitious_workflows}</MarkdownText>
<a href="#section-horizon" className="see-more">
On the Horizon
</a>
</div>
</div>
</div>
);
}
function NavToc() {
return (
<nav className="nav-toc">
<a href="#section-work">What You Work On</a>
<a href="#section-usage">How You Use QC</a>
<a href="#section-wins">Impressive Things</a>
<a href="#section-friction">Where Things Go Wrong</a>
<a href="#section-features">Features to Try</a>
<a href="#section-patterns">New Usage Patterns</a>
<a href="#section-horizon">On the Horizon</a>
</nav>
);
}
function ProjectAreas({ qualitative, topGoals, topTools }) {
const { projectAreas } = qualitative;
// Convert topTools (array of tuples) to object for chart if needed
const topToolsObj = Array.isArray(topTools)
? Object.fromEntries(topTools)
: topTools;
return (
<>
<h2
id="section-work"
className="text-xl font-semibold text-slate-900 mt-8 mb-4"
>
What You Work On
</h2>
{Array.isArray(projectAreas?.areas) && projectAreas.areas.length > 0 && (
<div className="project-areas mb-6">
{projectAreas.areas.map((area, idx) => (
<div key={idx} className="project-area">
<div className="area-header">
<span className="area-name">{area.name}</span>
<span className="area-count">
~{area.session_count} sessions
</span>
</div>
<div className="area-desc">
<MarkdownText>{area.description}</MarkdownText>
</div>
</div>
))}
</div>
)}
<div
style={{
display: 'grid',
gridTemplateColumns: 'repeat(2, 1fr)',
gap: '24px',
marginBottom: '24px',
}}
>
{topGoals && Object.keys(topGoals).length > 0 && (
<HorizontalBarChart
data={topGoals}
title="What You Wanted"
color="#0ea5e9"
/>
)}
{topToolsObj && Object.keys(topToolsObj).length > 0 && (
<HorizontalBarChart
data={topToolsObj}
title="Top Tools Used"
color="#6366f1"
/>
)}
</div>
</>
);
}
function InteractionStyle({ qualitative, insights }) {
const { interactionStyle } = qualitative;
if (!interactionStyle) return null;
return (
<>
<h2
id="section-usage"
className="text-xl font-semibold text-slate-900 mt-8 mb-4"
>
How You Use Qwen Code
</h2>
<div className="narrative">
<p>
<MarkdownText>{interactionStyle.narrative}</MarkdownText>
</p>
{interactionStyle.key_pattern && (
<div className="key-insight">
<strong>Key pattern:</strong>{' '}
<MarkdownText>{interactionStyle.key_pattern}</MarkdownText>
</div>
)}
</div>
<DashboardCards insights={insights} />
<HeatmapSection heatmap={insights.heatmap} />
</>
);
}
function ImpressiveWorkflows({ qualitative, primarySuccess, outcomes }) {
const { impressiveWorkflows } = qualitative;
if (!impressiveWorkflows) return null;
return (
<>
<h2
id="section-wins"
className="text-xl font-semibold text-slate-900 mt-8 mb-4"
>
Impressive Things You Did
</h2>
{impressiveWorkflows.intro && (
<p className="section-intro">
<MarkdownText>{impressiveWorkflows.intro}</MarkdownText>
</p>
)}
<div className="big-wins">
{Array.isArray(impressiveWorkflows.impressive_workflows) &&
impressiveWorkflows.impressive_workflows.map((win, idx) => (
<div key={idx} className="big-win">
<div className="big-win-title">{win.title}</div>
<div className="big-win-desc">
<MarkdownText>{win.description}</MarkdownText>
</div>
</div>
))}
</div>
<div
style={{
display: 'grid',
gridTemplateColumns: 'repeat(2, 1fr)',
gap: '24px',
marginTop: '24px',
marginBottom: '24px',
}}
>
{primarySuccess && Object.keys(primarySuccess).length > 0 && (
<HorizontalBarChart
data={primarySuccess}
title="What Helped Most (Qwen's Capabilities)"
color="#3b82f6"
allowedKeys={[
'fast_accurate_search',
'correct_code_edits',
'good_explanations',
'proactive_help',
'multi_file_changes',
'good_debugging',
]}
/>
)}
{outcomes && Object.keys(outcomes).length > 0 && (
<HorizontalBarChart
data={outcomes}
title="Outcomes"
color="#8b5cf6"
allowedKeys={[
'fully_achieved',
'mostly_achieved',
'partially_achieved',
'not_achieved',
'unclear_from_transcript',
]}
/>
)}
</div>
</>
);
}
// Format label for display (capitalize and replace underscores with spaces)
function formatLabel(label) {
if (label === 'unclear_from_transcript') {
return 'Unclear';
}
return label
.split('_')
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.join(' ');
}
// Horizontal Bar Chart Component
function HorizontalBarChart({
data,
title,
color = '#3b82f6',
allowedKeys = null,
}) {
if (!data || Object.keys(data).length === 0) return null;
// Filter and sort entries
let entries = Object.entries(data);
if (allowedKeys) {
entries = entries.filter(([key]) => allowedKeys.includes(key));
}
entries.sort((a, b) => b[1] - a[1]);
if (entries.length === 0) return null;
const maxValue = Math.max(...entries.map(([, count]) => count));
return (
<div
className="bar-chart-card"
style={{
flex: 1,
minWidth: 0,
backgroundColor: '#ffffff',
borderRadius: '12px',
padding: '20px',
boxShadow: '0 1px 3px rgba(0, 0, 0, 0.08)',
border: '1px solid #e2e8f0',
}}
>
<h3
style={{
fontSize: '13px',
fontWeight: 700,
color: '#64748b',
marginTop: 0,
marginBottom: '16px',
textTransform: 'uppercase',
letterSpacing: '0.5px',
}}
>
{title}
</h3>
<div
className="bar-chart"
style={{ display: 'flex', flexDirection: 'column', gap: '10px' }}
>
{entries.map(([label, count]) => {
const percentage = maxValue > 0 ? (count / maxValue) * 100 : 0;
return (
<div
key={label}
className="bar-row"
style={{ display: 'flex', alignItems: 'center', gap: '12px' }}
>
<div
className="bar-label"
style={{
width: '130px',
fontSize: '13px',
color: '#475569',
textAlign: 'left',
flexShrink: 0,
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis',
}}
>
{formatLabel(label)}
</div>
<div
className="bar-wrapper"
style={{
flex: 1,
display: 'flex',
alignItems: 'center',
gap: '10px',
minWidth: 0,
}}
>
<div
className="bar-bg"
style={{
flex: 1,
height: '8px',
backgroundColor: '#f1f5f9',
borderRadius: '4px',
overflow: 'hidden',
}}
>
<div
className="bar-fill"
style={{
width: `${percentage}%`,
height: '100%',
backgroundColor: color,
borderRadius: '4px',
transition: 'width 0.3s ease',
}}
/>
</div>
<span
className="bar-value"
style={{
fontSize: '13px',
fontWeight: 600,
color: '#475569',
minWidth: '24px',
textAlign: 'right',
}}
>
{count}
</span>
</div>
</div>
);
})}
</div>
</div>
);
}
function FrictionPoints({ qualitative, satisfaction, friction }) {
const { frictionPoints } = qualitative;
if (!frictionPoints) return null;
return (
<>
<h2
id="section-friction"
className="text-xl font-semibold text-slate-900 mt-8 mb-4"
>
Where Things Go Wrong
</h2>
{frictionPoints.intro && (
<p className="section-intro">
<MarkdownText>{frictionPoints.intro}</MarkdownText>
</p>
)}
<div className="friction-categories">
{Array.isArray(frictionPoints.categories) &&
frictionPoints.categories.map((cat, idx) => (
<div key={idx} className="friction-category">
<div className="friction-title">{cat.category}</div>
<div className="friction-desc">
<MarkdownText>{cat.description}</MarkdownText>
</div>
{Array.isArray(cat.examples) && cat.examples.length > 0 && (
<ul className="friction-examples">
{cat.examples.map((ex, i) => (
<li key={i}>
<MarkdownText>{ex}</MarkdownText>
</li>
))}
</ul>
)}
</div>
))}
</div>
{/* Facets Data Charts */}
<div
style={{
display: 'grid',
gridTemplateColumns: 'repeat(2, 1fr)',
gap: '24px',
marginTop: '24px',
marginBottom: '24px',
}}
>
{friction && Object.keys(friction).length > 0 && (
<HorizontalBarChart
data={friction}
title="Primary Friction Types"
color="#ef4444"
allowedKeys={[
'misunderstood_request',
'wrong_approach',
'buggy_code',
'user_rejected_action',
'excessive_changes',
]}
/>
)}
{satisfaction && Object.keys(satisfaction).length > 0 && (
<HorizontalBarChart
data={satisfaction}
title="Inferred Satisfaction (model-estimated)"
color="#10b981"
allowedKeys={[
'happy',
'satisfied',
'likely_satisfied',
'dissatisfied',
'frustrated',
]}
/>
)}
</div>
</>
);
}
// Qwen.md Additions Section Component
function QwenMdAdditionsSection({ additions }) {
const [checkedState, setCheckedState] = useState(
new Array(additions.length).fill(true),
);
const [copiedAll, setCopiedAll] = useState(false);
const handleCheckboxChange = (position) => {
const updatedCheckedState = checkedState.map((item, index) =>
index === position ? !item : item,
);
setCheckedState(updatedCheckedState);
};
const handleCopyAll = () => {
const textToCopy = additions
.filter((_, index) => checkedState[index])
.map((item) => item.addition)
.join('\n\n');
if (!textToCopy) return;
navigator.clipboard.writeText(textToCopy).then(() => {
setCopiedAll(true);
setTimeout(() => setCopiedAll(false), 2000);
});
};
const checkedCount = checkedState.filter(Boolean).length;
return (
<div className="qwen-md-section">
<h3>Suggested QWEN.md Additions</h3>
<p className="text-xs text-slate-500 mb-3">
Just copy this into Qwen Code to add it to your QWEN.md.
</p>
<div className="qwen-md-actions" style={{ marginBottom: '12px' }}>
<button
className={`copy-all-btn ${copiedAll ? 'copied' : ''}`}
onClick={handleCopyAll}
disabled={checkedCount === 0}
>
{copiedAll ? 'Copied All!' : `Copy All Checked (${checkedCount})`}
</button>
</div>
{additions.map((item, idx) => (
<div key={idx} className="qwen-md-item">
<input
type="checkbox"
checked={checkedState[idx]}
onChange={() => handleCheckboxChange(idx)}
className="cmd-checkbox"
/>
<div style={{ flex: 1 }}>
<code className="cmd-code">{item.addition}</code>
<div className="cmd-why">
<MarkdownText>{item.why}</MarkdownText>
</div>
</div>
<CopyButton text={item.addition} />
</div>
))}
</div>
);
}
function Improvements({ qualitative }) {
const { improvements } = qualitative;
if (!improvements) return null;
return (
<>
<h2
id="section-features"
className="text-xl font-semibold text-slate-900 mt-8 mb-4"
>
Existing QC Features to Try
</h2>
{/* QWEN.md Additions */}
{Array.isArray(improvements.Qwen_md_additions) &&
improvements.Qwen_md_additions.length > 0 && (
<QwenMdAdditionsSection additions={improvements.Qwen_md_additions} />
)}
<p className="text-xs text-slate-500 mb-3">
Just copy this into Qwen Code and it&apos;ll set it up for you.
</p>
{/* Features to Try */}
<div className="features-section">
{Array.isArray(improvements.features_to_try) &&
improvements.features_to_try.map((feat, idx) => (
<div key={idx} className="feature-card">
<div className="feature-title">{feat.feature}</div>
<div className="feature-oneliner">
<MarkdownText>{feat.one_liner}</MarkdownText>
</div>
<div className="feature-why">
<strong>Why for you:</strong>{' '}
<MarkdownText>{feat.why_for_you}</MarkdownText>
</div>
<div className="feature-examples">
<div className="feature-example">
<div className="example-code-row">
<code className="example-code">{feat.example_code}</code>
<CopyButton text={feat.example_code} />
</div>
</div>
</div>
</div>
))}
</div>
<h2
id="section-patterns"
className="text-xl font-semibold text-slate-900 mt-8 mb-4"
>
New Ways to Use Qwen Code
</h2>
<p className="text-xs text-slate-500 mb-3">
Just copy this into Qwen Code and it&apos;ll walk you through it.
</p>
<div className="patterns-section">
{Array.isArray(improvements.usage_patterns) &&
improvements.usage_patterns.map((pat, idx) => (
<div key={idx} className="pattern-card">
<div className="pattern-title">{pat.title}</div>
<div className="pattern-summary">
<MarkdownText>{pat.suggestion}</MarkdownText>
</div>
<div className="pattern-detail">
<MarkdownText>{pat.detail}</MarkdownText>
</div>
<div className="copyable-prompt-section">
<div className="prompt-label">Paste into Qwen Code:</div>
<div className="copyable-prompt-row">
<code className="copyable-prompt">{pat.copyable_prompt}</code>
<CopyButton text={pat.copyable_prompt} />
</div>
</div>
</div>
))}
</div>
</>
);
}
function FutureOpportunities({ qualitative }) {
const { futureOpportunities } = qualitative;
if (!futureOpportunities) return null;
return (
<>
<h2
id="section-horizon"
className="text-xl font-semibold text-slate-900 mt-8 mb-4"
>
On the Horizon
</h2>
{futureOpportunities.intro && (
<p className="section-intro">
<MarkdownText>{futureOpportunities.intro}</MarkdownText>
</p>
)}
<div className="horizon-section">
{Array.isArray(futureOpportunities.opportunities) &&
futureOpportunities.opportunities.map((opp, idx) => (
<div key={idx} className="horizon-card">
<div className="horizon-title">{opp.title}</div>
<div className="horizon-possible">
<MarkdownText>{opp.whats_possible}</MarkdownText>
</div>
<div className="horizon-tip">
<strong>Getting started:</strong>{' '}
<MarkdownText>{opp.how_to_try}</MarkdownText>
</div>
<div className="pattern-prompt">
<div className="prompt-label">Paste into Qwen Code:</div>
<div
style={{
display: 'flex',
alignItems: 'flex-start',
gap: '8px',
}}
>
<code style={{ flex: 1 }}>{opp.copyable_prompt}</code>
<CopyButton text={opp.copyable_prompt} />
</div>
</div>
</div>
))}
</div>
</>
);
}
function MemorableMoment({ qualitative }) {
const { memorableMoment } = qualitative;
if (!memorableMoment) return null;
return (
<div className="fun-ending">
<div className="fun-headline">&quot;{memorableMoment.headline}&quot;</div>
<div className="fun-detail">
<MarkdownText>{memorableMoment.detail}</MarkdownText>
</div>
</div>
);
}

View file

@ -1,41 +0,0 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
/* eslint-disable react/prop-types */
/* eslint-disable no-undef */
const { useState, useRef, useEffect } = React;
// Simple Markdown Parser Component
function MarkdownText({ children }) {
if (!children || typeof children !== 'string') return children;
// Split by bold markers (**text**)
const parts = children.split(/(\*\*.*?\*\*)/g);
return (
<>
{parts.map((part, i) => {
if (part.startsWith('**') && part.endsWith('**') && part.length >= 4) {
return <strong key={i}>{part.slice(2, -2)}</strong>;
}
return part;
})}
</>
);
}
function CopyButton({ text, label = 'Copy' }) {
const [copied, setCopied] = useState(false);
const handleCopy = () => {
navigator.clipboard.writeText(text).then(() => {
setCopied(true);
setTimeout(() => setCopied(false), 2000);
});
};
return (
<button className="copy-btn" onClick={handleCopy}>
{copied ? 'Copied!' : label}
</button>
);
}

File diff suppressed because it is too large Load diff