fix: resolve app show/hide behavior and tray icon handling (#129)

This commit is contained in:
Medcl
2025-02-09 21:51:03 +08:00
committed by GitHub
parent 17b3b97bec
commit bab38b3226
19 changed files with 1267 additions and 642 deletions

View File

@@ -11,16 +11,17 @@
},
"dependencies": {
"@headlessui/react": "^2.1.10",
"@react-oauth/google": "^0.12.1",
"@tauri-apps/api": "^2.2.0",
"@tauri-apps/plugin-autostart": "~2",
"@tauri-apps/plugin-deep-link": "^2.2.0",
"@tauri-apps/plugin-dialog": "^2.2.0",
"@tauri-apps/plugin-global-shortcut": "~2.0.0",
"@tauri-apps/plugin-http": "~2.0.1",
"@tauri-apps/plugin-os": "^2.2.0",
"@tauri-apps/plugin-shell": ">=2.0.0",
"@tauri-apps/plugin-websocket": "~2",
"@tauri-apps/plugin-window": "2.0.0-alpha.1",
"@tauri-apps/plugin-updater": "^2.3.0",
"ahooks": "^3.8.4",
"axios": "^1.7.7",
"clsx": "^2.1.1",

47
pnpm-lock.yaml generated
View File

@@ -11,9 +11,6 @@ importers:
'@headlessui/react':
specifier: ^2.1.10
version: 2.1.10(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@react-oauth/google':
specifier: ^0.12.1
version: 0.12.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@tauri-apps/api':
specifier: ^2.2.0
version: 2.2.0
@@ -23,6 +20,9 @@ importers:
'@tauri-apps/plugin-deep-link':
specifier: ^2.2.0
version: 2.2.0
'@tauri-apps/plugin-dialog':
specifier: ^2.2.0
version: 2.2.0
'@tauri-apps/plugin-global-shortcut':
specifier: ~2.0.0
version: 2.0.0
@@ -35,6 +35,9 @@ importers:
'@tauri-apps/plugin-shell':
specifier: '>=2.0.0'
version: 2.0.0
'@tauri-apps/plugin-updater':
specifier: ^2.3.0
version: 2.5.0
'@tauri-apps/plugin-websocket':
specifier: ~2
version: 2.0.0
@@ -513,12 +516,6 @@ packages:
peerDependencies:
react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0
'@react-oauth/google@0.12.1':
resolution: {integrity: sha512-qagsy22t+7UdkYAiT5ZhfM4StXi9PPNvw0zuwNmabrWyMKddczMtBIOARflbaIj+wHiQjnMAsZmzsUYuXeyoSg==}
peerDependencies:
react: '>=16.8.0'
react-dom: '>=16.8.0'
'@react-stately/utils@3.10.4':
resolution: {integrity: sha512-gBEQEIMRh5f60KCm7QKQ2WfvhB2gLUr9b72sqUdIZ2EG+xuPgaIlCBeSicvjmjBvYZwOjoOEnmIkcx2GHp/HWw==}
peerDependencies:
@@ -557,55 +554,46 @@ packages:
resolution: {integrity: sha512-0KXvIJQMOImLCVCz9uvvdPgfyWo93aHHp8ui3FrtOP57svqrF/roSSR5pjqL2hcMp0ljeGlU4q9o/rQaAQ3AYA==}
cpu: [arm]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-arm-musleabihf@4.24.0':
resolution: {integrity: sha512-it2BW6kKFVh8xk/BnHfakEeoLPv8STIISekpoF+nBgWM4d55CZKc7T4Dx1pEbTnYm/xEKMgy1MNtYuoA8RFIWw==}
cpu: [arm]
os: [linux]
libc: [musl]
'@rollup/rollup-linux-arm64-gnu@4.24.0':
resolution: {integrity: sha512-i0xTLXjqap2eRfulFVlSnM5dEbTVque/3Pi4g2y7cxrs7+a9De42z4XxKLYJ7+OhE3IgxvfQM7vQc43bwTgPwA==}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-arm64-musl@4.24.0':
resolution: {integrity: sha512-9E6MKUJhDuDh604Qco5yP/3qn3y7SLXYuiC0Rpr89aMScS2UAmK1wHP2b7KAa1nSjWJc/f/Lc0Wl1L47qjiyQw==}
cpu: [arm64]
os: [linux]
libc: [musl]
'@rollup/rollup-linux-powerpc64le-gnu@4.24.0':
resolution: {integrity: sha512-2XFFPJ2XMEiF5Zi2EBf4h73oR1V/lycirxZxHZNc93SqDN/IWhYYSYj8I9381ikUFXZrz2v7r2tOVk2NBwxrWw==}
cpu: [ppc64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-riscv64-gnu@4.24.0':
resolution: {integrity: sha512-M3Dg4hlwuntUCdzU7KjYqbbd+BLq3JMAOhCKdBE3TcMGMZbKkDdJ5ivNdehOssMCIokNHFOsv7DO4rlEOfyKpg==}
cpu: [riscv64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-s390x-gnu@4.24.0':
resolution: {integrity: sha512-mjBaoo4ocxJppTorZVKWFpy1bfFj9FeCMJqzlMQGjpNPY9JwQi7OuS1axzNIk0nMX6jSgy6ZURDZ2w0QW6D56g==}
cpu: [s390x]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-x64-gnu@4.24.0':
resolution: {integrity: sha512-ZXFk7M72R0YYFN5q13niV0B7G8/5dcQ9JDp8keJSfr3GoZeXEoMHP/HlvqROA3OMbMdfr19IjCeNAnPUG93b6A==}
cpu: [x64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-x64-musl@4.24.0':
resolution: {integrity: sha512-w1i+L7kAXZNdYl+vFvzSZy8Y1arS7vMgIy8wusXJzRrPyof5LAb02KGr1PD2EkRcl73kHulIID0M501lN+vobQ==}
cpu: [x64]
os: [linux]
libc: [musl]
'@rollup/rollup-win32-arm64-msvc@4.24.0':
resolution: {integrity: sha512-VXBrnPWgBpVDCVY6XF3LEW0pOU51KbaHhccHw6AS6vBWIC60eqsH19DAeeObl+g8nKAz04QFdl/Cefta0xQtUQ==}
@@ -664,28 +652,24 @@ packages:
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@tauri-apps/cli-linux-arm64-musl@2.2.7':
resolution: {integrity: sha512-+8HZ+txff/Y3YjAh80XcLXcX8kpGXVdr1P8AfjLHxHdS6QD4Md+acSxGTTNbplmHuBaSHJvuTvZf9tU1eDCTDg==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
libc: [musl]
'@tauri-apps/cli-linux-x64-gnu@2.2.7':
resolution: {integrity: sha512-ahlSnuCnUntblp9dG7/w5ZWZOdzRFi3zl0oScgt7GF4KNAOEa7duADsxPA4/FT2hLRa0SvpqtD4IYFvCxoVv3Q==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
libc: [glibc]
'@tauri-apps/cli-linux-x64-musl@2.2.7':
resolution: {integrity: sha512-+qKAWnJRSX+pjjRbKAQgTdFY8ecdcu8UdJ69i7wn3ZcRn2nMMzOO2LOMOTQV42B7/Q64D1pIpmZj9yblTMvadA==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
libc: [musl]
'@tauri-apps/cli-win32-arm64-msvc@2.2.7':
resolution: {integrity: sha512-aa86nRnrwT04u9D9fhf5JVssuAZlUCCc8AjqQjqODQjMd4BMA2+d4K9qBMpEG/1kVh95vZaNsLogjEaqSTTw4A==}
@@ -716,6 +700,9 @@ packages:
'@tauri-apps/plugin-deep-link@2.2.0':
resolution: {integrity: sha512-H6mkxr2KZ3XJcKL44tiq6cOjCw9DL8OgU1xjn3j26Qsn+H/roPFiyhR7CHuB8Ar+sQFj4YVlfmJwtBajK2FETQ==}
'@tauri-apps/plugin-dialog@2.2.0':
resolution: {integrity: sha512-6bLkYK68zyK31418AK5fNccCdVuRnNpbxquCl8IqgFByOgWFivbiIlvb79wpSXi0O+8k8RCSsIpOquebusRVSg==}
'@tauri-apps/plugin-global-shortcut@2.0.0':
resolution: {integrity: sha512-pnB4CUwFVjg4twtBSxoLJ4uLFTYxsvOdC1zIbG581pYzhYatOl6mjB+ijD5SSXgiS/jNoqMcfkOF9PWAisurew==}
@@ -728,6 +715,9 @@ packages:
'@tauri-apps/plugin-shell@2.0.0':
resolution: {integrity: sha512-OpW2+ycgJLrEoZityWeWYk+6ZWP9VyiAfbO+N/O8VfLkqyOym8kXh7odKDfINx9RAotkSGBtQM4abyKfJDkcUg==}
'@tauri-apps/plugin-updater@2.5.0':
resolution: {integrity: sha512-CWpwrkgpMESDPgJ0EuXgQTI/U9zeZQ9NLUvMyuWVrsuRez7tJ/3c7y73LAkvkI6+ekUxVaRJrxYrSfd8W+DRvQ==}
'@tauri-apps/plugin-websocket@2.0.0':
resolution: {integrity: sha512-O2qRxZCljd4g+ceJhW7LfgQr+fg0fBBiAaLiMopoKL6TXKMnhBHOenp4nZ5/MoVTr77OQIDNO6Jp/c1YwiRVtQ==}
@@ -2649,11 +2639,6 @@ snapshots:
clsx: 2.1.1
react: 18.3.1
'@react-oauth/google@0.12.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
'@react-stately/utils@3.10.4(react@18.3.1)':
dependencies:
'@swc/helpers': 0.5.13
@@ -2780,6 +2765,10 @@ snapshots:
dependencies:
'@tauri-apps/api': 2.2.0
'@tauri-apps/plugin-dialog@2.2.0':
dependencies:
'@tauri-apps/api': 2.2.0
'@tauri-apps/plugin-global-shortcut@2.0.0':
dependencies:
'@tauri-apps/api': 2.2.0
@@ -2796,6 +2785,10 @@ snapshots:
dependencies:
'@tauri-apps/api': 2.2.0
'@tauri-apps/plugin-updater@2.5.0':
dependencies:
'@tauri-apps/api': 2.2.0
'@tauri-apps/plugin-websocket@2.0.0':
dependencies:
'@tauri-apps/api': 2.2.0

597
src-tauri/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -14,23 +14,31 @@ name = "coco_lib"
crate-type = ["staticlib", "cdylib", "rlib"]
[build-dependencies]
tauri-build = { version = "2.0.0", features = [] }
tauri-build = { version = "2", features = [] }
[dependencies]
pizza-common = { git = "https://github.com/infinilabs/pizza-common", branch = "main" }
tauri = { version = "2.0.6", features = ["macos-private-api", "tray-icon", "image-png", "unstable"] }
tauri-plugin-shell = "2.0.0"
tauri = { version = "2", features = ["protocol-asset", "macos-private-api", "tray-icon", "image-ico", "image-png", "unstable"] }
tauri-plugin-shell = "2"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
tauri-plugin-http = "2"
tauri-plugin-websocket = "2"
tauri-plugin-theme = "2.1.2"
tauri-nspanel = { git = "https://github.com/ahkohd/tauri-nspanel", branch = "v2" }
tauri-plugin-deep-link = "2.0.0"
tauri-plugin-single-instance = "2.0.0"
tauri-plugin-store = "2.2.0"
tauri-plugin-os = "2"
tauri-plugin-dialog = "2"
tauri-plugin-fs = "2"
tauri-plugin-updater = "2"
tauri-plugin-process = "2"
tauri-plugin-drag = "2"
[target."cfg(target_os = \"macos\")".dependencies]
tauri-nspanel = { git = "https://github.com/ahkohd/tauri-nspanel", branch = "v2" }
reqwest = "0.12.12"
futures = "0.3.31"
ordered-float = { version = "4.6.0", default-features = false }

8
src-tauri/Coco.desktop Normal file
View File

@@ -0,0 +1,8 @@
[Desktop Entry]
Type=Application
Name={{{name}}}
Exec={{{exec}}}
Icon={{{icon}}}
Categories={{{categories}}}
Comment={{{comment}}}
Terminal=false

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

BIN
src-tauri/assets/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

BIN
src-tauri/assets/tray.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

View File

@@ -55,9 +55,6 @@
{
"identifier": "http:default",
"allow": [
{
"url": "http://localhost:9000"
},
{
"url": "https://coco.infini.cloud"
}

View File

@@ -18,7 +18,7 @@ use reqwest::Client;
use std::path::PathBuf;
#[cfg(target_os = "macos")]
use tauri::ActivationPolicy;
use tauri::{AppHandle, Emitter, Listener, Manager, Runtime, WebviewWindow};
use tauri::{AppHandle, Emitter, Listener, Manager, Runtime, WebviewWindow, WindowEvent};
use tauri_plugin_autostart::MacosLauncher;
use tauri_plugin_deep_link::DeepLinkExt;
use tokio::runtime::Runtime as RT;
@@ -73,7 +73,7 @@ struct Payload {
pub fn run() {
let mut ctx = tauri::generate_context!();
tauri::Builder::default()
let app = tauri::Builder::default()
.plugin(tauri_nspanel::init())
.plugin(tauri_plugin_http::init())
.plugin(tauri_plugin_shell::init())
@@ -96,7 +96,7 @@ pub fn run() {
shortcut::get_current_shortcut,
change_autostart,
hide_coco,
switch_tray_icon,
// switch_tray_icon,
server::servers::add_coco_server,
server::servers::remove_coco_server,
server::servers::list_coco_servers,
@@ -158,9 +158,31 @@ pub fn run() {
setup::default(app, main_window.clone(), settings_window.clone());
Ok(())
})
.run(ctx)
}).on_window_event(|window, event| match event {
WindowEvent::CloseRequested { api, .. } => {
dbg!("Close requested event received");
window.hide().unwrap();
api.prevent_close();
}
_ => {}
}).build(ctx)
.expect("error while running tauri application");
app.run(|app_handle, event| match event {
#[cfg(target_os = "macos")]
tauri::RunEvent::Reopen {
has_visible_windows,
..
} => {
dbg!("Reopen event received: has_visible_windows = {}", has_visible_windows);
if has_visible_windows {
return;
}
}
_ => {
let _ = app_handle;
}
});
}
pub async fn init<R: Runtime>(app_handle: &AppHandle<R>) {
@@ -253,7 +275,7 @@ fn hide_coco(app: tauri::AppHandle) {
}
fn handle_open_coco(app: &AppHandle) {
// println!("Open Coco menu clicked!");
println!("Open Coco menu clicked!");
if let Some(window) = app.get_window(MAIN_WINDOW_LABEL) {
window.show().unwrap();
@@ -279,36 +301,36 @@ fn handle_hide_coco(app: &AppHandle) {
}
}
#[tauri::command]
fn switch_tray_icon(app: tauri::AppHandle, is_dark_mode: bool) {
let app_handle = app.app_handle();
// println!("is_dark_mode: {}", is_dark_mode);
const DARK_ICON_PATH: &[u8] = include_bytes!("../icons/dark@2x.png");
const LIGHT_ICON_PATH: &[u8] = include_bytes!("../icons/light@2x.png");
let icon_path: &[u8] = if is_dark_mode {
DARK_ICON_PATH
} else {
LIGHT_ICON_PATH
};
let tray = match app_handle.tray_by_id("tray") {
Some(tray) => tray,
None => {
eprintln!("Tray with ID 'tray' not found");
return;
}
};
if let Err(e) = tray.set_icon(Some(
tauri::image::Image::from_bytes(icon_path)
.unwrap_or_else(|e| panic!("Failed to load icon from bytes: {}", e)),
)) {
eprintln!("Failed to set tray icon: {}", e);
}
}
// #[tauri::command]
// fn switch_tray_icon(app: tauri::AppHandle, is_dark_mode: bool) {
// let app_handle = app.app_handle();
//
// // println!("is_dark_mode: {}", is_dark_mode);
//
// const DARK_ICON_PATH: &[u8] = include_bytes!("../icons/dark@2x.png");
// const LIGHT_ICON_PATH: &[u8] = include_bytes!("../icons/light@2x.png");
//
// let icon_path: &[u8] = if is_dark_mode {
// DARK_ICON_PATH
// } else {
// LIGHT_ICON_PATH
// };
//
// let tray = match app_handle.tray_by_id("tray") {
// Some(tray) => tray,
// None => {
// eprintln!("Tray with ID 'tray' not found");
// return;
// }
// };
//
// if let Err(e) = tray.set_icon(Some(
// tauri::image::Image::from_bytes(icon_path)
// .unwrap_or_else(|e| panic!("Failed to load icon from bytes: {}", e)),
// )) {
// eprintln!("Failed to set tray icon: {}", e);
// }
// }
fn enable_tray(app: &mut tauri::App) {
use tauri::{
@@ -335,8 +357,9 @@ fn enable_tray(app: &mut tauri::App) {
.unwrap();
let _tray = TrayIconBuilder::with_id("tray")
.icon_as_template(true)
// .icon(app.default_window_icon().unwrap().clone())
.icon(Image::from_bytes(include_bytes!("../icons/light@2x.png")).expect("REASON"))
.icon(Image::from_bytes(include_bytes!("../assets/tray-mac.ico")).expect("Failed to load icon"))
.menu(&menu)
.on_menu_event(|app, event| match event.id.as_ref() {
"open" => {
@@ -351,7 +374,7 @@ fn enable_tray(app: &mut tauri::App) {
"settings" => {
// windows failed to open second window, issue: https://github.com/tauri-apps/tauri/issues/11144 https://github.com/tauri-apps/tauri/issues/8196
//#[cfg(windows)]
let _ = app.emit("open_settings", "");
let _ = app.emit("open_settings", "settings");
// #[cfg(not(windows))]
// open_settings(&app);

View File

@@ -1,7 +1,7 @@
{
"$schema": "https://schema.tauri.app/config/2.0.0",
"productName": "Coco AI",
"version": "0.1.0",
"version": "../package.json",
"identifier": "rs.coco.app",
"build": {
"beforeDevCommand": "pnpm dev",
@@ -23,12 +23,13 @@
"maximizable": false,
"skipTaskbar": true,
"resizable": false,
"alwaysOnTop": false,
"alwaysOnTop": true,
"acceptFirstMouse": true,
"shadow": true,
"transparent": true,
"fullscreen": false,
"center": false,
"visible": false,
"windowEffects": {
"effects": [],
"radius": 12
@@ -43,7 +44,8 @@
"transparent": true,
"maximizable": false,
"skipTaskbar": false,
"dragDropEnabled": true,
"dragDropEnabled": false,
"hiddenTitle": true,
"visible": false,
"windowEffects": {
"effects": [
@@ -54,17 +56,30 @@
}
],
"security": {
"csp": null
"csp": null,
"dangerousDisableAssetCspModification": true,
"assetProtocol": {
"enable": true,
"scope": {
"allow": [
"**/*"
],
"requireLiteralLeadingDot": false
}
}
}
},
"bundle": {
"active": true,
"targets": "all",
"windows": {
"certificateThumbprint": null,
"digestAlgorithm": "sha256",
"timestampUrl": ""
},
"targets": [
"nsis",
"dmg",
"app",
"appimage",
"deb",
"rpm"
],
"shortDescription": "Coco AI",
"icon": [
"icons/32x32.png",
"icons/128x128.png",
@@ -108,6 +123,12 @@
]
},
"window": {},
"updater": {
"pubkey": "",
"endpoints": [
"https://api.coco.rs/update"
]
},
"websocket": {},
"shell": {},
"globalShortcut": {},

View File

@@ -0,0 +1,15 @@
{
"identifier": "rs.coco.app",
"bundle": {
"linux": {
"deb": {
"depends": [],
"desktopTemplate": "./Coco.desktop"
},
"rpm": {
"depends": [],
"desktopTemplate": "./Coco.desktop"
}
}
}
}

View File

@@ -0,0 +1,11 @@
{
"identifier": "rs.coco.app",
"bundle": {
"externalBin": [
],
"resources": [
"assets/tray-mac.ico",
"assets/drag-icon.png"
]
}
}

View File

@@ -0,0 +1,15 @@
{
"identifier": "rs.coco.app",
"bundle": {
"externalBin": [
],
"windows": {
"digestAlgorithm": "sha256",
"nsis": {
"languages": [
],
"installMode": "both"
}
}
}
}

View File

@@ -1,406 +1,419 @@
import {
Library,
Mic,
Send,
Plus,
AudioLines,
Image,
ArrowBigLeft,
Search,
} from "lucide-react";
import { useRef, useState, useEffect, useCallback } from "react";
import { listen } from "@tauri-apps/api/event";
import { isTauri } from "@tauri-apps/api/core";
import {ArrowBigLeft, AudioLines, Image, Library, Mic, Plus, Search, Send,} from "lucide-react";
import {useCallback, useEffect, useRef, useState} from "react";
import {listen} from "@tauri-apps/api/event";
import {invoke, isTauri} from "@tauri-apps/api/core";
import ChatSwitch from "@/components/Common/ChatSwitch";
import AutoResizeTextarea from "./AutoResizeTextarea";
import { useChatStore } from "@/stores/chatStore";
import {useChatStore} from "@/stores/chatStore";
import StopIcon from "@/icons/Stop";
import { useAppStore } from "@/stores/appStore";
import { useSearchStore } from "@/stores/searchStore";
import { metaOrCtrlKey } from "@/utils/keyboardUtils";
import {useAppStore} from "@/stores/appStore";
import {useSearchStore} from "@/stores/searchStore";
import {metaOrCtrlKey} from "@/utils/keyboardUtils";
interface ChatInputProps {
onSend: (message: string) => void;
disabled: boolean;
disabledChange: () => void;
changeMode: (isChatMode: boolean) => void;
isChatMode: boolean;
inputValue: string;
changeInput: (val: string) => void;
reconnect: () => void;
onSend: (message: string) => void;
disabled: boolean;
disabledChange: () => void;
changeMode: (isChatMode: boolean) => void;
isChatMode: boolean;
inputValue: string;
changeInput: (val: string) => void;
reconnect: () => void;
}
export default function ChatInput({
onSend,
disabled,
changeMode,
isChatMode,
inputValue,
changeInput,
disabledChange,
reconnect,
}: ChatInputProps) {
const showTooltip = useAppStore((state: { showTooltip: boolean }) => state.showTooltip);
onSend,
disabled,
changeMode,
isChatMode,
inputValue,
changeInput,
disabledChange,
reconnect,
}: ChatInputProps) {
const showTooltip = useAppStore((state: { showTooltip: boolean }) => state.showTooltip);
const sourceData = useSearchStore((state: { sourceData: any; }) => state.sourceData);
const setSourceData = useSearchStore((state: { setSourceData: any; }) => state.setSourceData);
const sourceData = useSearchStore((state: { sourceData: any; }) => state.sourceData);
const setSourceData = useSearchStore((state: { setSourceData: any; }) => state.setSourceData);
useEffect(() => {
setSourceData(undefined);
}, []);
useEffect(() => {
setSourceData(undefined);
}, []);
const inputRef = useRef<HTMLInputElement>(null);
const textareaRef = useRef<{ reset: () => void; focus: () => void }>(null);
const inputRef = useRef<HTMLInputElement>(null);
const textareaRef = useRef<{ reset: () => void; focus: () => void }>(null);
const { curChatEnd, connected } = useChatStore();
const {curChatEnd, connected} = useChatStore();
const [isCommandPressed, setIsCommandPressed] = useState(false);
const [isCommandPressed, setIsCommandPressed] = useState(false);
const handleToggleFocus = useCallback(() => {
if (isChatMode) {
textareaRef.current?.focus();
} else {
inputRef.current?.focus();
}
}, [isChatMode, textareaRef, inputRef]);
const handleSubmit = useCallback(() => {
const trimmedValue = inputValue.trim();
if (trimmedValue && !disabled) {
onSend(trimmedValue);
}
}, [inputValue, disabled, onSend]);
const pressedKeys = new Set<string>();
const handleKeyDown = useCallback(
(e: KeyboardEvent) => {
console.log("handleKeyDown", e.code, e.key);
pressedKeys.add(e.key);
if (e.key === metaOrCtrlKey()) {
setIsCommandPressed(true);
}
if (pressedKeys.has(metaOrCtrlKey())) {
// e.preventDefault();
switch (e.code) {
case "Comma":
setIsCommandPressed(false);
break;
case "KeyI":
handleToggleFocus();
break;
case "ArrowLeft":
setSourceData(undefined);
break;
case "KeyM":
console.log("KeyM");
break;
case "Enter":
isChatMode && (curChatEnd ? handleSubmit() : disabledChange?.());
break;
case "KeyO":
console.log("KeyO");
break;
case "KeyU":
console.log("KeyU");
break;
case "KeyN":
console.log("KeyN");
break;
case "KeyG":
console.log("KeyG");
break;
default:
break;
}
}
},
[
handleToggleFocus,
isChatMode,
handleSubmit,
setSourceData,
setIsCommandPressed,
disabledChange,
curChatEnd,
]
);
const handleKeyUp = useCallback((e: KeyboardEvent) => {
pressedKeys.delete(e.key);
if (e.key === metaOrCtrlKey()) {
setIsCommandPressed(false);
}
}, []);
useEffect(() => {
window.addEventListener("keydown", handleKeyDown);
window.addEventListener("keyup", handleKeyUp);
return () => {
window.removeEventListener("keydown", handleKeyDown);
window.removeEventListener("keyup", handleKeyUp);
};
}, [handleKeyDown, handleKeyUp]);
useEffect(() => {
if (!isTauri()) return;
const setupListener = async () => {
const unlisten = await listen("tauri://focus", () => {
// console.log("Window focused!");
const handleToggleFocus = useCallback(() => {
if (isChatMode) {
textareaRef.current?.focus();
textareaRef.current?.focus();
} else {
inputRef.current?.focus();
inputRef.current?.focus();
}
});
}, [isChatMode, textareaRef, inputRef]);
return unlisten;
};
const handleSubmit = useCallback(() => {
const trimmedValue = inputValue.trim();
if (trimmedValue && !disabled) {
onSend(trimmedValue);
}
}, [inputValue, disabled, onSend]);
let unlisten: (() => void) | undefined;
const pressedKeys = new Set<string>();
setupListener().then((unlistener) => {
unlisten = unlistener;
});
const handleKeyDown = useCallback(
(e: KeyboardEvent) => {
console.log("handleKeyDown", e.code, e.key);
return () => {
unlisten?.();
};
}, [isChatMode]);
const openChatAI = async () => {
console.log("Chat AI opened.");
};
const [countdown, setCountdown] = useState(5);
useEffect(() => {
if (connected) return;
if (countdown <= 0) {
ReconnectClick();
return;
}
const timer = setTimeout(() => {
setCountdown((prev) => prev - 1);
}, 1000);
return () => clearTimeout(timer);
}, [countdown, connected]);
const ReconnectClick = () => {
setCountdown(5);
reconnect();
};
return (
<div className="w-full relative">
<div className="p-2 flex items-center dark:text-[#D8D8D8] bg-[#ededed] dark:bg-[#202126] rounded transition-all relative">
<div className="flex flex-wrap gap-2 flex-1 items-center relative">
{!isChatMode && !sourceData ? (
<Search
className="w-4 h-4 text-[#ccc] dark:text-[#d8d8d8]"
/>
) : (
!isChatMode && sourceData ? (
<ArrowBigLeft
className="w-4 h-4 text-[#ccc] dark:text-[#d8d8d8] cursor-pointer"
onClick={() => setSourceData(undefined)}
/>
) : null
)}
{isChatMode ? (
<AutoResizeTextarea
ref={textareaRef}
input={inputValue}
setInput={changeInput}
connected={connected}
handleKeyDown={(e) => {
if (e.key === "Enter") {
e.preventDefault();
handleSubmit();
if (e.key === "Escape") {
console.log("Escape:" + inputValue);
if (inputValue) {
changeInput("");
return;
} else {
console.log("empty value, but Escape key pressed.");
invoke("hide_coco").then(() => {
console.log("Hide Coco");
}).finally(() => {
console.log("Hide Coco");
})
}
}}
/>
) : (
<input
ref={inputRef}
type="text"
autoFocus
autoComplete="off"
autoCapitalize="none"
spellCheck="false"
className="text-base font-normal flex-1 outline-none min-w-[200px] text-[#333] dark:text-[#d8d8d8] placeholder-text-xs placeholder-[#999] dark:placeholder-gray-500 bg-transparent"
placeholder="Search whatever you want ..."
value={inputValue}
onChange={(e) => {
onSend(e.target.value);
}}
/>
)}
{showTooltip && isCommandPressed && !isChatMode && sourceData ? (
}
pressedKeys.add(e.key);
if (e.key === metaOrCtrlKey()) {
setIsCommandPressed(true);
}
if (pressedKeys.has(metaOrCtrlKey())) {
// e.preventDefault();
switch (e.code) {
case "Comma":
setIsCommandPressed(false);
break;
case "KeyI":
handleToggleFocus();
break;
case "ArrowLeft":
setSourceData(undefined);
break;
case "KeyM":
console.log("KeyM");
break;
case "Enter":
isChatMode && (curChatEnd ? handleSubmit() : disabledChange?.());
break;
case "KeyO":
console.log("KeyO");
break;
case "KeyU":
console.log("KeyU");
break;
case "KeyN":
console.log("KeyN");
break;
case "KeyG":
console.log("KeyG");
break;
default:
break;
}
}
},
[
handleToggleFocus,
isChatMode,
handleSubmit,
setSourceData,
setIsCommandPressed,
disabledChange,
curChatEnd,
]
);
const handleKeyUp = useCallback((e: KeyboardEvent) => {
pressedKeys.delete(e.key);
if (e.key === metaOrCtrlKey()) {
setIsCommandPressed(false);
}
}, []);
useEffect(() => {
window.addEventListener("keydown", handleKeyDown);
window.addEventListener("keyup", handleKeyUp);
return () => {
window.removeEventListener("keydown", handleKeyDown);
window.removeEventListener("keyup", handleKeyUp);
};
}, [handleKeyDown, handleKeyUp]);
useEffect(() => {
if (!isTauri()) return;
const setupListener = async () => {
const unlisten = await listen("tauri://focus", () => {
// console.log("Window focused!");
if (isChatMode) {
textareaRef.current?.focus();
} else {
inputRef.current?.focus();
}
});
return unlisten;
};
let unlisten: (() => void) | undefined;
setupListener().then((unlistener) => {
unlisten = unlistener;
});
return () => {
unlisten?.();
};
}, [isChatMode]);
const openChatAI = async () => {
console.log("Chat AI opened.");
};
const [countdown, setCountdown] = useState(5);
useEffect(() => {
if (connected) return;
if (countdown <= 0) {
ReconnectClick();
return;
}
const timer = setTimeout(() => {
setCountdown((prev) => prev - 1);
}, 1000);
return () => clearTimeout(timer);
}, [countdown, connected]);
const ReconnectClick = () => {
setCountdown(5);
reconnect();
};
return (
<div className="w-full relative">
<div
className={`absolute left-0 w-4 h-4 flex items-center justify-center font-normal text-xs text-[#333] leading-[14px] bg-[#ccc] dark:bg-[#6B6B6B] rounded-md shadow-[-6px_0px_6px_2px_#ededed] dark:shadow-[-6px_0px_6px_2px_#202126]`}
>
className="p-2 flex items-center dark:text-[#D8D8D8] bg-[#ededed] dark:bg-[#202126] rounded transition-all relative">
<div className="flex flex-wrap gap-2 flex-1 items-center relative">
{!isChatMode && !sourceData ? (
<Search
className="w-4 h-4 text-[#ccc] dark:text-[#d8d8d8]"
/>
) : (
!isChatMode && sourceData ? (
<ArrowBigLeft
className="w-4 h-4 text-[#ccc] dark:text-[#d8d8d8] cursor-pointer"
onClick={() => setSourceData(undefined)}
/>
) : null
)}
{isChatMode ? (
<AutoResizeTextarea
ref={textareaRef}
input={inputValue}
setInput={changeInput}
connected={connected}
handleKeyDown={(e) => {
if (e.key === "Enter") {
e.preventDefault();
handleSubmit();
}
}}
/>
) : (
<input
ref={inputRef}
type="text"
autoFocus
autoComplete="off"
autoCapitalize="none"
spellCheck="false"
className="text-base font-normal flex-1 outline-none min-w-[200px] text-[#333] dark:text-[#d8d8d8] placeholder-text-xs placeholder-[#999] dark:placeholder-gray-500 bg-transparent"
placeholder="Search whatever you want ..."
value={inputValue}
onChange={(e) => {
onSend(e.target.value);
}}
/>
)}
{showTooltip && isCommandPressed && !isChatMode && sourceData ? (
<div
className={`absolute left-0 w-4 h-4 flex items-center justify-center font-normal text-xs text-[#333] leading-[14px] bg-[#ccc] dark:bg-[#6B6B6B] rounded-md shadow-[-6px_0px_6px_2px_#ededed] dark:shadow-[-6px_0px_6px_2px_#202126]`}
>
</div>
) : null}
{showTooltip && isCommandPressed ? (
<div
className={`absolute ${!isChatMode && sourceData ? "left-7" : ""
} w-4 h-4 flex items-center justify-center font-normal text-xs text-[#333] leading-[14px] bg-[#ccc] dark:bg-[#6B6B6B] rounded-md shadow-[-6px_0px_6px_2px_#ededed] dark:shadow-[-6px_0px_6px_2px_#202126]`}
>
I
</div>
) : null}
</div>
{isChatMode ? (
<button
className="p-1 hover:bg-gray-50 dark:hover:bg-gray-700 rounded-full transition-colors"
type="button"
>
<Mic className="w-4 h-4 text-[#999] dark:text-[#999]"/>
</button>
) : null}
{isChatMode && curChatEnd ? (
<button
className={`ml-1 p-1 ${inputValue
? "bg-[#0072FF]"
: "bg-[#E4E5F0] dark:bg-[rgb(84,84,84)]"
} rounded-full transition-colors`}
type="submit"
onClick={() => onSend(inputValue.trim())}
>
<Send className="w-4 h-4 text-white"/>
</button>
) : null}
{isChatMode && !curChatEnd ? (
<button
className={`ml-1 px-1 bg-[#0072FF] rounded-full transition-colors`}
type="submit"
onClick={() => disabledChange()}
>
<StopIcon
size={16}
className="w-4 h-4 text-white"
aria-label="Stop message"
/>
</button>
) : null}
{showTooltip && isChatMode && isCommandPressed ? (
<div
className={`absolute right-10 w-4 h-4 flex items-center justify-center font-normal text-xs text-[#333] leading-[14px] bg-[#ccc] dark:bg-[#6B6B6B] rounded-md shadow-[-6px_0px_6px_2px_#fff] dark:shadow-[-6px_0px_6px_2px_#000]`}
>
M
</div>
) : null}
{showTooltip && isChatMode && isCommandPressed ? (
<div
className={`absolute right-3 w-4 h-4 flex items-end justify-center font-normal text-xs text-[#333] leading-[14px] bg-[#ccc] dark:bg-[#6B6B6B] rounded-md shadow-[-6px_0px_6px_2px_#fff] dark:shadow-[-6px_0px_6px_2px_#000]`}
>
</div>
) : null}
{!connected && isChatMode ? (
<div
className="absolute top-0 right-0 bottom-0 left-0 px-2 py-4 bg-red-500/10 rounded-md font-normal text-xs text-gray-400 flex items-center gap-4">
Unable to connect to the server
<div
className="w-[96px] h-[24px] bg-[#0061FF] rounded-[12px] font-normal text-xs text-white flex items-center justify-center cursor-pointer"
onClick={ReconnectClick}
>
Reconnect ({countdown})
</div>
</div>
) : null}
</div>
) : null}
{showTooltip && isCommandPressed ? (
<div
className={`absolute ${!isChatMode && sourceData ? "left-7" : ""
} w-4 h-4 flex items-center justify-center font-normal text-xs text-[#333] leading-[14px] bg-[#ccc] dark:bg-[#6B6B6B] rounded-md shadow-[-6px_0px_6px_2px_#ededed] dark:shadow-[-6px_0px_6px_2px_#202126]`}
data-tauri-drag-region
className="flex justify-between items-center p-2"
>
I
{isChatMode ? (
<div className="flex gap-2 text-xs text-[#333] dark:text-[#d8d8d8]">
<button
className="inline-flex items-center rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors relative"
onClick={openChatAI}
>
<Library className="w-4 h-4 mr-1 text-[#000] dark:text-[#d8d8d8]"/>
Coco
</button>
<button
className="inline-flex items-center rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 transition-color relative">
<Plus className="w-4 h-4 mr-1 text-[#000] dark:text-[#d8d8d8]"/>
Upload
</button>
{showTooltip && isCommandPressed ? (
<div
className={`absolute left-2 w-4 h-4 flex items-center justify-center font-normal text-xs text-[#333] leading-[14px] bg-[#ccc] dark:bg-[#6B6B6B] rounded-md shadow-[-6px_0px_6px_2px_#fff] dark:shadow-[-6px_0px_6px_2px_#000]`}
>
O
</div>
) : null}
{showTooltip && isCommandPressed ? (
<div
className={`absolute left-16 w-4 h-4 flex items-center justify-center font-normal text-xs text-[#333] leading-[14px] bg-[#ccc] dark:bg-[#6B6B6B] rounded-md shadow-[-6px_0px_6px_2px_#fff] dark:shadow-[-6px_0px_6px_2px_#000]`}
>
U
</div>
) : null}
</div>
) : (
<div className="w-28 flex gap-2 relative">
<button
className="inline-flex items-center rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors relative"
onClick={openChatAI}
>
<AudioLines className="w-4 h-4 text-[#000] dark:text-[#d8d8d8]"/>
</button>
<button
className="inline-flex items-center rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 transition-color relative">
<Image className="w-4 h-4 text-[#000] dark:text-[#d8d8d8]"/>
</button>
{showTooltip && isCommandPressed ? (
<div
className={`absolute left-0 w-4 h-4 flex items-center justify-center font-normal text-xs text-[#333] leading-[14px] bg-[#ccc] dark:bg-[#6B6B6B] rounded-md shadow-[-6px_0px_6px_2px_#fff] dark:shadow-[-6px_0px_6px_2px_#000]`}
>
N
</div>
) : null}
{showTooltip && isCommandPressed ? (
<div
className={`absolute left-6 w-4 h-4 flex items-center justify-center font-normal text-xs text-[#333] leading-[14px] bg-[#ccc] dark:bg-[#6B6B6B] rounded-md shadow-[-6px_0px_6px_2px_#fff] dark:shadow-[-6px_0px_6px_2px_#000]`}
>
G
</div>
) : null}
</div>
)}
<div className="relative w-16 flex justify-end items-center">
{showTooltip && isCommandPressed ? (
<div
className={`absolute left-1 z-10 w-4 h-4 flex items-center justify-center font-normal text-xs text-[#333] leading-[14px] bg-[#ccc] dark:bg-[#6B6B6B] rounded-md shadow-[-6px_0px_6px_2px_#fff] dark:shadow-[-6px_0px_6px_2px_#000]`}
>
T
</div>
) : null}
<ChatSwitch
isChatMode={isChatMode}
onChange={(value: boolean) => {
value && disabledChange();
changeMode(value);
setSourceData(undefined);
}}
/>
</div>
</div>
) : null}
</div>
{isChatMode ? (
<button
className="p-1 hover:bg-gray-50 dark:hover:bg-gray-700 rounded-full transition-colors"
type="button"
>
<Mic className="w-4 h-4 text-[#999] dark:text-[#999]" />
</button>
) : null}
{isChatMode && curChatEnd ? (
<button
className={`ml-1 p-1 ${inputValue
? "bg-[#0072FF]"
: "bg-[#E4E5F0] dark:bg-[rgb(84,84,84)]"
} rounded-full transition-colors`}
type="submit"
onClick={() => onSend(inputValue.trim())}
>
<Send className="w-4 h-4 text-white" />
</button>
) : null}
{isChatMode && !curChatEnd ? (
<button
className={`ml-1 px-1 bg-[#0072FF] rounded-full transition-colors`}
type="submit"
onClick={() => disabledChange()}
>
<StopIcon
size={16}
className="w-4 h-4 text-white"
aria-label="Stop message"
/>
</button>
) : null}
{showTooltip && isChatMode && isCommandPressed ? (
<div
className={`absolute right-10 w-4 h-4 flex items-center justify-center font-normal text-xs text-[#333] leading-[14px] bg-[#ccc] dark:bg-[#6B6B6B] rounded-md shadow-[-6px_0px_6px_2px_#fff] dark:shadow-[-6px_0px_6px_2px_#000]`}
>
M
</div>
) : null}
{showTooltip && isChatMode && isCommandPressed ? (
<div
className={`absolute right-3 w-4 h-4 flex items-end justify-center font-normal text-xs text-[#333] leading-[14px] bg-[#ccc] dark:bg-[#6B6B6B] rounded-md shadow-[-6px_0px_6px_2px_#fff] dark:shadow-[-6px_0px_6px_2px_#000]`}
>
</div>
) : null}
{!connected && isChatMode ? (
<div className="absolute top-0 right-0 bottom-0 left-0 px-2 py-4 bg-red-500/10 rounded-md font-normal text-xs text-gray-400 flex items-center gap-4">
Unable to connect to the server
<div
className="w-[96px] h-[24px] bg-[#0061FF] rounded-[12px] font-normal text-xs text-white flex items-center justify-center cursor-pointer"
onClick={ReconnectClick}
>
Reconnect ({countdown})
</div>
</div>
) : null}
</div>
<div
data-tauri-drag-region
className="flex justify-between items-center p-2"
>
{isChatMode ? (
<div className="flex gap-2 text-xs text-[#333] dark:text-[#d8d8d8]">
<button
className="inline-flex items-center rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors relative"
onClick={openChatAI}
>
<Library className="w-4 h-4 mr-1 text-[#000] dark:text-[#d8d8d8]" />
Coco
</button>
<button className="inline-flex items-center rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 transition-color relative">
<Plus className="w-4 h-4 mr-1 text-[#000] dark:text-[#d8d8d8]" />
Upload
</button>
{showTooltip && isCommandPressed ? (
<div
className={`absolute left-2 w-4 h-4 flex items-center justify-center font-normal text-xs text-[#333] leading-[14px] bg-[#ccc] dark:bg-[#6B6B6B] rounded-md shadow-[-6px_0px_6px_2px_#fff] dark:shadow-[-6px_0px_6px_2px_#000]`}
>
O
</div>
) : null}
{showTooltip && isCommandPressed ? (
<div
className={`absolute left-16 w-4 h-4 flex items-center justify-center font-normal text-xs text-[#333] leading-[14px] bg-[#ccc] dark:bg-[#6B6B6B] rounded-md shadow-[-6px_0px_6px_2px_#fff] dark:shadow-[-6px_0px_6px_2px_#000]`}
>
U
</div>
) : null}
</div>
) : (
<div className="w-28 flex gap-2 relative">
<button
className="inline-flex items-center rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors relative"
onClick={openChatAI}
>
<AudioLines className="w-4 h-4 text-[#000] dark:text-[#d8d8d8]" />
</button>
<button className="inline-flex items-center rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 transition-color relative">
<Image className="w-4 h-4 text-[#000] dark:text-[#d8d8d8]" />
</button>
{showTooltip && isCommandPressed ? (
<div
className={`absolute left-0 w-4 h-4 flex items-center justify-center font-normal text-xs text-[#333] leading-[14px] bg-[#ccc] dark:bg-[#6B6B6B] rounded-md shadow-[-6px_0px_6px_2px_#fff] dark:shadow-[-6px_0px_6px_2px_#000]`}
>
N
</div>
) : null}
{showTooltip && isCommandPressed ? (
<div
className={`absolute left-6 w-4 h-4 flex items-center justify-center font-normal text-xs text-[#333] leading-[14px] bg-[#ccc] dark:bg-[#6B6B6B] rounded-md shadow-[-6px_0px_6px_2px_#fff] dark:shadow-[-6px_0px_6px_2px_#000]`}
>
G
</div>
) : null}
</div>
)}
<div className="relative w-16 flex justify-end items-center">
{showTooltip && isCommandPressed ? (
<div
className={`absolute left-1 z-10 w-4 h-4 flex items-center justify-center font-normal text-xs text-[#333] leading-[14px] bg-[#ccc] dark:bg-[#6B6B6B] rounded-md shadow-[-6px_0px_6px_2px_#fff] dark:shadow-[-6px_0px_6px_2px_#000]`}
>
T
</div>
) : null}
<ChatSwitch
isChatMode={isChatMode}
onChange={(value: boolean) => {
value && disabledChange();
changeMode(value);
setSourceData(undefined);
}}
/>
</div>
</div>
</div>
);
);
}

View File

@@ -1,32 +1,36 @@
import { useEffect } from "react";
import { isTauri, invoke } from "@tauri-apps/api/core";
import { listen } from "@tauri-apps/api/event";
import {useEffect} from "react";
import {invoke, isTauri} from "@tauri-apps/api/core";
import {listen} from "@tauri-apps/api/event";
const useEscape = () => {
const handleEscape = async (event: KeyboardEvent) => {
if (event.key === "Escape") {
event.preventDefault();
// Hide the Tauri app window when 'Esc' is pressed
await invoke("hide_coco");
console.log("App window hidden successfully.");
}
};
const handleEscape = async (event: KeyboardEvent) => {
if (event.key === "Escape") {
console.log("Escape key pressed.");
useEffect(() => {
if(!isTauri()) return;
const unlisten = listen("tauri://focus", () => {
// Add event listener for keydown
window.addEventListener("keydown", handleEscape);
});
event.preventDefault();
// Cleanup event listener on component unmount
return () => {
unlisten.then((unlistenFn) => unlistenFn());
// Hide the Tauri app window when 'Esc' is pressed
await invoke("hide_coco");
window.removeEventListener("keydown", handleEscape);
console.log("App window hidden successfully.");
}
};
}, []);
useEffect(() => {
if (!isTauri()) return;
const unlisten = listen("tauri://focus", () => {
// Add event listener for keydown
window.addEventListener("keydown", handleEscape);
});
// Cleanup event listener on component unmount
return () => {
unlisten.then((unlistenFn) => unlistenFn());
window.removeEventListener("keydown", handleEscape);
};
}, []);
};
export default useEscape;

View File

@@ -1,124 +1,149 @@
import { useState, useRef, useEffect } from "react";
import { isTauri } from "@tauri-apps/api/core";
import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow";
import { LogicalSize } from "@tauri-apps/api/dpi";
import {useEffect, useRef, useState} from "react";
import {invoke, isTauri} from "@tauri-apps/api/core";
import {getCurrentWebviewWindow} from "@tauri-apps/api/webviewWindow";
import {LogicalSize} from "@tauri-apps/api/dpi";
import InputBox from "@/components/Search/InputBox";
import Search from "@/components/Search/Search";
import ChatAI, { ChatAIRef } from "@/components/Assistant/Chat";
import { useAppStore } from "@/stores/appStore";
import { useAuthStore } from "@/stores/authStore";
import ChatAI, {ChatAIRef} from "@/components/Assistant/Chat";
import {useAppStore} from "@/stores/appStore";
import {useAuthStore} from "@/stores/authStore";
import ApiDetails from "@/components/Common/ApiDetails";
export default function DesktopApp() {
const initializeListeners = useAppStore((state) => state.initializeListeners);
const initializeListeners_auth = useAuthStore(
(state) => state.initializeListeners
);
const initializeListeners = useAppStore((state) => state.initializeListeners);
const initializeListeners_auth = useAuthStore(
(state) => state.initializeListeners
);
useEffect(() => {
initializeListeners();
initializeListeners_auth();
}, []);
useEffect(() => {
initializeListeners();
initializeListeners_auth();
const chatAIRef = useRef<ChatAIRef>(null);
// Listen for window focus and blur events
const handleBlur = () => {
console.log("Window blurred");
invoke('hide_coco').then(() => {
console.log("Hide Coco");
}).finally(() => {
console.log("Hide Coco");
});
};
const [isChatMode, setIsChatMode] = useState(false);
const [input, setInput] = useState("");
const [isTransitioned, setIsTransitioned] = useState(false);
const handleFocus = () => {
// Optionally, show the window if needed when focus is regained
console.log("Window focused");
};
async function changeMode(value: boolean) {
setIsChatMode(value);
setIsTransitioned(value);
}
window.addEventListener('blur', handleBlur);
window.addEventListener('focus', handleFocus);
async function changeInput(value: string) {
setInput(value);
}
// Clean up event listeners on component unmount
return () => {
window.removeEventListener('blur', handleBlur);
window.removeEventListener('focus', handleFocus);
};
const handleSendMessage = async (value: string) => {
setInput(value);
if (isChatMode) {
if (isTauri()) {
await getCurrentWebviewWindow()?.setSize(new LogicalSize(680, 596));
}
chatAIRef.current?.init();
}, []);
const chatAIRef = useRef<ChatAIRef>(null);
const [isChatMode, setIsChatMode] = useState(false);
const [input, setInput] = useState("");
const [isTransitioned, setIsTransitioned] = useState(false);
async function changeMode(value: boolean) {
setIsChatMode(value);
setIsTransitioned(value);
}
};
const cancelChat = () => {
chatAIRef.current?.cancelChat();
};
const reconnect = () => {
chatAIRef.current?.reconnect();
};
const isTyping = false;
async function changeInput(value: string) {
setInput(value);
}
return (
<div
data-tauri-drag-region
className={`w-full h-full m-auto rounded-xl overflow-hidden relative border border-[#E6E6E6] dark:border-[#272626] ${
isTransitioned
? "bg-chat_bg_light dark:bg-chat_bg_dark"
: "bg-search_bg_light dark:bg-search_bg_dark"
} bg-no-repeat bg-cover bg-center`}
>
<div
data-tauri-drag-region
className={`p-2 pb-0 absolute w-full flex items-center justify-center transition-all duration-500 ${
isTransitioned
? "top-[500px] h-[90px] border-t"
: "top-0 h-[90px] border-b"
} border-[#E6E6E6] dark:border-[#272626]`}
>
<InputBox
isChatMode={isChatMode}
inputValue={input}
onSend={handleSendMessage}
disabled={isTyping}
disabledChange={() => {
cancelChat();
}}
changeMode={changeMode}
changeInput={changeInput}
reconnect={reconnect}
/>
</div>
const handleSendMessage = async (value: string) => {
setInput(value);
if (isChatMode) {
if (isTauri()) {
await getCurrentWebviewWindow()?.setSize(new LogicalSize(680, 596));
}
chatAIRef.current?.init();
}
};
const cancelChat = () => {
chatAIRef.current?.cancelChat();
};
<div
data-tauri-drag-region
className={`absolute w-full transition-opacity duration-500 ${
isTransitioned ? "opacity-0 pointer-events-none" : "opacity-100"
} bottom-0 h-[500px] `}
>
<Search
key="Search"
input={input}
isChatMode={isChatMode}
changeInput={changeInput}
/>
</div>
const reconnect = () => {
chatAIRef.current?.reconnect();
};
const isTyping = false;
<div
data-tauri-drag-region
className={`absolute w-full transition-all duration-500 ${
isTransitioned
? "top-0 opacity-100 pointer-events-auto"
: "-top-[506px] opacity-0 pointer-events-none"
} h-[500px]`}
>
{isTransitioned && isChatMode ? (
<ChatAI
ref={chatAIRef}
key="ChatAI"
inputValue={input}
isTransitioned={isTransitioned}
changeInput={changeInput}
/>
) : null}
</div>
return (
<div
data-tauri-drag-region
className={`w-full h-full m-auto rounded-xl overflow-hidden relative border border-[#E6E6E6] dark:border-[#272626] ${
isTransitioned
? "bg-chat_bg_light dark:bg-chat_bg_dark"
: "bg-search_bg_light dark:bg-search_bg_dark"
} bg-no-repeat bg-cover bg-center`}
>
<div
data-tauri-drag-region
className={`p-2 pb-0 absolute w-full flex items-center justify-center transition-all duration-500 ${
isTransitioned
? "top-[500px] h-[90px] border-t"
: "top-0 h-[90px] border-b"
} border-[#E6E6E6] dark:border-[#272626]`}
>
<InputBox
isChatMode={isChatMode}
inputValue={input}
onSend={handleSendMessage}
disabled={isTyping}
disabledChange={() => {
cancelChat();
}}
changeMode={changeMode}
changeInput={changeInput}
reconnect={reconnect}
/>
</div>
<ApiDetails />
</div>
);
<div
data-tauri-drag-region
className={`absolute w-full transition-opacity duration-500 ${
isTransitioned ? "opacity-0 pointer-events-none" : "opacity-100"
} bottom-0 h-[500px] `}
>
<Search
key="Search"
input={input}
isChatMode={isChatMode}
changeInput={changeInput}
/>
</div>
<div
data-tauri-drag-region
className={`absolute w-full transition-all duration-500 ${
isTransitioned
? "top-0 opacity-100 pointer-events-auto"
: "-top-[506px] opacity-0 pointer-events-none"
} h-[500px]`}
>
{isTransitioned && isChatMode ? (
<ChatAI
ref={chatAIRef}
key="ChatAI"
inputValue={input}
isTransitioned={isTransitioned}
changeInput={changeInput}
/>
) : null}
</div>
<ApiDetails/>
</div>
);
}