Safe mode for AI diagnostics, align alarm keys with device enums, and multi-diagnosis frontend
- Remove API key gate so knowledge base works without Mistral key - Return 204 No Content instead of 500 when no diagnosis available - Rewrite AlarmKnowledgeBase keys to match Sinexcel property names and Growatt enum names - Fix SQLite OrderBy crash in DiagnosticService - Frontend: show latest 3 unique alarms with independent expand/collapse and handle 204 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
0c918e86ae
commit
6460328eb0
|
|
@ -763,13 +763,10 @@ public class Controller : ControllerBase
|
||||||
installation.Product != (int)ProductType.SodiStoreMax)
|
installation.Product != (int)ProductType.SodiStoreMax)
|
||||||
return BadRequest("AI diagnostics not available for this product.");
|
return BadRequest("AI diagnostics not available for this product.");
|
||||||
|
|
||||||
if (!DiagnosticService.IsEnabled)
|
|
||||||
return StatusCode(503, "AI diagnostics not configured.");
|
|
||||||
|
|
||||||
var result = await DiagnosticService.DiagnoseAsync(installationId, errorDescription);
|
var result = await DiagnosticService.DiagnoseAsync(installationId, errorDescription);
|
||||||
|
|
||||||
if (result is null)
|
if (result is null)
|
||||||
return StatusCode(500, "Diagnosis failed – please try again later.");
|
return NoContent(); // no diagnosis available (not in knowledge base, no API key)
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
@ -784,16 +781,17 @@ public class Controller : ControllerBase
|
||||||
{
|
{
|
||||||
var testCases = new[]
|
var testCases = new[]
|
||||||
{
|
{
|
||||||
// Sinexcel alarms
|
// Sinexcel alarms (keys match SinexcelRecord.Api.cs property names)
|
||||||
"Fan fault",
|
"FanFault",
|
||||||
"Abnormal grid voltage",
|
"AbnormalGridVoltage",
|
||||||
"Battery 1not connected",
|
"Battery1NotConnected",
|
||||||
"Inverter power tube fault",
|
"InverterPowerTubeFault",
|
||||||
"Island protection",
|
"IslandProtection",
|
||||||
// Growatt alarms
|
// Growatt alarms (keys match GrowattWarningCode/GrowattErrorCode enum names)
|
||||||
"Warning 300",
|
"NoUtilityGrid",
|
||||||
"Warning 500",
|
"BatteryCommunicationFailure",
|
||||||
"Error 408",
|
"BmsFault",
|
||||||
|
"OverTemperature",
|
||||||
"AFCI Fault",
|
"AFCI Fault",
|
||||||
// Unknown alarm (should return null - would call Mistral)
|
// Unknown alarm (should return null - would call Mistral)
|
||||||
"Some unknown alarm XYZ123"
|
"Some unknown alarm XYZ123"
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -68,7 +68,9 @@ public static class DiagnosticService
|
||||||
|
|
||||||
var recentDescriptions = Db.Errors
|
var recentDescriptions = Db.Errors
|
||||||
.Where(e => e.InstallationId == installationId)
|
.Where(e => e.InstallationId == installationId)
|
||||||
.OrderByDescending(e => e.Date + " " + e.Time) // Date/Time stored as strings in DB
|
.OrderByDescending(e => e.Date)
|
||||||
|
.ThenByDescending(e => e.Time)
|
||||||
|
.ToList() // materialize before LINQ-to-objects ops
|
||||||
.Select(e => e.Description)
|
.Select(e => e.Description)
|
||||||
.Distinct() // deduplicate — same error repeated adds no signal
|
.Distinct() // deduplicate — same error repeated adds no signal
|
||||||
.Take(5)
|
.Take(5)
|
||||||
|
|
|
||||||
|
|
@ -47,10 +47,9 @@ function Log(props: LogProps) {
|
||||||
const tokencontext = useContext(TokenContext);
|
const tokencontext = useContext(TokenContext);
|
||||||
const { removeToken } = tokencontext;
|
const { removeToken } = tokencontext;
|
||||||
|
|
||||||
const [diagnosis, setDiagnosis] = useState<DiagnosticResponse | null>(null);
|
const [diagnoses, setDiagnoses] = useState<{ description: string; response: DiagnosticResponse }[]>([]);
|
||||||
const [diagnosisLoading, setDiagnosisLoading] = useState(false);
|
const [diagnosisLoading, setDiagnosisLoading] = useState(false);
|
||||||
const [diagnosisExpanded, setDiagnosisExpanded] = useState(false);
|
const [expandedDiagnoses, setExpandedDiagnoses] = useState<Set<number>>(new Set());
|
||||||
const [diagnosedError, setDiagnosedError] = useState<string>('');
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
axiosConfig
|
axiosConfig
|
||||||
|
|
@ -78,30 +77,48 @@ function Log(props: LogProps) {
|
||||||
});
|
});
|
||||||
}, [updateCount]);
|
}, [updateCount]);
|
||||||
|
|
||||||
// fetch AI diagnosis for the first unseen error (or warning if no unseen errors)
|
// fetch AI diagnosis for the latest 3 unique errors/warnings
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const target = errors.find(e => !e.seen) || warnings.find(w => !w.seen);
|
// combine errors and warnings, filter non-errors, sort by date+time descending, deduplicate
|
||||||
|
const ignore = new Set(['NoAlarm', '0', '']);
|
||||||
|
const all = [...errors, ...warnings]
|
||||||
|
.filter(item => !ignore.has(item.description.trim()))
|
||||||
|
.sort((a, b) => (b.date + ' ' + b.time).localeCompare(a.date + ' ' + a.time));
|
||||||
|
|
||||||
if (!target) {
|
const seen = new Set<string>();
|
||||||
setDiagnosis(null);
|
const targets: ErrorMessage[] = [];
|
||||||
setDiagnosedError('');
|
for (const item of all) {
|
||||||
|
if (!seen.has(item.description)) {
|
||||||
|
seen.add(item.description);
|
||||||
|
targets.push(item);
|
||||||
|
if (targets.length >= 3) break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (targets.length === 0) {
|
||||||
|
setDiagnoses([]);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// already have a diagnosis for this exact description — skip
|
// check if the targets changed compared to what we already have
|
||||||
if (target.description === diagnosedError && diagnosis) return;
|
const currentDescs = diagnoses.map(d => d.description).join('|');
|
||||||
|
const newDescs = targets.map(t => t.description).join('|');
|
||||||
|
if (currentDescs === newDescs) return;
|
||||||
|
|
||||||
setDiagnosisLoading(true);
|
setDiagnosisLoading(true);
|
||||||
|
Promise.all(
|
||||||
|
targets.map(target =>
|
||||||
axiosConfig
|
axiosConfig
|
||||||
.get(`/DiagnoseError?installationId=${props.id}&errorDescription=${encodeURIComponent(target.description)}`)
|
.get(`/DiagnoseError?installationId=${props.id}&errorDescription=${encodeURIComponent(target.description)}`)
|
||||||
.then((res: AxiosResponse<DiagnosticResponse>) => {
|
.then((res: AxiosResponse<DiagnosticResponse>) => {
|
||||||
setDiagnosis(res.data);
|
if (res.status === 204 || !res.data || !res.data.explanation) return null;
|
||||||
setDiagnosedError(target.description);
|
return { description: target.description, response: res.data };
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => null)
|
||||||
setDiagnosis(null);
|
)
|
||||||
})
|
).then(results => {
|
||||||
.finally(() => {
|
setDiagnoses(results.filter(r => r !== null) as { description: string; response: DiagnosticResponse }[]);
|
||||||
|
}).finally(() => {
|
||||||
setDiagnosisLoading(false);
|
setDiagnosisLoading(false);
|
||||||
});
|
});
|
||||||
}, [errors, warnings]);
|
}, [errors, warnings]);
|
||||||
|
|
@ -437,56 +454,61 @@ function Log(props: LogProps) {
|
||||||
<Container maxWidth="xl">
|
<Container maxWidth="xl">
|
||||||
<Grid container>
|
<Grid container>
|
||||||
|
|
||||||
{/* AI Diagnosis banner — shown when loading or a diagnosis is available */}
|
{/* AI Diagnosis banner — shown when loading or diagnoses are available */}
|
||||||
{(diagnosisLoading || diagnosis) && (
|
{diagnosisLoading && (
|
||||||
<Grid item xs={12} md={12}>
|
<Grid item xs={12} md={12}>
|
||||||
<Card sx={{ marginTop: '20px', borderLeft: '4px solid #1976d2' }}>
|
<Card sx={{ marginTop: '20px', borderLeft: '4px solid #1976d2' }}>
|
||||||
<Box sx={{ padding: '16px' }}>
|
<Box sx={{ padding: '16px', display: 'flex', alignItems: 'center', gap: '12px' }}>
|
||||||
|
|
||||||
{/* loading state */}
|
|
||||||
{diagnosisLoading && (
|
|
||||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
|
|
||||||
<CircularProgress size={22} />
|
<CircularProgress size={22} />
|
||||||
<Typography variant="body2" color="text.secondary">
|
<Typography variant="body2" color="text.secondary">
|
||||||
<FormattedMessage id="ai_analyzing" defaultMessage="AI is analyzing..." />
|
<FormattedMessage id="ai_analyzing" defaultMessage="AI is analyzing..." />
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
|
</Card>
|
||||||
|
</Grid>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* diagnosis result */}
|
{!diagnosisLoading && diagnoses.map((diag, idx) => {
|
||||||
{diagnosis && !diagnosisLoading && (
|
const isExpanded = expandedDiagnoses.has(idx);
|
||||||
<>
|
return (
|
||||||
|
<Grid item xs={12} md={12} key={idx}>
|
||||||
|
<Card sx={{ marginTop: idx === 0 ? '20px' : '10px', borderLeft: '4px solid #1976d2' }}>
|
||||||
|
<Box sx={{ padding: '16px' }}>
|
||||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: '8px', mb: 1 }}>
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: '8px', mb: 1 }}>
|
||||||
<Typography variant="subtitle2" fontWeight="bold" color="primary">
|
<Typography variant="subtitle2" fontWeight="bold" color="primary">
|
||||||
<FormattedMessage id="ai_diagnosis" defaultMessage="AI Diagnosis" />
|
<FormattedMessage id="ai_diagnosis" defaultMessage="AI Diagnosis" />
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography variant="caption" color="text.secondary">
|
<Typography variant="caption" color="text.secondary">
|
||||||
{diagnosedError}
|
{diag.description}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Typography variant="body2">
|
<Typography variant="body2">
|
||||||
{diagnosis.explanation}
|
{diag.response.explanation}
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
size="small"
|
size="small"
|
||||||
onClick={() => setDiagnosisExpanded(!diagnosisExpanded)}
|
onClick={() => {
|
||||||
|
const next = new Set(expandedDiagnoses);
|
||||||
|
if (isExpanded) next.delete(idx); else next.add(idx);
|
||||||
|
setExpandedDiagnoses(next);
|
||||||
|
}}
|
||||||
sx={{ textTransform: 'none', p: 0, mt: 1 }}
|
sx={{ textTransform: 'none', p: 0, mt: 1 }}
|
||||||
>
|
>
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id={diagnosisExpanded ? 'ai_show_less' : 'ai_show_details'}
|
id={isExpanded ? 'ai_show_less' : 'ai_show_details'}
|
||||||
defaultMessage={diagnosisExpanded ? 'Show less' : 'Show details'}
|
defaultMessage={isExpanded ? 'Show less' : 'Show details'}
|
||||||
/>
|
/>
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
{diagnosisExpanded && (
|
{isExpanded && (
|
||||||
<Box sx={{ mt: 1 }}>
|
<Box sx={{ mt: 1 }}>
|
||||||
<Typography variant="caption" fontWeight="bold">
|
<Typography variant="caption" fontWeight="bold">
|
||||||
<FormattedMessage id="ai_likely_causes" defaultMessage="Likely causes:" />
|
<FormattedMessage id="ai_likely_causes" defaultMessage="Likely causes:" />
|
||||||
</Typography>
|
</Typography>
|
||||||
<ul style={{ margin: '4px 0', paddingLeft: '20px' }}>
|
<ul style={{ margin: '4px 0', paddingLeft: '20px' }}>
|
||||||
{diagnosis.causes.map((cause, i) => (
|
{diag.response.causes.map((cause, i) => (
|
||||||
<li key={i}><Typography variant="caption">{cause}</Typography></li>
|
<li key={i}><Typography variant="caption">{cause}</Typography></li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
|
|
@ -495,18 +517,17 @@ function Log(props: LogProps) {
|
||||||
<FormattedMessage id="ai_next_steps" defaultMessage="Suggested next steps:" />
|
<FormattedMessage id="ai_next_steps" defaultMessage="Suggested next steps:" />
|
||||||
</Typography>
|
</Typography>
|
||||||
<ol style={{ margin: '4px 0', paddingLeft: '20px' }}>
|
<ol style={{ margin: '4px 0', paddingLeft: '20px' }}>
|
||||||
{diagnosis.nextSteps.map((step, i) => (
|
{diag.response.nextSteps.map((step, i) => (
|
||||||
<li key={i}><Typography variant="caption">{step}</Typography></li>
|
<li key={i}><Typography variant="caption">{step}</Typography></li>
|
||||||
))}
|
))}
|
||||||
</ol>
|
</ol>
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Box>
|
</Box>
|
||||||
</Card>
|
</Card>
|
||||||
</Grid>
|
</Grid>
|
||||||
)}
|
);
|
||||||
|
})}
|
||||||
|
|
||||||
<Grid item xs={12} md={12}>
|
<Grid item xs={12} md={12}>
|
||||||
<Button
|
<Button
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue