Refine homepage navbar layout (#529)

This commit is contained in:
shenlan 2025-10-16 16:15:24 +08:00 committed by GitHub
parent 8360e1e474
commit 627ade65c6
2 changed files with 427 additions and 328 deletions

View File

@ -1,6 +1,6 @@
'use client'
import { useEffect, useRef, useState } from 'react'
import { useCallback, useEffect, useRef, useState } from 'react'
import { marked } from 'marked'
import DOMPurify from 'dompurify'
import { ChatBubble } from './ChatBubble'
@ -9,14 +9,35 @@ import { SourceHint } from './SourceHint'
const MAX_MESSAGES = 20
const MAX_CACHE_SIZE = 50
function normalizeInput(text: string) {
return text
.trim()
.replace(/[\s.,!?;:]+$/u, '')
.replace(/```[\s\S]*?```/g, '')
}
function renderMarkdown(text: string) {
// marked.parse has a return type of string | Promise<string>
// but in our usage it executes synchronously. Cast to string to
// satisfy the DOMPurify.sanitize type expectations.
return DOMPurify.sanitize(marked.parse(text) as string)
}
type InitialQuestionPayload = {
key: number
text: string
}
export function AskAIDialog({
open,
onMinimize,
onEnd
onEnd,
initialQuestion
}: {
open: boolean
onMinimize: () => void
onEnd: () => void
initialQuestion?: InitialQuestionPayload
}) {
const [question, setQuestion] = useState('')
const [messages, setMessages] = useState<{ sender: 'user' | 'ai'; text: string }[]>([])
@ -27,6 +48,7 @@ export function AskAIDialog({
new Map<string, { answer: string; sources: any[]; timestamp: number }>()
)
const requestIdRef = useRef(0)
const processedInitialRef = useRef<number | null>(null)
useEffect(() => {
return () => {
@ -35,26 +57,12 @@ export function AskAIDialog({
}
}, [])
function normalizeInput(text: string) {
return text
.trim()
.replace(/[\s.,!?;:]+$/u, '')
.replace(/```[\s\S]*?```/g, '')
}
function renderMarkdown(text: string) {
// marked.parse has a return type of string | Promise<string>
// but in our usage it executes synchronously. Cast to string to
// satisfy the DOMPurify.sanitize type expectations.
return DOMPurify.sanitize(marked.parse(text) as string)
}
async function streamChat(
const streamChat = useCallback(async (
url: string,
body: any,
update: (text: string, src?: any[]) => void,
timeout = 15000
) {
) => {
abortRef.current?.abort()
const controller = new AbortController()
let timer: NodeJS.Timeout | null = null
@ -130,10 +138,11 @@ export function AskAIDialog({
if (timer) clearTimeout(timer)
if (abortRef.current === controller) abortRef.current = null
}
}
}, [])
async function performAsk() {
const normalized = normalizeInput(question)
const performAsk = useCallback(async (override?: string) => {
const inputValue = override ?? question
const normalized = normalizeInput(inputValue)
if (!normalized) return
const now = Date.now()
const cached = cacheRef.current.get(normalized)
@ -262,21 +271,36 @@ export function AskAIDialog({
else if (err.message) message = err.message
updateAI(message)
}
}
}, [messages, question, streamChat])
function handleAsk() {
abortRef.current?.abort()
if (debounceRef.current) clearTimeout(debounceRef.current)
debounceRef.current = setTimeout(performAsk, 300)
debounceRef.current = setTimeout(() => performAsk(), 300)
}
function handleEnd() {
setMessages([])
setQuestion('')
setSources([])
processedInitialRef.current = null
onEnd()
}
useEffect(() => {
if (!open) {
processedInitialRef.current = null
return
}
if (!initialQuestion) return
if (processedInitialRef.current === initialQuestion.key) return
const normalizedInitial = normalizeInput(initialQuestion.text)
if (!normalizedInitial) return
processedInitialRef.current = initialQuestion.key
setQuestion(normalizedInitial)
performAsk(normalizedInitial)
}, [initialQuestion, open, performAsk])
if (!open) return null
return (

View File

@ -1,12 +1,15 @@
'use client'
import Link from 'next/link'
import Image from 'next/image'
import { useEffect, useMemo, useRef, useState } from 'react'
import { FormEvent, useEffect, useMemo, useRef, useState } from 'react'
import { Search } from 'lucide-react'
import { useLanguage } from '../i18n/LanguageProvider'
import { translations } from '../i18n/translations'
import LanguageToggle from './LanguageToggle'
import ReleaseChannelSelector, { ReleaseChannel } from './ReleaseChannelSelector'
import { getFeatureToggleInfo } from '@lib/featureToggles'
import { useUser } from '@lib/userStore'
import { AskAIDialog } from './AskAIDialog'
const CHANNEL_ORDER: ReleaseChannel[] = ['stable', 'beta', 'develop']
const DEFAULT_CHANNELS: ReleaseChannel[] = ['stable']
@ -21,17 +24,9 @@ type NavSubItem = {
enabled?: boolean
}
type NavItem = {
key: string
label: string
children: NavSubItem[]
}
export default function Navbar() {
const [menuOpen, setMenuOpen] = useState(false)
const [openSection, setOpenSection] = useState<string | null>(null)
const [activeMenu, setActiveMenu] = useState<string | null>(null)
const [activeItem, setActiveItem] = useState<string | null>(null)
const [mobileServicesOpen, setMobileServicesOpen] = useState(false)
const [selectedChannels, setSelectedChannels] = useState<ReleaseChannel[]>(['stable'])
const { language } = useLanguage()
const { user } = useUser()
@ -145,100 +140,61 @@ export default function Navbar() {
const accountLabel = nav.account.title
const navItems: NavItem[] = [
{
key: 'openSource',
label: nav.openSource.title,
children: [
{
key: 'features',
label: nav.openSource.features,
href: '#features',
},
{
key: 'projects',
label: nav.openSource.projects,
href: '#open-sources',
},
{
key: 'download',
label: nav.openSource.download,
href: '#download',
},
],
},
{
key: 'services',
label: nav.services.title,
children: [
{
key: 'artifact',
label: nav.services.artifact,
href: '/download',
togglePath: '/download',
},
{
key: 'cloudIac',
label: nav.services.cloudIac,
href: '/cloud_iac',
togglePath: '/cloud_iac',
},
{
key: 'insight',
label: nav.services.insight,
href: '/insight',
togglePath: '/insight',
},
{
key: 'docs',
label: nav.services.docs,
href: '/docs',
togglePath: '/docs',
},
],
},
...(!user
? [
{
key: 'account',
label: accountLabel,
children: accountChildren,
},
]
: []),
]
const serviceItems: NavSubItem[] = useMemo(() => {
const rawItems: NavSubItem[] = [
{
key: 'artifact',
label: nav.services.artifact,
href: '/download',
togglePath: '/download',
},
{
key: 'cloudIac',
label: nav.services.cloudIac,
href: '/cloud_iac',
togglePath: '/cloud_iac',
},
{
key: 'insight',
label: nav.services.insight,
href: '/insight',
togglePath: '/insight',
},
{
key: 'docs',
label: nav.services.docs,
href: '/docs',
togglePath: '/docs',
},
]
const visibleNavItems: NavItem[] = navItems
.map((item) => ({
...item,
children: item.children
.map((child) => {
if (!child.togglePath) {
return { ...child, enabled: true }
}
return rawItems
.map((child) => {
if (!child.togglePath) {
return { ...child, enabled: true }
}
const { enabled, channel } = getFeatureToggleInfo('globalNavigation', child.togglePath)
const derivedChannels = child.channels ?? (channel ? [channel] : undefined)
const { enabled, channel } = getFeatureToggleInfo('globalNavigation', child.togglePath)
const derivedChannels = child.channels ?? (channel ? [channel] : undefined)
return {
...child,
enabled,
channels: derivedChannels,
}
})
.filter((child) => {
if (child.enabled === false) {
return false
}
return {
...child,
enabled,
channels: derivedChannels,
}
})
.filter((child) => {
if (child.enabled === false) {
return false
}
const childChannels: ReleaseChannel[] = child.channels?.length
? child.channels
: DEFAULT_CHANNELS
return childChannels.some((channel) => selectedChannelSet.has(channel))
})
.map(({ enabled: _enabled, ...child }) => child),
}))
.filter((item) => item.children.length > 0)
const childChannels: ReleaseChannel[] = child.channels?.length
? child.channels
: DEFAULT_CHANNELS
return childChannels.some((channel) => selectedChannelSet.has(channel))
})
.map(({ enabled: _enabled, ...child }) => child)
}, [nav.services.artifact, nav.services.cloudIac, nav.services.docs, nav.services.insight, selectedChannelSet])
const toggleChannel = (channel: ReleaseChannel) => {
if (channel === 'stable') return
@ -264,227 +220,346 @@ export default function Navbar() {
)
}
const toggleSection = (section: string) => {
setOpenSection((prev) => (prev === section ? null : section))
const isChinese = language === 'zh'
const labels = {
home: isChinese ? '首页' : 'Home',
docs: isChinese ? '文档' : 'Docs',
download: isChinese ? '下载' : 'Download',
moreServices: isChinese ? '更多服务' : 'More services',
searchPlaceholder: isChinese ? '请输入关键字搜索内容' : 'Ask anything about your docs',
}
const [searchValue, setSearchValue] = useState('')
const [askDialogOpen, setAskDialogOpen] = useState(false)
const [pendingQuestion, setPendingQuestion] = useState<{ key: number; text: string } | null>(null)
const mainLinks = [
{ key: 'home', label: labels.home, href: '/' },
{ key: 'docs', label: labels.docs, href: '/docs' },
{ key: 'download', label: labels.download, href: '/download' },
]
const handleSearchSubmit = (event: FormEvent<HTMLFormElement>) => {
event.preventDefault()
const trimmed = searchValue.trim()
if (!trimmed) return
setPendingQuestion({ key: Date.now(), text: trimmed })
setAskDialogOpen(true)
setSearchValue('')
}
return (
<nav className="fixed top-0 w-full z-50 bg-white/70 backdrop-blur border-b border-gray-200">
<div className="max-w-7xl mx-auto px-4 py-4 flex justify-between items-center">
<a href="#" className="text-xl font-bold text-gray-900 flex items-center gap-2">
<Image
src="/icons/cloudnative_32.png"
alt="logo"
width={24}
height={24}
className="h-6 w-6"
unoptimized
/>
CloudNative Suite
</a>
<>
<nav className="fixed top-0 z-50 w-full border-b border-gray-200 bg-white/80 backdrop-blur">
<div className="mx-auto flex max-w-7xl flex-col px-4">
<div className="flex items-center gap-6 py-4">
<div className="flex flex-1 items-center gap-8">
<Link href="/" className="flex items-center gap-2 text-xl font-semibold text-gray-900">
<Image
src="/icons/cloudnative_32.png"
alt="logo"
width={24}
height={24}
className="h-6 w-6"
unoptimized
/>
CloudNative Suite
</Link>
<div className="hidden lg:flex items-center gap-6 text-sm font-medium text-gray-700">
{mainLinks.map((link) => (
<Link key={link.key} href={link.href} className="transition hover:text-purple-600">
{link.label}
</Link>
))}
{serviceItems.length > 0 ? (
<div className="group relative">
<button className="flex items-center gap-1 transition hover:text-purple-600">
<span>{labels.moreServices}</span>
<svg
className="h-4 w-4 text-gray-500 transition group-hover:text-purple-600"
fill="none"
stroke="currentColor"
strokeWidth={2}
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path strokeLinecap="round" strokeLinejoin="round" d="M19 9l-7 7-7-7" />
</svg>
</button>
<div className="pointer-events-none absolute left-0 top-full hidden min-w-[200px] translate-y-1 rounded-lg border border-gray-200 bg-white py-2 text-sm text-gray-700 opacity-0 shadow-lg transition-all duration-200 group-hover:pointer-events-auto group-hover:block group-hover:translate-y-2 group-hover:opacity-100">
{serviceItems.map((child) => {
const isExternal = child.href.startsWith('http')
if (isExternal) {
return (
<a
key={child.key}
href={child.href}
className="flex items-center justify-between gap-2 px-4 py-2 transition hover:bg-gray-100 hover:text-purple-600"
target="_blank"
rel="noopener noreferrer"
>
<span>{child.label}</span>
{getPreviewBadge(child.channels)}
</a>
)
}
{/* Desktop Navigation */}
<div className="hidden md:flex items-center gap-6 text-sm text-gray-900">
{visibleNavItems.map((item) => {
const dropdownPosition = item.key === 'account' ? 'right-0' : 'left-0'
return (
<div key={item.key} className="relative group">
<button
className={`hover:text-purple-600 ${
activeMenu === item.key ? 'text-purple-600' : ''
}`}
>
{item.label}
</button>
<div
className={`absolute ${dropdownPosition} top-full pt-2 opacity-0 translate-y-1 transform transition-all duration-200 group-hover:opacity-100 group-hover:translate-y-0 pointer-events-none group-hover:pointer-events-auto`}
>
<div className="bg-white rounded-md shadow-lg border border-gray-200 py-2">
{item.children.map((child) => {
const isExternal = child.href.startsWith('http')
return (
<a
key={child.key}
href={child.href}
className={`block px-4 py-2 hover:bg-gray-100 whitespace-nowrap ${
activeItem === child.key ? 'text-purple-600' : ''
}`}
onClick={() => {
setActiveMenu(item.key)
setActiveItem(child.key)
}}
target={isExternal ? '_blank' : undefined}
rel={isExternal ? 'noopener noreferrer' : undefined}
>
<span className="flex items-center gap-2">
return (
<Link
key={child.key}
href={child.href}
className="flex items-center justify-between gap-2 px-4 py-2 transition hover:bg-gray-100 hover:text-purple-600"
>
<span>{child.label}</span>
{getPreviewBadge(child.channels)}
</span>
</a>
)
})}
</Link>
)
})}
</div>
</div>
</div>
) : null}
</div>
)
})}
{user ? (
<div className="relative" ref={accountMenuRef}>
<button
type="button"
onClick={() => setAccountMenuOpen((prev) => !prev)}
className="flex h-10 w-10 items-center justify-center rounded-full bg-purple-600 text-sm font-semibold text-white shadow-sm transition hover:bg-purple-500 focus:outline-none focus:ring-2 focus:ring-purple-200 focus:ring-offset-2"
aria-haspopup="menu"
aria-expanded={accountMenuOpen}
>
{accountInitial}
</button>
{accountMenuOpen ? (
<div className="absolute right-0 mt-2 w-56 overflow-hidden rounded-xl border border-gray-200 bg-white shadow-lg">
<div className="border-b border-gray-100 bg-purple-50/60 px-4 py-3">
<p className="text-sm font-semibold text-gray-900">{user.username}</p>
<p className="text-xs text-gray-500">{user.email}</p>
</div>
<div className="py-1 text-sm text-gray-700">
<a
href="/panel"
className="block px-4 py-2 hover:bg-gray-100"
onClick={() => setAccountMenuOpen(false)}
>
{accountCopy.userCenter}
</a>
<a
href="/logout"
className="flex w-full items-center px-4 py-2 text-left text-red-600 hover:bg-red-50"
onClick={() => setAccountMenuOpen(false)}
>
{accountCopy.logout}
</a>
</div>
</div>
) : null}
</div>
) : null}
<div className="flex items-center gap-3">
<LanguageToggle />
<ReleaseChannelSelector
selected={selectedChannels}
onToggle={toggleChannel}
variant="icon"
/>
</div>
</div>
{/* Mobile Hamburger Button */}
<button
className="md:hidden flex items-center text-gray-900 focus:outline-none"
onClick={() => setMenuOpen(!menuOpen)}
aria-label="Toggle menu"
>
<svg
className="w-6 h-6"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
{menuOpen ? (
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
) : (
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" />
)}
</svg>
</button>
</div>
{/* Mobile Menu */}
{menuOpen && (
<div className="md:hidden bg-white/95 backdrop-blur border-t border-gray-200 px-4 pb-4">
{visibleNavItems.map((item) => (
<div key={item.key}>
<button
onClick={() => toggleSection(item.key)}
className={`w-full flex justify-between items-center py-2 text-gray-900 ${
openSection === item.key || activeMenu === item.key ? 'text-purple-600' : ''
}`}
>
<span>{item.label}</span>
<svg
className={`w-4 h-4 transform transition-transform ${
openSection === item.key ? 'rotate-180' : ''
}`}
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
<div className="hidden flex-1 items-center justify-end gap-4 lg:flex">
<form onSubmit={handleSearchSubmit} className="relative w-full max-w-xs">
<input
type="search"
value={searchValue}
onChange={(event) => setSearchValue(event.target.value)}
placeholder={labels.searchPlaceholder}
className="w-full rounded-full border border-gray-200 bg-gray-50 py-2 pl-4 pr-10 text-sm text-gray-900 transition focus:border-purple-500 focus:bg-white focus:outline-none focus:ring-2 focus:ring-purple-100"
/>
<button
type="submit"
className="absolute right-2 top-1/2 flex h-7 w-7 -translate-y-1/2 items-center justify-center rounded-full bg-purple-600 text-white transition hover:bg-purple-500"
aria-label="Ask AI"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
</svg>
</button>
{openSection === item.key && (
<div className="pl-4">
{item.children.map((child) => {
const isExternal = child.href.startsWith('http')
return (
<a
key={child.key}
href={child.href}
onClick={() => {
setMenuOpen(false)
setActiveMenu(item.key)
setActiveItem(child.key)
}}
className={`block py-1 text-gray-900 hover:text-purple-600 ${
activeItem === child.key ? 'text-purple-600' : ''
}`}
target={isExternal ? '_blank' : undefined}
rel={isExternal ? 'noopener noreferrer' : undefined}
>
<span className="flex items-center gap-2">
<span>{child.label}</span>
{getPreviewBadge(child.channels)}
</span>
</a>
)
})}
</div>
)}
</div>
))}
{user ? (
<div className="mt-4 rounded-xl bg-purple-50 p-4 text-purple-700">
<div className="flex items-center gap-3">
<span className="flex h-10 w-10 items-center justify-center rounded-full bg-purple-600 text-sm font-semibold text-white">
{accountInitial}
</span>
<div>
<p className="text-sm font-semibold">{user.username}</p>
<p className="text-xs text-purple-300">{user.email}</p>
<Search className="h-4 w-4" />
</button>
</form>
{user ? (
<div className="relative" ref={accountMenuRef}>
<button
type="button"
onClick={() => setAccountMenuOpen((prev) => !prev)}
className="flex h-10 w-10 items-center justify-center rounded-full bg-purple-600 text-sm font-semibold text-white shadow-sm transition hover:bg-purple-500 focus:outline-none focus:ring-2 focus:ring-purple-200 focus:ring-offset-2"
aria-haspopup="menu"
aria-expanded={accountMenuOpen}
>
{accountInitial}
</button>
{accountMenuOpen ? (
<div className="absolute right-0 mt-2 w-56 overflow-hidden rounded-xl border border-gray-200 bg-white shadow-lg">
<div className="border-b border-gray-100 bg-purple-50/60 px-4 py-3">
<p className="text-sm font-semibold text-gray-900">{user.username}</p>
<p className="text-xs text-gray-500">{user.email}</p>
</div>
<div className="py-1 text-sm text-gray-700">
<Link
href="/panel"
className="block px-4 py-2 hover:bg-gray-100"
onClick={() => setAccountMenuOpen(false)}
>
{accountCopy.userCenter}
</Link>
<Link
href="/logout"
className="flex w-full items-center px-4 py-2 text-left text-red-600 hover:bg-red-50"
onClick={() => setAccountMenuOpen(false)}
>
{accountCopy.logout}
</Link>
</div>
</div>
) : null}
</div>
) : (
<div className="flex items-center gap-3 text-sm font-medium text-gray-700">
<Link href="/login" className="transition hover:text-purple-600">
{nav.account.login}
</Link>
<span className="h-3 w-px bg-gray-300" aria-hidden="true" />
<Link
href="/register"
className="rounded-full border border-purple-100 px-4 py-1.5 text-purple-600 transition hover:border-purple-200 hover:bg-purple-50"
>
{nav.account.register}
</Link>
</div>
)}
<LanguageToggle />
<ReleaseChannelSelector
selected={selectedChannels}
onToggle={toggleChannel}
variant="icon"
/>
</div>
<button
className="flex items-center text-gray-900 focus:outline-none lg:hidden"
onClick={() => setMenuOpen((prev) => !prev)}
aria-label="Toggle menu"
>
<svg
className="h-6 w-6"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
{menuOpen ? (
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
) : (
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" />
)}
</svg>
</button>
</div>
{menuOpen ? (
<div className="flex flex-col gap-4 border-t border-gray-200 py-4 lg:hidden">
<form onSubmit={handleSearchSubmit} className="relative">
<input
type="search"
value={searchValue}
onChange={(event) => setSearchValue(event.target.value)}
placeholder={labels.searchPlaceholder}
className="w-full rounded-full border border-gray-200 bg-gray-50 py-2 pl-4 pr-10 text-sm text-gray-900 transition focus:border-purple-500 focus:bg-white focus:outline-none focus:ring-2 focus:ring-purple-100"
/>
<button
type="submit"
className="absolute right-2 top-1/2 flex h-8 w-8 -translate-y-1/2 items-center justify-center rounded-full bg-purple-600 text-white transition hover:bg-purple-500"
aria-label="Ask AI"
>
<Search className="h-4 w-4" />
</button>
</form>
<div className="flex flex-col gap-2 text-sm font-medium text-gray-700">
{mainLinks.map((link) => (
<Link key={link.key} href={link.href} className="py-2" onClick={() => setMenuOpen(false)}>
{link.label}
</Link>
))}
{serviceItems.length > 0 ? (
<div>
<button
className="flex w-full items-center justify-between py-2"
onClick={() => setMobileServicesOpen((prev) => !prev)}
>
<span>{labels.moreServices}</span>
<svg
className={`h-4 w-4 transform transition ${mobileServicesOpen ? 'rotate-180' : ''}`}
fill="none"
stroke="currentColor"
strokeWidth={2}
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path strokeLinecap="round" strokeLinejoin="round" d="M19 9l-7 7-7-7" />
</svg>
</button>
{mobileServicesOpen ? (
<div className="pl-4 text-sm text-gray-600">
{serviceItems.map((child) => {
const isExternal = child.href.startsWith('http')
if (isExternal) {
return (
<a
key={child.key}
href={child.href}
className="block py-1.5"
target="_blank"
rel="noopener noreferrer"
onClick={() => setMenuOpen(false)}
>
<span className="flex items-center gap-2">
<span>{child.label}</span>
{getPreviewBadge(child.channels)}
</span>
</a>
)
}
return (
<Link
key={child.key}
href={child.href}
className="block py-1.5"
onClick={() => setMenuOpen(false)}
>
<span className="flex items-center gap-2">
<span>{child.label}</span>
{getPreviewBadge(child.channels)}
</span>
</Link>
)
})}
</div>
) : null}
</div>
) : null}
</div>
{user ? (
<div className="rounded-xl bg-purple-50 p-4 text-purple-700">
<div className="flex items-center gap-3">
<span className="flex h-10 w-10 items-center justify-center rounded-full bg-purple-600 text-sm font-semibold text-white">
{accountInitial}
</span>
<div>
<p className="text-sm font-semibold">{user.username}</p>
<p className="text-xs text-purple-300">{user.email}</p>
</div>
</div>
<Link
href="/panel"
className="mt-3 inline-flex items-center justify-center rounded-lg bg-white/80 px-3 py-1.5 text-xs font-semibold text-purple-600 transition hover:bg-white"
onClick={() => setMenuOpen(false)}
>
{accountCopy.userCenter}
</Link>
<Link
href="/logout"
className="mt-3 inline-flex items-center justify-center rounded-lg border border-purple-200 px-3 py-1.5 text-xs font-semibold text-purple-600 transition hover:border-purple-300 hover:bg-purple-100 focus:outline-none focus:ring-2 focus:ring-purple-200 focus:ring-offset-2"
onClick={() => setMenuOpen(false)}
>
{accountCopy.logout}
</Link>
</div>
) : (
<div className="flex items-center gap-3 text-sm font-medium text-gray-700">
<Link href="/login" className="py-2" onClick={() => setMenuOpen(false)}>
{nav.account.login}
</Link>
<span className="h-3 w-px bg-gray-300" aria-hidden="true" />
<Link
href="/register"
className="rounded-full border border-purple-100 px-4 py-1.5 text-purple-600 transition hover:border-purple-200 hover:bg-purple-50"
onClick={() => setMenuOpen(false)}
>
{nav.account.register}
</Link>
</div>
)}
<div className="flex flex-col gap-2">
<ReleaseChannelSelector selected={selectedChannels} onToggle={toggleChannel} />
<LanguageToggle />
</div>
<a
href="/panel"
className="mt-3 inline-flex items-center justify-center rounded-lg bg-white/80 px-3 py-1.5 text-xs font-semibold text-purple-600 transition hover:bg-white"
onClick={() => setMenuOpen(false)}
>
{accountCopy.userCenter}
</a>
<a
href="/logout"
className="mt-3 inline-flex items-center justify-center rounded-lg border border-purple-200 px-3 py-1.5 text-xs font-semibold text-purple-600 transition hover:border-purple-300 hover:bg-purple-100 focus:outline-none focus:ring-2 focus:ring-purple-200 focus:ring-offset-2"
onClick={() => setMenuOpen(false)}
>
{accountCopy.logout}
</a>
</div>
) : null}
<div className="pt-2 flex flex-col gap-2">
<ReleaseChannelSelector selected={selectedChannels} onToggle={toggleChannel} />
<LanguageToggle />
</div>
</div>
)}
</nav>
</nav>
<AskAIDialog
open={askDialogOpen}
onMinimize={() => setAskDialogOpen(false)}
onEnd={() => {
setAskDialogOpen(false)
setPendingQuestion(null)
}}
initialQuestion={pendingQuestion ?? undefined}
/>
</>
)
}