apolinario commited on
Commit
c12ee07
·
1 Parent(s): eb4c9f0

apply scroll shenanigans

Browse files
ui/src/app/api/hf-jobs/route.ts CHANGED
@@ -152,6 +152,7 @@ import os
152
  import sys
153
  import subprocess
154
  import argparse
 
155
  import oyaml as yaml
156
  from datasets import load_dataset
157
  from huggingface_hub import HfApi, create_repo, upload_folder, snapshot_download
@@ -694,15 +695,42 @@ def generate_model_card_readme(repo_id: str, config: dict, model_name: str, cura
694
  gallery_section = "<Gallery />\\n\\n" + "### Prompts\\n\\n" + "\\n".join(prompt_bullets) + "\\n\\n"
695
 
696
  # Determine torch dtype based on model
697
- dtype = "torch.bfloat16" if "flux" in arch.lower() else "torch.float16"
698
-
 
 
 
 
 
 
699
  # Find the main safetensors file for usage example
700
  main_safetensors = f"{model_name}.safetensors"
701
  if uploaded_files:
702
  safetensors_files = [f for f in uploaded_files if f.endswith('.safetensors')]
703
  if safetensors_files:
704
- main_safetensors = safetensors_files[0]
705
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
706
  # Construct YAML frontmatter
707
  frontmatter = {
708
  "tags": tags,
 
152
  import sys
153
  import subprocess
154
  import argparse
155
+ import re
156
  import oyaml as yaml
157
  from datasets import load_dataset
158
  from huggingface_hub import HfApi, create_repo, upload_folder, snapshot_download
 
695
  gallery_section = "<Gallery />\\n\\n" + "### Prompts\\n\\n" + "\\n".join(prompt_bullets) + "\\n\\n"
696
 
697
  # Determine torch dtype based on model
698
+ dtype = "torch.bfloat16"
699
+ try:
700
+ arch_lower = arch.lower()
701
+ except AttributeError:
702
+ arch_lower = ""
703
+ if "sd15" in arch_lower or "sdxl" in arch_lower:
704
+ dtype = "torch.float16"
705
+
706
  # Find the main safetensors file for usage example
707
  main_safetensors = f"{model_name}.safetensors"
708
  if uploaded_files:
709
  safetensors_files = [f for f in uploaded_files if f.endswith('.safetensors')]
710
  if safetensors_files:
711
+ preferred_name = f"{model_name}.safetensors"
712
+ exact_match = next(
713
+ (
714
+ f
715
+ for f in safetensors_files
716
+ if os.path.basename(f) == preferred_name or f == preferred_name
717
+ ),
718
+ None,
719
+ )
720
+
721
+ if exact_match:
722
+ main_safetensors = exact_match
723
+ else:
724
+ def extract_step(filename: str) -> int:
725
+ match = re.search(r"_(\d+)\.safetensors$", os.path.basename(filename))
726
+ return int(match.group(1)) if match else -1
727
+
728
+ safetensors_files.sort(
729
+ key=lambda f: (extract_step(f), f),
730
+ reverse=True,
731
+ )
732
+ main_safetensors = safetensors_files[0]
733
+
734
  # Construct YAML frontmatter
735
  frontmatter = {
736
  "tags": tags,
ui/src/app/jobs/new/SimplifiedJob.tsx CHANGED
@@ -501,32 +501,62 @@ export default function SimplifiedJob({
501
  }, [datasetOptions]);
502
 
503
  useEffect(() => {
504
- if (trainDatasetResolvedName) {
505
- return;
506
- }
507
  const currentFolder = dataset.folder_path;
508
  if (!currentFolder || currentFolder === defaultDatasetConfig.folder_path) {
509
  return;
510
  }
 
511
  const match = existingOptions.find(option => option.value === currentFolder);
512
- if (match) {
 
 
 
 
513
  setTrainDatasetResolvedName(match.label);
514
  }
515
- }, [existingOptions, dataset.folder_path, trainDatasetResolvedName]);
516
 
517
- useEffect(() => {
518
- if (controlDatasetResolvedName) {
519
- return;
 
 
520
  }
 
 
 
 
 
 
 
 
 
521
  const controlPathValue = typeof dataset.control_path === 'string' ? dataset.control_path : null;
522
  if (!controlPathValue) {
523
  return;
524
  }
 
525
  const match = existingOptions.find(option => option.value === controlPathValue);
526
- if (match) {
 
 
 
 
527
  setControlDatasetResolvedName(match.label);
528
  }
529
- }, [existingOptions, dataset.control_path, controlDatasetResolvedName]);
 
 
 
 
 
 
 
 
 
 
 
 
 
530
 
531
  const handleDatasetRename = useCallback(
532
  async (target: 'train' | 'control') => {
@@ -823,6 +853,10 @@ export default function SimplifiedJob({
823
  const selected = existingOptions.find(option => option.value === value);
824
  setTrainDatasetResolvedName(selected?.label || null);
825
  setTrainPreviewRefresh(prev => prev + 1);
 
 
 
 
826
  }}
827
  options={existingOptions}
828
  />
@@ -838,6 +872,10 @@ export default function SimplifiedJob({
838
  const matched = existingOptions.find(option => option.value === trimmed);
839
  setTrainDatasetResolvedName(matched?.label || null);
840
  setTrainPreviewRefresh(prev => prev + 1);
 
 
 
 
841
  }}
842
  placeholder="/path/to/your/dataset"
843
  required
@@ -927,6 +965,10 @@ export default function SimplifiedJob({
927
  const selected = existingOptions.find(option => option.value === value);
928
  setControlDatasetResolvedName(selected?.label || null);
929
  setControlPreviewRefresh(prev => prev + 1);
 
 
 
 
930
  }}
931
  options={existingOptions}
932
  />
@@ -943,6 +985,10 @@ export default function SimplifiedJob({
943
  const matched = existingOptions.find(option => option.value === trimmed);
944
  setControlDatasetResolvedName(matched?.label || null);
945
  setControlPreviewRefresh(prev => prev + 1);
 
 
 
 
946
  }}
947
  placeholder="/path/to/control/images"
948
  />
 
501
  }, [datasetOptions]);
502
 
503
  useEffect(() => {
 
 
 
504
  const currentFolder = dataset.folder_path;
505
  if (!currentFolder || currentFolder === defaultDatasetConfig.folder_path) {
506
  return;
507
  }
508
+
509
  const match = existingOptions.find(option => option.value === currentFolder);
510
+ if (!match) {
511
+ return;
512
+ }
513
+
514
+ if (trainDatasetResolvedName !== match.label) {
515
  setTrainDatasetResolvedName(match.label);
516
  }
 
517
 
518
+ if (!trainDatasetNameTouched) {
519
+ if (trainDatasetName !== match.label) {
520
+ setTrainDatasetName(match.label);
521
+ }
522
+ setTrainDatasetNameTouched(true);
523
  }
524
+ }, [
525
+ existingOptions,
526
+ dataset.folder_path,
527
+ trainDatasetResolvedName,
528
+ trainDatasetNameTouched,
529
+ trainDatasetName,
530
+ ]);
531
+
532
+ useEffect(() => {
533
  const controlPathValue = typeof dataset.control_path === 'string' ? dataset.control_path : null;
534
  if (!controlPathValue) {
535
  return;
536
  }
537
+
538
  const match = existingOptions.find(option => option.value === controlPathValue);
539
+ if (!match) {
540
+ return;
541
+ }
542
+
543
+ if (controlDatasetResolvedName !== match.label) {
544
  setControlDatasetResolvedName(match.label);
545
  }
546
+
547
+ if (!controlDatasetNameTouched) {
548
+ if (controlDatasetName !== match.label) {
549
+ setControlDatasetName(match.label);
550
+ }
551
+ setControlDatasetNameTouched(true);
552
+ }
553
+ }, [
554
+ existingOptions,
555
+ dataset.control_path,
556
+ controlDatasetResolvedName,
557
+ controlDatasetNameTouched,
558
+ controlDatasetName,
559
+ ]);
560
 
561
  const handleDatasetRename = useCallback(
562
  async (target: 'train' | 'control') => {
 
853
  const selected = existingOptions.find(option => option.value === value);
854
  setTrainDatasetResolvedName(selected?.label || null);
855
  setTrainPreviewRefresh(prev => prev + 1);
856
+ if (selected?.label) {
857
+ setTrainDatasetName(selected.label);
858
+ setTrainDatasetNameTouched(true);
859
+ }
860
  }}
861
  options={existingOptions}
862
  />
 
872
  const matched = existingOptions.find(option => option.value === trimmed);
873
  setTrainDatasetResolvedName(matched?.label || null);
874
  setTrainPreviewRefresh(prev => prev + 1);
875
+ if (matched?.label) {
876
+ setTrainDatasetName(matched.label);
877
+ setTrainDatasetNameTouched(true);
878
+ }
879
  }}
880
  placeholder="/path/to/your/dataset"
881
  required
 
965
  const selected = existingOptions.find(option => option.value === value);
966
  setControlDatasetResolvedName(selected?.label || null);
967
  setControlPreviewRefresh(prev => prev + 1);
968
+ if (selected?.label) {
969
+ setControlDatasetName(selected.label);
970
+ setControlDatasetNameTouched(true);
971
+ }
972
  }}
973
  options={existingOptions}
974
  />
 
985
  const matched = existingOptions.find(option => option.value === trimmed);
986
  setControlDatasetResolvedName(matched?.label || null);
987
  setControlPreviewRefresh(prev => prev + 1);
988
+ if (matched?.label) {
989
+ setControlDatasetName(matched.label);
990
+ setControlDatasetNameTouched(true);
991
+ }
992
  }}
993
  placeholder="/path/to/control/images"
994
  />
ui/src/app/jobs/new/page.tsx CHANGED
@@ -1,6 +1,6 @@
1
  'use client';
2
 
3
- import { useEffect, useState } from 'react';
4
  import { useSearchParams, useRouter } from 'next/navigation';
5
  import Link from 'next/link';
6
  import { defaultJobConfig, defaultDatasetConfig, migrateJobConfig } from './jobConfig';
@@ -25,6 +25,7 @@ import { getUserDatasetPath, updateUserDatasetPath } from '@/utils/storage/datas
25
  import { apiClient } from '@/utils/api';
26
  import { useAuth } from '@/contexts/AuthContext';
27
  import HFLoginButton from '@/components/HFLoginButton';
 
28
 
29
  const isDev = process.env.NODE_ENV === 'development';
30
 
@@ -50,6 +51,7 @@ export default function TrainingForm() {
50
  usingBrowserDb ? 'hf-jobs' : 'local',
51
  );
52
  const [hfJobSubmitted, setHfJobSubmitted] = useState(false);
 
53
 
54
  useEffect(() => {
55
  if (!isSettingsLoaded || !isAuthenticated) return;
@@ -122,7 +124,7 @@ export default function TrainingForm() {
122
 
123
  if (parsedJobConfig.is_hf_job) {
124
  setTrainingBackend('hf-jobs');
125
- setHfJobSubmitted(true);
126
  }
127
  })
128
  .catch(error => console.error('Error fetching training:', error));
@@ -149,6 +151,14 @@ export default function TrainingForm() {
149
  }
150
  }, [activeTab, showAdvancedView]);
151
 
 
 
 
 
 
 
 
 
152
  const saveJob = async () => {
153
  if (!isAuthenticated) return;
154
  if (status === 'saving') return;
@@ -184,11 +194,39 @@ export default function TrainingForm() {
184
  }
185
  };
186
 
 
 
 
 
 
 
 
 
 
187
  const handleSubmit = async (e: React.FormEvent) => {
188
  e.preventDefault();
189
- saveJob();
190
  };
191
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
192
  return (
193
  <>
194
  <div className="relative z-20">
@@ -228,7 +266,7 @@ export default function TrainingForm() {
228
  <div>
229
  <Button
230
  className="text-gray-200 bg-green-800 hover:bg-green-700 px-3 py-1 rounded-md"
231
- onClick={() => saveJob()}
232
  disabled={!isAuthenticated || status === 'saving'}
233
  >
234
  {status === 'saving'
@@ -364,6 +402,34 @@ export default function TrainingForm() {
364
  )}
365
  </>
366
  )}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
367
  </>
368
  );
369
  }
 
1
  'use client';
2
 
3
+ import { useCallback, useEffect, useState } from 'react';
4
  import { useSearchParams, useRouter } from 'next/navigation';
5
  import Link from 'next/link';
6
  import { defaultJobConfig, defaultDatasetConfig, migrateJobConfig } from './jobConfig';
 
25
  import { apiClient } from '@/utils/api';
26
  import { useAuth } from '@/contexts/AuthContext';
27
  import HFLoginButton from '@/components/HFLoginButton';
28
+ import { Modal } from '@/components/Modal';
29
 
30
  const isDev = process.env.NODE_ENV === 'development';
31
 
 
51
  usingBrowserDb ? 'hf-jobs' : 'local',
52
  );
53
  const [hfJobSubmitted, setHfJobSubmitted] = useState(false);
54
+ const [showHFSaveModal, setShowHFSaveModal] = useState(false);
55
 
56
  useEffect(() => {
57
  if (!isSettingsLoaded || !isAuthenticated) return;
 
124
 
125
  if (parsedJobConfig.is_hf_job) {
126
  setTrainingBackend('hf-jobs');
127
+ setHfJobSubmitted(Boolean((parsedJobConfig as any)?.hf_job_submitted));
128
  }
129
  })
130
  .catch(error => console.error('Error fetching training:', error));
 
151
  }
152
  }, [activeTab, showAdvancedView]);
153
 
154
+ const scrollToHFStart = useCallback(() => {
155
+ if (typeof window === 'undefined') return;
156
+ const target = document.getElementById('hf-start-training');
157
+ if (target) {
158
+ target.scrollIntoView({ behavior: 'smooth', block: 'start' });
159
+ }
160
+ }, []);
161
+
162
  const saveJob = async () => {
163
  if (!isAuthenticated) return;
164
  if (status === 'saving') return;
 
194
  }
195
  };
196
 
197
+ const handleCreateJobClick = () => {
198
+ if (!isAuthenticated || status === 'saving') return;
199
+ if (trainingBackend === 'hf-jobs' && !hfJobSubmitted) {
200
+ setShowHFSaveModal(true);
201
+ return;
202
+ }
203
+ saveJob();
204
+ };
205
+
206
  const handleSubmit = async (e: React.FormEvent) => {
207
  e.preventDefault();
208
+ handleCreateJobClick();
209
  };
210
 
211
+ const handleStartFirst = () => {
212
+ setShowHFSaveModal(false);
213
+ if (typeof window !== 'undefined') {
214
+ window.location.hash = '#hf-start-training';
215
+ }
216
+ setTimeout(() => {
217
+ scrollToHFStart();
218
+ }, 100);
219
+ };
220
+
221
+ useEffect(() => {
222
+ if (typeof window === 'undefined') return;
223
+ if (window.location.hash === '#hf-start-training') {
224
+ setTimeout(() => {
225
+ scrollToHFStart();
226
+ }, 100);
227
+ }
228
+ }, [activeTab, scrollToHFStart]);
229
+
230
  return (
231
  <>
232
  <div className="relative z-20">
 
266
  <div>
267
  <Button
268
  className="text-gray-200 bg-green-800 hover:bg-green-700 px-3 py-1 rounded-md"
269
+ onClick={handleCreateJobClick}
270
  disabled={!isAuthenticated || status === 'saving'}
271
  >
272
  {status === 'saving'
 
402
  )}
403
  </>
404
  )}
405
+ <Modal
406
+ isOpen={showHFSaveModal}
407
+ onClose={() => setShowHFSaveModal(false)}
408
+ title="Create job without starting?"
409
+ >
410
+ <p className="text-sm text-gray-300">
411
+ You are about to create this job without launching it on HF Jobs. Would you like to start the submission flow instead?
412
+ </p>
413
+ <div className="mt-6 flex justify-end gap-3">
414
+ <button
415
+ type="button"
416
+ onClick={handleStartFirst}
417
+ className="px-4 py-2 rounded-md bg-blue-600 hover:bg-blue-500 text-white text-sm font-semibold"
418
+ >
419
+ Start first
420
+ </button>
421
+ <button
422
+ type="button"
423
+ onClick={() => {
424
+ setShowHFSaveModal(false);
425
+ saveJob();
426
+ }}
427
+ className="px-4 py-2 rounded-md bg-gray-700 hover:bg-gray-600 text-gray-200 text-sm"
428
+ >
429
+ Create without starting
430
+ </button>
431
+ </div>
432
+ </Modal>
433
  </>
434
  );
435
  }
ui/src/components/HFJobsWorkflow.tsx CHANGED
@@ -716,7 +716,7 @@ export default function HFJobsWorkflow({ jobConfig, onComplete, hackathonEligibl
716
 
717
  return (
718
  <div className="space-y-6">
719
- <h2 className="text-lg font-semibold text-gray-100">Start training</h2>
720
  {/* Progress indicator */}
721
  <div className="flex items-center justify-between mb-6">
722
  {(['validate', 'upload', 'submit', 'complete'] as Step[]).map((step, index) => (
 
716
 
717
  return (
718
  <div className="space-y-6">
719
+ <h2 id="hf-start-training" className="text-lg font-semibold text-gray-100">Start training</h2>
720
  {/* Progress indicator */}
721
  <div className="flex items-center justify-between mb-6">
722
  {(['validate', 'upload', 'submit', 'complete'] as Step[]).map((step, index) => (
ui/src/components/JobActionBar.tsx CHANGED
@@ -1,5 +1,5 @@
1
  import Link from 'next/link';
2
- import { Eye, Trash2, Pen, Play, Pause, ExternalLink, Upload } from 'lucide-react';
3
  import { Button } from '@headlessui/react';
4
  import { openConfirm } from '@/components/ConfirmModal';
5
  import { startJob, stopJob, deleteJob, getAvaliableJobActions } from '@/utils/jobs';
@@ -34,15 +34,6 @@ export default function JobActionBar({ job, onRefresh, afterDelete, className, h
34
 
35
  return (
36
  <div className={`${className}`}>
37
- {isHFJob && !hfJobSubmitted && (
38
- <Link
39
- href={`/jobs/new?id=${job.id}`}
40
- className="ml-2 text-yellow-400 hover:text-yellow-300 inline-block"
41
- title="Submit to HF Jobs"
42
- >
43
- <Upload size={16} />
44
- </Link>
45
- )}
46
  {canStart && !isHFJob && (
47
  <Button
48
  onClick={async () => {
 
1
  import Link from 'next/link';
2
+ import { Eye, Trash2, Pen, Play, Pause, ExternalLink } from 'lucide-react';
3
  import { Button } from '@headlessui/react';
4
  import { openConfirm } from '@/components/ConfirmModal';
5
  import { startJob, stopJob, deleteJob, getAvaliableJobActions } from '@/utils/jobs';
 
34
 
35
  return (
36
  <div className={`${className}`}>
 
 
 
 
 
 
 
 
 
37
  {canStart && !isHFJob && (
38
  <Button
39
  onClick={async () => {
ui/src/components/JobOverview.tsx CHANGED
@@ -7,12 +7,14 @@ import { useEffect, useMemo, useRef, useState } from 'react';
7
  import useJobLog from '@/hooks/useJobLog';
8
  import { JobConfig, JobRecord } from '@/types';
9
  import HFJobStatus from './HFJobStatus';
 
10
 
11
  interface JobOverviewProps {
12
  job: JobRecord;
13
  }
14
 
15
  export default function JobOverview({ job }: JobOverviewProps) {
 
16
  // Parse job config to check if it's an HF Job
17
  const jobConfig = useMemo(() => {
18
  try {
@@ -112,9 +114,18 @@ export default function JobOverview({ job }: JobOverviewProps) {
112
  hfJobNamespace={jobConfig.hf_job_namespace}
113
  />
114
  ) : isHFJob && !hfJobSubmitted ? (
115
- <span className="px-3 py-1 rounded-full text-sm bg-yellow-500/10 text-yellow-500">
116
- Pending Submission
117
- </span>
 
 
 
 
 
 
 
 
 
118
  ) : (
119
  <span className={`px-3 py-1 rounded-full text-sm ${getStatusColor(job.status)}`}>
120
  {job.status}
 
7
  import useJobLog from '@/hooks/useJobLog';
8
  import { JobConfig, JobRecord } from '@/types';
9
  import HFJobStatus from './HFJobStatus';
10
+ import { useRouter } from 'next/navigation';
11
 
12
  interface JobOverviewProps {
13
  job: JobRecord;
14
  }
15
 
16
  export default function JobOverview({ job }: JobOverviewProps) {
17
+ const router = useRouter();
18
  // Parse job config to check if it's an HF Job
19
  const jobConfig = useMemo(() => {
20
  try {
 
114
  hfJobNamespace={jobConfig.hf_job_namespace}
115
  />
116
  ) : isHFJob && !hfJobSubmitted ? (
117
+ <div className="flex items-center gap-2">
118
+ <span className="px-3 py-1 rounded-full text-sm bg-yellow-500/10 text-yellow-500">
119
+ Pending Submission
120
+ </span>
121
+ <button
122
+ type="button"
123
+ onClick={() => router.push(`/jobs/new?id=${job.id}#hf-start-training`)}
124
+ className="px-3 py-1 rounded text-sm bg-blue-600 hover:bg-blue-500 text-white transition-colors"
125
+ >
126
+ Submit
127
+ </button>
128
+ </div>
129
  ) : (
130
  <span className={`px-3 py-1 rounded-full text-sm ${getStatusColor(job.status)}`}>
131
  {job.status}
ui/src/components/JobsTable.tsx CHANGED
@@ -102,14 +102,20 @@ export default function JobsTable({ onlyActive = false }: JobsTableProps) {
102
  hfJobNamespace={jobConfig.hf_job_namespace}
103
  />
104
  );
105
- } else {
106
- // HF Job that hasn't been submitted yet
107
- return (
108
- <span className="text-yellow-400">
109
- Pending Submission
110
- </span>
111
- );
112
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
113
  }
114
 
115
  // Local job status
 
102
  hfJobNamespace={jobConfig.hf_job_namespace}
103
  />
104
  );
 
 
 
 
 
 
 
105
  }
106
+
107
+ // HF Job that hasn't been submitted yet
108
+ return (
109
+ <div className="flex items-center gap-2 text-sm">
110
+ <span className="text-yellow-400">Pending Submission</span>
111
+ <Link
112
+ href={`/jobs/new?id=${row.id}#hf-start-training`}
113
+ className="font-semibold text-blue-400 hover:text-blue-300"
114
+ >
115
+ Submit
116
+ </Link>
117
+ </div>
118
+ );
119
  }
120
 
121
  // Local job status