page.tsx 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212
  1. "use client";
  2. import { useState, useEffect, useRef, useCallback } from "react";
  3. import { Header } from "@/components/layout";
  4. import { AppLayout } from "@/components/layout";
  5. import { Button } from "@/components/ui/button";
  6. import { Card, CardContent } from "@/components/ui/card";
  7. import { Loader2, RefreshCw, AlertCircle } from "lucide-react";
  8. import { EnhancedQueueList } from "@/components/features/queue/enhanced-queue-list";
  9. import {
  10. apiClient,
  11. type QueueStatus,
  12. type JobInfo,
  13. } from "@/lib/api";
  14. import { toast } from "sonner";
  15. export default function QueuePage() {
  16. const [queueStatus, setQueueStatus] = useState<QueueStatus | null>(null);
  17. const [loading, setLoading] = useState(true);
  18. const [actionLoading, setActionLoading] = useState(false);
  19. const [error, setError] = useState<string | null>(null);
  20. const intervalRef = useRef<NodeJS.Timeout | null>(null);
  21. const isMountedRef = useRef(true);
  22. // Fetch queue status from the API
  23. const fetchQueueStatus = useCallback(async () => {
  24. if (!isMountedRef.current) return;
  25. try {
  26. const status = await apiClient.getQueueStatus();
  27. if (isMountedRef.current) {
  28. setQueueStatus(status);
  29. setError(null);
  30. }
  31. } catch (err) {
  32. if (isMountedRef.current) {
  33. const errorMessage = err instanceof Error ? err.message : "Failed to fetch queue status";
  34. setError(errorMessage);
  35. console.error("Error fetching queue status:", err);
  36. }
  37. } finally {
  38. if (isMountedRef.current) {
  39. setLoading(false);
  40. }
  41. }
  42. }, []);
  43. // Set up intelligent auto-refresh with proper cleanup
  44. useEffect(() => {
  45. // Initial fetch
  46. fetchQueueStatus();
  47. // Set up interval for auto-refresh
  48. const startAutoRefresh = () => {
  49. if (intervalRef.current) {
  50. clearInterval(intervalRef.current);
  51. }
  52. intervalRef.current = setInterval(() => {
  53. fetchQueueStatus();
  54. }, 2000); // Refresh every 2 seconds for better responsiveness
  55. };
  56. startAutoRefresh();
  57. // Cleanup function
  58. return () => {
  59. isMountedRef.current = false;
  60. if (intervalRef.current) {
  61. clearInterval(intervalRef.current);
  62. intervalRef.current = null;
  63. }
  64. };
  65. }, [fetchQueueStatus]);
  66. // Adjust refresh interval based on queue activity
  67. useEffect(() => {
  68. if (!queueStatus?.jobs) return;
  69. const hasActiveJobs = queueStatus.jobs.some(
  70. job => job.status === 'processing' || job.status === 'loading' || job.status === 'queued'
  71. );
  72. // Use faster refresh for active jobs, slower for idle
  73. const newInterval = hasActiveJobs ? 1000 : 3000;
  74. if (intervalRef.current) {
  75. clearInterval(intervalRef.current);
  76. intervalRef.current = setInterval(() => {
  77. fetchQueueStatus();
  78. }, newInterval);
  79. }
  80. }, [queueStatus, fetchQueueStatus]);
  81. // Handle manual refresh
  82. const handleRefresh = useCallback(() => {
  83. setLoading(true);
  84. fetchQueueStatus();
  85. }, [fetchQueueStatus]);
  86. // Handle job cancellation
  87. const handleCancelJob = useCallback(async (jobId: string) => {
  88. setActionLoading(true);
  89. try {
  90. await apiClient.cancelJob(jobId);
  91. toast.success(`Job ${jobId} cancelled successfully`);
  92. // Refresh queue status after cancellation
  93. fetchQueueStatus();
  94. } catch (err) {
  95. const errorMessage = err instanceof Error ? err.message : "Failed to cancel job";
  96. toast.error(`Failed to cancel job: ${errorMessage}`);
  97. console.error(`Failed to cancel job: ${errorMessage}`, err);
  98. } finally {
  99. setActionLoading(false);
  100. }
  101. }, [fetchQueueStatus]);
  102. // Handle queue clearing
  103. const handleClearQueue = useCallback(async () => {
  104. if (!queueStatus?.jobs.length) return;
  105. if (!confirm("Are you sure you want to clear all jobs from the queue? This action cannot be undone.")) {
  106. return;
  107. }
  108. setActionLoading(true);
  109. try {
  110. await apiClient.clearQueue();
  111. toast.success("Queue cleared successfully");
  112. // Refresh queue status after clearing
  113. fetchQueueStatus();
  114. } catch (err) {
  115. const errorMessage = err instanceof Error ? err.message : "Failed to clear queue";
  116. toast.error(`Failed to clear queue: ${errorMessage}`);
  117. console.error(`Failed to clear queue: ${errorMessage}`, err);
  118. } finally {
  119. setActionLoading(false);
  120. }
  121. }, [queueStatus?.jobs.length, fetchQueueStatus]);
  122. // Handle copying job parameters
  123. const handleCopyParameters = useCallback((job: JobInfo) => {
  124. try {
  125. const params = {
  126. prompt: job.prompt || "",
  127. negative_prompt: job.message || "",
  128. // Add other relevant parameters as needed
  129. };
  130. const paramsText = JSON.stringify(params, null, 2);
  131. navigator.clipboard.writeText(paramsText);
  132. toast.success("Job parameters copied to clipboard");
  133. } catch (err) {
  134. toast.error("Failed to copy parameters");
  135. console.error("Failed to copy parameters:", err);
  136. }
  137. }, []);
  138. return (
  139. <AppLayout>
  140. <Header
  141. title="Queue Management"
  142. description="Monitor and manage generation jobs in the queue"
  143. />
  144. <div className="container mx-auto p-6">
  145. {error && (
  146. <Card className="mb-6 border-red-200 bg-red-50 dark:border-red-800 dark:bg-red-950/20">
  147. <CardContent className="pt-6">
  148. <div className="flex items-center gap-3 text-red-700 dark:text-red-300">
  149. <AlertCircle className="h-5 w-5" />
  150. <div>
  151. <h3 className="font-medium">Error loading queue</h3>
  152. <p className="text-sm">{error}</p>
  153. </div>
  154. <Button
  155. variant="outline"
  156. size="sm"
  157. onClick={handleRefresh}
  158. className="ml-auto"
  159. >
  160. <RefreshCw className="h-4 w-4 mr-2" />
  161. Retry
  162. </Button>
  163. </div>
  164. </CardContent>
  165. </Card>
  166. )}
  167. {loading && !queueStatus ? (
  168. <Card>
  169. <CardContent className="pt-6">
  170. <div className="flex items-center justify-center py-12">
  171. <Loader2 className="h-8 w-8 animate-spin mr-3" />
  172. <span>Loading queue status...</span>
  173. </div>
  174. </CardContent>
  175. </Card>
  176. ) : (
  177. <EnhancedQueueList
  178. queueStatus={queueStatus}
  179. loading={loading}
  180. onRefresh={handleRefresh}
  181. onCancelJob={handleCancelJob}
  182. onClearQueue={handleClearQueue}
  183. actionLoading={actionLoading}
  184. onCopyParameters={handleCopyParameters}
  185. />
  186. )}
  187. </div>
  188. </AppLayout>
  189. );
  190. }