diff --git a/package-lock.json b/package-lock.json index b61cbd41..2f57b778 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5808,12 +5808,6 @@ "uuid": "dist/bin/uuid" } }, - "node_modules/next/node_modules/@next/env": { - "version": "15.3.2", - "resolved": "https://registry.npmjs.org/@next/env/-/env-15.3.2.tgz", - "integrity": "sha512-xURk++7P7qR9JG1jJtLzPzf0qEvqCN0A/T3DXf8IPMKo9/6FfjxtEffRJIIew/bIL4T3C2jLLqBor8B/zVlx6g==", - "license": "MIT" - }, "node_modules/next/node_modules/postcss": { "version": "8.4.31", "funding": [ diff --git a/src/app/experimental/fine-tune/page.tsx b/src/app/experimental/fine-tune/page.tsx index 12b5b54f..8b347fe5 100644 --- a/src/app/experimental/fine-tune/page.tsx +++ b/src/app/experimental/fine-tune/page.tsx @@ -4,7 +4,7 @@ import * as React from 'react'; import '@patternfly/react-core/dist/styles/base.css'; import { AppLayout, FeaturePages } from '@/components/AppLayout'; -import FineTuning from '@/components/Experimental/FineTuning'; +import FineTuning from '@/components/Experimental/FineTuning/FineTuningJobs'; const FineTune: React.FunctionComponent = () => { return ( diff --git a/src/components/Contribute/Knowledge/KnowledgeSeedExamples/KnowledgeSeedExamples.scss b/src/components/Contribute/Knowledge/KnowledgeSeedExamples/KnowledgeSeedExamples.scss index 6593a459..75712e09 100644 --- a/src/components/Contribute/Knowledge/KnowledgeSeedExamples/KnowledgeSeedExamples.scss +++ b/src/components/Contribute/Knowledge/KnowledgeSeedExamples/KnowledgeSeedExamples.scss @@ -8,12 +8,16 @@ border-style: solid; &.pf-m-info { background-color: var(--pf-t--color--blue--10); + --pf-v6-c-alert--m-info__icon--Color: var(--pf-t--color--black); + --pf-v6-c-alert--m-info__title--Color: var(--pf-t--color--black); } &.pf-m-warning { background-color: #fdf7e7; + --pf-v6-c-alert--m-warning__title--Color: var(--pf-t--color--black); } &.pf-m-danger { background-color: #faeae8; + --pf-v6-c-alert--m-danger__title--Color: var(--pf-t--color--black); } } } @@ -32,7 +36,7 @@ padding-bottom: 1rem; &:not(:focus-visible) { - outline: var(--pf-t--global--background--color--primary--default) auto 1px; + outline: transparent auto 0; } } } @@ -40,7 +44,7 @@ position: absolute; bottom: calc(var(--pf-t--global--spacer--xs) + 3px); left: var(--pf-t--global--spacer--md); - background-color: var(--pf-t--global--background--color--primary--default); + background-color: transparent; width: calc(100% - 2 * var(--pf-t--global--spacer--lg)); } } diff --git a/src/components/Experimental/FineTuning/AddFineTuningJobModal.tsx b/src/components/Experimental/FineTuning/AddFineTuningJobModal.tsx new file mode 100644 index 00000000..4c169eaf --- /dev/null +++ b/src/components/Experimental/FineTuning/AddFineTuningJobModal.tsx @@ -0,0 +1,276 @@ +// src/components/Experimental/FineTuning/index.tsx +'use client'; + +import React from 'react'; +import { + Modal, + Form, + FormGroup, + Dropdown, + DropdownItem, + DropdownList, + MenuToggle, + MenuToggleElement, + NumberInput, + Button, + Alert, + ModalHeader, + ModalBody, + ModalFooter, + Flex, + FlexItem +} from '@patternfly/react-core'; +import { Model, Branch, Job } from '@/components/Experimental/FineTuning/types'; + +interface Props { + models: Model[]; + branches: Branch[]; + onClose: (newJob?: Job) => void; +} + +const AddFineTuningJobModal: React.FC = ({ models, branches, onClose }) => { + const [errorMessage, setErrorMessage] = React.useState( + !models.length || !branches.length ? 'No data available for creating fine tuning jobs.' : '' + ); + + const [selectedModel, setSelectedModel] = React.useState(''); + const [selectedBranch, setSelectedBranch] = React.useState(''); + const [selectedEpochs, setSelectedEpochs] = React.useState(''); + + const [isModelDropdownOpen, setIsModelDropdownOpen] = React.useState(false); + const [isBranchDropdownOpen, setIsBranchDropdownOpen] = React.useState(false); + + const isValid = !!selectedBranch && !!selectedModel && selectedEpochs; + + const handleGenerateClick = async () => { + if (!selectedModel || !selectedBranch) { + setErrorMessage('Please select both a model and a branch.'); + return; + } + if (!selectedEpochs) { + setErrorMessage('Please enter the number of epochs.'); + return; + } + try { + const response = await fetch('/api/fine-tune/data/generate', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ modelName: selectedModel, branchName: selectedBranch, epochs: selectedEpochs }), // Include epochs + cache: 'no-cache' + }); + const result = await response.json(); + if (response.ok) { + const newJob: Job = { + job_id: result.job_id, + status: 'running', + type: result.job_id.startsWith('g-') ? 'generate' : result.job_id.startsWith('p-') ? 'pipeline' : 'train', + start_time: new Date().toISOString() + }; + onClose(newJob); + } else { + setErrorMessage(result.error || 'Failed to start generate job'); + } + } catch (error) { + console.error('Error starting generate job:', error); + setErrorMessage('Error starting generate job'); + } + }; + + const handleTrainClick = async () => { + if (!selectedModel || !selectedBranch) { + setErrorMessage('Please select both a model and a branch.'); + return; + } + if (!selectedEpochs) { + setErrorMessage('Please enter the number of epochs.'); + return; + } + + try { + console.log('Sending train request with:', { + modelName: selectedModel, + branchName: selectedBranch, + epochs: selectedEpochs + }); + + const response = await fetch('/api/fine-tune/model/train', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + modelName: selectedModel, + branchName: selectedBranch, + epochs: selectedEpochs + }), + cache: 'no-cache' + }); + const result = await response.json(); + if (response.ok) { + const newJob: Job = { + job_id: result.job_id, + status: 'running', + type: result.job_id.startsWith('g-') ? 'generate' : result.job_id.startsWith('p-') ? 'pipeline' : 'train', + start_time: new Date().toISOString() + }; + onClose(newJob); + } else { + setErrorMessage(result.error || 'Failed to start train job'); + } + } catch (error) { + console.error('Error starting train job:', error); + setErrorMessage('Error starting train job'); + } + }; + + const handlePipelineClick = async () => { + if (!selectedModel || !selectedBranch) { + setErrorMessage('Please select both a model and a branch.'); + return; + } + if (!selectedEpochs) { + setErrorMessage('Please enter the number of epochs.'); + return; + } + + try { + console.debug('Sending pipeline generate-train request with:', { + modelName: selectedModel, + branchName: selectedBranch, + epochs: selectedEpochs + }); + const response = await fetch('/api/fine-tune/pipeline/generate-train', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ modelName: selectedModel, branchName: selectedBranch, epochs: selectedEpochs }), // Include epochs + cache: 'no-cache' + }); + const result = await response.json(); + if (response.ok && result.pipeline_job_id) { + // Add the new job to the job list + const newJob: Job = { + job_id: result.pipeline_job_id, + status: 'running', + type: 'pipeline', + branch: selectedBranch, + start_time: new Date().toISOString() + }; + onClose(newJob); + console.debug('New pipeline job added:', newJob); + } else { + setErrorMessage(result.error || 'Failed to start generate-train pipeline'); + console.warn('Pipeline action failed:', result.error); + } + } catch (error) { + console.error('Error starting generate-train pipeline job:', error); + setErrorMessage('Error starting generate-train pipeline job'); + } + }; + + return ( + onClose()} variant="small"> + + + + {errorMessage ? ( + + + + ) : null} + +
+ + { + setSelectedBranch(value as string); + setIsBranchDropdownOpen(false); + }} + onOpenChange={(isOpen) => setIsBranchDropdownOpen(isOpen)} + toggle={(toggleRef: React.Ref) => ( + setIsBranchDropdownOpen(!isBranchDropdownOpen)} isExpanded={isBranchDropdownOpen}> + {selectedBranch || 'Select a branch'} + + )} + > + + {branches.map((branch) => ( + + {branch.name} + + ))} + + + + + + { + setSelectedModel(value as string); + setIsModelDropdownOpen(false); + }} + onOpenChange={(isOpen) => setIsModelDropdownOpen(isOpen)} + toggle={(toggleRef: React.Ref) => ( + setIsModelDropdownOpen(!isModelDropdownOpen)} isExpanded={isModelDropdownOpen}> + {selectedModel || 'Select a model'} + + )} + > + + {models.map((model) => ( + + {model.name} + + ))} + + + + + {/* New FormGroup for Epoch Selection using NumberInput */} + + { + const newValue = typeof selectedEpochs === 'number' ? selectedEpochs - 1 : 0; + setSelectedEpochs(newValue >= 1 ? newValue : 1); // Ensure minimum of 1 + }} + onChange={(event: React.FormEvent) => { + const value = (event.target as HTMLInputElement).value; + const parsedValue = value === '' ? '' : Number(value); + if (parsedValue === '' || (Number.isInteger(parsedValue) && parsedValue > 0)) { + setSelectedEpochs(parsedValue); + } + }} + onPlus={() => { + const newValue = typeof selectedEpochs === 'number' ? selectedEpochs + 1 : 1; + setSelectedEpochs(newValue); + }} + inputName="epochs" + inputAriaLabel="Number of Epochs" + minusBtnAriaLabel="decrease number of epochs" + plusBtnAriaLabel="increase number of epochs" + min={1} + /> + +
+
+
+
+ + + + + + +
+ ); +}; + +export default AddFineTuningJobModal; diff --git a/src/components/Experimental/FineTuning/FineTuningJobs.tsx b/src/components/Experimental/FineTuning/FineTuningJobs.tsx new file mode 100644 index 00000000..6a602e0e --- /dev/null +++ b/src/components/Experimental/FineTuning/FineTuningJobs.tsx @@ -0,0 +1,384 @@ +// src/components/Experimental/FineTuning/index.tsx +'use client'; + +import React, { useState, useEffect, useRef } from 'react'; +import Image from 'next/image'; +import { format } from 'date-fns'; +import { + ToggleGroupItem, + ToggleGroup, + Flex, + FlexItem, + Bullseye, + EmptyStateVariant, + ExpandableSection, + CodeBlock, + CodeBlockCode, + PageSection, + Card, + CardTitle, + CardBody, + CardFooter, + Title, + Button, + EmptyState, + EmptyStateBody, + Spinner, + Alert, + EmptyStateFooter, + EmptyStateActions +} from '@patternfly/react-core'; +import { CheckCircleIcon, ExclamationCircleIcon, SearchIcon } from '@patternfly/react-icons'; +import { useTheme } from '@/context/ThemeContext'; +import { Model, Branch, Job } from '@/components/Experimental/FineTuning/types'; +import AddFineTuningJobModal from '@/components/Experimental/FineTuning/AddFineTuningJobModal'; + +const FineTuning: React.FC = () => { + const { theme } = useTheme(); + const [models, setModels] = useState([]); + const [branches, setBranches] = useState([]); + const [isLoading, setIsLoading] = useState(true); + const [errorMessage, setErrorMessage] = useState(''); + + const [isModalOpen, setIsModalOpen] = useState(false); + + const [jobs, setJobs] = useState([]); + const [selectedStatus, setSelectedStatus] = useState('all'); // 'successful', 'pending', 'failed', 'all' + + // State for managing expanded jobs and their logs + const [expandedJobs, setExpandedJobs] = useState<{ [jobId: string]: boolean }>({}); + const [jobLogs, setJobLogs] = useState<{ [jobId: string]: string }>({}); + + // Ref to store intervals for each job's logs + const logsIntervals = useRef<{ [jobId: string]: NodeJS.Timeout }>({}); + + const mapJobType = (job: Job) => { + let jobType: 'generate' | 'train' | 'pipeline' | 'model-serve' | 'vllm-run'; + + if (job.job_id.startsWith('g-')) { + jobType = 'generate'; + } else if (job.job_id.startsWith('p-')) { + jobType = 'pipeline'; + } else if (job.job_id.startsWith('ml-')) { + jobType = 'model-serve'; + } else if (job.job_id.startsWith('v-')) { + jobType = 'vllm-run'; // New categorization for 'v-' jobs + } else { + jobType = 'train'; + } + + return { ...job, type: jobType }; + }; + + // Fetch models, branches, and jobs when the component mounts + useEffect(() => { + let canceled = false; + let refreshIntervalId: NodeJS.Timeout; + + const fetchJobs = async () => { + console.debug('Fetching jobs from /api/fine-tune/jobs'); + + try { + const response = await fetch('/api/fine-tune/jobs', { cache: 'no-cache' }); + console.debug(`Fetch response status: ${response.status}`); + if (!response.ok) { + const errorText = await response.text(); + console.error(`Failed to fetch jobs: ${response.status} ${errorText}`); + return; + } + + const data = await response.json(); + console.debug('Polling: Jobs data fetched successfully:', data); + + const safeJobsData = Array.isArray(data) ? data : []; + const updatedJobs = safeJobsData + .map((job: Job) => mapJobType(job)) + .sort((a, b) => new Date(b.start_time).getTime() - new Date(a.start_time).getTime()); + + if (!canceled) { + setJobs(updatedJobs); + } + } catch (error) { + console.error('Error fetching jobs during polling:', error); + } + }; + + const fetchData = async () => { + try { + // Fetch models + const modelsResponse = await fetch('/api/fine-tune/models', { cache: 'no-cache' }); + if (canceled) { + return; + } + console.log(modelsResponse); + if (!modelsResponse.ok) { + throw new Error('Failed to fetch models'); + } + const modelsData = await modelsResponse.json(); + + // Fetch branches + const branchesResponse = await fetch('/api/fine-tune/git/branches', { cache: 'no-cache' }); + if (canceled) { + return; + } + if (!branchesResponse.ok) { + throw new Error('Failed to fetch git branches'); + } + const branchesData = await branchesResponse.json(); + + // Fetch jobs + await fetchJobs(); + if (!canceled) { + setModels(modelsData); + setBranches(branchesData.branches); + } + } catch (error) { + console.error('Error fetching data:', error); + setErrorMessage('Error fetching data'); + } finally { + setIsLoading(false); + } + }; + + fetchData().then(() => { + if (!canceled) { + refreshIntervalId = setInterval(fetchJobs, 30000); + } + }); + + return () => { + canceled = true; + clearInterval(refreshIntervalId); + }; + }, []); + + // Clean up all intervals on component unmount + useEffect(() => { + return () => { + // eslint-disable-next-line react-hooks/exhaustive-deps + Object.values(logsIntervals.current).forEach(clearInterval); + }; + }, []); + + const formatDate = (isoDate?: string) => { + if (!isoDate) return 'N/A'; + const date = new Date(isoDate); + return format(date, 'PPpp'); + }; + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const handleToggleChange = (event: React.MouseEvent | React.KeyboardEvent | MouseEvent, _selected: boolean) => { + const id = (event.currentTarget as HTMLButtonElement).id; + setSelectedStatus(id); + }; + + const filteredJobs = jobs.filter((job) => { + if (job.job_id.startsWith('ml-') || job.job_id.startsWith('v-')) { + return false; // Exclude model serve and vllm prefixed jobs from the dashboard list + } + if (selectedStatus === 'successful') return job.status === 'finished'; + if (selectedStatus === 'pending') return job.status === 'running'; + if (selectedStatus === 'failed') return job.status === 'failed'; + return true; // 'all' + }); + + const handleCreateButtonClick = () => { + setIsModalOpen(true); + }; + + const handleModalClose = (newJob?: Job) => { + if (newJob) { + setJobs((prev) => [...prev, newJob]); + } + setIsModalOpen(false); + }; + + const fetchJobLogs = async (jobId: string) => { + try { + const response = await fetch(`/api/fine-tune/jobs/${jobId}/logs`, { + headers: { + 'Cache-Control': 'no-cache' + }, + cache: 'no-cache' + }); + + if (response.ok) { + const logsText = await response.text(); + setJobLogs((prev) => ({ ...prev, [jobId]: logsText })); + } else { + const errorText = await response.text(); + setJobLogs((prev) => ({ ...prev, [jobId]: `Failed to fetch logs: ${response.status} ${errorText}` })); + console.warn(`Failed to fetch logs for job ${jobId}: ${response.status} ${errorText}`); + } + } catch (error) { + console.error(`Error fetching job logs for job ${jobId}:`, error); + setJobLogs((prev) => ({ ...prev, [jobId]: 'Error fetching logs.' })); + } + }; + + const handleToggleLogs = (jobId: string, isExpanding: boolean) => { + console.debug(`Toggling logs for job ID: ${jobId}. Expanding: ${isExpanding}`); + setExpandedJobs((prev) => ({ ...prev, [jobId]: isExpanding })); + + if (isExpanding) { + // Fetch logs immediately + fetchJobLogs(jobId); + + // Set up interval to fetch logs every 10 seconds + const intervalId = setInterval(() => { + fetchJobLogs(jobId); + }, 10000); + + // Store the interval ID + logsIntervals.current[jobId] = intervalId; + } else { + // Clear the interval if it exists + if (logsIntervals.current[jobId]) { + clearInterval(logsIntervals.current[jobId]); + delete logsIntervals.current[jobId]; + } + } + }; + + return ( + <> + + + + + Fine tuning jobs + + + {jobs.length > 0 ? ( + + + + ) : null} + + + + + {isLoading ? ( + + + + ) : ( + <> + {errorMessage ? : null} + {!jobs.length ? ( + ( + No documents + )} + titleText="No fine tuning jobs" + variant={EmptyStateVariant.lg} + > + You have not created any fine-tuning jobs yet. Use the Create+ button to get started. + + + + + + + ) : ( + + + + + + + + + + {filteredJobs.length > 0 ? ( + filteredJobs.map((job) => { + const isExpanded = expandedJobs[job.job_id] || false; + const logs = jobLogs[job.job_id]; + return ( + + + + {/* TODO: fix the status icons to have color, e.g. red/green */} + {job.status === 'finished' ? ( + + ) : job.status === 'failed' ? ( + + ) : null} + {job.type === 'generate' + ? 'Generate Job' + : job.type === 'pipeline' + ? 'Generate & Train Pipeline' + : job.type === 'model-serve' + ? 'Model Serve Job' + : 'Train Job'} + + + {/* If fields are added, the percentages need to be tweaked to keep columns lined up across cards. */} +
+
+ Job ID: {job.job_id} +
+
+ Status: {job.status} +
+
+ Start Time: {formatDate(job.start_time)} +
+
+ End Time: {formatDate(job.end_time)} +
+
+
+ + {/* Expandable section for logs */} + handleToggleLogs(job.job_id, expanded)} + isExpanded={isExpanded} + > + {logs ? ( + + {logs} + + ) : ( + + )} + + +
+
+ ); + }) + ) : ( + + + No matching fine tuning jobs found + + + + + + )} +
+ )} + + )} +
+ {isModalOpen ? : null} + + ); +}; + +export default FineTuning; diff --git a/src/components/Experimental/FineTuning/index.tsx b/src/components/Experimental/FineTuning/index.tsx deleted file mode 100644 index 647fed01..00000000 --- a/src/components/Experimental/FineTuning/index.tsx +++ /dev/null @@ -1,625 +0,0 @@ -// src/components/Experimental/FineTuning/index.tsx -'use client'; - -import React, { useState, useEffect, useRef } from 'react'; -import Image from 'next/image'; -import { format } from 'date-fns'; -import { - ToggleGroupItem, - ToggleGroup, - Flex, - FlexItem, - Bullseye, - EmptyStateVariant, - Modal, - Form, - FormGroup, - Dropdown, - DropdownItem, - DropdownList, - ExpandableSection, - CodeBlock, - CodeBlockCode, - MenuToggle, - MenuToggleElement, - PageSection, - Card, - CardTitle, - CardBody, - CardFooter, - NumberInput, - Title, - Button, - EmptyState, - EmptyStateBody, - Spinner, - Alert, - EmptyStateFooter, - EmptyStateActions -} from '@patternfly/react-core'; -import { CheckCircleIcon, ExclamationCircleIcon, SearchIcon } from '@patternfly/react-icons'; -import { useTheme } from '@/context/ThemeContext'; - -interface Model { - name: string; - last_modified: string; - size: string; -} - -interface Branch { - name: string; - creationDate: number; -} - -interface Job { - job_id: string; - status: string; - type?: 'generate' | 'train' | 'pipeline' | 'model-serve' | 'vllm-run'; - branch?: string; - start_time: string; // ISO timestamp - end_time?: string; -} - -const FineTuning: React.FC = () => { - const { theme } = useTheme(); - const [models, setModels] = useState([]); - const [branches, setBranches] = useState([]); - const [isLoading, setIsLoading] = useState(true); - const [errorMessage, setErrorMessage] = useState(''); - - const [selectedModel, setSelectedModel] = useState(''); - const [selectedBranch, setSelectedBranch] = useState(''); - const [selectedEpochs, setSelectedEpochs] = useState(''); - - const [isModalOpen, setIsModalOpen] = useState(false); - const [isModelDropdownOpen, setIsModelDropdownOpen] = useState(false); - const [isBranchDropdownOpen, setIsBranchDropdownOpen] = useState(false); - - const [jobs, setJobs] = useState([]); - const [selectedStatus, setSelectedStatus] = useState('all'); // 'successful', 'pending', 'failed', 'all' - - // State for managing expanded jobs and their logs - const [expandedJobs, setExpandedJobs] = useState<{ [jobId: string]: boolean }>({}); - const [jobLogs, setJobLogs] = useState<{ [jobId: string]: string }>({}); - - // Ref to store intervals for each job's logs - const logsIntervals = useRef<{ [jobId: string]: NodeJS.Timeout }>({}); - - const mapJobType = (job: Job) => { - let jobType: 'generate' | 'train' | 'pipeline' | 'model-serve' | 'vllm-run'; - - if (job.job_id.startsWith('g-')) { - jobType = 'generate'; - } else if (job.job_id.startsWith('p-')) { - jobType = 'pipeline'; - } else if (job.job_id.startsWith('ml-')) { - jobType = 'model-serve'; - } else if (job.job_id.startsWith('v-')) { - jobType = 'vllm-run'; // New categorization for 'v-' jobs - } else { - jobType = 'train'; - } - - return { ...job, type: jobType }; - }; - - // Fetch models, branches, and jobs when the component mounts - useEffect(() => { - let canceled = false; - let refreshIntervalId: NodeJS.Timeout; - - const fetchJobs = async () => { - console.debug('Fetching jobs from /api/fine-tune/jobs'); - - try { - const response = await fetch('/api/fine-tune/jobs', { cache: 'no-cache' }); - console.debug(`Fetch response status: ${response.status}`); - if (!response.ok) { - const errorText = await response.text(); - console.error(`Failed to fetch jobs: ${response.status} ${errorText}`); - return; - } - - const data = await response.json(); - console.debug('Polling: Jobs data fetched successfully:', data); - - const safeJobsData = Array.isArray(data) ? data : []; - const updatedJobs = safeJobsData - .map((job: Job) => mapJobType(job)) - .sort((a, b) => new Date(b.start_time).getTime() - new Date(a.start_time).getTime()); - - if (!canceled) { - setJobs(updatedJobs); - } - } catch (error) { - console.error('Error fetching jobs during polling:', error); - } - }; - - const fetchData = async () => { - try { - // Fetch models - const modelsResponse = await fetch('/api/fine-tune/models', { cache: 'no-cache' }); - if (canceled) { - return; - } - if (!modelsResponse.ok) { - throw new Error('Failed to fetch models'); - } - const modelsData = await modelsResponse.json(); - - // Fetch branches - const branchesResponse = await fetch('/api/fine-tune/git/branches', { cache: 'no-cache' }); - if (canceled) { - return; - } - if (!branchesResponse.ok) { - throw new Error('Failed to fetch git branches'); - } - const branchesData = await branchesResponse.json(); - - // Fetch jobs - await fetchJobs(); - if (!canceled) { - setModels(modelsData); - setBranches(branchesData.branches); - } - } catch (error) { - console.error('Error fetching data:', error); - setErrorMessage('Error fetching data'); - } finally { - setIsLoading(false); - } - }; - - fetchData().then(() => { - if (!canceled) { - refreshIntervalId = setInterval(fetchJobs, 10000); - } - }); - - return () => { - canceled = true; - clearInterval(refreshIntervalId); - }; - }, []); - - // Clean up all intervals on component unmount - useEffect(() => { - return () => { - // eslint-disable-next-line react-hooks/exhaustive-deps - Object.values(logsIntervals.current).forEach(clearInterval); - }; - }, []); - - const formatDate = (isoDate?: string) => { - if (!isoDate) return 'N/A'; - const date = new Date(isoDate); - return format(date, 'PPpp'); - }; - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const handleToggleChange = (event: React.MouseEvent | React.KeyboardEvent | MouseEvent, _selected: boolean) => { - const id = (event.currentTarget as HTMLButtonElement).id; - setSelectedStatus(id); - }; - - const filteredJobs = jobs.filter((job) => { - if (job.job_id.startsWith('ml-') || job.job_id.startsWith('v-')) { - return false; // Exclude model serve and vllm prefixed jobs from the dashboard list - } - if (selectedStatus === 'successful') return job.status === 'finished'; - if (selectedStatus === 'pending') return job.status === 'running'; - if (selectedStatus === 'failed') return job.status === 'failed'; - return true; // 'all' - }); - - const handleCreateButtonClick = () => { - setIsModalOpen(true); - }; - - const handleModalClose = () => { - setIsModalOpen(false); - setErrorMessage(''); - setSelectedBranch(''); - setSelectedModel(''); - setSelectedEpochs(10); - }; - - const handleGenerateClick = async () => { - if (!selectedModel || !selectedBranch) { - setErrorMessage('Please select both a model and a branch.'); - return; - } - if (selectedEpochs === '') { - setErrorMessage('Please enter the number of epochs.'); - return; - } - setIsModalOpen(false); - try { - const response = await fetch('/api/fine-tune/data/generate', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ modelName: selectedModel, branchName: selectedBranch, epochs: selectedEpochs }), // Include epochs - cache: 'no-cache' - }); - const result = await response.json(); - if (response.ok) { - const newJob: Job = { - job_id: result.job_id, - status: 'running', - type: result.job_id.startsWith('g-') ? 'generate' : result.job_id.startsWith('p-') ? 'pipeline' : 'train', - start_time: new Date().toISOString() - }; - setJobs((prevJobs) => [...prevJobs, newJob]); - } else { - setErrorMessage(result.error || 'Failed to start generate job'); - } - } catch (error) { - console.error('Error starting generate job:', error); - setErrorMessage('Error starting generate job'); - } - }; - - const handleTrainClick = async () => { - if (!selectedModel || !selectedBranch) { - setErrorMessage('Please select both a model and a branch.'); - return; - } - if (selectedEpochs === '') { - setErrorMessage('Please enter the number of epochs.'); - return; - } - setIsModalOpen(false); - try { - console.log('Sending train request with:', { - modelName: selectedModel, - branchName: selectedBranch, - epochs: selectedEpochs - }); - - const response = await fetch('/api/fine-tune/model/train', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - modelName: selectedModel, - branchName: selectedBranch, - epochs: selectedEpochs - }), - cache: 'no-cache' - }); - const result = await response.json(); - if (response.ok) { - const newJob: Job = { - job_id: result.job_id, - status: 'running', - type: result.job_id.startsWith('g-') ? 'generate' : result.job_id.startsWith('p-') ? 'pipeline' : 'train', - start_time: new Date().toISOString() - }; - setJobs((prevJobs) => [...prevJobs, newJob]); - } else { - setErrorMessage(result.error || 'Failed to start train job'); - } - } catch (error) { - console.error('Error starting train job:', error); - setErrorMessage('Error starting train job'); - } - }; - - const handlePipelineClick = async () => { - if (!selectedModel || !selectedBranch) { - setErrorMessage('Please select both a model and a branch.'); - return; - } - if (selectedEpochs === '') { - setErrorMessage('Please enter the number of epochs.'); - return; - } - setIsModalOpen(false); - try { - console.debug('Sending pipeline generate-train request with:', { - modelName: selectedModel, - branchName: selectedBranch, - epochs: selectedEpochs - }); - const response = await fetch('/api/fine-tune/pipeline/generate-train', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ modelName: selectedModel, branchName: selectedBranch, epochs: selectedEpochs }), // Include epochs - cache: 'no-cache' - }); - const result = await response.json(); - if (response.ok && result.pipeline_job_id) { - // Add the new job to the job list - const newJob: Job = { - job_id: result.pipeline_job_id, - status: 'running', - type: 'pipeline', - branch: selectedBranch, - start_time: new Date().toISOString() - }; - setJobs((prevJobs) => [...prevJobs, newJob]); - console.debug('New pipeline job added:', newJob); - } else { - setErrorMessage(result.error || 'Failed to start generate-train pipeline'); - console.warn('Pipeline action failed:', result.error); - } - } catch (error) { - console.error('Error starting generate-train pipeline job:', error); - setErrorMessage('Error starting generate-train pipeline job'); - } - }; - - const fetchJobLogs = async (jobId: string) => { - try { - const response = await fetch(`/api/fine-tune/jobs/${jobId}/logs`, { - headers: { - 'Cache-Control': 'no-cache' - }, - cache: 'no-cache' - }); - - if (response.ok) { - const logsText = await response.text(); - setJobLogs((prev) => ({ ...prev, [jobId]: logsText })); - } else { - const errorText = await response.text(); - setJobLogs((prev) => ({ ...prev, [jobId]: `Failed to fetch logs: ${response.status} ${errorText}` })); - console.warn(`Failed to fetch logs for job ${jobId}: ${response.status} ${errorText}`); - } - } catch (error) { - console.error(`Error fetching job logs for job ${jobId}:`, error); - setJobLogs((prev) => ({ ...prev, [jobId]: 'Error fetching logs.' })); - } - }; - - const handleToggleLogs = (jobId: string, isExpanding: boolean) => { - console.debug(`Toggling logs for job ID: ${jobId}. Expanding: ${isExpanding}`); - setExpandedJobs((prev) => ({ ...prev, [jobId]: isExpanding })); - - if (isExpanding) { - // Fetch logs immediately - fetchJobLogs(jobId); - - // Set up interval to fetch logs every 10 seconds - const intervalId = setInterval(() => { - fetchJobLogs(jobId); - }, 10000); - - // Store the interval ID - logsIntervals.current[jobId] = intervalId; - } else { - // Clear the interval if it exists - if (logsIntervals.current[jobId]) { - clearInterval(logsIntervals.current[jobId]); - delete logsIntervals.current[jobId]; - } - } - }; - - return ( - <> - - - - - Fine tuning jobs - - - {jobs.length > 0 ? ( - - - - ) : null} - - - - - {isLoading ? ( - - - - ) : !jobs.length ? ( - ( - No documents - )} - titleText="No fine tuning jobs" - variant={EmptyStateVariant.lg} - > - You have not created any fine-tuning jobs yet. Use the Create+ button to get started. - - - - - - - ) : ( - - - - - - - - - - {filteredJobs.length > 0 ? ( - filteredJobs.map((job) => { - const isExpanded = expandedJobs[job.job_id] || false; - const logs = jobLogs[job.job_id]; - return ( - - - - {/* TODO: fix the status icons to have color, e.g. red/green */} - {job.status === 'finished' ? ( - - ) : job.status === 'failed' ? ( - - ) : null} - {job.type === 'generate' - ? 'Generate Job' - : job.type === 'pipeline' - ? 'Generate & Train Pipeline' - : job.type === 'model-serve' - ? 'Model Serve Job' - : 'Train Job'} - - - {/* If fields are added, the percentages need to be tweaked to keep columns lined up across cards. */} -
-
- Job ID: {job.job_id} -
-
- Status: {job.status} -
-
- Start Time: {formatDate(job.start_time)} -
-
- End Time: {formatDate(job.end_time)} -
-
-
- - {/* Expandable section for logs */} - handleToggleLogs(job.job_id, expanded)} - isExpanded={isExpanded} - > - {logs ? ( - - {logs} - - ) : ( - - )} - - -
-
- ); - }) - ) : ( - - - No matching fine tuning jobs found - - - - - - )} -
- )} -
- - - {errorMessage && } -
- - { - setSelectedBranch(value as string); - setIsBranchDropdownOpen(false); - }} - onOpenChange={(isOpen) => setIsBranchDropdownOpen(isOpen)} - toggle={(toggleRef: React.Ref) => ( - setIsBranchDropdownOpen(!isBranchDropdownOpen)} isExpanded={isBranchDropdownOpen}> - {selectedBranch || 'Select a branch'} - - )} - > - - {branches.map((branch) => ( - - {branch.name} - - ))} - - - - - - { - setSelectedModel(value as string); - setIsModelDropdownOpen(false); - }} - onOpenChange={(isOpen) => setIsModelDropdownOpen(isOpen)} - toggle={(toggleRef: React.Ref) => ( - setIsModelDropdownOpen(!isModelDropdownOpen)} isExpanded={isModelDropdownOpen}> - {selectedModel || 'Select a model'} - - )} - > - - {models.map((model) => ( - - {model.name} - - ))} - - - - - {/* New FormGroup for Epoch Selection using NumberInput */} - - { - const newValue = typeof selectedEpochs === 'number' ? selectedEpochs - 1 : 0; - setSelectedEpochs(newValue >= 1 ? newValue : 1); // Ensure minimum of 1 - }} - onChange={(event: React.FormEvent) => { - const value = (event.target as HTMLInputElement).value; - const parsedValue = value === '' ? '' : Number(value); - if (parsedValue === '' || (Number.isInteger(parsedValue) && parsedValue > 0)) { - setSelectedEpochs(parsedValue); - } - }} - onPlus={() => { - const newValue = typeof selectedEpochs === 'number' ? selectedEpochs + 1 : 1; - setSelectedEpochs(newValue); - }} - inputName="epochs" - inputAriaLabel="Number of Epochs" - minusBtnAriaLabel="decrease number of epochs" - plusBtnAriaLabel="increase number of epochs" - min={1} - /> - - -
- - - -
- -
-
- - ); -}; - -export default FineTuning; diff --git a/src/components/Experimental/FineTuning/types.ts b/src/components/Experimental/FineTuning/types.ts new file mode 100644 index 00000000..aa2cb14f --- /dev/null +++ b/src/components/Experimental/FineTuning/types.ts @@ -0,0 +1,19 @@ +export interface Model { + name: string; + last_modified: string; + size: string; +} + +export interface Branch { + name: string; + creationDate: number; +} + +export interface Job { + job_id: string; + status: string; + type?: 'generate' | 'train' | 'pipeline' | 'model-serve' | 'vllm-run'; + branch?: string; + start_time: string; // ISO timestamp + end_time?: string; +} diff --git a/src/styles/globals.scss b/src/styles/globals.scss index efc90eb2..65ad9b47 100644 --- a/src/styles/globals.scss +++ b/src/styles/globals.scss @@ -3,17 +3,11 @@ @import '@patternfly/chatbot/dist/css/main.css'; @import 'chatbot.scss'; -.square-button { - border-radius: 6px !important; -} - -/* Custom class to make PatternFly Labels square */ -.square-label.pf-c-label { - border-radius: 0 !important; -} - .pf-v6-c-menu__list-item.destructive-action-item:not(.pf-m-disabled) { .pf-v6-c-menu__item-text { color: var(--pf-t--global--color--status--danger--100); + .pf-v6-theme-dark & { + color: var(--pf-t--color--red-orange--30); + } } }