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/LICENSE +88 -0
- package/README.md +274 -0
- package/dist/chunk-743CKPHW.js +60 -0
- package/dist/chunk-7ARYEFAH.js +52 -0
- package/dist/chunk-MZZY4JXW.js +63 -0
- package/dist/chunk-ZVJQFWUI.js +272 -0
- package/dist/cli.js +531 -0
- package/dist/config-RMVFR3GN.js +13 -0
- package/dist/image-2H3GUQI6.js +16 -0
- package/dist/mcp/server.js +133 -0
- package/dist/tui.js +1274 -0
- package/package.json +94 -0
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
|
+
};
|