mirror of
https://github.com/infinilabs/coco-app.git
synced 2025-12-16 11:37:47 +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": {
|
"dependencies": {
|
||||||
"@headlessui/react": "^2.1.10",
|
"@headlessui/react": "^2.1.10",
|
||||||
"@tauri-apps/api": ">=2.0.0",
|
"@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-shell": ">=2.0.0",
|
||||||
|
"@tauri-apps/plugin-websocket": "~2",
|
||||||
|
"axios": "^1.7.7",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"framer-motion": "^11.11.11",
|
"framer-motion": "^11.11.11",
|
||||||
"i18next": "^23.16.2",
|
"i18next": "^23.16.2",
|
||||||
@@ -23,6 +25,7 @@
|
|||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-hotkeys-hook": "^4.5.1",
|
"react-hotkeys-hook": "^4.5.1",
|
||||||
"react-i18next": "^15.1.0",
|
"react-i18next": "^15.1.0",
|
||||||
|
"react-markdown": "^9.0.1",
|
||||||
"react-router-dom": "^6.27.0",
|
"react-router-dom": "^6.27.0",
|
||||||
"zustand": "^5.0.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-nspanel",
|
||||||
"tauri-plugin-http",
|
"tauri-plugin-http",
|
||||||
"tauri-plugin-shell",
|
"tauri-plugin-shell",
|
||||||
|
"tauri-plugin-websocket",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -591,6 +592,12 @@ dependencies = [
|
|||||||
"syn 2.0.79",
|
"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]]
|
[[package]]
|
||||||
name = "data-url"
|
name = "data-url"
|
||||||
version = "0.3.1"
|
version = "0.3.1"
|
||||||
@@ -3043,6 +3050,17 @@ dependencies = [
|
|||||||
"stable_deref_trait",
|
"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]]
|
[[package]]
|
||||||
name = "sha2"
|
name = "sha2"
|
||||||
version = "0.10.8"
|
version = "0.10.8"
|
||||||
@@ -3547,6 +3565,25 @@ dependencies = [
|
|||||||
"tokio",
|
"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]]
|
[[package]]
|
||||||
name = "tauri-runtime"
|
name = "tauri-runtime"
|
||||||
version = "2.1.0"
|
version = "2.1.0"
|
||||||
@@ -3759,6 +3796,22 @@ dependencies = [
|
|||||||
"tokio",
|
"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]]
|
[[package]]
|
||||||
name = "tokio-util"
|
name = "tokio-util"
|
||||||
version = "0.7.12"
|
version = "0.7.12"
|
||||||
@@ -3883,6 +3936,26 @@ version = "0.2.5"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
|
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]]
|
[[package]]
|
||||||
name = "typeid"
|
name = "typeid"
|
||||||
version = "1.0.2"
|
version = "1.0.2"
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ serde_json = "1"
|
|||||||
tauri-plugin-http = "2"
|
tauri-plugin-http = "2"
|
||||||
|
|
||||||
tauri-nspanel = { git = "https://github.com/ahkohd/tauri-nspanel", rev = "005240c" }
|
tauri-nspanel = { git = "https://github.com/ahkohd/tauri-nspanel", rev = "005240c" }
|
||||||
|
tauri-plugin-websocket = "2"
|
||||||
|
|
||||||
[profile.dev]
|
[profile.dev]
|
||||||
incremental = true # Compile your binary in smaller steps.
|
incremental = true # Compile your binary in smaller steps.
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
"windows": ["main"],
|
"windows": ["main"],
|
||||||
"permissions": [
|
"permissions": [
|
||||||
"core:event:allow-listen",
|
"core:event:allow-listen",
|
||||||
"core:window:default",
|
"core:window:default",
|
||||||
"core:window:allow-start-dragging",
|
"core:window:allow-start-dragging",
|
||||||
"core:webview:allow-create-webview",
|
"core:webview:allow-create-webview",
|
||||||
"core:window:allow-show",
|
"core:window:allow-show",
|
||||||
@@ -24,8 +24,15 @@
|
|||||||
"http:allow-fetch-send",
|
"http:allow-fetch-send",
|
||||||
{
|
{
|
||||||
"identifier": "http:default",
|
"identifier": "http:default",
|
||||||
"allow": [{ "url": "https://*.tauri.app" }],
|
"allow": [
|
||||||
"deny": [{ "url": "https://private.tauri.app" }]
|
{
|
||||||
}
|
"url": "http://localhost:2900"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"deny": []
|
||||||
|
},
|
||||||
|
"websocket:default",
|
||||||
|
"websocket:allow-connect",
|
||||||
|
"websocket:allow-send"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ fn main() {
|
|||||||
coco_lib::run();
|
coco_lib::run();
|
||||||
|
|
||||||
tauri::Builder::default()
|
tauri::Builder::default()
|
||||||
|
.plugin(tauri_plugin_websocket::init())
|
||||||
|
.plugin(tauri_plugin_http::init())
|
||||||
.plugin(tauri_nspanel::init())
|
.plugin(tauri_nspanel::init())
|
||||||
.invoke_handler(tauri::generate_handler![
|
.invoke_handler(tauri::generate_handler![
|
||||||
show_panel,
|
show_panel,
|
||||||
|
|||||||
@@ -14,15 +14,13 @@
|
|||||||
"windows": [
|
"windows": [
|
||||||
{
|
{
|
||||||
"title": "Coco AI",
|
"title": "Coco AI",
|
||||||
"width": 800,
|
"width": 900,
|
||||||
"height": 110,
|
"height": 800,
|
||||||
"maxHeight": 600,
|
"maxHeight": 800,
|
||||||
"transparent": true,
|
"transparent": true,
|
||||||
"resizable": true,
|
"resizable": true,
|
||||||
"fullscreen": false,
|
"fullscreen": false,
|
||||||
"decorations": false,
|
"decorations": false
|
||||||
"label": "main",
|
|
||||||
"url": "/"
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"security": {
|
"security": {
|
||||||
@@ -46,6 +44,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"plugins": {
|
"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 {
|
import {
|
||||||
useState,
|
useState,
|
||||||
type FormEvent,
|
type FormEvent,
|
||||||
@@ -9,10 +9,15 @@ import {
|
|||||||
|
|
||||||
interface ChatInputProps {
|
interface ChatInputProps {
|
||||||
onSend: (message: string) => void;
|
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 [input, setInput] = useState("");
|
||||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
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"
|
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}
|
disabled={disabled}
|
||||||
/>
|
/>
|
||||||
<button
|
{disabled ? (
|
||||||
type="submit"
|
<button
|
||||||
disabled={disabled || !input.trim()}
|
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"
|
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" />
|
<OctagonX
|
||||||
</button>
|
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>
|
</div>
|
||||||
<p className="mt-2 text-xs text-gray-500 dark:text-gray-400">
|
<p className="mt-2 text-xs text-gray-500 dark:text-gray-400">
|
||||||
Press Enter to send, Shift + Enter for new line
|
Press Enter to send, Shift + Enter for new line
|
||||||
|
|||||||
@@ -11,15 +11,18 @@ interface ChatMessageProps {
|
|||||||
|
|
||||||
export function ChatMessage({ message, isTyping }: ChatMessageProps) {
|
export function ChatMessage({ message, isTyping }: ChatMessageProps) {
|
||||||
const [isAnimationComplete, setIsAnimationComplete] = useState(!isTyping);
|
const [isAnimationComplete, setIsAnimationComplete] = useState(!isTyping);
|
||||||
const isAssistant = message.role === "assistant";
|
const isAssistant = message._source?.type === "assistant";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`py-8 ${
|
className={`py-8 flex ${isAssistant ? "justify-start" : "justify-end"}`}
|
||||||
isAssistant ? "bg-gray-50/50 dark:bg-gray-800/30" : ""
|
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
<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
|
<div
|
||||||
className={`flex-shrink-0 h-8 w-8 rounded-lg flex items-center justify-center ${
|
className={`flex-shrink-0 h-8 w-8 rounded-lg flex items-center justify-center ${
|
||||||
isAssistant
|
isAssistant
|
||||||
@@ -33,7 +36,13 @@ export function ChatMessage({ message, isTyping }: ChatMessageProps) {
|
|||||||
<User className="h-5 w-5 text-white" />
|
<User className="h-5 w-5 text-white" />
|
||||||
)}
|
)}
|
||||||
</div>
|
</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">
|
<p className="font-medium text-sm text-gray-900 dark:text-gray-100">
|
||||||
{isAssistant ? "Assistant" : "You"}
|
{isAssistant ? "Assistant" : "You"}
|
||||||
</p>
|
</p>
|
||||||
@@ -42,7 +51,7 @@ export function ChatMessage({ message, isTyping }: ChatMessageProps) {
|
|||||||
{isTyping && isAssistant ? (
|
{isTyping && isAssistant ? (
|
||||||
<>
|
<>
|
||||||
<TypingAnimation
|
<TypingAnimation
|
||||||
text={message.content}
|
text={message._source?.message || ""}
|
||||||
onComplete={() => setIsAnimationComplete(true)}
|
onComplete={() => setIsAnimationComplete(true)}
|
||||||
/>
|
/>
|
||||||
{!isAnimationComplete && (
|
{!isAnimationComplete && (
|
||||||
@@ -50,7 +59,7 @@ export function ChatMessage({ message, isTyping }: ChatMessageProps) {
|
|||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
message.content
|
message._source?.message || ""
|
||||||
)}
|
)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -42,9 +42,9 @@ export function Sidebar({
|
|||||||
<div className="flex-1 overflow-y-auto px-3 pb-3 space-y-1">
|
<div className="flex-1 overflow-y-auto px-3 pb-3 space-y-1">
|
||||||
{chats.map((chat) => (
|
{chats.map((chat) => (
|
||||||
<div
|
<div
|
||||||
key={chat.id}
|
key={chat._id}
|
||||||
className={`group relative rounded-lg transition-all ${
|
className={`group relative rounded-lg transition-all ${
|
||||||
activeChat.id === chat.id
|
activeChat._id === chat._id
|
||||||
? isDark
|
? isDark
|
||||||
? "bg-gray-700/50 text-white"
|
? "bg-gray-700/50 text-white"
|
||||||
: "bg-white text-gray-900 shadow-sm"
|
: "bg-white text-gray-900 shadow-sm"
|
||||||
@@ -59,7 +59,7 @@ export function Sidebar({
|
|||||||
>
|
>
|
||||||
<MessageSquare
|
<MessageSquare
|
||||||
className={`h-4 w-4 flex-shrink-0 ${
|
className={`h-4 w-4 flex-shrink-0 ${
|
||||||
activeChat.id === chat.id
|
activeChat._id === chat._id
|
||||||
? isDark
|
? isDark
|
||||||
? "text-indigo-400"
|
? "text-indigo-400"
|
||||||
: "text-indigo-600"
|
: "text-indigo-600"
|
||||||
@@ -68,11 +68,11 @@ export function Sidebar({
|
|||||||
: "text-gray-500"
|
: "text-gray-500"
|
||||||
}`}
|
}`}
|
||||||
/>
|
/>
|
||||||
<span className="truncate">{chat.title}</span>
|
<span className="truncate">{chat.title || chat._id}</span>
|
||||||
</button>
|
</button>
|
||||||
{chats.length > 1 && (
|
{/* {chats.length > 1 && (
|
||||||
<button
|
<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 ${
|
className={`absolute right-2 top-1/2 -translate-y-1/2 p-2 rounded-md opacity-0 group-hover:opacity-100 transition-all ${
|
||||||
isDark
|
isDark
|
||||||
? "hover:bg-gray-600 text-gray-400 hover:text-red-400"
|
? "hover:bg-gray-600 text-gray-400 hover:text-red-400"
|
||||||
@@ -81,8 +81,8 @@ export function Sidebar({
|
|||||||
>
|
>
|
||||||
<Trash2 className="h-4 w-4" />
|
<Trash2 className="h-4 w-4" />
|
||||||
</button>
|
</button>
|
||||||
)}
|
)} */}
|
||||||
{activeChat.id === chat.id && (
|
{activeChat._id === chat._id && (
|
||||||
<div
|
<div
|
||||||
className={`absolute left-0 top-1/2 -translate-y-1/2 w-1 h-6 rounded-full ${
|
className={`absolute left-0 top-1/2 -translate-y-1/2 w-1 h-6 rounded-full ${
|
||||||
isDark ? "bg-indigo-400" : "bg-indigo-600"
|
isDark ? "bg-indigo-400" : "bg-indigo-600"
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
import ReactMarkdown from "react-markdown";
|
||||||
|
|
||||||
|
import "./index.css";
|
||||||
interface TypingAnimationProps {
|
interface TypingAnimationProps {
|
||||||
text: string;
|
text: string;
|
||||||
onComplete?: () => void;
|
onComplete?: () => void;
|
||||||
@@ -27,5 +29,7 @@ export function TypingAnimation({
|
|||||||
}
|
}
|
||||||
}, [currentIndex, text, speed, onComplete]);
|
}, [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 { useState, useRef, useEffect } from "react";
|
||||||
import { Menu } from "lucide-react";
|
import { Menu, Loader } from "lucide-react";
|
||||||
|
|
||||||
import { ThemeToggle } from "./ThemeToggle";
|
import { ThemeToggle } from "./ThemeToggle";
|
||||||
import { ChatMessage } from "./ChatMessage";
|
import { ChatMessage } from "./ChatMessage";
|
||||||
import { ChatInput } from "./ChatInput";
|
import { ChatInput } from "./ChatInput";
|
||||||
import { Sidebar } from "./Sidebar";
|
import { Sidebar } from "./Sidebar";
|
||||||
import type { Message, Chat } from "./types";
|
import type { Chat, Message } from "./types";
|
||||||
import { useTheme } from "../ThemeProvider";
|
import { useTheme } from "../ThemeProvider";
|
||||||
import ChatSwitch from "../SearchChat/ChatSwitch";
|
import ChatSwitch from "../SearchChat/ChatSwitch";
|
||||||
import { Footer } from "../SearchChat/Footer";
|
import { Footer } from "../SearchChat/Footer";
|
||||||
|
import { tauriFetch } from "../../api/tauriFetchClient";
|
||||||
const INITIAL_CHAT: Chat = {
|
import { useWebSocket } from "../../hooks/useWebSocket";
|
||||||
id: "1",
|
|
||||||
title: "New Chat",
|
|
||||||
messages: [
|
|
||||||
{
|
|
||||||
id: "1",
|
|
||||||
role: "assistant",
|
|
||||||
content: "Hello! How can I help you today?",
|
|
||||||
timestamp: new Date(),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
createdAt: new Date(),
|
|
||||||
};
|
|
||||||
|
|
||||||
interface ChatAIProps {
|
interface ChatAIProps {
|
||||||
changeMode: (isChatMode: boolean) => void;
|
changeMode: (isChatMode: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ChatAI({ changeMode }: ChatAIProps) {
|
export default function ChatAI({ changeMode }: ChatAIProps) {
|
||||||
const [chats, setChats] = useState<Chat[]>([INITIAL_CHAT]);
|
const [chats, setChats] = useState<Chat[]>([]);
|
||||||
const [activeChat, setActiveChat] = useState<Chat>(INITIAL_CHAT);
|
const [activeChat, setActiveChat] = useState<Chat>();
|
||||||
const [isSidebarOpen, setIsSidebarOpen] = useState(false);
|
const [isSidebarOpen, setIsSidebarOpen] = useState(false);
|
||||||
const [isTyping, setIsTyping] = useState(false);
|
const [isTyping, setIsTyping] = useState(false);
|
||||||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||||
const { theme } = useTheme();
|
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 = () => {
|
const scrollToBottom = () => {
|
||||||
messagesEndRef.current?.scrollIntoView({
|
messagesEndRef.current?.scrollIntoView({
|
||||||
behavior: "smooth",
|
behavior: "smooth",
|
||||||
@@ -45,31 +118,28 @@ export default function ChatAI({ changeMode }: ChatAIProps) {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
scrollToBottom();
|
scrollToBottom();
|
||||||
}, [activeChat.messages, isTyping]);
|
}, [activeChat?.messages, isTyping, curMessage]);
|
||||||
|
|
||||||
const createNewChat = () => {
|
const createNewChat = async () => {
|
||||||
const newChat: Chat = {
|
try {
|
||||||
id: Date.now().toString(),
|
const response = await tauriFetch({
|
||||||
title: "New Chat",
|
url: "/chat/_new",
|
||||||
messages: [
|
method: "POST",
|
||||||
{
|
});
|
||||||
id: "1",
|
console.log("_new", response);
|
||||||
role: "assistant",
|
const newChat: Chat = response.data;
|
||||||
content: "Hello! How can I help you today?",
|
setChats((prev) => [newChat, ...prev]);
|
||||||
timestamp: new Date(),
|
setActiveChat(newChat);
|
||||||
},
|
setIsSidebarOpen(false);
|
||||||
],
|
} catch (error) {
|
||||||
createdAt: new Date(),
|
console.error("Failed to fetch user data:", error);
|
||||||
};
|
}
|
||||||
setChats((prev) => [newChat, ...prev]);
|
|
||||||
setActiveChat(newChat);
|
|
||||||
setIsSidebarOpen(false);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const deleteChat = (chatId: string) => {
|
const deleteChat = (chatId: string) => {
|
||||||
setChats((prev) => prev.filter((chat) => chat.id !== chatId));
|
setChats((prev) => prev.filter((chat) => chat._id !== chatId));
|
||||||
if (activeChat.id === chatId) {
|
if (activeChat?._id === chatId) {
|
||||||
const remainingChats = chats.filter((chat) => chat.id !== chatId);
|
const remainingChats = chats.filter((chat) => chat._id !== chatId);
|
||||||
if (remainingChats.length > 0) {
|
if (remainingChats.length > 0) {
|
||||||
setActiveChat(remainingChats[0]);
|
setActiveChat(remainingChats[0]);
|
||||||
} else {
|
} else {
|
||||||
@@ -78,54 +148,79 @@ export default function ChatAI({ changeMode }: ChatAIProps) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSendMessage = (content: string) => {
|
const handleSendMessage = async (content: string) => {
|
||||||
const newMessage: Message = {
|
if (!activeChat?._id) return;
|
||||||
id: Date.now().toString(),
|
try {
|
||||||
role: "user",
|
const response = await tauriFetch({
|
||||||
content,
|
url: `/chat/${activeChat?._id}/_send`,
|
||||||
timestamp: new Date(),
|
method: "POST",
|
||||||
};
|
headers: {
|
||||||
|
WEBSOCKET_SESSION_ID: websocketId,
|
||||||
const updatedChat = {
|
},
|
||||||
...activeChat,
|
body: JSON.stringify({ message: content }),
|
||||||
title:
|
});
|
||||||
activeChat.messages.length === 1
|
console.log("_send", response, websocketId);
|
||||||
? content.slice(0, 30) + "..."
|
const updatedChat: Chat = {
|
||||||
: activeChat.title,
|
...activeChat,
|
||||||
messages: [...activeChat.messages, newMessage],
|
messages: [...(activeChat?.messages || []), ...(response.data || [])],
|
||||||
};
|
|
||||||
|
|
||||||
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 finalChat = {
|
setActiveChat(updatedChat);
|
||||||
...updatedChat,
|
setIsTyping(true);
|
||||||
messages: [...updatedChat.messages, assistantMessage],
|
setCurChatEnd(false);
|
||||||
};
|
} catch (error) {
|
||||||
|
console.error("Failed to fetch user data:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
setActiveChat(finalChat);
|
const chatHistory = async (chat: Chat) => {
|
||||||
setChats((prev) =>
|
try {
|
||||||
prev.map((chat) => (chat.id === activeChat.id ? finalChat : chat))
|
const response = await tauriFetch({
|
||||||
);
|
url: `/chat/${chat._id}/_history`,
|
||||||
setTimeout(() => setIsTyping(false), 500);
|
method: "GET",
|
||||||
}, 1000);
|
});
|
||||||
|
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 (
|
return (
|
||||||
<div className="h-screen pb-8">
|
<div className="h-screen pb-8 rounded-xl overflow-hidden">
|
||||||
<div className="h-[100%] flex">
|
<div className="h-[100%] flex">
|
||||||
{/* Sidebar */}
|
{/* Sidebar */}
|
||||||
<div
|
<div
|
||||||
@@ -135,17 +230,16 @@ export default function ChatAI({ changeMode }: ChatAIProps) {
|
|||||||
theme === "dark" ? "bg-gray-800" : "bg-gray-100"
|
theme === "dark" ? "bg-gray-800" : "bg-gray-100"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<Sidebar
|
{activeChat ? (
|
||||||
chats={chats}
|
<Sidebar
|
||||||
activeChat={activeChat}
|
chats={chats}
|
||||||
isDark={theme === "dark"}
|
activeChat={activeChat}
|
||||||
onNewChat={createNewChat}
|
isDark={theme === "dark"}
|
||||||
onSelectChat={(chat: any) => {
|
onNewChat={createNewChat}
|
||||||
setActiveChat(chat);
|
onSelectChat={onSelectChat}
|
||||||
setIsSidebarOpen(false);
|
onDeleteChat={deleteChat}
|
||||||
}}
|
/>
|
||||||
onDeleteChat={deleteChat}
|
) : null}
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Main content */}
|
{/* Main content */}
|
||||||
@@ -178,18 +272,44 @@ export default function ChatAI({ changeMode }: ChatAIProps) {
|
|||||||
</header>
|
</header>
|
||||||
|
|
||||||
{/* Chat messages */}
|
{/* Chat messages */}
|
||||||
<div className="flex-1 overflow-y-auto">
|
<div className="flex-1 overflow-y-auto custom-scrollbar">
|
||||||
{activeChat.messages.map((message, index) => (
|
{activeChat?.messages?.map((message, index) => (
|
||||||
<ChatMessage
|
<ChatMessage
|
||||||
key={message.id}
|
key={message._id + index}
|
||||||
message={message}
|
message={message}
|
||||||
isTyping={
|
isTyping={
|
||||||
isTyping &&
|
isTyping &&
|
||||||
index === activeChat.messages.length - 1 &&
|
index === (activeChat.messages?.length || 0) - 1 &&
|
||||||
message.role === "assistant"
|
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 ref={messagesEndRef} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -199,12 +319,16 @@ export default function ChatAI({ changeMode }: ChatAIProps) {
|
|||||||
theme === "dark" ? "border-gray-800" : "border-gray-200"
|
theme === "dark" ? "border-gray-800" : "border-gray-200"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<ChatInput onSend={handleSendMessage} disabled={isTyping} />
|
<ChatInput
|
||||||
|
onSend={handleSendMessage}
|
||||||
|
disabled={isTyping}
|
||||||
|
disabledChange={setIsTyping}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Footer isChat={true}/>
|
<Footer isChat={true} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,26 @@
|
|||||||
export interface Message {
|
export interface Message {
|
||||||
id: string;
|
_id: string;
|
||||||
role: 'user' | 'assistant';
|
_source: ISource;
|
||||||
content: string;
|
[key: string]: any;
|
||||||
timestamp: Date;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ISource {
|
||||||
|
id?: string;
|
||||||
|
created?: string;
|
||||||
|
updated?: string;
|
||||||
|
status?: string;
|
||||||
|
session_id?: string;
|
||||||
|
type?: string;
|
||||||
|
message?: any;
|
||||||
|
}
|
||||||
export interface Chat {
|
export interface Chat {
|
||||||
id: string;
|
_id: string;
|
||||||
title: string;
|
_index?: string;
|
||||||
messages: Message[];
|
_type?: string;
|
||||||
createdAt: Date;
|
_source?: ISource;
|
||||||
}
|
_score?: number;
|
||||||
|
found?: boolean;
|
||||||
|
title?: string;
|
||||||
|
messages?: any[];
|
||||||
|
[key: string]: any;
|
||||||
|
}
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ export const Footer = ({ isChat }: FooterProps) => {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
style={{ zIndex: 999 }}
|
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">
|
<div className="flex items-center">
|
||||||
<Menu as="div" className="relative">
|
<Menu as="div" className="relative">
|
||||||
|
|||||||
@@ -6,14 +6,13 @@ import Search from "./Search";
|
|||||||
import ChatAI from "../ChatAI";
|
import ChatAI from "../ChatAI";
|
||||||
|
|
||||||
export default function SearchChat() {
|
export default function SearchChat() {
|
||||||
const [isChatMode, setIsChatMode] = useState(false);
|
const [isChatMode, setIsChatMode] = useState(true);
|
||||||
|
|
||||||
async function changeMode(value: boolean) {
|
async function changeMode(value: boolean) {
|
||||||
console.log(11111, value);
|
|
||||||
if (value) {
|
if (value) {
|
||||||
await getCurrentWebviewWindow().setSize(new LogicalSize(900, 700));
|
await getCurrentWebviewWindow()?.setSize(new LogicalSize(900, 800));
|
||||||
} else {
|
} else {
|
||||||
await getCurrentWebviewWindow().setSize(new LogicalSize(800, 110));
|
await getCurrentWebviewWindow()?.setSize(new LogicalSize(900, 110));
|
||||||
}
|
}
|
||||||
setIsChatMode(value);
|
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 {
|
.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`
|
// 3. tell vite to ignore watching `src-tauri`
|
||||||
ignored: ["**/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