mirror of
https://github.com/infinilabs/coco-app.git
synced 2025-12-16 03:27:43 +01:00
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:
@@ -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
751
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
73
src-tauri/Cargo.lock
generated
73
src-tauri/Cargo.lock
generated
@@ -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"
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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": {}
|
||||
}
|
||||
}
|
||||
73
src/api/tauriFetchClient.ts
Normal file
73
src/api/tauriFetchClient.ts
Normal 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;
|
||||
}
|
||||
};
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>;
|
||||
}
|
||||
|
||||
8
src/components/ChatAI/index.css
Normal file
8
src/components/ChatAI/index.css
Normal file
@@ -0,0 +1,8 @@
|
||||
.markdown-content p {
|
||||
margin: 1em 0;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.markdown-content strong {
|
||||
font-weight: bold;
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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
55
src/hooks/useWebSocket.ts
Normal 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 };
|
||||
}
|
||||
@@ -26,7 +26,7 @@
|
||||
}
|
||||
|
||||
.dark body {
|
||||
@apply text-gray-100;
|
||||
@apply text-gray-100 rounded-lg shadow-lg overflow-hidden antialiased;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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/, ""),
|
||||
},
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
Reference in New Issue
Block a user