+
+
setIsOpen(false)}
+ />
+
+
+
+
+
+
setSearch(e.target.value)}
+ />
+
+
+ Esc
+
+ to close
+
+
+
+
+ {filteredCommands.length === 0 ? (
+
+ No results found.
+
+ ) : (
+
+ {filteredCommands.map((command, index) => (
+
{
+ command.action();
+ setIsOpen(false);
+ }}
+ >
+
+ {command.icon}
+
+
+
{command.title}
+
+ {command.description}
+
+
+
+ ))}
+
+ )}
+
+
+
+
+
+ );
+}
diff --git a/src/components/Footer.tsx b/src/components/Footer.tsx
new file mode 100644
index 00000000..56cf506d
--- /dev/null
+++ b/src/components/Footer.tsx
@@ -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 (
+
+
+
+
+
+
+ Version 1.0.0
+
+
+
+
+
+
+ );
+};
+
+export default Footer;
diff --git a/src/components/Header.tsx b/src/components/Header.tsx
new file mode 100644
index 00000000..544bd6e4
--- /dev/null
+++ b/src/components/Header.tsx
@@ -0,0 +1,19 @@
+import ThemeToggle from "./ThemeToggle";
+import LangToggle from "./LangToggle";
+
+export default function Header() {
+ return (
+
+
Coco
+
+
+
+

+
+
+ );
+}
diff --git a/src/components/LangToggle.tsx b/src/components/LangToggle.tsx
new file mode 100644
index 00000000..753e4840
--- /dev/null
+++ b/src/components/LangToggle.tsx
@@ -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 (
+
+ );
+}
diff --git a/src/components/Raycast.tsx b/src/components/Raycast.tsx
new file mode 100644
index 00000000..e2cbed9c
--- /dev/null
+++ b/src/components/Raycast.tsx
@@ -0,0 +1,28 @@
+import { Command } from "lucide-react";
+import { CommandPalette } from "./CommandPalette";
+
+function Raycast() {
+ return (
+
+
+
+
+
+ Press
+
+ ⌘
+
+
+ K
+
+ to open command palette
+
+
+
+
+
+
+ );
+}
+
+export default Raycast;
diff --git a/src/components/SearchChat/DocumentDetail.tsx b/src/components/SearchChat/DocumentDetail.tsx
new file mode 100644
index 00000000..19df8bc1
--- /dev/null
+++ b/src/components/SearchChat/DocumentDetail.tsx
@@ -0,0 +1,71 @@
+import React from "react";
+import { Calendar, User, Clock } from "lucide-react";
+
+interface DocumentDetailProps {
+ documentId?: string;
+}
+
+export const DocumentDetail: React.FC
= ({
+ documentId,
+}) => {
+ if (!documentId) {
+ return (
+
+ 请选择一个文档查看详情
+
+ );
+ }
+
+ return (
+
+
+
+ 产品需求规划文档
+
+
+
+
+
+ 2024-02-20
+
+
+
+ 张小明
+
+
+
+ 最近更新于 2小时前
+
+
+
+
+

+
+
+
文档概述
+
+ 本文档详细说明了2024年Q1的产品规划方向和具体功能需求。包含了用户研究结果、
+ 竞品分析、功能优先级排序等重要内容。产品团队可以基于此文档进行后续的设计和开发工作。
+
+
+
主要内容
+
+ - 用户痛点分析与解决方案
+ - 核心功能详细说明
+ - 交互流程设计
+ - 技术可行性评估
+ - 项目时间节点规划
+
+
+
+ 通过实施本文档中规划的功能,我们期望能够提升用户体验,增强产品竞争力,
+ 实现Q1的业务增长目标。
+
+
+
+ );
+};
diff --git a/src/components/SearchChat/DocumentList.tsx b/src/components/SearchChat/DocumentList.tsx
new file mode 100644
index 00000000..40812b14
--- /dev/null
+++ b/src/components/SearchChat/DocumentList.tsx
@@ -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 ;
+ case "code":
+ return ;
+ default:
+ return ;
+ }
+};
+
+const getOwnerIcon = (owner: Document["owner"]) => {
+ switch (owner) {
+ case "team":
+ return ;
+ case "public":
+ return ;
+ default:
+ return ;
+ }
+};
+
+interface DocumentListProps {
+ onSelectDocument: (id: string) => void;
+ selectedId?: string;
+}
+
+export const DocumentList: React.FC = ({
+ onSelectDocument,
+ selectedId,
+}) => {
+ return (
+
+ {documents.map((doc) => (
+
+ ))}
+
+ );
+};
diff --git a/src/components/SearchChat/SearchHeader.tsx b/src/components/SearchChat/SearchHeader.tsx
new file mode 100644
index 00000000..7e4df131
--- /dev/null
+++ b/src/components/SearchChat/SearchHeader.tsx
@@ -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 = ({
+ label,
+ options,
+ value,
+ onChange,
+}) => {
+ return (
+
+ );
+};
+
+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 (
+
+
+ 搜索到 200 条数据
+
+
+
+
+
+
+
+ );
+};
diff --git a/src/components/SearchChat/SearchResults.tsx b/src/components/SearchChat/SearchResults.tsx
new file mode 100644
index 00000000..3680ff64
--- /dev/null
+++ b/src/components/SearchChat/SearchResults.tsx
@@ -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 (
+
+
+ {/* Left Panel */}
+
+
+ {/* Right Panel */}
+
+
+
+
+
+ );
+};
diff --git a/src/components/SearchChat/index.tsx b/src/components/SearchChat/index.tsx
new file mode 100644
index 00000000..6d8b1265
--- /dev/null
+++ b/src/components/SearchChat/index.tsx
@@ -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([]);
+ 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 (
+
+
+
+ {/* Search Bar */}
+
+
+
+ {tags.map((tag) => (
+
+ {tag.text}
+
+
+ ))}
+ setInput(e.target.value)}
+ onKeyDown={handleKeyDown}
+ />
+
+
+
+
+
+ {/* Controls */}
+
+
+
+
+
+
+
+ {/* Switch */}
+
+
+ Chat 模式
+
+
+
+
+
+
+
+
+ {/* Search Results Panel */}
+
+
+
+ );
+}
+
+export default Search;
diff --git a/src/components/Settings/GeneralSettings.tsx b/src/components/Settings/GeneralSettings.tsx
new file mode 100644
index 00000000..9510ad91
--- /dev/null
+++ b/src/components/Settings/GeneralSettings.tsx
@@ -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 (
+
+
+
+ General Settings
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ setTheme(value as "light" | "dark" | "system")
+ }
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/components/Settings/SettingsItem.tsx b/src/components/Settings/SettingsItem.tsx
new file mode 100644
index 00000000..56c2bf3b
--- /dev/null
+++ b/src/components/Settings/SettingsItem.tsx
@@ -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 (
+
+
+
+
+
+ {title}
+
+
+ {description}
+
+
+
+ {children}
+
+ );
+}
diff --git a/src/components/Settings/SettingsPanel.tsx b/src/components/Settings/SettingsPanel.tsx
new file mode 100644
index 00000000..6b902d59
--- /dev/null
+++ b/src/components/Settings/SettingsPanel.tsx
@@ -0,0 +1,17 @@
+import React from "react";
+
+interface SettingsPanelProps {
+ title: string;
+ children: React.ReactNode;
+}
+
+const SettingsPanel: React.FC = ({ title, children }) => {
+ return (
+
+ {/*
{title}
*/}
+ {children}
+
+ );
+};
+
+export default SettingsPanel;
diff --git a/src/components/Settings/SettingsSelect.tsx b/src/components/Settings/SettingsSelect.tsx
new file mode 100644
index 00000000..06903df7
--- /dev/null
+++ b/src/components/Settings/SettingsSelect.tsx
@@ -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 (
+
+ );
+}
diff --git a/src/components/Settings/SettingsToggle.tsx b/src/components/Settings/SettingsToggle.tsx
new file mode 100644
index 00000000..701843dc
--- /dev/null
+++ b/src/components/Settings/SettingsToggle.tsx
@@ -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 (
+
+ {label}
+
+
+ );
+}
diff --git a/src/components/Settings/ThemeSelector.tsx b/src/components/Settings/ThemeSelector.tsx
new file mode 100644
index 00000000..d32681b9
--- /dev/null
+++ b/src/components/Settings/ThemeSelector.tsx
@@ -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 (
+
+ );
+};
+
+export default ThemeSelector;
\ No newline at end of file
diff --git a/src/components/Settings/index.tsx b/src/components/Settings/index.tsx
new file mode 100644
index 00000000..073b98a5
--- /dev/null
+++ b/src/components/Settings/index.tsx
@@ -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 (
+
+ );
+}
+
+function SettingItem({ icon: Icon, title, description, action }: any) {
+ return (
+
+
+
+
+
+
+
{title}
+
{description}
+
+
+ {action}
+
+ );
+}
+
+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 (
+
+
+
+
+
+ {sections.map((section) => (
+ setActiveSection(section.id)}
+ />
+ ))}
+
+
+
+ {/* Main Content */}
+
+
+
+ Settings
+
+
+
+
+ English
+
+
+ }
+ />
+
+ 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"
+ >
+
+
+ }
+ />
+
+
+ Configure
+
+ }
+ />
+
+
+
+
+ }
+ />
+
+
+
+
+
+
+ );
+}
+
+export default AppSettings;
diff --git a/src/components/Settings/index2.tsx b/src/components/Settings/index2.tsx
new file mode 100644
index 00000000..d4ea798e
--- /dev/null
+++ b/src/components/Settings/index2.tsx
@@ -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("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 (
+
+
+
+
+
+
Coco Settings
+
+
+
+
+ {tabs.map((tab) => (
+
+ `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.name}
+
+ ))}
+
+
+
+
+
+
+
+
+
+
+
+ Extensions settings content
+
+
+
+
+
+
+ Account settings content
+
+
+
+
+
+
+ Organizations settings content
+
+
+
+
+
+
+ Advanced settings content
+
+
+
+
+
+
+ About settings content
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+export function ThemeOption({
+ icon: Icon,
+ title,
+ theme,
+}: {
+ icon: any;
+ title: string;
+ theme: Theme;
+}) {
+ const { theme: currentTheme, setTheme } = useContext(ThemeContext);
+ const isSelected = currentTheme === theme;
+
+ return (
+
+ );
+}
+
+export default SettingsPage;
diff --git a/src/components/ThemeProvider.tsx b/src/components/ThemeProvider.tsx
new file mode 100644
index 00000000..b64c0e9c
--- /dev/null
+++ b/src/components/ThemeProvider.tsx
@@ -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(initialState);
+
+export function ThemeProvider({
+ children,
+ defaultTheme = "system",
+ storageKey = "theme",
+ ...props
+}: ThemeProviderProps) {
+ const [theme, setTheme] = useState(
+ () => (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 (
+
+ {children}
+
+ );
+}
+
+export const useTheme = () => {
+ const context = useContext(ThemeProviderContext);
+
+ if (context === undefined)
+ throw new Error("useTheme 必须在 ThemeProvider 中使用");
+
+ return context;
+};
diff --git a/src/components/ThemeToggle.tsx b/src/components/ThemeToggle.tsx
new file mode 100644
index 00000000..3e8f9a9d
--- /dev/null
+++ b/src/components/ThemeToggle.tsx
@@ -0,0 +1,24 @@
+import { Sun, Moon } from "lucide-react";
+
+import { useTheme } from "./ThemeProvider";
+
+const ThemeToggle = () => {
+ const { theme, setTheme } = useTheme();
+ return (
+
+ );
+};
+
+export default ThemeToggle;
diff --git a/src/error-page.tsx b/src/error-page.tsx
new file mode 100644
index 00000000..d71f115e
--- /dev/null
+++ b/src/error-page.tsx
@@ -0,0 +1,16 @@
+import { useRouteError } from "react-router-dom";
+
+export default function ErrorPage() {
+ const error: any = useRouteError();
+ console.error(error);
+
+ return (
+
+
Oops!
+
Sorry, an unexpected error has occurred.
+
+ {error.statusText || error.message}
+
+
+ );
+}
diff --git a/src/hooks/useEscape.ts b/src/hooks/useEscape.ts
new file mode 100644
index 00000000..be36a2e8
--- /dev/null
+++ b/src/hooks/useEscape.ts
@@ -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;
diff --git a/src/i18n.ts b/src/i18n.ts
new file mode 100644
index 00000000..0cd3ed2c
--- /dev/null
+++ b/src/i18n.ts
@@ -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;
diff --git a/src/icons/ArrowLeft.tsx b/src/icons/ArrowLeft.tsx
new file mode 100644
index 00000000..31f6a2df
--- /dev/null
+++ b/src/icons/ArrowLeft.tsx
@@ -0,0 +1,12 @@
+import SVGWrap from "./SVGWrap";
+
+export default function ArrowLeft(props: I.SVG) {
+ return (
+
+
+
+ );
+}
diff --git a/src/icons/Ask.tsx b/src/icons/Ask.tsx
new file mode 100644
index 00000000..81989c39
--- /dev/null
+++ b/src/icons/Ask.tsx
@@ -0,0 +1,16 @@
+import SVGWrap from "./SVGWrap";
+
+export default function Ask(props: I.SVG) {
+ return (
+
+
+
+ );
+}
diff --git a/src/icons/Link.tsx b/src/icons/Link.tsx
new file mode 100644
index 00000000..77a10318
--- /dev/null
+++ b/src/icons/Link.tsx
@@ -0,0 +1,12 @@
+import SVGWrap from "./SVGWrap";
+
+export default function Link(props: I.SVG) {
+ return (
+
+
+
+ );
+}
diff --git a/src/icons/Pin.tsx b/src/icons/Pin.tsx
new file mode 100644
index 00000000..96bd56dd
--- /dev/null
+++ b/src/icons/Pin.tsx
@@ -0,0 +1,13 @@
+import SVGWrap from "./SVGWrap";
+
+export default function Pin(props: I.SVG) {
+ return (
+
+
+
+ );
+}
diff --git a/src/icons/Reload.tsx b/src/icons/Reload.tsx
new file mode 100644
index 00000000..ba353842
--- /dev/null
+++ b/src/icons/Reload.tsx
@@ -0,0 +1,12 @@
+import SVGWrap from "./SVGWrap";
+
+export default function Reload(props: I.SVG) {
+ return (
+
+
+
+ );
+}
diff --git a/src/icons/SVGWrap.tsx b/src/icons/SVGWrap.tsx
new file mode 100644
index 00000000..6c3e0014
--- /dev/null
+++ b/src/icons/SVGWrap.tsx
@@ -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 (
+
+
+
+ );
+}
diff --git a/src/icons/Send.tsx b/src/icons/Send.tsx
new file mode 100644
index 00000000..47f0a6cd
--- /dev/null
+++ b/src/icons/Send.tsx
@@ -0,0 +1,15 @@
+import SVGWrap from "./SVGWrap";
+
+export default function Send(props: I.SVG) {
+ return (
+
+
+
+
+
+
+ );
+}
diff --git a/src/icons/Setting.tsx b/src/icons/Setting.tsx
new file mode 100644
index 00000000..91ca113a
--- /dev/null
+++ b/src/icons/Setting.tsx
@@ -0,0 +1,12 @@
+import SVGWrap from "./SVGWrap";
+
+export default function Setting(props: I.SVG) {
+ return (
+
+
+
+
+
+
+ );
+}
diff --git a/src/icons/ThemeDark.tsx b/src/icons/ThemeDark.tsx
new file mode 100644
index 00000000..e63ca8cc
--- /dev/null
+++ b/src/icons/ThemeDark.tsx
@@ -0,0 +1,12 @@
+import SVGWrap from "./SVGWrap";
+
+export default function ThemeDark(props: I.SVG) {
+ return (
+
+
+
+ );
+}
diff --git a/src/icons/ThemeLight.tsx b/src/icons/ThemeLight.tsx
new file mode 100644
index 00000000..15b30b2f
--- /dev/null
+++ b/src/icons/ThemeLight.tsx
@@ -0,0 +1,12 @@
+import SVGWrap from "./SVGWrap";
+
+export default function ThemeLight(props: I.SVG) {
+ return (
+
+
+
+ );
+}
diff --git a/src/icons/ThemeSystem.tsx b/src/icons/ThemeSystem.tsx
new file mode 100644
index 00000000..896faeb3
--- /dev/null
+++ b/src/icons/ThemeSystem.tsx
@@ -0,0 +1,12 @@
+import SVGWrap from "./SVGWrap";
+
+export default function ThemeSystem(props: I.SVG) {
+ return (
+
+
+
+ );
+}
diff --git a/src/icons/UnPin.tsx b/src/icons/UnPin.tsx
new file mode 100644
index 00000000..6db96a5d
--- /dev/null
+++ b/src/icons/UnPin.tsx
@@ -0,0 +1,12 @@
+import SVGWrap from "./SVGWrap";
+
+export default function UnPin(props: I.SVG) {
+ return (
+
+
+
+ );
+}
diff --git a/src/index.d.ts b/src/index.d.ts
new file mode 100644
index 00000000..53d18027
--- /dev/null
+++ b/src/index.d.ts
@@ -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 {
+ children?: React.ReactNode;
+ size?: number;
+ title?: string;
+ action?: boolean;
+ onClick?: (e: React.MouseEvent) => void;
+ }
+}
+
+declare global {
+ interface Window {
+ __TAURI__: Record;
+ }
+}
diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json
new file mode 100644
index 00000000..d8dc32dc
--- /dev/null
+++ b/src/locales/en/translation.json
@@ -0,0 +1,7 @@
+{
+ "welcome": "Welcome to Coco App",
+ "home": "Home",
+ "settings": "Settings",
+ "activeTheme": "Current theme:",
+ "InputMessage": "Input your message here..."
+}
diff --git a/src/locales/zh/translation.json b/src/locales/zh/translation.json
new file mode 100644
index 00000000..e9803e7a
--- /dev/null
+++ b/src/locales/zh/translation.json
@@ -0,0 +1,7 @@
+{
+ "welcome": "欢迎使用 Coco App",
+ "home": "主页",
+ "settings": "设置",
+ "activeTheme": "当前主题:",
+ "InputMessage": "在此输入您的消息..."
+}
diff --git a/src/main.css b/src/main.css
new file mode 100644
index 00000000..353f6452
--- /dev/null
+++ b/src/main.css
@@ -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;
+ }
+}
diff --git a/src/main.tsx b/src/main.tsx
index 2be325ed..14c8f56b 100644
--- a/src/main.tsx
+++ b/src/main.tsx
@@ -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(
-
- ,
+
+
+
+
);
diff --git a/src/routes/index.tsx b/src/routes/index.tsx
new file mode 100644
index 00000000..b926122e
--- /dev/null
+++ b/src/routes/index.tsx
@@ -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: ,
+ errorElement: ,
+ },
+ {
+ path: "/settings",
+ element: ,
+ errorElement: ,
+ },
+]);
diff --git a/src/routes/root.tsx b/src/routes/root.tsx
new file mode 100644
index 00000000..bf579506
--- /dev/null
+++ b/src/routes/root.tsx
@@ -0,0 +1,43 @@
+export default function Root() {
+ return (
+ <>
+
+
+ >
+ );
+}
\ No newline at end of file
diff --git a/src/stores/themeStore.ts b/src/stores/themeStore.ts
new file mode 100644
index 00000000..599ccbbf
--- /dev/null
+++ b/src/stores/themeStore.ts
@@ -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()(
+ // 持久化中间件
+ 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")
+ ),
+ }
+ )
+);
diff --git a/tailwind.config.js b/tailwind.config.js
new file mode 100644
index 00000000..24066172
--- /dev/null
+++ b/tailwind.config.js
@@ -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) / )",
+ secondary: "rgb(var(--color-secondary) / )",
+ background: "rgb(var(--color-background) / )",
+ foreground: "rgb(var(--color-foreground) / )",
+ separator: "rgb(var(--color-separator) / )",
+ },
+ textColor: {
+ primary: "rgb(var(--color-foreground) / )",
+ },
+ animation: {
+ "fade-in": "fade-in 0.2s ease-in-out",
+ },
+ keyframes: {
+ "fade-in": {
+ "0%": { opacity: "0" },
+ "100%": { opacity: "1" },
+ },
+ },
+ },
+ },
+ plugins: [],
+ mode: "jit",
+ darkMode: "class",
+};