|
@@ -2,6 +2,14 @@ import { Command } from "commander";
|
|
|
import { StateManager } from "@actalk/inkos-core";
|
|
import { StateManager } from "@actalk/inkos-core";
|
|
|
import { loadConfig, findProjectRoot, resolveBookId, log, logError } from "../utils.js";
|
|
import { loadConfig, findProjectRoot, resolveBookId, log, logError } from "../utils.js";
|
|
|
|
|
|
|
|
|
|
+interface TokenStats {
|
|
|
|
|
+ readonly totalPromptTokens: number;
|
|
|
|
|
+ readonly totalCompletionTokens: number;
|
|
|
|
|
+ readonly totalTokens: number;
|
|
|
|
|
+ readonly avgTokensPerChapter: number;
|
|
|
|
|
+ readonly recentTrend: ReadonlyArray<{ readonly chapter: number; readonly totalTokens: number }>;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
interface AnalyticsData {
|
|
interface AnalyticsData {
|
|
|
readonly bookId: string;
|
|
readonly bookId: string;
|
|
|
readonly totalChapters: number;
|
|
readonly totalChapters: number;
|
|
@@ -11,6 +19,7 @@ interface AnalyticsData {
|
|
|
readonly topIssueCategories: ReadonlyArray<{ readonly category: string; readonly count: number }>;
|
|
readonly topIssueCategories: ReadonlyArray<{ readonly category: string; readonly count: number }>;
|
|
|
readonly chaptersWithMostIssues: ReadonlyArray<{ readonly chapter: number; readonly issueCount: number }>;
|
|
readonly chaptersWithMostIssues: ReadonlyArray<{ readonly chapter: number; readonly issueCount: number }>;
|
|
|
readonly statusDistribution: Record<string, number>;
|
|
readonly statusDistribution: Record<string, number>;
|
|
|
|
|
+ readonly tokenStats?: TokenStats;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
export function computeAnalytics(
|
|
export function computeAnalytics(
|
|
@@ -20,6 +29,11 @@ export function computeAnalytics(
|
|
|
readonly status: string;
|
|
readonly status: string;
|
|
|
readonly wordCount: number;
|
|
readonly wordCount: number;
|
|
|
readonly auditIssues: ReadonlyArray<string>;
|
|
readonly auditIssues: ReadonlyArray<string>;
|
|
|
|
|
+ readonly tokenUsage?: {
|
|
|
|
|
+ readonly promptTokens: number;
|
|
|
|
|
+ readonly completionTokens: number;
|
|
|
|
|
+ readonly totalTokens: number;
|
|
|
|
|
+ };
|
|
|
}>,
|
|
}>,
|
|
|
): AnalyticsData {
|
|
): AnalyticsData {
|
|
|
const totalChapters = chapters.length;
|
|
const totalChapters = chapters.length;
|
|
@@ -65,6 +79,30 @@ export function computeAnalytics(
|
|
|
statusDistribution[ch.status] = (statusDistribution[ch.status] ?? 0) + 1;
|
|
statusDistribution[ch.status] = (statusDistribution[ch.status] ?? 0) + 1;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ // Token stats
|
|
|
|
|
+ const chaptersWithUsage = chapters.filter((ch) => ch.tokenUsage);
|
|
|
|
|
+ let tokenStats: TokenStats | undefined;
|
|
|
|
|
+ if (chaptersWithUsage.length > 0) {
|
|
|
|
|
+ const totalPromptTokens = chaptersWithUsage.reduce((sum, ch) => sum + (ch.tokenUsage?.promptTokens ?? 0), 0);
|
|
|
|
|
+ const totalCompletionTokens = chaptersWithUsage.reduce((sum, ch) => sum + (ch.tokenUsage?.completionTokens ?? 0), 0);
|
|
|
|
|
+ const totalTokens = chaptersWithUsage.reduce((sum, ch) => sum + (ch.tokenUsage?.totalTokens ?? 0), 0);
|
|
|
|
|
+ const avgTokensPerChapter = Math.round(totalTokens / chaptersWithUsage.length);
|
|
|
|
|
+
|
|
|
|
|
+ // Recent 5 chapters trend (sorted by chapter number)
|
|
|
|
|
+ const recentTrend = [...chaptersWithUsage]
|
|
|
|
|
+ .sort((a, b) => a.number - b.number)
|
|
|
|
|
+ .slice(-5)
|
|
|
|
|
+ .map((ch) => ({ chapter: ch.number, totalTokens: ch.tokenUsage?.totalTokens ?? 0 }));
|
|
|
|
|
+
|
|
|
|
|
+ tokenStats = {
|
|
|
|
|
+ totalPromptTokens,
|
|
|
|
|
+ totalCompletionTokens,
|
|
|
|
|
+ totalTokens,
|
|
|
|
|
+ avgTokensPerChapter,
|
|
|
|
|
+ recentTrend,
|
|
|
|
|
+ };
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
return {
|
|
return {
|
|
|
bookId,
|
|
bookId,
|
|
|
totalChapters,
|
|
totalChapters,
|
|
@@ -74,11 +112,13 @@ export function computeAnalytics(
|
|
|
topIssueCategories,
|
|
topIssueCategories,
|
|
|
chaptersWithMostIssues,
|
|
chaptersWithMostIssues,
|
|
|
statusDistribution,
|
|
statusDistribution,
|
|
|
|
|
+ tokenStats,
|
|
|
};
|
|
};
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
export const analyticsCommand = new Command("analytics")
|
|
export const analyticsCommand = new Command("analytics")
|
|
|
- .description("Show analytics for a book")
|
|
|
|
|
|
|
+ .alias("stats")
|
|
|
|
|
+ .description("Show analytics and token stats for a book")
|
|
|
.argument("[book-id]", "Book ID (auto-detected if only one book)")
|
|
.argument("[book-id]", "Book ID (auto-detected if only one book)")
|
|
|
.option("--json", "Output JSON")
|
|
.option("--json", "Output JSON")
|
|
|
.action(async (bookIdArg: string | undefined, opts) => {
|
|
.action(async (bookIdArg: string | undefined, opts) => {
|
|
@@ -110,6 +150,21 @@ export const analyticsCommand = new Command("analytics")
|
|
|
log("");
|
|
log("");
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ if (analytics.tokenStats) {
|
|
|
|
|
+ log(" Token usage:");
|
|
|
|
|
+ log(` Total tokens: ${analytics.tokenStats.totalTokens.toLocaleString()}`);
|
|
|
|
|
+ log(` Prompt tokens: ${analytics.tokenStats.totalPromptTokens.toLocaleString()}`);
|
|
|
|
|
+ log(` Completion tokens: ${analytics.tokenStats.totalCompletionTokens.toLocaleString()}`);
|
|
|
|
|
+ log(` Avg tokens/chapter: ${analytics.tokenStats.avgTokensPerChapter.toLocaleString()}`);
|
|
|
|
|
+ if (analytics.tokenStats.recentTrend.length > 0) {
|
|
|
|
|
+ log(" Recent trend:");
|
|
|
|
|
+ for (const { chapter, totalTokens } of analytics.tokenStats.recentTrend) {
|
|
|
|
|
+ log(` Ch.${chapter}: ${totalTokens.toLocaleString()} tokens`);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ log("");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
if (analytics.topIssueCategories.length > 0) {
|
|
if (analytics.topIssueCategories.length > 0) {
|
|
|
log(" Most common issue categories:");
|
|
log(" Most common issue categories:");
|
|
|
for (const { category, count } of analytics.topIssueCategories) {
|
|
for (const { category, count } of analytics.topIssueCategories) {
|