8 Commits
v0.9.1 ... main

Author SHA1 Message Date
BiggerRain
3a9c9ec9eb style: img styles (#1015) 2025-12-18 15:51:00 +08:00
BiggerRain
f7c0600480 feat: add open button to launch installed extension (#1013)
* chore: up

* support query string main_extension_id

* chore: up

* fix tests

* open non-group/extension extensions

* dbg

* chore: upadate

* extension SearchSource now accepts empty querystring

* update

* chore: open

* chore: input

* remove DBG statements

* chore: icon

* style: adjust styles

* docs: update release notes

---------

Co-authored-by: Steve Lau <stevelauc@outlook.com>
2025-12-18 15:50:26 +08:00
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
115 changed files with 4519 additions and 2823 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,24 @@ 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
- feat: add open button to launch installed extension #1013
### 🐛 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>,
/* /*
@@ -216,20 +218,19 @@ impl<'ext> PartialEq<ExtensionBundleId> for ExtensionBundleIdBorrowed<'ext> {
} }
} }
impl Extension { #[tauri::command]
/// Whether this extension could be searched. pub(crate) fn extension_on_opened(extension: Extension) -> Option<OnOpened> {
pub(crate) fn searchable(&self) -> bool { _extension_on_opened(&extension)
self.on_opened().is_some() }
}
/// Return what will happen when we open this extension. /// Return what will happen when we open this extension.
/// ///
/// `None` if it cannot be opened. /// `None` if it cannot be opened.
pub(crate) fn on_opened(&self) -> Option<OnOpened> { pub(crate) fn _extension_on_opened(extension: &Extension) -> Option<OnOpened> {
let settings = self.settings.clone(); let settings = extension.settings.clone();
let permission = self.permission.clone(); let permission = extension.permission.clone();
match self.r#type { match extension.r#type {
// This function, at the time of writing this comment, is primarily // This function, at the time of writing this comment, is primarily
// used by third-party extensions. // used by third-party extensions.
// //
@@ -258,9 +259,9 @@ impl Extension {
ExtensionType::Command => { ExtensionType::Command => {
let ty = ExtensionOnOpenedType::Command { let ty = ExtensionOnOpenedType::Command {
action: self.action.clone().unwrap_or_else(|| { action: extension.action.clone().unwrap_or_else(|| {
panic!( panic!(
"Command extension [{}]'s [action] field is not set, something wrong with your extension validity check", self.id "Command extension [{}]'s [action] field is not set, something wrong with your extension validity check", extension.id
) )
}), }),
}; };
@@ -274,9 +275,9 @@ impl Extension {
Some(OnOpened::Extension(extension_on_opened)) Some(OnOpened::Extension(extension_on_opened))
} }
ExtensionType::Quicklink => { ExtensionType::Quicklink => {
let quicklink = self.quicklink.clone().unwrap_or_else(|| { let quicklink = extension.quicklink.clone().unwrap_or_else(|| {
panic!( panic!(
"Quicklink extension [{}]'s [quicklink] field is not set, something wrong with your extension validity check", self.id "Quicklink extension [{}]'s [quicklink] field is not set, something wrong with your extension validity check", extension.id
) )
}); });
@@ -296,12 +297,12 @@ impl Extension {
ExtensionType::Script => todo!("not supported yet"), ExtensionType::Script => todo!("not supported yet"),
ExtensionType::Setting => todo!("not supported yet"), ExtensionType::Setting => todo!("not supported yet"),
ExtensionType::View => { ExtensionType::View => {
let name = self.name.clone(); let name = extension.name.clone();
let icon = self.icon.clone(); let icon = extension.icon.clone();
let page = self.page.as_ref().unwrap_or_else(|| { let page = extension.page.as_ref().unwrap_or_else(|| {
panic!("View extension [{}]'s [page] field is not set, something wrong with your extension validity check", self.id); panic!("View extension [{}]'s [page] field is not set, something wrong with your extension validity check", extension.id);
}).clone(); }).clone();
let ui = self.ui.clone(); let ui = extension.ui.clone();
let extension_on_opened_type = ExtensionOnOpenedType::View { let extension_on_opened_type = ExtensionOnOpenedType::View {
name, name,
@@ -322,6 +323,12 @@ impl Extension {
unreachable!("Extensions of type [Unknown] should never be opened") unreachable!("Extensions of type [Unknown] should never be opened")
} }
} }
}
impl Extension {
/// Whether this extension could be searched.
pub(crate) fn searchable(&self) -> bool {
_extension_on_opened(self).is_some()
} }
pub(crate) fn get_sub_extension(&self, sub_extension_id: &str) -> Option<&Self> { pub(crate) fn get_sub_extension(&self, sub_extension_id: &str) -> Option<&Self> {
@@ -419,6 +426,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 +711,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 +754,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 +769,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 +842,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 +851,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 +2223,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

@@ -16,6 +16,8 @@ use crate::common::search::QueryResponse;
use crate::common::search::QuerySource; use crate::common::search::QuerySource;
use crate::common::search::SearchQuery; use crate::common::search::SearchQuery;
use crate::common::traits::SearchSource; use crate::common::traits::SearchSource;
use crate::extension::_extension_on_opened;
use crate::extension::ExtensionBundleId;
use crate::extension::ExtensionBundleIdBorrowed; use crate::extension::ExtensionBundleIdBorrowed;
use crate::extension::ExtensionType; use crate::extension::ExtensionType;
use crate::extension::PLUGIN_JSON_FIELD_MINIMUM_COCO_VERSION; use crate::extension::PLUGIN_JSON_FIELD_MINIMUM_COCO_VERSION;
@@ -26,11 +28,13 @@ use crate::util::platform::Platform;
use crate::util::version::COCO_VERSION; use crate::util::version::COCO_VERSION;
use crate::util::version::parse_coco_semver; use crate::util::version::parse_coco_semver;
use async_trait::async_trait; use async_trait::async_trait;
use borrowme::Borrow;
use borrowme::ToOwned; use borrowme::ToOwned;
use check::general_check; use check::general_check;
use function_name::named; use function_name::named;
use semver::Version as SemVer; use semver::Version as SemVer;
use serde_json::Value as Json; use serde_json::Value as Json;
use snafu::prelude::*;
use std::io::ErrorKind; use std::io::ErrorKind;
use std::ops::Deref; use std::ops::Deref;
use std::path::Path; use std::path::Path;
@@ -44,6 +48,7 @@ use tauri_plugin_global_shortcut::GlobalShortcutExt;
use tauri_plugin_global_shortcut::ShortcutState; use tauri_plugin_global_shortcut::ShortcutState;
use tokio::fs::read_dir; use tokio::fs::read_dir;
use tokio::sync::RwLock; use tokio::sync::RwLock;
use tokio::sync::RwLockReadGuard;
use tokio::sync::RwLockWriteGuard; use tokio::sync::RwLockWriteGuard;
pub(crate) fn get_third_party_extension_directory(tauri_app_handle: &AppHandle) -> PathBuf { pub(crate) fn get_third_party_extension_directory(tauri_app_handle: &AppHandle) -> PathBuf {
@@ -393,6 +398,26 @@ impl ThirdPartyExtensionsSearchSource {
extension.get_sub_extension_mut(sub_extension_id) extension.get_sub_extension_mut(sub_extension_id)
} }
/// Return an immutable reference to the extension specified by `bundle_id` if it exists.
fn get_extension<'lock, 'extensions>(
extensions_read_lock: &'lock RwLockReadGuard<'extensions, Vec<Extension>>,
bundle_id: &ExtensionBundleIdBorrowed<'_>,
) -> Option<&'lock Extension> {
let index = extensions_read_lock.iter().position(|ext| {
ext.id == bundle_id.extension_id && ext.developer.as_deref() == bundle_id.developer
})?;
let extension = extensions_read_lock
.get(index)
.expect("just checked this extension exists");
let Some(sub_extension_id) = bundle_id.sub_extension_id else {
return Some(extension);
};
extension.get_sub_extension(sub_extension_id)
}
/// Difference between this function and `enable_extension()` /// Difference between this function and `enable_extension()`
/// ///
/// This function does the actual job, i.e., to enable/activate the extension. /// This function does the actual job, i.e., to enable/activate the extension.
@@ -407,7 +432,7 @@ impl ThirdPartyExtensionsSearchSource {
) -> Result<(), String> { ) -> Result<(), String> {
if extension.supports_alias_hotkey() { if extension.supports_alias_hotkey() {
if let Some(ref hotkey) = extension.hotkey { if let Some(ref hotkey) = extension.hotkey {
let on_opened = extension.on_opened().unwrap_or_else(|| panic!( "extension has hotkey, but on_open() returns None, extension ID [{}], extension type [{:?}]", extension.id, extension.r#type)); let on_opened = _extension_on_opened(extension).unwrap_or_else(|| panic!( "extension has hotkey, but on_open() returns None, extension ID [{}], extension type [{:?}]", extension.id, extension.r#type));
let extension_id_clone = extension.id.clone(); let extension_id_clone = extension.id.clone();
tauri_app_handle tauri_app_handle
@@ -681,7 +706,7 @@ impl ThirdPartyExtensionsSearchSource {
)?; )?;
// Set hotkey // Set hotkey
let on_opened = extension.on_opened().unwrap_or_else(|| panic!( let on_opened = _extension_on_opened(extension).unwrap_or_else(|| panic!(
"setting hotkey for an extension that cannot be opened, extension ID [{:?}], extension type [{:?}]", bundle_id, extension.r#type, "setting hotkey for an extension that cannot be opened, extension ID [{:?}], extension type [{:?}]", bundle_id, extension.r#type,
)); ));
@@ -867,6 +892,59 @@ impl ThirdPartyExtensionsSearchSource {
pub(crate) async fn extensions_snapshot(&self) -> Vec<Extension> { pub(crate) async fn extensions_snapshot(&self) -> Vec<Extension> {
self.inner.extensions.read().await.clone() self.inner.extensions.read().await.clone()
} }
/// Open the specified third-party extension.
async fn open(
&self,
tauri_app_handle: AppHandle,
bundle_id: &ExtensionBundleIdBorrowed<'_>,
) -> Result<(), OpenThirdPartyExtensionError> {
let extensions_read_lock = self.inner.extensions.read().await;
let Some(ext) = Self::get_extension(&extensions_read_lock, bundle_id) else {
log::warn!(
"trying to open() a third-party extension [{:?}] that does not exist",
bundle_id
);
return Err(OpenThirdPartyExtensionError::ExtensionNotFound {
bundle_id: bundle_id.to_owned(),
});
};
let Some(on_opened) = _extension_on_opened(ext) else {
log::warn!("third-party extension [{:?}] cannot be opened", bundle_id);
return Err(OpenThirdPartyExtensionError::ExtensionCannotBeOpened {
bundle_id: bundle_id.to_owned(),
});
};
crate::common::document::open(tauri_app_handle, on_opened, None)
.await
.map_err(|err_msg| OpenThirdPartyExtensionError::OnOpenedOpenError { msg: err_msg })?;
Ok(())
}
}
#[derive(Debug, Snafu, serde::Serialize)]
pub(crate) enum OpenThirdPartyExtensionError {
#[snafu(display("extension '{:?}' does not exist", bundle_id))]
ExtensionNotFound { bundle_id: ExtensionBundleId },
#[snafu(display("extension '{:?}' cannot be opened", bundle_id))]
ExtensionCannotBeOpened { bundle_id: ExtensionBundleId },
#[snafu(display("executing open(OnOpened) failed: '{}'", msg))]
OnOpenedOpenError { msg: String },
}
#[tauri::command]
pub(crate) async fn open_third_party_extension(
tauri_app_handle: AppHandle,
bundle_id: ExtensionBundleId,
) -> Result<(), OpenThirdPartyExtensionError> {
THIRD_PARTY_EXTENSIONS_SEARCH_SOURCE
.get()
.unwrap_or_else(|| panic!("THIRD_PARTY_EXTENSIONS_SEARCH_SOURCE is not set"))
.open(tauri_app_handle, &bundle_id.borrow())
.await
} }
pub(crate) static THIRD_PARTY_EXTENSIONS_SEARCH_SOURCE: OnceLock<ThirdPartyExtensionsSearchSource> = pub(crate) static THIRD_PARTY_EXTENSIONS_SEARCH_SOURCE: OnceLock<ThirdPartyExtensionsSearchSource> =
@@ -890,25 +968,37 @@ impl SearchSource for ThirdPartyExtensionsSearchSource {
} }
} }
// query main_extension_id querysource
// main_extension_id querysource
// query querysource datasource
async fn search( async fn search(
&self, &self,
_tauri_app_handle: AppHandle, _tauri_app_handle: AppHandle,
query: SearchQuery, query: SearchQuery,
) -> Result<QueryResponse, SearchError> { ) -> Result<QueryResponse, SearchError> {
let Some(query_string) = query.query_strings.get("query") else { let opt_lowercase_query_string: Option<String> = {
return Ok(QueryResponse { match query.query_strings.get("query") {
source: self.get_type(), Some(query_string) => {
hits: Vec::new(), if query_string.is_empty() {
total_hits: 0, None
}); } else {
Some(query_string.to_lowercase())
}
}
None => None,
}
}; };
let opt_data_source = query let opt_data_source = query
.query_strings .query_strings
.get("datasource") .get("datasource")
.map(|owned_str| owned_str.to_string()); .map(|str| str.to_string());
let opt_main_extension_id = query
.query_strings
.get("main_extension_id")
.map(|str| str.to_string());
let query_lower = query_string.to_lowercase();
let inner_clone = Arc::clone(&self.inner); let inner_clone = Arc::clone(&self.inner);
let closure = move || { let closure = move || {
@@ -916,10 +1006,22 @@ impl SearchSource for ThirdPartyExtensionsSearchSource {
let extensions_read_lock = let extensions_read_lock =
futures::executor::block_on(async { inner_clone.extensions.read().await }); futures::executor::block_on(async { inner_clone.extensions.read().await });
let main_extension_filter_closure = |ext: &&Extension| -> bool {
// field minimum_coco_extension is only set for main
// extensions, so we only check main extensions.
let condition1 = ext.enabled && is_extension_compatible(Extension::clone(ext));
let condition2 = if let Some(ref main_extension_id) = opt_main_extension_id {
&ext.id == main_extension_id
} else {
true
};
condition1 && condition2
};
for extension in extensions_read_lock for extension in extensions_read_lock
.iter() .iter()
// field minimum_coco_extension is only set for main extensions. .filter(main_extension_filter_closure)
.filter(|ext| ext.enabled && is_extension_compatible(Extension::clone(ext)))
{ {
if extension.r#type.contains_sub_items() { if extension.r#type.contains_sub_items() {
let opt_main_extension_lowercase_name = let opt_main_extension_lowercase_name =
@@ -934,7 +1036,7 @@ impl SearchSource for ThirdPartyExtensionsSearchSource {
for command in commands.iter().filter(|cmd| cmd.enabled) { for command in commands.iter().filter(|cmd| cmd.enabled) {
if let Some(hit) = extension_to_hit( if let Some(hit) = extension_to_hit(
command, command,
&query_lower, opt_lowercase_query_string.as_deref(),
opt_data_source.as_deref(), opt_data_source.as_deref(),
opt_main_extension_lowercase_name.as_deref(), opt_main_extension_lowercase_name.as_deref(),
) { ) {
@@ -947,7 +1049,7 @@ impl SearchSource for ThirdPartyExtensionsSearchSource {
for script in scripts.iter().filter(|script| script.enabled) { for script in scripts.iter().filter(|script| script.enabled) {
if let Some(hit) = extension_to_hit( if let Some(hit) = extension_to_hit(
script, script,
&query_lower, opt_lowercase_query_string.as_deref(),
opt_data_source.as_deref(), opt_data_source.as_deref(),
opt_main_extension_lowercase_name.as_deref(), opt_main_extension_lowercase_name.as_deref(),
) { ) {
@@ -960,7 +1062,7 @@ impl SearchSource for ThirdPartyExtensionsSearchSource {
for quicklink in quicklinks.iter().filter(|link| link.enabled) { for quicklink in quicklinks.iter().filter(|link| link.enabled) {
if let Some(hit) = extension_to_hit( if let Some(hit) = extension_to_hit(
quicklink, quicklink,
&query_lower, opt_lowercase_query_string.as_deref(),
opt_data_source.as_deref(), opt_data_source.as_deref(),
opt_main_extension_lowercase_name.as_deref(), opt_main_extension_lowercase_name.as_deref(),
) { ) {
@@ -973,7 +1075,7 @@ impl SearchSource for ThirdPartyExtensionsSearchSource {
for view in views.iter().filter(|view| view.enabled) { for view in views.iter().filter(|view| view.enabled) {
if let Some(hit) = extension_to_hit( if let Some(hit) = extension_to_hit(
view, view,
&query_lower, opt_lowercase_query_string.as_deref(),
opt_data_source.as_deref(), opt_data_source.as_deref(),
opt_main_extension_lowercase_name.as_deref(), opt_main_extension_lowercase_name.as_deref(),
) { ) {
@@ -982,9 +1084,12 @@ impl SearchSource for ThirdPartyExtensionsSearchSource {
} }
} }
} else { } else {
if let Some(hit) = if let Some(hit) = extension_to_hit(
extension_to_hit(extension, &query_lower, opt_data_source.as_deref(), None) extension,
{ opt_lowercase_query_string.as_deref(),
opt_data_source.as_deref(),
None,
) {
hits.push(hit); hits.push(hit);
} }
} }
@@ -1029,9 +1134,9 @@ pub(crate) async fn uninstall_extension(
/// This argument is needed as an "extension" type extension should return all its /// This argument is needed as an "extension" type extension should return all its
/// sub-extensions when the query string matches its name. To do this, we pass the /// sub-extensions when the query string matches its name. To do this, we pass the
/// extension name, score it and take that into account. /// extension name, score it and take that into account.
pub(crate) fn extension_to_hit( fn extension_to_hit(
extension: &Extension, extension: &Extension,
query_lower: &str, opt_lowercase_query_string: Option<&str>,
opt_data_source: Option<&str>, opt_data_source: Option<&str>,
opt_main_extension_lowercase_name: Option<&str>, opt_main_extension_lowercase_name: Option<&str>,
) -> Option<(Document, f64)> { ) -> Option<(Document, f64)> {
@@ -1050,11 +1155,11 @@ pub(crate) fn extension_to_hit(
} }
let mut total_score = 0.0; let mut total_score = 0.0;
if let Some(query_lower) = opt_lowercase_query_string {
// Score based on title match // Score based on title match
// Title is considered more important, so it gets a higher weight. // Title is considered more important, so it gets a higher weight.
if let Some(title_score) = if let Some(title_score) =
calculate_text_similarity(&query_lower, &extension.name.to_lowercase()) calculate_text_similarity(query_lower, &extension.name.to_lowercase())
{ {
total_score += title_score; total_score += title_score;
} }
@@ -1062,7 +1167,8 @@ pub(crate) fn extension_to_hit(
// Score based on alias match if available // Score based on alias match if available
// Alias is considered less important than title, so it gets a lower weight. // Alias is considered less important than title, so it gets a lower weight.
if let Some(alias) = &extension.alias { if let Some(alias) = &extension.alias {
if let Some(alias_score) = calculate_text_similarity(&query_lower, &alias.to_lowercase()) { if let Some(alias_score) = calculate_text_similarity(query_lower, &alias.to_lowercase())
{
total_score += alias_score; total_score += alias_score;
} }
} }
@@ -1073,15 +1179,19 @@ pub(crate) fn extension_to_hit(
// into account. // into account.
if let Some(main_extension_lowercase_id) = opt_main_extension_lowercase_name { if let Some(main_extension_lowercase_id) = opt_main_extension_lowercase_name {
if let Some(main_extension_score) = if let Some(main_extension_score) =
calculate_text_similarity(&query_lower, main_extension_lowercase_id) calculate_text_similarity(query_lower, main_extension_lowercase_id)
{ {
total_score += main_extension_score; total_score += main_extension_score;
} }
} }
// Only include if there's some relevance (score is meaningfully positive) // Only filter by score if query string is set
if total_score > 0.01 { if total_score == 0.0 {
let on_opened = extension.on_opened().unwrap_or_else(|| { return None;
}
}
let on_opened = _extension_on_opened(extension).unwrap_or_else(|| {
panic!( panic!(
"extension (id [{}], type [{:?}]) is searchable, and should have a valid on_opened", "extension (id [{}], type [{:?}]) is searchable, and should have a valid on_opened",
extension.id, extension.r#type extension.id, extension.r#type
@@ -1107,7 +1217,4 @@ pub(crate) fn extension_to_hit(
}; };
Some((document, total_score)) Some((document, total_score))
} else {
None
}
} }

View File

@@ -177,6 +177,7 @@ pub fn run() {
extension::enable_extension, extension::enable_extension,
extension::disable_extension, extension::disable_extension,
extension::set_extension_alias, extension::set_extension_alias,
extension::extension_on_opened,
extension::register_extension_hotkey, extension::register_extension_hotkey,
extension::unregister_extension_hotkey, extension::unregister_extension_hotkey,
extension::is_extension_enabled, extension::is_extension_enabled,
@@ -185,6 +186,7 @@ pub fn run() {
extension::third_party::install::store::install_extension_from_store, extension::third_party::install::store::install_extension_from_store,
extension::third_party::install::local_extension::install_local_extension, extension::third_party::install::local_extension::install_local_extension,
extension::third_party::uninstall_extension, extension::third_party::uninstall_extension,
extension::third_party::open_third_party_extension,
extension::is_extension_compatible, extension::is_extension_compatible,
extension::api::apis, extension::api::apis,
extension::api::fs::read_dir, extension::api::fs::read_dir,

View File

@@ -17,6 +17,20 @@ use std::collections::HashMap;
use std::sync::Arc; use std::sync::Arc;
use tauri::{AppHandle, Manager}; use tauri::{AppHandle, Manager};
use tokio::time::{Duration, timeout}; use tokio::time::{Duration, timeout};
/// Available `query_strings`:
///
/// * "querysource": the query/search source to search
/// * "datasource": the data source to search. If this is provided, then
/// "querysource" has to be specified as well.
/// * "main_extension_id": Currently, only the "extensions" query source
/// supports this. If you set
///
/// ```text
/// {"querysource": "extension", "main_extension_id"}
/// ```
///
/// then only the extension with this ID will be returned, if exists.
#[named] #[named]
#[tauri::command] #[tauri::command]
pub async fn query_coco_fusion( pub async fn query_coco_fusion(
@@ -26,6 +40,10 @@ pub async fn query_coco_fusion(
query_strings: HashMap<String, String>, query_strings: HashMap<String, String>,
query_timeout: u64, query_timeout: u64,
) -> Result<MultiSourceQueryResponse, SearchError> { ) -> Result<MultiSourceQueryResponse, SearchError> {
if query_strings.contains_key("datasource") && !query_strings.contains_key("querysource") {
panic!("[querysource] has to be provided if [datasource] is set")
}
let opt_query_source_id = query_strings.get("querysource"); let opt_query_source_id = query_strings.get("querysource");
let search_sources = tauri_app_handle.state::<SearchSourceRegistry>(); let search_sources = tauri_app_handle.state::<SearchSourceRegistry>();
let query_source_list = search_sources.get_sources().await; let query_source_list = search_sources.get_sources().await;

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

@@ -81,15 +81,25 @@ async function invokeWithErrorHandler<T>(
return result; return result;
} catch (error: any) { } catch (error: any) {
const errorMessage = error || "Command execution failed"; console.log(error);
const errorMessage =
error instanceof Error
? error.message
: typeof error === "object"
? JSON.stringify(error)
: String(error || "Command execution failed");
// 401 Unauthorized // 401 Unauthorized
if (errorMessage.includes("Unauthorized")) { if (errorMessage.includes("Unauthorized")) {
handleLogout(); handleLogout();
} else { } else {
addError(command + ":" + errorMessage, "error"); addError(command + ":" + errorMessage, "error");
} }
if (error instanceof Error) {
throw error; throw error;
} }
throw new Error(errorMessage);
}
} }
export function list_coco_servers(): Promise<Server[]> { export function list_coco_servers(): Promise<Server[]> {

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 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,
}, },
@@ -79,8 +81,10 @@ const AutoResizeTextarea = forwardRef<
let height = lineHeight; let height = lineHeight;
let minHeight = lineHeight; let minHeight = lineHeight;
const hasNewline = /[\r\n]/.test(input); const hasNewline = /[\r\n]/.test(input);
const hasContent = input.length > 0;
const firstLineExceeds = const firstLineExceeds =
calcRef.current?.offsetWidth >= firstLineMaxWidth - 32; hasContent &&
(calcRef.current?.offsetWidth ?? 0) >= Math.max(firstLineMaxWidth - 32, 0);
if (hasNewline || firstLineExceeds) { if (hasNewline || firstLineExceeds) {
minHeight = lineHeight * 2; minHeight = lineHeight * 2;
@@ -115,7 +119,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

@@ -77,7 +77,7 @@ export const DocumentDetail: React.FC<DocumentDetailProps> = ({ document }) => {
{/* Document Summary */} {/* Document Summary */}
{document?.summary && ( {document?.summary && (
<div className="mb-4 text-xs text-[rgba(153,153,153,1)] dark:text-[#D8D8D8] whitespace-pre-wrap break-words"> <div className="mb-4 text-xs text-[rgba(153,153,153,1)] dark:text-[#D8D8D8] whitespace-pre-wrap wrap-break-word">
{document.summary} {document.summary}
</div> </div>
)} )}

View File

@@ -88,6 +88,9 @@ export const DocumentList: React.FC<DocumentListProps> = ({
rich_category: sourceData?.rich_categories[0]?.key, rich_category: sourceData?.rich_categories[0]?.key,
}; };
} }
if (sourceData?.main_extension_id) {
queryStrings.main_extension_id = sourceData?.main_extension_id
}
let response: any; let response: any;
if (isTauri) { if (isTauri) {
@@ -178,7 +181,7 @@ export const DocumentList: React.FC<DocumentListProps> = ({
{ {
target: containerRef, target: containerRef,
isNoMore: (d) => !d?.hasMore, isNoMore: (d) => !d?.hasMore,
reloadDeps: [input?.trim(), JSON.stringify(sourceData)], reloadDeps: [input, JSON.stringify(sourceData)],
onFinally: (data) => { onFinally: (data) => {
if (data?.page === 1) return; if (data?.page === 1) return;
if (selectedItem === null) return; if (selectedItem === null) return;
@@ -211,7 +214,7 @@ export const DocumentList: React.FC<DocumentListProps> = ({
list: [], list: [],
})); }));
loadingFromRef.current = -1; loadingFromRef.current = -1;
}, [input]); }, [input, JSON.stringify(sourceData)]);
const { visibleContextMenu } = useSearchStore(); const { visibleContextMenu } = useSearchStore();
@@ -292,10 +295,10 @@ export const DocumentList: React.FC<DocumentListProps> = ({
return ( return (
<div <div
className={`border-r border-gray-200 dark:border-gray-700 flex flex-col h-full overflow-x-hidden ${ className={`border-r border-gray-200 dark:border-gray-700 flex flex-col h-full overflow-x-hidden ${
viewMode === "list" ? "w-[100%]" : "w-[50%]" viewMode === "list" ? "w-full" : "w-[50%]"
}`} }`}
> >
<div className="px-2 flex-shrink-0"> <div className="px-2 shrink-0">
<SearchHeader <SearchHeader
total={total} total={total}
viewMode={viewMode} viewMode={viewMode}
@@ -306,6 +309,10 @@ export const DocumentList: React.FC<DocumentListProps> = ({
<Scrollbar className="flex-1 overflow-auto pr-0.5" ref={containerRef}> <Scrollbar className="flex-1 overflow-auto pr-0.5" ref={containerRef}>
{data?.list && data.list.length > 0 && ( {data?.list && data.list.length > 0 && (
<div> <div>
{(() => {
console.log("Rendering list with items:", data.list.length);
return null;
})()}
{data.list.map((hit, index) => ( {data.list.map((hit, index) => (
<SearchListItem <SearchListItem
key={hit.document.id + index} key={hit.document.id + index}

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,
@@ -8,6 +8,7 @@ import {
Loader, Loader,
Trash2, Trash2,
User, User,
SquareArrowOutUpRight,
} from "lucide-react"; } from "lucide-react";
import { FC, useState } from "react"; import { FC, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
@@ -15,15 +16,24 @@ import { useTranslation } from "react-i18next";
import { useSearchStore } from "@/stores/searchStore"; import { useSearchStore } from "@/stores/searchStore";
import DeleteDialog from "../Common/DeleteDialog"; import DeleteDialog from "../Common/DeleteDialog";
import PreviewImage from "../Common/PreviewImage"; import PreviewImage from "../Common/PreviewImage";
import platformAdapter from "@/utils/platformAdapter";
interface ExtensionDetailProps { interface ExtensionDetailProps {
onInstall: () => void; onInstall: () => void;
onUninstall: () => void; onUninstall: () => void;
changeInput: (value: string) => void;
} }
const ExtensionDetail: FC<ExtensionDetailProps> = (props) => { const ExtensionDetail: FC<ExtensionDetailProps> = (props) => {
const { onInstall, onUninstall } = props; const { onInstall, onUninstall, changeInput } = props;
const { selectedExtension, installingExtensions } = useSearchStore(); const {
selectedExtension,
installingExtensions,
setVisibleExtensionStore,
setVisibleExtensionDetail,
setSourceData,
setSearchValue,
} = useSearchStore();
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
const { t } = useTranslation(); const { t } = useTranslation();
@@ -37,6 +47,53 @@ const ExtensionDetail: FC<ExtensionDetailProps> = (props) => {
setIsOpen(false); setIsOpen(false);
}; };
const extensionOpen = (item: any) => {
setSourceData({
source: {
name: item.name,
icon: item.icon,
},
querySource: {
id: "extensions",
},
main_extension_id: item.id,
});
setSearchValue(item.name || "");
};
const otherExtensionOpen = (item: any) => {
changeInput("");
//
const extension = { ...item };
let developerId = extension.developer.id;
let extensionId = extension.id;
const bundleId = {
developer: developerId,
extension_id: extensionId,
sub_extension_id: null,
};
platformAdapter.invokeBackend("open_third_party_extension", {
bundleId,
});
};
const handleOpen = async (item: any) => {
if (!item) return;
// close extension store
setVisibleExtensionStore(false);
setVisibleExtensionDetail(false);
//
if (item.type === "group" || item.type === "extension") {
extensionOpen(item);
} else {
otherExtensionOpen(item);
}
};
const renderDivider = () => { const renderDivider = () => {
return <div className="my-4 h-px bg-[#E6E6E6] dark:bg-[#262626]"></div>; return <div className="my-4 h-px bg-[#E6E6E6] dark:bg-[#262626]"></div>;
}; };
@@ -69,13 +126,19 @@ const ExtensionDetail: FC<ExtensionDetailProps> = (props) => {
<div className="pt-2"> <div className="pt-2">
{selectedExtension.installed ? ( {selectedExtension.installed ? (
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Trash2 <Button
className="size-4 text-red-500 cursor-pointer" className="flex justify-center items-center h-6 px-3 rounded-full bg-[#007BFF] hover:bg-[#007BFF] text-white ring-0 ring-offset-0 focus-visible:ring-0 focus-visible:ring-offset-0 outline-none"
onClick={() => { onClick={() => handleOpen(selectedExtension)}
setIsOpen(true); >
}} <SquareArrowOutUpRight className="size-4" />
/> </Button>
<div className="flex items-center gap-1 h-6 px-2 rounded-full text-[#22C461] bg-[#22C461]/20"> <Button
className="flex justify-center items-center h-6 px-3 rounded-full bg-[#FFE2E2] hover:bg-[#FFE2E2] text-red-500 ring-0 ring-offset-0 focus-visible:ring-0 focus-visible:ring-offset-0 outline-none"
onClick={() => setIsOpen(true)}
>
<Trash2 className="size-4" />
</Button>
<div className="flex items-center gap-1 h-6 px-2 rounded-full text-[#999999] bg-[#E6E6E6]">
<CircleCheck className="size-4" /> <CircleCheck className="size-4" />
<span>{t("extensionDetail.hints.installed")}</span> <span>{t("extensionDetail.hints.installed")}</span>
</div> </div>
@@ -170,7 +233,8 @@ const ExtensionDetail: FC<ExtensionDetailProps> = (props) => {
<DeleteDialog <DeleteDialog
reverseButtonPosition reverseButtonPosition
isOpen={isOpen} isOpen={isOpen}
title={`${t("extensionDetail.deleteDialog.title")} ${selectedExtension.name title={`${t("extensionDetail.deleteDialog.title")} ${
selectedExtension.name
}`} }`}
description={t("extensionDetail.deleteDialog.description")} description={t("extensionDetail.deleteDialog.description")}
cancelButtonProps={{ cancelButtonProps={{

View File

@@ -73,7 +73,13 @@ export interface SearchExtensionItem {
}>; }>;
} }
const ExtensionStore = ({ extensionId }: { extensionId?: string }) => { const ExtensionStore = ({
extensionId,
changeInput,
}: {
extensionId?: string;
changeInput: (value: string) => void;
}) => {
const { const {
searchValue, searchValue,
selectedExtension, selectedExtension,
@@ -244,7 +250,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();
@@ -295,6 +301,7 @@ const ExtensionStore = ({ extensionId }: { extensionId?: string }) => {
<ExtensionDetail <ExtensionDetail
onInstall={handleInstall} onInstall={handleInstall}
onUninstall={handleUnInstall} onUninstall={handleUnInstall}
changeInput={changeInput}
/> />
) : ( ) : (
<> <>
@@ -306,7 +313,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,13 @@ 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;
@@ -124,7 +124,9 @@ const SearchResultsPanel = memo<{
// If state gets updated, render the UI // If state gets updated, render the UI
if (visibleExtensionStore) { if (visibleExtensionStore) {
return <ExtensionStore extensionId={extensionId} />; return (
<ExtensionStore extensionId={extensionId} changeInput={changeInput} />
);
} }
// Render the view extension // Render the view extension
@@ -133,11 +135,14 @@ const SearchResultsPanel = memo<{
} }
if (goAskAi) return <AskAi isChatMode={isChatMode} />; if (goAskAi) return <AskAi isChatMode={isChatMode} />;
if (sourceData) {
return <SearchResults input={input} isChatMode={isChatMode} />;
}
if (suggests.length === 0) return <NoResults />; if (suggests.length === 0) return <NoResults />;
return sourceData ? ( return (
<SearchResults input={input} isChatMode={isChatMode} />
) : (
<DropdownList <DropdownList
suggests={suggests} suggests={suggests}
searchData={searchData} searchData={searchData}

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

@@ -302,6 +302,7 @@ export function useChatActions(
); );
const getChatHistory = useCallback(async () => { const getChatHistory = useCallback(async () => {
try {
let response: any; let response: any;
if (isTauri) { if (isTauri) {
if (await unrequitable()) { if (await unrequitable()) {
@@ -326,6 +327,9 @@ export function useChatActions(
const hits = response?.hits?.hits || []; const hits = response?.hits?.hits || [];
setChats(hits); setChats(hits);
} catch (error) {
console.error("getChatHistory error:", error);
}
}, [ }, [
currentService?.id, currentService?.id,
keyword, keyword,

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)
} }
}, []); }, []);

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