audiencemeter 0.1.0

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 ADDED
@@ -0,0 +1,2195 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __getOwnPropNames = Object.getOwnPropertyNames;
3
+ var __esm = (fn, res) => function __init() {
4
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
5
+ };
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+
11
+ // src/lib/config.ts
12
+ var config_exports = {};
13
+ __export(config_exports, {
14
+ clearAuthToken: () => clearAuthToken,
15
+ getApiUrl: () => getApiUrl,
16
+ getAuthToken: () => getAuthToken,
17
+ getConfig: () => getConfig,
18
+ getRefreshToken: () => getRefreshToken,
19
+ setApiUrl: () => setApiUrl,
20
+ setAuthToken: () => setAuthToken,
21
+ setRefreshToken: () => setRefreshToken
22
+ });
23
+ import Conf from "conf";
24
+ function getConfig() {
25
+ return config;
26
+ }
27
+ function getAuthToken() {
28
+ const token = config.get("authToken");
29
+ if (!token) {
30
+ throw new Error("Not authenticated. Run: audiencemeter login");
31
+ }
32
+ return token;
33
+ }
34
+ function setAuthToken(token) {
35
+ config.set("authToken", token);
36
+ }
37
+ function clearAuthToken() {
38
+ config.set("authToken", "");
39
+ config.set("refreshToken", "");
40
+ }
41
+ function getRefreshToken() {
42
+ return config.get("refreshToken");
43
+ }
44
+ function setRefreshToken(token) {
45
+ config.set("refreshToken", token);
46
+ }
47
+ function getApiUrl() {
48
+ return config.get("apiUrl");
49
+ }
50
+ function setApiUrl(url) {
51
+ config.set("apiUrl", url);
52
+ }
53
+ var config;
54
+ var init_config = __esm({
55
+ "src/lib/config.ts"() {
56
+ "use strict";
57
+ config = new Conf({
58
+ projectName: "audiencemeter",
59
+ schema: {
60
+ apiUrl: {
61
+ type: "string",
62
+ default: "https://api.audiencemeter.pro/v1"
63
+ },
64
+ authToken: {
65
+ type: "string",
66
+ default: ""
67
+ },
68
+ refreshToken: {
69
+ type: "string",
70
+ default: ""
71
+ }
72
+ }
73
+ });
74
+ }
75
+ });
76
+
77
+ // src/lib/theme.ts
78
+ import { rgbColor } from "terminui";
79
+ var BRAND_COLORS, icon, BRAND;
80
+ var init_theme = __esm({
81
+ "src/lib/theme.ts"() {
82
+ "use strict";
83
+ BRAND_COLORS = {
84
+ purple: rgbColor(175, 95, 255),
85
+ // Brand purple
86
+ cyan: rgbColor(80, 220, 220),
87
+ // Info/borders
88
+ green: rgbColor(80, 220, 120),
89
+ // Success/gauges
90
+ yellow: rgbColor(255, 200, 60),
91
+ // PINs
92
+ dimCyan: rgbColor(60, 160, 170),
93
+ // Borders, labels
94
+ red: rgbColor(255, 100, 100),
95
+ // Errors
96
+ dim: rgbColor(128, 128, 128),
97
+ // Dimmed text
98
+ white: rgbColor(230, 230, 230)
99
+ // Primary text
100
+ };
101
+ icon = {
102
+ session: "\u25CF",
103
+ // ●
104
+ live: "\u25CF",
105
+ // ●
106
+ ended: "\u25CB",
107
+ // ○
108
+ upcoming: "\u25D4",
109
+ // ◔
110
+ check: "\u2714",
111
+ // ✔
112
+ cross: "\u2718",
113
+ // ✘
114
+ arrow: "\u276F",
115
+ // ❯
116
+ dot: "\xB7",
117
+ // ·
118
+ bar: "\u2503",
119
+ // ┃
120
+ dash: "\u2500",
121
+ // ─
122
+ star: "\u2605",
123
+ // ★
124
+ sparkle: "\u2728"
125
+ // ✨
126
+ };
127
+ BRAND = {
128
+ name: "AudienceMeter",
129
+ tagline: "Speaker feedback, beautifully managed",
130
+ version: "0.1.0"
131
+ };
132
+ }
133
+ });
134
+
135
+ // src/lib/render.ts
136
+ import {
137
+ createTestBackendState,
138
+ createTestBackend,
139
+ createTerminal,
140
+ testBackendCellAt
141
+ } from "terminui";
142
+ import { terminalDrawJsx } from "terminui/jsx";
143
+ function fgAnsi(c) {
144
+ if (!c || c.type === "reset") return "";
145
+ if (c.type === "rgb") return `\x1B[38;2;${c.r};${c.g};${c.b}m`;
146
+ if (c.type === "indexed") return `\x1B[38;5;${c.index}m`;
147
+ return NAMED_FG[c.type] ? `\x1B[${NAMED_FG[c.type]}m` : "";
148
+ }
149
+ function bgAnsi(c) {
150
+ if (!c || c.type === "reset") return "";
151
+ if (c.type === "rgb") return `\x1B[48;2;${c.r};${c.g};${c.b}m`;
152
+ if (c.type === "indexed") return `\x1B[48;5;${c.index}m`;
153
+ return NAMED_BG[c.type] ? `\x1B[${NAMED_BG[c.type]}m` : "";
154
+ }
155
+ function modAnsi(mod) {
156
+ if (!mod) return "";
157
+ let s = "";
158
+ if (mod & 1) s += "\x1B[1m";
159
+ if (mod & 2) s += "\x1B[2m";
160
+ if (mod & 4) s += "\x1B[3m";
161
+ if (mod & 8) s += "\x1B[4m";
162
+ if (mod & 64) s += "\x1B[7m";
163
+ return s;
164
+ }
165
+ function cellToAnsi(cell) {
166
+ const fg = fgAnsi(cell.fg);
167
+ const bg = bgAnsi(cell.bg);
168
+ const mod = modAnsi(cell.modifier);
169
+ const style = fg + bg + mod;
170
+ if (style) {
171
+ return style + cell.symbol + RESET;
172
+ }
173
+ return cell.symbol;
174
+ }
175
+ function renderJsxToString(width, height, node) {
176
+ const state = createTestBackendState(width, height);
177
+ const terminal = createTerminal(createTestBackend(state));
178
+ terminalDrawJsx(terminal, node);
179
+ const lines = [];
180
+ for (let y = 0; y < height; y++) {
181
+ let line = "";
182
+ let hasStyle = false;
183
+ for (let x = 0; x < width; x++) {
184
+ const cell = testBackendCellAt(state, x, y);
185
+ if (!cell) {
186
+ if (hasStyle) {
187
+ line += RESET;
188
+ hasStyle = false;
189
+ }
190
+ line += " ";
191
+ continue;
192
+ }
193
+ const style = fgAnsi(cell.fg) + bgAnsi(cell.bg) + modAnsi(cell.modifier);
194
+ if (style) {
195
+ if (hasStyle) line += RESET;
196
+ line += style + cell.symbol;
197
+ hasStyle = true;
198
+ } else {
199
+ if (hasStyle) {
200
+ line += RESET;
201
+ hasStyle = false;
202
+ }
203
+ line += cell.symbol;
204
+ }
205
+ }
206
+ if (hasStyle) line += RESET;
207
+ lines.push(line.trimEnd());
208
+ }
209
+ while (lines.length > 0 && stripAnsi(lines[lines.length - 1]).trim() === "") {
210
+ lines.pop();
211
+ }
212
+ return lines.join("\n");
213
+ }
214
+ function getTermWidth() {
215
+ return Math.min(process.stdout.columns || 80, 120);
216
+ }
217
+ function stripAnsi(s) {
218
+ return s.replace(/\x1b\[[0-9;]*m/g, "");
219
+ }
220
+ var NAMED_FG, NAMED_BG, RESET;
221
+ var init_render = __esm({
222
+ "src/lib/render.ts"() {
223
+ "use strict";
224
+ NAMED_FG = {
225
+ black: "30",
226
+ red: "31",
227
+ green: "32",
228
+ yellow: "33",
229
+ blue: "34",
230
+ magenta: "35",
231
+ cyan: "36",
232
+ gray: "37",
233
+ white: "97",
234
+ "dark-gray": "90",
235
+ "light-red": "91",
236
+ "light-green": "92",
237
+ "light-yellow": "93",
238
+ "light-blue": "94",
239
+ "light-magenta": "95",
240
+ "light-cyan": "96"
241
+ };
242
+ NAMED_BG = {
243
+ black: "40",
244
+ red: "41",
245
+ green: "42",
246
+ yellow: "43",
247
+ blue: "44",
248
+ magenta: "45",
249
+ cyan: "46",
250
+ gray: "47",
251
+ white: "107",
252
+ "dark-gray": "100",
253
+ "light-red": "101",
254
+ "light-green": "102",
255
+ "light-yellow": "103",
256
+ "light-blue": "104",
257
+ "light-magenta": "105",
258
+ "light-cyan": "106"
259
+ };
260
+ RESET = "\x1B[0m";
261
+ }
262
+ });
263
+
264
+ // src/components/KeyValue.tsx
265
+ import { VStack, HStack, Text } from "terminui/jsx";
266
+ import { lengthConstraint, fillConstraint } from "terminui";
267
+ import { jsx, jsxs } from "terminui/jsx-runtime";
268
+ function KeyValue({ pairs }) {
269
+ const maxKeyLen = Math.max(...pairs.map(([k]) => k.length));
270
+ const constraints = pairs.map(() => lengthConstraint(1));
271
+ const rows = pairs.map(([key, value]) => {
272
+ const padded = key.padStart(maxKeyLen);
273
+ return /* @__PURE__ */ jsxs(HStack, { constraints: [lengthConstraint(maxKeyLen + 2), fillConstraint(1)], children: [
274
+ /* @__PURE__ */ jsx(Text, { fg: BRAND_COLORS.dim, children: padded + " " }),
275
+ /* @__PURE__ */ jsx(Text, { bold: true, fg: BRAND_COLORS.white, children: value })
276
+ ] });
277
+ });
278
+ return /* @__PURE__ */ jsx(VStack, { constraints, children: rows });
279
+ }
280
+ var init_KeyValue = __esm({
281
+ "src/components/KeyValue.tsx"() {
282
+ "use strict";
283
+ init_theme();
284
+ }
285
+ });
286
+
287
+ // src/commands/auth.ts
288
+ var auth_exports = {};
289
+ __export(auth_exports, {
290
+ authCommand: () => authCommand,
291
+ loginCommand: () => loginCommand,
292
+ logoutCommand: () => logoutCommand
293
+ });
294
+ import { Command } from "commander";
295
+ import http from "http";
296
+ import { URL } from "url";
297
+ import * as p from "@clack/prompts";
298
+ var SUPABASE_URL, authCommand, loginCommand, logoutCommand;
299
+ var init_auth = __esm({
300
+ "src/commands/auth.ts"() {
301
+ "use strict";
302
+ init_config();
303
+ init_theme();
304
+ init_render();
305
+ init_KeyValue();
306
+ SUPABASE_URL = "https://gflvvytymdmrbjpmymhb.supabase.co";
307
+ authCommand = new Command("auth").description(
308
+ "Manage authentication"
309
+ );
310
+ loginCommand = new Command("login").description("Log in via browser (Google OAuth)");
311
+ loginCommand.action(async () => {
312
+ p.intro(`${BRAND.name} -- Login`);
313
+ const s = p.spinner();
314
+ s.start("Starting local auth server...");
315
+ try {
316
+ const token = await new Promise((resolve, reject) => {
317
+ const server = http.createServer((req, res) => {
318
+ if (!req.url) {
319
+ res.writeHead(400);
320
+ res.end("Bad request");
321
+ return;
322
+ }
323
+ const url = new URL(req.url, "http://localhost");
324
+ if (url.pathname === "/callback") {
325
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
326
+ res.end(`<!DOCTYPE html>
327
+ <html><head><meta charset="utf-8"><title>${BRAND.name} \u2014 Login</title>
328
+ <style>body{font-family:system-ui;display:flex;justify-content:center;align-items:center;height:100vh;margin:0;background:#0a0a0a;color:#e5e5e5}
329
+ .card{text-align:center;padding:2rem;border-radius:12px;background:#171717;border:1px solid #333}</style></head>
330
+ <body><div class="card"><p>Processing authentication...</p></div>
331
+ <script>
332
+ const h=window.location.hash.substring(1);const p=new URLSearchParams(h);const t=p.get('access_token');const r=p.get('refresh_token');
333
+ if(t){let u='/token?access_token='+encodeURIComponent(t);if(r)u+='&refresh_token='+encodeURIComponent(r);window.location.href=u}
334
+ else{document.querySelector('.card').innerHTML='<p style="color:#f87171">Authentication failed. No token found.</p><p>You can close this tab.</p>'}
335
+ </script></body></html>`);
336
+ return;
337
+ }
338
+ if (url.pathname === "/token") {
339
+ const accessToken = url.searchParams.get("access_token");
340
+ const refreshTokenParam = url.searchParams.get("refresh_token");
341
+ if (accessToken) {
342
+ if (refreshTokenParam) {
343
+ setRefreshToken(refreshTokenParam);
344
+ }
345
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
346
+ res.end(`<!DOCTYPE html>
347
+ <html><head><meta charset="utf-8"><title>${BRAND.name} \u2014 Logged In</title>
348
+ <style>body{font-family:system-ui;display:flex;justify-content:center;align-items:center;height:100vh;margin:0;background:#0a0a0a;color:#e5e5e5}
349
+ .card{text-align:center;padding:2rem;border-radius:12px;background:#171717;border:1px solid #333}
350
+ .check{font-size:3rem;margin-bottom:1rem}</style></head>
351
+ <body><div class="card"><div class="check">&#x2714;</div><h2>Logged in!</h2><p>Return to your terminal.</p></div></body></html>`);
352
+ resolve(accessToken);
353
+ server.close();
354
+ } else {
355
+ res.writeHead(400, { "Content-Type": "text/html" });
356
+ res.end("<p>Missing access token.</p>");
357
+ }
358
+ return;
359
+ }
360
+ res.writeHead(404);
361
+ res.end("Not found");
362
+ });
363
+ server.listen(0, () => {
364
+ const address = server.address();
365
+ if (!address || typeof address === "string") {
366
+ reject(new Error("Failed to start local server"));
367
+ return;
368
+ }
369
+ const port = address.port;
370
+ const redirectUrl = `http://localhost:${port}/callback`;
371
+ const authUrl = `${SUPABASE_URL}/auth/v1/authorize?provider=google&redirect_to=${encodeURIComponent(redirectUrl)}`;
372
+ s.message("Opening browser for authentication...");
373
+ import("open").then((m) => m.default(authUrl)).catch(() => {
374
+ s.stop("Could not open browser automatically");
375
+ p.note(authUrl, "Open this URL manually");
376
+ });
377
+ });
378
+ setTimeout(() => {
379
+ server.close();
380
+ reject(new Error("Authentication timed out after 120 seconds"));
381
+ }, 12e4);
382
+ });
383
+ setAuthToken(token);
384
+ s.stop(`${icon.check} Logged in successfully!`);
385
+ const payload = JSON.parse(
386
+ Buffer.from(token.split(".")[1], "base64").toString()
387
+ );
388
+ if (payload.email) {
389
+ p.log.info(`Signed in as ${payload.email}`);
390
+ }
391
+ p.outro("Token stored. You can now use AudienceMeter CLI commands.");
392
+ process.exit(0);
393
+ } catch (error) {
394
+ s.stop(
395
+ `${icon.cross} Login failed: ${error instanceof Error ? error.message : "Unknown error"}`
396
+ );
397
+ process.exit(1);
398
+ }
399
+ });
400
+ logoutCommand = new Command("logout").description("Log out and clear stored credentials").action(async () => {
401
+ clearAuthToken();
402
+ p.log.success(`${icon.check} Logged out successfully.`);
403
+ });
404
+ authCommand.command("whoami").description("Show current authenticated user").action(async () => {
405
+ try {
406
+ const { getAuthToken: getAuthToken2 } = await Promise.resolve().then(() => (init_config(), config_exports));
407
+ const token = getAuthToken2();
408
+ const apiUrl = getApiUrl();
409
+ const payload = JSON.parse(
410
+ Buffer.from(token.split(".")[1], "base64").toString()
411
+ );
412
+ const output = renderJsxToString(
413
+ getTermWidth(),
414
+ 3,
415
+ KeyValue({
416
+ pairs: [
417
+ ["Email", payload.email || "unknown"],
418
+ ["User ID", payload.sub || "unknown"],
419
+ ["API URL", apiUrl]
420
+ ]
421
+ })
422
+ );
423
+ console.log(output);
424
+ } catch (error) {
425
+ if (error instanceof Error && error.message.includes("Not authenticated")) {
426
+ p.log.warn("Not logged in. Run: audiencemeter login");
427
+ } else {
428
+ p.log.error(
429
+ error instanceof Error ? error.message : "Unknown error"
430
+ );
431
+ }
432
+ }
433
+ });
434
+ authCommand.command("token").description("Print the current stored auth token").action(async () => {
435
+ try {
436
+ const { getAuthToken: getAuthToken2 } = await Promise.resolve().then(() => (init_config(), config_exports));
437
+ const token = getAuthToken2();
438
+ console.log(token);
439
+ } catch (error) {
440
+ if (error instanceof Error && error.message.includes("Not authenticated")) {
441
+ p.log.warn("Not logged in. Run: audiencemeter login");
442
+ } else {
443
+ p.log.error(
444
+ error instanceof Error ? error.message : "Unknown error"
445
+ );
446
+ }
447
+ }
448
+ });
449
+ authCommand.addCommand(loginCommand);
450
+ authCommand.addCommand(logoutCommand);
451
+ }
452
+ });
453
+
454
+ // src/lib/api-client.ts
455
+ function isTokenExpired(token) {
456
+ try {
457
+ const payload = JSON.parse(
458
+ Buffer.from(token.split(".")[1], "base64").toString()
459
+ );
460
+ if (!payload.exp) return false;
461
+ return Date.now() >= (payload.exp - 60) * 1e3;
462
+ } catch {
463
+ return true;
464
+ }
465
+ }
466
+ async function refreshAccessToken() {
467
+ const refreshToken = getRefreshToken();
468
+ if (!refreshToken) return null;
469
+ try {
470
+ const response = await fetch(`${SUPABASE_URL2}/auth/v1/token?grant_type=refresh_token`, {
471
+ method: "POST",
472
+ headers: {
473
+ "Content-Type": "application/json",
474
+ "apikey": SUPABASE_ANON_KEY
475
+ },
476
+ body: JSON.stringify({ refresh_token: refreshToken })
477
+ });
478
+ if (!response.ok) return null;
479
+ const data = await response.json();
480
+ if (data.access_token) {
481
+ setAuthToken(data.access_token);
482
+ if (data.refresh_token) {
483
+ setRefreshToken(data.refresh_token);
484
+ }
485
+ return data.access_token;
486
+ }
487
+ return null;
488
+ } catch {
489
+ return null;
490
+ }
491
+ }
492
+ async function ensureValidToken() {
493
+ const token = getAuthToken();
494
+ if (!isTokenExpired(token)) return token;
495
+ const newToken = await refreshAccessToken();
496
+ if (newToken) return newToken;
497
+ throw new Error("Session expired. Run: audiencemeter login");
498
+ }
499
+ function createApiClient() {
500
+ const baseUrl = getApiUrl();
501
+ const authToken = getAuthToken();
502
+ return new ApiClient(baseUrl, authToken);
503
+ }
504
+ async function createApiClientWithRefresh() {
505
+ const baseUrl = getApiUrl();
506
+ const authToken = await ensureValidToken();
507
+ return new ApiClient(baseUrl, authToken);
508
+ }
509
+ function getUserIdFromToken() {
510
+ const token = getAuthToken();
511
+ const payload = JSON.parse(
512
+ Buffer.from(token.split(".")[1], "base64").toString()
513
+ );
514
+ return payload.sub;
515
+ }
516
+ function getUserEmailFromToken() {
517
+ const token = getAuthToken();
518
+ const payload = JSON.parse(
519
+ Buffer.from(token.split(".")[1], "base64").toString()
520
+ );
521
+ return payload.email || "unknown";
522
+ }
523
+ var SUPABASE_URL2, SUPABASE_ANON_KEY, ApiClient;
524
+ var init_api_client = __esm({
525
+ "src/lib/api-client.ts"() {
526
+ "use strict";
527
+ init_config();
528
+ SUPABASE_URL2 = "https://gflvvytymdmrbjpmymhb.supabase.co";
529
+ SUPABASE_ANON_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImdmbHZ2eXR5bWRtcmJqcG15bWhiIiwicm9sZSI6ImFub24iLCJpYXQiOjE2ODAyNTIwMjIsImV4cCI6MTk5NTgyODAyMn0.1m-3IhFB-87AKk_-UIPzB0O1URgBwl78oKu8sNe8aFU";
530
+ ApiClient = class {
531
+ constructor(baseUrl, authToken) {
532
+ this.baseUrl = baseUrl;
533
+ this.authToken = authToken;
534
+ }
535
+ async request(method, path, body) {
536
+ const url = `${this.baseUrl}${path}`;
537
+ const headers = {
538
+ "Content-Type": "application/json",
539
+ Authorization: this.authToken
540
+ };
541
+ const options = { method, headers };
542
+ if (body !== void 0) {
543
+ options.body = JSON.stringify(body);
544
+ }
545
+ const response = await fetch(url, options);
546
+ if (!response.ok) {
547
+ let errorMessage;
548
+ try {
549
+ const errorBody = await response.json();
550
+ errorMessage = errorBody.message || errorBody.error || response.statusText;
551
+ } catch {
552
+ errorMessage = response.statusText;
553
+ }
554
+ if (response.status === 401) {
555
+ throw new Error("Not authenticated. Run: audiencemeter login");
556
+ }
557
+ throw new Error(`API error (${response.status}): ${errorMessage}`);
558
+ }
559
+ const text2 = await response.text();
560
+ if (!text2) return void 0;
561
+ return JSON.parse(text2);
562
+ }
563
+ async get(path) {
564
+ return this.request("GET", path);
565
+ }
566
+ async post(path, body) {
567
+ return this.request("POST", path, body);
568
+ }
569
+ async patch(path, body) {
570
+ return this.request("PATCH", path, body);
571
+ }
572
+ async delete(path) {
573
+ return this.request("DELETE", path);
574
+ }
575
+ };
576
+ }
577
+ });
578
+
579
+ // src/lib/templates.ts
580
+ function getTemplate(name) {
581
+ if (!(name in TEMPLATES)) {
582
+ return null;
583
+ }
584
+ return TEMPLATES[name];
585
+ }
586
+ function getTemplateNames() {
587
+ return Object.keys(TEMPLATES);
588
+ }
589
+ var TEMPLATES;
590
+ var init_templates = __esm({
591
+ "src/lib/templates.ts"() {
592
+ "use strict";
593
+ TEMPLATES = {
594
+ talk: {
595
+ description: "Conference talk or presentation (45 min)",
596
+ durationMinutes: 45,
597
+ ratings: [
598
+ { label: "Content Quality", maxValue: 5 },
599
+ { label: "Delivery", maxValue: 5 },
600
+ { label: "Clarity", maxValue: 5 }
601
+ ],
602
+ links: []
603
+ },
604
+ workshop: {
605
+ description: "Hands-on workshop or lab session (120 min)",
606
+ durationMinutes: 120,
607
+ ratings: [
608
+ { label: "Content Quality", maxValue: 5 },
609
+ { label: "Hands-on Experience", maxValue: 5 },
610
+ { label: "Pace", maxValue: 5 },
611
+ { label: "Clarity", maxValue: 5 }
612
+ ],
613
+ links: []
614
+ },
615
+ lightning: {
616
+ description: "Lightning talk (10 min)",
617
+ durationMinutes: 10,
618
+ ratings: [
619
+ { label: "Content Quality", maxValue: 5 },
620
+ { label: "Delivery", maxValue: 5 }
621
+ ],
622
+ links: []
623
+ },
624
+ panel: {
625
+ description: "Panel discussion (60 min)",
626
+ durationMinutes: 60,
627
+ ratings: [
628
+ { label: "Topic Relevance", maxValue: 5 },
629
+ { label: "Discussion Quality", maxValue: 5 },
630
+ { label: "Moderation", maxValue: 5 }
631
+ ],
632
+ links: []
633
+ }
634
+ };
635
+ }
636
+ });
637
+
638
+ // src/commands/create.ts
639
+ var create_exports = {};
640
+ __export(create_exports, {
641
+ createCommand: () => createCommand
642
+ });
643
+ import { Command as Command2 } from "commander";
644
+ import * as p2 from "@clack/prompts";
645
+ import qrcode from "qrcode-terminal";
646
+ function padDate(n) {
647
+ return String(n).padStart(2, "0");
648
+ }
649
+ function formatLocalDate(d) {
650
+ return `${d.getFullYear()}-${padDate(d.getMonth() + 1)}-${padDate(d.getDate())}`;
651
+ }
652
+ function formatLocalTime(d) {
653
+ return `${padDate(d.getHours())}:${padDate(d.getMinutes())}`;
654
+ }
655
+ var createCommand;
656
+ var init_create = __esm({
657
+ "src/commands/create.ts"() {
658
+ "use strict";
659
+ init_api_client();
660
+ init_templates();
661
+ init_theme();
662
+ init_render();
663
+ init_KeyValue();
664
+ createCommand = new Command2("create").description("Create a new feedback session").option("--template <type>", "Session template").option("--project <name>", "Project name (optional)").option("--name <name>", "Session name").option("--date <date>", "Session date (YYYY-MM-DD)").option("--time <time>", "Session time (HH:MM, 24h format)").option(
665
+ "--duration <minutes>",
666
+ "Duration in minutes",
667
+ parseInt
668
+ ).option("--json", "Output as JSON").action(async (options) => {
669
+ const isInteractive = !options.json && process.stdout.isTTY && !options.template;
670
+ if (isInteractive) {
671
+ p2.intro(`${BRAND.name} -- Create Session`);
672
+ }
673
+ try {
674
+ let templateName = options.template;
675
+ if (!templateName) {
676
+ if (!isInteractive) {
677
+ p2.log.error(
678
+ "Missing --template. Available: " + getTemplateNames().join(", ")
679
+ );
680
+ process.exit(1);
681
+ }
682
+ const selected = await p2.select({
683
+ message: "What type of session?",
684
+ options: Object.entries(TEMPLATES).map(([key, tmpl]) => ({
685
+ value: key,
686
+ label: key.charAt(0).toUpperCase() + key.slice(1),
687
+ hint: tmpl.description
688
+ }))
689
+ });
690
+ if (p2.isCancel(selected)) {
691
+ p2.cancel("Cancelled.");
692
+ process.exit(0);
693
+ }
694
+ templateName = selected;
695
+ }
696
+ const template = getTemplate(templateName);
697
+ if (!template) {
698
+ p2.log.error(
699
+ `Unknown template: "${templateName}". Available: ${getTemplateNames().join(", ")}`
700
+ );
701
+ process.exit(1);
702
+ }
703
+ let sessionName = options.name;
704
+ if (!sessionName && isInteractive) {
705
+ const input = await p2.text({
706
+ message: "Session name?",
707
+ placeholder: "My Talk at DevFest 2026",
708
+ validate: (v) => {
709
+ if (!v?.trim()) return "Session name is required";
710
+ }
711
+ });
712
+ if (p2.isCancel(input)) {
713
+ p2.cancel("Cancelled.");
714
+ process.exit(0);
715
+ }
716
+ sessionName = input;
717
+ }
718
+ if (!sessionName) {
719
+ p2.log.error("Missing --name flag.");
720
+ process.exit(1);
721
+ }
722
+ const now = /* @__PURE__ */ new Date();
723
+ let sessionDate;
724
+ if (options.date) {
725
+ const time = options.time || "00:00";
726
+ sessionDate = (/* @__PURE__ */ new Date(`${options.date}T${time}`)).toISOString();
727
+ } else if (isInteractive) {
728
+ const dateInput = await p2.text({
729
+ message: "Session date? (YYYY-MM-DD)",
730
+ placeholder: formatLocalDate(now),
731
+ defaultValue: formatLocalDate(now),
732
+ validate: (v) => {
733
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(v || ""))
734
+ return "Use YYYY-MM-DD format";
735
+ if (isNaN(new Date(v).getTime()))
736
+ return "Invalid date";
737
+ }
738
+ });
739
+ if (p2.isCancel(dateInput)) {
740
+ p2.cancel("Cancelled.");
741
+ process.exit(0);
742
+ }
743
+ const timeInput = await p2.text({
744
+ message: "Start time? (24h format)",
745
+ placeholder: formatLocalTime(now),
746
+ defaultValue: formatLocalTime(now),
747
+ validate: (v) => {
748
+ if (!/^\d{1,2}:\d{2}$/.test(v || ""))
749
+ return "Use HH:MM format (e.g. 14:30)";
750
+ }
751
+ });
752
+ if (p2.isCancel(timeInput)) {
753
+ p2.cancel("Cancelled.");
754
+ process.exit(0);
755
+ }
756
+ sessionDate = (/* @__PURE__ */ new Date(`${dateInput}T${timeInput}`)).toISOString();
757
+ } else {
758
+ sessionDate = now.toISOString();
759
+ }
760
+ let duration = options.duration || template.durationMinutes;
761
+ if (!options.duration && isInteractive) {
762
+ const input = await p2.text({
763
+ message: "Duration (minutes)?",
764
+ defaultValue: String(template.durationMinutes),
765
+ placeholder: String(template.durationMinutes),
766
+ validate: (v) => {
767
+ const n = parseInt(v || "", 10);
768
+ if (isNaN(n) || n < 1) return "Enter a valid number of minutes";
769
+ }
770
+ });
771
+ if (p2.isCancel(input)) {
772
+ p2.cancel("Cancelled.");
773
+ process.exit(0);
774
+ }
775
+ duration = parseInt(input, 10);
776
+ }
777
+ const s = p2.spinner();
778
+ s.start("Creating session...");
779
+ const client = createApiClient();
780
+ const userId = getUserIdFromToken();
781
+ let projectId;
782
+ const projectName = options.project || "";
783
+ if (projectName) {
784
+ try {
785
+ s.message("Resolving project...");
786
+ const projectsResp = await client.get(
787
+ `/users/${userId}/projects`
788
+ );
789
+ const projects = Array.isArray(projectsResp) ? projectsResp : projectsResp?.data || [];
790
+ const existing = projects.find(
791
+ (proj) => proj.name.toLowerCase() === projectName.toLowerCase()
792
+ );
793
+ if (existing) {
794
+ projectId = existing.id;
795
+ } else {
796
+ s.message("Creating project...");
797
+ const newProject = await client.post(
798
+ `/users/${userId}/projects`,
799
+ { name: projectName }
800
+ );
801
+ projectId = newProject.id;
802
+ }
803
+ } catch {
804
+ }
805
+ }
806
+ const sessionBody = {
807
+ name: sessionName,
808
+ date: sessionDate,
809
+ durationMinutes: duration,
810
+ ratings: [...template.ratings],
811
+ links: [...template.links],
812
+ ...projectId && { projectId }
813
+ };
814
+ s.message("Creating session...");
815
+ const session = await client.post("/sessions", sessionBody);
816
+ s.stop(`${icon.check} Session created!`);
817
+ if (options.json) {
818
+ console.log(JSON.stringify(session, null, 2));
819
+ } else {
820
+ const pairs = [
821
+ ["Name", String(session.name || sessionName)],
822
+ ["PIN", String(session.pin || "")],
823
+ ["Template", templateName],
824
+ ["Duration", `${duration} minutes`],
825
+ ["Date", new Date(sessionDate).toLocaleString()]
826
+ ];
827
+ if (session.id) {
828
+ pairs.push(["ID", String(session.id)]);
829
+ }
830
+ console.log();
831
+ const output = renderJsxToString(
832
+ getTermWidth(),
833
+ pairs.length,
834
+ KeyValue({ pairs })
835
+ );
836
+ console.log(output);
837
+ if (session.pin) {
838
+ const joinUrl = `https://app.audiencemeter.pro/s/${session.pin}`;
839
+ console.log();
840
+ p2.note(joinUrl, "Join URL");
841
+ console.log();
842
+ await new Promise((resolve) => {
843
+ qrcode.generate(joinUrl, { small: true }, (code) => {
844
+ console.log(code);
845
+ resolve();
846
+ });
847
+ });
848
+ }
849
+ p2.outro("Share the PIN, URL, or QR code with your audience!");
850
+ }
851
+ } catch (error) {
852
+ p2.log.error(
853
+ `Failed to create session: ${error instanceof Error ? error.message : "Unknown error"}`
854
+ );
855
+ if (error instanceof Error && error.message.includes("Not authenticated")) {
856
+ p2.log.warn("Run: audiencemeter login");
857
+ }
858
+ process.exit(1);
859
+ }
860
+ });
861
+ }
862
+ });
863
+
864
+ // src/lib/node-backend.ts
865
+ function createNodeBackend() {
866
+ const stdout = process.stdout;
867
+ return {
868
+ size: () => ({
869
+ width: stdout.columns || 80,
870
+ height: stdout.rows || 24
871
+ }),
872
+ draw: (content) => {
873
+ let out = "";
874
+ for (const { x, y, cell } of content) {
875
+ out += `${CSI}${y + 1};${x + 1}H`;
876
+ out += cellToAnsi(cell);
877
+ }
878
+ stdout.write(out);
879
+ },
880
+ flush: () => {
881
+ },
882
+ hideCursor: () => {
883
+ stdout.write(`${CSI}?25l`);
884
+ },
885
+ showCursor: () => {
886
+ stdout.write(`${CSI}?25h`);
887
+ },
888
+ getCursorPosition: () => ({ x: 0, y: 0 }),
889
+ setCursorPosition: (pos) => {
890
+ stdout.write(`${CSI}${pos.y + 1};${pos.x + 1}H`);
891
+ },
892
+ clear: () => {
893
+ stdout.write(`${CSI}2J${CSI}H`);
894
+ }
895
+ };
896
+ }
897
+ var CSI;
898
+ var init_node_backend = __esm({
899
+ "src/lib/node-backend.ts"() {
900
+ "use strict";
901
+ init_render();
902
+ CSI = "\x1B[";
903
+ }
904
+ });
905
+
906
+ // src/lib/input.ts
907
+ var input_exports = {};
908
+ __export(input_exports, {
909
+ enterFullScreen: () => enterFullScreen,
910
+ exitFullScreen: () => exitFullScreen,
911
+ setupKeyboardInput: () => setupKeyboardInput
912
+ });
913
+ import readline from "readline";
914
+ function setupKeyboardInput(handler) {
915
+ readline.emitKeypressEvents(process.stdin);
916
+ if (process.stdin.isTTY) {
917
+ process.stdin.setRawMode(true);
918
+ }
919
+ process.stdin.resume();
920
+ const listener = (_str, key) => {
921
+ if (key) {
922
+ handler({
923
+ name: key.name ?? "",
924
+ ctrl: key.ctrl ?? false,
925
+ meta: key.meta ?? false,
926
+ shift: key.shift ?? false,
927
+ sequence: key.sequence ?? ""
928
+ });
929
+ }
930
+ };
931
+ process.stdin.on("keypress", listener);
932
+ return () => {
933
+ process.stdin.removeListener("keypress", listener);
934
+ if (process.stdin.isTTY) {
935
+ process.stdin.setRawMode(false);
936
+ }
937
+ process.stdin.pause();
938
+ };
939
+ }
940
+ function enterFullScreen() {
941
+ if (isFullScreen) return;
942
+ isFullScreen = true;
943
+ process.stdout.write(ENTER_ALT_SCREEN);
944
+ process.stdout.write(HIDE_CURSOR);
945
+ }
946
+ function exitFullScreen() {
947
+ if (!isFullScreen) return;
948
+ isFullScreen = false;
949
+ process.stdout.write(SHOW_CURSOR);
950
+ process.stdout.write(EXIT_ALT_SCREEN);
951
+ }
952
+ var ENTER_ALT_SCREEN, EXIT_ALT_SCREEN, HIDE_CURSOR, SHOW_CURSOR, isFullScreen;
953
+ var init_input = __esm({
954
+ "src/lib/input.ts"() {
955
+ "use strict";
956
+ ENTER_ALT_SCREEN = "\x1B[?1049h";
957
+ EXIT_ALT_SCREEN = "\x1B[?1049l";
958
+ HIDE_CURSOR = "\x1B[?25l";
959
+ SHOW_CURSOR = "\x1B[?25h";
960
+ isFullScreen = false;
961
+ process.on("exit", () => {
962
+ if (isFullScreen) {
963
+ try {
964
+ process.stdout.write(SHOW_CURSOR);
965
+ } catch {
966
+ }
967
+ try {
968
+ process.stdout.write(EXIT_ALT_SCREEN);
969
+ } catch {
970
+ }
971
+ isFullScreen = false;
972
+ }
973
+ });
974
+ process.on("SIGINT", () => {
975
+ if (isFullScreen) {
976
+ exitFullScreen();
977
+ }
978
+ process.exit(0);
979
+ });
980
+ process.on("SIGTERM", () => {
981
+ if (isFullScreen) {
982
+ exitFullScreen();
983
+ }
984
+ process.exit(0);
985
+ });
986
+ process.on("uncaughtException", (err) => {
987
+ if (isFullScreen) {
988
+ exitFullScreen();
989
+ }
990
+ console.error(err);
991
+ process.exit(1);
992
+ });
993
+ }
994
+ });
995
+
996
+ // src/components/Header.tsx
997
+ import { Text as Text2 } from "terminui/jsx";
998
+ import { jsx as jsx3 } from "terminui/jsx-runtime";
999
+ function Header({ version } = {}) {
1000
+ const ver = version || BRAND.version;
1001
+ return /* @__PURE__ */ jsx3(Text2, { bold: true, fg: BRAND_COLORS.purple, align: "center", children: `${BRAND.name} v${ver}` });
1002
+ }
1003
+ var init_Header = __esm({
1004
+ "src/components/Header.tsx"() {
1005
+ "use strict";
1006
+ init_theme();
1007
+ }
1008
+ });
1009
+
1010
+ // src/components/TabBar.tsx
1011
+ import { Tabs } from "terminui/jsx";
1012
+ import { createStyle, Modifier } from "terminui";
1013
+ import { jsx as jsx4 } from "terminui/jsx-runtime";
1014
+ function TabBar({ titles, selected }) {
1015
+ return /* @__PURE__ */ jsx4(
1016
+ Tabs,
1017
+ {
1018
+ titles,
1019
+ selected,
1020
+ border: true,
1021
+ fg: BRAND_COLORS.dimCyan,
1022
+ highlightStyle: createStyle({ fg: BRAND_COLORS.purple, addModifier: Modifier.BOLD })
1023
+ }
1024
+ );
1025
+ }
1026
+ var init_TabBar = __esm({
1027
+ "src/components/TabBar.tsx"() {
1028
+ "use strict";
1029
+ init_theme();
1030
+ }
1031
+ });
1032
+
1033
+ // src/components/ShortcutBar.tsx
1034
+ import { Text as Text3 } from "terminui/jsx";
1035
+ import { jsx as jsx5 } from "terminui/jsx-runtime";
1036
+ function ShortcutBar({ shortcuts }) {
1037
+ const text2 = shortcuts.map((s) => `${s.key}: ${s.label}`).join(" | ");
1038
+ return /* @__PURE__ */ jsx5(Text3, { fg: BRAND_COLORS.dimCyan, children: ` ${text2}` });
1039
+ }
1040
+ var init_ShortcutBar = __esm({
1041
+ "src/components/ShortcutBar.tsx"() {
1042
+ "use strict";
1043
+ init_theme();
1044
+ }
1045
+ });
1046
+
1047
+ // src/components/Dashboard.tsx
1048
+ import { VStack as VStack2, HStack as HStack2, Box as Box2, Text as Text4, List, Gauge } from "terminui/jsx";
1049
+ import { fillConstraint as fillConstraint3, lengthConstraint as lengthConstraint3, createListState, createStyle as createStyle2, Modifier as Modifier2 } from "terminui";
1050
+ import { jsx as jsx6, jsxs as jsxs2 } from "terminui/jsx-runtime";
1051
+ function DashboardTab({ sessions, stats, selectedIndex, loading }) {
1052
+ const recent = sessions.slice(0, 5);
1053
+ const listItems = recent.length > 0 ? recent.map((s) => `${icon.session} ${s.name} ${s.pin} ${s.status}`) : [loading ? "Loading sessions..." : "No sessions yet. Press Tab to go to Create."];
1054
+ const listState = createListState();
1055
+ listState.selected = recent.length > 0 ? selectedIndex : void 0;
1056
+ return /* @__PURE__ */ jsxs2(HStack2, { constraints: [fillConstraint3(2), fillConstraint3(1)], children: [
1057
+ /* @__PURE__ */ jsx6(Box2, { border: true, borderType: "rounded", fg: BRAND_COLORS.dimCyan, title: " Recent Sessions ", children: /* @__PURE__ */ jsx6(
1058
+ List,
1059
+ {
1060
+ items: listItems,
1061
+ state: listState,
1062
+ fg: BRAND_COLORS.cyan,
1063
+ highlightStyle: createStyle2({ fg: BRAND_COLORS.purple, addModifier: Modifier2.BOLD })
1064
+ }
1065
+ ) }),
1066
+ /* @__PURE__ */ jsxs2(VStack2, { constraints: [lengthConstraint3(3), lengthConstraint3(3), fillConstraint3(1)], children: [
1067
+ /* @__PURE__ */ jsx6(Box2, { border: true, borderType: "rounded", fg: BRAND_COLORS.dimCyan, title: " Sessions ", children: /* @__PURE__ */ jsx6(Text4, { bold: true, fg: BRAND_COLORS.white, align: "center", children: String(stats.totalSessions) }) }),
1068
+ /* @__PURE__ */ jsx6(Box2, { border: true, borderType: "rounded", fg: BRAND_COLORS.dimCyan, title: " Participants ", children: /* @__PURE__ */ jsx6(Text4, { bold: true, fg: BRAND_COLORS.white, align: "center", children: String(stats.totalParticipants) }) }),
1069
+ /* @__PURE__ */ jsx6(Box2, { border: true, borderType: "rounded", fg: BRAND_COLORS.dimCyan, title: " Engagement ", children: /* @__PURE__ */ jsx6(
1070
+ Gauge,
1071
+ {
1072
+ percent: stats.avgEngagement,
1073
+ fg: BRAND_COLORS.green
1074
+ }
1075
+ ) })
1076
+ ] })
1077
+ ] });
1078
+ }
1079
+ function SessionsTab({ sessions, selectedIndex, loading }) {
1080
+ if (sessions.length === 0) {
1081
+ return /* @__PURE__ */ jsx6(Box2, { border: true, borderType: "rounded", fg: BRAND_COLORS.dimCyan, children: /* @__PURE__ */ jsx6(Text4, { fg: BRAND_COLORS.dim, children: loading ? "Loading sessions..." : "No sessions found. Create one with the Create tab." }) });
1082
+ }
1083
+ const listItems = sessions.map(
1084
+ (s) => `${icon.session} ${s.name.padEnd(30).slice(0, 30)} ${s.pin} ${s.status.padEnd(10).slice(0, 10)} ${s.date}`
1085
+ );
1086
+ const listState = createListState();
1087
+ listState.selected = selectedIndex;
1088
+ return /* @__PURE__ */ jsx6(Box2, { border: true, borderType: "rounded", fg: BRAND_COLORS.dimCyan, title: ` Sessions (${sessions.length}) `, children: /* @__PURE__ */ jsx6(
1089
+ List,
1090
+ {
1091
+ items: listItems,
1092
+ state: listState,
1093
+ fg: BRAND_COLORS.cyan,
1094
+ highlightStyle: createStyle2({ fg: BRAND_COLORS.purple, addModifier: Modifier2.BOLD })
1095
+ }
1096
+ ) });
1097
+ }
1098
+ function CreateTab() {
1099
+ return /* @__PURE__ */ jsx6(Box2, { border: true, borderType: "rounded", fg: BRAND_COLORS.dimCyan, title: " Create Session ", children: /* @__PURE__ */ jsx6(Text4, { fg: BRAND_COLORS.white, children: ` Press Enter to create a new session` }) });
1100
+ }
1101
+ function AuthTab({ authEmail, authExpired }) {
1102
+ if (authExpired) {
1103
+ return /* @__PURE__ */ jsx6(Box2, { border: true, borderType: "rounded", title: " Authentication ", children: /* @__PURE__ */ jsx6(Text4, { fg: BRAND_COLORS.yellow, children: ` ${icon.cross} Session expired. Press Enter to re-login.` }) });
1104
+ }
1105
+ return /* @__PURE__ */ jsx6(Box2, { border: true, borderType: "rounded", title: " Authentication ", children: /* @__PURE__ */ jsx6(Text4, { fg: authEmail ? BRAND_COLORS.green : BRAND_COLORS.yellow, children: authEmail ? ` ${icon.check} Logged in as ${authEmail}` : ` ${icon.cross} Not logged in. Press Enter to login.` }) });
1106
+ }
1107
+ function Dashboard({ selectedTab, sessions, stats, selectedIndex, authEmail, authExpired, loading, error }) {
1108
+ let content;
1109
+ switch (selectedTab) {
1110
+ case 0:
1111
+ content = /* @__PURE__ */ jsx6(DashboardTab, { sessions, stats, selectedIndex, loading });
1112
+ break;
1113
+ case 1:
1114
+ content = /* @__PURE__ */ jsx6(SessionsTab, { sessions, selectedIndex, loading });
1115
+ break;
1116
+ case 2:
1117
+ content = /* @__PURE__ */ jsx6(CreateTab, {});
1118
+ break;
1119
+ case 3:
1120
+ content = /* @__PURE__ */ jsx6(AuthTab, { authEmail, authExpired });
1121
+ break;
1122
+ default:
1123
+ content = /* @__PURE__ */ jsx6(DashboardTab, { sessions, stats, selectedIndex });
1124
+ }
1125
+ if (error) {
1126
+ return /* @__PURE__ */ jsxs2(VStack2, { constraints: [lengthConstraint3(1), lengthConstraint3(3), lengthConstraint3(1), fillConstraint3(1), lengthConstraint3(1)], children: [
1127
+ /* @__PURE__ */ jsx6(Header, {}),
1128
+ /* @__PURE__ */ jsx6(TabBar, { titles: [...TAB_TITLES], selected: selectedTab }),
1129
+ /* @__PURE__ */ jsx6(Text4, { fg: BRAND_COLORS.yellow, align: "center", children: `${icon.cross} ${error}` }),
1130
+ content,
1131
+ /* @__PURE__ */ jsx6(ShortcutBar, { shortcuts: SHORTCUTS })
1132
+ ] });
1133
+ }
1134
+ return /* @__PURE__ */ jsxs2(VStack2, { constraints: [lengthConstraint3(1), lengthConstraint3(3), fillConstraint3(1), lengthConstraint3(1)], children: [
1135
+ /* @__PURE__ */ jsx6(Header, {}),
1136
+ /* @__PURE__ */ jsx6(TabBar, { titles: [...TAB_TITLES], selected: selectedTab }),
1137
+ content,
1138
+ /* @__PURE__ */ jsx6(ShortcutBar, { shortcuts: SHORTCUTS })
1139
+ ] });
1140
+ }
1141
+ var TAB_TITLES, SHORTCUTS;
1142
+ var init_Dashboard = __esm({
1143
+ "src/components/Dashboard.tsx"() {
1144
+ "use strict";
1145
+ init_theme();
1146
+ init_Header();
1147
+ init_TabBar();
1148
+ init_ShortcutBar();
1149
+ TAB_TITLES = ["Dashboard", "Sessions", "Create", "Auth"];
1150
+ SHORTCUTS = [
1151
+ { key: "Tab/1-4", label: "switch" },
1152
+ { key: "j/k", label: "navigate" },
1153
+ { key: "Enter", label: "select" },
1154
+ { key: "Esc/q", label: "quit" }
1155
+ ];
1156
+ }
1157
+ });
1158
+
1159
+ // src/components/MetricsPanel.tsx
1160
+ import { VStack as VStack3, Gauge as Gauge2, LineGauge, Text as Text5 } from "terminui/jsx";
1161
+ import { lengthConstraint as lengthConstraint4 } from "terminui";
1162
+ import { jsx as jsx7 } from "terminui/jsx-runtime";
1163
+ function gaugeColor(ratio) {
1164
+ if (ratio >= 70) return BRAND_COLORS.green;
1165
+ if (ratio >= 40) return BRAND_COLORS.cyan;
1166
+ return BRAND_COLORS.yellow;
1167
+ }
1168
+ function MetricsPanel({ engagementRate, feedbackRate, participants }) {
1169
+ const items = [];
1170
+ const constraints = [];
1171
+ if (participants !== void 0) {
1172
+ items.push(
1173
+ /* @__PURE__ */ jsx7(Text5, { bold: true, fg: BRAND_COLORS.white, children: ` ${String(participants)} participants` })
1174
+ );
1175
+ constraints.push(lengthConstraint4(1));
1176
+ }
1177
+ if (engagementRate !== void 0 && engagementRate > 0) {
1178
+ items.push(
1179
+ /* @__PURE__ */ jsx7(
1180
+ Gauge2,
1181
+ {
1182
+ percent: engagementRate,
1183
+ border: true,
1184
+ title: ` Engagement ${engagementRate}% `,
1185
+ fg: gaugeColor(engagementRate)
1186
+ }
1187
+ )
1188
+ );
1189
+ constraints.push(lengthConstraint4(3));
1190
+ }
1191
+ if (feedbackRate !== void 0 && feedbackRate > 0) {
1192
+ items.push(
1193
+ /* @__PURE__ */ jsx7(
1194
+ LineGauge,
1195
+ {
1196
+ percent: feedbackRate,
1197
+ border: true,
1198
+ title: ` Feedback ${feedbackRate}% `,
1199
+ fg: gaugeColor(feedbackRate)
1200
+ }
1201
+ )
1202
+ );
1203
+ constraints.push(lengthConstraint4(3));
1204
+ }
1205
+ if (items.length === 0) {
1206
+ return /* @__PURE__ */ jsx7(Text5, { fg: BRAND_COLORS.dim, children: "No metrics available" });
1207
+ }
1208
+ return /* @__PURE__ */ jsx7(VStack3, { constraints, children: items });
1209
+ }
1210
+ var init_MetricsPanel = __esm({
1211
+ "src/components/MetricsPanel.tsx"() {
1212
+ "use strict";
1213
+ init_theme();
1214
+ }
1215
+ });
1216
+
1217
+ // src/components/SessionDetail.tsx
1218
+ import { VStack as VStack4, HStack as HStack3, Box as Box4, Text as Text6, Sparkline, BarChart } from "terminui/jsx";
1219
+ import { fillConstraint as fillConstraint4, lengthConstraint as lengthConstraint5, createBarGroup, createBar } from "terminui";
1220
+ import { jsx as jsx8, jsxs as jsxs3 } from "terminui/jsx-runtime";
1221
+ function statusBadge(status) {
1222
+ switch (status) {
1223
+ case "live":
1224
+ return `${icon.live} live`;
1225
+ case "ended":
1226
+ return `${icon.ended} ended`;
1227
+ case "upcoming":
1228
+ return `${icon.upcoming} upcoming`;
1229
+ default:
1230
+ return status;
1231
+ }
1232
+ }
1233
+ function SessionDetail({ session, metrics, timeline, analysis, status }) {
1234
+ const sections = [];
1235
+ const sectionConstraints = [];
1236
+ sections.push(
1237
+ /* @__PURE__ */ jsx8(Text6, { bold: true, fg: BRAND_COLORS.purple, children: ` ${session.name || "Untitled Session"} ${statusBadge(status)}` })
1238
+ );
1239
+ sectionConstraints.push(lengthConstraint5(1));
1240
+ const dateStr = session.date ? new Date(session.date).toLocaleString() : session.createdAt ? new Date(session.createdAt).toLocaleString() : "--";
1241
+ const durationStr = session.durationMinutes ? `${session.durationMinutes} minutes` : "--";
1242
+ const infoPairs = [
1243
+ ["PIN", session.pin || "--"],
1244
+ ["Date", dateStr],
1245
+ ["Duration", durationStr]
1246
+ ];
1247
+ if (session.id) {
1248
+ infoPairs.push(["ID", session.id]);
1249
+ }
1250
+ const hasMetrics = metrics && (metrics.engagementRate !== void 0 && metrics.engagementRate > 0 || metrics.feedbackCompletionRate !== void 0 && metrics.feedbackCompletionRate > 0 || metrics.totalParticipants !== void 0);
1251
+ if (hasMetrics) {
1252
+ const metricsHeight = 1 + (metrics.totalParticipants !== void 0 ? 1 : 0) + (metrics.engagementRate !== void 0 && metrics.engagementRate > 0 ? 3 : 0) + (metrics.feedbackCompletionRate !== void 0 && metrics.feedbackCompletionRate > 0 ? 3 : 0);
1253
+ const infoHeight = infoPairs.length + 2;
1254
+ const panelHeight = Math.max(infoHeight, metricsHeight + 2);
1255
+ sections.push(
1256
+ /* @__PURE__ */ jsxs3(HStack3, { constraints: [fillConstraint4(1), fillConstraint4(1)], children: [
1257
+ /* @__PURE__ */ jsx8(Box4, { border: true, borderType: "rounded", fg: BRAND_COLORS.dimCyan, title: " Session Info ", children: /* @__PURE__ */ jsx8(KeyValue, { pairs: infoPairs }) }),
1258
+ /* @__PURE__ */ jsx8(Box4, { border: true, borderType: "rounded", fg: BRAND_COLORS.dimCyan, title: " Metrics ", children: /* @__PURE__ */ jsx8(
1259
+ MetricsPanel,
1260
+ {
1261
+ engagementRate: metrics.engagementRate,
1262
+ feedbackRate: metrics.feedbackCompletionRate,
1263
+ participants: metrics.totalParticipants
1264
+ }
1265
+ ) })
1266
+ ] })
1267
+ );
1268
+ sectionConstraints.push(lengthConstraint5(panelHeight));
1269
+ } else {
1270
+ sections.push(
1271
+ /* @__PURE__ */ jsx8(Box4, { border: true, borderType: "rounded", fg: BRAND_COLORS.dimCyan, title: " Session Info ", children: /* @__PURE__ */ jsx8(KeyValue, { pairs: infoPairs }) })
1272
+ );
1273
+ sectionConstraints.push(lengthConstraint5(infoPairs.length + 2));
1274
+ }
1275
+ const positiveData = timeline.map((b) => b.positive || 0);
1276
+ if (positiveData.some((v) => v > 0)) {
1277
+ sections.push(
1278
+ /* @__PURE__ */ jsx8(Box4, { border: true, borderType: "rounded", fg: BRAND_COLORS.dimCyan, title: " Reactions Over Time ", children: /* @__PURE__ */ jsx8(
1279
+ Sparkline,
1280
+ {
1281
+ data: positiveData,
1282
+ fg: BRAND_COLORS.green
1283
+ }
1284
+ ) })
1285
+ );
1286
+ sectionConstraints.push(lengthConstraint5(5));
1287
+ }
1288
+ const totalPositive = timeline.reduce((sum, b) => sum + (b.positive || 0), 0);
1289
+ const totalNegative = timeline.reduce((sum, b) => sum + (b.negative || 0), 0);
1290
+ if (totalPositive > 0 || totalNegative > 0) {
1291
+ const barData = [
1292
+ createBarGroup([createBar(totalPositive)], "Positive"),
1293
+ createBarGroup([createBar(totalNegative)], "Negative")
1294
+ ];
1295
+ sections.push(
1296
+ /* @__PURE__ */ jsx8(Box4, { border: true, borderType: "rounded", fg: BRAND_COLORS.dimCyan, title: " Reaction Summary ", children: /* @__PURE__ */ jsx8(
1297
+ BarChart,
1298
+ {
1299
+ data: barData,
1300
+ fg: BRAND_COLORS.cyan
1301
+ }
1302
+ ) })
1303
+ );
1304
+ const maxVal = Math.max(totalPositive, totalNegative, 1);
1305
+ sectionConstraints.push(lengthConstraint5(Math.max(8, maxVal + 4)));
1306
+ }
1307
+ if (analysis && typeof analysis === "object") {
1308
+ const summary = analysis.summary || analysis.feedbackSummary || "";
1309
+ if (summary) {
1310
+ sections.push(
1311
+ /* @__PURE__ */ jsx8(Box4, { border: true, borderType: "rounded", fg: BRAND_COLORS.purple, title: ` ${icon.sparkle} AI Analysis `, children: /* @__PURE__ */ jsx8(Text6, { fg: BRAND_COLORS.white, children: summary }) })
1312
+ );
1313
+ const summaryLines = Math.ceil(summary.length / 70) + 2;
1314
+ sectionConstraints.push(lengthConstraint5(summaryLines));
1315
+ }
1316
+ const insights = analysis.coachingInsights;
1317
+ if (insights && insights.length > 0) {
1318
+ const insightLines = insights.slice(0, 3).map(
1319
+ (insight) => ` ${icon.arrow} ${insight}`
1320
+ ).join("\n");
1321
+ sections.push(
1322
+ /* @__PURE__ */ jsx8(Box4, { border: true, borderType: "rounded", fg: BRAND_COLORS.dimCyan, title: " Coaching Insights ", children: /* @__PURE__ */ jsx8(Text6, { fg: BRAND_COLORS.white, children: insightLines }) })
1323
+ );
1324
+ sectionConstraints.push(lengthConstraint5(Math.min(insights.length, 3) + 2));
1325
+ }
1326
+ }
1327
+ if (session.questions && session.questions.length > 0) {
1328
+ const questionLines = session.questions.map(
1329
+ (q) => ` ${icon.dot} [${q.type}] ${q.label}`
1330
+ ).join("\n");
1331
+ sections.push(
1332
+ /* @__PURE__ */ jsx8(Box4, { border: true, borderType: "rounded", fg: BRAND_COLORS.dimCyan, title: " Rating Criteria ", children: /* @__PURE__ */ jsx8(Text6, { fg: BRAND_COLORS.white, children: questionLines }) })
1333
+ );
1334
+ sectionConstraints.push(lengthConstraint5(session.questions.length + 2));
1335
+ }
1336
+ if (session.links && session.links.length > 0) {
1337
+ const linkLines = session.links.map(
1338
+ (link) => ` ${icon.dot} ${link.label}: ${link.url}`
1339
+ ).join("\n");
1340
+ sections.push(
1341
+ /* @__PURE__ */ jsx8(Box4, { border: true, borderType: "rounded", fg: BRAND_COLORS.dimCyan, title: " Links ", children: /* @__PURE__ */ jsx8(Text6, { fg: BRAND_COLORS.white, children: linkLines }) })
1342
+ );
1343
+ sectionConstraints.push(lengthConstraint5(session.links.length + 2));
1344
+ }
1345
+ if (session.pin) {
1346
+ sections.push(
1347
+ /* @__PURE__ */ jsx8(Box4, { border: true, borderType: "rounded", fg: BRAND_COLORS.dimCyan, title: " Join URL ", children: /* @__PURE__ */ jsx8(Text6, { bold: true, fg: BRAND_COLORS.cyan, children: `https://app.audiencemeter.pro/s/${session.pin}` }) })
1348
+ );
1349
+ sectionConstraints.push(lengthConstraint5(3));
1350
+ }
1351
+ return /* @__PURE__ */ jsx8(VStack4, { constraints: sectionConstraints, children: sections });
1352
+ }
1353
+ var init_SessionDetail = __esm({
1354
+ "src/components/SessionDetail.tsx"() {
1355
+ "use strict";
1356
+ init_theme();
1357
+ init_KeyValue();
1358
+ init_MetricsPanel();
1359
+ }
1360
+ });
1361
+
1362
+ // src/lib/app.tsx
1363
+ var app_exports = {};
1364
+ __export(app_exports, {
1365
+ startDashboard: () => startDashboard,
1366
+ startInteractiveList: () => startInteractiveList
1367
+ });
1368
+ import { createTerminal as createTerminal2, terminalResize, createListState as createListState2 } from "terminui";
1369
+ import { VStack as VStack5, Box as Box5, List as List2, Text as Text7, terminalDrawJsx as terminalDrawJsx2 } from "terminui/jsx";
1370
+ import { fillConstraint as fillConstraint5, lengthConstraint as lengthConstraint6, createStyle as createStyle3, Modifier as Modifier3 } from "terminui";
1371
+ import { jsx as jsx9, jsxs as jsxs4 } from "terminui/jsx-runtime";
1372
+ function getSessionStatus(session) {
1373
+ const sessionDate = session.date ? new Date(session.date) : session.createdAt ? new Date(session.createdAt) : null;
1374
+ if (!sessionDate) return "unknown";
1375
+ const now = /* @__PURE__ */ new Date();
1376
+ const durationMs = (session.durationMinutes || 60) * 60 * 1e3;
1377
+ const endTime = new Date(sessionDate.getTime() + durationMs);
1378
+ if (now < sessionDate) return "upcoming";
1379
+ if (now >= sessionDate && now <= endTime) return "live";
1380
+ return "ended";
1381
+ }
1382
+ function statusDisplay(status) {
1383
+ switch (status) {
1384
+ case "live":
1385
+ return `${icon.live} live`;
1386
+ case "ended":
1387
+ return `${icon.ended} ended`;
1388
+ case "upcoming":
1389
+ return `${icon.upcoming} soon`;
1390
+ default:
1391
+ return status;
1392
+ }
1393
+ }
1394
+ function toSessionRows(sessions) {
1395
+ return sessions.map((session) => {
1396
+ const date = session.date ? new Date(session.date).toLocaleDateString() : session.createdAt ? new Date(session.createdAt).toLocaleDateString() : "--";
1397
+ const duration = session.durationMinutes ? `${session.durationMinutes}min` : "--";
1398
+ const status = getSessionStatus(session);
1399
+ const raw = session;
1400
+ const count = raw._count?.attendees ?? raw.participantCount ?? "--";
1401
+ return {
1402
+ name: session.name || "--",
1403
+ pin: session.pin || "--",
1404
+ status: statusDisplay(status),
1405
+ date,
1406
+ duration,
1407
+ participants: String(count)
1408
+ };
1409
+ });
1410
+ }
1411
+ function renderDashboard(terminal, state) {
1412
+ terminalDrawJsx2(terminal, /* @__PURE__ */ jsx9(
1413
+ Dashboard,
1414
+ {
1415
+ selectedTab: state.selectedTab,
1416
+ sessions: state.sessions,
1417
+ stats: state.stats,
1418
+ selectedIndex: state.selectedIndex,
1419
+ authEmail: state.authEmail,
1420
+ authExpired: state.authExpired,
1421
+ loading: state.loading,
1422
+ error: state.error
1423
+ }
1424
+ ));
1425
+ }
1426
+ function renderSessionDetail(terminal, session, metrics, timeline, analysis, status) {
1427
+ const detailShortcuts = [
1428
+ { key: "Esc/q", label: "back" }
1429
+ ];
1430
+ terminalDrawJsx2(terminal, /* @__PURE__ */ jsxs4(VStack5, { constraints: [fillConstraint5(1), lengthConstraint6(1)], children: [
1431
+ /* @__PURE__ */ jsx9(
1432
+ SessionDetail,
1433
+ {
1434
+ session,
1435
+ metrics,
1436
+ timeline,
1437
+ analysis,
1438
+ status
1439
+ }
1440
+ ),
1441
+ /* @__PURE__ */ jsx9(ShortcutBar, { shortcuts: detailShortcuts })
1442
+ ] }));
1443
+ }
1444
+ function renderInteractiveList(terminal, sessionRows, total, selectedIndex) {
1445
+ const listItems = sessionRows.map(
1446
+ (s) => `${icon.session} ${s.name.padEnd(30).slice(0, 30)} ${s.pin} ${s.status.padEnd(10).slice(0, 10)} ${s.date}`
1447
+ );
1448
+ const listState = createListState2();
1449
+ listState.selected = selectedIndex;
1450
+ terminalDrawJsx2(terminal, /* @__PURE__ */ jsxs4(VStack5, { constraints: [lengthConstraint6(1), fillConstraint5(1), lengthConstraint6(1)], children: [
1451
+ /* @__PURE__ */ jsx9(Text7, { bold: true, fg: BRAND_COLORS.purple, align: "center", children: `Sessions (${sessionRows.length} of ${total})` }),
1452
+ /* @__PURE__ */ jsx9(Box5, { border: true, borderType: "rounded", fg: BRAND_COLORS.dimCyan, children: /* @__PURE__ */ jsx9(
1453
+ List2,
1454
+ {
1455
+ items: listItems,
1456
+ state: listState,
1457
+ fg: BRAND_COLORS.cyan,
1458
+ highlightStyle: createStyle3({ fg: BRAND_COLORS.purple, addModifier: Modifier3.BOLD })
1459
+ }
1460
+ ) }),
1461
+ /* @__PURE__ */ jsx9(Text7, { fg: BRAND_COLORS.dimCyan, children: " j/k: navigate | Enter: view | q: quit" })
1462
+ ] }));
1463
+ }
1464
+ async function fetchSessionDetail(sessionId) {
1465
+ const client = createApiClient();
1466
+ const isUuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(sessionId);
1467
+ const path = !isUuid ? `/sessions/by-pin/${sessionId}` : `/sessions/${sessionId}`;
1468
+ const session = await client.get(path);
1469
+ let metrics = null;
1470
+ let timeline = [];
1471
+ let analysis = null;
1472
+ if (session.id) {
1473
+ const results = await Promise.allSettled([
1474
+ client.get(`/sessions/${session.id}/metrics`),
1475
+ client.get(`/sessions/${session.id}/sentiment-timeline`),
1476
+ client.get(`/sessions/${session.id}/analysis`)
1477
+ ]);
1478
+ if (results[0].status === "fulfilled") metrics = results[0].value;
1479
+ if (results[1].status === "fulfilled") timeline = results[1].value || [];
1480
+ if (results[2].status === "fulfilled") analysis = results[2].value;
1481
+ }
1482
+ const status = getSessionStatus(session);
1483
+ return {
1484
+ session: {
1485
+ id: session.id,
1486
+ name: session.name,
1487
+ pin: session.pin,
1488
+ date: session.date ? String(session.date) : void 0,
1489
+ durationMinutes: session.durationMinutes,
1490
+ createdAt: session.createdAt ? String(session.createdAt) : void 0,
1491
+ questions: session.questions,
1492
+ links: session.links
1493
+ },
1494
+ metrics: metrics ? {
1495
+ totalParticipants: metrics.totalParticipants,
1496
+ engagementRate: metrics.engagementRate,
1497
+ feedbackCompletionRate: metrics.feedbackCompletionRate
1498
+ } : null,
1499
+ timeline,
1500
+ analysis,
1501
+ status
1502
+ };
1503
+ }
1504
+ async function startDashboard() {
1505
+ let userId;
1506
+ try {
1507
+ userId = getUserIdFromToken();
1508
+ } catch {
1509
+ console.error("Not authenticated. Run: audiencemeter login");
1510
+ process.exit(1);
1511
+ return;
1512
+ }
1513
+ let authEmail = null;
1514
+ try {
1515
+ authEmail = getUserEmailFromToken();
1516
+ } catch {
1517
+ authEmail = null;
1518
+ }
1519
+ enterFullScreen();
1520
+ const backend = createNodeBackend();
1521
+ const terminal = createTerminal2(backend);
1522
+ let authExpired = false;
1523
+ try {
1524
+ const token = (await Promise.resolve().then(() => (init_config(), config_exports))).getAuthToken();
1525
+ authExpired = isTokenExpired(token);
1526
+ } catch {
1527
+ }
1528
+ const state = {
1529
+ selectedTab: 0,
1530
+ sessions: [],
1531
+ rawSessions: [],
1532
+ total: 0,
1533
+ stats: { totalSessions: 0, totalParticipants: 0, avgEngagement: 0 },
1534
+ selectedIndex: 0,
1535
+ authEmail: authExpired ? null : authEmail,
1536
+ authExpired,
1537
+ showingSession: null,
1538
+ running: true,
1539
+ loading: true,
1540
+ error: null
1541
+ };
1542
+ renderDashboard(terminal, state);
1543
+ let detailData = null;
1544
+ const rerender = () => {
1545
+ if (state.showingSession && detailData) {
1546
+ renderSessionDetail(
1547
+ terminal,
1548
+ detailData.session,
1549
+ detailData.metrics,
1550
+ detailData.timeline,
1551
+ detailData.analysis,
1552
+ detailData.status
1553
+ );
1554
+ } else {
1555
+ renderDashboard(terminal, state);
1556
+ }
1557
+ };
1558
+ const cleanupInput = setupKeyboardInput(async (key) => {
1559
+ if (!state.running) return;
1560
+ if (key.ctrl && key.name === "c") {
1561
+ state.running = false;
1562
+ cleanupInput();
1563
+ exitFullScreen();
1564
+ process.exit(0);
1565
+ return;
1566
+ }
1567
+ if (state.showingSession) {
1568
+ if (key.name === "escape" || key.name === "q") {
1569
+ state.showingSession = null;
1570
+ detailData = null;
1571
+ rerender();
1572
+ }
1573
+ return;
1574
+ }
1575
+ if (key.name === "tab") {
1576
+ state.selectedTab = (state.selectedTab + 1) % 4;
1577
+ state.selectedIndex = 0;
1578
+ rerender();
1579
+ return;
1580
+ }
1581
+ if (["1", "2", "3", "4"].includes(key.name)) {
1582
+ state.selectedTab = parseInt(key.name, 10) - 1;
1583
+ state.selectedIndex = 0;
1584
+ rerender();
1585
+ return;
1586
+ }
1587
+ const itemCount = state.selectedTab === 0 ? Math.min(state.sessions.length, 5) : state.sessions.length;
1588
+ if (itemCount > 0) {
1589
+ if (key.name === "j" || key.name === "down") {
1590
+ state.selectedIndex = (state.selectedIndex + 1) % itemCount;
1591
+ rerender();
1592
+ return;
1593
+ }
1594
+ if (key.name === "k" || key.name === "up") {
1595
+ state.selectedIndex = (state.selectedIndex - 1 + itemCount) % itemCount;
1596
+ rerender();
1597
+ return;
1598
+ }
1599
+ }
1600
+ if (key.name === "return") {
1601
+ if (state.selectedTab === 0 || state.selectedTab === 1) {
1602
+ const rawSession = state.rawSessions[state.selectedIndex];
1603
+ if (rawSession) {
1604
+ const sessionId = rawSession.id || rawSession.pin;
1605
+ if (sessionId) {
1606
+ state.showingSession = sessionId;
1607
+ try {
1608
+ detailData = await fetchSessionDetail(sessionId);
1609
+ } catch {
1610
+ detailData = {
1611
+ session: {
1612
+ name: rawSession.name,
1613
+ pin: rawSession.pin,
1614
+ date: rawSession.date ? String(rawSession.date) : void 0,
1615
+ durationMinutes: rawSession.durationMinutes,
1616
+ createdAt: rawSession.createdAt ? String(rawSession.createdAt) : void 0
1617
+ },
1618
+ metrics: null,
1619
+ timeline: [],
1620
+ analysis: null,
1621
+ status: getSessionStatus(rawSession)
1622
+ };
1623
+ }
1624
+ rerender();
1625
+ }
1626
+ }
1627
+ return;
1628
+ }
1629
+ if (state.selectedTab === 2) {
1630
+ state.running = false;
1631
+ cleanupInput();
1632
+ exitFullScreen();
1633
+ try {
1634
+ const { createCommand: createCommand2 } = await Promise.resolve().then(() => (init_create(), create_exports));
1635
+ await createCommand2.parseAsync(["node", "audiencemeter"], { from: "node" });
1636
+ } catch {
1637
+ }
1638
+ return;
1639
+ }
1640
+ if (state.selectedTab === 3) {
1641
+ state.running = false;
1642
+ cleanupInput();
1643
+ exitFullScreen();
1644
+ try {
1645
+ const { authCommand: authCommand2 } = await Promise.resolve().then(() => (init_auth(), auth_exports));
1646
+ const action = state.authEmail && !state.authExpired ? "whoami" : "login";
1647
+ await authCommand2.parseAsync(["node", "audiencemeter", action], { from: "node" });
1648
+ } catch {
1649
+ }
1650
+ return;
1651
+ }
1652
+ }
1653
+ if (key.name === "q" || key.name === "escape") {
1654
+ state.running = false;
1655
+ cleanupInput();
1656
+ exitFullScreen();
1657
+ return;
1658
+ }
1659
+ });
1660
+ const onResize = () => {
1661
+ terminalResize(terminal, backend.size());
1662
+ rerender();
1663
+ };
1664
+ process.stdout.on("resize", onResize);
1665
+ (async () => {
1666
+ try {
1667
+ const client = await createApiClientWithRefresh();
1668
+ if (state.authExpired) {
1669
+ state.authExpired = false;
1670
+ state.authEmail = getUserEmailFromToken();
1671
+ state.error = null;
1672
+ }
1673
+ const queryParams = new URLSearchParams();
1674
+ queryParams.set("take", "50");
1675
+ queryParams.set("sort", "createdAt");
1676
+ queryParams.set("order", "desc");
1677
+ const response = await client.get(
1678
+ `/users/${userId}/sessions?${queryParams.toString()}`
1679
+ );
1680
+ const sessions = Array.isArray(response) ? response : response?.data || [];
1681
+ const total = Array.isArray(response) ? sessions.length : response?.total || sessions.length;
1682
+ state.rawSessions = sessions;
1683
+ state.sessions = toSessionRows(sessions);
1684
+ state.total = total;
1685
+ let totalParticipants = 0;
1686
+ for (const s of sessions) {
1687
+ const raw = s;
1688
+ const count = raw._count?.attendees ?? raw.participantCount ?? 0;
1689
+ totalParticipants += Number(count) || 0;
1690
+ }
1691
+ state.stats = {
1692
+ totalSessions: total,
1693
+ totalParticipants,
1694
+ avgEngagement: 0
1695
+ };
1696
+ state.loading = false;
1697
+ rerender();
1698
+ } catch (err) {
1699
+ state.loading = false;
1700
+ const msg = err instanceof Error ? err.message : "Failed to load sessions";
1701
+ if (msg.includes("expired") || msg.includes("Not authenticated")) {
1702
+ state.authExpired = true;
1703
+ state.authEmail = null;
1704
+ state.error = "Session expired. Go to Auth tab and press Enter to re-login.";
1705
+ } else {
1706
+ state.error = msg;
1707
+ }
1708
+ rerender();
1709
+ }
1710
+ })();
1711
+ return new Promise((resolve) => {
1712
+ const check = setInterval(() => {
1713
+ if (!state.running) {
1714
+ clearInterval(check);
1715
+ process.stdout.removeListener("resize", onResize);
1716
+ resolve();
1717
+ }
1718
+ }, 100);
1719
+ });
1720
+ }
1721
+ async function startInteractiveList(sessionRows, rawSessions, total) {
1722
+ enterFullScreen();
1723
+ const backend = createNodeBackend();
1724
+ const terminal = createTerminal2(backend);
1725
+ let selectedIndex = 0;
1726
+ let running = true;
1727
+ let showingDetail = false;
1728
+ let detailData = null;
1729
+ const rerender = () => {
1730
+ if (showingDetail && detailData) {
1731
+ renderSessionDetail(
1732
+ terminal,
1733
+ detailData.session,
1734
+ detailData.metrics,
1735
+ detailData.timeline,
1736
+ detailData.analysis,
1737
+ detailData.status
1738
+ );
1739
+ } else {
1740
+ renderInteractiveList(terminal, sessionRows, total, selectedIndex);
1741
+ }
1742
+ };
1743
+ rerender();
1744
+ const cleanupInput = setupKeyboardInput(async (key) => {
1745
+ if (!running) return;
1746
+ if (key.ctrl && key.name === "c") {
1747
+ running = false;
1748
+ cleanupInput();
1749
+ exitFullScreen();
1750
+ process.exit(0);
1751
+ return;
1752
+ }
1753
+ if (showingDetail) {
1754
+ if (key.name === "escape" || key.name === "q") {
1755
+ showingDetail = false;
1756
+ detailData = null;
1757
+ rerender();
1758
+ }
1759
+ return;
1760
+ }
1761
+ if (sessionRows.length > 0) {
1762
+ if (key.name === "j" || key.name === "down") {
1763
+ selectedIndex = (selectedIndex + 1) % sessionRows.length;
1764
+ rerender();
1765
+ return;
1766
+ }
1767
+ if (key.name === "k" || key.name === "up") {
1768
+ selectedIndex = (selectedIndex - 1 + sessionRows.length) % sessionRows.length;
1769
+ rerender();
1770
+ return;
1771
+ }
1772
+ }
1773
+ if (key.name === "return" && sessionRows.length > 0) {
1774
+ const rawSession = rawSessions[selectedIndex];
1775
+ if (rawSession) {
1776
+ const sessionId = rawSession.id || rawSession.pin;
1777
+ if (sessionId) {
1778
+ showingDetail = true;
1779
+ try {
1780
+ detailData = await fetchSessionDetail(sessionId);
1781
+ } catch {
1782
+ detailData = {
1783
+ session: {
1784
+ name: rawSession.name,
1785
+ pin: rawSession.pin,
1786
+ date: rawSession.date ? String(rawSession.date) : void 0,
1787
+ durationMinutes: rawSession.durationMinutes,
1788
+ createdAt: rawSession.createdAt ? String(rawSession.createdAt) : void 0
1789
+ },
1790
+ metrics: null,
1791
+ timeline: [],
1792
+ analysis: null,
1793
+ status: getSessionStatus(rawSession)
1794
+ };
1795
+ }
1796
+ rerender();
1797
+ }
1798
+ }
1799
+ return;
1800
+ }
1801
+ if (key.name === "q" || key.name === "escape") {
1802
+ running = false;
1803
+ cleanupInput();
1804
+ exitFullScreen();
1805
+ return;
1806
+ }
1807
+ });
1808
+ const onResize = () => {
1809
+ terminalResize(terminal, backend.size());
1810
+ rerender();
1811
+ };
1812
+ process.stdout.on("resize", onResize);
1813
+ return new Promise((resolve) => {
1814
+ const check = setInterval(() => {
1815
+ if (!running) {
1816
+ clearInterval(check);
1817
+ process.stdout.removeListener("resize", onResize);
1818
+ resolve();
1819
+ }
1820
+ }, 100);
1821
+ });
1822
+ }
1823
+ var init_app = __esm({
1824
+ "src/lib/app.tsx"() {
1825
+ "use strict";
1826
+ init_node_backend();
1827
+ init_input();
1828
+ init_api_client();
1829
+ init_Dashboard();
1830
+ init_SessionDetail();
1831
+ init_ShortcutBar();
1832
+ init_theme();
1833
+ }
1834
+ });
1835
+
1836
+ // src/index.ts
1837
+ init_auth();
1838
+ init_create();
1839
+ import { Command as Command6 } from "commander";
1840
+
1841
+ // src/commands/list.ts
1842
+ init_api_client();
1843
+ init_theme();
1844
+ init_render();
1845
+ import { Command as Command3 } from "commander";
1846
+ import * as p3 from "@clack/prompts";
1847
+
1848
+ // src/components/SessionTable.tsx
1849
+ init_theme();
1850
+ import { Table, Box } from "terminui/jsx";
1851
+ import { fillConstraint as fillConstraint2, lengthConstraint as lengthConstraint2 } from "terminui";
1852
+ import { jsx as jsx2 } from "terminui/jsx-runtime";
1853
+ function SessionTable({ sessions, total }) {
1854
+ const widths = [
1855
+ fillConstraint2(1),
1856
+ // Name -- takes remaining space
1857
+ lengthConstraint2(7),
1858
+ // PIN
1859
+ lengthConstraint2(10),
1860
+ // Status
1861
+ lengthConstraint2(12),
1862
+ // Date
1863
+ lengthConstraint2(10),
1864
+ // Duration
1865
+ lengthConstraint2(6)
1866
+ // Ppl
1867
+ ];
1868
+ const header = ["Name", "PIN", "Status", "Date", "Duration", "Ppl"];
1869
+ const rows = sessions.map((s) => [
1870
+ s.name,
1871
+ s.pin,
1872
+ s.status,
1873
+ s.date,
1874
+ s.duration,
1875
+ s.participants
1876
+ ]);
1877
+ return /* @__PURE__ */ jsx2(
1878
+ Box,
1879
+ {
1880
+ border: true,
1881
+ borderType: "rounded",
1882
+ fg: BRAND_COLORS.dimCyan,
1883
+ title: ` Sessions (${sessions.length} of ${total}) `,
1884
+ children: /* @__PURE__ */ jsx2(
1885
+ Table,
1886
+ {
1887
+ widths,
1888
+ header,
1889
+ rows,
1890
+ fg: BRAND_COLORS.cyan,
1891
+ columnSpacing: 1
1892
+ }
1893
+ )
1894
+ }
1895
+ );
1896
+ }
1897
+
1898
+ // src/commands/list.ts
1899
+ function getSessionStatus2(session) {
1900
+ const sessionDate = session.date ? new Date(session.date) : session.createdAt ? new Date(session.createdAt) : null;
1901
+ if (!sessionDate) return "unknown";
1902
+ const now = /* @__PURE__ */ new Date();
1903
+ const durationMs = (session.durationMinutes || 60) * 60 * 1e3;
1904
+ const endTime = new Date(sessionDate.getTime() + durationMs);
1905
+ if (now < sessionDate) return "upcoming";
1906
+ if (now >= sessionDate && now <= endTime) return "live";
1907
+ return "ended";
1908
+ }
1909
+ var listCommand = new Command3("list").alias("ls").description("List your feedback sessions").option("--project <name>", "Filter by project name").option("--limit <n>", "Max sessions to display", parseInt, 20).option("--json", "Output as JSON").action(async (options) => {
1910
+ const s = p3.spinner();
1911
+ s.start("Fetching sessions...");
1912
+ try {
1913
+ const client = createApiClient();
1914
+ const userId = getUserIdFromToken();
1915
+ const queryParams = new URLSearchParams();
1916
+ queryParams.set("take", String(options.limit));
1917
+ queryParams.set("sort", "createdAt");
1918
+ queryParams.set("order", "desc");
1919
+ const response = await client.get(
1920
+ `/users/${userId}/sessions?${queryParams.toString()}`
1921
+ );
1922
+ s.stop();
1923
+ const sessions = Array.isArray(response) ? response : response?.data || [];
1924
+ const total = Array.isArray(response) ? sessions.length : response?.total || sessions.length;
1925
+ if (sessions.length === 0) {
1926
+ p3.log.warn("No sessions found.");
1927
+ p3.log.info(
1928
+ 'Create one: audiencemeter create --template talk --project "My Talk"'
1929
+ );
1930
+ return;
1931
+ }
1932
+ if (options.json) {
1933
+ console.log(JSON.stringify(sessions, null, 2));
1934
+ return;
1935
+ }
1936
+ const statusDisplay2 = (status) => {
1937
+ switch (status) {
1938
+ case "live":
1939
+ return `${icon.live} live`;
1940
+ case "ended":
1941
+ return `${icon.ended} ended`;
1942
+ case "upcoming":
1943
+ return `${icon.upcoming} soon`;
1944
+ default:
1945
+ return status;
1946
+ }
1947
+ };
1948
+ const rows = sessions.map((session) => {
1949
+ const date = session.date ? new Date(session.date).toLocaleDateString() : session.createdAt ? new Date(session.createdAt).toLocaleDateString() : "--";
1950
+ const duration = session.durationMinutes ? `${session.durationMinutes}min` : "--";
1951
+ const status = getSessionStatus2(session);
1952
+ const raw = session;
1953
+ const count = raw._count?.attendees ?? raw.participantCount ?? "--";
1954
+ const participants = String(count);
1955
+ return {
1956
+ name: session.name || "--",
1957
+ pin: session.pin || "--",
1958
+ status: statusDisplay2(status),
1959
+ date,
1960
+ duration,
1961
+ participants
1962
+ };
1963
+ });
1964
+ if (process.stdout.isTTY && !options.json) {
1965
+ const { startInteractiveList: startInteractiveList2 } = await Promise.resolve().then(() => (init_app(), app_exports));
1966
+ await startInteractiveList2(rows, sessions, total);
1967
+ return;
1968
+ }
1969
+ const tableHeight = rows.length + 4;
1970
+ const output = renderJsxToString(
1971
+ getTermWidth(),
1972
+ tableHeight,
1973
+ SessionTable({ sessions: rows, total })
1974
+ );
1975
+ console.log(output);
1976
+ } catch (error) {
1977
+ s.stop(
1978
+ `${icon.cross} Failed to list sessions: ${error instanceof Error ? error.message : "Unknown error"}`
1979
+ );
1980
+ if (error instanceof Error && error.message.includes("Not authenticated")) {
1981
+ p3.log.warn("Run: audiencemeter login");
1982
+ }
1983
+ process.exit(1);
1984
+ }
1985
+ });
1986
+
1987
+ // src/commands/show.ts
1988
+ init_api_client();
1989
+ init_theme();
1990
+ init_render();
1991
+ init_SessionDetail();
1992
+ import { Command as Command4 } from "commander";
1993
+ import * as p4 from "@clack/prompts";
1994
+ function getSessionStatus3(session) {
1995
+ const sessionDate = session.date ? new Date(session.date) : session.createdAt ? new Date(session.createdAt) : null;
1996
+ if (!sessionDate) return "unknown";
1997
+ const now = /* @__PURE__ */ new Date();
1998
+ const durationMs = (session.durationMinutes || 60) * 60 * 1e3;
1999
+ const endTime = new Date(sessionDate.getTime() + durationMs);
2000
+ if (now < sessionDate) return "upcoming";
2001
+ if (now >= sessionDate && now <= endTime) return "live";
2002
+ return "ended";
2003
+ }
2004
+ var showCommand = new Command4("show").description("Show session details with rich metrics").argument("<session-id>", "Session ID (UUID) or PIN (5 characters)").option("--json", "Output as JSON").action(async (sessionId, options) => {
2005
+ const s = p4.spinner();
2006
+ s.start("Fetching session...");
2007
+ try {
2008
+ const client = createApiClient();
2009
+ const isUuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(sessionId);
2010
+ const path = !isUuid ? `/sessions/by-pin/${sessionId}` : `/sessions/${sessionId}`;
2011
+ const session = await client.get(path);
2012
+ let metrics = null;
2013
+ let timeline = [];
2014
+ let analysis = null;
2015
+ let isOwner = false;
2016
+ try {
2017
+ isOwner = !!session.userId && session.userId === getUserIdFromToken();
2018
+ } catch {
2019
+ }
2020
+ if (session.id && isOwner) {
2021
+ s.message("Fetching metrics...");
2022
+ const results = await Promise.allSettled([
2023
+ client.get(`/sessions/${session.id}/metrics`),
2024
+ client.get(
2025
+ `/sessions/${session.id}/sentiment-timeline`
2026
+ ),
2027
+ client.get(
2028
+ `/sessions/${session.id}/analysis`
2029
+ )
2030
+ ]);
2031
+ if (results[0].status === "fulfilled") metrics = results[0].value;
2032
+ if (results[1].status === "fulfilled")
2033
+ timeline = results[1].value || [];
2034
+ if (results[2].status === "fulfilled") analysis = results[2].value;
2035
+ }
2036
+ s.stop();
2037
+ if (options.json) {
2038
+ const output2 = {
2039
+ ...session,
2040
+ ...metrics && { metrics },
2041
+ ...timeline.length && { timeline },
2042
+ ...analysis && { analysis }
2043
+ };
2044
+ console.log(JSON.stringify(output2, null, 2));
2045
+ return;
2046
+ }
2047
+ const status = getSessionStatus3(session);
2048
+ let height = 3;
2049
+ const infoPairCount = 3 + (session.id ? 1 : 0);
2050
+ const hasMetrics = metrics && (metrics.engagementRate !== void 0 && metrics.engagementRate > 0 || metrics.feedbackCompletionRate !== void 0 && metrics.feedbackCompletionRate > 0 || metrics.totalParticipants !== void 0);
2051
+ if (hasMetrics) {
2052
+ const metricsH = 1 + (metrics.totalParticipants !== void 0 ? 1 : 0) + (metrics.engagementRate !== void 0 && metrics.engagementRate > 0 ? 3 : 0) + (metrics.feedbackCompletionRate !== void 0 && metrics.feedbackCompletionRate > 0 ? 3 : 0);
2053
+ height += Math.max(infoPairCount + 2, metricsH + 2);
2054
+ } else {
2055
+ height += infoPairCount + 2;
2056
+ }
2057
+ const positiveData = timeline.map((b) => b.positive || 0);
2058
+ if (positiveData.some((v) => v > 0)) {
2059
+ height += 5;
2060
+ }
2061
+ const totalPositive = timeline.reduce((sum, b) => sum + (b.positive || 0), 0);
2062
+ const totalNegative = timeline.reduce((sum, b) => sum + (b.negative || 0), 0);
2063
+ if (totalPositive > 0 || totalNegative > 0) {
2064
+ const maxVal = Math.max(totalPositive, totalNegative, 1);
2065
+ height += Math.max(8, maxVal + 4);
2066
+ }
2067
+ if (analysis && typeof analysis === "object") {
2068
+ const summary = analysis.summary || analysis.feedbackSummary || "";
2069
+ if (summary) {
2070
+ height += Math.ceil(summary.length / 70) + 2;
2071
+ }
2072
+ const insights = analysis.coachingInsights;
2073
+ if (insights && insights.length > 0) {
2074
+ height += Math.min(insights.length, 3) + 2;
2075
+ }
2076
+ }
2077
+ if (session.questions && session.questions.length > 0) {
2078
+ height += session.questions.length + 2;
2079
+ }
2080
+ if (session.links && session.links.length > 0) {
2081
+ height += session.links.length + 2;
2082
+ }
2083
+ if (session.pin) {
2084
+ height += 3;
2085
+ }
2086
+ const sessionData = {
2087
+ id: session.id,
2088
+ name: session.name,
2089
+ pin: session.pin,
2090
+ date: session.date,
2091
+ durationMinutes: session.durationMinutes,
2092
+ createdAt: session.createdAt,
2093
+ questions: session.questions,
2094
+ links: session.links
2095
+ };
2096
+ const metricsData = metrics ? {
2097
+ totalParticipants: metrics.totalParticipants,
2098
+ engagementRate: metrics.engagementRate,
2099
+ feedbackCompletionRate: metrics.feedbackCompletionRate
2100
+ } : null;
2101
+ console.log();
2102
+ const output = renderJsxToString(
2103
+ getTermWidth(),
2104
+ height,
2105
+ SessionDetail({
2106
+ session: sessionData,
2107
+ metrics: metricsData,
2108
+ timeline,
2109
+ analysis,
2110
+ status
2111
+ })
2112
+ );
2113
+ console.log(output);
2114
+ } catch (error) {
2115
+ s.stop(
2116
+ `${icon.cross} Failed to fetch session: ${error instanceof Error ? error.message : "Unknown error"}`
2117
+ );
2118
+ if (error instanceof Error && error.message.includes("Not authenticated")) {
2119
+ p4.log.warn("Run: audiencemeter login");
2120
+ }
2121
+ process.exit(1);
2122
+ }
2123
+ });
2124
+
2125
+ // src/commands/delete.ts
2126
+ init_api_client();
2127
+ init_theme();
2128
+ import { Command as Command5 } from "commander";
2129
+ import * as p5 from "@clack/prompts";
2130
+ var deleteCommand = new Command5("delete").alias("rm").description("Delete a session").argument("<session-id>", "Session ID (UUID)").option("--force", "Skip confirmation prompt").action(async (sessionId, options) => {
2131
+ try {
2132
+ const client = createApiClient();
2133
+ let sessionName = sessionId;
2134
+ try {
2135
+ const session = await client.get(
2136
+ `/sessions/${sessionId}`
2137
+ );
2138
+ if (session.name) {
2139
+ sessionName = session.name;
2140
+ }
2141
+ } catch {
2142
+ }
2143
+ if (!options.force) {
2144
+ const confirmed = await p5.confirm({
2145
+ message: `Delete session "${sessionName}"? This cannot be undone.`
2146
+ });
2147
+ if (p5.isCancel(confirmed) || !confirmed) {
2148
+ p5.log.info("Cancelled.");
2149
+ return;
2150
+ }
2151
+ }
2152
+ const s = p5.spinner();
2153
+ s.start("Deleting session...");
2154
+ await client.delete(`/sessions/${sessionId}`);
2155
+ s.stop(`${icon.check} Session "${sessionName}" deleted.`);
2156
+ } catch (error) {
2157
+ p5.log.error(
2158
+ `Failed to delete session: ${error instanceof Error ? error.message : "Unknown error"}`
2159
+ );
2160
+ if (error instanceof Error && error.message.includes("Not authenticated")) {
2161
+ p5.log.warn("Run: audiencemeter login");
2162
+ }
2163
+ process.exit(1);
2164
+ }
2165
+ });
2166
+
2167
+ // src/index.ts
2168
+ init_theme();
2169
+ var program = new Command6();
2170
+ program.name("audiencemeter").description(
2171
+ `${BRAND.name} CLI \u2014 ${BRAND.tagline}`
2172
+ ).version(BRAND.version, "-v, --version");
2173
+ program.addCommand(loginCommand);
2174
+ program.addCommand(logoutCommand);
2175
+ program.addCommand(authCommand);
2176
+ program.addCommand(createCommand);
2177
+ program.addCommand(listCommand);
2178
+ program.addCommand(showCommand);
2179
+ program.addCommand(deleteCommand);
2180
+ var hasArgs = process.argv.length > 2;
2181
+ if (hasArgs) {
2182
+ program.parse(process.argv);
2183
+ } else if (process.stdout.isTTY) {
2184
+ Promise.resolve().then(() => (init_app(), app_exports)).then(({ startDashboard: startDashboard2 }) => startDashboard2()).catch((err) => {
2185
+ Promise.resolve().then(() => (init_input(), input_exports)).then(({ exitFullScreen: exitFullScreen2 }) => {
2186
+ exitFullScreen2();
2187
+ }).catch(() => {
2188
+ });
2189
+ console.error(err instanceof Error ? err.message : String(err));
2190
+ process.exit(1);
2191
+ });
2192
+ } else {
2193
+ program.help();
2194
+ }
2195
+ //# sourceMappingURL=index.js.map