fix: mouse & keyDown (#122)

* feat: impl Coco server related APIs

* chore: remove unused method

* fix: invoke Rust interfaces in tauri::run()

* chore: add invoke

* feat: add add_coco_server

* fix: trim the tailing forward slash

* feat: interface get_user_profiles

* chore: add

* fix: store the servers in add interface

* chore: ass

* fix: skip non-publich servers with no token

* feat: add

* feat: get datasources and connectors

* fix: invoke interfaces in tauri::run()

* chore: add SidebarRef

* refactor: refactoring coco-app

* refactor: refactoring coco app

* refactor: refactoring project layout

* refactor: refactoring server management

* chore: cleanup code

* chore: display error when connect failed

* refactor: refactoring refresh server's info

* refactor: refactoring how to connect the coco serverg

* chore: rename to cloud

* refactor: refactoring remove coco server

* fix: refresh current selected server

* fix: reset server selection

* chore: update login status

* feat: add error message tips

* fix: fix login and logout

* refactor: refactoring http client

* fix: fix the datasources

* chore: minor fix

* refactor: refactoring code

* fix: fix search api

* chore: optimize part of icons

* chore: fix build

* refactor: search list icon

* refactor: search list icon

* chore: lib

* feat: add plugin-os

* feat: add data-dark

* fix: mouse & keyDown

* fix: mouse & keyDown

* fix: mouse & keyDown

---------

Co-authored-by: Steve Lau <stevelauc@outlook.com>
Co-authored-by: medcl <m@medcl.net>
This commit is contained in:
BiggerRain
2025-02-07 20:26:45 +08:00
committed by GitHub
parent 1b1d9bfc40
commit 81a02890d6
9 changed files with 582 additions and 585 deletions

View File

@@ -19,27 +19,23 @@ import { invoke } from "@tauri-apps/api/core";
import { UserProfile } from "./UserProfile"; import { UserProfile } from "./UserProfile";
import { DataSourcesList } from "./DataSourcesList"; import { DataSourcesList } from "./DataSourcesList";
import { Sidebar } from "./Sidebar"; import { Sidebar } from "./Sidebar";
import { Connect } from "./Connect.tsx"; import { Connect } from "./Connect";
import { OpenURLWithBrowser } from "@/utils"; import { OpenURLWithBrowser } from "@/utils";
import { useAppStore } from "@/stores/appStore"; import { useAppStore } from "@/stores/appStore";
import { useConnectStore } from "@/stores/connectStore"; import { useConnectStore } from "@/stores/connectStore";
import bannerImg from "@/assets/images/coco-cloud-banner.jpeg"; import bannerImg from "@/assets/images/coco-cloud-banner.jpeg";
export default function Cloud() { export default function Cloud() {
const SidebarRef = useRef<{ refreshData: () => void; }>(null); const SidebarRef = useRef<{ refreshData: () => void }>(null);
// const [error, setError] = useState<string | null>(null);
const error = useAppStore((state) => state.error); const error = useAppStore((state) => state.error);
const setError = useAppStore((state) => state.setError); const setError = useAppStore((state) => state.setError);
const [isConnect, setIsConnect] = useState(true); const [isConnect, setIsConnect] = useState(true);
// const [ssoRequestID, setSSORequestID] = useState("");
const ssoRequestID = useAppStore((state) => state.ssoRequestID); const ssoRequestID = useAppStore((state) => state.ssoRequestID);
const setSSORequestID = useAppStore((state) => state.setSSORequestID); const setSSORequestID = useAppStore((state) => state.setSSORequestID);
// const ssoServerID = useAppStore((state) => state.ssoServerID);
// const setSSOServerID = useAppStore((state) => state.setSSOServerID);
const endpoint = useAppStore((state) => state.endpoint); const endpoint = useAppStore((state) => state.endpoint);
const currentService = useConnectStore((state) => state.currentService); const currentService = useConnectStore((state) => state.currentService);
@@ -50,10 +46,6 @@ export default function Cloud() {
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [refreshLoading, setRefreshLoading] = useState(false); const [refreshLoading, setRefreshLoading] = useState(false);
// const [profiles, setProfiles] = useState<any>({});
// const [userInfo, setUserInfo] = useState<any>({});
// fetch the servers // fetch the servers
useEffect(() => { useEffect(() => {
@@ -65,28 +57,9 @@ export default function Cloud() {
setLoading(false); setLoading(false);
setRefreshLoading(false); setRefreshLoading(false);
setError(""); setError("");
// setEndpoint(currentService.endpoint);
setIsConnect(true); setIsConnect(true);
// setUserInfo(profiles[endpoint] || {})
}, [JSON.stringify(currentService)]); }, [JSON.stringify(currentService)]);
// const get_user_profiles = useCallback(() => {
// invoke("get_user_profiles")
// .then((res: any) => {
// console.log("get_user_profiles", res);
// setProfiles(res);
// console.log("setUserInfo", res[endpoint]);
// setUserInfo(res[endpoint] || {})
// })
// .catch((err: any) => {
// console.error(err);
// });
// }, [endpoint]);
useEffect(() => {
// get_user_profiles()
}, [])
const fetchServers = async (resetSelection: boolean) => { const fetchServers = async (resetSelection: boolean) => {
invoke("list_coco_servers") invoke("list_coco_servers")
.then((res: any) => { .then((res: any) => {
@@ -107,10 +80,13 @@ export default function Cloud() {
const add_coco_server = (endpointLink: string) => { const add_coco_server = (endpointLink: string) => {
if (!endpointLink) { if (!endpointLink) {
throw new Error('Endpoint is required'); throw new Error("Endpoint is required");
} }
if (!endpointLink.startsWith("http://") && !endpointLink.startsWith("https://")) { if (
throw new Error('Invalid Endpoint'); !endpointLink.startsWith("http://") &&
!endpointLink.startsWith("https://")
) {
throw new Error("Invalid Endpoint");
} }
setRefreshLoading(true); setRefreshLoading(true);
@@ -142,7 +118,6 @@ export default function Cloud() {
const handleOAuthCallback = useCallback( const handleOAuthCallback = useCallback(
async (code: string | null, serverId: string | null) => { async (code: string | null, serverId: string | null) => {
if (!code) { if (!code) {
setError("No authorization code received"); setError("No authorization code received");
return; return;
@@ -153,7 +128,7 @@ export default function Cloud() {
await invoke("handle_sso_callback", { await invoke("handle_sso_callback", {
serverId: serverId, // Make sure 'server_id' is the correct argument serverId: serverId, // Make sure 'server_id' is the correct argument
requestId: ssoRequestID, // Make sure 'request_id' is the correct argument requestId: ssoRequestID, // Make sure 'request_id' is the correct argument
code: code code: code,
}); });
if (serverId != null) { if (serverId != null) {
@@ -165,11 +140,9 @@ export default function Cloud() {
.catch((err) => { .catch((err) => {
setError(err); setError(err);
}); });
} catch (e) { } catch (e) {
console.error("Sign in failed:", e); console.error("Sign in failed:", e);
setError("SSO login failed: " + e); setError("SSO login failed: " + e);
// setAuth(undefined, endpoint);
throw error; throw error;
} finally { } finally {
setLoading(false); setLoading(false);
@@ -180,7 +153,6 @@ export default function Cloud() {
const handleUrl = (url: string) => { const handleUrl = (url: string) => {
try { try {
// url = "coco://oauth_callback?code=cuhhi8o2sdbbbcoe0g10ktmht6aky3jmd4xkwsgvzf748i4zdgr898bfeu3kze7ffdusdtbgtnpke8ng3fe6&provider=coco-cloud/"
const urlObject = new URL(url); const urlObject = new URL(url);
console.log("handle urlObject:", urlObject); console.log("handle urlObject:", urlObject);
@@ -196,27 +168,19 @@ export default function Cloud() {
const serverId = currentService?.id; const serverId = currentService?.id;
handleOAuthCallback(code, serverId); handleOAuthCallback(code, serverId);
// switch (urlObject.hostname) {
// case "/oauth_callback":
// break;
// default:
// console.log("Unhandled deep link path:", urlObject.pathname);
// }
} catch (err) { } catch (err) {
console.error("Failed to parse URL:", err); console.error("Failed to parse URL:", err);
setError("Invalid URL format: " + err); setError("Invalid URL format: " + err);
} }
}; };
// Fetch the initial deep link intent // Fetch the initial deep link intent
useEffect(() => { useEffect(() => {
// Test the handleUrl function
// handleUrl("coco://oauth_callback?code=cui88lg2sdb4dnu97jpgypcugrskkt1i3venntth7gk52exnq8hxufxvqn8hhegoaw369s394bcyb6ehtnhz&request_id=642a985c-6baa-4ec8-be41-d8c6ddbc0e60&provider=coco-cloud/");
// Function to handle pasted URL // Function to handle pasted URL
const handlePaste = (event: any) => { const handlePaste = (event: any) => {
const pastedText = event.clipboardData.getData('text'); const pastedText = event.clipboardData.getData("text");
console.log("handle paste text:", pastedText); console.log("handle paste text:", pastedText);
if (isValidCallbackUrl(pastedText)) { if (isValidCallbackUrl(pastedText)) {
// Handle the URL as if it's a deep link // Handle the URL as if it's a deep link
@@ -227,11 +191,11 @@ export default function Cloud() {
// Function to check if the pasted URL is valid for our deep link scheme // Function to check if the pasted URL is valid for our deep link scheme
const isValidCallbackUrl = (url: string) => { const isValidCallbackUrl = (url: string) => {
return url && url.startsWith('coco://oauth_callback'); return url && url.startsWith("coco://oauth_callback");
}; };
// Adding event listener for paste events // Adding event listener for paste events
document.addEventListener('paste', handlePaste); document.addEventListener("paste", handlePaste);
getCurrentDeepLinkUrls() getCurrentDeepLinkUrls()
.then((urls) => { .then((urls) => {
@@ -251,27 +215,15 @@ export default function Cloud() {
return () => { return () => {
unlisten.then((fn) => fn()); unlisten.then((fn) => fn());
document.removeEventListener('paste', handlePaste); document.removeEventListener("paste", handlePaste);
}; };
}, [ssoRequestID]); }, [ssoRequestID]);
// const generateLogin = () => {
// const requestID = uuidv4();
// setSSORequestID(requestID);
// setSSOServerID(currentService?.id); // Set server ID
//
// // The URL is now updated when ssoRequestID and ssoServerID are both set
// };
const LoginClick = useCallback(() => { const LoginClick = useCallback(() => {
if (loading) return; // Prevent multiple clicks if already loading if (loading) return; // Prevent multiple clicks if already loading
// If the appUid doesn't exist, generate one
// if (!ssoRequestID) {
let requestID = uuidv4(); let requestID = uuidv4();
setSSORequestID(requestID); setSSORequestID(requestID);
// setSSOServerID(currentService?.id);
// }
// Generate the login URL with the current appUid // Generate the login URL with the current appUid
const url = `${currentService?.auth_provider?.sso?.url}/?provider=${currentService?.id}&product=coco&request_id=${requestID}`; const url = `${currentService?.auth_provider?.sso?.url}/?provider=${currentService?.id}&product=coco&request_id=${requestID}`;
@@ -283,7 +235,6 @@ export default function Cloud() {
// Start loading state // Start loading state
setLoading(true); setLoading(true);
}, [ssoRequestID, loading, currentService]); }, [ssoRequestID, loading, currentService]);
const refreshClick = (id: string) => { const refreshClick = (id: string) => {
@@ -291,7 +242,7 @@ export default function Cloud() {
invoke("refresh_coco_server_info", { id }) invoke("refresh_coco_server_info", { id })
.then((res: any) => { .then((res: any) => {
console.log("refresh_coco_server_info", id, JSON.stringify(res)); console.log("refresh_coco_server_info", id, JSON.stringify(res));
fetchServers(false).then(r => { fetchServers(false).then((r) => {
console.log("fetchServers", r); console.log("fetchServers", r);
}); });
// update currentService // update currentService
@@ -320,7 +271,8 @@ export default function Cloud() {
.catch((err: any) => { .catch((err: any) => {
setError(err); setError(err);
console.error(err); console.error(err);
}).finally(() => { })
.finally(() => {
setRefreshLoading(false); setRefreshLoading(false);
}); });
} }
@@ -329,9 +281,9 @@ export default function Cloud() {
invoke("remove_coco_server", { id }) invoke("remove_coco_server", { id })
.then((res: any) => { .then((res: any) => {
console.log("remove_coco_server", id, JSON.stringify(res)); console.log("remove_coco_server", id, JSON.stringify(res));
fetchServers(true).then(r => { fetchServers(true).then((r) => {
console.log("fetchServers", r); console.log("fetchServers", r);
}) });
}) })
.catch((err: any) => { .catch((err: any) => {
// TODO display the error message // TODO display the error message
@@ -342,10 +294,13 @@ export default function Cloud() {
return ( return (
<div className="flex bg-gray-50 dark:bg-gray-900"> <div className="flex bg-gray-50 dark:bg-gray-900">
<Sidebar ref={SidebarRef} onAddServer={onAddServer} serverList={serverList} /> <Sidebar
ref={SidebarRef}
onAddServer={onAddServer}
serverList={serverList}
/>
<main className="flex-1 p-4 py-8"> <main className="flex-1 p-4 py-8">
{isConnect ? ( {isConnect ? (
<div className="max-w-4xl mx-auto"> <div className="max-w-4xl mx-auto">
<div className="w-full rounded-[4px] bg-[rgba(229,229,229,1)] dark:bg-gray-800 mb-6"> <div className="w-full rounded-[4px] bg-[rgba(229,229,229,1)] dark:bg-gray-800 mb-6">
@@ -364,7 +319,9 @@ export default function Cloud() {
<div className="flex gap-2"> <div className="flex gap-2">
<button <button
className="p-2 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300 rounded-[6px] bg-white dark:bg-gray-800 border border-[rgba(228,229,239,1)] dark:border-gray-700" className="p-2 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300 rounded-[6px] bg-white dark:bg-gray-800 border border-[rgba(228,229,239,1)] dark:border-gray-700"
onClick={() => OpenURLWithBrowser(currentService?.provider?.website)} onClick={() =>
OpenURLWithBrowser(currentService?.provider?.website)
}
> >
<Globe className="w-3.5 h-3.5" /> <Globe className="w-3.5 h-3.5" />
</button> </button>
@@ -373,7 +330,8 @@ export default function Cloud() {
onClick={() => refreshClick(currentService?.id)} onClick={() => refreshClick(currentService?.id)}
> >
<RefreshCcw <RefreshCcw
className={`w-3.5 h-3.5 ${refreshLoading ? "animate-spin" : "" className={`w-3.5 h-3.5 ${
refreshLoading ? "animate-spin" : ""
}`} }`}
/> />
</button> </button>
@@ -416,7 +374,11 @@ export default function Cloud() {
Account Information Account Information
</h2> </h2>
{currentService?.profile ? ( {currentService?.profile ? (
<UserProfile server={currentService?.id} userInfo={currentService?.profile} onLogout={onLogout} /> <UserProfile
server={currentService?.id}
userInfo={currentService?.profile}
onLogout={onLogout}
/>
) : ( ) : (
<div> <div>
{/* Login Button (conditionally rendered when not loading) */} {/* Login Button (conditionally rendered when not loading) */}
@@ -446,7 +408,8 @@ export default function Cloud() {
}} }}
className="text-xl text-blue-500 hover:text-blue-600" className="text-xl text-blue-500 hover:text-blue-600"
> >
<Copy className="inline mr-2" /> {/* Lucide Copy Icon */} <Copy className="inline mr-2" />{" "}
{/* Lucide Copy Icon */}
</button> </button>
</div> </div>
)} )}
@@ -455,7 +418,9 @@ export default function Cloud() {
<button <button
className="text-xs text-[#0096FB] dark:text-blue-400 block" className="text-xs text-[#0096FB] dark:text-blue-400 block"
onClick={() => onClick={() =>
OpenURLWithBrowser(currentService?.provider?.privacy_policy) OpenURLWithBrowser(
currentService?.provider?.privacy_policy
)
} }
> >
EULA | Privacy Policy EULA | Privacy Policy
@@ -465,7 +430,9 @@ export default function Cloud() {
</div> </div>
) : null} ) : null}
{currentService?.profile ? <DataSourcesList server={currentService?.id} /> : null} {currentService?.profile ? (
<DataSourcesList server={currentService?.id} />
) : null}
</div> </div>
) : ( ) : (
<Connect setIsConnect={setIsConnect} onAddServer={add_coco_server} /> <Connect setIsConnect={setIsConnect} onAddServer={add_coco_server} />

View File

@@ -107,7 +107,7 @@ export default function ChatInput({
useEffect(() => { useEffect(() => {
const setupListener = async () => { const setupListener = async () => {
const unlisten = await listen("tauri://focus", () => { const unlisten = await listen("tauri://focus", () => {
console.log("Window focused!"); // console.log("Window focused!");
if (isChatMode) { if (isChatMode) {
textareaRef.current?.focus(); textareaRef.current?.focus();
} else { } else {

View File

@@ -1,4 +1,4 @@
import React, { useState, useRef, useEffect } from "react"; import React, { useState, useRef, useEffect, useCallback } from "react";
import { useInfiniteScroll } from "ahooks"; import { useInfiniteScroll } from "ahooks";
import { isTauri, invoke } from "@tauri-apps/api/core"; import { isTauri, invoke } from "@tauri-apps/api/core";
import { open } from "@tauri-apps/plugin-shell"; import { open } from "@tauri-apps/plugin-shell";
@@ -29,10 +29,11 @@ export const DocumentList: React.FC<DocumentListProps> = ({
const [total, setTotal] = useState(0); const [total, setTotal] = useState(0);
const containerRef = useRef<HTMLDivElement>(null); const containerRef = useRef<HTMLDivElement>(null);
const itemRefs = useRef<(HTMLDivElement | null)[]>([]); const itemRefs = useRef<(HTMLDivElement | null)[]>([]);
const [isKeyboardMode, setIsKeyboardMode] = useState(false);
const { data, loading } = useInfiniteScroll( const { data, loading } = useInfiniteScroll(
async (d) => { async (d) => {
const from = d?.list.length || 0; const from = d?.list?.length || 0;
let queryStrings: any = { let queryStrings: any = {
query: input, query: input,
@@ -55,40 +56,26 @@ export const DocumentList: React.FC<DocumentListProps> = ({
const list = response?.hits || []; const list = response?.hits || [];
const total = response?.total_hits || 0; const total = response?.total_hits || 0;
// console.log("docs:", list, total);
setTotal(total); setTotal(total);
getDocDetail(list[0] || {});
return { return {
list, list: list,
hasMore: from + list.length < total, hasMore: list.length === PAGE_SIZE,
}; };
} catch (error) { } catch (error) {
console.error("Failed to fetch documents:", error); console.error("Failed to fetch documents:", error);
return { return {
list: [], list: d?.list || [],
hasMore: false, hasMore: false,
}; };
} }
}, },
{ {
target: containerRef, target: containerRef,
isNoMore: (d) => (d?.list.length || 0) >= total, isNoMore: (d) => !d?.hasMore,
reloadDeps: [input, JSON.stringify(sourceData)], reloadDeps: [input, JSON.stringify(sourceData)],
onBefore: () => {
setTimeout(() => {
const parentRef = containerRef.current;
if (parentRef && parentRef.childElementCount > 10) {
const itemHeight =
(parentRef.firstChild as HTMLElement)?.offsetHeight || 80;
parentRef.scrollTo({
top: (parentRef.lastChild as HTMLElement)?.offsetTop - itemHeight,
behavior: "instant",
});
}
});
},
onFinally: (data) => onFinally(data, containerRef), onFinally: (data) => onFinally(data, containerRef),
} }
); );
@@ -96,22 +83,31 @@ export const DocumentList: React.FC<DocumentListProps> = ({
const onFinally = (data: any, ref: any) => { const onFinally = (data: any, ref: any) => {
if (data?.page === 1) return; if (data?.page === 1) return;
const parentRef = ref.current; const parentRef = ref.current;
if (!parentRef) return; if (!parentRef || selectedItem === null) return;
const itemHeight = parentRef.firstChild?.offsetHeight || 80;
parentRef.scrollTo({ const targetElement = itemRefs.current[selectedItem];
top: if (!targetElement) return;
parentRef.lastChild?.offsetTop - (data?.list?.length + 1) * itemHeight,
requestAnimationFrame(() => {
targetElement.scrollIntoView({
behavior: "instant", behavior: "instant",
block: "nearest",
});
}); });
}; };
function onMouseEnter(index: number, item: any) { const onMouseEnter = useCallback(
(index: number, item: any) => {
if (isKeyboardMode) return;
getDocDetail(item); getDocDetail(item);
setSelectedItem(index); setSelectedItem(index);
} },
[isKeyboardMode, getDocDetail]
);
useEffect(() => { useEffect(() => {
setSelectedItem(null); setSelectedItem(null);
setIsKeyboardMode(false);
}, [isChatMode, input]); }, [isChatMode, input]);
const handleOpenURL = async (url: string) => { const handleOpenURL = async (url: string) => {
@@ -126,17 +122,32 @@ export const DocumentList: React.FC<DocumentListProps> = ({
} }
}; };
const handleKeyDown = (e: KeyboardEvent) => { const handleKeyDown = useCallback(
(e: KeyboardEvent) => {
if (!data?.list?.length) return; if (!data?.list?.length) return;
if (e.key === "ArrowUp" || e.key === "ArrowDown") {
e.preventDefault();
setIsKeyboardMode(true);
if (e.key === "ArrowUp") { if (e.key === "ArrowUp") {
e.preventDefault(); setSelectedItem((prev) => {
setSelectedItem((prev) => (prev === null || prev === 0 ? 0 : prev - 1)); const newIndex = prev === null || prev === 0 ? 0 : prev - 1;
} else if (e.key === "ArrowDown") { getDocDetail(data.list[newIndex]?.document);
e.preventDefault(); return newIndex;
setSelectedItem((prev) => });
prev === null ? 0 : prev === data?.list?.length - 1 ? prev : prev + 1 } else {
); setSelectedItem((prev) => {
const newIndex =
prev === null
? 0
: prev === data.list.length - 1
? prev
: prev + 1;
getDocDetail(data.list[newIndex]?.document);
return newIndex;
});
}
} else if (e.key === "Meta") { } else if (e.key === "Meta") {
e.preventDefault(); e.preventDefault();
} }
@@ -147,22 +158,39 @@ export const DocumentList: React.FC<DocumentListProps> = ({
handleOpenURL(item?.url); handleOpenURL(item?.url);
} }
} }
},
[data, selectedItem, getDocDetail]
);
useEffect(() => {
const handleMouseMove = (e: MouseEvent) => {
if (e.movementX !== 0 || e.movementY !== 0) {
setIsKeyboardMode(false);
}
}; };
window.addEventListener("mousemove", handleMouseMove);
return () => {
window.removeEventListener("mousemove", handleMouseMove);
};
}, []);
useEffect(() => { useEffect(() => {
window.addEventListener("keydown", handleKeyDown); window.addEventListener("keydown", handleKeyDown);
return () => { return () => {
window.removeEventListener("keydown", handleKeyDown); window.removeEventListener("keydown", handleKeyDown);
}; };
}, [selectedItem]); }, [handleKeyDown]);
useEffect(() => { useEffect(() => {
if (selectedItem !== null && itemRefs.current[selectedItem]) { if (selectedItem !== null && itemRefs.current[selectedItem]) {
requestAnimationFrame(() => {
itemRefs.current[selectedItem]?.scrollIntoView({ itemRefs.current[selectedItem]?.scrollIntoView({
behavior: "smooth", behavior: "instant",
block: "nearest", block: "nearest",
}); });
});
} }
}, [selectedItem]); }, [selectedItem]);
@@ -196,13 +224,8 @@ export const DocumentList: React.FC<DocumentListProps> = ({
}`} }`}
> >
<div className="flex gap-2 items-center flex-1 min-w-0"> <div className="flex gap-2 items-center flex-1 min-w-0">
<ItemIcon item={item} /> <ItemIcon item={item} />
<span <span className={`text-sm truncate`}>{item?.title}</span>
className={`text-sm truncate`}
>
{item?.title}
</span>
</div> </div>
</div> </div>
); );

View File

@@ -91,16 +91,16 @@ export default function Footer({ }: FooterProps) {
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="gap-1 flex items-center text-[#666] dark:text-[#666] text-sm"> <div className="gap-1 flex items-center text-[#666] dark:text-[#666] text-sm">
<span className="mr-1.5 ">Quick open</span> <span className="mr-1.5 ">Quick open</span>
<kbd className="docsearch-modal-footer-commands-key pr-1"> <kbd className="coco-modal-footer-commands-key pr-1">
<Command className="w-3 h-3" /> <Command className="w-3 h-3" />
</kbd> </kbd>
<kbd className="docsearch-modal-footer-commands-key pr-1"> <kbd className="coco-modal-footer-commands-key pr-1">
<ArrowDown01 className="w-3 h-3" /> <ArrowDown01 className="w-3 h-3" />
</kbd> </kbd>
</div> </div>
<div className="flex items-center text-[#666] dark:text-[#666] text-sm"> <div className="flex items-center text-[#666] dark:text-[#666] text-sm">
<span className="mr-1.5 ">Open</span> <span className="mr-1.5 ">Open</span>
<kbd className="docsearch-modal-footer-commands-key pr-1"> <kbd className="coco-modal-footer-commands-key pr-1">
<CornerDownLeft className="w-3 h-3" /> <CornerDownLeft className="w-3 h-3" />
</kbd> </kbd>
</div> </div>

View File

@@ -152,7 +152,7 @@ export default function ChatInput({
if (!isTauri()) return; if (!isTauri()) return;
const setupListener = async () => { const setupListener = async () => {
const unlisten = await listen("tauri://focus", () => { const unlisten = await listen("tauri://focus", () => {
console.log("Window focused!"); // console.log("Window focused!");
if (isChatMode) { if (isChatMode) {
textareaRef.current?.focus(); textareaRef.current?.focus();
} else { } else {

View File

@@ -21,11 +21,8 @@ import { ShortcutItem } from "./ShortcutItem";
import { Shortcut } from "./shortcut"; import { Shortcut } from "./shortcut";
import { useShortcutEditor } from "@/hooks/useShortcutEditor"; import { useShortcutEditor } from "@/hooks/useShortcutEditor";
import { useAppStore } from "@/stores/appStore"; import { useAppStore } from "@/stores/appStore";
import {AppTheme} from "@/utils/tauri.ts"; import { AppTheme } from "@/utils/tauri";
import {useTheme} from "@/contexts/ThemeContext.tsx"; import { useTheme } from "@/contexts/ThemeContext";
// import { useAuthStore } from "@/stores/authStore";
// import { useConnectStore } from "@/stores/connectStore";
export function ThemeOption({ export function ThemeOption({
icon: Icon, icon: Icon,

View File

@@ -7,7 +7,7 @@ import React, {
} from "react"; } from "react";
import { isTauri, invoke } from "@tauri-apps/api/core"; import { isTauri, invoke } from "@tauri-apps/api/core";
import { getCurrentWindow } from "@tauri-apps/api/window"; import { getCurrentWindow } from "@tauri-apps/api/window";
import { listen } from "@tauri-apps/api/event"; import { listen, emit } from "@tauri-apps/api/event";
import { AppTheme, WindowTheme } from "../utils/tauri"; import { AppTheme, WindowTheme } from "../utils/tauri";
import { useThemeStore } from "../stores/themeStore"; import { useThemeStore } from "../stores/themeStore";
@@ -45,6 +45,8 @@ export function ThemeProvider({ children }: { children: React.ReactNode }) {
unlisten = await currentWindow.onThemeChanged(({ payload: w_theme }) => { unlisten = await currentWindow.onThemeChanged(({ payload: w_theme }) => {
console.log("window New theme:", w_theme); console.log("window New theme:", w_theme);
setWindowTheme(w_theme); setWindowTheme(w_theme);
// Update tray icon
switchTrayIcon(w_theme);
if (theme === "auto") applyTheme(w_theme); if (theme === "auto") applyTheme(w_theme);
}); });
}; };
@@ -75,6 +77,8 @@ export function ThemeProvider({ children }: { children: React.ReactNode }) {
const root = window.document.documentElement; const root = window.document.documentElement;
root.classList.remove("light", "dark"); root.classList.remove("light", "dark");
root.classList.add(displayTheme); root.classList.add(displayTheme);
//
root.setAttribute("data-theme", displayTheme);
} }
// Apply theme to UI and sync with Tauri // Apply theme to UI and sync with Tauri
@@ -91,16 +95,13 @@ export function ThemeProvider({ children }: { children: React.ReactNode }) {
console.error("Failed to update window theme:", err); console.error("Failed to update window theme:", err);
} }
// Update tray icon
await switchTrayIcon(displayTheme);
// Notify other windows to update the theme // Notify other windows to update the theme
// try { try {
// console.log("theme-changed", displayTheme); // console.log("theme-changed", displayTheme);
// await emit("theme-changed", { theme: displayTheme }); await emit("theme-changed", { theme: displayTheme });
// } catch (err) { } catch (err) {
// console.error("Failed to emit theme-changed event:", err); console.error("Failed to emit theme-changed event:", err);
// } }
} }
}; };
@@ -126,19 +127,18 @@ export function ThemeProvider({ children }: { children: React.ReactNode }) {
// Handle theme changes from user interaction // Handle theme changes from user interaction
const changeTheme = async (newTheme: AppTheme) => { const changeTheme = async (newTheme: AppTheme) => {
console.log("Theme changed to:", newTheme);
setTheme(newTheme); setTheme(newTheme);
const displayTheme = getDisplayTheme(newTheme); const displayTheme = getDisplayTheme(newTheme);
await applyTheme(displayTheme); await applyTheme(displayTheme);
}; };
useEffect(() => { useEffect(() => {
if (!isTauri()) return;
let unlisten: () => void; let unlisten: () => void;
const setupListener = async () => { const setupListener = async () => {
unlisten = await listen("theme-changed", (event: any) => { unlisten = await listen("theme-changed", (event: any) => {
console.log("Theme updated to:", event.payload); // console.log("Theme updated to:", event.payload);
changeClassTheme(event.payload.theme) changeClassTheme(event.payload.theme)
}); });
}; };

View File

@@ -2,61 +2,64 @@
@tailwind components; @tailwind components;
@tailwind utilities; @tailwind utilities;
@layer { /* Base variables */
:root { :root {
--spacing-base: 12px;
--modal-width: 560px;
--modal-height: 600px;
--searchbox-height: 56px;
--hit-height: 56px;
--footer-height: 44px;
--icon-stroke-width: 1.4;
--background: #ffffff; --background: #ffffff;
--foreground: #09090b; --foreground: #09090b;
--border: #e3e3e7; --border: #e3e3e7;
--docsearch-primary-color: rgb(149, 5, 153);
--docsearch-text-color: rgb(28, 30, 33);
--docsearch-spacing: 12px;
--docsearch-icon-stroke-width: 1.4;
--docsearch-highlight-color: var(--docsearch-primary-color);
--docsearch-muted-color: rgb(150, 159, 175);
--docsearch-modal-container-background: rgba(101, 108, 133, .8);
--docsearch-modal-width: 560px;
--docsearch-modal-height: 600px;
--docsearch-modal-background: rgb(245, 246, 247);
--docsearch-modal-shadow: inset 1px 1px 0 0 rgba(255, 255, 255, .5), 0 3px 8px 0 rgba(85, 90, 100, 1);
--docsearch-searchbox-height: 56px;
--docsearch-searchbox-background: rgb(235, 237, 240);
--docsearch-searchbox-focus-background: #fff;
--docsearch-searchbox-shadow: inset 0 0 0 2px var(--docsearch-primary-color);
--docsearch-hit-height: 56px;
--docsearch-hit-color: rgb(68, 73, 80);
--docsearch-hit-active-color: #fff;
--docsearch-hit-background: #fff;
--docsearch-hit-shadow: 0 1px 3px 0 rgb(212, 217, 225);
--docsearch-key-gradient: linear-gradient(-225deg, rgb(213, 219, 228) 0%, rgb(248, 248, 248) 100%);
--docsearch-key-shadow: inset 0 -2px 0 0 rgb(205, 205, 230), inset 0 0 1px 1px #fff, 0 1px 2px 1px rgba(30, 35, 90, .4);
--docsearch-footer-height: 44px;
--docsearch-footer-background: #fff;
--docsearch-footer-shadow: 0 -1px 0 0 rgb(224, 227, 232), 0 -3px 6px 0 rgba(69, 98, 155, .12);
--docsearch-icon-color: rgb(21, 21, 21);
} }
.dark { /* Light theme */
[data-theme="light"] {
--coco-primary-color: rgb(149, 5, 153);
--coco-text-color: rgb(28, 30, 33);
--coco-muted-color: rgb(150, 159, 175);
--coco-modal-container-background: rgba(101, 108, 133, .8);
--coco-modal-background: rgb(245, 246, 247);
--coco-modal-shadow: inset 1px 1px 0 0 rgba(255, 255, 255, .5), 0 3px 8px 0 rgba(85, 90, 100, 1);
--coco-searchbox-background: rgb(235, 237, 240);
--coco-searchbox-focus-background: #fff;
--coco-hit-color: rgb(68, 73, 80);
--coco-hit-active-color: #fff;
--coco-hit-background: #fff;
--coco-hit-shadow: 0 1px 3px 0 rgb(212, 217, 225);
--coco-key-gradient: linear-gradient(-225deg, rgb(213, 219, 228) 0%, rgb(248, 248, 248) 100%);
--coco-key-shadow: inset 0 -2px 0 0 rgb(205, 205, 230), inset 0 0 1px 1px #fff, 0 1px 2px 1px rgba(30, 35, 90, .4);
--coco-footer-background: #fff;
--coco-footer-shadow: 0 -1px 0 0 rgb(224, 227, 232), 0 -3px 6px 0 rgba(69, 98, 155, .12);
--coco-icon-color: rgb(21, 21, 21);
}
/* Dark theme */
[data-theme="dark"] {
--background: #09090b; --background: #09090b;
--foreground: #f9f9f9; --foreground: #f9f9f9;
--border: #27272a; --border: #27272a;
--docsearch-text-color: rgb(245, 246, 247); --coco-text-color: rgb(245, 246, 247);
--docsearch-modal-container-background: rgba(9, 10, 17, .8); --coco-modal-container-background: rgba(9, 10, 17, .8);
--docsearch-modal-background: rgb(21, 23, 42); --coco-modal-background: rgb(21, 23, 42);
--docsearch-modal-shadow: inset 1px 1px 0 0 rgb(44, 46, 64), 0 3px 8px 0 rgb(0, 3, 9); --coco-modal-shadow: inset 1px 1px 0 0 rgb(44, 46, 64), 0 3px 8px 0 rgb(0, 3, 9);
--docsearch-searchbox-background: rgb(9, 10, 17); --coco-searchbox-background: rgb(9, 10, 17);
--docsearch-searchbox-focus-background: #000; --coco-searchbox-focus-background: #000;
--docsearch-hit-color: rgb(190, 195, 201); --coco-hit-color: rgb(190, 195, 201);
--docsearch-hit-shadow: none; --coco-hit-shadow: none;
--docsearch-hit-background: rgb(9, 10, 17); --coco-hit-background: rgb(9, 10, 17);
--docsearch-key-gradient: linear-gradient(-26.5deg, rgb(86, 88, 114) 0%, rgb(49, 53, 91) 100%); --coco-key-gradient: linear-gradient(-26.5deg, rgb(86, 88, 114) 0%, rgb(49, 53, 91) 100%);
--docsearch-key-shadow: inset 0 -2px 0 0 rgb(40, 45, 85), inset 0 0 1px 1px rgb(81, 87, 125), 0 2px 2px 0 rgba(3, 4, 9, .3); --coco-key-shadow: inset 0 -2px 0 0 rgb(40, 45, 85), inset 0 0 1px 1px rgb(81, 87, 125), 0 2px 2px 0 rgba(3, 4, 9, .3);
--docsearch-footer-background: rgb(30, 33, 54); --coco-footer-background: rgb(30, 33, 54);
--docsearch-footer-shadow: inset 0 1px 0 0 rgba(73, 76, 106, .5), 0 -4px 8px 0 rgba(0, 0, 0, .2); --coco-footer-shadow: inset 0 1px 0 0 rgba(73, 76, 106, .5), 0 -4px 8px 0 rgba(0, 0, 0, .2);
--docsearch-muted-color: rgb(127, 132, 151); --coco-muted-color: rgb(127, 132, 151);
--docsearch-icon-color: rgb(255, 255, 255); --coco-icon-color: rgb(255, 255, 255);
}
} }
/* Base styles */
@layer base { @layer base {
* { * {
@apply box-border border-[--border]; @apply box-border border-[--border];
@@ -81,6 +84,7 @@
} }
} }
/* Component styles */
@layer components { @layer components {
.settings-input { .settings-input {
@apply block w-full rounded-md border-gray-300 dark:border-gray-600 @apply block w-full rounded-md border-gray-300 dark:border-gray-600
@@ -99,7 +103,9 @@
} }
} }
/* Utility styles */
@layer utilities { @layer utilities {
/* Scrollbar styles */
.custom-scrollbar { .custom-scrollbar {
scrollbar-width: thin; scrollbar-width: thin;
scrollbar-color: #cbd5e1 transparent; scrollbar-color: #cbd5e1 transparent;
@@ -126,10 +132,12 @@
background-color: #475569; background-color: #475569;
} }
/* Background styles */
.bg-100 { .bg-100 {
background-size: 100% 100%; background-size: 100% 100%;
} }
/* Error page styles */
#error-page { #error-page {
display: flex; display: flex;
justify-content: center; justify-content: center;
@@ -187,18 +195,20 @@
background-color: #f79c42; background-color: #f79c42;
} }
.docsearch-modal-footer-commands-key { /* coco styles */
.coco-modal-footer-commands-key {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
border-radius: 4px; border-radius: 4px;
border: 0px; border: 0;
padding: 2px; padding: 2px;
background: var(--docsearch-key-gradient); background: var(--coco-key-gradient);
box-shadow: var(--docsearch-key-shadow); box-shadow: var(--coco-key-shadow);
color: var(--docsearch-muted-color); color: var(--coco-muted-color);
} }
/* User selection styles */
.user-select { .user-select {
-webkit-touch-callout: none; -webkit-touch-callout: none;
-webkit-user-select: none; -webkit-user-select: none;

View File

@@ -42,5 +42,5 @@ export default {
}, },
plugins: [], plugins: [],
mode: "jit", mode: "jit",
darkMode: "class", darkMode: ["class", '[data-theme="dark"]'],
}; };