@ai-lighthouse/cli 1.0.0 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +2573 -12
- package/package.json +10 -4
- package/.ai-lighthouse/audit_example.com_2025-12-15T12-10-43.json +0 -183
- package/.ai-lighthouse/audit_fayeed.dev_2026-01-07T19-32-28.html +0 -743
- package/.ai-lighthouse/audit_fayeed.dev_2026-01-07T19-33-02.html +0 -757
- package/.ai-lighthouse/audit_github.com_2025-12-15T11-53-21.json +0 -168
- package/.ai-lighthouse/audit_github.com_2025-12-15T12-04-06.json +0 -168
- package/.ai-lighthouse/audit_github.com_2025-12-15T12-05-10.json +0 -168
- package/.ai-lighthouse/audit_github.com_2025-12-15T12-09-45.json +0 -168
- package/.ai-lighthouse/audit_github.com_2025-12-15T12-11-07.json +0 -168
- package/.ai-lighthouse/audit_github.com_2025-12-15T12-13-28.json +0 -168
- package/.ai-lighthouse/audit_github.com_2025-12-15T12-14-59.json +0 -205
- package/.ai-lighthouse/audit_github.com_2025-12-15T12-18-07.json +0 -205
- package/.ai-lighthouse/audit_github.com_2025-12-15T12-18-44.json +0 -205
- package/.ai-lighthouse/audit_github.com_2025-12-15T12-21-38.json +0 -205
- package/.ai-lighthouse/audit_github.com_2025-12-15T12-22-21.json +0 -205
- package/.ai-lighthouse/audit_github.com_2025-12-15T12-22-46.json +0 -205
- package/.ai-lighthouse/audit_github.com_2025-12-15T12-23-18.json +0 -205
- package/.ai-lighthouse/audit_github.com_2025-12-15T12-24-43.json +0 -205
- package/.ai-lighthouse/audit_github.com_2025-12-17T12-15-08.json +0 -168
- package/.ai-lighthouse/audit_github.com_2025-12-17T12-15-57.json +0 -168
- package/.ai-lighthouse/audit_github.com_2025-12-17T12-17-11.json +0 -168
- package/.ai-lighthouse/audit_github.com_2025-12-17T12-22-17.json +0 -168
- package/.ai-lighthouse/audit_github.com_2025-12-17T12-22-42.json +0 -168
- package/.ai-lighthouse/audit_github.com_2025-12-17T12-23-56.json +0 -168
- package/.ai-lighthouse/audit_github.com_2025-12-17T12-25-24.json +0 -168
- package/.ai-lighthouse/audit_github.com_2025-12-17T12-25-40.json +0 -168
- package/.ai-lighthouse/audit_github.com_2025-12-17T12-27-02.json +0 -168
- package/.ai-lighthouse/audit_github.com_2025-12-17T12-27-20.json +0 -168
- package/.ai-lighthouse/audit_github.com_2025-12-17T12-29-56.json +0 -168
- package/.ai-lighthouse/audit_github.com_2025-12-17T12-32-27.json +0 -168
- package/.ai-lighthouse/audit_github.com_2025-12-17T12-33-00.json +0 -168
- package/.ai-lighthouse/audit_github.com_2025-12-17T12-34-49.json +0 -168
- package/.ai-lighthouse/audit_stripe.com_2025-12-15T12-11-31.json +0 -168
- package/.ai-lighthouse/audit_stripe.com_2025-12-15T12-11-45.json +0 -168
- package/.ai-lighthouse/audit_tailwindcss.com_2025-12-15T12-12-01.json +0 -169
- package/.ai-lighthouse/crawl_example.com_2025-12-15T12-03-08.json +0 -24
- package/.ai-lighthouse/crawl_example.com_2025-12-15T12-03-23.json +0 -24
- package/.ai-lighthouse/crawl_github.com_2025-12-15T11-41-34.json +0 -21
- package/.ai-lighthouse/crawl_github.com_2025-12-15T11-42-09.json +0 -21
- package/.ai-lighthouse/crawl_github.com_2025-12-15T11-42-45.json +0 -21
- package/.ai-lighthouse/crawl_github.com_2025-12-15T11-43-02.json +0 -21
- package/.ai-lighthouse/crawl_github.com_2025-12-15T11-43-26.json +0 -21
- package/.ai-lighthouse/crawl_github.com_2025-12-15T11-47-46.json +0 -906
- package/.ai-lighthouse/crawl_github.com_2025-12-15T11-50-27.json +0 -906
- package/.ai-lighthouse/crawl_github.com_2025-12-15T11-52-59.json +0 -906
- package/.ai-lighthouse/crawl_github.com_2025-12-15T12-03-33.json +0 -28
- package/CLI_UI_README.md +0 -211
- package/EXAMPLES.md +0 -87
- package/IMPLEMENTATION.md +0 -215
- package/USAGE.md +0 -264
- package/WIZARD_GUIDE.md +0 -340
- package/bin/cli.js +0 -2
- package/dist/commands/audit-interactive.d.ts +0 -2
- package/dist/commands/audit-interactive.js +0 -106
- package/dist/commands/audit-wizard.d.ts +0 -2
- package/dist/commands/audit-wizard.js +0 -110
- package/dist/commands/audit.d.ts +0 -2
- package/dist/commands/audit.js +0 -940
- package/dist/commands/crawl.d.ts +0 -2
- package/dist/commands/crawl.js +0 -267
- package/dist/commands/report.d.ts +0 -2
- package/dist/commands/report.js +0 -304
- package/dist/index.d.ts +0 -1
- package/dist/ui/AuditReportUI.d.ts +0 -10
- package/dist/ui/AuditReportUI.js +0 -76
- package/dist/ui/SetupWizard.d.ts +0 -18
- package/dist/ui/SetupWizard.js +0 -179
- package/dist/ui/components/AIUnderstandingSection.d.ts +0 -6
- package/dist/ui/components/AIUnderstandingSection.js +0 -87
- package/dist/ui/components/HallucinationSection.d.ts +0 -6
- package/dist/ui/components/HallucinationSection.js +0 -84
- package/dist/ui/components/IssuesSection.d.ts +0 -6
- package/dist/ui/components/IssuesSection.js +0 -84
- package/dist/ui/components/MessageAlignmentSection.d.ts +0 -6
- package/dist/ui/components/MessageAlignmentSection.js +0 -108
- package/dist/ui/components/OverviewSection.d.ts +0 -6
- package/dist/ui/components/OverviewSection.js +0 -107
- package/dist/ui/components/ScoreDisplay.d.ts +0 -8
- package/dist/ui/components/ScoreDisplay.js +0 -41
- package/dist/ui/components/TechnicalSection.d.ts +0 -7
- package/dist/ui/components/TechnicalSection.js +0 -110
- package/dist/utils/comprehensive-formatter.d.ts +0 -5
- package/dist/utils/comprehensive-formatter.js +0 -370
- package/src/commands/audit-interactive.ts +0 -149
- package/src/commands/audit-wizard.ts +0 -137
- package/src/commands/audit.ts +0 -1012
- package/src/commands/crawl.ts +0 -307
- package/src/commands/report.ts +0 -321
- package/src/index.ts +0 -22
- package/src/ui/AuditReportUI.tsx +0 -151
- package/src/ui/SetupWizard.tsx +0 -294
- package/src/ui/components/AIUnderstandingSection.tsx +0 -183
- package/src/ui/components/HallucinationSection.tsx +0 -172
- package/src/ui/components/IssuesSection.tsx +0 -140
- package/src/ui/components/MessageAlignmentSection.tsx +0 -203
- package/src/ui/components/OverviewSection.tsx +0 -157
- package/src/ui/components/ScoreDisplay.tsx +0 -58
- package/src/ui/components/TechnicalSection.tsx +0 -200
- package/src/utils/comprehensive-formatter.ts +0 -455
- package/test.sh +0 -31
- package/tsconfig.json +0 -25
package/dist/index.js
CHANGED
|
@@ -1,15 +1,2576 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { Command } from "commander";
|
|
5
|
+
|
|
6
|
+
// src/commands/audit.ts
|
|
7
|
+
import chalk2 from "chalk";
|
|
8
|
+
import ora from "ora";
|
|
9
|
+
import { analyzeUrlWithRules } from "@ai-lighthouse/scanner";
|
|
10
|
+
import { calculateAIReadiness, formatAIReadinessReport } from "@ai-lighthouse/scanner";
|
|
11
|
+
import { exportAuditReport, generateScoringSummary } from "@ai-lighthouse/scanner";
|
|
12
|
+
import { writeFile, mkdir } from "fs/promises";
|
|
13
|
+
import { join, resolve } from "path";
|
|
14
|
+
import { existsSync } from "fs";
|
|
15
|
+
import html_to_pdf from "html-pdf-node";
|
|
16
|
+
|
|
17
|
+
// src/utils/comprehensive-formatter.ts
|
|
18
|
+
import chalk from "chalk";
|
|
19
|
+
function formatComprehensiveReport(result, aiReadiness) {
|
|
20
|
+
const sections = [];
|
|
21
|
+
if (result.llm) {
|
|
22
|
+
sections.push(formatLLMSection(result.llm));
|
|
23
|
+
}
|
|
24
|
+
if (result.chunking) {
|
|
25
|
+
sections.push(formatChunkingSection(result.chunking));
|
|
26
|
+
}
|
|
27
|
+
if (result.extractability) {
|
|
28
|
+
sections.push(formatExtractabilitySection(result.extractability));
|
|
29
|
+
}
|
|
30
|
+
if (result.hallucinationReport) {
|
|
31
|
+
sections.push(formatHallucinationSection(result.hallucinationReport));
|
|
32
|
+
}
|
|
33
|
+
if (result.mirrorReport) {
|
|
34
|
+
sections.push(formatMirrorReportSection(result.mirrorReport));
|
|
35
|
+
}
|
|
36
|
+
if (aiReadiness?.dimensions) {
|
|
37
|
+
sections.push(formatDimensionsSection(aiReadiness.dimensions));
|
|
38
|
+
}
|
|
39
|
+
if (aiReadiness?.quickWins && aiReadiness.quickWins.length > 0) {
|
|
40
|
+
sections.push(formatQuickWinsSection(aiReadiness.quickWins));
|
|
41
|
+
}
|
|
42
|
+
return sections.join("\n\n");
|
|
43
|
+
}
|
|
44
|
+
function formatLLMSection(llm) {
|
|
45
|
+
const lines = [];
|
|
46
|
+
lines.push(chalk.bold.blue("\u{1F4DD} AI Understanding Analysis"));
|
|
47
|
+
lines.push("\u2500".repeat(70));
|
|
48
|
+
if (llm.summary) {
|
|
49
|
+
lines.push(chalk.bold("Summary:"));
|
|
50
|
+
lines.push(` ${llm.summary}`);
|
|
51
|
+
lines.push("");
|
|
52
|
+
}
|
|
53
|
+
if (llm.pageType) {
|
|
54
|
+
lines.push(chalk.bold("Inferred Page Type:"));
|
|
55
|
+
lines.push(` ${chalk.magenta.bold(llm.pageType)}`);
|
|
56
|
+
if (llm.pageTypeInsights && llm.pageTypeInsights.length > 0) {
|
|
57
|
+
lines.push("");
|
|
58
|
+
lines.push(chalk.bold("\u{1F4A1} AI-Generated Insights:"));
|
|
59
|
+
llm.pageTypeInsights.forEach((insight) => {
|
|
60
|
+
lines.push(` ${chalk.cyan("\u2022")} ${insight}`);
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
lines.push("");
|
|
64
|
+
}
|
|
65
|
+
if (llm.keyTopics && llm.keyTopics.length > 0) {
|
|
66
|
+
lines.push(chalk.bold("Key Topics:"));
|
|
67
|
+
lines.push(` ${llm.keyTopics.map((t) => chalk.blue(t)).join(", ")}`);
|
|
68
|
+
lines.push("");
|
|
69
|
+
}
|
|
70
|
+
const metadata = [];
|
|
71
|
+
if (llm.readingLevel) {
|
|
72
|
+
metadata.push(`Reading Level: ${llm.readingLevel.description}`);
|
|
73
|
+
}
|
|
74
|
+
if (llm.sentiment) {
|
|
75
|
+
metadata.push(`Sentiment: ${llm.sentiment}`);
|
|
76
|
+
}
|
|
77
|
+
if (llm.technicalDepth) {
|
|
78
|
+
metadata.push(`Technical Depth: ${llm.technicalDepth}`);
|
|
79
|
+
}
|
|
80
|
+
if (metadata.length > 0) {
|
|
81
|
+
lines.push(chalk.bold("Metadata:"));
|
|
82
|
+
metadata.forEach((m) => lines.push(` ${m}`));
|
|
83
|
+
lines.push("");
|
|
84
|
+
}
|
|
85
|
+
if (llm.topEntities && llm.topEntities.length > 0) {
|
|
86
|
+
lines.push(chalk.bold("\u{1F50D} Key Entities:"));
|
|
87
|
+
llm.topEntities.slice(0, 5).forEach((entity) => {
|
|
88
|
+
const relevance = entity.relevance ? ` - ${Math.round(entity.relevance * 100)}% relevance` : "";
|
|
89
|
+
lines.push(` ${chalk.cyan("\u2022")} ${chalk.bold(entity.name)} ${chalk.dim(`(${entity.type})${relevance}`)}`);
|
|
90
|
+
});
|
|
91
|
+
lines.push("");
|
|
92
|
+
}
|
|
93
|
+
if (llm.questions && llm.questions.length > 0) {
|
|
94
|
+
lines.push(chalk.bold("\u2753 Questions AI Can Answer:"));
|
|
95
|
+
llm.questions.slice(0, 5).forEach((q, idx) => {
|
|
96
|
+
const difficulty = chalk.dim(`[${q.difficulty.toUpperCase()}]`);
|
|
97
|
+
lines.push(` ${idx + 1}. ${difficulty} ${q.question}`);
|
|
98
|
+
});
|
|
99
|
+
lines.push("");
|
|
100
|
+
}
|
|
101
|
+
if (llm.suggestedFAQ && llm.suggestedFAQ.length > 0) {
|
|
102
|
+
lines.push(chalk.bold("\u{1F4A1} Suggested FAQs:"));
|
|
103
|
+
llm.suggestedFAQ.filter((f) => f.importance === "high").slice(0, 3).forEach((faq, idx) => {
|
|
104
|
+
lines.push(` ${idx + 1}. Q: ${chalk.yellow(faq.question)}`);
|
|
105
|
+
lines.push(` A: ${chalk.dim(faq.suggestedAnswer)}`);
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
return lines.join("\n");
|
|
109
|
+
}
|
|
110
|
+
function formatChunkingSection(chunking) {
|
|
111
|
+
const lines = [];
|
|
112
|
+
lines.push(chalk.bold.green("\u{1F4C4} Content Chunking Analysis"));
|
|
113
|
+
lines.push("\u2500".repeat(70));
|
|
114
|
+
const grid = [
|
|
115
|
+
["Strategy", chunking.chunkingStrategy],
|
|
116
|
+
["Total Chunks", chunking.totalChunks.toString()],
|
|
117
|
+
["Avg Tokens/Chunk", chunking.averageTokensPerChunk.toString()],
|
|
118
|
+
["Avg Noise Ratio", `${(chunking.averageNoiseRatio * 100).toFixed(1)}%`]
|
|
119
|
+
];
|
|
120
|
+
grid.forEach(([label, value]) => {
|
|
121
|
+
lines.push(` ${chalk.bold(label + ":").padEnd(25)} ${chalk.cyan(value)}`);
|
|
122
|
+
});
|
|
123
|
+
if (chunking.chunkingStrategy === "heading-based") {
|
|
124
|
+
lines.push("");
|
|
125
|
+
lines.push(chalk.green(" \u2713 Heading-based chunking is ideal for AI comprehension"));
|
|
126
|
+
} else if (chunking.chunkingStrategy === "paragraph-based") {
|
|
127
|
+
lines.push("");
|
|
128
|
+
lines.push(chalk.yellow(" \u26A0 Consider adding headings for better semantic structure"));
|
|
129
|
+
}
|
|
130
|
+
if (chunking.chunks && chunking.chunks.length > 0) {
|
|
131
|
+
lines.push("");
|
|
132
|
+
lines.push(chalk.bold("Chunk Distribution:"));
|
|
133
|
+
const tokenCounts = chunking.chunks.map((c) => c.tokenCount);
|
|
134
|
+
const min = Math.min(...tokenCounts);
|
|
135
|
+
const max = Math.max(...tokenCounts);
|
|
136
|
+
const avg = tokenCounts.reduce((a, b) => a + b, 0) / tokenCounts.length;
|
|
137
|
+
lines.push(` Min Tokens: ${min}, Max Tokens: ${max}, Avg: ${avg.toFixed(0)}`);
|
|
138
|
+
const noiseRatios = chunking.chunks.map((c) => c.noiseRatio);
|
|
139
|
+
const avgNoise = noiseRatios.reduce((a, b) => a + b, 0) / noiseRatios.length;
|
|
140
|
+
lines.push(` Avg Noise per Chunk: ${(avgNoise * 100).toFixed(1)}%`);
|
|
141
|
+
}
|
|
142
|
+
return lines.join("\n");
|
|
143
|
+
}
|
|
144
|
+
function formatExtractabilitySection(extractability) {
|
|
145
|
+
const lines = [];
|
|
146
|
+
lines.push(chalk.bold.yellow("\u{1F504} Extractability Analysis"));
|
|
147
|
+
lines.push("\u2500".repeat(70));
|
|
148
|
+
const grid = [
|
|
149
|
+
["Overall Score", `${extractability.score.extractabilityScore}/100`],
|
|
150
|
+
["Server-Rendered", `${extractability.score.serverRenderedPercent}%`]
|
|
151
|
+
];
|
|
152
|
+
grid.forEach(([label, value]) => {
|
|
153
|
+
lines.push(` ${chalk.bold(label + ":").padEnd(25)} ${chalk.cyan(value)}`);
|
|
154
|
+
});
|
|
155
|
+
lines.push("");
|
|
156
|
+
lines.push(chalk.bold("Content Type Extractability:"));
|
|
157
|
+
Object.entries(extractability.contentTypes).forEach(([type, data]) => {
|
|
158
|
+
const percentage = data.percentage;
|
|
159
|
+
const color = percentage >= 80 ? chalk.green : percentage >= 50 ? chalk.yellow : chalk.red;
|
|
160
|
+
lines.push(` ${chalk.bold(type.charAt(0).toUpperCase() + type.slice(1) + ":").padEnd(15)} ${color(`${percentage}%`)} (${data.extractable}/${data.total})`);
|
|
161
|
+
});
|
|
162
|
+
const overallScore = extractability.score.extractabilityScore;
|
|
163
|
+
if (overallScore >= 80) {
|
|
164
|
+
lines.push("");
|
|
165
|
+
lines.push(chalk.green(" \u2713 Good extractability - AI can easily read your content"));
|
|
166
|
+
} else if (overallScore < 50) {
|
|
167
|
+
lines.push("");
|
|
168
|
+
lines.push(chalk.red(" \u26A0 Low extractability - Consider server-side rendering"));
|
|
169
|
+
}
|
|
170
|
+
return lines.join("\n");
|
|
171
|
+
}
|
|
172
|
+
function formatHallucinationSection(report) {
|
|
173
|
+
const lines = [];
|
|
174
|
+
lines.push(chalk.bold.red("\u26A0\uFE0F Hallucination Risk Assessment"));
|
|
175
|
+
lines.push("\u2500".repeat(70));
|
|
176
|
+
const riskScore = report.hallucinationRiskScore;
|
|
177
|
+
const riskColor = riskScore >= 70 ? chalk.red : riskScore >= 40 ? chalk.yellow : chalk.green;
|
|
178
|
+
lines.push(` ${chalk.bold("Risk Score:")} ${riskColor.bold(`${riskScore}/100`)}`);
|
|
179
|
+
if (report.factCheckSummary) {
|
|
180
|
+
lines.push("");
|
|
181
|
+
lines.push(chalk.bold("Fact Check Summary:"));
|
|
182
|
+
const summary = report.factCheckSummary;
|
|
183
|
+
lines.push(` Total Facts: ${chalk.cyan(summary.totalFacts)}`);
|
|
184
|
+
lines.push(` Verified: ${chalk.green(summary.verifiedFacts)}`);
|
|
185
|
+
lines.push(` Unverified: ${chalk.yellow(summary.unverifiedFacts)}`);
|
|
186
|
+
lines.push(` Contradictions: ${chalk.red(summary.contradictions)}`);
|
|
187
|
+
if (summary.ambiguities !== void 0) {
|
|
188
|
+
lines.push(` Ambiguities: ${chalk.yellow(summary.ambiguities)}`);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
if (report.factCheckSummary && report.factCheckSummary.unverifiedFacts > 0) {
|
|
192
|
+
lines.push("");
|
|
193
|
+
lines.push(chalk.yellow("\u{1F4A1} Tip: Add citations and links to verify claims and reduce AI hallucination risk"));
|
|
194
|
+
}
|
|
195
|
+
if (report.triggers && report.triggers.length > 0) {
|
|
196
|
+
const highSeverityTriggers = report.triggers.filter(
|
|
197
|
+
(t) => t.severity === "high" || t.severity === "critical"
|
|
198
|
+
);
|
|
199
|
+
if (highSeverityTriggers.length > 0) {
|
|
200
|
+
lines.push("");
|
|
201
|
+
lines.push(chalk.bold("\u{1F6A8} High-Risk Triggers:"));
|
|
202
|
+
highSeverityTriggers.slice(0, 5).forEach((trigger, idx) => {
|
|
203
|
+
lines.push(` ${idx + 1}. ${chalk.red(`[${trigger.severity.toUpperCase()}]`)} ${trigger.type}`);
|
|
204
|
+
lines.push(` ${chalk.dim(trigger.description)}`);
|
|
205
|
+
if (trigger.confidence) {
|
|
206
|
+
lines.push(` ${chalk.dim(`Confidence: ${Math.round(trigger.confidence * 100)}%`)}`);
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
if (report.recommendations && report.recommendations.length > 0) {
|
|
212
|
+
lines.push("");
|
|
213
|
+
lines.push(chalk.bold("\u{1F4A1} Recommendations:"));
|
|
214
|
+
report.recommendations.slice(0, 3).forEach((rec, idx) => {
|
|
215
|
+
lines.push(` ${idx + 1}. ${rec}`);
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
return lines.join("\n");
|
|
219
|
+
}
|
|
220
|
+
function formatMirrorReportSection(report) {
|
|
221
|
+
const lines = [];
|
|
222
|
+
lines.push(chalk.bold.magenta("\u{1F50D} AI Misunderstanding Check"));
|
|
223
|
+
lines.push("\u2500".repeat(70));
|
|
224
|
+
const alignmentColor = report.summary.alignmentScore >= 80 ? chalk.green : report.summary.alignmentScore >= 60 ? chalk.yellow : chalk.red;
|
|
225
|
+
const clarityColor = report.summary.clarityScore >= 80 ? chalk.green : report.summary.clarityScore >= 60 ? chalk.yellow : chalk.red;
|
|
226
|
+
lines.push(` ${chalk.bold("Alignment Score:").padEnd(25)} ${alignmentColor.bold(`${report.summary.alignmentScore}/100`)}`);
|
|
227
|
+
lines.push(` ${chalk.bold("Clarity Score:").padEnd(25)} ${clarityColor.bold(`${report.summary.clarityScore}/100`)}`);
|
|
228
|
+
lines.push(` ${chalk.bold("Critical Issues:").padEnd(25)} ${chalk.red(report.summary.critical)}`);
|
|
229
|
+
lines.push(` ${chalk.bold("Major Issues:").padEnd(25)} ${chalk.yellow(report.summary.major)}`);
|
|
230
|
+
if (report.llmInterpretation) {
|
|
231
|
+
lines.push("");
|
|
232
|
+
lines.push(chalk.bold.blue("\u{1F916} What AI Actually Understood"));
|
|
233
|
+
lines.push(chalk.dim(` (${Math.round(report.llmInterpretation.confidence * 100)}% confident)`));
|
|
234
|
+
if (report.llmInterpretation.productName) {
|
|
235
|
+
lines.push(` ${chalk.bold("Product:")} ${report.llmInterpretation.productName}`);
|
|
236
|
+
}
|
|
237
|
+
if (report.llmInterpretation.purpose) {
|
|
238
|
+
lines.push(` ${chalk.bold("Purpose:")} ${report.llmInterpretation.purpose}`);
|
|
239
|
+
}
|
|
240
|
+
if (report.llmInterpretation.valueProposition) {
|
|
241
|
+
lines.push(` ${chalk.bold.magenta("\u{1F48E} Value:")} ${report.llmInterpretation.valueProposition}`);
|
|
242
|
+
}
|
|
243
|
+
if (report.llmInterpretation.keyBenefits && report.llmInterpretation.keyBenefits.length > 0) {
|
|
244
|
+
lines.push(` ${chalk.bold("Benefits:")}`);
|
|
245
|
+
report.llmInterpretation.keyBenefits.forEach((benefit) => {
|
|
246
|
+
lines.push(` \u2022 ${benefit}`);
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
if (report.llmInterpretation.keyFeatures && report.llmInterpretation.keyFeatures.length > 0) {
|
|
250
|
+
lines.push(` ${chalk.bold("Features:")}`);
|
|
251
|
+
report.llmInterpretation.keyFeatures.slice(0, 3).forEach((feature) => {
|
|
252
|
+
lines.push(` \u2022 ${feature}`);
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
if (report.llmInterpretation.targetAudience) {
|
|
256
|
+
lines.push(` ${chalk.bold("Audience:")} ${report.llmInterpretation.targetAudience}`);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
if (report.mismatches && report.mismatches.length > 0) {
|
|
260
|
+
const priorityMismatches = report.mismatches.filter(
|
|
261
|
+
(m) => m.severity === "critical" || m.severity === "major"
|
|
262
|
+
);
|
|
263
|
+
if (priorityMismatches.length > 0) {
|
|
264
|
+
lines.push("");
|
|
265
|
+
lines.push(chalk.bold("Priority Mismatches:"));
|
|
266
|
+
priorityMismatches.slice(0, 5).forEach((mismatch, idx) => {
|
|
267
|
+
const icon = mismatch.severity === "critical" ? "\u{1F534}" : "\u{1F7E1}";
|
|
268
|
+
lines.push(` ${icon} ${idx + 1}. ${chalk.bold(mismatch.field)}`);
|
|
269
|
+
lines.push(` ${chalk.dim(mismatch.description)}`);
|
|
270
|
+
lines.push(` ${chalk.cyan("\u2192")} ${mismatch.recommendation}`);
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
if (report.recommendations && report.recommendations.length > 0) {
|
|
275
|
+
lines.push("");
|
|
276
|
+
lines.push(chalk.bold("\u{1F4A1} Top Recommendations:"));
|
|
277
|
+
report.recommendations.slice(0, 3).forEach((rec, idx) => {
|
|
278
|
+
lines.push(` ${idx + 1}. ${rec}`);
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
return lines.join("\n");
|
|
282
|
+
}
|
|
283
|
+
function formatDimensionsSection(dimensions) {
|
|
284
|
+
const lines = [];
|
|
285
|
+
lines.push(chalk.bold.cyan("\u{1F3AF} Dimension Analysis"));
|
|
286
|
+
lines.push("\u2500".repeat(70));
|
|
287
|
+
const dimensionDescriptions = {
|
|
288
|
+
technical: "\u2699\uFE0F Technical",
|
|
289
|
+
contentQuality: "\u{1F4DD} Content Quality",
|
|
290
|
+
crawlability: "\u{1F577}\uFE0F Crawlability",
|
|
291
|
+
discoverability: "\u{1F50D} Discoverability",
|
|
292
|
+
knowledge: "\u{1F9E0} Knowledge",
|
|
293
|
+
extractability: "\u{1F504} Extractability",
|
|
294
|
+
comprehensibility: "\u{1F4A1} Comprehensibility",
|
|
295
|
+
trustworthiness: "\u2705 Trustworthiness",
|
|
296
|
+
accessibility: "\u267F Accessibility"
|
|
297
|
+
};
|
|
298
|
+
Object.entries(dimensions).forEach(([key, dim]) => {
|
|
299
|
+
const name = dimensionDescriptions[key] || key;
|
|
300
|
+
const scoreColor = dim.score >= 90 ? chalk.green : dim.score >= 75 ? chalk.yellow : dim.score >= 60 ? chalk.hex("#FFA500") : chalk.red;
|
|
301
|
+
lines.push("");
|
|
302
|
+
lines.push(`${name}: ${scoreColor.bold(`${Math.round(dim.score)}/100`)} ${chalk.dim(`(${dim.status})`)}`);
|
|
303
|
+
if (dim.strengths && dim.strengths.length > 0) {
|
|
304
|
+
lines.push(` ${chalk.green("Strengths:")} ${dim.strengths.join(", ")}`);
|
|
305
|
+
}
|
|
306
|
+
if (dim.weaknesses && dim.weaknesses.length > 0) {
|
|
307
|
+
lines.push(` ${chalk.yellow("Weaknesses:")} ${dim.weaknesses.join(", ")}`);
|
|
308
|
+
}
|
|
309
|
+
if (dim.recommendation) {
|
|
310
|
+
lines.push(` ${chalk.cyan("\u2192")} ${dim.recommendation}`);
|
|
311
|
+
}
|
|
312
|
+
});
|
|
313
|
+
return lines.join("\n");
|
|
314
|
+
}
|
|
315
|
+
function formatQuickWinsSection(quickWins) {
|
|
316
|
+
const lines = [];
|
|
317
|
+
lines.push(chalk.bold.yellow("\u26A1 Quick Wins (High Impact, Low Effort)"));
|
|
318
|
+
lines.push("\u2500".repeat(70));
|
|
319
|
+
quickWins.slice(0, 5).forEach((win, idx) => {
|
|
320
|
+
lines.push("");
|
|
321
|
+
lines.push(`${chalk.bold(`${idx + 1}.`)} ${chalk.yellow(win.issue)}`);
|
|
322
|
+
lines.push(` ${chalk.dim(`Impact: ${win.impact} \xB7 Effort: ${win.effort}`)}`);
|
|
323
|
+
lines.push(` ${chalk.cyan("\u2192")} ${win.fix}`);
|
|
324
|
+
});
|
|
325
|
+
return lines.join("\n");
|
|
326
|
+
}
|
|
327
|
+
function formatDetailedIssues(issues) {
|
|
328
|
+
const lines = [];
|
|
329
|
+
lines.push(chalk.bold("\u26A0\uFE0F All Issues"));
|
|
330
|
+
lines.push("\u2500".repeat(70));
|
|
331
|
+
const grouped = {
|
|
332
|
+
critical: issues.filter((i) => i.severity === "critical"),
|
|
333
|
+
high: issues.filter((i) => i.severity === "high"),
|
|
334
|
+
medium: issues.filter((i) => i.severity === "medium"),
|
|
335
|
+
low: issues.filter((i) => i.severity === "low")
|
|
336
|
+
};
|
|
337
|
+
lines.push("");
|
|
338
|
+
lines.push(chalk.bold("Issue Count by Severity:"));
|
|
339
|
+
lines.push(` Critical: ${chalk.red.bold(grouped.critical.length)}`);
|
|
340
|
+
lines.push(` High: ${chalk.yellow.bold(grouped.high.length)}`);
|
|
341
|
+
lines.push(` Medium: ${chalk.blue.bold(grouped.medium.length)}`);
|
|
342
|
+
lines.push(` Low: ${chalk.dim(grouped.low.length)}`);
|
|
343
|
+
for (const [severity, severityIssues] of Object.entries(grouped)) {
|
|
344
|
+
if (severityIssues.length === 0) continue;
|
|
345
|
+
lines.push("");
|
|
346
|
+
lines.push(chalk.bold(`${severity.toUpperCase()} Issues:`));
|
|
347
|
+
severityIssues.forEach((issue, idx) => {
|
|
348
|
+
const icon = severity === "critical" ? "\u{1F534}" : severity === "high" ? "\u{1F7E0}" : severity === "medium" ? "\u{1F7E1}" : "\u{1F535}";
|
|
349
|
+
lines.push("");
|
|
350
|
+
lines.push(`${icon} ${idx + 1}. ${chalk.bold(issue.message || issue.title)}`);
|
|
351
|
+
lines.push(` ${chalk.dim(`Category: ${issue.category} \xB7 Impact: ${issue.impact}`)}`);
|
|
352
|
+
if (issue.evidence) {
|
|
353
|
+
const evidenceText = typeof issue.evidence === "string" ? issue.evidence : issue.evidence.join(", ");
|
|
354
|
+
lines.push(` ${chalk.dim(evidenceText.substring(0, 100))}${evidenceText.length > 100 ? "..." : ""}`);
|
|
355
|
+
}
|
|
356
|
+
if (issue.element) {
|
|
357
|
+
lines.push(` ${chalk.dim(issue.element.substring(0, 100))}${issue.element.length > 100 ? "..." : ""}`);
|
|
358
|
+
}
|
|
359
|
+
lines.push(` ${chalk.cyan("\u{1F4A1} Fix:")} ${issue.suggested_fix || issue.remediation}`);
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
return lines.join("\n");
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// src/commands/audit.ts
|
|
366
|
+
import { render } from "ink";
|
|
367
|
+
import React10 from "react";
|
|
368
|
+
|
|
369
|
+
// src/ui/AuditReportUI.tsx
|
|
370
|
+
import React8, { useState as useState2 } from "react";
|
|
371
|
+
import { Box as Box8, Text as Text8, useInput } from "ink";
|
|
372
|
+
import Spinner from "ink-spinner";
|
|
373
|
+
|
|
374
|
+
// src/ui/components/ScoreDisplay.tsx
|
|
375
|
+
import React from "react";
|
|
376
|
+
import { Box, Text } from "ink";
|
|
377
|
+
import Gradient from "ink-gradient";
|
|
378
|
+
import BigText from "ink-big-text";
|
|
379
|
+
var ScoreDisplay = ({ score, grade, url }) => {
|
|
380
|
+
const getGradeColor = (grade2) => {
|
|
381
|
+
if (grade2.startsWith("A")) return "green";
|
|
382
|
+
if (grade2.startsWith("B")) return "blue";
|
|
383
|
+
if (grade2.startsWith("C")) return "yellow";
|
|
384
|
+
return "red";
|
|
385
|
+
};
|
|
386
|
+
const getScoreMessage = (score2) => {
|
|
387
|
+
if (score2 >= 90) return { status: "Excellent", message: "Your site is AI-ready!", color: "green" };
|
|
388
|
+
if (score2 >= 80) return { status: "Good", message: "Your site works well with AI", color: "blue" };
|
|
389
|
+
if (score2 >= 70) return { status: "Fair", message: "Room for improvement", color: "yellow" };
|
|
390
|
+
if (score2 >= 60) return { status: "Poor", message: "Needs significant work", color: "yellow" };
|
|
391
|
+
return { status: "Critical", message: "Major issues detected", color: "red" };
|
|
392
|
+
};
|
|
393
|
+
const { status, message, color } = getScoreMessage(score);
|
|
394
|
+
return /* @__PURE__ */ React.createElement(Box, { flexDirection: "column", paddingY: 1 }, /* @__PURE__ */ React.createElement(Box, { marginBottom: 1 }, /* @__PURE__ */ React.createElement(Gradient, { name: "rainbow" }, /* @__PURE__ */ React.createElement(BigText, { text: `${score}`, font: "block" }))), /* @__PURE__ */ React.createElement(Box, { marginBottom: 1 }, /* @__PURE__ */ React.createElement(Text, { bold: true, color: getGradeColor(grade) }, "Grade: ", grade, " \u2022 ", status)), /* @__PURE__ */ React.createElement(Box, { marginBottom: 1 }, /* @__PURE__ */ React.createElement(Text, { dimColor: true }, url)), /* @__PURE__ */ React.createElement(
|
|
395
|
+
Box,
|
|
396
|
+
{
|
|
397
|
+
borderStyle: "round",
|
|
398
|
+
borderColor: color,
|
|
399
|
+
paddingX: 2,
|
|
400
|
+
paddingY: 1
|
|
401
|
+
},
|
|
402
|
+
/* @__PURE__ */ React.createElement(Text, { color }, message)
|
|
403
|
+
));
|
|
404
|
+
};
|
|
405
|
+
|
|
406
|
+
// src/ui/components/OverviewSection.tsx
|
|
407
|
+
import React2 from "react";
|
|
408
|
+
import { Box as Box2, Text as Text2 } from "ink";
|
|
409
|
+
var OverviewSection = ({ aiReadiness }) => {
|
|
410
|
+
const getStatusColor = (status) => {
|
|
411
|
+
if (status === "excellent") return "green";
|
|
412
|
+
if (status === "good") return "blue";
|
|
413
|
+
if (status === "needs-work") return "yellow";
|
|
414
|
+
return "red";
|
|
415
|
+
};
|
|
416
|
+
const renderProgressBar = (score, width = 20) => {
|
|
417
|
+
const filled = Math.round(score / 100 * width);
|
|
418
|
+
const empty = width - filled;
|
|
419
|
+
const color = score >= 80 ? "green" : score >= 60 ? "yellow" : "red";
|
|
420
|
+
return /* @__PURE__ */ React2.createElement(Text2, { color }, "\u2588".repeat(filled), /* @__PURE__ */ React2.createElement(Text2, { dimColor: true }, "\u2591".repeat(empty)), ` ${Math.round(score)}%`);
|
|
421
|
+
};
|
|
422
|
+
return /* @__PURE__ */ React2.createElement(Box2, { flexDirection: "column", paddingY: 1 }, /* @__PURE__ */ React2.createElement(
|
|
423
|
+
Box2,
|
|
424
|
+
{
|
|
425
|
+
flexDirection: "column",
|
|
426
|
+
borderStyle: "round",
|
|
427
|
+
borderColor: "cyan",
|
|
428
|
+
paddingX: 2,
|
|
429
|
+
paddingY: 1,
|
|
430
|
+
marginBottom: 1
|
|
431
|
+
},
|
|
432
|
+
/* @__PURE__ */ React2.createElement(Text2, { bold: true, color: "cyan" }, "\u{1F916} AI Agent Perspective"),
|
|
433
|
+
/* @__PURE__ */ React2.createElement(Box2, { marginTop: 1, flexDirection: "column" }, /* @__PURE__ */ React2.createElement(Box2, null, /* @__PURE__ */ React2.createElement(Text2, null, aiReadiness.aiPerspective?.canUnderstand ? "\u2705" : "\u274C", " Can Understand", aiReadiness.aiPerspective?.canExtract ? "\u2705" : "\u274C", " Can Extract")), /* @__PURE__ */ React2.createElement(Box2, null, /* @__PURE__ */ React2.createElement(Text2, null, aiReadiness.aiPerspective?.canIndex ? "\u2705" : "\u274C", " Can Index", aiReadiness.aiPerspective?.canAnswer ? "\u2705" : "\u274C", " Can Answer")), /* @__PURE__ */ React2.createElement(Box2, { marginTop: 1 }, /* @__PURE__ */ React2.createElement(Text2, { dimColor: true }, "Confidence: ", Math.round((aiReadiness.aiPerspective?.confidence || 0) * 100), "%"))),
|
|
434
|
+
aiReadiness.aiPerspective?.mainBlockers && aiReadiness.aiPerspective.mainBlockers.length > 0 && /* @__PURE__ */ React2.createElement(Box2, { marginTop: 1, flexDirection: "column" }, /* @__PURE__ */ React2.createElement(Text2, { bold: true, color: "red" }, "Main Blockers:"), aiReadiness.aiPerspective.mainBlockers.map((blocker, idx) => /* @__PURE__ */ React2.createElement(Text2, { key: idx, color: "red" }, "\u2022 ", blocker)))
|
|
435
|
+
), /* @__PURE__ */ React2.createElement(Box2, { flexDirection: "column", marginTop: 1 }, /* @__PURE__ */ React2.createElement(Text2, { bold: true, underline: true }, "\u{1F3AF} Dimension Scores"), Object.entries(aiReadiness.dimensions || {}).map(([key, dim]) => /* @__PURE__ */ React2.createElement(Box2, { key, flexDirection: "column", marginTop: 1 }, /* @__PURE__ */ React2.createElement(Box2, { justifyContent: "space-between" }, /* @__PURE__ */ React2.createElement(Text2, { bold: true, color: getStatusColor(dim.status) }, getDimensionIcon(key), " ", formatDimensionName(key)), /* @__PURE__ */ React2.createElement(Text2, { color: getStatusColor(dim.status) }, Math.round(dim.score), "/100")), /* @__PURE__ */ React2.createElement(Box2, { marginTop: 0.5 }, renderProgressBar(dim.score)), dim.recommendation && /* @__PURE__ */ React2.createElement(Box2, { marginTop: 0.5 }, /* @__PURE__ */ React2.createElement(Text2, { dimColor: true }, "\u2192 ", dim.recommendation))))), aiReadiness.quickWins && aiReadiness.quickWins.length > 0 && /* @__PURE__ */ React2.createElement(
|
|
436
|
+
Box2,
|
|
437
|
+
{
|
|
438
|
+
flexDirection: "column",
|
|
439
|
+
borderStyle: "round",
|
|
440
|
+
borderColor: "yellow",
|
|
441
|
+
paddingX: 2,
|
|
442
|
+
paddingY: 1,
|
|
443
|
+
marginTop: 1
|
|
444
|
+
},
|
|
445
|
+
/* @__PURE__ */ React2.createElement(Text2, { bold: true, color: "yellow" }, "\u26A1 Quick Wins (High Impact, Low Effort)"),
|
|
446
|
+
aiReadiness.quickWins.slice(0, 5).map((win, idx) => /* @__PURE__ */ React2.createElement(Box2, { key: idx, flexDirection: "column", marginTop: 1 }, /* @__PURE__ */ React2.createElement(Text2, { bold: true }, idx + 1, ". ", win.issue), /* @__PURE__ */ React2.createElement(Text2, { dimColor: true }, "Impact: ", win.impact, " \u2022 Effort: ", win.effort), /* @__PURE__ */ React2.createElement(Text2, { color: "cyan" }, "\u2192 ", win.fix)))
|
|
447
|
+
));
|
|
448
|
+
};
|
|
449
|
+
function getDimensionIcon(key) {
|
|
450
|
+
const icons = {
|
|
451
|
+
technical: "\u2699\uFE0F",
|
|
452
|
+
contentQuality: "\u{1F4DD}",
|
|
453
|
+
crawlability: "\u{1F577}\uFE0F",
|
|
454
|
+
discoverability: "\u{1F50D}",
|
|
455
|
+
knowledge: "\u{1F9E0}",
|
|
456
|
+
extractability: "\u{1F504}",
|
|
457
|
+
comprehensibility: "\u{1F4A1}",
|
|
458
|
+
trustworthiness: "\u2705",
|
|
459
|
+
accessibility: "\u267F"
|
|
460
|
+
};
|
|
461
|
+
return icons[key] || "\u{1F4CC}";
|
|
462
|
+
}
|
|
463
|
+
function formatDimensionName(key) {
|
|
464
|
+
const names = {
|
|
465
|
+
technical: "Technical",
|
|
466
|
+
contentQuality: "Content Quality",
|
|
467
|
+
crawlability: "Crawlability",
|
|
468
|
+
discoverability: "Discoverability",
|
|
469
|
+
knowledge: "Knowledge",
|
|
470
|
+
extractability: "Extractability",
|
|
471
|
+
comprehensibility: "Comprehensibility",
|
|
472
|
+
trustworthiness: "Trustworthiness",
|
|
473
|
+
accessibility: "Accessibility"
|
|
474
|
+
};
|
|
475
|
+
return names[key] || key;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// src/ui/components/IssuesSection.tsx
|
|
479
|
+
import React3, { useState } from "react";
|
|
480
|
+
import { Box as Box3, Text as Text3 } from "ink";
|
|
481
|
+
var IssuesSection = ({ issues }) => {
|
|
482
|
+
const [selectedSeverity, setSelectedSeverity] = useState("all");
|
|
483
|
+
const [expandedIssue, setExpandedIssue] = useState(null);
|
|
484
|
+
const getSeverityColor2 = (severity) => {
|
|
485
|
+
if (severity === "critical") return "red";
|
|
486
|
+
if (severity === "high") return "magenta";
|
|
487
|
+
if (severity === "medium") return "yellow";
|
|
488
|
+
return "blue";
|
|
489
|
+
};
|
|
490
|
+
const getSeverityIcon = (severity) => {
|
|
491
|
+
if (severity === "critical") return "\u{1F534}";
|
|
492
|
+
if (severity === "high") return "\u{1F7E0}";
|
|
493
|
+
if (severity === "medium") return "\u{1F7E1}";
|
|
494
|
+
return "\u{1F535}";
|
|
495
|
+
};
|
|
496
|
+
const grouped = {
|
|
497
|
+
critical: issues.filter((i) => i.severity === "critical"),
|
|
498
|
+
high: issues.filter((i) => i.severity === "high"),
|
|
499
|
+
medium: issues.filter((i) => i.severity === "medium"),
|
|
500
|
+
low: issues.filter((i) => i.severity === "low")
|
|
501
|
+
};
|
|
502
|
+
const filteredIssues = selectedSeverity === "all" ? issues : grouped[selectedSeverity] || [];
|
|
503
|
+
return /* @__PURE__ */ React3.createElement(Box3, { flexDirection: "column", paddingY: 1 }, /* @__PURE__ */ React3.createElement(
|
|
504
|
+
Box3,
|
|
505
|
+
{
|
|
506
|
+
flexDirection: "column",
|
|
507
|
+
borderStyle: "round",
|
|
508
|
+
borderColor: "blue",
|
|
509
|
+
paddingX: 2,
|
|
510
|
+
paddingY: 1,
|
|
511
|
+
marginBottom: 1
|
|
512
|
+
},
|
|
513
|
+
/* @__PURE__ */ React3.createElement(Text3, { bold: true, color: "blue" }, "\u26A0\uFE0F Issues Summary"),
|
|
514
|
+
/* @__PURE__ */ React3.createElement(Box3, { marginTop: 1 }, /* @__PURE__ */ React3.createElement(Box3, { flexDirection: "column", width: "50%" }, /* @__PURE__ */ React3.createElement(Text3, null, /* @__PURE__ */ React3.createElement(Text3, { color: "red", bold: true }, "Critical:"), " ", grouped.critical.length), /* @__PURE__ */ React3.createElement(Text3, null, /* @__PURE__ */ React3.createElement(Text3, { color: "magenta", bold: true }, "High:"), " ", grouped.high.length)), /* @__PURE__ */ React3.createElement(Box3, { flexDirection: "column", width: "50%" }, /* @__PURE__ */ React3.createElement(Text3, null, /* @__PURE__ */ React3.createElement(Text3, { color: "yellow", bold: true }, "Medium:"), " ", grouped.medium.length), /* @__PURE__ */ React3.createElement(Text3, null, /* @__PURE__ */ React3.createElement(Text3, { color: "blue", bold: true }, "Low:"), " ", grouped.low.length)))
|
|
515
|
+
), /* @__PURE__ */ React3.createElement(Box3, { flexDirection: "column" }, /* @__PURE__ */ React3.createElement(Text3, { bold: true, underline: true }, selectedSeverity === "all" ? "All Issues" : `${selectedSeverity.toUpperCase()} Issues`, " (", filteredIssues.length, ")"), filteredIssues.map((issue, idx) => /* @__PURE__ */ React3.createElement(
|
|
516
|
+
Box3,
|
|
517
|
+
{
|
|
518
|
+
key: idx,
|
|
519
|
+
flexDirection: "column",
|
|
520
|
+
borderStyle: "round",
|
|
521
|
+
borderColor: getSeverityColor2(issue.severity),
|
|
522
|
+
paddingX: 2,
|
|
523
|
+
paddingY: 1,
|
|
524
|
+
marginTop: 1
|
|
525
|
+
},
|
|
526
|
+
/* @__PURE__ */ React3.createElement(Box3, null, /* @__PURE__ */ React3.createElement(Text3, null, getSeverityIcon(issue.severity), " ", /* @__PURE__ */ React3.createElement(Text3, { bold: true, color: getSeverityColor2(issue.severity) }, issue.message || issue.title))),
|
|
527
|
+
/* @__PURE__ */ React3.createElement(Box3, { marginTop: 0.5 }, /* @__PURE__ */ React3.createElement(Text3, { dimColor: true }, "Category: ", issue.category, " \u2022 Impact: ", issue.impact)),
|
|
528
|
+
issue.evidence && /* @__PURE__ */ React3.createElement(Box3, { marginTop: 0.5 }, /* @__PURE__ */ React3.createElement(Text3, { dimColor: true }, typeof issue.evidence === "string" ? issue.evidence.substring(0, 100) : Array.isArray(issue.evidence) ? issue.evidence.join(", ").substring(0, 100) : "", (typeof issue.evidence === "string" ? issue.evidence : issue.evidence?.join(", ") || "").length > 100 ? "..." : "")),
|
|
529
|
+
/* @__PURE__ */ React3.createElement(
|
|
530
|
+
Box3,
|
|
531
|
+
{
|
|
532
|
+
marginTop: 1,
|
|
533
|
+
borderStyle: "single",
|
|
534
|
+
borderColor: "cyan",
|
|
535
|
+
paddingX: 1,
|
|
536
|
+
paddingY: 0.5
|
|
537
|
+
},
|
|
538
|
+
/* @__PURE__ */ React3.createElement(Text3, { color: "cyan" }, "\u{1F4A1} Fix: ", issue.suggested_fix || issue.remediation)
|
|
539
|
+
)
|
|
540
|
+
))));
|
|
541
|
+
};
|
|
542
|
+
|
|
543
|
+
// src/ui/components/AIUnderstandingSection.tsx
|
|
544
|
+
import React4 from "react";
|
|
545
|
+
import { Box as Box4, Text as Text4 } from "ink";
|
|
546
|
+
var AIUnderstandingSection = ({ llm }) => {
|
|
547
|
+
if (!llm || Object.keys(llm).length === 0) {
|
|
548
|
+
return /* @__PURE__ */ React4.createElement(Box4, { flexDirection: "column", paddingY: 1 }, /* @__PURE__ */ React4.createElement(
|
|
549
|
+
Box4,
|
|
550
|
+
{
|
|
551
|
+
flexDirection: "column",
|
|
552
|
+
borderStyle: "round",
|
|
553
|
+
borderColor: "yellow",
|
|
554
|
+
paddingX: 2,
|
|
555
|
+
paddingY: 1
|
|
556
|
+
},
|
|
557
|
+
/* @__PURE__ */ React4.createElement(Text4, { bold: true, color: "yellow" }, "\u{1F4A1} Enable AI Understanding Analysis"),
|
|
558
|
+
/* @__PURE__ */ React4.createElement(Box4, { marginTop: 1, flexDirection: "column" }, /* @__PURE__ */ React4.createElement(Text4, null, "LLM analysis is not enabled. To see AI understanding insights, run:"), /* @__PURE__ */ React4.createElement(Box4, { marginTop: 1, borderStyle: "single", paddingX: 1 }, /* @__PURE__ */ React4.createElement(Text4, { color: "cyan" }, "ai-lighthouse audit [URL] --enable-llm --llm-provider openai --llm-api-key YOUR_KEY")), /* @__PURE__ */ React4.createElement(Box4, { marginTop: 1 }, /* @__PURE__ */ React4.createElement(Text4, { dimColor: true }, "Supported providers: openai, anthropic, ollama, local")))
|
|
559
|
+
));
|
|
560
|
+
}
|
|
561
|
+
return /* @__PURE__ */ React4.createElement(Box4, { flexDirection: "column", paddingY: 1 }, llm.pageType && /* @__PURE__ */ React4.createElement(
|
|
562
|
+
Box4,
|
|
563
|
+
{
|
|
564
|
+
flexDirection: "column",
|
|
565
|
+
borderStyle: "round",
|
|
566
|
+
borderColor: "magenta",
|
|
567
|
+
paddingX: 2,
|
|
568
|
+
paddingY: 1,
|
|
569
|
+
marginBottom: 1
|
|
570
|
+
},
|
|
571
|
+
/* @__PURE__ */ React4.createElement(Text4, { bold: true, color: "magenta" }, "\u{1F4C4} Inferred Page Type"),
|
|
572
|
+
/* @__PURE__ */ React4.createElement(Text4, { color: "cyan" }, llm.pageType)
|
|
573
|
+
), llm.pageTypeInsights && llm.pageTypeInsights.length > 0 && /* @__PURE__ */ React4.createElement(Box4, { flexDirection: "column", marginBottom: 1 }, /* @__PURE__ */ React4.createElement(Text4, { bold: true, underline: true, color: "blue" }, "\u{1F4A1} AI-Generated Insights"), llm.pageTypeInsights.map((insight, idx) => /* @__PURE__ */ React4.createElement(Box4, { key: idx, marginTop: 0.5 }, /* @__PURE__ */ React4.createElement(Text4, { color: "cyan" }, "\u2022 ", insight)))), llm.summary && /* @__PURE__ */ React4.createElement(Box4, { flexDirection: "column", marginBottom: 1 }, /* @__PURE__ */ React4.createElement(Text4, { bold: true, underline: true }, "Summary"), /* @__PURE__ */ React4.createElement(Box4, { marginTop: 0.5 }, /* @__PURE__ */ React4.createElement(Text4, null, llm.summary))), llm.keyTopics && llm.keyTopics.length > 0 && /* @__PURE__ */ React4.createElement(Box4, { flexDirection: "column", marginBottom: 1 }, /* @__PURE__ */ React4.createElement(Text4, { bold: true, underline: true }, "\u{1F3F7}\uFE0F Key Topics"), /* @__PURE__ */ React4.createElement(Box4, { marginTop: 0.5 }, /* @__PURE__ */ React4.createElement(Text4, null, llm.keyTopics.map((t) => `[${t}]`).join(" ")))), /* @__PURE__ */ React4.createElement(Box4, { flexDirection: "column", marginBottom: 1 }, /* @__PURE__ */ React4.createElement(Text4, { bold: true, underline: true }, "Metadata"), /* @__PURE__ */ React4.createElement(Box4, { marginTop: 0.5, flexDirection: "column" }, llm.readingLevel && /* @__PURE__ */ React4.createElement(Text4, null, /* @__PURE__ */ React4.createElement(Text4, { bold: true }, "Reading Level:"), " ", llm.readingLevel.description), llm.sentiment && /* @__PURE__ */ React4.createElement(Text4, null, /* @__PURE__ */ React4.createElement(Text4, { bold: true }, "Sentiment:"), " ", llm.sentiment), llm.technicalDepth && /* @__PURE__ */ React4.createElement(Text4, null, /* @__PURE__ */ React4.createElement(Text4, { bold: true }, "Technical Depth:"), " ", llm.technicalDepth))), llm.topEntities && llm.topEntities.length > 0 && /* @__PURE__ */ React4.createElement(Box4, { flexDirection: "column", marginBottom: 1 }, /* @__PURE__ */ React4.createElement(Text4, { bold: true, underline: true }, "\u{1F50D} Key Entities"), llm.topEntities.slice(0, 5).map((entity, idx) => /* @__PURE__ */ React4.createElement(Box4, { key: idx, marginTop: 0.5 }, /* @__PURE__ */ React4.createElement(Text4, null, "\u2022 ", /* @__PURE__ */ React4.createElement(Text4, { bold: true }, entity.name), " ", /* @__PURE__ */ React4.createElement(Text4, { dimColor: true }, "(", entity.type, ") - ", Math.round((entity.relevance || 0) * 100), "% relevance"))))), llm.questions && llm.questions.length > 0 && /* @__PURE__ */ React4.createElement(Box4, { flexDirection: "column", marginBottom: 1 }, /* @__PURE__ */ React4.createElement(Text4, { bold: true, underline: true }, "\u2753 Questions AI Can Answer"), llm.questions.slice(0, 5).map((q, idx) => /* @__PURE__ */ React4.createElement(Box4, { key: idx, marginTop: 0.5 }, /* @__PURE__ */ React4.createElement(Text4, null, idx + 1, ". ", /* @__PURE__ */ React4.createElement(Text4, { dimColor: true }, "[", q.difficulty.toUpperCase(), "]"), " ", q.question)))), llm.suggestedFAQ && llm.suggestedFAQ.length > 0 && /* @__PURE__ */ React4.createElement(
|
|
574
|
+
Box4,
|
|
575
|
+
{
|
|
576
|
+
flexDirection: "column",
|
|
577
|
+
borderStyle: "round",
|
|
578
|
+
borderColor: "yellow",
|
|
579
|
+
paddingX: 2,
|
|
580
|
+
paddingY: 1
|
|
581
|
+
},
|
|
582
|
+
/* @__PURE__ */ React4.createElement(Text4, { bold: true, color: "yellow" }, "\u{1F4A1} Suggested FAQs"),
|
|
583
|
+
llm.suggestedFAQ.filter((f) => f.importance === "high").slice(0, 3).map((faq, idx) => /* @__PURE__ */ React4.createElement(Box4, { key: idx, flexDirection: "column", marginTop: 1 }, /* @__PURE__ */ React4.createElement(Text4, null, /* @__PURE__ */ React4.createElement(Text4, { bold: true }, "Q:"), " ", faq.question), /* @__PURE__ */ React4.createElement(Text4, { dimColor: true }, /* @__PURE__ */ React4.createElement(Text4, { bold: true }, "A:"), " ", faq.suggestedAnswer)))
|
|
584
|
+
));
|
|
585
|
+
};
|
|
586
|
+
|
|
587
|
+
// src/ui/components/HallucinationSection.tsx
|
|
588
|
+
import React5 from "react";
|
|
589
|
+
import { Box as Box5, Text as Text5 } from "ink";
|
|
590
|
+
var HallucinationSection = ({ hallucinationReport }) => {
|
|
591
|
+
if (!hallucinationReport || Object.keys(hallucinationReport).length === 0) {
|
|
592
|
+
return /* @__PURE__ */ React5.createElement(Box5, { flexDirection: "column", paddingY: 1 }, /* @__PURE__ */ React5.createElement(
|
|
593
|
+
Box5,
|
|
594
|
+
{
|
|
595
|
+
flexDirection: "column",
|
|
596
|
+
borderStyle: "round",
|
|
597
|
+
borderColor: "yellow",
|
|
598
|
+
paddingX: 2,
|
|
599
|
+
paddingY: 1
|
|
600
|
+
},
|
|
601
|
+
/* @__PURE__ */ React5.createElement(Text5, { bold: true, color: "yellow" }, "\u{1F4A1} Enable Hallucination Detection"),
|
|
602
|
+
/* @__PURE__ */ React5.createElement(Box5, { marginTop: 1, flexDirection: "column" }, /* @__PURE__ */ React5.createElement(Text5, null, "Hallucination detection is not enabled. To see risk assessment, run:"), /* @__PURE__ */ React5.createElement(Box5, { marginTop: 1, borderStyle: "single", paddingX: 1 }, /* @__PURE__ */ React5.createElement(Text5, { color: "cyan" }, "ai-lighthouse audit [URL] --enable-hallucination --enable-llm --llm-provider openai --llm-api-key YOUR_KEY")), /* @__PURE__ */ React5.createElement(Box5, { marginTop: 1 }, /* @__PURE__ */ React5.createElement(Text5, { dimColor: true }, "Note: Hallucination detection requires LLM analysis to be enabled")))
|
|
603
|
+
));
|
|
604
|
+
}
|
|
605
|
+
const riskScore = hallucinationReport.hallucinationRiskScore;
|
|
606
|
+
const getRiskColor = (score) => {
|
|
607
|
+
if (score >= 70) return "red";
|
|
608
|
+
if (score >= 40) return "yellow";
|
|
609
|
+
return "green";
|
|
610
|
+
};
|
|
611
|
+
return /* @__PURE__ */ React5.createElement(Box5, { flexDirection: "column", paddingY: 1 }, /* @__PURE__ */ React5.createElement(
|
|
612
|
+
Box5,
|
|
613
|
+
{
|
|
614
|
+
flexDirection: "column",
|
|
615
|
+
borderStyle: "round",
|
|
616
|
+
borderColor: getRiskColor(riskScore),
|
|
617
|
+
paddingX: 2,
|
|
618
|
+
paddingY: 1,
|
|
619
|
+
marginBottom: 1
|
|
620
|
+
},
|
|
621
|
+
/* @__PURE__ */ React5.createElement(Text5, { bold: true, color: getRiskColor(riskScore) }, "\u26A0\uFE0F Hallucination Risk Assessment"),
|
|
622
|
+
/* @__PURE__ */ React5.createElement(Box5, { marginTop: 1 }, /* @__PURE__ */ React5.createElement(Text5, null, /* @__PURE__ */ React5.createElement(Text5, { bold: true }, "Risk Score: "), /* @__PURE__ */ React5.createElement(Text5, { bold: true, color: getRiskColor(riskScore) }, riskScore, "/100")))
|
|
623
|
+
), hallucinationReport.factCheckSummary && /* @__PURE__ */ React5.createElement(
|
|
624
|
+
Box5,
|
|
625
|
+
{
|
|
626
|
+
flexDirection: "column",
|
|
627
|
+
borderStyle: "round",
|
|
628
|
+
borderColor: "blue",
|
|
629
|
+
paddingX: 2,
|
|
630
|
+
paddingY: 1,
|
|
631
|
+
marginBottom: 1
|
|
632
|
+
},
|
|
633
|
+
/* @__PURE__ */ React5.createElement(Text5, { bold: true, color: "blue" }, "\u{1F4CA} Fact Check Summary"),
|
|
634
|
+
/* @__PURE__ */ React5.createElement(Box5, { marginTop: 1, flexDirection: "column" }, /* @__PURE__ */ React5.createElement(Text5, null, /* @__PURE__ */ React5.createElement(Text5, { bold: true }, "Total Facts:"), " ", hallucinationReport.factCheckSummary.totalFacts), /* @__PURE__ */ React5.createElement(Text5, null, /* @__PURE__ */ React5.createElement(Text5, { bold: true, color: "green" }, "Verified:"), " ", hallucinationReport.factCheckSummary.verifiedFacts), /* @__PURE__ */ React5.createElement(Text5, null, /* @__PURE__ */ React5.createElement(Text5, { bold: true, color: "yellow" }, "Unverified:"), " ", hallucinationReport.factCheckSummary.unverifiedFacts), /* @__PURE__ */ React5.createElement(Text5, null, /* @__PURE__ */ React5.createElement(Text5, { bold: true, color: "red" }, "Contradictions:"), " ", hallucinationReport.factCheckSummary.contradictions), hallucinationReport.factCheckSummary.ambiguities !== void 0 && /* @__PURE__ */ React5.createElement(Text5, null, /* @__PURE__ */ React5.createElement(Text5, { bold: true, color: "yellow" }, "Ambiguities:"), " ", hallucinationReport.factCheckSummary.ambiguities))
|
|
635
|
+
), hallucinationReport.factCheckSummary?.unverifiedFacts > 0 && /* @__PURE__ */ React5.createElement(
|
|
636
|
+
Box5,
|
|
637
|
+
{
|
|
638
|
+
flexDirection: "column",
|
|
639
|
+
borderStyle: "round",
|
|
640
|
+
borderColor: "yellow",
|
|
641
|
+
paddingX: 2,
|
|
642
|
+
paddingY: 1,
|
|
643
|
+
marginBottom: 1
|
|
644
|
+
},
|
|
645
|
+
/* @__PURE__ */ React5.createElement(Text5, { color: "yellow" }, "\u{1F4A1} Tip: Add citations and links to verify claims and reduce AI hallucination risk")
|
|
646
|
+
), hallucinationReport.triggers && hallucinationReport.triggers.length > 0 && /* @__PURE__ */ React5.createElement(React5.Fragment, null, hallucinationReport.triggers.filter((t) => t.severity === "high" || t.severity === "critical").length > 0 && /* @__PURE__ */ React5.createElement(Box5, { flexDirection: "column", marginBottom: 1 }, /* @__PURE__ */ React5.createElement(Text5, { bold: true, underline: true, color: "red" }, "\u{1F6A8} High-Risk Triggers"), hallucinationReport.triggers.filter((t) => t.severity === "high" || t.severity === "critical").slice(0, 5).map((trigger, idx) => /* @__PURE__ */ React5.createElement(
|
|
647
|
+
Box5,
|
|
648
|
+
{
|
|
649
|
+
key: idx,
|
|
650
|
+
flexDirection: "column",
|
|
651
|
+
borderStyle: "single",
|
|
652
|
+
borderColor: "red",
|
|
653
|
+
paddingX: 1,
|
|
654
|
+
paddingY: 0.5,
|
|
655
|
+
marginTop: 1
|
|
656
|
+
},
|
|
657
|
+
/* @__PURE__ */ React5.createElement(Text5, null, idx + 1, ". ", /* @__PURE__ */ React5.createElement(Text5, { bold: true, color: "red" }, "[", trigger.severity.toUpperCase(), "]"), " ", trigger.type),
|
|
658
|
+
/* @__PURE__ */ React5.createElement(Text5, { dimColor: true }, trigger.description),
|
|
659
|
+
trigger.confidence && /* @__PURE__ */ React5.createElement(Text5, { dimColor: true }, "Confidence: ", Math.round(trigger.confidence * 100), "%")
|
|
660
|
+
)))), hallucinationReport.recommendations && hallucinationReport.recommendations.length > 0 && /* @__PURE__ */ React5.createElement(Box5, { flexDirection: "column" }, /* @__PURE__ */ React5.createElement(Text5, { bold: true, underline: true, color: "cyan" }, "\u{1F4A1} Recommendations"), hallucinationReport.recommendations.slice(0, 3).map((rec, idx) => /* @__PURE__ */ React5.createElement(Box5, { key: idx, marginTop: 0.5 }, /* @__PURE__ */ React5.createElement(Text5, { color: "cyan" }, idx + 1, ". ", rec)))));
|
|
661
|
+
};
|
|
662
|
+
|
|
663
|
+
// src/ui/components/MessageAlignmentSection.tsx
|
|
664
|
+
import React6 from "react";
|
|
665
|
+
import { Box as Box6, Text as Text6 } from "ink";
|
|
666
|
+
var MessageAlignmentSection = ({ mirrorReport }) => {
|
|
667
|
+
if (!mirrorReport || Object.keys(mirrorReport).length === 0) {
|
|
668
|
+
return /* @__PURE__ */ React6.createElement(Box6, { flexDirection: "column", paddingY: 1 }, /* @__PURE__ */ React6.createElement(
|
|
669
|
+
Box6,
|
|
670
|
+
{
|
|
671
|
+
flexDirection: "column",
|
|
672
|
+
borderStyle: "round",
|
|
673
|
+
borderColor: "yellow",
|
|
674
|
+
paddingX: 2,
|
|
675
|
+
paddingY: 1
|
|
676
|
+
},
|
|
677
|
+
/* @__PURE__ */ React6.createElement(Text6, { bold: true, color: "yellow" }, "\u{1F4A1} Enable Message Alignment Analysis"),
|
|
678
|
+
/* @__PURE__ */ React6.createElement(Box6, { marginTop: 1, flexDirection: "column" }, /* @__PURE__ */ React6.createElement(Text6, null, "Message alignment analysis is not available. To see what AI understands, run:"), /* @__PURE__ */ React6.createElement(Box6, { marginTop: 1, borderStyle: "single", paddingX: 1 }, /* @__PURE__ */ React6.createElement(Text6, { color: "cyan" }, "ai-lighthouse audit [URL] --enable-llm --llm-provider openai --llm-api-key YOUR_KEY")), /* @__PURE__ */ React6.createElement(Box6, { marginTop: 1 }, /* @__PURE__ */ React6.createElement(Text6, { dimColor: true }, "This requires LLM analysis to compare AI understanding with your intended message")))
|
|
679
|
+
));
|
|
680
|
+
}
|
|
681
|
+
const getScoreColor = (score) => {
|
|
682
|
+
if (score >= 80) return "green";
|
|
683
|
+
if (score >= 60) return "yellow";
|
|
684
|
+
return "red";
|
|
685
|
+
};
|
|
686
|
+
return /* @__PURE__ */ React6.createElement(Box6, { flexDirection: "column", paddingY: 1 }, /* @__PURE__ */ React6.createElement(
|
|
687
|
+
Box6,
|
|
688
|
+
{
|
|
689
|
+
flexDirection: "column",
|
|
690
|
+
borderStyle: "round",
|
|
691
|
+
borderColor: "magenta",
|
|
692
|
+
paddingX: 2,
|
|
693
|
+
paddingY: 1,
|
|
694
|
+
marginBottom: 1
|
|
695
|
+
},
|
|
696
|
+
/* @__PURE__ */ React6.createElement(Text6, { bold: true, color: "magenta" }, "\u{1F50D} AI Misunderstanding Check"),
|
|
697
|
+
/* @__PURE__ */ React6.createElement(Box6, { marginTop: 1, flexDirection: "column" }, /* @__PURE__ */ React6.createElement(Box6, null, /* @__PURE__ */ React6.createElement(Text6, null, /* @__PURE__ */ React6.createElement(Text6, { bold: true }, "Alignment Score:"), " ", /* @__PURE__ */ React6.createElement(Text6, { bold: true, color: getScoreColor(mirrorReport.summary.alignmentScore) }, mirrorReport.summary.alignmentScore, "/100"))), /* @__PURE__ */ React6.createElement(Box6, null, /* @__PURE__ */ React6.createElement(Text6, null, /* @__PURE__ */ React6.createElement(Text6, { bold: true }, "Clarity Score:"), " ", /* @__PURE__ */ React6.createElement(Text6, { bold: true, color: getScoreColor(mirrorReport.summary.clarityScore) }, mirrorReport.summary.clarityScore, "/100"))), /* @__PURE__ */ React6.createElement(Box6, null, /* @__PURE__ */ React6.createElement(Text6, null, /* @__PURE__ */ React6.createElement(Text6, { bold: true }, "Critical Issues:"), " ", /* @__PURE__ */ React6.createElement(Text6, { color: "red" }, mirrorReport.summary.critical))), /* @__PURE__ */ React6.createElement(Box6, null, /* @__PURE__ */ React6.createElement(Text6, null, /* @__PURE__ */ React6.createElement(Text6, { bold: true }, "Major Issues:"), " ", /* @__PURE__ */ React6.createElement(Text6, { color: "yellow" }, mirrorReport.summary.major))))
|
|
698
|
+
), mirrorReport.llmInterpretation && /* @__PURE__ */ React6.createElement(
|
|
699
|
+
Box6,
|
|
700
|
+
{
|
|
701
|
+
flexDirection: "column",
|
|
702
|
+
borderStyle: "round",
|
|
703
|
+
borderColor: "blue",
|
|
704
|
+
paddingX: 2,
|
|
705
|
+
paddingY: 1,
|
|
706
|
+
marginBottom: 1
|
|
707
|
+
},
|
|
708
|
+
/* @__PURE__ */ React6.createElement(Text6, { bold: true, color: "blue" }, "\u{1F916} What AI Actually Understood"),
|
|
709
|
+
/* @__PURE__ */ React6.createElement(Text6, { dimColor: true }, "(", Math.round(mirrorReport.llmInterpretation.confidence * 100), "% confident)"),
|
|
710
|
+
/* @__PURE__ */ React6.createElement(Box6, { marginTop: 1, flexDirection: "column" }, mirrorReport.llmInterpretation.productName && /* @__PURE__ */ React6.createElement(Text6, null, /* @__PURE__ */ React6.createElement(Text6, { bold: true }, "Product:"), " ", mirrorReport.llmInterpretation.productName), mirrorReport.llmInterpretation.purpose && /* @__PURE__ */ React6.createElement(Text6, null, /* @__PURE__ */ React6.createElement(Text6, { bold: true }, "Purpose:"), " ", mirrorReport.llmInterpretation.purpose), mirrorReport.llmInterpretation.valueProposition && /* @__PURE__ */ React6.createElement(Text6, null, /* @__PURE__ */ React6.createElement(Text6, { bold: true, color: "magenta" }, "\u{1F48E} Value:"), " ", mirrorReport.llmInterpretation.valueProposition)),
|
|
711
|
+
mirrorReport.llmInterpretation.keyBenefits && mirrorReport.llmInterpretation.keyBenefits.length > 0 && /* @__PURE__ */ React6.createElement(Box6, { marginTop: 1, flexDirection: "column" }, /* @__PURE__ */ React6.createElement(Text6, { bold: true }, "Benefits:"), mirrorReport.llmInterpretation.keyBenefits.map((benefit, idx) => /* @__PURE__ */ React6.createElement(Text6, { key: idx }, "\u2022 ", benefit))),
|
|
712
|
+
mirrorReport.llmInterpretation.keyFeatures && mirrorReport.llmInterpretation.keyFeatures.length > 0 && /* @__PURE__ */ React6.createElement(Box6, { marginTop: 1, flexDirection: "column" }, /* @__PURE__ */ React6.createElement(Text6, { bold: true }, "Features:"), mirrorReport.llmInterpretation.keyFeatures.slice(0, 3).map((feature, idx) => /* @__PURE__ */ React6.createElement(Text6, { key: idx }, "\u2022 ", feature))),
|
|
713
|
+
mirrorReport.llmInterpretation.targetAudience && /* @__PURE__ */ React6.createElement(Box6, { marginTop: 1 }, /* @__PURE__ */ React6.createElement(Text6, null, /* @__PURE__ */ React6.createElement(Text6, { bold: true }, "Audience:"), " ", mirrorReport.llmInterpretation.targetAudience))
|
|
714
|
+
), mirrorReport.mismatches && mirrorReport.mismatches.length > 0 && /* @__PURE__ */ React6.createElement(React6.Fragment, null, mirrorReport.mismatches.filter((m) => m.severity === "critical" || m.severity === "major").length > 0 && /* @__PURE__ */ React6.createElement(Box6, { flexDirection: "column", marginBottom: 1 }, /* @__PURE__ */ React6.createElement(Text6, { bold: true, underline: true }, "Priority Mismatches"), mirrorReport.mismatches.filter((m) => m.severity === "critical" || m.severity === "major").slice(0, 5).map((mismatch, idx) => /* @__PURE__ */ React6.createElement(
|
|
715
|
+
Box6,
|
|
716
|
+
{
|
|
717
|
+
key: idx,
|
|
718
|
+
flexDirection: "column",
|
|
719
|
+
borderStyle: "single",
|
|
720
|
+
borderColor: mismatch.severity === "critical" ? "red" : "yellow",
|
|
721
|
+
paddingX: 1,
|
|
722
|
+
paddingY: 0.5,
|
|
723
|
+
marginTop: 1
|
|
724
|
+
},
|
|
725
|
+
/* @__PURE__ */ React6.createElement(Text6, null, mismatch.severity === "critical" ? "\u{1F534}" : "\u{1F7E1}", " ", idx + 1, ". ", /* @__PURE__ */ React6.createElement(Text6, { bold: true }, mismatch.field)),
|
|
726
|
+
/* @__PURE__ */ React6.createElement(Text6, { dimColor: true }, mismatch.description),
|
|
727
|
+
/* @__PURE__ */ React6.createElement(Box6, { marginTop: 0.5 }, /* @__PURE__ */ React6.createElement(Text6, { color: "cyan" }, "\u2192 ", mismatch.recommendation))
|
|
728
|
+
)))), mirrorReport.recommendations && mirrorReport.recommendations.length > 0 && /* @__PURE__ */ React6.createElement(Box6, { flexDirection: "column" }, /* @__PURE__ */ React6.createElement(Text6, { bold: true, underline: true, color: "cyan" }, "\u{1F4A1} Top Recommendations"), mirrorReport.recommendations.slice(0, 3).map((rec, idx) => /* @__PURE__ */ React6.createElement(Box6, { key: idx, marginTop: 0.5 }, /* @__PURE__ */ React6.createElement(Text6, { color: "cyan" }, idx + 1, ". ", rec)))));
|
|
729
|
+
};
|
|
730
|
+
|
|
731
|
+
// src/ui/components/TechnicalSection.tsx
|
|
732
|
+
import React7 from "react";
|
|
733
|
+
import { Box as Box7, Text as Text7 } from "ink";
|
|
734
|
+
var TechnicalSection = ({ result, scoring }) => {
|
|
735
|
+
const renderProgressBar = (score, width = 20) => {
|
|
736
|
+
const filled = Math.round(score / 100 * width);
|
|
737
|
+
const empty = width - filled;
|
|
738
|
+
const color = score >= 80 ? "green" : score >= 60 ? "yellow" : "red";
|
|
739
|
+
return /* @__PURE__ */ React7.createElement(Text7, { color }, "\u2588".repeat(filled), /* @__PURE__ */ React7.createElement(Text7, { dimColor: true }, "\u2591".repeat(empty)), ` ${Math.round(score)}%`);
|
|
740
|
+
};
|
|
741
|
+
return /* @__PURE__ */ React7.createElement(Box7, { flexDirection: "column", paddingY: 1 }, /* @__PURE__ */ React7.createElement(
|
|
742
|
+
Box7,
|
|
743
|
+
{
|
|
744
|
+
flexDirection: "column",
|
|
745
|
+
borderStyle: "round",
|
|
746
|
+
borderColor: "blue",
|
|
747
|
+
paddingX: 2,
|
|
748
|
+
paddingY: 1,
|
|
749
|
+
marginBottom: 1
|
|
750
|
+
},
|
|
751
|
+
/* @__PURE__ */ React7.createElement(Text7, { bold: true, color: "blue" }, "\u{1F4CA} Category Scores"),
|
|
752
|
+
/* @__PURE__ */ React7.createElement(Box7, { marginTop: 1, flexDirection: "column" }, scoring?.crawlability !== void 0 && /* @__PURE__ */ React7.createElement(Box7, { flexDirection: "column", marginTop: 0.5 }, /* @__PURE__ */ React7.createElement(Box7, { justifyContent: "space-between" }, /* @__PURE__ */ React7.createElement(Text7, { bold: true }, "\u{1F577}\uFE0F Crawlability"), /* @__PURE__ */ React7.createElement(Text7, null, Math.round(scoring.crawlability), "/100")), /* @__PURE__ */ React7.createElement(Box7, { marginTop: 0.5 }, renderProgressBar(scoring.crawlability))), scoring?.structure !== void 0 && /* @__PURE__ */ React7.createElement(Box7, { flexDirection: "column", marginTop: 1 }, /* @__PURE__ */ React7.createElement(Box7, { justifyContent: "space-between" }, /* @__PURE__ */ React7.createElement(Text7, { bold: true }, "\u{1F4D0} Structure"), /* @__PURE__ */ React7.createElement(Text7, null, Math.round(scoring.structure), "/100")), /* @__PURE__ */ React7.createElement(Box7, { marginTop: 0.5 }, renderProgressBar(scoring.structure))), scoring?.schema_coverage !== void 0 && /* @__PURE__ */ React7.createElement(Box7, { flexDirection: "column", marginTop: 1 }, /* @__PURE__ */ React7.createElement(Box7, { justifyContent: "space-between" }, /* @__PURE__ */ React7.createElement(Text7, { bold: true }, "\u{1F3F7}\uFE0F Schema Coverage"), /* @__PURE__ */ React7.createElement(Text7, null, Math.round(scoring.schema_coverage), "/100")), /* @__PURE__ */ React7.createElement(Box7, { marginTop: 0.5 }, renderProgressBar(scoring.schema_coverage))), scoring?.content_clarity !== void 0 && /* @__PURE__ */ React7.createElement(Box7, { flexDirection: "column", marginTop: 1 }, /* @__PURE__ */ React7.createElement(Box7, { justifyContent: "space-between" }, /* @__PURE__ */ React7.createElement(Text7, { bold: true }, "\u{1F4DD} Content Clarity"), /* @__PURE__ */ React7.createElement(Text7, null, Math.round(scoring.content_clarity), "/100")), /* @__PURE__ */ React7.createElement(Box7, { marginTop: 0.5 }, renderProgressBar(scoring.content_clarity))))
|
|
753
|
+
), result.chunking && Object.keys(result.chunking).length > 0 ? /* @__PURE__ */ React7.createElement(
|
|
754
|
+
Box7,
|
|
755
|
+
{
|
|
756
|
+
flexDirection: "column",
|
|
757
|
+
borderStyle: "round",
|
|
758
|
+
borderColor: "green",
|
|
759
|
+
paddingX: 2,
|
|
760
|
+
paddingY: 1,
|
|
761
|
+
marginBottom: 1
|
|
762
|
+
},
|
|
763
|
+
/* @__PURE__ */ React7.createElement(Text7, { bold: true, color: "green" }, "\u{1F4C4} Content Chunking Analysis"),
|
|
764
|
+
/* @__PURE__ */ React7.createElement(Box7, { marginTop: 1, flexDirection: "column" }, /* @__PURE__ */ React7.createElement(Text7, null, /* @__PURE__ */ React7.createElement(Text7, { bold: true }, "Strategy:"), " ", result.chunking.chunkingStrategy), /* @__PURE__ */ React7.createElement(Text7, null, /* @__PURE__ */ React7.createElement(Text7, { bold: true }, "Total Chunks:"), " ", result.chunking.totalChunks), /* @__PURE__ */ React7.createElement(Text7, null, /* @__PURE__ */ React7.createElement(Text7, { bold: true }, "Avg Tokens/Chunk:"), " ", result.chunking.averageTokensPerChunk), /* @__PURE__ */ React7.createElement(Text7, null, /* @__PURE__ */ React7.createElement(Text7, { bold: true }, "Avg Noise Ratio:"), " ", (result.chunking.averageNoiseRatio * 100).toFixed(1), "%")),
|
|
765
|
+
result.chunking.chunkingStrategy === "heading-based" && /* @__PURE__ */ React7.createElement(Box7, { marginTop: 1 }, /* @__PURE__ */ React7.createElement(Text7, { color: "green" }, "\u2713 Heading-based chunking is ideal for AI comprehension")),
|
|
766
|
+
result.chunking.chunkingStrategy === "paragraph-based" && /* @__PURE__ */ React7.createElement(Box7, { marginTop: 1 }, /* @__PURE__ */ React7.createElement(Text7, { color: "yellow" }, "\u26A0 Consider adding headings for better semantic structure"))
|
|
767
|
+
) : /* @__PURE__ */ React7.createElement(
|
|
768
|
+
Box7,
|
|
769
|
+
{
|
|
770
|
+
flexDirection: "column",
|
|
771
|
+
borderStyle: "round",
|
|
772
|
+
borderColor: "gray",
|
|
773
|
+
paddingX: 2,
|
|
774
|
+
paddingY: 1,
|
|
775
|
+
marginBottom: 1
|
|
776
|
+
},
|
|
777
|
+
/* @__PURE__ */ React7.createElement(Text7, { dimColor: true }, "\u{1F4A1} Enable chunking analysis with --enable-chunking to see how your content is divided for AI processing")
|
|
778
|
+
), result.extractability && Object.keys(result.extractability).length > 0 ? /* @__PURE__ */ React7.createElement(
|
|
779
|
+
Box7,
|
|
780
|
+
{
|
|
781
|
+
flexDirection: "column",
|
|
782
|
+
borderStyle: "round",
|
|
783
|
+
borderColor: "yellow",
|
|
784
|
+
paddingX: 2,
|
|
785
|
+
paddingY: 1,
|
|
786
|
+
marginBottom: 1
|
|
787
|
+
},
|
|
788
|
+
/* @__PURE__ */ React7.createElement(Text7, { bold: true, color: "yellow" }, "\u{1F504} Extractability Analysis"),
|
|
789
|
+
/* @__PURE__ */ React7.createElement(Box7, { marginTop: 1, flexDirection: "column" }, /* @__PURE__ */ React7.createElement(Text7, null, /* @__PURE__ */ React7.createElement(Text7, { bold: true }, "Overall Score:"), " ", result.extractability.score.extractabilityScore, "/100"), /* @__PURE__ */ React7.createElement(Text7, null, /* @__PURE__ */ React7.createElement(Text7, { bold: true }, "Server-Rendered:"), " ", result.extractability.score.serverRenderedPercent, "%")),
|
|
790
|
+
/* @__PURE__ */ React7.createElement(Box7, { marginTop: 1, flexDirection: "column" }, /* @__PURE__ */ React7.createElement(Text7, { bold: true, underline: true }, "Content Type Extractability:"), Object.entries(result.extractability.contentTypes).map(([type, data]) => {
|
|
791
|
+
const percentage = data.percentage;
|
|
792
|
+
const color = percentage >= 80 ? "green" : percentage >= 50 ? "yellow" : "red";
|
|
793
|
+
return /* @__PURE__ */ React7.createElement(Box7, { key: type, marginTop: 0.5 }, /* @__PURE__ */ React7.createElement(Text7, null, /* @__PURE__ */ React7.createElement(Text7, { bold: true }, type.charAt(0).toUpperCase() + type.slice(1), ":"), " ", /* @__PURE__ */ React7.createElement(Text7, { color }, percentage, "%"), " ", /* @__PURE__ */ React7.createElement(Text7, { dimColor: true }, "(", data.extractable, "/", data.total, ")")));
|
|
794
|
+
})),
|
|
795
|
+
result.extractability.score.extractabilityScore >= 80 && /* @__PURE__ */ React7.createElement(Box7, { marginTop: 1 }, /* @__PURE__ */ React7.createElement(Text7, { color: "green" }, "\u2713 Good extractability - AI can easily read your content")),
|
|
796
|
+
result.extractability.score.extractabilityScore < 50 && /* @__PURE__ */ React7.createElement(Box7, { marginTop: 1 }, /* @__PURE__ */ React7.createElement(Text7, { color: "red" }, "\u26A0 Low extractability - Consider server-side rendering"))
|
|
797
|
+
) : /* @__PURE__ */ React7.createElement(
|
|
798
|
+
Box7,
|
|
799
|
+
{
|
|
800
|
+
flexDirection: "column",
|
|
801
|
+
borderStyle: "round",
|
|
802
|
+
borderColor: "gray",
|
|
803
|
+
paddingX: 2,
|
|
804
|
+
paddingY: 1,
|
|
805
|
+
marginBottom: 1
|
|
806
|
+
},
|
|
807
|
+
/* @__PURE__ */ React7.createElement(Text7, { dimColor: true }, "\u{1F4A1} Enable extractability analysis with --enable-extractability to see how well AI can extract your content")
|
|
808
|
+
));
|
|
809
|
+
};
|
|
810
|
+
|
|
811
|
+
// src/ui/AuditReportUI.tsx
|
|
812
|
+
var AuditReportUI = ({
|
|
813
|
+
url,
|
|
814
|
+
result,
|
|
815
|
+
aiReadiness,
|
|
816
|
+
loading = false,
|
|
817
|
+
currentStep = ""
|
|
818
|
+
}) => {
|
|
819
|
+
const [currentTab, setCurrentTab] = useState2("overview");
|
|
820
|
+
const tabs = [
|
|
821
|
+
{ key: "overview", label: "Overview", icon: "\u{1F4CA}" },
|
|
822
|
+
{ key: "issues", label: "Issues", icon: "\u26A0\uFE0F" },
|
|
823
|
+
{ key: "ai-understanding", label: "AI Understanding", icon: "\u{1F9E0}" },
|
|
824
|
+
{ key: "hallucination", label: "Hallucination Risk", icon: "\u26A0\uFE0F" },
|
|
825
|
+
{ key: "message-alignment", label: "Message Alignment", icon: "\u{1F50D}" },
|
|
826
|
+
{ key: "technical", label: "Technical", icon: "\u2699\uFE0F" }
|
|
827
|
+
];
|
|
828
|
+
useInput((input, key) => {
|
|
829
|
+
if (loading) return;
|
|
830
|
+
if (key.leftArrow) {
|
|
831
|
+
const currentIndex = tabs.findIndex((t) => t.key === currentTab);
|
|
832
|
+
const prevIndex = currentIndex > 0 ? currentIndex - 1 : tabs.length - 1;
|
|
833
|
+
setCurrentTab(tabs[prevIndex].key);
|
|
834
|
+
}
|
|
835
|
+
if (key.rightArrow) {
|
|
836
|
+
const currentIndex = tabs.findIndex((t) => t.key === currentTab);
|
|
837
|
+
const nextIndex = currentIndex < tabs.length - 1 ? currentIndex + 1 : 0;
|
|
838
|
+
setCurrentTab(tabs[nextIndex].key);
|
|
839
|
+
}
|
|
840
|
+
const num = parseInt(input);
|
|
841
|
+
if (num >= 1 && num <= tabs.length) {
|
|
842
|
+
setCurrentTab(tabs[num - 1].key);
|
|
843
|
+
}
|
|
844
|
+
});
|
|
845
|
+
if (loading) {
|
|
846
|
+
return /* @__PURE__ */ React8.createElement(Box8, { flexDirection: "column", paddingY: 1 }, /* @__PURE__ */ React8.createElement(Box8, null, /* @__PURE__ */ React8.createElement(Text8, { color: "green" }, /* @__PURE__ */ React8.createElement(Spinner, { type: "dots" })), /* @__PURE__ */ React8.createElement(Text8, null, " ", currentStep || "Loading...")));
|
|
847
|
+
}
|
|
848
|
+
const overallScore = aiReadiness?.overall || 0;
|
|
849
|
+
const grade = aiReadiness?.grade || "N/A";
|
|
850
|
+
return /* @__PURE__ */ React8.createElement(Box8, { flexDirection: "column" }, /* @__PURE__ */ React8.createElement(
|
|
851
|
+
Box8,
|
|
852
|
+
{
|
|
853
|
+
flexDirection: "column",
|
|
854
|
+
borderStyle: "round",
|
|
855
|
+
borderColor: "cyan",
|
|
856
|
+
paddingX: 2,
|
|
857
|
+
paddingY: 1
|
|
858
|
+
},
|
|
859
|
+
/* @__PURE__ */ React8.createElement(Text8, { bold: true, color: "cyan" }, "\u{1F6A8} AI Lighthouse Report")
|
|
860
|
+
), /* @__PURE__ */ React8.createElement(ScoreDisplay, { score: overallScore, grade, url }), /* @__PURE__ */ React8.createElement(
|
|
861
|
+
Box8,
|
|
862
|
+
{
|
|
863
|
+
flexDirection: "row",
|
|
864
|
+
borderStyle: "single",
|
|
865
|
+
borderColor: "blue",
|
|
866
|
+
paddingX: 1,
|
|
867
|
+
marginTop: 1
|
|
868
|
+
},
|
|
869
|
+
tabs.map((tab, index) => {
|
|
870
|
+
const isActive = tab.key === currentTab;
|
|
871
|
+
return /* @__PURE__ */ React8.createElement(Box8, { key: tab.key, marginRight: 1 }, /* @__PURE__ */ React8.createElement(
|
|
872
|
+
Text8,
|
|
873
|
+
{
|
|
874
|
+
bold: isActive,
|
|
875
|
+
color: isActive ? "cyan" : "gray",
|
|
876
|
+
backgroundColor: isActive ? "blue" : void 0
|
|
877
|
+
},
|
|
878
|
+
` ${index + 1}. ${tab.icon} ${tab.label} `
|
|
879
|
+
));
|
|
880
|
+
})
|
|
881
|
+
), /* @__PURE__ */ React8.createElement(Box8, { marginTop: 1, marginBottom: 1 }, /* @__PURE__ */ React8.createElement(Text8, { dimColor: true }, "Use \u2190 \u2192 arrow keys or numbers (1-", tabs.length, ") to navigate tabs")), /* @__PURE__ */ React8.createElement(Box8, { flexDirection: "column" }, currentTab === "overview" && /* @__PURE__ */ React8.createElement(OverviewSection, { aiReadiness }), currentTab === "issues" && /* @__PURE__ */ React8.createElement(IssuesSection, { issues: result.issues || [] }), currentTab === "ai-understanding" && /* @__PURE__ */ React8.createElement(AIUnderstandingSection, { llm: result.llm }), currentTab === "hallucination" && /* @__PURE__ */ React8.createElement(HallucinationSection, { hallucinationReport: result.hallucinationReport }), currentTab === "message-alignment" && /* @__PURE__ */ React8.createElement(MessageAlignmentSection, { mirrorReport: result.mirrorReport }), currentTab === "technical" && /* @__PURE__ */ React8.createElement(TechnicalSection, { result, scoring: result.scoring })), /* @__PURE__ */ React8.createElement(
|
|
882
|
+
Box8,
|
|
883
|
+
{
|
|
884
|
+
marginTop: 2,
|
|
885
|
+
borderStyle: "round",
|
|
886
|
+
borderColor: "gray",
|
|
887
|
+
paddingX: 2,
|
|
888
|
+
paddingY: 1
|
|
889
|
+
},
|
|
890
|
+
/* @__PURE__ */ React8.createElement(Text8, { dimColor: true }, "Press Ctrl+C to exit \u2022 Report generated at ", (/* @__PURE__ */ new Date()).toLocaleString())
|
|
891
|
+
));
|
|
892
|
+
};
|
|
893
|
+
|
|
894
|
+
// src/ui/SetupWizard.tsx
|
|
895
|
+
import React9, { useState as useState3 } from "react";
|
|
896
|
+
import { Box as Box9, Text as Text9, useApp } from "ink";
|
|
897
|
+
import TextInput from "ink-text-input";
|
|
898
|
+
import SelectInput from "ink-select-input";
|
|
899
|
+
var SetupWizard = ({ onComplete, initialUrl }) => {
|
|
900
|
+
const { exit } = useApp();
|
|
901
|
+
const [step, setStep] = useState3(initialUrl ? "features" : "url");
|
|
902
|
+
const [url, setUrl] = useState3(initialUrl || "");
|
|
903
|
+
const [selectedFeatures, setSelectedFeatures] = useState3([]);
|
|
904
|
+
const [llmProvider, setLlmProvider] = useState3("");
|
|
905
|
+
const [llmModel, setLlmModel] = useState3("");
|
|
906
|
+
const [llmApiKey, setLlmApiKey] = useState3("");
|
|
907
|
+
const [llmBaseUrl, setLlmBaseUrl] = useState3("");
|
|
908
|
+
const featureOptions = [
|
|
909
|
+
{ label: "\u{1F9E0} AI Understanding (LLM Analysis)", value: "llm" },
|
|
910
|
+
{ label: "\u{1F4C4} Content Chunking Analysis", value: "chunking" },
|
|
911
|
+
{ label: "\u{1F504} Extractability Analysis", value: "extractability" },
|
|
912
|
+
{ label: "\u26A0\uFE0F Hallucination Detection", value: "hallucination" },
|
|
913
|
+
{ label: "\u2705 Continue with selected features", value: "done" }
|
|
914
|
+
];
|
|
915
|
+
const llmProviderOptions = [
|
|
916
|
+
{ label: "OpenAI (GPT-4, GPT-3.5)", value: "openai" },
|
|
917
|
+
{ label: "Anthropic (Claude)", value: "anthropic" },
|
|
918
|
+
{ label: "Ollama (Local)", value: "ollama" },
|
|
919
|
+
{ label: "Custom/Local Provider", value: "local" }
|
|
920
|
+
];
|
|
921
|
+
const handleUrlSubmit = (value) => {
|
|
922
|
+
setUrl(value);
|
|
923
|
+
setStep("features");
|
|
924
|
+
};
|
|
925
|
+
const handleFeatureSelect = (item) => {
|
|
926
|
+
if (item.value === "done") {
|
|
927
|
+
if (selectedFeatures.includes("llm") || selectedFeatures.includes("hallucination")) {
|
|
928
|
+
setStep("llm-provider");
|
|
929
|
+
} else {
|
|
930
|
+
completeSetup();
|
|
931
|
+
}
|
|
932
|
+
} else {
|
|
933
|
+
if (selectedFeatures.includes(item.value)) {
|
|
934
|
+
setSelectedFeatures(selectedFeatures.filter((f) => f !== item.value));
|
|
935
|
+
} else {
|
|
936
|
+
setSelectedFeatures([...selectedFeatures, item.value]);
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
};
|
|
940
|
+
const handleLlmProviderSelect = (item) => {
|
|
941
|
+
setLlmProvider(item.value);
|
|
942
|
+
if (item.value === "ollama") {
|
|
943
|
+
setLlmBaseUrl("http://localhost:11434");
|
|
944
|
+
}
|
|
945
|
+
setStep("llm-model");
|
|
946
|
+
};
|
|
947
|
+
const handleLlmModelSubmit = (value) => {
|
|
948
|
+
setLlmModel(value);
|
|
949
|
+
if (llmProvider === "ollama") {
|
|
950
|
+
setStep("llm-base-url");
|
|
951
|
+
} else {
|
|
952
|
+
setStep("llm-api-key");
|
|
953
|
+
}
|
|
954
|
+
};
|
|
955
|
+
const handleLlmApiKeySubmit = (value) => {
|
|
956
|
+
setLlmApiKey(value);
|
|
957
|
+
if (llmProvider === "local" || llmProvider === "anthropic") {
|
|
958
|
+
setStep("llm-base-url");
|
|
959
|
+
} else {
|
|
960
|
+
completeSetup();
|
|
961
|
+
}
|
|
962
|
+
};
|
|
963
|
+
const handleLlmBaseUrlSubmit = (value) => {
|
|
964
|
+
setLlmBaseUrl(value);
|
|
965
|
+
completeSetup();
|
|
966
|
+
};
|
|
967
|
+
const completeSetup = () => {
|
|
968
|
+
const config = {
|
|
969
|
+
url,
|
|
970
|
+
enableLlm: selectedFeatures.includes("llm") || selectedFeatures.includes("hallucination"),
|
|
971
|
+
enableChunking: selectedFeatures.includes("chunking"),
|
|
972
|
+
enableExtractability: selectedFeatures.includes("extractability"),
|
|
973
|
+
enableHallucination: selectedFeatures.includes("hallucination")
|
|
974
|
+
};
|
|
975
|
+
if (config.enableLlm) {
|
|
976
|
+
config.llmProvider = llmProvider;
|
|
977
|
+
config.llmModel = llmModel;
|
|
978
|
+
config.llmApiKey = llmApiKey || void 0;
|
|
979
|
+
config.llmBaseUrl = llmBaseUrl || void 0;
|
|
980
|
+
}
|
|
981
|
+
onComplete(config);
|
|
982
|
+
exit();
|
|
983
|
+
};
|
|
984
|
+
const getModelPlaceholder = () => {
|
|
985
|
+
switch (llmProvider) {
|
|
986
|
+
case "openai":
|
|
987
|
+
return "gpt-4o-mini (default)";
|
|
988
|
+
case "anthropic":
|
|
989
|
+
return "claude-3-5-sonnet-20241022 (default)";
|
|
990
|
+
case "ollama":
|
|
991
|
+
return "qwen2.5:0.5b";
|
|
992
|
+
default:
|
|
993
|
+
return "model-name";
|
|
994
|
+
}
|
|
995
|
+
};
|
|
996
|
+
const getApiKeyPlaceholder = () => {
|
|
997
|
+
switch (llmProvider) {
|
|
998
|
+
case "openai":
|
|
999
|
+
return "sk-...";
|
|
1000
|
+
case "anthropic":
|
|
1001
|
+
return "sk-ant-...";
|
|
1002
|
+
default:
|
|
1003
|
+
return "your-api-key";
|
|
1004
|
+
}
|
|
1005
|
+
};
|
|
1006
|
+
return /* @__PURE__ */ React9.createElement(Box9, { flexDirection: "column", paddingY: 1 }, /* @__PURE__ */ React9.createElement(
|
|
1007
|
+
Box9,
|
|
1008
|
+
{
|
|
1009
|
+
flexDirection: "column",
|
|
1010
|
+
borderStyle: "round",
|
|
1011
|
+
borderColor: "cyan",
|
|
1012
|
+
paddingX: 2,
|
|
1013
|
+
paddingY: 1,
|
|
1014
|
+
marginBottom: 1
|
|
1015
|
+
},
|
|
1016
|
+
/* @__PURE__ */ React9.createElement(Text9, { bold: true, color: "cyan" }, "\u{1F6A8} AI Lighthouse Setup Wizard"),
|
|
1017
|
+
/* @__PURE__ */ React9.createElement(Text9, { dimColor: true }, "Configure your audit settings")
|
|
1018
|
+
), step === "url" && /* @__PURE__ */ React9.createElement(Box9, { flexDirection: "column" }, /* @__PURE__ */ React9.createElement(Text9, { bold: true }, "Enter the URL to audit:"), /* @__PURE__ */ React9.createElement(Box9, { marginTop: 1 }, /* @__PURE__ */ React9.createElement(Text9, { color: "cyan" }, "URL: "), /* @__PURE__ */ React9.createElement(
|
|
1019
|
+
TextInput,
|
|
1020
|
+
{
|
|
1021
|
+
value: url,
|
|
1022
|
+
onChange: setUrl,
|
|
1023
|
+
onSubmit: handleUrlSubmit,
|
|
1024
|
+
placeholder: "https://example.com"
|
|
1025
|
+
}
|
|
1026
|
+
)), /* @__PURE__ */ React9.createElement(Box9, { marginTop: 1 }, /* @__PURE__ */ React9.createElement(Text9, { dimColor: true }, "Press Enter to continue"))), step === "features" && /* @__PURE__ */ React9.createElement(Box9, { flexDirection: "column" }, /* @__PURE__ */ React9.createElement(Text9, { bold: true }, "Select features to enable:"), /* @__PURE__ */ React9.createElement(Box9, { marginTop: 1 }, /* @__PURE__ */ React9.createElement(Text9, { dimColor: true }, "Selected: ", selectedFeatures.length === 0 ? "None (basic audit only)" : selectedFeatures.map((f) => {
|
|
1027
|
+
const feature = featureOptions.find((opt) => opt.value === f);
|
|
1028
|
+
return feature?.label.split(" ")[0];
|
|
1029
|
+
}).join(", "))), /* @__PURE__ */ React9.createElement(Box9, { marginTop: 1 }, /* @__PURE__ */ React9.createElement(
|
|
1030
|
+
SelectInput,
|
|
1031
|
+
{
|
|
1032
|
+
items: featureOptions.map((opt) => ({
|
|
1033
|
+
...opt,
|
|
1034
|
+
label: selectedFeatures.includes(opt.value) && opt.value !== "done" ? `\u2713 ${opt.label}` : opt.label
|
|
1035
|
+
})),
|
|
1036
|
+
onSelect: handleFeatureSelect
|
|
1037
|
+
}
|
|
1038
|
+
)), /* @__PURE__ */ React9.createElement(Box9, { marginTop: 1 }, /* @__PURE__ */ React9.createElement(Text9, { dimColor: true }, "Use \u2191\u2193 to navigate, Enter to toggle/continue"))), step === "llm-provider" && /* @__PURE__ */ React9.createElement(Box9, { flexDirection: "column" }, /* @__PURE__ */ React9.createElement(Text9, { bold: true }, "Select LLM provider:"), /* @__PURE__ */ React9.createElement(Box9, { marginTop: 1 }, /* @__PURE__ */ React9.createElement(SelectInput, { items: llmProviderOptions, onSelect: handleLlmProviderSelect })), /* @__PURE__ */ React9.createElement(Box9, { marginTop: 1 }, /* @__PURE__ */ React9.createElement(Text9, { dimColor: true }, "Use \u2191\u2193 to navigate, Enter to select"))), step === "llm-model" && /* @__PURE__ */ React9.createElement(Box9, { flexDirection: "column" }, /* @__PURE__ */ React9.createElement(Text9, { bold: true }, "Enter LLM model name:"), /* @__PURE__ */ React9.createElement(Box9, { marginTop: 1 }, /* @__PURE__ */ React9.createElement(Text9, { color: "cyan" }, "Model: "), /* @__PURE__ */ React9.createElement(
|
|
1039
|
+
TextInput,
|
|
1040
|
+
{
|
|
1041
|
+
value: llmModel,
|
|
1042
|
+
onChange: setLlmModel,
|
|
1043
|
+
onSubmit: handleLlmModelSubmit,
|
|
1044
|
+
placeholder: getModelPlaceholder()
|
|
1045
|
+
}
|
|
1046
|
+
)), /* @__PURE__ */ React9.createElement(Box9, { marginTop: 1 }, /* @__PURE__ */ React9.createElement(Text9, { dimColor: true }, "Leave empty for default, or press Enter to continue"))), step === "llm-api-key" && /* @__PURE__ */ React9.createElement(Box9, { flexDirection: "column" }, /* @__PURE__ */ React9.createElement(Text9, { bold: true }, "Enter API key:"), /* @__PURE__ */ React9.createElement(Box9, { marginTop: 1 }, /* @__PURE__ */ React9.createElement(Text9, { color: "cyan" }, "API Key: "), /* @__PURE__ */ React9.createElement(
|
|
1047
|
+
TextInput,
|
|
1048
|
+
{
|
|
1049
|
+
value: llmApiKey,
|
|
1050
|
+
onChange: setLlmApiKey,
|
|
1051
|
+
onSubmit: handleLlmApiKeySubmit,
|
|
1052
|
+
placeholder: getApiKeyPlaceholder(),
|
|
1053
|
+
mask: "*"
|
|
1054
|
+
}
|
|
1055
|
+
)), /* @__PURE__ */ React9.createElement(Box9, { marginTop: 1 }, /* @__PURE__ */ React9.createElement(Text9, { dimColor: true }, "Your API key will not be stored"))), step === "llm-base-url" && /* @__PURE__ */ React9.createElement(Box9, { flexDirection: "column" }, /* @__PURE__ */ React9.createElement(Text9, { bold: true }, "Enter API base URL (optional):"), /* @__PURE__ */ React9.createElement(Box9, { marginTop: 1 }, /* @__PURE__ */ React9.createElement(Text9, { color: "cyan" }, "Base URL: "), /* @__PURE__ */ React9.createElement(
|
|
1056
|
+
TextInput,
|
|
1057
|
+
{
|
|
1058
|
+
value: llmBaseUrl,
|
|
1059
|
+
onChange: setLlmBaseUrl,
|
|
1060
|
+
onSubmit: handleLlmBaseUrlSubmit,
|
|
1061
|
+
placeholder: llmProvider === "ollama" ? "http://localhost:11434" : "https://api.example.com"
|
|
1062
|
+
}
|
|
1063
|
+
)), /* @__PURE__ */ React9.createElement(Box9, { marginTop: 1 }, /* @__PURE__ */ React9.createElement(Text9, { dimColor: true }, "Press Enter to continue (leave empty for default)"))));
|
|
1064
|
+
};
|
|
1065
|
+
|
|
1066
|
+
// src/commands/audit.ts
|
|
1067
|
+
function auditCommand(program2) {
|
|
1068
|
+
program2.command("audit").description("Audit a website for AI readiness").argument("<url>", "URL to audit").option("-o, --output <format>", "Output format: json, html, pdf, lhr, csv, interactive", "interactive").option("-r, --rules <preset>", "Rule preset: default, strict, minimal", "default").option("-d, --depth <number>", "Crawl depth (for multi-page audits)", parseInt, 1).option("-p, --pages <urls>", "Comma-separated list of specific pages to audit").option("--cache-ttl <seconds>", "Cache TTL in seconds to avoid re-fetching", parseInt).option("--threshold <score>", "Minimum score threshold (exit 1 if below)", parseInt).option("--max-chunk-tokens <number>", "Maximum tokens per content chunk", parseInt, 1200).option("--chunking-strategy <strategy>", "Chunking strategy: auto, heading-based, paragraph-based", "auto").option("--enable-chunking", "Enable detailed content chunking analysis", false).option("--enable-extractability", "Enable extractability mapping", false).option("--enable-hallucination", "Enable hallucination detection", false).option("--enable-llm", "Enable LLM comprehension analysis", false).option("--min-impact <number>", "Minimum impact score to include", parseInt, 8).option("--min-confidence <number>", "Minimum confidence to include (0-1)", parseFloat, 0.7).option("--max-issues <number>", "Maximum issues to return", parseInt, 20).option("--llm-provider <provider>", "LLM provider: openai, anthropic, ollama, local").option("--llm-model <model>", "LLM model name").option("--llm-base-url <url>", "LLM API base URL").option("--llm-api-key <key>", "LLM API key").action(async (url, options) => {
|
|
1069
|
+
const hasFeatureFlags = options.enableChunking || options.enableExtractability || options.enableHallucination || options.enableLlm || options.llmProvider;
|
|
1070
|
+
if (options.output === "interactive" && !hasFeatureFlags) {
|
|
1071
|
+
const originalConsoleError = console.error;
|
|
1072
|
+
const originalConsoleWarn = console.warn;
|
|
1073
|
+
console.error = () => {
|
|
1074
|
+
};
|
|
1075
|
+
console.warn = () => {
|
|
1076
|
+
};
|
|
1077
|
+
let auditConfig = null;
|
|
1078
|
+
const wizardRender = render(
|
|
1079
|
+
React10.createElement(SetupWizard, {
|
|
1080
|
+
initialUrl: url,
|
|
1081
|
+
onComplete: (config) => {
|
|
1082
|
+
auditConfig = config;
|
|
1083
|
+
}
|
|
1084
|
+
})
|
|
1085
|
+
);
|
|
1086
|
+
await wizardRender.waitUntilExit();
|
|
1087
|
+
if (!auditConfig) {
|
|
1088
|
+
console.error = originalConsoleError;
|
|
1089
|
+
console.warn = originalConsoleWarn;
|
|
1090
|
+
console.log("\nAudit cancelled.");
|
|
1091
|
+
process.exit(0);
|
|
1092
|
+
}
|
|
1093
|
+
url = auditConfig.url;
|
|
1094
|
+
options.enableChunking = auditConfig.enableChunking;
|
|
1095
|
+
options.enableExtractability = auditConfig.enableExtractability;
|
|
1096
|
+
options.enableHallucination = auditConfig.enableHallucination;
|
|
1097
|
+
options.enableLlm = auditConfig.enableLlm;
|
|
1098
|
+
options.llmProvider = auditConfig.llmProvider;
|
|
1099
|
+
options.llmModel = auditConfig.llmModel;
|
|
1100
|
+
options.llmApiKey = auditConfig.llmApiKey;
|
|
1101
|
+
options.llmBaseUrl = auditConfig.llmBaseUrl;
|
|
1102
|
+
}
|
|
1103
|
+
if (options.output === "interactive") {
|
|
1104
|
+
const originalConsoleError = console.error;
|
|
1105
|
+
const originalConsoleWarn = console.warn;
|
|
1106
|
+
console.error = () => {
|
|
1107
|
+
};
|
|
1108
|
+
console.warn = () => {
|
|
1109
|
+
};
|
|
1110
|
+
const { waitUntilExit, clear, rerender } = render(
|
|
1111
|
+
React10.createElement(AuditReportUI, {
|
|
1112
|
+
url,
|
|
1113
|
+
result: {},
|
|
1114
|
+
aiReadiness: {},
|
|
1115
|
+
loading: true,
|
|
1116
|
+
currentStep: "Starting audit..."
|
|
1117
|
+
})
|
|
1118
|
+
);
|
|
1119
|
+
try {
|
|
1120
|
+
const urlObj = new URL(url);
|
|
1121
|
+
const scanOptions = {
|
|
1122
|
+
maxChunkTokens: options.maxChunkTokens,
|
|
1123
|
+
chunkingStrategy: options.chunkingStrategy,
|
|
1124
|
+
enableChunking: options.enableChunking,
|
|
1125
|
+
enableExtractability: options.enableExtractability,
|
|
1126
|
+
enableHallucinationDetection: options.enableHallucination,
|
|
1127
|
+
enableLLM: options.enableLlm,
|
|
1128
|
+
minImpactScore: options.minImpact,
|
|
1129
|
+
minConfidence: options.minConfidence,
|
|
1130
|
+
maxIssues: options.maxIssues
|
|
1131
|
+
};
|
|
1132
|
+
if (options.enableLlm && options.llmProvider) {
|
|
1133
|
+
scanOptions.llmConfig = {
|
|
1134
|
+
provider: options.llmProvider,
|
|
1135
|
+
model: options.llmModel,
|
|
1136
|
+
baseUrl: options.llmBaseUrl,
|
|
1137
|
+
apiKey: options.llmApiKey
|
|
1138
|
+
};
|
|
1139
|
+
}
|
|
1140
|
+
rerender(
|
|
1141
|
+
React10.createElement(AuditReportUI, {
|
|
1142
|
+
url: urlObj.href,
|
|
1143
|
+
result: {},
|
|
1144
|
+
aiReadiness: {},
|
|
1145
|
+
loading: true,
|
|
1146
|
+
currentStep: "Scanning page..."
|
|
1147
|
+
})
|
|
1148
|
+
);
|
|
1149
|
+
const result = await analyzeUrlWithRules(url, scanOptions);
|
|
1150
|
+
rerender(
|
|
1151
|
+
React10.createElement(AuditReportUI, {
|
|
1152
|
+
url: urlObj.href,
|
|
1153
|
+
result,
|
|
1154
|
+
aiReadiness: {},
|
|
1155
|
+
loading: true,
|
|
1156
|
+
currentStep: "Calculating AI readiness scores..."
|
|
1157
|
+
})
|
|
1158
|
+
);
|
|
1159
|
+
const aiReadiness = calculateAIReadiness(result);
|
|
1160
|
+
clear();
|
|
1161
|
+
const finalRender = render(
|
|
1162
|
+
React10.createElement(AuditReportUI, {
|
|
1163
|
+
url: urlObj.href,
|
|
1164
|
+
result,
|
|
1165
|
+
aiReadiness,
|
|
1166
|
+
loading: false
|
|
1167
|
+
})
|
|
1168
|
+
);
|
|
1169
|
+
await finalRender.waitUntilExit();
|
|
1170
|
+
console.error = originalConsoleError;
|
|
1171
|
+
console.warn = originalConsoleWarn;
|
|
1172
|
+
if (options.threshold !== void 0) {
|
|
1173
|
+
const overallScore = aiReadiness.overall;
|
|
1174
|
+
if (overallScore !== void 0 && overallScore < options.threshold) {
|
|
1175
|
+
process.exit(1);
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
} catch (error) {
|
|
1179
|
+
console.error = originalConsoleError;
|
|
1180
|
+
console.warn = originalConsoleWarn;
|
|
1181
|
+
clear();
|
|
1182
|
+
console.log("\n" + chalk2.bold.red("\u274C Audit Failed"));
|
|
1183
|
+
console.log(chalk2.red("\u2500".repeat(70)));
|
|
1184
|
+
console.log(chalk2.red(error instanceof Error ? error.message : String(error)));
|
|
1185
|
+
console.log("\n" + chalk2.dim("Please check the URL and your configuration."));
|
|
1186
|
+
process.exit(1);
|
|
1187
|
+
}
|
|
1188
|
+
return;
|
|
1189
|
+
}
|
|
1190
|
+
const spinner = ora("Starting audit...").start();
|
|
1191
|
+
try {
|
|
1192
|
+
const urlObj = new URL(url);
|
|
1193
|
+
spinner.text = `Auditing ${chalk2.cyan(urlObj.href)}...`;
|
|
1194
|
+
const scanOptions = {
|
|
1195
|
+
maxChunkTokens: options.maxChunkTokens,
|
|
1196
|
+
chunkingStrategy: options.chunkingStrategy,
|
|
1197
|
+
enableChunking: options.enableChunking,
|
|
1198
|
+
enableExtractability: options.enableExtractability,
|
|
1199
|
+
enableHallucinationDetection: options.enableHallucination,
|
|
1200
|
+
enableLLM: options.enableLlm,
|
|
1201
|
+
minImpactScore: options.minImpact,
|
|
1202
|
+
minConfidence: options.minConfidence,
|
|
1203
|
+
maxIssues: options.maxIssues
|
|
1204
|
+
};
|
|
1205
|
+
if (options.enableLlm && options.llmProvider) {
|
|
1206
|
+
scanOptions.llmConfig = {
|
|
1207
|
+
provider: options.llmProvider,
|
|
1208
|
+
model: options.llmModel,
|
|
1209
|
+
baseUrl: options.llmBaseUrl,
|
|
1210
|
+
apiKey: options.llmApiKey
|
|
1211
|
+
};
|
|
1212
|
+
}
|
|
1213
|
+
spinner.text = "Scanning page...";
|
|
1214
|
+
const result = await analyzeUrlWithRules(url, scanOptions);
|
|
1215
|
+
spinner.text = "Calculating scores...";
|
|
1216
|
+
const aiReadiness = calculateAIReadiness(result);
|
|
1217
|
+
spinner.text = "Generating report...";
|
|
1218
|
+
const auditReportJson = exportAuditReport(result);
|
|
1219
|
+
const auditReport = JSON.parse(auditReportJson);
|
|
1220
|
+
const outputDir = resolve(process.cwd(), ".ai-lighthouse");
|
|
1221
|
+
if (!existsSync(outputDir)) {
|
|
1222
|
+
await mkdir(outputDir, { recursive: true });
|
|
1223
|
+
}
|
|
1224
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/:/g, "-").split(".")[0];
|
|
1225
|
+
const baseFilename = `audit_${new URL(url).hostname}_${timestamp}`;
|
|
1226
|
+
if (options.output === "json") {
|
|
1227
|
+
const jsonPath = join(outputDir, `${baseFilename}.json`);
|
|
1228
|
+
await writeFile(jsonPath, auditReportJson);
|
|
1229
|
+
spinner.succeed(chalk2.green("Audit complete!"));
|
|
1230
|
+
console.log(chalk2.dim(`Report saved to: ${jsonPath}`));
|
|
1231
|
+
} else if (options.output === "html") {
|
|
1232
|
+
const htmlPath = join(outputDir, `${baseFilename}.html`);
|
|
1233
|
+
const html = generateHTMLReport(auditReport, aiReadiness, result);
|
|
1234
|
+
await writeFile(htmlPath, html);
|
|
1235
|
+
spinner.succeed(chalk2.green("Audit complete!"));
|
|
1236
|
+
console.log(chalk2.dim(`HTML report saved to: ${htmlPath}`));
|
|
1237
|
+
console.log(chalk2.dim(`\u{1F4A1} Tip: Open the HTML file and use your browser's "Print > Save as PDF" to export as PDF`));
|
|
1238
|
+
} else if (options.output === "pdf") {
|
|
1239
|
+
spinner.text = "Generating PDF...";
|
|
1240
|
+
const pdfPath = join(outputDir, `${baseFilename}.pdf`);
|
|
1241
|
+
const html = generateHTMLReport(auditReport, aiReadiness, result);
|
|
1242
|
+
const file = { content: html };
|
|
1243
|
+
const pdfOptions = {
|
|
1244
|
+
format: "A4",
|
|
1245
|
+
printBackground: false
|
|
1246
|
+
};
|
|
1247
|
+
const pdfBuffer = await html_to_pdf.generatePdf(file, pdfOptions);
|
|
1248
|
+
await writeFile(pdfPath, pdfBuffer);
|
|
1249
|
+
spinner.succeed(chalk2.green("PDF generated!"));
|
|
1250
|
+
console.log(chalk2.dim(`PDF report saved to: ${pdfPath}`));
|
|
1251
|
+
} else if (options.output === "lhr") {
|
|
1252
|
+
const lhrPath = join(outputDir, `${baseFilename}.lhr.json`);
|
|
1253
|
+
const lhr = convertToLighthouseFormat(auditReport);
|
|
1254
|
+
await writeFile(lhrPath, JSON.stringify(lhr, null, 2));
|
|
1255
|
+
spinner.succeed(chalk2.green("Audit complete!"));
|
|
1256
|
+
console.log(chalk2.dim(`Lighthouse-compatible report saved to: ${lhrPath}`));
|
|
1257
|
+
} else if (options.output === "csv") {
|
|
1258
|
+
const csvPath = join(outputDir, `${baseFilename}.csv`);
|
|
1259
|
+
const csv = generateCSVReport(auditReport);
|
|
1260
|
+
await writeFile(csvPath, csv);
|
|
1261
|
+
spinner.succeed(chalk2.green("Audit complete!"));
|
|
1262
|
+
console.log(chalk2.dim(`CSV report saved to: ${csvPath}`));
|
|
1263
|
+
}
|
|
1264
|
+
console.log("\n" + chalk2.bold("\u{1F4CA} AI Readiness Summary"));
|
|
1265
|
+
console.log(formatAIReadinessReport(aiReadiness));
|
|
1266
|
+
console.log("\n" + chalk2.bold("\u{1F4C8} Technical Scores"));
|
|
1267
|
+
console.log(generateScoringSummary(result.scoring));
|
|
1268
|
+
console.log("\n" + chalk2.bold("\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550"));
|
|
1269
|
+
console.log(chalk2.bold.cyan(" COMPREHENSIVE ANALYSIS "));
|
|
1270
|
+
console.log(chalk2.bold("\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550"));
|
|
1271
|
+
console.log(formatComprehensiveReport(result, aiReadiness));
|
|
1272
|
+
if (result.issues && result.issues.length > 0) {
|
|
1273
|
+
console.log("\n" + formatDetailedIssues(result.issues));
|
|
1274
|
+
}
|
|
1275
|
+
if (options.threshold !== void 0) {
|
|
1276
|
+
const overallScore = auditReport?.scores?.overall;
|
|
1277
|
+
if (overallScore !== void 0 && overallScore < options.threshold) {
|
|
1278
|
+
console.log(chalk2.red(`
|
|
1279
|
+
\u274C Score ${overallScore} is below threshold ${options.threshold}`));
|
|
1280
|
+
process.exit(1);
|
|
1281
|
+
} else if (overallScore !== void 0) {
|
|
1282
|
+
console.log(chalk2.green(`
|
|
1283
|
+
\u2705 Score ${overallScore} meets threshold ${options.threshold}`));
|
|
1284
|
+
}
|
|
1285
|
+
}
|
|
1286
|
+
} catch (error) {
|
|
1287
|
+
spinner.fail(chalk2.red("Audit failed"));
|
|
1288
|
+
if (error instanceof Error) {
|
|
1289
|
+
console.error(chalk2.red(error.message));
|
|
1290
|
+
}
|
|
1291
|
+
process.exit(1);
|
|
1292
|
+
}
|
|
1293
|
+
});
|
|
1294
|
+
}
|
|
1295
|
+
function generateHTMLReport(report, aiReadiness, scanResult) {
|
|
1296
|
+
return `<!DOCTYPE html>
|
|
1297
|
+
<html lang="en">
|
|
1298
|
+
<head>
|
|
1299
|
+
<meta charset="UTF-8">
|
|
1300
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
1301
|
+
<title>AI Lighthouse Report - ${report.input?.requested_url}</title>
|
|
1302
|
+
<style>
|
|
1303
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
1304
|
+
body {
|
|
1305
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
|
|
1306
|
+
line-height: 1.6;
|
|
1307
|
+
color: #333;
|
|
1308
|
+
background: #f5f5f5;
|
|
1309
|
+
padding: 20px;
|
|
1310
|
+
}
|
|
1311
|
+
.container { color: #333; max-width: 1200px; margin: 0 auto; background: white; padding: 40px; border-radius: 8px; }
|
|
1312
|
+
h1 { color: #2563eb; margin-bottom: 10px; font-size: 2em; }
|
|
1313
|
+
h2 { color: #1e40af; margin-top: 30px; margin-bottom: 15px; border-bottom: 2px solid #dbeafe; padding-bottom: 10px; }
|
|
1314
|
+
h3 { color: #1e40af; margin-top: 20px; margin-bottom: 10px; font-size: 1.2em; }
|
|
1315
|
+
.header { margin-bottom: 30px; }
|
|
1316
|
+
.url { color: #64748b; font-size: 0.95em; }
|
|
1317
|
+
.timestamp { color: #94a3b8; font-size: 0.85em; }
|
|
1318
|
+
|
|
1319
|
+
.ai-readiness-banner {
|
|
1320
|
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
1321
|
+
color: white;
|
|
1322
|
+
padding: 30px;
|
|
1323
|
+
border-radius: 8px;
|
|
1324
|
+
margin: 20px 0;
|
|
1325
|
+
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
|
|
1326
|
+
}
|
|
1327
|
+
.ai-readiness-banner h2 { color: white; border: none; margin: 0 0 20px 0; }
|
|
1328
|
+
.overall-score { font-size: 3em; font-weight: bold; margin: 10px 0; }
|
|
1329
|
+
.grade-badge { background: rgba(255,255,255,0.2); padding: 8px 16px; border-radius: 20px; display: inline-block; margin: 10px 0; }
|
|
1330
|
+
.agent-perspective { background: rgba(255,255,255,0.1); padding: 20px; border-radius: 8px; margin: 20px 0; }
|
|
1331
|
+
.agent-status { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 15px; margin: 15px 0; }
|
|
1332
|
+
.agent-status-item { display: flex; align-items: center; gap: 10px; }
|
|
1333
|
+
|
|
1334
|
+
.scores { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 20px; margin: 20px 0; }
|
|
1335
|
+
.score-card {
|
|
1336
|
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
1337
|
+
color: white;
|
|
1338
|
+
padding: 20px;
|
|
1339
|
+
border-radius: 8px;
|
|
1340
|
+
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
|
|
1341
|
+
}
|
|
1342
|
+
.score-card h3 { font-size: 0.9em; opacity: 0.9; margin-bottom: 10px; }
|
|
1343
|
+
.score-value { font-size: 2.5em; font-weight: bold; }
|
|
1344
|
+
.grade { font-size: 1.2em; opacity: 0.8; margin-left: 10px; }
|
|
1345
|
+
|
|
1346
|
+
.dimensions { display: grid; gap: 15px; margin: 20px 0; }
|
|
1347
|
+
.dimension { background: #f8fafc; border-left: 4px solid #cbd5e1; padding: 20px; border-radius: 4px; }
|
|
1348
|
+
.dimension.excellent { border-left-color: #10b981; }
|
|
1349
|
+
.dimension.good { border-left-color: #84cc16; }
|
|
1350
|
+
.dimension.needs-work { border-left-color: #f59e0b; }
|
|
1351
|
+
.dimension.critical { border-left-color: #dc2626; }
|
|
1352
|
+
.dimension-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px; }
|
|
1353
|
+
.dimension-name { font-weight: 600; font-size: 1.1em; color: #1e293b; }
|
|
1354
|
+
.dimension-score { font-size: 1.5em; font-weight: bold; }
|
|
1355
|
+
.dimension-level { color: #64748b; font-size: 0.9em; margin-bottom: 10px; text-transform: uppercase; }
|
|
1356
|
+
.dimension-recommendations { margin-top: 10px; padding-left: 20px; }
|
|
1357
|
+
.dimension-recommendations li { margin: 5px 0; color: #475569; }
|
|
1358
|
+
|
|
1359
|
+
.quick-wins { background: #fffbeb; border: 2px solid #fbbf24; padding: 20px; border-radius: 8px; margin: 20px 0; }
|
|
1360
|
+
.quick-wins h3 { color: #92400e; margin-top: 0; }
|
|
1361
|
+
.quick-win { background: white; padding: 15px; margin: 10px 0; border-radius: 4px; border-left: 3px solid #f59e0b; }
|
|
1362
|
+
.quick-win-header { font-weight: 600; color: #1e293b; margin-bottom: 8px; }
|
|
1363
|
+
.quick-win-meta { font-size: 0.85em; color: #64748b; }
|
|
1364
|
+
|
|
1365
|
+
.priorities { margin: 20px 0; }
|
|
1366
|
+
.priority-section { background: #f8fafc; padding: 20px; border-radius: 8px; margin: 15px 0; }
|
|
1367
|
+
.priority-section.immediate { border-left: 4px solid #dc2626; }
|
|
1368
|
+
.priority-section.short-term { border-left: 4px solid #f59e0b; }
|
|
1369
|
+
.priority-section.long-term { border-left: 4px solid #3b82f6; }
|
|
1370
|
+
.priority-item { margin: 10px 0; padding-left: 20px; }
|
|
1371
|
+
|
|
1372
|
+
.issues { margin: 20px 0; }
|
|
1373
|
+
.issue {
|
|
1374
|
+
background: #f8fafc;
|
|
1375
|
+
border-left: 4px solid #cbd5e1;
|
|
1376
|
+
padding: 15px;
|
|
1377
|
+
margin-bottom: 15px;
|
|
1378
|
+
border-radius: 4px;
|
|
1379
|
+
}
|
|
1380
|
+
.issue.critical { border-left-color: #dc2626; background: #fef2f2; }
|
|
1381
|
+
.issue.high { border-left-color: #ea580c; background: #fff7ed; }
|
|
1382
|
+
.issue.medium { border-left-color: #f59e0b; background: #fffbeb; }
|
|
1383
|
+
.issue.low { border-left-color: #84cc16; background: #f7fee7; }
|
|
1384
|
+
.issue-title { font-weight: 600; color: #1e293b; margin-bottom: 8px; }
|
|
1385
|
+
.issue-meta { font-size: 0.85em; color: #64748b; margin-bottom: 8px; }
|
|
1386
|
+
.issue-desc { color: #475569; margin-bottom: 8px; }
|
|
1387
|
+
.issue-fix { color: #0f766e; background: #f0fdfa; padding: 10px; border-radius: 4px; font-size: 0.9em; }
|
|
1388
|
+
|
|
1389
|
+
.entity-list { display: grid; gap: 15px; }
|
|
1390
|
+
.entity { background: #f0f9ff; border: 1px solid #bae6fd; padding: 15px; border-radius: 4px; }
|
|
1391
|
+
.entity-name { font-weight: 600; color: #0c4a6e; }
|
|
1392
|
+
.entity-type { color: #0369a1; font-size: 0.85em; }
|
|
1393
|
+
|
|
1394
|
+
.print-button {
|
|
1395
|
+
background: #2563eb;
|
|
1396
|
+
color: white;
|
|
1397
|
+
padding: 12px 24px;
|
|
1398
|
+
border: none;
|
|
1399
|
+
border-radius: 6px;
|
|
1400
|
+
cursor: pointer;
|
|
1401
|
+
font-size: 1em;
|
|
1402
|
+
margin: 20px 0;
|
|
1403
|
+
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
|
1404
|
+
}
|
|
1405
|
+
.print-button:hover { background: #1e40af; }
|
|
1406
|
+
|
|
1407
|
+
@media print {
|
|
1408
|
+
body { background: white; padding: 0; }
|
|
1409
|
+
.container { box-shadow: none; }
|
|
1410
|
+
.print-button { display: none; }
|
|
1411
|
+
}
|
|
1412
|
+
</style>
|
|
1413
|
+
</head>
|
|
1414
|
+
<body>
|
|
1415
|
+
<div class="container">
|
|
1416
|
+
<div class="header">
|
|
1417
|
+
<h1>\u{1F6A8} AI Lighthouse Report</h1>
|
|
1418
|
+
<div class="url">${report.input?.requested_url}</div>
|
|
1419
|
+
<div class="timestamp">Generated: ${new Date(report.scanned_at).toLocaleString()}</div>
|
|
1420
|
+
</div>
|
|
1421
|
+
|
|
1422
|
+
${aiReadiness ? `
|
|
1423
|
+
<div class="ai-readiness-banner">
|
|
1424
|
+
<h2>\u{1F916} AI Readiness Assessment</h2>
|
|
1425
|
+
<div class="overall-score">${Math.round(aiReadiness.overall)}/100</div>
|
|
1426
|
+
<div class="grade-badge">Grade: ${aiReadiness.grade}</div>
|
|
1427
|
+
<div style="margin: 10px 0; opacity: 0.9;">
|
|
1428
|
+
Top ${aiReadiness.benchmark?.topPercentile || 0}% of sites \u2022
|
|
1429
|
+
Improvement potential: +${Math.round(aiReadiness.benchmark?.improvement || 0)} points to best-in-class
|
|
1430
|
+
</div>
|
|
1431
|
+
|
|
1432
|
+
<div class="agent-perspective">
|
|
1433
|
+
<strong>AI Agent Perspective:</strong>
|
|
1434
|
+
<div class="agent-status">
|
|
1435
|
+
<div class="agent-status-item">
|
|
1436
|
+
<span>${aiReadiness.aiPerspective?.canUnderstand ? "\u2705" : "\u274C"}</span>
|
|
1437
|
+
<span>Can Understand</span>
|
|
1438
|
+
</div>
|
|
1439
|
+
<div class="agent-status-item">
|
|
1440
|
+
<span>${aiReadiness.aiPerspective?.canExtract ? "\u2705" : "\u274C"}</span>
|
|
1441
|
+
<span>Can Extract</span>
|
|
1442
|
+
</div>
|
|
1443
|
+
<div class="agent-status-item">
|
|
1444
|
+
<span>${aiReadiness.aiPerspective?.canIndex ? "\u2705" : "\u274C"}</span>
|
|
1445
|
+
<span>Can Index</span>
|
|
1446
|
+
</div>
|
|
1447
|
+
<div class="agent-status-item">
|
|
1448
|
+
<span>${aiReadiness.aiPerspective?.canAnswer ? "\u2705" : "\u274C"}</span>
|
|
1449
|
+
<span>Can Answer Questions</span>
|
|
1450
|
+
</div>
|
|
1451
|
+
</div>
|
|
1452
|
+
<div style="margin-top: 15px;">
|
|
1453
|
+
<strong>Confidence Level:</strong> ${Math.round((aiReadiness.aiPerspective?.confidence || 0) * 100)}%
|
|
1454
|
+
</div>
|
|
1455
|
+
</div>
|
|
1456
|
+
|
|
1457
|
+
${(aiReadiness.aiPerspective?.mainBlockers || []).length > 0 ? `
|
|
1458
|
+
<div style="margin-top: 20px;">
|
|
1459
|
+
<strong>Main Blockers:</strong>
|
|
1460
|
+
<ul style="margin: 10px 0; padding-left: 20px;">
|
|
1461
|
+
${(aiReadiness.aiPerspective?.mainBlockers || []).map((blocker) => `<li>${blocker}</li>`).join("")}
|
|
1462
|
+
</ul>
|
|
1463
|
+
</div>
|
|
1464
|
+
` : ""}
|
|
1465
|
+
</div>
|
|
1466
|
+
` : ""}
|
|
1467
|
+
|
|
1468
|
+
<h2>\u{1F4CA} Technical Scores</h2>
|
|
1469
|
+
<div class="scores">
|
|
1470
|
+
<div class="score-card">
|
|
1471
|
+
<h3>Overall Score</h3>
|
|
1472
|
+
<div>
|
|
1473
|
+
<span class="score-value">${report.scores?.overall || 0}</span>
|
|
1474
|
+
<span class="grade">${getLetterGrade(report.scores?.overall || 0)}</span>
|
|
1475
|
+
</div>
|
|
1476
|
+
</div>
|
|
1477
|
+
<div class="score-card">
|
|
1478
|
+
<h3>AI Readiness</h3>
|
|
1479
|
+
<div class="score-value">${report.scores?.ai_readiness || 0}</div>
|
|
1480
|
+
</div>
|
|
1481
|
+
<div class="score-card">
|
|
1482
|
+
<h3>Crawlability</h3>
|
|
1483
|
+
<div class="score-value">${report.scores?.crawlability || 0}</div>
|
|
1484
|
+
</div>
|
|
1485
|
+
<div class="score-card">
|
|
1486
|
+
<h3>Content Clarity</h3>
|
|
1487
|
+
<div class="score-value">${report.scores?.content_clarity || 0}</div>
|
|
1488
|
+
</div>
|
|
1489
|
+
<div class="score-card">
|
|
1490
|
+
<h3>Schema Coverage</h3>
|
|
1491
|
+
<div class="score-value">${report.scores?.schema_coverage || 0}</div>
|
|
1492
|
+
</div>
|
|
1493
|
+
<div class="score-card">
|
|
1494
|
+
<h3>Structure</h3>
|
|
1495
|
+
<div class="score-value">${report.scores?.structure || 0}</div>
|
|
1496
|
+
</div>
|
|
1497
|
+
</div>
|
|
1498
|
+
|
|
1499
|
+
${aiReadiness?.dimensions ? `
|
|
1500
|
+
<h2>\u{1F3AF} Dimension Analysis</h2>
|
|
1501
|
+
<div class="dimensions">
|
|
1502
|
+
${Object.entries(aiReadiness.dimensions).map(([key, dim]) => `
|
|
1503
|
+
<div class="dimension ${dim.status}">
|
|
1504
|
+
<div class="dimension-header">
|
|
1505
|
+
<div class="dimension-name">${getEmojiForDimension(key)} ${formatDimensionName2(key)}</div>
|
|
1506
|
+
<div class="dimension-score">${Math.round(dim.score)}/100</div>
|
|
1507
|
+
</div>
|
|
1508
|
+
<div class="dimension-level">${dim.status}</div>
|
|
1509
|
+
${dim.strengths && dim.strengths.length > 0 ? `
|
|
1510
|
+
<div><strong>Strengths:</strong> ${dim.strengths.join(", ")}</div>
|
|
1511
|
+
` : ""}
|
|
1512
|
+
${dim.weaknesses && dim.weaknesses.length > 0 ? `
|
|
1513
|
+
<div style="margin-top: 8px;"><strong>Weaknesses:</strong> ${dim.weaknesses.join(", ")}</div>
|
|
1514
|
+
` : ""}
|
|
1515
|
+
${dim.recommendation ? `
|
|
1516
|
+
<div class="dimension-recommendations">
|
|
1517
|
+
<div style="margin-top: 10px;">\u2192 ${dim.recommendation}</div>
|
|
1518
|
+
</div>
|
|
1519
|
+
` : ""}
|
|
1520
|
+
</div>
|
|
1521
|
+
`).join("")}
|
|
1522
|
+
</div>
|
|
1523
|
+
` : ""}
|
|
1524
|
+
|
|
1525
|
+
${aiReadiness?.quickWins && aiReadiness.quickWins.length > 0 ? `
|
|
1526
|
+
<div class="quick-wins">
|
|
1527
|
+
<h3>\u26A1 Quick Wins (High Impact, Low Effort)</h3>
|
|
1528
|
+
${aiReadiness.quickWins.slice(0, 5).map((win, idx) => `
|
|
1529
|
+
<div class="quick-win">
|
|
1530
|
+
<div class="quick-win-header">${idx + 1}. ${win.issue}</div>
|
|
1531
|
+
<div class="quick-win-meta">Impact: ${win.impact} | Effort: ${win.effort}</div>
|
|
1532
|
+
<div style="margin-top: 8px; color: #0f766e;">\u2192 ${win.fix}</div>
|
|
1533
|
+
</div>
|
|
1534
|
+
`).join("")}
|
|
1535
|
+
</div>
|
|
1536
|
+
` : ""}
|
|
1537
|
+
|
|
1538
|
+
${aiReadiness?.roadmap ? `
|
|
1539
|
+
<h2>\u{1F4CB} Priority Roadmap</h2>
|
|
1540
|
+
<div class="priorities">
|
|
1541
|
+
${aiReadiness.roadmap.immediate && aiReadiness.roadmap.immediate.length > 0 ? `
|
|
1542
|
+
<div class="priority-section immediate">
|
|
1543
|
+
<h3>\u{1F534} Immediate (< 1 day)</h3>
|
|
1544
|
+
${aiReadiness.roadmap.immediate.slice(0, 5).map((item) => `
|
|
1545
|
+
<div class="priority-item">\u2022 ${item}</div>
|
|
1546
|
+
`).join("")}
|
|
1547
|
+
</div>
|
|
1548
|
+
` : ""}
|
|
1549
|
+
|
|
1550
|
+
${aiReadiness.roadmap.shortTerm && aiReadiness.roadmap.shortTerm.length > 0 ? `
|
|
1551
|
+
<div class="priority-section short-term">
|
|
1552
|
+
<h3>\u{1F7E1} Short-term (1-7 days)</h3>
|
|
1553
|
+
${aiReadiness.roadmap.shortTerm.slice(0, 5).map((item) => `
|
|
1554
|
+
<div class="priority-item">\u2022 ${item}</div>
|
|
1555
|
+
`).join("")}
|
|
1556
|
+
</div>
|
|
1557
|
+
` : ""}
|
|
1558
|
+
|
|
1559
|
+
${aiReadiness.roadmap.longTerm && aiReadiness.roadmap.longTerm.length > 0 ? `
|
|
1560
|
+
<div class="priority-section long-term">
|
|
1561
|
+
<h3>\u{1F535} Long-term (> 7 days)</h3>
|
|
1562
|
+
${aiReadiness.roadmap.longTerm.slice(0, 5).map((item) => `
|
|
1563
|
+
<div class="priority-item">\u2022 ${item}</div>
|
|
1564
|
+
`).join("")}
|
|
1565
|
+
</div>
|
|
1566
|
+
` : ""}
|
|
1567
|
+
</div>
|
|
1568
|
+
` : ""}
|
|
1569
|
+
|
|
1570
|
+
${scanResult?.llm ? `
|
|
1571
|
+
<h2>\u{1F4DD} AI Understanding Analysis</h2>
|
|
1572
|
+
<div style="background: #f0f9ff; border: 2px solid #0ea5e9; padding: 20px; border-radius: 8px; margin: 20px 0;">
|
|
1573
|
+
<div style="margin-bottom: 15px;">
|
|
1574
|
+
<strong>Summary:</strong> ${scanResult.llm.summary || "N/A"}
|
|
1575
|
+
</div>
|
|
1576
|
+
${scanResult.llm.pageType ? `
|
|
1577
|
+
<div style="margin-bottom: 15px;">
|
|
1578
|
+
<strong>\u{1F4C4} Page Type:</strong> ${scanResult.llm.pageType}
|
|
1579
|
+
</div>
|
|
1580
|
+
` : ""}
|
|
1581
|
+
${scanResult.llm.keyTopics && scanResult.llm.keyTopics.length > 0 ? `
|
|
1582
|
+
<div style="margin-bottom: 15px;">
|
|
1583
|
+
<strong>\u{1F3F7}\uFE0F Key Topics:</strong> ${scanResult.llm.keyTopics.join(", ")}
|
|
1584
|
+
</div>
|
|
1585
|
+
` : ""}
|
|
1586
|
+
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 15px; margin-top: 15px;">
|
|
1587
|
+
${scanResult.llm.readingLevel ? `
|
|
1588
|
+
<div><strong>\u{1F4CA} Reading Level:</strong> ${scanResult.llm.readingLevel.description}</div>
|
|
1589
|
+
` : ""}
|
|
1590
|
+
${scanResult.llm.sentiment ? `
|
|
1591
|
+
<div><strong>\u{1F3AD} Sentiment:</strong> ${scanResult.llm.sentiment}</div>
|
|
1592
|
+
` : ""}
|
|
1593
|
+
${scanResult.llm.technicalDepth ? `
|
|
1594
|
+
<div><strong>\u{1F3AF} Technical Depth:</strong> ${scanResult.llm.technicalDepth}</div>
|
|
1595
|
+
` : ""}
|
|
1596
|
+
</div>
|
|
1597
|
+
|
|
1598
|
+
${scanResult.llm.pageTypeInsights && scanResult.llm.pageTypeInsights.length > 0 ? `
|
|
1599
|
+
<div style="margin-top: 20px; background: #dbeafe; padding: 15px; border-radius: 4px; border-left: 4px solid #0ea5e9;">
|
|
1600
|
+
<div style="font-weight: 600; margin-bottom: 10px;">\u{1F4A1} AI-Generated Insights for ${scanResult.llm.pageType || "This Page"}</div>
|
|
1601
|
+
<ul style="margin: 0; padding-left: 20px;">
|
|
1602
|
+
${scanResult.llm.pageTypeInsights.map((insight) => `<li style="margin: 5px 0;">${insight}</li>`).join("")}
|
|
1603
|
+
</ul>
|
|
1604
|
+
</div>
|
|
1605
|
+
` : ""}
|
|
1606
|
+
|
|
1607
|
+
|
|
1608
|
+
${scanResult.llm.topEntities && scanResult.llm.topEntities.length > 0 ? `
|
|
1609
|
+
<div style="margin-top: 20px;">
|
|
1610
|
+
<strong>\u{1F50D} Key Entities:</strong>
|
|
1611
|
+
<div style="margin-top: 10px; display: grid; gap: 10px;">
|
|
1612
|
+
${scanResult.llm.topEntities.slice(0, 5).map((entity) => `
|
|
1613
|
+
<div style="background: white; padding: 10px; border-radius: 4px; border-left: 3px solid #0ea5e9;">
|
|
1614
|
+
<strong>${entity.name}</strong> (${entity.type}) - ${Math.round((entity.relevance || 0) * 100)}% relevance
|
|
1615
|
+
</div>
|
|
1616
|
+
`).join("")}
|
|
1617
|
+
</div>
|
|
1618
|
+
</div>
|
|
1619
|
+
` : ""}
|
|
1620
|
+
|
|
1621
|
+
${scanResult.llm.questions && scanResult.llm.questions.length > 0 ? `
|
|
1622
|
+
<div style="margin-top: 20px;">
|
|
1623
|
+
<strong>\u2753 Key Questions AI Can Answer:</strong>
|
|
1624
|
+
<ol style="margin-top: 10px; padding-left: 25px;">
|
|
1625
|
+
${scanResult.llm.questions.slice(0, 5).map((q) => `
|
|
1626
|
+
<li style="margin: 8px 0;"><span style="text-transform: uppercase; font-size: 0.8em; background: #dbeafe; padding: 2px 6px; border-radius: 3px;">${q.difficulty}</span> ${q.question}</li>
|
|
1627
|
+
`).join("")}
|
|
1628
|
+
</ol>
|
|
1629
|
+
</div>
|
|
1630
|
+
` : ""}
|
|
1631
|
+
|
|
1632
|
+
${scanResult.llm.suggestedFAQ && scanResult.llm.suggestedFAQ.length > 0 ? `
|
|
1633
|
+
<div style="margin-top: 20px;">
|
|
1634
|
+
<strong>\u{1F4A1} Suggested FAQs:</strong>
|
|
1635
|
+
<div style="margin-top: 10px; display: grid; gap: 15px;">
|
|
1636
|
+
${scanResult.llm.suggestedFAQ.filter((f) => f.importance === "high").slice(0, 3).map((faq) => `
|
|
1637
|
+
<div style="background: white; padding: 15px; border-radius: 4px; border-left: 3px solid #f59e0b;">
|
|
1638
|
+
<div style="font-weight: 600; margin-bottom: 5px;">Q: ${faq.question}</div>
|
|
1639
|
+
<div style="color: #64748b;">A: ${faq.suggestedAnswer}</div>
|
|
1640
|
+
</div>
|
|
1641
|
+
`).join("")}
|
|
1642
|
+
</div>
|
|
1643
|
+
</div>
|
|
1644
|
+
` : ""}
|
|
1645
|
+
</div>
|
|
1646
|
+
` : ""}
|
|
1647
|
+
|
|
1648
|
+
${scanResult?.chunking ? `
|
|
1649
|
+
<h2>\u{1F4C4} Content Chunking Analysis</h2>
|
|
1650
|
+
<div style="background: #f0fdf4; border: 2px solid #10b981; padding: 20px; border-radius: 8px; margin: 20px 0;">
|
|
1651
|
+
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 15px;">
|
|
1652
|
+
<div><strong>Strategy:</strong> ${scanResult.chunking.chunkingStrategy}</div>
|
|
1653
|
+
<div><strong>Total Chunks:</strong> ${scanResult.chunking.totalChunks}</div>
|
|
1654
|
+
<div><strong>Avg Tokens/Chunk:</strong> ${scanResult.chunking.averageTokensPerChunk}</div>
|
|
1655
|
+
<div><strong>Avg Noise:</strong> ${(scanResult.chunking.averageNoiseRatio * 100).toFixed(1)}%</div>
|
|
1656
|
+
</div>
|
|
1657
|
+
</div>
|
|
1658
|
+
` : ""}
|
|
1659
|
+
|
|
1660
|
+
${scanResult?.extractability ? `
|
|
1661
|
+
<h2>\u{1F504} Extractability Analysis</h2>
|
|
1662
|
+
<div style="background: #fef3c7; border: 2px solid #f59e0b; padding: 20px; border-radius: 8px; margin: 20px 0;">
|
|
1663
|
+
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 15px; margin-bottom: 15px;">
|
|
1664
|
+
<div><strong>Overall Score:</strong> ${scanResult.extractability.score.extractabilityScore}/100</div>
|
|
1665
|
+
<div><strong>Server-Rendered:</strong> ${scanResult.extractability.score.serverRenderedPercent}%</div>
|
|
1666
|
+
</div>
|
|
1667
|
+
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 15px;">
|
|
1668
|
+
<div><strong>Text Extractable:</strong> ${scanResult.extractability.contentTypes.text.percentage}%</div>
|
|
1669
|
+
<div><strong>Images Extractable:</strong> ${scanResult.extractability.contentTypes.images.percentage}%</div>
|
|
1670
|
+
</div>
|
|
1671
|
+
</div>
|
|
1672
|
+
` : ""}
|
|
1673
|
+
|
|
1674
|
+
${scanResult?.hallucinationReport ? `
|
|
1675
|
+
<h2>\u26A0\uFE0F Hallucination Risk Assessment</h2>
|
|
1676
|
+
<div style="background: #fef2f2; border: 2px solid #ef4444; padding: 20px; border-radius: 8px; margin: 20px 0;">
|
|
1677
|
+
<div style="font-size: 1.5em; font-weight: bold; margin-bottom: 15px;">
|
|
1678
|
+
Risk Score: ${scanResult.hallucinationReport.hallucinationRiskScore}/100
|
|
1679
|
+
</div>
|
|
1680
|
+
|
|
1681
|
+
${scanResult.hallucinationReport.factCheckSummary ? `
|
|
1682
|
+
<div style="margin-top: 20px; background: white; padding: 15px; border-radius: 4px;">
|
|
1683
|
+
<strong>\u{1F4CA} Fact Check Summary:</strong>
|
|
1684
|
+
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 10px; margin-top: 10px;">
|
|
1685
|
+
<div>\u2705 Verified: ${scanResult.hallucinationReport.factCheckSummary.verifiedFacts}</div>
|
|
1686
|
+
<div>\u2753 Unverified: ${scanResult.hallucinationReport.factCheckSummary.unverifiedFacts}</div>
|
|
1687
|
+
<div>\u26A0\uFE0F Contradictions: ${scanResult.hallucinationReport.factCheckSummary.contradictions}</div>
|
|
1688
|
+
<div>\u{1F914} Ambiguities: ${scanResult.hallucinationReport.factCheckSummary.ambiguities}</div>
|
|
1689
|
+
</div>
|
|
1690
|
+
</div>
|
|
1691
|
+
` : ""}
|
|
1692
|
+
|
|
1693
|
+
${scanResult.hallucinationReport.factCheckSummary && scanResult.hallucinationReport.factCheckSummary.unverifiedFacts > 0 ? `
|
|
1694
|
+
<div style="margin-top: 15px; background: #fef9c3; border-left: 4px solid #eab308; padding: 15px; border-radius: 4px;">
|
|
1695
|
+
<div style="display: flex; align-items: start; gap: 10px;">
|
|
1696
|
+
<span style="font-size: 1.5em;">\u{1F4A1}</span>
|
|
1697
|
+
<div>
|
|
1698
|
+
<div style="font-weight: 600; margin-bottom: 8px;">Why Some Facts Can't Be Verified</div>
|
|
1699
|
+
<div style="color: #854d0e; margin-bottom: 8px;">
|
|
1700
|
+
AI systems need external sources to verify claims. When information lacks citations, links, or
|
|
1701
|
+
references, it becomes difficult to confirm accuracy, increasing the risk of AI hallucination or misinformation.
|
|
1702
|
+
</div>
|
|
1703
|
+
<div style="font-weight: 600; margin-top: 12px; margin-bottom: 6px;">Best Practices:</div>
|
|
1704
|
+
<ul style="margin: 0; padding-left: 20px; color: #854d0e;">
|
|
1705
|
+
<li>Add source links directly in or near claim text</li>
|
|
1706
|
+
<li>Include dates for time-sensitive information</li>
|
|
1707
|
+
<li>Link to authoritative sources (research, official docs)</li>
|
|
1708
|
+
<li>Provide context for statistics and data points</li>
|
|
1709
|
+
<li>Use schema.org markup to specify citations</li>
|
|
1710
|
+
</ul>
|
|
1711
|
+
</div>
|
|
1712
|
+
</div>
|
|
1713
|
+
</div>
|
|
1714
|
+
` : ""}
|
|
1715
|
+
|
|
1716
|
+
${scanResult.hallucinationReport.triggers && scanResult.hallucinationReport.triggers.length > 0 ? `
|
|
1717
|
+
<div style="margin-top: 20px;">
|
|
1718
|
+
<strong>\u{1F6A8} Identified Triggers:</strong>
|
|
1719
|
+
<div style="margin-top: 10px; display: grid; gap: 10px;">
|
|
1720
|
+
${scanResult.hallucinationReport.triggers.filter((t) => t.severity === "high" || t.severity === "critical").slice(0, 5).map((trigger) => `
|
|
1721
|
+
<div style="background: white; padding: 15px; border-radius: 4px; border-left: 3px solid #dc2626;">
|
|
1722
|
+
<div style="font-weight: 600; text-transform: uppercase; font-size: 0.85em; color: #dc2626;">${trigger.type} - ${trigger.severity}</div>
|
|
1723
|
+
<div style="margin-top: 5px;">${trigger.description}</div>
|
|
1724
|
+
<div style="margin-top: 5px; font-size: 0.9em; color: #64748b;">Confidence: ${Math.round((trigger.confidence || 0) * 100)}%</div>
|
|
1725
|
+
</div>
|
|
1726
|
+
`).join("")}
|
|
1727
|
+
</div>
|
|
1728
|
+
</div>
|
|
1729
|
+
` : ""}
|
|
1730
|
+
|
|
1731
|
+
${scanResult.hallucinationReport.recommendations && scanResult.hallucinationReport.recommendations.length > 0 ? `
|
|
1732
|
+
<div style="margin-top: 20px;">
|
|
1733
|
+
<strong>\u{1F4A1} Recommendations:</strong>
|
|
1734
|
+
<ul style="margin-top: 10px; padding-left: 25px;">
|
|
1735
|
+
${scanResult.hallucinationReport.recommendations.slice(0, 3).map((rec) => `<li style="margin: 5px 0;">${rec}</li>`).join("")}
|
|
1736
|
+
</ul>
|
|
1737
|
+
</div>
|
|
1738
|
+
` : ""}
|
|
1739
|
+
</div>
|
|
1740
|
+
` : ""}
|
|
1741
|
+
|
|
1742
|
+
${scanResult?.mirrorReport ? `
|
|
1743
|
+
<h2>\u{1F50D} AI Misunderstanding Check</h2>
|
|
1744
|
+
<div style="background: #faf5ff; border: 2px solid #a855f7; padding: 20px; border-radius: 8px; margin: 20px 0;">
|
|
1745
|
+
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 15px; margin-bottom: 15px;">
|
|
1746
|
+
<div><strong>\u{1F3AF} Alignment Score:</strong> ${scanResult.mirrorReport.summary.alignmentScore}/100</div>
|
|
1747
|
+
<div><strong>\u{1F4D6} Clarity Score:</strong> ${scanResult.mirrorReport.summary.clarityScore}/100</div>
|
|
1748
|
+
<div><strong>\u26A0\uFE0F Critical Issues:</strong> ${scanResult.mirrorReport.summary.critical}</div>
|
|
1749
|
+
<div><strong>\u{1F7E1} Major Issues:</strong> ${scanResult.mirrorReport.summary.major}</div>
|
|
1750
|
+
</div>
|
|
1751
|
+
|
|
1752
|
+
${scanResult.mirrorReport.llmInterpretation ? `
|
|
1753
|
+
<div style="background: #dbeafe; padding: 16px; border-radius: 8px; margin-bottom: 16px; border: 1px solid #3b82f6;">
|
|
1754
|
+
<div style="font-weight: bold; margin-bottom: 12px; color: #1e40af;">
|
|
1755
|
+
\u{1F916} What AI Actually Understood (${Math.round(scanResult.mirrorReport.llmInterpretation.confidence * 100)}% confident)
|
|
1756
|
+
</div>
|
|
1757
|
+
|
|
1758
|
+
<div style="display: grid; grid-template-columns: repeat(2, 1fr); gap: 12px;">
|
|
1759
|
+
${scanResult.mirrorReport.llmInterpretation.productName ? `
|
|
1760
|
+
<div>
|
|
1761
|
+
<div style="font-size: 11px; font-weight: 600; color: #64748b; margin-bottom: 4px;">PRODUCT NAME</div>
|
|
1762
|
+
<div style="font-size: 14px;">${scanResult.mirrorReport.llmInterpretation.productName}</div>
|
|
1763
|
+
</div>
|
|
1764
|
+
` : ""}
|
|
1765
|
+
|
|
1766
|
+
${scanResult.mirrorReport.llmInterpretation.purpose ? `
|
|
1767
|
+
<div style="grid-column: 1 / -1;">
|
|
1768
|
+
<div style="font-size: 11px; font-weight: 600; color: #64748b; margin-bottom: 4px;">MAIN PURPOSE</div>
|
|
1769
|
+
<div style="font-size: 14px;">${scanResult.mirrorReport.llmInterpretation.purpose}</div>
|
|
1770
|
+
</div>
|
|
1771
|
+
` : ""}
|
|
1772
|
+
|
|
1773
|
+
${scanResult.mirrorReport.llmInterpretation.valueProposition ? `
|
|
1774
|
+
<div style="grid-column: 1 / -1;">
|
|
1775
|
+
<div style="font-size: 11px; font-weight: 600; color: #64748b; margin-bottom: 4px;">\u{1F48E} UNIQUE VALUE</div>
|
|
1776
|
+
<div style="font-size: 14px; font-weight: 600; color: #7c3aed;">${scanResult.mirrorReport.llmInterpretation.valueProposition}</div>
|
|
1777
|
+
</div>
|
|
1778
|
+
` : ""}
|
|
1779
|
+
|
|
1780
|
+
${scanResult.mirrorReport.llmInterpretation.keyBenefits && scanResult.mirrorReport.llmInterpretation.keyBenefits.length > 0 ? `
|
|
1781
|
+
<div>
|
|
1782
|
+
<div style="font-size: 11px; font-weight: 600; color: #64748b; margin-bottom: 4px;">KEY BENEFITS</div>
|
|
1783
|
+
<ul style="font-size: 14px; margin: 0; padding-left: 20px;">
|
|
1784
|
+
${scanResult.mirrorReport.llmInterpretation.keyBenefits.map((b) => `<li style="margin: 4px 0;">${b}</li>`).join("")}
|
|
1785
|
+
</ul>
|
|
1786
|
+
</div>
|
|
1787
|
+
` : ""}
|
|
1788
|
+
|
|
1789
|
+
${scanResult.mirrorReport.llmInterpretation.keyFeatures && scanResult.mirrorReport.llmInterpretation.keyFeatures.length > 0 ? `
|
|
1790
|
+
<div>
|
|
1791
|
+
<div style="font-size: 11px; font-weight: 600; color: #64748b; margin-bottom: 4px;">KEY FEATURES</div>
|
|
1792
|
+
<ul style="font-size: 14px; margin: 0; padding-left: 20px;">
|
|
1793
|
+
${scanResult.mirrorReport.llmInterpretation.keyFeatures.slice(0, 3).map((f) => `<li style="margin: 4px 0;">${f}</li>`).join("")}
|
|
1794
|
+
</ul>
|
|
1795
|
+
</div>
|
|
1796
|
+
` : ""}
|
|
1797
|
+
</div>
|
|
1798
|
+
</div>
|
|
1799
|
+
` : ""}
|
|
1800
|
+
|
|
1801
|
+
${scanResult.mirrorReport.mismatches && scanResult.mirrorReport.mismatches.length > 0 ? `
|
|
1802
|
+
<div style="margin-top: 20px;">
|
|
1803
|
+
<strong>Priority Mismatches:</strong>
|
|
1804
|
+
<div style="margin-top: 10px; display: grid; gap: 10px;">
|
|
1805
|
+
${scanResult.mirrorReport.mismatches.filter((m) => m.severity === "critical" || m.severity === "major").slice(0, 5).map((mismatch) => `
|
|
1806
|
+
<div style="background: white; padding: 15px; border-radius: 4px; border-left: 3px solid ${mismatch.severity === "critical" ? "#dc2626" : "#f59e0b"};">
|
|
1807
|
+
<div style="font-weight: 600;">${mismatch.severity === "critical" ? "\u{1F534}" : "\u{1F7E1}"} ${mismatch.field}</div>
|
|
1808
|
+
<div style="margin-top: 5px; color: #64748b;">${mismatch.description}</div>
|
|
1809
|
+
<div style="margin-top: 10px; padding: 10px; background: #f0fdfa; border-radius: 4px;">
|
|
1810
|
+
<strong>Fix:</strong> ${mismatch.recommendation}
|
|
1811
|
+
</div>
|
|
1812
|
+
</div>
|
|
1813
|
+
`).join("")}
|
|
1814
|
+
</div>
|
|
1815
|
+
</div>
|
|
1816
|
+
` : ""}
|
|
1817
|
+
|
|
1818
|
+
${scanResult.mirrorReport.recommendations && scanResult.mirrorReport.recommendations.length > 0 ? `
|
|
1819
|
+
<div style="margin-top: 20px;">
|
|
1820
|
+
<strong>\u{1F4A1} Top Recommendations:</strong>
|
|
1821
|
+
<ul style="margin-top: 10px; padding-left: 25px;">
|
|
1822
|
+
${scanResult.mirrorReport.recommendations.slice(0, 3).map((rec) => `<li style="margin: 5px 0;">${rec}</li>`).join("")}
|
|
1823
|
+
</ul>
|
|
1824
|
+
</div>
|
|
1825
|
+
` : ""}
|
|
1826
|
+
</div>
|
|
1827
|
+
` : ""}
|
|
1828
|
+
|
|
1829
|
+
<h2>\u26A0\uFE0F All Issues (${report.issues?.length || 0})</h2>
|
|
1830
|
+
<div class="issues">
|
|
1831
|
+
${(report.issues || []).map((issue) => `
|
|
1832
|
+
<div class="issue ${issue.severity}">
|
|
1833
|
+
<div class="issue-title">${issue.message}</div>
|
|
1834
|
+
<div class="issue-meta">
|
|
1835
|
+
<span style="text-transform: uppercase; font-weight: 600;">${issue.severity}</span>
|
|
1836
|
+
\xB7 Impact: ${issue.impact}
|
|
1837
|
+
\xB7 Category: ${issue.category}
|
|
1838
|
+
</div>
|
|
1839
|
+
<div class="issue-desc">${issue.evidence || "No additional evidence"}</div>
|
|
1840
|
+
<div class="issue-fix"><strong>Fix:</strong> ${issue.suggested_fix}</div>
|
|
1841
|
+
</div>
|
|
1842
|
+
`).join("")}
|
|
1843
|
+
</div>
|
|
1844
|
+
|
|
1845
|
+
${(report.entities || []).length > 0 ? `
|
|
1846
|
+
<h2>\u{1F3F7}\uFE0F Detected Entities (${(report.entities || []).length})</h2>
|
|
1847
|
+
<div class="entity-list">
|
|
1848
|
+
${(report.entities || []).map((entity) => `
|
|
1849
|
+
<div class="entity">
|
|
1850
|
+
<div class="entity-name">${entity.name}</div>
|
|
1851
|
+
<div class="entity-type">${entity.type} \xB7 Source: ${entity.source}</div>
|
|
1852
|
+
${entity.description ? `<div style="margin-top: 8px; color: #334155;">${entity.description}</div>` : ""}
|
|
1853
|
+
</div>
|
|
1854
|
+
`).join("")}
|
|
1855
|
+
</div>
|
|
1856
|
+
` : ""}
|
|
1857
|
+
</div>
|
|
1858
|
+
</body>
|
|
1859
|
+
</html>`;
|
|
1860
|
+
}
|
|
1861
|
+
function getLetterGrade(score) {
|
|
1862
|
+
if (score >= 90) return "A";
|
|
1863
|
+
if (score >= 80) return "B";
|
|
1864
|
+
if (score >= 70) return "C";
|
|
1865
|
+
if (score >= 60) return "D";
|
|
1866
|
+
return "F";
|
|
1867
|
+
}
|
|
1868
|
+
function getEmojiForDimension(name) {
|
|
1869
|
+
const emojiMap = {
|
|
1870
|
+
"contentQuality": "\u{1F4DD}",
|
|
1871
|
+
"discoverability": "\u{1F50D}",
|
|
1872
|
+
"extractability": "\u{1F504}",
|
|
1873
|
+
"comprehensibility": "\u{1F9E0}",
|
|
1874
|
+
"trustworthiness": "\u2705",
|
|
1875
|
+
"structured": "\u{1F4CA}",
|
|
1876
|
+
"semantic": "\u{1F3F7}\uFE0F"
|
|
1877
|
+
};
|
|
1878
|
+
return emojiMap[name] || "\u{1F4CC}";
|
|
1879
|
+
}
|
|
1880
|
+
function formatDimensionName2(key) {
|
|
1881
|
+
const nameMap = {
|
|
1882
|
+
"contentQuality": "Content Quality",
|
|
1883
|
+
"discoverability": "Discoverability",
|
|
1884
|
+
"extractability": "Extractability",
|
|
1885
|
+
"comprehensibility": "Comprehensibility",
|
|
1886
|
+
"trustworthiness": "Trustworthiness"
|
|
1887
|
+
};
|
|
1888
|
+
return nameMap[key] || key;
|
|
1889
|
+
}
|
|
1890
|
+
function convertToLighthouseFormat(report) {
|
|
1891
|
+
return {
|
|
1892
|
+
lighthouseVersion: "1.0.0",
|
|
1893
|
+
userAgent: "AI-Lighthouse/1.0.0",
|
|
1894
|
+
fetchTime: report.scanned_at,
|
|
1895
|
+
requestedUrl: report.input?.requested_url,
|
|
1896
|
+
finalUrl: report.input?.final_url,
|
|
1897
|
+
categories: {
|
|
1898
|
+
"ai-readiness": {
|
|
1899
|
+
id: "ai-readiness",
|
|
1900
|
+
title: "AI Readiness",
|
|
1901
|
+
score: (report.scores?.ai_readiness || 0) / 100
|
|
1902
|
+
},
|
|
1903
|
+
"crawlability": {
|
|
1904
|
+
id: "crawlability",
|
|
1905
|
+
title: "Crawlability",
|
|
1906
|
+
score: (report.scores?.crawlability || 0) / 100
|
|
1907
|
+
},
|
|
1908
|
+
"content-clarity": {
|
|
1909
|
+
id: "content-clarity",
|
|
1910
|
+
title: "Content Clarity",
|
|
1911
|
+
score: (report.scores?.content_clarity || 0) / 100
|
|
1912
|
+
}
|
|
1913
|
+
},
|
|
1914
|
+
audits: (report.issues || []).reduce((acc, issue, idx) => {
|
|
1915
|
+
acc[`issue-${idx}`] = {
|
|
1916
|
+
id: issue.id,
|
|
1917
|
+
title: issue.message,
|
|
1918
|
+
description: issue.evidence || "",
|
|
1919
|
+
score: issue.severity === "critical" ? 0 : issue.severity === "high" ? 0.25 : 0.5,
|
|
1920
|
+
displayValue: issue.suggested_fix
|
|
1921
|
+
};
|
|
1922
|
+
return acc;
|
|
1923
|
+
}, {})
|
|
1924
|
+
};
|
|
1925
|
+
}
|
|
1926
|
+
function generateCSVReport(report) {
|
|
1927
|
+
const headers = ["ID", "Severity", "Category", "Message", "Impact", "Suggested Fix"];
|
|
1928
|
+
const rows = (report.issues || []).map((issue) => [
|
|
1929
|
+
issue.id,
|
|
1930
|
+
issue.severity,
|
|
1931
|
+
issue.category,
|
|
1932
|
+
`"${issue.message.replace(/"/g, '""')}"`,
|
|
1933
|
+
issue.impact,
|
|
1934
|
+
`"${issue.suggested_fix.replace(/"/g, '""')}"`
|
|
1935
|
+
]);
|
|
1936
|
+
return [headers.join(","), ...rows.map((r) => r.join(","))].join("\n");
|
|
1937
|
+
}
|
|
1938
|
+
|
|
1939
|
+
// src/commands/crawl.ts
|
|
1940
|
+
import chalk3 from "chalk";
|
|
1941
|
+
import ora2 from "ora";
|
|
1942
|
+
import { analyzeUrlWithRules as analyzeUrlWithRules2 } from "@ai-lighthouse/scanner";
|
|
1943
|
+
import { writeFile as writeFile2, mkdir as mkdir2 } from "fs/promises";
|
|
1944
|
+
import { join as join2, resolve as resolve2 } from "path";
|
|
1945
|
+
import { existsSync as existsSync2 } from "fs";
|
|
1946
|
+
function crawlCommand(program2) {
|
|
1947
|
+
program2.command("crawl").description("Crawl and audit multiple pages from a website").argument("<url>", "Starting URL to crawl from").option("-d, --depth <number>", "Maximum crawl depth", (val) => parseInt(val, 10), 2).option("--sitemap", "Parse sitemap.xml for URLs", false).option("--max-pages <number>", "Maximum number of pages to crawl", (val) => parseInt(val, 10), 50).option("-o, --output <format>", "Output format: json, html", "json").option("--follow-external", "Follow external links", false).option("--respect-robots", "Respect robots.txt rules", true).action(async (url, options) => {
|
|
1948
|
+
const spinner = ora2("Starting crawl...").start();
|
|
1949
|
+
try {
|
|
1950
|
+
const urlObj = new URL(url);
|
|
1951
|
+
const baseUrl = `${urlObj.protocol}//${urlObj.host}`;
|
|
1952
|
+
spinner.text = "Discovering URLs...";
|
|
1953
|
+
let urlsToCrawl = [];
|
|
1954
|
+
if (options.sitemap) {
|
|
1955
|
+
const sitemapUrl = `${baseUrl}/sitemap.xml`;
|
|
1956
|
+
spinner.text = `Fetching sitemap from ${sitemapUrl}...`;
|
|
1957
|
+
urlsToCrawl = await parseSitemap(sitemapUrl);
|
|
1958
|
+
spinner.succeed(`Found ${urlsToCrawl.length} URLs in sitemap`);
|
|
1959
|
+
} else {
|
|
1960
|
+
urlsToCrawl = await crawlByDepth(url, options.depth, options.maxPages, options.followExternal);
|
|
1961
|
+
spinner.succeed(`Discovered ${urlsToCrawl.length} URLs`);
|
|
1962
|
+
}
|
|
1963
|
+
if (urlsToCrawl.length > options.maxPages) {
|
|
1964
|
+
urlsToCrawl = urlsToCrawl.slice(0, options.maxPages);
|
|
1965
|
+
}
|
|
1966
|
+
const results = [];
|
|
1967
|
+
for (let i = 0; i < urlsToCrawl.length; i++) {
|
|
1968
|
+
const pageUrl = urlsToCrawl[i];
|
|
1969
|
+
spinner.text = `Auditing ${i + 1}/${urlsToCrawl.length}: ${pageUrl}`;
|
|
1970
|
+
try {
|
|
1971
|
+
const result = await analyzeUrlWithRules2(pageUrl, {
|
|
1972
|
+
maxChunkTokens: 1200,
|
|
1973
|
+
enableChunking: false,
|
|
1974
|
+
enableExtractability: false,
|
|
1975
|
+
enableLLM: false,
|
|
1976
|
+
minImpactScore: 8
|
|
1977
|
+
});
|
|
1978
|
+
results.push(result);
|
|
1979
|
+
} catch (error) {
|
|
1980
|
+
console.error(chalk3.yellow(`
|
|
1981
|
+
\u26A0\uFE0F Failed to audit ${pageUrl}: ${error}`));
|
|
1982
|
+
}
|
|
1983
|
+
}
|
|
1984
|
+
spinner.text = "Generating crawl report...";
|
|
1985
|
+
const crawlReport = {
|
|
1986
|
+
crawl_id: generateId(),
|
|
1987
|
+
crawled_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1988
|
+
base_url: baseUrl,
|
|
1989
|
+
start_url: url,
|
|
1990
|
+
total_pages: results.length,
|
|
1991
|
+
crawl_depth: options.depth,
|
|
1992
|
+
pages: results,
|
|
1993
|
+
summary: {
|
|
1994
|
+
avgOverallScore: average(results.map((r) => r.scoring.overallScore)),
|
|
1995
|
+
avgAIReadinessScore: average(results.map((r) => r.scoring.categoryScores.find((c) => c.category === "AI Readiness")?.score || 0)),
|
|
1996
|
+
total_issues: results.reduce((sum, r) => sum + r.issues.length, 0),
|
|
1997
|
+
issues_by_severity: aggregateIssuesBySeverity(results)
|
|
1998
|
+
}
|
|
1999
|
+
};
|
|
2000
|
+
const outputDir = resolve2(process.cwd(), ".ai-lighthouse");
|
|
2001
|
+
if (!existsSync2(outputDir)) {
|
|
2002
|
+
await mkdir2(outputDir, { recursive: true });
|
|
2003
|
+
}
|
|
2004
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/:/g, "-").split(".")[0];
|
|
2005
|
+
const baseFilename = `crawl_${new URL(url).hostname}_${timestamp}`;
|
|
2006
|
+
if (options.output === "json") {
|
|
2007
|
+
const jsonPath = join2(outputDir, `${baseFilename}.json`);
|
|
2008
|
+
await writeFile2(jsonPath, JSON.stringify(crawlReport, null, 2));
|
|
2009
|
+
spinner.succeed(chalk3.green("Crawl complete!"));
|
|
2010
|
+
console.log(chalk3.dim(`Report saved to: ${jsonPath}`));
|
|
2011
|
+
} else if (options.output === "html") {
|
|
2012
|
+
const htmlPath = join2(outputDir, `${baseFilename}.html`);
|
|
2013
|
+
const html = generateCrawlHTML(crawlReport);
|
|
2014
|
+
await writeFile2(htmlPath, html);
|
|
2015
|
+
spinner.succeed(chalk3.green("Crawl complete!"));
|
|
2016
|
+
console.log(chalk3.dim(`HTML report saved to: ${htmlPath}`));
|
|
2017
|
+
}
|
|
2018
|
+
console.log("\n" + chalk3.bold("\u{1F310} Crawl Summary"));
|
|
2019
|
+
console.log(`Pages audited: ${chalk3.cyan(crawlReport.total_pages)}`);
|
|
2020
|
+
console.log(`Average overall score: ${chalk3.cyan(crawlReport.summary.avgOverallScore.toFixed(1))}`);
|
|
2021
|
+
console.log(`Average AI readiness: ${chalk3.cyan(crawlReport.summary.avgAIReadinessScore.toFixed(1))}`);
|
|
2022
|
+
console.log(`Total issues found: ${chalk3.yellow(crawlReport.summary.total_issues)}`);
|
|
2023
|
+
} catch (error) {
|
|
2024
|
+
spinner.fail(chalk3.red("Crawl failed"));
|
|
2025
|
+
if (error instanceof Error) {
|
|
2026
|
+
console.error(chalk3.red(error.message));
|
|
2027
|
+
}
|
|
2028
|
+
process.exit(1);
|
|
2029
|
+
}
|
|
2030
|
+
});
|
|
2031
|
+
}
|
|
2032
|
+
async function parseSitemap(sitemapUrl) {
|
|
2033
|
+
try {
|
|
2034
|
+
const response = await fetch(sitemapUrl);
|
|
2035
|
+
if (!response.ok) {
|
|
2036
|
+
throw new Error(`Failed to fetch sitemap: ${response.statusText}`);
|
|
2037
|
+
}
|
|
2038
|
+
const text = await response.text();
|
|
2039
|
+
const urlMatches = text.match(/<loc>(.*?)<\/loc>/g) || [];
|
|
2040
|
+
return urlMatches.map((match) => match.replace(/<\/?loc>/g, ""));
|
|
2041
|
+
} catch (error) {
|
|
2042
|
+
console.warn(chalk3.yellow(`Could not parse sitemap: ${error}`));
|
|
2043
|
+
return [];
|
|
2044
|
+
}
|
|
2045
|
+
}
|
|
2046
|
+
async function crawlByDepth(startUrl, maxDepth, maxPages, followExternal) {
|
|
2047
|
+
const visited = /* @__PURE__ */ new Set();
|
|
2048
|
+
const toVisit = [{ url: startUrl, depth: 0 }];
|
|
2049
|
+
const baseHost = new URL(startUrl).host;
|
|
2050
|
+
while (toVisit.length > 0 && visited.size < maxPages) {
|
|
2051
|
+
const { url, depth } = toVisit.shift();
|
|
2052
|
+
if (visited.has(url) || depth > maxDepth) {
|
|
2053
|
+
continue;
|
|
2054
|
+
}
|
|
2055
|
+
visited.add(url);
|
|
2056
|
+
if (depth < maxDepth) {
|
|
2057
|
+
try {
|
|
2058
|
+
const links = await extractLinks(url);
|
|
2059
|
+
for (const link of links) {
|
|
2060
|
+
const linkHost = new URL(link).host;
|
|
2061
|
+
if (followExternal || linkHost === baseHost) {
|
|
2062
|
+
if (!visited.has(link)) {
|
|
2063
|
+
toVisit.push({ url: link, depth: depth + 1 });
|
|
2064
|
+
}
|
|
2065
|
+
}
|
|
2066
|
+
}
|
|
2067
|
+
} catch (error) {
|
|
2068
|
+
}
|
|
2069
|
+
}
|
|
2070
|
+
}
|
|
2071
|
+
return Array.from(visited);
|
|
2072
|
+
}
|
|
2073
|
+
async function extractLinks(url) {
|
|
2074
|
+
try {
|
|
2075
|
+
const response = await fetch(url);
|
|
2076
|
+
const html = await response.text();
|
|
2077
|
+
const linkMatches = html.match(/href=["'](https?:\/\/[^"']+)["']/g) || [];
|
|
2078
|
+
return linkMatches.map((match) => match.match(/href=["'](https?:\/\/[^"']+)["']/)[1]).filter((link, idx, arr) => arr.indexOf(link) === idx);
|
|
2079
|
+
} catch {
|
|
2080
|
+
return [];
|
|
2081
|
+
}
|
|
2082
|
+
}
|
|
2083
|
+
function average(numbers) {
|
|
2084
|
+
if (numbers.length === 0) return 0;
|
|
2085
|
+
return numbers.reduce((sum, n) => sum + n, 0) / numbers.length;
|
|
2086
|
+
}
|
|
2087
|
+
function aggregateIssuesBySeverity(results) {
|
|
2088
|
+
const counts = {
|
|
2089
|
+
critical: 0,
|
|
2090
|
+
high: 0,
|
|
2091
|
+
medium: 0,
|
|
2092
|
+
low: 0,
|
|
2093
|
+
info: 0
|
|
2094
|
+
};
|
|
2095
|
+
for (const result of results) {
|
|
2096
|
+
for (const issue of result.issues) {
|
|
2097
|
+
counts[issue.severity] = (counts[issue.severity] || 0) + 1;
|
|
2098
|
+
}
|
|
2099
|
+
}
|
|
2100
|
+
return counts;
|
|
2101
|
+
}
|
|
2102
|
+
function generateId() {
|
|
2103
|
+
return `crawl_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
2104
|
+
}
|
|
2105
|
+
function generateCrawlHTML(report) {
|
|
2106
|
+
return `<!DOCTYPE html>
|
|
2107
|
+
<html lang="en">
|
|
2108
|
+
<head>
|
|
2109
|
+
<meta charset="UTF-8">
|
|
2110
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
2111
|
+
<title>Crawl Report - ${report.base_url}</title>
|
|
2112
|
+
<style>
|
|
2113
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
2114
|
+
body {
|
|
2115
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
2116
|
+
background: #f5f5f5;
|
|
2117
|
+
padding: 20px;
|
|
2118
|
+
}
|
|
2119
|
+
.container { max-width: 1400px; margin: 0 auto; }
|
|
2120
|
+
.header { background: white; padding: 30px; border-radius: 8px; margin-bottom: 20px; box-shadow: 0 2px 8px rgba(0,0,0,0.1); }
|
|
2121
|
+
h1 { color: #2563eb; margin-bottom: 10px; }
|
|
2122
|
+
.summary { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 15px; margin-top: 20px; }
|
|
2123
|
+
.stat { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 20px; border-radius: 8px; }
|
|
2124
|
+
.stat-value { font-size: 2em; font-weight: bold; }
|
|
2125
|
+
.stat-label { opacity: 0.9; font-size: 0.9em; }
|
|
2126
|
+
.pages { display: grid; gap: 15px; }
|
|
2127
|
+
.page-card { background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
|
|
2128
|
+
.page-url { color: #1e40af; font-weight: 600; margin-bottom: 10px; word-break: break-all; }
|
|
2129
|
+
.page-scores { display: flex; gap: 15px; margin-top: 10px; }
|
|
2130
|
+
.score-badge { background: #dbeafe; color: #1e40af; padding: 5px 12px; border-radius: 4px; font-size: 0.9em; }
|
|
2131
|
+
</style>
|
|
2132
|
+
</head>
|
|
2133
|
+
<body>
|
|
2134
|
+
<div class="container">
|
|
2135
|
+
<div class="header">
|
|
2136
|
+
<h1>\u{1F310} Crawl Report</h1>
|
|
2137
|
+
<div style="color: #64748b; margin-top: 5px;">${report.base_url}</div>
|
|
2138
|
+
<div style="color: #94a3b8; font-size: 0.85em; margin-top: 5px;">
|
|
2139
|
+
Generated: ${new Date(report.crawled_at).toLocaleString()}
|
|
2140
|
+
</div>
|
|
2141
|
+
|
|
2142
|
+
<div class="summary">
|
|
2143
|
+
<div class="stat">
|
|
2144
|
+
<div class="stat-label">Pages Audited</div>
|
|
2145
|
+
<div class="stat-value">${report.total_pages}</div>
|
|
2146
|
+
</div>
|
|
2147
|
+
<div class="stat">
|
|
2148
|
+
<div class="stat-label">Avg Overall Score</div>
|
|
2149
|
+
<div class="stat-value">${report.summary.avgOverallScore.toFixed(1)}</div>
|
|
2150
|
+
</div>
|
|
2151
|
+
<div class="stat">
|
|
2152
|
+
<div class="stat-label">Avg AI Readiness</div>
|
|
2153
|
+
<div class="stat-value">${report.summary.avgAIReadinessScore.toFixed(1)}</div>
|
|
2154
|
+
</div> avg
|
|
2155
|
+
<div class="stat">
|
|
2156
|
+
<div class="stat-label">Total Issues</div>
|
|
2157
|
+
<div class="stat-value">${report.summary.total_issues}</div>
|
|
2158
|
+
</div>
|
|
2159
|
+
</div>
|
|
2160
|
+
</div>
|
|
2161
|
+
|
|
2162
|
+
<div class="pages">
|
|
2163
|
+
${report.pages.map((page) => `
|
|
2164
|
+
<div class="page-card">
|
|
2165
|
+
<div class="page-url">${page.input.requested_url}</div>
|
|
2166
|
+
<div class="page-scores">
|
|
2167
|
+
<div class="score-badge">Overall: ${page.scores.overall}</div>
|
|
2168
|
+
<div class="score-badge">AI Readiness: ${page.scores.ai_readiness}</div>
|
|
2169
|
+
<div class="score-badge">Issues: ${page.issues.length}</div>
|
|
2170
|
+
</div>
|
|
2171
|
+
</div>
|
|
2172
|
+
`).join("")}
|
|
2173
|
+
</div>
|
|
2174
|
+
</div>
|
|
2175
|
+
</body>
|
|
2176
|
+
</html>`;
|
|
2177
|
+
}
|
|
2178
|
+
|
|
2179
|
+
// src/commands/report.ts
|
|
2180
|
+
import chalk4 from "chalk";
|
|
2181
|
+
import ora3 from "ora";
|
|
2182
|
+
import open from "open";
|
|
2183
|
+
import { readFile as readFile2, writeFile as writeFile3 } from "fs/promises";
|
|
2184
|
+
import { resolve as resolve3 } from "path";
|
|
2185
|
+
import { existsSync as existsSync3 } from "fs";
|
|
2186
|
+
function reportCommand(program2) {
|
|
2187
|
+
program2.command("report").description("Generate and view reports from saved audit results").argument("<file>", "Path to audit JSON file (e.g., ./last_run.json)").option("--open", "Open the report in browser", false).option("-f, --format <format>", "Output format: html, json, csv", "html").action(async (file, options) => {
|
|
2188
|
+
const spinner = ora3("Loading audit results...").start();
|
|
2189
|
+
try {
|
|
2190
|
+
const filePath = resolve3(process.cwd(), file);
|
|
2191
|
+
if (!existsSync3(filePath)) {
|
|
2192
|
+
throw new Error(`File not found: ${filePath}`);
|
|
2193
|
+
}
|
|
2194
|
+
const jsonContent = await readFile2(filePath, "utf-8");
|
|
2195
|
+
const report = JSON.parse(jsonContent);
|
|
2196
|
+
spinner.text = "Generating report...";
|
|
2197
|
+
const isCrawlReport = "pages" in report && Array.isArray(report.pages);
|
|
2198
|
+
if (options.format === "html") {
|
|
2199
|
+
const htmlPath = filePath.replace(/\.json$/, ".html");
|
|
2200
|
+
const html = isCrawlReport ? generateCrawlHTML2(report) : generateSingleHTML(report);
|
|
2201
|
+
await writeFile3(htmlPath, html);
|
|
2202
|
+
spinner.succeed(chalk4.green("Report generated!"));
|
|
2203
|
+
console.log(chalk4.dim(`HTML saved to: ${htmlPath}`));
|
|
2204
|
+
if (options.open) {
|
|
2205
|
+
spinner.text = "Opening report in browser...";
|
|
2206
|
+
await open(htmlPath);
|
|
2207
|
+
spinner.succeed("Report opened in browser");
|
|
2208
|
+
}
|
|
2209
|
+
} else if (options.format === "json") {
|
|
2210
|
+
spinner.succeed("Report loaded");
|
|
2211
|
+
console.log(JSON.stringify(report, null, 2));
|
|
2212
|
+
} else if (options.format === "csv") {
|
|
2213
|
+
const csvPath = filePath.replace(/\.json$/, ".csv");
|
|
2214
|
+
const csv = generateCSV(report);
|
|
2215
|
+
await writeFile3(csvPath, csv);
|
|
2216
|
+
spinner.succeed(chalk4.green("CSV report generated!"));
|
|
2217
|
+
console.log(chalk4.dim(`CSV saved to: ${csvPath}`));
|
|
2218
|
+
}
|
|
2219
|
+
if (isCrawlReport) {
|
|
2220
|
+
console.log("\n" + chalk4.bold("\u{1F310} Crawl Summary"));
|
|
2221
|
+
console.log(`Pages: ${chalk4.cyan(report.total_pages)}`);
|
|
2222
|
+
console.log(`Avg Score: ${chalk4.cyan(report.summary.avg_overall_score.toFixed(1))}`);
|
|
2223
|
+
console.log(`Total Issues: ${chalk4.yellow(report.summary.total_issues)}`);
|
|
2224
|
+
} else {
|
|
2225
|
+
console.log("\n" + chalk4.bold("\u{1F4CA} Audit Summary"));
|
|
2226
|
+
console.log(`URL: ${chalk4.cyan(report.input.requested_url)}`);
|
|
2227
|
+
console.log(`Overall Score: ${chalk4.cyan(report.scores.overall)}`);
|
|
2228
|
+
console.log(`Issues: ${chalk4.yellow(report.issues.length)}`);
|
|
2229
|
+
}
|
|
2230
|
+
} catch (error) {
|
|
2231
|
+
spinner.fail(chalk4.red("Failed to generate report"));
|
|
2232
|
+
if (error instanceof Error) {
|
|
2233
|
+
console.error(chalk4.red(error.message));
|
|
2234
|
+
}
|
|
2235
|
+
process.exit(1);
|
|
2236
|
+
}
|
|
2237
|
+
});
|
|
2238
|
+
}
|
|
2239
|
+
function generateSingleHTML(report) {
|
|
2240
|
+
return `<!DOCTYPE html>
|
|
2241
|
+
<html lang="en">
|
|
2242
|
+
<head>
|
|
2243
|
+
<meta charset="UTF-8">
|
|
2244
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
2245
|
+
<title>AI Lighthouse Report - ${report.input.requested_url}</title>
|
|
2246
|
+
<style>
|
|
2247
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
2248
|
+
body {
|
|
2249
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
2250
|
+
background: #f5f5f5;
|
|
2251
|
+
padding: 20px;
|
|
2252
|
+
line-height: 1.6;
|
|
2253
|
+
}
|
|
2254
|
+
.container { max-width: 1200px; margin: 0 auto; }
|
|
2255
|
+
.header { background: white; padding: 40px; border-radius: 8px; margin-bottom: 20px; box-shadow: 0 2px 8px rgba(0,0,0,0.1); }
|
|
2256
|
+
h1 { color: #2563eb; margin-bottom: 10px; }
|
|
2257
|
+
.scores { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 15px; margin: 25px 0; }
|
|
2258
|
+
.score-card {
|
|
2259
|
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
2260
|
+
color: white;
|
|
2261
|
+
padding: 20px;
|
|
2262
|
+
border-radius: 8px;
|
|
2263
|
+
text-align: center;
|
|
2264
|
+
}
|
|
2265
|
+
.score-value { font-size: 2.5em; font-weight: bold; }
|
|
2266
|
+
.score-label { font-size: 0.9em; opacity: 0.9; margin-top: 5px; }
|
|
2267
|
+
.section { background: white; padding: 30px; border-radius: 8px; margin-bottom: 20px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
|
|
2268
|
+
.section-title { color: #1e40af; font-size: 1.5em; margin-bottom: 20px; border-bottom: 2px solid #dbeafe; padding-bottom: 10px; }
|
|
2269
|
+
.issues { display: grid; gap: 15px; }
|
|
2270
|
+
.issue {
|
|
2271
|
+
background: #f8fafc;
|
|
2272
|
+
border-left: 4px solid #cbd5e1;
|
|
2273
|
+
padding: 15px;
|
|
2274
|
+
border-radius: 4px;
|
|
2275
|
+
}
|
|
2276
|
+
.issue.critical { border-left-color: #dc2626; background: #fef2f2; }
|
|
2277
|
+
.issue.high { border-left-color: #ea580c; background: #fff7ed; }
|
|
2278
|
+
.issue.medium { border-left-color: #f59e0b; background: #fffbeb; }
|
|
2279
|
+
.issue-title { font-weight: 600; color: #1e293b; margin-bottom: 8px; font-size: 1.05em; }
|
|
2280
|
+
.issue-meta { font-size: 0.85em; color: #64748b; margin-bottom: 10px; }
|
|
2281
|
+
.issue-fix { color: #0f766e; background: #f0fdfa; padding: 12px; border-radius: 4px; margin-top: 10px; }
|
|
2282
|
+
.entity { background: #f0f9ff; border: 1px solid #bae6fd; padding: 15px; border-radius: 6px; margin-bottom: 15px; }
|
|
2283
|
+
.entity-name { font-weight: 600; color: #0c4a6e; font-size: 1.1em; }
|
|
2284
|
+
</style>
|
|
2285
|
+
</head>
|
|
2286
|
+
<body>
|
|
2287
|
+
<div class="container">
|
|
2288
|
+
<div class="header">
|
|
2289
|
+
<h1>\u{1F6A8} AI Lighthouse Report</h1>
|
|
2290
|
+
<div style="color: #64748b; margin-top: 5px;">${report.input.requested_url}</div>
|
|
2291
|
+
<div style="color: #94a3b8; font-size: 0.85em; margin-top: 5px;">
|
|
2292
|
+
${new Date(report.scanned_at).toLocaleString()}
|
|
2293
|
+
</div>
|
|
2294
|
+
|
|
2295
|
+
<div class="scores">
|
|
2296
|
+
<div class="score-card">
|
|
2297
|
+
<div class="score-value">${report.scores.overall}</div>
|
|
2298
|
+
<div class="score-label">Overall</div>
|
|
2299
|
+
</div>
|
|
2300
|
+
<div class="score-card">
|
|
2301
|
+
<div class="score-value">${report.scores.ai_readiness}</div>
|
|
2302
|
+
<div class="score-label">AI Readiness</div>
|
|
2303
|
+
</div>
|
|
2304
|
+
<div class="score-card">
|
|
2305
|
+
<div class="score-value">${report.scores.crawlability}</div>
|
|
2306
|
+
<div class="score-label">Crawlability</div>
|
|
2307
|
+
</div>
|
|
2308
|
+
<div class="score-card">
|
|
2309
|
+
<div class="score-value">${report.scores.content_clarity}</div>
|
|
2310
|
+
<div class="score-label">Content Clarity</div>
|
|
2311
|
+
</div>
|
|
2312
|
+
<div class="score-card">
|
|
2313
|
+
<div class="score-value">${report.scores.schema_coverage}</div>
|
|
2314
|
+
<div class="score-label">Schema</div>
|
|
2315
|
+
</div>
|
|
2316
|
+
</div>
|
|
2317
|
+
</div>
|
|
2318
|
+
|
|
2319
|
+
${report.issues.length > 0 ? `
|
|
2320
|
+
<div class="section">
|
|
2321
|
+
<div class="section-title">\u26A0\uFE0F Issues (${report.issues.length})</div>
|
|
2322
|
+
<div class="issues">
|
|
2323
|
+
${report.issues.map((issue) => `
|
|
2324
|
+
<div class="issue ${issue.severity}">
|
|
2325
|
+
<div class="issue-title">${issue.message}</div>
|
|
2326
|
+
<div class="issue-meta">
|
|
2327
|
+
<span style="text-transform: uppercase; font-weight: 600; color: ${getSeverityColor(issue.severity)}">
|
|
2328
|
+
${issue.severity}
|
|
2329
|
+
</span>
|
|
2330
|
+
\xB7 ${issue.category}
|
|
2331
|
+
</div>
|
|
2332
|
+
${issue.evidence ? `<div style="color: #475569; margin: 10px 0;">${issue.evidence}</div>` : ""}
|
|
2333
|
+
<div class="issue-fix"><strong>\u{1F4A1} Fix:</strong> ${issue.suggested_fix}</div>
|
|
2334
|
+
</div>
|
|
2335
|
+
`).join("")}
|
|
2336
|
+
</div>
|
|
2337
|
+
</div>
|
|
2338
|
+
` : ""}
|
|
2339
|
+
|
|
2340
|
+
${report.entities && report.entities.length > 0 ? `
|
|
2341
|
+
<div class="section">
|
|
2342
|
+
<div class="section-title">\u{1F3F7}\uFE0F Detected Entities (${report.entities.length})</div>
|
|
2343
|
+
${report.entities.map((entity) => `
|
|
2344
|
+
<div class="entity">
|
|
2345
|
+
<div class="entity-name">${entity.name}</div>
|
|
2346
|
+
<div style="color: #0369a1; font-size: 0.9em; margin-top: 3px;">
|
|
2347
|
+
${entity.type} \xB7 ${entity.source}
|
|
2348
|
+
</div>
|
|
2349
|
+
${entity.description ? `<div style="margin-top: 10px; color: #334155;">${entity.description}</div>` : ""}
|
|
2350
|
+
</div>
|
|
2351
|
+
`).join("")}
|
|
2352
|
+
</div>
|
|
2353
|
+
` : ""}
|
|
2354
|
+
</div>
|
|
2355
|
+
</body>
|
|
2356
|
+
</html>`;
|
|
2357
|
+
}
|
|
2358
|
+
function generateCrawlHTML2(report) {
|
|
2359
|
+
return `<!DOCTYPE html>
|
|
2360
|
+
<html lang="en">
|
|
2361
|
+
<head>
|
|
2362
|
+
<meta charset="UTF-8">
|
|
2363
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
2364
|
+
<title>Crawl Report - ${report.base_url}</title>
|
|
2365
|
+
<style>
|
|
2366
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
2367
|
+
body {
|
|
2368
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
2369
|
+
background: #f5f5f5;
|
|
2370
|
+
padding: 20px;
|
|
2371
|
+
}
|
|
2372
|
+
.container { max-width: 1400px; margin: 0 auto; }
|
|
2373
|
+
.header { background: white; padding: 30px; border-radius: 8px; margin-bottom: 20px; box-shadow: 0 2px 8px rgba(0,0,0,0.1); }
|
|
2374
|
+
h1 { color: #2563eb; }
|
|
2375
|
+
.summary { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 15px; margin-top: 20px; }
|
|
2376
|
+
.stat { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 20px; border-radius: 8px; text-align: center; }
|
|
2377
|
+
.stat-value { font-size: 2em; font-weight: bold; }
|
|
2378
|
+
.pages { display: grid; gap: 15px; }
|
|
2379
|
+
.page-card { background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
|
|
2380
|
+
.page-url { color: #1e40af; font-weight: 600; word-break: break-all; margin-bottom: 12px; }
|
|
2381
|
+
.page-scores { display: flex; flex-wrap: wrap; gap: 10px; }
|
|
2382
|
+
.score-badge { background: #dbeafe; color: #1e40af; padding: 6px 12px; border-radius: 4px; font-size: 0.9em; }
|
|
2383
|
+
</style>
|
|
2384
|
+
</head>
|
|
2385
|
+
<body>
|
|
2386
|
+
<div class="container">
|
|
2387
|
+
<div class="header">
|
|
2388
|
+
<h1>\u{1F310} Multi-Page Crawl Report</h1>
|
|
2389
|
+
<div style="color: #64748b; margin-top: 8px;">${report.base_url}</div>
|
|
2390
|
+
<div style="color: #94a3b8; font-size: 0.85em; margin-top: 5px;">
|
|
2391
|
+
${new Date(report.crawled_at).toLocaleString()}
|
|
2392
|
+
</div>
|
|
2393
|
+
|
|
2394
|
+
<div class="summary">
|
|
2395
|
+
<div class="stat">
|
|
2396
|
+
<div class="stat-value">${report.total_pages}</div>
|
|
2397
|
+
<div style="opacity: 0.9; margin-top: 5px;">Pages</div>
|
|
2398
|
+
</div>
|
|
2399
|
+
<div class="stat">
|
|
2400
|
+
<div class="stat-value">${report.summary.avg_overall_score.toFixed(1)}</div>
|
|
2401
|
+
<div style="opacity: 0.9; margin-top: 5px;">Avg Score</div>
|
|
2402
|
+
</div>
|
|
2403
|
+
<div class="stat">
|
|
2404
|
+
<div class="stat-value">${report.summary.avg_ai_readiness.toFixed(1)}</div>
|
|
2405
|
+
<div style="opacity: 0.9; margin-top: 5px;">Avg AI Readiness</div>
|
|
2406
|
+
</div>
|
|
2407
|
+
<div class="stat">
|
|
2408
|
+
<div class="stat-value">${report.summary.total_issues}</div>
|
|
2409
|
+
<div style="opacity: 0.9; margin-top: 5px;">Total Issues</div>
|
|
2410
|
+
</div>
|
|
2411
|
+
</div>
|
|
2412
|
+
</div>
|
|
2413
|
+
|
|
2414
|
+
<div class="pages">
|
|
2415
|
+
${report.pages.map((page, idx) => `
|
|
2416
|
+
<div class="page-card">
|
|
2417
|
+
<div style="color: #94a3b8; font-size: 0.85em; margin-bottom: 5px;">Page ${idx + 1}</div>
|
|
2418
|
+
<div class="page-url">${page.input.requested_url}</div>
|
|
2419
|
+
<div class="page-scores">
|
|
2420
|
+
<div class="score-badge">Overall: ${page.scores.overall}</div>
|
|
2421
|
+
<div class="score-badge">AI Readiness: ${page.scores.ai_readiness}</div>
|
|
2422
|
+
<div class="score-badge">Crawlability: ${page.scores.crawlability}</div>
|
|
2423
|
+
<div class="score-badge">Issues: ${page.issues.length}</div>
|
|
2424
|
+
</div>
|
|
2425
|
+
</div>
|
|
2426
|
+
`).join("")}
|
|
2427
|
+
</div>
|
|
2428
|
+
</div>
|
|
2429
|
+
</body>
|
|
2430
|
+
</html>`;
|
|
2431
|
+
}
|
|
2432
|
+
function generateCSV(report) {
|
|
2433
|
+
const isCrawlReport = "pages" in report && Array.isArray(report.pages);
|
|
2434
|
+
if (isCrawlReport) {
|
|
2435
|
+
const headers = ["Page", "URL", "Overall Score", "AI Readiness", "Crawlability", "Issues"];
|
|
2436
|
+
const rows = report.pages.map((page, idx) => [
|
|
2437
|
+
idx + 1,
|
|
2438
|
+
`"${page.input.requested_url}"`,
|
|
2439
|
+
page.scores.overall,
|
|
2440
|
+
page.scores.ai_readiness,
|
|
2441
|
+
page.scores.crawlability,
|
|
2442
|
+
page.issues.length
|
|
2443
|
+
]);
|
|
2444
|
+
return [headers.join(","), ...rows.map((r) => r.join(","))].join("\n");
|
|
2445
|
+
} else {
|
|
2446
|
+
const headers = ["ID", "Severity", "Category", "Message", "Fix"];
|
|
2447
|
+
const rows = report.issues.map((issue) => [
|
|
2448
|
+
issue.id,
|
|
2449
|
+
issue.severity,
|
|
2450
|
+
issue.category,
|
|
2451
|
+
`"${issue.message.replace(/"/g, '""')}"`,
|
|
2452
|
+
`"${issue.suggested_fix.replace(/"/g, '""')}"`
|
|
2453
|
+
]);
|
|
2454
|
+
return [headers.join(","), ...rows.map((r) => r.join(","))].join("\n");
|
|
2455
|
+
}
|
|
2456
|
+
}
|
|
2457
|
+
function getSeverityColor(severity) {
|
|
2458
|
+
const colors = {
|
|
2459
|
+
critical: "#dc2626",
|
|
2460
|
+
high: "#ea580c",
|
|
2461
|
+
medium: "#f59e0b",
|
|
2462
|
+
low: "#84cc16",
|
|
2463
|
+
info: "#3b82f6"
|
|
2464
|
+
};
|
|
2465
|
+
return colors[severity] || "#64748b";
|
|
2466
|
+
}
|
|
2467
|
+
|
|
2468
|
+
// src/commands/audit-wizard.ts
|
|
2469
|
+
import { render as render2 } from "ink";
|
|
2470
|
+
import React11 from "react";
|
|
2471
|
+
import { analyzeUrlWithRules as analyzeUrlWithRules3, calculateAIReadiness as calculateAIReadiness2 } from "@ai-lighthouse/scanner";
|
|
2472
|
+
function auditWizardCommand(program2) {
|
|
2473
|
+
program2.command("wizard").alias("w").description("Interactive wizard to configure and run an audit").argument("[url]", "Optional URL to audit (will prompt if not provided)").action(async (url) => {
|
|
2474
|
+
const originalConsoleError = console.error;
|
|
2475
|
+
const originalConsoleWarn = console.warn;
|
|
2476
|
+
console.error = () => {
|
|
2477
|
+
};
|
|
2478
|
+
console.warn = () => {
|
|
2479
|
+
};
|
|
2480
|
+
let auditConfig = null;
|
|
2481
|
+
const wizardRender = render2(
|
|
2482
|
+
React11.createElement(SetupWizard, {
|
|
2483
|
+
initialUrl: url,
|
|
2484
|
+
onComplete: (config) => {
|
|
2485
|
+
auditConfig = config;
|
|
2486
|
+
}
|
|
2487
|
+
})
|
|
2488
|
+
);
|
|
2489
|
+
await wizardRender.waitUntilExit();
|
|
2490
|
+
if (!auditConfig) {
|
|
2491
|
+
console.error = originalConsoleError;
|
|
2492
|
+
console.warn = originalConsoleWarn;
|
|
2493
|
+
console.log("\nAudit cancelled.");
|
|
2494
|
+
process.exit(0);
|
|
2495
|
+
}
|
|
2496
|
+
const { clear, rerender } = render2(
|
|
2497
|
+
React11.createElement(AuditReportUI, {
|
|
2498
|
+
url: auditConfig.url,
|
|
2499
|
+
result: {},
|
|
2500
|
+
aiReadiness: {},
|
|
2501
|
+
loading: true,
|
|
2502
|
+
currentStep: "Starting audit..."
|
|
2503
|
+
})
|
|
2504
|
+
);
|
|
2505
|
+
try {
|
|
2506
|
+
const scanOptions = {
|
|
2507
|
+
maxChunkTokens: 1200,
|
|
2508
|
+
chunkingStrategy: "auto",
|
|
2509
|
+
enableChunking: auditConfig.enableChunking,
|
|
2510
|
+
enableExtractability: auditConfig.enableExtractability,
|
|
2511
|
+
enableHallucinationDetection: auditConfig.enableHallucination,
|
|
2512
|
+
enableLLM: auditConfig.enableLlm,
|
|
2513
|
+
minImpactScore: 8,
|
|
2514
|
+
minConfidence: 0.7,
|
|
2515
|
+
maxIssues: 20
|
|
2516
|
+
};
|
|
2517
|
+
if (auditConfig.enableLlm && auditConfig.llmProvider) {
|
|
2518
|
+
scanOptions.llmConfig = {
|
|
2519
|
+
provider: auditConfig.llmProvider,
|
|
2520
|
+
model: auditConfig.llmModel,
|
|
2521
|
+
baseUrl: auditConfig.llmBaseUrl,
|
|
2522
|
+
apiKey: auditConfig.llmApiKey
|
|
2523
|
+
};
|
|
2524
|
+
}
|
|
2525
|
+
rerender(
|
|
2526
|
+
React11.createElement(AuditReportUI, {
|
|
2527
|
+
url: auditConfig.url,
|
|
2528
|
+
result: {},
|
|
2529
|
+
aiReadiness: {},
|
|
2530
|
+
loading: true,
|
|
2531
|
+
currentStep: "Scanning page..."
|
|
2532
|
+
})
|
|
2533
|
+
);
|
|
2534
|
+
const result = await analyzeUrlWithRules3(auditConfig.url, scanOptions);
|
|
2535
|
+
rerender(
|
|
2536
|
+
React11.createElement(AuditReportUI, {
|
|
2537
|
+
url: auditConfig.url,
|
|
2538
|
+
result,
|
|
2539
|
+
aiReadiness: {},
|
|
2540
|
+
loading: true,
|
|
2541
|
+
currentStep: "Calculating AI readiness scores..."
|
|
2542
|
+
})
|
|
2543
|
+
);
|
|
2544
|
+
const aiReadiness = calculateAIReadiness2(result);
|
|
2545
|
+
clear();
|
|
2546
|
+
const finalRender = render2(
|
|
2547
|
+
React11.createElement(AuditReportUI, {
|
|
2548
|
+
url: auditConfig.url,
|
|
2549
|
+
result,
|
|
2550
|
+
aiReadiness,
|
|
2551
|
+
loading: false
|
|
2552
|
+
})
|
|
2553
|
+
);
|
|
2554
|
+
await finalRender.waitUntilExit();
|
|
2555
|
+
console.error = originalConsoleError;
|
|
2556
|
+
console.warn = originalConsoleWarn;
|
|
2557
|
+
} catch (error) {
|
|
2558
|
+
console.error = originalConsoleError;
|
|
2559
|
+
console.warn = originalConsoleWarn;
|
|
2560
|
+
clear();
|
|
2561
|
+
console.log("\n\x1B[1m\x1B[31m\u274C Audit Failed\x1B[0m");
|
|
2562
|
+
console.log("\x1B[31m" + "\u2500".repeat(70) + "\x1B[0m");
|
|
2563
|
+
console.log("\x1B[31m" + (error instanceof Error ? error.message : String(error)) + "\x1B[0m");
|
|
2564
|
+
console.log("\n\x1B[2mPlease check the URL and your configuration.\x1B[0m");
|
|
2565
|
+
process.exit(1);
|
|
2566
|
+
}
|
|
2567
|
+
});
|
|
2568
|
+
}
|
|
2569
|
+
|
|
2570
|
+
// src/index.ts
|
|
2571
|
+
var program = new Command();
|
|
2572
|
+
program.name("ai-lighthouse").description("AI Lighthouse - Audit websites for AI readiness and SEO optimization").version("1.0.0");
|
|
2573
|
+
auditWizardCommand(program);
|
|
13
2574
|
auditCommand(program);
|
|
14
2575
|
crawlCommand(program);
|
|
15
2576
|
reportCommand(program);
|