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;
}