@oh-my-pi/omp-stats 14.9.5 → 14.9.8

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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@oh-my-pi/omp-stats",
4
- "version": "14.9.5",
4
+ "version": "14.9.8",
5
5
  "description": "Local observability dashboard for pi AI usage statistics",
6
6
  "homepage": "https://github.com/can1357/oh-my-pi",
7
7
  "author": "Can Boluk",
@@ -37,8 +37,8 @@
37
37
  "fmt": "biome format --write ."
38
38
  },
39
39
  "dependencies": {
40
- "@oh-my-pi/pi-ai": "14.9.5",
41
- "@oh-my-pi/pi-utils": "14.9.5",
40
+ "@oh-my-pi/pi-ai": "14.9.8",
41
+ "@oh-my-pi/pi-utils": "14.9.8",
42
42
  "@tailwindcss/node": "^4.2.4",
43
43
  "chart.js": "^4.5.1",
44
44
  "date-fns": "^4.1.0",
package/src/aggregator.ts CHANGED
@@ -19,67 +19,177 @@ import {
19
19
  insertMessageStats,
20
20
  insertUserMessageStats,
21
21
  setFileOffset,
22
+ updateUserMessageLinks,
22
23
  } from "./db";
23
- import { getSessionEntry, listAllSessionFiles, parseSessionFile } from "./parser";
24
+ import { getSessionEntry, listAllSessionFiles, type ParseSessionResult } from "./parser";
25
+ import type { SyncWorkerRequest, SyncWorkerResponse } from "./sync-worker";
26
+ // `with { type: "file" }` resolves to the worker's absolute path at runtime
27
+ // (dev) and survives bundling (the asset is copied alongside the build).
28
+ // tsgo doesn't recognize Bun's file-URL import attribute and would raise
29
+ // TS1192/TS5097 here; Bun honors it. Same suppression pattern lives in
30
+ // `tab-supervisor.ts` and `context-manager.ts`.
31
+ // @ts-expect-error -- Bun file-URL import (see comment above).
32
+ import syncWorkerUrl from "./sync-worker.ts" with { type: "file" };
24
33
  import type { BehaviorDashboardStats, DashboardStats, MessageStats, RequestDetails } from "./types";
25
34
 
26
35
  /**
27
- * Sync a single session file to the database.
28
- * Only processes new entries since the last sync.
36
+ * Apply a freshly parsed result to the database. Runs entirely on the
37
+ * main thread so the single SQLite handle owns every write.
29
38
  */
30
- async function syncSessionFile(sessionFile: string): Promise<number> {
31
- // Get file stats
32
- let fileStats: fs.Stats;
33
- try {
34
- fileStats = await fs.promises.stat(sessionFile);
35
- } catch {
36
- return 0;
37
- }
39
+ function applyParseResult(sessionFile: string, lastModified: number, result: ParseSessionResult): number {
40
+ if (result.stats.length > 0) insertMessageStats(result.stats);
41
+ if (result.userStats.length > 0) insertUserMessageStats(result.userStats);
42
+ if (result.userLinks.length > 0) updateUserMessageLinks(result.userLinks);
43
+ setFileOffset(sessionFile, result.newOffset, lastModified);
44
+ return result.stats.length + result.userStats.length;
45
+ }
38
46
 
39
- const lastModified = fileStats.mtimeMs;
47
+ /**
48
+ * Progress event emitted after each session file is fully processed.
49
+ * `current` is the number of files completed (skipped + parsed),
50
+ * `total` is the size of the work set. `processed` is the running total
51
+ * of inserted rows.
52
+ */
53
+ export interface SyncProgress {
54
+ current: number;
55
+ total: number;
56
+ processed: number;
57
+ sessionFile: string;
58
+ }
40
59
 
41
- // Check if file has changed since last sync
42
- const stored = getFileOffset(sessionFile);
43
- if (stored && stored.lastModified >= lastModified) {
44
- return 0; // File hasn't changed
45
- }
60
+ export interface SyncOptions {
61
+ /** Called after each file completes. Synchronous; keep it cheap. */
62
+ onProgress?: (event: SyncProgress) => void;
63
+ /**
64
+ * Worker pool size. Defaults to a sensible value derived from the host
65
+ * (capped to avoid drowning a small machine in workers). Set to `1` to
66
+ * force serial parsing without spawning workers.
67
+ */
68
+ workers?: number;
69
+ }
46
70
 
47
- // Parse file from last offset
48
- const fromOffset = stored?.offset ?? 0;
49
- const { stats, userStats, newOffset } = await parseSessionFile(sessionFile, fromOffset);
71
+ function defaultWorkerCount(): number {
72
+ // `navigator.hardwareConcurrency` is the portable answer in Bun; fall
73
+ // back to a small fixed pool if it's somehow unavailable.
74
+ const hw = typeof navigator !== "undefined" ? (navigator.hardwareConcurrency ?? 0) : 0;
75
+ const raw = hw > 0 ? hw : 4;
76
+ // Cap at 8 - parse is JSON-bound, and SQLite writes serialize on main
77
+ // thread anyway, so more workers stop helping.
78
+ return Math.min(8, Math.max(2, Math.floor(raw)));
79
+ }
50
80
 
51
- if (stats.length > 0) {
52
- insertMessageStats(stats);
53
- }
54
- if (userStats.length > 0) {
55
- insertUserMessageStats(userStats);
56
- }
81
+ interface WorkerHandle {
82
+ worker: Worker;
83
+ busy: boolean;
84
+ resolve: ((res: ParseSessionResult) => void) | null;
85
+ reject: ((err: Error) => void) | null;
86
+ }
57
87
 
58
- // Update offset tracker
59
- setFileOffset(sessionFile, newOffset, lastModified);
88
+ function spawnWorker(): WorkerHandle {
89
+ const worker = new Worker(syncWorkerUrl, { type: "module" });
90
+ const handle: WorkerHandle = { worker, busy: false, resolve: null, reject: null };
91
+ worker.onmessage = (event: MessageEvent<SyncWorkerResponse>) => {
92
+ const { resolve, reject } = handle;
93
+ handle.resolve = null;
94
+ handle.reject = null;
95
+ handle.busy = false;
96
+ if (!resolve || !reject) return;
97
+ if (event.data.ok) resolve(event.data.result);
98
+ else reject(new Error(event.data.error));
99
+ };
100
+ worker.onerror = (event: ErrorEvent) => {
101
+ const { reject } = handle;
102
+ handle.resolve = null;
103
+ handle.reject = null;
104
+ handle.busy = false;
105
+ reject?.(event.error instanceof Error ? event.error : new Error(event.message || "worker error"));
106
+ };
107
+ return handle;
108
+ }
60
109
 
61
- return stats.length + userStats.length;
110
+ function dispatch(handle: WorkerHandle, request: SyncWorkerRequest): Promise<ParseSessionResult> {
111
+ if (handle.busy) {
112
+ return Promise.reject(new Error("worker is busy - this is a bug in the dispatcher"));
113
+ }
114
+ const { promise, resolve, reject } = Promise.withResolvers<ParseSessionResult>();
115
+ handle.busy = true;
116
+ handle.resolve = resolve;
117
+ handle.reject = reject;
118
+ handle.worker.postMessage(request);
119
+ return promise;
62
120
  }
63
121
 
64
122
  /**
65
123
  * Sync all session files to the database.
66
- * Returns the number of new entries processed.
124
+ *
125
+ * Parsing fans out across a worker pool (one in-flight job per worker)
126
+ * while DB writes and offset bookkeeping stay on the calling thread so the
127
+ * single SQLite handle stays uncontended. `onProgress` fires once per
128
+ * completed file (skipped files included so the bar walks at a steady
129
+ * rate).
67
130
  */
68
- export async function syncAllSessions(): Promise<{ processed: number; files: number }> {
131
+ export async function syncAllSessions(opts?: SyncOptions): Promise<{ processed: number; files: number }> {
69
132
  await initDb();
70
133
 
71
134
  const files = await listAllSessionFiles();
135
+ if (files.length === 0) return { processed: 0, files: 0 };
136
+
72
137
  let totalProcessed = 0;
73
138
  let filesProcessed = 0;
139
+ let completed = 0;
140
+ let cursor = 0;
141
+
142
+ const poolSize = Math.max(1, Math.min(files.length, opts?.workers ?? defaultWorkerCount()));
143
+ const handles: WorkerHandle[] = [];
144
+ for (let i = 0; i < poolSize; i++) handles.push(spawnWorker());
145
+
146
+ const report = (sessionFile: string) => {
147
+ completed++;
148
+ opts?.onProgress?.({
149
+ current: completed,
150
+ total: files.length,
151
+ processed: totalProcessed,
152
+ sessionFile,
153
+ });
154
+ };
74
155
 
75
- for (const file of files) {
76
- const count = await syncSessionFile(file);
77
- if (count > 0) {
78
- totalProcessed += count;
79
- filesProcessed++;
156
+ async function drain(handle: WorkerHandle): Promise<void> {
157
+ while (true) {
158
+ const idx = cursor++;
159
+ if (idx >= files.length) return;
160
+ const sessionFile = files[idx];
161
+
162
+ let fileStats: fs.Stats;
163
+ try {
164
+ fileStats = await fs.promises.stat(sessionFile);
165
+ } catch {
166
+ report(sessionFile);
167
+ continue;
168
+ }
169
+ const lastModified = fileStats.mtimeMs;
170
+ const stored = getFileOffset(sessionFile);
171
+ if (stored && stored.lastModified >= lastModified) {
172
+ report(sessionFile);
173
+ continue;
174
+ }
175
+
176
+ const fromOffset = stored?.offset ?? 0;
177
+ const result = await dispatch(handle, { sessionFile, fromOffset });
178
+ const inserted = applyParseResult(sessionFile, lastModified, result);
179
+ if (inserted > 0) {
180
+ totalProcessed += inserted;
181
+ filesProcessed++;
182
+ }
183
+ report(sessionFile);
80
184
  }
81
185
  }
82
186
 
187
+ try {
188
+ await Promise.all(handles.map(drain));
189
+ } finally {
190
+ for (const handle of handles) handle.worker.terminate();
191
+ }
192
+
83
193
  return { processed: totalProcessed, files: filesProcessed };
84
194
  }
85
195
 
@@ -51,10 +51,14 @@ const CHART_THEMES = {
51
51
  } as const;
52
52
 
53
53
  const METRIC_OPTIONS = [
54
- { value: "yellingSentences", label: "Yelling" },
54
+ { value: "yelling", label: "Yelling" },
55
55
  { value: "profanity", label: "Profanity" },
56
- { value: "dramaRuns", label: "Drama (!!! / ???)" },
57
- { value: "total", label: "All three combined" },
56
+ { value: "anguish", label: "Anguish (!!!, nooo, dude, ..)" },
57
+ { value: "negation", label: "Negation (no/nope/wrong)" },
58
+ { value: "repetition", label: "Repetition (i meant, still doesnt)" },
59
+ { value: "blame", label: "Blame (you didnt, stop X-ing)" },
60
+ { value: "frustration", label: "Frustration (neg + rep + blame)" },
61
+ { value: "total", label: "All signals combined" },
58
62
  ] as const;
59
63
  type Metric = (typeof METRIC_OPTIONS)[number]["value"];
60
64
 
@@ -70,7 +74,10 @@ interface BehaviorChartProps {
70
74
  }
71
75
 
72
76
  function pointHits(point: BehaviorTimeSeriesPoint, metric: Metric): number {
73
- if (metric === "total") return point.yellingSentences + point.profanity + point.dramaRuns;
77
+ if (metric === "frustration") return point.negation + point.repetition + point.blame;
78
+ if (metric === "total") {
79
+ return point.yelling + point.profanity + point.anguish + point.negation + point.repetition + point.blame;
80
+ }
74
81
  return point[metric];
75
82
  }
76
83
 
@@ -30,7 +30,8 @@ const MODEL_COLORS = [
30
30
  const SERIES_COLORS = {
31
31
  yelling: "#fbbf24", // amber
32
32
  profanity: "#f87171", // red
33
- drama: "#a78bfa", // violet
33
+ anguish: "#a78bfa", // violet
34
+ frustration: "#22d3ee", // cyan - new semantic signals
34
35
  } as const;
35
36
 
36
37
  const CHART_THEMES = {
@@ -65,7 +66,8 @@ interface DailyPoint {
65
66
  timestamp: number;
66
67
  yelling: number;
67
68
  profanity: number;
68
- drama: number;
69
+ anguish: number;
70
+ frustration: number;
69
71
  total: number;
70
72
  }
71
73
 
@@ -73,7 +75,7 @@ interface ModelTrendSeries {
73
75
  data: DailyPoint[];
74
76
  }
75
77
 
76
- const GRID_TEMPLATE = "2fr 0.9fr 0.9fr 0.9fr 0.9fr 0.9fr 140px 40px";
78
+ const GRID_TEMPLATE = "2fr 0.9fr 0.8fr 0.8fr 0.8fr 0.9fr 0.8fr 140px 40px";
77
79
 
78
80
  function formatInt(value: number): string {
79
81
  return value.toLocaleString();
@@ -81,7 +83,13 @@ function formatInt(value: number): string {
81
83
 
82
84
  function totalHitRate(model: BehaviorModelStats): number {
83
85
  if (model.totalMessages === 0) return 0;
84
- const hits = model.totalYellingSentences + model.totalProfanity + model.totalDramaRuns;
86
+ const hits =
87
+ model.totalYelling +
88
+ model.totalProfanity +
89
+ model.totalAnguish +
90
+ model.totalNegation +
91
+ model.totalRepetition +
92
+ model.totalBlame;
85
93
  return hits / model.totalMessages;
86
94
  }
87
95
 
@@ -128,7 +136,8 @@ export function BehaviorModelsTable({ models, behaviorSeries }: BehaviorModelsTa
128
136
  <div className="text-right">Messages</div>
129
137
  <div className="text-right">CAPS %</div>
130
138
  <div className="text-right">Profanity %</div>
131
- <div className="text-right">Drama %</div>
139
+ <div className="text-right">Anguish %</div>
140
+ <div className="text-right">Frustration %</div>
132
141
  <div className="text-right">Hits %</div>
133
142
  <div className="text-center">Trend</div>
134
143
  <div />
@@ -140,7 +149,8 @@ export function BehaviorModelsTable({ models, behaviorSeries }: BehaviorModelsTa
140
149
  const trend = trendByKey.get(key)?.data ?? [];
141
150
  const trendColor = MODEL_COLORS[index % MODEL_COLORS.length];
142
151
  const isExpanded = expandedKey === key;
143
- const totalHits = model.totalYellingSentences + model.totalProfanity + model.totalDramaRuns;
152
+ const totalFrustration = model.totalNegation + model.totalRepetition + model.totalBlame;
153
+ const totalHits = model.totalYelling + model.totalProfanity + model.totalAnguish + totalFrustration;
144
154
 
145
155
  return (
146
156
  <div key={key} className="border-t border-[var(--border-subtle)]">
@@ -158,13 +168,16 @@ export function BehaviorModelsTable({ models, behaviorSeries }: BehaviorModelsTa
158
168
  {formatInt(model.totalMessages)}
159
169
  </div>
160
170
  <div className="text-right text-[var(--text-secondary)] font-mono text-sm">
161
- {formatRate(model.totalYellingSentences, model.totalMessages)}
171
+ {formatRate(model.totalYelling, model.totalMessages)}
162
172
  </div>
163
173
  <div className="text-right text-[var(--text-secondary)] font-mono text-sm">
164
174
  {formatRate(model.totalProfanity, model.totalMessages)}
165
175
  </div>
166
176
  <div className="text-right text-[var(--text-secondary)] font-mono text-sm">
167
- {formatRate(model.totalDramaRuns, model.totalMessages)}
177
+ {formatRate(model.totalAnguish, model.totalMessages)}
178
+ </div>
179
+ <div className="text-right text-[var(--text-secondary)] font-mono text-sm">
180
+ {formatRate(totalFrustration, model.totalMessages)}
168
181
  </div>
169
182
  <div className="text-right text-[var(--text-secondary)] font-mono text-sm">
170
183
  {formatRate(totalHits, model.totalMessages)}
@@ -188,7 +201,7 @@ export function BehaviorModelsTable({ models, behaviorSeries }: BehaviorModelsTa
188
201
  <div className="space-y-4 text-sm">
189
202
  <DetailRow
190
203
  label="Yelling (CAPS)"
191
- total={model.totalYellingSentences}
204
+ total={model.totalYelling}
192
205
  messages={model.totalMessages}
193
206
  valueClass="text-[var(--accent-amber,#fbbf24)]"
194
207
  />
@@ -199,11 +212,29 @@ export function BehaviorModelsTable({ models, behaviorSeries }: BehaviorModelsTa
199
212
  valueClass="text-[var(--accent-red,#f87171)]"
200
213
  />
201
214
  <DetailRow
202
- label="Drama (!!! / ???)"
203
- total={model.totalDramaRuns}
215
+ label="Anguish (!!!, nooo, dude, ..)"
216
+ total={model.totalAnguish}
204
217
  messages={model.totalMessages}
205
218
  valueClass="text-[var(--accent-violet,#a78bfa)]"
206
219
  />
220
+ <DetailRow
221
+ label="Negation (no/nope/wrong)"
222
+ total={model.totalNegation}
223
+ messages={model.totalMessages}
224
+ valueClass="text-[var(--accent-cyan,#22d3ee)]"
225
+ />
226
+ <DetailRow
227
+ label="Repetition (i meant, still doesnt)"
228
+ total={model.totalRepetition}
229
+ messages={model.totalMessages}
230
+ valueClass="text-[var(--accent-cyan,#22d3ee)]"
231
+ />
232
+ <DetailRow
233
+ label="Blame (you didnt, stop X-ing)"
234
+ total={model.totalBlame}
235
+ messages={model.totalMessages}
236
+ valueClass="text-[var(--accent-cyan,#22d3ee)]"
237
+ />
207
238
  <DetailRow
208
239
  label="Avg chars / msg"
209
240
  total={model.totalChars}
@@ -322,9 +353,18 @@ function BreakdownChart({ data, chartTheme }: { data: DailyPoint[]; chartTheme:
322
353
  borderWidth: 2,
323
354
  },
324
355
  {
325
- label: "Drama",
326
- data: data.map(d => d.drama),
327
- borderColor: SERIES_COLORS.drama,
356
+ label: "Anguish",
357
+ data: data.map(d => d.anguish),
358
+ borderColor: SERIES_COLORS.anguish,
359
+ backgroundColor: "transparent",
360
+ tension: 0.4,
361
+ pointRadius: 0,
362
+ borderWidth: 2,
363
+ },
364
+ {
365
+ label: "Frustration",
366
+ data: data.map(d => d.frustration),
367
+ borderColor: SERIES_COLORS.frustration,
328
368
  backgroundColor: "transparent",
329
369
  tension: 0.4,
330
370
  pointRadius: 0,
@@ -394,13 +434,15 @@ function buildTrendLookup(points: BehaviorTimeSeriesPoint[]): Map<string, ModelT
394
434
  timestamp: point.timestamp,
395
435
  yelling: 0,
396
436
  profanity: 0,
397
- drama: 0,
437
+ anguish: 0,
438
+ frustration: 0,
398
439
  total: 0,
399
440
  };
400
- existing.yelling += point.yellingSentences;
441
+ existing.yelling += point.yelling;
401
442
  existing.profanity += point.profanity;
402
- existing.drama += point.dramaRuns;
403
- existing.total = existing.yelling + existing.profanity + existing.drama;
443
+ existing.anguish += point.anguish;
444
+ existing.frustration += point.negation + point.repetition + point.blame;
445
+ existing.total = existing.yelling + existing.profanity + existing.anguish + existing.frustration;
404
446
  dayMap.set(point.timestamp, existing);
405
447
  }
406
448
 
@@ -412,7 +454,8 @@ function buildTrendLookup(points: BehaviorTimeSeriesPoint[]): Map<string, ModelT
412
454
  timestamp: ts,
413
455
  yelling: 0,
414
456
  profanity: 0,
415
- drama: 0,
457
+ anguish: 0,
458
+ frustration: 0,
416
459
  total: 0,
417
460
  },
418
461
  );
@@ -10,14 +10,26 @@ function formatInt(value: number): string {
10
10
  return value.toLocaleString();
11
11
  }
12
12
 
13
+ /**
14
+ * Per-message rate for a signal. Uses 2 decimals so a 0.01-hits-per-msg model
15
+ * still distinguishes from a true zero, and never shows `NaN` or `Infinity`
16
+ * when there are no messages.
17
+ */
18
+ function perMsg(total: number, messages: number): string | undefined {
19
+ if (messages <= 0) return undefined;
20
+ return `${(total / messages).toFixed(2)} / msg`;
21
+ }
22
+
13
23
  export function BehaviorSummary({ overall, behaviorSeries }: BehaviorSummaryProps) {
14
- // Top "ranted-at" model: model that absorbed the most caps + profanity + drama.
24
+ // Top "ranted-at" model: model that absorbed the most caps + profanity +
25
+ // anguish + frustration (negation/repetition/blame).
15
26
  const topModel = useMemo(() => {
16
27
  const totals = new Map<string, { model: string; provider: string; score: number }>();
17
28
  for (const point of behaviorSeries) {
18
29
  const key = `${point.model}::${point.provider}`;
19
30
  const existing = totals.get(key);
20
- const score = point.yellingSentences + point.profanity + point.dramaRuns;
31
+ const score =
32
+ point.yelling + point.profanity + point.anguish + point.negation + point.repetition + point.blame;
21
33
  if (existing) {
22
34
  existing.score += score;
23
35
  } else {
@@ -31,36 +43,44 @@ export function BehaviorSummary({ overall, behaviorSeries }: BehaviorSummaryProp
31
43
  return best;
32
44
  }, [behaviorSeries]);
33
45
 
34
- const capsPerMsg = overall.totalMessages > 0 ? overall.totalYellingSentences / overall.totalMessages : 0;
46
+ const totalFrustration = overall.totalNegation + overall.totalRepetition + overall.totalBlame;
47
+ const messages = overall.totalMessages;
35
48
 
36
49
  const cards: Array<{ label: string; value: string; sub?: string }> = [
37
50
  {
38
51
  label: "Messages",
39
52
  value: formatInt(overall.totalMessages),
53
+ sub: messages > 0 ? "in selected range" : undefined,
40
54
  },
41
55
  {
42
56
  label: "Yelling",
43
- value: formatInt(overall.totalYellingSentences),
44
- sub: overall.totalMessages > 0 ? `${capsPerMsg.toFixed(2)} / msg` : undefined,
57
+ value: formatInt(overall.totalYelling),
58
+ sub: perMsg(overall.totalYelling, messages),
45
59
  },
46
60
  {
47
61
  label: "Profanity hits",
48
62
  value: formatInt(overall.totalProfanity),
63
+ sub: perMsg(overall.totalProfanity, messages),
64
+ },
65
+ {
66
+ label: "Anguish",
67
+ value: formatInt(overall.totalAnguish),
68
+ sub: perMsg(overall.totalAnguish, messages),
49
69
  },
50
70
  {
51
- label: "Drama runs",
52
- value: formatInt(overall.totalDramaRuns),
53
- sub: "!!! / ???",
71
+ label: "Frustration",
72
+ value: formatInt(totalFrustration),
73
+ sub: perMsg(totalFrustration, messages),
54
74
  },
55
75
  {
56
76
  label: "Most yelled-at",
57
- value: topModel?.model ?? "",
77
+ value: topModel?.model ?? "\u2014",
58
78
  sub: topModel ? `${formatInt(topModel.score)} hits` : undefined,
59
79
  },
60
80
  ];
61
81
 
62
82
  return (
63
- <div className="grid grid-cols-2 sm:grid-cols-5 gap-4">
83
+ <div className="grid grid-cols-2 sm:grid-cols-6 gap-4">
64
84
  {cards.map(card => (
65
85
  <div key={card.label} className="surface px-4 py-3">
66
86
  <p className="text-xs text-[var(--text-muted)] mb-1">{card.label}</p>
@@ -135,17 +135,23 @@ export interface BehaviorTimeSeriesPoint {
135
135
  model: string;
136
136
  provider: string;
137
137
  messages: number;
138
- yellingSentences: number;
138
+ yelling: number;
139
139
  profanity: number;
140
- dramaRuns: number;
140
+ anguish: number;
141
+ negation: number;
142
+ repetition: number;
143
+ blame: number;
141
144
  chars: number;
142
145
  }
143
146
 
144
147
  export interface BehaviorOverallStats {
145
148
  totalMessages: number;
146
- totalYellingSentences: number;
149
+ totalYelling: number;
147
150
  totalProfanity: number;
148
- totalDramaRuns: number;
151
+ totalAnguish: number;
152
+ totalNegation: number;
153
+ totalRepetition: number;
154
+ totalBlame: number;
149
155
  totalChars: number;
150
156
  firstTimestamp: number;
151
157
  lastTimestamp: number;
@@ -155,9 +161,12 @@ export interface BehaviorModelStats {
155
161
  model: string;
156
162
  provider: string;
157
163
  totalMessages: number;
158
- totalYellingSentences: number;
164
+ totalYelling: number;
159
165
  totalProfanity: number;
160
- totalDramaRuns: number;
166
+ totalAnguish: number;
167
+ totalNegation: number;
168
+ totalRepetition: number;
169
+ totalBlame: number;
161
170
  totalChars: number;
162
171
  lastTimestamp: number;
163
172
  }