Commit 362f9163 authored by edy's avatar edy

fix(ui): restrict home chat attachments to images

parent 105938fc
...@@ -91,7 +91,6 @@ import { ...@@ -91,7 +91,6 @@ import {
toUiChatMessage toUiChatMessage
} from "./lib/chat-utils"; } from "./lib/chat-utils";
import { import {
COMPOSER_ATTACHMENT_ACCEPT,
EMPTY_SESSION_ID, EMPTY_SESSION_ID,
HOME_CHAT_PROJECT_ID, HOME_CHAT_PROJECT_ID,
SUCCESS_NOTICE_TIMEOUT_MS SUCCESS_NOTICE_TIMEOUT_MS
...@@ -322,6 +321,7 @@ export default function App() { ...@@ -322,6 +321,7 @@ export default function App() {
}); });
const { const {
attachmentInputRef, attachmentInputRef,
attachmentAccept,
composerAttachments, composerAttachments,
isComposerDragOver, isComposerDragOver,
clearComposerAttachment, clearComposerAttachment,
...@@ -332,7 +332,10 @@ export default function App() { ...@@ -332,7 +332,10 @@ export default function App() {
handleComposerDragOver, handleComposerDragOver,
handleComposerDragLeave, handleComposerDragLeave,
handleComposerDrop handleComposerDrop
} = useComposerAttachments({ setErrorText }); } = useComposerAttachments({
mode: viewMode === "chat" ? "image-only" : "all-supported",
setErrorText
});
const { const {
isComposerResizeActive, isComposerResizeActive,
composerShellStyle, composerShellStyle,
...@@ -1222,7 +1225,7 @@ export default function App() { ...@@ -1222,7 +1225,7 @@ export default function App() {
isComposerDragOver, isComposerDragOver,
isComposerResizeActive, isComposerResizeActive,
composerShellStyle, composerShellStyle,
attachmentAccept: COMPOSER_ATTACHMENT_ACCEPT, attachmentAccept,
attachments: composerAttachments, attachments: composerAttachments,
composerPlaceholder, composerPlaceholder,
sendButtonLabel, sendButtonLabel,
......
import { useRef, useState } from "react"; import { useEffect, useRef, useState } from "react";
import type { ChangeEvent, DragEvent as ReactDragEvent } from "react"; import type { ChangeEvent, DragEvent as ReactDragEvent } from "react";
import type { ChatAttachment } from "@qjclaw/shared-types"; import type { ChatAttachment } from "@qjclaw/shared-types";
import { import {
COMPOSER_ATTACHMENT_ACCEPT,
IMAGE_ATTACHMENT_EXTENSIONS, IMAGE_ATTACHMENT_EXTENSIONS,
SUPPORTED_ATTACHMENT_EXTENSIONS SUPPORTED_ATTACHMENT_EXTENSIONS
} from "../../lib/constants"; } from "../../lib/constants";
import { desktopApi } from "../../lib/desktop-api"; import { desktopApi } from "../../lib/desktop-api";
export type ComposerAttachmentMode = "image-only" | "all-supported";
interface UseComposerAttachmentsOptions { interface UseComposerAttachmentsOptions {
mode: ComposerAttachmentMode;
setErrorText: (value: string) => void; setErrorText: (value: string) => void;
} }
export function useComposerAttachments({ setErrorText }: UseComposerAttachmentsOptions) { const COMPOSER_IMAGE_ATTACHMENT_ACCEPT = [...IMAGE_ATTACHMENT_EXTENSIONS].join(",");
const HOME_IMAGE_ATTACHMENT_ERROR = "首页对话仅支持上传图片附件。";
const SUPPORTED_ATTACHMENT_ERROR = "Supported attachments: images, MP3, PDF, PPT, Excel, Word, CSV, TXT, Markdown, and JSON.";
function resolveExtension(name: string, localPath: string): string {
const candidate = name || localPath;
const index = candidate.lastIndexOf(".");
return index >= 0 ? candidate.slice(index).toLowerCase() : "";
}
function isImageAttachment(attachment: ChatAttachment): boolean {
const mimeType = attachment.mimeType?.trim().toLowerCase() ?? "";
const extension = resolveExtension(attachment.name, attachment.localPath);
return attachment.kind === "image" || mimeType.startsWith("image/") || IMAGE_ATTACHMENT_EXTENSIONS.has(extension);
}
export function useComposerAttachments({ mode, setErrorText }: UseComposerAttachmentsOptions) {
const [composerAttachments, setComposerAttachments] = useState<ChatAttachment[]>([]); const [composerAttachments, setComposerAttachments] = useState<ChatAttachment[]>([]);
const [isComposerDragOver, setIsComposerDragOver] = useState(false); const [isComposerDragOver, setIsComposerDragOver] = useState(false);
const attachmentInputRef = useRef<HTMLInputElement | null>(null); const attachmentInputRef = useRef<HTMLInputElement | null>(null);
const composerDragDepthRef = useRef(0); const composerDragDepthRef = useRef(0);
const composerModeRef = useRef(mode);
const attachmentAccept = mode === "image-only" ? COMPOSER_IMAGE_ATTACHMENT_ACCEPT : COMPOSER_ATTACHMENT_ACCEPT;
useEffect(() => {
composerModeRef.current = mode;
}, [mode]);
useEffect(() => {
if (mode !== "image-only") {
return;
}
const filtered = composerAttachments.filter((attachment) => isImageAttachment(attachment));
if (filtered.length === composerAttachments.length) {
return;
}
setErrorText(HOME_IMAGE_ATTACHMENT_ERROR);
setComposerAttachments(filtered);
}, [composerAttachments, mode, setErrorText]);
function resolveComposerAttachmentKind(file: File, localPath: string): ChatAttachment["kind"] | null { function resolveComposerAttachmentKind(file: File, localPath: string): ChatAttachment["kind"] | null {
if (file.type.startsWith("image/")) { if (file.type.startsWith("image/")) {
return "image"; return "image";
} }
const extension = (file.name ? file.name.slice(file.name.lastIndexOf(".")) : localPath.slice(localPath.lastIndexOf("."))).toLowerCase(); const extension = resolveExtension(file.name, localPath);
if (IMAGE_ATTACHMENT_EXTENSIONS.has(extension)) { if (IMAGE_ATTACHMENT_EXTENSIONS.has(extension)) {
return "image"; return "image";
} }
...@@ -61,15 +101,20 @@ export function useComposerAttachments({ setErrorText }: UseComposerAttachmentsO ...@@ -61,15 +101,20 @@ export function useComposerAttachments({ setErrorText }: UseComposerAttachmentsO
} }
function acceptComposerAttachmentFile(file: File) { function acceptComposerAttachmentFile(file: File) {
const kind = resolveComposerAttachmentKind(file, file.name);
if (mode === "image-only" && kind !== "image") {
setErrorText(HOME_IMAGE_ATTACHMENT_ERROR);
return;
}
const localPath = (file as File & { path?: string }).path?.trim(); const localPath = (file as File & { path?: string }).path?.trim();
if (!localPath) { if (!localPath) {
setErrorText("The desktop client did not provide a local file path, so this attachment cannot be sent into the project workspace."); setErrorText("The desktop client did not provide a local file path, so this attachment cannot be sent into the project workspace.");
return; return;
} }
const kind = resolveComposerAttachmentKind(file, localPath);
if (!kind) { if (!kind) {
setErrorText("Supported attachments: images, MP3, PDF, PPT, Excel, Word, CSV, TXT, Markdown, and JSON."); setErrorText(SUPPORTED_ATTACHMENT_ERROR);
return; return;
} }
...@@ -129,6 +174,20 @@ export function useComposerAttachments({ setErrorText }: UseComposerAttachmentsO ...@@ -129,6 +174,20 @@ export function useComposerAttachments({ setErrorText }: UseComposerAttachmentsO
async function openAttachmentPicker() { async function openAttachmentPicker() {
if (window.qjcDesktop) { if (window.qjcDesktop) {
if (composerModeRef.current === "image-only") {
const attachment = await desktopApi.chat.pickImageAttachment();
if (!attachment) {
return;
}
if (composerModeRef.current === "image-only" && !isImageAttachment(attachment)) {
setErrorText(HOME_IMAGE_ATTACHMENT_ERROR);
return;
}
setErrorText("");
appendComposerAttachments([attachment]);
return;
}
const attachments = await desktopApi.chat.pickAttachments(); const attachments = await desktopApi.chat.pickAttachments();
if (!attachments.length) { if (!attachments.length) {
return; return;
...@@ -155,6 +214,7 @@ export function useComposerAttachments({ setErrorText }: UseComposerAttachmentsO ...@@ -155,6 +214,7 @@ export function useComposerAttachments({ setErrorText }: UseComposerAttachmentsO
return { return {
attachmentInputRef, attachmentInputRef,
attachmentAccept,
composerAttachments, composerAttachments,
isComposerDragOver, isComposerDragOver,
clearComposerAttachment, clearComposerAttachment,
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment