mirror of
https://github.com/infinilabs/coco-app.git
synced 2025-12-16 11:37:47 +01:00
feat: support for uploading files to the server (#310)
* feat: support for uploading files to the server * feat: field Internationalization * refactor: encapsulation attachment-related requests * feat: support for getting a list of attachments that have been uploaded for a session * feat: the session displays the number and list of uploaded files * feat: internalization * feat: wrapping the Checkbox component * feat: add checkbox * feat: support for deleting uploaded files * feat: support for selecting uploaded files * refactor: optimize the display of file icons * refactor: hide file uploads when there is no sessionId
This commit is contained in:
@@ -18,6 +18,7 @@
|
||||
"release-beta": "release-it --preRelease=beta --preReleaseBase=1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ant-design/icons": "^6.0.0",
|
||||
"@headlessui/react": "^2.2.0",
|
||||
"@tauri-apps/api": "^2.4.0",
|
||||
"@tauri-apps/plugin-autostart": "~2.2.0",
|
||||
|
||||
59
pnpm-lock.yaml
generated
59
pnpm-lock.yaml
generated
@@ -8,6 +8,9 @@ importers:
|
||||
|
||||
.:
|
||||
dependencies:
|
||||
'@ant-design/icons':
|
||||
specifier: ^6.0.0
|
||||
version: 6.0.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
'@headlessui/react':
|
||||
specifier: ^2.2.0
|
||||
version: 2.2.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
@@ -206,6 +209,23 @@ packages:
|
||||
resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==}
|
||||
engines: {node: '>=6.0.0'}
|
||||
|
||||
'@ant-design/colors@8.0.0':
|
||||
resolution: {integrity: sha512-6YzkKCw30EI/E9kHOIXsQDHmMvTllT8STzjMb4K2qzit33RW2pqCJP0sk+hidBntXxE+Vz4n1+RvCTfBw6OErw==}
|
||||
|
||||
'@ant-design/fast-color@3.0.0':
|
||||
resolution: {integrity: sha512-eqvpP7xEDm2S7dUzl5srEQCBTXZMmY3ekf97zI+M2DHOYyKdJGH0qua0JACHTqbkRnD/KHFQP9J1uMJ/XWVzzA==}
|
||||
engines: {node: '>=8.x'}
|
||||
|
||||
'@ant-design/icons-svg@4.4.2':
|
||||
resolution: {integrity: sha512-vHbT+zJEVzllwP+CM+ul7reTEfBR0vgxFe7+lREAsAA7YGsYpboiq2sQNeQeRvh09GfQgs/GyFEvZpJ9cLXpXA==}
|
||||
|
||||
'@ant-design/icons@6.0.0':
|
||||
resolution: {integrity: sha512-o0aCCAlHc1o4CQcapAwWzHeaW2x9F49g7P3IDtvtNXgHowtRWYb7kiubt8sQPFvfVIVU/jLw2hzeSlNt0FU+Uw==}
|
||||
engines: {node: '>=8'}
|
||||
peerDependencies:
|
||||
react: '>=16.0.0'
|
||||
react-dom: '>=16.0.0'
|
||||
|
||||
'@antfu/install-pkg@1.0.0':
|
||||
resolution: {integrity: sha512-xvX6P/lo1B3ej0OsaErAjqgFYzYVcJpamjLAFLYh9vRJngBrMoUG7aVnrGTeqM7yxbyTD5p3F2+0/QUEh8Vzhw==}
|
||||
|
||||
@@ -864,6 +884,12 @@ packages:
|
||||
resolution: {integrity: sha512-c83qWb22rNRuB0UaVCI0uRPNRr8Z0FWnEIvT47jiHAmOIUHbBOg5XvV7pM5x+rKn9HRpjxquDbXYSXr3fAKFcw==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
'@rc-component/util@1.2.1':
|
||||
resolution: {integrity: sha512-AUVu6jO+lWjQnUOOECwu8iR0EdElQgWW5NBv5vP/Uf9dWbAX3udhMutRlkVXjuac2E40ghkFy+ve00mc/3Fymg==}
|
||||
peerDependencies:
|
||||
react: '>=18.0.0'
|
||||
react-dom: '>=18.0.0'
|
||||
|
||||
'@react-aria/focus@3.20.1':
|
||||
resolution: {integrity: sha512-lgYs+sQ1TtBrAXnAdRBQrBo0/7o5H6IrfDxec1j+VRpcXL0xyk0xPq+m3lZp8typzIghqDgpnKkJ5Jf4OrzPIw==}
|
||||
peerDependencies:
|
||||
@@ -1506,6 +1532,9 @@ packages:
|
||||
resolution: {integrity: sha512-cYY9mypksY8NRqgDB1XD1RiJL338v/551niynFTGkZOO2LHuB2OmOYxDIe/ttN9AHwrqdum1360G3ald0W9kCg==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
classnames@2.5.1:
|
||||
resolution: {integrity: sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==}
|
||||
|
||||
cli-boxes@3.0.0:
|
||||
resolution: {integrity: sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g==}
|
||||
engines: {node: '>=10'}
|
||||
@@ -2898,6 +2927,9 @@ packages:
|
||||
react-native:
|
||||
optional: true
|
||||
|
||||
react-is@18.3.1:
|
||||
resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==}
|
||||
|
||||
react-markdown@9.1.0:
|
||||
resolution: {integrity: sha512-xaijuJB0kzGiUdG7nc2MOMDUDBWPyGAjZtUrow9XxUeua8IqeP+VlIfAZ3bphpcLTnSZXz6z9jcVC/TCwbfgdw==}
|
||||
peerDependencies:
|
||||
@@ -3519,6 +3551,23 @@ snapshots:
|
||||
'@jridgewell/gen-mapping': 0.3.8
|
||||
'@jridgewell/trace-mapping': 0.3.25
|
||||
|
||||
'@ant-design/colors@8.0.0':
|
||||
dependencies:
|
||||
'@ant-design/fast-color': 3.0.0
|
||||
|
||||
'@ant-design/fast-color@3.0.0': {}
|
||||
|
||||
'@ant-design/icons-svg@4.4.2': {}
|
||||
|
||||
'@ant-design/icons@6.0.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
|
||||
dependencies:
|
||||
'@ant-design/colors': 8.0.0
|
||||
'@ant-design/icons-svg': 4.4.2
|
||||
'@rc-component/util': 1.2.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
classnames: 2.5.1
|
||||
react: 18.3.1
|
||||
react-dom: 18.3.1(react@18.3.1)
|
||||
|
||||
'@antfu/install-pkg@1.0.0':
|
||||
dependencies:
|
||||
package-manager-detector: 0.2.11
|
||||
@@ -4089,6 +4138,12 @@ snapshots:
|
||||
'@pnpm/network.ca-file': 1.0.2
|
||||
config-chain: 1.1.13
|
||||
|
||||
'@rc-component/util@1.2.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
|
||||
dependencies:
|
||||
react: 18.3.1
|
||||
react-dom: 18.3.1(react@18.3.1)
|
||||
react-is: 18.3.1
|
||||
|
||||
'@react-aria/focus@3.20.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
|
||||
dependencies:
|
||||
'@react-aria/interactions': 3.24.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
@@ -4716,6 +4771,8 @@ snapshots:
|
||||
|
||||
ci-info@4.2.0: {}
|
||||
|
||||
classnames@2.5.1: {}
|
||||
|
||||
cli-boxes@3.0.0: {}
|
||||
|
||||
cli-cursor@5.0.0:
|
||||
@@ -6385,6 +6442,8 @@ snapshots:
|
||||
optionalDependencies:
|
||||
react-dom: 18.3.1(react@18.3.1)
|
||||
|
||||
react-is@18.3.1: {}
|
||||
|
||||
react-markdown@9.1.0(@types/react@18.3.19)(react@18.3.1):
|
||||
dependencies:
|
||||
'@types/hast': 3.0.4
|
||||
|
||||
18
src-tauri/Cargo.lock
generated
18
src-tauri/Cargo.lock
generated
@@ -777,6 +777,7 @@ dependencies = [
|
||||
"tokio",
|
||||
"tokio-native-tls",
|
||||
"tokio-tungstenite 0.20.1",
|
||||
"tokio-util",
|
||||
"tungstenite 0.24.0",
|
||||
"url",
|
||||
"walkdir",
|
||||
@@ -3240,6 +3241,16 @@ version = "0.3.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
|
||||
|
||||
[[package]]
|
||||
name = "mime_guess"
|
||||
version = "2.0.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e"
|
||||
dependencies = [
|
||||
"mime",
|
||||
"unicase",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "minimal-lexical"
|
||||
version = "0.2.1"
|
||||
@@ -4782,6 +4793,7 @@ dependencies = [
|
||||
"js-sys",
|
||||
"log",
|
||||
"mime",
|
||||
"mime_guess",
|
||||
"native-tls",
|
||||
"once_cell",
|
||||
"percent-encoding",
|
||||
@@ -6736,6 +6748,12 @@ dependencies = [
|
||||
"unic-common",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicase"
|
||||
version = "2.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.18"
|
||||
|
||||
@@ -47,7 +47,7 @@ tokio-native-tls = "0.3" # For wss connections
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
tokio-tungstenite = { version = "0.20", features = ["rustls-tls-webpki-roots"] }
|
||||
hyper = { version = "0.14", features = ["client"] }
|
||||
reqwest = "0.12.12"
|
||||
reqwest = { version = "0.12", features = ["json", "multipart"] }
|
||||
futures = "0.3.31"
|
||||
ordered-float = { version = "4.6.0", default-features = false }
|
||||
lazy_static = "1.5.0"
|
||||
@@ -68,6 +68,7 @@ url = "2.5.2"
|
||||
http = "1.1.0"
|
||||
tungstenite = "0.24.0"
|
||||
env_logger = "0.11.5"
|
||||
tokio-util = "0.7.14"
|
||||
|
||||
[target."cfg(target_os = \"macos\")".dependencies]
|
||||
tauri-nspanel = { git = "https://github.com/ahkohd/tauri-nspanel", branch = "v2" }
|
||||
|
||||
@@ -124,7 +124,10 @@ pub fn run() {
|
||||
// server::get_coco_server_connectors,
|
||||
server::websocket::connect_to_server,
|
||||
server::websocket::disconnect,
|
||||
get_app_search_source
|
||||
get_app_search_source,
|
||||
server::attachment::upload_attachment,
|
||||
server::attachment::get_attachment,
|
||||
server::attachment::delete_attachment,
|
||||
])
|
||||
.setup(|app| {
|
||||
let registry = SearchSourceRegistry::default();
|
||||
|
||||
151
src-tauri/src/server/attachment.rs
Normal file
151
src-tauri/src/server/attachment.rs
Normal file
@@ -0,0 +1,151 @@
|
||||
use super::servers::{get_server_by_id, get_server_token};
|
||||
use crate::server::http_client::HttpClient;
|
||||
use reqwest::multipart::{Form, Part};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
use std::{collections::HashMap, path::PathBuf};
|
||||
use tauri::command;
|
||||
use tokio::fs::File;
|
||||
use tokio_util::codec::{BytesCodec, FramedRead};
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct UploadAttachmentResponse {
|
||||
pub acknowledged: bool,
|
||||
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: String,
|
||||
pub _id: String,
|
||||
pub _score: f64,
|
||||
pub _source: AttachmentSource,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct AttachmentHits {
|
||||
pub total: Value,
|
||||
pub max_score: f64,
|
||||
pub hits: Vec<AttachmentHit>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct GetAttachmentResponse {
|
||||
pub took: u32,
|
||||
pub timed_out: bool,
|
||||
pub _shards: Value,
|
||||
pub hits: AttachmentHits,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct DeleteAttachmentResponse {
|
||||
pub _id: String,
|
||||
pub result: String,
|
||||
}
|
||||
|
||||
#[command]
|
||||
pub async fn upload_attachment(
|
||||
server_id: String,
|
||||
session_id: String,
|
||||
file_paths: Vec<PathBuf>,
|
||||
) -> Result<UploadAttachmentResponse, String> {
|
||||
let mut form = Form::new();
|
||||
|
||||
for file_path in file_paths {
|
||||
let file = File::open(&file_path)
|
||||
.await
|
||||
.map_err(|err| err.to_string())?;
|
||||
|
||||
let stream = FramedRead::new(file, BytesCodec::new());
|
||||
let file_name = file_path
|
||||
.file_name()
|
||||
.and_then(|n| n.to_str())
|
||||
.ok_or("Invalid filename")?;
|
||||
|
||||
let part =
|
||||
Part::stream(reqwest::Body::wrap_stream(stream)).file_name(file_name.to_string());
|
||||
|
||||
form = form.part("files", part);
|
||||
}
|
||||
|
||||
let server = get_server_by_id(&server_id).ok_or("Server not found")?;
|
||||
let url = HttpClient::join_url(&server.endpoint, &format!("chat/{}/_upload", session_id));
|
||||
|
||||
let token = get_server_token(&server_id).await?;
|
||||
let mut headers = HashMap::new();
|
||||
if let Some(token) = token {
|
||||
headers.insert("X-API-TOKEN".to_string(), token.access_token);
|
||||
}
|
||||
|
||||
let client = reqwest::Client::new();
|
||||
let response = client
|
||||
.post(url)
|
||||
.multipart(form)
|
||||
.headers((&headers).try_into().map_err(|err| format!("{}", err))?)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|err| err.to_string())?;
|
||||
|
||||
if response.status().is_success() {
|
||||
let result = response
|
||||
.json::<UploadAttachmentResponse>()
|
||||
.await
|
||||
.map_err(|err| err.to_string())?;
|
||||
|
||||
Ok(result)
|
||||
} else {
|
||||
Err(format!("Upload failed with status: {}", response.status()))
|
||||
}
|
||||
}
|
||||
|
||||
#[command]
|
||||
pub async fn get_attachment(
|
||||
server_id: String,
|
||||
session_id: String,
|
||||
) -> Result<GetAttachmentResponse, String> {
|
||||
let mut query_params = HashMap::new();
|
||||
query_params.insert("session".to_string(), serde_json::Value::String(session_id));
|
||||
|
||||
let response = HttpClient::get(&server_id, "/attachment/_search", Some(query_params)).await?;
|
||||
|
||||
if response.status().is_success() {
|
||||
response
|
||||
.json::<GetAttachmentResponse>()
|
||||
.await
|
||||
.map_err(|e| e.to_string())
|
||||
} else {
|
||||
Err(format!("Request failed with status: {}", response.status()))
|
||||
}
|
||||
}
|
||||
|
||||
#[command]
|
||||
pub async fn delete_attachment(server_id: String, id: String) -> Result<bool, String> {
|
||||
let response =
|
||||
HttpClient::delete(&server_id, &format!("/attachment/{}", id), None, None).await?;
|
||||
|
||||
if response.status().is_success() {
|
||||
response
|
||||
.json::<DeleteAttachmentResponse>()
|
||||
.await
|
||||
.map_err(|e| e.to_string())?
|
||||
.result
|
||||
.eq("deleted")
|
||||
.then_some(true)
|
||||
.ok_or("Delete operation was not successful".to_string())
|
||||
} else {
|
||||
Err(format!("Delete failed with status: {}", response.status()))
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,11 @@
|
||||
//! This file contains Rust APIs related to Coco Server management.
|
||||
|
||||
pub mod attachment;
|
||||
pub mod auth;
|
||||
pub mod servers;
|
||||
pub mod connector;
|
||||
pub mod datasource;
|
||||
pub mod http_client;
|
||||
pub mod profile;
|
||||
pub mod search;
|
||||
pub mod servers;
|
||||
pub mod websocket;
|
||||
|
||||
73
src/api/attachment.ts
Normal file
73
src/api/attachment.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
|
||||
interface UploadAttachmentPayload {
|
||||
serverId: string;
|
||||
sessionId: string;
|
||||
filePaths: string[];
|
||||
}
|
||||
|
||||
interface UploadAttachmentResponse {
|
||||
acknowledged: boolean;
|
||||
attachments: string[];
|
||||
}
|
||||
|
||||
type GetAttachmentPayload = Omit<UploadAttachmentPayload, "filePaths">;
|
||||
|
||||
export interface AttachmentHit {
|
||||
_index: string;
|
||||
_type: string;
|
||||
_id: string;
|
||||
_score: number;
|
||||
_source: {
|
||||
id: string;
|
||||
created: string;
|
||||
updated: string;
|
||||
session: string;
|
||||
name: string;
|
||||
icon: string;
|
||||
url: string;
|
||||
size: number;
|
||||
};
|
||||
}
|
||||
|
||||
interface GetAttachmentResponse {
|
||||
took: number;
|
||||
timed_out: boolean;
|
||||
_shards: {
|
||||
total: number;
|
||||
successful: number;
|
||||
skipped: number;
|
||||
failed: number;
|
||||
};
|
||||
hits: {
|
||||
total: {
|
||||
value: number;
|
||||
relation: string;
|
||||
};
|
||||
max_score: number;
|
||||
hits: AttachmentHit[];
|
||||
};
|
||||
}
|
||||
|
||||
interface DeleteAttachmentPayload {
|
||||
serverId: string;
|
||||
id: string;
|
||||
}
|
||||
|
||||
export const uploadAttachment = async (payload: UploadAttachmentPayload) => {
|
||||
const response = await invoke<UploadAttachmentResponse>("upload_attachment", {
|
||||
...payload,
|
||||
});
|
||||
|
||||
if (response?.acknowledged) {
|
||||
return response.attachments;
|
||||
}
|
||||
};
|
||||
|
||||
export const getAttachment = (payload: GetAttachmentPayload) => {
|
||||
return invoke<GetAttachmentResponse>("get_attachment", { ...payload });
|
||||
};
|
||||
|
||||
export const deleteAttachment = (payload: DeleteAttachmentPayload) => {
|
||||
return invoke<boolean>("delete_attachment", { ...payload });
|
||||
};
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useRef, useEffect } from "react";
|
||||
import { useRef, useEffect, useMemo } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { ChatMessage } from "@/components/ChatMessage";
|
||||
@@ -7,6 +7,8 @@ import FileList from "@/components/Assistant/FileList";
|
||||
import { useChatScroll } from "@/hooks/useChatScroll";
|
||||
import { useChatStore } from "@/stores/chatStore";
|
||||
import type { Chat, IChunkData } from "./types";
|
||||
import SessionFile from "./SessionFile";
|
||||
import { useConnectStore } from "@/stores/connectStore";
|
||||
|
||||
interface ChatContentProps {
|
||||
activeChat?: Chat;
|
||||
@@ -41,12 +43,21 @@ export const ChatContent = ({
|
||||
handleSendMessage,
|
||||
getFileUrl,
|
||||
}: ChatContentProps) => {
|
||||
const sessionId = useConnectStore((state) => state.currentSessionId);
|
||||
const setCurrentSessionId = useConnectStore((state) => {
|
||||
return state.setCurrentSessionId;
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
setCurrentSessionId(activeChat?._id);
|
||||
}, [activeChat]);
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
const uploadFiles = useChatStore((state) => state.uploadFiles);
|
||||
|
||||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
|
||||
const { scrollToBottom } = useChatScroll(messagesEndRef);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -69,7 +80,7 @@ export const ChatContent = ({
|
||||
}, [scrollToBottom]);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full justify-between overflow-hidden">
|
||||
<div className="relative 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">
|
||||
<Greetings />
|
||||
|
||||
@@ -143,11 +154,13 @@ export const ChatContent = ({
|
||||
<div ref={messagesEndRef} />
|
||||
</div>
|
||||
|
||||
{uploadFiles.length > 0 && (
|
||||
<div className="max-h-[120px] overflow-auto p-2">
|
||||
<FileList getFileUrl={getFileUrl}/>
|
||||
{sessionId && uploadFiles.length > 0 && (
|
||||
<div key={sessionId} className="max-h-[120px] overflow-auto p-2">
|
||||
<FileList sessionId={sessionId} getFileUrl={getFileUrl} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{sessionId && <SessionFile sessionId={sessionId} />}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,42 +1,88 @@
|
||||
import { useEffect, useMemo } from "react";
|
||||
import { filesize } from "filesize";
|
||||
import { X } from "lucide-react";
|
||||
import { useAsyncEffect } from "ahooks";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { useChatStore } from "@/stores/chatStore";
|
||||
import { isImage } from "@/utils";
|
||||
import { useConnectStore } from "@/stores/connectStore";
|
||||
import { deleteAttachment, uploadAttachment } from "@/api/attachment";
|
||||
import FileIcon from "../Common/Icons/FileIcon";
|
||||
|
||||
interface FileListProps {
|
||||
sessionId: string;
|
||||
getFileUrl: (path: string) => string;
|
||||
}
|
||||
|
||||
const FileList = ({ getFileUrl }: FileListProps) => {
|
||||
const FileList = (props: FileListProps) => {
|
||||
const { sessionId } = props;
|
||||
const { t } = useTranslation();
|
||||
const uploadFiles = useChatStore((state) => state.uploadFiles);
|
||||
const setUploadFiles = useChatStore((state) => state.setUploadFiles);
|
||||
const currentService = useConnectStore((state) => state.currentService);
|
||||
|
||||
const deleteFile = (id: string) => {
|
||||
const serverId = useMemo(() => {
|
||||
return currentService.id;
|
||||
}, [currentService]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
setUploadFiles([]);
|
||||
};
|
||||
}, []);
|
||||
|
||||
useAsyncEffect(async () => {
|
||||
if (uploadFiles.length === 0) return;
|
||||
|
||||
for await (const item of uploadFiles) {
|
||||
const { uploaded, path } = item;
|
||||
|
||||
if (uploaded) continue;
|
||||
|
||||
const attachmentIds = await uploadAttachment({
|
||||
serverId,
|
||||
sessionId,
|
||||
filePaths: [path],
|
||||
});
|
||||
|
||||
if (!attachmentIds) continue;
|
||||
|
||||
Object.assign(item, {
|
||||
uploaded: true,
|
||||
attachmentId: attachmentIds[0],
|
||||
});
|
||||
|
||||
setUploadFiles(uploadFiles);
|
||||
}
|
||||
}, [uploadFiles]);
|
||||
|
||||
const deleteFile = async (id: string, attachmentId: string) => {
|
||||
setUploadFiles(uploadFiles.filter((file) => file.id !== id));
|
||||
|
||||
deleteAttachment({ serverId, id: attachmentId });
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-wrap gap-y-2 -mx-1 text-sm">
|
||||
{uploadFiles.map((file) => {
|
||||
const { id, path, icon, name, extname, size } = file;
|
||||
const { id, name, extname, size, uploaded, attachmentId } = file;
|
||||
|
||||
return (
|
||||
<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="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>
|
||||
{attachmentId && (
|
||||
<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, attachmentId);
|
||||
}}
|
||||
>
|
||||
<X className="size-[10px] text-white" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<img
|
||||
src={getFileUrl(isImage(path) ? path : icon)}
|
||||
className="size-[40px]"
|
||||
/>
|
||||
<FileIcon extname={extname} />
|
||||
|
||||
<div className="flex flex-col justify-between overflow-hidden">
|
||||
<div className="truncate text-[#333333] dark:text-[#D8D8D8]">
|
||||
@@ -44,12 +90,16 @@ const FileList = ({ getFileUrl }: FileListProps) => {
|
||||
</div>
|
||||
|
||||
<div className="text-xs text-[#999999]">
|
||||
<div className="flex gap-2">
|
||||
{extname && <span>{extname}</span>}
|
||||
<span>
|
||||
{filesize(size, { standard: "jedec", spacer: "" })}
|
||||
</span>
|
||||
</div>
|
||||
{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>
|
||||
|
||||
160
src/components/Assistant/SessionFile.tsx
Normal file
160
src/components/Assistant/SessionFile.tsx
Normal file
@@ -0,0 +1,160 @@
|
||||
import {
|
||||
AttachmentHit,
|
||||
deleteAttachment,
|
||||
getAttachment,
|
||||
} from "@/api/attachment";
|
||||
import { useConnectStore } from "@/stores/connectStore";
|
||||
import clsx from "clsx";
|
||||
import { filesize } from "filesize";
|
||||
import { Files, Trash2, X } from "lucide-react";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import Checkbox from "../Common/Checkbox";
|
||||
import FileIcon from "../Common/Icons/FileIcon";
|
||||
|
||||
interface SessionFileProps {
|
||||
sessionId: string;
|
||||
}
|
||||
|
||||
const SessionFile = (props: SessionFileProps) => {
|
||||
const { sessionId } = props;
|
||||
const { t } = useTranslation();
|
||||
const currentService = useConnectStore((state) => state.currentService);
|
||||
const [uploadedFiles, setUploadedFiles] = useState<AttachmentHit[]>([]);
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [checkList, setCheckList] = useState<string[]>([]);
|
||||
|
||||
const serverId = useMemo(() => {
|
||||
return currentService.id;
|
||||
}, [currentService]);
|
||||
|
||||
useEffect(() => {
|
||||
setUploadedFiles([]);
|
||||
|
||||
getUploadedFiles();
|
||||
}, [sessionId]);
|
||||
|
||||
const getUploadedFiles = async () => {
|
||||
const response = await getAttachment({ serverId, sessionId });
|
||||
|
||||
setUploadedFiles(response.hits.hits);
|
||||
};
|
||||
|
||||
const handleDelete = async (id: string) => {
|
||||
const result = await deleteAttachment({ serverId, id });
|
||||
|
||||
if (!result) return;
|
||||
|
||||
getUploadedFiles();
|
||||
};
|
||||
|
||||
const handleCheckAll = (checked: boolean) => {
|
||||
if (checked) {
|
||||
setCheckList(uploadedFiles.map((item) => item._source.id));
|
||||
} else {
|
||||
setCheckList([]);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCheck = (checked: boolean, id: string) => {
|
||||
if (checked) {
|
||||
setCheckList([...checkList, id]);
|
||||
} else {
|
||||
setCheckList(checkList.filter((item) => item !== id));
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={clsx("select-none", {
|
||||
hidden: uploadedFiles.length === 0,
|
||||
})}
|
||||
>
|
||||
<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 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]">
|
||||
{uploadedFiles.length}
|
||||
</div>
|
||||
</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]">
|
||||
{t("assistant.sessionFile.description")}
|
||||
</span>
|
||||
|
||||
<Checkbox
|
||||
indeterminate
|
||||
checked={checkList.length === uploadedFiles.length}
|
||||
onChange={handleCheckAll}
|
||||
/>
|
||||
</div>
|
||||
<ul className="flex-1 overflow-auto flex flex-col gap-2 mt-6">
|
||||
{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;
|
||||
@@ -67,6 +67,13 @@ const AudioRecording: FC<AudioRecordingProps> = (props) => {
|
||||
const recordedUrl = URL.createObjectURL(blob);
|
||||
console.log("recorded:", recordedUrl);
|
||||
|
||||
// 获取文件大小(单位:字节)
|
||||
const fileSizeInBytes = blob.size;
|
||||
// 转换为 MB,保留两位小数
|
||||
const fileSizeInMB = (fileSizeInBytes / (1024 * 1024)).toFixed(2);
|
||||
|
||||
console.log("recorded:", recordedUrl, `size: ${fileSizeInMB}MB`);
|
||||
|
||||
// setAudioUrl(recordedUrl);
|
||||
});
|
||||
|
||||
|
||||
34
src/components/Common/Checkbox/index.tsx
Normal file
34
src/components/Common/Checkbox/index.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
import {
|
||||
CheckboxProps as HeadlessCheckboxProps,
|
||||
Checkbox as HeadlessCheckbox,
|
||||
} from "@headlessui/react";
|
||||
import clsx from "clsx";
|
||||
import { CheckIcon } from "lucide-react";
|
||||
|
||||
interface CheckboxProps extends HeadlessCheckboxProps {
|
||||
indeterminate?: boolean;
|
||||
}
|
||||
|
||||
const Checkbox = (props: CheckboxProps) => {
|
||||
const { indeterminate, className, ...rest } = props;
|
||||
|
||||
return (
|
||||
<HeadlessCheckbox
|
||||
{...rest}
|
||||
className={clsx(
|
||||
"group size-4 rounded-sm border border-black/30 dark:border-white/30 data-[checked]:bg-[#2F54EB] data-[checked]:!border-[#2F54EB] transition cursor-pointer",
|
||||
className
|
||||
)}
|
||||
>
|
||||
{indeterminate && (
|
||||
<div className="size-full flex items-center justify-center group-data-[checked]:hidden">
|
||||
<div className="size-2 bg-[#2F54EB]"></div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<CheckIcon className="hidden size-[14px] text-white group-data-[checked]:block" />
|
||||
</HeadlessCheckbox>
|
||||
);
|
||||
};
|
||||
|
||||
export default Checkbox;
|
||||
21
src/components/Common/Icons/FileIcon/AudioIcon.tsx
Normal file
21
src/components/Common/Icons/FileIcon/AudioIcon.tsx
Normal file
@@ -0,0 +1,21 @@
|
||||
const AudioIcon = () => {
|
||||
return (
|
||||
<svg
|
||||
width="1em"
|
||||
height="1em"
|
||||
viewBox="0 0 16 16"
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<title>audio</title>
|
||||
<g stroke="none" strokeWidth="1" fill="none" fillRule="evenodd">
|
||||
<path
|
||||
d="M14.1178571,4.0125 C14.225,4.11964286 14.2857143,4.26428571 14.2857143,4.41607143 L14.2857143,15.4285714 C14.2857143,15.7446429 14.0303571,16 13.7142857,16 L2.28571429,16 C1.96964286,16 1.71428571,15.7446429 1.71428571,15.4285714 L1.71428571,0.571428571 C1.71428571,0.255357143 1.96964286,0 2.28571429,0 L9.86964286,0 C10.0214286,0 10.1678571,0.0607142857 10.275,0.167857143 L14.1178571,4.0125 Z M10.7315824,7.11216117 C10.7428131,7.15148751 10.7485063,7.19218979 10.7485063,7.23309113 L10.7485063,8.07742614 C10.7484199,8.27364959 10.6183424,8.44607275 10.4296853,8.50003683 L8.32984514,9.09986306 L8.32984514,11.7071803 C8.32986605,12.5367078 7.67249692,13.217028 6.84345686,13.2454634 L6.79068592,13.2463395 C6.12766108,13.2463395 5.53916361,12.8217001 5.33010655,12.1924966 C5.1210495,11.563293 5.33842118,10.8709227 5.86959669,10.4741173 C6.40077221,10.0773119 7.12636292,10.0652587 7.67042486,10.4442027 L7.67020842,7.74937024 L7.68449368,7.74937024 C7.72405122,7.59919041 7.83988806,7.48101083 7.98924584,7.4384546 L10.1880418,6.81004755 C10.42156,6.74340323 10.6648954,6.87865515 10.7315824,7.11216117 Z M9.60714286,1.31785714 L12.9678571,4.67857143 L9.60714286,4.67857143 L9.60714286,1.31785714 Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export default AudioIcon;
|
||||
21
src/components/Common/Icons/FileIcon/VideoIcon.tsx
Normal file
21
src/components/Common/Icons/FileIcon/VideoIcon.tsx
Normal file
@@ -0,0 +1,21 @@
|
||||
const VideoIcon = () => {
|
||||
return (
|
||||
<svg
|
||||
width="1em"
|
||||
height="1em"
|
||||
viewBox="0 0 16 16"
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<title>video</title>
|
||||
<g stroke="none" strokeWidth="1" fill="none" fillRule="evenodd">
|
||||
<path
|
||||
d="M14.1178571,4.0125 C14.225,4.11964286 14.2857143,4.26428571 14.2857143,4.41607143 L14.2857143,15.4285714 C14.2857143,15.7446429 14.0303571,16 13.7142857,16 L2.28571429,16 C1.96964286,16 1.71428571,15.7446429 1.71428571,15.4285714 L1.71428571,0.571428571 C1.71428571,0.255357143 1.96964286,0 2.28571429,0 L9.86964286,0 C10.0214286,0 10.1678571,0.0607142857 10.275,0.167857143 L14.1178571,4.0125 Z M12.9678571,4.67857143 L9.60714286,1.31785714 L9.60714286,4.67857143 L12.9678571,4.67857143 Z M10.5379461,10.3101106 L6.68957555,13.0059749 C6.59910784,13.0693494 6.47439406,13.0473861 6.41101953,12.9569184 C6.3874624,12.9232903 6.37482581,12.8832269 6.37482581,12.8421686 L6.37482581,7.45043999 C6.37482581,7.33998304 6.46436886,7.25043999 6.57482581,7.25043999 C6.61588409,7.25043999 6.65594753,7.26307658 6.68957555,7.28663371 L10.5379461,9.98249803 C10.6284138,10.0458726 10.6503772,10.1705863 10.5870027,10.2610541 C10.5736331,10.2801392 10.5570312,10.2967411 10.5379461,10.3101106 Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export default VideoIcon;
|
||||
154
src/components/Common/Icons/FileIcon/index.tsx
Normal file
154
src/components/Common/Icons/FileIcon/index.tsx
Normal file
@@ -0,0 +1,154 @@
|
||||
import {
|
||||
FileExcelFilled,
|
||||
FileImageFilled,
|
||||
FileMarkdownFilled,
|
||||
FilePdfFilled,
|
||||
FilePptFilled,
|
||||
FileTextFilled,
|
||||
FileWordFilled,
|
||||
FileZipFilled,
|
||||
} from "@ant-design/icons";
|
||||
|
||||
import AudioIcon from "./AudioIcon";
|
||||
import VideoIcon from "./VideoIcon";
|
||||
import { cloneElement, FC, useMemo } from "react";
|
||||
import clsx from "clsx";
|
||||
|
||||
interface FileIconProps {
|
||||
extname: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const FileIcon: FC<FileIconProps> = (props) => {
|
||||
const { extname, className } = props;
|
||||
|
||||
const presetFileIcons = [
|
||||
{
|
||||
icon: <FileExcelFilled />,
|
||||
color: "#22b35e",
|
||||
extnames: ["xlsx", "xls", "csv", "xlsm", "xltx", "xltm", "xlsb"],
|
||||
},
|
||||
{
|
||||
icon: <FileImageFilled />,
|
||||
color: "#13c2c2",
|
||||
extnames: [
|
||||
"png",
|
||||
"jpg",
|
||||
"jpeg",
|
||||
"gif",
|
||||
"bmp",
|
||||
"webp",
|
||||
"svg",
|
||||
"ico",
|
||||
"tiff",
|
||||
"raw",
|
||||
"heic",
|
||||
"psd",
|
||||
"ai",
|
||||
],
|
||||
},
|
||||
{
|
||||
icon: <FileMarkdownFilled />,
|
||||
color: "#722ed1",
|
||||
extnames: ["md", "mdx", "markdown", "mdown", "mkd", "mkdn"],
|
||||
},
|
||||
{
|
||||
icon: <FilePdfFilled />,
|
||||
color: "#ff4d4f",
|
||||
extnames: ["pdf", "xps", "oxps"],
|
||||
},
|
||||
{
|
||||
icon: <FilePptFilled />,
|
||||
color: "#d04423",
|
||||
extnames: [
|
||||
"ppt",
|
||||
"pptx",
|
||||
"pps",
|
||||
"ppsx",
|
||||
"pot",
|
||||
"potx",
|
||||
"pptm",
|
||||
"potm",
|
||||
"ppsm",
|
||||
],
|
||||
},
|
||||
{
|
||||
icon: <FileWordFilled />,
|
||||
color: "#1677ff",
|
||||
extnames: ["doc", "docx", "dot", "dotx", "docm", "dotm", "rtf", "odt"],
|
||||
},
|
||||
{
|
||||
icon: <FileZipFilled />,
|
||||
color: "#fab714",
|
||||
extnames: [
|
||||
"zip",
|
||||
"rar",
|
||||
"7z",
|
||||
"tar",
|
||||
"gz",
|
||||
"bz2",
|
||||
"xz",
|
||||
"tgz",
|
||||
"iso",
|
||||
"dmg",
|
||||
],
|
||||
},
|
||||
{
|
||||
icon: <VideoIcon />,
|
||||
color: "#7b61ff",
|
||||
extnames: [
|
||||
"mp4",
|
||||
"avi",
|
||||
"mov",
|
||||
"wmv",
|
||||
"flv",
|
||||
"mkv",
|
||||
"webm",
|
||||
"m4v",
|
||||
"mpeg",
|
||||
"mpg",
|
||||
"3gp",
|
||||
"rmvb",
|
||||
"ts",
|
||||
],
|
||||
},
|
||||
{
|
||||
icon: <AudioIcon />,
|
||||
color: "#eb2f96",
|
||||
extnames: [
|
||||
"mp3",
|
||||
"wav",
|
||||
"flac",
|
||||
"ape",
|
||||
"aac",
|
||||
"ogg",
|
||||
"wma",
|
||||
"m4a",
|
||||
"opus",
|
||||
"ac3",
|
||||
"mid",
|
||||
"midi",
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const [icon, iconColor] = useMemo(() => {
|
||||
for (const item of presetFileIcons) {
|
||||
const { icon, color, extnames } = item;
|
||||
|
||||
if (extnames.includes(extname)) {
|
||||
return [icon, color];
|
||||
}
|
||||
}
|
||||
|
||||
return [<FileTextFilled key="defaultIcon" />, "#8c8c8c"];
|
||||
}, [extname]);
|
||||
|
||||
return (
|
||||
<div className={clsx("text-3xl", className)} style={{ color: iconColor }}>
|
||||
{icon}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default FileIcon;
|
||||
@@ -14,6 +14,8 @@ import SearchPopover from "./SearchPopover";
|
||||
import AudioRecording from "../AudioRecording";
|
||||
import { hide_coco } from "@/commands";
|
||||
import { DataSource } from "@/types/commands";
|
||||
import InputExtra from "./InputExtra";
|
||||
import { useConnectStore } from "@/stores/connectStore";
|
||||
|
||||
interface ChatInputProps {
|
||||
onSend: (message: string) => void;
|
||||
@@ -60,16 +62,16 @@ export default function ChatInput({
|
||||
isChatPage = false,
|
||||
getDataSourcesByServer,
|
||||
setupWindowFocusListener,
|
||||
}: // checkScreenPermission,
|
||||
// requestScreenPermission,
|
||||
// getScreenMonitors,
|
||||
// getScreenWindows,
|
||||
// captureMonitorScreenshot,
|
||||
// captureWindowScreenshot,
|
||||
// openFileDialog,
|
||||
// getFileMetadata,
|
||||
// getFileIcon,
|
||||
ChatInputProps) {
|
||||
checkScreenPermission,
|
||||
requestScreenPermission,
|
||||
getScreenMonitors,
|
||||
getScreenWindows,
|
||||
captureMonitorScreenshot,
|
||||
captureWindowScreenshot,
|
||||
openFileDialog,
|
||||
getFileMetadata,
|
||||
getFileIcon,
|
||||
}: ChatInputProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const showTooltip = useAppStore(
|
||||
@@ -85,6 +87,8 @@ ChatInputProps) {
|
||||
(state: { setSourceData: any }) => state.setSourceData
|
||||
);
|
||||
|
||||
const sessionId = useConnectStore((state) => state.currentSessionId);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
changeInput("");
|
||||
@@ -360,17 +364,19 @@ ChatInputProps) {
|
||||
>
|
||||
{isChatMode ? (
|
||||
<div className="flex gap-2 text-sm text-[#333] dark:text-[#d8d8d8]">
|
||||
{/* <InputExtra
|
||||
checkScreenPermission={checkScreenPermission}
|
||||
requestScreenPermission={requestScreenPermission}
|
||||
getScreenMonitors={getScreenMonitors}
|
||||
getScreenWindows={getScreenWindows}
|
||||
captureMonitorScreenshot={captureMonitorScreenshot}
|
||||
captureWindowScreenshot={captureWindowScreenshot}
|
||||
openFileDialog={openFileDialog}
|
||||
getFileMetadata={getFileMetadata}
|
||||
getFileIcon={getFileIcon}
|
||||
/> */}
|
||||
{sessionId && (
|
||||
<InputExtra
|
||||
checkScreenPermission={checkScreenPermission}
|
||||
requestScreenPermission={requestScreenPermission}
|
||||
getScreenMonitors={getScreenMonitors}
|
||||
getScreenWindows={getScreenWindows}
|
||||
captureMonitorScreenshot={captureMonitorScreenshot}
|
||||
captureWindowScreenshot={captureWindowScreenshot}
|
||||
openFileDialog={openFileDialog}
|
||||
getFileMetadata={getFileMetadata}
|
||||
getFileIcon={getFileIcon}
|
||||
/>
|
||||
)}
|
||||
|
||||
<button
|
||||
className={clsx(
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
import { useState, useEffect, useCallback, useMemo } from "react";
|
||||
import {
|
||||
Checkbox,
|
||||
Popover,
|
||||
PopoverButton,
|
||||
PopoverPanel,
|
||||
} from "@headlessui/react";
|
||||
import { Popover, PopoverButton, PopoverPanel } from "@headlessui/react";
|
||||
import {
|
||||
ChevronDownIcon,
|
||||
RefreshCw,
|
||||
@@ -19,6 +14,7 @@ import TypeIcon from "@/components/Common/Icons/TypeIcon";
|
||||
import { useConnectStore } from "@/stores/connectStore";
|
||||
import { useSearchStore } from "@/stores/searchStore";
|
||||
import { DataSource } from "@/types/commands";
|
||||
import Checkbox from "../Common/Checkbox";
|
||||
|
||||
interface SearchPopoverProps {
|
||||
isSearchActive: boolean;
|
||||
@@ -193,19 +189,11 @@ export default function SearchPopover({
|
||||
dataSourceList.length - 1
|
||||
: sourceDataIds?.includes(id)
|
||||
}
|
||||
indeterminate={isAll}
|
||||
onChange={(value) =>
|
||||
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 cursor-pointer"
|
||||
>
|
||||
{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>
|
||||
/>
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
|
||||
@@ -233,6 +233,14 @@
|
||||
"source": {
|
||||
"fetch_source": "Found {{count}} results",
|
||||
"pick_source": "{{count}} results"
|
||||
},
|
||||
"fileList": {
|
||||
"uploading": "Uploading...",
|
||||
"uploaded": "Uploaded"
|
||||
},
|
||||
"sessionFile": {
|
||||
"title": "Files in the conversation",
|
||||
"description": "Only the selected files will participate in the current conversation"
|
||||
}
|
||||
},
|
||||
"cloud": {
|
||||
|
||||
@@ -233,6 +233,14 @@
|
||||
"source": {
|
||||
"fetch_source": "找到 {{count}} 个结果",
|
||||
"pick_source": "{{count}} 个结果"
|
||||
},
|
||||
"fileList": {
|
||||
"uploading": "上传中...",
|
||||
"uploaded": "已上传"
|
||||
},
|
||||
"sessionFile": {
|
||||
"title": "对话中的文件",
|
||||
"description": "只有选中的文件才会参与当前对话"
|
||||
}
|
||||
},
|
||||
"cloud": {
|
||||
|
||||
@@ -9,6 +9,8 @@ interface UploadFile extends Metadata {
|
||||
id: string;
|
||||
path: string;
|
||||
icon: string;
|
||||
uploaded?: boolean;
|
||||
attachmentId?: string;
|
||||
}
|
||||
|
||||
export type IChatStore = {
|
||||
|
||||
@@ -21,6 +21,8 @@ export type IConnectStore = {
|
||||
setDatasourceData: (datasourceData: any[], key: string) => void;
|
||||
connectionTimeout: number;
|
||||
setConnectionTimeout: (connectionTimeout: number) => void;
|
||||
currentSessionId?: string;
|
||||
setCurrentSessionId: (currentSessionId?: string) => void;
|
||||
};
|
||||
|
||||
export const useConnectStore = create<IConnectStore>()(
|
||||
@@ -80,6 +82,9 @@ export const useConnectStore = create<IConnectStore>()(
|
||||
setConnectionTimeout: (connectionTimeout: number) => {
|
||||
return set(() => ({ connectionTimeout }));
|
||||
},
|
||||
setCurrentSessionId(currentSessionId) {
|
||||
return set(() => ({ currentSessionId }));
|
||||
},
|
||||
}),
|
||||
{
|
||||
name: "connect-store",
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { useState } from "react";
|
||||
import { isTauri } from "@tauri-apps/api/core";
|
||||
import { convertFileSrc as tauriConvertFileSrc } from "@tauri-apps/api/core";
|
||||
import type { OpenDialogOptions } from "@tauri-apps/plugin-dialog";
|
||||
|
||||
export interface EventPayloads {
|
||||
"language-changed": {
|
||||
@@ -51,9 +53,9 @@ export interface PlatformAdapter {
|
||||
getScreenshotableWindows: () => Promise<any[]>;
|
||||
captureMonitorScreenshot: (id: number) => Promise<string>;
|
||||
captureWindowScreenshot: (id: number) => Promise<string>;
|
||||
openFileDialog: (options: {
|
||||
multiple: boolean;
|
||||
}) => Promise<string | string[] | null>;
|
||||
openFileDialog: (
|
||||
options: OpenDialogOptions
|
||||
) => Promise<string | string[] | null>;
|
||||
getFileMetadata: (path: string) => Promise<any>;
|
||||
getFileIcon: (path: string, size: number) => Promise<string>;
|
||||
checkUpdate: () => Promise<any>;
|
||||
@@ -124,8 +126,7 @@ export const createTauriAdapter = (): PlatformAdapter => {
|
||||
|
||||
convertFileSrc(path: string): string {
|
||||
if (isTauri()) {
|
||||
const { convertFileSrc } = require("@tauri-apps/api/core");
|
||||
return convertFileSrc(path);
|
||||
return tauriConvertFileSrc(path);
|
||||
}
|
||||
return path;
|
||||
},
|
||||
@@ -215,7 +216,7 @@ export const createTauriAdapter = (): PlatformAdapter => {
|
||||
return "";
|
||||
},
|
||||
|
||||
async openFileDialog(options: { multiple: boolean }) {
|
||||
async openFileDialog(options: OpenDialogOptions) {
|
||||
if (isTauri()) {
|
||||
const { open } = await import("@tauri-apps/plugin-dialog");
|
||||
return open(options);
|
||||
@@ -391,7 +392,7 @@ export const createWebAdapter = (): PlatformAdapter => {
|
||||
return "";
|
||||
},
|
||||
|
||||
async openFileDialog(options: { multiple: boolean }): Promise<null> {
|
||||
async openFileDialog(options: OpenDialogOptions): Promise<null> {
|
||||
console.log("Web mode simulated open file dialog", options);
|
||||
return null;
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user