plotlink-ows 1.0.33 → 1.2.95
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/README.md +4 -0
- package/app/lib/active-wallet.ts +260 -0
- package/app/lib/agent-command.ts +85 -0
- package/app/lib/agent-readiness.ts +133 -0
- package/app/lib/apply-schema.ts +55 -0
- package/app/lib/bubble-text.ts +160 -0
- package/app/lib/cartoon-coach.ts +198 -0
- package/app/lib/cartoon-markdown.ts +83 -0
- package/app/lib/cartoon-prompt.ts +122 -0
- package/app/lib/cartoon-readiness.ts +813 -0
- package/app/lib/clean-image-sync.ts +245 -0
- package/app/lib/codex-images.ts +152 -0
- package/app/lib/cut-asset-diagnostics.ts +120 -0
- package/app/lib/cuts.ts +302 -0
- package/app/lib/fonts.ts +109 -0
- package/app/lib/generate-claude-md.ts +8 -1
- package/app/lib/generate-story-instructions.ts +731 -0
- package/app/lib/image-asset-validate.ts +123 -0
- package/app/lib/lettering-status.ts +133 -0
- package/app/lib/overlays.ts +637 -0
- package/app/lib/paths.ts +10 -0
- package/app/lib/public-title.ts +65 -0
- package/app/lib/publish.ts +16 -2
- package/app/lib/story-progress.ts +242 -0
- package/app/lib/terminal-protocol.ts +16 -0
- package/app/lib/terminal-redact.ts +50 -0
- package/app/prisma/schema.sql +25 -0
- package/app/routes/agent.ts +42 -0
- package/app/routes/codex-images.ts +67 -0
- package/app/routes/dashboard.ts +6 -4
- package/app/routes/publish.ts +259 -45
- package/app/routes/settings.ts +92 -37
- package/app/routes/stories.ts +961 -5
- package/app/routes/terminal.ts +383 -31
- package/app/routes/wallet.ts +58 -30
- package/app/server.ts +47 -12
- package/app/vite.config.ts +6 -0
- package/app/web/components/CartoonNextAction.tsx +145 -0
- package/app/web/components/CartoonPreview.tsx +267 -0
- package/app/web/components/CartoonPublishPage.tsx +407 -0
- package/app/web/components/CartoonPublishPreview.tsx +121 -0
- package/app/web/components/CartoonStepGuide.tsx +90 -0
- package/app/web/components/CartoonWorkflowNav.tsx +68 -0
- package/app/web/components/CodexImportPicker.tsx +230 -0
- package/app/web/components/CutListPanel.tsx +1337 -0
- package/app/web/components/Dashboard.tsx +15 -6
- package/app/web/components/EpisodesPage.tsx +80 -0
- package/app/web/components/FinishEpisodePanel.tsx +151 -0
- package/app/web/components/Layout.tsx +7 -4
- package/app/web/components/LetteringEditor.tsx +1182 -0
- package/app/web/components/PreviewPanel.tsx +952 -78
- package/app/web/components/Settings.tsx +63 -0
- package/app/web/components/StoriesPage.tsx +745 -33
- package/app/web/components/StoryBrowser.tsx +22 -14
- package/app/web/components/StoryInfoPage.tsx +266 -0
- package/app/web/components/StoryProgressPanel.tsx +446 -0
- package/app/web/components/TerminalPanel.tsx +233 -11
- package/app/web/components/WalletCard.tsx +110 -8
- package/app/web/components/WorkflowCoach.tsx +156 -0
- package/app/web/components/asset-image.tsx +114 -0
- package/app/web/components/asset-test-utils.ts +44 -0
- package/app/web/components/export-cut.ts +320 -0
- package/app/web/dist/assets/export-cut-che5mMWc.js +1 -0
- package/app/web/dist/assets/index-CcfChGEK.css +32 -0
- package/app/web/dist/assets/index-Dc2TQ3Ij.js +143 -0
- package/app/web/dist/index.html +2 -2
- package/app/web/lib/cartoon-publish-summary.ts +43 -0
- package/app/web/lib/codex-import.ts +94 -0
- package/app/web/lib/image-compress.ts +53 -0
- package/app/web/lib/import-image.ts +58 -0
- package/app/web/lib/publish-helpers.ts +385 -0
- package/app/web/lib/upload-retry.ts +130 -0
- package/app/web/lib/verify-public-title.ts +105 -0
- package/app/web/styles.css +9 -0
- package/bin/plotlink-ows.js +53 -16
- package/bin/startup-plan.cjs +58 -0
- package/lib/genres.ts +92 -0
- package/package.json +60 -20
- package/scripts/gen-schema-sql.mjs +49 -0
- package/scripts/package-hygiene.mjs +116 -0
- package/scripts/preflight.mjs +173 -0
- package/scripts/start-smoke.mjs +128 -0
- package/app/node_modules/.prisma/local-client/client.d.ts +0 -1
- package/app/node_modules/.prisma/local-client/client.js +0 -5
- package/app/node_modules/.prisma/local-client/default.d.ts +0 -1
- package/app/node_modules/.prisma/local-client/default.js +0 -5
- package/app/node_modules/.prisma/local-client/edge.d.ts +0 -1
- package/app/node_modules/.prisma/local-client/edge.js +0 -184
- package/app/node_modules/.prisma/local-client/index-browser.js +0 -173
- package/app/node_modules/.prisma/local-client/index.d.ts +0 -3304
- package/app/node_modules/.prisma/local-client/index.js +0 -207
- package/app/node_modules/.prisma/local-client/libquery_engine-darwin-arm64.dylib.node +0 -0
- package/app/node_modules/.prisma/local-client/package.json +0 -183
- package/app/node_modules/.prisma/local-client/query_engine_bg.js +0 -2
- package/app/node_modules/.prisma/local-client/query_engine_bg.wasm +0 -0
- package/app/node_modules/.prisma/local-client/runtime/edge-esm.js +0 -35
- package/app/node_modules/.prisma/local-client/runtime/edge.js +0 -35
- package/app/node_modules/.prisma/local-client/runtime/index-browser.d.ts +0 -370
- package/app/node_modules/.prisma/local-client/runtime/index-browser.js +0 -17
- package/app/node_modules/.prisma/local-client/runtime/library.d.ts +0 -3982
- package/app/node_modules/.prisma/local-client/runtime/library.js +0 -147
- package/app/node_modules/.prisma/local-client/runtime/react-native.js +0 -84
- package/app/node_modules/.prisma/local-client/runtime/wasm-compiler-edge.js +0 -85
- package/app/node_modules/.prisma/local-client/runtime/wasm-engine-edge.js +0 -38
- package/app/node_modules/.prisma/local-client/schema.prisma +0 -21
- package/app/node_modules/.prisma/local-client/wasm-edge-light-loader.mjs +0 -5
- package/app/node_modules/.prisma/local-client/wasm-worker-loader.mjs +0 -5
- package/app/node_modules/.prisma/local-client/wasm.d.ts +0 -1
- package/app/node_modules/.prisma/local-client/wasm.js +0 -191
- package/app/web/dist/assets/index-B-2Ft7Yv.css +0 -32
- package/app/web/dist/assets/index-DxATSk7X.js +0 -134
- package/packages/cli/node_modules/commander/LICENSE +0 -22
- package/packages/cli/node_modules/commander/Readme.md +0 -1149
- package/packages/cli/node_modules/commander/esm.mjs +0 -16
- package/packages/cli/node_modules/commander/index.js +0 -24
- package/packages/cli/node_modules/commander/lib/argument.js +0 -149
- package/packages/cli/node_modules/commander/lib/command.js +0 -2662
- package/packages/cli/node_modules/commander/lib/error.js +0 -39
- package/packages/cli/node_modules/commander/lib/help.js +0 -709
- package/packages/cli/node_modules/commander/lib/option.js +0 -367
- package/packages/cli/node_modules/commander/lib/suggestSimilar.js +0 -101
- package/packages/cli/node_modules/commander/package-support.json +0 -16
- package/packages/cli/node_modules/commander/package.json +0 -82
- package/packages/cli/node_modules/commander/typings/esm.d.mts +0 -3
- package/packages/cli/node_modules/commander/typings/index.d.ts +0 -1045
- package/packages/cli/node_modules/resolve-from/index.d.ts +0 -31
- package/packages/cli/node_modules/resolve-from/index.js +0 -47
- package/packages/cli/node_modules/resolve-from/license +0 -9
- package/packages/cli/node_modules/resolve-from/package.json +0 -36
- package/packages/cli/node_modules/resolve-from/readme.md +0 -72
- package/packages/cli/node_modules/tsup/LICENSE +0 -21
- package/packages/cli/node_modules/tsup/README.md +0 -75
- package/packages/cli/node_modules/tsup/assets/cjs_shims.js +0 -13
- package/packages/cli/node_modules/tsup/assets/esm_shims.js +0 -9
- package/packages/cli/node_modules/tsup/assets/package.json +0 -3
- package/packages/cli/node_modules/tsup/dist/chunk-DI5BO6XE.js +0 -153
- package/packages/cli/node_modules/tsup/dist/chunk-JZ25TPTY.js +0 -42
- package/packages/cli/node_modules/tsup/dist/chunk-PEEXUWMS.js +0 -6
- package/packages/cli/node_modules/tsup/dist/chunk-TWFEYLU4.js +0 -352
- package/packages/cli/node_modules/tsup/dist/chunk-VGC3FXLU.js +0 -203
- package/packages/cli/node_modules/tsup/dist/cli-default.js +0 -12
- package/packages/cli/node_modules/tsup/dist/cli-main.js +0 -8
- package/packages/cli/node_modules/tsup/dist/cli-node.js +0 -14
- package/packages/cli/node_modules/tsup/dist/index.d.ts +0 -511
- package/packages/cli/node_modules/tsup/dist/index.js +0 -1711
- package/packages/cli/node_modules/tsup/dist/rollup.js +0 -6949
- package/packages/cli/node_modules/tsup/package.json +0 -99
- package/packages/cli/node_modules/tsup/schema.json +0 -362
- package/public/screenshot-1.png +0 -0
- package/public/screenshot-2.png +0 -0
- package/public/screenshot-3.png +0 -0
- package/scripts/e2e-verify.ts +0 -1100
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Persistent right-panel workflow navigation for cartoon stories (#439, spec §2).
|
|
3
|
+
*
|
|
4
|
+
* A normal webtoon creator should not need the file tree: this compact tab bar
|
|
5
|
+
* sits above the right-panel content whenever a CARTOON story is selected and
|
|
6
|
+
* routes between the workflow pages — Progress, Story Info, Whitepaper, Genesis /
|
|
7
|
+
* Episode 1, Episodes, Publish. The left file tree stays for power users; opening
|
|
8
|
+
* a file directly just reflects the closest workflow tab here.
|
|
9
|
+
*
|
|
10
|
+
* Fiction renders no nav (the caller only mounts this for cartoon stories), so
|
|
11
|
+
* the fiction UX is unchanged.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
export type CartoonWorkflowTab =
|
|
15
|
+
| "progress"
|
|
16
|
+
| "story-info"
|
|
17
|
+
| "whitepaper"
|
|
18
|
+
| "genesis"
|
|
19
|
+
| "episodes"
|
|
20
|
+
| "publish";
|
|
21
|
+
|
|
22
|
+
const TABS: { key: CartoonWorkflowTab; label: string }[] = [
|
|
23
|
+
{ key: "progress", label: "Progress" },
|
|
24
|
+
{ key: "story-info", label: "Story Info" },
|
|
25
|
+
{ key: "whitepaper", label: "Whitepaper" },
|
|
26
|
+
{ key: "genesis", label: "Genesis / Ep 1" },
|
|
27
|
+
{ key: "episodes", label: "Episodes" },
|
|
28
|
+
{ key: "publish", label: "Publish" },
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
interface CartoonWorkflowNavProps {
|
|
32
|
+
storyTitle: string;
|
|
33
|
+
active: CartoonWorkflowTab;
|
|
34
|
+
onSelect: (tab: CartoonWorkflowTab) => void;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function CartoonWorkflowNav({ storyTitle, active, onSelect }: CartoonWorkflowNavProps) {
|
|
38
|
+
return (
|
|
39
|
+
<div className="flex-shrink-0 border-b border-border bg-surface/40" data-testid="cartoon-workflow-nav">
|
|
40
|
+
<div className="flex items-center gap-2 px-3 pt-2">
|
|
41
|
+
<span className="text-[10px] font-medium uppercase tracking-[0.14em] text-accent">Cartoon</span>
|
|
42
|
+
<span className="text-xs font-serif text-foreground truncate">{storyTitle}</span>
|
|
43
|
+
</div>
|
|
44
|
+
<div className="flex items-center gap-1 px-2 py-1.5 overflow-x-auto" role="tablist">
|
|
45
|
+
{TABS.map((tab) => {
|
|
46
|
+
const isActive = tab.key === active;
|
|
47
|
+
return (
|
|
48
|
+
<button
|
|
49
|
+
key={tab.key}
|
|
50
|
+
role="tab"
|
|
51
|
+
aria-selected={isActive}
|
|
52
|
+
data-testid={`nav-tab-${tab.key}`}
|
|
53
|
+
data-active={isActive}
|
|
54
|
+
onClick={() => onSelect(tab.key)}
|
|
55
|
+
className={`flex-shrink-0 rounded-full px-2.5 py-1 text-[11px] font-medium transition-colors ${
|
|
56
|
+
isActive
|
|
57
|
+
? "bg-accent text-white"
|
|
58
|
+
: "text-muted hover:text-foreground hover:bg-surface"
|
|
59
|
+
}`}
|
|
60
|
+
>
|
|
61
|
+
{tab.label}
|
|
62
|
+
</button>
|
|
63
|
+
);
|
|
64
|
+
})}
|
|
65
|
+
</div>
|
|
66
|
+
</div>
|
|
67
|
+
);
|
|
68
|
+
}
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import { useEffect, useMemo, useState } from "react";
|
|
2
|
+
import { listCodexCacheImages, fetchCodexCacheFile, type CodexCacheImage } from "../lib/codex-import";
|
|
3
|
+
|
|
4
|
+
type AuthFetch = (url: string, opts?: RequestInit) => Promise<Response>;
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Codex generated-image cache picker (#403, visual selection + filtering #409).
|
|
8
|
+
*
|
|
9
|
+
* Lists the recent images in Codex's generated-image cache (newest first) and
|
|
10
|
+
* lets the writer import one straight into the current cut — so a Codex-generated
|
|
11
|
+
* PNG no longer requires hunting through a hidden `~/.codex/generated_images`
|
|
12
|
+
* folder in an OS file dialog. Picking an image fetches its bytes as a File and
|
|
13
|
+
* hands it to `onImport`, which runs the SAME in-browser PNG→WebP conversion +
|
|
14
|
+
* upload-clean path as a manually-selected file, so the asset constraints and
|
|
15
|
+
* upload validation are unchanged.
|
|
16
|
+
*
|
|
17
|
+
* #409: the cache can hold a long run of near-identical `ig_<hash>.png` names, so
|
|
18
|
+
* the picker is built for *visual* selection — a large thumbnail leads each row,
|
|
19
|
+
* the noisy hash filename is demoted to a hover title, and the readable metadata
|
|
20
|
+
* (how recently it was generated + its size) is what the writer reads. A filter
|
|
21
|
+
* box narrows a long list by filename. The list stays read-only until the writer
|
|
22
|
+
* explicitly clicks Import.
|
|
23
|
+
*
|
|
24
|
+
* Read-only and best-effort: a missing/empty cache (e.g. Codex not installed)
|
|
25
|
+
* simply shows an empty state with no error, since this is an optional shortcut
|
|
26
|
+
* over the still-present manual "Upload clean image" button.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
/** Load an auth-protected URL as a blob object URL for an <img> thumbnail. */
|
|
30
|
+
function useAuthedObjectUrl(url: string, authFetch: AuthFetch): string | null {
|
|
31
|
+
const [objectUrl, setObjectUrl] = useState<string | null>(null);
|
|
32
|
+
useEffect(() => {
|
|
33
|
+
let revoked: string | null = null;
|
|
34
|
+
let cancelled = false;
|
|
35
|
+
(async () => {
|
|
36
|
+
try {
|
|
37
|
+
const res = await authFetch(url);
|
|
38
|
+
if (!res.ok) return;
|
|
39
|
+
const blob = await res.blob();
|
|
40
|
+
if (cancelled) return;
|
|
41
|
+
revoked = URL.createObjectURL(blob);
|
|
42
|
+
setObjectUrl(revoked);
|
|
43
|
+
} catch {
|
|
44
|
+
/* best-effort thumbnail; the row still imports without it */
|
|
45
|
+
}
|
|
46
|
+
})();
|
|
47
|
+
return () => {
|
|
48
|
+
cancelled = true;
|
|
49
|
+
if (revoked) URL.revokeObjectURL(revoked);
|
|
50
|
+
};
|
|
51
|
+
}, [url, authFetch]);
|
|
52
|
+
return objectUrl;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function formatSize(bytes: number): string {
|
|
56
|
+
if (bytes >= 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
57
|
+
if (bytes >= 1024) return `${Math.round(bytes / 1024)} KB`;
|
|
58
|
+
return `${bytes} B`;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Human "how long ago" label for a cache image's mtime (#409). Pure and
|
|
63
|
+
* now-injectable so it's deterministic in tests. The cache lists newest-first, so
|
|
64
|
+
* this is the writer's main cue for "which one did I just generate".
|
|
65
|
+
*/
|
|
66
|
+
export function formatRelativeTime(mtimeMs: number, nowMs: number): string {
|
|
67
|
+
const diff = nowMs - mtimeMs;
|
|
68
|
+
if (!Number.isFinite(diff) || diff < 45_000) return "just now";
|
|
69
|
+
const mins = Math.round(diff / 60_000);
|
|
70
|
+
if (mins < 60) return `${mins}m ago`;
|
|
71
|
+
const hours = Math.round(diff / 3_600_000);
|
|
72
|
+
if (hours < 24) return `${hours}h ago`;
|
|
73
|
+
const days = Math.round(diff / 86_400_000);
|
|
74
|
+
if (days < 7) return `${days}d ago`;
|
|
75
|
+
const weeks = Math.round(diff / (7 * 86_400_000));
|
|
76
|
+
return `${weeks}w ago`;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function CodexThumb({ image, authFetch }: { image: CodexCacheImage; authFetch: AuthFetch }) {
|
|
80
|
+
const url = useAuthedObjectUrl(`/api/codex/images/${encodeURIComponent(image.token)}`, authFetch);
|
|
81
|
+
if (!url) {
|
|
82
|
+
return <div className="w-16 h-16 flex-shrink-0 rounded border border-border bg-surface" />;
|
|
83
|
+
}
|
|
84
|
+
return (
|
|
85
|
+
<img
|
|
86
|
+
src={url}
|
|
87
|
+
alt={image.name}
|
|
88
|
+
className="w-16 h-16 flex-shrink-0 rounded border border-border object-cover bg-white"
|
|
89
|
+
/>
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function CodexImportPicker({
|
|
94
|
+
authFetch,
|
|
95
|
+
cutId,
|
|
96
|
+
onImport,
|
|
97
|
+
onClose,
|
|
98
|
+
}: {
|
|
99
|
+
authFetch: AuthFetch;
|
|
100
|
+
cutId: number;
|
|
101
|
+
/** Receives the fetched cache file; runs the shared PNG→WebP import + upload. */
|
|
102
|
+
onImport: (file: File) => Promise<void>;
|
|
103
|
+
onClose: () => void;
|
|
104
|
+
}) {
|
|
105
|
+
const [images, setImages] = useState<CodexCacheImage[] | null>(null);
|
|
106
|
+
const [error, setError] = useState<string | null>(null);
|
|
107
|
+
const [importingToken, setImportingToken] = useState<string | null>(null);
|
|
108
|
+
const [query, setQuery] = useState("");
|
|
109
|
+
|
|
110
|
+
useEffect(() => {
|
|
111
|
+
let cancelled = false;
|
|
112
|
+
(async () => {
|
|
113
|
+
const list = await listCodexCacheImages(authFetch);
|
|
114
|
+
if (!cancelled) setImages(list);
|
|
115
|
+
})();
|
|
116
|
+
return () => {
|
|
117
|
+
cancelled = true;
|
|
118
|
+
};
|
|
119
|
+
}, [authFetch]);
|
|
120
|
+
|
|
121
|
+
const trimmedQuery = query.trim().toLowerCase();
|
|
122
|
+
const filtered = useMemo(() => {
|
|
123
|
+
if (!images) return [];
|
|
124
|
+
if (!trimmedQuery) return images;
|
|
125
|
+
return images.filter((img) => img.name.toLowerCase().includes(trimmedQuery));
|
|
126
|
+
}, [images, trimmedQuery]);
|
|
127
|
+
|
|
128
|
+
// One timestamp per render so all rows share the same "x ago" reference point.
|
|
129
|
+
const now = Date.now();
|
|
130
|
+
|
|
131
|
+
const handlePick = async (image: CodexCacheImage) => {
|
|
132
|
+
setError(null);
|
|
133
|
+
setImportingToken(image.token);
|
|
134
|
+
try {
|
|
135
|
+
const file = await fetchCodexCacheFile(authFetch, image);
|
|
136
|
+
await onImport(file);
|
|
137
|
+
} catch (err) {
|
|
138
|
+
setError(err instanceof Error ? err.message : "Could not import the generated image");
|
|
139
|
+
} finally {
|
|
140
|
+
setImportingToken(null);
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
const hasImages = images !== null && images.length > 0;
|
|
145
|
+
|
|
146
|
+
return (
|
|
147
|
+
<div
|
|
148
|
+
className="rounded border border-border bg-surface/60 p-2 space-y-2"
|
|
149
|
+
data-testid={`codex-picker-${cutId}`}
|
|
150
|
+
>
|
|
151
|
+
<div className="flex items-center justify-between">
|
|
152
|
+
<p className="text-[11px] font-medium text-foreground">Import a Codex-generated image</p>
|
|
153
|
+
<button
|
|
154
|
+
onClick={onClose}
|
|
155
|
+
data-testid={`codex-picker-close-${cutId}`}
|
|
156
|
+
className="text-[11px] text-muted hover:text-foreground"
|
|
157
|
+
>
|
|
158
|
+
Close
|
|
159
|
+
</button>
|
|
160
|
+
</div>
|
|
161
|
+
|
|
162
|
+
{hasImages && (
|
|
163
|
+
<div className="flex items-center gap-2">
|
|
164
|
+
<input
|
|
165
|
+
type="search"
|
|
166
|
+
value={query}
|
|
167
|
+
onChange={(e) => setQuery(e.target.value)}
|
|
168
|
+
placeholder="Filter by file name…"
|
|
169
|
+
data-testid={`codex-picker-search-${cutId}`}
|
|
170
|
+
className="min-w-0 flex-1 px-2 py-1 text-[11px] border border-border rounded bg-transparent focus:border-accent focus:outline-none"
|
|
171
|
+
/>
|
|
172
|
+
<span className="text-[10px] text-muted whitespace-nowrap" data-testid={`codex-picker-count-${cutId}`}>
|
|
173
|
+
{trimmedQuery ? `${filtered.length} of ${images!.length}` : `${images!.length} image${images!.length === 1 ? "" : "s"}`}
|
|
174
|
+
</span>
|
|
175
|
+
</div>
|
|
176
|
+
)}
|
|
177
|
+
|
|
178
|
+
{images === null && (
|
|
179
|
+
<p className="text-[11px] text-muted" data-testid={`codex-picker-loading-${cutId}`}>
|
|
180
|
+
Looking for generated images…
|
|
181
|
+
</p>
|
|
182
|
+
)}
|
|
183
|
+
|
|
184
|
+
{images !== null && images.length === 0 && (
|
|
185
|
+
<p className="text-[11px] text-muted" data-testid={`codex-picker-empty-${cutId}`}>
|
|
186
|
+
No generated images found in the Codex cache yet. Generate art in Codex, then reopen this
|
|
187
|
+
list — or use “Upload clean image” to pick a file.
|
|
188
|
+
</p>
|
|
189
|
+
)}
|
|
190
|
+
|
|
191
|
+
{hasImages && filtered.length === 0 && (
|
|
192
|
+
<p className="text-[11px] text-muted" data-testid={`codex-picker-no-match-${cutId}`}>
|
|
193
|
+
No generated images match “{query.trim()}”.
|
|
194
|
+
</p>
|
|
195
|
+
)}
|
|
196
|
+
|
|
197
|
+
{hasImages && filtered.length > 0 && (
|
|
198
|
+
<ul className="space-y-1 max-h-72 overflow-y-auto">
|
|
199
|
+
{filtered.map((img) => (
|
|
200
|
+
<li
|
|
201
|
+
key={img.token}
|
|
202
|
+
data-testid={`codex-image-${img.token}`}
|
|
203
|
+
className="flex items-center gap-2 rounded border border-border bg-background/40 p-1.5"
|
|
204
|
+
>
|
|
205
|
+
<CodexThumb image={img} authFetch={authFetch} />
|
|
206
|
+
<div className="min-w-0 flex-1">
|
|
207
|
+
<p className="text-[11px] text-foreground">
|
|
208
|
+
{formatRelativeTime(img.mtimeMs, now)} · {formatSize(img.size)}
|
|
209
|
+
</p>
|
|
210
|
+
<p className="truncate text-[10px] font-mono text-muted" title={img.name}>
|
|
211
|
+
{img.name}
|
|
212
|
+
</p>
|
|
213
|
+
</div>
|
|
214
|
+
<button
|
|
215
|
+
onClick={() => handlePick(img)}
|
|
216
|
+
disabled={importingToken !== null}
|
|
217
|
+
data-testid={`codex-import-${img.token}`}
|
|
218
|
+
className="px-2 py-1 text-[11px] border border-accent/30 text-accent rounded hover:bg-accent/5 disabled:opacity-50"
|
|
219
|
+
>
|
|
220
|
+
{importingToken === img.token ? "Importing…" : "Import to this cut"}
|
|
221
|
+
</button>
|
|
222
|
+
</li>
|
|
223
|
+
))}
|
|
224
|
+
</ul>
|
|
225
|
+
)}
|
|
226
|
+
|
|
227
|
+
{error && <p className="text-[11px] text-error">{error}</p>}
|
|
228
|
+
</div>
|
|
229
|
+
);
|
|
230
|
+
}
|