apolinario commited on
Commit
3947e91
·
1 Parent(s): a4f7e16

apply fixes

Browse files
ui/src/app/dashboard/page.tsx CHANGED
@@ -1,14 +1,68 @@
1
  'use client';
2
 
 
3
  import JobsTable from '@/components/JobsTable';
4
  import { TopBar, MainContent } from '@/components/layout';
5
  import Link from 'next/link';
6
  import { useAuth } from '@/contexts/AuthContext';
7
  import HFLoginButton from '@/components/HFLoginButton';
 
 
8
 
9
  export default function Dashboard() {
10
- const { status: authStatus, namespace } = useAuth();
 
11
  const isAuthenticated = authStatus === 'authenticated';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
12
 
13
  return (
14
  <>
@@ -38,50 +92,97 @@ export default function Dashboard() {
38
  </p>
39
  </div>
40
  {isAuthenticated ? (
41
- <div className="flex flex-wrap items-center gap-3 text-sm">
42
- <Link
43
- href="/jobs/new"
44
- className="px-4 py-2 rounded-md bg-blue-600 hover:bg-blue-500 text-white transition-colors"
45
- >
46
- Create a Training Job
47
- </Link>
48
- <Link
49
- href="/datasets"
50
- className="px-4 py-2 rounded-md bg-gray-800 hover:bg-gray-700 text-gray-200 transition-colors"
51
- >
52
- Manage Datasets
53
- </Link>
54
- <Link
55
- href="/settings"
56
- className="px-4 py-2 rounded-md border border-gray-700 text-gray-300 hover:border-gray-600 transition-colors"
57
- >
58
- Settings
59
- </Link>
60
- </div>
61
- ) : (
62
- <div className="flex flex-col gap-2 text-sm">
63
- <div className="flex flex-wrap items-start gap-4">
64
- <HFLoginButton size="md" />
65
- <div className="flex flex-col gap-2 text-sm text-gray-300 max-w-sm">
66
  <p>
67
- Planning to join LoRA Frenzi? Make sure your training runs are 5,000 steps or fewer and join the{' '}
68
- <a
69
- href="https://huggingface.co/organizations/lora-training-frenzi/share/kEyyVNQXBPWqmARdwHFVdIiFqqONHZPOtz"
70
- target="_blank"
71
- rel="noopener noreferrer"
72
- className="text-blue-400 underline"
73
- >
74
- LoRA Frenzi organization
75
- </a>{' '}
76
- before authorizing the <code className="bg-gray-800 px-1 rounded">lorafrenzi</code> organization when logging in.
77
  </p>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
78
  <img
79
  src="https://huggingface.co/spaces/multimodalart/ai-toolkit/resolve/main/add_org_to_oauth.png"
80
  alt="Authorize lorafrenzi organization"
81
- className="max-w-sm rounded border border-gray-800"
82
  />
83
- </div>
84
- </div>
 
 
 
 
85
  <Link
86
  href="/settings"
87
  className="text-xs text-blue-400 hover:text-blue-300"
 
1
  'use client';
2
 
3
+ import { useEffect, useMemo, useState } from 'react';
4
  import JobsTable from '@/components/JobsTable';
5
  import { TopBar, MainContent } from '@/components/layout';
6
  import Link from 'next/link';
7
  import { useAuth } from '@/contexts/AuthContext';
8
  import HFLoginButton from '@/components/HFLoginButton';
9
+ import useSettings from '@/hooks/useSettings';
10
+ import { apiClient } from '@/utils/api';
11
 
12
  export default function Dashboard() {
13
+ const { status: authStatus, namespace, token: authToken } = useAuth();
14
+ const { settings } = useSettings();
15
  const isAuthenticated = authStatus === 'authenticated';
16
+ const effectiveToken = useMemo(() => authToken || settings.HF_TOKEN, [authToken, settings.HF_TOKEN]);
17
+
18
+ type OrgStatus = 'idle' | 'checking' | 'member' | 'missing' | 'error';
19
+ const [orgStatus, setOrgStatus] = useState<OrgStatus>('idle');
20
+
21
+ useEffect(() => {
22
+ if (!isAuthenticated) {
23
+ setOrgStatus('idle');
24
+ return;
25
+ }
26
+
27
+ if (!effectiveToken) {
28
+ setOrgStatus('idle');
29
+ return;
30
+ }
31
+
32
+ let cancelled = false;
33
+ setOrgStatus('checking');
34
+
35
+ apiClient
36
+ .post('/api/hf-hub', {
37
+ action: 'whoami',
38
+ token: effectiveToken,
39
+ })
40
+ .then(response => {
41
+ if (cancelled) return;
42
+ const orgsRaw = response.data?.user?.orgs ?? response.data?.user?.organizations ?? [];
43
+ const REQUIRED_ORG = 'lora-training-frenzi';
44
+ const isMember = Array.isArray(orgsRaw)
45
+ ? orgsRaw.some((org: any) => {
46
+ if (!org) return false;
47
+ if (typeof org === 'string') {
48
+ return org === REQUIRED_ORG;
49
+ }
50
+ const nameMatch = org?.name || org?.organization || org?.namespace || org?.id;
51
+ return nameMatch === REQUIRED_ORG;
52
+ })
53
+ : false;
54
+ setOrgStatus(isMember ? 'member' : 'missing');
55
+ })
56
+ .catch(() => {
57
+ if (!cancelled) {
58
+ setOrgStatus('error');
59
+ }
60
+ });
61
+
62
+ return () => {
63
+ cancelled = true;
64
+ };
65
+ }, [effectiveToken, isAuthenticated]);
66
 
67
  return (
68
  <>
 
92
  </p>
93
  </div>
94
  {isAuthenticated ? (
95
+ <div className="flex flex-col gap-4 text-sm">
96
+ {orgStatus === 'checking' && (
97
+ <div className="text-xs text-gray-400">Checking your LoRA Frenzi membership…</div>
98
+ )}
99
+ {orgStatus === 'missing' && (
100
+ <div className="border border-yellow-700 bg-yellow-900/20 text-yellow-100 rounded-lg p-4 space-y-2">
101
+ <p className="font-semibold">LoRA Frenzi reminder</p>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
102
  <p>
103
+ Your Hugging Face account isn&apos;t in the <code className="bg-yellow-800/50 px-1 rounded">lora-training-frenzi</code> organization yet.
104
+ To participate:
 
 
 
 
 
 
 
 
105
  </p>
106
+ <ol className="list-decimal list-inside space-y-1 text-sm">
107
+ <li>
108
+ Join the{' '}
109
+ <a
110
+ href="https://huggingface.co/organizations/lora-training-frenzi/share/kEyyVNQXBPWqmARdwHFVdIiFqqONHZPOtz"
111
+ target="_blank"
112
+ rel="noopener noreferrer"
113
+ className="text-blue-200 underline"
114
+ >
115
+ LoRA Frenzi organization
116
+ </a>
117
+ .
118
+ </li>
119
+ <li>
120
+ Remove the “Ostris AI Toolkit” app from{' '}
121
+ <a
122
+ href="https://huggingface.co/settings/connected-applications"
123
+ target="_blank"
124
+ rel="noopener noreferrer"
125
+ className="text-blue-200 underline"
126
+ >
127
+ Hugging Face &gt; Connected Applications
128
+ </a>
129
+ .
130
+ </li>
131
+ <li>Log out here and sign in again so we can refresh your permissions.</li>
132
+ </ol>
133
+ </div>
134
+ )}
135
+ {orgStatus === 'error' && (
136
+ <div className="border border-red-700 bg-red-900/20 text-red-100 rounded-lg p-4 text-sm">
137
+ We couldn&apos;t verify your organization membership right now. You can retry later or ensure your token is valid.
138
+ </div>
139
+ )}
140
+ <div className="flex flex-wrap items-center gap-3">
141
+ <Link
142
+ href="/jobs/new"
143
+ className="px-4 py-2 rounded-md bg-blue-600 hover:bg-blue-500 text-white transition-colors"
144
+ >
145
+ Create a Training Job
146
+ </Link>
147
+ <Link
148
+ href="/datasets"
149
+ className="px-4 py-2 rounded-md bg-gray-800 hover:bg-gray-700 text-gray-200 transition-colors"
150
+ >
151
+ Manage Datasets
152
+ </Link>
153
+ <Link
154
+ href="/settings"
155
+ className="px-4 py-2 rounded-md border border-gray-700 text-gray-300 hover:border-gray-600 transition-colors"
156
+ >
157
+ Settings
158
+ </Link>
159
+ </div>
160
+ </div>
161
+ ) : (
162
+ <div className="flex flex-col gap-4 text-sm text-gray-300">
163
+ <ol className="list-decimal list-inside space-y-4">
164
+ <li>
165
+ Join the{' '}
166
+ <a
167
+ href="https://huggingface.co/organizations/lora-training-frenzi/share/kEyyVNQXBPWqmARdwHFVdIiFqqONHZPOtz"
168
+ target="_blank"
169
+ rel="noopener noreferrer"
170
+ className="text-blue-400 underline"
171
+ >
172
+ LoRA Frenzi organization
173
+ </a>{' '}
174
+ first, then authorize the <code className="bg-gray-800 px-1 rounded">lorafrenzi</code> org when prompted.
175
  <img
176
  src="https://huggingface.co/spaces/multimodalart/ai-toolkit/resolve/main/add_org_to_oauth.png"
177
  alt="Authorize lorafrenzi organization"
178
+ className="mt-3 max-w-sm rounded border border-gray-800"
179
  />
180
+ </li>
181
+ <li className="space-y-3">
182
+ <span>Sign in with Hugging Face to create and track your jobs.</span>
183
+ <HFLoginButton size="md" />
184
+ </li>
185
+ </ol>
186
  <Link
187
  href="/settings"
188
  className="text-xs text-blue-400 hover:text-blue-300"
ui/src/app/jobs/new/SimplifiedJob.tsx CHANGED
@@ -61,7 +61,6 @@ export default function SimplifiedJob({
61
  onHFJobComplete,
62
  forceHFBackend = false,
63
  datasetOptions,
64
- settings,
65
  namespace,
66
  }: SimplifiedJobProps) {
67
  const [showAdvanced, setShowAdvanced] = useState(false);
@@ -409,7 +408,7 @@ export default function SimplifiedJob({
409
  <div className="flex gap-2 mb-4">
410
  {([
411
  { value: 'upload', label: 'Upload Files' },
412
- { value: 'existing', label: 'Existing Path' },
413
  ] as const).map(option => (
414
  <button
415
  key={option.value}
@@ -473,12 +472,6 @@ export default function SimplifiedJob({
473
  placeholder="Enter a name for this LoRA"
474
  required
475
  />
476
- <TextInput
477
- label="Job Name"
478
- value={jobConfig.meta?.name || ''}
479
- onChange={value => setJobConfig(value, 'meta.name')}
480
- placeholder="Display name for this job"
481
- />
482
  </div>
483
  <div className="grid grid-cols-1 md:grid-cols-2 gap-6 mt-6">
484
  <SelectInput
@@ -519,6 +512,75 @@ export default function SimplifiedJob({
519
  </div>
520
  </Card>
521
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
522
  <Card title="Dataset">
523
  {renderDatasetModeSwitch(trainDatasetMode, value => {
524
  setTrainDatasetMode(value);
@@ -666,75 +728,6 @@ export default function SimplifiedJob({
666
  )}
667
  </Card>
668
 
669
- <Card>
670
- <div className="flex items-center justify-between">
671
- <h2 className="text-lg font-semibold text-gray-100">Advanced Options</h2>
672
- <Button
673
- type="button"
674
- className="flex items-center gap-2 text-sm text-blue-300 hover:text-blue-200"
675
- onClick={() => setShowAdvanced(prev => !prev)}
676
- >
677
- {showAdvanced ? <ChevronUp className="h-4 w-4" /> : <ChevronDown className="h-4 w-4" />}
678
- {showAdvanced ? 'Hide' : 'Show'}
679
- </Button>
680
- </div>
681
- {showAdvanced && (
682
- <div className="mt-6 grid grid-cols-1 md:grid-cols-2 gap-6">
683
- <SelectInput
684
- label="Training Backend"
685
- value={trainingBackend}
686
- onChange={value => setTrainingBackend?.(value as 'local' | 'hf-jobs')}
687
- options={effectiveBackendOptions}
688
- disabled={forceHFBackend || !setTrainingBackend}
689
- />
690
- {trainingBackend === 'local' && (
691
- <SelectInput
692
- label="GPU"
693
- value={gpuIDs ?? ''}
694
- onChange={value => setGpuIDs(value)}
695
- options={gpuList.map((gpu: any) => ({ value: `${gpu.index}`, label: `GPU #${gpu.index}` }))}
696
- />
697
- )}
698
- <NumberInput
699
- label="Learning Rate"
700
- value={process.train.lr}
701
- onChange={value => setJobConfig(value, 'config.process[0].train.lr')}
702
- min={0}
703
- step={0.000001}
704
- required
705
- />
706
- <NumberInput
707
- label="LoRA Rank"
708
- value={process.network?.linear ?? 32}
709
- onChange={handleLoraRankChange}
710
- min={1}
711
- required
712
- />
713
- <SelectInput
714
- label="Optimizer"
715
- value={process.train.optimizer}
716
- onChange={value => setJobConfig(value, 'config.process[0].train.optimizer')}
717
- options={[
718
- { value: 'adamw8bit', label: 'AdamW8Bit' },
719
- { value: 'adafactor', label: 'Adafactor' },
720
- ]}
721
- />
722
- <div className="flex items-center gap-2 pt-2">
723
- <Checkbox
724
- label="Skip First Sample"
725
- checked={process.train.skip_first_sample}
726
- onChange={value => setJobConfig(value, 'config.process[0].train.skip_first_sample')}
727
- />
728
- <Checkbox
729
- label="Disable Sampling"
730
- checked={process.train.disable_sampling}
731
- onChange={value => setJobConfig(value, 'config.process[0].train.disable_sampling')}
732
- />
733
- </div>
734
- </div>
735
- )}
736
- </Card>
737
-
738
  {trainingBackend === 'hf-jobs' && (
739
  <div className="space-y-6">
740
  <HFJobsWorkflow
 
61
  onHFJobComplete,
62
  forceHFBackend = false,
63
  datasetOptions,
 
64
  namespace,
65
  }: SimplifiedJobProps) {
66
  const [showAdvanced, setShowAdvanced] = useState(false);
 
408
  <div className="flex gap-2 mb-4">
409
  {([
410
  { value: 'upload', label: 'Upload Files' },
411
+ { value: 'existing', label: 'Existing Dataset' },
412
  ] as const).map(option => (
413
  <button
414
  key={option.value}
 
472
  placeholder="Enter a name for this LoRA"
473
  required
474
  />
 
 
 
 
 
 
475
  </div>
476
  <div className="grid grid-cols-1 md:grid-cols-2 gap-6 mt-6">
477
  <SelectInput
 
512
  </div>
513
  </Card>
514
 
515
+ <Card>
516
+ <div className="flex items-center justify-between">
517
+ <h2 className="text-lg font-semibold text-gray-100">Advanced Options</h2>
518
+ <Button
519
+ type="button"
520
+ className="flex items-center gap-2 text-sm text-blue-300 hover:text-blue-200"
521
+ onClick={() => setShowAdvanced(prev => !prev)}
522
+ >
523
+ {showAdvanced ? <ChevronUp className="h-4 w-4" /> : <ChevronDown className="h-4 w-4" />}
524
+ {showAdvanced ? 'Hide' : 'Show'}
525
+ </Button>
526
+ </div>
527
+ {showAdvanced && (
528
+ <div className="mt-6 grid grid-cols-1 md:grid-cols-2 gap-6">
529
+ <SelectInput
530
+ label="Training Backend"
531
+ value={trainingBackend}
532
+ onChange={value => setTrainingBackend?.(value as 'local' | 'hf-jobs')}
533
+ options={effectiveBackendOptions}
534
+ disabled={forceHFBackend || !setTrainingBackend}
535
+ />
536
+ {trainingBackend === 'local' && (
537
+ <SelectInput
538
+ label="GPU"
539
+ value={gpuIDs ?? ''}
540
+ onChange={value => setGpuIDs(value)}
541
+ options={gpuList.map((gpu: any) => ({ value: `${gpu.index}`, label: `GPU #${gpu.index}` }))}
542
+ />
543
+ )}
544
+ <NumberInput
545
+ label="Learning Rate"
546
+ value={process.train.lr}
547
+ onChange={value => setJobConfig(value, 'config.process[0].train.lr')}
548
+ min={0}
549
+ step={0.000001}
550
+ required
551
+ />
552
+ <NumberInput
553
+ label="LoRA Rank"
554
+ value={process.network?.linear ?? 32}
555
+ onChange={handleLoraRankChange}
556
+ min={1}
557
+ required
558
+ />
559
+ <SelectInput
560
+ label="Optimizer"
561
+ value={process.train.optimizer}
562
+ onChange={value => setJobConfig(value, 'config.process[0].train.optimizer')}
563
+ options={[
564
+ { value: 'adamw8bit', label: 'AdamW8Bit' },
565
+ { value: 'adafactor', label: 'Adafactor' },
566
+ ]}
567
+ />
568
+ <div className="flex items-center gap-2 pt-2">
569
+ <Checkbox
570
+ label="Skip First Sample"
571
+ checked={process.train.skip_first_sample}
572
+ onChange={value => setJobConfig(value, 'config.process[0].train.skip_first_sample')}
573
+ />
574
+ <Checkbox
575
+ label="Disable Sampling"
576
+ checked={process.train.disable_sampling}
577
+ onChange={value => setJobConfig(value, 'config.process[0].train.disable_sampling')}
578
+ />
579
+ </div>
580
+ </div>
581
+ )}
582
+ </Card>
583
+
584
  <Card title="Dataset">
585
  {renderDatasetModeSwitch(trainDatasetMode, value => {
586
  setTrainDatasetMode(value);
 
728
  )}
729
  </Card>
730
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
731
  {trainingBackend === 'hf-jobs' && (
732
  <div className="space-y-6">
733
  <HFJobsWorkflow
ui/src/app/jobs/new/page.tsx CHANGED
@@ -201,70 +201,57 @@ export default function TrainingForm() {
201
 
202
  return (
203
  <>
204
- <TopBar>
205
- <div>
206
- <Button className="text-gray-500 dark:text-gray-300 px-3 mt-1" onClick={() => history.back()}>
207
- <FaChevronLeft />
208
- </Button>
209
- </div>
210
- <div>
211
- <h1 className="text-lg">{runId ? 'Edit Training Job' : 'New Training Job'}</h1>
212
- </div>
213
- <div className="flex-1"></div>
214
- {showAdvancedView && isAuthenticated && (
215
- <>
216
- <div>
217
- <SelectInput
218
- value={`${gpuIDs}`}
219
- onChange={value => setGpuIDs(value)}
220
- options={gpuList.map((gpu: any) => ({ value: `${gpu.index}`, label: `GPU #${gpu.index}` }))}
221
- />
222
- </div>
223
- <div className="mx-4 bg-gray-200 dark:bg-gray-800 w-1 h-6"></div>
224
- </>
225
- )}
226
-
227
- {isAuthenticated && activeTab === 'original' && (
228
- <div className="pr-2">
229
- <Button
230
- className="text-gray-200 bg-gray-800 px-3 py-1 rounded-md"
231
- onClick={() => setShowAdvancedView(!showAdvancedView)}
232
- >
233
- {showAdvancedView ? 'Show Original Layout' : 'Show Advanced Layout'}
234
  </Button>
235
  </div>
236
- )}
237
- <div>
238
- <Button
239
- className="text-gray-200 bg-green-800 hover:bg-green-700 px-3 py-1 rounded-md"
240
- onClick={() => saveJob()}
241
- disabled={!isAuthenticated || status === 'saving'}
242
- >
243
- {status === 'saving'
244
- ? 'Saving...'
245
- : runId
246
- ? 'Update Job'
247
- : 'Create Job'}
248
- </Button>
249
- </div>
250
- </TopBar>
 
251
 
252
- {!isAuthenticated ? (
253
- <MainContent>
254
- <div className="border border-gray-800 rounded-lg p-6 bg-gray-900 text-gray-400 text-sm flex flex-col gap-4">
255
- <p>You need to sign in with Hugging Face or provide a valid access token before creating or editing jobs.</p>
256
- <div className="flex items-center gap-3">
257
- <HFLoginButton size="sm" />
258
- <Link href="/settings" className="text-xs text-blue-400 hover:text-blue-300">
259
- Manage authentication in Settings
260
- </Link>
261
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
262
  </div>
263
- </MainContent>
264
- ) : (
265
- <>
266
- <div className="bg-gray-950 border-b border-gray-800">
267
- <div className="flex px-6 gap-4">
268
  {(
269
  [
270
  { id: 'simplified', label: 'Simplified View' },
@@ -273,10 +260,10 @@ export default function TrainingForm() {
273
  ).map(tab => (
274
  <button
275
  key={tab.id}
276
- className={`px-4 py-3 text-sm font-medium border-b-2 transition-colors ${
277
  activeTab === tab.id
278
- ? 'border-blue-500 text-blue-300'
279
- : 'border-transparent text-gray-400 hover:text-gray-300'
280
  }`}
281
  onClick={() => setActiveTab(tab.id)}
282
  type="button"
@@ -286,9 +273,25 @@ export default function TrainingForm() {
286
  ))}
287
  </div>
288
  </div>
 
 
289
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
290
  {activeTab === 'simplified' ? (
291
- <MainContent>
292
  <SimplifiedJob
293
  jobConfig={jobConfig}
294
  setJobConfig={setJobConfig}
@@ -312,22 +315,24 @@ export default function TrainingForm() {
312
  />
313
  </MainContent>
314
  ) : showAdvancedView ? (
315
- <div className="px-6 py-8">
316
- <AdvancedJob
317
- jobConfig={jobConfig}
318
- setJobConfig={setJobConfig}
319
- status={status}
320
- handleSubmit={handleSubmit}
321
- runId={runId}
322
- gpuIDs={gpuIDs}
323
- setGpuIDs={setGpuIDs}
324
- gpuList={gpuList}
325
- datasetOptions={datasetOptions}
326
- settings={settings}
327
- />
328
- </div>
 
 
329
  ) : (
330
- <MainContent>
331
  <ErrorBoundary
332
  fallback={
333
  <div className="flex items-center justify-center h-64 text-lg text-red-600 font-medium bg-red-100 dark:bg-red-900/20 dark:text-red-400 border border-red-300 dark:border-red-700 rounded-lg">
 
201
 
202
  return (
203
  <>
204
+ <div className="relative z-20">
205
+ <TopBar>
206
+ <div>
207
+ <Button className="text-gray-500 dark:text-gray-300 px-3 mt-1" onClick={() => history.back()}>
208
+ <FaChevronLeft />
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
209
  </Button>
210
  </div>
211
+ <div>
212
+ <h1 className="text-lg">{runId ? 'Edit Training Job' : 'New Training Job'}</h1>
213
+ </div>
214
+ <div className="flex-1"></div>
215
+ {showAdvancedView && isAuthenticated && (
216
+ <>
217
+ <div>
218
+ <SelectInput
219
+ value={`${gpuIDs}`}
220
+ onChange={value => setGpuIDs(value)}
221
+ options={gpuList.map((gpu: any) => ({ value: `${gpu.index}`, label: `GPU #${gpu.index}` }))}
222
+ />
223
+ </div>
224
+ <div className="mx-4 bg-gray-200 dark:bg-gray-800 w-1 h-6"></div>
225
+ </>
226
+ )}
227
 
228
+ {isAuthenticated && activeTab === 'original' && (
229
+ <div className="pr-2">
230
+ <Button
231
+ className="text-gray-200 bg-gray-800 px-3 py-1 rounded-md"
232
+ onClick={() => setShowAdvancedView(!showAdvancedView)}
233
+ >
234
+ {showAdvancedView ? 'Show Original Layout' : 'Show Advanced Layout'}
235
+ </Button>
 
236
  </div>
237
+ )}
238
+ <div>
239
+ <Button
240
+ className="text-gray-200 bg-green-800 hover:bg-green-700 px-3 py-1 rounded-md"
241
+ onClick={() => saveJob()}
242
+ disabled={!isAuthenticated || status === 'saving'}
243
+ >
244
+ {status === 'saving'
245
+ ? 'Saving...'
246
+ : runId
247
+ ? 'Update Job'
248
+ : 'Create Job'}
249
+ </Button>
250
  </div>
251
+ </TopBar>
252
+ {isAuthenticated && (
253
+ <div className="bg-gray-900/70 border-b border-gray-700">
254
+ <div className="flex justify-center gap-4 px-6 py-3">
 
255
  {(
256
  [
257
  { id: 'simplified', label: 'Simplified View' },
 
260
  ).map(tab => (
261
  <button
262
  key={tab.id}
263
+ className={`px-6 py-2 text-sm font-semibold rounded-t-md border-b-2 transition-colors ${
264
  activeTab === tab.id
265
+ ? 'border-blue-500 text-blue-200 bg-blue-500/10'
266
+ : 'border-transparent text-gray-400 hover:text-gray-200'
267
  }`}
268
  onClick={() => setActiveTab(tab.id)}
269
  type="button"
 
273
  ))}
274
  </div>
275
  </div>
276
+ )}
277
+ </div>
278
 
279
+ {!isAuthenticated ? (
280
+ <MainContent className="pt-28">
281
+ <div className="border border-gray-800 rounded-lg p-6 bg-gray-900 text-gray-400 text-sm flex flex-col gap-4">
282
+ <p>You need to sign in with Hugging Face or provide a valid access token before creating or editing jobs.</p>
283
+ <div className="flex items-center gap-3">
284
+ <HFLoginButton size="sm" />
285
+ <Link href="/settings" className="text-xs text-blue-400 hover:text-blue-300">
286
+ Manage authentication in Settings
287
+ </Link>
288
+ </div>
289
+ </div>
290
+ </MainContent>
291
+ ) : (
292
+ <>
293
  {activeTab === 'simplified' ? (
294
+ <MainContent className="pt-28">
295
  <SimplifiedJob
296
  jobConfig={jobConfig}
297
  setJobConfig={setJobConfig}
 
315
  />
316
  </MainContent>
317
  ) : showAdvancedView ? (
318
+ <MainContent className="pt-28">
319
+ <div className="px-2 pb-6">
320
+ <AdvancedJob
321
+ jobConfig={jobConfig}
322
+ setJobConfig={setJobConfig}
323
+ status={status}
324
+ handleSubmit={handleSubmit}
325
+ runId={runId}
326
+ gpuIDs={gpuIDs}
327
+ setGpuIDs={setGpuIDs}
328
+ gpuList={gpuList}
329
+ datasetOptions={datasetOptions}
330
+ settings={settings}
331
+ />
332
+ </div>
333
+ </MainContent>
334
  ) : (
335
+ <MainContent className="pt-28">
336
  <ErrorBoundary
337
  fallback={
338
  <div className="flex items-center justify-center h-64 text-lg text-red-600 font-medium bg-red-100 dark:bg-red-900/20 dark:text-red-400 border border-red-300 dark:border-red-700 rounded-lg">