feat: add search-chat

This commit is contained in:
rain
2024-10-28 17:34:48 +08:00
parent 0998da1cd9
commit 3052f54674
57 changed files with 3650 additions and 181 deletions

12
.editorconfig Normal file
View File

@@ -0,0 +1,12 @@
# EditorConfig is awesome: https://EditorConfig.org
# top-most EditorConfig file
root = true
[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = false
insert_final_newline = false

View File

@@ -4,7 +4,7 @@
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Tauri + React + Typescript</title>
<title>Coco</title>
</head>
<body>

View File

@@ -10,17 +10,32 @@
"tauri": "tauri"
},
"dependencies": {
"@headlessui/react": "^2.1.10",
"@tauri-apps/api": ">=2.0.0",
"@tauri-apps/plugin-http": "~2",
"@tauri-apps/plugin-shell": ">=2.0.0",
"clsx": "^2.1.1",
"i18next": "^23.16.2",
"lodash": "^4.17.21",
"lucide-react": "^0.453.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"@tauri-apps/api": ">=2.0.0",
"@tauri-apps/plugin-shell": ">=2.0.0"
"react-hotkeys-hook": "^4.5.1",
"react-i18next": "^15.1.0",
"react-router-dom": "^6.27.0",
"zustand": "^5.0.0"
},
"devDependencies": {
"@tauri-apps/cli": ">=2.0.0",
"@types/lodash": "^4.17.12",
"@types/react": "^18.2.15",
"@types/react-dom": "^18.2.7",
"@types/react-i18next": "^8.1.0",
"@vitejs/plugin-react": "^4.2.1",
"autoprefixer": "^10.4.20",
"postcss": "^8.4.47",
"tailwindcss": "^3.4.14",
"typescript": "^5.2.2",
"vite": "^5.3.1",
"@tauri-apps/cli": ">=2.0.0"
"vite": "^5.3.1"
}
}

1172
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

6
postcss.config.js Normal file
View File

@@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

380
src-tauri/Cargo.lock generated
View File

@@ -85,6 +85,12 @@ dependencies = [
"system-deps",
]
[[package]]
name = "atomic-waker"
version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
[[package]]
name = "autocfg"
version = "1.4.0"
@@ -341,6 +347,7 @@ dependencies = [
"serde_json",
"tauri",
"tauri-build",
"tauri-plugin-http",
"tauri-plugin-shell",
]
@@ -353,7 +360,7 @@ dependencies = [
"bitflags 2.6.0",
"block",
"cocoa-foundation",
"core-foundation",
"core-foundation 0.10.0",
"core-graphics",
"foreign-types",
"libc",
@@ -368,7 +375,7 @@ checksum = "e14045fb83be07b5acf1c0884b2180461635b433455fa35d1cd6f17f1450679d"
dependencies = [
"bitflags 2.6.0",
"block",
"core-foundation",
"core-foundation 0.10.0",
"core-graphics-types",
"libc",
"objc",
@@ -390,6 +397,44 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e"
[[package]]
name = "cookie"
version = "0.18.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747"
dependencies = [
"percent-encoding",
"time",
"version_check",
]
[[package]]
name = "cookie_store"
version = "0.21.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4934e6b7e8419148b6ef56950d277af8561060b56afd59e2aadf98b59fce6baa"
dependencies = [
"cookie",
"idna 0.5.0",
"log",
"publicsuffix",
"serde",
"serde_derive",
"serde_json",
"time",
"url",
]
[[package]]
name = "core-foundation"
version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f"
dependencies = [
"core-foundation-sys",
"libc",
]
[[package]]
name = "core-foundation"
version = "0.10.0"
@@ -413,7 +458,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fa95a34622365fa5bbf40b20b75dba8dfa8c94c734aea8ac9a5ca38af14316f1"
dependencies = [
"bitflags 2.6.0",
"core-foundation",
"core-foundation 0.10.0",
"core-graphics-types",
"foreign-types",
"libc",
@@ -426,7 +471,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb"
dependencies = [
"bitflags 2.6.0",
"core-foundation",
"core-foundation 0.10.0",
"libc",
]
@@ -545,6 +590,12 @@ dependencies = [
"syn 2.0.79",
]
[[package]]
name = "data-url"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c297a1c74b71ae29df00c3e22dd9534821d60eb9af5a0192823fa2acea70c2a"
[[package]]
name = "deranged"
version = "0.3.11"
@@ -1165,6 +1216,25 @@ dependencies = [
"syn 2.0.79",
]
[[package]]
name = "h2"
version = "0.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "524e8ac6999421f49a846c2d4411f337e53497d8ec55d67753beffa43c5d9205"
dependencies = [
"atomic-waker",
"bytes",
"fnv",
"futures-core",
"futures-sink",
"http",
"indexmap 2.6.0",
"slab",
"tokio",
"tokio-util",
"tracing",
]
[[package]]
name = "hashbrown"
version = "0.12.3"
@@ -1264,6 +1334,7 @@ dependencies = [
"bytes",
"futures-channel",
"futures-util",
"h2",
"http",
"http-body",
"httparse",
@@ -1274,6 +1345,24 @@ dependencies = [
"want",
]
[[package]]
name = "hyper-rustls"
version = "0.27.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08afdbb5c31130e3034af566421053ab03787c640246a446327f550d11bcb333"
dependencies = [
"futures-util",
"http",
"hyper",
"hyper-util",
"rustls",
"rustls-pki-types",
"tokio",
"tokio-rustls",
"tower-service",
"webpki-roots",
]
[[package]]
name = "hyper-util"
version = "0.1.9"
@@ -1332,6 +1421,16 @@ version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
[[package]]
name = "idna"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e14ddfc70884202db2244c223200c204c2bda1bc6e0998d11b5e024d657209e6"
dependencies = [
"unicode-bidi",
"unicode-normalization",
]
[[package]]
name = "idna"
version = "0.5.0"
@@ -2355,6 +2454,22 @@ dependencies = [
"unicode-ident",
]
[[package]]
name = "psl-types"
version = "2.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33cb294fe86a74cbcf50d4445b37da762029549ebeea341421c7c70370f86cac"
[[package]]
name = "publicsuffix"
version = "2.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96a8c1bda5ae1af7f99a2962e49df150414a43d62404644d98dd5c3a93d07457"
dependencies = [
"idna 0.3.0",
"psl-types",
]
[[package]]
name = "quick-xml"
version = "0.32.0"
@@ -2364,6 +2479,54 @@ dependencies = [
"memchr",
]
[[package]]
name = "quinn"
version = "0.11.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8c7c5fdde3cdae7203427dc4f0a68fe0ed09833edc525a03456b153b79828684"
dependencies = [
"bytes",
"pin-project-lite",
"quinn-proto",
"quinn-udp",
"rustc-hash",
"rustls",
"socket2",
"thiserror",
"tokio",
"tracing",
]
[[package]]
name = "quinn-proto"
version = "0.11.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fadfaed2cd7f389d0161bb73eeb07b7b78f8691047a6f3e73caaeae55310a4a6"
dependencies = [
"bytes",
"rand 0.8.5",
"ring",
"rustc-hash",
"rustls",
"slab",
"thiserror",
"tinyvec",
"tracing",
]
[[package]]
name = "quinn-udp"
version = "0.5.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4fe68c2e9e1a1234e218683dbdf9f9dfcb094113c5ac2b938dfcb9bab4c4140b"
dependencies = [
"libc",
"once_cell",
"socket2",
"tracing",
"windows-sys 0.59.0",
]
[[package]]
name = "quote"
version = "1.0.37"
@@ -2517,12 +2680,17 @@ checksum = "f713147fbe92361e52392c73b8c9e48c04c6625bce969ef54dc901e58e042a7b"
dependencies = [
"base64 0.22.1",
"bytes",
"cookie",
"cookie_store",
"encoding_rs",
"futures-core",
"futures-util",
"h2",
"http",
"http-body",
"http-body-util",
"hyper",
"hyper-rustls",
"hyper-util",
"ipnet",
"js-sys",
@@ -2531,11 +2699,17 @@ dependencies = [
"once_cell",
"percent-encoding",
"pin-project-lite",
"quinn",
"rustls",
"rustls-pemfile",
"rustls-pki-types",
"serde",
"serde_json",
"serde_urlencoded",
"sync_wrapper",
"system-configuration",
"tokio",
"tokio-rustls",
"tokio-util",
"tower-service",
"url",
@@ -2543,15 +2717,37 @@ dependencies = [
"wasm-bindgen-futures",
"wasm-streams",
"web-sys",
"webpki-roots",
"windows-registry",
]
[[package]]
name = "ring"
version = "0.17.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d"
dependencies = [
"cc",
"cfg-if",
"getrandom 0.2.15",
"libc",
"spin",
"untrusted",
"windows-sys 0.52.0",
]
[[package]]
name = "rustc-demangle"
version = "0.1.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f"
[[package]]
name = "rustc-hash"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "583034fd73374156e66797ed8e5b0d5690409c9226b22d87cb7f19821c05d152"
[[package]]
name = "rustc_version"
version = "0.4.1"
@@ -2561,6 +2757,46 @@ dependencies = [
"semver",
]
[[package]]
name = "rustls"
version = "0.23.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5fbb44d7acc4e873d613422379f69f237a1b141928c02f6bc6ccfddddc2d7993"
dependencies = [
"once_cell",
"ring",
"rustls-pki-types",
"rustls-webpki",
"subtle",
"zeroize",
]
[[package]]
name = "rustls-pemfile"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50"
dependencies = [
"rustls-pki-types",
]
[[package]]
name = "rustls-pki-types"
version = "1.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "16f1201b3c9a7ee8039bcadc17b7e605e2945b27eee7631788c1bd2b0643674b"
[[package]]
name = "rustls-webpki"
version = "0.102.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9"
dependencies = [
"ring",
"rustls-pki-types",
"untrusted",
]
[[package]]
name = "ryu"
version = "1.0.18"
@@ -2898,6 +3134,12 @@ dependencies = [
"system-deps",
]
[[package]]
name = "spin"
version = "0.9.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
[[package]]
name = "stable_deref_trait"
version = "1.2.0"
@@ -2936,6 +3178,12 @@ version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]]
name = "subtle"
version = "2.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
[[package]]
name = "swift-rs"
version = "1.0.7"
@@ -2978,6 +3226,27 @@ dependencies = [
"futures-core",
]
[[package]]
name = "system-configuration"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b"
dependencies = [
"bitflags 2.6.0",
"core-foundation 0.9.4",
"system-configuration-sys",
]
[[package]]
name = "system-configuration-sys"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4"
dependencies = [
"core-foundation-sys",
"libc",
]
[[package]]
name = "system-deps"
version = "6.2.2"
@@ -2999,7 +3268,7 @@ checksum = "a0dbbebe82d02044dfa481adca1550d6dd7bd16e086bc34fa0fbecceb5a63751"
dependencies = [
"bitflags 2.6.0",
"cocoa",
"core-foundation",
"core-foundation 0.10.0",
"core-graphics",
"crossbeam-channel",
"dispatch",
@@ -3049,9 +3318,9 @@ checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1"
[[package]]
name = "tauri"
version = "2.0.3"
version = "2.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fd96d46534b10765ce0c6208f9451d98ea38636364a41b272d3610c70dd0e4c3"
checksum = "5ce2818e803ce3097987296623ed8c0d9f65ed93b4137ff9a83e168bdbf62932"
dependencies = [
"anyhow",
"bytes",
@@ -3177,6 +3446,49 @@ dependencies = [
"walkdir",
]
[[package]]
name = "tauri-plugin-fs"
version = "2.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96ba7d46e86db8c830d143ef90ab5a453328365b0cc834c24edea4267b16aba0"
dependencies = [
"anyhow",
"dunce",
"glob",
"percent-encoding",
"schemars",
"serde",
"serde_json",
"serde_repr",
"tauri",
"tauri-plugin",
"thiserror",
"url",
"uuid",
]
[[package]]
name = "tauri-plugin-http"
version = "2.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c752aee1b00ec3c4d4f440095995d9bd2c640b478f2067d1fba388900b82eb96"
dependencies = [
"data-url",
"http",
"regex",
"reqwest",
"schemars",
"serde",
"serde_json",
"tauri",
"tauri-plugin",
"tauri-plugin-fs",
"thiserror",
"tokio",
"url",
"urlpattern",
]
[[package]]
name = "tauri-plugin-shell"
version = "2.0.1"
@@ -3219,9 +3531,9 @@ dependencies = [
[[package]]
name = "tauri-runtime-wry"
version = "2.1.0"
version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aaac63b65df8e85570993eaf93ae1dd73a6fb66d8bd99674ce65f41dc3c63e7d"
checksum = "1431602bcc71f2f840ad623915c9842ecc32999b867c4a787d975a17a9625cc6"
dependencies = [
"gtk",
"http",
@@ -3384,9 +3696,32 @@ dependencies = [
"mio",
"pin-project-lite",
"socket2",
"tokio-macros",
"windows-sys 0.52.0",
]
[[package]]
name = "tokio-macros"
version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.79",
]
[[package]]
name = "tokio-rustls"
version = "0.26.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c7bc40d0e5a97695bb96e27995cd3a08538541b0a846f65bba7a359f36700d4"
dependencies = [
"rustls",
"rustls-pki-types",
"tokio",
]
[[package]]
name = "tokio-util"
version = "0.7.12"
@@ -3591,6 +3926,12 @@ version = "1.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493"
[[package]]
name = "untrusted"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
[[package]]
name = "url"
version = "2.5.2"
@@ -3598,7 +3939,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c"
dependencies = [
"form_urlencoded",
"idna",
"idna 0.5.0",
"percent-encoding",
"serde",
]
@@ -3828,6 +4169,15 @@ dependencies = [
"system-deps",
]
[[package]]
name = "webpki-roots"
version = "0.26.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "841c67bff177718f1d4dfefde8d8f0e78f9b6589319ba88312f567fc5841a958"
dependencies = [
"rustls-pki-types",
]
[[package]]
name = "webview2-com"
version = "0.33.0"
@@ -4237,9 +4587,9 @@ dependencies = [
[[package]]
name = "wry"
version = "0.46.0"
version = "0.46.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "469a3765ecc3e8aa9ccdf3c5a52c82697ec03037cd60494488763880d31a1b3a"
checksum = "cd5cdf57c66813d97601181349c63b96994b3074fc3d7a31a8cce96e968e3bbd"
dependencies = [
"base64 0.22.1",
"block2",
@@ -4317,3 +4667,9 @@ dependencies = [
"quote",
"syn 2.0.79",
]
[[package]]
name = "zeroize"
version = "1.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde"

View File

@@ -22,6 +22,7 @@ tauri = { version = "2.0.0", features = [] }
tauri-plugin-shell = "2.0.0"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
tauri-plugin-http = "2"
[profile.dev]
incremental = true # Compile your binary in smaller steps.
@@ -31,4 +32,4 @@ codegen-units = 1 # Allows LLVM to perform better optimization.
lto = true # Enables link-time-optimizations.
opt-level = "s" # Prioritizes small binary size. Use `3` if you prefer speed.
panic = "abort" # Higher performance by disabling panic handlers.
strip = true # Ensures debug symbols are removed.
strip = true # Ensures debug symbols are removed.

View File

@@ -5,6 +5,16 @@
"windows": ["main"],
"permissions": [
"core:default",
"shell:allow-open"
"shell:allow-open",
"http:default",
"http:allow-fetch",
"http:allow-fetch-cancel",
"http:allow-fetch-read-body",
"http:allow-fetch-send",
{
"identifier": "http:default",
"allow": [{ "url": "https://*.tauri.app" }],
"deny": [{ "url": "https://private.tauri.app" }]
}
]
}

View File

@@ -7,6 +7,7 @@ fn greet(name: &str) -> String {
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.plugin(tauri_plugin_http::init())
.plugin(tauri_plugin_shell::init())
.invoke_handler(tauri::generate_handler![greet])
.run(tauri::generate_context!())

View File

@@ -15,7 +15,6 @@
"title": "coco",
"width": 800,
"height": 600
}
],
"security": {
@@ -32,5 +31,8 @@
"icons/icon.icns",
"icons/icon.ico"
]
},
"plugins": {
"window": {}
}
}

View File

@@ -1,116 +0,0 @@
.logo.vite:hover {
filter: drop-shadow(0 0 2em #747bff);
}
.logo.react:hover {
filter: drop-shadow(0 0 2em #61dafb);
}
:root {
font-family: Inter, Avenir, Helvetica, Arial, sans-serif;
font-size: 16px;
line-height: 24px;
font-weight: 400;
color: #0f0f0f;
background-color: #f6f6f6;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
-webkit-text-size-adjust: 100%;
}
.container {
margin: 0;
padding-top: 10vh;
display: flex;
flex-direction: column;
justify-content: center;
text-align: center;
}
.logo {
height: 6em;
padding: 1.5em;
will-change: filter;
transition: 0.75s;
}
.logo.tauri:hover {
filter: drop-shadow(0 0 2em #24c8db);
}
.row {
display: flex;
justify-content: center;
}
a {
font-weight: 500;
color: #646cff;
text-decoration: inherit;
}
a:hover {
color: #535bf2;
}
h1 {
text-align: center;
}
input,
button {
border-radius: 8px;
border: 1px solid transparent;
padding: 0.6em 1.2em;
font-size: 1em;
font-weight: 500;
font-family: inherit;
color: #0f0f0f;
background-color: #ffffff;
transition: border-color 0.25s;
box-shadow: 0 2px 2px rgba(0, 0, 0, 0.2);
}
button {
cursor: pointer;
}
button:hover {
border-color: #396cd8;
}
button:active {
border-color: #396cd8;
background-color: #e8e8e8;
}
input,
button {
outline: none;
}
#greet-input {
margin-right: 5px;
}
@media (prefers-color-scheme: dark) {
:root {
color: #f6f6f6;
background-color: #2f2f2f;
}
a:hover {
color: #24c8db;
}
input,
button {
color: #ffffff;
background-color: #0f0f0f98;
}
button:active {
background-color: #0f0f0f69;
}
}

View File

@@ -1,51 +1,33 @@
import { useState } from "react";
import reactLogo from "./assets/react.svg";
import { invoke } from "@tauri-apps/api/core";
import "./App.css";
import { useTranslation } from "react-i18next";
import useEscape from "./hooks/useEscape";
import "./i18n";
import Header from "./components/Header";
import Chat from "./components/Chat";
import Raycast from "./components/Raycast";
import Footer from "./components/Footer";
function App() {
const [greetMsg, setGreetMsg] = useState("");
const [name, setName] = useState("");
const { t } = useTranslation();
async function greet() {
// Learn more about Tauri commands at https://tauri.app/v1/guides/features/command
setGreetMsg(await invoke("greet", { name }));
}
useEscape();
return (
<div className="container">
<h1>Welcome to Tauri!</h1>
<div
className={`
bg-background
w-screen h-screen
`}
>
{/* <Header /> */}
<main className="w-[100%] h-[100%] flex flex-col items-center justify-center">
<div className="text-xl text-primary">{t("welcome")}</div>
<div className="row">
<a href="https://vitejs.dev" target="_blank">
<img src="/vite.svg" className="logo vite" alt="Vite logo" />
</a>
<a href="https://tauri.app" target="_blank">
<img src="/tauri.svg" className="logo tauri" alt="Tauri logo" />
</a>
<a href="https://reactjs.org" target="_blank">
<img src={reactLogo} className="logo react" alt="React logo" />
</a>
</div>
<p>Click on the Tauri, Vite, and React logos to learn more.</p>
<form
className="row"
onSubmit={(e) => {
e.preventDefault();
greet();
}}
>
<input
id="greet-input"
onChange={(e) => setName(e.currentTarget.value)}
placeholder="Enter a name..."
/>
<button type="submit">Greet</button>
</form>
<p>{greetMsg}</p>
<div className="mx-0 mt-5 w-[100%]">
<Raycast />
</div>
</main>
<Footer />
</div>
);
}

105
src/components/Chat.tsx Normal file
View File

@@ -0,0 +1,105 @@
import { useState, useEffect, useRef } from "react";
// import { invoke } from "@tauri-apps/api/core";
import { useHotkeys } from "react-hotkeys-hook";
import debounce from "lodash/debounce";
import { Textarea } from "@headlessui/react";
import clsx from "clsx";
import { useTranslation } from "react-i18next";
import { fetch } from "@tauri-apps/plugin-http";
import SendIcon from "../icons/Send";
export default function ChatInput() {
const { t } = useTranslation();
const inputRef = useRef<HTMLTextAreaElement>(null);
const [message, setMessage] = useState("");
const [info, setInfo] = useState("");
const isMac = true;
useEffect(() => {
const syncMessage = debounce(async () => {
try {
// await invoke("ask_sync", { message: JSON.stringify(message) });
} catch (error) {
console.error("Error syncing message:", error);
}
}, 300); // Debounce by 300ms
syncMessage();
return () => syncMessage.cancel(); // Cleanup debounce on unmount
}, [message]);
useHotkeys(
isMac ? "meta+enter" : "ctrl+enter",
async (event: KeyboardEvent) => {
event.preventDefault();
await handleSend();
},
{
enableOnFormTags: true,
},
[message]
);
const handleInput = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
setInfo("");
setMessage(e.target.value);
};
const handleSend = async () => {
if (!message) return;
try {
// await invoke("ask_send", { message: JSON.stringify(message) });
// Send a GET request
const response = await fetch("https://test.tauri.app/data.json", {
method: "GET",
});
setInfo(JSON.stringify(response));
console.log(response.status); // e.g. 200
console.log(response.statusText); // e.g. "OK"
} catch (error) {
console.error("Error sending message:", error);
setInfo(JSON.stringify(error));
}
setMessage("");
if (inputRef.current) {
inputRef.current.value = "";
inputRef.current.focus();
}
};
return (
<div className="w-[100%] h-[100%]">
<div>{info}</div>
<div className="relative flex dark:bg-app-gray-2/[0.98] dark:text-slate-200 items-center gap-1">
<Textarea
ref={inputRef}
onChange={handleInput}
spellCheck="false"
autoFocus
className={clsx(
"mt-3 block w-full resize-none rounded-xl border border-transparent bg-white/10",
"py-3 px-4 text-sm text-black placeholder-gray-500 shadow-md",
// Transition for smoother appearance changes
"transition-colors duration-300 ease-in-out",
// Dark mode styles
"dark:bg-gray-800 dark:text-white dark:placeholder-gray-400",
"focus:border-blue-400 focus:ring-2 focus:ring-blue-400 focus:ring-opacity-50",
"dark:focus:border-white dark:focus:ring-white/25",
"focus:outline-none"
)}
placeholder={t("InputMessage")}
/>
<SendIcon
size={30}
className="absolute right-2 text-gray-400/80 dark:text-gray-600 cursor-pointer"
onClick={handleSend}
title={`Send message (${isMac ? "⌘⏎" : "⌃⏎"})`}
aria-label="Send message"
/>
</div>
</div>
);
}

View File

@@ -0,0 +1,189 @@
import React, { useState, useEffect, useCallback } from "react";
import {
Search,
Settings,
Calculator,
Calendar,
Mail,
Music,
User,
} from "lucide-react";
interface CommandItem {
id: string;
icon: React.ReactNode;
title: string;
description: string;
action: () => void;
}
export function CommandPalette() {
const [isOpen, setIsOpen] = useState(false);
const [search, setSearch] = useState("");
const [selectedIndex, setSelectedIndex] = useState(0);
const commands: CommandItem[] = [
{
id: "settings",
icon: <Settings className="w-5 h-5" />,
title: "Settings",
description: "Adjust your preferences",
action: () => console.log("Settings clicked"),
},
{
id: "calculator",
icon: <Calculator className="w-5 h-5" />,
title: "Calculator",
description: "Perform quick calculations",
action: () => console.log("Calculator clicked"),
},
{
id: "calendar",
icon: <Calendar className="w-5 h-5" />,
title: "Calendar",
description: "View your schedule",
action: () => console.log("Calendar clicked"),
},
{
id: "mail",
icon: <Mail className="w-5 h-5" />,
title: "Mail",
description: "Check your inbox",
action: () => console.log("Mail clicked"),
},
{
id: "music",
icon: <Music className="w-5 h-5" />,
title: "Music",
description: "Control playback",
action: () => console.log("Music clicked"),
},
{
id: "profile",
icon: <User className="w-5 h-5" />,
title: "Profile",
description: "View your profile",
action: () => console.log("Profile clicked"),
},
];
const filteredCommands = commands.filter(
(command) =>
command.title.toLowerCase().includes(search.toLowerCase()) ||
command.description.toLowerCase().includes(search.toLowerCase())
);
const handleKeyDown = useCallback(
(e: KeyboardEvent) => {
if (e.key === "k" && (e.metaKey || e.ctrlKey)) {
e.preventDefault();
setIsOpen((prev) => !prev);
} else if (e.key === "Escape") {
setIsOpen(false);
} else if (e.key === "ArrowDown") {
e.preventDefault();
setSelectedIndex((prev) =>
prev < filteredCommands.length - 1 ? prev + 1 : prev
);
} else if (e.key === "ArrowUp") {
e.preventDefault();
setSelectedIndex((prev) => (prev > 0 ? prev - 1 : prev));
} else if (e.key === "Enter" && filteredCommands[selectedIndex]) {
filteredCommands[selectedIndex].action();
setIsOpen(false);
}
},
[filteredCommands, selectedIndex]
);
useEffect(() => {
document.addEventListener("keydown", handleKeyDown);
return () => document.removeEventListener("keydown", handleKeyDown);
}, [handleKeyDown]);
useEffect(() => {
setSelectedIndex(0);
}, [search]);
if (!isOpen) return null;
return (
<div className="fixed inset-0 z-50 overflow-y-auto">
<div className="min-h-screen px-4 text-center">
<div
className="fixed inset-0 bg-black/50 transition-opacity"
onClick={() => setIsOpen(false)}
/>
<div className="inline-block w-full max-w-2xl my-16 text-left align-middle transition-all transform">
<div className="relative bg-white rounded-xl shadow-2xl overflow-hidden">
<div className="flex items-center px-4 border-b border-gray-200">
<Search className="w-5 h-5 text-gray-400" />
<input
autoFocus
type="text"
className="w-full px-4 py-4 text-gray-700 bg-transparent border-none focus:outline-none focus:ring-0"
placeholder="Search commands..."
value={search}
onChange={(e) => setSearch(e.target.value)}
/>
<div className="w-32 flex items-center gap-1">
<kbd className="px-2 py-1 text-xs font-semibold text-gray-500 bg-gray-100 border border-gray-200 rounded-md">
Esc
</kbd>
<span className="text-gray-400">to close</span>
</div>
</div>
<div className="max-h-[60vh] overflow-y-auto">
{filteredCommands.length === 0 ? (
<div className="px-4 py-14 text-center text-gray-500">
No results found.
</div>
) : (
<div className="py-2">
{filteredCommands.map((command, index) => (
<div
key={command.id}
className={`px-4 py-3 flex items-center gap-3 cursor-pointer transition-colors ${
selectedIndex === index
? "bg-blue-50 text-blue-700"
: "text-gray-700 hover:bg-gray-50"
}`}
onClick={() => {
command.action();
setIsOpen(false);
}}
>
<div
className={`${
selectedIndex === index
? "text-blue-600"
: "text-gray-400"
}`}
>
{command.icon}
</div>
<div>
<div className="font-medium">{command.title}</div>
<div
className={`text-sm ${
selectedIndex === index
? "text-blue-500"
: "text-gray-500"
}`}
>
{command.description}
</div>
</div>
</div>
))}
</div>
)}
</div>
</div>
</div>
</div>
</div>
);
}

95
src/components/Footer.tsx Normal file
View File

@@ -0,0 +1,95 @@
import { Menu, MenuButton, MenuItems, MenuItem } from "@headlessui/react";
import { Command, Settings, LogOut, User, ChevronUp, Home } from "lucide-react";
import { Link } from "react-router-dom";
const Footer = () => {
return (
<div className="fixed bottom-0 left-0 right-0 bg-white dark:bg-gray-800 border-t dark:border-gray-700">
<div className="max-w-6xl mx-auto px-4 h-14 flex items-center justify-between">
<Menu as="div" className="relative">
<MenuButton className="flex items-center space-x-2 px-3 py-1.5 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors">
<Command className="w-5 h-5 text-gray-600 dark:text-gray-400" />
<span className="text-sm font-medium text-gray-700 dark:text-gray-300">
Coco
</span>
<ChevronUp className="w-4 h-4 text-gray-500 dark:text-gray-400" />
</MenuButton>
<MenuItems className="absolute bottom-full mb-2 left-0 w-64 rounded-lg bg-white dark:bg-gray-800 shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none">
<div className="p-1">
<MenuItem>
{({ active }) => (
<button
className={`${
active
? "bg-gray-100 dark:bg-gray-700"
: "text-gray-900 dark:text-gray-100"
} group flex w-full items-center rounded-md px-3 py-2 text-sm`}
>
<Home className="w-4 h-4 mr-2" />
<Link to={`/`}>Home</Link>
</button>
)}
</MenuItem>
<MenuItem>
{({ active }) => (
<button
className={`${
active
? "bg-gray-100 dark:bg-gray-700"
: "text-gray-900 dark:text-gray-100"
} group flex w-full items-center rounded-md px-3 py-2 text-sm`}
>
<User className="w-4 h-4 mr-2" />
Profile
</button>
)}
</MenuItem>
<MenuItem>
{({ active }) => (
<button
className={`${
active
? "bg-gray-100 dark:bg-gray-700"
: "text-gray-900 dark:text-gray-100"
} group flex w-full items-center rounded-md px-3 py-2 text-sm`}
>
<Settings className="w-4 h-4 mr-2" />
<Link to={`settings`}>Settings</Link>
</button>
)}
</MenuItem>
<div className="h-px bg-gray-200 dark:bg-gray-700 my-1" />
<MenuItem>
{({ active }) => (
<button
className={`${
active
? "bg-gray-100 dark:bg-gray-700"
: "text-gray-900 dark:text-gray-100"
} group flex w-full items-center rounded-md px-3 py-2 text-sm`}
>
<LogOut className="w-4 h-4 mr-2" />
Sign Out
</button>
)}
</MenuItem>
</div>
</MenuItems>
</Menu>
<div className="flex items-center space-x-4">
<span className="text-xs text-gray-500 dark:text-gray-400">
Version 1.0.0
</span>
<div className="h-4 w-px bg-gray-200 dark:bg-gray-700" />
<button className="text-xs text-gray-500 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white transition-colors">
Check for Updates
</button>
</div>
</div>
</div>
);
};
export default Footer;

19
src/components/Header.tsx Normal file
View File

@@ -0,0 +1,19 @@
import ThemeToggle from "./ThemeToggle";
import LangToggle from "./LangToggle";
export default function Header() {
return (
<div className="fixed w-[100%] h-10 px-4 flex justify-between items-center">
<div>Coco</div>
<div className="flex items-center gap-2">
<LangToggle />
<ThemeToggle />
<img
className="h-5 w-5 rounded-full ring-2 ring-white"
src="https://images.unsplash.com/photo-1491528323818-fdd1faba62cc?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80"
alt=""
/>
</div>
</div>
);
}

View File

@@ -0,0 +1,55 @@
import { Menu, MenuButton, MenuItem, MenuItems } from "@headlessui/react";
import { ChevronDown, Globe2 } from "lucide-react";
import { useState } from "react";
import { useTranslation } from "react-i18next";
interface Language {
code: string;
name: string;
flag: string;
keyboard: string;
}
const languages: Language[] = [
{ code: "en", name: "English", flag: "🇺🇸", keyboard: "E" },
{ code: "zh", name: "中文", flag: "🇨🇳", keyboard: "Z" },
];
export default function LangToggle() {
const { i18n } = useTranslation();
const [currentLng, setCurrentLng] = useState(languages[0]);
const changeLanguage = (lng: Language) => {
setCurrentLng(lng);
i18n.changeLanguage(lng.code);
};
return (
<Menu>
<MenuButton className="inline-flex items-center gap-2 rounded-md py-1.5 px-3 text-sm/6 font-semibold dark:text-white shadow-inner dark:shadow-white/10 focus:outline-none dark:data-[hover]:bg-gray-700 dark:data-[open]:bg-gray-700 data-[focus]:outline-1 dark:data-[focus]:outline-white">
<Globe2 className="h-4 w-4 text-gray-600" />
<span className="text-base">{currentLng.flag}</span>
<span>{currentLng.name}</span>
<ChevronDown className="size-4 dark:fill-white/60" />
</MenuButton>
<MenuItems
anchor="bottom end"
className="w-[160px] origin-top-right rounded-xl border dark:border-white/5 dark:bg-white/5 p-1 text-sm/6 dark:text-white transition duration-100 ease-out [--anchor-gap:var(--spacing-1)] focus:outline-none data-[closed]:scale-95 data-[closed]:opacity-0"
>
{languages.map((language) => (
<MenuItem key={language.code}>
<button
className="group flex w-full items-center gap-2 rounded-lg py-1.5 px-3 dark:data-[focus]:bg-white/10"
onClick={() => changeLanguage(language)}
>
<span className="mr-1 text-base">{language.flag}</span>
<span>{language.name}</span>
<kbd className="ml-auto hidden font-sans text-xs dark:text-white/50 group-data-[focus]:inline">
{language.keyboard}
</kbd>
</button>
</MenuItem>
))}
</MenuItems>
</Menu>
);
}

View File

@@ -0,0 +1,28 @@
import { Command } from "lucide-react";
import { CommandPalette } from "./CommandPalette";
function Raycast() {
return (
<div className="h-[100%] w-[100%]">
<div className="mx-auto px-4 py-16">
<div className="text-center">
<div className="inline-flex items-center gap-2 bg-white px-4 py-2 rounded-lg shadow-sm border border-gray-200">
<Command className="w-5 h-5 text-gray-500" />
<span className="text-gray-600">Press</span>
<kbd className="px-2 py-1 text-sm font-semibold text-gray-700 bg-gray-100 border border-gray-200 rounded-md">
</kbd>
<kbd className="px-2 py-1 text-sm font-semibold text-gray-700 bg-gray-100 border border-gray-200 rounded-md">
K
</kbd>
<span className="text-gray-600">to open command palette</span>
</div>
</div>
</div>
<CommandPalette />
</div>
);
}
export default Raycast;

View File

@@ -0,0 +1,71 @@
import React from "react";
import { Calendar, User, Clock } from "lucide-react";
interface DocumentDetailProps {
documentId?: string;
}
export const DocumentDetail: React.FC<DocumentDetailProps> = ({
documentId,
}) => {
if (!documentId) {
return (
<div className="h-full flex items-center justify-center text-gray-400">
</div>
);
}
return (
<div className="p-8 space-y-8">
<div className="space-y-6">
<h2 className="text-2xl font-semibold text-gray-900">
</h2>
<div className="flex items-center gap-6 text-sm text-gray-500">
<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 className="flex items-center gap-2">
<Clock className="w-4 h-4" />
<span> 2</span>
</div>
</div>
</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 max-w-none">
<h3 className="text-lg font-medium text-gray-900"></h3>
<p className="text-gray-600 leading-relaxed">
2024Q1的产品规划方向和具体功能需求
</p>
<h3 className="text-lg font-medium text-gray-900 mt-6"></h3>
<ul className="list-disc pl-4 text-gray-600 space-y-2">
<li></li>
<li></li>
<li></li>
<li></li>
<li></li>
</ul>
<p className="text-gray-600 leading-relaxed mt-6">
Q1的业务增长目标
</p>
</div>
</div>
);
};

View File

@@ -0,0 +1,100 @@
import React from "react";
import { FileText, Image, FileCode, Users, User, Globe } from "lucide-react";
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" />;
case "code":
return <FileCode className="w-5 h-5 text-green-500" />;
default:
return <FileText className="w-5 h-5 text-purple-500" />;
}
};
const getOwnerIcon = (owner: Document["owner"]) => {
switch (owner) {
case "team":
return <Users className="w-4 h-4 text-blue-500" />;
case "public":
return <Globe className="w-4 h-4 text-green-500" />;
default:
return <User className="w-4 h-4 text-gray-500" />;
}
};
interface DocumentListProps {
onSelectDocument: (id: string) => void;
selectedId?: string;
}
export const DocumentList: React.FC<DocumentListProps> = ({
onSelectDocument,
selectedId,
}) => {
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 transition-colors ${
selectedId === doc.id ? "bg-blue-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">
{doc.title}
</span>
<span className="mt-0.5">{getOwnerIcon(doc.owner)}</span>
</div>
<p className="text-xs text-gray-500 mt-1 line-clamp-2">
{doc.description}
</p>
<span className="text-xs text-gray-400 mt-1 block">{doc.date}</span>
</div>
</button>
))}
</div>
);
};

View File

@@ -0,0 +1,106 @@
import React, { useState, Fragment } from "react";
import { Menu, MenuButton, MenuItems, MenuItem } from "@headlessui/react";
import { ChevronDown } from "lucide-react";
interface FilterOption {
id: string;
label: string;
}
interface FilterDropdownProps {
label: string;
options: FilterOption[];
value?: string;
onChange: (value: string) => void;
}
const FilterDropdown: React.FC<FilterDropdownProps> = ({
label,
options,
value,
onChange,
}) => {
return (
<Menu as="div" className="relative text-xs">
<MenuButton className="inline-flex items-center px-3 py-1.5 text-sm bg-white text-gray-700 hover:bg-gray-50 rounded-lg border border-gray-200 font-medium">
{label}
<ChevronDown className="w-4 h-4 ml-1.5 text-gray-500" />
</MenuButton>
<MenuItems className="absolute right-0 mt-1 w-48 bg-white rounded-lg shadow-lg border border-gray-200 py-1 z-10 focus:outline-none">
{options.map((option) => (
<MenuItem key={option.id}>
{({ active }) => (
<button
onClick={() => onChange(option.id)}
className={`w-full text-left px-4 py-2 text-sm ${
active ? "bg-gray-50" : ""
} ${
value === option.id
? "text-blue-600 bg-blue-50"
: "text-gray-700"
}`}
>
{option.label}
</button>
)}
</MenuItem>
))}
</MenuItems>
</Menu>
);
};
const typeOptions: FilterOption[] = [
{ id: "all", label: "全部类型" },
{ id: "doc", label: "文档" },
{ id: "image", label: "图片" },
{ id: "code", label: "代码" },
];
const ownerOptions: FilterOption[] = [
{ id: "all", label: "全部归属" },
{ id: "personal", label: "个人" },
{ id: "team", label: "团队" },
{ id: "public", label: "公开" },
];
const creatorOptions: FilterOption[] = [
{ id: "all", label: "全部创建者" },
{ id: "me", label: "我创建的" },
{ id: "shared", label: "共享给我的" },
];
export const SearchHeader: React.FC = () => {
const [typeFilter, setTypeFilter] = useState("all");
const [ownerFilter, setOwnerFilter] = useState("all");
const [creatorFilter, setCreatorFilter] = useState("all");
return (
<div className="flex items-center justify-between py-4 border-b border-gray-200 text-xs">
<div className="text-gray-600">
<span className="font-medium text-gray-900">200</span>
</div>
<div className="flex gap-3">
<FilterDropdown
label="类型"
options={typeOptions}
value={typeFilter}
onChange={setTypeFilter}
/>
<FilterDropdown
label="归属"
options={ownerOptions}
value={ownerFilter}
onChange={setOwnerFilter}
/>
<FilterDropdown
label="创建者"
options={creatorOptions}
value={creatorFilter}
onChange={setCreatorFilter}
/>
</div>
</div>
);
};

View File

@@ -0,0 +1,33 @@
import React, { useState } from "react";
import { SearchHeader } from "./SearchHeader";
import { DocumentList } from "./DocumentList";
import { DocumentDetail } from "./DocumentDetail";
export const SearchResults: React.FC = () => {
const [selectedDocumentId, setSelectedDocumentId] = useState("1"); // Default to first document
return (
<div className="bg-white rounded-xl shadow-sm border border-gray-200 mt-4 overflow-hidden">
<div className="flex h-[calc(100vh-220px)]">
{/* Left Panel */}
<div className="w-[420px] border-r border-gray-200 flex flex-col overflow-hidden">
<div className="px-4 flex-shrink-0">
<SearchHeader />
</div>
<div className="overflow-y-auto flex-1">
<DocumentList
onSelectDocument={setSelectedDocumentId}
selectedId={selectedDocumentId}
/>
</div>
</div>
{/* Right Panel */}
<div className="flex-1 overflow-y-auto">
<DocumentDetail documentId={selectedDocumentId} />
</div>
</div>
</div>
);
};

View File

@@ -0,0 +1,110 @@
import React, { useState } from "react";
import { Mic, Filter, Upload, MessageSquare } from "lucide-react";
import { Switch } from "@headlessui/react";
import { SearchResults } from "./SearchResults";
interface Tag {
id: string;
text: string;
}
function Search() {
const [tags, setTags] = useState<Tag[]>([]);
const [input, setInput] = useState("");
const [isChatMode, setIsChatMode] = useState(false);
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === "Enter" && input.trim()) {
setTags([...tags, { id: Date.now().toString(), text: input.trim() }]);
setInput("");
}
};
const removeTag = (tagId: string) => {
setTags(tags.filter((tag) => tag.id !== tagId));
};
return (
<div className="min-h-screen bg-gray-50 flex items-start justify-center pt-10 px-4">
<div className="w-full max-w-3xl space-y-4">
<div className="border b-t-none border-gray-200 rounded-xl">
{/* Search Bar */}
<div className="relative">
<div className="flex items-center bg-white rounded-xl shadow-sm border border-gray-200 p-2 focus-within:ring-2 focus-within:ring-blue-100 focus-within:border-blue-400 transition-all">
<div className="flex flex-wrap gap-2 flex-1 min-h-12 items-center">
{tags.map((tag) => (
<span
key={tag.id}
className="inline-flex items-center bg-blue-50 text-blue-600 px-2.5 py-1 rounded-lg text-sm"
>
{tag.text}
<button
onClick={() => removeTag(tag.id)}
className="ml-1.5 text-blue-400 hover:text-blue-600"
>
×
</button>
</span>
))}
<input
type="text"
className="flex-1 outline-none min-w-[200px] text-gray-800 placeholder-gray-400"
placeholder="有问题尽管问 Coco"
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyDown={handleKeyDown}
/>
</div>
<button className="p-2 hover:bg-gray-50 rounded-lg transition-colors">
<Mic className="w-5 h-5 text-gray-400" />
</button>
</div>
</div>
{/* Controls */}
<div className="flex justify-between items-center p-2">
<div className="flex gap-3">
<button className="inline-flex items-center px-2 py-1 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors">
<MessageSquare className="w-4 h-4 mr-2" /> Coco
</button>
<button className="inline-flex items-center px-2 py-1 bg-white border border-gray-200 rounded-lg hover:bg-gray-50 transition-colors text-gray-700">
<Filter className="w-4 h-4 mr-2" />
</button>
<button className="inline-flex items-center px-2 py-1 bg-white border border-gray-200 rounded-lg hover:bg-gray-50 transition-colors text-gray-700">
<Upload className="w-4 h-4 mr-2" />
</button>
</div>
{/* Switch */}
<div className="flex items-center">
<span className="mr-3 text-sm font-medium text-gray-700">
Chat
</span>
<Switch
checked={isChatMode}
onChange={setIsChatMode}
className={`${
isChatMode ? "bg-blue-600" : "bg-gray-200"
} relative inline-flex h-6 w-11 items-center rounded-full transition-colors focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2`}
>
<span
className={`${
isChatMode ? "translate-x-6" : "translate-x-1"
} inline-block h-4 w-4 transform rounded-full bg-white transition-transform`}
/>
</Switch>
</div>
</div>
</div>
{/* Search Results Panel */}
<SearchResults />
</div>
</div>
);
}
export default Search;

View File

@@ -0,0 +1,107 @@
import { useState } from "react";
import {
Command,
Monitor,
Palette,
Layout,
Star,
Moon,
Sun
} from "lucide-react";
import SettingsItem from "./SettingsItem";
import SettingsSelect from "./SettingsSelect";
import SettingsToggle from "./SettingsToggle";
import { ThemeOption } from "./index2";
interface GeneralSettingsProps {
theme: "light" | "dark" | "system";
setTheme: (theme: "light" | "dark" | "system") => void;
}
export default function GeneralSettings({
theme,
setTheme,
}: GeneralSettingsProps) {
const [launchAtLogin, setLaunchAtLogin] = useState(true);
return (
<div className="space-y-8">
<div>
<h2 className="text-lg font-semibold text-gray-900 dark:text-white mb-4">
General Settings
</h2>
<div className="space-y-6">
<SettingsItem
icon={Command}
title="Launch at Login"
description="Automatically start Coco when you login"
>
<SettingsToggle
checked={launchAtLogin}
onChange={setLaunchAtLogin}
label="Launch at login"
/>
</SettingsItem>
<SettingsItem
icon={Command}
title="Coco Hotkey"
description="Global shortcut to open Coco"
>
<button className="px-3 py-1.5 text-sm bg-gray-100 dark:bg-gray-700 rounded-md border border-gray-200 dark:border-gray-600 hover:bg-gray-200 dark:hover:bg-gray-600 transition-colors duration-200">
Space
</button>
</SettingsItem>
<SettingsItem
icon={Monitor}
title="Window Mode"
description="Choose how Coco appears on your screen"
>
<SettingsSelect
options={["Standard Window", "Compact Mode", "Full Screen"]}
/>
</SettingsItem>
<SettingsItem
icon={Palette}
title="Appearance"
description="Choose your preferred theme"
>
<SettingsSelect
options={["Light", "Dark", "system"]}
value={theme}
onChange={(value) =>
setTheme(value as "light" | "dark" | "system")
}
/>
</SettingsItem>
<div className="grid grid-cols-3 gap-4">
<ThemeOption icon={Sun} title="Light" theme="light" />
<ThemeOption icon={Moon} title="Dark" theme="dark" />
<ThemeOption icon={Monitor} title="System" theme="system" />
</div>
<SettingsItem
icon={Layout}
title="Text Size"
description="Adjust the application text size"
>
<SettingsSelect options={["Small", "Medium", "Large"]} />
</SettingsItem>
<SettingsItem
icon={Star}
title="Favorites"
description="Manage your favorite commands"
>
<button className="text-sm text-blue-600 dark:text-blue-400 hover:text-blue-700 dark:hover:text-blue-300 font-medium transition-colors duration-200">
Manage Favorites
</button>
</SettingsItem>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,32 @@
import { LucideIcon } from "lucide-react";
interface SettingsItemProps {
icon: LucideIcon;
title: string;
description: string;
children: React.ReactNode;
}
export default function SettingsItem({
icon: Icon,
title,
description,
children,
}: SettingsItemProps) {
return (
<div className="flex items-center justify-between">
<div className="flex items-center space-x-3">
<Icon className="h-5 w-5 text-gray-400 dark:text-gray-500" />
<div>
<h3 className="text-sm font-medium text-gray-900 dark:text-gray-100">
{title}
</h3>
<p className="text-sm text-gray-500 dark:text-gray-400">
{description}
</p>
</div>
</div>
{children}
</div>
);
}

View File

@@ -0,0 +1,17 @@
import React from "react";
interface SettingsPanelProps {
title: string;
children: React.ReactNode;
}
const SettingsPanel: React.FC<SettingsPanelProps> = ({ title, children }) => {
return (
<div className="bg-white dark:bg-gray-800 rounded-xl p-6 shadow-sm">
{/* <h2 className="text-xl font-semibold mb-6">{title}</h2> */}
{children}
</div>
);
};
export default SettingsPanel;

View File

@@ -0,0 +1,27 @@
import { Select } from '@headlessui/react'
interface SettingsSelectProps {
options: string[];
value?: string;
onChange?: (value: string) => void;
}
export default function SettingsSelect({
options,
value,
onChange,
}: SettingsSelectProps) {
return (
<Select
value={value}
onChange={(e) => onChange?.(e.target.value)}
className="rounded-md border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 shadow-sm focus:border-blue-500 focus:ring-blue-500"
>
{options.map((option) => (
<option key={option} value={option.toLowerCase()}>
{option}
</option>
))}
</Select>
);
}

View File

@@ -0,0 +1,30 @@
import { Switch } from "@headlessui/react";
interface SettingsToggleProps {
checked: boolean;
onChange: (checked: boolean) => void;
label: string;
}
export default function SettingsToggle({
checked,
onChange,
label,
}: SettingsToggleProps) {
return (
<Switch
checked={checked}
onChange={onChange}
className={`${checked ? "bg-blue-600" : "bg-gray-200 dark:bg-gray-700"}
relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent
transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2`}
>
<span className="sr-only">{label}</span>
<span
className={`${checked ? "translate-x-5" : "translate-x-0"}
pointer-events-none relative inline-block h-5 w-5 transform rounded-full bg-white shadow
ring-0 transition duration-200 ease-in-out`}
/>
</Switch>
);
}

View File

@@ -0,0 +1,48 @@
import React, { useContext } from 'react';
import { Menu } from '@headlessui/react';
import { Monitor, Moon, Sun } from 'lucide-react';
import { Theme, ThemeContext } from './index2';
const ThemeSelector = () => {
const { theme, setTheme } = useContext(ThemeContext);
const themes: { value: Theme; label: string; icon: any }[] = [
{ value: 'light', label: 'Light', icon: Sun },
{ value: 'dark', label: 'Dark', icon: Moon },
{ value: 'system', label: 'System', icon: Monitor },
];
const currentTheme = themes.find((t) => t.value === theme);
return (
<Menu as="div" className="relative">
<Menu.Button className="flex items-center space-x-2 px-4 py-2 rounded-lg bg-gray-100 dark:bg-gray-800 hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors">
{currentTheme && <currentTheme.icon className="w-4 h-4" />}
<span>{currentTheme?.label}</span>
</Menu.Button>
<Menu.Items className="absolute right-0 mt-2 w-48 rounded-lg bg-white dark:bg-gray-800 shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none">
<div className="p-1">
{themes.map((item) => (
<Menu.Item key={item.value}>
{({ active }) => (
<button
onClick={() => setTheme(item.value)}
className={`${
active
? 'bg-gray-100 dark:bg-gray-700'
: 'text-gray-900 dark:text-gray-100'
} group flex w-full items-center rounded-md px-2 py-2 text-sm`}
>
<item.icon className="w-4 h-4 mr-2" />
{item.label}
</button>
)}
</Menu.Item>
))}
</div>
</Menu.Items>
</Menu>
);
};
export default ThemeSelector;

View File

@@ -0,0 +1,145 @@
import { useState } from "react";
import {
Settings,
Search,
Command,
Keyboard,
Globe,
Zap,
ChevronRight,
Moon,
Sun,
} from "lucide-react";
function NavItem({ icon: Icon, label, active, onClick }: any) {
return (
<button
onClick={onClick}
className={`w-full flex items-center px-3 py-2 rounded-lg text-sm ${
active
? "bg-indigo-50 text-indigo-600"
: "text-gray-700 hover:bg-gray-100"
}`}
>
<Icon className="w-4 h-4 mr-3" />
<span className="font-medium">{label}</span>
</button>
);
}
function SettingItem({ icon: Icon, title, description, action }: any) {
return (
<div className="flex items-center justify-between py-4 border-b border-gray-100">
<div className="flex items-start space-x-3">
<div className="p-2 bg-gray-50 rounded-lg">
<Icon className="w-5 h-5 text-gray-600" />
</div>
<div>
<h3 className="text-sm font-medium text-gray-900">{title}</h3>
<p className="text-sm text-gray-500">{description}</p>
</div>
</div>
{action}
</div>
);
}
function AppSettings() {
const [activeSection, setActiveSection] = useState("general");
const [darkMode, setDarkMode] = useState(false);
const sections = [
{ id: "general", label: "General", icon: Settings },
{ id: "appearance", label: "Appearance", icon: Search },
{ id: "extensions", label: "Extensions", icon: Command },
{ id: "keyboard", label: "Keyboard", icon: Keyboard },
{ id: "advanced", label: "Advanced", icon: Zap },
];
return (
<div className="min-h-screen bg-gray-50">
<div className="max-w-7xl mx-auto px-4 py-8">
<div className="bg-white rounded-2xl shadow-sm border border-gray-200 flex min-h-[500px]">
<div className="w-64 p-4 border-r border-gray-100">
<div className="space-y-1">
{sections.map((section) => (
<NavItem
key={section.id}
icon={section.icon}
label={section.label}
active={activeSection === section.id}
onClick={() => setActiveSection(section.id)}
/>
))}
</div>
</div>
{/* Main Content */}
<div className="flex-1 p-6">
<div className="max-w-3xl">
<h1 className="text-2xl font-semibold text-gray-900 mb-6">
Settings
</h1>
<div className="space-y-1">
<SettingItem
icon={Globe}
title="Language"
description="Choose your preferred language for the interface"
action={
<button className="flex items-center text-sm text-gray-600 hover:text-gray-900">
English
<ChevronRight className="w-4 h-4 ml-1" />
</button>
}
/>
<SettingItem
icon={darkMode ? Moon : Sun}
title="Appearance"
description="Switch between light and dark mode"
action={
<button
onClick={() => setDarkMode(!darkMode)}
className="relative inline-flex h-6 w-11 items-center rounded-full bg-gray-200 transition-colors focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"
>
<span
className={`${
darkMode ? "translate-x-6" : "translate-x-1"
} inline-block h-4 w-4 transform rounded-full bg-white transition-transform`}
/>
</button>
}
/>
<SettingItem
icon={Command}
title="Keyboard Shortcuts"
description="Customize your keyboard shortcuts"
action={
<button className="px-3 py-1 text-sm text-gray-600 hover:text-gray-900 border border-gray-200 rounded-md hover:border-gray-300">
Configure
</button>
}
/>
<SettingItem
icon={Zap}
title="Performance Mode"
description="Optimize for better performance"
action={
<button className="relative inline-flex h-6 w-11 items-center rounded-full bg-indigo-600 transition-colors focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2">
<span className="translate-x-6 inline-block h-4 w-4 transform rounded-full bg-white transition-transform" />
</button>
}
/>
</div>
</div>
</div>
</div>
</div>
</div>
);
}
export default AppSettings;

View File

@@ -0,0 +1,157 @@
import { createContext, useContext, useState } from "react";
import { Tab, TabGroup, TabList, TabPanel, TabPanels } from "@headlessui/react";
import {
Monitor,
Moon,
Sun,
Keyboard,
Bell,
Palette,
Shield,
Workflow,
Settings,
Puzzle,
User,
Users,
Settings2,
Info,
} from "lucide-react";
import SettingsPanel from "./SettingsPanel";
import GeneralSettings from "./GeneralSettings";
import Footer from "../Footer";
export type Theme = "light" | "dark" | "system";
export const ThemeContext = createContext<{
theme: Theme;
setTheme: (theme: Theme) => void;
}>({
theme: "system",
setTheme: () => {},
});
function SettingsPage() {
const [theme, setTheme] = useState<Theme>("system");
const tabs = [
{ name: "General", icon: Settings },
{ name: "Extensions", icon: Puzzle },
{ name: "Account", icon: User },
{ name: "Organizations", icon: Users },
{ name: "Advanced", icon: Settings2 },
{ name: "About", icon: Info },
];
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
<div className={theme}>
<div className="min-h-screen bg-white dark:bg-gray-900 text-gray-900 dark:text-white">
<div className="max-w-6xl mx-auto p-4">
<div className="flex items-center justify-center mb-2">
<h1 className="text-xl font-bold">Coco Settings</h1>
</div>
<TabGroup>
<TabList className="flex space-x-1 rounded-xl bg-gray-100 dark:bg-gray-800 p-1">
{tabs.map((tab) => (
<Tab
key={tab.name}
className={({ selected }) =>
`w-full rounded-lg py-2.5 text-sm font-medium leading-5
${
selected
? "bg-white dark:bg-gray-700 shadow text-gray-900 dark:text-white"
: "text-gray-700 dark:text-gray-400 hover:bg-white/[0.12] hover:text-gray-900 dark:hover:text-white"
}
flex items-center justify-center space-x-2 focus:outline-none`
}
>
<tab.icon className="w-4 h-4" />
<span>{tab.name}</span>
</Tab>
))}
</TabList>
<TabPanels className="mt-6">
<TabPanel>
<SettingsPanel title="">
<GeneralSettings theme={theme} setTheme={setTheme} />
</SettingsPanel>
</TabPanel>
<TabPanel>
<SettingsPanel title="">
<div className="text-gray-600 dark:text-gray-400">
Extensions settings content
</div>
</SettingsPanel>
</TabPanel>
<TabPanel>
<SettingsPanel title="">
<div className="text-gray-600 dark:text-gray-400">
Account settings content
</div>
</SettingsPanel>
</TabPanel>
<TabPanel>
<SettingsPanel title="">
<div className="text-gray-600 dark:text-gray-400">
Organizations settings content
</div>
</SettingsPanel>
</TabPanel>
<TabPanel>
<SettingsPanel title="">
<div className="text-gray-600 dark:text-gray-400">
Advanced settings content
</div>
</SettingsPanel>
</TabPanel>
<TabPanel>
<SettingsPanel title="">
<div className="text-gray-600 dark:text-gray-400">
About settings content
</div>
</SettingsPanel>
</TabPanel>
</TabPanels>
</TabGroup>
</div>
</div>
</div>
<Footer />
</ThemeContext.Provider>
);
}
export function ThemeOption({
icon: Icon,
title,
theme,
}: {
icon: any;
title: string;
theme: Theme;
}) {
const { theme: currentTheme, setTheme } = useContext(ThemeContext);
const isSelected = currentTheme === theme;
return (
<button
onClick={() => setTheme(theme)}
className={`p-4 rounded-lg border-2 ${
isSelected
? "border-blue-500 bg-blue-50 dark:bg-blue-900/20"
: "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`}
>
<Icon className={`w-6 h-6 ${isSelected ? "text-blue-500" : ""}`} />
<span
className={`text-sm font-medium ${isSelected ? "text-blue-500" : ""}`}
>
{title}
</span>
</button>
);
}
export default SettingsPage;

View File

@@ -0,0 +1,73 @@
import { createContext, useContext, useEffect, useState } from "react";
type Theme = "dark" | "light" | "system";
type ThemeProviderProps = {
children: React.ReactNode;
defaultTheme?: Theme;
storageKey?: string;
};
type ThemeProviderState = {
theme: Theme;
setTheme: (theme: Theme) => void;
};
const initialState: ThemeProviderState = {
theme: "system",
setTheme: () => null,
};
const ThemeProviderContext = createContext<ThemeProviderState>(initialState);
export function ThemeProvider({
children,
defaultTheme = "system",
storageKey = "theme",
...props
}: ThemeProviderProps) {
const [theme, setTheme] = useState<Theme>(
() => (localStorage.getItem(storageKey) as Theme) || defaultTheme
);
useEffect(() => {
const root = window.document.documentElement;
root.classList.remove("light", "dark");
if (theme === "system") {
const systemTheme = window.matchMedia("(prefers-color-scheme: dark)")
.matches
? "dark"
: "light";
root.classList.add(systemTheme);
return;
}
root.classList.add(theme);
}, [theme]);
const value = {
theme,
setTheme: (theme: Theme) => {
localStorage.setItem(storageKey, theme);
setTheme(theme);
},
};
return (
<ThemeProviderContext.Provider {...props} value={value}>
{children}
</ThemeProviderContext.Provider>
);
}
export const useTheme = () => {
const context = useContext(ThemeProviderContext);
if (context === undefined)
throw new Error("useTheme 必须在 ThemeProvider 中使用");
return context;
};

View File

@@ -0,0 +1,24 @@
import { Sun, Moon } from "lucide-react";
import { useTheme } from "./ThemeProvider";
const ThemeToggle = () => {
const { theme, setTheme } = useTheme();
return (
<button
onClick={() => setTheme(theme === "light" ? "dark" : "light")}
className="inline-flex rounded-md p-2 hover:bg-zinc-100 dark:hover:bg-zinc-900"
>
<Sun
size={20}
className="rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0"
/>
<Moon
size={20}
className="absolute rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100"
/>
</button>
);
};
export default ThemeToggle;

16
src/error-page.tsx Normal file
View File

@@ -0,0 +1,16 @@
import { useRouteError } from "react-router-dom";
export default function ErrorPage() {
const error: any = useRouteError();
console.error(error);
return (
<div id="error-page">
<h1>Oops!</h1>
<p>Sorry, an unexpected error has occurred.</p>
<p>
<i>{error.statusText || error.message}</i>
</p>
</div>
);
}

20
src/hooks/useEscape.ts Normal file
View File

@@ -0,0 +1,20 @@
import { invoke } from "@tauri-apps/api/core";
import { useEffect } from "react";
const useEscape = () => {
const handleEscape = (event: KeyboardEvent) => {
if (event.key === "Escape") {
event.preventDefault();
invoke("hide");
}
};
useEffect(() => {
window.addEventListener("keydown", handleEscape);
return () => window.removeEventListener("keydown", handleEscape);
}, []);
};
export default useEscape;

23
src/i18n.ts Normal file
View File

@@ -0,0 +1,23 @@
import i18n from "i18next";
import { initReactI18next } from "react-i18next";
import enTranslation from "./locales/en/translation.json";
import zhTranslation from "./locales/zh/translation.json";
i18n.use(initReactI18next).init({
resources: {
en: {
translation: enTranslation,
},
zh: {
translation: zhTranslation,
},
},
lng: "en",
fallbackLng: "en",
interpolation: {
escapeValue: false,
},
});
export default i18n;

12
src/icons/ArrowLeft.tsx Normal file
View File

@@ -0,0 +1,12 @@
import SVGWrap from "./SVGWrap";
export default function ArrowLeft(props: I.SVG) {
return (
<SVGWrap {...props} viewBox="0 0 24 24">
<path
fill="currentColor"
d="m7.85 13l2.85 2.85q.3.3.288.7t-.288.7q-.3.3-.712.313t-.713-.288L4.7 12.7q-.3-.3-.3-.7t.3-.7l4.575-4.575q.3-.3.713-.287t.712.312q.275.3.288.7t-.288.7L7.85 11H19q.425 0 .713.288T20 12t-.288.713T19 13z"
/>
</SVGWrap>
);
}

16
src/icons/Ask.tsx Normal file
View File

@@ -0,0 +1,16 @@
import SVGWrap from "./SVGWrap";
export default function Ask(props: I.SVG) {
return (
<SVGWrap {...props} viewBox="0 0 24 24">
<path
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M13.038 19.927A9.93 9.93 0 0 1 7.7 19L3 20l1.3-3.9C1.976 12.663 2.874 8.228 6.4 5.726c3.526-2.501 8.59-2.296 11.845.48c1.993 1.7 2.93 4.043 2.746 6.346M19 16l-2 3h4l-2 3"
/>
</SVGWrap>
);
}

12
src/icons/Link.tsx Normal file
View File

@@ -0,0 +1,12 @@
import SVGWrap from "./SVGWrap";
export default function Link(props: I.SVG) {
return (
<SVGWrap {...props} viewBox="0 0 256 256">
<path
fill="currentColor"
d="M117.18 188.74a12 12 0 0 1 0 17l-5.12 5.12A58.26 58.26 0 0 1 70.6 228a58.62 58.62 0 0 1-41.46-100.08l34.75-34.75a58.64 58.64 0 0 1 98.56 28.11a12 12 0 1 1-23.37 5.44a34.65 34.65 0 0 0-58.22-16.58l-34.75 34.75A34.62 34.62 0 0 0 70.57 204a34.41 34.41 0 0 0 24.49-10.14l5.11-5.12a12 12 0 0 1 17.01 0M226.83 45.17a58.65 58.65 0 0 0-82.93 0l-5.11 5.11a12 12 0 0 0 17 17l5.12-5.12a34.63 34.63 0 1 1 49 49l-34.81 34.7A34.39 34.39 0 0 1 150.61 156a34.63 34.63 0 0 1-33.69-26.72a12 12 0 0 0-23.38 5.44A58.64 58.64 0 0 0 150.56 180h.05a58.28 58.28 0 0 0 41.47-17.17l34.75-34.75a58.62 58.62 0 0 0 0-82.91"
/>
</SVGWrap>
);
}

13
src/icons/Pin.tsx Normal file
View File

@@ -0,0 +1,13 @@
import SVGWrap from "./SVGWrap";
export default function Pin(props: I.SVG) {
return (
<SVGWrap {...props} viewBox="0 0 24 24">
<path
fill="currentColor"
fillRule="evenodd"
d="M16 9V4h1c.55 0 1-.45 1-1s-.45-1-1-1H7c-.55 0-1 .45-1 1s.45 1 1 1h1v5c0 1.66-1.34 3-3 3v2h5.97v7l1 1l1-1v-7H19v-2c-1.66 0-3-1.34-3-3"
/>
</SVGWrap>
);
}

12
src/icons/Reload.tsx Normal file
View File

@@ -0,0 +1,12 @@
import SVGWrap from "./SVGWrap";
export default function Reload(props: I.SVG) {
return (
<SVGWrap {...props} viewBox="0 0 24 24">
<path
fill="currentColor"
d="M12 20q-3.35 0-5.675-2.325T4 12t2.325-5.675T12 4q1.725 0 3.3.712T18 6.75V5q0-.425.288-.712T19 4t.713.288T20 5v5q0 .425-.288.713T19 11h-5q-.425 0-.712-.288T13 10t.288-.712T14 9h3.2q-.8-1.4-2.187-2.2T12 6Q9.5 6 7.75 7.75T6 12t1.75 4.25T12 18q1.7 0 3.113-.862t2.187-2.313q.2-.35.563-.487t.737-.013q.4.125.575.525t-.025.75q-1.025 2-2.925 3.2T12 20"
/>
</SVGWrap>
);
}

43
src/icons/SVGWrap.tsx Normal file
View File

@@ -0,0 +1,43 @@
import React from "react";
import clsx from "clsx";
export default function SVGWrap({
size = 18,
children,
type,
className,
title,
onClick,
action = false,
...props
}: I.SVG) {
const handleClick = (e: React.MouseEvent) => {
onClick && onClick(e);
};
return (
<i
style={{
width: size,
height: size,
}}
title={title}
onClick={handleClick}
className={clsx(
"inline-flex items-center justify-center rounded-sm p-[2px] text-slate-500 dark:text-slate-500 transition-all",
{
"cursor-pointer hover:bg-slate-300/50 hover:dark:bg-white/10": action,
},
className
)}
>
<svg
xmlns="http://www.w3.org/2000/svg"
style={{ widows: "100%", height: "100%" }}
{...props}
>
{children}
</svg>
</i>
);
}

15
src/icons/Send.tsx Normal file
View File

@@ -0,0 +1,15 @@
import SVGWrap from "./SVGWrap";
export default function Send(props: I.SVG) {
return (
<SVGWrap {...props} viewBox="0 0 24 24">
<g fill="none">
<path d="M24 0v24H0V0zM12.593 23.258l-.011.002l-.071.035l-.02.004l-.014-.004l-.071-.035c-.01-.004-.019-.001-.024.005l-.004.01l-.017.428l.005.02l.01.013l.104.074l.015.004l.012-.004l.104-.074l.012-.016l.004-.017l-.017-.427c-.002-.01-.009-.017-.017-.018m.265-.113l-.013.002l-.185.093l-.01.01l-.003.011l.018.43l.005.012l.008.007l.201.093c.012.004.023 0 .029-.008l.004-.014l-.034-.614c-.003-.012-.01-.02-.02-.022m-.715.002a.023.023 0 0 0-.027.006l-.006.014l-.034.614c0 .012.007.02.017.024l.015-.002l.201-.093l.01-.008l.004-.011l.017-.43l-.003-.012l-.01-.01z" />
<path
fill="currentColor"
d="m21.433 4.861l-6 15.5a1 1 0 0 1-1.624.362l-3.382-3.235l-2.074 2.073a.5.5 0 0 1-.853-.354v-4.519L2.309 9.723a1 1 0 0 1 .442-1.691l17.5-4.5a1 1 0 0 1 1.181 1.329ZM19 6.001L8.032 13.152l1.735 1.66L19 6Z"
/>
</g>
</SVGWrap>
);
}

12
src/icons/Setting.tsx Normal file
View File

@@ -0,0 +1,12 @@
import SVGWrap from "./SVGWrap";
export default function Setting(props: I.SVG) {
return (
<SVGWrap {...props} viewBox="0 0 24 24">
<g fill="none" stroke="currentColor" strokeWidth="2">
<path d="M3.082 13.945c-.529-.95-.793-1.426-.793-1.945c0-.519.264-.994.793-1.944L4.43 7.63l1.426-2.381c.559-.933.838-1.4 1.287-1.66c.45-.259.993-.267 2.08-.285L12 3.26l2.775.044c1.088.018 1.631.026 2.08.286c.45.26.73.726 1.288 1.659L19.57 7.63l1.35 2.426c.528.95.792 1.425.792 1.944c0 .519-.264.994-.793 1.944L19.57 16.37l-1.426 2.381c-.559.933-.838 1.4-1.287 1.66c-.45.259-.993.267-2.08.285L12 20.74l-2.775-.044c-1.088-.018-1.631-.026-2.08-.286c-.45-.26-.73-.726-1.288-1.659L4.43 16.37z" />
<circle cx="12" cy="12" r="3" />
</g>
</SVGWrap>
);
}

12
src/icons/ThemeDark.tsx Normal file
View File

@@ -0,0 +1,12 @@
import SVGWrap from "./SVGWrap";
export default function ThemeDark(props: I.SVG) {
return (
<SVGWrap {...props} viewBox="0 0 24 24">
<path
fill="currentColor"
d="M12 21q-3.775 0-6.387-2.613T3 12q0-3.45 2.25-5.988T11 3.05q.325-.05.575.088t.4.362t.163.525t-.188.575q-.425.65-.638 1.375T11.1 7.5q0 2.25 1.575 3.825T16.5 12.9q.775 0 1.538-.225t1.362-.625q.275-.175.563-.162t.512.137q.25.125.388.375t.087.6q-.35 3.45-2.937 5.725T12 21m0-2q2.2 0 3.95-1.213t2.55-3.162q-.5.125-1 .2t-1 .075q-3.075 0-5.238-2.163T9.1 7.5q0-.5.075-1t.2-1q-1.95.8-3.163 2.55T5 12q0 2.9 2.05 4.95T12 19m-.25-6.75"
/>
</SVGWrap>
);
}

12
src/icons/ThemeLight.tsx Normal file
View File

@@ -0,0 +1,12 @@
import SVGWrap from "./SVGWrap";
export default function ThemeLight(props: I.SVG) {
return (
<SVGWrap {...props} viewBox="0 0 24 24">
<path
fill="currentColor"
d="M12 15q1.25 0 2.125-.875T15 12t-.875-2.125T12 9t-2.125.875T9 12t.875 2.125T12 15m0 2q-2.075 0-3.537-1.463T7 12t1.463-3.537T12 7t3.538 1.463T17 12t-1.463 3.538T12 17M2 13q-.425 0-.712-.288T1 12t.288-.712T2 11h2q.425 0 .713.288T5 12t-.288.713T4 13zm18 0q-.425 0-.712-.288T19 12t.288-.712T20 11h2q.425 0 .713.288T23 12t-.288.713T22 13zm-8-8q-.425 0-.712-.288T11 4V2q0-.425.288-.712T12 1t.713.288T13 2v2q0 .425-.288.713T12 5m0 18q-.425 0-.712-.288T11 22v-2q0-.425.288-.712T12 19t.713.288T13 20v2q0 .425-.288.713T12 23M5.65 7.05L4.575 6q-.3-.275-.288-.7t.288-.725q.3-.3.725-.3t.7.3L7.05 5.65q.275.3.275.7t-.275.7t-.687.288t-.713-.288M18 19.425l-1.05-1.075q-.275-.3-.275-.712t.275-.688q.275-.3.688-.287t.712.287L19.425 18q.3.275.288.7t-.288.725q-.3.3-.725.3t-.7-.3M16.95 7.05q-.3-.275-.288-.687t.288-.713L18 4.575q.275-.3.7-.288t.725.288q.3.3.3.725t-.3.7L18.35 7.05q-.3.275-.7.275t-.7-.275M4.575 19.425q-.3-.3-.3-.725t.3-.7l1.075-1.05q.3-.275.712-.275t.688.275q.3.275.288.688t-.288.712L6 19.425q-.275.3-.7.288t-.725-.288M12 12"
/>
</SVGWrap>
);
}

12
src/icons/ThemeSystem.tsx Normal file
View File

@@ -0,0 +1,12 @@
import SVGWrap from "./SVGWrap";
export default function ThemeSystem(props: I.SVG) {
return (
<SVGWrap {...props} viewBox="0 0 24 24">
<path
fill="currentColor"
d="M12 22c5.523 0 10-4.477 10-10S17.523 2 12 2S2 6.477 2 12s4.477 10 10 10m0-2V4a8 8 0 1 1 0 16"
/>
</SVGWrap>
);
}

12
src/icons/UnPin.tsx Normal file
View File

@@ -0,0 +1,12 @@
import SVGWrap from "./SVGWrap";
export default function UnPin(props: I.SVG) {
return (
<SVGWrap {...props} viewBox="0 0 24 24">
<path
fill="currentColor"
d="M14 4v5c0 1.12.37 2.16 1 3H9c.65-.86 1-1.9 1-3V4zm3-2H7c-.55 0-1 .45-1 1s.45 1 1 1h1v5c0 1.66-1.34 3-3 3v2h5.97v7l1 1l1-1v-7H19v-2c-1.66 0-3-1.34-3-3V4h1c.55 0 1-.45 1-1s-.45-1-1-1"
/>
</SVGWrap>
);
}

22
src/index.d.ts vendored Normal file
View File

@@ -0,0 +1,22 @@
declare namespace I {
export type AppConf = {
theme: "light" | "dark" | "system";
stay_on_top: boolean;
ask_mode: boolean;
mac_header_hidden: boolean;
};
export interface SVG extends React.SVGProps<SVGSVGElement> {
children?: React.ReactNode;
size?: number;
title?: string;
action?: boolean;
onClick?: (e: React.MouseEvent) => void;
}
}
declare global {
interface Window {
__TAURI__: Record<string, unknown>;
}
}

View File

@@ -0,0 +1,7 @@
{
"welcome": "Welcome to Coco App",
"home": "Home",
"settings": "Settings",
"activeTheme": "Current theme:",
"InputMessage": "Input your message here..."
}

View File

@@ -0,0 +1,7 @@
{
"welcome": "欢迎使用 Coco App",
"home": "主页",
"settings": "设置",
"activeTheme": "当前主题:",
"InputMessage": "在此输入您的消息..."
}

49
src/main.css Normal file
View File

@@ -0,0 +1,49 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer {
:root {
--background: #ffffff;
--foreground: #09090b;
--border: #e3e3e7;
}
.dark {
--background: #09090b;
--foreground: #f9f9f9;
--border: #27272a;
}
}
@layer base {
* {
@apply box-border border-[--border];
}
body {
@apply bg-gray-50 text-gray-900 antialiased;
}
.dark body {
@apply bg-gray-900 text-gray-100;
}
}
@layer components {
.settings-input {
@apply block w-full rounded-md border-gray-300 dark:border-gray-600
bg-white dark:bg-gray-700
text-gray-900 dark:text-gray-100
shadow-sm focus:border-blue-500 focus:ring-blue-500
transition-colors duration-200;
}
.settings-select {
@apply text-sm rounded-md border-gray-300 dark:border-gray-600
bg-white dark:bg-gray-700
text-gray-900 dark:text-gray-100
shadow-sm focus:border-blue-500 focus:ring-blue-500
transition-colors duration-200;
}
}

View File

@@ -1,9 +1,16 @@
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App";
import { RouterProvider } from "react-router-dom";
import { ThemeProvider } from "./components/ThemeProvider";
import { router } from "./routes/index";
import './main.css';
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
<React.StrictMode>
<App />
</React.StrictMode>,
<ThemeProvider defaultTheme="dark" storageKey="theme">
<RouterProvider router={router} />
</ThemeProvider>
</React.StrictMode>
);

20
src/routes/index.tsx Normal file
View File

@@ -0,0 +1,20 @@
import { createBrowserRouter } from "react-router-dom";
import App from "../App";
import ErrorPage from "../error-page";
import Settings from "../components/Settings";
import Settings2 from "../components/Settings/index2";
import SearchChat from "../components/SearchChat";
export const router = createBrowserRouter([
{
path: "/",
element: <SearchChat />,
errorElement: <ErrorPage />,
},
{
path: "/settings",
element: <Settings2 />,
errorElement: <ErrorPage />,
},
]);

43
src/routes/root.tsx Normal file
View File

@@ -0,0 +1,43 @@
export default function Root() {
return (
<>
<div id="sidebar">
<h1>React Router Contacts</h1>
<div>
<form id="search-form" role="search">
<input
id="q"
aria-label="Search contacts"
placeholder="Search"
type="search"
name="q"
/>
<div
id="search-spinner"
aria-hidden
hidden={true}
/>
<div
className="sr-only"
aria-live="polite"
></div>
</form>
<form method="post">
<button type="submit">New</button>
</form>
</div>
<nav>
<ul>
<li>
<a href={`/contacts/1`}>Your Name</a>
</li>
<li>
<a href={`/contacts/2`}>Your Friend</a>
</li>
</ul>
</nav>
</div>
<div id="detail"></div>
</>
);
}

30
src/stores/themeStore.ts Normal file
View File

@@ -0,0 +1,30 @@
import { create } from "zustand";
import { persist, createJSONStorage } from "zustand/middleware";
export type ITheme = "dark" | "light" | "system";
export type IThemeStore = {
themes: ITheme[];
activeTheme: ITheme;
setTheme: (theme: ITheme) => void;
};
export const useThemeStore = create<IThemeStore>()(
// 持久化中间件
persist(
(set) => ({
themes: ["dark", "light", "system"],
activeTheme: "system",
setTheme: (activeTheme: ITheme) => set(() => ({ activeTheme })),
}),
{
name: "active-theme", // 存储在 storage 中的 key 名
// storage: createJSONStorage(() => sessionStorage), // 存储数据库配置,默认使用 localstorage
// 过滤函数
partialize: (state) =>
Object.fromEntries(
Object.entries(state).filter(([key]) => key === "activeTheme")
),
}
)
);

30
tailwind.config.js Normal file
View File

@@ -0,0 +1,30 @@
/** @type {import('tailwindcss').Config} */
export default {
content: ["./index.html", "./src/**/*.{html,js,jsx,ts,tsx}"],
theme: {
extend: {
backgroundColor: {
primary: "rgb(var(--color-primary) / <alpha-value>)",
secondary: "rgb(var(--color-secondary) / <alpha-value>)",
background: "rgb(var(--color-background) / <alpha-value>)",
foreground: "rgb(var(--color-foreground) / <alpha-value>)",
separator: "rgb(var(--color-separator) / <alpha-value>)",
},
textColor: {
primary: "rgb(var(--color-foreground) / <alpha-value>)",
},
animation: {
"fade-in": "fade-in 0.2s ease-in-out",
},
keyframes: {
"fade-in": {
"0%": { opacity: "0" },
"100%": { opacity: "1" },
},
},
},
},
plugins: [],
mode: "jit",
darkMode: "class",
};