ControlPage.tsx 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241
  1. import { useState, useEffect } from 'react';
  2. import { useQuery } from '@tanstack/react-query';
  3. import { useSearchParams } from 'react-router-dom';
  4. import { api } from '../api/client';
  5. import type { PrinterStatus } from '../api/client';
  6. import { CameraFeed } from '../components/control/CameraFeed';
  7. import { PrintStatus } from '../components/control/PrintStatus';
  8. import { TemperatureColumn } from '../components/control/TemperatureColumn';
  9. import { JogPad } from '../components/control/JogPad';
  10. import { BedControls } from '../components/control/BedControls';
  11. import { ExtruderControls } from '../components/control/ExtruderControls';
  12. import { AMSSectionDual } from '../components/control/AMSSectionDual';
  13. import { Loader2, WifiOff, Video, Webcam, HardDrive, Settings } from 'lucide-react';
  14. export function ControlPage() {
  15. const [searchParams, setSearchParams] = useSearchParams();
  16. const [selectedPrinterId, setSelectedPrinterId] = useState<number | null>(null);
  17. // Fetch all printers
  18. const { data: printers, isLoading: loadingPrinters } = useQuery({
  19. queryKey: ['printers'],
  20. queryFn: api.getPrinters,
  21. });
  22. // Get statuses for all printers
  23. const { data: statuses } = useQuery({
  24. queryKey: ['printerStatuses'],
  25. queryFn: async () => {
  26. if (!printers) return {};
  27. const statusMap: Record<number, PrinterStatus> = {};
  28. await Promise.all(
  29. printers.map(async (p) => {
  30. try {
  31. statusMap[p.id] = await api.getPrinterStatus(p.id);
  32. } catch {
  33. // Printer offline
  34. }
  35. })
  36. );
  37. return statusMap;
  38. },
  39. enabled: !!printers && printers.length > 0,
  40. refetchInterval: 2000,
  41. });
  42. // Initialize selected printer from URL or first printer
  43. useEffect(() => {
  44. const printerParam = searchParams.get('printer');
  45. if (printerParam) {
  46. const id = parseInt(printerParam, 10);
  47. if (!isNaN(id)) {
  48. setSelectedPrinterId(id);
  49. return;
  50. }
  51. }
  52. // Default to first printer
  53. if (printers && printers.length > 0 && !selectedPrinterId) {
  54. setSelectedPrinterId(printers[0].id);
  55. }
  56. }, [printers, searchParams, selectedPrinterId]);
  57. // Update URL when printer changes
  58. const handlePrinterSelect = (printerId: number) => {
  59. setSelectedPrinterId(printerId);
  60. setSearchParams({ printer: String(printerId) });
  61. };
  62. const selectedPrinter = printers?.find((p) => p.id === selectedPrinterId);
  63. const selectedStatus = selectedPrinterId ? statuses?.[selectedPrinterId] : null;
  64. if (loadingPrinters) {
  65. return (
  66. <div className="flex items-center justify-center h-screen">
  67. <Loader2 className="w-8 h-8 animate-spin text-bambu-green" />
  68. </div>
  69. );
  70. }
  71. if (!printers || printers.length === 0) {
  72. return (
  73. <div className="flex flex-col items-center justify-center h-screen text-bambu-gray">
  74. <WifiOff className="w-16 h-16 mb-4" />
  75. <p className="text-xl">No printers configured</p>
  76. <p className="text-sm mt-2">Add a printer in the Printers page first</p>
  77. </div>
  78. );
  79. }
  80. return (
  81. <div className="h-screen flex flex-col bg-bambu-dark">
  82. {/* Printer Tabs */}
  83. <div className="bg-bambu-dark-secondary border-b border-bambu-dark-tertiary">
  84. <div className="flex overflow-x-auto">
  85. {printers.map((printer) => {
  86. const status = statuses?.[printer.id];
  87. const isConnected = status?.connected ?? false;
  88. const isSelected = printer.id === selectedPrinterId;
  89. return (
  90. <button
  91. key={printer.id}
  92. onClick={() => handlePrinterSelect(printer.id)}
  93. className={`flex items-center gap-2 px-6 py-3 text-sm font-medium transition-colors whitespace-nowrap border-b-2 ${
  94. isSelected
  95. ? 'border-bambu-green text-bambu-green bg-bambu-dark'
  96. : 'border-transparent text-bambu-gray hover:text-white hover:bg-bambu-dark-tertiary'
  97. }`}
  98. >
  99. <span
  100. className={`w-2 h-2 rounded-full ${
  101. isConnected ? 'bg-bambu-green' : 'bg-red-500'
  102. }`}
  103. />
  104. {printer.name}
  105. {status?.state && status.state !== 'IDLE' && (
  106. <span className="text-xs px-2 py-0.5 rounded bg-bambu-dark-tertiary">
  107. {status.state}
  108. </span>
  109. )}
  110. </button>
  111. );
  112. })}
  113. </div>
  114. </div>
  115. {/* Main Content - Bambu Studio Layout */}
  116. {selectedPrinter && (
  117. <div className="flex-1 flex overflow-hidden">
  118. {/* Left Panel - Camera & Print Progress */}
  119. <div className="flex-1 flex flex-col bg-bambu-dark">
  120. {/* Camera Header Icons - same height as Control header */}
  121. <div className="flex items-center justify-end gap-2 px-3 py-2.5 bg-bambu-dark-secondary border-b border-bambu-dark-tertiary min-h-[44px]">
  122. <button className="p-1.5 rounded hover:bg-bambu-dark-tertiary text-bambu-gray hover:text-white">
  123. <HardDrive className="w-4 h-4" />
  124. </button>
  125. <button className="p-1.5 rounded hover:bg-bambu-dark-tertiary text-bambu-gray hover:text-white">
  126. <Video className="w-4 h-4" />
  127. </button>
  128. <button className="p-1.5 rounded hover:bg-bambu-dark-tertiary text-bambu-gray hover:text-white">
  129. <Webcam className="w-4 h-4" />
  130. </button>
  131. <button className="p-1.5 rounded hover:bg-bambu-dark-tertiary text-bambu-gray hover:text-white">
  132. <Settings className="w-4 h-4" />
  133. </button>
  134. </div>
  135. {/* Camera Feed - Embedded directly */}
  136. <div className="flex-1 bg-black">
  137. <CameraFeed
  138. printerId={selectedPrinter.id}
  139. isConnected={selectedStatus?.connected ?? false}
  140. />
  141. </div>
  142. {/* Status Bar */}
  143. <div className="h-1 bg-bambu-green" />
  144. {/* Print Progress with integrated controls */}
  145. <div className="bg-bambu-dark-secondary p-4 px-5">
  146. <PrintStatus
  147. printerId={selectedPrinter.id}
  148. status={selectedStatus}
  149. />
  150. </div>
  151. </div>
  152. {/* Right Panel - Control */}
  153. <div className="w-[620px] flex flex-col bg-bambu-dark-secondary border-l border-bambu-dark-tertiary overflow-y-auto">
  154. {/* Control Header - same height as Camera header */}
  155. <div className="flex items-center justify-between px-3 py-2.5 border-b border-bambu-dark-tertiary min-h-[44px]">
  156. <span className="text-sm text-bambu-gray">Control</span>
  157. <div className="flex gap-2">
  158. <button className="px-4 py-1.5 text-xs rounded bg-bambu-green text-white hover:bg-bambu-green-dark">
  159. Printer Parts
  160. </button>
  161. <button className="px-4 py-1.5 text-xs rounded bg-bambu-green text-white hover:bg-bambu-green-dark">
  162. Print Options
  163. </button>
  164. <button className="px-4 py-1.5 text-xs rounded bg-bambu-green text-white hover:bg-bambu-green-dark">
  165. Calibration
  166. </button>
  167. </div>
  168. </div>
  169. {/* Connection Warning */}
  170. {!selectedStatus?.connected && (
  171. <div className="m-3 p-3 bg-red-500/20 border border-red-500/50 rounded-lg flex items-center gap-3">
  172. <WifiOff className="w-4 h-4 text-red-500" />
  173. <span className="text-sm text-red-400">
  174. Printer is not connected. Controls are disabled.
  175. </span>
  176. </div>
  177. )}
  178. {/* Control Body */}
  179. <div className="flex-1 p-4 bg-bambu-dark">
  180. {/* Top Section: Temp + Movement + Extruder */}
  181. <div className="flex gap-6 mb-4" style={{ minHeight: '300px' }}>
  182. {/* Temperature Column */}
  183. <TemperatureColumn
  184. printerId={selectedPrinter.id}
  185. status={selectedStatus}
  186. nozzleCount={selectedPrinter.nozzle_count}
  187. />
  188. {/* Movement Column */}
  189. <div className="flex-1 flex gap-6 items-center justify-center">
  190. {/* Jog Section */}
  191. <div className="flex flex-col items-center">
  192. <JogPad
  193. printerId={selectedPrinter.id}
  194. status={selectedStatus}
  195. />
  196. <BedControls
  197. printerId={selectedPrinter.id}
  198. status={selectedStatus}
  199. />
  200. </div>
  201. {/* Extruder Section */}
  202. <ExtruderControls
  203. printerId={selectedPrinter.id}
  204. status={selectedStatus}
  205. nozzleCount={selectedPrinter.nozzle_count}
  206. />
  207. </div>
  208. </div>
  209. {/* AMS Section */}
  210. <AMSSectionDual
  211. printerId={selectedPrinter.id}
  212. status={selectedStatus}
  213. nozzleCount={selectedPrinter.nozzle_count}
  214. />
  215. </div>
  216. </div>
  217. </div>
  218. )}
  219. </div>
  220. );
  221. }