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
out
src/components/web
SearchChatDemo/
web.md
# Editor directories and files
# .vscode/*
@@ -26,3 +28,5 @@ src/components/web
*.sln
*.sw?
.env
.trae

View File

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

View File

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

View File

@@ -8,9 +8,10 @@
"build": "tsc && vite build",
"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:beta": "cd dist/search-chat && npm publish --tag beta",
"publish:web:alpha": "cd dist/search-chat && npm publish --tag alpha",
"publish:web:rc": "cd dist/search-chat && npm publish --tag rc",
"publish:web:beta": "cd out/search-chat && npm publish --tag beta",
"publish:web:alpha": "cd out/search-chat && npm publish --tag alpha",
"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",
"tauri": "tauri",
"release": "release-it",
@@ -18,10 +19,18 @@
"release-beta": "release-it --preRelease=beta --preReleaseBase=1"
},
"dependencies": {
"@headlessui/react": "^2.2.2",
"@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-slider": "^1.2.1",
"@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/plugin-autostart": "~2.2.0",
"@tauri-apps/plugin-clipboard-manager": "~2.3.2",
@@ -77,6 +86,8 @@
"zustand": "^5.0.4"
},
"devDependencies": {
"@tailwindcss/postcss": "^4.1.17",
"@tailwindcss/vite": "^4.0.0",
"@tauri-apps/cli": "^2.5.0",
"@types/dom-speech-recognition": "^0.0.4",
"@types/lodash-es": "^4.17.12",
@@ -93,7 +104,7 @@
"postcss": "^8.5.3",
"release-it": "^18.1.2",
"sass": "^1.87.0",
"tailwindcss": "^3.4.17",
"tailwindcss": "^4.0.0",
"tsup": "^8.4.0",
"tsx": "^4.19.4",
"typescript": "^5.8.3",

1550
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,7 @@
export default {
plugins: {
tailwindcss: {},
// Tailwind v4 PostCSS plugin has moved to @tailwindcss/postcss
'@tailwindcss/postcss': {},
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,
} from "./tools";
type Fn = (data: FcResponse<any>) => unknown;
type Fn = (data: FcResponse<unknown>) => unknown;
interface IAnyObj {
[index: string]: unknown;
@@ -85,8 +85,26 @@ export const Get = <T>(
new Promise((resolve) => {
const appStore = JSON.parse(localStorage.getItem("app-store") || "{}");
let baseURL = appStore.state?.endpoint_http;
if (!baseURL || baseURL === "undefined") {
// In Vite dev, prefer using the proxy by keeping requests relative
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 = "";
}
@@ -117,8 +135,25 @@ export const Post = <T>(
return new Promise((resolve) => {
const appStore = JSON.parse(localStorage.getItem("app-store") || "{}");
let baseURL = appStore.state?.endpoint_http;
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 = "";
}

View File

@@ -41,15 +41,20 @@ const AssistantItem = memo(
)}
onClick={onClick}
>
<div className="flex items-center justify-center size-6 bg-white border border-[#E6E6E6] rounded-full overflow-hidden">
{_source?.icon?.startsWith("font_") ? (
<FontIcon name={_source?.icon} className="size-4" />
{_source?.icon?.startsWith("font_") ? (
<FontIcon
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="font-medium text-gray-900 dark:text-white truncate">
<div className="text-sm font-medium text-gray-900 dark:text-white truncate">
{_source?.name || "-"}
</div>
<div className="text-xs text-gray-500 dark:text-gray-400 truncate">

View File

@@ -1,8 +1,11 @@
import { useState, useRef, useCallback, useEffect } from "react";
import { ChevronDownIcon, RefreshCw } from "lucide-react";
import { useTranslation } from "react-i18next";
import { isNil } from "lodash-es";
import { Popover, PopoverButton, PopoverPanel } from "@headlessui/react";
import {
Popover,
PopoverTrigger,
PopoverContent,
} from "@/components/ui/popover";
import { useDebounce, useKeyPress, usePagination } from "ahooks";
import clsx from "clsx";
@@ -17,6 +20,7 @@ import { AssistantFetcher } from "./AssistantFetcher";
import AssistantItem from "./AssistantItem";
import Pagination from "@/components/Common/Pagination";
import { useSearchStore } from "@/stores/searchStore";
import { Button } from "../ui/button";
interface AssistantListProps {
assistantIDs?: string[];
@@ -83,6 +87,7 @@ export function AssistantList({ assistantIDs = [] }: AssistantListProps) {
const [highlightIndex, setHighlightIndex] = useState<number>(-1);
const [isKeyboardActive, setIsKeyboardActive] = useState(false);
const [open, setOpen] = useState(false);
useEffect(() => {
const targetId = askAiAssistantId ?? targetAssistantId;
@@ -105,7 +110,7 @@ export function AssistantList({ assistantIDs = [] }: AssistantListProps) {
useKeyPress(
["uparrow", "downarrow", "enter"],
(event, key) => {
const isClose = isNil(popoverButtonRef.current?.dataset["open"]);
const isClose = !open;
if (isClose) return;
@@ -161,26 +166,29 @@ export function AssistantList({ assistantIDs = [] }: AssistantListProps) {
}, []);
return (
<div className="relative">
<Popover ref={popoverRef}>
<PopoverButton
<div ref={popoverRef} className="relative">
<Popover
open={open}
onOpenChange={(v) => {
setOpen(v);
}}
>
<PopoverTrigger
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_") ? (
<FontIcon
name={currentAssistant._source.icon}
className="w-3 h-3"
/>
) : (
<img
src={logoImg}
className="w-3 h-3"
alt={t("assistant.message.logo")}
/>
)}
</div>
{currentAssistant?._source?.icon?.startsWith("font_") ? (
<FontIcon
name={currentAssistant._source.icon}
className="w-4 h-4"
/>
) : (
<img
src={logoImg}
className="w-4 h-4"
alt={t("assistant.message.logo")}
/>
)}
<div className="max-w-[100px] truncate">
{currentAssistant?._source?.name || "Coco AI"}
</div>
@@ -190,12 +198,14 @@ export function AssistantList({ assistantIDs = [] }: AssistantListProps) {
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>
</PopoverButton>
</PopoverTrigger>
<PopoverPanel
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"
<PopoverContent
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}
>
<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}
</div>
<button
<Button
variant="outline"
size="icon"
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}
>
<VisibleKey shortcut="R" onKeyPress={handleRefresh}>
@@ -218,7 +230,7 @@ export function AssistantList({ assistantIDs = [] }: AssistantListProps) {
)}
/>
</VisibleKey>
</button>
</Button>
</div>
<VisibleKey
@@ -234,8 +246,8 @@ export function AssistantList({ assistantIDs = [] }: AssistantListProps) {
autoFocus
value={keyword}
placeholder={t("assistant.popover.search")}
className="w-full h-8 px-2 bg-transparent border rounded-[6px] dark:border-white/10"
onChange={(event) => {
className="w-full h-8"
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
setKeyword(event.target.value);
}}
/>
@@ -272,7 +284,7 @@ export function AssistantList({ assistantIDs = [] }: AssistantListProps) {
<NoDataImage />
</div>
)}
</PopoverPanel>
</PopoverContent>
</Popover>
</div>
);

View File

@@ -388,7 +388,7 @@ const ChatAI = memo(
<div
data-tauri-drag-region
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
clearChat={clearChat}

View File

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

View File

@@ -28,7 +28,7 @@ const ConnectPrompt = () => {
<p className="mb-4 w-[388px]">{t("assistant.chat.connect_tip")}</p>
<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}
>
<span>{t("assistant.chat.connect")}</span>

View File

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

View File

@@ -122,7 +122,7 @@ const Splash = ({ assistantIDs = [], startPage }: SplashProps) => {
return (
<li key={id} className="mobile:w-full w-1/2 p-1">
<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={() => {
setCurrentAssistant(item);

View File

@@ -34,7 +34,7 @@ const AudioRecording: FC<AudioRecordingProps> = (props) => {
const state = useReactive({ ...INITIAL_STATE });
const containerRef = useRef<HTMLDivElement>(null);
const recordRef = useRef<RecordPlugin>();
const { withVisibility, addError } = useAppStore();
const { addError } = useAppStore();
const { currentService } = useConnectStore();
const { wavesurfer } = useWavesurfer({
@@ -146,7 +146,7 @@ const AudioRecording: FC<AudioRecordingProps> = (props) => {
};
const startRecording = async () => {
await withVisibility(checkPermission);
await checkPermission();
state.isRecording = true;
recordRef.current?.startRecording();
};
@@ -173,9 +173,9 @@ const AudioRecording: FC<AudioRecordingProps> = (props) => {
<div
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(
"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()}

View File

@@ -107,7 +107,7 @@ export const MessageActions = ({
<button
id={copyButtonId}
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 ? (
<Check
@@ -131,7 +131,7 @@ export const MessageActions = ({
{!isRefreshOnly && (
<button
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" : ""
}`}
>
@@ -151,7 +151,7 @@ export const MessageActions = ({
{!isRefreshOnly && (
<button
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" : ""
}`}
>
@@ -172,7 +172,7 @@ export const MessageActions = ({
<>
<button
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
className={`w-4 h-4 ${
@@ -191,7 +191,7 @@ export const MessageActions = ({
{question && (
<button
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" : ""
}`}
>

View File

@@ -59,7 +59,7 @@ export function DataSourceItem({ name, icon, connector }: DataSourceItemProps) {
{icon?.startsWith("font_") ? (
<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">

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">
{t("cloud.dataSource.title")}
<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()}
>
<RefreshCcw

View File

@@ -165,7 +165,7 @@ const LoginButton: FC<LoginButtonProps> = memo((props) => {
return (
<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}
aria-label={t("cloud.login")}
>
@@ -186,7 +186,7 @@ const LoadingState: FC<LoadingStateProps> = memo((props) => {
return (
<div className="flex items-center space-x-2 mb-3">
<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}
>
{t("cloud.cancel")}

View File

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

View File

@@ -53,7 +53,7 @@ export const Sidebar = forwardRef<{ refreshData: () => void }, SidebarProps>(
<img
src={item?.provider?.icon || cocoLogoImg}
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) => {
const target = e.target as HTMLImageElement;
target.src = cocoLogoImg;

View File

@@ -62,13 +62,13 @@ const ApiDetails: React.FC = () => {
{logs.map((log, index) => (
<div
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">
Latest Request {index + 1}:
</h4>
<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)}
</pre>
</div>
@@ -87,7 +87,7 @@ const ApiDetails: React.FC = () => {
</h4>
{showIndex === index ? (
<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)}
</pre>
</div>
@@ -98,7 +98,7 @@ const ApiDetails: React.FC = () => {
<>
<h4 className="font-semibold text-red-800 mt-4">Error:</h4>
<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)}
</pre>
</div>

View File

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

View File

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

View File

@@ -8,7 +8,7 @@ const Copyright = () => {
const renderLogo = () => {
return (
<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>
);
};

View File

@@ -1,24 +1,20 @@
import {
Button,
ButtonProps,
Description,
Dialog,
DialogPanel,
DialogTitle,
} from "@headlessui/react";
import { FC, KeyboardEvent } from "react";
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { FC, KeyboardEvent, ComponentProps } from "react";
import clsx from "clsx";
import { twMerge } from "tailwind-merge";
import VisibleKey from "./VisibleKey";
import { useTranslation } from "react-i18next";
type ShadButtonProps = ComponentProps<typeof Button>;
interface DeleteDialogProps {
isOpen: boolean;
title: string;
description: string;
deleteButtonProps?: ButtonProps;
cancelButtonProps?: ButtonProps;
deleteButtonProps?: ShadButtonProps;
cancelButtonProps?: ShadButtonProps;
reverseButtonPosition?: boolean;
setIsOpen: (isOpen: boolean) => void;
onCancel: () => void;
@@ -49,69 +45,60 @@ const DeleteDialog: FC<DeleteDialogProps> = (props) => {
};
return (
<Dialog
open={isOpen}
onClose={() => setIsOpen(false)}
className="relative z-1000"
>
<div
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>
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<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)]">
<DialogHeader className="mb-2">
<DialogTitle className="text-base font-bold">{title}</DialogTitle>
<DialogDescription className="text-sm">{description}</DialogDescription>
</DialogHeader>
<div
className={clsx("flex gap-4 self-end", {
"flex-row-reverse": reverseButtonPosition,
})}
<div
className={clsx("flex gap-4 self-end", {
"flex-row-reverse": reverseButtonPosition,
})}
>
<VisibleKey
shortcut="N"
shortcutClassName="left-[unset] right-0"
onKeyPress={onCancel}
>
<VisibleKey
shortcut="N"
shortcutClassName="left-[unset] right-0"
onKeyPress={onCancel}
<Button
{...cancelButtonProps}
autoFocus
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
{...cancelButtonProps}
autoFocus
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>
{t("deleteDialog.button.cancel")}
</Button>
</VisibleKey>
<VisibleKey
shortcut="Y"
shortcutClassName="left-[unset] right-0"
onKeyPress={onDelete}
<VisibleKey
shortcut="Y"
shortcutClassName="left-[unset] right-0"
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
{...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);
}}
>
{t("deleteDialog.button.delete")}
</Button>
</VisibleKey>
</div>
</DialogPanel>
</div>
{t("deleteDialog.button.delete")}
</Button>
</VisibleKey>
</div>
</DialogContent>
</Dialog>
);
};

View File

@@ -1,10 +1,11 @@
import {
Button,
Description,
Dialog,
DialogPanel,
DialogContent,
DialogHeader,
DialogTitle,
} from "@headlessui/react";
DialogDescription,
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { useTranslation } from "react-i18next";
import VisibleKey from "@/components/Common/VisibleKey";
@@ -36,69 +37,63 @@ const DeleteDialog = ({
};
return (
<Dialog
open={isOpen}
onClose={() => setIsOpen(false)}
className="relative z-1000"
>
<div
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-xl rounded-lg">
<div className="flex flex-col gap-3">
<DialogTitle className="text-base font-bold">
{t("history_list.delete_modal.title")}
</DialogTitle>
<Description className="text-sm">
{t("history_list.delete_modal.description", {
replace: [
active?._source?.title ||
active?._source?.message ||
active?._id,
],
})}
</Description>
</div>
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<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">
<DialogHeader className="mb-2">
<DialogTitle className="text-base font-bold">
{t("history_list.delete_modal.title")}
</DialogTitle>
<DialogDescription className="text-sm">
{t("history_list.delete_modal.description", {
replace: [
active?._source?.title ||
active?._source?.message ||
active?._id,
],
})}
</DialogDescription>
</DialogHeader>
<div className="flex gap-4 self-end">
<VisibleKey
shortcut="N"
shortcutClassName="left-[unset] right-0"
onKeyPress={() => setIsOpen(false)}
<div className="flex gap-4 self-end">
<VisibleKey
shortcut="N"
shortcutClassName="left-[unset] right-0"
onKeyPress={() => {
setIsOpen(false);
}}
>
<Button
variant="outline"
autoFocus
onClick={() => setIsOpen(false)}
onKeyDown={(event) => {
handleEnter(event, () => {
setIsOpen(false);
});
}}
>
<Button
autoFocus
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"
onClick={() => setIsOpen(false)}
onKeyDown={(event) => {
handleEnter(event, () => {
setIsOpen(false);
});
}}
>
{t("history_list.delete_modal.button.cancel")}
</Button>
</VisibleKey>
{t("history_list.delete_modal.button.cancel")}
</Button>
</VisibleKey>
<VisibleKey
shortcut="Y"
shortcutClassName="left-[unset] right-0"
onKeyPress={handleRemove}
<VisibleKey
shortcut="Y"
shortcutClassName="left-[unset] right-0"
onKeyPress={handleRemove}
>
<Button
variant="destructive"
className="text-white"
onClick={handleRemove}
onKeyDown={(event) => {
handleEnter(event, handleRemove);
}}
>
<Button
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"
onClick={handleRemove}
onKeyDown={(event) => {
handleEnter(event, handleRemove);
}}
>
{t("history_list.delete_modal.button.delete")}
</Button>
</VisibleKey>
</div>
</DialogPanel>
</div>
{t("history_list.delete_modal.button.delete")}
</Button>
</VisibleKey>
</div>
</DialogContent>
</Dialog>
);
};

View File

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

View File

@@ -1,10 +1,16 @@
import { FC, useRef, useCallback, useState } from "react";
import { Input, Popover, PopoverButton, PopoverPanel } from "@headlessui/react";
import { FC, useRef, useCallback, useState, useEffect } from "react";
import { Ellipsis } from "lucide-react";
import clsx from "clsx";
import { useTranslation } from "react-i18next";
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 VisibleKey from "../VisibleKey";
@@ -31,9 +37,11 @@ const HistoryListItem: FC<HistoryListItemProps> = ({
const moreButtonRef = useRef<HTMLButtonElement>(null);
const { _id, _source } = item;
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 [open, setOpen] = useState(false);
const onContextMenu = useCallback(
(e: React.MouseEvent) => {
@@ -72,24 +80,34 @@ const HistoryListItem: FC<HistoryListItemProps> = ({
},
];
useEffect(() => {
if (!(isSelected || isHovered) || isEdit) {
setOpen(false);
}
}, [isSelected, isHovered, isEdit]);
return (
<li
key={_id}
id={_id}
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={() => {
if (!isActive) {
if (!isSelected) {
setIsEdit(false);
}
onSelect(item);
}}
onMouseEnter={onMouseEnter}
onMouseLeave={() => {
setOpen(false);
}}
onContextMenu={onContextMenu}
>
<div
@@ -99,11 +117,11 @@ const HistoryListItem: FC<HistoryListItemProps> = ({
/>
<div className="flex-1 flex items-center justify-between gap-2 px-2 overflow-hidden">
{isEdit && isActive ? (
{isEdit && isSelected ? (
<Input
autoFocus
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) => {
if (event.key !== "Enter") return;
@@ -128,7 +146,7 @@ const HistoryListItem: FC<HistoryListItemProps> = ({
)}
<div className="flex items-center gap-2">
{isActive && !isEdit && (
{!isEdit && isSelected && (
<VisibleKey
shortcut="↑↓"
rootClassName="w-6"
@@ -136,56 +154,73 @@ const HistoryListItem: FC<HistoryListItemProps> = ({
/>
)}
<Popover>
{isActive && !isEdit && (
<PopoverButton ref={moreButtonRef} className="flex gap-2">
<VisibleKey
shortcut="O"
onKeyPress={() => {
moreButtonRef.current?.click();
}}
>
<Ellipsis className="size-4 text-[#979797]" />
</VisibleKey>
</PopoverButton>
)}
<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();
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger
ref={moreButtonRef}
className={clsx("flex gap-2", {
"opacity-100 pointer-events-auto":
!isEdit && (isSelected || isHovered),
"opacity-0 pointer-events-none": !(
!isEdit &&
(isSelected || isHovered)
),
})}
onClick={(e) => {
e.stopPropagation();
setOpen((prev) => !prev);
}}
>
{menuItems.map((menuItem) => {
const {
label,
icon: Icon,
shortcut,
iconColor,
onClick,
} = menuItem;
<VisibleKey
shortcut="O"
onKeyPress={() => {
moreButtonRef.current?.click();
}}
>
<Ellipsis className="size-4 text-[#979797]" />
</VisibleKey>
</PopoverTrigger>
return (
<button
key={label}
className="flex items-center gap-2 px-3 py-2 text-sm rounded-[6px] hover:bg-[#EDEDED] dark:hover:bg-[#2B2C31] transition"
onClick={onClick}
>
<VisibleKey shortcut={shortcut} onKeyPress={onClick}>
<Icon
className="size-4"
style={{
color: iconColor,
}}
/>
</VisibleKey>
<PopoverPortal>
<PopoverContent
side="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();
}}
onMouseLeave={() => {
setOpen(false);
}}
>
{menuItems.map((menuItem) => {
const {
label,
icon: Icon,
shortcut,
iconColor,
onClick,
} = menuItem;
<span>{t(label)}</span>
</button>
);
})}
</PopoverPanel>
return (
<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"
onClick={onClick}
>
<VisibleKey shortcut={shortcut} onKeyPress={onClick}>
<Icon
className="size-4"
style={{
color: iconColor,
}}
/>
</VisibleKey>
<span>{t(label)}</span>
</button>
);
})}
</PopoverContent>
</PopoverPortal>
</Popover>
</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 { FC, useMemo, useRef, useState } from "react";
import clsx from "clsx";
@@ -9,6 +9,7 @@ import VisibleKey from "../VisibleKey";
import { Chat } from "@/types/chat";
import { closeHistoryPanel } from "@/utils";
import HistoryListContent from "./HistoryListContent";
import { Button } from "@/components/ui/button";
interface HistoryListProps {
historyPanelId?: string;
@@ -57,21 +58,21 @@ const HistoryList: FC<HistoryListProps> = (props) => {
"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-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 gap-1 p-2 border-b border-input">
<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
shortcut="F"
onKeyPress={() => {
searchInputRef.current?.focus();
}}
>
<Search className="size-4 text-[#6B7280]" />
<Search className="size-4 text-muted-foreground" />
</VisibleKey>
<Input
autoFocus
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")}
onChange={(event) => {
debouncedSearch(event.target.value);
@@ -79,18 +80,20 @@ const HistoryList: FC<HistoryListProps> = (props) => {
/>
</div>
<div
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"
<Button
variant="outline"
size="icon"
className="size-8"
onClick={handleRefresh}
>
<VisibleKey shortcut="R" onKeyPress={handleRefresh}>
<RefreshCcw
className={clsx("size-4", {
className={clsx("size-4 text-[#0287FF]", {
"animate-spin": isRefresh,
})}
/>
</VisibleKey>
</div>
</Button>
</div>
<div className="flex-1 px-2 overflow-auto custom-scrollbar">
@@ -104,10 +107,10 @@ const HistoryList: FC<HistoryListProps> = (props) => {
</div>
{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">
<PanelLeftClose
className="size-4 text-black/80 dark:text-white/80 cursor-pointer"
className="size-4 text-muted-foreground cursor-pointer"
onClick={closeHistoryPanel}
/>
</VisibleKey>

View File

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

View File

@@ -1,6 +1,7 @@
import { ChevronLeft, ChevronRight } from "lucide-react";
import VisibleKey from "./VisibleKey";
import { cn } from "@/lib/utils";
interface PaginationProps {
current: number;
@@ -19,10 +20,15 @@ function Pagination({
}: PaginationProps) {
return (
<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}>
<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>
<div className="text-xs">
@@ -30,7 +36,12 @@ function Pagination({
</div>
<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>
</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 { 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;

View File

@@ -1,20 +1,20 @@
import { RefObject } from "react";
import clsx from "clsx";
import { ArrowDown } from "lucide-react";
import { Button } from "../ui/button";
interface ScrollToBottomProps {
scrollRef: RefObject<HTMLDivElement>;
isAtBottom: boolean;
}
const ScrollToBottom = ({
scrollRef,
isAtBottom,
}: ScrollToBottomProps) => {
const ScrollToBottom = ({ scrollRef, isAtBottom }: ScrollToBottomProps) => {
return (
<button
<Button
size="icon"
variant="outline"
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,
}
@@ -27,7 +27,7 @@ const ScrollToBottom = ({
}}
>
<ArrowDown className="size-5" />
</button>
</Button>
);
};

View File

@@ -1,40 +1,38 @@
import {
Popover,
PopoverButton,
PopoverPanel,
PopoverPanelProps,
} from "@headlessui/react";
import { FC, ReactNode } from "react";
import { useBoolean } from "ahooks";
import clsx from "clsx";
import { FC, ReactNode } from "react";
interface Tooltip2Props extends PopoverPanelProps {
import {
Popover,
PopoverTrigger,
PopoverContent,
} from "@/components/ui/popover";
interface Tooltip2Props {
content: string;
children: ReactNode;
className?: string;
}
const Tooltip2: FC<Tooltip2Props> = (props) => {
const { content, children, anchor = "top", ...rest } = props;
const { content, children, className } = props;
const [visible, { setTrue, setFalse }] = useBoolean(false);
return (
<Popover>
<PopoverButton onMouseOver={setTrue} onMouseOut={setFalse}>
<PopoverTrigger onMouseOver={setTrue} onMouseOut={setFalse}>
{children}
</PopoverButton>
<PopoverPanel
{...rest}
static
anchor={anchor}
</PopoverTrigger>
<PopoverContent
side="top"
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}
</PopoverPanel>
</PopoverContent>
</Popover>
);
};

View File

@@ -71,7 +71,7 @@ export default function Footer({ setIsPinnedWeb }: FooterProps) {
<div className="flex items-center gap-2">
<img
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>
</div>
@@ -81,7 +81,7 @@ export default function Footer({ setIsPinnedWeb }: FooterProps) {
if (visibleExtensionStore) {
return (
<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>
</div>
);
@@ -100,7 +100,7 @@ export default function Footer({ setIsPinnedWeb }: FooterProps) {
{hasUpdate ? (
<div className="cursor-pointer" onClick={() => setVisible(true)}>
<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>
) : (
sourceData?.source?.name ||
@@ -117,7 +117,7 @@ export default function Footer({ setIsPinnedWeb }: FooterProps) {
<div
data-tauri-drag-region={isTauri}
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,
}
@@ -137,7 +137,7 @@ export default function Footer({ setIsPinnedWeb }: FooterProps) {
</div>
</div>
) : (
<WebLogin panelClassName="bottom-5 left-0" />
<WebLogin side="top" align="start" />
)}
<div className={`flex mobile:hidden items-center gap-3`}>

View File

@@ -37,7 +37,7 @@ export const NoResults = () => {
<div className="flex gap-2">
<WebLoginButton />
<WebRefreshButton className="size-8" />
<WebRefreshButton />
</div>
</div>
);
@@ -54,7 +54,7 @@ export const NoResults = () => {
<span
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,
}
@@ -63,7 +63,7 @@ export const NoResults = () => {
{formatKey(modifierKey)}
</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}
</span>
</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";
const Footer = () => {
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">
<Menu as="div" className="relative">
<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">
<DropdownMenu>
<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
src={logoImg}
className="w-5 h-5 text-gray-600 dark:text-gray-400"
@@ -16,7 +19,7 @@ const Footer = () => {
Coco
</span>
{/* <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">
<div className="p-1">
@@ -27,7 +30,7 @@ const Footer = () => {
active
? "bg-gray-100 dark:bg-gray-700"
: "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" />
<Link to={`/`}>Home</Link>
@@ -41,7 +44,7 @@ const Footer = () => {
active
? "bg-gray-100 dark:bg-gray-700"
: "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" />
Profile
@@ -55,7 +58,7 @@ const Footer = () => {
active
? "bg-gray-100 dark:bg-gray-700"
: "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" />
<Link to={`settings`}>Settings</Link>
@@ -70,7 +73,7 @@ const Footer = () => {
active
? "bg-gray-100 dark:bg-gray-700"
: "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" />
Sign Out
@@ -79,7 +82,7 @@ const Footer = () => {
</MenuItem>
</div>
</MenuItems> */}
</Menu>
</DropdownMenu>
<div className="flex items-center space-x-4">
<span className="text-xs text-gray-500 dark:text-gray-400">

View File

@@ -111,7 +111,7 @@ const VisibleKey: FC<VisibleKeyProps> = (props) => {
{showTooltip && visibleShortcut ? (
<div
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
)}
>

View File

@@ -40,7 +40,7 @@ const AiOverview: FC<AiSummaryProps> = (props) => {
)}
>
<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={() => {
setVisible(false);
}}

View File

@@ -1,3 +1,4 @@
import { cn } from "@/lib/utils";
import { useAppStore } from "@/stores/appStore";
import { useWebConfigStore } from "@/stores/webConfigStore";
import { useBoolean } from "ahooks";
@@ -37,6 +38,7 @@ const AutoResizeTextarea = forwardRef<
setInput,
handleKeyDown,
chatPlaceholder,
lineCount,
onLineCountChange,
firstLineMaxWidth,
},
@@ -115,7 +117,12 @@ const AutoResizeTextarea = forwardRef<
autoComplete="off"
autoCapitalize="none"
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")}
aria-label={t("search.textarea.ariaLabel")}
value={input}

View File

@@ -12,7 +12,6 @@ import {
} from "lucide-react";
import { cloneElement, useEffect, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import { Input } from "@headlessui/react";
import { useOSKeyPress } from "@/hooks/useOSKeyPress";
import { useSearchStore } from "@/stores/searchStore";
@@ -292,9 +291,9 @@ const ContextMenu = ({ formatUrl }: ContextMenuProps) => {
ref={containerRef}
id={visibleContextMenu ? CONTEXT_MENU_PANEL_ID : ""}
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>
</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) => (
<kbd
key={key}
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,
}
@@ -363,7 +362,7 @@ const ContextMenu = ({ formatUrl }: ContextMenuProps) => {
searchInputRef.current?.focus();
}}
>
<Input
<input
ref={searchInputRef}
autoFocus
autoCorrect="off"

View File

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

View File

@@ -252,7 +252,7 @@ export default function ChatInput({
replace: [akiAiTooltipPrefix, askAI.name],
})}
</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")}
</div>
</div>
@@ -276,8 +276,8 @@ export default function ChatInput({
return (
<VisibleKey
shortcut={returnToInput}
rootClassName="flex-1 flex items-center justify-center"
shortcutClassName="!left-0 !translate-x-0"
rootClassName="flex-1 flex items-center justify-center w-full"
shortcutClassName="!left-auto !right-2 !translate-x-0"
>
<AutoResizeTextarea
ref={textareaRef}
@@ -308,14 +308,14 @@ export default function ChatInput({
<div className={`w-full relative`}>
<div
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()}
{visibleSearchBar() && (
<div
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,
}

View File

@@ -187,9 +187,9 @@ const InputControls = ({
{source?.type === "deep_think" && source?.config?.visible && (
<button
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}
@@ -250,7 +250,7 @@ const InputControls = ({
!visibleExtensionStore && (
<div
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
? "text-[#881c94]"

View File

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

View File

@@ -1,10 +1,14 @@
import { useState, useEffect, useCallback, useRef } from "react";
import { Popover, PopoverButton, PopoverPanel } from "@headlessui/react";
import { ChevronDownIcon, RefreshCw, Layers, Hammer } from "lucide-react";
import clsx from "clsx";
import { useTranslation } from "react-i18next";
import { useDebounce } from "ahooks";
import {
Popover,
PopoverTrigger,
PopoverContent,
} from "@/components/ui/popover";
import CommonIcon from "@/components/Common/Icons/CommonIcon";
import { useConnectStore } from "@/stores/connectStore";
import { useSearchStore } from "@/stores/searchStore";
@@ -16,6 +20,7 @@ import NoDataImage from "@/components/Common/NoDataImage";
import PopoverInput from "@/components/Common/PopoverInput";
import Pagination from "@/components/Common/Pagination";
import { SearchQuery } from "@/utils";
import { Button } from "../ui/button";
interface MCPPopoverProps {
mcp_servers: any;
@@ -79,6 +84,7 @@ export default function MCPPopover({
}, [currentService?.id, debouncedKeyword, getMCPByServer]);
const popoverButtonRef = useRef<HTMLButtonElement>(null);
const [open, setOpen] = useState(false);
const mcpSearch = useShortcutsStore((state) => state.mcpSearch);
const mcpSearchScope = useShortcutsStore((state) => {
return state.mcpSearchScope;
@@ -166,9 +172,9 @@ export default function MCPPopover({
return (
<div
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}
@@ -191,8 +197,14 @@ export default function MCPPopover({
{t("search.input.MCP")}
</span>
<Popover className="relative">
<PopoverButton ref={popoverButtonRef} className="flex items-center">
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger
ref={popoverButtonRef}
className="flex items-center"
onClick={(e) => {
e.stopPropagation();
}}
>
<VisibleKey
shortcut={mcpSearchScope}
onKeyPress={() => {
@@ -200,29 +212,35 @@ export default function MCPPopover({
}}
>
<ChevronDownIcon
className={clsx("size-3", [
className={clsx("size-3 cursor-pointer", [
isMCPActive
? "text-[#0072FF] dark:text-[#0072FF]"
: "text-[#333] dark:text-white",
])}
/>
</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
className="text-sm"
onClick={(e) => {
onClick={(e: React.MouseEvent<HTMLDivElement>) => {
e.stopPropagation();
}}
>
<div className="p-3">
<div className="p-2">
<div className="flex justify-between">
<span>{t("search.input.searchPopover.title")}</span>
<div
<Button
variant="outline"
size="icon"
className="size-6"
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}>
<RefreshCw
@@ -231,7 +249,7 @@ export default function MCPPopover({
}`}
/>
</VisibleKey>
</div>
</Button>
</div>
<div className="relative h-8 my-2">
@@ -250,7 +268,7 @@ export default function MCPPopover({
value={keyword}
ref={searchInputRef}
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);
}}
/>
@@ -280,7 +298,7 @@ export default function MCPPopover({
>
<div className="flex items-center gap-2 overflow-hidden">
{isAll ? (
<Layers className="size-[16px] text-[#0287FF]" />
<Layers className="min-w-4 min-h-4 size-4 text-[#0287FF]" />
) : (
<CommonIcon
item={item}
@@ -290,7 +308,7 @@ export default function MCPPopover({
"default_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
checked={isChecked()}
indeterminate={isAll}
@@ -339,7 +357,7 @@ export default function MCPPopover({
/>
)}
</div>
</PopoverPanel>
</PopoverContent>
</Popover>
</>
)}

View File

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

View File

@@ -1,15 +1,16 @@
import { useSearchStore } from "@/stores/searchStore";
import { ChevronLeft, Search } from "lucide-react";
import { FC } from "react";
import clsx from "clsx";
import FontIcon from "@/components/Common/Icons/FontIcon";
import { FC } from "react";
import lightDefaultIcon from "@/assets/images/source_default.png";
import darkDefaultIcon from "@/assets/images/source_default_dark.png";
import { useThemeStore } from "@/stores/themeStore";
import platformAdapter from "@/utils/platformAdapter";
import { navigateBack, visibleSearchBar } from "@/utils";
import VisibleKey from "../Common/VisibleKey";
import clsx from "clsx";
import { cn } from "@/lib/utils";
interface MultilevelWrapperProps {
title?: string;
@@ -36,7 +37,7 @@ const MultilevelWrapper: FC<MultilevelWrapperProps> = (props) => {
<div
data-tauri-drag-region
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(),
"w-[calc(100vw-16px)] rounded-r-lg": !visibleSearchBar(),
@@ -50,7 +51,7 @@ const MultilevelWrapper: FC<MultilevelWrapperProps> = (props) => {
/>
</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>
</div>
@@ -115,7 +116,14 @@ export default function SearchIcons({
}
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]" />
</div>
);

View File

@@ -1,10 +1,14 @@
import { useState, useEffect, useCallback, useRef } from "react";
import { Popover, PopoverButton, PopoverPanel } from "@headlessui/react";
import { ChevronDownIcon, RefreshCw, Layers, Globe } from "lucide-react";
import clsx from "clsx";
import { useTranslation } from "react-i18next";
import { useDebounce } from "ahooks";
import {
Popover,
PopoverTrigger,
PopoverContent,
} from "@/components/ui/popover";
import CommonIcon from "@/components/Common/Icons/CommonIcon";
import { useConnectStore } from "@/stores/connectStore";
import { useSearchStore } from "@/stores/searchStore";
@@ -15,6 +19,7 @@ import VisibleKey from "@/components/Common/VisibleKey";
import NoDataImage from "@/components/Common/NoDataImage";
import PopoverInput from "@/components/Common/PopoverInput";
import Pagination from "@/components/Common/Pagination";
import { Button } from "../ui/button";
interface SearchPopoverProps {
datasource: any;
@@ -85,6 +90,7 @@ export default function SearchPopover({
}, [currentService?.id, debouncedKeyword, getDataSourcesByServer]);
const popoverButtonRef = useRef<HTMLButtonElement>(null);
const [open, setOpen] = useState(false);
const internetSearch = useShortcutsStore((state) => state.internetSearch);
const internetSearchScope = useShortcutsStore((state) => {
return state.internetSearchScope;
@@ -172,9 +178,9 @@ export default function SearchPopover({
return (
<div
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}
@@ -199,8 +205,14 @@ export default function SearchPopover({
{t("search.input.search")}
</span>
<Popover className="relative">
<PopoverButton ref={popoverButtonRef} className="flex items-center">
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger
ref={popoverButtonRef}
className="flex items-center"
onClick={(e) => {
e.stopPropagation();
}}
>
<VisibleKey
shortcut={internetSearchScope}
onKeyPress={() => {
@@ -208,29 +220,35 @@ export default function SearchPopover({
}}
>
<ChevronDownIcon
className={clsx("size-3", [
className={clsx("size-3 cursor-pointer", [
isSearchActive
? "text-[#0072FF] dark:text-[#0072FF]"
: "text-[#333] dark:text-white",
])}
/>
</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
className="text-sm"
onClick={(e) => {
onClick={(e: React.MouseEvent<HTMLDivElement>) => {
e.stopPropagation();
}}
>
<div className="p-3">
<div className="p-2">
<div className="flex justify-between">
<span>{t("search.input.searchPopover.title")}</span>
<div
<Button
variant="outline"
size="icon"
className="size-6"
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}>
<RefreshCw
@@ -239,7 +257,7 @@ export default function SearchPopover({
}`}
/>
</VisibleKey>
</div>
</Button>
</div>
<div className="relative h-8 my-2">
@@ -258,7 +276,7 @@ export default function SearchPopover({
value={keyword}
ref={searchInputRef}
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);
}}
/>
@@ -288,7 +306,7 @@ export default function SearchPopover({
>
<div className="flex items-center gap-2 overflow-hidden">
{isAll ? (
<Layers className="size-[16px] text-[#0287FF]" />
<Layers className="size-4 text-[#0287FF]" />
) : (
<CommonIcon
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
checked={isChecked()}
indeterminate={isAll}
@@ -347,7 +365,7 @@ export default function SearchPopover({
/>
)}
</div>
</PopoverPanel>
</PopoverContent>
</Popover>
</>
)}

View File

@@ -35,7 +35,10 @@ import {
visibleSearchBar,
} from "@/utils";
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 { useSearchStore } from "@/stores/searchStore";
@@ -383,11 +386,11 @@ function SearchChat({
<div
data-tauri-drag-region={isTauri}
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
? "bg-bottom bg-chat_bg_light dark:bg-chat_bg_dark"
: "bg-top bg-search_bg_light dark:bg-search_bg_dark",
? "bg-bottom bg-[url('/assets/chat_bg_light.png')] dark:bg-[url('/assets/chat_bg_dark.png')]"
: "bg-top bg-[url('/assets/search_bg_light.png')] dark:bg-[url('/assets/search_bg_dark.png')]",
],
{
"size-full": !isTauri,
@@ -438,7 +441,7 @@ function SearchChat({
{!hideMiddleBorder && (
<div
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"
)}
/>

View File

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

View File

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

View File

@@ -117,7 +117,7 @@ const SelectionSettings = () => {
<h2 className="text-lg font-semibold">{t("selection.title")}</h2>
</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="rounded-xl border border-gray-200 bg-white/70 shadow-sm dark:border-gray-700 dark:bg-gray-900/40">
<HeaderToolbar
@@ -148,7 +148,7 @@ const SelectionSettings = () => {
</SettingsItem>
{selectionEnabled && (
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
<div className="grid grid-cols-1 gap-4">
<SettingsItem
icon={Sparkles}
title={t("selection.display.title")}

View File

@@ -1,7 +1,14 @@
import { useTranslation } from "react-i18next";
import { Command, RotateCcw } from "lucide-react";
import {
Select,
SelectTrigger,
SelectContent,
SelectItem,
SelectValue,
} from "@/components/ui/select";
import { useEffect } from "react";
import { Button } from "@headlessui/react";
import { Button } from "@/components/ui/button";
import clsx from "clsx";
import { formatKey } from "@/utils/keyboardUtils";
@@ -246,21 +253,21 @@ const Shortcuts = () => {
title={t("settings.advanced.shortcuts.modifierKey.title")}
description={t("settings.advanced.shortcuts.modifierKey.description")}
>
<select
<Select
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"
onChange={(event) => {
setModifierKey(event.target.value as ModifierKey);
}}
onValueChange={(v) => setModifierKey(v as ModifierKey)}
>
{modifierKeys.map((item) => {
return (
<option key={item} value={item}>
<SelectTrigger className="h-8 w-40">
<SelectValue />
</SelectTrigger>
<SelectContent>
{modifierKeys.map((item) => (
<SelectItem key={item} value={item}>
{formatKey(item)}
</option>
);
})}
</select>
</SelectItem>
))}
</SelectContent>
</Select>
</SettingsItem>
{list.map((item) => {
@@ -279,6 +286,7 @@ const Shortcuts = () => {
<span>{formatKey(modifierKey)}</span>
<span>+</span>
<SettingsInput
className="w-20"
value={value}
max={1}
onChange={(value) => {
@@ -287,23 +295,14 @@ const Shortcuts = () => {
/>
<Button
variant="outline"
disabled={disabled}
className={clsx(
"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,
}
)}
size="icon"
onClick={() => {
handleChange(initialValue, setValue);
}}
>
<RotateCcw
className={clsx("size-4 text-[#999]", {
"!text-[#0072FF]": !disabled,
})}
/>
<RotateCcw className={clsx("size-4 opacity-80")} />
</Button>
</div>
</SettingsItem>

View File

@@ -21,8 +21,15 @@ import SettingsInput from "@/components//Settings/SettingsInput";
import platformAdapter from "@/utils/platformAdapter";
import UpdateSettings from "./components/UpdateSettings";
import SettingsToggle from "../SettingsToggle";
// import SelectionSettings from "./components/Selection";
// import { isMac } from "@/utils/platform";
import SelectionSettings from "./components/Selection";
import { isMac } from "@/utils/platform";
import {
Select,
SelectTrigger,
SelectContent,
SelectItem,
SelectValue,
} from "@/components/ui/select";
const Advanced = () => {
const { t } = useTranslation();
@@ -169,29 +176,27 @@ const Advanced = () => {
title={t(title)}
description={t(description)}
>
<select
value={value}
onChange={(event) => {
onChange(event.target.value as never);
}}
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;
return (
<option key={value} value={value}>
{t(label)}
</option>
);
})}
</select>
<Select value={value as string} onValueChange={(v) => onChange(v as never)}>
<SelectTrigger className="h-8 w-44">
<SelectValue className="truncate" />
</SelectTrigger>
<SelectContent>
{items.map((item) => {
const { label, value } = item;
return (
<SelectItem key={value} value={value as string}>
{t(label)}
</SelectItem>
);
})}
</SelectContent>
</Select>
</SettingsItem>
);
})}
</div>
{/* {isMac && <SelectionSettings />} */}
{isMac && <SelectionSettings />}
<Shortcuts />
@@ -278,33 +283,35 @@ const Advanced = () => {
"settings.advanced.other.localSearchResultWeight.description"
)}
>
<select
value={localSearchResultWeight}
onChange={(event) => {
const weight = Number(event.target.value);
<Select
value={String(localSearchResultWeight)}
onValueChange={(v) => {
const weight = Number(v);
setLocalSearchResultWeight(weight);
platformAdapter.invokeBackend("set_local_query_source_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">
{t("settings.advanced.other.localSearchResultWeight.options.low")}
</option>
<option value="1">
{t(
"settings.advanced.other.localSearchResultWeight.options.medium"
)}
</option>
<option value="2">
{t(
"settings.advanced.other.localSearchResultWeight.options.high"
)}
</option>
</select>
<SelectTrigger className="h-8 w-44">
<SelectValue className="truncate" />
</SelectTrigger>
<SelectContent>
<SelectItem value="0.5">
{t("settings.advanced.other.localSearchResultWeight.options.low")}
</SelectItem>
<SelectItem value="1">
{t(
"settings.advanced.other.localSearchResultWeight.options.medium"
)}
</SelectItem>
<SelectItem value="2">
{t(
"settings.advanced.other.localSearchResultWeight.options.high"
)}
</SelectItem>
</SelectContent>
</Select>
</SettingsItem>
<SettingsItem

View File

@@ -13,6 +13,7 @@ import Shortcut from "../Shortcut";
import SettingsToggle from "@/components/Settings/SettingsToggle";
import { platform } from "@/utils/platform";
import { useExtensionsStore } from "@/stores/extensionsStore";
import { cn } from "@/lib/utils";
const Content = () => {
const { rootState } = useContext(ExtensionsContext);
@@ -165,7 +166,9 @@ const Item: FC<ItemProps> = (props) => {
<SettingsInput
defaultValue={alias}
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) => {
handleChange(String(value));
}}
@@ -292,7 +295,7 @@ const Item: FC<ItemProps> = (props) => {
return (
<>
<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":
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")}
</div>
@@ -88,9 +88,7 @@ const AiOverview = () => {
return (
<div>
<div className="mb-2 text-[#666] dark:text-white/70">
{label}
</div>
<div className="mb-2">{label}</div>
<SettingsInput
type="number"

View File

@@ -1,4 +1,4 @@
import { Button } from "@headlessui/react";
import { Button } from "@/components/ui/button";
import { useMount } from "ahooks";
import { useState } from "react";
import { useTranslation } from "react-i18next";
@@ -35,15 +35,13 @@ const Applications = () => {
return (
<>
<div className="text-[#999]">
<p className="font-bold mb-2">
{t("settings.extensions.application.details.searchScope")}
</p>
<p className="mb-2">
{t("settings.extensions.application.details.searchScope")}
</p>
<p>
{t("settings.extensions.application.details.searchScopeDescription")}
</p>
</div>
<p className="text-[#999]">
{t("settings.extensions.application.details.searchScopeDescription")}
</p>
<DirectoryScope
paths={paths}
@@ -72,18 +70,18 @@ const Applications = () => {
}}
/>
<div className="text-[#999] mt-4">
<p className="font-bold mb-2">
{t("settings.extensions.application.details.rebuildIndex")}
</p>
<p className="mt-4 mb-2">
{t("settings.extensions.application.details.rebuildIndex")}
</p>
<p>
{t("settings.extensions.application.details.rebuildIndexDescription")}
</p>
</div>
<p className="text-[#999]">
{t("settings.extensions.application.details.rebuildIndexDescription")}
</p>
<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}
>
{t("settings.extensions.application.details.reindex")}

View File

@@ -1,6 +1,6 @@
import { useAppStore } from "@/stores/appStore";
import platformAdapter from "@/utils/platformAdapter";
import { Button } from "@headlessui/react";
import { Button } from "@/components/ui/button";
import clsx from "clsx";
import { castArray } from "lodash-es";
import { Folder, SquareArrowOutUpRight, X } from "lucide-react";
@@ -82,7 +82,7 @@ const DirectoryScope: FC<DirectoryScopeProps> = (props) => {
return (
<div
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">
<Folder className="size-4" />
@@ -112,7 +112,9 @@ const DirectoryScope: FC<DirectoryScopeProps> = (props) => {
)}
<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}
>
{t("settings.extensions.directoryScope.button.addDirectories")}

View File

@@ -82,7 +82,7 @@ const FileSearch = () => {
{t("settings.extensions.fileSearch.description")}
</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")}
</div>
@@ -99,10 +99,7 @@ const FileSearch = () => {
return (
<>
<div
key={label}
className="mt-4 mb-2 text-[#666] dark:text-white/70"
>
<div key={label} className="mt-4 mb-2">
{label}
</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")}
</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) => {
return (
<div
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>
@@ -140,7 +137,7 @@ const FileSearch = () => {
<SettingsInput
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) => {
if (event.code !== "Enter") return;

View File

@@ -4,7 +4,13 @@ import { isArray } from "lodash-es";
import { useAsyncEffect, useMount } from "ahooks";
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 { ExtensionId } from "@/components/Settings/Extensions/index";
import { useConnectStore } from "@/stores/connectStore";
@@ -175,27 +181,40 @@ const SharedAi: FC<SharedAiProps> = (props) => {
<>
<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")}
</div>
{selectList.map((item) => {
const { label, value, data, searchable, onChange, onSearch } = item;
const { label, value, data, searchable, onChange } = item;
return (
<div key={label} className="mt-4">
<div className="mb-2 text-[#666] dark:text-white/70">{label}</div>
<SettingsSelectPro
<Select
value={value}
options={data}
searchable={searchable}
onChange={onChange}
onSearch={onSearch}
placeholder={
isLoadingAssistants && searchable ? "Loading..." : undefined
}
/>
onValueChange={(v) => onChange?.(v)}
disabled={searchable && isLoadingAssistants}
>
<SelectTrigger className="ml-1 h-9 w-full max-w-[480px]">
<SelectValue
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>
);
})}

View File

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

View File

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

View File

@@ -36,6 +36,13 @@ import {
} from "@/commands";
import platformAdapter from "@/utils/platformAdapter";
import { useAppearanceStore, WindowMode } from "@/stores/appearanceStore";
import {
Select,
SelectTrigger,
SelectContent,
SelectItem,
SelectValue,
} from "@/components/ui/select";
export function ThemeOption({
icon: Icon,
@@ -83,8 +90,6 @@ export default function GeneralSettings() {
const { showTooltip, setShowTooltip, language, setLanguage } = useAppStore();
const { windowMode, setWindowMode } = useAppearanceStore();
const fetchAutoStartStatus = async () => {
if (isTauri()) {
try {
@@ -283,7 +288,7 @@ export default function GeneralSettings() {
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",
{
"!border-blue-500 bg-blue-50 dark:bg-blue-900/20":
"border-blue-500! bg-blue-50! dark:bg-blue-900/20!":
isSelected,
}
)}
@@ -307,28 +312,31 @@ export default function GeneralSettings() {
})}
</div>
<SettingsItem
icon={Globe}
title={t("settings.language.title")}
description={t("settings.language.description")}
>
<div className="flex items-center gap-2">
<select
<Select
value={currentLanguage}
onChange={(event) => {
const lang = event.currentTarget.value;
onValueChange={(lang) => {
setLanguage(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>
<option value="zh">{t("settings.language.chinese")}</option>
</select>
<SelectTrigger className="h-8 w-44">
<SelectValue className="truncate" />
</SelectTrigger>
<SelectContent>
<SelectItem value="en">
{t("settings.language.english")}
</SelectItem>
<SelectItem value="zh">
{t("settings.language.chinese")}
</SelectItem>
</SelectContent>
</Select>
</div>
</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 { FC, FocusEvent } from "react";
import { FC, FocusEvent, InputHTMLAttributes } from "react";
import { twMerge } from "tailwind-merge";
interface SettingsInputProps
extends Omit<InputProps, "onChange" | "className"> {
extends Omit<
InputHTMLAttributes<HTMLInputElement>,
"onChange" | "className"
> {
className?: string;
onChange?: (value?: string | number) => void;
}
@@ -35,10 +38,7 @@ const SettingsInput: FC<SettingsInputProps> = (props) => {
<Input
{...rest}
autoCorrect="off"
className={twMerge(
"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
)}
className={twMerge("w-44 h-8", className)}
onBlur={handleBlur}
onChange={(event) => {
onChange?.(event.target.value);

View File

@@ -7,7 +7,7 @@ interface SettingsPanelProps {
const SettingsPanel: React.FC<SettingsPanelProps> = ({ children }) => {
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> */}
{children}
</div>

View File

@@ -1,7 +1,8 @@
import { useBoolean, useClickAway, useDebounce } from "ahooks";
import clsx from "clsx";
import { FC, useEffect, useMemo, useRef, useState } from "react";
import SettingsInput from "./SettingsInput";
import { Input } from "@/components/ui/input";
import NoDataImage from "../Common/NoDataImage";
interface SettingsSelectProProps {
@@ -47,7 +48,7 @@ const SettingsSelectPro: FC<SettingsSelectProProps> = (props) => {
return (
<div ref={containerRef} className="relative">
<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}
>
{option?.[labelField] ?? (
@@ -57,7 +58,7 @@ const SettingsSelectPro: FC<SettingsSelectProProps> = (props) => {
<div
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,
}
@@ -65,12 +66,12 @@ const SettingsSelectPro: FC<SettingsSelectProProps> = (props) => {
>
{searchable && (
<div className="px-2 mb-2">
<SettingsInput
<Input
autoFocus
value={searchValue}
className="w-full"
onChange={(value) => {
setSearchValue(String(value));
className="w-full h-8 border-0 shadow-none focus-visible:ring-0 focus-visible:ring-offset-0"
onChange={(e) => {
setSearchValue(String(e.target.value));
}}
/>
</div>
@@ -83,9 +84,9 @@ const SettingsSelectPro: FC<SettingsSelectProProps> = (props) => {
<div
key={item?.[valueField] ?? index}
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],
}
)}

View File

@@ -1,28 +1,26 @@
import { Switch, SwitchProps } from "@headlessui/react";
import { Switch } from "@/components/ui/switch";
import clsx from "clsx";
interface SettingsToggleProps extends SwitchProps {
type BaseSwitchProps = React.ComponentProps<typeof Switch>;
interface SettingsToggleProps
extends Omit<BaseSwitchProps, "onChange" | "onCheckedChange"> {
label: string;
className?: string;
onChange?: (checked: boolean) => void;
}
export default function SettingsToggle(props: SettingsToggleProps) {
const { label, className, ...rest } = props;
const { label, className, onChange, ...rest } = props;
return (
<Switch
{...rest}
aria-label={label}
onCheckedChange={(v) => onChange?.(v)}
className={clsx(
`group relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent
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`,
"h-5 w-9",
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 { 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 { noop } from "lodash-es";
import { LoaderCircle, X } from "lucide-react";
import { useInterval, useReactive } from "ahooks";
import clsx from "clsx";
@@ -141,117 +141,107 @@ const UpdateApp = ({ isCheckPage }: UpdateAppProps) => {
return (
<Dialog
open={isCheckPage ? true : visible}
as="div"
id="update-app-dialog"
className="relative z-10 focus:outline-none"
onClose={noop}
onOpenChange={(v) => {
if (!isCheckPage) setVisible(v);
}}
>
<div
className={`fixed inset-0 z-10 w-screen overflow-y-auto ${
<DialogContent
id="update-app-dialog"
overlayClassName={clsx("bg-transparent backdrop-blur-0 rounded-xl")}
className={clsx(
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
data-tauri-drag-region
className={clsx(
"flex min-h-full items-center justify-center",
!isCheckPage && "p-4"
"w-full flex flex-col items-center justify-center px-6",
isCheckPage && "h-full"
)}
>
<DialogPanel
transition
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
{!isCheckPage && isOptional && (
<X
className={clsx(
"mb-3 mt-6 bg-[#0072FF] text-white text-sm px-[14px] py-[8px] rounded-lg",
cursorClassName,
state.loading && "opacity-50"
"absolute h-5 w-5 top-3 right-3 text-muted-foreground",
cursorClassName
)}
onClick={updateInfo ? handleDownload : handleSkip}
>
{state.loading ? (
<div className="flex justify-center items-center gap-2">
<LoaderCircle className="animate-spin size-5" />
{percent}%
</div>
) : updateInfo ? (
t("update.button.install")
) : (
t("update.button.ok")
)}
</Button>
onClick={handleCancel}
role="button"
aria-label="Close dialog"
tabIndex={0}
/>
)}
{updateInfo && isOptional && (
<div
className={clsx("text-xs text-[#999]", cursorClassName)}
onClick={handleSkip}
>
{t("update.skip_version")}
</div>
<img src={isDark ? darkIcon : lightIcon} className="h-6" />
<div className="text-sm leading-5 py-2 text-foreground text-center">
{updateInfo ? (
isOptional ? (
t("update.optional_description")
) : (
<>
<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>
</DialogContent>
</Dialog>
);
};

View File

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

View File

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

View File

@@ -12,7 +12,7 @@ const UserAvatar: FC<UserAvatarProps> = (props) => {
return (
<div
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
)}
>

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

View File

@@ -1,52 +1,46 @@
import * as React from "react";
import { Slot } from "@radix-ui/react-slot";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
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: {
variant: {
default:
"bg-primary text-primary-foreground shadow 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",
"bg-primary text-primary-foreground hover:bg-primary/90",
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",
link: "text-primary underline-offset-4 hover:underline",
destructive:
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
},
size: {
default: "h-9 px-4 py-2",
sm: "h-8 rounded-[6px] px-3 text-xs",
lg: "h-10 rounded-[6px] px-8",
sm: "h-8 px-3",
md: "h-9 px-4",
lg: "h-10 px-6",
icon: "h-9 w-9",
},
},
defaultVariants: {
variant: "default",
size: "default",
size: "md",
},
}
);
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean;
}
VariantProps<typeof buttonVariants> {}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button";
({ className, variant, size, ...props }, ref) => {
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
<button
ref={ref}
className={cn(buttonVariants({ variant, size }), className)}
{...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 SeparatorPrimitive from "@radix-ui/react-separator"
import { cn } from "@/lib/utils"
import * as React from "react";
import * as SeparatorPrimitive from "@radix-ui/react-separator";
import { cn } from "@/lib/utils";
const Separator = React.forwardRef<
React.ElementRef<typeof SeparatorPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
>(
(
{ className, orientation = "horizontal", decorative = true, ...props },
ref
) => (
<SeparatorPrimitive.Root
ref={ref}
decorative={decorative}
orientation={orientation}
className={cn(
"shrink-0 bg-border",
orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]",
className
)}
{...props}
/>
)
)
Separator.displayName = SeparatorPrimitive.Root.displayName
>(({ className, orientation = "horizontal", decorative, ...props }, ref) => (
<SeparatorPrimitive.Root
ref={ref}
decorative={decorative}
orientation={orientation}
className={cn(
"shrink-0 bg-border",
orientation === "horizontal" ? "h-px w-full" : "h-full w-px",
className
)}
{...props}
/>
));
Separator.displayName = SeparatorPrimitive.Root.displayName;
export { Separator };
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";

View File

@@ -1,8 +1,14 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@import "tailwindcss";
/* Tailwind v4: ensure class extraction scans our source files */
@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 */
.coco-container,
:root {
--spacing-base: 12px;
--modal-width: 560px;
@@ -11,13 +17,126 @@
--hit-height: 56px;
--footer-height: 44px;
--icon-stroke-width: 1.4;
--background: #ffffff;
--foreground: #09090b;
--border: #e3e3e7;
--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"] {
--coco-primary-color: rgb(149, 5, 153);
--coco-text-color: rgb(28, 30, 33);
@@ -45,12 +164,45 @@
--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"] {
/* 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);
--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);
@@ -75,137 +227,76 @@
--coco-icon-color: rgb(255, 255, 255);
}
/* Base styles */
/* Base styles (scoped to coco container to avoid global overrides) */
@layer base {
* {
@apply box-border border-[--border] outline-none;
}
html {
@apply h-full overscroll-none select-none;
@apply overscroll-none;
}
body,
#root {
@apply h-full text-gray-900 antialiased;
.coco-container * {
@apply box-border outline-none;
}
.dark body,
.dark #root {
@apply text-gray-100;
.coco-container {
@apply antialiased rounded-xl text-foreground;
}
.input-body {
@apply rounded-[6px] overflow-hidden;
.coco-container .input-body {
@apply rounded-xl overflow-hidden;
}
.icon {
.coco-container .icon {
width: 1em;
height: 1em;
vertical-align: -0.15em;
fill: currentColor;
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 */
@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
@apply block w-full rounded-md border border-border
bg-background text-foreground
shadow-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring
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
@apply text-sm rounded-md border border-border
bg-background text-foreground
shadow-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring
transition-colors duration-200;
}
}
/* Utility styles */
/* Utility styles (scoped to coco container where reasonable) */
@layer utilities {
/* Fallback for Tailwind v4 class extraction edge-cases: ensure rounded-xl exists */
.rounded-xl {
border-radius: 0.75rem;
}
/* Scrollbar styles */
.custom-scrollbar {
.coco-container .custom-scrollbar {
scrollbar-width: thin;
scrollbar-color: #cbd5e1 transparent;
}
.custom-scrollbar::-webkit-scrollbar {
.coco-container .custom-scrollbar::-webkit-scrollbar {
width: 6px;
}
.custom-scrollbar::-webkit-scrollbar-track {
.coco-container .custom-scrollbar::-webkit-scrollbar-track {
background: transparent;
}
.custom-scrollbar::-webkit-scrollbar-thumb {
.coco-container .custom-scrollbar::-webkit-scrollbar-thumb {
background-color: #cbd5e1;
border-radius: 3px;
}
.dark .custom-scrollbar {
.dark.coco-container .custom-scrollbar {
scrollbar-color: #475569 transparent;
}
.dark .custom-scrollbar::-webkit-scrollbar-thumb {
.dark.coco-container .custom-scrollbar::-webkit-scrollbar-thumb {
background-color: #475569;
}
@@ -315,11 +406,11 @@
}
}
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
/* Hide the waveform visualization for speech-to-text (only appears in production with two waveforms) */
::part(progress) {
display: none;
}
::part(scroll) {
overflow: hidden;
}

View File

@@ -1,12 +1,16 @@
import ReactDOM from "react-dom/client";
import { RouterProvider } from "react-router-dom";
import platformAdapter from "@/utils/platformAdapter";
import { router } from "./routes/index";
import { routerWeb } from "./routes/web";
import "./i18n";
import '@/utils/global-logger';
import "./main.css";
const isTauri = platformAdapter.isTauri();
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 { 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 { useTranslation } from "react-i18next";
import { listen } from "@tauri-apps/api/event";
@@ -18,13 +18,14 @@ import { useAppStore } from "@/stores/appStore";
import { useExtensionsStore } from "@/stores/extensionsStore";
import { useAppearanceStore } from "@/stores/appearanceStore";
const tabIndexMap: { [key: string]: number } = {
general: 0,
extensions: 1,
connect: 2,
advanced: 3,
about: 4,
};
const tabValues = [
"general",
"extensions",
"connect",
"advanced",
"about",
] as const;
type TabValue = (typeof tabValues)[number];
function SettingsPage() {
const { t } = useTranslation();
@@ -32,22 +33,21 @@ function SettingsPage() {
useTray();
const tabs = [
{ name: t("settings.tabs.general"), icon: Settings },
{ name: t("settings.tabs.extensions"), icon: Puzzle },
{ name: t("settings.tabs.connect"), icon: Server },
{ name: t("settings.tabs.advanced"), icon: Settings2 },
{ name: t("settings.tabs.about"), icon: Info },
const tabs: { name: string; icon: any; value: TabValue }[] = [
{ name: t("settings.tabs.general"), icon: Settings, value: "general" },
{ name: t("settings.tabs.extensions"), icon: Puzzle, value: "extensions" },
{ name: t("settings.tabs.connect"), icon: Server, value: "connect" },
{ name: t("settings.tabs.advanced"), icon: Settings2, value: "advanced" },
{ name: t("settings.tabs.about"), icon: Info, value: "about" },
];
const [defaultIndex, setDefaultIndex] = useState<number>(0);
const [selectedTab, setSelectedTab] = useState<TabValue>("general");
useEffect(() => {
const unlisten = listen("tab_index", (event) => {
const tabName = event.payload as string;
const index = tabIndexMap[tabName];
if (index !== -1) {
setDefaultIndex(index);
const tabName = event.payload as TabValue;
if (tabValues.includes(tabName)) {
setSelectedTab(tabName);
}
});
@@ -67,7 +67,7 @@ function SettingsPage() {
"config-extension",
({ payload }) => {
platformAdapter.showWindow();
setDefaultIndex(1);
setSelectedTab("extensions");
setConfigId(payload);
}
);
@@ -82,69 +82,60 @@ function SettingsPage() {
}, []);
useEffect(() => {
document.body.style.overflow = defaultIndex === 1 ? "hidden" : "auto";
}, [defaultIndex]);
document.body.style.overflow =
selectedTab === "extensions" ? "hidden" : "auto";
}, [selectedTab]);
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">
<TabGroup
selectedIndex={defaultIndex}
onChange={(index) => {
setDefaultIndex(index);
}}
<Tabs
value={selectedTab}
onValueChange={(v) => setSelectedTab(v as TabValue)}
>
<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) => (
<Tab
key={tab.name}
className={({ selected }) =>
`w-full rounded-lg py-2.5 text-sm font-medium leading-5
${
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`
}
<TabsTrigger
key={tab.value}
value={tab.value}
className="flex-1 gap-2 h-full"
>
<tab.icon className="w-4 h-4" />
<span>{tab.name}</span>
</Tab>
))}
</TabList>
<tab.icon className="size-4" />
<TabPanels className="mt-2">
<TabPanel>
<SettingsPanel title="">
<GeneralSettings />
</SettingsPanel>
</TabPanel>
<TabPanel>
<SettingsPanel title="">
<Extensions />
</SettingsPanel>
</TabPanel>
<TabPanel>
<Cloud />
</TabPanel>
<TabPanel>
<SettingsPanel title="">
<Advanced />
</SettingsPanel>
</TabPanel>
<TabPanel>
<SettingsPanel title="">
<AboutView />
</SettingsPanel>
</TabPanel>
</TabPanels>
</TabGroup>
<span>{tab.name}</span>
</TabsTrigger>
))}
</TabsList>
<TabsContent value="general">
<SettingsPanel title="">
<GeneralSettings />
</SettingsPanel>
</TabsContent>
<TabsContent value="extensions">
<SettingsPanel title="">
<Extensions />
</SettingsPanel>
</TabsContent>
<TabsContent value="connect">
<Cloud />
</TabsContent>
<TabsContent value="advanced">
<SettingsPanel title="">
<Advanced />
</SettingsPanel>
</TabsContent>
<TabsContent value="about">
<SettingsPanel title="">
<AboutView />
</SettingsPanel>
</TabsContent>
</Tabs>
</div>
</div>
<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 "@/i18n";
import "@/web.css";
import { useIconfontScript } from "@/hooks/useScript";
// Styles are distributed separately in the library build (out/search-chat/index.css)
interface WebAppProps {
headers?: Record<string, unknown>;
@@ -117,6 +118,7 @@ function WebApp({
useEscape();
useModifierKeyPress();
useViewportHeight();
useIconfontScript();
useEffect(() => {
setDisabled(!loginInfo && !integration?.guest?.enabled);
@@ -125,7 +127,7 @@ function WebApp({
return (
<div
id="searchChat-container"
className={`coco-container relative ${theme}`}
className={`coco-container relative ${theme} border! border-(--border) rounded-xl`}
data-theme={theme}
style={{
maxWidth: `${width}px`,

View File

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

View File

@@ -15,7 +15,7 @@ import { useModifierKeyPress } from "@/hooks/useModifierKeyPress";
import { useIconfontScript } from "@/hooks/useScript";
import { Extension } from "@/components/Settings/Extensions";
import { useExtensionsStore } from "@/stores/extensionsStore";
import { useSelectionStore } from "@/stores/selectionStore";
import { useSelectionStore, startSelectionStorePersistence } from "@/stores/selectionStore";
import { useServers } from "@/hooks/useServers";
import { useDeepLinkManager } from "@/hooks/useDeepLinkManager";
// import { useSelectionWindow } from "@/hooks/useSelectionWindow";
@@ -30,6 +30,11 @@ export default function LayoutOutlet() {
// Initialize selection store synchronization
useSelectionStore();
// Initialize Tauri-backed persistence for selection store only in desktop mode.
useMount(() => {
startSelectionStorePersistence();
});
// init servers isTauri
useServers();
// init deep link manager
@@ -39,13 +44,11 @@ export default function LayoutOutlet() {
i18n.changeLanguage(language);
}, [language]);
function updateBodyClass(path: string) {
function updateBodyClass(_path: string) {
const body = document.body;
body.classList.remove("input-body");
if (path === "/ui") {
body.classList.add("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
body.classList.add("input-body");
}
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 { createTauriStore } from '@tauri-store/zustand';
import { create } from "zustand";
import platformAdapter from "@/utils/platformAdapter";
type IconConfig =
| { type: "lucide"; name: string; color?: string }
@@ -37,9 +37,18 @@ export const useSelectionStore = create<SelectionStore>((set) => ({
setSelectionEnabled: (selectionEnabled) => set({ selectionEnabled }),
}));
// A handle to the Tauri plugin.
// We will need this to start the store.
export const tauriHandler = createTauriStore('selection-store', useSelectionStore, {
saveOnChange: true,
autoStart: true,
});
/**
* Initialize Selection store persistence on Tauri only.
* In Web mode, this is a no-op to avoid loading Tauri-specific plugins.
*
* 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