6 Commits
v0.9.1 ... main

Author SHA1 Message Date
BiggerRain
ed8a1cb477 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>
2025-12-18 10:26:13 +08:00
SteveLauC
abf20f81ff refactor: treat Applications and File Search as normal extensions (#1012)
I was digging into an issue that the "File Search" extension does
not appear in the extension list under some cases, then I realized
that, in list_extensions(), "File Search" is another extension that
needs to be processed separately, just like the "Applications"
extension.

Both extensions are of type group/extension, which semantically should
behave like a folder, i.e., they contain sub-extensions, but they
do not. In our implementation, they are more like command extensions.

So we treat them as normal extensions in list_extensioins().
2025-12-17 15:08:01 +08:00
SteveLauC
65a48efdde fix: implement custom serialization for Extension.minimum_coco_version (#1010)
Coco's version string does not adhere to semver's spec, so we had to
write a custom deserialization impl to do the conversion:

```
0.9.1-SNPASHOT-2560 => 0.9.1-SNAPSHOT.2560
```

But we forget to serialize versions to Coco's format when writing
extensions to disk. When Coco reads extension from disk, it sees
valid semantic versions, which are not expected:

```
[WAR] [third_party:167] invalid extension: [base64-converter]:
  field [minimum_coco_version] has invalid version:
    failed to parse build number 'SNAPSHOT.2560'', caused by: ['invalid digit found in string']
```

This commit provides a custom serialization impl to fix the issue.
2025-12-16 15:29:36 +08:00
ayangweb
0613238876 refactor: hide the filter bar in quick ai access (#1008) 2025-12-15 09:25:13 +08:00
SteveLauC
501f6df473 chore: show error msg (not err code) when installing exts via deeplink/store fails (#1007)
* chore: show error msg (not err code) when installing exts via deeplink fails

When installing extensions via deeplink fails, previous implementation
showed the raw error code returned from the backend interfaces, which
is not user-friendly. We now call installExtensionError() to interrupt
the error code to get a human-readable error message, then show it to
the users.

* fix: correct install extension error when installing via store
2025-12-14 09:24:13 +08:00
ayangweb
67c8c4bdfa fix: fix the abnormal input height issue (#1006)
* fix: fix the abnormal input height issue

* docs: update changelog
2025-12-09 15:07:41 +08:00
107 changed files with 4021 additions and 2600 deletions

4
.gitignore vendored
View File

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

View File

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

View File

@@ -8,11 +8,22 @@ title: "Release Notes"
Information about release notes of Coco App is provided here. Information about release notes of Coco App is provided here.
## Latest (In development) ## Latest (In development)
### ❌ Breaking changes ### ❌ Breaking changes
### 🚀 Features ### 🚀 Features
### 🐛 Bug fix ### 🐛 Bug fix
- fix: fix the abnormal input height issue #1006
- fix: implement custom serialization for Extension.minimum_coco_version #1010
### ✈️ Improvements ### ✈️ Improvements
- refactor: replace legacy components with shadcn/ui components #1002
- chore: show error msg (not err code) when installing exts via deeplink fails #1007
- refactor: treat Applications and File Search as normal extensions #1012
## 0.9.1 (2025-12-05) ## 0.9.1 (2025-12-05)
### ❌ Breaking changes ### ❌ Breaking changes

View File

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

1550
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

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

39
scripts/buildWebAfter.ts Normal file
View File

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

View File

@@ -135,7 +135,9 @@ pub struct Extension {
/// It is only for third-party extensions. Built-in extensions should always /// It is only for third-party extensions. Built-in extensions should always
/// set this field to `None`. /// set this field to `None`.
#[serde(deserialize_with = "deserialize_coco_semver")] #[serde(deserialize_with = "deserialize_coco_semver")]
#[serde(default)] // None if this field is missing #[serde(serialize_with = "serialize_coco_semver")]
// None if this field is missing, required as we use custom deserilize method.
#[serde(default)]
minimum_coco_version: Option<SemVer>, minimum_coco_version: Option<SemVer>,
/* /*
@@ -419,6 +421,37 @@ where
Ok(Some(semver)) Ok(Some(semver))
} }
/// Serialize Coco SemVer to a string.
///
/// For a `SemVer`, there are 2 possible input cases, guarded by `to_semver()`:
///
/// 1. "x.y.z" => "x.y.z"
/// 2. "x.y.z-SNAPSHOT.2560" => "x.y.z-SNAPSHOT-2560"
fn serialize_coco_semver<S>(version: &Option<SemVer>, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
match version {
Some(v) => {
assert!(v.build.is_empty());
let s = if v.pre.is_empty() {
format!("{}.{}.{}", v.major, v.minor, v.patch)
} else {
format!(
"{}.{}.{}-{}",
v.major,
v.minor,
v.patch,
v.pre.as_str().replace('.', "-")
)
};
serializer.serialize_str(&s)
}
None => serializer.serialize_none(),
}
}
#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)] #[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
pub(crate) struct CommandAction { pub(crate) struct CommandAction {
pub(crate) exec: String, pub(crate) exec: String,
@@ -673,7 +706,30 @@ fn filter_out_extensions(
extensions.retain(|ext| { extensions.retain(|ext| {
let ty = ext.r#type; let ty = ext.r#type;
ty == ExtensionType::Group || ty == ExtensionType::Extension || ty == extension_type
if ty.contains_sub_items() {
/*
* We should not filter out group/extension extensions, with 2
* exceptions: "Applications" and "File Search". They contains
* no sub-extensions, so we treat them as normal extensions.
*
* When `extenison_type` is "Application", we return the "Applications"
* extension as well because it is the entry to access the application
* list.
*/
if ext.developer.is_none()
&& ext.id == built_in::application::QUERYSOURCE_ID_DATASOURCE_ID_DATASOURCE_NAME
{
ty == extension_type || extension_type == ExtensionType::Application
} else if ext.developer.is_none() && ext.id == built_in::file_search::EXTENSION_ID {
ty == extension_type
} else {
// We should not filter out group/extension extensions
true
}
} else {
ty == extension_type
}
}); });
// Filter sub-extensions to only include the requested type // Filter sub-extensions to only include the requested type
@@ -693,19 +749,6 @@ fn filter_out_extensions(
} }
} }
} }
// Application is special, technically, it should never be filtered out by
// this condition. But if our users will be surprising if they choose a
// non-Application type and see it in the results. So we do this to remedy the
// issue
if let Some(idx) = extensions.iter().position(|ext| {
ext.developer.is_none()
&& ext.id == built_in::application::QUERYSOURCE_ID_DATASOURCE_ID_DATASOURCE_NAME
}) {
if extension_type != ExtensionType::Application {
extensions.remove(idx);
}
}
} }
// apply query filter // apply query filter
@@ -721,8 +764,23 @@ fn filter_out_extensions(
extensions.retain(|ext| { extensions.retain(|ext| {
if ext.r#type.contains_sub_items() { if ext.r#type.contains_sub_items() {
/*
* We should keep all the group/extension extensions. But we
* have 2 exceptions: "Applications" and "File Search". Even
* though they are of type group/extension, they do not contain
* sub-extensions, so they are more like commands, apply the
* `match_closure` here
*/
if ext.developer.is_none()
&& (ext.id
== built_in::application::QUERYSOURCE_ID_DATASOURCE_ID_DATASOURCE_NAME
|| ext.id == built_in::file_search::EXTENSION_ID)
{
match_closure(ext)
} else {
// Keep all group/extension types // Keep all group/extension types
true true
}
} else { } else {
// Apply filter to non-group/extension types // Apply filter to non-group/extension types
match_closure(ext) match_closure(ext)
@@ -779,7 +837,8 @@ pub(crate) async fn list_extensions(
// Cleanup after filtering extensions, don't do it if filter is not performed. // Cleanup after filtering extensions, don't do it if filter is not performed.
// //
// Remove parent extensions (Group/Extension types) that have no sub-items after filtering // Remove parent extensions (Group/Extension types) that have no sub-items
// after filtering
let filter_performed = query.is_some() || extension_type.is_some() || list_enabled; let filter_performed = query.is_some() || extension_type.is_some() || list_enabled;
if filter_performed { if filter_performed {
extensions.retain(|ext| { extensions.retain(|ext| {
@@ -787,12 +846,21 @@ pub(crate) async fn list_extensions(
return true; return true;
} }
// We don't do this filter to applications since it is always empty, load at runtime. /*
if ext.developer.is_none() * Two exceptions: "Applications" and "File Search"
&& ext.id == built_in::application::QUERYSOURCE_ID_DATASOURCE_ID_DATASOURCE_NAME *
* They are of type group/extension, but they contain no sub
* extensions, which means technically, we should filter them
* out. However, we sould not do this because they are not real
* group/extension extensions.
*/
if ext.developer.is_none() {
if ext.id == built_in::application::QUERYSOURCE_ID_DATASOURCE_ID_DATASOURCE_NAME
|| ext.id == built_in::file_search::EXTENSION_ID
{ {
return true; return true;
} }
}
let has_commands = ext let has_commands = ext
.commands .commands
@@ -2150,4 +2218,31 @@ mod tests {
let deserialized: FileSystemAccess = serde_json::from_str(&serialized).unwrap(); let deserialized: FileSystemAccess = serde_json::from_str(&serialized).unwrap();
assert_eq!(original, deserialized); assert_eq!(original, deserialized);
} }
#[test]
fn test_serialize_coco_semver_none() {
let version: Option<SemVer> = None;
let mut serializer = serde_json::Serializer::new(Vec::new());
serialize_coco_semver(&version, &mut serializer).unwrap();
let serialized = String::from_utf8(serializer.into_inner()).unwrap();
assert_eq!(serialized, "null");
}
#[test]
fn test_serialize_coco_semver_simple() {
let version: Option<SemVer> = Some(SemVer::parse("1.2.3").unwrap());
let mut serializer = serde_json::Serializer::new(Vec::new());
serialize_coco_semver(&version, &mut serializer).unwrap();
let serialized = String::from_utf8(serializer.into_inner()).unwrap();
assert_eq!(serialized, "\"1.2.3\"");
}
#[test]
fn test_serialize_coco_semver_with_pre() {
let version: Option<SemVer> = Some(SemVer::parse("1.2.3-SNAPSHOT.1234").unwrap());
let mut serializer = serde_json::Serializer::new(Vec::new());
serialize_coco_semver(&version, &mut serializer).unwrap();
let serialized = String::from_utf8(serializer.into_inner()).unwrap();
assert_eq!(serialized, "\"1.2.3-SNAPSHOT-1234\"");
}
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -44,7 +44,7 @@ export function DataSourcesList({ server }: { server: string }) {
<h2 className="flex justify-between text-xl font-medium text-gray-900 dark:text-white"> <h2 className="flex justify-between text-xl font-medium text-gray-900 dark:text-white">
{t("cloud.dataSource.title")} {t("cloud.dataSource.title")}
<button <button
className="p-2 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300 rounded-[6px] bg-white dark:bg-gray-800 border border-[rgba(228,229,239,1)] dark:border-gray-700" className="p-2 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300 rounded-md bg-white dark:bg-gray-800 border border-[rgba(228,229,239,1)] dark:border-gray-700"
onClick={() => initServerAppData()} onClick={() => initServerAppData()}
> >
<RefreshCcw <RefreshCcw

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,24 +1,20 @@
import { import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from "@/components/ui/dialog";
Button, import { Button } from "@/components/ui/button";
ButtonProps, import { FC, KeyboardEvent, ComponentProps } from "react";
Description,
Dialog,
DialogPanel,
DialogTitle,
} from "@headlessui/react";
import { FC, KeyboardEvent } from "react";
import clsx from "clsx"; import clsx from "clsx";
import { twMerge } from "tailwind-merge"; import { twMerge } from "tailwind-merge";
import VisibleKey from "./VisibleKey"; import VisibleKey from "./VisibleKey";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
type ShadButtonProps = ComponentProps<typeof Button>;
interface DeleteDialogProps { interface DeleteDialogProps {
isOpen: boolean; isOpen: boolean;
title: string; title: string;
description: string; description: string;
deleteButtonProps?: ButtonProps; deleteButtonProps?: ShadButtonProps;
cancelButtonProps?: ButtonProps; cancelButtonProps?: ShadButtonProps;
reverseButtonPosition?: boolean; reverseButtonPosition?: boolean;
setIsOpen: (isOpen: boolean) => void; setIsOpen: (isOpen: boolean) => void;
onCancel: () => void; onCancel: () => void;
@@ -49,20 +45,12 @@ const DeleteDialog: FC<DeleteDialogProps> = (props) => {
}; };
return ( return (
<Dialog <Dialog open={isOpen} onOpenChange={setIsOpen}>
open={isOpen} <DialogContent className="flex flex-col justify-between w-[360px] h-[160px] p-3 text-[#333] dark:text-white/90 border border-[#e6e6e6] bg-white dark:bg-[#202126] dark:border-white/10 shadow-[0_4px_10px_rgba(0,0,0,0.2)] rounded-lg dark:shadow-[0_8px_20px_rgba(0,0,0,0.4)]">
onClose={() => setIsOpen(false)} <DialogHeader className="mb-2">
className="relative z-1000"
>
<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> <DialogTitle className="text-base font-bold">{title}</DialogTitle>
<Description className="text-sm">{description}</Description> <DialogDescription className="text-sm">{description}</DialogDescription>
</div> </DialogHeader>
<div <div
className={clsx("flex gap-4 self-end", { className={clsx("flex gap-4 self-end", {
@@ -110,8 +98,7 @@ const DeleteDialog: FC<DeleteDialogProps> = (props) => {
</Button> </Button>
</VisibleKey> </VisibleKey>
</div> </div>
</DialogPanel> </DialogContent>
</div>
</Dialog> </Dialog>
); );
}; };

View File

@@ -1,10 +1,11 @@
import { import {
Button,
Description,
Dialog, Dialog,
DialogPanel, DialogContent,
DialogHeader,
DialogTitle, DialogTitle,
} from "@headlessui/react"; DialogDescription,
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import VisibleKey from "@/components/Common/VisibleKey"; import VisibleKey from "@/components/Common/VisibleKey";
@@ -36,21 +37,13 @@ const DeleteDialog = ({
}; };
return ( return (
<Dialog <Dialog open={isOpen} onOpenChange={setIsOpen}>
open={isOpen} <DialogContent className="flex flex-col justify-between w-[360px] h-40 p-3 text-[#333] dark:text-white/90 border border-[#e6e6e6] bg-white dark:bg-[#202126] dark:border-white/10 shadow-xl rounded-lg">
onClose={() => setIsOpen(false)} <DialogHeader className="mb-2">
className="relative z-1000"
>
<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"> <DialogTitle className="text-base font-bold">
{t("history_list.delete_modal.title")} {t("history_list.delete_modal.title")}
</DialogTitle> </DialogTitle>
<Description className="text-sm"> <DialogDescription className="text-sm">
{t("history_list.delete_modal.description", { {t("history_list.delete_modal.description", {
replace: [ replace: [
active?._source?.title || active?._source?.title ||
@@ -58,18 +51,20 @@ const DeleteDialog = ({
active?._id, active?._id,
], ],
})} })}
</Description> </DialogDescription>
</div> </DialogHeader>
<div className="flex gap-4 self-end"> <div className="flex gap-4 self-end">
<VisibleKey <VisibleKey
shortcut="N" shortcut="N"
shortcutClassName="left-[unset] right-0" shortcutClassName="left-[unset] right-0"
onKeyPress={() => setIsOpen(false)} onKeyPress={() => {
setIsOpen(false);
}}
> >
<Button <Button
variant="outline"
autoFocus 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)} onClick={() => setIsOpen(false)}
onKeyDown={(event) => { onKeyDown={(event) => {
handleEnter(event, () => { handleEnter(event, () => {
@@ -87,7 +82,8 @@ const DeleteDialog = ({
onKeyPress={handleRemove} onKeyPress={handleRemove}
> >
<Button <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" variant="destructive"
className="text-white"
onClick={handleRemove} onClick={handleRemove}
onKeyDown={(event) => { onKeyDown={(event) => {
handleEnter(event, handleRemove); handleEnter(event, handleRemove);
@@ -97,8 +93,7 @@ const DeleteDialog = ({
</Button> </Button>
</VisibleKey> </VisibleKey>
</div> </div>
</DialogPanel> </DialogContent>
</div>
</Dialog> </Dialog>
); );
}; };

View File

@@ -113,7 +113,8 @@ const HistoryListContent: FC<HistoryListContentProps> = ({
const scrollToElement = useCallback( const scrollToElement = useCallback(
(elementId: string, isKeyboardNav: boolean) => { (elementId: string, isKeyboardNav: boolean) => {
if (!listRef.current) return; if (!listRef.current) return;
if (typeof window === 'undefined' || typeof document === 'undefined') return; if (typeof window === "undefined" || typeof document === "undefined")
return;
const element = listRef.current.querySelector(`#${elementId}`); const element = listRef.current.querySelector(`#${elementId}`);
if (!element) return; if (!element) return;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -244,7 +244,7 @@ const ExtensionStore = ({ extensionId }: { extensionId?: string }) => {
"info" "info"
); );
} catch (error) { } catch (error) {
installExtensionError(String(error)); installExtensionError(error);
} finally { } finally {
const { installingExtensions } = useSearchStore.getState(); const { installingExtensions } = useSearchStore.getState();
@@ -306,7 +306,7 @@ const ExtensionStore = ({ extensionId }: { extensionId?: string }) => {
<div <div
key={id} key={id}
className={clsx( className={clsx(
"flex justify-between gap-4 h-[40px] px-2 rounded-lg cursor-pointer text-[#333] dark:text-[#d8d8d8] transition", "flex justify-between gap-4 h-10 px-2 rounded-lg cursor-pointer text-[#333] dark:text-[#d8d8d8] transition",
{ {
"bg-black/10 dark:bg-white/15": "bg-black/10 dark:bg-white/15":
selectedExtension?.id === id, selectedExtension?.id === id,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -12,6 +12,7 @@ import platformAdapter from "@/utils/platformAdapter";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { MAIN_WINDOW_LABEL, SETTINGS_WINDOW_LABEL } from "@/constants"; import { MAIN_WINDOW_LABEL, SETTINGS_WINDOW_LABEL } from "@/constants";
import { useAsyncEffect, useEventListener } from "ahooks"; import { useAsyncEffect, useEventListener } from "ahooks";
import { installExtensionError } from "@/utils";
export interface DeepLinkHandler { export interface DeepLinkHandler {
pattern: string; pattern: string;
@@ -78,7 +79,7 @@ export function useDeepLinkManager() {
addError(t("deepLink.extensionInstallSuccessfully"), "info"); addError(t("deepLink.extensionInstallSuccessfully"), "info");
console.log("Extension installed successfully:", extensionId); console.log("Extension installed successfully:", extensionId);
} catch (error) { } catch (error) {
addError(String(error)); installExtensionError(error)
} }
}, []); }, []);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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