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, ChevronDownIcon,
ChevronUpIcon, ChevronUpIcon,
EyeOffIcon, EyeOffIcon,
PencilIcon,
PlusIcon, PlusIcon,
PollIcon, PollIcon,
ReplyIcon, ReplyIcon,
@@ -25,6 +26,7 @@ import {
SpinnerIcon, SpinnerIcon,
XIcon, XIcon,
} from '../components/icons'; } from '../components/icons';
import { ImageAnnotator } from '../components/ImageAnnotator';
import { InCallPanel } from '../components/InCallPanel'; import { InCallPanel } from '../components/InCallPanel';
import { IncomingCallPanel } from '../components/IncomingCallPanel'; import { IncomingCallPanel } from '../components/IncomingCallPanel';
import { CallPreviewPanel } from '../components/CallPreviewPanel'; import { CallPreviewPanel } from '../components/CallPreviewPanel';
@@ -154,6 +156,7 @@ export function ConversationPage() {
const [sendError, setSendError] = useState<string | null>(null); const [sendError, setSendError] = useState<string | null>(null);
const [stickToBottom, setStickToBottom] = useState(true); const [stickToBottom, setStickToBottom] = useState(true);
const [attachments, setAttachments] = useState<File[]>([]); const [attachments, setAttachments] = useState<File[]>([]);
const [annotatingIndex, setAnnotatingIndex] = useState<number | null>(null);
const [infoPanelOpen, setInfoPanelOpen] = useState(false); const [infoPanelOpen, setInfoPanelOpen] = useState(false);
const [mediaDrawerOpen, setMediaDrawerOpen] = useState(false); const [mediaDrawerOpen, setMediaDrawerOpen] = useState(false);
const [pollDialogOpen, setPollDialogOpen] = useState(false); const [pollDialogOpen, setPollDialogOpen] = useState(false);
@@ -917,6 +920,9 @@ export function ConversationPage() {
key={idx} key={idx}
file={file} file={file}
onRemove={() => setAttachments((prev) => prev.filter((_, i) => i !== idx))} onRemove={() => setAttachments((prev) => prev.filter((_, i) => i !== idx))}
{...(file.type.startsWith('image/')
? { onEdit: () => setAnnotatingIndex(idx) }
: {})}
/> />
))} ))}
</div> </div>
@@ -1163,6 +1169,17 @@ export function ConversationPage() {
}} }}
onUnpin={(messageId) => void handleTogglePin(messageId)} 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> </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 isImage = file.type.startsWith('image/');
const [url, setUrl] = useState<string | null>(null); const [url, setUrl] = useState<string | null>(null);
useEffect(() => { useEffect(() => {
@@ -1400,7 +1425,7 @@ function AttachmentPreview({ file, onRemove }: { file: File; onRemove: () => voi
return () => URL.revokeObjectURL(u); return () => URL.revokeObjectURL(u);
}, [file, isImage]); }, [file, isImage]);
return ( 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 ? ( {isImage && url ? (
<img src={url} alt={file.name} className="block h-20 w-20 object-cover" /> <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> <span className="text-fg-muted">{(file.size / 1024).toFixed(0)} KB</span>
</div> </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 <button
type="button" type="button"
onClick={onRemove} onClick={onRemove}