+ {/* City input for wellness widgets */}
+ {['run-suitability', 'health-stats'].includes(widgetType) && (
+
+
+ setCity(e.target.value)}
+ />
+
+
+ )}
+
-
-
- {selectedDevices.length === 0 ? (
-
- Please select a device first
-
- ) : (
- <>
- {(['stat', 'gauge'].includes(widgetType)) && (
-
-
-
This widget type supports only one metric
-
- )}
-
- {availableMetrics.length === 0 ? (
-
- No metrics found for this device
-
- ) : (
- availableMetrics.map((metric) => (
-
- ))
- )}
+
+
+ {/* Metric Selection - Skip for widgets with hardcoded metrics */}
+ {!['run-suitability', 'health-stats'].includes(widgetType) && (
+
+
+ {selectedDevices.length === 0 ? (
+
+ Please select a device first
- >
- )}
-
+ ) : (
+ <>
+ {(['stat', 'gauge'].includes(widgetType)) && (
+
+
+
This widget type supports only one metric
+
+ )}
+
+ {availableMetrics.length === 0 ? (
+
+ No metrics found for this device
+
+ ) : (
+ availableMetrics.map((metric) => (
+
+ ))
+ )}
+
+ >
+ )}
+
+ )}
@@ -402,6 +536,194 @@ export default function AddWidgetModal({ isOpen, onClose, onAdd }: AddWidgetModa
/>
>
+ ) : widgetType === 'calendar' ? (
+ <>
+
+
+
+ setCalendarUrl(e.target.value)}
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+ setTitle(e.target.value)}
+ />
+
+ >
+ ) : widgetType === 'daily-briefing' ? (
+ <>
+
+
+
+
+
+
+
+
+
+
+
+
+ setCity(e.target.value)}
+ />
+
+
+ {(briefingType === 'schedule' || briefingType === 'full') && (
+ <>
+
+
+
+ setCalendarUrl(e.target.value)}
+ />
+
+
+
+
+
+ {calendarUrl && (
+
+
+
+
+ )}
+
+ {devices.length > 0 && (
+
+
+
+
+
+ )}
+ >
+ )}
+
+
+
+ setTitle(e.target.value)}
+ />
+
+ >
) : (
// Original configuration for sensor-based widgets
<>
@@ -478,14 +800,23 @@ export default function AddWidgetModal({ isOpen, onClose, onAdd }: AddWidgetModa
Widget Summary
Type: {widgetType}
-
Device: {devices.find(d => d.id === selectedDevices[0])?.name}
-
Metrics: {selectedMetrics.join(', ')}
+
Device: {needsDevice ? (devices.find(d => d.id === selectedDevices[0])?.name || 'Not selected') : 'Not required'}
+
Metrics: {needsMetrics ? (selectedMetrics.join(', ') || 'Not selected') : 'Not required'}
Size: {widgetWidth} × {widgetHeight}
+ {widgetType === 'calendar' && (
+
Range: Next {calendarRangeHours}h
+ )}
+ {widgetType === 'daily-briefing' && (
+
Briefing: {briefingType.charAt(0).toUpperCase() + briefingType.slice(1)}
+ )}
-
- {(['stat', 'gauge'].includes(widget.type)) && (
-
-
-
This widget type supports only one metric
-
- )}
-
- {availableMetrics.length === 0 ? (
-
- Loading metrics...
+ {!['weather', 'air-quality', 'run-suitability', 'health-stats', 'calendar', 'daily-briefing'].includes(widget.type) && (
+
+
+ {(['stat', 'gauge'].includes(widget.type)) && (
+
+
+
This widget type supports only one metric
- ) : (
- availableMetrics.map((metric) => (
-
- ))
)}
+
+ {!selectedDeviceId ? (
+
+ Please select a device first
+
+ ) : availableMetrics.length === 0 ? (
+
+ Loading metrics...
+
+ ) : (
+ availableMetrics.map((metric) => (
+
+ ))
+ )}
+
-
+ )}
{/* Time Range */}
-
-
-
-
+ {!['calendar', 'daily-briefing'].includes(widget.type) && (
+
+
+
+
+ )}
+
+ {widget.type === 'calendar' && (
+ <>
+
+
+
+ setCalendarUrl(e.target.value)}
+ />
+
+
+
+
+
+
+
+
+ >
+ )}
+
+ {widget.type === 'daily-briefing' && (
+ <>
+
+
+
+
+
+
+
+
+
+
+
+ setCity(e.target.value)}
+ />
+
+
+ {(briefingType === 'schedule' || briefingType === 'full') && (
+ <>
+
+
+
+ setCalendarUrl(e.target.value)}
+ />
+
+
+
+
+ {calendarUrl && (
+
+
+
+
+ )}
+
+ {devices.length > 0 && (
+
+
+
+
+ )}
+ >
+ )}
+ >
+ )}
{/* Size */}
diff --git a/frontend/src/components/widgets/AiInsightWidget.tsx b/frontend/src/components/widgets/AiInsightWidget.tsx
index 31b9837..bf28216 100644
--- a/frontend/src/components/widgets/AiInsightWidget.tsx
+++ b/frontend/src/components/widgets/AiInsightWidget.tsx
@@ -7,6 +7,38 @@ interface AiInsightWidgetProps {
config: WidgetConfig
}
+interface TrendSummary {
+ status: 'excellent' | 'good' | 'fair' | 'poor'
+ summary: string
+ trends: Array<{
+ metric: string
+ direction: 'improving' | 'stable' | 'degrading'
+ description: string
+ }>
+ comfort_score: {
+ rating: number
+ description: string
+ }
+ patterns: string[]
+ recommendations: string[]
+ forecast: string
+}
+
+interface AnomalyDetection {
+ status: 'normal' | 'warning' | 'critical'
+ summary: string
+ anomalies: Array<{
+ metric: string
+ severity: 'low' | 'medium' | 'high' | 'critical'
+ description: string
+ value: string
+ expected: string
+ }>
+ impacts: string[]
+ actions: string[]
+ root_causes: string[]
+}
+
export default function AiInsightWidget({ config }: AiInsightWidgetProps) {
const { deviceIds, metricIds, timeframe, title } = config
const [promptType, setPromptType] = useState<'trend_summary' | 'anomaly_detection'>('trend_summary')
@@ -40,6 +72,52 @@ export default function AiInsightWidget({ config }: AiInsightWidgetProps) {
refetch()
}
+ // Parse JSON analysis if it's a string
+ const parsedAnalysis = analysis?.analysis ? (() => {
+ try {
+ return typeof analysis.analysis === 'string'
+ ? JSON.parse(analysis.analysis)
+ : analysis.analysis
+ } catch {
+ return null // If parsing fails, return null to show raw text
+ }
+ })() : null
+
+ const isTrendSummary = promptType === 'trend_summary' && parsedAnalysis
+ const isAnomalyDetection = promptType === 'anomaly_detection' && parsedAnalysis
+
+ const getStatusColor = (status: string) => {
+ switch (status) {
+ case 'excellent': return 'badge-success'
+ case 'good': return 'badge-info'
+ case 'fair': return 'badge-warning'
+ case 'poor': return 'badge-error'
+ case 'normal': return 'badge-success'
+ case 'warning': return 'badge-warning'
+ case 'critical': return 'badge-error'
+ default: return 'badge-ghost'
+ }
+ }
+
+ const getDirectionIcon = (direction: string) => {
+ switch (direction) {
+ case 'improving': return '↑'
+ case 'degrading': return '↓'
+ case 'stable': return '→'
+ default: return '•'
+ }
+ }
+
+ const getSeverityColor = (severity: string) => {
+ switch (severity) {
+ case 'critical': return 'badge-error'
+ case 'high': return 'badge-warning'
+ case 'medium': return 'badge-warning badge-outline'
+ case 'low': return 'badge-info'
+ default: return 'badge-ghost'
+ }
+ }
+
return (
@@ -99,20 +177,239 @@ export default function AiInsightWidget({ config }: AiInsightWidgetProps) {
{analysis && showAnalysis && !isLoading && (
-
-
- {promptType === 'trend_summary' ? 'Trend Analysis' : 'Anomaly Detection'}
-
-
- {analysis.data_points_analyzed} data points analyzed
-
-
-
-
-
- {analysis.analysis}
-
-
+ {/* Structured Display for Trend Summary */}
+ {isTrendSummary && parsedAnalysis && (
+ <>
+
+
+ {parsedAnalysis.status.toUpperCase()}
+
+
+ {analysis.data_points_analyzed} data points
+
+
+
+ {/* Summary */}
+
+
+
+
Summary
+
{parsedAnalysis.summary}
+
+
+
+ {/* Comfort Score */}
+ {parsedAnalysis.comfort_score && (
+
+
+
+
+ {parsedAnalysis.comfort_score.rating}
+
+
+
Comfort Score
+
{parsedAnalysis.comfort_score.description}
+
+
+
+
+ )}
+
+ {/* Trends */}
+ {parsedAnalysis.trends && parsedAnalysis.trends.length > 0 && (
+
+
Trends
+
+ {parsedAnalysis.trends.map((trend: TrendSummary['trends'][0], i: number) => (
+
+
+
+
{getDirectionIcon(trend.direction)}
+
+
{trend.metric}
+
{trend.description}
+
+
{trend.direction}
+
+
+
+ ))}
+
+
+ )}
+
+ {/* Patterns */}
+ {parsedAnalysis.patterns && parsedAnalysis.patterns.length > 0 && (
+
+
Patterns Detected
+
+ {parsedAnalysis.patterns.map((pattern: string, i: number) => (
+ -
+ ▸
+ {pattern}
+
+ ))}
+
+
+ )}
+
+ {/* Recommendations */}
+ {parsedAnalysis.recommendations && parsedAnalysis.recommendations.length > 0 && (
+
+
Recommendations
+
+ {parsedAnalysis.recommendations.map((rec: string, i: number) => (
+
+ ))}
+
+
+ )}
+
+ {/* Forecast */}
+ {parsedAnalysis.forecast && (
+
+
+
+
Forecast
+
{parsedAnalysis.forecast}
+
+
+ )}
+ >
+ )}
+
+ {/* Structured Display for Anomaly Detection */}
+ {isAnomalyDetection && parsedAnalysis && (
+ <>
+
+
+ {parsedAnalysis.status.toUpperCase()}
+
+
+ {analysis.data_points_analyzed} data points
+
+
+
+ {/* Summary */}
+
+
+
+
Summary
+
{parsedAnalysis.summary}
+
+
+
+ {/* Anomalies */}
+ {parsedAnalysis.anomalies && parsedAnalysis.anomalies.length > 0 && (
+
+
Anomalies Detected
+
+ {parsedAnalysis.anomalies.map((anomaly: AnomalyDetection['anomalies'][0], i: number) => (
+
+
+
+
+
+ {anomaly.metric}
+
+ {anomaly.severity}
+
+
+
{anomaly.description}
+
+ Current: {anomaly.value}
+ {' • '}
+ Expected: {anomaly.expected}
+
+
+
+
+
+ ))}
+
+
+ )}
+
+ {/* Impacts */}
+ {parsedAnalysis.impacts && parsedAnalysis.impacts.length > 0 && (
+
+
Potential Impacts
+
+ {parsedAnalysis.impacts.map((impact: string, i: number) => (
+ -
+ ▸
+ {impact}
+
+ ))}
+
+
+ )}
+
+ {/* Actions */}
+ {parsedAnalysis.actions && parsedAnalysis.actions.length > 0 && (
+
+
Recommended Actions
+
+ {parsedAnalysis.actions.map((action: string, i: number) => (
+
+ ))}
+
+
+ )}
+
+ {/* Root Causes */}
+ {parsedAnalysis.root_causes && parsedAnalysis.root_causes.length > 0 && (
+
+
Possible Root Causes
+
+ {parsedAnalysis.root_causes.map((cause: string, i: number) => (
+ -
+ ▸
+ {cause}
+
+ ))}
+
+
+ )}
+ >
+ )}
+
+ {/* Fallback: Raw Text Display */}
+ {!parsedAnalysis && (
+ <>
+
+
+ {promptType === 'trend_summary' ? 'Trend Analysis' : 'Anomaly Detection'}
+
+
+ {analysis.data_points_analyzed} data points analyzed
+
+
+
+
+
+ {analysis.analysis}
+
+
+ >
+ )}
+