chore: stop chat & ui style (#25)
@@ -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",
|
||||
|
||||
16
pnpm-lock.yaml
generated
@@ -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
|
||||
|
||||
|
Before Width: | Height: | Size: 244 KiB After Width: | Height: | Size: 244 KiB |
|
Before Width: | Height: | Size: 130 KiB After Width: | Height: | Size: 130 KiB |
|
Before Width: | Height: | Size: 71 KiB After Width: | Height: | Size: 71 KiB |
|
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 40 KiB |
|
Before Width: | Height: | Size: 192 KiB After Width: | Height: | Size: 192 KiB |
|
Before Width: | Height: | Size: 120 KiB After Width: | Height: | Size: 120 KiB |
BIN
src/assets/chat_bg_dark.png
Normal file
|
After Width: | Height: | Size: 244 KiB |
BIN
src/assets/chat_bg_light.png
Normal file
|
After Width: | Height: | Size: 130 KiB |
BIN
src/assets/inputbox_bg_dark.png
Normal file
|
After Width: | Height: | Size: 71 KiB |
BIN
src/assets/inputbox_bg_light.png
Normal file
|
After Width: | Height: | Size: 40 KiB |
BIN
src/assets/search_bg_dark.png
Normal file
|
After Width: | Height: | Size: 192 KiB |
BIN
src/assets/search_bg_light.png
Normal file
|
After Width: | Height: | Size: 120 KiB |
@@ -22,15 +22,17 @@ interface ChatAIProps {
|
||||
|
||||
export interface ChatAIRef {
|
||||
init: () => void;
|
||||
cancelChat: () => void;
|
||||
}
|
||||
|
||||
const ChatAI = forwardRef<ChatAIRef, ChatAIProps>(
|
||||
({ inputValue, isTransitioned, changeInput }, ref) => {
|
||||
useImperativeHandle(ref, () => ({
|
||||
init: init,
|
||||
cancelChat: cancelChat,
|
||||
}));
|
||||
|
||||
const { curChatEnd, setCurChatEnd, stopChat } = useChatStore();
|
||||
const { curChatEnd, setCurChatEnd } = useChatStore();
|
||||
|
||||
const [activeChat, setActiveChat] = useState<Chat>();
|
||||
const [isTyping, setIsTyping] = useState(false);
|
||||
@@ -39,6 +41,13 @@ const ChatAI = forwardRef<ChatAIRef, ChatAIProps>(
|
||||
|
||||
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<ChatAIRef, 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 "";
|
||||
}
|
||||
);
|
||||
|
||||
// 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<ChatAIRef, ChatAIProps>(
|
||||
if (curChatEnd) {
|
||||
simulateAssistantResponse();
|
||||
}
|
||||
}, [messages, curChatEnd, stopChat]);
|
||||
}, [messages, curChatEnd]);
|
||||
|
||||
const scrollToBottom = () => {
|
||||
messagesEndRef.current?.scrollIntoView({
|
||||
@@ -125,6 +136,7 @@ const ChatAI = forwardRef<ChatAIRef, ChatAIProps>(
|
||||
};
|
||||
|
||||
const init = () => {
|
||||
if (!curChatEnd) return;
|
||||
if (!activeChat?._id) {
|
||||
createNewChat();
|
||||
} else {
|
||||
@@ -145,6 +157,7 @@ const ChatAI = forwardRef<ChatAIRef, ChatAIProps>(
|
||||
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<ChatAIRef, ChatAIProps>(
|
||||
};
|
||||
|
||||
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",
|
||||
|
||||
@@ -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<HTMLTextAreaElement>(null);
|
||||
@@ -58,7 +59,7 @@ export function ChatInput({
|
||||
|
||||
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">
|
||||
<div className="bg-inputbox_bg_light dark:bg-inputbox_bg_dark bg-cover rounded-xl border border-[#E6E6E6] dark:border-[#272626]">
|
||||
{/* Search Bar */}
|
||||
<div className="relative">
|
||||
<div className="p-[13px] flex items-center bg-white dark:bg-[#202126] rounded-xl transition-all">
|
||||
@@ -72,14 +73,29 @@ export function ChatInput({
|
||||
<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
|
||||
className={`ml-1 p-1 ${
|
||||
input ? "bg-[#0072FF]" : "bg-[#E4E5F0]"
|
||||
} rounded-full transition-colors`}
|
||||
onClick={(e) => handleSubmit(e as unknown as FormEvent)}
|
||||
>
|
||||
<Send className="w-4 h-4 text-white hover:text-[#999]" />
|
||||
</button>
|
||||
{curChatEnd ? (
|
||||
<button
|
||||
className={`ml-1 p-1 ${
|
||||
input ? "bg-[#0072FF]" : "bg-[#E4E5F0]"
|
||||
} rounded-full transition-colors`}
|
||||
onClick={(e) => handleSubmit(e as unknown as FormEvent)}
|
||||
>
|
||||
<Send className="w-4 h-4 text-white hover:text-[#999]" />
|
||||
</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>
|
||||
|
||||
@@ -98,15 +114,6 @@ export function ChatInput({
|
||||
Upload
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Switch */}
|
||||
<ChatSwitch
|
||||
isChatMode={true}
|
||||
onChange={(value) => {
|
||||
changeMode(value);
|
||||
setInput("");
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -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 (
|
||||
<div className={`h-full flex flex-col ${className}`}>
|
||||
<div className="p-4">
|
||||
<button
|
||||
onClick={onNewChat}
|
||||
className={`w-full flex items-center gap-3 px-4 py-3 text-sm rounded-lg transition-all ${
|
||||
isDark
|
||||
? " text-white hover:bg-gray-600 active:bg-gray-500"
|
||||
: " text-gray-700 hover:bg-gray-50 active:bg-gray-100 shadow-sm"
|
||||
}`}
|
||||
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`}
|
||||
>
|
||||
<Plus
|
||||
className={`h-4 w-4 ${
|
||||
isDark ? "text-indigo-400" : "text-indigo-600"
|
||||
}`}
|
||||
/>
|
||||
<Plus className={`h-4 w-4 text-indigo-600 dark:text-indigo-400`} />
|
||||
New Chat
|
||||
</button>
|
||||
</div>
|
||||
@@ -44,30 +31,14 @@ export function Sidebar({
|
||||
{chats.map((chat) => (
|
||||
<div
|
||||
key={chat._id}
|
||||
className={`group relative rounded-lg transition-all ${
|
||||
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"
|
||||
}`}
|
||||
className={`group relative rounded-lg transition-all hover:border border-[#E6E6E6] dark:border-[#272626] text-gray-900 shadow-sm dark:text-white`}
|
||||
>
|
||||
<button
|
||||
className="w-full flex items-center gap-3 px-4 py-3 text-sm text-left"
|
||||
onClick={() => onSelectChat(chat)}
|
||||
>
|
||||
<MessageSquare
|
||||
className={`h-4 w-4 flex-shrink-0 ${
|
||||
activeChat._id === chat._id
|
||||
? isDark
|
||||
? "text-indigo-400"
|
||||
: "text-indigo-600"
|
||||
: isDark
|
||||
? "text-gray-400"
|
||||
: "text-gray-500"
|
||||
}`}
|
||||
className={`h-4 w-4 flex-shrink-0 text-indigo-600 dark:text-indigo-400`}
|
||||
/>
|
||||
<span className="truncate">{chat.title || chat._id}</span>
|
||||
</button>
|
||||
@@ -85,9 +56,7 @@ export function Sidebar({
|
||||
)} */}
|
||||
{activeChat._id === chat._id && (
|
||||
<div
|
||||
className={`absolute left-0 top-1/2 -translate-y-1/2 w-1 h-6 rounded-full ${
|
||||
isDark ? "bg-indigo-400" : "bg-indigo-600"
|
||||
}`}
|
||||
className={`absolute left-0 top-1/2 -translate-y-1/2 w-1 h-6 rounded-full bg-indigo-600 dark:bg-indigo-400`}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -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 (
|
||||
<div className="h-screen bg-chat_bg_light dark:bg-chat_bg_dark bg-cover">
|
||||
<div className="h-screen">
|
||||
<div className="h-[100%] flex">
|
||||
{/* Sidebar */}
|
||||
{isSidebarOpen ? (
|
||||
<div
|
||||
className={`fixed inset-y-0 left-0 z-50 w-64 transform ${
|
||||
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 ? (
|
||||
<Sidebar
|
||||
@@ -251,7 +262,7 @@ export default function ChatAI({}: ChatAIProps) {
|
||||
) : null}
|
||||
|
||||
{/* Main content */}
|
||||
<div className={`flex-1 flex flex-col`}>
|
||||
<div className={`flex-1 flex flex-col bg-white dark:bg-gray-900`}>
|
||||
<header
|
||||
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}
|
||||
/>
|
||||
) : 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>
|
||||
|
||||
@@ -310,11 +314,12 @@ export default function ChatAI({}: ChatAIProps) {
|
||||
<ChatInput
|
||||
onSend={handleSendMessage}
|
||||
disabled={isTyping}
|
||||
disabledChange={(value) => {
|
||||
curChatEnd={curChatEnd}
|
||||
disabledChange={() => {
|
||||
cancelChat();
|
||||
setIsTyping(value);
|
||||
setCurChatEnd(true);
|
||||
setIsTyping(false);
|
||||
}}
|
||||
changeMode={() => {}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -11,7 +11,7 @@ function DropdownList({ selected, suggests }: DropdownListProps) {
|
||||
const [selectedItem, setSelectedItem] = useState<number | null>(null);
|
||||
const [showIndex, setShowIndex] = useState<boolean>(false);
|
||||
const containerRef = useRef<HTMLDivElement>(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) {
|
||||
<div
|
||||
ref={containerRef}
|
||||
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}
|
||||
>
|
||||
<div className="p-2 text-xs text-[#999] dark:text-[#666]">
|
||||
Results
|
||||
</div>
|
||||
{suggests?.map((item, index) => {
|
||||
const isSelected = selectedItem === index;
|
||||
return (
|
||||
<button
|
||||
<div
|
||||
key={item._id}
|
||||
ref={(el) => (itemRefs.current[index] = el)}
|
||||
onMouseEnter={() => setSelectedItem(index)}
|
||||
@@ -120,7 +123,7 @@ function DropdownList({ selected, suggests }: DropdownListProps) {
|
||||
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
|
||||
? "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)]"
|
||||
@@ -144,7 +147,7 @@ function DropdownList({ selected, suggests }: DropdownListProps) {
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
@@ -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<HTMLInputElement>(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({
|
||||
<Mic className="w-4 h-4 text-[#999] dark:text-[#999]" />
|
||||
</button>
|
||||
) : null}
|
||||
{isChatMode && stopChat ? (
|
||||
{isChatMode && curChatEnd ? (
|
||||
<button
|
||||
className={`ml-1 p-1 ${
|
||||
inputValue ? "bg-[#0072FF]" : "bg-[#E4E5F0]"
|
||||
inputValue ? "bg-[#0072FF]" : "bg-[#E4E5F0] dark:bg-[#545454]"
|
||||
} rounded-full transition-colors`}
|
||||
type="submit"
|
||||
onClick={() => onSend(inputValue.trim())}
|
||||
>
|
||||
<Send className="w-4 h-4 text-white hover:text-[#999]" />
|
||||
<Send className="w-4 h-4 text-white" />
|
||||
</button>
|
||||
) : null}
|
||||
{isChatMode && !stopChat ? (
|
||||
{isChatMode && !curChatEnd ? (
|
||||
<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"
|
||||
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>
|
||||
) : null}
|
||||
</div>
|
||||
@@ -147,7 +144,7 @@ export default function ChatInput({
|
||||
<ChatSwitch
|
||||
isChatMode={isChatMode}
|
||||
onChange={(value) => {
|
||||
setStopChat(!value);
|
||||
value && disabledChange();
|
||||
changeMode(value);
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -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 (
|
||||
<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"
|
||||
} bottom-0 h-[500px]`}
|
||||
} top-[96px]`}
|
||||
style={{
|
||||
backgroundPosition: "-1px 0",
|
||||
backgroundSize: "101% 100%",
|
||||
}}
|
||||
>
|
||||
{isChatMode ? null : (
|
||||
<div
|
||||
className={`min-h-full w-full flex items-start justify-center overflow-hidden relative`}
|
||||
>
|
||||
<div className={`max-h-[498px] pb-10 w-full relative`}>
|
||||
{/* Search Results Panel */}
|
||||
{suggests.length > 0 ? (
|
||||
<DropdownList
|
||||
|
||||
@@ -1,11 +1,45 @@
|
||||
import { useEffect, useState, useRef } from "react";
|
||||
// import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow";
|
||||
// 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 Search from "./Search";
|
||||
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() {
|
||||
const chatAIRef = useRef<ChatAIRef>(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`}
|
||||
>
|
||||
<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]"
|
||||
}`}
|
||||
>
|
||||
@@ -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() {
|
||||
</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
|
||||
? "top-0 opacity-100 pointer-events-auto"
|
||||
: "-top-[506px] opacity-0 pointer-events-none"
|
||||
|
||||
@@ -24,16 +24,16 @@ export default function SVGWrap({
|
||||
title={title}
|
||||
onClick={handleClick}
|
||||
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
|
||||
)}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
style={{ widows: "100%", height: "100%" }}
|
||||
style={{ width: "100%", height: "100%" }}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
|
||||
28
src/icons/Stop.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -11,12 +11,12 @@ export default {
|
||||
separator: "rgb(var(--color-separator) / <alpha-value>)",
|
||||
},
|
||||
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) / <alpha-value>)",
|
||||
@@ -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)",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||