mirror of
https://github.com/infinilabs/coco-app.git
synced 2025-12-16 19:47:43 +01:00
feat: chat support for uploading files (#229)
* feat: chat support for uploading files * refactor: out of focus hidden window * refactor: filtering files larger than 100M * refactor: displayed in the chat content area * refactor: hide window when out of focus
This commit is contained in:
@@ -29,11 +29,13 @@
|
||||
"axios": "^1.7.7",
|
||||
"clsx": "^2.1.1",
|
||||
"dotenv": "^16.4.7",
|
||||
"filesize": "^10.1.6",
|
||||
"i18next": "^23.16.2",
|
||||
"i18next-browser-languagedetector": "^8.0.3",
|
||||
"lodash-es": "^4.17.21",
|
||||
"lucide-react": "^0.461.0",
|
||||
"mermaid": "^11.4.0",
|
||||
"nanoid": "^5.1.2",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-hotkeys-hook": "^4.5.1",
|
||||
@@ -46,6 +48,7 @@
|
||||
"remark-breaks": "^4.0.0",
|
||||
"remark-gfm": "^4.0.0",
|
||||
"remark-math": "^6.0.0",
|
||||
"tauri-plugin-fs-pro-api": "^2.3.1",
|
||||
"use-debounce": "^10.0.4",
|
||||
"uuid": "^11.0.3",
|
||||
"zustand": "^5.0.0"
|
||||
|
||||
29
pnpm-lock.yaml
generated
29
pnpm-lock.yaml
generated
@@ -56,6 +56,9 @@ importers:
|
||||
dotenv:
|
||||
specifier: ^16.4.7
|
||||
version: 16.4.7
|
||||
filesize:
|
||||
specifier: ^10.1.6
|
||||
version: 10.1.6
|
||||
i18next:
|
||||
specifier: ^23.16.2
|
||||
version: 23.16.2
|
||||
@@ -71,6 +74,9 @@ importers:
|
||||
mermaid:
|
||||
specifier: ^11.4.0
|
||||
version: 11.4.0
|
||||
nanoid:
|
||||
specifier: ^5.1.2
|
||||
version: 5.1.2
|
||||
react:
|
||||
specifier: ^18.2.0
|
||||
version: 18.3.1
|
||||
@@ -107,6 +113,9 @@ importers:
|
||||
remark-math:
|
||||
specifier: ^6.0.0
|
||||
version: 6.0.0
|
||||
tauri-plugin-fs-pro-api:
|
||||
specifier: ^2.3.1
|
||||
version: 2.3.1
|
||||
use-debounce:
|
||||
specifier: ^10.0.4
|
||||
version: 10.0.4(react@18.3.1)
|
||||
@@ -1879,6 +1888,10 @@ packages:
|
||||
resolution: {integrity: sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
filesize@10.1.6:
|
||||
resolution: {integrity: sha512-sJslQKU2uM33qH5nqewAwVB2QgR6w1aMNsYUp3aN5rMRyXEwJGmZvaWzeJFNTOXWlHQyBFCWrdj3fV/fsTOX8w==}
|
||||
engines: {node: '>= 10.4.0'}
|
||||
|
||||
fill-range@7.1.1:
|
||||
resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==}
|
||||
engines: {node: '>=8'}
|
||||
@@ -2556,6 +2569,11 @@ packages:
|
||||
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
|
||||
hasBin: true
|
||||
|
||||
nanoid@5.1.2:
|
||||
resolution: {integrity: sha512-b+CiXQCNMUGe0Ri64S9SXFcP9hogjAJ2Rd6GdVxhPLRm7mhGaM7VgOvCAJ1ZshfHbqVDI3uqTI5C8/GaKuLI7g==}
|
||||
engines: {node: ^18 || >=20}
|
||||
hasBin: true
|
||||
|
||||
netmask@2.0.2:
|
||||
resolution: {integrity: sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==}
|
||||
engines: {node: '>= 0.4.0'}
|
||||
@@ -3087,6 +3105,9 @@ packages:
|
||||
engines: {node: '>=14.0.0'}
|
||||
hasBin: true
|
||||
|
||||
tauri-plugin-fs-pro-api@2.3.1:
|
||||
resolution: {integrity: sha512-fx/zITX9MWoDZ603FKWSybluZqJUEOvHU+H6kj3iRJNyoGFHoNkajpQbiK5cu81spQbGBlP9sV2HkaCI07gQ+Q==}
|
||||
|
||||
thenify-all@1.6.0:
|
||||
resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==}
|
||||
engines: {node: '>=0.8'}
|
||||
@@ -5047,6 +5068,8 @@ snapshots:
|
||||
dependencies:
|
||||
is-unicode-supported: 2.1.0
|
||||
|
||||
filesize@10.1.6: {}
|
||||
|
||||
fill-range@7.1.1:
|
||||
dependencies:
|
||||
to-regex-range: 5.0.1
|
||||
@@ -5963,6 +5986,8 @@ snapshots:
|
||||
|
||||
nanoid@3.3.7: {}
|
||||
|
||||
nanoid@5.1.2: {}
|
||||
|
||||
netmask@2.0.2: {}
|
||||
|
||||
new-github-release-url@2.0.0:
|
||||
@@ -6602,6 +6627,10 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- ts-node
|
||||
|
||||
tauri-plugin-fs-pro-api@2.3.1:
|
||||
dependencies:
|
||||
'@tauri-apps/api': 2.2.0
|
||||
|
||||
thenify-all@1.6.0:
|
||||
dependencies:
|
||||
thenify: 3.3.1
|
||||
|
||||
808
src-tauri/Cargo.lock
generated
808
src-tauri/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -38,6 +38,7 @@ tauri-plugin-fs = "2"
|
||||
tauri-plugin-updater = "2"
|
||||
tauri-plugin-process = "2"
|
||||
tauri-plugin-drag = "2"
|
||||
tauri-plugin-fs-pro = "2"
|
||||
|
||||
tokio-native-tls = "0.3" # For wss connections
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
|
||||
@@ -62,6 +62,8 @@
|
||||
}
|
||||
],
|
||||
"deny": []
|
||||
}
|
||||
},
|
||||
"dialog:default",
|
||||
"fs-pro:default"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
mod assistant;
|
||||
mod autostart;
|
||||
mod common;
|
||||
mod local;
|
||||
mod search;
|
||||
mod server;
|
||||
mod setup;
|
||||
mod shortcut;
|
||||
mod util;
|
||||
|
||||
mod setup;
|
||||
mod assistant;
|
||||
|
||||
use crate::common::register::SearchSourceRegistry;
|
||||
// use crate::common::traits::SearchSource;
|
||||
use crate::common::{MAIN_WINDOW_LABEL, SETTINGS_WINDOW_LABEL};
|
||||
@@ -80,7 +79,9 @@ pub fn run() {
|
||||
))
|
||||
.plugin(tauri_plugin_theme::init(ctx.config_mut()))
|
||||
.plugin(tauri_plugin_deep_link::init())
|
||||
.plugin(tauri_plugin_store::Builder::default().build());
|
||||
.plugin(tauri_plugin_store::Builder::default().build())
|
||||
.plugin(tauri_plugin_dialog::init())
|
||||
.plugin(tauri_plugin_fs_pro::init());
|
||||
|
||||
// Conditional compilation for macOS
|
||||
#[cfg(target_os = "macos")]
|
||||
|
||||
@@ -22,6 +22,7 @@ import { Sidebar } from "@/components/Assistant/Sidebar";
|
||||
import { useConnectStore } from "@/stores/connectStore";
|
||||
import { useSearchStore } from "@/stores/searchStore";
|
||||
import { IServer } from "@/stores/appStore";
|
||||
import FileList from "../Search/FileList";
|
||||
|
||||
interface ChatAIProps {
|
||||
isTransitioned: boolean;
|
||||
@@ -91,6 +92,7 @@ const ChatAI = memo(
|
||||
const [isSidebarOpenChat, setIsSidebarOpenChat] = useState(isSidebarOpen);
|
||||
const [chats, setChats] = useState<Chat[]>([]);
|
||||
const sourceDataIds = useSearchStore((state) => state.sourceDataIds);
|
||||
const uploadFiles = useChatStore((state) => state.uploadFiles);
|
||||
|
||||
useEffect(() => {
|
||||
activeChatProp && setActiveChat(activeChatProp);
|
||||
@@ -641,7 +643,6 @@ const ChatAI = memo(
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<ChatHeader
|
||||
onCreateNewChat={clearChat}
|
||||
onOpenChatAI={openChatAI}
|
||||
@@ -653,9 +654,9 @@ const ChatAI = memo(
|
||||
activeChat={activeChat}
|
||||
reconnect={reconnect}
|
||||
/>
|
||||
|
||||
{/* Chat messages */}
|
||||
<div className="w-full overflow-x-hidden overflow-y-auto border-t border-[rgba(0,0,0,0.1)] dark:border-[rgba(255,255,255,0.15)] custom-scrollbar relative">
|
||||
<div className="flex flex-col h-full justify-between overflow-hidden">
|
||||
<div className="flex-1 w-full overflow-x-hidden overflow-y-auto border-t border-[rgba(0,0,0,0.1)] dark:border-[rgba(255,255,255,0.15)] custom-scrollbar relative">
|
||||
<ChatMessage
|
||||
key={"greetings"}
|
||||
message={{
|
||||
@@ -666,7 +667,6 @@ const ChatAI = memo(
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
{activeChat?.messages?.map((message, index) => (
|
||||
<ChatMessage
|
||||
key={message._id + index}
|
||||
@@ -675,7 +675,6 @@ const ChatAI = memo(
|
||||
onResend={handleSendMessage}
|
||||
/>
|
||||
))}
|
||||
|
||||
{(query_intent ||
|
||||
fetch_source ||
|
||||
pick_source ||
|
||||
@@ -703,7 +702,6 @@ const ChatAI = memo(
|
||||
response={response}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{timedoutShow ? (
|
||||
<ChatMessage
|
||||
key={"timedout"}
|
||||
@@ -719,7 +717,6 @@ const ChatAI = memo(
|
||||
isTyping={false}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{errorShow ? (
|
||||
<ChatMessage
|
||||
key={"error"}
|
||||
@@ -735,9 +732,15 @@ const ChatAI = memo(
|
||||
isTyping={false}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
<div ref={messagesEndRef} />
|
||||
</div>
|
||||
|
||||
{uploadFiles.length > 0 && (
|
||||
<div className="max-h-[120px] overflow-auto p-2 border-t border-[#E6E6E6] cl">
|
||||
<FileList />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
53
src/components/Search/FileList.tsx
Normal file
53
src/components/Search/FileList.tsx
Normal file
@@ -0,0 +1,53 @@
|
||||
import { useChatStore } from "@/stores/chatStore";
|
||||
import { convertFileSrc } from "@tauri-apps/api/core";
|
||||
import { filesize } from "filesize";
|
||||
import { X } from "lucide-react";
|
||||
|
||||
const FileList = () => {
|
||||
const uploadFiles = useChatStore((state) => state.uploadFiles);
|
||||
const setUploadFiles = useChatStore((state) => state.setUploadFiles);
|
||||
|
||||
const deleteFile = (id: string) => {
|
||||
setUploadFiles(uploadFiles.filter((file) => file.id !== id));
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-wrap gap-y-2 -mx-1 text-sm">
|
||||
{uploadFiles.map((file) => {
|
||||
const { id, icon, name, extname, size } = file;
|
||||
|
||||
return (
|
||||
<div key={id} className="w-1/3 px-1">
|
||||
<div className="relative group flex items-center rounded-sm bg-black/10 p-1">
|
||||
<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);
|
||||
}}
|
||||
>
|
||||
<X className="size-[10px] text-white" />
|
||||
</div>
|
||||
|
||||
<img src={convertFileSrc(icon)} className="size-[40px]" />
|
||||
|
||||
<div className="flex-1 flex flex-col justify-between">
|
||||
<div className="truncate">{name}</div>
|
||||
|
||||
<div className="text-xs text-black/60">
|
||||
<div className="flex gap-2">
|
||||
{extname && <span>{extname}</span>}
|
||||
<span>
|
||||
{filesize(size, { standard: "jedec", spacer: "" })}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default FileList;
|
||||
@@ -32,6 +32,7 @@ import { metaOrCtrlKey } from "@/utils/keyboardUtils";
|
||||
import { useConnectStore } from "@/stores/connectStore";
|
||||
import TypeIcon from "@/components/Common/Icons/TypeIcon";
|
||||
import { isArray } from "lodash-es";
|
||||
import InputExtra from "./InputExtra";
|
||||
|
||||
interface ChatInputProps {
|
||||
onSend: (message: string) => void;
|
||||
@@ -448,8 +449,10 @@ export default function ChatInput({
|
||||
>
|
||||
{isChatMode ? (
|
||||
<div className="flex gap-2 text-xs text-[#333] dark:text-[#d8d8d8]">
|
||||
<InputExtra />
|
||||
|
||||
<button
|
||||
className={`h-5 px-2 inline-flex items-center border rounded-[10px] transition-colors relative ${
|
||||
className={`h-5 px-2 inline-flex justify-center items-center gap-1 border rounded-[10px] transition-colors relative ${
|
||||
isDeepThinkActive
|
||||
? "bg-[rgba(0,114,255,0.3)] border-[rgba(0,114,255,0.3)]"
|
||||
: "border-[#262727]"
|
||||
@@ -457,12 +460,13 @@ export default function ChatInput({
|
||||
onClick={DeepThinkClick}
|
||||
>
|
||||
<Brain
|
||||
className={`w-3 h-3 mr-1 ${
|
||||
className={`size-3 ${
|
||||
isDeepThinkActive
|
||||
? "text-[#0072FF] dark:text-[#0072FF]"
|
||||
: "text-[#333] dark:text-white"
|
||||
}`}
|
||||
/>
|
||||
{isDeepThinkActive && (
|
||||
<span
|
||||
className={
|
||||
isDeepThinkActive ? "text-[#0072FF]" : "dark:text-white"
|
||||
@@ -470,9 +474,11 @@ export default function ChatInput({
|
||||
>
|
||||
{t("search.input.deepThink")}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
|
||||
<div
|
||||
className={`h-5 px-2 inline-flex items-center border rounded-[10px] transition-colors relative cursor-pointer ${
|
||||
className={`h-5 px-2 inline-flex items-center justify-center gap-1 border rounded-[10px] transition-colors relative cursor-pointer ${
|
||||
isSearchActive
|
||||
? "bg-[rgba(0,114,255,0.3)] border-[rgba(0,114,255,0.3)]"
|
||||
: "border-[#262727]"
|
||||
@@ -480,13 +486,15 @@ export default function ChatInput({
|
||||
onClick={SearchClick}
|
||||
>
|
||||
<Globe
|
||||
className={`w-3 h-3 mr-1 ${
|
||||
className={`size-3 ${
|
||||
isSearchActive
|
||||
? "text-[#0072FF] dark:text-[#0072FF]"
|
||||
: "text-[#333] dark:text-white"
|
||||
}`}
|
||||
/>
|
||||
|
||||
{isSearchActive && (
|
||||
<>
|
||||
<span
|
||||
className={
|
||||
isSearchActive ? "text-[#0072FF]" : "dark:text-white"
|
||||
@@ -553,7 +561,10 @@ export default function ChatInput({
|
||||
{isAll ? (
|
||||
<Layers className="size-[16px] text-[#0287FF]" />
|
||||
) : (
|
||||
<TypeIcon item={item} className="size-[16px]" />
|
||||
<TypeIcon
|
||||
item={item}
|
||||
className="size-[16px]"
|
||||
/>
|
||||
)}
|
||||
|
||||
<span>{isAll ? t(name) : name}</span>
|
||||
@@ -586,6 +597,8 @@ export default function ChatInput({
|
||||
</div>
|
||||
</PopoverPanel>
|
||||
</Popover>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/*<button*/}
|
||||
|
||||
88
src/components/Search/InputExtra.tsx
Normal file
88
src/components/Search/InputExtra.tsx
Normal file
@@ -0,0 +1,88 @@
|
||||
import { Plus } from "lucide-react";
|
||||
import { Menu, MenuButton, MenuItem, MenuItems } from "@headlessui/react";
|
||||
import { open } from "@tauri-apps/plugin-dialog";
|
||||
import { find, isNil } from "lodash-es";
|
||||
import { useChatStore } from "@/stores/chatStore";
|
||||
import { metadata, icon } from "tauri-plugin-fs-pro-api";
|
||||
import { nanoid } from "nanoid";
|
||||
import Tooltip from "../Common/Tooltip";
|
||||
|
||||
const InputExtra = () => {
|
||||
const uploadFiles = useChatStore((state) => state.uploadFiles);
|
||||
const setUploadFiles = useChatStore((state) => state.setUploadFiles);
|
||||
|
||||
const uploadFile = async () => {
|
||||
const selectedFiles = await open({
|
||||
multiple: true,
|
||||
});
|
||||
|
||||
if (isNil(selectedFiles)) return;
|
||||
|
||||
const files: typeof uploadFiles = [];
|
||||
|
||||
for await (const path of selectedFiles) {
|
||||
if (find(uploadFiles, { path })) continue;
|
||||
|
||||
const stat = await metadata(path);
|
||||
|
||||
if (stat.size / 1024 / 1024 > 100) {
|
||||
continue;
|
||||
}
|
||||
|
||||
files.push({
|
||||
...stat,
|
||||
id: nanoid(),
|
||||
path,
|
||||
icon: await icon(path),
|
||||
});
|
||||
}
|
||||
|
||||
console.log("files", files);
|
||||
|
||||
setUploadFiles([...uploadFiles, ...files]);
|
||||
};
|
||||
|
||||
const menuItems = [
|
||||
{
|
||||
label: "上传文件",
|
||||
event: uploadFile,
|
||||
},
|
||||
// {
|
||||
// label: "截取屏幕截图",
|
||||
// event: () => {},
|
||||
// },
|
||||
];
|
||||
|
||||
return (
|
||||
<Menu>
|
||||
<MenuButton>
|
||||
<Tooltip content="支持截图、上传文件,最多 50个,单个文件最大 100 MB。">
|
||||
<div className="group h-5 px-2 flex justify-center items-center border rounded-[10px] transition-colors relative border-[#262727] hover:bg-[rgba(0,114,255,0.3)] hover:border-[rgba(0,114,255,0.3)]">
|
||||
<Plus className="size-3 text-[#333] dark:text-white group-hover:text-[#0072FF] hover:dark:text-[#0072FF]" />
|
||||
</div>
|
||||
</Tooltip>
|
||||
</MenuButton>
|
||||
<MenuItems
|
||||
anchor="bottom start"
|
||||
className="p-1 text-sm bg-white dark:bg-[#202126] rounded-lg shadow-xs border border-gray-200 dark:border-gray-700"
|
||||
>
|
||||
{menuItems.map((item) => {
|
||||
const { label, event } = item;
|
||||
|
||||
return (
|
||||
<MenuItem key={label}>
|
||||
<div
|
||||
className="px-3 py-2 hover:bg-black/5 hover:dark:bg-white/5 rounded-lg cursor-pointer"
|
||||
onClick={event}
|
||||
>
|
||||
{label}
|
||||
</div>
|
||||
</MenuItem>
|
||||
);
|
||||
})}
|
||||
</MenuItems>
|
||||
</Menu>
|
||||
);
|
||||
};
|
||||
|
||||
export default InputExtra;
|
||||
@@ -3,6 +3,13 @@ import {
|
||||
persist,
|
||||
// createJSONStorage
|
||||
} from "zustand/middleware";
|
||||
import { Metadata } from "tauri-plugin-fs-pro-api";
|
||||
|
||||
interface UploadFile extends Metadata {
|
||||
id: string;
|
||||
path: string;
|
||||
icon: string;
|
||||
}
|
||||
|
||||
export type IChatStore = {
|
||||
curChatEnd: boolean;
|
||||
@@ -13,6 +20,8 @@ export type IChatStore = {
|
||||
setConnected: (value: boolean) => void;
|
||||
messages: string;
|
||||
setMessages: (value: string | ((prev: string) => string)) => void;
|
||||
uploadFiles: UploadFile[];
|
||||
setUploadFiles: (value: UploadFile[]) => void;
|
||||
};
|
||||
|
||||
export const useChatStore = create<IChatStore>()(
|
||||
@@ -29,6 +38,10 @@ export const useChatStore = create<IChatStore>()(
|
||||
set((state) => ({
|
||||
messages: typeof value === "function" ? value(state.messages) : value,
|
||||
})),
|
||||
uploadFiles: [],
|
||||
setUploadFiles: (uploadFiles: UploadFile[]) => {
|
||||
return set(() => ({ uploadFiles }));
|
||||
},
|
||||
}),
|
||||
{
|
||||
name: "chat-state",
|
||||
|
||||
Reference in New Issue
Block a user