feat(P4A.T4): wire ImageAnnotator into composer attachment preview

Hover an image attachment in the composer to reveal a pencil-edit button;
clicking it opens ImageAnnotator and Save replaces the File in attachments[].

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
byGalax
2026-05-16 19:48:57 +02:00
parent d623a59c87
commit 11a9c8b173
+38 -2
View File
@@ -16,6 +16,7 @@ import {
ChevronDownIcon,
ChevronUpIcon,
EyeOffIcon,
PencilIcon,
PlusIcon,
PollIcon,
ReplyIcon,
@@ -25,6 +26,7 @@ import {
SpinnerIcon,
XIcon,
} from '../components/icons';
import { ImageAnnotator } from '../components/ImageAnnotator';
import { InCallPanel } from '../components/InCallPanel';
import { IncomingCallPanel } from '../components/IncomingCallPanel';
import { CallPreviewPanel } from '../components/CallPreviewPanel';
@@ -154,6 +156,7 @@ export function ConversationPage() {
const [sendError, setSendError] = useState<string | null>(null);
const [stickToBottom, setStickToBottom] = useState(true);
const [attachments, setAttachments] = useState<File[]>([]);
const [annotatingIndex, setAnnotatingIndex] = useState<number | null>(null);
const [infoPanelOpen, setInfoPanelOpen] = useState(false);
const [mediaDrawerOpen, setMediaDrawerOpen] = useState(false);
const [pollDialogOpen, setPollDialogOpen] = useState(false);
@@ -917,6 +920,9 @@ export function ConversationPage() {
key={idx}
file={file}
onRemove={() => setAttachments((prev) => prev.filter((_, i) => i !== idx))}
{...(file.type.startsWith('image/')
? { onEdit: () => setAnnotatingIndex(idx) }
: {})}
/>
))}
</div>
@@ -1163,6 +1169,17 @@ export function ConversationPage() {
}}
onUnpin={(messageId) => void handleTogglePin(messageId)}
/>
{annotatingIndex !== null && attachments[annotatingIndex] && (
<ImageAnnotator
file={attachments[annotatingIndex]!}
onCancel={() => setAnnotatingIndex(null)}
onSave={(next) => {
setAttachments((prev) => prev.map((f, i) => (i === annotatingIndex ? next : f)));
setAnnotatingIndex(null);
}}
/>
)}
</div>
);
}
@@ -1390,7 +1407,15 @@ function Banner({ children }: { children: React.ReactNode }) {
);
}
function AttachmentPreview({ file, onRemove }: { file: File; onRemove: () => void }) {
function AttachmentPreview({
file,
onRemove,
onEdit,
}: {
file: File;
onRemove: () => void;
onEdit?: () => void;
}) {
const isImage = file.type.startsWith('image/');
const [url, setUrl] = useState<string | null>(null);
useEffect(() => {
@@ -1400,7 +1425,7 @@ function AttachmentPreview({ file, onRemove }: { file: File; onRemove: () => voi
return () => URL.revokeObjectURL(u);
}, [file, isImage]);
return (
<div className="relative overflow-hidden rounded-lg border border-line bg-surface-2 dark:bg-[#2b2d31]">
<div className="group relative overflow-hidden rounded-lg border border-line bg-surface-2 dark:bg-[#2b2d31]">
{isImage && url ? (
<img src={url} alt={file.name} className="block h-20 w-20 object-cover" />
) : (
@@ -1412,6 +1437,17 @@ function AttachmentPreview({ file, onRemove }: { file: File; onRemove: () => voi
<span className="text-fg-muted">{(file.size / 1024).toFixed(0)} KB</span>
</div>
)}
{isImage && onEdit && (
<button
type="button"
onClick={onEdit}
aria-label="Bearbeiten"
title="Bearbeiten"
className="absolute bottom-1 left-1 flex h-5 w-5 cursor-pointer items-center justify-center rounded-full bg-black/70 text-white opacity-0 transition group-hover:opacity-100 hover:bg-accent/80"
>
<PencilIcon className="h-3 w-3" />
</button>
)}
<button
type="button"
onClick={onRemove}