mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-07-09 17:19:02 +00:00
fix(insight): Harden insight facet normalization and empty qualitative handling (#3557)
* Harden insight facet normalization and empty qualitative handling * feat: enhance AtAGlance component to accept target sections for dynamic rendering
This commit is contained in:
parent
4e1b3827e9
commit
c52c9f172b
4 changed files with 958 additions and 208 deletions
|
|
@ -12,6 +12,14 @@ import type {
|
|||
SessionFacets,
|
||||
} from '../types/StaticInsightTypes.js';
|
||||
|
||||
const mockLogger = vi.hoisted(() => ({
|
||||
info: vi.fn(),
|
||||
error: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
debug: vi.fn(),
|
||||
}));
|
||||
const mockRunSideQuery = vi.hoisted(() => vi.fn());
|
||||
|
||||
// Mock dependencies
|
||||
vi.mock('@qwen-code/qwen-code-core', async () => {
|
||||
const actual = await vi.importActual<
|
||||
|
|
@ -20,12 +28,8 @@ vi.mock('@qwen-code/qwen-code-core', async () => {
|
|||
return {
|
||||
...actual,
|
||||
read: vi.fn(),
|
||||
createDebugLogger: vi.fn(() => ({
|
||||
info: vi.fn(),
|
||||
error: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
debug: vi.fn(),
|
||||
})),
|
||||
createDebugLogger: vi.fn(() => mockLogger),
|
||||
runSideQuery: mockRunSideQuery,
|
||||
};
|
||||
});
|
||||
|
||||
|
|
@ -53,6 +57,9 @@ describe('DataProcessor', () => {
|
|||
vi.clearAllMocks();
|
||||
|
||||
mockGenerateJson = vi.fn();
|
||||
mockRunSideQuery.mockImplementation((_config, request) =>
|
||||
mockGenerateJson(request),
|
||||
);
|
||||
mockConfig = {
|
||||
getBaseLlmClient: vi.fn(() => ({
|
||||
generateJson: mockGenerateJson,
|
||||
|
|
@ -375,6 +382,76 @@ describe('DataProcessor', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('should ignore zero and non-finite count values', () => {
|
||||
const facets = [
|
||||
{
|
||||
session_id: 's1',
|
||||
underlying_goal: 'test',
|
||||
goal_categories: { coding: 0, debugging: Number.NaN, testing: 2 },
|
||||
outcome: 'fully_achieved',
|
||||
user_satisfaction_counts: { satisfied: 0, happy: 1 },
|
||||
Qwen_helpfulness: 'very_helpful',
|
||||
session_type: 'single_task',
|
||||
friction_counts: { slow_response: Number.POSITIVE_INFINITY },
|
||||
friction_detail: '',
|
||||
primary_success: 'none',
|
||||
brief_summary: 'Test summary',
|
||||
},
|
||||
] as unknown as SessionFacets[];
|
||||
|
||||
const result = (
|
||||
dataProcessor as unknown as {
|
||||
aggregateFacetsData(facets: SessionFacets[]): {
|
||||
satisfactionAgg: Record<string, number>;
|
||||
frictionAgg: Record<string, number>;
|
||||
goalsAgg: Record<string, number>;
|
||||
};
|
||||
}
|
||||
).aggregateFacetsData(facets);
|
||||
|
||||
expect(result.satisfactionAgg).toEqual({ happy: 1 });
|
||||
expect(result.frictionAgg).toEqual({});
|
||||
expect(result.goalsAgg).toEqual({ testing: 2 });
|
||||
});
|
||||
|
||||
it('should ignore malformed count objects when aggregating facets', () => {
|
||||
const facets = [
|
||||
{
|
||||
session_id: 's1',
|
||||
underlying_goal: 'test',
|
||||
goal_categories: null,
|
||||
outcome: null,
|
||||
user_satisfaction_counts: null,
|
||||
Qwen_helpfulness: 'very_helpful',
|
||||
session_type: 'single_task',
|
||||
friction_counts: null,
|
||||
friction_detail: '',
|
||||
primary_success: null,
|
||||
brief_summary: 'Test summary',
|
||||
},
|
||||
] as unknown as SessionFacets[];
|
||||
|
||||
const result = (
|
||||
dataProcessor as unknown as {
|
||||
aggregateFacetsData(facets: SessionFacets[]): {
|
||||
satisfactionAgg: Record<string, number>;
|
||||
frictionAgg: Record<string, number>;
|
||||
primarySuccessAgg: Record<string, number>;
|
||||
outcomesAgg: Record<string, number>;
|
||||
goalsAgg: Record<string, number>;
|
||||
};
|
||||
}
|
||||
).aggregateFacetsData(facets);
|
||||
|
||||
expect(result.satisfactionAgg).toEqual({});
|
||||
expect(result.frictionAgg).toEqual({});
|
||||
expect(result.primarySuccessAgg).toEqual({});
|
||||
expect(result.goalsAgg).toEqual({});
|
||||
expect(result.outcomesAgg).toEqual({
|
||||
unclear_from_transcript: 1,
|
||||
});
|
||||
});
|
||||
|
||||
it('should aggregate friction counts', () => {
|
||||
const facets: SessionFacets[] = [
|
||||
{
|
||||
|
|
@ -631,6 +708,111 @@ describe('DataProcessor', () => {
|
|||
);
|
||||
});
|
||||
|
||||
it('should normalize malformed LLM facet fields', async () => {
|
||||
mockGenerateJson.mockResolvedValue({
|
||||
underlying_goal: ' Test goal ',
|
||||
goal_categories: { coding: 1 },
|
||||
outcome: null,
|
||||
user_satisfaction_counts: null,
|
||||
Qwen_helpfulness: 'invalid',
|
||||
session_type: null,
|
||||
friction_counts: null,
|
||||
friction_detail: null,
|
||||
primary_success: null,
|
||||
brief_summary: ' Test summary ',
|
||||
});
|
||||
|
||||
const records: ChatRecord[] = [
|
||||
{
|
||||
sessionId: 'test-session',
|
||||
timestamp: new Date().toISOString(),
|
||||
type: 'user',
|
||||
message: {
|
||||
role: 'user',
|
||||
parts: [{ text: 'Help me with code' }],
|
||||
},
|
||||
uuid: '',
|
||||
parentUuid: null,
|
||||
cwd: '',
|
||||
version: '',
|
||||
},
|
||||
];
|
||||
|
||||
const result = await (
|
||||
dataProcessor as unknown as {
|
||||
analyzeSession(records: ChatRecord[]): Promise<SessionFacets | null>;
|
||||
}
|
||||
).analyzeSession(records);
|
||||
|
||||
expect(result).toEqual({
|
||||
session_id: 'test-session',
|
||||
underlying_goal: 'Test goal',
|
||||
goal_categories: { coding: 1 },
|
||||
outcome: 'unclear_from_transcript',
|
||||
user_satisfaction_counts: {},
|
||||
Qwen_helpfulness: 'moderately_helpful',
|
||||
session_type: 'single_task',
|
||||
friction_counts: {},
|
||||
friction_detail: '',
|
||||
primary_success: 'none',
|
||||
brief_summary: 'Test summary',
|
||||
});
|
||||
expect(mockLogger.debug).toHaveBeenCalledWith(
|
||||
'Normalized unknown insight enum value "invalid" to fallback "moderately_helpful"',
|
||||
);
|
||||
});
|
||||
|
||||
it('should normalize case-variant LLM facet fields to canonical values', async () => {
|
||||
mockGenerateJson.mockResolvedValue({
|
||||
underlying_goal: 'Test goal',
|
||||
goal_categories: { coding: 1 },
|
||||
outcome: 'FULLY_ACHIEVED',
|
||||
user_satisfaction_counts: null,
|
||||
Qwen_helpfulness: 'Very_Helpful',
|
||||
session_type: 'Multi_Task',
|
||||
friction_counts: null,
|
||||
friction_detail: null,
|
||||
primary_success: null,
|
||||
brief_summary: 'Test summary',
|
||||
});
|
||||
|
||||
const records: ChatRecord[] = [
|
||||
{
|
||||
sessionId: 'test-session',
|
||||
timestamp: new Date().toISOString(),
|
||||
type: 'user',
|
||||
message: {
|
||||
role: 'user',
|
||||
parts: [{ text: 'Help me with code' }],
|
||||
},
|
||||
uuid: '',
|
||||
parentUuid: null,
|
||||
cwd: '',
|
||||
version: '',
|
||||
},
|
||||
];
|
||||
|
||||
const result = await (
|
||||
dataProcessor as unknown as {
|
||||
analyzeSession(records: ChatRecord[]): Promise<SessionFacets | null>;
|
||||
}
|
||||
).analyzeSession(records);
|
||||
|
||||
expect(result).toEqual({
|
||||
session_id: 'test-session',
|
||||
underlying_goal: 'Test goal',
|
||||
goal_categories: { coding: 1 },
|
||||
outcome: 'fully_achieved',
|
||||
user_satisfaction_counts: {},
|
||||
Qwen_helpfulness: 'very_helpful',
|
||||
session_type: 'multi_task',
|
||||
friction_counts: {},
|
||||
friction_detail: '',
|
||||
primary_success: 'none',
|
||||
brief_summary: 'Test summary',
|
||||
});
|
||||
});
|
||||
|
||||
it('should return null when LLM returns empty result', async () => {
|
||||
mockGenerateJson.mockResolvedValue({});
|
||||
|
||||
|
|
@ -1233,6 +1415,25 @@ describe('DataProcessor', () => {
|
|||
expect(result).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should return undefined when all qualitative sections are empty', async () => {
|
||||
mockGenerateJson.mockResolvedValue({});
|
||||
|
||||
const result = await (
|
||||
dataProcessor as unknown as {
|
||||
generateQualitativeInsights(
|
||||
metrics: Omit<InsightData, 'facets' | 'qualitative'>,
|
||||
facets: SessionFacets[],
|
||||
): Promise<
|
||||
| import('../types/QualitativeInsightTypes.js').QualitativeInsights
|
||||
| undefined
|
||||
>;
|
||||
}
|
||||
).generateQualitativeInsights(mockMetrics, mockFacets);
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
expect(mockGenerateJson).toHaveBeenCalledTimes(8);
|
||||
});
|
||||
|
||||
it('should return full qualitative data when all LLM calls succeed', async () => {
|
||||
mockGenerateJson.mockResolvedValue({ intro: 'test', areas: [] });
|
||||
|
||||
|
|
@ -1329,5 +1530,138 @@ describe('DataProcessor', () => {
|
|||
expect(result).toHaveLength(1);
|
||||
expect(result[0].session_id).toBe('conversational');
|
||||
});
|
||||
|
||||
it('should normalize cached facets before reusing them', async () => {
|
||||
const conversationalRecords: ChatRecord[] = [
|
||||
{
|
||||
sessionId: 'cached-session',
|
||||
timestamp: '2025-01-15T10:00:00Z',
|
||||
type: 'user',
|
||||
message: { role: 'user', parts: [{ text: 'Hello' }] },
|
||||
uuid: '',
|
||||
parentUuid: null,
|
||||
cwd: '',
|
||||
version: '',
|
||||
},
|
||||
{
|
||||
sessionId: 'cached-session',
|
||||
timestamp: '2025-01-15T10:01:00Z',
|
||||
type: 'assistant',
|
||||
message: { role: 'assistant', parts: [{ text: 'Hi' }] },
|
||||
uuid: '',
|
||||
parentUuid: null,
|
||||
cwd: '',
|
||||
version: '',
|
||||
},
|
||||
];
|
||||
|
||||
mockedReadJsonlFile.mockResolvedValue(conversationalRecords);
|
||||
mockedFs.readFile.mockResolvedValue(
|
||||
JSON.stringify({
|
||||
underlying_goal: ' Cached goal ',
|
||||
goal_categories: null,
|
||||
outcome: null,
|
||||
user_satisfaction_counts: null,
|
||||
Qwen_helpfulness: 'very_helpful',
|
||||
session_type: 'single_task',
|
||||
friction_counts: null,
|
||||
friction_detail: null,
|
||||
primary_success: null,
|
||||
brief_summary: ' Cached summary ',
|
||||
}),
|
||||
);
|
||||
|
||||
const files = [{ path: '/test/cached-session.jsonl', mtime: 1000 }];
|
||||
|
||||
const result = await (
|
||||
dataProcessor as unknown as {
|
||||
generateFacets(
|
||||
files: Array<{ path: string; mtime: number }>,
|
||||
facetsOutputDir?: string,
|
||||
): Promise<SessionFacets[]>;
|
||||
}
|
||||
).generateFacets(files, '/facets');
|
||||
|
||||
expect(mockGenerateJson).not.toHaveBeenCalled();
|
||||
expect(result).toEqual([
|
||||
{
|
||||
session_id: 'cached-session',
|
||||
underlying_goal: 'Cached goal',
|
||||
goal_categories: {},
|
||||
outcome: 'unclear_from_transcript',
|
||||
user_satisfaction_counts: {},
|
||||
Qwen_helpfulness: 'very_helpful',
|
||||
session_type: 'single_task',
|
||||
friction_counts: {},
|
||||
friction_detail: '',
|
||||
primary_success: 'none',
|
||||
brief_summary: 'Cached summary',
|
||||
},
|
||||
]);
|
||||
expect(mockedFs.writeFile).toHaveBeenCalledWith(
|
||||
expect.stringMatching(/[\\/]facets[\\/]cached-session\.json$/),
|
||||
JSON.stringify(result[0], null, 2),
|
||||
'utf-8',
|
||||
);
|
||||
});
|
||||
|
||||
it('should reuse normalized cached facets when writeback fails', async () => {
|
||||
const conversationalRecords: ChatRecord[] = [
|
||||
{
|
||||
sessionId: 'cached-session',
|
||||
timestamp: '2025-01-15T10:00:00Z',
|
||||
type: 'user',
|
||||
message: { role: 'user', parts: [{ text: 'Hello' }] },
|
||||
uuid: '',
|
||||
parentUuid: null,
|
||||
cwd: '',
|
||||
version: '',
|
||||
},
|
||||
{
|
||||
sessionId: 'cached-session',
|
||||
timestamp: '2025-01-15T10:01:00Z',
|
||||
type: 'assistant',
|
||||
message: { role: 'assistant', parts: [{ text: 'Hi' }] },
|
||||
uuid: '',
|
||||
parentUuid: null,
|
||||
cwd: '',
|
||||
version: '',
|
||||
},
|
||||
];
|
||||
const writeError = new Error('disk full');
|
||||
|
||||
mockedReadJsonlFile.mockResolvedValue(conversationalRecords);
|
||||
mockedFs.readFile.mockResolvedValue(
|
||||
JSON.stringify({
|
||||
underlying_goal: 'Cached goal',
|
||||
brief_summary: 'Cached summary',
|
||||
}),
|
||||
);
|
||||
mockedFs.writeFile.mockRejectedValueOnce(writeError);
|
||||
|
||||
const files = [{ path: '/test/cached-session.jsonl', mtime: 1000 }];
|
||||
|
||||
const result = await (
|
||||
dataProcessor as unknown as {
|
||||
generateFacets(
|
||||
files: Array<{ path: string; mtime: number }>,
|
||||
facetsOutputDir?: string,
|
||||
): Promise<SessionFacets[]>;
|
||||
}
|
||||
).generateFacets(files, '/facets');
|
||||
|
||||
expect(mockGenerateJson).not.toHaveBeenCalled();
|
||||
expect(result).toEqual([
|
||||
expect.objectContaining({
|
||||
session_id: 'cached-session',
|
||||
underlying_goal: 'Cached goal',
|
||||
brief_summary: 'Cached summary',
|
||||
}),
|
||||
]);
|
||||
expect(mockLogger.warn).toHaveBeenCalledWith(
|
||||
'Failed to write back normalized facet for cached-session:',
|
||||
writeError,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -39,6 +39,167 @@ import {
|
|||
const logger = createDebugLogger('DataProcessor');
|
||||
|
||||
const CONCURRENCY_LIMIT = 4;
|
||||
const SESSION_OUTCOMES = [
|
||||
'fully_achieved',
|
||||
'mostly_achieved',
|
||||
'partially_achieved',
|
||||
'not_achieved',
|
||||
'unclear_from_transcript',
|
||||
] as const;
|
||||
const OUTCOME_FALLBACK = 'unclear_from_transcript';
|
||||
const QWEN_HELPFULNESS_LEVELS = [
|
||||
'unhelpful',
|
||||
'slightly_helpful',
|
||||
'moderately_helpful',
|
||||
'very_helpful',
|
||||
'essential',
|
||||
] as const;
|
||||
const SESSION_TYPES = [
|
||||
'single_task',
|
||||
'multi_task',
|
||||
'iterative_refinement',
|
||||
'exploration',
|
||||
'quick_question',
|
||||
] as const;
|
||||
const PRIMARY_SUCCESS_VALUES = [
|
||||
'none',
|
||||
'fast_accurate_search',
|
||||
'correct_code_edits',
|
||||
'good_explanations',
|
||||
'proactive_help',
|
||||
'multi_file_changes',
|
||||
'good_debugging',
|
||||
] as const;
|
||||
const PRIMARY_SUCCESS_FALLBACK = 'none';
|
||||
|
||||
// Keep in sync with packages/web-templates/src/insight/src/App.tsx.
|
||||
function hasMeaningfulInsightValue(value: unknown): boolean {
|
||||
if (typeof value === 'string') {
|
||||
return value.trim().length > 0;
|
||||
}
|
||||
|
||||
if (typeof value === 'number') {
|
||||
return Number.isFinite(value) && value !== 0;
|
||||
}
|
||||
|
||||
if (typeof value === 'boolean') {
|
||||
return value;
|
||||
}
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
return value.some((item) => hasMeaningfulInsightValue(item));
|
||||
}
|
||||
|
||||
if (value && typeof value === 'object') {
|
||||
return Object.values(value).some((item) => hasMeaningfulInsightValue(item));
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function normalizeInsightText(value: unknown): string {
|
||||
return typeof value === 'string' ? value.trim() : '';
|
||||
}
|
||||
|
||||
function normalizeInsightCountRecord(value: unknown): Record<string, number> {
|
||||
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return Object.entries(value).reduce<Record<string, number>>(
|
||||
(acc, [key, count]) => {
|
||||
if (typeof count === 'number' && Number.isFinite(count) && count > 0) {
|
||||
acc[key] = count;
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
{},
|
||||
);
|
||||
}
|
||||
|
||||
function getInsightCountEntries(value: unknown): Array<[string, number]> {
|
||||
return Object.entries(normalizeInsightCountRecord(value));
|
||||
}
|
||||
|
||||
function normalizeInsightEnum<T extends string>(
|
||||
value: unknown,
|
||||
allowed: readonly T[],
|
||||
fallback: T,
|
||||
): T {
|
||||
const trimmed = typeof value === 'string' ? value.trim() : '';
|
||||
if (trimmed) {
|
||||
const match = allowed.find(
|
||||
(item) => String(item).toLowerCase() === trimmed.toLowerCase(),
|
||||
);
|
||||
if (match) return match;
|
||||
}
|
||||
|
||||
logger.debug(
|
||||
`Normalized unknown insight enum value "${String(value)}" to fallback "${fallback}"`,
|
||||
);
|
||||
return fallback;
|
||||
}
|
||||
|
||||
function normalizeSessionFacet(
|
||||
facet: unknown,
|
||||
sessionId: string,
|
||||
): SessionFacets | null {
|
||||
if (!facet || typeof facet !== 'object' || Array.isArray(facet)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const rawFacet = facet as Record<string, unknown>;
|
||||
const normalizedFacet: SessionFacets = {
|
||||
session_id: sessionId,
|
||||
underlying_goal: normalizeInsightText(rawFacet['underlying_goal']),
|
||||
goal_categories: normalizeInsightCountRecord(rawFacet['goal_categories']),
|
||||
outcome: normalizeInsightEnum(
|
||||
rawFacet['outcome'],
|
||||
SESSION_OUTCOMES,
|
||||
OUTCOME_FALLBACK,
|
||||
),
|
||||
user_satisfaction_counts: normalizeInsightCountRecord(
|
||||
rawFacet['user_satisfaction_counts'],
|
||||
),
|
||||
Qwen_helpfulness: normalizeInsightEnum(
|
||||
rawFacet['Qwen_helpfulness'],
|
||||
QWEN_HELPFULNESS_LEVELS,
|
||||
'moderately_helpful',
|
||||
),
|
||||
session_type: normalizeInsightEnum(
|
||||
rawFacet['session_type'],
|
||||
SESSION_TYPES,
|
||||
'single_task',
|
||||
),
|
||||
friction_counts: normalizeInsightCountRecord(rawFacet['friction_counts']),
|
||||
friction_detail: normalizeInsightText(rawFacet['friction_detail']),
|
||||
primary_success: normalizeInsightEnum(
|
||||
rawFacet['primary_success'],
|
||||
PRIMARY_SUCCESS_VALUES,
|
||||
PRIMARY_SUCCESS_FALLBACK,
|
||||
),
|
||||
brief_summary: normalizeInsightText(rawFacet['brief_summary']),
|
||||
};
|
||||
|
||||
const meaningfulContent = {
|
||||
underlying_goal: normalizedFacet.underlying_goal,
|
||||
goal_categories: normalizedFacet.goal_categories,
|
||||
outcome:
|
||||
normalizedFacet.outcome === OUTCOME_FALLBACK
|
||||
? ''
|
||||
: normalizedFacet.outcome,
|
||||
user_satisfaction_counts: normalizedFacet.user_satisfaction_counts,
|
||||
friction_counts: normalizedFacet.friction_counts,
|
||||
friction_detail: normalizedFacet.friction_detail,
|
||||
primary_success:
|
||||
normalizedFacet.primary_success === PRIMARY_SUCCESS_FALLBACK
|
||||
? ''
|
||||
: normalizedFacet.primary_success,
|
||||
brief_summary: normalizedFacet.brief_summary,
|
||||
};
|
||||
|
||||
return hasMeaningfulInsightValue(meaningfulContent) ? normalizedFacet : null;
|
||||
}
|
||||
|
||||
export class DataProcessor {
|
||||
constructor(private config: Config) {}
|
||||
|
|
@ -213,10 +374,17 @@ export class DataProcessor {
|
|||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
...(result as unknown as SessionFacets),
|
||||
session_id: records[0].sessionId,
|
||||
};
|
||||
const sessionId = records[0].sessionId;
|
||||
const normalizedFacet = normalizeSessionFacet(result, sessionId);
|
||||
|
||||
if (!normalizedFacet) {
|
||||
logger.warn(
|
||||
`Ignoring malformed insight facet for session ${sessionId}`,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
return normalizedFacet;
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
`Failed to analyze session ${records[0]?.sessionId}:`,
|
||||
|
|
@ -343,28 +511,38 @@ export class DataProcessor {
|
|||
|
||||
facets.forEach((facet) => {
|
||||
// Aggregate satisfaction
|
||||
Object.entries(facet.user_satisfaction_counts).forEach(([sat, count]) => {
|
||||
satisfactionAgg[sat] = (satisfactionAgg[sat] || 0) + count;
|
||||
});
|
||||
getInsightCountEntries(facet.user_satisfaction_counts).forEach(
|
||||
([sat, count]) => {
|
||||
satisfactionAgg[sat] = (satisfactionAgg[sat] || 0) + count;
|
||||
},
|
||||
);
|
||||
|
||||
// Aggregate friction
|
||||
Object.entries(facet.friction_counts).forEach(([fric, count]) => {
|
||||
getInsightCountEntries(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;
|
||||
const primarySuccess = normalizeInsightEnum(
|
||||
facet.primary_success,
|
||||
PRIMARY_SUCCESS_VALUES,
|
||||
PRIMARY_SUCCESS_FALLBACK,
|
||||
);
|
||||
if (primarySuccess !== PRIMARY_SUCCESS_FALLBACK) {
|
||||
primarySuccessAgg[primarySuccess] =
|
||||
(primarySuccessAgg[primarySuccess] || 0) + 1;
|
||||
}
|
||||
|
||||
// Aggregate outcomes
|
||||
if (facet.outcome) {
|
||||
outcomesAgg[facet.outcome] = (outcomesAgg[facet.outcome] || 0) + 1;
|
||||
}
|
||||
const outcome = normalizeInsightEnum(
|
||||
facet.outcome,
|
||||
SESSION_OUTCOMES,
|
||||
OUTCOME_FALLBACK,
|
||||
);
|
||||
outcomesAgg[outcome] = (outcomesAgg[outcome] || 0) + 1;
|
||||
|
||||
// Aggregate goals
|
||||
Object.entries(facet.goal_categories).forEach(([goal, count]) => {
|
||||
getInsightCountEntries(facet.goal_categories).forEach(([goal, count]) => {
|
||||
goalsAgg[goal] = (goalsAgg[goal] || 0) + count;
|
||||
});
|
||||
});
|
||||
|
|
@ -671,7 +849,7 @@ export class DataProcessor {
|
|||
),
|
||||
);
|
||||
|
||||
return {
|
||||
const qualitative = {
|
||||
impressiveWorkflows,
|
||||
projectAreas,
|
||||
futureOpportunities,
|
||||
|
|
@ -681,6 +859,8 @@ export class DataProcessor {
|
|||
interactionStyle,
|
||||
atAGlance,
|
||||
};
|
||||
|
||||
return hasMeaningfulInsightValue(qualitative) ? qualitative : undefined;
|
||||
} catch (e) {
|
||||
logger.error('Error generating qualitative insights:', e);
|
||||
return undefined;
|
||||
|
|
@ -700,27 +880,38 @@ export class DataProcessor {
|
|||
|
||||
facets.forEach((facet) => {
|
||||
// Aggregate goals
|
||||
Object.entries(facet.goal_categories).forEach(([goal, count]) => {
|
||||
getInsightCountEntries(facet.goal_categories).forEach(([goal, count]) => {
|
||||
goalsAgg[goal] = (goalsAgg[goal] || 0) + count;
|
||||
});
|
||||
|
||||
// Aggregate outcomes
|
||||
outcomesAgg[facet.outcome] = (outcomesAgg[facet.outcome] || 0) + 1;
|
||||
const outcome = normalizeInsightEnum(
|
||||
facet.outcome,
|
||||
SESSION_OUTCOMES,
|
||||
OUTCOME_FALLBACK,
|
||||
);
|
||||
outcomesAgg[outcome] = (outcomesAgg[outcome] || 0) + 1;
|
||||
|
||||
// Aggregate satisfaction
|
||||
Object.entries(facet.user_satisfaction_counts).forEach(([sat, count]) => {
|
||||
satisfactionAgg[sat] = (satisfactionAgg[sat] || 0) + count;
|
||||
});
|
||||
getInsightCountEntries(facet.user_satisfaction_counts).forEach(
|
||||
([sat, count]) => {
|
||||
satisfactionAgg[sat] = (satisfactionAgg[sat] || 0) + count;
|
||||
},
|
||||
);
|
||||
|
||||
// Aggregate friction
|
||||
Object.entries(facet.friction_counts).forEach(([fric, count]) => {
|
||||
getInsightCountEntries(facet.friction_counts).forEach(([fric, count]) => {
|
||||
frictionAgg[fric] = (frictionAgg[fric] || 0) + count;
|
||||
});
|
||||
|
||||
// Aggregate success (primary_success)
|
||||
if (facet.primary_success && facet.primary_success !== 'none') {
|
||||
successAgg[facet.primary_success] =
|
||||
(successAgg[facet.primary_success] || 0) + 1;
|
||||
const primarySuccess = normalizeInsightEnum(
|
||||
facet.primary_success,
|
||||
PRIMARY_SUCCESS_VALUES,
|
||||
PRIMARY_SUCCESS_FALLBACK,
|
||||
);
|
||||
if (primarySuccess !== PRIMARY_SUCCESS_FALLBACK) {
|
||||
successAgg[primarySuccess] = (successAgg[primarySuccess] || 0) + 1;
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -748,13 +939,16 @@ export class DataProcessor {
|
|||
|
||||
// 2. SESSION SUMMARIES section
|
||||
const sessionSummaries = facets
|
||||
.map((f) => `- ${f.brief_summary}`)
|
||||
.map((f) => normalizeInsightText(f.brief_summary))
|
||||
.filter((summary) => summary.length > 0)
|
||||
.map((summary) => `- ${summary}`)
|
||||
.join('\n');
|
||||
|
||||
// 3. FRICTION DETAILS section
|
||||
const frictionDetails = facets
|
||||
.filter((f) => f.friction_detail && f.friction_detail.trim().length > 0)
|
||||
.map((f) => `- ${f.friction_detail}`)
|
||||
.map((f) => normalizeInsightText(f.friction_detail))
|
||||
.filter((detail) => detail.length > 0)
|
||||
.map((detail) => `- ${detail}`)
|
||||
.join('\n');
|
||||
|
||||
return `DATA:
|
||||
|
|
@ -1072,6 +1266,31 @@ None captured`;
|
|||
'utf-8',
|
||||
);
|
||||
const existingFacet = JSON.parse(existingData);
|
||||
const normalizedFacet = normalizeSessionFacet(
|
||||
existingFacet,
|
||||
sessionId,
|
||||
);
|
||||
if (!normalizedFacet) {
|
||||
logger.warn(
|
||||
`Cached insight facet for ${sessionId} is malformed, regenerating.`,
|
||||
);
|
||||
throw new Error('Malformed cached insight facet');
|
||||
}
|
||||
const normalizedData = JSON.stringify(normalizedFacet, null, 2);
|
||||
if (existingData !== normalizedData) {
|
||||
try {
|
||||
await fs.writeFile(
|
||||
existingFacetPath,
|
||||
normalizedData,
|
||||
'utf-8',
|
||||
);
|
||||
} catch (writeError) {
|
||||
logger.warn(
|
||||
`Failed to write back normalized facet for ${sessionId}:`,
|
||||
writeError,
|
||||
);
|
||||
}
|
||||
}
|
||||
completed++;
|
||||
if (onProgress) {
|
||||
const percent = 20 + Math.round((completed / total) * 60);
|
||||
|
|
@ -1081,7 +1300,7 @@ None captured`;
|
|||
`${completed}/${total}`,
|
||||
);
|
||||
}
|
||||
return existingFacet;
|
||||
return normalizedFacet;
|
||||
} catch (readError) {
|
||||
// File doesn't exist or is invalid, proceed to analyze
|
||||
if ((readError as NodeJS.ErrnoException).code !== 'ENOENT') {
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { StatsRow } from './Header';
|
|||
import {
|
||||
AtAGlance,
|
||||
NavToc,
|
||||
type NavTocSection,
|
||||
ProjectAreas,
|
||||
InteractionStyle,
|
||||
ImpressiveWorkflows,
|
||||
|
|
@ -18,6 +19,48 @@ import type { InsightData } from './types';
|
|||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
import React from 'react';
|
||||
|
||||
// Keep in sync with packages/cli/src/services/insight/generators/DataProcessor.ts.
|
||||
function hasMeaningfulValue(value: unknown): boolean {
|
||||
if (typeof value === 'string') {
|
||||
return value.trim().length > 0;
|
||||
}
|
||||
|
||||
if (typeof value === 'number') {
|
||||
return Number.isFinite(value) && value !== 0;
|
||||
}
|
||||
|
||||
if (typeof value === 'boolean') {
|
||||
return value;
|
||||
}
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
return value.some((item) => hasMeaningfulValue(item));
|
||||
}
|
||||
|
||||
if (value && typeof value === 'object') {
|
||||
return Object.values(value).some((item) => hasMeaningfulValue(item));
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function hasRecordEntries(
|
||||
value?: Record<string, number> | Array<[string, number]>,
|
||||
): boolean {
|
||||
if (Array.isArray(value)) {
|
||||
return value.some(([, count]) => Number.isFinite(count) && count !== 0);
|
||||
}
|
||||
|
||||
return (
|
||||
!!value &&
|
||||
Object.values(value).some((count) => Number.isFinite(count) && count !== 0)
|
||||
);
|
||||
}
|
||||
|
||||
function hasMeaningfulArray(value: unknown): boolean {
|
||||
return Array.isArray(value) && value.some((item) => hasMeaningfulValue(item));
|
||||
}
|
||||
|
||||
// Main App Component
|
||||
function InsightApp({ data }: { data: InsightData }) {
|
||||
const [cardTheme, setCardTheme] = useState<Theme>('dark');
|
||||
|
|
@ -96,6 +139,78 @@ function InsightApp({ data }: { data: InsightData }) {
|
|||
dateRangeStr = `${formatDate(minDate)} to ${formatDate(maxDate)}`;
|
||||
}
|
||||
|
||||
const showAtAGlance = hasMeaningfulValue(data.qualitative?.atAGlance);
|
||||
const showProjectAreas =
|
||||
!!data.qualitative &&
|
||||
(hasMeaningfulValue(data.qualitative.projectAreas) ||
|
||||
hasRecordEntries(data.topGoals) ||
|
||||
hasRecordEntries(data.topTools));
|
||||
const showInteractionStyle = hasMeaningfulValue(
|
||||
data.qualitative?.interactionStyle,
|
||||
);
|
||||
const showImpressiveWorkflows =
|
||||
!!data.qualitative &&
|
||||
(hasMeaningfulValue(data.qualitative.impressiveWorkflows) ||
|
||||
hasRecordEntries(data.primarySuccess) ||
|
||||
hasRecordEntries(data.outcomes));
|
||||
const showFrictionPoints =
|
||||
!!data.qualitative &&
|
||||
(hasMeaningfulValue(data.qualitative.frictionPoints) ||
|
||||
hasRecordEntries(data.satisfaction) ||
|
||||
hasRecordEntries(data.friction));
|
||||
const showFeatures =
|
||||
!!data.qualitative &&
|
||||
(hasMeaningfulArray(data.qualitative.improvements?.Qwen_md_additions) ||
|
||||
hasMeaningfulArray(data.qualitative.improvements?.features_to_try));
|
||||
const showPatterns =
|
||||
!!data.qualitative &&
|
||||
hasMeaningfulArray(data.qualitative.improvements?.usage_patterns);
|
||||
const showFutureOpportunities = hasMeaningfulValue(
|
||||
data.qualitative?.futureOpportunities,
|
||||
);
|
||||
const showMemorableMoment = hasMeaningfulValue(
|
||||
data.qualitative?.memorableMoment,
|
||||
);
|
||||
|
||||
const navSections: NavTocSection[] = [];
|
||||
if (showProjectAreas) {
|
||||
navSections.push({ href: '#section-work', label: 'What You Work On' });
|
||||
}
|
||||
if (showInteractionStyle) {
|
||||
navSections.push({
|
||||
href: '#section-usage',
|
||||
label: 'How You Use Qwen Code',
|
||||
});
|
||||
}
|
||||
if (showImpressiveWorkflows) {
|
||||
navSections.push({ href: '#section-wins', label: 'Impressive Things' });
|
||||
}
|
||||
if (showFrictionPoints) {
|
||||
navSections.push({
|
||||
href: '#section-friction',
|
||||
label: 'Where Things Go Wrong',
|
||||
});
|
||||
}
|
||||
if (showFeatures) {
|
||||
navSections.push({ href: '#section-features', label: 'Features to Try' });
|
||||
}
|
||||
if (showPatterns) {
|
||||
navSections.push({
|
||||
href: '#section-patterns',
|
||||
label: 'New Usage Patterns',
|
||||
});
|
||||
}
|
||||
if (showFutureOpportunities) {
|
||||
navSections.push({ href: '#section-horizon', label: 'On the Horizon' });
|
||||
}
|
||||
|
||||
const atAGlanceTargetSections = {
|
||||
wins: showImpressiveWorkflows,
|
||||
friction: showFrictionPoints,
|
||||
features: showFeatures,
|
||||
horizon: showFutureOpportunities,
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
{/* Elegant Header */}
|
||||
|
|
@ -115,49 +230,60 @@ function InsightApp({ data }: { data: InsightData }) {
|
|||
</div>
|
||||
</header>
|
||||
|
||||
{data.qualitative && (
|
||||
<>
|
||||
<AtAGlance qualitative={data.qualitative} />
|
||||
<NavToc />
|
||||
</>
|
||||
{showAtAGlance && data.qualitative && (
|
||||
<AtAGlance
|
||||
qualitative={data.qualitative}
|
||||
targetSections={atAGlanceTargetSections}
|
||||
/>
|
||||
)}
|
||||
|
||||
{navSections.length > 0 && <NavToc sections={navSections} />}
|
||||
|
||||
<StatsRow data={data} />
|
||||
|
||||
{data.qualitative && (
|
||||
<>
|
||||
<ProjectAreas
|
||||
qualitative={data.qualitative}
|
||||
topGoals={data.topGoals}
|
||||
topTools={data.topTools}
|
||||
/>
|
||||
</>
|
||||
{showProjectAreas && data.qualitative && (
|
||||
<ProjectAreas
|
||||
qualitative={data.qualitative}
|
||||
topGoals={data.topGoals}
|
||||
topTools={data.topTools}
|
||||
/>
|
||||
)}
|
||||
|
||||
{data.qualitative && (
|
||||
<>
|
||||
<InteractionStyle qualitative={data.qualitative} insights={data} />
|
||||
</>
|
||||
{showInteractionStyle && 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} />
|
||||
</>
|
||||
{showImpressiveWorkflows && (
|
||||
<ImpressiveWorkflows
|
||||
qualitative={data.qualitative}
|
||||
primarySuccess={data.primarySuccess ?? {}}
|
||||
outcomes={data.outcomes ?? {}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{showFrictionPoints && (
|
||||
<FrictionPoints
|
||||
qualitative={data.qualitative}
|
||||
satisfaction={data.satisfaction ?? {}}
|
||||
friction={data.friction ?? {}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{(showFeatures || showPatterns) && data.qualitative && (
|
||||
<Improvements qualitative={data.qualitative} />
|
||||
)}
|
||||
|
||||
{showFutureOpportunities && data.qualitative && (
|
||||
<FutureOpportunities qualitative={data.qualitative} />
|
||||
)}
|
||||
|
||||
{showMemorableMoment && data.qualitative && (
|
||||
<MemorableMoment qualitative={data.qualitative} />
|
||||
)}
|
||||
|
||||
{/*
|
||||
The share card remains available even if qualitative sections are absent.
|
||||
*/}
|
||||
<ShareCard data={data} theme={cardTheme} />
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -10,57 +10,99 @@ import React from 'react';
|
|||
// Qualitative Insight Components
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
export function AtAGlance({ qualitative }: { qualitative: QualitativeData }) {
|
||||
function hasMeaningfulText(value: unknown): value is string {
|
||||
return typeof value === 'string' && value.trim().length > 0;
|
||||
}
|
||||
|
||||
export interface AtAGlanceTargetSections {
|
||||
wins: boolean;
|
||||
friction: boolean;
|
||||
features: boolean;
|
||||
horizon: boolean;
|
||||
}
|
||||
|
||||
export function AtAGlance({
|
||||
qualitative,
|
||||
targetSections,
|
||||
}: {
|
||||
qualitative: QualitativeData;
|
||||
targetSections: AtAGlanceTargetSections;
|
||||
}) {
|
||||
const { atAGlance } = qualitative;
|
||||
if (!atAGlance) return null;
|
||||
|
||||
const sections = [
|
||||
{
|
||||
key: 'wins',
|
||||
label: "What's working:",
|
||||
body: atAGlance.whats_working,
|
||||
href: '#section-wins',
|
||||
link: 'Impressive Things You Did →',
|
||||
showLink: targetSections.wins,
|
||||
},
|
||||
{
|
||||
key: 'friction',
|
||||
label: "What's hindering you:",
|
||||
body: atAGlance.whats_hindering,
|
||||
href: '#section-friction',
|
||||
link: 'Where Things Go Wrong →',
|
||||
showLink: targetSections.friction,
|
||||
},
|
||||
{
|
||||
key: 'features',
|
||||
label: 'Quick wins to try:',
|
||||
body: atAGlance.quick_wins,
|
||||
href: '#section-features',
|
||||
link: 'Features to Try →',
|
||||
showLink: targetSections.features,
|
||||
},
|
||||
{
|
||||
key: 'horizon',
|
||||
label: 'Ambitious workflows:',
|
||||
body: atAGlance.ambitious_workflows,
|
||||
href: '#section-horizon',
|
||||
link: 'On the Horizon →',
|
||||
showLink: targetSections.horizon,
|
||||
},
|
||||
].filter((section) => hasMeaningfulText(section.body));
|
||||
|
||||
if (sections.length === 0) 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'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'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>
|
||||
{sections.map((section) => (
|
||||
<div key={section.key} className="glance-section">
|
||||
<strong>{section.label}</strong>{' '}
|
||||
<MarkdownText>{section.body}</MarkdownText>
|
||||
{section.showLink && (
|
||||
<a href={section.href} className="see-more">
|
||||
{section.link}
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function NavToc() {
|
||||
export interface NavTocSection {
|
||||
href: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
export function NavToc({ sections }: { sections: NavTocSection[] }) {
|
||||
if (sections.length === 0) return null;
|
||||
|
||||
return (
|
||||
<nav className="nav-toc">
|
||||
<a href="#section-work">What You Work On</a>
|
||||
<a href="#section-usage">How You Use Qwen Code</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>
|
||||
{sections.map((section) => (
|
||||
<a key={section.href} href={section.href}>
|
||||
{section.label}
|
||||
</a>
|
||||
))}
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
|
|
@ -176,12 +218,11 @@ export function ImpressiveWorkflows({
|
|||
primarySuccess,
|
||||
outcomes,
|
||||
}: {
|
||||
qualitative: QualitativeData;
|
||||
qualitative?: QualitativeData;
|
||||
primarySuccess: Record<string, number>;
|
||||
outcomes: Record<string, number>;
|
||||
}) {
|
||||
const { impressiveWorkflows } = qualitative;
|
||||
if (!impressiveWorkflows) return null;
|
||||
const impressiveWorkflows = qualitative?.impressiveWorkflows;
|
||||
|
||||
return (
|
||||
<>
|
||||
|
|
@ -191,22 +232,24 @@ export function ImpressiveWorkflows({
|
|||
>
|
||||
Impressive Things You Did
|
||||
</h2>
|
||||
{impressiveWorkflows.intro && (
|
||||
{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>
|
||||
{impressiveWorkflows && (
|
||||
<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>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div
|
||||
style={{
|
||||
|
|
@ -277,7 +320,9 @@ function HorizontalBarChart({
|
|||
if (!data || Object.keys(data).length === 0) return null;
|
||||
|
||||
// Filter and sort entries
|
||||
let entries = Object.entries(data);
|
||||
let entries = Object.entries(data).filter(
|
||||
([, count]) => Number.isFinite(count) && count !== 0,
|
||||
);
|
||||
if (allowedKeys) {
|
||||
entries = entries.filter(([key]) => allowedKeys.includes(key));
|
||||
}
|
||||
|
|
@ -400,12 +445,11 @@ export function FrictionPoints({
|
|||
satisfaction,
|
||||
friction,
|
||||
}: {
|
||||
qualitative: QualitativeData;
|
||||
qualitative?: QualitativeData;
|
||||
satisfaction?: Record<string, number>;
|
||||
friction?: Record<string, number>;
|
||||
}) {
|
||||
const { frictionPoints } = qualitative;
|
||||
if (!frictionPoints) return null;
|
||||
const frictionPoints = qualitative?.frictionPoints;
|
||||
|
||||
return (
|
||||
<>
|
||||
|
|
@ -415,31 +459,33 @@ export function FrictionPoints({
|
|||
>
|
||||
Where Things Go Wrong
|
||||
</h2>
|
||||
{frictionPoints.intro && (
|
||||
{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>
|
||||
{frictionPoints && (
|
||||
<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>
|
||||
{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>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Facets Data Charts */}
|
||||
<div
|
||||
|
|
@ -566,81 +612,106 @@ export function Improvements({
|
|||
const { improvements } = qualitative;
|
||||
if (!improvements) return null;
|
||||
|
||||
const hasFeatureSuggestions =
|
||||
(Array.isArray(improvements.Qwen_md_additions) &&
|
||||
improvements.Qwen_md_additions.length > 0) ||
|
||||
(Array.isArray(improvements.features_to_try) &&
|
||||
improvements.features_to_try.length > 0);
|
||||
const hasUsagePatterns =
|
||||
Array.isArray(improvements.usage_patterns) &&
|
||||
improvements.usage_patterns.length > 0;
|
||||
|
||||
if (!hasFeatureSuggestions && !hasUsagePatterns) return null;
|
||||
|
||||
return (
|
||||
<>
|
||||
<h2
|
||||
id="section-features"
|
||||
className="text-xl font-semibold text-slate-900 mt-8 mb-4"
|
||||
>
|
||||
Existing Qwen Code Features to Try
|
||||
</h2>
|
||||
{hasFeatureSuggestions && (
|
||||
<>
|
||||
<h2
|
||||
id="section-features"
|
||||
className="text-xl font-semibold text-slate-900 mt-8 mb-4"
|
||||
>
|
||||
Existing Qwen Code 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} />
|
||||
)}
|
||||
{/* 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'll set it up for you.
|
||||
</p>
|
||||
<p className="text-xs text-slate-500 mb-3">
|
||||
Just copy this into Qwen Code and it'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} />
|
||||
{/* 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>
|
||||
</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'll walk you through it.
|
||||
</p>
|
||||
{hasUsagePatterns && (
|
||||
<>
|
||||
<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'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 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>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue