2 Commits

Author SHA1 Message Date
ayang
9ee6b9a6c9 feat: add file upload failure handling and alert message 2025-05-16 14:32:11 +08:00
ayang
24b1758b11 refactor: enabling the InputExtra component 2025-05-15 15:50:03 +08:00
7 changed files with 131 additions and 36 deletions

View File

@@ -4,10 +4,11 @@ import { X } from "lucide-react";
import { useAsyncEffect } from "ahooks";
import { useTranslation } from "react-i18next";
import { useChatStore } from "@/stores/chatStore";
import { UploadFile, useChatStore } from "@/stores/chatStore";
import { useConnectStore } from "@/stores/connectStore";
import FileIcon from "../Common/Icons/FileIcon";
import platformAdapter from "@/utils/platformAdapter";
import Tooltip2 from "../Common/Tooltip2";
interface FileListProps {
sessionId: string;
@@ -39,29 +40,42 @@ const FileList = (props: FileListProps) => {
if (uploaded) continue;
const attachmentIds: any = await platformAdapter.commands(
"upload_attachment",
{
serverId,
sessionId,
filePaths: [path],
try {
const attachmentIds: any = await platformAdapter.commands(
"upload_attachment",
{
serverId,
sessionId,
filePaths: [path],
}
);
if (!attachmentIds) {
throw new Error("Failed to get attachment id");
} else {
Object.assign(item, {
uploaded: true,
attachmentId: attachmentIds[0],
});
}
);
if (!attachmentIds) continue;
Object.assign(item, {
uploaded: true,
attachmentId: attachmentIds[0],
});
setUploadFiles(uploadFiles);
setUploadFiles(uploadFiles);
} catch (error) {
Object.assign(item, {
uploadFailed: true,
failedMessage: String(error),
});
}
}
}, [uploadFiles]);
const deleteFile = async (id: string, attachmentId: string) => {
const deleteFile = async (file: UploadFile) => {
const { id, uploadFailed, attachmentId } = file;
setUploadFiles(uploadFiles.filter((file) => file.id !== id));
if (uploadFailed) return;
platformAdapter.commands("delete_attachment", {
serverId,
id: attachmentId,
@@ -71,16 +85,25 @@ const FileList = (props: FileListProps) => {
return (
<div className="flex flex-wrap gap-y-2 -mx-1 text-sm">
{uploadFiles.map((file) => {
const { id, name, extname, size, uploaded, attachmentId } = file;
const {
id,
name,
extname,
size,
uploaded,
attachmentId,
uploadFailed,
failedMessage,
} = file;
return (
<div key={id} className="w-1/3 px-1">
<div className="relative group flex items-center gap-1 p-1 rounded-[4px] bg-[#dedede] dark:bg-[#202126]">
{attachmentId && (
{(uploadFailed || attachmentId) && (
<div
className="absolute flex justify-center items-center size-[14px] bg-red-600 top-0 right-0 rounded-full cursor-pointer translate-x-[5px] -translate-y-[5px] transition opacity-0 group-hover:opacity-100 "
onClick={() => {
deleteFile(id, attachmentId);
deleteFile(file);
}}
>
<X className="size-[10px] text-white" />
@@ -94,16 +117,24 @@ const FileList = (props: FileListProps) => {
{name}
</div>
<div className="text-xs text-[#999999]">
{uploaded ? (
<div className="flex gap-2">
{extname && <span>{extname}</span>}
<span>
{filesize(size, { standard: "jedec", spacer: "" })}
</span>
</div>
<div className="text-xs">
{uploadFailed && failedMessage ? (
<Tooltip2 content={failedMessage}>
<span className="text-red-500">Upload Failed</span>
</Tooltip2>
) : (
<span>{t("assistant.fileList.uploading")}</span>
<div className="text-[#999]">
{uploaded ? (
<div className="flex gap-2">
{extname && <span>{extname}</span>}
<span>
{filesize(size, { standard: "jedec", spacer: "" })}
</span>
</div>
) : (
<span>{t("assistant.fileList.uploading")}</span>
)}
</div>
)}
</div>
</div>

View File

@@ -0,0 +1,42 @@
import {
Popover,
PopoverButton,
PopoverPanel,
PopoverPanelProps,
} from "@headlessui/react";
import { useBoolean } from "ahooks";
import clsx from "clsx";
import { FC, ReactNode } from "react";
interface Tooltip2Props extends PopoverPanelProps {
content: string;
children: ReactNode;
}
const Tooltip2: FC<Tooltip2Props> = (props) => {
const { content, children, anchor = "top", ...rest } = props;
const [visible, { setTrue, setFalse }] = useBoolean(false);
return (
<Popover>
<PopoverButton onMouseOver={setTrue} onMouseOut={setFalse}>
{children}
</PopoverButton>
<PopoverPanel
{...rest}
static
anchor={anchor}
className={clsx(
"fixed z-1000 p-2 rounded-md text-xs text-white bg-black/75 hidden",
{
"!block": visible,
}
)}
>
{content}
</PopoverPanel>
</Popover>
);
};
export default Tooltip2;

View File

@@ -14,7 +14,7 @@ import SearchPopover from "./SearchPopover";
import MCPPopover from "./MCPPopover";
// import AudioRecording from "../AudioRecording";
import { DataSource } from "@/types/commands";
// import InputExtra from "./InputExtra";
import InputExtra from "./InputExtra";
import { useConnectStore } from "@/stores/connectStore";
import { useShortcutsStore } from "@/stores/shortcutsStore";
import Copyright from "@/components/Common/Copyright";
@@ -95,6 +95,15 @@ export default function ChatInput({
hasModules = [],
searchPlaceholder,
chatPlaceholder,
checkScreenPermission,
requestScreenPermission,
getScreenMonitors,
getScreenWindows,
captureWindowScreenshot,
captureMonitorScreenshot,
openFileDialog,
getFileMetadata,
getFileIcon,
}: ChatInputProps) {
const { t } = useTranslation();
@@ -105,7 +114,7 @@ export default function ChatInput({
const sourceData = useSearchStore((state) => state.sourceData);
const setSourceData = useSearchStore((state) => state.setSourceData);
// const sessionId = useConnectStore((state) => state.currentSessionId);
const sessionId = useConnectStore((state) => state.currentSessionId);
const modifierKey = useShortcutsStore((state) => state.modifierKey);
const modeSwitch = useShortcutsStore((state) => state.modeSwitch);
const returnToInput = useShortcutsStore((state) => state.returnToInput);
@@ -373,7 +382,7 @@ export default function ChatInput({
>
{isChatMode ? (
<div className="flex gap-2 text-[12px] leading-3 text-[#333] dark:text-[#d8d8d8]">
{/* {sessionId && (
{sessionId && (
<InputExtra
checkScreenPermission={checkScreenPermission}
requestScreenPermission={requestScreenPermission}
@@ -385,7 +394,7 @@ export default function ChatInput({
getFileMetadata={getFileMetadata}
getFileIcon={getFileIcon}
/>
)} */}
)}
{source?.type === "deep_think" && source?.config?.visible && (
<button

View File

@@ -73,6 +73,7 @@ const InputExtra = ({
const modifierKeyPressed = useShortcutsStore((state) => {
return state.modifierKeyPressed;
});
const addError = useAppStore((state) => state.addError);
const state = useReactive<State>({
screenshotableMonitors: [],
@@ -104,6 +105,8 @@ const InputExtra = ({
const stat = await getFileMetadata(path);
if (stat.size / 1024 / 1024 > 100) {
addError(t("search.input.uploadFileHints.maxSize"));
continue;
}
@@ -184,8 +187,8 @@ const InputExtra = ({
return (
<Menu>
<MenuButton className="size-6">
<Tooltip content="支持截图、上传文件,最多 50个单个文件最大 100 MB。">
<MenuButton as="div" className="size-6">
<Tooltip content={t("search.input.uploadFileHints.tooltip")}>
<div className="size-full flex justify-center items-center rounded-lg transition hover:bg-[#EDEDED] dark:hover:bg-[#202126]">
<Plus
className={clsx("size-5", {

View File

@@ -287,6 +287,10 @@
"searchPopover": {
"title": "Search Scope",
"allScope": "All Scope"
},
"uploadFileHints": {
"tooltip": "Support screenshots, upload files, up to 50, single file up to 100 MB.",
"maxSize": "The file size cannot exceed 100 MB."
}
},
"main": {

View File

@@ -287,6 +287,10 @@
"searchPopover": {
"title": "搜索范围",
"allScope": "所有范围"
},
"uploadFileHints": {
"tooltip": "支持截图、上传文件,最多 50个单个文件最大 100 MB。",
"maxSize": "文件大小不能超过 100 MB。"
}
},
"main": {

View File

@@ -5,12 +5,14 @@ import {
} from "zustand/middleware";
import { Metadata } from "tauri-plugin-fs-pro-api";
interface UploadFile extends Metadata {
export interface UploadFile extends Metadata {
id: string;
path: string;
icon: string;
uploaded?: boolean;
attachmentId?: string;
uploadFailed?: boolean;
failedMessage?: string;
}
export type IChatStore = {