chore: stop chat & ui style (#25)

This commit is contained in:
BiggerRain
2024-11-27 19:41:54 +08:00
committed by GitHub
parent e50c595195
commit 764e508ce6
25 changed files with 216 additions and 142 deletions

View File

@@ -15,6 +15,7 @@
"@tauri-apps/plugin-http": "~2.0.1", "@tauri-apps/plugin-http": "~2.0.1",
"@tauri-apps/plugin-shell": ">=2.0.0", "@tauri-apps/plugin-shell": ">=2.0.0",
"@tauri-apps/plugin-websocket": "~2", "@tauri-apps/plugin-websocket": "~2",
"@tauri-apps/plugin-window": "2.0.0-alpha.1",
"@traptitech/markdown-it-katex": "^3.6.0", "@traptitech/markdown-it-katex": "^3.6.0",
"axios": "^1.7.7", "axios": "^1.7.7",
"clsx": "^2.1.1", "clsx": "^2.1.1",

16
pnpm-lock.yaml generated
View File

@@ -23,6 +23,9 @@ importers:
'@tauri-apps/plugin-websocket': '@tauri-apps/plugin-websocket':
specifier: ~2 specifier: ~2
version: 2.0.0 version: 2.0.0
'@tauri-apps/plugin-window':
specifier: 2.0.0-alpha.1
version: 2.0.0-alpha.1
'@traptitech/markdown-it-katex': '@traptitech/markdown-it-katex':
specifier: ^3.6.0 specifier: ^3.6.0
version: 3.6.0 version: 3.6.0
@@ -607,6 +610,10 @@ packages:
'@tanstack/virtual-core@3.10.8': '@tanstack/virtual-core@3.10.8':
resolution: {integrity: sha512-PBu00mtt95jbKFi6Llk9aik8bnR3tR/oQP1o3TSi+iG//+Q2RTIzCEgKkHG8BB86kxMNW6O8wku+Lmi+QFR6jA==} resolution: {integrity: sha512-PBu00mtt95jbKFi6Llk9aik8bnR3tR/oQP1o3TSi+iG//+Q2RTIzCEgKkHG8BB86kxMNW6O8wku+Lmi+QFR6jA==}
'@tauri-apps/api@2.0.0-alpha.6':
resolution: {integrity: sha512-ZMOc3eu9amwvkC6M69h3hWt4/EsFaAXmtkiw4xd2LN59/lTb4ZQiVfq2QKlRcu1rj3n/Tcr7U30ZopvHwXBGIg==}
engines: {node: '>= 14.6.0', npm: '>= 6.6.0', yarn: '>= 1.19.1'}
'@tauri-apps/api@2.0.2': '@tauri-apps/api@2.0.2':
resolution: {integrity: sha512-3wSwmG+1kr6WrgAFKK5ijkNFPp8TT3FLj3YHUb5EwMO+3FxX4uWlfSWkeeBy+Kc1RsKzugtYLuuya+98Flj+3w==} resolution: {integrity: sha512-3wSwmG+1kr6WrgAFKK5ijkNFPp8TT3FLj3YHUb5EwMO+3FxX4uWlfSWkeeBy+Kc1RsKzugtYLuuya+98Flj+3w==}
@@ -688,6 +695,9 @@ packages:
'@tauri-apps/plugin-websocket@2.0.0': '@tauri-apps/plugin-websocket@2.0.0':
resolution: {integrity: sha512-O2qRxZCljd4g+ceJhW7LfgQr+fg0fBBiAaLiMopoKL6TXKMnhBHOenp4nZ5/MoVTr77OQIDNO6Jp/c1YwiRVtQ==} resolution: {integrity: sha512-O2qRxZCljd4g+ceJhW7LfgQr+fg0fBBiAaLiMopoKL6TXKMnhBHOenp4nZ5/MoVTr77OQIDNO6Jp/c1YwiRVtQ==}
'@tauri-apps/plugin-window@2.0.0-alpha.1':
resolution: {integrity: sha512-dFOAgal/3Txz3SQ+LNQq0AK1EPC+acdaFlwPVB/6KXUZYmaFleIlzgxDVoJCQ+/xOhxvYrdQaFLefh0I/Kldbg==}
'@traptitech/markdown-it-katex@3.6.0': '@traptitech/markdown-it-katex@3.6.0':
resolution: {integrity: sha512-CnJzTWxsgLGXFdSrWRaGz7GZ1kUUi8g3E9HzJmeveX1YwVJavrKYqysktfHZQsujdnRqV5O7g8FPKEA/aeTkOQ==} resolution: {integrity: sha512-CnJzTWxsgLGXFdSrWRaGz7GZ1kUUi8g3E9HzJmeveX1YwVJavrKYqysktfHZQsujdnRqV5O7g8FPKEA/aeTkOQ==}
@@ -2667,6 +2677,8 @@ snapshots:
'@tanstack/virtual-core@3.10.8': {} '@tanstack/virtual-core@3.10.8': {}
'@tauri-apps/api@2.0.0-alpha.6': {}
'@tauri-apps/api@2.0.2': {} '@tauri-apps/api@2.0.2': {}
'@tauri-apps/cli-darwin-arm64@2.0.3': '@tauri-apps/cli-darwin-arm64@2.0.3':
@@ -2724,6 +2736,10 @@ snapshots:
dependencies: dependencies:
'@tauri-apps/api': 2.0.2 '@tauri-apps/api': 2.0.2
'@tauri-apps/plugin-window@2.0.0-alpha.1':
dependencies:
'@tauri-apps/api': 2.0.0-alpha.6
'@traptitech/markdown-it-katex@3.6.0': '@traptitech/markdown-it-katex@3.6.0':
dependencies: dependencies:
katex: 0.16.11 katex: 0.16.11

View File

Before

Width:  |  Height:  |  Size: 244 KiB

After

Width:  |  Height:  |  Size: 244 KiB

View File

Before

Width:  |  Height:  |  Size: 130 KiB

After

Width:  |  Height:  |  Size: 130 KiB

View File

Before

Width:  |  Height:  |  Size: 71 KiB

After

Width:  |  Height:  |  Size: 71 KiB

View File

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 40 KiB

View File

Before

Width:  |  Height:  |  Size: 192 KiB

After

Width:  |  Height:  |  Size: 192 KiB

View File

Before

Width:  |  Height:  |  Size: 120 KiB

After

Width:  |  Height:  |  Size: 120 KiB

BIN
src/assets/chat_bg_dark.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 244 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 130 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 192 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 120 KiB

View File

@@ -22,15 +22,17 @@ interface ChatAIProps {
export interface ChatAIRef { export interface ChatAIRef {
init: () => void; init: () => void;
cancelChat: () => void;
} }
const ChatAI = forwardRef<ChatAIRef, ChatAIProps>( const ChatAI = forwardRef<ChatAIRef, ChatAIProps>(
({ inputValue, isTransitioned, changeInput }, ref) => { ({ inputValue, isTransitioned, changeInput }, ref) => {
useImperativeHandle(ref, () => ({ useImperativeHandle(ref, () => ({
init: init, init: init,
cancelChat: cancelChat,
})); }));
const { curChatEnd, setCurChatEnd, stopChat } = useChatStore(); const { curChatEnd, setCurChatEnd } = useChatStore();
const [activeChat, setActiveChat] = useState<Chat>(); const [activeChat, setActiveChat] = useState<Chat>();
const [isTyping, setIsTyping] = useState(false); const [isTyping, setIsTyping] = useState(false);
@@ -39,6 +41,13 @@ const ChatAI = forwardRef<ChatAIRef, ChatAIProps>(
const [websocketId, setWebsocketId] = useState(""); const [websocketId, setWebsocketId] = useState("");
const [curMessage, setCurMessage] = useState(""); const [curMessage, setCurMessage] = useState("");
const [curId, setCurId] = useState("");
const curChatEndRef = useRef(curChatEnd);
curChatEndRef.current = curChatEnd;
const curIdRef = useRef(curId);
curIdRef.current = curId;
const { messages, setMessages } = useWebSocket( const { messages, setMessages } = useWebSocket(
"ws://localhost:2900/ws", "ws://localhost:2900/ws",
(msg) => { (msg) => {
@@ -48,28 +57,30 @@ const ChatAI = forwardRef<ChatAIRef, ChatAIProps>(
} }
if (msg.includes("PRIVATE")) { if (msg.includes("PRIVATE")) {
if (msg.includes("assistant finished output")) { if (
msg.includes("assistant finished output") ||
curChatEndRef.current
) {
setCurChatEnd(true); setCurChatEnd(true);
} else { } else {
const cleanedData = msg.replace(/^PRIVATE /, ""); const cleanedData = msg.replace(/^PRIVATE /, "");
try { try {
const chunkData = JSON.parse(cleanedData); const chunkData = JSON.parse(cleanedData);
if (chunkData.reply_to_message === curIdRef.current) {
setCurMessage((prev) => prev + chunkData.message_chunk); setCurMessage((prev) => prev + chunkData.message_chunk);
return chunkData.message_chunk; return chunkData.message_chunk;
}
} catch (error) { } catch (error) {
console.error("JSON Parse error:", error); console.error("JSON Parse error:", error);
} }
return "";
} }
} }
return "";
} }
); );
// websocket // websocket
useEffect(() => { useEffect(() => {
if (messages.length === 0 || !activeChat?._id || stopChat) return; if (messages.length === 0 || !activeChat?._id) return;
const simulateAssistantResponse = () => { const simulateAssistantResponse = () => {
console.log("messages", messages); console.log("messages", messages);
@@ -94,7 +105,7 @@ const ChatAI = forwardRef<ChatAIRef, ChatAIProps>(
if (curChatEnd) { if (curChatEnd) {
simulateAssistantResponse(); simulateAssistantResponse();
} }
}, [messages, curChatEnd, stopChat]); }, [messages, curChatEnd]);
const scrollToBottom = () => { const scrollToBottom = () => {
messagesEndRef.current?.scrollIntoView({ messagesEndRef.current?.scrollIntoView({
@@ -125,6 +136,7 @@ const ChatAI = forwardRef<ChatAIRef, ChatAIProps>(
}; };
const init = () => { const init = () => {
if (!curChatEnd) return;
if (!activeChat?._id) { if (!activeChat?._id) {
createNewChat(); createNewChat();
} else { } else {
@@ -145,6 +157,7 @@ const ChatAI = forwardRef<ChatAIRef, ChatAIProps>(
body: JSON.stringify({ message: content }), body: JSON.stringify({ message: content }),
}); });
console.log("_send", response, websocketId); console.log("_send", response, websocketId);
setCurId(response.data[0]?._id);
const updatedChat: Chat = { const updatedChat: Chat = {
...newChat, ...newChat,
messages: [...(newChat?.messages || []), ...(response.data || [])], messages: [...(newChat?.messages || []), ...(response.data || [])],
@@ -172,24 +185,21 @@ const ChatAI = forwardRef<ChatAIRef, ChatAIProps>(
}; };
const cancelChat = async () => { const cancelChat = async () => {
setCurChatEnd(true);
setIsTyping(false);
if (!activeChat?._id) return; if (!activeChat?._id) return;
try { try {
const response = await tauriFetch({ const response = await tauriFetch({
url: `/chat/${activeChat._id}/_cancel`, url: `/chat/${activeChat._id}/_cancel`,
method: "POST", method: "POST",
}); });
console.log("_cancel", response); console.log("_cancel", response);
} catch (error) { } catch (error) {
console.error("Failed to fetch user data:", error); console.error("Failed to fetch user data:", error);
} }
}; };
useEffect(() => {
if (curChatEnd) {
cancelChat();
}
}, [curChatEnd]);
async function openChatAI() { async function openChatAI() {
createWin({ createWin({
label: "chat", label: "chat",

View File

@@ -6,20 +6,21 @@ import {
useRef, useRef,
useEffect, useEffect,
} from "react"; } from "react";
import ChatSwitch from "../SearchChat/ChatSwitch";
import AutoResizeTextarea from "./AutoResizeTextarea"; import AutoResizeTextarea from "./AutoResizeTextarea";
import StopIcon from "../../icons/Stop";
interface ChatInputProps { interface ChatInputProps {
onSend: (message: string) => void; onSend: (message: string) => void;
disabled: boolean; disabled: boolean;
disabledChange: (disabled: boolean) => void; curChatEnd: boolean;
changeMode: (isChatMode: boolean) => void; disabledChange: () => void;
} }
export function ChatInput({ export function ChatInput({
onSend, onSend,
disabled, disabled,
changeMode, curChatEnd,
disabledChange,
}: ChatInputProps) { }: ChatInputProps) {
const [input, setInput] = useState(""); const [input, setInput] = useState("");
const textareaRef = useRef<HTMLTextAreaElement>(null); const textareaRef = useRef<HTMLTextAreaElement>(null);
@@ -58,7 +59,7 @@ export function ChatInput({
return ( return (
<form onSubmit={handleSubmit} className="w-full rounded-xl overflow-hidden"> <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"> <div className="bg-inputbox_bg_light dark:bg-inputbox_bg_dark bg-cover rounded-xl border border-[#E6E6E6] dark:border-[#272626]">
{/* Search Bar */} {/* Search Bar */}
<div className="relative"> <div className="relative">
<div className="p-[13px] flex items-center bg-white dark:bg-[#202126] rounded-xl transition-all"> <div className="p-[13px] flex items-center bg-white dark:bg-[#202126] rounded-xl transition-all">
@@ -72,6 +73,7 @@ export function ChatInput({
<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]" /> <Mic className="w-4 h-4 text-[#999] dark:text-[#999]" />
</button> </button>
{curChatEnd ? (
<button <button
className={`ml-1 p-1 ${ className={`ml-1 p-1 ${
input ? "bg-[#0072FF]" : "bg-[#E4E5F0]" input ? "bg-[#0072FF]" : "bg-[#E4E5F0]"
@@ -80,6 +82,20 @@ export function ChatInput({
> >
<Send className="w-4 h-4 text-white hover:text-[#999]" /> <Send className="w-4 h-4 text-white hover:text-[#999]" />
</button> </button>
) : null}
{!curChatEnd ? (
<button
className={`ml-1 px-1 bg-[#0072FF] rounded-full transition-colors`}
type="submit"
onClick={() => disabledChange()}
>
<StopIcon
size={16}
className="w-4 h-4 text-white"
aria-label="Stop message"
/>
</button>
) : null}
</div> </div>
</div> </div>
@@ -98,15 +114,6 @@ export function ChatInput({
Upload Upload
</button> </button>
</div> </div>
{/* Switch */}
<ChatSwitch
isChatMode={true}
onChange={(value) => {
changeMode(value);
setInput("");
}}
/>
</div> </div>
</div> </div>
</form> </form>

View File

@@ -1,7 +1,5 @@
import { MessageSquare, Plus } from "lucide-react"; import { MessageSquare, Plus } from "lucide-react";
import type { Chat } from "./types"; import type { Chat } from "./types";
import { useTheme } from "../ThemeProvider";
interface SidebarProps { interface SidebarProps {
chats: Chat[]; chats: Chat[];
activeChat: Chat; activeChat: Chat;
@@ -18,25 +16,14 @@ export function Sidebar({
onSelectChat, onSelectChat,
className = "", className = "",
}: SidebarProps) { }: SidebarProps) {
const { theme } = useTheme();
const isDark = theme === "dark";
return ( return (
<div className={`h-full flex flex-col ${className}`}> <div className={`h-full flex flex-col ${className}`}>
<div className="p-4"> <div className="p-4">
<button <button
onClick={onNewChat} onClick={onNewChat}
className={`w-full flex items-center gap-3 px-4 py-3 text-sm rounded-lg transition-all ${ className={`w-full flex items-center gap-3 px-4 py-3 text-sm rounded-lg transition-all border border-[#E6E6E6] dark:border-[#272626] text-gray-700 hover:bg-gray-50 active:bg-gray-100 shadow-sm dark:text-white dark:hover:bg-gray-600 dark:active:bg-gray-500`}
isDark
? " text-white hover:bg-gray-600 active:bg-gray-500"
: " text-gray-700 hover:bg-gray-50 active:bg-gray-100 shadow-sm"
}`}
> >
<Plus <Plus className={`h-4 w-4 text-indigo-600 dark:text-indigo-400`} />
className={`h-4 w-4 ${
isDark ? "text-indigo-400" : "text-indigo-600"
}`}
/>
New Chat New Chat
</button> </button>
</div> </div>
@@ -44,30 +31,14 @@ export function Sidebar({
{chats.map((chat) => ( {chats.map((chat) => (
<div <div
key={chat._id} key={chat._id}
className={`group relative rounded-lg transition-all ${ className={`group relative rounded-lg transition-all hover:border border-[#E6E6E6] dark:border-[#272626] text-gray-900 shadow-sm dark:text-white`}
activeChat._id === chat._id
? isDark
? " text-white"
: " text-gray-900 shadow-sm"
: isDark
? "text-gray-300 hover:bg-gray-700/30"
: "text-gray-600 hover:bg-white/10"
}`}
> >
<button <button
className="w-full flex items-center gap-3 px-4 py-3 text-sm text-left" className="w-full flex items-center gap-3 px-4 py-3 text-sm text-left"
onClick={() => onSelectChat(chat)} onClick={() => onSelectChat(chat)}
> >
<MessageSquare <MessageSquare
className={`h-4 w-4 flex-shrink-0 ${ className={`h-4 w-4 flex-shrink-0 text-indigo-600 dark:text-indigo-400`}
activeChat._id === chat._id
? isDark
? "text-indigo-400"
: "text-indigo-600"
: isDark
? "text-gray-400"
: "text-gray-500"
}`}
/> />
<span className="truncate">{chat.title || chat._id}</span> <span className="truncate">{chat.title || chat._id}</span>
</button> </button>
@@ -85,9 +56,7 @@ export function Sidebar({
)} */} )} */}
{activeChat._id === chat._id && ( {activeChat._id === chat._id && (
<div <div
className={`absolute left-0 top-1/2 -translate-y-1/2 w-1 h-6 rounded-full ${ className={`absolute left-0 top-1/2 -translate-y-1/2 w-1 h-6 rounded-full bg-indigo-600 dark:bg-indigo-400`}
isDark ? "bg-indigo-400" : "bg-indigo-600"
}`}
/> />
)} )}
</div> </div>

View File

@@ -22,6 +22,14 @@ export default function ChatAI({}: ChatAIProps) {
const [websocketId, setWebsocketId] = useState(""); const [websocketId, setWebsocketId] = useState("");
const [curMessage, setCurMessage] = useState(""); const [curMessage, setCurMessage] = useState("");
const [curChatEnd, setCurChatEnd] = useState(true); const [curChatEnd, setCurChatEnd] = useState(true);
const [curId, setCurId] = useState("");
const curChatEndRef = useRef(curChatEnd);
curChatEndRef.current = curChatEnd;
const curIdRef = useRef(curId);
curIdRef.current = curId;
const { messages, setMessages } = useWebSocket( const { messages, setMessages } = useWebSocket(
"ws://localhost:2900/ws", "ws://localhost:2900/ws",
(msg) => { (msg) => {
@@ -31,22 +39,24 @@ export default function ChatAI({}: ChatAIProps) {
} }
if (msg.includes("PRIVATE")) { if (msg.includes("PRIVATE")) {
if (msg.includes("assistant finished output")) { if (
msg.includes("assistant finished output") ||
curChatEndRef.current
) {
setCurChatEnd(true); setCurChatEnd(true);
} else { } else {
const cleanedData = msg.replace(/^PRIVATE /, ""); const cleanedData = msg.replace(/^PRIVATE /, "");
try { try {
const chunkData = JSON.parse(cleanedData); const chunkData = JSON.parse(cleanedData);
if (chunkData.reply_to_message === curIdRef.current) {
setCurMessage((prev) => prev + chunkData.message_chunk); setCurMessage((prev) => prev + chunkData.message_chunk);
return chunkData.message_chunk; return chunkData.message_chunk;
}
} catch (error) { } catch (error) {
console.error("JSON Parse error:", error); console.error("JSON Parse error:", error);
} }
return "";
} }
} }
return "";
} }
); );
@@ -153,6 +163,7 @@ export default function ChatAI({}: ChatAIProps) {
body: JSON.stringify({ message: content }), body: JSON.stringify({ message: content }),
}); });
console.log("_send", response, websocketId); console.log("_send", response, websocketId);
setCurId(response.data[0]?._id);
const updatedChat: Chat = { const updatedChat: Chat = {
...activeChat, ...activeChat,
messages: [...(activeChat?.messages || []), ...(response.data || [])], messages: [...(activeChat?.messages || []), ...(response.data || [])],
@@ -229,14 +240,14 @@ export default function ChatAI({}: ChatAIProps) {
} }
return ( return (
<div className="h-screen bg-chat_bg_light dark:bg-chat_bg_dark bg-cover"> <div className="h-screen">
<div className="h-[100%] flex"> <div className="h-[100%] flex">
{/* Sidebar */} {/* Sidebar */}
{isSidebarOpen ? ( {isSidebarOpen ? (
<div <div
className={`fixed inset-y-0 left-0 z-50 w-64 transform ${ className={`fixed inset-y-0 left-0 z-50 w-64 transform ${
isSidebarOpen ? "translate-x-0" : "-translate-x-full" isSidebarOpen ? "translate-x-0" : "-translate-x-full"
} transition-transform duration-300 ease-in-out md:translate-x-0 md:static md:block`} } transition-transform duration-300 ease-in-out md:translate-x-0 md:static md:block bg-gray-100 dark:bg-gray-800`}
> >
{activeChat ? ( {activeChat ? (
<Sidebar <Sidebar
@@ -251,7 +262,7 @@ export default function ChatAI({}: ChatAIProps) {
) : null} ) : null}
{/* Main content */} {/* Main content */}
<div className={`flex-1 flex flex-col`}> <div className={`flex-1 flex flex-col bg-white dark:bg-gray-900`}>
<header <header
className={`flex items-center justify-between p-2 border-b border-gray-200 dark:border-gray-800`} className={`flex items-center justify-between p-2 border-b border-gray-200 dark:border-gray-800`}
> >
@@ -295,13 +306,6 @@ export default function ChatAI({}: ChatAIProps) {
isTyping={!curChatEnd} isTyping={!curChatEnd}
/> />
) : null} ) : null}
{isTyping && (
<div className="flex pt-0 pb-4 pl-20 gap-2 items-center text-gray-500 dark:text-gray-400">
<div className="w-2 h-2 rounded-full bg-current animate-bounce" />
<div className="w-2 h-2 rounded-full bg-current animate-bounce [animation-delay:0.2s]" />
<div className="w-2 h-2 rounded-full bg-current animate-bounce [animation-delay:0.4s]" />
</div>
)}
<div ref={messagesEndRef} /> <div ref={messagesEndRef} />
</div> </div>
@@ -310,11 +314,12 @@ export default function ChatAI({}: ChatAIProps) {
<ChatInput <ChatInput
onSend={handleSendMessage} onSend={handleSendMessage}
disabled={isTyping} disabled={isTyping}
disabledChange={(value) => { curChatEnd={curChatEnd}
disabledChange={() => {
cancelChat(); cancelChat();
setIsTyping(value); setCurChatEnd(true);
setIsTyping(false);
}} }}
changeMode={() => {}}
/> />
</div> </div>
</div> </div>

View File

@@ -11,7 +11,7 @@ function DropdownList({ selected, suggests }: DropdownListProps) {
const [selectedItem, setSelectedItem] = useState<number | null>(null); const [selectedItem, setSelectedItem] = useState<number | null>(null);
const [showIndex, setShowIndex] = useState<boolean>(false); const [showIndex, setShowIndex] = useState<boolean>(false);
const containerRef = useRef<HTMLDivElement>(null); const containerRef = useRef<HTMLDivElement>(null);
const itemRefs = useRef<(HTMLButtonElement | null)[]>([]); const itemRefs = useRef<(HTMLDivElement | null)[]>([]);
const handleOpenURL = async (url: string) => { const handleOpenURL = async (url: string) => {
if (!url) return; if (!url) return;
@@ -103,13 +103,16 @@ function DropdownList({ selected, suggests }: DropdownListProps) {
<div <div
ref={containerRef} ref={containerRef}
data-tauri-drag-region data-tauri-drag-region
className="h-[500px] w-full p-2 pb-10 flex flex-col rounded-xl overflow-y-auto overflow-hidden focus:outline-none" className="max-h-[458px] w-full p-2 flex flex-col rounded-xl overflow-y-auto overflow-hidden custom-scrollbar focus:outline-none"
tabIndex={0} tabIndex={0}
> >
<div className="p-2 text-xs text-[#999] dark:text-[#666]">
Results
</div>
{suggests?.map((item, index) => { {suggests?.map((item, index) => {
const isSelected = selectedItem === index; const isSelected = selectedItem === index;
return ( return (
<button <div
key={item._id} key={item._id}
ref={(el) => (itemRefs.current[index] = el)} ref={(el) => (itemRefs.current[index] = el)}
onMouseEnter={() => setSelectedItem(index)} onMouseEnter={() => setSelectedItem(index)}
@@ -120,7 +123,7 @@ function DropdownList({ selected, suggests }: DropdownListProps) {
selected(item); selected(item);
} }
}} }}
className={`w-full h-10 px-2 text-sm flex items-center justify-between rounded-lg transition-colors ${ className={`w-full px-2 py-2.5 text-sm flex items-center justify-between rounded-lg transition-colors ${
isSelected isSelected
? "bg-[rgba(0,0,0,0.1)] dark:bg-[rgba(255,255,255,0.1)]" ? "bg-[rgba(0,0,0,0.1)] dark:bg-[rgba(255,255,255,0.1)]"
: "hover:bg-[rgba(0,0,0,0.1)] dark:hover:bg-[rgba(255,255,255,0.1)]" : "hover:bg-[rgba(0,0,0,0.1)] dark:hover:bg-[rgba(255,255,255,0.1)]"
@@ -144,7 +147,7 @@ function DropdownList({ selected, suggests }: DropdownListProps) {
</div> </div>
) : null} ) : null}
</div> </div>
</button> </div>
); );
})} })}
</div> </div>

View File

@@ -1,22 +1,15 @@
import { import { Library, Mic, Send, Plus, AudioLines, Image } from "lucide-react";
Library,
Mic,
Send,
Plus,
AudioLines,
Image,
CircleStop,
} from "lucide-react";
import { useRef, type KeyboardEvent } from "react"; import { useRef, type KeyboardEvent } from "react";
import ChatSwitch from "../SearchChat/ChatSwitch"; import ChatSwitch from "../SearchChat/ChatSwitch";
import AutoResizeTextarea from "./AutoResizeTextarea"; import AutoResizeTextarea from "./AutoResizeTextarea";
import { useChatStore } from "../../stores/chatStore"; import { useChatStore } from "../../stores/chatStore";
import StopIcon from "../../icons/Stop";
interface ChatInputProps { interface ChatInputProps {
onSend: (message: string) => void; onSend: (message: string) => void;
disabled: boolean; disabled: boolean;
disabledChange: (disabled: boolean) => void; disabledChange: () => void;
changeMode: (isChatMode: boolean) => void; changeMode: (isChatMode: boolean) => void;
isChatMode: boolean; isChatMode: boolean;
inputValue: string; inputValue: string;
@@ -30,14 +23,14 @@ export default function ChatInput({
isChatMode, isChatMode,
inputValue, inputValue,
changeInput, changeInput,
disabledChange,
}: ChatInputProps) { }: ChatInputProps) {
const inputRef = useRef<HTMLInputElement>(null); const inputRef = useRef<HTMLInputElement>(null);
const { stopChat, setStopChat } = useChatStore(); const { curChatEnd } = useChatStore();
const handleSubmit = () => { const handleSubmit = () => {
if (inputValue.trim() && !disabled) { if (inputValue.trim() && !disabled) {
setStopChat(false);
onSend(inputValue.trim()); onSend(inputValue.trim());
} }
}; };
@@ -90,24 +83,28 @@ export default function ChatInput({
<Mic className="w-4 h-4 text-[#999] dark:text-[#999]" /> <Mic className="w-4 h-4 text-[#999] dark:text-[#999]" />
</button> </button>
) : null} ) : null}
{isChatMode && stopChat ? ( {isChatMode && curChatEnd ? (
<button <button
className={`ml-1 p-1 ${ className={`ml-1 p-1 ${
inputValue ? "bg-[#0072FF]" : "bg-[#E4E5F0]" inputValue ? "bg-[#0072FF]" : "bg-[#E4E5F0] dark:bg-[#545454]"
} rounded-full transition-colors`} } rounded-full transition-colors`}
type="submit" type="submit"
onClick={() => onSend(inputValue.trim())} onClick={() => onSend(inputValue.trim())}
> >
<Send className="w-4 h-4 text-white hover:text-[#999]" /> <Send className="w-4 h-4 text-white" />
</button> </button>
) : null} ) : null}
{isChatMode && !stopChat ? ( {isChatMode && !curChatEnd ? (
<button <button
className={`ml-1 p-1 bg-[#0072FF] rounded-full transition-colors`} className={`ml-1 px-1 bg-[#0072FF] rounded-full transition-colors`}
type="submit" type="submit"
onClick={() => setStopChat(true)} onClick={() => disabledChange()}
> >
<CircleStop className="w-4 h-4 text-white hover:text-[#999]" /> <StopIcon
size={16}
className="w-4 h-4 text-white"
aria-label="Stop message"
/>
</button> </button>
) : null} ) : null}
</div> </div>
@@ -147,7 +144,7 @@ export default function ChatInput({
<ChatSwitch <ChatSwitch
isChatMode={isChatMode} isChatMode={isChatMode}
onChange={(value) => { onChange={(value) => {
setStopChat(!value); value && disabledChange();
changeMode(value); changeMode(value);
}} }}
/> />

View File

@@ -35,7 +35,7 @@ function Search({ isTransitioned, isChatMode, input }: SearchProps) {
// } else { // } else {
// await getCurrentWebviewWindow().setSize(new LogicalSize(680, 90)); // await getCurrentWebviewWindow().setSize(new LogicalSize(680, 90));
// } // }
setSuggests(data); setSuggests([...data, ...data, ...data, ...data]);
setIsSearchComplete(true); setIsSearchComplete(true);
} catch (error) { } catch (error) {
console.error("Failed to fetch user data:", error); console.error("Failed to fetch user data:", error);
@@ -56,23 +56,20 @@ function Search({ isTransitioned, isChatMode, input }: SearchProps) {
!isChatMode && debouncedSearch(); !isChatMode && debouncedSearch();
}, [input]); }, [input]);
console.log(11111, isChatMode);
if (isChatMode || suggests.length === 0) return null; if (isChatMode || suggests.length === 0) return null;
return ( return (
<div <div
className={`rounded-xl overflow-hidden bg-search_bg_light dark:bg-search_bg_dark bg-cover border border-[#E6E6E6] dark:border-[#272626] absolute w-full transition-opacity duration-500 ${ className={`shadow-window-custom rounded-xl overflow-hidden bg-search_bg_light dark:bg-search_bg_dark bg-cover border border-[#E6E6E6] dark:border-[#272626] absolute w-full transition-opacity duration-500 ${
isTransitioned ? "opacity-0 pointer-events-none" : "opacity-100" isTransitioned ? "opacity-0 pointer-events-none" : "opacity-100"
} bottom-0 h-[500px]`} } top-[96px]`}
style={{ style={{
backgroundPosition: "-1px 0", backgroundPosition: "-1px 0",
backgroundSize: "101% 100%", backgroundSize: "101% 100%",
}} }}
> >
{isChatMode ? null : ( {isChatMode ? null : (
<div <div className={`max-h-[498px] pb-10 w-full relative`}>
className={`min-h-full w-full flex items-start justify-center overflow-hidden relative`}
>
{/* Search Results Panel */} {/* Search Results Panel */}
{suggests.length > 0 ? ( {suggests.length > 0 ? (
<DropdownList <DropdownList

View File

@@ -1,11 +1,45 @@
import { useEffect, useState, useRef } from "react"; import { useEffect, useState, useRef } from "react";
// import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow"; // import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow";
// import { LogicalSize } from "@tauri-apps/api/dpi"; // import { LogicalSize } from "@tauri-apps/api/dpi";
// import { Window, LogicalPosition } from "@tauri-apps/api/window";
// import { currentMonitor } from "@tauri-apps/plugin-window";
import InputBox from "./InputBox"; import InputBox from "./InputBox";
import Search from "./Search"; import Search from "./Search";
import ChatAI, { ChatAIRef } from "../ChatAI/Chat"; import ChatAI, { ChatAIRef } from "../ChatAI/Chat";
// const appWindow = new Window("main");
// async function preventOutOfBounds() {
// const monitor = await currentMonitor();
// if (monitor) {
// const screenBounds = {
// x: monitor.position.x,
// y: monitor.position.y,
// width: monitor.size.width,
// height: monitor.size.height,
// };
// const windowPosition = await appWindow.outerPosition();
// const windowSize = await appWindow.outerSize();
// let newX = windowPosition.x;
// let newY = windowPosition.y;
// if (newX < screenBounds.x) newX = screenBounds.x;
// if (newY < screenBounds.y) newY = screenBounds.y;
// if (newX + windowSize.width > screenBounds.x + screenBounds.width)
// newX = screenBounds.x + screenBounds.width - windowSize.width;
// if (newY + windowSize.height > screenBounds.y + screenBounds.height)
// newY = screenBounds.y + screenBounds.height - windowSize.height;
// if (newX !== windowPosition.x || newY !== windowPosition.y) {
// await appWindow.setPosition(new LogicalPosition(newX, newY));
// }
// }
// }
export default function SearchChat() { export default function SearchChat() {
const chatAIRef = useRef<ChatAIRef>(null); const chatAIRef = useRef<ChatAIRef>(null);
@@ -13,6 +47,16 @@ export default function SearchChat() {
const [input, setInput] = useState(""); const [input, setInput] = useState("");
const [isTransitioned, setIsTransitioned] = useState(false); const [isTransitioned, setIsTransitioned] = useState(false);
// useEffect(() => {
// const unlisten = appWindow.listen("tauri://move", () => {
// preventOutOfBounds();
// });
// return () => {
// unlisten.then((off: any) => off());
// };
// }, []);
async function setWindowSize() { async function setWindowSize() {
if (isTransitioned) { if (isTransitioned) {
// await getCurrentWebviewWindow()?.setSize(new LogicalSize(680, 600)); // await getCurrentWebviewWindow()?.setSize(new LogicalSize(680, 600));
@@ -42,9 +86,8 @@ export default function SearchChat() {
chatAIRef.current?.init(); chatAIRef.current?.init();
} }
}; };
const cancelChat = () => {}; const cancelChat = () => {
const setIsTyping = (value: any) => { chatAIRef.current?.cancelChat();
console.log(value);
}; };
const isTyping = false; const isTyping = false;
@@ -54,7 +97,7 @@ export default function SearchChat() {
className={`w-full h-full min-h-screen mx-auto overflow-hidden relative`} className={`w-full h-full min-h-screen mx-auto overflow-hidden relative`}
> >
<div <div
className={`rounded-xl overflow-hidden bg-inputbox_bg_light dark:bg-inputbox_bg_dark bg-cover border border-[#E6E6E6] dark:border-[#272626] absolute z-100 w-full flex items-center justify-center duration-500 ${ className={`shadow-window-custom rounded-xl overflow-hidden bg-inputbox_bg_light dark:bg-inputbox_bg_dark bg-cover border border-[#E6E6E6] dark:border-[#272626] absolute z-100 w-full flex items-center justify-center duration-500 ${
isTransitioned ? "top-[506px] h-[90px]" : "top-0 h-[90px]" isTransitioned ? "top-[506px] h-[90px]" : "top-0 h-[90px]"
}`} }`}
> >
@@ -63,9 +106,8 @@ export default function SearchChat() {
inputValue={input} inputValue={input}
onSend={handleSendMessage} onSend={handleSendMessage}
disabled={isTyping} disabled={isTyping}
disabledChange={(value) => { disabledChange={() => {
cancelChat(); cancelChat();
setIsTyping(value);
}} }}
changeMode={changeMode} changeMode={changeMode}
changeInput={changeInput} changeInput={changeInput}
@@ -73,7 +115,7 @@ export default function SearchChat() {
</div> </div>
<div <div
className={`rounded-xl overflow-hidden bg-chat_bg_light dark:bg-chat_bg_dark bg-cover border border-[#E6E6E6] dark:border-[#272626] absolute w-full transition-all duration-500 ${ className={`shadow-window-custom rounded-xl overflow-hidden bg-chat_bg_light dark:bg-chat_bg_dark bg-cover border border-[#E6E6E6] dark:border-[#272626] absolute w-full transition-all duration-500 ${
isTransitioned isTransitioned
? "top-0 opacity-100 pointer-events-auto" ? "top-0 opacity-100 pointer-events-auto"
: "-top-[506px] opacity-0 pointer-events-none" : "-top-[506px] opacity-0 pointer-events-none"

View File

@@ -24,16 +24,16 @@ export default function SVGWrap({
title={title} title={title}
onClick={handleClick} onClick={handleClick}
className={clsx( className={clsx(
"inline-flex items-center justify-center rounded-sm p-[2px] text-slate-500 dark:text-slate-500 transition-all", "inline-flex items-center justify-center rounded-sm p-[2px] transition-all",
{ {
"cursor-pointer hover:bg-slate-300/50 hover:dark:bg-white/10": action, "cursor-pointer": action,
}, },
className className
)} )}
> >
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
style={{ widows: "100%", height: "100%" }} style={{ width: "100%", height: "100%" }}
{...props} {...props}
> >
{children} {children}

28
src/icons/Stop.tsx Normal file
View File

@@ -0,0 +1,28 @@
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>
</SVGWrap>
);
}

View File

@@ -11,12 +11,12 @@ export default {
separator: "rgb(var(--color-separator) / <alpha-value>)", separator: "rgb(var(--color-separator) / <alpha-value>)",
}, },
backgroundImage: { backgroundImage: {
chat_bg_light: "url('/public/chat_bg_light.png')", chat_bg_light: "url('./assets/chat_bg_light.png')",
chat_bg_dark: "url('/public/chat_bg_dark.png')", chat_bg_dark: "url('./assets/chat_bg_dark.png')",
search_bg_light: "url('/public/search_bg_light.png')", search_bg_light: "url('./assets/search_bg_light.png')",
search_bg_dark: "url('/public/search_bg_dark.png')", search_bg_dark: "url('./assets/search_bg_dark.png')",
inputbox_bg_light: "url('/public/inputbox_bg_light.png')", inputbox_bg_light: "url('./assets/inputbox_bg_light.png')",
inputbox_bg_dark: "url('/public/inputbox_bg_dark.png')", inputbox_bg_dark: "url('./assets/inputbox_bg_dark.png')",
}, },
textColor: { textColor: {
primary: "rgb(var(--color-foreground) / <alpha-value>)", primary: "rgb(var(--color-foreground) / <alpha-value>)",
@@ -31,8 +31,7 @@ export default {
}, },
}, },
boxShadow: { boxShadow: {
"window-custom": "window-custom": "0px 16px 32px 0px rgba(0,0,0,0.3)",
"0 1px 5px 0 rgba(0, 0, 0, 0.15), 0 1px 5px -1px rgba(0, 0, 0, 0.1), 0 2px 5px rgba(0, 0, 0, 0.1)",
}, },
}, },
}, },