mirror of
https://github.com/infinilabs/coco-app.git
synced 2025-12-16 11:37:47 +01:00
refactor: enabling the upload file component (#755)
* refactor: enabling the upload file component * update
This commit is contained in:
@@ -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))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
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;
|
||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -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,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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": {
|
||||||
|
|||||||
@@ -342,6 +342,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;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SynthesizeItem {
|
interface SynthesizeItem {
|
||||||
|
|||||||
Reference in New Issue
Block a user