diff --git a/.prettierrc b/.prettierrc index 85e2eb0ca..7a0676110 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 000000000..06461d66d --- /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 f7cbd950f..0da977400 100644 --- a/electron.vite.config.ts +++ b/electron.vite.config.ts @@ -58,6 +58,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 abaadac84..341c152e0 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 f225b4c8c..66515bc1f 100644 --- a/package.json +++ b/package.json @@ -128,9 +128,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.9.1", - "@swc/plugin-styled-components": "^7.1.5", + "@swc/plugin-styled-components": "^9.0.2", + "@tailwindcss/vite": "^4.1.5", "@tanstack/react-query": "^5.27.0", "@tanstack/react-virtual": "^3.13.12", "@testing-library/dom": "^10.4.0", @@ -154,7 +162,7 @@ "@uiw/codemirror-extensions-langs": "^4.23.14", "@uiw/codemirror-themes-all": "^4.23.14", "@uiw/react-codemirror": "^4.23.14", - "@vitejs/plugin-react-swc": "^3.9.0", + "@vitejs/plugin-react-swc": "^3.11.0", "@vitest/browser": "^3.2.4", "@vitest/coverage-v8": "^3.2.4", "@vitest/ui": "^3.2.4", @@ -168,7 +176,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", @@ -208,12 +218,13 @@ "lint-staged": "^15.5.0", "lodash": "^4.17.21", "lru-cache": "^11.1.0", - "lucide-react": "^0.525.0", + "lucide-react": "^0.536.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", @@ -222,6 +233,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", @@ -252,13 +264,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/scripts/update-languages.ts b/scripts/update-languages.ts index 91416a973..58640637b 100644 --- a/scripts/update-languages.ts +++ b/scripts/update-languages.ts @@ -1,6 +1,6 @@ import { exec } from 'child_process' import * as fs from 'fs/promises' -import linguistLanguages from 'linguist-languages' +import * as linguistLanguages from 'linguist-languages' import * as path from 'path' import { promisify } from 'util' diff --git a/src/main/knowledge/preprocess/MineruPreprocessProvider.ts b/src/main/knowledge/preprocess/MineruPreprocessProvider.ts index afc19ae34..a86b3cc61 100644 --- a/src/main/knowledge/preprocess/MineruPreprocessProvider.ts +++ b/src/main/knowledge/preprocess/MineruPreprocessProvider.ts @@ -273,7 +273,7 @@ export default class MineruPreprocessProvider extends BasePreprocessProvider { const response = await fetch(uploadUrl, { method: 'PUT', - body: fileBuffer, + body: new Uint8Array(fileBuffer), headers: { 'Content-Type': 'application/pdf' } diff --git a/src/renderer/src/Router.tsx b/src/renderer/src/Router.tsx index 624c6ccc4..132f42cf3 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/aiCore/middleware/feat/ImageGenerationMiddleware.ts b/src/renderer/src/aiCore/middleware/feat/ImageGenerationMiddleware.ts index ceb8d791d..500381607 100644 --- a/src/renderer/src/aiCore/middleware/feat/ImageGenerationMiddleware.ts +++ b/src/renderer/src/aiCore/middleware/feat/ImageGenerationMiddleware.ts @@ -50,7 +50,9 @@ export const ImageGenerationMiddleware: CompletionsMiddleware = if (!block.file) return null const binaryData: Uint8Array = await FileManager.readBinaryImage(block.file) const mimeType = `${block.file.type}/${block.file.ext.slice(1)}` - return await toFile(new Blob([binaryData]), block.file.origin_name || 'image.png', { type: mimeType }) + return await toFile(new Blob([binaryData.slice()]), block.file.origin_name || 'image.png', { + type: mimeType + }) }) ) imageFiles = imageFiles.concat(userImages.filter(Boolean) as Blob[]) diff --git a/src/renderer/src/assets/styles/index.scss b/src/renderer/src/assets/styles/index.scss index 0a6696bd9..d1c32c17e 100644 --- a/src/renderer/src/assets/styles/index.scss +++ b/src/renderer/src/assets/styles/index.scss @@ -49,6 +49,7 @@ body { font-family: var(--font-family); text-rendering: optimizeLegibility; transition: background-color 0.3s linear; + background-color: unset; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; diff --git a/src/renderer/src/assets/styles/tailwind.css b/src/renderer/src/assets/styles/tailwind.css new file mode 100644 index 000000000..1cb99ed02 --- /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/CodeBlockView/MermaidPreview.tsx b/src/renderer/src/components/CodeBlockView/MermaidPreview.tsx index b57c4a68a..b2ed8b0d6 100644 --- a/src/renderer/src/components/CodeBlockView/MermaidPreview.tsx +++ b/src/renderer/src/components/CodeBlockView/MermaidPreview.tsx @@ -142,7 +142,7 @@ const MermaidPreview: React.FC = ({ children, setTools }) => }> {(mermaidError || error) && {mermaidError || error}} - + ) diff --git a/src/renderer/src/components/ImageViewer.tsx b/src/renderer/src/components/ImageViewer.tsx index ddb28a4d5..a18f4d00c 100644 --- a/src/renderer/src/components/ImageViewer.tsx +++ b/src/renderer/src/components/ImageViewer.tsx @@ -36,7 +36,7 @@ const ImageViewer: React.FC = ({ src, style, ...props }) => { if (!match) throw new Error('无效的 base64 图片格式') const mimeType = match[1] const byteArray = Base64.toUint8Array(match[2]) - const blob = new Blob([byteArray], { type: mimeType }) + const blob = new Blob([byteArray.slice()], { type: mimeType }) await navigator.clipboard.write([new ClipboardItem({ [mimeType]: blob })]) } else if (src.startsWith('file://')) { // 处理本地文件路径 diff --git a/src/renderer/src/components/Popups/AddAssistantPopup.tsx b/src/renderer/src/components/Popups/AddAssistantPopup.tsx index b3ea93662..efef3354b 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 81ebad43e..d4383fec7 100644 --- a/src/renderer/src/components/Tab/TabContainer.tsx +++ b/src/renderer/src/components/Tab/TabContainer.tsx @@ -12,16 +12,15 @@ import { ThemeMode } from '@renderer/types' import { classNames } from '@renderer/utils' import { Tooltip } from 'antd' import { + Compass, FileSearch, Folder, Home, Languages, - LayoutGrid, Monitor, Moon, Palette, Settings, - Sparkle, SquareTerminal, Sun, X @@ -41,14 +40,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': @@ -57,6 +56,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/__tests__/__snapshots__/InputEmbeddingDimension.test.tsx.snap b/src/renderer/src/components/__tests__/__snapshots__/InputEmbeddingDimension.test.tsx.snap index f05613003..694f03157 100644 --- a/src/renderer/src/components/__tests__/__snapshots__/InputEmbeddingDimension.test.tsx.snap +++ b/src/renderer/src/components/__tests__/__snapshots__/InputEmbeddingDimension.test.tsx.snap @@ -8,7 +8,7 @@ exports[`InputEmbeddingDimension > basic rendering > should match snapshot with @@ -43,7 +43,7 @@ exports[`InputEmbeddingDimension > basic rendering > should match snapshot with diff --git a/src/renderer/src/components/__tests__/__snapshots__/Spinner.test.tsx.snap b/src/renderer/src/components/__tests__/__snapshots__/Spinner.test.tsx.snap index aa374d932..3dd4e2238 100644 --- a/src/renderer/src/components/__tests__/__snapshots__/Spinner.test.tsx.snap +++ b/src/renderer/src/components/__tests__/__snapshots__/Spinner.test.tsx.snap @@ -26,6 +26,7 @@ exports[`Spinner > should match snapshot 1`] = ` stroke-linecap="round" stroke-linejoin="round" stroke-width="2" + style="color: unset;" viewBox="0 0 24 24" width="16" xmlns="http://www.w3.org/2000/svg" diff --git a/src/renderer/src/components/app/Sidebar.tsx b/src/renderer/src/components/app/Sidebar.tsx index eb67cf16f..077e1846e 100644 --- a/src/renderer/src/components/app/Sidebar.tsx +++ b/src/renderer/src/components/app/Sidebar.tsx @@ -16,16 +16,15 @@ import { isEmoji } from '@renderer/utils' import { Avatar, Tooltip } from 'antd' import { CircleHelp, + Compass, FileSearch, Folder, Languages, - LayoutGrid, MessageSquare, Monitor, Moon, Palette, Settings, - Sparkle, Sun } from 'lucide-react' import { FC } from 'react' @@ -39,7 +38,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() @@ -53,8 +52,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) @@ -148,22 +147,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 71755a0dc..21dc9e0b6 100644 --- a/src/renderer/src/context/ThemeProvider.tsx +++ b/src/renderer/src/context/ThemeProvider.tsx @@ -23,6 +23,12 @@ interface ThemeProviderProps extends PropsWithChildren { defaultTheme?: ThemeMode } +const tailwindThemeChange = (theme: ThemeMode) => { + const root = window.document.documentElement + root.classList.remove('light', 'dark') + root.classList.add(theme) +} + export const ThemeProvider: React.FC = ({ children }) => { // 用户设置的主题 const { theme: settedTheme, setTheme: setSettedTheme } = useSettings() @@ -64,6 +70,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 bf6a3cb6a..835f4830b 100644 --- a/src/renderer/src/entryPoint.tsx +++ b/src/renderer/src/entryPoint.tsx @@ -1,4 +1,5 @@ import './assets/styles/index.scss' +import './assets/styles/tailwind.css' import '@ant-design/v5-patch-for-react-19' import { createRoot } from 'react-dom/client' diff --git a/src/renderer/src/hooks/use-mobile.ts b/src/renderer/src/hooks/use-mobile.ts new file mode 100644 index 000000000..4331d5c56 --- /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 4915ef5eb..c01ee7d5f 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: 'title.discover' } as const export const getTitleLabel = (key: string): string => { @@ -127,7 +128,8 @@ const sidebarIconKeyMap = { translate: 'translate.title', minapp: 'minapp.title', knowledge: 'knowledge.title', - files: 'files.title' + files: 'files.title', + discover: 'discover.title' } as const export const getSidebarIconLabel = (key: string): string => { diff --git a/src/renderer/src/i18n/locales/en-us.json b/src/renderer/src/i18n/locales/en-us.json index ae2cc8a76..489b732b4 100644 --- a/src/renderer/src/i18n/locales/en-us.json +++ b/src/renderer/src/i18n/locales/en-us.json @@ -740,6 +740,13 @@ "warning": "Warning", "you": "You" }, + "discover": { + "install": "Installation", + "title": "Discover", + "uninstall": "uninstall", + "update": "update", + "update_all": "Full Update" + }, "docs": { "title": "Docs" }, @@ -3443,6 +3450,7 @@ "title": { "agents": "Agents", "apps": "Apps", + "discover": "Discover", "files": "Files", "home": "Home", "knowledge": "Knowledge Base", diff --git a/src/renderer/src/i18n/locales/ja-jp.json b/src/renderer/src/i18n/locales/ja-jp.json index 8e3d2a9c1..ef1efc156 100644 --- a/src/renderer/src/i18n/locales/ja-jp.json +++ b/src/renderer/src/i18n/locales/ja-jp.json @@ -740,6 +740,13 @@ "warning": "警告", "you": "あなた" }, + "discover": { + "install": "インストール", + "title": "発見", + "uninstall": "アンインストール", + "update": "更新", + "update_all": "すべて更新" + }, "docs": { "title": "ドキュメント" }, @@ -3443,6 +3450,7 @@ "title": { "agents": "エージェント", "apps": "アプリ", + "discover": "発見", "files": "ファイル", "home": "ホーム", "knowledge": "ナレッジベース", diff --git a/src/renderer/src/i18n/locales/ru-ru.json b/src/renderer/src/i18n/locales/ru-ru.json index c959dda58..88519bf69 100644 --- a/src/renderer/src/i18n/locales/ru-ru.json +++ b/src/renderer/src/i18n/locales/ru-ru.json @@ -740,6 +740,13 @@ "warning": "Предупреждение", "you": "Вы" }, + "discover": { + "install": "Установка", + "title": "Откройте для себя", + "uninstall": "десинь", + "update": "обновление", + "update_all": "Все обновления" + }, "docs": { "title": "Документация" }, @@ -3443,6 +3450,7 @@ "title": { "agents": "Агенты", "apps": "Приложения", + "discover": "Откройте для себя", "files": "Файлы", "home": "Главная", "knowledge": "База знаний", diff --git a/src/renderer/src/i18n/locales/zh-cn.json b/src/renderer/src/i18n/locales/zh-cn.json index 78bd4143c..0fc3ad78c 100644 --- a/src/renderer/src/i18n/locales/zh-cn.json +++ b/src/renderer/src/i18n/locales/zh-cn.json @@ -740,6 +740,13 @@ "warning": "警告", "you": "用户" }, + "discover": { + "install": "安装", + "title": "发现", + "uninstall": "卸载", + "update": "更新", + "update_all": "全部更新" + }, "docs": { "title": "帮助文档" }, @@ -3443,6 +3450,7 @@ "title": { "agents": "智能体", "apps": "小程序", + "discover": "发现", "files": "文件", "home": "首页", "knowledge": "知识库", diff --git a/src/renderer/src/i18n/locales/zh-tw.json b/src/renderer/src/i18n/locales/zh-tw.json index 84869ab5c..a299fade5 100644 --- a/src/renderer/src/i18n/locales/zh-tw.json +++ b/src/renderer/src/i18n/locales/zh-tw.json @@ -740,6 +740,13 @@ "warning": "警告", "you": "您" }, + "discover": { + "install": "安裝", + "title": "發現", + "uninstall": "卸載", + "update": "更新", + "update_all": "全部更新" + }, "docs": { "title": "說明文件" }, @@ -3443,6 +3450,7 @@ "title": { "agents": "智能體", "apps": "小程序", + "discover": "發現", "files": "文件", "home": "主頁", "knowledge": "知識庫", diff --git a/src/renderer/src/i18n/translate/el-gr.json b/src/renderer/src/i18n/translate/el-gr.json index 7ba4a69d9..5c702db58 100644 --- a/src/renderer/src/i18n/translate/el-gr.json +++ b/src/renderer/src/i18n/translate/el-gr.json @@ -740,6 +740,13 @@ "warning": "Προσοχή", "you": "Εσείς" }, + "discover": { + "install": "Εγκατάσταση", + "title": "ανακάλυψη", + "uninstall": "απεγκατάσταση", + "update": "ενημέρωση", + "update_all": "Όλα τα πρόσφατα" + }, "docs": { "title": "Βοήθεια" }, @@ -3439,6 +3446,7 @@ "title": { "agents": "Πράκτορες", "apps": "Εφαρμογές", + "discover": "Ανακάλυψη", "files": "Αρχεία", "home": "Αρχική Σελίδα", "knowledge": "Βάση Γνώσης", diff --git a/src/renderer/src/i18n/translate/es-es.json b/src/renderer/src/i18n/translate/es-es.json index 83ec9a0ac..fa07f9ce6 100644 --- a/src/renderer/src/i18n/translate/es-es.json +++ b/src/renderer/src/i18n/translate/es-es.json @@ -740,6 +740,13 @@ "warning": "Advertencia", "you": "Usuario" }, + "discover": { + "install": "instalación", + "title": "Descubrir", + "uninstall": "desinstalar", + "update": "actualización", + "update_all": "Todo actualizado." + }, "docs": { "title": "Documentación de Ayuda" }, @@ -3439,6 +3446,7 @@ "title": { "agents": "Agentes", "apps": "Aplicaciones", + "discover": "Descubrir", "files": "Archivos", "home": "Inicio", "knowledge": "Base de conocimiento", diff --git a/src/renderer/src/i18n/translate/fr-fr.json b/src/renderer/src/i18n/translate/fr-fr.json index 6f84b6add..e2f30569a 100644 --- a/src/renderer/src/i18n/translate/fr-fr.json +++ b/src/renderer/src/i18n/translate/fr-fr.json @@ -740,6 +740,13 @@ "warning": "Avertissement", "you": "Vous" }, + "discover": { + "install": "installer", + "title": "Découvrir", + "uninstall": "désinstaller", + "update": "Mise à jour", + "update_all": "Mises à jour complètes" + }, "docs": { "title": "Documentation d'aide" }, @@ -3439,6 +3446,7 @@ "title": { "agents": "Agent intelligent", "apps": "Mini-programmes", + "discover": "Découvrir", "files": "Fichiers", "home": "Page d'accueil", "knowledge": "Base de connaissances", diff --git a/src/renderer/src/i18n/translate/pt-pt.json b/src/renderer/src/i18n/translate/pt-pt.json index 1dc8ed5cd..7ebec7665 100644 --- a/src/renderer/src/i18n/translate/pt-pt.json +++ b/src/renderer/src/i18n/translate/pt-pt.json @@ -740,6 +740,13 @@ "warning": "Aviso", "you": "Você" }, + "discover": { + "install": "Instalação", + "title": "Descoberta", + "uninstall": "desinstalar", + "update": "atualização", + "update_all": "Todas as atualizações" + }, "docs": { "title": "Documentação de Ajuda" }, @@ -3439,6 +3446,7 @@ "title": { "agents": "Agentes", "apps": "Miniaplicativos", + "discover": "Descoberta", "files": "Arquivos", "home": "Página Inicial", "knowledge": "Base de Conhecimento", 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 000000000..d4391edb5 --- /dev/null +++ b/src/renderer/src/pages/discover/components/DiscoverMain.tsx @@ -0,0 +1,19 @@ +import React, { Suspense } from 'react' +import { Navigate, Route, Routes } from 'react-router-dom' + +import { ROUTERS } from '../routers' + +const DiscoverContent: React.FC = () => { + return ( + + + } /> + {ROUTERS.map((route) => ( + } /> + ))} + + + ) +} + +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 000000000..95df9f96b --- /dev/null +++ b/src/renderer/src/pages/discover/components/DiscoverSidebar.tsx @@ -0,0 +1,59 @@ +// 还没测,目前助手和小程序用不到这个 + +import { Badge } from '@renderer/ui/badge' +import { + Sidebar, + SidebarContent, + SidebarMenu, + SidebarMenuButton, + SidebarMenuSubItem, + SidebarProvider +} from '@renderer/ui/sidebar' + +import { InternalCategory } from '../type' + +interface DiscoverSidebarProps { + activeCategory: InternalCategory | undefined +} + +export default function DiscoverSidebar({ activeCategory }: 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 000000000..3132dfe79 --- /dev/null +++ b/src/renderer/src/pages/discover/hooks/useDiscoverCategories.ts @@ -0,0 +1,53 @@ +import { CherryStoreType } from '@renderer/types/cherryStore' +import { useCallback, useMemo } from 'react' +import { useLocation, useNavigate, useSearchParams } from 'react-router-dom' + +import { ROUTERS, ROUTERS_MAP } from '../routers' + +export function useDiscoverCategories() { + const location = useLocation() + const navigate = useNavigate() + const [searchParams, setSearchParams] = useSearchParams() + + const activeTabId = useMemo(() => { + // e.g., location.pathname = /discover/assistant, segments = ['discover', 'assistant'] + const pathSegments = location.pathname.split('/').filter(Boolean) + const currentTabId = pathSegments[1] as CherryStoreType + + return ROUTERS_MAP.has(currentTabId) ? currentTabId : ROUTERS[0].id + }, [location.pathname]) + + const activeCategoryId = useMemo(() => { + return searchParams.get('categoryId') || 'all' + }, [searchParams]) + + const handleSelectTab = useCallback( + (newTabId: string) => { + if (activeTabId !== newTabId) { + navigate(`/discover/${newTabId}`) + } + }, + [activeTabId, navigate] + ) + + const handleSelectCategory = useCallback( + (newCategoryId: string) => { + if (activeCategoryId !== newCategoryId) { + setSearchParams({ categoryId: newCategoryId }) + } + }, + [activeCategoryId, setSearchParams] + ) + + const currentCategory = useMemo(() => { + return ROUTERS_MAP.get(activeTabId) + }, [activeTabId]) + + return { + activeTabId, + activeCategoryId, + currentCategory, + handleSelectTab, + handleSelectCategory + } +} diff --git a/src/renderer/src/pages/discover/index.tsx b/src/renderer/src/pages/discover/index.tsx new file mode 100644 index 000000000..bffc9318c --- /dev/null +++ b/src/renderer/src/pages/discover/index.tsx @@ -0,0 +1,46 @@ +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' +import { ROUTERS } from './routers' + +export default function DiscoverPage() { + const { t } = useTranslation() + const { activeTabId, currentCategory, handleSelectTab } = useDiscoverCategories() + + const tabs = useMemo(() => ROUTERS.map((router) => ({ id: router.id, label: router.title })), []) + + return ( +
+
+ + + {t('discover.title')} + + + + {ROUTERS.length > 0 && ( +
+ +
+ )} + +
+ {currentCategory?.hasSidebar && ( +
+ +
+ )} + +
+ +
+
+
+
+ ) +} 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 727e06566..0e5a8db40 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 { HStack } from '@renderer/components/Layout' import ListItem from '@renderer/components/ListItem' @@ -179,27 +178,28 @@ 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} + /> +
*/} + {/*
*/} + {/*
*/}
@@ -310,14 +310,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; @@ -335,10 +334,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 2e08ff3cd..2b93672e7 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 72% rename from src/renderer/src/pages/minapps/MinAppsPage.tsx rename to src/renderer/src/pages/discover/pages/minapps/MinAppsPage.tsx index db82bf43a..c52d75247 100644 --- a/src/renderer/src/pages/minapps/MinAppsPage.tsx +++ b/src/renderer/src/pages/discover/pages/minapps/MinAppsPage.tsx @@ -1,8 +1,6 @@ -import { Navbar, NavbarMain } from '@renderer/components/app/Navbar' import App from '@renderer/components/MinApp/MinApp' import Scrollbar from '@renderer/components/Scrollbar' import { useMinapps } from '@renderer/hooks/useMinapps' -import { useNavbarPosition } from '@renderer/hooks/useSettings' import { Button, Input } from 'antd' import { Search, SettingsIcon } from 'lucide-react' import React, { FC, useState } from 'react' @@ -16,7 +14,7 @@ const AppsPage: FC = () => { const { t } = useTranslation() const [search, setSearch] = useState('') const { minapps } = useMinapps() - const { isTopNavbar } = useNavbarPosition() + // const { isTopNavbar } = useNavbarPosition() const filteredApps = search ? minapps.filter( @@ -37,38 +35,40 @@ const AppsPage: FC = () => { return ( - - - {t('minapp.title')} - } - value={search} - onChange={(e) => setSearch(e.target.value)} - /> -
+ {/* */} + {/*
*/} - {isTopNavbar && ( + {/* {isTopNavbar && ( { onClick={() => MinappSettingsPopup.show()} /> - )} + )} */} {filteredApps.map((app) => ( @@ -114,24 +114,23 @@ const ContentContainer = styled.div` flex: 1; flex-direction: row; justify-content: center; - height: 100%; + overflow: hidden; ` -const HeaderContainer = styled.div` - display: flex; - flex-direction: row; - justify-content: center; - align-items: center; - height: 60px; - width: 100%; - gap: 10px; -` +// const HeaderContainer = styled.div` +// display: flex; +// flex-direction: row; +// justify-content: center; +// align-items: center; +// height: 60px; +// width: 100%; +// gap: 10px; +// ` const MainContainer = styled.div` display: flex; flex: 1; flex-direction: row; - height: calc(100vh - var(--navbar-height)); ` const RightContainer = styled(Scrollbar)` @@ -140,7 +139,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 000000000..c5de9324b --- /dev/null +++ b/src/renderer/src/pages/discover/routers.ts @@ -0,0 +1,50 @@ +import i18n from '@renderer/i18n' +import { CherryStoreType } from '@renderer/types/cherryStore' +import { lazy } from 'react' + +export const ROUTERS = [ + { + id: CherryStoreType.ASSISTANT, + title: i18n.t('assistants.title'), + path: CherryStoreType.ASSISTANT, + component: lazy(() => import('./pages/agents/AgentsPage')), + hasSidebar: false, // 目前都没有侧边栏 + items: [{ id: 'all', name: `All ${i18n.t('assistants.title')}` }] // 预设 "All" 子分类 + }, + { + id: CherryStoreType.MINI_APP, + title: i18n.t('minapp.title'), + path: CherryStoreType.MINI_APP, + component: lazy(() => import('./pages/minapps/MinAppsPage')), + hasSidebar: false, // 目前都没有侧边栏 + items: [{ id: 'all', name: `All ${i18n.t('minapp.title')}` }] // 预设 "All" 子分类 + } + // { + // 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 const ROUTERS_MAP = new Map(ROUTERS.map((router) => [router.id, router])) diff --git a/src/renderer/src/pages/discover/type.ts b/src/renderer/src/pages/discover/type.ts new file mode 100644 index 000000000..79c1dd367 --- /dev/null +++ b/src/renderer/src/pages/discover/type.ts @@ -0,0 +1,7 @@ +export interface InternalCategory { + id: string + title: string + path: string + hasSidebar?: boolean + items: Array<{ id: string; name: string; count?: number }> +} diff --git a/src/renderer/src/pages/home/Messages/Blocks/index.tsx b/src/renderer/src/pages/home/Messages/Blocks/index.tsx index 3da83aeec..efd7e737f 100644 --- a/src/renderer/src/pages/home/Messages/Blocks/index.tsx +++ b/src/renderer/src/pages/home/Messages/Blocks/index.tsx @@ -3,7 +3,7 @@ import type { RootState } from '@renderer/store' import { messageBlocksSelectors } from '@renderer/store/messageBlock' import type { ImageMessageBlock, MainTextMessageBlock, Message, MessageBlock } from '@renderer/types/newMessage' import { MessageBlockStatus, MessageBlockType } from '@renderer/types/newMessage' -import { AnimatePresence, motion } from 'motion/react' +import { AnimatePresence, motion, type Variants } from 'motion/react' import React, { useMemo } from 'react' import { useSelector } from 'react-redux' import styled from 'styled-components' @@ -25,7 +25,7 @@ interface AnimatedBlockWrapperProps { enableAnimation: boolean } -const blockWrapperVariants = { +const blockWrapperVariants: Variants = { visible: { opacity: 1, x: 0, diff --git a/src/renderer/src/pages/home/Messages/MessageContent.tsx b/src/renderer/src/pages/home/Messages/MessageContent.tsx index 61b113a01..420cd7d46 100644 --- a/src/renderer/src/pages/home/Messages/MessageContent.tsx +++ b/src/renderer/src/pages/home/Messages/MessageContent.tsx @@ -15,7 +15,9 @@ const MessageContent: React.FC = ({ message }) => { <> {!isEmpty(message.mentions) && ( - {message.mentions?.map((model) => {'@' + model.name})} + {message.mentions?.map((model) => ( + {'@' + model.name} + ))} )} diff --git a/src/renderer/src/pages/home/Messages/MessageImage.tsx b/src/renderer/src/pages/home/Messages/MessageImage.tsx index a0faf46d0..413f0a2cb 100644 --- a/src/renderer/src/pages/home/Messages/MessageImage.tsx +++ b/src/renderer/src/pages/home/Messages/MessageImage.tsx @@ -62,7 +62,10 @@ const MessageImage: FC = ({ block }) => { byteArrays.push(byteArray) } - const blob = new Blob(byteArrays, { type: mimeType }) + const blob = new Blob( + byteArrays.map((b) => b.slice()), + { type: mimeType } + ) await navigator.clipboard.write([new ClipboardItem({ [mimeType]: blob })]) } else { throw new Error('无效的 base64 图片格式') diff --git a/src/renderer/src/pages/launchpad/LaunchpadPage.tsx b/src/renderer/src/pages/launchpad/LaunchpadPage.tsx index 0a19d6c18..b5dfb96c0 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 c19fabc70..a04765e9a 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/migrate.ts b/src/renderer/src/store/migrate.ts index 9189faa6b..7530b4b03 100644 --- a/src/renderer/src/store/migrate.ts +++ b/src/renderer/src/store/migrate.ts @@ -2006,6 +2006,28 @@ const migrateConfig = { logger.error('migrate 127 error', error as Error) return state } + }, + '128': (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) { + logger.error('migrate 128 error', error as Error) + return state + } } } diff --git a/src/renderer/src/store/settings.ts b/src/renderer/src/store/settings.ts index be1d950b2..3d5e28f6d 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/types/cherryStore.ts b/src/renderer/src/types/cherryStore.ts new file mode 100644 index 000000000..65984a852 --- /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 000000000..153a11bde --- /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 000000000..e48aff75d --- /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 000000000..5e867e49a --- /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 000000000..086bde501 --- /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 000000000..0bff33aa8 --- /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 000000000..751ed4920 --- /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 ( +