mirror of
https://github.com/infinilabs/coco-app.git
synced 2025-12-18 12:37:45 +01:00
Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3a9c9ec9eb | ||
|
|
f7c0600480 | ||
|
|
ed8a1cb477 | ||
|
|
abf20f81ff | ||
|
|
65a48efdde | ||
|
|
0613238876 | ||
|
|
501f6df473 | ||
|
|
67c8c4bdfa |
4
.gitignore
vendored
4
.gitignore
vendored
@@ -13,6 +13,8 @@ dist-ssr
|
|||||||
*.local
|
*.local
|
||||||
out
|
out
|
||||||
src/components/web
|
src/components/web
|
||||||
|
SearchChatDemo/
|
||||||
|
web.md
|
||||||
|
|
||||||
# Editor directories and files
|
# Editor directories and files
|
||||||
# .vscode/*
|
# .vscode/*
|
||||||
@@ -26,3 +28,5 @@ src/components/web
|
|||||||
*.sln
|
*.sln
|
||||||
*.sw?
|
*.sw?
|
||||||
.env
|
.env
|
||||||
|
|
||||||
|
.trae
|
||||||
5
.vscode/settings.json
vendored
5
.vscode/settings.json
vendored
@@ -37,15 +37,18 @@
|
|||||||
"meval",
|
"meval",
|
||||||
"Minimizable",
|
"Minimizable",
|
||||||
"msvc",
|
"msvc",
|
||||||
|
"njsproj",
|
||||||
"nord",
|
"nord",
|
||||||
"nowrap",
|
"nowrap",
|
||||||
"nspanel",
|
"nspanel",
|
||||||
"nsstring",
|
"nsstring",
|
||||||
|
"ntvs",
|
||||||
"objc",
|
"objc",
|
||||||
"overscan",
|
"overscan",
|
||||||
"partialize",
|
"partialize",
|
||||||
"patchelf",
|
"patchelf",
|
||||||
"Quicklink",
|
"Quicklink",
|
||||||
|
"Quicklinks",
|
||||||
"Raycast",
|
"Raycast",
|
||||||
"rehype",
|
"rehype",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
@@ -54,6 +57,7 @@
|
|||||||
"rustup",
|
"rustup",
|
||||||
"screenshotable",
|
"screenshotable",
|
||||||
"serde",
|
"serde",
|
||||||
|
"Shadcn",
|
||||||
"swatinem",
|
"swatinem",
|
||||||
"tailwindcss",
|
"tailwindcss",
|
||||||
"tauri",
|
"tauri",
|
||||||
@@ -61,6 +65,7 @@
|
|||||||
"timedout",
|
"timedout",
|
||||||
"titlebar",
|
"titlebar",
|
||||||
"tpddns",
|
"tpddns",
|
||||||
|
"trae",
|
||||||
"traptitech",
|
"traptitech",
|
||||||
"unlisten",
|
"unlisten",
|
||||||
"unlistener",
|
"unlistener",
|
||||||
|
|||||||
@@ -8,11 +8,24 @@ title: "Release Notes"
|
|||||||
Information about release notes of Coco App is provided here.
|
Information about release notes of Coco App is provided here.
|
||||||
|
|
||||||
## Latest (In development)
|
## Latest (In development)
|
||||||
|
|
||||||
### ❌ Breaking changes
|
### ❌ Breaking changes
|
||||||
|
|
||||||
### 🚀 Features
|
### 🚀 Features
|
||||||
|
|
||||||
|
- feat: add open button to launch installed extension #1013
|
||||||
|
|
||||||
### 🐛 Bug fix
|
### 🐛 Bug fix
|
||||||
|
|
||||||
|
- fix: fix the abnormal input height issue #1006
|
||||||
|
- fix: implement custom serialization for Extension.minimum_coco_version #1010
|
||||||
|
|
||||||
### ✈️ Improvements
|
### ✈️ Improvements
|
||||||
|
|
||||||
|
- refactor: replace legacy components with shadcn/ui components #1002
|
||||||
|
- chore: show error msg (not err code) when installing exts via deeplink fails #1007
|
||||||
|
- refactor: treat Applications and File Search as normal extensions #1012
|
||||||
|
|
||||||
## 0.9.1 (2025-12-05)
|
## 0.9.1 (2025-12-05)
|
||||||
|
|
||||||
### ❌ Breaking changes
|
### ❌ Breaking changes
|
||||||
|
|||||||
21
package.json
21
package.json
@@ -8,9 +8,10 @@
|
|||||||
"build": "tsc && vite build",
|
"build": "tsc && vite build",
|
||||||
"build:web": "cross-env BUILD_TARGET=web tsc && cross-env BUILD_TARGET=web tsup --format esm",
|
"build:web": "cross-env BUILD_TARGET=web tsc && cross-env BUILD_TARGET=web tsup --format esm",
|
||||||
"publish:web": "cd out/search-chat && npm publish",
|
"publish:web": "cd out/search-chat && npm publish",
|
||||||
"publish:web:beta": "cd dist/search-chat && npm publish --tag beta",
|
"publish:web:beta": "cd out/search-chat && npm publish --tag beta",
|
||||||
"publish:web:alpha": "cd dist/search-chat && npm publish --tag alpha",
|
"publish:web:alpha": "cd out/search-chat && npm publish --tag alpha",
|
||||||
"publish:web:rc": "cd dist/search-chat && npm publish --tag rc",
|
"publish:web:rc": "cd out/search-chat && npm publish --tag rc",
|
||||||
|
"publish:web:otp": "cd out/search-chat && npm publish --access public --otp $NPM_OTP",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"tauri": "tauri",
|
"tauri": "tauri",
|
||||||
"release": "release-it",
|
"release": "release-it",
|
||||||
@@ -18,10 +19,18 @@
|
|||||||
"release-beta": "release-it --preRelease=beta --preReleaseBase=1"
|
"release-beta": "release-it --preRelease=beta --preReleaseBase=1"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@headlessui/react": "^2.2.2",
|
|
||||||
"@infinilabs/custom-icons": "0.0.4",
|
"@infinilabs/custom-icons": "0.0.4",
|
||||||
|
"@radix-ui/react-checkbox": "^1.1.5",
|
||||||
|
"@radix-ui/react-dialog": "^1.1.4",
|
||||||
|
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
||||||
|
"@radix-ui/react-label": "^2.1.1",
|
||||||
|
"@radix-ui/react-popover": "^1.1.6",
|
||||||
|
"@radix-ui/react-select": "^2.1.4",
|
||||||
"@radix-ui/react-separator": "^1.1.8",
|
"@radix-ui/react-separator": "^1.1.8",
|
||||||
|
"@radix-ui/react-slider": "^1.2.1",
|
||||||
"@radix-ui/react-slot": "^1.2.3",
|
"@radix-ui/react-slot": "^1.2.3",
|
||||||
|
"@radix-ui/react-switch": "^1.1.3",
|
||||||
|
"@radix-ui/react-tabs": "^1.1.13",
|
||||||
"@tauri-apps/api": "^2.5.0",
|
"@tauri-apps/api": "^2.5.0",
|
||||||
"@tauri-apps/plugin-autostart": "~2.2.0",
|
"@tauri-apps/plugin-autostart": "~2.2.0",
|
||||||
"@tauri-apps/plugin-clipboard-manager": "~2.3.2",
|
"@tauri-apps/plugin-clipboard-manager": "~2.3.2",
|
||||||
@@ -77,6 +86,8 @@
|
|||||||
"zustand": "^5.0.4"
|
"zustand": "^5.0.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@tailwindcss/postcss": "^4.1.17",
|
||||||
|
"@tailwindcss/vite": "^4.0.0",
|
||||||
"@tauri-apps/cli": "^2.5.0",
|
"@tauri-apps/cli": "^2.5.0",
|
||||||
"@types/dom-speech-recognition": "^0.0.4",
|
"@types/dom-speech-recognition": "^0.0.4",
|
||||||
"@types/lodash-es": "^4.17.12",
|
"@types/lodash-es": "^4.17.12",
|
||||||
@@ -93,7 +104,7 @@
|
|||||||
"postcss": "^8.5.3",
|
"postcss": "^8.5.3",
|
||||||
"release-it": "^18.1.2",
|
"release-it": "^18.1.2",
|
||||||
"sass": "^1.87.0",
|
"sass": "^1.87.0",
|
||||||
"tailwindcss": "^3.4.17",
|
"tailwindcss": "^4.0.0",
|
||||||
"tsup": "^8.4.0",
|
"tsup": "^8.4.0",
|
||||||
"tsx": "^4.19.4",
|
"tsx": "^4.19.4",
|
||||||
"typescript": "^5.8.3",
|
"typescript": "^5.8.3",
|
||||||
|
|||||||
1550
pnpm-lock.yaml
generated
1550
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,7 @@
|
|||||||
export default {
|
export default {
|
||||||
plugins: {
|
plugins: {
|
||||||
tailwindcss: {},
|
// Tailwind v4 PostCSS plugin has moved to @tailwindcss/postcss
|
||||||
|
'@tailwindcss/postcss': {},
|
||||||
autoprefixer: {},
|
autoprefixer: {},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
39
scripts/buildWebAfter.ts
Normal file
39
scripts/buildWebAfter.ts
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import { readFileSync, writeFileSync } from "fs";
|
||||||
|
import { join, dirname } from "path";
|
||||||
|
import { fileURLToPath } from "url";
|
||||||
|
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = dirname(__filename);
|
||||||
|
|
||||||
|
const extractCssVars = () => {
|
||||||
|
const filePath = join(__dirname, "../out/search-chat/index.css");
|
||||||
|
|
||||||
|
const cssContent = readFileSync(filePath, "utf-8");
|
||||||
|
|
||||||
|
const vars: Record<string, string> = {};
|
||||||
|
|
||||||
|
const propertyBlockRegex = /@property\s+(--[\w-]+)\s*\{([\s\S]*?)\}/g;
|
||||||
|
|
||||||
|
let match: RegExpExecArray | null;
|
||||||
|
|
||||||
|
while ((match = propertyBlockRegex.exec(cssContent))) {
|
||||||
|
const [, varName, body] = match;
|
||||||
|
|
||||||
|
const initialValueMatch = /initial-value\s*:\s*([^;]+);/.exec(body);
|
||||||
|
|
||||||
|
if (initialValueMatch) {
|
||||||
|
vars[varName] = initialValueMatch[1].trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const cssVarsBlock =
|
||||||
|
`.coco-container {\n` +
|
||||||
|
Object.entries(vars)
|
||||||
|
.map(([k, v]) => ` ${k}: ${v};`)
|
||||||
|
.join("\n") +
|
||||||
|
`\n}\n`;
|
||||||
|
|
||||||
|
writeFileSync(filePath, `${cssContent}\n${cssVarsBlock}`, "utf-8");
|
||||||
|
};
|
||||||
|
|
||||||
|
extractCssVars();
|
||||||
@@ -135,7 +135,9 @@ pub struct Extension {
|
|||||||
/// It is only for third-party extensions. Built-in extensions should always
|
/// It is only for third-party extensions. Built-in extensions should always
|
||||||
/// set this field to `None`.
|
/// set this field to `None`.
|
||||||
#[serde(deserialize_with = "deserialize_coco_semver")]
|
#[serde(deserialize_with = "deserialize_coco_semver")]
|
||||||
#[serde(default)] // None if this field is missing
|
#[serde(serialize_with = "serialize_coco_semver")]
|
||||||
|
// None if this field is missing, required as we use custom deserilize method.
|
||||||
|
#[serde(default)]
|
||||||
minimum_coco_version: Option<SemVer>,
|
minimum_coco_version: Option<SemVer>,
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -216,20 +218,19 @@ impl<'ext> PartialEq<ExtensionBundleId> for ExtensionBundleIdBorrowed<'ext> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Extension {
|
#[tauri::command]
|
||||||
/// Whether this extension could be searched.
|
pub(crate) fn extension_on_opened(extension: Extension) -> Option<OnOpened> {
|
||||||
pub(crate) fn searchable(&self) -> bool {
|
_extension_on_opened(&extension)
|
||||||
self.on_opened().is_some()
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/// Return what will happen when we open this extension.
|
/// Return what will happen when we open this extension.
|
||||||
///
|
///
|
||||||
/// `None` if it cannot be opened.
|
/// `None` if it cannot be opened.
|
||||||
pub(crate) fn on_opened(&self) -> Option<OnOpened> {
|
pub(crate) fn _extension_on_opened(extension: &Extension) -> Option<OnOpened> {
|
||||||
let settings = self.settings.clone();
|
let settings = extension.settings.clone();
|
||||||
let permission = self.permission.clone();
|
let permission = extension.permission.clone();
|
||||||
|
|
||||||
match self.r#type {
|
match extension.r#type {
|
||||||
// This function, at the time of writing this comment, is primarily
|
// This function, at the time of writing this comment, is primarily
|
||||||
// used by third-party extensions.
|
// used by third-party extensions.
|
||||||
//
|
//
|
||||||
@@ -258,9 +259,9 @@ impl Extension {
|
|||||||
|
|
||||||
ExtensionType::Command => {
|
ExtensionType::Command => {
|
||||||
let ty = ExtensionOnOpenedType::Command {
|
let ty = ExtensionOnOpenedType::Command {
|
||||||
action: self.action.clone().unwrap_or_else(|| {
|
action: extension.action.clone().unwrap_or_else(|| {
|
||||||
panic!(
|
panic!(
|
||||||
"Command extension [{}]'s [action] field is not set, something wrong with your extension validity check", self.id
|
"Command extension [{}]'s [action] field is not set, something wrong with your extension validity check", extension.id
|
||||||
)
|
)
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
@@ -274,9 +275,9 @@ impl Extension {
|
|||||||
Some(OnOpened::Extension(extension_on_opened))
|
Some(OnOpened::Extension(extension_on_opened))
|
||||||
}
|
}
|
||||||
ExtensionType::Quicklink => {
|
ExtensionType::Quicklink => {
|
||||||
let quicklink = self.quicklink.clone().unwrap_or_else(|| {
|
let quicklink = extension.quicklink.clone().unwrap_or_else(|| {
|
||||||
panic!(
|
panic!(
|
||||||
"Quicklink extension [{}]'s [quicklink] field is not set, something wrong with your extension validity check", self.id
|
"Quicklink extension [{}]'s [quicklink] field is not set, something wrong with your extension validity check", extension.id
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -296,12 +297,12 @@ impl Extension {
|
|||||||
ExtensionType::Script => todo!("not supported yet"),
|
ExtensionType::Script => todo!("not supported yet"),
|
||||||
ExtensionType::Setting => todo!("not supported yet"),
|
ExtensionType::Setting => todo!("not supported yet"),
|
||||||
ExtensionType::View => {
|
ExtensionType::View => {
|
||||||
let name = self.name.clone();
|
let name = extension.name.clone();
|
||||||
let icon = self.icon.clone();
|
let icon = extension.icon.clone();
|
||||||
let page = self.page.as_ref().unwrap_or_else(|| {
|
let page = extension.page.as_ref().unwrap_or_else(|| {
|
||||||
panic!("View extension [{}]'s [page] field is not set, something wrong with your extension validity check", self.id);
|
panic!("View extension [{}]'s [page] field is not set, something wrong with your extension validity check", extension.id);
|
||||||
}).clone();
|
}).clone();
|
||||||
let ui = self.ui.clone();
|
let ui = extension.ui.clone();
|
||||||
|
|
||||||
let extension_on_opened_type = ExtensionOnOpenedType::View {
|
let extension_on_opened_type = ExtensionOnOpenedType::View {
|
||||||
name,
|
name,
|
||||||
@@ -322,6 +323,12 @@ impl Extension {
|
|||||||
unreachable!("Extensions of type [Unknown] should never be opened")
|
unreachable!("Extensions of type [Unknown] should never be opened")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Extension {
|
||||||
|
/// Whether this extension could be searched.
|
||||||
|
pub(crate) fn searchable(&self) -> bool {
|
||||||
|
_extension_on_opened(self).is_some()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn get_sub_extension(&self, sub_extension_id: &str) -> Option<&Self> {
|
pub(crate) fn get_sub_extension(&self, sub_extension_id: &str) -> Option<&Self> {
|
||||||
@@ -419,6 +426,37 @@ where
|
|||||||
Ok(Some(semver))
|
Ok(Some(semver))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Serialize Coco SemVer to a string.
|
||||||
|
///
|
||||||
|
/// For a `SemVer`, there are 2 possible input cases, guarded by `to_semver()`:
|
||||||
|
///
|
||||||
|
/// 1. "x.y.z" => "x.y.z"
|
||||||
|
/// 2. "x.y.z-SNAPSHOT.2560" => "x.y.z-SNAPSHOT-2560"
|
||||||
|
fn serialize_coco_semver<S>(version: &Option<SemVer>, serializer: S) -> Result<S::Ok, S::Error>
|
||||||
|
where
|
||||||
|
S: serde::Serializer,
|
||||||
|
{
|
||||||
|
match version {
|
||||||
|
Some(v) => {
|
||||||
|
assert!(v.build.is_empty());
|
||||||
|
|
||||||
|
let s = if v.pre.is_empty() {
|
||||||
|
format!("{}.{}.{}", v.major, v.minor, v.patch)
|
||||||
|
} else {
|
||||||
|
format!(
|
||||||
|
"{}.{}.{}-{}",
|
||||||
|
v.major,
|
||||||
|
v.minor,
|
||||||
|
v.patch,
|
||||||
|
v.pre.as_str().replace('.', "-")
|
||||||
|
)
|
||||||
|
};
|
||||||
|
serializer.serialize_str(&s)
|
||||||
|
}
|
||||||
|
None => serializer.serialize_none(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
|
#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
|
||||||
pub(crate) struct CommandAction {
|
pub(crate) struct CommandAction {
|
||||||
pub(crate) exec: String,
|
pub(crate) exec: String,
|
||||||
@@ -673,7 +711,30 @@ fn filter_out_extensions(
|
|||||||
|
|
||||||
extensions.retain(|ext| {
|
extensions.retain(|ext| {
|
||||||
let ty = ext.r#type;
|
let ty = ext.r#type;
|
||||||
ty == ExtensionType::Group || ty == ExtensionType::Extension || ty == extension_type
|
|
||||||
|
if ty.contains_sub_items() {
|
||||||
|
/*
|
||||||
|
* We should not filter out group/extension extensions, with 2
|
||||||
|
* exceptions: "Applications" and "File Search". They contains
|
||||||
|
* no sub-extensions, so we treat them as normal extensions.
|
||||||
|
*
|
||||||
|
* When `extenison_type` is "Application", we return the "Applications"
|
||||||
|
* extension as well because it is the entry to access the application
|
||||||
|
* list.
|
||||||
|
*/
|
||||||
|
if ext.developer.is_none()
|
||||||
|
&& ext.id == built_in::application::QUERYSOURCE_ID_DATASOURCE_ID_DATASOURCE_NAME
|
||||||
|
{
|
||||||
|
ty == extension_type || extension_type == ExtensionType::Application
|
||||||
|
} else if ext.developer.is_none() && ext.id == built_in::file_search::EXTENSION_ID {
|
||||||
|
ty == extension_type
|
||||||
|
} else {
|
||||||
|
// We should not filter out group/extension extensions
|
||||||
|
true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ty == extension_type
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Filter sub-extensions to only include the requested type
|
// Filter sub-extensions to only include the requested type
|
||||||
@@ -693,19 +754,6 @@ fn filter_out_extensions(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Application is special, technically, it should never be filtered out by
|
|
||||||
// this condition. But if our users will be surprising if they choose a
|
|
||||||
// non-Application type and see it in the results. So we do this to remedy the
|
|
||||||
// issue
|
|
||||||
if let Some(idx) = extensions.iter().position(|ext| {
|
|
||||||
ext.developer.is_none()
|
|
||||||
&& ext.id == built_in::application::QUERYSOURCE_ID_DATASOURCE_ID_DATASOURCE_NAME
|
|
||||||
}) {
|
|
||||||
if extension_type != ExtensionType::Application {
|
|
||||||
extensions.remove(idx);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// apply query filter
|
// apply query filter
|
||||||
@@ -721,8 +769,23 @@ fn filter_out_extensions(
|
|||||||
|
|
||||||
extensions.retain(|ext| {
|
extensions.retain(|ext| {
|
||||||
if ext.r#type.contains_sub_items() {
|
if ext.r#type.contains_sub_items() {
|
||||||
|
/*
|
||||||
|
* We should keep all the group/extension extensions. But we
|
||||||
|
* have 2 exceptions: "Applications" and "File Search". Even
|
||||||
|
* though they are of type group/extension, they do not contain
|
||||||
|
* sub-extensions, so they are more like commands, apply the
|
||||||
|
* `match_closure` here
|
||||||
|
*/
|
||||||
|
if ext.developer.is_none()
|
||||||
|
&& (ext.id
|
||||||
|
== built_in::application::QUERYSOURCE_ID_DATASOURCE_ID_DATASOURCE_NAME
|
||||||
|
|| ext.id == built_in::file_search::EXTENSION_ID)
|
||||||
|
{
|
||||||
|
match_closure(ext)
|
||||||
|
} else {
|
||||||
// Keep all group/extension types
|
// Keep all group/extension types
|
||||||
true
|
true
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// Apply filter to non-group/extension types
|
// Apply filter to non-group/extension types
|
||||||
match_closure(ext)
|
match_closure(ext)
|
||||||
@@ -779,7 +842,8 @@ pub(crate) async fn list_extensions(
|
|||||||
|
|
||||||
// Cleanup after filtering extensions, don't do it if filter is not performed.
|
// Cleanup after filtering extensions, don't do it if filter is not performed.
|
||||||
//
|
//
|
||||||
// Remove parent extensions (Group/Extension types) that have no sub-items after filtering
|
// Remove parent extensions (Group/Extension types) that have no sub-items
|
||||||
|
// after filtering
|
||||||
let filter_performed = query.is_some() || extension_type.is_some() || list_enabled;
|
let filter_performed = query.is_some() || extension_type.is_some() || list_enabled;
|
||||||
if filter_performed {
|
if filter_performed {
|
||||||
extensions.retain(|ext| {
|
extensions.retain(|ext| {
|
||||||
@@ -787,12 +851,21 @@ pub(crate) async fn list_extensions(
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// We don't do this filter to applications since it is always empty, load at runtime.
|
/*
|
||||||
if ext.developer.is_none()
|
* Two exceptions: "Applications" and "File Search"
|
||||||
&& ext.id == built_in::application::QUERYSOURCE_ID_DATASOURCE_ID_DATASOURCE_NAME
|
*
|
||||||
|
* They are of type group/extension, but they contain no sub
|
||||||
|
* extensions, which means technically, we should filter them
|
||||||
|
* out. However, we sould not do this because they are not real
|
||||||
|
* group/extension extensions.
|
||||||
|
*/
|
||||||
|
if ext.developer.is_none() {
|
||||||
|
if ext.id == built_in::application::QUERYSOURCE_ID_DATASOURCE_ID_DATASOURCE_NAME
|
||||||
|
|| ext.id == built_in::file_search::EXTENSION_ID
|
||||||
{
|
{
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let has_commands = ext
|
let has_commands = ext
|
||||||
.commands
|
.commands
|
||||||
@@ -2150,4 +2223,31 @@ mod tests {
|
|||||||
let deserialized: FileSystemAccess = serde_json::from_str(&serialized).unwrap();
|
let deserialized: FileSystemAccess = serde_json::from_str(&serialized).unwrap();
|
||||||
assert_eq!(original, deserialized);
|
assert_eq!(original, deserialized);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_serialize_coco_semver_none() {
|
||||||
|
let version: Option<SemVer> = None;
|
||||||
|
let mut serializer = serde_json::Serializer::new(Vec::new());
|
||||||
|
serialize_coco_semver(&version, &mut serializer).unwrap();
|
||||||
|
let serialized = String::from_utf8(serializer.into_inner()).unwrap();
|
||||||
|
assert_eq!(serialized, "null");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_serialize_coco_semver_simple() {
|
||||||
|
let version: Option<SemVer> = Some(SemVer::parse("1.2.3").unwrap());
|
||||||
|
let mut serializer = serde_json::Serializer::new(Vec::new());
|
||||||
|
serialize_coco_semver(&version, &mut serializer).unwrap();
|
||||||
|
let serialized = String::from_utf8(serializer.into_inner()).unwrap();
|
||||||
|
assert_eq!(serialized, "\"1.2.3\"");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_serialize_coco_semver_with_pre() {
|
||||||
|
let version: Option<SemVer> = Some(SemVer::parse("1.2.3-SNAPSHOT.1234").unwrap());
|
||||||
|
let mut serializer = serde_json::Serializer::new(Vec::new());
|
||||||
|
serialize_coco_semver(&version, &mut serializer).unwrap();
|
||||||
|
let serialized = String::from_utf8(serializer.into_inner()).unwrap();
|
||||||
|
assert_eq!(serialized, "\"1.2.3-SNAPSHOT-1234\"");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
169
src-tauri/src/extension/third_party/mod.rs
vendored
169
src-tauri/src/extension/third_party/mod.rs
vendored
@@ -16,6 +16,8 @@ use crate::common::search::QueryResponse;
|
|||||||
use crate::common::search::QuerySource;
|
use crate::common::search::QuerySource;
|
||||||
use crate::common::search::SearchQuery;
|
use crate::common::search::SearchQuery;
|
||||||
use crate::common::traits::SearchSource;
|
use crate::common::traits::SearchSource;
|
||||||
|
use crate::extension::_extension_on_opened;
|
||||||
|
use crate::extension::ExtensionBundleId;
|
||||||
use crate::extension::ExtensionBundleIdBorrowed;
|
use crate::extension::ExtensionBundleIdBorrowed;
|
||||||
use crate::extension::ExtensionType;
|
use crate::extension::ExtensionType;
|
||||||
use crate::extension::PLUGIN_JSON_FIELD_MINIMUM_COCO_VERSION;
|
use crate::extension::PLUGIN_JSON_FIELD_MINIMUM_COCO_VERSION;
|
||||||
@@ -26,11 +28,13 @@ use crate::util::platform::Platform;
|
|||||||
use crate::util::version::COCO_VERSION;
|
use crate::util::version::COCO_VERSION;
|
||||||
use crate::util::version::parse_coco_semver;
|
use crate::util::version::parse_coco_semver;
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
|
use borrowme::Borrow;
|
||||||
use borrowme::ToOwned;
|
use borrowme::ToOwned;
|
||||||
use check::general_check;
|
use check::general_check;
|
||||||
use function_name::named;
|
use function_name::named;
|
||||||
use semver::Version as SemVer;
|
use semver::Version as SemVer;
|
||||||
use serde_json::Value as Json;
|
use serde_json::Value as Json;
|
||||||
|
use snafu::prelude::*;
|
||||||
use std::io::ErrorKind;
|
use std::io::ErrorKind;
|
||||||
use std::ops::Deref;
|
use std::ops::Deref;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
@@ -44,6 +48,7 @@ use tauri_plugin_global_shortcut::GlobalShortcutExt;
|
|||||||
use tauri_plugin_global_shortcut::ShortcutState;
|
use tauri_plugin_global_shortcut::ShortcutState;
|
||||||
use tokio::fs::read_dir;
|
use tokio::fs::read_dir;
|
||||||
use tokio::sync::RwLock;
|
use tokio::sync::RwLock;
|
||||||
|
use tokio::sync::RwLockReadGuard;
|
||||||
use tokio::sync::RwLockWriteGuard;
|
use tokio::sync::RwLockWriteGuard;
|
||||||
|
|
||||||
pub(crate) fn get_third_party_extension_directory(tauri_app_handle: &AppHandle) -> PathBuf {
|
pub(crate) fn get_third_party_extension_directory(tauri_app_handle: &AppHandle) -> PathBuf {
|
||||||
@@ -393,6 +398,26 @@ impl ThirdPartyExtensionsSearchSource {
|
|||||||
extension.get_sub_extension_mut(sub_extension_id)
|
extension.get_sub_extension_mut(sub_extension_id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Return an immutable reference to the extension specified by `bundle_id` if it exists.
|
||||||
|
fn get_extension<'lock, 'extensions>(
|
||||||
|
extensions_read_lock: &'lock RwLockReadGuard<'extensions, Vec<Extension>>,
|
||||||
|
bundle_id: &ExtensionBundleIdBorrowed<'_>,
|
||||||
|
) -> Option<&'lock Extension> {
|
||||||
|
let index = extensions_read_lock.iter().position(|ext| {
|
||||||
|
ext.id == bundle_id.extension_id && ext.developer.as_deref() == bundle_id.developer
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let extension = extensions_read_lock
|
||||||
|
.get(index)
|
||||||
|
.expect("just checked this extension exists");
|
||||||
|
|
||||||
|
let Some(sub_extension_id) = bundle_id.sub_extension_id else {
|
||||||
|
return Some(extension);
|
||||||
|
};
|
||||||
|
|
||||||
|
extension.get_sub_extension(sub_extension_id)
|
||||||
|
}
|
||||||
|
|
||||||
/// Difference between this function and `enable_extension()`
|
/// Difference between this function and `enable_extension()`
|
||||||
///
|
///
|
||||||
/// This function does the actual job, i.e., to enable/activate the extension.
|
/// This function does the actual job, i.e., to enable/activate the extension.
|
||||||
@@ -407,7 +432,7 @@ impl ThirdPartyExtensionsSearchSource {
|
|||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
if extension.supports_alias_hotkey() {
|
if extension.supports_alias_hotkey() {
|
||||||
if let Some(ref hotkey) = extension.hotkey {
|
if let Some(ref hotkey) = extension.hotkey {
|
||||||
let on_opened = extension.on_opened().unwrap_or_else(|| panic!( "extension has hotkey, but on_open() returns None, extension ID [{}], extension type [{:?}]", extension.id, extension.r#type));
|
let on_opened = _extension_on_opened(extension).unwrap_or_else(|| panic!( "extension has hotkey, but on_open() returns None, extension ID [{}], extension type [{:?}]", extension.id, extension.r#type));
|
||||||
let extension_id_clone = extension.id.clone();
|
let extension_id_clone = extension.id.clone();
|
||||||
|
|
||||||
tauri_app_handle
|
tauri_app_handle
|
||||||
@@ -681,7 +706,7 @@ impl ThirdPartyExtensionsSearchSource {
|
|||||||
)?;
|
)?;
|
||||||
|
|
||||||
// Set hotkey
|
// Set hotkey
|
||||||
let on_opened = extension.on_opened().unwrap_or_else(|| panic!(
|
let on_opened = _extension_on_opened(extension).unwrap_or_else(|| panic!(
|
||||||
"setting hotkey for an extension that cannot be opened, extension ID [{:?}], extension type [{:?}]", bundle_id, extension.r#type,
|
"setting hotkey for an extension that cannot be opened, extension ID [{:?}], extension type [{:?}]", bundle_id, extension.r#type,
|
||||||
));
|
));
|
||||||
|
|
||||||
@@ -867,6 +892,59 @@ impl ThirdPartyExtensionsSearchSource {
|
|||||||
pub(crate) async fn extensions_snapshot(&self) -> Vec<Extension> {
|
pub(crate) async fn extensions_snapshot(&self) -> Vec<Extension> {
|
||||||
self.inner.extensions.read().await.clone()
|
self.inner.extensions.read().await.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Open the specified third-party extension.
|
||||||
|
async fn open(
|
||||||
|
&self,
|
||||||
|
tauri_app_handle: AppHandle,
|
||||||
|
bundle_id: &ExtensionBundleIdBorrowed<'_>,
|
||||||
|
) -> Result<(), OpenThirdPartyExtensionError> {
|
||||||
|
let extensions_read_lock = self.inner.extensions.read().await;
|
||||||
|
let Some(ext) = Self::get_extension(&extensions_read_lock, bundle_id) else {
|
||||||
|
log::warn!(
|
||||||
|
"trying to open() a third-party extension [{:?}] that does not exist",
|
||||||
|
bundle_id
|
||||||
|
);
|
||||||
|
return Err(OpenThirdPartyExtensionError::ExtensionNotFound {
|
||||||
|
bundle_id: bundle_id.to_owned(),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
let Some(on_opened) = _extension_on_opened(ext) else {
|
||||||
|
log::warn!("third-party extension [{:?}] cannot be opened", bundle_id);
|
||||||
|
return Err(OpenThirdPartyExtensionError::ExtensionCannotBeOpened {
|
||||||
|
bundle_id: bundle_id.to_owned(),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
crate::common::document::open(tauri_app_handle, on_opened, None)
|
||||||
|
.await
|
||||||
|
.map_err(|err_msg| OpenThirdPartyExtensionError::OnOpenedOpenError { msg: err_msg })?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Snafu, serde::Serialize)]
|
||||||
|
pub(crate) enum OpenThirdPartyExtensionError {
|
||||||
|
#[snafu(display("extension '{:?}' does not exist", bundle_id))]
|
||||||
|
ExtensionNotFound { bundle_id: ExtensionBundleId },
|
||||||
|
#[snafu(display("extension '{:?}' cannot be opened", bundle_id))]
|
||||||
|
ExtensionCannotBeOpened { bundle_id: ExtensionBundleId },
|
||||||
|
#[snafu(display("executing open(OnOpened) failed: '{}'", msg))]
|
||||||
|
OnOpenedOpenError { msg: String },
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub(crate) async fn open_third_party_extension(
|
||||||
|
tauri_app_handle: AppHandle,
|
||||||
|
bundle_id: ExtensionBundleId,
|
||||||
|
) -> Result<(), OpenThirdPartyExtensionError> {
|
||||||
|
THIRD_PARTY_EXTENSIONS_SEARCH_SOURCE
|
||||||
|
.get()
|
||||||
|
.unwrap_or_else(|| panic!("THIRD_PARTY_EXTENSIONS_SEARCH_SOURCE is not set"))
|
||||||
|
.open(tauri_app_handle, &bundle_id.borrow())
|
||||||
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) static THIRD_PARTY_EXTENSIONS_SEARCH_SOURCE: OnceLock<ThirdPartyExtensionsSearchSource> =
|
pub(crate) static THIRD_PARTY_EXTENSIONS_SEARCH_SOURCE: OnceLock<ThirdPartyExtensionsSearchSource> =
|
||||||
@@ -890,25 +968,37 @@ impl SearchSource for ThirdPartyExtensionsSearchSource {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// query main_extension_id querysource
|
||||||
|
// main_extension_id querysource
|
||||||
|
// query querysource datasource
|
||||||
async fn search(
|
async fn search(
|
||||||
&self,
|
&self,
|
||||||
_tauri_app_handle: AppHandle,
|
_tauri_app_handle: AppHandle,
|
||||||
query: SearchQuery,
|
query: SearchQuery,
|
||||||
) -> Result<QueryResponse, SearchError> {
|
) -> Result<QueryResponse, SearchError> {
|
||||||
let Some(query_string) = query.query_strings.get("query") else {
|
let opt_lowercase_query_string: Option<String> = {
|
||||||
return Ok(QueryResponse {
|
match query.query_strings.get("query") {
|
||||||
source: self.get_type(),
|
Some(query_string) => {
|
||||||
hits: Vec::new(),
|
if query_string.is_empty() {
|
||||||
total_hits: 0,
|
None
|
||||||
});
|
} else {
|
||||||
|
Some(query_string.to_lowercase())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => None,
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let opt_data_source = query
|
let opt_data_source = query
|
||||||
.query_strings
|
.query_strings
|
||||||
.get("datasource")
|
.get("datasource")
|
||||||
.map(|owned_str| owned_str.to_string());
|
.map(|str| str.to_string());
|
||||||
|
|
||||||
|
let opt_main_extension_id = query
|
||||||
|
.query_strings
|
||||||
|
.get("main_extension_id")
|
||||||
|
.map(|str| str.to_string());
|
||||||
|
|
||||||
let query_lower = query_string.to_lowercase();
|
|
||||||
let inner_clone = Arc::clone(&self.inner);
|
let inner_clone = Arc::clone(&self.inner);
|
||||||
|
|
||||||
let closure = move || {
|
let closure = move || {
|
||||||
@@ -916,10 +1006,22 @@ impl SearchSource for ThirdPartyExtensionsSearchSource {
|
|||||||
let extensions_read_lock =
|
let extensions_read_lock =
|
||||||
futures::executor::block_on(async { inner_clone.extensions.read().await });
|
futures::executor::block_on(async { inner_clone.extensions.read().await });
|
||||||
|
|
||||||
|
let main_extension_filter_closure = |ext: &&Extension| -> bool {
|
||||||
|
// field minimum_coco_extension is only set for main
|
||||||
|
// extensions, so we only check main extensions.
|
||||||
|
let condition1 = ext.enabled && is_extension_compatible(Extension::clone(ext));
|
||||||
|
let condition2 = if let Some(ref main_extension_id) = opt_main_extension_id {
|
||||||
|
&ext.id == main_extension_id
|
||||||
|
} else {
|
||||||
|
true
|
||||||
|
};
|
||||||
|
|
||||||
|
condition1 && condition2
|
||||||
|
};
|
||||||
|
|
||||||
for extension in extensions_read_lock
|
for extension in extensions_read_lock
|
||||||
.iter()
|
.iter()
|
||||||
// field minimum_coco_extension is only set for main extensions.
|
.filter(main_extension_filter_closure)
|
||||||
.filter(|ext| ext.enabled && is_extension_compatible(Extension::clone(ext)))
|
|
||||||
{
|
{
|
||||||
if extension.r#type.contains_sub_items() {
|
if extension.r#type.contains_sub_items() {
|
||||||
let opt_main_extension_lowercase_name =
|
let opt_main_extension_lowercase_name =
|
||||||
@@ -934,7 +1036,7 @@ impl SearchSource for ThirdPartyExtensionsSearchSource {
|
|||||||
for command in commands.iter().filter(|cmd| cmd.enabled) {
|
for command in commands.iter().filter(|cmd| cmd.enabled) {
|
||||||
if let Some(hit) = extension_to_hit(
|
if let Some(hit) = extension_to_hit(
|
||||||
command,
|
command,
|
||||||
&query_lower,
|
opt_lowercase_query_string.as_deref(),
|
||||||
opt_data_source.as_deref(),
|
opt_data_source.as_deref(),
|
||||||
opt_main_extension_lowercase_name.as_deref(),
|
opt_main_extension_lowercase_name.as_deref(),
|
||||||
) {
|
) {
|
||||||
@@ -947,7 +1049,7 @@ impl SearchSource for ThirdPartyExtensionsSearchSource {
|
|||||||
for script in scripts.iter().filter(|script| script.enabled) {
|
for script in scripts.iter().filter(|script| script.enabled) {
|
||||||
if let Some(hit) = extension_to_hit(
|
if let Some(hit) = extension_to_hit(
|
||||||
script,
|
script,
|
||||||
&query_lower,
|
opt_lowercase_query_string.as_deref(),
|
||||||
opt_data_source.as_deref(),
|
opt_data_source.as_deref(),
|
||||||
opt_main_extension_lowercase_name.as_deref(),
|
opt_main_extension_lowercase_name.as_deref(),
|
||||||
) {
|
) {
|
||||||
@@ -960,7 +1062,7 @@ impl SearchSource for ThirdPartyExtensionsSearchSource {
|
|||||||
for quicklink in quicklinks.iter().filter(|link| link.enabled) {
|
for quicklink in quicklinks.iter().filter(|link| link.enabled) {
|
||||||
if let Some(hit) = extension_to_hit(
|
if let Some(hit) = extension_to_hit(
|
||||||
quicklink,
|
quicklink,
|
||||||
&query_lower,
|
opt_lowercase_query_string.as_deref(),
|
||||||
opt_data_source.as_deref(),
|
opt_data_source.as_deref(),
|
||||||
opt_main_extension_lowercase_name.as_deref(),
|
opt_main_extension_lowercase_name.as_deref(),
|
||||||
) {
|
) {
|
||||||
@@ -973,7 +1075,7 @@ impl SearchSource for ThirdPartyExtensionsSearchSource {
|
|||||||
for view in views.iter().filter(|view| view.enabled) {
|
for view in views.iter().filter(|view| view.enabled) {
|
||||||
if let Some(hit) = extension_to_hit(
|
if let Some(hit) = extension_to_hit(
|
||||||
view,
|
view,
|
||||||
&query_lower,
|
opt_lowercase_query_string.as_deref(),
|
||||||
opt_data_source.as_deref(),
|
opt_data_source.as_deref(),
|
||||||
opt_main_extension_lowercase_name.as_deref(),
|
opt_main_extension_lowercase_name.as_deref(),
|
||||||
) {
|
) {
|
||||||
@@ -982,9 +1084,12 @@ impl SearchSource for ThirdPartyExtensionsSearchSource {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if let Some(hit) =
|
if let Some(hit) = extension_to_hit(
|
||||||
extension_to_hit(extension, &query_lower, opt_data_source.as_deref(), None)
|
extension,
|
||||||
{
|
opt_lowercase_query_string.as_deref(),
|
||||||
|
opt_data_source.as_deref(),
|
||||||
|
None,
|
||||||
|
) {
|
||||||
hits.push(hit);
|
hits.push(hit);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1029,9 +1134,9 @@ pub(crate) async fn uninstall_extension(
|
|||||||
/// This argument is needed as an "extension" type extension should return all its
|
/// This argument is needed as an "extension" type extension should return all its
|
||||||
/// sub-extensions when the query string matches its name. To do this, we pass the
|
/// sub-extensions when the query string matches its name. To do this, we pass the
|
||||||
/// extension name, score it and take that into account.
|
/// extension name, score it and take that into account.
|
||||||
pub(crate) fn extension_to_hit(
|
fn extension_to_hit(
|
||||||
extension: &Extension,
|
extension: &Extension,
|
||||||
query_lower: &str,
|
opt_lowercase_query_string: Option<&str>,
|
||||||
opt_data_source: Option<&str>,
|
opt_data_source: Option<&str>,
|
||||||
opt_main_extension_lowercase_name: Option<&str>,
|
opt_main_extension_lowercase_name: Option<&str>,
|
||||||
) -> Option<(Document, f64)> {
|
) -> Option<(Document, f64)> {
|
||||||
@@ -1050,11 +1155,11 @@ pub(crate) fn extension_to_hit(
|
|||||||
}
|
}
|
||||||
|
|
||||||
let mut total_score = 0.0;
|
let mut total_score = 0.0;
|
||||||
|
if let Some(query_lower) = opt_lowercase_query_string {
|
||||||
// Score based on title match
|
// Score based on title match
|
||||||
// Title is considered more important, so it gets a higher weight.
|
// Title is considered more important, so it gets a higher weight.
|
||||||
if let Some(title_score) =
|
if let Some(title_score) =
|
||||||
calculate_text_similarity(&query_lower, &extension.name.to_lowercase())
|
calculate_text_similarity(query_lower, &extension.name.to_lowercase())
|
||||||
{
|
{
|
||||||
total_score += title_score;
|
total_score += title_score;
|
||||||
}
|
}
|
||||||
@@ -1062,7 +1167,8 @@ pub(crate) fn extension_to_hit(
|
|||||||
// Score based on alias match if available
|
// Score based on alias match if available
|
||||||
// Alias is considered less important than title, so it gets a lower weight.
|
// Alias is considered less important than title, so it gets a lower weight.
|
||||||
if let Some(alias) = &extension.alias {
|
if let Some(alias) = &extension.alias {
|
||||||
if let Some(alias_score) = calculate_text_similarity(&query_lower, &alias.to_lowercase()) {
|
if let Some(alias_score) = calculate_text_similarity(query_lower, &alias.to_lowercase())
|
||||||
|
{
|
||||||
total_score += alias_score;
|
total_score += alias_score;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1073,15 +1179,19 @@ pub(crate) fn extension_to_hit(
|
|||||||
// into account.
|
// into account.
|
||||||
if let Some(main_extension_lowercase_id) = opt_main_extension_lowercase_name {
|
if let Some(main_extension_lowercase_id) = opt_main_extension_lowercase_name {
|
||||||
if let Some(main_extension_score) =
|
if let Some(main_extension_score) =
|
||||||
calculate_text_similarity(&query_lower, main_extension_lowercase_id)
|
calculate_text_similarity(query_lower, main_extension_lowercase_id)
|
||||||
{
|
{
|
||||||
total_score += main_extension_score;
|
total_score += main_extension_score;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only include if there's some relevance (score is meaningfully positive)
|
// Only filter by score if query string is set
|
||||||
if total_score > 0.01 {
|
if total_score == 0.0 {
|
||||||
let on_opened = extension.on_opened().unwrap_or_else(|| {
|
return None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let on_opened = _extension_on_opened(extension).unwrap_or_else(|| {
|
||||||
panic!(
|
panic!(
|
||||||
"extension (id [{}], type [{:?}]) is searchable, and should have a valid on_opened",
|
"extension (id [{}], type [{:?}]) is searchable, and should have a valid on_opened",
|
||||||
extension.id, extension.r#type
|
extension.id, extension.r#type
|
||||||
@@ -1107,7 +1217,4 @@ pub(crate) fn extension_to_hit(
|
|||||||
};
|
};
|
||||||
|
|
||||||
Some((document, total_score))
|
Some((document, total_score))
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -177,6 +177,7 @@ pub fn run() {
|
|||||||
extension::enable_extension,
|
extension::enable_extension,
|
||||||
extension::disable_extension,
|
extension::disable_extension,
|
||||||
extension::set_extension_alias,
|
extension::set_extension_alias,
|
||||||
|
extension::extension_on_opened,
|
||||||
extension::register_extension_hotkey,
|
extension::register_extension_hotkey,
|
||||||
extension::unregister_extension_hotkey,
|
extension::unregister_extension_hotkey,
|
||||||
extension::is_extension_enabled,
|
extension::is_extension_enabled,
|
||||||
@@ -185,6 +186,7 @@ pub fn run() {
|
|||||||
extension::third_party::install::store::install_extension_from_store,
|
extension::third_party::install::store::install_extension_from_store,
|
||||||
extension::third_party::install::local_extension::install_local_extension,
|
extension::third_party::install::local_extension::install_local_extension,
|
||||||
extension::third_party::uninstall_extension,
|
extension::third_party::uninstall_extension,
|
||||||
|
extension::third_party::open_third_party_extension,
|
||||||
extension::is_extension_compatible,
|
extension::is_extension_compatible,
|
||||||
extension::api::apis,
|
extension::api::apis,
|
||||||
extension::api::fs::read_dir,
|
extension::api::fs::read_dir,
|
||||||
|
|||||||
@@ -17,6 +17,20 @@ use std::collections::HashMap;
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use tauri::{AppHandle, Manager};
|
use tauri::{AppHandle, Manager};
|
||||||
use tokio::time::{Duration, timeout};
|
use tokio::time::{Duration, timeout};
|
||||||
|
|
||||||
|
/// Available `query_strings`:
|
||||||
|
///
|
||||||
|
/// * "querysource": the query/search source to search
|
||||||
|
/// * "datasource": the data source to search. If this is provided, then
|
||||||
|
/// "querysource" has to be specified as well.
|
||||||
|
/// * "main_extension_id": Currently, only the "extensions" query source
|
||||||
|
/// supports this. If you set
|
||||||
|
///
|
||||||
|
/// ```text
|
||||||
|
/// {"querysource": "extension", "main_extension_id"}
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// then only the extension with this ID will be returned, if exists.
|
||||||
#[named]
|
#[named]
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn query_coco_fusion(
|
pub async fn query_coco_fusion(
|
||||||
@@ -26,6 +40,10 @@ pub async fn query_coco_fusion(
|
|||||||
query_strings: HashMap<String, String>,
|
query_strings: HashMap<String, String>,
|
||||||
query_timeout: u64,
|
query_timeout: u64,
|
||||||
) -> Result<MultiSourceQueryResponse, SearchError> {
|
) -> Result<MultiSourceQueryResponse, SearchError> {
|
||||||
|
if query_strings.contains_key("datasource") && !query_strings.contains_key("querysource") {
|
||||||
|
panic!("[querysource] has to be provided if [datasource] is set")
|
||||||
|
}
|
||||||
|
|
||||||
let opt_query_source_id = query_strings.get("querysource");
|
let opt_query_source_id = query_strings.get("querysource");
|
||||||
let search_sources = tauri_app_handle.state::<SearchSourceRegistry>();
|
let search_sources = tauri_app_handle.state::<SearchSourceRegistry>();
|
||||||
let query_source_list = search_sources.get_sources().await;
|
let query_source_list = search_sources.get_sources().await;
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import {
|
|||||||
handleNetworkError,
|
handleNetworkError,
|
||||||
} from "./tools";
|
} from "./tools";
|
||||||
|
|
||||||
type Fn = (data: FcResponse<any>) => unknown;
|
type Fn = (data: FcResponse<unknown>) => unknown;
|
||||||
|
|
||||||
interface IAnyObj {
|
interface IAnyObj {
|
||||||
[index: string]: unknown;
|
[index: string]: unknown;
|
||||||
@@ -85,8 +85,26 @@ export const Get = <T>(
|
|||||||
new Promise((resolve) => {
|
new Promise((resolve) => {
|
||||||
const appStore = JSON.parse(localStorage.getItem("app-store") || "{}");
|
const appStore = JSON.parse(localStorage.getItem("app-store") || "{}");
|
||||||
|
|
||||||
let baseURL = appStore.state?.endpoint_http;
|
// In Vite dev, prefer using the proxy by keeping requests relative
|
||||||
if (!baseURL || baseURL === "undefined") {
|
const isDev = (import.meta as any).env?.DEV === true;
|
||||||
|
const PROXY_PREFIXES: readonly string[] = [
|
||||||
|
"account",
|
||||||
|
"chat",
|
||||||
|
"query",
|
||||||
|
"connector",
|
||||||
|
"integration",
|
||||||
|
"assistant",
|
||||||
|
"datasource",
|
||||||
|
"settings",
|
||||||
|
"mcp_server",
|
||||||
|
];
|
||||||
|
const shouldProxy =
|
||||||
|
isDev &&
|
||||||
|
url.startsWith("/") &&
|
||||||
|
PROXY_PREFIXES.some((p) => url.startsWith(`/${p}`));
|
||||||
|
|
||||||
|
let baseURL: string = appStore.state?.endpoint_http as string;
|
||||||
|
if (!baseURL || baseURL === "undefined" || shouldProxy) {
|
||||||
baseURL = "";
|
baseURL = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -117,8 +135,25 @@ export const Post = <T>(
|
|||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
const appStore = JSON.parse(localStorage.getItem("app-store") || "{}");
|
const appStore = JSON.parse(localStorage.getItem("app-store") || "{}");
|
||||||
|
|
||||||
let baseURL = appStore.state?.endpoint_http;
|
const isDev = (import.meta as any).env?.DEV === true;
|
||||||
if (!baseURL || baseURL === "undefined") {
|
const PROXY_PREFIXES: readonly string[] = [
|
||||||
|
"account",
|
||||||
|
"chat",
|
||||||
|
"query",
|
||||||
|
"connector",
|
||||||
|
"integration",
|
||||||
|
"assistant",
|
||||||
|
"datasource",
|
||||||
|
"settings",
|
||||||
|
"mcp_server",
|
||||||
|
];
|
||||||
|
const shouldProxy =
|
||||||
|
isDev &&
|
||||||
|
url.startsWith("/") &&
|
||||||
|
PROXY_PREFIXES.some((p) => url.startsWith(`/${p}`));
|
||||||
|
|
||||||
|
let baseURL: string = appStore.state?.endpoint_http as string;
|
||||||
|
if (!baseURL || baseURL === "undefined" || shouldProxy) {
|
||||||
baseURL = "";
|
baseURL = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -81,15 +81,25 @@ async function invokeWithErrorHandler<T>(
|
|||||||
|
|
||||||
return result;
|
return result;
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
const errorMessage = error || "Command execution failed";
|
console.log(error);
|
||||||
|
const errorMessage =
|
||||||
|
error instanceof Error
|
||||||
|
? error.message
|
||||||
|
: typeof error === "object"
|
||||||
|
? JSON.stringify(error)
|
||||||
|
: String(error || "Command execution failed");
|
||||||
// 401 Unauthorized
|
// 401 Unauthorized
|
||||||
if (errorMessage.includes("Unauthorized")) {
|
if (errorMessage.includes("Unauthorized")) {
|
||||||
handleLogout();
|
handleLogout();
|
||||||
} else {
|
} else {
|
||||||
addError(command + ":" + errorMessage, "error");
|
addError(command + ":" + errorMessage, "error");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (error instanceof Error) {
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
throw new Error(errorMessage);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function list_coco_servers(): Promise<Server[]> {
|
export function list_coco_servers(): Promise<Server[]> {
|
||||||
|
|||||||
@@ -41,15 +41,20 @@ const AssistantItem = memo(
|
|||||||
)}
|
)}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
>
|
>
|
||||||
<div className="flex items-center justify-center size-6 bg-white border border-[#E6E6E6] rounded-full overflow-hidden">
|
|
||||||
{_source?.icon?.startsWith("font_") ? (
|
{_source?.icon?.startsWith("font_") ? (
|
||||||
<FontIcon name={_source?.icon} className="size-4" />
|
<FontIcon
|
||||||
|
name={_source?.icon}
|
||||||
|
className="w-4 h-4 rounded-full dark:drop-shadow-[0_0_6px_rgb(255,255,255)]"
|
||||||
|
/>
|
||||||
) : (
|
) : (
|
||||||
<img src={logoImg} className="size-4" alt={name} />
|
<img
|
||||||
|
src={logoImg}
|
||||||
|
className="w-4 h-4 rounded-full dark:drop-shadow-[0_0_6px_rgb(255,255,255)]"
|
||||||
|
alt={name}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
|
||||||
<div className="text-left flex-1 min-w-0">
|
<div className="text-left flex-1 min-w-0">
|
||||||
<div className="font-medium text-gray-900 dark:text-white truncate">
|
<div className="text-sm font-medium text-gray-900 dark:text-white truncate">
|
||||||
{_source?.name || "-"}
|
{_source?.name || "-"}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xs text-gray-500 dark:text-gray-400 truncate">
|
<div className="text-xs text-gray-500 dark:text-gray-400 truncate">
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
import { useState, useRef, useCallback, useEffect } from "react";
|
import { useState, useRef, useCallback, useEffect } from "react";
|
||||||
import { ChevronDownIcon, RefreshCw } from "lucide-react";
|
import { ChevronDownIcon, RefreshCw } from "lucide-react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { isNil } from "lodash-es";
|
import {
|
||||||
import { Popover, PopoverButton, PopoverPanel } from "@headlessui/react";
|
Popover,
|
||||||
|
PopoverTrigger,
|
||||||
|
PopoverContent,
|
||||||
|
} from "@/components/ui/popover";
|
||||||
import { useDebounce, useKeyPress, usePagination } from "ahooks";
|
import { useDebounce, useKeyPress, usePagination } from "ahooks";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
|
|
||||||
@@ -17,6 +20,7 @@ import { AssistantFetcher } from "./AssistantFetcher";
|
|||||||
import AssistantItem from "./AssistantItem";
|
import AssistantItem from "./AssistantItem";
|
||||||
import Pagination from "@/components/Common/Pagination";
|
import Pagination from "@/components/Common/Pagination";
|
||||||
import { useSearchStore } from "@/stores/searchStore";
|
import { useSearchStore } from "@/stores/searchStore";
|
||||||
|
import { Button } from "../ui/button";
|
||||||
|
|
||||||
interface AssistantListProps {
|
interface AssistantListProps {
|
||||||
assistantIDs?: string[];
|
assistantIDs?: string[];
|
||||||
@@ -83,6 +87,7 @@ export function AssistantList({ assistantIDs = [] }: AssistantListProps) {
|
|||||||
|
|
||||||
const [highlightIndex, setHighlightIndex] = useState<number>(-1);
|
const [highlightIndex, setHighlightIndex] = useState<number>(-1);
|
||||||
const [isKeyboardActive, setIsKeyboardActive] = useState(false);
|
const [isKeyboardActive, setIsKeyboardActive] = useState(false);
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const targetId = askAiAssistantId ?? targetAssistantId;
|
const targetId = askAiAssistantId ?? targetAssistantId;
|
||||||
@@ -105,7 +110,7 @@ export function AssistantList({ assistantIDs = [] }: AssistantListProps) {
|
|||||||
useKeyPress(
|
useKeyPress(
|
||||||
["uparrow", "downarrow", "enter"],
|
["uparrow", "downarrow", "enter"],
|
||||||
(event, key) => {
|
(event, key) => {
|
||||||
const isClose = isNil(popoverButtonRef.current?.dataset["open"]);
|
const isClose = !open;
|
||||||
|
|
||||||
if (isClose) return;
|
if (isClose) return;
|
||||||
|
|
||||||
@@ -161,26 +166,29 @@ export function AssistantList({ assistantIDs = [] }: AssistantListProps) {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative">
|
<div ref={popoverRef} className="relative">
|
||||||
<Popover ref={popoverRef}>
|
<Popover
|
||||||
<PopoverButton
|
open={open}
|
||||||
ref={popoverButtonRef}
|
onOpenChange={(v) => {
|
||||||
className="h-6 p-1 px-1.5 flex items-center gap-1 rounded-full bg-white dark:bg-[#202126] text-sm/6 font-semibold text-gray-800 dark:text-[#d8d8d8] border border-gray-200 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-700 focus:outline-none"
|
setOpen(v);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<PopoverTrigger
|
||||||
|
ref={popoverButtonRef}
|
||||||
|
className="h-6 p-1 px-1.5 flex items-center gap-1 rounded-full border border-input bg-background text-sm/6 font-semibold text-foreground hover:bg-accent hover:text-accent-foreground focus:outline-none"
|
||||||
>
|
>
|
||||||
<div className="w-4 h-4 flex justify-center items-center rounded-full bg-white border border-[#E6E6E6]">
|
|
||||||
{currentAssistant?._source?.icon?.startsWith("font_") ? (
|
{currentAssistant?._source?.icon?.startsWith("font_") ? (
|
||||||
<FontIcon
|
<FontIcon
|
||||||
name={currentAssistant._source.icon}
|
name={currentAssistant._source.icon}
|
||||||
className="w-3 h-3"
|
className="w-4 h-4"
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<img
|
<img
|
||||||
src={logoImg}
|
src={logoImg}
|
||||||
className="w-3 h-3"
|
className="w-4 h-4"
|
||||||
alt={t("assistant.message.logo")}
|
alt={t("assistant.message.logo")}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
|
||||||
<div className="max-w-[100px] truncate">
|
<div className="max-w-[100px] truncate">
|
||||||
{currentAssistant?._source?.name || "Coco AI"}
|
{currentAssistant?._source?.name || "Coco AI"}
|
||||||
</div>
|
</div>
|
||||||
@@ -190,12 +198,14 @@ export function AssistantList({ assistantIDs = [] }: AssistantListProps) {
|
|||||||
popoverButtonRef.current?.click();
|
popoverButtonRef.current?.click();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ChevronDownIcon className="size-4 text-gray-500 dark:text-gray-400 transition-transform" />
|
<ChevronDownIcon className="size-4 text-muted-foreground transition-transform" />
|
||||||
</VisibleKey>
|
</VisibleKey>
|
||||||
</PopoverButton>
|
</PopoverTrigger>
|
||||||
|
|
||||||
<PopoverPanel
|
<PopoverContent
|
||||||
className="absolute z-50 top-full mt-1 left-0 w-60 rounded-xl bg-white dark:bg-[#202126] p-3 text-sm/6 text-[#333] dark:text-[#D8D8D8] shadow-lg border dark:border-white/10 focus:outline-none max-h-[calc(100vh-150px)] overflow-y-auto"
|
side="bottom"
|
||||||
|
align="start"
|
||||||
|
className="z-50 w-60 rounded-xl p-3 shadow-lg focus:outline-none max-h-[calc(100vh-150px)] overflow-y-auto"
|
||||||
onMouseMove={handleMouseMove}
|
onMouseMove={handleMouseMove}
|
||||||
>
|
>
|
||||||
<div className="flex items-center justify-between text-sm font-bold">
|
<div className="flex items-center justify-between text-sm font-bold">
|
||||||
@@ -203,9 +213,11 @@ export function AssistantList({ assistantIDs = [] }: AssistantListProps) {
|
|||||||
{t("assistant.popover.title")}({pagination.total})
|
{t("assistant.popover.title")}({pagination.total})
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="icon"
|
||||||
onClick={handleRefresh}
|
onClick={handleRefresh}
|
||||||
className="flex items-center justify-center size-6 bg-white dark:bg-[#202126] rounded-lg border dark:border-white/10"
|
className="size-6"
|
||||||
disabled={isRefreshing}
|
disabled={isRefreshing}
|
||||||
>
|
>
|
||||||
<VisibleKey shortcut="R" onKeyPress={handleRefresh}>
|
<VisibleKey shortcut="R" onKeyPress={handleRefresh}>
|
||||||
@@ -218,7 +230,7 @@ export function AssistantList({ assistantIDs = [] }: AssistantListProps) {
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</VisibleKey>
|
</VisibleKey>
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<VisibleKey
|
<VisibleKey
|
||||||
@@ -234,8 +246,8 @@ export function AssistantList({ assistantIDs = [] }: AssistantListProps) {
|
|||||||
autoFocus
|
autoFocus
|
||||||
value={keyword}
|
value={keyword}
|
||||||
placeholder={t("assistant.popover.search")}
|
placeholder={t("assistant.popover.search")}
|
||||||
className="w-full h-8 px-2 bg-transparent border rounded-[6px] dark:border-white/10"
|
className="w-full h-8"
|
||||||
onChange={(event) => {
|
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
setKeyword(event.target.value);
|
setKeyword(event.target.value);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@@ -272,7 +284,7 @@ export function AssistantList({ assistantIDs = [] }: AssistantListProps) {
|
|||||||
<NoDataImage />
|
<NoDataImage />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</PopoverPanel>
|
</PopoverContent>
|
||||||
</Popover>
|
</Popover>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -388,7 +388,7 @@ const ChatAI = memo(
|
|||||||
<div
|
<div
|
||||||
data-tauri-drag-region
|
data-tauri-drag-region
|
||||||
data-chat-instance={instanceId}
|
data-chat-instance={instanceId}
|
||||||
className={`flex flex-col rounded-[6px] h-full overflow-hidden relative`}
|
className={`flex flex-col rounded-md h-full overflow-hidden relative`}
|
||||||
>
|
>
|
||||||
<ChatHeader
|
<ChatHeader
|
||||||
clearChat={clearChat}
|
clearChat={clearChat}
|
||||||
|
|||||||
@@ -95,13 +95,13 @@ export function ChatHeader({
|
|||||||
{isChatPage ? null : (
|
{isChatPage ? null : (
|
||||||
<button className="inline-flex" onClick={onOpenChatAI}>
|
<button className="inline-flex" onClick={onOpenChatAI}>
|
||||||
<VisibleKey shortcut={external} onKeyPress={onOpenChatAI}>
|
<VisibleKey shortcut={external} onKeyPress={onOpenChatAI}>
|
||||||
<WindowsFullIcon className="rotate-30 scale-x-[-1]" />
|
<WindowsFullIcon className="scale-x-[-1]" />
|
||||||
</VisibleKey>
|
</VisibleKey>
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<WebLogin panelClassName="top-8 right-0" />
|
<WebLogin side="bottom" align="end" />
|
||||||
)}
|
)}
|
||||||
</header>
|
</header>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ const ConnectPrompt = () => {
|
|||||||
<p className="mb-4 w-[388px]">{t("assistant.chat.connect_tip")}</p>
|
<p className="mb-4 w-[388px]">{t("assistant.chat.connect_tip")}</p>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
className="flex items-center gap-2 px-6 py-2 rounded-[6px] text-[#0072ff] transition-colors"
|
className="flex items-center gap-2 px-6 py-2 rounded-md text-[#0072ff] transition-colors"
|
||||||
onClick={handleConnect}
|
onClick={handleConnect}
|
||||||
>
|
>
|
||||||
<span>{t("assistant.chat.connect")}</span>
|
<span>{t("assistant.chat.connect")}</span>
|
||||||
|
|||||||
@@ -1,9 +1,13 @@
|
|||||||
import { useState, useCallback, useEffect, useRef } from "react";
|
import { useState, useCallback, useEffect, useRef } from "react";
|
||||||
import { Settings, RefreshCw, Check, Server } from "lucide-react";
|
import { Settings, RefreshCw, Check, Server } from "lucide-react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Popover, PopoverButton, PopoverPanel } from "@headlessui/react";
|
import {
|
||||||
|
Popover,
|
||||||
|
PopoverTrigger,
|
||||||
|
PopoverContent,
|
||||||
|
} from "@/components/ui/popover";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
import { useKeyPress } from "ahooks";
|
import { useKeyPress } from "ahooks";
|
||||||
import { isNil } from "lodash-es";
|
|
||||||
|
|
||||||
import logoImg from "@/assets/icon.svg";
|
import logoImg from "@/assets/icon.svg";
|
||||||
import ServerIcon from "@/icons/Server";
|
import ServerIcon from "@/icons/Server";
|
||||||
@@ -61,6 +65,7 @@ export function ServerList({ clearChat }: ServerListProps) {
|
|||||||
|
|
||||||
const popoverRef = useRef<HTMLDivElement>(null);
|
const popoverRef = useRef<HTMLDivElement>(null);
|
||||||
const serverListButtonRef = useRef<HTMLButtonElement>(null);
|
const serverListButtonRef = useRef<HTMLButtonElement>(null);
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
const { refreshServerList } = useServers();
|
const { refreshServerList } = useServers();
|
||||||
|
|
||||||
@@ -143,7 +148,7 @@ export function ServerList({ clearChat }: ServerListProps) {
|
|||||||
["uparrow", "downarrow", "enter"],
|
["uparrow", "downarrow", "enter"],
|
||||||
async (event, key) => {
|
async (event, key) => {
|
||||||
const service = await getCurrentWindowService();
|
const service = await getCurrentWindowService();
|
||||||
const isClose = isNil(serverListButtonRef.current?.dataset["open"]);
|
const isClose = !open;
|
||||||
const length = serverList.length;
|
const length = serverList.length;
|
||||||
|
|
||||||
if (isClose || length <= 1) return;
|
if (isClose || length <= 1) return;
|
||||||
@@ -182,8 +187,9 @@ export function ServerList({ clearChat }: ServerListProps) {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover ref={popoverRef} className="relative">
|
<div ref={popoverRef} className="relative">
|
||||||
<PopoverButton ref={serverListButtonRef} className="flex items-center">
|
<Popover open={open} onOpenChange={setOpen}>
|
||||||
|
<PopoverTrigger ref={serverListButtonRef} className="flex items-center">
|
||||||
<VisibleKey
|
<VisibleKey
|
||||||
shortcut={serviceListShortcut}
|
shortcut={serviceListShortcut}
|
||||||
onKeyPress={() => {
|
onKeyPress={() => {
|
||||||
@@ -192,39 +198,45 @@ export function ServerList({ clearChat }: ServerListProps) {
|
|||||||
>
|
>
|
||||||
<ServerIcon />
|
<ServerIcon />
|
||||||
</VisibleKey>
|
</VisibleKey>
|
||||||
</PopoverButton>
|
</PopoverTrigger>
|
||||||
|
|
||||||
<PopoverPanel
|
<PopoverContent
|
||||||
|
side="bottom"
|
||||||
|
align="end"
|
||||||
onMouseMove={handleMouseMove}
|
onMouseMove={handleMouseMove}
|
||||||
className="absolute right-0 z-10 mt-2 min-w-[240px] bg-white dark:bg-[#202126] rounded-lg shadow-lg border border-gray-200 dark:border-gray-700"
|
className="z-10 min-w-60 rounded-lg shadow-lg"
|
||||||
>
|
>
|
||||||
<div className="p-3">
|
<div className="p-3">
|
||||||
<div className="flex items-center justify-between mb-3 whitespace-nowrap">
|
<div className="flex items-center justify-between mb-3 whitespace-nowrap">
|
||||||
<h3 className="text-sm font-medium text-gray-900 dark:text-gray-100">
|
<h3 className="text-sm font-medium text-foreground">
|
||||||
{t("assistant.chat.servers")}
|
{t("assistant.chat.servers")}
|
||||||
</h3>
|
</h3>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<button
|
<Button
|
||||||
onClick={openSettings}
|
onClick={openSettings}
|
||||||
className="p-1 rounded-[6px] hover:bg-gray-100 dark:hover:bg-gray-800 text-gray-500 dark:text-gray-400"
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="rounded-md focus-visible:ring-0 focus-visible:ring-offset-0"
|
||||||
>
|
>
|
||||||
<VisibleKey shortcut=",">
|
<VisibleKey shortcut=",">
|
||||||
<Settings className="h-4 w-4 text-[#0287FF]" />
|
<Settings className="h-4 w-4 text-primary" />
|
||||||
</VisibleKey>
|
</VisibleKey>
|
||||||
</button>
|
</Button>
|
||||||
<button
|
<Button
|
||||||
onClick={handleRefresh}
|
onClick={handleRefresh}
|
||||||
className="p-1 rounded-[6px] hover:bg-gray-100 dark:hover:bg-gray-800 text-gray-500 dark:text-gray-400"
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="rounded-md focus-visible:ring-0 focus-visible:ring-offset-0"
|
||||||
disabled={isRefreshing}
|
disabled={isRefreshing}
|
||||||
>
|
>
|
||||||
<VisibleKey shortcut="R" onKeyPress={handleRefresh}>
|
<VisibleKey shortcut="R" onKeyPress={handleRefresh}>
|
||||||
<RefreshCw
|
<RefreshCw
|
||||||
className={`h-4 w-4 text-[#0287FF] transition-transform duration-1000 ${
|
className={`h-4 w-4 text-primary transition-transform duration-1000 ${
|
||||||
isRefreshing ? "animate-spin" : ""
|
isRefreshing ? "animate-spin" : ""
|
||||||
}`}
|
}`}
|
||||||
/>
|
/>
|
||||||
</VisibleKey>
|
</VisibleKey>
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
@@ -237,25 +249,25 @@ export function ServerList({ clearChat }: ServerListProps) {
|
|||||||
${
|
${
|
||||||
currentService?.id === server.id ||
|
currentService?.id === server.id ||
|
||||||
highlightId === server.id
|
highlightId === server.id
|
||||||
? "bg-gray-100 dark:bg-gray-800"
|
? "bg-muted"
|
||||||
: "hover:bg-gray-50 dark:hover:bg-gray-800/50"
|
: "hover:bg-muted"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2 min-w-0">
|
<div className="flex items-center gap-2 min-w-0">
|
||||||
<img
|
<img
|
||||||
src={server?.provider?.icon || logoImg}
|
src={server?.provider?.icon || logoImg}
|
||||||
alt={server.name}
|
alt={server.name}
|
||||||
className="w-6 h-6 rounded-full bg-gray-100 dark:bg-gray-800 dark:drop-shadow-[0_0_6px_rgb(255,255,255)]"
|
className="w-6 h-6 rounded-full dark:drop-shadow-[0_0_6px_rgb(255,255,255)]"
|
||||||
onError={(e) => {
|
onError={(e) => {
|
||||||
const target = e.target as HTMLImageElement;
|
const target = e.target as HTMLImageElement;
|
||||||
target.src = logoImg;
|
target.src = logoImg;
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<div className="text-left flex-1 min-w-0">
|
<div className="text-left flex-1 min-w-0">
|
||||||
<div className="text-sm font-medium text-gray-900 dark:text-gray-100 truncate max-w-[200px]">
|
<div className="text-sm font-medium text-foreground truncate max-w-[200px]">
|
||||||
{server.name}
|
{server.name}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xs text-gray-500 dark:text-gray-400 truncate max-w-[200px]">
|
<div className="text-xs text-muted-foreground truncate max-w-[200px]">
|
||||||
{t("assistant.chat.aiAssistant")}:{" "}
|
{t("assistant.chat.aiAssistant")}:{" "}
|
||||||
{server.stats?.assistant_count || 1}
|
{server.stats?.assistant_count || 1}
|
||||||
</div>
|
</div>
|
||||||
@@ -274,7 +286,7 @@ export function ServerList({ clearChat }: ServerListProps) {
|
|||||||
shortcut="↓↑"
|
shortcut="↓↑"
|
||||||
shortcutClassName="w-6 -translate-x-4"
|
shortcutClassName="w-6 -translate-x-4"
|
||||||
>
|
>
|
||||||
<Check className="w-full h-full text-gray-500 dark:text-gray-400" />
|
<Check className="w-full h-full text-muted-foreground" />
|
||||||
</VisibleKey>
|
</VisibleKey>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -283,8 +295,8 @@ export function ServerList({ clearChat }: ServerListProps) {
|
|||||||
))
|
))
|
||||||
) : (
|
) : (
|
||||||
<div className="flex flex-col items-center justify-center py-6 text-center">
|
<div className="flex flex-col items-center justify-center py-6 text-center">
|
||||||
<Server className="w-8 h-8 text-gray-400 dark:text-gray-600 mb-2" />
|
<Server className="w-8 h-8 text-muted-foreground mb-2" />
|
||||||
<p className="text-sm text-gray-500 dark:text-gray-400">
|
<p className="text-sm text-muted-foreground">
|
||||||
{t("assistant.chat.noServers")}
|
{t("assistant.chat.noServers")}
|
||||||
</p>
|
</p>
|
||||||
<button
|
<button
|
||||||
@@ -297,7 +309,8 @@ export function ServerList({ clearChat }: ServerListProps) {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</PopoverPanel>
|
</PopoverContent>
|
||||||
</Popover>
|
</Popover>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -122,7 +122,7 @@ const Splash = ({ assistantIDs = [], startPage }: SplashProps) => {
|
|||||||
return (
|
return (
|
||||||
<li key={id} className="mobile:w-full w-1/2 p-1">
|
<li key={id} className="mobile:w-full w-1/2 p-1">
|
||||||
<div
|
<div
|
||||||
className="group h-[74px] px-3 py-2 text-sm rounded-xl border dark:border-[#262626] bg-white dark:bg-black cursor-pointer transition hover:!border-[#0087FF]"
|
className="group h-[74px] px-3 py-2 text-sm rounded-xl border border-input bg-white dark:bg-black cursor-pointer transition hover:border-[#0087FF]!"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setCurrentAssistant(item);
|
setCurrentAssistant(item);
|
||||||
|
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ const AudioRecording: FC<AudioRecordingProps> = (props) => {
|
|||||||
const state = useReactive({ ...INITIAL_STATE });
|
const state = useReactive({ ...INITIAL_STATE });
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
const recordRef = useRef<RecordPlugin>();
|
const recordRef = useRef<RecordPlugin>();
|
||||||
const { withVisibility, addError } = useAppStore();
|
const { addError } = useAppStore();
|
||||||
const { currentService } = useConnectStore();
|
const { currentService } = useConnectStore();
|
||||||
|
|
||||||
const { wavesurfer } = useWavesurfer({
|
const { wavesurfer } = useWavesurfer({
|
||||||
@@ -146,7 +146,7 @@ const AudioRecording: FC<AudioRecordingProps> = (props) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const startRecording = async () => {
|
const startRecording = async () => {
|
||||||
await withVisibility(checkPermission);
|
await checkPermission();
|
||||||
state.isRecording = true;
|
state.isRecording = true;
|
||||||
recordRef.current?.startRecording();
|
recordRef.current?.startRecording();
|
||||||
};
|
};
|
||||||
@@ -173,9 +173,9 @@ const AudioRecording: FC<AudioRecordingProps> = (props) => {
|
|||||||
|
|
||||||
<div
|
<div
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"absolute -inset-2 flex items-center gap-1 px-1 rounded translate-x-full transition-all bg-[#ededed] dark:bg-[#202126]",
|
"absolute inset-0 flex items-center gap-1 px-1 rounded translate-x-full transition-all bg-[#ededed] dark:bg-[#202126]",
|
||||||
{
|
{
|
||||||
"!translate-x-0": state.isRecording || state.converting,
|
"translate-x-0!": state.isRecording || state.converting,
|
||||||
}
|
}
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
@@ -184,7 +184,7 @@ const AudioRecording: FC<AudioRecordingProps> = (props) => {
|
|||||||
className={clsx(
|
className={clsx(
|
||||||
"flex items-center justify-center size-6 bg-white dark:bg-black rounded-full transition cursor-pointer",
|
"flex items-center justify-center size-6 bg-white dark:bg-black rounded-full transition cursor-pointer",
|
||||||
{
|
{
|
||||||
"!cursor-not-allowed opacity-50": state.converting,
|
"cursor-not-allowed! opacity-50": state.converting,
|
||||||
}
|
}
|
||||||
)}
|
)}
|
||||||
onClick={() => resetState()}
|
onClick={() => resetState()}
|
||||||
|
|||||||
@@ -107,7 +107,7 @@ export const MessageActions = ({
|
|||||||
<button
|
<button
|
||||||
id={copyButtonId}
|
id={copyButtonId}
|
||||||
onClick={handleCopy}
|
onClick={handleCopy}
|
||||||
className="p-1 hover:bg-black/5 dark:hover:bg-white/5 rounded-lg transition-colors"
|
className="p-1 rounded-lg hover:bg-muted transition-colors"
|
||||||
>
|
>
|
||||||
{copied ? (
|
{copied ? (
|
||||||
<Check
|
<Check
|
||||||
@@ -131,7 +131,7 @@ export const MessageActions = ({
|
|||||||
{!isRefreshOnly && (
|
{!isRefreshOnly && (
|
||||||
<button
|
<button
|
||||||
onClick={handleLike}
|
onClick={handleLike}
|
||||||
className={`p-1 hover:bg-black/5 dark:hover:bg-white/5 rounded-lg transition-colors ${
|
className={`p-1 rounded-lg hover:bg-muted transition-colors ${
|
||||||
liked ? "animate-shake" : ""
|
liked ? "animate-shake" : ""
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
@@ -151,7 +151,7 @@ export const MessageActions = ({
|
|||||||
{!isRefreshOnly && (
|
{!isRefreshOnly && (
|
||||||
<button
|
<button
|
||||||
onClick={handleDislike}
|
onClick={handleDislike}
|
||||||
className={`p-1 hover:bg-black/5 dark:hover:bg-white/5 rounded-lg transition-colors ${
|
className={`p-1 rounded-lg hover:bg-muted transition-colors ${
|
||||||
disliked ? "animate-shake" : ""
|
disliked ? "animate-shake" : ""
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
@@ -172,7 +172,7 @@ export const MessageActions = ({
|
|||||||
<>
|
<>
|
||||||
<button
|
<button
|
||||||
onClick={handleSpeak}
|
onClick={handleSpeak}
|
||||||
className="p-1 hover:bg-black/5 dark:hover:bg-white/5 rounded-lg transition-colors"
|
className="p-1 rounded-lg hover:bg-muted transition-colors"
|
||||||
>
|
>
|
||||||
<Volume2
|
<Volume2
|
||||||
className={`w-4 h-4 ${
|
className={`w-4 h-4 ${
|
||||||
@@ -191,7 +191,7 @@ export const MessageActions = ({
|
|||||||
{question && (
|
{question && (
|
||||||
<button
|
<button
|
||||||
onClick={handleResend}
|
onClick={handleResend}
|
||||||
className={`p-1 hover:bg-black/5 dark:hover:bg-white/5 rounded-lg transition-colors ${
|
className={`p-1 rounded-lg hover:bg-muted transition-colors ${
|
||||||
isResending ? "animate-spin" : ""
|
isResending ? "animate-spin" : ""
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ export function DataSourceItem({ name, icon, connector }: DataSourceItemProps) {
|
|||||||
{icon?.startsWith("font_") ? (
|
{icon?.startsWith("font_") ? (
|
||||||
<FontIcon name={icon} className="size-6" />
|
<FontIcon name={icon} className="size-6" />
|
||||||
) : (
|
) : (
|
||||||
<img src={getTypeIcon()} alt={name} className="size-6 dark:drop-shadow-[0_0_6px_rgb(255,255,255)]" />
|
<img src={getTypeIcon()} alt={name} className="size-6 rounded-full dark:drop-shadow-[0_0_6px_rgb(255,255,255)]" />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<span className="font-medium text-gray-900 dark:text-white">
|
<span className="font-medium text-gray-900 dark:text-white">
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ export function DataSourcesList({ server }: { server: string }) {
|
|||||||
<h2 className="flex justify-between text-xl font-medium text-gray-900 dark:text-white">
|
<h2 className="flex justify-between text-xl font-medium text-gray-900 dark:text-white">
|
||||||
{t("cloud.dataSource.title")}
|
{t("cloud.dataSource.title")}
|
||||||
<button
|
<button
|
||||||
className="p-2 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300 rounded-[6px] bg-white dark:bg-gray-800 border border-[rgba(228,229,239,1)] dark:border-gray-700"
|
className="p-2 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300 rounded-md bg-white dark:bg-gray-800 border border-[rgba(228,229,239,1)] dark:border-gray-700"
|
||||||
onClick={() => initServerAppData()}
|
onClick={() => initServerAppData()}
|
||||||
>
|
>
|
||||||
<RefreshCcw
|
<RefreshCcw
|
||||||
|
|||||||
@@ -165,7 +165,7 @@ const LoginButton: FC<LoginButtonProps> = memo((props) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
className="px-6 py-2 bg-blue-500 text-white rounded-[6px] hover:bg-blue-600 transition-colors mb-3"
|
className="px-6 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors mb-3"
|
||||||
onClick={LoginClick}
|
onClick={LoginClick}
|
||||||
aria-label={t("cloud.login")}
|
aria-label={t("cloud.login")}
|
||||||
>
|
>
|
||||||
@@ -186,7 +186,7 @@ const LoadingState: FC<LoadingStateProps> = memo((props) => {
|
|||||||
return (
|
return (
|
||||||
<div className="flex items-center space-x-2 mb-3">
|
<div className="flex items-center space-x-2 mb-3">
|
||||||
<button
|
<button
|
||||||
className="px-6 py-2 text-white bg-red-500 rounded-[6px] hover:bg-red-600 transition-colors"
|
className="px-6 py-2 text-white bg-red-500 rounded-md hover:bg-red-600 transition-colors"
|
||||||
onClick={onCancel}
|
onClick={onCancel}
|
||||||
>
|
>
|
||||||
{t("cloud.cancel")}
|
{t("cloud.cancel")}
|
||||||
|
|||||||
@@ -18,7 +18,9 @@ const ServiceHeader = memo(
|
|||||||
({ refreshLoading, refreshClick }: ServiceHeaderProps) => {
|
({ refreshLoading, refreshClick }: ServiceHeaderProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const cloudSelectService = useConnectStore((state) => state.cloudSelectService);
|
const cloudSelectService = useConnectStore(
|
||||||
|
(state) => state.cloudSelectService
|
||||||
|
);
|
||||||
|
|
||||||
const { enableServer, removeServer } = useServers();
|
const { enableServer, removeServer } = useServers();
|
||||||
|
|
||||||
@@ -46,7 +48,7 @@ const ServiceHeader = memo(
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
className="p-2 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300 rounded-[6px] bg-white dark:bg-gray-800 border border-[rgba(228,229,239,1)] dark:border-gray-700"
|
className="p-2 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300 rounded-md bg-white dark:bg-gray-800 border border-[rgba(228,229,239,1)] dark:border-gray-700"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
OpenURLWithBrowser(cloudSelectService?.provider?.website)
|
OpenURLWithBrowser(cloudSelectService?.provider?.website)
|
||||||
}
|
}
|
||||||
@@ -54,7 +56,7 @@ const ServiceHeader = memo(
|
|||||||
<Globe className="w-3.5 h-3.5" />
|
<Globe className="w-3.5 h-3.5" />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
className="p-2 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300 rounded-[6px] bg-white dark:bg-gray-800 border border-[rgba(228,229,239,1)] dark:border-gray-700"
|
className="p-2 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300 rounded-md bg-white dark:bg-gray-800 border border-[rgba(228,229,239,1)] dark:border-gray-700"
|
||||||
onClick={() => refreshClick(cloudSelectService?.id)}
|
onClick={() => refreshClick(cloudSelectService?.id)}
|
||||||
>
|
>
|
||||||
<RefreshCcw
|
<RefreshCcw
|
||||||
@@ -63,7 +65,7 @@ const ServiceHeader = memo(
|
|||||||
</button>
|
</button>
|
||||||
{!cloudSelectService?.builtin && (
|
{!cloudSelectService?.builtin && (
|
||||||
<button
|
<button
|
||||||
className="p-2 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300 rounded-[6px] bg-white dark:bg-gray-800 border border-[rgba(228,229,239,1)] dark:border-gray-700"
|
className="p-2 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300 rounded-md bg-white dark:bg-gray-800 border border-[rgba(228,229,239,1)] dark:border-gray-700"
|
||||||
onClick={() => removeServer(cloudSelectService?.id)}
|
onClick={() => removeServer(cloudSelectService?.id)}
|
||||||
>
|
>
|
||||||
<Trash2 className="w-3.5 h-3.5 text-[#ff4747]" />
|
<Trash2 className="w-3.5 h-3.5 text-[#ff4747]" />
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ export const Sidebar = forwardRef<{ refreshData: () => void }, SidebarProps>(
|
|||||||
<img
|
<img
|
||||||
src={item?.provider?.icon || cocoLogoImg}
|
src={item?.provider?.icon || cocoLogoImg}
|
||||||
alt={`${item.name} logo`}
|
alt={`${item.name} logo`}
|
||||||
className="w-5 h-5 flex-shrink-0 dark:drop-shadow-[0_0_6px_rgb(255,255,255)]"
|
className="w-5 h-5 shrink-0 rounded-full dark:drop-shadow-[0_0_6px_rgb(255,255,255)]"
|
||||||
onError={(e) => {
|
onError={(e) => {
|
||||||
const target = e.target as HTMLImageElement;
|
const target = e.target as HTMLImageElement;
|
||||||
target.src = cocoLogoImg;
|
target.src = cocoLogoImg;
|
||||||
|
|||||||
@@ -62,13 +62,13 @@ const ApiDetails: React.FC = () => {
|
|||||||
{logs.map((log, index) => (
|
{logs.map((log, index) => (
|
||||||
<div
|
<div
|
||||||
key={index}
|
key={index}
|
||||||
className="p-4 border rounded-[6px] shadow-sm bg-gray-50"
|
className="p-4 border rounded-md shadow-sm bg-gray-50"
|
||||||
>
|
>
|
||||||
<h4 className="font-semibold text-gray-800">
|
<h4 className="font-semibold text-gray-800">
|
||||||
Latest Request {index + 1}:
|
Latest Request {index + 1}:
|
||||||
</h4>
|
</h4>
|
||||||
<div className="text-sm text-gray-700 mt-1">
|
<div className="text-sm text-gray-700 mt-1">
|
||||||
<pre className="bg-gray-100 p-2 rounded-[6px] whitespace-pre-wrap">
|
<pre className="bg-gray-100 p-2 rounded-md whitespace-pre-wrap">
|
||||||
{JSON.stringify(log.request, null, 2)}
|
{JSON.stringify(log.request, null, 2)}
|
||||||
</pre>
|
</pre>
|
||||||
</div>
|
</div>
|
||||||
@@ -87,7 +87,7 @@ const ApiDetails: React.FC = () => {
|
|||||||
</h4>
|
</h4>
|
||||||
{showIndex === index ? (
|
{showIndex === index ? (
|
||||||
<div className="text-sm text-gray-700 mt-1">
|
<div className="text-sm text-gray-700 mt-1">
|
||||||
<pre className="bg-green-100 p-2 rounded-[6px] text-green-700 whitespace-pre-wrap">
|
<pre className="bg-green-100 p-2 rounded-md text-green-700 whitespace-pre-wrap">
|
||||||
{JSON.stringify(log.response, null, 2)}
|
{JSON.stringify(log.response, null, 2)}
|
||||||
</pre>
|
</pre>
|
||||||
</div>
|
</div>
|
||||||
@@ -98,7 +98,7 @@ const ApiDetails: React.FC = () => {
|
|||||||
<>
|
<>
|
||||||
<h4 className="font-semibold text-red-800 mt-4">Error:</h4>
|
<h4 className="font-semibold text-red-800 mt-4">Error:</h4>
|
||||||
<div className="text-sm text-gray-700 mt-1">
|
<div className="text-sm text-gray-700 mt-1">
|
||||||
<pre className="bg-red-100 p-2 rounded-[6px] text-red-700 whitespace-pre-wrap">
|
<pre className="bg-red-100 p-2 rounded-md text-red-700 whitespace-pre-wrap">
|
||||||
{JSON.stringify(log.error, null, 2)}
|
{JSON.stringify(log.error, null, 2)}
|
||||||
</pre>
|
</pre>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import React, { useEffect, useCallback } from "react";
|
|||||||
import { Bot, Search } from "lucide-react";
|
import { Bot, Search } from "lucide-react";
|
||||||
|
|
||||||
import platformAdapter from "@/utils/platformAdapter";
|
import platformAdapter from "@/utils/platformAdapter";
|
||||||
|
import clsx from "clsx";
|
||||||
|
|
||||||
interface ChatSwitchProps {
|
interface ChatSwitchProps {
|
||||||
isChatMode: boolean;
|
isChatMode: boolean;
|
||||||
@@ -29,19 +30,31 @@ const ChatSwitch: React.FC<ChatSwitchProps> = ({ isChatMode, onChange }) => {
|
|||||||
<div
|
<div
|
||||||
role="switch"
|
role="switch"
|
||||||
aria-checked={isChatMode}
|
aria-checked={isChatMode}
|
||||||
className={`relative flex items-center justify-between w-10 h-[20px] rounded-full cursor-pointer transition-colors duration-300 ${
|
className={`relative flex items-center justify-between w-10 h-5 rounded-full cursor-pointer transition-colors duration-300 ${
|
||||||
isChatMode ? "bg-[#0072ff]" : "bg-[var(--coco-primary-color)]"
|
isChatMode ? "bg-[#0072ff]" : "bg-(--coco-primary-color)"
|
||||||
}`}
|
}`}
|
||||||
onClick={handleToggle}
|
onClick={handleToggle}
|
||||||
>
|
>
|
||||||
<div className="absolute top-0 left-0 w-full h-full pointer-events-none flex items-center justify-between px-1">
|
|
||||||
{isChatMode ? <Bot className="w-4 h-4 text-white" /> : <div></div>}
|
|
||||||
{!isChatMode ? <Search className="w-4 h-4 text-white" /> : <div></div>}
|
|
||||||
</div>
|
|
||||||
<div
|
<div
|
||||||
className={`absolute top-[1px] left-[1px] h-[18px] w-[18px] bg-white rounded-full shadow-md transform transition-transform duration-300 ${
|
className={clsx(
|
||||||
isChatMode ? "translate-x-5" : "translate-x-0"
|
"absolute inset-0 pointer-events-none flex items-center px-1 text-white",
|
||||||
}`}
|
{
|
||||||
|
"justify-end": !isChatMode,
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{isChatMode ? (
|
||||||
|
<Bot className="size-4" />
|
||||||
|
) : (
|
||||||
|
<Search className="size-4" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className={clsx(
|
||||||
|
"absolute top-px h-4.5 w-4.5 bg-white rounded-full shadow-md",
|
||||||
|
[isChatMode ? "right-px" : "left-px"]
|
||||||
|
)}
|
||||||
></div>
|
></div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,33 +1,35 @@
|
|||||||
import {
|
import * as CheckboxPrimitive from "@radix-ui/react-checkbox";
|
||||||
CheckboxProps as HeadlessCheckboxProps,
|
import type { ComponentProps } from "react";
|
||||||
Checkbox as HeadlessCheckbox,
|
|
||||||
} from "@headlessui/react";
|
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import { CheckIcon } from "lucide-react";
|
import { CheckIcon } from "lucide-react";
|
||||||
|
|
||||||
interface CheckboxProps extends HeadlessCheckboxProps {
|
interface CheckboxProps
|
||||||
|
extends Omit<ComponentProps<typeof CheckboxPrimitive.Root>, "onCheckedChange" | "onChange"> {
|
||||||
indeterminate?: boolean;
|
indeterminate?: boolean;
|
||||||
|
onChange?: (checked: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Checkbox = (props: CheckboxProps) => {
|
const Checkbox = (props: CheckboxProps) => {
|
||||||
const { indeterminate, className, ...rest } = props;
|
const { indeterminate, className, onChange, checked, ...rest } = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<HeadlessCheckbox
|
<CheckboxPrimitive.Root
|
||||||
{...rest}
|
{...rest}
|
||||||
|
checked={checked}
|
||||||
|
onCheckedChange={(v) => onChange?.(v === true)}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"group size-4 rounded-sm border border-black/30 dark:border-white/30 data-[checked]:bg-[#2F54EB] data-[checked]:!border-[#2F54EB] transition cursor-pointer",
|
"group h-4 w-4 rounded-sm border border-black/30 dark:border-white/30 data-[state=checked]:bg-[#2F54EB] data-[state=checked]:border-[#2F54EB] transition cursor-pointer inline-flex items-center justify-center",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{indeterminate && (
|
{indeterminate && (
|
||||||
<div className="size-full flex items-center justify-center group-data-[checked]:hidden">
|
<div className="h-full w-full flex items-center justify-center group-data-[state=checked]:hidden">
|
||||||
<div className="size-2 bg-[#2F54EB]"></div>
|
<div className="h-2 w-2 bg-[#2F54EB]"></div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<CheckIcon className="hidden size-[14px] text-white group-data-[checked]:block" />
|
<CheckIcon className="hidden h-[14px] w-[14px] text-white group-data-[state=checked]:block" />
|
||||||
</HeadlessCheckbox>
|
</CheckboxPrimitive.Root>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ const Copyright = () => {
|
|||||||
const renderLogo = () => {
|
const renderLogo = () => {
|
||||||
return (
|
return (
|
||||||
<a href="https://coco.rs/" target="_blank">
|
<a href="https://coco.rs/" target="_blank">
|
||||||
<img src={isDark ? logoDark : logoLight} alt="Logo" className="h-4" />
|
<img src={isDark ? logoDark : logoLight} alt="Logo" className="h-4!" />
|
||||||
</a>
|
</a>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,24 +1,20 @@
|
|||||||
import {
|
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from "@/components/ui/dialog";
|
||||||
Button,
|
import { Button } from "@/components/ui/button";
|
||||||
ButtonProps,
|
import { FC, KeyboardEvent, ComponentProps } from "react";
|
||||||
Description,
|
|
||||||
Dialog,
|
|
||||||
DialogPanel,
|
|
||||||
DialogTitle,
|
|
||||||
} from "@headlessui/react";
|
|
||||||
import { FC, KeyboardEvent } from "react";
|
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import { twMerge } from "tailwind-merge";
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
import VisibleKey from "./VisibleKey";
|
import VisibleKey from "./VisibleKey";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
|
type ShadButtonProps = ComponentProps<typeof Button>;
|
||||||
|
|
||||||
interface DeleteDialogProps {
|
interface DeleteDialogProps {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
title: string;
|
title: string;
|
||||||
description: string;
|
description: string;
|
||||||
deleteButtonProps?: ButtonProps;
|
deleteButtonProps?: ShadButtonProps;
|
||||||
cancelButtonProps?: ButtonProps;
|
cancelButtonProps?: ShadButtonProps;
|
||||||
reverseButtonPosition?: boolean;
|
reverseButtonPosition?: boolean;
|
||||||
setIsOpen: (isOpen: boolean) => void;
|
setIsOpen: (isOpen: boolean) => void;
|
||||||
onCancel: () => void;
|
onCancel: () => void;
|
||||||
@@ -49,20 +45,12 @@ const DeleteDialog: FC<DeleteDialogProps> = (props) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog
|
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||||
open={isOpen}
|
<DialogContent className="flex flex-col justify-between w-[360px] h-[160px] p-3 text-[#333] dark:text-white/90 border border-[#e6e6e6] bg-white dark:bg-[#202126] dark:border-white/10 shadow-[0_4px_10px_rgba(0,0,0,0.2)] rounded-lg dark:shadow-[0_8px_20px_rgba(0,0,0,0.4)]">
|
||||||
onClose={() => setIsOpen(false)}
|
<DialogHeader className="mb-2">
|
||||||
className="relative z-1000"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
id="headlessui-popover-panel:delete-history"
|
|
||||||
className="fixed inset-0 flex items-center justify-center w-screen"
|
|
||||||
>
|
|
||||||
<DialogPanel className="flex flex-col justify-between w-[360px] h-[160px] p-3 text-[#333] dark:text-white/90 border border-[#e6e6e6] bg-white dark:bg-[#202126] dark:border-white/10 shadow-[0_4px_10px_rgba(0,0,0,0.2)] rounded-lg dark:shadow-[0_8px_20px_rgba(0,0,0,0.4)]">
|
|
||||||
<div className="flex flex-col gap-3">
|
|
||||||
<DialogTitle className="text-base font-bold">{title}</DialogTitle>
|
<DialogTitle className="text-base font-bold">{title}</DialogTitle>
|
||||||
<Description className="text-sm">{description}</Description>
|
<DialogDescription className="text-sm">{description}</DialogDescription>
|
||||||
</div>
|
</DialogHeader>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className={clsx("flex gap-4 self-end", {
|
className={clsx("flex gap-4 self-end", {
|
||||||
@@ -110,8 +98,7 @@ const DeleteDialog: FC<DeleteDialogProps> = (props) => {
|
|||||||
</Button>
|
</Button>
|
||||||
</VisibleKey>
|
</VisibleKey>
|
||||||
</div>
|
</div>
|
||||||
</DialogPanel>
|
</DialogContent>
|
||||||
</div>
|
|
||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import {
|
import {
|
||||||
Button,
|
|
||||||
Description,
|
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogPanel,
|
DialogContent,
|
||||||
|
DialogHeader,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
} from "@headlessui/react";
|
DialogDescription,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import VisibleKey from "@/components/Common/VisibleKey";
|
import VisibleKey from "@/components/Common/VisibleKey";
|
||||||
@@ -36,21 +37,13 @@ const DeleteDialog = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog
|
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||||
open={isOpen}
|
<DialogContent className="flex flex-col justify-between w-[360px] h-40 p-3 text-[#333] dark:text-white/90 border border-[#e6e6e6] bg-white dark:bg-[#202126] dark:border-white/10 shadow-xl rounded-lg">
|
||||||
onClose={() => setIsOpen(false)}
|
<DialogHeader className="mb-2">
|
||||||
className="relative z-1000"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
id="headlessui-popover-panel:delete-history"
|
|
||||||
className="fixed inset-0 flex items-center justify-center w-screen"
|
|
||||||
>
|
|
||||||
<DialogPanel className="flex flex-col justify-between w-[360px] h-[160px] p-3 text-[#333] dark:text-white/90 border border-[#e6e6e6] bg-white dark:bg-[#202126] dark:border-white/10 shadow-xl rounded-lg">
|
|
||||||
<div className="flex flex-col gap-3">
|
|
||||||
<DialogTitle className="text-base font-bold">
|
<DialogTitle className="text-base font-bold">
|
||||||
{t("history_list.delete_modal.title")}
|
{t("history_list.delete_modal.title")}
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
<Description className="text-sm">
|
<DialogDescription className="text-sm">
|
||||||
{t("history_list.delete_modal.description", {
|
{t("history_list.delete_modal.description", {
|
||||||
replace: [
|
replace: [
|
||||||
active?._source?.title ||
|
active?._source?.title ||
|
||||||
@@ -58,18 +51,20 @@ const DeleteDialog = ({
|
|||||||
active?._id,
|
active?._id,
|
||||||
],
|
],
|
||||||
})}
|
})}
|
||||||
</Description>
|
</DialogDescription>
|
||||||
</div>
|
</DialogHeader>
|
||||||
|
|
||||||
<div className="flex gap-4 self-end">
|
<div className="flex gap-4 self-end">
|
||||||
<VisibleKey
|
<VisibleKey
|
||||||
shortcut="N"
|
shortcut="N"
|
||||||
shortcutClassName="left-[unset] right-0"
|
shortcutClassName="left-[unset] right-0"
|
||||||
onKeyPress={() => setIsOpen(false)}
|
onKeyPress={() => {
|
||||||
|
setIsOpen(false);
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
|
variant="outline"
|
||||||
autoFocus
|
autoFocus
|
||||||
className="h-8 px-4 text-sm text-[#666666] bg-[#F8F9FA] dark:text-white dark:bg-[#202126] border border-[#E6E6E6] dark:border-white/10 rounded-lg focus:border-black/30 dark:focus:border-white/50 transition"
|
|
||||||
onClick={() => setIsOpen(false)}
|
onClick={() => setIsOpen(false)}
|
||||||
onKeyDown={(event) => {
|
onKeyDown={(event) => {
|
||||||
handleEnter(event, () => {
|
handleEnter(event, () => {
|
||||||
@@ -87,7 +82,8 @@ const DeleteDialog = ({
|
|||||||
onKeyPress={handleRemove}
|
onKeyPress={handleRemove}
|
||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
className="h-8 px-4 text-sm text-white bg-[#EF4444] rounded-lg border border-[#EF4444] focus:border-black/30 dark:focus:border-white/50 transition"
|
variant="destructive"
|
||||||
|
className="text-white"
|
||||||
onClick={handleRemove}
|
onClick={handleRemove}
|
||||||
onKeyDown={(event) => {
|
onKeyDown={(event) => {
|
||||||
handleEnter(event, handleRemove);
|
handleEnter(event, handleRemove);
|
||||||
@@ -97,8 +93,7 @@ const DeleteDialog = ({
|
|||||||
</Button>
|
</Button>
|
||||||
</VisibleKey>
|
</VisibleKey>
|
||||||
</div>
|
</div>
|
||||||
</DialogPanel>
|
</DialogContent>
|
||||||
</div>
|
|
||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -113,7 +113,8 @@ const HistoryListContent: FC<HistoryListContentProps> = ({
|
|||||||
const scrollToElement = useCallback(
|
const scrollToElement = useCallback(
|
||||||
(elementId: string, isKeyboardNav: boolean) => {
|
(elementId: string, isKeyboardNav: boolean) => {
|
||||||
if (!listRef.current) return;
|
if (!listRef.current) return;
|
||||||
if (typeof window === 'undefined' || typeof document === 'undefined') return;
|
if (typeof window === "undefined" || typeof document === "undefined")
|
||||||
|
return;
|
||||||
|
|
||||||
const element = listRef.current.querySelector(`#${elementId}`);
|
const element = listRef.current.querySelector(`#${elementId}`);
|
||||||
if (!element) return;
|
if (!element) return;
|
||||||
|
|||||||
@@ -1,10 +1,16 @@
|
|||||||
import { FC, useRef, useCallback, useState } from "react";
|
import { FC, useRef, useCallback, useState, useEffect } from "react";
|
||||||
import { Input, Popover, PopoverButton, PopoverPanel } from "@headlessui/react";
|
|
||||||
import { Ellipsis } from "lucide-react";
|
import { Ellipsis } from "lucide-react";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Pencil, Trash2 } from "lucide-react";
|
import { Pencil, Trash2 } from "lucide-react";
|
||||||
|
|
||||||
|
import {
|
||||||
|
Popover,
|
||||||
|
PopoverTrigger,
|
||||||
|
PopoverContent,
|
||||||
|
PopoverPortal,
|
||||||
|
} from "@/components/ui/popover";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
import type { Chat } from "@/types/chat";
|
import type { Chat } from "@/types/chat";
|
||||||
import VisibleKey from "../VisibleKey";
|
import VisibleKey from "../VisibleKey";
|
||||||
|
|
||||||
@@ -31,9 +37,11 @@ const HistoryListItem: FC<HistoryListItemProps> = ({
|
|||||||
const moreButtonRef = useRef<HTMLButtonElement>(null);
|
const moreButtonRef = useRef<HTMLButtonElement>(null);
|
||||||
const { _id, _source } = item;
|
const { _id, _source } = item;
|
||||||
const title = _source?.title ?? _id;
|
const title = _source?.title ?? _id;
|
||||||
const isActive = item._id === active?._id || item._id === highlightId;
|
const isSelected = item._id === active?._id;
|
||||||
|
const isHovered = item._id === highlightId;
|
||||||
|
|
||||||
const [isEdit, setIsEdit] = useState(false);
|
const [isEdit, setIsEdit] = useState(false);
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
const onContextMenu = useCallback(
|
const onContextMenu = useCallback(
|
||||||
(e: React.MouseEvent) => {
|
(e: React.MouseEvent) => {
|
||||||
@@ -72,24 +80,34 @@ const HistoryListItem: FC<HistoryListItemProps> = ({
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!(isSelected || isHovered) || isEdit) {
|
||||||
|
setOpen(false);
|
||||||
|
}
|
||||||
|
}, [isSelected, isHovered, isEdit]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<li
|
<li
|
||||||
key={_id}
|
key={_id}
|
||||||
id={_id}
|
id={_id}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"flex items-center mt-1 h-10 rounded-lg cursor-pointer hover:bg-[#EDEDED] dark:hover:bg-[#353F4D] transition",
|
"group flex w-full items-center mt-1 h-10 rounded-lg cursor-pointer hover:bg-[#EDEDED] dark:hover:bg-[#353F4D] transition-colors",
|
||||||
{
|
{
|
||||||
"!bg-[#E5E7EB] dark:!bg-[#2B3444]": isActive,
|
"bg-[#E5E7EB] dark:bg-[#2B3444]": isSelected,
|
||||||
|
"bg-[#EDEDED] dark:bg-[#353F4D]": isHovered && !isSelected,
|
||||||
}
|
}
|
||||||
)}
|
)}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (!isActive) {
|
if (!isSelected) {
|
||||||
setIsEdit(false);
|
setIsEdit(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
onSelect(item);
|
onSelect(item);
|
||||||
}}
|
}}
|
||||||
onMouseEnter={onMouseEnter}
|
onMouseEnter={onMouseEnter}
|
||||||
|
onMouseLeave={() => {
|
||||||
|
setOpen(false);
|
||||||
|
}}
|
||||||
onContextMenu={onContextMenu}
|
onContextMenu={onContextMenu}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
@@ -99,11 +117,11 @@ const HistoryListItem: FC<HistoryListItemProps> = ({
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="flex-1 flex items-center justify-between gap-2 px-2 overflow-hidden">
|
<div className="flex-1 flex items-center justify-between gap-2 px-2 overflow-hidden">
|
||||||
{isEdit && isActive ? (
|
{isEdit && isSelected ? (
|
||||||
<Input
|
<Input
|
||||||
autoFocus
|
autoFocus
|
||||||
defaultValue={title}
|
defaultValue={title}
|
||||||
className="flex-1 -mx-px outline-none bg-transparent border border-[#0061FF] rounded-[4px]"
|
className="flex-1 -mx-px outline-none bg-transparent border border-[#0061FF] rounded-sm"
|
||||||
onKeyDown={(event) => {
|
onKeyDown={(event) => {
|
||||||
if (event.key !== "Enter") return;
|
if (event.key !== "Enter") return;
|
||||||
|
|
||||||
@@ -128,7 +146,7 @@ const HistoryListItem: FC<HistoryListItemProps> = ({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
{isActive && !isEdit && (
|
{!isEdit && isSelected && (
|
||||||
<VisibleKey
|
<VisibleKey
|
||||||
shortcut="↑↓"
|
shortcut="↑↓"
|
||||||
rootClassName="w-6"
|
rootClassName="w-6"
|
||||||
@@ -136,9 +154,22 @@ const HistoryListItem: FC<HistoryListItemProps> = ({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Popover>
|
<Popover open={open} onOpenChange={setOpen}>
|
||||||
{isActive && !isEdit && (
|
<PopoverTrigger
|
||||||
<PopoverButton ref={moreButtonRef} className="flex gap-2">
|
ref={moreButtonRef}
|
||||||
|
className={clsx("flex gap-2", {
|
||||||
|
"opacity-100 pointer-events-auto":
|
||||||
|
!isEdit && (isSelected || isHovered),
|
||||||
|
"opacity-0 pointer-events-none": !(
|
||||||
|
!isEdit &&
|
||||||
|
(isSelected || isHovered)
|
||||||
|
),
|
||||||
|
})}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
setOpen((prev) => !prev);
|
||||||
|
}}
|
||||||
|
>
|
||||||
<VisibleKey
|
<VisibleKey
|
||||||
shortcut="O"
|
shortcut="O"
|
||||||
onKeyPress={() => {
|
onKeyPress={() => {
|
||||||
@@ -147,15 +178,18 @@ const HistoryListItem: FC<HistoryListItemProps> = ({
|
|||||||
>
|
>
|
||||||
<Ellipsis className="size-4 text-[#979797]" />
|
<Ellipsis className="size-4 text-[#979797]" />
|
||||||
</VisibleKey>
|
</VisibleKey>
|
||||||
</PopoverButton>
|
</PopoverTrigger>
|
||||||
)}
|
|
||||||
|
|
||||||
<PopoverPanel
|
<PopoverPortal>
|
||||||
anchor="bottom"
|
<PopoverContent
|
||||||
|
side="bottom"
|
||||||
className="flex flex-col rounded-lg shadow-md z-100 bg-white dark:bg-[#202126] p-1 border border-black/2 dark:border-white/10"
|
className="flex flex-col rounded-lg shadow-md z-100 bg-white dark:bg-[#202126] p-1 border border-black/2 dark:border-white/10"
|
||||||
onClick={(event) => {
|
onClick={(event) => {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
}}
|
}}
|
||||||
|
onMouseLeave={() => {
|
||||||
|
setOpen(false);
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{menuItems.map((menuItem) => {
|
{menuItems.map((menuItem) => {
|
||||||
const {
|
const {
|
||||||
@@ -169,7 +203,7 @@ const HistoryListItem: FC<HistoryListItemProps> = ({
|
|||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
key={label}
|
key={label}
|
||||||
className="flex items-center gap-2 px-3 py-2 text-sm rounded-[6px] hover:bg-[#EDEDED] dark:hover:bg-[#2B2C31] transition"
|
className="flex items-center gap-2 px-3 py-2 text-sm rounded-md hover:bg-[#EDEDED] dark:hover:bg-[#2B2C31] transition"
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
>
|
>
|
||||||
<VisibleKey shortcut={shortcut} onKeyPress={onClick}>
|
<VisibleKey shortcut={shortcut} onKeyPress={onClick}>
|
||||||
@@ -185,7 +219,8 @@ const HistoryListItem: FC<HistoryListItemProps> = ({
|
|||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</PopoverPanel>
|
</PopoverContent>
|
||||||
|
</PopoverPortal>
|
||||||
</Popover>
|
</Popover>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Input } from "@headlessui/react";
|
import { Input } from "@/components/ui/input";
|
||||||
import { debounce } from "lodash-es";
|
import { debounce } from "lodash-es";
|
||||||
import { FC, useMemo, useRef, useState } from "react";
|
import { FC, useMemo, useRef, useState } from "react";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
@@ -9,6 +9,7 @@ import VisibleKey from "../VisibleKey";
|
|||||||
import { Chat } from "@/types/chat";
|
import { Chat } from "@/types/chat";
|
||||||
import { closeHistoryPanel } from "@/utils";
|
import { closeHistoryPanel } from "@/utils";
|
||||||
import HistoryListContent from "./HistoryListContent";
|
import HistoryListContent from "./HistoryListContent";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
|
||||||
interface HistoryListProps {
|
interface HistoryListProps {
|
||||||
historyPanelId?: string;
|
historyPanelId?: string;
|
||||||
@@ -57,21 +58,21 @@ const HistoryList: FC<HistoryListProps> = (props) => {
|
|||||||
"flex flex-col h-screen text-sm bg-[#F3F4F6] dark:bg-[#1F2937]"
|
"flex flex-col h-screen text-sm bg-[#F3F4F6] dark:bg-[#1F2937]"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="flex gap-1 p-2 border-b dark:border-[#343D4D]">
|
<div className="flex gap-1 p-2 border-b border-input">
|
||||||
<div className="flex-1 flex items-center gap-2 px-2 rounded-lg border transition border-[#E6E6E6] bg-[#F8F9FA] dark:bg-[#2B3444] dark:border-[#343D4D] focus-within:border-[#0061FF]">
|
<div className="flex-1 h-8 flex items-center px-2 rounded-lg border border-input bg-background transition focus-within:ring-2 focus-within:ring-ring focus-within:ring-offset-2 focus-within:ring-offset-background">
|
||||||
<VisibleKey
|
<VisibleKey
|
||||||
shortcut="F"
|
shortcut="F"
|
||||||
onKeyPress={() => {
|
onKeyPress={() => {
|
||||||
searchInputRef.current?.focus();
|
searchInputRef.current?.focus();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Search className="size-4 text-[#6B7280]" />
|
<Search className="size-4 text-muted-foreground" />
|
||||||
</VisibleKey>
|
</VisibleKey>
|
||||||
|
|
||||||
<Input
|
<Input
|
||||||
autoFocus
|
autoFocus
|
||||||
ref={searchInputRef}
|
ref={searchInputRef}
|
||||||
className="w-full bg-transparent outline-none"
|
className="w-full h-8 bg-transparent border-0 shadow-none focus-visible:ring-0 focus-visible:ring-offset-0"
|
||||||
placeholder={t("history_list.search.placeholder")}
|
placeholder={t("history_list.search.placeholder")}
|
||||||
onChange={(event) => {
|
onChange={(event) => {
|
||||||
debouncedSearch(event.target.value);
|
debouncedSearch(event.target.value);
|
||||||
@@ -79,18 +80,20 @@ const HistoryList: FC<HistoryListProps> = (props) => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<Button
|
||||||
className="size-8 flex items-center justify-center rounded-lg border text-[#0072FF] border-[#E6E6E6] bg-[#F3F4F6] dark:border-[#343D4D] dark:bg-[#1F2937] hover:bg-[#F8F9FA] dark:hover:bg-[#353F4D] cursor-pointer transition"
|
variant="outline"
|
||||||
|
size="icon"
|
||||||
|
className="size-8"
|
||||||
onClick={handleRefresh}
|
onClick={handleRefresh}
|
||||||
>
|
>
|
||||||
<VisibleKey shortcut="R" onKeyPress={handleRefresh}>
|
<VisibleKey shortcut="R" onKeyPress={handleRefresh}>
|
||||||
<RefreshCcw
|
<RefreshCcw
|
||||||
className={clsx("size-4", {
|
className={clsx("size-4 text-[#0287FF]", {
|
||||||
"animate-spin": isRefresh,
|
"animate-spin": isRefresh,
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
</VisibleKey>
|
</VisibleKey>
|
||||||
</div>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex-1 px-2 overflow-auto custom-scrollbar">
|
<div className="flex-1 px-2 overflow-auto custom-scrollbar">
|
||||||
@@ -104,10 +107,10 @@ const HistoryList: FC<HistoryListProps> = (props) => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{historyPanelId && (
|
{historyPanelId && (
|
||||||
<div className="flex justify-end p-2 border-t dark:border-[#343D4D]">
|
<div className="flex justify-end p-2 border-t border-input">
|
||||||
<VisibleKey shortcut="Esc" shortcutClassName="w-7">
|
<VisibleKey shortcut="Esc" shortcutClassName="w-7">
|
||||||
<PanelLeftClose
|
<PanelLeftClose
|
||||||
className="size-4 text-black/80 dark:text-white/80 cursor-pointer"
|
className="size-4 text-muted-foreground cursor-pointer"
|
||||||
onClick={closeHistoryPanel}
|
onClick={closeHistoryPanel}
|
||||||
/>
|
/>
|
||||||
</VisibleKey>
|
</VisibleKey>
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ function UniversalIcon({
|
|||||||
icon,
|
icon,
|
||||||
defaultIcon = File,
|
defaultIcon = File,
|
||||||
appIcon = false,
|
appIcon = false,
|
||||||
className = "w-5 h-5 flex-shrink-0",
|
className = "w-5 h-5 shrink-0",
|
||||||
onClick = () => {},
|
onClick = () => {},
|
||||||
wrapWithIconWrapper = true,
|
wrapWithIconWrapper = true,
|
||||||
}: UniversalIconProps) {
|
}: UniversalIconProps) {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { ChevronLeft, ChevronRight } from "lucide-react";
|
import { ChevronLeft, ChevronRight } from "lucide-react";
|
||||||
|
|
||||||
import VisibleKey from "./VisibleKey";
|
import VisibleKey from "./VisibleKey";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
interface PaginationProps {
|
interface PaginationProps {
|
||||||
current: number;
|
current: number;
|
||||||
@@ -19,10 +20,15 @@ function Pagination({
|
|||||||
}: PaginationProps) {
|
}: PaginationProps) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`flex items-center justify-between h-8 px-3 text-[#999] border-t dark:border-t-white/10 ${className}`}
|
className={`flex items-center justify-between h-8 px-2 text-muted-foreground border-t border-input ${className}`}
|
||||||
>
|
>
|
||||||
<VisibleKey shortcut="leftarrow" onKeyPress={onPrev}>
|
<VisibleKey shortcut="leftarrow" onKeyPress={onPrev}>
|
||||||
<ChevronLeft className="size-4 cursor-pointer" onClick={onPrev} />
|
<ChevronLeft
|
||||||
|
className={cn("size-4 cursor-pointer", {
|
||||||
|
"cursor-not-allowed opacity-50": current === 1,
|
||||||
|
})}
|
||||||
|
onClick={onPrev}
|
||||||
|
/>
|
||||||
</VisibleKey>
|
</VisibleKey>
|
||||||
|
|
||||||
<div className="text-xs">
|
<div className="text-xs">
|
||||||
@@ -30,7 +36,12 @@ function Pagination({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<VisibleKey shortcut="rightarrow" onKeyPress={onNext}>
|
<VisibleKey shortcut="rightarrow" onKeyPress={onNext}>
|
||||||
<ChevronRight className="size-4 cursor-pointer" onClick={onNext} />
|
<ChevronRight
|
||||||
|
className={cn("size-4 cursor-pointer", {
|
||||||
|
"cursor-not-allowed opacity-50": current === totalPage,
|
||||||
|
})}
|
||||||
|
onClick={onNext}
|
||||||
|
/>
|
||||||
</VisibleKey>
|
</VisibleKey>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Input, InputProps } from "@headlessui/react";
|
import type { InputProps } from "@/components/ui/input";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
import { useKeyPress } from "ahooks";
|
import { useKeyPress } from "ahooks";
|
||||||
import { forwardRef, useImperativeHandle, useRef } from "react";
|
import { forwardRef, useImperativeHandle, useRef } from "react";
|
||||||
|
|
||||||
@@ -29,7 +30,7 @@ const PopoverInput = forwardRef<HTMLInputElement, InputProps>((props, ref) => {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
return <Input autoCorrect="off" ref={inputRef} {...props} />;
|
return <Input autoCorrect="off" ref={inputRef} {...(props as any)} />;
|
||||||
});
|
});
|
||||||
|
|
||||||
export default PopoverInput;
|
export default PopoverInput;
|
||||||
|
|||||||
@@ -1,20 +1,20 @@
|
|||||||
import { RefObject } from "react";
|
import { RefObject } from "react";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import { ArrowDown } from "lucide-react";
|
import { ArrowDown } from "lucide-react";
|
||||||
|
import { Button } from "../ui/button";
|
||||||
|
|
||||||
interface ScrollToBottomProps {
|
interface ScrollToBottomProps {
|
||||||
scrollRef: RefObject<HTMLDivElement>;
|
scrollRef: RefObject<HTMLDivElement>;
|
||||||
isAtBottom: boolean;
|
isAtBottom: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ScrollToBottom = ({
|
const ScrollToBottom = ({ scrollRef, isAtBottom }: ScrollToBottomProps) => {
|
||||||
scrollRef,
|
|
||||||
isAtBottom,
|
|
||||||
}: ScrollToBottomProps) => {
|
|
||||||
return (
|
return (
|
||||||
<button
|
<Button
|
||||||
|
size="icon"
|
||||||
|
variant="outline"
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"absolute right-4 bottom-4 flex items-center justify-center size-8 border bg-white rounded-full shadow dark:border-[#272828] dark:bg-black dark:shadow-white/15",
|
"absolute right-4 bottom-4 border border-border rounded-full shadow dark:shadow-white/15",
|
||||||
{
|
{
|
||||||
hidden: isAtBottom,
|
hidden: isAtBottom,
|
||||||
}
|
}
|
||||||
@@ -27,7 +27,7 @@ const ScrollToBottom = ({
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ArrowDown className="size-5" />
|
<ArrowDown className="size-5" />
|
||||||
</button>
|
</Button>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,40 +1,38 @@
|
|||||||
import {
|
import { FC, ReactNode } from "react";
|
||||||
Popover,
|
|
||||||
PopoverButton,
|
|
||||||
PopoverPanel,
|
|
||||||
PopoverPanelProps,
|
|
||||||
} from "@headlessui/react";
|
|
||||||
import { useBoolean } from "ahooks";
|
import { useBoolean } from "ahooks";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import { FC, ReactNode } from "react";
|
import {
|
||||||
|
Popover,
|
||||||
interface Tooltip2Props extends PopoverPanelProps {
|
PopoverTrigger,
|
||||||
|
PopoverContent,
|
||||||
|
} from "@/components/ui/popover";
|
||||||
|
interface Tooltip2Props {
|
||||||
content: string;
|
content: string;
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Tooltip2: FC<Tooltip2Props> = (props) => {
|
const Tooltip2: FC<Tooltip2Props> = (props) => {
|
||||||
const { content, children, anchor = "top", ...rest } = props;
|
const { content, children, className } = props;
|
||||||
const [visible, { setTrue, setFalse }] = useBoolean(false);
|
const [visible, { setTrue, setFalse }] = useBoolean(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover>
|
<Popover>
|
||||||
<PopoverButton onMouseOver={setTrue} onMouseOut={setFalse}>
|
<PopoverTrigger onMouseOver={setTrue} onMouseOut={setFalse}>
|
||||||
{children}
|
{children}
|
||||||
</PopoverButton>
|
</PopoverTrigger>
|
||||||
<PopoverPanel
|
<PopoverContent
|
||||||
{...rest}
|
side="top"
|
||||||
static
|
|
||||||
anchor={anchor}
|
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"fixed z-1000 p-2 rounded-[6px] text-xs text-white bg-black/75 hidden",
|
"z-1000 p-2 rounded-md text-xs text-white bg-black/75 hidden",
|
||||||
{
|
{
|
||||||
"!block": visible,
|
block: visible,
|
||||||
}
|
},
|
||||||
|
className
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{content}
|
{content}
|
||||||
</PopoverPanel>
|
</PopoverContent>
|
||||||
</Popover>
|
</Popover>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ export default function Footer({ setIsPinnedWeb }: FooterProps) {
|
|||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<img
|
<img
|
||||||
src={selectedExtension.icon}
|
src={selectedExtension.icon}
|
||||||
className="size-5 dark:drop-shadow-[0_0_6px_rgb(255,255,255)]"
|
className="h-5 w-5 dark:drop-shadow-[0_0_6px_rgb(255,255,255)]"
|
||||||
/>
|
/>
|
||||||
<span className="text-sm">{selectedExtension.name}</span>
|
<span className="text-sm">{selectedExtension.name}</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -81,7 +81,7 @@ export default function Footer({ setIsPinnedWeb }: FooterProps) {
|
|||||||
if (visibleExtensionStore) {
|
if (visibleExtensionStore) {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<FontIcon name="font_Store" className="size-5" />
|
<FontIcon name="font_Store" className="h-5 w-5" />
|
||||||
<span className="text-sm">Extension Store</span>
|
<span className="text-sm">Extension Store</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -100,7 +100,7 @@ export default function Footer({ setIsPinnedWeb }: FooterProps) {
|
|||||||
{hasUpdate ? (
|
{hasUpdate ? (
|
||||||
<div className="cursor-pointer" onClick={() => setVisible(true)}>
|
<div className="cursor-pointer" onClick={() => setVisible(true)}>
|
||||||
<span>{t("search.footer.updateAvailable")}</span>
|
<span>{t("search.footer.updateAvailable")}</span>
|
||||||
<span className="absolute top-0 -right-2 size-1.5 bg-[#FF3434] rounded-full"></span>
|
<span className="absolute top-0 -right-2 h-1.5 w-1.5 bg-[#FF3434] rounded-full"></span>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
sourceData?.source?.name ||
|
sourceData?.source?.name ||
|
||||||
@@ -117,7 +117,7 @@ export default function Footer({ setIsPinnedWeb }: FooterProps) {
|
|||||||
<div
|
<div
|
||||||
data-tauri-drag-region={isTauri}
|
data-tauri-drag-region={isTauri}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"px-4 z-999 mx-[1px] h-8 absolute bottom-0 left-0 right-0 border-t border-gray-200 dark:border-gray-700 flex items-center justify-between rounded-[6px] rounded-t-none",
|
"px-4 z-999 mx-px h-8 absolute bottom-0 left-0 right-0 border-t! border-gray-200 dark:border-gray-700 flex items-center justify-between rounded-md rounded-t-none",
|
||||||
{
|
{
|
||||||
"overflow-hidden": isTauri,
|
"overflow-hidden": isTauri,
|
||||||
}
|
}
|
||||||
@@ -137,7 +137,7 @@ export default function Footer({ setIsPinnedWeb }: FooterProps) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<WebLogin panelClassName="bottom-5 left-0" />
|
<WebLogin side="top" align="start" />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className={`flex mobile:hidden items-center gap-3`}>
|
<div className={`flex mobile:hidden items-center gap-3`}>
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ export const NoResults = () => {
|
|||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<WebLoginButton />
|
<WebLoginButton />
|
||||||
|
|
||||||
<WebRefreshButton className="size-8" />
|
<WebRefreshButton />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -54,7 +54,7 @@ export const NoResults = () => {
|
|||||||
|
|
||||||
<span
|
<span
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"ml-3 h-5 min-w-5 rounded-[6px] border border-[#D8D8D8] flex justify-center items-center",
|
"ml-3 h-5 min-w-5 rounded-md border border-[#D8D8D8] flex justify-center items-center",
|
||||||
{
|
{
|
||||||
"px-1": !isMac,
|
"px-1": !isMac,
|
||||||
}
|
}
|
||||||
@@ -63,7 +63,7 @@ export const NoResults = () => {
|
|||||||
{formatKey(modifierKey)}
|
{formatKey(modifierKey)}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<span className="ml-1 w-5 h-5 rounded-[6px] border border-[#D8D8D8] flex justify-center items-center">
|
<span className="ml-1 w-5 h-5 rounded-md border border-[#D8D8D8] flex justify-center items-center">
|
||||||
{modeSwitch}
|
{modeSwitch}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,13 +1,16 @@
|
|||||||
import { Menu, MenuButton } from "@headlessui/react";
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
} from "@/components/ui/dropdown-menu";
|
||||||
|
|
||||||
import logoImg from "@/assets/icon.svg";
|
import logoImg from "@/assets/icon.svg";
|
||||||
|
|
||||||
const Footer = () => {
|
const Footer = () => {
|
||||||
return (
|
return (
|
||||||
<div className="fixed bottom-0 left-0 right-0 bg-white dark:bg-gray-800 border-t dark:border-gray-700">
|
<div className="fixed bottom-0 left-0 right-0 bg-white dark:bg-gray-800 border-t border-gray-200 dark:border-white/10">
|
||||||
<div className="max-w-6xl mx-auto px-4 h-8 flex items-center justify-between">
|
<div className="max-w-6xl mx-auto px-4 h-8 flex items-center justify-between">
|
||||||
<Menu as="div" className="relative">
|
<DropdownMenu>
|
||||||
<MenuButton className="h-7 flex items-center space-x-2 px-1 py-1.5 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors">
|
<DropdownMenuTrigger className="h-7 flex items-center space-x-2 px-1 py-1.5 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors">
|
||||||
<img
|
<img
|
||||||
src={logoImg}
|
src={logoImg}
|
||||||
className="w-5 h-5 text-gray-600 dark:text-gray-400"
|
className="w-5 h-5 text-gray-600 dark:text-gray-400"
|
||||||
@@ -16,7 +19,7 @@ const Footer = () => {
|
|||||||
Coco
|
Coco
|
||||||
</span>
|
</span>
|
||||||
{/* <ChevronUp className="w-4 h-4 text-gray-500 dark:text-gray-400" /> */}
|
{/* <ChevronUp className="w-4 h-4 text-gray-500 dark:text-gray-400" /> */}
|
||||||
</MenuButton>
|
</DropdownMenuTrigger>
|
||||||
|
|
||||||
{/* <MenuItems className="absolute bottom-full mb-2 left-0 w-64 rounded-lg bg-white dark:bg-gray-800 shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none">
|
{/* <MenuItems className="absolute bottom-full mb-2 left-0 w-64 rounded-lg bg-white dark:bg-gray-800 shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none">
|
||||||
<div className="p-1">
|
<div className="p-1">
|
||||||
@@ -27,7 +30,7 @@ const Footer = () => {
|
|||||||
active
|
active
|
||||||
? "bg-gray-100 dark:bg-gray-700"
|
? "bg-gray-100 dark:bg-gray-700"
|
||||||
: "text-gray-900 dark:text-gray-100"
|
: "text-gray-900 dark:text-gray-100"
|
||||||
} group flex w-full items-center rounded-[6px] px-3 py-2 text-sm`}
|
} group flex w-full items-center rounded-md px-3 py-2 text-sm`}
|
||||||
>
|
>
|
||||||
<Home className="w-4 h-4 mr-2" />
|
<Home className="w-4 h-4 mr-2" />
|
||||||
<Link to={`/`}>Home</Link>
|
<Link to={`/`}>Home</Link>
|
||||||
@@ -41,7 +44,7 @@ const Footer = () => {
|
|||||||
active
|
active
|
||||||
? "bg-gray-100 dark:bg-gray-700"
|
? "bg-gray-100 dark:bg-gray-700"
|
||||||
: "text-gray-900 dark:text-gray-100"
|
: "text-gray-900 dark:text-gray-100"
|
||||||
} group flex w-full items-center rounded-[6px] px-3 py-2 text-sm`}
|
} group flex w-full items-center rounded-md px-3 py-2 text-sm`}
|
||||||
>
|
>
|
||||||
<User className="w-4 h-4 mr-2" />
|
<User className="w-4 h-4 mr-2" />
|
||||||
Profile
|
Profile
|
||||||
@@ -55,7 +58,7 @@ const Footer = () => {
|
|||||||
active
|
active
|
||||||
? "bg-gray-100 dark:bg-gray-700"
|
? "bg-gray-100 dark:bg-gray-700"
|
||||||
: "text-gray-900 dark:text-gray-100"
|
: "text-gray-900 dark:text-gray-100"
|
||||||
} group flex w-full items-center rounded-[6px] px-3 py-2 text-sm`}
|
} group flex w-full items-center rounded-md px-3 py-2 text-sm`}
|
||||||
>
|
>
|
||||||
<Settings className="w-4 h-4 mr-2" />
|
<Settings className="w-4 h-4 mr-2" />
|
||||||
<Link to={`settings`}>Settings</Link>
|
<Link to={`settings`}>Settings</Link>
|
||||||
@@ -70,7 +73,7 @@ const Footer = () => {
|
|||||||
active
|
active
|
||||||
? "bg-gray-100 dark:bg-gray-700"
|
? "bg-gray-100 dark:bg-gray-700"
|
||||||
: "text-gray-900 dark:text-gray-100"
|
: "text-gray-900 dark:text-gray-100"
|
||||||
} group flex w-full items-center rounded-[6px] px-3 py-2 text-sm`}
|
} group flex w-full items-center rounded-md px-3 py-2 text-sm`}
|
||||||
>
|
>
|
||||||
<LogOut className="w-4 h-4 mr-2" />
|
<LogOut className="w-4 h-4 mr-2" />
|
||||||
Sign Out
|
Sign Out
|
||||||
@@ -79,7 +82,7 @@ const Footer = () => {
|
|||||||
</MenuItem>
|
</MenuItem>
|
||||||
</div>
|
</div>
|
||||||
</MenuItems> */}
|
</MenuItems> */}
|
||||||
</Menu>
|
</DropdownMenu>
|
||||||
|
|
||||||
<div className="flex items-center space-x-4">
|
<div className="flex items-center space-x-4">
|
||||||
<span className="text-xs text-gray-500 dark:text-gray-400">
|
<span className="text-xs text-gray-500 dark:text-gray-400">
|
||||||
|
|||||||
@@ -111,7 +111,7 @@ const VisibleKey: FC<VisibleKeyProps> = (props) => {
|
|||||||
{showTooltip && visibleShortcut ? (
|
{showTooltip && visibleShortcut ? (
|
||||||
<div
|
<div
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"size-4 flex items-center justify-center font-normal text-xs text-[#333] leading-[14px] bg-[#ccc] dark:bg-[#6B6B6B] rounded-[6px] shadow-[-6px_0px_6px_2px_#fff] dark:shadow-[-6px_0px_6px_2px_#000] absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2",
|
"size-4 flex items-center justify-center font-normal text-xs text-[#333] leading-[14px] bg-[#ccc] dark:bg-[#6B6B6B] rounded-md shadow-[-6px_0px_6px_2px_#fff] dark:shadow-[-6px_0px_6px_2px_#000] absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2",
|
||||||
shortcutClassName
|
shortcutClassName
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ const AiOverview: FC<AiSummaryProps> = (props) => {
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="absolute top-2 right-2 flex items-center justify-center size-[20px] border rounded-[6px] cursor-pointer dark:border-[#282828]"
|
className="absolute top-2 right-2 flex items-center justify-center size-[20px] border rounded-md cursor-pointer dark:border-[#282828]"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setVisible(false);
|
setVisible(false);
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { cn } from "@/lib/utils";
|
||||||
import { useAppStore } from "@/stores/appStore";
|
import { useAppStore } from "@/stores/appStore";
|
||||||
import { useWebConfigStore } from "@/stores/webConfigStore";
|
import { useWebConfigStore } from "@/stores/webConfigStore";
|
||||||
import { useBoolean } from "ahooks";
|
import { useBoolean } from "ahooks";
|
||||||
@@ -37,6 +38,7 @@ const AutoResizeTextarea = forwardRef<
|
|||||||
setInput,
|
setInput,
|
||||||
handleKeyDown,
|
handleKeyDown,
|
||||||
chatPlaceholder,
|
chatPlaceholder,
|
||||||
|
lineCount,
|
||||||
onLineCountChange,
|
onLineCountChange,
|
||||||
firstLineMaxWidth,
|
firstLineMaxWidth,
|
||||||
},
|
},
|
||||||
@@ -79,8 +81,10 @@ const AutoResizeTextarea = forwardRef<
|
|||||||
let height = lineHeight;
|
let height = lineHeight;
|
||||||
let minHeight = lineHeight;
|
let minHeight = lineHeight;
|
||||||
const hasNewline = /[\r\n]/.test(input);
|
const hasNewline = /[\r\n]/.test(input);
|
||||||
|
const hasContent = input.length > 0;
|
||||||
const firstLineExceeds =
|
const firstLineExceeds =
|
||||||
calcRef.current?.offsetWidth >= firstLineMaxWidth - 32;
|
hasContent &&
|
||||||
|
(calcRef.current?.offsetWidth ?? 0) >= Math.max(firstLineMaxWidth - 32, 0);
|
||||||
|
|
||||||
if (hasNewline || firstLineExceeds) {
|
if (hasNewline || firstLineExceeds) {
|
||||||
minHeight = lineHeight * 2;
|
minHeight = lineHeight * 2;
|
||||||
@@ -115,7 +119,12 @@ const AutoResizeTextarea = forwardRef<
|
|||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
autoCapitalize="none"
|
autoCapitalize="none"
|
||||||
spellCheck="false"
|
spellCheck="false"
|
||||||
className="auto-resize-textarea text-base flex-1 outline-none w-full min-w-[200px] text-[#333] dark:text-[#d8d8d8] placeholder-text-xs placeholder-[#999] dark:placeholder-gray-500 bg-transparent custom-scrollbar resize-none overflow-y-auto"
|
className={cn(
|
||||||
|
"auto-resize-textarea text-base flex-1 outline-none w-full min-w-[200px] text-[#333] dark:text-[#d8d8d8] placeholder-text-xs placeholder-[#999] dark:placeholder-gray-500 bg-transparent custom-scrollbar resize-none overflow-y-auto",
|
||||||
|
{
|
||||||
|
"overflow-y-hidden": lineCount === 1,
|
||||||
|
}
|
||||||
|
)}
|
||||||
placeholder={chatPlaceholder || t("search.textarea.placeholder")}
|
placeholder={chatPlaceholder || t("search.textarea.placeholder")}
|
||||||
aria-label={t("search.textarea.ariaLabel")}
|
aria-label={t("search.textarea.ariaLabel")}
|
||||||
value={input}
|
value={input}
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import {
|
|||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { cloneElement, useEffect, useRef, useState } from "react";
|
import { cloneElement, useEffect, useRef, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Input } from "@headlessui/react";
|
|
||||||
|
|
||||||
import { useOSKeyPress } from "@/hooks/useOSKeyPress";
|
import { useOSKeyPress } from "@/hooks/useOSKeyPress";
|
||||||
import { useSearchStore } from "@/stores/searchStore";
|
import { useSearchStore } from "@/stores/searchStore";
|
||||||
@@ -292,9 +291,9 @@ const ContextMenu = ({ formatUrl }: ContextMenuProps) => {
|
|||||||
ref={containerRef}
|
ref={containerRef}
|
||||||
id={visibleContextMenu ? CONTEXT_MENU_PANEL_ID : ""}
|
id={visibleContextMenu ? CONTEXT_MENU_PANEL_ID : ""}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"absolute bottom-[50px] right-[18px] w-[300px] flex flex-col gap-2 scale-0 transition origin-bottom-right text-sm p-3 pb-0 bg-white dark:bg-black rounded-lg shadow-xs border border-[#EDEDED] dark:border-[#272828] shadow-lg dark:shadow-white/15",
|
"absolute bottom-[50px] right-[18px] w-[300px] flex flex-col gap-2 scale-0 transition origin-bottom-right text-sm p-3 pb-0 bg-white dark:bg-black rounded-lg border border-[#EDEDED] dark:border-[#272828] shadow-lg dark:shadow-white/15",
|
||||||
{
|
{
|
||||||
"!scale-100": visibleContextMenu,
|
"scale-100": visibleContextMenu,
|
||||||
}
|
}
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
@@ -329,12 +328,12 @@ const ContextMenu = ({ formatUrl }: ContextMenuProps) => {
|
|||||||
<span style={{ color }}>{name}</span>
|
<span style={{ color }}>{name}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex gap-[4px] text-black/60 dark:text-white/60">
|
<div className="flex gap-1 text-black/60 dark:text-white/60">
|
||||||
{keys.map((key) => (
|
{keys.map((key) => (
|
||||||
<kbd
|
<kbd
|
||||||
key={key}
|
key={key}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"flex justify-center items-center font-sans h-[20px] min-w-[20px] text-[10px] rounded-[6px] border border-[#EDEDED] dark:border-white/10 bg-white dark:bg-[#202126]",
|
"flex justify-center items-center font-sans h-5 min-w-5 text-[10px] rounded-md border border-[#EDEDED] dark:border-white/10 bg-white dark:bg-[#202126]",
|
||||||
{
|
{
|
||||||
"px-1": key.length > 1,
|
"px-1": key.length > 1,
|
||||||
}
|
}
|
||||||
@@ -363,7 +362,7 @@ const ContextMenu = ({ formatUrl }: ContextMenuProps) => {
|
|||||||
searchInputRef.current?.focus();
|
searchInputRef.current?.focus();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Input
|
<input
|
||||||
ref={searchInputRef}
|
ref={searchInputRef}
|
||||||
autoFocus
|
autoFocus
|
||||||
autoCorrect="off"
|
autoCorrect="off"
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ export const DocumentDetail: React.FC<DocumentDetailProps> = ({ document }) => {
|
|||||||
|
|
||||||
{/* Document Summary */}
|
{/* Document Summary */}
|
||||||
{document?.summary && (
|
{document?.summary && (
|
||||||
<div className="mb-4 text-xs text-[rgba(153,153,153,1)] dark:text-[#D8D8D8] whitespace-pre-wrap break-words">
|
<div className="mb-4 text-xs text-[rgba(153,153,153,1)] dark:text-[#D8D8D8] whitespace-pre-wrap wrap-break-word">
|
||||||
{document.summary}
|
{document.summary}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -88,6 +88,9 @@ export const DocumentList: React.FC<DocumentListProps> = ({
|
|||||||
rich_category: sourceData?.rich_categories[0]?.key,
|
rich_category: sourceData?.rich_categories[0]?.key,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
if (sourceData?.main_extension_id) {
|
||||||
|
queryStrings.main_extension_id = sourceData?.main_extension_id
|
||||||
|
}
|
||||||
|
|
||||||
let response: any;
|
let response: any;
|
||||||
if (isTauri) {
|
if (isTauri) {
|
||||||
@@ -178,7 +181,7 @@ export const DocumentList: React.FC<DocumentListProps> = ({
|
|||||||
{
|
{
|
||||||
target: containerRef,
|
target: containerRef,
|
||||||
isNoMore: (d) => !d?.hasMore,
|
isNoMore: (d) => !d?.hasMore,
|
||||||
reloadDeps: [input?.trim(), JSON.stringify(sourceData)],
|
reloadDeps: [input, JSON.stringify(sourceData)],
|
||||||
onFinally: (data) => {
|
onFinally: (data) => {
|
||||||
if (data?.page === 1) return;
|
if (data?.page === 1) return;
|
||||||
if (selectedItem === null) return;
|
if (selectedItem === null) return;
|
||||||
@@ -211,7 +214,7 @@ export const DocumentList: React.FC<DocumentListProps> = ({
|
|||||||
list: [],
|
list: [],
|
||||||
}));
|
}));
|
||||||
loadingFromRef.current = -1;
|
loadingFromRef.current = -1;
|
||||||
}, [input]);
|
}, [input, JSON.stringify(sourceData)]);
|
||||||
|
|
||||||
const { visibleContextMenu } = useSearchStore();
|
const { visibleContextMenu } = useSearchStore();
|
||||||
|
|
||||||
@@ -292,10 +295,10 @@ export const DocumentList: React.FC<DocumentListProps> = ({
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`border-r border-gray-200 dark:border-gray-700 flex flex-col h-full overflow-x-hidden ${
|
className={`border-r border-gray-200 dark:border-gray-700 flex flex-col h-full overflow-x-hidden ${
|
||||||
viewMode === "list" ? "w-[100%]" : "w-[50%]"
|
viewMode === "list" ? "w-full" : "w-[50%]"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<div className="px-2 flex-shrink-0">
|
<div className="px-2 shrink-0">
|
||||||
<SearchHeader
|
<SearchHeader
|
||||||
total={total}
|
total={total}
|
||||||
viewMode={viewMode}
|
viewMode={viewMode}
|
||||||
@@ -306,6 +309,10 @@ export const DocumentList: React.FC<DocumentListProps> = ({
|
|||||||
<Scrollbar className="flex-1 overflow-auto pr-0.5" ref={containerRef}>
|
<Scrollbar className="flex-1 overflow-auto pr-0.5" ref={containerRef}>
|
||||||
{data?.list && data.list.length > 0 && (
|
{data?.list && data.list.length > 0 && (
|
||||||
<div>
|
<div>
|
||||||
|
{(() => {
|
||||||
|
console.log("Rendering list with items:", data.list.length);
|
||||||
|
return null;
|
||||||
|
})()}
|
||||||
{data.list.map((hit, index) => (
|
{data.list.map((hit, index) => (
|
||||||
<SearchListItem
|
<SearchListItem
|
||||||
key={hit.document.id + index}
|
key={hit.document.id + index}
|
||||||
|
|||||||
@@ -46,8 +46,8 @@ const DropdownListItem = memo(
|
|||||||
aria-selected={isSelected}
|
aria-selected={isSelected}
|
||||||
id={`search-item-${currentIndex}`}
|
id={`search-item-${currentIndex}`}
|
||||||
className={clsx("p-2 transition rounded-lg", {
|
className={clsx("p-2 transition rounded-lg", {
|
||||||
"bg-[#EDEDED] dark:bg-[#202126]": isSelected,
|
"bg-muted": isSelected,
|
||||||
"!p-0": isAiOverview,
|
"p-0!": isAiOverview,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{isCalculator && <Calculator item={item} isSelected={isSelected} />}
|
{isCalculator && <Calculator item={item} isSelected={isSelected} />}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Button } from "@headlessui/react";
|
import { Button } from "@/components/ui/button";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import {
|
import {
|
||||||
CircleCheck,
|
CircleCheck,
|
||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
Loader,
|
Loader,
|
||||||
Trash2,
|
Trash2,
|
||||||
User,
|
User,
|
||||||
|
SquareArrowOutUpRight,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { FC, useState } from "react";
|
import { FC, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
@@ -15,15 +16,24 @@ import { useTranslation } from "react-i18next";
|
|||||||
import { useSearchStore } from "@/stores/searchStore";
|
import { useSearchStore } from "@/stores/searchStore";
|
||||||
import DeleteDialog from "../Common/DeleteDialog";
|
import DeleteDialog from "../Common/DeleteDialog";
|
||||||
import PreviewImage from "../Common/PreviewImage";
|
import PreviewImage from "../Common/PreviewImage";
|
||||||
|
import platformAdapter from "@/utils/platformAdapter";
|
||||||
|
|
||||||
interface ExtensionDetailProps {
|
interface ExtensionDetailProps {
|
||||||
onInstall: () => void;
|
onInstall: () => void;
|
||||||
onUninstall: () => void;
|
onUninstall: () => void;
|
||||||
|
changeInput: (value: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ExtensionDetail: FC<ExtensionDetailProps> = (props) => {
|
const ExtensionDetail: FC<ExtensionDetailProps> = (props) => {
|
||||||
const { onInstall, onUninstall } = props;
|
const { onInstall, onUninstall, changeInput } = props;
|
||||||
const { selectedExtension, installingExtensions } = useSearchStore();
|
const {
|
||||||
|
selectedExtension,
|
||||||
|
installingExtensions,
|
||||||
|
setVisibleExtensionStore,
|
||||||
|
setVisibleExtensionDetail,
|
||||||
|
setSourceData,
|
||||||
|
setSearchValue,
|
||||||
|
} = useSearchStore();
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
@@ -37,6 +47,53 @@ const ExtensionDetail: FC<ExtensionDetailProps> = (props) => {
|
|||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const extensionOpen = (item: any) => {
|
||||||
|
setSourceData({
|
||||||
|
source: {
|
||||||
|
name: item.name,
|
||||||
|
icon: item.icon,
|
||||||
|
},
|
||||||
|
querySource: {
|
||||||
|
id: "extensions",
|
||||||
|
},
|
||||||
|
main_extension_id: item.id,
|
||||||
|
});
|
||||||
|
setSearchValue(item.name || "");
|
||||||
|
};
|
||||||
|
|
||||||
|
const otherExtensionOpen = (item: any) => {
|
||||||
|
changeInput("");
|
||||||
|
//
|
||||||
|
const extension = { ...item };
|
||||||
|
|
||||||
|
let developerId = extension.developer.id;
|
||||||
|
let extensionId = extension.id;
|
||||||
|
|
||||||
|
const bundleId = {
|
||||||
|
developer: developerId,
|
||||||
|
extension_id: extensionId,
|
||||||
|
sub_extension_id: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
platformAdapter.invokeBackend("open_third_party_extension", {
|
||||||
|
bundleId,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleOpen = async (item: any) => {
|
||||||
|
if (!item) return;
|
||||||
|
|
||||||
|
// close extension store
|
||||||
|
setVisibleExtensionStore(false);
|
||||||
|
setVisibleExtensionDetail(false);
|
||||||
|
//
|
||||||
|
if (item.type === "group" || item.type === "extension") {
|
||||||
|
extensionOpen(item);
|
||||||
|
} else {
|
||||||
|
otherExtensionOpen(item);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const renderDivider = () => {
|
const renderDivider = () => {
|
||||||
return <div className="my-4 h-px bg-[#E6E6E6] dark:bg-[#262626]"></div>;
|
return <div className="my-4 h-px bg-[#E6E6E6] dark:bg-[#262626]"></div>;
|
||||||
};
|
};
|
||||||
@@ -69,13 +126,19 @@ const ExtensionDetail: FC<ExtensionDetailProps> = (props) => {
|
|||||||
<div className="pt-2">
|
<div className="pt-2">
|
||||||
{selectedExtension.installed ? (
|
{selectedExtension.installed ? (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Trash2
|
<Button
|
||||||
className="size-4 text-red-500 cursor-pointer"
|
className="flex justify-center items-center h-6 px-3 rounded-full bg-[#007BFF] hover:bg-[#007BFF] text-white ring-0 ring-offset-0 focus-visible:ring-0 focus-visible:ring-offset-0 outline-none"
|
||||||
onClick={() => {
|
onClick={() => handleOpen(selectedExtension)}
|
||||||
setIsOpen(true);
|
>
|
||||||
}}
|
<SquareArrowOutUpRight className="size-4" />
|
||||||
/>
|
</Button>
|
||||||
<div className="flex items-center gap-1 h-6 px-2 rounded-full text-[#22C461] bg-[#22C461]/20">
|
<Button
|
||||||
|
className="flex justify-center items-center h-6 px-3 rounded-full bg-[#FFE2E2] hover:bg-[#FFE2E2] text-red-500 ring-0 ring-offset-0 focus-visible:ring-0 focus-visible:ring-offset-0 outline-none"
|
||||||
|
onClick={() => setIsOpen(true)}
|
||||||
|
>
|
||||||
|
<Trash2 className="size-4" />
|
||||||
|
</Button>
|
||||||
|
<div className="flex items-center gap-1 h-6 px-2 rounded-full text-[#999999] bg-[#E6E6E6]">
|
||||||
<CircleCheck className="size-4" />
|
<CircleCheck className="size-4" />
|
||||||
<span>{t("extensionDetail.hints.installed")}</span>
|
<span>{t("extensionDetail.hints.installed")}</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -170,7 +233,8 @@ const ExtensionDetail: FC<ExtensionDetailProps> = (props) => {
|
|||||||
<DeleteDialog
|
<DeleteDialog
|
||||||
reverseButtonPosition
|
reverseButtonPosition
|
||||||
isOpen={isOpen}
|
isOpen={isOpen}
|
||||||
title={`${t("extensionDetail.deleteDialog.title")} ${selectedExtension.name
|
title={`${t("extensionDetail.deleteDialog.title")} ${
|
||||||
|
selectedExtension.name
|
||||||
}`}
|
}`}
|
||||||
description={t("extensionDetail.deleteDialog.description")}
|
description={t("extensionDetail.deleteDialog.description")}
|
||||||
cancelButtonProps={{
|
cancelButtonProps={{
|
||||||
|
|||||||
@@ -73,7 +73,13 @@ export interface SearchExtensionItem {
|
|||||||
}>;
|
}>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ExtensionStore = ({ extensionId }: { extensionId?: string }) => {
|
const ExtensionStore = ({
|
||||||
|
extensionId,
|
||||||
|
changeInput,
|
||||||
|
}: {
|
||||||
|
extensionId?: string;
|
||||||
|
changeInput: (value: string) => void;
|
||||||
|
}) => {
|
||||||
const {
|
const {
|
||||||
searchValue,
|
searchValue,
|
||||||
selectedExtension,
|
selectedExtension,
|
||||||
@@ -244,7 +250,7 @@ const ExtensionStore = ({ extensionId }: { extensionId?: string }) => {
|
|||||||
"info"
|
"info"
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
installExtensionError(String(error));
|
installExtensionError(error);
|
||||||
} finally {
|
} finally {
|
||||||
const { installingExtensions } = useSearchStore.getState();
|
const { installingExtensions } = useSearchStore.getState();
|
||||||
|
|
||||||
@@ -295,6 +301,7 @@ const ExtensionStore = ({ extensionId }: { extensionId?: string }) => {
|
|||||||
<ExtensionDetail
|
<ExtensionDetail
|
||||||
onInstall={handleInstall}
|
onInstall={handleInstall}
|
||||||
onUninstall={handleUnInstall}
|
onUninstall={handleUnInstall}
|
||||||
|
changeInput={changeInput}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
@@ -306,7 +313,7 @@ const ExtensionStore = ({ extensionId }: { extensionId?: string }) => {
|
|||||||
<div
|
<div
|
||||||
key={id}
|
key={id}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"flex justify-between gap-4 h-[40px] px-2 rounded-lg cursor-pointer text-[#333] dark:text-[#d8d8d8] transition",
|
"flex justify-between gap-4 h-10 px-2 rounded-lg cursor-pointer text-[#333] dark:text-[#d8d8d8] transition",
|
||||||
{
|
{
|
||||||
"bg-black/10 dark:bg-white/15":
|
"bg-black/10 dark:bg-white/15":
|
||||||
selectedExtension?.id === id,
|
selectedExtension?.id === id,
|
||||||
|
|||||||
@@ -252,7 +252,7 @@ export default function ChatInput({
|
|||||||
replace: [akiAiTooltipPrefix, askAI.name],
|
replace: [akiAiTooltipPrefix, askAI.name],
|
||||||
})}
|
})}
|
||||||
</span>
|
</span>
|
||||||
<div className="flex items-center justify-center px-1 h-[20px] text-xs rounded-[6px] border border-black/10 dark:border-[#545454]">
|
<div className="flex items-center justify-center px-1 h-5 text-xs rounded-md border border-black/10 dark:border-[#545454]">
|
||||||
{formatKey(modifierKey)} + {formatKey("Enter")}
|
{formatKey(modifierKey)} + {formatKey("Enter")}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -276,8 +276,8 @@ export default function ChatInput({
|
|||||||
return (
|
return (
|
||||||
<VisibleKey
|
<VisibleKey
|
||||||
shortcut={returnToInput}
|
shortcut={returnToInput}
|
||||||
rootClassName="flex-1 flex items-center justify-center"
|
rootClassName="flex-1 flex items-center justify-center w-full"
|
||||||
shortcutClassName="!left-0 !translate-x-0"
|
shortcutClassName="!left-auto !right-2 !translate-x-0"
|
||||||
>
|
>
|
||||||
<AutoResizeTextarea
|
<AutoResizeTextarea
|
||||||
ref={textareaRef}
|
ref={textareaRef}
|
||||||
@@ -308,14 +308,13 @@ export default function ChatInput({
|
|||||||
<div className={`w-full relative`}>
|
<div className={`w-full relative`}>
|
||||||
<div
|
<div
|
||||||
ref={containerRef}
|
ref={containerRef}
|
||||||
className={`flex items-center dark:text-[#D8D8D8] rounded-[6px] transition-all relative overflow-hidden`}
|
className={`flex items-center dark:text-[#D8D8D8] rounded-md transition-all relative overflow-hidden`}
|
||||||
>
|
>
|
||||||
{lineCount === 1 && renderSearchIcon()}
|
{lineCount === 1 && renderSearchIcon()}
|
||||||
|
|
||||||
{visibleSearchBar() && (
|
{visibleSearchBar() && (
|
||||||
<div
|
<div
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"relative w-full p-2 bg-[#ededed] dark:bg-[#202126]",
|
"min-h-10 w-full p-[7px] bg-[#ededed] dark:bg-[#202126]",
|
||||||
{
|
{
|
||||||
"flex items-center gap-2": lineCount === 1,
|
"flex items-center gap-2": lineCount === 1,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -187,9 +187,9 @@ const InputControls = ({
|
|||||||
{source?.type === "deep_think" && source?.config?.visible && (
|
{source?.type === "deep_think" && source?.config?.visible && (
|
||||||
<button
|
<button
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"flex items-center justify-center gap-1 h-[20px] px-1 rounded-[6px] transition hover:bg-[#EDEDED] dark:hover:bg-[#202126]",
|
"flex items-center justify-center gap-1 h-5 px-1 rounded-md transition hover:bg-[#EDEDED] dark:hover:bg-[#202126] cursor-pointer",
|
||||||
{
|
{
|
||||||
"!bg-[rgba(0,114,255,0.3)]": isDeepThinkActive,
|
"bg-[rgba(0,114,255,0.3)]!": isDeepThinkActive,
|
||||||
}
|
}
|
||||||
)}
|
)}
|
||||||
onClick={setIsDeepThinkActive}
|
onClick={setIsDeepThinkActive}
|
||||||
@@ -250,7 +250,7 @@ const InputControls = ({
|
|||||||
!visibleExtensionStore && (
|
!visibleExtensionStore && (
|
||||||
<div
|
<div
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"inline-flex items-center gap-1 h-[20px] px-1 rounded-full hover:!text-[#881c94] cursor-pointer transition",
|
"inline-flex items-center gap-1 h-5 px-1 rounded-full hover:text-[#881c94]! cursor-pointer transition",
|
||||||
[
|
[
|
||||||
enabledAiOverview
|
enabledAiOverview
|
||||||
? "text-[#881c94]"
|
? "text-[#881c94]"
|
||||||
|
|||||||
@@ -2,14 +2,16 @@ import { FC, Fragment, MouseEvent, useEffect, useRef } from "react";
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { ChevronRight, Plus } from "lucide-react";
|
import { ChevronRight, Plus } from "lucide-react";
|
||||||
import {
|
import {
|
||||||
Menu,
|
DropdownMenu,
|
||||||
MenuButton,
|
DropdownMenuTrigger,
|
||||||
MenuItem,
|
DropdownMenuContent,
|
||||||
MenuItems,
|
DropdownMenuItem,
|
||||||
|
} from "@/components/ui/dropdown-menu";
|
||||||
|
import {
|
||||||
Popover,
|
Popover,
|
||||||
PopoverButton,
|
PopoverTrigger,
|
||||||
PopoverPanel,
|
PopoverContent,
|
||||||
} from "@headlessui/react";
|
} from "@/components/ui/popover";
|
||||||
import { castArray, find, isNil } from "lodash-es";
|
import { castArray, find, isNil } from "lodash-es";
|
||||||
import { nanoid } from "nanoid";
|
import { nanoid } from "nanoid";
|
||||||
import { useCreation, useMount, useReactive } from "ahooks";
|
import { useCreation, useMount, useReactive } from "ahooks";
|
||||||
@@ -198,8 +200,8 @@ const InputUpload: FC<InputUploadProps> = (props) => {
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Menu>
|
<DropdownMenu>
|
||||||
<MenuButton className="flex items-center justify-center h-[20px] px-1 rounded-[6px] transition hover:bg-[#EDEDED] dark:hover:bg-[#202126]">
|
<DropdownMenuTrigger className="flex items-center justify-center h-[20px] px-1 rounded-md transition hover:bg-[#EDEDED] dark:hover:bg-[#202126]">
|
||||||
<Tooltip
|
<Tooltip
|
||||||
content={t("search.input.uploadFileHints.tooltip", {
|
content={t("search.input.uploadFileHints.tooltip", {
|
||||||
replace: [
|
replace: [
|
||||||
@@ -212,32 +214,41 @@ const InputUpload: FC<InputUploadProps> = (props) => {
|
|||||||
<Plus className="size-3 scale-[1.3]" />
|
<Plus className="size-3 scale-[1.3]" />
|
||||||
</VisibleKey>
|
</VisibleKey>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</MenuButton>
|
</DropdownMenuTrigger>
|
||||||
|
|
||||||
<MenuItems
|
<DropdownMenuContent
|
||||||
anchor="bottom start"
|
side="bottom"
|
||||||
className="p-1 text-sm bg-white dark:bg-[#202126] rounded-lg shadow-xs border border-gray-200 dark:border-gray-700"
|
align="start"
|
||||||
|
className="p-1 text-sm rounded-lg"
|
||||||
>
|
>
|
||||||
{menuItems.map((item) => {
|
{menuItems.map((item) => {
|
||||||
const { label, children, clickEvent } = item;
|
const { label, children, clickEvent } = item;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MenuItem key={label}>
|
<DropdownMenuItem
|
||||||
|
key={label}
|
||||||
|
onSelect={(e: Event) => {
|
||||||
|
if (children) e.preventDefault();
|
||||||
|
}}
|
||||||
|
className="px-0 py-0"
|
||||||
|
>
|
||||||
{children ? (
|
{children ? (
|
||||||
<Popover>
|
<Popover>
|
||||||
<PopoverButton
|
<PopoverTrigger asChild>
|
||||||
className="flex items-center justify-between gap-2 px-3 py-2 hover:bg-black/5 hover:dark:bg-white/5 rounded-lg cursor-pointer"
|
<div
|
||||||
|
className="flex items-center justify-between gap-2 px-3 py-2 rounded-lg hover:bg-muted cursor-pointer"
|
||||||
onClick={clickEvent}
|
onClick={clickEvent}
|
||||||
>
|
>
|
||||||
<span>{label}</span>
|
<span>{label}</span>
|
||||||
|
|
||||||
<ChevronRight className="size-4" />
|
<ChevronRight className="size-4" />
|
||||||
</PopoverButton>
|
</div>
|
||||||
|
</PopoverTrigger>
|
||||||
|
|
||||||
<PopoverPanel
|
<PopoverContent
|
||||||
transition
|
side="right"
|
||||||
anchor="right"
|
align="start"
|
||||||
className="p-1 text-sm bg-white dark:bg-[#202126] rounded-lg shadow-xs border border-gray-200 dark:border-gray-700"
|
className="p-1 text-sm rounded-lg"
|
||||||
>
|
>
|
||||||
{children.map((childItem) => {
|
{children.map((childItem) => {
|
||||||
const { groupName, groupItems } = childItem;
|
const { groupName, groupItems } = childItem;
|
||||||
@@ -259,7 +270,7 @@ const InputUpload: FC<InputUploadProps> = (props) => {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={id}
|
key={id}
|
||||||
className="px-3 py-2 hover:bg-black/5 hover:dark:bg-white/5 rounded-lg cursor-pointer"
|
className="px-3 py-2 rounded-lg hover:bg-muted cursor-pointer"
|
||||||
onClick={clickEvent}
|
onClick={clickEvent}
|
||||||
>
|
>
|
||||||
{label}
|
{label}
|
||||||
@@ -269,21 +280,21 @@ const InputUpload: FC<InputUploadProps> = (props) => {
|
|||||||
</Fragment>
|
</Fragment>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</PopoverPanel>
|
</PopoverContent>
|
||||||
</Popover>
|
</Popover>
|
||||||
) : (
|
) : (
|
||||||
<div
|
<div
|
||||||
className="px-3 py-2 hover:bg-black/5 hover:dark:bg-white/5 rounded-lg cursor-pointer"
|
className="px-3 py-2 rounded-lg hover:bg-muted cursor-pointer"
|
||||||
onClick={clickEvent}
|
onClick={clickEvent}
|
||||||
>
|
>
|
||||||
{label}
|
{label}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</MenuItem>
|
</DropdownMenuItem>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</MenuItems>
|
</DropdownMenuContent>
|
||||||
</Menu>
|
</DropdownMenu>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,14 @@
|
|||||||
import { useState, useEffect, useCallback, useRef } from "react";
|
import { useState, useEffect, useCallback, useRef } from "react";
|
||||||
import { Popover, PopoverButton, PopoverPanel } from "@headlessui/react";
|
|
||||||
import { ChevronDownIcon, RefreshCw, Layers, Hammer } from "lucide-react";
|
import { ChevronDownIcon, RefreshCw, Layers, Hammer } from "lucide-react";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useDebounce } from "ahooks";
|
import { useDebounce } from "ahooks";
|
||||||
|
|
||||||
|
import {
|
||||||
|
Popover,
|
||||||
|
PopoverTrigger,
|
||||||
|
PopoverContent,
|
||||||
|
} from "@/components/ui/popover";
|
||||||
import CommonIcon from "@/components/Common/Icons/CommonIcon";
|
import CommonIcon from "@/components/Common/Icons/CommonIcon";
|
||||||
import { useConnectStore } from "@/stores/connectStore";
|
import { useConnectStore } from "@/stores/connectStore";
|
||||||
import { useSearchStore } from "@/stores/searchStore";
|
import { useSearchStore } from "@/stores/searchStore";
|
||||||
@@ -16,6 +20,7 @@ import NoDataImage from "@/components/Common/NoDataImage";
|
|||||||
import PopoverInput from "@/components/Common/PopoverInput";
|
import PopoverInput from "@/components/Common/PopoverInput";
|
||||||
import Pagination from "@/components/Common/Pagination";
|
import Pagination from "@/components/Common/Pagination";
|
||||||
import { SearchQuery } from "@/utils";
|
import { SearchQuery } from "@/utils";
|
||||||
|
import { Button } from "../ui/button";
|
||||||
|
|
||||||
interface MCPPopoverProps {
|
interface MCPPopoverProps {
|
||||||
mcp_servers: any;
|
mcp_servers: any;
|
||||||
@@ -79,6 +84,7 @@ export default function MCPPopover({
|
|||||||
}, [currentService?.id, debouncedKeyword, getMCPByServer]);
|
}, [currentService?.id, debouncedKeyword, getMCPByServer]);
|
||||||
|
|
||||||
const popoverButtonRef = useRef<HTMLButtonElement>(null);
|
const popoverButtonRef = useRef<HTMLButtonElement>(null);
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
const mcpSearch = useShortcutsStore((state) => state.mcpSearch);
|
const mcpSearch = useShortcutsStore((state) => state.mcpSearch);
|
||||||
const mcpSearchScope = useShortcutsStore((state) => {
|
const mcpSearchScope = useShortcutsStore((state) => {
|
||||||
return state.mcpSearchScope;
|
return state.mcpSearchScope;
|
||||||
@@ -166,9 +172,9 @@ export default function MCPPopover({
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"flex justify-center items-center gap-1 h-[20px] px-1 rounded-[6px] transition hover:bg-[#EDEDED] dark:hover:bg-[#202126] cursor-pointer",
|
"flex justify-center items-center gap-1 h-5 px-1 rounded-md transition cursor-pointer hover:bg-[#EDEDED] dark:hover:bg-[#202126]",
|
||||||
{
|
{
|
||||||
"!bg-[rgba(0,114,255,0.3)]": isMCPActive,
|
"bg-[rgba(0,114,255,0.3)]!": isMCPActive,
|
||||||
}
|
}
|
||||||
)}
|
)}
|
||||||
onClick={setIsMCPActive}
|
onClick={setIsMCPActive}
|
||||||
@@ -191,8 +197,14 @@ export default function MCPPopover({
|
|||||||
{t("search.input.MCP")}
|
{t("search.input.MCP")}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<Popover className="relative">
|
<Popover open={open} onOpenChange={setOpen}>
|
||||||
<PopoverButton ref={popoverButtonRef} className="flex items-center">
|
<PopoverTrigger
|
||||||
|
ref={popoverButtonRef}
|
||||||
|
className="flex items-center"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
}}
|
||||||
|
>
|
||||||
<VisibleKey
|
<VisibleKey
|
||||||
shortcut={mcpSearchScope}
|
shortcut={mcpSearchScope}
|
||||||
onKeyPress={() => {
|
onKeyPress={() => {
|
||||||
@@ -200,29 +212,35 @@ export default function MCPPopover({
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ChevronDownIcon
|
<ChevronDownIcon
|
||||||
className={clsx("size-3", [
|
className={clsx("size-3 cursor-pointer", [
|
||||||
isMCPActive
|
isMCPActive
|
||||||
? "text-[#0072FF] dark:text-[#0072FF]"
|
? "text-[#0072FF] dark:text-[#0072FF]"
|
||||||
: "text-[#333] dark:text-white",
|
: "text-[#333] dark:text-white",
|
||||||
])}
|
])}
|
||||||
/>
|
/>
|
||||||
</VisibleKey>
|
</VisibleKey>
|
||||||
</PopoverButton>
|
</PopoverTrigger>
|
||||||
|
|
||||||
<PopoverPanel className="absolute z-50 left-0 bottom-6 w-[240px] overflow-y-auto bg-white dark:bg-[#202126] rounded-lg shadow-lg border border-gray-200 dark:border-gray-700">
|
<PopoverContent
|
||||||
|
side="top"
|
||||||
|
align="start"
|
||||||
|
className="z-50 w-60 overflow-y-auto rounded-lg shadow-lg p-0"
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
className="text-sm"
|
className="text-sm"
|
||||||
onClick={(e) => {
|
onClick={(e: React.MouseEvent<HTMLDivElement>) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="p-3">
|
<div className="p-2">
|
||||||
<div className="flex justify-between">
|
<div className="flex justify-between">
|
||||||
<span>{t("search.input.searchPopover.title")}</span>
|
<span>{t("search.input.searchPopover.title")}</span>
|
||||||
|
|
||||||
<div
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="icon"
|
||||||
|
className="size-6"
|
||||||
onClick={handleRefresh}
|
onClick={handleRefresh}
|
||||||
className="size-[24px] flex justify-center items-center rounded-lg border border-black/10 dark:border-white/10 cursor-pointer"
|
|
||||||
>
|
>
|
||||||
<VisibleKey shortcut="R" onKeyPress={handleRefresh}>
|
<VisibleKey shortcut="R" onKeyPress={handleRefresh}>
|
||||||
<RefreshCw
|
<RefreshCw
|
||||||
@@ -231,7 +249,7 @@ export default function MCPPopover({
|
|||||||
}`}
|
}`}
|
||||||
/>
|
/>
|
||||||
</VisibleKey>
|
</VisibleKey>
|
||||||
</div>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="relative h-8 my-2">
|
<div className="relative h-8 my-2">
|
||||||
@@ -250,7 +268,7 @@ export default function MCPPopover({
|
|||||||
value={keyword}
|
value={keyword}
|
||||||
ref={searchInputRef}
|
ref={searchInputRef}
|
||||||
className="size-full px-2 rounded-lg border dark:border-white/10 bg-transparent"
|
className="size-full px-2 rounded-lg border dark:border-white/10 bg-transparent"
|
||||||
onChange={(e) => {
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
setKeyword(e.target.value);
|
setKeyword(e.target.value);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@@ -280,7 +298,7 @@ export default function MCPPopover({
|
|||||||
>
|
>
|
||||||
<div className="flex items-center gap-2 overflow-hidden">
|
<div className="flex items-center gap-2 overflow-hidden">
|
||||||
{isAll ? (
|
{isAll ? (
|
||||||
<Layers className="size-[16px] text-[#0287FF]" />
|
<Layers className="min-w-4 min-h-4 size-4 text-[#0287FF]" />
|
||||||
) : (
|
) : (
|
||||||
<CommonIcon
|
<CommonIcon
|
||||||
item={item}
|
item={item}
|
||||||
@@ -290,7 +308,7 @@ export default function MCPPopover({
|
|||||||
"default_icon",
|
"default_icon",
|
||||||
]}
|
]}
|
||||||
itemIcon={item.icon}
|
itemIcon={item.icon}
|
||||||
className="size-4"
|
className="min-w-4 min-h-4 size-4"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -308,7 +326,7 @@ export default function MCPPopover({
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="flex justify-center items-center size-[24px]">
|
<div className="flex justify-center items-center size-6">
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={isChecked()}
|
checked={isChecked()}
|
||||||
indeterminate={isAll}
|
indeterminate={isAll}
|
||||||
@@ -339,7 +357,7 @@ export default function MCPPopover({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</PopoverPanel>
|
</PopoverContent>
|
||||||
</Popover>
|
</Popover>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { useEffect, memo, useRef, useCallback, useState } from "react";
|
import { useEffect, memo, useRef, useCallback, useState } from "react";
|
||||||
|
import clsx from "clsx";
|
||||||
|
|
||||||
import DropdownList from "./DropdownList";
|
import DropdownList from "./DropdownList";
|
||||||
import { SearchResults } from "@/components/Search/SearchResults";
|
import { SearchResults } from "@/components/Search/SearchResults";
|
||||||
@@ -12,7 +13,6 @@ import ExtensionStore from "./ExtensionStore";
|
|||||||
import platformAdapter from "@/utils/platformAdapter";
|
import platformAdapter from "@/utils/platformAdapter";
|
||||||
import ViewExtension from "./ViewExtension";
|
import ViewExtension from "./ViewExtension";
|
||||||
import { visibleFooterBar } from "@/utils";
|
import { visibleFooterBar } from "@/utils";
|
||||||
import clsx from "clsx";
|
|
||||||
|
|
||||||
const SearchResultsPanel = memo<{
|
const SearchResultsPanel = memo<{
|
||||||
input: string;
|
input: string;
|
||||||
@@ -124,7 +124,9 @@ const SearchResultsPanel = memo<{
|
|||||||
|
|
||||||
// If state gets updated, render the UI
|
// If state gets updated, render the UI
|
||||||
if (visibleExtensionStore) {
|
if (visibleExtensionStore) {
|
||||||
return <ExtensionStore extensionId={extensionId} />;
|
return (
|
||||||
|
<ExtensionStore extensionId={extensionId} changeInput={changeInput} />
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Render the view extension
|
// Render the view extension
|
||||||
@@ -133,11 +135,14 @@ const SearchResultsPanel = memo<{
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (goAskAi) return <AskAi isChatMode={isChatMode} />;
|
if (goAskAi) return <AskAi isChatMode={isChatMode} />;
|
||||||
|
|
||||||
|
if (sourceData) {
|
||||||
|
return <SearchResults input={input} isChatMode={isChatMode} />;
|
||||||
|
}
|
||||||
|
|
||||||
if (suggests.length === 0) return <NoResults />;
|
if (suggests.length === 0) return <NoResults />;
|
||||||
|
|
||||||
return sourceData ? (
|
return (
|
||||||
<SearchResults input={input} isChatMode={isChatMode} />
|
|
||||||
) : (
|
|
||||||
<DropdownList
|
<DropdownList
|
||||||
suggests={suggests}
|
suggests={suggests}
|
||||||
searchData={searchData}
|
searchData={searchData}
|
||||||
|
|||||||
@@ -1,15 +1,16 @@
|
|||||||
import { useSearchStore } from "@/stores/searchStore";
|
import { useSearchStore } from "@/stores/searchStore";
|
||||||
import { ChevronLeft, Search } from "lucide-react";
|
import { ChevronLeft, Search } from "lucide-react";
|
||||||
|
import { FC } from "react";
|
||||||
|
import clsx from "clsx";
|
||||||
|
|
||||||
import FontIcon from "@/components/Common/Icons/FontIcon";
|
import FontIcon from "@/components/Common/Icons/FontIcon";
|
||||||
import { FC } from "react";
|
|
||||||
import lightDefaultIcon from "@/assets/images/source_default.png";
|
import lightDefaultIcon from "@/assets/images/source_default.png";
|
||||||
import darkDefaultIcon from "@/assets/images/source_default_dark.png";
|
import darkDefaultIcon from "@/assets/images/source_default_dark.png";
|
||||||
import { useThemeStore } from "@/stores/themeStore";
|
import { useThemeStore } from "@/stores/themeStore";
|
||||||
import platformAdapter from "@/utils/platformAdapter";
|
import platformAdapter from "@/utils/platformAdapter";
|
||||||
import { navigateBack, visibleSearchBar } from "@/utils";
|
import { navigateBack, visibleSearchBar } from "@/utils";
|
||||||
import VisibleKey from "../Common/VisibleKey";
|
import VisibleKey from "../Common/VisibleKey";
|
||||||
import clsx from "clsx";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
interface MultilevelWrapperProps {
|
interface MultilevelWrapperProps {
|
||||||
title?: string;
|
title?: string;
|
||||||
@@ -36,7 +37,7 @@ const MultilevelWrapper: FC<MultilevelWrapperProps> = (props) => {
|
|||||||
<div
|
<div
|
||||||
data-tauri-drag-region
|
data-tauri-drag-region
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"flex items-center h-[40px] gap-1 px-2 border border-[#EDEDED] dark:border-[#202126] rounded-l-lg",
|
"flex items-center h-10 gap-1 px-2 border border-(--border) rounded-l-lg",
|
||||||
{
|
{
|
||||||
"justify-center": visibleSearchBar(),
|
"justify-center": visibleSearchBar(),
|
||||||
"w-[calc(100vw-16px)] rounded-r-lg": !visibleSearchBar(),
|
"w-[calc(100vw-16px)] rounded-r-lg": !visibleSearchBar(),
|
||||||
@@ -50,7 +51,7 @@ const MultilevelWrapper: FC<MultilevelWrapperProps> = (props) => {
|
|||||||
/>
|
/>
|
||||||
</VisibleKey>
|
</VisibleKey>
|
||||||
|
|
||||||
<div className="size-5 [&>*]:size-full">{renderIcon()}</div>
|
<div className="size-5 *:size-full">{renderIcon()}</div>
|
||||||
|
|
||||||
<span className="text-sm whitespace-nowrap">{title}</span>
|
<span className="text-sm whitespace-nowrap">{title}</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -115,7 +116,14 @@ export default function SearchIcons({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-center pl-2 h-[40px] bg-[#ededed] dark:bg-[#202126]">
|
<div
|
||||||
|
className={cn(
|
||||||
|
"flex items-center justify-center bg-[#ededed] dark:bg-[#202126]",
|
||||||
|
{
|
||||||
|
"pl-2 h-10": lineCount === 1,
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
>
|
||||||
<Search className="w-4 h-4 text-[#ccc] dark:text-[#d8d8d8]" />
|
<Search className="w-4 h-4 text-[#ccc] dark:text-[#d8d8d8]" />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,10 +1,14 @@
|
|||||||
import { useState, useEffect, useCallback, useRef } from "react";
|
import { useState, useEffect, useCallback, useRef } from "react";
|
||||||
import { Popover, PopoverButton, PopoverPanel } from "@headlessui/react";
|
|
||||||
import { ChevronDownIcon, RefreshCw, Layers, Globe } from "lucide-react";
|
import { ChevronDownIcon, RefreshCw, Layers, Globe } from "lucide-react";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useDebounce } from "ahooks";
|
import { useDebounce } from "ahooks";
|
||||||
|
|
||||||
|
import {
|
||||||
|
Popover,
|
||||||
|
PopoverTrigger,
|
||||||
|
PopoverContent,
|
||||||
|
} from "@/components/ui/popover";
|
||||||
import CommonIcon from "@/components/Common/Icons/CommonIcon";
|
import CommonIcon from "@/components/Common/Icons/CommonIcon";
|
||||||
import { useConnectStore } from "@/stores/connectStore";
|
import { useConnectStore } from "@/stores/connectStore";
|
||||||
import { useSearchStore } from "@/stores/searchStore";
|
import { useSearchStore } from "@/stores/searchStore";
|
||||||
@@ -15,6 +19,7 @@ import VisibleKey from "@/components/Common/VisibleKey";
|
|||||||
import NoDataImage from "@/components/Common/NoDataImage";
|
import NoDataImage from "@/components/Common/NoDataImage";
|
||||||
import PopoverInput from "@/components/Common/PopoverInput";
|
import PopoverInput from "@/components/Common/PopoverInput";
|
||||||
import Pagination from "@/components/Common/Pagination";
|
import Pagination from "@/components/Common/Pagination";
|
||||||
|
import { Button } from "../ui/button";
|
||||||
|
|
||||||
interface SearchPopoverProps {
|
interface SearchPopoverProps {
|
||||||
datasource: any;
|
datasource: any;
|
||||||
@@ -85,6 +90,7 @@ export default function SearchPopover({
|
|||||||
}, [currentService?.id, debouncedKeyword, getDataSourcesByServer]);
|
}, [currentService?.id, debouncedKeyword, getDataSourcesByServer]);
|
||||||
|
|
||||||
const popoverButtonRef = useRef<HTMLButtonElement>(null);
|
const popoverButtonRef = useRef<HTMLButtonElement>(null);
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
const internetSearch = useShortcutsStore((state) => state.internetSearch);
|
const internetSearch = useShortcutsStore((state) => state.internetSearch);
|
||||||
const internetSearchScope = useShortcutsStore((state) => {
|
const internetSearchScope = useShortcutsStore((state) => {
|
||||||
return state.internetSearchScope;
|
return state.internetSearchScope;
|
||||||
@@ -172,9 +178,9 @@ export default function SearchPopover({
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"flex justify-center items-center gap-1 h-[20px] px-1 rounded-[6px] transition hover:bg-[#EDEDED] dark:hover:bg-[#202126] cursor-pointer",
|
"flex justify-center items-center gap-1 h-5 px-1 rounded-md transition cursor-pointer hover:bg-[#EDEDED] dark:hover:bg-[#202126]",
|
||||||
{
|
{
|
||||||
"!bg-[rgba(0,114,255,0.3)]": isSearchActive,
|
"bg-[rgba(0,114,255,0.3)]!": isSearchActive,
|
||||||
}
|
}
|
||||||
)}
|
)}
|
||||||
onClick={setIsSearchActive}
|
onClick={setIsSearchActive}
|
||||||
@@ -199,8 +205,14 @@ export default function SearchPopover({
|
|||||||
{t("search.input.search")}
|
{t("search.input.search")}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<Popover className="relative">
|
<Popover open={open} onOpenChange={setOpen}>
|
||||||
<PopoverButton ref={popoverButtonRef} className="flex items-center">
|
<PopoverTrigger
|
||||||
|
ref={popoverButtonRef}
|
||||||
|
className="flex items-center"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
}}
|
||||||
|
>
|
||||||
<VisibleKey
|
<VisibleKey
|
||||||
shortcut={internetSearchScope}
|
shortcut={internetSearchScope}
|
||||||
onKeyPress={() => {
|
onKeyPress={() => {
|
||||||
@@ -208,29 +220,35 @@ export default function SearchPopover({
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ChevronDownIcon
|
<ChevronDownIcon
|
||||||
className={clsx("size-3", [
|
className={clsx("size-3 cursor-pointer", [
|
||||||
isSearchActive
|
isSearchActive
|
||||||
? "text-[#0072FF] dark:text-[#0072FF]"
|
? "text-[#0072FF] dark:text-[#0072FF]"
|
||||||
: "text-[#333] dark:text-white",
|
: "text-[#333] dark:text-white",
|
||||||
])}
|
])}
|
||||||
/>
|
/>
|
||||||
</VisibleKey>
|
</VisibleKey>
|
||||||
</PopoverButton>
|
</PopoverTrigger>
|
||||||
|
|
||||||
<PopoverPanel className="absolute z-50 left-0 bottom-6 w-[240px] overflow-y-auto bg-white dark:bg-[#202126] rounded-lg shadow-lg border border-gray-200 dark:border-gray-700">
|
<PopoverContent
|
||||||
|
side="top"
|
||||||
|
align="start"
|
||||||
|
className="z-50 w-60 overflow-y-auto rounded-lg shadow-lg p-0"
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
className="text-sm"
|
className="text-sm"
|
||||||
onClick={(e) => {
|
onClick={(e: React.MouseEvent<HTMLDivElement>) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="p-3">
|
<div className="p-2">
|
||||||
<div className="flex justify-between">
|
<div className="flex justify-between">
|
||||||
<span>{t("search.input.searchPopover.title")}</span>
|
<span>{t("search.input.searchPopover.title")}</span>
|
||||||
|
|
||||||
<div
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="icon"
|
||||||
|
className="size-6"
|
||||||
onClick={handleRefresh}
|
onClick={handleRefresh}
|
||||||
className="size-[24px] flex justify-center items-center rounded-lg border border-black/10 dark:border-white/10 cursor-pointer"
|
|
||||||
>
|
>
|
||||||
<VisibleKey shortcut="R" onKeyPress={handleRefresh}>
|
<VisibleKey shortcut="R" onKeyPress={handleRefresh}>
|
||||||
<RefreshCw
|
<RefreshCw
|
||||||
@@ -239,7 +257,7 @@ export default function SearchPopover({
|
|||||||
}`}
|
}`}
|
||||||
/>
|
/>
|
||||||
</VisibleKey>
|
</VisibleKey>
|
||||||
</div>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="relative h-8 my-2">
|
<div className="relative h-8 my-2">
|
||||||
@@ -258,7 +276,7 @@ export default function SearchPopover({
|
|||||||
value={keyword}
|
value={keyword}
|
||||||
ref={searchInputRef}
|
ref={searchInputRef}
|
||||||
className="size-full px-2 rounded-lg border dark:border-white/10 bg-transparent"
|
className="size-full px-2 rounded-lg border dark:border-white/10 bg-transparent"
|
||||||
onChange={(e) => {
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
setKeyword(e.target.value);
|
setKeyword(e.target.value);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@@ -288,7 +306,7 @@ export default function SearchPopover({
|
|||||||
>
|
>
|
||||||
<div className="flex items-center gap-2 overflow-hidden">
|
<div className="flex items-center gap-2 overflow-hidden">
|
||||||
{isAll ? (
|
{isAll ? (
|
||||||
<Layers className="size-[16px] text-[#0287FF]" />
|
<Layers className="size-4 text-[#0287FF]" />
|
||||||
) : (
|
) : (
|
||||||
<CommonIcon
|
<CommonIcon
|
||||||
item={item}
|
item={item}
|
||||||
@@ -316,7 +334,7 @@ export default function SearchPopover({
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="flex justify-center items-center size-[24px]">
|
<div className="flex justify-center items-center size-6">
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={isChecked()}
|
checked={isChecked()}
|
||||||
indeterminate={isAll}
|
indeterminate={isAll}
|
||||||
@@ -347,7 +365,7 @@ export default function SearchPopover({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</PopoverPanel>
|
</PopoverContent>
|
||||||
</Popover>
|
</Popover>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -35,7 +35,10 @@ import {
|
|||||||
visibleSearchBar,
|
visibleSearchBar,
|
||||||
} from "@/utils";
|
} from "@/utils";
|
||||||
import { useTauriFocus } from "@/hooks/useTauriFocus";
|
import { useTauriFocus } from "@/hooks/useTauriFocus";
|
||||||
import { POPOVER_PANEL_SELECTOR, WINDOW_CENTER_BASELINE_HEIGHT } from "@/constants";
|
import {
|
||||||
|
POPOVER_PANEL_SELECTOR,
|
||||||
|
WINDOW_CENTER_BASELINE_HEIGHT,
|
||||||
|
} from "@/constants";
|
||||||
import { useChatStore } from "@/stores/chatStore";
|
import { useChatStore } from "@/stores/chatStore";
|
||||||
import { useSearchStore } from "@/stores/searchStore";
|
import { useSearchStore } from "@/stores/searchStore";
|
||||||
|
|
||||||
@@ -383,11 +386,11 @@ function SearchChat({
|
|||||||
<div
|
<div
|
||||||
data-tauri-drag-region={isTauri}
|
data-tauri-drag-region={isTauri}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"m-auto overflow-hidden relative bg-no-repeat bg-white dark:bg-black flex flex-col",
|
"m-auto overflow-hidden relative bg-no-repeat flex flex-col",
|
||||||
[
|
[
|
||||||
isTransitioned
|
isTransitioned
|
||||||
? "bg-bottom bg-chat_bg_light dark:bg-chat_bg_dark"
|
? "bg-bottom bg-[url('/assets/chat_bg_light.png')] dark:bg-[url('/assets/chat_bg_dark.png')]"
|
||||||
: "bg-top bg-search_bg_light dark:bg-search_bg_dark",
|
: "bg-top bg-[url('/assets/search_bg_light.png')] dark:bg-[url('/assets/search_bg_dark.png')]",
|
||||||
],
|
],
|
||||||
{
|
{
|
||||||
"size-full": !isTauri,
|
"size-full": !isTauri,
|
||||||
@@ -438,7 +441,7 @@ function SearchChat({
|
|||||||
{!hideMiddleBorder && (
|
{!hideMiddleBorder && (
|
||||||
<div
|
<div
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"pointer-events-none absolute left-0 right-0 h-[1px] bg-[#E6E6E6] dark:bg-[#272626]",
|
"pointer-events-none absolute left-0 right-0 h-px bg-[#E6E6E6] dark:bg-[#272626]",
|
||||||
isTransitioned ? "top-0" : "bottom-0"
|
isTransitioned ? "top-0" : "bottom-0"
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -6,6 +6,13 @@ import { nanoid } from "nanoid";
|
|||||||
|
|
||||||
import { AssistantFetcher } from "@/components/Assistant/AssistantFetcher";
|
import { AssistantFetcher } from "@/components/Assistant/AssistantFetcher";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select";
|
||||||
import { ButtonConfig } from "./config";
|
import { ButtonConfig } from "./config";
|
||||||
import { useThemeStore } from "@/stores/themeStore";
|
import { useThemeStore } from "@/stores/themeStore";
|
||||||
import { useAppStore } from "@/stores/appStore";
|
import { useAppStore } from "@/stores/appStore";
|
||||||
@@ -169,43 +176,58 @@ export default function AddChatDialog({
|
|||||||
<label className="text-sm font-medium text-muted-foreground">
|
<label className="text-sm font-medium text-muted-foreground">
|
||||||
{t("selection.bind.service")}
|
{t("selection.bind.service")}
|
||||||
</label>
|
</label>
|
||||||
<select
|
<Select
|
||||||
className="h-8 rounded-md px-2 py-1 text-sm bg-white dark:bg-[#0B1220] w-full"
|
|
||||||
value={serverId}
|
value={serverId}
|
||||||
onChange={(e) => setServerId(e.target.value)}
|
onValueChange={(v) => setServerId(v === "__default__" ? "" : v)}
|
||||||
>
|
>
|
||||||
<option value="" disabled>
|
<SelectTrigger className="h-8 w-full">
|
||||||
|
<SelectValue className="truncate" placeholder={t("selection.bind.defaultService") as string} />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="__default__" disabled>
|
||||||
{t("selection.bind.defaultService")}
|
{t("selection.bind.defaultService")}
|
||||||
</option>
|
</SelectItem>
|
||||||
{serverList.map((s: any) => (
|
{serverList.map((s: any) => (
|
||||||
<option key={s.id} value={s.id}>
|
<SelectItem key={s.id} value={s.id}>
|
||||||
{s.name || s.endpoint || s.id}
|
{s.name || s.endpoint || s.id}
|
||||||
</option>
|
</SelectItem>
|
||||||
))}
|
))}
|
||||||
</select>
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-1.5">
|
<div className="space-y-1.5">
|
||||||
<label className="text-sm font-medium text-muted-foreground">
|
<label className="text-sm font-medium text-muted-foreground">
|
||||||
{t("selection.bind.assistant")}
|
{t("selection.bind.assistant")}
|
||||||
</label>
|
</label>
|
||||||
<select
|
<Select
|
||||||
className="h-8 rounded-md px-2 py-1 text-sm bg-white dark:bg-[#0B1220] w-full"
|
|
||||||
value={assistantId}
|
value={assistantId}
|
||||||
onChange={(e) => setAssistantId(e.target.value)}
|
onValueChange={(v) => setAssistantId(v === "__default__" ? "" : v)}
|
||||||
disabled={loading || !serverId}
|
disabled={loading || !serverId}
|
||||||
>
|
>
|
||||||
<option value="" disabled>
|
<SelectTrigger className="h-8 w-full">
|
||||||
{loading
|
<SelectValue
|
||||||
|
className="truncate"
|
||||||
|
placeholder={
|
||||||
|
(loading
|
||||||
? t("common.loading")
|
? t("common.loading")
|
||||||
: t("selection.bind.defaultAssistant")}
|
: t("selection.bind.defaultAssistant")) as string
|
||||||
</option>
|
}
|
||||||
|
/>
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{!loading && (
|
||||||
|
<SelectItem value="__default__">
|
||||||
|
{t("selection.bind.defaultAssistant")}
|
||||||
|
</SelectItem>
|
||||||
|
)}
|
||||||
{!loading &&
|
{!loading &&
|
||||||
assistantList.map((a: any) => (
|
assistantList.map((a: any) => (
|
||||||
<option key={a._id} value={a._id}>
|
<SelectItem key={a._id} value={a._id}>
|
||||||
{a._source?.name || a._id}
|
{a._source?.name || a._id}
|
||||||
</option>
|
</SelectItem>
|
||||||
))}
|
))}
|
||||||
</select>
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -7,6 +7,13 @@ import { AssistantFetcher } from "@/components/Assistant/AssistantFetcher";
|
|||||||
import { setCurrentWindowService } from "@/commands/windowService";
|
import { setCurrentWindowService } from "@/commands/windowService";
|
||||||
import { AddChatButton } from "./AddChatButton";
|
import { AddChatButton } from "./AddChatButton";
|
||||||
import { ButtonConfig, resolveLucideIcon } from "./config";
|
import { ButtonConfig, resolveLucideIcon } from "./config";
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select";
|
||||||
|
|
||||||
const ASSISTANT_CACHE_KEY = "assistant_list_cache";
|
const ASSISTANT_CACHE_KEY = "assistant_list_cache";
|
||||||
|
|
||||||
@@ -250,50 +257,58 @@ const ButtonsList = ({ buttons, setButtons, serverList }: ButtonsListProps) => {
|
|||||||
<div className="ml-auto flex items-center gap-2">
|
<div className="ml-auto flex items-center gap-2">
|
||||||
{isChat && (
|
{isChat && (
|
||||||
<>
|
<>
|
||||||
<select
|
<Select
|
||||||
className="rounded-md px-2 py-1 text-sm bg-white dark:bg-[#0B1220] w-44"
|
|
||||||
value={btn.action.assistantServerId || ""}
|
value={btn.action.assistantServerId || ""}
|
||||||
onChange={(e) => handleServerSelect(btn, e.target.value)}
|
onValueChange={(v) =>
|
||||||
title={t("selection.bind.service")}
|
handleServerSelect(btn, v === "__default__" ? "" : v)
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<option value="">
|
<SelectTrigger className="h-8 w-60">
|
||||||
|
<SelectValue className="truncate" placeholder={t("selection.bind.defaultService") as string} />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="__default__">
|
||||||
{t("selection.bind.defaultService")}
|
{t("selection.bind.defaultService")}
|
||||||
</option>
|
</SelectItem>
|
||||||
{serverList.map((s: any) => (
|
{serverList.map((s: any) => (
|
||||||
<option key={s.id} value={s.id}>
|
<SelectItem key={s.id} value={s.id}>
|
||||||
{s.name || s.endpoint || s.id}
|
{s.name || s.endpoint || s.id}
|
||||||
</option>
|
</SelectItem>
|
||||||
))}
|
))}
|
||||||
</select>
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
|
||||||
{(() => {
|
{(() => {
|
||||||
const sid = btn.action.assistantServerId;
|
const sid = btn.action.assistantServerId;
|
||||||
const list = (sid && assistantByServer[sid]) || [];
|
const list = (sid && assistantByServer[sid]) || [];
|
||||||
const loading = !!(sid && assistantLoadingByServer[sid]);
|
const loading = !!(sid && assistantLoadingByServer[sid]);
|
||||||
return (
|
return (
|
||||||
<select
|
<Select
|
||||||
className="rounded-md px-2 py-1 text-sm bg-white dark:bg-[#0B1220] w-44"
|
|
||||||
value={btn.action.assistantId || ""}
|
value={btn.action.assistantId || ""}
|
||||||
onChange={(e) =>
|
onValueChange={(v) =>
|
||||||
handleAssistantSelect(btn, e.target.value)
|
handleAssistantSelect(
|
||||||
|
btn,
|
||||||
|
v === "__default__" ? "" : v
|
||||||
|
)
|
||||||
}
|
}
|
||||||
title={t("selection.bind.assistant")}
|
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
>
|
>
|
||||||
<option value="">
|
<SelectTrigger className="h-8 w-60">
|
||||||
|
<SelectValue className="truncate" placeholder={t("selection.bind.defaultAssistant") as string} />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{!loading && (
|
||||||
|
<SelectItem value="__default__">
|
||||||
{t("selection.bind.defaultAssistant")}
|
{t("selection.bind.defaultAssistant")}
|
||||||
</option>
|
</SelectItem>
|
||||||
{loading && (
|
|
||||||
<option value="" disabled>
|
|
||||||
{t("common.loading")}
|
|
||||||
</option>
|
|
||||||
)}
|
)}
|
||||||
{list.map((a: any) => (
|
{list.map((a: any) => (
|
||||||
<option key={a._id} value={a._id}>
|
<SelectItem key={a._id} value={a._id}>
|
||||||
{a._source?.name || a._id}
|
{a._source?.name || a._id}
|
||||||
</option>
|
</SelectItem>
|
||||||
))}
|
))}
|
||||||
</select>
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
);
|
);
|
||||||
})()}
|
})()}
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -117,7 +117,7 @@ const SelectionSettings = () => {
|
|||||||
<h2 className="text-lg font-semibold">{t("selection.title")}</h2>
|
<h2 className="text-lg font-semibold">{t("selection.title")}</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="relative rounded-xl p-4 bg-gradient-to-r from-[#E6F0FA] to-[#FFF1F1]">
|
<div className="relative rounded-xl p-4 bg-linear-to-r from-[#E6F0FA] to-[#FFF1F1] dark:from-[#0B1220] dark:to-[#1A2234] dark:border dark:border-gray-800 dark:shadow-sm transition-colors">
|
||||||
<div className="flex items-center flex-col" aria-hidden="true">
|
<div className="flex items-center flex-col" aria-hidden="true">
|
||||||
<div className="rounded-xl border border-gray-200 bg-white/70 shadow-sm dark:border-gray-700 dark:bg-gray-900/40">
|
<div className="rounded-xl border border-gray-200 bg-white/70 shadow-sm dark:border-gray-700 dark:bg-gray-900/40">
|
||||||
<HeaderToolbar
|
<HeaderToolbar
|
||||||
@@ -148,7 +148,7 @@ const SelectionSettings = () => {
|
|||||||
</SettingsItem>
|
</SettingsItem>
|
||||||
|
|
||||||
{selectionEnabled && (
|
{selectionEnabled && (
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
<div className="grid grid-cols-1 gap-4">
|
||||||
<SettingsItem
|
<SettingsItem
|
||||||
icon={Sparkles}
|
icon={Sparkles}
|
||||||
title={t("selection.display.title")}
|
title={t("selection.display.title")}
|
||||||
|
|||||||
@@ -1,7 +1,14 @@
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Command, RotateCcw } from "lucide-react";
|
import { Command, RotateCcw } from "lucide-react";
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { Button } from "@headlessui/react";
|
import { Button } from "@/components/ui/button";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
|
|
||||||
import { formatKey } from "@/utils/keyboardUtils";
|
import { formatKey } from "@/utils/keyboardUtils";
|
||||||
@@ -246,21 +253,21 @@ const Shortcuts = () => {
|
|||||||
title={t("settings.advanced.shortcuts.modifierKey.title")}
|
title={t("settings.advanced.shortcuts.modifierKey.title")}
|
||||||
description={t("settings.advanced.shortcuts.modifierKey.description")}
|
description={t("settings.advanced.shortcuts.modifierKey.description")}
|
||||||
>
|
>
|
||||||
<select
|
<Select
|
||||||
value={modifierKey}
|
value={modifierKey}
|
||||||
className="px-3 py-1.5 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
|
onValueChange={(v) => setModifierKey(v as ModifierKey)}
|
||||||
onChange={(event) => {
|
|
||||||
setModifierKey(event.target.value as ModifierKey);
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{modifierKeys.map((item) => {
|
<SelectTrigger className="h-8 w-40">
|
||||||
return (
|
<SelectValue />
|
||||||
<option key={item} value={item}>
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{modifierKeys.map((item) => (
|
||||||
|
<SelectItem key={item} value={item}>
|
||||||
{formatKey(item)}
|
{formatKey(item)}
|
||||||
</option>
|
</SelectItem>
|
||||||
);
|
))}
|
||||||
})}
|
</SelectContent>
|
||||||
</select>
|
</Select>
|
||||||
</SettingsItem>
|
</SettingsItem>
|
||||||
|
|
||||||
{list.map((item) => {
|
{list.map((item) => {
|
||||||
@@ -279,6 +286,7 @@ const Shortcuts = () => {
|
|||||||
<span>{formatKey(modifierKey)}</span>
|
<span>{formatKey(modifierKey)}</span>
|
||||||
<span>+</span>
|
<span>+</span>
|
||||||
<SettingsInput
|
<SettingsInput
|
||||||
|
className="w-20"
|
||||||
value={value}
|
value={value}
|
||||||
max={1}
|
max={1}
|
||||||
onChange={(value) => {
|
onChange={(value) => {
|
||||||
@@ -287,23 +295,14 @@ const Shortcuts = () => {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
|
variant="outline"
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
className={clsx(
|
size="icon"
|
||||||
"flex items-center justify-center size-8 rounded-[6px] border border-black/5 dark:border-white/10 transition",
|
|
||||||
{
|
|
||||||
"hover:border-[#0072FF]": !disabled,
|
|
||||||
"opacity-70 cursor-not-allowed": disabled,
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
handleChange(initialValue, setValue);
|
handleChange(initialValue, setValue);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<RotateCcw
|
<RotateCcw className={clsx("size-4 opacity-80")} />
|
||||||
className={clsx("size-4 text-[#999]", {
|
|
||||||
"!text-[#0072FF]": !disabled,
|
|
||||||
})}
|
|
||||||
/>
|
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</SettingsItem>
|
</SettingsItem>
|
||||||
|
|||||||
@@ -21,8 +21,15 @@ import SettingsInput from "@/components//Settings/SettingsInput";
|
|||||||
import platformAdapter from "@/utils/platformAdapter";
|
import platformAdapter from "@/utils/platformAdapter";
|
||||||
import UpdateSettings from "./components/UpdateSettings";
|
import UpdateSettings from "./components/UpdateSettings";
|
||||||
import SettingsToggle from "../SettingsToggle";
|
import SettingsToggle from "../SettingsToggle";
|
||||||
// import SelectionSettings from "./components/Selection";
|
import SelectionSettings from "./components/Selection";
|
||||||
// import { isMac } from "@/utils/platform";
|
import { isMac } from "@/utils/platform";
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select";
|
||||||
|
|
||||||
const Advanced = () => {
|
const Advanced = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@@ -169,29 +176,27 @@ const Advanced = () => {
|
|||||||
title={t(title)}
|
title={t(title)}
|
||||||
description={t(description)}
|
description={t(description)}
|
||||||
>
|
>
|
||||||
<select
|
<Select value={value as string} onValueChange={(v) => onChange(v as never)}>
|
||||||
value={value}
|
<SelectTrigger className="h-8 w-44">
|
||||||
onChange={(event) => {
|
<SelectValue className="truncate" />
|
||||||
onChange(event.target.value as never);
|
</SelectTrigger>
|
||||||
}}
|
<SelectContent>
|
||||||
className="px-3 py-1.5 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
||||||
>
|
|
||||||
{items.map((item) => {
|
{items.map((item) => {
|
||||||
const { label, value } = item;
|
const { label, value } = item;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<option key={value} value={value}>
|
<SelectItem key={value} value={value as string}>
|
||||||
{t(label)}
|
{t(label)}
|
||||||
</option>
|
</SelectItem>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</select>
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
</SettingsItem>
|
</SettingsItem>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* {isMac && <SelectionSettings />} */}
|
{isMac && <SelectionSettings />}
|
||||||
|
|
||||||
<Shortcuts />
|
<Shortcuts />
|
||||||
|
|
||||||
@@ -278,33 +283,35 @@ const Advanced = () => {
|
|||||||
"settings.advanced.other.localSearchResultWeight.description"
|
"settings.advanced.other.localSearchResultWeight.description"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<select
|
<Select
|
||||||
value={localSearchResultWeight}
|
value={String(localSearchResultWeight)}
|
||||||
onChange={(event) => {
|
onValueChange={(v) => {
|
||||||
const weight = Number(event.target.value);
|
const weight = Number(v);
|
||||||
|
|
||||||
setLocalSearchResultWeight(weight);
|
setLocalSearchResultWeight(weight);
|
||||||
|
|
||||||
platformAdapter.invokeBackend("set_local_query_source_weight", {
|
platformAdapter.invokeBackend("set_local_query_source_weight", {
|
||||||
value: weight,
|
value: weight,
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
className="px-3 py-1.5 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
||||||
>
|
>
|
||||||
<option value="0.5">
|
<SelectTrigger className="h-8 w-44">
|
||||||
|
<SelectValue className="truncate" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="0.5">
|
||||||
{t("settings.advanced.other.localSearchResultWeight.options.low")}
|
{t("settings.advanced.other.localSearchResultWeight.options.low")}
|
||||||
</option>
|
</SelectItem>
|
||||||
<option value="1">
|
<SelectItem value="1">
|
||||||
{t(
|
{t(
|
||||||
"settings.advanced.other.localSearchResultWeight.options.medium"
|
"settings.advanced.other.localSearchResultWeight.options.medium"
|
||||||
)}
|
)}
|
||||||
</option>
|
</SelectItem>
|
||||||
<option value="2">
|
<SelectItem value="2">
|
||||||
{t(
|
{t(
|
||||||
"settings.advanced.other.localSearchResultWeight.options.high"
|
"settings.advanced.other.localSearchResultWeight.options.high"
|
||||||
)}
|
)}
|
||||||
</option>
|
</SelectItem>
|
||||||
</select>
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
</SettingsItem>
|
</SettingsItem>
|
||||||
|
|
||||||
<SettingsItem
|
<SettingsItem
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import Shortcut from "../Shortcut";
|
|||||||
import SettingsToggle from "@/components/Settings/SettingsToggle";
|
import SettingsToggle from "@/components/Settings/SettingsToggle";
|
||||||
import { platform } from "@/utils/platform";
|
import { platform } from "@/utils/platform";
|
||||||
import { useExtensionsStore } from "@/stores/extensionsStore";
|
import { useExtensionsStore } from "@/stores/extensionsStore";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
const Content = () => {
|
const Content = () => {
|
||||||
const { rootState } = useContext(ExtensionsContext);
|
const { rootState } = useContext(ExtensionsContext);
|
||||||
@@ -165,7 +166,9 @@ const Item: FC<ItemProps> = (props) => {
|
|||||||
<SettingsInput
|
<SettingsInput
|
||||||
defaultValue={alias}
|
defaultValue={alias}
|
||||||
placeholder={t("settings.extensions.hints.addAlias")}
|
placeholder={t("settings.extensions.hints.addAlias")}
|
||||||
className="!w-[90%] !h-6 !border-transparent rounded-[4px]"
|
className={cn(
|
||||||
|
"w-[90%] h-6 px-1 py-0 border-none rounded-sm shadow-none bg-transparent placeholder:text-[#999]"
|
||||||
|
)}
|
||||||
onChange={(value) => {
|
onChange={(value) => {
|
||||||
handleChange(String(value));
|
handleChange(String(value));
|
||||||
}}
|
}}
|
||||||
@@ -292,7 +295,7 @@ const Item: FC<ItemProps> = (props) => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div
|
<div
|
||||||
className={clsx("-mx-2 px-2 text-sm rounded-[6px]", {
|
className={clsx("-mx-2 px-2 text-sm rounded-md", {
|
||||||
"bg-[#f0f6fe] dark:bg-gray-700":
|
"bg-[#f0f6fe] dark:bg-gray-700":
|
||||||
id === rootState.activeExtension?.id,
|
id === rootState.activeExtension?.id,
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ const AiOverview = () => {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<>
|
<>
|
||||||
<div className="mt-6 text-[#333] dark:text-white/90">
|
<div className="mt-6">
|
||||||
{t("settings.extensions.aiOverview.details.aiOverviewTrigger.title")}
|
{t("settings.extensions.aiOverview.details.aiOverviewTrigger.title")}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -88,9 +88,7 @@ const AiOverview = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className="mb-2 text-[#666] dark:text-white/70">
|
<div className="mb-2">{label}</div>
|
||||||
{label}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<SettingsInput
|
<SettingsInput
|
||||||
type="number"
|
type="number"
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Button } from "@headlessui/react";
|
import { Button } from "@/components/ui/button";
|
||||||
import { useMount } from "ahooks";
|
import { useMount } from "ahooks";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
@@ -35,15 +35,13 @@ const Applications = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="text-[#999]">
|
<p className="mb-2">
|
||||||
<p className="font-bold mb-2">
|
|
||||||
{t("settings.extensions.application.details.searchScope")}
|
{t("settings.extensions.application.details.searchScope")}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p>
|
<p className="text-[#999]">
|
||||||
{t("settings.extensions.application.details.searchScopeDescription")}
|
{t("settings.extensions.application.details.searchScopeDescription")}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
|
||||||
|
|
||||||
<DirectoryScope
|
<DirectoryScope
|
||||||
paths={paths}
|
paths={paths}
|
||||||
@@ -72,18 +70,18 @@ const Applications = () => {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="text-[#999] mt-4">
|
<p className="mt-4 mb-2">
|
||||||
<p className="font-bold mb-2">
|
|
||||||
{t("settings.extensions.application.details.rebuildIndex")}
|
{t("settings.extensions.application.details.rebuildIndex")}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p>
|
<p className="text-[#999]">
|
||||||
{t("settings.extensions.application.details.rebuildIndexDescription")}
|
{t("settings.extensions.application.details.rebuildIndexDescription")}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
className="w-full h-8 my-4 text-[#0087FF] border border-[#EEF0F3] hover:!border-[#0087FF] dark:border-gray-700 rounded-[6px] transition"
|
variant="outline"
|
||||||
|
className="w-full my-4"
|
||||||
|
// className="w-full h-8 my-4 text-[#0087FF] border border-[#EEF0F3] hover:border-[#0087FF] dark:border-gray-700 rounded-md transition"
|
||||||
onClick={handleReindex}
|
onClick={handleReindex}
|
||||||
>
|
>
|
||||||
{t("settings.extensions.application.details.reindex")}
|
{t("settings.extensions.application.details.reindex")}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useAppStore } from "@/stores/appStore";
|
import { useAppStore } from "@/stores/appStore";
|
||||||
import platformAdapter from "@/utils/platformAdapter";
|
import platformAdapter from "@/utils/platformAdapter";
|
||||||
import { Button } from "@headlessui/react";
|
import { Button } from "@/components/ui/button";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import { castArray } from "lodash-es";
|
import { castArray } from "lodash-es";
|
||||||
import { Folder, SquareArrowOutUpRight, X } from "lucide-react";
|
import { Folder, SquareArrowOutUpRight, X } from "lucide-react";
|
||||||
@@ -82,7 +82,7 @@ const DirectoryScope: FC<DirectoryScopeProps> = (props) => {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={item}
|
key={item}
|
||||||
className="flex items-center justify-between gap-2"
|
className="flex items-center justify-between gap-2 text-[#666] dark:text-white/70"
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-1 flex-1 overflow-hidden">
|
<div className="flex items-center gap-1 flex-1 overflow-hidden">
|
||||||
<Folder className="size-4" />
|
<Folder className="size-4" />
|
||||||
@@ -112,7 +112,9 @@ const DirectoryScope: FC<DirectoryScopeProps> = (props) => {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
className="w-full h-8 text-[#0087FF] border border-[#EEF0F3] hover:!border-[#0087FF] dark:border-gray-700 rounded-[6px] transition"
|
variant="outline"
|
||||||
|
className="w-full"
|
||||||
|
size="sm"
|
||||||
onClick={handleAdd}
|
onClick={handleAdd}
|
||||||
>
|
>
|
||||||
{t("settings.extensions.directoryScope.button.addDirectories")}
|
{t("settings.extensions.directoryScope.button.addDirectories")}
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ const FileSearch = () => {
|
|||||||
{t("settings.extensions.fileSearch.description")}
|
{t("settings.extensions.fileSearch.description")}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-4 mb-2 text-[#666] dark:text-white/70">
|
<div className="mt-4 mb-2">
|
||||||
{t("settings.extensions.fileSearch.label.searchBy")}
|
{t("settings.extensions.fileSearch.label.searchBy")}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -99,10 +99,7 @@ const FileSearch = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div
|
<div key={label} className="mt-4 mb-2">
|
||||||
key={label}
|
|
||||||
className="mt-4 mb-2 text-[#666] dark:text-white/70"
|
|
||||||
>
|
|
||||||
{label}
|
{label}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -111,16 +108,16 @@ const FileSearch = () => {
|
|||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|
||||||
<div className="mt-4 mb-2 text-[#666] dark:text-white/70">
|
<div className="mt-4 mb-2">
|
||||||
{t("settings.extensions.fileSearch.label.searchFileTypes")}
|
{t("settings.extensions.fileSearch.label.searchFileTypes")}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-wrap gap-2 p-2 border rounded-[6px] dark:border-gray-700">
|
<div className="flex flex-wrap items-center gap-2 p-2 rounded-lg border border-input bg-background hover:border-[#0072FF] focus-within:border-[#0072FF] transition">
|
||||||
{config.file_types.map((item) => {
|
{config.file_types.map((item) => {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={item}
|
key={item}
|
||||||
className="flex items-center gap-1 h-6 px-1 rounded bg-[#f0f0f0] dark:bg-[#444]"
|
className="flex items-center gap-1 h-6 px-2 rounded-full text-xs border border-black/5 dark:border-white/10 bg-black/5 dark:bg-white/10"
|
||||||
>
|
>
|
||||||
<span>{item}</span>
|
<span>{item}</span>
|
||||||
|
|
||||||
@@ -140,7 +137,7 @@ const FileSearch = () => {
|
|||||||
|
|
||||||
<SettingsInput
|
<SettingsInput
|
||||||
placeholder=".*"
|
placeholder=".*"
|
||||||
className="h-6 border-0 -ml-2"
|
className="h-6 w-24 px-2 border-0 outline-none focus-visible:ring-0 focus-visible:ring-offset-0"
|
||||||
onKeyDown={(event) => {
|
onKeyDown={(event) => {
|
||||||
if (event.code !== "Enter") return;
|
if (event.code !== "Enter") return;
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,13 @@ import { isArray } from "lodash-es";
|
|||||||
import { useAsyncEffect, useMount } from "ahooks";
|
import { useAsyncEffect, useMount } from "ahooks";
|
||||||
|
|
||||||
import { AssistantFetcher } from "@/components/Assistant/AssistantFetcher";
|
import { AssistantFetcher } from "@/components/Assistant/AssistantFetcher";
|
||||||
import SettingsSelectPro from "@/components/Settings/SettingsSelectPro";
|
import {
|
||||||
|
Select,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select";
|
||||||
import { useAppStore } from "@/stores/appStore";
|
import { useAppStore } from "@/stores/appStore";
|
||||||
import { ExtensionId } from "@/components/Settings/Extensions/index";
|
import { ExtensionId } from "@/components/Settings/Extensions/index";
|
||||||
import { useConnectStore } from "@/stores/connectStore";
|
import { useConnectStore } from "@/stores/connectStore";
|
||||||
@@ -175,27 +181,40 @@ const SharedAi: FC<SharedAiProps> = (props) => {
|
|||||||
<>
|
<>
|
||||||
<div className="text-[#999]">{renderDescription()}</div>
|
<div className="text-[#999]">{renderDescription()}</div>
|
||||||
|
|
||||||
<div className="mt-6 text-[#333] dark:text-white/90">
|
<div className="mt-6">
|
||||||
{t("settings.extensions.shardAi.details.linkedAssistant.title")}
|
{t("settings.extensions.shardAi.details.linkedAssistant.title")}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{selectList.map((item) => {
|
{selectList.map((item) => {
|
||||||
const { label, value, data, searchable, onChange, onSearch } = item;
|
const { label, value, data, searchable, onChange } = item;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={label} className="mt-4">
|
<div key={label} className="mt-4">
|
||||||
<div className="mb-2 text-[#666] dark:text-white/70">{label}</div>
|
<div className="mb-2 text-[#666] dark:text-white/70">{label}</div>
|
||||||
|
|
||||||
<SettingsSelectPro
|
<Select
|
||||||
value={value}
|
value={value}
|
||||||
options={data}
|
onValueChange={(v) => onChange?.(v)}
|
||||||
searchable={searchable}
|
disabled={searchable && isLoadingAssistants}
|
||||||
onChange={onChange}
|
>
|
||||||
onSearch={onSearch}
|
<SelectTrigger className="ml-1 h-9 w-full max-w-[480px]">
|
||||||
|
<SelectValue
|
||||||
|
className="truncate"
|
||||||
placeholder={
|
placeholder={
|
||||||
isLoadingAssistants && searchable ? "Loading..." : undefined
|
(searchable && isLoadingAssistants
|
||||||
|
? (t("common.loading") as string)
|
||||||
|
: undefined) as string | undefined
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{data?.map((opt: any) => (
|
||||||
|
<SelectItem key={opt.id} value={opt.id}>
|
||||||
|
{opt.name || opt.id}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -9,7 +9,13 @@ import AiOverview from "./AiOverview";
|
|||||||
import Calculator from "./Calculator";
|
import Calculator from "./Calculator";
|
||||||
import FileSearch from "./FileSearch";
|
import FileSearch from "./FileSearch";
|
||||||
import { Ellipsis, Info } from "lucide-react";
|
import { Ellipsis, Info } from "lucide-react";
|
||||||
import { Menu, MenuButton, MenuItem, MenuItems } from "@headlessui/react";
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuContent,
|
||||||
|
} from "@/components/ui/dropdown-menu";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
import platformAdapter from "@/utils/platformAdapter";
|
import platformAdapter from "@/utils/platformAdapter";
|
||||||
import { useAppStore } from "@/stores/appStore";
|
import { useAppStore } from "@/stores/appStore";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
@@ -93,36 +99,39 @@ const Details = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex-1 h-full pr-4 pb-4 overflow-auto">
|
<div className="flex-1 h-full p-4 overflow-auto">
|
||||||
<div className="flex items-start justify-between gap-4 mb-4">
|
<div className="flex items-start justify-between gap-4 mb-2">
|
||||||
<h2 className="m-0 text-lg font-semibold text-gray-900 dark:text-white">
|
<h2 className="m-0 text-lg font-semibold text-gray-900 dark:text-white">
|
||||||
{rootState.activeExtension?.name}
|
{rootState.activeExtension?.name}
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
{rootState.activeExtension?.developer && (
|
{rootState.activeExtension?.developer && (
|
||||||
<Menu>
|
<DropdownMenu>
|
||||||
<MenuButton className="h-7">
|
<DropdownMenuTrigger asChild>
|
||||||
<Ellipsis className="size-5 text-[#999]" />
|
<Button
|
||||||
</MenuButton>
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
<MenuItems
|
className="h-7 w-7 p-0 focus-visible:ring-0 focus-visible:ring-offset-0"
|
||||||
anchor="bottom end"
|
|
||||||
className="p-1 text-sm bg-white dark:bg-[#202126] rounded-lg shadow-xs border border-gray-200 dark:border-gray-700"
|
|
||||||
>
|
>
|
||||||
<MenuItem>
|
<Ellipsis className="h-4 w-4 text-muted-foreground" />
|
||||||
<div
|
</Button>
|
||||||
className="px-3 py-2 text-nowrap text-red-500 hover:bg-black/5 hover:dark:bg-white/5 rounded-lg cursor-pointer"
|
</DropdownMenuTrigger>
|
||||||
onClick={async () => {
|
<DropdownMenuContent
|
||||||
|
side="bottom"
|
||||||
|
align="end"
|
||||||
|
className="p-1 text-sm rounded-lg"
|
||||||
|
>
|
||||||
|
<DropdownMenuItem
|
||||||
|
className="px-3 py-2 text-nowrap text-red-500 rounded-lg hover:bg-muted"
|
||||||
|
onSelect={async (e: Event) => {
|
||||||
|
e.preventDefault();
|
||||||
try {
|
try {
|
||||||
const { id, developer } = rootState.activeExtension!;
|
const { id, developer } = rootState.activeExtension!;
|
||||||
|
|
||||||
await platformAdapter.invokeBackend(
|
await platformAdapter.invokeBackend("uninstall_extension", {
|
||||||
"uninstall_extension",
|
|
||||||
{
|
|
||||||
extensionId: id,
|
extensionId: id,
|
||||||
developer: developer,
|
developer: developer,
|
||||||
}
|
});
|
||||||
);
|
|
||||||
|
|
||||||
Object.assign(rootState, {
|
Object.assign(rootState, {
|
||||||
activeExtension: void 0,
|
activeExtension: void 0,
|
||||||
@@ -141,10 +150,9 @@ const Details = () => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{t("settings.extensions.hints.uninstall")}
|
{t("settings.extensions.hints.uninstall")}
|
||||||
</div>
|
</DropdownMenuItem>
|
||||||
</MenuItem>
|
</DropdownMenuContent>
|
||||||
</MenuItems>
|
</DropdownMenu>
|
||||||
</Menu>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -3,15 +3,21 @@ import { useReactive } from "ahooks";
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import type { LiteralUnion } from "type-fest";
|
import type { LiteralUnion } from "type-fest";
|
||||||
import { cloneDeep, sortBy } from "lodash-es";
|
import { cloneDeep, sortBy } from "lodash-es";
|
||||||
import clsx from "clsx";
|
|
||||||
import { Plus } from "lucide-react";
|
import { Plus } from "lucide-react";
|
||||||
import { Menu, MenuButton, MenuItem, MenuItems } from "@headlessui/react";
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
} from "@/components/ui/dropdown-menu";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||||
|
|
||||||
import platformAdapter from "@/utils/platformAdapter";
|
import platformAdapter from "@/utils/platformAdapter";
|
||||||
import Content from "./components/Content";
|
import Content from "./components/Content";
|
||||||
import Details from "./components/Details";
|
import Details from "./components/Details";
|
||||||
import { useExtensionsStore } from "@/stores/extensionsStore";
|
import { useExtensionsStore } from "@/stores/extensionsStore";
|
||||||
import SettingsInput from "../SettingsInput";
|
|
||||||
import { useAppStore } from "@/stores/appStore";
|
import { useAppStore } from "@/stores/appStore";
|
||||||
import { installExtensionError } from "@/utils";
|
import { installExtensionError } from "@/utils";
|
||||||
|
|
||||||
@@ -184,36 +190,37 @@ export const Extensions = () => {
|
|||||||
rootState: state,
|
rootState: state,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="flex h-[calc(100vh-128px)] -mx-6 gap-4 text-sm">
|
<div className="flex h-[calc(100vh-128px)] -mx-6 text-sm">
|
||||||
<div className="w-2/3 h-full px-4 border-r dark:border-gray-700 overflow-auto">
|
<div className="w-2/3 h-full px-4 border-r border-border overflow-auto">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<h2 className="text-lg font-semibold text-gray-900 dark:text-white">
|
<h2 className="text-lg font-semibold text-gray-900 dark:text-white">
|
||||||
{t("settings.extensions.title")}
|
{t("settings.extensions.title")}
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<Menu>
|
<DropdownMenu>
|
||||||
<MenuButton className="flex items-center justify-center size-6 border rounded-[6px] dark:border-gray-700 hover:!border-[#0096FB] transition">
|
<DropdownMenuTrigger asChild>
|
||||||
<Plus className="size-4 text-[#0096FB]" />
|
<Button variant="outline" size="icon" className="size-6">
|
||||||
</MenuButton>
|
<Plus className="h-4 w-4 text-primary" />
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
|
||||||
<MenuItems
|
<DropdownMenuContent
|
||||||
anchor={{ gap: 4 }}
|
sideOffset={4}
|
||||||
className="p-1 text-sm bg-white dark:bg-[#202126] rounded-lg shadow-xs border border-gray-200 dark:border-gray-700"
|
className="p-1 text-sm rounded-lg"
|
||||||
>
|
>
|
||||||
<MenuItem>
|
<DropdownMenuItem
|
||||||
<div
|
className="px-3 py-2 rounded-lg hover:bg-muted"
|
||||||
className="px-3 py-2 hover:bg-black/5 hover:dark:bg-white/5 rounded-lg cursor-pointer"
|
onSelect={(e: Event) => {
|
||||||
onClick={() => {
|
e.preventDefault();
|
||||||
platformAdapter.emitEvent("open-extension-store");
|
platformAdapter.emitEvent("open-extension-store");
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{t("settings.extensions.menuItem.extensionStore")}
|
{t("settings.extensions.menuItem.extensionStore")}
|
||||||
</div>
|
</DropdownMenuItem>
|
||||||
</MenuItem>
|
<DropdownMenuItem
|
||||||
<MenuItem>
|
className="px-3 py-2 rounded-lg hover:bg-muted"
|
||||||
<div
|
onSelect={async (e: Event) => {
|
||||||
className="px-3 py-2 hover:bg-black/5 hover:dark:bg-white/5 rounded-lg cursor-pointer"
|
e.preventDefault();
|
||||||
onClick={async () => {
|
|
||||||
try {
|
try {
|
||||||
const path = await platformAdapter.openFileDialog({
|
const path = await platformAdapter.openFileDialog({
|
||||||
directory: true,
|
directory: true,
|
||||||
@@ -238,41 +245,33 @@ export const Extensions = () => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{t("settings.extensions.menuItem.localExtensionImport")}
|
{t("settings.extensions.menuItem.localExtensionImport")}
|
||||||
</div>
|
</DropdownMenuItem>
|
||||||
</MenuItem>
|
</DropdownMenuContent>
|
||||||
</MenuItems>
|
</DropdownMenu>
|
||||||
</Menu>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex justify-between gap-6 my-4">
|
<div className="flex items-center justify-between gap-6 my-4">
|
||||||
<div className="flex h-8 border dark:border-gray-700 rounded-[6px] overflow-hidden">
|
<Tabs
|
||||||
{state.categories.map((item) => {
|
value={state.currentCategory}
|
||||||
return (
|
onValueChange={(v) => {
|
||||||
<div
|
state.currentCategory = v as Category;
|
||||||
key={item}
|
|
||||||
className={clsx(
|
|
||||||
"flex items-center h-full px-4 cursor-pointer",
|
|
||||||
{
|
|
||||||
"bg-[#F0F6FE] dark:bg-gray-700":
|
|
||||||
item === state.currentCategory,
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
onClick={() => {
|
|
||||||
state.currentCategory = item;
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
<TabsList>
|
||||||
|
{state.categories.map((item) => (
|
||||||
|
<TabsTrigger key={item} value={item}>
|
||||||
{item}
|
{item}
|
||||||
</div>
|
</TabsTrigger>
|
||||||
);
|
))}
|
||||||
})}
|
</TabsList>
|
||||||
</div>
|
</Tabs>
|
||||||
|
|
||||||
<SettingsInput
|
<Input
|
||||||
className="flex-1"
|
className="flex-1 h-8"
|
||||||
placeholder="Search"
|
placeholder="Search"
|
||||||
value={state.searchValue}
|
value={state.searchValue ?? ""}
|
||||||
onChange={(value) => {
|
onChange={(e) => {
|
||||||
state.searchValue = String(value);
|
state.searchValue = e.target.value;
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -36,6 +36,13 @@ import {
|
|||||||
} from "@/commands";
|
} from "@/commands";
|
||||||
import platformAdapter from "@/utils/platformAdapter";
|
import platformAdapter from "@/utils/platformAdapter";
|
||||||
import { useAppearanceStore, WindowMode } from "@/stores/appearanceStore";
|
import { useAppearanceStore, WindowMode } from "@/stores/appearanceStore";
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select";
|
||||||
|
|
||||||
export function ThemeOption({
|
export function ThemeOption({
|
||||||
icon: Icon,
|
icon: Icon,
|
||||||
@@ -83,8 +90,6 @@ export default function GeneralSettings() {
|
|||||||
const { showTooltip, setShowTooltip, language, setLanguage } = useAppStore();
|
const { showTooltip, setShowTooltip, language, setLanguage } = useAppStore();
|
||||||
const { windowMode, setWindowMode } = useAppearanceStore();
|
const { windowMode, setWindowMode } = useAppearanceStore();
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const fetchAutoStartStatus = async () => {
|
const fetchAutoStartStatus = async () => {
|
||||||
if (isTauri()) {
|
if (isTauri()) {
|
||||||
try {
|
try {
|
||||||
@@ -283,7 +288,7 @@ export default function GeneralSettings() {
|
|||||||
className={clsx(
|
className={clsx(
|
||||||
"p-4 rounded-lg border-2 border-gray-200 dark:border-gray-700 hover:border-gray-300 dark:hover:border-gray-600 flex flex-col items-center justify-center space-y-2 transition-all",
|
"p-4 rounded-lg border-2 border-gray-200 dark:border-gray-700 hover:border-gray-300 dark:hover:border-gray-600 flex flex-col items-center justify-center space-y-2 transition-all",
|
||||||
{
|
{
|
||||||
"!border-blue-500 bg-blue-50 dark:bg-blue-900/20":
|
"border-blue-500! bg-blue-50! dark:bg-blue-900/20!":
|
||||||
isSelected,
|
isSelected,
|
||||||
}
|
}
|
||||||
)}
|
)}
|
||||||
@@ -307,28 +312,31 @@ export default function GeneralSettings() {
|
|||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<SettingsItem
|
<SettingsItem
|
||||||
icon={Globe}
|
icon={Globe}
|
||||||
title={t("settings.language.title")}
|
title={t("settings.language.title")}
|
||||||
description={t("settings.language.description")}
|
description={t("settings.language.description")}
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<select
|
<Select
|
||||||
value={currentLanguage}
|
value={currentLanguage}
|
||||||
onChange={(event) => {
|
onValueChange={(lang) => {
|
||||||
const lang = event.currentTarget.value;
|
|
||||||
|
|
||||||
setLanguage(lang);
|
setLanguage(lang);
|
||||||
|
|
||||||
platformAdapter.invokeBackend("update_app_lang", { lang });
|
platformAdapter.invokeBackend("update_app_lang", { lang });
|
||||||
}}
|
}}
|
||||||
className="px-3 py-1.5 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
||||||
>
|
>
|
||||||
<option value="en">{t("settings.language.english")}</option>
|
<SelectTrigger className="h-8 w-44">
|
||||||
<option value="zh">{t("settings.language.chinese")}</option>
|
<SelectValue className="truncate" />
|
||||||
</select>
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="en">
|
||||||
|
{t("settings.language.english")}
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value="zh">
|
||||||
|
{t("settings.language.chinese")}
|
||||||
|
</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
</SettingsItem>
|
</SettingsItem>
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
import { Input, InputProps } from "@headlessui/react";
|
import { Input } from "@/components/ui/input";
|
||||||
import { isNumber } from "lodash-es";
|
import { isNumber } from "lodash-es";
|
||||||
import { FC, FocusEvent } from "react";
|
import { FC, FocusEvent, InputHTMLAttributes } from "react";
|
||||||
import { twMerge } from "tailwind-merge";
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
interface SettingsInputProps
|
interface SettingsInputProps
|
||||||
extends Omit<InputProps, "onChange" | "className"> {
|
extends Omit<
|
||||||
|
InputHTMLAttributes<HTMLInputElement>,
|
||||||
|
"onChange" | "className"
|
||||||
|
> {
|
||||||
className?: string;
|
className?: string;
|
||||||
onChange?: (value?: string | number) => void;
|
onChange?: (value?: string | number) => void;
|
||||||
}
|
}
|
||||||
@@ -35,10 +38,7 @@ const SettingsInput: FC<SettingsInputProps> = (props) => {
|
|||||||
<Input
|
<Input
|
||||||
{...rest}
|
{...rest}
|
||||||
autoCorrect="off"
|
autoCorrect="off"
|
||||||
className={twMerge(
|
className={twMerge("w-44 h-8", className)}
|
||||||
"w-20 h-8 px-2 rounded-[6px] border bg-transparent border-black/5 dark:border-white/10 hover:!border-[#0072FF] focus:!border-[#0072FF] transition",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
onBlur={handleBlur}
|
onBlur={handleBlur}
|
||||||
onChange={(event) => {
|
onChange={(event) => {
|
||||||
onChange?.(event.target.value);
|
onChange?.(event.target.value);
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ interface SettingsPanelProps {
|
|||||||
|
|
||||||
const SettingsPanel: React.FC<SettingsPanelProps> = ({ children }) => {
|
const SettingsPanel: React.FC<SettingsPanelProps> = ({ children }) => {
|
||||||
return (
|
return (
|
||||||
<div className="bg-white dark:bg-gray-800 rounded-xl p-6">
|
<div className="bg-background text-foreground rounded-xl p-6">
|
||||||
{/* <h2 className="text-xl font-semibold mb-6">{title}</h2> */}
|
{/* <h2 className="text-xl font-semibold mb-6">{title}</h2> */}
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { useBoolean, useClickAway, useDebounce } from "ahooks";
|
import { useBoolean, useClickAway, useDebounce } from "ahooks";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import { FC, useEffect, useMemo, useRef, useState } from "react";
|
import { FC, useEffect, useMemo, useRef, useState } from "react";
|
||||||
import SettingsInput from "./SettingsInput";
|
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
import NoDataImage from "../Common/NoDataImage";
|
import NoDataImage from "../Common/NoDataImage";
|
||||||
|
|
||||||
interface SettingsSelectProProps {
|
interface SettingsSelectProProps {
|
||||||
@@ -47,7 +48,7 @@ const SettingsSelectPro: FC<SettingsSelectProProps> = (props) => {
|
|||||||
return (
|
return (
|
||||||
<div ref={containerRef} className="relative">
|
<div ref={containerRef} className="relative">
|
||||||
<div
|
<div
|
||||||
className="flex items-center h-8 px-3 truncate rounded-[6px] border dark:bg-[#1F2937] bg-white dark:border-[#374151]"
|
className="flex items-center h-9 px-3 truncate rounded-md border border-input bg-background text-foreground shadow-sm"
|
||||||
onClick={toggle}
|
onClick={toggle}
|
||||||
>
|
>
|
||||||
{option?.[labelField] ?? (
|
{option?.[labelField] ?? (
|
||||||
@@ -57,7 +58,7 @@ const SettingsSelectPro: FC<SettingsSelectProProps> = (props) => {
|
|||||||
|
|
||||||
<div
|
<div
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"absolute z-100 top-10 left-0 right-0 rounded-[6px] py-2 border dark:border-[#374151] bg-white dark:bg-[#1F2937] shadow-[0_5px_15px_rgba(0,0,0,0.2)] dark:shadow-[0_5px_10px_rgba(0,0,0,0.3)]",
|
"absolute z-50 top-11 left-0 right-0 rounded-md p-2 border border-input bg-popover text-popover-foreground shadow-md",
|
||||||
{
|
{
|
||||||
hidden: !open,
|
hidden: !open,
|
||||||
}
|
}
|
||||||
@@ -65,12 +66,12 @@ const SettingsSelectPro: FC<SettingsSelectProProps> = (props) => {
|
|||||||
>
|
>
|
||||||
{searchable && (
|
{searchable && (
|
||||||
<div className="px-2 mb-2">
|
<div className="px-2 mb-2">
|
||||||
<SettingsInput
|
<Input
|
||||||
autoFocus
|
autoFocus
|
||||||
value={searchValue}
|
value={searchValue}
|
||||||
className="w-full"
|
className="w-full h-8 border-0 shadow-none focus-visible:ring-0 focus-visible:ring-offset-0"
|
||||||
onChange={(value) => {
|
onChange={(e) => {
|
||||||
setSearchValue(String(value));
|
setSearchValue(String(e.target.value));
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -83,9 +84,9 @@ const SettingsSelectPro: FC<SettingsSelectProProps> = (props) => {
|
|||||||
<div
|
<div
|
||||||
key={item?.[valueField] ?? index}
|
key={item?.[valueField] ?? index}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"h-8 leading-8 px-2 rounded-[6px] hover:bg-[#EDEDED] hover:dark:bg-[#374151] transition cursor-pointer",
|
"h-8 leading-8 px-2 rounded-md hover:bg-accent hover:text-accent-foreground transition cursor-pointer",
|
||||||
{
|
{
|
||||||
"bg-[#EDEDED] dark:bg-[#374151]":
|
"bg-accent text-accent-foreground":
|
||||||
value === item?.[valueField],
|
value === item?.[valueField],
|
||||||
}
|
}
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,28 +1,26 @@
|
|||||||
import { Switch, SwitchProps } from "@headlessui/react";
|
import { Switch } from "@/components/ui/switch";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
|
|
||||||
interface SettingsToggleProps extends SwitchProps {
|
type BaseSwitchProps = React.ComponentProps<typeof Switch>;
|
||||||
|
interface SettingsToggleProps
|
||||||
|
extends Omit<BaseSwitchProps, "onChange" | "onCheckedChange"> {
|
||||||
label: string;
|
label: string;
|
||||||
className?: string;
|
className?: string;
|
||||||
|
onChange?: (checked: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function SettingsToggle(props: SettingsToggleProps) {
|
export default function SettingsToggle(props: SettingsToggleProps) {
|
||||||
const { label, className, ...rest } = props;
|
const { label, className, onChange, ...rest } = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Switch
|
<Switch
|
||||||
{...rest}
|
{...rest}
|
||||||
|
aria-label={label}
|
||||||
|
onCheckedChange={(v) => onChange?.(v)}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
`group relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent
|
"h-5 w-9",
|
||||||
transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 bg-gray-200 data-[checked]:bg-blue-600`,
|
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
>
|
|
||||||
<span className="sr-only">{label}</span>
|
|
||||||
<span
|
|
||||||
className="pointer-events-none relative inline-block h-5 w-5 transform rounded-full bg-white shadow
|
|
||||||
ring-0 transition duration-200 ease-in-out translate-x-0 group-data-[checked]:translate-x-5"
|
|
||||||
/>
|
/>
|
||||||
</Switch>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useCallback, useMemo, useEffect } from "react";
|
import { useCallback, useMemo, useEffect } from "react";
|
||||||
import { Button, Dialog, DialogPanel } from "@headlessui/react";
|
import { Dialog, DialogContent } from "@/components/ui/dialog";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { noop } from "lodash-es";
|
|
||||||
import { LoaderCircle, X } from "lucide-react";
|
import { LoaderCircle, X } from "lucide-react";
|
||||||
import { useInterval, useReactive } from "ahooks";
|
import { useInterval, useReactive } from "ahooks";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
@@ -141,39 +141,30 @@ const UpdateApp = ({ isCheckPage }: UpdateAppProps) => {
|
|||||||
return (
|
return (
|
||||||
<Dialog
|
<Dialog
|
||||||
open={isCheckPage ? true : visible}
|
open={isCheckPage ? true : visible}
|
||||||
as="div"
|
onOpenChange={(v) => {
|
||||||
id="update-app-dialog"
|
if (!isCheckPage) setVisible(v);
|
||||||
className="relative z-10 focus:outline-none"
|
}}
|
||||||
onClose={noop}
|
|
||||||
>
|
>
|
||||||
<div
|
<DialogContent
|
||||||
className={`fixed inset-0 z-10 w-screen overflow-y-auto ${
|
id="update-app-dialog"
|
||||||
|
overlayClassName={clsx("bg-transparent backdrop-blur-0 rounded-xl")}
|
||||||
|
className={clsx(
|
||||||
isCheckPage
|
isCheckPage
|
||||||
? "rounded-lg bg-white dark:bg-[#333] border border-[#EDEDED] dark:border-black/20 shadow-md"
|
? "inset-0 left-0 top-0 translate-x-0 translate-y-0 w-full h-screen max-w-none rounded-lg border-none bg-background text-foreground p-0"
|
||||||
: ""
|
: "w-[340px] py-8 flex flex-col items-center rounded-lg border border-input bg-background text-foreground shadow-md"
|
||||||
}`}
|
)}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
data-tauri-drag-region
|
data-tauri-drag-region
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"flex min-h-full items-center justify-center",
|
"w-full flex flex-col items-center justify-center px-6",
|
||||||
!isCheckPage && "p-4"
|
isCheckPage && "h-full"
|
||||||
)}
|
|
||||||
>
|
|
||||||
<DialogPanel
|
|
||||||
transition
|
|
||||||
className={clsx(
|
|
||||||
"relative w-[340px] py-8 flex flex-col items-center",
|
|
||||||
{
|
|
||||||
"rounded-lg bg-white dark:bg-[#333] border border-[#EDEDED] dark:border-black/20 shadow-md":
|
|
||||||
!isCheckPage,
|
|
||||||
}
|
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{!isCheckPage && isOptional && (
|
{!isCheckPage && isOptional && (
|
||||||
<X
|
<X
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"absolute size-5 top-3 right-3 text-[#999] dark:text-[#D8D8D8]",
|
"absolute h-5 w-5 top-3 right-3 text-muted-foreground",
|
||||||
cursorClassName
|
cursorClassName
|
||||||
)}
|
)}
|
||||||
onClick={handleCancel}
|
onClick={handleCancel}
|
||||||
@@ -185,7 +176,7 @@ const UpdateApp = ({ isCheckPage }: UpdateAppProps) => {
|
|||||||
|
|
||||||
<img src={isDark ? darkIcon : lightIcon} className="h-6" />
|
<img src={isDark ? darkIcon : lightIcon} className="h-6" />
|
||||||
|
|
||||||
<div className="text-[#333] text-sm leading-5 py-2 dark:text-[#D8D8D8] text-center">
|
<div className="text-sm leading-5 py-2 text-foreground text-center">
|
||||||
{updateInfo ? (
|
{updateInfo ? (
|
||||||
isOptional ? (
|
isOptional ? (
|
||||||
t("update.optional_description")
|
t("update.optional_description")
|
||||||
@@ -202,7 +193,7 @@ const UpdateApp = ({ isCheckPage }: UpdateAppProps) => {
|
|||||||
|
|
||||||
{updateInfo ? (
|
{updateInfo ? (
|
||||||
<div
|
<div
|
||||||
className="text-xs text-[#0072FF] cursor-pointer"
|
className="text-xs text-primary cursor-pointer"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
OpenURLWithBrowser(
|
OpenURLWithBrowser(
|
||||||
"https://docs.infinilabs.com/coco-app/main/docs/release-notes"
|
"https://docs.infinilabs.com/coco-app/main/docs/release-notes"
|
||||||
@@ -212,18 +203,18 @@ const UpdateApp = ({ isCheckPage }: UpdateAppProps) => {
|
|||||||
v{updateInfo.version} {t("update.releaseNotes")}
|
v{updateInfo.version} {t("update.releaseNotes")}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className={clsx("text-xs text-[#999]", cursorClassName)}>
|
<div
|
||||||
|
className={clsx("text-xs text-muted-foreground", cursorClassName)}
|
||||||
|
>
|
||||||
{t("update.latest", {
|
{t("update.latest", {
|
||||||
replace: [
|
replace: [updateInfo?.version || process.env.VERSION || "N/A"],
|
||||||
updateInfo?.version || process.env.VERSION || "N/A",
|
|
||||||
],
|
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"mb-3 mt-6 bg-[#0072FF] text-white text-sm px-[14px] py-[8px] rounded-lg",
|
"mb-3 mt-6 bg-primary text-primary-foreground text-sm px-[14px] py-[8px] rounded-lg",
|
||||||
cursorClassName,
|
cursorClassName,
|
||||||
state.loading && "opacity-50"
|
state.loading && "opacity-50"
|
||||||
)}
|
)}
|
||||||
@@ -231,7 +222,7 @@ const UpdateApp = ({ isCheckPage }: UpdateAppProps) => {
|
|||||||
>
|
>
|
||||||
{state.loading ? (
|
{state.loading ? (
|
||||||
<div className="flex justify-center items-center gap-2">
|
<div className="flex justify-center items-center gap-2">
|
||||||
<LoaderCircle className="animate-spin size-5" />
|
<LoaderCircle className="animate-spin h-5 w-5" />
|
||||||
{percent}%
|
{percent}%
|
||||||
</div>
|
</div>
|
||||||
) : updateInfo ? (
|
) : updateInfo ? (
|
||||||
@@ -243,15 +234,14 @@ const UpdateApp = ({ isCheckPage }: UpdateAppProps) => {
|
|||||||
|
|
||||||
{updateInfo && isOptional && (
|
{updateInfo && isOptional && (
|
||||||
<div
|
<div
|
||||||
className={clsx("text-xs text-[#999]", cursorClassName)}
|
className={clsx("text-xs text-muted-foreground", cursorClassName)}
|
||||||
onClick={handleSkip}
|
onClick={handleSkip}
|
||||||
>
|
>
|
||||||
{t("update.skip_version")}
|
{t("update.skip_version")}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</DialogPanel>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useAppStore } from "@/stores/appStore";
|
import { useAppStore } from "@/stores/appStore";
|
||||||
import { Button } from "@headlessui/react";
|
import { Button } from "@/components/ui/button";
|
||||||
import { SquareArrowOutUpRight } from "lucide-react";
|
import { SquareArrowOutUpRight } from "lucide-react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
@@ -12,10 +12,7 @@ const LoginButton = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button className="h-8" onClick={handleClick}>
|
||||||
className="px-6 h-8 text-white bg-[#0287FF] flex rounded-[8px] items-center justify-center gap-1"
|
|
||||||
onClick={handleClick}
|
|
||||||
>
|
|
||||||
<span>{t("webLogin.buttons.login")}</span>
|
<span>{t("webLogin.buttons.login")}</span>
|
||||||
|
|
||||||
<SquareArrowOutUpRight className="size-4" />
|
<SquareArrowOutUpRight className="size-4" />
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { RefreshCw } from "lucide-react";
|
import { RefreshCw } from "lucide-react";
|
||||||
import { FC, useState } from "react";
|
import { FC, useState } from "react";
|
||||||
import { Button, ButtonProps } from "@headlessui/react";
|
import { Button, ButtonProps } from "@/components/ui/button";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
|
|
||||||
import { useWebConfigStore } from "@/stores/webConfigStore";
|
import { useWebConfigStore } from "@/stores/webConfigStore";
|
||||||
@@ -25,10 +25,9 @@ const RefreshButton: FC<ButtonProps> = (props) => {
|
|||||||
<Button
|
<Button
|
||||||
{...rest}
|
{...rest}
|
||||||
onClick={handleRefresh}
|
onClick={handleRefresh}
|
||||||
className={clsx(
|
variant="outline"
|
||||||
"flex items-center justify-center size-6 bg-white dark:bg-[#202126] rounded-[8px] border dark:border-white/10",
|
size="icon"
|
||||||
className
|
className={clsx("size-8", className)}
|
||||||
)}
|
|
||||||
disabled={isRefreshing}
|
disabled={isRefreshing}
|
||||||
>
|
>
|
||||||
<VisibleKey shortcut="R" onKeyPress={handleRefresh}>
|
<VisibleKey shortcut="R" onKeyPress={handleRefresh}>
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ const UserAvatar: FC<UserAvatarProps> = (props) => {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"flex items-center justify-center size-5 rounded-full border dark:border-white/10 overflow-hidden",
|
"flex items-center justify-center size-5 rounded-full border border-border overflow-hidden",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,4 +1,8 @@
|
|||||||
import { Popover, PopoverButton, PopoverPanel } from "@headlessui/react";
|
import {
|
||||||
|
Popover,
|
||||||
|
PopoverTrigger,
|
||||||
|
PopoverContent,
|
||||||
|
} from "@/components/ui/popover";
|
||||||
import { useWebConfigStore } from "@/stores/webConfigStore";
|
import { useWebConfigStore } from "@/stores/webConfigStore";
|
||||||
import { LogOut } from "lucide-react";
|
import { LogOut } from "lucide-react";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
@@ -10,21 +14,18 @@ import RefreshButton from "./RefreshButton";
|
|||||||
import LoginButton from "./LoginButton";
|
import LoginButton from "./LoginButton";
|
||||||
import { FC } from "react";
|
import { FC } from "react";
|
||||||
import Copyright from "../Common/Copyright";
|
import Copyright from "../Common/Copyright";
|
||||||
|
import { PopoverContentProps } from "@radix-ui/react-popover";
|
||||||
|
import { Button } from "../ui/button";
|
||||||
|
|
||||||
interface WebLoginProps {
|
const WebLogin: FC<PopoverContentProps> = (props) => {
|
||||||
panelClassName: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const WebLogin: FC<WebLoginProps> = (props) => {
|
|
||||||
const { panelClassName } = props;
|
|
||||||
const { integration, loginInfo, setIntegration, setLoginInfo } =
|
const { integration, loginInfo, setIntegration, setLoginInfo } =
|
||||||
useWebConfigStore();
|
useWebConfigStore();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative">
|
<div className="flex items-center relative text-sm">
|
||||||
<Popover>
|
<Popover>
|
||||||
<PopoverButton>
|
<PopoverTrigger className="cursor-pointer">
|
||||||
{loginInfo ? (
|
{loginInfo ? (
|
||||||
<UserAvatar />
|
<UserAvatar />
|
||||||
) : (
|
) : (
|
||||||
@@ -33,38 +34,35 @@ const WebLogin: FC<WebLoginProps> = (props) => {
|
|||||||
className="size-5 text-[#999]"
|
className="size-5 text-[#999]"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</PopoverButton>
|
</PopoverTrigger>
|
||||||
|
|
||||||
<PopoverPanel
|
<PopoverContent {...props} className="p-0">
|
||||||
className={clsx(
|
|
||||||
"absolute z-50 w-[300px] rounded-xl bg-white dark:bg-[#202126] text-sm/6 text-[#333] dark:text-[#D8D8D8] shadow-lg border dark:border-white/10 -translate-y-2",
|
|
||||||
panelClassName
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<div className="p-3">
|
<div className="p-3">
|
||||||
<div className="flex items-center justify-between mb-2">
|
<div className="flex items-center justify-between mb-2">
|
||||||
<span>{t("webLogin.title")}</span>
|
<span>{t("webLogin.title")}</span>
|
||||||
|
|
||||||
<RefreshButton />
|
<RefreshButton className="size-6" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="py-2">
|
<div className="py-2">
|
||||||
{loginInfo ? (
|
{loginInfo ? (
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center gap-3">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<UserAvatar
|
<UserAvatar
|
||||||
className="!size-12"
|
className="h-12 w-12"
|
||||||
icon={{ className: "!size-6" }}
|
icon={{ className: "h-6 w-6" }}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<span>{loginInfo.name}</span>
|
<span>{loginInfo?.name}</span>
|
||||||
<span className="text-[#999]">{loginInfo.email}</span>
|
<span className="text-[#999]">{loginInfo?.email}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button
|
<Button
|
||||||
className="flex items-center justify-center size-6 bg-white dark:bg-[#202126] rounded-[8px] border dark:border-white/10"
|
variant="outline"
|
||||||
|
size="icon"
|
||||||
|
className="size-6"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
await Post("/account/logout", void 0);
|
await Post("/account/logout", void 0);
|
||||||
|
|
||||||
@@ -77,7 +75,7 @@ const WebLogin: FC<WebLoginProps> = (props) => {
|
|||||||
"size-3 text-[#0287FF] transition-transform duration-1000"
|
"size-3 text-[#0287FF] transition-transform duration-1000"
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex flex-col items-center gap-3">
|
<div className="flex flex-col items-center gap-3">
|
||||||
@@ -93,10 +91,10 @@ const WebLogin: FC<WebLoginProps> = (props) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="p-3 border-t dark:border-t-white/10">
|
<div className="p-3 border-t border-border">
|
||||||
<Copyright />
|
<Copyright />
|
||||||
</div>
|
</div>
|
||||||
</PopoverPanel>
|
</PopoverContent>
|
||||||
</Popover>
|
</Popover>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,52 +1,46 @@
|
|||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { Slot } from "@radix-ui/react-slot";
|
|
||||||
import { cva, type VariantProps } from "class-variance-authority";
|
import { cva, type VariantProps } from "class-variance-authority";
|
||||||
|
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
const buttonVariants = cva(
|
const buttonVariants = cva(
|
||||||
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-[6px] text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 ring-offset-background cursor-pointer disabled:cursor-not-allowed disabled:pointer-events-none disabled:opacity-50",
|
||||||
{
|
{
|
||||||
variants: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
default:
|
default:
|
||||||
"bg-primary text-primary-foreground shadow hover:bg-primary/90",
|
"bg-primary text-primary-foreground hover:bg-primary/90",
|
||||||
destructive:
|
|
||||||
"bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
|
|
||||||
outline:
|
|
||||||
"border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
|
|
||||||
secondary:
|
secondary:
|
||||||
"bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
|
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||||
|
outline:
|
||||||
|
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
|
||||||
ghost: "hover:bg-accent hover:text-accent-foreground",
|
ghost: "hover:bg-accent hover:text-accent-foreground",
|
||||||
link: "text-primary underline-offset-4 hover:underline",
|
destructive:
|
||||||
|
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
|
||||||
},
|
},
|
||||||
size: {
|
size: {
|
||||||
default: "h-9 px-4 py-2",
|
sm: "h-8 px-3",
|
||||||
sm: "h-8 rounded-[6px] px-3 text-xs",
|
md: "h-9 px-4",
|
||||||
lg: "h-10 rounded-[6px] px-8",
|
lg: "h-10 px-6",
|
||||||
icon: "h-9 w-9",
|
icon: "h-9 w-9",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
defaultVariants: {
|
defaultVariants: {
|
||||||
variant: "default",
|
variant: "default",
|
||||||
size: "default",
|
size: "md",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
export interface ButtonProps
|
export interface ButtonProps
|
||||||
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||||
VariantProps<typeof buttonVariants> {
|
VariantProps<typeof buttonVariants> {}
|
||||||
asChild?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||||
({ className, variant, size, asChild = false, ...props }, ref) => {
|
({ className, variant, size, ...props }, ref) => {
|
||||||
const Comp = asChild ? Slot : "button";
|
|
||||||
return (
|
return (
|
||||||
<Comp
|
<button
|
||||||
className={cn(buttonVariants({ variant, size, className }))}
|
|
||||||
ref={ref}
|
ref={ref}
|
||||||
|
className={cn(buttonVariants({ variant, size }), className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
25
src/components/ui/checkbox.tsx
Normal file
25
src/components/ui/checkbox.tsx
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
import * as CheckboxPrimitive from "@radix-ui/react-checkbox";
|
||||||
|
import { Check } from "lucide-react";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
const Checkbox = React.forwardRef<
|
||||||
|
React.ElementRef<typeof CheckboxPrimitive.Root>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<CheckboxPrimitive.Root
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"peer h-4 w-4 shrink-0 rounded-sm border border-input bg-background shadow ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<CheckboxPrimitive.Indicator className="flex items-center justify-center text-primary-foreground">
|
||||||
|
<Check className="h-3.5 w-3.5" />
|
||||||
|
</CheckboxPrimitive.Indicator>
|
||||||
|
</CheckboxPrimitive.Root>
|
||||||
|
));
|
||||||
|
Checkbox.displayName = CheckboxPrimitive.Root.displayName;
|
||||||
|
|
||||||
|
export { Checkbox };
|
||||||
108
src/components/ui/dialog.tsx
Normal file
108
src/components/ui/dialog.tsx
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
import * as DialogPrimitive from "@radix-ui/react-dialog";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
const Dialog = DialogPrimitive.Root;
|
||||||
|
const DialogTrigger = DialogPrimitive.Trigger;
|
||||||
|
const DialogPortal = DialogPrimitive.Portal;
|
||||||
|
const DialogClose = DialogPrimitive.Close;
|
||||||
|
|
||||||
|
const DialogOverlay = React.forwardRef<
|
||||||
|
React.ElementRef<typeof DialogPrimitive.Overlay>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<DialogPrimitive.Overlay
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"fixed inset-0 z-3000 bg-black/60 backdrop-blur-sm",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
|
||||||
|
|
||||||
|
type DialogContentProps = React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content> & {
|
||||||
|
overlayClassName?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const DialogContent = React.forwardRef<
|
||||||
|
React.ElementRef<typeof DialogPrimitive.Content>,
|
||||||
|
DialogContentProps
|
||||||
|
>(({ className, overlayClassName, ...props }, ref) => (
|
||||||
|
<DialogPortal>
|
||||||
|
<DialogOverlay className={overlayClassName} />
|
||||||
|
<DialogPrimitive.Content
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"fixed left-1/2 top-1/2 z-3001 w-full max-w-lg -translate-x-1/2 -translate-y-1/2 rounded-lg border border-input bg-background p-6 text-foreground shadow-lg focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 ring-offset-background",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</DialogPortal>
|
||||||
|
));
|
||||||
|
DialogContent.displayName = DialogPrimitive.Content.displayName;
|
||||||
|
|
||||||
|
const DialogHeader = ({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"flex flex-col space-y-2 text-center sm:text-left",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
const DialogFooter = ({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
const DialogTitle = React.forwardRef<
|
||||||
|
React.ElementRef<typeof DialogPrimitive.Title>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<DialogPrimitive.Title
|
||||||
|
ref={ref}
|
||||||
|
className={cn("text-lg font-semibold", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
DialogTitle.displayName = DialogPrimitive.Title.displayName;
|
||||||
|
|
||||||
|
const DialogDescription = React.forwardRef<
|
||||||
|
React.ElementRef<typeof DialogPrimitive.Description>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<DialogPrimitive.Description
|
||||||
|
ref={ref}
|
||||||
|
className={cn("text-sm text-muted-foreground", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
DialogDescription.displayName = DialogPrimitive.Description.displayName;
|
||||||
|
|
||||||
|
export {
|
||||||
|
Dialog,
|
||||||
|
DialogTrigger,
|
||||||
|
DialogPortal,
|
||||||
|
DialogOverlay,
|
||||||
|
DialogContent,
|
||||||
|
DialogHeader,
|
||||||
|
DialogFooter,
|
||||||
|
DialogTitle,
|
||||||
|
DialogDescription,
|
||||||
|
DialogClose,
|
||||||
|
};
|
||||||
78
src/components/ui/dropdown-menu.tsx
Normal file
78
src/components/ui/dropdown-menu.tsx
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
const DropdownMenu = DropdownMenuPrimitive.Root;
|
||||||
|
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;
|
||||||
|
|
||||||
|
const DropdownMenuContent = React.forwardRef<
|
||||||
|
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
|
||||||
|
>(({ className, side = "bottom", align = "start", sideOffset = 8, ...props }, ref) => (
|
||||||
|
<DropdownMenuPrimitive.Portal>
|
||||||
|
<DropdownMenuPrimitive.Content
|
||||||
|
ref={ref}
|
||||||
|
side={side}
|
||||||
|
align={align}
|
||||||
|
sideOffset={sideOffset}
|
||||||
|
className={cn(
|
||||||
|
"z-50 min-w-40 rounded-lg border border-input bg-background p-1 text-foreground shadow-lg",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</DropdownMenuPrimitive.Portal>
|
||||||
|
));
|
||||||
|
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName;
|
||||||
|
|
||||||
|
const DropdownMenuItem = React.forwardRef<
|
||||||
|
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<DropdownMenuPrimitive.Item
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"flex cursor-pointer select-none items-center rounded-md px-3 py-2 text-sm outline-none focus:bg-muted",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName;
|
||||||
|
|
||||||
|
const DropdownMenuLabel = React.forwardRef<
|
||||||
|
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<DropdownMenuPrimitive.Label
|
||||||
|
ref={ref}
|
||||||
|
className={cn("px-3 py-1 text-xs text-muted-foreground", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName;
|
||||||
|
|
||||||
|
const DropdownMenuSeparator = React.forwardRef<
|
||||||
|
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<DropdownMenuPrimitive.Separator
|
||||||
|
ref={ref}
|
||||||
|
className={cn("my-1 h-px bg-muted", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName;
|
||||||
|
|
||||||
|
const DropdownMenuGroup = DropdownMenuPrimitive.Group;
|
||||||
|
|
||||||
|
export {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuLabel,
|
||||||
|
DropdownMenuSeparator,
|
||||||
|
DropdownMenuGroup,
|
||||||
|
};
|
||||||
|
|
||||||
24
src/components/ui/input.tsx
Normal file
24
src/components/ui/input.tsx
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
export interface InputProps
|
||||||
|
extends React.InputHTMLAttributes<HTMLInputElement> {}
|
||||||
|
|
||||||
|
const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
||||||
|
({ className, type = "text", ...props }, ref) => {
|
||||||
|
return (
|
||||||
|
<input
|
||||||
|
type={type}
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm text-foreground shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 ring-offset-background disabled:cursor-not-allowed disabled:opacity-50",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
Input.displayName = "Input";
|
||||||
|
|
||||||
|
export { Input };
|
||||||
21
src/components/ui/label.tsx
Normal file
21
src/components/ui/label.tsx
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
import * as LabelPrimitive from "@radix-ui/react-label";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
const Label = React.forwardRef<
|
||||||
|
React.ElementRef<typeof LabelPrimitive.Root>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<LabelPrimitive.Root
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
Label.displayName = LabelPrimitive.Root.displayName;
|
||||||
|
|
||||||
|
export { Label };
|
||||||
|
|
||||||
97
src/components/ui/multi-select.tsx
Normal file
97
src/components/ui/multi-select.tsx
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
import * as PopoverPrimitive from "@radix-ui/react-popover";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import { Checkbox } from "@/components/ui/checkbox";
|
||||||
|
|
||||||
|
type Option = { value: string; label: string };
|
||||||
|
|
||||||
|
export interface MultiSelectProps {
|
||||||
|
options: Option[];
|
||||||
|
value: string[];
|
||||||
|
onChange?: (next: string[]) => void;
|
||||||
|
placeholder?: string;
|
||||||
|
className?: string;
|
||||||
|
disabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const MultiSelect: React.FC<MultiSelectProps> = ({
|
||||||
|
options,
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
placeholder = "",
|
||||||
|
className,
|
||||||
|
disabled,
|
||||||
|
}) => {
|
||||||
|
const [open, setOpen] = React.useState(false);
|
||||||
|
const values = React.useMemo(() => new Set(value), [value]);
|
||||||
|
|
||||||
|
const toggle = (v: string) => {
|
||||||
|
const next = new Set(values);
|
||||||
|
if (next.has(v)) next.delete(v);
|
||||||
|
else next.add(v);
|
||||||
|
onChange?.(Array.from(next));
|
||||||
|
};
|
||||||
|
|
||||||
|
const display = React.useMemo(() => {
|
||||||
|
if (values.size === 0) return placeholder;
|
||||||
|
const labels = options
|
||||||
|
.filter((o) => values.has(o.value))
|
||||||
|
.map((o) => o.label);
|
||||||
|
return labels.join(", ");
|
||||||
|
}, [options, values, placeholder]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PopoverPrimitive.Root open={open} onOpenChange={setOpen}>
|
||||||
|
<PopoverPrimitive.Trigger asChild>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
disabled={disabled}
|
||||||
|
className={cn(
|
||||||
|
"flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<span className={cn(values.size === 0 && "text-muted-foreground")}>{display}</span>
|
||||||
|
<svg
|
||||||
|
className="h-4 w-4 opacity-70"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
>
|
||||||
|
<path d="M6 9l6 6 6-6" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</PopoverPrimitive.Trigger>
|
||||||
|
<PopoverPrimitive.Content
|
||||||
|
sideOffset={4}
|
||||||
|
className={cn(
|
||||||
|
"z-50 w-(--radix-popover-trigger-width) min-w-[220px] rounded-md border border-input bg-popover p-2 text-popover-foreground shadow-md outline-none",
|
||||||
|
"ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="max-h-48 overflow-y-auto space-y-1">
|
||||||
|
{options.map((opt) => {
|
||||||
|
const checked = values.has(opt.value) ? "checked" : "unchecked";
|
||||||
|
return (
|
||||||
|
<label
|
||||||
|
key={opt.value}
|
||||||
|
className="flex cursor-pointer items-center gap-2 rounded-sm px-2 py-1 hover:bg-accent hover:text-accent-foreground"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
toggle(opt.value);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Checkbox checked={checked === "checked"} className="h-4 w-4" />
|
||||||
|
<span className="text-sm">{opt.label}</span>
|
||||||
|
</label>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</PopoverPrimitive.Content>
|
||||||
|
</PopoverPrimitive.Root>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
43
src/components/ui/popover.tsx
Normal file
43
src/components/ui/popover.tsx
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
import * as PopoverPrimitive from "@radix-ui/react-popover";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
const Popover = PopoverPrimitive.Root;
|
||||||
|
const PopoverTrigger = PopoverPrimitive.Trigger;
|
||||||
|
const PopoverPortal = PopoverPrimitive.Portal;
|
||||||
|
|
||||||
|
const PopoverContent = React.forwardRef<
|
||||||
|
React.ElementRef<typeof PopoverPrimitive.Content>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content> & {
|
||||||
|
panelId?: string;
|
||||||
|
}
|
||||||
|
>(
|
||||||
|
(
|
||||||
|
{
|
||||||
|
className,
|
||||||
|
panelId,
|
||||||
|
side = "bottom",
|
||||||
|
align = "start",
|
||||||
|
sideOffset = 8,
|
||||||
|
...props
|
||||||
|
},
|
||||||
|
ref
|
||||||
|
) => (
|
||||||
|
<PopoverPrimitive.Content
|
||||||
|
ref={ref}
|
||||||
|
side={side}
|
||||||
|
align={align}
|
||||||
|
sideOffset={sideOffset}
|
||||||
|
className={cn(
|
||||||
|
"z-50 rounded-lg border border-input bg-background p-1 text-foreground shadow-lg outline-none",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
data-popover-panel
|
||||||
|
id={panelId}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
);
|
||||||
|
PopoverContent.displayName = "PopoverContent";
|
||||||
|
|
||||||
|
export { Popover, PopoverTrigger, PopoverContent, PopoverPortal };
|
||||||
160
src/components/ui/select.tsx
Normal file
160
src/components/ui/select.tsx
Normal file
@@ -0,0 +1,160 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
import * as SelectPrimitive from "@radix-ui/react-select"
|
||||||
|
import { Check, ChevronDown, ChevronUp } from "lucide-react"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const Select = SelectPrimitive.Root
|
||||||
|
|
||||||
|
const SelectGroup = SelectPrimitive.Group
|
||||||
|
|
||||||
|
const SelectValue = SelectPrimitive.Value
|
||||||
|
|
||||||
|
const SelectTrigger = React.forwardRef<
|
||||||
|
React.ElementRef<typeof SelectPrimitive.Trigger>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
|
||||||
|
>(({ className, children, ...props }, ref) => (
|
||||||
|
<SelectPrimitive.Trigger
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"flex h-9 w-full items-center justify-between whitespace-nowrap rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm ring-offset-background data-placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
<SelectPrimitive.Icon asChild>
|
||||||
|
<ChevronDown className="h-4 w-4 opacity-50" />
|
||||||
|
</SelectPrimitive.Icon>
|
||||||
|
</SelectPrimitive.Trigger>
|
||||||
|
))
|
||||||
|
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
|
||||||
|
|
||||||
|
const SelectScrollUpButton = React.forwardRef<
|
||||||
|
React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<SelectPrimitive.ScrollUpButton
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"flex cursor-default items-center justify-center py-1",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<ChevronUp className="h-4 w-4" />
|
||||||
|
</SelectPrimitive.ScrollUpButton>
|
||||||
|
))
|
||||||
|
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
|
||||||
|
|
||||||
|
const SelectScrollDownButton = React.forwardRef<
|
||||||
|
React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<SelectPrimitive.ScrollDownButton
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"flex cursor-default items-center justify-center py-1",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<ChevronDown className="h-4 w-4" />
|
||||||
|
</SelectPrimitive.ScrollDownButton>
|
||||||
|
))
|
||||||
|
SelectScrollDownButton.displayName =
|
||||||
|
SelectPrimitive.ScrollDownButton.displayName
|
||||||
|
|
||||||
|
const SelectContent = React.forwardRef<
|
||||||
|
React.ElementRef<typeof SelectPrimitive.Content>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
|
||||||
|
>(({ className, children, position = "popper", side = "bottom", avoidCollisions = false, sideOffset = 4, ...props }, ref) => (
|
||||||
|
<SelectPrimitive.Portal>
|
||||||
|
<SelectPrimitive.Content
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"relative z-50 max-h-[var(--radix-select-content-available-height)] min-w-[var(--radix-select-trigger-width)] w-[var(--radix-select-trigger-width)] overflow-hidden rounded-md border border-input bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[var(--radix-select-content-transform-origin)]",
|
||||||
|
position === "popper" &&
|
||||||
|
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
position={position}
|
||||||
|
side={side}
|
||||||
|
avoidCollisions={avoidCollisions}
|
||||||
|
sideOffset={sideOffset}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<SelectScrollUpButton />
|
||||||
|
<SelectPrimitive.Viewport
|
||||||
|
className={cn(
|
||||||
|
"p-1 max-h-[50vh] overflow-y-auto",
|
||||||
|
position === "popper" &&
|
||||||
|
"w-full min-w-[var(--radix-select-trigger-width)]"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</SelectPrimitive.Viewport>
|
||||||
|
<SelectScrollDownButton />
|
||||||
|
</SelectPrimitive.Content>
|
||||||
|
</SelectPrimitive.Portal>
|
||||||
|
))
|
||||||
|
SelectContent.displayName = SelectPrimitive.Content.displayName
|
||||||
|
|
||||||
|
const SelectLabel = React.forwardRef<
|
||||||
|
React.ElementRef<typeof SelectPrimitive.Label>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<SelectPrimitive.Label
|
||||||
|
ref={ref}
|
||||||
|
className={cn("px-2 py-1.5 text-sm font-semibold", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
SelectLabel.displayName = SelectPrimitive.Label.displayName
|
||||||
|
|
||||||
|
const SelectItem = React.forwardRef<
|
||||||
|
React.ElementRef<typeof SelectPrimitive.Item>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
|
||||||
|
>(({ className, children, ...props }, ref) => (
|
||||||
|
<SelectPrimitive.Item
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-2 pr-8 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<span className="absolute right-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||||
|
<SelectPrimitive.ItemIndicator>
|
||||||
|
<Check className="h-4 w-4" />
|
||||||
|
</SelectPrimitive.ItemIndicator>
|
||||||
|
</span>
|
||||||
|
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
||||||
|
</SelectPrimitive.Item>
|
||||||
|
))
|
||||||
|
SelectItem.displayName = SelectPrimitive.Item.displayName
|
||||||
|
|
||||||
|
const SelectSeparator = React.forwardRef<
|
||||||
|
React.ElementRef<typeof SelectPrimitive.Separator>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<SelectPrimitive.Separator
|
||||||
|
ref={ref}
|
||||||
|
className={cn("-mx-1 my-1 h-px bg-muted", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
SelectSeparator.displayName = SelectPrimitive.Separator.displayName
|
||||||
|
|
||||||
|
export {
|
||||||
|
Select,
|
||||||
|
SelectGroup,
|
||||||
|
SelectValue,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectContent,
|
||||||
|
SelectLabel,
|
||||||
|
SelectItem,
|
||||||
|
SelectSeparator,
|
||||||
|
SelectScrollUpButton,
|
||||||
|
SelectScrollDownButton,
|
||||||
|
}
|
||||||
@@ -1,29 +1,24 @@
|
|||||||
import * as React from "react"
|
import * as React from "react";
|
||||||
import * as SeparatorPrimitive from "@radix-ui/react-separator"
|
import * as SeparatorPrimitive from "@radix-ui/react-separator";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
|
|
||||||
const Separator = React.forwardRef<
|
const Separator = React.forwardRef<
|
||||||
React.ElementRef<typeof SeparatorPrimitive.Root>,
|
React.ElementRef<typeof SeparatorPrimitive.Root>,
|
||||||
React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
|
React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
|
||||||
>(
|
>(({ className, orientation = "horizontal", decorative, ...props }, ref) => (
|
||||||
(
|
|
||||||
{ className, orientation = "horizontal", decorative = true, ...props },
|
|
||||||
ref
|
|
||||||
) => (
|
|
||||||
<SeparatorPrimitive.Root
|
<SeparatorPrimitive.Root
|
||||||
ref={ref}
|
ref={ref}
|
||||||
decorative={decorative}
|
decorative={decorative}
|
||||||
orientation={orientation}
|
orientation={orientation}
|
||||||
className={cn(
|
className={cn(
|
||||||
"shrink-0 bg-border",
|
"shrink-0 bg-border",
|
||||||
orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]",
|
orientation === "horizontal" ? "h-px w-full" : "h-full w-px",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
));
|
||||||
)
|
Separator.displayName = SeparatorPrimitive.Root.displayName;
|
||||||
Separator.displayName = SeparatorPrimitive.Root.displayName
|
|
||||||
|
export { Separator };
|
||||||
|
|
||||||
export { Separator }
|
|
||||||
|
|||||||
27
src/components/ui/slider.tsx
Normal file
27
src/components/ui/slider.tsx
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
import * as SliderPrimitive from "@radix-ui/react-slider";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
const Slider = React.forwardRef<
|
||||||
|
React.ElementRef<typeof SliderPrimitive.Root>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof SliderPrimitive.Root>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<SliderPrimitive.Root
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"relative flex w-full touch-none select-none items-center",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<SliderPrimitive.Track className="relative h-1.5 w-full grow overflow-hidden rounded-full bg-secondary">
|
||||||
|
<SliderPrimitive.Range className="absolute h-full bg-primary" />
|
||||||
|
</SliderPrimitive.Track>
|
||||||
|
<SliderPrimitive.Thumb className="block h-4 w-4 rounded-full border-2 border-primary bg-background ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50" />
|
||||||
|
</SliderPrimitive.Root>
|
||||||
|
));
|
||||||
|
|
||||||
|
Slider.displayName = SliderPrimitive.Root.displayName;
|
||||||
|
|
||||||
|
export { Slider };
|
||||||
|
|
||||||
27
src/components/ui/switch.tsx
Normal file
27
src/components/ui/switch.tsx
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
import * as SwitchPrimitives from "@radix-ui/react-switch"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const Switch = React.forwardRef<
|
||||||
|
React.ElementRef<typeof SwitchPrimitives.Root>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<SwitchPrimitives.Root
|
||||||
|
className={cn(
|
||||||
|
"peer inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
ref={ref}
|
||||||
|
>
|
||||||
|
<SwitchPrimitives.Thumb
|
||||||
|
className={cn(
|
||||||
|
"pointer-events-none block h-4 w-4 rounded-full bg-white shadow ring-0 transition-transform data-[state=checked]:translate-x-4 data-[state=unchecked]:translate-x-0"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</SwitchPrimitives.Root>
|
||||||
|
))
|
||||||
|
Switch.displayName = SwitchPrimitives.Root.displayName
|
||||||
|
|
||||||
|
export { Switch }
|
||||||
53
src/components/ui/tabs.tsx
Normal file
53
src/components/ui/tabs.tsx
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
import * as TabsPrimitive from "@radix-ui/react-tabs";
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
const Tabs = TabsPrimitive.Root;
|
||||||
|
|
||||||
|
const TabsList = React.forwardRef<
|
||||||
|
React.ElementRef<typeof TabsPrimitive.List>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<TabsPrimitive.List
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"inline-flex h-9 items-center justify-center rounded-lg bg-muted p-1 text-muted-foreground",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
TabsList.displayName = TabsPrimitive.List.displayName;
|
||||||
|
|
||||||
|
const TabsTrigger = React.forwardRef<
|
||||||
|
React.ElementRef<typeof TabsPrimitive.Trigger>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<TabsPrimitive.Trigger
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"inline-flex items-center justify-center whitespace-nowrap rounded-md px-3 py-1 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName;
|
||||||
|
|
||||||
|
const TabsContent = React.forwardRef<
|
||||||
|
React.ElementRef<typeof TabsPrimitive.Content>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<TabsPrimitive.Content
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
TabsContent.displayName = TabsPrimitive.Content.displayName;
|
||||||
|
|
||||||
|
export { Tabs, TabsList, TabsTrigger, TabsContent };
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
export const POPOVER_PANEL_SELECTOR = '[id^="headlessui-popover-panel"]';
|
export const POPOVER_PANEL_SELECTOR = '[data-popover-panel]';
|
||||||
|
|
||||||
export const HISTORY_PANEL_ID = "headlessui-popover-panel:history-panel";
|
export const HISTORY_PANEL_ID = "headlessui-popover-panel:history-panel";
|
||||||
|
|
||||||
|
|||||||
@@ -302,6 +302,7 @@ export function useChatActions(
|
|||||||
);
|
);
|
||||||
|
|
||||||
const getChatHistory = useCallback(async () => {
|
const getChatHistory = useCallback(async () => {
|
||||||
|
try {
|
||||||
let response: any;
|
let response: any;
|
||||||
if (isTauri) {
|
if (isTauri) {
|
||||||
if (await unrequitable()) {
|
if (await unrequitable()) {
|
||||||
@@ -326,6 +327,9 @@ export function useChatActions(
|
|||||||
|
|
||||||
const hits = response?.hits?.hits || [];
|
const hits = response?.hits?.hits || [];
|
||||||
setChats(hits);
|
setChats(hits);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("getChatHistory error:", error);
|
||||||
|
}
|
||||||
}, [
|
}, [
|
||||||
currentService?.id,
|
currentService?.id,
|
||||||
keyword,
|
keyword,
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import platformAdapter from "@/utils/platformAdapter";
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { MAIN_WINDOW_LABEL, SETTINGS_WINDOW_LABEL } from "@/constants";
|
import { MAIN_WINDOW_LABEL, SETTINGS_WINDOW_LABEL } from "@/constants";
|
||||||
import { useAsyncEffect, useEventListener } from "ahooks";
|
import { useAsyncEffect, useEventListener } from "ahooks";
|
||||||
|
import { installExtensionError } from "@/utils";
|
||||||
|
|
||||||
export interface DeepLinkHandler {
|
export interface DeepLinkHandler {
|
||||||
pattern: string;
|
pattern: string;
|
||||||
@@ -78,7 +79,7 @@ export function useDeepLinkManager() {
|
|||||||
addError(t("deepLink.extensionInstallSuccessfully"), "info");
|
addError(t("deepLink.extensionInstallSuccessfully"), "info");
|
||||||
console.log("Extension installed successfully:", extensionId);
|
console.log("Extension installed successfully:", extensionId);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
addError(String(error));
|
installExtensionError(error)
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user