feat(webui): support document attachments with ingress safeguards (#4771)
* feat: support document attachments in webui * fix(webui): normalize document attachment MIME * refactor(webui): move attachment policy out of channel * fix(webui): reject oversized attachments before send * fix(webui): align Portuguese attachment errors * refactor(webui): separate ingress and transport limits * fix(webui): reject malformed attachment payloads
This commit is contained in:
@@ -17,7 +17,7 @@ import type {
|
||||
OutboundMedia,
|
||||
GoalStateWsPayload,
|
||||
ToolProgressEvent,
|
||||
UIImage,
|
||||
UIMediaAttachment,
|
||||
UIFileEdit,
|
||||
UIMessage,
|
||||
UITurnPhase,
|
||||
@@ -464,15 +464,15 @@ function findFileEditTraceIndex(
|
||||
* separately (e.g. via ``fetchWebuiThread``) since the server only replays
|
||||
* live events.
|
||||
*/
|
||||
/** Payload passed to ``send`` when the user attaches one or more images.
|
||||
/** Payload passed to ``send`` when the user attaches one or more files.
|
||||
*
|
||||
* ``media`` is handed to the wire client verbatim; ``preview`` powers the
|
||||
* optimistic user bubble (blob URLs so the preview appears before the server
|
||||
* acks the frame). Keeping the two separate lets the bubble re-use the local
|
||||
* blob URL even after the server persists the file under a different name. */
|
||||
export interface SendImage {
|
||||
* optimistic user bubble. Keeping the two separate lets the bubble re-use the
|
||||
* local data URL even after the server persists the file under a different
|
||||
* name. */
|
||||
export interface SendAttachment {
|
||||
media: OutboundMedia;
|
||||
preview: UIImage;
|
||||
preview: UIMediaAttachment;
|
||||
}
|
||||
|
||||
export interface SendOptions {
|
||||
@@ -520,7 +520,7 @@ export function useNanobotStream(
|
||||
runStartedAt: number | null;
|
||||
/** Latest sustained goal for this ``chatId`` (``goal_state`` WS events). */
|
||||
goalState: GoalStateWsPayload | undefined;
|
||||
send: (content: string, images?: SendImage[], options?: SendOptions) => void;
|
||||
send: (content: string, images?: SendAttachment[], options?: SendOptions) => void;
|
||||
transcribeAudio: (dataUrl: string, options?: { durationMs?: number }) => Promise<string>;
|
||||
stop: () => void;
|
||||
setMessages: React.Dispatch<React.SetStateAction<UIMessage[]>>;
|
||||
@@ -1135,12 +1135,12 @@ export function useNanobotStream(
|
||||
]);
|
||||
|
||||
const send = useCallback(
|
||||
(content: string, images?: SendImage[], options?: SendOptions) => {
|
||||
(content: string, images?: SendAttachment[], options?: SendOptions) => {
|
||||
if (!chatId) return;
|
||||
const hasImages = !!images && images.length > 0;
|
||||
// Text is optional when images are attached — the agent will still see
|
||||
// the image blocks via ``media`` paths.
|
||||
if (!hasImages && !content.trim()) return;
|
||||
const hasAttachments = !!images && images.length > 0;
|
||||
// Text is optional when files are attached — the agent will still see
|
||||
// them via ``media`` paths.
|
||||
if (!hasAttachments && !content.trim()) return;
|
||||
|
||||
const sideChannel = options?.sideChannel === true;
|
||||
const finalizeActiveTurn = options?.finalizeActiveTurn === true;
|
||||
@@ -1151,7 +1151,7 @@ export function useNanobotStream(
|
||||
}
|
||||
const turnId = crypto.randomUUID();
|
||||
if (sideChannel) sideChannelTurnIdsRef.current.add(turnId);
|
||||
const previews = hasImages ? images!.map((i) => i.preview) : undefined;
|
||||
const previews = hasAttachments ? images!.map((i) => i.preview) : undefined;
|
||||
setMessages((prev) => {
|
||||
if (!sideChannel || finalizeActiveTurn) {
|
||||
buffer.current = null;
|
||||
@@ -1171,14 +1171,14 @@ export function useNanobotStream(
|
||||
turnPhase: "user",
|
||||
turnSeq: 0,
|
||||
createdAt: Date.now(),
|
||||
...(previews ? { images: previews } : {}),
|
||||
...(previews ? { media: previews } : {}),
|
||||
...(options?.cliApps?.length ? { cliApps: options.cliApps } : {}),
|
||||
...(options?.mcpPresets?.length ? { mcpPresets: options.mcpPresets } : {}),
|
||||
},
|
||||
];
|
||||
});
|
||||
if (!sideChannel) setIsStreaming(true);
|
||||
const wireMedia = hasImages ? images!.map((i) => i.media) : undefined;
|
||||
const wireMedia = hasAttachments ? images!.map((i) => i.media) : undefined;
|
||||
const wireOptions = { ...options, turnId };
|
||||
delete wireOptions.sideChannel;
|
||||
delete wireOptions.finalizeActiveTurn;
|
||||
|
||||
Reference in New Issue
Block a user