Files
seisee/frontend_src/App.tsx.backup

347 lines
20 KiB
Plaintext
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useState, useEffect, useCallback } from 'react';
import { MapContainer, TileLayer } from 'react-leaflet';
import NodeMarkers from './components/NodeMarkers';
import Sidebar from './components/Sidebar';
import SeismicSection from './SeismicSection';
import H5Coverage from './components/H5Coverage';
import DataDocumentation from './components/DataDocumentation';
import H5Dashboard from './components/H5Dashboard';
import CampaignDocs from "./components/CampaignDocs";
import { Node, DataWindow } from './types';
const API_BASE = '/seismic/api';
const DEFAULT_CENTER: [number, number] = [43.40, 3.70];
function App() {
const [nodes, setNodes] = useState<Node[]>([]);
const [dates, setDates] = useState<string[]>([]);
const [selectedDate, setSelectedDate] = useState<string>('');
const [selectedChannel, setSelectedChannel] = useState<string>('ch0');
const [selectedNode, setSelectedNode] = useState<Node | null>(null);
const [currentTime, setCurrentTime] = useState<number>(0);
const [dataWindow, setDataWindow] = useState<DataWindow | null>(null);
const [loading, setLoading] = useState(true);
const [sampleRate, setSampleRate] = useState(200);
const [showOnlyWithData, setShowOnlyWithData] = useState(false);
const [rmsTimeline, setRmsTimeline] = useState<any>(null);
const [allAdcValues, setAllAdcValues] = useState<Record<string, number>>({});
const [isPlaying, setIsPlaying] = useState(false);
const [playSpeed, setPlaySpeed] = useState(1);
const [chatOpen, setChatOpen] = useState(false);
const [chatMsg, setChatMsg] = useState('');
const [chatHistory, setChatHistory] = useState<any[]>([]);
const [showSection, setShowSection] = useState(false);
const [showDashboard, setShowDashboard] = useState(false);
const [showCampaignDocs, setShowCampaignDocs] = useState(false);
const [showDocumentation, setShowDocumentation] = useState(false);
const [showCoverage, setShowCoverage] = useState(false);
const [migrationStatus, setMigrationStatus] = useState<any>(null);
// 🔗 Deep-linking: Lire les paramètres URL au démarrage
useEffect(() => {
const params = new URLSearchParams(window.location.search);
const urlDate = params.get('date');
const urlChannel = params.get('channel');
const urlTime = params.get('time');
if (urlDate) setSelectedDate(urlDate);
if (urlChannel) setSelectedChannel(urlChannel);
if (urlTime) setCurrentTime(parseFloat(urlTime));
if (urlNode && nodes.length > 0) {
const node = nodes.find(n => n.id === urlNode);
if (node) setSelectedNode(node);
}
}, [nodes]); // Se déclenche quand les nodes sont chargés
// 🔗 Deep-linking: Synchroniser l'URL quand les états changent
useEffect(() => {
const params = new URLSearchParams();
if (selectedDate) params.set('date', selectedDate);
if (selectedChannel) params.set('channel', selectedChannel);
if (currentTime > 1000000) params.set('time', currentTime.toString());
if (selectedNode) params.set('node', selectedNode.id);
const newUrl = `${window.location.pathname}?${params.toString()}`;
window.history.pushState({}, '', newUrl);
}, [selectedDate, selectedChannel, currentTime, selectedNode]);
useEffect(() => {
fetch(`${API_BASE}/nodes`).then(r => r.json()).then(data => {
setNodes(data.nodes || []);
setSampleRate(data.sampleRateHz || 200);
setLoading(false);
});
}, []);
useEffect(() => {
fetch(`${API_BASE}/dates`).then(r => r.json()).then(data => {
const dateList = data.dates || [];
setDates(dateList);
if (dateList.length > 0 && !selectedDate) setSelectedDate(dateList[dateList.length - 1]);
});
}, []);
// Polling de la migration
useEffect(() => {
const checkStatus = () => {
fetch(`${API_BASE}/migration-status`)
.then(r => r.json())
.then(data => setMigrationStatus(data))
.catch(() => {});
};
const interval = setInterval(checkStatus, 5000);
checkStatus();
return () => clearInterval(interval);
}, []);
useEffect(() => {
if (!selectedDate || !selectedChannel) return;
setRmsTimeline(null);
fetch(`${API_BASE}/rms-timeline?date=${selectedDate}&channel=${selectedChannel}`)
.then(res => res.json())
.then(data => {
if (data && data.nodes && Object.keys(data.nodes).length > 0) {
setRmsTimeline(data);
const allPts = Object.values(data.nodes).flat() as any[];
if (allPts.length > 0) {
const minTs = Math.min(...allPts.map(p => p.ts));
if (minTs > 1000000) setCurrentTime(minTs);
}
} else {
const ts = new Date(selectedDate).getTime() / 1000;
if (!isNaN(ts)) setCurrentTime(ts);
}
});
}, [selectedDate, selectedChannel]);
useEffect(() => {
if (!isPlaying) return;
const interval = setInterval(() => {
setCurrentTime(prev => prev + (60 * (playSpeed / 5)));
}, 200);
return () => clearInterval(interval);
}, [isPlaying, playSpeed]);
useEffect(() => {
if (!rmsTimeline || !rmsTimeline.nodes || !currentTime) {
setAllAdcValues({});
return;
}
const values: Record<string, number> = {};
Object.entries(rmsTimeline.nodes).forEach(([nodeId, dataPoints]) => {
const pts = dataPoints as any[];
const point = pts.find(p => p.ts >= currentTime) || pts[pts.length - 1];
if (point) values[nodeId] = point.rms;
});
setAllAdcValues(values);
}, [currentTime, rmsTimeline]);
const loadNodeData = useCallback(async () => {
if (!selectedNode || !selectedDate || currentTime < 1000000) return;
setLoading(true);
try {
const res = await fetch(`${API_BASE}/data?node=${selectedNode.id}&date=${selectedDate}&channel=${selectedChannel}&start=${Math.floor(currentTime)}`);
const data = await res.json();
if (data && data.samples) setDataWindow({ ...data, node: selectedNode.id, date: selectedDate, channel: selectedChannel });
else setDataWindow(null);
} catch (e) { setDataWindow(null); }
setLoading(false);
}, [selectedNode, selectedDate, selectedChannel, currentTime]);
useEffect(() => { loadNodeData(); }, [selectedNode, selectedDate, selectedChannel]);
const formatTime = (ts: number) => {
if (ts < 1000000) return '--:--:--';
return new Date(ts * 1000).toISOString().substr(11, 8);
};
const sendChat = async () => {
if (!chatMsg) return;
const newHist = [...chatHistory, { role: 'user', text: chatMsg }];
setChatHistory(newHist);
setChatMsg('');
try {
const res = await fetch(`${API_BASE}/chat`, {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ message: chatMsg })
});
const data = await res.json();
setChatHistory([...newHist, { role: 'agent', text: data.response }]);
} catch (e) { setChatHistory([...newHist, { role: 'agent', text: "Erreur agent." }]); }
};
return (<>
<div className="app-container" style={{ display: 'flex', flexDirection: 'column', height: '100vh', width: '100vw', background: '#0f172a', color: '#fff' }}>
<header style={{ height: '60px', display: 'flex', alignItems: 'center', padding: '0 20px', background: '#1e293b', borderBottom: '1px solid #334155' }}>
<h1 style={{ margin: 0, fontSize: '1.2rem' }}>Seismic Viewer</h1>
{migrationStatus && migrationStatus.processed_files < migrationStatus.total_files && (
<div style={{ marginLeft: '20px', background: '#334155', padding: '5px 10px', borderRadius: '15px', fontSize: '0.75rem', border: '1px solid #4ade80' }}>
<span style={{ color: '#4ade80', fontWeight: 'bold' }}>⚡ Migration : </span>
{Math.round((migrationStatus.processed_files / migrationStatus.total_files) * 100)}%
({migrationStatus.processed_files}/{migrationStatus.total_files})
</div>
)}
<div style={{ color: '#94a3b8', fontSize: '0.8rem', marginLeft: '20px' }}>
Nodes: {nodes.length} | Actifs: {nodes.filter(n=>n.hasDates).length}
<button onClick={() => setShowDashboard(true)} style={{ background: '#f59e0b', color: '#fff', border: 'none', padding: '5px 15px', borderRadius: '4px', cursor: 'pointer', fontWeight: 'bold' }}>🎯 Dashboard H5</button>
<button onClick={() => setShowCampaignDocs(true)} style={{ background: "#8b5cf6", border: "none", color: "#fff", padding: "8px 16px", borderRadius: "6px", cursor: "pointer", fontWeight: "bold", fontSize: "0.9rem" }}>📚 Documentation Campagne</button>
</div>
<div style={{ marginLeft: 'auto', display: 'flex', gap: '10px' }}>
<button onClick={() => setShowSection(true)} style={{ background: '#8b5cf6', color: '#fff', border: 'none', padding: '5px 15px', borderRadius: '4px', cursor: 'pointer' }}>Vue Globale</button>
<button onClick={() => setShowDocumentation(true)} style={{ background: '#10b981', color: '#fff', border: 'none', padding: '5px 15px', borderRadius: '4px', cursor: 'pointer' }}>📖 Documentation</button>
<button onClick={() => setShowCoverage(true)} style={{ background: '#3b82f6', color: '#fff', border: 'none', padding: '5px 15px', borderRadius: '4px', cursor: 'pointer' }}>Coverage H5</button>
<select value={selectedDate} onChange={e => setSelectedDate(e.target.value)} style={{ background: '#0f172a', color: '#fff', border: '1px solid #334155' }}>
{dates.map(d => <option key={d} value={d}>{d}</option>)}
</select>
<select value={selectedChannel} onChange={e => setSelectedChannel(e.target.value)} style={{ background: '#0f172a', color: '#fff', border: '1px solid #334155' }}>
{['ch0', 'ch1', 'ch2', 'ch3'].map(ch => <option key={ch} value={ch}>{ch.toUpperCase()}</option>)}
</select>
</div>
</header>
<div style={{ height: '50px', background: '#0f172a', display: 'flex', alignItems: 'center', padding: '0 20px', gap: '15px', borderBottom: '1px solid #334155' }}>
<button onClick={() => setIsPlaying(!isPlaying)} aria-label={isPlaying ? 'Mettre en pause' : 'Lancer la lecture'} style={{ background: isPlaying ? '#ef4444' : '#22c55e', color: '#fff', border: 'none', padding: '5px 15px', borderRadius: '4px', fontWeight: 'bold', cursor: 'pointer' }}>
{isPlaying ? 'PAUSE' : 'PLAY'}
</button>
<div style={{ display: 'flex', gap: '2px', background: '#1e293b', padding: '2px', borderRadius: '4px' }}>
{[1, 10, 100, 1000].map(s => (
<button key={s} onClick={() => setPlaySpeed(s)} aria-label={`Vitesse ${s}x`} style={{ background: playSpeed === s ? '#3b82f6' : 'transparent', color: '#fff', border: 'none', padding: '2px 8px', fontSize: '0.7rem', cursor: 'pointer' }}>x{s}</button>
))}
</div>
<span style={{ fontFamily: 'monospace', color: '#4ade80', fontSize: '1.1rem', minWidth: '80px' }}>{formatTime(currentTime)}</span>
<input type="range" aria-label="Curseur temporel" style={{ flex: 1 }} min={currentTime - 1800} max={currentTime + 1800} value={currentTime} onChange={e => { setCurrentTime(Number(e.target.value)); setIsPlaying(false); }} />
</div>
<main style={{ flex: 1, display: 'flex', overflow: 'hidden' }}>
<div style={{ flex: 1, position: 'relative' }}>
<MapContainer center={DEFAULT_CENTER} zoom={13} style={{ height: '100%', width: '100%' }}>
<TileLayer url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png" />
<NodeMarkers nodes={nodes} selectedNode={selectedNode} onSelectNode={setSelectedNode} adcValues={allAdcValues} showOnlyWithData={showOnlyWithData} />
</MapContainer>
</div>
<Sidebar selectedNode={selectedNode} dataWindow={dataWindow} loading={loading} sampleRate={sampleRate} />
</main>
<SeismicSection nodes={nodes} currentTime={currentTime} channel={selectedChannel} visible={showSection} onClose={() => setShowSection(false)} />
<div style={{ position: 'fixed', bottom: '20px', right: '20px', zIndex: 10000 }}>
{!chatOpen ? (
<button onClick={() => setChatOpen(true)} aria-label="Ouvrir le chat assistant" style={{ width: '50px', height: '50px', borderRadius: '25px', background: '#e94560', border: 'none', color: '#fff', fontSize: '20px', cursor: 'pointer', boxShadow: '0 4px 12px rgba(0,0,0,0.3)' }}>💬</button>
) : (
<div style={{ background: '#1e293b', width: '300px', height: '400px', borderRadius: '8px', display: 'flex', flexDirection: 'column', border: '1px solid #334155', boxShadow: '0 8px 24px rgba(0,0,0,0.4)' }}>
<div style={{ padding: '10px', background: '#334155', display: 'flex', justifyContent: 'space-between', borderRadius: '8px 8px 0 0' }}>
<span style={{ fontSize: '0.8rem', fontWeight: 'bold' }}>Assistant</span>
<button onClick={() => setChatOpen(false)} aria-label="Fermer le chat" style={{ background: 'none', border: 'none', color: '#fff', cursor: 'pointer' }}>×</button>
</div>
<div style={{ flex: 1, overflowY: 'auto', padding: '10px', display: 'flex', flexDirection: 'column', gap: '8px' }}>
{chatHistory.map((m, i) => (
<div key={i} style={{ alignSelf: m.role === 'user' ? 'flex-end' : 'flex-start', background: m.role === 'user' ? '#3b82f6' : '#475569', padding: '6px 10px', borderRadius: '8px', fontSize: '0.8rem', maxWidth: '85%' }}>{m.text}</div>
))}
</div>
<div style={{ padding: '10px', borderTop: '1px solid #334155', display: 'flex', gap: '5px' }}>
<input value={chatMsg} onChange={e => setChatMsg(e.target.value)} onKeyPress={e => e.key === 'Enter' && sendChat()} aria-label="Poser une question" style={{ flex: 1, background: '#0f172a', border: '1px solid #334155', color: '#fff', padding: '5px', borderRadius: '4px', fontSize: '0.8rem' }} placeholder="Question…" />
<button onClick={sendChat} style={{ background: '#e94560', border: 'none', color: '#fff', padding: '5px 10px', borderRadius: '4px', fontSize: '0.8rem' }}>OK</button>
</div>
</div>
)}
</div>
{showDashboard && (
<div style={{ position: "fixed", top: 0, left: 0, right: 0, bottom: 0, background: "#f1f5f9", zIndex: 9998, overflow: "auto" }}>
<div style={{ position: "relative" }}>
<button onClick={() => setShowDashboard(false)} style={{ position: "fixed", top: "10px", right: "10px", background: "#ef4444", color: "white", border: "none", padding: "10px 20px", borderRadius: "4px", cursor: "pointer", fontWeight: "bold", zIndex: 9999 }}>✕ Fermer Dashboard</button>
<H5Dashboard />
</div>
</div>
)}
<div style={{ position: "fixed", top: 0, left: 0, right: 0, bottom: 0, background: "#f1f5f9", zIndex: 9998, overflow: "auto" }}>
<div style={{ position: "relative" }}>
{showCampaignDocs && (
<button onClick={() => setShowCampaignDocs(false)} style={{ position: "fixed", top: "10px", right: "10px", background: "#ef4444", color: "white", border: "none", padding: "10px 20px", borderRadius: "4px", cursor: "pointer", fontWeight: "bold", zIndex: 9999 }}>✕ Fermer</button>
<CampaignDocs />
)}
</div>
</div>
)}
<div style={{ position: 'fixed', top: 0, left: 0, right: 0, bottom: 0, background: '#f1f5f9', zIndex: 9998, overflow: 'auto' }}>
{showCampaignDocs && (
<div style={{ position: "fixed", top: 0, left: 0, right: 0, bottom: 0, background: "#f1f5f9", zIndex: 9998, overflow: "auto" }}>
<div style={{ position: "relative" }}>
<button onClick={() => setShowCampaignDocs(false)} style={{ position: "fixed", top: "10px", right: "10px", background: "#ef4444", color: "white", border: "none", padding: "10px 20px", borderRadius: "4px", cursor: "pointer", fontWeight: "bold", zIndex: 9999 }}>✕ Fermer</button>
<CampaignDocs />
)}
</div>
</div>
)}
<div style={{ position: 'relative' }}>
{showCampaignDocs && (
<div style={{ position: "fixed", top: 0, left: 0, right: 0, bottom: 0, background: "#f1f5f9", zIndex: 9998, overflow: "auto" }}>
<div style={{ position: "relative" }}>
<button onClick={() => setShowCampaignDocs(false)} style={{ position: "fixed", top: "10px", right: "10px", background: "#ef4444", color: "white", border: "none", padding: "10px 20px", borderRadius: "4px", cursor: "pointer", fontWeight: "bold", zIndex: 9999 }}>✕ Fermer</button>
<CampaignDocs />
)}
</div>
</div>
)}
<button onClick={() => setShowDashboard(false)} style={{ position: 'fixed', top: '10px', right: '10px', background: '#ef4444', color: 'white', border: 'none', padding: '10px 20px', borderRadius: '4px', cursor: 'pointer', fontWeight: 'bold', zIndex: 9999 }}>✕ Fermer Dashboard</button>
{showCampaignDocs && (
<div style={{ position: "fixed", top: 0, left: 0, right: 0, bottom: 0, background: "#f1f5f9", zIndex: 9998, overflow: "auto" }}>
<div style={{ position: "relative" }}>
<button onClick={() => setShowCampaignDocs(false)} style={{ position: "fixed", top: "10px", right: "10px", background: "#ef4444", color: "white", border: "none", padding: "10px 20px", borderRadius: "4px", cursor: "pointer", fontWeight: "bold", zIndex: 9999 }}>✕ Fermer</button>
<CampaignDocs />
)}
</div>
</div>
)}
<H5Dashboard />
{showCampaignDocs && (
<div style={{ position: "fixed", top: 0, left: 0, right: 0, bottom: 0, background: "#f1f5f9", zIndex: 9998, overflow: "auto" }}>
<div style={{ position: "relative" }}>
<button onClick={() => setShowCampaignDocs(false)} style={{ position: "fixed", top: "10px", right: "10px", background: "#ef4444", color: "white", border: "none", padding: "10px 20px", borderRadius: "4px", cursor: "pointer", fontWeight: "bold", zIndex: 9999 }}>✕ Fermer</button>
<CampaignDocs />
)}
</div>
</div>
)}
</div>
{showCampaignDocs && (
<div style={{ position: "fixed", top: 0, left: 0, right: 0, bottom: 0, background: "#f1f5f9", zIndex: 9998, overflow: "auto" }}>
<div style={{ position: "relative" }}>
<button onClick={() => setShowCampaignDocs(false)} style={{ position: "fixed", top: "10px", right: "10px", background: "#ef4444", color: "white", border: "none", padding: "10px 20px", borderRadius: "4px", cursor: "pointer", fontWeight: "bold", zIndex: 9999 }}>✕ Fermer</button>
<CampaignDocs />
)}
</div>
</div>
)}
</div>
{showCampaignDocs && (
<div style={{ position: "fixed", top: 0, left: 0, right: 0, bottom: 0, background: "#f1f5f9", zIndex: 9998, overflow: "auto" }}>
<div style={{ position: "relative" }}>
<button onClick={() => setShowCampaignDocs(false)} style={{ position: "fixed", top: "10px", right: "10px", background: "#ef4444", color: "white", border: "none", padding: "10px 20px", borderRadius: "4px", cursor: "pointer", fontWeight: "bold", zIndex: 9999 }}>✕ Fermer</button>
<CampaignDocs />
)}
</div>
</div>
)}
)}
{showCampaignDocs && (
<div style={{ position: "fixed", top: 0, left: 0, right: 0, bottom: 0, background: "#f1f5f9", zIndex: 9998, overflow: "auto" }}>
<div style={{ position: "relative" }}>
<button onClick={() => setShowCampaignDocs(false)} style={{ position: "fixed", top: "10px", right: "10px", background: "#ef4444", color: "white", border: "none", padding: "10px 20px", borderRadius: "4px", cursor: "pointer", fontWeight: "bold", zIndex: 9999 }}>✕ Fermer</button>
<CampaignDocs />
)}
</div>
</div>
)}
{showDocumentation && <DataDocumentation onClose={() => setShowDocumentation(false)} />}
{showCoverage && <H5Coverage onClose={() => setShowCoverage(false)} />}
</div>
</>
);
}
export default App;