feat(insight): update insight template and app to React, enhance export functionality

This commit is contained in:
DragonnZhang 2026-01-23 20:06:06 +08:00
parent 2931e75a17
commit 6cb0bb078c
5 changed files with 472 additions and 329 deletions

View file

@ -5,8 +5,7 @@
*/ */
import fs from 'fs/promises'; import fs from 'fs/promises';
import path from 'path'; import path, { dirname } from 'path';
import { dirname } from 'path';
import { fileURLToPath } from 'url'; import { fileURLToPath } from 'url';
import type { InsightData } from '../types/StaticInsightTypes.js'; import type { InsightData } from '../types/StaticInsightTypes.js';

View file

@ -25,27 +25,22 @@
</p> </p>
</header> </header>
<div class="mt-6 flex justify-center"> <!-- React App Mount Point -->
<button <div id="react-root"></div>
id="export-btn"
class="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]"
onclick="handleExport()"
>
Export as Image
<span class="text-slate-200 transition group-hover:translate-x-0.5">
</span>
</button>
</div>
</div> </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 --> <!-- 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/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> <script src="https://cdn.jsdelivr.net/npm/html2canvas@1.4.1/dist/html2canvas.min.js"></script>
<!-- Application Data --> <!-- Application Data -->
<script> <script type="text/babel">
window.INSIGHT_DATA = {{DATA_PLACEHOLDER}}; window.INSIGHT_DATA = {{DATA_PLACEHOLDER}};
{{SCRIPTS_PLACEHOLDER}} {{SCRIPTS_PLACEHOLDER}}
</script> </script>

View file

@ -1,205 +1,104 @@
// Native JavaScript implementation of the insight app /* eslint-disable react/prop-types */
// This replaces the React-based App.tsx functionality /* 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 // Main App Component
document.addEventListener('DOMContentLoaded', function() { function InsightApp({ data }) {
initializeApp(); if (!data) {
}); return (
<div className="text-center text-slate-600">
function initializeApp() { No insight data available
const insights = window.INSIGHT_DATA; </div>
);
if (!insights) {
showError('No insight data available');
return;
} }
// Create the main content return (
createInsightContent(insights); <div>
<DashboardCards insights={data} />
// Initialize charts <HeatmapSection heatmap={data.heatmap} />
initializeHourChart(insights); <TokenUsageSection tokenUsage={data.tokenUsage} />
<AchievementsSection achievements={data.achievements} />
// Initialize heatmap <ExportButton />
initializeHeatmap(insights); </div>
);
} }
function createInsightContent(insights) { // Dashboard Cards Component
const container = document.getElementById('container'); function DashboardCards({ insights }) {
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 cardClass = 'glass-card p-6';
const sectionTitleClass = 'text-lg font-semibold tracking-tight text-slate-900'; const sectionTitleClass =
'text-lg font-semibold tracking-tight text-slate-900';
const captionClass = 'text-sm font-medium text-slate-500'; const captionClass = 'text-sm font-medium text-slate-500';
return ` return (
<div class="grid gap-4 md:grid-cols-3 md:gap-6"> <div className="grid gap-4 md:grid-cols-3 md:gap-6">
<div class="${cardClass} h-full"> <StreakCard
<div class="flex items-start justify-between"> 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> <div>
<p class="${captionClass}">Current Streak</p> <p className={captionClass}>Current Streak</p>
<p class="mt-1 text-4xl font-bold text-slate-900"> <p className="mt-1 text-4xl font-bold text-slate-900">
${insights.currentStreak} {currentStreak}
<span class="ml-2 text-base font-semibold text-slate-500"> <span className="ml-2 text-base font-semibold text-slate-500">
days days
</span> </span>
</p> </p>
</div> </div>
<span class="rounded-full bg-emerald-50 px-4 py-2 text-sm font-semibold text-emerald-700"> <span className="rounded-full bg-emerald-50 px-4 py-2 text-sm font-semibold text-emerald-700">
Longest ${insights.longestStreak}d Longest {longestStreak}d
</span> </span>
</div> </div>
</div> </div>
);
}
<div class="${cardClass} h-full"> // Active Hours Chart Component
<div class="flex items-center justify-between"> function ActiveHoursChart({ activeHours, cardClass, sectionTitleClass }) {
<h3 class="${sectionTitleClass}">Active Hours</h3> const chartRef = useRef(null);
<span class="rounded-full bg-slate-100 px-3 py-1 text-xs font-semibold text-slate-600"> const chartInstance = useRef(null);
24h
</span>
</div>
<div class="mt-4 h-56 w-full">
<canvas id="hour-chart"></canvas>
</div>
</div>
<div class="${cardClass} h-full space-y-3"> useEffect(() => {
<h3 class="${sectionTitleClass}">Work Session</h3> if (chartInstance.current) {
<div class="grid grid-cols-2 gap-3 text-sm text-slate-700"> chartInstance.current.destroy();
<div class="rounded-xl bg-slate-50 px-3 py-2">
<p class="text-xs font-semibold uppercase tracking-wide text-slate-500">
Longest
</p>
<p class="mt-1 text-lg font-semibold text-slate-900">
${insights.longestWorkDuration}m
</p>
</div>
<div class="rounded-xl bg-slate-50 px-3 py-2">
<p class="text-xs font-semibold uppercase tracking-wide text-slate-500">
Date
</p>
<p class="mt-1 text-lg font-semibold text-slate-900">
${insights.longestWorkDate || '-'}
</p>
</div>
<div class="col-span-2 rounded-xl bg-slate-50 px-3 py-2">
<p class="text-xs font-semibold uppercase tracking-wide text-slate-500">
Last Active
</p>
<p class="mt-1 text-lg font-semibold text-slate-900">
${insights.latestActiveTime || '-'}
</p>
</div>
</div>
</div>
</div>
<div class="${cardClass} mt-4 space-y-4 md:mt-6">
<div class="flex items-center justify-between">
<h3 class="${sectionTitleClass}">Activity Heatmap</h3>
<span class="text-xs font-semibold text-slate-500">
Past year
</span>
</div>
<div class="heatmap-container">
<div id="heatmap" class="min-w-[720px] rounded-xl border border-slate-100 bg-white/70 p-4 shadow-inner shadow-slate-100">
<!-- Heatmap will be inserted here -->
</div>
</div>
</div>
<div class="${cardClass} mt-4 md:mt-6">
<div class="space-y-3">
<h3 class="${sectionTitleClass}">Token Usage</h3>
<div class="grid grid-cols-3 gap-3">
<div class="rounded-xl bg-slate-50 px-4 py-3">
<p class="text-xs font-semibold uppercase tracking-wide text-slate-500">
Input
</p>
<p class="mt-1 text-2xl font-bold text-slate-900">
${calculateTotalTokens(insights.tokenUsage, 'input').toLocaleString()}
</p>
</div>
<div class="rounded-xl bg-slate-50 px-4 py-3">
<p class="text-xs font-semibold uppercase tracking-wide text-slate-500">
Output
</p>
<p class="mt-1 text-2xl font-bold text-slate-900">
${calculateTotalTokens(insights.tokenUsage, 'output').toLocaleString()}
</p>
</div>
<div class="rounded-xl bg-slate-50 px-4 py-3">
<p class="text-xs font-semibold uppercase tracking-wide text-slate-500">
Total
</p>
<p class="mt-1 text-2xl font-bold text-slate-900">
${calculateTotalTokens(insights.tokenUsage, 'total').toLocaleString()}
</p>
</div>
</div>
</div>
</div>
<div class="${cardClass} mt-4 space-y-4 md:mt-6">
<div class="flex items-center justify-between">
<h3 class="${sectionTitleClass}">Achievements</h3>
<span class="text-xs font-semibold text-slate-500">
${insights.achievements.length} total
</span>
</div>
${insights.achievements.length === 0 ?
'<p class="text-sm text-slate-600">No achievements yet. Keep coding!</p>' :
`<div class="divide-y divide-slate-200">
${insights.achievements.map(achievement => `
<div class="flex flex-col gap-1 py-3 text-left">
<span class="text-base font-semibold text-slate-900">
${achievement.name}
</span>
<p class="text-sm text-slate-600">
${achievement.description}
</p>
</div>
`).join('')}
</div>`
} }
</div>
`;
}
function calculateTotalTokens(tokenUsage, type) { const canvas = chartRef.current;
return Object.values(tokenUsage).reduce((acc, usage) => acc + usage[type], 0);
}
function initializeHourChart(insights) {
const canvas = document.getElementById('hour-chart');
if (!canvas || !window.Chart) return; 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 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'); const ctx = canvas.getContext('2d');
if (!ctx) return; if (!ctx) return;
hourChartInstance = new Chart(ctx, { chartInstance.current = new Chart(ctx, {
type: 'bar', type: 'bar',
data: { data: {
labels, labels,
@ -229,18 +128,93 @@ function initializeHourChart(insights) {
}, },
}, },
}); });
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>
);
} }
function initializeHeatmap(insights) { // Work Session Card Component
const heatmapContainer = document.getElementById('heatmap'); function WorkSessionCard({
if (!heatmapContainer) return; longestWorkDuration,
longestWorkDate,
// Create a simple SVG heatmap latestActiveTime,
const svg = createHeatmapSVG(insights.heatmap); cardClass,
heatmapContainer.innerHTML = svg; 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>
);
} }
function createHeatmapSVG(heatmapData) { // 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 width = 1000;
const height = 150; const height = 150;
const cellSize = 14; const cellSize = 14;
@ -258,8 +232,6 @@ function createHeatmapSVG(heatmapData) {
currentDate.setDate(currentDate.getDate() + 1); currentDate.setDate(currentDate.getDate() + 1);
} }
// Calculate max value for color scaling
const maxValue = Math.max(...Object.values(heatmapData));
const colorLevels = [0, 2, 4, 10, 20]; const colorLevels = [0, 2, 4, 10, 20];
const colors = ['#e2e8f0', '#a5d8ff', '#74c0fc', '#339af0', '#1c7ed6']; const colors = ['#e2e8f0', '#a5d8ff', '#74c0fc', '#339af0', '#1c7ed6'];
@ -271,14 +243,53 @@ function createHeatmapSVG(heatmapData) {
return colors[1]; return colors[1];
} }
let svg = `<svg class="heatmap-svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}">`;
// Calculate grid dimensions
const weeksInYear = Math.ceil(dates.length / 7); const weeksInYear = Math.ceil(dates.length / 7);
const startX = 50; const startX = 50;
const startY = 20; const startY = 20;
dates.forEach((date, index) => { 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 week = Math.floor(index / 7);
const day = index % 7; const day = index % 7;
@ -289,76 +300,211 @@ function createHeatmapSVG(heatmapData) {
const value = heatmapData[dateKey] || 0; const value = heatmapData[dateKey] || 0;
const color = getColor(value); const color = getColor(value);
svg += `<rect class="heatmap-day" return (
x="${x}" y="${y}" <rect
width="${cellSize}" height="${cellSize}" key={dateKey}
className="heatmap-day"
x={x}
y={y}
width={cellSize}
height={cellSize}
rx="2" rx="2"
fill="${color}" fill={color}
data-date="${dateKey}" data-date={dateKey}
data-count="${value}"> data-count={value}
<title>${dateKey}: ${value} activities</title> >
</rect>`; <title>
}); {dateKey}: {value} activities
</title>
</rect>
);
})}
// Add month labels {/* Render month labels */}
const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', {monthLabels.map((label, index) => (
'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; <text key={index} x={label.x} y="15" fontSize="12" fill="#64748b">
let currentMonth = oneYearAgo.getMonth(); {label.text}
let monthX = startX; </text>
))}
for (let week = 0; week < weeksInYear; week++) { {/* Render legend */}
const weekDate = new Date(oneYearAgo); <text x={startX} y={height - 40} fontSize="12" fill="#64748b">
weekDate.setDate(weekDate.getDate() + week * 7); Less
</text>
if (weekDate.getMonth() !== currentMonth) { {colors.map((color, index) => {
currentMonth = weekDate.getMonth();
svg += `<text x="${monthX}" y="15" font-size="12" fill="#64748b">${months[currentMonth]}</text>`;
monthX = startX + week * (cellSize + cellPadding);
}
}
// Add legend
const legendY = height - 30;
svg += '<text x="' + startX + '" y="' + (legendY - 10) + '" font-size="12" fill="#64748b">Less</text>';
colors.forEach((color, index) => {
const legendX = startX + 40 + index * (cellSize + 2); const legendX = startX + 40 + index * (cellSize + 2);
svg += `<rect x="${legendX}" y="${legendY}" width="10" height="10" rx="2" fill="${color}"></rect>`; return (
}); <rect
key={index}
svg += '<text x="' + (startX + 40 + colors.length * (cellSize + 2) + 5) + '" y="' + (legendY + 9) + '" font-size="12" fill="#64748b">More</text>'; x={legendX}
y={height - 30}
svg += '</svg>'; width="10"
return svg; height="10"
rx="2"
fill={color}
/>
);
})}
<text
x={startX + 40 + colors.length * (cellSize + 2) + 5}
y={height - 21}
fontSize="12"
fill="#64748b"
>
More
</text>
</svg>
);
} }
// Export functionality // Token Usage Section Component
function handleExport() { 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 (
<div className={`${cardClass} mt-4 md:mt-6`}>
<div className="space-y-3">
<h3 className={sectionTitleClass}>Token Usage</h3>
<div className="grid grid-cols-3 gap-3">
<TokenUsageCard
label="Input"
value={calculateTotalTokens(tokenUsage, 'input').toLocaleString()}
/>
<TokenUsageCard
label="Output"
value={calculateTotalTokens(tokenUsage, 'output').toLocaleString()}
/>
<TokenUsageCard
label="Total"
value={calculateTotalTokens(tokenUsage, 'total').toLocaleString()}
/>
</div>
</div>
</div>
);
}
// Token Usage Card Component
function TokenUsageCard({ label, value }) {
return (
<div className="rounded-xl bg-slate-50 px-4 py-3">
<p className="text-xs font-semibold uppercase tracking-wide text-slate-500">
{label}
</p>
<p className="mt-1 text-2xl font-bold text-slate-900">{value}</p>
</div>
);
}
// Achievements Section Component
function AchievementsSection({ achievements }) {
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}>Achievements</h3>
<span className="text-xs font-semibold text-slate-500">
{achievements.length} total
</span>
</div>
{achievements.length === 0 ? (
<p className="text-sm text-slate-600">
No achievements yet. Keep coding!
</p>
) : (
<div className="divide-y divide-slate-200">
{achievements.map((achievement, index) => (
<AchievementItem key={index} achievement={achievement} />
))}
</div>
)}
</div>
);
}
// Achievement Item Component
function AchievementItem({ achievement }) {
return (
<div className="flex flex-col gap-1 py-3 text-left">
<span className="text-base font-semibold text-slate-900">
{achievement.name}
</span>
<p className="text-sm text-slate-600">{achievement.description}</p>
</div>
);
}
// Export Button Component
function ExportButton() {
const [isExporting, setIsExporting] = useState(false);
const handleExport = async () => {
const container = document.getElementById('container'); const container = document.getElementById('container');
const button = document.getElementById('export-btn');
if (!container || !window.html2canvas) { if (!container || !window.html2canvas) {
alert('Export functionality is not available.'); alert('Export functionality is not available.');
return; return;
} }
button.style.display = 'none'; setIsExporting(true);
html2canvas(container, { try {
const canvas = await html2canvas(container, {
scale: 2, scale: 2,
useCORS: true, useCORS: true,
logging: false, logging: false,
}).then(function(canvas) { });
const imgData = canvas.toDataURL('image/png'); const imgData = canvas.toDataURL('image/png');
const link = document.createElement('a'); const link = document.createElement('a');
link.href = imgData; link.href = imgData;
link.download = `qwen-insights-${new Date().toISOString().slice(0, 10)}.png`; link.download = `qwen-insights-${new Date().toISOString().slice(0, 10)}.png`;
link.click(); link.click();
} catch (error) {
button.style.display = 'block'; console.error('Export error:', error);
}).catch(function(error) {
console.error('Error capturing image:', error);
alert('Failed to export image. Please try again.'); alert('Failed to export image. Please try again.');
button.style.display = 'block'; } 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

@ -49,7 +49,10 @@ function copyFilesRecursive(source, target, rootSourceDir) {
const normalizedPath = relativePath.replace(/\\/g, '/'); const normalizedPath = relativePath.replace(/\\/g, '/');
const isLocaleJs = const isLocaleJs =
ext === '.js' && normalizedPath.startsWith('i18n/locales/'); 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); fs.copyFileSync(sourcePath, targetPath);
} }
} }