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",
|
"axios": "^1.7.7",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"dotenv": "^16.4.7",
|
"dotenv": "^16.4.7",
|
||||||
|
"filesize": "^10.1.6",
|
||||||
"i18next": "^23.16.2",
|
"i18next": "^23.16.2",
|
||||||
"i18next-browser-languagedetector": "^8.0.3",
|
"i18next-browser-languagedetector": "^8.0.3",
|
||||||
"lodash-es": "^4.17.21",
|
"lodash-es": "^4.17.21",
|
||||||
"lucide-react": "^0.461.0",
|
"lucide-react": "^0.461.0",
|
||||||
"mermaid": "^11.4.0",
|
"mermaid": "^11.4.0",
|
||||||
|
"nanoid": "^5.1.2",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-hotkeys-hook": "^4.5.1",
|
"react-hotkeys-hook": "^4.5.1",
|
||||||
@@ -46,6 +48,7 @@
|
|||||||
"remark-breaks": "^4.0.0",
|
"remark-breaks": "^4.0.0",
|
||||||
"remark-gfm": "^4.0.0",
|
"remark-gfm": "^4.0.0",
|
||||||
"remark-math": "^6.0.0",
|
"remark-math": "^6.0.0",
|
||||||
|
"tauri-plugin-fs-pro-api": "^2.3.1",
|
||||||
"use-debounce": "^10.0.4",
|
"use-debounce": "^10.0.4",
|
||||||
"uuid": "^11.0.3",
|
"uuid": "^11.0.3",
|
||||||
"zustand": "^5.0.0"
|
"zustand": "^5.0.0"
|
||||||
|
|||||||
29
pnpm-lock.yaml
generated
29
pnpm-lock.yaml
generated
@@ -56,6 +56,9 @@ importers:
|
|||||||
dotenv:
|
dotenv:
|
||||||
specifier: ^16.4.7
|
specifier: ^16.4.7
|
||||||
version: 16.4.7
|
version: 16.4.7
|
||||||
|
filesize:
|
||||||
|
specifier: ^10.1.6
|
||||||
|
version: 10.1.6
|
||||||
i18next:
|
i18next:
|
||||||
specifier: ^23.16.2
|
specifier: ^23.16.2
|
||||||
version: 23.16.2
|
version: 23.16.2
|
||||||
@@ -71,6 +74,9 @@ importers:
|
|||||||
mermaid:
|
mermaid:
|
||||||
specifier: ^11.4.0
|
specifier: ^11.4.0
|
||||||
version: 11.4.0
|
version: 11.4.0
|
||||||
|
nanoid:
|
||||||
|
specifier: ^5.1.2
|
||||||
|
version: 5.1.2
|
||||||
react:
|
react:
|
||||||
specifier: ^18.2.0
|
specifier: ^18.2.0
|
||||||
version: 18.3.1
|
version: 18.3.1
|
||||||
@@ -107,6 +113,9 @@ importers:
|
|||||||
remark-math:
|
remark-math:
|
||||||
specifier: ^6.0.0
|
specifier: ^6.0.0
|
||||||
version: 6.0.0
|
version: 6.0.0
|
||||||
|
tauri-plugin-fs-pro-api:
|
||||||
|
specifier: ^2.3.1
|
||||||
|
version: 2.3.1
|
||||||
use-debounce:
|
use-debounce:
|
||||||
specifier: ^10.0.4
|
specifier: ^10.0.4
|
||||||
version: 10.0.4(react@18.3.1)
|
version: 10.0.4(react@18.3.1)
|
||||||
@@ -1879,6 +1888,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==}
|
resolution: {integrity: sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
|
filesize@10.1.6:
|
||||||
|
resolution: {integrity: sha512-sJslQKU2uM33qH5nqewAwVB2QgR6w1aMNsYUp3aN5rMRyXEwJGmZvaWzeJFNTOXWlHQyBFCWrdj3fV/fsTOX8w==}
|
||||||
|
engines: {node: '>= 10.4.0'}
|
||||||
|
|
||||||
fill-range@7.1.1:
|
fill-range@7.1.1:
|
||||||
resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==}
|
resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
@@ -2556,6 +2569,11 @@ packages:
|
|||||||
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
|
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
nanoid@5.1.2:
|
||||||
|
resolution: {integrity: sha512-b+CiXQCNMUGe0Ri64S9SXFcP9hogjAJ2Rd6GdVxhPLRm7mhGaM7VgOvCAJ1ZshfHbqVDI3uqTI5C8/GaKuLI7g==}
|
||||||
|
engines: {node: ^18 || >=20}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
netmask@2.0.2:
|
netmask@2.0.2:
|
||||||
resolution: {integrity: sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==}
|
resolution: {integrity: sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==}
|
||||||
engines: {node: '>= 0.4.0'}
|
engines: {node: '>= 0.4.0'}
|
||||||
@@ -3087,6 +3105,9 @@ packages:
|
|||||||
engines: {node: '>=14.0.0'}
|
engines: {node: '>=14.0.0'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
tauri-plugin-fs-pro-api@2.3.1:
|
||||||
|
resolution: {integrity: sha512-fx/zITX9MWoDZ603FKWSybluZqJUEOvHU+H6kj3iRJNyoGFHoNkajpQbiK5cu81spQbGBlP9sV2HkaCI07gQ+Q==}
|
||||||
|
|
||||||
thenify-all@1.6.0:
|
thenify-all@1.6.0:
|
||||||
resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==}
|
resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==}
|
||||||
engines: {node: '>=0.8'}
|
engines: {node: '>=0.8'}
|
||||||
@@ -5047,6 +5068,8 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
is-unicode-supported: 2.1.0
|
is-unicode-supported: 2.1.0
|
||||||
|
|
||||||
|
filesize@10.1.6: {}
|
||||||
|
|
||||||
fill-range@7.1.1:
|
fill-range@7.1.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
to-regex-range: 5.0.1
|
to-regex-range: 5.0.1
|
||||||
@@ -5963,6 +5986,8 @@ snapshots:
|
|||||||
|
|
||||||
nanoid@3.3.7: {}
|
nanoid@3.3.7: {}
|
||||||
|
|
||||||
|
nanoid@5.1.2: {}
|
||||||
|
|
||||||
netmask@2.0.2: {}
|
netmask@2.0.2: {}
|
||||||
|
|
||||||
new-github-release-url@2.0.0:
|
new-github-release-url@2.0.0:
|
||||||
@@ -6602,6 +6627,10 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- ts-node
|
- ts-node
|
||||||
|
|
||||||
|
tauri-plugin-fs-pro-api@2.3.1:
|
||||||
|
dependencies:
|
||||||
|
'@tauri-apps/api': 2.2.0
|
||||||
|
|
||||||
thenify-all@1.6.0:
|
thenify-all@1.6.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
thenify: 3.3.1
|
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-updater = "2"
|
||||||
tauri-plugin-process = "2"
|
tauri-plugin-process = "2"
|
||||||
tauri-plugin-drag = "2"
|
tauri-plugin-drag = "2"
|
||||||
|
tauri-plugin-fs-pro = "2"
|
||||||
|
|
||||||
tokio-native-tls = "0.3" # For wss connections
|
tokio-native-tls = "0.3" # For wss connections
|
||||||
tokio = { version = "1", features = ["full"] }
|
tokio = { version = "1", features = ["full"] }
|
||||||
|
|||||||
@@ -62,6 +62,8 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"deny": []
|
"deny": []
|
||||||
}
|
},
|
||||||
|
"dialog:default",
|
||||||
|
"fs-pro:default"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,13 @@
|
|||||||
|
mod assistant;
|
||||||
mod autostart;
|
mod autostart;
|
||||||
mod common;
|
mod common;
|
||||||
mod local;
|
mod local;
|
||||||
mod search;
|
mod search;
|
||||||
mod server;
|
mod server;
|
||||||
|
mod setup;
|
||||||
mod shortcut;
|
mod shortcut;
|
||||||
mod util;
|
mod util;
|
||||||
|
|
||||||
mod setup;
|
|
||||||
mod assistant;
|
|
||||||
|
|
||||||
use crate::common::register::SearchSourceRegistry;
|
use crate::common::register::SearchSourceRegistry;
|
||||||
// use crate::common::traits::SearchSource;
|
// use crate::common::traits::SearchSource;
|
||||||
use crate::common::{MAIN_WINDOW_LABEL, SETTINGS_WINDOW_LABEL};
|
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_theme::init(ctx.config_mut()))
|
||||||
.plugin(tauri_plugin_deep_link::init())
|
.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
|
// Conditional compilation for macOS
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import { Sidebar } from "@/components/Assistant/Sidebar";
|
|||||||
import { useConnectStore } from "@/stores/connectStore";
|
import { useConnectStore } from "@/stores/connectStore";
|
||||||
import { useSearchStore } from "@/stores/searchStore";
|
import { useSearchStore } from "@/stores/searchStore";
|
||||||
import { IServer } from "@/stores/appStore";
|
import { IServer } from "@/stores/appStore";
|
||||||
|
import FileList from "../Search/FileList";
|
||||||
|
|
||||||
interface ChatAIProps {
|
interface ChatAIProps {
|
||||||
isTransitioned: boolean;
|
isTransitioned: boolean;
|
||||||
@@ -91,6 +92,7 @@ const ChatAI = memo(
|
|||||||
const [isSidebarOpenChat, setIsSidebarOpenChat] = useState(isSidebarOpen);
|
const [isSidebarOpenChat, setIsSidebarOpenChat] = useState(isSidebarOpen);
|
||||||
const [chats, setChats] = useState<Chat[]>([]);
|
const [chats, setChats] = useState<Chat[]>([]);
|
||||||
const sourceDataIds = useSearchStore((state) => state.sourceDataIds);
|
const sourceDataIds = useSearchStore((state) => state.sourceDataIds);
|
||||||
|
const uploadFiles = useChatStore((state) => state.uploadFiles);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
activeChatProp && setActiveChat(activeChatProp);
|
activeChatProp && setActiveChat(activeChatProp);
|
||||||
@@ -641,7 +643,6 @@ const ChatAI = memo(
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<ChatHeader
|
<ChatHeader
|
||||||
onCreateNewChat={clearChat}
|
onCreateNewChat={clearChat}
|
||||||
onOpenChatAI={openChatAI}
|
onOpenChatAI={openChatAI}
|
||||||
@@ -653,90 +654,92 @@ const ChatAI = memo(
|
|||||||
activeChat={activeChat}
|
activeChat={activeChat}
|
||||||
reconnect={reconnect}
|
reconnect={reconnect}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Chat messages */}
|
{/* 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">
|
||||||
<ChatMessage
|
<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">
|
||||||
key={"greetings"}
|
|
||||||
message={{
|
|
||||||
_id: "greetings",
|
|
||||||
_source: {
|
|
||||||
type: "assistant",
|
|
||||||
message: t("assistant.chat.greetings"),
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{activeChat?.messages?.map((message, index) => (
|
|
||||||
<ChatMessage
|
<ChatMessage
|
||||||
key={message._id + index}
|
key={"greetings"}
|
||||||
message={message}
|
|
||||||
isTyping={false}
|
|
||||||
onResend={handleSendMessage}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
|
|
||||||
{(query_intent ||
|
|
||||||
fetch_source ||
|
|
||||||
pick_source ||
|
|
||||||
deep_read ||
|
|
||||||
think ||
|
|
||||||
response) &&
|
|
||||||
activeChat?._id ? (
|
|
||||||
<ChatMessage
|
|
||||||
key={"current"}
|
|
||||||
message={{
|
message={{
|
||||||
_id: "current",
|
_id: "greetings",
|
||||||
_source: {
|
_source: {
|
||||||
type: "assistant",
|
type: "assistant",
|
||||||
message: "",
|
message: t("assistant.chat.greetings"),
|
||||||
question: Question,
|
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
onResend={handleSendMessage}
|
|
||||||
isTyping={!curChatEnd}
|
|
||||||
query_intent={query_intent}
|
|
||||||
fetch_source={fetch_source}
|
|
||||||
pick_source={pick_source}
|
|
||||||
deep_read={deep_read}
|
|
||||||
think={think}
|
|
||||||
response={response}
|
|
||||||
/>
|
/>
|
||||||
) : null}
|
{activeChat?.messages?.map((message, index) => (
|
||||||
|
<ChatMessage
|
||||||
|
key={message._id + index}
|
||||||
|
message={message}
|
||||||
|
isTyping={false}
|
||||||
|
onResend={handleSendMessage}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
{(query_intent ||
|
||||||
|
fetch_source ||
|
||||||
|
pick_source ||
|
||||||
|
deep_read ||
|
||||||
|
think ||
|
||||||
|
response) &&
|
||||||
|
activeChat?._id ? (
|
||||||
|
<ChatMessage
|
||||||
|
key={"current"}
|
||||||
|
message={{
|
||||||
|
_id: "current",
|
||||||
|
_source: {
|
||||||
|
type: "assistant",
|
||||||
|
message: "",
|
||||||
|
question: Question,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
onResend={handleSendMessage}
|
||||||
|
isTyping={!curChatEnd}
|
||||||
|
query_intent={query_intent}
|
||||||
|
fetch_source={fetch_source}
|
||||||
|
pick_source={pick_source}
|
||||||
|
deep_read={deep_read}
|
||||||
|
think={think}
|
||||||
|
response={response}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
{timedoutShow ? (
|
||||||
|
<ChatMessage
|
||||||
|
key={"timedout"}
|
||||||
|
message={{
|
||||||
|
_id: "timedout",
|
||||||
|
_source: {
|
||||||
|
type: "assistant",
|
||||||
|
message: t("assistant.chat.timedout"),
|
||||||
|
question: Question,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
onResend={handleSendMessage}
|
||||||
|
isTyping={false}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
{errorShow ? (
|
||||||
|
<ChatMessage
|
||||||
|
key={"error"}
|
||||||
|
message={{
|
||||||
|
_id: "error",
|
||||||
|
_source: {
|
||||||
|
type: "assistant",
|
||||||
|
message: t("assistant.chat.error"),
|
||||||
|
question: Question,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
onResend={handleSendMessage}
|
||||||
|
isTyping={false}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
<div ref={messagesEndRef} />
|
||||||
|
</div>
|
||||||
|
|
||||||
{timedoutShow ? (
|
{uploadFiles.length > 0 && (
|
||||||
<ChatMessage
|
<div className="max-h-[120px] overflow-auto p-2 border-t border-[#E6E6E6] cl">
|
||||||
key={"timedout"}
|
<FileList />
|
||||||
message={{
|
</div>
|
||||||
_id: "timedout",
|
)}
|
||||||
_source: {
|
|
||||||
type: "assistant",
|
|
||||||
message: t("assistant.chat.timedout"),
|
|
||||||
question: Question,
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
onResend={handleSendMessage}
|
|
||||||
isTyping={false}
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
{errorShow ? (
|
|
||||||
<ChatMessage
|
|
||||||
key={"error"}
|
|
||||||
message={{
|
|
||||||
_id: "error",
|
|
||||||
_source: {
|
|
||||||
type: "assistant",
|
|
||||||
message: t("assistant.chat.error"),
|
|
||||||
question: Question,
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
onResend={handleSendMessage}
|
|
||||||
isTyping={false}
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
<div ref={messagesEndRef} />
|
|
||||||
</div>
|
</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 { useConnectStore } from "@/stores/connectStore";
|
||||||
import TypeIcon from "@/components/Common/Icons/TypeIcon";
|
import TypeIcon from "@/components/Common/Icons/TypeIcon";
|
||||||
import { isArray } from "lodash-es";
|
import { isArray } from "lodash-es";
|
||||||
|
import InputExtra from "./InputExtra";
|
||||||
|
|
||||||
interface ChatInputProps {
|
interface ChatInputProps {
|
||||||
onSend: (message: string) => void;
|
onSend: (message: string) => void;
|
||||||
@@ -448,8 +449,10 @@ export default function ChatInput({
|
|||||||
>
|
>
|
||||||
{isChatMode ? (
|
{isChatMode ? (
|
||||||
<div className="flex gap-2 text-xs text-[#333] dark:text-[#d8d8d8]">
|
<div className="flex gap-2 text-xs text-[#333] dark:text-[#d8d8d8]">
|
||||||
|
<InputExtra />
|
||||||
|
|
||||||
<button
|
<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
|
isDeepThinkActive
|
||||||
? "bg-[rgba(0,114,255,0.3)] border-[rgba(0,114,255,0.3)]"
|
? "bg-[rgba(0,114,255,0.3)] border-[rgba(0,114,255,0.3)]"
|
||||||
: "border-[#262727]"
|
: "border-[#262727]"
|
||||||
@@ -457,22 +460,25 @@ export default function ChatInput({
|
|||||||
onClick={DeepThinkClick}
|
onClick={DeepThinkClick}
|
||||||
>
|
>
|
||||||
<Brain
|
<Brain
|
||||||
className={`w-3 h-3 mr-1 ${
|
className={`size-3 ${
|
||||||
isDeepThinkActive
|
isDeepThinkActive
|
||||||
? "text-[#0072FF] dark:text-[#0072FF]"
|
? "text-[#0072FF] dark:text-[#0072FF]"
|
||||||
: "text-[#333] dark:text-white"
|
: "text-[#333] dark:text-white"
|
||||||
}`}
|
}`}
|
||||||
/>
|
/>
|
||||||
<span
|
{isDeepThinkActive && (
|
||||||
className={
|
<span
|
||||||
isDeepThinkActive ? "text-[#0072FF]" : "dark:text-white"
|
className={
|
||||||
}
|
isDeepThinkActive ? "text-[#0072FF]" : "dark:text-white"
|
||||||
>
|
}
|
||||||
{t("search.input.deepThink")}
|
>
|
||||||
</span>
|
{t("search.input.deepThink")}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div
|
<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
|
isSearchActive
|
||||||
? "bg-[rgba(0,114,255,0.3)] border-[rgba(0,114,255,0.3)]"
|
? "bg-[rgba(0,114,255,0.3)] border-[rgba(0,114,255,0.3)]"
|
||||||
: "border-[#262727]"
|
: "border-[#262727]"
|
||||||
@@ -480,112 +486,119 @@ export default function ChatInput({
|
|||||||
onClick={SearchClick}
|
onClick={SearchClick}
|
||||||
>
|
>
|
||||||
<Globe
|
<Globe
|
||||||
className={`w-3 h-3 mr-1 ${
|
className={`size-3 ${
|
||||||
isSearchActive
|
isSearchActive
|
||||||
? "text-[#0072FF] dark:text-[#0072FF]"
|
? "text-[#0072FF] dark:text-[#0072FF]"
|
||||||
: "text-[#333] dark:text-white"
|
: "text-[#333] dark:text-white"
|
||||||
}`}
|
}`}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<span
|
{isSearchActive && (
|
||||||
className={
|
<>
|
||||||
isSearchActive ? "text-[#0072FF]" : "dark:text-white"
|
<span
|
||||||
}
|
className={
|
||||||
>
|
isSearchActive ? "text-[#0072FF]" : "dark:text-white"
|
||||||
{t("search.input.search")}
|
}
|
||||||
</span>
|
|
||||||
|
|
||||||
<Popover>
|
|
||||||
<PopoverButton className={clsx("flex items-center")}>
|
|
||||||
<ChevronDownIcon
|
|
||||||
className={clsx("size-4", [
|
|
||||||
isSearchActive
|
|
||||||
? "text-[#0072FF] dark:text-[#0072FF]"
|
|
||||||
: "text-[#333] dark:text-white",
|
|
||||||
])}
|
|
||||||
/>
|
|
||||||
</PopoverButton>
|
|
||||||
|
|
||||||
<PopoverPanel
|
|
||||||
anchor="top start"
|
|
||||||
className="min-w-[220px] bg-white dark:bg-[#202126] rounded-lg shadow-lg border border-gray-200 dark:border-gray-700"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className="text-sm px-[12px] py-[18px]"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<div className="flex justify-between mb-[18px]">
|
{t("search.input.search")}
|
||||||
<span>{t("search.input.searchPopover.title")}</span>
|
</span>
|
||||||
|
|
||||||
|
<Popover>
|
||||||
|
<PopoverButton className={clsx("flex items-center")}>
|
||||||
|
<ChevronDownIcon
|
||||||
|
className={clsx("size-4", [
|
||||||
|
isSearchActive
|
||||||
|
? "text-[#0072FF] dark:text-[#0072FF]"
|
||||||
|
: "text-[#333] dark:text-white",
|
||||||
|
])}
|
||||||
|
/>
|
||||||
|
</PopoverButton>
|
||||||
|
|
||||||
|
<PopoverPanel
|
||||||
|
anchor="top start"
|
||||||
|
className="min-w-[220px] bg-white dark:bg-[#202126] rounded-lg shadow-lg border border-gray-200 dark:border-gray-700"
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
onClick={async () => {
|
className="text-sm px-[12px] py-[18px]"
|
||||||
setIsRefreshDataSource(true);
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
getDataSourceList();
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
setIsRefreshDataSource(false);
|
|
||||||
}, 1000);
|
|
||||||
}}
|
}}
|
||||||
className="size-[24px] flex justify-center items-center rounded-lg border border-black/10 dark:border-white/10"
|
|
||||||
>
|
>
|
||||||
<RefreshCw
|
<div className="flex justify-between mb-[18px]">
|
||||||
className={`size-3 text-[#0287FF] transition-transform duration-1000 ${
|
<span>{t("search.input.searchPopover.title")}</span>
|
||||||
isRefreshDataSource ? "animate-spin" : ""
|
|
||||||
}`}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<ul className="flex flex-col gap-[16px]">
|
|
||||||
{state.dataSourceList?.map((item, index) => {
|
|
||||||
const { id, name } = item;
|
|
||||||
|
|
||||||
const isAll = index === 0;
|
<div
|
||||||
|
onClick={async () => {
|
||||||
|
setIsRefreshDataSource(true);
|
||||||
|
|
||||||
return (
|
getDataSourceList();
|
||||||
<li
|
|
||||||
key={id}
|
setTimeout(() => {
|
||||||
className="flex justify-between items-center"
|
setIsRefreshDataSource(false);
|
||||||
|
}, 1000);
|
||||||
|
}}
|
||||||
|
className="size-[24px] flex justify-center items-center rounded-lg border border-black/10 dark:border-white/10"
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-[8px]">
|
<RefreshCw
|
||||||
{isAll ? (
|
className={`size-3 text-[#0287FF] transition-transform duration-1000 ${
|
||||||
<Layers className="size-[16px] text-[#0287FF]" />
|
isRefreshDataSource ? "animate-spin" : ""
|
||||||
) : (
|
}`}
|
||||||
<TypeIcon item={item} className="size-[16px]" />
|
/>
|
||||||
)}
|
</div>
|
||||||
|
</div>
|
||||||
|
<ul className="flex flex-col gap-[16px]">
|
||||||
|
{state.dataSourceList?.map((item, index) => {
|
||||||
|
const { id, name } = item;
|
||||||
|
|
||||||
<span>{isAll ? t(name) : name}</span>
|
const isAll = index === 0;
|
||||||
</div>
|
|
||||||
|
|
||||||
<Checkbox
|
return (
|
||||||
checked={
|
<li
|
||||||
isAll
|
key={id}
|
||||||
? sourceDataIds.length ===
|
className="flex justify-between items-center"
|
||||||
state.dataSourceList.length - 1
|
>
|
||||||
: sourceDataIds?.includes(id)
|
<div className="flex items-center gap-[8px]">
|
||||||
}
|
{isAll ? (
|
||||||
onChange={(value) =>
|
<Layers className="size-[16px] text-[#0287FF]" />
|
||||||
onSelectDataSource(id, value, isAll)
|
) : (
|
||||||
}
|
<TypeIcon
|
||||||
className="group size-[14px] rounded-sm border border-black/30 dark:border-white/30 data-[checked]:bg-[#2F54EB] data-[checked]:!border-[#2F54EB] transition"
|
item={item}
|
||||||
>
|
className="size-[16px]"
|
||||||
{isAll && (
|
/>
|
||||||
<div className="size-full flex items-center justify-center group-data-[checked]:hidden">
|
)}
|
||||||
<div className="size-[6px] bg-[#2F54EB]"></div>
|
|
||||||
|
<span>{isAll ? t(name) : name}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
|
|
||||||
<CheckIcon className="hidden size-[12px] text-white group-data-[checked]:block" />
|
<Checkbox
|
||||||
</Checkbox>
|
checked={
|
||||||
</li>
|
isAll
|
||||||
);
|
? sourceDataIds.length ===
|
||||||
})}
|
state.dataSourceList.length - 1
|
||||||
</ul>
|
: sourceDataIds?.includes(id)
|
||||||
</div>
|
}
|
||||||
</PopoverPanel>
|
onChange={(value) =>
|
||||||
</Popover>
|
onSelectDataSource(id, value, isAll)
|
||||||
|
}
|
||||||
|
className="group size-[14px] rounded-sm border border-black/30 dark:border-white/30 data-[checked]:bg-[#2F54EB] data-[checked]:!border-[#2F54EB] transition"
|
||||||
|
>
|
||||||
|
{isAll && (
|
||||||
|
<div className="size-full flex items-center justify-center group-data-[checked]:hidden">
|
||||||
|
<div className="size-[6px] bg-[#2F54EB]"></div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<CheckIcon className="hidden size-[12px] text-white group-data-[checked]:block" />
|
||||||
|
</Checkbox>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</PopoverPanel>
|
||||||
|
</Popover>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/*<button*/}
|
{/*<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,
|
persist,
|
||||||
// createJSONStorage
|
// createJSONStorage
|
||||||
} from "zustand/middleware";
|
} from "zustand/middleware";
|
||||||
|
import { Metadata } from "tauri-plugin-fs-pro-api";
|
||||||
|
|
||||||
|
interface UploadFile extends Metadata {
|
||||||
|
id: string;
|
||||||
|
path: string;
|
||||||
|
icon: string;
|
||||||
|
}
|
||||||
|
|
||||||
export type IChatStore = {
|
export type IChatStore = {
|
||||||
curChatEnd: boolean;
|
curChatEnd: boolean;
|
||||||
@@ -13,6 +20,8 @@ export type IChatStore = {
|
|||||||
setConnected: (value: boolean) => void;
|
setConnected: (value: boolean) => void;
|
||||||
messages: string;
|
messages: string;
|
||||||
setMessages: (value: string | ((prev: string) => string)) => void;
|
setMessages: (value: string | ((prev: string) => string)) => void;
|
||||||
|
uploadFiles: UploadFile[];
|
||||||
|
setUploadFiles: (value: UploadFile[]) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useChatStore = create<IChatStore>()(
|
export const useChatStore = create<IChatStore>()(
|
||||||
@@ -29,6 +38,10 @@ export const useChatStore = create<IChatStore>()(
|
|||||||
set((state) => ({
|
set((state) => ({
|
||||||
messages: typeof value === "function" ? value(state.messages) : value,
|
messages: typeof value === "function" ? value(state.messages) : value,
|
||||||
})),
|
})),
|
||||||
|
uploadFiles: [],
|
||||||
|
setUploadFiles: (uploadFiles: UploadFile[]) => {
|
||||||
|
return set(() => ({ uploadFiles }));
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
name: "chat-state",
|
name: "chat-state",
|
||||||
|
|||||||
Reference in New Issue
Block a user