model-status-bar.tsx 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181
  1. 'use client';
  2. import { useState, useEffect } from 'react';
  3. import { apiClient, type ModelInfo, type QueueStatus, type JobInfo } from '@/lib/api';
  4. import { AlertCircle, CheckCircle2, Loader2, Activity, Image } from 'lucide-react';
  5. import { cn } from '@/lib/utils';
  6. export function ModelStatusBar() {
  7. const [loadedModel, setLoadedModel] = useState<ModelInfo | null>(null);
  8. const [loading, setLoading] = useState(true);
  9. const [queueStatus, setQueueStatus] = useState<QueueStatus | null>(null);
  10. const [activeJob, setActiveJob] = useState<JobInfo | null>(null);
  11. const [recentlyCompleted, setRecentlyCompleted] = useState<JobInfo[]>([]);
  12. useEffect(() => {
  13. const checkStatus = async () => {
  14. try {
  15. const [loadedModels, queue] = await Promise.all([
  16. apiClient.getModels(undefined, true),
  17. apiClient.getQueueStatus(),
  18. ]);
  19. setLoadedModel(loadedModels.models.length > 0 ? loadedModels.models[0] : null);
  20. setQueueStatus(queue);
  21. // Find active/processing job
  22. const processing = queue.jobs.find(
  23. (job) => job.status === 'processing' || job.status === 'queued'
  24. );
  25. setActiveJob(processing || null);
  26. // Keep track of recently completed jobs (last 30 seconds)
  27. const now = Date.now();
  28. const thirtySecondsAgo = now - 30000;
  29. // Update recently completed jobs
  30. const completedJobs = queue.jobs.filter(
  31. (job) => job.status === 'completed' &&
  32. job.updated_at &&
  33. new Date(job.updated_at).getTime() > thirtySecondsAgo
  34. );
  35. setRecentlyCompleted(completedJobs);
  36. } catch (error) {
  37. console.error('Failed to check status:', error);
  38. } finally {
  39. setLoading(false);
  40. }
  41. };
  42. checkStatus();
  43. // Poll every 2 seconds when there's an active job, otherwise every 5 seconds
  44. const pollInterval = activeJob ? 2000 : 5000;
  45. const interval = setInterval(checkStatus, pollInterval);
  46. return () => clearInterval(interval);
  47. }, [activeJob]);
  48. if (loading) {
  49. return null;
  50. }
  51. // Determine status styling
  52. let statusBg = '';
  53. let statusBorder = '';
  54. let statusText = '';
  55. let icon = null;
  56. let content = null;
  57. if (activeJob && activeJob.status === 'processing') {
  58. // Active generation in progress
  59. statusBg = 'bg-blue-600 dark:bg-blue-700';
  60. statusBorder = 'border-blue-500 dark:border-blue-600';
  61. statusText = 'text-white';
  62. icon = <Loader2 className="h-4 w-4 flex-shrink-0 animate-spin" />;
  63. const progress = activeJob.progress !== undefined ? Math.round(activeJob.progress * 100) : 0;
  64. content = (
  65. <>
  66. <span className="font-semibold">Generating:</span>
  67. <span className="truncate">{activeJob.id}</span>
  68. <div className="flex items-center gap-2 ml-auto">
  69. <div className="w-40 h-2.5 bg-blue-900/50 dark:bg-blue-950/50 rounded-full overflow-hidden border border-blue-400/30">
  70. <div
  71. className="h-full bg-blue-200 dark:bg-blue-300 transition-all duration-300"
  72. style={{ width: `${progress}%` }}
  73. />
  74. </div>
  75. <span className="text-sm font-semibold min-w-[3rem] text-right">{progress}%</span>
  76. </div>
  77. </>
  78. );
  79. } else if (activeJob && activeJob.status === 'queued') {
  80. // Job queued but not processing yet
  81. statusBg = 'bg-purple-600 dark:bg-purple-700';
  82. statusBorder = 'border-purple-500 dark:border-purple-600';
  83. statusText = 'text-white';
  84. icon = <Activity className="h-4 w-4 flex-shrink-0" />;
  85. content = (
  86. <>
  87. <span className="font-semibold">Queued:</span>
  88. <span className="truncate">{queueStatus?.size || 0} job(s) waiting</span>
  89. {activeJob.queue_position !== undefined && (
  90. <span className="text-sm ml-auto">Position: {activeJob.queue_position}</span>
  91. )}
  92. </>
  93. );
  94. } else if (recentlyCompleted.length > 0) {
  95. // Show recently completed jobs with their results
  96. const latestCompleted = recentlyCompleted[0];
  97. const hasOutputs = (latestCompleted.outputs?.length ?? 0) > 0 || (latestCompleted.result?.images?.length ?? 0) > 0;
  98. statusBg = 'bg-green-600 dark:bg-green-700';
  99. statusBorder = 'border-green-500 dark:border-green-600';
  100. statusText = 'text-white';
  101. icon = hasOutputs ? <Image className="h-4 w-4 flex-shrink-0" /> : <CheckCircle2 className="h-4 w-4 flex-shrink-0" />;
  102. const outputCount = (latestCompleted.outputs?.length ?? 0) + (latestCompleted.result?.images?.length ?? 0);
  103. content = (
  104. <>
  105. <span className="font-semibold">Completed:</span>
  106. <span className="truncate">{latestCompleted.id}</span>
  107. {hasOutputs && (
  108. <>
  109. <span className="text-sm">• Generated {outputCount} image{outputCount !== 1 ? 's' : ''}</span>
  110. <div className="flex items-center gap-2 ml-auto">
  111. <div className="w-40 h-2.5 bg-green-900/50 dark:bg-green-950/50 rounded-full overflow-hidden border border-green-400/30">
  112. <div className="h-full bg-green-200 dark:bg-green-300" style={{ width: '100%' }} />
  113. </div>
  114. <span className="text-sm font-semibold min-w-[3rem] text-right">100%</span>
  115. </div>
  116. </>
  117. )}
  118. </>
  119. );
  120. } else if (loadedModel) {
  121. // Model loaded, ready
  122. statusBg = 'bg-green-600 dark:bg-green-700';
  123. statusBorder = 'border-green-500 dark:border-green-600';
  124. statusText = 'text-white';
  125. icon = <CheckCircle2 className="h-4 w-4 flex-shrink-0" />;
  126. content = (
  127. <>
  128. <span className="font-semibold">Model Ready:</span>
  129. <span className="truncate">{loadedModel.name}</span>
  130. {loadedModel.sha256_short && (
  131. <span className="text-sm opacity-90 ml-auto">({loadedModel.sha256_short})</span>
  132. )}
  133. </>
  134. );
  135. } else {
  136. // No model loaded
  137. statusBg = 'bg-amber-600 dark:bg-amber-700';
  138. statusBorder = 'border-amber-500 dark:border-amber-600';
  139. statusText = 'text-white';
  140. icon = <AlertCircle className="h-4 w-4 flex-shrink-0" />;
  141. content = (
  142. <>
  143. <span className="font-semibold">No Model Loaded</span>
  144. <span className="text-sm opacity-90">Please load a model from the Models page</span>
  145. </>
  146. );
  147. }
  148. return (
  149. <div
  150. className={cn(
  151. 'fixed bottom-0 left-64 right-0 border-t-2 px-4 py-3 shadow-lg z-35',
  152. statusBg,
  153. statusBorder,
  154. statusText
  155. )}
  156. style={{ zIndex: 35 }}
  157. >
  158. <div className="container mx-auto flex items-center gap-3 text-sm">
  159. {icon}
  160. {content}
  161. </div>
  162. </div>
  163. );
  164. }