model-status-bar.tsx 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140
  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 } 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. useEffect(() => {
  12. const checkStatus = async () => {
  13. try {
  14. const [loadedModels, queue] = await Promise.all([
  15. apiClient.getModels(undefined, true),
  16. apiClient.getQueueStatus(),
  17. ]);
  18. setLoadedModel(loadedModels.length > 0 ? loadedModels[0] : null);
  19. setQueueStatus(queue);
  20. // Find active/processing job
  21. const processing = queue.jobs.find(
  22. (job) => job.status === 'processing' || job.status === 'queued'
  23. );
  24. setActiveJob(processing || null);
  25. } catch (error) {
  26. console.error('Failed to check status:', error);
  27. } finally {
  28. setLoading(false);
  29. }
  30. };
  31. checkStatus();
  32. // Poll every 1 second when there's an active job, otherwise every 5 seconds
  33. const pollInterval = activeJob ? 1000 : 5000;
  34. const interval = setInterval(checkStatus, pollInterval);
  35. return () => clearInterval(interval);
  36. }, [activeJob]);
  37. if (loading) {
  38. return null;
  39. }
  40. // Determine status styling
  41. let statusBg = '';
  42. let statusBorder = '';
  43. let statusText = '';
  44. let icon = null;
  45. let content = null;
  46. if (activeJob && activeJob.status === 'processing') {
  47. // Active generation in progress
  48. statusBg = 'bg-blue-600 dark:bg-blue-700';
  49. statusBorder = 'border-blue-500 dark:border-blue-600';
  50. statusText = 'text-white';
  51. icon = <Loader2 className="h-4 w-4 flex-shrink-0 animate-spin" />;
  52. const progress = activeJob.progress !== undefined ? Math.round(activeJob.progress * 100) : 0;
  53. content = (
  54. <>
  55. <span className="font-semibold">Generating:</span>
  56. <span className="truncate">{activeJob.id}</span>
  57. <div className="flex items-center gap-2 ml-auto">
  58. <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">
  59. <div
  60. className="h-full bg-blue-200 dark:bg-blue-300 transition-all duration-300"
  61. style={{ width: `${progress}%` }}
  62. />
  63. </div>
  64. <span className="text-sm font-semibold min-w-[3rem] text-right">{progress}%</span>
  65. </div>
  66. </>
  67. );
  68. } else if (activeJob && activeJob.status === 'queued') {
  69. // Job queued but not processing yet
  70. statusBg = 'bg-purple-600 dark:bg-purple-700';
  71. statusBorder = 'border-purple-500 dark:border-purple-600';
  72. statusText = 'text-white';
  73. icon = <Activity className="h-4 w-4 flex-shrink-0" />;
  74. content = (
  75. <>
  76. <span className="font-semibold">Queued:</span>
  77. <span className="truncate">{queueStatus?.size || 0} job(s) waiting</span>
  78. {activeJob.queue_position !== undefined && (
  79. <span className="text-sm ml-auto">Position: {activeJob.queue_position}</span>
  80. )}
  81. </>
  82. );
  83. } else if (loadedModel) {
  84. // Model loaded, ready
  85. statusBg = 'bg-green-600 dark:bg-green-700';
  86. statusBorder = 'border-green-500 dark:border-green-600';
  87. statusText = 'text-white';
  88. icon = <CheckCircle2 className="h-4 w-4 flex-shrink-0" />;
  89. content = (
  90. <>
  91. <span className="font-semibold">Model Ready:</span>
  92. <span className="truncate">{loadedModel.name}</span>
  93. {loadedModel.sha256_short && (
  94. <span className="text-sm opacity-90 ml-auto">({loadedModel.sha256_short})</span>
  95. )}
  96. </>
  97. );
  98. } else {
  99. // No model loaded
  100. statusBg = 'bg-amber-600 dark:bg-amber-700';
  101. statusBorder = 'border-amber-500 dark:border-amber-600';
  102. statusText = 'text-white';
  103. icon = <AlertCircle className="h-4 w-4 flex-shrink-0" />;
  104. content = (
  105. <>
  106. <span className="font-semibold">No Model Loaded</span>
  107. <span className="text-sm opacity-90">Please load a model from the Models page</span>
  108. </>
  109. );
  110. }
  111. return (
  112. <div
  113. className={cn(
  114. 'fixed bottom-0 left-64 right-0 border-t-2 px-4 py-3 shadow-lg z-35',
  115. statusBg,
  116. statusBorder,
  117. statusText
  118. )}
  119. style={{ zIndex: 35 }}
  120. >
  121. <div className="container mx-auto flex items-center gap-3 text-sm">
  122. {icon}
  123. {content}
  124. </div>
  125. </div>
  126. );
  127. }