@camstack/addon-admin-ui 0.1.2 → 0.1.4

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.
Files changed (127) hide show
  1. package/dist/assets/index-BoVZEQ1j.js +598 -0
  2. package/dist/assets/index-DwSc8ann.css +1 -0
  3. package/{index.html → dist/index.html} +3 -1
  4. package/dist/server/addon.d.ts +11 -0
  5. package/dist/server/addon.js +50 -0
  6. package/dist/server/addon.js.map +1 -0
  7. package/package.json +4 -1
  8. package/src/App.tsx +0 -71
  9. package/src/components/addons/AddonCard.tsx +0 -355
  10. package/src/components/addons/AddonUploadZone.tsx +0 -69
  11. package/src/components/addons/CapabilityBadge.tsx +0 -55
  12. package/src/components/addons/CapabilityMap.tsx +0 -133
  13. package/src/components/addons/UpdatesList.tsx +0 -108
  14. package/src/components/agents/AgentCard.tsx +0 -281
  15. package/src/components/agents/AgentLogs.tsx +0 -231
  16. package/src/components/agents/ProcessList.tsx +0 -127
  17. package/src/components/agents/ProcessTree.tsx +0 -369
  18. package/src/components/agents/TaskList.tsx +0 -68
  19. package/src/components/cameras/CameraCard.tsx +0 -60
  20. package/src/components/cameras/LiveEventsPanel.tsx +0 -91
  21. package/src/components/cameras/ProviderSection.tsx +0 -50
  22. package/src/components/cameras/StreamArea.tsx +0 -107
  23. package/src/components/cameras/tabs/AddonsTab.tsx +0 -113
  24. package/src/components/cameras/tabs/CameraEventsTab.tsx +0 -129
  25. package/src/components/cameras/tabs/PipelineTab.tsx +0 -118
  26. package/src/components/cameras/tabs/StreamsTab.tsx +0 -114
  27. package/src/components/dashboard/BlockPicker.tsx +0 -54
  28. package/src/components/dashboard/BlockWrapper.tsx +0 -97
  29. package/src/components/dashboard/DashboardGrid.tsx +0 -160
  30. package/src/components/dashboard/block-registry.ts +0 -15
  31. package/src/components/dashboard/blocks/PipelineStagesBlock.tsx +0 -39
  32. package/src/components/dashboard/blocks/StorageBlock.tsx +0 -66
  33. package/src/components/dashboard/blocks/SystemStatusBlock.tsx +0 -67
  34. package/src/components/dashboard/blocks/index.ts +0 -32
  35. package/src/components/device/DeviceHeader.tsx +0 -116
  36. package/src/components/device/FloatingPanel.tsx +0 -132
  37. package/src/components/device/FloatingPanelManager.tsx +0 -167
  38. package/src/components/device/PanelContent.tsx +0 -196
  39. package/src/components/device/QuickConfigWizard.tsx +0 -507
  40. package/src/components/device/tabs/DetectionConfigTab.tsx +0 -96
  41. package/src/components/device/tabs/EventsTab.tsx +0 -19
  42. package/src/components/device/tabs/LogsTab.tsx +0 -22
  43. package/src/components/device/tabs/OverviewTab.tsx +0 -104
  44. package/src/components/device/tabs/ProviderSettingsTab.tsx +0 -34
  45. package/src/components/device/tabs/RecordingTab.tsx +0 -47
  46. package/src/components/device/tabs/ReplTab.tsx +0 -153
  47. package/src/components/device/tabs/TrackTrailTab.tsx +0 -49
  48. package/src/components/device/tabs/ZonesTab.tsx +0 -98
  49. package/src/components/device/zone-editor/ZoneCanvas.tsx +0 -354
  50. package/src/components/device/zone-editor/ZoneForm.tsx +0 -128
  51. package/src/components/device/zone-editor/ZoneList.tsx +0 -150
  52. package/src/components/form-builder/FormBuilder.tsx +0 -135
  53. package/src/components/form-builder/FormField.tsx +0 -732
  54. package/src/components/form-builder/ModelSelector.tsx +0 -239
  55. package/src/components/integrations/AddDeviceDialog.tsx +0 -205
  56. package/src/components/integrations/CompactDeviceCard.tsx +0 -35
  57. package/src/components/integrations/DeviceCard.tsx +0 -29
  58. package/src/components/integrations/DeviceDiscoveryStep.tsx +0 -105
  59. package/src/components/integrations/DeviceGrid.tsx +0 -79
  60. package/src/components/integrations/DeviceGroupHeader.tsx +0 -17
  61. package/src/components/integrations/DiscoveredDeviceCard.tsx +0 -26
  62. package/src/components/integrations/IntegrationCard.tsx +0 -40
  63. package/src/components/integrations/IntegrationWizard.tsx +0 -172
  64. package/src/components/integrations/ProviderConfigForm.tsx +0 -89
  65. package/src/components/integrations/ProviderPicker.tsx +0 -91
  66. package/src/components/integrations/SnapshotPopover.tsx +0 -68
  67. package/src/components/metrics/AgentLoad.tsx +0 -105
  68. package/src/components/metrics/IntegrationUsage.tsx +0 -73
  69. package/src/components/metrics/PipelineStatus.tsx +0 -74
  70. package/src/components/metrics/ProcessResources.tsx +0 -123
  71. package/src/components/pipeline/PhaseSettings.tsx +0 -131
  72. package/src/components/shared/CapabilityBadges.tsx +0 -30
  73. package/src/components/shared/ProviderIcon.tsx +0 -42
  74. package/src/components/shared/StatusBadge.tsx +0 -23
  75. package/src/components/shared/WebRtcPlayer.tsx +0 -211
  76. package/src/components/timeline/EventMarker.tsx +0 -32
  77. package/src/components/timeline/TimelineBar.tsx +0 -131
  78. package/src/components/ui/ConfirmDialog.tsx +0 -115
  79. package/src/components/ui/ToastContainer.tsx +0 -92
  80. package/src/contexts/auth-context.tsx +0 -91
  81. package/src/hooks/useBackendClient.ts +0 -6
  82. package/src/hooks/useTheme.ts +0 -1
  83. package/src/i18n/en.json +0 -164
  84. package/src/i18n/index.ts +0 -29
  85. package/src/i18n/it.json +0 -164
  86. package/src/index.css +0 -63
  87. package/src/layouts/AddonPageLoader.tsx +0 -120
  88. package/src/layouts/AppLayout.tsx +0 -254
  89. package/src/layouts/ProtectedRoute.tsx +0 -25
  90. package/src/lib/addon-page-context.ts +0 -29
  91. package/src/lib/backend.ts +0 -16
  92. package/src/main.tsx +0 -21
  93. package/src/pages/AccessDenied.tsx +0 -22
  94. package/src/pages/Cameras.tsx +0 -127
  95. package/src/pages/Dashboard.tsx +0 -6
  96. package/src/pages/DeviceDetail.tsx +0 -175
  97. package/src/pages/IntegrationDetail.tsx +0 -222
  98. package/src/pages/Integrations.tsx +0 -333
  99. package/src/pages/Login.tsx +0 -106
  100. package/src/pages/Metrics.tsx +0 -18
  101. package/src/pages/PipelineConfig.tsx +0 -282
  102. package/src/pages/Showroom.tsx +0 -351
  103. package/src/pages/Timeline.tsx +0 -269
  104. package/src/pages/system/Addons.tsx +0 -396
  105. package/src/pages/system/Agents.tsx +0 -362
  106. package/src/pages/system/Logs.tsx +0 -131
  107. package/src/pages/system/Models.tsx +0 -102
  108. package/src/pages/system/Processes.tsx +0 -129
  109. package/src/pages/system/Repl.tsx +0 -148
  110. package/src/pages/system/Settings.tsx +0 -168
  111. package/src/pages/system/Users.tsx +0 -174
  112. package/src/server/addon.ts +0 -54
  113. package/src/types/config-ui.ts +0 -28
  114. package/src/types/dashboard.ts +0 -39
  115. package/tsconfig.json +0 -29
  116. package/tsconfig.server.json +0 -16
  117. package/tsup.config.ts +0 -20
  118. package/vite.config.ts +0 -68
  119. /package/{public → dist}/brand/logo-dark.svg +0 -0
  120. /package/{public → dist}/brand/logo-horizontal-dark.svg +0 -0
  121. /package/{public → dist}/brand/logo-horizontal-light.svg +0 -0
  122. /package/{public → dist}/brand/logo-light.svg +0 -0
  123. /package/{public → dist}/brand/logo-wide-dark.svg +0 -0
  124. /package/{public → dist}/brand/logo-wide-light.svg +0 -0
  125. /package/{public → dist}/favicon.svg +0 -0
  126. /package/{public → dist}/vendor/react-jsx-runtime.mjs +0 -0
  127. /package/{public → dist}/vendor/react.mjs +0 -0
@@ -1,116 +0,0 @@
1
- import { useState } from 'react'
2
- import { Link } from 'react-router-dom'
3
- import { ChevronRight, Video, ScrollText, Terminal, Bell, Zap } from 'lucide-react'
4
- import { StatusBadge } from '../shared/StatusBadge'
5
- import { ProviderIcon, getProviderLabel } from '../shared/ProviderIcon'
6
- import { CapabilityBadges } from '../shared/CapabilityBadges'
7
- import { QuickConfigWizard } from './QuickConfigWizard'
8
- import type { PanelId } from './FloatingPanelManager'
9
-
10
- interface DeviceHeaderProps {
11
- deviceId: string
12
- name: string
13
- status: string
14
- provider: string
15
- capabilities: string[]
16
- activePanel: PanelId | null
17
- onTogglePanel: (panel: PanelId) => void
18
- }
19
-
20
- const PANEL_BUTTONS: Array<{
21
- id: PanelId
22
- icon: React.ComponentType<{ className?: string }>
23
- label: string
24
- }> = [
25
- { id: 'stream', icon: Video, label: 'Stream' },
26
- { id: 'logs', icon: ScrollText, label: 'Logs' },
27
- { id: 'repl', icon: Terminal, label: 'REPL' },
28
- { id: 'events', icon: Bell, label: 'Events' },
29
- ]
30
-
31
- export function DeviceHeader({
32
- deviceId,
33
- name,
34
- status,
35
- provider,
36
- capabilities,
37
- activePanel,
38
- onTogglePanel,
39
- }: DeviceHeaderProps) {
40
- const [wizardOpen, setWizardOpen] = useState(false)
41
-
42
- return (
43
- <>
44
- <div className="border-b border-border bg-surface px-6 py-4">
45
- {/* Breadcrumb */}
46
- <nav className="flex items-center gap-1 text-[11px] text-foreground-subtle mb-3">
47
- <Link to="/integrations" className="hover:text-foreground transition-colors">
48
- Integrations
49
- </Link>
50
- <ChevronRight className="h-3 w-3" />
51
- <span className="text-foreground-subtle">{getProviderLabel(provider)}</span>
52
- <ChevronRight className="h-3 w-3" />
53
- <span className="text-foreground">{name}</span>
54
- </nav>
55
-
56
- {/* Header row */}
57
- <div className="flex items-center justify-between gap-4">
58
- <div className="flex items-center gap-3 min-w-0">
59
- <ProviderIcon type={provider} size="lg" />
60
- <div className="min-w-0">
61
- <div className="flex items-center gap-2">
62
- <h1 className="text-base font-semibold text-foreground truncate">{name}</h1>
63
- <StatusBadge status={status} />
64
- </div>
65
- <div className="mt-1">
66
- <CapabilityBadges capabilities={capabilities} />
67
- </div>
68
- </div>
69
- </div>
70
-
71
- {/* Action buttons */}
72
- <div className="flex items-center gap-1 flex-shrink-0">
73
- {/* Quick Config wizard button */}
74
- <button
75
- onClick={() => setWizardOpen(true)}
76
- title="Quick Config"
77
- className="inline-flex items-center gap-1.5 rounded-lg px-2.5 py-1.5 text-[11px] font-medium border border-border text-foreground-subtle hover:text-foreground hover:bg-surface-hover transition-colors"
78
- >
79
- <Zap className="h-3.5 w-3.5" />
80
- Quick Config
81
- </button>
82
-
83
- {/* Divider */}
84
- <div className="h-5 w-px bg-border mx-1" />
85
-
86
- {/* Floating panel toggle buttons */}
87
- {PANEL_BUTTONS.map(({ id, icon: Icon, label }) => {
88
- const isActive = activePanel === id
89
- return (
90
- <button
91
- key={id}
92
- onClick={() => onTogglePanel(id)}
93
- title={label}
94
- className={`inline-flex items-center justify-center rounded-lg p-2 transition-colors ${
95
- isActive
96
- ? 'bg-primary/15 text-primary'
97
- : 'text-foreground-subtle hover:text-foreground hover:bg-surface-hover'
98
- }`}
99
- >
100
- <Icon className="h-4 w-4" />
101
- </button>
102
- )
103
- })}
104
- </div>
105
- </div>
106
- </div>
107
-
108
- {/* Quick Config Wizard (modal) */}
109
- <QuickConfigWizard
110
- open={wizardOpen}
111
- onClose={() => setWizardOpen(false)}
112
- deviceId={deviceId}
113
- />
114
- </>
115
- )
116
- }
@@ -1,132 +0,0 @@
1
- import { useRef, useCallback } from 'react'
2
- import { X, Minimize2, Maximize2, GripHorizontal } from 'lucide-react'
3
- import type { LucideIcon } from 'lucide-react'
4
-
5
- interface FloatingPanelProps {
6
- id: string
7
- title: string
8
- icon: LucideIcon
9
- isOpen: boolean
10
- isFloating: boolean
11
- position?: { x: number; y: number }
12
- size?: { w: number; h: number }
13
- onClose: () => void
14
- onToggleFloat: () => void
15
- onPositionChange?: (pos: { x: number; y: number }) => void
16
- children: React.ReactNode
17
- }
18
-
19
- export function FloatingPanel({
20
- title,
21
- icon: Icon,
22
- isFloating,
23
- position = { x: 80, y: 80 },
24
- size = { w: 400, h: 480 },
25
- onClose,
26
- onToggleFloat,
27
- onPositionChange,
28
- children,
29
- }: FloatingPanelProps) {
30
- const panelRef = useRef<HTMLDivElement>(null)
31
- const dragging = useRef(false)
32
- const dragOffset = useRef({ x: 0, y: 0 })
33
-
34
- const handleMouseDown = useCallback(
35
- (e: React.MouseEvent<HTMLDivElement>) => {
36
- if (!isFloating) return
37
- dragging.current = true
38
- dragOffset.current = {
39
- x: e.clientX - position.x,
40
- y: e.clientY - position.y,
41
- }
42
-
43
- function onMouseMove(ev: MouseEvent) {
44
- if (!dragging.current) return
45
- onPositionChange?.({
46
- x: ev.clientX - dragOffset.current.x,
47
- y: ev.clientY - dragOffset.current.y,
48
- })
49
- }
50
-
51
- function onMouseUp() {
52
- dragging.current = false
53
- window.removeEventListener('mousemove', onMouseMove)
54
- window.removeEventListener('mouseup', onMouseUp)
55
- }
56
-
57
- window.addEventListener('mousemove', onMouseMove)
58
- window.addEventListener('mouseup', onMouseUp)
59
- },
60
- [isFloating, position.x, position.y, onPositionChange],
61
- )
62
-
63
- if (isFloating) {
64
- return (
65
- <div
66
- ref={panelRef}
67
- className="fixed z-40 flex flex-col rounded-xl border border-border bg-surface shadow-2xl overflow-hidden"
68
- style={{
69
- left: position.x,
70
- top: position.y,
71
- width: size.w,
72
- height: size.h,
73
- }}
74
- >
75
- {/* Title bar — drag handle */}
76
- <div
77
- onMouseDown={handleMouseDown}
78
- className="flex items-center gap-2 border-b border-border bg-surface-hover px-3 py-2 cursor-grab select-none active:cursor-grabbing flex-shrink-0"
79
- >
80
- <GripHorizontal className="h-3.5 w-3.5 text-foreground-subtle" />
81
- <Icon className="h-3.5 w-3.5 text-primary" />
82
- <span className="flex-1 text-xs font-semibold text-foreground">{title}</span>
83
- <button
84
- onClick={onToggleFloat}
85
- title="Dock panel"
86
- className="text-foreground-subtle hover:text-foreground transition-colors p-0.5 rounded"
87
- >
88
- <Minimize2 className="h-3.5 w-3.5" />
89
- </button>
90
- <button
91
- onClick={onClose}
92
- title="Close"
93
- className="text-foreground-subtle hover:text-danger transition-colors p-0.5 rounded"
94
- >
95
- <X className="h-3.5 w-3.5" />
96
- </button>
97
- </div>
98
-
99
- {/* Content */}
100
- <div className="flex-1 overflow-y-auto p-3 text-xs">{children}</div>
101
- </div>
102
- )
103
- }
104
-
105
- // Docked mode — rendered by FloatingPanelManager in the right column
106
- return (
107
- <div className="flex flex-col flex-1 min-h-0 border-b border-border last:border-b-0">
108
- {/* Header */}
109
- <div className="flex items-center gap-2 border-b border-border bg-surface-hover px-3 py-2 flex-shrink-0">
110
- <Icon className="h-3.5 w-3.5 text-primary" />
111
- <span className="flex-1 text-xs font-semibold text-foreground">{title}</span>
112
- <button
113
- onClick={onToggleFloat}
114
- title="Detach panel"
115
- className="text-foreground-subtle hover:text-foreground transition-colors p-0.5 rounded"
116
- >
117
- <Maximize2 className="h-3.5 w-3.5" />
118
- </button>
119
- <button
120
- onClick={onClose}
121
- title="Close"
122
- className="text-foreground-subtle hover:text-danger transition-colors p-0.5 rounded"
123
- >
124
- <X className="h-3.5 w-3.5" />
125
- </button>
126
- </div>
127
-
128
- {/* Content */}
129
- <div className="flex-1 overflow-y-auto p-3 text-xs">{children}</div>
130
- </div>
131
- )
132
- }
@@ -1,167 +0,0 @@
1
- import { useState, useCallback } from 'react'
2
- import { Video, ScrollText, Terminal, Bell } from 'lucide-react'
3
- import type { LucideIcon } from 'lucide-react'
4
- import { FloatingPanel } from './FloatingPanel'
5
- import {
6
- StreamPanelContent,
7
- LogsPanelContent,
8
- ReplPanelContent,
9
- EventsPanelContent,
10
- } from './PanelContent'
11
-
12
- export type PanelId = 'stream' | 'logs' | 'repl' | 'events'
13
-
14
- interface PanelState {
15
- isOpen: boolean
16
- isFloating: boolean
17
- position: { x: number; y: number }
18
- size: { w: number; h: number }
19
- }
20
-
21
- interface PanelConfig {
22
- id: PanelId
23
- title: string
24
- icon: LucideIcon
25
- }
26
-
27
- const PANEL_CONFIGS: PanelConfig[] = [
28
- { id: 'stream', title: 'Stream', icon: Video },
29
- { id: 'logs', title: 'Logs', icon: ScrollText },
30
- { id: 'repl', title: 'REPL', icon: Terminal },
31
- { id: 'events', title: 'Events', icon: Bell },
32
- ]
33
-
34
- const DEFAULT_PANEL_STATE: PanelState = {
35
- isOpen: false,
36
- isFloating: false,
37
- position: { x: 80, y: 80 },
38
- size: { w: 400, h: 480 },
39
- }
40
-
41
- function getInitialPanelStates(): Record<PanelId, PanelState> {
42
- return {
43
- stream: { ...DEFAULT_PANEL_STATE, position: { x: 80, y: 80 } },
44
- logs: { ...DEFAULT_PANEL_STATE, position: { x: 120, y: 120 } },
45
- repl: { ...DEFAULT_PANEL_STATE, position: { x: 160, y: 160 } },
46
- events: { ...DEFAULT_PANEL_STATE, position: { x: 200, y: 200 } },
47
- }
48
- }
49
-
50
- interface FloatingPanelManagerProps {
51
- deviceId: string
52
- /** Externally controlled open panels — toggled from DeviceHeader */
53
- openPanelId: PanelId | null
54
- onPanelClosed: (id: PanelId) => void
55
- children: React.ReactNode
56
- }
57
-
58
- export function FloatingPanelManager({
59
- deviceId,
60
- openPanelId,
61
- onPanelClosed,
62
- children,
63
- }: FloatingPanelManagerProps) {
64
- const [panelStates, setPanelStates] = useState<Record<PanelId, PanelState>>(getInitialPanelStates)
65
-
66
- const updatePanel = useCallback((id: PanelId, patch: Partial<PanelState>) => {
67
- setPanelStates((prev) => ({
68
- ...prev,
69
- [id]: { ...prev[id], ...patch },
70
- }))
71
- }, [])
72
-
73
- function handleClose(id: PanelId) {
74
- updatePanel(id, { isOpen: false, isFloating: false })
75
- onPanelClosed(id)
76
- }
77
-
78
- function handleToggleFloat(id: PanelId) {
79
- updatePanel(id, { isFloating: !panelStates[id].isFloating })
80
- }
81
-
82
- function handlePositionChange(id: PanelId, pos: { x: number; y: number }) {
83
- updatePanel(id, { position: pos })
84
- }
85
-
86
- // Determine which panels are visible (open or just toggled open externally)
87
- const visiblePanels = PANEL_CONFIGS.filter(({ id }) => {
88
- const state = panelStates[id]
89
- return state.isOpen || id === openPanelId
90
- })
91
-
92
- // Sync external toggle: if openPanelId was set externally and panel isn't already open, open it
93
- if (openPanelId !== null && !panelStates[openPanelId].isOpen) {
94
- updatePanel(openPanelId, { isOpen: true })
95
- }
96
-
97
- const dockedPanels = visiblePanels.filter(({ id }) => !panelStates[id].isFloating)
98
- const floatingPanels = visiblePanels.filter(({ id }) => panelStates[id].isFloating)
99
-
100
- function renderPanelContent(id: PanelId) {
101
- switch (id) {
102
- case 'stream':
103
- return <StreamPanelContent deviceId={deviceId} />
104
- case 'logs':
105
- return <LogsPanelContent />
106
- case 'repl':
107
- return <ReplPanelContent deviceId={deviceId} />
108
- case 'events':
109
- return <EventsPanelContent />
110
- }
111
- }
112
-
113
- return (
114
- <>
115
- {/* Main layout: tab content + docked panels column */}
116
- <div className="flex flex-1 min-h-0 overflow-hidden">
117
- {/* Tab content area */}
118
- <div className="flex-1 overflow-y-auto p-6">{children}</div>
119
-
120
- {/* Docked panels column */}
121
- {dockedPanels.length > 0 && (
122
- <div className="w-[360px] flex-shrink-0 border-l border-border bg-surface flex flex-col overflow-hidden">
123
- {dockedPanels.map(({ id, title, icon }) => {
124
- const state = panelStates[id]
125
- return (
126
- <FloatingPanel
127
- key={id}
128
- id={id}
129
- title={title}
130
- icon={icon}
131
- isOpen={state.isOpen}
132
- isFloating={false}
133
- onClose={() => handleClose(id)}
134
- onToggleFloat={() => handleToggleFloat(id)}
135
- >
136
- {renderPanelContent(id)}
137
- </FloatingPanel>
138
- )
139
- })}
140
- </div>
141
- )}
142
- </div>
143
-
144
- {/* Floating panels (overlays) */}
145
- {floatingPanels.map(({ id, title, icon }) => {
146
- const state = panelStates[id]
147
- return (
148
- <FloatingPanel
149
- key={id}
150
- id={id}
151
- title={title}
152
- icon={icon}
153
- isOpen={state.isOpen}
154
- isFloating={true}
155
- position={state.position}
156
- size={state.size}
157
- onClose={() => handleClose(id)}
158
- onToggleFloat={() => handleToggleFloat(id)}
159
- onPositionChange={(pos) => handlePositionChange(id, pos)}
160
- >
161
- {renderPanelContent(id)}
162
- </FloatingPanel>
163
- )
164
- })}
165
- </>
166
- )
167
- }
@@ -1,196 +0,0 @@
1
- import { ScrollText, Terminal, Bell, Play, Trash2, ChevronUp } from 'lucide-react'
2
- import { useState, useRef } from 'react'
3
- import { useMutation } from '@tanstack/react-query'
4
- import { useBackendClient } from '../../hooks/useBackendClient'
5
- import { WebRtcPlayer } from '../shared/WebRtcPlayer'
6
-
7
- // ---- Stream Panel ----
8
- interface StreamPanelContentProps {
9
- deviceId: string
10
- }
11
-
12
- export function StreamPanelContent({ deviceId }: StreamPanelContentProps) {
13
- const client = useBackendClient()
14
- const serverUrl = (client as unknown as { serverUrl?: string }).serverUrl ?? window.location.origin
15
-
16
- return (
17
- <div className="flex flex-col h-full gap-2">
18
- <WebRtcPlayer
19
- serverUrl={serverUrl}
20
- streamId={deviceId}
21
- className="flex-1 min-h-0 rounded-lg"
22
- />
23
- </div>
24
- )
25
- }
26
-
27
- // ---- Logs Panel ----
28
- export function LogsPanelContent() {
29
- return (
30
- <div className="flex flex-col items-center justify-center h-full py-8 text-foreground-subtle">
31
- <ScrollText className="h-8 w-8 mb-3 opacity-30" />
32
- <p className="text-sm font-medium text-foreground">Device logs</p>
33
- <p className="text-xs mt-1 opacity-70 text-center">Coming with backend integration</p>
34
- </div>
35
- )
36
- }
37
-
38
- // ---- REPL Panel ----
39
- interface ReplPanelContentProps {
40
- deviceId: string
41
- }
42
-
43
- interface HistoryEntry {
44
- id: number
45
- code: string
46
- result: string
47
- error: boolean
48
- ts: string
49
- }
50
-
51
- let replPanelCounter = 0
52
-
53
- export function ReplPanelContent({ deviceId }: ReplPanelContentProps) {
54
- const client = useBackendClient()
55
- const [code, setCode] = useState('')
56
- const [history, setHistory] = useState<HistoryEntry[]>([])
57
- const textareaRef = useRef<HTMLTextAreaElement>(null)
58
-
59
- const evalMutation = useMutation({
60
- mutationFn: (src: string) =>
61
- client.replEval(src, { type: 'device' as const, deviceId }),
62
- onSuccess: (data, src) => {
63
- const result = typeof data === 'string' ? data : JSON.stringify(data, null, 2)
64
- setHistory((prev) => [
65
- ...prev,
66
- {
67
- id: ++replPanelCounter,
68
- code: src,
69
- result,
70
- error: false,
71
- ts: new Date().toLocaleTimeString('en-GB', { hour12: false }),
72
- },
73
- ])
74
- setCode('')
75
- },
76
- onError: (err: unknown, src) => {
77
- const result = err instanceof Error ? err.message : String(err)
78
- setHistory((prev) => [
79
- ...prev,
80
- {
81
- id: ++replPanelCounter,
82
- code: src,
83
- result,
84
- error: true,
85
- ts: new Date().toLocaleTimeString('en-GB', { hour12: false }),
86
- },
87
- ])
88
- },
89
- })
90
-
91
- function handleExecute() {
92
- const trimmed = code.trim()
93
- if (!trimmed) return
94
- evalMutation.mutate(trimmed)
95
- }
96
-
97
- function handleKeyDown(e: React.KeyboardEvent<HTMLTextAreaElement>) {
98
- if ((e.metaKey || e.ctrlKey) && e.key === 'Enter') {
99
- e.preventDefault()
100
- handleExecute()
101
- }
102
- }
103
-
104
- function reuseCode(src: string) {
105
- setCode(src)
106
- textareaRef.current?.focus()
107
- }
108
-
109
- return (
110
- <div className="flex flex-col gap-2 h-full">
111
- <div className="flex items-center justify-between">
112
- <span className="text-[10px] text-foreground-subtle font-mono">
113
- device: <span className="text-foreground">{deviceId}</span>
114
- </span>
115
- {history.length > 0 && (
116
- <button
117
- onClick={() => setHistory([])}
118
- className="inline-flex items-center gap-1 text-[10px] text-foreground-subtle hover:text-foreground"
119
- >
120
- <Trash2 className="h-3 w-3" />
121
- Clear
122
- </button>
123
- )}
124
- </div>
125
-
126
- {/* Input */}
127
- <div className="rounded border border-border bg-surface overflow-hidden flex-shrink-0">
128
- <div className="flex items-center justify-between border-b border-border px-2 py-1">
129
- <span className="text-[10px] text-foreground-subtle font-mono">Ctrl+Enter to run</span>
130
- <button
131
- onClick={handleExecute}
132
- disabled={evalMutation.isPending || !code.trim()}
133
- className="inline-flex items-center gap-1 rounded px-2 py-0.5 text-[10px] font-medium bg-primary text-primary-foreground hover:bg-primary/90 disabled:opacity-50"
134
- >
135
- <Play className="h-3 w-3" />
136
- {evalMutation.isPending ? '...' : 'Run'}
137
- </button>
138
- </div>
139
- <textarea
140
- ref={textareaRef}
141
- value={code}
142
- onChange={(e) => setCode(e.target.value)}
143
- onKeyDown={handleKeyDown}
144
- rows={3}
145
- placeholder="// JavaScript..."
146
- className="w-full resize-none bg-background p-2 font-mono text-[11px] text-foreground placeholder:text-foreground-subtle focus:outline-none"
147
- spellCheck={false}
148
- />
149
- </div>
150
-
151
- {/* History */}
152
- <div className="flex-1 overflow-y-auto space-y-2">
153
- {history.length === 0 && (
154
- <div className="text-[10px] text-foreground-subtle">No evaluations yet</div>
155
- )}
156
- {[...history].reverse().map((entry) => (
157
- <div key={entry.id} className="rounded border border-border bg-surface overflow-hidden">
158
- <div className="flex items-start justify-between gap-1 border-b border-border bg-background px-2 py-1">
159
- <pre className="font-mono text-[10px] text-foreground whitespace-pre-wrap break-all flex-1">
160
- {entry.code}
161
- </pre>
162
- <div className="flex items-center gap-1 flex-shrink-0">
163
- <span className="text-[9px] text-foreground-subtle">{entry.ts}</span>
164
- <button
165
- onClick={() => reuseCode(entry.code)}
166
- title="Reuse"
167
- className="text-foreground-subtle hover:text-foreground"
168
- >
169
- <ChevronUp className="h-3 w-3" />
170
- </button>
171
- </div>
172
- </div>
173
- <pre
174
- className={`px-2 py-1.5 font-mono text-[10px] whitespace-pre-wrap break-all ${
175
- entry.error ? 'text-danger' : 'text-success'
176
- }`}
177
- >
178
- {entry.result}
179
- </pre>
180
- </div>
181
- ))}
182
- </div>
183
- </div>
184
- )
185
- }
186
-
187
- // ---- Events Panel ----
188
- export function EventsPanelContent() {
189
- return (
190
- <div className="flex flex-col items-center justify-center h-full py-8 text-foreground-subtle">
191
- <Bell className="h-8 w-8 mb-3 opacity-30" />
192
- <p className="text-sm font-medium text-foreground">No events</p>
193
- <p className="text-xs mt-1 opacity-70 text-center">Device events will appear here as they occur</p>
194
- </div>
195
- )
196
- }