Spaces:
Paused
Paused
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"
|
| 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 |
-
|
| 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 |
-
|
| 518 |
-
|
| 519 |
-
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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(
|
| 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 |
-
|
| 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={
|
| 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
|
| 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 |
-
<
|
| 116 |
-
|
| 117 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|