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:
@@ -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}
|
||||||
|
|||||||
Reference in New Issue
Block a user