CompareArchivesModal.tsx 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190
  1. import { useEffect } from 'react';
  2. import { useQuery } from '@tanstack/react-query';
  3. import { X, Check, AlertTriangle, Loader2 } from 'lucide-react';
  4. import { api } from '../api/client';
  5. import type { ArchiveComparison } from '../api/client';
  6. import { Button } from './Button';
  7. interface CompareArchivesModalProps {
  8. archiveIds: number[];
  9. onClose: () => void;
  10. }
  11. export function CompareArchivesModal({ archiveIds, onClose }: CompareArchivesModalProps) {
  12. // Close on Escape key
  13. useEffect(() => {
  14. const handleKeyDown = (e: KeyboardEvent) => {
  15. if (e.key === 'Escape') onClose();
  16. };
  17. window.addEventListener('keydown', handleKeyDown);
  18. return () => window.removeEventListener('keydown', handleKeyDown);
  19. }, [onClose]);
  20. const { data: comparison, isLoading, error } = useQuery({
  21. queryKey: ['archive-comparison', archiveIds],
  22. queryFn: () => api.compareArchives(archiveIds),
  23. });
  24. return (
  25. <div className="fixed inset-0 bg-black/80 flex items-center justify-center z-50 p-4" onClick={onClose}>
  26. <div className="bg-bambu-dark-secondary rounded-lg max-w-4xl w-full max-h-[90vh] flex flex-col border border-bambu-dark-tertiary" onClick={(e) => e.stopPropagation()}>
  27. {/* Header */}
  28. <div className="flex items-center justify-between p-4 border-b border-bambu-dark-tertiary">
  29. <h3 className="text-lg font-semibold text-white">
  30. Compare Archives ({archiveIds.length})
  31. </h3>
  32. <button
  33. onClick={onClose}
  34. className="text-bambu-gray hover:text-white p-1"
  35. >
  36. <X className="w-5 h-5" />
  37. </button>
  38. </div>
  39. {/* Content */}
  40. <div className="flex-1 overflow-auto p-4 bg-bambu-dark-secondary">
  41. {isLoading ? (
  42. <div className="flex items-center justify-center py-12">
  43. <Loader2 className="w-8 h-8 text-bambu-green animate-spin" />
  44. </div>
  45. ) : error ? (
  46. <div className="text-center py-12 text-red-400">
  47. <AlertTriangle className="w-12 h-12 mx-auto mb-4 opacity-50" />
  48. <p>Failed to load comparison</p>
  49. <p className="text-sm text-bambu-gray mt-2">
  50. {error instanceof Error ? error.message : 'Unknown error'}
  51. </p>
  52. </div>
  53. ) : comparison ? (
  54. <ComparisonContent comparison={comparison} />
  55. ) : null}
  56. </div>
  57. {/* Footer */}
  58. <div className="p-4 border-t border-bambu-dark-tertiary">
  59. <Button variant="secondary" onClick={onClose} className="w-full">
  60. Close
  61. </Button>
  62. </div>
  63. </div>
  64. </div>
  65. );
  66. }
  67. function ComparisonContent({ comparison }: { comparison: ArchiveComparison }) {
  68. return (
  69. <div className="space-y-6">
  70. {/* Archive Headers */}
  71. <div className="overflow-x-auto">
  72. <table className="w-full">
  73. <thead>
  74. <tr>
  75. <th className="text-left text-sm text-bambu-gray font-medium pb-2 pr-4 min-w-[150px]">
  76. Setting
  77. </th>
  78. {comparison.archives.map((archive) => (
  79. <th
  80. key={archive.id}
  81. className="text-left text-sm font-medium pb-2 px-2 min-w-[120px]"
  82. >
  83. <div className="text-white truncate max-w-[150px]" title={archive.print_name}>
  84. {archive.print_name}
  85. </div>
  86. <div className={`text-xs ${
  87. archive.status === 'completed' ? 'text-status-ok' :
  88. archive.status === 'failed' ? 'text-status-error' : 'text-bambu-gray'
  89. }`}>
  90. {archive.status}
  91. </div>
  92. </th>
  93. ))}
  94. </tr>
  95. </thead>
  96. <tbody className="divide-y divide-bambu-gray/20">
  97. {comparison.comparison.map((field) => (
  98. <tr
  99. key={field.field}
  100. className={field.has_difference ? 'bg-yellow-500/5' : ''}
  101. >
  102. <td className="py-2 pr-4 text-sm">
  103. <div className="flex items-center gap-2">
  104. {field.has_difference && (
  105. <AlertTriangle className="w-3 h-3 text-yellow-400 flex-shrink-0" />
  106. )}
  107. <span className={field.has_difference ? 'text-yellow-400' : 'text-bambu-gray'}>
  108. {field.label}
  109. </span>
  110. </div>
  111. </td>
  112. {field.values.map((value, idx) => (
  113. <td key={idx} className="py-2 px-2 text-sm text-white">
  114. {value ?? <span className="text-bambu-gray/50">-</span>}
  115. {field.unit && value !== null && (
  116. <span className="text-bambu-gray ml-1">{field.unit}</span>
  117. )}
  118. </td>
  119. ))}
  120. </tr>
  121. ))}
  122. </tbody>
  123. </table>
  124. </div>
  125. {/* Differences Summary */}
  126. {comparison.differences.length > 0 && (
  127. <div className="p-4 bg-yellow-500/10 border border-yellow-500/30 rounded-lg">
  128. <h4 className="text-sm font-medium text-yellow-400 mb-2 flex items-center gap-2">
  129. <AlertTriangle className="w-4 h-4" />
  130. {comparison.differences.length} Difference{comparison.differences.length > 1 ? 's' : ''} Found
  131. </h4>
  132. <ul className="text-sm text-white/80 space-y-1">
  133. {comparison.differences.slice(0, 5).map((diff) => (
  134. <li key={diff.field}>
  135. <span className="text-yellow-400">{diff.label}</span>: {diff.values.join(' vs ')} {diff.unit || ''}
  136. </li>
  137. ))}
  138. {comparison.differences.length > 5 && (
  139. <li className="text-bambu-gray">
  140. ...and {comparison.differences.length - 5} more
  141. </li>
  142. )}
  143. </ul>
  144. </div>
  145. )}
  146. {/* Success Correlation */}
  147. {comparison.success_correlation.has_both_outcomes ? (
  148. <div className="p-4 bg-bambu-dark rounded-lg">
  149. <h4 className="text-sm font-medium text-white mb-3 flex items-center gap-2">
  150. <Check className="w-4 h-4 text-bambu-green" />
  151. Success/Failure Analysis
  152. </h4>
  153. <div className="flex items-center gap-4 text-sm mb-3">
  154. <span className="text-bambu-green">
  155. {comparison.success_correlation.successful_count} successful
  156. </span>
  157. <span className="text-red-400">
  158. {comparison.success_correlation.failed_count} failed
  159. </span>
  160. </div>
  161. {comparison.success_correlation.insights && comparison.success_correlation.insights.length > 0 ? (
  162. <div className="space-y-2">
  163. {comparison.success_correlation.insights.map((insight) => (
  164. <div key={insight.field} className="text-sm p-2 bg-bambu-dark-secondary rounded">
  165. <span className="text-white font-medium">{insight.label}:</span>{' '}
  166. <span className="text-white/80">{insight.insight}</span>
  167. </div>
  168. ))}
  169. </div>
  170. ) : (
  171. <p className="text-sm text-bambu-gray">No clear correlations found between settings and outcomes.</p>
  172. )}
  173. </div>
  174. ) : (
  175. <div className="p-4 bg-bambu-dark rounded-lg text-sm text-bambu-gray">
  176. <p>{comparison.success_correlation.message || 'Need both successful and failed prints for correlation analysis.'}</p>
  177. </div>
  178. )}
  179. </div>
  180. );
  181. }