feat: add categories and icons (#107)
* feat: add search data type * feat: add search store * feat: search result add category * chore: UI adjustments * chore: UI adjustments * chore: add css duration * feat: add type icon & add categories * feat: add doc click open url * chore: remove debug * chore: optimize page details
4
.env
@@ -1,3 +1,3 @@
|
||||
COCO_SERVER_URL=https://coco.infini.cloud # http://localhost:2900
|
||||
COCO_SERVER_URL=https://infini.tpddns.cn:27200 #https://coco.infini.cloud # http://localhost:2900
|
||||
|
||||
COCO_WEBSOCKET_URL=wss://coco.infini.cloud/ws # ws://localhost:2900/ws
|
||||
COCO_WEBSOCKET_URL=wss://infini.tpddns.cn:27200/ws #wss://coco.infini.cloud/ws # ws://localhost:2900/ws
|
||||
2
.vscode/settings.json
vendored
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"cSpell.words": [
|
||||
"ahooks",
|
||||
"autolaunch",
|
||||
"Avenir",
|
||||
"callout",
|
||||
@@ -30,6 +31,7 @@
|
||||
"tailwindcss",
|
||||
"tauri",
|
||||
"titlebar",
|
||||
"tpddns",
|
||||
"traptitech",
|
||||
"unlisten",
|
||||
"unlistener",
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
"@tauri-apps/plugin-shell": ">=2.0.0",
|
||||
"@tauri-apps/plugin-websocket": "~2",
|
||||
"@tauri-apps/plugin-window": "2.0.0-alpha.1",
|
||||
"ahooks": "^3.8.4",
|
||||
"axios": "^1.7.7",
|
||||
"clsx": "^2.1.1",
|
||||
"dotenv": "^16.4.7",
|
||||
|
||||
62
pnpm-lock.yaml
generated
@@ -41,6 +41,9 @@ importers:
|
||||
'@tauri-apps/plugin-window':
|
||||
specifier: 2.0.0-alpha.1
|
||||
version: 2.0.0-alpha.1
|
||||
ahooks:
|
||||
specifier: ^3.8.4
|
||||
version: 3.8.4(react@18.3.1)
|
||||
axios:
|
||||
specifier: ^1.7.7
|
||||
version: 1.7.7
|
||||
@@ -551,46 +554,55 @@ packages:
|
||||
resolution: {integrity: sha512-0KXvIJQMOImLCVCz9uvvdPgfyWo93aHHp8ui3FrtOP57svqrF/roSSR5pjqL2hcMp0ljeGlU4q9o/rQaAQ3AYA==}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-arm-musleabihf@4.24.0':
|
||||
resolution: {integrity: sha512-it2BW6kKFVh8xk/BnHfakEeoLPv8STIISekpoF+nBgWM4d55CZKc7T4Dx1pEbTnYm/xEKMgy1MNtYuoA8RFIWw==}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-linux-arm64-gnu@4.24.0':
|
||||
resolution: {integrity: sha512-i0xTLXjqap2eRfulFVlSnM5dEbTVque/3Pi4g2y7cxrs7+a9De42z4XxKLYJ7+OhE3IgxvfQM7vQc43bwTgPwA==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-arm64-musl@4.24.0':
|
||||
resolution: {integrity: sha512-9E6MKUJhDuDh604Qco5yP/3qn3y7SLXYuiC0Rpr89aMScS2UAmK1wHP2b7KAa1nSjWJc/f/Lc0Wl1L47qjiyQw==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-linux-powerpc64le-gnu@4.24.0':
|
||||
resolution: {integrity: sha512-2XFFPJ2XMEiF5Zi2EBf4h73oR1V/lycirxZxHZNc93SqDN/IWhYYSYj8I9381ikUFXZrz2v7r2tOVk2NBwxrWw==}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-riscv64-gnu@4.24.0':
|
||||
resolution: {integrity: sha512-M3Dg4hlwuntUCdzU7KjYqbbd+BLq3JMAOhCKdBE3TcMGMZbKkDdJ5ivNdehOssMCIokNHFOsv7DO4rlEOfyKpg==}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-s390x-gnu@4.24.0':
|
||||
resolution: {integrity: sha512-mjBaoo4ocxJppTorZVKWFpy1bfFj9FeCMJqzlMQGjpNPY9JwQi7OuS1axzNIk0nMX6jSgy6ZURDZ2w0QW6D56g==}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-x64-gnu@4.24.0':
|
||||
resolution: {integrity: sha512-ZXFk7M72R0YYFN5q13niV0B7G8/5dcQ9JDp8keJSfr3GoZeXEoMHP/HlvqROA3OMbMdfr19IjCeNAnPUG93b6A==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-x64-musl@4.24.0':
|
||||
resolution: {integrity: sha512-w1i+L7kAXZNdYl+vFvzSZy8Y1arS7vMgIy8wusXJzRrPyof5LAb02KGr1PD2EkRcl73kHulIID0M501lN+vobQ==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-win32-arm64-msvc@4.24.0':
|
||||
resolution: {integrity: sha512-VXBrnPWgBpVDCVY6XF3LEW0pOU51KbaHhccHw6AS6vBWIC60eqsH19DAeeObl+g8nKAz04QFdl/Cefta0xQtUQ==}
|
||||
@@ -649,24 +661,28 @@ packages:
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@tauri-apps/cli-linux-arm64-musl@2.0.3':
|
||||
resolution: {integrity: sha512-I4MVD7nf6lLLRmNQPpe5beEIFM6q7Zkmh77ROA5BNu/+vHNL5kiTMD+bmd10ZL2r753A6pO7AvqkIxcBuIl0tg==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@tauri-apps/cli-linux-x64-gnu@2.0.3':
|
||||
resolution: {integrity: sha512-C6Jkx2zZGKkoi+sg5FK9GoH/0EvAaOgrZfF5azV5EALGba46g7VpWcZgp9zFUd7K2IzTi+0OOY8TQ2OVfKZgew==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@tauri-apps/cli-linux-x64-musl@2.0.3':
|
||||
resolution: {integrity: sha512-qi4ghmTfSAl+EEUDwmwI9AJUiOLNSmU1RgiGgcPRE+7A/W+Am9UnxYySAiRbB/gJgTl9sj/pqH5Y9duP1/sqHg==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@tauri-apps/cli-win32-arm64-msvc@2.0.3':
|
||||
resolution: {integrity: sha512-UXxHkYmFesC97qVmZre4vY7oDxRDtC2OeKNv0bH+iSnuUp/ROxzJYGyaelnv9Ybvgl4YVqDCnxgB28qMM938TA==}
|
||||
@@ -901,6 +917,12 @@ packages:
|
||||
engines: {node: '>=0.4.0'}
|
||||
hasBin: true
|
||||
|
||||
ahooks@3.8.4:
|
||||
resolution: {integrity: sha512-39wDEw2ZHvypaT14EpMMk4AzosHWt0z9bulY0BeDsvc9PqJEV+Kjh/4TZfftSsotBMq52iYIOFPd3PR56e0ZJg==}
|
||||
engines: {node: '>=8.0.0'}
|
||||
peerDependencies:
|
||||
react: ^16.8.0 || ^17.0.0 || ^18.0.0
|
||||
|
||||
ansi-regex@5.0.1:
|
||||
resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}
|
||||
engines: {node: '>=8'}
|
||||
@@ -1428,6 +1450,9 @@ packages:
|
||||
resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
intersection-observer@0.12.2:
|
||||
resolution: {integrity: sha512-7m1vEcPCxXYI8HqnL8CKI6siDyD+eIWSwgB3DZA+ZTogxk9I4CDnj4wilt9x/+/QbHI4YG5YZNmC6458/e9Ktg==}
|
||||
|
||||
is-alphabetical@2.0.1:
|
||||
resolution: {integrity: sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==}
|
||||
|
||||
@@ -1478,6 +1503,10 @@ packages:
|
||||
resolution: {integrity: sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w==}
|
||||
hasBin: true
|
||||
|
||||
js-cookie@3.0.5:
|
||||
resolution: {integrity: sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==}
|
||||
engines: {node: '>=14'}
|
||||
|
||||
js-tokens@4.0.0:
|
||||
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
|
||||
|
||||
@@ -1867,6 +1896,9 @@ packages:
|
||||
peerDependencies:
|
||||
react: ^18.3.1
|
||||
|
||||
react-fast-compare@3.2.2:
|
||||
resolution: {integrity: sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==}
|
||||
|
||||
react-hotkeys-hook@4.5.1:
|
||||
resolution: {integrity: sha512-scAEJOh3Irm0g95NIn6+tQVf/OICCjsQsC9NBHfQws/Vxw4sfq1tDQut5fhTEvPraXhu/sHxRd9lOtxzyYuNAg==}
|
||||
peerDependencies:
|
||||
@@ -1947,6 +1979,9 @@ packages:
|
||||
remark-stringify@11.0.0:
|
||||
resolution: {integrity: sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==}
|
||||
|
||||
resize-observer-polyfill@1.5.1:
|
||||
resolution: {integrity: sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==}
|
||||
|
||||
resolve@1.22.8:
|
||||
resolution: {integrity: sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==}
|
||||
hasBin: true
|
||||
@@ -1978,6 +2013,10 @@ packages:
|
||||
scheduler@0.23.2:
|
||||
resolution: {integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==}
|
||||
|
||||
screenfull@5.2.0:
|
||||
resolution: {integrity: sha512-9BakfsO2aUQN2K9Fdbj87RJIEZ82Q9IGim7FqM5OsebfoFC6ZHXgDq/KvniuLTPdeM8wY2o6Dj3WQ7KeQCj3cA==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
semver@6.3.1:
|
||||
resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==}
|
||||
hasBin: true
|
||||
@@ -2985,6 +3024,19 @@ snapshots:
|
||||
|
||||
acorn@8.14.0: {}
|
||||
|
||||
ahooks@3.8.4(react@18.3.1):
|
||||
dependencies:
|
||||
'@babel/runtime': 7.25.9
|
||||
dayjs: 1.11.13
|
||||
intersection-observer: 0.12.2
|
||||
js-cookie: 3.0.5
|
||||
lodash: 4.17.21
|
||||
react: 18.3.1
|
||||
react-fast-compare: 3.2.2
|
||||
resize-observer-polyfill: 1.5.1
|
||||
screenfull: 5.2.0
|
||||
tslib: 2.8.0
|
||||
|
||||
ansi-regex@5.0.1: {}
|
||||
|
||||
ansi-regex@6.1.0: {}
|
||||
@@ -3570,6 +3622,8 @@ snapshots:
|
||||
|
||||
internmap@2.0.3: {}
|
||||
|
||||
intersection-observer@0.12.2: {}
|
||||
|
||||
is-alphabetical@2.0.1: {}
|
||||
|
||||
is-alphanumerical@2.0.1:
|
||||
@@ -3611,6 +3665,8 @@ snapshots:
|
||||
|
||||
jiti@1.21.6: {}
|
||||
|
||||
js-cookie@3.0.5: {}
|
||||
|
||||
js-tokens@4.0.0: {}
|
||||
|
||||
jsesc@3.0.2: {}
|
||||
@@ -4223,6 +4279,8 @@ snapshots:
|
||||
react: 18.3.1
|
||||
scheduler: 0.23.2
|
||||
|
||||
react-fast-compare@3.2.2: {}
|
||||
|
||||
react-hotkeys-hook@4.5.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
|
||||
dependencies:
|
||||
react: 18.3.1
|
||||
@@ -4349,6 +4407,8 @@ snapshots:
|
||||
mdast-util-to-markdown: 2.1.2
|
||||
unified: 11.0.5
|
||||
|
||||
resize-observer-polyfill@1.5.1: {}
|
||||
|
||||
resolve@1.22.8:
|
||||
dependencies:
|
||||
is-core-module: 2.15.1
|
||||
@@ -4400,6 +4460,8 @@ snapshots:
|
||||
dependencies:
|
||||
loose-envify: 1.4.0
|
||||
|
||||
screenfull@5.2.0: {}
|
||||
|
||||
semver@6.3.1: {}
|
||||
|
||||
shebang-command@2.0.0:
|
||||
|
||||
@@ -83,7 +83,6 @@ pub fn run() {
|
||||
// show_panel,
|
||||
// hide_panel,
|
||||
// close_panel
|
||||
shortcut::check_shortcut_available,
|
||||
])
|
||||
.setup(|app| {
|
||||
init(app.app_handle());
|
||||
@@ -168,7 +167,7 @@ fn hide_coco(app: tauri::AppHandle) {
|
||||
}
|
||||
|
||||
fn handle_open_coco(app: &AppHandle) {
|
||||
println!("Open Coco menu clicked!");
|
||||
// println!("Open Coco menu clicked!");
|
||||
|
||||
if let Some(window) = app.get_window("main") {
|
||||
window.show().unwrap();
|
||||
@@ -179,7 +178,7 @@ fn handle_open_coco(app: &AppHandle) {
|
||||
}
|
||||
|
||||
fn handle_hide_coco(app: &AppHandle) {
|
||||
println!("Hide Coco menu clicked!");
|
||||
// println!("Hide Coco menu clicked!");
|
||||
|
||||
if let Some(window) = app.get_window("main") {
|
||||
if let Err(err) = window.hide() {
|
||||
@@ -196,7 +195,7 @@ fn handle_hide_coco(app: &AppHandle) {
|
||||
fn switch_tray_icon(app: tauri::AppHandle, is_dark_mode: bool) {
|
||||
let app_handle = app.app_handle();
|
||||
|
||||
println!("is_dark_mode: {}", is_dark_mode);
|
||||
// println!("is_dark_mode: {}", is_dark_mode);
|
||||
|
||||
const DARK_ICON_PATH: &[u8] = include_bytes!("../icons/dark@2x.png");
|
||||
const LIGHT_ICON_PATH: &[u8] = include_bytes!("../icons/light@2x.png");
|
||||
|
||||
@@ -160,31 +160,3 @@ pub fn _get_shortcut<R: Runtime>(app: &AppHandle<R>) -> String {
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn check_shortcut_available(key: String) -> bool {
|
||||
// 这里可以实现系统级的快捷键检查
|
||||
// 可以检查是否与其他应用的全局快捷键冲突
|
||||
// 返回 true 表示可用,false 表示已被占用
|
||||
|
||||
// 简单实现示例:
|
||||
!is_system_shortcut(&key)
|
||||
}
|
||||
|
||||
fn is_system_shortcut(key: &str) -> bool {
|
||||
let system_shortcuts = vec![
|
||||
"Command+C",
|
||||
"Command+V",
|
||||
"Command+X",
|
||||
"Command+A",
|
||||
"Command+Z",
|
||||
"Control+C",
|
||||
"Control+V",
|
||||
"Control+X",
|
||||
"Control+A",
|
||||
"Control+Z",
|
||||
// 添加更多系统快捷键
|
||||
];
|
||||
|
||||
system_shortcuts.contains(&key)
|
||||
}
|
||||
|
||||
BIN
src/assets/app-icon.png
Normal file
|
After Width: | Height: | Size: 460 KiB |
BIN
src/assets/hugo_site/blog.png
Normal file
|
After Width: | Height: | Size: 753 B |
BIN
src/assets/hugo_site/icon.png
Normal file
|
After Width: | Height: | Size: 1.0 KiB |
BIN
src/assets/hugo_site/news.png
Normal file
|
After Width: | Height: | Size: 621 B |
BIN
src/assets/hugo_site/web.png
Normal file
|
After Width: | Height: | Size: 710 B |
BIN
src/assets/hugo_site/web_page.png
Normal file
|
After Width: | Height: | Size: 439 B |
BIN
src/assets/images/file_efault.png
Normal file
|
After Width: | Height: | Size: 2.1 KiB |
BIN
src/assets/images/source_default.png
Normal file
|
After Width: | Height: | Size: 3.9 KiB |
BIN
src/assets/images/source_default_dark.png
Normal file
|
After Width: | Height: | Size: 3.0 KiB |
@@ -1,28 +1,67 @@
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { CircleAlert, Bolt, X } from "lucide-react";
|
||||
import {
|
||||
CircleAlert,
|
||||
Bolt,
|
||||
X,
|
||||
SquareArrowRight,
|
||||
// UserRoundPen,
|
||||
} from "lucide-react";
|
||||
|
||||
import { isTauri } from "@tauri-apps/api/core";
|
||||
import { open } from "@tauri-apps/plugin-shell";
|
||||
|
||||
import { useAppStore } from "@/stores/appStore";
|
||||
import { useSearchStore } from "@/stores/searchStore";
|
||||
import source_default_img from "@/assets/images/source_default.png";
|
||||
import source_default_dark_img from "@/assets/images/source_default_dark.png";
|
||||
import file_efault_img from "@/assets/images/file_efault.png";
|
||||
import { useTheme } from "@/contexts/ThemeContext";
|
||||
|
||||
type ISearchData = Record<string, any[]>;
|
||||
|
||||
interface DropdownListProps {
|
||||
selected: (item: any) => void;
|
||||
suggests: any[];
|
||||
SearchData: ISearchData;
|
||||
IsError: boolean;
|
||||
isSearchComplete: boolean;
|
||||
isChatMode: boolean;
|
||||
}
|
||||
|
||||
function DropdownList({ selected, suggests, IsError }: DropdownListProps) {
|
||||
function DropdownList({
|
||||
selected,
|
||||
suggests,
|
||||
SearchData,
|
||||
IsError,
|
||||
isChatMode,
|
||||
}: DropdownListProps) {
|
||||
let globalIndex = 0;
|
||||
const globalItemIndexMap: any[] = [];
|
||||
// const letterFirstIndex: any = {
|
||||
// a: 0,
|
||||
// s: 0,
|
||||
// d: 0,
|
||||
// f: 0,
|
||||
// };
|
||||
|
||||
const { theme } = useTheme();
|
||||
|
||||
const connector_data = useAppStore((state) => state.connector_data);
|
||||
const datasourceData = useAppStore((state) => state.datasourceData);
|
||||
const endpoint_http = useAppStore((state) => state.endpoint_http);
|
||||
const setSourceData = useSearchStore((state) => state.setSourceData);
|
||||
|
||||
const [showError, setShowError] = useState<boolean>(IsError);
|
||||
const [selectedItem, setSelectedItem] = useState<number | null>(null);
|
||||
const [selectedName, setSelectedName] = useState<string>("");
|
||||
const [showIndex, setShowIndex] = useState<boolean>(false);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const itemRefs = useRef<(HTMLDivElement | null)[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
isChatMode && setSelectedItem(null);
|
||||
}, [isChatMode]);
|
||||
|
||||
const handleOpenURL = async (url: string) => {
|
||||
if (!url) return;
|
||||
try {
|
||||
@@ -46,9 +85,12 @@ function DropdownList({ selected, suggests, IsError }: DropdownListProps) {
|
||||
|
||||
if (e.key === "ArrowUp") {
|
||||
e.preventDefault();
|
||||
setSelectedItem((prev) =>
|
||||
prev === null || prev === 0 ? suggests.length - 1 : prev - 1
|
||||
);
|
||||
setSelectedItem((prev) => {
|
||||
const res =
|
||||
prev === null || prev === 0 ? suggests.length - 1 : prev - 1;
|
||||
|
||||
return res;
|
||||
});
|
||||
} else if (e.key === "ArrowDown") {
|
||||
e.preventDefault();
|
||||
setSelectedItem((prev) =>
|
||||
@@ -56,12 +98,22 @@ function DropdownList({ selected, suggests, IsError }: DropdownListProps) {
|
||||
);
|
||||
} else if (e.key === "Meta") {
|
||||
e.preventDefault();
|
||||
if (selectedItem !== null) {
|
||||
const item = globalItemIndexMap[selectedItem];
|
||||
setSelectedName(item?._source?.source?.name);
|
||||
}
|
||||
setShowIndex(true);
|
||||
}
|
||||
|
||||
if (e.key === "ArrowRight" && selectedItem !== null) {
|
||||
e.preventDefault();
|
||||
const item = globalItemIndexMap[selectedItem];
|
||||
goToTwoPage(item);
|
||||
}
|
||||
|
||||
if (e.key === "Enter" && selectedItem !== null) {
|
||||
// console.log("Enter key pressed", selectedItem);
|
||||
const item = suggests[selectedItem];
|
||||
const item = globalItemIndexMap[selectedItem];
|
||||
if (item?._source?.url) {
|
||||
handleOpenURL(item?._source?.url);
|
||||
} else {
|
||||
@@ -71,7 +123,7 @@ function DropdownList({ selected, suggests, IsError }: DropdownListProps) {
|
||||
|
||||
if (e.key >= "0" && e.key <= "9" && showIndex) {
|
||||
// console.log(`number ${e.key}`);
|
||||
const item = suggests[parseInt(e.key, 10)];
|
||||
const item = globalItemIndexMap[parseInt(e.key, 10)];
|
||||
if (item?._source?.url) {
|
||||
handleOpenURL(item?._source?.url);
|
||||
} else {
|
||||
@@ -108,26 +160,87 @@ function DropdownList({ selected, suggests, IsError }: DropdownListProps) {
|
||||
}
|
||||
}, [selectedItem]);
|
||||
|
||||
function getIcon(_source: any) {
|
||||
const id = _source?.source?.id || "";
|
||||
function findConnectorIcon(item: any) {
|
||||
const id = item?._source?.source?.id || "";
|
||||
|
||||
const result = datasourceData.find((item: any) => item._source.id === id);
|
||||
|
||||
const connector_id = result?._source?.connector?.id;
|
||||
|
||||
const result1 = connector_data.find(
|
||||
(item: any) => item._source.id === connector_id
|
||||
const result_source = datasourceData.find(
|
||||
(data: any) => data._source.id === id
|
||||
);
|
||||
|
||||
const icons = result1?._source?.assets?.icons || {};
|
||||
const connector_id = result_source?._source?.connector?.id;
|
||||
|
||||
if (icons[_source.icon]?.includes("http")) {
|
||||
return icons[_source.icon];
|
||||
const result_connector = connector_data.find(
|
||||
(data: any) => data._source.id === connector_id
|
||||
);
|
||||
|
||||
return result_connector?._source;
|
||||
}
|
||||
|
||||
function getTypeIcon(item: any) {
|
||||
const connectorSource = findConnectorIcon(item);
|
||||
const icons = connectorSource?.icon;
|
||||
|
||||
if (!icons) {
|
||||
return theme === "dark" ? source_default_dark_img : source_default_img;
|
||||
}
|
||||
|
||||
if (icons?.includes("http")) {
|
||||
return icons;
|
||||
} else {
|
||||
return endpoint_http + icons[_source.icon];
|
||||
return endpoint_http + icons;
|
||||
}
|
||||
}
|
||||
|
||||
function getIcon(item: any) {
|
||||
const connectorSource = findConnectorIcon(item);
|
||||
const icons = connectorSource?.assets?.icons || {};
|
||||
|
||||
const selectedIcon = icons[item?._source?.icon];
|
||||
|
||||
if (!selectedIcon) {
|
||||
return file_efault_img;
|
||||
}
|
||||
|
||||
if (selectedIcon?.includes("http")) {
|
||||
return selectedIcon;
|
||||
} else {
|
||||
return endpoint_http + selectedIcon;
|
||||
}
|
||||
}
|
||||
|
||||
function getRichIcon(item: any) {
|
||||
const connectorSource = findConnectorIcon(item);
|
||||
const icons = connectorSource?.assets?.icons || {};
|
||||
|
||||
const selectedIcon = icons[item?._source?.rich_categories?.[0]?.icon];
|
||||
|
||||
if (!selectedIcon) {
|
||||
return theme === "dark" ? source_default_dark_img : source_default_img;
|
||||
}
|
||||
|
||||
if (selectedIcon?.includes("http")) {
|
||||
return selectedIcon;
|
||||
} else {
|
||||
return endpoint_http + selectedIcon;
|
||||
}
|
||||
}
|
||||
|
||||
function goToTwoPage(item: any) {
|
||||
setSourceData(item);
|
||||
selected && selected(item);
|
||||
}
|
||||
|
||||
// function numberToLetter(num: number): string {
|
||||
// const mapping = ["A", "S", "D", "F"];
|
||||
// if (num >= 0 && num < mapping.length) {
|
||||
// const letter = mapping[num];
|
||||
// letterFirstIndex[letter.toLocaleLowerCase()] = globalIndex
|
||||
// return letter
|
||||
// } else {
|
||||
// return "";
|
||||
// }
|
||||
// }
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
@@ -147,52 +260,127 @@ function DropdownList({ selected, suggests, IsError }: DropdownListProps) {
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
<div className="p-2 text-xs text-[#999] dark:text-[#666]">Results</div>
|
||||
{suggests?.map((item, index) => {
|
||||
const isSelected = selectedItem === index;
|
||||
return (
|
||||
<div
|
||||
key={item._id}
|
||||
ref={(el) => (itemRefs.current[index] = el)}
|
||||
onMouseEnter={() => setSelectedItem(index)}
|
||||
onClick={() => {
|
||||
if (item?._source?.url) {
|
||||
handleOpenURL(item?._source?.url);
|
||||
} else {
|
||||
selected(item);
|
||||
}
|
||||
}}
|
||||
className={`w-full px-2 py-2.5 text-sm flex items-center justify-between rounded-lg transition-colors ${
|
||||
isSelected
|
||||
? "bg-[rgba(0,0,0,0.1)] dark:bg-[rgba(255,255,255,0.1)] hover:bg-[rgba(0,0,0,0.1)] dark:hover:bg-[rgba(255,255,255,0.1)]"
|
||||
: ""
|
||||
}`}
|
||||
>
|
||||
<div className="flex gap-2 items-center">
|
||||
<img
|
||||
className="w-5 h-5"
|
||||
src={getIcon(item?._source)}
|
||||
alt="icon"
|
||||
{Object.entries(SearchData).map(([sourceName, items]) => (
|
||||
<div key={sourceName}>
|
||||
{items.length > 2 ? (
|
||||
<div className="p-2 text-xs text-[#999] dark:text-[#666] flex items-center gap-2.5 relative">
|
||||
<img className="w-4 h-4" src={getTypeIcon(items[0])} alt="icon" />
|
||||
{sourceName}
|
||||
<div className="flex-1 border-b border-b-[#e6e6e6] dark:border-b-[#272626]"></div>
|
||||
<SquareArrowRight
|
||||
className="w-4 h-4 cursor-pointer"
|
||||
onClick={() => goToTwoPage(items[0])}
|
||||
/>
|
||||
<span className="text-[#333] dark:text-[#d8d8d8] truncate w-80 text-left">
|
||||
{item?._source?.title}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex gap-2 items-center relative">
|
||||
<span className="text-sm text-[#666] dark:text-[#666] truncate w-52 text-right">
|
||||
{item?._source?.source?.name}
|
||||
</span>
|
||||
{showIndex && index < 10 ? (
|
||||
{showIndex && sourceName === selectedName ? (
|
||||
<div
|
||||
className={`absolute right-0 w-4 h-4 flex items-center justify-center font-normal text-xs text-[#333] leading-[14px] bg-[#ccc] dark:bg-[#6B6B6B] shadow-[-6px_0px_6px_2px_#e6e6e6] dark:shadow-[-6px_0px_6px_2px_#000] rounded-md`}
|
||||
className={`absolute right-2
|
||||
w-4 h-4 flex items-end 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]`}
|
||||
>
|
||||
{index}
|
||||
→
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
) : null}
|
||||
{items.map((item: any) => {
|
||||
const isSelected = selectedItem === globalIndex;
|
||||
const currentIndex = globalIndex;
|
||||
globalItemIndexMap.push(item);
|
||||
globalIndex++;
|
||||
return (
|
||||
<div
|
||||
key={item._id}
|
||||
ref={(el) => (itemRefs.current[currentIndex] = el)}
|
||||
onMouseEnter={() => setSelectedItem(currentIndex)}
|
||||
onClick={() => {
|
||||
if (item?._source?.url) {
|
||||
handleOpenURL(item?._source?.url);
|
||||
} else {
|
||||
selected(item);
|
||||
}
|
||||
}}
|
||||
className={`w-full px-2 py-2.5 text-sm flex items-center justify-between rounded-lg transition-colors ${
|
||||
isSelected
|
||||
? "text-white bg-[#950599] hover:bg-[#950599]"
|
||||
: "text-[#333] dark:text-[#d8d8d8]"
|
||||
}`}
|
||||
>
|
||||
<div className="flex gap-2 items-center justify-start max-w-[450px]">
|
||||
<img className="w-5 h-5" src={getIcon(item)} alt="icon" />
|
||||
<span
|
||||
className={`text-sm truncate text-left ${
|
||||
isSelected ? "font-medium" : ""
|
||||
}`}
|
||||
>
|
||||
{item?._source?.title}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex-1 min-w-[180px] h-full text-[12px] flex gap-2 items-center justify-end relative">
|
||||
<span
|
||||
className={`text-[12px] truncate ${
|
||||
isSelected
|
||||
? "text-[#DCDCDC]"
|
||||
: "text-[#999] dark:text-[#666]"
|
||||
}`}
|
||||
>
|
||||
{(item?._source?.category || "") +
|
||||
(item?._source?.subcategory
|
||||
? `/${item?._source?.subcategory}`
|
||||
: "")}
|
||||
</span>
|
||||
{item?._source?.rich_categories ? (
|
||||
<div className="truncate flex gap-2">
|
||||
<img
|
||||
className="w-4 h-4 cursor-pointer"
|
||||
src={getRichIcon(item)}
|
||||
alt="icon"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
goToTwoPage(item);
|
||||
}}
|
||||
/>
|
||||
{item?._source?.rich_categories?.map((rich_item: any) => (
|
||||
<span
|
||||
className={`${
|
||||
isSelected ? "text-[#C8C8C8]" : "text-[#666]"
|
||||
} text-right mr-1`}
|
||||
>
|
||||
{rich_item?.label}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{isSelected ? (
|
||||
<div
|
||||
className={`absolute ${
|
||||
showIndex && currentIndex < 10 ? "right-7" : "right-0"
|
||||
} w-4 h-4 flex items-end justify-center font-normal text-xs text-[#333] leading-[14px] bg-[#ccc] dark:bg-[#6B6B6B] rounded-md ${
|
||||
isSelected
|
||||
? "shadow-[-6px_0px_6px_2px_#950599]"
|
||||
: "shadow-[-6px_0px_6px_2px_#fff] dark:shadow-[-6px_0px_6px_2px_#000]"
|
||||
}`}
|
||||
>
|
||||
↩︎
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{showIndex && currentIndex < 10 ? (
|
||||
<div
|
||||
className={`absolute right-0 w-4 h-4 flex items-center justify-center font-normal text-xs text-[#333] leading-[14px] bg-[#ccc] dark:bg-[#6B6B6B] rounded-md ${
|
||||
isSelected
|
||||
? "shadow-[-6px_0px_6px_2px_#950599]"
|
||||
: "shadow-[-6px_0px_6px_2px_#fff] dark:shadow-[-6px_0px_6px_2px_#000]"
|
||||
}`}
|
||||
>
|
||||
{currentIndex}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5,7 +5,12 @@ import {
|
||||
CornerDownLeft,
|
||||
} from "lucide-react";
|
||||
|
||||
import logoImg from "@/assets/32x32.png";
|
||||
import logoImg from "@/assets/app-icon.png";
|
||||
import source_default_img from "@/assets/images/source_default.png";
|
||||
import source_default_dark_img from "@/assets/images/source_default_dark.png";
|
||||
import { useSearchStore } from "@/stores/searchStore";
|
||||
import { useAppStore } from "@/stores/appStore";
|
||||
import { useTheme } from "@/contexts/ThemeContext";
|
||||
|
||||
interface FooterProps {
|
||||
isChat: boolean;
|
||||
@@ -13,16 +18,58 @@ interface FooterProps {
|
||||
}
|
||||
|
||||
export default function Footer({ name }: FooterProps) {
|
||||
const sourceData = useSearchStore((state) => state.sourceData);
|
||||
const connector_data = useAppStore((state) => state.connector_data);
|
||||
const datasourceData = useAppStore((state) => state.datasourceData);
|
||||
const endpoint_http = useAppStore((state) => state.endpoint_http);
|
||||
|
||||
const { theme } = useTheme();
|
||||
|
||||
function findConnectorIcon(item: any) {
|
||||
const id = item?._source?.source?.id || "";
|
||||
|
||||
const result_source = datasourceData.find(
|
||||
(data: any) => data._source.id === id
|
||||
);
|
||||
|
||||
const connector_id = result_source?._source?.connector?.id;
|
||||
|
||||
const result_connector = connector_data.find(
|
||||
(data: any) => data._source.id === connector_id
|
||||
);
|
||||
|
||||
return result_connector?._source;
|
||||
}
|
||||
|
||||
function getTypeIcon(item: any) {
|
||||
const connectorSource = findConnectorIcon(item);
|
||||
const icons = connectorSource?.icon;
|
||||
|
||||
if (!icons) {
|
||||
return theme === "dark" ? source_default_dark_img : source_default_img;
|
||||
}
|
||||
|
||||
if (icons?.includes("http")) {
|
||||
return icons;
|
||||
} else {
|
||||
return endpoint_http + icons;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
data-tauri-drag-region
|
||||
className="px-4 z-999 mx-[1px] h-10 absolute bottom-0 left-0 right-0 border-t border-gray-200 dark:border-gray-700 flex items-center justify-between rounded-xl rounded-t-none overflow-hidden"
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<div className="flex items-center space-x-4">
|
||||
<img src={logoImg} className="w-5 h-5" />
|
||||
<div className="flex items-center space-x-2">
|
||||
{sourceData?._source?.source?.name ? (
|
||||
<img className="w-5 h-5" src={getTypeIcon(sourceData)} alt="icon" />
|
||||
) : (
|
||||
<img src={logoImg} className="w-5 h-5" />
|
||||
)}
|
||||
<span className="text-xs text-gray-500 dark:text-gray-400">
|
||||
Version 1.0.0
|
||||
{sourceData?._source?.source?.name || "Version 1.0.0"}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -36,12 +83,18 @@ export default function Footer({ name }: FooterProps) {
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="gap-1 flex items-center text-[#666] dark:text-[#666] text-sm">
|
||||
<span className="mr-1.5 ">Quick open</span>
|
||||
<Command className="w-5 h-5 p-1 border rounded-[6px] dark:text-[#666] dark:border-[rgba(255,255,255,0.15)]" />
|
||||
<ArrowDown01 className="w-5 h-5 p-1 border rounded-[6px] dark:text-[#666] dark:border-[rgba(255,255,255,0.15)]" />
|
||||
<kbd className="docsearch-modal-footer-commands-key pr-1">
|
||||
<Command className="w-3 h-3" />
|
||||
</kbd>
|
||||
<kbd className="docsearch-modal-footer-commands-key pr-1">
|
||||
<ArrowDown01 className="w-3 h-3" />
|
||||
</kbd>
|
||||
</div>
|
||||
<div className="flex items-center text-[#666] dark:text-[#666] text-sm">
|
||||
<span className="mr-1.5 ">Open</span>
|
||||
<CornerDownLeft className="w-5 h-5 p-1 border rounded-[6px] dark:text-[#666] dark:border-[rgba(255,255,255,0.15)]" />
|
||||
<kbd className="docsearch-modal-footer-commands-key pr-1">
|
||||
<CornerDownLeft className="w-3 h-3" />
|
||||
</kbd>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,12 @@
|
||||
import { Library, Mic, Send, Plus, AudioLines, Image } from "lucide-react";
|
||||
import {
|
||||
Library,
|
||||
Mic,
|
||||
Send,
|
||||
Plus,
|
||||
AudioLines,
|
||||
Image,
|
||||
SquareArrowLeft,
|
||||
} from "lucide-react";
|
||||
import { useRef, useState, useEffect, useCallback } from "react";
|
||||
import { listen } from "@tauri-apps/api/event";
|
||||
import { isTauri } from "@tauri-apps/api/core";
|
||||
@@ -8,6 +16,7 @@ import AutoResizeTextarea from "./AutoResizeTextarea";
|
||||
import { useChatStore } from "@/stores/chatStore";
|
||||
import StopIcon from "@/icons/Stop";
|
||||
import { useAppStore } from "@/stores/appStore";
|
||||
import { useSearchStore } from "@/stores/searchStore";
|
||||
|
||||
interface ChatInputProps {
|
||||
onSend: (message: string) => void;
|
||||
@@ -32,6 +41,13 @@ export default function ChatInput({
|
||||
}: ChatInputProps) {
|
||||
const showTooltip = useAppStore((state) => state.showTooltip);
|
||||
|
||||
const sourceData = useSearchStore((state) => state.sourceData);
|
||||
const setSourceData = useSearchStore((state) => state.setSourceData);
|
||||
|
||||
useEffect(() => {
|
||||
setSourceData(undefined);
|
||||
}, []);
|
||||
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const textareaRef = useRef<{ reset: () => void; focus: () => void }>(null);
|
||||
|
||||
@@ -65,11 +81,14 @@ export default function ChatInput({
|
||||
case "KeyI":
|
||||
handleToggleFocus();
|
||||
break;
|
||||
case "ArrowLeft":
|
||||
setSourceData(undefined);
|
||||
break;
|
||||
case "KeyM":
|
||||
console.log("KeyM");
|
||||
break;
|
||||
case "Enter":
|
||||
isChatMode && handleSubmit();
|
||||
isChatMode && (curChatEnd ? handleSubmit() : disabledChange());
|
||||
break;
|
||||
case "KeyO":
|
||||
console.log("KeyO");
|
||||
@@ -139,7 +158,7 @@ export default function ChatInput({
|
||||
|
||||
const [countdown, setCountdown] = useState(5);
|
||||
useEffect(() => {
|
||||
if (connected) return
|
||||
if (connected) return;
|
||||
if (countdown <= 0) {
|
||||
ReconnectClick();
|
||||
return;
|
||||
@@ -153,14 +172,21 @@ export default function ChatInput({
|
||||
}, [countdown, connected]);
|
||||
|
||||
const ReconnectClick = () => {
|
||||
setCountdown(5)
|
||||
reconnect()
|
||||
}
|
||||
setCountdown(5);
|
||||
reconnect();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-full relative">
|
||||
<div className="p-[12px] flex items-center dark:text-[#D8D8D8] bg-[#ededed] dark:bg-[#202126] rounded transition-all relative">
|
||||
<div className="p-2 flex items-center dark:text-[#D8D8D8] bg-[#ededed] dark:bg-[#202126] rounded transition-all relative">
|
||||
<div className="flex flex-wrap gap-2 flex-1 items-center relative">
|
||||
{!isChatMode && sourceData ? (
|
||||
<SquareArrowLeft
|
||||
className="w-4 h-4 text-[#000] dark:text-[#d8d8d8] cursor-pointer"
|
||||
onClick={() => setSourceData(undefined)}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{isChatMode ? (
|
||||
<AutoResizeTextarea
|
||||
ref={textareaRef}
|
||||
@@ -190,11 +216,20 @@ export default function ChatInput({
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{showTooltip && isCommandPressed && !isChatMode && sourceData ? (
|
||||
<div
|
||||
className={`absolute left-0 w-4 h-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_#ededed] dark:shadow-[-6px_0px_6px_2px_#202126]`}
|
||||
>
|
||||
←
|
||||
</div>
|
||||
) : null}
|
||||
{showTooltip && isCommandPressed ? (
|
||||
<div
|
||||
className={`absolute bg-black bg-opacity-70 text-white font-bold px-2 py-0.5 rounded-md text-xs transition-opacity duration-200`}
|
||||
className={`absolute ${
|
||||
!isChatMode && sourceData ? "left-7" : ""
|
||||
} w-4 h-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_#ededed] dark:shadow-[-6px_0px_6px_2px_#202126]`}
|
||||
>
|
||||
⌘ + I
|
||||
I
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
@@ -237,28 +272,31 @@ export default function ChatInput({
|
||||
|
||||
{showTooltip && isChatMode && isCommandPressed ? (
|
||||
<div
|
||||
className={`absolute right-16 bg-black bg-opacity-70 text-white font-bold px-2 py-0.5 rounded-md text-xs transition-opacity duration-200`}
|
||||
className={`absolute right-10 w-4 h-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]`}
|
||||
>
|
||||
⌘ + M
|
||||
M
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{showTooltip && isChatMode && isCommandPressed ? (
|
||||
<div
|
||||
className={`absolute right-1 bg-black bg-opacity-70 text-white font-bold px-2 py-0.5 rounded-md text-xs transition-opacity duration-200`}
|
||||
className={`absolute right-3 w-4 h-4 flex items-end 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]`}
|
||||
>
|
||||
⌘ + ↩︎
|
||||
↩︎
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{!connected && isChatMode ? (
|
||||
<div className="absolute top-0 right-0 bottom-0 left-0 px-2 py-4 bg-red-500/10 rounded-md font-normal text-xs text-gray-400 flex items-center gap-4">
|
||||
Unable to connect to the server
|
||||
<div className="w-[96px] h-[24px] bg-[#0061FF] rounded-[12px] font-normal text-xs text-white flex items-center justify-center cursor-pointer" onClick={ReconnectClick}>
|
||||
<div
|
||||
className="w-[96px] h-[24px] bg-[#0061FF] rounded-[12px] font-normal text-xs text-white flex items-center justify-center cursor-pointer"
|
||||
onClick={ReconnectClick}
|
||||
>
|
||||
Reconnect ({countdown})
|
||||
</div>
|
||||
</div>
|
||||
): null}
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<div
|
||||
@@ -280,16 +318,16 @@ export default function ChatInput({
|
||||
</button>
|
||||
{showTooltip && isCommandPressed ? (
|
||||
<div
|
||||
className={`absolute left-2 bg-black bg-opacity-70 text-white font-bold px-2 py-0.5 rounded-md text-xs transition-opacity duration-200`}
|
||||
className={`absolute left-2 w-4 h-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]`}
|
||||
>
|
||||
⌘ + O
|
||||
O
|
||||
</div>
|
||||
) : null}
|
||||
{showTooltip && isCommandPressed ? (
|
||||
<div
|
||||
className={`absolute left-16 bg-black bg-opacity-70 text-white font-bold px-2 py-0.5 rounded-md text-xs transition-opacity duration-200`}
|
||||
className={`absolute left-16 w-4 h-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]`}
|
||||
>
|
||||
⌘ + U
|
||||
U
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
@@ -306,27 +344,27 @@ export default function ChatInput({
|
||||
</button>
|
||||
{showTooltip && isCommandPressed ? (
|
||||
<div
|
||||
className={`absolute left-0 bg-black bg-opacity-70 text-white font-bold px-2 py-0.5 rounded-md text-xs transition-opacity duration-200`}
|
||||
className={`absolute left-0 w-4 h-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]`}
|
||||
>
|
||||
⌘ + N
|
||||
N
|
||||
</div>
|
||||
) : null}
|
||||
{showTooltip && isCommandPressed ? (
|
||||
<div
|
||||
className={`absolute left-14 bg-black bg-opacity-70 text-white font-bold px-2 py-0.5 rounded-md text-xs transition-opacity duration-200`}
|
||||
className={`absolute left-6 w-4 h-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]`}
|
||||
>
|
||||
⌘ + G
|
||||
G
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="relative w-24 flex justify-end items-center">
|
||||
<div className="relative w-16 flex justify-end items-center">
|
||||
{showTooltip && isCommandPressed ? (
|
||||
<div
|
||||
className={`absolute left-0 z-10 bg-black bg-opacity-70 text-white font-bold px-2 py-0.5 rounded-md text-xs transition-opacity duration-200`}
|
||||
className={`absolute left-1 z-10 w-4 h-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]`}
|
||||
>
|
||||
⌘ + T
|
||||
T
|
||||
</div>
|
||||
) : null}
|
||||
<ChatSwitch
|
||||
@@ -334,6 +372,7 @@ export default function ChatInput({
|
||||
onChange={(value) => {
|
||||
value && disabledChange();
|
||||
changeMode(value);
|
||||
setSourceData(undefined);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -8,6 +8,9 @@ import Footer from "./Footer";
|
||||
import { tauriFetch } from "@/api/tauriFetchClient";
|
||||
import noDataImg from "@/assets/coconut-tree.png";
|
||||
import { useAppStore } from "@/stores/appStore";
|
||||
// import { res_search } from "@/mock/index";
|
||||
import { SearchResults } from "../SearchChat/SearchResults";
|
||||
import { useSearchStore } from "@/stores/searchStore";
|
||||
|
||||
interface SearchProps {
|
||||
changeInput: (val: string) => void;
|
||||
@@ -18,8 +21,11 @@ interface SearchProps {
|
||||
function Search({ isChatMode, input }: SearchProps) {
|
||||
const appStore = useAppStore();
|
||||
|
||||
const sourceData = useSearchStore((state) => state.sourceData);
|
||||
|
||||
const [IsError, setIsError] = useState<boolean>(false);
|
||||
const [suggests, setSuggests] = useState<any[]>([]);
|
||||
const [SearchData, setSearchData] = useState<any>({});
|
||||
const [isSearchComplete, setIsSearchComplete] = useState(false);
|
||||
const [selectedItem, setSelectedItem] = useState<any>();
|
||||
|
||||
@@ -55,13 +61,18 @@ function Search({ isChatMode, input }: SearchProps) {
|
||||
const getSuggest = async () => {
|
||||
if (!input) return;
|
||||
//
|
||||
// const list = [];
|
||||
// for (let i = 0; i < input.length; i++) {
|
||||
// list.push({
|
||||
// _source: { url: `https://www.google.com/search?q=${i}`, _id: i },
|
||||
// });
|
||||
// }
|
||||
// mock
|
||||
// let list = res_search?.hits?.hits;
|
||||
// setSuggests(list);
|
||||
// const search_data = list.reduce((acc: any, item) => {
|
||||
// const name = item._source.source.name;
|
||||
// if (!acc[name]) {
|
||||
// acc[name] = [];
|
||||
// }
|
||||
// acc[name].push(item);
|
||||
// return acc;
|
||||
// }, {});
|
||||
// setSearchData(search_data);
|
||||
// return;
|
||||
//
|
||||
try {
|
||||
@@ -72,10 +83,20 @@ function Search({ isChatMode, input }: SearchProps) {
|
||||
});
|
||||
|
||||
console.log("_suggest", input, response);
|
||||
const data = response.data?.hits?.hits || [];
|
||||
let data = response.data?.hits?.hits || [];
|
||||
setSuggests(data);
|
||||
setIsError(false);
|
||||
const search_data = data.reduce((acc: any, item: any) => {
|
||||
const name = item?._source?.source?.name;
|
||||
if (!acc[name]) {
|
||||
acc[name] = [];
|
||||
}
|
||||
acc[name].push(item);
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
setSearchData(search_data);
|
||||
|
||||
setIsError(false);
|
||||
setIsSearchComplete(true);
|
||||
} catch (error) {
|
||||
setSuggests([]);
|
||||
@@ -95,7 +116,7 @@ function Search({ isChatMode, input }: SearchProps) {
|
||||
const debouncedSearch = useCallback(debounce(getSuggest, 300), [input]);
|
||||
|
||||
useEffect(() => {
|
||||
!isChatMode && debouncedSearch();
|
||||
!isChatMode && !sourceData && debouncedSearch();
|
||||
if (!input) setSuggests([]);
|
||||
}, [input]);
|
||||
|
||||
@@ -103,12 +124,18 @@ function Search({ isChatMode, input }: SearchProps) {
|
||||
<div ref={mainWindowRef} className={`h-[500px] pb-10 w-full relative`}>
|
||||
{/* Search Results Panel */}
|
||||
{suggests.length > 0 ? (
|
||||
<DropdownList
|
||||
suggests={suggests}
|
||||
IsError={IsError}
|
||||
isSearchComplete={isSearchComplete}
|
||||
selected={(item) => setSelectedItem(item)}
|
||||
/>
|
||||
sourceData ? (
|
||||
<SearchResults input={input} isChatMode={isChatMode} />
|
||||
) : (
|
||||
<DropdownList
|
||||
suggests={suggests}
|
||||
SearchData={SearchData}
|
||||
IsError={IsError}
|
||||
isSearchComplete={isSearchComplete}
|
||||
isChatMode={isChatMode}
|
||||
selected={(item) => setSelectedItem(item)}
|
||||
/>
|
||||
)
|
||||
) : (
|
||||
<div
|
||||
data-tauri-drag-region
|
||||
|
||||
@@ -34,7 +34,7 @@ const ChatSwitch: React.FC<ChatSwitchProps> = ({ isChatMode, onChange }) => {
|
||||
role="switch"
|
||||
aria-checked={isChatMode}
|
||||
className={`relative flex items-center justify-between w-10 h-[18px] rounded-full cursor-pointer transition-colors duration-300 ${
|
||||
isChatMode ? "bg-[#0072ff]" : "bg-[#6000FF]"
|
||||
isChatMode ? "bg-[#0072ff]" : "bg-[#950599]"
|
||||
}`}
|
||||
onClick={handleToggle}
|
||||
>
|
||||
|
||||
@@ -1,78 +1,138 @@
|
||||
import React from "react";
|
||||
import { Calendar, User, Clock } from "lucide-react";
|
||||
|
||||
import { useAppStore } from "@/stores/appStore";
|
||||
import {formatter} from "@/utils/index"
|
||||
import source_default_img from "@/assets/images/source_default.png";
|
||||
import source_default_dark_img from "@/assets/images/source_default_dark.png";
|
||||
import { useTheme } from "@/contexts/ThemeContext";
|
||||
|
||||
interface DocumentDetailProps {
|
||||
documentId?: string;
|
||||
document: any;
|
||||
}
|
||||
|
||||
export const DocumentDetail: React.FC<DocumentDetailProps> = ({
|
||||
documentId,
|
||||
}) => {
|
||||
if (!documentId) {
|
||||
return (
|
||||
<div className="h-full flex items-center justify-center text-gray-400 dark:text-gray-500">
|
||||
请选择一个文档查看详情
|
||||
</div>
|
||||
export const DocumentDetail: React.FC<DocumentDetailProps> = ({ document }) => {
|
||||
const connector_data = useAppStore((state) => state.connector_data);
|
||||
const datasourceData = useAppStore((state) => state.datasourceData);
|
||||
const endpoint_http = useAppStore((state) => state.endpoint_http);
|
||||
|
||||
const { theme } = useTheme();
|
||||
|
||||
function findConnectorIcon(item: any) {
|
||||
const id = item?._source?.source?.id || "";
|
||||
|
||||
const result_source = datasourceData.find(
|
||||
(data: any) => data._source.id === id
|
||||
);
|
||||
|
||||
const connector_id = result_source?._source?.connector?.id;
|
||||
|
||||
const result_connector = connector_data.find(
|
||||
(data: any) => data._source.id === connector_id
|
||||
);
|
||||
|
||||
return result_connector?._source;
|
||||
}
|
||||
|
||||
function getTypeIcon(item: any) {
|
||||
const connectorSource = findConnectorIcon(item);
|
||||
const icons = connectorSource?.icon;
|
||||
|
||||
if (!icons) {
|
||||
return theme === "dark" ? source_default_dark_img : source_default_img;
|
||||
}
|
||||
|
||||
if (icons?.includes("http")) {
|
||||
return icons;
|
||||
} else {
|
||||
return endpoint_http + icons;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="p-8 space-y-8">
|
||||
<div className="space-y-6">
|
||||
<h2 className="text-2xl font-semibold text-gray-900 dark:text-gray-100">
|
||||
产品需求规划文档
|
||||
</h2>
|
||||
|
||||
<div>
|
||||
<div className="flex items-center gap-6 text-sm text-gray-500 dark:text-gray-400">
|
||||
<div className="flex items-center gap-2">
|
||||
<Calendar className="w-4 h-4" />
|
||||
<span>2024-02-20</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<User className="w-4 h-4" />
|
||||
<span>张小明</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-6 text-sm text-gray-500 dark:text-gray-400">
|
||||
<div className="flex items-center gap-2">
|
||||
<Clock className="w-4 h-4" />
|
||||
<span>最近更新于 2小时前</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-4">
|
||||
<div className="font-normal text-xs text-[#666] dark:text-[#999] mb-2">
|
||||
Details
|
||||
</div>
|
||||
|
||||
<img
|
||||
{/* <div className="mb-4">
|
||||
<iframe
|
||||
src={document?._source?.metadata?.web_view_link}
|
||||
style={{ width: "100%", height: "500px" }}
|
||||
title="Text Preview"
|
||||
/>
|
||||
</div> */}
|
||||
|
||||
{/* <img
|
||||
src="https://images.unsplash.com/photo-1664575602276-acd073f104c1"
|
||||
alt="Document preview"
|
||||
className="w-full aspect-video object-cover rounded-xl shadow-md"
|
||||
/>
|
||||
/> */}
|
||||
|
||||
<div className="prose prose-gray dark:prose-invert max-w-none">
|
||||
<h3 className="text-lg font-medium text-gray-900 dark:text-gray-100">
|
||||
文档概述
|
||||
</h3>
|
||||
<p className="text-gray-600 dark:text-gray-300 leading-relaxed">
|
||||
本文档详细说明了2024年Q1的产品规划方向和具体功能需求。包含了用户研究结果、
|
||||
竞品分析、功能优先级排序等重要内容。产品团队可以基于此文档进行后续的设计和开发工作。
|
||||
</p>
|
||||
<div className="py-4 mt-4">
|
||||
<div className="flex justify-between flex-wrap font-normal text-xs mb-2.5">
|
||||
<div className="text-[#666]">Name</div>
|
||||
<div className="text-[#333] dark:text-[#D8D8D8] text-right w-60 break-words">
|
||||
{document?._source?.title || "-"}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3 className="text-lg font-medium text-gray-900 dark:text-gray-100 mt-6">
|
||||
主要内容
|
||||
</h3>
|
||||
<ul className="list-disc pl-4 text-gray-600 dark:text-gray-300 space-y-2">
|
||||
<li>用户痛点分析与解决方案</li>
|
||||
<li>核心功能详细说明</li>
|
||||
<li>交互流程设计</li>
|
||||
<li>技术可行性评估</li>
|
||||
<li>项目时间节点规划</li>
|
||||
</ul>
|
||||
|
||||
<p className="text-gray-600 dark:text-gray-300 leading-relaxed mt-6">
|
||||
通过实施本文档中规划的功能,我们期望能够提升用户体验,增强产品竞争力,
|
||||
实现Q1的业务增长目标。
|
||||
</p>
|
||||
<div className="flex justify-between flex-wrap font-normal text-xs mb-2.5">
|
||||
<div className="text-[#666]">Source</div>
|
||||
<div className="text-[#333] dark:text-[#D8D8D8] flex justify-end text-right w-56 break-words">
|
||||
<img
|
||||
className="w-4 h-4 mr-1"
|
||||
src={getTypeIcon(document)}
|
||||
alt="icon"
|
||||
/>
|
||||
{document?._source?.source?.name || "-"}
|
||||
</div>
|
||||
</div>
|
||||
{/* <div className="flex justify-between font-normal text-xs mb-2.5">
|
||||
<div className="text-[#666]">Where</div>
|
||||
<div className="text-[#333] dark:text-[#D8D8D8] text-right w-56 break-words">
|
||||
-
|
||||
</div>
|
||||
</div> */}
|
||||
{document?._source?.updated ? (
|
||||
<div className="flex justify-between flex-wrap font-normal text-xs mb-2.5">
|
||||
<div className="text-[#666]">Updated at</div>
|
||||
<div className="text-[#333] dark:text-[#D8D8D8] text-right w-56 break-words">
|
||||
{document?._source?.updated || "-"}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
{document?._source?.last_updated_by?.user?.username ? (
|
||||
<div className="flex justify-between flex-wrap font-normal text-xs mb-2.5">
|
||||
<div className="text-[#666]">Update by</div>
|
||||
<div className="text-[#333] dark:text-[#D8D8D8] text-right w-56 break-words">
|
||||
{document?._source?.last_updated_by?.user?.username || "-"}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
{document?._source?.owner?.username ? (
|
||||
<div className="flex justify-between flex-wrap font-normal text-xs mb-2.5">
|
||||
<div className="text-[#666]">Created by</div>
|
||||
<div className="text-[#333] dark:text-[#D8D8D8] text-right w-56 break-words">
|
||||
{document?._source?.owner?.username || "-"}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
{document?._source?.type ? (
|
||||
<div className="flex justify-between flex-wrap font-normal text-xs mb-2.5">
|
||||
<div className="text-[#666]">Type</div>
|
||||
<div className="text-[#333] dark:text-[#D8D8D8] text-right w-56 break-words">
|
||||
{document?._source?.type || "-"}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
{document?._source?.size ? (
|
||||
<div className="flex justify-between flex-wrap font-normal text-xs mb-2.5">
|
||||
<div className="text-[#666]">Size</div>
|
||||
<div className="text-[#333] dark:text-[#D8D8D8] text-right w-56 break-words">
|
||||
{formatter.bytes(document?._source?.size || 0)}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,91 +1,269 @@
|
||||
import React from 'react';
|
||||
import { FileText, Image, FileCode, Users, User, Globe } from 'lucide-react';
|
||||
import React, { useState, useRef, useEffect } from "react";
|
||||
import { useInfiniteScroll } from "ahooks";
|
||||
import { isTauri } from "@tauri-apps/api/core";
|
||||
import { open } from "@tauri-apps/plugin-shell";
|
||||
|
||||
interface Document {
|
||||
id: string;
|
||||
title: string;
|
||||
type: 'text' | 'image' | 'code';
|
||||
owner: 'personal' | 'team' | 'public';
|
||||
description: string;
|
||||
date: string;
|
||||
}
|
||||
|
||||
const documents: Document[] = [
|
||||
{
|
||||
id: '1',
|
||||
title: '产品需求规划文档.doc',
|
||||
type: 'text',
|
||||
owner: 'team',
|
||||
description: '2024年Q1产品规划及功能需求文档,包含详细的功能描述和交互设计说明。',
|
||||
date: '2024-02-20'
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
title: 'UI设计规范.fig',
|
||||
type: 'image',
|
||||
owner: 'public',
|
||||
description: '最新的设计系统规范文档,包含组件库使用说明和设计标准。',
|
||||
date: '2024-02-19'
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
title: 'API接口文档.ts',
|
||||
type: 'code',
|
||||
owner: 'personal',
|
||||
description: 'TypeScript版本的API接口定义文档,包含所有接口的请求和响应类型。',
|
||||
date: '2024-02-18'
|
||||
},
|
||||
];
|
||||
|
||||
const getIcon = (type: Document['type']) => {
|
||||
switch (type) {
|
||||
case 'image':
|
||||
return <Image className="w-5 h-5 text-blue-500 dark:text-blue-400" />;
|
||||
case 'code':
|
||||
return <FileCode className="w-5 h-5 text-green-500 dark:text-green-400" />;
|
||||
default:
|
||||
return <FileText className="w-5 h-5 text-purple-500 dark:text-purple-400" />;
|
||||
}
|
||||
};
|
||||
|
||||
const getOwnerIcon = (owner: Document['owner']) => {
|
||||
switch (owner) {
|
||||
case 'team':
|
||||
return <Users className="w-4 h-4 text-blue-500 dark:text-blue-400" />;
|
||||
case 'public':
|
||||
return <Globe className="w-4 h-4 text-green-500 dark:text-green-400" />;
|
||||
default:
|
||||
return <User className="w-4 h-4 text-gray-500 dark:text-gray-400" />;
|
||||
}
|
||||
};
|
||||
import { useAppStore } from "@/stores/appStore";
|
||||
import { tauriFetch } from "@/api/tauriFetchClient";
|
||||
import { useSearchStore } from "@/stores/searchStore";
|
||||
import { SearchHeader } from "./SearchHeader";
|
||||
import file_efault_img from "@/assets/images/file_efault.png";
|
||||
import noDataImg from "@/assets/coconut-tree.png";
|
||||
|
||||
interface DocumentListProps {
|
||||
onSelectDocument: (id: string) => void;
|
||||
getDocDetail: (detail: any) => void;
|
||||
input: string;
|
||||
isChatMode: boolean;
|
||||
selectedId?: string;
|
||||
}
|
||||
|
||||
export const DocumentList: React.FC<DocumentListProps> = ({ onSelectDocument, selectedId }) => {
|
||||
const PAGE_SIZE = 20;
|
||||
|
||||
export const DocumentList: React.FC<DocumentListProps> = ({
|
||||
input,
|
||||
getDocDetail,
|
||||
isChatMode,
|
||||
}) => {
|
||||
const connector_data = useAppStore((state) => state.connector_data);
|
||||
const datasourceData = useAppStore((state) => state.datasourceData);
|
||||
const sourceData = useSearchStore((state) => state.sourceData);
|
||||
const endpoint_http = useAppStore((state) => state.endpoint_http);
|
||||
|
||||
const [selectedItem, setSelectedItem] = useState<number | null>(null);
|
||||
const [total, setTotal] = useState(0);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const itemRefs = useRef<(HTMLDivElement | null)[]>([]);
|
||||
|
||||
const { data, loading } = useInfiniteScroll(
|
||||
async (d) => {
|
||||
const page = d ? Math.ceil(d.list.length / PAGE_SIZE) + 1 : 1;
|
||||
|
||||
const from = (page - 1) * PAGE_SIZE;
|
||||
|
||||
let url = `/query/_search?query=${input}&datasource=${sourceData?._source?.source?.id}&from=${from}&size=${PAGE_SIZE}`;
|
||||
|
||||
if (sourceData?._source?.rich_categories) {
|
||||
url = `/query/_search?query=${input}&rich_category=${sourceData?._source?.rich_categories[0]?.key}&from=${from}&size=${PAGE_SIZE}`;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await tauriFetch({
|
||||
url,
|
||||
method: "GET",
|
||||
});
|
||||
|
||||
const list = response.data?.hits?.hits || [];
|
||||
const total = response.data?.hits?.total?.value || 0;
|
||||
|
||||
console.log("doc", url, response.data?.hits)
|
||||
|
||||
setTotal(total);
|
||||
|
||||
getDocDetail(list[0] || {});
|
||||
|
||||
return {
|
||||
list,
|
||||
hasMore: from + list.length < total,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch documents:", error);
|
||||
return {
|
||||
list: [],
|
||||
hasMore: false,
|
||||
};
|
||||
}
|
||||
},
|
||||
{
|
||||
target: containerRef,
|
||||
isNoMore: (d) => (d?.list.length || 0) >= total,
|
||||
reloadDeps: [input, JSON.stringify(sourceData)],
|
||||
onBefore: () => {
|
||||
setTimeout(() => {
|
||||
const parentRef = containerRef.current;
|
||||
if (parentRef && parentRef.childElementCount > 10) {
|
||||
const itemHeight = (parentRef.firstChild as HTMLElement)?.offsetHeight || 80;
|
||||
parentRef.scrollTo({
|
||||
top: (parentRef.lastChild as HTMLElement)?.offsetTop - itemHeight,
|
||||
behavior: 'instant',
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
onFinally: (data) => onFinally(data, containerRef),
|
||||
}
|
||||
);
|
||||
|
||||
const onFinally = (data: any, ref: any) => {
|
||||
if (data?.page === 1) return;
|
||||
const parentRef = ref.current;
|
||||
if (!parentRef) return;
|
||||
const itemHeight = parentRef.firstChild?.offsetHeight || 80;
|
||||
parentRef.scrollTo({
|
||||
top:
|
||||
parentRef.lastChild?.offsetTop - (data?.list?.length + 1) * itemHeight,
|
||||
behavior: 'instant',
|
||||
});
|
||||
};
|
||||
|
||||
function findConnectorIcon(item: any) {
|
||||
const id = item?._source?.source?.id || "";
|
||||
|
||||
const result_source = datasourceData.find(
|
||||
(data: any) => data._source.id === id
|
||||
);
|
||||
|
||||
const connector_id = result_source?._source?.connector?.id;
|
||||
|
||||
const result_connector = connector_data.find(
|
||||
(data: any) => data._source.id === connector_id
|
||||
);
|
||||
|
||||
return result_connector?._source;
|
||||
}
|
||||
|
||||
function getIcon(item: any) {
|
||||
const connectorSource = findConnectorIcon(item);
|
||||
const icons = connectorSource?.assets?.icons || {};
|
||||
|
||||
const selectedIcon = icons[item?._source?.icon];
|
||||
|
||||
if (!selectedIcon) {
|
||||
return file_efault_img;
|
||||
}
|
||||
|
||||
if (selectedIcon?.includes("http")) {
|
||||
return selectedIcon;
|
||||
} else {
|
||||
return endpoint_http + selectedIcon;
|
||||
}
|
||||
}
|
||||
|
||||
function onMouseEnter(index: number, item: any) {
|
||||
getDocDetail(item);
|
||||
setSelectedItem(index);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
setSelectedItem(null);
|
||||
}, [isChatMode, input]);
|
||||
|
||||
const handleOpenURL = async (url: string) => {
|
||||
if (!url) return;
|
||||
try {
|
||||
if (isTauri()) {
|
||||
await open(url);
|
||||
// console.log("URL opened in default browser");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to open URL:", error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (!data?.list?.length) return;
|
||||
|
||||
if (e.key === "ArrowUp") {
|
||||
e.preventDefault();
|
||||
setSelectedItem((prev) => (prev === null || prev === 0 ? 0 : prev - 1));
|
||||
} else if (e.key === "ArrowDown") {
|
||||
e.preventDefault();
|
||||
setSelectedItem((prev) =>
|
||||
prev === null ? 0 : prev === data?.list?.length - 1 ? prev : prev + 1
|
||||
);
|
||||
} else if (e.key === "Meta") {
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
if (e.key === "Enter" && selectedItem !== null) {
|
||||
const item = data?.list?.[selectedItem];
|
||||
if (item?._source?.url) {
|
||||
handleOpenURL(item?._source?.url);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener("keydown", handleKeyDown);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("keydown", handleKeyDown);
|
||||
};
|
||||
}, [selectedItem]);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedItem !== null && itemRefs.current[selectedItem]) {
|
||||
itemRefs.current[selectedItem]?.scrollIntoView({
|
||||
behavior: "smooth",
|
||||
block: "nearest",
|
||||
});
|
||||
}
|
||||
}, [selectedItem]);
|
||||
|
||||
return (
|
||||
<div className="space-y-1 py-2">
|
||||
{documents.map((doc) => (
|
||||
<button
|
||||
key={doc.id}
|
||||
onClick={() => onSelectDocument(doc.id)}
|
||||
className={`w-full flex items-start px-4 py-3 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700/50 transition-colors ${
|
||||
selectedId === doc.id ? 'bg-blue-50 dark:bg-blue-900/50' : ''
|
||||
}`}
|
||||
>
|
||||
<span className="mr-3 mt-0.5">{getIcon(doc.type)}</span>
|
||||
<div className="flex-1 text-left">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm font-medium text-gray-900 dark:text-gray-100">{doc.title}</span>
|
||||
<span className="mt-0.5">{getOwnerIcon(doc.owner)}</span>
|
||||
<div className="w-[50%] border-r border-gray-200 dark:border-gray-700 flex flex-col h-full">
|
||||
<div className="px-2 flex-shrink-0">
|
||||
<SearchHeader total={total} />
|
||||
</div>
|
||||
|
||||
<div
|
||||
ref={containerRef}
|
||||
className="flex-1 overflow-y-auto custom-scrollbar"
|
||||
>
|
||||
{data?.list.map((item: any, index: number) => {
|
||||
const isSelected = selectedItem === index;
|
||||
return (
|
||||
<div
|
||||
key={item._id + index}
|
||||
ref={(el) => (itemRefs.current[index] = el)}
|
||||
onMouseEnter={() => onMouseEnter(index, item)}
|
||||
onClick={() => {
|
||||
if (item?._source?.url) {
|
||||
handleOpenURL(item?._source?.url);
|
||||
}
|
||||
}}
|
||||
className={`w-full px-2 py-2.5 text-sm flex items-center gap-3 rounded-lg transition-colors cursor-pointer ${
|
||||
isSelected
|
||||
? "text-white bg-[#950599] hover:bg-[#950599]"
|
||||
: "text-[#333] dark:text-[#d8d8d8]"
|
||||
}`}
|
||||
>
|
||||
<div className="flex gap-2 items-center flex-1 min-w-0">
|
||||
<img
|
||||
className="w-5 h-5 flex-shrink-0"
|
||||
src={getIcon(item)}
|
||||
alt="icon"
|
||||
/>
|
||||
<span
|
||||
className={`text-sm truncate ${
|
||||
isSelected ? "font-medium" : ""
|
||||
}`}
|
||||
>
|
||||
{item?._source?.title}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1 line-clamp-2">{doc.description}</p>
|
||||
<span className="text-xs text-gray-400 dark:text-gray-500 mt-1 block">{doc.date}</span>
|
||||
);
|
||||
})}
|
||||
|
||||
{loading && (
|
||||
<div className="flex justify-center py-4">
|
||||
<span>Loading...</span>
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
)}
|
||||
|
||||
{!loading && data?.list.length === 0 && (
|
||||
<div
|
||||
data-tauri-drag-region
|
||||
className="h-full w-full flex flex-col items-center"
|
||||
>
|
||||
<img src={noDataImg} alt="no-data" className="w-16 h-16 mt-24" />
|
||||
<div className="mt-4 text-sm text-[#999] dark:text-[#666]">
|
||||
No Results
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -118,7 +118,7 @@ function Search({ isTransitioned, isChatMode, input }: SearchProps) {
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{selectedItem ? <SearchResults /> : null}
|
||||
{selectedItem ? <SearchResults input={input} isChatMode={isChatMode} /> : null}
|
||||
|
||||
{suggests.length > 0 || selectedItem ? (
|
||||
<Footer isChat={false} name={selectedItem?.source} />
|
||||
|
||||
@@ -58,15 +58,19 @@ const typeOptions: FilterOption[] = [
|
||||
{ id: "code", label: "Code" },
|
||||
];
|
||||
|
||||
export const SearchHeader: React.FC = () => {
|
||||
interface SearchHeaderProps {
|
||||
total: number;
|
||||
}
|
||||
|
||||
export const SearchHeader: React.FC<SearchHeaderProps> = ({ total }) => {
|
||||
const [typeFilter, setTypeFilter] = useState("all");
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-between py-1">
|
||||
<div className="text-xs text-gray-600 dark:text-gray-400">
|
||||
Find
|
||||
Found
|
||||
<span className="px-1 font-medium text-gray-900 dark:text-gray-100">
|
||||
200
|
||||
{total}
|
||||
</span>
|
||||
results
|
||||
</div>
|
||||
|
||||
@@ -1,32 +1,38 @@
|
||||
import React, { useState } from "react";
|
||||
import { SearchHeader } from "./SearchHeader";
|
||||
import { useState } from "react";
|
||||
import { DocumentList } from "./DocumentList";
|
||||
import { DocumentDetail } from "./DocumentDetail";
|
||||
|
||||
export const SearchResults: React.FC = () => {
|
||||
interface SearchResultsProps {
|
||||
input: string;
|
||||
isChatMode: boolean;
|
||||
}
|
||||
|
||||
export function SearchResults({ input, isChatMode }: SearchResultsProps) {
|
||||
const [selectedDocumentId, setSelectedDocumentId] = useState("1");
|
||||
|
||||
const [detailData, setDetailData] = useState<any>({});
|
||||
|
||||
function getDocDetail(detail: any) {
|
||||
setDetailData(detail)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="max-h-[458px] w-full p-2 flex flex-col rounded-xl overflow-y-auto overflow-hidden custom-scrollbar focus:outline-none">
|
||||
<div className="flex">
|
||||
<div className="h-[458px] w-full p-2 pr-0 flex flex-col rounded-xl focus:outline-none">
|
||||
<div className="h-full flex">
|
||||
{/* Left Panel */}
|
||||
<div className="w-[50%] border-r border-gray-200 dark:border-gray-700 flex flex-col overflow-hidden">
|
||||
<div className="px-4 flex-shrink-0">
|
||||
<SearchHeader />
|
||||
</div>
|
||||
<div className="overflow-y-auto flex-1 custom-scrollbar">
|
||||
<DocumentList
|
||||
onSelectDocument={setSelectedDocumentId}
|
||||
selectedId={selectedDocumentId}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<DocumentList
|
||||
onSelectDocument={setSelectedDocumentId}
|
||||
selectedId={selectedDocumentId}
|
||||
input={input}
|
||||
getDocDetail={getDocDetail}
|
||||
isChatMode={isChatMode}
|
||||
/>
|
||||
|
||||
{/* Right Panel */}
|
||||
<div className="flex-1 overflow-y-auto custom-scrollbar">
|
||||
<DocumentDetail documentId={selectedDocumentId} />
|
||||
<DocumentDetail document={detailData}/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
30
src/hooks/useInfiniteScroll.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { useEffect, useRef } from "react";
|
||||
|
||||
const useInfiniteScroll = (callback: () => void) => {
|
||||
const loaderRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
if (entries[0].isIntersecting) {
|
||||
callback();
|
||||
}
|
||||
},
|
||||
{ threshold: 1.0 }
|
||||
);
|
||||
|
||||
if (loaderRef.current) {
|
||||
observer.observe(loaderRef.current);
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (loaderRef.current) {
|
||||
observer.unobserve(loaderRef.current);
|
||||
}
|
||||
};
|
||||
}, [callback]);
|
||||
|
||||
return loaderRef;
|
||||
};
|
||||
|
||||
export default useInfiniteScroll;
|
||||
@@ -27,6 +27,19 @@ const RESERVED_SHORTCUTS = [
|
||||
["Command", "M"],
|
||||
["Command", "Enter"],
|
||||
["Command", "ArrowLeft"],
|
||||
["Command", "ArrowRight"],
|
||||
["Command", "ArrowUp"],
|
||||
["Command", "ArrowDown"],
|
||||
["Command", "0"],
|
||||
["Command", "1"],
|
||||
["Command", "2"],
|
||||
["Command", "3"],
|
||||
["Command", "4"],
|
||||
["Command", "5"],
|
||||
["Command", "6"],
|
||||
["Command", "7"],
|
||||
["Command", "8"],
|
||||
["Command", "9"],
|
||||
];
|
||||
|
||||
export function useShortcutEditor(shortcut: Shortcut, onChange: (shortcut: Shortcut) => void) {
|
||||
|
||||
55
src/main.css
@@ -7,12 +7,53 @@
|
||||
--background: #ffffff;
|
||||
--foreground: #09090b;
|
||||
--border: #e3e3e7;
|
||||
--docsearch-primary-color: rgb(149, 5, 153);
|
||||
--docsearch-text-color: rgb(28, 30, 33);
|
||||
--docsearch-spacing: 12px;
|
||||
--docsearch-icon-stroke-width: 1.4;
|
||||
--docsearch-highlight-color: var(--docsearch-primary-color);
|
||||
--docsearch-muted-color: rgb(150, 159, 175);
|
||||
--docsearch-modal-container-background: rgba(101, 108, 133, .8);
|
||||
--docsearch-modal-width: 560px;
|
||||
--docsearch-modal-height: 600px;
|
||||
--docsearch-modal-background: rgb(245, 246, 247);
|
||||
--docsearch-modal-shadow: inset 1px 1px 0 0 rgba(255, 255, 255, .5), 0 3px 8px 0 rgba(85, 90, 100, 1);
|
||||
--docsearch-searchbox-height: 56px;
|
||||
--docsearch-searchbox-background: rgb(235, 237, 240);
|
||||
--docsearch-searchbox-focus-background: #fff;
|
||||
--docsearch-searchbox-shadow: inset 0 0 0 2px var(--docsearch-primary-color);
|
||||
--docsearch-hit-height: 56px;
|
||||
--docsearch-hit-color: rgb(68, 73, 80);
|
||||
--docsearch-hit-active-color: #fff;
|
||||
--docsearch-hit-background: #fff;
|
||||
--docsearch-hit-shadow: 0 1px 3px 0 rgb(212, 217, 225);
|
||||
--docsearch-key-gradient: linear-gradient(-225deg, rgb(213, 219, 228) 0%, rgb(248, 248, 248) 100%);
|
||||
--docsearch-key-shadow: inset 0 -2px 0 0 rgb(205, 205, 230), inset 0 0 1px 1px #fff, 0 1px 2px 1px rgba(30, 35, 90, .4);
|
||||
--docsearch-footer-height: 44px;
|
||||
--docsearch-footer-background: #fff;
|
||||
--docsearch-footer-shadow: 0 -1px 0 0 rgb(224, 227, 232), 0 -3px 6px 0 rgba(69, 98, 155, .12);
|
||||
--docsearch-icon-color: rgb(21, 21, 21);
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: #09090b;
|
||||
--foreground: #f9f9f9;
|
||||
--border: #27272a;
|
||||
--docsearch-text-color: rgb(245, 246, 247);
|
||||
--docsearch-modal-container-background: rgba(9, 10, 17, .8);
|
||||
--docsearch-modal-background: rgb(21, 23, 42);
|
||||
--docsearch-modal-shadow: inset 1px 1px 0 0 rgb(44, 46, 64), 0 3px 8px 0 rgb(0, 3, 9);
|
||||
--docsearch-searchbox-background: rgb(9, 10, 17);
|
||||
--docsearch-searchbox-focus-background: #000;
|
||||
--docsearch-hit-color: rgb(190, 195, 201);
|
||||
--docsearch-hit-shadow: none;
|
||||
--docsearch-hit-background: rgb(9, 10, 17);
|
||||
--docsearch-key-gradient: linear-gradient(-26.5deg, rgb(86, 88, 114) 0%, rgb(49, 53, 91) 100%);
|
||||
--docsearch-key-shadow: inset 0 -2px 0 0 rgb(40, 45, 85), inset 0 0 1px 1px rgb(81, 87, 125), 0 2px 2px 0 rgba(3, 4, 9, .3);
|
||||
--docsearch-footer-background: rgb(30, 33, 54);
|
||||
--docsearch-footer-shadow: inset 0 1px 0 0 rgba(73, 76, 106, .5), 0 -4px 8px 0 rgba(0, 0, 0, .2);
|
||||
--docsearch-muted-color: rgb(127, 132, 151);
|
||||
--docsearch-icon-color: rgb(255, 255, 255);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,7 +73,7 @@
|
||||
}
|
||||
|
||||
.input-body {
|
||||
@apply rounded-xl overflow-hidden
|
||||
@apply rounded-xl overflow-hidden;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -142,6 +183,18 @@
|
||||
background-color: #f79c42;
|
||||
}
|
||||
|
||||
.docsearch-modal-footer-commands-key {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 4px;
|
||||
border: 0px;
|
||||
padding: 2px;
|
||||
background: var(--docsearch-key-gradient);
|
||||
/* box-shadow: var(--docsearch-key-shadow); */
|
||||
color: var(--docsearch-muted-color);
|
||||
}
|
||||
|
||||
.user-select{
|
||||
-webkit-touch-callout: none;
|
||||
-webkit-user-select: none;
|
||||
|
||||
263
src/mock/index.ts
Normal file
@@ -0,0 +1,263 @@
|
||||
// mock
|
||||
export const res_search = {
|
||||
took: 2590,
|
||||
timed_out: false,
|
||||
_shards: {
|
||||
total: 1,
|
||||
successful: 1,
|
||||
skipped: 0,
|
||||
failed: 0
|
||||
},
|
||||
hits: {
|
||||
total: {
|
||||
value: 253,
|
||||
relation: "eq"
|
||||
},
|
||||
max_score: 32.709457,
|
||||
hits: [
|
||||
{
|
||||
_index: "coco_document",
|
||||
_type: "_doc",
|
||||
_id: "3ac857ef30d101b1e5880b53b1438b1a",
|
||||
_score: 32.709457,
|
||||
_source: {
|
||||
icon: "web",
|
||||
id: "3ac857ef30d101b1e5880b53b1438b1a",
|
||||
source: {
|
||||
name: "hugo_site",
|
||||
type: "connector"
|
||||
},
|
||||
type: "web_page",
|
||||
category: "Aggregation",
|
||||
subcategory: "Metric",
|
||||
title: "Avg aggregation",
|
||||
content: "",
|
||||
author: "liaosy",
|
||||
url: "https://pizza.rs/docs/references/aggregation/avg/",
|
||||
tags: [
|
||||
"avg",
|
||||
"aggregation"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
_index: "coco_document",
|
||||
_type: "_doc",
|
||||
_id: "2485a744c5dae1278a01c04d39bf60a6",
|
||||
_score: 32.37022,
|
||||
_source: {
|
||||
icon: "web",
|
||||
id: "2485a744c5dae1278a01c04d39bf60a6",
|
||||
source: {
|
||||
name: "hugo_site",
|
||||
type: "connector"
|
||||
},
|
||||
type: "web_page",
|
||||
title: "auto_generate_doc_id",
|
||||
content: "",
|
||||
author: "liaosy",
|
||||
url: "https://infinilabs.cn/docs/latest/gateway/references/filters/auto_generate_doc_id/"
|
||||
}
|
||||
},
|
||||
{
|
||||
_index: "coco_document",
|
||||
_type: "_doc",
|
||||
_id: "15aa340fa9ddfcfbf793b8707a4fa16b",
|
||||
_score: 21.983166,
|
||||
_source: {
|
||||
icon: "web",
|
||||
id: "15aa340fa9ddfcfbf793b8707a4fa16b",
|
||||
source: {
|
||||
name: "hugo_site",
|
||||
type: "connector"
|
||||
},
|
||||
type: "web_page",
|
||||
category: "Overview",
|
||||
subcategory: "Architecture",
|
||||
title: "Architecture",
|
||||
content: "",
|
||||
author: "yangfan",
|
||||
url: "https://pizza.rs/docs/overview/architecture/",
|
||||
tags: [
|
||||
"architecture"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
_index: "coco_document",
|
||||
_type: "_doc",
|
||||
_id: "f96b1af318d62a43a44f54731409ff52",
|
||||
_score: 21.887964,
|
||||
_source: {
|
||||
icon: "web",
|
||||
id: "f96b1af318d62a43a44f54731409ff52",
|
||||
source: {
|
||||
name: "hugo_site",
|
||||
type: "connector"
|
||||
},
|
||||
type: "web_page",
|
||||
title: "Introducing Coco AI in Two Minutes - A Quick Start Video 🥥",
|
||||
content: "",
|
||||
author: "yangfan",
|
||||
url: "https://blog.infinilabs.com/posts/2024/a-quick-start-viideo-to-introduce-coco-ai-in-two-minutes/",
|
||||
tags: [
|
||||
"Coco AI",
|
||||
"Search",
|
||||
"Gen-AI",
|
||||
"Enterprise"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
_index: "coco_document",
|
||||
_type: "_doc",
|
||||
_id: "3a806937f9e7fe55905a7f71d111e523",
|
||||
_score: 21.286049,
|
||||
_source: {
|
||||
icon: "web",
|
||||
id: "3a806937f9e7fe55905a7f71d111e523",
|
||||
source: {
|
||||
name: "hugo_site",
|
||||
type: "connector"
|
||||
},
|
||||
type: "web_page",
|
||||
category: "Catalog",
|
||||
subcategory: "Namespace",
|
||||
title: "Create a namespace",
|
||||
content: "",
|
||||
author: "yangfan",
|
||||
url: "https://pizza.rs/docs/references/namespace/create/",
|
||||
tags: [
|
||||
"create",
|
||||
"namespace"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
_index: "coco_document",
|
||||
_type: "_doc",
|
||||
_id: "04f48643c2c52b872c149e077765f8cb",
|
||||
_score: 21.286049,
|
||||
_source: {
|
||||
icon: "web",
|
||||
id: "04f48643c2c52b872c149e077765f8cb",
|
||||
source: {
|
||||
name: "hugo_site",
|
||||
type: "connector"
|
||||
},
|
||||
type: "web_page",
|
||||
category: "Document",
|
||||
subcategory: "Index",
|
||||
title: "Create a document",
|
||||
content: "",
|
||||
author: "zouwenan",
|
||||
url: "https://pizza.rs/docs/references/document/create/",
|
||||
tags: [
|
||||
"create",
|
||||
"index"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
_index: "coco_document",
|
||||
_type: "_doc",
|
||||
_id: "d101818b1e6d2eb23ca2f813ef3a9648",
|
||||
_score: 21.213268,
|
||||
_source: {
|
||||
icon: "web",
|
||||
id: "d101818b1e6d2eb23ca2f813ef3a9648",
|
||||
source: {
|
||||
name: "hugo_site",
|
||||
type: "connector"
|
||||
},
|
||||
type: "web_page",
|
||||
category: "Catalog",
|
||||
subcategory: "Collection",
|
||||
title: "Create a collection",
|
||||
content: " ",
|
||||
author: "zouwenan",
|
||||
url: "https://pizza.rs/docs/references/collection/create/",
|
||||
tags: [
|
||||
"create",
|
||||
"collection"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
_index: "coco_document",
|
||||
_type: "_doc",
|
||||
_id: "ee2228755039808c199e6812d09c745e",
|
||||
_score: 20.967154,
|
||||
_source: {
|
||||
icon: "web",
|
||||
id: "ee2228755039808c199e6812d09c745e",
|
||||
source: {
|
||||
name: "hugo_site",
|
||||
type: "connector"
|
||||
},
|
||||
type: "web_page",
|
||||
category: "Document",
|
||||
subcategory: "Index",
|
||||
title: "Delete a document",
|
||||
content: "",
|
||||
author: "zouwenan",
|
||||
url: "https://pizza.rs/docs/references/document/delete/",
|
||||
tags: [
|
||||
"delete",
|
||||
"index"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
_index: "coco_document",
|
||||
_type: "_doc",
|
||||
_id: "60584d12aba0ff569e7b79a7be168810",
|
||||
_score: 20.967154,
|
||||
_source: {
|
||||
icon: "web",
|
||||
id: "60584d12aba0ff569e7b79a7be168810",
|
||||
source: {
|
||||
name: "hugo_site",
|
||||
type: "connector"
|
||||
},
|
||||
type: "web_page",
|
||||
category: "Catalog",
|
||||
subcategory: "Namespace",
|
||||
title: "Delete a namespace",
|
||||
content: "",
|
||||
author: "zouwenan",
|
||||
url: "https://pizza.rs/docs/references/namespace/delete/",
|
||||
tags: [
|
||||
"delete",
|
||||
"namespace"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
_index: "coco_document",
|
||||
_type: "_doc",
|
||||
_id: "73e31d0feeb8d3d97e4b06a98de54672",
|
||||
_score: 20.934437,
|
||||
_source: {
|
||||
icon: "web",
|
||||
id: "73e31d0feeb8d3d97e4b06a98de54672",
|
||||
source: {
|
||||
name: "hugo_site",
|
||||
type: "connector"
|
||||
},
|
||||
type: "web_page",
|
||||
category: "Document",
|
||||
subcategory: "Index",
|
||||
title: "Replace a document",
|
||||
content: " ",
|
||||
author: "medcl",
|
||||
url: "https://pizza.rs/docs/references/document/replace/",
|
||||
tags: [
|
||||
"replace",
|
||||
"index"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -99,7 +99,7 @@ export default function DesktopApp() {
|
||||
>
|
||||
<div
|
||||
data-tauri-drag-region
|
||||
className={`p-[7px] pb-0 absolute w-full flex items-center justify-center transition-all duration-500 ${
|
||||
className={`p-2 pb-0 absolute w-full flex items-center justify-center transition-all duration-500 ${
|
||||
isTransitioned
|
||||
? "top-[500px] h-[90px] border-t"
|
||||
: "top-0 h-[90px] border-b"
|
||||
@@ -123,7 +123,7 @@ export default function DesktopApp() {
|
||||
data-tauri-drag-region
|
||||
className={`absolute w-full transition-opacity duration-500 ${
|
||||
isTransitioned ? "opacity-0 pointer-events-none" : "opacity-100"
|
||||
} bottom-0 h-[500px] user-select`}
|
||||
} bottom-0 h-[500px] `}
|
||||
>
|
||||
<Search
|
||||
key="Search"
|
||||
@@ -141,7 +141,7 @@ export default function DesktopApp() {
|
||||
: "-top-[506px] opacity-0 pointer-events-none"
|
||||
} h-[500px]`}
|
||||
>
|
||||
{isTransitioned ? (
|
||||
{isTransitioned && isChatMode ? (
|
||||
<ChatAI
|
||||
ref={chatAIRef}
|
||||
key="ChatAI"
|
||||
|
||||
22
src/stores/searchStore.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { create } from "zustand";
|
||||
import { persist } from "zustand/middleware";
|
||||
|
||||
export type ISearchStore = {
|
||||
sourceData: any;
|
||||
setSourceData: (sourceData: any) => void;
|
||||
};
|
||||
|
||||
export const useSearchStore = create<ISearchStore>()(
|
||||
persist(
|
||||
(set) => ({
|
||||
sourceData: undefined,
|
||||
setSourceData: (sourceData: any) => set({ sourceData }),
|
||||
}),
|
||||
{
|
||||
name: "search-store",
|
||||
partialize: (state) => ({
|
||||
sourceData: state.sourceData,
|
||||
}),
|
||||
}
|
||||
)
|
||||
);
|
||||
@@ -81,3 +81,19 @@ export const authWitheGithub = (uid: string) => {
|
||||
|
||||
location.href = `${authorizeUrl}?client_id=${"Ov23li4IcdbbWp2RgLTN"}&redirect_uri=${"http://localhost:1420/login"}`;
|
||||
};
|
||||
|
||||
const unitArr = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"] as const;
|
||||
|
||||
export const formatter = {
|
||||
bytes: (value: number): string => {
|
||||
if (!Number.isFinite(value) || value <= 0) {
|
||||
return "0B";
|
||||
}
|
||||
|
||||
const index = Math.floor(Math.log(value) / Math.log(1024));
|
||||
const size = (value / Math.pow(1024, index)).toFixed(1);
|
||||
|
||||
return size + (unitArr[index] ?? "B")
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -47,6 +47,11 @@ export default defineConfig(async () => ({
|
||||
changeOrigin: true,
|
||||
secure: false,
|
||||
},
|
||||
"/connector": {
|
||||
target: process.env.COCO_SERVER_URL,
|
||||
changeOrigin: true,
|
||||
secure: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
build: {
|
||||
|
||||