Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 0 additions & 11 deletions containers/image-converter/Dockerfile

This file was deleted.

170 changes: 0 additions & 170 deletions containers/image-converter/server.mjs

This file was deleted.

67 changes: 37 additions & 30 deletions src/features/workspaces/components/ai-chat/AiChatAttachmentItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
AttachmentTrigger,
} from "#/components/ui/attachment";
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "#/components/ui/dialog";
import { Skeleton } from "#/components/ui/skeleton";
import { Spinner } from "#/components/ui/spinner";
import type {
AttachmentData,
Expand Down Expand Up @@ -51,7 +52,7 @@ export function AiChatAttachmentItem({
data: AttachmentData;
onRemove?: () => void;
}) {
if (isPreviewableImageAttachment(data)) {
if (isImageAttachment(data)) {
return <AiChatImageAttachment data={data} onRemove={onRemove} />;
}

Expand All @@ -64,57 +65,63 @@ export function AiChatAttachmentItem({
);
}

function isPreviewableImageAttachment(
data: AttachmentData,
): data is FileAttachmentData & { status: "ready"; url: string } {
return (
data.type === "file" &&
data.status === "ready" &&
getMediaCategory(data) === "image" &&
Boolean(data.url)
);
function isImageAttachment(data: AttachmentData): data is FileAttachmentData {
return data.type === "file" && getMediaCategory(data) === "image";
}

function AiChatImageAttachment({
data,
onRemove,
}: {
data: FileAttachmentData & { status: "ready"; url: string };
data: FileAttachmentData;
onRemove?: () => void;
}) {
const [isOpen, setIsOpen] = useState(false);
const label = getAttachmentLabel(data);
const imageUrl = data.status === "ready" ? data.url : undefined;

return (
<>
<Attachment
className="cursor-zoom-in focus-within:ring-2"
className={imageUrl ? "cursor-zoom-in focus-within:ring-2" : undefined}
orientation="vertical"
size="default"
state={getAttachmentState(data)}
>
<AttachmentMedia variant="image">
<img
alt={label}
className="size-full object-cover"
height={96}
src={data.url}
width={96}
/>
{imageUrl ? (
<img
alt={label}
className="size-full object-cover"
height={96}
src={imageUrl}
width={96}
/>
) : (
<>
<Skeleton aria-hidden="true" className="size-full rounded-none bg-foreground/10" />
<span className="sr-only">Preparing {label}</span>
Comment on lines +81 to +103

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Handle ready images that have no URL.

Line 81 allows the valid FileAttachmentData state { status: "ready", url: undefined }. It is then marked done but permanently renders “Preparing …”. Require url for the ready variant at the attachment contract, or render an unavailable/error state for this invariant violation.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/features/workspaces/components/ai-chat/AiChatAttachmentItem.tsx` around
lines 81 - 103, Update AiChatAttachmentItem’s ready-image handling so a ready
attachment without a URL cannot remain in the done/loading presentation. Enforce
a defined url in the FileAttachmentData ready contract, or explicitly map this
invalid state to the unavailable/error attachment state and corresponding UI
instead of rendering “Preparing …”.

</>
)}
</AttachmentMedia>
<AttachmentTrigger aria-label={`Preview ${label}`} onClick={() => setIsOpen(true)} />
{imageUrl ? (
<AttachmentTrigger aria-label={`Preview ${label}`} onClick={() => setIsOpen(true)} />
) : null}
<AiChatAttachmentRemoveAction data={data} onRemove={onRemove} />
</Attachment>

<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogContent className="max-w-[min(96vw,900px)] gap-4 p-4 sm:max-w-4xl">
<DialogHeader className="pr-8">
<DialogTitle className="truncate text-base">{label}</DialogTitle>
</DialogHeader>
<div className="flex max-h-[78vh] min-h-0 items-center justify-center overflow-hidden rounded-lg bg-muted/40">
<img alt={label} className="max-h-[78vh] max-w-full object-contain" src={data.url} />
</div>
</DialogContent>
</Dialog>
{imageUrl ? (
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogContent className="max-w-[min(96vw,900px)] gap-4 p-4 sm:max-w-4xl">
<DialogHeader className="pr-8">
<DialogTitle className="truncate text-base">{label}</DialogTitle>
</DialogHeader>
<div className="flex max-h-[78vh] min-h-0 items-center justify-center overflow-hidden rounded-lg bg-muted/40">
<img alt={label} className="max-h-[78vh] max-w-full object-contain" src={imageUrl} />
</div>
</DialogContent>
</Dialog>
) : null}
</>
);
}
Expand Down
11 changes: 9 additions & 2 deletions src/features/workspaces/conversion/errors.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
export type WorkspaceFileConversionFailure = "conversion_failed" | "output_too_large";

export class WorkspaceFileConversionError extends Error {
readonly userMessage: string;

constructor(message: string, userMessage: string) {
super(message);
constructor(
message: string,
userMessage: string,
readonly failure: WorkspaceFileConversionFailure = "conversion_failed",
options?: ErrorOptions,
) {
super(message, options);
this.name = "WorkspaceFileConversionError";
this.userMessage = userMessage;
}
Expand Down
Loading