chore: UI/UE adjustment
* chore: UI/UE adjustment * chore: UI/UE adjustment
@@ -42,4 +42,6 @@ To start desktop development, run:
|
||||
|
||||
```
|
||||
pnpm tauri dev
|
||||
|
||||
pnpm tauri build --bundles app
|
||||
```
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<!doctype html>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<link rel="icon" type="image/x-icon" href="/icon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Coco</title>
|
||||
</head>
|
||||
|
||||
@@ -18,7 +18,6 @@
|
||||
"@traptitech/markdown-it-katex": "^3.6.0",
|
||||
"axios": "^1.7.7",
|
||||
"clsx": "^2.1.1",
|
||||
"framer-motion": "^11.11.11",
|
||||
"highlight.js": "^11.10.0",
|
||||
"i18next": "^23.16.2",
|
||||
"lodash": "^4.17.21",
|
||||
|
||||
24
pnpm-lock.yaml
generated
@@ -32,9 +32,6 @@ importers:
|
||||
clsx:
|
||||
specifier: ^2.1.1
|
||||
version: 2.1.1
|
||||
framer-motion:
|
||||
specifier: ^11.11.11
|
||||
version: 11.11.11(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
highlight.js:
|
||||
specifier: ^11.10.0
|
||||
version: 11.10.0
|
||||
@@ -1310,20 +1307,6 @@ packages:
|
||||
fraction.js@4.3.7:
|
||||
resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==}
|
||||
|
||||
framer-motion@11.11.11:
|
||||
resolution: {integrity: sha512-tuDH23ptJAKUHGydJQII9PhABNJBpB+z0P1bmgKK9QFIssHGlfPd6kxMq00LSKwE27WFsb2z0ovY0bpUyMvfRw==}
|
||||
peerDependencies:
|
||||
'@emotion/is-prop-valid': '*'
|
||||
react: ^18.0.0
|
||||
react-dom: ^18.0.0
|
||||
peerDependenciesMeta:
|
||||
'@emotion/is-prop-valid':
|
||||
optional: true
|
||||
react:
|
||||
optional: true
|
||||
react-dom:
|
||||
optional: true
|
||||
|
||||
fsevents@2.3.3:
|
||||
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
|
||||
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
|
||||
@@ -3420,13 +3403,6 @@ snapshots:
|
||||
|
||||
fraction.js@4.3.7: {}
|
||||
|
||||
framer-motion@11.11.11(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
|
||||
dependencies:
|
||||
tslib: 2.8.0
|
||||
optionalDependencies:
|
||||
react: 18.3.1
|
||||
react-dom: 18.3.1(react@18.3.1)
|
||||
|
||||
fsevents@2.3.3:
|
||||
optional: true
|
||||
|
||||
|
||||
BIN
public/icon.ico
Normal file
|
After Width: | Height: | Size: 134 KiB |
30
src-tauri/Entitlements.plist
Normal file
@@ -0,0 +1,30 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.security.cs.allow-jit</key>
|
||||
<true/>
|
||||
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
|
||||
<true/>
|
||||
<key>com.apple.security.cs.disable-library-validation</key>
|
||||
<true/>
|
||||
<key>com.apple.security.cs.allow-dyld-environment-variables</key>
|
||||
<true/>
|
||||
<key>com.apple.security.automation.apple-events</key>
|
||||
<true/>
|
||||
<key>com.apple.security.device.usb</key>
|
||||
<true/>
|
||||
<key>com.apple.security.device.camera</key>
|
||||
<true/>
|
||||
<key>com.apple.security.device.microphone</key>
|
||||
<true/>
|
||||
<key>com.apple.security.device.audio-input</key>
|
||||
<true/>
|
||||
<key>com.apple.security.network.server</key>
|
||||
<true/>
|
||||
<key>com.apple.security.network.client</key>
|
||||
<true/>
|
||||
<key>com.apple.security.inherit</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
10
src-tauri/Info.plist
Normal file
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>NSCameraUsageDescription</key>
|
||||
<string>Request camera access for WebRTC</string>
|
||||
<key>NSMicrophoneUsageDescription</key>
|
||||
<string>Request microphone access for WebRTC</string>
|
||||
</dict>
|
||||
</plist>
|
||||
BIN
src-tauri/assets/dmg-background.png
Normal file
|
After Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 3.4 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 6.8 KiB After Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 974 B After Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 7.6 KiB After Width: | Height: | Size: 42 KiB |
|
Before Width: | Height: | Size: 903 B After Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 8.4 KiB After Width: | Height: | Size: 47 KiB |
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 6.2 KiB |
|
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 8.4 KiB |
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 3.9 KiB |
|
Before Width: | Height: | Size: 85 KiB After Width: | Height: | Size: 134 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 64 KiB |
@@ -22,10 +22,10 @@
|
||||
"create": true,
|
||||
"decorations": false,
|
||||
"dragDropEnabled": true,
|
||||
"focus": false,
|
||||
"focus": true,
|
||||
"fullscreen": false,
|
||||
"height": 90,
|
||||
"maxHeight": 700,
|
||||
"height": 600,
|
||||
"maxHeight": 600,
|
||||
"minHeight": 90,
|
||||
"width": 680,
|
||||
"maxWidth": 680,
|
||||
@@ -38,7 +38,7 @@
|
||||
"maximized": false,
|
||||
"proxyUrl": "http://localhost:2900",
|
||||
"resizable": false,
|
||||
"shadow": true,
|
||||
"shadow": false,
|
||||
"skipTaskbar": false,
|
||||
"theme": null,
|
||||
"title": "Coco AI",
|
||||
@@ -73,7 +73,25 @@
|
||||
"icons/128x128@2x.png",
|
||||
"icons/icon.icns",
|
||||
"icons/icon.ico"
|
||||
]
|
||||
],
|
||||
"macOS": {
|
||||
"entitlements": "./Entitlements.plist",
|
||||
"dmg": {
|
||||
"background": "assets/dmg-background.png",
|
||||
"windowSize": {
|
||||
"width": 800,
|
||||
"height": 600
|
||||
},
|
||||
"windowPosition": {
|
||||
"x": 400,
|
||||
"y": 400
|
||||
},
|
||||
"applicationFolderPosition": {
|
||||
"x": 480,
|
||||
"y": 220
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"plugins": {
|
||||
"window": {},
|
||||
|
||||
@@ -25,7 +25,7 @@ const AutoResizeTextarea: React.FC<AutoResizeTextareaProps> = ({
|
||||
<textarea
|
||||
ref={textareaRef}
|
||||
className="text-xs flex-1 outline-none min-w-[200px] text-[#333] dark:text-[#d8d8d8] placeholder-text-xs placeholder-[#999] dark:placeholder-gray-500 bg-transparent"
|
||||
placeholder="Ask whatever you want....."
|
||||
placeholder="Ask whatever you want ..."
|
||||
value={input}
|
||||
onChange={(e) => setInput(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
|
||||
@@ -1,378 +1,272 @@
|
||||
import { useState, useRef, useEffect } from "react";
|
||||
import {
|
||||
useState,
|
||||
useRef,
|
||||
useEffect,
|
||||
forwardRef,
|
||||
useImperativeHandle,
|
||||
} from "react";
|
||||
import { MessageSquarePlus, PanelLeft } from "lucide-react";
|
||||
import { motion } from "framer-motion";
|
||||
|
||||
import { ChatMessage } from "./ChatMessage";
|
||||
import { ChatInput } from "./ChatInput";
|
||||
import { Sidebar } from "./Sidebar";
|
||||
import type { Chat, Message } from "./types";
|
||||
import { useTheme } from "../ThemeProvider";
|
||||
import { tauriFetch } from "../../api/tauriFetchClient";
|
||||
import { useWebSocket } from "../../hooks/useWebSocket";
|
||||
import useWindows from "../../hooks/useWindows";
|
||||
import { useChatStore } from "../../stores/chatStore";
|
||||
|
||||
interface ChatAIProps {
|
||||
changeMode: (isChatMode: boolean) => void;
|
||||
inputValue: string;
|
||||
isTransitioned: boolean;
|
||||
changeInput: (val: string) => void;
|
||||
}
|
||||
|
||||
export default function ChatAI({ changeMode, inputValue }: ChatAIProps) {
|
||||
const [chats, setChats] = useState<Chat[]>([]);
|
||||
const [activeChat, setActiveChat] = useState<Chat>();
|
||||
const [isSidebarOpen, setIsSidebarOpen] = useState(false);
|
||||
const [isTyping, setIsTyping] = useState(false);
|
||||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||
const { theme } = useTheme();
|
||||
const { createWin } = useWindows();
|
||||
export interface ChatAIRef {
|
||||
init: () => void;
|
||||
}
|
||||
|
||||
const [websocketId, setWebsocketId] = useState("");
|
||||
const [curMessage, setCurMessage] = useState("");
|
||||
const [curChatEnd, setCurChatEnd] = useState(true);
|
||||
const { messages, setMessages } = useWebSocket(
|
||||
"ws://localhost:2900/ws",
|
||||
(msg) => {
|
||||
if (msg.includes("websocket_session_id")) {
|
||||
const array = msg.split(" ");
|
||||
setWebsocketId(array[2]);
|
||||
}
|
||||
const ChatAI = forwardRef<ChatAIRef, ChatAIProps>(
|
||||
({ inputValue, isTransitioned, changeInput }, ref) => {
|
||||
useImperativeHandle(ref, () => ({
|
||||
init: init,
|
||||
}));
|
||||
|
||||
if (msg.includes("PRIVATE")) {
|
||||
if (msg.includes("assistant finished output")) {
|
||||
setCurChatEnd(true);
|
||||
} else {
|
||||
const cleanedData = msg.replace(/^PRIVATE /, "");
|
||||
try {
|
||||
const chunkData = JSON.parse(cleanedData);
|
||||
setCurMessage((prev) => prev + chunkData.message_chunk);
|
||||
return chunkData.message_chunk;
|
||||
} catch (error) {
|
||||
console.error("JSON Parse error:", error);
|
||||
}
|
||||
return "";
|
||||
const { curChatEnd, setCurChatEnd } = useChatStore();
|
||||
|
||||
const [activeChat, setActiveChat] = useState<Chat>();
|
||||
const [isTyping, setIsTyping] = useState(false);
|
||||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||
const { createWin } = useWindows();
|
||||
|
||||
const [websocketId, setWebsocketId] = useState("");
|
||||
const [curMessage, setCurMessage] = useState("");
|
||||
const { messages, setMessages } = useWebSocket(
|
||||
"ws://localhost:2900/ws",
|
||||
(msg) => {
|
||||
if (msg.includes("websocket_session_id")) {
|
||||
const array = msg.split(" ");
|
||||
setWebsocketId(array[2]);
|
||||
}
|
||||
|
||||
if (msg.includes("PRIVATE")) {
|
||||
if (msg.includes("assistant finished output")) {
|
||||
setCurChatEnd(true);
|
||||
} else {
|
||||
const cleanedData = msg.replace(/^PRIVATE /, "");
|
||||
try {
|
||||
const chunkData = JSON.parse(cleanedData);
|
||||
setCurMessage((prev) => prev + chunkData.message_chunk);
|
||||
return chunkData.message_chunk;
|
||||
} catch (error) {
|
||||
console.error("JSON Parse error:", error);
|
||||
}
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
);
|
||||
|
||||
return "";
|
||||
}
|
||||
);
|
||||
// websocket
|
||||
useEffect(() => {
|
||||
if (messages.length === 0 || !activeChat?._id) return;
|
||||
|
||||
// websocket
|
||||
useEffect(() => {
|
||||
if (messages.length === 0 || !activeChat?._id) return;
|
||||
const simulateAssistantResponse = () => {
|
||||
console.log("messages", messages);
|
||||
|
||||
const simulateAssistantResponse = () => {
|
||||
console.log("messages", messages);
|
||||
const assistantMessage: Message = {
|
||||
_id: activeChat._id,
|
||||
_source: {
|
||||
type: "assistant",
|
||||
message: 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 updatedChat = {
|
||||
...activeChat,
|
||||
messages: [...(activeChat.messages || []), assistantMessage],
|
||||
};
|
||||
setMessages("");
|
||||
setCurMessage("");
|
||||
setActiveChat(updatedChat);
|
||||
setTimeout(() => setIsTyping(false), 1000);
|
||||
const scrollToBottom = () => {
|
||||
messagesEndRef.current?.scrollIntoView({
|
||||
behavior: "smooth",
|
||||
block: "end",
|
||||
});
|
||||
};
|
||||
if (curChatEnd) {
|
||||
simulateAssistantResponse();
|
||||
}
|
||||
}, [messages, isTyping, curChatEnd]);
|
||||
|
||||
// getChatHistory
|
||||
useEffect(() => {
|
||||
getChatHistory();
|
||||
}, []);
|
||||
useEffect(() => {
|
||||
scrollToBottom();
|
||||
}, [activeChat?.messages, isTyping, curMessage]);
|
||||
|
||||
const getChatHistory = async () => {
|
||||
try {
|
||||
const response = await tauriFetch({
|
||||
url: "/chat/_history",
|
||||
method: "GET",
|
||||
});
|
||||
console.log("_history", response);
|
||||
const hits = response.data?.hits?.hits || [];
|
||||
setChats(hits);
|
||||
createNewChat();
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch user data:", error);
|
||||
}
|
||||
};
|
||||
const createNewChat = async () => {
|
||||
chatClose();
|
||||
try {
|
||||
const response = await tauriFetch({
|
||||
url: "/chat/_new",
|
||||
method: "POST",
|
||||
});
|
||||
console.log("_new", response);
|
||||
const newChat: Chat = response.data;
|
||||
|
||||
const scrollToBottom = () => {
|
||||
messagesEndRef.current?.scrollIntoView({
|
||||
behavior: "smooth",
|
||||
block: "end",
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
scrollToBottom();
|
||||
}, [activeChat?.messages, isTyping, curMessage]);
|
||||
|
||||
const createNewChat = async () => {
|
||||
try {
|
||||
const response = await tauriFetch({
|
||||
url: "/chat/_new",
|
||||
method: "POST",
|
||||
});
|
||||
console.log("_new", response);
|
||||
const newChat: Chat = response.data;
|
||||
setChats((prev) => [newChat, ...prev]);
|
||||
setActiveChat(newChat);
|
||||
setIsSidebarOpen(false);
|
||||
//
|
||||
console.log(1111, activeChat, inputValue);
|
||||
if (inputValue) {
|
||||
setTimeout(() => {
|
||||
handleSendMessage(inputValue);
|
||||
}, 500);
|
||||
setActiveChat(newChat);
|
||||
handleSendMessage(inputValue, newChat);
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch user data:", error);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch user data:", error);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const deleteChat = (chatId: string) => {
|
||||
setChats((prev) => prev.filter((chat) => chat._id !== chatId));
|
||||
if (activeChat?._id === chatId) {
|
||||
const remainingChats = chats.filter((chat) => chat._id !== chatId);
|
||||
if (remainingChats.length > 0) {
|
||||
setActiveChat(remainingChats[0]);
|
||||
} else {
|
||||
const init = () => {
|
||||
if (!activeChat?._id) {
|
||||
createNewChat();
|
||||
} else {
|
||||
handleSendMessage(inputValue);
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const handleSendMessage = async (content: string) => {
|
||||
if (!activeChat?._id) return;
|
||||
try {
|
||||
const response = await tauriFetch({
|
||||
url: `/chat/${activeChat?._id}/_send`,
|
||||
method: "POST",
|
||||
headers: {
|
||||
WEBSOCKET_SESSION_ID: websocketId,
|
||||
},
|
||||
body: JSON.stringify({ message: content }),
|
||||
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`,
|
||||
method: "POST",
|
||||
headers: {
|
||||
WEBSOCKET_SESSION_ID: websocketId,
|
||||
},
|
||||
body: JSON.stringify({ message: content }),
|
||||
});
|
||||
console.log("_send", response, websocketId);
|
||||
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 () => {
|
||||
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",
|
||||
title: "Coco AI",
|
||||
dragDropEnabled: true,
|
||||
center: true,
|
||||
width: 900,
|
||||
height: 800,
|
||||
alwaysOnTop: true,
|
||||
skipTaskbar: true,
|
||||
decorations: true,
|
||||
closable: true,
|
||||
url: "/ui/chat",
|
||||
});
|
||||
console.log("_send", response, websocketId);
|
||||
const updatedChat: Chat = {
|
||||
...activeChat,
|
||||
messages: [...(activeChat?.messages || []), ...(response.data || [])],
|
||||
};
|
||||
|
||||
setActiveChat(updatedChat);
|
||||
setIsTyping(true);
|
||||
setCurChatEnd(false);
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch user data:", error);
|
||||
}
|
||||
};
|
||||
|
||||
const chatHistory = async (chat: Chat) => {
|
||||
try {
|
||||
const response = await tauriFetch({
|
||||
url: `/chat/${chat._id}/_history`,
|
||||
method: "GET",
|
||||
});
|
||||
console.log("id_history", response);
|
||||
const hits = response.data?.hits?.hits || [];
|
||||
const updatedChat: Chat = {
|
||||
...chat,
|
||||
messages: hits,
|
||||
};
|
||||
setActiveChat(updatedChat);
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch user data:", error);
|
||||
}
|
||||
};
|
||||
if (!isTransitioned) return null;
|
||||
|
||||
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 onSelectChat = async (chat: any) => {
|
||||
chatClose();
|
||||
try {
|
||||
const response = await tauriFetch({
|
||||
url: `/chat/${chat._id}/_open`,
|
||||
method: "POST",
|
||||
});
|
||||
console.log("_open", response);
|
||||
chatHistory(response.data);
|
||||
setIsSidebarOpen(false);
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch user data:", error);
|
||||
}
|
||||
};
|
||||
|
||||
const cancelChat = async () => {
|
||||
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() {
|
||||
createWin({
|
||||
label: "chat",
|
||||
title: "Coco AI",
|
||||
dragDropEnabled: true,
|
||||
center: true,
|
||||
width: 900,
|
||||
height: 800,
|
||||
alwaysOnTop: true,
|
||||
skipTaskbar: true,
|
||||
decorations: true,
|
||||
closable: true,
|
||||
url: "/ui/chat",
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
className="h-screen rounded-xl overflow-hidden relative"
|
||||
>
|
||||
<div className="h-[calc(100%-100px)] flex rounded-xl overflow-hidden">
|
||||
{/* 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 ${
|
||||
theme === "dark" ? "bg-gray-800" : "bg-gray-100"
|
||||
}`}
|
||||
>
|
||||
{activeChat ? (
|
||||
<Sidebar
|
||||
chats={chats}
|
||||
activeChat={activeChat}
|
||||
isDark={theme === "dark"}
|
||||
onNewChat={createNewChat}
|
||||
onSelectChat={onSelectChat}
|
||||
onDeleteChat={deleteChat}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{/* Main content */}
|
||||
<div
|
||||
className={`flex-1 flex flex-col rounded-xl overflow-hidden bg-chat_bg_light dark:bg-chat_bg_dark bg-cover`}
|
||||
return (
|
||||
<div
|
||||
data-tauri-drag-region
|
||||
className={`h-[500px] flex flex-col rounded-xl overflow-hidden bg-chat_bg_light dark:bg-chat_bg_dark bg-cover`}
|
||||
>
|
||||
<header
|
||||
data-tauri-drag-region
|
||||
className={`flex items-center justify-between py-2 px-1`}
|
||||
>
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: -20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -20 }}
|
||||
transition={{ delay: 0.2 }}
|
||||
<button
|
||||
onClick={() => openChatAI()}
|
||||
className={`p-2 rounded-lg transition-colors text-[#333] dark:text-[#d8d8d8]`}
|
||||
>
|
||||
<header 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>
|
||||
<PanelLeft className="h-4 w-4" />
|
||||
</button>
|
||||
|
||||
{/* <ThemeToggle /> */}
|
||||
{/* <ThemeToggle /> */}
|
||||
|
||||
<button
|
||||
onClick={() => createNewChat()}
|
||||
className={`p-2 rounded-lg transition-colors text-[#333] dark:text-[#d8d8d8]`}
|
||||
>
|
||||
<MessageSquarePlus className="h-4 w-4" />
|
||||
</button>
|
||||
</header>
|
||||
</motion.div>
|
||||
|
||||
{/* Chat messages */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{ delay: 0.3 }}
|
||||
className="flex-1 overflow-y-auto border-t border-[rgba(0,0,0,0.1)] dark:border-[rgba(255,255,255,0.15)] custom-scrollbar"
|
||||
<button
|
||||
onClick={() => {
|
||||
createNewChat();
|
||||
}}
|
||||
className={`p-2 rounded-lg transition-colors text-[#333] dark:text-[#d8d8d8]`}
|
||||
>
|
||||
{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}
|
||||
{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} />
|
||||
</motion.div>
|
||||
<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,
|
||||
_source: {
|
||||
type: "assistant",
|
||||
message: curMessage,
|
||||
},
|
||||
}}
|
||||
isTyping={!curChatEnd}
|
||||
/>
|
||||
) : null}
|
||||
<div ref={messagesEndRef} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
{/* Input area */}
|
||||
<motion.div
|
||||
initial={{ y: 100 }}
|
||||
animate={{ y: 0 }}
|
||||
exit={{ y: 100 }}
|
||||
transition={{ type: "spring", stiffness: 300, damping: 30 }}
|
||||
className={`mt-2.5 rounded-xl overflow-hidden`}
|
||||
>
|
||||
<ChatInput
|
||||
onSend={handleSendMessage}
|
||||
disabled={isTyping}
|
||||
disabledChange={(value) => {
|
||||
cancelChat();
|
||||
setIsTyping(value);
|
||||
}}
|
||||
changeMode={changeMode}
|
||||
/>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
export default ChatAI;
|
||||
|
||||
@@ -19,7 +19,6 @@ interface ChatInputProps {
|
||||
export function ChatInput({
|
||||
onSend,
|
||||
disabled,
|
||||
disabledChange,
|
||||
changeMode,
|
||||
}: ChatInputProps) {
|
||||
const [input, setInput] = useState("");
|
||||
@@ -102,7 +101,7 @@ export function ChatInput({
|
||||
|
||||
{/* Switch */}
|
||||
<ChatSwitch
|
||||
isChat={true}
|
||||
isChatMode={true}
|
||||
onChange={(value) => {
|
||||
changeMode(value);
|
||||
setInput("");
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
import { Bot, User } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
|
||||
import type { Message } from "./types";
|
||||
// import { TypingAnimation } from "./TypingAnimation";
|
||||
import { Markdown } from "./Markdown";
|
||||
|
||||
interface ChatMessageProps {
|
||||
@@ -11,7 +7,6 @@ interface ChatMessageProps {
|
||||
}
|
||||
|
||||
export function ChatMessage({ message, isTyping }: ChatMessageProps) {
|
||||
const [isAnimationComplete, setIsAnimationComplete] = useState(!isTyping);
|
||||
const isAssistant = message._source?.type === "assistant";
|
||||
|
||||
return (
|
||||
@@ -23,7 +18,7 @@ export function ChatMessage({ message, isTyping }: ChatMessageProps) {
|
||||
isAssistant ? "" : "flex-row-reverse"
|
||||
}`}
|
||||
>
|
||||
<div
|
||||
{/* <div
|
||||
className={`flex-shrink-0 h-8 w-8 rounded-lg flex items-center justify-center ${
|
||||
isAssistant
|
||||
? "bg-gradient-to-br from-green-400 to-emerald-500"
|
||||
@@ -35,27 +30,23 @@ export function ChatMessage({ message, isTyping }: ChatMessageProps) {
|
||||
) : (
|
||||
<User className="h-5 w-5 text-white" />
|
||||
)}
|
||||
</div>
|
||||
</div> */}
|
||||
|
||||
<div
|
||||
className={`flex-1 space-y-2 ${
|
||||
isAssistant ? "text-left" : "text-right"
|
||||
}`}
|
||||
>
|
||||
<p className="font-medium text-sm text-gray-900 dark:text-gray-100">
|
||||
{isAssistant ? "Assistant" : "You"}
|
||||
<p className="font-semibold text-sm text-[#333] dark:text-[#d8d8d8]">
|
||||
{isAssistant ? "Summary" : ""}
|
||||
</p>
|
||||
<div className="prose dark:prose-invert prose-sm max-w-none">
|
||||
<p className="text-gray-600 dark:text-gray-300 leading-relaxed">
|
||||
<div className="text-[#333] dark:text-[#d8d8d8] leading-relaxed">
|
||||
{isAssistant ? (
|
||||
<>
|
||||
{/* <TypingAnimation
|
||||
text={message._source?.message || ""}
|
||||
onComplete={() => setIsAnimationComplete(true)}
|
||||
/> */}
|
||||
<Markdown
|
||||
key={isTyping ? "loading" : "done"}
|
||||
content={(message._source?.message || "")}
|
||||
content={message._source?.message || ""}
|
||||
loading={isTyping}
|
||||
onDoubleClickCapture={() => {}}
|
||||
/>
|
||||
@@ -64,9 +55,11 @@ export function ChatMessage({ message, isTyping }: ChatMessageProps) {
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
message._source?.message || ""
|
||||
<div className="px-3 py-2 bg-white dark:bg-[#202126] rounded-xl border border-black/12 dark:border-black/15 font-normal text-sm text-[#333333] dark:text-[#D8D8D8]">
|
||||
{message._source?.message || ""}
|
||||
</div>
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -37,8 +37,8 @@ export function Mermaid(props: { code: string }) {
|
||||
function viewSvgInNewWindow() {
|
||||
const svg = ref.current?.querySelector("svg");
|
||||
if (!svg) return;
|
||||
const text = new XMLSerializer().serializeToString(svg);
|
||||
const blob = new Blob([text], { type: "image/svg+xml" });
|
||||
// const text = new XMLSerializer().serializeToString(svg);
|
||||
// const blob = new Blob([text], { type: "image/svg+xml" });
|
||||
// view img
|
||||
// URL.createObjectURL(blob);
|
||||
}
|
||||
@@ -63,12 +63,13 @@ export function Mermaid(props: { code: string }) {
|
||||
}
|
||||
|
||||
// 7
|
||||
export function PreCode(props: { children: any }) {
|
||||
export function PreCode(props: { children?: any }) {
|
||||
const ref = useRef<HTMLPreElement>(null);
|
||||
// const previewRef = useRef<HTMLPreviewHander>(null);
|
||||
const [mermaidCode, setMermaidCode] = useState("");
|
||||
const [htmlCode, setHtmlCode] = useState("");
|
||||
const { height } = useWindowSize();
|
||||
console.log(htmlCode, height);
|
||||
|
||||
const renderArtifacts = useDebouncedCallback(() => {
|
||||
if (!ref.current) return;
|
||||
@@ -86,6 +87,7 @@ export function PreCode(props: { children: any }) {
|
||||
}, 600);
|
||||
|
||||
const enableArtifacts = true;
|
||||
console.log(enableArtifacts);
|
||||
|
||||
//Wrap the paragraph for plain-text
|
||||
useEffect(() => {
|
||||
@@ -137,7 +139,7 @@ export function PreCode(props: { children: any }) {
|
||||
}
|
||||
|
||||
// 6
|
||||
function CustomCode(props: { children: any; className?: string }) {
|
||||
function CustomCode(props: { children?: any; className?: string }) {
|
||||
const enableCodeFold = false;
|
||||
|
||||
const ref = useRef<HTMLPreElement>(null);
|
||||
@@ -213,13 +215,13 @@ function tryWrapHtmlCode(text: string) {
|
||||
return text
|
||||
.replace(
|
||||
/([`]*?)(\w*?)([\n\r]*?)(<!DOCTYPE html>)/g,
|
||||
(match, quoteStart, lang, newLine, doctype) => {
|
||||
(match, quoteStart, doctype) => {
|
||||
return !quoteStart ? "\n```html\n" + doctype : match;
|
||||
}
|
||||
)
|
||||
.replace(
|
||||
/(<\/body>)([\r\n\s]*?)(<\/html>)([\n\r]*)([`]*)([\n\r]*?)/g,
|
||||
(match, bodyEnd, space, htmlEnd, newLine, quoteEnd) => {
|
||||
(match, bodyEnd, space, htmlEnd, quoteEnd) => {
|
||||
return !quoteEnd ? bodyEnd + space + htmlEnd + "\n```\n" : match;
|
||||
}
|
||||
);
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { MessageSquare, Plus, Trash2 } from "lucide-react";
|
||||
import { MessageSquare, Plus } from "lucide-react";
|
||||
import type { Chat } from "./types";
|
||||
import { useTheme } from "../ThemeProvider";
|
||||
|
||||
interface SidebarProps {
|
||||
chats: Chat[];
|
||||
activeChat: Chat;
|
||||
isDark: boolean;
|
||||
onNewChat: () => void;
|
||||
onSelectChat: (chat: Chat) => void;
|
||||
onDeleteChat: (chatId: string) => void;
|
||||
@@ -14,12 +14,13 @@ interface SidebarProps {
|
||||
export function Sidebar({
|
||||
chats,
|
||||
activeChat,
|
||||
isDark,
|
||||
onNewChat,
|
||||
onSelectChat,
|
||||
onDeleteChat,
|
||||
className = "",
|
||||
}: SidebarProps) {
|
||||
const { theme } = useTheme();
|
||||
const isDark = theme === "dark";
|
||||
|
||||
return (
|
||||
<div className={`h-full flex flex-col ${className}`}>
|
||||
<div className="p-4">
|
||||
@@ -27,8 +28,8 @@ export function Sidebar({
|
||||
onClick={onNewChat}
|
||||
className={`w-full flex items-center gap-3 px-4 py-3 text-sm rounded-lg transition-all ${
|
||||
isDark
|
||||
? "bg-gray-700 text-white hover:bg-gray-600 active:bg-gray-500"
|
||||
: "bg-white text-gray-700 hover:bg-gray-50 active:bg-gray-100 shadow-sm"
|
||||
? " text-white hover:bg-gray-600 active:bg-gray-500"
|
||||
: " text-gray-700 hover:bg-gray-50 active:bg-gray-100 shadow-sm"
|
||||
}`}
|
||||
>
|
||||
<Plus
|
||||
@@ -46,8 +47,8 @@ export function Sidebar({
|
||||
className={`group relative rounded-lg transition-all ${
|
||||
activeChat._id === chat._id
|
||||
? isDark
|
||||
? "bg-gray-700/50 text-white"
|
||||
: "bg-white text-gray-900 shadow-sm"
|
||||
? " text-white"
|
||||
: " text-gray-900 shadow-sm"
|
||||
: isDark
|
||||
? "text-gray-300 hover:bg-gray-700/30"
|
||||
: "text-gray-600 hover:bg-white/10"
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import ReactMarkdown from "react-markdown";
|
||||
import remarkGfm from "remark-gfm";
|
||||
import "./index.css";
|
||||
|
||||
interface TypingAnimationProps {
|
||||
text: string;
|
||||
onComplete?: () => void;
|
||||
speed?: number;
|
||||
}
|
||||
|
||||
export function TypingAnimation({
|
||||
text,
|
||||
onComplete,
|
||||
speed = 30,
|
||||
}: TypingAnimationProps) {
|
||||
const [displayedText, setDisplayedText] = useState("");
|
||||
const [currentIndex, setCurrentIndex] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
if (currentIndex < text.length) {
|
||||
const timeout = setTimeout(() => {
|
||||
setDisplayedText((prev) => prev + text[currentIndex]);
|
||||
setCurrentIndex((prev) => prev + 1);
|
||||
}, speed);
|
||||
|
||||
return () => clearTimeout(timeout);
|
||||
} else if (onComplete) {
|
||||
onComplete();
|
||||
}
|
||||
}, [currentIndex, text, speed, onComplete]);
|
||||
|
||||
// console.log("text", text);
|
||||
|
||||
// return <ReactMarkdown>{text}</ReactMarkdown>;
|
||||
|
||||
return (
|
||||
<ReactMarkdown
|
||||
className="prose"
|
||||
children={text}
|
||||
remarkPlugins={[remarkGfm]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,13 +1,10 @@
|
||||
import { useState, useRef, useEffect } from "react";
|
||||
import { PanelRightClose, PanelRightOpen, X } from "lucide-react";
|
||||
import { motion } from "framer-motion";
|
||||
|
||||
// import { ThemeToggle } from "./ThemeToggle";
|
||||
import { ChatMessage } from "./ChatMessage";
|
||||
import { ChatInput } from "./ChatInput";
|
||||
import { Sidebar } from "./Sidebar";
|
||||
import type { Chat, Message } from "./types";
|
||||
import { useTheme } from "../ThemeProvider";
|
||||
import { tauriFetch } from "../../api/tauriFetchClient";
|
||||
import { useWebSocket } from "../../hooks/useWebSocket";
|
||||
import useWindows from "../../hooks/useWindows";
|
||||
@@ -20,7 +17,6 @@ export default function ChatAI({}: ChatAIProps) {
|
||||
const [isSidebarOpen, setIsSidebarOpen] = useState(true);
|
||||
const [isTyping, setIsTyping] = useState(false);
|
||||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||
const { theme } = useTheme();
|
||||
const { closeWin } = useWindows();
|
||||
|
||||
const [websocketId, setWebsocketId] = useState("");
|
||||
@@ -214,7 +210,7 @@ export default function ChatAI({}: ChatAIProps) {
|
||||
console.error("Failed to fetch user data:", error);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const cancelChat = async () => {
|
||||
if (!activeChat?._id) return;
|
||||
try {
|
||||
@@ -233,28 +229,19 @@ export default function ChatAI({}: ChatAIProps) {
|
||||
}
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
className="h-screen"
|
||||
>
|
||||
<div className="h-screen bg-chat_bg_light dark:bg-chat_bg_dark bg-cover">
|
||||
<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 ${
|
||||
theme === "dark" ? "bg-gray-800" : "bg-gray-100"
|
||||
}`}
|
||||
} transition-transform duration-300 ease-in-out md:translate-x-0 md:static md:block`}
|
||||
>
|
||||
{activeChat ? (
|
||||
<Sidebar
|
||||
chats={chats}
|
||||
activeChat={activeChat}
|
||||
isDark={theme === "dark"}
|
||||
onNewChat={createNewChat}
|
||||
onSelectChat={onSelectChat}
|
||||
onDeleteChat={deleteChat}
|
||||
@@ -264,51 +251,26 @@ export default function ChatAI({}: ChatAIProps) {
|
||||
) : null}
|
||||
|
||||
{/* Main content */}
|
||||
<div
|
||||
className={`flex-1 flex flex-col ${
|
||||
theme === "dark" ? "bg-gray-900" : "bg-white"
|
||||
}`}
|
||||
>
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: -20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -20 }}
|
||||
transition={{ delay: 0.2 }}
|
||||
<div className={`flex-1 flex flex-col`}>
|
||||
<header
|
||||
className={`flex items-center justify-between p-2 border-b border-gray-200 dark:border-gray-800`}
|
||||
>
|
||||
<header
|
||||
className={`flex items-center justify-between p-2 border-b ${
|
||||
theme === "dark" ? "border-gray-800" : "border-gray-200"
|
||||
}`}
|
||||
<button
|
||||
onClick={() => setIsSidebarOpen(!isSidebarOpen)}
|
||||
className={`rounded-lg transition-colors hover:bg-gray-100 text-gray-600 dark:hover:bg-gray-800 dark:text-gray-300`}
|
||||
>
|
||||
<button
|
||||
onClick={() => setIsSidebarOpen(!isSidebarOpen)}
|
||||
className={`rounded-lg transition-colors ${
|
||||
theme === "dark"
|
||||
? "hover:bg-gray-800 text-gray-300"
|
||||
: "hover:bg-gray-100 text-gray-600"
|
||||
}`}
|
||||
>
|
||||
{isSidebarOpen ? (
|
||||
<PanelRightClose className="h-6 w-6" />
|
||||
) : (
|
||||
<PanelRightOpen className="h-6 w-6" />
|
||||
)}
|
||||
</button>
|
||||
{isSidebarOpen ? (
|
||||
<PanelRightClose className="h-6 w-6" />
|
||||
) : (
|
||||
<PanelRightOpen className="h-6 w-6" />
|
||||
)}
|
||||
</button>
|
||||
|
||||
{/* <ThemeToggle /> */}
|
||||
|
||||
<X className="cursor-pointer" onClick={closeWindow} />
|
||||
</header>
|
||||
</motion.div>
|
||||
<X className="cursor-pointer" onClick={closeWindow} />
|
||||
</header>
|
||||
|
||||
{/* Chat messages */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{ delay: 0.3 }}
|
||||
className="flex-1 overflow-y-auto custom-scrollbar"
|
||||
>
|
||||
<div className="flex-1 overflow-y-auto custom-scrollbar">
|
||||
{activeChat?.messages?.map((message, index) => (
|
||||
<ChatMessage
|
||||
key={message._id + index}
|
||||
@@ -341,30 +303,22 @@ export default function ChatAI({}: ChatAIProps) {
|
||||
</div>
|
||||
)}
|
||||
<div ref={messagesEndRef} />
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
{/* Input area */}
|
||||
<motion.div
|
||||
initial={{ y: 100 }}
|
||||
animate={{ y: 0 }}
|
||||
exit={{ y: 100 }}
|
||||
transition={{ type: "spring", stiffness: 300, damping: 30 }}
|
||||
className={`border-t p-4 ${
|
||||
theme === "dark" ? "border-gray-800" : "border-gray-200"
|
||||
}`}
|
||||
>
|
||||
<div className={`border-t p-4 border-gray-200 dark:border-gray-800`}>
|
||||
<ChatInput
|
||||
onSend={handleSendMessage}
|
||||
disabled={isTyping}
|
||||
disabledChange={(value) => {
|
||||
cancelChat()
|
||||
cancelChat();
|
||||
setIsTyping(value);
|
||||
}}
|
||||
changeMode={() => {}}
|
||||
/>
|
||||
</motion.div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { useState, useEffect, useRef } from "react";
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
import { SearchIcon, MessageCircleIcon } from "lucide-react";
|
||||
|
||||
import { SearchResults } from "./SearchResults";
|
||||
@@ -43,12 +42,9 @@ export const CommandPalette: React.FC<CommandPaletteProps> = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<AnimatePresence>
|
||||
<div>
|
||||
{isOpen && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: -20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -20 }}
|
||||
<div
|
||||
className="fixed inset-0 z-50 flex items-start justify-center pt-20"
|
||||
>
|
||||
<div className="w-full max-w-2xl bg-white rounded-lg shadow-2xl">
|
||||
@@ -88,8 +84,8 @@ export const CommandPalette: React.FC<CommandPaletteProps> = ({
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
46
src/components/SearchChat/AutoResizeTextarea.tsx
Normal file
@@ -0,0 +1,46 @@
|
||||
import React, { useEffect, useRef } from "react";
|
||||
|
||||
interface AutoResizeTextareaProps {
|
||||
input: string;
|
||||
setInput: (value: string) => void;
|
||||
handleKeyDown?: (e: React.KeyboardEvent<HTMLTextAreaElement>) => void;
|
||||
}
|
||||
|
||||
const AutoResizeTextarea: React.FC<AutoResizeTextareaProps> = ({
|
||||
input,
|
||||
setInput,
|
||||
handleKeyDown,
|
||||
}) => {
|
||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const textarea = textareaRef.current;
|
||||
if (textarea) {
|
||||
textarea.style.height = "auto"; // Reset height to recalculate
|
||||
textarea.style.height = `${textarea.scrollHeight}px`; // Adjust based on content
|
||||
}
|
||||
}, [input]);
|
||||
|
||||
return (
|
||||
<textarea
|
||||
ref={textareaRef}
|
||||
autoComplete="off"
|
||||
autoCapitalize="none"
|
||||
spellCheck="false"
|
||||
className="text-xs flex-1 outline-none min-w-[200px] text-[#333] dark:text-[#d8d8d8] placeholder-text-xs placeholder-[#999] dark:placeholder-gray-500 bg-transparent"
|
||||
placeholder="Ask whatever you want ..."
|
||||
value={input}
|
||||
onChange={(e) => setInput(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
rows={1}
|
||||
style={{
|
||||
resize: "none", // Prevent manual resize
|
||||
overflow: "auto", // Enable scrollbars when needed
|
||||
maxHeight: "4.5rem", // Limit height to 3 rows (3 * 1.5 line-height)
|
||||
lineHeight: "1.5rem", // Line height to match row height
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default AutoResizeTextarea;
|
||||
@@ -1,21 +1,14 @@
|
||||
import React, { useState } from "react";
|
||||
import React from "react";
|
||||
import { Bot, Search } from "lucide-react";
|
||||
|
||||
interface ChatSwitchProps {
|
||||
isChat?: boolean;
|
||||
onChange?: (isChatMode: boolean) => void;
|
||||
isChatMode: boolean;
|
||||
onChange: (isChatMode: boolean) => void;
|
||||
}
|
||||
|
||||
const ChatSwitch: React.FC<ChatSwitchProps> = ({
|
||||
isChat = false,
|
||||
onChange,
|
||||
}) => {
|
||||
const [isChatMode, setIsChatMode] = useState(isChat);
|
||||
|
||||
const ChatSwitch: React.FC<ChatSwitchProps> = ({ isChatMode, onChange }) => {
|
||||
const handleToggle = () => {
|
||||
const newMode = !isChatMode;
|
||||
setIsChatMode(newMode);
|
||||
onChange?.(newMode);
|
||||
onChange?.(!isChatMode);
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -7,8 +7,9 @@ interface DropdownListProps {
|
||||
isSearchComplete: boolean;
|
||||
}
|
||||
|
||||
function DropdownList({ selected, suggests, isSearchComplete }: DropdownListProps) {
|
||||
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)[]>([]);
|
||||
|
||||
@@ -22,7 +23,13 @@ function DropdownList({ selected, suggests, isSearchComplete }: DropdownListProp
|
||||
}
|
||||
};
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent<HTMLDivElement>) => {
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
console.log(
|
||||
"handleKeyDown",
|
||||
e.key,
|
||||
showIndex,
|
||||
e.key >= "0" && e.key <= "9" && showIndex
|
||||
);
|
||||
if (!suggests.length) return;
|
||||
|
||||
if (e.key === "ArrowUp") {
|
||||
@@ -35,6 +42,9 @@ function DropdownList({ selected, suggests, isSearchComplete }: DropdownListProp
|
||||
setSelectedItem((prev) =>
|
||||
prev === null || prev === suggests.length - 1 ? 0 : prev + 1
|
||||
);
|
||||
} else if (e.key === "Meta") {
|
||||
e.preventDefault();
|
||||
setShowIndex(true);
|
||||
} else if (e.key === "Enter" && selectedItem !== null) {
|
||||
const item = suggests[selectedItem];
|
||||
if (item?._source?.url) {
|
||||
@@ -43,17 +53,42 @@ function DropdownList({ selected, suggests, isSearchComplete }: DropdownListProp
|
||||
selected(item);
|
||||
}
|
||||
}
|
||||
|
||||
if (e.key >= "0" && e.key <= "9" && showIndex) {
|
||||
console.log(`number ${e.key}`);
|
||||
const item = suggests[parseInt(e.key, 10)];
|
||||
if (item?._source?.url) {
|
||||
handleOpenURL(item?._source?.url);
|
||||
} else {
|
||||
selected(item);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleKeyUp = (e: KeyboardEvent) => {
|
||||
console.log("handleKeyUp", e.key);
|
||||
if (!suggests.length) return;
|
||||
|
||||
if (!e.metaKey) {
|
||||
setShowIndex(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener("keydown", handleKeyDown);
|
||||
window.addEventListener("keyup", handleKeyUp);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("keydown", handleKeyDown);
|
||||
window.removeEventListener("keyup", handleKeyUp);
|
||||
};
|
||||
}, [showIndex]);
|
||||
|
||||
useEffect(() => {
|
||||
if (suggests.length > 0) {
|
||||
setSelectedItem(0);
|
||||
|
||||
if (containerRef.current && isSearchComplete) {
|
||||
containerRef.current.focus();
|
||||
}
|
||||
}
|
||||
}, [JSON.stringify(suggests), isSearchComplete]);
|
||||
}, [JSON.stringify(suggests)]);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedItem !== null && itemRefs.current[selectedItem]) {
|
||||
@@ -67,53 +102,51 @@ function DropdownList({ selected, suggests, isSearchComplete }: DropdownListProp
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
className="h-[calc(100vh-100px)] mt-2.5 pb-10 flex flex-col bg-search_bg_light dark:bg-chat_bg_dark bg-cover rounded-xl overflow-hidden focus:outline-none"
|
||||
data-tauri-drag-region
|
||||
className="h-[500px] w-full p-2 pb-10 flex flex-col bg-search_bg_light dark:bg-chat_bg_dark bg-cover rounded-xl overflow-y-auto overflow-hidden focus:outline-none"
|
||||
tabIndex={0}
|
||||
onKeyDown={handleKeyDown}
|
||||
>
|
||||
<div className="flex-1 overflow-y-auto p-2">
|
||||
{suggests?.map((item, index) => {
|
||||
const isSelected = selectedItem === index;
|
||||
return (
|
||||
<button
|
||||
key={item._id}
|
||||
ref={(el) => (itemRefs.current[index] = el)}
|
||||
onClick={() => {
|
||||
setSelectedItem(index);
|
||||
if (item?._source?.url) {
|
||||
handleOpenURL(item?._source?.url);
|
||||
} else {
|
||||
selected(item);
|
||||
}
|
||||
}}
|
||||
className={`w-full h-10 px-2 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)]"
|
||||
}`}
|
||||
>
|
||||
<div className="flex gap-2 items-center">
|
||||
<img className="w-5 h-5" src={item?._source?.icon} alt="icon" />
|
||||
<span className="text-[#333] dark:text-[#d8d8d8]">
|
||||
{item?._source?.source}/{item?._source?.title}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex gap-2 items-center">
|
||||
<span className="text-sm text-[#666] dark:text-[#666]">
|
||||
{item?._source?.type}
|
||||
</span>
|
||||
{suggests?.map((item, index) => {
|
||||
const isSelected = selectedItem === index;
|
||||
return (
|
||||
<button
|
||||
key={item._id}
|
||||
ref={(el) => (itemRefs.current[index] = el)}
|
||||
onMouseEnter={() => setSelectedItem(index)}
|
||||
onClick={() => {
|
||||
if (item?._source?.url) {
|
||||
handleOpenURL(item?._source?.url);
|
||||
} else {
|
||||
selected(item);
|
||||
}
|
||||
}}
|
||||
className={`w-full h-10 px-2 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)]"
|
||||
}`}
|
||||
>
|
||||
<div className="flex gap-2 items-center">
|
||||
<img className="w-5 h-5" src={item?._source?.icon} alt="icon" />
|
||||
<span className="text-[#333] dark:text-[#d8d8d8] truncate w-80 text-left">
|
||||
{item?._source?.source}/{item?._source?.title}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex gap-2 items-center relative">
|
||||
<span className="text-sm text-[#666] dark:text-[#666] truncate w-52 text-right">
|
||||
{item?._source?.type}
|
||||
</span>
|
||||
{showIndex && index < 10 ? (
|
||||
<div
|
||||
className={`w-4 h-4 text-xs flex items-center justify-center text-[#e4e5ef] border border-[#e4e5ef] rounded-sm ${
|
||||
isSelected ? "text-blue-500 dark:bg-white" : ""
|
||||
}`}
|
||||
className={`absolute right-0 w-4 h-4 flex items-center justify-center font-normal text-xs text-[#333] leading-[14px] bg-[#ccc] dark:bg-[#6B6B6B] shadow-[-6px_0px_6px_2px_#e6e6e6] dark:shadow-[-6px_0px_6px_2px_#000] rounded-md`}
|
||||
>
|
||||
{index + 1}
|
||||
{index}
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,60 +1,50 @@
|
||||
import {
|
||||
Settings,
|
||||
LogOut,
|
||||
// Settings,
|
||||
// LogOut,
|
||||
Command,
|
||||
User,
|
||||
Home,
|
||||
ChevronUp,
|
||||
// User,
|
||||
// Home,
|
||||
// ChevronUp,
|
||||
ArrowDown01,
|
||||
AppWindowMac,
|
||||
ArrowDownUp,
|
||||
// ArrowDownUp,
|
||||
CornerDownLeft,
|
||||
} from "lucide-react";
|
||||
// import { Menu, MenuButton, MenuItems, MenuItem } from "@headlessui/react";
|
||||
// import { Link } from "react-router-dom";
|
||||
import { WebviewWindow } from "@tauri-apps/api/webviewWindow";
|
||||
|
||||
const shortcuts = [
|
||||
{ label: "Quick open", keys: "Tab" },
|
||||
{ label: "Open", keys: "CornerDownLeft" },
|
||||
];
|
||||
|
||||
const isChatShortcuts = [
|
||||
{ label: "Go to Search", keys: "⌘ + /" },
|
||||
{ label: "Open", keys: "⌘ + O" },
|
||||
];
|
||||
// import { WebviewWindow } from "@tauri-apps/api/webviewWindow";
|
||||
|
||||
interface FooterProps {
|
||||
isChat: boolean;
|
||||
name?: string;
|
||||
}
|
||||
|
||||
export const Footer = ({ isChat, name }: FooterProps) => {
|
||||
async function openWebviewWindowSettings() {
|
||||
const webview = new WebviewWindow("settings", {
|
||||
title: "Coco Settings",
|
||||
dragDropEnabled: true,
|
||||
center: true,
|
||||
width: 900,
|
||||
height: 700,
|
||||
alwaysOnTop: true,
|
||||
skipTaskbar: true,
|
||||
decorations: true,
|
||||
closable: true,
|
||||
url: "/ui/settings",
|
||||
});
|
||||
webview.once("tauri://created", function () {
|
||||
console.log("webview created");
|
||||
});
|
||||
webview.once("tauri://error", function (e) {
|
||||
console.log("error creating webview", e);
|
||||
});
|
||||
}
|
||||
export const Footer = ({ name }: FooterProps) => {
|
||||
// async function openWebviewWindowSettings() {
|
||||
// const webview = new WebviewWindow("settings", {
|
||||
// title: "Coco Settings",
|
||||
// dragDropEnabled: true,
|
||||
// center: true,
|
||||
// width: 900,
|
||||
// height: 700,
|
||||
// alwaysOnTop: true,
|
||||
// skipTaskbar: true,
|
||||
// decorations: true,
|
||||
// closable: true,
|
||||
// url: "/ui/settings",
|
||||
// });
|
||||
// webview.once("tauri://created", function () {
|
||||
// console.log("webview created");
|
||||
// });
|
||||
// webview.once("tauri://error", function (e) {
|
||||
// console.log("error creating webview", e);
|
||||
// });
|
||||
// }
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{ zIndex: 999 }}
|
||||
className="px-4 h-10 fixed bottom-0 left-0 right-0 border-t border-gray-200 dark:border-gray-700 flex items-center justify-between rounded-xl rounded-t-none overflow-hidden"
|
||||
data-tauri-drag-region
|
||||
className="px-4 z-999 mx-[1px] h-10 absolute bottom-0 left-0 right-0 border-t border-gray-200 dark:border-gray-700 flex items-center justify-between rounded-xl rounded-t-none overflow-hidden"
|
||||
>
|
||||
<div className="flex items-center">
|
||||
{
|
||||
|
||||
156
src/components/SearchChat/InputBox.tsx
Normal file
@@ -0,0 +1,156 @@
|
||||
import {
|
||||
Library,
|
||||
Mic,
|
||||
Send,
|
||||
Plus,
|
||||
AudioLines,
|
||||
Image,
|
||||
CircleStop,
|
||||
} from "lucide-react";
|
||||
import { useRef, type KeyboardEvent } from "react";
|
||||
|
||||
import ChatSwitch from "../SearchChat/ChatSwitch";
|
||||
import AutoResizeTextarea from "./AutoResizeTextarea";
|
||||
import { useChatStore } from "../../stores/chatStore";
|
||||
|
||||
interface ChatInputProps {
|
||||
onSend: (message: string) => void;
|
||||
disabled: boolean;
|
||||
disabledChange: (disabled: boolean) => void;
|
||||
changeMode: (isChatMode: boolean) => void;
|
||||
isChatMode: boolean;
|
||||
inputValue: string;
|
||||
changeInput: (val: string) => void;
|
||||
}
|
||||
|
||||
export default function ChatInput({
|
||||
onSend,
|
||||
disabled,
|
||||
changeMode,
|
||||
isChatMode,
|
||||
inputValue,
|
||||
changeInput,
|
||||
}: ChatInputProps) {
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const { curChatEnd, setCurChatEnd } = useChatStore();
|
||||
|
||||
const handleSubmit = () => {
|
||||
if (inputValue.trim() && !disabled) {
|
||||
onSend(inputValue.trim());
|
||||
}
|
||||
};
|
||||
|
||||
const handleKeyDown = (e: KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
if (e.key === "Enter" && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
handleSubmit();
|
||||
}
|
||||
};
|
||||
|
||||
const openChatAI = async () => {
|
||||
console.log("Chat AI opened.");
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-full rounded-xl overflow-hidden">
|
||||
<div className="bg-inputbox_bg_light dark:bg-inputbox_bg_dark bg-cover rounded-xl">
|
||||
<div className="p-[13px] flex items-center dark:text-[#D8D8D8] bg-white dark:bg-[#202126] rounded-xl transition-all">
|
||||
<div className="flex flex-wrap gap-2 flex-1 items-center">
|
||||
{isChatMode ? (
|
||||
<AutoResizeTextarea
|
||||
input={inputValue}
|
||||
setInput={changeInput}
|
||||
handleKeyDown={handleKeyDown}
|
||||
/>
|
||||
) : (
|
||||
<input
|
||||
ref={inputRef}
|
||||
type="text"
|
||||
autoFocus
|
||||
autoComplete="off"
|
||||
autoCapitalize="none"
|
||||
spellCheck="false"
|
||||
className="text-xs leading-6 font-normal flex-1 outline-none min-w-[200px] text-[#333] dark:text-[#d8d8d8] placeholder-text-xs placeholder-[#999] dark:placeholder-gray-500 bg-transparent"
|
||||
placeholder="Search whatever you want ..."
|
||||
value={inputValue}
|
||||
onChange={(e) => {
|
||||
onSend(e.target.value);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{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}
|
||||
{isChatMode && curChatEnd ? (
|
||||
<button
|
||||
className={`ml-1 p-1 ${
|
||||
inputValue ? "bg-[#0072FF]" : "bg-[#E4E5F0]"
|
||||
} rounded-full transition-colors`}
|
||||
type="submit"
|
||||
onClick={() => onSend(inputValue.trim())}
|
||||
>
|
||||
<Send className="w-4 h-4 text-white hover:text-[#999]" />
|
||||
</button>
|
||||
) : null}
|
||||
{isChatMode && !curChatEnd ? (
|
||||
<button
|
||||
className={`ml-1 p-1 bg-[#0072FF] rounded-full transition-colors`}
|
||||
type="submit"
|
||||
onClick={() => setCurChatEnd(true)}
|
||||
>
|
||||
<CircleStop className="w-4 h-4 text-white hover:text-[#999]" />
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<div
|
||||
data-tauri-drag-region
|
||||
className="flex justify-between items-center p-2 rounded-xl"
|
||||
>
|
||||
{isChatMode ? (
|
||||
<div className="flex gap-1 text-xs text-[#333] dark:text-[#d8d8d8]">
|
||||
<button
|
||||
className="inline-flex items-center p-1 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors "
|
||||
onClick={openChatAI}
|
||||
>
|
||||
<Library className="w-4 h-4 mr-1 text-[#000] dark:text-[#d8d8d8]" />
|
||||
Coco
|
||||
</button>
|
||||
<button className="inline-flex items-center p-1 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 transition-color">
|
||||
<Plus className="w-4 h-4 mr-1 text-[#000] dark:text-[#d8d8d8]" />
|
||||
Upload
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex gap-1">
|
||||
<button
|
||||
className="inline-flex items-center p-1 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors "
|
||||
onClick={openChatAI}
|
||||
>
|
||||
<AudioLines className="w-4 h-4 text-[#000] dark:text-[#d8d8d8]" />
|
||||
</button>
|
||||
<button className="inline-flex items-center p-1 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 transition-color">
|
||||
<Image className="w-4 h-4 text-[#000] dark:text-[#d8d8d8]" />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<ChatSwitch
|
||||
isChatMode={isChatMode}
|
||||
onChange={(value) => {
|
||||
changeMode(value);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,42 +1,25 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import {
|
||||
Mic,
|
||||
Library,
|
||||
AudioLines,
|
||||
SquareChevronLeft,
|
||||
Send,
|
||||
Plus,
|
||||
Image,
|
||||
} from "lucide-react";
|
||||
import {
|
||||
WebviewWindow,
|
||||
getCurrentWebviewWindow,
|
||||
} from "@tauri-apps/api/webviewWindow";
|
||||
import { LogicalSize } from "@tauri-apps/api/dpi";
|
||||
import { motion } from "framer-motion";
|
||||
import { useEffect, useState, useCallback } from "react";
|
||||
// import {
|
||||
// WebviewWindow,
|
||||
// getCurrentWebviewWindow,
|
||||
// } from "@tauri-apps/api/webviewWindow";
|
||||
// import { LogicalSize } from "@tauri-apps/api/dpi";
|
||||
|
||||
import DropdownList from "./DropdownList";
|
||||
import { Footer } from "./Footer";
|
||||
import ChatSwitch from "./ChatSwitch";
|
||||
import { SearchResults } from "./SearchResults";
|
||||
import { tauriFetch } from "../../api/tauriFetchClient";
|
||||
|
||||
interface Tag {
|
||||
id: string;
|
||||
text: string;
|
||||
}
|
||||
|
||||
interface SearchProps {
|
||||
changeMode: (isChatMode: boolean) => void;
|
||||
changeInput: (val: string) => void;
|
||||
isTransitioned: boolean;
|
||||
isChatMode: boolean;
|
||||
input: string;
|
||||
}
|
||||
|
||||
function Search({ changeMode, changeInput, isChatMode }: SearchProps) {
|
||||
const [tags, setTags] = useState<Tag[]>([]);
|
||||
function Search({ isTransitioned, isChatMode, input }: SearchProps) {
|
||||
const [suggests, setSuggests] = useState<any[]>([]);
|
||||
const [isSearchComplete, setIsSearchComplete] = useState(false);
|
||||
const [input, setInput] = useState("");
|
||||
const [selectedItem, setSelectedItem] = useState<any>(null);
|
||||
|
||||
const getSuggest = async () => {
|
||||
@@ -45,211 +28,61 @@ function Search({ changeMode, changeInput, isChatMode }: SearchProps) {
|
||||
url: `/query/_search?query=${input}`,
|
||||
method: "GET",
|
||||
});
|
||||
console.log("_suggest", response);
|
||||
console.log("_suggest", input, response);
|
||||
const data = response.data?.hits?.hits || [];
|
||||
if (data.length > 0) {
|
||||
await getCurrentWebviewWindow().setSize(new LogicalSize(680, 600));
|
||||
} else {
|
||||
await getCurrentWebviewWindow().setSize(new LogicalSize(680, 90));
|
||||
}
|
||||
// if (data.length > 0) {
|
||||
// await getCurrentWebviewWindow().setSize(new LogicalSize(680, 600));
|
||||
// } else {
|
||||
// await getCurrentWebviewWindow().setSize(new LogicalSize(680, 90));
|
||||
// }
|
||||
setSuggests(data);
|
||||
setIsSearchComplete(true);
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch user data:", error);
|
||||
}
|
||||
};
|
||||
const getDataList = async () => {
|
||||
if (isChatMode) {
|
||||
changeInput(input);
|
||||
} else {
|
||||
getSuggest();
|
||||
// setTags([...tags, { id: Date.now().toString(), text: input.trim() }]);
|
||||
// setInput("");
|
||||
}
|
||||
};
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||
if (e.key === "Enter") {
|
||||
getDataList();
|
||||
}
|
||||
};
|
||||
|
||||
const removeTag = async (tagId: string) => {
|
||||
const newTag = tags.filter((tag) => tag.id !== tagId);
|
||||
setTags(newTag);
|
||||
if (newTag.length === 0) {
|
||||
await getCurrentWebviewWindow().setSize(new LogicalSize(680, 90));
|
||||
}
|
||||
};
|
||||
|
||||
async function openChatAI() {
|
||||
return;
|
||||
const webview = new WebviewWindow("chat", {
|
||||
title: "Coco AI",
|
||||
dragDropEnabled: true,
|
||||
center: true,
|
||||
width: 900,
|
||||
height: 700,
|
||||
alwaysOnTop: true,
|
||||
skipTaskbar: true,
|
||||
decorations: true,
|
||||
closable: true,
|
||||
url: "/ui/chat",
|
||||
});
|
||||
webview.once("tauri://created", function () {
|
||||
console.log("webview created");
|
||||
});
|
||||
webview.once("tauri://error", function (e) {
|
||||
console.log("error creating webview", e);
|
||||
});
|
||||
function debounce(fn: Function, delay: number) {
|
||||
let timer: NodeJS.Timeout;
|
||||
return (...args: any[]) => {
|
||||
clearTimeout(timer);
|
||||
timer = setTimeout(() => fn(...args), delay);
|
||||
};
|
||||
}
|
||||
|
||||
const debouncedSearch = useCallback(debounce(getSuggest, 300), [input]);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedItem) {
|
||||
setTags([]);
|
||||
}
|
||||
}, [selectedItem]);
|
||||
!isChatMode && debouncedSearch();
|
||||
}, [input]);
|
||||
|
||||
if (suggests.length === 0) return null;
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{ duration: 0 }}
|
||||
className={`min-h-screen flex items-start justify-center rounded-xl overflow-hidden`}
|
||||
<div
|
||||
className={`absolute w-full transition-opacity duration-500 ${
|
||||
isTransitioned ? "opacity-0 pointer-events-none" : "opacity-100"
|
||||
} bottom-0 h-[500px]`}
|
||||
>
|
||||
<div className="w-full rounded-xl overflow-hidden">
|
||||
<div className="bg-inputbox_bg_light dark:bg-inputbox_bg_dark bg-cover rounded-xl">
|
||||
{/* Search Bar */}
|
||||
<div className="relative">
|
||||
<div className="p-[13px] flex items-center dark:text-[#D8D8D8] bg-white dark:bg-[#202126] rounded-xl transition-all">
|
||||
<div className="flex flex-wrap gap-2 flex-1 h-6 items-center">
|
||||
{/* {tags.map((tag) => (
|
||||
<span
|
||||
key={tag.id}
|
||||
className="inline-flex items-center bg-blue-50 dark:bg-blue-900/50 text-blue-600 dark:text-blue-400 px-2.5 py-1 rounded-lg text-sm"
|
||||
>
|
||||
{tag.text}
|
||||
<button
|
||||
onClick={() => removeTag(tag.id)}
|
||||
className="ml-1.5 text-blue-400 hover:text-blue-600 dark:text-blue-500 dark:hover:text-blue-400"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</span>
|
||||
))} */}
|
||||
{!isChatMode && selectedItem ? (
|
||||
<SquareChevronLeft
|
||||
className="cursor-pointer text-gray-400 dark:text-gray-500"
|
||||
onClick={() => setSelectedItem(null)}
|
||||
/>
|
||||
) : null}
|
||||
<input
|
||||
type="text"
|
||||
className="text-xs font-normal flex-1 outline-none min-w-[200px] text-[#333] dark:text-[#d8d8d8] placeholder-text-xs placeholder-[#999] dark:placeholder-gray-500 bg-transparent"
|
||||
placeholder="Ask whatever you want....."
|
||||
value={input}
|
||||
onChange={(e) => {
|
||||
setInput(e.target.value);
|
||||
setIsSearchComplete(false);
|
||||
}}
|
||||
onKeyDown={handleKeyDown}
|
||||
/>
|
||||
</div>
|
||||
{/* <button className="p-2 hover:bg-gray-50 dark:hover:bg-gray-700 rounded-full transition-colors">
|
||||
<Mic className="w-3 h-3 text-[#333] dark:text-gray-500" />
|
||||
</button> */}
|
||||
{/* <button
|
||||
className={`ml-1 p-2 ${
|
||||
input ? "bg-[rgba(66,133,244,1)]" : "bg-[#E4E5F0]"
|
||||
} rounded-full transition-colors`}
|
||||
onClick={() => getDataList()}
|
||||
>
|
||||
<Send className="w-3 h-3 text-white hover:text-[#333]" />
|
||||
</button> */}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Controls */}
|
||||
<div className="h-10 px-3 flex justify-between items-center rounded-xl overflow-hidden">
|
||||
{isChatMode ? (
|
||||
<div className="flex gap-1 text-xs text-[#101010] dark:text-gray-300">
|
||||
<button
|
||||
className="inline-flex items-center p-1 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors "
|
||||
onClick={openChatAI}
|
||||
>
|
||||
<Library className="w-3 h-3 mr-1 text-[#000] dark:text-[#d8d8d8]" />
|
||||
Coco
|
||||
</button>
|
||||
<button className="inline-flex items-center p-1 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 transition-color">
|
||||
<Plus className="w-3 h-3 mr-1 text-[#000] dark:text-[#d8d8d8]" />
|
||||
Upload
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex gap-1">
|
||||
<button
|
||||
className="inline-flex items-center p-1 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors "
|
||||
onClick={openChatAI}
|
||||
>
|
||||
<AudioLines className="w-3.5 h-3 text-[#000] dark:text-[#d8d8d8]" />
|
||||
</button>
|
||||
<button className="inline-flex items-center p-1 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 transition-color">
|
||||
<Image className="w-3 h-3 text-[#000] dark:text-[#d8d8d8]" />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Switch */}
|
||||
<ChatSwitch
|
||||
isChat={isChatMode}
|
||||
onChange={(value) => {
|
||||
changeMode(value);
|
||||
setInput("");
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={`min-h-full w-full flex items-start justify-center rounded-xl overflow-hidden relative`}
|
||||
>
|
||||
{/* Search Results Panel */}
|
||||
{!isChatMode && suggests.length > 0 ? (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -20 }}
|
||||
transition={{ delay: 0.2 }}
|
||||
>
|
||||
<DropdownList
|
||||
suggests={suggests}
|
||||
isSearchComplete={isSearchComplete}
|
||||
selected={(item) => setSelectedItem(item)}
|
||||
/>
|
||||
</motion.div>
|
||||
{suggests.length > 0 ? (
|
||||
<DropdownList
|
||||
suggests={suggests}
|
||||
isSearchComplete={isSearchComplete}
|
||||
selected={(item) => setSelectedItem(item)}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{!isChatMode && selectedItem ? (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -20 }}
|
||||
transition={{ delay: 0.2 }}
|
||||
>
|
||||
<SearchResults />
|
||||
</motion.div>
|
||||
{selectedItem ? <SearchResults /> : null}
|
||||
|
||||
{suggests.length > 0 || selectedItem ? (
|
||||
<Footer isChat={false} name={selectedItem?.source} />
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{!isChatMode && (suggests.length > 0 || selectedItem) ? (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: 20 }}
|
||||
transition={{ delay: 0.2 }}
|
||||
>
|
||||
<Footer isChat={false} name={selectedItem?.source} />
|
||||
</motion.div>
|
||||
) : null}
|
||||
</motion.div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
48
src/components/SearchChat/Transition.tsx
Normal file
@@ -0,0 +1,48 @@
|
||||
import { useState } from "react";
|
||||
|
||||
const TransitionComponent = () => {
|
||||
const [isTransitioned, setIsTransitioned] = useState(false);
|
||||
|
||||
const handleToggle = () => {
|
||||
setIsTransitioned(!isTransitioned);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
data-tauri-drag-region
|
||||
className="w-[680px] h-[600px] mx-auto bg-gray-100 overflow-hidden relative"
|
||||
>
|
||||
<div
|
||||
data-tauri-drag-region
|
||||
className={`absolute w-full bg-red-500 text-white flex items-center justify-center transition-all duration-500 ${
|
||||
isTransitioned ? "top-[510px] h-[90px]" : "top-0 h-[90px]"
|
||||
}`}
|
||||
>
|
||||
<button
|
||||
className="px-4 py-2 bg-white text-black rounded"
|
||||
onClick={handleToggle}
|
||||
>
|
||||
Toggle
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div
|
||||
data-tauri-drag-region
|
||||
className={`absolute w-full bg-green-500 transition-opacity duration-500 ${
|
||||
isTransitioned ? "opacity-0 pointer-events-none" : "opacity-100"
|
||||
} bottom-0 h-[500px]`}
|
||||
></div>
|
||||
|
||||
<div
|
||||
data-tauri-drag-region
|
||||
className={`absolute w-full bg-yellow-500 transition-all duration-500 ${
|
||||
isTransitioned
|
||||
? "top-0 opacity-100 pointer-events-auto"
|
||||
: "-top-[510px] opacity-0 pointer-events-none"
|
||||
} h-[500px]`}
|
||||
></div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TransitionComponent;
|
||||
@@ -1,44 +1,100 @@
|
||||
import { useState } from "react";
|
||||
import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow";
|
||||
import { LogicalSize } from "@tauri-apps/api/dpi";
|
||||
import { AnimatePresence, LayoutGroup } from "framer-motion";
|
||||
import { useEffect, useState, useRef } from "react";
|
||||
// import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow";
|
||||
// import { LogicalSize } from "@tauri-apps/api/dpi";
|
||||
|
||||
import InputBox from "./InputBox";
|
||||
import Search from "./Search";
|
||||
import ChatAI from "../ChatAI/Chat";
|
||||
import ChatAI, { ChatAIRef } from "../ChatAI/Chat";
|
||||
|
||||
export default function SearchChat() {
|
||||
const chatAIRef = useRef<ChatAIRef>(null);
|
||||
|
||||
const [isChatMode, setIsChatMode] = useState(false);
|
||||
const [input, setInput] = useState("");
|
||||
const [isTransitioned, setIsTransitioned] = useState(false);
|
||||
|
||||
async function setWindowSize() {
|
||||
if (isTransitioned) {
|
||||
// await getCurrentWebviewWindow()?.setSize(new LogicalSize(680, 600));
|
||||
} else {
|
||||
// await getCurrentWebviewWindow()?.setSize(new LogicalSize(680, 90));
|
||||
}
|
||||
}
|
||||
useEffect(() => {
|
||||
setWindowSize();
|
||||
}, [isTransitioned]);
|
||||
|
||||
async function changeMode(value: boolean) {
|
||||
setIsChatMode(value);
|
||||
setInput("");
|
||||
if (!value) {
|
||||
await getCurrentWebviewWindow()?.setSize(new LogicalSize(680, 90));
|
||||
setIsTransitioned(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function changeInput(value: string) {
|
||||
setInput(value);
|
||||
if (isChatMode) {
|
||||
await getCurrentWebviewWindow()?.setSize(new LogicalSize(680, 600));
|
||||
}
|
||||
}
|
||||
|
||||
const handleSendMessage = async (value: string) => {
|
||||
setInput(value);
|
||||
if (isChatMode) {
|
||||
setIsTransitioned(true);
|
||||
chatAIRef.current?.init();
|
||||
}
|
||||
};
|
||||
const cancelChat = () => {};
|
||||
const setIsTyping = (value: any) => {
|
||||
console.log(value);
|
||||
};
|
||||
const isTyping = false;
|
||||
|
||||
return (
|
||||
<LayoutGroup>
|
||||
<AnimatePresence mode="wait">
|
||||
{isChatMode && input ? (
|
||||
<ChatAI key="ChatAI" inputValue={input} changeMode={changeMode} />
|
||||
) : (
|
||||
<Search
|
||||
key="Search"
|
||||
isChatMode={isChatMode}
|
||||
changeMode={changeMode}
|
||||
changeInput={changeInput}
|
||||
/>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</LayoutGroup>
|
||||
<div
|
||||
data-tauri-drag-region
|
||||
className={`w-full h-full min-h-screen mx-auto overflow-hidden relative`}
|
||||
>
|
||||
<div
|
||||
className={`absolute z-100 w-full flex items-center justify-center duration-500 ${
|
||||
isTransitioned ? "top-[510px] h-[90px]" : "top-0 h-[90px]"
|
||||
}`}
|
||||
>
|
||||
<InputBox
|
||||
isChatMode={isChatMode}
|
||||
inputValue={input}
|
||||
onSend={handleSendMessage}
|
||||
disabled={isTyping}
|
||||
disabledChange={(value) => {
|
||||
cancelChat();
|
||||
setIsTyping(value);
|
||||
}}
|
||||
changeMode={changeMode}
|
||||
changeInput={changeInput}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={`absolute w-full transition-all duration-500 ${
|
||||
isTransitioned
|
||||
? "top-0 opacity-100 pointer-events-auto"
|
||||
: "-top-[510px] opacity-0 pointer-events-none"
|
||||
} h-[500px]`}
|
||||
>
|
||||
<ChatAI
|
||||
ref={chatAIRef}
|
||||
key="ChatAI"
|
||||
inputValue={input}
|
||||
isTransitioned={isTransitioned}
|
||||
changeInput={changeInput}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Search
|
||||
key="Search"
|
||||
input={input}
|
||||
isChatMode={isChatMode}
|
||||
isTransitioned={isTransitioned}
|
||||
changeInput={changeInput}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ interface SettingsPanelProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
const SettingsPanel: React.FC<SettingsPanelProps> = ({ title, children }) => {
|
||||
const SettingsPanel: React.FC<SettingsPanelProps> = ({ children }) => {
|
||||
return (
|
||||
<div className="bg-white dark:bg-gray-800 rounded-xl p-6 shadow-sm">
|
||||
{/* <h2 className="text-xl font-semibold mb-6">{title}</h2> */}
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import React, { useContext } from 'react';
|
||||
import { Menu } from '@headlessui/react';
|
||||
import { Monitor, Moon, Sun } from 'lucide-react';
|
||||
import { Theme, ThemeContext } from './index2';
|
||||
import { useContext } from "react";
|
||||
import { Menu } from "@headlessui/react";
|
||||
import { Monitor, Moon, Sun } from "lucide-react";
|
||||
import { Theme, ThemeContext } from "./index2";
|
||||
|
||||
const ThemeSelector = () => {
|
||||
const { theme, setTheme } = useContext(ThemeContext);
|
||||
|
||||
const themes: { value: Theme; label: string; icon: any }[] = [
|
||||
{ value: 'light', label: 'Light', icon: Sun },
|
||||
{ value: 'dark', label: 'Dark', icon: Moon },
|
||||
{ value: 'system', label: 'System', icon: Monitor },
|
||||
{ value: "light", label: "Light", icon: Sun },
|
||||
{ value: "dark", label: "Dark", icon: Moon },
|
||||
{ value: "system", label: "System", icon: Monitor },
|
||||
];
|
||||
|
||||
const currentTheme = themes.find((t) => t.value === theme);
|
||||
@@ -29,8 +29,8 @@ const ThemeSelector = () => {
|
||||
onClick={() => setTheme(item.value)}
|
||||
className={`${
|
||||
active
|
||||
? 'bg-gray-100 dark:bg-gray-700'
|
||||
: 'text-gray-900 dark:text-gray-100'
|
||||
? "bg-gray-100 dark:bg-gray-700"
|
||||
: "text-gray-900 dark:text-gray-100"
|
||||
} group flex w-full items-center rounded-md px-2 py-2 text-sm`}
|
||||
>
|
||||
<item.icon className="w-4 h-4 mr-2" />
|
||||
@@ -45,4 +45,4 @@ const ThemeSelector = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default ThemeSelector;
|
||||
export default ThemeSelector;
|
||||
|
||||
@@ -14,7 +14,9 @@ type ThemeProviderState = {
|
||||
setTheme: (theme: Theme) => void;
|
||||
};
|
||||
|
||||
const ThemeProviderContext = createContext<ThemeProviderState | undefined>(undefined);
|
||||
const ThemeProviderContext = createContext<ThemeProviderState | undefined>(
|
||||
undefined
|
||||
);
|
||||
|
||||
export function ThemeProvider({
|
||||
children,
|
||||
@@ -29,21 +31,20 @@ export function ThemeProvider({
|
||||
if (storedTheme) {
|
||||
setThemeState(storedTheme);
|
||||
}
|
||||
|
||||
|
||||
let unlistenThemeChanges: (() => void) | undefined;
|
||||
|
||||
|
||||
const setupThemeListener = async () => {
|
||||
unlistenThemeChanges = await listenForThemeChanges();
|
||||
};
|
||||
|
||||
|
||||
setupThemeListener();
|
||||
|
||||
|
||||
return () => {
|
||||
// Cleanup listeners on unmount
|
||||
unlistenThemeChanges?.();
|
||||
};
|
||||
}, [storageKey]);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
applyTheme(theme);
|
||||
@@ -76,12 +77,14 @@ export function ThemeProvider({
|
||||
}
|
||||
|
||||
// Listen for theme changes
|
||||
const unlisten = await currentWindow.onThemeChanged(({ payload: newTheme }) => {
|
||||
if (theme === "system") {
|
||||
applyTheme(newTheme as Theme);
|
||||
console.log("New theme: " + theme);
|
||||
const unlisten = await currentWindow.onThemeChanged(
|
||||
({ payload: newTheme }) => {
|
||||
if (theme === "system") {
|
||||
applyTheme(newTheme as Theme);
|
||||
console.log("New theme: " + theme);
|
||||
}
|
||||
}
|
||||
});
|
||||
);
|
||||
|
||||
return () => {
|
||||
unlisten();
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import { Link, useLocation, useNavigate } from "react-router-dom";
|
||||
import { Link, useNavigate } from "react-router-dom";
|
||||
|
||||
export default function Header() {
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const showBack = location.pathname !== "/ui";
|
||||
|
||||
return (
|
||||
<div>
|
||||
|
||||
@@ -5,6 +5,7 @@ import ErrorPage from "../error-page";
|
||||
// import Settings from "../components/Settings";
|
||||
import Settings2 from "../components/Settings/index2";
|
||||
import SearchChat from "../components/SearchChat";
|
||||
import Transition from "../components/SearchChat/Transition";
|
||||
import ChatAI from "../components/ChatAI";
|
||||
import MySearch from "../components/MySearch";
|
||||
import Layout from "./Layout";
|
||||
@@ -19,6 +20,7 @@ export const router = createBrowserRouter([
|
||||
{ path: "/ui/settings", element: <Settings2 /> },
|
||||
{ path: "/ui/chat", element: <ChatAI /> },
|
||||
{ path: "/ui/search", element: <MySearch /> },
|
||||
{ path: "/ui/transition", element: <Transition /> },
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
||||
27
src/stores/chatStore.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { create } from "zustand";
|
||||
import {
|
||||
persist,
|
||||
// createJSONStorage
|
||||
} from "zustand/middleware";
|
||||
|
||||
export type IChatStore = {
|
||||
curChatEnd: boolean;
|
||||
setCurChatEnd: (value: boolean) => void;
|
||||
};
|
||||
|
||||
export const useChatStore = create<IChatStore>()(
|
||||
persist(
|
||||
(set) => ({
|
||||
curChatEnd: true,
|
||||
setCurChatEnd: (value: boolean) => set(() => ({ curChatEnd: value })),
|
||||
}),
|
||||
{
|
||||
name: "chat-state",
|
||||
// storage: createJSONStorage(() => sessionStorage),
|
||||
partialize: (state) =>
|
||||
Object.fromEntries(
|
||||
Object.entries(state).filter(([key]) => key === "curChatEnd")
|
||||
),
|
||||
}
|
||||
)
|
||||
);
|
||||
@@ -1,5 +1,8 @@
|
||||
import { create } from "zustand";
|
||||
import { persist, createJSONStorage } from "zustand/middleware";
|
||||
import {
|
||||
persist,
|
||||
// createJSONStorage
|
||||
} from "zustand/middleware";
|
||||
|
||||
export type ITheme = "dark" | "light" | "system";
|
||||
|
||||
@@ -18,7 +21,7 @@ export const useThemeStore = create<IThemeStore>()(
|
||||
}),
|
||||
{
|
||||
name: "active-theme",
|
||||
// storage: createJSONStorage(() => sessionStorage),
|
||||
// storage: createJSONStorage(() => sessionStorage),
|
||||
partialize: (state) =>
|
||||
Object.fromEntries(
|
||||
Object.entries(state).filter(([key]) => key === "activeTheme")
|
||||
|
||||