chore: add websocket & http api about chat AI (#7)

* chore: add websocket & http api about chat AI

* feat: Chat AI API debug

* chore: modify the config
This commit is contained in:
BiggerRain
2024-11-09 11:30:36 +08:00
committed by GitHub
parent b52c604a2d
commit 76d06dc3fe
20 changed files with 1297 additions and 154 deletions

View File

@@ -12,8 +12,10 @@
"dependencies": {
"@headlessui/react": "^2.1.10",
"@tauri-apps/api": ">=2.0.0",
"@tauri-apps/plugin-http": "~2",
"@tauri-apps/plugin-http": "~2.0.1",
"@tauri-apps/plugin-shell": ">=2.0.0",
"@tauri-apps/plugin-websocket": "~2",
"axios": "^1.7.7",
"clsx": "^2.1.1",
"framer-motion": "^11.11.11",
"i18next": "^23.16.2",
@@ -23,6 +25,7 @@
"react-dom": "^18.2.0",
"react-hotkeys-hook": "^4.5.1",
"react-i18next": "^15.1.0",
"react-markdown": "^9.0.1",
"react-router-dom": "^6.27.0",
"zustand": "^5.0.0"
},

751
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

73
src-tauri/Cargo.lock generated
View File

@@ -350,6 +350,7 @@ dependencies = [
"tauri-nspanel",
"tauri-plugin-http",
"tauri-plugin-shell",
"tauri-plugin-websocket",
]
[[package]]
@@ -591,6 +592,12 @@ dependencies = [
"syn 2.0.79",
]
[[package]]
name = "data-encoding"
version = "2.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8566979429cf69b49a5c740c60791108e86440e8be149bbea4fe54d2c32d6e2"
[[package]]
name = "data-url"
version = "0.3.1"
@@ -3043,6 +3050,17 @@ dependencies = [
"stable_deref_trait",
]
[[package]]
name = "sha1"
version = "0.10.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba"
dependencies = [
"cfg-if",
"cpufeatures",
"digest",
]
[[package]]
name = "sha2"
version = "0.10.8"
@@ -3547,6 +3565,25 @@ dependencies = [
"tokio",
]
[[package]]
name = "tauri-plugin-websocket"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2793b69e1dd494beed1e0a29865f38bd00011e7ccc35a3cfde8e3939328b790"
dependencies = [
"futures-util",
"http",
"log",
"rand 0.8.5",
"serde",
"serde_json",
"tauri",
"tauri-plugin",
"thiserror",
"tokio",
"tokio-tungstenite",
]
[[package]]
name = "tauri-runtime"
version = "2.1.0"
@@ -3759,6 +3796,22 @@ dependencies = [
"tokio",
]
[[package]]
name = "tokio-tungstenite"
version = "0.24.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "edc5f74e248dc973e0dbb7b74c7e0d6fcc301c694ff50049504004ef4d0cdcd9"
dependencies = [
"futures-util",
"log",
"rustls",
"rustls-pki-types",
"tokio",
"tokio-rustls",
"tungstenite",
"webpki-roots",
]
[[package]]
name = "tokio-util"
version = "0.7.12"
@@ -3883,6 +3936,26 @@ version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
[[package]]
name = "tungstenite"
version = "0.24.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "18e5b8366ee7a95b16d32197d0b2604b43a0be89dc5fac9f8e96ccafbaedda8a"
dependencies = [
"byteorder",
"bytes",
"data-encoding",
"http",
"httparse",
"log",
"rand 0.8.5",
"rustls",
"rustls-pki-types",
"sha1",
"thiserror",
"utf-8",
]
[[package]]
name = "typeid"
version = "1.0.2"

View File

@@ -25,6 +25,7 @@ serde_json = "1"
tauri-plugin-http = "2"
tauri-nspanel = { git = "https://github.com/ahkohd/tauri-nspanel", rev = "005240c" }
tauri-plugin-websocket = "2"
[profile.dev]
incremental = true # Compile your binary in smaller steps.

View File

@@ -5,7 +5,7 @@
"windows": ["main"],
"permissions": [
"core:event:allow-listen",
"core:window:default",
"core:window:default",
"core:window:allow-start-dragging",
"core:webview:allow-create-webview",
"core:window:allow-show",
@@ -24,8 +24,15 @@
"http:allow-fetch-send",
{
"identifier": "http:default",
"allow": [{ "url": "https://*.tauri.app" }],
"deny": [{ "url": "https://private.tauri.app" }]
}
"allow": [
{
"url": "http://localhost:2900"
}
],
"deny": []
},
"websocket:default",
"websocket:allow-connect",
"websocket:allow-send"
]
}

View File

@@ -12,6 +12,8 @@ fn main() {
coco_lib::run();
tauri::Builder::default()
.plugin(tauri_plugin_websocket::init())
.plugin(tauri_plugin_http::init())
.plugin(tauri_nspanel::init())
.invoke_handler(tauri::generate_handler![
show_panel,

View File

@@ -14,15 +14,13 @@
"windows": [
{
"title": "Coco AI",
"width": 800,
"height": 110,
"maxHeight": 600,
"width": 900,
"height": 800,
"maxHeight": 800,
"transparent": true,
"resizable": true,
"fullscreen": false,
"decorations": false,
"label": "main",
"url": "/"
"decorations": false
}
],
"security": {
@@ -46,6 +44,7 @@
]
},
"plugins": {
"window": {}
"window": {},
"websocket": {}
}
}

View File

@@ -0,0 +1,73 @@
import { fetch } from "@tauri-apps/plugin-http";
const baseURL = "http://localhost:2900";
interface FetchRequestConfig {
url: string;
method?: "GET" | "POST" | "PUT" | "DELETE";
headers?: Record<string, string>;
body?: any;
timeout?: number;
parseAs?: "json" | "text" | "binary";
}
interface FetchResponse<T = any> {
data: T;
status: number;
statusText: string;
headers: Headers;
}
const timeoutPromise = (ms: number) => {
return new Promise<never>((_, reject) =>
setTimeout(() => reject(new Error(`Request timed out after ${ms} ms`)), ms)
);
};
export const tauriFetch = async <T = any>({
url,
method = "GET",
headers = {},
body,
timeout = 30,
parseAs = "json",
}: FetchRequestConfig): Promise<FetchResponse<T>> => {
try {
url = baseURL + url;
if (method !== "GET") {
headers["Content-Type"] = "application/json";
}
const fetchPromise = fetch(url, {
method,
headers,
body,
});
const response = await Promise.race([
fetchPromise,
timeoutPromise(timeout * 1000),
]);
const statusText = response.ok ? "OK" : "Error";
let data: any;
if (parseAs === "json") {
data = await response.json();
} else if (parseAs === "text") {
data = await response.text();
} else {
data = await response.arrayBuffer();
}
return {
data,
status: response.status,
statusText,
headers: response.headers,
};
} catch (error) {
console.error("Request failed:", error);
throw error;
}
};

View File

@@ -1,4 +1,4 @@
import { SendHorizontal } from "lucide-react";
import { SendHorizontal, OctagonX } from "lucide-react";
import {
useState,
type FormEvent,
@@ -9,10 +9,15 @@ import {
interface ChatInputProps {
onSend: (message: string) => void;
disabled?: boolean;
disabled: boolean;
disabledChange: (disabled: boolean) => void;
}
export function ChatInput({ onSend, disabled }: ChatInputProps) {
export function ChatInput({
onSend,
disabled,
disabledChange,
}: ChatInputProps) {
const [input, setInput] = useState("");
const textareaRef = useRef<HTMLTextAreaElement>(null);
@@ -62,13 +67,25 @@ export function ChatInput({ onSend, disabled }: ChatInputProps) {
className="w-full resize-none rounded-lg border-0 bg-gray-50 dark:bg-gray-800/50 py-3 pl-4 pr-12 text-sm leading-6 text-gray-900 dark:text-gray-100 shadow-sm ring-1 ring-gray-200 dark:ring-gray-700 placeholder:text-gray-400 dark:placeholder:text-gray-500 focus:ring-2 focus:ring-indigo-500 dark:focus:ring-indigo-400 transition-shadow"
disabled={disabled}
/>
<button
type="submit"
disabled={disabled || !input.trim()}
className="absolute right-2 bottom-2.5 rounded-md p-2 text-gray-400 hover:text-gray-600 dark:text-gray-500 dark:hover:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700/50 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
>
<SendHorizontal className="h-5 w-5" />
</button>
{disabled ? (
<button
type="submit"
className="absolute right-2 bottom-2.5 rounded-md p-2 text-gray-400 hover:text-gray-600 dark:text-gray-500 dark:hover:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700/50 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
>
<OctagonX
className="h-5 w-5"
onClick={() => disabledChange(false)}
/>
</button>
) : (
<button
type="submit"
disabled={disabled || !input.trim()}
className="absolute right-2 bottom-2.5 rounded-md p-2 text-gray-400 hover:text-gray-600 dark:text-gray-500 dark:hover:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700/50 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
>
<SendHorizontal className="h-5 w-5" />
</button>
)}
</div>
<p className="mt-2 text-xs text-gray-500 dark:text-gray-400">
Press Enter to send, Shift + Enter for new line

View File

@@ -11,15 +11,18 @@ interface ChatMessageProps {
export function ChatMessage({ message, isTyping }: ChatMessageProps) {
const [isAnimationComplete, setIsAnimationComplete] = useState(!isTyping);
const isAssistant = message.role === "assistant";
const isAssistant = message._source?.type === "assistant";
return (
<div
className={`py-8 ${
isAssistant ? "bg-gray-50/50 dark:bg-gray-800/30" : ""
}`}
className={`py-8 flex ${isAssistant ? "justify-start" : "justify-end"}`}
>
<div className="max-w-3xl mx-auto px-4 sm:px-6 lg:px-8 flex gap-6">
<div
className={`max-w-3xl px-4 sm:px-6 lg:px-8 flex gap-4 ${
isAssistant ? "" : "flex-row-reverse"
}`}
>
{/* 头像部分 */}
<div
className={`flex-shrink-0 h-8 w-8 rounded-lg flex items-center justify-center ${
isAssistant
@@ -33,7 +36,13 @@ export function ChatMessage({ message, isTyping }: ChatMessageProps) {
<User className="h-5 w-5 text-white" />
)}
</div>
<div className="flex-1 space-y-2">
{/* 消息内容 */}
<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>
@@ -42,7 +51,7 @@ export function ChatMessage({ message, isTyping }: ChatMessageProps) {
{isTyping && isAssistant ? (
<>
<TypingAnimation
text={message.content}
text={message._source?.message || ""}
onComplete={() => setIsAnimationComplete(true)}
/>
{!isAnimationComplete && (
@@ -50,7 +59,7 @@ export function ChatMessage({ message, isTyping }: ChatMessageProps) {
)}
</>
) : (
message.content
message._source?.message || ""
)}
</p>
</div>

View File

@@ -42,9 +42,9 @@ export function Sidebar({
<div className="flex-1 overflow-y-auto px-3 pb-3 space-y-1">
{chats.map((chat) => (
<div
key={chat.id}
key={chat._id}
className={`group relative rounded-lg transition-all ${
activeChat.id === chat.id
activeChat._id === chat._id
? isDark
? "bg-gray-700/50 text-white"
: "bg-white text-gray-900 shadow-sm"
@@ -59,7 +59,7 @@ export function Sidebar({
>
<MessageSquare
className={`h-4 w-4 flex-shrink-0 ${
activeChat.id === chat.id
activeChat._id === chat._id
? isDark
? "text-indigo-400"
: "text-indigo-600"
@@ -68,11 +68,11 @@ export function Sidebar({
: "text-gray-500"
}`}
/>
<span className="truncate">{chat.title}</span>
<span className="truncate">{chat.title || chat._id}</span>
</button>
{chats.length > 1 && (
{/* {chats.length > 1 && (
<button
onClick={() => onDeleteChat(chat.id)}
onClick={() => onDeleteChat(chat._id)}
className={`absolute right-2 top-1/2 -translate-y-1/2 p-2 rounded-md opacity-0 group-hover:opacity-100 transition-all ${
isDark
? "hover:bg-gray-600 text-gray-400 hover:text-red-400"
@@ -81,8 +81,8 @@ export function Sidebar({
>
<Trash2 className="h-4 w-4" />
</button>
)}
{activeChat.id === chat.id && (
)} */}
{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"

View File

@@ -1,5 +1,7 @@
import { useEffect, useState } from "react";
import ReactMarkdown from "react-markdown";
import "./index.css";
interface TypingAnimationProps {
text: string;
onComplete?: () => void;
@@ -27,5 +29,7 @@ export function TypingAnimation({
}
}, [currentIndex, text, speed, onComplete]);
return <>{displayedText}</>;
// console.log("text", text);
return <ReactMarkdown>{text}</ReactMarkdown>;
}

View File

@@ -0,0 +1,8 @@
.markdown-content p {
margin: 1em 0;
line-height: 1.6;
}
.markdown-content strong {
font-weight: bold;
}

View File

@@ -1,41 +1,114 @@
import { useState, useRef, useEffect } from "react";
import { Menu } from "lucide-react";
import { Menu, Loader } from "lucide-react";
import { ThemeToggle } from "./ThemeToggle";
import { ChatMessage } from "./ChatMessage";
import { ChatInput } from "./ChatInput";
import { Sidebar } from "./Sidebar";
import type { Message, Chat } from "./types";
import type { Chat, Message } from "./types";
import { useTheme } from "../ThemeProvider";
import ChatSwitch from "../SearchChat/ChatSwitch";
import { Footer } from "../SearchChat/Footer";
const INITIAL_CHAT: Chat = {
id: "1",
title: "New Chat",
messages: [
{
id: "1",
role: "assistant",
content: "Hello! How can I help you today?",
timestamp: new Date(),
},
],
createdAt: new Date(),
};
import { tauriFetch } from "../../api/tauriFetchClient";
import { useWebSocket } from "../../hooks/useWebSocket";
interface ChatAIProps {
changeMode: (isChatMode: boolean) => void;
}
export default function ChatAI({ changeMode }: ChatAIProps) {
const [chats, setChats] = useState<Chat[]>([INITIAL_CHAT]);
const [activeChat, setActiveChat] = useState<Chat>(INITIAL_CHAT);
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 [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]);
}
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 "";
}
);
// websocket
// websocket
useEffect(() => {
if (messages.length === 0 || !activeChat?._id) return;
const simulateAssistantResponse = () => {
console.log("messages", messages);
const assistantMessage: Message = {
_id: activeChat._id,
_source: {
type: "assistant",
message: messages,
},
};
const updatedChat = {
...activeChat,
messages: [...(activeChat.messages || []), assistantMessage],
};
setMessages("");
setCurMessage("");
setActiveChat(updatedChat);
setTimeout(() => setIsTyping(false), 1000);
};
if (curChatEnd) {
simulateAssistantResponse();
}
}, [messages, isTyping, curChatEnd]);
// getChatHistory
useEffect(() => {
getChatHistory();
}, []);
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);
if (hits[0]) {
onSelectChat(hits[0]);
} else {
createNewChat();
}
} catch (error) {
console.error("Failed to fetch user data:", error);
}
};
const scrollToBottom = () => {
messagesEndRef.current?.scrollIntoView({
behavior: "smooth",
@@ -45,31 +118,28 @@ export default function ChatAI({ changeMode }: ChatAIProps) {
useEffect(() => {
scrollToBottom();
}, [activeChat.messages, isTyping]);
}, [activeChat?.messages, isTyping, curMessage]);
const createNewChat = () => {
const newChat: Chat = {
id: Date.now().toString(),
title: "New Chat",
messages: [
{
id: "1",
role: "assistant",
content: "Hello! How can I help you today?",
timestamp: new Date(),
},
],
createdAt: new Date(),
};
setChats((prev) => [newChat, ...prev]);
setActiveChat(newChat);
setIsSidebarOpen(false);
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);
} 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);
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 {
@@ -78,54 +148,79 @@ export default function ChatAI({ changeMode }: ChatAIProps) {
}
};
const handleSendMessage = (content: string) => {
const newMessage: Message = {
id: Date.now().toString(),
role: "user",
content,
timestamp: new Date(),
};
const updatedChat = {
...activeChat,
title:
activeChat.messages.length === 1
? content.slice(0, 30) + "..."
: activeChat.title,
messages: [...activeChat.messages, newMessage],
};
setActiveChat(updatedChat);
setChats((prev) =>
prev.map((chat) => (chat.id === activeChat.id ? updatedChat : chat))
);
setIsTyping(true);
// Simulate assistant response
setTimeout(() => {
const assistantMessage: Message = {
id: (Date.now() + 1).toString(),
role: "assistant",
content:
"This is a simulated response. In a real application, this would be connected to an AI backend.",
timestamp: new Date(),
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 }),
});
console.log("_send", response, websocketId);
const updatedChat: Chat = {
...activeChat,
messages: [...(activeChat?.messages || []), ...(response.data || [])],
};
const finalChat = {
...updatedChat,
messages: [...updatedChat.messages, assistantMessage],
};
setActiveChat(updatedChat);
setIsTyping(true);
setCurChatEnd(false);
} catch (error) {
console.error("Failed to fetch user data:", error);
}
};
setActiveChat(finalChat);
setChats((prev) =>
prev.map((chat) => (chat.id === activeChat.id ? finalChat : chat))
);
setTimeout(() => setIsTyping(false), 500);
}, 1000);
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);
}
};
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);
}
};
return (
<div className="h-screen pb-8">
<div className="h-screen pb-8 rounded-xl overflow-hidden">
<div className="h-[100%] flex">
{/* Sidebar */}
<div
@@ -135,17 +230,16 @@ export default function ChatAI({ changeMode }: ChatAIProps) {
theme === "dark" ? "bg-gray-800" : "bg-gray-100"
}`}
>
<Sidebar
chats={chats}
activeChat={activeChat}
isDark={theme === "dark"}
onNewChat={createNewChat}
onSelectChat={(chat: any) => {
setActiveChat(chat);
setIsSidebarOpen(false);
}}
onDeleteChat={deleteChat}
/>
{activeChat ? (
<Sidebar
chats={chats}
activeChat={activeChat}
isDark={theme === "dark"}
onNewChat={createNewChat}
onSelectChat={onSelectChat}
onDeleteChat={deleteChat}
/>
) : null}
</div>
{/* Main content */}
@@ -178,18 +272,44 @@ export default function ChatAI({ changeMode }: ChatAIProps) {
</header>
{/* Chat messages */}
<div className="flex-1 overflow-y-auto">
{activeChat.messages.map((message, index) => (
<div className="flex-1 overflow-y-auto custom-scrollbar">
{activeChat?.messages?.map((message, index) => (
<ChatMessage
key={message.id}
key={message._id + index}
message={message}
isTyping={
isTyping &&
index === activeChat.messages.length - 1 &&
message.role === "assistant"
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={isTyping}
/>
) : null}
{/* Loading */}
{/* {isTyping && (
<div className="flex pt-0 pb-4 pl-20">
<Loader className="animate-spin h-5 w-5 text-gray-500" />
</div>
)} */}
{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>
@@ -199,12 +319,16 @@ export default function ChatAI({ changeMode }: ChatAIProps) {
theme === "dark" ? "border-gray-800" : "border-gray-200"
}`}
>
<ChatInput onSend={handleSendMessage} disabled={isTyping} />
<ChatInput
onSend={handleSendMessage}
disabled={isTyping}
disabledChange={setIsTyping}
/>
</div>
</div>
</div>
<Footer isChat={true}/>
<Footer isChat={true} />
</div>
);
}

View File

@@ -1,13 +1,26 @@
export interface Message {
id: string;
role: 'user' | 'assistant';
content: string;
timestamp: Date;
_id: string;
_source: ISource;
[key: string]: any;
}
export interface ISource {
id?: string;
created?: string;
updated?: string;
status?: string;
session_id?: string;
type?: string;
message?: any;
}
export interface Chat {
id: string;
title: string;
messages: Message[];
createdAt: Date;
}
_id: string;
_index?: string;
_type?: string;
_source?: ISource;
_score?: number;
found?: boolean;
title?: string;
messages?: any[];
[key: string]: any;
}

View File

@@ -44,7 +44,7 @@ export const Footer = ({ isChat }: FooterProps) => {
return (
<div
style={{ zIndex: 999 }}
className="fixed bottom-0 left-0 right-0 bg-white dark:bg-gray-800 border-t border-gray-200 dark:border-gray-700 px-4 h-8 flex items-center justify-between"
className="fixed bottom-0 left-0 right-0 bg-white dark:bg-gray-800 border-t border-gray-200 dark:border-gray-700 px-4 h-8 flex items-center justify-between rounded-xl rounded-t-none overflow-hidden"
>
<div className="flex items-center">
<Menu as="div" className="relative">

View File

@@ -6,14 +6,13 @@ import Search from "./Search";
import ChatAI from "../ChatAI";
export default function SearchChat() {
const [isChatMode, setIsChatMode] = useState(false);
const [isChatMode, setIsChatMode] = useState(true);
async function changeMode(value: boolean) {
console.log(11111, value);
if (value) {
await getCurrentWebviewWindow().setSize(new LogicalSize(900, 700));
await getCurrentWebviewWindow()?.setSize(new LogicalSize(900, 800));
} else {
await getCurrentWebviewWindow().setSize(new LogicalSize(800, 110));
await getCurrentWebviewWindow()?.setSize(new LogicalSize(900, 110));
}
setIsChatMode(value);
}

55
src/hooks/useWebSocket.ts Normal file
View File

@@ -0,0 +1,55 @@
import { useEffect, useState } from "react";
export function useWebSocket(
url: string,
filterMessages?: (message: string) => string
) {
const [ws, setWs] = useState<WebSocket | null>(null);
const [messages, setMessages] = useState<string>("");
const [connected, setConnected] = useState(false);
useEffect(() => {
// 创建 WebSocket 连接
const websocket = new WebSocket(url);
websocket.onopen = () => {
console.log("WebSocket 连接成功");
setConnected(true);
};
websocket.onmessage = (event) => {
// console.log("收到消息:", event.data);
const data = filterMessages ? filterMessages(event.data) : event.data;
if (data) {
setMessages((prevMessages) => prevMessages + data);
}
};
websocket.onclose = () => {
console.log("WebSocket 连接关闭");
setConnected(false);
};
websocket.onerror = (error) => {
console.error("WebSocket 连接错误:", error);
};
// 将 WebSocket 实例保存在状态中
setWs(websocket);
// 在组件卸载时关闭 WebSocket 连接
return () => {
websocket.close();
};
}, [url]);
// 发送消息的函数
const sendMessage = (message: string) => {
if (ws && connected) {
ws.send(message);
console.log("发送消息:", message);
}
};
return { messages, connected, sendMessage, setMessages };
}

View File

@@ -26,7 +26,7 @@
}
.dark body {
@apply text-gray-100;
@apply text-gray-100 rounded-lg shadow-lg overflow-hidden antialiased;
}
}

View File

@@ -27,5 +27,12 @@ export default defineConfig(async () => ({
// 3. tell vite to ignore watching `src-tauri`
ignored: ["**/src-tauri/**"],
},
proxy: {
"/chat": {
target: "http://localhost:2900",
changeOrigin: true,
rewrite: (path) => path.replace(/^\/chat/, ""),
},
},
},
}));