Skip to content

Commit aa7a123

Browse files
committed
more work on comic breakup, models
1 parent a4e5616 commit aa7a123

56 files changed

Lines changed: 3304 additions & 898 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

AGENTS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ Use the chrome-devtools in a terminal to drive and probe the running app.
4545
- Composed from repeated **ImagePanel** instances to normalize uploads, drag/drop, and clearing.
4646
4. **Supporting Panels**
4747
- `HistoryStrip` (and `ImageSlot` thumbnails) keep recent generations accessible for drag-over to the main panels.
48-
- `CapabilityPanel`, `ToolPanel`, and files under components/tools describe the registry-driven tool controls.
48+
- `ToolModelPicker`, `ToolPanel`, and files under components/tools describe the registry-driven tool controls (each tool picks its own model + reasoning).
4949
- `ReferenceImagesPanel`, `ImageInfoPanel`, and `ImageToolsPanel` subcomponents round out auxiliary UI.
5050

5151
## UI

components/CapabilityPanel.tsx

Lines changed: 0 additions & 106 deletions
This file was deleted.

components/ImageInfoPanel.tsx

Lines changed: 77 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -59,12 +59,18 @@ const resolveStyleSummary = (item: ImageRecord): string | null => {
5959
return `${style.name} (${styleId})`;
6060
};
6161

62+
// Prompts longer than this are collapsed by default so the info panel doesn't
63+
// grow taller than the viewport and run off the screen.
64+
const PROMPT_COLLAPSE_THRESHOLD = 280;
65+
6266
export const ImageInfoPanel: React.FC<ImageInfoPanelProps> = ({ item }) => {
6367
const tool = TOOLS.find((t) => t.id === item.toolId);
6468
const promptContent =
6569
item.promptUsed && item.promptUsed.length ? item.promptUsed : "Prompt unavailable.";
70+
const isPromptLong = promptContent.length > PROMPT_COLLAPSE_THRESHOLD;
6671

6772
const [promptCopied, setPromptCopied] = React.useState(false);
73+
const [promptExpanded, setPromptExpanded] = React.useState(false);
6874
const copyResetTimeoutRef = React.useRef<number | null>(null);
6975

7076
React.useEffect(() => {
@@ -155,6 +161,37 @@ export const ImageInfoPanel: React.FC<ImageInfoPanelProps> = ({ item }) => {
155161
gap: "6px",
156162
}}
157163
>
164+
{item.caption && item.caption.trim().length > 0 && (
165+
<div
166+
style={{
167+
marginBottom: 8,
168+
paddingBottom: 8,
169+
borderBottom: `1px solid ${theme.colors.border}`,
170+
}}
171+
>
172+
<span
173+
style={{
174+
display: "block",
175+
marginBottom: 4,
176+
color: theme.colors.textMuted,
177+
}}
178+
>
179+
Associated text:
180+
</span>
181+
<div
182+
data-testid="image-caption"
183+
style={{
184+
color: theme.colors.textPrimary,
185+
fontSize: "12px",
186+
whiteSpace: "pre-wrap",
187+
lineHeight: 1.4,
188+
}}
189+
>
190+
{item.caption}
191+
</div>
192+
</div>
193+
)}
194+
158195
<div style={rowStyle}>
159196
<span style={{ color: theme.colors.textMuted }}>Tool:</span>
160197
<span
@@ -218,24 +255,53 @@ export const ImageInfoPanel: React.FC<ImageInfoPanelProps> = ({ item }) => {
218255
}}
219256
>
220257
<span style={{ display: "block" }}>Full Prompt:</span>
221-
<Tooltip title={promptCopied ? "Copied" : "Copy prompt"}>
222-
<IconButton
223-
aria-label="Copy full prompt"
224-
onClick={handleCopyPrompt}
225-
size="small"
226-
data-testid="copy-full-prompt"
227-
sx={{ color: theme.colors.textMuted }}
228-
>
229-
<ContentCopyIcon fontSize="inherit" />
230-
</IconButton>
231-
</Tooltip>
258+
<div style={{ display: "flex", alignItems: "center", gap: 4 }}>
259+
{isPromptLong && (
260+
<button
261+
type="button"
262+
onClick={() => setPromptExpanded((prev) => !prev)}
263+
data-testid="toggle-full-prompt"
264+
aria-expanded={promptExpanded}
265+
style={{
266+
background: "none",
267+
border: "none",
268+
padding: 0,
269+
cursor: "pointer",
270+
color: theme.colors.textSecondary,
271+
fontSize: "11px",
272+
textDecoration: "underline",
273+
}}
274+
>
275+
{promptExpanded ? "Show less" : "Show more"}
276+
</button>
277+
)}
278+
<Tooltip title={promptCopied ? "Copied" : "Copy prompt"}>
279+
<IconButton
280+
aria-label="Copy full prompt"
281+
onClick={handleCopyPrompt}
282+
size="small"
283+
data-testid="copy-full-prompt"
284+
sx={{ color: theme.colors.textMuted }}
285+
>
286+
<ContentCopyIcon fontSize="inherit" />
287+
</IconButton>
288+
</Tooltip>
289+
</div>
232290
</div>
233291
<div
234292
style={{
235293
color: theme.colors.textPrimary,
236294
fontSize: "11px",
237295
whiteSpace: "pre-wrap",
238296
lineHeight: 1.4,
297+
...(isPromptLong && !promptExpanded
298+
? {
299+
maxHeight: "7.5em",
300+
overflow: "hidden",
301+
maskImage: "linear-gradient(to bottom, black 60%, transparent 100%)",
302+
WebkitMaskImage: "linear-gradient(to bottom, black 60%, transparent 100%)",
303+
}
304+
: {}),
239305
}}
240306
>
241307
{promptContent}

components/ImageSlot.tsx

Lines changed: 35 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -23,16 +23,11 @@ import {
2323
setInternalImageDragData,
2424
} from "./dragConstants";
2525
import { recordDragDelayMs } from "./dndDragState";
26-
import {
27-
getTypeFromFileName,
28-
getTypeFromMime,
29-
handleCopy as copyImageToClipboard,
30-
handlePaste as pasteImageFromClipboard,
31-
} from "../lib/clipboardUtils";
26+
import { getTypeFromFileName, handlePaste as pasteImageFromClipboard } from "../lib/clipboardUtils";
3227
import { getImageFileExtensionFromMimeType, getMimeTypeFromUrl } from "../lib/imageUtils";
3328
import { hasImageFilePayload, getImageFileFromDataTransfer } from "../lib/dragUtils";
34-
import { TOOLS } from "./tools/tools-registry";
35-
import { getModelNameById } from "../lib/modelsCatalog";
29+
import { copyImageRecordWithFeedback } from "./copyImageRecordToClipboard";
30+
import { subscribeToImageCopyFeedback } from "../lib/imageCopyFeedback";
3631

3732
let transparentDragImage: HTMLImageElement | null = null;
3833
const getTransparentDragImage = (): HTMLImageElement | null => {
@@ -347,6 +342,35 @@ export const ImageSlot: React.FC<ImageSlotProps> = ({
347342
}
348343
}, [image, disabled]);
349344

345+
// Surface the "Copied!" status badge whenever this image is copied — whether
346+
// from this slot's own copy button or from the global Ctrl/Cmd+C shortcut.
347+
const imageId = image?.id;
348+
React.useEffect(() => {
349+
if (!imageId) return;
350+
let resetTimeout: number | null = null;
351+
const clearReset = () => {
352+
if (resetTimeout !== null) {
353+
window.clearTimeout(resetTimeout);
354+
resetTimeout = null;
355+
}
356+
};
357+
358+
const unsubscribe = subscribeToImageCopyFeedback(imageId, (status) => {
359+
clearReset();
360+
setThumbnailStatus(status);
361+
if (status === "copied") {
362+
resetTimeout = window.setTimeout(() => setThumbnailStatus("idle"), 1500);
363+
} else if (status === "copyError") {
364+
resetTimeout = window.setTimeout(() => setThumbnailStatus("idle"), 3000);
365+
}
366+
});
367+
368+
return () => {
369+
unsubscribe();
370+
clearReset();
371+
};
372+
}, [imageId]);
373+
350374
const openFilePicker = () => {
351375
if (!onUpload || disabled) return;
352376
fileInputRef.current?.click();
@@ -377,37 +401,9 @@ export const ImageSlot: React.FC<ImageSlotProps> = ({
377401

378402
const handleCopy = async () => {
379403
if (!image || !mergedControls.copy) return;
380-
381-
try {
382-
setThumbnailStatus("copying");
383-
384-
const tool = TOOLS.find((t) => t.id === image.toolId) || null;
385-
const isNewImageTool = tool?.editImage === false;
386-
const modelId = (image.model || "").trim();
387-
const modelName = getModelNameById(modelId) || modelId;
388-
const reasoningLevel = (image.reasoningLevel || "").trim();
389-
const pngMetadata = modelId
390-
? isNewImageTool
391-
? {
392-
IllustratorModel: modelName,
393-
IllustratorModelId: modelId,
394-
IllustratorReasoningLevel: reasoningLevel,
395-
}
396-
: {
397-
EditorModel: modelName,
398-
EditorModelId: modelId,
399-
EditorReasoningLevel: reasoningLevel,
400-
}
401-
: undefined;
402-
403-
await copyImageToClipboard(image.imageData, pngMetadata);
404-
setThumbnailStatus("copied");
405-
setTimeout(() => setThumbnailStatus("idle"), 1500);
406-
} catch (err) {
407-
console.error("Failed to copy image:", err);
408-
setThumbnailStatus("copyError");
409-
setTimeout(() => setThumbnailStatus("idle"), 3000);
410-
}
404+
// Status badge ("Copying..." → "Copied!") is driven by the copy-feedback
405+
// subscription below, so a copy started here OR via Ctrl+C looks identical.
406+
await copyImageRecordWithFeedback(image);
411407
};
412408

413409
const handleDownload = () => {

components/ImageSlotActions.tsx

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -450,9 +450,15 @@ export const ImageSlotActions = React.forwardRef<ImageSlotActionsHandle, ImageSl
450450
if (!image || disabled || orderedActions.length === 0) return null;
451451

452452
const removeAction = orderedActions.find((action) => action.key === "remove");
453-
const overflowActions = orderedActions.filter((action) => action.key !== "remove");
453+
// Surface copy as a dedicated, always-on-hover button (rather than burying
454+
// it in the "..." overflow) so a single hover-click copies the thumbnail.
455+
const copyAction = orderedActions.find((action) => action.key === "copy");
456+
const overflowActions = orderedActions.filter(
457+
(action) => action.key !== "remove" && action.key !== "copy",
458+
);
454459

455460
const showRemove = isHovered;
461+
const showCopy = isHovered;
456462
const showMoreTrigger =
457463
overflowActions.length > 0 && ((isHovered && isThumbMoreReady) || isThumbOverflowOpen);
458464

@@ -477,6 +483,25 @@ export const ImageSlotActions = React.forwardRef<ImageSlotActionsHandle, ImageSl
477483
</div>
478484
) : null}
479485

486+
{copyAction ? (
487+
<div
488+
style={{
489+
position: "absolute",
490+
bottom: cornerOffset,
491+
right: cornerOffset,
492+
display: "flex",
493+
flexDirection: "column",
494+
gap: 4,
495+
zIndex: 20,
496+
opacity: showCopy ? 1 : 0,
497+
pointerEvents: disabled ? "none" : showCopy ? "auto" : "none",
498+
transition: "opacity 120ms ease",
499+
}}
500+
>
501+
{renderActionButton(copyAction)}
502+
</div>
503+
) : null}
504+
480505
{overflowActions.length > 0 ? (
481506
<div
482507
style={{

0 commit comments

Comments
 (0)