chore: add deep think & stop icon & chat page (#141)

* fix: stop icon & useCallback

* fix: add deep think & stop icon

* chore: chat app should not always on top

* refactor: ui styles

* build: build error

* refactor: ui styles

---------

Co-authored-by: medcl <m@medcl.net>
This commit is contained in:
BiggerRain
2025-02-17 16:37:33 +08:00
committed by GitHub
parent d3bc5452e0
commit 6291df9f13
11 changed files with 509 additions and 408 deletions

View File

@@ -1,302 +1,318 @@
import {
useState,
useRef,
useEffect,
forwardRef,
useImperativeHandle,
} from "react";
import { MessageSquarePlus, PanelLeft } from "lucide-react";
import { isTauri } from "@tauri-apps/api/core";
import {forwardRef, memo, useCallback, useEffect, useImperativeHandle, useRef, useState,} from "react";
import {MessageSquarePlus, PanelLeft} from "lucide-react";
import {isTauri} from "@tauri-apps/api/core";
import {ChatMessage} from "./ChatMessage";
import type {Chat, Message} from "./types";
import {tauriFetch} from "../../api/tauriFetchClient";
import {useWebSocket} from "../../hooks/useWebSocket";
import {useChatStore} from "../../stores/chatStore";
import {useWindows} from "../../hooks/useWindows";
import {clientEnv} from "@/utils/env";
import { ChatMessage } from "./ChatMessage";
import type { Chat, Message } from "./types";
import { tauriFetch } from "../../api/tauriFetchClient";
import { useWebSocket } from "../../hooks/useWebSocket";
import { useChatStore } from "../../stores/chatStore";
import { useWindows } from "../../hooks/useWindows";
import { clientEnv } from "@/utils/env";
// import { useAppStore } from '@/stores/appStore';
interface ChatAIProps {
inputValue: string;
isTransitioned: boolean;
changeInput: (val: string) => void;
isSearchActive?: boolean;
isTransitioned: boolean;
changeInput: (val: string) => void;
isSearchActive?: boolean;
isDeepThinkActive?: boolean;
}
export interface ChatAIRef {
init: () => void;
cancelChat: () => void;
connected: boolean;
reconnect: () => void;
init: (value: string) => void;
cancelChat: () => void;
connected: boolean;
reconnect: () => void;
}
const ChatAI = forwardRef<ChatAIRef, ChatAIProps>(
({ inputValue, isTransitioned, changeInput, isSearchActive }, ref) => {
useImperativeHandle(ref, () => ({
init: init,
cancelChat: cancelChat,
connected: connected,
reconnect: reconnect
}));
const ChatAI = memo(forwardRef<ChatAIRef, ChatAIProps>(
({isTransitioned, changeInput, isSearchActive, isDeepThinkActive}, ref) => {
useImperativeHandle(ref, () => ({
init: init,
cancelChat: cancelChat,
connected: connected,
reconnect: reconnect
}));
// const appStore = useAppStore();
// const appStore = useAppStore();
const { createWin } = useWindows();
const {createWin} = useWindows();
const { curChatEnd, setCurChatEnd, setConnected } = useChatStore();
const {curChatEnd, setCurChatEnd, setConnected} = useChatStore();
const [activeChat, setActiveChat] = useState<Chat>();
const [isTyping, setIsTyping] = useState(false);
const messagesEndRef = useRef<HTMLDivElement>(null);
const [activeChat, setActiveChat] = useState<Chat>();
const [isTyping, setIsTyping] = useState(false);
const messagesEndRef = useRef<HTMLDivElement>(null);
const [websocketId, setWebsocketId] = useState("");
const [curMessage, setCurMessage] = useState("");
const [curId, setCurId] = useState("");
const [websocketId, setWebsocketId] = useState("");
const [curMessage, setCurMessage] = useState("");
const [curId, setCurId] = useState("");
const curChatEndRef = useRef(curChatEnd);
curChatEndRef.current = curChatEnd;
const curChatEndRef = useRef(curChatEnd);
curChatEndRef.current = curChatEnd;
const curIdRef = useRef(curId);
curIdRef.current = curId;
const curIdRef = useRef(curId);
curIdRef.current = curId;
// console.log("chat useWebSocket", clientEnv.COCO_WEBSOCKET_URL)
const { messages, setMessages, connected, reconnect } = useWebSocket(
clientEnv.COCO_WEBSOCKET_URL,
(msg) => {
// console.log("msg", msg);
const handleMessageChunk = useCallback((chunk: string) => {
setCurMessage(prev => prev + chunk);
}, []);
if (msg.includes("websocket-session-id")) {
const array = msg.split(" ");
setWebsocketId(array[2]);
}
// console.log("chat useWebSocket", clientEnv.COCO_WEBSOCKET_URL)
const {messages, setMessages, connected, reconnect} = useWebSocket(
clientEnv.COCO_WEBSOCKET_URL,
(msg) => {
// console.log("msg", msg);
if (msg.includes("PRIVATE")) {
if (
msg.includes("assistant finished output") ||
curChatEndRef.current
) {
setCurChatEnd(true);
} else {
const cleanedData = msg.replace(/^PRIVATE /, "");
try {
const chunkData = JSON.parse(cleanedData);
if (chunkData.reply_to_message === curIdRef.current) {
setCurMessage((prev) => prev + chunkData.message_chunk);
return chunkData.message_chunk;
}
} catch (error) {
console.error("JSON Parse error:", error);
if (msg.includes("websocket-session-id")) {
const array = msg.split(" ");
setWebsocketId(array[2]);
}
if (msg.includes("PRIVATE")) {
if (
msg.includes("assistant finished output") ||
curChatEndRef.current
) {
setCurChatEnd(true);
} else {
const cleanedData = msg.replace(/^PRIVATE /, "");
try {
const chunkData = JSON.parse(cleanedData);
if (chunkData.reply_to_message === curIdRef.current) {
handleMessageChunk(chunkData.message_chunk)
return chunkData.message_chunk;
}
} catch (error) {
console.error("JSON Parse error:", error);
}
}
}
}
}
}
}
);
);
useEffect(()=>{
setConnected(connected)
}, [connected])
useEffect(() => {
setConnected(connected)
}, [connected])
// websocket
useEffect(() => {
if (messages.length === 0 || !activeChat?._id) return;
const simulateAssistantResponse = useCallback(() => {
if (messages.length === 0 || !activeChat?._id) return;
const simulateAssistantResponse = () => {
console.log("messages", messages);
console.log("messages", messages);
const assistantMessage: Message = {
_id: activeChat._id,
_source: {
type: "assistant",
message: messages,
},
};
const updatedChat = {
...activeChat,
messages: [...(activeChat.messages || []), assistantMessage],
};
setMessages("");
setCurMessage("");
setActiveChat(updatedChat);
setTimeout(() => setIsTyping(false), 1000);
};
if (curChatEnd) {
simulateAssistantResponse();
}
}, [messages, curChatEnd]);
const scrollToBottom = () => {
messagesEndRef.current?.scrollIntoView({
behavior: "smooth",
block: "end",
});
};
useEffect(() => {
scrollToBottom();
}, [activeChat?.messages, isTyping, curMessage]);
const createNewChat = async () => {
chatClose();
try {
const response = await tauriFetch({
url: "/chat/_new",
method: "POST",
});
console.log("_new", response);
const newChat: Chat = response.data;
setActiveChat(newChat);
handleSendMessage(inputValue, newChat);
} catch (error) {
console.error("Failed to fetch user data:", error);
}
};
const init = () => {
if (!curChatEnd) return;
if (!activeChat?._id) {
createNewChat();
} else {
handleSendMessage(inputValue);
}
};
const handleSendMessage = async (content: string, newChat?: Chat) => {
newChat = newChat || activeChat;
if (!newChat?._id || !content) return;
try {
const response = await tauriFetch({
url: `/chat/${newChat?._id}/_send?search=${isSearchActive}`,
method: "POST",
headers: {
"WEBSOCKET-SESSION-ID": websocketId,
},
body: JSON.stringify({ message: content }),
});
console.log("_send", response, websocketId);
setCurId(response.data[0]?._id);
const updatedChat: Chat = {
...newChat,
messages: [...(newChat?.messages || []), ...(response.data || [])],
};
changeInput("");
setActiveChat(updatedChat);
setIsTyping(true);
setCurChatEnd(false);
} catch (error) {
console.error("Failed to fetch user data:", error);
}
};
const chatClose = async () => {
if (!activeChat?._id) return;
try {
const response = await tauriFetch({
url: `/chat/${activeChat._id}/_close`,
method: "POST",
});
console.log("_close", response);
} catch (error) {
console.error("Failed to fetch user data:", error);
}
};
const cancelChat = async () => {
setCurChatEnd(true);
setIsTyping(false);
if (!activeChat?._id) return;
try {
const response = await tauriFetch({
url: `/chat/${activeChat._id}/_cancel`,
method: "POST",
});
console.log("_cancel", response);
} catch (error) {
console.error("Failed to fetch user data:", error);
}
};
async function openChatAI() {
if (isTauri()) {
createWin && createWin({
label: "chat",
title: "Coco Chat",
dragDropEnabled: true,
center: true,
width: 900,
height: 800,
alwaysOnTop: true,
skipTaskbar: true,
decorations: true,
closable: true,
url: "/ui/app/chat",
});
}
}
if (!isTransitioned) return null;
return (
<div
data-tauri-drag-region
className={`h-[500px] flex flex-col rounded-xl overflow-hidden`}
>
<header
data-tauri-drag-region
className={`flex items-center justify-between py-2 px-1`}
>
<button
onClick={() => openChatAI()}
className={`p-2 rounded-lg transition-colors text-[#333] dark:text-[#d8d8d8]`}
>
<PanelLeft className="h-4 w-4" />
</button>
<button
onClick={() => {
createNewChat();
}}
className={`p-2 rounded-lg transition-colors text-[#333] dark:text-[#d8d8d8]`}
>
<MessageSquarePlus className="h-4 w-4" />
</button>
</header>
{/* Chat messages */}
<div className="overflow-y-auto border-t border-[rgba(0,0,0,0.1)] dark:border-[rgba(255,255,255,0.15)] custom-scrollbar">
{activeChat?.messages?.map((message, index) => (
<ChatMessage
key={message._id + index}
message={message}
isTyping={
isTyping &&
index === (activeChat.messages?.length || 0) - 1 &&
message._source?.type === "assistant"
}
/>
))}
{!curChatEnd && activeChat?._id ? (
<ChatMessage
key={"last"}
message={{
_id: activeChat?._id,
const assistantMessage: Message = {
_id: activeChat._id,
_source: {
type: "assistant",
message: curMessage,
type: "assistant",
message: messages,
},
}}
isTyping={!curChatEnd}
/>
) : null}
<div ref={messagesEndRef} />
</div>
</div>
);
}
);
};
const updatedChat = {
...activeChat,
messages: [...(activeChat.messages || []), assistantMessage],
};
setMessages("");
setCurMessage("");
console.log("updatedChat", updatedChat);
setActiveChat(updatedChat);
const timer = setTimeout(() => setIsTyping(false), 1000);
return () => clearTimeout(timer);
}, [activeChat?._id]);
// websocket
useEffect(() => {
if (curChatEnd) {
simulateAssistantResponse();
}
}, [messages, curChatEnd]);
const scrollToBottom = () => {
messagesEndRef.current?.scrollIntoView({
behavior: "smooth",
block: "end",
});
};
useEffect(() => {
scrollToBottom();
}, [activeChat?.messages, isTyping, curMessage]);
useEffect(() => {
return () => {
chatClose();
setMessages("");
setCurMessage("");
setActiveChat(undefined);
setIsTyping(false);
setCurChatEnd(true);
};
}, []);
const createNewChat = useCallback(async (value: string = "") => {
chatClose();
try {
const response = await tauriFetch({
url: "/chat/_new",
method: "POST",
});
console.log("_new", response);
const newChat: Chat = response.data;
setActiveChat(newChat);
handleSendMessage(value, newChat);
} catch (error) {
console.error("Failed to fetch user data:", error);
}
}, []);
const init = (value: string) => {
if (!curChatEnd) return;
if (!activeChat?._id) {
createNewChat(value);
} else {
handleSendMessage(value);
}
};
const handleSendMessage = async (content: string, newChat?: Chat) => {
newChat = newChat || activeChat;
if (!newChat?._id || !content) return;
try {
const response = await tauriFetch({
url: `/chat/${newChat?._id}/_send?search=${isSearchActive}&deep_thinking=${isDeepThinkActive}`,
method: "POST",
headers: {
"WEBSOCKET-SESSION-ID": websocketId,
},
body: JSON.stringify({message: content}),
});
console.log("_send", response, websocketId);
setCurId(response.data[0]?._id);
const updatedChat: Chat = {
...newChat,
messages: [...(newChat?.messages || []), ...(response.data || [])],
};
changeInput("");
console.log("updatedChat2", updatedChat);
setActiveChat(updatedChat);
setIsTyping(true);
setCurChatEnd(false);
} catch (error) {
console.error("Failed to fetch user data:", error);
}
};
const chatClose = async () => {
if (!activeChat?._id) return;
try {
const response = await tauriFetch({
url: `/chat/${activeChat._id}/_close`,
method: "POST",
});
console.log("_close", response);
} catch (error) {
console.error("Failed to fetch user data:", error);
}
};
const cancelChat = async () => {
setCurChatEnd(true);
setIsTyping(false);
if (!activeChat?._id) return;
try {
const response = await tauriFetch({
url: `/chat/${activeChat._id}/_cancel`,
method: "POST",
});
console.log("_cancel", response);
} catch (error) {
console.error("Failed to fetch user data:", error);
}
};
async function openChatAI() {
if (isTauri()) {
createWin && createWin({
label: "chat",
title: "Coco Chat",
dragDropEnabled: true,
center: true,
width: 1000,
height: 800,
alwaysOnTop: false,
skipTaskbar: false,
decorations: true,
closable: true,
url: "/ui/chat",
});
}
}
if (!isTransitioned) return null;
return (
<div
data-tauri-drag-region
className={`h-[500px] flex flex-col rounded-xl overflow-hidden`}
>
<header
data-tauri-drag-region
className={`flex items-center justify-between py-2 px-1`}
>
<button
onClick={() => openChatAI()}
className={`p-2 rounded-lg transition-colors text-[#333] dark:text-[#d8d8d8]`}
>
<PanelLeft className="h-4 w-4"/>
</button>
<button
onClick={() => {
createNewChat();
}}
className={`p-2 rounded-lg transition-colors text-[#333] dark:text-[#d8d8d8]`}
>
<MessageSquarePlus className="h-4 w-4"/>
</button>
</header>
{/* Chat messages */}
<div
className="w-full overflow-x-hidden overflow-y-auto border-t border-[rgba(0,0,0,0.1)] dark:border-[rgba(255,255,255,0.15)] custom-scrollbar">
{activeChat?.messages?.map((message, index) => (
<ChatMessage
key={message._id + index}
message={message}
isTyping={
isTyping &&
index === (activeChat.messages?.length || 0) - 1 &&
message._source?.type === "assistant"
}
/>
))}
{!curChatEnd && activeChat?._id ? (
<ChatMessage
key={"last"}
message={{
_id: activeChat?._id,
_source: {
type: "assistant",
message: curMessage,
},
}}
isTyping={!curChatEnd}
/>
) : null}
<div ref={messagesEndRef}/>
</div>
</div>
);
}
));
export default ChatAI;

View File

@@ -1,4 +1,4 @@
import { Mic, Send, Globe } from "lucide-react";
import { Send, Globe, Brain } from "lucide-react";
import {
useState,
type FormEvent,
@@ -6,8 +6,9 @@ import {
useRef,
useEffect,
} from "react";
import AutoResizeTextarea from "./AutoResizeTextarea";
import StopIcon from "../../icons/Stop";
import StopIcon from "@/icons/Stop";
interface ChatInputProps {
onSend: (message: string) => void;
@@ -16,6 +17,8 @@ interface ChatInputProps {
disabledChange: () => void;
isSearchActive: boolean;
setIsSearchActive: () => void;
isDeepThinkActive: boolean;
setIsDeepThinkActive: () => void;
}
export function ChatInput({
@@ -25,8 +28,9 @@ export function ChatInput({
disabledChange,
isSearchActive,
setIsSearchActive,
isDeepThinkActive,
setIsDeepThinkActive,
}: ChatInputProps) {
const [input, setInput] = useState("");
const textareaRef = useRef<HTMLTextAreaElement>(null);
@@ -64,6 +68,10 @@ export function ChatInput({
setIsSearchActive();
};
const DeepThinkClick = () => {
setIsDeepThinkActive();
};
return (
<form onSubmit={handleSubmit} className="w-full rounded-xl overflow-hidden">
<div className="bg-inputbox_bg_light dark:bg-inputbox_bg_dark bg-cover rounded-xl border border-[#E6E6E6] dark:border-[#272626]">
@@ -77,9 +85,9 @@ export function ChatInput({
handleKeyDown={handleKeyDown}
/>
</div>
<button className="p-1 hover:bg-gray-50 dark:hover:bg-gray-700 rounded-full transition-colors">
{/* <button className="p-1 hover:bg-gray-50 dark:hover:bg-gray-700 rounded-full transition-colors">
<Mic className="w-4 h-4 text-[#999] dark:text-[#999]" />
</button>
</button> */}
{curChatEnd ? (
<button
className={`ml-1 p-1 ${
@@ -108,20 +116,40 @@ export function ChatInput({
{/* Controls */}
<div className="flex justify-between items-center p-2 rounded-xl overflow-hidden">
<div className="flex gap-1 text-xs text-[#333] dark:text-[#d8d8d8]">
<div className="flex gap-1 px-[5px] text-xs text-[#333] dark:text-[#fff]">
<button
type="button"
className={`inline-flex items-center rounded-lg transition-colors relative py-1 px-[5px]`}
className={`h-5 px-2 inline-flex items-center border rounded-[10px] transition-colors relative ${
isDeepThinkActive ? "bg-[rgba(0,114,255,0.3)] border-[rgba(0,114,255,0.3)]" : "border-[#262727]"
}`}
onClick={DeepThinkClick}
>
<Brain
className={`w-3 h-3 mr-1 ${
isDeepThinkActive
? "text-[#0072FF] dark:text-[#0072FF]"
: "text-[#333] dark:text-white"
}`}
/>
<span className={isDeepThinkActive ? "text-[#0072FF]" : "dark:text-white"}>
Deep Think
</span>
</button>
<button
type="button"
className={`h-5 px-2 inline-flex items-center border rounded-[10px] transition-colors relative ${
isSearchActive ? "bg-[rgba(0,114,255,0.3)] border-[rgba(0,114,255,0.3)]" : "border-[#262727]"
}`}
onClick={SearchClick}
>
<Globe
className={`w-4 h-4 mr-1 ${
className={`w-3 h-3 mr-1 ${
isSearchActive
? "text-[#0072FF] dark:text-[#0072FF]"
: "text-[#000] dark:text-[#d8d8d8]"
: "text-[#333] dark:text-white"
}`}
/>
<span className={isSearchActive ? "text-[#0072FF]" : ""}>
<span className={isSearchActive ? "text-[#0072FF]" : "dark:text-white"}>
Search
</span>
</button>

View File

@@ -1,10 +1,11 @@
import { Brain, ChevronDown, ChevronUp, Search, SquareArrowOutUpRight } from "lucide-react";
import { Brain, ChevronDown, ChevronUp } from "lucide-react";
import { useState, useEffect, useRef } from "react";
import type { Message } from "./types";
import Markdown from "./Markdown";
import { formatThinkingMessage, OpenURLWithBrowser } from "@/utils/index";
import { formatThinkingMessage } from "@/utils/index";
import logoImg from "@/assets/icon.svg";
import { SourceResult } from "./SourceResult";
interface ChatMessageProps {
message: Message;
@@ -13,7 +14,6 @@ interface ChatMessageProps {
export function ChatMessage({ message, isTyping }: ChatMessageProps) {
const [isThinkingExpanded, setIsThinkingExpanded] = useState(true);
const [isSourceExpanded, setIsSourceExpanded] = useState(false);
const [responseTime, setResponseTime] = useState(0);
const startTimeRef = useRef<number | null>(null);
const hasStartedRef = useRef(false);
@@ -58,92 +58,25 @@ export function ChatMessage({ message, isTyping }: ChatMessageProps) {
{segment.isThinking || segment.thinkContent ? (
<div className="space-y-2 mb-3">
{segment.text?.includes("<Source") && (
<div>
<button
onClick={() =>
setIsSourceExpanded((prev) => !prev)
}
className="inline-flex items-center gap-2 px-2 py-1 bg-gray-100/50 dark:bg-gray-800/50 rounded hover:bg-gray-200/50 dark:hover:bg-gray-700/50 transition-colors"
>
<Search className="w-4 h-4 text-gray-500" />
<span className="text-xs text-gray-500 dark:text-gray-400">
Found{" "}
{segment.text.match(
/total=["']?(\d+)["']?/
)?.[1] || "0"}{" "}
results
</span>
{isSourceExpanded ? (
<ChevronUp className="w-4 h-4" />
) : (
<ChevronDown className="w-4 h-4" />
)}
</button>
{isSourceExpanded && (
<div className="mt-2 bg-white dark:bg-[#202126] rounded-lg overflow-hidden border border-gray-100 dark:border-gray-800 shadow-sm">
{(() => {
try {
const sourceMatch = segment.text.match(
/<Source[^>]*>(.*?)<\/Source>/s
);
if (!sourceMatch) return null;
const sourceData = JSON.parse(
sourceMatch[1]
);
return sourceData.map(
(item: any, idx: number) => (
<div
key={idx}
onClick={() => {
if (item.url) {
OpenURLWithBrowser(item.url);
}
}}
className="flex items-center px-3 py-2.5 hover:bg-gray-50 dark:hover:bg-black/10 border-b border-gray-100 dark:border-gray-800 last:border-b-0 cursor-pointer transition-colors"
>
<div className="flex-1 min-w-0 flex items-center gap-2">
<div className="flex-1 min-w-0">
<div className="text-sm text-gray-900 dark:text-[#D8D8D8] truncate font-medium group-hover:text-blue-500 dark:group-hover:text-blue-400">
{item.title || item.category}
</div>
</div>
<div className="flex items-center gap-2 flex-shrink-0 text-xs text-gray-500 dark:text-[#8B8B8B]">
<span>{item.source?.name}</span>
<SquareArrowOutUpRight className="w-3 h-3"/>
</div>
</div>
</div>
)
);
} catch (error) {
console.error(
"Failed to parse source data:",
error
);
return null;
}
})()}
</div>
)}
</div>
<SourceResult text={segment.text} />
)}
<button
onClick={() =>
setIsThinkingExpanded((prev) => !prev)
}
className="inline-flex items-center gap-2 px-2 py-1 bg-gray-100/50 dark:bg-gray-800/50 rounded hover:bg-gray-200/50 dark:hover:bg-gray-700/50 transition-colors"
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-gray-500" />
<span className="text-xs text-gray-500 dark:text-gray-400 italic">
<Brain className="w-4 h-4 animate-pulse text-[#999999]" />
<span className="text-xs text-[#999999] italic">
AI is thinking...
</span>
</>
) : (
<>
<Brain className="w-4 h-4 text-gray-500" />
<span className="text-xs text-gray-500 dark:text-gray-400">
<Brain className="w-4 h-4 text-[#999999]" />
<span className="text-xs text-[#999999]">
Thought for {responseTime.toFixed(1)} seconds
</span>
</>

View File

@@ -0,0 +1,98 @@
import {
Search,
ChevronUp,
ChevronDown,
SquareArrowOutUpRight,
} from "lucide-react";
import { useState } from "react";
import { OpenURLWithBrowser } from "@/utils/index";
interface SourceResultProps {
text: string;
}
interface SourceItem {
url?: string;
title?: string;
category?: string;
source?: {
name: string;
};
}
export function SourceResult({ text }: SourceResultProps) {
const [isSourceExpanded, setIsSourceExpanded] = useState(false);
if (!text?.includes("<Source")) {
return null;
}
const getSourceData = (): SourceItem[] => {
try {
const sourceMatch = text.match(/<Source[^>]*>(.*?)<\/Source>/s);
if (!sourceMatch) return [];
return JSON.parse(sourceMatch[1]);
} catch (error) {
console.error("Failed to parse source data:", error);
return [];
}
};
const totalResults = text.match(/total=["']?(\d+)["']?/)?.[1] || "0";
const sourceData = getSourceData();
return (
<div
className={`mt-2 ${
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 ${
isSourceExpanded ? "w-full" : "border border-[#E6E6E6] dark:border-[#272626]"
}`}
>
<div className="flex gap-2">
<Search className="w-4 h-4 text-[#999999] dark:text-[#999999]" />
<span className="text-xs text-[#999999] dark:text-[#999999]">
Found {totalResults} results
</span>
</div>
{isSourceExpanded ? (
<ChevronUp className="w-4 h-4 text-[#999999] dark:text-[#999999]" />
) : (
<ChevronDown className="w-4 h-4 text-[#999999] dark:text-[#999999]" />
)}
</button>
{isSourceExpanded && (
<div className="">
{sourceData.map((item, idx) => (
<div
key={idx}
onClick={() => item.url && OpenURLWithBrowser(item.url)}
className="group flex items-center px-2 py-1 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="flex-1 min-w-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="flex items-center gap-2 flex-shrink-0">
<span className="text-xs text-[#999999] dark:text-[#999999]">
{item.source?.name}
</span>
<SquareArrowOutUpRight className="w-3 h-3 text-[#999999] dark:text-[#999999]" />
</div>
</div>
</div>
))}
</div>
)}
</div>
);
}

View File

@@ -1,4 +1,4 @@
import { Library, Mic, Send, Plus, AudioLines, Image } from "lucide-react";
import { Library, Send, Plus, AudioLines, Image } from "lucide-react";
import { useRef, useState, useEffect, useCallback } from "react";
import { listen } from "@tauri-apps/api/event";
@@ -176,14 +176,14 @@ export default function ChatInput({
) : null}
</div>
{isChatMode ? (
{/* {isChatMode ? (
<button
className="p-1 hover:bg-gray-50 dark:hover:bg-gray-700 rounded-full transition-colors"
type="button"
>
<Mic className="w-4 h-4 text-[#999] dark:text-[#999]" />
</button>
) : null}
) : null} */}
{isChatMode && curChatEnd ? (
<button

View File

@@ -1,4 +1,4 @@
import { ArrowBigLeft, Mic, Search, Send, Globe } from "lucide-react";
import { ArrowBigLeft, Search, Send, Globe, Brain } from "lucide-react";
import { useCallback, useEffect, useRef, useState } from "react";
import { listen } from "@tauri-apps/api/event";
import { invoke, isTauri } from "@tauri-apps/api/core";
@@ -22,6 +22,8 @@ interface ChatInputProps {
reconnect: () => void;
isSearchActive: boolean;
setIsSearchActive: () => void;
isDeepThinkActive: boolean;
setIsDeepThinkActive: () => void;
}
export default function ChatInput({
@@ -35,6 +37,8 @@ export default function ChatInput({
reconnect,
isSearchActive,
setIsSearchActive,
isDeepThinkActive,
setIsDeepThinkActive,
}: ChatInputProps) {
const showTooltip = useAppStore(
(state: { showTooltip: boolean }) => state.showTooltip
@@ -216,6 +220,10 @@ export default function ChatInput({
setIsSearchActive();
};
const DeepThinkClick = () => {
setIsDeepThinkActive();
};
return (
<div className="w-full relative">
<div className="p-2 flex items-center dark:text-[#D8D8D8] bg-[#ededed] dark:bg-[#202126] rounded transition-all relative">
@@ -276,14 +284,23 @@ export default function ChatInput({
) : null}
</div>
{isChatMode ? (
{/* {isChatMode ? (
<button
className="p-1 hover:bg-gray-50 dark:hover:bg-gray-700 rounded-full transition-colors"
className={`p-1 hover:bg-gray-50 dark:hover:bg-gray-700 rounded-full transition-colors ${
isListening ? "bg-blue-100 dark:bg-blue-900" : ""
}`}
type="button"
onClick={() => {}}
>
<Mic className="w-4 h-4 text-[#999] dark:text-[#999]" />
<Mic
className={`w-4 h-4 ${
isListening
? "text-blue-500 animate-pulse"
: "text-[#999] dark:text-[#999]"
}`}
/>
</button>
) : null}
) : null} */}
{isChatMode && curChatEnd ? (
<button
@@ -347,6 +364,21 @@ export default function ChatInput({
>
{isChatMode ? (
<div className="flex gap-2 text-xs text-[#333] dark:text-[#d8d8d8]">
<button
className={`inline-flex items-center rounded-lg transition-colors relative py-1`}
onClick={DeepThinkClick}
>
<Brain
className={`w-4 h-4 mr-1 ${
isDeepThinkActive
? "text-[#0072FF] dark:text-[#0072FF]"
: "text-[#000] dark:text-[#d8d8d8]"
}`}
/>
<span className={isDeepThinkActive ? "text-[#0072FF]" : ""}>
Deep Think
</span>
</button>
<button
className={`inline-flex items-center rounded-lg transition-colors relative py-1`}
onClick={SearchClick}

View File

@@ -3,26 +3,14 @@ import SVGWrap from "./SVGWrap";
export default function Stop(props: I.SVG) {
return (
<SVGWrap viewBox="0 0 16 16" {...props}>
<g stroke="currentColor" strokeWidth="1" fill="none" fillRule="evenodd">
<path
d="M8,0.333333333 C12.2341831,0.333333333 15.6666667,3.76581692 15.6666667,8 C15.6666667,12.2341831 12.2341831,15.6666667 8,15.6666667 C3.76581692,15.6666667 0.333333333,12.2341831 0.333333333,8 C0.333333333,3.76581692 3.76581692,0.333333333 8,0.333333333 Z M8,1.66666667 C4.50219658,1.66666667 1.66666667,4.50219658 1.66666667,8 C1.66666667,11.4978034 4.50219658,14.3333333 8,14.3333333 C11.4978034,14.3333333 14.3333333,11.4978034 14.3333333,8 C14.3333333,4.50219658 11.4978034,1.66666667 8,1.66666667 Z"
fill="currentColor"
fillRule="nonzero"
></path>
<rect
fill="currentColor"
x="6"
y="6"
width="4"
height="4"
rx="0.666666687"
></rect>
<path
d="M9.33333333,5.33333333 C10.069713,5.33333333 10.6666667,5.93028701 10.6666667,6.66666669 L10.6666667,9.33333333 C10.6666667,10.069713 10.069713,10.6666667 9.33333333,10.6666667 L6.66666669,10.6666667 C5.93028701,10.6666667 5.33333333,10.069713 5.33333333,9.33333333 L5.33333333,6.66666669 C5.33333333,5.93028701 5.93028701,5.33333333 6.66666669,5.33333333 L9.33333333,5.33333333 Z M9.33333333,6.66666669 L6.66666667,6.66666669 L6.66666667,9.33333333 L9.33333333,9.33333333 L9.33333333,6.66666669 Z"
fill="currentColor"
fillRule="nonzero"
></path>
</g>
<g id="输入区域融合" stroke="none" strokeWidth="1" fill="none" fillRule="evenodd">
<g id="搜索结果" transform="translate(-1324, -770)" fill="#FFFFFF" fillRule="nonzero">
<g id="停止" transform="translate(1324, 770)">
<rect id="矩形" opacity="0" x="0" y="0" width="16" height="16"></rect>
<path d="M4.64003125,12.7998906 L11.360125,12.7998906 C12.1554063,12.7998906 12.8,12.1552969 12.8,11.3600156 L12.8,4.64 C12.8,3.84475 12.1554375,3.2 11.360125,3.2 L4.64003125,3.2 C3.84476562,3.2 3.20003125,3.84475 3.20003125,4.64 L3.20003125,11.36 C3.20003125,12.15525 3.84473437,12.7998906 4.64003125,12.7998906 L4.64003125,12.7998906 Z" id="路径"></path>
</g>
</g>
</g>
</SVGWrap>
);
}

View File

@@ -2,13 +2,13 @@ import { useState, useRef, useEffect } from "react";
import { PanelRightClose, PanelRightOpen, X } from "lucide-react";
import { isTauri } from "@tauri-apps/api/core";
import { ChatMessage } from "./ChatMessage";
import { ChatInput } from "./ChatInput";
import { Sidebar } from "./Sidebar";
import type { Chat, Message } from "./types";
import { tauriFetch } from "../../api/tauriFetchClient";
import { useWebSocket } from "../../hooks/useWebSocket";
import { useWindows } from "../../hooks/useWindows";
import { ChatMessage } from "@/components/Assistant/ChatMessage";
import { ChatInput } from "@/components/Assistant/ChatInput";
import { Sidebar } from "@/components/Assistant/Sidebar";
import type { Chat, Message } from "@/components/Assistant/types";
import { tauriFetch } from "@/api/tauriFetchClient";
import { useWebSocket } from "@/hooks/useWebSocket";
import { useWindows } from "@/hooks/useWindows";
import { clientEnv } from "@/utils/env";
// import { useAppStore } from '@/stores/appStore';
import ApiDetails from "@/components/Common/ApiDetails";
@@ -33,6 +33,7 @@ export default function ChatAI({}: ChatAIProps) {
const [curId, setCurId] = useState("");
const [isSearchActive, setIsSearchActive] = useState(false);
const [isDeepThinkActive, setIsDeepThinkActive] = useState(false);
const curChatEndRef = useRef(curChatEnd);
curChatEndRef.current = curChatEnd;
@@ -168,7 +169,7 @@ export default function ChatAI({}: ChatAIProps) {
if (!activeChat?._id) return;
try {
const response = await tauriFetch({
url: `/chat/${activeChat?._id}/_send?search=${isSearchActive}`,
url: `/chat/${activeChat?._id}/_send?search=${isSearchActive}&deep_thinking=${isDeepThinkActive}`,
method: "POST",
headers: {
"WEBSOCKET-SESSION-ID": websocketId,
@@ -286,9 +287,9 @@ export default function ChatAI({}: ChatAIProps) {
className={`rounded-lg transition-colors hover:bg-gray-100 text-gray-600 dark:hover:bg-gray-800 dark:text-gray-300`}
>
{isSidebarOpen ? (
<PanelRightClose className="h-6 w-6" />
) : (
<PanelRightOpen className="h-6 w-6" />
) : (
<PanelRightClose className="h-6 w-6" />
)}
</button>
@@ -337,6 +338,8 @@ export default function ChatAI({}: ChatAIProps) {
}}
isSearchActive={isSearchActive}
setIsSearchActive={() => setIsSearchActive((prev) => !prev)}
isDeepThinkActive={isDeepThinkActive}
setIsDeepThinkActive={() => setIsDeepThinkActive((prev) => !prev)}
/>
</div>
</div>

View File

@@ -53,6 +53,7 @@ export default function DesktopApp() {
const [isTransitioned, setIsTransitioned] = useState(false);
const [isSearchActive, setIsSearchActive] = useState(false);
const [isDeepThinkActive, setIsDeepThinkActive] = useState(false);
async function changeMode(value: boolean) {
setIsChatMode(value);
@@ -69,7 +70,7 @@ export default function DesktopApp() {
if (isTauri()) {
await getCurrentWebviewWindow()?.setSize(new LogicalSize(680, 596));
}
chatAIRef.current?.init();
chatAIRef.current?.init(value);
}
};
const cancelChat = () => {
@@ -110,7 +111,9 @@ export default function DesktopApp() {
changeInput={changeInput}
reconnect={reconnect}
isSearchActive={isSearchActive}
setIsSearchActive={() => setIsSearchActive((prev) => !prev)}
setIsSearchActive={() => setIsSearchActive((prev) => !prev)}
isDeepThinkActive={isDeepThinkActive}
setIsDeepThinkActive={() => setIsDeepThinkActive((prev) => !prev)}
/>
</div>
@@ -140,10 +143,10 @@ export default function DesktopApp() {
<ChatAI
ref={chatAIRef}
key="ChatAI"
inputValue={input}
isTransitioned={isTransitioned}
changeInput={changeInput}
isSearchActive={isSearchActive}
isDeepThinkActive={isDeepThinkActive}
/>
) : null}
</div>

View File

@@ -1,10 +1,10 @@
import { createBrowserRouter } from "react-router-dom";
import Layout from "./layout.tsx";
import Layout from "./layout";
import ErrorPage from "@/error-page";
import DesktopApp from "@/pages/main/index.tsx";
import SettingsPage from "@/pages/settings/index.tsx";
import ChatAI from "@/components/Assistant";
import DesktopApp from "@/pages/main/index";
import SettingsPage from "@/pages/settings/index";
import ChatAI from "@/pages/chat/index";
export const router = createBrowserRouter([
{
@@ -14,7 +14,7 @@ export const router = createBrowserRouter([
children: [
{ path: "/ui", element: <DesktopApp /> },
{ path: "/ui/settings", element: <SettingsPage /> },
{ path: "/ui/app/chat", element: <ChatAI /> },
{ path: "/ui/chat", element: <ChatAI /> },
],
},
]);

View File

@@ -1,4 +1,4 @@
export const clientEnv = {
COCO_SERVER_URL: "http://localhost:9000", //"https://coco.infini.cloud", // http://localhost:9000
COCO_WEBSOCKET_URL: "ws://localhost:9000/ws", // "wss://coco.infini.cloud/ws", // ws://localhost:9000/ws
COCO_SERVER_URL: "https://coco.infini.cloud", // http://localhost:9000
COCO_WEBSOCKET_URL: "wss://coco.infini.cloud/ws", // ws://localhost:9000/ws
};