mirror of
https://github.com/infinilabs/coco-app.git
synced 2025-12-16 19:47:43 +01:00
refactor: AI conversation rendering logic (#216)
* chore: chat message * chore: new chat id * chore: chat message * chore: chat messages * chore: think messages display * chore: chat message * chore: chat message * refactor: the message logic * chore: add useCallback params * chore: add ThinkingSegment * chore: add QueryIntent * chore: loading & text * chore: add JSON * chore: add * chore: source * chore: add ws-error listen * chore: style * fix: ws reconnect * chore: ws * chore: ws * refactor: AI conversation rendering logic * chore: update note
This commit is contained in:
@@ -30,6 +30,7 @@ Information about release notes of Coco Server is provided here.
|
|||||||
- Refactoring assistant api #195
|
- Refactoring assistant api #195
|
||||||
- Refactor: remove websocket_session_id from message request #206
|
- Refactor: remove websocket_session_id from message request #206
|
||||||
- Refactor: the display of search results and the logic of creating new chats #207
|
- Refactor: the display of search results and the logic of creating new chats #207
|
||||||
|
- Refactor: AI conversation rendering logic #216
|
||||||
- Refresh all server's info on purpose, get the actual health info #225
|
- Refresh all server's info on purpose, get the actual health info #225
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -52,15 +52,15 @@ where
|
|||||||
.await
|
.await
|
||||||
.map_err(|e| format!("Failed to parse JSON: {}", e))?;
|
.map_err(|e| format!("Failed to parse JSON: {}", e))?;
|
||||||
|
|
||||||
|
// dbg!(&body);
|
||||||
|
|
||||||
let search_response: SearchResponse<T> = serde_json::from_value(body)
|
let search_response: SearchResponse<T> = serde_json::from_value(body)
|
||||||
.map_err(|e| format!("Failed to deserialize search response: {}", e))?;
|
.map_err(|e| format!("Failed to deserialize search response: {}", e))?;
|
||||||
|
|
||||||
Ok(search_response)
|
Ok(search_response)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn parse_search_hits<T>(
|
pub async fn parse_search_hits<T>(response: Response) -> Result<Vec<SearchHit<T>>, Box<dyn Error>>
|
||||||
response: Response,
|
|
||||||
) -> Result<Vec<SearchHit<T>>, Box<dyn Error>>
|
|
||||||
where
|
where
|
||||||
T: for<'de> Deserialize<'de> + std::fmt::Debug,
|
T: for<'de> Deserialize<'de> + std::fmt::Debug,
|
||||||
{
|
{
|
||||||
@@ -69,13 +69,15 @@ where
|
|||||||
Ok(response.hits.hits)
|
Ok(response.hits.hits)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn parse_search_results<T>(
|
pub async fn parse_search_results<T>(response: Response) -> Result<Vec<T>, Box<dyn Error>>
|
||||||
response: Response,
|
|
||||||
) -> Result<Vec<T>, Box<dyn Error>>
|
|
||||||
where
|
where
|
||||||
T: for<'de> Deserialize<'de> + std::fmt::Debug,
|
T: for<'de> Deserialize<'de> + std::fmt::Debug,
|
||||||
{
|
{
|
||||||
Ok(parse_search_hits(response).await?.into_iter().map(|hit| hit._source).collect())
|
Ok(parse_search_hits(response)
|
||||||
|
.await?
|
||||||
|
.into_iter()
|
||||||
|
.map(|hit| hit._source)
|
||||||
|
.collect())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn parse_search_results_with_score<T>(
|
pub async fn parse_search_results_with_score<T>(
|
||||||
@@ -84,7 +86,11 @@ pub async fn parse_search_results_with_score<T>(
|
|||||||
where
|
where
|
||||||
T: for<'de> Deserialize<'de> + std::fmt::Debug,
|
T: for<'de> Deserialize<'de> + std::fmt::Debug,
|
||||||
{
|
{
|
||||||
Ok(parse_search_hits(response).await?.into_iter().map(|hit| (hit._source, hit._score)).collect())
|
Ok(parse_search_hits(response)
|
||||||
|
.await?
|
||||||
|
.into_iter()
|
||||||
|
.map(|hit| (hit._source, hit._score))
|
||||||
|
.collect())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize)]
|
#[derive(Debug, Clone, Serialize)]
|
||||||
@@ -139,4 +145,3 @@ pub struct MultiSourceQueryResponse {
|
|||||||
pub hits: Vec<QueryHits>,
|
pub hits: Vec<QueryHits>,
|
||||||
pub total_hits: usize,
|
pub total_hits: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -128,7 +128,7 @@ pub async fn connect_to_server(
|
|||||||
msg = ws.next() => {
|
msg = ws.next() => {
|
||||||
match msg {
|
match msg {
|
||||||
Some(Ok(Message::Text(text))) => {
|
Some(Ok(Message::Text(text))) => {
|
||||||
println!("Received message: {}", text);
|
//println!("Received message: {}", text);
|
||||||
let _ = app_handle_clone.emit("ws-message", text);
|
let _ = app_handle_clone.emit("ws-message", text);
|
||||||
},
|
},
|
||||||
Some(Err(WsError::ConnectionClosed)) => {
|
Some(Err(WsError::ConnectionClosed)) => {
|
||||||
@@ -169,7 +169,6 @@ pub async fn connect_to_server(
|
|||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn disconnect(state: tauri::State<'_, WebSocketManager>) -> Result<(), String> {
|
pub async fn disconnect(state: tauri::State<'_, WebSocketManager>) -> Result<(), String> {
|
||||||
|
|
||||||
// Send cancellation signal
|
// Send cancellation signal
|
||||||
if let Some(cancel_tx) = state.cancel_tx.lock().await.take() {
|
if let Some(cancel_tx) = state.cancel_tx.lock().await.take() {
|
||||||
let _ = cancel_tx.send(()).await;
|
let _ = cancel_tx.send(()).await;
|
||||||
|
|||||||
@@ -13,14 +13,15 @@ import { useTranslation } from "react-i18next";
|
|||||||
import { debounce } from "lodash-es";
|
import { debounce } from "lodash-es";
|
||||||
import { listen } from "@tauri-apps/api/event";
|
import { listen } from "@tauri-apps/api/event";
|
||||||
|
|
||||||
import { ChatMessage } from "./ChatMessage";
|
import { ChatMessage } from "@/components/ChatMessage";
|
||||||
import type { Chat } from "./types";
|
import type { Chat, IChunkData } from "./types";
|
||||||
import { useChatStore } from "@/stores/chatStore";
|
import { useChatStore } from "@/stores/chatStore";
|
||||||
import { useWindows } from "@/hooks/useWindows";
|
import { useWindows } from "@/hooks/useWindows";
|
||||||
import { ChatHeader } from "./ChatHeader";
|
import { ChatHeader } from "./ChatHeader";
|
||||||
import { Sidebar } from "@/components/Assistant/Sidebar";
|
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";
|
||||||
|
|
||||||
interface ChatAIProps {
|
interface ChatAIProps {
|
||||||
isTransitioned: boolean;
|
isTransitioned: boolean;
|
||||||
@@ -72,24 +73,16 @@ const ChatAI = memo(
|
|||||||
|
|
||||||
const { createWin } = useWindows();
|
const { createWin } = useWindows();
|
||||||
|
|
||||||
const {
|
const { curChatEnd, setCurChatEnd, connected, setConnected } =
|
||||||
curChatEnd,
|
useChatStore();
|
||||||
setCurChatEnd,
|
|
||||||
connected,
|
|
||||||
setConnected,
|
|
||||||
messages,
|
|
||||||
setMessages,
|
|
||||||
} = useChatStore();
|
|
||||||
const currentService = useConnectStore((state) => state.currentService);
|
const currentService = useConnectStore((state) => state.currentService);
|
||||||
|
|
||||||
const [activeChat, setActiveChat] = useState<Chat>();
|
const [activeChat, setActiveChat] = useState<Chat>();
|
||||||
const [isTyping, setIsTyping] = useState(false);
|
|
||||||
const [timedoutShow, setTimedoutShow] = useState(false);
|
const [timedoutShow, setTimedoutShow] = useState(false);
|
||||||
const [errorShow, setErrorShow] = useState(false);
|
const [errorShow, setErrorShow] = useState(false);
|
||||||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
const [curMessage, setCurMessage] = useState("");
|
|
||||||
|
|
||||||
const curChatEndRef = useRef(curChatEnd);
|
const curChatEndRef = useRef(curChatEnd);
|
||||||
curChatEndRef.current = curChatEnd;
|
curChatEndRef.current = curChatEnd;
|
||||||
|
|
||||||
@@ -103,14 +96,12 @@ const ChatAI = memo(
|
|||||||
activeChatProp && setActiveChat(activeChatProp);
|
activeChatProp && setActiveChat(activeChatProp);
|
||||||
}, [activeChatProp]);
|
}, [activeChatProp]);
|
||||||
|
|
||||||
const handleMessageChunk = useCallback((chunk: string) => {
|
const reconnect = async (server?: IServer) => {
|
||||||
setCurMessage((prev) => prev + chunk);
|
server = server || currentService;
|
||||||
}, []);
|
if (!server?.id) return;
|
||||||
|
|
||||||
const reconnect = async () => {
|
|
||||||
if (!currentService?.id) return;
|
|
||||||
try {
|
try {
|
||||||
await invoke("connect_to_server", { id: currentService?.id });
|
console.log("reconnect", 1111111, server.id);
|
||||||
|
await invoke("connect_to_server", { id: server.id });
|
||||||
setConnected(true);
|
setConnected(true);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to connect:", error);
|
console.error("Failed to connect:", error);
|
||||||
@@ -119,16 +110,87 @@ const ChatAI = memo(
|
|||||||
|
|
||||||
const messageTimeoutRef = useRef<NodeJS.Timeout>();
|
const messageTimeoutRef = useRef<NodeJS.Timeout>();
|
||||||
|
|
||||||
|
const [Question, setQuestion] = useState<string>("");
|
||||||
|
|
||||||
|
const [query_intent, setQuery_intent] = useState<IChunkData>();
|
||||||
|
const deal_query_intent = useCallback((data: IChunkData) => {
|
||||||
|
setQuery_intent((prev: IChunkData | undefined): IChunkData => {
|
||||||
|
if (!prev) return data;
|
||||||
|
return {
|
||||||
|
...prev,
|
||||||
|
message_chunk: prev.message_chunk + data.message_chunk,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const [fetch_source, setFetch_source] = useState<IChunkData>();
|
||||||
|
const deal_fetch_source = useCallback((data: IChunkData) => {
|
||||||
|
setFetch_source(data);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const [pick_source, setPick_source] = useState<IChunkData>();
|
||||||
|
const deal_pick_source = useCallback((data: IChunkData) => {
|
||||||
|
setPick_source((prev: IChunkData | undefined): IChunkData => {
|
||||||
|
if (!prev) return data;
|
||||||
|
return {
|
||||||
|
...prev,
|
||||||
|
message_chunk: prev.message_chunk + data.message_chunk,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const [deep_read, setDeep_read] = useState<IChunkData>();
|
||||||
|
const deal_deep_read = useCallback((data: IChunkData) => {
|
||||||
|
setDeep_read((prev: IChunkData | undefined): IChunkData => {
|
||||||
|
if (!prev) return data;
|
||||||
|
return {
|
||||||
|
...prev,
|
||||||
|
message_chunk: prev.message_chunk + "&" + data.message_chunk,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const [think, setThink] = useState<IChunkData>();
|
||||||
|
const deal_think = useCallback((data: IChunkData) => {
|
||||||
|
setThink((prev: IChunkData | undefined): IChunkData => {
|
||||||
|
if (!prev) return data;
|
||||||
|
return {
|
||||||
|
...prev,
|
||||||
|
message_chunk: prev.message_chunk + data.message_chunk,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const [response, setResponse] = useState<IChunkData>();
|
||||||
|
const deal_response = useCallback((data: IChunkData) => {
|
||||||
|
setResponse((prev: IChunkData | undefined): IChunkData => {
|
||||||
|
if (!prev) return data;
|
||||||
|
return {
|
||||||
|
...prev,
|
||||||
|
message_chunk: prev.message_chunk + data.message_chunk,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const clearCurrentChat = useCallback(() => {
|
||||||
|
setQuery_intent(undefined);
|
||||||
|
setFetch_source(undefined);
|
||||||
|
setPick_source(undefined);
|
||||||
|
setDeep_read(undefined);
|
||||||
|
setThink(undefined);
|
||||||
|
setResponse(undefined);
|
||||||
|
}, []);
|
||||||
|
|
||||||
const dealMsg = useCallback(
|
const dealMsg = useCallback(
|
||||||
(msg: string) => {
|
(msg: string) => {
|
||||||
// console.log("msg:", msg);
|
|
||||||
if (messageTimeoutRef.current) {
|
if (messageTimeoutRef.current) {
|
||||||
clearTimeout(messageTimeoutRef.current);
|
clearTimeout(messageTimeoutRef.current);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (msg.includes("PRIVATE")) {
|
if (!msg.includes("PRIVATE")) return;
|
||||||
|
|
||||||
messageTimeoutRef.current = setTimeout(() => {
|
messageTimeoutRef.current = setTimeout(() => {
|
||||||
if (!curChatEnd && isTyping) {
|
if (!curChatEnd) {
|
||||||
console.log("AI response timeout");
|
console.log("AI response timeout");
|
||||||
setTimedoutShow(true);
|
setTimedoutShow(true);
|
||||||
cancelChat();
|
cancelChat();
|
||||||
@@ -136,71 +198,89 @@ const ChatAI = memo(
|
|||||||
}, 30000);
|
}, 30000);
|
||||||
|
|
||||||
if (msg.includes("assistant finished output")) {
|
if (msg.includes("assistant finished output")) {
|
||||||
if (messageTimeoutRef.current) {
|
|
||||||
clearTimeout(messageTimeoutRef.current);
|
clearTimeout(messageTimeoutRef.current);
|
||||||
}
|
|
||||||
console.log("AI finished output");
|
console.log("AI finished output");
|
||||||
setCurChatEnd(true);
|
setCurChatEnd(true);
|
||||||
} else {
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const cleanedData = msg.replace(/^PRIVATE /, "");
|
const cleanedData = msg.replace(/^PRIVATE /, "");
|
||||||
try {
|
try {
|
||||||
// console.log("cleanedData", cleanedData);
|
|
||||||
const chunkData = JSON.parse(cleanedData);
|
const chunkData = JSON.parse(cleanedData);
|
||||||
// console.log("msg1:", chunkData, curIdRef.current);
|
|
||||||
|
|
||||||
if (chunkData.reply_to_message === curIdRef.current) {
|
if (chunkData.reply_to_message !== curIdRef.current) return;
|
||||||
handleMessageChunk(chunkData.message_chunk);
|
|
||||||
return chunkData.message_chunk;
|
// ['query_intent', 'fetch_source', 'pick_source', 'deep_read', 'think', 'response'];
|
||||||
|
if (chunkData.chunk_type === "query_intent") {
|
||||||
|
deal_query_intent(chunkData);
|
||||||
|
} else if (chunkData.chunk_type === "fetch_source") {
|
||||||
|
deal_fetch_source(chunkData);
|
||||||
|
} else if (chunkData.chunk_type === "pick_source") {
|
||||||
|
deal_pick_source(chunkData);
|
||||||
|
} else if (chunkData.chunk_type === "deep_read") {
|
||||||
|
deal_deep_read(chunkData);
|
||||||
|
} else if (chunkData.chunk_type === "think") {
|
||||||
|
deal_think(chunkData);
|
||||||
|
} else if (chunkData.chunk_type === "response") {
|
||||||
|
deal_response(chunkData);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("parse error:", error);
|
console.error("parse error:", error);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
[curChatEnd, isTyping]
|
[curChatEnd]
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const unlisten = listen("ws-message", (event) => {
|
if (curChatEnd) {
|
||||||
const data = dealMsg(String(event.payload));
|
simulateAssistantResponse();
|
||||||
if (data) {
|
|
||||||
setMessages((prev) => prev + data);
|
|
||||||
}
|
}
|
||||||
|
}, [curChatEnd]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let unlisten_error = null;
|
||||||
|
|
||||||
|
if (connected) {
|
||||||
|
setErrorShow(false);
|
||||||
|
unlisten_error = listen("ws-error", (event) => {
|
||||||
|
console.error("WebSocket error:", event.payload);
|
||||||
|
setConnected(false);
|
||||||
|
setErrorShow(true);
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
unlisten.then((fn) => fn());
|
unlisten_error?.then((fn) => fn());
|
||||||
};
|
};
|
||||||
}, []);
|
}, [connected]);
|
||||||
|
|
||||||
const assistantMessage = useMemo(() => {
|
useEffect(() => {
|
||||||
if (!activeChat?._id || (!curMessage && !messages)) return null;
|
let unlisten_message = null;
|
||||||
return {
|
if (connected) {
|
||||||
_id: activeChat._id,
|
setErrorShow(false);
|
||||||
_source: {
|
unlisten_message = listen("ws-message", (event) => {
|
||||||
type: "assistant",
|
dealMsg(String(event.payload));
|
||||||
message: curMessage || messages,
|
});
|
||||||
},
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
unlisten_message?.then((fn) => fn());
|
||||||
};
|
};
|
||||||
}, [activeChat?._id, curMessage, messages]);
|
}, [dealMsg, connected]);
|
||||||
|
|
||||||
const updatedChat = useMemo(() => {
|
const updatedChat = useMemo(() => {
|
||||||
if (!activeChat?._id || !assistantMessage) return null;
|
if (!activeChat?._id) return null;
|
||||||
return {
|
return {
|
||||||
...activeChat,
|
...activeChat,
|
||||||
messages: [...(activeChat.messages || []), assistantMessage],
|
messages: [...(activeChat.messages || [])],
|
||||||
};
|
};
|
||||||
}, [activeChat, assistantMessage]);
|
}, [activeChat]);
|
||||||
|
|
||||||
const simulateAssistantResponse = useCallback(() => {
|
const simulateAssistantResponse = useCallback(() => {
|
||||||
if (!updatedChat) return;
|
if (!updatedChat) return;
|
||||||
|
|
||||||
console.log("updatedChat:", updatedChat);
|
console.log("updatedChat:", updatedChat);
|
||||||
setActiveChat(updatedChat);
|
setActiveChat(updatedChat);
|
||||||
setMessages("");
|
|
||||||
setCurMessage("");
|
|
||||||
setIsTyping(false);
|
|
||||||
}, [updatedChat]);
|
}, [updatedChat]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -271,7 +351,15 @@ const ChatAI = memo(
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
scrollToBottom();
|
scrollToBottom();
|
||||||
}, [activeChat?.messages, isTyping, curMessage]);
|
}, [
|
||||||
|
activeChat?.messages,
|
||||||
|
query_intent?.message_chunk,
|
||||||
|
fetch_source?.message_chunk,
|
||||||
|
pick_source?.message_chunk,
|
||||||
|
deep_read?.message_chunk,
|
||||||
|
think?.message_chunk,
|
||||||
|
response?.message_chunk,
|
||||||
|
]);
|
||||||
|
|
||||||
const clearChat = () => {
|
const clearChat = () => {
|
||||||
console.log("clearChat");
|
console.log("clearChat");
|
||||||
@@ -281,12 +369,15 @@ const ChatAI = memo(
|
|||||||
clearChatPage && clearChatPage();
|
clearChatPage && clearChatPage();
|
||||||
};
|
};
|
||||||
|
|
||||||
const createNewChat = useCallback(async (value: string = "") => {
|
const createNewChat = useCallback(
|
||||||
|
async (value: string = "") => {
|
||||||
setTimedoutShow(false);
|
setTimedoutShow(false);
|
||||||
setErrorShow(false);
|
setErrorShow(false);
|
||||||
chatClose();
|
chatClose();
|
||||||
|
clearCurrentChat();
|
||||||
|
setQuestion(value);
|
||||||
try {
|
try {
|
||||||
console.log("sourceDataIds", sourceDataIds);
|
// console.log("sourceDataIds", sourceDataIds);
|
||||||
let response: any = await invoke("new_chat", {
|
let response: any = await invoke("new_chat", {
|
||||||
serverId: currentService?.id,
|
serverId: currentService?.id,
|
||||||
message: value,
|
message: value,
|
||||||
@@ -311,13 +402,14 @@ const ChatAI = memo(
|
|||||||
changeInput && changeInput("");
|
changeInput && changeInput("");
|
||||||
console.log("updatedChat2", updatedChat);
|
console.log("updatedChat2", updatedChat);
|
||||||
setActiveChat(updatedChat);
|
setActiveChat(updatedChat);
|
||||||
setIsTyping(true);
|
|
||||||
setCurChatEnd(false);
|
setCurChatEnd(false);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setErrorShow(true);
|
setErrorShow(true);
|
||||||
console.error("Failed to fetch user data:", error);
|
console.error("Failed to fetch user data:", error);
|
||||||
}
|
}
|
||||||
}, []);
|
},
|
||||||
|
[isSearchActive, isDeepThinkActive]
|
||||||
|
);
|
||||||
|
|
||||||
const init = (value: string) => {
|
const init = (value: string) => {
|
||||||
if (!curChatEnd) return;
|
if (!curChatEnd) return;
|
||||||
@@ -332,10 +424,14 @@ const ChatAI = memo(
|
|||||||
async (content: string, newChat?: Chat) => {
|
async (content: string, newChat?: Chat) => {
|
||||||
newChat = newChat || activeChat;
|
newChat = newChat || activeChat;
|
||||||
if (!newChat?._id || !content) return;
|
if (!newChat?._id || !content) return;
|
||||||
|
setQuestion(content);
|
||||||
|
await chatHistory(newChat);
|
||||||
|
|
||||||
setTimedoutShow(false);
|
setTimedoutShow(false);
|
||||||
setErrorShow(false);
|
setErrorShow(false);
|
||||||
|
clearCurrentChat();
|
||||||
try {
|
try {
|
||||||
console.log("sourceDataIds", sourceDataIds);
|
// console.log("sourceDataIds", sourceDataIds);
|
||||||
let response: any = await invoke("send_message", {
|
let response: any = await invoke("send_message", {
|
||||||
serverId: currentService?.id,
|
serverId: currentService?.id,
|
||||||
sessionId: newChat?._id,
|
sessionId: newChat?._id,
|
||||||
@@ -358,7 +454,6 @@ const ChatAI = memo(
|
|||||||
changeInput && changeInput("");
|
changeInput && changeInput("");
|
||||||
console.log("updatedChat2", updatedChat);
|
console.log("updatedChat2", updatedChat);
|
||||||
setActiveChat(updatedChat);
|
setActiveChat(updatedChat);
|
||||||
setIsTyping(true);
|
|
||||||
setCurChatEnd(false);
|
setCurChatEnd(false);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setErrorShow(true);
|
setErrorShow(true);
|
||||||
@@ -383,12 +478,7 @@ const ChatAI = memo(
|
|||||||
};
|
};
|
||||||
|
|
||||||
const cancelChat = async () => {
|
const cancelChat = async () => {
|
||||||
if (curMessage || messages) {
|
|
||||||
simulateAssistantResponse();
|
|
||||||
}
|
|
||||||
|
|
||||||
setCurChatEnd(true);
|
setCurChatEnd(true);
|
||||||
setIsTyping(false);
|
|
||||||
if (!activeChat?._id) return;
|
if (!activeChat?._id) return;
|
||||||
try {
|
try {
|
||||||
let response: any = await invoke("cancel_session_chat", {
|
let response: any = await invoke("cancel_session_chat", {
|
||||||
@@ -427,10 +517,7 @@ const ChatAI = memo(
|
|||||||
clearTimeout(messageTimeoutRef.current);
|
clearTimeout(messageTimeoutRef.current);
|
||||||
}
|
}
|
||||||
chatClose();
|
chatClose();
|
||||||
setMessages("");
|
|
||||||
setCurMessage("");
|
|
||||||
setActiveChat(undefined);
|
setActiveChat(undefined);
|
||||||
setIsTyping(false);
|
|
||||||
setCurChatEnd(true);
|
setCurChatEnd(true);
|
||||||
scrollToBottom.cancel();
|
scrollToBottom.cancel();
|
||||||
};
|
};
|
||||||
@@ -564,6 +651,7 @@ const ChatAI = memo(
|
|||||||
}}
|
}}
|
||||||
isSidebarOpen={isSidebarOpenChat}
|
isSidebarOpen={isSidebarOpenChat}
|
||||||
activeChat={activeChat}
|
activeChat={activeChat}
|
||||||
|
reconnect={reconnect}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Chat messages */}
|
{/* Chat messages */}
|
||||||
@@ -577,32 +665,42 @@ const ChatAI = memo(
|
|||||||
message: t("assistant.chat.greetings"),
|
message: t("assistant.chat.greetings"),
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
isTyping={false}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{activeChat?.messages?.map((message, index) => (
|
{activeChat?.messages?.map((message, index) => (
|
||||||
<ChatMessage
|
<ChatMessage
|
||||||
key={message._id + index}
|
key={message._id + index}
|
||||||
message={message}
|
message={message}
|
||||||
isTyping={
|
isTyping={false}
|
||||||
isTyping &&
|
onResend={handleSendMessage}
|
||||||
index === (activeChat.messages?.length || 0) - 1 &&
|
|
||||||
message._source?.type === "assistant"
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
{!curChatEnd && activeChat?._id ? (
|
{(query_intent ||
|
||||||
|
fetch_source ||
|
||||||
|
pick_source ||
|
||||||
|
deep_read ||
|
||||||
|
think ||
|
||||||
|
response) &&
|
||||||
|
activeChat?._id ? (
|
||||||
<ChatMessage
|
<ChatMessage
|
||||||
key={"last"}
|
key={"current"}
|
||||||
message={{
|
message={{
|
||||||
_id: activeChat?._id,
|
_id: "current",
|
||||||
_source: {
|
_source: {
|
||||||
type: "assistant",
|
type: "assistant",
|
||||||
message: curMessage,
|
message: "",
|
||||||
|
question: Question,
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
|
onResend={handleSendMessage}
|
||||||
isTyping={!curChatEnd}
|
isTyping={!curChatEnd}
|
||||||
|
query_intent={query_intent}
|
||||||
|
fetch_source={fetch_source}
|
||||||
|
pick_source={pick_source}
|
||||||
|
deep_read={deep_read}
|
||||||
|
think={think}
|
||||||
|
response={response}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
@@ -614,8 +712,10 @@ const ChatAI = memo(
|
|||||||
_source: {
|
_source: {
|
||||||
type: "assistant",
|
type: "assistant",
|
||||||
message: t("assistant.chat.timedout"),
|
message: t("assistant.chat.timedout"),
|
||||||
|
question: Question,
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
|
onResend={handleSendMessage}
|
||||||
isTyping={false}
|
isTyping={false}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
@@ -628,8 +728,10 @@ const ChatAI = memo(
|
|||||||
_source: {
|
_source: {
|
||||||
type: "assistant",
|
type: "assistant",
|
||||||
message: t("assistant.chat.error"),
|
message: t("assistant.chat.error"),
|
||||||
|
question: Question,
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
|
onResend={handleSendMessage}
|
||||||
isTyping={false}
|
isTyping={false}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ interface ChatHeaderProps {
|
|||||||
setIsSidebarOpen: () => void;
|
setIsSidebarOpen: () => void;
|
||||||
isSidebarOpen: boolean;
|
isSidebarOpen: boolean;
|
||||||
activeChat: Chat | undefined;
|
activeChat: Chat | undefined;
|
||||||
|
reconnect: (server?: IServer) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ChatHeader({
|
export function ChatHeader({
|
||||||
@@ -45,6 +46,7 @@ export function ChatHeader({
|
|||||||
onOpenChatAI,
|
onOpenChatAI,
|
||||||
setIsSidebarOpen,
|
setIsSidebarOpen,
|
||||||
activeChat,
|
activeChat,
|
||||||
|
reconnect,
|
||||||
}: ChatHeaderProps) {
|
}: ChatHeaderProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
@@ -52,7 +54,7 @@ export function ChatHeader({
|
|||||||
const isPinned = useAppStore((state) => state.isPinned);
|
const isPinned = useAppStore((state) => state.isPinned);
|
||||||
const setIsPinned = useAppStore((state) => state.setIsPinned);
|
const setIsPinned = useAppStore((state) => state.setIsPinned);
|
||||||
|
|
||||||
const { setConnected, setMessages } = useChatStore();
|
const { connected, setConnected, setMessages } = useChatStore();
|
||||||
|
|
||||||
const [serverList, setServerList] = useState<IServer[]>([]);
|
const [serverList, setServerList] = useState<IServer[]>([]);
|
||||||
const [isRefreshing, setIsRefreshing] = useState(false);
|
const [isRefreshing, setIsRefreshing] = useState(false);
|
||||||
@@ -68,9 +70,18 @@ export function ChatHeader({
|
|||||||
);
|
);
|
||||||
// console.log("list_coco_servers", enabledServers);
|
// console.log("list_coco_servers", enabledServers);
|
||||||
setServerList(enabledServers);
|
setServerList(enabledServers);
|
||||||
if (resetSelection && enabledServers.length > 0 && !currentService) {
|
|
||||||
|
if (resetSelection && enabledServers.length > 0) {
|
||||||
|
const currentServiceExists = enabledServers.some(
|
||||||
|
server => server.id === currentService?.id
|
||||||
|
);
|
||||||
|
|
||||||
|
if (currentServiceExists) {
|
||||||
|
switchServer(currentService);
|
||||||
|
} else {
|
||||||
switchServer(enabledServers[enabledServers.length - 1]);
|
switchServer(enabledServers[enabledServers.length - 1]);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.catch((err: any) => {
|
.catch((err: any) => {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
@@ -79,10 +90,17 @@ export function ChatHeader({
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchServers(true);
|
fetchServers(true);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
// Cleanup logic if needed
|
||||||
|
disconnect();
|
||||||
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const disconnect = async () => {
|
const disconnect = async () => {
|
||||||
|
if (!connected) return;
|
||||||
try {
|
try {
|
||||||
|
console.log("disconnect", 33333333);
|
||||||
await invoke("disconnect");
|
await invoke("disconnect");
|
||||||
setConnected(false);
|
setConnected(false);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -90,15 +108,6 @@ export function ChatHeader({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const connect = async (server: IServer) => {
|
|
||||||
try {
|
|
||||||
await invoke("connect_to_server", { id: server.id });
|
|
||||||
setConnected(true);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Failed to connect:", error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const switchServer = async (server: IServer) => {
|
const switchServer = async (server: IServer) => {
|
||||||
try {
|
try {
|
||||||
// Switch UI first, then switch server connection
|
// Switch UI first, then switch server connection
|
||||||
@@ -108,7 +117,7 @@ export function ChatHeader({
|
|||||||
onCreateNewChat();
|
onCreateNewChat();
|
||||||
//
|
//
|
||||||
await disconnect();
|
await disconnect();
|
||||||
await connect(server);
|
reconnect && reconnect(server);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("switchServer:", error);
|
console.error("switchServer:", error);
|
||||||
}
|
}
|
||||||
@@ -185,7 +194,7 @@ export function ChatHeader({
|
|||||||
|
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-sm font-medium text-gray-900 dark:text-gray-100">
|
<h2 className="text-sm font-medium text-gray-900 dark:text-gray-100">
|
||||||
{activeChat?.title || activeChat?._id}
|
{activeChat?._source?.title || activeChat?._id}
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,133 +0,0 @@
|
|||||||
import { Brain, ChevronDown, ChevronUp } from "lucide-react";
|
|
||||||
import { useState, memo } from "react";
|
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
|
|
||||||
import type { Message } from "./types";
|
|
||||||
import Markdown from "./Markdown";
|
|
||||||
import { formatThinkingMessage } from "@/utils/index";
|
|
||||||
import logoImg from "@/assets/icon.svg";
|
|
||||||
import { SourceResult } from "./SourceResult";
|
|
||||||
// import { ThinkingSteps } from "./ThinkingSteps";
|
|
||||||
|
|
||||||
interface ChatMessageProps {
|
|
||||||
message: Message;
|
|
||||||
isTyping?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ChatMessage = memo(function ChatMessage({
|
|
||||||
message,
|
|
||||||
isTyping,
|
|
||||||
}: ChatMessageProps) {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const [isThinkingExpanded, setIsThinkingExpanded] = useState(true);
|
|
||||||
|
|
||||||
const isAssistant = message._source?.type === "assistant";
|
|
||||||
|
|
||||||
const segments = formatThinkingMessage(message._source?.message || "");
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={`py-8 flex ${isAssistant ? "justify-start" : "justify-end"}`}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className={`max-w-3xl px-4 sm:px-6 lg:px-8 flex gap-4 ${
|
|
||||||
isAssistant ? "" : "flex-row-reverse"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className={`flex-1 space-y-2 ${
|
|
||||||
isAssistant ? "text-left" : "text-right"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<p className="flex items-center gap-4 font-semibold text-sm text-[#333] dark:text-[#d8d8d8]">
|
|
||||||
{isAssistant ? (
|
|
||||||
<img
|
|
||||||
src={logoImg}
|
|
||||||
className="w-6 h-6"
|
|
||||||
alt={t("assistant.message.logo")}
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
{isAssistant ? t("assistant.message.aiName") : ""}
|
|
||||||
</p>
|
|
||||||
<div className="prose dark:prose-invert prose-sm max-w-none">
|
|
||||||
<div className="text-[#333] dark:text-[#d8d8d8] leading-relaxed">
|
|
||||||
{isAssistant ? (
|
|
||||||
<>
|
|
||||||
{/* <ThinkingSteps currentStep={4} /> */}
|
|
||||||
{segments.map((segment, index) => (
|
|
||||||
<span key={index}>
|
|
||||||
{segment.isSource ? (
|
|
||||||
<SourceResult text={segment.text} />
|
|
||||||
) : segment.isThinking ? (
|
|
||||||
<div className="space-y-2 mb-3 w-full">
|
|
||||||
<button
|
|
||||||
onClick={() =>
|
|
||||||
setIsThinkingExpanded((prev) => !prev)
|
|
||||||
}
|
|
||||||
className="inline-flex items-center gap-2 px-2 py-1 rounded-xl transition-colors border border-[#E6E6E6] dark:border-[#272626]"
|
|
||||||
>
|
|
||||||
{isTyping ? (
|
|
||||||
<>
|
|
||||||
<Brain className="w-4 h-4 animate-pulse text-[#999999]" />
|
|
||||||
<span className="text-xs text-[#999999] italic">
|
|
||||||
{t("assistant.message.thinking")}
|
|
||||||
</span>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<Brain className="w-4 h-4 text-[#999999]" />
|
|
||||||
<span className="text-xs text-[#999999]">
|
|
||||||
{t("assistant.message.thoughtTime")}
|
|
||||||
</span>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{segment.thinkContent &&
|
|
||||||
(isThinkingExpanded ? (
|
|
||||||
<ChevronUp className="w-4 h-4" />
|
|
||||||
) : (
|
|
||||||
<ChevronDown className="w-4 h-4" />
|
|
||||||
))}
|
|
||||||
</button>
|
|
||||||
{isThinkingExpanded && segment.thinkContent && (
|
|
||||||
<div className="pl-2 border-l-2 border-[e5e5e5]">
|
|
||||||
<div className="text-[#8b8b8b] dark:text-[#a6a6a6] space-y-2">
|
|
||||||
{segment.thinkContent.split("\n").map(
|
|
||||||
(paragraph, idx) =>
|
|
||||||
paragraph.trim() && (
|
|
||||||
<p key={idx} className="text-sm">
|
|
||||||
{paragraph}
|
|
||||||
</p>
|
|
||||||
)
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
) : segment.text ? (
|
|
||||||
<div className="space-y-4">
|
|
||||||
<Markdown
|
|
||||||
key={`${index}-${isTyping ? "loading" : "done"}`}
|
|
||||||
content={segment.text}
|
|
||||||
loading={isTyping}
|
|
||||||
onDoubleClickCapture={() => {}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
</span>
|
|
||||||
))}
|
|
||||||
{isTyping && (
|
|
||||||
<span className="inline-block w-1.5 h-4 ml-0.5 -mb-0.5 bg-current animate-pulse" />
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<div className="px-3 py-2 bg-white dark:bg-[#202126] rounded-xl border border-black/12 dark:border-black/15 font-normal text-sm text-[#333333] dark:text-[#D8D8D8]">
|
|
||||||
{message._source?.message || ""}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
@@ -2,6 +2,7 @@ import { useTranslation } from "react-i18next";
|
|||||||
import { MessageSquare, Plus } from "lucide-react";
|
import { MessageSquare, Plus } from "lucide-react";
|
||||||
|
|
||||||
import type { Chat } from "./types";
|
import type { Chat } from "./types";
|
||||||
|
|
||||||
interface SidebarProps {
|
interface SidebarProps {
|
||||||
chats: Chat[];
|
chats: Chat[];
|
||||||
activeChat: Chat | undefined;
|
activeChat: Chat | undefined;
|
||||||
|
|||||||
@@ -12,54 +12,24 @@ import { OpenURLWithBrowser } from "@/utils/index";
|
|||||||
|
|
||||||
interface SourceResultProps {
|
interface SourceResultProps {
|
||||||
text: string;
|
text: string;
|
||||||
|
prefix?: string;
|
||||||
|
data?: any[];
|
||||||
|
total?: string;
|
||||||
|
type?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SourceItem {
|
export const SourceResult = ({
|
||||||
url?: string;
|
prefix,
|
||||||
title?: string;
|
data,
|
||||||
category?: string;
|
total,
|
||||||
source?: {
|
type,
|
||||||
name: string;
|
}: SourceResultProps) => {
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function SourceResult({ text }: SourceResultProps) {
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [isSourceExpanded, setIsSourceExpanded] = useState(false);
|
const [isSourceExpanded, setIsSourceExpanded] = useState(false);
|
||||||
|
|
||||||
if (!text?.includes("<Source")) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const getSourceData = (): SourceItem[] => {
|
|
||||||
try {
|
|
||||||
// console.log("Raw text:", text);
|
|
||||||
|
|
||||||
const sourceRegex = /<Source.*?total=(\d+)>\s*(.*?)\s*<\/Source>/s;
|
|
||||||
const sourceMatch = text.match(sourceRegex);
|
|
||||||
|
|
||||||
if (!sourceMatch) {
|
|
||||||
// console.log("No source match found");
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
const jsonContent = sourceMatch[2];
|
|
||||||
// console.log("Extracted JSON:", jsonContent);
|
|
||||||
|
|
||||||
const parsedData = JSON.parse(jsonContent);
|
|
||||||
return parsedData;
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Failed to parse source data:", error);
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const totalResults = text.match(/total=["']?(\d+)["']?/)?.[1] || "0";
|
|
||||||
const sourceData = getSourceData();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`mt-2 ${
|
className={`mt-2 mb-2 w-[98%] ${
|
||||||
isSourceExpanded
|
isSourceExpanded
|
||||||
? "rounded-lg overflow-hidden border border-[#E6E6E6] dark:border-[#272626]"
|
? "rounded-lg overflow-hidden border border-[#E6E6E6] dark:border-[#272626]"
|
||||||
: ""
|
: ""
|
||||||
@@ -67,53 +37,58 @@ export function SourceResult({ text }: SourceResultProps) {
|
|||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
onClick={() => setIsSourceExpanded((prev) => !prev)}
|
onClick={() => setIsSourceExpanded((prev) => !prev)}
|
||||||
className={`inline-flex justify-between items-center gap-2 px-2 py-1 rounded-xl transition-colors ${
|
className={`inline-flex justify-between items-center gap-2 px-2 py-1 rounded-xl transition-colors whitespace-nowrap ${
|
||||||
isSourceExpanded
|
isSourceExpanded
|
||||||
? "w-full"
|
? "w-full"
|
||||||
: "border border-[#E6E6E6] dark:border-[#272626]"
|
: "border border-[#E6E6E6] dark:border-[#272626]"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<div className="flex gap-2">
|
<div className="flex-1 min-w-0 flex items-center gap-2">
|
||||||
<Search className="w-4 h-4 text-[#999999] dark:text-[#999999]" />
|
<Search className="w-4 h-4 text-[#38C200] flex-shrink-0" />
|
||||||
<span className="text-xs text-[#999999] dark:text-[#999999]">
|
<span className="text-xs text-[#999999]">
|
||||||
{t("assistant.source.foundResults", {
|
{t(`assistant.message.steps.${type}`, {
|
||||||
count: Number(totalResults),
|
count: Number(total),
|
||||||
})}
|
})}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{isSourceExpanded ? (
|
{isSourceExpanded ? (
|
||||||
<ChevronUp className="w-4 h-4 text-[#999999] dark:text-[#999999]" />
|
<ChevronUp className="w-4 h-4 text-[#999999]" />
|
||||||
) : (
|
) : (
|
||||||
<ChevronDown className="w-4 h-4 text-[#999999] dark:text-[#999999]" />
|
<ChevronDown className="w-4 h-4 text-[#999999]" />
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{isSourceExpanded && (
|
{isSourceExpanded && (
|
||||||
<div className="">
|
<>
|
||||||
{sourceData.map((item, idx) => (
|
{prefix && (
|
||||||
|
<div className="px-3 py-2 bg-[#F7F7F7] dark:bg-[#1E1E1E] text-[#666666] dark:text-[#A3A3A3] text-xs leading-relaxed border-b border-[#E6E6E6] dark:border-[#272626]">
|
||||||
|
{prefix}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{data?.map((item, idx) => (
|
||||||
<div
|
<div
|
||||||
key={idx}
|
key={idx}
|
||||||
onClick={() => item.url && OpenURLWithBrowser(item.url)}
|
onClick={() => item.url && OpenURLWithBrowser(item.url)}
|
||||||
className="group flex items-center p-2 hover:bg-[#F7F7F7] dark:hover:bg-[#2C2C2C] border-b border-[#E6E6E6] dark:border-[#272626] last:border-b-0 cursor-pointer transition-colors"
|
className="group flex items-center p-2 hover:bg-[#F7F7F7] dark:hover:bg-[#2C2C2C] border-b border-[#E6E6E6] dark:border-[#272626] last:border-b-0 cursor-pointer transition-colors"
|
||||||
>
|
>
|
||||||
<div className="flex-1 min-w-0 flex items-center gap-2">
|
<div className="w-full flex items-center gap-2">
|
||||||
<div className="flex-1 min-w-0 flex items-center gap-1">
|
<div className="w-[75%] flex items-center gap-1">
|
||||||
<Globe className="w-3 h-3" />
|
<Globe className="w-3 h-3 flex-shrink-0" />
|
||||||
<div className="text-xs text-[#333333] dark:text-[#D8D8D8] truncate font-normal group-hover:text-[#0072FF] dark:group-hover:text-[#0072FF]">
|
<div className="text-xs text-[#333333] dark:text-[#D8D8D8] truncate font-normal group-hover:text-[#0072FF] dark:group-hover:text-[#0072FF]">
|
||||||
{item.title || item.category}
|
{item.title || item.category}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2 flex-shrink-0">
|
<div className="w-[25%] flex items-center justify-end gap-2">
|
||||||
<span className="text-xs text-[#999999] dark:text-[#999999]">
|
<span className="text-xs text-[#999999] dark:text-[#999999] truncate">
|
||||||
{item.source?.name}
|
{item.source?.name}
|
||||||
</span>
|
</span>
|
||||||
<SquareArrowOutUpRight className="w-3 h-3 text-[#999999] dark:text-[#999999]" />
|
<SquareArrowOutUpRight className="w-3 h-3 text-[#999999] dark:text-[#999999] flex-shrink-0" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -1,84 +0,0 @@
|
|||||||
import { useState } from "react";
|
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
import { Brain, ChevronDown, ChevronUp } from "lucide-react";
|
|
||||||
|
|
||||||
interface ThinkingStepsProps {
|
|
||||||
currentStep?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ThinkingSteps = ({ currentStep = 4 }: ThinkingStepsProps) => {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const [isExpanded, setIsExpanded] = useState(true);
|
|
||||||
|
|
||||||
const steps = [
|
|
||||||
"Understand the query",
|
|
||||||
"Retrieve documents",
|
|
||||||
"Intelligent pre-selection",
|
|
||||||
"Deep reading",
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={`mt-2 ${
|
|
||||||
isExpanded
|
|
||||||
? "rounded-lg overflow-hidden border border-[#E6E6E6] dark:border-[#272626]"
|
|
||||||
: ""
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
onClick={() => setIsExpanded((prev) => !prev)}
|
|
||||||
className={`inline-flex justify-between items-center gap-2 px-2 py-1 rounded-xl transition-colors ${
|
|
||||||
isExpanded
|
|
||||||
? "w-full"
|
|
||||||
: "border border-[#E6E6E6] dark:border-[#272626]"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<Brain className="w-4 h-4 text-[#999999]" />
|
|
||||||
<span className="text-xs text-[#999999]">
|
|
||||||
{t("assistant.message.thinking")}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
{isExpanded ? (
|
|
||||||
<ChevronUp className="w-4 h-4 text-[#999999]" />
|
|
||||||
) : (
|
|
||||||
<ChevronDown className="w-4 h-4 text-[#999999]" />
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{isExpanded && (
|
|
||||||
<div className="p-2 space-y-2">
|
|
||||||
{steps.map((step, index) => (
|
|
||||||
<div
|
|
||||||
key={index}
|
|
||||||
className="flex items-center gap-2 p-2 hover:bg-[#F7F7F7] dark:hover:bg-[#2C2C2C] border-b border-[#E6E6E6] dark:border-[#272626] last:border-b-0"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className={`w-4 h-4 rounded-full ${
|
|
||||||
index < currentStep - 1
|
|
||||||
? "bg-[#22C493]"
|
|
||||||
: "border-2 border-[#0072FF]"
|
|
||||||
} flex items-center justify-center flex-shrink-0`}
|
|
||||||
>
|
|
||||||
{index < currentStep - 1 ? (
|
|
||||||
<span className="text-white text-xs">✓</span>
|
|
||||||
) : (
|
|
||||||
<span className="text-[#0072FF] text-xs animate-pulse">
|
|
||||||
⋯
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<span className="text-xs text-[#333333] dark:text-[#D8D8D8]">
|
|
||||||
{t(
|
|
||||||
`assistant.message.steps.${step
|
|
||||||
.toLowerCase()
|
|
||||||
.replace(/\s+/g, "_")}`
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
.markdown-content p {
|
|
||||||
margin: 1em 0;
|
|
||||||
line-height: 1.6;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-content strong {
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
@@ -12,6 +12,8 @@ export interface ISource {
|
|||||||
session_id?: string;
|
session_id?: string;
|
||||||
type?: string;
|
type?: string;
|
||||||
message?: any;
|
message?: any;
|
||||||
|
title?: string;
|
||||||
|
question?: string;
|
||||||
}
|
}
|
||||||
export interface Chat {
|
export interface Chat {
|
||||||
_id: string;
|
_id: string;
|
||||||
@@ -25,3 +27,14 @@ export interface Chat {
|
|||||||
payload?: string;
|
payload?: string;
|
||||||
[key: string]: any;
|
[key: string]: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface IChunkData {
|
||||||
|
session_id: string;
|
||||||
|
message_id: string;
|
||||||
|
message_type: string;
|
||||||
|
reply_to_message: string;
|
||||||
|
chunk_sequence: number;
|
||||||
|
chunk_type: string;
|
||||||
|
message_chunk: string;
|
||||||
|
[key: string]: any;
|
||||||
|
}
|
||||||
|
|||||||
98
src/components/ChatMessage/DeepRead.tsx
Normal file
98
src/components/ChatMessage/DeepRead.tsx
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
import { ChevronDown, ChevronUp, Loader, BadgeCheck } from "lucide-react";
|
||||||
|
import { useState, useEffect } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
|
import type { IChunkData } from "@/components/Assistant/types";
|
||||||
|
|
||||||
|
interface DeepReadeProps {
|
||||||
|
ChunkData?: IChunkData;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DeepRead = ({ ChunkData }: DeepReadeProps) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const [isThinkingExpanded, setIsThinkingExpanded] = useState(true);
|
||||||
|
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [prevContent, setPrevContent] = useState("");
|
||||||
|
|
||||||
|
const [Data, setData] = useState<string[]>([]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!ChunkData?.message_chunk) return;
|
||||||
|
const timerID = setTimeout(() => {
|
||||||
|
if (ChunkData.message_chunk === prevContent) {
|
||||||
|
setLoading(false);
|
||||||
|
clearTimeout(timerID);
|
||||||
|
}
|
||||||
|
}, 500);
|
||||||
|
setPrevContent(ChunkData.message_chunk);
|
||||||
|
return () => {
|
||||||
|
timerID && clearTimeout(timerID);
|
||||||
|
};
|
||||||
|
}, [ChunkData?.message_chunk, prevContent, loading]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!ChunkData?.message_chunk) return;
|
||||||
|
try {
|
||||||
|
if (ChunkData.message_chunk.includes("&")) {
|
||||||
|
const contentArray = ChunkData.message_chunk.split("&").filter(Boolean);
|
||||||
|
setData(contentArray);
|
||||||
|
} else {
|
||||||
|
setData([ChunkData.message_chunk]);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Failed to parse query data:", e);
|
||||||
|
}
|
||||||
|
}, [ChunkData?.message_chunk]);
|
||||||
|
|
||||||
|
// Must be after hooks !!!
|
||||||
|
if (!ChunkData) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-2 mb-3 w-full">
|
||||||
|
<button
|
||||||
|
onClick={() => setIsThinkingExpanded((prev) => !prev)}
|
||||||
|
className="inline-flex items-center gap-2 px-2 py-1 rounded-xl transition-colors border border-[#E6E6E6] dark:border-[#272626]"
|
||||||
|
>
|
||||||
|
{loading ? (
|
||||||
|
<>
|
||||||
|
<Loader className="w-4 h-4 animate-spin text-[#1990FF]" />
|
||||||
|
<span className="text-xs text-[#999999] italic">
|
||||||
|
{t(`assistant.message.steps.${ChunkData?.chunk_type}`)}
|
||||||
|
</span>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<BadgeCheck className="w-4 h-4 text-[#38C200]" />
|
||||||
|
<span className="text-xs text-[#999999]">
|
||||||
|
{t(`assistant.message.steps.${ChunkData?.chunk_type}`, {
|
||||||
|
count: Number(Data.length),
|
||||||
|
})}
|
||||||
|
</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{isThinkingExpanded ? (
|
||||||
|
<ChevronUp className="w-4 h-4" />
|
||||||
|
) : (
|
||||||
|
<ChevronDown className="w-4 h-4" />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
{isThinkingExpanded && (
|
||||||
|
<div className="pl-2 border-l-2 border-[e5e5e5]">
|
||||||
|
<div className="text-[#8b8b8b] dark:text-[#a6a6a6] space-y-2">
|
||||||
|
<div className="mb-4 space-y-3 text-xs">
|
||||||
|
{Data?.map((item) => (
|
||||||
|
<div key={item} className="flex flex-col gap-2">
|
||||||
|
<div className="text-xs text-[#999999] dark:text-[#808080]">
|
||||||
|
- {item}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
130
src/components/ChatMessage/FetchSource.tsx
Normal file
130
src/components/ChatMessage/FetchSource.tsx
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
import {
|
||||||
|
Search,
|
||||||
|
ChevronUp,
|
||||||
|
ChevronDown,
|
||||||
|
SquareArrowOutUpRight,
|
||||||
|
Globe,
|
||||||
|
} from "lucide-react";
|
||||||
|
import { useState, useEffect } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
|
import { OpenURLWithBrowser } from "@/utils/index";
|
||||||
|
import type { IChunkData } from "@/components/Assistant/types";
|
||||||
|
|
||||||
|
interface FetchSourceProps {
|
||||||
|
ChunkData?: IChunkData;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ISourceData {
|
||||||
|
category: string;
|
||||||
|
icon: string;
|
||||||
|
id: string;
|
||||||
|
size: number;
|
||||||
|
source: {
|
||||||
|
type: string;
|
||||||
|
name: string;
|
||||||
|
id: string;
|
||||||
|
};
|
||||||
|
summary: string;
|
||||||
|
thumbnail: string;
|
||||||
|
title: string;
|
||||||
|
updated: string | null;
|
||||||
|
url: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const FetchSource = ({ ChunkData }: FetchSourceProps) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [isSourceExpanded, setIsSourceExpanded] = useState(false);
|
||||||
|
|
||||||
|
const [total, setTotal] = useState(0);
|
||||||
|
const [data, setData] = useState<ISourceData[]>([]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!ChunkData?.message_chunk) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const match = ChunkData.message_chunk.match(
|
||||||
|
/\u003cPayload total=(\d+)\u003e/
|
||||||
|
);
|
||||||
|
if (match) {
|
||||||
|
setTotal(Number(match[1]));
|
||||||
|
}
|
||||||
|
|
||||||
|
const jsonMatch = ChunkData.message_chunk.match(/\[(.*)\]/s);
|
||||||
|
if (jsonMatch) {
|
||||||
|
const jsonData = JSON.parse(jsonMatch[0]);
|
||||||
|
setData(jsonData);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Failed to parse fetch source data:", e);
|
||||||
|
}
|
||||||
|
}, [ChunkData?.message_chunk]);
|
||||||
|
|
||||||
|
// Must be after hooks !!!
|
||||||
|
if (!ChunkData) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`mt-2 mb-2 w-[98%] ${
|
||||||
|
isSourceExpanded
|
||||||
|
? "rounded-lg overflow-hidden border border-[#E6E6E6] dark:border-[#272626]"
|
||||||
|
: ""
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
onClick={() => setIsSourceExpanded((prev) => !prev)}
|
||||||
|
className={`inline-flex justify-between items-center gap-2 px-2 py-1 rounded-xl transition-colors whitespace-nowrap ${
|
||||||
|
isSourceExpanded
|
||||||
|
? "w-full"
|
||||||
|
: "border border-[#E6E6E6] dark:border-[#272626]"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="flex-1 min-w-0 flex items-center gap-2">
|
||||||
|
<Search className="w-4 h-4 text-[#38C200] flex-shrink-0" />
|
||||||
|
<span className="text-xs text-[#999999]">
|
||||||
|
{t(`assistant.message.steps.${ChunkData?.chunk_type}`, {
|
||||||
|
count: Number(total),
|
||||||
|
})}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{isSourceExpanded ? (
|
||||||
|
<ChevronUp className="w-4 h-4 text-[#999999]" />
|
||||||
|
) : (
|
||||||
|
<ChevronDown className="w-4 h-4 text-[#999999]" />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{isSourceExpanded && (
|
||||||
|
<>
|
||||||
|
{/* {prefix && (
|
||||||
|
<div className="px-3 py-2 bg-[#F7F7F7] dark:bg-[#1E1E1E] text-[#666666] dark:text-[#A3A3A3] text-xs leading-relaxed border-b border-[#E6E6E6] dark:border-[#272626]">
|
||||||
|
{prefix}
|
||||||
|
</div>
|
||||||
|
)} */}
|
||||||
|
{data?.map((item, idx) => (
|
||||||
|
<div
|
||||||
|
key={idx}
|
||||||
|
onClick={() => item.url && OpenURLWithBrowser(item.url)}
|
||||||
|
className="group flex items-center p-2 hover:bg-[#F7F7F7] dark:hover:bg-[#2C2C2C] border-b border-[#E6E6E6] dark:border-[#272626] last:border-b-0 cursor-pointer transition-colors"
|
||||||
|
>
|
||||||
|
<div className="w-full flex items-center gap-2">
|
||||||
|
<div className="w-[75%] flex items-center gap-1">
|
||||||
|
<Globe className="w-3 h-3 flex-shrink-0" />
|
||||||
|
<div className="text-xs text-[#333333] dark:text-[#D8D8D8] truncate font-normal group-hover:text-[#0072FF] dark:group-hover:text-[#0072FF]">
|
||||||
|
{item.title || item.category}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="w-[25%] flex items-center justify-end gap-2">
|
||||||
|
<span className="text-xs text-[#999999] dark:text-[#999999] truncate">
|
||||||
|
{item.source?.name}
|
||||||
|
</span>
|
||||||
|
<SquareArrowOutUpRight className="w-3 h-3 text-[#999999] dark:text-[#999999] flex-shrink-0" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -9,7 +9,7 @@ import RehypeHighlight from "rehype-highlight";
|
|||||||
import mermaid from "mermaid";
|
import mermaid from "mermaid";
|
||||||
import { useDebouncedCallback } from "use-debounce";
|
import { useDebouncedCallback } from "use-debounce";
|
||||||
|
|
||||||
import { copyToClipboard, useWindowSize } from "../../utils";
|
import { copyToClipboard, useWindowSize } from "@/utils";
|
||||||
|
|
||||||
import "./markdown.css";
|
import "./markdown.css";
|
||||||
import "./highlight.css";
|
import "./highlight.css";
|
||||||
152
src/components/ChatMessage/MessageActions.tsx
Normal file
152
src/components/ChatMessage/MessageActions.tsx
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
import {
|
||||||
|
Check,
|
||||||
|
Copy,
|
||||||
|
ThumbsUp,
|
||||||
|
ThumbsDown,
|
||||||
|
Volume2,
|
||||||
|
RotateCcw,
|
||||||
|
} from "lucide-react";
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
interface MessageActionsProps {
|
||||||
|
content: string;
|
||||||
|
question?: string;
|
||||||
|
onResend?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const MessageActions = ({
|
||||||
|
content,
|
||||||
|
question,
|
||||||
|
onResend,
|
||||||
|
}: MessageActionsProps) => {
|
||||||
|
const [copied, setCopied] = useState(false);
|
||||||
|
const [liked, setLiked] = useState(false);
|
||||||
|
const [disliked, setDisliked] = useState(false);
|
||||||
|
const [isSpeaking, setIsSpeaking] = useState(false);
|
||||||
|
const [isResending, setIsResending] = useState(false);
|
||||||
|
|
||||||
|
const handleCopy = async () => {
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(content);
|
||||||
|
setCopied(true);
|
||||||
|
const timerID = setTimeout(() => {
|
||||||
|
setCopied(false);
|
||||||
|
clearTimeout(timerID);
|
||||||
|
}, 2000);
|
||||||
|
} catch (err) {
|
||||||
|
console.error("copy error:", err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleLike = () => {
|
||||||
|
setLiked(!liked);
|
||||||
|
setDisliked(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDislike = () => {
|
||||||
|
setDisliked(!disliked);
|
||||||
|
setLiked(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSpeak = () => {
|
||||||
|
if ("speechSynthesis" in window) {
|
||||||
|
if (isSpeaking) {
|
||||||
|
window.speechSynthesis.cancel();
|
||||||
|
setIsSpeaking(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const utterance = new SpeechSynthesisUtterance(content);
|
||||||
|
utterance.lang = "zh-CN";
|
||||||
|
|
||||||
|
utterance.onend = () => {
|
||||||
|
setIsSpeaking(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
setIsSpeaking(true);
|
||||||
|
window.speechSynthesis.speak(utterance);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleResend = () => {
|
||||||
|
if (onResend) {
|
||||||
|
setIsResending(true);
|
||||||
|
onResend();
|
||||||
|
const timerID = setTimeout(() => {
|
||||||
|
setIsResending(false);
|
||||||
|
clearTimeout(timerID);
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex items-center gap-1 mt-2">
|
||||||
|
<button
|
||||||
|
onClick={handleCopy}
|
||||||
|
className="p-1 hover:bg-black/5 dark:hover:bg-white/5 rounded-lg transition-colors"
|
||||||
|
>
|
||||||
|
{copied ? (
|
||||||
|
<Check className="w-4 h-4 text-[#38C200] dark:text-[#38C200]" />
|
||||||
|
) : (
|
||||||
|
<Copy className="w-4 h-4 text-[#666666] dark:text-[#A3A3A3]" />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={handleLike}
|
||||||
|
className={`p-1 hover:bg-black/5 dark:hover:bg-white/5 rounded-lg transition-colors ${
|
||||||
|
liked ? "animate-shake" : ""
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<ThumbsUp
|
||||||
|
className={`w-4 h-4 ${
|
||||||
|
liked
|
||||||
|
? "text-[#1990FF] dark:text-[#1990FF]"
|
||||||
|
: "text-[#666666] dark:text-[#A3A3A3]"
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={handleDislike}
|
||||||
|
className={`p-1 hover:bg-black/5 dark:hover:bg-white/5 rounded-lg transition-colors ${
|
||||||
|
disliked ? "animate-shake" : ""
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<ThumbsDown
|
||||||
|
className={`w-4 h-4 ${
|
||||||
|
disliked
|
||||||
|
? "text-[#1990FF] dark:text-[#1990FF]"
|
||||||
|
: "text-[#666666] dark:text-[#A3A3A3]"
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={handleSpeak}
|
||||||
|
className="p-1 hover:bg-black/5 dark:hover:bg-white/5 rounded-lg transition-colors"
|
||||||
|
>
|
||||||
|
<Volume2
|
||||||
|
className={`w-4 h-4 ${
|
||||||
|
isSpeaking
|
||||||
|
? "text-[#1990FF] dark:text-[#1990FF]"
|
||||||
|
: "text-[#666666] dark:text-[#A3A3A3]"
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
{question && (
|
||||||
|
<button
|
||||||
|
onClick={handleResend}
|
||||||
|
className={`p-1 hover:bg-black/5 dark:hover:bg-white/5 rounded-lg transition-colors ${
|
||||||
|
isResending ? "animate-spin" : ""
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<RotateCcw
|
||||||
|
className={`w-4 h-4 ${
|
||||||
|
isResending
|
||||||
|
? "text-[#1990FF] dark:text-[#1990FF]"
|
||||||
|
: "text-[#666666] dark:text-[#A3A3A3]"
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
139
src/components/ChatMessage/PickSource.tsx
Normal file
139
src/components/ChatMessage/PickSource.tsx
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
import { ChevronDown, ChevronUp, Loader, BadgeCheck } from "lucide-react";
|
||||||
|
import { useState, useEffect } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
|
import type { IChunkData } from "@/components/Assistant/types";
|
||||||
|
|
||||||
|
interface PickSourceProps {
|
||||||
|
ChunkData?: IChunkData;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IData {
|
||||||
|
explain: string;
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PickSource = ({ ChunkData }: PickSourceProps) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const [isThinkingExpanded, setIsThinkingExpanded] = useState(true);
|
||||||
|
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [prevContent, setPrevContent] = useState("");
|
||||||
|
|
||||||
|
const [Data, setData] = useState<IData[]>([]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!ChunkData?.message_chunk) return;
|
||||||
|
|
||||||
|
const timerID = setTimeout(() => {
|
||||||
|
if (ChunkData.message_chunk === prevContent) {
|
||||||
|
const cleanContent = ChunkData.message_chunk?.replace(/^"|"$/g, "");
|
||||||
|
const match = cleanContent.match(/<JSON>([\s\S]*?)<\/JSON>/);
|
||||||
|
if (match && match[1]) {
|
||||||
|
const data = JSON.parse(match[1].trim());
|
||||||
|
if (
|
||||||
|
Array.isArray(data) &&
|
||||||
|
data.every((item) => item.id && item.title && item.explain)
|
||||||
|
) {
|
||||||
|
setData(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setLoading(false);
|
||||||
|
clearTimeout(timerID);
|
||||||
|
}
|
||||||
|
}, 500);
|
||||||
|
setPrevContent(ChunkData.message_chunk);
|
||||||
|
return () => {
|
||||||
|
timerID && clearTimeout(timerID);
|
||||||
|
};
|
||||||
|
}, [ChunkData?.message_chunk, prevContent]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!ChunkData?.message_chunk) return;
|
||||||
|
try {
|
||||||
|
const cleanContent = ChunkData.message_chunk.replace(/^"|"$/g, "");
|
||||||
|
const allMatches = cleanContent.match(/<JSON>([\s\S]*?)<\/JSON>/g);
|
||||||
|
if (allMatches) {
|
||||||
|
for (let i = allMatches.length - 1; i >= 0; i--) {
|
||||||
|
try {
|
||||||
|
const jsonString = allMatches[i].replace(/<JSON>|<\/JSON>/g, "");
|
||||||
|
const data = JSON.parse(jsonString);
|
||||||
|
if (
|
||||||
|
Array.isArray(data) &&
|
||||||
|
data.every((item) => item.id && item.title && item.explain)
|
||||||
|
) {
|
||||||
|
setData(data);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Failed to parse pick source data:", e);
|
||||||
|
}
|
||||||
|
}, [ChunkData?.message_chunk]);
|
||||||
|
|
||||||
|
// Must be after hooks !!!
|
||||||
|
if (!ChunkData) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-2 mb-3 w-full">
|
||||||
|
<button
|
||||||
|
onClick={() => setIsThinkingExpanded((prev) => !prev)}
|
||||||
|
className="inline-flex items-center gap-2 px-2 py-1 rounded-xl transition-colors border border-[#E6E6E6] dark:border-[#272626]"
|
||||||
|
>
|
||||||
|
{loading ? (
|
||||||
|
<>
|
||||||
|
<Loader className="w-4 h-4 animate-spin text-[#1990FF]" />
|
||||||
|
<span className="text-xs text-[#999999] italic">
|
||||||
|
{t(`assistant.message.steps.${ChunkData?.chunk_type}`, {
|
||||||
|
count: Data?.length,
|
||||||
|
})}
|
||||||
|
</span>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<BadgeCheck className="w-4 h-4 text-[#38C200]" />
|
||||||
|
<span className="text-xs text-[#999999]">
|
||||||
|
{t(`assistant.message.steps.${ChunkData?.chunk_type}`, {
|
||||||
|
count: Data?.length,
|
||||||
|
})}
|
||||||
|
</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{isThinkingExpanded ? (
|
||||||
|
<ChevronUp className="w-4 h-4" />
|
||||||
|
) : (
|
||||||
|
<ChevronDown className="w-4 h-4" />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
{isThinkingExpanded && (
|
||||||
|
<div className="pl-2 border-l-2 border-[e5e5e5]">
|
||||||
|
<div className="text-[#8b8b8b] dark:text-[#a6a6a6] space-y-2">
|
||||||
|
<div className="mb-4 space-y-3 text-xs">
|
||||||
|
{Data?.map((item) => (
|
||||||
|
<div
|
||||||
|
key={item.id}
|
||||||
|
className="p-3 rounded-lg border border-[#E6E6E6] dark:border-[#272626] bg-white dark:bg-[#1E1E1E] hover:bg-gray-50 dark:hover:bg-[#2C2C2C] transition-colors"
|
||||||
|
>
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<div className="text-sm font-medium text-[#333333] dark:text-[#D8D8D8]">
|
||||||
|
{item.title}
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-[#666666] dark:text-[#A3A3A3] line-clamp-2">
|
||||||
|
{item.explain}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
148
src/components/ChatMessage/QueryIntent.tsx
Normal file
148
src/components/ChatMessage/QueryIntent.tsx
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
import { ChevronDown, ChevronUp, Loader, BadgeCheck } from "lucide-react";
|
||||||
|
import { useState, useEffect } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
|
import type { IChunkData } from "@/components/Assistant/types";
|
||||||
|
|
||||||
|
interface QueryIntentProps {
|
||||||
|
ChunkData?: IChunkData;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IQueryData {
|
||||||
|
category: string;
|
||||||
|
intent: string;
|
||||||
|
query: string[];
|
||||||
|
keyword: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const QueryIntent = ({ ChunkData }: QueryIntentProps) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const [isThinkingExpanded, setIsThinkingExpanded] = useState(true);
|
||||||
|
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [prevContent, setPrevContent] = useState("");
|
||||||
|
|
||||||
|
const [Data, setData] = useState<IQueryData | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!ChunkData?.message_chunk) return;
|
||||||
|
const timerID = setTimeout(() => {
|
||||||
|
if (ChunkData.message_chunk === prevContent) {
|
||||||
|
const cleanContent = ChunkData.message_chunk.replace(/^"|"$/g, "");
|
||||||
|
const allMatches = cleanContent.match(/<JSON>([\s\S]*?)<\/JSON>/g);
|
||||||
|
if (allMatches) {
|
||||||
|
const lastMatch = allMatches[allMatches.length - 1];
|
||||||
|
const jsonString = lastMatch.replace(/<JSON>|<\/JSON>/g, "");
|
||||||
|
const data = JSON.parse(jsonString);
|
||||||
|
setData(data);
|
||||||
|
}
|
||||||
|
setLoading(false);
|
||||||
|
clearTimeout(timerID);
|
||||||
|
}
|
||||||
|
}, 500);
|
||||||
|
setPrevContent(ChunkData.message_chunk);
|
||||||
|
return () => {
|
||||||
|
timerID && clearTimeout(timerID);
|
||||||
|
};
|
||||||
|
}, [ChunkData?.message_chunk, prevContent, loading]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!ChunkData?.message_chunk) return;
|
||||||
|
try {
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Failed to parse query data:", e);
|
||||||
|
}
|
||||||
|
}, [ChunkData?.message_chunk]);
|
||||||
|
|
||||||
|
// Must be after hooks !!!
|
||||||
|
if (!ChunkData) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-2 mb-3 w-full">
|
||||||
|
<button
|
||||||
|
onClick={() => setIsThinkingExpanded((prev) => !prev)}
|
||||||
|
className="inline-flex items-center gap-2 px-2 py-1 rounded-xl transition-colors border border-[#E6E6E6] dark:border-[#272626]"
|
||||||
|
>
|
||||||
|
{loading ? (
|
||||||
|
<>
|
||||||
|
<Loader className="w-4 h-4 animate-spin text-[#1990FF]" />
|
||||||
|
<span className="text-xs text-[#999999] italic">
|
||||||
|
{t(`assistant.message.steps.${ChunkData?.chunk_type}`)}
|
||||||
|
</span>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<BadgeCheck className="w-4 h-4 text-[#38C200]" />
|
||||||
|
<span className="text-xs text-[#999999]">
|
||||||
|
{t(`assistant.message.steps.${ChunkData?.chunk_type}`)}
|
||||||
|
</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{isThinkingExpanded ? (
|
||||||
|
<ChevronUp className="w-4 h-4" />
|
||||||
|
) : (
|
||||||
|
<ChevronDown className="w-4 h-4" />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
{isThinkingExpanded && (
|
||||||
|
<div className="pl-2 border-l-2 border-[e5e5e5]">
|
||||||
|
<div className="text-[#8b8b8b] dark:text-[#a6a6a6] space-y-2">
|
||||||
|
<div className="mb-4 space-y-2 text-xs">
|
||||||
|
{Data?.keyword ? (
|
||||||
|
<div className="flex gap-1">
|
||||||
|
<span className="text-[#999999]">
|
||||||
|
{t("assistant.message.steps.keywords")}:
|
||||||
|
</span>
|
||||||
|
<div className="flex flex-wrap gap-1">
|
||||||
|
{Data?.keyword?.map((keyword, index) => (
|
||||||
|
<span
|
||||||
|
key={index}
|
||||||
|
className="text-[#333333] dark:text-[#D8D8D8]"
|
||||||
|
>
|
||||||
|
{keyword}
|
||||||
|
{index < 2 && "、"}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
{Data?.category ? (
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<span className="text-[#999999]">
|
||||||
|
{t("assistant.message.steps.questionType")}:
|
||||||
|
</span>
|
||||||
|
<span className="text-[#333333] dark:text-[#D8D8D8]">
|
||||||
|
{Data?.category}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
{Data?.intent ? (
|
||||||
|
<div className="flex items-start gap-1">
|
||||||
|
<span className="text-[#999999]">
|
||||||
|
{t("assistant.message.steps.userIntent")}:
|
||||||
|
</span>
|
||||||
|
<div className="flex-1 text-[#333333] dark:text-[#D8D8D8]">
|
||||||
|
{Data?.intent}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
{Data?.query ? (
|
||||||
|
<div className="flex items-start gap-1">
|
||||||
|
<span className="text-[#999999]">
|
||||||
|
{t("assistant.message.steps.relatedQuestions")}:
|
||||||
|
</span>
|
||||||
|
<div className="flex-1 flex flex-col text-[#333333] dark:text-[#D8D8D8]">
|
||||||
|
{Data?.query?.map((question) => (
|
||||||
|
<span key={question}>- {question}</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
78
src/components/ChatMessage/Think.tsx
Normal file
78
src/components/ChatMessage/Think.tsx
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
import { Brain, ChevronDown, ChevronUp } from "lucide-react";
|
||||||
|
import { useState, useEffect } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
|
import type { IChunkData } from "@/components/Assistant/types";
|
||||||
|
|
||||||
|
interface ThinkProps {
|
||||||
|
ChunkData?: IChunkData;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Think = ({ ChunkData }: ThinkProps) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [isThinkingExpanded, setIsThinkingExpanded] = useState(true);
|
||||||
|
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [Data, setData] = useState("");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!ChunkData?.message_chunk) return;
|
||||||
|
const timerID = setTimeout(() => {
|
||||||
|
if (ChunkData.message_chunk === Data) {
|
||||||
|
setLoading(false);
|
||||||
|
clearTimeout(timerID);
|
||||||
|
}
|
||||||
|
}, 500);
|
||||||
|
setData(ChunkData?.message_chunk);
|
||||||
|
return () => {
|
||||||
|
timerID && clearTimeout(timerID);
|
||||||
|
};
|
||||||
|
}, [ChunkData?.message_chunk, Data, loading]);
|
||||||
|
|
||||||
|
// Must be after hooks !!!
|
||||||
|
if (!ChunkData) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-2 mb-3 w-full">
|
||||||
|
<button
|
||||||
|
onClick={() => setIsThinkingExpanded((prev) => !prev)}
|
||||||
|
className="inline-flex items-center gap-2 px-2 py-1 rounded-xl transition-colors border border-[#E6E6E6] dark:border-[#272626]"
|
||||||
|
>
|
||||||
|
{loading ? (
|
||||||
|
<>
|
||||||
|
<Brain className="w-4 h-4 animate-pulse text-[#999999]" />
|
||||||
|
<span className="text-xs text-[#999999] italic">
|
||||||
|
{t(`assistant.message.steps.${ChunkData?.chunk_type}`)}
|
||||||
|
</span>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Brain className="w-4 h-4 text-[#38C200]" />
|
||||||
|
<span className="text-xs text-[#999999]">
|
||||||
|
{t("assistant.message.steps.thoughtTime")}
|
||||||
|
</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{isThinkingExpanded ? (
|
||||||
|
<ChevronUp className="w-4 h-4" />
|
||||||
|
) : (
|
||||||
|
<ChevronDown className="w-4 h-4" />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
{isThinkingExpanded && (
|
||||||
|
<div className="pl-2 border-l-2 border-[e5e5e5]">
|
||||||
|
<div className="text-[#8b8b8b] dark:text-[#a6a6a6] space-y-2">
|
||||||
|
{Data?.split("\n").map(
|
||||||
|
(paragraph, idx) =>
|
||||||
|
paragraph.trim() && (
|
||||||
|
<p key={idx} className="text-sm">
|
||||||
|
{paragraph}
|
||||||
|
</p>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
114
src/components/ChatMessage/index.tsx
Normal file
114
src/components/ChatMessage/index.tsx
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
import { memo } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
|
import logoImg from "@/assets/icon.svg";
|
||||||
|
import type { Message, IChunkData } from "@/components/Assistant/types";
|
||||||
|
import { QueryIntent } from "./QueryIntent";
|
||||||
|
import { FetchSource } from "./FetchSource";
|
||||||
|
import { PickSource } from "./PickSource";
|
||||||
|
import { DeepRead } from "./DeepRead";
|
||||||
|
import { Think } from "./Think";
|
||||||
|
import { MessageActions } from "./MessageActions";
|
||||||
|
import Markdown from "./Markdown";
|
||||||
|
|
||||||
|
interface ChatMessageProps {
|
||||||
|
message: Message;
|
||||||
|
isTyping?: boolean;
|
||||||
|
query_intent?: IChunkData;
|
||||||
|
fetch_source?: IChunkData;
|
||||||
|
pick_source?: IChunkData;
|
||||||
|
deep_read?: IChunkData;
|
||||||
|
think?: IChunkData;
|
||||||
|
response?: IChunkData;
|
||||||
|
onResend?: (value: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ChatMessage = memo(function ChatMessage({
|
||||||
|
message,
|
||||||
|
isTyping,
|
||||||
|
query_intent,
|
||||||
|
fetch_source,
|
||||||
|
pick_source,
|
||||||
|
deep_read,
|
||||||
|
think,
|
||||||
|
response,
|
||||||
|
onResend,
|
||||||
|
}: ChatMessageProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const isAssistant = message?._source?.type === "assistant";
|
||||||
|
const messageContent = message?._source?.message || "";
|
||||||
|
const question = message?._source?.question || "";
|
||||||
|
|
||||||
|
console.log(11111, messageContent);
|
||||||
|
|
||||||
|
const renderContent = () => {
|
||||||
|
if (!isAssistant) {
|
||||||
|
return (
|
||||||
|
<div className="px-3 py-2 bg-white dark:bg-[#202126] rounded-xl border border-black/12 dark:border-black/15 font-normal text-sm text-[#333333] dark:text-[#D8D8D8]">
|
||||||
|
{messageContent}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<QueryIntent ChunkData={query_intent} />
|
||||||
|
<FetchSource ChunkData={fetch_source} />
|
||||||
|
<PickSource ChunkData={pick_source} />
|
||||||
|
<DeepRead ChunkData={deep_read} />
|
||||||
|
<Think ChunkData={think} />
|
||||||
|
<Markdown
|
||||||
|
content={messageContent || response?.message_chunk || ""}
|
||||||
|
loading={isTyping}
|
||||||
|
onDoubleClickCapture={() => {}}
|
||||||
|
/>
|
||||||
|
{isTyping && (
|
||||||
|
<div className="inline-block w-1.5 h-5 ml-0.5 -mb-0.5 bg-[#666666] dark:bg-[#A3A3A3] rounded-sm animate-typing" />
|
||||||
|
)}
|
||||||
|
{isTyping === false &&
|
||||||
|
(messageContent || response?.message_chunk) && (
|
||||||
|
<MessageActions
|
||||||
|
content={messageContent || response?.message_chunk || ""}
|
||||||
|
question={question}
|
||||||
|
onResend={() => {
|
||||||
|
onResend && onResend(question);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`py-8 flex ${isAssistant ? "justify-start" : "justify-end"}`}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={`px-4 flex gap-4 ${
|
||||||
|
isAssistant ? "w-full" : "flex-row-reverse"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={`space-y-2 ${isAssistant ? "text-left" : "text-right"}`}
|
||||||
|
>
|
||||||
|
<p className="flex items-center gap-4 font-semibold text-sm text-[#333] dark:text-[#d8d8d8]">
|
||||||
|
{isAssistant ? (
|
||||||
|
<img
|
||||||
|
src={logoImg}
|
||||||
|
className="w-6 h-6"
|
||||||
|
alt={t("assistant.message.logo")}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
{isAssistant ? t("assistant.message.aiName") : ""}
|
||||||
|
</p>
|
||||||
|
<div className="prose dark:prose-invert prose-sm max-w-none">
|
||||||
|
<div className="text-[#333] dark:text-[#d8d8d8] leading-relaxed">
|
||||||
|
{renderContent()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
@@ -93,7 +93,7 @@
|
|||||||
.markdown-body {
|
.markdown-body {
|
||||||
-ms-text-size-adjust: 100%;
|
-ms-text-size-adjust: 100%;
|
||||||
-webkit-text-size-adjust: 100%;
|
-webkit-text-size-adjust: 100%;
|
||||||
margin: 0;
|
margin: 16px 0 0;
|
||||||
color: var(--color-fg-default);
|
color: var(--color-fg-default);
|
||||||
background-color: var(--color-canvas-default);
|
background-color: var(--color-canvas-default);
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useOSKeyPress } from "@/hooks/useOSKeyPress";
|
import { useOSKeyPress } from "@/hooks/useOSKeyPress";
|
||||||
import { useSearchStore } from "@/stores/searchStore";
|
import { useSearchStore } from "@/stores/searchStore";
|
||||||
import { copyToClipboard, OpenURLWithBrowser } from "@/utils";
|
import { copyToClipboard, OpenURLWithBrowser } from "@/utils";
|
||||||
import { isMac, isWin } from "@/utils/platform";
|
import { isMac } from "@/utils/platform";
|
||||||
import {
|
import {
|
||||||
useClickAway,
|
useClickAway,
|
||||||
useCreation,
|
useCreation,
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import { useEffect, useRef, useState, useCallback } from "react";
|
import { useEffect, useRef, useState, useCallback } from "react";
|
||||||
import { CircleAlert, Bolt, X, ArrowBigRight } from "lucide-react";
|
import { CircleAlert, Bolt, X, ArrowBigRight } from "lucide-react";
|
||||||
|
import { isNil } from "lodash-es";
|
||||||
|
import { useUnmount } from "ahooks";
|
||||||
|
|
||||||
import { useSearchStore } from "@/stores/searchStore";
|
import { useSearchStore } from "@/stores/searchStore";
|
||||||
import ThemedIcon from "@/components/Common/Icons/ThemedIcon";
|
import ThemedIcon from "@/components/Common/Icons/ThemedIcon";
|
||||||
@@ -8,8 +10,6 @@ import TypeIcon from "@/components/Common/Icons/TypeIcon";
|
|||||||
import SearchListItem from "./SearchListItem";
|
import SearchListItem from "./SearchListItem";
|
||||||
import { metaOrCtrlKey, isMetaOrCtrlKey } from "@/utils/keyboardUtils";
|
import { metaOrCtrlKey, isMetaOrCtrlKey } from "@/utils/keyboardUtils";
|
||||||
import { OpenURLWithBrowser } from "@/utils/index";
|
import { OpenURLWithBrowser } from "@/utils/index";
|
||||||
import { isNil, isPlainObject } from "lodash-es";
|
|
||||||
import { useUnmount } from "ahooks";
|
|
||||||
|
|
||||||
type ISearchData = Record<string, any[]>;
|
type ISearchData = Record<string, any[]>;
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,4 @@
|
|||||||
import {
|
import { ArrowDown01, Command, CornerDownLeft } from "lucide-react";
|
||||||
ArrowDown01,
|
|
||||||
Command,
|
|
||||||
CornerDownLeft,
|
|
||||||
Pin,
|
|
||||||
PinOff,
|
|
||||||
} from "lucide-react";
|
|
||||||
import { emit } from "@tauri-apps/api/event";
|
import { emit } from "@tauri-apps/api/event";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { getCurrentWindow } from "@tauri-apps/api/window";
|
import { getCurrentWindow } from "@tauri-apps/api/window";
|
||||||
@@ -14,6 +8,8 @@ import { useSearchStore } from "@/stores/searchStore";
|
|||||||
import TypeIcon from "@/components/Common/Icons/TypeIcon";
|
import TypeIcon from "@/components/Common/Icons/TypeIcon";
|
||||||
import { useAppStore } from "@/stores/appStore";
|
import { useAppStore } from "@/stores/appStore";
|
||||||
import { isMac } from "@/utils/platform";
|
import { isMac } from "@/utils/platform";
|
||||||
|
import PinOffIcon from "@/icons/PinOff";
|
||||||
|
import PinIcon from "@/icons/Pin";
|
||||||
|
|
||||||
interface FooterProps {
|
interface FooterProps {
|
||||||
isChat: boolean;
|
isChat: boolean;
|
||||||
@@ -65,17 +61,12 @@ export default function Footer({}: FooterProps) {
|
|||||||
version: process.env.VERSION || "v1.0.0",
|
version: process.env.VERSION || "v1.0.0",
|
||||||
})}
|
})}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
onClick={togglePin}
|
onClick={togglePin}
|
||||||
className={`rounded-lg hover:bg-gray-100 dark:hover:bg-gray-800 ${
|
className={`${isPinned ? "text-blue-500" : ""}`}
|
||||||
isPinned ? "text-blue-500" : ""
|
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
{isPinned ? (
|
{isPinned ? <PinIcon /> : <PinOffIcon />}
|
||||||
<Pin className="h-3 w-3" />
|
|
||||||
) : (
|
|
||||||
<PinOff className="h-3 w-3" />
|
|
||||||
)}
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -262,8 +262,10 @@ export default function ChatInput({
|
|||||||
const [countdown, setCountdown] = useState(5);
|
const [countdown, setCountdown] = useState(5);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (!isChatMode) return;
|
||||||
if (connected) return;
|
if (connected) return;
|
||||||
if (countdown <= 0) {
|
if (countdown <= 0) {
|
||||||
|
console.log("ReconnectClick", 111111);
|
||||||
ReconnectClick();
|
ReconnectClick();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -273,7 +275,7 @@ export default function ChatInput({
|
|||||||
}, 1000);
|
}, 1000);
|
||||||
|
|
||||||
return () => clearTimeout(timer);
|
return () => clearTimeout(timer);
|
||||||
}, [countdown, connected]);
|
}, [countdown, connected, isChatMode]);
|
||||||
|
|
||||||
const ReconnectClick = () => {
|
const ReconnectClick = () => {
|
||||||
setCountdown(5);
|
setCountdown(5);
|
||||||
|
|||||||
@@ -131,14 +131,20 @@
|
|||||||
"message": {
|
"message": {
|
||||||
"logo": "Coco AI Logo",
|
"logo": "Coco AI Logo",
|
||||||
"aiName": "Coco AI",
|
"aiName": "Coco AI",
|
||||||
"thinking": "AI is thinking...",
|
|
||||||
"thoughtTime": "Thought for a few seconds",
|
|
||||||
"thinkingButton": "View thinking process",
|
"thinkingButton": "View thinking process",
|
||||||
"steps": {
|
"steps": {
|
||||||
"understand_the_query": "Understand the query",
|
"query_intent": "Understand the query",
|
||||||
"retrieve_documents": "Retrieve documents",
|
"source_zero": "Searching for relevant documents",
|
||||||
"intelligent_pre_selection": "Intelligent pre-selection",
|
"fetch_source": "Retrieve {{count}} documents",
|
||||||
"deep_reading": "Deep reading"
|
"pick_source": "Intelligent pick {{count}} results",
|
||||||
|
"deep_read": "Deep reading",
|
||||||
|
"think": "AI is thinking...",
|
||||||
|
"thoughtTime": "Thought for a few seconds",
|
||||||
|
"keywords": "Keywords",
|
||||||
|
"questionType": "Query Type",
|
||||||
|
"userIntent": "User Intent",
|
||||||
|
"relatedQuestions": "Query",
|
||||||
|
"informationSeeking": "Information Seeking"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sidebar": {
|
"sidebar": {
|
||||||
@@ -148,7 +154,8 @@
|
|||||||
"untitledChat": "Untitled Chat"
|
"untitledChat": "Untitled Chat"
|
||||||
},
|
},
|
||||||
"source": {
|
"source": {
|
||||||
"foundResults": "Found {{count}} results"
|
"fetch_source": "Found {{count}} results",
|
||||||
|
"pick_source": "{{count}} results"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"cloud": {
|
"cloud": {
|
||||||
|
|||||||
@@ -131,14 +131,20 @@
|
|||||||
"message": {
|
"message": {
|
||||||
"logo": "Coco AI 图标",
|
"logo": "Coco AI 图标",
|
||||||
"aiName": "Coco AI",
|
"aiName": "Coco AI",
|
||||||
"thinking": "AI 正在思考...",
|
|
||||||
"thoughtTime": "思考了数秒",
|
|
||||||
"thinkingButton": "查看思考过程",
|
"thinkingButton": "查看思考过程",
|
||||||
"steps": {
|
"steps": {
|
||||||
"understand_the_query": "理解查询",
|
"query_intent": "理解查询",
|
||||||
"retrieve_documents": "检索文档",
|
"source_zero": "正在搜索相关文档",
|
||||||
"intelligent_pre_selection": "智能预选",
|
"fetch_source": "检索 {{count}} 份文档",
|
||||||
"deep_reading": "深度阅读"
|
"pick_source": "智能预选 {{count}} 个结果",
|
||||||
|
"deep_read": "深度阅读,",
|
||||||
|
"think": "AI 正在思考...",
|
||||||
|
"thoughtTime": "思考了数秒",
|
||||||
|
"keywords": "关键词",
|
||||||
|
"questionType": "查询类型",
|
||||||
|
"userIntent": "用户意图",
|
||||||
|
"relatedQuestions": "查询",
|
||||||
|
"informationSeeking": "信息查询"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sidebar": {
|
"sidebar": {
|
||||||
@@ -148,7 +154,8 @@
|
|||||||
"untitledChat": "未命名对话"
|
"untitledChat": "未命名对话"
|
||||||
},
|
},
|
||||||
"source": {
|
"source": {
|
||||||
"foundResults": "找到 {{count}} 个结果"
|
"fetch_source": "找到 {{count}} 个结果",
|
||||||
|
"pick_source": "{{count}} 个结果"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"cloud": {
|
"cloud": {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React from "react";
|
// import React from "react";
|
||||||
import ReactDOM from "react-dom/client";
|
import ReactDOM from "react-dom/client";
|
||||||
import { RouterProvider } from "react-router-dom";
|
import { RouterProvider } from "react-router-dom";
|
||||||
|
|
||||||
@@ -9,9 +9,9 @@ import "./i18n";
|
|||||||
import "./main.css";
|
import "./main.css";
|
||||||
|
|
||||||
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
|
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
|
||||||
<React.StrictMode>
|
//<React.StrictMode>
|
||||||
<ThemeProvider>
|
<ThemeProvider>
|
||||||
<RouterProvider router={router} />
|
<RouterProvider router={router} />
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
</React.StrictMode>
|
// </React.StrictMode>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -90,49 +90,4 @@ export const formatter = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const formatThinkingMessage = (message: string) => {
|
|
||||||
const segments: Array<{
|
|
||||||
text: string;
|
|
||||||
isThinking: boolean;
|
|
||||||
thinkContent: string;
|
|
||||||
isSource?: boolean;
|
|
||||||
}> = [];
|
|
||||||
|
|
||||||
if (!message) return segments;
|
|
||||||
|
|
||||||
const sourceRegex = /(<Source.*?>.*?<\/Source>)/gs;
|
|
||||||
const parts = message.split(sourceRegex);
|
|
||||||
|
|
||||||
parts.forEach(part => {
|
|
||||||
if (part.startsWith('<Source')) {
|
|
||||||
segments.push({
|
|
||||||
text: part,
|
|
||||||
isThinking: false,
|
|
||||||
thinkContent: '',
|
|
||||||
isSource: true
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
const thinkParts = part.split(/(<think>.*?<\/think>)/s);
|
|
||||||
|
|
||||||
thinkParts.forEach(thinkPart => {
|
|
||||||
if (thinkPart.startsWith('<think>')) {
|
|
||||||
const content = thinkPart.replace(/<\/?think>/g, '');
|
|
||||||
segments.push({
|
|
||||||
text: '',
|
|
||||||
isThinking: true,
|
|
||||||
thinkContent: content.trim()
|
|
||||||
});
|
|
||||||
} else if (thinkPart.trim()) {
|
|
||||||
segments.push({
|
|
||||||
text: thinkPart.trim(),
|
|
||||||
isThinking: false,
|
|
||||||
thinkContent: ''
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return segments;
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|||||||
@@ -23,12 +23,24 @@ 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',
|
||||||
|
'shake': 'shake 0.5s ease-in-out',
|
||||||
},
|
},
|
||||||
keyframes: {
|
keyframes: {
|
||||||
"fade-in": {
|
"fade-in": {
|
||||||
"0%": { opacity: "0" },
|
"0%": { opacity: "0" },
|
||||||
"100%": { opacity: "1" },
|
"100%": { opacity: "1" },
|
||||||
},
|
},
|
||||||
|
typing: {
|
||||||
|
'0%': { opacity: '0.3' },
|
||||||
|
'50%': { opacity: '1' },
|
||||||
|
'100%': { opacity: '0.3' },
|
||||||
|
},
|
||||||
|
shake: {
|
||||||
|
'0%, 100%': { transform: 'rotate(0deg)' },
|
||||||
|
'25%': { 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