mirror of
https://github.com/infinilabs/coco-app.git
synced 2025-12-16 19:47:43 +01:00
feat: add new UI about no-data page (#80)
* feat: new UI redesign * feat: add new ui no-data page
This commit is contained in:
6
.vscode/settings.json
vendored
6
.vscode/settings.json
vendored
@@ -12,6 +12,7 @@
|
||||
"INFINI",
|
||||
"inputbox",
|
||||
"katex",
|
||||
"khtml",
|
||||
"localstorage",
|
||||
"lucide",
|
||||
"maximizable",
|
||||
@@ -38,5 +39,8 @@
|
||||
"[rust]": {
|
||||
"editor.defaultFormatter": "rust-lang.rust-analyzer",
|
||||
"editor.formatOnSave": true
|
||||
}
|
||||
},
|
||||
"i18n-ally.localesPaths": [
|
||||
"src/locales"
|
||||
]
|
||||
}
|
||||
106
src-tauri/Cargo.lock
generated
106
src-tauri/Cargo.lock
generated
@@ -358,9 +358,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "brotli"
|
||||
version = "6.0.0"
|
||||
version = "7.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "74f7971dbd9326d58187408ab83117d8ac1bb9c17b085fdacd1cf2f598719b6b"
|
||||
checksum = "cc97b8f16f944bba54f0433f07e30be199b6dc2bd25937444bbad560bcea29bd"
|
||||
dependencies = [
|
||||
"alloc-no-stdlib",
|
||||
"alloc-stdlib",
|
||||
@@ -546,6 +546,7 @@ dependencies = [
|
||||
"serde_json",
|
||||
"tauri",
|
||||
"tauri-build",
|
||||
"tauri-nspanel",
|
||||
"tauri-plugin-autostart",
|
||||
"tauri-plugin-global-shortcut",
|
||||
"tauri-plugin-http",
|
||||
@@ -1143,15 +1144,6 @@ dependencies = [
|
||||
"miniz_oxide",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fluent-uri"
|
||||
version = "0.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "17c704e9dbe1ddd863da1e6ff3567795087b1eb201ce80d8fa81162e1516500d"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fnv"
|
||||
version = "1.0.7"
|
||||
@@ -2002,9 +1994,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "json-patch"
|
||||
version = "2.0.0"
|
||||
version = "3.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5b1fb8864823fad91877e6caea0baca82e49e8db50f8e5c9f9a453e27d3330fc"
|
||||
checksum = "863726d7afb6bc2590eeff7135d923545e5e964f004c2ccf8716c25e70a86f08"
|
||||
dependencies = [
|
||||
"jsonptr",
|
||||
"serde",
|
||||
@@ -2014,11 +2006,10 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "jsonptr"
|
||||
version = "0.4.7"
|
||||
version = "0.6.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1c6e529149475ca0b2820835d3dce8fcc41c6b943ca608d32f35b449255e4627"
|
||||
checksum = "5dea2b27dd239b2556ed7a25ba842fe47fd602e7fc7433c2a8d6106d4d9edd70"
|
||||
dependencies = [
|
||||
"fluent-uri",
|
||||
"serde",
|
||||
"serde_json",
|
||||
]
|
||||
@@ -2337,6 +2328,17 @@ dependencies = [
|
||||
"malloc_buf",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc-foundation"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1add1b659e36c9607c7aab864a76c7a4c2760cd0cd2e120f3fb8b952c7e22bf9"
|
||||
dependencies = [
|
||||
"block",
|
||||
"objc",
|
||||
"objc_id",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc-sys"
|
||||
version = "0.3.5"
|
||||
@@ -2555,6 +2557,15 @@ dependencies = [
|
||||
"objc2-foundation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc_id"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c92d4ddb4bd7b50d730c215ff871754d0da6b2178849f8a2a2ab69712d0c073b"
|
||||
dependencies = [
|
||||
"objc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "object"
|
||||
version = "0.36.5"
|
||||
@@ -3844,9 +3855,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tao"
|
||||
version = "0.30.3"
|
||||
version = "0.30.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a0dbbebe82d02044dfa481adca1550d6dd7bd16e086bc34fa0fbecceb5a63751"
|
||||
checksum = "6682a07cf5bab0b8a2bd20d0a542917ab928b5edb75ebd4eda6b05cbaab872da"
|
||||
dependencies = [
|
||||
"bitflags 2.6.0",
|
||||
"cocoa",
|
||||
@@ -3900,9 +3911,9 @@ checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1"
|
||||
|
||||
[[package]]
|
||||
name = "tauri"
|
||||
version = "2.0.5"
|
||||
version = "2.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5ce2818e803ce3097987296623ed8c0d9f65ed93b4137ff9a83e168bdbf62932"
|
||||
checksum = "e545de0a2dfe296fa67db208266cd397c5a55ae782da77973ef4c4fac90e9f2c"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bytes",
|
||||
@@ -3938,7 +3949,7 @@ dependencies = [
|
||||
"tauri-runtime",
|
||||
"tauri-runtime-wry",
|
||||
"tauri-utils",
|
||||
"thiserror 1.0.64",
|
||||
"thiserror 2.0.6",
|
||||
"tokio",
|
||||
"tray-icon",
|
||||
"url",
|
||||
@@ -3951,9 +3962,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tauri-build"
|
||||
version = "2.0.1"
|
||||
version = "2.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "935f9b3c49b22b3e2e485a57f46d61cd1ae07b1cbb2ba87387a387caf2d8c4e7"
|
||||
checksum = "7bd2a4bcfaf5fb9f4be72520eefcb61ae565038f8ccba2a497d8c28f463b8c01"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"cargo_toml",
|
||||
@@ -3973,9 +3984,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tauri-codegen"
|
||||
version = "2.0.1"
|
||||
version = "2.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "95d7443dd4f0b597704b6a14b964ee2ed16e99928d8e6292ae9825f09fbcd30e"
|
||||
checksum = "bf79faeecf301d3e969b1fae977039edb77a4c1f25cc0a961be298b54bff97cf"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"brotli",
|
||||
@@ -3991,7 +4002,7 @@ dependencies = [
|
||||
"sha2",
|
||||
"syn 2.0.90",
|
||||
"tauri-utils",
|
||||
"thiserror 1.0.64",
|
||||
"thiserror 2.0.6",
|
||||
"time",
|
||||
"url",
|
||||
"uuid",
|
||||
@@ -4000,9 +4011,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tauri-macros"
|
||||
version = "2.0.1"
|
||||
version = "2.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4d2c0963ccfc3f5194415f2cce7acc975942a8797fbabfb0aa1ed6f59326ae7f"
|
||||
checksum = "c52027c8c5afb83166dacddc092ee8fff50772f9646d461d8c33ee887e447a03"
|
||||
dependencies = [
|
||||
"heck 0.5.0",
|
||||
"proc-macro2",
|
||||
@@ -4012,6 +4023,22 @@ dependencies = [
|
||||
"tauri-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-nspanel"
|
||||
version = "2.0.0"
|
||||
source = "git+https://github.com/ahkohd/tauri-nspanel?branch=v2#23b30f0f1974c35673db3234f1f1bd214fa9c4e9"
|
||||
dependencies = [
|
||||
"bitflags 2.6.0",
|
||||
"block",
|
||||
"cocoa",
|
||||
"core-foundation 0.10.0",
|
||||
"core-graphics",
|
||||
"objc",
|
||||
"objc-foundation",
|
||||
"objc_id",
|
||||
"tauri",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin"
|
||||
version = "2.0.1"
|
||||
@@ -4164,9 +4191,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tauri-runtime"
|
||||
version = "2.1.0"
|
||||
version = "2.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c8f437293d6f5e5dce829250f4dbdce4e0b52905e297a6689cc2963eb53ac728"
|
||||
checksum = "cce18d43f80d4aba3aa8a0c953bbe835f3d0f2370aca75e8dbb14bd4bab27958"
|
||||
dependencies = [
|
||||
"dpi",
|
||||
"gtk",
|
||||
@@ -4176,16 +4203,16 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tauri-utils",
|
||||
"thiserror 1.0.64",
|
||||
"thiserror 2.0.6",
|
||||
"url",
|
||||
"windows",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-runtime-wry"
|
||||
version = "2.1.1"
|
||||
version = "2.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1431602bcc71f2f840ad623915c9842ecc32999b867c4a787d975a17a9625cc6"
|
||||
checksum = "9f442a38863e10129ffe2cec7bd09c2dcf8a098a3a27801a476a304d5bb991d2"
|
||||
dependencies = [
|
||||
"gtk",
|
||||
"http",
|
||||
@@ -4209,9 +4236,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tauri-utils"
|
||||
version = "2.0.1"
|
||||
version = "2.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c38b0230d6880cf6dd07b6d7dd7789a0869f98ac12146e0d18d1c1049215a045"
|
||||
checksum = "9271a88f99b4adea0dc71d0baca4505475a0bbd139fb135f62958721aaa8fe54"
|
||||
dependencies = [
|
||||
"brotli",
|
||||
"cargo_metadata",
|
||||
@@ -4219,6 +4246,7 @@ dependencies = [
|
||||
"dunce",
|
||||
"glob",
|
||||
"html5ever",
|
||||
"http",
|
||||
"infer",
|
||||
"json-patch",
|
||||
"kuchikiki",
|
||||
@@ -4235,7 +4263,7 @@ dependencies = [
|
||||
"serde_json",
|
||||
"serde_with",
|
||||
"swift-rs",
|
||||
"thiserror 1.0.64",
|
||||
"thiserror 2.0.6",
|
||||
"toml 0.8.2",
|
||||
"url",
|
||||
"urlpattern",
|
||||
@@ -5356,12 +5384,13 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wry"
|
||||
version = "0.46.3"
|
||||
version = "0.47.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cd5cdf57c66813d97601181349c63b96994b3074fc3d7a31a8cce96e968e3bbd"
|
||||
checksum = "61ce51277d65170f6379d8cda935c80e3c2d1f0ff712a123c8bddb11b31a4b73"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"block2",
|
||||
"cookie",
|
||||
"crossbeam-channel",
|
||||
"dpi",
|
||||
"dunce",
|
||||
@@ -5386,6 +5415,7 @@ dependencies = [
|
||||
"soup3",
|
||||
"tao-macros",
|
||||
"thiserror 1.0.64",
|
||||
"url",
|
||||
"webkit2gtk",
|
||||
"webkit2gtk-sys",
|
||||
"webview2-com",
|
||||
|
||||
@@ -18,7 +18,7 @@ crate-type = ["staticlib", "cdylib", "rlib"]
|
||||
tauri-build = { version = "2.0.0", features = [] }
|
||||
|
||||
[dependencies]
|
||||
tauri = { version = "2.0.0", features = ["macos-private-api", "tray-icon", "image-png", "unstable"] }
|
||||
tauri = { version = "2.0.6", features = ["macos-private-api", "tray-icon", "image-png", "unstable"] }
|
||||
tauri-plugin-shell = "2.0.0"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
@@ -27,6 +27,8 @@ 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" }
|
||||
|
||||
[profile.dev]
|
||||
incremental = true # Compile your binary in smaller steps.
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use std::{fs::create_dir, io::Read};
|
||||
|
||||
use tauri::{AppHandle, Emitter, Listener, Manager, Runtime, WebviewWindow};
|
||||
use tauri_nspanel::{panel_delegate, ManagerExt, WebviewWindowExt};
|
||||
use tauri_plugin_autostart::MacosLauncher;
|
||||
use tauri_plugin_global_shortcut::{GlobalShortcutExt, Shortcut};
|
||||
|
||||
@@ -31,6 +32,29 @@ fn change_window_height(handle: AppHandle, height: u32) {
|
||||
window.set_size(size).unwrap();
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn show_panel(handle: AppHandle) {
|
||||
let panel = handle.get_webview_panel("main").unwrap();
|
||||
|
||||
panel.show();
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn hide_panel(handle: AppHandle) {
|
||||
let panel = handle.get_webview_panel("main").unwrap();
|
||||
|
||||
panel.order_out(None);
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn close_panel(handle: AppHandle) {
|
||||
let panel = handle.get_webview_panel("main").unwrap();
|
||||
|
||||
panel.released_when_closed(true);
|
||||
|
||||
panel.close();
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
struct ThemeChangedPayload {
|
||||
is_dark_mode: bool,
|
||||
@@ -41,6 +65,7 @@ pub fn run() {
|
||||
let mut ctx = tauri::generate_context!();
|
||||
|
||||
tauri::Builder::default()
|
||||
.plugin(tauri_nspanel::init())
|
||||
.plugin(tauri_plugin_http::init())
|
||||
.plugin(tauri_plugin_shell::init())
|
||||
.plugin(tauri_plugin_autostart::init(
|
||||
@@ -56,6 +81,9 @@ pub fn run() {
|
||||
change_autostart,
|
||||
hide_coco,
|
||||
switch_tray_icon,
|
||||
show_panel,
|
||||
hide_panel,
|
||||
close_panel
|
||||
])
|
||||
.setup(|app| {
|
||||
init(app.app_handle());
|
||||
@@ -80,7 +108,34 @@ pub fn run() {
|
||||
.expect("error while running tauri application");
|
||||
}
|
||||
|
||||
fn init(_app_handle: &AppHandle) {}
|
||||
fn init(app_handle: &AppHandle) {
|
||||
let window: WebviewWindow = app_handle.get_webview_window("main").unwrap();
|
||||
|
||||
let panel = window.to_panel().unwrap();
|
||||
|
||||
let delegate = panel_delegate!(MyPanelDelegate {
|
||||
window_did_become_key,
|
||||
window_did_resign_key
|
||||
});
|
||||
|
||||
let handle = app_handle.to_owned();
|
||||
|
||||
delegate.set_listener(Box::new(move |delegate_name: String| {
|
||||
match delegate_name.as_str() {
|
||||
"window_did_become_key" => {
|
||||
let app_name = handle.package_info().name.to_owned();
|
||||
|
||||
println!("[info]: {:?} panel becomes key window!", app_name);
|
||||
}
|
||||
"window_did_resign_key" => {
|
||||
println!("[info]: panel resigned from key window!");
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}));
|
||||
|
||||
panel.set_delegate(delegate);
|
||||
}
|
||||
|
||||
fn enable_shortcut(app: &mut tauri::App) {
|
||||
use tauri_plugin_global_shortcut::{GlobalShortcutExt, Shortcut, ShortcutState};
|
||||
|
||||
@@ -13,17 +13,20 @@
|
||||
"macOSPrivateApi": true,
|
||||
"windows": [
|
||||
{
|
||||
"decorations": false,
|
||||
"height": 90,
|
||||
"width": 680,
|
||||
"minimizable": true,
|
||||
"shadow": true,
|
||||
"title": "Coco AI",
|
||||
"transparent": true,
|
||||
"url": "/ui",
|
||||
"height": 590,
|
||||
"width": 680,
|
||||
"decorations": false,
|
||||
"minimizable": false,
|
||||
"maximizable": false,
|
||||
"shadow": false,
|
||||
"transparent": true,
|
||||
"fullscreen": false,
|
||||
"center": false,
|
||||
"windowEffects": {
|
||||
"effects": [],
|
||||
"radius": 20
|
||||
"radius": 12
|
||||
}
|
||||
}
|
||||
],
|
||||
|
||||
BIN
src/assets/coconut-tree.png
Normal file
BIN
src/assets/coconut-tree.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 7.5 KiB |
65
src/components/AppAI/AutoResizeTextarea.tsx
Normal file
65
src/components/AppAI/AutoResizeTextarea.tsx
Normal file
@@ -0,0 +1,65 @@
|
||||
import { useEffect, useRef, useImperativeHandle, forwardRef } from "react";
|
||||
|
||||
interface AutoResizeTextareaProps {
|
||||
input: string;
|
||||
setInput: (value: string) => void;
|
||||
handleKeyDown?: (e: React.KeyboardEvent<HTMLTextAreaElement>) => void;
|
||||
}
|
||||
|
||||
// Forward ref to allow parent to interact with this component
|
||||
const AutoResizeTextarea = forwardRef<
|
||||
{ reset: () => void; focus: () => void },
|
||||
AutoResizeTextareaProps
|
||||
>(({ input, setInput, handleKeyDown }, ref) => {
|
||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const textarea = textareaRef.current;
|
||||
if (textarea) {
|
||||
const prevHeight = textarea.style.height;
|
||||
textarea.style.height = "auto"; // Reset height to recalculate
|
||||
if (textarea.style.height !== prevHeight) {
|
||||
textarea.style.height = `${textarea.scrollHeight}px`; // Adjust based on content
|
||||
}
|
||||
}
|
||||
}, [input]);
|
||||
|
||||
// Expose methods to the parent via ref
|
||||
useImperativeHandle(ref, () => ({
|
||||
reset: () => {
|
||||
setInput("");
|
||||
const textarea = textareaRef.current;
|
||||
if (textarea) {
|
||||
textarea.style.height = "auto";
|
||||
}
|
||||
},
|
||||
focus: () => {
|
||||
textareaRef.current?.focus();
|
||||
},
|
||||
}));
|
||||
|
||||
return (
|
||||
<textarea
|
||||
ref={textareaRef}
|
||||
autoFocus
|
||||
autoComplete="off"
|
||||
autoCapitalize="none"
|
||||
spellCheck="false"
|
||||
className="text-base flex-1 outline-none min-w-[200px] text-[#333] dark:text-[#d8d8d8] placeholder-text-xs placeholder-[#999] dark:placeholder-gray-500 bg-transparent"
|
||||
placeholder="Ask whatever you want ..."
|
||||
aria-label="Ask whatever you want ..."
|
||||
value={input}
|
||||
onChange={(e) => setInput(e.target.value)}
|
||||
onKeyDown={(e) => handleKeyDown?.(e)}
|
||||
rows={1}
|
||||
style={{
|
||||
resize: "none", // Prevent manual resize
|
||||
overflow: "auto", // Enable scrollbars when needed
|
||||
maxHeight: "4.5rem", // Limit height to 3 rows (3 * 1.5 line-height)
|
||||
lineHeight: "1.5rem", // Line height to match row height
|
||||
}}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
export default AutoResizeTextarea;
|
||||
161
src/components/AppAI/DropdownList.tsx
Normal file
161
src/components/AppAI/DropdownList.tsx
Normal file
@@ -0,0 +1,161 @@
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { isTauri } from "@tauri-apps/api/core";
|
||||
|
||||
interface DropdownListProps {
|
||||
selected: (item: any) => void;
|
||||
suggests: any[];
|
||||
isSearchComplete: boolean;
|
||||
}
|
||||
|
||||
function DropdownList({ selected, suggests }: DropdownListProps) {
|
||||
const [selectedItem, setSelectedItem] = useState<number | null>(null);
|
||||
const [showIndex, setShowIndex] = useState<boolean>(false);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const itemRefs = useRef<(HTMLDivElement | null)[]>([]);
|
||||
|
||||
const handleOpenURL = async (url: string) => {
|
||||
if (!url) return;
|
||||
try {
|
||||
if (isTauri()) {
|
||||
const { open } = await import("@tauri-apps/plugin-shell");
|
||||
await open(url);
|
||||
console.log("URL opened in default browser");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to open URL:", error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
console.log(
|
||||
"handleKeyDown",
|
||||
e.key,
|
||||
showIndex,
|
||||
e.key >= "0" && e.key <= "9" && showIndex
|
||||
);
|
||||
if (!suggests.length) return;
|
||||
|
||||
if (e.key === "ArrowUp") {
|
||||
e.preventDefault();
|
||||
setSelectedItem((prev) =>
|
||||
prev === null || prev === 0 ? suggests.length - 1 : prev - 1
|
||||
);
|
||||
} else if (e.key === "ArrowDown") {
|
||||
e.preventDefault();
|
||||
setSelectedItem((prev) =>
|
||||
prev === null || prev === suggests.length - 1 ? 0 : prev + 1
|
||||
);
|
||||
} else if (e.key === "Meta") {
|
||||
e.preventDefault();
|
||||
setShowIndex(true);
|
||||
}
|
||||
|
||||
if (e.key === "Enter" && selectedItem !== null) {
|
||||
console.log("Enter key pressed", selectedItem);
|
||||
const item = suggests[selectedItem];
|
||||
if (item?._source?.url) {
|
||||
handleOpenURL(item?._source?.url);
|
||||
} else {
|
||||
selected(item);
|
||||
}
|
||||
}
|
||||
|
||||
if (e.key >= "0" && e.key <= "9" && showIndex) {
|
||||
console.log(`number ${e.key}`);
|
||||
const item = suggests[parseInt(e.key, 10)];
|
||||
if (item?._source?.url) {
|
||||
handleOpenURL(item?._source?.url);
|
||||
} else {
|
||||
selected(item);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleKeyUp = (e: KeyboardEvent) => {
|
||||
console.log("handleKeyUp", e.key);
|
||||
if (!suggests.length) return;
|
||||
|
||||
if (!e.metaKey) {
|
||||
setShowIndex(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener("keydown", handleKeyDown);
|
||||
window.addEventListener("keyup", handleKeyUp);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("keydown", handleKeyDown);
|
||||
window.removeEventListener("keyup", handleKeyUp);
|
||||
};
|
||||
}, [showIndex, selectedItem, suggests]);
|
||||
|
||||
useEffect(() => {
|
||||
if (suggests.length > 0) {
|
||||
setSelectedItem(0);
|
||||
}
|
||||
}, [JSON.stringify(suggests)]);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedItem !== null && itemRefs.current[selectedItem]) {
|
||||
itemRefs.current[selectedItem]?.scrollIntoView({
|
||||
behavior: "smooth",
|
||||
block: "nearest",
|
||||
});
|
||||
}
|
||||
}, [selectedItem]);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
data-tauri-drag-region
|
||||
className="h-[458px] w-full p-2 flex flex-col overflow-y-auto custom-scrollbar focus:outline-none"
|
||||
tabIndex={0}
|
||||
>
|
||||
<div className="p-2 text-xs text-[#999] dark:text-[#666]">Results</div>
|
||||
{suggests?.map((item, index) => {
|
||||
const isSelected = selectedItem === index;
|
||||
return (
|
||||
<div
|
||||
key={item._id}
|
||||
ref={(el) => (itemRefs.current[index] = el)}
|
||||
onMouseEnter={() => setSelectedItem(index)}
|
||||
onClick={() => {
|
||||
if (item?._source?.url) {
|
||||
handleOpenURL(item?._source?.url);
|
||||
} else {
|
||||
selected(item);
|
||||
}
|
||||
}}
|
||||
className={`w-full px-2 py-2.5 text-sm flex items-center justify-between rounded-lg transition-colors ${
|
||||
isSelected
|
||||
? "bg-[rgba(0,0,0,0.1)] dark:bg-[rgba(255,255,255,0.1)] hover:bg-[rgba(0,0,0,0.1)] dark:hover:bg-[rgba(255,255,255,0.1)]"
|
||||
: ""
|
||||
}`}
|
||||
>
|
||||
<div className="flex gap-2 items-center">
|
||||
<img className="w-5 h-5" src={item?._source?.icon} alt="icon" />
|
||||
<span className="text-[#333] dark:text-[#d8d8d8] truncate w-80 text-left">
|
||||
{item?._source?.title}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex gap-2 items-center relative">
|
||||
<span className="text-sm text-[#666] dark:text-[#666] truncate w-52 text-right">
|
||||
{item?._source?.source}
|
||||
</span>
|
||||
{showIndex && index < 10 ? (
|
||||
<div
|
||||
className={`absolute right-0 w-4 h-4 flex items-center justify-center font-normal text-xs text-[#333] leading-[14px] bg-[#ccc] dark:bg-[#6B6B6B] shadow-[-6px_0px_6px_2px_#e6e6e6] dark:shadow-[-6px_0px_6px_2px_#000] rounded-md`}
|
||||
>
|
||||
{index}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default DropdownList;
|
||||
40
src/components/AppAI/Footer.tsx
Normal file
40
src/components/AppAI/Footer.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
import {
|
||||
Command,
|
||||
ArrowDown01,
|
||||
AppWindowMac,
|
||||
CornerDownLeft,
|
||||
} from "lucide-react";
|
||||
|
||||
interface FooterProps {
|
||||
isChat: boolean;
|
||||
name?: string;
|
||||
}
|
||||
|
||||
export default function Footer({ name }: FooterProps) {
|
||||
return (
|
||||
<div
|
||||
data-tauri-drag-region
|
||||
className="px-4 z-999 mx-[1px] h-10 absolute bottom-0 left-0 right-0 border-t border-gray-200 dark:border-gray-700 flex items-center justify-between rounded-xl rounded-t-none overflow-hidden"
|
||||
>
|
||||
<div className="flex items-center">
|
||||
{name ? (
|
||||
<div className="flex gap-2 items-center text-[#666] text-xs">
|
||||
<AppWindowMac className="w-5 h-5" /> {name}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="gap-1 flex items-center text-[#666] dark:text-[#666] text-sm">
|
||||
<span className="mr-1.5 ">Quick open</span>
|
||||
<Command className="w-5 h-5 p-1 border rounded-[6px] dark:text-[#666] dark:border-[rgba(255,255,255,0.15)]" />
|
||||
<ArrowDown01 className="w-5 h-5 p-1 border rounded-[6px] dark:text-[#666] dark:border-[rgba(255,255,255,0.15)]" />
|
||||
</div>
|
||||
<div className="flex items-center text-[#666] dark:text-[#666] text-sm">
|
||||
<span className="mr-1.5 ">Open</span>
|
||||
<CornerDownLeft className="w-5 h-5 p-1 border rounded-[6px] dark:text-[#666] dark:border-[rgba(255,255,255,0.15)]" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
311
src/components/AppAI/InputBox.tsx
Normal file
311
src/components/AppAI/InputBox.tsx
Normal file
@@ -0,0 +1,311 @@
|
||||
import { Library, Mic, Send, Plus, AudioLines, Image } 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 ChatSwitch from "../SearchChat/ChatSwitch";
|
||||
import AutoResizeTextarea from "./AutoResizeTextarea";
|
||||
import { useChatStore } from "@/stores/chatStore";
|
||||
import StopIcon from "@/icons/Stop";
|
||||
import { useAppStore } from "@/stores/appStore";
|
||||
|
||||
interface ChatInputProps {
|
||||
onSend: (message: string) => void;
|
||||
disabled: boolean;
|
||||
disabledChange: () => void;
|
||||
changeMode: (isChatMode: boolean) => void;
|
||||
isChatMode: boolean;
|
||||
inputValue: string;
|
||||
changeInput: (val: string) => void;
|
||||
}
|
||||
|
||||
export default function ChatInput({
|
||||
onSend,
|
||||
disabled,
|
||||
changeMode,
|
||||
isChatMode,
|
||||
inputValue,
|
||||
changeInput,
|
||||
disabledChange,
|
||||
}: ChatInputProps) {
|
||||
const showTooltip = useAppStore((state) => state.showTooltip);
|
||||
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const textareaRef = useRef<{ reset: () => void; focus: () => void }>(null);
|
||||
|
||||
const { curChatEnd } = useChatStore();
|
||||
|
||||
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 handleKeyDown = useCallback(
|
||||
(e: KeyboardEvent) => {
|
||||
if (e.code === "MetaLeft" || e.code === "MetaRight") {
|
||||
setIsCommandPressed(true);
|
||||
}
|
||||
|
||||
if (e.metaKey) {
|
||||
switch (e.code) {
|
||||
case "KeyI":
|
||||
handleToggleFocus();
|
||||
break;
|
||||
case "KeyM":
|
||||
console.log("KeyM");
|
||||
break;
|
||||
case "Enter":
|
||||
isChatMode && handleSubmit();
|
||||
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]
|
||||
);
|
||||
|
||||
const handleKeyUp = useCallback((e: KeyboardEvent) => {
|
||||
if (e.code === "MetaLeft" || e.code === "MetaRight") {
|
||||
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.");
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-full relative">
|
||||
<div className="p-[12px] 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 ? (
|
||||
<AutoResizeTextarea
|
||||
ref={textareaRef}
|
||||
input={inputValue}
|
||||
setInput={changeInput}
|
||||
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 ? (
|
||||
<div
|
||||
className={`absolute bg-black bg-opacity-70 text-white font-bold px-2 py-0.5 rounded-md text-xs transition-opacity duration-200`}
|
||||
>
|
||||
⌘ + 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-16 bg-black bg-opacity-70 text-white font-bold px-2 py-0.5 rounded-md text-xs transition-opacity duration-200`}
|
||||
>
|
||||
⌘ + M
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{showTooltip && isChatMode && isCommandPressed ? (
|
||||
<div
|
||||
className={`absolute right-1 bg-black bg-opacity-70 text-white font-bold px-2 py-0.5 rounded-md text-xs transition-opacity duration-200`}
|
||||
>
|
||||
⌘ + ↩︎
|
||||
</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 bg-black bg-opacity-70 text-white font-bold px-2 py-0.5 rounded-md text-xs transition-opacity duration-200`}
|
||||
>
|
||||
⌘ + O
|
||||
</div>
|
||||
) : null}
|
||||
{showTooltip && isCommandPressed ? (
|
||||
<div
|
||||
className={`absolute left-16 bg-black bg-opacity-70 text-white font-bold px-2 py-0.5 rounded-md text-xs transition-opacity duration-200`}
|
||||
>
|
||||
⌘ + 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 bg-black bg-opacity-70 text-white font-bold px-2 py-0.5 rounded-md text-xs transition-opacity duration-200`}
|
||||
>
|
||||
⌘ + N
|
||||
</div>
|
||||
) : null}
|
||||
{showTooltip && isCommandPressed ? (
|
||||
<div
|
||||
className={`absolute left-14 bg-black bg-opacity-70 text-white font-bold px-2 py-0.5 rounded-md text-xs transition-opacity duration-200`}
|
||||
>
|
||||
⌘ + G
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="relative w-24 flex justify-end items-center">
|
||||
{showTooltip && isCommandPressed ? (
|
||||
<div
|
||||
className={`absolute left-0 z-10 bg-black bg-opacity-70 text-white font-bold px-2 py-0.5 rounded-md text-xs transition-opacity duration-200`}
|
||||
>
|
||||
⌘ + T
|
||||
</div>
|
||||
) : null}
|
||||
<ChatSwitch
|
||||
isChatMode={isChatMode}
|
||||
onChange={(value) => {
|
||||
value && disabledChange();
|
||||
changeMode(value);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
128
src/components/AppAI/Search.tsx
Normal file
128
src/components/AppAI/Search.tsx
Normal file
@@ -0,0 +1,128 @@
|
||||
import { useEffect, useState, useCallback, useRef } from "react";
|
||||
import { Command } from "lucide-react";
|
||||
|
||||
// import { isTauri } from "@tauri-apps/api/core";
|
||||
|
||||
import DropdownList from "./DropdownList";
|
||||
import Footer from "./Footer";
|
||||
import { tauriFetch } from "@/api/tauriFetchClient";
|
||||
import noDataImg from "@/assets/coconut-tree.png";
|
||||
|
||||
interface SearchProps {
|
||||
changeInput: (val: string) => void;
|
||||
isChatMode: boolean;
|
||||
input: string;
|
||||
}
|
||||
|
||||
function Search({ isChatMode, input }: SearchProps) {
|
||||
const [suggests, setSuggests] = useState<any[]>([]);
|
||||
const [isSearchComplete, setIsSearchComplete] = useState(false);
|
||||
const [selectedItem, setSelectedItem] = useState<any>();
|
||||
|
||||
const mainWindowRef = useRef<HTMLDivElement>(null);
|
||||
// useEffect(() => {
|
||||
// if (!isTauri()) return;
|
||||
// const element = mainWindowRef.current;
|
||||
// if (!element) return;
|
||||
|
||||
// const resizeObserver = new ResizeObserver(async (entries) => {
|
||||
// const { getCurrentWebviewWindow } = await import(
|
||||
// "@tauri-apps/api/webviewWindow"
|
||||
// );
|
||||
// const { LogicalSize } = await import("@tauri-apps/api/dpi");
|
||||
|
||||
// for (let entry of entries) {
|
||||
// let newHeight = entry.contentRect.height;
|
||||
// console.log("Height updated:", newHeight);
|
||||
// newHeight = newHeight + 90 + (newHeight === 0 ? 0 : 46);
|
||||
// await getCurrentWebviewWindow()?.setSize(
|
||||
// new LogicalSize(680, newHeight)
|
||||
// );
|
||||
// }
|
||||
// });
|
||||
|
||||
// resizeObserver.observe(element);
|
||||
|
||||
// return () => {
|
||||
// resizeObserver.disconnect();
|
||||
// };
|
||||
// }, [suggests]);
|
||||
|
||||
const getSuggest = async () => {
|
||||
if (!input) return;
|
||||
//
|
||||
// const list = [];
|
||||
// for (let i = 0; i < input.length; i++) {
|
||||
// list.push({
|
||||
// _source: { url: `https://www.google.com/search?q=${i}`, _id: i },
|
||||
// });
|
||||
// }
|
||||
// setSuggests(list);
|
||||
// return;
|
||||
//
|
||||
try {
|
||||
const response = await tauriFetch({
|
||||
url: `/query/_search?query=${input}`,
|
||||
method: "GET",
|
||||
});
|
||||
console.log("_suggest", input, response);
|
||||
const data = response.data?.hits?.hits || [];
|
||||
setSuggests(data);
|
||||
|
||||
setIsSearchComplete(true);
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch user data:", error);
|
||||
}
|
||||
};
|
||||
|
||||
function debounce(fn: Function, delay: number) {
|
||||
let timer: NodeJS.Timeout;
|
||||
return (...args: any[]) => {
|
||||
clearTimeout(timer);
|
||||
timer = setTimeout(() => fn(...args), delay);
|
||||
};
|
||||
}
|
||||
|
||||
const debouncedSearch = useCallback(debounce(getSuggest, 300), [input]);
|
||||
|
||||
useEffect(() => {
|
||||
!isChatMode && debouncedSearch();
|
||||
if (!input) setSuggests([]);
|
||||
}, [input]);
|
||||
|
||||
return (
|
||||
<div ref={mainWindowRef} className={`h-[500px] pb-10 w-full relative`}>
|
||||
{/* Search Results Panel */}
|
||||
{suggests.length > 0 ? (
|
||||
<DropdownList
|
||||
suggests={suggests}
|
||||
isSearchComplete={isSearchComplete}
|
||||
selected={(item) => setSelectedItem(item)}
|
||||
/>
|
||||
) : (
|
||||
<div
|
||||
data-tauri-drag-region
|
||||
className="h-full w-full flex flex-col items-center"
|
||||
>
|
||||
<img src={noDataImg} alt="no-data" className="w-16 h-16 mt-24" />
|
||||
<div className="mt-4 text-sm text-[#999] dark:text-[#666]">
|
||||
No Results
|
||||
</div>
|
||||
<div className="mt-10 text-sm text-[#333] dark:text-[#D8D8D8] flex">
|
||||
Ask Coco AI
|
||||
<span className="ml-3 w-5 h-5 rounded-[6px] border border-[#D8D8D8] flex justify-center items-center">
|
||||
<Command className="w-3 h-3" />
|
||||
</span>
|
||||
<span className="ml-1 w-5 h-5 rounded-[6px] border border-[#D8D8D8] flex justify-center items-center">
|
||||
T
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Footer isChat={false} name={selectedItem?.source} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Search;
|
||||
@@ -205,7 +205,7 @@ const ChatAI = forwardRef<ChatAIRef, ChatAIProps>(
|
||||
|
||||
async function openChatAI() {
|
||||
if (isTauri()) {
|
||||
createWin({
|
||||
createWin && createWin({
|
||||
label: "chat",
|
||||
title: "Coco AI",
|
||||
dragDropEnabled: true,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useEffect } from "react";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { isTauri, invoke } from "@tauri-apps/api/core";
|
||||
import { listen } from "@tauri-apps/api/event";
|
||||
|
||||
const useEscape = () => {
|
||||
@@ -13,6 +13,7 @@ const useEscape = () => {
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if(!isTauri()) return;
|
||||
const unlisten = listen("tauri://focus", () => {
|
||||
// Add event listener for keydown
|
||||
window.addEventListener("keydown", handleEscape);
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useEffect, useCallback } from "react";
|
||||
import { getAllWindows, getCurrentWindow } from "@tauri-apps/api/window";
|
||||
import { WebviewWindow } from "@tauri-apps/api/webviewWindow";
|
||||
import { listen } from "@tauri-apps/api/event";
|
||||
import { isTauri } from "@tauri-apps/api/core";
|
||||
|
||||
const defaultWindowConfig = {
|
||||
label: "",
|
||||
@@ -20,6 +21,7 @@ const defaultWindowConfig = {
|
||||
};
|
||||
|
||||
export const useWindows = () => {
|
||||
if (!isTauri()) return {}
|
||||
const appWindow = getCurrentWindow();
|
||||
|
||||
const createWin = useCallback(async (options: any) => {
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
|
||||
body,
|
||||
#root {
|
||||
@apply text-gray-900 rounded-xl antialiased;
|
||||
@apply text-gray-900 rounded-xl overflow-hidden antialiased;
|
||||
-webkit-touch-callout: none;
|
||||
-webkit-user-select: none;
|
||||
-khtml-user-select: none;
|
||||
|
||||
105
src/pages/app/index.tsx
Normal file
105
src/pages/app/index.tsx
Normal file
@@ -0,0 +1,105 @@
|
||||
import { useState, useRef } from "react";
|
||||
import { isTauri } from "@tauri-apps/api/core";
|
||||
|
||||
import InputBox from "@/components/AppAI/InputBox";
|
||||
import Search from "@/components/AppAI/Search";
|
||||
import ChatAI, { ChatAIRef } from "@/components/ChatAI/Chat";
|
||||
|
||||
export default function DesktopApp() {
|
||||
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);
|
||||
}
|
||||
|
||||
async function changeInput(value: string) {
|
||||
setInput(value);
|
||||
}
|
||||
|
||||
const handleSendMessage = async (value: string) => {
|
||||
setInput(value);
|
||||
if (isChatMode) {
|
||||
if (isTauri()) {
|
||||
const { getCurrentWebviewWindow } = await import(
|
||||
"@tauri-apps/api/webviewWindow"
|
||||
);
|
||||
const { LogicalSize } = await import("@tauri-apps/api/dpi");
|
||||
|
||||
await getCurrentWebviewWindow()?.setSize(new LogicalSize(680, 596));
|
||||
}
|
||||
chatAIRef.current?.init();
|
||||
}
|
||||
};
|
||||
const cancelChat = () => {
|
||||
chatAIRef.current?.cancelChat();
|
||||
};
|
||||
const isTyping = false;
|
||||
|
||||
return (
|
||||
<div
|
||||
data-tauri-drag-region
|
||||
className={`shadow-window-custom w-[680px] h-[590px] 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-[7px] 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}
|
||||
/>
|
||||
</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]`}
|
||||
>
|
||||
<ChatAI
|
||||
ref={chatAIRef}
|
||||
key="ChatAI"
|
||||
inputValue={input}
|
||||
isTransitioned={isTransitioned}
|
||||
changeInput={changeInput}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import ChatAI from "../components/ChatAI";
|
||||
import MySearch from "../components/MySearch";
|
||||
import Layout from "./Layout";
|
||||
import WebApp from "../pages/web";
|
||||
import DesktopApp from "../pages/app";
|
||||
|
||||
export const router = createBrowserRouter([
|
||||
{
|
||||
@@ -17,7 +18,8 @@ export const router = createBrowserRouter([
|
||||
element: <Layout />,
|
||||
errorElement: <ErrorPage />,
|
||||
children: [
|
||||
{ path: "/ui", element: <SearchChat /> },
|
||||
{ path: "/ui", element: <DesktopApp /> },
|
||||
{ path: "/ui/old", element: <SearchChat /> },
|
||||
{ path: "/ui/settings", element: <Settings2 /> },
|
||||
{ path: "/ui/chat", element: <ChatAI /> },
|
||||
{ path: "/ui/search", element: <MySearch /> },
|
||||
|
||||
@@ -14,7 +14,11 @@
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"baseUrl": "./",
|
||||
"paths": {
|
||||
"@/*": ["src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["src"],
|
||||
"references": [{ "path": "./tsconfig.node.json" }]
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
import { defineConfig } from "vite";
|
||||
import react from "@vitejs/plugin-react";
|
||||
import path from 'path';
|
||||
|
||||
const host = process.env.TAURI_DEV_HOST;
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig(async () => ({
|
||||
plugins: [react()],
|
||||
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': path.resolve(__dirname, './src'),
|
||||
},
|
||||
},
|
||||
// Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build`
|
||||
//
|
||||
// 1. prevent vite from obscuring rust errors
|
||||
|
||||
Reference in New Issue
Block a user