@thehoneyjar/sigil-hud 0.1.0
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/LICENSE.md +660 -0
- package/README.md +146 -0
- package/dist/index.d.ts +911 -0
- package/dist/index.js +3079 -0
- package/package.json +68 -0
- package/src/components/DataSourceIndicator.tsx +185 -0
- package/src/components/DiagnosticsPanel.tsx +444 -0
- package/src/components/FeedbackPrompt.tsx +348 -0
- package/src/components/HudPanel.tsx +179 -0
- package/src/components/HudTrigger.tsx +81 -0
- package/src/components/IssueList.tsx +228 -0
- package/src/components/LensPanel.tsx +286 -0
- package/src/components/ObservationCaptureModal.tsx +502 -0
- package/src/components/PhysicsAnalysis.tsx +273 -0
- package/src/components/SimulationPanel.tsx +173 -0
- package/src/components/StateComparison.tsx +238 -0
- package/src/hooks/useDataSource.ts +324 -0
- package/src/hooks/useKeyboardShortcuts.ts +125 -0
- package/src/hooks/useObservationCapture.ts +154 -0
- package/src/hooks/useSignalCapture.ts +138 -0
- package/src/index.ts +112 -0
- package/src/providers/HudProvider.tsx +115 -0
- package/src/store.ts +60 -0
- package/src/styles/theme.ts +256 -0
- package/src/types.ts +276 -0
package/package.json
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@thehoneyjar/sigil-hud",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Diagnostic HUD for Sigil - composable React components for development",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"module": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"import": "./dist/index.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist",
|
|
17
|
+
"src"
|
|
18
|
+
],
|
|
19
|
+
"dependencies": {
|
|
20
|
+
"zustand": "^4.5.0"
|
|
21
|
+
},
|
|
22
|
+
"peerDependencies": {
|
|
23
|
+
"react": ">=18.0.0"
|
|
24
|
+
},
|
|
25
|
+
"optionalDependencies": {
|
|
26
|
+
"@thehoneyjar/sigil-anchor": "4.3.1",
|
|
27
|
+
"@thehoneyjar/sigil-diagnostics": "0.1.0",
|
|
28
|
+
"@thehoneyjar/sigil-lens": "0.1.0",
|
|
29
|
+
"@thehoneyjar/sigil-simulation": "0.1.0",
|
|
30
|
+
"@thehoneyjar/sigil-fork": "0.1.0"
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"@types/node": "^20.11.0",
|
|
34
|
+
"@types/react": "^18.0.0",
|
|
35
|
+
"react": "^18.0.0",
|
|
36
|
+
"tsup": "^8.0.0",
|
|
37
|
+
"typescript": "^5.0.0",
|
|
38
|
+
"@thehoneyjar/sigil-anchor": "4.3.1",
|
|
39
|
+
"@thehoneyjar/sigil-simulation": "0.1.0",
|
|
40
|
+
"@thehoneyjar/sigil-fork": "0.1.0",
|
|
41
|
+
"@thehoneyjar/sigil-lens": "0.1.0",
|
|
42
|
+
"@thehoneyjar/sigil-diagnostics": "0.1.0"
|
|
43
|
+
},
|
|
44
|
+
"sideEffects": false,
|
|
45
|
+
"license": "MIT",
|
|
46
|
+
"publishConfig": {
|
|
47
|
+
"access": "public",
|
|
48
|
+
"registry": "https://registry.npmjs.org/"
|
|
49
|
+
},
|
|
50
|
+
"repository": {
|
|
51
|
+
"type": "git",
|
|
52
|
+
"url": "https://github.com/0xHoneyJar/sigil.git",
|
|
53
|
+
"directory": "packages/hud"
|
|
54
|
+
},
|
|
55
|
+
"bugs": {
|
|
56
|
+
"url": "https://github.com/0xHoneyJar/sigil/issues"
|
|
57
|
+
},
|
|
58
|
+
"homepage": "https://github.com/0xHoneyJar/sigil/tree/main/packages/hud#readme",
|
|
59
|
+
"engines": {
|
|
60
|
+
"node": ">=20.0.0"
|
|
61
|
+
},
|
|
62
|
+
"scripts": {
|
|
63
|
+
"build": "tsup",
|
|
64
|
+
"dev": "tsup --watch",
|
|
65
|
+
"typecheck": "tsc --noEmit",
|
|
66
|
+
"clean": "rm -rf dist"
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Data Source Indicator Component
|
|
3
|
+
*
|
|
4
|
+
* Shows the source and staleness of displayed data values.
|
|
5
|
+
* Full implementation in TASK-202.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Data source type
|
|
10
|
+
*/
|
|
11
|
+
export type DataSourceType = 'on-chain' | 'indexed' | 'cached' | 'unknown'
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Props for DataSourceIndicator
|
|
15
|
+
*/
|
|
16
|
+
export interface DataSourceIndicatorProps {
|
|
17
|
+
/** Data source type */
|
|
18
|
+
source: DataSourceType
|
|
19
|
+
/** Block number for on-chain data */
|
|
20
|
+
blockNumber?: number
|
|
21
|
+
/** Current block for staleness calculation */
|
|
22
|
+
currentBlock?: number
|
|
23
|
+
/** Timestamp for time-based staleness */
|
|
24
|
+
timestamp?: number
|
|
25
|
+
/** Whether to show expanded details */
|
|
26
|
+
expanded?: boolean
|
|
27
|
+
/** Callback when clicked */
|
|
28
|
+
onClick?: () => void
|
|
29
|
+
/** Custom class name */
|
|
30
|
+
className?: string
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Source type colors and labels
|
|
35
|
+
*/
|
|
36
|
+
const sourceConfig: Record<
|
|
37
|
+
DataSourceType,
|
|
38
|
+
{ label: string; color: string; bgColor: string }
|
|
39
|
+
> = {
|
|
40
|
+
'on-chain': { label: 'On-Chain', color: '#22c55e', bgColor: 'rgba(34, 197, 94, 0.1)' },
|
|
41
|
+
indexed: { label: 'Indexed', color: '#3b82f6', bgColor: 'rgba(59, 130, 246, 0.1)' },
|
|
42
|
+
cached: { label: 'Cached', color: '#eab308', bgColor: 'rgba(234, 179, 8, 0.1)' },
|
|
43
|
+
unknown: { label: 'Unknown', color: '#666', bgColor: 'rgba(100, 100, 100, 0.1)' },
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Calculate staleness status
|
|
48
|
+
*/
|
|
49
|
+
function getStaleness(props: DataSourceIndicatorProps): {
|
|
50
|
+
level: 'fresh' | 'stale' | 'very-stale'
|
|
51
|
+
label: string
|
|
52
|
+
} {
|
|
53
|
+
const { source, blockNumber, currentBlock, timestamp } = props
|
|
54
|
+
|
|
55
|
+
// Block-based staleness for on-chain data
|
|
56
|
+
if (source === 'on-chain' && blockNumber && currentBlock) {
|
|
57
|
+
const blocksBehind = currentBlock - blockNumber
|
|
58
|
+
if (blocksBehind <= 1) return { level: 'fresh', label: 'current' }
|
|
59
|
+
if (blocksBehind <= 5) return { level: 'stale', label: `${blocksBehind} blocks behind` }
|
|
60
|
+
return { level: 'very-stale', label: `${blocksBehind} blocks behind` }
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Time-based staleness for indexed/cached
|
|
64
|
+
if (timestamp) {
|
|
65
|
+
const secondsAgo = Math.floor((Date.now() - timestamp) / 1000)
|
|
66
|
+
if (secondsAgo < 30) return { level: 'fresh', label: 'just now' }
|
|
67
|
+
if (secondsAgo < 60) return { level: 'fresh', label: `${secondsAgo}s ago` }
|
|
68
|
+
if (secondsAgo < 300) return { level: 'stale', label: `${Math.floor(secondsAgo / 60)}m ago` }
|
|
69
|
+
return { level: 'very-stale', label: `${Math.floor(secondsAgo / 60)}m ago` }
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return { level: 'fresh', label: '' }
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Staleness color mapping
|
|
77
|
+
*/
|
|
78
|
+
const stalenessColors = {
|
|
79
|
+
fresh: '#22c55e',
|
|
80
|
+
stale: '#eab308',
|
|
81
|
+
'very-stale': '#ef4444',
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Data Source Indicator component
|
|
86
|
+
*/
|
|
87
|
+
export function DataSourceIndicator({
|
|
88
|
+
source,
|
|
89
|
+
blockNumber,
|
|
90
|
+
currentBlock,
|
|
91
|
+
timestamp,
|
|
92
|
+
expanded = false,
|
|
93
|
+
onClick,
|
|
94
|
+
className = '',
|
|
95
|
+
}: DataSourceIndicatorProps) {
|
|
96
|
+
const config = sourceConfig[source]
|
|
97
|
+
const staleness = getStaleness({ source, blockNumber, currentBlock, timestamp })
|
|
98
|
+
|
|
99
|
+
return (
|
|
100
|
+
<button
|
|
101
|
+
onClick={onClick}
|
|
102
|
+
className={className}
|
|
103
|
+
style={{
|
|
104
|
+
...styles.container,
|
|
105
|
+
cursor: onClick ? 'pointer' : 'default',
|
|
106
|
+
}}
|
|
107
|
+
>
|
|
108
|
+
{/* Source Badge */}
|
|
109
|
+
<span
|
|
110
|
+
style={{
|
|
111
|
+
...styles.badge,
|
|
112
|
+
backgroundColor: config.bgColor,
|
|
113
|
+
borderColor: `${config.color}40`,
|
|
114
|
+
color: config.color,
|
|
115
|
+
}}
|
|
116
|
+
>
|
|
117
|
+
{config.label}
|
|
118
|
+
</span>
|
|
119
|
+
|
|
120
|
+
{/* Staleness Indicator */}
|
|
121
|
+
{staleness.label && (
|
|
122
|
+
<span style={{ ...styles.staleness, color: stalenessColors[staleness.level] }}>
|
|
123
|
+
<span style={styles.dot}>●</span>
|
|
124
|
+
{staleness.label}
|
|
125
|
+
</span>
|
|
126
|
+
)}
|
|
127
|
+
|
|
128
|
+
{/* Expanded Details */}
|
|
129
|
+
{expanded && blockNumber && (
|
|
130
|
+
<div style={styles.expanded}>
|
|
131
|
+
<span style={styles.detailLabel}>Block:</span>
|
|
132
|
+
<span style={styles.detailValue}>#{blockNumber.toLocaleString()}</span>
|
|
133
|
+
</div>
|
|
134
|
+
)}
|
|
135
|
+
</button>
|
|
136
|
+
)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Styles
|
|
141
|
+
*/
|
|
142
|
+
const styles: Record<string, React.CSSProperties> = {
|
|
143
|
+
container: {
|
|
144
|
+
display: 'inline-flex',
|
|
145
|
+
alignItems: 'center',
|
|
146
|
+
gap: '6px',
|
|
147
|
+
padding: '0',
|
|
148
|
+
backgroundColor: 'transparent',
|
|
149
|
+
border: 'none',
|
|
150
|
+
fontSize: '10px',
|
|
151
|
+
fontFamily: 'inherit',
|
|
152
|
+
},
|
|
153
|
+
badge: {
|
|
154
|
+
padding: '2px 6px',
|
|
155
|
+
borderRadius: '4px',
|
|
156
|
+
fontSize: '9px',
|
|
157
|
+
fontWeight: 600,
|
|
158
|
+
border: '1px solid',
|
|
159
|
+
textTransform: 'uppercase',
|
|
160
|
+
letterSpacing: '0.3px',
|
|
161
|
+
},
|
|
162
|
+
staleness: {
|
|
163
|
+
display: 'flex',
|
|
164
|
+
alignItems: 'center',
|
|
165
|
+
gap: '3px',
|
|
166
|
+
fontSize: '10px',
|
|
167
|
+
},
|
|
168
|
+
dot: {
|
|
169
|
+
fontSize: '6px',
|
|
170
|
+
},
|
|
171
|
+
expanded: {
|
|
172
|
+
display: 'flex',
|
|
173
|
+
gap: '4px',
|
|
174
|
+
marginLeft: '4px',
|
|
175
|
+
paddingLeft: '4px',
|
|
176
|
+
borderLeft: '1px solid rgba(255, 255, 255, 0.1)',
|
|
177
|
+
},
|
|
178
|
+
detailLabel: {
|
|
179
|
+
color: '#666',
|
|
180
|
+
},
|
|
181
|
+
detailValue: {
|
|
182
|
+
color: '#888',
|
|
183
|
+
fontFamily: 'monospace',
|
|
184
|
+
},
|
|
185
|
+
}
|
|
@@ -0,0 +1,444 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Diagnostics Panel Component
|
|
3
|
+
*
|
|
4
|
+
* Panel for physics compliance checking and issue detection.
|
|
5
|
+
* Implements TASK-201 requirements:
|
|
6
|
+
* - Physics analysis display
|
|
7
|
+
* - Data source indicators
|
|
8
|
+
* - Issue detection
|
|
9
|
+
* - Quick action buttons
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { useState, useCallback, useEffect } from 'react'
|
|
13
|
+
import { useHud } from '../providers/HudProvider'
|
|
14
|
+
import { PhysicsAnalysis } from './PhysicsAnalysis'
|
|
15
|
+
import { IssueList } from './IssueList'
|
|
16
|
+
import { DataSourceIndicator } from './DataSourceIndicator'
|
|
17
|
+
import type { EffectType, ComplianceResult, DiagnosticIssue } from '../types'
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Props for DiagnosticsPanel
|
|
21
|
+
*/
|
|
22
|
+
export interface DiagnosticsPanelProps {
|
|
23
|
+
/** Custom class name */
|
|
24
|
+
className?: string
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Current analysis state
|
|
29
|
+
*/
|
|
30
|
+
interface AnalysisState {
|
|
31
|
+
component: string | null
|
|
32
|
+
effect: EffectType | null
|
|
33
|
+
compliance: ComplianceResult | null
|
|
34
|
+
issues: DiagnosticIssue[]
|
|
35
|
+
isLoading: boolean
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const initialAnalysisState: AnalysisState = {
|
|
39
|
+
component: null,
|
|
40
|
+
effect: null,
|
|
41
|
+
compliance: null,
|
|
42
|
+
issues: [],
|
|
43
|
+
isLoading: false,
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Diagnostics panel for physics analysis
|
|
48
|
+
*/
|
|
49
|
+
export function DiagnosticsPanel({ className = '' }: DiagnosticsPanelProps) {
|
|
50
|
+
const { diagnosticsService, activePanel, config } = useHud()
|
|
51
|
+
const [analysis, setAnalysis] = useState<AnalysisState>(initialAnalysisState)
|
|
52
|
+
const [symptom, setSymptom] = useState('')
|
|
53
|
+
const [diagnosis, setDiagnosis] = useState<string | null>(null)
|
|
54
|
+
const [isLoading, setIsLoading] = useState(false)
|
|
55
|
+
const [showSymptomInput, setShowSymptomInput] = useState(false)
|
|
56
|
+
|
|
57
|
+
// Analyze component when selected (placeholder - could be wired to component selection)
|
|
58
|
+
const analyzeComponent = useCallback(
|
|
59
|
+
async (componentName: string, code?: string) => {
|
|
60
|
+
if (!diagnosticsService) return
|
|
61
|
+
|
|
62
|
+
setAnalysis((prev) => ({ ...prev, isLoading: true }))
|
|
63
|
+
try {
|
|
64
|
+
const result = await diagnosticsService.analyze(componentName, code)
|
|
65
|
+
setAnalysis({
|
|
66
|
+
component: result.component,
|
|
67
|
+
effect: result.effect,
|
|
68
|
+
compliance: result.compliance,
|
|
69
|
+
issues: result.issues,
|
|
70
|
+
isLoading: false,
|
|
71
|
+
})
|
|
72
|
+
} catch (error) {
|
|
73
|
+
setAnalysis({
|
|
74
|
+
...initialAnalysisState,
|
|
75
|
+
isLoading: false,
|
|
76
|
+
})
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
[diagnosticsService]
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
// Handle diagnose symptom
|
|
83
|
+
const handleDiagnose = useCallback(async () => {
|
|
84
|
+
if (!diagnosticsService || !symptom) return
|
|
85
|
+
|
|
86
|
+
setIsLoading(true)
|
|
87
|
+
try {
|
|
88
|
+
const result = diagnosticsService.diagnose(symptom)
|
|
89
|
+
setDiagnosis(result)
|
|
90
|
+
} catch (error) {
|
|
91
|
+
setDiagnosis(`Error: ${error instanceof Error ? error.message : 'Unknown error'}`)
|
|
92
|
+
} finally {
|
|
93
|
+
setIsLoading(false)
|
|
94
|
+
}
|
|
95
|
+
}, [diagnosticsService, symptom])
|
|
96
|
+
|
|
97
|
+
// Handle clear
|
|
98
|
+
const handleClear = useCallback(() => {
|
|
99
|
+
setSymptom('')
|
|
100
|
+
setDiagnosis(null)
|
|
101
|
+
setShowSymptomInput(false)
|
|
102
|
+
}, [])
|
|
103
|
+
|
|
104
|
+
// Handle capture observation (quick action)
|
|
105
|
+
const handleCaptureObservation = useCallback(() => {
|
|
106
|
+
// Trigger observation capture modal via keyboard shortcut simulation
|
|
107
|
+
const event = new KeyboardEvent('keydown', {
|
|
108
|
+
key: 'o',
|
|
109
|
+
metaKey: true,
|
|
110
|
+
shiftKey: true,
|
|
111
|
+
})
|
|
112
|
+
window.dispatchEvent(event)
|
|
113
|
+
}, [])
|
|
114
|
+
|
|
115
|
+
// Handle record signal (quick action)
|
|
116
|
+
const handleRecordSignal = useCallback(() => {
|
|
117
|
+
// TODO: Implement signal recording - TASK-204
|
|
118
|
+
console.log('Record signal triggered')
|
|
119
|
+
}, [])
|
|
120
|
+
|
|
121
|
+
// Handle issue click
|
|
122
|
+
const handleIssueClick = useCallback((issue: DiagnosticIssue) => {
|
|
123
|
+
// Could open a detail panel or link to docs
|
|
124
|
+
console.log('Issue clicked:', issue.code)
|
|
125
|
+
}, [])
|
|
126
|
+
|
|
127
|
+
// Don't render if not the active panel
|
|
128
|
+
if (activePanel !== 'diagnostics') return null
|
|
129
|
+
|
|
130
|
+
// Show message if diagnostics service not available
|
|
131
|
+
if (!diagnosticsService) {
|
|
132
|
+
return (
|
|
133
|
+
<div className={className} style={styles.container}>
|
|
134
|
+
<div style={styles.header}>
|
|
135
|
+
<span style={styles.title}>Diagnostics</span>
|
|
136
|
+
</div>
|
|
137
|
+
<div style={styles.unavailable}>
|
|
138
|
+
<p style={styles.unavailableText}>Diagnostics service not available.</p>
|
|
139
|
+
<p style={styles.unavailableHint}>
|
|
140
|
+
Install @sigil/diagnostics to enable physics analysis.
|
|
141
|
+
</p>
|
|
142
|
+
</div>
|
|
143
|
+
</div>
|
|
144
|
+
)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return (
|
|
148
|
+
<div className={className} style={styles.container}>
|
|
149
|
+
{/* Header with Quick Actions */}
|
|
150
|
+
<div style={styles.header}>
|
|
151
|
+
<span style={styles.title}>Diagnostics</span>
|
|
152
|
+
<div style={styles.quickActions}>
|
|
153
|
+
{config.observationCapture && (
|
|
154
|
+
<button
|
|
155
|
+
onClick={handleCaptureObservation}
|
|
156
|
+
style={styles.quickActionButton}
|
|
157
|
+
title="Capture Observation (Cmd+Shift+O)"
|
|
158
|
+
>
|
|
159
|
+
📸
|
|
160
|
+
</button>
|
|
161
|
+
)}
|
|
162
|
+
{config.signalCapture && (
|
|
163
|
+
<button
|
|
164
|
+
onClick={handleRecordSignal}
|
|
165
|
+
style={styles.quickActionButton}
|
|
166
|
+
title="Record Signal"
|
|
167
|
+
>
|
|
168
|
+
📡
|
|
169
|
+
</button>
|
|
170
|
+
)}
|
|
171
|
+
</div>
|
|
172
|
+
</div>
|
|
173
|
+
|
|
174
|
+
{/* Physics Analysis Section */}
|
|
175
|
+
<PhysicsAnalysis
|
|
176
|
+
effect={analysis.effect}
|
|
177
|
+
compliance={analysis.compliance}
|
|
178
|
+
isLoading={analysis.isLoading}
|
|
179
|
+
/>
|
|
180
|
+
|
|
181
|
+
{/* Data Source Indicator (placeholder for TASK-202) */}
|
|
182
|
+
<div style={styles.section}>
|
|
183
|
+
<span style={styles.sectionLabel}>Data Sources</span>
|
|
184
|
+
<div style={styles.dataSourceRow}>
|
|
185
|
+
<DataSourceIndicator source="on-chain" blockNumber={19234567} currentBlock={19234569} />
|
|
186
|
+
</div>
|
|
187
|
+
</div>
|
|
188
|
+
|
|
189
|
+
{/* Issues Section */}
|
|
190
|
+
<div style={styles.section}>
|
|
191
|
+
<IssueList
|
|
192
|
+
issues={analysis.issues}
|
|
193
|
+
onIssueClick={handleIssueClick}
|
|
194
|
+
maxVisible={3}
|
|
195
|
+
/>
|
|
196
|
+
</div>
|
|
197
|
+
|
|
198
|
+
{/* Symptom Input (toggleable) */}
|
|
199
|
+
{!showSymptomInput ? (
|
|
200
|
+
<button
|
|
201
|
+
onClick={() => setShowSymptomInput(true)}
|
|
202
|
+
style={styles.showDiagnoseButton}
|
|
203
|
+
>
|
|
204
|
+
+ Describe a symptom to diagnose
|
|
205
|
+
</button>
|
|
206
|
+
) : (
|
|
207
|
+
<div style={styles.diagnoseSection}>
|
|
208
|
+
<label style={styles.inputLabel}>Describe the issue or symptom</label>
|
|
209
|
+
<textarea
|
|
210
|
+
value={symptom}
|
|
211
|
+
onChange={(e) => setSymptom(e.target.value)}
|
|
212
|
+
placeholder="e.g., Dialog flickers on open, Hydration mismatch, Button feels slow..."
|
|
213
|
+
rows={2}
|
|
214
|
+
style={styles.textarea}
|
|
215
|
+
/>
|
|
216
|
+
<div style={styles.diagnoseActions}>
|
|
217
|
+
<button
|
|
218
|
+
onClick={handleDiagnose}
|
|
219
|
+
disabled={!symptom || isLoading}
|
|
220
|
+
style={{
|
|
221
|
+
...styles.diagnoseButton,
|
|
222
|
+
opacity: symptom ? 1 : 0.5,
|
|
223
|
+
cursor: symptom ? 'pointer' : 'not-allowed',
|
|
224
|
+
}}
|
|
225
|
+
>
|
|
226
|
+
{isLoading ? 'Analyzing...' : 'Diagnose'}
|
|
227
|
+
</button>
|
|
228
|
+
<button onClick={handleClear} style={styles.cancelButton}>
|
|
229
|
+
Cancel
|
|
230
|
+
</button>
|
|
231
|
+
</div>
|
|
232
|
+
</div>
|
|
233
|
+
)}
|
|
234
|
+
|
|
235
|
+
{/* Diagnosis Result */}
|
|
236
|
+
{diagnosis && (
|
|
237
|
+
<div style={styles.diagnosisResult}>
|
|
238
|
+
<div
|
|
239
|
+
style={styles.diagnosisContent}
|
|
240
|
+
dangerouslySetInnerHTML={{
|
|
241
|
+
__html: formatDiagnosis(diagnosis),
|
|
242
|
+
}}
|
|
243
|
+
/>
|
|
244
|
+
</div>
|
|
245
|
+
)}
|
|
246
|
+
|
|
247
|
+
{/* Quick Diagnose Buttons */}
|
|
248
|
+
<div style={styles.quickDiagnose}>
|
|
249
|
+
<span style={styles.quickDiagnoseLabel}>Quick Diagnose</span>
|
|
250
|
+
<div style={styles.quickDiagnoseGrid}>
|
|
251
|
+
{['hydration mismatch', 'dialog glitch', 'slow performance', 'layout shift'].map(
|
|
252
|
+
(quick) => (
|
|
253
|
+
<button
|
|
254
|
+
key={quick}
|
|
255
|
+
onClick={() => {
|
|
256
|
+
setSymptom(quick)
|
|
257
|
+
setShowSymptomInput(true)
|
|
258
|
+
}}
|
|
259
|
+
style={styles.quickDiagnoseButton}
|
|
260
|
+
>
|
|
261
|
+
{quick}
|
|
262
|
+
</button>
|
|
263
|
+
)
|
|
264
|
+
)}
|
|
265
|
+
</div>
|
|
266
|
+
</div>
|
|
267
|
+
</div>
|
|
268
|
+
)
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Format diagnosis for display (simple markdown-like formatting)
|
|
273
|
+
*/
|
|
274
|
+
function formatDiagnosis(text: string): string {
|
|
275
|
+
return text
|
|
276
|
+
.replace(/\*\*(.*?)\*\*/g, '<strong style="color: #10b981">$1</strong>')
|
|
277
|
+
.replace(
|
|
278
|
+
/`([^`]+)`/g,
|
|
279
|
+
'<code style="background: rgba(255,255,255,0.1); padding: 2px 4px; border-radius: 2px;">$1</code>'
|
|
280
|
+
)
|
|
281
|
+
.replace(
|
|
282
|
+
/```(\w+)?\n([\s\S]*?)```/g,
|
|
283
|
+
'<pre style="background: rgba(0,0,0,0.3); padding: 8px; border-radius: 4px; overflow-x: auto; margin: 8px 0;">$2</pre>'
|
|
284
|
+
)
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Styles
|
|
289
|
+
*/
|
|
290
|
+
const styles: Record<string, React.CSSProperties> = {
|
|
291
|
+
container: {
|
|
292
|
+
display: 'flex',
|
|
293
|
+
flexDirection: 'column',
|
|
294
|
+
gap: '16px',
|
|
295
|
+
},
|
|
296
|
+
header: {
|
|
297
|
+
display: 'flex',
|
|
298
|
+
justifyContent: 'space-between',
|
|
299
|
+
alignItems: 'center',
|
|
300
|
+
},
|
|
301
|
+
title: {
|
|
302
|
+
fontSize: '12px',
|
|
303
|
+
fontWeight: 600,
|
|
304
|
+
color: '#fff',
|
|
305
|
+
},
|
|
306
|
+
quickActions: {
|
|
307
|
+
display: 'flex',
|
|
308
|
+
gap: '4px',
|
|
309
|
+
},
|
|
310
|
+
quickActionButton: {
|
|
311
|
+
width: '28px',
|
|
312
|
+
height: '28px',
|
|
313
|
+
display: 'flex',
|
|
314
|
+
alignItems: 'center',
|
|
315
|
+
justifyContent: 'center',
|
|
316
|
+
backgroundColor: 'rgba(255, 255, 255, 0.05)',
|
|
317
|
+
border: '1px solid rgba(255, 255, 255, 0.1)',
|
|
318
|
+
borderRadius: '4px',
|
|
319
|
+
cursor: 'pointer',
|
|
320
|
+
fontSize: '12px',
|
|
321
|
+
transition: 'background-color 0.15s ease-out',
|
|
322
|
+
},
|
|
323
|
+
unavailable: {
|
|
324
|
+
padding: '16px',
|
|
325
|
+
textAlign: 'center',
|
|
326
|
+
},
|
|
327
|
+
unavailableText: {
|
|
328
|
+
color: '#666',
|
|
329
|
+
fontSize: '11px',
|
|
330
|
+
margin: 0,
|
|
331
|
+
},
|
|
332
|
+
unavailableHint: {
|
|
333
|
+
color: '#555',
|
|
334
|
+
fontSize: '10px',
|
|
335
|
+
marginTop: '4px',
|
|
336
|
+
},
|
|
337
|
+
section: {
|
|
338
|
+
display: 'flex',
|
|
339
|
+
flexDirection: 'column',
|
|
340
|
+
gap: '8px',
|
|
341
|
+
},
|
|
342
|
+
sectionLabel: {
|
|
343
|
+
fontSize: '10px',
|
|
344
|
+
fontWeight: 600,
|
|
345
|
+
color: '#888',
|
|
346
|
+
textTransform: 'uppercase',
|
|
347
|
+
letterSpacing: '0.5px',
|
|
348
|
+
},
|
|
349
|
+
dataSourceRow: {
|
|
350
|
+
display: 'flex',
|
|
351
|
+
alignItems: 'center',
|
|
352
|
+
gap: '8px',
|
|
353
|
+
},
|
|
354
|
+
showDiagnoseButton: {
|
|
355
|
+
width: '100%',
|
|
356
|
+
padding: '8px 12px',
|
|
357
|
+
backgroundColor: 'rgba(255, 255, 255, 0.02)',
|
|
358
|
+
border: '1px dashed rgba(255, 255, 255, 0.1)',
|
|
359
|
+
borderRadius: '4px',
|
|
360
|
+
color: '#666',
|
|
361
|
+
fontSize: '11px',
|
|
362
|
+
cursor: 'pointer',
|
|
363
|
+
textAlign: 'left',
|
|
364
|
+
},
|
|
365
|
+
diagnoseSection: {
|
|
366
|
+
display: 'flex',
|
|
367
|
+
flexDirection: 'column',
|
|
368
|
+
gap: '8px',
|
|
369
|
+
},
|
|
370
|
+
inputLabel: {
|
|
371
|
+
fontSize: '10px',
|
|
372
|
+
color: '#888',
|
|
373
|
+
},
|
|
374
|
+
textarea: {
|
|
375
|
+
width: '100%',
|
|
376
|
+
padding: '8px',
|
|
377
|
+
backgroundColor: 'rgba(255, 255, 255, 0.05)',
|
|
378
|
+
border: '1px solid rgba(255, 255, 255, 0.1)',
|
|
379
|
+
borderRadius: '4px',
|
|
380
|
+
color: '#fff',
|
|
381
|
+
fontSize: '11px',
|
|
382
|
+
fontFamily: 'inherit',
|
|
383
|
+
resize: 'vertical',
|
|
384
|
+
},
|
|
385
|
+
diagnoseActions: {
|
|
386
|
+
display: 'flex',
|
|
387
|
+
gap: '8px',
|
|
388
|
+
},
|
|
389
|
+
diagnoseButton: {
|
|
390
|
+
flex: 1,
|
|
391
|
+
padding: '6px 12px',
|
|
392
|
+
backgroundColor: 'rgba(16, 185, 129, 0.2)',
|
|
393
|
+
border: '1px solid rgba(16, 185, 129, 0.3)',
|
|
394
|
+
borderRadius: '4px',
|
|
395
|
+
color: '#10b981',
|
|
396
|
+
fontSize: '11px',
|
|
397
|
+
cursor: 'pointer',
|
|
398
|
+
},
|
|
399
|
+
cancelButton: {
|
|
400
|
+
padding: '6px 12px',
|
|
401
|
+
backgroundColor: 'rgba(255, 255, 255, 0.05)',
|
|
402
|
+
border: '1px solid rgba(255, 255, 255, 0.1)',
|
|
403
|
+
borderRadius: '4px',
|
|
404
|
+
color: '#888',
|
|
405
|
+
fontSize: '11px',
|
|
406
|
+
cursor: 'pointer',
|
|
407
|
+
},
|
|
408
|
+
diagnosisResult: {
|
|
409
|
+
padding: '12px',
|
|
410
|
+
backgroundColor: 'rgba(16, 185, 129, 0.05)',
|
|
411
|
+
border: '1px solid rgba(16, 185, 129, 0.2)',
|
|
412
|
+
borderRadius: '4px',
|
|
413
|
+
},
|
|
414
|
+
diagnosisContent: {
|
|
415
|
+
fontSize: '11px',
|
|
416
|
+
color: '#fff',
|
|
417
|
+
whiteSpace: 'pre-wrap',
|
|
418
|
+
lineHeight: 1.6,
|
|
419
|
+
},
|
|
420
|
+
quickDiagnose: {
|
|
421
|
+
display: 'flex',
|
|
422
|
+
flexDirection: 'column',
|
|
423
|
+
gap: '8px',
|
|
424
|
+
},
|
|
425
|
+
quickDiagnoseLabel: {
|
|
426
|
+
fontSize: '10px',
|
|
427
|
+
color: '#888',
|
|
428
|
+
},
|
|
429
|
+
quickDiagnoseGrid: {
|
|
430
|
+
display: 'grid',
|
|
431
|
+
gridTemplateColumns: 'repeat(2, 1fr)',
|
|
432
|
+
gap: '4px',
|
|
433
|
+
},
|
|
434
|
+
quickDiagnoseButton: {
|
|
435
|
+
padding: '6px 8px',
|
|
436
|
+
backgroundColor: 'rgba(255, 255, 255, 0.02)',
|
|
437
|
+
border: '1px solid rgba(255, 255, 255, 0.05)',
|
|
438
|
+
borderRadius: '4px',
|
|
439
|
+
color: '#888',
|
|
440
|
+
fontSize: '10px',
|
|
441
|
+
cursor: 'pointer',
|
|
442
|
+
textAlign: 'center',
|
|
443
|
+
},
|
|
444
|
+
}
|