diff --git a/packages/cli/src/services/insight/generators/DataProcessor.ts b/packages/cli/src/services/insight/generators/DataProcessor.ts index 5ca95a3e2..481f79b39 100644 --- a/packages/cli/src/services/insight/generators/DataProcessor.ts +++ b/packages/cli/src/services/insight/generators/DataProcessor.ts @@ -281,8 +281,14 @@ export class DataProcessor { if (onProgress) onProgress('Generating qualitative insights', 80); const qualitative = await this.generateQualitativeInsights(metrics, facets); - // Aggregate satisfaction and friction data from facets - const { satisfactionAgg, frictionAgg } = this.aggregateFacetsData(facets); + // Aggregate satisfaction, friction, success and outcome data from facets + const { + satisfactionAgg, + frictionAgg, + primarySuccessAgg, + outcomesAgg, + goalsAgg, + } = this.aggregateFacetsData(facets); if (onProgress) onProgress('Finalizing report', 100); @@ -291,6 +297,9 @@ export class DataProcessor { qualitative, satisfaction: satisfactionAgg, friction: frictionAgg, + primarySuccess: primarySuccessAgg, + outcomes: outcomesAgg, + topGoals: goalsAgg, }; } @@ -298,9 +307,15 @@ export class DataProcessor { private aggregateFacetsData(facets: SessionFacets[]): { satisfactionAgg: Record; frictionAgg: Record; + primarySuccessAgg: Record; + outcomesAgg: Record; + goalsAgg: Record; } { const satisfactionAgg: Record = {}; const frictionAgg: Record = {}; + const primarySuccessAgg: Record = {}; + const outcomesAgg: Record = {}; + const goalsAgg: Record = {}; facets.forEach((facet) => { // Aggregate satisfaction @@ -312,9 +327,31 @@ export class DataProcessor { Object.entries(facet.friction_counts).forEach(([fric, count]) => { frictionAgg[fric] = (frictionAgg[fric] || 0) + count; }); + + // Aggregate primary success + if (facet.primary_success && facet.primary_success !== 'none') { + primarySuccessAgg[facet.primary_success] = + (primarySuccessAgg[facet.primary_success] || 0) + 1; + } + + // Aggregate outcomes + if (facet.outcome) { + outcomesAgg[facet.outcome] = (outcomesAgg[facet.outcome] || 0) + 1; + } + + // Aggregate goals + Object.entries(facet.goal_categories).forEach(([goal, count]) => { + goalsAgg[goal] = (goalsAgg[goal] || 0) + count; + }); }); - return { satisfactionAgg, frictionAgg }; + return { + satisfactionAgg, + frictionAgg, + primarySuccessAgg, + outcomesAgg, + goalsAgg, + }; } private async generateQualitativeInsights( diff --git a/packages/cli/src/services/insight/prompts/InsightPrompts.ts b/packages/cli/src/services/insight/prompts/InsightPrompts.ts index e3668d9d5..182d5acfe 100644 --- a/packages/cli/src/services/insight/prompts/InsightPrompts.ts +++ b/packages/cli/src/services/insight/prompts/InsightPrompts.ts @@ -5,7 +5,15 @@ CRITICAL GUIDELINES: 1. **goal_categories**: Count ONLY what the USER explicitly asked for. - DO NOT count Qwen's autonomous codebase exploration - DO NOT count work Qwen decided to do on its own - - ONLY count when user says "can you...", "please...", "I need...", "let's..." + - ONLY count when user says "can you...", "please...", "I need...", "let's... + - POSSIBLE CATEGORIES (but be open to others that appear in the data): + - bug_fix + - feature_request + - debugging + - test_creation + - code_refactoring + - documentation_update + " 2. **user_satisfaction_counts**: Base ONLY on explicit user signals. - "Yay!", "great!", "perfect!" → happy diff --git a/packages/cli/src/services/insight/templates/scripts/components/App.js b/packages/cli/src/services/insight/templates/scripts/components/App.js index 64e1c801c..722198e64 100644 --- a/packages/cli/src/services/insight/templates/scripts/components/App.js +++ b/packages/cli/src/services/insight/templates/scripts/components/App.js @@ -40,7 +40,11 @@ function InsightApp({ data }) { {data.qualitative && ( <> - + )} @@ -54,7 +58,11 @@ function InsightApp({ data }) { {data.qualitative && ( <> - + @@ -75,18 +78,47 @@ function ProjectAreas({ qualitative }) { > What You Work On -
- {projectAreas.areas.map((area, idx) => ( -
-
- {area.name} - ~{area.session_count} sessions + + {Array.isArray(projectAreas?.areas) && projectAreas.areas.length > 0 && ( +
+ {projectAreas.areas.map((area, idx) => ( +
+
+ {area.name} + + ~{area.session_count} sessions + +
+
+ {area.description} +
-
- {area.description} -
-
- ))} + ))} +
+ )} + +
+ {topGoals && Object.keys(topGoals).length > 0 && ( + + )} + {topToolsObj && Object.keys(topToolsObj).length > 0 && ( + + )}
); @@ -119,7 +151,7 @@ function InteractionStyle({ qualitative }) { ); } -function ImpressiveWorkflows({ qualitative }) { +function ImpressiveWorkflows({ qualitative, primarySuccess, outcomes }) { const { impressiveWorkflows } = qualitative; if (!impressiveWorkflows) return null; @@ -147,12 +179,55 @@ function ImpressiveWorkflows({ qualitative }) {
))}
+ +
+ {primarySuccess && Object.keys(primarySuccess).length > 0 && ( + + )} + {outcomes && Object.keys(outcomes).length > 0 && ( + + )} +
); } // 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)) @@ -197,6 +272,7 @@ function HorizontalBarChart({ fontSize: '13px', fontWeight: 700, color: '#64748b', + marginTop: 0, marginBottom: '16px', textTransform: 'uppercase', letterSpacing: '0.5px', @@ -324,7 +400,8 @@ function FrictionPoints({ qualitative, satisfaction, friction }) { {/* Facets Data Charts */}
- {parts.map((part, i) => { - if (part.startsWith('**') && part.endsWith('**') && part.length >= 4) { - return {part.slice(2, -2)}; - } - return part; - })} - - ); -} - -// Header Component -function Header({ data, dateRangeStr }) { - const { totalMessages, totalSessions } = data; - - return ( -
-

- Qwen Code Insights -

-

- {totalMessages - ? `${totalMessages} messages across ${totalSessions} sessions` - : 'Your personalized coding journey and patterns'} - {dateRangeStr && ` | ${dateRangeStr}`} -

-
- ); -} - -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 ( -
-
-
{totalMessages}
-
Messages
-
-
-
- +{totalLinesAdded}/-{totalLinesRemoved} -
-
Lines
-
-
-
{totalFiles}
-
Files
-
-
-
{daysSpan}
-
Days
-
-
-
{msgsPerDay}
-
Msgs/Day
-
-
- ); -} - -// Main App Component -function InsightApp({ data }) { - if (!data) { - return ( -
- No insight data available -
- ); - } - - // 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 ( -
-
- - {data.qualitative && ( - <> - - - - )} - - - - - - {data.qualitative && ( - <> - - - )} - - - - {data.qualitative && ( - <> - - - )} - - {data.qualitative && ( - <> - - - - - - - )} - - -
- ); -} - -// ----------------------------------------------------------------------------- -// Qualitative Insight Components -// ----------------------------------------------------------------------------- - -function AtAGlance({ qualitative }) { - const { atAGlance } = qualitative; - if (!atAGlance) return null; - - return ( -
-
At a Glance
-
-
- What's working:{' '} - {atAGlance.whats_working} - - Impressive Things You Did → - -
-
- What's hindering you:{' '} - {atAGlance.whats_hindering} - - Where Things Go Wrong → - -
-
- Quick wins to try:{' '} - {atAGlance.quick_wins} - - Features to Try → - -
-
- Ambitious workflows:{' '} - {atAGlance.ambitious_workflows} - - On the Horizon → - -
-
-
- ); -} - -function NavToc() { - return ( - - ); -} - -function ProjectAreas({ qualitative }) { - const { projectAreas } = qualitative; - if (!Array.isArray(projectAreas?.areas) || !projectAreas.areas.length) - return null; - - return ( - <> -

- What You Work On -

-
- {projectAreas.areas.map((area, idx) => ( -
-
- {area.name} - ~{area.session_count} sessions -
-
- {area.description} -
-
- ))} -
- - ); -} - -function InteractionStyle({ qualitative }) { - const { interactionStyle } = qualitative; - if (!interactionStyle) return null; - - return ( - <> -

- How You Use Qwen Code -

-
-

- {interactionStyle.narrative} -

- {interactionStyle.key_pattern && ( -
- Key pattern:{' '} - {interactionStyle.key_pattern} -
- )} -
- - ); -} - -function ImpressiveWorkflows({ qualitative }) { - const { impressiveWorkflows } = qualitative; - if (!impressiveWorkflows) return null; - - return ( - <> -

- Impressive Things You Did -

- {impressiveWorkflows.intro && ( -

- {impressiveWorkflows.intro} -

- )} -
- {Array.isArray(impressiveWorkflows.impressive_workflows) && - impressiveWorkflows.impressive_workflows.map((win, idx) => ( -
-
{win.title}
-
- {win.description} -
-
- ))} -
- - ); -} - -// 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 ( -
-

- {title} -

-
- {entries.map(([label, count]) => { - const percentage = maxValue > 0 ? (count / maxValue) * 100 : 0; - return ( -
-
- {label} -
-
-
-
-
- - {count} - -
-
- ); - })} -
-
- ); -} - -function FrictionPoints({ qualitative, satisfaction, friction }) { - const { frictionPoints } = qualitative; - if (!frictionPoints) return null; - - return ( - <> -

- Where Things Go Wrong -

- {frictionPoints.intro && ( -

- {frictionPoints.intro} -

- )} -
- {Array.isArray(frictionPoints.categories) && - frictionPoints.categories.map((cat, idx) => ( -
-
{cat.category}
-
- {cat.description} -
- {Array.isArray(cat.examples) && cat.examples.length > 0 && ( -
    - {cat.examples.map((ex, i) => ( -
  • - {ex} -
  • - ))} -
- )} -
- ))} -
- - {/* Facets Data Charts */} -
- {friction && Object.keys(friction).length > 0 && ( - - )} - {satisfaction && Object.keys(satisfaction).length > 0 && ( - - )} -
- - ); -} - -function CopyButton({ text, label = 'Copy' }) { - const [copied, setCopied] = useState(false); - - const handleCopy = () => { - navigator.clipboard.writeText(text).then(() => { - setCopied(true); - setTimeout(() => setCopied(false), 2000); - }); - }; - - return ( - - ); -} -// 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 ( -
-

Suggested QWEN.md Additions

-

- Just copy this into Qwen Code to add it to your QWEN.md. -

- -
- -
- - {additions.map((item, idx) => ( -
- handleCheckboxChange(idx)} - className="cmd-checkbox" - /> -
- {item.addition} -
- {item.why} -
-
- -
- ))} -
- ); -} - -function Improvements({ qualitative }) { - const { improvements } = qualitative; - if (!improvements) return null; - - return ( - <> -

- Existing QC Features to Try -

- - {/* QWEN.md Additions */} - {Array.isArray(improvements.Qwen_md_additions) && - improvements.Qwen_md_additions.length > 0 && ( - - )} - -

- Just copy this into Qwen Code and it'll set it up for you. -

- - {/* Features to Try */} -
- {Array.isArray(improvements.features_to_try) && - improvements.features_to_try.map((feat, idx) => ( -
-
{feat.feature}
-
- {feat.one_liner} -
-
- Why for you:{' '} - {feat.why_for_you} -
-
-
-
- {feat.example_code} - -
-
-
-
- ))} -
- -

- New Ways to Use Qwen Code -

-

- Just copy this into Qwen Code and it'll walk you through it. -

- -
- {Array.isArray(improvements.usage_patterns) && - improvements.usage_patterns.map((pat, idx) => ( -
-
{pat.title}
-
- {pat.suggestion} -
-
- {pat.detail} -
-
-
Paste into Qwen Code:
-
- {pat.copyable_prompt} - -
-
-
- ))} -
- - ); -} - -function FutureOpportunities({ qualitative }) { - const { futureOpportunities } = qualitative; - if (!futureOpportunities) return null; - - return ( - <> -

- On the Horizon -

- {futureOpportunities.intro && ( -

- {futureOpportunities.intro} -

- )} - -
- {Array.isArray(futureOpportunities.opportunities) && - futureOpportunities.opportunities.map((opp, idx) => ( -
-
{opp.title}
-
- {opp.whats_possible} -
-
- Getting started:{' '} - {opp.how_to_try} -
-
-
Paste into Qwen Code:
-
- {opp.copyable_prompt} - -
-
-
- ))} -
- - ); -} - -function MemorableMoment({ qualitative }) { - const { memorableMoment } = qualitative; - if (!memorableMoment) return null; - - return ( -
-
"{memorableMoment.headline}"
-
- {memorableMoment.detail} -
-
- ); -} - -// ----------------------------------------------------------------------------- -// 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 ( -
- - - -
- ); -} - -// Streak Card Component -function StreakCard({ currentStreak, longestStreak, cardClass, captionClass }) { - return ( -
-
-
-

Current Streak

-

- {currentStreak} - - days - -

-
- - Longest {longestStreak}d - -
-
- ); -} - -// 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 ( -
-
-

Active Hours

- - 24h - -
-
- -
-
- ); -} - -// Work Session Card Component -function WorkSessionCard({ - longestWorkDuration, - longestWorkDate, - latestActiveTime, - cardClass, - sectionTitleClass, -}) { - return ( -
-

Work Session

-
-
-

- Longest -

-

- {longestWorkDuration}m -

-
-
-

- Date -

-

- {longestWorkDate || '-'} -

-
-
-

- Last Active -

-

- {latestActiveTime || '-'} -

-
-
-
- ); -} - -// Heatmap Section Component -function HeatmapSection({ heatmap }) { - const cardClass = 'glass-card p-6'; - const sectionTitleClass = - 'text-lg font-semibold tracking-tight text-slate-900'; - - return ( -
-
-

Activity Heatmap

- Past year -
-
-
- -
-
-
- ); -} - -// 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 ( - - {/* 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 ( - - - {dateKey}: {value} activities - - - ); - })} - - {/* Render month labels */} - {monthLabels.map((label, index) => ( - - {label.text} - - ))} - - {/* Render legend */} - - Less - - {colors.map((color, index) => { - const legendX = startX + 40 + index * (cellSize + 2); - return ( - - ); - })} - - More - - - ); -} - -// 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 ( -
- -
- ); -} - -// 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, - }); -} diff --git a/packages/cli/src/services/insight/types/StaticInsightTypes.ts b/packages/cli/src/services/insight/types/StaticInsightTypes.ts index 392b3fabe..29ce39f16 100644 --- a/packages/cli/src/services/insight/types/StaticInsightTypes.ts +++ b/packages/cli/src/services/insight/types/StaticInsightTypes.ts @@ -28,6 +28,9 @@ export interface InsightData { qualitative?: QualitativeInsights; satisfaction?: Record; friction?: Record; + primarySuccess?: Record; + outcomes?: Record; + topGoals?: Record; } export interface StreakData {