refactor: replace legacy components with shadcn/ui components (#1002)

* chore: shadcn config

* feat: add shadcn ui config

* style: adjust styles

* style: adjust styles

* refactor: update style

* style: adjust styles

* style: adjust styles

* style: adjust styles

* style: adjust styles

* refactor: update

* refactor: update

* refactor: update

* refactor: update

* style: adjust styles

* style: adjust styles

* refactor: update

* refactor: update

* refactor: update

* refactor: update

* refactor: update

* refactor: update

* style: web styles

* refactor: update

* style: web styles

* style: web styles

* refactor: update

* refactor: update

* refactor: update

* chhore: add

* chore: add

* refactor: update

* refactor: update

* refactor: update

* refactor: update

* chore: update

* refactor: update

* refactor: update

* refactor: update

* refactor: update

* refactor: update

* refactor: update

* chore: rename

* refactor: update

* refactor: update

* chore: add

* refactor: update

* chore: update

* chroe: up

* refactor: update

* refactor: update

* chore: up

* refactor: update

* chore: up

* feat: support for extracting css variables

* chore: update

* fix: fixed dark mode

* refactor: update

* refactor: update

* refactor: update

* refactor: update

* docs: update release notes

* style: adjust styles

* style: adjust styles

* refactor: update

* refactor: update

* refactor: update

* refactor: update

* refactor: update

* refactor: update

---------

Co-authored-by: ayang <473033518@qq.com>
This commit is contained in:
BiggerRain
2025-12-18 10:26:13 +08:00
committed by GitHub
parent abf20f81ff
commit ed8a1cb477
103 changed files with 3857 additions and 2544 deletions

4
.gitignore vendored
View File

@@ -13,6 +13,8 @@ dist-ssr
*.local *.local
out out
src/components/web src/components/web
SearchChatDemo/
web.md
# Editor directories and files # Editor directories and files
# .vscode/* # .vscode/*
@@ -26,3 +28,5 @@ src/components/web
*.sln *.sln
*.sw? *.sw?
.env .env
.trae

View File

@@ -37,15 +37,18 @@
"meval", "meval",
"Minimizable", "Minimizable",
"msvc", "msvc",
"njsproj",
"nord", "nord",
"nowrap", "nowrap",
"nspanel", "nspanel",
"nsstring", "nsstring",
"ntvs",
"objc", "objc",
"overscan", "overscan",
"partialize", "partialize",
"patchelf", "patchelf",
"Quicklink", "Quicklink",
"Quicklinks",
"Raycast", "Raycast",
"rehype", "rehype",
"reqwest", "reqwest",
@@ -54,6 +57,7 @@
"rustup", "rustup",
"screenshotable", "screenshotable",
"serde", "serde",
"Shadcn",
"swatinem", "swatinem",
"tailwindcss", "tailwindcss",
"tauri", "tauri",
@@ -61,6 +65,7 @@
"timedout", "timedout",
"titlebar", "titlebar",
"tpddns", "tpddns",
"trae",
"traptitech", "traptitech",
"unlisten", "unlisten",
"unlistener", "unlistener",

View File

@@ -20,6 +20,7 @@ Information about release notes of Coco App is provided here.
### ✈️ Improvements ### ✈️ Improvements
- refactor: replace legacy components with shadcn/ui components #1002
- chore: show error msg (not err code) when installing exts via deeplink fails #1007 - chore: show error msg (not err code) when installing exts via deeplink fails #1007
- refactor: treat Applications and File Search as normal extensions #1012 - refactor: treat Applications and File Search as normal extensions #1012

View File

@@ -8,9 +8,10 @@
"build": "tsc && vite build", "build": "tsc && vite build",
"build:web": "cross-env BUILD_TARGET=web tsc && cross-env BUILD_TARGET=web tsup --format esm", "build:web": "cross-env BUILD_TARGET=web tsc && cross-env BUILD_TARGET=web tsup --format esm",
"publish:web": "cd out/search-chat && npm publish", "publish:web": "cd out/search-chat && npm publish",
"publish:web:beta": "cd dist/search-chat && npm publish --tag beta", "publish:web:beta": "cd out/search-chat && npm publish --tag beta",
"publish:web:alpha": "cd dist/search-chat && npm publish --tag alpha", "publish:web:alpha": "cd out/search-chat && npm publish --tag alpha",
"publish:web:rc": "cd dist/search-chat && npm publish --tag rc", "publish:web:rc": "cd out/search-chat && npm publish --tag rc",
"publish:web:otp": "cd out/search-chat && npm publish --access public --otp $NPM_OTP",
"preview": "vite preview", "preview": "vite preview",
"tauri": "tauri", "tauri": "tauri",
"release": "release-it", "release": "release-it",
@@ -18,10 +19,18 @@
"release-beta": "release-it --preRelease=beta --preReleaseBase=1" "release-beta": "release-it --preRelease=beta --preReleaseBase=1"
}, },
"dependencies": { "dependencies": {
"@headlessui/react": "^2.2.2",
"@infinilabs/custom-icons": "0.0.4", "@infinilabs/custom-icons": "0.0.4",
"@radix-ui/react-checkbox": "^1.1.5",
"@radix-ui/react-dialog": "^1.1.4",
"@radix-ui/react-dropdown-menu": "^2.1.16",
"@radix-ui/react-label": "^2.1.1",
"@radix-ui/react-popover": "^1.1.6",
"@radix-ui/react-select": "^2.1.4",
"@radix-ui/react-separator": "^1.1.8", "@radix-ui/react-separator": "^1.1.8",
"@radix-ui/react-slider": "^1.2.1",
"@radix-ui/react-slot": "^1.2.3", "@radix-ui/react-slot": "^1.2.3",
"@radix-ui/react-switch": "^1.1.3",
"@radix-ui/react-tabs": "^1.1.13",
"@tauri-apps/api": "^2.5.0", "@tauri-apps/api": "^2.5.0",
"@tauri-apps/plugin-autostart": "~2.2.0", "@tauri-apps/plugin-autostart": "~2.2.0",
"@tauri-apps/plugin-clipboard-manager": "~2.3.2", "@tauri-apps/plugin-clipboard-manager": "~2.3.2",
@@ -77,6 +86,8 @@
"zustand": "^5.0.4" "zustand": "^5.0.4"
}, },
"devDependencies": { "devDependencies": {
"@tailwindcss/postcss": "^4.1.17",
"@tailwindcss/vite": "^4.0.0",
"@tauri-apps/cli": "^2.5.0", "@tauri-apps/cli": "^2.5.0",
"@types/dom-speech-recognition": "^0.0.4", "@types/dom-speech-recognition": "^0.0.4",
"@types/lodash-es": "^4.17.12", "@types/lodash-es": "^4.17.12",
@@ -93,11 +104,11 @@
"postcss": "^8.5.3", "postcss": "^8.5.3",
"release-it": "^18.1.2", "release-it": "^18.1.2",
"sass": "^1.87.0", "sass": "^1.87.0",
"tailwindcss": "^3.4.17", "tailwindcss": "^4.0.0",
"tsup": "^8.4.0", "tsup": "^8.4.0",
"tsx": "^4.19.4", "tsx": "^4.19.4",
"typescript": "^5.8.3", "typescript": "^5.8.3",
"vite": "^5.4.19" "vite": "^5.4.19"
}, },
"packageManager": "pnpm@10.11.0+sha512.6540583f41cc5f628eb3d9773ecee802f4f9ef9923cc45b69890fb47991d4b092964694ec3a4f738a420c918a333062c8b925d312f42e4f0c263eb603551f977" "packageManager": "pnpm@10.11.0+sha512.6540583f41cc5f628eb3d9773ecee802f4f9ef9923cc45b69890fb47991d4b092964694ec3a4f738a420c918a333062c8b925d312f42e4f0c263eb603551f977"
} }

1550
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,7 @@
export default { export default {
plugins: { plugins: {
tailwindcss: {}, // Tailwind v4 PostCSS plugin has moved to @tailwindcss/postcss
'@tailwindcss/postcss': {},
autoprefixer: {}, autoprefixer: {},
}, },
} }

39
scripts/buildWebAfter.ts Normal file
View File

@@ -0,0 +1,39 @@
import { readFileSync, writeFileSync } from "fs";
import { join, dirname } from "path";
import { fileURLToPath } from "url";
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const extractCssVars = () => {
const filePath = join(__dirname, "../out/search-chat/index.css");
const cssContent = readFileSync(filePath, "utf-8");
const vars: Record<string, string> = {};
const propertyBlockRegex = /@property\s+(--[\w-]+)\s*\{([\s\S]*?)\}/g;
let match: RegExpExecArray | null;
while ((match = propertyBlockRegex.exec(cssContent))) {
const [, varName, body] = match;
const initialValueMatch = /initial-value\s*:\s*([^;]+);/.exec(body);
if (initialValueMatch) {
vars[varName] = initialValueMatch[1].trim();
}
}
const cssVarsBlock =
`.coco-container {\n` +
Object.entries(vars)
.map(([k, v]) => ` ${k}: ${v};`)
.join("\n") +
`\n}\n`;
writeFileSync(filePath, `${cssContent}\n${cssVarsBlock}`, "utf-8");
};
extractCssVars();

View File

@@ -12,7 +12,7 @@ import {
handleNetworkError, handleNetworkError,
} from "./tools"; } from "./tools";
type Fn = (data: FcResponse<any>) => unknown; type Fn = (data: FcResponse<unknown>) => unknown;
interface IAnyObj { interface IAnyObj {
[index: string]: unknown; [index: string]: unknown;
@@ -85,8 +85,26 @@ export const Get = <T>(
new Promise((resolve) => { new Promise((resolve) => {
const appStore = JSON.parse(localStorage.getItem("app-store") || "{}"); const appStore = JSON.parse(localStorage.getItem("app-store") || "{}");
let baseURL = appStore.state?.endpoint_http; // In Vite dev, prefer using the proxy by keeping requests relative
if (!baseURL || baseURL === "undefined") { const isDev = (import.meta as any).env?.DEV === true;
const PROXY_PREFIXES: readonly string[] = [
"account",
"chat",
"query",
"connector",
"integration",
"assistant",
"datasource",
"settings",
"mcp_server",
];
const shouldProxy =
isDev &&
url.startsWith("/") &&
PROXY_PREFIXES.some((p) => url.startsWith(`/${p}`));
let baseURL: string = appStore.state?.endpoint_http as string;
if (!baseURL || baseURL === "undefined" || shouldProxy) {
baseURL = ""; baseURL = "";
} }
@@ -117,8 +135,25 @@ export const Post = <T>(
return new Promise((resolve) => { return new Promise((resolve) => {
const appStore = JSON.parse(localStorage.getItem("app-store") || "{}"); const appStore = JSON.parse(localStorage.getItem("app-store") || "{}");
let baseURL = appStore.state?.endpoint_http; const isDev = (import.meta as any).env?.DEV === true;
if (!baseURL || baseURL === "undefined") { const PROXY_PREFIXES: readonly string[] = [
"account",
"chat",
"query",
"connector",
"integration",
"assistant",
"datasource",
"settings",
"mcp_server",
];
const shouldProxy =
isDev &&
url.startsWith("/") &&
PROXY_PREFIXES.some((p) => url.startsWith(`/${p}`));
let baseURL: string = appStore.state?.endpoint_http as string;
if (!baseURL || baseURL === "undefined" || shouldProxy) {
baseURL = ""; baseURL = "";
} }

View File

@@ -41,15 +41,20 @@ const AssistantItem = memo(
)} )}
onClick={onClick} onClick={onClick}
> >
<div className="flex items-center justify-center size-6 bg-white border border-[#E6E6E6] rounded-full overflow-hidden"> {_source?.icon?.startsWith("font_") ? (
{_source?.icon?.startsWith("font_") ? ( <FontIcon
<FontIcon name={_source?.icon} className="size-4" /> name={_source?.icon}
className="w-4 h-4 rounded-full dark:drop-shadow-[0_0_6px_rgb(255,255,255)]"
/>
) : ( ) : (
<img src={logoImg} className="size-4" alt={name} /> <img
src={logoImg}
className="w-4 h-4 rounded-full dark:drop-shadow-[0_0_6px_rgb(255,255,255)]"
alt={name}
/>
)} )}
</div>
<div className="text-left flex-1 min-w-0"> <div className="text-left flex-1 min-w-0">
<div className="font-medium text-gray-900 dark:text-white truncate"> <div className="text-sm font-medium text-gray-900 dark:text-white truncate">
{_source?.name || "-"} {_source?.name || "-"}
</div> </div>
<div className="text-xs text-gray-500 dark:text-gray-400 truncate"> <div className="text-xs text-gray-500 dark:text-gray-400 truncate">
@@ -67,4 +72,4 @@ const AssistantItem = memo(
) )
); );
export default AssistantItem; export default AssistantItem;

View File

@@ -1,8 +1,11 @@
import { useState, useRef, useCallback, useEffect } from "react"; import { useState, useRef, useCallback, useEffect } from "react";
import { ChevronDownIcon, RefreshCw } from "lucide-react"; import { ChevronDownIcon, RefreshCw } from "lucide-react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { isNil } from "lodash-es"; import {
import { Popover, PopoverButton, PopoverPanel } from "@headlessui/react"; Popover,
PopoverTrigger,
PopoverContent,
} from "@/components/ui/popover";
import { useDebounce, useKeyPress, usePagination } from "ahooks"; import { useDebounce, useKeyPress, usePagination } from "ahooks";
import clsx from "clsx"; import clsx from "clsx";
@@ -17,6 +20,7 @@ import { AssistantFetcher } from "./AssistantFetcher";
import AssistantItem from "./AssistantItem"; import AssistantItem from "./AssistantItem";
import Pagination from "@/components/Common/Pagination"; import Pagination from "@/components/Common/Pagination";
import { useSearchStore } from "@/stores/searchStore"; import { useSearchStore } from "@/stores/searchStore";
import { Button } from "../ui/button";
interface AssistantListProps { interface AssistantListProps {
assistantIDs?: string[]; assistantIDs?: string[];
@@ -83,6 +87,7 @@ export function AssistantList({ assistantIDs = [] }: AssistantListProps) {
const [highlightIndex, setHighlightIndex] = useState<number>(-1); const [highlightIndex, setHighlightIndex] = useState<number>(-1);
const [isKeyboardActive, setIsKeyboardActive] = useState(false); const [isKeyboardActive, setIsKeyboardActive] = useState(false);
const [open, setOpen] = useState(false);
useEffect(() => { useEffect(() => {
const targetId = askAiAssistantId ?? targetAssistantId; const targetId = askAiAssistantId ?? targetAssistantId;
@@ -105,7 +110,7 @@ export function AssistantList({ assistantIDs = [] }: AssistantListProps) {
useKeyPress( useKeyPress(
["uparrow", "downarrow", "enter"], ["uparrow", "downarrow", "enter"],
(event, key) => { (event, key) => {
const isClose = isNil(popoverButtonRef.current?.dataset["open"]); const isClose = !open;
if (isClose) return; if (isClose) return;
@@ -161,26 +166,29 @@ export function AssistantList({ assistantIDs = [] }: AssistantListProps) {
}, []); }, []);
return ( return (
<div className="relative"> <div ref={popoverRef} className="relative">
<Popover ref={popoverRef}> <Popover
<PopoverButton open={open}
onOpenChange={(v) => {
setOpen(v);
}}
>
<PopoverTrigger
ref={popoverButtonRef} ref={popoverButtonRef}
className="h-6 p-1 px-1.5 flex items-center gap-1 rounded-full bg-white dark:bg-[#202126] text-sm/6 font-semibold text-gray-800 dark:text-[#d8d8d8] border border-gray-200 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-700 focus:outline-none" className="h-6 p-1 px-1.5 flex items-center gap-1 rounded-full border border-input bg-background text-sm/6 font-semibold text-foreground hover:bg-accent hover:text-accent-foreground focus:outline-none"
> >
<div className="w-4 h-4 flex justify-center items-center rounded-full bg-white border border-[#E6E6E6]"> {currentAssistant?._source?.icon?.startsWith("font_") ? (
{currentAssistant?._source?.icon?.startsWith("font_") ? ( <FontIcon
<FontIcon name={currentAssistant._source.icon}
name={currentAssistant._source.icon} className="w-4 h-4"
className="w-3 h-3" />
/> ) : (
) : ( <img
<img src={logoImg}
src={logoImg} className="w-4 h-4"
className="w-3 h-3" alt={t("assistant.message.logo")}
alt={t("assistant.message.logo")} />
/> )}
)}
</div>
<div className="max-w-[100px] truncate"> <div className="max-w-[100px] truncate">
{currentAssistant?._source?.name || "Coco AI"} {currentAssistant?._source?.name || "Coco AI"}
</div> </div>
@@ -190,12 +198,14 @@ export function AssistantList({ assistantIDs = [] }: AssistantListProps) {
popoverButtonRef.current?.click(); popoverButtonRef.current?.click();
}} }}
> >
<ChevronDownIcon className="size-4 text-gray-500 dark:text-gray-400 transition-transform" /> <ChevronDownIcon className="size-4 text-muted-foreground transition-transform" />
</VisibleKey> </VisibleKey>
</PopoverButton> </PopoverTrigger>
<PopoverPanel <PopoverContent
className="absolute z-50 top-full mt-1 left-0 w-60 rounded-xl bg-white dark:bg-[#202126] p-3 text-sm/6 text-[#333] dark:text-[#D8D8D8] shadow-lg border dark:border-white/10 focus:outline-none max-h-[calc(100vh-150px)] overflow-y-auto" side="bottom"
align="start"
className="z-50 w-60 rounded-xl p-3 shadow-lg focus:outline-none max-h-[calc(100vh-150px)] overflow-y-auto"
onMouseMove={handleMouseMove} onMouseMove={handleMouseMove}
> >
<div className="flex items-center justify-between text-sm font-bold"> <div className="flex items-center justify-between text-sm font-bold">
@@ -203,9 +213,11 @@ export function AssistantList({ assistantIDs = [] }: AssistantListProps) {
{t("assistant.popover.title")}{pagination.total} {t("assistant.popover.title")}{pagination.total}
</div> </div>
<button <Button
variant="outline"
size="icon"
onClick={handleRefresh} onClick={handleRefresh}
className="flex items-center justify-center size-6 bg-white dark:bg-[#202126] rounded-lg border dark:border-white/10" className="size-6"
disabled={isRefreshing} disabled={isRefreshing}
> >
<VisibleKey shortcut="R" onKeyPress={handleRefresh}> <VisibleKey shortcut="R" onKeyPress={handleRefresh}>
@@ -218,7 +230,7 @@ export function AssistantList({ assistantIDs = [] }: AssistantListProps) {
)} )}
/> />
</VisibleKey> </VisibleKey>
</button> </Button>
</div> </div>
<VisibleKey <VisibleKey
@@ -234,8 +246,8 @@ export function AssistantList({ assistantIDs = [] }: AssistantListProps) {
autoFocus autoFocus
value={keyword} value={keyword}
placeholder={t("assistant.popover.search")} placeholder={t("assistant.popover.search")}
className="w-full h-8 px-2 bg-transparent border rounded-[6px] dark:border-white/10" className="w-full h-8"
onChange={(event) => { onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
setKeyword(event.target.value); setKeyword(event.target.value);
}} }}
/> />
@@ -272,7 +284,7 @@ export function AssistantList({ assistantIDs = [] }: AssistantListProps) {
<NoDataImage /> <NoDataImage />
</div> </div>
)} )}
</PopoverPanel> </PopoverContent>
</Popover> </Popover>
</div> </div>
); );

View File

@@ -388,7 +388,7 @@ const ChatAI = memo(
<div <div
data-tauri-drag-region data-tauri-drag-region
data-chat-instance={instanceId} data-chat-instance={instanceId}
className={`flex flex-col rounded-[6px] h-full overflow-hidden relative`} className={`flex flex-col rounded-md h-full overflow-hidden relative`}
> >
<ChatHeader <ChatHeader
clearChat={clearChat} clearChat={clearChat}

View File

@@ -95,13 +95,13 @@ export function ChatHeader({
{isChatPage ? null : ( {isChatPage ? null : (
<button className="inline-flex" onClick={onOpenChatAI}> <button className="inline-flex" onClick={onOpenChatAI}>
<VisibleKey shortcut={external} onKeyPress={onOpenChatAI}> <VisibleKey shortcut={external} onKeyPress={onOpenChatAI}>
<WindowsFullIcon className="rotate-30 scale-x-[-1]" /> <WindowsFullIcon className="scale-x-[-1]" />
</VisibleKey> </VisibleKey>
</button> </button>
)} )}
</div> </div>
) : ( ) : (
<WebLogin panelClassName="top-8 right-0" /> <WebLogin side="bottom" align="end" />
)} )}
</header> </header>
); );

View File

@@ -28,7 +28,7 @@ const ConnectPrompt = () => {
<p className="mb-4 w-[388px]">{t("assistant.chat.connect_tip")}</p> <p className="mb-4 w-[388px]">{t("assistant.chat.connect_tip")}</p>
<button <button
className="flex items-center gap-2 px-6 py-2 rounded-[6px] text-[#0072ff] transition-colors" className="flex items-center gap-2 px-6 py-2 rounded-md text-[#0072ff] transition-colors"
onClick={handleConnect} onClick={handleConnect}
> >
<span>{t("assistant.chat.connect")}</span> <span>{t("assistant.chat.connect")}</span>

View File

@@ -1,9 +1,13 @@
import { useState, useCallback, useEffect, useRef } from "react"; import { useState, useCallback, useEffect, useRef } from "react";
import { Settings, RefreshCw, Check, Server } from "lucide-react"; import { Settings, RefreshCw, Check, Server } from "lucide-react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Popover, PopoverButton, PopoverPanel } from "@headlessui/react"; import {
Popover,
PopoverTrigger,
PopoverContent,
} from "@/components/ui/popover";
import { Button } from "@/components/ui/button";
import { useKeyPress } from "ahooks"; import { useKeyPress } from "ahooks";
import { isNil } from "lodash-es";
import logoImg from "@/assets/icon.svg"; import logoImg from "@/assets/icon.svg";
import ServerIcon from "@/icons/Server"; import ServerIcon from "@/icons/Server";
@@ -61,6 +65,7 @@ export function ServerList({ clearChat }: ServerListProps) {
const popoverRef = useRef<HTMLDivElement>(null); const popoverRef = useRef<HTMLDivElement>(null);
const serverListButtonRef = useRef<HTMLButtonElement>(null); const serverListButtonRef = useRef<HTMLButtonElement>(null);
const [open, setOpen] = useState(false);
const { refreshServerList } = useServers(); const { refreshServerList } = useServers();
@@ -143,7 +148,7 @@ export function ServerList({ clearChat }: ServerListProps) {
["uparrow", "downarrow", "enter"], ["uparrow", "downarrow", "enter"],
async (event, key) => { async (event, key) => {
const service = await getCurrentWindowService(); const service = await getCurrentWindowService();
const isClose = isNil(serverListButtonRef.current?.dataset["open"]); const isClose = !open;
const length = serverList.length; const length = serverList.length;
if (isClose || length <= 1) return; if (isClose || length <= 1) return;
@@ -182,122 +187,130 @@ export function ServerList({ clearChat }: ServerListProps) {
}, []); }, []);
return ( return (
<Popover ref={popoverRef} className="relative"> <div ref={popoverRef} className="relative">
<PopoverButton ref={serverListButtonRef} className="flex items-center"> <Popover open={open} onOpenChange={setOpen}>
<VisibleKey <PopoverTrigger ref={serverListButtonRef} className="flex items-center">
shortcut={serviceListShortcut} <VisibleKey
onKeyPress={() => { shortcut={serviceListShortcut}
serverListButtonRef.current?.click(); onKeyPress={() => {
}} serverListButtonRef.current?.click();
> }}
<ServerIcon /> >
</VisibleKey> <ServerIcon />
</PopoverButton> </VisibleKey>
</PopoverTrigger>
<PopoverPanel <PopoverContent
onMouseMove={handleMouseMove} side="bottom"
className="absolute right-0 z-10 mt-2 min-w-[240px] bg-white dark:bg-[#202126] rounded-lg shadow-lg border border-gray-200 dark:border-gray-700" align="end"
> onMouseMove={handleMouseMove}
<div className="p-3"> className="z-10 min-w-60 rounded-lg shadow-lg"
<div className="flex items-center justify-between mb-3 whitespace-nowrap"> >
<h3 className="text-sm font-medium text-gray-900 dark:text-gray-100"> <div className="p-3">
{t("assistant.chat.servers")} <div className="flex items-center justify-between mb-3 whitespace-nowrap">
</h3> <h3 className="text-sm font-medium text-foreground">
<div className="flex items-center gap-2"> {t("assistant.chat.servers")}
<button </h3>
onClick={openSettings} <div className="flex items-center gap-2">
className="p-1 rounded-[6px] hover:bg-gray-100 dark:hover:bg-gray-800 text-gray-500 dark:text-gray-400" <Button
> onClick={openSettings}
<VisibleKey shortcut=","> variant="ghost"
<Settings className="h-4 w-4 text-[#0287FF]" /> size="icon"
</VisibleKey> className="rounded-md focus-visible:ring-0 focus-visible:ring-offset-0"
</button> >
<button <VisibleKey shortcut=",">
onClick={handleRefresh} <Settings className="h-4 w-4 text-primary" />
className="p-1 rounded-[6px] hover:bg-gray-100 dark:hover:bg-gray-800 text-gray-500 dark:text-gray-400" </VisibleKey>
disabled={isRefreshing} </Button>
> <Button
<VisibleKey shortcut="R" onKeyPress={handleRefresh}> onClick={handleRefresh}
<RefreshCw variant="ghost"
className={`h-4 w-4 text-[#0287FF] transition-transform duration-1000 ${ size="icon"
isRefreshing ? "animate-spin" : "" className="rounded-md focus-visible:ring-0 focus-visible:ring-offset-0"
}`} disabled={isRefreshing}
/> >
</VisibleKey> <VisibleKey shortcut="R" onKeyPress={handleRefresh}>
</button> <RefreshCw
className={`h-4 w-4 text-primary transition-transform duration-1000 ${
isRefreshing ? "animate-spin" : ""
}`}
/>
</VisibleKey>
</Button>
</div>
</div> </div>
</div> <div className="space-y-1">
<div className="space-y-1"> {list.length > 0 ? (
{list.length > 0 ? ( list.map((server) => (
list.map((server) => ( <div
<div key={server.id}
key={server.id} onClick={() => switchServer(server)}
onClick={() => switchServer(server)} className={`w-full flex items-center justify-between gap-1 p-2 rounded-lg transition-colors whitespace-nowrap
className={`w-full flex items-center justify-between gap-1 p-2 rounded-lg transition-colors whitespace-nowrap
${ ${
currentService?.id === server.id || currentService?.id === server.id ||
highlightId === server.id highlightId === server.id
? "bg-gray-100 dark:bg-gray-800" ? "bg-muted"
: "hover:bg-gray-50 dark:hover:bg-gray-800/50" : "hover:bg-muted"
}`} }`}
> >
<div className="flex items-center gap-2 min-w-0"> <div className="flex items-center gap-2 min-w-0">
<img <img
src={server?.provider?.icon || logoImg} src={server?.provider?.icon || logoImg}
alt={server.name} alt={server.name}
className="w-6 h-6 rounded-full bg-gray-100 dark:bg-gray-800 dark:drop-shadow-[0_0_6px_rgb(255,255,255)]" className="w-6 h-6 rounded-full dark:drop-shadow-[0_0_6px_rgb(255,255,255)]"
onError={(e) => { onError={(e) => {
const target = e.target as HTMLImageElement; const target = e.target as HTMLImageElement;
target.src = logoImg; target.src = logoImg;
}} }}
/> />
<div className="text-left flex-1 min-w-0"> <div className="text-left flex-1 min-w-0">
<div className="text-sm font-medium text-gray-900 dark:text-gray-100 truncate max-w-[200px]"> <div className="text-sm font-medium text-foreground truncate max-w-[200px]">
{server.name} {server.name}
</div>
<div className="text-xs text-muted-foreground truncate max-w-[200px]">
{t("assistant.chat.aiAssistant")}:{" "}
{server.stats?.assistant_count || 1}
</div>
</div> </div>
<div className="text-xs text-gray-500 dark:text-gray-400 truncate max-w-[200px]"> </div>
{t("assistant.chat.aiAssistant")}:{" "} <div className="flex flex-col items-center gap-2">
{server.stats?.assistant_count || 1} <StatusIndicator
enabled={server.enabled}
public={server.public}
hasProfile={!!server?.profile}
status={server.health?.status}
/>
<div className="size-4 flex justify-end">
{currentService?.id === server.id && (
<VisibleKey
shortcut="↓↑"
shortcutClassName="w-6 -translate-x-4"
>
<Check className="w-full h-full text-muted-foreground" />
</VisibleKey>
)}
</div> </div>
</div> </div>
</div> </div>
<div className="flex flex-col items-center gap-2"> ))
<StatusIndicator ) : (
enabled={server.enabled} <div className="flex flex-col items-center justify-center py-6 text-center">
public={server.public} <Server className="w-8 h-8 text-muted-foreground mb-2" />
hasProfile={!!server?.profile} <p className="text-sm text-muted-foreground">
status={server.health?.status} {t("assistant.chat.noServers")}
/> </p>
<div className="size-4 flex justify-end"> <button
{currentService?.id === server.id && ( onClick={openSettings}
<VisibleKey className="mt-2 text-xs text-[#0287FF] hover:underline"
shortcut="↓↑" >
shortcutClassName="w-6 -translate-x-4" {t("assistant.chat.addServer")}
> </button>
<Check className="w-full h-full text-gray-500 dark:text-gray-400" />
</VisibleKey>
)}
</div>
</div>
</div> </div>
)) )}
) : ( </div>
<div className="flex flex-col items-center justify-center py-6 text-center">
<Server className="w-8 h-8 text-gray-400 dark:text-gray-600 mb-2" />
<p className="text-sm text-gray-500 dark:text-gray-400">
{t("assistant.chat.noServers")}
</p>
<button
onClick={openSettings}
className="mt-2 text-xs text-[#0287FF] hover:underline"
>
{t("assistant.chat.addServer")}
</button>
</div>
)}
</div> </div>
</div> </PopoverContent>
</PopoverPanel> </Popover>
</Popover> </div>
); );
} }

View File

@@ -122,7 +122,7 @@ const Splash = ({ assistantIDs = [], startPage }: SplashProps) => {
return ( return (
<li key={id} className="mobile:w-full w-1/2 p-1"> <li key={id} className="mobile:w-full w-1/2 p-1">
<div <div
className="group h-[74px] px-3 py-2 text-sm rounded-xl border dark:border-[#262626] bg-white dark:bg-black cursor-pointer transition hover:!border-[#0087FF]" className="group h-[74px] px-3 py-2 text-sm rounded-xl border border-input bg-white dark:bg-black cursor-pointer transition hover:border-[#0087FF]!"
onClick={() => { onClick={() => {
setCurrentAssistant(item); setCurrentAssistant(item);

View File

@@ -34,7 +34,7 @@ const AudioRecording: FC<AudioRecordingProps> = (props) => {
const state = useReactive({ ...INITIAL_STATE }); const state = useReactive({ ...INITIAL_STATE });
const containerRef = useRef<HTMLDivElement>(null); const containerRef = useRef<HTMLDivElement>(null);
const recordRef = useRef<RecordPlugin>(); const recordRef = useRef<RecordPlugin>();
const { withVisibility, addError } = useAppStore(); const { addError } = useAppStore();
const { currentService } = useConnectStore(); const { currentService } = useConnectStore();
const { wavesurfer } = useWavesurfer({ const { wavesurfer } = useWavesurfer({
@@ -146,7 +146,7 @@ const AudioRecording: FC<AudioRecordingProps> = (props) => {
}; };
const startRecording = async () => { const startRecording = async () => {
await withVisibility(checkPermission); await checkPermission();
state.isRecording = true; state.isRecording = true;
recordRef.current?.startRecording(); recordRef.current?.startRecording();
}; };
@@ -173,9 +173,9 @@ const AudioRecording: FC<AudioRecordingProps> = (props) => {
<div <div
className={clsx( className={clsx(
"absolute -inset-2 flex items-center gap-1 px-1 rounded translate-x-full transition-all bg-[#ededed] dark:bg-[#202126]", "absolute inset-0 flex items-center gap-1 px-1 rounded translate-x-full transition-all bg-[#ededed] dark:bg-[#202126]",
{ {
"!translate-x-0": state.isRecording || state.converting, "translate-x-0!": state.isRecording || state.converting,
} }
)} )}
> >
@@ -184,7 +184,7 @@ const AudioRecording: FC<AudioRecordingProps> = (props) => {
className={clsx( className={clsx(
"flex items-center justify-center size-6 bg-white dark:bg-black rounded-full transition cursor-pointer", "flex items-center justify-center size-6 bg-white dark:bg-black rounded-full transition cursor-pointer",
{ {
"!cursor-not-allowed opacity-50": state.converting, "cursor-not-allowed! opacity-50": state.converting,
} }
)} )}
onClick={() => resetState()} onClick={() => resetState()}

View File

@@ -107,7 +107,7 @@ export const MessageActions = ({
<button <button
id={copyButtonId} id={copyButtonId}
onClick={handleCopy} onClick={handleCopy}
className="p-1 hover:bg-black/5 dark:hover:bg-white/5 rounded-lg transition-colors" className="p-1 rounded-lg hover:bg-muted transition-colors"
> >
{copied ? ( {copied ? (
<Check <Check
@@ -131,7 +131,7 @@ export const MessageActions = ({
{!isRefreshOnly && ( {!isRefreshOnly && (
<button <button
onClick={handleLike} onClick={handleLike}
className={`p-1 hover:bg-black/5 dark:hover:bg-white/5 rounded-lg transition-colors ${ className={`p-1 rounded-lg hover:bg-muted transition-colors ${
liked ? "animate-shake" : "" liked ? "animate-shake" : ""
}`} }`}
> >
@@ -151,7 +151,7 @@ export const MessageActions = ({
{!isRefreshOnly && ( {!isRefreshOnly && (
<button <button
onClick={handleDislike} onClick={handleDislike}
className={`p-1 hover:bg-black/5 dark:hover:bg-white/5 rounded-lg transition-colors ${ className={`p-1 rounded-lg hover:bg-muted transition-colors ${
disliked ? "animate-shake" : "" disliked ? "animate-shake" : ""
}`} }`}
> >
@@ -172,7 +172,7 @@ export const MessageActions = ({
<> <>
<button <button
onClick={handleSpeak} onClick={handleSpeak}
className="p-1 hover:bg-black/5 dark:hover:bg-white/5 rounded-lg transition-colors" className="p-1 rounded-lg hover:bg-muted transition-colors"
> >
<Volume2 <Volume2
className={`w-4 h-4 ${ className={`w-4 h-4 ${
@@ -191,7 +191,7 @@ export const MessageActions = ({
{question && ( {question && (
<button <button
onClick={handleResend} onClick={handleResend}
className={`p-1 hover:bg-black/5 dark:hover:bg-white/5 rounded-lg transition-colors ${ className={`p-1 rounded-lg hover:bg-muted transition-colors ${
isResending ? "animate-spin" : "" isResending ? "animate-spin" : ""
}`} }`}
> >

View File

@@ -59,7 +59,7 @@ export function DataSourceItem({ name, icon, connector }: DataSourceItemProps) {
{icon?.startsWith("font_") ? ( {icon?.startsWith("font_") ? (
<FontIcon name={icon} className="size-6" /> <FontIcon name={icon} className="size-6" />
) : ( ) : (
<img src={getTypeIcon()} alt={name} className="size-6 dark:drop-shadow-[0_0_6px_rgb(255,255,255)]" /> <img src={getTypeIcon()} alt={name} className="size-6 rounded-full dark:drop-shadow-[0_0_6px_rgb(255,255,255)]" />
)} )}
<span className="font-medium text-gray-900 dark:text-white"> <span className="font-medium text-gray-900 dark:text-white">

View File

@@ -44,7 +44,7 @@ export function DataSourcesList({ server }: { server: string }) {
<h2 className="flex justify-between text-xl font-medium text-gray-900 dark:text-white"> <h2 className="flex justify-between text-xl font-medium text-gray-900 dark:text-white">
{t("cloud.dataSource.title")} {t("cloud.dataSource.title")}
<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-md bg-white dark:bg-gray-800 border border-[rgba(228,229,239,1)] dark:border-gray-700"
onClick={() => initServerAppData()} onClick={() => initServerAppData()}
> >
<RefreshCcw <RefreshCcw

View File

@@ -165,7 +165,7 @@ const LoginButton: FC<LoginButtonProps> = memo((props) => {
return ( return (
<button <button
className="px-6 py-2 bg-blue-500 text-white rounded-[6px] hover:bg-blue-600 transition-colors mb-3" className="px-6 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors mb-3"
onClick={LoginClick} onClick={LoginClick}
aria-label={t("cloud.login")} aria-label={t("cloud.login")}
> >
@@ -186,7 +186,7 @@ const LoadingState: FC<LoadingStateProps> = memo((props) => {
return ( return (
<div className="flex items-center space-x-2 mb-3"> <div className="flex items-center space-x-2 mb-3">
<button <button
className="px-6 py-2 text-white bg-red-500 rounded-[6px] hover:bg-red-600 transition-colors" className="px-6 py-2 text-white bg-red-500 rounded-md hover:bg-red-600 transition-colors"
onClick={onCancel} onClick={onCancel}
> >
{t("cloud.cancel")} {t("cloud.cancel")}

View File

@@ -18,7 +18,9 @@ const ServiceHeader = memo(
({ refreshLoading, refreshClick }: ServiceHeaderProps) => { ({ refreshLoading, refreshClick }: ServiceHeaderProps) => {
const { t } = useTranslation(); const { t } = useTranslation();
const cloudSelectService = useConnectStore((state) => state.cloudSelectService); const cloudSelectService = useConnectStore(
(state) => state.cloudSelectService
);
const { enableServer, removeServer } = useServers(); const { enableServer, removeServer } = useServers();
@@ -46,7 +48,7 @@ const ServiceHeader = memo(
/> />
<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-md bg-white dark:bg-gray-800 border border-[rgba(228,229,239,1)] dark:border-gray-700"
onClick={() => onClick={() =>
OpenURLWithBrowser(cloudSelectService?.provider?.website) OpenURLWithBrowser(cloudSelectService?.provider?.website)
} }
@@ -54,7 +56,7 @@ const ServiceHeader = memo(
<Globe className="w-3.5 h-3.5" /> <Globe className="w-3.5 h-3.5" />
</button> </button>
<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-md bg-white dark:bg-gray-800 border border-[rgba(228,229,239,1)] dark:border-gray-700"
onClick={() => refreshClick(cloudSelectService?.id)} onClick={() => refreshClick(cloudSelectService?.id)}
> >
<RefreshCcw <RefreshCcw
@@ -63,7 +65,7 @@ const ServiceHeader = memo(
</button> </button>
{!cloudSelectService?.builtin && ( {!cloudSelectService?.builtin && (
<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-md bg-white dark:bg-gray-800 border border-[rgba(228,229,239,1)] dark:border-gray-700"
onClick={() => removeServer(cloudSelectService?.id)} onClick={() => removeServer(cloudSelectService?.id)}
> >
<Trash2 className="w-3.5 h-3.5 text-[#ff4747]" /> <Trash2 className="w-3.5 h-3.5 text-[#ff4747]" />

View File

@@ -53,7 +53,7 @@ export const Sidebar = forwardRef<{ refreshData: () => void }, SidebarProps>(
<img <img
src={item?.provider?.icon || cocoLogoImg} src={item?.provider?.icon || cocoLogoImg}
alt={`${item.name} logo`} alt={`${item.name} logo`}
className="w-5 h-5 flex-shrink-0 dark:drop-shadow-[0_0_6px_rgb(255,255,255)]" className="w-5 h-5 shrink-0 rounded-full dark:drop-shadow-[0_0_6px_rgb(255,255,255)]"
onError={(e) => { onError={(e) => {
const target = e.target as HTMLImageElement; const target = e.target as HTMLImageElement;
target.src = cocoLogoImg; target.src = cocoLogoImg;

View File

@@ -62,13 +62,13 @@ const ApiDetails: React.FC = () => {
{logs.map((log, index) => ( {logs.map((log, index) => (
<div <div
key={index} key={index}
className="p-4 border rounded-[6px] shadow-sm bg-gray-50" className="p-4 border rounded-md shadow-sm bg-gray-50"
> >
<h4 className="font-semibold text-gray-800"> <h4 className="font-semibold text-gray-800">
Latest Request {index + 1}: Latest Request {index + 1}:
</h4> </h4>
<div className="text-sm text-gray-700 mt-1"> <div className="text-sm text-gray-700 mt-1">
<pre className="bg-gray-100 p-2 rounded-[6px] whitespace-pre-wrap"> <pre className="bg-gray-100 p-2 rounded-md whitespace-pre-wrap">
{JSON.stringify(log.request, null, 2)} {JSON.stringify(log.request, null, 2)}
</pre> </pre>
</div> </div>
@@ -87,7 +87,7 @@ const ApiDetails: React.FC = () => {
</h4> </h4>
{showIndex === index ? ( {showIndex === index ? (
<div className="text-sm text-gray-700 mt-1"> <div className="text-sm text-gray-700 mt-1">
<pre className="bg-green-100 p-2 rounded-[6px] text-green-700 whitespace-pre-wrap"> <pre className="bg-green-100 p-2 rounded-md text-green-700 whitespace-pre-wrap">
{JSON.stringify(log.response, null, 2)} {JSON.stringify(log.response, null, 2)}
</pre> </pre>
</div> </div>
@@ -98,7 +98,7 @@ const ApiDetails: React.FC = () => {
<> <>
<h4 className="font-semibold text-red-800 mt-4">Error:</h4> <h4 className="font-semibold text-red-800 mt-4">Error:</h4>
<div className="text-sm text-gray-700 mt-1"> <div className="text-sm text-gray-700 mt-1">
<pre className="bg-red-100 p-2 rounded-[6px] text-red-700 whitespace-pre-wrap"> <pre className="bg-red-100 p-2 rounded-md text-red-700 whitespace-pre-wrap">
{JSON.stringify(log.error, null, 2)} {JSON.stringify(log.error, null, 2)}
</pre> </pre>
</div> </div>

View File

@@ -2,6 +2,7 @@ import React, { useEffect, useCallback } from "react";
import { Bot, Search } from "lucide-react"; import { Bot, Search } from "lucide-react";
import platformAdapter from "@/utils/platformAdapter"; import platformAdapter from "@/utils/platformAdapter";
import clsx from "clsx";
interface ChatSwitchProps { interface ChatSwitchProps {
isChatMode: boolean; isChatMode: boolean;
@@ -29,19 +30,31 @@ const ChatSwitch: React.FC<ChatSwitchProps> = ({ isChatMode, onChange }) => {
<div <div
role="switch" role="switch"
aria-checked={isChatMode} aria-checked={isChatMode}
className={`relative flex items-center justify-between w-10 h-[20px] rounded-full cursor-pointer transition-colors duration-300 ${ className={`relative flex items-center justify-between w-10 h-5 rounded-full cursor-pointer transition-colors duration-300 ${
isChatMode ? "bg-[#0072ff]" : "bg-[var(--coco-primary-color)]" isChatMode ? "bg-[#0072ff]" : "bg-(--coco-primary-color)"
}`} }`}
onClick={handleToggle} onClick={handleToggle}
> >
<div className="absolute top-0 left-0 w-full h-full pointer-events-none flex items-center justify-between px-1">
{isChatMode ? <Bot className="w-4 h-4 text-white" /> : <div></div>}
{!isChatMode ? <Search className="w-4 h-4 text-white" /> : <div></div>}
</div>
<div <div
className={`absolute top-[1px] left-[1px] h-[18px] w-[18px] bg-white rounded-full shadow-md transform transition-transform duration-300 ${ className={clsx(
isChatMode ? "translate-x-5" : "translate-x-0" "absolute inset-0 pointer-events-none flex items-center px-1 text-white",
}`} {
"justify-end": !isChatMode,
}
)}
>
{isChatMode ? (
<Bot className="size-4" />
) : (
<Search className="size-4" />
)}
</div>
<div
className={clsx(
"absolute top-px h-4.5 w-4.5 bg-white rounded-full shadow-md",
[isChatMode ? "right-px" : "left-px"]
)}
></div> ></div>
</div> </div>
); );

View File

@@ -1,33 +1,35 @@
import { import * as CheckboxPrimitive from "@radix-ui/react-checkbox";
CheckboxProps as HeadlessCheckboxProps, import type { ComponentProps } from "react";
Checkbox as HeadlessCheckbox,
} from "@headlessui/react";
import clsx from "clsx"; import clsx from "clsx";
import { CheckIcon } from "lucide-react"; import { CheckIcon } from "lucide-react";
interface CheckboxProps extends HeadlessCheckboxProps { interface CheckboxProps
extends Omit<ComponentProps<typeof CheckboxPrimitive.Root>, "onCheckedChange" | "onChange"> {
indeterminate?: boolean; indeterminate?: boolean;
onChange?: (checked: boolean) => void;
} }
const Checkbox = (props: CheckboxProps) => { const Checkbox = (props: CheckboxProps) => {
const { indeterminate, className, ...rest } = props; const { indeterminate, className, onChange, checked, ...rest } = props;
return ( return (
<HeadlessCheckbox <CheckboxPrimitive.Root
{...rest} {...rest}
checked={checked}
onCheckedChange={(v) => onChange?.(v === true)}
className={clsx( className={clsx(
"group size-4 rounded-sm border border-black/30 dark:border-white/30 data-[checked]:bg-[#2F54EB] data-[checked]:!border-[#2F54EB] transition cursor-pointer", "group h-4 w-4 rounded-sm border border-black/30 dark:border-white/30 data-[state=checked]:bg-[#2F54EB] data-[state=checked]:border-[#2F54EB] transition cursor-pointer inline-flex items-center justify-center",
className className
)} )}
> >
{indeterminate && ( {indeterminate && (
<div className="size-full flex items-center justify-center group-data-[checked]:hidden"> <div className="h-full w-full flex items-center justify-center group-data-[state=checked]:hidden">
<div className="size-2 bg-[#2F54EB]"></div> <div className="h-2 w-2 bg-[#2F54EB]"></div>
</div> </div>
)} )}
<CheckIcon className="hidden size-[14px] text-white group-data-[checked]:block" /> <CheckIcon className="hidden h-[14px] w-[14px] text-white group-data-[state=checked]:block" />
</HeadlessCheckbox> </CheckboxPrimitive.Root>
); );
}; };

View File

@@ -8,7 +8,7 @@ const Copyright = () => {
const renderLogo = () => { const renderLogo = () => {
return ( return (
<a href="https://coco.rs/" target="_blank"> <a href="https://coco.rs/" target="_blank">
<img src={isDark ? logoDark : logoLight} alt="Logo" className="h-4" /> <img src={isDark ? logoDark : logoLight} alt="Logo" className="h-4!" />
</a> </a>
); );
}; };

View File

@@ -1,24 +1,20 @@
import { import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from "@/components/ui/dialog";
Button, import { Button } from "@/components/ui/button";
ButtonProps, import { FC, KeyboardEvent, ComponentProps } from "react";
Description,
Dialog,
DialogPanel,
DialogTitle,
} from "@headlessui/react";
import { FC, KeyboardEvent } from "react";
import clsx from "clsx"; import clsx from "clsx";
import { twMerge } from "tailwind-merge"; import { twMerge } from "tailwind-merge";
import VisibleKey from "./VisibleKey"; import VisibleKey from "./VisibleKey";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
type ShadButtonProps = ComponentProps<typeof Button>;
interface DeleteDialogProps { interface DeleteDialogProps {
isOpen: boolean; isOpen: boolean;
title: string; title: string;
description: string; description: string;
deleteButtonProps?: ButtonProps; deleteButtonProps?: ShadButtonProps;
cancelButtonProps?: ButtonProps; cancelButtonProps?: ShadButtonProps;
reverseButtonPosition?: boolean; reverseButtonPosition?: boolean;
setIsOpen: (isOpen: boolean) => void; setIsOpen: (isOpen: boolean) => void;
onCancel: () => void; onCancel: () => void;
@@ -49,69 +45,60 @@ const DeleteDialog: FC<DeleteDialogProps> = (props) => {
}; };
return ( return (
<Dialog <Dialog open={isOpen} onOpenChange={setIsOpen}>
open={isOpen} <DialogContent className="flex flex-col justify-between w-[360px] h-[160px] p-3 text-[#333] dark:text-white/90 border border-[#e6e6e6] bg-white dark:bg-[#202126] dark:border-white/10 shadow-[0_4px_10px_rgba(0,0,0,0.2)] rounded-lg dark:shadow-[0_8px_20px_rgba(0,0,0,0.4)]">
onClose={() => setIsOpen(false)} <DialogHeader className="mb-2">
className="relative z-1000" <DialogTitle className="text-base font-bold">{title}</DialogTitle>
> <DialogDescription className="text-sm">{description}</DialogDescription>
<div </DialogHeader>
id="headlessui-popover-panel:delete-history"
className="fixed inset-0 flex items-center justify-center w-screen"
>
<DialogPanel className="flex flex-col justify-between w-[360px] h-[160px] p-3 text-[#333] dark:text-white/90 border border-[#e6e6e6] bg-white dark:bg-[#202126] dark:border-white/10 shadow-[0_4px_10px_rgba(0,0,0,0.2)] rounded-lg dark:shadow-[0_8px_20px_rgba(0,0,0,0.4)]">
<div className="flex flex-col gap-3">
<DialogTitle className="text-base font-bold">{title}</DialogTitle>
<Description className="text-sm">{description}</Description>
</div>
<div <div
className={clsx("flex gap-4 self-end", { className={clsx("flex gap-4 self-end", {
"flex-row-reverse": reverseButtonPosition, "flex-row-reverse": reverseButtonPosition,
})} })}
>
<VisibleKey
shortcut="N"
shortcutClassName="left-[unset] right-0"
onKeyPress={onCancel}
> >
<VisibleKey <Button
shortcut="N" {...cancelButtonProps}
shortcutClassName="left-[unset] right-0" autoFocus
onKeyPress={onCancel} className={twMerge(
"h-8 px-4 text-sm text-[#666666] bg-[#F8F9FA] dark:text-white dark:bg-[#202126] border border-[#E6E6E6] dark:border-white/10 rounded-lg focus:border-black/30 dark:focus:border-white/50 transition",
cancelButtonProps?.className as string
)}
onClick={onCancel}
onKeyDown={(event) => {
handleEnter(event, onCancel);
}}
> >
<Button {t("deleteDialog.button.cancel")}
{...cancelButtonProps} </Button>
autoFocus </VisibleKey>
className={twMerge(
"h-8 px-4 text-sm text-[#666666] bg-[#F8F9FA] dark:text-white dark:bg-[#202126] border border-[#E6E6E6] dark:border-white/10 rounded-lg focus:border-black/30 dark:focus:border-white/50 transition",
cancelButtonProps?.className as string
)}
onClick={onCancel}
onKeyDown={(event) => {
handleEnter(event, onCancel);
}}
>
{t("deleteDialog.button.cancel")}
</Button>
</VisibleKey>
<VisibleKey <VisibleKey
shortcut="Y" shortcut="Y"
shortcutClassName="left-[unset] right-0" shortcutClassName="left-[unset] right-0"
onKeyPress={onDelete} onKeyPress={onDelete}
>
<Button
{...deleteButtonProps}
className={twMerge(
"h-8 px-4 text-sm text-white bg-[#EF4444] rounded-lg border border-[#EF4444] focus:border-black/30 dark:focus:border-white/50 transition",
deleteButtonProps?.className as string
)}
onClick={onDelete}
onKeyDown={(event) => {
handleEnter(event, onDelete);
}}
> >
<Button {t("deleteDialog.button.delete")}
{...deleteButtonProps} </Button>
className={twMerge( </VisibleKey>
"h-8 px-4 text-sm text-white bg-[#EF4444] rounded-lg border border-[#EF4444] focus:border-black/30 dark:focus:border-white/50 transition", </div>
deleteButtonProps?.className as string </DialogContent>
)}
onClick={onDelete}
onKeyDown={(event) => {
handleEnter(event, onDelete);
}}
>
{t("deleteDialog.button.delete")}
</Button>
</VisibleKey>
</div>
</DialogPanel>
</div>
</Dialog> </Dialog>
); );
}; };

View File

@@ -1,10 +1,11 @@
import { import {
Button,
Description,
Dialog, Dialog,
DialogPanel, DialogContent,
DialogHeader,
DialogTitle, DialogTitle,
} from "@headlessui/react"; DialogDescription,
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import VisibleKey from "@/components/Common/VisibleKey"; import VisibleKey from "@/components/Common/VisibleKey";
@@ -36,69 +37,63 @@ const DeleteDialog = ({
}; };
return ( return (
<Dialog <Dialog open={isOpen} onOpenChange={setIsOpen}>
open={isOpen} <DialogContent className="flex flex-col justify-between w-[360px] h-40 p-3 text-[#333] dark:text-white/90 border border-[#e6e6e6] bg-white dark:bg-[#202126] dark:border-white/10 shadow-xl rounded-lg">
onClose={() => setIsOpen(false)} <DialogHeader className="mb-2">
className="relative z-1000" <DialogTitle className="text-base font-bold">
> {t("history_list.delete_modal.title")}
<div </DialogTitle>
id="headlessui-popover-panel:delete-history" <DialogDescription className="text-sm">
className="fixed inset-0 flex items-center justify-center w-screen" {t("history_list.delete_modal.description", {
> replace: [
<DialogPanel className="flex flex-col justify-between w-[360px] h-[160px] p-3 text-[#333] dark:text-white/90 border border-[#e6e6e6] bg-white dark:bg-[#202126] dark:border-white/10 shadow-xl rounded-lg"> active?._source?.title ||
<div className="flex flex-col gap-3"> active?._source?.message ||
<DialogTitle className="text-base font-bold"> active?._id,
{t("history_list.delete_modal.title")} ],
</DialogTitle> })}
<Description className="text-sm"> </DialogDescription>
{t("history_list.delete_modal.description", { </DialogHeader>
replace: [
active?._source?.title ||
active?._source?.message ||
active?._id,
],
})}
</Description>
</div>
<div className="flex gap-4 self-end"> <div className="flex gap-4 self-end">
<VisibleKey <VisibleKey
shortcut="N" shortcut="N"
shortcutClassName="left-[unset] right-0" shortcutClassName="left-[unset] right-0"
onKeyPress={() => setIsOpen(false)} onKeyPress={() => {
setIsOpen(false);
}}
>
<Button
variant="outline"
autoFocus
onClick={() => setIsOpen(false)}
onKeyDown={(event) => {
handleEnter(event, () => {
setIsOpen(false);
});
}}
> >
<Button {t("history_list.delete_modal.button.cancel")}
autoFocus </Button>
className="h-8 px-4 text-sm text-[#666666] bg-[#F8F9FA] dark:text-white dark:bg-[#202126] border border-[#E6E6E6] dark:border-white/10 rounded-lg focus:border-black/30 dark:focus:border-white/50 transition" </VisibleKey>
onClick={() => setIsOpen(false)}
onKeyDown={(event) => {
handleEnter(event, () => {
setIsOpen(false);
});
}}
>
{t("history_list.delete_modal.button.cancel")}
</Button>
</VisibleKey>
<VisibleKey <VisibleKey
shortcut="Y" shortcut="Y"
shortcutClassName="left-[unset] right-0" shortcutClassName="left-[unset] right-0"
onKeyPress={handleRemove} onKeyPress={handleRemove}
>
<Button
variant="destructive"
className="text-white"
onClick={handleRemove}
onKeyDown={(event) => {
handleEnter(event, handleRemove);
}}
> >
<Button {t("history_list.delete_modal.button.delete")}
className="h-8 px-4 text-sm text-white bg-[#EF4444] rounded-lg border border-[#EF4444] focus:border-black/30 dark:focus:border-white/50 transition" </Button>
onClick={handleRemove} </VisibleKey>
onKeyDown={(event) => { </div>
handleEnter(event, handleRemove); </DialogContent>
}}
>
{t("history_list.delete_modal.button.delete")}
</Button>
</VisibleKey>
</div>
</DialogPanel>
</div>
</Dialog> </Dialog>
); );
}; };

View File

@@ -113,7 +113,8 @@ const HistoryListContent: FC<HistoryListContentProps> = ({
const scrollToElement = useCallback( const scrollToElement = useCallback(
(elementId: string, isKeyboardNav: boolean) => { (elementId: string, isKeyboardNav: boolean) => {
if (!listRef.current) return; if (!listRef.current) return;
if (typeof window === 'undefined' || typeof document === 'undefined') return; if (typeof window === "undefined" || typeof document === "undefined")
return;
const element = listRef.current.querySelector(`#${elementId}`); const element = listRef.current.querySelector(`#${elementId}`);
if (!element) return; if (!element) return;
@@ -123,7 +124,7 @@ const HistoryListContent: FC<HistoryListContentProps> = ({
const isVisible = const isVisible =
rect.top >= 0 && rect.top >= 0 &&
rect.bottom <= rect.bottom <=
(window.innerHeight || document.documentElement.clientHeight); (window.innerHeight || document.documentElement.clientHeight);
// Only scroll if element is not visible // Only scroll if element is not visible
if (!isVisible) { if (!isVisible) {

View File

@@ -1,10 +1,16 @@
import { FC, useRef, useCallback, useState } from "react"; import { FC, useRef, useCallback, useState, useEffect } from "react";
import { Input, Popover, PopoverButton, PopoverPanel } from "@headlessui/react";
import { Ellipsis } from "lucide-react"; import { Ellipsis } from "lucide-react";
import clsx from "clsx"; import clsx from "clsx";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Pencil, Trash2 } from "lucide-react"; import { Pencil, Trash2 } from "lucide-react";
import {
Popover,
PopoverTrigger,
PopoverContent,
PopoverPortal,
} from "@/components/ui/popover";
import { Input } from "@/components/ui/input";
import type { Chat } from "@/types/chat"; import type { Chat } from "@/types/chat";
import VisibleKey from "../VisibleKey"; import VisibleKey from "../VisibleKey";
@@ -31,9 +37,11 @@ const HistoryListItem: FC<HistoryListItemProps> = ({
const moreButtonRef = useRef<HTMLButtonElement>(null); const moreButtonRef = useRef<HTMLButtonElement>(null);
const { _id, _source } = item; const { _id, _source } = item;
const title = _source?.title ?? _id; const title = _source?.title ?? _id;
const isActive = item._id === active?._id || item._id === highlightId; const isSelected = item._id === active?._id;
const isHovered = item._id === highlightId;
const [isEdit, setIsEdit] = useState(false); const [isEdit, setIsEdit] = useState(false);
const [open, setOpen] = useState(false);
const onContextMenu = useCallback( const onContextMenu = useCallback(
(e: React.MouseEvent) => { (e: React.MouseEvent) => {
@@ -72,24 +80,34 @@ const HistoryListItem: FC<HistoryListItemProps> = ({
}, },
]; ];
useEffect(() => {
if (!(isSelected || isHovered) || isEdit) {
setOpen(false);
}
}, [isSelected, isHovered, isEdit]);
return ( return (
<li <li
key={_id} key={_id}
id={_id} id={_id}
className={clsx( className={clsx(
"flex items-center mt-1 h-10 rounded-lg cursor-pointer hover:bg-[#EDEDED] dark:hover:bg-[#353F4D] transition", "group flex w-full items-center mt-1 h-10 rounded-lg cursor-pointer hover:bg-[#EDEDED] dark:hover:bg-[#353F4D] transition-colors",
{ {
"!bg-[#E5E7EB] dark:!bg-[#2B3444]": isActive, "bg-[#E5E7EB] dark:bg-[#2B3444]": isSelected,
"bg-[#EDEDED] dark:bg-[#353F4D]": isHovered && !isSelected,
} }
)} )}
onClick={() => { onClick={() => {
if (!isActive) { if (!isSelected) {
setIsEdit(false); setIsEdit(false);
} }
onSelect(item); onSelect(item);
}} }}
onMouseEnter={onMouseEnter} onMouseEnter={onMouseEnter}
onMouseLeave={() => {
setOpen(false);
}}
onContextMenu={onContextMenu} onContextMenu={onContextMenu}
> >
<div <div
@@ -99,11 +117,11 @@ const HistoryListItem: FC<HistoryListItemProps> = ({
/> />
<div className="flex-1 flex items-center justify-between gap-2 px-2 overflow-hidden"> <div className="flex-1 flex items-center justify-between gap-2 px-2 overflow-hidden">
{isEdit && isActive ? ( {isEdit && isSelected ? (
<Input <Input
autoFocus autoFocus
defaultValue={title} defaultValue={title}
className="flex-1 -mx-px outline-none bg-transparent border border-[#0061FF] rounded-[4px]" className="flex-1 -mx-px outline-none bg-transparent border border-[#0061FF] rounded-sm"
onKeyDown={(event) => { onKeyDown={(event) => {
if (event.key !== "Enter") return; if (event.key !== "Enter") return;
@@ -128,7 +146,7 @@ const HistoryListItem: FC<HistoryListItemProps> = ({
)} )}
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{isActive && !isEdit && ( {!isEdit && isSelected && (
<VisibleKey <VisibleKey
shortcut="↑↓" shortcut="↑↓"
rootClassName="w-6" rootClassName="w-6"
@@ -136,56 +154,73 @@ const HistoryListItem: FC<HistoryListItemProps> = ({
/> />
)} )}
<Popover> <Popover open={open} onOpenChange={setOpen}>
{isActive && !isEdit && ( <PopoverTrigger
<PopoverButton ref={moreButtonRef} className="flex gap-2"> ref={moreButtonRef}
<VisibleKey className={clsx("flex gap-2", {
shortcut="O" "opacity-100 pointer-events-auto":
onKeyPress={() => { !isEdit && (isSelected || isHovered),
moreButtonRef.current?.click(); "opacity-0 pointer-events-none": !(
}} !isEdit &&
> (isSelected || isHovered)
<Ellipsis className="size-4 text-[#979797]" /> ),
</VisibleKey> })}
</PopoverButton> onClick={(e) => {
)} e.stopPropagation();
setOpen((prev) => !prev);
<PopoverPanel
anchor="bottom"
className="flex flex-col rounded-lg shadow-md z-100 bg-white dark:bg-[#202126] p-1 border border-black/2 dark:border-white/10"
onClick={(event) => {
event.stopPropagation();
}} }}
> >
{menuItems.map((menuItem) => { <VisibleKey
const { shortcut="O"
label, onKeyPress={() => {
icon: Icon, moreButtonRef.current?.click();
shortcut, }}
iconColor, >
onClick, <Ellipsis className="size-4 text-[#979797]" />
} = menuItem; </VisibleKey>
</PopoverTrigger>
return ( <PopoverPortal>
<button <PopoverContent
key={label} side="bottom"
className="flex items-center gap-2 px-3 py-2 text-sm rounded-[6px] hover:bg-[#EDEDED] dark:hover:bg-[#2B2C31] transition" className="flex flex-col rounded-lg shadow-md z-100 bg-white dark:bg-[#202126] p-1 border border-black/2 dark:border-white/10"
onClick={onClick} onClick={(event) => {
> event.stopPropagation();
<VisibleKey shortcut={shortcut} onKeyPress={onClick}> }}
<Icon onMouseLeave={() => {
className="size-4" setOpen(false);
style={{ }}
color: iconColor, >
}} {menuItems.map((menuItem) => {
/> const {
</VisibleKey> label,
icon: Icon,
shortcut,
iconColor,
onClick,
} = menuItem;
<span>{t(label)}</span> return (
</button> <button
); key={label}
})} className="flex items-center gap-2 px-3 py-2 text-sm rounded-md hover:bg-[#EDEDED] dark:hover:bg-[#2B2C31] transition"
</PopoverPanel> onClick={onClick}
>
<VisibleKey shortcut={shortcut} onKeyPress={onClick}>
<Icon
className="size-4"
style={{
color: iconColor,
}}
/>
</VisibleKey>
<span>{t(label)}</span>
</button>
);
})}
</PopoverContent>
</PopoverPortal>
</Popover> </Popover>
</div> </div>
</div> </div>

View File

@@ -1,4 +1,4 @@
import { Input } from "@headlessui/react"; import { Input } from "@/components/ui/input";
import { debounce } from "lodash-es"; import { debounce } from "lodash-es";
import { FC, useMemo, useRef, useState } from "react"; import { FC, useMemo, useRef, useState } from "react";
import clsx from "clsx"; import clsx from "clsx";
@@ -9,6 +9,7 @@ import VisibleKey from "../VisibleKey";
import { Chat } from "@/types/chat"; import { Chat } from "@/types/chat";
import { closeHistoryPanel } from "@/utils"; import { closeHistoryPanel } from "@/utils";
import HistoryListContent from "./HistoryListContent"; import HistoryListContent from "./HistoryListContent";
import { Button } from "@/components/ui/button";
interface HistoryListProps { interface HistoryListProps {
historyPanelId?: string; historyPanelId?: string;
@@ -57,21 +58,21 @@ const HistoryList: FC<HistoryListProps> = (props) => {
"flex flex-col h-screen text-sm bg-[#F3F4F6] dark:bg-[#1F2937]" "flex flex-col h-screen text-sm bg-[#F3F4F6] dark:bg-[#1F2937]"
)} )}
> >
<div className="flex gap-1 p-2 border-b dark:border-[#343D4D]"> <div className="flex gap-1 p-2 border-b border-input">
<div className="flex-1 flex items-center gap-2 px-2 rounded-lg border transition border-[#E6E6E6] bg-[#F8F9FA] dark:bg-[#2B3444] dark:border-[#343D4D] focus-within:border-[#0061FF]"> <div className="flex-1 h-8 flex items-center px-2 rounded-lg border border-input bg-background transition focus-within:ring-2 focus-within:ring-ring focus-within:ring-offset-2 focus-within:ring-offset-background">
<VisibleKey <VisibleKey
shortcut="F" shortcut="F"
onKeyPress={() => { onKeyPress={() => {
searchInputRef.current?.focus(); searchInputRef.current?.focus();
}} }}
> >
<Search className="size-4 text-[#6B7280]" /> <Search className="size-4 text-muted-foreground" />
</VisibleKey> </VisibleKey>
<Input <Input
autoFocus autoFocus
ref={searchInputRef} ref={searchInputRef}
className="w-full bg-transparent outline-none" className="w-full h-8 bg-transparent border-0 shadow-none focus-visible:ring-0 focus-visible:ring-offset-0"
placeholder={t("history_list.search.placeholder")} placeholder={t("history_list.search.placeholder")}
onChange={(event) => { onChange={(event) => {
debouncedSearch(event.target.value); debouncedSearch(event.target.value);
@@ -79,18 +80,20 @@ const HistoryList: FC<HistoryListProps> = (props) => {
/> />
</div> </div>
<div <Button
className="size-8 flex items-center justify-center rounded-lg border text-[#0072FF] border-[#E6E6E6] bg-[#F3F4F6] dark:border-[#343D4D] dark:bg-[#1F2937] hover:bg-[#F8F9FA] dark:hover:bg-[#353F4D] cursor-pointer transition" variant="outline"
size="icon"
className="size-8"
onClick={handleRefresh} onClick={handleRefresh}
> >
<VisibleKey shortcut="R" onKeyPress={handleRefresh}> <VisibleKey shortcut="R" onKeyPress={handleRefresh}>
<RefreshCcw <RefreshCcw
className={clsx("size-4", { className={clsx("size-4 text-[#0287FF]", {
"animate-spin": isRefresh, "animate-spin": isRefresh,
})} })}
/> />
</VisibleKey> </VisibleKey>
</div> </Button>
</div> </div>
<div className="flex-1 px-2 overflow-auto custom-scrollbar"> <div className="flex-1 px-2 overflow-auto custom-scrollbar">
@@ -104,10 +107,10 @@ const HistoryList: FC<HistoryListProps> = (props) => {
</div> </div>
{historyPanelId && ( {historyPanelId && (
<div className="flex justify-end p-2 border-t dark:border-[#343D4D]"> <div className="flex justify-end p-2 border-t border-input">
<VisibleKey shortcut="Esc" shortcutClassName="w-7"> <VisibleKey shortcut="Esc" shortcutClassName="w-7">
<PanelLeftClose <PanelLeftClose
className="size-4 text-black/80 dark:text-white/80 cursor-pointer" className="size-4 text-muted-foreground cursor-pointer"
onClick={closeHistoryPanel} onClick={closeHistoryPanel}
/> />
</VisibleKey> </VisibleKey>

View File

@@ -41,7 +41,7 @@ function UniversalIcon({
icon, icon,
defaultIcon = File, defaultIcon = File,
appIcon = false, appIcon = false,
className = "w-5 h-5 flex-shrink-0", className = "w-5 h-5 shrink-0",
onClick = () => {}, onClick = () => {},
wrapWithIconWrapper = true, wrapWithIconWrapper = true,
}: UniversalIconProps) { }: UniversalIconProps) {

View File

@@ -1,6 +1,7 @@
import { ChevronLeft, ChevronRight } from "lucide-react"; import { ChevronLeft, ChevronRight } from "lucide-react";
import VisibleKey from "./VisibleKey"; import VisibleKey from "./VisibleKey";
import { cn } from "@/lib/utils";
interface PaginationProps { interface PaginationProps {
current: number; current: number;
@@ -19,10 +20,15 @@ function Pagination({
}: PaginationProps) { }: PaginationProps) {
return ( return (
<div <div
className={`flex items-center justify-between h-8 px-3 text-[#999] border-t dark:border-t-white/10 ${className}`} className={`flex items-center justify-between h-8 px-2 text-muted-foreground border-t border-input ${className}`}
> >
<VisibleKey shortcut="leftarrow" onKeyPress={onPrev}> <VisibleKey shortcut="leftarrow" onKeyPress={onPrev}>
<ChevronLeft className="size-4 cursor-pointer" onClick={onPrev} /> <ChevronLeft
className={cn("size-4 cursor-pointer", {
"cursor-not-allowed opacity-50": current === 1,
})}
onClick={onPrev}
/>
</VisibleKey> </VisibleKey>
<div className="text-xs"> <div className="text-xs">
@@ -30,7 +36,12 @@ function Pagination({
</div> </div>
<VisibleKey shortcut="rightarrow" onKeyPress={onNext}> <VisibleKey shortcut="rightarrow" onKeyPress={onNext}>
<ChevronRight className="size-4 cursor-pointer" onClick={onNext} /> <ChevronRight
className={cn("size-4 cursor-pointer", {
"cursor-not-allowed opacity-50": current === totalPage,
})}
onClick={onNext}
/>
</VisibleKey> </VisibleKey>
</div> </div>
); );

View File

@@ -1,4 +1,5 @@
import { Input, InputProps } from "@headlessui/react"; import type { InputProps } from "@/components/ui/input";
import { Input } from "@/components/ui/input";
import { useKeyPress } from "ahooks"; import { useKeyPress } from "ahooks";
import { forwardRef, useImperativeHandle, useRef } from "react"; import { forwardRef, useImperativeHandle, useRef } from "react";
@@ -29,7 +30,7 @@ const PopoverInput = forwardRef<HTMLInputElement, InputProps>((props, ref) => {
} }
); );
return <Input autoCorrect="off" ref={inputRef} {...props} />; return <Input autoCorrect="off" ref={inputRef} {...(props as any)} />;
}); });
export default PopoverInput; export default PopoverInput;

View File

@@ -1,20 +1,20 @@
import { RefObject } from "react"; import { RefObject } from "react";
import clsx from "clsx"; import clsx from "clsx";
import { ArrowDown } from "lucide-react"; import { ArrowDown } from "lucide-react";
import { Button } from "../ui/button";
interface ScrollToBottomProps { interface ScrollToBottomProps {
scrollRef: RefObject<HTMLDivElement>; scrollRef: RefObject<HTMLDivElement>;
isAtBottom: boolean; isAtBottom: boolean;
} }
const ScrollToBottom = ({ const ScrollToBottom = ({ scrollRef, isAtBottom }: ScrollToBottomProps) => {
scrollRef,
isAtBottom,
}: ScrollToBottomProps) => {
return ( return (
<button <Button
size="icon"
variant="outline"
className={clsx( className={clsx(
"absolute right-4 bottom-4 flex items-center justify-center size-8 border bg-white rounded-full shadow dark:border-[#272828] dark:bg-black dark:shadow-white/15", "absolute right-4 bottom-4 border border-border rounded-full shadow dark:shadow-white/15",
{ {
hidden: isAtBottom, hidden: isAtBottom,
} }
@@ -27,7 +27,7 @@ const ScrollToBottom = ({
}} }}
> >
<ArrowDown className="size-5" /> <ArrowDown className="size-5" />
</button> </Button>
); );
}; };

View File

@@ -1,40 +1,38 @@
import { import { FC, ReactNode } from "react";
Popover,
PopoverButton,
PopoverPanel,
PopoverPanelProps,
} from "@headlessui/react";
import { useBoolean } from "ahooks"; import { useBoolean } from "ahooks";
import clsx from "clsx"; import clsx from "clsx";
import { FC, ReactNode } from "react"; import {
Popover,
interface Tooltip2Props extends PopoverPanelProps { PopoverTrigger,
PopoverContent,
} from "@/components/ui/popover";
interface Tooltip2Props {
content: string; content: string;
children: ReactNode; children: ReactNode;
className?: string;
} }
const Tooltip2: FC<Tooltip2Props> = (props) => { const Tooltip2: FC<Tooltip2Props> = (props) => {
const { content, children, anchor = "top", ...rest } = props; const { content, children, className } = props;
const [visible, { setTrue, setFalse }] = useBoolean(false); const [visible, { setTrue, setFalse }] = useBoolean(false);
return ( return (
<Popover> <Popover>
<PopoverButton onMouseOver={setTrue} onMouseOut={setFalse}> <PopoverTrigger onMouseOver={setTrue} onMouseOut={setFalse}>
{children} {children}
</PopoverButton> </PopoverTrigger>
<PopoverPanel <PopoverContent
{...rest} side="top"
static
anchor={anchor}
className={clsx( className={clsx(
"fixed z-1000 p-2 rounded-[6px] text-xs text-white bg-black/75 hidden", "z-1000 p-2 rounded-md text-xs text-white bg-black/75 hidden",
{ {
"!block": visible, block: visible,
} },
className
)} )}
> >
{content} {content}
</PopoverPanel> </PopoverContent>
</Popover> </Popover>
); );
}; };

View File

@@ -71,7 +71,7 @@ export default function Footer({ setIsPinnedWeb }: FooterProps) {
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<img <img
src={selectedExtension.icon} src={selectedExtension.icon}
className="size-5 dark:drop-shadow-[0_0_6px_rgb(255,255,255)]" className="h-5 w-5 rounded-full dark:drop-shadow-[0_0_6px_rgb(255,255,255)]"
/> />
<span className="text-sm">{selectedExtension.name}</span> <span className="text-sm">{selectedExtension.name}</span>
</div> </div>
@@ -81,7 +81,7 @@ export default function Footer({ setIsPinnedWeb }: FooterProps) {
if (visibleExtensionStore) { if (visibleExtensionStore) {
return ( return (
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<FontIcon name="font_Store" className="size-5" /> <FontIcon name="font_Store" className="h-5 w-5" />
<span className="text-sm">Extension Store</span> <span className="text-sm">Extension Store</span>
</div> </div>
); );
@@ -100,7 +100,7 @@ export default function Footer({ setIsPinnedWeb }: FooterProps) {
{hasUpdate ? ( {hasUpdate ? (
<div className="cursor-pointer" onClick={() => setVisible(true)}> <div className="cursor-pointer" onClick={() => setVisible(true)}>
<span>{t("search.footer.updateAvailable")}</span> <span>{t("search.footer.updateAvailable")}</span>
<span className="absolute top-0 -right-2 size-1.5 bg-[#FF3434] rounded-full"></span> <span className="absolute top-0 -right-2 h-1.5 w-1.5 bg-[#FF3434] rounded-full"></span>
</div> </div>
) : ( ) : (
sourceData?.source?.name || sourceData?.source?.name ||
@@ -117,7 +117,7 @@ export default function Footer({ setIsPinnedWeb }: FooterProps) {
<div <div
data-tauri-drag-region={isTauri} data-tauri-drag-region={isTauri}
className={clsx( className={clsx(
"px-4 z-999 mx-[1px] h-8 absolute bottom-0 left-0 right-0 border-t border-gray-200 dark:border-gray-700 flex items-center justify-between rounded-[6px] rounded-t-none", "px-4 z-999 mx-px h-8 absolute bottom-0 left-0 right-0 border-t! border-gray-200 dark:border-gray-700 flex items-center justify-between rounded-md rounded-t-none",
{ {
"overflow-hidden": isTauri, "overflow-hidden": isTauri,
} }
@@ -137,7 +137,7 @@ export default function Footer({ setIsPinnedWeb }: FooterProps) {
</div> </div>
</div> </div>
) : ( ) : (
<WebLogin panelClassName="bottom-5 left-0" /> <WebLogin side="top" align="start" />
)} )}
<div className={`flex mobile:hidden items-center gap-3`}> <div className={`flex mobile:hidden items-center gap-3`}>

View File

@@ -37,7 +37,7 @@ export const NoResults = () => {
<div className="flex gap-2"> <div className="flex gap-2">
<WebLoginButton /> <WebLoginButton />
<WebRefreshButton className="size-8" /> <WebRefreshButton />
</div> </div>
</div> </div>
); );
@@ -54,7 +54,7 @@ export const NoResults = () => {
<span <span
className={clsx( className={clsx(
"ml-3 h-5 min-w-5 rounded-[6px] border border-[#D8D8D8] flex justify-center items-center", "ml-3 h-5 min-w-5 rounded-md border border-[#D8D8D8] flex justify-center items-center",
{ {
"px-1": !isMac, "px-1": !isMac,
} }
@@ -63,7 +63,7 @@ export const NoResults = () => {
{formatKey(modifierKey)} {formatKey(modifierKey)}
</span> </span>
<span className="ml-1 w-5 h-5 rounded-[6px] border border-[#D8D8D8] flex justify-center items-center"> <span className="ml-1 w-5 h-5 rounded-md border border-[#D8D8D8] flex justify-center items-center">
{modeSwitch} {modeSwitch}
</span> </span>
</div> </div>

View File

@@ -1,13 +1,16 @@
import { Menu, MenuButton } from "@headlessui/react"; import {
DropdownMenu,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import logoImg from "@/assets/icon.svg"; import logoImg from "@/assets/icon.svg";
const Footer = () => { const Footer = () => {
return ( return (
<div className="fixed bottom-0 left-0 right-0 bg-white dark:bg-gray-800 border-t dark:border-gray-700"> <div className="fixed bottom-0 left-0 right-0 bg-white dark:bg-gray-800 border-t border-gray-200 dark:border-white/10">
<div className="max-w-6xl mx-auto px-4 h-8 flex items-center justify-between"> <div className="max-w-6xl mx-auto px-4 h-8 flex items-center justify-between">
<Menu as="div" className="relative"> <DropdownMenu>
<MenuButton className="h-7 flex items-center space-x-2 px-1 py-1.5 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors"> <DropdownMenuTrigger className="h-7 flex items-center space-x-2 px-1 py-1.5 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors">
<img <img
src={logoImg} src={logoImg}
className="w-5 h-5 text-gray-600 dark:text-gray-400" className="w-5 h-5 text-gray-600 dark:text-gray-400"
@@ -16,7 +19,7 @@ const Footer = () => {
Coco Coco
</span> </span>
{/* <ChevronUp className="w-4 h-4 text-gray-500 dark:text-gray-400" /> */} {/* <ChevronUp className="w-4 h-4 text-gray-500 dark:text-gray-400" /> */}
</MenuButton> </DropdownMenuTrigger>
{/* <MenuItems className="absolute bottom-full mb-2 left-0 w-64 rounded-lg bg-white dark:bg-gray-800 shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none"> {/* <MenuItems className="absolute bottom-full mb-2 left-0 w-64 rounded-lg bg-white dark:bg-gray-800 shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none">
<div className="p-1"> <div className="p-1">
@@ -27,7 +30,7 @@ const Footer = () => {
active active
? "bg-gray-100 dark:bg-gray-700" ? "bg-gray-100 dark:bg-gray-700"
: "text-gray-900 dark:text-gray-100" : "text-gray-900 dark:text-gray-100"
} group flex w-full items-center rounded-[6px] px-3 py-2 text-sm`} } group flex w-full items-center rounded-md px-3 py-2 text-sm`}
> >
<Home className="w-4 h-4 mr-2" /> <Home className="w-4 h-4 mr-2" />
<Link to={`/`}>Home</Link> <Link to={`/`}>Home</Link>
@@ -41,7 +44,7 @@ const Footer = () => {
active active
? "bg-gray-100 dark:bg-gray-700" ? "bg-gray-100 dark:bg-gray-700"
: "text-gray-900 dark:text-gray-100" : "text-gray-900 dark:text-gray-100"
} group flex w-full items-center rounded-[6px] px-3 py-2 text-sm`} } group flex w-full items-center rounded-md px-3 py-2 text-sm`}
> >
<User className="w-4 h-4 mr-2" /> <User className="w-4 h-4 mr-2" />
Profile Profile
@@ -55,7 +58,7 @@ const Footer = () => {
active active
? "bg-gray-100 dark:bg-gray-700" ? "bg-gray-100 dark:bg-gray-700"
: "text-gray-900 dark:text-gray-100" : "text-gray-900 dark:text-gray-100"
} group flex w-full items-center rounded-[6px] px-3 py-2 text-sm`} } group flex w-full items-center rounded-md px-3 py-2 text-sm`}
> >
<Settings className="w-4 h-4 mr-2" /> <Settings className="w-4 h-4 mr-2" />
<Link to={`settings`}>Settings</Link> <Link to={`settings`}>Settings</Link>
@@ -70,7 +73,7 @@ const Footer = () => {
active active
? "bg-gray-100 dark:bg-gray-700" ? "bg-gray-100 dark:bg-gray-700"
: "text-gray-900 dark:text-gray-100" : "text-gray-900 dark:text-gray-100"
} group flex w-full items-center rounded-[6px] px-3 py-2 text-sm`} } group flex w-full items-center rounded-md px-3 py-2 text-sm`}
> >
<LogOut className="w-4 h-4 mr-2" /> <LogOut className="w-4 h-4 mr-2" />
Sign Out Sign Out
@@ -79,7 +82,7 @@ const Footer = () => {
</MenuItem> </MenuItem>
</div> </div>
</MenuItems> */} </MenuItems> */}
</Menu> </DropdownMenu>
<div className="flex items-center space-x-4"> <div className="flex items-center space-x-4">
<span className="text-xs text-gray-500 dark:text-gray-400"> <span className="text-xs text-gray-500 dark:text-gray-400">

View File

@@ -111,7 +111,7 @@ const VisibleKey: FC<VisibleKeyProps> = (props) => {
{showTooltip && visibleShortcut ? ( {showTooltip && visibleShortcut ? (
<div <div
className={clsx( className={clsx(
"size-4 flex items-center justify-center font-normal text-xs text-[#333] leading-[14px] bg-[#ccc] dark:bg-[#6B6B6B] rounded-[6px] shadow-[-6px_0px_6px_2px_#fff] dark:shadow-[-6px_0px_6px_2px_#000] absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2", "size-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] absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2",
shortcutClassName shortcutClassName
)} )}
> >

View File

@@ -40,7 +40,7 @@ const AiOverview: FC<AiSummaryProps> = (props) => {
)} )}
> >
<div <div
className="absolute top-2 right-2 flex items-center justify-center size-[20px] border rounded-[6px] cursor-pointer dark:border-[#282828]" className="absolute top-2 right-2 flex items-center justify-center size-[20px] border rounded-md cursor-pointer dark:border-[#282828]"
onClick={() => { onClick={() => {
setVisible(false); setVisible(false);
}} }}

View File

@@ -1,3 +1,4 @@
import { cn } from "@/lib/utils";
import { useAppStore } from "@/stores/appStore"; import { useAppStore } from "@/stores/appStore";
import { useWebConfigStore } from "@/stores/webConfigStore"; import { useWebConfigStore } from "@/stores/webConfigStore";
import { useBoolean } from "ahooks"; import { useBoolean } from "ahooks";
@@ -37,6 +38,7 @@ const AutoResizeTextarea = forwardRef<
setInput, setInput,
handleKeyDown, handleKeyDown,
chatPlaceholder, chatPlaceholder,
lineCount,
onLineCountChange, onLineCountChange,
firstLineMaxWidth, firstLineMaxWidth,
}, },
@@ -115,7 +117,12 @@ const AutoResizeTextarea = forwardRef<
autoComplete="off" autoComplete="off"
autoCapitalize="none" autoCapitalize="none"
spellCheck="false" spellCheck="false"
className="auto-resize-textarea text-base flex-1 outline-none w-full min-w-[200px] text-[#333] dark:text-[#d8d8d8] placeholder-text-xs placeholder-[#999] dark:placeholder-gray-500 bg-transparent custom-scrollbar resize-none overflow-y-auto" className={cn(
"auto-resize-textarea text-base flex-1 outline-none w-full min-w-[200px] text-[#333] dark:text-[#d8d8d8] placeholder-text-xs placeholder-[#999] dark:placeholder-gray-500 bg-transparent custom-scrollbar resize-none overflow-y-auto",
{
"overflow-y-hidden": lineCount === 1,
}
)}
placeholder={chatPlaceholder || t("search.textarea.placeholder")} placeholder={chatPlaceholder || t("search.textarea.placeholder")}
aria-label={t("search.textarea.ariaLabel")} aria-label={t("search.textarea.ariaLabel")}
value={input} value={input}

View File

@@ -12,7 +12,6 @@ import {
} from "lucide-react"; } from "lucide-react";
import { cloneElement, useEffect, useRef, useState } from "react"; import { cloneElement, useEffect, useRef, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Input } from "@headlessui/react";
import { useOSKeyPress } from "@/hooks/useOSKeyPress"; import { useOSKeyPress } from "@/hooks/useOSKeyPress";
import { useSearchStore } from "@/stores/searchStore"; import { useSearchStore } from "@/stores/searchStore";
@@ -292,9 +291,9 @@ const ContextMenu = ({ formatUrl }: ContextMenuProps) => {
ref={containerRef} ref={containerRef}
id={visibleContextMenu ? CONTEXT_MENU_PANEL_ID : ""} id={visibleContextMenu ? CONTEXT_MENU_PANEL_ID : ""}
className={clsx( className={clsx(
"absolute bottom-[50px] right-[18px] w-[300px] flex flex-col gap-2 scale-0 transition origin-bottom-right text-sm p-3 pb-0 bg-white dark:bg-black rounded-lg shadow-xs border border-[#EDEDED] dark:border-[#272828] shadow-lg dark:shadow-white/15", "absolute bottom-[50px] right-[18px] w-[300px] flex flex-col gap-2 scale-0 transition origin-bottom-right text-sm p-3 pb-0 bg-white dark:bg-black rounded-lg border border-[#EDEDED] dark:border-[#272828] shadow-lg dark:shadow-white/15",
{ {
"!scale-100": visibleContextMenu, "scale-100": visibleContextMenu,
} }
)} )}
> >
@@ -329,12 +328,12 @@ const ContextMenu = ({ formatUrl }: ContextMenuProps) => {
<span style={{ color }}>{name}</span> <span style={{ color }}>{name}</span>
</div> </div>
<div className="flex gap-[4px] text-black/60 dark:text-white/60"> <div className="flex gap-1 text-black/60 dark:text-white/60">
{keys.map((key) => ( {keys.map((key) => (
<kbd <kbd
key={key} key={key}
className={clsx( className={clsx(
"flex justify-center items-center font-sans h-[20px] min-w-[20px] text-[10px] rounded-[6px] border border-[#EDEDED] dark:border-white/10 bg-white dark:bg-[#202126]", "flex justify-center items-center font-sans h-5 min-w-5 text-[10px] rounded-md border border-[#EDEDED] dark:border-white/10 bg-white dark:bg-[#202126]",
{ {
"px-1": key.length > 1, "px-1": key.length > 1,
} }
@@ -363,7 +362,7 @@ const ContextMenu = ({ formatUrl }: ContextMenuProps) => {
searchInputRef.current?.focus(); searchInputRef.current?.focus();
}} }}
> >
<Input <input
ref={searchInputRef} ref={searchInputRef}
autoFocus autoFocus
autoCorrect="off" autoCorrect="off"

View File

@@ -46,8 +46,8 @@ const DropdownListItem = memo(
aria-selected={isSelected} aria-selected={isSelected}
id={`search-item-${currentIndex}`} id={`search-item-${currentIndex}`}
className={clsx("p-2 transition rounded-lg", { className={clsx("p-2 transition rounded-lg", {
"bg-[#EDEDED] dark:bg-[#202126]": isSelected, "bg-muted": isSelected,
"!p-0": isAiOverview, "p-0!": isAiOverview,
})} })}
> >
{isCalculator && <Calculator item={item} isSelected={isSelected} />} {isCalculator && <Calculator item={item} isSelected={isSelected} />}

View File

@@ -1,4 +1,4 @@
import { Button } from "@headlessui/react"; import { Button } from "@/components/ui/button";
import dayjs from "dayjs"; import dayjs from "dayjs";
import { import {
CircleCheck, CircleCheck,

View File

@@ -252,7 +252,7 @@ export default function ChatInput({
replace: [akiAiTooltipPrefix, askAI.name], replace: [akiAiTooltipPrefix, askAI.name],
})} })}
</span> </span>
<div className="flex items-center justify-center px-1 h-[20px] text-xs rounded-[6px] border border-black/10 dark:border-[#545454]"> <div className="flex items-center justify-center px-1 h-5 text-xs rounded-md border border-black/10 dark:border-[#545454]">
{formatKey(modifierKey)} + {formatKey("Enter")} {formatKey(modifierKey)} + {formatKey("Enter")}
</div> </div>
</div> </div>
@@ -276,8 +276,8 @@ export default function ChatInput({
return ( return (
<VisibleKey <VisibleKey
shortcut={returnToInput} shortcut={returnToInput}
rootClassName="flex-1 flex items-center justify-center" rootClassName="flex-1 flex items-center justify-center w-full"
shortcutClassName="!left-0 !translate-x-0" shortcutClassName="!left-auto !right-2 !translate-x-0"
> >
<AutoResizeTextarea <AutoResizeTextarea
ref={textareaRef} ref={textareaRef}
@@ -308,14 +308,14 @@ export default function ChatInput({
<div className={`w-full relative`}> <div className={`w-full relative`}>
<div <div
ref={containerRef} ref={containerRef}
className={`flex items-center dark:text-[#D8D8D8] rounded-[6px] transition-all relative overflow-hidden`} className={`flex items-center dark:text-[#D8D8D8] rounded-md transition-all relative overflow-hidden`}
> >
{lineCount === 1 && renderSearchIcon()} {lineCount === 1 && renderSearchIcon()}
{visibleSearchBar() && ( {visibleSearchBar() && (
<div <div
className={clsx( className={clsx(
"relative w-full p-2 bg-[#ededed] dark:bg-[#202126]", "min-h-10 w-full p-[7px] bg-[#ededed] dark:bg-[#202126]",
{ {
"flex items-center gap-2": lineCount === 1, "flex items-center gap-2": lineCount === 1,
} }

View File

@@ -187,9 +187,9 @@ const InputControls = ({
{source?.type === "deep_think" && source?.config?.visible && ( {source?.type === "deep_think" && source?.config?.visible && (
<button <button
className={clsx( className={clsx(
"flex items-center justify-center gap-1 h-[20px] px-1 rounded-[6px] transition hover:bg-[#EDEDED] dark:hover:bg-[#202126]", "flex items-center justify-center gap-1 h-5 px-1 rounded-md transition hover:bg-[#EDEDED] dark:hover:bg-[#202126] cursor-pointer",
{ {
"!bg-[rgba(0,114,255,0.3)]": isDeepThinkActive, "bg-[rgba(0,114,255,0.3)]!": isDeepThinkActive,
} }
)} )}
onClick={setIsDeepThinkActive} onClick={setIsDeepThinkActive}
@@ -250,7 +250,7 @@ const InputControls = ({
!visibleExtensionStore && ( !visibleExtensionStore && (
<div <div
className={clsx( className={clsx(
"inline-flex items-center gap-1 h-[20px] px-1 rounded-full hover:!text-[#881c94] cursor-pointer transition", "inline-flex items-center gap-1 h-5 px-1 rounded-full hover:text-[#881c94]! cursor-pointer transition",
[ [
enabledAiOverview enabledAiOverview
? "text-[#881c94]" ? "text-[#881c94]"

View File

@@ -2,14 +2,16 @@ import { FC, Fragment, MouseEvent, useEffect, useRef } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { ChevronRight, Plus } from "lucide-react"; import { ChevronRight, Plus } from "lucide-react";
import { import {
Menu, DropdownMenu,
MenuButton, DropdownMenuTrigger,
MenuItem, DropdownMenuContent,
MenuItems, DropdownMenuItem,
} from "@/components/ui/dropdown-menu";
import {
Popover, Popover,
PopoverButton, PopoverTrigger,
PopoverPanel, PopoverContent,
} from "@headlessui/react"; } from "@/components/ui/popover";
import { castArray, find, isNil } from "lodash-es"; import { castArray, find, isNil } from "lodash-es";
import { nanoid } from "nanoid"; import { nanoid } from "nanoid";
import { useCreation, useMount, useReactive } from "ahooks"; import { useCreation, useMount, useReactive } from "ahooks";
@@ -198,8 +200,8 @@ const InputUpload: FC<InputUploadProps> = (props) => {
]); ]);
return ( return (
<Menu> <DropdownMenu>
<MenuButton className="flex items-center justify-center h-[20px] px-1 rounded-[6px] transition hover:bg-[#EDEDED] dark:hover:bg-[#202126]"> <DropdownMenuTrigger className="flex items-center justify-center h-[20px] px-1 rounded-md transition hover:bg-[#EDEDED] dark:hover:bg-[#202126]">
<Tooltip <Tooltip
content={t("search.input.uploadFileHints.tooltip", { content={t("search.input.uploadFileHints.tooltip", {
replace: [ replace: [
@@ -212,32 +214,41 @@ const InputUpload: FC<InputUploadProps> = (props) => {
<Plus className="size-3 scale-[1.3]" /> <Plus className="size-3 scale-[1.3]" />
</VisibleKey> </VisibleKey>
</Tooltip> </Tooltip>
</MenuButton> </DropdownMenuTrigger>
<MenuItems <DropdownMenuContent
anchor="bottom start" side="bottom"
className="p-1 text-sm bg-white dark:bg-[#202126] rounded-lg shadow-xs border border-gray-200 dark:border-gray-700" align="start"
className="p-1 text-sm rounded-lg"
> >
{menuItems.map((item) => { {menuItems.map((item) => {
const { label, children, clickEvent } = item; const { label, children, clickEvent } = item;
return ( return (
<MenuItem key={label}> <DropdownMenuItem
key={label}
onSelect={(e: Event) => {
if (children) e.preventDefault();
}}
className="px-0 py-0"
>
{children ? ( {children ? (
<Popover> <Popover>
<PopoverButton <PopoverTrigger asChild>
className="flex items-center justify-between gap-2 px-3 py-2 hover:bg-black/5 hover:dark:bg-white/5 rounded-lg cursor-pointer" <div
onClick={clickEvent} className="flex items-center justify-between gap-2 px-3 py-2 rounded-lg hover:bg-muted cursor-pointer"
> onClick={clickEvent}
<span>{label}</span> >
<span>{label}</span>
<ChevronRight className="size-4" /> <ChevronRight className="size-4" />
</PopoverButton> </div>
</PopoverTrigger>
<PopoverPanel <PopoverContent
transition side="right"
anchor="right" align="start"
className="p-1 text-sm bg-white dark:bg-[#202126] rounded-lg shadow-xs border border-gray-200 dark:border-gray-700" className="p-1 text-sm rounded-lg"
> >
{children.map((childItem) => { {children.map((childItem) => {
const { groupName, groupItems } = childItem; const { groupName, groupItems } = childItem;
@@ -259,7 +270,7 @@ const InputUpload: FC<InputUploadProps> = (props) => {
return ( return (
<div <div
key={id} key={id}
className="px-3 py-2 hover:bg-black/5 hover:dark:bg-white/5 rounded-lg cursor-pointer" className="px-3 py-2 rounded-lg hover:bg-muted cursor-pointer"
onClick={clickEvent} onClick={clickEvent}
> >
{label} {label}
@@ -269,21 +280,21 @@ const InputUpload: FC<InputUploadProps> = (props) => {
</Fragment> </Fragment>
); );
})} })}
</PopoverPanel> </PopoverContent>
</Popover> </Popover>
) : ( ) : (
<div <div
className="px-3 py-2 hover:bg-black/5 hover:dark:bg-white/5 rounded-lg cursor-pointer" className="px-3 py-2 rounded-lg hover:bg-muted cursor-pointer"
onClick={clickEvent} onClick={clickEvent}
> >
{label} {label}
</div> </div>
)} )}
</MenuItem> </DropdownMenuItem>
); );
})} })}
</MenuItems> </DropdownMenuContent>
</Menu> </DropdownMenu>
); );
}; };

View File

@@ -1,10 +1,14 @@
import { useState, useEffect, useCallback, useRef } from "react"; import { useState, useEffect, useCallback, useRef } from "react";
import { Popover, PopoverButton, PopoverPanel } from "@headlessui/react";
import { ChevronDownIcon, RefreshCw, Layers, Hammer } from "lucide-react"; import { ChevronDownIcon, RefreshCw, Layers, Hammer } from "lucide-react";
import clsx from "clsx"; import clsx from "clsx";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { useDebounce } from "ahooks"; import { useDebounce } from "ahooks";
import {
Popover,
PopoverTrigger,
PopoverContent,
} from "@/components/ui/popover";
import CommonIcon from "@/components/Common/Icons/CommonIcon"; import CommonIcon from "@/components/Common/Icons/CommonIcon";
import { useConnectStore } from "@/stores/connectStore"; import { useConnectStore } from "@/stores/connectStore";
import { useSearchStore } from "@/stores/searchStore"; import { useSearchStore } from "@/stores/searchStore";
@@ -16,6 +20,7 @@ import NoDataImage from "@/components/Common/NoDataImage";
import PopoverInput from "@/components/Common/PopoverInput"; import PopoverInput from "@/components/Common/PopoverInput";
import Pagination from "@/components/Common/Pagination"; import Pagination from "@/components/Common/Pagination";
import { SearchQuery } from "@/utils"; import { SearchQuery } from "@/utils";
import { Button } from "../ui/button";
interface MCPPopoverProps { interface MCPPopoverProps {
mcp_servers: any; mcp_servers: any;
@@ -79,6 +84,7 @@ export default function MCPPopover({
}, [currentService?.id, debouncedKeyword, getMCPByServer]); }, [currentService?.id, debouncedKeyword, getMCPByServer]);
const popoverButtonRef = useRef<HTMLButtonElement>(null); const popoverButtonRef = useRef<HTMLButtonElement>(null);
const [open, setOpen] = useState(false);
const mcpSearch = useShortcutsStore((state) => state.mcpSearch); const mcpSearch = useShortcutsStore((state) => state.mcpSearch);
const mcpSearchScope = useShortcutsStore((state) => { const mcpSearchScope = useShortcutsStore((state) => {
return state.mcpSearchScope; return state.mcpSearchScope;
@@ -166,9 +172,9 @@ export default function MCPPopover({
return ( return (
<div <div
className={clsx( className={clsx(
"flex justify-center items-center gap-1 h-[20px] px-1 rounded-[6px] transition hover:bg-[#EDEDED] dark:hover:bg-[#202126] cursor-pointer", "flex justify-center items-center gap-1 h-5 px-1 rounded-md transition cursor-pointer hover:bg-[#EDEDED] dark:hover:bg-[#202126]",
{ {
"!bg-[rgba(0,114,255,0.3)]": isMCPActive, "bg-[rgba(0,114,255,0.3)]!": isMCPActive,
} }
)} )}
onClick={setIsMCPActive} onClick={setIsMCPActive}
@@ -191,8 +197,14 @@ export default function MCPPopover({
{t("search.input.MCP")} {t("search.input.MCP")}
</span> </span>
<Popover className="relative"> <Popover open={open} onOpenChange={setOpen}>
<PopoverButton ref={popoverButtonRef} className="flex items-center"> <PopoverTrigger
ref={popoverButtonRef}
className="flex items-center"
onClick={(e) => {
e.stopPropagation();
}}
>
<VisibleKey <VisibleKey
shortcut={mcpSearchScope} shortcut={mcpSearchScope}
onKeyPress={() => { onKeyPress={() => {
@@ -200,29 +212,35 @@ export default function MCPPopover({
}} }}
> >
<ChevronDownIcon <ChevronDownIcon
className={clsx("size-3", [ className={clsx("size-3 cursor-pointer", [
isMCPActive isMCPActive
? "text-[#0072FF] dark:text-[#0072FF]" ? "text-[#0072FF] dark:text-[#0072FF]"
: "text-[#333] dark:text-white", : "text-[#333] dark:text-white",
])} ])}
/> />
</VisibleKey> </VisibleKey>
</PopoverButton> </PopoverTrigger>
<PopoverPanel className="absolute z-50 left-0 bottom-6 w-[240px] overflow-y-auto bg-white dark:bg-[#202126] rounded-lg shadow-lg border border-gray-200 dark:border-gray-700"> <PopoverContent
side="top"
align="start"
className="z-50 w-60 overflow-y-auto rounded-lg shadow-lg p-0"
>
<div <div
className="text-sm" className="text-sm"
onClick={(e) => { onClick={(e: React.MouseEvent<HTMLDivElement>) => {
e.stopPropagation(); e.stopPropagation();
}} }}
> >
<div className="p-3"> <div className="p-2">
<div className="flex justify-between"> <div className="flex justify-between">
<span>{t("search.input.searchPopover.title")}</span> <span>{t("search.input.searchPopover.title")}</span>
<div <Button
variant="outline"
size="icon"
className="size-6"
onClick={handleRefresh} onClick={handleRefresh}
className="size-[24px] flex justify-center items-center rounded-lg border border-black/10 dark:border-white/10 cursor-pointer"
> >
<VisibleKey shortcut="R" onKeyPress={handleRefresh}> <VisibleKey shortcut="R" onKeyPress={handleRefresh}>
<RefreshCw <RefreshCw
@@ -231,7 +249,7 @@ export default function MCPPopover({
}`} }`}
/> />
</VisibleKey> </VisibleKey>
</div> </Button>
</div> </div>
<div className="relative h-8 my-2"> <div className="relative h-8 my-2">
@@ -250,7 +268,7 @@ export default function MCPPopover({
value={keyword} value={keyword}
ref={searchInputRef} ref={searchInputRef}
className="size-full px-2 rounded-lg border dark:border-white/10 bg-transparent" className="size-full px-2 rounded-lg border dark:border-white/10 bg-transparent"
onChange={(e) => { onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setKeyword(e.target.value); setKeyword(e.target.value);
}} }}
/> />
@@ -280,7 +298,7 @@ export default function MCPPopover({
> >
<div className="flex items-center gap-2 overflow-hidden"> <div className="flex items-center gap-2 overflow-hidden">
{isAll ? ( {isAll ? (
<Layers className="size-[16px] text-[#0287FF]" /> <Layers className="min-w-4 min-h-4 size-4 text-[#0287FF]" />
) : ( ) : (
<CommonIcon <CommonIcon
item={item} item={item}
@@ -290,7 +308,7 @@ export default function MCPPopover({
"default_icon", "default_icon",
]} ]}
itemIcon={item.icon} itemIcon={item.icon}
className="size-4" className="min-w-4 min-h-4 size-4"
/> />
)} )}
@@ -308,7 +326,7 @@ export default function MCPPopover({
}} }}
/> />
<div className="flex justify-center items-center size-[24px]"> <div className="flex justify-center items-center size-6">
<Checkbox <Checkbox
checked={isChecked()} checked={isChecked()}
indeterminate={isAll} indeterminate={isAll}
@@ -339,7 +357,7 @@ export default function MCPPopover({
/> />
)} )}
</div> </div>
</PopoverPanel> </PopoverContent>
</Popover> </Popover>
</> </>
)} )}

View File

@@ -1,4 +1,5 @@
import { useEffect, memo, useRef, useCallback, useState } from "react"; import { useEffect, memo, useRef, useCallback, useState } from "react";
import clsx from "clsx";
import DropdownList from "./DropdownList"; import DropdownList from "./DropdownList";
import { SearchResults } from "@/components/Search/SearchResults"; import { SearchResults } from "@/components/Search/SearchResults";
@@ -12,7 +13,6 @@ import ExtensionStore from "./ExtensionStore";
import platformAdapter from "@/utils/platformAdapter"; import platformAdapter from "@/utils/platformAdapter";
import ViewExtension from "./ViewExtension"; import ViewExtension from "./ViewExtension";
import { visibleFooterBar } from "@/utils"; import { visibleFooterBar } from "@/utils";
import clsx from "clsx";
const SearchResultsPanel = memo<{ const SearchResultsPanel = memo<{
input: string; input: string;

View File

@@ -1,15 +1,16 @@
import { useSearchStore } from "@/stores/searchStore"; import { useSearchStore } from "@/stores/searchStore";
import { ChevronLeft, Search } from "lucide-react"; import { ChevronLeft, Search } from "lucide-react";
import { FC } from "react";
import clsx from "clsx";
import FontIcon from "@/components/Common/Icons/FontIcon"; import FontIcon from "@/components/Common/Icons/FontIcon";
import { FC } from "react";
import lightDefaultIcon from "@/assets/images/source_default.png"; import lightDefaultIcon from "@/assets/images/source_default.png";
import darkDefaultIcon from "@/assets/images/source_default_dark.png"; import darkDefaultIcon from "@/assets/images/source_default_dark.png";
import { useThemeStore } from "@/stores/themeStore"; import { useThemeStore } from "@/stores/themeStore";
import platformAdapter from "@/utils/platformAdapter"; import platformAdapter from "@/utils/platformAdapter";
import { navigateBack, visibleSearchBar } from "@/utils"; import { navigateBack, visibleSearchBar } from "@/utils";
import VisibleKey from "../Common/VisibleKey"; import VisibleKey from "../Common/VisibleKey";
import clsx from "clsx"; import { cn } from "@/lib/utils";
interface MultilevelWrapperProps { interface MultilevelWrapperProps {
title?: string; title?: string;
@@ -36,7 +37,7 @@ const MultilevelWrapper: FC<MultilevelWrapperProps> = (props) => {
<div <div
data-tauri-drag-region data-tauri-drag-region
className={clsx( className={clsx(
"flex items-center h-10 gap-1 px-2 border border-[#EDEDED] dark:border-[#202126] rounded-l-lg", "flex items-center h-10 gap-1 px-2 border border-(--border) rounded-l-lg",
{ {
"justify-center": visibleSearchBar(), "justify-center": visibleSearchBar(),
"w-[calc(100vw-16px)] rounded-r-lg": !visibleSearchBar(), "w-[calc(100vw-16px)] rounded-r-lg": !visibleSearchBar(),
@@ -50,7 +51,7 @@ const MultilevelWrapper: FC<MultilevelWrapperProps> = (props) => {
/> />
</VisibleKey> </VisibleKey>
<div className="size-5 [&>*]:size-full">{renderIcon()}</div> <div className="size-5 *:size-full">{renderIcon()}</div>
<span className="text-sm whitespace-nowrap">{title}</span> <span className="text-sm whitespace-nowrap">{title}</span>
</div> </div>
@@ -115,7 +116,14 @@ export default function SearchIcons({
} }
return ( return (
<div className="flex items-center justify-center pl-2 h-10 bg-[#ededed] dark:bg-[#202126]"> <div
className={cn(
"flex items-center justify-center bg-[#ededed] dark:bg-[#202126]",
{
"pl-2 h-10": lineCount === 1,
}
)}
>
<Search className="w-4 h-4 text-[#ccc] dark:text-[#d8d8d8]" /> <Search className="w-4 h-4 text-[#ccc] dark:text-[#d8d8d8]" />
</div> </div>
); );

View File

@@ -1,10 +1,14 @@
import { useState, useEffect, useCallback, useRef } from "react"; import { useState, useEffect, useCallback, useRef } from "react";
import { Popover, PopoverButton, PopoverPanel } from "@headlessui/react";
import { ChevronDownIcon, RefreshCw, Layers, Globe } from "lucide-react"; import { ChevronDownIcon, RefreshCw, Layers, Globe } from "lucide-react";
import clsx from "clsx"; import clsx from "clsx";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { useDebounce } from "ahooks"; import { useDebounce } from "ahooks";
import {
Popover,
PopoverTrigger,
PopoverContent,
} from "@/components/ui/popover";
import CommonIcon from "@/components/Common/Icons/CommonIcon"; import CommonIcon from "@/components/Common/Icons/CommonIcon";
import { useConnectStore } from "@/stores/connectStore"; import { useConnectStore } from "@/stores/connectStore";
import { useSearchStore } from "@/stores/searchStore"; import { useSearchStore } from "@/stores/searchStore";
@@ -15,6 +19,7 @@ import VisibleKey from "@/components/Common/VisibleKey";
import NoDataImage from "@/components/Common/NoDataImage"; import NoDataImage from "@/components/Common/NoDataImage";
import PopoverInput from "@/components/Common/PopoverInput"; import PopoverInput from "@/components/Common/PopoverInput";
import Pagination from "@/components/Common/Pagination"; import Pagination from "@/components/Common/Pagination";
import { Button } from "../ui/button";
interface SearchPopoverProps { interface SearchPopoverProps {
datasource: any; datasource: any;
@@ -85,6 +90,7 @@ export default function SearchPopover({
}, [currentService?.id, debouncedKeyword, getDataSourcesByServer]); }, [currentService?.id, debouncedKeyword, getDataSourcesByServer]);
const popoverButtonRef = useRef<HTMLButtonElement>(null); const popoverButtonRef = useRef<HTMLButtonElement>(null);
const [open, setOpen] = useState(false);
const internetSearch = useShortcutsStore((state) => state.internetSearch); const internetSearch = useShortcutsStore((state) => state.internetSearch);
const internetSearchScope = useShortcutsStore((state) => { const internetSearchScope = useShortcutsStore((state) => {
return state.internetSearchScope; return state.internetSearchScope;
@@ -172,9 +178,9 @@ export default function SearchPopover({
return ( return (
<div <div
className={clsx( className={clsx(
"flex justify-center items-center gap-1 h-[20px] px-1 rounded-[6px] transition hover:bg-[#EDEDED] dark:hover:bg-[#202126] cursor-pointer", "flex justify-center items-center gap-1 h-5 px-1 rounded-md transition cursor-pointer hover:bg-[#EDEDED] dark:hover:bg-[#202126]",
{ {
"!bg-[rgba(0,114,255,0.3)]": isSearchActive, "bg-[rgba(0,114,255,0.3)]!": isSearchActive,
} }
)} )}
onClick={setIsSearchActive} onClick={setIsSearchActive}
@@ -199,8 +205,14 @@ export default function SearchPopover({
{t("search.input.search")} {t("search.input.search")}
</span> </span>
<Popover className="relative"> <Popover open={open} onOpenChange={setOpen}>
<PopoverButton ref={popoverButtonRef} className="flex items-center"> <PopoverTrigger
ref={popoverButtonRef}
className="flex items-center"
onClick={(e) => {
e.stopPropagation();
}}
>
<VisibleKey <VisibleKey
shortcut={internetSearchScope} shortcut={internetSearchScope}
onKeyPress={() => { onKeyPress={() => {
@@ -208,29 +220,35 @@ export default function SearchPopover({
}} }}
> >
<ChevronDownIcon <ChevronDownIcon
className={clsx("size-3", [ className={clsx("size-3 cursor-pointer", [
isSearchActive isSearchActive
? "text-[#0072FF] dark:text-[#0072FF]" ? "text-[#0072FF] dark:text-[#0072FF]"
: "text-[#333] dark:text-white", : "text-[#333] dark:text-white",
])} ])}
/> />
</VisibleKey> </VisibleKey>
</PopoverButton> </PopoverTrigger>
<PopoverPanel className="absolute z-50 left-0 bottom-6 w-[240px] overflow-y-auto bg-white dark:bg-[#202126] rounded-lg shadow-lg border border-gray-200 dark:border-gray-700"> <PopoverContent
side="top"
align="start"
className="z-50 w-60 overflow-y-auto rounded-lg shadow-lg p-0"
>
<div <div
className="text-sm" className="text-sm"
onClick={(e) => { onClick={(e: React.MouseEvent<HTMLDivElement>) => {
e.stopPropagation(); e.stopPropagation();
}} }}
> >
<div className="p-3"> <div className="p-2">
<div className="flex justify-between"> <div className="flex justify-between">
<span>{t("search.input.searchPopover.title")}</span> <span>{t("search.input.searchPopover.title")}</span>
<div <Button
variant="outline"
size="icon"
className="size-6"
onClick={handleRefresh} onClick={handleRefresh}
className="size-[24px] flex justify-center items-center rounded-lg border border-black/10 dark:border-white/10 cursor-pointer"
> >
<VisibleKey shortcut="R" onKeyPress={handleRefresh}> <VisibleKey shortcut="R" onKeyPress={handleRefresh}>
<RefreshCw <RefreshCw
@@ -239,7 +257,7 @@ export default function SearchPopover({
}`} }`}
/> />
</VisibleKey> </VisibleKey>
</div> </Button>
</div> </div>
<div className="relative h-8 my-2"> <div className="relative h-8 my-2">
@@ -258,7 +276,7 @@ export default function SearchPopover({
value={keyword} value={keyword}
ref={searchInputRef} ref={searchInputRef}
className="size-full px-2 rounded-lg border dark:border-white/10 bg-transparent" className="size-full px-2 rounded-lg border dark:border-white/10 bg-transparent"
onChange={(e) => { onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setKeyword(e.target.value); setKeyword(e.target.value);
}} }}
/> />
@@ -288,7 +306,7 @@ export default function SearchPopover({
> >
<div className="flex items-center gap-2 overflow-hidden"> <div className="flex items-center gap-2 overflow-hidden">
{isAll ? ( {isAll ? (
<Layers className="size-[16px] text-[#0287FF]" /> <Layers className="size-4 text-[#0287FF]" />
) : ( ) : (
<CommonIcon <CommonIcon
item={item} item={item}
@@ -316,7 +334,7 @@ export default function SearchPopover({
}} }}
/> />
<div className="flex justify-center items-center size-[24px]"> <div className="flex justify-center items-center size-6">
<Checkbox <Checkbox
checked={isChecked()} checked={isChecked()}
indeterminate={isAll} indeterminate={isAll}
@@ -347,7 +365,7 @@ export default function SearchPopover({
/> />
)} )}
</div> </div>
</PopoverPanel> </PopoverContent>
</Popover> </Popover>
</> </>
)} )}

View File

@@ -35,7 +35,10 @@ import {
visibleSearchBar, visibleSearchBar,
} from "@/utils"; } from "@/utils";
import { useTauriFocus } from "@/hooks/useTauriFocus"; import { useTauriFocus } from "@/hooks/useTauriFocus";
import { POPOVER_PANEL_SELECTOR, WINDOW_CENTER_BASELINE_HEIGHT } from "@/constants"; import {
POPOVER_PANEL_SELECTOR,
WINDOW_CENTER_BASELINE_HEIGHT,
} from "@/constants";
import { useChatStore } from "@/stores/chatStore"; import { useChatStore } from "@/stores/chatStore";
import { useSearchStore } from "@/stores/searchStore"; import { useSearchStore } from "@/stores/searchStore";
@@ -383,11 +386,11 @@ function SearchChat({
<div <div
data-tauri-drag-region={isTauri} data-tauri-drag-region={isTauri}
className={clsx( className={clsx(
"m-auto overflow-hidden relative bg-no-repeat bg-white dark:bg-black flex flex-col", "m-auto overflow-hidden relative bg-no-repeat flex flex-col",
[ [
isTransitioned isTransitioned
? "bg-bottom bg-chat_bg_light dark:bg-chat_bg_dark" ? "bg-bottom bg-[url('/assets/chat_bg_light.png')] dark:bg-[url('/assets/chat_bg_dark.png')]"
: "bg-top bg-search_bg_light dark:bg-search_bg_dark", : "bg-top bg-[url('/assets/search_bg_light.png')] dark:bg-[url('/assets/search_bg_dark.png')]",
], ],
{ {
"size-full": !isTauri, "size-full": !isTauri,
@@ -438,7 +441,7 @@ function SearchChat({
{!hideMiddleBorder && ( {!hideMiddleBorder && (
<div <div
className={clsx( className={clsx(
"pointer-events-none absolute left-0 right-0 h-[1px] bg-[#E6E6E6] dark:bg-[#272626]", "pointer-events-none absolute left-0 right-0 h-px bg-[#E6E6E6] dark:bg-[#272626]",
isTransitioned ? "top-0" : "bottom-0" isTransitioned ? "top-0" : "bottom-0"
)} )}
/> />

View File

@@ -6,6 +6,13 @@ import { nanoid } from "nanoid";
import { AssistantFetcher } from "@/components/Assistant/AssistantFetcher"; import { AssistantFetcher } from "@/components/Assistant/AssistantFetcher";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import {
Select,
SelectTrigger,
SelectContent,
SelectItem,
SelectValue,
} from "@/components/ui/select";
import { ButtonConfig } from "./config"; import { ButtonConfig } from "./config";
import { useThemeStore } from "@/stores/themeStore"; import { useThemeStore } from "@/stores/themeStore";
import { useAppStore } from "@/stores/appStore"; import { useAppStore } from "@/stores/appStore";
@@ -169,43 +176,58 @@ export default function AddChatDialog({
<label className="text-sm font-medium text-muted-foreground"> <label className="text-sm font-medium text-muted-foreground">
{t("selection.bind.service")} {t("selection.bind.service")}
</label> </label>
<select <Select
className="h-8 rounded-md px-2 py-1 text-sm bg-white dark:bg-[#0B1220] w-full"
value={serverId} value={serverId}
onChange={(e) => setServerId(e.target.value)} onValueChange={(v) => setServerId(v === "__default__" ? "" : v)}
> >
<option value="" disabled> <SelectTrigger className="h-8 w-full">
{t("selection.bind.defaultService")} <SelectValue className="truncate" placeholder={t("selection.bind.defaultService") as string} />
</option> </SelectTrigger>
{serverList.map((s: any) => ( <SelectContent>
<option key={s.id} value={s.id}> <SelectItem value="__default__" disabled>
{s.name || s.endpoint || s.id} {t("selection.bind.defaultService")}
</option> </SelectItem>
))} {serverList.map((s: any) => (
</select> <SelectItem key={s.id} value={s.id}>
{s.name || s.endpoint || s.id}
</SelectItem>
))}
</SelectContent>
</Select>
</div> </div>
<div className="space-y-1.5"> <div className="space-y-1.5">
<label className="text-sm font-medium text-muted-foreground"> <label className="text-sm font-medium text-muted-foreground">
{t("selection.bind.assistant")} {t("selection.bind.assistant")}
</label> </label>
<select <Select
className="h-8 rounded-md px-2 py-1 text-sm bg-white dark:bg-[#0B1220] w-full"
value={assistantId} value={assistantId}
onChange={(e) => setAssistantId(e.target.value)} onValueChange={(v) => setAssistantId(v === "__default__" ? "" : v)}
disabled={loading || !serverId} disabled={loading || !serverId}
> >
<option value="" disabled> <SelectTrigger className="h-8 w-full">
{loading <SelectValue
? t("common.loading") className="truncate"
: t("selection.bind.defaultAssistant")} placeholder={
</option> (loading
{!loading && ? t("common.loading")
assistantList.map((a: any) => ( : t("selection.bind.defaultAssistant")) as string
<option key={a._id} value={a._id}> }
{a._source?.name || a._id} />
</option> </SelectTrigger>
))} <SelectContent>
</select> {!loading && (
<SelectItem value="__default__">
{t("selection.bind.defaultAssistant")}
</SelectItem>
)}
{!loading &&
assistantList.map((a: any) => (
<SelectItem key={a._id} value={a._id}>
{a._source?.name || a._id}
</SelectItem>
))}
</SelectContent>
</Select>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -7,6 +7,13 @@ import { AssistantFetcher } from "@/components/Assistant/AssistantFetcher";
import { setCurrentWindowService } from "@/commands/windowService"; import { setCurrentWindowService } from "@/commands/windowService";
import { AddChatButton } from "./AddChatButton"; import { AddChatButton } from "./AddChatButton";
import { ButtonConfig, resolveLucideIcon } from "./config"; import { ButtonConfig, resolveLucideIcon } from "./config";
import {
Select,
SelectTrigger,
SelectContent,
SelectItem,
SelectValue,
} from "@/components/ui/select";
const ASSISTANT_CACHE_KEY = "assistant_list_cache"; const ASSISTANT_CACHE_KEY = "assistant_list_cache";
@@ -250,50 +257,58 @@ const ButtonsList = ({ buttons, setButtons, serverList }: ButtonsListProps) => {
<div className="ml-auto flex items-center gap-2"> <div className="ml-auto flex items-center gap-2">
{isChat && ( {isChat && (
<> <>
<select <Select
className="rounded-md px-2 py-1 text-sm bg-white dark:bg-[#0B1220] w-44"
value={btn.action.assistantServerId || ""} value={btn.action.assistantServerId || ""}
onChange={(e) => handleServerSelect(btn, e.target.value)} onValueChange={(v) =>
title={t("selection.bind.service")} handleServerSelect(btn, v === "__default__" ? "" : v)
}
> >
<option value=""> <SelectTrigger className="h-8 w-60">
{t("selection.bind.defaultService")} <SelectValue className="truncate" placeholder={t("selection.bind.defaultService") as string} />
</option> </SelectTrigger>
{serverList.map((s: any) => ( <SelectContent>
<option key={s.id} value={s.id}> <SelectItem value="__default__">
{s.name || s.endpoint || s.id} {t("selection.bind.defaultService")}
</option> </SelectItem>
))} {serverList.map((s: any) => (
</select> <SelectItem key={s.id} value={s.id}>
{s.name || s.endpoint || s.id}
</SelectItem>
))}
</SelectContent>
</Select>
{(() => { {(() => {
const sid = btn.action.assistantServerId; const sid = btn.action.assistantServerId;
const list = (sid && assistantByServer[sid]) || []; const list = (sid && assistantByServer[sid]) || [];
const loading = !!(sid && assistantLoadingByServer[sid]); const loading = !!(sid && assistantLoadingByServer[sid]);
return ( return (
<select <Select
className="rounded-md px-2 py-1 text-sm bg-white dark:bg-[#0B1220] w-44"
value={btn.action.assistantId || ""} value={btn.action.assistantId || ""}
onChange={(e) => onValueChange={(v) =>
handleAssistantSelect(btn, e.target.value) handleAssistantSelect(
btn,
v === "__default__" ? "" : v
)
} }
title={t("selection.bind.assistant")}
disabled={loading} disabled={loading}
> >
<option value=""> <SelectTrigger className="h-8 w-60">
{t("selection.bind.defaultAssistant")} <SelectValue className="truncate" placeholder={t("selection.bind.defaultAssistant") as string} />
</option> </SelectTrigger>
{loading && ( <SelectContent>
<option value="" disabled> {!loading && (
{t("common.loading")} <SelectItem value="__default__">
</option> {t("selection.bind.defaultAssistant")}
)} </SelectItem>
{list.map((a: any) => ( )}
<option key={a._id} value={a._id}> {list.map((a: any) => (
{a._source?.name || a._id} <SelectItem key={a._id} value={a._id}>
</option> {a._source?.name || a._id}
))} </SelectItem>
</select> ))}
</SelectContent>
</Select>
); );
})()} })()}
</> </>

View File

@@ -117,7 +117,7 @@ const SelectionSettings = () => {
<h2 className="text-lg font-semibold">{t("selection.title")}</h2> <h2 className="text-lg font-semibold">{t("selection.title")}</h2>
</div> </div>
<div className="relative rounded-xl p-4 bg-gradient-to-r from-[#E6F0FA] to-[#FFF1F1]"> <div className="relative rounded-xl p-4 bg-linear-to-r from-[#E6F0FA] to-[#FFF1F1] dark:from-[#0B1220] dark:to-[#1A2234] dark:border dark:border-gray-800 dark:shadow-sm transition-colors">
<div className="flex items-center flex-col" aria-hidden="true"> <div className="flex items-center flex-col" aria-hidden="true">
<div className="rounded-xl border border-gray-200 bg-white/70 shadow-sm dark:border-gray-700 dark:bg-gray-900/40"> <div className="rounded-xl border border-gray-200 bg-white/70 shadow-sm dark:border-gray-700 dark:bg-gray-900/40">
<HeaderToolbar <HeaderToolbar
@@ -148,7 +148,7 @@ const SelectionSettings = () => {
</SettingsItem> </SettingsItem>
{selectionEnabled && ( {selectionEnabled && (
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4"> <div className="grid grid-cols-1 gap-4">
<SettingsItem <SettingsItem
icon={Sparkles} icon={Sparkles}
title={t("selection.display.title")} title={t("selection.display.title")}

View File

@@ -1,7 +1,14 @@
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Command, RotateCcw } from "lucide-react"; import { Command, RotateCcw } from "lucide-react";
import {
Select,
SelectTrigger,
SelectContent,
SelectItem,
SelectValue,
} from "@/components/ui/select";
import { useEffect } from "react"; import { useEffect } from "react";
import { Button } from "@headlessui/react"; import { Button } from "@/components/ui/button";
import clsx from "clsx"; import clsx from "clsx";
import { formatKey } from "@/utils/keyboardUtils"; import { formatKey } from "@/utils/keyboardUtils";
@@ -246,21 +253,21 @@ const Shortcuts = () => {
title={t("settings.advanced.shortcuts.modifierKey.title")} title={t("settings.advanced.shortcuts.modifierKey.title")}
description={t("settings.advanced.shortcuts.modifierKey.description")} description={t("settings.advanced.shortcuts.modifierKey.description")}
> >
<select <Select
value={modifierKey} value={modifierKey}
className="px-3 py-1.5 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500" onValueChange={(v) => setModifierKey(v as ModifierKey)}
onChange={(event) => {
setModifierKey(event.target.value as ModifierKey);
}}
> >
{modifierKeys.map((item) => { <SelectTrigger className="h-8 w-40">
return ( <SelectValue />
<option key={item} value={item}> </SelectTrigger>
<SelectContent>
{modifierKeys.map((item) => (
<SelectItem key={item} value={item}>
{formatKey(item)} {formatKey(item)}
</option> </SelectItem>
); ))}
})} </SelectContent>
</select> </Select>
</SettingsItem> </SettingsItem>
{list.map((item) => { {list.map((item) => {
@@ -279,6 +286,7 @@ const Shortcuts = () => {
<span>{formatKey(modifierKey)}</span> <span>{formatKey(modifierKey)}</span>
<span>+</span> <span>+</span>
<SettingsInput <SettingsInput
className="w-20"
value={value} value={value}
max={1} max={1}
onChange={(value) => { onChange={(value) => {
@@ -287,23 +295,14 @@ const Shortcuts = () => {
/> />
<Button <Button
variant="outline"
disabled={disabled} disabled={disabled}
className={clsx( size="icon"
"flex items-center justify-center size-8 rounded-[6px] border border-black/5 dark:border-white/10 transition",
{
"hover:border-[#0072FF]": !disabled,
"opacity-70 cursor-not-allowed": disabled,
}
)}
onClick={() => { onClick={() => {
handleChange(initialValue, setValue); handleChange(initialValue, setValue);
}} }}
> >
<RotateCcw <RotateCcw className={clsx("size-4 opacity-80")} />
className={clsx("size-4 text-[#999]", {
"!text-[#0072FF]": !disabled,
})}
/>
</Button> </Button>
</div> </div>
</SettingsItem> </SettingsItem>

View File

@@ -21,8 +21,15 @@ import SettingsInput from "@/components//Settings/SettingsInput";
import platformAdapter from "@/utils/platformAdapter"; import platformAdapter from "@/utils/platformAdapter";
import UpdateSettings from "./components/UpdateSettings"; import UpdateSettings from "./components/UpdateSettings";
import SettingsToggle from "../SettingsToggle"; import SettingsToggle from "../SettingsToggle";
// import SelectionSettings from "./components/Selection"; import SelectionSettings from "./components/Selection";
// import { isMac } from "@/utils/platform"; import { isMac } from "@/utils/platform";
import {
Select,
SelectTrigger,
SelectContent,
SelectItem,
SelectValue,
} from "@/components/ui/select";
const Advanced = () => { const Advanced = () => {
const { t } = useTranslation(); const { t } = useTranslation();
@@ -169,29 +176,27 @@ const Advanced = () => {
title={t(title)} title={t(title)}
description={t(description)} description={t(description)}
> >
<select <Select value={value as string} onValueChange={(v) => onChange(v as never)}>
value={value} <SelectTrigger className="h-8 w-44">
onChange={(event) => { <SelectValue className="truncate" />
onChange(event.target.value as never); </SelectTrigger>
}} <SelectContent>
className="px-3 py-1.5 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500" {items.map((item) => {
> const { label, value } = item;
{items.map((item) => { return (
const { label, value } = item; <SelectItem key={value} value={value as string}>
{t(label)}
return ( </SelectItem>
<option key={value} value={value}> );
{t(label)} })}
</option> </SelectContent>
); </Select>
})}
</select>
</SettingsItem> </SettingsItem>
); );
})} })}
</div> </div>
{/* {isMac && <SelectionSettings />} */} {isMac && <SelectionSettings />}
<Shortcuts /> <Shortcuts />
@@ -278,33 +283,35 @@ const Advanced = () => {
"settings.advanced.other.localSearchResultWeight.description" "settings.advanced.other.localSearchResultWeight.description"
)} )}
> >
<select <Select
value={localSearchResultWeight} value={String(localSearchResultWeight)}
onChange={(event) => { onValueChange={(v) => {
const weight = Number(event.target.value); const weight = Number(v);
setLocalSearchResultWeight(weight); setLocalSearchResultWeight(weight);
platformAdapter.invokeBackend("set_local_query_source_weight", { platformAdapter.invokeBackend("set_local_query_source_weight", {
value: weight, value: weight,
}); });
}} }}
className="px-3 py-1.5 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
> >
<option value="0.5"> <SelectTrigger className="h-8 w-44">
{t("settings.advanced.other.localSearchResultWeight.options.low")} <SelectValue className="truncate" />
</option> </SelectTrigger>
<option value="1"> <SelectContent>
{t( <SelectItem value="0.5">
"settings.advanced.other.localSearchResultWeight.options.medium" {t("settings.advanced.other.localSearchResultWeight.options.low")}
)} </SelectItem>
</option> <SelectItem value="1">
<option value="2"> {t(
{t( "settings.advanced.other.localSearchResultWeight.options.medium"
"settings.advanced.other.localSearchResultWeight.options.high" )}
)} </SelectItem>
</option> <SelectItem value="2">
</select> {t(
"settings.advanced.other.localSearchResultWeight.options.high"
)}
</SelectItem>
</SelectContent>
</Select>
</SettingsItem> </SettingsItem>
<SettingsItem <SettingsItem

View File

@@ -13,6 +13,7 @@ import Shortcut from "../Shortcut";
import SettingsToggle from "@/components/Settings/SettingsToggle"; import SettingsToggle from "@/components/Settings/SettingsToggle";
import { platform } from "@/utils/platform"; import { platform } from "@/utils/platform";
import { useExtensionsStore } from "@/stores/extensionsStore"; import { useExtensionsStore } from "@/stores/extensionsStore";
import { cn } from "@/lib/utils";
const Content = () => { const Content = () => {
const { rootState } = useContext(ExtensionsContext); const { rootState } = useContext(ExtensionsContext);
@@ -165,7 +166,9 @@ const Item: FC<ItemProps> = (props) => {
<SettingsInput <SettingsInput
defaultValue={alias} defaultValue={alias}
placeholder={t("settings.extensions.hints.addAlias")} placeholder={t("settings.extensions.hints.addAlias")}
className="!w-[90%] !h-6 !border-transparent rounded-[4px]" className={cn(
"w-[90%] h-6 px-1 py-0 border-none rounded-sm shadow-none bg-transparent placeholder:text-[#999]"
)}
onChange={(value) => { onChange={(value) => {
handleChange(String(value)); handleChange(String(value));
}} }}
@@ -292,7 +295,7 @@ const Item: FC<ItemProps> = (props) => {
return ( return (
<> <>
<div <div
className={clsx("-mx-2 px-2 text-sm rounded-[6px]", { className={clsx("-mx-2 px-2 text-sm rounded-md", {
"bg-[#f0f6fe] dark:bg-gray-700": "bg-[#f0f6fe] dark:bg-gray-700":
id === rootState.activeExtension?.id, id === rootState.activeExtension?.id,
})} })}

View File

@@ -72,7 +72,7 @@ const AiOverview = () => {
/> />
<> <>
<div className="mt-6 text-[#333] dark:text-white/90"> <div className="mt-6">
{t("settings.extensions.aiOverview.details.aiOverviewTrigger.title")} {t("settings.extensions.aiOverview.details.aiOverviewTrigger.title")}
</div> </div>
@@ -88,9 +88,7 @@ const AiOverview = () => {
return ( return (
<div> <div>
<div className="mb-2 text-[#666] dark:text-white/70"> <div className="mb-2">{label}</div>
{label}
</div>
<SettingsInput <SettingsInput
type="number" type="number"

View File

@@ -1,4 +1,4 @@
import { Button } from "@headlessui/react"; import { Button } from "@/components/ui/button";
import { useMount } from "ahooks"; import { useMount } from "ahooks";
import { useState } from "react"; import { useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
@@ -35,15 +35,13 @@ const Applications = () => {
return ( return (
<> <>
<div className="text-[#999]"> <p className="mb-2">
<p className="font-bold mb-2"> {t("settings.extensions.application.details.searchScope")}
{t("settings.extensions.application.details.searchScope")} </p>
</p>
<p> <p className="text-[#999]">
{t("settings.extensions.application.details.searchScopeDescription")} {t("settings.extensions.application.details.searchScopeDescription")}
</p> </p>
</div>
<DirectoryScope <DirectoryScope
paths={paths} paths={paths}
@@ -72,18 +70,18 @@ const Applications = () => {
}} }}
/> />
<div className="text-[#999] mt-4"> <p className="mt-4 mb-2">
<p className="font-bold mb-2"> {t("settings.extensions.application.details.rebuildIndex")}
{t("settings.extensions.application.details.rebuildIndex")} </p>
</p>
<p> <p className="text-[#999]">
{t("settings.extensions.application.details.rebuildIndexDescription")} {t("settings.extensions.application.details.rebuildIndexDescription")}
</p> </p>
</div>
<Button <Button
className="w-full h-8 my-4 text-[#0087FF] border border-[#EEF0F3] hover:!border-[#0087FF] dark:border-gray-700 rounded-[6px] transition" variant="outline"
className="w-full my-4"
// className="w-full h-8 my-4 text-[#0087FF] border border-[#EEF0F3] hover:border-[#0087FF] dark:border-gray-700 rounded-md transition"
onClick={handleReindex} onClick={handleReindex}
> >
{t("settings.extensions.application.details.reindex")} {t("settings.extensions.application.details.reindex")}

View File

@@ -1,6 +1,6 @@
import { useAppStore } from "@/stores/appStore"; import { useAppStore } from "@/stores/appStore";
import platformAdapter from "@/utils/platformAdapter"; import platformAdapter from "@/utils/platformAdapter";
import { Button } from "@headlessui/react"; import { Button } from "@/components/ui/button";
import clsx from "clsx"; import clsx from "clsx";
import { castArray } from "lodash-es"; import { castArray } from "lodash-es";
import { Folder, SquareArrowOutUpRight, X } from "lucide-react"; import { Folder, SquareArrowOutUpRight, X } from "lucide-react";
@@ -82,7 +82,7 @@ const DirectoryScope: FC<DirectoryScopeProps> = (props) => {
return ( return (
<div <div
key={item} key={item}
className="flex items-center justify-between gap-2" className="flex items-center justify-between gap-2 text-[#666] dark:text-white/70"
> >
<div className="flex items-center gap-1 flex-1 overflow-hidden"> <div className="flex items-center gap-1 flex-1 overflow-hidden">
<Folder className="size-4" /> <Folder className="size-4" />
@@ -112,7 +112,9 @@ const DirectoryScope: FC<DirectoryScopeProps> = (props) => {
)} )}
<Button <Button
className="w-full h-8 text-[#0087FF] border border-[#EEF0F3] hover:!border-[#0087FF] dark:border-gray-700 rounded-[6px] transition" variant="outline"
className="w-full"
size="sm"
onClick={handleAdd} onClick={handleAdd}
> >
{t("settings.extensions.directoryScope.button.addDirectories")} {t("settings.extensions.directoryScope.button.addDirectories")}

View File

@@ -82,7 +82,7 @@ const FileSearch = () => {
{t("settings.extensions.fileSearch.description")} {t("settings.extensions.fileSearch.description")}
</div> </div>
<div className="mt-4 mb-2 text-[#666] dark:text-white/70"> <div className="mt-4 mb-2">
{t("settings.extensions.fileSearch.label.searchBy")} {t("settings.extensions.fileSearch.label.searchBy")}
</div> </div>
@@ -99,10 +99,7 @@ const FileSearch = () => {
return ( return (
<> <>
<div <div key={label} className="mt-4 mb-2">
key={label}
className="mt-4 mb-2 text-[#666] dark:text-white/70"
>
{label} {label}
</div> </div>
@@ -111,16 +108,16 @@ const FileSearch = () => {
); );
})} })}
<div className="mt-4 mb-2 text-[#666] dark:text-white/70"> <div className="mt-4 mb-2">
{t("settings.extensions.fileSearch.label.searchFileTypes")} {t("settings.extensions.fileSearch.label.searchFileTypes")}
</div> </div>
<div className="flex flex-wrap gap-2 p-2 border rounded-[6px] dark:border-gray-700"> <div className="flex flex-wrap items-center gap-2 p-2 rounded-lg border border-input bg-background hover:border-[#0072FF] focus-within:border-[#0072FF] transition">
{config.file_types.map((item) => { {config.file_types.map((item) => {
return ( return (
<div <div
key={item} key={item}
className="flex items-center gap-1 h-6 px-1 rounded bg-[#f0f0f0] dark:bg-[#444]" className="flex items-center gap-1 h-6 px-2 rounded-full text-xs border border-black/5 dark:border-white/10 bg-black/5 dark:bg-white/10"
> >
<span>{item}</span> <span>{item}</span>
@@ -140,7 +137,7 @@ const FileSearch = () => {
<SettingsInput <SettingsInput
placeholder=".*" placeholder=".*"
className="h-6 border-0 -ml-2" className="h-6 w-24 px-2 border-0 outline-none focus-visible:ring-0 focus-visible:ring-offset-0"
onKeyDown={(event) => { onKeyDown={(event) => {
if (event.code !== "Enter") return; if (event.code !== "Enter") return;

View File

@@ -4,7 +4,13 @@ import { isArray } from "lodash-es";
import { useAsyncEffect, useMount } from "ahooks"; import { useAsyncEffect, useMount } from "ahooks";
import { AssistantFetcher } from "@/components/Assistant/AssistantFetcher"; import { AssistantFetcher } from "@/components/Assistant/AssistantFetcher";
import SettingsSelectPro from "@/components/Settings/SettingsSelectPro"; import {
Select,
SelectTrigger,
SelectContent,
SelectItem,
SelectValue,
} from "@/components/ui/select";
import { useAppStore } from "@/stores/appStore"; import { useAppStore } from "@/stores/appStore";
import { ExtensionId } from "@/components/Settings/Extensions/index"; import { ExtensionId } from "@/components/Settings/Extensions/index";
import { useConnectStore } from "@/stores/connectStore"; import { useConnectStore } from "@/stores/connectStore";
@@ -175,27 +181,40 @@ const SharedAi: FC<SharedAiProps> = (props) => {
<> <>
<div className="text-[#999]">{renderDescription()}</div> <div className="text-[#999]">{renderDescription()}</div>
<div className="mt-6 text-[#333] dark:text-white/90"> <div className="mt-6">
{t("settings.extensions.shardAi.details.linkedAssistant.title")} {t("settings.extensions.shardAi.details.linkedAssistant.title")}
</div> </div>
{selectList.map((item) => { {selectList.map((item) => {
const { label, value, data, searchable, onChange, onSearch } = item; const { label, value, data, searchable, onChange } = item;
return ( return (
<div key={label} className="mt-4"> <div key={label} className="mt-4">
<div className="mb-2 text-[#666] dark:text-white/70">{label}</div> <div className="mb-2 text-[#666] dark:text-white/70">{label}</div>
<SettingsSelectPro <Select
value={value} value={value}
options={data} onValueChange={(v) => onChange?.(v)}
searchable={searchable} disabled={searchable && isLoadingAssistants}
onChange={onChange} >
onSearch={onSearch} <SelectTrigger className="ml-1 h-9 w-full max-w-[480px]">
placeholder={ <SelectValue
isLoadingAssistants && searchable ? "Loading..." : undefined className="truncate"
} placeholder={
/> (searchable && isLoadingAssistants
? (t("common.loading") as string)
: undefined) as string | undefined
}
/>
</SelectTrigger>
<SelectContent>
{data?.map((opt: any) => (
<SelectItem key={opt.id} value={opt.id}>
{opt.name || opt.id}
</SelectItem>
))}
</SelectContent>
</Select>
</div> </div>
); );
})} })}

View File

@@ -9,7 +9,13 @@ import AiOverview from "./AiOverview";
import Calculator from "./Calculator"; import Calculator from "./Calculator";
import FileSearch from "./FileSearch"; import FileSearch from "./FileSearch";
import { Ellipsis, Info } from "lucide-react"; import { Ellipsis, Info } from "lucide-react";
import { Menu, MenuButton, MenuItem, MenuItems } from "@headlessui/react"; import {
DropdownMenu,
DropdownMenuTrigger,
DropdownMenuItem,
DropdownMenuContent,
} from "@/components/ui/dropdown-menu";
import { Button } from "@/components/ui/button";
import platformAdapter from "@/utils/platformAdapter"; import platformAdapter from "@/utils/platformAdapter";
import { useAppStore } from "@/stores/appStore"; import { useAppStore } from "@/stores/appStore";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
@@ -93,58 +99,60 @@ const Details = () => {
}; };
return ( return (
<div className="flex-1 h-full pr-4 pb-4 overflow-auto"> <div className="flex-1 h-full p-4 overflow-auto">
<div className="flex items-start justify-between gap-4 mb-4"> <div className="flex items-start justify-between gap-4 mb-2">
<h2 className="m-0 text-lg font-semibold text-gray-900 dark:text-white"> <h2 className="m-0 text-lg font-semibold text-gray-900 dark:text-white">
{rootState.activeExtension?.name} {rootState.activeExtension?.name}
</h2> </h2>
{rootState.activeExtension?.developer && ( {rootState.activeExtension?.developer && (
<Menu> <DropdownMenu>
<MenuButton className="h-7"> <DropdownMenuTrigger asChild>
<Ellipsis className="size-5 text-[#999]" /> <Button
</MenuButton> variant="ghost"
size="icon"
<MenuItems className="h-7 w-7 p-0 focus-visible:ring-0 focus-visible:ring-offset-0"
anchor="bottom end" >
className="p-1 text-sm bg-white dark:bg-[#202126] rounded-lg shadow-xs border border-gray-200 dark:border-gray-700" <Ellipsis className="h-4 w-4 text-muted-foreground" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent
side="bottom"
align="end"
className="p-1 text-sm rounded-lg"
> >
<MenuItem> <DropdownMenuItem
<div className="px-3 py-2 text-nowrap text-red-500 rounded-lg hover:bg-muted"
className="px-3 py-2 text-nowrap text-red-500 hover:bg-black/5 hover:dark:bg-white/5 rounded-lg cursor-pointer" onSelect={async (e: Event) => {
onClick={async () => { e.preventDefault();
try { try {
const { id, developer } = rootState.activeExtension!; const { id, developer } = rootState.activeExtension!;
await platformAdapter.invokeBackend( await platformAdapter.invokeBackend("uninstall_extension", {
"uninstall_extension", extensionId: id,
{ developer: developer,
extensionId: id, });
developer: developer,
}
);
Object.assign(rootState, { Object.assign(rootState, {
activeExtension: void 0, activeExtension: void 0,
extensions: rootState.extensions.filter((item) => { extensions: rootState.extensions.filter((item) => {
return item.id !== id; return item.id !== id;
}), }),
}); });
addError( addError(
t("settings.extensions.hints.uninstallSuccess"), t("settings.extensions.hints.uninstallSuccess"),
"info" "info"
); );
} catch (error) { } catch (error) {
addError(String(error)); addError(String(error));
} }
}} }}
> >
{t("settings.extensions.hints.uninstall")} {t("settings.extensions.hints.uninstall")}
</div> </DropdownMenuItem>
</MenuItem> </DropdownMenuContent>
</MenuItems> </DropdownMenu>
</Menu>
)} )}
</div> </div>

View File

@@ -3,15 +3,21 @@ import { useReactive } from "ahooks";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import type { LiteralUnion } from "type-fest"; import type { LiteralUnion } from "type-fest";
import { cloneDeep, sortBy } from "lodash-es"; import { cloneDeep, sortBy } from "lodash-es";
import clsx from "clsx";
import { Plus } from "lucide-react"; import { Plus } from "lucide-react";
import { Menu, MenuButton, MenuItem, MenuItems } from "@headlessui/react"; import {
DropdownMenu,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuItem,
} from "@/components/ui/dropdown-menu";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
import platformAdapter from "@/utils/platformAdapter"; import platformAdapter from "@/utils/platformAdapter";
import Content from "./components/Content"; import Content from "./components/Content";
import Details from "./components/Details"; import Details from "./components/Details";
import { useExtensionsStore } from "@/stores/extensionsStore"; import { useExtensionsStore } from "@/stores/extensionsStore";
import SettingsInput from "../SettingsInput";
import { useAppStore } from "@/stores/appStore"; import { useAppStore } from "@/stores/appStore";
import { installExtensionError } from "@/utils"; import { installExtensionError } from "@/utils";
@@ -184,95 +190,88 @@ export const Extensions = () => {
rootState: state, rootState: state,
}} }}
> >
<div className="flex h-[calc(100vh-128px)] -mx-6 gap-4 text-sm"> <div className="flex h-[calc(100vh-128px)] -mx-6 text-sm">
<div className="w-2/3 h-full px-4 border-r dark:border-gray-700 overflow-auto"> <div className="w-2/3 h-full px-4 border-r border-border overflow-auto">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<h2 className="text-lg font-semibold text-gray-900 dark:text-white"> <h2 className="text-lg font-semibold text-gray-900 dark:text-white">
{t("settings.extensions.title")} {t("settings.extensions.title")}
</h2> </h2>
<Menu> <DropdownMenu>
<MenuButton className="flex items-center justify-center size-6 border rounded-[6px] dark:border-gray-700 hover:!border-[#0096FB] transition"> <DropdownMenuTrigger asChild>
<Plus className="size-4 text-[#0096FB]" /> <Button variant="outline" size="icon" className="size-6">
</MenuButton> <Plus className="h-4 w-4 text-primary" />
</Button>
</DropdownMenuTrigger>
<MenuItems <DropdownMenuContent
anchor={{ gap: 4 }} sideOffset={4}
className="p-1 text-sm bg-white dark:bg-[#202126] rounded-lg shadow-xs border border-gray-200 dark:border-gray-700" className="p-1 text-sm rounded-lg"
> >
<MenuItem> <DropdownMenuItem
<div className="px-3 py-2 rounded-lg hover:bg-muted"
className="px-3 py-2 hover:bg-black/5 hover:dark:bg-white/5 rounded-lg cursor-pointer" onSelect={(e: Event) => {
onClick={() => { e.preventDefault();
platformAdapter.emitEvent("open-extension-store"); platformAdapter.emitEvent("open-extension-store");
}} }}
> >
{t("settings.extensions.menuItem.extensionStore")} {t("settings.extensions.menuItem.extensionStore")}
</div> </DropdownMenuItem>
</MenuItem> <DropdownMenuItem
<MenuItem> className="px-3 py-2 rounded-lg hover:bg-muted"
<div onSelect={async (e: Event) => {
className="px-3 py-2 hover:bg-black/5 hover:dark:bg-white/5 rounded-lg cursor-pointer" e.preventDefault();
onClick={async () => { try {
try { const path = await platformAdapter.openFileDialog({
const path = await platformAdapter.openFileDialog({ directory: true,
directory: true, });
});
if (!path) return; if (!path) return;
await platformAdapter.invokeBackend( await platformAdapter.invokeBackend(
"install_local_extension", "install_local_extension",
{ path } { path }
); );
await getExtensions(); await getExtensions();
addError( addError(
t("settings.extensions.hints.importSuccess"), t("settings.extensions.hints.importSuccess"),
"info" "info"
); );
} catch (error) { } catch (error) {
installExtensionError(error); installExtensionError(error);
} }
}} }}
> >
{t("settings.extensions.menuItem.localExtensionImport")} {t("settings.extensions.menuItem.localExtensionImport")}
</div> </DropdownMenuItem>
</MenuItem> </DropdownMenuContent>
</MenuItems> </DropdownMenu>
</Menu>
</div> </div>
<div className="flex justify-between gap-6 my-4"> <div className="flex items-center justify-between gap-6 my-4">
<div className="flex h-8 border dark:border-gray-700 rounded-[6px] overflow-hidden"> <Tabs
{state.categories.map((item) => { value={state.currentCategory}
return ( onValueChange={(v) => {
<div state.currentCategory = v as Category;
key={item} }}
className={clsx( >
"flex items-center h-full px-4 cursor-pointer", <TabsList>
{ {state.categories.map((item) => (
"bg-[#F0F6FE] dark:bg-gray-700": <TabsTrigger key={item} value={item}>
item === state.currentCategory,
}
)}
onClick={() => {
state.currentCategory = item;
}}
>
{item} {item}
</div> </TabsTrigger>
); ))}
})} </TabsList>
</div> </Tabs>
<SettingsInput <Input
className="flex-1" className="flex-1 h-8"
placeholder="Search" placeholder="Search"
value={state.searchValue} value={state.searchValue ?? ""}
onChange={(value) => { onChange={(e) => {
state.searchValue = String(value); state.searchValue = e.target.value;
}} }}
/> />
</div> </div>

View File

@@ -36,6 +36,13 @@ import {
} from "@/commands"; } from "@/commands";
import platformAdapter from "@/utils/platformAdapter"; import platformAdapter from "@/utils/platformAdapter";
import { useAppearanceStore, WindowMode } from "@/stores/appearanceStore"; import { useAppearanceStore, WindowMode } from "@/stores/appearanceStore";
import {
Select,
SelectTrigger,
SelectContent,
SelectItem,
SelectValue,
} from "@/components/ui/select";
export function ThemeOption({ export function ThemeOption({
icon: Icon, icon: Icon,
@@ -83,8 +90,6 @@ export default function GeneralSettings() {
const { showTooltip, setShowTooltip, language, setLanguage } = useAppStore(); const { showTooltip, setShowTooltip, language, setLanguage } = useAppStore();
const { windowMode, setWindowMode } = useAppearanceStore(); const { windowMode, setWindowMode } = useAppearanceStore();
const fetchAutoStartStatus = async () => { const fetchAutoStartStatus = async () => {
if (isTauri()) { if (isTauri()) {
try { try {
@@ -283,7 +288,7 @@ export default function GeneralSettings() {
className={clsx( className={clsx(
"p-4 rounded-lg border-2 border-gray-200 dark:border-gray-700 hover:border-gray-300 dark:hover:border-gray-600 flex flex-col items-center justify-center space-y-2 transition-all", "p-4 rounded-lg border-2 border-gray-200 dark:border-gray-700 hover:border-gray-300 dark:hover:border-gray-600 flex flex-col items-center justify-center space-y-2 transition-all",
{ {
"!border-blue-500 bg-blue-50 dark:bg-blue-900/20": "border-blue-500! bg-blue-50! dark:bg-blue-900/20!":
isSelected, isSelected,
} }
)} )}
@@ -307,28 +312,31 @@ export default function GeneralSettings() {
})} })}
</div> </div>
<SettingsItem <SettingsItem
icon={Globe} icon={Globe}
title={t("settings.language.title")} title={t("settings.language.title")}
description={t("settings.language.description")} description={t("settings.language.description")}
> >
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<select <Select
value={currentLanguage} value={currentLanguage}
onChange={(event) => { onValueChange={(lang) => {
const lang = event.currentTarget.value;
setLanguage(lang); setLanguage(lang);
platformAdapter.invokeBackend("update_app_lang", { lang }); platformAdapter.invokeBackend("update_app_lang", { lang });
}} }}
className="px-3 py-1.5 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
> >
<option value="en">{t("settings.language.english")}</option> <SelectTrigger className="h-8 w-44">
<option value="zh">{t("settings.language.chinese")}</option> <SelectValue className="truncate" />
</select> </SelectTrigger>
<SelectContent>
<SelectItem value="en">
{t("settings.language.english")}
</SelectItem>
<SelectItem value="zh">
{t("settings.language.chinese")}
</SelectItem>
</SelectContent>
</Select>
</div> </div>
</SettingsItem> </SettingsItem>

View File

@@ -1,10 +1,13 @@
import { Input, InputProps } from "@headlessui/react"; import { Input } from "@/components/ui/input";
import { isNumber } from "lodash-es"; import { isNumber } from "lodash-es";
import { FC, FocusEvent } from "react"; import { FC, FocusEvent, InputHTMLAttributes } from "react";
import { twMerge } from "tailwind-merge"; import { twMerge } from "tailwind-merge";
interface SettingsInputProps interface SettingsInputProps
extends Omit<InputProps, "onChange" | "className"> { extends Omit<
InputHTMLAttributes<HTMLInputElement>,
"onChange" | "className"
> {
className?: string; className?: string;
onChange?: (value?: string | number) => void; onChange?: (value?: string | number) => void;
} }
@@ -35,10 +38,7 @@ const SettingsInput: FC<SettingsInputProps> = (props) => {
<Input <Input
{...rest} {...rest}
autoCorrect="off" autoCorrect="off"
className={twMerge( className={twMerge("w-44 h-8", className)}
"w-20 h-8 px-2 rounded-[6px] border bg-transparent border-black/5 dark:border-white/10 hover:!border-[#0072FF] focus:!border-[#0072FF] transition",
className
)}
onBlur={handleBlur} onBlur={handleBlur}
onChange={(event) => { onChange={(event) => {
onChange?.(event.target.value); onChange?.(event.target.value);

View File

@@ -7,7 +7,7 @@ interface SettingsPanelProps {
const SettingsPanel: React.FC<SettingsPanelProps> = ({ children }) => { const SettingsPanel: React.FC<SettingsPanelProps> = ({ children }) => {
return ( return (
<div className="bg-white dark:bg-gray-800 rounded-xl p-6"> <div className="bg-background text-foreground rounded-xl p-6">
{/* <h2 className="text-xl font-semibold mb-6">{title}</h2> */} {/* <h2 className="text-xl font-semibold mb-6">{title}</h2> */}
{children} {children}
</div> </div>

View File

@@ -1,7 +1,8 @@
import { useBoolean, useClickAway, useDebounce } from "ahooks"; import { useBoolean, useClickAway, useDebounce } from "ahooks";
import clsx from "clsx"; import clsx from "clsx";
import { FC, useEffect, useMemo, useRef, useState } from "react"; import { FC, useEffect, useMemo, useRef, useState } from "react";
import SettingsInput from "./SettingsInput";
import { Input } from "@/components/ui/input";
import NoDataImage from "../Common/NoDataImage"; import NoDataImage from "../Common/NoDataImage";
interface SettingsSelectProProps { interface SettingsSelectProProps {
@@ -47,7 +48,7 @@ const SettingsSelectPro: FC<SettingsSelectProProps> = (props) => {
return ( return (
<div ref={containerRef} className="relative"> <div ref={containerRef} className="relative">
<div <div
className="flex items-center h-8 px-3 truncate rounded-[6px] border dark:bg-[#1F2937] bg-white dark:border-[#374151]" className="flex items-center h-9 px-3 truncate rounded-md border border-input bg-background text-foreground shadow-sm"
onClick={toggle} onClick={toggle}
> >
{option?.[labelField] ?? ( {option?.[labelField] ?? (
@@ -57,7 +58,7 @@ const SettingsSelectPro: FC<SettingsSelectProProps> = (props) => {
<div <div
className={clsx( className={clsx(
"absolute z-100 top-10 left-0 right-0 rounded-[6px] py-2 border dark:border-[#374151] bg-white dark:bg-[#1F2937] shadow-[0_5px_15px_rgba(0,0,0,0.2)] dark:shadow-[0_5px_10px_rgba(0,0,0,0.3)]", "absolute z-50 top-11 left-0 right-0 rounded-md p-2 border border-input bg-popover text-popover-foreground shadow-md",
{ {
hidden: !open, hidden: !open,
} }
@@ -65,12 +66,12 @@ const SettingsSelectPro: FC<SettingsSelectProProps> = (props) => {
> >
{searchable && ( {searchable && (
<div className="px-2 mb-2"> <div className="px-2 mb-2">
<SettingsInput <Input
autoFocus autoFocus
value={searchValue} value={searchValue}
className="w-full" className="w-full h-8 border-0 shadow-none focus-visible:ring-0 focus-visible:ring-offset-0"
onChange={(value) => { onChange={(e) => {
setSearchValue(String(value)); setSearchValue(String(e.target.value));
}} }}
/> />
</div> </div>
@@ -83,9 +84,9 @@ const SettingsSelectPro: FC<SettingsSelectProProps> = (props) => {
<div <div
key={item?.[valueField] ?? index} key={item?.[valueField] ?? index}
className={clsx( className={clsx(
"h-8 leading-8 px-2 rounded-[6px] hover:bg-[#EDEDED] hover:dark:bg-[#374151] transition cursor-pointer", "h-8 leading-8 px-2 rounded-md hover:bg-accent hover:text-accent-foreground transition cursor-pointer",
{ {
"bg-[#EDEDED] dark:bg-[#374151]": "bg-accent text-accent-foreground":
value === item?.[valueField], value === item?.[valueField],
} }
)} )}

View File

@@ -1,28 +1,26 @@
import { Switch, SwitchProps } from "@headlessui/react"; import { Switch } from "@/components/ui/switch";
import clsx from "clsx"; import clsx from "clsx";
interface SettingsToggleProps extends SwitchProps { type BaseSwitchProps = React.ComponentProps<typeof Switch>;
interface SettingsToggleProps
extends Omit<BaseSwitchProps, "onChange" | "onCheckedChange"> {
label: string; label: string;
className?: string; className?: string;
onChange?: (checked: boolean) => void;
} }
export default function SettingsToggle(props: SettingsToggleProps) { export default function SettingsToggle(props: SettingsToggleProps) {
const { label, className, ...rest } = props; const { label, className, onChange, ...rest } = props;
return ( return (
<Switch <Switch
{...rest} {...rest}
aria-label={label}
onCheckedChange={(v) => onChange?.(v)}
className={clsx( className={clsx(
`group relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent "h-5 w-9",
transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 bg-gray-200 data-[checked]:bg-blue-600`,
className className
)} )}
> />
<span className="sr-only">{label}</span>
<span
className="pointer-events-none relative inline-block h-5 w-5 transform rounded-full bg-white shadow
ring-0 transition duration-200 ease-in-out translate-x-0 group-data-[checked]:translate-x-5"
/>
</Switch>
); );
} }

View File

@@ -1,7 +1,7 @@
import { useCallback, useMemo, useEffect } from "react"; import { useCallback, useMemo, useEffect } from "react";
import { Button, Dialog, DialogPanel } from "@headlessui/react"; import { Dialog, DialogContent } from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { noop } from "lodash-es";
import { LoaderCircle, X } from "lucide-react"; import { LoaderCircle, X } from "lucide-react";
import { useInterval, useReactive } from "ahooks"; import { useInterval, useReactive } from "ahooks";
import clsx from "clsx"; import clsx from "clsx";
@@ -141,117 +141,107 @@ const UpdateApp = ({ isCheckPage }: UpdateAppProps) => {
return ( return (
<Dialog <Dialog
open={isCheckPage ? true : visible} open={isCheckPage ? true : visible}
as="div" onOpenChange={(v) => {
id="update-app-dialog" if (!isCheckPage) setVisible(v);
className="relative z-10 focus:outline-none" }}
onClose={noop}
> >
<div <DialogContent
className={`fixed inset-0 z-10 w-screen overflow-y-auto ${ id="update-app-dialog"
overlayClassName={clsx("bg-transparent backdrop-blur-0 rounded-xl")}
className={clsx(
isCheckPage isCheckPage
? "rounded-lg bg-white dark:bg-[#333] border border-[#EDEDED] dark:border-black/20 shadow-md" ? "inset-0 left-0 top-0 translate-x-0 translate-y-0 w-full h-screen max-w-none rounded-lg border-none bg-background text-foreground p-0"
: "" : "w-[340px] py-8 flex flex-col items-center rounded-lg border border-input bg-background text-foreground shadow-md"
}`} )}
> >
<div <div
data-tauri-drag-region data-tauri-drag-region
className={clsx( className={clsx(
"flex min-h-full items-center justify-center", "w-full flex flex-col items-center justify-center px-6",
!isCheckPage && "p-4" isCheckPage && "h-full"
)} )}
> >
<DialogPanel {!isCheckPage && isOptional && (
transition <X
className={clsx(
"relative w-[340px] py-8 flex flex-col items-center",
{
"rounded-lg bg-white dark:bg-[#333] border border-[#EDEDED] dark:border-black/20 shadow-md":
!isCheckPage,
}
)}
>
{!isCheckPage && isOptional && (
<X
className={clsx(
"absolute size-5 top-3 right-3 text-[#999] dark:text-[#D8D8D8]",
cursorClassName
)}
onClick={handleCancel}
role="button"
aria-label="Close dialog"
tabIndex={0}
/>
)}
<img src={isDark ? darkIcon : lightIcon} className="h-6" />
<div className="text-[#333] text-sm leading-5 py-2 dark:text-[#D8D8D8] text-center">
{updateInfo ? (
isOptional ? (
t("update.optional_description")
) : (
<>
<p>{t("update.force_description1")}</p>
<p>{t("update.force_description2")}</p>
</>
)
) : (
t("update.date")
)}
</div>
{updateInfo ? (
<div
className="text-xs text-[#0072FF] cursor-pointer"
onClick={() =>
OpenURLWithBrowser(
"https://docs.infinilabs.com/coco-app/main/docs/release-notes"
)
}
>
v{updateInfo.version} {t("update.releaseNotes")}
</div>
) : (
<div className={clsx("text-xs text-[#999]", cursorClassName)}>
{t("update.latest", {
replace: [
updateInfo?.version || process.env.VERSION || "N/A",
],
})}
</div>
)}
<Button
className={clsx( className={clsx(
"mb-3 mt-6 bg-[#0072FF] text-white text-sm px-[14px] py-[8px] rounded-lg", "absolute h-5 w-5 top-3 right-3 text-muted-foreground",
cursorClassName, cursorClassName
state.loading && "opacity-50"
)} )}
onClick={updateInfo ? handleDownload : handleSkip} onClick={handleCancel}
> role="button"
{state.loading ? ( aria-label="Close dialog"
<div className="flex justify-center items-center gap-2"> tabIndex={0}
<LoaderCircle className="animate-spin size-5" /> />
{percent}% )}
</div>
) : updateInfo ? (
t("update.button.install")
) : (
t("update.button.ok")
)}
</Button>
{updateInfo && isOptional && ( <img src={isDark ? darkIcon : lightIcon} className="h-6" />
<div
className={clsx("text-xs text-[#999]", cursorClassName)} <div className="text-sm leading-5 py-2 text-foreground text-center">
onClick={handleSkip} {updateInfo ? (
> isOptional ? (
{t("update.skip_version")} t("update.optional_description")
</div> ) : (
<>
<p>{t("update.force_description1")}</p>
<p>{t("update.force_description2")}</p>
</>
)
) : (
t("update.date")
)} )}
</DialogPanel> </div>
{updateInfo ? (
<div
className="text-xs text-primary cursor-pointer"
onClick={() =>
OpenURLWithBrowser(
"https://docs.infinilabs.com/coco-app/main/docs/release-notes"
)
}
>
v{updateInfo.version} {t("update.releaseNotes")}
</div>
) : (
<div
className={clsx("text-xs text-muted-foreground", cursorClassName)}
>
{t("update.latest", {
replace: [updateInfo?.version || process.env.VERSION || "N/A"],
})}
</div>
)}
<Button
className={clsx(
"mb-3 mt-6 bg-primary text-primary-foreground text-sm px-[14px] py-[8px] rounded-lg",
cursorClassName,
state.loading && "opacity-50"
)}
onClick={updateInfo ? handleDownload : handleSkip}
>
{state.loading ? (
<div className="flex justify-center items-center gap-2">
<LoaderCircle className="animate-spin h-5 w-5" />
{percent}%
</div>
) : updateInfo ? (
t("update.button.install")
) : (
t("update.button.ok")
)}
</Button>
{updateInfo && isOptional && (
<div
className={clsx("text-xs text-muted-foreground", cursorClassName)}
onClick={handleSkip}
>
{t("update.skip_version")}
</div>
)}
</div> </div>
</div> </DialogContent>
</Dialog> </Dialog>
); );
}; };

View File

@@ -1,5 +1,5 @@
import { useAppStore } from "@/stores/appStore"; import { useAppStore } from "@/stores/appStore";
import { Button } from "@headlessui/react"; import { Button } from "@/components/ui/button";
import { SquareArrowOutUpRight } from "lucide-react"; import { SquareArrowOutUpRight } from "lucide-react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
@@ -12,10 +12,7 @@ const LoginButton = () => {
}; };
return ( return (
<Button <Button className="h-8" onClick={handleClick}>
className="px-6 h-8 text-white bg-[#0287FF] flex rounded-[8px] items-center justify-center gap-1"
onClick={handleClick}
>
<span>{t("webLogin.buttons.login")}</span> <span>{t("webLogin.buttons.login")}</span>
<SquareArrowOutUpRight className="size-4" /> <SquareArrowOutUpRight className="size-4" />

View File

@@ -1,6 +1,6 @@
import { RefreshCw } from "lucide-react"; import { RefreshCw } from "lucide-react";
import { FC, useState } from "react"; import { FC, useState } from "react";
import { Button, ButtonProps } from "@headlessui/react"; import { Button, ButtonProps } from "@/components/ui/button";
import clsx from "clsx"; import clsx from "clsx";
import { useWebConfigStore } from "@/stores/webConfigStore"; import { useWebConfigStore } from "@/stores/webConfigStore";
@@ -25,10 +25,9 @@ const RefreshButton: FC<ButtonProps> = (props) => {
<Button <Button
{...rest} {...rest}
onClick={handleRefresh} onClick={handleRefresh}
className={clsx( variant="outline"
"flex items-center justify-center size-6 bg-white dark:bg-[#202126] rounded-[8px] border dark:border-white/10", size="icon"
className className={clsx("size-8", className)}
)}
disabled={isRefreshing} disabled={isRefreshing}
> >
<VisibleKey shortcut="R" onKeyPress={handleRefresh}> <VisibleKey shortcut="R" onKeyPress={handleRefresh}>

View File

@@ -12,7 +12,7 @@ const UserAvatar: FC<UserAvatarProps> = (props) => {
return ( return (
<div <div
className={clsx( className={clsx(
"flex items-center justify-center size-5 rounded-full border dark:border-white/10 overflow-hidden", "flex items-center justify-center size-5 rounded-full border border-border overflow-hidden",
className className
)} )}
> >

View File

@@ -1,4 +1,8 @@
import { Popover, PopoverButton, PopoverPanel } from "@headlessui/react"; import {
Popover,
PopoverTrigger,
PopoverContent,
} from "@/components/ui/popover";
import { useWebConfigStore } from "@/stores/webConfigStore"; import { useWebConfigStore } from "@/stores/webConfigStore";
import { LogOut } from "lucide-react"; import { LogOut } from "lucide-react";
import clsx from "clsx"; import clsx from "clsx";
@@ -10,21 +14,18 @@ import RefreshButton from "./RefreshButton";
import LoginButton from "./LoginButton"; import LoginButton from "./LoginButton";
import { FC } from "react"; import { FC } from "react";
import Copyright from "../Common/Copyright"; import Copyright from "../Common/Copyright";
import { PopoverContentProps } from "@radix-ui/react-popover";
import { Button } from "../ui/button";
interface WebLoginProps { const WebLogin: FC<PopoverContentProps> = (props) => {
panelClassName: string;
}
const WebLogin: FC<WebLoginProps> = (props) => {
const { panelClassName } = props;
const { integration, loginInfo, setIntegration, setLoginInfo } = const { integration, loginInfo, setIntegration, setLoginInfo } =
useWebConfigStore(); useWebConfigStore();
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
<div className="relative"> <div className="flex items-center relative text-sm">
<Popover> <Popover>
<PopoverButton> <PopoverTrigger className="cursor-pointer">
{loginInfo ? ( {loginInfo ? (
<UserAvatar /> <UserAvatar />
) : ( ) : (
@@ -33,38 +34,35 @@ const WebLogin: FC<WebLoginProps> = (props) => {
className="size-5 text-[#999]" className="size-5 text-[#999]"
/> />
)} )}
</PopoverButton> </PopoverTrigger>
<PopoverPanel <PopoverContent {...props} className="p-0">
className={clsx(
"absolute z-50 w-[300px] rounded-xl bg-white dark:bg-[#202126] text-sm/6 text-[#333] dark:text-[#D8D8D8] shadow-lg border dark:border-white/10 -translate-y-2",
panelClassName
)}
>
<div className="p-3"> <div className="p-3">
<div className="flex items-center justify-between mb-2"> <div className="flex items-center justify-between mb-2">
<span>{t("webLogin.title")}</span> <span>{t("webLogin.title")}</span>
<RefreshButton /> <RefreshButton className="size-6" />
</div> </div>
<div className="py-2"> <div className="py-2">
{loginInfo ? ( {loginInfo ? (
<div className="flex justify-between items-center"> <div className="flex justify-between items-center gap-3">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<UserAvatar <UserAvatar
className="!size-12" className="h-12 w-12"
icon={{ className: "!size-6" }} icon={{ className: "h-6 w-6" }}
/> />
<div className="flex flex-col"> <div className="flex flex-col">
<span>{loginInfo.name}</span> <span>{loginInfo?.name}</span>
<span className="text-[#999]">{loginInfo.email}</span> <span className="text-[#999]">{loginInfo?.email}</span>
</div> </div>
</div> </div>
<button <Button
className="flex items-center justify-center size-6 bg-white dark:bg-[#202126] rounded-[8px] border dark:border-white/10" variant="outline"
size="icon"
className="size-6"
onClick={async () => { onClick={async () => {
await Post("/account/logout", void 0); await Post("/account/logout", void 0);
@@ -77,7 +75,7 @@ const WebLogin: FC<WebLoginProps> = (props) => {
"size-3 text-[#0287FF] transition-transform duration-1000" "size-3 text-[#0287FF] transition-transform duration-1000"
)} )}
/> />
</button> </Button>
</div> </div>
) : ( ) : (
<div className="flex flex-col items-center gap-3"> <div className="flex flex-col items-center gap-3">
@@ -93,10 +91,10 @@ const WebLogin: FC<WebLoginProps> = (props) => {
</div> </div>
</div> </div>
<div className="p-3 border-t dark:border-t-white/10"> <div className="p-3 border-t border-border">
<Copyright /> <Copyright />
</div> </div>
</PopoverPanel> </PopoverContent>
</Popover> </Popover>
</div> </div>
); );

View File

@@ -1,52 +1,46 @@
import * as React from "react"; import * as React from "react";
import { Slot } from "@radix-ui/react-slot";
import { cva, type VariantProps } from "class-variance-authority"; import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
const buttonVariants = cva( const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-[6px] text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0", "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 ring-offset-background cursor-pointer disabled:cursor-not-allowed disabled:pointer-events-none disabled:opacity-50",
{ {
variants: { variants: {
variant: { variant: {
default: default:
"bg-primary text-primary-foreground shadow hover:bg-primary/90", "bg-primary text-primary-foreground hover:bg-primary/90",
destructive:
"bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
outline:
"border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
secondary: secondary:
"bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80", "bg-secondary text-secondary-foreground hover:bg-secondary/80",
outline:
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
ghost: "hover:bg-accent hover:text-accent-foreground", ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline", destructive:
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
}, },
size: { size: {
default: "h-9 px-4 py-2", sm: "h-8 px-3",
sm: "h-8 rounded-[6px] px-3 text-xs", md: "h-9 px-4",
lg: "h-10 rounded-[6px] px-8", lg: "h-10 px-6",
icon: "h-9 w-9", icon: "h-9 w-9",
}, },
}, },
defaultVariants: { defaultVariants: {
variant: "default", variant: "default",
size: "default", size: "md",
}, },
} }
); );
export interface ButtonProps export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>, extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> { VariantProps<typeof buttonVariants> {}
asChild?: boolean;
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>( const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => { ({ className, variant, size, ...props }, ref) => {
const Comp = asChild ? Slot : "button";
return ( return (
<Comp <button
className={cn(buttonVariants({ variant, size, className }))}
ref={ref} ref={ref}
className={cn(buttonVariants({ variant, size }), className)}
{...props} {...props}
/> />
); );

View File

@@ -0,0 +1,25 @@
import * as React from "react";
import * as CheckboxPrimitive from "@radix-ui/react-checkbox";
import { Check } from "lucide-react";
import { cn } from "@/lib/utils";
const Checkbox = React.forwardRef<
React.ElementRef<typeof CheckboxPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
>(({ className, ...props }, ref) => (
<CheckboxPrimitive.Root
ref={ref}
className={cn(
"peer h-4 w-4 shrink-0 rounded-sm border border-input bg-background shadow ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
className
)}
{...props}
>
<CheckboxPrimitive.Indicator className="flex items-center justify-center text-primary-foreground">
<Check className="h-3.5 w-3.5" />
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
));
Checkbox.displayName = CheckboxPrimitive.Root.displayName;
export { Checkbox };

View File

@@ -0,0 +1,108 @@
import * as React from "react";
import * as DialogPrimitive from "@radix-ui/react-dialog";
import { cn } from "@/lib/utils";
const Dialog = DialogPrimitive.Root;
const DialogTrigger = DialogPrimitive.Trigger;
const DialogPortal = DialogPrimitive.Portal;
const DialogClose = DialogPrimitive.Close;
const DialogOverlay = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Overlay
ref={ref}
className={cn(
"fixed inset-0 z-3000 bg-black/60 backdrop-blur-sm",
className
)}
{...props}
/>
));
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
type DialogContentProps = React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content> & {
overlayClassName?: string;
};
const DialogContent = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Content>,
DialogContentProps
>(({ className, overlayClassName, ...props }, ref) => (
<DialogPortal>
<DialogOverlay className={overlayClassName} />
<DialogPrimitive.Content
ref={ref}
className={cn(
"fixed left-1/2 top-1/2 z-3001 w-full max-w-lg -translate-x-1/2 -translate-y-1/2 rounded-lg border border-input bg-background p-6 text-foreground shadow-lg focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 ring-offset-background",
className
)}
{...props}
/>
</DialogPortal>
));
DialogContent.displayName = DialogPrimitive.Content.displayName;
const DialogHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col space-y-2 text-center sm:text-left",
className
)}
{...props}
/>
);
const DialogFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
className
)}
{...props}
/>
);
const DialogTitle = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Title
ref={ref}
className={cn("text-lg font-semibold", className)}
{...props}
/>
));
DialogTitle.displayName = DialogPrimitive.Title.displayName;
const DialogDescription = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Description
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
));
DialogDescription.displayName = DialogPrimitive.Description.displayName;
export {
Dialog,
DialogTrigger,
DialogPortal,
DialogOverlay,
DialogContent,
DialogHeader,
DialogFooter,
DialogTitle,
DialogDescription,
DialogClose,
};

View File

@@ -0,0 +1,78 @@
import * as React from "react";
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
import { cn } from "@/lib/utils";
const DropdownMenu = DropdownMenuPrimitive.Root;
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;
const DropdownMenuContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
>(({ className, side = "bottom", align = "start", sideOffset = 8, ...props }, ref) => (
<DropdownMenuPrimitive.Portal>
<DropdownMenuPrimitive.Content
ref={ref}
side={side}
align={align}
sideOffset={sideOffset}
className={cn(
"z-50 min-w-40 rounded-lg border border-input bg-background p-1 text-foreground shadow-lg",
className
)}
{...props}
/>
</DropdownMenuPrimitive.Portal>
));
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName;
const DropdownMenuItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item>
>(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.Item
ref={ref}
className={cn(
"flex cursor-pointer select-none items-center rounded-md px-3 py-2 text-sm outline-none focus:bg-muted",
className
)}
{...props}
/>
));
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName;
const DropdownMenuLabel = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label>
>(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.Label
ref={ref}
className={cn("px-3 py-1 text-xs text-muted-foreground", className)}
{...props}
/>
));
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName;
const DropdownMenuSeparator = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
>(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.Separator
ref={ref}
className={cn("my-1 h-px bg-muted", className)}
{...props}
/>
));
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName;
const DropdownMenuGroup = DropdownMenuPrimitive.Group;
export {
DropdownMenu,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuGroup,
};

View File

@@ -0,0 +1,24 @@
import * as React from "react";
import { cn } from "@/lib/utils";
export interface InputProps
extends React.InputHTMLAttributes<HTMLInputElement> {}
const Input = React.forwardRef<HTMLInputElement, InputProps>(
({ className, type = "text", ...props }, ref) => {
return (
<input
type={type}
ref={ref}
className={cn(
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm text-foreground shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 ring-offset-background disabled:cursor-not-allowed disabled:opacity-50",
className
)}
{...props}
/>
);
}
);
Input.displayName = "Input";
export { Input };

View File

@@ -0,0 +1,21 @@
import * as React from "react";
import * as LabelPrimitive from "@radix-ui/react-label";
import { cn } from "@/lib/utils";
const Label = React.forwardRef<
React.ElementRef<typeof LabelPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
>(({ className, ...props }, ref) => (
<LabelPrimitive.Root
ref={ref}
className={cn(
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
className
)}
{...props}
/>
));
Label.displayName = LabelPrimitive.Root.displayName;
export { Label };

View File

@@ -0,0 +1,97 @@
import * as React from "react";
import * as PopoverPrimitive from "@radix-ui/react-popover";
import { cn } from "@/lib/utils";
import { Checkbox } from "@/components/ui/checkbox";
type Option = { value: string; label: string };
export interface MultiSelectProps {
options: Option[];
value: string[];
onChange?: (next: string[]) => void;
placeholder?: string;
className?: string;
disabled?: boolean;
}
export const MultiSelect: React.FC<MultiSelectProps> = ({
options,
value,
onChange,
placeholder = "",
className,
disabled,
}) => {
const [open, setOpen] = React.useState(false);
const values = React.useMemo(() => new Set(value), [value]);
const toggle = (v: string) => {
const next = new Set(values);
if (next.has(v)) next.delete(v);
else next.add(v);
onChange?.(Array.from(next));
};
const display = React.useMemo(() => {
if (values.size === 0) return placeholder;
const labels = options
.filter((o) => values.has(o.value))
.map((o) => o.label);
return labels.join(", ");
}, [options, values, placeholder]);
return (
<PopoverPrimitive.Root open={open} onOpenChange={setOpen}>
<PopoverPrimitive.Trigger asChild>
<button
type="button"
disabled={disabled}
className={cn(
"flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
className
)}
>
<span className={cn(values.size === 0 && "text-muted-foreground")}>{display}</span>
<svg
className="h-4 w-4 opacity-70"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M6 9l6 6 6-6" />
</svg>
</button>
</PopoverPrimitive.Trigger>
<PopoverPrimitive.Content
sideOffset={4}
className={cn(
"z-50 w-(--radix-popover-trigger-width) min-w-[220px] rounded-md border border-input bg-popover p-2 text-popover-foreground shadow-md outline-none",
"ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
)}
>
<div className="max-h-48 overflow-y-auto space-y-1">
{options.map((opt) => {
const checked = values.has(opt.value) ? "checked" : "unchecked";
return (
<label
key={opt.value}
className="flex cursor-pointer items-center gap-2 rounded-sm px-2 py-1 hover:bg-accent hover:text-accent-foreground"
onClick={(e) => {
e.preventDefault();
toggle(opt.value);
}}
>
<Checkbox checked={checked === "checked"} className="h-4 w-4" />
<span className="text-sm">{opt.label}</span>
</label>
);
})}
</div>
</PopoverPrimitive.Content>
</PopoverPrimitive.Root>
);
};

View File

@@ -0,0 +1,43 @@
import * as React from "react";
import * as PopoverPrimitive from "@radix-ui/react-popover";
import { cn } from "@/lib/utils";
const Popover = PopoverPrimitive.Root;
const PopoverTrigger = PopoverPrimitive.Trigger;
const PopoverPortal = PopoverPrimitive.Portal;
const PopoverContent = React.forwardRef<
React.ElementRef<typeof PopoverPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content> & {
panelId?: string;
}
>(
(
{
className,
panelId,
side = "bottom",
align = "start",
sideOffset = 8,
...props
},
ref
) => (
<PopoverPrimitive.Content
ref={ref}
side={side}
align={align}
sideOffset={sideOffset}
className={cn(
"z-50 rounded-lg border border-input bg-background p-1 text-foreground shadow-lg outline-none",
className
)}
data-popover-panel
id={panelId}
{...props}
/>
)
);
PopoverContent.displayName = "PopoverContent";
export { Popover, PopoverTrigger, PopoverContent, PopoverPortal };

View File

@@ -0,0 +1,160 @@
import * as React from "react"
import * as SelectPrimitive from "@radix-ui/react-select"
import { Check, ChevronDown, ChevronUp } from "lucide-react"
import { cn } from "@/lib/utils"
const Select = SelectPrimitive.Root
const SelectGroup = SelectPrimitive.Group
const SelectValue = SelectPrimitive.Value
const SelectTrigger = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
<SelectPrimitive.Trigger
ref={ref}
className={cn(
"flex h-9 w-full items-center justify-between whitespace-nowrap rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm ring-offset-background data-placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
className
)}
{...props}
>
{children}
<SelectPrimitive.Icon asChild>
<ChevronDown className="h-4 w-4 opacity-50" />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
))
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
const SelectScrollUpButton = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
>(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollUpButton
ref={ref}
className={cn(
"flex cursor-default items-center justify-center py-1",
className
)}
{...props}
>
<ChevronUp className="h-4 w-4" />
</SelectPrimitive.ScrollUpButton>
))
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
const SelectScrollDownButton = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
>(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollDownButton
ref={ref}
className={cn(
"flex cursor-default items-center justify-center py-1",
className
)}
{...props}
>
<ChevronDown className="h-4 w-4" />
</SelectPrimitive.ScrollDownButton>
))
SelectScrollDownButton.displayName =
SelectPrimitive.ScrollDownButton.displayName
const SelectContent = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
>(({ className, children, position = "popper", side = "bottom", avoidCollisions = false, sideOffset = 4, ...props }, ref) => (
<SelectPrimitive.Portal>
<SelectPrimitive.Content
ref={ref}
className={cn(
"relative z-50 max-h-[var(--radix-select-content-available-height)] min-w-[var(--radix-select-trigger-width)] w-[var(--radix-select-trigger-width)] overflow-hidden rounded-md border border-input bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[var(--radix-select-content-transform-origin)]",
position === "popper" &&
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
className
)}
position={position}
side={side}
avoidCollisions={avoidCollisions}
sideOffset={sideOffset}
{...props}
>
<SelectScrollUpButton />
<SelectPrimitive.Viewport
className={cn(
"p-1 max-h-[50vh] overflow-y-auto",
position === "popper" &&
"w-full min-w-[var(--radix-select-trigger-width)]"
)}
>
{children}
</SelectPrimitive.Viewport>
<SelectScrollDownButton />
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
))
SelectContent.displayName = SelectPrimitive.Content.displayName
const SelectLabel = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
>(({ className, ...props }, ref) => (
<SelectPrimitive.Label
ref={ref}
className={cn("px-2 py-1.5 text-sm font-semibold", className)}
{...props}
/>
))
SelectLabel.displayName = SelectPrimitive.Label.displayName
const SelectItem = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
>(({ className, children, ...props }, ref) => (
<SelectPrimitive.Item
ref={ref}
className={cn(
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-2 pr-8 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50",
className
)}
{...props}
>
<span className="absolute right-2 flex h-3.5 w-3.5 items-center justify-center">
<SelectPrimitive.ItemIndicator>
<Check className="h-4 w-4" />
</SelectPrimitive.ItemIndicator>
</span>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item>
))
SelectItem.displayName = SelectPrimitive.Item.displayName
const SelectSeparator = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
>(({ className, ...props }, ref) => (
<SelectPrimitive.Separator
ref={ref}
className={cn("-mx-1 my-1 h-px bg-muted", className)}
{...props}
/>
))
SelectSeparator.displayName = SelectPrimitive.Separator.displayName
export {
Select,
SelectGroup,
SelectValue,
SelectTrigger,
SelectContent,
SelectLabel,
SelectItem,
SelectSeparator,
SelectScrollUpButton,
SelectScrollDownButton,
}

View File

@@ -1,29 +1,24 @@
import * as React from "react" import * as React from "react";
import * as SeparatorPrimitive from "@radix-ui/react-separator" import * as SeparatorPrimitive from "@radix-ui/react-separator";
import { cn } from "@/lib/utils";
import { cn } from "@/lib/utils"
const Separator = React.forwardRef< const Separator = React.forwardRef<
React.ElementRef<typeof SeparatorPrimitive.Root>, React.ElementRef<typeof SeparatorPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root> React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
>( >(({ className, orientation = "horizontal", decorative, ...props }, ref) => (
( <SeparatorPrimitive.Root
{ className, orientation = "horizontal", decorative = true, ...props }, ref={ref}
ref decorative={decorative}
) => ( orientation={orientation}
<SeparatorPrimitive.Root className={cn(
ref={ref} "shrink-0 bg-border",
decorative={decorative} orientation === "horizontal" ? "h-px w-full" : "h-full w-px",
orientation={orientation} className
className={cn( )}
"shrink-0 bg-border", {...props}
orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]", />
className ));
)} Separator.displayName = SeparatorPrimitive.Root.displayName;
{...props}
/> export { Separator };
)
)
Separator.displayName = SeparatorPrimitive.Root.displayName
export { Separator }

View File

@@ -0,0 +1,27 @@
import * as React from "react";
import * as SliderPrimitive from "@radix-ui/react-slider";
import { cn } from "@/lib/utils";
const Slider = React.forwardRef<
React.ElementRef<typeof SliderPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof SliderPrimitive.Root>
>(({ className, ...props }, ref) => (
<SliderPrimitive.Root
ref={ref}
className={cn(
"relative flex w-full touch-none select-none items-center",
className
)}
{...props}
>
<SliderPrimitive.Track className="relative h-1.5 w-full grow overflow-hidden rounded-full bg-secondary">
<SliderPrimitive.Range className="absolute h-full bg-primary" />
</SliderPrimitive.Track>
<SliderPrimitive.Thumb className="block h-4 w-4 rounded-full border-2 border-primary bg-background ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50" />
</SliderPrimitive.Root>
));
Slider.displayName = SliderPrimitive.Root.displayName;
export { Slider };

View File

@@ -0,0 +1,27 @@
import * as React from "react"
import * as SwitchPrimitives from "@radix-ui/react-switch"
import { cn } from "@/lib/utils"
const Switch = React.forwardRef<
React.ElementRef<typeof SwitchPrimitives.Root>,
React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>
>(({ className, ...props }, ref) => (
<SwitchPrimitives.Root
className={cn(
"peer inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input",
className
)}
{...props}
ref={ref}
>
<SwitchPrimitives.Thumb
className={cn(
"pointer-events-none block h-4 w-4 rounded-full bg-white shadow ring-0 transition-transform data-[state=checked]:translate-x-4 data-[state=unchecked]:translate-x-0"
)}
/>
</SwitchPrimitives.Root>
))
Switch.displayName = SwitchPrimitives.Root.displayName
export { Switch }

View File

@@ -0,0 +1,53 @@
import * as React from "react";
import * as TabsPrimitive from "@radix-ui/react-tabs";
import { cn } from "@/lib/utils";
const Tabs = TabsPrimitive.Root;
const TabsList = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.List>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
>(({ className, ...props }, ref) => (
<TabsPrimitive.List
ref={ref}
className={cn(
"inline-flex h-9 items-center justify-center rounded-lg bg-muted p-1 text-muted-foreground",
className
)}
{...props}
/>
));
TabsList.displayName = TabsPrimitive.List.displayName;
const TabsTrigger = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Trigger
ref={ref}
className={cn(
"inline-flex items-center justify-center whitespace-nowrap rounded-md px-3 py-1 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow",
className
)}
{...props}
/>
));
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName;
const TabsContent = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Content
ref={ref}
className={cn(
"mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
className
)}
{...props}
/>
));
TabsContent.displayName = TabsPrimitive.Content.displayName;
export { Tabs, TabsList, TabsTrigger, TabsContent };

View File

@@ -1,4 +1,4 @@
export const POPOVER_PANEL_SELECTOR = '[id^="headlessui-popover-panel"]'; export const POPOVER_PANEL_SELECTOR = '[data-popover-panel]';
export const HISTORY_PANEL_ID = "headlessui-popover-panel:history-panel"; export const HISTORY_PANEL_ID = "headlessui-popover-panel:history-panel";

View File

@@ -1,8 +1,14 @@
@tailwind base; @import "tailwindcss";
@tailwind components; /* Tailwind v4: ensure class extraction scans our source files */
@tailwind utilities; @source "../index.html";
@source "./**/*.{ts,tsx}";
/* Tailwind v4 custom variant for dark mode */
@custom-variant dark (&:where(.dark, .dark *));
@custom-variant dark (&:where([data-theme="dark"], [data-theme="dark"] *));
/* Base variables */ /* Base variables */
.coco-container,
:root { :root {
--spacing-base: 12px; --spacing-base: 12px;
--modal-width: 560px; --modal-width: 560px;
@@ -11,13 +17,126 @@
--hit-height: 56px; --hit-height: 56px;
--footer-height: 44px; --footer-height: 44px;
--icon-stroke-width: 1.4; --icon-stroke-width: 1.4;
--background: #ffffff;
--foreground: #09090b;
--border: #e3e3e7;
--coco-primary-color: rgb(149, 5, 153); --coco-primary-color: rgb(149, 5, 153);
/* Default coco light extras to ensure availability when data-theme="auto" */
--coco-text-color: rgb(28, 30, 33);
--coco-muted-color: rgb(150, 159, 175);
--coco-modal-container-background: rgba(101, 108, 133, 0.8);
--coco-modal-background: rgb(245, 246, 247);
--coco-modal-shadow: inset 1px 1px 0 0 rgba(255, 255, 255, 0.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, 0.4);
--coco-footer-background: #fff;
--coco-footer-shadow: 0 -1px 0 0 rgb(224, 227, 232),
0 -3px 6px 0 rgba(69, 98, 155, 0.12);
--coco-icon-color: rgb(21, 21, 21);
/* Theme tokens using oklch color space */
--radius: 0.65rem;
/* shadcn blue theme (light) */
--background: hsl(0 0% 100%);
--foreground: hsl(222.2 84% 4.9%);
--card: hsl(0 0% 100%);
--card-foreground: hsl(222.2 84% 4.9%);
--popover: hsl(0 0% 100%);
--popover-foreground: hsl(222.2 84% 4.9%);
--primary: hsl(221.2 83.2% 53.3%);
--primary-foreground: hsl(210 40% 98%);
--secondary: hsl(210 40% 96.1%);
--secondary-foreground: hsl(222.2 47.4% 11.2%);
--muted: hsl(210 40% 96.1%);
--muted-foreground: hsl(215.4 16.3% 46.9%);
--accent: hsl(210 40% 96.1%);
--accent-foreground: hsl(222.2 47.4% 11.2%);
--destructive: hsl(0 84.2% 60.2%);
--border: hsl(214.3 31.8% 91.4%);
--input: hsl(214.3 31.8% 91.4%);
--ring: hsl(221.2 83.2% 53.3%);
--chart-1: oklch(0.809 0.105 251.813);
--chart-2: oklch(0.623 0.214 259.815);
--chart-3: oklch(0.546 0.245 262.881);
--chart-4: oklch(0.488 0.243 264.376);
--chart-5: oklch(0.424 0.199 265.638);
--sidebar: oklch(0.985 0 0);
--sidebar-foreground: oklch(0.141 0.005 285.823);
--sidebar-primary: oklch(0.546 0.245 262.881);
--sidebar-primary-foreground: oklch(0.97 0.014 254.604);
--sidebar-accent: oklch(0.967 0.001 286.375);
--sidebar-accent-foreground: oklch(0.21 0.006 285.885);
--sidebar-border: oklch(0.92 0.004 286.32);
--sidebar-ring: oklch(0.708 0 0);
} }
/* Light theme */ @theme {
/* Map tokens directly; they are oklch(...) or other full values */
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-destructive-foreground: var(--destructive-foreground);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
--color-chart-1: var(--chart-1);
--color-chart-2: var(--chart-2);
--color-chart-3: var(--chart-3);
--color-chart-4: var(--chart-4);
--color-chart-5: var(--chart-5);
}
#searchChat-container {
/* Map tokens directly; they are oklch(...) or other full values */
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-destructive-foreground: var(--destructive-foreground);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
--color-chart-1: var(--chart-1);
--color-chart-2: var(--chart-2);
--color-chart-3: var(--chart-3);
--color-chart-4: var(--chart-4);
--color-chart-5: var(--chart-5);
}
/* Light theme extras for coco-specific tokens */
.light.coco-container,
[data-theme="light"] { [data-theme="light"] {
--coco-primary-color: rgb(149, 5, 153); --coco-primary-color: rgb(149, 5, 153);
--coco-text-color: rgb(28, 30, 33); --coco-text-color: rgb(28, 30, 33);
@@ -45,12 +164,45 @@
--coco-icon-color: rgb(21, 21, 21); --coco-icon-color: rgb(21, 21, 21);
} }
/* Dark theme */ /* Dark theme tokens mapped for both `.dark` class and `[data-theme="dark"]` */
.dark.coco-container,
[data-theme="dark"] { [data-theme="dark"] {
/* shadcn blue theme (dark) */
--background: hsl(222.2 84% 4.9%);
--foreground: hsl(210 40% 98%);
--card: hsl(222.2 84% 4.9%);
--card-foreground: hsl(210 40% 98%);
--popover: hsl(222.2 84% 4.9%);
--popover-foreground: hsl(210 40% 98%);
--primary: hsl(221.2 83.2% 53.3%);
--primary-foreground: hsl(210 40% 98%);
--secondary: hsl(217.2 32.6% 17.5%);
--secondary-foreground: hsl(210 40% 98%);
--muted: hsl(217.2 32.6% 17.5%);
--muted-foreground: hsl(215 20.2% 65.1%);
--accent: hsl(217.2 32.6% 17.5%);
--accent-foreground: hsl(210 40% 98%);
--destructive: hsl(0 62.8% 30.6%);
--border: hsl(217.2 32.6% 17.5%);
--input: hsl(217.2 32.6% 17.5%);
--ring: hsl(221.2 83.2% 53.3%);
--chart-1: oklch(0.809 0.105 251.813);
--chart-2: oklch(0.623 0.214 259.815);
--chart-3: oklch(0.546 0.245 262.881);
--chart-4: oklch(0.488 0.243 264.376);
--chart-5: oklch(0.424 0.199 265.638);
--sidebar: oklch(0.21 0.006 285.885);
--sidebar-foreground: oklch(0.985 0 0);
--sidebar-primary: oklch(0.623 0.214 259.815);
--sidebar-primary-foreground: oklch(0.97 0.014 254.604);
--sidebar-accent: oklch(0.274 0.006 286.033);
--sidebar-accent-foreground: oklch(0.985 0 0);
--sidebar-border: oklch(1 0 0 / 10%);
--sidebar-ring: oklch(0.439 0 0);
/* coco dark extras */
--coco-primary-color: rgb(149, 5, 153); --coco-primary-color: rgb(149, 5, 153);
--background: #09090b;
--foreground: #f9f9f9;
--border: #27272a;
--coco-text-color: rgb(245, 246, 247); --coco-text-color: rgb(245, 246, 247);
--coco-modal-container-background: rgba(9, 10, 17, 0.8); --coco-modal-container-background: rgba(9, 10, 17, 0.8);
--coco-modal-background: rgb(21, 23, 42); --coco-modal-background: rgb(21, 23, 42);
@@ -75,137 +227,76 @@
--coco-icon-color: rgb(255, 255, 255); --coco-icon-color: rgb(255, 255, 255);
} }
/* Base styles */ /* Base styles (scoped to coco container to avoid global overrides) */
@layer base { @layer base {
* {
@apply box-border border-[--border] outline-none;
}
html { html {
@apply h-full overscroll-none select-none; @apply overscroll-none;
} }
.coco-container * {
body, @apply box-border outline-none;
#root {
@apply h-full text-gray-900 antialiased;
} }
.coco-container {
.dark body, @apply antialiased rounded-xl text-foreground;
.dark #root {
@apply text-gray-100;
} }
.coco-container .input-body {
.input-body { @apply rounded-xl overflow-hidden;
@apply rounded-[6px] overflow-hidden;
} }
.coco-container .icon {
.icon {
width: 1em; width: 1em;
height: 1em; height: 1em;
vertical-align: -0.15em; vertical-align: -0.15em;
fill: currentColor; fill: currentColor;
overflow: hidden; overflow: hidden;
} }
:root {
--background: 0 0% 100%;
--foreground: 0 0% 3.9%;
--card: 0 0% 100%;
--card-foreground: 0 0% 3.9%;
--popover: 0 0% 100%;
--popover-foreground: 0 0% 3.9%;
--primary: 0 0% 9%;
--primary-foreground: 0 0% 98%;
--secondary: 0 0% 96.1%;
--secondary-foreground: 0 0% 9%;
--muted: 0 0% 96.1%;
--muted-foreground: 0 0% 45.1%;
--accent: 0 0% 96.1%;
--accent-foreground: 0 0% 9%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 0 0% 98%;
--border: 0 0% 89.8%;
--input: 0 0% 89.8%;
--ring: 0 0% 3.9%;
--chart-1: 12 76% 61%;
--chart-2: 173 58% 39%;
--chart-3: 197 37% 24%;
--chart-4: 43 74% 66%;
--chart-5: 27 87% 67%;
--radius: 0.5rem;
}
.dark {
--background: 0 0% 3.9%;
--foreground: 0 0% 98%;
--card: 0 0% 3.9%;
--card-foreground: 0 0% 98%;
--popover: 0 0% 3.9%;
--popover-foreground: 0 0% 98%;
--primary: 0 0% 98%;
--primary-foreground: 0 0% 9%;
--secondary: 0 0% 14.9%;
--secondary-foreground: 0 0% 98%;
--muted: 0 0% 14.9%;
--muted-foreground: 0 0% 63.9%;
--accent: 0 0% 14.9%;
--accent-foreground: 0 0% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 0 0% 98%;
--border: 0 0% 14.9%;
--input: 0 0% 14.9%;
--ring: 0 0% 83.1%;
--chart-1: 220 70% 50%;
--chart-2: 160 60% 45%;
--chart-3: 30 80% 55%;
--chart-4: 280 65% 60%;
--chart-5: 340 75% 55%;
}
} }
/* Component styles */ /* Component styles */
@layer components { @layer components {
.settings-input { .settings-input {
@apply block w-full rounded-[6px] border-gray-300 dark:border-gray-600 @apply block w-full rounded-md border border-border
bg-white dark:bg-gray-700 bg-background text-foreground
text-gray-900 dark:text-gray-100 shadow-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring
shadow-sm focus:border-blue-500 focus:ring-blue-500
transition-colors duration-200; transition-colors duration-200;
} }
.settings-select { .settings-select {
@apply text-sm rounded-[6px] border-gray-300 dark:border-gray-600 @apply text-sm rounded-md border border-border
bg-white dark:bg-gray-700 bg-background text-foreground
text-gray-900 dark:text-gray-100 shadow-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring
shadow-sm focus:border-blue-500 focus:ring-blue-500
transition-colors duration-200; transition-colors duration-200;
} }
} }
/* Utility styles */ /* Utility styles (scoped to coco container where reasonable) */
@layer utilities { @layer utilities {
/* Fallback for Tailwind v4 class extraction edge-cases: ensure rounded-xl exists */
.rounded-xl {
border-radius: 0.75rem;
}
/* Scrollbar styles */ /* Scrollbar styles */
.custom-scrollbar { .coco-container .custom-scrollbar {
scrollbar-width: thin; scrollbar-width: thin;
scrollbar-color: #cbd5e1 transparent; scrollbar-color: #cbd5e1 transparent;
} }
.custom-scrollbar::-webkit-scrollbar { .coco-container .custom-scrollbar::-webkit-scrollbar {
width: 6px; width: 6px;
} }
.custom-scrollbar::-webkit-scrollbar-track { .coco-container .custom-scrollbar::-webkit-scrollbar-track {
background: transparent; background: transparent;
} }
.custom-scrollbar::-webkit-scrollbar-thumb { .coco-container .custom-scrollbar::-webkit-scrollbar-thumb {
background-color: #cbd5e1; background-color: #cbd5e1;
border-radius: 3px; border-radius: 3px;
} }
.dark .custom-scrollbar { .dark.coco-container .custom-scrollbar {
scrollbar-color: #475569 transparent; scrollbar-color: #475569 transparent;
} }
.dark .custom-scrollbar::-webkit-scrollbar-thumb { .dark.coco-container .custom-scrollbar::-webkit-scrollbar-thumb {
background-color: #475569; background-color: #475569;
} }
@@ -315,11 +406,11 @@
} }
} }
@layer base { /* Hide the waveform visualization for speech-to-text (only appears in production with two waveforms) */
* { ::part(progress) {
@apply border-border; display: none;
} }
body {
@apply bg-background text-foreground; ::part(scroll) {
} overflow: hidden;
} }

View File

@@ -1,12 +1,16 @@
import ReactDOM from "react-dom/client"; import ReactDOM from "react-dom/client";
import { RouterProvider } from "react-router-dom"; import { RouterProvider } from "react-router-dom";
import platformAdapter from "@/utils/platformAdapter";
import { router } from "./routes/index"; import { router } from "./routes/index";
import { routerWeb } from "./routes/web";
import "./i18n"; import "./i18n";
import '@/utils/global-logger'; import '@/utils/global-logger';
import "./main.css"; import "./main.css";
const isTauri = platformAdapter.isTauri();
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render( ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
<RouterProvider router={router} /> <RouterProvider router={isTauri ? router : routerWeb} />
); );

View File

@@ -1,5 +1,5 @@
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import { Tab, TabGroup, TabList, TabPanel, TabPanels } from "@headlessui/react"; import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs";
import { Settings, Puzzle, Settings2, Info, Server } from "lucide-react"; import { Settings, Puzzle, Settings2, Info, Server } from "lucide-react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { listen } from "@tauri-apps/api/event"; import { listen } from "@tauri-apps/api/event";
@@ -18,13 +18,14 @@ import { useAppStore } from "@/stores/appStore";
import { useExtensionsStore } from "@/stores/extensionsStore"; import { useExtensionsStore } from "@/stores/extensionsStore";
import { useAppearanceStore } from "@/stores/appearanceStore"; import { useAppearanceStore } from "@/stores/appearanceStore";
const tabIndexMap: { [key: string]: number } = { const tabValues = [
general: 0, "general",
extensions: 1, "extensions",
connect: 2, "connect",
advanced: 3, "advanced",
about: 4, "about",
}; ] as const;
type TabValue = (typeof tabValues)[number];
function SettingsPage() { function SettingsPage() {
const { t } = useTranslation(); const { t } = useTranslation();
@@ -32,22 +33,21 @@ function SettingsPage() {
useTray(); useTray();
const tabs = [ const tabs: { name: string; icon: any; value: TabValue }[] = [
{ name: t("settings.tabs.general"), icon: Settings }, { name: t("settings.tabs.general"), icon: Settings, value: "general" },
{ name: t("settings.tabs.extensions"), icon: Puzzle }, { name: t("settings.tabs.extensions"), icon: Puzzle, value: "extensions" },
{ name: t("settings.tabs.connect"), icon: Server }, { name: t("settings.tabs.connect"), icon: Server, value: "connect" },
{ name: t("settings.tabs.advanced"), icon: Settings2 }, { name: t("settings.tabs.advanced"), icon: Settings2, value: "advanced" },
{ name: t("settings.tabs.about"), icon: Info }, { name: t("settings.tabs.about"), icon: Info, value: "about" },
]; ];
const [defaultIndex, setDefaultIndex] = useState<number>(0); const [selectedTab, setSelectedTab] = useState<TabValue>("general");
useEffect(() => { useEffect(() => {
const unlisten = listen("tab_index", (event) => { const unlisten = listen("tab_index", (event) => {
const tabName = event.payload as string; const tabName = event.payload as TabValue;
const index = tabIndexMap[tabName]; if (tabValues.includes(tabName)) {
if (index !== -1) { setSelectedTab(tabName);
setDefaultIndex(index);
} }
}); });
@@ -67,7 +67,7 @@ function SettingsPage() {
"config-extension", "config-extension",
({ payload }) => { ({ payload }) => {
platformAdapter.showWindow(); platformAdapter.showWindow();
setDefaultIndex(1); setSelectedTab("extensions");
setConfigId(payload); setConfigId(payload);
} }
); );
@@ -82,69 +82,60 @@ function SettingsPage() {
}, []); }, []);
useEffect(() => { useEffect(() => {
document.body.style.overflow = defaultIndex === 1 ? "hidden" : "auto"; document.body.style.overflow =
}, [defaultIndex]); selectedTab === "extensions" ? "hidden" : "auto";
}, [selectedTab]);
return ( return (
<div> <>
<div className="min-h-screen pb-8 bg-white dark:bg-gray-900 text-gray-900 dark:text-white"> <div className="min-h-screen pb-8 bg-background text-foreground">
<div className="max-w-6xl mx-auto p-4"> <div className="max-w-6xl mx-auto p-4">
<TabGroup <Tabs
selectedIndex={defaultIndex} value={selectedTab}
onChange={(index) => { onValueChange={(v) => setSelectedTab(v as TabValue)}
setDefaultIndex(index);
}}
> >
<TabList className="flex space-x-1 rounded-xl bg-gray-100 dark:bg-gray-800 p-1"> <TabsList className="flex h-10 rounded-xl">
{tabs.map((tab) => ( {tabs.map((tab) => (
<Tab <TabsTrigger
key={tab.name} key={tab.value}
className={({ selected }) => value={tab.value}
`w-full rounded-lg py-2.5 text-sm font-medium leading-5 className="flex-1 gap-2 h-full"
${
selected
? "bg-white dark:bg-gray-700 shadow text-gray-900 dark:text-white"
: "text-gray-700 dark:text-gray-400 hover:bg-white/[0.12] hover:text-gray-900 dark:hover:text-white"
}
flex items-center justify-center space-x-2 focus:outline-none`
}
> >
<tab.icon className="w-4 h-4" /> <tab.icon className="size-4" />
<span>{tab.name}</span>
</Tab>
))}
</TabList>
<TabPanels className="mt-2"> <span>{tab.name}</span>
<TabPanel> </TabsTrigger>
<SettingsPanel title=""> ))}
<GeneralSettings /> </TabsList>
</SettingsPanel>
</TabPanel> <TabsContent value="general">
<TabPanel> <SettingsPanel title="">
<SettingsPanel title=""> <GeneralSettings />
<Extensions /> </SettingsPanel>
</SettingsPanel> </TabsContent>
</TabPanel> <TabsContent value="extensions">
<TabPanel> <SettingsPanel title="">
<Cloud /> <Extensions />
</TabPanel> </SettingsPanel>
<TabPanel> </TabsContent>
<SettingsPanel title=""> <TabsContent value="connect">
<Advanced /> <Cloud />
</SettingsPanel> </TabsContent>
</TabPanel> <TabsContent value="advanced">
<TabPanel> <SettingsPanel title="">
<SettingsPanel title=""> <Advanced />
<AboutView /> </SettingsPanel>
</SettingsPanel> </TabsContent>
</TabPanel> <TabsContent value="about">
</TabPanels> <SettingsPanel title="">
</TabGroup> <AboutView />
</SettingsPanel>
</TabsContent>
</Tabs>
</div> </div>
</div> </div>
<Footer /> <Footer />
</div> </>
); );
} }

View File

@@ -0,0 +1,114 @@
import { useState } from "react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Checkbox } from "@/components/ui/checkbox";
import { Switch } from "@/components/ui/switch";
import { Separator } from "@/components/ui/separator";
import { Slider } from "@/components/ui/slider";
import { MultiSelect } from "@/components/ui/multi-select";
import {
Dialog,
DialogTrigger,
DialogContent,
DialogHeader,
DialogTitle,
DialogDescription,
DialogFooter,
} from "@/components/ui/dialog";
import {
Select,
SelectTrigger,
SelectContent,
SelectItem,
SelectValue,
} from "@/components/ui/select";
const ShadcnDemo = () => {
const [checked, setChecked] = useState(false);
const [enabled, setEnabled] = useState(false);
const [sliderValue, setSliderValue] = useState<number[]>([50]);
const [selected, setSelected] = useState<string[]>([]);
const options = [
{ value: "a", label: "选项 A" },
{ value: "b", label: "选项 B" },
{ value: "c", label: "选项 C" },
{ value: "d", label: "选项 D" },
];
return (
<div className="p-6 space-y-6">
<h2 className="text-xl font-semibold">Shadcn </h2>
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
<div className="space-y-2">
<Label htmlFor="name"></Label>
<Input id="name" placeholder="输入名称" />
</div>
<div className="space-y-2">
<Label></Label>
<Select>
<SelectTrigger>
<SelectValue placeholder="请选择" />
</SelectTrigger>
<SelectContent>
<SelectItem value="a"> A</SelectItem>
<SelectItem value="b"> B</SelectItem>
<SelectItem value="c"> C</SelectItem>
</SelectContent>
</Select>
</div>
</div>
<Separator />
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
<div className="space-y-2">
<Label></Label>
<MultiSelect options={options} value={selected} onChange={setSelected} />
<div className="text-sm text-muted-foreground">{selected.length ? selected.join(", ") : "无"}</div>
</div>
<div className="space-y-2">
<Label>{sliderValue[0]}</Label>
<Slider value={sliderValue} onValueChange={setSliderValue} max={100} step={1} />
</div>
</div>
<div className="flex items-center gap-6">
<div className="flex items-center gap-2">
<Checkbox checked={checked} onCheckedChange={(v) => setChecked(Boolean(v))} />
<span>{checked ? "已选" : "未选"}</span>
</div>
<div className="flex items-center gap-2">
<Switch checked={enabled} onCheckedChange={setEnabled} />
<span>{enabled ? "开启" : "关闭"}</span>
</div>
</div>
<div className="flex gap-3">
<Button></Button>
<Button variant="secondary"></Button>
<Button variant="outline"></Button>
<Dialog>
<DialogTrigger asChild>
<Button variant="ghost"></Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle></DialogTitle>
<DialogDescription> Shadcn Dialog </DialogDescription>
</DialogHeader>
<DialogFooter>
<Button variant="secondary"></Button>
<Button></Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
</div>
);
};
export default ShadcnDemo;

View File

@@ -16,7 +16,8 @@ import { Get } from "@/api/axiosRequest";
import { useWebConfigStore } from "@/stores/webConfigStore"; import { useWebConfigStore } from "@/stores/webConfigStore";
import "@/i18n"; import "@/i18n";
import "@/web.css"; import { useIconfontScript } from "@/hooks/useScript";
// Styles are distributed separately in the library build (out/search-chat/index.css)
interface WebAppProps { interface WebAppProps {
headers?: Record<string, unknown>; headers?: Record<string, unknown>;
@@ -117,6 +118,7 @@ function WebApp({
useEscape(); useEscape();
useModifierKeyPress(); useModifierKeyPress();
useViewportHeight(); useViewportHeight();
useIconfontScript();
useEffect(() => { useEffect(() => {
setDisabled(!loginInfo && !integration?.guest?.enabled); setDisabled(!loginInfo && !integration?.guest?.enabled);
@@ -125,7 +127,7 @@ function WebApp({
return ( return (
<div <div
id="searchChat-container" id="searchChat-container"
className={`coco-container relative ${theme}`} className={`coco-container relative ${theme} border! border-(--border) rounded-xl`}
data-theme={theme} data-theme={theme}
style={{ style={{
maxWidth: `${width}px`, maxWidth: `${width}px`,

View File

@@ -7,9 +7,8 @@ import ErrorPage from "@/pages/error/index";
const DesktopApp = lazy(() => import("@/pages/main/index")); const DesktopApp = lazy(() => import("@/pages/main/index"));
const SettingsPage = lazy(() => import("@/pages/settings/index")); const SettingsPage = lazy(() => import("@/pages/settings/index"));
const StandaloneChat = lazy(() => import("@/pages/chat/index")); const StandaloneChat = lazy(() => import("@/pages/chat/index"));
const WebPage = lazy(() => import("@/pages/web/index"));
const CheckPage = lazy(() => import("@/pages/check/index")); const CheckPage = lazy(() => import("@/pages/check/index"));
// const SelectionWindow = lazy(() => import("@/pages/selection/index")); const SelectionWindow = lazy(() => import("@/pages/selection/index"));
const routerOptions = { const routerOptions = {
basename: "/", basename: "/",
@@ -30,8 +29,7 @@ export const router = createBrowserRouter(
{ path: "/ui/settings", element: (<Suspense fallback={<></>}><SettingsPage /></Suspense>) }, { path: "/ui/settings", element: (<Suspense fallback={<></>}><SettingsPage /></Suspense>) },
{ path: "/ui/chat", element: (<Suspense fallback={<></>}><StandaloneChat /></Suspense>) }, { path: "/ui/chat", element: (<Suspense fallback={<></>}><StandaloneChat /></Suspense>) },
{ path: "/ui/check", element: (<Suspense fallback={<></>}><CheckPage /></Suspense>) }, { path: "/ui/check", element: (<Suspense fallback={<></>}><CheckPage /></Suspense>) },
// { path: "/ui/selection", element: (<Suspense fallback={<></>}><SelectionWindow /></Suspense>) }, { path: "/ui/selection", element: (<Suspense fallback={<></>}><SelectionWindow /></Suspense>) },
{ path: "/web", element: (<Suspense fallback={<></>}><WebPage /></Suspense>) },
], ],
}, },
], ],

View File

@@ -15,7 +15,7 @@ import { useModifierKeyPress } from "@/hooks/useModifierKeyPress";
import { useIconfontScript } from "@/hooks/useScript"; import { useIconfontScript } from "@/hooks/useScript";
import { Extension } from "@/components/Settings/Extensions"; import { Extension } from "@/components/Settings/Extensions";
import { useExtensionsStore } from "@/stores/extensionsStore"; import { useExtensionsStore } from "@/stores/extensionsStore";
import { useSelectionStore } from "@/stores/selectionStore"; import { useSelectionStore, startSelectionStorePersistence } from "@/stores/selectionStore";
import { useServers } from "@/hooks/useServers"; import { useServers } from "@/hooks/useServers";
import { useDeepLinkManager } from "@/hooks/useDeepLinkManager"; import { useDeepLinkManager } from "@/hooks/useDeepLinkManager";
// import { useSelectionWindow } from "@/hooks/useSelectionWindow"; // import { useSelectionWindow } from "@/hooks/useSelectionWindow";
@@ -30,6 +30,11 @@ export default function LayoutOutlet() {
// Initialize selection store synchronization // Initialize selection store synchronization
useSelectionStore(); useSelectionStore();
// Initialize Tauri-backed persistence for selection store only in desktop mode.
useMount(() => {
startSelectionStorePersistence();
});
// init servers isTauri // init servers isTauri
useServers(); useServers();
// init deep link manager // init deep link manager
@@ -39,13 +44,11 @@ export default function LayoutOutlet() {
i18n.changeLanguage(language); i18n.changeLanguage(language);
}, [language]); }, [language]);
function updateBodyClass(path: string) { function updateBodyClass(_path: string) {
const body = document.body; const body = document.body;
body.classList.remove("input-body"); // Ensure rounded corners and clipping are applied to the whole window
// Tailwind v4 + Tauri: relying on container radius may not show at window edges
if (path === "/ui") { body.classList.add("input-body");
body.classList.add("input-body");
}
} }
useMount(async () => { useMount(async () => {

30
src/routes/web.tsx Normal file
View File

@@ -0,0 +1,30 @@
import { createBrowserRouter } from "react-router-dom";
import { Suspense, lazy } from "react";
import ErrorPage from "@/pages/error/index";
const WebPage = lazy(() => import("@/pages/web/index"));
const routerOptions = {
basename: "/",
future: {
v7_startTransition: true,
v7_relativeSplatPath: true,
},
} as const;
export const routerWeb = createBrowserRouter(
[
{
path: "/",
errorElement: <ErrorPage />,
element: (
<Suspense fallback={<></>}>
<WebPage />
</Suspense>
),
},
],
routerOptions
);

View File

@@ -1,5 +1,5 @@
import { create } from 'zustand'; import { create } from "zustand";
import { createTauriStore } from '@tauri-store/zustand'; import platformAdapter from "@/utils/platformAdapter";
type IconConfig = type IconConfig =
| { type: "lucide"; name: string; color?: string } | { type: "lucide"; name: string; color?: string }
@@ -37,9 +37,18 @@ export const useSelectionStore = create<SelectionStore>((set) => ({
setSelectionEnabled: (selectionEnabled) => set({ selectionEnabled }), setSelectionEnabled: (selectionEnabled) => set({ selectionEnabled }),
})); }));
// A handle to the Tauri plugin. /**
// We will need this to start the store. * Initialize Selection store persistence on Tauri only.
export const tauriHandler = createTauriStore('selection-store', useSelectionStore, { * In Web mode, this is a no-op to avoid loading Tauri-specific plugins.
saveOnChange: true, *
autoStart: true, * Returns a promise that resolves when persistence has been started on Tauri.
}); */
export async function startSelectionStorePersistence(): Promise<void> {
if (!platformAdapter.isTauri()) return;
const { createTauriStore } = await import("@tauri-store/zustand");
createTauriStore("selection-store", useSelectionStore, {
saveOnChange: true,
autoStart: true,
});
}

View File

@@ -1,741 +0,0 @@
/* @tailwind base; */
@tailwind components;
@tailwind utilities;
/* Base variables */
:host,
:root,
.searchbox-container,
.coco-container {
--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;
--foreground: #09090b;
--border: #e3e3e7;
--coco-primary-color: rgb(149, 5, 153);
}
/* Light theme */
.light.coco-container {
--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, 0.8);
--coco-modal-background: rgb(245, 246, 247);
--coco-modal-shadow: inset 1px 1px 0 0 rgba(255, 255, 255, 0.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, 0.4);
--coco-footer-background: #fff;
--coco-footer-shadow: 0 -1px 0 0 rgb(224, 227, 232),
0 -3px 6px 0 rgba(69, 98, 155, 0.12);
--coco-icon-color: rgb(21, 21, 21);
}
/* Dark theme */
.dark.coco-container {
--coco-primary-color: rgb(149, 5, 153);
--background: #09090b;
--foreground: #f9f9f9;
--border: #27272a;
--coco-text-color: rgb(245, 246, 247);
--coco-modal-container-background: rgba(9, 10, 17, 0.8);
--coco-modal-background: rgb(21, 23, 42);
--coco-modal-shadow: inset 1px 1px 0 0 rgb(44, 46, 64),
0 3px 8px 0 rgb(0, 3, 9);
--coco-searchbox-background: rgb(9, 10, 17);
--coco-searchbox-focus-background: #000;
--coco-hit-color: rgb(190, 195, 201);
--coco-hit-shadow: none;
--coco-hit-background: rgb(9, 10, 17);
--coco-key-gradient: linear-gradient(
-26.5deg,
rgb(86, 88, 114) 0%,
rgb(49, 53, 91) 100%
);
--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, 0.3);
--coco-footer-background: rgb(30, 33, 54);
--coco-footer-shadow: inset 0 1px 0 0 rgba(73, 76, 106, 0.5),
0 -4px 8px 0 rgba(0, 0, 0, 0.2);
--coco-muted-color: rgb(127, 132, 151);
--coco-icon-color: rgb(255, 255, 255);
}
@media (prefers-color-scheme: dark) {
.dark.coco-container {
--coco-primary-color: rgb(149, 5, 153);
--background: #09090b;
--foreground: #f9f9f9;
--border: #27272a;
--coco-text-color: rgb(245, 246, 247);
--coco-modal-container-background: rgba(9, 10, 17, 0.8);
--coco-modal-background: rgb(21, 23, 42);
--coco-modal-shadow: inset 1px 1px 0 0 rgb(44, 46, 64),
0 3px 8px 0 rgb(0, 3, 9);
--coco-searchbox-background: rgb(9, 10, 17);
--coco-searchbox-focus-background: #000;
--coco-hit-color: rgb(190, 195, 201);
--coco-hit-shadow: none;
--coco-hit-background: rgb(9, 10, 17);
--coco-key-gradient: linear-gradient(
-26.5deg,
rgb(86, 88, 114) 0%,
rgb(49, 53, 91) 100%
);
--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, 0.3);
--coco-footer-background: rgb(30, 33, 54);
--coco-footer-shadow: inset 0 1px 0 0 rgba(73, 76, 106, 0.5),
0 -4px 8px 0 rgba(0, 0, 0, 0.2);
--coco-muted-color: rgb(127, 132, 151);
--coco-icon-color: rgb(255, 255, 255);
}
}
/* html,
:host {
line-height: 1.5;
-webkit-text-size-adjust: 100%;
-moz-tab-size: 4;
-o-tab-size: 4;
tab-size: 4;
font-family: ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji",
"Segoe UI Emoji", Segoe UI Symbol, "Noto Color Emoji";
font-feature-settings: normal;
font-variation-settings: normal;
-webkit-tap-highlight-color: transparent;
} */
/* html {
height: 100%;
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
overscroll-behavior: none;
}
body,
#root {
height: 100%;
--tw-text-opacity: 1;
color: rgb(17 24 39 / var(--tw-text-opacity, 1));
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
} */
/* body {
margin: 0;
line-height: inherit;
} */
.dark body,
.dark #root,
.dark.coco-container,
.dark.coco-container {
--tw-text-opacity: 1;
color: rgb(243 244 246 / var(--tw-text-opacity, 1));
}
.input-body {
overflow: hidden;
border-radius: 0.375rem;
}
.icon {
width: 1em;
height: 1em;
vertical-align: -0.15em;
fill: currentColor;
overflow: hidden;
}
.\!container {
width: 100% !important;
}
.container {
width: 100%;
}
@media (min-width: 640px) {
.\!container {
max-width: 640px !important;
}
.container {
max-width: 640px;
}
}
@media (min-width: 768px) {
.\!container {
max-width: 768px !important;
}
.container {
max-width: 768px;
}
}
@media (min-width: 1024px) {
.\!container {
max-width: 1024px !important;
}
.container {
max-width: 1024px;
}
}
@media (min-width: 1280px) {
.\!container {
max-width: 1280px !important;
}
.container {
max-width: 1280px;
}
}
@media (min-width: 1536px) {
.\!container {
max-width: 1536px !important;
}
.container {
max-width: 1536px;
}
}
.coco-container {
line-height: 1.5;
-webkit-text-size-adjust: 100%;
-moz-tab-size: 4;
-o-tab-size: 4;
tab-size: 4;
font-family: ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji",
"Segoe UI Emoji", Segoe UI Symbol, "Noto Color Emoji";
font-feature-settings: normal;
font-variation-settings: normal;
-webkit-tap-highlight-color: transparent;
*,
:before,
:after {
--tw-border-spacing-x: 0;
--tw-border-spacing-y: 0;
--tw-translate-x: 0;
--tw-translate-y: 0;
--tw-rotate: 0;
--tw-skew-x: 0;
--tw-skew-y: 0;
--tw-scale-x: 1;
--tw-scale-y: 1;
--tw-pan-x: ;
--tw-pan-y: ;
--tw-pinch-zoom: ;
--tw-scroll-snap-strictness: proximity;
--tw-gradient-from-position: ;
--tw-gradient-via-position: ;
--tw-gradient-to-position: ;
--tw-ordinal: ;
--tw-slashed-zero: ;
--tw-numeric-figure: ;
--tw-numeric-spacing: ;
--tw-numeric-fraction: ;
--tw-ring-inset: ;
--tw-ring-offset-width: 0px;
--tw-ring-offset-color: #fff;
--tw-ring-color: rgb(59 130 246 / 0.5);
--tw-ring-offset-shadow: 0 0 #0000;
--tw-ring-shadow: 0 0 #0000;
--tw-shadow: 0 0 #0000;
--tw-shadow-colored: 0 0 #0000;
--tw-blur: ;
--tw-brightness: ;
--tw-contrast: ;
--tw-grayscale: ;
--tw-hue-rotate: ;
--tw-invert: ;
--tw-saturate: ;
--tw-sepia: ;
--tw-drop-shadow: ;
--tw-backdrop-blur: ;
--tw-backdrop-brightness: ;
--tw-backdrop-contrast: ;
--tw-backdrop-grayscale: ;
--tw-backdrop-hue-rotate: ;
--tw-backdrop-invert: ;
--tw-backdrop-opacity: ;
--tw-backdrop-saturate: ;
--tw-backdrop-sepia: ;
--tw-contain-size: ;
--tw-contain-layout: ;
--tw-contain-paint: ;
--tw-contain-style: ;
}
::backdrop {
--tw-border-spacing-x: 0;
--tw-border-spacing-y: 0;
--tw-translate-x: 0;
--tw-translate-y: 0;
--tw-rotate: 0;
--tw-skew-x: 0;
--tw-skew-y: 0;
--tw-scale-x: 1;
--tw-scale-y: 1;
--tw-pan-x: ;
--tw-pan-y: ;
--tw-pinch-zoom: ;
--tw-scroll-snap-strictness: proximity;
--tw-gradient-from-position: ;
--tw-gradient-via-position: ;
--tw-gradient-to-position: ;
--tw-ordinal: ;
--tw-slashed-zero: ;
--tw-numeric-figure: ;
--tw-numeric-spacing: ;
--tw-numeric-fraction: ;
--tw-ring-inset: ;
--tw-ring-offset-width: 0px;
--tw-ring-offset-color: #fff;
--tw-ring-color: rgb(59 130 246 / 0.5);
--tw-ring-offset-shadow: 0 0 #0000;
--tw-ring-shadow: 0 0 #0000;
--tw-shadow: 0 0 #0000;
--tw-shadow-colored: 0 0 #0000;
--tw-blur: ;
--tw-brightness: ;
--tw-contrast: ;
--tw-grayscale: ;
--tw-hue-rotate: ;
--tw-invert: ;
--tw-saturate: ;
--tw-sepia: ;
--tw-drop-shadow: ;
--tw-backdrop-blur: ;
--tw-backdrop-brightness: ;
--tw-backdrop-contrast: ;
--tw-backdrop-grayscale: ;
--tw-backdrop-hue-rotate: ;
--tw-backdrop-invert: ;
--tw-backdrop-opacity: ;
--tw-backdrop-saturate: ;
--tw-backdrop-sepia: ;
--tw-contain-size: ;
--tw-contain-layout: ;
--tw-contain-paint: ;
--tw-contain-style: ;
}
*,
:before,
:after {
box-sizing: border-box;
border-width: 0;
border-style: solid;
border-color: #e5e7eb;
}
:before,
:after {
--tw-content: "";
}
hr {
height: 0;
color: inherit;
border-top-width: 1px;
}
abbr:where([title]) {
-webkit-text-decoration: underline dotted;
text-decoration: underline dotted;
}
h1,
h2,
h3,
h4,
h5,
h6 {
font-size: inherit;
font-weight: inherit;
}
a {
color: inherit;
text-decoration: inherit;
}
b,
strong {
font-weight: bolder;
}
code,
kbd,
samp,
pre {
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,
Liberation Mono, Courier New, monospace;
font-feature-settings: normal;
font-variation-settings: normal;
font-size: 1em;
}
small {
font-size: 80%;
}
sub,
sup {
font-size: 75%;
line-height: 0;
position: relative;
vertical-align: baseline;
}
sub {
bottom: -0.25em;
}
sup {
top: -0.5em;
}
button,
input,
optgroup,
select,
textarea {
font-family: inherit;
font-feature-settings: inherit;
font-variation-settings: inherit;
font-size: 100%;
font-weight: inherit;
line-height: inherit;
letter-spacing: inherit;
color: inherit;
margin: 0;
padding: 0;
}
button,
select {
text-transform: none;
}
button,
input:where([type="button"]),
input:where([type="reset"]),
input:where([type="submit"]) {
-webkit-appearance: button;
background-color: transparent;
background-image: none;
}
:-moz-focusring {
outline: auto;
}
:-moz-ui-invalid {
box-shadow: none;
}
progress {
vertical-align: baseline;
}
::-webkit-inner-spin-button,
::-webkit-outer-spin-button {
height: auto;
}
[type="search"] {
-webkit-appearance: textfield;
outline-offset: -2px;
}
::-webkit-search-decoration {
-webkit-appearance: none;
}
::-webkit-file-upload-button {
-webkit-appearance: button;
font: inherit;
}
summary {
display: list-item;
}
blockquote,
dl,
dd,
h1,
h2,
h3,
h4,
h5,
h6,
hr,
figure,
p,
pre {
margin: inherit;
}
fieldset {
margin: 0;
padding: 0;
}
legend {
padding: 0;
}
ol,
ul,
menu {
list-style: none;
margin: inherit;
padding: inherit;
}
dialog {
padding: 0;
}
textarea {
resize: vertical;
}
input::-moz-placeholder,
textarea::-moz-placeholder {
opacity: 1;
color: #9ca3af;
}
input::placeholder,
textarea::placeholder {
opacity: 1;
color: #9ca3af;
}
button,
[role="button"] {
cursor: pointer;
}
:disabled {
cursor: default;
}
img,
svg,
video,
canvas,
audio,
iframe,
embed,
object {
display: block;
vertical-align: middle;
}
img,
video {
max-width: 100%;
height: auto;
}
[hidden]:where(:not([hidden="until-found"])) {
display: none;
}
* {
box-sizing: border-box;
border-color: var(--border);
}
.settings-input {
display: block;
width: 100%;
border-radius: 0.375rem;
--tw-border-opacity: 1;
border-color: rgb(209 213 219 / var(--tw-border-opacity, 1));
--tw-bg-opacity: 1;
background-color: rgb(255 255 255 / var(--tw-bg-opacity, 1));
--tw-text-opacity: 1;
color: rgb(17 24 39 / var(--tw-text-opacity, 1));
--tw-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05);
--tw-shadow-colored: 0 1px 2px 0 var(--tw-shadow-color);
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000),
var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
transition-property: color, background-color, border-color,
text-decoration-color, fill, stroke;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
transition-duration: 0.2s;
}
.settings-input:focus {
--tw-border-opacity: 1;
border-color: rgb(59 130 246 / var(--tw-border-opacity, 1));
--tw-ring-opacity: 1;
--tw-ring-color: rgb(59 130 246 / var(--tw-ring-opacity, 1));
}
.settings-input:is([data-theme="dark"] *) {
--tw-border-opacity: 1;
border-color: rgb(75 85 99 / var(--tw-border-opacity, 1));
--tw-bg-opacity: 1;
background-color: rgb(55 65 81 / var(--tw-bg-opacity, 1));
--tw-text-opacity: 1;
color: rgb(243 244 246 / var(--tw-text-opacity, 1));
}
.settings-select {
border-radius: 0.375rem;
--tw-border-opacity: 1;
border-color: rgb(209 213 219 / var(--tw-border-opacity, 1));
--tw-bg-opacity: 1;
background-color: rgb(255 255 255 / var(--tw-bg-opacity, 1));
font-size: 0.875rem;
line-height: 1.25rem;
--tw-text-opacity: 1;
color: rgb(17 24 39 / var(--tw-text-opacity, 1));
--tw-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05);
--tw-shadow-colored: 0 1px 2px 0 var(--tw-shadow-color);
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000),
var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
transition-property: color, background-color, border-color,
text-decoration-color, fill, stroke;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
transition-duration: 0.2s;
}
.settings-select:focus {
--tw-border-opacity: 1;
border-color: rgb(59 130 246 / var(--tw-border-opacity, 1));
--tw-ring-opacity: 1;
--tw-ring-color: rgb(59 130 246 / var(--tw-ring-opacity, 1));
}
.settings-select:is([data-theme="dark"] *) {
--tw-border-opacity: 1;
border-color: rgb(75 85 99 / var(--tw-border-opacity, 1));
--tw-bg-opacity: 1;
background-color: rgb(55 65 81 / var(--tw-bg-opacity, 1));
--tw-text-opacity: 1;
color: rgb(243 244 246 / var(--tw-text-opacity, 1));
}
}
/* Component styles */
@layer components {
.settings-input {
@apply block w-full rounded-[6px] border-gray-300 dark:border-gray-600
bg-white dark:bg-gray-700
text-gray-900 dark:text-gray-100
shadow-sm focus:border-blue-500 focus:ring-blue-500
transition-colors duration-200;
}
.settings-select {
@apply text-sm rounded-[6px] border-gray-300 dark:border-gray-600
bg-white dark:bg-gray-700
text-gray-900 dark:text-gray-100
shadow-sm focus:border-blue-500 focus:ring-blue-500
transition-colors duration-200;
}
}
/* Utility styles */
@layer utilities {
/* Scrollbar styles */
.custom-scrollbar {
scrollbar-width: thin;
scrollbar-color: #cbd5e1 transparent;
}
.custom-scrollbar::-webkit-scrollbar {
width: 6px;
}
.custom-scrollbar::-webkit-scrollbar-track {
background: transparent;
}
.custom-scrollbar::-webkit-scrollbar-thumb {
background-color: #cbd5e1;
border-radius: 3px;
}
.dark .custom-scrollbar {
scrollbar-color: #475569 transparent;
}
.dark .custom-scrollbar::-webkit-scrollbar-thumb {
background-color: #475569;
}
/* Background styles */
.bg-100 {
background-size: 100% 100%;
}
/* Error page styles */
#error-page {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
background: linear-gradient(to right, #f79c42, #f2d600);
font-family: "Arial", sans-serif;
color: #fff;
text-align: center;
padding: 0 20px;
}
.error-content {
background-color: rgba(0, 0, 0, 0.6);
padding: 40px;
border-radius: 8px;
box-shadow: 0px 10px 20px rgba(0, 0, 0, 0.2);
max-width: 500px;
width: 100%;
margin: 0 20px;
}
.error-title {
font-size: 60px;
font-weight: bold;
margin-bottom: 20px;
color: #f2d600;
}
.error-message {
font-size: 18px;
margin-bottom: 20px;
font-weight: 300;
color: #fff;
}
.error-details {
font-size: 16px;
color: #ffcc00;
font-style: italic;
}
.error-content button {
background-color: #f2d600;
border: none;
padding: 10px 20px;
font-size: 16px;
color: #333;
border-radius: 5px;
cursor: pointer;
transition: background-color 0.3s ease;
}
.error-content button:hover {
background-color: #f79c42;
}
/* coco styles */
.coco-modal-footer-commands-key {
display: flex;
align-items: center;
justify-content: center;
border-radius: 4px;
border: 0;
padding: 2px;
background: var(--coco-key-gradient);
box-shadow: var(--coco-key-shadow);
color: var(--coco-muted-color);
}
/* User selection styles */
.user-select {
-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
.user-select-text {
-webkit-touch-callout: text;
-webkit-user-select: text;
-khtml-user-select: text;
-moz-user-select: text;
-ms-user-select: text;
user-select: text;
}
}

Some files were not shown because too many files have changed in this diff Show More