From e5c616c12a17ce657e0df9a27a3acbd6c43e0541 Mon Sep 17 00:00:00 2001 From: Haitao Pan Date: Fri, 30 Jan 2026 18:02:11 +0800 Subject: [PATCH] feat: refactor navigation and sidebar structure, implement AI Assistant layout modes --- skills/ui.engineering.principles.v1.md | 160 ++++++ src/app/about/page.tsx | 208 ++++---- .../insight/layout/InsightSidebarContent.tsx | 216 ++++++++ src/app/components/insight/layout/Sidebar.tsx | 214 +------- src/app/docs/DocsSidebar.tsx | 227 +------- src/app/docs/DocsSidebarContent.tsx | 233 +++++++++ src/app/docs/layout.tsx | 40 +- src/app/page.tsx | 256 +++++---- .../panel/components/PanelSidebarContent.tsx | 210 ++++++++ src/app/panel/components/Sidebar.tsx | 210 +------- .../services/moltbot/chats/MoltbotChat.tsx | 118 ++++- src/app/services/page.tsx | 320 +++++++++--- src/components/AppSidebarContent.tsx | 70 +++ src/components/Sidebar.tsx | 64 +-- src/components/UnifiedNavigation.tsx | 488 ++++++++++++++++++ src/components/home/HomeSidebarContent.tsx | 111 ++++ src/components/home/Sidebar.tsx | 106 +--- src/components/layout/SidebarRoot.tsx | 47 ++ src/lib/navigation.ts | 181 +++++++ src/lib/utils.ts | 6 + 20 files changed, 2395 insertions(+), 1090 deletions(-) create mode 100644 skills/ui.engineering.principles.v1.md create mode 100644 src/app/components/insight/layout/InsightSidebarContent.tsx create mode 100644 src/app/docs/DocsSidebarContent.tsx create mode 100644 src/app/panel/components/PanelSidebarContent.tsx create mode 100644 src/components/AppSidebarContent.tsx create mode 100644 src/components/UnifiedNavigation.tsx create mode 100644 src/components/home/HomeSidebarContent.tsx create mode 100644 src/components/layout/SidebarRoot.tsx create mode 100644 src/lib/navigation.ts create mode 100644 src/lib/utils.ts diff --git a/skills/ui.engineering.principles.v1.md b/skills/ui.engineering.principles.v1.md new file mode 100644 index 0000000..f7cd17a --- /dev/null +++ b/skills/ui.engineering.principles.v1.md @@ -0,0 +1,160 @@ + +UI Engineering Principles v1(for Code Agents) + +目标:在不指定框架/库细节的前提下,让 agent 自主做正确的 UI 工程决策。 +输出要求:每次改动必须同时满足 可维护 / 可测试 / 可演进 / 可回滚。 + +1) 先决策再动手(Decision Log) + +在修改前,agent 必须在 PR 描述或注释里输出一段 Decision Log(最多 10 行): + +组件边界如何划分(为什么) + +状态放哪(为什么) + +哪些是公共骨架,哪些是 slot/children +兼容性策略(是否破坏现有 API) +风险点与回滚点 + +不求长,但求明确。没有 Decision Log 视为不合格提交。 + +2) 组件设计准则(边界与组合) +2.1 单一职责 + +一个组件只负责一个“稳定职责” + +如果组件名需要用 “And/With/Plus” 连接多个职责 → 必须拆分 + +2.2 优先 Composition(组合)而不是 Config(配置) + +优先通过 children / slots 组合 UI 结构 + +禁止为“结构差异”引入大量 boolean/enum props + +只有在“结构稳定但样式/行为轻微差异”时才允许 variant + +2.3 公共骨架 + 可插拔区域 + +抽取稳定的 Layout Skeleton 为 Root/Wrapper + +差异点必须落在 children/slots(Header/Nav/Footer/Actions 等) + +3) 状态管理准则(少、近、可注入) +3.1 状态最小化 + +不新增“可从现有数据推导”的 state(用 derived/computed) + +不新增“只服务渲染”的 state(用 CSS/DOM 或 memo) + +3.2 状态就近原则 + +UI 局部状态(open/collapsed/hover)放组件内部 + +跨多个子组件共享但局部范围内 → Context(组件树内) + +全局业务状态 → store/server(仅必要时) + +3.3 可测试性要求 + +关键 UI 状态必须可通过 props 注入或通过 context mock + +逻辑优先放 hook,视图保持“纯 render” + +4) 样式与可覆盖性(不锁死) +4.1 className 可组合 + +所有可复用组件必须接受 className 并合并 + +禁止写死样式导致无法覆盖(除非明确是私有组件) + +4.2 语义优先 + +使用语义标签:nav/aside/header/main/button + +可交互元素必须可键盘操作(button/link,不用 div 冒充) + +4.3 选择器稳定 + +使用 data-* 或有限的 class 作为状态标记(如 collapsed/active) + +避免复杂层级选择器导致脆弱 + +5) 兼容性与演进(不破坏用户) +5.1 默认向后兼容 + +任何改动尽量不破坏旧 API(props/DOM 结构/路由) + +若必须破坏:提供迁移方式或兼容层,并在说明中写清 + +5.2 小步可回滚 + +每个提交尽量原子化(Atomic Commit) + +重构必须可分阶段:先抽公共,再迁移,再清理旧代码 + +6) 性能准则(先可读,后优化) +6.1 不做“预优化” + +禁止在没有证据时引入复杂 memo/useCallback/useMemo + +若新增 memo:必须说明为什么必要(避免不必要渲染/昂贵计算) + +6.2 可见性能问题再优化 + +性能优化必须是“局部、可验证、可回滚”的 + +7) 测试准则(最小但关键) +7.1 测试优先覆盖“行为”,不是“实现” + +测试用户可感知行为:导航高亮、折叠、权限可见性等 + +不测试内部实现细节(class 名、组件层级等) + +7.2 最小覆盖要求 + +重构 UI:至少补 1 个关键路径测试(unit 或 integration) + +引入新状态/交互:必须有测试覆盖 + +8) 文档与可读性(未来的你也是用户) +8.1 代码即文档 + +组件 API 清晰可读(prop 名可解释) + +复杂逻辑提取为函数/hook,并用短注释解释“为什么” + +8.2 禁止“聪明代码” + +避免迷宫式条件渲染 + +避免过度抽象(抽象必须带来复用或一致性收益) + +9) 输出格式要求(agent 必须遵守) + +每次提交/变更输出: + +Decision Log(≤10 行) + +变更摘要(改了什么) + +风险点(可能破坏什么) + +验证方式(如何验证:命令 + 预期) + +回滚方式(如何快速退回) + +10) 最终验收(Self-check) + +Agent 在结束前必须自检并明确回答(是/否): + +是否减少了重复,而不是引入更多分支? + +是否让新增需求更容易通过组合实现? + +是否状态更少、更近、更可测? + +是否可覆盖样式并保持语义化? + +是否可回滚(原子提交/分阶段)? + +任意一项为“否”,必须解释原因或调整实现。 diff --git a/src/app/about/page.tsx b/src/app/about/page.tsx index 9be73fa..792c07b 100644 --- a/src/app/about/page.tsx +++ b/src/app/about/page.tsx @@ -1,98 +1,126 @@ -'use client' +"use client"; -import React from 'react' -import { translations } from '../../i18n/translations' -import { useLanguage } from '../../i18n/LanguageProvider' -import Navbar from '../../components/Navbar' -import Footer from '../../components/Footer' +import React from "react"; +import { translations } from "../../i18n/translations"; +import { useLanguage } from "../../i18n/LanguageProvider"; +import UnifiedNavigation from "../../components/UnifiedNavigation"; +import Footer from "../../components/Footer"; export default function AboutPage() { - const { language } = useLanguage() - const t = translations[language].about + const { language } = useLanguage(); + const t = translations[language].about; - return ( -
-
+ return ( +
+
-
- +
+ -
-
- - {/* Header */} -
-

- {t.title} -

-

- {t.subtitle} -

-
- - {/* Disclaimer Section */} -
-
-
- -
-
-

Disclaimer

-

- {t.disclaimer} -

-
-
-
- - {/* Acknowledgments */} -
-
-

- {t.acknowledgments} -

- -
-

- {t.toolsTitle} -

- -

- {t.toolsNote} -

-
-
- -
-
-
- -

- {t.opensource} -

-
-
-
- -
-
- -
+
+
+ {/* Header */} +
+

+ {t.title} +

+

{t.subtitle}

-
- ) + + {/* Disclaimer Section */} +
+
+
+ + + + + +
+
+

+ Disclaimer +

+

+ {t.disclaimer} +

+
+
+
+ + {/* Acknowledgments */} +
+
+

+ {t.acknowledgments} +

+ +
+

+ {t.toolsTitle} +

+ +

{t.toolsNote}

+
+
+ +
+
+
+ + + +

{t.opensource}

+
+
+
+
+
+ +
+
+
+ ); } diff --git a/src/app/components/insight/layout/InsightSidebarContent.tsx b/src/app/components/insight/layout/InsightSidebarContent.tsx new file mode 100644 index 0000000..f508bd9 --- /dev/null +++ b/src/app/components/insight/layout/InsightSidebarContent.tsx @@ -0,0 +1,216 @@ +'use client' + +import React from 'react' +import { BellRing, Compass, Layers, Sparkles, type LucideIcon, PanelLeftClose, PanelLeftOpen, EyeOff } from 'lucide-react' +import { QueryLanguage, TopologyMode } from '../../insight/store/urlState' +import { SidebarHeader, SidebarContent } from '../../../../components/layout/SidebarRoot' + +interface InsightSidebarContentProps { + topologyMode: TopologyMode + activeLanguages: QueryLanguage[] + onSelectSection: (section: string) => void + onTopologyChange: (mode: TopologyMode) => void + onToggleLanguage: (language: QueryLanguage) => void + onToggleCollapse: () => void + onHide: () => void + activeSection: string + collapsed: boolean +} + +const sections: { id: string; label: string; icon: LucideIcon }[] = [ + { id: 'topology', label: 'Topology', icon: Layers }, + { id: 'explore', label: 'Explore', icon: Compass }, + { id: 'slo', label: 'SLO & Alerts', icon: BellRing }, + { id: 'ai', label: 'AI Assistant', icon: Sparkles } +] + +const topologyOptions: { id: TopologyMode; label: string; hint: string }[] = [ + { id: 'application', label: 'Application', hint: 'Services and dependencies' }, + { id: 'network', label: 'Network', hint: 'Gateways, meshes and edges' }, + { id: 'resource', label: 'Resource', hint: 'Clusters, nodes and workloads' } +] + +const languageOptions: { id: QueryLanguage; label: string; description: string }[] = [ + { id: 'promql', label: 'PromQL', description: 'Metrics analytics' }, + { id: 'logql', label: 'LogQL', description: 'Log navigation' }, + { id: 'traceql', label: 'TraceQL', description: 'Trace exploration' } +] + +const languageLabels: Record = { + promql: 'Prometheus metrics', + logql: 'Log stream', + traceql: 'Distributed traces' +} + +export function InsightSidebarContent({ + topologyMode, + activeLanguages, + activeSection, + onSelectSection, + onTopologyChange, + onToggleLanguage, + onToggleCollapse, + onHide, + collapsed +}: InsightSidebarContentProps) { + return ( + <> + + {!collapsed && ( +
+

Insight Workbench

+

+ Navigate topology, run cross-domain queries and keep SLOs on track. +

+
+ )} +
+ + +
+
+ + + + +
+ {!collapsed ? ( + <> +

Active explorers

+
    + {activeLanguages.map(language => ( +
  • + {languageLabels[language]} + QL +
  • + ))} + {activeLanguages.length === 0 &&
  • No languages selected.
  • } +
+ + ) : ( +
+

Active

+
+ {activeLanguages.map(language => ( + {languageLabels[language]} + ))} + {activeLanguages.length === 0 && None} +
+
+ )} +
+
+ + ) +} diff --git a/src/app/components/insight/layout/Sidebar.tsx b/src/app/components/insight/layout/Sidebar.tsx index b481255..43f2520 100644 --- a/src/app/components/insight/layout/Sidebar.tsx +++ b/src/app/components/insight/layout/Sidebar.tsx @@ -1,8 +1,9 @@ 'use client' -import { BellRing, Compass, Layers, Sparkles, type LucideIcon, PanelLeftClose, PanelLeftOpen, EyeOff } from 'lucide-react' - +import React from 'react' import { QueryLanguage, TopologyMode } from '../../insight/store/urlState' +import { SidebarRoot } from '../../../../components/layout/SidebarRoot' +import { InsightSidebarContent } from './InsightSidebarContent' interface SidebarProps { topologyMode: TopologyMode @@ -16,210 +17,15 @@ interface SidebarProps { collapsed: boolean } -const sections: { id: string; label: string; icon: LucideIcon }[] = [ - { id: 'topology', label: 'Topology', icon: Layers }, - { id: 'explore', label: 'Explore', icon: Compass }, - { id: 'slo', label: 'SLO & Alerts', icon: BellRing }, - { id: 'ai', label: 'AI Assistant', icon: Sparkles } -] +export function Sidebar(props: SidebarProps) { + const { collapsed } = props -const topologyOptions: { id: TopologyMode; label: string; hint: string }[] = [ - { id: 'application', label: 'Application', hint: 'Services and dependencies' }, - { id: 'network', label: 'Network', hint: 'Gateways, meshes and edges' }, - { id: 'resource', label: 'Resource', hint: 'Clusters, nodes and workloads' } -] - -const languageOptions: { id: QueryLanguage; label: string; description: string }[] = [ - { id: 'promql', label: 'PromQL', description: 'Metrics analytics' }, - { id: 'logql', label: 'LogQL', description: 'Log navigation' }, - { id: 'traceql', label: 'TraceQL', description: 'Trace exploration' } -] - -export function Sidebar({ - topologyMode, - activeLanguages, - activeSection, - onSelectSection, - onTopologyChange, - onToggleLanguage, - onToggleCollapse, - onHide, - collapsed -}: SidebarProps) { return ( - + > + + ) } - -const languageLabels: Record = { - promql: 'Prometheus metrics', - logql: 'Log stream', - traceql: 'Distributed traces' -} diff --git a/src/app/docs/DocsSidebar.tsx b/src/app/docs/DocsSidebar.tsx index b8ddbec..f1d5840 100644 --- a/src/app/docs/DocsSidebar.tsx +++ b/src/app/docs/DocsSidebar.tsx @@ -1,235 +1,20 @@ 'use client' -import Link from 'next/link' import { usePathname } from 'next/navigation' -import { useState, useEffect } from 'react' -import { - ChevronRight, - ChevronDown, - Book, - Settings, - Zap, - Shield, - HelpCircle, - Code, - Terminal, - Activity, - GraduationCap, - Layout, - Layers, - Puzzle -} from 'lucide-react' -import type { DocCollection, DocVersionOption } from './types' +import type { DocCollection } from './types' +import { SidebarRoot } from '../../components/layout/SidebarRoot' +import { DocsSidebarContent } from './DocsSidebarContent' interface DocsSidebarProps { collections: DocCollection[] } -// Helper to humanize category names -const humanize = (s: string) => { - if (!s) return '' - return s.replace(/-/g, ' ').replace(/\b\w/g, c => c.toUpperCase()) -} - -// Icon mapping for categories -const ICON_MAP: Record = { - 'getting-started': Book, - 'architecture': Zap, - 'usage': Settings, - 'advanced': GraduationCap, - 'api': Code, - 'development': Terminal, - 'operations': Activity, - 'governance': Shield, - 'appendix': HelpCircle, - 'integrations': Puzzle, - 'overview': Layout, - 'core-concepts': Layers, -} - -const ADVANCED_GROUP = ['api', 'development', 'operations', 'governance', 'advanced'] - export default function DocsSidebar({ collections }: DocsSidebarProps) { const pathname = usePathname() - // Sort collections: Console first, then others alphabetically - const sortedCollections = [...collections].sort((a, b) => { - if (a.slug.includes('console')) return -1 - if (b.slug.includes('console')) return 1 - return a.title.localeCompare(b.title) - }) - return ( - - ) -} - -function CollectionGroup({ collection, activePath }: { collection: DocCollection; activePath: string }) { - const [isOpen, setIsOpen] = useState(true) - - // Group versions by category - const grouped: Record = {} - const topLevel: DocVersionOption[] = [] - - collection.versions.forEach(v => { - const category = v.category - if (!category || category === 'overview' || category === 'index') { - topLevel.push(v) - } else { - if (!grouped[category]) grouped[category] = [] - grouped[category].push(v) - } - }) - - const hasAdvanced = ADVANCED_GROUP.some(k => grouped[k]) - - return ( -
- - - {isOpen && ( -
- {/* Uncategorized / Overview / README */} - {topLevel.length > 0 && ( -
    - {topLevel.map(v => ( - - ))} -
- )} - - {/* Main Categories (Getting Started, Architecture, etc.) */} -
- {Object.entries(grouped) - .filter(([k]) => !ADVANCED_GROUP.includes(k)) - .sort((a, b) => a[0].localeCompare(b[0])) - .map(([category, versions]) => ( - - ))} -
- - {/* Advanced Section Dropdown */} - {hasAdvanced && ( - - )} -
- )} -
- ) -} - -function CategorySection({ title, versions, collectionSlug, activePath }: { title: string; versions: DocVersionOption[]; collectionSlug: string; activePath: string }) { - const Icon = ICON_MAP[title] || Book - - // Auto-expand if active - const isActive = versions.some(v => activePath === `/docs/${collectionSlug}/${v.slug}`) - const [isExpanded, setIsExpanded] = useState(true) - - return ( -
-
- - {humanize(title)} -
-
    - {versions.map(v => ( - - ))} -
-
- ) -} - -function AdvancedSection({ grouped, collectionSlug, activePath }: { grouped: Record; collectionSlug: string; activePath: string }) { - // Check if anything inside is active to auto-expand - const isInsideActive = ADVANCED_GROUP.some(k => grouped[k]?.some(v => activePath === `/docs/${collectionSlug}/${v.slug}`)) - - const [isExpanded, setIsExpanded] = useState(isInsideActive) - - useEffect(() => { - if (isInsideActive) setIsExpanded(true) - }, [isInsideActive]) - - return ( -
- - - {isExpanded && ( -
- {ADVANCED_GROUP.map(k => grouped[k] && ( -
-
- - {humanize(k)} -
-
    - {grouped[k].map(v => ( - - ))} -
-
- ))} -
- )} -
- ) -} - -function SidebarLink({ version, collectionSlug, activePath }: { version: DocVersionOption; collectionSlug: string; activePath: string }) { - const href = `/docs/${collectionSlug}/${version.slug}` - const isPageActive = activePath === href - - return ( -
  • - - {isPageActive && } - {version.title} - {!isPageActive && } - -
  • + + + ) } diff --git a/src/app/docs/DocsSidebarContent.tsx b/src/app/docs/DocsSidebarContent.tsx new file mode 100644 index 0000000..42c271e --- /dev/null +++ b/src/app/docs/DocsSidebarContent.tsx @@ -0,0 +1,233 @@ +'use client' + +import Link from 'next/link' +import { useState, useEffect } from 'react' +import { + ChevronRight, + ChevronDown, + Book, + Settings, + Zap, + Shield, + HelpCircle, + Code, + Terminal, + Activity, + GraduationCap, + Layout, + Layers, + Puzzle +} from 'lucide-react' +import type { DocCollection, DocVersionOption } from './types' +import { SidebarContent } from '../../components/layout/SidebarRoot' + +interface DocsSidebarContentProps { + collections: DocCollection[] + activePath: string +} + +// Helper to humanize category names +const humanize = (s: string) => { + if (!s) return '' + return s.replace(/-/g, ' ').replace(/\b\w/g, c => c.toUpperCase()) +} + +// Icon mapping for categories +const ICON_MAP: Record = { + 'getting-started': Book, + 'architecture': Zap, + 'usage': Settings, + 'advanced': GraduationCap, + 'api': Code, + 'development': Terminal, + 'operations': Activity, + 'governance': Shield, + 'appendix': HelpCircle, + 'integrations': Puzzle, + 'overview': Layout, + 'core-concepts': Layers, +} + +const ADVANCED_GROUP = ['api', 'development', 'operations', 'governance', 'advanced'] + +export function DocsSidebarContent({ collections, activePath }: DocsSidebarContentProps) { + // Sort collections: Console first, then others alphabetically + const sortedCollections = [...collections].sort((a, b) => { + if (a.slug.includes('console')) return -1 + if (b.slug.includes('console')) return 1 + return a.title.localeCompare(b.title) + }) + + return ( + + + + ) +} + +function CollectionGroup({ collection, activePath }: { collection: DocCollection; activePath: string }) { + const [isOpen, setIsOpen] = useState(true) + + // Group versions by category + const grouped: Record = {} + const topLevel: DocVersionOption[] = [] + + collection.versions.forEach(v => { + const category = v.category + if (!category || category === 'overview' || category === 'index') { + topLevel.push(v) + } else { + if (!grouped[category]) grouped[category] = [] + grouped[category].push(v) + } + }) + + const hasAdvanced = ADVANCED_GROUP.some(k => grouped[k]) + + return ( +
    + + + {isOpen && ( +
    + {/* Uncategorized / Overview / README */} + {topLevel.length > 0 && ( +
      + {topLevel.map(v => ( + + ))} +
    + )} + + {/* Main Categories (Getting Started, Architecture, etc.) */} +
    + {Object.entries(grouped) + .filter(([k]) => !ADVANCED_GROUP.includes(k)) + .sort((a, b) => a[0].localeCompare(b[0])) + .map(([category, versions]) => ( + + ))} +
    + + {/* Advanced Section Dropdown */} + {hasAdvanced && ( + + )} +
    + )} +
    + ) +} + +function CategorySection({ title, versions, collectionSlug, activePath }: { title: string; versions: DocVersionOption[]; collectionSlug: string; activePath: string }) { + const Icon = ICON_MAP[title] || Book + + // Auto-expand if active + const isActive = versions.some(v => activePath === `/docs/${collectionSlug}/${v.slug}`) + + return ( +
    +
    + + {humanize(title)} +
    +
      + {versions.map(v => ( + + ))} +
    +
    + ) +} + +function AdvancedSection({ grouped, collectionSlug, activePath }: { grouped: Record; collectionSlug: string; activePath: string }) { + // Check if anything inside is active to auto-expand + const isInsideActive = ADVANCED_GROUP.some(k => grouped[k]?.some(v => activePath === `/docs/${collectionSlug}/${v.slug}`)) + + const [isExpanded, setIsExpanded] = useState(isInsideActive) + + useEffect(() => { + if (isInsideActive) setIsExpanded(true) + }, [isInsideActive]) + + return ( +
    + + + {isExpanded && ( +
    + {ADVANCED_GROUP.map(k => grouped[k] && ( +
    +
    + + {humanize(k)} +
    +
      + {grouped[k].map(v => ( + + ))} +
    +
    + ))} +
    + )} +
    + ) +} + +function SidebarLink({ version, collectionSlug, activePath }: { version: DocVersionOption; collectionSlug: string; activePath: string }) { + const href = `/docs/${collectionSlug}/${version.slug}` + const isPageActive = activePath === href + + return ( +
  • + + {isPageActive && } + {version.title} + {!isPageActive && } + +
  • + ) +} diff --git a/src/app/docs/layout.tsx b/src/app/docs/layout.tsx index 7f9ac3b..c6663a0 100644 --- a/src/app/docs/layout.tsx +++ b/src/app/docs/layout.tsx @@ -1,21 +1,25 @@ -import { getDocCollections } from './resources.server' -import DocsSidebar from './DocsSidebar' -import Navbar from '@components/Navbar' -import Footer from '@components/Footer' +import { getDocCollections } from "./resources.server"; +import DocsSidebar from "./DocsSidebar"; +import UnifiedNavigation from "@components/UnifiedNavigation"; +import Footer from "@components/Footer"; -export default async function DocsLayout({ children }: { children: React.ReactNode }) { - const collections = await getDocCollections() +export default async function DocsLayout({ + children, +}: { + children: React.ReactNode; +}) { + const collections = await getDocCollections(); - return ( -
    - -
    - -
    - {children} -
    -
    -
    -
    - ) + return ( +
    + +
    + +
    + {children} +
    +
    +
    +
    + ); } diff --git a/src/app/page.tsx b/src/app/page.tsx index 926b0eb..e492870 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,6 +1,6 @@ -'use client' +"use client"; -export const dynamic = 'error' +export const dynamic = "error"; import { AppWindow, @@ -17,88 +17,113 @@ import { Sparkles, Terminal, Users, -} from 'lucide-react' -import Footer from '../components/Footer' -import Navbar from '../components/Navbar' -import { useUserStore } from '../lib/userStore' -import { useLanguage } from '../i18n/LanguageProvider' -import { translations } from '../i18n/translations' +} from "lucide-react"; +import Footer from "../components/Footer"; +import UnifiedNavigation from "../components/UnifiedNavigation"; +import { useUserStore } from "../lib/userStore"; +import { useLanguage } from "../i18n/LanguageProvider"; +import { translations } from "../i18n/translations"; const iconMap: Record = { // English keys - 'Create your app': PlusCircle, - 'Register your app': ShieldCheck, - 'Deploy your app': Users, - 'Add a new user to your project': Users, - 'Register a new application': AppWindow, - 'Deploy your application': Command, - 'Invite a user': MousePointerClick, - 'Get started': Sparkles, - 'Creating your application': AppWindow, - 'More about Authentication': ShieldCheck, - 'Understanding Authorization': Lock, - 'Machine-to-Machine': Layers, - 'Connect via CLI': Terminal, - 'REST & Admin APIs': Link, + "Create your app": PlusCircle, + "Register your app": ShieldCheck, + "Deploy your app": Users, + "Add a new user to your project": Users, + "Register a new application": AppWindow, + "Deploy your application": Command, + "Invite a user": MousePointerClick, + "Get started": Sparkles, + "Creating your application": AppWindow, + "More about Authentication": ShieldCheck, + "Understanding Authorization": Lock, + "Machine-to-Machine": Layers, + "Connect via CLI": Terminal, + "REST & Admin APIs": Link, // Chinese keys - '创建您的应用': PlusCircle, - '注册您的应用': ShieldCheck, - '部署您的应用': Users, - '向项目添加新用户': Users, - '注册新应用程序': AppWindow, - '部署您的应用程序': Command, - '邀请用户': MousePointerClick, - '开始使用': Sparkles, - '创建您的应用程序': AppWindow, - '关于身份验证': ShieldCheck, - '了解授权': Lock, - '机器对机器': Layers, - '通过 CLI 连接': Terminal, -} + 创建您的应用: PlusCircle, + 注册您的应用: ShieldCheck, + 部署您的应用: Users, + 向项目添加新用户: Users, + 注册新应用程序: AppWindow, + 部署您的应用程序: Command, + 邀请用户: MousePointerClick, + 开始使用: Sparkles, + 创建您的应用程序: AppWindow, + 关于身份验证: ShieldCheck, + 了解授权: Lock, + 机器对机器: Layers, + "通过 CLI 连接": Terminal, +}; -const getIcon = (key: string, fallback: any) => iconMap[key] || fallback +const getIcon = (key: string, fallback: any) => iconMap[key] || fallback; const nextSteps = [ - { title: 'Add a new user to your project', status: 'NEW', icon: Users }, - { title: 'Register a new application', status: 'NEW', icon: AppWindow }, - { title: 'Deploy your application', status: 'READY', icon: Command }, - { title: 'Invite a user', status: 'READY', icon: MousePointerClick }, -] + { title: "Add a new user to your project", status: "NEW", icon: Users }, + { title: "Register a new application", status: "NEW", icon: AppWindow }, + { title: "Deploy your application", status: "READY", icon: Command }, + { title: "Invite a user", status: "READY", icon: MousePointerClick }, +]; const stats = [ - { value: '~150k', label: 'Applications integrated with Cloud-Neutral Toolkit' }, - { value: '~330k', label: 'Daily active users' }, - { value: '7', label: 'Go check out our examples & guides' }, -] + { + value: "~150k", + label: "Applications integrated with Cloud-Neutral Toolkit", + }, + { value: "~330k", label: "Daily active users" }, + { value: "7", label: "Go check out our examples & guides" }, +]; const shortcuts = [ - { title: 'Get started', description: 'An overview of using Cloud-Neutral Toolkit', icon: Sparkles }, - { title: 'Creating your application', description: 'Integrate Cloud-Neutral Toolkit into your application', icon: AppWindow }, { - title: 'More about Authentication', - description: 'Understand all about authenticating with Cloud-Neutral Toolkit', + title: "Get started", + description: "An overview of using Cloud-Neutral Toolkit", + icon: Sparkles, + }, + { + title: "Creating your application", + description: "Integrate Cloud-Neutral Toolkit into your application", + icon: AppWindow, + }, + { + title: "More about Authentication", + description: + "Understand all about authenticating with Cloud-Neutral Toolkit", icon: ShieldCheck, }, { - title: 'Understanding Authorization', - description: 'Scope out all about authorization using Cloud-Neutral Toolkit', + title: "Understanding Authorization", + description: + "Scope out all about authorization using Cloud-Neutral Toolkit", icon: Lock, }, - { title: 'Machine-to-Machine', description: 'Integrate Cloud-Neutral Toolkit into your services', icon: Layers }, - { title: 'Connect via CLI', description: 'Connect Cloud-Neutral Toolkit with your application via CLI', icon: Terminal }, { - title: 'REST & Admin APIs', - description: 'Programmatically integrate Cloud-Neutral Toolkit into your application', + title: "Machine-to-Machine", + description: "Integrate Cloud-Neutral Toolkit into your services", + icon: Layers, + }, + { + title: "Connect via CLI", + description: "Connect Cloud-Neutral Toolkit with your application via CLI", + icon: Terminal, + }, + { + title: "REST & Admin APIs", + description: + "Programmatically integrate Cloud-Neutral Toolkit into your application", icon: Link, }, -] +]; export default function HomePage() { return (
    -
    +
    - +
    @@ -108,20 +133,24 @@ export default function HomePage() {
    - ) + ); } -function HeroSection() { - const { user } = useUserStore() - const { language } = useLanguage() - const t = translations[language].marketing.home +export function HeroSection() { + const { user } = useUserStore(); + const { language } = useLanguage(); + const t = translations[language].marketing.home; return (
    -

    {t.hero.eyebrow}

    -

    {t.hero.title}

    +

    + {t.hero.eyebrow} +

    +

    + {t.hero.title} +

    {t.hero.subtitle}

    @@ -158,9 +187,12 @@ function HeroSection() {
    {t.heroCards.map((card, index: number) => { - const Icon = getIcon(card.title, PlusCircle) + const Icon = getIcon(card.title, PlusCircle); return ( -
    +
    @@ -172,87 +204,108 @@ function HeroSection() {
    - ) + ); })}
    - ) + ); } -function NextStepsSection() { - const { language } = useLanguage() - const t = translations[language].marketing.home +export function NextStepsSection() { + const { language } = useLanguage(); + const t = translations[language].marketing.home; return (
    -

    {t.nextSteps.title}

    - {t.nextSteps.badge} +

    + {t.nextSteps.title} +

    + + {t.nextSteps.badge} +
    {t.nextSteps.items.map((item, index: number) => { - const Icon = getIcon(item.title, Users) + const Icon = getIcon(item.title, Users); return ( -
    +
    - {item.status} + + {item.status} +
    -

    {item.title}

    +

    + {item.title} +

    - ) + ); })}
    - ) + ); } -function StatsSection() { - const { language } = useLanguage() - const t = translations[language].marketing.home +export function StatsSection() { + const { language } = useLanguage(); + const t = translations[language].marketing.home; return (
    {t.stats.map((stat, index: number) => (
    -
    {stat.value}
    +
    + {stat.value} +

    {stat.label}

    ))}
    - ) + ); } -function ShortcutsSection() { - const { language } = useLanguage() - const t = translations[language].marketing.home +export function ShortcutsSection() { + const { language } = useLanguage(); + const t = translations[language].marketing.home; return (
    -

    {t.shortcuts.title}

    +

    + {t.shortcuts.title} +

    {t.shortcuts.subtitle}

    - - - + + +
    {t.shortcuts.items.map((item, index: number) => { - const Icon = getIcon(item.title, Sparkles) + const Icon = getIcon(item.title, Sparkles); return (
    -
    {item.title}
    +
    + {item.title} +

    {item.description}

    - +
    - ) + ); })}
    - ) + ); } function LogoPill({ label }: { label: string }) { @@ -281,5 +339,5 @@ function LogoPill({ label }: { label: string }) {
    {label} - ) + ); } diff --git a/src/app/panel/components/PanelSidebarContent.tsx b/src/app/panel/components/PanelSidebarContent.tsx new file mode 100644 index 0000000..5db761c --- /dev/null +++ b/src/app/panel/components/PanelSidebarContent.tsx @@ -0,0 +1,210 @@ +'use client' + +import Link from 'next/link' +import { usePathname } from 'next/navigation' +import { useMemo, type ComponentType } from 'react' + +import { getExtensionRegistry } from '@extensions/loader' +import { useLanguage } from '@i18n/LanguageProvider' +import { translations } from '@i18n/translations' +import { resolveAccess } from '@lib/accessControl' +import { useUserStore } from '@lib/userStore' +import { SidebarHeader, SidebarContent } from '../../../components/layout/SidebarRoot' + +const registry = getExtensionRegistry() +const PlaceholderIcon: ComponentType<{ className?: string }> = () => null + +interface NavItem { + href: string + label: string + description: string + Icon: ComponentType<{ className?: string }> + disabled: boolean +} + +interface NavSection { + title: string + items: NavItem[] +} + +function isActive(pathname: string, href: string) { + if (href === '/panel') { + return pathname === '/panel' + } + return pathname.startsWith(href) +} + +export interface PanelSidebarContentProps { + onNavigate?: () => void +} + +export function PanelSidebarContent({ onNavigate }: PanelSidebarContentProps) { + const pathname = usePathname() + const { language } = useLanguage() + const copy = translations[language].userCenter.mfa + const user = useUserStore((state) => state.user) + const requiresSetup = Boolean(user && (!user.mfaEnabled || user.mfaPending)) + + const navSections = useMemo(() => { + return registry.sidebar + .map((section) => { + const items = section.items + .map((item) => { + const { route } = item + const guardResult = route.guard ? resolveAccess(user, route.guard) : { allowed: true } + const requiresRole = Boolean(route.guard?.roles?.length) + if (requiresRole && !guardResult.allowed) { + return null + } + + const disabledByGuard = !requiresRole && !guardResult.allowed + const disabled = + item.disabled || + disabledByGuard || + (requiresSetup && route.path !== '/panel/account') + + const Icon = route.icon ?? PlaceholderIcon + + return { + href: route.path, + label: route.label, + description: route.description ?? '', + Icon, + disabled, + } + }) + .filter((value): value is NavItem => Boolean(value)) + + if (items.length === 0) { + return null + } + + return { + title: section.title, + items, + } + }) + .filter((value): value is NavSection => Boolean(value)) + }, [requiresSetup, user]) + + return ( + <> + +

    XControl

    +

    User Center

    +

    在同一处掌控权限与功能特性。

    + + {requiresSetup ? ( +
    +

    {copy.pendingHint}

    +

    {copy.lockedMessage}

    +
    + + {copy.actions.setup} + + + {copy.actions.docs} + +
    +
    + ) : null} +
    + + + {navSections.map((section) => { + const sectionDisabled = section.items.every((item) => item.disabled) + + return ( +
    +

    + {section.title} +

    +
    + {section.items.map((item) => { + const active = isActive(pathname, item.href) + const { Icon } = item + + const baseClasses = [ + 'group flex items-center gap-3 rounded-[var(--radius-xl)] border px-3 py-3 text-sm transition-colors', + ] + if (item.disabled) { + baseClasses.push( + 'cursor-not-allowed border-dashed border-[color:var(--color-surface-border)] text-[var(--color-text-subtle)] opacity-60', + ) + } else { + baseClasses.push( + 'border-transparent text-[var(--color-text-subtle)] hover:border-[color:var(--color-primary-border)] hover:bg-[var(--color-surface-hover)] hover:text-[var(--color-primary)]', + ) + } + if (active) { + baseClasses.push( + 'border-[color:var(--color-primary)] bg-[var(--color-primary-muted)] text-[var(--color-primary)] shadow-[var(--shadow-sm)]', + ) + } + + const iconClasses = ['flex h-8 w-8 items-center justify-center rounded-xl transition-colors'] + if (active) { + iconClasses.push('bg-[var(--color-primary)] text-[var(--color-primary-foreground)]') + } else if (item.disabled) { + iconClasses.push('bg-[var(--color-surface-muted)] text-[var(--color-text-subtle)] opacity-60') + } else { + iconClasses.push( + 'bg-[var(--color-surface-muted)] text-[var(--color-text-subtle)] group-hover:bg-[var(--color-primary-muted)] group-hover:text-[var(--color-primary)]', + ) + } + + const descriptionClasses = [ + 'text-xs transition-colors', + item.disabled + ? 'text-[var(--color-text-subtle)] opacity-60' + : 'text-[var(--color-text-subtle)] group-hover:text-[var(--color-primary)]', + ] + + const content = ( +
    + + + + + {item.label} + {item.description} + +
    + ) + + if (item.disabled) { + return ( +
    + {content} +
    + ) + } + + return ( + + {content} + + ) + })} +
    +
    + ) + })} +
    + + ) +} diff --git a/src/app/panel/components/Sidebar.tsx b/src/app/panel/components/Sidebar.tsx index 8023eda..403a80f 100644 --- a/src/app/panel/components/Sidebar.tsx +++ b/src/app/panel/components/Sidebar.tsx @@ -1,213 +1,19 @@ 'use client' -import Link from 'next/link' -import { usePathname } from 'next/navigation' -import { useMemo, type ComponentType } from 'react' +import React from 'react' +import { SidebarRoot } from '../../../components/layout/SidebarRoot' +import { PanelSidebarContent, PanelSidebarContentProps } from './PanelSidebarContent' -import { getExtensionRegistry } from '@extensions/loader' -import { useLanguage } from '@i18n/LanguageProvider' -import { translations } from '@i18n/translations' -import { resolveAccess } from '@lib/accessControl' -import { useUserStore } from '@lib/userStore' - -const registry = getExtensionRegistry() -const PlaceholderIcon: ComponentType<{ className?: string }> = () => null - -export interface SidebarProps { +export interface SidebarProps extends PanelSidebarContentProps { className?: string - onNavigate?: () => void -} - -interface NavItem { - href: string - label: string - description: string - Icon: ComponentType<{ className?: string }> - disabled: boolean -} - -interface NavSection { - title: string - items: NavItem[] -} - -function isActive(pathname: string, href: string) { - if (href === '/panel') { - return pathname === '/panel' - } - return pathname.startsWith(href) } export default function Sidebar({ className = '', onNavigate }: SidebarProps) { - const pathname = usePathname() - const { language } = useLanguage() - const copy = translations[language].userCenter.mfa - const user = useUserStore((state) => state.user) - const requiresSetup = Boolean(user && (!user.mfaEnabled || user.mfaPending)) - - const navSections = useMemo(() => { - return registry.sidebar - .map((section) => { - const items = section.items - .map((item) => { - const { route } = item - const guardResult = route.guard ? resolveAccess(user, route.guard) : { allowed: true } - const requiresRole = Boolean(route.guard?.roles?.length) - if (requiresRole && !guardResult.allowed) { - return null - } - - const disabledByGuard = !requiresRole && !guardResult.allowed - const disabled = - item.disabled || - disabledByGuard || - (requiresSetup && route.path !== '/panel/account') - - const Icon = route.icon ?? PlaceholderIcon - - return { - href: route.path, - label: route.label, - description: route.description ?? '', - Icon, - disabled, - } - }) - .filter((value): value is NavItem => Boolean(value)) - - if (items.length === 0) { - return null - } - - return { - title: section.title, - items, - } - }) - .filter((value): value is NavSection => Boolean(value)) - }, [requiresSetup, user]) - return ( - + + ) } diff --git a/src/app/services/moltbot/chats/MoltbotChat.tsx b/src/app/services/moltbot/chats/MoltbotChat.tsx index 3ac8986..d197bc9 100644 --- a/src/app/services/moltbot/chats/MoltbotChat.tsx +++ b/src/app/services/moltbot/chats/MoltbotChat.tsx @@ -1,7 +1,23 @@ 'use client' import { useState, useEffect, useRef } from 'react' -import { useSearchParams } from 'next/navigation' +import { useSearchParams, useRouter } from 'next/navigation' +import { + PanelLeft, + PanelRight, + Maximize2, + Minus, + X, +} from 'lucide-react' +import { + HeroSection, + NextStepsSection, + StatsSection, + ShortcutsSection +} from '@/app/page' +import Footer from '@components/Footer' + +export type ChatLayoutMode = 'left' | 'right' | 'full' const quickActions = [ { id: 'hello', label: 'Say Hello', prompt: 'Hello, who are you?' }, @@ -9,14 +25,15 @@ const quickActions = [ ] export function MoltbotChat() { + const router = useRouter() const searchParams = useSearchParams() const initialQuery = searchParams.get('q') - // Use a ref to track if we've processed the initial query to avoid double-sending in strict mode const hasProcessedInitialQuery = useRef(false) const [message, setMessage] = useState('') const [loading, setLoading] = useState(false) + const [layout, setLayout] = useState('full') const [conversation, setConversation] = useState< { author: 'user' | 'ai'; text: string; timestamp: number }[] >([]) @@ -30,7 +47,7 @@ export function MoltbotChat() { async function appendMessage(prompt: string) { const timestamp = Date.now() - setConversation(prev => [ + setConversation((prev) => [ ...prev, { author: 'user', text: prompt, timestamp }, ]) @@ -38,7 +55,6 @@ export function MoltbotChat() { setLoading(true) try { - // Use internal proxy to handle CORS and auth const response = await fetch('/api/moltbot/chat', { method: 'POST', headers: { @@ -55,12 +71,12 @@ export function MoltbotChat() { const data = await response.json() const reply = data.reply || data.message || JSON.stringify(data) - setConversation(prev => [ + setConversation((prev) => [ ...prev, { author: 'ai', text: reply, timestamp: Date.now() } ]) } catch (error: any) { - setConversation(prev => [ + setConversation((prev) => [ ...prev, { author: 'ai', text: `Error: ${error.message}. Please try again later.`, timestamp: Date.now() } ]) @@ -82,12 +98,65 @@ export function MoltbotChat() { } } - return ( -
    + const HomeContent = () => ( +
    + + + + +
    +
    + ) + + const renderChat = (isSidebar = false) => ( +
    -
    -

    Moltbot AI

    -

    Your personal cloud assistant.

    +
    +
    + 🦞 +
    +
    +

    AI Assistant

    +

    Online

    +
    +
    + +
    + + + + +
    @@ -95,17 +164,16 @@ export function MoltbotChat() { {conversation.length === 0 ? (
    - {/* Moltbot Icon placeholder */}
    🦞
    -

    Welcome to Moltbot

    +

    Welcome to AI Assistant

    Ask me anything about your infrastructure, logs, or just say hello.

    -
    - {quickActions.map(action => ( +
    + {quickActions.map((action: { id: string, label: string, prompt: string }) => (
    ) : (
      - {conversation.map(entry => ( + {conversation.map((entry: { author: string, text: string, timestamp: number }) => (
    • - Press Enter to send + Press Enter
      ) + + if (layout === 'full') { + return renderChat(false) + } + + return ( +
      + {layout === 'left' && renderChat(true)} +
      + +
      + {layout === 'right' && renderChat(true)} +
      + ) } diff --git a/src/app/services/page.tsx b/src/app/services/page.tsx index a7ad1fc..a0fc456 100644 --- a/src/app/services/page.tsx +++ b/src/app/services/page.tsx @@ -1,6 +1,6 @@ -'use client' +"use client"; -import Link from 'next/link' +import Link from "next/link"; import { Activity, ArrowRight, @@ -10,101 +10,180 @@ import { MessageSquare, PenSquare, Package, -} from 'lucide-react' -import Footer from '../../components/Footer' -import Navbar from '../../components/Navbar' -import { useLanguage } from '../../i18n/LanguageProvider' -import { useViewStore } from '../../components/theme/viewStore' -import Material3Layout from './Material3Layout' +} from "lucide-react"; +import Footer from "../../components/Footer"; +import UnifiedNavigation from "../../components/UnifiedNavigation"; +import { useLanguage } from "../../i18n/LanguageProvider"; +import { useViewStore } from "../../components/theme/viewStore"; +import Material3Layout from "./Material3Layout"; -const placeholderCount = 3 +const placeholderCount = 3; type ServiceCardData = { - key: string - name: string - description: string - href: string - icon: any - external?: boolean -} + key: string; + name: string; + description: string; + href: string; + icon: any; + external?: boolean; +}; -const ServiceCard = ({ service, view, isChinese }: { service: ServiceCardData, view: 'classic' | 'material', isChinese: boolean }) => { - const isMaterial = view === 'material' +const ServiceCard = ({ + service, + view, + isChinese, +}: { + service: ServiceCardData; + view: "classic" | "material"; + isChinese: boolean; +}) => { + const isMaterial = view === "material"; const cardContent = ( -
      +
      -
      +
      -
      {service.name}
      -

      {service.description}

      +
      + {service.name} +
      +

      + {service.description} +

      - - {isChinese ? '打开' : 'Open'} + + {isChinese ? "打开" : "Open"}
      - ) + ); if (service.external) { return ( - + {cardContent} - ) + ); } return ( {cardContent} - ) -} + ); +}; -const PlaceholderCard = ({ view, isChinese }: { view: 'classic' | 'material', isChinese: boolean }) => { - const isMaterial = view === 'material' - const placeholderLabel = isChinese ? '更多服务即将上线' : 'More services coming soon' +const PlaceholderCard = ({ + view, + isChinese, +}: { + view: "classic" | "material"; + isChinese: boolean; +}) => { + const isMaterial = view === "material"; + const placeholderLabel = isChinese + ? "更多服务即将上线" + : "More services coming soon"; const placeholderDescription = isChinese - ? '预留卡片位置,持续扩充入口。' - : 'Reserved slots for new service entries.' + ? "预留卡片位置,持续扩充入口。" + : "Reserved slots for new service entries."; return ( -
      +
      -
      +
      -
      {placeholderLabel}
      -

      {placeholderDescription}

      +
      + {placeholderLabel} +
      +

      + {placeholderDescription} +

      - - {isChinese ? '敬请期待' : 'Stay tuned'} + + {isChinese ? "敬请期待" : "Stay tuned"}
      - ) -} + ); +}; -const ServiceGrid = ({ view, services, isChinese }: { view: 'classic' | 'material', services: ServiceCardData[], isChinese: boolean }) => { +const ServiceGrid = ({ + view, + services, + isChinese, +}: { + view: "classic" | "material"; + services: ServiceCardData[]; + isChinese: boolean; +}) => { return (
      {services.map((service) => ( - + ))} {Array.from({ length: placeholderCount }).map((_, index) => ( - + ))}
      - ) -} + ); +}; const ClawdbotLogo = (props: any) => ( ( alt="Clawdbot" {...props} /> -) +); export default function ServicesPage() { - const { view, isHydrated } = useViewStore() - const { language } = useLanguage() - const isChinese = language === 'zh' + const { view, isHydrated } = useViewStore(); + const { language } = useLanguage(); + const isChinese = language === "zh"; const services: ServiceCardData[] = [ - { key: 'editor', name: isChinese ? '编辑器' : 'Editor', description: isChinese ? 'Markdown 发布与排版的在线编辑器。' : 'Markdown publishing and layout editor.', href: 'https://markdown-publisher.svc.plus', icon: PenSquare, external: true, }, - { key: 'wechat-to-markdown', name: isChinese ? '微信转 Markdown' : 'WeChat to Markdown', description: isChinese ? '一键将公众号内容转换为 Markdown。' : 'Convert WeChat articles into Markdown.', href: 'https://wechat-to-markdown.svc.plus', icon: MessageSquare, external: true, }, - { key: 'page-reading', name: isChinese ? 'Page Reading Agent' : 'Page Reading Agent', description: isChinese ? '智能网页阅读与分析服务。' : 'Intelligent web page reading and analysis service.', href: 'https://page-reading.svc.plus', icon: FileText, external: true, }, - { key: 'artifact', name: isChinese ? '制品 / 镜像' : 'Artifact / Mirror', description: isChinese ? '获取核心制品、镜像与下载资源。' : 'Get core artifacts, mirrors, and downloads.', href: '/download', icon: Package, }, - { key: 'cloudIac', name: isChinese ? '云 IaC 目录' : 'Cloud IaC Catalog', description: isChinese ? '浏览云基础设施目录与自动化蓝图。' : 'Browse cloud IaC catalog and automation blueprints.', href: '/cloud_iac', icon: Layers, }, - { key: 'insight', name: isChinese ? 'Insight 工作台' : 'Insight Workbench', description: isChinese ? '进入观测、告警与智能协作控制面。' : 'Observability, alerts, and AI-assisted operations.', href: '/insight', icon: Activity, }, - { key: 'docs', name: isChinese ? '文档 / 解决方案' : 'Docs / Solutions', description: isChinese ? '阅读文档、方案与产品指南。' : 'Read documentation, solutions, and guides.', href: '/docs', icon: BookOpen, }, - { key: 'moltbot', name: isChinese ? 'Moltbot 服务' : 'Moltbot Service', description: isChinese ? 'Moltbot 节点管理服务。' : 'Moltbot node management service.', href: 'https://clawdbot.svc.plus/', icon: ClawdbotLogo, external: true, }, - ] + { + key: "editor", + name: isChinese ? "编辑器" : "Editor", + description: isChinese + ? "Markdown 发布与排版的在线编辑器。" + : "Markdown publishing and layout editor.", + href: "https://markdown-publisher.svc.plus", + icon: PenSquare, + external: true, + }, + { + key: "wechat-to-markdown", + name: isChinese ? "微信转 Markdown" : "WeChat to Markdown", + description: isChinese + ? "一键将公众号内容转换为 Markdown。" + : "Convert WeChat articles into Markdown.", + href: "https://wechat-to-markdown.svc.plus", + icon: MessageSquare, + external: true, + }, + { + key: "page-reading", + name: isChinese ? "Page Reading Agent" : "Page Reading Agent", + description: isChinese + ? "智能网页阅读与分析服务。" + : "Intelligent web page reading and analysis service.", + href: "https://page-reading.svc.plus", + icon: FileText, + external: true, + }, + { + key: "artifact", + name: isChinese ? "制品 / 镜像" : "Artifact / Mirror", + description: isChinese + ? "获取核心制品、镜像与下载资源。" + : "Get core artifacts, mirrors, and downloads.", + href: "/download", + icon: Package, + }, + { + key: "cloudIac", + name: isChinese ? "云 IaC 目录" : "Cloud IaC Catalog", + description: isChinese + ? "浏览云基础设施目录与自动化蓝图。" + : "Browse cloud IaC catalog and automation blueprints.", + href: "/cloud_iac", + icon: Layers, + }, + { + key: "insight", + name: isChinese ? "Insight 工作台" : "Insight Workbench", + description: isChinese + ? "进入观测、告警与智能协作控制面。" + : "Observability, alerts, and AI-assisted operations.", + href: "/insight", + icon: Activity, + }, + { + key: "docs", + name: isChinese ? "文档 / 解决方案" : "Docs / Solutions", + description: isChinese + ? "阅读文档、方案与产品指南。" + : "Read documentation, solutions, and guides.", + href: "/docs", + icon: BookOpen, + }, + { + key: "moltbot", + name: isChinese ? "Moltbot 服务" : "Moltbot Service", + description: isChinese + ? "Moltbot 节点管理服务。" + : "Moltbot node management service.", + href: "https://clawdbot.svc.plus/", + icon: ClawdbotLogo, + external: true, + }, + ]; if (!isHydrated) { - return null + return null; } - if (view === 'material') { + if (view === "material") { return (
      -

      Service Overview

      +

      + Service Overview +

      - Real-time metrics and system health for your current production environment. + Real-time metrics and system health for your current production + environment.

      - +
      - ) + ); } return ( @@ -155,25 +309,31 @@ export default function ServicesPage() { aria-hidden />
      - +

      - {isChinese ? '更多服务' : 'More services'} + {isChinese ? "更多服务" : "More services"}

      - {isChinese ? '在这里进入所有扩展入口' : 'Access every extended entry point'} + {isChinese + ? "在这里进入所有扩展入口" + : "Access every extended entry point"}

      {isChinese - ? '沿用当前主页布局的卡片网格,把新增工具与服务统一收拢。' - : 'A card grid aligned with the current homepage layout for all services.'} + ? "沿用当前主页布局的卡片网格,把新增工具与服务统一收拢。" + : "A card grid aligned with the current homepage layout for all services."}

      - +
      - ) + ); } diff --git a/src/components/AppSidebarContent.tsx b/src/components/AppSidebarContent.tsx new file mode 100644 index 0000000..37f34ce --- /dev/null +++ b/src/components/AppSidebarContent.tsx @@ -0,0 +1,70 @@ +'use client' + +import React from 'react' +import Link from 'next/link' +import { usePathname } from 'next/navigation' +import { + Terminal, + LayoutDashboard, + Rocket, + Database, + Key, + History, + Settings, + Plus, +} from 'lucide-react' +import { SidebarHeader, SidebarContent, SidebarFooter } from './layout/SidebarRoot' + +const navItems = [ + { href: '/panel', label: 'Console', icon: LayoutDashboard }, + { href: '/deployments', label: 'Deployments', icon: Rocket }, + { href: '/resources', label: 'Resources', icon: Database }, + { href: '/api-keys', label: 'API Keys', icon: Key }, + { href: '/logs', label: 'Logs', icon: History }, + { href: '/settings', label: 'Settings', icon: Settings }, +] + +export function AppSidebarContent() { + const pathname = usePathname() + + return ( + <> + +
      +
      + +
      +
      +

      console.svc.plus

      +

      Eye-Care Mode

      +
      +
      +
      + + + + + + + + + + ) +} diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx index 6f14e24..a7432b7 100644 --- a/src/components/Sidebar.tsx +++ b/src/components/Sidebar.tsx @@ -1,66 +1,14 @@ 'use client' -import Link from 'next/link' -import { usePathname } from 'next/navigation' -import { - Terminal, - LayoutDashboard, - Rocket, - Database, - Key, - History, - Settings, - Plus, -} from 'lucide-react' - -const navItems = [ - { href: '/dashboard', label: 'Dashboard', icon: LayoutDashboard }, - { href: '/deployments', label: 'Deployments', icon: Rocket }, - { href: '/resources', label: 'Resources', icon: Database }, - { href: '/api-keys', label: 'API Keys', icon: Key }, - { href: '/logs', label: 'Logs', icon: History }, - { href: '/settings', label: 'Settings', icon: Settings }, -] +import React from 'react' +import { SidebarRoot } from './layout/SidebarRoot' +import { AppSidebarContent } from './AppSidebarContent' export function Sidebar() { - const pathname = usePathname() - return ( - + + + ) } diff --git a/src/components/UnifiedNavigation.tsx b/src/components/UnifiedNavigation.tsx new file mode 100644 index 0000000..b11fa88 --- /dev/null +++ b/src/components/UnifiedNavigation.tsx @@ -0,0 +1,488 @@ +"use client"; +import Link from "next/link"; +import Image from "next/image"; +import { usePathname } from "next/navigation"; +import { useEffect, useRef, useState } from "react"; +import { useLanguage } from "../i18n/LanguageProvider"; +import { Menu, X, Sun, Moon, Monitor, Plus } from "lucide-react"; +import { translations } from "../i18n/translations"; +import LanguageToggle from "./LanguageToggle"; +import { AskAIButton } from "./AskAIButton"; +import ReleaseChannelSelector from "./ReleaseChannelSelector"; +import { useUserStore } from "@lib/userStore"; +import { + createNavConfig, + filterNavItems, + type NavItem, + type ReleaseChannel, + DEFAULT_CHANNELS, + RELEASE_CHANNEL_STORAGE_KEY, + CHANNEL_ORDER, +} from "@lib/navigation"; + +const getLabel = ( + label: string | ((lang: string) => string), + lang: string, +): string => { + return typeof label === "function" ? label(lang) : label; +}; + +export default function UnifiedNavigation() { + const pathname = usePathname(); + const [menuOpen, setMenuOpen] = useState(false); + const [selectedChannels, setSelectedChannels] = useState([ + "stable", + ]); + const navRef = useRef(null); + const { language } = useLanguage(); + const user = useUserStore((state) => state.user); + const nav = translations[language].nav; + const accountCopy = nav.account; + const accountInitial = + user?.username?.charAt(0)?.toUpperCase() ?? + user?.email?.charAt(0)?.toUpperCase() ?? + "?"; + const [accountMenuOpen, setAccountMenuOpen] = useState(false); + const accountMenuRef = useRef(null); + const isChinese = language === "zh"; + + useEffect(() => { + if (typeof window === "undefined") return; + + const stored = window.localStorage.getItem(RELEASE_CHANNEL_STORAGE_KEY); + if (!stored) return; + + try { + const parsed = JSON.parse(stored) as unknown; + if (!Array.isArray(parsed)) return; + + const normalized = CHANNEL_ORDER.filter((channel) => + parsed.includes(channel), + ); + if (normalized.length === 0) return; + + const restored: ReleaseChannel[] = normalized.includes("stable") + ? normalized + : [...DEFAULT_CHANNELS, ...normalized]; + setSelectedChannels((current) => { + if ( + current.length === restored.length && + current.every((value, index) => value === restored[index]) + ) { + return current; + } + return restored; + }); + } catch (error) { + console.warn("Failed to restore release channels selection", error); + } + }, []); + + useEffect(() => { + if (typeof window === "undefined") return; + window.localStorage.setItem( + RELEASE_CHANNEL_STORAGE_KEY, + JSON.stringify(selectedChannels), + ); + }, [selectedChannels]); + + useEffect(() => { + if (!accountMenuOpen) { + return; + } + + const handleClickOutside = (event: MouseEvent) => { + if ( + accountMenuRef.current && + !accountMenuRef.current.contains(event.target as Node) + ) { + setAccountMenuOpen(false); + } + }; + + document.addEventListener("mousedown", handleClickOutside); + return () => { + document.removeEventListener("mousedown", handleClickOutside); + }; + }, [accountMenuOpen]); + + useEffect(() => { + setAccountMenuOpen(false); + }, [user]); + + useEffect(() => { + if (typeof window === "undefined") { + return; + } + + const element = navRef.current; + if (!element) { + return; + } + + const updateOffset = () => { + const height = element.getBoundingClientRect().height; + document.documentElement.style.setProperty( + "--app-shell-nav-offset", + `${height}px`, + ); + }; + + updateOffset(); + + const resizeObserver = new ResizeObserver(() => { + updateOffset(); + }); + + resizeObserver.observe(element); + window.addEventListener("resize", updateOffset); + + return () => { + window.removeEventListener("resize", updateOffset); + resizeObserver.disconnect(); + }; + }, []); + + const toggleChannel = (channel: ReleaseChannel) => { + if (channel === "stable") return; + setSelectedChannels((prev) => + prev.includes(channel) + ? prev.filter((value) => value !== channel) + : [...prev, channel], + ); + }; + + const { mainNav, secondaryNav, accountNav } = createNavConfig( + language, + !!user, + !!user?.isAdmin, + !!user?.isOperator, + ); + + const filteredMainNav = filterNavItems(mainNav, user); + const filteredSecondaryNav = filterNavItems(secondaryNav, user); + + const isHiddenRoute = pathname + ? [ + "/login", + "/register", + "/xstream", + "/xcloudflow", + "/xscopehub", + "/blogs", + ].some((prefix) => pathname.startsWith(prefix)) + : false; + + if (isHiddenRoute) { + return null; + } + + const isActive = (item: NavItem): boolean => { + if (item.active) { + return item.active(pathname || ""); + } + return pathname === item.href; + }; + + return ( + <> + + +
      + +
      + + ); +} diff --git a/src/components/home/HomeSidebarContent.tsx b/src/components/home/HomeSidebarContent.tsx new file mode 100644 index 0000000..0125f04 --- /dev/null +++ b/src/components/home/HomeSidebarContent.tsx @@ -0,0 +1,111 @@ +'use client' + +import React from 'react' +import { useLanguage } from '../../i18n/LanguageProvider' +import { SidebarHeader, SidebarContent } from '../layout/SidebarRoot' + +const sidebarContent = { + zh: { + title: '文档与社区', + sections: [ + { + slug: 'docs', + title: '文档与教程', + items: [ + { label: '产品文档', description: '按模块拆分的接口与操作指南。', href: '#' }, + { label: '快速入门', description: '5 分钟完成首个项目的配置与发布。', href: '#' }, + { label: '操作指南', description: '常见任务的分步操作示例。', href: '#' }, + ], + }, + { + slug: 'practices', + title: '最佳实践', + items: [ + { label: 'GitOps 标准化', description: '流水线模板、审批与回滚策略。', href: '#' }, + { label: 'IaC 资产管理', description: 'Terraform 与 Pulumi 资产治理指引。', href: '#' }, + { label: '多云治理手册', description: '跨区域合规、网络隔离与零信任。', href: '#' }, + ], + }, + { + slug: 'community', + title: '社区与支持', + items: [ + { label: '发布说明', description: '版本更新、缺陷修复与补丁计划。', href: '#' }, + { label: '社区日程', description: 'Workshop、Live Demo 与用户群活动。', href: '#' }, + { label: '加入讨论', description: '提交 Issue 或加入 Slack/微信群。', href: '#' }, + ], + }, + ], + }, + en: { + title: 'Documentation & Community', + sections: [ + { + slug: 'docs', + title: 'Docs & Tutorials', + items: [ + { label: 'Product Docs', description: 'Module-specific API and operations guides.', href: '#' }, + { label: 'Quickstart', description: 'Configure and ship your first project in minutes.', href: '#' }, + { label: 'How-to Guides', description: 'Step-by-step recipes for recurring tasks.', href: '#' }, + ], + }, + { + slug: 'practices', + title: 'Best Practices', + items: [ + { label: 'GitOps Playbooks', description: 'Pipeline templates, approvals, and rollbacks.', href: '#' }, + { label: 'IaC Governance', description: 'Guidance for Terraform and Pulumi asset control.', href: '#' }, + { label: 'Multi-Cloud Handbook', description: 'Compliance, network isolation, and Zero Trust.', href: '#' }, + ], + }, + { + slug: 'community', + title: 'Community & Support', + items: [ + { label: 'Release Notes', description: 'Updates, fixes, and patch availability.', href: '#' }, + { label: 'Community Calendar', description: 'Workshops, live demos, and user groups.', href: '#' }, + { label: 'Join the Conversation', description: 'File issues or join Slack/WeChat.', href: '#' }, + ], + }, + ], + }, +} + +export function HomeSidebarContent() { + const { language } = useLanguage() + const data = sidebarContent[language as keyof typeof sidebarContent] + + return ( + <> + +

      {data.title}

      +

      + {language === 'zh' + ? '查阅文档、最佳实践与社区讨论,保持交付与治理同频。' + : 'Stay aligned with docs, practices, and community conversations.'} +

      +
      + + + {data.sections.map((section) => ( +
      +

      {section.title}

      +
        + {section.items.map((item) => ( +
      • + + {item.label} + + {item.description &&

        {item.description}

        } +
      • + ))} +
      +
      + ))} +
      + + ) +} diff --git a/src/components/home/Sidebar.tsx b/src/components/home/Sidebar.tsx index 2330200..62e8efd 100644 --- a/src/components/home/Sidebar.tsx +++ b/src/components/home/Sidebar.tsx @@ -1,107 +1,13 @@ 'use client' -import { useLanguage } from '../../i18n/LanguageProvider' -const sidebarContent = { - zh: { - title: '文档与社区', - sections: [ - { - slug: 'docs', - title: '文档与教程', - items: [ - { label: '产品文档', description: '按模块拆分的接口与操作指南。', href: '#' }, - { label: '快速入门', description: '5 分钟完成首个项目的配置与发布。', href: '#' }, - { label: '操作指南', description: '常见任务的分步操作示例。', href: '#' }, - ], - }, - { - slug: 'practices', - title: '最佳实践', - items: [ - { label: 'GitOps 标准化', description: '流水线模板、审批与回滚策略。', href: '#' }, - { label: 'IaC 资产管理', description: 'Terraform 与 Pulumi 资产治理指引。', href: '#' }, - { label: '多云治理手册', description: '跨区域合规、网络隔离与零信任。', href: '#' }, - ], - }, - { - slug: 'community', - title: '社区与支持', - items: [ - { label: '发布说明', description: '版本更新、缺陷修复与补丁计划。', href: '#' }, - { label: '社区日程', description: 'Workshop、Live Demo 与用户群活动。', href: '#' }, - { label: '加入讨论', description: '提交 Issue 或加入 Slack/微信群。', href: '#' }, - ], - }, - ], - }, - en: { - title: 'Documentation & Community', - sections: [ - { - slug: 'docs', - title: 'Docs & Tutorials', - items: [ - { label: 'Product Docs', description: 'Module-specific API and operations guides.', href: '#' }, - { label: 'Quickstart', description: 'Configure and ship your first project in minutes.', href: '#' }, - { label: 'How-to Guides', description: 'Step-by-step recipes for recurring tasks.', href: '#' }, - ], - }, - { - slug: 'practices', - title: 'Best Practices', - items: [ - { label: 'GitOps Playbooks', description: 'Pipeline templates, approvals, and rollbacks.', href: '#' }, - { label: 'IaC Governance', description: 'Guidance for Terraform and Pulumi asset control.', href: '#' }, - { label: 'Multi-Cloud Handbook', description: 'Compliance, network isolation, and Zero Trust.', href: '#' }, - ], - }, - { - slug: 'community', - title: 'Community & Support', - items: [ - { label: 'Release Notes', description: 'Updates, fixes, and patch availability.', href: '#' }, - { label: 'Community Calendar', description: 'Workshops, live demos, and user groups.', href: '#' }, - { label: 'Join the Conversation', description: 'File issues or join Slack/WeChat.', href: '#' }, - ], - }, - ], - }, -} +import React from 'react' +import { SidebarRoot } from '../layout/SidebarRoot' +import { HomeSidebarContent } from './HomeSidebarContent' export default function Sidebar() { - const { language } = useLanguage() - const data = sidebarContent[language] - return ( - + + + ) } diff --git a/src/components/layout/SidebarRoot.tsx b/src/components/layout/SidebarRoot.tsx new file mode 100644 index 0000000..415bb78 --- /dev/null +++ b/src/components/layout/SidebarRoot.tsx @@ -0,0 +1,47 @@ +'use client' + +import React from 'react' +import { cn } from '@/lib/utils' + +interface SidebarRootProps { + children: React.ReactNode + className?: string +} + +/** + * SidebarRoot - The base skeleton for all sidebars. + * Provides the container and common layout behavior. + */ +export function SidebarRoot({ children, className }: SidebarRootProps) { + return ( + + ) +} + +/** + * SidebarHeader - Top section of the sidebar (e.g., Branding, Logo). + */ +export function SidebarHeader({ children, className }: { children: React.ReactNode; className?: string }) { + return
      {children}
      +} + +/** + * SidebarContent - Middle scrollable section of the sidebar. + */ +export function SidebarContent({ children, className }: { children: React.ReactNode; className?: string }) { + return
      {children}
      +} + +/** + * SidebarFooter - Bottom fixed section of the sidebar (e.g., User, Settings, Call to Action). + */ +export function SidebarFooter({ children, className }: { children: React.ReactNode; className?: string }) { + return
      {children}
      +} diff --git a/src/lib/navigation.ts b/src/lib/navigation.ts new file mode 100644 index 0000000..503b168 --- /dev/null +++ b/src/lib/navigation.ts @@ -0,0 +1,181 @@ +"use client"; + +import type { LucideIcon } from "lucide-react"; +import { + MessageSquare, + BarChart2, + Link as LinkIcon, + Server, + LayoutDashboard, + Rocket, + Database, + Key, + History, + Settings, + Plus, + Home, + FileText, + Book, + Info, +} from "lucide-react"; + +export type ReleaseChannel = "stable" | "beta" | "develop"; + +export type NavItem = { + key: string; + label: string | ((lang: string) => string); + href: string; + icon?: LucideIcon | ((props: any) => React.ReactNode); + active?: (pathname: string) => boolean; + channels?: ReleaseChannel[]; + enabled?: boolean; + badge?: string; + children?: NavItem[]; + showOn?: "desktop" | "mobile" | "both"; + requireAuth?: boolean; + requireAdmin?: boolean; + requireOperator?: boolean; +}; + +export const CHANNEL_ORDER: ReleaseChannel[] = ["stable", "beta", "develop"]; +export const DEFAULT_CHANNELS: ReleaseChannel[] = ["stable"]; +export const RELEASE_CHANNEL_STORAGE_KEY = "cloudnative-suite.releaseChannels"; + +export const getLabel = ( + label: string | ((lang: string) => string), + lang: string, +): string => { + return typeof label === "function" ? label(lang) : label; +}; + +export const createNavConfig = ( + language: string, + isLoggedIn: boolean, + isAdmin: boolean, + isOperator: boolean, +): { + mainNav: NavItem[]; + secondaryNav: NavItem[]; + accountNav: NavItem[]; +} => { + const isChinese = language === "zh"; + + const mainNav: NavItem[] = [ + { + key: "home", + label: isChinese ? "首页" : "Home", + href: "/", + icon: Home, + active: (pathname) => pathname === "/", + showOn: "both", + }, + { + key: "chat", + label: (lang) => (lang === "zh" ? "AI 助手" : "AI Assistant"), + href: "/services/moltbot/chats", + icon: MessageSquare, + active: (pathname) => pathname?.startsWith("/services/moltbot"), + showOn: "both", + }, + { + key: "docs", + label: isChinese ? "文档" : "Docs", + href: "/docs", + icon: FileText, + active: (pathname) => pathname?.startsWith("/docs"), + showOn: "both", + }, + { + key: "console", + label: isChinese ? "控制台" : "Console", + href: "/panel", + icon: LayoutDashboard, + active: (pathname) => pathname.startsWith("/panel") && !pathname.startsWith("/panel/management"), + showOn: "both", + }, + { + key: "services", + label: isChinese ? "更多服务" : "More Services", + href: "/services", + icon: Plus, + active: (pathname) => pathname.startsWith("/services") && !pathname.startsWith("/services/moltbot"), + showOn: "both", + }, + { + key: "about", + label: isChinese ? "关于" : "About", + href: "/about", + icon: Info, + active: (pathname) => pathname === "/about", + showOn: "both", + }, + ]; + + const secondaryNav: NavItem[] = [ + { + key: "management", + label: isChinese ? "实例管理" : "Instances", + href: "/panel/management", + icon: Server, + active: (pathname) => pathname === "/panel/management", + showOn: "both", + requireAdmin: true, + requireOperator: true, + }, + ]; + + const accountNav: NavItem[] = isLoggedIn + ? [ + { + key: "userCenter", + label: isChinese ? "用户中心" : "User Center", + href: "/panel", + icon: BarChart2, + showOn: "both", + }, + ...(isAdmin || isOperator + ? [ + { + key: "management", + label: isChinese ? "管理" : "Management", + href: "/panel/management", + icon: Settings, + showOn: "both" as const, + }, + ] + : []), + { + key: "logout", + label: isChinese ? "退出登录" : "Logout", + href: "/logout", + showOn: "both", + badge: isChinese ? "退出" : "Logout", + }, + ] + : [ + { + key: "register", + label: isChinese ? "注册" : "Register", + href: "/register", + showOn: "both", + }, + { + key: "login", + label: isChinese ? "登录" : "Login", + href: "/login", + showOn: "both", + }, + ]; + + return { mainNav, secondaryNav, accountNav }; +}; + +export const filterNavItems = (items: NavItem[], user: any): NavItem[] => { + return items.filter((item) => { + if (item.requireAuth && !user) return false; + if (item.requireAdmin && !user?.isAdmin) return false; + if (item.requireOperator && !user?.isOperator) return false; + if (item.enabled === false) return false; + return true; + }); +}; diff --git a/src/lib/utils.ts b/src/lib/utils.ts new file mode 100644 index 0000000..ccd54c7 --- /dev/null +++ b/src/lib/utils.ts @@ -0,0 +1,6 @@ +import { type ClassValue, clsx } from 'clsx' +import { twMerge } from 'tailwind-merge' + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)) +}