1 Commits

Author SHA1 Message Date
Steve Lau
6b0c477ff8 refactor: give more weight to commands 2025-12-16 16:49:13 +08:00
131 changed files with 2929 additions and 5073 deletions

4
.gitignore vendored
View File

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

View File

@@ -10,7 +10,6 @@
"dataurl",
"deeplink",
"deepthink",
"Detch",
"dtolnay",
"dyld",
"elif",
@@ -38,18 +37,15 @@
"meval",
"Minimizable",
"msvc",
"njsproj",
"nord",
"nowrap",
"nspanel",
"nsstring",
"ntvs",
"objc",
"overscan",
"partialize",
"patchelf",
"Quicklink",
"Quicklinks",
"Raycast",
"rehype",
"reqwest",
@@ -57,9 +53,7 @@
"rgba",
"rustup",
"screenshotable",
"seprate",
"serde",
"Shadcn",
"swatinem",
"tailwindcss",
"tauri",
@@ -67,7 +61,6 @@
"timedout",
"titlebar",
"tpddns",
"trae",
"traptitech",
"unlisten",
"unlistener",

View File

@@ -7,27 +7,12 @@ title: "Release Notes"
Information about release notes of Coco App is provided here.
## Latest (In development)
## Latest (In development)
### ❌ Breaking changes
### 🚀 Features
- feat: support app search even if Spotlight is disabled #1028
### 🐛 Bug fix
### ✈️ Improvements
## 0.10.0 (2025-12-19)
### ❌ Breaking changes
### 🚀 Features
- feat: resizable extension UI #1009
- feat: add open button to launch installed extension #1013
### 🐛 Bug fix
- fix: fix the abnormal input height issue #1006
@@ -35,9 +20,7 @@ Information about release notes of Coco App is provided here.
### ✈️ Improvements
- refactor: replace legacy components with shadcn/ui components #1002
- chore: show error msg (not err code) when installing exts via deeplink fails #1007
- refactor: treat Applications and File Search as normal extensions #1012
## 0.9.1 (2025-12-05)

View File

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

1550
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -1,39 +0,0 @@
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();

16
src-tauri/Cargo.lock generated
View File

@@ -332,7 +332,7 @@ checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61"
[[package]]
name = "applications"
version = "0.3.1"
source = "git+https://github.com/infinilabs/applications-rs?rev=ec174b7761bfa5eb7af0a93218b014e2d1505643#ec174b7761bfa5eb7af0a93218b014e2d1505643"
source = "git+https://github.com/infinilabs/applications-rs?rev=b5fac4034a40d42e72f727f1aa1cc1f19fe86653#b5fac4034a40d42e72f727f1aa1cc1f19fe86653"
dependencies = [
"anyhow",
"core-foundation 0.9.4",
@@ -1132,7 +1132,7 @@ dependencies = [
[[package]]
name = "coco"
version = "0.10.0"
version = "0.9.1"
dependencies = [
"actix-files",
"actix-web",
@@ -1184,7 +1184,6 @@ dependencies = [
"scraper",
"semver",
"serde",
"serde-inline-default",
"serde_json",
"serde_plain",
"snafu",
@@ -6309,17 +6308,6 @@ dependencies = [
"serde_derive",
]
[[package]]
name = "serde-inline-default"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "92d48532bc0781ac622a5fea0f16502d3b4f1af0fcebe56d618120969f35d315"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.111",
]
[[package]]
name = "serde-untagged"
version = "0.1.9"

View File

@@ -1,6 +1,6 @@
[package]
name = "coco"
version = "0.10.0"
version = "0.9.1"
description = "Search, connect, collaborate all in one place."
authors = ["INFINI Labs"]
edition = "2024"
@@ -62,7 +62,7 @@ tauri-plugin-drag = "2"
tauri-plugin-macos-permissions = "2"
tauri-plugin-fs-pro = "2"
tauri-plugin-screenshots = "2"
applications = { git = "https://github.com/infinilabs/applications-rs", rev = "ec174b7761bfa5eb7af0a93218b014e2d1505643" }
applications = { git = "https://github.com/infinilabs/applications-rs", rev = "b5fac4034a40d42e72f727f1aa1cc1f19fe86653" }
tokio-native-tls = "0.3" # For wss connections
tokio = { version = "1", features = ["full"] }
tokio-tungstenite = { version = "0.20", features = ["native-tls"] }
@@ -122,7 +122,6 @@ actix-web = "4.11.0"
tauri-plugin-clipboard-manager = "2"
tauri-plugin-zustand = "1"
snafu = "0.8.9"
serde-inline-default = "1.0.0"
[dev-dependencies]
tempfile = "3.23.0"

View File

@@ -31,11 +31,6 @@
"core:window:deny-internal-toggle-maximize",
"core:window:allow-set-shadow",
"core:window:allow-set-position",
"core:window:allow-set-theme",
"core:window:allow-unminimize",
"core:window:allow-set-fullscreen",
"core:window:allow-set-resizable",
"core:window:allow-maximize",
"core:app:allow-set-app-theme",
"shell:default",
"http:default",
@@ -70,10 +65,12 @@
"fs-pro:default",
"macos-permissions:default",
"screenshots:default",
"core:window:allow-set-theme",
"process:default",
"updater:default",
"windows-version:default",
"log:default",
"opener:default"
"opener:default",
"core:window:allow-unminimize"
]
}

View File

@@ -152,31 +152,14 @@ pub struct Extension {
}
/// Settings that control the built-in UI Components
#[serde_inline_default::serde_inline_default]
#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
pub(crate) struct ViewExtensionUISettings {
/// Show the search bar
#[serde_inline_default(false)]
search_bar: bool,
/// Show the filter bar
#[serde_inline_default(false)]
filter_bar: bool,
/// Show the footer
#[serde_inline_default(false)]
footer: bool,
/// The recommended width of the window for this extension
width: Option<u32>,
/// The recommended heigh of the window for this extension
height: Option<u32>,
/// Is the extension window's size adjustable?
#[serde_inline_default(false)]
resizable: bool,
/// Detch the extension window from Coco's main window.
///
/// If true, user can click the detach button to open this
/// extension in a seprate window.
#[serde_inline_default(false)]
detachable: bool,
}
/// Bundle ID uniquely identifies an extension.
@@ -235,117 +218,112 @@ impl<'ext> PartialEq<ExtensionBundleId> for ExtensionBundleIdBorrowed<'ext> {
}
}
#[tauri::command]
pub(crate) fn extension_on_opened(extension: Extension) -> Option<OnOpened> {
_extension_on_opened(&extension)
}
/// Return what will happen when we open this extension.
///
/// `None` if it cannot be opened.
pub(crate) fn _extension_on_opened(extension: &Extension) -> Option<OnOpened> {
let settings = extension.settings.clone();
let permission = extension.permission.clone();
match extension.r#type {
// This function, at the time of writing this comment, is primarily
// used by third-party extensions.
//
// Built-in extensions don't use this as they are technically not
// "struct Extension"s. Typically, they directly construct a
// "struct Document" from their own type.
ExtensionType::Calculator => unreachable!("this is handled by frontend"),
ExtensionType::AiExtension => unreachable!(
"currently, all AI extensions we have are non-searchable, so we won't open them"
),
ExtensionType::Application => {
// We can have a impl like:
//
// Some(OnOpened::Application { app_path: self.id.clone() })
//
// but it won't be used.
unreachable!(
"Applications are not \"struct Extension\" under the hood, they won't call this method"
)
}
// These 2 types of extensions cannot be opened
ExtensionType::Group => return None,
ExtensionType::Extension => return None,
ExtensionType::Command => {
let ty = ExtensionOnOpenedType::Command {
action: extension.action.clone().unwrap_or_else(|| {
panic!(
"Command extension [{}]'s [action] field is not set, something wrong with your extension validity check", extension.id
)
}),
};
let extension_on_opened = ExtensionOnOpened {
ty,
settings,
permission,
};
Some(OnOpened::Extension(extension_on_opened))
}
ExtensionType::Quicklink => {
let quicklink = extension.quicklink.clone().unwrap_or_else(|| {
panic!(
"Quicklink extension [{}]'s [quicklink] field is not set, something wrong with your extension validity check", extension.id
)
});
let ty = ExtensionOnOpenedType::Quicklink {
link: quicklink.link,
open_with: quicklink.open_with,
};
let extension_on_opened = ExtensionOnOpened {
ty,
settings,
permission,
};
Some(OnOpened::Extension(extension_on_opened))
}
ExtensionType::Script => todo!("not supported yet"),
ExtensionType::Setting => todo!("not supported yet"),
ExtensionType::View => {
let name = extension.name.clone();
let icon = extension.icon.clone();
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", extension.id);
}).clone();
let ui = extension.ui.clone();
let extension_on_opened_type = ExtensionOnOpenedType::View {
name,
icon,
page,
ui,
};
let extension_on_opened = ExtensionOnOpened {
ty: extension_on_opened_type,
settings,
permission,
};
let on_opened = OnOpened::Extension(extension_on_opened);
Some(on_opened)
}
ExtensionType::Unknown => {
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()
self.on_opened().is_some()
}
/// Return what will happen when we open this extension.
///
/// `None` if it cannot be opened.
pub(crate) fn on_opened(&self) -> Option<OnOpened> {
let settings = self.settings.clone();
let permission = self.permission.clone();
match self.r#type {
// This function, at the time of writing this comment, is primarily
// used by third-party extensions.
//
// Built-in extensions don't use this as they are technically not
// "struct Extension"s. Typically, they directly construct a
// "struct Document" from their own type.
ExtensionType::Calculator => unreachable!("this is handled by frontend"),
ExtensionType::AiExtension => unreachable!(
"currently, all AI extensions we have are non-searchable, so we won't open them"
),
ExtensionType::Application => {
// We can have a impl like:
//
// Some(OnOpened::Application { app_path: self.id.clone() })
//
// but it won't be used.
unreachable!(
"Applications are not \"struct Extension\" under the hood, they won't call this method"
)
}
// These 2 types of extensions cannot be opened
ExtensionType::Group => return None,
ExtensionType::Extension => return None,
ExtensionType::Command => {
let ty = ExtensionOnOpenedType::Command {
action: self.action.clone().unwrap_or_else(|| {
panic!(
"Command extension [{}]'s [action] field is not set, something wrong with your extension validity check", self.id
)
}),
};
let extension_on_opened = ExtensionOnOpened {
ty,
settings,
permission,
};
Some(OnOpened::Extension(extension_on_opened))
}
ExtensionType::Quicklink => {
let quicklink = self.quicklink.clone().unwrap_or_else(|| {
panic!(
"Quicklink extension [{}]'s [quicklink] field is not set, something wrong with your extension validity check", self.id
)
});
let ty = ExtensionOnOpenedType::Quicklink {
link: quicklink.link,
open_with: quicklink.open_with,
};
let extension_on_opened = ExtensionOnOpened {
ty,
settings,
permission,
};
Some(OnOpened::Extension(extension_on_opened))
}
ExtensionType::Script => todo!("not supported yet"),
ExtensionType::Setting => todo!("not supported yet"),
ExtensionType::View => {
let name = self.name.clone();
let icon = self.icon.clone();
let page = self.page.as_ref().unwrap_or_else(|| {
panic!("View extension [{}]'s [page] field is not set, something wrong with your extension validity check", self.id);
}).clone();
let ui = self.ui.clone();
let extension_on_opened_type = ExtensionOnOpenedType::View {
name,
icon,
page,
ui,
};
let extension_on_opened = ExtensionOnOpened {
ty: extension_on_opened_type,
settings,
permission,
};
let on_opened = OnOpened::Extension(extension_on_opened);
Some(on_opened)
}
ExtensionType::Unknown => {
unreachable!("Extensions of type [Unknown] should never be opened")
}
}
}
pub(crate) fn get_sub_extension(&self, sub_extension_id: &str) -> Option<&Self> {
@@ -728,30 +706,7 @@ fn filter_out_extensions(
extensions.retain(|ext| {
let ty = ext.r#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
}
ty == ExtensionType::Group || ty == ExtensionType::Extension || ty == extension_type
});
// Filter sub-extensions to only include the requested type
@@ -771,6 +726,19 @@ 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
@@ -786,23 +754,8 @@ fn filter_out_extensions(
extensions.retain(|ext| {
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
true
}
// Keep all group/extension types
true
} else {
// Apply filter to non-group/extension types
match_closure(ext)
@@ -859,8 +812,7 @@ pub(crate) async fn list_extensions(
// 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;
if filter_performed {
extensions.retain(|ext| {
@@ -868,20 +820,11 @@ pub(crate) async fn list_extensions(
return true;
}
/*
* Two exceptions: "Applications" and "File Search"
*
* 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;
}
// We don't do this filter to applications since it is always empty, load at runtime.
if ext.developer.is_none()
&& ext.id == built_in::application::QUERYSOURCE_ID_DATASOURCE_ID_DATASOURCE_NAME
{
return true;
}
let has_commands = ext

View File

@@ -16,8 +16,6 @@ use crate::common::search::QueryResponse;
use crate::common::search::QuerySource;
use crate::common::search::SearchQuery;
use crate::common::traits::SearchSource;
use crate::extension::_extension_on_opened;
use crate::extension::ExtensionBundleId;
use crate::extension::ExtensionBundleIdBorrowed;
use crate::extension::ExtensionType;
use crate::extension::PLUGIN_JSON_FIELD_MINIMUM_COCO_VERSION;
@@ -28,13 +26,11 @@ use crate::util::platform::Platform;
use crate::util::version::COCO_VERSION;
use crate::util::version::parse_coco_semver;
use async_trait::async_trait;
use borrowme::Borrow;
use borrowme::ToOwned;
use check::general_check;
use function_name::named;
use semver::Version as SemVer;
use serde_json::Value as Json;
use snafu::prelude::*;
use std::io::ErrorKind;
use std::ops::Deref;
use std::path::Path;
@@ -48,7 +44,6 @@ use tauri_plugin_global_shortcut::GlobalShortcutExt;
use tauri_plugin_global_shortcut::ShortcutState;
use tokio::fs::read_dir;
use tokio::sync::RwLock;
use tokio::sync::RwLockReadGuard;
use tokio::sync::RwLockWriteGuard;
pub(crate) fn get_third_party_extension_directory(tauri_app_handle: &AppHandle) -> PathBuf {
@@ -398,26 +393,6 @@ impl ThirdPartyExtensionsSearchSource {
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()`
///
/// This function does the actual job, i.e., to enable/activate the extension.
@@ -432,7 +407,7 @@ impl ThirdPartyExtensionsSearchSource {
) -> Result<(), String> {
if extension.supports_alias_hotkey() {
if let Some(ref hotkey) = extension.hotkey {
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 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 extension_id_clone = extension.id.clone();
tauri_app_handle
@@ -706,7 +681,7 @@ impl ThirdPartyExtensionsSearchSource {
)?;
// Set hotkey
let on_opened = _extension_on_opened(extension).unwrap_or_else(|| panic!(
let on_opened = extension.on_opened().unwrap_or_else(|| panic!(
"setting hotkey for an extension that cannot be opened, extension ID [{:?}], extension type [{:?}]", bundle_id, extension.r#type,
));
@@ -892,59 +867,6 @@ impl ThirdPartyExtensionsSearchSource {
pub(crate) async fn extensions_snapshot(&self) -> Vec<Extension> {
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> =
@@ -968,37 +890,25 @@ impl SearchSource for ThirdPartyExtensionsSearchSource {
}
}
// query main_extension_id querysource
// main_extension_id querysource
// query querysource datasource
async fn search(
&self,
_tauri_app_handle: AppHandle,
query: SearchQuery,
) -> Result<QueryResponse, SearchError> {
let opt_lowercase_query_string: Option<String> = {
match query.query_strings.get("query") {
Some(query_string) => {
if query_string.is_empty() {
None
} else {
Some(query_string.to_lowercase())
}
}
None => None,
}
let Some(query_string) = query.query_strings.get("query") else {
return Ok(QueryResponse {
source: self.get_type(),
hits: Vec::new(),
total_hits: 0,
});
};
let opt_data_source = query
.query_strings
.get("datasource")
.map(|str| str.to_string());
let opt_main_extension_id = query
.query_strings
.get("main_extension_id")
.map(|str| str.to_string());
.map(|owned_str| owned_str.to_string());
let query_lower = query_string.to_lowercase();
let inner_clone = Arc::clone(&self.inner);
let closure = move || {
@@ -1006,22 +916,10 @@ impl SearchSource for ThirdPartyExtensionsSearchSource {
let extensions_read_lock =
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
.iter()
.filter(main_extension_filter_closure)
// field minimum_coco_extension is only set for main extensions.
.filter(|ext| ext.enabled && is_extension_compatible(Extension::clone(ext)))
{
if extension.r#type.contains_sub_items() {
let opt_main_extension_lowercase_name =
@@ -1036,7 +934,7 @@ impl SearchSource for ThirdPartyExtensionsSearchSource {
for command in commands.iter().filter(|cmd| cmd.enabled) {
if let Some(hit) = extension_to_hit(
command,
opt_lowercase_query_string.as_deref(),
&query_lower,
opt_data_source.as_deref(),
opt_main_extension_lowercase_name.as_deref(),
) {
@@ -1049,7 +947,7 @@ impl SearchSource for ThirdPartyExtensionsSearchSource {
for script in scripts.iter().filter(|script| script.enabled) {
if let Some(hit) = extension_to_hit(
script,
opt_lowercase_query_string.as_deref(),
&query_lower,
opt_data_source.as_deref(),
opt_main_extension_lowercase_name.as_deref(),
) {
@@ -1062,7 +960,7 @@ impl SearchSource for ThirdPartyExtensionsSearchSource {
for quicklink in quicklinks.iter().filter(|link| link.enabled) {
if let Some(hit) = extension_to_hit(
quicklink,
opt_lowercase_query_string.as_deref(),
&query_lower,
opt_data_source.as_deref(),
opt_main_extension_lowercase_name.as_deref(),
) {
@@ -1075,7 +973,7 @@ impl SearchSource for ThirdPartyExtensionsSearchSource {
for view in views.iter().filter(|view| view.enabled) {
if let Some(hit) = extension_to_hit(
view,
opt_lowercase_query_string.as_deref(),
&query_lower,
opt_data_source.as_deref(),
opt_main_extension_lowercase_name.as_deref(),
) {
@@ -1084,12 +982,9 @@ impl SearchSource for ThirdPartyExtensionsSearchSource {
}
}
} else {
if let Some(hit) = extension_to_hit(
extension,
opt_lowercase_query_string.as_deref(),
opt_data_source.as_deref(),
None,
) {
if let Some(hit) =
extension_to_hit(extension, &query_lower, opt_data_source.as_deref(), None)
{
hits.push(hit);
}
}
@@ -1134,9 +1029,9 @@ pub(crate) async fn uninstall_extension(
/// 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
/// extension name, score it and take that into account.
fn extension_to_hit(
pub(crate) fn extension_to_hit(
extension: &Extension,
opt_lowercase_query_string: Option<&str>,
query_lower: &str,
opt_data_source: Option<&str>,
opt_main_extension_lowercase_name: Option<&str>,
) -> Option<(Document, f64)> {
@@ -1155,66 +1050,64 @@ fn extension_to_hit(
}
let mut total_score = 0.0;
if let Some(query_lower) = opt_lowercase_query_string {
// Score based on title match
// Title is considered more important, so it gets a higher weight.
if let Some(title_score) =
calculate_text_similarity(query_lower, &extension.name.to_lowercase())
{
total_score += title_score;
}
// Score based on alias match if available
// Alias is considered less important than title, so it gets a lower weight.
if let Some(alias) = &extension.alias {
if let Some(alias_score) = calculate_text_similarity(query_lower, &alias.to_lowercase())
{
total_score += alias_score;
}
}
// Score based on title match
// Title is considered more important, so it gets a higher weight.
if let Some(title_score) =
calculate_text_similarity(&query_lower, &extension.name.to_lowercase())
{
total_score += title_score;
}
// An "extension" type extension should return all its
// sub-extensions when the query string matches its ID.
// To do this, we score the extension ID and take that
// into account.
if let Some(main_extension_lowercase_id) = opt_main_extension_lowercase_name {
if let Some(main_extension_score) =
calculate_text_similarity(query_lower, main_extension_lowercase_id)
{
total_score += main_extension_score;
}
}
// Only filter by score if query string is set
if total_score == 0.0 {
return None;
// Score based on alias match if available
// Alias is considered less important than title, so it gets a lower weight.
if let Some(alias) = &extension.alias {
if let Some(alias_score) = calculate_text_similarity(&query_lower, &alias.to_lowercase()) {
total_score += alias_score;
}
}
let on_opened = _extension_on_opened(extension).unwrap_or_else(|| {
panic!(
"extension (id [{}], type [{:?}]) is searchable, and should have a valid on_opened",
extension.id, extension.r#type
)
});
let url = on_opened.url();
// An "extension" type extension should return all its
// sub-extensions when the query string matches its ID.
// To do this, we score the extension ID and take that
// into account.
if let Some(main_extension_lowercase_id) = opt_main_extension_lowercase_name {
if let Some(main_extension_score) =
calculate_text_similarity(&query_lower, main_extension_lowercase_id)
{
total_score += main_extension_score;
}
}
let document = Document {
id: extension.id.clone(),
title: Some(extension.name.clone()),
icon: Some(extension.icon.clone()),
on_opened: Some(on_opened),
url: Some(url),
category: Some(extension_type_string.clone()),
source: Some(DataSourceReference {
id: Some(extension_type_string.clone()),
name: Some(extension_type_string.clone()),
icon: None,
r#type: Some(extension_type_string),
}),
// Only include if there's some relevance (score is meaningfully positive)
if total_score > 0.01 {
let on_opened = extension.on_opened().unwrap_or_else(|| {
panic!(
"extension (id [{}], type [{:?}]) is searchable, and should have a valid on_opened",
extension.id, extension.r#type
)
});
let url = on_opened.url();
..Default::default()
};
let document = Document {
id: extension.id.clone(),
title: Some(extension.name.clone()),
icon: Some(extension.icon.clone()),
on_opened: Some(on_opened),
url: Some(url),
category: Some(extension_type_string.clone()),
source: Some(DataSourceReference {
id: Some(extension_type_string.clone()),
name: Some(extension_type_string.clone()),
icon: None,
r#type: Some(extension_type_string),
}),
Some((document, total_score))
..Default::default()
};
Some((document, total_score))
} else {
None
}
}

View File

@@ -29,6 +29,7 @@ use tauri_plugin_autostart::MacosLauncher;
/// Tauri store name
pub(crate) const COCO_TAURI_STORE: &str = "coco_tauri_store";
pub(crate) const WINDOW_CENTER_BASELINE_HEIGHT: i32 = 590;
lazy_static! {
static ref PREVIOUS_MONITOR_NAME: Mutex<Option<String>> = Mutex::new(None);
@@ -43,6 +44,37 @@ lazy_static! {
/// you access it.
pub(crate) static GLOBAL_TAURI_APP_HANDLE: OnceLock<AppHandle> = OnceLock::new();
#[tauri::command]
async fn change_window_height(handle: AppHandle, height: u32) {
let window: WebviewWindow = handle.get_webview_window(MAIN_WINDOW_LABEL).unwrap();
let mut size = window.outer_size().unwrap();
size.height = height;
window.set_size(size).unwrap();
// Center the window horizontally and vertically based on the baseline height of 590
let monitor = window.primary_monitor().ok().flatten().or_else(|| {
window
.available_monitors()
.ok()
.and_then(|ms| ms.into_iter().next())
});
if let Some(monitor) = monitor {
let monitor_position = monitor.position();
let monitor_size = monitor.size();
let outer_size = window.outer_size().unwrap();
let window_width = outer_size.width as i32;
let x = monitor_position.x + (monitor_size.width as i32 - window_width) / 2;
let y =
monitor_position.y + (monitor_size.height as i32 - WINDOW_CENTER_BASELINE_HEIGHT) / 2;
let _ = window.set_position(PhysicalPosition::new(x, y));
}
}
// Removed unused Payload to avoid unnecessary serde derive macro invocations
#[cfg_attr(mobile, tauri::mobile_entry_point)]
@@ -91,6 +123,7 @@ pub fn run() {
let app = app_builder
.invoke_handler(tauri::generate_handler![
change_window_height,
shortcut::change_shortcut,
shortcut::unregister_shortcut,
shortcut::get_current_shortcut,
@@ -144,7 +177,6 @@ pub fn run() {
extension::enable_extension,
extension::disable_extension,
extension::set_extension_alias,
extension::extension_on_opened,
extension::register_extension_hotkey,
extension::unregister_extension_hotkey,
extension::is_extension_enabled,
@@ -153,7 +185,6 @@ pub fn run() {
extension::third_party::install::store::install_extension_from_store,
extension::third_party::install::local_extension::install_local_extension,
extension::third_party::uninstall_extension,
extension::third_party::open_third_party_extension,
extension::is_extension_compatible,
extension::api::apis,
extension::api::fs::read_dir,
@@ -347,13 +378,12 @@ fn move_window_to_active_monitor(window: &WebviewWindow) {
return;
}
};
let window_width = window_size.width as i32;
let window_height = 590 * scale_factor as i32;
// Horizontal center uses actual width, vertical center uses 590 baseline
let window_x = monitor_position.x + (monitor_size.width as i32 - window_width) / 2;
let window_y = monitor_position.y + (monitor_size.height as i32 - window_height) / 2;
let window_y = monitor_position.y
+ (monitor_size.height as i32 - WINDOW_CENTER_BASELINE_HEIGHT) / 2;
if let Err(e) = window.set_position(PhysicalPosition::new(window_x, window_y)) {
log::error!("Failed to move window: {}", e);

View File

@@ -17,20 +17,6 @@ use std::collections::HashMap;
use std::sync::Arc;
use tauri::{AppHandle, Manager};
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]
#[tauri::command]
pub async fn query_coco_fusion(
@@ -40,10 +26,6 @@ pub async fn query_coco_fusion(
query_strings: HashMap<String, String>,
query_timeout: u64,
) -> 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 search_sources = tauri_app_handle.state::<SearchSourceRegistry>();
let query_source_list = search_sources.get_sources().await;
@@ -299,6 +281,17 @@ async fn query_coco_fusion_multi_query_sources(
}
}
/*
* Temporary patch
*
* Extensions should have a higher weight than query results.
*/
for (query_source, hits) in all_hits_grouped_by_query_source.iter_mut() {
if query_source.r#type == LOCAL_QUERY_SOURCE_TYPE && query_source.id == "extensions" {
hits.iter_mut().for_each(|hit| hit.score = hit.score * 1.5);
}
}
/*
* Sort hits within each source by score (descending) in case data sources
* do not sort them

View File

@@ -32,7 +32,7 @@ pub fn platform(
let panel = main_window.to_panel::<NsPanel>().unwrap();
// set level
panel.set_level(PanelLevel::Dock.value());
panel.set_level(PanelLevel::Utility.value());
// Do not steal focus from other windows
panel.set_style_mask(StyleMask::empty().nonactivating_panel().into());

View File

@@ -20,7 +20,7 @@
"width": 680,
"decorations": false,
"minimizable": false,
"maximizable": true,
"maximizable": false,
"skipTaskbar": true,
"resizable": false,
"acceptFirstMouse": true,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -63,7 +63,6 @@ export function Connect({ setIsConnect, onAddServer }: ConnectServiceProps) {
type="text"
id="endpoint"
value={endpointLink}
autoCorrect="off"
placeholder={t("cloud.connect.serverPlaceholder")}
onChange={onChangeEndpoint}
className="text-[#101010] dark:text-white flex-1 px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent dark:bg-gray-800"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -57,13 +57,13 @@ const ErrorNotification = ({
>
<div className="flex items-center">
{visibleError.type === "error" && (
<AlertCircle className="size-5 shrink-0 text-red-500 mr-2" />
<AlertCircle className="w-5 h-5 text-red-500 mr-2" />
)}
{visibleError.type === "warning" && (
<AlertTriangle className="size-5 shrink-0 text-yellow-500 mr-2" />
<AlertTriangle className="w-5 h-5 text-yellow-500 mr-2" />
)}
{visibleError.type === "info" && (
<Info className="size-5 shrink-0 text-blue-500 mr-2" />
<Info className="w-5 h-5 text-blue-500 mr-2" />
)}
<span className="text-sm text-gray-700 dark:text-gray-200">
@@ -78,7 +78,7 @@ const ErrorNotification = ({
</div>
<X
className="size-5 shrink-0 ml-4 cursor-pointer text-gray-400 hover:text-gray-600"
className="w-5 h-5 ml-4 cursor-pointer text-gray-400 hover:text-gray-600"
onClick={() => removeError(visibleError.id)}
/>
</div>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,35 @@
import { Input, InputProps } from "@headlessui/react";
import { useKeyPress } from "ahooks";
import { forwardRef, useImperativeHandle, useRef } from "react";
import { POPOVER_PANEL_SELECTOR } from "@/constants";
const PopoverInput = forwardRef<HTMLInputElement, InputProps>((props, ref) => {
const inputRef = useRef<HTMLInputElement>(null);
useImperativeHandle(ref, () => inputRef.current!);
useKeyPress(
"esc",
(event) => {
if (inputRef.current === document.activeElement) {
event.preventDefault();
event.stopPropagation();
inputRef.current?.blur();
const parentPanel = inputRef.current?.closest(POPOVER_PANEL_SELECTOR);
if (parentPanel instanceof HTMLElement) {
parentPanel.focus();
}
}
},
{
target: inputRef,
}
);
return <Input autoCorrect="off" ref={inputRef} {...props} />;
});
export default PopoverInput;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,11 +1,9 @@
import { FC, HTMLAttributes, useEffect, useRef, useState } from "react";
import { useKeyPress } from "ahooks";
import clsx from "clsx";
import { last } from "lodash-es";
import {
OPENED_POPOVER_TRIGGER_SELECTOR,
POPOVER_PANEL_SELECTOR,
} from "@/constants";
import { POPOVER_PANEL_SELECTOR } from "@/constants";
import { useShortcutsStore } from "@/stores/shortcutsStore";
import { useAppStore } from "@/stores/appStore";
import { KeyType } from "ahooks/lib/useKeyPress";
@@ -45,21 +43,22 @@ const VisibleKey: FC<VisibleKeyProps> = (props) => {
const [visibleShortcut, setVisibleShortcut] = useState<boolean>();
useEffect(() => {
const popoverPanelEl = document.querySelector(POPOVER_PANEL_SELECTOR);
const openedPopoverTriggerEl = document.querySelector(
OPENED_POPOVER_TRIGGER_SELECTOR
);
const popoverPanelEls = document.querySelectorAll(POPOVER_PANEL_SELECTOR);
const popoverPanelEl = last(popoverPanelEls);
if (!openPopover || !popoverPanelEl) {
return setVisibleShortcut(modifierKeyPressed);
}
const isChildInPanel = popoverPanelEl?.contains(childrenRef.current);
const isChildInTrigger = openedPopoverTriggerEl?.contains(
childrenRef.current
const popoverButtonEl = document.querySelector(
`[aria-controls="${popoverPanelEl.id}"]`
);
const isChildInPopover = isChildInPanel || isChildInTrigger;
const isChildInPanel = popoverPanelEl?.contains(childrenRef.current);
const isChildInButton = popoverButtonEl?.contains(childrenRef.current);
const isChildInPopover = isChildInPanel || isChildInButton;
setVisibleShortcut(isChildInPopover && modifierKeyPressed);
}, [openPopover, modifierKeyPressed]);
@@ -112,7 +111,7 @@ const VisibleKey: FC<VisibleKeyProps> = (props) => {
{showTooltip && visibleShortcut ? (
<div
className={clsx(
"size-4 flex items-center justify-center font-normal text-xs text-[#333] leading-3.5 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",
"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",
shortcutClassName
)}
>

View File

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

View File

@@ -1,6 +1,5 @@
import { useCallback, useRef, useMemo, useState, useEffect } from "react";
import { cloneDeep, isEmpty } from "lodash-es";
import { useKeyPress } from "ahooks";
import { useSearchStore } from "@/stores/searchStore";
import { useExtensionsStore } from "@/stores/extensionsStore";
@@ -9,6 +8,7 @@ import { Get } from "@/api/axiosRequest";
import type { Assistant } from "@/types/chat";
import { useAppStore } from "@/stores/appStore";
import { canNavigateBack, navigateBack } from "@/utils";
import { useKeyPress } from "ahooks";
import { useShortcutsStore } from "@/stores/shortcutsStore";
interface AssistantManagerProps {
@@ -167,7 +167,7 @@ export function useAssistantManager({
const { selectedSearchContent, visibleExtensionStore } =
useSearchStore.getState();
// console.log("selectedSearchContent", selectedSearchContent);
console.log("selectedSearchContent", selectedSearchContent);
const { id, type, category } = selectedSearchContent ?? {};

View File

@@ -1,4 +1,3 @@
import { cn } from "@/lib/utils";
import { useAppStore } from "@/stores/appStore";
import { useWebConfigStore } from "@/stores/webConfigStore";
import { useBoolean } from "ahooks";
@@ -38,7 +37,6 @@ const AutoResizeTextarea = forwardRef<
setInput,
handleKeyDown,
chatPlaceholder,
lineCount,
onLineCountChange,
firstLineMaxWidth,
},
@@ -81,10 +79,8 @@ const AutoResizeTextarea = forwardRef<
let height = lineHeight;
let minHeight = lineHeight;
const hasNewline = /[\r\n]/.test(input);
const hasContent = input.length > 0;
const firstLineExceeds =
hasContent &&
(calcRef.current?.offsetWidth ?? 0) >= Math.max(firstLineMaxWidth - 32, 0);
calcRef.current?.offsetWidth >= firstLineMaxWidth - 32;
if (hasNewline || firstLineExceeds) {
minHeight = lineHeight * 2;
@@ -119,12 +115,7 @@ const AutoResizeTextarea = forwardRef<
autoComplete="off"
autoCapitalize="none"
spellCheck="false"
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,
}
)}
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"
placeholder={chatPlaceholder || t("search.textarea.placeholder")}
aria-label={t("search.textarea.ariaLabel")}
value={input}

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,4 +1,4 @@
import { Button } from "@/components/ui/button";
import { Button } from "@headlessui/react";
import dayjs from "dayjs";
import {
CircleCheck,
@@ -8,7 +8,6 @@ import {
Loader,
Trash2,
User,
SquareArrowOutUpRight,
} from "lucide-react";
import { FC, useState } from "react";
import { useTranslation } from "react-i18next";
@@ -16,24 +15,15 @@ import { useTranslation } from "react-i18next";
import { useSearchStore } from "@/stores/searchStore";
import DeleteDialog from "../Common/DeleteDialog";
import PreviewImage from "../Common/PreviewImage";
import platformAdapter from "@/utils/platformAdapter";
interface ExtensionDetailProps {
onInstall: () => void;
onUninstall: () => void;
changeInput: (value: string) => void;
}
const ExtensionDetail: FC<ExtensionDetailProps> = (props) => {
const { onInstall, onUninstall, changeInput } = props;
const {
selectedExtension,
installingExtensions,
setVisibleExtensionStore,
setVisibleExtensionDetail,
setSourceData,
setSearchValue,
} = useSearchStore();
const { onInstall, onUninstall } = props;
const { selectedExtension, installingExtensions } = useSearchStore();
const [isOpen, setIsOpen] = useState(false);
const { t } = useTranslation();
@@ -47,53 +37,6 @@ const ExtensionDetail: FC<ExtensionDetailProps> = (props) => {
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 = () => {
return <div className="my-4 h-px bg-[#E6E6E6] dark:bg-[#262626]"></div>;
};
@@ -118,7 +61,7 @@ const ExtensionDetail: FC<ExtensionDetailProps> = (props) => {
</div>
<div className="flex items-center gap-1">
<FolderDown className="size-4" />
<span>{selectedExtension.stats?.installs ?? 0}</span>
<span>{selectedExtension.stats.installs}</span>
</div>
</div>
</div>
@@ -126,19 +69,13 @@ const ExtensionDetail: FC<ExtensionDetailProps> = (props) => {
<div className="pt-2">
{selectedExtension.installed ? (
<div className="flex items-center gap-2">
<Button
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={() => handleOpen(selectedExtension)}
>
<SquareArrowOutUpRight className="size-4" />
</Button>
<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]">
<Trash2
className="size-4 text-red-500 cursor-pointer"
onClick={() => {
setIsOpen(true);
}}
/>
<div className="flex items-center gap-1 h-6 px-2 rounded-full text-[#22C461] bg-[#22C461]/20">
<CircleCheck className="size-4" />
<span>{t("extensionDetail.hints.installed")}</span>
</div>
@@ -233,9 +170,8 @@ const ExtensionDetail: FC<ExtensionDetailProps> = (props) => {
<DeleteDialog
reverseButtonPosition
isOpen={isOpen}
title={`${t("extensionDetail.deleteDialog.title")} ${
selectedExtension.name
}`}
title={`${t("extensionDetail.deleteDialog.title")} ${selectedExtension.name
}`}
description={t("extensionDetail.deleteDialog.description")}
cancelButtonProps={{
className:
@@ -243,7 +179,7 @@ const ExtensionDetail: FC<ExtensionDetailProps> = (props) => {
}}
deleteButtonProps={{
className:
"text-white bg-[#FF4949] hover:bg-[#FF4949] border-[#E6E6E6] dark:border-white/10",
"!text-[#FF4949] bg-[#F8F9FA] dark:text-white dark:bg-[#202126] border-[#E6E6E6] dark:border-white/10",
}}
setIsOpen={setIsOpen}
onCancel={handleCancel}

View File

@@ -73,13 +73,7 @@ export interface SearchExtensionItem {
}>;
}
const ExtensionStore = ({
extensionId,
changeInput,
}: {
extensionId?: string;
changeInput: (value: string) => void;
}) => {
const ExtensionStore = ({ extensionId }: { extensionId?: string }) => {
const {
searchValue,
selectedExtension,
@@ -301,7 +295,6 @@ const ExtensionStore = ({
<ExtensionDetail
onInstall={handleInstall}
onUninstall={handleUnInstall}
changeInput={changeInput}
/>
) : (
<>
@@ -348,7 +341,7 @@ const ExtensionStore = ({
<div className="flex items-center gap-1 text-[#999]">
<FolderDown className="size-4" />
<span>{stats?.installs ?? 0}</span>
<span>{stats.installs}</span>
</div>
</div>
</div>

View File

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

View File

@@ -14,7 +14,7 @@ import { useShortcutsStore } from "@/stores/shortcutsStore";
import { useAppStore } from "@/stores/appStore";
import { useSearchStore } from "@/stores/searchStore";
import { useExtensionsStore } from "@/stores/extensionsStore";
import { parseSearchQuery, SearchQuery, canNavigateBack } from "@/utils";
import { parseSearchQuery, SearchQuery } from "@/utils";
import InputUpload from "./InputUpload";
import Copyright from "../Common/Copyright";
@@ -187,9 +187,9 @@ const InputControls = ({
{source?.type === "deep_think" && source?.config?.visible && (
<button
className={clsx(
"flex items-center justify-center gap-1 h-5 px-1 rounded-md transition hover:bg-[#EDEDED] dark:hover:bg-[#202126] cursor-pointer",
"flex items-center justify-center gap-1 h-[20px] px-1 rounded-[6px] transition hover:bg-[#EDEDED] dark:hover:bg-[#202126]",
{
"bg-[rgba(0,114,255,0.3)]!": isDeepThinkActive,
"!bg-[rgba(0,114,255,0.3)]": isDeepThinkActive,
}
)}
onClick={setIsDeepThinkActive}
@@ -250,7 +250,7 @@ const InputControls = ({
!visibleExtensionStore && (
<div
className={clsx(
"inline-flex items-center gap-1 h-5 px-1 rounded-full hover:text-[#881c94]! cursor-pointer transition",
"inline-flex items-center gap-1 h-[20px] px-1 rounded-full hover:!text-[#881c94] cursor-pointer transition",
[
enabledAiOverview
? "text-[#881c94]"
@@ -283,7 +283,7 @@ const InputControls = ({
</div>
)}
{isChatPage || hasModules?.length !== 2 || canNavigateBack() ? null : (
{isChatPage || hasModules?.length !== 2 ? null : (
<div className="relative w-16 flex justify-end items-center">
<div className="absolute right-[52px] -top-2 z-10">
<VisibleKey

View File

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

View File

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

View File

@@ -1,5 +1,4 @@
import { useEffect, memo, useRef, useCallback, useState } from "react";
import clsx from "clsx";
import DropdownList from "./DropdownList";
import { SearchResults } from "@/components/Search/SearchResults";
@@ -13,6 +12,7 @@ import ExtensionStore from "./ExtensionStore";
import platformAdapter from "@/utils/platformAdapter";
import ViewExtension from "./ViewExtension";
import { visibleFooterBar } from "@/utils";
import clsx from "clsx";
const SearchResultsPanel = memo<{
input: string;
@@ -124,9 +124,7 @@ const SearchResultsPanel = memo<{
// If state gets updated, render the UI
if (visibleExtensionStore) {
return (
<ExtensionStore extensionId={extensionId} changeInput={changeInput} />
);
return <ExtensionStore extensionId={extensionId} />;
}
// Render the view extension
@@ -135,14 +133,11 @@ const SearchResultsPanel = memo<{
}
if (goAskAi) return <AskAi isChatMode={isChatMode} />;
if (sourceData) {
return <SearchResults input={input} isChatMode={isChatMode} />;
}
if (suggests.length === 0) return <NoResults />;
return (
return sourceData ? (
<SearchResults input={input} isChatMode={isChatMode} />
) : (
<DropdownList
suggests={suggests}
searchData={searchData}

View File

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

View File

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

View File

@@ -1,52 +1,19 @@
import React from "react";
import { useState, useEffect, useMemo, useRef, useCallback } from "react";
import { useTranslation } from "react-i18next";
import { Maximize2, Minimize2, Focus } from "lucide-react";
import { useState, useEffect, useMemo } from "react";
import { useSearchStore } from "@/stores/searchStore";
import {
ExtensionFileSystemPermission,
FileSystemAccess,
ViewExtensionUISettingsOrNull,
} from "../Settings/Extensions";
import platformAdapter from "@/utils/platformAdapter";
import { useShortcutsStore } from "@/stores/shortcutsStore";
import { isMac } from "@/utils/platform";
import { useAppStore } from "@/stores/appStore";
const ViewExtension: React.FC = () => {
const { viewExtensionOpened } = useSearchStore();
const isTauri = useAppStore((state) => state.isTauri);
// Complete list of the backend APIs, grouped by their category.
const [apis, setApis] = useState<Map<string, string[]> | null>(null);
const { setModifierKeyPressed } = useShortcutsStore();
const { t } = useTranslation();
const [isFullscreen, setIsFullscreen] = useState(false);
const prevWindowRef = useRef<{
width: number;
height: number;
resizable: boolean;
x: number;
y: number;
} | null>(null);
const fullscreenPrevRef = useRef<{
width: number;
height: number;
resizable: boolean;
x: number;
y: number;
} | null>(null);
const iframeRef = useRef<HTMLIFrameElement | null>(null);
const [scale, setScale] = useState(1);
const [fallbackViewSize, setFallbackViewSize] = useState<{
width: number;
height: number;
} | null>(() => {
if (typeof window === "undefined") return null;
return { width: window.innerWidth, height: window.innerHeight };
});
if (viewExtensionOpened == null) {
// When this view gets loaded, this state should not be NULL.
@@ -189,6 +156,7 @@ const ViewExtension: React.FC = () => {
}
};
window.addEventListener("message", messageHandler);
console.info("Coco extension API listener is up");
return () => {
window.removeEventListener("message", messageHandler);
@@ -196,261 +164,15 @@ const ViewExtension: React.FC = () => {
}, [reversedApis, permission]); // Add apiPermissions as dependency
const fileUrl = viewExtensionOpened[2];
const ui: ViewExtensionUISettingsOrNull = useMemo(() => {
return viewExtensionOpened[4] as ViewExtensionUISettingsOrNull;
}, [viewExtensionOpened]);
const resizable = ui?.resizable;
const uiWidth = ui && typeof ui.width === "number" ? ui.width : null;
const uiHeight = ui && typeof ui.height === "number" ? ui.height : null;
const hasExplicitWindowSize = uiWidth != null && uiHeight != null;
const baseWidth = useMemo(() => {
if (uiWidth != null) return uiWidth;
if (fallbackViewSize != null) return fallbackViewSize.width;
return 0;
}, [uiWidth, fallbackViewSize]);
const baseHeight = useMemo(() => {
if (uiHeight != null) return uiHeight;
if (fallbackViewSize != null) return fallbackViewSize.height;
return 0;
}, [uiHeight, fallbackViewSize]);
const recomputeScale = useCallback(async () => {
if (!hasExplicitWindowSize) {
setScale(1);
return;
}
const size = await platformAdapter.getWindowSize();
const nextScale = Math.min(
size.width / baseWidth,
size.height / baseHeight
);
setScale(Math.max(nextScale, 0.1));
}, [hasExplicitWindowSize, baseWidth, baseHeight]);
const applyFullscreen = useCallback(
async (next: boolean) => {
if (next) {
const size = await platformAdapter.getWindowSize();
const resizable = await platformAdapter.isWindowResizable();
const pos = await platformAdapter.getWindowPosition();
fullscreenPrevRef.current = {
width: size.width,
height: size.height,
resizable,
x: pos.x,
y: pos.y,
};
if (isMac && isTauri) {
const monitor = await platformAdapter.getMonitorFromCursor();
if (!monitor) return;
const window = await platformAdapter.getCurrentWebviewWindow();
const factor = await window.scaleFactor();
const { size, position } = monitor;
const { width, height } = size.toLogical(factor);
const { x, y } = position.toLogical(factor);
await platformAdapter.setWindowSize(width, height);
await platformAdapter.setWindowPosition(x, y);
await platformAdapter.setWindowResizable(true);
await recomputeScale();
} else {
await platformAdapter.setWindowFullscreen(true);
await recomputeScale();
}
} else {
if (!isMac) {
await platformAdapter.setWindowFullscreen(false);
}
if (fullscreenPrevRef.current) {
const prev = fullscreenPrevRef.current;
await platformAdapter.setWindowSize(prev.width, prev.height);
await platformAdapter.setWindowResizable(prev.resizable);
await platformAdapter.setWindowPosition(prev.x, prev.y);
fullscreenPrevRef.current = null;
await recomputeScale();
} else if (hasExplicitWindowSize) {
const nextResizable =
ui && typeof ui.resizable === "boolean" ? ui.resizable : true;
await platformAdapter.setWindowSize(uiWidth, uiHeight);
await platformAdapter.setWindowResizable(nextResizable);
await platformAdapter.centerOnCurrentMonitor();
await recomputeScale();
} else {
await recomputeScale();
}
setTimeout(() => {
iframeRef.current?.focus();
try {
iframeRef.current?.contentWindow?.focus();
} catch {}
}, 0);
}
},
[ui, recomputeScale]
);
useEffect(() => {
const applyWindowSettings = async () => {
if (viewExtensionOpened != null) {
const size = await platformAdapter.getWindowSize();
const resizable = await platformAdapter.isWindowResizable();
const pos = await platformAdapter.getWindowPosition();
setFallbackViewSize({ width: size.width, height: size.height });
prevWindowRef.current = {
width: size.width,
height: size.height,
resizable,
x: pos.x,
y: pos.y,
};
if (hasExplicitWindowSize) {
const nextResizable =
ui && typeof ui.resizable === "boolean" ? ui.resizable : true;
await platformAdapter.setWindowSize(uiWidth, uiHeight);
await platformAdapter.setWindowResizable(nextResizable);
await platformAdapter.centerOnCurrentMonitor();
await recomputeScale();
} else {
await recomputeScale();
}
setTimeout(() => {
iframeRef.current?.focus();
try {
iframeRef.current?.contentWindow?.focus();
} catch {}
}, 0);
} else {
if (prevWindowRef.current) {
const prev = prevWindowRef.current;
await platformAdapter.setWindowSize(prev.width, prev.height);
await platformAdapter.setWindowResizable(prev.resizable);
await platformAdapter.centerOnCurrentMonitor();
prevWindowRef.current = null;
await recomputeScale();
setTimeout(() => {
iframeRef.current?.focus();
}, 0);
}
}
};
applyWindowSettings();
return () => {
if (prevWindowRef.current) {
const prev = prevWindowRef.current;
platformAdapter.setWindowSize(prev.width, prev.height);
platformAdapter.setWindowResizable(prev.resizable);
platformAdapter.centerOnCurrentMonitor();
prevWindowRef.current = null;
}
};
}, [
viewExtensionOpened,
ui,
hasExplicitWindowSize,
uiWidth,
uiHeight,
recomputeScale,
]);
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === "Escape" && isFullscreen) {
applyFullscreen(false);
setIsFullscreen(false);
}
};
window.addEventListener("keydown", handleKeyDown, { capture: true });
return () => {
window.removeEventListener("keydown", handleKeyDown, {
capture: true,
} as any);
};
}, [isFullscreen, applyFullscreen]);
return (
<div className="relative w-full h-full">
{resizable && (
<button
aria-label={
isFullscreen
? t("viewExtension.fullscreen.exit")
: t("viewExtension.fullscreen.enter")
}
className="absolute top-2 right-2 z-10 rounded-md bg-black/40 text-white p-2 hover:bg-black/60 focus:outline-none"
onClick={async () => {
const next = !isFullscreen;
await applyFullscreen(next);
setIsFullscreen(next);
if (next) {
iframeRef.current?.focus();
try {
iframeRef.current?.contentWindow?.focus();
} catch {}
}
}}
>
{isFullscreen ? (
<Minimize2 className="size-4" />
) : (
<Maximize2 className="size-4" />
)}
</button>
)}
{/* Focus helper button */}
{resizable && (
<button
aria-label={t("viewExtension.focus")}
className="absolute top-2 right-12 z-10 rounded-md bg-black/40 text-white p-2 hover:bg-black/60 focus:outline-none"
onClick={() => {
iframeRef.current?.focus();
try {
iframeRef.current?.contentWindow?.focus();
} catch {}
}}
>
<Focus className="size-4" />
</button>
)}
<div
className="w-full h-full flex items-center justify-center"
onMouseDownCapture={() => {
iframeRef.current?.focus();
}}
onPointerDown={() => {
iframeRef.current?.focus();
}}
onClickCapture={() => {
iframeRef.current?.focus();
}}
>
<iframe
ref={iframeRef}
src={fileUrl}
className="border-0 w-full h-full"
style={{
transform: `scale(${scale})`,
transformOrigin: "center center",
outline: "none",
}}
allow="fullscreen; pointer-lock; gamepad"
allowFullScreen
tabIndex={-1}
onLoad={(event) => {
event.currentTarget.focus();
try {
iframeRef.current?.contentWindow?.focus();
} catch {}
}}
/>
</div>
</div>
<iframe
src={fileUrl}
className="w-full h-full border-0"
onLoad={(event) => {
event.currentTarget.focus();
}}
/>
);
};

View File

@@ -35,10 +35,7 @@ import {
visibleSearchBar,
} from "@/utils";
import { useTauriFocus } from "@/hooks/useTauriFocus";
import {
POPOVER_PANEL_SELECTOR,
WINDOW_CENTER_BASELINE_HEIGHT,
} from "@/constants";
import { POPOVER_PANEL_SELECTOR, WINDOW_CENTER_BASELINE_HEIGHT } from "@/constants";
import { useChatStore } from "@/stores/chatStore";
import { useSearchStore } from "@/stores/searchStore";
@@ -110,13 +107,9 @@ function SearchChat({
let collapseWindowTimer = useRef<ReturnType<typeof setTimeout>>();
const setWindowSize = useCallback(() => {
const { viewExtensionOpened } = useSearchStore.getState();
if (collapseWindowTimer.current) {
clearTimeout(collapseWindowTimer.current);
}
if (viewExtensionOpened != null) {
return;
}
const width = 680;
let height = WINDOW_CENTER_BASELINE_HEIGHT;
@@ -181,25 +174,6 @@ function SearchChat({
onFocus: debouncedSetWindowSize,
});
useEffect(() => {
const unlisten = platformAdapter.listenEvent("refresh-window-size", () => {
debouncedSetWindowSize();
});
return () => {
unlisten
.then((fn) => {
try {
typeof fn === "function" && fn();
} catch {
// ignore
}
})
.catch(() => {
// ignore
});
};
}, [debouncedSetWindowSize]);
useEffect(() => {
dispatch({
type: "SET_SEARCH_ACTIVE",
@@ -409,11 +383,11 @@ function SearchChat({
<div
data-tauri-drag-region={isTauri}
className={clsx(
"m-auto overflow-hidden relative bg-no-repeat flex flex-col bg-cover",
"m-auto overflow-hidden relative bg-no-repeat bg-white dark:bg-black flex flex-col",
[
isTransitioned
? "bg-bottom bg-[url('/assets/chat_bg_light.png')] dark:bg-[url('/assets/chat_bg_dark.png')]"
: "bg-top bg-[url('/assets/search_bg_light.png')] dark:bg-[url('/assets/search_bg_dark.png')]",
? "bg-bottom bg-chat_bg_light dark:bg-chat_bg_dark"
: "bg-top bg-search_bg_light dark:bg-search_bg_dark",
],
{
"size-full": !isTauri,
@@ -424,6 +398,7 @@ function SearchChat({
}
)}
style={{
backgroundSize: "auto 590px",
opacity: blurred ? blurOpacity / 100 : normalOpacity / 100,
}}
>
@@ -463,7 +438,7 @@ function SearchChat({
{!hideMiddleBorder && (
<div
className={clsx(
"pointer-events-none absolute left-0 right-0 h-px bg-[#E6E6E6] dark:bg-[#272626]",
"pointer-events-none absolute left-0 right-0 h-[1px] bg-[#E6E6E6] dark:bg-[#272626]",
isTransitioned ? "top-0" : "bottom-0"
)}
/>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,5 @@
import { useAppStore } from "@/stores/appStore";
import { Button } from "@/components/ui/button";
import { Button } from "@headlessui/react";
import { SquareArrowOutUpRight } from "lucide-react";
import { useTranslation } from "react-i18next";
@@ -12,7 +12,10 @@ const LoginButton = () => {
};
return (
<Button className="h-8" onClick={handleClick}>
<Button
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>
<SquareArrowOutUpRight className="size-4" />

View File

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

View File

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

View File

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

View File

@@ -1,46 +1,52 @@
import * as React from "react";
import { Slot } from "@radix-ui/react-slot";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-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",
"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",
{
variants: {
variant: {
default:
"bg-primary text-primary-foreground hover:bg-primary/90",
secondary:
"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",
"bg-primary text-primary-foreground shadow hover:bg-primary/90",
destructive:
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
"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:
"bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
sm: "h-8 px-3",
md: "h-9 px-4",
lg: "h-10 px-6",
default: "h-9 px-4 py-2",
sm: "h-8 rounded-[6px] px-3 text-xs",
lg: "h-10 rounded-[6px] px-8",
icon: "h-9 w-9",
},
},
defaultVariants: {
variant: "default",
size: "md",
size: "default",
},
}
);
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {}
VariantProps<typeof buttonVariants> {
asChild?: boolean;
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, ...props }, ref) => {
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button";
return (
<button
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
className={cn(buttonVariants({ variant, size }), className)}
{...props}
/>
);

View File

@@ -1,25 +0,0 @@
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

@@ -1,108 +0,0 @@
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

@@ -1,78 +0,0 @@
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

@@ -1,24 +0,0 @@
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

@@ -1,21 +0,0 @@
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

@@ -1,97 +0,0 @@
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>
);
};

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