mirror of
https://github.com/infinilabs/coco-app.git
synced 2025-12-16 19:47:43 +01:00
feat: history added search and action menus (#322)
* feat: history added search and action menus * refactor: refinement of the dark theme * feat: add renamed input box style * feat: internalization * refactor: optimize the bright theme style * refactor: change dark theme style * feat: added api for deleting and modifying conversations * feat: supported search * feat: support for modifying the title * feat: support for deleting sessions * refactor: remove popup internationalization
This commit is contained in:
@@ -35,6 +35,7 @@
|
|||||||
"@wavesurfer/react": "^1.0.9",
|
"@wavesurfer/react": "^1.0.9",
|
||||||
"ahooks": "^3.8.4",
|
"ahooks": "^3.8.4",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
|
"dayjs": "^1.11.13",
|
||||||
"dotenv": "^16.4.7",
|
"dotenv": "^16.4.7",
|
||||||
"filesize": "^10.1.6",
|
"filesize": "^10.1.6",
|
||||||
"i18next": "^23.16.8",
|
"i18next": "^23.16.8",
|
||||||
|
|||||||
3
pnpm-lock.yaml
generated
3
pnpm-lock.yaml
generated
@@ -59,6 +59,9 @@ importers:
|
|||||||
clsx:
|
clsx:
|
||||||
specifier: ^2.1.1
|
specifier: ^2.1.1
|
||||||
version: 2.1.1
|
version: 2.1.1
|
||||||
|
dayjs:
|
||||||
|
specifier: ^1.11.13
|
||||||
|
version: 1.11.13
|
||||||
dotenv:
|
dotenv:
|
||||||
specifier: ^16.4.7
|
specifier: ^16.4.7
|
||||||
version: 16.4.7
|
version: 16.4.7
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ pub async fn chat_history<R: Runtime>(
|
|||||||
server_id: String,
|
server_id: String,
|
||||||
from: u32,
|
from: u32,
|
||||||
size: u32,
|
size: u32,
|
||||||
|
query: Option<String>,
|
||||||
) -> Result<String, String> {
|
) -> Result<String, String> {
|
||||||
let mut query_params: HashMap<String, Value> = HashMap::new();
|
let mut query_params: HashMap<String, Value> = HashMap::new();
|
||||||
if from > 0 {
|
if from > 0 {
|
||||||
@@ -21,6 +22,10 @@ pub async fn chat_history<R: Runtime>(
|
|||||||
query_params.insert("size".to_string(), size.into());
|
query_params.insert("size".to_string(), size.into());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let Some(query) = query {
|
||||||
|
query_params.insert("query".to_string(), query.into());
|
||||||
|
}
|
||||||
|
|
||||||
let response = HttpClient::get(&server_id, "/chat/_history", Some(query_params))
|
let response = HttpClient::get(&server_id, "/chat/_history", Some(query_params))
|
||||||
.await
|
.await
|
||||||
.map_err(|e| format!("Error get sessions: {}", e))?;
|
.map_err(|e| format!("Error get sessions: {}", e))?;
|
||||||
@@ -135,7 +140,8 @@ pub async fn new_chat<R: Runtime>(
|
|||||||
let mut headers = HashMap::new();
|
let mut headers = HashMap::new();
|
||||||
headers.insert("WEBSOCKET-SESSION-ID".to_string(), websocket_id.into());
|
headers.insert("WEBSOCKET-SESSION-ID".to_string(), websocket_id.into());
|
||||||
|
|
||||||
let response = HttpClient::advanced_post(&server_id, "/chat/_new", Some(headers), query_params, body)
|
let response =
|
||||||
|
HttpClient::advanced_post(&server_id, "/chat/_new", Some(headers), query_params, body)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| format!("Error sending message: {}", e))?;
|
.map_err(|e| format!("Error sending message: {}", e))?;
|
||||||
|
|
||||||
@@ -174,10 +180,58 @@ pub async fn send_message<R: Runtime>(
|
|||||||
headers.insert("WEBSOCKET-SESSION-ID".to_string(), websocket_id.into());
|
headers.insert("WEBSOCKET-SESSION-ID".to_string(), websocket_id.into());
|
||||||
|
|
||||||
let body = reqwest::Body::from(serde_json::to_string(&msg).unwrap());
|
let body = reqwest::Body::from(serde_json::to_string(&msg).unwrap());
|
||||||
let response =
|
let response = HttpClient::advanced_post(
|
||||||
HttpClient::advanced_post(&server_id, path.as_str(), Some(headers), query_params, Some(body))
|
&server_id,
|
||||||
|
path.as_str(),
|
||||||
|
Some(headers),
|
||||||
|
query_params,
|
||||||
|
Some(body),
|
||||||
|
)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| format!("Error cancel session: {}", e))?;
|
.map_err(|e| format!("Error cancel session: {}", e))?;
|
||||||
|
|
||||||
handle_raw_response(response).await?
|
handle_raw_response(response).await?
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn delete_session_chat(server_id: String, session_id: String) -> Result<bool, String> {
|
||||||
|
let response =
|
||||||
|
HttpClient::delete(&server_id, &format!("/chat/{}", session_id), None, None).await?;
|
||||||
|
|
||||||
|
if response.status().is_success() {
|
||||||
|
Ok(true)
|
||||||
|
} else {
|
||||||
|
Err(format!("Delete failed with status: {}", response.status()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn update_session_chat(
|
||||||
|
server_id: String,
|
||||||
|
session_id: String,
|
||||||
|
title: Option<String>,
|
||||||
|
context: Option<HashMap<String, Value>>,
|
||||||
|
) -> Result<bool, String> {
|
||||||
|
let mut body = HashMap::new();
|
||||||
|
if let Some(title) = title {
|
||||||
|
body.insert("title".to_string(), Value::String(title));
|
||||||
|
}
|
||||||
|
if let Some(context) = context {
|
||||||
|
body.insert(
|
||||||
|
"context".to_string(),
|
||||||
|
Value::Object(context.into_iter().collect()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let response = HttpClient::put(
|
||||||
|
&server_id,
|
||||||
|
&format!("/chat/{}", session_id),
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
Some(reqwest::Body::from(serde_json::to_string(&body).unwrap())),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(|e| format!("Error updating session: {}", e))?;
|
||||||
|
|
||||||
|
Ok(response.status().is_success())
|
||||||
|
}
|
||||||
|
|||||||
@@ -120,6 +120,8 @@ pub fn run() {
|
|||||||
assistant::open_session_chat,
|
assistant::open_session_chat,
|
||||||
assistant::close_session_chat,
|
assistant::close_session_chat,
|
||||||
assistant::cancel_session_chat,
|
assistant::cancel_session_chat,
|
||||||
|
assistant::delete_session_chat,
|
||||||
|
assistant::update_session_chat,
|
||||||
// server::get_coco_server_datasources,
|
// server::get_coco_server_datasources,
|
||||||
// server::get_coco_server_connectors,
|
// server::get_coco_server_connectors,
|
||||||
server::websocket::connect_to_server,
|
server::websocket::connect_to_server,
|
||||||
|
|||||||
@@ -1,6 +1,12 @@
|
|||||||
import { invoke } from '@tauri-apps/api/core';
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
|
|
||||||
import { ServerTokenResponse, Server, Connector, DataSource, GetResponse } from "@/types/commands"
|
import {
|
||||||
|
ServerTokenResponse,
|
||||||
|
Server,
|
||||||
|
Connector,
|
||||||
|
DataSource,
|
||||||
|
GetResponse,
|
||||||
|
} from "@/types/commands";
|
||||||
|
|
||||||
export function get_server_token(id: string): Promise<ServerTokenResponse> {
|
export function get_server_token(id: string): Promise<ServerTokenResponse> {
|
||||||
return invoke(`get_server_token`, { id });
|
return invoke(`get_server_token`, { id });
|
||||||
@@ -70,15 +76,18 @@ export function chat_history({
|
|||||||
serverId,
|
serverId,
|
||||||
from = 0,
|
from = 0,
|
||||||
size = 20,
|
size = 20,
|
||||||
|
query = "",
|
||||||
}: {
|
}: {
|
||||||
serverId: string;
|
serverId: string;
|
||||||
from?: number;
|
from?: number;
|
||||||
size?: number;
|
size?: number;
|
||||||
|
query?: string;
|
||||||
}): Promise<string> {
|
}): Promise<string> {
|
||||||
return invoke(`chat_history`, {
|
return invoke(`chat_history`, {
|
||||||
serverId,
|
serverId,
|
||||||
from,
|
from,
|
||||||
size,
|
size,
|
||||||
|
query,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -180,3 +189,18 @@ export function send_message({
|
|||||||
queryParams,
|
queryParams,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const delete_session_chat = (serverId: string, sessionId: string) => {
|
||||||
|
return invoke<boolean>(`delete_session_chat`, { serverId, sessionId });
|
||||||
|
};
|
||||||
|
|
||||||
|
export const update_session_chat = (payload: {
|
||||||
|
serverId: string;
|
||||||
|
sessionId: string;
|
||||||
|
title?: string;
|
||||||
|
context?: {
|
||||||
|
attachments?: string[];
|
||||||
|
};
|
||||||
|
}): Promise<boolean> => {
|
||||||
|
return invoke<boolean>("update_session_chat", payload);
|
||||||
|
};
|
||||||
|
|||||||
274
src/components/Common/HistoryList/index.tsx
Normal file
274
src/components/Common/HistoryList/index.tsx
Normal file
@@ -0,0 +1,274 @@
|
|||||||
|
import { Chat } from "@/components/Assistant/types";
|
||||||
|
import {
|
||||||
|
Description,
|
||||||
|
Dialog,
|
||||||
|
DialogPanel,
|
||||||
|
DialogTitle,
|
||||||
|
Input,
|
||||||
|
Menu,
|
||||||
|
MenuButton,
|
||||||
|
MenuItem,
|
||||||
|
MenuItems,
|
||||||
|
} from "@headlessui/react";
|
||||||
|
import { debounce, groupBy, isNil } from "lodash-es";
|
||||||
|
import { FC, useMemo, useState } from "react";
|
||||||
|
import dayjs from "dayjs";
|
||||||
|
import isSameOrAfter from "dayjs/plugin/isSameOrAfter";
|
||||||
|
import clsx from "clsx";
|
||||||
|
import { Ellipsis, Pencil, RefreshCcw, Search, Trash2 } from "lucide-react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
|
dayjs.extend(isSameOrAfter);
|
||||||
|
|
||||||
|
interface HistoryListProps {
|
||||||
|
list: Chat[];
|
||||||
|
active?: Chat;
|
||||||
|
onSearch: (keyword: string) => void;
|
||||||
|
onRefresh: () => void;
|
||||||
|
onSelect: (chat: Chat) => void;
|
||||||
|
onRename: (chat: Chat, title: string) => void;
|
||||||
|
onRemove: (chatId: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const HistoryList: FC<HistoryListProps> = (props) => {
|
||||||
|
const { list, active, onSearch, onRefresh, onSelect, onRename, onRemove } =
|
||||||
|
props;
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [isEdit, setIsEdit] = useState(false);
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
|
||||||
|
const sortedList = useMemo(() => {
|
||||||
|
if (isNil(list)) return {};
|
||||||
|
|
||||||
|
const now = dayjs();
|
||||||
|
|
||||||
|
return groupBy(list, (chat) => {
|
||||||
|
const date = dayjs(chat._source?.updated);
|
||||||
|
|
||||||
|
if (date.isSame(now, "day")) {
|
||||||
|
return "history_list.date.today";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (date.isSame(now.subtract(1, "day"), "day")) {
|
||||||
|
return "history_list.date.yesterday";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (date.isSameOrAfter(now.subtract(7, "day"), "day")) {
|
||||||
|
return "history_list.date.last7Days";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (date.isSameOrAfter(now.subtract(30, "day"), "day")) {
|
||||||
|
return "history_list.date.last30Days";
|
||||||
|
}
|
||||||
|
|
||||||
|
return date.format("YYYY-MM");
|
||||||
|
});
|
||||||
|
}, [list]);
|
||||||
|
|
||||||
|
const menuItems = [
|
||||||
|
// {
|
||||||
|
// label: "history_list.menu.share",
|
||||||
|
// icon: Share2,
|
||||||
|
// onClick: () => {},
|
||||||
|
// },
|
||||||
|
{
|
||||||
|
label: "history_list.menu.rename",
|
||||||
|
icon: Pencil,
|
||||||
|
onClick: () => {
|
||||||
|
setIsEdit(true);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "history_list.menu.delete",
|
||||||
|
icon: Trash2,
|
||||||
|
iconColor: "#FF2018",
|
||||||
|
onClick: () => {
|
||||||
|
setIsOpen(true);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const debouncedSearch = useMemo(() => {
|
||||||
|
return debounce((value: string) => onSearch(value), 500);
|
||||||
|
}, [onSearch]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={clsx(
|
||||||
|
"h-full overflow-auto px-3 py-2 text-sm bg-[#F3F4F6] dark:bg-[#1F2937]"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="flex gap-1 children:h-8">
|
||||||
|
<div className="flex-1 flex items-center gap-2 px-2 rounded-lg border transition border-[#E6E6E6] bg-[#F8F9FA] dark:bg-[#2B3444] dark:border-[#343D4D] focus-within:border-[#0061FF]">
|
||||||
|
<Search className="size-4 text-[#6B7280]" />
|
||||||
|
|
||||||
|
<Input
|
||||||
|
className="w-full bg-transparent outline-none"
|
||||||
|
placeholder={t("history_list.search.placeholder")}
|
||||||
|
onChange={(event) => {
|
||||||
|
debouncedSearch(event.target.value);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className="size-8 flex items-center justify-center rounded-lg border text-[#0072FF] border-[#E6E6E6] bg-[#F3F4F6] dark:border-[#343D4D] dark:bg-[#1F2937] hover:bg-[#F8F9FA] dark:hover:bg-[#353F4D] cursor-pointer transition"
|
||||||
|
onClick={onRefresh}
|
||||||
|
>
|
||||||
|
<RefreshCcw className="size-4" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-6">
|
||||||
|
{Object.entries(sortedList).map(([label, list]) => {
|
||||||
|
return (
|
||||||
|
<div key={label}>
|
||||||
|
<span className="text-xs text-[#999] px-3">{t(label)}</span>
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
{list.map((item) => {
|
||||||
|
const { _id, _source } = item;
|
||||||
|
|
||||||
|
const isActive = _id === active?._id;
|
||||||
|
const title = _source?.title ?? _id;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<li
|
||||||
|
key={_id}
|
||||||
|
className={clsx(
|
||||||
|
"flex items-center mt-1 h-10 rounded-lg cursor-pointer hover:bg-[#F8F9FA] dark:hover:bg-[#353F4D] transition",
|
||||||
|
{
|
||||||
|
"!bg-[#E5E7EB] dark:!bg-[#2B3444]": isActive,
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
onClick={() => {
|
||||||
|
if (!isActive) {
|
||||||
|
setIsEdit(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
onSelect(item);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={clsx("w-1 h-6 rounded-sm bg-[#0072FF]", {
|
||||||
|
"opacity-0": _id !== active?._id,
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="flex-1 flex items-center justify-between gap-2 px-2 overflow-hidden">
|
||||||
|
{isEdit && isActive ? (
|
||||||
|
<Input
|
||||||
|
defaultValue={title}
|
||||||
|
className="flex-1 -mx-px outline-none bg-transparent border border-[#0061FF] rounded-[4px]"
|
||||||
|
onKeyDown={(event) => {
|
||||||
|
if (event.key !== "Enter") return;
|
||||||
|
|
||||||
|
onRename(item, event.currentTarget.value);
|
||||||
|
|
||||||
|
setIsEdit(false);
|
||||||
|
}}
|
||||||
|
onBlur={(event) => {
|
||||||
|
onRename(item, event.target.value);
|
||||||
|
|
||||||
|
setIsEdit(false);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<span className="truncate">{title}</span>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Menu>
|
||||||
|
{isActive && !isEdit && (
|
||||||
|
<MenuButton>
|
||||||
|
<Ellipsis className="size-4 text-[#979797]" />
|
||||||
|
</MenuButton>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<MenuItems
|
||||||
|
anchor="bottom"
|
||||||
|
className="flex flex-col rounded-lg shadow-md z-100 bg-white dark:bg-[#202126] p-1 border border-black/2 dark:border-white/10"
|
||||||
|
>
|
||||||
|
{menuItems.map((menuItem) => {
|
||||||
|
const {
|
||||||
|
label,
|
||||||
|
icon: Icon,
|
||||||
|
iconColor,
|
||||||
|
onClick,
|
||||||
|
} = menuItem;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MenuItem key={label}>
|
||||||
|
<button
|
||||||
|
className="flex items-center gap-2 px-3 py-2 text-sm rounded-md hover:bg-[#EDEDED] dark:hover:bg-[#2B2C31] transition"
|
||||||
|
onClick={() => onClick()}
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
className="size-4"
|
||||||
|
style={{
|
||||||
|
color: iconColor,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<span>{t(label)}</span>
|
||||||
|
</button>
|
||||||
|
</MenuItem>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</MenuItems>
|
||||||
|
</Menu>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Dialog
|
||||||
|
open={isOpen}
|
||||||
|
onClose={() => setIsOpen(false)}
|
||||||
|
className="relative z-1000"
|
||||||
|
>
|
||||||
|
<div className="fixed inset-0 flex items-center justify-center w-screen">
|
||||||
|
<DialogPanel className="flex flex-col justify-between w-[360px] h-[160px] p-3 border border-[#e6e6e6] bg-white dark:bg-[#202126] dark:border-white/10 shadow-xl rounded-lg">
|
||||||
|
<div className="flex flex-col gap-3">
|
||||||
|
<DialogTitle className="text-base font-bold text-[#333]">
|
||||||
|
{t("history_list.delete_modal.title")}
|
||||||
|
</DialogTitle>
|
||||||
|
<Description className="text-sm text-[#333]">
|
||||||
|
{t("history_list.delete_modal.description", {
|
||||||
|
replace: [active?._source?.title || active?._id],
|
||||||
|
})}
|
||||||
|
</Description>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex gap-4 self-end">
|
||||||
|
<button
|
||||||
|
className="h-8 px-4 text-sm text-[#666666] bg-[#F8F9FA] dark:text-white dark:bg-[#202126] border border-[#E6E6E6] dark:border-white/10 rounded-lg"
|
||||||
|
onClick={() => setIsOpen(false)}
|
||||||
|
>
|
||||||
|
{t("history_list.delete_modal.button.cancel")}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="h-8 px-4 text-sm text-white bg-[#EF4444] rounded-lg"
|
||||||
|
onClick={() => {
|
||||||
|
if (!active?._id) return;
|
||||||
|
|
||||||
|
onRemove(active._id);
|
||||||
|
|
||||||
|
setIsOpen(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t("history_list.delete_modal.button.delete")}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</DialogPanel>
|
||||||
|
</div>
|
||||||
|
</Dialog>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default HistoryList;
|
||||||
@@ -294,5 +294,29 @@
|
|||||||
},
|
},
|
||||||
"error": {
|
"error": {
|
||||||
"message": "Sorry, there is an error in your Coco App. Please contact the administrator."
|
"message": "Sorry, there is an error in your Coco App. Please contact the administrator."
|
||||||
|
},
|
||||||
|
"history_list": {
|
||||||
|
"search": {
|
||||||
|
"placeholder": "Search"
|
||||||
|
},
|
||||||
|
"date": {
|
||||||
|
"today": "Today",
|
||||||
|
"yesterday": "Yesterday",
|
||||||
|
"last7Days": "Last 7 Days",
|
||||||
|
"last30Days": "Last 30 Days"
|
||||||
|
},
|
||||||
|
"menu": {
|
||||||
|
"share": "Share",
|
||||||
|
"rename": "Rename",
|
||||||
|
"delete": "Delete"
|
||||||
|
},
|
||||||
|
"delete_modal": {
|
||||||
|
"title": "Delete chat?",
|
||||||
|
"description": "This will delete \"{{0}}\"",
|
||||||
|
"button": {
|
||||||
|
"delete": "Delete",
|
||||||
|
"cancel": "Cancel"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -293,5 +293,29 @@
|
|||||||
},
|
},
|
||||||
"error": {
|
"error": {
|
||||||
"message": "抱歉,Coco 应用出现了错误。请联系管理员。"
|
"message": "抱歉,Coco 应用出现了错误。请联系管理员。"
|
||||||
|
},
|
||||||
|
"history_list": {
|
||||||
|
"search": {
|
||||||
|
"placeholder": "搜索"
|
||||||
|
},
|
||||||
|
"date": {
|
||||||
|
"today": "今天",
|
||||||
|
"yesterday": "昨天",
|
||||||
|
"last7Days": "最近 7 天",
|
||||||
|
"last30Days": "最近 30 天"
|
||||||
|
},
|
||||||
|
"menu": {
|
||||||
|
"share": "分享",
|
||||||
|
"rename": "重命名",
|
||||||
|
"delete": "删除"
|
||||||
|
},
|
||||||
|
"delete_modal": {
|
||||||
|
"title": "删除聊天?",
|
||||||
|
"description": "这将删除“{{0}}”",
|
||||||
|
"button": {
|
||||||
|
"delete": "删除",
|
||||||
|
"cancel": "取消"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,8 +15,7 @@ import { open } from "@tauri-apps/plugin-dialog";
|
|||||||
import { metadata, icon } from "tauri-plugin-fs-pro-api";
|
import { metadata, icon } from "tauri-plugin-fs-pro-api";
|
||||||
|
|
||||||
import ChatAI, { ChatAIRef } from "@/components/Assistant/Chat";
|
import ChatAI, { ChatAIRef } from "@/components/Assistant/Chat";
|
||||||
import { Sidebar } from "@/components/Assistant/Sidebar";
|
import type { Chat as typeChat } from "@/components/Assistant/types";
|
||||||
import type { Chat } from "@/components/Assistant/types";
|
|
||||||
import { useConnectStore } from "@/stores/connectStore";
|
import { useConnectStore } from "@/stores/connectStore";
|
||||||
import InputBox from "@/components/Search/InputBox";
|
import InputBox from "@/components/Search/InputBox";
|
||||||
import {
|
import {
|
||||||
@@ -25,8 +24,11 @@ import {
|
|||||||
close_session_chat,
|
close_session_chat,
|
||||||
open_session_chat,
|
open_session_chat,
|
||||||
get_datasources_by_server,
|
get_datasources_by_server,
|
||||||
|
delete_session_chat,
|
||||||
|
update_session_chat,
|
||||||
} from "@/commands";
|
} from "@/commands";
|
||||||
import { DataSource } from "@/types/commands"
|
import { DataSource } from "@/types/commands";
|
||||||
|
import HistoryList from "@/components/Common/HistoryList";
|
||||||
|
|
||||||
interface ChatProps {}
|
interface ChatProps {}
|
||||||
|
|
||||||
@@ -35,8 +37,8 @@ export default function Chat({}: ChatProps) {
|
|||||||
|
|
||||||
const chatAIRef = useRef<ChatAIRef>(null);
|
const chatAIRef = useRef<ChatAIRef>(null);
|
||||||
|
|
||||||
const [chats, setChats] = useState<Chat[]>([]);
|
const [chats, setChats] = useState<typeChat[]>([]);
|
||||||
const [activeChat, setActiveChat] = useState<Chat>();
|
const [activeChat, setActiveChat] = useState<typeChat>();
|
||||||
const [isSidebarOpen, setIsSidebarOpen] = useState(true);
|
const [isSidebarOpen, setIsSidebarOpen] = useState(true);
|
||||||
const isTyping = false;
|
const isTyping = false;
|
||||||
|
|
||||||
@@ -44,12 +46,13 @@ export default function Chat({}: ChatProps) {
|
|||||||
|
|
||||||
const [isSearchActive, setIsSearchActive] = useState(false);
|
const [isSearchActive, setIsSearchActive] = useState(false);
|
||||||
const [isDeepThinkActive, setIsDeepThinkActive] = useState(false);
|
const [isDeepThinkActive, setIsDeepThinkActive] = useState(false);
|
||||||
|
const [keyword, setKeyword] = useState("");
|
||||||
|
|
||||||
const isChatPage = true;
|
const isChatPage = true;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
getChatHistory();
|
getChatHistory();
|
||||||
}, []);
|
}, [keyword]);
|
||||||
|
|
||||||
const getChatHistory = async () => {
|
const getChatHistory = async () => {
|
||||||
try {
|
try {
|
||||||
@@ -57,6 +60,7 @@ export default function Chat({}: ChatProps) {
|
|||||||
serverId: currentService?.id,
|
serverId: currentService?.id,
|
||||||
from: 0,
|
from: 0,
|
||||||
size: 20,
|
size: 20,
|
||||||
|
query: keyword,
|
||||||
});
|
});
|
||||||
response = JSON.parse(response || "");
|
response = JSON.parse(response || "");
|
||||||
console.log("_history", response);
|
console.log("_history", response);
|
||||||
@@ -72,24 +76,24 @@ export default function Chat({}: ChatProps) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const deleteChat = (chatId: string) => {
|
// const deleteChat = (chatId: string) => {
|
||||||
setChats((prev) => prev.filter((chat) => chat._id !== chatId));
|
// setChats((prev) => prev.filter((chat) => chat._id !== chatId));
|
||||||
if (activeChat?._id === chatId) {
|
// if (activeChat?._id === chatId) {
|
||||||
const remainingChats = chats.filter((chat) => chat._id !== chatId);
|
// const remainingChats = chats.filter((chat) => chat._id !== chatId);
|
||||||
if (remainingChats.length > 0) {
|
// if (remainingChats.length > 0) {
|
||||||
setActiveChat(remainingChats[0]);
|
// setActiveChat(remainingChats[0]);
|
||||||
} else {
|
// } else {
|
||||||
chatAIRef.current?.init("");
|
// chatAIRef.current?.init("");
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
};
|
// };
|
||||||
|
|
||||||
const handleSendMessage = async (content: string) => {
|
const handleSendMessage = async (content: string) => {
|
||||||
setInput(content);
|
setInput(content);
|
||||||
chatAIRef.current?.init(content);
|
chatAIRef.current?.init(content);
|
||||||
};
|
};
|
||||||
|
|
||||||
const chatHistory = async (chat: Chat) => {
|
const chatHistory = async (chat: typeChat) => {
|
||||||
try {
|
try {
|
||||||
let response: any = await session_chat_history({
|
let response: any = await session_chat_history({
|
||||||
serverId: currentService?.id,
|
serverId: currentService?.id,
|
||||||
@@ -100,7 +104,7 @@ export default function Chat({}: ChatProps) {
|
|||||||
response = JSON.parse(response || "");
|
response = JSON.parse(response || "");
|
||||||
console.log("id_history", response);
|
console.log("id_history", response);
|
||||||
const hits = response?.hits?.hits || [];
|
const hits = response?.hits?.hits || [];
|
||||||
const updatedChat: Chat = {
|
const updatedChat: typeChat = {
|
||||||
...chat,
|
...chat,
|
||||||
messages: hits,
|
messages: hits,
|
||||||
};
|
};
|
||||||
@@ -203,6 +207,33 @@ export default function Chat({}: ChatProps) {
|
|||||||
return icon(path, size);
|
return icon(path, size);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const handleSearch = (keyword: string) => {
|
||||||
|
setKeyword(keyword);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRename = async (chat: typeChat, title: string) => {
|
||||||
|
if (!currentService?.id) return;
|
||||||
|
|
||||||
|
console.log("chat", chat);
|
||||||
|
console.log("title", title);
|
||||||
|
|
||||||
|
await update_session_chat({
|
||||||
|
serverId: currentService.id,
|
||||||
|
sessionId: chat?._id,
|
||||||
|
title,
|
||||||
|
});
|
||||||
|
|
||||||
|
getChatHistory();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDelete = async (id: string) => {
|
||||||
|
if (!currentService?.id) return;
|
||||||
|
|
||||||
|
await delete_session_chat(currentService.id, id);
|
||||||
|
|
||||||
|
getChatHistory();
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-screen">
|
<div className="h-screen">
|
||||||
<div className="h-[100%] flex">
|
<div className="h-[100%] flex">
|
||||||
@@ -213,15 +244,14 @@ export default function Chat({}: ChatProps) {
|
|||||||
isSidebarOpen ? "translate-x-0" : "-translate-x-full"
|
isSidebarOpen ? "translate-x-0" : "-translate-x-full"
|
||||||
} transition-transform duration-300 ease-in-out md:translate-x-0 md:static md:block bg-gray-100 dark:bg-gray-800`}
|
} transition-transform duration-300 ease-in-out md:translate-x-0 md:static md:block bg-gray-100 dark:bg-gray-800`}
|
||||||
>
|
>
|
||||||
<Sidebar
|
<HistoryList
|
||||||
chats={chats}
|
list={chats}
|
||||||
activeChat={activeChat}
|
active={activeChat}
|
||||||
onNewChat={() => {
|
onSearch={handleSearch}
|
||||||
chatAIRef.current?.clearChat();
|
onRefresh={getChatHistory}
|
||||||
}}
|
onSelect={onSelectChat}
|
||||||
onSelectChat={onSelectChat}
|
onRename={handleRename}
|
||||||
onDeleteChat={deleteChat}
|
onRemove={handleDelete}
|
||||||
fetchChatHistory={getChatHistory}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|||||||
@@ -23,8 +23,8 @@ export default {
|
|||||||
},
|
},
|
||||||
animation: {
|
animation: {
|
||||||
"fade-in": "fade-in 0.2s ease-in-out",
|
"fade-in": "fade-in 0.2s ease-in-out",
|
||||||
'typing': 'typing 1.5s ease-in-out infinite',
|
typing: "typing 1.5s ease-in-out infinite",
|
||||||
'shake': 'shake 0.5s ease-in-out',
|
shake: "shake 0.5s ease-in-out",
|
||||||
},
|
},
|
||||||
keyframes: {
|
keyframes: {
|
||||||
"fade-in": {
|
"fade-in": {
|
||||||
@@ -32,15 +32,15 @@ export default {
|
|||||||
"100%": { opacity: "1" },
|
"100%": { opacity: "1" },
|
||||||
},
|
},
|
||||||
typing: {
|
typing: {
|
||||||
'0%': { opacity: '0.3' },
|
"0%": { opacity: "0.3" },
|
||||||
'50%': { opacity: '1' },
|
"50%": { opacity: "1" },
|
||||||
'100%': { opacity: '0.3' },
|
"100%": { opacity: "0.3" },
|
||||||
},
|
},
|
||||||
shake: {
|
shake: {
|
||||||
'0%, 100%': { transform: 'rotate(0deg)' },
|
"0%, 100%": { transform: "rotate(0deg)" },
|
||||||
'25%': { transform: 'rotate(-20deg)' },
|
"25%": { transform: "rotate(-20deg)" },
|
||||||
'75%': { transform: 'rotate(20deg)' }
|
"75%": { transform: "rotate(20deg)" },
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
boxShadow: {
|
boxShadow: {
|
||||||
"window-custom": "0px 16px 32px 0px rgba(0,0,0,0.3)",
|
"window-custom": "0px 16px 32px 0px rgba(0,0,0,0.3)",
|
||||||
|
|||||||
Reference in New Issue
Block a user