Files
MuyueWorkspace/web/src/components/App.jsx
Augustin cd9ae5f4b9
All checks were successful
Stable Release / stable (push) Successful in 1m36s
fix(ui): restore Tests tab, fix renderMarkdown undefined error
- Restore Tests tab in navigation (was removed by mistake)
- Fix renderMarkdown ReferenceError by restoring the callback (raw=false always)
- Keep Copy MD button and removal of raw-md/collapse toggles

Assisted-by: GLM-5.1 via Crush <crush@charm.land>
2026-04-28 11:56:47 +02:00

170 lines
6.2 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useState, useEffect, useCallback, useMemo, useRef } from 'react'
import { LayoutDashboard, Sparkles, Terminal, Settings, TestTube2 } from 'lucide-react'
import api from '../api/client'
import { getTheme, applyTheme } from '../themes'
import { useI18n } from '../i18n'
import Dashboard from './Dashboard'
import Studio from './Studio'
import Shell from './Shell'
import Config from './Config'
import Tests from './Tests'
import OnboardingWizard from './OnboardingWizard'
export default function App() {
const [activeTab, setActiveTab] = useState('dash')
const [info, setInfo] = useState({})
const [clock, setClock] = useState(new Date())
const [isSudo, setIsSudo] = useState(false)
const [dashRefreshKey, setDashRefreshKey] = useState(0)
const dashRefreshRef = useRef(null)
const [config, setConfig] = useState(null)
const [showOnboarding, setShowOnboarding] = useState(false)
const { t, layout } = useI18n()
const TABS = useMemo(() => [
{ id: 'dash', label: t('tabs.dashboard'), icon: <LayoutDashboard size={15} /> },
{ id: 'studio', label: t('tabs.studio'), icon: <Sparkles size={15} /> },
{ id: 'shell', label: t('tabs.shell'), icon: <Terminal size={15} /> },
{ id: 'tests', label: 'Tests', icon: <TestTube2 size={15} /> },
{ id: 'config', label: t('tabs.config'), icon: <Settings size={15} /> },
], [t])
useEffect(() => {
api.getInfo().then(d => { setInfo(d); setIsSudo(!!d.sudo) }).catch(() => {})
api.getConfig().then(d => {
setConfig(d)
const theme = d.profile?.preferences?.theme || 'cyberpunk-red'
applyTheme(getTheme(theme))
const hasProfile = d.profile?.name || d.profile?.pseudo
if (!hasProfile) setShowOnboarding(true)
}).catch(() => {
applyTheme(getTheme('cyberpunk-red'))
setShowOnboarding(true)
})
}, [])
useEffect(() => {
const id = setInterval(() => setClock(new Date()), 1000)
return () => clearInterval(id)
}, [])
useEffect(() => {
const onKey = (e) => {
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return
if (!e.ctrlKey && !e.metaKey) return
const map = {
Digit1: 'dash',
Digit2: 'studio',
Digit3: 'shell',
Digit4: 'tests',
Digit5: 'config',
}
if (map[e.code]) {
e.preventDefault()
setActiveTab(map[e.code])
return
}
if (e.ctrlKey && e.code === 'KeyR') {
e.preventDefault()
if (dashRefreshRef.current) dashRefreshRef.current()
}
}
window.addEventListener('keydown', onKey)
return () => window.removeEventListener('keydown', onKey)
}, [])
const switchTab = useCallback((tabId) => setActiveTab(tabId), [])
useEffect(() => {
const handler = () => setActiveTab('shell')
window.addEventListener('navigate-to-shell', handler)
return () => window.removeEventListener('navigate-to-shell', handler)
}, [])
const WINDOW_SHORTCUTS = useMemo(() => ({
dash: [],
studio: [
{ keys: layout.keys.enter, desc: t('statusbar.sendMessage') },
{ keys: `${layout.keys.shift}+${layout.keys.enter}`, desc: t('statusbar.newLine') },
],
shell: [
{ keys: `${layout.keys.ctrl}+${layout.keys.shift}+C`, desc: t('statusbar.copy') },
{ keys: `${layout.keys.ctrl}+${layout.keys.shift}+V`, desc: t('statusbar.paste') },
{ keys: `${layout.keys.ctrl}+${layout.keys.shift}+F`, desc: t('statusbar.search') },
{ keys: `${layout.keys.ctrl}++/${layout.keys.ctrl}+`, desc: t('statusbar.zoom') },
{ keys: layout.keys.enter, desc: t('statusbar.runCommand') },
{ keys: `${layout.keys.up}/${layout.keys.down}`, desc: t('statusbar.commandHistory') },
],
tests: [],
config: [],
}), [layout, t])
return (
<div className="app-layout">
<header className="header">
<div className="header-brand">
<img src="/muyue-64.png" alt="Muyue" className="header-logo-img" width="22" height="22" style={{ borderRadius: 4, verticalAlign: 'middle' }} />
<span className="header-logo">MUYUE</span>
<span className="header-version">v{info.version || '...'}</span>
</div>
<nav className="header-nav">
{TABS.map(tab => (
<div
key={tab.id}
className={`nav-tab ${activeTab === tab.id ? 'active' : ''}`}
onClick={() => switchTab(tab.id)}
role="tab"
aria-selected={activeTab === tab.id}
>
<span className="tab-icon">{tab.icon}</span>
{tab.label}
</div>
))}
</nav>
<div className="header-spacer" />
<span className="header-clock">
{clock.toLocaleTimeString(layout.locale, { hour: '2-digit', minute: '2-digit' })}
</span>
</header>
<main className="content">
<div className={activeTab === 'dash' ? '' : 'tab-hidden'}><Dashboard api={api} refreshRef={dashRefreshRef} /></div>
<div className={activeTab === 'studio' ? '' : 'tab-hidden'}><Studio api={api} /></div>
<div className={activeTab === 'shell' ? '' : 'tab-hidden'}><Shell api={api} isSudo={isSudo} /></div>
<div className={activeTab === 'tests' ? '' : 'tab-hidden'}><Tests api={api} /></div>
<div className={activeTab === 'config' ? '' : 'tab-hidden'}><Config api={api} /></div>
</main>
<footer className="statusbar">
<div className="statusbar-left">
{isSudo && <span className="statusbar-sudo"> SUDO</span>}
{activeTab === 'dash' && (
<span className="statusbar-shortcut">
<kbd>{layout.keys.ctrl}+R</kbd> refresh
</span>
)}
<FooterShortcuts shortcuts={WINDOW_SHORTCUTS[activeTab] || []} />
</div>
<div className="statusbar-right">
<span style={{ fontFamily: 'var(--font-mono)' }}>
{layout.keys.ctrl}+{layout.keys.range} {t('statusbar.switchWindow')}
</span>
</div>
</footer>
{showOnboarding && <OnboardingWizard api={api} onComplete={() => setShowOnboarding(false)} />}
</div>
)
}
function FooterShortcuts({ shortcuts }) {
return shortcuts.map((s, i) => (
<span key={i} className="statusbar-shortcut">
<kbd>{s.keys}</kbd> {s.desc}
</span>
))
}