pixelmuse 0.2.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/tui.js ADDED
@@ -0,0 +1,1274 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ PixelmuseClient,
4
+ deleteApiKey,
5
+ deleteTemplate,
6
+ extractVariables,
7
+ getApiKey,
8
+ getTemplate,
9
+ isValidKeyFormat,
10
+ listTemplates,
11
+ pollGeneration,
12
+ saveApiKey,
13
+ saveTemplate
14
+ } from "./chunk-ZVJQFWUI.js";
15
+ import {
16
+ autoSave,
17
+ hasChafa,
18
+ imageToBuffer,
19
+ renderImageDirect
20
+ } from "./chunk-MZZY4JXW.js";
21
+ import {
22
+ readSettings,
23
+ writeSettings
24
+ } from "./chunk-7ARYEFAH.js";
25
+
26
+ // src/tui.tsx
27
+ import { render } from "ink";
28
+
29
+ // src/app.tsx
30
+ import { useEffect as useEffect9 } from "react";
31
+ import { Box as Box17, useApp as useApp2, useInput as useInput9 } from "ink";
32
+
33
+ // src/hooks/useRouter.ts
34
+ import { useReducer, useCallback } from "react";
35
+ function reducer(state, action) {
36
+ switch (action.type) {
37
+ case "navigate":
38
+ return { current: action.route, history: [...state.history, state.current] };
39
+ case "back": {
40
+ if (state.history.length === 0) return state;
41
+ const prev = state.history.at(-1);
42
+ if (!prev) return state;
43
+ return { current: prev, history: state.history.slice(0, -1) };
44
+ }
45
+ case "replace":
46
+ return { ...state, current: action.route };
47
+ }
48
+ }
49
+ function useRouter(initial = { screen: "home" }) {
50
+ const [state, dispatch] = useReducer(reducer, { current: initial, history: [] });
51
+ const navigate = useCallback((route) => dispatch({ type: "navigate", route }), []);
52
+ const back = useCallback(() => dispatch({ type: "back" }), []);
53
+ const replace = useCallback((route) => dispatch({ type: "replace", route }), []);
54
+ return { route: state.current, navigate, back, replace, canGoBack: state.history.length > 0 };
55
+ }
56
+
57
+ // src/hooks/useAuth.ts
58
+ import { useState, useEffect, useCallback as useCallback2 } from "react";
59
+ function useAuth() {
60
+ const [state, setState] = useState({
61
+ apiKey: null,
62
+ account: null,
63
+ isAuthenticated: false,
64
+ isLoading: true,
65
+ error: null
66
+ });
67
+ useEffect(() => {
68
+ let cancelled = false;
69
+ (async () => {
70
+ const key = await getApiKey();
71
+ if (cancelled) return;
72
+ if (key) {
73
+ setState((s) => ({ ...s, apiKey: key, isAuthenticated: true, isLoading: false }));
74
+ try {
75
+ const client = new PixelmuseClient(key);
76
+ const account = await client.getAccount();
77
+ if (!cancelled) setState((s) => ({ ...s, account }));
78
+ } catch {
79
+ }
80
+ } else {
81
+ setState((s) => ({ ...s, isLoading: false }));
82
+ }
83
+ })();
84
+ return () => {
85
+ cancelled = true;
86
+ };
87
+ }, []);
88
+ const login = useCallback2(async (key) => {
89
+ if (!isValidKeyFormat(key)) {
90
+ return { success: false, error: "Invalid key format. Keys start with pm_live_ or pm_test_" };
91
+ }
92
+ setState((s) => ({ ...s, isLoading: true, error: null }));
93
+ try {
94
+ const client = new PixelmuseClient(key);
95
+ const account = await client.getAccount();
96
+ await saveApiKey(key);
97
+ setState({ apiKey: key, account, isAuthenticated: true, isLoading: false, error: null });
98
+ return { success: true };
99
+ } catch (err) {
100
+ const msg = err instanceof Error ? err.message : "Login failed";
101
+ setState((s) => ({ ...s, isLoading: false, error: msg }));
102
+ return { success: false, error: msg };
103
+ }
104
+ }, []);
105
+ const logout = useCallback2(async () => {
106
+ await deleteApiKey();
107
+ setState({ apiKey: null, account: null, isAuthenticated: false, isLoading: false, error: null });
108
+ }, []);
109
+ const refreshAccount = useCallback2(async () => {
110
+ if (!state.apiKey) return;
111
+ try {
112
+ const client = new PixelmuseClient(state.apiKey);
113
+ const account = await client.getAccount();
114
+ setState((s) => ({ ...s, account }));
115
+ } catch {
116
+ }
117
+ }, [state.apiKey]);
118
+ return { ...state, login, logout, refreshAccount };
119
+ }
120
+
121
+ // src/hooks/useApi.ts
122
+ import { useMemo } from "react";
123
+ function useApi(apiKey) {
124
+ return useMemo(() => apiKey ? new PixelmuseClient(apiKey) : null, [apiKey]);
125
+ }
126
+
127
+ // src/hooks/useConfig.ts
128
+ import { useState as useState2, useCallback as useCallback3 } from "react";
129
+ function useConfig() {
130
+ const [settings, setSettings] = useState2(() => readSettings());
131
+ const updateSettings = useCallback3((updates) => {
132
+ const next = { ...settings, ...updates };
133
+ writeSettings(next);
134
+ setSettings(next);
135
+ }, [settings]);
136
+ return { settings, updateSettings };
137
+ }
138
+
139
+ // src/app.tsx
140
+ import { Spinner as Spinner9 } from "@inkjs/ui";
141
+
142
+ // src/components/Header.tsx
143
+ import { Box, Text } from "ink";
144
+ import { jsx, jsxs } from "react/jsx-runtime";
145
+ var SCREEN_LABELS = {
146
+ home: "Home",
147
+ login: "Login",
148
+ generate: "Generate",
149
+ gallery: "Gallery",
150
+ "gallery-detail": "Image Detail",
151
+ models: "Models",
152
+ prompts: "Prompts",
153
+ "prompt-editor": "Prompt Editor",
154
+ account: "Account",
155
+ "buy-credits": "Buy Credits"
156
+ };
157
+ function Header({ route }) {
158
+ const label = SCREEN_LABELS[route.screen] ?? route.screen;
159
+ return /* @__PURE__ */ jsxs(Box, { borderStyle: "single", borderColor: "gray", paddingX: 1, marginBottom: 1, children: [
160
+ /* @__PURE__ */ jsx(Text, { bold: true, color: "magenta", children: "pixelmuse" }),
161
+ /* @__PURE__ */ jsx(Text, { color: "gray", children: " / " }),
162
+ /* @__PURE__ */ jsx(Text, { color: "white", children: label })
163
+ ] });
164
+ }
165
+
166
+ // src/components/StatusBar.tsx
167
+ import { Box as Box2, Text as Text2 } from "ink";
168
+ import { Fragment, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
169
+ function StatusBar({ account, hints }) {
170
+ return /* @__PURE__ */ jsxs2(Box2, { marginTop: 1, paddingX: 1, borderStyle: "single", borderColor: "gray", children: [
171
+ account && /* @__PURE__ */ jsxs2(Fragment, { children: [
172
+ /* @__PURE__ */ jsxs2(Text2, { color: "green", children: [
173
+ "credits: ",
174
+ account.credits.total
175
+ ] }),
176
+ /* @__PURE__ */ jsx2(Text2, { color: "gray", children: " | " }),
177
+ /* @__PURE__ */ jsxs2(Text2, { color: "cyan", children: [
178
+ "plan: ",
179
+ account.plan
180
+ ] }),
181
+ /* @__PURE__ */ jsx2(Text2, { color: "gray", children: " | " })
182
+ ] }),
183
+ /* @__PURE__ */ jsx2(Text2, { color: "gray", children: hints ?? "q quit | esc back" })
184
+ ] });
185
+ }
186
+
187
+ // src/screens/Home.tsx
188
+ import { Box as Box3, Text as Text3 } from "ink";
189
+ import { Select } from "@inkjs/ui";
190
+ import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
191
+ function Home({ account, navigate, onLogout }) {
192
+ const items = [
193
+ { label: "Generate Image", value: "generate" },
194
+ { label: "Gallery", value: "gallery" },
195
+ { label: "Prompt Templates", value: "prompts" },
196
+ { label: "Models", value: "models" },
197
+ { label: "Account & Credits", value: "account" },
198
+ { label: "Logout", value: "logout" }
199
+ ];
200
+ return /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", gap: 1, children: [
201
+ account && /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", children: [
202
+ /* @__PURE__ */ jsxs3(Text3, { children: [
203
+ "Welcome, ",
204
+ /* @__PURE__ */ jsx3(Text3, { bold: true, children: account.email })
205
+ ] }),
206
+ /* @__PURE__ */ jsxs3(Text3, { color: "green", children: [
207
+ account.credits.total,
208
+ " credits available"
209
+ ] })
210
+ ] }),
211
+ /* @__PURE__ */ jsx3(
212
+ Select,
213
+ {
214
+ options: items,
215
+ onChange: (value) => {
216
+ switch (value) {
217
+ case "generate":
218
+ navigate({ screen: "generate" });
219
+ break;
220
+ case "gallery":
221
+ navigate({ screen: "gallery" });
222
+ break;
223
+ case "prompts":
224
+ navigate({ screen: "prompts" });
225
+ break;
226
+ case "models":
227
+ navigate({ screen: "models" });
228
+ break;
229
+ case "account":
230
+ navigate({ screen: "account" });
231
+ break;
232
+ case "logout":
233
+ onLogout();
234
+ break;
235
+ }
236
+ }
237
+ }
238
+ )
239
+ ] });
240
+ }
241
+
242
+ // src/screens/Login.tsx
243
+ import { useState as useState3 } from "react";
244
+ import { Box as Box4, Text as Text4 } from "ink";
245
+ import { TextInput, Spinner, StatusMessage } from "@inkjs/ui";
246
+ import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
247
+ function Login({ onLogin, onSuccess }) {
248
+ const [loading, setLoading] = useState3(false);
249
+ const [error, setError] = useState3(null);
250
+ const handleSubmit = async (value) => {
251
+ const key = value.trim();
252
+ if (!key) return;
253
+ setLoading(true);
254
+ setError(null);
255
+ const result = await onLogin(key);
256
+ setLoading(false);
257
+ if (result.success) {
258
+ onSuccess();
259
+ } else {
260
+ setError(result.error ?? "Login failed");
261
+ }
262
+ };
263
+ return /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", gap: 1, children: [
264
+ /* @__PURE__ */ jsx4(Text4, { bold: true, children: "Enter your Pixelmuse API key" }),
265
+ /* @__PURE__ */ jsxs4(Text4, { color: "gray", children: [
266
+ "Get your key at",
267
+ " ",
268
+ /* @__PURE__ */ jsx4(Text4, { color: "cyan", underline: true, children: "pixelmuse.studio/settings/api-keys" })
269
+ ] }),
270
+ error && /* @__PURE__ */ jsx4(StatusMessage, { variant: "error", children: error }),
271
+ loading ? /* @__PURE__ */ jsx4(Spinner, { label: "Validating key..." }) : /* @__PURE__ */ jsxs4(Box4, { children: [
272
+ /* @__PURE__ */ jsx4(Text4, { color: "gray", children: "Key: " }),
273
+ /* @__PURE__ */ jsx4(TextInput, { placeholder: "pm_live_...", onSubmit: handleSubmit })
274
+ ] })
275
+ ] });
276
+ }
277
+
278
+ // src/screens/Generate.tsx
279
+ import { useState as useState4, useEffect as useEffect2, useCallback as useCallback4 } from "react";
280
+ import { Box as Box6, Text as Text6, useApp, useInput } from "ink";
281
+ import { Select as Select2, TextInput as TextInput2 } from "@inkjs/ui";
282
+
283
+ // src/components/GenerationProgress.tsx
284
+ import { Box as Box5, Text as Text5 } from "ink";
285
+ import { Spinner as Spinner2, ProgressBar } from "@inkjs/ui";
286
+ import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
287
+ function GenerationProgress({ provider, progress = 0, elapsed = 0, model }) {
288
+ const elapsedStr = `${Math.round(elapsed)}s`;
289
+ if (provider === "gemini") {
290
+ return /* @__PURE__ */ jsx5(Box5, { flexDirection: "column", gap: 1, children: /* @__PURE__ */ jsx5(Spinner2, { label: `Generating with ${model ?? "Gemini"}... (${elapsedStr})` }) });
291
+ }
292
+ return /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", gap: 1, children: [
293
+ /* @__PURE__ */ jsxs5(Text5, { children: [
294
+ "Generating with ",
295
+ /* @__PURE__ */ jsx5(Text5, { bold: true, children: model ?? "Replicate" }),
296
+ "... (",
297
+ elapsedStr,
298
+ ")"
299
+ ] }),
300
+ /* @__PURE__ */ jsx5(ProgressBar, { value: Math.round(progress * 100) })
301
+ ] });
302
+ }
303
+
304
+ // src/screens/Generate.tsx
305
+ import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
306
+ var GEMINI_MODELS = /* @__PURE__ */ new Set(["nano-banana-2", "nano-banana-pro", "imagen-3"]);
307
+ var MODEL_OPTIONS = [
308
+ { label: "Nano Banana 2 (1 credit, fast)", value: "nano-banana-2" },
309
+ { label: "Nano Banana Pro (4 credits)", value: "nano-banana-pro" },
310
+ { label: "Flux Schnell (1 credit)", value: "flux-schnell" },
311
+ { label: "Google Imagen 3 (1 credit)", value: "imagen-3" },
312
+ { label: "Recraft V4 (1 credit)", value: "recraft-v4" },
313
+ { label: "Recraft V4 Pro (7 credits)", value: "recraft-v4-pro" }
314
+ ];
315
+ var ASPECT_OPTIONS = [
316
+ { label: "1:1 Square", value: "1:1" },
317
+ { label: "16:9 Landscape", value: "16:9" },
318
+ { label: "9:16 Portrait", value: "9:16" },
319
+ { label: "3:2 Landscape", value: "3:2" },
320
+ { label: "2:3 Portrait", value: "2:3" },
321
+ { label: "4:3 Landscape", value: "4:3" },
322
+ { label: "21:9 Ultrawide", value: "21:9" }
323
+ ];
324
+ function Generate({
325
+ client,
326
+ navigate,
327
+ initialPrompt,
328
+ initialModel,
329
+ initialAspectRatio,
330
+ initialStyle,
331
+ defaultModel = "nano-banana-2",
332
+ defaultAspectRatio = "1:1",
333
+ defaultStyle = "none"
334
+ }) {
335
+ const { exit } = useApp();
336
+ const [step, setStep] = useState4(initialPrompt ? "model" : "prompt");
337
+ const [prompt, setPrompt] = useState4(initialPrompt ?? "");
338
+ const [model, setModel] = useState4(initialModel ?? defaultModel);
339
+ const [aspectRatio, setAspectRatio] = useState4(
340
+ initialAspectRatio ?? defaultAspectRatio
341
+ );
342
+ const [style] = useState4(initialStyle ?? defaultStyle);
343
+ const [generation, setGeneration] = useState4(null);
344
+ const [error, setError] = useState4(null);
345
+ const [progress, setProgress] = useState4(0);
346
+ const [elapsed, setElapsed] = useState4(0);
347
+ useEffect2(() => {
348
+ if (initialPrompt && initialModel) {
349
+ if (initialAspectRatio && initialStyle) {
350
+ setStep("generating");
351
+ } else if (initialAspectRatio) {
352
+ setStep("generating");
353
+ } else {
354
+ setStep("options");
355
+ }
356
+ }
357
+ }, []);
358
+ const generate = useCallback4(async () => {
359
+ setStep("generating");
360
+ setError(null);
361
+ const start = Date.now();
362
+ const timer = setInterval(() => {
363
+ setElapsed((Date.now() - start) / 1e3);
364
+ }, 100);
365
+ try {
366
+ let gen = await client.generate({
367
+ prompt,
368
+ model,
369
+ aspect_ratio: aspectRatio,
370
+ style: style === "none" ? void 0 : style
371
+ });
372
+ if (gen.status === "processing" || gen.status === "pending") {
373
+ gen = await pollGeneration(client, gen.id, {
374
+ onProgress: (e, p) => {
375
+ setElapsed(e);
376
+ setProgress(p);
377
+ }
378
+ });
379
+ }
380
+ clearInterval(timer);
381
+ setGeneration(gen);
382
+ let imagePath = null;
383
+ if (gen.output?.[0]) {
384
+ const buf = await imageToBuffer(gen.output[0]);
385
+ imagePath = autoSave(gen.id, buf);
386
+ }
387
+ const payload = { type: "preview", generation: gen, imagePath };
388
+ exit(void 0);
389
+ globalThis.__pixelmuse_preview = payload;
390
+ setStep("preview");
391
+ } catch (err) {
392
+ clearInterval(timer);
393
+ setError(err instanceof Error ? err.message : "Generation failed");
394
+ setStep("preview");
395
+ }
396
+ }, [client, prompt, model, aspectRatio, style, exit]);
397
+ useEffect2(() => {
398
+ if (step === "generating" && !generation && !error) {
399
+ generate();
400
+ }
401
+ }, [step]);
402
+ useInput(
403
+ (input) => {
404
+ if (step !== "preview") return;
405
+ if (input === "r") {
406
+ setGeneration(null);
407
+ setError(null);
408
+ setProgress(0);
409
+ setElapsed(0);
410
+ setStep("generating");
411
+ } else if (input === "h") {
412
+ navigate({ screen: "home" });
413
+ }
414
+ }
415
+ );
416
+ if (step === "prompt") {
417
+ return /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", gap: 1, children: [
418
+ /* @__PURE__ */ jsx6(Text6, { bold: true, children: "Enter your prompt:" }),
419
+ /* @__PURE__ */ jsx6(
420
+ TextInput2,
421
+ {
422
+ placeholder: "A cinematic landscape with dramatic lighting...",
423
+ onSubmit: (value) => {
424
+ if (value.trim()) {
425
+ setPrompt(value.trim());
426
+ setStep("model");
427
+ }
428
+ }
429
+ }
430
+ )
431
+ ] });
432
+ }
433
+ if (step === "model") {
434
+ return /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", gap: 1, children: [
435
+ /* @__PURE__ */ jsx6(Text6, { bold: true, children: "Select model:" }),
436
+ /* @__PURE__ */ jsx6(Text6, { color: "gray", wrap: "truncate", children: prompt }),
437
+ /* @__PURE__ */ jsx6(
438
+ Select2,
439
+ {
440
+ options: MODEL_OPTIONS,
441
+ onChange: (value) => {
442
+ setModel(value);
443
+ setStep("options");
444
+ }
445
+ }
446
+ )
447
+ ] });
448
+ }
449
+ if (step === "options") {
450
+ return /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", gap: 1, children: [
451
+ /* @__PURE__ */ jsx6(Text6, { bold: true, children: "Aspect ratio:" }),
452
+ /* @__PURE__ */ jsx6(
453
+ Select2,
454
+ {
455
+ options: ASPECT_OPTIONS,
456
+ onChange: (value) => {
457
+ setAspectRatio(value);
458
+ setStep("generating");
459
+ }
460
+ }
461
+ )
462
+ ] });
463
+ }
464
+ if (step === "generating") {
465
+ const provider = GEMINI_MODELS.has(model) ? "gemini" : "replicate";
466
+ return /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", gap: 1, children: [
467
+ /* @__PURE__ */ jsx6(Text6, { color: "gray", wrap: "truncate", children: prompt }),
468
+ /* @__PURE__ */ jsx6(GenerationProgress, { provider, progress, elapsed, model })
469
+ ] });
470
+ }
471
+ return /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", gap: 1, children: [
472
+ /* @__PURE__ */ jsxs6(Text6, { color: "red", children: [
473
+ "Error: ",
474
+ error
475
+ ] }),
476
+ /* @__PURE__ */ jsx6(Text6, { color: "gray", children: "[r] retry | [h] home" })
477
+ ] });
478
+ }
479
+
480
+ // src/screens/Gallery.tsx
481
+ import { useState as useState6, useEffect as useEffect3 } from "react";
482
+ import { Box as Box8, Text as Text8, useInput as useInput3 } from "ink";
483
+ import { Spinner as Spinner3 } from "@inkjs/ui";
484
+
485
+ // src/components/ImageGrid.tsx
486
+ import { useState as useState5 } from "react";
487
+ import { Box as Box7, Text as Text7, useInput as useInput2 } from "ink";
488
+ import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
489
+ function ImageGrid({ generations, columns = 3, onSelect }) {
490
+ const [selectedIndex, setSelectedIndex] = useState5(0);
491
+ useInput2((input, key) => {
492
+ if (key.leftArrow) {
493
+ setSelectedIndex((i) => Math.max(0, i - 1));
494
+ } else if (key.rightArrow) {
495
+ setSelectedIndex((i) => Math.min(generations.length - 1, i + 1));
496
+ } else if (key.upArrow) {
497
+ setSelectedIndex((i) => Math.max(0, i - columns));
498
+ } else if (key.downArrow) {
499
+ setSelectedIndex((i) => Math.min(generations.length - 1, i + columns));
500
+ } else if (key.return) {
501
+ const gen = generations[selectedIndex];
502
+ if (gen) onSelect(gen);
503
+ }
504
+ });
505
+ const rows = [];
506
+ for (let i = 0; i < generations.length; i += columns) {
507
+ rows.push(generations.slice(i, i + columns));
508
+ }
509
+ const cellWidth = Math.floor((process.stdout.columns ?? 80) / columns) - 2;
510
+ return /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", children: [
511
+ rows.map((row, rowIndex) => /* @__PURE__ */ jsx7(Box7, { children: row.map((gen, colIndex) => {
512
+ const index = rowIndex * columns + colIndex;
513
+ const isSelected = index === selectedIndex;
514
+ return /* @__PURE__ */ jsxs7(
515
+ Box7,
516
+ {
517
+ width: cellWidth,
518
+ borderStyle: isSelected ? "bold" : "single",
519
+ borderColor: isSelected ? "magenta" : "gray",
520
+ flexDirection: "column",
521
+ paddingX: 1,
522
+ children: [
523
+ /* @__PURE__ */ jsxs7(Text7, { color: gen.status === "succeeded" ? "green" : gen.status === "failed" ? "red" : "yellow", children: [
524
+ gen.status === "succeeded" ? "\u25CF" : gen.status === "failed" ? "\u2717" : "\u25CC",
525
+ " ",
526
+ gen.model
527
+ ] }),
528
+ /* @__PURE__ */ jsx7(Text7, { color: "gray", wrap: "truncate", children: gen.prompt.slice(0, cellWidth - 4) }),
529
+ /* @__PURE__ */ jsx7(Text7, { color: "gray", dimColor: true, children: new Date(gen.created_at).toLocaleDateString() })
530
+ ]
531
+ },
532
+ gen.id
533
+ );
534
+ }) }, rowIndex)),
535
+ /* @__PURE__ */ jsx7(Text7, { color: "gray", dimColor: true, children: "\u2190\u2192\u2191\u2193 navigate | enter select" })
536
+ ] });
537
+ }
538
+
539
+ // src/screens/Gallery.tsx
540
+ import { jsx as jsx8, jsxs as jsxs8 } from "react/jsx-runtime";
541
+ function Gallery({ client, navigate }) {
542
+ const [generations, setGenerations] = useState6([]);
543
+ const [loading, setLoading] = useState6(true);
544
+ const [error, setError] = useState6(null);
545
+ const [hasMore, setHasMore] = useState6(false);
546
+ const [cursor, setCursor] = useState6();
547
+ const load = async (nextCursor) => {
548
+ setLoading(true);
549
+ try {
550
+ const result = await client.listGenerations({ cursor: nextCursor, limit: 12 });
551
+ if (nextCursor) {
552
+ setGenerations((prev) => [...prev, ...result.data]);
553
+ } else {
554
+ setGenerations(result.data);
555
+ }
556
+ setHasMore(result.has_more);
557
+ setCursor(result.next_cursor ?? void 0);
558
+ } catch (err) {
559
+ setError(err instanceof Error ? err.message : "Failed to load gallery");
560
+ }
561
+ setLoading(false);
562
+ };
563
+ useEffect3(() => {
564
+ load();
565
+ }, []);
566
+ useInput3((input) => {
567
+ if (input === "l" && hasMore && !loading) {
568
+ load(cursor);
569
+ }
570
+ });
571
+ if (loading && generations.length === 0) {
572
+ return /* @__PURE__ */ jsx8(Spinner3, { label: "Loading gallery..." });
573
+ }
574
+ if (error) {
575
+ return /* @__PURE__ */ jsxs8(Text8, { color: "red", children: [
576
+ "Error: ",
577
+ error
578
+ ] });
579
+ }
580
+ if (generations.length === 0) {
581
+ return /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", gap: 1, children: [
582
+ /* @__PURE__ */ jsx8(Text8, { children: "No generations yet." }),
583
+ /* @__PURE__ */ jsx8(Text8, { color: "gray", children: "Generate your first image to see it here!" })
584
+ ] });
585
+ }
586
+ return /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", gap: 1, children: [
587
+ /* @__PURE__ */ jsx8(
588
+ ImageGrid,
589
+ {
590
+ generations,
591
+ onSelect: (gen) => navigate({ screen: "gallery-detail", id: gen.id })
592
+ }
593
+ ),
594
+ hasMore && /* @__PURE__ */ jsx8(Text8, { color: "gray", children: "[l] load more" }),
595
+ loading && /* @__PURE__ */ jsx8(Spinner3, { label: "Loading more..." })
596
+ ] });
597
+ }
598
+
599
+ // src/screens/GalleryDetail.tsx
600
+ import { useState as useState8, useEffect as useEffect5 } from "react";
601
+ import { Box as Box10, Text as Text10, useInput as useInput4 } from "ink";
602
+ import { ConfirmInput, Spinner as Spinner5 } from "@inkjs/ui";
603
+
604
+ // src/components/ImagePreview.tsx
605
+ import { useState as useState7, useEffect as useEffect4 } from "react";
606
+ import { Box as Box9, Text as Text9 } from "ink";
607
+ import { Spinner as Spinner4 } from "@inkjs/ui";
608
+ import { execSync } from "child_process";
609
+ import { jsx as jsx9 } from "react/jsx-runtime";
610
+ function ImagePreview({ source, width }) {
611
+ const [ansi, setAnsi] = useState7(null);
612
+ const [state, setState] = useState7("loading");
613
+ useEffect4(() => {
614
+ if (typeof source !== "string" || !hasChafa()) {
615
+ setState("error");
616
+ return;
617
+ }
618
+ try {
619
+ const cols = width ?? Math.min(process.stdout.columns ?? 80, 80);
620
+ const result = execSync(
621
+ `chafa --format symbols --size ${cols} --animate off "${source}"`,
622
+ { encoding: "utf-8", maxBuffer: 10 * 1024 * 1024 }
623
+ );
624
+ setAnsi(result);
625
+ setState("done");
626
+ } catch {
627
+ setState("error");
628
+ }
629
+ }, [source, width]);
630
+ if (state === "error") {
631
+ return /* @__PURE__ */ jsx9(Box9, { children: /* @__PURE__ */ jsx9(Text9, { color: "red", children: "[Preview unavailable \u2014 install chafa for best results]" }) });
632
+ }
633
+ if (state === "loading") {
634
+ return /* @__PURE__ */ jsx9(Box9, { children: /* @__PURE__ */ jsx9(Spinner4, { label: "Rendering preview..." }) });
635
+ }
636
+ return /* @__PURE__ */ jsx9(Text9, { children: ansi });
637
+ }
638
+
639
+ // src/screens/GalleryDetail.tsx
640
+ import { jsx as jsx10, jsxs as jsxs9 } from "react/jsx-runtime";
641
+ function GalleryDetail({ client, generationId, navigate, back }) {
642
+ const [generation, setGeneration] = useState8(null);
643
+ const [imagePath, setImagePath] = useState8(null);
644
+ const [loading, setLoading] = useState8(true);
645
+ const [confirming, setConfirming] = useState8(false);
646
+ const [error, setError] = useState8(null);
647
+ useEffect5(() => {
648
+ let cancelled = false;
649
+ (async () => {
650
+ try {
651
+ const gen = await client.getGeneration(generationId);
652
+ if (cancelled) return;
653
+ setGeneration(gen);
654
+ if (gen.output?.[0]) {
655
+ const buf = await imageToBuffer(gen.output[0]);
656
+ if (!cancelled) {
657
+ const path = autoSave(gen.id, buf);
658
+ setImagePath(path);
659
+ }
660
+ }
661
+ } catch (err) {
662
+ if (!cancelled) setError(err instanceof Error ? err.message : "Failed to load");
663
+ }
664
+ if (!cancelled) setLoading(false);
665
+ })();
666
+ return () => {
667
+ cancelled = true;
668
+ };
669
+ }, [generationId]);
670
+ useInput4((input) => {
671
+ if (confirming) return;
672
+ if (input === "d") setConfirming(true);
673
+ if (input === "r" && generation) {
674
+ navigate({ screen: "generate", prompt: generation.prompt, model: generation.model });
675
+ }
676
+ });
677
+ const handleConfirm = async () => {
678
+ setConfirming(false);
679
+ try {
680
+ await client.deleteGeneration(generationId);
681
+ back();
682
+ } catch (err) {
683
+ setError(err instanceof Error ? err.message : "Delete failed");
684
+ }
685
+ };
686
+ const handleCancel = () => {
687
+ setConfirming(false);
688
+ };
689
+ if (loading) return /* @__PURE__ */ jsx10(Spinner5, { label: "Loading..." });
690
+ if (error) return /* @__PURE__ */ jsxs9(Text10, { color: "red", children: [
691
+ "Error: ",
692
+ error
693
+ ] });
694
+ if (!generation) return /* @__PURE__ */ jsx10(Text10, { color: "red", children: "Generation not found" });
695
+ return /* @__PURE__ */ jsxs9(Box10, { flexDirection: "column", gap: 1, children: [
696
+ imagePath && /* @__PURE__ */ jsx10(ImagePreview, { source: imagePath }),
697
+ /* @__PURE__ */ jsxs9(Box10, { flexDirection: "column", children: [
698
+ /* @__PURE__ */ jsxs9(Text10, { children: [
699
+ /* @__PURE__ */ jsx10(Text10, { bold: true, children: "Prompt: " }),
700
+ generation.prompt
701
+ ] }),
702
+ /* @__PURE__ */ jsxs9(Text10, { children: [
703
+ /* @__PURE__ */ jsx10(Text10, { bold: true, children: "Model: " }),
704
+ generation.model
705
+ ] }),
706
+ /* @__PURE__ */ jsxs9(Text10, { children: [
707
+ /* @__PURE__ */ jsx10(Text10, { bold: true, children: "Credits: " }),
708
+ /* @__PURE__ */ jsx10(Text10, { color: "green", children: generation.credits_charged })
709
+ ] }),
710
+ /* @__PURE__ */ jsxs9(Text10, { children: [
711
+ /* @__PURE__ */ jsx10(Text10, { bold: true, children: "Created: " }),
712
+ new Date(generation.created_at).toLocaleString()
713
+ ] }),
714
+ generation.completed_at && /* @__PURE__ */ jsxs9(Text10, { children: [
715
+ /* @__PURE__ */ jsx10(Text10, { bold: true, children: "Duration: " }),
716
+ Math.round(
717
+ (new Date(generation.completed_at).getTime() - new Date(generation.created_at).getTime()) / 1e3
718
+ ),
719
+ "s"
720
+ ] })
721
+ ] }),
722
+ confirming ? /* @__PURE__ */ jsxs9(Box10, { children: [
723
+ /* @__PURE__ */ jsx10(Text10, { color: "red", children: "Delete this generation? " }),
724
+ /* @__PURE__ */ jsx10(ConfirmInput, { onConfirm: handleConfirm, onCancel: handleCancel })
725
+ ] }) : /* @__PURE__ */ jsx10(Text10, { color: "gray", children: "[d] delete | [r] regenerate | esc back" })
726
+ ] });
727
+ }
728
+
729
+ // src/screens/Models.tsx
730
+ import { useState as useState9, useEffect as useEffect6 } from "react";
731
+ import { Box as Box12, Text as Text12, useInput as useInput5 } from "ink";
732
+ import { Spinner as Spinner6 } from "@inkjs/ui";
733
+
734
+ // src/components/ModelTable.tsx
735
+ import { Box as Box11, Text as Text11 } from "ink";
736
+ import { jsx as jsx11, jsxs as jsxs10 } from "react/jsx-runtime";
737
+ function ModelTable({ models, selectedIndex }) {
738
+ return /* @__PURE__ */ jsxs10(Box11, { flexDirection: "column", children: [
739
+ /* @__PURE__ */ jsxs10(Box11, { children: [
740
+ /* @__PURE__ */ jsx11(Box11, { width: 22, children: /* @__PURE__ */ jsx11(Text11, { bold: true, color: "gray", children: "Model" }) }),
741
+ /* @__PURE__ */ jsx11(Box11, { width: 8, children: /* @__PURE__ */ jsx11(Text11, { bold: true, color: "gray", children: "Credits" }) }),
742
+ /* @__PURE__ */ jsx11(Box11, { width: 40, children: /* @__PURE__ */ jsx11(Text11, { bold: true, color: "gray", children: "Strengths" }) })
743
+ ] }),
744
+ models.map((m, i) => {
745
+ const isSelected = i === selectedIndex;
746
+ return /* @__PURE__ */ jsxs10(Box11, { children: [
747
+ /* @__PURE__ */ jsx11(Box11, { width: 22, children: /* @__PURE__ */ jsxs10(Text11, { color: isSelected ? "magenta" : "white", bold: isSelected, children: [
748
+ isSelected ? "> " : " ",
749
+ m.name
750
+ ] }) }),
751
+ /* @__PURE__ */ jsx11(Box11, { width: 8, children: /* @__PURE__ */ jsx11(Text11, { color: m.credit_cost > 1 ? "yellow" : "green", children: m.credit_cost }) }),
752
+ /* @__PURE__ */ jsx11(Box11, { width: 40, children: /* @__PURE__ */ jsx11(Text11, { color: "gray", children: m.strengths.join(", ") }) })
753
+ ] }, m.id);
754
+ })
755
+ ] });
756
+ }
757
+
758
+ // src/screens/Models.tsx
759
+ import { jsx as jsx12, jsxs as jsxs11 } from "react/jsx-runtime";
760
+ function Models({ navigate }) {
761
+ const [models, setModels] = useState9([]);
762
+ const [selectedIndex, setSelectedIndex] = useState9(0);
763
+ const [loading, setLoading] = useState9(true);
764
+ const [error, setError] = useState9(null);
765
+ useEffect6(() => {
766
+ PixelmuseClient.listModels().then(setModels).catch((err) => setError(err instanceof Error ? err.message : "Failed to load")).finally(() => setLoading(false));
767
+ }, []);
768
+ useInput5((input, key) => {
769
+ if (key.upArrow) setSelectedIndex((i) => Math.max(0, i - 1));
770
+ if (key.downArrow) setSelectedIndex((i) => Math.min(models.length - 1, i + 1));
771
+ if (key.return || input === "g") {
772
+ const model = models[selectedIndex];
773
+ if (model) navigate({ screen: "generate", model: model.id });
774
+ }
775
+ });
776
+ if (loading) return /* @__PURE__ */ jsx12(Spinner6, { label: "Loading models..." });
777
+ if (error) return /* @__PURE__ */ jsxs11(Text12, { color: "red", children: [
778
+ "Error: ",
779
+ error
780
+ ] });
781
+ const selected = models[selectedIndex];
782
+ return /* @__PURE__ */ jsxs11(Box12, { flexDirection: "column", gap: 1, children: [
783
+ /* @__PURE__ */ jsx12(ModelTable, { models, selectedIndex }),
784
+ selected && /* @__PURE__ */ jsxs11(Box12, { flexDirection: "column", borderStyle: "single", borderColor: "gray", paddingX: 1, children: [
785
+ /* @__PURE__ */ jsx12(Text12, { bold: true, children: selected.name }),
786
+ /* @__PURE__ */ jsx12(Text12, { color: "gray", children: selected.description }),
787
+ /* @__PURE__ */ jsxs11(Text12, { children: [
788
+ "Aspect ratios: ",
789
+ /* @__PURE__ */ jsx12(Text12, { color: "cyan", children: selected.supported_aspect_ratios.join(", ") })
790
+ ] }),
791
+ selected.weaknesses.length > 0 && /* @__PURE__ */ jsxs11(Text12, { children: [
792
+ "Weaknesses: ",
793
+ /* @__PURE__ */ jsx12(Text12, { color: "yellow", children: selected.weaknesses.join(", ") })
794
+ ] })
795
+ ] }),
796
+ /* @__PURE__ */ jsx12(Text12, { color: "gray", children: "\u2191\u2193 navigate | enter or [g] generate with model" })
797
+ ] });
798
+ }
799
+
800
+ // src/screens/Prompts.tsx
801
+ import { useState as useState10 } from "react";
802
+ import { Box as Box13, Text as Text13, useInput as useInput6 } from "ink";
803
+ import { jsx as jsx13, jsxs as jsxs12 } from "react/jsx-runtime";
804
+ function Prompts({ navigate }) {
805
+ const [templates, setTemplates] = useState10(() => listTemplates());
806
+ const [selectedIndex, setSelectedIndex] = useState10(0);
807
+ useInput6((input, key) => {
808
+ if (key.upArrow) setSelectedIndex((i) => Math.max(0, i - 1));
809
+ if (key.downArrow) setSelectedIndex((i) => Math.min(templates.length - 1, i + 1));
810
+ if (input === "n") navigate({ screen: "prompt-editor" });
811
+ if (input === "e") {
812
+ const t = templates[selectedIndex];
813
+ if (t) navigate({ screen: "prompt-editor", name: t.name });
814
+ }
815
+ if (input === "u") {
816
+ const t = templates[selectedIndex];
817
+ if (t) {
818
+ navigate({
819
+ screen: "generate",
820
+ prompt: t.prompt,
821
+ model: t.defaults.model,
822
+ aspectRatio: t.defaults.aspect_ratio,
823
+ style: t.defaults.style
824
+ });
825
+ }
826
+ }
827
+ if (input === "d") {
828
+ const t = templates[selectedIndex];
829
+ if (t) {
830
+ deleteTemplate(t.name);
831
+ setTemplates(listTemplates());
832
+ setSelectedIndex((i) => Math.min(i, templates.length - 2));
833
+ }
834
+ }
835
+ });
836
+ if (templates.length === 0) {
837
+ return /* @__PURE__ */ jsxs12(Box13, { flexDirection: "column", gap: 1, children: [
838
+ /* @__PURE__ */ jsx13(Text13, { children: "No prompt templates saved." }),
839
+ /* @__PURE__ */ jsx13(Text13, { color: "gray", children: "[n] create new template" })
840
+ ] });
841
+ }
842
+ return /* @__PURE__ */ jsxs12(Box13, { flexDirection: "column", gap: 1, children: [
843
+ templates.map((t, i) => {
844
+ const isSelected = i === selectedIndex;
845
+ return /* @__PURE__ */ jsxs12(Box13, { flexDirection: "column", children: [
846
+ /* @__PURE__ */ jsxs12(Text13, { color: isSelected ? "magenta" : "white", bold: isSelected, children: [
847
+ isSelected ? "> " : " ",
848
+ t.name
849
+ ] }),
850
+ /* @__PURE__ */ jsxs12(Text13, { color: "gray", children: [
851
+ " ",
852
+ t.description,
853
+ t.tags.length > 0 && ` [${t.tags.join(", ")}]`
854
+ ] })
855
+ ] }, t.name);
856
+ }),
857
+ /* @__PURE__ */ jsx13(Text13, { color: "gray", children: "[n] new | [e] edit | [u] use | [d] delete | esc back" })
858
+ ] });
859
+ }
860
+
861
+ // src/screens/PromptEditor.tsx
862
+ import { useState as useState11 } from "react";
863
+ import { Box as Box14, Text as Text14 } from "ink";
864
+ import { TextInput as TextInput3 } from "@inkjs/ui";
865
+ import { jsx as jsx14, jsxs as jsxs13 } from "react/jsx-runtime";
866
+ function PromptEditor({ existingName, onDone: _onDone }) {
867
+ const existing = existingName ? getTemplate(existingName) : null;
868
+ const [step, setStep] = useState11(existing ? "prompt" : "name");
869
+ const [name, setName] = useState11(existing?.name ?? "");
870
+ const [description, setDescription] = useState11(existing?.description ?? "");
871
+ const [prompt, setPrompt] = useState11(existing?.prompt ?? "");
872
+ const [tags, setTags] = useState11(existing?.tags.join(", ") ?? "");
873
+ const handleSave = () => {
874
+ const vars = extractVariables(prompt);
875
+ const variables = {};
876
+ for (const v of vars) {
877
+ variables[v] = existing?.variables[v] ?? "";
878
+ }
879
+ const template = {
880
+ name,
881
+ description,
882
+ prompt,
883
+ defaults: existing?.defaults ?? {},
884
+ variables,
885
+ tags: tags.split(",").map((t) => t.trim()).filter(Boolean)
886
+ };
887
+ saveTemplate(template);
888
+ setStep("done");
889
+ };
890
+ if (step === "done") {
891
+ return /* @__PURE__ */ jsxs13(Box14, { flexDirection: "column", gap: 1, children: [
892
+ /* @__PURE__ */ jsxs13(Text14, { color: "green", children: [
893
+ 'Template "',
894
+ name,
895
+ '" saved!'
896
+ ] }),
897
+ /* @__PURE__ */ jsx14(Text14, { color: "gray", children: "Press esc to go back" })
898
+ ] });
899
+ }
900
+ return /* @__PURE__ */ jsxs13(Box14, { flexDirection: "column", gap: 1, children: [
901
+ /* @__PURE__ */ jsxs13(Text14, { bold: true, children: [
902
+ existing ? "Edit" : "New",
903
+ " Prompt Template"
904
+ ] }),
905
+ step === "name" && /* @__PURE__ */ jsxs13(Box14, { children: [
906
+ /* @__PURE__ */ jsx14(Text14, { children: "Name: " }),
907
+ /* @__PURE__ */ jsx14(
908
+ TextInput3,
909
+ {
910
+ defaultValue: name,
911
+ placeholder: "blog-thumbnail",
912
+ onSubmit: (v) => {
913
+ setName(v.trim());
914
+ setStep("description");
915
+ }
916
+ }
917
+ )
918
+ ] }),
919
+ step === "description" && /* @__PURE__ */ jsxs13(Box14, { children: [
920
+ /* @__PURE__ */ jsx14(Text14, { children: "Description: " }),
921
+ /* @__PURE__ */ jsx14(
922
+ TextInput3,
923
+ {
924
+ defaultValue: description,
925
+ placeholder: "Dark-themed blog post thumbnail",
926
+ onSubmit: (v) => {
927
+ setDescription(v.trim());
928
+ setStep("prompt");
929
+ }
930
+ }
931
+ )
932
+ ] }),
933
+ step === "prompt" && /* @__PURE__ */ jsxs13(Box14, { flexDirection: "column", gap: 1, children: [
934
+ /* @__PURE__ */ jsxs13(Text14, { color: "gray", children: [
935
+ "Use ",
936
+ "{{variable}}",
937
+ " for placeholders"
938
+ ] }),
939
+ /* @__PURE__ */ jsxs13(Box14, { children: [
940
+ /* @__PURE__ */ jsx14(Text14, { children: "Prompt: " }),
941
+ /* @__PURE__ */ jsx14(
942
+ TextInput3,
943
+ {
944
+ defaultValue: prompt,
945
+ placeholder: "A cinematic {{subject}} on a dark background...",
946
+ onSubmit: (v) => {
947
+ setPrompt(v.trim());
948
+ setStep("tags");
949
+ }
950
+ }
951
+ )
952
+ ] })
953
+ ] }),
954
+ step === "tags" && /* @__PURE__ */ jsxs13(Box14, { children: [
955
+ /* @__PURE__ */ jsx14(Text14, { children: "Tags (comma separated): " }),
956
+ /* @__PURE__ */ jsx14(
957
+ TextInput3,
958
+ {
959
+ defaultValue: tags,
960
+ placeholder: "blog, thumbnail, dark",
961
+ onSubmit: (v) => {
962
+ setTags(v);
963
+ handleSave();
964
+ }
965
+ }
966
+ )
967
+ ] })
968
+ ] });
969
+ }
970
+
971
+ // src/screens/Account.tsx
972
+ import { useState as useState12, useEffect as useEffect7 } from "react";
973
+ import { Box as Box15, Text as Text15, useInput as useInput7 } from "ink";
974
+ import { Spinner as Spinner7 } from "@inkjs/ui";
975
+ import { jsx as jsx15, jsxs as jsxs14 } from "react/jsx-runtime";
976
+ function Account({ client, account, navigate, onLogout }) {
977
+ const [usage, setUsage] = useState12(null);
978
+ const [loading, setLoading] = useState12(true);
979
+ useEffect7(() => {
980
+ const now = /* @__PURE__ */ new Date();
981
+ const thirtyDaysAgo = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1e3);
982
+ client.getUsage({ start: thirtyDaysAgo.toISOString(), end: now.toISOString() }).then(setUsage).catch(() => {
983
+ }).finally(() => setLoading(false));
984
+ }, []);
985
+ useInput7((input) => {
986
+ if (input === "b") navigate({ screen: "buy-credits" });
987
+ if (input === "l") onLogout();
988
+ });
989
+ if (!account) return /* @__PURE__ */ jsx15(Spinner7, { label: "Loading account..." });
990
+ return /* @__PURE__ */ jsxs14(Box15, { flexDirection: "column", gap: 1, children: [
991
+ /* @__PURE__ */ jsxs14(Box15, { flexDirection: "column", children: [
992
+ /* @__PURE__ */ jsxs14(Text15, { children: [
993
+ /* @__PURE__ */ jsx15(Text15, { bold: true, children: "Email: " }),
994
+ account.email
995
+ ] }),
996
+ /* @__PURE__ */ jsxs14(Text15, { children: [
997
+ /* @__PURE__ */ jsx15(Text15, { bold: true, children: "Plan: " }),
998
+ /* @__PURE__ */ jsx15(Text15, { color: "cyan", children: account.plan })
999
+ ] }),
1000
+ /* @__PURE__ */ jsxs14(Text15, { children: [
1001
+ /* @__PURE__ */ jsx15(Text15, { bold: true, children: "Credits: " }),
1002
+ /* @__PURE__ */ jsx15(Text15, { color: "green", children: account.credits.total }),
1003
+ /* @__PURE__ */ jsxs14(Text15, { color: "gray", children: [
1004
+ " ",
1005
+ "(subscription: ",
1006
+ account.credits.subscription,
1007
+ ", purchased: ",
1008
+ account.credits.purchased,
1009
+ ")"
1010
+ ] })
1011
+ ] }),
1012
+ /* @__PURE__ */ jsxs14(Text15, { children: [
1013
+ /* @__PURE__ */ jsx15(Text15, { bold: true, children: "Rate limit: " }),
1014
+ account.rate_limit.requests_per_minute,
1015
+ " req/min"
1016
+ ] })
1017
+ ] }),
1018
+ loading ? /* @__PURE__ */ jsx15(Spinner7, { label: "Loading usage..." }) : usage ? /* @__PURE__ */ jsxs14(Box15, { flexDirection: "column", children: [
1019
+ /* @__PURE__ */ jsx15(Text15, { bold: true, children: "30-Day Usage" }),
1020
+ /* @__PURE__ */ jsxs14(Text15, { children: [
1021
+ "Generations: ",
1022
+ /* @__PURE__ */ jsx15(Text15, { color: "cyan", children: usage.generations_count }),
1023
+ " | Credits used:",
1024
+ " ",
1025
+ /* @__PURE__ */ jsx15(Text15, { color: "yellow", children: usage.credits_used })
1026
+ ] }),
1027
+ usage.by_model.length > 0 && /* @__PURE__ */ jsx15(Box15, { flexDirection: "column", marginTop: 1, children: usage.by_model.map((m) => /* @__PURE__ */ jsxs14(Text15, { children: [
1028
+ " ",
1029
+ m.model,
1030
+ ": ",
1031
+ m.count,
1032
+ " generations (",
1033
+ m.credits,
1034
+ " credits)"
1035
+ ] }, m.model)) })
1036
+ ] }) : null,
1037
+ /* @__PURE__ */ jsx15(Text15, { color: "gray", children: "[b] buy credits | [l] logout | esc back" })
1038
+ ] });
1039
+ }
1040
+
1041
+ // src/screens/BuyCredits.tsx
1042
+ import { useState as useState13, useEffect as useEffect8 } from "react";
1043
+ import { Box as Box16, Text as Text16, useInput as useInput8 } from "ink";
1044
+ import { Select as Select3, Spinner as Spinner8 } from "@inkjs/ui";
1045
+ import open from "open";
1046
+ import { jsx as jsx16, jsxs as jsxs15 } from "react/jsx-runtime";
1047
+ function BuyCredits({ client, onRefreshAccount, back }) {
1048
+ const [packages, setPackages] = useState13([]);
1049
+ const [loading, setLoading] = useState13(true);
1050
+ const [checkoutOpened, setCheckoutOpened] = useState13(false);
1051
+ const [error, setError] = useState13(null);
1052
+ useEffect8(() => {
1053
+ PixelmuseClient.listPackages().then(setPackages).catch((err) => setError(err instanceof Error ? err.message : "Failed to load")).finally(() => setLoading(false));
1054
+ }, []);
1055
+ useInput8(() => {
1056
+ if (checkoutOpened) {
1057
+ onRefreshAccount();
1058
+ back();
1059
+ }
1060
+ });
1061
+ if (loading) return /* @__PURE__ */ jsx16(Spinner8, { label: "Loading packages..." });
1062
+ if (error) return /* @__PURE__ */ jsxs15(Text16, { color: "red", children: [
1063
+ "Error: ",
1064
+ error
1065
+ ] });
1066
+ if (checkoutOpened) {
1067
+ return /* @__PURE__ */ jsxs15(Box16, { flexDirection: "column", gap: 1, children: [
1068
+ /* @__PURE__ */ jsx16(Text16, { color: "green", children: "Checkout opened in your browser." }),
1069
+ /* @__PURE__ */ jsx16(Text16, { color: "gray", children: "Press any key to refresh your balance and go back." })
1070
+ ] });
1071
+ }
1072
+ const options = packages.map((p) => ({
1073
+ label: `${p.label} \u2014 ${p.credits + p.bonus_credits} credits \u2014 $${p.price_usd}`,
1074
+ value: p.name
1075
+ }));
1076
+ return /* @__PURE__ */ jsxs15(Box16, { flexDirection: "column", gap: 1, children: [
1077
+ /* @__PURE__ */ jsx16(Text16, { bold: true, children: "Buy Credits" }),
1078
+ /* @__PURE__ */ jsx16(
1079
+ Select3,
1080
+ {
1081
+ options,
1082
+ onChange: async (value) => {
1083
+ try {
1084
+ const { checkout_url } = await client.createCheckout({
1085
+ package: value
1086
+ });
1087
+ await open(checkout_url);
1088
+ setCheckoutOpened(true);
1089
+ } catch (err) {
1090
+ setError(err instanceof Error ? err.message : "Checkout failed");
1091
+ }
1092
+ }
1093
+ }
1094
+ )
1095
+ ] });
1096
+ }
1097
+
1098
+ // src/app.tsx
1099
+ import { jsx as jsx17, jsxs as jsxs16 } from "react/jsx-runtime";
1100
+ var PUBLIC_SCREENS = /* @__PURE__ */ new Set(["login", "models"]);
1101
+ function App({ initialRoute }) {
1102
+ const { exit } = useApp2();
1103
+ const { route, navigate, back, replace } = useRouter(initialRoute ?? { screen: "home" });
1104
+ const auth = useAuth();
1105
+ const client = useApi(auth.apiKey);
1106
+ const { settings } = useConfig();
1107
+ useEffect9(() => {
1108
+ if (!auth.isLoading && !auth.isAuthenticated && !PUBLIC_SCREENS.has(route.screen)) {
1109
+ replace({ screen: "login" });
1110
+ }
1111
+ }, [auth.isLoading, auth.isAuthenticated, route.screen]);
1112
+ useInput9((input, key) => {
1113
+ if (input === "q") exit();
1114
+ if (key.escape) back();
1115
+ });
1116
+ if (auth.isLoading) {
1117
+ return /* @__PURE__ */ jsxs16(Box17, { flexDirection: "column", children: [
1118
+ /* @__PURE__ */ jsx17(Header, { route }),
1119
+ /* @__PURE__ */ jsx17(Spinner9, { label: "Loading..." })
1120
+ ] });
1121
+ }
1122
+ const renderScreen = () => {
1123
+ switch (route.screen) {
1124
+ case "login":
1125
+ return /* @__PURE__ */ jsx17(
1126
+ Login,
1127
+ {
1128
+ onLogin: auth.login,
1129
+ onSuccess: () => navigate({ screen: "home" })
1130
+ }
1131
+ );
1132
+ case "home":
1133
+ return /* @__PURE__ */ jsx17(
1134
+ Home,
1135
+ {
1136
+ account: auth.account,
1137
+ navigate,
1138
+ onLogout: async () => {
1139
+ await auth.logout();
1140
+ replace({ screen: "login" });
1141
+ }
1142
+ }
1143
+ );
1144
+ case "generate":
1145
+ if (!client) return null;
1146
+ return /* @__PURE__ */ jsx17(
1147
+ Generate,
1148
+ {
1149
+ client,
1150
+ navigate,
1151
+ initialPrompt: route.prompt,
1152
+ initialModel: route.model,
1153
+ initialAspectRatio: route.aspectRatio,
1154
+ initialStyle: route.style,
1155
+ defaultModel: settings.defaultModel,
1156
+ defaultAspectRatio: settings.defaultAspectRatio,
1157
+ defaultStyle: settings.defaultStyle
1158
+ }
1159
+ );
1160
+ case "gallery":
1161
+ if (!client) return null;
1162
+ return /* @__PURE__ */ jsx17(Gallery, { client, navigate });
1163
+ case "gallery-detail":
1164
+ if (!client) return null;
1165
+ return /* @__PURE__ */ jsx17(
1166
+ GalleryDetail,
1167
+ {
1168
+ client,
1169
+ generationId: route.id,
1170
+ navigate,
1171
+ back
1172
+ }
1173
+ );
1174
+ case "models":
1175
+ return /* @__PURE__ */ jsx17(Models, { navigate });
1176
+ case "prompts":
1177
+ return /* @__PURE__ */ jsx17(Prompts, { navigate });
1178
+ case "prompt-editor":
1179
+ return /* @__PURE__ */ jsx17(PromptEditor, { existingName: route.name, onDone: back });
1180
+ case "account":
1181
+ if (!client) return null;
1182
+ return /* @__PURE__ */ jsx17(
1183
+ Account,
1184
+ {
1185
+ client,
1186
+ account: auth.account,
1187
+ navigate,
1188
+ onLogout: async () => {
1189
+ await auth.logout();
1190
+ replace({ screen: "login" });
1191
+ }
1192
+ }
1193
+ );
1194
+ case "buy-credits":
1195
+ if (!client) return null;
1196
+ return /* @__PURE__ */ jsx17(
1197
+ BuyCredits,
1198
+ {
1199
+ client,
1200
+ onRefreshAccount: auth.refreshAccount,
1201
+ back
1202
+ }
1203
+ );
1204
+ default:
1205
+ return null;
1206
+ }
1207
+ };
1208
+ return /* @__PURE__ */ jsxs16(Box17, { flexDirection: "column", children: [
1209
+ /* @__PURE__ */ jsx17(Header, { route }),
1210
+ renderScreen(),
1211
+ /* @__PURE__ */ jsx17(StatusBar, { account: auth.account })
1212
+ ] });
1213
+ }
1214
+
1215
+ // src/tui.tsx
1216
+ import { jsx as jsx18 } from "react/jsx-runtime";
1217
+ function showPreview(payload) {
1218
+ const { generation, imagePath } = payload;
1219
+ if (imagePath) {
1220
+ renderImageDirect(imagePath);
1221
+ }
1222
+ console.log();
1223
+ console.log(
1224
+ `Model: \x1B[1m${generation.model}\x1B[0m | Credits: \x1B[32m${generation.credits_charged}\x1B[0m`
1225
+ );
1226
+ if (imagePath) {
1227
+ console.log(`\x1B[2mSaved to ${imagePath}\x1B[0m`);
1228
+ }
1229
+ console.log();
1230
+ console.log("\x1B[90m[r] regenerate | [g] gallery | [h] home | [q] quit\x1B[0m");
1231
+ return new Promise((resolve) => {
1232
+ const { stdin } = process;
1233
+ const wasRaw = stdin.isRaw;
1234
+ stdin.setRawMode(true);
1235
+ stdin.resume();
1236
+ const handler = (data) => {
1237
+ const key = data.toString();
1238
+ stdin.setRawMode(wasRaw ?? false);
1239
+ stdin.removeListener("data", handler);
1240
+ stdin.pause();
1241
+ switch (key) {
1242
+ case "r":
1243
+ resolve({ screen: "generate" });
1244
+ break;
1245
+ case "g":
1246
+ resolve({ screen: "gallery" });
1247
+ break;
1248
+ case "h":
1249
+ resolve({ screen: "home" });
1250
+ break;
1251
+ default:
1252
+ resolve(null);
1253
+ }
1254
+ };
1255
+ stdin.on("data", handler);
1256
+ });
1257
+ }
1258
+ async function launchTui(initialRoute = { screen: "home" }) {
1259
+ let route = initialRoute;
1260
+ while (route) {
1261
+ const instance = render(/* @__PURE__ */ jsx18(App, { initialRoute: route }));
1262
+ await instance.waitUntilExit();
1263
+ const payload = globalThis.__pixelmuse_preview;
1264
+ delete globalThis.__pixelmuse_preview;
1265
+ if (payload?.type === "preview") {
1266
+ route = await showPreview(payload);
1267
+ } else {
1268
+ route = null;
1269
+ }
1270
+ }
1271
+ }
1272
+ export {
1273
+ launchTui
1274
+ };