feat: Implement vision menu, processed data panel, and UI improvements
- Add VisionMenu component with Camera (QRCode) and Barcode (Keyence) submenus - Remove old CameraPanel component and replace with dropdown menu structure - Add ProcessedDataPanel to display processed data in bottom dock (5 rows visible) - Create SystemStatusPanel component with horizontal button layout (START/STOP/RESET) - Create EventLogPanel component for better code organization - Add device initialization feature with 7-axis progress tracking - Add GetProcessedData and GetInitializeStatus backend methods - Update Header menu layout to vertical (icon on top, text below) for more space - Update HomePage layout with bottom-docked ProcessedDataPanel - Refactor HomePage to use new modular components 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -343,6 +343,94 @@ class CommunicationLayer {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public async initializeDevice(): Promise<{ success: boolean; message: string }> {
|
||||
if (isWebView && machine) {
|
||||
const resultJson = await machine.InitializeDevice();
|
||||
return JSON.parse(resultJson);
|
||||
} else {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!this.isConnected) {
|
||||
setTimeout(() => {
|
||||
if (!this.isConnected) reject({ success: false, message: "WebSocket connection timeout" });
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
const timeoutId = setTimeout(() => {
|
||||
this.listeners = this.listeners.filter(cb => cb !== handler);
|
||||
reject({ success: false, message: "Initialize device timeout" });
|
||||
}, 10000);
|
||||
|
||||
const handler = (data: any) => {
|
||||
if (data.type === 'DEVICE_INITIALIZED') {
|
||||
clearTimeout(timeoutId);
|
||||
this.listeners = this.listeners.filter(cb => cb !== handler);
|
||||
resolve(data.data);
|
||||
}
|
||||
};
|
||||
this.listeners.push(handler);
|
||||
this.ws?.send(JSON.stringify({ type: 'INITIALIZE_DEVICE' }));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public async getInitializeStatus(): Promise<string> {
|
||||
if (isWebView && machine) {
|
||||
return await machine.GetInitializeStatus();
|
||||
} else {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!this.isConnected) {
|
||||
setTimeout(() => {
|
||||
if (!this.isConnected) reject("WebSocket connection timeout");
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
const timeoutId = setTimeout(() => {
|
||||
this.listeners = this.listeners.filter(cb => cb !== handler);
|
||||
reject("Initialize status fetch timeout");
|
||||
}, 10000);
|
||||
|
||||
const handler = (data: any) => {
|
||||
if (data.type === 'INITIALIZE_STATUS_DATA') {
|
||||
clearTimeout(timeoutId);
|
||||
this.listeners = this.listeners.filter(cb => cb !== handler);
|
||||
resolve(JSON.stringify(data.data));
|
||||
}
|
||||
};
|
||||
this.listeners.push(handler);
|
||||
this.ws?.send(JSON.stringify({ type: 'GET_INITIALIZE_STATUS' }));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public async getProcessedData(): Promise<string> {
|
||||
if (isWebView && machine) {
|
||||
return await machine.GetProcessedData();
|
||||
} else {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!this.isConnected) {
|
||||
setTimeout(() => {
|
||||
if (!this.isConnected) reject("WebSocket connection timeout");
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
const timeoutId = setTimeout(() => {
|
||||
this.listeners = this.listeners.filter(cb => cb !== handler);
|
||||
reject("Processed data fetch timeout");
|
||||
}, 10000);
|
||||
|
||||
const handler = (data: any) => {
|
||||
if (data.type === 'PROCESSED_DATA') {
|
||||
clearTimeout(timeoutId);
|
||||
this.listeners = this.listeners.filter(cb => cb !== handler);
|
||||
resolve(JSON.stringify(data.data));
|
||||
}
|
||||
};
|
||||
this.listeners.push(handler);
|
||||
this.ws?.send(JSON.stringify({ type: 'GET_PROCESSED_DATA' }));
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const comms = new CommunicationLayer();
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Camera, Crosshair } from 'lucide-react';
|
||||
import { PanelHeader } from './common/PanelHeader';
|
||||
|
||||
interface CameraPanelProps {
|
||||
videoRef: React.RefObject<HTMLVideoElement>;
|
||||
}
|
||||
|
||||
export const CameraPanel: React.FC<CameraPanelProps> = ({ videoRef }) => (
|
||||
<div className="h-full flex flex-col">
|
||||
<PanelHeader title="Vision Feed" icon={Camera} />
|
||||
<div className="flex-1 bg-black relative overflow-hidden border border-slate-800 group">
|
||||
<video ref={videoRef} autoPlay playsInline className="w-full h-full object-cover opacity-80" />
|
||||
|
||||
{/* HUD OVERLAY */}
|
||||
<div className="absolute inset-0 pointer-events-none">
|
||||
<div className="absolute top-0 left-0 w-full h-full border-[20px] border-neon-blue/10 clip-tech-inv"></div>
|
||||
<Crosshair className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-12 h-12 text-neon-blue opacity-50" />
|
||||
|
||||
<div className="absolute top-4 right-4 flex flex-col items-end gap-1">
|
||||
<span className="text-[10px] font-mono text-neon-red animate-pulse">● REC</span>
|
||||
<span className="text-xs font-mono text-neon-blue">1920x1080 @ 60FPS</span>
|
||||
</div>
|
||||
|
||||
<div className="absolute bottom-4 left-4 text-neon-blue/80 font-mono text-xs">
|
||||
EXPOSURE: AUTO<br />
|
||||
GAIN: 12dB<br />
|
||||
FOCUS: 150mm
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-4 gap-2 mt-3">
|
||||
<button className="bg-slate-800 text-slate-400 text-[10px] py-2 hover:bg-neon-blue hover:text-black transition-colors">ZOOM +</button>
|
||||
<button className="bg-slate-800 text-slate-400 text-[10px] py-2 hover:bg-neon-blue hover:text-black transition-colors">ZOOM -</button>
|
||||
<button className="bg-slate-800 text-slate-400 text-[10px] py-2 hover:bg-neon-blue hover:text-black transition-colors">SNAP</button>
|
||||
<button className="bg-slate-800 text-slate-400 text-[10px] py-2 hover:bg-neon-blue hover:text-black transition-colors">SETTINGS</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
27
FrontEnd/components/EventLogPanel.tsx
Normal file
27
FrontEnd/components/EventLogPanel.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
import React from 'react';
|
||||
import { Terminal } from 'lucide-react';
|
||||
import { CyberPanel } from './common/CyberPanel';
|
||||
import { LogEntry } from '../types';
|
||||
|
||||
interface EventLogPanelProps {
|
||||
logs: LogEntry[];
|
||||
}
|
||||
|
||||
export const EventLogPanel: React.FC<EventLogPanelProps> = ({ logs }) => {
|
||||
return (
|
||||
<CyberPanel className="flex-1 flex flex-col min-h-0">
|
||||
<div className="mb-2 flex items-center justify-between text-xs text-neon-blue font-bold tracking-widest uppercase border-b border-white/10 pb-2">
|
||||
<span>Event Log</span>
|
||||
<Terminal className="w-3 h-3" />
|
||||
</div>
|
||||
<div className="flex-1 overflow-y-auto font-mono text-[10px] space-y-1 pr-1 custom-scrollbar">
|
||||
{logs.map(log => (
|
||||
<div key={log.id} className={`flex gap-2 ${log.type === 'error' ? 'text-red-500' : log.type === 'warning' ? 'text-amber-400' : 'text-slate-400'}`}>
|
||||
<span className="opacity-50">[{log.timestamp}]</span>
|
||||
<span>{log.message}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CyberPanel>
|
||||
);
|
||||
};
|
||||
@@ -1,6 +1,7 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Target, CheckCircle2, Loader2 } from 'lucide-react';
|
||||
import { Target, CheckCircle2, Loader2, AlertCircle } from 'lucide-react';
|
||||
import { TechButton } from './common/TechButton';
|
||||
import { comms } from '../communication';
|
||||
|
||||
interface InitializeModalProps {
|
||||
isOpen: boolean;
|
||||
@@ -17,74 +18,111 @@ interface AxisState {
|
||||
|
||||
export const InitializeModal: React.FC<InitializeModalProps> = ({ isOpen, onClose }) => {
|
||||
const [axes, setAxes] = useState<AxisState[]>([
|
||||
{ name: 'X-Axis', status: 'pending', progress: 0 },
|
||||
{ name: 'Y-Axis', status: 'pending', progress: 0 },
|
||||
{ name: 'Z-Axis', status: 'pending', progress: 0 },
|
||||
{ name: 'PZ_PICK', status: 'pending', progress: 0 },
|
||||
{ name: 'PL_UPDN', status: 'pending', progress: 0 },
|
||||
{ name: 'PR_UPDN', status: 'pending', progress: 0 },
|
||||
{ name: 'PL_MOVE', status: 'pending', progress: 0 },
|
||||
{ name: 'PR_MOVE', status: 'pending', progress: 0 },
|
||||
{ name: 'Z_THETA', status: 'pending', progress: 0 },
|
||||
{ name: 'PX_PICK', status: 'pending', progress: 0 },
|
||||
]);
|
||||
const [isInitializing, setIsInitializing] = useState(false);
|
||||
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
||||
const [statusInterval, setStatusInterval] = useState<NodeJS.Timeout | null>(null);
|
||||
|
||||
// Reset state when modal closes
|
||||
useEffect(() => {
|
||||
if (!isOpen) {
|
||||
setAxes([
|
||||
{ name: 'X-Axis', status: 'pending', progress: 0 },
|
||||
{ name: 'Y-Axis', status: 'pending', progress: 0 },
|
||||
{ name: 'Z-Axis', status: 'pending', progress: 0 },
|
||||
{ name: 'PZ_PICK', status: 'pending', progress: 0 },
|
||||
{ name: 'PL_UPDN', status: 'pending', progress: 0 },
|
||||
{ name: 'PR_UPDN', status: 'pending', progress: 0 },
|
||||
{ name: 'PL_MOVE', status: 'pending', progress: 0 },
|
||||
{ name: 'PR_MOVE', status: 'pending', progress: 0 },
|
||||
{ name: 'Z_THETA', status: 'pending', progress: 0 },
|
||||
{ name: 'PX_PICK', status: 'pending', progress: 0 },
|
||||
]);
|
||||
setIsInitializing(false);
|
||||
setErrorMessage(null);
|
||||
if (statusInterval) {
|
||||
clearInterval(statusInterval);
|
||||
setStatusInterval(null);
|
||||
}
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
const startInitialization = () => {
|
||||
setIsInitializing(true);
|
||||
// Cleanup interval on unmount
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (statusInterval) {
|
||||
clearInterval(statusInterval);
|
||||
}
|
||||
};
|
||||
}, [statusInterval]);
|
||||
|
||||
// Initialize each axis with 3 second delay between them
|
||||
axes.forEach((axis, index) => {
|
||||
const delay = index * 3000; // 0s, 3s, 6s
|
||||
const updateStatus = async () => {
|
||||
try {
|
||||
const statusJson = await comms.getInitializeStatus();
|
||||
const status = JSON.parse(statusJson);
|
||||
|
||||
// Start initialization after delay
|
||||
setTimeout(() => {
|
||||
setAxes(prev => {
|
||||
const next = [...prev];
|
||||
next[index] = { ...next[index], status: 'initializing', progress: 0 };
|
||||
return next;
|
||||
});
|
||||
if (!status.isInitializing) {
|
||||
// Initialization complete
|
||||
if (statusInterval) {
|
||||
clearInterval(statusInterval);
|
||||
setStatusInterval(null);
|
||||
}
|
||||
setIsInitializing(false);
|
||||
|
||||
// Progress animation (3 seconds)
|
||||
const startTime = Date.now();
|
||||
const duration = 3000;
|
||||
const interval = setInterval(() => {
|
||||
const elapsed = Date.now() - startTime;
|
||||
const progress = Math.min((elapsed / duration) * 100, 100);
|
||||
// Check if all axes are home set
|
||||
const allComplete = status.axes.every((axis: any) => axis.isHomeSet);
|
||||
if (allComplete) {
|
||||
// Auto close after 1 second
|
||||
setTimeout(() => {
|
||||
onClose();
|
||||
}, 1000);
|
||||
}
|
||||
}
|
||||
|
||||
setAxes(prev => {
|
||||
const next = [...prev];
|
||||
next[index] = { ...next[index], progress };
|
||||
return next;
|
||||
});
|
||||
|
||||
if (progress >= 100) {
|
||||
clearInterval(interval);
|
||||
setAxes(prev => {
|
||||
const next = [...prev];
|
||||
next[index] = { ...next[index], status: 'completed', progress: 100 };
|
||||
return next;
|
||||
});
|
||||
|
||||
// Check if all axes are completed
|
||||
setAxes(prev => {
|
||||
if (prev.every(a => a.status === 'completed')) {
|
||||
// Auto close after 500ms
|
||||
setTimeout(() => {
|
||||
onClose();
|
||||
}, 500);
|
||||
}
|
||||
return prev;
|
||||
});
|
||||
// Update axis states
|
||||
setAxes(prev => {
|
||||
return prev.map(axis => {
|
||||
const backendAxis = status.axes.find((a: any) => a.name === axis.name);
|
||||
if (backendAxis) {
|
||||
const progress = backendAxis.progress;
|
||||
const isComplete = backendAxis.isHomeSet;
|
||||
return {
|
||||
...axis,
|
||||
progress,
|
||||
status: isComplete ? 'completed' : progress > 0 ? 'initializing' : 'pending'
|
||||
};
|
||||
}
|
||||
}, 50);
|
||||
}, delay);
|
||||
});
|
||||
return axis;
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('[InitializeModal] Status update error:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const startInitialization = async () => {
|
||||
try {
|
||||
setErrorMessage(null);
|
||||
const result = await comms.initializeDevice();
|
||||
|
||||
if (!result.success) {
|
||||
setErrorMessage(result.message);
|
||||
return;
|
||||
}
|
||||
|
||||
setIsInitializing(true);
|
||||
|
||||
// Start polling for status updates every 200ms
|
||||
const interval = setInterval(updateStatus, 200);
|
||||
setStatusInterval(interval);
|
||||
} catch (error: any) {
|
||||
setErrorMessage(error.message || 'Failed to start initialization');
|
||||
console.error('[InitializeModal] Initialization error:', error);
|
||||
}
|
||||
};
|
||||
|
||||
if (!isOpen) return null;
|
||||
@@ -93,7 +131,7 @@ export const InitializeModal: React.FC<InitializeModalProps> = ({ isOpen, onClos
|
||||
|
||||
return (
|
||||
<div className="absolute inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm">
|
||||
<div className="w-[600px] glass-holo p-8 border border-neon-blue shadow-glow-blue relative flex flex-col">
|
||||
<div className="w-[700px] max-h-[90vh] overflow-y-auto glass-holo p-8 border border-neon-blue shadow-glow-blue relative flex flex-col">
|
||||
<button
|
||||
onClick={onClose}
|
||||
disabled={isInitializing}
|
||||
@@ -103,10 +141,22 @@ export const InitializeModal: React.FC<InitializeModalProps> = ({ isOpen, onClos
|
||||
</button>
|
||||
|
||||
<h2 className="text-2xl font-tech font-bold text-neon-blue mb-8 border-b border-white/10 pb-4 flex items-center gap-3">
|
||||
<Target className="animate-pulse" /> AXIS INITIALIZATION
|
||||
<Target className="animate-pulse" /> DEVICE INITIALIZATION
|
||||
</h2>
|
||||
|
||||
<div className="space-y-6 mb-8">
|
||||
{errorMessage && (
|
||||
<div className="mb-6 p-4 bg-red-500/10 border border-red-500 rounded flex items-start gap-3">
|
||||
<AlertCircle className="w-5 h-5 text-red-500 flex-shrink-0 mt-0.5" />
|
||||
<div className="flex-1">
|
||||
<div className="font-tech font-bold text-red-500 mb-1">INITIALIZATION ERROR</div>
|
||||
<pre className="text-sm text-slate-300 whitespace-pre-wrap font-mono">
|
||||
{errorMessage}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-4 mb-8">
|
||||
{axes.map((axis, index) => (
|
||||
<div key={axis.name} className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
@@ -118,7 +168,7 @@ export const InitializeModal: React.FC<InitializeModalProps> = ({ isOpen, onClos
|
||||
) : (
|
||||
<div className="w-5 h-5 border-2 border-slate-600 rounded-full" />
|
||||
)}
|
||||
<span className="font-tech font-bold text-lg text-white tracking-wider">
|
||||
<span className="font-tech font-bold text-base text-white tracking-wider">
|
||||
{axis.name}
|
||||
</span>
|
||||
</div>
|
||||
@@ -140,7 +190,7 @@ export const InitializeModal: React.FC<InitializeModalProps> = ({ isOpen, onClos
|
||||
</div>
|
||||
|
||||
{/* Progress Bar */}
|
||||
<div className="h-3 bg-black/50 border border-slate-700 overflow-hidden">
|
||||
<div className="h-2.5 bg-black/50 border border-slate-700 overflow-hidden">
|
||||
<div
|
||||
className={`h-full transition-all duration-100 ${
|
||||
axis.status === 'completed'
|
||||
|
||||
133
FrontEnd/components/ProcessedDataPanel.tsx
Normal file
133
FrontEnd/components/ProcessedDataPanel.tsx
Normal file
@@ -0,0 +1,133 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Database, RefreshCw } from 'lucide-react';
|
||||
import { comms } from '../communication';
|
||||
import { ProcessedDataRow } from '../types';
|
||||
import { PanelHeader } from './common/PanelHeader';
|
||||
import { CyberPanel } from './common/CyberPanel';
|
||||
|
||||
export const ProcessedDataPanel: React.FC = () => {
|
||||
const [data, setData] = useState<ProcessedDataRow[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const loadData = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const dataJson = await comms.getProcessedData();
|
||||
const parsedData = JSON.parse(dataJson) as ProcessedDataRow[];
|
||||
// Display only the first 5 rows
|
||||
setData(parsedData.slice(0, 5));
|
||||
} catch (error) {
|
||||
console.error('[ProcessedDataPanel] Failed to load data:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
loadData();
|
||||
// Refresh every 5 seconds
|
||||
const interval = setInterval(loadData, 5000);
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
|
||||
const getRowBackgroundColor = (row: ProcessedDataRow) => {
|
||||
if (row.REMARK.startsWith('(BYPASS')) {
|
||||
return 'bg-sky-300/20';
|
||||
}
|
||||
return row.LOC === 'L' ? 'bg-slate-700/20' : 'bg-slate-800/10';
|
||||
};
|
||||
|
||||
const getRowTextColor = (row: ProcessedDataRow) => {
|
||||
if (row.REMARK.startsWith('(BYPASS')) {
|
||||
return 'text-white';
|
||||
}
|
||||
if (!row.PRNATTACH) {
|
||||
return 'text-red-500';
|
||||
}
|
||||
if (!row.PRNVALID) {
|
||||
return 'text-emerald-900';
|
||||
}
|
||||
return 'text-slate-200';
|
||||
};
|
||||
|
||||
return (
|
||||
<CyberPanel className="flex flex-col h-full">
|
||||
<PanelHeader
|
||||
icon={Database}
|
||||
title="PROCESSED DATA"
|
||||
className="mb-3"
|
||||
>
|
||||
<button
|
||||
onClick={loadData}
|
||||
disabled={loading}
|
||||
className="p-1.5 hover:bg-white/10 rounded transition-colors disabled:opacity-50"
|
||||
title="Refresh"
|
||||
>
|
||||
<RefreshCw className={`w-4 h-4 ${loading ? 'animate-spin' : ''}`} />
|
||||
</button>
|
||||
</PanelHeader>
|
||||
|
||||
<div className="flex-1 overflow-auto">
|
||||
<table className="w-full text-xs border-collapse">
|
||||
<thead className="sticky top-0 bg-slate-900/90 backdrop-blur-sm z-10">
|
||||
<tr className="border-b border-neon-blue/30">
|
||||
<th className="px-2 py-1.5 text-left font-tech text-neon-blue">R</th>
|
||||
<th className="px-2 py-1.5 text-left font-tech text-neon-blue">MODEL</th>
|
||||
<th className="px-2 py-1.5 text-left font-tech text-neon-blue">START</th>
|
||||
<th className="px-2 py-1.5 text-left font-tech text-neon-blue">BATCH</th>
|
||||
<th className="px-2 py-1.5 text-left font-tech text-neon-blue">SID</th>
|
||||
<th className="px-2 py-1.5 text-left font-tech text-neon-blue">RID</th>
|
||||
<th className="px-2 py-1.5 text-left font-tech text-neon-blue">VENDER</th>
|
||||
<th className="px-2 py-1.5 text-right font-tech text-neon-blue">QTY</th>
|
||||
<th className="px-2 py-1.5 text-right font-tech text-neon-blue">(MAX)</th>
|
||||
<th className="px-2 py-1.5 text-left font-tech text-neon-blue">MFG</th>
|
||||
<th className="px-2 py-1.5 text-left font-tech text-neon-blue">V.LOT</th>
|
||||
<th className="px-2 py-1.5 text-left font-tech text-neon-blue">PARTNO</th>
|
||||
<th className="px-2 py-1.5 text-left font-tech text-neon-blue">CPN</th>
|
||||
<th className="px-2 py-1.5 text-left font-tech text-neon-blue">Remark</th>
|
||||
<th className="px-2 py-1.5 text-center font-tech text-neon-blue">Attach</th>
|
||||
<th className="px-2 py-1.5 text-center font-tech text-neon-blue">Valid</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{data.length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan={16} className="px-2 py-8 text-center text-slate-500 font-mono text-xs">
|
||||
{loading ? 'Loading...' : 'No data available'}
|
||||
</td>
|
||||
</tr>
|
||||
) : (
|
||||
data.map((row, index) => (
|
||||
<tr
|
||||
key={index}
|
||||
className={`border-b border-white/5 ${getRowBackgroundColor(row)} ${getRowTextColor(row)} hover:bg-white/5 transition-colors`}
|
||||
>
|
||||
<td className="px-2 py-1.5 font-mono text-center">{row.target}</td>
|
||||
<td className="px-2 py-1.5 font-mono">{row.JTYPE}</td>
|
||||
<td className="px-2 py-1.5 font-mono">{row.STIME}</td>
|
||||
<td className="px-2 py-1.5 font-mono">{row.BATCH}</td>
|
||||
<td className="px-2 py-1.5 font-mono font-bold">{row.SID}</td>
|
||||
<td className="px-2 py-1.5 font-mono font-bold">{row.RID}</td>
|
||||
<td className="px-2 py-1.5 font-mono text-center">{row.VNAME}</td>
|
||||
<td className="px-2 py-1.5 font-mono text-right">{row.QTY.toLocaleString()}</td>
|
||||
<td className="px-2 py-1.5 font-mono text-right">{row.qtymax.toLocaleString()}</td>
|
||||
<td className="px-2 py-1.5 font-mono text-center">{row.MFGDATE}</td>
|
||||
<td className="px-2 py-1.5 font-mono">{row.VLOT}</td>
|
||||
<td className="px-2 py-1.5 font-mono">{row.PARTNO}</td>
|
||||
<td className="px-2 py-1.5 font-mono">{row.MCN}</td>
|
||||
<td className="px-2 py-1.5 font-mono">{row.REMARK}</td>
|
||||
<td className="px-2 py-1.5 text-center">
|
||||
<span className={`inline-block w-3 h-3 rounded-full ${row.PRNATTACH ? 'bg-neon-green' : 'bg-red-500'}`} />
|
||||
</td>
|
||||
<td className="px-2 py-1.5 text-center">
|
||||
<span className={`inline-block w-3 h-3 rounded-full ${row.PRNVALID ? 'bg-neon-green' : 'bg-red-500'}`} />
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</CyberPanel>
|
||||
);
|
||||
};
|
||||
53
FrontEnd/components/SystemStatusPanel.tsx
Normal file
53
FrontEnd/components/SystemStatusPanel.tsx
Normal file
@@ -0,0 +1,53 @@
|
||||
import React from 'react';
|
||||
import { Play, Square, RotateCw } from 'lucide-react';
|
||||
import { TechButton } from './common/TechButton';
|
||||
import { CyberPanel } from './common/CyberPanel';
|
||||
import { SystemState } from '../types';
|
||||
|
||||
interface SystemStatusPanelProps {
|
||||
systemState: SystemState;
|
||||
onControl: (action: 'start' | 'stop' | 'reset') => void;
|
||||
}
|
||||
|
||||
export const SystemStatusPanel: React.FC<SystemStatusPanelProps> = ({
|
||||
systemState,
|
||||
onControl
|
||||
}) => {
|
||||
return (
|
||||
<CyberPanel className="flex-none">
|
||||
<div className="mb-3 text-xs text-neon-blue font-bold tracking-widest uppercase">System Status</div>
|
||||
<div className={`text-2xl font-tech font-bold mb-4 ${systemState === SystemState.RUNNING ? 'text-neon-green text-shadow-glow-green' : 'text-slate-400'}`}>
|
||||
{systemState}
|
||||
</div>
|
||||
|
||||
{/* Horizontal Button Layout */}
|
||||
<div className="grid grid-cols-3 gap-2">
|
||||
<TechButton
|
||||
variant="green"
|
||||
className="flex flex-col items-center justify-center gap-1 h-20"
|
||||
active={systemState === SystemState.RUNNING}
|
||||
onClick={() => onControl('start')}
|
||||
>
|
||||
<Play className="w-5 h-5" />
|
||||
<span className="text-xs">START</span>
|
||||
</TechButton>
|
||||
<TechButton
|
||||
variant="amber"
|
||||
className="flex flex-col items-center justify-center gap-1 h-20"
|
||||
active={systemState === SystemState.PAUSED}
|
||||
onClick={() => onControl('stop')}
|
||||
>
|
||||
<Square className="w-5 h-5 fill-current" />
|
||||
<span className="text-xs">STOP</span>
|
||||
</TechButton>
|
||||
<TechButton
|
||||
className="flex flex-col items-center justify-center gap-1 h-20"
|
||||
onClick={() => onControl('reset')}
|
||||
>
|
||||
<RotateCw className="w-5 h-5" />
|
||||
<span className="text-xs">RESET</span>
|
||||
</TechButton>
|
||||
</div>
|
||||
</CyberPanel>
|
||||
);
|
||||
};
|
||||
156
FrontEnd/components/VisionMenu.tsx
Normal file
156
FrontEnd/components/VisionMenu.tsx
Normal file
@@ -0,0 +1,156 @@
|
||||
import React, { useState, useRef, useEffect } from 'react';
|
||||
import { Camera, ChevronRight, X } from 'lucide-react';
|
||||
|
||||
interface VisionMenuProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export const VisionMenu: React.FC<VisionMenuProps> = ({ isOpen, onClose }) => {
|
||||
const [activeSubmenu, setActiveSubmenu] = useState<string | null>(null);
|
||||
const menuRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (menuRef.current && !menuRef.current.contains(event.target as Node)) {
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
if (isOpen) {
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
}
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', handleClickOutside);
|
||||
};
|
||||
}, [isOpen, onClose]);
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
const handleQRCodeCamera = () => {
|
||||
console.log('[VisionMenu] Camera (QRCode) clicked');
|
||||
// TODO: Implement QR Code camera functionality
|
||||
};
|
||||
|
||||
const handleKeyenceBarcode = () => {
|
||||
console.log('[VisionMenu] Barcode (Keyence) clicked');
|
||||
// TODO: Implement Keyence barcode functionality
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={menuRef}
|
||||
className="absolute top-20 left-1/2 -translate-x-1/2 z-50 bg-black/95 backdrop-blur-md border border-neon-blue/50 rounded-lg shadow-glow-blue min-w-[300px]"
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between p-4 border-b border-white/10">
|
||||
<div className="flex items-center gap-3">
|
||||
<Camera className="w-5 h-5 text-neon-blue" />
|
||||
<h3 className="font-tech font-bold text-lg text-white">VISION</h3>
|
||||
</div>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="text-slate-400 hover:text-white transition-colors"
|
||||
>
|
||||
<X className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Menu Items */}
|
||||
<div className="p-2">
|
||||
{/* Camera (QRCode) */}
|
||||
<div className="relative">
|
||||
<button
|
||||
onMouseEnter={() => setActiveSubmenu('qrcode')}
|
||||
onClick={handleQRCodeCamera}
|
||||
className="w-full flex items-center justify-between px-4 py-3 text-left text-slate-300 hover:bg-neon-blue/10 hover:text-neon-blue rounded transition-colors font-tech"
|
||||
>
|
||||
<span>Camera (QRCode)</span>
|
||||
<ChevronRight className="w-4 h-4" />
|
||||
</button>
|
||||
|
||||
{/* QRCode Submenu */}
|
||||
{activeSubmenu === 'qrcode' && (
|
||||
<div
|
||||
className="absolute left-full top-0 ml-1 bg-black/95 backdrop-blur-md border border-neon-blue/50 rounded-lg shadow-glow-blue min-w-[200px] p-2"
|
||||
onMouseLeave={() => setActiveSubmenu(null)}
|
||||
>
|
||||
<button
|
||||
onClick={() => console.log('[VisionMenu] QRCode - Connect')}
|
||||
className="w-full px-4 py-2 text-left text-slate-300 hover:bg-neon-blue/10 hover:text-neon-blue rounded transition-colors text-sm"
|
||||
>
|
||||
Connect
|
||||
</button>
|
||||
<div className="h-px bg-white/10 my-1" />
|
||||
<button
|
||||
onClick={() => console.log('[VisionMenu] QRCode - Get Image')}
|
||||
className="w-full px-4 py-2 text-left text-slate-300 hover:bg-neon-blue/10 hover:text-neon-blue rounded transition-colors text-sm"
|
||||
>
|
||||
Get Image
|
||||
</button>
|
||||
<button
|
||||
onClick={() => console.log('[VisionMenu] QRCode - Live View')}
|
||||
className="w-full px-4 py-2 text-left text-slate-300 hover:bg-neon-blue/10 hover:text-neon-blue rounded transition-colors text-sm"
|
||||
>
|
||||
Live View
|
||||
</button>
|
||||
<button
|
||||
onClick={() => console.log('[VisionMenu] QRCode - Read Test')}
|
||||
className="w-full px-4 py-2 text-left text-slate-300 hover:bg-neon-blue/10 hover:text-neon-blue rounded transition-colors text-sm"
|
||||
>
|
||||
Read Test
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Barcode (Keyence) */}
|
||||
<div className="relative">
|
||||
<button
|
||||
onMouseEnter={() => setActiveSubmenu('keyence')}
|
||||
onClick={handleKeyenceBarcode}
|
||||
className="w-full flex items-center justify-between px-4 py-3 text-left text-slate-300 hover:bg-neon-blue/10 hover:text-neon-blue rounded transition-colors font-tech"
|
||||
>
|
||||
<span>Barcode (Keyence)</span>
|
||||
<ChevronRight className="w-4 h-4" />
|
||||
</button>
|
||||
|
||||
{/* Keyence Submenu */}
|
||||
{activeSubmenu === 'keyence' && (
|
||||
<div
|
||||
className="absolute left-full top-0 ml-1 bg-black/95 backdrop-blur-md border border-neon-blue/50 rounded-lg shadow-glow-blue min-w-[200px] p-2"
|
||||
onMouseLeave={() => setActiveSubmenu(null)}
|
||||
>
|
||||
<button
|
||||
onClick={() => console.log('[VisionMenu] Keyence - Get Image')}
|
||||
className="w-full px-4 py-2 text-left text-slate-300 hover:bg-neon-blue/10 hover:text-neon-blue rounded transition-colors text-sm"
|
||||
>
|
||||
Get Image
|
||||
</button>
|
||||
<div className="h-px bg-white/10 my-1" />
|
||||
<button
|
||||
onClick={() => console.log('[VisionMenu] Keyence - Trigger On')}
|
||||
className="w-full px-4 py-2 text-left text-slate-300 hover:bg-neon-blue/10 hover:text-neon-blue rounded transition-colors text-sm"
|
||||
>
|
||||
Trigger On
|
||||
</button>
|
||||
<button
|
||||
onClick={() => console.log('[VisionMenu] Keyence - Trigger Off')}
|
||||
className="w-full px-4 py-2 text-left text-slate-300 hover:bg-neon-blue/10 hover:text-neon-blue rounded transition-colors text-sm"
|
||||
>
|
||||
Trigger Off
|
||||
</button>
|
||||
<button
|
||||
onClick={() => console.log('[VisionMenu] Keyence - Save Image')}
|
||||
className="w-full px-4 py-2 text-left text-slate-300 hover:bg-neon-blue/10 hover:text-neon-blue rounded transition-colors text-sm"
|
||||
>
|
||||
Save Image
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,6 +1,7 @@
|
||||
import React from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import { useNavigate, useLocation } from 'react-router-dom';
|
||||
import { Activity, Settings, Move, Camera, Layers, Cpu, Target } from 'lucide-react';
|
||||
import { VisionMenu } from '../VisionMenu';
|
||||
|
||||
interface HeaderProps {
|
||||
currentTime: Date;
|
||||
@@ -12,12 +13,15 @@ interface HeaderProps {
|
||||
export const Header: React.FC<HeaderProps> = ({ currentTime, onTabChange, activeTab }) => {
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const [showVisionMenu, setShowVisionMenu] = useState(false);
|
||||
|
||||
const isWebView = typeof window !== 'undefined' && !!window.chrome?.webview;
|
||||
const isIOPage = location.pathname === '/io-monitor';
|
||||
|
||||
return (
|
||||
<header className="absolute top-0 left-0 right-0 h-20 px-6 flex items-center justify-between z-40 bg-gradient-to-b from-black/80 to-transparent pointer-events-none">
|
||||
<>
|
||||
<VisionMenu isOpen={showVisionMenu} onClose={() => setShowVisionMenu(false)} />
|
||||
<header className="absolute top-0 left-0 right-0 h-20 px-6 flex items-center justify-between z-40 bg-gradient-to-b from-black/80 to-transparent pointer-events-none">
|
||||
<div
|
||||
className="flex items-center gap-4 pointer-events-auto cursor-pointer group"
|
||||
onClick={() => {
|
||||
@@ -47,7 +51,7 @@ export const Header: React.FC<HeaderProps> = ({ currentTime, onTabChange, active
|
||||
{/* IO Tab Switcher (only on IO page) */}
|
||||
|
||||
|
||||
<div className="bg-black/40 backdrop-blur-md p-1 rounded-full border border-white/10 flex gap-1">
|
||||
<div className="bg-black/40 backdrop-blur-md p-1 rounded-2xl border border-white/10 flex gap-1">
|
||||
{[
|
||||
{ id: 'recipe', icon: Layers, label: 'RECIPE', path: '/' },
|
||||
{ id: 'io', icon: Activity, label: 'I/O MONITOR', path: '/io-monitor' },
|
||||
@@ -67,21 +71,27 @@ export const Header: React.FC<HeaderProps> = ({ currentTime, onTabChange, active
|
||||
if (item.id === 'io') {
|
||||
navigate('/io-monitor');
|
||||
onTabChange(null);
|
||||
setShowVisionMenu(false);
|
||||
} else if (item.id === 'camera') {
|
||||
setShowVisionMenu(!showVisionMenu);
|
||||
onTabChange(null);
|
||||
} else {
|
||||
if (location.pathname !== '/') {
|
||||
navigate('/');
|
||||
}
|
||||
setShowVisionMenu(false);
|
||||
onTabChange(activeTab === item.id ? null : item.id as any);
|
||||
}
|
||||
}}
|
||||
className={`
|
||||
flex items-center gap-2 px-6 py-2 rounded-full font-tech font-bold text-sm transition-all border border-transparent
|
||||
flex flex-col items-center justify-center gap-1 px-3 py-2 rounded-xl font-tech font-bold text-[10px] transition-all border border-transparent min-w-[70px]
|
||||
${isActive
|
||||
? 'bg-neon-blue/10 text-neon-blue border-neon-blue shadow-glow-blue'
|
||||
: 'text-slate-400 hover:text-white hover:bg-white/5'}
|
||||
`}
|
||||
>
|
||||
<item.icon className="w-4 h-4" /> {item.label}
|
||||
<item.icon className="w-5 h-5" />
|
||||
<span className="leading-tight">{item.label}</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
@@ -97,5 +107,6 @@ export const Header: React.FC<HeaderProps> = ({ currentTime, onTabChange, active
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Play, Square, RotateCw, AlertTriangle, Siren, Terminal } from 'lucide-react';
|
||||
import { AlertTriangle, Siren } from 'lucide-react';
|
||||
import { Machine3D } from '../components/Machine3D';
|
||||
import { SettingsModal } from '../components/SettingsModal';
|
||||
import { InitializeModal } from '../components/InitializeModal';
|
||||
import { RecipePanel } from '../components/RecipePanel';
|
||||
import { MotionPanel } from '../components/MotionPanel';
|
||||
import { CameraPanel } from '../components/CameraPanel';
|
||||
import { CyberPanel } from '../components/common/CyberPanel';
|
||||
import { TechButton } from '../components/common/TechButton';
|
||||
import { ModelInfoPanel } from '../components/ModelInfoPanel';
|
||||
import { ProcessedDataPanel } from '../components/ProcessedDataPanel';
|
||||
import { SystemStatusPanel } from '../components/SystemStatusPanel';
|
||||
import { EventLogPanel } from '../components/EventLogPanel';
|
||||
import { SystemState, Recipe, IOPoint, LogEntry, RobotTarget, ConfigItem } from '../types';
|
||||
|
||||
interface HomePageProps {
|
||||
@@ -46,14 +47,8 @@ export const HomePage: React.FC<HomePageProps> = ({
|
||||
onCloseTab,
|
||||
videoRef
|
||||
}) => {
|
||||
useEffect(() => {
|
||||
if (activeTab === 'camera' && navigator.mediaDevices?.getUserMedia) {
|
||||
navigator.mediaDevices.getUserMedia({ video: true }).then(s => { if (videoRef.current) videoRef.current.srcObject = s });
|
||||
}
|
||||
}, [activeTab, videoRef]);
|
||||
|
||||
return (
|
||||
<main className="relative w-full h-full flex gap-6 px-6">
|
||||
<main className="relative w-full h-full">
|
||||
{/* 3D Canvas (Background Layer) */}
|
||||
<div className="absolute inset-0 z-0">
|
||||
<Machine3D target={robotTarget} ioState={ioPoints} doorStates={doorStates} />
|
||||
@@ -78,11 +73,10 @@ export const HomePage: React.FC<HomePageProps> = ({
|
||||
</div>
|
||||
|
||||
{/* Floating Panel (Left) */}
|
||||
{activeTab && activeTab !== 'setting' && activeTab !== 'recipe' && (
|
||||
<div className="w-[450px] z-20 animate-in slide-in-from-left-20 duration-500 fade-in">
|
||||
{activeTab === 'motion' && (
|
||||
<div className="absolute left-6 top-0 bottom-52 w-[450px] z-20 animate-in slide-in-from-left-20 duration-500 fade-in">
|
||||
<CyberPanel className="h-full flex flex-col">
|
||||
{activeTab === 'motion' && <MotionPanel robotTarget={robotTarget} onMove={onMove} />}
|
||||
{activeTab === 'camera' && <CameraPanel videoRef={videoRef} />}
|
||||
<MotionPanel robotTarget={robotTarget} onMove={onMove} />
|
||||
</CyberPanel>
|
||||
</div>
|
||||
)}
|
||||
@@ -109,54 +103,15 @@ export const HomePage: React.FC<HomePageProps> = ({
|
||||
/>
|
||||
|
||||
{/* Right Sidebar (Dashboard) */}
|
||||
<div className="w-80 ml-auto z-20 flex flex-col gap-4">
|
||||
<div className="absolute right-6 top-0 bottom-52 w-80 z-20 flex flex-col gap-4">
|
||||
<ModelInfoPanel currentRecipe={currentRecipe} />
|
||||
<SystemStatusPanel systemState={systemState} onControl={onControl} />
|
||||
<EventLogPanel logs={logs} />
|
||||
</div>
|
||||
|
||||
<CyberPanel className="flex-none">
|
||||
<div className="mb-2 text-xs text-neon-blue font-bold tracking-widest uppercase">System Status</div>
|
||||
<div className={`text-3xl font-tech font-bold mb-4 ${systemState === SystemState.RUNNING ? 'text-neon-green text-shadow-glow-green' : 'text-slate-400'}`}>
|
||||
{systemState}
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
<TechButton
|
||||
variant="green"
|
||||
className="w-full flex items-center justify-center gap-2"
|
||||
active={systemState === SystemState.RUNNING}
|
||||
onClick={() => onControl('start')}
|
||||
>
|
||||
<Play className="w-4 h-4" /> START AUTO
|
||||
</TechButton>
|
||||
<TechButton
|
||||
variant="amber"
|
||||
className="w-full flex items-center justify-center gap-2"
|
||||
active={systemState === SystemState.PAUSED}
|
||||
onClick={() => onControl('stop')}
|
||||
>
|
||||
<Square className="w-4 h-4 fill-current" /> STOP / PAUSE
|
||||
</TechButton>
|
||||
<TechButton
|
||||
className="w-full flex items-center justify-center gap-2"
|
||||
onClick={() => onControl('reset')}
|
||||
>
|
||||
<RotateCw className="w-4 h-4" /> SYSTEM RESET
|
||||
</TechButton>
|
||||
</div>
|
||||
</CyberPanel>
|
||||
|
||||
<CyberPanel className="flex-1 flex flex-col min-h-0">
|
||||
<div className="mb-2 flex items-center justify-between text-xs text-neon-blue font-bold tracking-widest uppercase border-b border-white/10 pb-2">
|
||||
<span>Event Log</span>
|
||||
<Terminal className="w-3 h-3" />
|
||||
</div>
|
||||
<div className="flex-1 overflow-y-auto font-mono text-[10px] space-y-1 pr-1 custom-scrollbar">
|
||||
{logs.map(log => (
|
||||
<div key={log.id} className={`flex gap-2 ${log.type === 'error' ? 'text-red-500' : log.type === 'warning' ? 'text-amber-400' : 'text-slate-400'}`}>
|
||||
<span className="opacity-50">[{log.timestamp}]</span>
|
||||
<span>{log.message}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CyberPanel>
|
||||
{/* Bottom Docked - Processed Data Panel */}
|
||||
<div className="absolute left-6 right-6 bottom-10 h-48 z-20">
|
||||
<ProcessedDataPanel />
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
|
||||
@@ -58,6 +58,26 @@ export interface ConfigItem {
|
||||
Description: string;
|
||||
}
|
||||
|
||||
export interface ProcessedDataRow {
|
||||
target: string;
|
||||
JTYPE: string;
|
||||
STIME: string;
|
||||
BATCH: string;
|
||||
SID: string;
|
||||
RID: string;
|
||||
VNAME: string;
|
||||
LOC: string;
|
||||
QTY: number;
|
||||
qtymax: number;
|
||||
MFGDATE: string;
|
||||
VLOT: string;
|
||||
PARTNO: string;
|
||||
MCN: string;
|
||||
REMARK: string;
|
||||
PRNATTACH: boolean;
|
||||
PRNVALID: boolean;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
chrome?: {
|
||||
@@ -76,6 +96,9 @@ declare global {
|
||||
GetRecipe(recipeTitle: string): Promise<string>;
|
||||
SaveRecipe(recipeTitle: string, recipeData: string): Promise<string>;
|
||||
SaveConfig(configJson: string): Promise<void>;
|
||||
InitializeDevice(): Promise<string>;
|
||||
GetInitializeStatus(): Promise<string>;
|
||||
GetProcessedData(): Promise<string>;
|
||||
}
|
||||
};
|
||||
addEventListener(type: string, listener: (event: any) => void): void;
|
||||
|
||||
Reference in New Issue
Block a user