Newer
Older
CNCTools / ReferenceSurfaceGenerator / frontend / src / features / dxf_layered_curves / index.js
import React, { useState, useEffect } from 'react';
import axios from 'axios';
import { Container, Card, ProgressBar, Alert, Button, Form, Row, Col, ListGroup, Badge } from 'react-bootstrap';

const API_URL = process.env.NODE_ENV === 'development' ? 'http://localhost:8000' : '';
const WS_URL = process.env.NODE_ENV === 'development' 
    ? 'ws://localhost:8000' 
    : window.location.protocol.replace('http', 'ws') + '//' + window.location.host;

// --- Individual Job Item Component ---
const JobItem = ({ job, onJobDelete }) => {
  const getVariant = (status) => {
    switch (status) {
      case 'PENDING': return 'info';
      case 'PROCESSING': return 'primary';
      case 'COMPLETE': return 'success';
      case 'FAILED': return 'danger';
      default: return 'secondary';
    }
  };

  const handleDelete = async () => {
    if (window.confirm(`Are you sure you want to delete job ${job.filename} (${job.id})?`)) {
      try {
        await axios.delete(`${API_URL}/api/jobs/${job.id}`);
        onJobDelete(job.id);
      } catch (error) {
        console.error("Error deleting job:", error);
        alert("Failed to delete job.");
      }
    }
  };

  return (
    <ListGroup.Item>
      <div className="d-flex justify-content-between align-items-center">
        <div>
          <strong>{job.filename}</strong> (ID: {job.id.substring(0, 8) + '...'})
          <br />
          <small className="text-muted">Status: <Badge bg={getVariant(job.status)}>{job.status}</Badge></small>
        </div>
        <div>
          {job.status === 'COMPLETE' && job.download_url && (
            <Button variant="success" href={`${API_URL}${job.download_url}`} size="sm" className="me-2">
              Download
            </Button>
          )}
          <Button variant="outline-danger" size="sm" onClick={handleDelete}>
            Delete
          </Button>
        </div>
      </div>

      {job.status === 'PROCESSING' && <ProgressBar now={job.progress} label={`${job.progress}%`} animated className="mt-2" />}
      
      {job.status !== 'FAILED' && job.message && (
        <small className="d-block mt-1 text-muted">{job.message}</small>
      )}

      {job.status === 'FAILED' && job.message && (
        <Alert variant="danger" className="mt-2 mb-0" style={{ fontSize: '0.875em', padding: '0.5rem 1rem' }}>
          <strong>Error:</strong> {job.message}
        </Alert>
      )}
    </ListGroup.Item>
  );
};

// --- Upload Component (modified) ---
const UploadComponent = ({ setJobs }) => {
  const [file, setFile] = useState(null);
  const [numLayers, setNumLayers] = useState(20);
  const [numPoints, setNumPoints] = useState(30);
  const [uploadProgress, setUploadProgress] = useState(0);
  const [uploadStatusMessage, setUploadStatusMessage] = useState('Select a file to begin.');
  const [error, setError] = useState(null);
  const [isUploading, setIsUploading] = useState(false);

  const handleFileChange = (event) => {
    setFile(event.target.files[0]);
    setUploadStatusMessage(event.target.files[0] ? event.target.files[0].name : 'Select a file to begin.');
    setUploadProgress(0);
    setError(null);
  };

  const handleUpload = async () => {
    if (!file) return;

    setIsUploading(true);
    setUploadProgress(0);
    setError(null);
    setUploadStatusMessage('Uploading file...');

    const formData = new FormData();
    formData.append('file', file);
    formData.append('num_layers', numLayers);
    formData.append('num_points_per_layer', numPoints);

    try {
      const uploadResponse = await axios.post(`${API_URL}/api/features/dxf_layered_curves/upload/`, formData, {
        onUploadProgress: (progressEvent) => {
          const percentCompleted = Math.round((progressEvent.loaded * 100) / progressEvent.total);
          setUploadProgress(percentCompleted);
          setUploadStatusMessage(`Uploading... ${percentCompleted}%`);
        },
      });

      const { job_id, filename, status } = uploadResponse.data;
      setJobs(prevJobs => [{
        id: job_id,
        filename: filename,
        status: status,
        progress: 0,
        message: `File '${filename}' uploaded, awaiting processing.`, 
        params: { num_layers: numLayers, num_points_per_layer: numPoints },
        timestamp: new Date().toISOString()
      }, ...prevJobs]);

      setUploadStatusMessage(`Job ${job_id.substring(0, 8)}... created. Waiting for WebSocket updates.`);
      setUploadProgress(0);
      setFile(null);

    } catch (err) {
      console.error(err);
      setError(err.response?.data?.detail || 'File upload failed. Please try again.');
    } finally {
      setIsUploading(false);
    }
  };

  return (
    <Card className="mb-4">
      <Card.Body>
        <Card.Title>Upload Mesh File</Card.Title>
        <Card.Text>
          Select a <code>.obj</code>, <code>.stl</code>, or <code>.3mf</code> file to process. The tool will generate a DXF file containing the layered profiles of your model.
        </Card.Text>
        <Form>
          <Form.Group controlId="formFile" className="mb-3">
            <Form.Label>Select Mesh File</Form.Label>
            <Form.Control type="file" onChange={handleFileChange} accept=".obj,.stl,.3mf" />
          </Form.Group>

          <Row className="mb-3">
            <Form.Group as={Col} controlId="formNumLayers">
              <Form.Label>Number of Layers</Form.Label>
              <Form.Control 
                type="number" 
                value={numLayers} 
                onChange={(e) => setNumLayers(parseInt(e.target.value))} 
                min="2" 
                max="100" 
              />
            </Form.Group>
            <Form.Group as={Col} controlId="formNumPoints">
              <Form.Label>Points per Layer</Form.Label>
              <Form.Control 
                type="number" 
                value={numPoints} 
                onChange={(e) => setNumPoints(parseInt(e.target.value))} 
                min="3" 
                max="200" 
              />
            </Form.Group>
          </Row>
          
          {file && (
            <Button variant="primary" onClick={handleUpload} className="mb-3" disabled={isUploading}>
              {isUploading ? 'Processing...' : 'Start Processing'}
            </Button>
          )}
        </Form>

        <hr />

        <h6>Upload Status</h6>
        <p className="text-muted">{uploadStatusMessage}</p>
        {uploadProgress > 0 && uploadProgress < 100 && (
            <ProgressBar now={uploadProgress} label={`${uploadProgress}%`} animated />
        )}

        {error && (
          <Alert variant="danger" className="mt-3">
            {error}
          </Alert>
        )}
      </Card.Body>
    </Card>
  );
};

const DxfLayeredCurves = () => {
    const [jobs, setJobs] = useState([]);
    const webSocketRef = React.useRef(null);
  
    useEffect(() => {
      const fetchJobs = async () => {
        try {
          const response = await axios.get(`${API_URL}/api/jobs`);
          const sortedJobs = response.data.sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp));
          setJobs(sortedJobs.filter(j => j.feature_id === 'dxf_layered_curves'));
        } catch (error) {
          console.error("Error fetching jobs:", error);
        }
      };
      
      fetchJobs();
      const interval = setInterval(fetchJobs, 5000);
  
      return () => clearInterval(interval);
    }, []);
  
    useEffect(() => {
      const jobToTrack = jobs.find(j => j.status === 'QUEUED' || j.status === 'PROCESSING');
  
      if (!jobToTrack || (webSocketRef.current && webSocketRef.current.jobId === jobToTrack.id)) {
        return;
      }
  
      if (webSocketRef.current) {
        webSocketRef.current.close();
      }
  
      const ws = new WebSocket(`${WS_URL}/ws/${jobToTrack.id}`);
      ws.jobId = jobToTrack.id;
  
      ws.onopen = () => console.log(`WebSocket connected for job ${jobToTrack.id}`);
  
      ws.onmessage = (event) => {
        const updatedJob = JSON.parse(event.data);
        setJobs(prevJobs => {
          const jobsMap = new Map(prevJobs.map(j => [j.id, j]));
          jobsMap.set(updatedJob.id, { ...jobsMap.get(updatedJob.id), ...updatedJob });
          return Array.from(jobsMap.values()).sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp));
        });
      };
      
      ws.onclose = (event) => {
        console.log(`WebSocket disconnected for job ${jobToTrack.id}. Code: ${event.code}, Reason: ${event.reason}`);
        if (webSocketRef.current && webSocketRef.current.jobId === jobToTrack.id) {
            webSocketRef.current = null;
        }
      };
      
      ws.onerror = (error) => console.error(`WebSocket error for job ${jobToTrack.id}:`, error);
  
      webSocketRef.current = ws;
  
      return () => {
        if (ws && ws.readyState < 2) {
          console.log(`Cleaning up WebSocket for job ${ws.jobId}`);
          ws.close();
        }
      };
    }, [jobs]);
  
    const handleJobDelete = (jobId) => {
      setJobs(prevJobs => prevJobs.filter(job => job.id !== jobId));
    };
  
    const processingJobs = jobs.filter(j => j.status === 'PROCESSING' || j.status === 'QUEUED');
    const otherJobs = jobs.filter(j => j.status !== 'PROCESSING' && j.status !== 'QUEUED');
    
    return (
      <Container>
        <h1>DXF Layered Curves</h1>
        <UploadComponent setJobs={setJobs} />
  
        {processingJobs.length > 0 && (
          <Card className="mt-4">
            <Card.Body>
              <Card.Title>Currently Processing</Card.Title>
              <ListGroup variant="flush">
                {processingJobs.map(job => (
                   <JobItem key={job.id} job={job} onJobDelete={handleJobDelete} />
                ))}
              </ListGroup>
            </Card.Body>
          </Card>
        )}
  
        <Card className="mt-4">
          <Card.Body>
            <Card.Title>Job History</Card.Title>
            {otherJobs.length === 0 && processingJobs.length === 0 ? (
              <Card.Text>No jobs to show. Upload a file to begin.</Card.Text>
            ) : (
              <ListGroup variant="flush">
                {otherJobs.map(job => (
                  <JobItem key={job.id} job={job} onJobDelete={handleJobDelete} />
                ))}
              </ListGroup>
            )}
          </Card.Body>
        </Card>
      </Container>
    );
}

export default DxfLayeredCurves;