From 0d6156cc1b9486bb869390fe9d68273bd75bb8bd Mon Sep 17 00:00:00 2001 From: MyPrototypeWhat Date: Wed, 30 Jul 2025 19:42:58 +0800 Subject: [PATCH] feat(discover): implement Discover feature with routing and UI components - Added a new Discover page with sidebar and main content areas. - Integrated routing for Discover, including subcategories and tabs. - Created components for Discover sidebar and main content. - Updated localization files to include new Discover titles and labels. - Refactored existing components to accommodate the new Discover feature. - Enhanced sidebar icons and navigation for better user experience. --- .prettierrc | 4 +- components.json | 21 + electron.vite.config.ts | 1 + eslint.config.mjs | 3 +- package.json | 20 +- src/renderer/src/Router.tsx | 8 +- src/renderer/src/assets/styles/tailwind.css | 146 ++ .../components/Popups/AddAssistantPopup.tsx | 2 +- .../src/components/Tab/TabContainer.tsx | 13 +- src/renderer/src/components/app/Sidebar.tsx | 23 +- src/renderer/src/context/ThemeProvider.tsx | 1 + src/renderer/src/entryPoint.tsx | 1 + src/renderer/src/hooks/use-mobile.ts | 19 + src/renderer/src/i18n/label.ts | 7 +- src/renderer/src/i18n/locales/zh-cn.json | 7 + .../discover/components/DiscoverMain.tsx | 37 + .../discover/components/DiscoverSidebar.tsx | 64 + .../discover/hooks/useDiscoverCategories.ts | 118 ++ src/renderer/src/pages/discover/index.tsx | 63 + .../pages}/agents/AgentsPage.tsx | 50 +- .../pages}/agents/agentGroupTranslations.ts | 0 .../agents/components/AddAgentPopup.tsx | 0 .../pages}/agents/components/AgentCard.tsx | 0 .../agents/components/AgentGroupIcon.tsx | 2 +- .../agents/components/ImportAgentPopup.tsx | 0 .../agents/components/ManageAgentsPopup.tsx | 0 .../{ => discover/pages}/agents/index.ts | 0 .../pages}/minapps/MinAppsPage.tsx | 6 +- .../MiniappSettings/MinappSettingsPopup.tsx | 0 .../MiniappSettings/MiniAppIconsManager.tsx | 0 .../MiniappSettings/MiniAppSettings.tsx | 0 .../pages}/minapps/NewAppButton.tsx | 0 src/renderer/src/pages/discover/routers.ts | 62 + .../src/pages/discover/utils/index.ts | 0 src/renderer/src/pages/discover/utils/util.ts | 0 .../src/pages/launchpad/LaunchpadPage.tsx | 4 +- .../DisplaySettings/SidebarIconsManager.tsx | 5 +- src/renderer/src/store/index.ts | 2 +- src/renderer/src/store/migrate.ts | 21 + src/renderer/src/store/settings.ts | 17 +- src/renderer/src/store/tabs.ts | 2 +- src/renderer/src/types/cherryStore.ts | 24 + src/renderer/src/ui/badge.tsx | 46 + src/renderer/src/ui/button.tsx | 59 + src/renderer/src/ui/input.tsx | 21 + src/renderer/src/ui/separator.tsx | 28 + src/renderer/src/ui/sheet.tsx | 137 ++ src/renderer/src/ui/sidebar.tsx | 724 ++++++++++ src/renderer/src/ui/skeleton.tsx | 13 + src/renderer/src/ui/tooltip.tsx | 61 + src/renderer/src/ui/vercel-tabs.tsx | 115 ++ src/renderer/src/utils/index.ts | 6 + tsconfig.json | 6 +- tsconfig.node.json | 5 +- vitest.config.ts | 3 +- yarn.lock | 1176 ++++++++++++++++- 56 files changed, 3049 insertions(+), 104 deletions(-) create mode 100644 components.json create mode 100644 src/renderer/src/assets/styles/tailwind.css create mode 100644 src/renderer/src/hooks/use-mobile.ts create mode 100644 src/renderer/src/pages/discover/components/DiscoverMain.tsx create mode 100644 src/renderer/src/pages/discover/components/DiscoverSidebar.tsx create mode 100644 src/renderer/src/pages/discover/hooks/useDiscoverCategories.ts create mode 100644 src/renderer/src/pages/discover/index.tsx rename src/renderer/src/pages/{ => discover/pages}/agents/AgentsPage.tsx (91%) rename src/renderer/src/pages/{ => discover/pages}/agents/agentGroupTranslations.ts (100%) rename src/renderer/src/pages/{ => discover/pages}/agents/components/AddAgentPopup.tsx (100%) rename src/renderer/src/pages/{ => discover/pages}/agents/components/AgentCard.tsx (100%) rename src/renderer/src/pages/{ => discover/pages}/agents/components/AgentGroupIcon.tsx (94%) rename src/renderer/src/pages/{ => discover/pages}/agents/components/ImportAgentPopup.tsx (100%) rename src/renderer/src/pages/{ => discover/pages}/agents/components/ManageAgentsPopup.tsx (100%) rename src/renderer/src/pages/{ => discover/pages}/agents/index.ts (100%) rename src/renderer/src/pages/{ => discover/pages}/minapps/MinAppsPage.tsx (97%) rename src/renderer/src/pages/{ => discover/pages}/minapps/MiniappSettings/MinappSettingsPopup.tsx (100%) rename src/renderer/src/pages/{ => discover/pages}/minapps/MiniappSettings/MiniAppIconsManager.tsx (100%) rename src/renderer/src/pages/{ => discover/pages}/minapps/MiniappSettings/MiniAppSettings.tsx (100%) rename src/renderer/src/pages/{ => discover/pages}/minapps/NewAppButton.tsx (100%) create mode 100644 src/renderer/src/pages/discover/routers.ts create mode 100644 src/renderer/src/pages/discover/utils/index.ts create mode 100644 src/renderer/src/pages/discover/utils/util.ts create mode 100644 src/renderer/src/types/cherryStore.ts create mode 100644 src/renderer/src/ui/badge.tsx create mode 100644 src/renderer/src/ui/button.tsx create mode 100644 src/renderer/src/ui/input.tsx create mode 100644 src/renderer/src/ui/separator.tsx create mode 100644 src/renderer/src/ui/sheet.tsx create mode 100644 src/renderer/src/ui/sidebar.tsx create mode 100644 src/renderer/src/ui/skeleton.tsx create mode 100644 src/renderer/src/ui/tooltip.tsx create mode 100644 src/renderer/src/ui/vercel-tabs.tsx diff --git a/.prettierrc b/.prettierrc index 85e2eb0ca6..7a06761104 100644 --- a/.prettierrc +++ b/.prettierrc @@ -3,9 +3,11 @@ "endOfLine": "lf", "jsonRecursiveSort": true, "jsonSortOrder": "{\"*\": \"lexical\"}", - "plugins": ["prettier-plugin-sort-json"], + "plugins": ["prettier-plugin-sort-json", "prettier-plugin-tailwindcss"], "printWidth": 120, "semi": false, "singleQuote": true, + "tailwindFunctions": ["clsx"], + "tailwindStylesheet": "./src/renderer/src/assets/styles/tailwind.css", "trailingComma": "none" } diff --git a/components.json b/components.json new file mode 100644 index 0000000000..06461d66da --- /dev/null +++ b/components.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": false, + "tsx": true, + "tailwind": { + "config": "", + "css": "src/renderer/src/assets/styles/tailwind.css", + "baseColor": "zinc", + "cssVariables": true, + "prefix": "" + }, + "aliases": { + "components": "@renderer/ui/third-party", + "utils": "@renderer/utils", + "ui": "@renderer/ui", + "lib": "@renderer/lib", + "hooks": "@renderer/hooks" + }, + "iconLibrary": "lucide" +} \ No newline at end of file diff --git a/electron.vite.config.ts b/electron.vite.config.ts index bf64d71992..55b60a76d7 100644 --- a/electron.vite.config.ts +++ b/electron.vite.config.ts @@ -60,6 +60,7 @@ export default defineConfig({ }, renderer: { plugins: [ + (async () => (await import('@tailwindcss/vite')).default())(), react({ tsDecorators: true, plugins: [ diff --git a/eslint.config.mjs b/eslint.config.mjs index abaadac841..341c152e08 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -122,7 +122,8 @@ export default defineConfig([ '.yarn/**', '.gitignore', 'scripts/cloudflare-worker.js', - 'src/main/integration/nutstore/sso/lib/**' + 'src/main/integration/nutstore/sso/lib/**', + 'src/renderer/src/ui/**' ] } ]) diff --git a/package.json b/package.json index 93b21a23fd..36cd499ed3 100644 --- a/package.json +++ b/package.json @@ -132,9 +132,17 @@ "@opentelemetry/sdk-trace-node": "^2.0.0", "@opentelemetry/sdk-trace-web": "^2.0.0", "@playwright/test": "^1.52.0", + "@radix-ui/react-collapsible": "^1.1.10", + "@radix-ui/react-dialog": "^1.1.14", + "@radix-ui/react-dropdown-menu": "^2.1.14", + "@radix-ui/react-separator": "^1.1.7", + "@radix-ui/react-slot": "^1.2.3", + "@radix-ui/react-tabs": "^1.1.11", + "@radix-ui/react-tooltip": "^1.2.7", "@reduxjs/toolkit": "^2.2.5", "@shikijs/markdown-it": "^3.7.0", "@swc/plugin-styled-components": "^7.1.5", + "@tailwindcss/vite": "^4.1.5", "@tanstack/react-query": "^5.27.0", "@tanstack/react-virtual": "^3.13.12", "@testing-library/dom": "^10.4.0", @@ -178,7 +186,9 @@ "axios": "^1.7.3", "browser-image-compression": "^2.0.2", "chardet": "^2.1.0", + "class-variance-authority": "^0.7.1", "cli-progress": "^3.12.0", + "clsx": "^2.1.1", "code-inspector-plugin": "^0.20.14", "color": "^5.0.0", "country-flag-emoji-polyfill": "0.1.8", @@ -219,12 +229,13 @@ "lint-staged": "^15.5.0", "lodash": "^4.17.21", "lru-cache": "^11.1.0", - "lucide-react": "^0.525.0", + "lucide-react": "^0.534.0", "macos-release": "^3.4.0", "markdown-it": "^14.1.0", "mermaid": "^11.7.0", "mime": "^4.0.4", - "motion": "^12.10.5", + "motion": "^12.12.1", + "next-themes": "^0.4.6", "notion-helper": "^1.3.22", "npx-scope-finder": "^1.2.0", "openai": "patch:openai@npm%3A5.1.0#~/.yarn/patches/openai-npm-5.1.0-0e7b3ccb07.patch", @@ -232,6 +243,7 @@ "playwright": "^1.52.0", "prettier": "^3.5.3", "prettier-plugin-sort-json": "^4.1.1", + "prettier-plugin-tailwindcss": "^0.6.11", "proxy-agent": "^6.5.0", "rc-virtual-list": "^3.18.6", "react": "^19.0.0", @@ -262,13 +274,17 @@ "strict-url-sanitise": "^0.0.1", "string-width": "^7.2.0", "styled-components": "^6.1.11", + "tailwind-merge": "^3.3.1", + "tailwindcss": "^4.1.5", "tar": "^7.4.3", "tiny-pinyin": "^1.3.2", "tokenx": "^1.1.0", "tsx": "^4.20.3", + "tw-animate-css": "^1.3.6", "typescript": "^5.6.2", "undici": "6.21.2", "unified": "^11.0.5", + "usehooks-ts": "^3.1.1", "uuid": "^10.0.0", "vite": "npm:rolldown-vite@latest", "vitest": "^3.2.4", diff --git a/src/renderer/src/Router.tsx b/src/renderer/src/Router.tsx index 624c6ccc47..132f42cf33 100644 --- a/src/renderer/src/Router.tsx +++ b/src/renderer/src/Router.tsx @@ -7,12 +7,11 @@ import Sidebar from './components/app/Sidebar' import TabsContainer from './components/Tab/TabContainer' import NavigationHandler from './handler/NavigationHandler' import { useNavbarPosition } from './hooks/useSettings' -import AgentsPage from './pages/agents/AgentsPage' +import DiscoverPage from './pages/discover' import FilesPage from './pages/files/FilesPage' import HomePage from './pages/home/HomePage' import KnowledgePage from './pages/knowledge/KnowledgePage' import LaunchpadPage from './pages/launchpad/LaunchpadPage' -import MinAppsPage from './pages/minapps/MinAppsPage' import PaintingsRoutePage from './pages/paintings/PaintingsRoutePage' import SettingsPage from './pages/settings/SettingsPage' import TranslatePage from './pages/translate/TranslatePage' @@ -24,14 +23,15 @@ const Router: FC = () => { return ( } /> - } /> + {/* } /> */} } /> } /> } /> } /> - } /> + {/* } /> */} } /> } /> + } /> ) }, []) diff --git a/src/renderer/src/assets/styles/tailwind.css b/src/renderer/src/assets/styles/tailwind.css new file mode 100644 index 0000000000..1cb99ed026 --- /dev/null +++ b/src/renderer/src/assets/styles/tailwind.css @@ -0,0 +1,146 @@ +@import 'tailwindcss' source('../../../../renderer'); +@import 'tw-animate-css'; + +@custom-variant dark (&:is(.dark *)); + +/* 如需自定义: +1. 清晰地组织自定义 CSS 到相应的层中。 +2. 基础样式(如全局重置、链接样式)放入 base 层; +3. 可复用的组件样式(如果仍使用 @apply 或原生 CSS 嵌套创建)放入 components 层; +4. 新的自定义工具类放入 utilities 层。 +*/ + +:root { + --radius: 0.625rem; + --background: oklch(1 0 0); + --foreground: oklch(0.141 0.005 285.823); + --card: oklch(1 0 0); + --card-foreground: oklch(0.141 0.005 285.823); + --popover: oklch(1 0 0); + --popover-foreground: oklch(0.141 0.005 285.823); + --primary: oklch(0.21 0.006 285.885); + --primary-foreground: oklch(0.985 0 0); + --secondary: oklch(0.967 0.001 286.375); + --secondary-foreground: oklch(0.21 0.006 285.885); + --muted: oklch(0.967 0.001 286.375); + --muted-foreground: oklch(0.552 0.016 285.938); + --accent: oklch(0.967 0.001 286.375); + --accent-foreground: oklch(0.21 0.006 285.885); + --destructive: oklch(0.577 0.245 27.325); + --border: oklch(0.92 0.004 286.32); + --input: oklch(0.92 0.004 286.32); + --ring: oklch(0.705 0.015 286.067); + --chart-1: oklch(0.646 0.222 41.116); + --chart-2: oklch(0.6 0.118 184.704); + --chart-3: oklch(0.398 0.07 227.392); + --chart-4: oklch(0.828 0.189 84.429); + --chart-5: oklch(0.769 0.188 70.08); + --sidebar: oklch(0.985 0 0); + --sidebar-foreground: oklch(0.141 0.005 285.823); + --sidebar-primary: oklch(0.21 0.006 285.885); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.967 0.001 286.375); + --sidebar-accent-foreground: oklch(0.21 0.006 285.885); + --sidebar-border: oklch(0.92 0.004 286.32); + --sidebar-ring: oklch(0.705 0.015 286.067); +} + +.dark { + --background: oklch(0.141 0.005 285.823); + --foreground: oklch(0.985 0 0); + --card: oklch(0.21 0.006 285.885); + --card-foreground: oklch(0.985 0 0); + --popover: oklch(0.21 0.006 285.885); + --popover-foreground: oklch(0.985 0 0); + --primary: oklch(0.92 0.004 286.32); + --primary-foreground: oklch(0.21 0.006 285.885); + --secondary: oklch(0.274 0.006 286.033); + --secondary-foreground: oklch(0.985 0 0); + --muted: oklch(0.274 0.006 286.033); + --muted-foreground: oklch(0.705 0.015 286.067); + --accent: oklch(0.274 0.006 286.033); + --accent-foreground: oklch(0.985 0 0); + --destructive: oklch(0.704 0.191 22.216); + --border: oklch(1 0 0 / 10%); + --input: oklch(1 0 0 / 15%); + --ring: oklch(0.552 0.016 285.938); + --chart-1: oklch(0.488 0.243 264.376); + --chart-2: oklch(0.696 0.17 162.48); + --chart-3: oklch(0.769 0.188 70.08); + --chart-4: oklch(0.627 0.265 303.9); + --chart-5: oklch(0.645 0.246 16.439); + --sidebar: oklch(0.21 0.006 285.885); + --sidebar-foreground: oklch(0.985 0 0); + --sidebar-primary: oklch(0.488 0.243 264.376); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.274 0.006 286.033); + --sidebar-accent-foreground: oklch(0.985 0 0); + --sidebar-border: oklch(1 0 0 / 10%); + --sidebar-ring: oklch(0.552 0.016 285.938); +} + +@theme inline { + --color-background: var(--background); + --color-foreground: var(--foreground); + --color-card: var(--card); + --color-card-foreground: var(--card-foreground); + --color-popover: var(--popover); + --color-popover-foreground: var(--popover-foreground); + --color-primary: var(--primary); + --color-primary-foreground: var(--primary-foreground); + --color-secondary: var(--secondary); + --color-secondary-foreground: var(--secondary-foreground); + --color-muted: var(--muted); + --color-muted-foreground: var(--muted-foreground); + --color-accent: var(--accent); + --color-accent-foreground: var(--accent-foreground); + --color-destructive: var(--destructive); + --color-destructive-foreground: var(--destructive-foreground); + --color-border: var(--border); + --color-input: var(--input); + --color-ring: var(--ring); + --color-chart-1: var(--chart-1); + --color-chart-2: var(--chart-2); + --color-chart-3: var(--chart-3); + --color-chart-4: var(--chart-4); + --color-chart-5: var(--chart-5); + --radius-sm: calc(var(--radius) - 4px); + --radius-md: calc(var(--radius) - 2px); + --radius-lg: var(--radius); + --radius-xl: calc(var(--radius) + 4px); + --color-sidebar: var(--sidebar); + --color-sidebar-foreground: var(--sidebar-foreground); + --color-sidebar-primary: var(--sidebar-primary); + --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); + --color-sidebar-accent: var(--sidebar-accent); + --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); + --color-sidebar-border: var(--sidebar-border); + --color-sidebar-ring: var(--sidebar-ring); + --animate-marquee: marquee var(--duration) infinite linear; + --animate-marquee-vertical: marquee-vertical var(--duration) linear infinite; + @keyframes marquee { + from { + transform: translateX(0); + } + to { + transform: translateX(calc(-100% - var(--gap))); + } + } + @keyframes marquee-vertical { + from { + transform: translateY(0); + } + to { + transform: translateY(calc(-100% - var(--gap))); + } + } +} + +@layer base { + * { + @apply border-border outline-ring/50; + } + body { + @apply bg-background text-foreground; + } +} diff --git a/src/renderer/src/components/Popups/AddAssistantPopup.tsx b/src/renderer/src/components/Popups/AddAssistantPopup.tsx index c8bfdb0276..ec55d646c1 100644 --- a/src/renderer/src/components/Popups/AddAssistantPopup.tsx +++ b/src/renderer/src/components/Popups/AddAssistantPopup.tsx @@ -1,7 +1,7 @@ import { TopView } from '@renderer/components/TopView' import { useAgents } from '@renderer/hooks/useAgents' import { useAssistants, useDefaultAssistant } from '@renderer/hooks/useAssistant' -import { useSystemAgents } from '@renderer/pages/agents' +import { useSystemAgents } from '@renderer/pages/discover/pages/agents' import { createAssistantFromAgent } from '@renderer/services/AssistantService' import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService' import { Agent, Assistant } from '@renderer/types' diff --git a/src/renderer/src/components/Tab/TabContainer.tsx b/src/renderer/src/components/Tab/TabContainer.tsx index 35c80fc5e6..5dccbd808a 100644 --- a/src/renderer/src/components/Tab/TabContainer.tsx +++ b/src/renderer/src/components/Tab/TabContainer.tsx @@ -11,15 +11,14 @@ import { addTab, removeTab, setActiveTab } from '@renderer/store/tabs' import { ThemeMode } from '@renderer/types' import { classNames } from '@renderer/utils' import { + Compass, FileSearch, Folder, Home, Languages, - LayoutGrid, Moon, Palette, Settings, - Sparkle, SquareTerminal, Sun, X @@ -38,14 +37,14 @@ const getTabIcon = (tabId: string): React.ReactNode | undefined => { switch (tabId) { case 'home': return - case 'agents': - return + // case 'agents': + // return case 'translate': return case 'paintings': return - case 'apps': - return + // case 'apps': + // return case 'knowledge': return case 'mcp': @@ -54,6 +53,8 @@ const getTabIcon = (tabId: string): React.ReactNode | undefined => { return case 'settings': return + case 'discover': + return default: return null } diff --git a/src/renderer/src/components/app/Sidebar.tsx b/src/renderer/src/components/app/Sidebar.tsx index 5325b0a4b4..944a390fe1 100644 --- a/src/renderer/src/components/app/Sidebar.tsx +++ b/src/renderer/src/components/app/Sidebar.tsx @@ -16,15 +16,14 @@ import { isEmoji } from '@renderer/utils' import { Avatar, Tooltip } from 'antd' import { CircleHelp, + Compass, FileSearch, Folder, Languages, - LayoutGrid, MessageSquare, Moon, Palette, Settings, - Sparkle, Sun } from 'lucide-react' import { FC } from 'react' @@ -38,7 +37,7 @@ import { SidebarOpenedMinappTabs, SidebarPinnedApps } from './PinnedMinapps' const Sidebar: FC = () => { const { hideMinappPopup, openMinapp } = useMinappPopup() const { minappShow, currentMinappId } = useRuntime() - const { sidebarIcons } = useSettings() + // const { sidebarIcons } = useSettings() const { pinned } = useMinapps() const { pathname } = useLocation() @@ -52,8 +51,8 @@ const Sidebar: FC = () => { const backgroundColor = useNavBackgroundColor() - const showPinnedApps = pinned.length > 0 && sidebarIcons.visible.includes('minapp') - + // const showPinnedApps = pinned.length > 0 && sidebarIcons.visible.includes('minapp') + const showPinnedApps = pinned.length > 0 const to = async (path: string) => { await modelGenerating() navigate(path) @@ -141,22 +140,24 @@ const MainMenus: FC = () => { const iconMap = { assistants: , - agents: , + // agents: , paintings: , translate: , - minapp: , + // minapp: , knowledge: , - files: + files: , + discover: } const pathMap = { assistants: '/', - agents: '/agents', + // agents: '/agents', paintings: `/paintings/${defaultPaintingProvider}`, translate: '/translate', - minapp: '/apps', + // minapp: '/apps', knowledge: '/knowledge', - files: '/files' + files: '/files', + discover: '/discover' } return sidebarIcons.visible.map((icon) => { diff --git a/src/renderer/src/context/ThemeProvider.tsx b/src/renderer/src/context/ThemeProvider.tsx index 71755a0dc2..9e4d6d4572 100644 --- a/src/renderer/src/context/ThemeProvider.tsx +++ b/src/renderer/src/context/ThemeProvider.tsx @@ -64,6 +64,7 @@ export const ThemeProvider: React.FC = ({ children }) => { useEffect(() => { window.api.setTheme(settedTheme) + // tailwindThemeChange(settedTheme) }, [settedTheme]) return ( diff --git a/src/renderer/src/entryPoint.tsx b/src/renderer/src/entryPoint.tsx index bf6a3cb6a5..ad7b213778 100644 --- a/src/renderer/src/entryPoint.tsx +++ b/src/renderer/src/entryPoint.tsx @@ -1,3 +1,4 @@ +import './assets/styles/tailwind.css' import './assets/styles/index.scss' import '@ant-design/v5-patch-for-react-19' diff --git a/src/renderer/src/hooks/use-mobile.ts b/src/renderer/src/hooks/use-mobile.ts new file mode 100644 index 0000000000..2b0fe1dfef --- /dev/null +++ b/src/renderer/src/hooks/use-mobile.ts @@ -0,0 +1,19 @@ +import * as React from "react" + +const MOBILE_BREAKPOINT = 768 + +export function useIsMobile() { + const [isMobile, setIsMobile] = React.useState(undefined) + + React.useEffect(() => { + const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`) + const onChange = () => { + setIsMobile(window.innerWidth < MOBILE_BREAKPOINT) + } + mql.addEventListener("change", onChange) + setIsMobile(window.innerWidth < MOBILE_BREAKPOINT) + return () => mql.removeEventListener("change", onChange) + }, []) + + return !!isMobile +} diff --git a/src/renderer/src/i18n/label.ts b/src/renderer/src/i18n/label.ts index 4915ef5eba..a451b73019 100644 --- a/src/renderer/src/i18n/label.ts +++ b/src/renderer/src/i18n/label.ts @@ -93,8 +93,8 @@ export const getProgressLabel = (key: string): string => { } const titleKeyMap = { - agents: 'title.agents', - apps: 'title.apps', + // agents: 'title.agents', + // apps: 'title.apps', files: 'title.files', home: 'title.home', knowledge: 'title.knowledge', @@ -103,7 +103,8 @@ const titleKeyMap = { memories: 'title.memories', paintings: 'title.paintings', settings: 'title.settings', - translate: 'title.translate' + translate: 'title.translate', + discover: 'discover.title' } as const export const getTitleLabel = (key: string): string => { diff --git a/src/renderer/src/i18n/locales/zh-cn.json b/src/renderer/src/i18n/locales/zh-cn.json index 1a7d633a33..99691177df 100644 --- a/src/renderer/src/i18n/locales/zh-cn.json +++ b/src/renderer/src/i18n/locales/zh-cn.json @@ -729,6 +729,13 @@ "warning": "警告", "you": "用户" }, + "discover": { + "install": "安装", + "title": "发现", + "uninstall": "卸载", + "update": "更新", + "update_all": "全部更新" + }, "docs": { "title": "帮助文档" }, diff --git a/src/renderer/src/pages/discover/components/DiscoverMain.tsx b/src/renderer/src/pages/discover/components/DiscoverMain.tsx new file mode 100644 index 0000000000..e1940e30d5 --- /dev/null +++ b/src/renderer/src/pages/discover/components/DiscoverMain.tsx @@ -0,0 +1,37 @@ +import React, { Suspense } from 'react' +import { Navigate, Route, Routes, useLocation } from 'react-router-dom' + +import { discoverRouters, InternalCategory } from '../routers' + +export interface DiscoverContentProps { + activeTabId: string // This should be one of the CherryStoreType values, e.g., "Assistant" + // selectedSubcategoryId: string + currentCategory: InternalCategory | undefined +} + +const DiscoverContent: React.FC = ({ activeTabId, currentCategory }) => { + const location = useLocation() // To see the current path for debugging or more complex logic + + if (!currentCategory || !activeTabId) { + return
Loading: Category or Tab ID missing...
+ } + + if (!activeTabId && !location.pathname.startsWith('/discover/')) { + return // Fallback redirect, adjust as needed + } + + return ( + Loading...}> + + {discoverRouters.map((_Route) => { + if (!_Route.component) return null + return } /> + })} + + Discover Feature Not Found at {location.pathname}} /> + + + ) +} + +export default DiscoverContent diff --git a/src/renderer/src/pages/discover/components/DiscoverSidebar.tsx b/src/renderer/src/pages/discover/components/DiscoverSidebar.tsx new file mode 100644 index 0000000000..aa60d965ef --- /dev/null +++ b/src/renderer/src/pages/discover/components/DiscoverSidebar.tsx @@ -0,0 +1,64 @@ +import { SubCategoryItem } from '@renderer/types/cherryStore' +import { Badge } from '@renderer/ui/badge' +import { + Sidebar, + SidebarContent, + SidebarMenu, + SidebarMenuButton, + SidebarMenuSubItem, + SidebarProvider +} from '@renderer/ui/sidebar' + +import { InternalCategory } from '../hooks/useDiscoverCategories' + +interface DiscoverSidebarProps { + activeCategory: InternalCategory | undefined + selectedSubcategory: string + onSelectSubcategory: (subcategoryId: string, row?: SubCategoryItem) => void +} + +export default function DiscoverSidebar({ + activeCategory, + selectedSubcategory, + onSelectSubcategory +}: DiscoverSidebarProps) { + if (!activeCategory) { + return ( + + +

No active category selected.

+
+
+ ) + } + + return ( + + + + + {activeCategory.items && + activeCategory.items.length > 0 && + activeCategory.items.map((subItem) => ( + + { + onSelectSubcategory(subItem.id, subItem) + }} + size="sm"> + {subItem.name} + {typeof subItem.count === 'number' && ( + + {subItem.count} + + )} + + + ))} + + + + + ) +} diff --git a/src/renderer/src/pages/discover/hooks/useDiscoverCategories.ts b/src/renderer/src/pages/discover/hooks/useDiscoverCategories.ts new file mode 100644 index 0000000000..067a2b272b --- /dev/null +++ b/src/renderer/src/pages/discover/hooks/useDiscoverCategories.ts @@ -0,0 +1,118 @@ +import { useCallback, useEffect, useMemo, useRef, useState } from 'react' +import { useLocation, useNavigate } from 'react-router-dom' + +import { CATEGORY_REGISTRY, InternalCategory } from '../routers' + +// 导出接口供其他文件使用 +export type { InternalCategory } + +// Helper to find category by path +const findCategoryByPath = (path: string | undefined): InternalCategory | undefined => + CATEGORY_REGISTRY.find((cat) => cat.path === path) + +// Helper to find category by id (activeTab) +const findCategoryById = (id: string | undefined): InternalCategory | undefined => + CATEGORY_REGISTRY.find((cat) => cat.id === id) + +export function useDiscoverCategories() { + const [activeTab, setActiveTab] = useState('') + const [selectedSubcategory, setSelectedSubcategory] = useState('all') + + const navigate = useNavigate() + const location = useLocation() + + // 使用 useRef 来跟踪是否是用户手动导航,避免重复渲染 + const isUserNavigationRef = useRef(false) + + // URL 同步逻辑 - 适配新的 URL 格式 /discover/xxx?category=xxx + useEffect(() => { + const pathSegments = location.pathname.split('/').filter(Boolean) + const currentCategoryPath = pathSegments.length >= 2 && pathSegments[0] === 'discover' ? pathSegments[1] : undefined + const searchParams = new URLSearchParams(location.search) + const categoryFromQuery = searchParams.get('category') + const subcategoryFromQuery = searchParams.get('subcategory') || 'all' + + // 处理基础路径重定向 + if (location.pathname === '/discover' || location.pathname === '/discover/') { + if (CATEGORY_REGISTRY.length > 0) { + const firstCategory = CATEGORY_REGISTRY[0] + navigate(`/discover/${firstCategory.path}?category=${firstCategory.id}&subcategory=all`, { replace: true }) + } + return + } + + // 根据URL格式,优先使用 category 查询参数 + let targetCategoryId: string | null = categoryFromQuery + + // 如果没有 category 参数,尝试从路径推断 + if (!targetCategoryId && currentCategoryPath) { + const categoryFromPath = findCategoryByPath(currentCategoryPath) + targetCategoryId = categoryFromPath?.id || null + } + + // 处理无效分类重定向 + if (!targetCategoryId || !findCategoryById(targetCategoryId)) { + if (CATEGORY_REGISTRY.length > 0) { + const firstCategory = CATEGORY_REGISTRY[0] + navigate(`/discover/${firstCategory.path}?category=${firstCategory.id}&subcategory=all`, { replace: true }) + } + return + } + + // 只有当状态确实需要更新时才更新 + if (activeTab !== targetCategoryId) { + setActiveTab(targetCategoryId) + } + + if (selectedSubcategory !== subcategoryFromQuery) { + setSelectedSubcategory(subcategoryFromQuery) + } + + // 重置用户导航标记 + isUserNavigationRef.current = false + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [location.pathname, location.search, navigate]) // 故意不包含 activeTab 和 selectedSubcategory 以避免重复渲染 + + const currentCategory = useMemo(() => { + return findCategoryById(activeTab) + }, [activeTab]) + + // 优化的 Tab 选择处理,使用 useCallback 避免重复渲染 + // 更新为新的 URL 格式 /discover/xxx?category=xxx&subcategory=xxx + const handleSelectTab = useCallback( + (tabId: string) => { + if (activeTab === tabId) return // 如果已经是当前 tab,直接返回 + + const categoryToSelect = findCategoryById(tabId) + if (categoryToSelect?.path) { + isUserNavigationRef.current = true + navigate(`/discover/${categoryToSelect.path}?category=${tabId}&subcategory=all`) + } + }, + [activeTab, navigate] + ) + + // 优化的子分类选择处理 + const handleSelectSubcategory = useCallback( + (subcategoryId: string) => { + if (selectedSubcategory === subcategoryId) return // 如果已经是当前子分类,直接返回 + + const currentCatDetails = findCategoryById(activeTab) + if (currentCatDetails?.path) { + isUserNavigationRef.current = true + navigate(`/discover/${currentCatDetails.path}?category=${activeTab}&subcategory=${subcategoryId}`) + } + }, + [selectedSubcategory, activeTab, navigate] + ) + + return { + categories: CATEGORY_REGISTRY, // 直接返回静态注册表 + activeTab, + selectedSubcategory, + currentCategory, + handleSelectTab, + handleSelectSubcategory, + setActiveTab + } +} diff --git a/src/renderer/src/pages/discover/index.tsx b/src/renderer/src/pages/discover/index.tsx new file mode 100644 index 0000000000..ca7837e546 --- /dev/null +++ b/src/renderer/src/pages/discover/index.tsx @@ -0,0 +1,63 @@ +import { Navbar, NavbarCenter } from '@renderer/components/app/Navbar' +import { Tabs } from '@renderer/ui/vercel-tabs' +import { useMemo } from 'react' +import { useTranslation } from 'react-i18next' + +import DiscoverMain from './components/DiscoverMain' +import DiscoverSidebar from './components/DiscoverSidebar' +import { useDiscoverCategories } from './hooks/useDiscoverCategories' + +export default function DiscoverPage() { + const { t } = useTranslation() + const { categories, activeTab, selectedSubcategory, currentCategory, handleSelectTab, handleSelectSubcategory } = + useDiscoverCategories() + + // 使用 useMemo 优化 tabs 数据,避免每次渲染都创建新数组 + const vercelTabsData = useMemo(() => { + return categories.map((category) => ({ + id: category.id, + label: category.title + })) + }, [categories]) + + return ( +
+
+ + + {t('discover.title')} + + + + {categories.length > 0 && ( +
+ +
+ )} + +
+ {currentCategory?.hasSidebar && ( +
+ +
+ )} + {/* {!currentCategory && categories.length > 0 && ( +
Select a category...
+ )} */} + +
+ +
+
+
+
+ ) +} diff --git a/src/renderer/src/pages/agents/AgentsPage.tsx b/src/renderer/src/pages/discover/pages/agents/AgentsPage.tsx similarity index 91% rename from src/renderer/src/pages/agents/AgentsPage.tsx rename to src/renderer/src/pages/discover/pages/agents/AgentsPage.tsx index 45f4846430..267163e915 100644 --- a/src/renderer/src/pages/agents/AgentsPage.tsx +++ b/src/renderer/src/pages/discover/pages/agents/AgentsPage.tsx @@ -1,5 +1,4 @@ import { ImportOutlined, PlusOutlined } from '@ant-design/icons' -import { Navbar, NavbarCenter } from '@renderer/components/app/Navbar' import CustomTag from '@renderer/components/CustomTag' import ListItem from '@renderer/components/ListItem' import Scrollbar from '@renderer/components/Scrollbar' @@ -183,27 +182,29 @@ const AgentsPage: FC = () => { return ( - - - {t('agents.title')} - } - value={searchInput} - maxLength={50} - onChange={handleSearchInputChange} - onPressEnter={handleSearch} - onBlur={handleSearchInputBlur} - /> -
- - + {/* */} + {/* */} + {/* {t('agents.title')} */} +
+ } + value={searchInput} + maxLength={50} + onChange={handleSearchInputChange} + onPressEnter={handleSearch} + onBlur={handleSearchInputBlur} + /> +
+
+ {/* */} + {/* */}
@@ -314,14 +315,13 @@ const AgentsPage: FC = () => { const Container = styled.div` display: flex; - flex: 1; flex-direction: column; height: 100%; + overflow: hidden; ` const AgentsGroupList = styled(Scrollbar)` min-width: 160px; - height: calc(100vh - var(--navbar-height)); display: flex; flex-direction: column; gap: 8px; @@ -339,10 +339,10 @@ const AgentsGroupList = styled(Scrollbar)` const Main = styled.div` flex: 1; display: flex; + overflow: hidden; ` const AgentsListContainer = styled.div` - height: calc(100vh - var(--navbar-height)); flex: 1; display: flex; flex-direction: column; diff --git a/src/renderer/src/pages/agents/agentGroupTranslations.ts b/src/renderer/src/pages/discover/pages/agents/agentGroupTranslations.ts similarity index 100% rename from src/renderer/src/pages/agents/agentGroupTranslations.ts rename to src/renderer/src/pages/discover/pages/agents/agentGroupTranslations.ts diff --git a/src/renderer/src/pages/agents/components/AddAgentPopup.tsx b/src/renderer/src/pages/discover/pages/agents/components/AddAgentPopup.tsx similarity index 100% rename from src/renderer/src/pages/agents/components/AddAgentPopup.tsx rename to src/renderer/src/pages/discover/pages/agents/components/AddAgentPopup.tsx diff --git a/src/renderer/src/pages/agents/components/AgentCard.tsx b/src/renderer/src/pages/discover/pages/agents/components/AgentCard.tsx similarity index 100% rename from src/renderer/src/pages/agents/components/AgentCard.tsx rename to src/renderer/src/pages/discover/pages/agents/components/AgentCard.tsx diff --git a/src/renderer/src/pages/agents/components/AgentGroupIcon.tsx b/src/renderer/src/pages/discover/pages/agents/components/AgentGroupIcon.tsx similarity index 94% rename from src/renderer/src/pages/agents/components/AgentGroupIcon.tsx rename to src/renderer/src/pages/discover/pages/agents/components/AgentGroupIcon.tsx index 2e08ff3cd4..2b93672e75 100644 --- a/src/renderer/src/pages/agents/components/AgentGroupIcon.tsx +++ b/src/renderer/src/pages/discover/pages/agents/components/AgentGroupIcon.tsx @@ -1,4 +1,4 @@ -import { groupTranslations } from '@renderer/pages/agents/agentGroupTranslations' +import { groupTranslations } from '@renderer/pages/discover/pages/agents/agentGroupTranslations' import { DynamicIcon, IconName } from 'lucide-react/dynamic' import { FC } from 'react' import { useTranslation } from 'react-i18next' diff --git a/src/renderer/src/pages/agents/components/ImportAgentPopup.tsx b/src/renderer/src/pages/discover/pages/agents/components/ImportAgentPopup.tsx similarity index 100% rename from src/renderer/src/pages/agents/components/ImportAgentPopup.tsx rename to src/renderer/src/pages/discover/pages/agents/components/ImportAgentPopup.tsx diff --git a/src/renderer/src/pages/agents/components/ManageAgentsPopup.tsx b/src/renderer/src/pages/discover/pages/agents/components/ManageAgentsPopup.tsx similarity index 100% rename from src/renderer/src/pages/agents/components/ManageAgentsPopup.tsx rename to src/renderer/src/pages/discover/pages/agents/components/ManageAgentsPopup.tsx diff --git a/src/renderer/src/pages/agents/index.ts b/src/renderer/src/pages/discover/pages/agents/index.ts similarity index 100% rename from src/renderer/src/pages/agents/index.ts rename to src/renderer/src/pages/discover/pages/agents/index.ts diff --git a/src/renderer/src/pages/minapps/MinAppsPage.tsx b/src/renderer/src/pages/discover/pages/minapps/MinAppsPage.tsx similarity index 97% rename from src/renderer/src/pages/minapps/MinAppsPage.tsx rename to src/renderer/src/pages/discover/pages/minapps/MinAppsPage.tsx index db82bf43a3..b10c7281ab 100644 --- a/src/renderer/src/pages/minapps/MinAppsPage.tsx +++ b/src/renderer/src/pages/discover/pages/minapps/MinAppsPage.tsx @@ -39,7 +39,8 @@ const AppsPage: FC = () => { - {t('minapp.title')} + {/* {t('minapp.title')} */} + {/*
*/} { icon={} onClick={MinappSettingsPopup.show} /> + {/*
*/}
@@ -131,7 +133,6 @@ const MainContainer = styled.div` display: flex; flex: 1; flex-direction: row; - height: calc(100vh - var(--navbar-height)); ` const RightContainer = styled(Scrollbar)` @@ -140,7 +141,6 @@ const RightContainer = styled(Scrollbar)` flex-direction: column; height: 100%; align-items: center; - height: calc(100vh - var(--navbar-height)); ` const AppsContainerWrapper = styled(Scrollbar)` diff --git a/src/renderer/src/pages/minapps/MiniappSettings/MinappSettingsPopup.tsx b/src/renderer/src/pages/discover/pages/minapps/MiniappSettings/MinappSettingsPopup.tsx similarity index 100% rename from src/renderer/src/pages/minapps/MiniappSettings/MinappSettingsPopup.tsx rename to src/renderer/src/pages/discover/pages/minapps/MiniappSettings/MinappSettingsPopup.tsx diff --git a/src/renderer/src/pages/minapps/MiniappSettings/MiniAppIconsManager.tsx b/src/renderer/src/pages/discover/pages/minapps/MiniappSettings/MiniAppIconsManager.tsx similarity index 100% rename from src/renderer/src/pages/minapps/MiniappSettings/MiniAppIconsManager.tsx rename to src/renderer/src/pages/discover/pages/minapps/MiniappSettings/MiniAppIconsManager.tsx diff --git a/src/renderer/src/pages/minapps/MiniappSettings/MiniAppSettings.tsx b/src/renderer/src/pages/discover/pages/minapps/MiniappSettings/MiniAppSettings.tsx similarity index 100% rename from src/renderer/src/pages/minapps/MiniappSettings/MiniAppSettings.tsx rename to src/renderer/src/pages/discover/pages/minapps/MiniappSettings/MiniAppSettings.tsx diff --git a/src/renderer/src/pages/minapps/NewAppButton.tsx b/src/renderer/src/pages/discover/pages/minapps/NewAppButton.tsx similarity index 100% rename from src/renderer/src/pages/minapps/NewAppButton.tsx rename to src/renderer/src/pages/discover/pages/minapps/NewAppButton.tsx diff --git a/src/renderer/src/pages/discover/routers.ts b/src/renderer/src/pages/discover/routers.ts new file mode 100644 index 0000000000..8be3f1ae52 --- /dev/null +++ b/src/renderer/src/pages/discover/routers.ts @@ -0,0 +1,62 @@ +import i18n from '@renderer/i18n' +import { CherryStoreType } from '@renderer/types/cherryStore' +import { lazy } from 'react' + +export const discoverRouters = [ + { + id: CherryStoreType.ASSISTANT, + title: i18n.t('assistants.title'), + path: 'assistant', + component: lazy(() => import('./pages/agents/AgentsPage')) + }, + { + id: CherryStoreType.MINI_APP, + title: i18n.t('minapp.title'), + path: 'mini-app', + component: lazy(() => import('./pages/minapps/MinAppsPage')) + } + // { + // id: CherryStoreType.TRANSLATE, + // title: i18n.t('translate.title'), + // path: 'translate', + // component: lazy(() => import('../translate/TranslatePage')) + // }, + // { + // id: CherryStoreType.FILES, + // title: i18n.t('files.title'), + // path: 'files', + // component: lazy(() => import('../files/FilesPage')) + // }, + // { + // id: CherryStoreType.PAINTINGS, + // title: i18n.t('paintings.title'), + // path: 'paintings/*', + // isPrefix: true, + // component: lazy(() => import('../paintings/PaintingsRoutePage')) + // } + // { + // id: CherryStoreType.MCP_SERVER, + // title: i18n.t('common.mcp'), + // path: 'mcp-servers/*', + // isPrefix: true, + // component: lazy(() => import('../mcp-servers')) + // } +] + +// 静态注册表 - 避免每次渲染都重新生成 +export interface InternalCategory { + id: string + title: string + path: string + hasSidebar?: boolean + items: Array<{ id: string; name: string; count?: number }> +} + +// 预生成的分类注册表 +export const CATEGORY_REGISTRY: InternalCategory[] = discoverRouters.map((router) => ({ + id: router.id, + title: router.title, + path: router.path, + hasSidebar: false, // 目前都没有侧边栏 + items: [{ id: 'all', name: `All ${router.title}` }] // 预设 "All" 子分类 +})) diff --git a/src/renderer/src/pages/discover/utils/index.ts b/src/renderer/src/pages/discover/utils/index.ts new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/renderer/src/pages/discover/utils/util.ts b/src/renderer/src/pages/discover/utils/util.ts new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/renderer/src/pages/launchpad/LaunchpadPage.tsx b/src/renderer/src/pages/launchpad/LaunchpadPage.tsx index c189ae0864..a38a83b090 100644 --- a/src/renderer/src/pages/launchpad/LaunchpadPage.tsx +++ b/src/renderer/src/pages/launchpad/LaunchpadPage.tsx @@ -20,7 +20,7 @@ const LaunchpadPage: FC = () => { { icon: , text: t('title.apps'), - path: '/apps', + path: '/discover/mini-app', bgColor: 'linear-gradient(135deg, #8B5CF6, #A855F7)' // 小程序:紫色,代表多功能和灵活性 }, { @@ -38,7 +38,7 @@ const LaunchpadPage: FC = () => { { icon: , text: t('title.agents'), - path: '/agents', + path: '/discover/assistant', bgColor: 'linear-gradient(135deg, #6366F1, #4F46E5)' // AI助手:靛蓝渐变,代表智能和科技 }, { diff --git a/src/renderer/src/pages/settings/DisplaySettings/SidebarIconsManager.tsx b/src/renderer/src/pages/settings/DisplaySettings/SidebarIconsManager.tsx index c19fabc70f..a04765e9a5 100644 --- a/src/renderer/src/pages/settings/DisplaySettings/SidebarIconsManager.tsx +++ b/src/renderer/src/pages/settings/DisplaySettings/SidebarIconsManager.tsx @@ -11,7 +11,7 @@ import { getSidebarIconLabel } from '@renderer/i18n/label' import { useAppDispatch } from '@renderer/store' import { setSidebarIcons } from '@renderer/store/settings' import { message } from 'antd' -import { FileSearch, Folder, Languages, LayoutGrid, MessageSquareQuote, Palette, Sparkle } from 'lucide-react' +import { Compass, FileSearch, Folder, Languages, LayoutGrid, MessageSquareQuote, Palette, Sparkle } from 'lucide-react' import { FC, useCallback, useMemo } from 'react' import { useTranslation } from 'react-i18next' import styled from 'styled-components' @@ -116,7 +116,8 @@ const SidebarIconsManager: FC = ({ translate: , minapp: , knowledge: , - files: + files: , + discover: }), [] ) diff --git a/src/renderer/src/store/index.ts b/src/renderer/src/store/index.ts index 74fdc28fd5..f818559c38 100644 --- a/src/renderer/src/store/index.ts +++ b/src/renderer/src/store/index.ts @@ -60,7 +60,7 @@ const persistedReducer = persistReducer( { key: 'cherry-studio', storage, - version: 125, + version: 126, blacklist: ['runtime', 'messages', 'messageBlocks', 'tabs'], migrate }, diff --git a/src/renderer/src/store/migrate.ts b/src/renderer/src/store/migrate.ts index 12d0069a36..128cc7c251 100644 --- a/src/renderer/src/store/migrate.ts +++ b/src/renderer/src/store/migrate.ts @@ -1935,6 +1935,27 @@ const migrateConfig = { logger.error('migrate 125 error', error as Error) return state } + }, + '126': (state: RootState) => { + try { + const visibleIcons = state.settings.sidebarIcons.visible + if (visibleIcons.includes('discover')) { + return state + } + const filteredIcons = visibleIcons.filter((icon) => icon !== 'agents' && icon !== 'minapp') + return { + ...state, + settings: { + ...state.settings, + sidebarIcons: { + ...state.settings.sidebarIcons, + visible: [...filteredIcons, 'discover'] + } + } + } + } catch (error) { + return state + } } } diff --git a/src/renderer/src/store/settings.ts b/src/renderer/src/store/settings.ts index 4e366e7edc..296537cae5 100644 --- a/src/renderer/src/store/settings.ts +++ b/src/renderer/src/store/settings.ts @@ -20,16 +20,25 @@ import { RemoteSyncState } from './backup' export type SendMessageShortcut = 'Enter' | 'Shift+Enter' | 'Ctrl+Enter' | 'Command+Enter' | 'Alt+Enter' -export type SidebarIcon = 'assistants' | 'agents' | 'paintings' | 'translate' | 'minapp' | 'knowledge' | 'files' +export type SidebarIcon = + | 'assistants' + | 'agents' + | 'paintings' + | 'translate' + | 'minapp' + | 'knowledge' + | 'files' + | 'discover' export const DEFAULT_SIDEBAR_ICONS: SidebarIcon[] = [ 'assistants', - 'agents', + // 'agents', 'paintings', 'translate', - 'minapp', + // 'minapp', 'knowledge', - 'files' + 'files', + 'discover' ] export interface NutstoreSyncRuntime extends RemoteSyncState {} diff --git a/src/renderer/src/store/tabs.ts b/src/renderer/src/store/tabs.ts index 4f90eb4e81..01dc7b1fb3 100644 --- a/src/renderer/src/store/tabs.ts +++ b/src/renderer/src/store/tabs.ts @@ -5,7 +5,7 @@ export interface Tab { path: string } -interface TabsState { +export interface TabsState { tabs: Tab[] activeTabId: string } diff --git a/src/renderer/src/types/cherryStore.ts b/src/renderer/src/types/cherryStore.ts new file mode 100644 index 0000000000..3dd2910186 --- /dev/null +++ b/src/renderer/src/types/cherryStore.ts @@ -0,0 +1,24 @@ +export enum CherryStoreType { + ASSISTANT = 'Assistant', + MINI_APP = 'Mini-App', + KNOWLEDGE = 'Knowledge', + MCP_SERVER = 'MCP-Server', + MODEL_PROVIDER = 'Model-Provider', + AGENT = 'Agent', + TRANSLATE = 'Translate', + PAINTINGS = 'Paintings', + FILES = 'Files' +} + +export interface SubCategoryItem { + id: string + name: string + count?: number // count 是可选的,因为并非所有二级分类都有 + isActive?: boolean +} + +export interface Category { + id: CherryStoreType + title: string + items: SubCategoryItem[] +} diff --git a/src/renderer/src/ui/badge.tsx b/src/renderer/src/ui/badge.tsx new file mode 100644 index 0000000000..153a11bde6 --- /dev/null +++ b/src/renderer/src/ui/badge.tsx @@ -0,0 +1,46 @@ +import * as React from "react" +import { Slot } from "@radix-ui/react-slot" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@renderer/utils/index" + +const badgeVariants = cva( + "inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden", + { + variants: { + variant: { + default: + "border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90", + secondary: + "border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90", + destructive: + "border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", + outline: + "text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +function Badge({ + className, + variant, + asChild = false, + ...props +}: React.ComponentProps<"span"> & + VariantProps & { asChild?: boolean }) { + const Comp = asChild ? Slot : "span" + + return ( + + ) +} + +export { Badge, badgeVariants } diff --git a/src/renderer/src/ui/button.tsx b/src/renderer/src/ui/button.tsx new file mode 100644 index 0000000000..e48aff75d8 --- /dev/null +++ b/src/renderer/src/ui/button.tsx @@ -0,0 +1,59 @@ +import * as React from "react" +import { Slot } from "@radix-ui/react-slot" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@renderer/utils/index" + +const buttonVariants = cva( + "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", + { + variants: { + variant: { + default: + "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90", + destructive: + "bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", + outline: + "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50", + secondary: + "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80", + ghost: + "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50", + link: "text-primary underline-offset-4 hover:underline", + }, + size: { + default: "h-9 px-4 py-2 has-[>svg]:px-3", + sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5", + lg: "h-10 rounded-md px-6 has-[>svg]:px-4", + icon: "size-9", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + } +) + +function Button({ + className, + variant, + size, + asChild = false, + ...props +}: React.ComponentProps<"button"> & + VariantProps & { + asChild?: boolean + }) { + const Comp = asChild ? Slot : "button" + + return ( + + ) +} + +export { Button, buttonVariants } diff --git a/src/renderer/src/ui/input.tsx b/src/renderer/src/ui/input.tsx new file mode 100644 index 0000000000..5e867e49aa --- /dev/null +++ b/src/renderer/src/ui/input.tsx @@ -0,0 +1,21 @@ +import * as React from "react" + +import { cn } from "@renderer/utils/index" + +function Input({ className, type, ...props }: React.ComponentProps<"input">) { + return ( + + ) +} + +export { Input } diff --git a/src/renderer/src/ui/separator.tsx b/src/renderer/src/ui/separator.tsx new file mode 100644 index 0000000000..086bde501e --- /dev/null +++ b/src/renderer/src/ui/separator.tsx @@ -0,0 +1,28 @@ +"use client" + +import * as React from "react" +import * as SeparatorPrimitive from "@radix-ui/react-separator" + +import { cn } from "@renderer/utils/index" + +function Separator({ + className, + orientation = "horizontal", + decorative = true, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +export { Separator } diff --git a/src/renderer/src/ui/sheet.tsx b/src/renderer/src/ui/sheet.tsx new file mode 100644 index 0000000000..0bff33aa8d --- /dev/null +++ b/src/renderer/src/ui/sheet.tsx @@ -0,0 +1,137 @@ +import * as React from "react" +import * as SheetPrimitive from "@radix-ui/react-dialog" +import { XIcon } from "lucide-react" + +import { cn } from "@renderer/utils/index" + +function Sheet({ ...props }: React.ComponentProps) { + return +} + +function SheetTrigger({ + ...props +}: React.ComponentProps) { + return +} + +function SheetClose({ + ...props +}: React.ComponentProps) { + return +} + +function SheetPortal({ + ...props +}: React.ComponentProps) { + return +} + +function SheetOverlay({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function SheetContent({ + className, + children, + side = "right", + ...props +}: React.ComponentProps & { + side?: "top" | "right" | "bottom" | "left" +}) { + return ( + + + + {children} + + + Close + + + + ) +} + +function SheetHeader({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function SheetFooter({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function SheetTitle({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function SheetDescription({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +export { + Sheet, + SheetTrigger, + SheetClose, + SheetContent, + SheetHeader, + SheetFooter, + SheetTitle, + SheetDescription, +} diff --git a/src/renderer/src/ui/sidebar.tsx b/src/renderer/src/ui/sidebar.tsx new file mode 100644 index 0000000000..751ed49209 --- /dev/null +++ b/src/renderer/src/ui/sidebar.tsx @@ -0,0 +1,724 @@ +import * as React from "react" +import { Slot } from "@radix-ui/react-slot" +import { cva, VariantProps } from "class-variance-authority" +import { PanelLeftIcon } from "lucide-react" + +import { useIsMobile } from "@renderer/hooks/use-mobile" +import { cn } from "@renderer/utils/index" +import { Button } from "@renderer/ui/button" +import { Input } from "@renderer/ui/input" +import { Separator } from "@renderer/ui/separator" +import { + Sheet, + SheetContent, + SheetDescription, + SheetHeader, + SheetTitle, +} from "@renderer/ui/sheet" +import { Skeleton } from "@renderer/ui/skeleton" +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@renderer/ui/tooltip" + +const SIDEBAR_COOKIE_NAME = "sidebar_state" +const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7 +const SIDEBAR_WIDTH = "16rem" +const SIDEBAR_WIDTH_MOBILE = "18rem" +const SIDEBAR_WIDTH_ICON = "3rem" +const SIDEBAR_KEYBOARD_SHORTCUT = "b" + +type SidebarContextProps = { + state: "expanded" | "collapsed" + open: boolean + setOpen: (open: boolean) => void + openMobile: boolean + setOpenMobile: (open: boolean) => void + isMobile: boolean + toggleSidebar: () => void +} + +const SidebarContext = React.createContext(null) + +function useSidebar() { + const context = React.useContext(SidebarContext) + if (!context) { + throw new Error("useSidebar must be used within a SidebarProvider.") + } + + return context +} + +function SidebarProvider({ + defaultOpen = true, + open: openProp, + onOpenChange: setOpenProp, + className, + style, + children, + ...props +}: React.ComponentProps<"div"> & { + defaultOpen?: boolean + open?: boolean + onOpenChange?: (open: boolean) => void +}) { + const isMobile = useIsMobile() + const [openMobile, setOpenMobile] = React.useState(false) + + // This is the internal state of the sidebar. + // We use openProp and setOpenProp for control from outside the component. + const [_open, _setOpen] = React.useState(defaultOpen) + const open = openProp ?? _open + const setOpen = React.useCallback( + (value: boolean | ((value: boolean) => boolean)) => { + const openState = typeof value === "function" ? value(open) : value + if (setOpenProp) { + setOpenProp(openState) + } else { + _setOpen(openState) + } + + // This sets the cookie to keep the sidebar state. + document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}` + }, + [setOpenProp, open] + ) + + // Helper to toggle the sidebar. + const toggleSidebar = React.useCallback(() => { + return isMobile ? setOpenMobile((open) => !open) : setOpen((open) => !open) + }, [isMobile, setOpen, setOpenMobile]) + + // Adds a keyboard shortcut to toggle the sidebar. + React.useEffect(() => { + const handleKeyDown = (event: KeyboardEvent) => { + if ( + event.key === SIDEBAR_KEYBOARD_SHORTCUT && + (event.metaKey || event.ctrlKey) + ) { + event.preventDefault() + toggleSidebar() + } + } + + window.addEventListener("keydown", handleKeyDown) + return () => window.removeEventListener("keydown", handleKeyDown) + }, [toggleSidebar]) + + // We add a state so that we can do data-state="expanded" or "collapsed". + // This makes it easier to style the sidebar with Tailwind classes. + const state = open ? "expanded" : "collapsed" + + const contextValue = React.useMemo( + () => ({ + state, + open, + setOpen, + isMobile, + openMobile, + setOpenMobile, + toggleSidebar, + }), + [state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar] + ) + + return ( + + +
+ {children} +
+
+
+ ) +} + +function Sidebar({ + side = "left", + variant = "sidebar", + collapsible = "offcanvas", + className, + children, + ...props +}: React.ComponentProps<"div"> & { + side?: "left" | "right" + variant?: "sidebar" | "floating" | "inset" + collapsible?: "offcanvas" | "icon" | "none" +}) { + const { isMobile, state, openMobile, setOpenMobile } = useSidebar() + + if (collapsible === "none") { + return ( +
+ {children} +
+ ) + } + + if (isMobile) { + return ( + + + + Sidebar + Displays the mobile sidebar. + +
{children}
+
+
+ ) + } + + return ( +
+ {/* This is what handles the sidebar gap on desktop */} +
+ +
+ ) +} + +function SidebarTrigger({ + className, + onClick, + ...props +}: React.ComponentProps) { + const { toggleSidebar } = useSidebar() + + return ( + + ) +} + +function SidebarRail({ className, ...props }: React.ComponentProps<"button">) { + const { toggleSidebar } = useSidebar() + + return ( +