dashboard-nanobot/frontend/src/App.tsx

171 lines
6.7 KiB
TypeScript
Raw Normal View History

2026-03-02 02:54:40 +00:00
import { useEffect, useMemo, useState } from 'react';
2026-03-02 07:51:47 +00:00
import { MoonStar, SunMedium, X } from 'lucide-react';
2026-03-01 16:26:03 +00:00
import {
useAppStore,
} from './store/appStore';
import { useBotsSync } from './hooks/useBotsSync';
import { ImageFactoryModule } from './modules/images/ImageFactoryModule';
import { BotWizardModule } from './modules/onboarding/BotWizardModule';
import { BotDashboardModule } from './modules/dashboard/BotDashboardModule';
import { pickLocale } from './i18n';
import { appZhCn } from './i18n/app.zh-cn';
import { appEn } from './i18n/app.en';
2026-03-03 08:12:27 +00:00
import { LucentIconButton } from './components/lucent/LucentIconButton';
import { LucentTooltip } from './components/lucent/LucentTooltip';
2026-03-01 16:26:03 +00:00
import './App.css';
function App() {
2026-03-02 02:54:40 +00:00
const { theme, setTheme, locale, setLocale, activeBots } = useAppStore();
2026-03-01 16:26:03 +00:00
const [showImageFactory, setShowImageFactory] = useState(false);
const [showCreateWizard, setShowCreateWizard] = useState(false);
useBotsSync();
const t = pickLocale(locale, { 'zh-cn': appZhCn, en: appEn });
const urlView = useMemo(() => {
const params = new URLSearchParams(window.location.search);
2026-03-03 16:45:51 +00:00
const pathMatch = window.location.pathname.match(/^\/bot\/([^/?#]+)/i);
let forcedBotIdFromPath = '';
if (pathMatch?.[1]) {
try {
forcedBotIdFromPath = decodeURIComponent(pathMatch[1]).trim();
} catch {
forcedBotIdFromPath = String(pathMatch[1]).trim();
}
}
const forcedBotIdFromQuery =
2026-03-01 16:26:03 +00:00
(params.get('botId') || params.get('bot_id') || params.get('id') || '').trim();
2026-03-03 16:45:51 +00:00
const forcedBotId = forcedBotIdFromPath || forcedBotIdFromQuery;
2026-03-01 16:26:03 +00:00
const compactRaw = (params.get('compact') || params.get('h5') || params.get('mobile') || '').trim().toLowerCase();
const compactByFlag = ['1', 'true', 'yes', 'on'].includes(compactRaw);
const compactMode = compactByFlag || forcedBotId.length > 0;
return { forcedBotId, compactMode };
}, []);
2026-03-02 02:54:40 +00:00
useEffect(() => {
const forced = urlView.forcedBotId;
if (!forced) {
document.title = t.title;
return;
}
const bot = activeBots[forced];
const botName = String(bot?.name || '').trim();
document.title = botName ? `${t.title} - ${botName}` : `${t.title} - ${forced}`;
}, [activeBots, t.title, urlView.forcedBotId]);
2026-03-01 16:26:03 +00:00
return (
<div className={`app-shell ${urlView.compactMode ? 'app-shell-compact' : ''}`} data-theme={theme}>
<div className="app-frame">
<header className="app-header">
<div className="row-between app-header-top">
<div className="app-title">
<img src="/app-bot-icon.svg" alt="Nanobot" className="app-title-icon" />
<div>
<h1>{t.title}</h1>
</div>
</div>
<div className="global-switches">
<div className="switch-compact">
2026-03-03 08:12:27 +00:00
<LucentTooltip content={t.dark}>
<button
className={`switch-btn ${theme === 'dark' ? 'active' : ''}`}
onClick={() => setTheme('dark')}
aria-label={t.dark}
>
<MoonStar size={14} />
</button>
</LucentTooltip>
<LucentTooltip content={t.light}>
<button
className={`switch-btn ${theme === 'light' ? 'active' : ''}`}
onClick={() => setTheme('light')}
aria-label={t.light}
>
<SunMedium size={14} />
</button>
</LucentTooltip>
2026-03-01 16:26:03 +00:00
</div>
<div className="switch-compact">
2026-03-03 08:12:27 +00:00
<LucentTooltip content={t.zh}>
<button
className={`switch-btn switch-btn-lang ${locale === 'zh' ? 'active' : ''}`}
onClick={() => setLocale('zh')}
aria-label={t.zh}
>
<span>ZH</span>
</button>
</LucentTooltip>
<LucentTooltip content={t.en}>
<button
className={`switch-btn switch-btn-lang ${locale === 'en' ? 'active' : ''}`}
onClick={() => setLocale('en')}
aria-label={t.en}
>
<span>EN</span>
</button>
</LucentTooltip>
2026-03-01 16:26:03 +00:00
</div>
</div>
</div>
</header>
<main className="main-stage">
<BotDashboardModule
onOpenCreateWizard={() => setShowCreateWizard(true)}
onOpenImageFactory={() => setShowImageFactory(true)}
forcedBotId={urlView.forcedBotId || undefined}
compactMode={urlView.compactMode}
/>
</main>
</div>
{!urlView.compactMode && showImageFactory && (
<div className="modal-mask app-modal-mask" onClick={() => setShowImageFactory(false)}>
<div className="modal-card app-modal-card" onClick={(e) => e.stopPropagation()}>
2026-03-02 07:51:47 +00:00
<div className="modal-title-row modal-title-with-close">
<div className="modal-title-main">
<h3>{t.nav.images.title}</h3>
</div>
<div className="modal-title-actions">
2026-03-03 08:12:27 +00:00
<LucentIconButton className="btn btn-secondary btn-sm icon-btn" onClick={() => setShowImageFactory(false)} tooltip={t.close} aria-label={t.close}>
2026-03-02 07:51:47 +00:00
<X size={14} />
2026-03-03 08:12:27 +00:00
</LucentIconButton>
2026-03-02 07:51:47 +00:00
</div>
2026-03-01 16:26:03 +00:00
</div>
<div className="app-modal-body">
<ImageFactoryModule />
</div>
</div>
</div>
)}
{!urlView.compactMode && showCreateWizard && (
<div className="modal-mask app-modal-mask" onClick={() => setShowCreateWizard(false)}>
<div className="modal-card app-modal-card" onClick={(e) => e.stopPropagation()}>
2026-03-02 07:51:47 +00:00
<div className="modal-title-row modal-title-with-close">
<div className="modal-title-main">
<h3>{t.nav.onboarding.title}</h3>
</div>
<div className="modal-title-actions">
2026-03-03 08:12:27 +00:00
<LucentIconButton className="btn btn-secondary btn-sm icon-btn" onClick={() => setShowCreateWizard(false)} tooltip={t.close} aria-label={t.close}>
2026-03-02 07:51:47 +00:00
<X size={14} />
2026-03-03 08:12:27 +00:00
</LucentIconButton>
2026-03-02 07:51:47 +00:00
</div>
2026-03-01 16:26:03 +00:00
</div>
<div className="app-modal-body">
<BotWizardModule
onCreated={() => {
setShowCreateWizard(false);
}}
onGoDashboard={() => setShowCreateWizard(false)}
/>
</div>
</div>
</div>
)}
</div>
);
}
export default App;