mirror of
https://github.com/infinilabs/coco-app.git
synced 2025-12-18 20:39:25 +01:00
Compare commits
2 Commits
release_pr
...
upload-fil
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9ee6b9a6c9 | ||
|
|
24b1758b11 |
@@ -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 { UploadFile, useChatStore } 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;
|
||||||
@@ -39,6 +40,7 @@ const FileList = (props: FileListProps) => {
|
|||||||
|
|
||||||
if (uploaded) continue;
|
if (uploaded) continue;
|
||||||
|
|
||||||
|
try {
|
||||||
const attachmentIds: any = await platformAdapter.commands(
|
const attachmentIds: any = await platformAdapter.commands(
|
||||||
"upload_attachment",
|
"upload_attachment",
|
||||||
{
|
{
|
||||||
@@ -48,20 +50,32 @@ const FileList = (props: FileListProps) => {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!attachmentIds) continue;
|
if (!attachmentIds) {
|
||||||
|
throw new Error("Failed to get attachment id");
|
||||||
|
} else {
|
||||||
Object.assign(item, {
|
Object.assign(item, {
|
||||||
uploaded: true,
|
uploaded: true,
|
||||||
attachmentId: attachmentIds[0],
|
attachmentId: attachmentIds[0],
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
setUploadFiles(uploadFiles);
|
setUploadFiles(uploadFiles);
|
||||||
|
} catch (error) {
|
||||||
|
Object.assign(item, {
|
||||||
|
uploadFailed: true,
|
||||||
|
failedMessage: String(error),
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, [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 +85,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,7 +117,13 @@ const FileList = (props: FileListProps) => {
|
|||||||
{name}
|
{name}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="text-xs text-[#999999]">
|
<div className="text-xs">
|
||||||
|
{uploadFailed && failedMessage ? (
|
||||||
|
<Tooltip2 content={failedMessage}>
|
||||||
|
<span className="text-red-500">Upload Failed</span>
|
||||||
|
</Tooltip2>
|
||||||
|
) : (
|
||||||
|
<div className="text-[#999]">
|
||||||
{uploaded ? (
|
{uploaded ? (
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
{extname && <span>{extname}</span>}
|
{extname && <span>{extname}</span>}
|
||||||
@@ -106,6 +135,8 @@ const FileList = (props: FileListProps) => {
|
|||||||
<span>{t("assistant.fileList.uploading")}</span>
|
<span>{t("assistant.fileList.uploading")}</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
42
src/components/Common/Tooltip2.tsx
Normal file
42
src/components/Common/Tooltip2.tsx
Normal 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;
|
||||||
@@ -14,7 +14,7 @@ import SearchPopover from "./SearchPopover";
|
|||||||
import MCPPopover from "./MCPPopover";
|
import MCPPopover from "./MCPPopover";
|
||||||
// import AudioRecording from "../AudioRecording";
|
// import AudioRecording from "../AudioRecording";
|
||||||
import { DataSource } from "@/types/commands";
|
import { DataSource } from "@/types/commands";
|
||||||
// import InputExtra from "./InputExtra";
|
import InputExtra from "./InputExtra";
|
||||||
import { useConnectStore } from "@/stores/connectStore";
|
import { useConnectStore } from "@/stores/connectStore";
|
||||||
import { useShortcutsStore } from "@/stores/shortcutsStore";
|
import { useShortcutsStore } from "@/stores/shortcutsStore";
|
||||||
import Copyright from "@/components/Common/Copyright";
|
import Copyright from "@/components/Common/Copyright";
|
||||||
@@ -95,6 +95,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();
|
||||||
|
|
||||||
@@ -105,7 +114,7 @@ export default function ChatInput({
|
|||||||
const sourceData = useSearchStore((state) => state.sourceData);
|
const sourceData = useSearchStore((state) => state.sourceData);
|
||||||
const setSourceData = useSearchStore((state) => state.setSourceData);
|
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 modifierKey = useShortcutsStore((state) => state.modifierKey);
|
||||||
const modeSwitch = useShortcutsStore((state) => state.modeSwitch);
|
const modeSwitch = useShortcutsStore((state) => state.modeSwitch);
|
||||||
const returnToInput = useShortcutsStore((state) => state.returnToInput);
|
const returnToInput = useShortcutsStore((state) => state.returnToInput);
|
||||||
@@ -373,7 +382,7 @@ export default function ChatInput({
|
|||||||
>
|
>
|
||||||
{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 && (
|
{sessionId && (
|
||||||
<InputExtra
|
<InputExtra
|
||||||
checkScreenPermission={checkScreenPermission}
|
checkScreenPermission={checkScreenPermission}
|
||||||
requestScreenPermission={requestScreenPermission}
|
requestScreenPermission={requestScreenPermission}
|
||||||
@@ -385,7 +394,7 @@ export default function ChatInput({
|
|||||||
getFileMetadata={getFileMetadata}
|
getFileMetadata={getFileMetadata}
|
||||||
getFileIcon={getFileIcon}
|
getFileIcon={getFileIcon}
|
||||||
/>
|
/>
|
||||||
)} */}
|
)}
|
||||||
|
|
||||||
{source?.type === "deep_think" && source?.config?.visible && (
|
{source?.type === "deep_think" && source?.config?.visible && (
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -73,6 +73,7 @@ const InputExtra = ({
|
|||||||
const modifierKeyPressed = useShortcutsStore((state) => {
|
const modifierKeyPressed = useShortcutsStore((state) => {
|
||||||
return state.modifierKeyPressed;
|
return state.modifierKeyPressed;
|
||||||
});
|
});
|
||||||
|
const addError = useAppStore((state) => state.addError);
|
||||||
|
|
||||||
const state = useReactive<State>({
|
const state = useReactive<State>({
|
||||||
screenshotableMonitors: [],
|
screenshotableMonitors: [],
|
||||||
@@ -104,6 +105,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,8 +187,8 @@ const InputExtra = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Menu>
|
<Menu>
|
||||||
<MenuButton className="size-6">
|
<MenuButton as="div" className="size-6">
|
||||||
<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]">
|
<div className="size-full flex justify-center items-center rounded-lg transition hover:bg-[#EDEDED] dark:hover:bg-[#202126]">
|
||||||
<Plus
|
<Plus
|
||||||
className={clsx("size-5", {
|
className={clsx("size-5", {
|
||||||
|
|||||||
@@ -287,6 +287,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": {
|
||||||
|
|||||||
@@ -287,6 +287,10 @@
|
|||||||
"searchPopover": {
|
"searchPopover": {
|
||||||
"title": "搜索范围",
|
"title": "搜索范围",
|
||||||
"allScope": "所有范围"
|
"allScope": "所有范围"
|
||||||
|
},
|
||||||
|
"uploadFileHints": {
|
||||||
|
"tooltip": "支持截图、上传文件,最多 50个,单个文件最大 100 MB。",
|
||||||
|
"maxSize": "文件大小不能超过 100 MB。"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"main": {
|
"main": {
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type IChatStore = {
|
export type IChatStore = {
|
||||||
|
|||||||
Reference in New Issue
Block a user