feat: add window webview & add Footer & change style (#2)

* chore: add Footer

* feat: add winfow webview
This commit is contained in:
BiggerRain
2024-10-30 20:57:03 +08:00
committed by GitHub
parent c89a49d227
commit fd86a5ef4a
22 changed files with 490 additions and 171 deletions

View File

@@ -28,8 +28,7 @@ To set up the Coco App for development:
```bash
cd coco
pnpm install
pnpm tauri android init
pnpm tauri ios init
pnpm tauri dev
```
#### Desktop Development:
@@ -39,18 +38,3 @@ To start desktop development, run:
```
pnpm tauri dev
```
#### Android Development:
For Android development, run:
```
pnpm tauri android dev
```
#### iOS Development:
For iOS development, run:
```
pnpm tauri ios dev
```

View File

@@ -28,6 +28,7 @@
"devDependencies": {
"@tauri-apps/cli": ">=2.0.0",
"@types/lodash": "^4.17.12",
"@types/node": "^22.8.4",
"@types/react": "^18.2.15",
"@types/react-dom": "^18.2.7",
"@types/react-i18next": "^8.1.0",

26
pnpm-lock.yaml generated
View File

@@ -57,6 +57,9 @@ importers:
'@types/lodash':
specifier: ^4.17.12
version: 4.17.12
'@types/node':
specifier: ^22.8.4
version: 22.8.4
'@types/react':
specifier: ^18.2.15
version: 18.3.11
@@ -68,7 +71,7 @@ importers:
version: 8.1.0(i18next@23.16.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@vitejs/plugin-react':
specifier: ^4.2.1
version: 4.3.2(vite@5.4.8)
version: 4.3.2(vite@5.4.8(@types/node@22.8.4))
autoprefixer:
specifier: ^10.4.20
version: 10.4.20(postcss@8.4.47)
@@ -83,7 +86,7 @@ importers:
version: 5.6.3
vite:
specifier: ^5.3.1
version: 5.4.8
version: 5.4.8(@types/node@22.8.4)
packages:
@@ -622,6 +625,9 @@ packages:
'@types/lodash@4.17.12':
resolution: {integrity: sha512-sviUmCE8AYdaF/KIHLDJBQgeYzPBI0vf/17NaYehBJfYD1j6/L95Slh07NlyK2iNyBNaEkb3En2jRt+a8y3xZQ==}
'@types/node@22.8.4':
resolution: {integrity: sha512-SpNNxkftTJOPk0oN+y2bIqurEXHTA2AOZ3EJDDKeJ5VzkvvORSvmQXGQarcOzWV1ac7DCaPBEdMDxBsM+d8jWw==}
'@types/prop-types@15.7.13':
resolution: {integrity: sha512-hCZTSvwbzWGvhqxp/RqVqwU999pBf2vp7hzIjiYOsl8wqOmUxkQ6ddw1cV3l8811+kdUFus/q4d1Y3E3SyEifA==}
@@ -1200,6 +1206,9 @@ packages:
engines: {node: '>=14.17'}
hasBin: true
undici-types@6.19.8:
resolution: {integrity: sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==}
update-browserslist-db@1.1.1:
resolution: {integrity: sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A==}
hasBin: true
@@ -1744,6 +1753,10 @@ snapshots:
'@types/lodash@4.17.12': {}
'@types/node@22.8.4':
dependencies:
undici-types: 6.19.8
'@types/prop-types@15.7.13': {}
'@types/react-dom@18.3.1':
@@ -1764,14 +1777,14 @@ snapshots:
'@types/prop-types': 15.7.13
csstype: 3.1.3
'@vitejs/plugin-react@4.3.2(vite@5.4.8)':
'@vitejs/plugin-react@4.3.2(vite@5.4.8(@types/node@22.8.4))':
dependencies:
'@babel/core': 7.25.8
'@babel/plugin-transform-react-jsx-self': 7.25.7(@babel/core@7.25.8)
'@babel/plugin-transform-react-jsx-source': 7.25.7(@babel/core@7.25.8)
'@types/babel__core': 7.20.5
react-refresh: 0.14.2
vite: 5.4.8
vite: 5.4.8(@types/node@22.8.4)
transitivePeerDependencies:
- supports-color
@@ -2316,6 +2329,8 @@ snapshots:
typescript@5.6.3: {}
undici-types@6.19.8: {}
update-browserslist-db@1.1.1(browserslist@4.24.0):
dependencies:
browserslist: 4.24.0
@@ -2324,12 +2339,13 @@ snapshots:
util-deprecate@1.0.2: {}
vite@5.4.8:
vite@5.4.8(@types/node@22.8.4):
dependencies:
esbuild: 0.21.5
postcss: 8.4.47
rollup: 4.24.0
optionalDependencies:
'@types/node': 22.8.4
fsevents: 2.3.3
void-elements@3.1.0: {}

37
src-tauri/Cargo.lock generated
View File

@@ -347,6 +347,7 @@ dependencies = [
"serde_json",
"tauri",
"tauri-build",
"tauri-nspanel",
"tauri-plugin-http",
"tauri-plugin-shell",
]
@@ -1876,6 +1877,17 @@ dependencies = [
"malloc_buf",
]
[[package]]
name = "objc-foundation"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1add1b659e36c9607c7aab864a76c7a4c2760cd0cd2e120f3fb8b952c7e22bf9"
dependencies = [
"block",
"objc",
"objc_id",
]
[[package]]
name = "objc-sys"
version = "0.3.5"
@@ -2094,6 +2106,15 @@ dependencies = [
"objc2-foundation",
]
[[package]]
name = "objc_id"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c92d4ddb4bd7b50d730c215ff871754d0da6b2178849f8a2a2ab69712d0c073b"
dependencies = [
"objc",
]
[[package]]
name = "object"
version = "0.36.5"
@@ -3429,6 +3450,22 @@ dependencies = [
"tauri-utils",
]
[[package]]
name = "tauri-nspanel"
version = "2.0.0-beta"
source = "git+https://github.com/ahkohd/tauri-nspanel?rev=005240c#005240ce43fe02108ce6d0aec6e2e436c46f46b7"
dependencies = [
"bitflags 2.6.0",
"block",
"cocoa",
"core-foundation 0.10.0",
"core-graphics",
"objc",
"objc-foundation",
"objc_id",
"tauri",
]
[[package]]
name = "tauri-plugin"
version = "2.0.1"

View File

@@ -24,6 +24,8 @@ serde = { version = "1", features = ["derive"] }
serde_json = "1"
tauri-plugin-http = "2"
tauri-nspanel = { git = "https://github.com/ahkohd/tauri-nspanel", rev = "005240c" }
[profile.dev]
incremental = true # Compile your binary in smaller steps.

View File

@@ -4,6 +4,17 @@
"description": "Capability for the main window",
"windows": ["main"],
"permissions": [
"core:event:allow-listen",
"core:window:default",
"core:window:allow-start-dragging",
"core:webview:allow-create-webview",
"core:window:allow-show",
"core:webview:allow-create-webview-window",
"core:webview:allow-webview-close",
"core:window:allow-close",
"core:window:allow-hide",
"core:webview:allow-set-webview-size",
"core:window:allow-set-size",
"core:default",
"shell:allow-open",
"http:default",

View File

@@ -1,6 +1,80 @@
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
#![cfg_attr(
not(debug_assertions),
target_os = "windows",
windows_subsystem = "windows"
)]
use tauri::{AppHandle, Manager, WebviewWindow};
use tauri_nspanel::{panel_delegate, ManagerExt, WebviewWindowExt};
fn main() {
coco_lib::run()
coco_lib::run();
tauri::Builder::default()
.plugin(tauri_nspanel::init())
.invoke_handler(tauri::generate_handler![
show_panel,
hide_panel,
close_panel
])
.setup(|app| {
init(app.app_handle());
Ok(())
})
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
fn init(app_handle: &AppHandle) {
let window: WebviewWindow = app_handle.get_webview_window("main").unwrap();
let panel = window.to_panel().unwrap();
let delegate = panel_delegate!(MyPanelDelegate {
window_did_become_key,
window_did_resign_key
});
let handle = app_handle.to_owned();
delegate.set_listener(Box::new(move |delegate_name: String| {
match delegate_name.as_str() {
"window_did_become_key" => {
let app_name = handle.package_info().name.to_owned();
println!("[info]: {:?} panel becomes key window!", app_name);
}
"window_did_resign_key" => {
println!("[info]: panel resigned from key window!");
}
_ => (),
}
}));
panel.set_delegate(delegate);
}
#[tauri::command]
fn show_panel(handle: AppHandle) {
let panel = handle.get_webview_panel("main").unwrap();
panel.show();
}
#[tauri::command]
fn hide_panel(handle: AppHandle) {
let panel = handle.get_webview_panel("main").unwrap();
panel.order_out(None);
}
#[tauri::command]
fn close_panel(handle: AppHandle) {
let panel = handle.get_webview_panel("main").unwrap();
panel.released_when_closed(true);
panel.close();
}

View File

@@ -14,10 +14,14 @@
{
"title": "Coco AI",
"width": 800,
"height": 600,
"height": 150,
"maxHeight": 600,
"transparent": true,
"resizable": false,
"fullscreen": false,
"decorations": false
"decorations": false,
"label": "main",
"url": "/"
}
],
"security": {
@@ -27,6 +31,11 @@
"bundle": {
"active": true,
"targets": "all",
"windows": {
"certificateThumbprint": null,
"digestAlgorithm": "sha256",
"timestampUrl": ""
},
"icon": [
"icons/32x32.png",
"icons/128x128.png",
@@ -38,4 +47,4 @@
"plugins": {
"window": {}
}
}
}

View File

@@ -5,9 +5,9 @@ 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">
<div className="max-w-6xl mx-auto px-4 h-8 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">
<MenuButton className="h-7 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

View File

@@ -10,7 +10,7 @@ export const DocumentDetail: React.FC<DocumentDetailProps> = ({
}) => {
if (!documentId) {
return (
<div className="h-full flex items-center justify-center text-gray-400">
<div className="h-full flex items-center justify-center text-gray-400 dark:text-gray-500">
</div>
);
@@ -19,22 +19,26 @@ export const DocumentDetail: React.FC<DocumentDetailProps> = ({
return (
<div className="p-8 space-y-8">
<div className="space-y-6">
<h2 className="text-2xl font-semibold text-gray-900">
<h2 className="text-2xl font-semibold text-gray-900 dark:text-gray-100">
</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-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-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 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>
@@ -45,15 +49,19 @@ export const DocumentDetail: React.FC<DocumentDetailProps> = ({
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">
<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">
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">
<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>
@@ -61,7 +69,7 @@ export const DocumentDetail: React.FC<DocumentDetailProps> = ({
<li></li>
</ul>
<p className="text-gray-600 leading-relaxed mt-6">
<p className="text-gray-600 dark:text-gray-300 leading-relaxed mt-6">
Q1的业务增长目标
</p>

View File

@@ -1,63 +1,61 @@
import React from "react";
import { FileText, Image, FileCode, Users, User, Globe } from "lucide-react";
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";
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: '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: '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",
id: '3',
title: 'API接口文档.ts',
type: 'code',
owner: 'personal',
description: 'TypeScript版本的API接口定义文档包含所有接口的请求和响应类型。',
date: '2024-02-18'
},
];
const getIcon = (type: Document["type"]) => {
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" />;
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" />;
return <FileText className="w-5 h-5 text-purple-500 dark:text-purple-400" />;
}
};
const getOwnerIcon = (owner: Document["owner"]) => {
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" />;
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" />;
return <User className="w-4 h-4 text-gray-500 dark:text-gray-400" />;
}
};
@@ -66,35 +64,28 @@ interface DocumentListProps {
selectedId?: string;
}
export const DocumentList: React.FC<DocumentListProps> = ({
onSelectDocument,
selectedId,
}) => {
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" : ""
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">
{doc.title}
</span>
<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>
<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>
<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>
</div>
</button>
))}
</div>
);
};
};

View File

@@ -0,0 +1,130 @@
import React from "react";
import { Settings, LogOut, Command, User, Home, ChevronUp } from "lucide-react";
import { Menu, MenuButton, MenuItems, MenuItem } from "@headlessui/react";
import { Link } from "react-router-dom";
import { WebviewWindow } from "@tauri-apps/api/webviewWindow";
const shortcuts = [
{ label: "翻页/换行", keys: "↓" },
{ label: "快速换行", keys: "Tab" },
{ label: "Talk to AI", keys: "⌘ + /" },
{ label: "Open", keys: "⌘ + O" },
];
export const Footer: React.FC = () => {
async function openWebviewWindowSettings() {
const webview = new WebviewWindow("settings", {
dragDropEnabled: true,
center: true,
width: 900,
height: 700,
alwaysOnTop: true,
skipTaskbar: true,
decorations: true,
closable: true,
url: "/settings",
});
webview.once("tauri://created", function () {
console.log("webview created");
});
webview.once("tauri://error", function (e) {
console.log("error creating webview", e);
});
}
return (
<div className="fixed bottom-0 left-0 right-0 bg-white dark:bg-gray-800 border-t border-gray-200 dark:border-gray-700 px-4 h-8 flex items-center justify-between">
<div className="flex items-center">
<Menu as="div" className="relative">
<MenuButton className="h-7 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`}
onClick={openWebviewWindowSettings}
>
<Settings className="w-4 h-4 mr-2" />
Settings
</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>
<div className="flex items-center gap-4">
{shortcuts.map((shortcut, index) => (
<div
key={index}
className="flex items-center text-gray-500 dark:text-gray-400 text-sm"
>
{index > 0 && (
<div className="w-px h-4 bg-gray-200 dark:bg-gray-700 mr-4" />
)}
<span className="mr-1.5">{shortcut.label}</span>
<kbd className="px-1.5 py-0.5 rounded bg-gray-100 dark:bg-gray-700 text-gray-500 dark:text-gray-400 text-xs font-medium">
{shortcut.keys}
</kbd>
</div>
))}
</div>
</div>
);
};

View File

@@ -1,4 +1,4 @@
import React, { useState, Fragment } from "react";
import React, { useState } from "react";
import { Menu, MenuButton, MenuItems, MenuItem } from "@headlessui/react";
import { ChevronDown } from "lucide-react";
@@ -21,24 +21,24 @@ const FilterDropdown: React.FC<FilterDropdownProps> = ({
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">
<Menu as="div" className="relative">
<MenuButton className="inline-flex items-center px-2.5 py-1 text-xs bg-white dark:bg-gray-800 text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700 rounded-lg border border-gray-200 dark:border-gray-700 font-medium">
{label}
<ChevronDown className="w-4 h-4 ml-1.5 text-gray-500" />
<ChevronDown className="w-3.5 h-3.5 ml-1 text-gray-500 dark:text-gray-400" />
</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">
<MenuItems className="absolute right-0 mt-1 w-44 bg-white dark:bg-gray-800 rounded-lg shadow-lg border border-gray-200 dark:border-gray-700 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" : ""
className={`w-full text-left px-3 py-1.5 text-xs ${
active ? "bg-gray-50 dark:bg-gray-700" : ""
} ${
value === option.id
? "text-blue-600 bg-blue-50"
: "text-gray-700"
? "text-blue-600 dark:text-blue-400 bg-blue-50 dark:bg-blue-900/50"
: "text-gray-700 dark:text-gray-300"
}`}
>
{option.label}
@@ -77,11 +77,15 @@ export const SearchHeader: React.FC = () => {
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 className="flex items-center justify-between py-3 border-b border-gray-200 dark:border-gray-700">
<div className="text-xs text-gray-600 dark:text-gray-400">
{" "}
<span className="font-medium text-gray-900 dark:text-gray-100">
200
</span>{" "}
</div>
<div className="flex gap-3">
<div className="flex gap-2">
<FilterDropdown
label="类型"
options={typeOptions}

View File

@@ -1,21 +1,20 @@
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
const [selectedDocumentId, setSelectedDocumentId] = useState("1");
return (
<div className="bg-white rounded-xl shadow-sm border border-gray-200 mt-4 overflow-hidden">
<div className="bg-white dark:bg-gray-800 rounded-xl shadow-sm border border-gray-200 dark:border-gray-700 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="w-[420px] 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">
<div className="overflow-y-auto flex-1 custom-scrollbar">
<DocumentList
onSelectDocument={setSelectedDocumentId}
selectedId={selectedDocumentId}
@@ -24,7 +23,7 @@ export const SearchResults: React.FC = () => {
</div>
{/* Right Panel */}
<div className="flex-1 overflow-y-auto">
<div className="flex-1 overflow-y-auto custom-scrollbar">
<DocumentDetail documentId={selectedDocumentId} />
</div>
</div>

View File

@@ -1,8 +1,11 @@
import React, { useState } from "react";
import { Mic, Filter, Upload, MessageSquare } from "lucide-react";
import { Mic, Filter, Upload } from "lucide-react";
import { Switch } from "@headlessui/react";
import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow";
import { SearchResults } from "./SearchResults";
import { Footer } from "./Footer";
import { LogicalSize } from "@tauri-apps/api/dpi";
interface Tag {
id: string;
@@ -14,34 +17,39 @@ function Search() {
const [input, setInput] = useState("");
const [isChatMode, setIsChatMode] = useState(false);
const handleKeyDown = (e: React.KeyboardEvent) => {
const handleKeyDown = async (e: React.KeyboardEvent) => {
if (e.key === "Enter" && input.trim()) {
setTags([...tags, { id: Date.now().toString(), text: input.trim() }]);
setInput("");
await getCurrentWebviewWindow().setSize(new LogicalSize(800, 600));
}
};
const removeTag = (tagId: string) => {
setTags(tags.filter((tag) => tag.id !== tagId));
const removeTag = async (tagId: string) => {
const newTag = tags.filter((tag) => tag.id !== tagId)
setTags(newTag);
if (newTag.length === 0) {
await getCurrentWebviewWindow().setSize(new LogicalSize(800, 150));
}
};
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">
<div className="max-h-screen flex items-start justify-center pb-8 rounded-xl">
<div className="w-full space-y-4">
<div className="border b-t-none border-gray-200 dark:border-gray-700 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 items-center bg-white dark:bg-gray-800 rounded-xl shadow-sm border border-gray-200 dark:border-gray-700 p-2 focus-within:ring-2 focus-within:ring-blue-100 dark:focus-within:ring-blue-900 focus-within:border-blue-400 dark:focus-within:border-blue-500 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"
className="inline-flex items-center bg-blue-50 dark:bg-blue-900/50 text-blue-600 dark:text-blue-400 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"
className="ml-1.5 text-blue-400 hover:text-blue-600 dark:text-blue-500 dark:hover:text-blue-400"
>
×
</button>
@@ -49,46 +57,44 @@ function Search() {
))}
<input
type="text"
className="flex-1 outline-none min-w-[200px] text-gray-800 placeholder-gray-400"
className="flex-1 outline-none min-w-[200px] text-gray-800 dark:text-gray-200 placeholder-gray-400 dark:placeholder-gray-500 bg-transparent"
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 className="p-2 hover:bg-gray-50 dark:hover:bg-gray-700 rounded-lg transition-colors">
<Mic className="w-5 h-5 text-gray-400 dark:text-gray-500" />
</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
<div className="flex gap-3 text-xs">
<button className="inline-flex items-center px-2 py-1 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors text-gray-700 dark:text-gray-300">
<Filter 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">
<button className="inline-flex items-center px-2 py-1 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors text-gray-700 dark:text-gray-300">
<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
<div className="flex items-center text-xs">
<span className="mr-3 text-sm font-medium text-gray-700 dark:text-gray-300">
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`}
isChatMode
? "bg-blue-600 dark:bg-blue-500"
: "bg-gray-200 dark:bg-gray-700"
} relative inline-flex h-6 w-11 items-center rounded-full transition-colors focus:outline-none focus:ring-2 focus:ring-blue-500 dark:focus:ring-blue-400 focus:ring-offset-2 dark:focus:ring-offset-gray-900`}
>
<span
className={`${
@@ -103,6 +109,8 @@ function Search() {
{/* Search Results Panel */}
<SearchResults />
</div>
<Footer />
</div>
);
}

View File

@@ -1,21 +1,7 @@
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 { Settings, Puzzle, User, Users, Settings2, Info } from "lucide-react";
import SettingsPanel from "./SettingsPanel";
import GeneralSettings from "./GeneralSettings";
import Footer from "../Footer";
@@ -45,7 +31,7 @@ function SettingsPage() {
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="min-h-screen pb-8 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>

View File

@@ -22,11 +22,11 @@
}
body {
@apply bg-gray-50 text-gray-900 antialiased;
@apply bg-gray-50 text-gray-900 rounded-lg antialiased;
}
.dark body {
@apply bg-gray-900 text-gray-100;
@apply bg-gray-900 text-gray-100 rounded-lg;
}
}
@@ -47,3 +47,31 @@
transition-colors duration-200;
}
}
@layer utilities {
.custom-scrollbar {
scrollbar-width: thin;
scrollbar-color: #cbd5e1 transparent;
}
.custom-scrollbar::-webkit-scrollbar {
width: 6px;
}
.custom-scrollbar::-webkit-scrollbar-track {
background: transparent;
}
.custom-scrollbar::-webkit-scrollbar-thumb {
background-color: #cbd5e1;
border-radius: 3px;
}
.dark .custom-scrollbar {
scrollbar-color: #475569 transparent;
}
.dark .custom-scrollbar::-webkit-scrollbar-thumb {
background-color: #475569;
}
}

View File

@@ -5,7 +5,7 @@ import { RouterProvider } from "react-router-dom";
import { ThemeProvider } from "./components/ThemeProvider";
import { router } from "./routes/index";
import './main.css';
import "./main.css";
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
<React.StrictMode>

21
src/routes/Header.tsx Normal file
View File

@@ -0,0 +1,21 @@
import { Link, useLocation, useNavigate } from "react-router-dom";
export default function Header() {
// 调用 navigate() 去你想去的地方 ⛱️
const navigate = useNavigate();
// 我在哪?
const location = useLocation();
const showBack = location.pathname !== "/";
return (
<div>
<div onClick={() => navigate("/")}>Home</div>
<div>
{/* 相当于 HTML 中的 <a>,点击后跳转页面 */}
<Link to="/settings" title="更多">
Settings
</Link>
</div>
</div>
);
}

11
src/routes/Layout.tsx Normal file
View File

@@ -0,0 +1,11 @@
import { Outlet } from "react-router-dom";
export default function Layout() {
return (
<div>
<main>
<Outlet />
</main>
</div>
);
}

View File

@@ -5,16 +5,16 @@ import ErrorPage from "../error-page";
import Settings from "../components/Settings";
import Settings2 from "../components/Settings/index2";
import SearchChat from "../components/SearchChat";
import Layout from './Layout'
export const router = createBrowserRouter([
{
path: "/",
element: <SearchChat />,
errorElement: <ErrorPage />,
},
{
path: "/settings",
element: <Settings2 />,
path: '/',
element: <Layout />,
errorElement: <ErrorPage />,
children: [
{ path: '/', element: <SearchChat /> },
{ path: '/settings', element: <Settings2 /> }
],
},
]);

View File

@@ -1,7 +1,6 @@
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
// @ts-expect-error process is a nodejs global
const host = process.env.TAURI_DEV_HOST;
// https://vitejs.dev/config/