@flame0510/project-aether 1.1.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +150 -0
- package/agent-templates/README.md +37 -0
- package/agent-templates/argus/AGENTS.md +52 -0
- package/agent-templates/argus/IDENTITY.md +7 -0
- package/agent-templates/argus/MEMORY.md +9 -0
- package/agent-templates/argus/SOUL.md +24 -0
- package/agent-templates/argus/TOOLS.md +8 -0
- package/agent-templates/atlas/AGENTS.md +47 -0
- package/agent-templates/atlas/HEARTBEAT.md +10 -0
- package/agent-templates/atlas/IDENTITY.md +6 -0
- package/agent-templates/atlas/MEMORY.md +9 -0
- package/agent-templates/atlas/SOUL.md +21 -0
- package/agent-templates/atlas/TOOLS.md +4 -0
- package/agent-templates/base-image/.trigger +0 -0
- package/agent-templates/base-image/Dockerfile +90 -0
- package/agent-templates/base-image/entrypoint.sh +37 -0
- package/agent-templates/custom/AGENTS.md +29 -0
- package/agent-templates/custom/BOOTSTRAP.md +56 -0
- package/agent-templates/custom/MEMORY.md +8 -0
- package/agent-templates/prometheus/AGENTS.md +37 -0
- package/agent-templates/prometheus/IDENTITY.md +6 -0
- package/agent-templates/prometheus/MEMORY.md +9 -0
- package/agent-templates/prometheus/SOUL.md +21 -0
- package/agent-templates/prometheus/TOOLS.md +4 -0
- package/app/agents/ChannelManager.tsx +465 -0
- package/app/agents/ImageDownloadBanner.tsx +159 -0
- package/app/agents/PageClient.tsx +1285 -0
- package/app/agents/create/PageClient.tsx +437 -0
- package/app/agents/create/page.tsx +126 -0
- package/app/agents/loading.tsx +14 -0
- package/app/agents/page.tsx +5 -0
- package/app/api/agents/[id]/backup/route.ts +169 -0
- package/app/api/agents/[id]/channels/pairing/route.ts +69 -0
- package/app/api/agents/[id]/channels/route.ts +14 -0
- package/app/api/agents/[id]/channels/telegram/route.ts +50 -0
- package/app/api/agents/[id]/lifecycle/route.ts +94 -0
- package/app/api/agents/[id]/recreate/route.ts +199 -0
- package/app/api/agents/[id]/restart/route.ts +33 -0
- package/app/api/agents/[id]/restore/route.ts +81 -0
- package/app/api/agents/[id]/route.ts +280 -0
- package/app/api/agents/channels-summary/route.ts +39 -0
- package/app/api/agents/config-json/route.ts +58 -0
- package/app/api/agents/create/route.ts +488 -0
- package/app/api/agents/download-image/route.ts +32 -0
- package/app/api/agents/image-status/route.ts +126 -0
- package/app/api/agents/route.ts +213 -0
- package/app/api/agents/token/route.ts +94 -0
- package/app/api/agents-active/route.ts +190 -0
- package/app/api/agents-config/route.ts +629 -0
- package/app/api/alerts/smoke/route.ts +26 -0
- package/app/api/aliases/add/route.ts +28 -0
- package/app/api/aliases/remove/route.ts +27 -0
- package/app/api/assistant/route.ts +204 -0
- package/app/api/auth/check/route.ts +38 -0
- package/app/api/auth/login/route.ts +85 -0
- package/app/api/auth/logout/route.ts +7 -0
- package/app/api/auth/status/route.ts +33 -0
- package/app/api/config/env/route.ts +132 -0
- package/app/api/config/restart/route.ts +26 -0
- package/app/api/containers/route.ts +75 -0
- package/app/api/cost-override/route.ts +46 -0
- package/app/api/costs/route.ts +84 -0
- package/app/api/credentials/[id]/reveal/route.ts +21 -0
- package/app/api/credentials/[id]/route.ts +52 -0
- package/app/api/credentials/[id]/sync/route.ts +58 -0
- package/app/api/credentials/detect/route.ts +246 -0
- package/app/api/credentials/route.ts +33 -0
- package/app/api/crons/[id]/route.ts +69 -0
- package/app/api/crons/route.ts +12 -0
- package/app/api/crons/runs/route.ts +31 -0
- package/app/api/debug/route.ts +11 -0
- package/app/api/envcheck/route.ts +18 -0
- package/app/api/events/route.ts +24 -0
- package/app/api/gateway/agent/route.ts +154 -0
- package/app/api/gateway/provider/balance/route.ts +38 -0
- package/app/api/gateway/provider/keys.ts +38 -0
- package/app/api/gateway/provider/oauth/route.ts +304 -0
- package/app/api/gateway/provider/route.ts +209 -0
- package/app/api/gateway/route.ts +223 -0
- package/app/api/gateway/sync.ts +189 -0
- package/app/api/lineage/route.ts +54 -0
- package/app/api/memory-context/route.ts +15 -0
- package/app/api/metrics/route.ts +35 -0
- package/app/api/models/route.ts +77 -0
- package/app/api/plugins/route.js +37 -0
- package/app/api/provider/upstream.ts +142 -0
- package/app/api/provider/v1/chat/completions/route.ts +146 -0
- package/app/api/provider/v1/models/route.ts +167 -0
- package/app/api/provider-usage/route.ts +629 -0
- package/app/api/session/route.ts +39 -0
- package/app/api/sessions/[id]/route.ts +38 -0
- package/app/api/sessions/route.ts +23 -0
- package/app/api/setup/agent-image/route.ts +102 -0
- package/app/api/setup/password/route.ts +111 -0
- package/app/api/setup/restart/route.ts +76 -0
- package/app/api/skills/delete/route.js +105 -0
- package/app/api/skills/promote/route.js +129 -0
- package/app/api/skills/route.js +203 -0
- package/app/api/skills/save/route.js +142 -0
- package/app/api/stats/route.ts +34 -0
- package/app/api/stats-since/route.ts +20 -0
- package/app/api/stream/route.ts +70 -0
- package/app/api/system-health/route.ts +233 -0
- package/app/api/tool-calls/route.ts +24 -0
- package/app/api/tools-config/route.ts +163 -0
- package/app/api/update-check/route.ts +127 -0
- package/app/api/version/route.ts +25 -0
- package/app/api/wizard/complete/route.ts +32 -0
- package/app/api/wizard/reset/route.ts +28 -0
- package/app/api/wizard/status/route.ts +28 -0
- package/app/api/workspace/route.ts +392 -0
- package/app/api/workspace/stream/route.ts +136 -0
- package/app/components/AppShell.tsx +33 -0
- package/app/components/AuthGuard.tsx +91 -0
- package/app/components/CostBreakdown.tsx +37 -0
- package/app/components/DashboardHeader.tsx +73 -0
- package/app/components/DashboardLayout.tsx +76 -0
- package/app/components/DashboardToolbar.tsx +61 -0
- package/app/components/FloatingActions.tsx +33 -0
- package/app/components/Icons.tsx +78 -0
- package/app/components/LineageGraphPage.tsx +142 -0
- package/app/components/LiveFeed.tsx +48 -0
- package/app/components/MobileBottomNav.tsx +84 -0
- package/app/components/ModelPickerModal.tsx +94 -0
- package/app/components/PasswordInput.tsx +86 -0
- package/app/components/PulseChat.tsx +362 -0
- package/app/components/Rev4aLoader.tsx +48 -0
- package/app/components/SessionDrawer.tsx +263 -0
- package/app/components/SessionTopology.tsx +289 -0
- package/app/components/Sidebar.tsx +294 -0
- package/app/components/Skeleton.tsx +133 -0
- package/app/components/SystemCockpit.tsx +169 -0
- package/app/components/VersionBanner.tsx +105 -0
- package/app/components/WizardButton.tsx +30 -0
- package/app/components/ui/Badge.tsx +30 -0
- package/app/components/ui/Button.tsx +59 -0
- package/app/components/ui/ConfirmModal.tsx +46 -0
- package/app/components/ui/FilterBar.tsx +33 -0
- package/app/components/ui/Input.tsx +25 -0
- package/app/components/ui/ItemList.tsx +83 -0
- package/app/components/ui/LoadingSpinner.tsx +30 -0
- package/app/components/ui/Metric.tsx +22 -0
- package/app/components/ui/Modal.tsx +42 -0
- package/app/components/ui/Page.tsx +18 -0
- package/app/components/ui/Pill.tsx +6 -0
- package/app/components/ui/PropertyList.tsx +24 -0
- package/app/components/ui/Select.tsx +26 -0
- package/app/components/ui/StatusCard.tsx +32 -0
- package/app/components/ui/Surface.tsx +14 -0
- package/app/components/ui/Tabs.tsx +55 -0
- package/app/components/ui/TemplateOption.tsx +59 -0
- package/app/components/ui/Toast.tsx +44 -0
- package/app/components/ui/index.ts +19 -0
- package/app/components/ui/tokens.ts +18 -0
- package/app/config/PageClient.tsx +293 -0
- package/app/config/loading.tsx +10 -0
- package/app/config/page.tsx +7 -0
- package/app/containers/ContainersClient.tsx +129 -0
- package/app/containers/loading.tsx +21 -0
- package/app/containers/page.tsx +5 -0
- package/app/containers/terminal/[id]/TerminalClient.tsx +313 -0
- package/app/containers/terminal/[id]/page.tsx +6 -0
- package/app/credentials/PageClient.tsx +598 -0
- package/app/credentials/loading.tsx +10 -0
- package/app/credentials/page.tsx +7 -0
- package/app/crons/PageClient.tsx +538 -0
- package/app/crons/loading.tsx +10 -0
- package/app/crons/page.tsx +5 -0
- package/app/design-system.ts +57 -0
- package/app/gateway/PageClient.tsx +1048 -0
- package/app/gateway/loading.tsx +24 -0
- package/app/gateway/page.tsx +5 -0
- package/app/globals.css +1474 -0
- package/app/layout.tsx +45 -0
- package/app/lib/models-context.tsx +41 -0
- package/app/lineage/loading.tsx +10 -0
- package/app/lineage/page.tsx +13 -0
- package/app/loading.tsx +16 -0
- package/app/login/page.tsx +160 -0
- package/app/memory/MemoryContextPageClient.tsx +196 -0
- package/app/memory/loading.tsx +10 -0
- package/app/memory/page.tsx +8 -0
- package/app/page.tsx +31 -0
- package/app/plugins/PageClient.tsx +296 -0
- package/app/plugins/loading.tsx +10 -0
- package/app/plugins/page.tsx +5 -0
- package/app/setup/PageClient.tsx +227 -0
- package/app/setup/loading.tsx +21 -0
- package/app/setup/page.tsx +7 -0
- package/app/skills/PageClient.tsx +499 -0
- package/app/skills/loading.tsx +10 -0
- package/app/skills/page.tsx +5 -0
- package/app/tools/ToolsPageClient.tsx +308 -0
- package/app/tools/loading.tsx +10 -0
- package/app/tools/page.tsx +62 -0
- package/app/tools/tools-catalog.ts +141 -0
- package/app/wizard/PageClient.tsx +485 -0
- package/app/wizard/icons.tsx +89 -0
- package/app/wizard/loading.tsx +24 -0
- package/app/wizard/page.tsx +7 -0
- package/app/wizard/storage.ts +33 -0
- package/app/wizard/useWizard.ts +101 -0
- package/app/workspace/WorkspaceClient.tsx +633 -0
- package/app/workspace/loading.tsx +36 -0
- package/app/workspace/page.tsx +8 -0
- package/bin/docker-entrypoint.sh +21 -0
- package/bin/postinstall.js +20 -0
- package/bin/rev4a.js +696 -0
- package/daemon.js +783 -0
- package/docs/ARCHITECTURE.md +557 -0
- package/docs/CONTAINER-TERMINAL.md +280 -0
- package/docs/DESIGN-SYSTEM.md +99 -0
- package/docs/FRONTEND-ARCHITECTURE.md +119 -0
- package/docs/REV4A.md +504 -0
- package/docs/dev/API-REFERENCE.md +1835 -0
- package/docs/dev/DATABASE.md +224 -0
- package/docs/dev/GATEWAY.md +300 -0
- package/docs/dev/PROVIDERS.md +229 -0
- package/docs/dev/SESSION-MAINTENANCE-PLAN.md +607 -0
- package/docs/dev/WORKSPACE.md +199 -0
- package/docs/rag/DATA-FRESHNESS.md +109 -0
- package/docs/rag/GLOSSARY.md +125 -0
- package/docs/rag/REV4A-OVERVIEW.md +135 -0
- package/docs/rag/WHAT-I-CAN-ANSWER.md +124 -0
- package/lib/agent-setup.ts +130 -0
- package/lib/alerts.ts +115 -0
- package/lib/apiFetch.ts +7 -0
- package/lib/auth.ts +28 -0
- package/lib/billing.ts +100 -0
- package/lib/buildAgentImage.ts +346 -0
- package/lib/channelManager.ts +386 -0
- package/lib/container.ts +60 -0
- package/lib/credentials/db.ts +86 -0
- package/lib/credentials/delivery.ts +448 -0
- package/lib/credentials/detect.ts +149 -0
- package/lib/credentials/hash.ts +9 -0
- package/lib/credentials/providers.ts +110 -0
- package/lib/credentials/vault.ts +195 -0
- package/lib/db-bootstrap.d.ts +3 -0
- package/lib/db-bootstrap.mjs +144 -0
- package/lib/db.ts +25 -0
- package/lib/docker-utils.ts +60 -0
- package/lib/hooks/useDashboard.ts +264 -0
- package/lib/hooks/useRev4aTimezone.ts +28 -0
- package/lib/hooks/useScreenshot.ts +30 -0
- package/lib/memory-context.ts +385 -0
- package/lib/model-catalogue.ts +131 -0
- package/lib/model-pricing.ts +63 -0
- package/lib/openclaw-cron.ts +350 -0
- package/lib/patterns/ApiAdapter.ts +73 -0
- package/lib/patterns/ApiClient.ts +65 -0
- package/lib/patterns/EventBus.ts +113 -0
- package/lib/patterns/FilterStrategy.ts +79 -0
- package/lib/patterns/SessionFactory.ts +117 -0
- package/lib/patterns/sessionPresentation.ts +117 -0
- package/lib/provider-balance.ts +163 -0
- package/lib/requireAuth.tsx +38 -0
- package/lib/rev4a-auth.d.ts +3 -0
- package/lib/rev4a-auth.js +36 -0
- package/lib/rev4a-paths.ts +145 -0
- package/lib/timezone.ts +46 -0
- package/lib/types/index.ts +96 -0
- package/lib/utils/format.ts +82 -0
- package/lineage.js +56 -0
- package/model-pricing.json +482 -0
- package/models.config.json +844 -0
- package/next-env.d.ts +6 -0
- package/next.config.mjs +7 -0
- package/package.json +82 -0
- package/public/apple-touch-icon.png +0 -0
- package/public/favicon-64.png +0 -0
- package/public/favicon.svg +58 -0
- package/public/icon-192-maskable.png +0 -0
- package/public/icon-192.png +0 -0
- package/public/icon-512-maskable.png +0 -0
- package/public/icon-512.png +0 -0
- package/public/manifest.json +38 -0
- package/public/rev4a-logo.png +0 -0
- package/public/rev4a-logo.svg +58 -0
- package/rev4a-rules/AGENTS.md +37 -0
- package/scripts/add-auth.cjs +185 -0
- package/scripts/backup.sh +66 -0
- package/scripts/check-providers.mjs +143 -0
- package/scripts/docker/traefik/docker-compose.yml +32 -0
- package/scripts/docker/traefik/dynamic/.gitkeep +18 -0
- package/scripts/docker/traefik/traefik.yml +37 -0
- package/scripts/remove-auth-profile.js +56 -0
- package/scripts/restore.sh +82 -0
- package/terminal-ws-server.js +139 -0
- package/tsconfig.json +42 -0
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect } from 'react';
|
|
4
|
+
import type { Costs } from '@/lib/types';
|
|
5
|
+
import { formatUsd } from '@/lib/utils/format';
|
|
6
|
+
import { useScreenshot } from '@/lib/hooks/useScreenshot';
|
|
7
|
+
import { useRev4aTimezone } from '@/lib/hooks/useRev4aTimezone';
|
|
8
|
+
import { formatDateTimeInTimezone } from '@/lib/timezone';
|
|
9
|
+
import { Skeleton } from './Skeleton';
|
|
10
|
+
|
|
11
|
+
interface DashboardHeaderProps {
|
|
12
|
+
costs: Costs;
|
|
13
|
+
loading?: boolean;
|
|
14
|
+
hideLogo?: boolean;
|
|
15
|
+
title?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const CameraIcon = () => (
|
|
19
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
|
|
20
|
+
<path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z"/>
|
|
21
|
+
<circle cx="12" cy="13" r="4"/>
|
|
22
|
+
</svg>
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
export default function DashboardHeader({ costs, loading: dataLoading = false, hideLogo = false, title }: DashboardHeaderProps) {
|
|
26
|
+
const [clock, setClock] = useState('');
|
|
27
|
+
const { loading, takeScreenshot } = useScreenshot();
|
|
28
|
+
const timezone = useRev4aTimezone();
|
|
29
|
+
|
|
30
|
+
useEffect(() => {
|
|
31
|
+
const tick = () =>
|
|
32
|
+
setClock(formatDateTimeInTimezone(Date.now(), {
|
|
33
|
+
day: '2-digit', month: '2-digit', year: 'numeric',
|
|
34
|
+
hour: '2-digit', minute: '2-digit', second: '2-digit',
|
|
35
|
+
hour12: false,
|
|
36
|
+
}, timezone));
|
|
37
|
+
tick();
|
|
38
|
+
const id = setInterval(tick, 1000);
|
|
39
|
+
return () => clearInterval(id);
|
|
40
|
+
}, [timezone]);
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
<header className="header">
|
|
44
|
+
<div className="logo">
|
|
45
|
+
{!hideLogo && <img src="/rev4a-logo.svg" alt="Rev4a" style={{ width: 22, height: 22, objectFit: 'contain' }} />}
|
|
46
|
+
<span>{title ?? 'REV4A'}</span>
|
|
47
|
+
</div>
|
|
48
|
+
<div className="meta">
|
|
49
|
+
<div suppressHydrationWarning>
|
|
50
|
+
<span className="meta-value" suppressHydrationWarning>{clock}</span>
|
|
51
|
+
</div>
|
|
52
|
+
<div>
|
|
53
|
+
<span className="meta-label">TODAY COST</span>
|
|
54
|
+
<span className="meta-value cost">{dataLoading ? <Skeleton className="skeleton--text" style={{ width: 58 }} /> : formatUsd(costs.today)}</span>
|
|
55
|
+
</div>
|
|
56
|
+
<div>
|
|
57
|
+
<span className="meta-label">ALL TIME</span>
|
|
58
|
+
<span className="meta-value cost">{dataLoading ? <Skeleton className="skeleton--text" style={{ width: 72 }} /> : formatUsd(costs.allTime)}</span>
|
|
59
|
+
</div>
|
|
60
|
+
<button
|
|
61
|
+
type="button"
|
|
62
|
+
className="agent-btn screenshot-btn-desktop"
|
|
63
|
+
onClick={takeScreenshot}
|
|
64
|
+
disabled={loading}
|
|
65
|
+
title="Screenshot"
|
|
66
|
+
style={{ padding: '4px 8px' }}
|
|
67
|
+
>
|
|
68
|
+
{loading ? '…' : <CameraIcon />}
|
|
69
|
+
</button>
|
|
70
|
+
</div>
|
|
71
|
+
</header>
|
|
72
|
+
);
|
|
73
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useResponsive } from '../design-system';
|
|
4
|
+
import type { Costs } from '@/lib/types';
|
|
5
|
+
import { useDashboard } from '@/lib/hooks/useDashboard';
|
|
6
|
+
import { useScreenshot } from '@/lib/hooks/useScreenshot';
|
|
7
|
+
import DashboardHeader from './DashboardHeader';
|
|
8
|
+
import SystemCockpit from './SystemCockpit';
|
|
9
|
+
import SessionDrawer from './SessionDrawer';
|
|
10
|
+
|
|
11
|
+
interface DashboardLayoutProps {
|
|
12
|
+
initialCosts?: Partial<Costs>;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export default function DashboardLayout({ initialCosts }: DashboardLayoutProps) {
|
|
16
|
+
const {
|
|
17
|
+
sessions,
|
|
18
|
+
events,
|
|
19
|
+
costs,
|
|
20
|
+
filter,
|
|
21
|
+
visibleEvents,
|
|
22
|
+
selectedSessionId,
|
|
23
|
+
selectSession,
|
|
24
|
+
hasSessionsLoaded,
|
|
25
|
+
hasCostLoaded,
|
|
26
|
+
} = useDashboard({ initialCosts });
|
|
27
|
+
|
|
28
|
+
const isMobile = useResponsive('md');
|
|
29
|
+
const { loading: screenshotLoading, takeScreenshot } = useScreenshot();
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<main className="shell">
|
|
33
|
+
<DashboardHeader costs={costs} loading={!hasCostLoaded} hideLogo title="DASHBOARD" />
|
|
34
|
+
{isMobile && (
|
|
35
|
+
<div style={{ display: 'flex', gap: 8, padding: '8px 12px', borderBottom: '1px solid var(--border)', background: 'var(--bg2)', justifyContent: 'flex-end' }}>
|
|
36
|
+
<button
|
|
37
|
+
type="button"
|
|
38
|
+
onClick={takeScreenshot}
|
|
39
|
+
disabled={screenshotLoading}
|
|
40
|
+
title="Screenshot"
|
|
41
|
+
style={{
|
|
42
|
+
padding: '6px 10px',
|
|
43
|
+
border: '1px solid var(--border)',
|
|
44
|
+
background: 'transparent',
|
|
45
|
+
color: 'var(--text-dim)',
|
|
46
|
+
cursor: 'pointer',
|
|
47
|
+
display: 'flex',
|
|
48
|
+
alignItems: 'center',
|
|
49
|
+
}}
|
|
50
|
+
>
|
|
51
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
|
|
52
|
+
<path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z"/>
|
|
53
|
+
<circle cx="12" cy="13" r="4"/>
|
|
54
|
+
</svg>
|
|
55
|
+
</button>
|
|
56
|
+
</div>
|
|
57
|
+
)}
|
|
58
|
+
|
|
59
|
+
<section className="layout layout--cockpit">
|
|
60
|
+
<SystemCockpit
|
|
61
|
+
sessions={sessions}
|
|
62
|
+
events={events}
|
|
63
|
+
costs={costs}
|
|
64
|
+
filter={filter}
|
|
65
|
+
visibleEvents={visibleEvents}
|
|
66
|
+
loading={!hasSessionsLoaded}
|
|
67
|
+
/>
|
|
68
|
+
</section>
|
|
69
|
+
|
|
70
|
+
<SessionDrawer
|
|
71
|
+
sessionId={selectedSessionId}
|
|
72
|
+
onClose={() => selectSession(null)}
|
|
73
|
+
/>
|
|
74
|
+
</main>
|
|
75
|
+
);
|
|
76
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import type { FilterConfig, Period } from '@/lib/types';
|
|
4
|
+
import { PERIODS } from '@/lib/types';
|
|
5
|
+
|
|
6
|
+
interface DashboardToolbarProps {
|
|
7
|
+
filter: FilterConfig;
|
|
8
|
+
onChange: (patch: Partial<FilterConfig>) => void;
|
|
9
|
+
agents?: string[];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export default function DashboardToolbar({ filter, onChange, agents = ['all'] }: DashboardToolbarProps) {
|
|
13
|
+
return (
|
|
14
|
+
<section className="toolbar">
|
|
15
|
+
<span className="toolbar-label">AGENTI</span>
|
|
16
|
+
|
|
17
|
+
{agents.map((name) => (
|
|
18
|
+
<button
|
|
19
|
+
key={name}
|
|
20
|
+
type="button"
|
|
21
|
+
className={`agent-btn ${filter.agent === name ? 'active' : ''}`}
|
|
22
|
+
onClick={() => onChange({ agent: name })}
|
|
23
|
+
>
|
|
24
|
+
{name}
|
|
25
|
+
</button>
|
|
26
|
+
))}
|
|
27
|
+
|
|
28
|
+
<span className="toolbar-sep">|</span>
|
|
29
|
+
|
|
30
|
+
<button
|
|
31
|
+
type="button"
|
|
32
|
+
className={`agent-btn ${filter.showOnlyActive ? 'active' : ''}`}
|
|
33
|
+
onClick={() => onChange({ showOnlyActive: !filter.showOnlyActive })}
|
|
34
|
+
>
|
|
35
|
+
solo attivi
|
|
36
|
+
</button>
|
|
37
|
+
|
|
38
|
+
<button
|
|
39
|
+
type="button"
|
|
40
|
+
className={`agent-btn ${!filter.showCron ? 'active' : ''}`}
|
|
41
|
+
onClick={() => onChange({ showCron: !filter.showCron })}
|
|
42
|
+
>
|
|
43
|
+
no cron
|
|
44
|
+
</button>
|
|
45
|
+
|
|
46
|
+
<span className="toolbar-sep">|</span>
|
|
47
|
+
<span className="toolbar-label">PERIODO</span>
|
|
48
|
+
|
|
49
|
+
{PERIODS.map((p) => (
|
|
50
|
+
<button
|
|
51
|
+
key={p}
|
|
52
|
+
type="button"
|
|
53
|
+
className={`agent-btn ${filter.period === p ? 'active' : ''}`}
|
|
54
|
+
onClick={() => onChange({ period: p })}
|
|
55
|
+
>
|
|
56
|
+
{p}
|
|
57
|
+
</button>
|
|
58
|
+
))}
|
|
59
|
+
</section>
|
|
60
|
+
);
|
|
61
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { usePathname } from 'next/navigation';
|
|
4
|
+
import PulseChat from './PulseChat';
|
|
5
|
+
import WizardButton from './WizardButton';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* FloatingActions
|
|
9
|
+
*
|
|
10
|
+
* Single container wrapping both floating buttons:
|
|
11
|
+
* - WizardButton (left)
|
|
12
|
+
* - PulseChat (right)
|
|
13
|
+
*
|
|
14
|
+
* Hidden on login, setup, and /wizard full-page.
|
|
15
|
+
*/
|
|
16
|
+
export default function FloatingActions() {
|
|
17
|
+
const pathname = usePathname();
|
|
18
|
+
|
|
19
|
+
if (pathname === '/login' || pathname === '/setup' || pathname === '/wizard') {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<div className="floating-actions">
|
|
25
|
+
<div className="floating-actions__left">
|
|
26
|
+
<WizardButton />
|
|
27
|
+
</div>
|
|
28
|
+
<div className="floating-actions__right">
|
|
29
|
+
<PulseChat />
|
|
30
|
+
</div>
|
|
31
|
+
</div>
|
|
32
|
+
);
|
|
33
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { SVGProps } from 'react';
|
|
4
|
+
|
|
5
|
+
type IconProps = SVGProps<SVGSVGElement> & { size?: number };
|
|
6
|
+
|
|
7
|
+
export function EyeIcon({ size = 20, ...props }: IconProps) {
|
|
8
|
+
return (
|
|
9
|
+
<svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" {...props}>
|
|
10
|
+
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z" />
|
|
11
|
+
<circle cx="12" cy="12" r="3" />
|
|
12
|
+
</svg>
|
|
13
|
+
);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function EyeOffIcon({ size = 20, ...props }: IconProps) {
|
|
17
|
+
return (
|
|
18
|
+
<svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" {...props}>
|
|
19
|
+
<path d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94" />
|
|
20
|
+
<path d="M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19" />
|
|
21
|
+
<line x1="1" y1="1" x2="23" y2="23" />
|
|
22
|
+
</svg>
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Shared wrapper so all section icons render with identical stroke styling. */
|
|
27
|
+
function Svg({ size = 20, children, ...props }: IconProps & { children: React.ReactNode }) {
|
|
28
|
+
return (
|
|
29
|
+
<svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" {...props}>
|
|
30
|
+
{children}
|
|
31
|
+
</svg>
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// PROPERTIES
|
|
36
|
+
export function InfoIcon(p: IconProps) {
|
|
37
|
+
return <Svg {...p}><circle cx="12" cy="12" r="10" /><line x1="12" y1="16" x2="12" y2="12" /><line x1="12" y1="8" x2="12.01" y2="8" /></Svg>;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// CONFIG (openclaw.json)
|
|
41
|
+
export function BracesIcon(p: IconProps) {
|
|
42
|
+
return <Svg {...p}><path d="M8 3H7a2 2 0 0 0-2 2v5a2 2 0 0 1-2 2 2 2 0 0 1 2 2v5a2 2 0 0 0 2 2h1" /><path d="M16 3h1a2 2 0 0 1 2 2v5a2 2 0 0 0 2 2 2 2 0 0 0-2 2v5a2 2 0 0 1-2 2h-1" /></Svg>;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// ACCESS TOKEN
|
|
46
|
+
export function KeyIcon(p: IconProps) {
|
|
47
|
+
return <Svg {...p}><path d="m15.5 7.5 3 3L22 7l-3-3" /><path d="m21 2-9.6 9.6" /><circle cx="7.5" cy="15.5" r="5.5" /></Svg>;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// DANGER ZONE (delete)
|
|
51
|
+
export function TrashIcon(p: IconProps) {
|
|
52
|
+
return <Svg {...p}><polyline points="3 6 5 6 21 6" /><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" /><line x1="10" y1="11" x2="10" y2="17" /><line x1="14" y1="11" x2="14" y2="17" /></Svg>;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// ENV (AGENT_ / MODEL_)
|
|
56
|
+
export function TerminalIcon(p: IconProps) {
|
|
57
|
+
return <Svg {...p}><polyline points="4 17 10 11 4 5" /><line x1="12" y1="19" x2="20" y2="19" /></Svg>;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// INTEGRATIONS
|
|
61
|
+
export function LinkIcon(p: IconProps) {
|
|
62
|
+
return <Svg {...p}><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" /><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" /></Svg>;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// BACKUP
|
|
66
|
+
export function ArchiveIcon(p: IconProps) {
|
|
67
|
+
return <Svg {...p}><polyline points="21 8 21 21 3 21 3 8" /><rect x="1" y="3" width="22" height="5" /><line x1="10" y1="12" x2="14" y2="12" /></Svg>;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// RESTART
|
|
71
|
+
export function RotateIcon(p: IconProps) {
|
|
72
|
+
return <Svg {...p}><polyline points="23 4 23 10 17 10" /><path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10" /></Svg>;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// RECREATE (rebuild from image)
|
|
76
|
+
export function PackageIcon(p: IconProps) {
|
|
77
|
+
return <Svg {...p}><line x1="16.5" y1="9.4" x2="7.5" y2="4.21" /><path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z" /><polyline points="3.27 6.96 12 12.01 20.73 6.96" /><line x1="12" y1="22.08" x2="12" y2="12" /></Svg>;
|
|
78
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useEffect, useRef, useState } from 'react';
|
|
4
|
+
import { useResponsive } from '../design-system';
|
|
5
|
+
import type { CSSProperties } from 'react';
|
|
6
|
+
import type { Costs } from '@/lib/types';
|
|
7
|
+
import { useDashboard } from '@/lib/hooks/useDashboard';
|
|
8
|
+
import { useScreenshot } from '@/lib/hooks/useScreenshot';
|
|
9
|
+
import DashboardHeader from './DashboardHeader';
|
|
10
|
+
import DashboardToolbar from './DashboardToolbar';
|
|
11
|
+
import LiveFeed from './LiveFeed';
|
|
12
|
+
import CostBreakdown from './CostBreakdown';
|
|
13
|
+
import SessionTopology, { type SessionTopologyHandle } from './SessionTopology';
|
|
14
|
+
import SessionDrawer from './SessionDrawer';
|
|
15
|
+
|
|
16
|
+
interface LineageGraphPageProps {
|
|
17
|
+
initialCosts?: Partial<Costs>;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export default function LineageGraphPage({ initialCosts }: LineageGraphPageProps) {
|
|
21
|
+
const {
|
|
22
|
+
costs,
|
|
23
|
+
filter,
|
|
24
|
+
selectedSessionId,
|
|
25
|
+
availableAgents,
|
|
26
|
+
visibleSessions,
|
|
27
|
+
visibleEvents,
|
|
28
|
+
setFilter,
|
|
29
|
+
selectSession,
|
|
30
|
+
hasSessionsLoaded,
|
|
31
|
+
hasCostLoaded,
|
|
32
|
+
} = useDashboard({ initialCosts });
|
|
33
|
+
|
|
34
|
+
const topologyRef = useRef<SessionTopologyHandle>(null);
|
|
35
|
+
const isMobile = useResponsive('md');
|
|
36
|
+
const { loading: screenshotLoading, takeScreenshot } = useScreenshot();
|
|
37
|
+
const [mobileTab, setMobileTab] = useState<'graph' | 'feed' | 'costs'>('graph');
|
|
38
|
+
|
|
39
|
+
const resetButtonStyle: CSSProperties = isMobile
|
|
40
|
+
? { width: '100%', justifyContent: 'center', minHeight: 38 }
|
|
41
|
+
: { flexShrink: 0, minWidth: 138 };
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<main className="shell">
|
|
45
|
+
<DashboardHeader costs={costs} loading={!hasCostLoaded} hideLogo title="LINEAGE" />
|
|
46
|
+
<DashboardToolbar filter={filter} onChange={setFilter} agents={availableAgents} />
|
|
47
|
+
|
|
48
|
+
{isMobile && (
|
|
49
|
+
<div style={{ display: 'flex', gap: 8, padding: '8px 12px', borderBottom: '1px solid var(--border)', background: 'var(--bg2)' }}>
|
|
50
|
+
{[
|
|
51
|
+
{ key: 'graph', label: 'GRAFO' },
|
|
52
|
+
{ key: 'feed', label: 'FEED' },
|
|
53
|
+
{ key: 'costs', label: 'COSTI' },
|
|
54
|
+
].map((t) => (
|
|
55
|
+
<button
|
|
56
|
+
key={t.key}
|
|
57
|
+
onClick={() => setMobileTab(t.key as 'graph' | 'feed' | 'costs')}
|
|
58
|
+
style={{
|
|
59
|
+
fontSize: 10,
|
|
60
|
+
letterSpacing: '0.08em',
|
|
61
|
+
padding: '6px 10px',
|
|
62
|
+
border: '1px solid var(--border)',
|
|
63
|
+
background: mobileTab === t.key ? 'var(--bg3)' : 'transparent',
|
|
64
|
+
color: mobileTab === t.key ? 'var(--violet)' : 'var(--text-dim)',
|
|
65
|
+
}}
|
|
66
|
+
>
|
|
67
|
+
{t.label}
|
|
68
|
+
</button>
|
|
69
|
+
))}
|
|
70
|
+
<button
|
|
71
|
+
type="button"
|
|
72
|
+
onClick={takeScreenshot}
|
|
73
|
+
disabled={screenshotLoading}
|
|
74
|
+
title="Screenshot"
|
|
75
|
+
style={{
|
|
76
|
+
marginLeft: 'auto',
|
|
77
|
+
padding: '6px 10px',
|
|
78
|
+
border: '1px solid var(--border)',
|
|
79
|
+
background: 'transparent',
|
|
80
|
+
color: 'var(--text-dim)',
|
|
81
|
+
cursor: 'pointer',
|
|
82
|
+
display: 'flex',
|
|
83
|
+
alignItems: 'center',
|
|
84
|
+
}}
|
|
85
|
+
>
|
|
86
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
|
|
87
|
+
<path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z"/>
|
|
88
|
+
<circle cx="12" cy="13" r="4"/>
|
|
89
|
+
</svg>
|
|
90
|
+
</button>
|
|
91
|
+
</div>
|
|
92
|
+
)}
|
|
93
|
+
|
|
94
|
+
<section className="layout" style={isMobile ? { display: 'block', height: 'calc(100vh - 150px)' } : undefined}>
|
|
95
|
+
{(!isMobile || mobileTab === 'graph') && (
|
|
96
|
+
<article className="panel graph-panel" style={isMobile ? { height: '100%', borderRight: 'none', padding: 16 } : { padding: 16 }}>
|
|
97
|
+
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 12, gap: 12 }}>
|
|
98
|
+
<div style={{ color: 'var(--text-dim)', fontSize: 11 }}>
|
|
99
|
+
Lineage map of sessions, sub-agents and operational relationships.
|
|
100
|
+
</div>
|
|
101
|
+
<button
|
|
102
|
+
type="button"
|
|
103
|
+
className="agent-btn"
|
|
104
|
+
aria-label="Reimposta vista grafo"
|
|
105
|
+
onClick={() => topologyRef.current?.resetView()}
|
|
106
|
+
style={resetButtonStyle}
|
|
107
|
+
>
|
|
108
|
+
⊙ Reimposta vista
|
|
109
|
+
</button>
|
|
110
|
+
</div>
|
|
111
|
+
{hasSessionsLoaded ? (
|
|
112
|
+
<SessionTopology
|
|
113
|
+
ref={topologyRef}
|
|
114
|
+
sessions={visibleSessions}
|
|
115
|
+
filter={filter.agent}
|
|
116
|
+
onNodeClick={selectSession}
|
|
117
|
+
emptyMessage={filter.showOnlyActive ? 'No active sessions in the selected period' : 'No sessions visible with current filters'}
|
|
118
|
+
/>
|
|
119
|
+
) : (
|
|
120
|
+
<div className="empty-state">
|
|
121
|
+
<span className="empty-state-icon">◎</span>
|
|
122
|
+
<span className="empty-state-msg">Loading lineage…</span>
|
|
123
|
+
</div>
|
|
124
|
+
)}
|
|
125
|
+
</article>
|
|
126
|
+
)}
|
|
127
|
+
|
|
128
|
+
{(!isMobile || mobileTab !== 'graph') && (
|
|
129
|
+
<aside className="side" style={isMobile ? { height: '100%' } : undefined}>
|
|
130
|
+
{(!isMobile || mobileTab === 'feed') && <LiveFeed events={hasSessionsLoaded ? visibleEvents : []} />}
|
|
131
|
+
{(!isMobile || mobileTab === 'costs') && <CostBreakdown byModel={costs.byModel} />}
|
|
132
|
+
</aside>
|
|
133
|
+
)}
|
|
134
|
+
</section>
|
|
135
|
+
|
|
136
|
+
<SessionDrawer
|
|
137
|
+
sessionId={selectedSessionId}
|
|
138
|
+
onClose={() => selectSession(null)}
|
|
139
|
+
/>
|
|
140
|
+
</main>
|
|
141
|
+
);
|
|
142
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import type { SessionEvent } from '@/lib/types';
|
|
4
|
+
import { deriveSessionDisplayLabel } from '@/lib/patterns/sessionPresentation';
|
|
5
|
+
import { formatTimeFromUnixSeconds } from '@/lib/utils/format';
|
|
6
|
+
import { SkeletonLines } from './Skeleton';
|
|
7
|
+
|
|
8
|
+
interface LiveFeedProps {
|
|
9
|
+
events: SessionEvent[];
|
|
10
|
+
loading?: boolean;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export default function LiveFeed({ events, loading = false }: LiveFeedProps) {
|
|
14
|
+
return (
|
|
15
|
+
<article className="panel feed-panel">
|
|
16
|
+
<h2>Live Events</h2>
|
|
17
|
+
<div>
|
|
18
|
+
{loading ? (
|
|
19
|
+
<div style={{ padding: 12 }}>
|
|
20
|
+
<SkeletonLines count={5} />
|
|
21
|
+
</div>
|
|
22
|
+
) : events.length === 0 ? (
|
|
23
|
+
<div className="empty-state">
|
|
24
|
+
<div className="empty-state-msg">Waiting for activity…</div>
|
|
25
|
+
</div>
|
|
26
|
+
) : (
|
|
27
|
+
events.map((item, index) => (
|
|
28
|
+
<div
|
|
29
|
+
key={`${item.ts ?? 0}-${item.event ?? 'evt'}-${index}`}
|
|
30
|
+
className="feed-item"
|
|
31
|
+
>
|
|
32
|
+
<span className="feed-time">{formatTimeFromUnixSeconds(item.ts)}</span>
|
|
33
|
+
<span className="feed-type">{item.type ?? item.event ?? 'event'}</span>
|
|
34
|
+
<span className="feed-label">
|
|
35
|
+
{deriveSessionDisplayLabel({
|
|
36
|
+
session_id: item.session_id ?? '',
|
|
37
|
+
label: item.session_label ?? item.label ?? null,
|
|
38
|
+
lineage_label: null,
|
|
39
|
+
task_preview: null,
|
|
40
|
+
})}
|
|
41
|
+
</span>
|
|
42
|
+
</div>
|
|
43
|
+
))
|
|
44
|
+
)}
|
|
45
|
+
</div>
|
|
46
|
+
</article>
|
|
47
|
+
);
|
|
48
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import Link from 'next/link';
|
|
4
|
+
import { usePathname } from 'next/navigation';
|
|
5
|
+
|
|
6
|
+
const navItems = [
|
|
7
|
+
{
|
|
8
|
+
label: 'Home',
|
|
9
|
+
path: '/',
|
|
10
|
+
icon: (
|
|
11
|
+
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
|
|
12
|
+
<path d="M3 9l6-7 6 7v6a2 2 0 01-2 2H5a2 2 0 01-2-2V9z"/>
|
|
13
|
+
<polyline points="7,17 7,10 11,10 11,17"/>
|
|
14
|
+
</svg>
|
|
15
|
+
),
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
label: 'Agents',
|
|
19
|
+
path: '/agents',
|
|
20
|
+
icon: (
|
|
21
|
+
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
|
|
22
|
+
<path d="M9 2a2.5 2.5 0 100 5 2.5 2.5 0 000-5z"/>
|
|
23
|
+
<path d="M5 16v-1.5A3.5 3.5 0 018.5 11h1A3.5 3.5 0 0113 14.5V16"/>
|
|
24
|
+
<circle cx="5" cy="5" r="1" fill="currentColor" stroke="none"/><circle cx="13" cy="5" r="1" fill="currentColor" stroke="none"/>
|
|
25
|
+
</svg>
|
|
26
|
+
),
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
label: 'Workspace',
|
|
30
|
+
path: '/workspace',
|
|
31
|
+
icon: (
|
|
32
|
+
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
|
|
33
|
+
<path d="M2.5 5a1.5 1.5 0 0 1 1.5-1.5h2.3l1.2 1.5H14a1.5 1.5 0 0 1 1.5 1.5v6A1.5 1.5 0 0 1 14 14H4a1.5 1.5 0 0 1-1.5-1.5V5z"/>
|
|
34
|
+
<path d="M2.5 6.5h13"/>
|
|
35
|
+
</svg>
|
|
36
|
+
),
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
label: 'Gateway',
|
|
40
|
+
path: '/gateway',
|
|
41
|
+
icon: (
|
|
42
|
+
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
|
|
43
|
+
<path d="M2.5 5.5H7M11 5.5h4.5M4.75 5.5a2 2 0 1 0 0 .01ZM13.25 5.5a2 2 0 1 0 0 .01ZM2.5 12.5H6M8 12.5h7.5M7 12.5a2 2 0 1 0 0 .01Z"/>
|
|
44
|
+
</svg>
|
|
45
|
+
),
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
label: 'Containers',
|
|
49
|
+
path: '/containers',
|
|
50
|
+
icon: (
|
|
51
|
+
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round">
|
|
52
|
+
<rect x="2" y="2" width="5.5" height="5.5" rx="1.5"/>
|
|
53
|
+
<rect x="10.5" y="2" width="5.5" height="5.5" rx="1.5"/>
|
|
54
|
+
<rect x="2" y="10.5" width="5.5" height="5.5" rx="1.5"/>
|
|
55
|
+
<rect x="10.5" y="10.5" width="5.5" height="5.5" rx="1.5"/>
|
|
56
|
+
</svg>
|
|
57
|
+
),
|
|
58
|
+
},
|
|
59
|
+
];
|
|
60
|
+
|
|
61
|
+
export default function MobileBottomNav() {
|
|
62
|
+
const pathname = usePathname();
|
|
63
|
+
|
|
64
|
+
return (
|
|
65
|
+
<nav className="mobile-bottom-nav">
|
|
66
|
+
<div className="mobile-bottom-nav__scroll">
|
|
67
|
+
{navItems.map((item) => {
|
|
68
|
+
const isActive = item.path === '/' ? pathname === '/' : pathname.startsWith(item.path);
|
|
69
|
+
return (
|
|
70
|
+
<Link
|
|
71
|
+
key={item.path}
|
|
72
|
+
href={item.path}
|
|
73
|
+
className={`mobile-bottom-nav__item ${isActive ? 'mobile-bottom-nav__item--active' : ''}`}
|
|
74
|
+
prefetch={true}
|
|
75
|
+
>
|
|
76
|
+
<span className="mobile-bottom-nav__icon">{item.icon}</span>
|
|
77
|
+
<span className="mobile-bottom-nav__label">{item.label}</span>
|
|
78
|
+
</Link>
|
|
79
|
+
);
|
|
80
|
+
})}
|
|
81
|
+
</div>
|
|
82
|
+
</nav>
|
|
83
|
+
);
|
|
84
|
+
}
|