diff --git a/package.json b/package.json index 311cc1fb..99b015f7 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "@tauri-apps/plugin-http": "~2.0.1", "@tauri-apps/plugin-shell": ">=2.0.0", "@tauri-apps/plugin-websocket": "~2", + "@tauri-apps/plugin-window": "2.0.0-alpha.1", "@traptitech/markdown-it-katex": "^3.6.0", "axios": "^1.7.7", "clsx": "^2.1.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f13fd565..d9e2d15d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -23,6 +23,9 @@ importers: '@tauri-apps/plugin-websocket': specifier: ~2 version: 2.0.0 + '@tauri-apps/plugin-window': + specifier: 2.0.0-alpha.1 + version: 2.0.0-alpha.1 '@traptitech/markdown-it-katex': specifier: ^3.6.0 version: 3.6.0 @@ -607,6 +610,10 @@ packages: '@tanstack/virtual-core@3.10.8': 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': resolution: {integrity: sha512-3wSwmG+1kr6WrgAFKK5ijkNFPp8TT3FLj3YHUb5EwMO+3FxX4uWlfSWkeeBy+Kc1RsKzugtYLuuya+98Flj+3w==} @@ -688,6 +695,9 @@ packages: '@tauri-apps/plugin-websocket@2.0.0': 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': resolution: {integrity: sha512-CnJzTWxsgLGXFdSrWRaGz7GZ1kUUi8g3E9HzJmeveX1YwVJavrKYqysktfHZQsujdnRqV5O7g8FPKEA/aeTkOQ==} @@ -2667,6 +2677,8 @@ snapshots: '@tanstack/virtual-core@3.10.8': {} + '@tauri-apps/api@2.0.0-alpha.6': {} + '@tauri-apps/api@2.0.2': {} '@tauri-apps/cli-darwin-arm64@2.0.3': @@ -2724,6 +2736,10 @@ snapshots: dependencies: '@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': dependencies: katex: 0.16.11 diff --git a/public/chat_bg_dark.png b/public/assets/chat_bg_dark.png similarity index 100% rename from public/chat_bg_dark.png rename to public/assets/chat_bg_dark.png diff --git a/public/chat_bg_light.png b/public/assets/chat_bg_light.png similarity index 100% rename from public/chat_bg_light.png rename to public/assets/chat_bg_light.png diff --git a/public/inputbox_bg_dark.png b/public/assets/inputbox_bg_dark.png similarity index 100% rename from public/inputbox_bg_dark.png rename to public/assets/inputbox_bg_dark.png diff --git a/public/inputbox_bg_light.png b/public/assets/inputbox_bg_light.png similarity index 100% rename from public/inputbox_bg_light.png rename to public/assets/inputbox_bg_light.png diff --git a/public/search_bg_dark.png b/public/assets/search_bg_dark.png similarity index 100% rename from public/search_bg_dark.png rename to public/assets/search_bg_dark.png diff --git a/public/search_bg_light.png b/public/assets/search_bg_light.png similarity index 100% rename from public/search_bg_light.png rename to public/assets/search_bg_light.png diff --git a/src/assets/chat_bg_dark.png b/src/assets/chat_bg_dark.png new file mode 100644 index 00000000..13d889b2 Binary files /dev/null and b/src/assets/chat_bg_dark.png differ diff --git a/src/assets/chat_bg_light.png b/src/assets/chat_bg_light.png new file mode 100644 index 00000000..ada740f3 Binary files /dev/null and b/src/assets/chat_bg_light.png differ diff --git a/src/assets/inputbox_bg_dark.png b/src/assets/inputbox_bg_dark.png new file mode 100644 index 00000000..94261c84 Binary files /dev/null and b/src/assets/inputbox_bg_dark.png differ diff --git a/src/assets/inputbox_bg_light.png b/src/assets/inputbox_bg_light.png new file mode 100644 index 00000000..b9ba9790 Binary files /dev/null and b/src/assets/inputbox_bg_light.png differ diff --git a/src/assets/search_bg_dark.png b/src/assets/search_bg_dark.png new file mode 100644 index 00000000..9a972896 Binary files /dev/null and b/src/assets/search_bg_dark.png differ diff --git a/src/assets/search_bg_light.png b/src/assets/search_bg_light.png new file mode 100644 index 00000000..940986c7 Binary files /dev/null and b/src/assets/search_bg_light.png differ diff --git a/src/components/ChatAI/Chat.tsx b/src/components/ChatAI/Chat.tsx index cf52d5ba..6ff2168d 100644 --- a/src/components/ChatAI/Chat.tsx +++ b/src/components/ChatAI/Chat.tsx @@ -22,15 +22,17 @@ interface ChatAIProps { export interface ChatAIRef { init: () => void; + cancelChat: () => void; } const ChatAI = forwardRef( ({ inputValue, isTransitioned, changeInput }, ref) => { useImperativeHandle(ref, () => ({ init: init, + cancelChat: cancelChat, })); - const { curChatEnd, setCurChatEnd, stopChat } = useChatStore(); + const { curChatEnd, setCurChatEnd } = useChatStore(); const [activeChat, setActiveChat] = useState(); const [isTyping, setIsTyping] = useState(false); @@ -39,6 +41,13 @@ const ChatAI = forwardRef( const [websocketId, setWebsocketId] = 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( "ws://localhost:2900/ws", (msg) => { @@ -48,28 +57,30 @@ const ChatAI = forwardRef( } if (msg.includes("PRIVATE")) { - if (msg.includes("assistant finished output")) { + if ( + msg.includes("assistant finished output") || + curChatEndRef.current + ) { setCurChatEnd(true); } else { const cleanedData = msg.replace(/^PRIVATE /, ""); try { const chunkData = JSON.parse(cleanedData); - setCurMessage((prev) => prev + chunkData.message_chunk); - return chunkData.message_chunk; + 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); } - return ""; } } - - return ""; } ); // websocket useEffect(() => { - if (messages.length === 0 || !activeChat?._id || stopChat) return; + if (messages.length === 0 || !activeChat?._id) return; const simulateAssistantResponse = () => { console.log("messages", messages); @@ -94,7 +105,7 @@ const ChatAI = forwardRef( if (curChatEnd) { simulateAssistantResponse(); } - }, [messages, curChatEnd, stopChat]); + }, [messages, curChatEnd]); const scrollToBottom = () => { messagesEndRef.current?.scrollIntoView({ @@ -125,6 +136,7 @@ const ChatAI = forwardRef( }; const init = () => { + if (!curChatEnd) return; if (!activeChat?._id) { createNewChat(); } else { @@ -145,6 +157,7 @@ const ChatAI = forwardRef( body: JSON.stringify({ message: content }), }); console.log("_send", response, websocketId); + setCurId(response.data[0]?._id); const updatedChat: Chat = { ...newChat, messages: [...(newChat?.messages || []), ...(response.data || [])], @@ -172,24 +185,21 @@ const ChatAI = forwardRef( }; 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); } }; - useEffect(() => { - if (curChatEnd) { - cancelChat(); - } - }, [curChatEnd]); - async function openChatAI() { createWin({ label: "chat", diff --git a/src/components/ChatAI/ChatInput.tsx b/src/components/ChatAI/ChatInput.tsx index f166fbd5..9b81fc8c 100644 --- a/src/components/ChatAI/ChatInput.tsx +++ b/src/components/ChatAI/ChatInput.tsx @@ -6,20 +6,21 @@ import { useRef, useEffect, } from "react"; -import ChatSwitch from "../SearchChat/ChatSwitch"; import AutoResizeTextarea from "./AutoResizeTextarea"; +import StopIcon from "../../icons/Stop"; interface ChatInputProps { onSend: (message: string) => void; disabled: boolean; - disabledChange: (disabled: boolean) => void; - changeMode: (isChatMode: boolean) => void; + curChatEnd: boolean; + disabledChange: () => void; } export function ChatInput({ onSend, disabled, - changeMode, + curChatEnd, + disabledChange, }: ChatInputProps) { const [input, setInput] = useState(""); const textareaRef = useRef(null); @@ -58,7 +59,7 @@ export function ChatInput({ return (
-
+
{/* Search Bar */}
@@ -72,14 +73,29 @@ export function ChatInput({ - + {curChatEnd ? ( + + ) : null} + {!curChatEnd ? ( + + ) : null}
@@ -98,15 +114,6 @@ export function ChatInput({ Upload
- - {/* Switch */} - { - changeMode(value); - setInput(""); - }} - />
diff --git a/src/components/ChatAI/Sidebar.tsx b/src/components/ChatAI/Sidebar.tsx index bd035b35..3314e695 100644 --- a/src/components/ChatAI/Sidebar.tsx +++ b/src/components/ChatAI/Sidebar.tsx @@ -1,7 +1,5 @@ import { MessageSquare, Plus } from "lucide-react"; import type { Chat } from "./types"; -import { useTheme } from "../ThemeProvider"; - interface SidebarProps { chats: Chat[]; activeChat: Chat; @@ -18,25 +16,14 @@ export function Sidebar({ onSelectChat, className = "", }: SidebarProps) { - const { theme } = useTheme(); - const isDark = theme === "dark"; - return (
@@ -44,30 +31,14 @@ export function Sidebar({ {chats.map((chat) => (
@@ -85,9 +56,7 @@ export function Sidebar({ )} */} {activeChat._id === chat._id && (
)}
diff --git a/src/components/ChatAI/index.tsx b/src/components/ChatAI/index.tsx index ab18b2f1..eb679cae 100644 --- a/src/components/ChatAI/index.tsx +++ b/src/components/ChatAI/index.tsx @@ -22,6 +22,14 @@ export default function ChatAI({}: ChatAIProps) { const [websocketId, setWebsocketId] = useState(""); const [curMessage, setCurMessage] = useState(""); 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( "ws://localhost:2900/ws", (msg) => { @@ -31,22 +39,24 @@ export default function ChatAI({}: ChatAIProps) { } if (msg.includes("PRIVATE")) { - if (msg.includes("assistant finished output")) { + if ( + msg.includes("assistant finished output") || + curChatEndRef.current + ) { setCurChatEnd(true); } else { const cleanedData = msg.replace(/^PRIVATE /, ""); try { const chunkData = JSON.parse(cleanedData); - setCurMessage((prev) => prev + chunkData.message_chunk); - return chunkData.message_chunk; + 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); } - return ""; } } - - return ""; } ); @@ -153,6 +163,7 @@ export default function ChatAI({}: ChatAIProps) { body: JSON.stringify({ message: content }), }); console.log("_send", response, websocketId); + setCurId(response.data[0]?._id); const updatedChat: Chat = { ...activeChat, messages: [...(activeChat?.messages || []), ...(response.data || [])], @@ -229,14 +240,14 @@ export default function ChatAI({}: ChatAIProps) { } return ( -
+
{/* Sidebar */} {isSidebarOpen ? (
{activeChat ? ( +
@@ -295,13 +306,6 @@ export default function ChatAI({}: ChatAIProps) { isTyping={!curChatEnd} /> ) : null} - {isTyping && ( -
-
-
-
-
- )}
@@ -310,11 +314,12 @@ export default function ChatAI({}: ChatAIProps) { { + curChatEnd={curChatEnd} + disabledChange={() => { cancelChat(); - setIsTyping(value); + setCurChatEnd(true); + setIsTyping(false); }} - changeMode={() => {}} />
diff --git a/src/components/SearchChat/DropdownList.tsx b/src/components/SearchChat/DropdownList.tsx index 6085b33d..63882cdf 100644 --- a/src/components/SearchChat/DropdownList.tsx +++ b/src/components/SearchChat/DropdownList.tsx @@ -11,7 +11,7 @@ function DropdownList({ selected, suggests }: DropdownListProps) { const [selectedItem, setSelectedItem] = useState(null); const [showIndex, setShowIndex] = useState(false); const containerRef = useRef(null); - const itemRefs = useRef<(HTMLButtonElement | null)[]>([]); + const itemRefs = useRef<(HTMLDivElement | null)[]>([]); const handleOpenURL = async (url: string) => { if (!url) return; @@ -103,13 +103,16 @@ function DropdownList({ selected, suggests }: DropdownListProps) {
+
+ Results +
{suggests?.map((item, index) => { const isSelected = selectedItem === index; return ( -
) : null}
- +
); })}
diff --git a/src/components/SearchChat/InputBox.tsx b/src/components/SearchChat/InputBox.tsx index d833564f..aa7e036a 100644 --- a/src/components/SearchChat/InputBox.tsx +++ b/src/components/SearchChat/InputBox.tsx @@ -1,22 +1,15 @@ -import { - Library, - Mic, - Send, - Plus, - AudioLines, - Image, - CircleStop, -} from "lucide-react"; +import { Library, Mic, Send, Plus, AudioLines, Image } from "lucide-react"; import { useRef, type KeyboardEvent } from "react"; import ChatSwitch from "../SearchChat/ChatSwitch"; import AutoResizeTextarea from "./AutoResizeTextarea"; import { useChatStore } from "../../stores/chatStore"; +import StopIcon from "../../icons/Stop"; interface ChatInputProps { onSend: (message: string) => void; disabled: boolean; - disabledChange: (disabled: boolean) => void; + disabledChange: () => void; changeMode: (isChatMode: boolean) => void; isChatMode: boolean; inputValue: string; @@ -30,14 +23,14 @@ export default function ChatInput({ isChatMode, inputValue, changeInput, + disabledChange, }: ChatInputProps) { const inputRef = useRef(null); - const { stopChat, setStopChat } = useChatStore(); + const { curChatEnd } = useChatStore(); const handleSubmit = () => { if (inputValue.trim() && !disabled) { - setStopChat(false); onSend(inputValue.trim()); } }; @@ -90,24 +83,28 @@ export default function ChatInput({ ) : null} - {isChatMode && stopChat ? ( + {isChatMode && curChatEnd ? ( ) : null} - {isChatMode && !stopChat ? ( + {isChatMode && !curChatEnd ? ( ) : null}
@@ -147,7 +144,7 @@ export default function ChatInput({ { - setStopChat(!value); + value && disabledChange(); changeMode(value); }} /> diff --git a/src/components/SearchChat/Search.tsx b/src/components/SearchChat/Search.tsx index 3bb28dca..3e1f6081 100644 --- a/src/components/SearchChat/Search.tsx +++ b/src/components/SearchChat/Search.tsx @@ -35,7 +35,7 @@ function Search({ isTransitioned, isChatMode, input }: SearchProps) { // } else { // await getCurrentWebviewWindow().setSize(new LogicalSize(680, 90)); // } - setSuggests(data); + setSuggests([...data, ...data, ...data, ...data]); setIsSearchComplete(true); } catch (error) { console.error("Failed to fetch user data:", error); @@ -56,23 +56,20 @@ function Search({ isTransitioned, isChatMode, input }: SearchProps) { !isChatMode && debouncedSearch(); }, [input]); - console.log(11111, isChatMode); if (isChatMode || suggests.length === 0) return null; return (
{isChatMode ? null : ( -
+
{/* Search Results Panel */} {suggests.length > 0 ? ( 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() { const chatAIRef = useRef(null); @@ -13,6 +47,16 @@ export default function SearchChat() { const [input, setInput] = useState(""); const [isTransitioned, setIsTransitioned] = useState(false); + // useEffect(() => { + // const unlisten = appWindow.listen("tauri://move", () => { + // preventOutOfBounds(); + // }); + + // return () => { + // unlisten.then((off: any) => off()); + // }; + // }, []); + async function setWindowSize() { if (isTransitioned) { // await getCurrentWebviewWindow()?.setSize(new LogicalSize(680, 600)); @@ -42,9 +86,8 @@ export default function SearchChat() { chatAIRef.current?.init(); } }; - const cancelChat = () => {}; - const setIsTyping = (value: any) => { - console.log(value); + const cancelChat = () => { + chatAIRef.current?.cancelChat(); }; const isTyping = false; @@ -54,7 +97,7 @@ export default function SearchChat() { className={`w-full h-full min-h-screen mx-auto overflow-hidden relative`} >
@@ -63,9 +106,8 @@ export default function SearchChat() { inputValue={input} onSend={handleSendMessage} disabled={isTyping} - disabledChange={(value) => { + disabledChange={() => { cancelChat(); - setIsTyping(value); }} changeMode={changeMode} changeInput={changeInput} @@ -73,7 +115,7 @@ export default function SearchChat() {
{children} diff --git a/src/icons/Stop.tsx b/src/icons/Stop.tsx new file mode 100644 index 00000000..52b38e35 --- /dev/null +++ b/src/icons/Stop.tsx @@ -0,0 +1,28 @@ +import SVGWrap from "./SVGWrap"; + +export default function Stop(props: I.SVG) { + return ( + + + + + + + + ); +} diff --git a/tailwind.config.js b/tailwind.config.js index c2cbb6c8..2b0b9f6b 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -11,12 +11,12 @@ export default { separator: "rgb(var(--color-separator) / )", }, backgroundImage: { - chat_bg_light: "url('/public/chat_bg_light.png')", - chat_bg_dark: "url('/public/chat_bg_dark.png')", - search_bg_light: "url('/public/search_bg_light.png')", - search_bg_dark: "url('/public/search_bg_dark.png')", - inputbox_bg_light: "url('/public/inputbox_bg_light.png')", - inputbox_bg_dark: "url('/public/inputbox_bg_dark.png')", + chat_bg_light: "url('./assets/chat_bg_light.png')", + chat_bg_dark: "url('./assets/chat_bg_dark.png')", + search_bg_light: "url('./assets/search_bg_light.png')", + search_bg_dark: "url('./assets/search_bg_dark.png')", + inputbox_bg_light: "url('./assets/inputbox_bg_light.png')", + inputbox_bg_dark: "url('./assets/inputbox_bg_dark.png')", }, textColor: { primary: "rgb(var(--color-foreground) / )", @@ -31,8 +31,7 @@ export default { }, }, boxShadow: { - "window-custom": - "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)", + "window-custom": "0px 16px 32px 0px rgba(0,0,0,0.3)", }, }, },