mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-28 05:16:23 -06:00
fix(ui): attach pasted data image text (#85392)
This commit is contained in:
committed by
GitHub
parent
d9c6c5f600
commit
a03a8d91f6
@@ -66,6 +66,7 @@ Docs: https://docs.openclaw.ai
|
||||
- CLI/update: keep managed Gateway service stop/restart status lines out of `openclaw update --json` stdout so package-update automation can parse the JSON payload.
|
||||
- Plugins: resolve OpenClaw plugin SDK subpaths for native external plugin runtimes without mutating package installs or broadening process-wide module resolution.
|
||||
- Agents/OpenAI: preserve Responses and Chat Completions `reasoning_tokens` usage metadata without double-counting it in aggregate output tokens. (#85319)
|
||||
- Control UI/chat: convert pasted `data:image/...;base64,...` clipboard text into an image attachment instead of dumping the payload into the composer. Fixes #62604. Thanks @cpwilhelmi.
|
||||
- Providers/Gemini: strip fractional seconds from web-search time range filters so Gemini accepts freshness-bound search requests. (#85071) Thanks @Noerr.
|
||||
- OpenAI Codex: preserve image input support for sparse `openai-codex/gpt-5.5` catalog rows. (#85095) Thanks @sercada.
|
||||
- Plugins/discovery: strip `-plugin` package suffixes when deriving plugin id hints so package names line up with manifest ids. (#85170) Thanks @JulyanXu.
|
||||
|
||||
@@ -1039,6 +1039,35 @@ describe("chat slash menu accessibility", () => {
|
||||
});
|
||||
|
||||
describe("chat attachment picker", () => {
|
||||
it("converts pasted data image text into an attachment", () => {
|
||||
const onAttachmentsChange = vi.fn();
|
||||
const container = renderChatView({ onAttachmentsChange });
|
||||
const textarea = requireElement(
|
||||
container,
|
||||
".agent-chat__composer-combobox > textarea",
|
||||
"composer textarea",
|
||||
);
|
||||
const base64 = btoa("png");
|
||||
const dataUrl = ` data:image/PNG;base64,${base64.slice(0, 2)}\n${base64.slice(2)} `;
|
||||
const event = new Event("paste", { bubbles: true, cancelable: true });
|
||||
Object.defineProperty(event, "clipboardData", {
|
||||
value: {
|
||||
items: { length: 0 },
|
||||
getData: (type: string) => (type === "text/plain" ? dataUrl : ""),
|
||||
},
|
||||
});
|
||||
|
||||
const allowed = textarea.dispatchEvent(event);
|
||||
|
||||
expect(allowed).toBe(false);
|
||||
const attachments = requireFirstAttachmentsChange(onAttachmentsChange);
|
||||
expect(attachments).toHaveLength(1);
|
||||
expect(attachments[0]?.fileName).toBe("pasted-image.png");
|
||||
expect(attachments[0]?.mimeType).toBe("image/png");
|
||||
expect(attachments[0]?.sizeBytes).toBe(3);
|
||||
expect(getChatAttachmentDataUrl(attachments[0])).toBe(`data:image/png;base64,${base64}`);
|
||||
});
|
||||
|
||||
it("accepts and previews non-video file attachments", async () => {
|
||||
const onAttachmentsChange = vi.fn();
|
||||
const container = renderChatView({ onAttachmentsChange });
|
||||
|
||||
@@ -453,6 +453,32 @@ function chatAttachmentFromFile(file: File, dataUrl: string): ChatAttachment {
|
||||
return registerChatAttachmentPayload({ attachment, dataUrl, file });
|
||||
}
|
||||
|
||||
function dataImageClipboardFile(dataUrl: string): { file: File; dataUrl: string } | null {
|
||||
const match = /^\s*data:(image\/[a-z0-9.+-]+);base64,([a-z0-9+/=\s]+)\s*$/i.exec(dataUrl);
|
||||
if (!match) {
|
||||
return null;
|
||||
}
|
||||
const mimeType = match[1].toLowerCase();
|
||||
if (!isSupportedChatAttachmentFile({ name: "pasted-image", type: mimeType })) {
|
||||
return null;
|
||||
}
|
||||
const base64 = match[2].replace(/\s+/g, "");
|
||||
try {
|
||||
const binary = atob(base64);
|
||||
const bytes = new Uint8Array(binary.length);
|
||||
for (let i = 0; i < binary.length; i++) {
|
||||
bytes[i] = binary.charCodeAt(i);
|
||||
}
|
||||
const extension = mimeType.split("/")[1]?.replace(/[^a-z0-9.+-]/gi, "") || "png";
|
||||
return {
|
||||
file: new File([bytes], `pasted-image.${extension}`, { type: mimeType }),
|
||||
dataUrl: `data:${mimeType};base64,${base64}`,
|
||||
};
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function isImageAttachment(att: ChatAttachment): boolean {
|
||||
return att.mimeType.startsWith("image/");
|
||||
}
|
||||
@@ -470,6 +496,16 @@ function handlePaste(e: ClipboardEvent, props: ChatProps) {
|
||||
}
|
||||
}
|
||||
if (imageItems.length === 0) {
|
||||
const text = e.clipboardData?.getData("text/plain");
|
||||
const pasted = text ? dataImageClipboardFile(text) : null;
|
||||
if (!pasted) {
|
||||
return;
|
||||
}
|
||||
e.preventDefault();
|
||||
props.onAttachmentsChange([
|
||||
...(props.attachments ?? []),
|
||||
chatAttachmentFromFile(pasted.file, pasted.dataUrl),
|
||||
]);
|
||||
return;
|
||||
}
|
||||
e.preventDefault();
|
||||
|
||||
Reference in New Issue
Block a user