refactor: enabling the upload file component (#755)

* refactor: enabling the upload file component

* update
This commit is contained in:
ayangweb
2025-07-10 17:26:44 +08:00
committed by GitHub
parent 5d37420109
commit b17949fe29
13 changed files with 365 additions and 288 deletions

View File

@@ -15,42 +15,6 @@ pub struct UploadAttachmentResponse {
pub attachments: Vec<String>, pub attachments: Vec<String>,
} }
#[derive(Debug, Serialize, Deserialize)]
pub struct AttachmentSource {
pub id: String,
pub created: String,
pub updated: String,
pub session: String,
pub name: String,
pub icon: String,
pub url: String,
pub size: u64,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct AttachmentHit {
pub _index: String,
pub _type: Option<String>,
pub _id: String,
pub _score: Option<f64>,
pub _source: AttachmentSource,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct AttachmentHits {
pub total: Value,
pub max_score: Option<f64>,
pub hits: Option<Vec<AttachmentHit>>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct GetAttachmentResponse {
pub took: u32,
pub timed_out: bool,
pub _shards: Option<Value>,
pub hits: AttachmentHits,
}
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
pub struct DeleteAttachmentResponse { pub struct DeleteAttachmentResponse {
pub _id: String, pub _id: String,
@@ -107,10 +71,7 @@ pub async fn upload_attachment(
} }
#[command] #[command]
pub async fn get_attachment( pub async fn get_attachment(server_id: String, session_id: String) -> Result<Value, String> {
server_id: String,
session_id: String,
) -> Result<GetAttachmentResponse, String> {
let mut query_params = Vec::new(); let mut query_params = Vec::new();
query_params.push(format!("session={}", session_id)); query_params.push(format!("session={}", session_id));
@@ -120,7 +81,7 @@ pub async fn get_attachment(
let body = get_response_body_text(response).await?; let body = get_response_body_text(response).await?;
serde_json::from_str::<GetAttachmentResponse>(&body) serde_json::from_str::<Value>(&body)
.map_err(|e| format!("Failed to parse attachment response: {}", e)) .map_err(|e| format!("Failed to parse attachment response: {}", e))
} }

View File

@@ -4,10 +4,11 @@ import { X } from "lucide-react";
import { useAsyncEffect } from "ahooks"; import { useAsyncEffect } from "ahooks";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { useChatStore } from "@/stores/chatStore"; import { useChatStore, UploadFile } from "@/stores/chatStore";
import { useConnectStore } from "@/stores/connectStore"; import { useConnectStore } from "@/stores/connectStore";
import FileIcon from "../Common/Icons/FileIcon"; import FileIcon from "../Common/Icons/FileIcon";
import platformAdapter from "@/utils/platformAdapter"; import platformAdapter from "@/utils/platformAdapter";
import Tooltip2 from "../Common/Tooltip2";
interface FileListProps { interface FileListProps {
sessionId: string; sessionId: string;
@@ -17,9 +18,8 @@ interface FileListProps {
const FileList = (props: FileListProps) => { const FileList = (props: FileListProps) => {
const { sessionId } = props; const { sessionId } = props;
const { t } = useTranslation(); const { t } = useTranslation();
const uploadFiles = useChatStore((state) => state.uploadFiles); const { uploadFiles, setUploadFiles } = useChatStore();
const setUploadFiles = useChatStore((state) => state.setUploadFiles); const { currentService } = useConnectStore();
const currentService = useConnectStore((state) => state.currentService);
const serverId = useMemo(() => { const serverId = useMemo(() => {
return currentService.id; return currentService.id;
@@ -39,29 +39,42 @@ const FileList = (props: FileListProps) => {
if (uploaded) continue; if (uploaded) continue;
const attachmentIds: any = await platformAdapter.commands( try {
"upload_attachment", const attachmentIds: any = await platformAdapter.commands(
{ "upload_attachment",
serverId, {
sessionId, serverId,
filePaths: [path], 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; setUploadFiles(uploadFiles);
} catch (error) {
Object.assign(item, { Object.assign(item, {
uploaded: true, uploadFailed: true,
attachmentId: attachmentIds[0], failedMessage: String(error),
}); });
}
setUploadFiles(uploadFiles);
} }
}, [uploadFiles]); }, [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)); setUploadFiles(uploadFiles.filter((file) => file.id !== id));
if (uploadFailed) return;
platformAdapter.commands("delete_attachment", { platformAdapter.commands("delete_attachment", {
serverId, serverId,
id: attachmentId, id: attachmentId,
@@ -71,16 +84,25 @@ const FileList = (props: FileListProps) => {
return ( return (
<div className="flex flex-wrap gap-y-2 -mx-1 text-sm"> <div className="flex flex-wrap gap-y-2 -mx-1 text-sm">
{uploadFiles.map((file) => { {uploadFiles.map((file) => {
const { id, name, extname, size, uploaded, attachmentId } = file; const {
id,
name,
extname,
size,
uploaded,
attachmentId,
uploadFailed,
failedMessage,
} = file;
return ( return (
<div key={id} className="w-1/3 px-1"> <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]"> <div className="relative group flex items-center gap-1 p-1 rounded-[4px] bg-[#dedede] dark:bg-[#202126]">
{attachmentId && ( {(uploadFailed || attachmentId) && (
<div <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 " 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={() => { onClick={() => {
deleteFile(id, attachmentId); deleteFile(file);
}} }}
> >
<X className="size-[10px] text-white" /> <X className="size-[10px] text-white" />
@@ -94,16 +116,24 @@ const FileList = (props: FileListProps) => {
{name} {name}
</div> </div>
<div className="text-xs text-[#999999]"> <div className="text-xs">
{uploaded ? ( {uploadFailed && failedMessage ? (
<div className="flex gap-2"> <Tooltip2 content={failedMessage}>
{extname && <span>{extname}</span>} <span className="text-red-500">Upload Failed</span>
<span> </Tooltip2>
{filesize(size, { standard: "jedec", spacer: "" })}
</span>
</div>
) : ( ) : (
<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>
</div> </div>

View File

@@ -1,174 +1,177 @@
import clsx from "clsx"; import clsx from "clsx";
import {filesize} from "filesize"; import { filesize } from "filesize";
import {Files, Trash2, X} from "lucide-react"; import { Files, Trash2, X } from "lucide-react";
import {useEffect, useMemo, useState} from "react"; import { useEffect, useMemo, useState } from "react";
import {useTranslation} from "react-i18next"; import { useTranslation } from "react-i18next";
import {useConnectStore} from "@/stores/connectStore"; import { useConnectStore } from "@/stores/connectStore";
import Checkbox from "@/components/Common/Checkbox"; import Checkbox from "@/components/Common/Checkbox";
import FileIcon from "@/components/Common/Icons/FileIcon"; import FileIcon from "@/components/Common/Icons/FileIcon";
import {AttachmentHit} from "@/types/commands"; import { AttachmentHit } from "@/types/commands";
import {useAppStore} from "@/stores/appStore"; import { useAppStore } from "@/stores/appStore";
import platformAdapter from "@/utils/platformAdapter"; import platformAdapter from "@/utils/platformAdapter";
interface SessionFileProps { interface SessionFileProps {
sessionId: string; sessionId: string;
} }
const SessionFile = (props: SessionFileProps) => { const SessionFile = (props: SessionFileProps) => {
const {sessionId} = props; const { sessionId } = props;
const {t} = useTranslation(); const { t } = useTranslation();
const isTauri = useAppStore((state) => state.isTauri); const isTauri = useAppStore((state) => state.isTauri);
const currentService = useConnectStore((state) => state.currentService); const currentService = useConnectStore((state) => state.currentService);
const [uploadedFiles, setUploadedFiles] = useState<AttachmentHit[]>([]); const [uploadedFiles, setUploadedFiles] = useState<AttachmentHit[]>([]);
const [visible, setVisible] = useState(false); const [visible, setVisible] = useState(false);
const [checkList, setCheckList] = useState<string[]>([]); const [checkList, setCheckList] = useState<string[]>([]);
const serverId = useMemo(() => { const serverId = useMemo(() => {
return currentService.id; return currentService.id;
}, [currentService]); }, [currentService]);
useEffect(() => { useEffect(() => {
setUploadedFiles([]); setUploadedFiles([]);
getUploadedFiles(); getUploadedFiles();
}, [sessionId]); }, [sessionId]);
const getUploadedFiles = async () => { const getUploadedFiles = async () => {
if (isTauri) { if (isTauri) {
const response: any = await platformAdapter.commands("get_attachment", { console.log("sessionId", sessionId);
serverId,
sessionId,
});
setUploadedFiles(response?.hits?.hits ?? []); const response: any = await platformAdapter.commands("get_attachment", {
} else { serverId,
} sessionId,
}; });
const handleDelete = async (id: string) => { console.log("get_attachment response", response);
let result;
if (isTauri) {
result = await platformAdapter.commands("delete_attachment", {
serverId,
id,
});
} else {
}
if (!result) return;
getUploadedFiles(); setUploadedFiles(response?.hits?.hits ?? []);
}; } else {
}
};
const handleCheckAll = (checked: boolean) => { const handleDelete = async (id: string) => {
if (checked) { let result;
setCheckList(uploadedFiles?.map((item) => item?._source?.id)); if (isTauri) {
} else { result = await platformAdapter.commands("delete_attachment", {
setCheckList([]); serverId,
} id,
}; });
} else {
}
if (!result) return;
const handleCheck = (checked: boolean, id: string) => { getUploadedFiles();
if (checked) { };
setCheckList([...checkList, id]);
} else {
setCheckList(checkList.filter((item) => item !== id));
}
};
return ( const handleCheckAll = (checked: boolean) => {
<div if (checked) {
className={clsx("select-none", { setCheckList(uploadedFiles?.map((item) => item?._source?.id));
hidden: uploadedFiles?.length === 0, } else {
})} setCheckList([]);
> }
<div };
className="absolute top-4 right-4 flex items-center justify-center size-8 rounded-lg bg-[#0072FF] cursor-pointer"
onClick={() => {
setVisible(true);
}}
>
<Files className="size-5 text-white"/>
<div const handleCheck = (checked: boolean, id: string) => {
className="absolute -top-2 -right-2 flex items-center justify-center min-w-4 h-4 px-1 text-white text-xs rounded-full bg-[#3DB954]"> if (checked) {
{uploadedFiles?.length} setCheckList([...checkList, id]);
</div> } else {
</div> setCheckList(checkList.filter((item) => item !== id));
}
};
<div return (
className={clsx( <div
"absolute inset-0 flex flex-col p-4 bg-white dark:bg-black", className={clsx("select-none", {
{ hidden: uploadedFiles?.length === 0,
hidden: !visible, })}
} >
)} <div
> className="absolute top-4 right-4 flex items-center justify-center size-8 rounded-lg bg-[#0072FF] cursor-pointer"
<X onClick={() => {
className="absolute top-4 right-4 size-5 text-[#999] cursor-pointer" setVisible(true);
onClick={() => { }}
setVisible(false); >
}} <Files className="size-5 text-white" />
/>
<div className="mb-2 text-sm text-[#333] dark:text-[#D8D8D8] font-bold"> <div className="absolute -top-2 -right-2 flex items-center justify-center min-w-4 h-4 px-1 text-white text-xs rounded-full bg-[#3DB954]">
{t("assistant.sessionFile.title")} {uploadedFiles?.length}
</div> </div>
<div className="flex items-center justify-between pr-2"> </div>
<div
className={clsx(
"absolute inset-0 flex flex-col p-4 bg-white dark:bg-black",
{
hidden: !visible,
}
)}
>
<X
className="absolute top-4 right-4 size-5 text-[#999] cursor-pointer"
onClick={() => {
setVisible(false);
}}
/>
<div className="mb-2 text-sm text-[#333] dark:text-[#D8D8D8] font-bold">
{t("assistant.sessionFile.title")}
</div>
<div className="flex items-center justify-between pr-2">
<span className="text-sm text-[#999]"> <span className="text-sm text-[#999]">
{t("assistant.sessionFile.description")} {t("assistant.sessionFile.description")}
</span> </span>
<Checkbox <Checkbox
indeterminate indeterminate
checked={checkList?.length === uploadedFiles?.length} checked={checkList?.length === uploadedFiles?.length}
onChange={handleCheckAll} onChange={handleCheckAll}
/> />
</div>
<ul className="flex-1 overflow-auto flex flex-col gap-2 mt-6 p-0">
{uploadedFiles?.map((item) => {
const {id, name, icon, size} = item._source;
return (
<li
key={id}
className="flex items-center justify-between min-h-12 px-2 rounded-[4px] bg-[#ededed] dark:bg-[#202126]"
>
<div className="flex items-center gap-2">
<FileIcon extname={icon}/>
<div>
<div className="text-sm leading-4 text-[#333] dark:text-[#D8D8D8]">
{name}
</div>
<div className="text-xs text-[#999]">
<span>{icon}</span>
<span className="pl-2">
{filesize(size, {standard: "jedec", spacer: ""})}
</span>
</div>
</div>
</div>
<div className="flex items-center gap-2">
<Trash2
className="size-4 text-[#999] cursor-pointer"
onClick={() => handleDelete(id)}
/>
<Checkbox
checked={checkList.includes(id)}
onChange={(checked) => handleCheck(checked, id)}
/>
</div>
</li>
);
})}
</ul>
</div>
</div> </div>
); <ul className="flex-1 overflow-auto flex flex-col gap-2 mt-6 p-0">
{uploadedFiles?.map((item) => {
const { id, name, icon, size } = item._source;
return (
<li
key={id}
className="flex items-center justify-between min-h-12 px-2 rounded-[4px] bg-[#ededed] dark:bg-[#202126]"
>
<div className="flex items-center gap-2">
<FileIcon extname={icon} />
<div>
<div className="text-sm leading-4 text-[#333] dark:text-[#D8D8D8]">
{name}
</div>
<div className="text-xs text-[#999]">
<span>{icon}</span>
<span className="pl-2">
{filesize(size, { standard: "jedec", spacer: "" })}
</span>
</div>
</div>
</div>
<div className="flex items-center gap-2">
<Trash2
className="size-4 text-[#999] cursor-pointer"
onClick={() => handleDelete(id)}
/>
<Checkbox
checked={checkList.includes(id)}
onChange={(checked) => handleCheck(checked, id)}
/>
</div>
</li>
);
})}
</ul>
</div>
</div>
);
}; };
export default SessionFile; export default SessionFile;

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

@@ -71,6 +71,15 @@ export default function ChatInput({
hasModules = [], hasModules = [],
searchPlaceholder, searchPlaceholder,
chatPlaceholder, chatPlaceholder,
checkScreenPermission,
requestScreenPermission,
getScreenMonitors,
getScreenWindows,
captureWindowScreenshot,
captureMonitorScreenshot,
openFileDialog,
getFileMetadata,
getFileIcon,
}: ChatInputProps) { }: ChatInputProps) {
const { t } = useTranslation(); const { t } = useTranslation();
@@ -351,6 +360,15 @@ export default function ChatInput({
isMCPActive={isMCPActive} isMCPActive={isMCPActive}
setIsMCPActive={setIsMCPActive} setIsMCPActive={setIsMCPActive}
changeMode={changeMode} changeMode={changeMode}
checkScreenPermission={checkScreenPermission}
requestScreenPermission={requestScreenPermission}
getScreenMonitors={getScreenMonitors}
getScreenWindows={getScreenWindows}
captureMonitorScreenshot={captureMonitorScreenshot}
captureWindowScreenshot={captureWindowScreenshot}
openFileDialog={openFileDialog}
getFileMetadata={getFileMetadata}
getFileIcon={getFileIcon}
/> />
</div> </div>
); );

View File

@@ -16,7 +16,7 @@ import { useAppStore } from "@/stores/appStore";
import { useSearchStore } from "@/stores/searchStore"; import { useSearchStore } from "@/stores/searchStore";
import { useExtensionsStore } from "@/stores/extensionsStore"; import { useExtensionsStore } from "@/stores/extensionsStore";
import { parseSearchQuery, SearchQuery } from "@/utils"; import { parseSearchQuery, SearchQuery } from "@/utils";
// import InputExtra from "./InputExtra"; import InputUpload from "./InputUpload";
// import AiSummaryIcon from "@/components/Common/Icons/AiSummaryIcon"; // import AiSummaryIcon from "@/components/Common/Icons/AiSummaryIcon";
interface InputControlsProps { interface InputControlsProps {
@@ -32,6 +32,17 @@ interface InputControlsProps {
searchPlaceholder?: string; searchPlaceholder?: string;
chatPlaceholder?: string; chatPlaceholder?: string;
changeMode?: (isChatMode: boolean) => void; changeMode?: (isChatMode: boolean) => void;
checkScreenPermission: () => Promise<boolean>;
requestScreenPermission: () => void;
getScreenMonitors: () => Promise<any[]>;
getScreenWindows: () => Promise<any[]>;
captureMonitorScreenshot: (id: number) => Promise<string>;
captureWindowScreenshot: (id: number) => Promise<string>;
openFileDialog: (options: {
multiple: boolean;
}) => Promise<string | string[] | null>;
getFileMetadata: (path: string) => Promise<any>;
getFileIcon: (path: string, size: number) => Promise<string>;
} }
const InputControls = ({ const InputControls = ({
@@ -45,12 +56,21 @@ const InputControls = ({
isChatPage, isChatPage,
hasModules, hasModules,
changeMode, changeMode,
checkScreenPermission,
requestScreenPermission,
getScreenMonitors,
getScreenWindows,
captureWindowScreenshot,
captureMonitorScreenshot,
openFileDialog,
getFileMetadata,
getFileIcon,
}: InputControlsProps) => { }: InputControlsProps) => {
const { t } = useTranslation(); const { t } = useTranslation();
const isTauri = useAppStore((state) => state.isTauri); const isTauri = useAppStore((state) => state.isTauri);
const currentAssistant = useConnectStore((state) => state.currentAssistant); const { currentAssistant, currentSessionId } = useConnectStore();
const { modeSwitch, deepThinking } = useShortcutsStore(); const { modeSwitch, deepThinking } = useShortcutsStore();
const source = currentAssistant?._source; const source = currentAssistant?._source;
@@ -151,24 +171,24 @@ const InputControls = ({
> >
{isChatMode ? ( {isChatMode ? (
<div className="flex gap-2 text-[12px] leading-3 text-[#333] dark:text-[#d8d8d8]"> <div className="flex gap-2 text-[12px] leading-3 text-[#333] dark:text-[#d8d8d8]">
{/* {sessionId && ( {currentSessionId && (
<InputExtra <InputUpload
checkScreenPermission={checkScreenPermission} checkScreenPermission={checkScreenPermission}
requestScreenPermission={requestScreenPermission} requestScreenPermission={requestScreenPermission}
getScreenMonitors={getScreenMonitors} getScreenMonitors={getScreenMonitors}
getScreenWindows={getScreenWindows} getScreenWindows={getScreenWindows}
captureMonitorScreenshot={captureMonitorScreenshot} captureMonitorScreenshot={captureMonitorScreenshot}
captureWindowScreenshot={captureWindowScreenshot} captureWindowScreenshot={captureWindowScreenshot}
openFileDialog={openFileDialog} openFileDialog={openFileDialog}
getFileMetadata={getFileMetadata} getFileMetadata={getFileMetadata}
getFileIcon={getFileIcon} getFileIcon={getFileIcon}
/> />
)} */} )}
{source?.type === "deep_think" && source?.config?.visible && ( {source?.type === "deep_think" && source?.config?.visible && (
<button <button
className={clsx( className={clsx(
"flex items-center gap-1 py-[3px] pl-1 pr-1.5 rounded-md transition hover:bg-[#EDEDED] dark:hover:bg-[#202126]", "flex items-center gap-1 p-1 rounded-md transition hover:bg-[#EDEDED] dark:hover:bg-[#202126]",
{ {
"!bg-[rgba(0,114,255,0.3)]": isDeepThinkActive, "!bg-[rgba(0,114,255,0.3)]": isDeepThinkActive,
} }
@@ -213,13 +233,14 @@ const InputControls = ({
getMCPByServer={getMCPByServer} getMCPByServer={getMCPByServer}
/> />
{!(source?.datasource?.enabled && source?.datasource?.visible) && {!currentSessionId &&
(source?.type !== "deep_think" || !source?.config?.visible) && !(source?.datasource?.enabled && source?.datasource?.visible) &&
!(source?.mcp_servers?.enabled && source?.mcp_servers?.visible) ? ( (source?.type !== "deep_think" || !source?.config?.visible) &&
<div className="px-[9px]"> !(source?.mcp_servers?.enabled && source?.mcp_servers?.visible) && (
<Copyright /> <div className="px-[9px]">
</div> <Copyright />
) : null} </div>
)}
</div> </div>
) : ( ) : (
<div data-tauri-drag-region className="w-28 flex gap-2 relative"> <div data-tauri-drag-region className="w-28 flex gap-2 relative">

View File

@@ -1,4 +1,4 @@
import { Fragment, MouseEvent } from "react"; import { FC, Fragment, MouseEvent } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { ChevronRight, Plus } from "lucide-react"; import { ChevronRight, Plus } from "lucide-react";
import { import {
@@ -35,7 +35,7 @@ interface MenuItem {
clickEvent?: (event: MouseEvent) => void; clickEvent?: (event: MouseEvent) => void;
} }
interface InputExtraProps { interface InputUploadProps {
checkScreenPermission: () => Promise<boolean>; checkScreenPermission: () => Promise<boolean>;
requestScreenPermission: () => void; requestScreenPermission: () => void;
getScreenMonitors: () => Promise<any[]>; getScreenMonitors: () => Promise<any[]>;
@@ -49,30 +49,22 @@ interface InputExtraProps {
getFileIcon: (path: string, size: number) => Promise<string>; getFileIcon: (path: string, size: number) => Promise<string>;
} }
const InputExtra = ({ const InputUpload: FC<InputUploadProps> = (props) => {
checkScreenPermission, const {
requestScreenPermission, checkScreenPermission,
getScreenMonitors, requestScreenPermission,
getScreenWindows, getScreenMonitors,
captureMonitorScreenshot, getScreenWindows,
captureWindowScreenshot, captureMonitorScreenshot,
openFileDialog, captureWindowScreenshot,
getFileMetadata, openFileDialog,
getFileIcon, getFileMetadata,
}: InputExtraProps) => { getFileIcon,
} = props;
const { t, i18n } = useTranslation(); const { t, i18n } = useTranslation();
const uploadFiles = useChatStore((state) => state.uploadFiles); const { uploadFiles, setUploadFiles } = useChatStore();
const setUploadFiles = useChatStore((state) => state.setUploadFiles); const { withVisibility, addError } = useAppStore();
const withVisibility = useAppStore((state) => state.withVisibility); const { modifierKey, addFile, modifierKeyPressed } = useShortcutsStore();
const modifierKey = useShortcutsStore((state) => {
return state.modifierKey;
});
const addFile = useShortcutsStore((state) => {
return state.addFile;
});
const modifierKeyPressed = useShortcutsStore((state) => {
return state.modifierKeyPressed;
});
const state = useReactive<State>({ const state = useReactive<State>({
screenshotableMonitors: [], screenshotableMonitors: [],
@@ -104,6 +96,8 @@ const InputExtra = ({
const stat = await getFileMetadata(path); const stat = await getFileMetadata(path);
if (stat.size / 1024 / 1024 > 100) { if (stat.size / 1024 / 1024 > 100) {
addError(t("search.input.uploadFileHints.maxSize"));
continue; continue;
} }
@@ -184,25 +178,23 @@ const InputExtra = ({
return ( return (
<Menu> <Menu>
<MenuButton className="size-6"> <MenuButton className="flex p-1 rounded-md transition hover:bg-[#EDEDED] dark:hover:bg-[#202126]">
<Tooltip content="支持截图、上传文件,最多 50个单个文件最大 100 MB。"> <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
<Plus className={clsx("size-3 scale-[1.3]", {
className={clsx("size-5", { hidden: modifierKeyPressed,
hidden: modifierKeyPressed, })}
})} />
/>
<div <div
className={clsx( className={clsx(
"size-4 flex items-center justify-center font-normal text-xs text-[#333] leading-[14px] bg-[#ccc] dark:bg-[#6B6B6B] rounded-md shadow-[-6px_0px_6px_2px_#fff] dark:shadow-[-6px_0px_6px_2px_#000]", "size-4 flex items-center justify-center font-normal text-xs text-[#333] leading-[14px] bg-[#ccc] dark:bg-[#6B6B6B] rounded-md shadow-[-6px_0px_6px_2px_#fff] dark:shadow-[-6px_0px_6px_2px_#000]",
{ {
hidden: !modifierKeyPressed, hidden: !modifierKeyPressed,
} }
)} )}
> >
{addFile} {addFile}
</div>
</div> </div>
</Tooltip> </Tooltip>
</MenuButton> </MenuButton>
@@ -280,4 +272,4 @@ const InputExtra = ({
); );
}; };
export default InputExtra; export default InputUpload;

View File

@@ -166,7 +166,7 @@ export default function MCPPopover({
return ( return (
<div <div
className={clsx( className={clsx(
"flex items-center gap-1 p-[3px] pr-1 rounded-md transition hover:bg-[#EDEDED] dark:hover:bg-[#202126] cursor-pointer", "flex items-center gap-1 p-1 rounded-md transition hover:bg-[#EDEDED] dark:hover:bg-[#202126] cursor-pointer",
{ {
"!bg-[rgba(0,114,255,0.3)]": isMCPActive, "!bg-[rgba(0,114,255,0.3)]": isMCPActive,
} }

View File

@@ -172,7 +172,7 @@ export default function SearchPopover({
return ( return (
<div <div
className={clsx( className={clsx(
"flex items-center gap-1 p-[3px] pr-1 rounded-md transition hover:bg-[#EDEDED] dark:hover:bg-[#202126] cursor-pointer", "flex items-center gap-1 p-1 rounded-md transition hover:bg-[#EDEDED] dark:hover:bg-[#202126] cursor-pointer",
{ {
"!bg-[rgba(0,114,255,0.3)]": isSearchActive, "!bg-[rgba(0,114,255,0.3)]": isSearchActive,
} }

View File

@@ -3,12 +3,12 @@ import platformAdapter from "@/utils/platformAdapter";
import { useTauriFocus } from "./useTauriFocus"; import { useTauriFocus } from "./useTauriFocus";
export function useWindowEvents() { export function useWindowEvents() {
const isPinned = useAppStore((state) => state.isPinned); const { setBlurred } = useAppStore();
const visible = useAppStore((state) => state.visible);
const setBlurred = useAppStore((state) => state.setBlurred);
useTauriFocus({ useTauriFocus({
onBlur() { onBlur() {
const { isPinned, visible } = useAppStore.getState();
if (isPinned || visible) { if (isPinned || visible) {
return setBlurred(true); return setBlurred(true);
} }

View File

@@ -342,6 +342,10 @@
"searchPopover": { "searchPopover": {
"title": "Search Scope", "title": "Search Scope",
"allScope": "All 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": { "main": {

View File

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

View File

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