@camstack/addon-admin-ui 0.1.1

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 (122) hide show
  1. package/index.html +22 -0
  2. package/package.json +69 -0
  3. package/public/brand/logo-dark.svg +16 -0
  4. package/public/brand/logo-horizontal-dark.svg +21 -0
  5. package/public/brand/logo-horizontal-light.svg +21 -0
  6. package/public/brand/logo-light.svg +16 -0
  7. package/public/brand/logo-wide-dark.svg +24 -0
  8. package/public/brand/logo-wide-light.svg +24 -0
  9. package/public/favicon.svg +8 -0
  10. package/public/vendor/react-jsx-runtime.mjs +24 -0
  11. package/public/vendor/react.mjs +16 -0
  12. package/src/App.tsx +71 -0
  13. package/src/components/addons/AddonCard.tsx +339 -0
  14. package/src/components/addons/AddonUploadZone.tsx +307 -0
  15. package/src/components/addons/CapabilityBadge.tsx +55 -0
  16. package/src/components/addons/CapabilityMap.tsx +133 -0
  17. package/src/components/addons/UpdatesList.tsx +119 -0
  18. package/src/components/agents/AgentCard.tsx +281 -0
  19. package/src/components/agents/AgentLogs.tsx +231 -0
  20. package/src/components/agents/ProcessList.tsx +127 -0
  21. package/src/components/agents/ProcessTree.tsx +369 -0
  22. package/src/components/agents/TaskList.tsx +68 -0
  23. package/src/components/cameras/CameraCard.tsx +60 -0
  24. package/src/components/cameras/LiveEventsPanel.tsx +91 -0
  25. package/src/components/cameras/ProviderSection.tsx +50 -0
  26. package/src/components/cameras/StreamArea.tsx +107 -0
  27. package/src/components/cameras/tabs/AddonsTab.tsx +113 -0
  28. package/src/components/cameras/tabs/CameraEventsTab.tsx +129 -0
  29. package/src/components/cameras/tabs/PipelineTab.tsx +118 -0
  30. package/src/components/cameras/tabs/StreamsTab.tsx +114 -0
  31. package/src/components/dashboard/BlockPicker.tsx +54 -0
  32. package/src/components/dashboard/BlockWrapper.tsx +97 -0
  33. package/src/components/dashboard/DashboardGrid.tsx +160 -0
  34. package/src/components/dashboard/block-registry.ts +15 -0
  35. package/src/components/dashboard/blocks/PipelineStagesBlock.tsx +39 -0
  36. package/src/components/dashboard/blocks/StorageBlock.tsx +66 -0
  37. package/src/components/dashboard/blocks/SystemStatusBlock.tsx +67 -0
  38. package/src/components/dashboard/blocks/index.ts +32 -0
  39. package/src/components/device/DeviceHeader.tsx +116 -0
  40. package/src/components/device/FloatingPanel.tsx +132 -0
  41. package/src/components/device/FloatingPanelManager.tsx +167 -0
  42. package/src/components/device/PanelContent.tsx +196 -0
  43. package/src/components/device/QuickConfigWizard.tsx +507 -0
  44. package/src/components/device/tabs/DetectionConfigTab.tsx +96 -0
  45. package/src/components/device/tabs/EventsTab.tsx +19 -0
  46. package/src/components/device/tabs/LogsTab.tsx +22 -0
  47. package/src/components/device/tabs/OverviewTab.tsx +104 -0
  48. package/src/components/device/tabs/ProviderSettingsTab.tsx +34 -0
  49. package/src/components/device/tabs/RecordingTab.tsx +47 -0
  50. package/src/components/device/tabs/ReplTab.tsx +153 -0
  51. package/src/components/device/tabs/TrackTrailTab.tsx +49 -0
  52. package/src/components/device/tabs/ZonesTab.tsx +98 -0
  53. package/src/components/device/zone-editor/ZoneCanvas.tsx +354 -0
  54. package/src/components/device/zone-editor/ZoneForm.tsx +128 -0
  55. package/src/components/device/zone-editor/ZoneList.tsx +150 -0
  56. package/src/components/form-builder/FormBuilder.tsx +135 -0
  57. package/src/components/form-builder/FormField.tsx +732 -0
  58. package/src/components/form-builder/ModelSelector.tsx +239 -0
  59. package/src/components/integrations/AddDeviceDialog.tsx +205 -0
  60. package/src/components/integrations/CompactDeviceCard.tsx +35 -0
  61. package/src/components/integrations/DeviceCard.tsx +29 -0
  62. package/src/components/integrations/DeviceDiscoveryStep.tsx +105 -0
  63. package/src/components/integrations/DeviceGrid.tsx +79 -0
  64. package/src/components/integrations/DeviceGroupHeader.tsx +17 -0
  65. package/src/components/integrations/DiscoveredDeviceCard.tsx +26 -0
  66. package/src/components/integrations/IntegrationCard.tsx +40 -0
  67. package/src/components/integrations/IntegrationWizard.tsx +171 -0
  68. package/src/components/integrations/ProviderConfigForm.tsx +89 -0
  69. package/src/components/integrations/ProviderPicker.tsx +91 -0
  70. package/src/components/integrations/SnapshotPopover.tsx +68 -0
  71. package/src/components/metrics/AgentLoad.tsx +113 -0
  72. package/src/components/metrics/IntegrationUsage.tsx +90 -0
  73. package/src/components/metrics/PipelineStatus.tsx +105 -0
  74. package/src/components/metrics/ProcessResources.tsx +139 -0
  75. package/src/components/pipeline/PhaseSettings.tsx +131 -0
  76. package/src/components/shared/CapabilityBadges.tsx +30 -0
  77. package/src/components/shared/ProviderIcon.tsx +42 -0
  78. package/src/components/shared/StatusBadge.tsx +23 -0
  79. package/src/components/shared/WebRtcPlayer.tsx +211 -0
  80. package/src/components/timeline/EventMarker.tsx +32 -0
  81. package/src/components/timeline/TimelineBar.tsx +131 -0
  82. package/src/components/ui/ConfirmDialog.tsx +115 -0
  83. package/src/components/ui/ToastContainer.tsx +92 -0
  84. package/src/contexts/auth-context.tsx +91 -0
  85. package/src/hooks/useBackendClient.ts +6 -0
  86. package/src/hooks/useTheme.ts +1 -0
  87. package/src/i18n/en.json +164 -0
  88. package/src/i18n/index.ts +29 -0
  89. package/src/i18n/it.json +164 -0
  90. package/src/index.css +63 -0
  91. package/src/layouts/AddonPageLoader.tsx +120 -0
  92. package/src/layouts/AppLayout.tsx +238 -0
  93. package/src/layouts/ProtectedRoute.tsx +25 -0
  94. package/src/lib/addon-page-context.ts +29 -0
  95. package/src/lib/backend.ts +16 -0
  96. package/src/main.tsx +21 -0
  97. package/src/pages/AccessDenied.tsx +22 -0
  98. package/src/pages/Cameras.tsx +127 -0
  99. package/src/pages/Dashboard.tsx +6 -0
  100. package/src/pages/DeviceDetail.tsx +175 -0
  101. package/src/pages/IntegrationDetail.tsx +224 -0
  102. package/src/pages/Integrations.tsx +330 -0
  103. package/src/pages/Login.tsx +106 -0
  104. package/src/pages/Metrics.tsx +18 -0
  105. package/src/pages/PipelineConfig.tsx +282 -0
  106. package/src/pages/Showroom.tsx +351 -0
  107. package/src/pages/Timeline.tsx +269 -0
  108. package/src/pages/system/Addons.tsx +525 -0
  109. package/src/pages/system/Agents.tsx +362 -0
  110. package/src/pages/system/Logs.tsx +131 -0
  111. package/src/pages/system/Models.tsx +102 -0
  112. package/src/pages/system/Processes.tsx +129 -0
  113. package/src/pages/system/Repl.tsx +148 -0
  114. package/src/pages/system/Settings.tsx +168 -0
  115. package/src/pages/system/Users.tsx +174 -0
  116. package/src/server/addon.ts +54 -0
  117. package/src/types/config-ui.ts +210 -0
  118. package/src/types/dashboard.ts +39 -0
  119. package/tsconfig.json +29 -0
  120. package/tsconfig.server.json +16 -0
  121. package/tsup.config.ts +20 -0
  122. package/vite.config.ts +68 -0
@@ -0,0 +1,132 @@
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
+ }
@@ -0,0 +1,167 @@
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
+ }
@@ -0,0 +1,196 @@
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
+ }