Compare commits

...

2 Commits

6 changed files with 917 additions and 699 deletions

File diff suppressed because it is too large Load Diff

View File

@ -3,18 +3,23 @@ import {
Alert,
Box,
Card,
Chip,
CircularProgress,
Container,
Divider,
Grid,
IconButton,
ListSubheader,
MenuItem,
Select,
TextField,
useTheme
} from '@mui/material';
import Typography from '@mui/material/Typography';
import { FormattedMessage } from 'react-intl';
import { FormattedMessage, useIntl } from 'react-intl';
import ErrorIcon from '@mui/icons-material/Error';
import WarningIcon from '@mui/icons-material/Warning';
import axiosConfig from '../../../Resources/axiosConfig';
import axiosConfig, { axiosConfigWithoutToken } from '../../../Resources/axiosConfig';
import { AxiosError, AxiosResponse } from 'axios/index';
import routes from '../../../Resources/routes.json';
import { useNavigate } from 'react-router-dom';
@ -24,6 +29,15 @@ import Button from '@mui/material/Button';
import FormControlLabel from '@mui/material/FormControlLabel';
import Checkbox from '@mui/material/Checkbox';
interface TestDiagnoseResult {
source: string;
alarm: string;
explanation?: string;
causes?: string[];
nextSteps?: string[];
message?: string;
}
interface LogProps {
errorLoadingS3Data: boolean;
id: number;
@ -47,11 +61,19 @@ function Log(props: LogProps) {
const navigate = useNavigate();
const tokencontext = useContext(TokenContext);
const { removeToken } = tokencontext;
const intl = useIntl();
const [diagnoses, setDiagnoses] = useState<{ description: string; lastSeen: string; response: DiagnosticResponse }[]>([]);
const [diagnosisLoading, setDiagnosisLoading] = useState(false);
const [expandedDiagnoses, setExpandedDiagnoses] = useState<Set<number>>(new Set());
// demo panel state
const [demoPanelOpen, setDemoPanelOpen] = useState(false);
const [demoAlarm, setDemoAlarm] = useState<string>('AbnormalGridVoltage');
const [demoCustom, setDemoCustom] = useState<string>('');
const [demoLoading, setDemoLoading] = useState(false);
const [demoResult, setDemoResult] = useState<TestDiagnoseResult | null>(null);
useEffect(() => {
axiosConfig
.get(`/GetAllErrorsForInstallation?id=${props.id}`)
@ -174,6 +196,63 @@ function Log(props: LogProps) {
});
};
const DEMO_ALARMS = {
sinexcel: [
'AbnormalGridVoltage',
'GridVoltagePhaseLoss',
'Battery1NotConnected',
'Battery1Overvoltage',
'LithiumBattery1DischargeForbidden',
'InsulationFault',
'FanFault',
'InverterPowerTubeFault',
'Pv1Overvoltage',
'IslandProtection',
'SystemDerating',
'DcBusOvervoltage',
],
growatt: [
'StringFault',
'PvShortCircuited',
'DcFuseBlown',
'DcInputVoltageTooHigh',
'NoUtilityGrid',
'GridVoltageOutOfRange',
'FanFailure',
'BatteryDisconnected',
'BmsFault',
'BatteryCommunicationFailure',
'BatteryVoltageTooHigh',
'OverTemperatureAlarm',
],
};
const runDemo = () => {
const alarm = demoAlarm === '__custom__' ? demoCustom.trim() : demoAlarm;
if (!alarm) return;
setDemoLoading(true);
setDemoResult(null);
axiosConfigWithoutToken
.get(`/TestDiagnoseError?errorDescription=${encodeURIComponent(alarm)}`)
.then((res: AxiosResponse<TestDiagnoseResult>) => {
setDemoResult(res.data);
})
.catch(() => {
setDemoResult({ source: 'Error', alarm, message: 'Request failed.' });
})
.finally(() => setDemoLoading(false));
};
const sourceChip = (source: string) => {
if (source === 'KnowledgeBase')
return <Chip label="Knowledge Base" size="small" sx={{ bgcolor: '#1976d2', color: '#fff', fontWeight: 'bold' }} />;
if (source === 'MistralAI')
return <Chip label="Mistral AI" size="small" sx={{ bgcolor: '#7b1fa2', color: '#fff', fontWeight: 'bold' }} />;
if (source === 'MistralFailed')
return <Chip label="Mistral failed" size="small" color="error" />;
return <Chip label="Not available" size="small" color="default" />;
};
const warningDescriptionMap: { [key: string]: string } = {
// BMS warnings
"TaM1": "TaM1: BMS temperature high",
@ -469,6 +548,103 @@ function Log(props: LogProps) {
<Container maxWidth="xl">
<Grid container>
{/* ── AI Diagnosis Demo Panel ── */}
<Grid item xs={12}>
<Box sx={{ marginTop: '20px' }}>
<Button
variant="outlined"
size="small"
onClick={() => { setDemoPanelOpen(!demoPanelOpen); setDemoResult(null); }}
sx={{ textTransform: 'none' }}
>
{demoPanelOpen
? <FormattedMessage id="demo_hide_button" defaultMessage="Hide AI Diagnosis Demo" />
: <FormattedMessage id="demo_test_button" defaultMessage="Test AI Diagnosis" />}
</Button>
</Box>
{demoPanelOpen && (
<Card sx={{ marginTop: '10px', borderLeft: '4px solid #9c27b0', padding: '16px' }}>
<Typography variant="subtitle2" fontWeight="bold" sx={{ mb: 1.5 }}>
<FormattedMessage id="demo_panel_title" defaultMessage="AI Diagnosis Demo" />
</Typography>
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: '10px', alignItems: 'flex-end' }}>
<Select
size="small"
value={demoAlarm}
onChange={e => { setDemoAlarm(e.target.value); setDemoResult(null); }}
sx={{ minWidth: 260 }}
>
<ListSubheader>Sinexcel</ListSubheader>
{DEMO_ALARMS.sinexcel.map(a => (
<MenuItem key={a} value={a}>{a}</MenuItem>
))}
<ListSubheader>Growatt</ListSubheader>
{DEMO_ALARMS.growatt.map(a => (
<MenuItem key={a} value={a}>{a}</MenuItem>
))}
<ListSubheader><FormattedMessage id="demo_custom_group" defaultMessage="Custom (may use Mistral AI)" /></ListSubheader>
<MenuItem value="__custom__"><FormattedMessage id="demo_custom_option" defaultMessage="Type custom alarm below…" /></MenuItem>
</Select>
{demoAlarm === '__custom__' && (
<TextField
size="small"
placeholder={intl.formatMessage({ id: 'demo_custom_placeholder', defaultMessage: 'e.g. UnknownBatteryFault' })}
value={demoCustom}
onChange={e => { setDemoCustom(e.target.value); setDemoResult(null); }}
sx={{ minWidth: 220 }}
/>
)}
<Button
variant="contained"
size="small"
onClick={runDemo}
disabled={demoLoading || (demoAlarm === '__custom__' && !demoCustom.trim())}
sx={{ textTransform: 'none', bgcolor: '#9c27b0', '&:hover': { bgcolor: '#7b1fa2' } }}
>
{demoLoading ? <CircularProgress size={18} sx={{ color: '#fff' }} /> : <FormattedMessage id="demo_diagnose_button" defaultMessage="Diagnose" />}
</Button>
</Box>
{demoResult && (
<Box sx={{ mt: 2 }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: '8px', mb: 1 }}>
<Typography variant="caption" color="text.secondary">
<strong>{demoResult.alarm}</strong>
</Typography>
{sourceChip(demoResult.source)}
</Box>
{demoResult.message ? (
<Typography variant="body2" color="text.secondary">{demoResult.message}</Typography>
) : (
<>
<Typography variant="body2" sx={{ mt: 0.5 }}>{demoResult.explanation}</Typography>
<Box sx={{ mt: 1 }}>
<Typography variant="caption" fontWeight="bold"><FormattedMessage id="ai_likely_causes" defaultMessage="Likely causes:" /></Typography>
<ul style={{ margin: '4px 0', paddingLeft: '20px' }}>
{(demoResult.causes ?? []).map((c, i) => (
<li key={i}><Typography variant="caption">{c}</Typography></li>
))}
</ul>
<Typography variant="caption" fontWeight="bold"><FormattedMessage id="ai_next_steps" defaultMessage="Suggested next steps:" /></Typography>
<ol style={{ margin: '4px 0', paddingLeft: '20px' }}>
{(demoResult.nextSteps ?? []).map((s, i) => (
<li key={i}><Typography variant="caption">{s}</Typography></li>
))}
</ol>
</Box>
</>
)}
</Box>
)}
</Card>
)}
</Grid>
{/* AI Diagnosis banner — shown when loading or diagnoses are available */}
{diagnosisLoading && (
<Grid item xs={12} md={12}>

View File

@ -130,5 +130,17 @@
"downloadingBatteryLog": "Das Batterieprotokoll wird heruntergeladen. Es wird im Downloads-Ordner gespeichert. Bitte warten...",
"confirmBatteryLogDownload": "Möchten Sie das Batterieprotokoll wirklich herunterladen?",
"downloadBatteryLogFailed": "Herunterladen des Batterieprotokolls fehlgeschlagen, bitte versuchen Sie es erneut.",
"noReportData": "Keine Berichtsdaten gefunden."
"noReportData": "Keine Berichtsdaten gefunden.",
"ai_analyzing": "KI analysiert...",
"ai_show_details": "Details anzeigen",
"ai_show_less": "Weniger anzeigen",
"ai_likely_causes": "Wahrscheinliche Ursachen:",
"ai_next_steps": "Empfohlene nächste Schritte:",
"demo_test_button": "KI-Diagnose testen",
"demo_hide_button": "KI-Diagnose Demo ausblenden",
"demo_panel_title": "KI-Diagnose Demo",
"demo_custom_group": "Benutzerdefiniert (kann Mistral KI verwenden)",
"demo_custom_option": "Benutzerdefinierten Alarm eingeben…",
"demo_custom_placeholder": "z.B. UnknownBatteryFault",
"demo_diagnose_button": "Diagnostizieren"
}

View File

@ -112,5 +112,17 @@
"downloadingBatteryLog": "The battery log is getting downloaded. It will be saved in the Downloads folder. Please wait...",
"confirmBatteryLogDownload": "Do you really want to download battery log?",
"downloadBatteryLogFailed": "Download battery log failed, please try again.",
"noReportData": "No report data found."
"noReportData": "No report data found.",
"ai_analyzing": "AI is analyzing...",
"ai_show_details": "Show details",
"ai_show_less": "Show less",
"ai_likely_causes": "Likely causes:",
"ai_next_steps": "Suggested next steps:",
"demo_test_button": "Test AI Diagnosis",
"demo_hide_button": "Hide AI Diagnosis Demo",
"demo_panel_title": "AI Diagnosis Demo",
"demo_custom_group": "Custom (may use Mistral AI)",
"demo_custom_option": "Type custom alarm below…",
"demo_custom_placeholder": "e.g. UnknownBatteryFault",
"demo_diagnose_button": "Diagnose"
}

View File

@ -124,5 +124,17 @@
"downloadingBatteryLog": "Le journal de la batterie est en cours de téléchargement. Il sera enregistré dans le dossier Téléchargements. Veuillez patienter...",
"confirmBatteryLogDownload": "Voulez-vous vraiment télécharger le journal de la batterie?",
"downloadBatteryLogFailed": "Échec du téléchargement du journal de la batterie, veuillez réessayer.",
"noReportData": "Aucune donnée de rapport trouvée."
"noReportData": "Aucune donnée de rapport trouvée.",
"ai_analyzing": "L'IA analyse...",
"ai_show_details": "Afficher les détails",
"ai_show_less": "Afficher moins",
"ai_likely_causes": "Causes probables :",
"ai_next_steps": "Prochaines étapes suggérées :",
"demo_test_button": "Tester le diagnostic IA",
"demo_hide_button": "Masquer la démo de diagnostic IA",
"demo_panel_title": "Démo de diagnostic IA",
"demo_custom_group": "Personnalisé (peut utiliser Mistral IA)",
"demo_custom_option": "Saisir une alarme personnalisée…",
"demo_custom_placeholder": "ex. UnknownBatteryFault",
"demo_diagnose_button": "Diagnostiquer"
}

View File

@ -135,5 +135,17 @@
"downloadingBatteryLog": "Il registro della batteria è in fase di download. Verrà salvato nella cartella Download. Attendere prego...",
"confirmBatteryLogDownload": "Vuoi davvero scaricare il registro della batteria?",
"downloadBatteryLogFailed": "Download del registro della batteria fallito, riprovare.",
"noReportData": "Nessun dato del rapporto trovato."
"noReportData": "Nessun dato del rapporto trovato.",
"ai_analyzing": "L'IA sta analizzando...",
"ai_show_details": "Mostra dettagli",
"ai_show_less": "Mostra meno",
"ai_likely_causes": "Cause probabili:",
"ai_next_steps": "Passi successivi suggeriti:",
"demo_test_button": "Testa diagnosi IA",
"demo_hide_button": "Nascondi demo diagnosi IA",
"demo_panel_title": "Demo diagnosi IA",
"demo_custom_group": "Personalizzato (potrebbe usare Mistral IA)",
"demo_custom_option": "Inserisci allarme personalizzato…",
"demo_custom_placeholder": "es. UnknownBatteryFault",
"demo_diagnose_button": "Diagnostica"
}