diff --git a/src/renderer/components/Settings/TransformerLabSettings.tsx b/src/renderer/components/Settings/TransformerLabSettings.tsx index e845ec3c9ade830810a748cf767f4a426f56c2b4..7d281b3c074fccad5ef77882ba585c83fe842e04 100644 --- a/src/renderer/components/Settings/TransformerLabSettings.tsx +++ b/src/renderer/components/Settings/TransformerLabSettings.tsx @@ -15,6 +15,10 @@ import { Table, Typography, Alert, + Tabs, + TabList, + Tab, + TabPanel, } from '@mui/joy'; import * as chatAPI from 'renderer/lib/transformerlab-api-sdk'; @@ -23,6 +27,8 @@ import { EyeIcon, EyeOffIcon, RotateCcwIcon } from 'lucide-react'; // Import the AIProvidersSettings component. import AIProvidersSettings from './AIProvidersSettings'; +import ViewJobsTab from './ViewJobsTab'; +import { alignBox } from '@nivo/core'; const fetcher = (url) => fetch(url).then((res) => res.json()); @@ -35,7 +41,7 @@ export default function TransformerLabSettings() { mutate: hftokenmutate, } = useSWR( chatAPI.Endpoints.Config.Get('HuggingfaceUserAccessToken'), - fetcher + fetcher, ); const [showJobsOfType, setShowJobsOfType] = React.useState('NONE'); const [showProvidersPage, setShowProvidersPage] = React.useState(false); @@ -71,171 +77,174 @@ export default function TransformerLabSettings() { ); } - - return ( - <> - <Typography level="h1" marginBottom={3}> + <Sheet + sx={{ + width: '100%', + height: '100%', + overflowY: 'hidden', + display: 'flex', + flexDirection: 'column', + }} + > + <Typography level="h1" marginBottom={1}> Transformer Lab Settings </Typography> - <Sheet sx={{ width: '100%', overflowY: 'auto' }}> - {canLogInToHuggingFaceIsLoading && <CircularProgress />} - <Typography level="title-lg" marginBottom={2}> - Huggingface Credentials: - </Typography> - {canLogInToHuggingFace?.message === 'OK' ? ( - <Alert color="success">Login to Huggingface Successful</Alert> - ) : ( - <> - <Alert color="danger" sx={{ mb: 1 }}> - Login to Huggingface Failed. Please set credentials below. - </Alert> - <FormControl sx={{ maxWidth: '500px' }}> - <FormLabel>User Access Token</FormLabel> - {hftokenisloading ? ( - <CircularProgress /> - ) : ( - <Input - name="hftoken" - defaultValue={hftoken} - type="password" - endDecorator={ - <IconButton - onClick={() => { - const x = document.getElementsByName('hftoken')[0]; - x.type = x.type === 'text' ? 'password' : 'text'; - setShowPassword(!showPassword); - }} + <Sheet + sx={{ + height: '100%', + overflowY: 'hidden', + display: 'flex', + flexDirection: 'column', + }} + > + <Tabs + defaultValue={0} + sx={{ + height: '100%', + display: 'flex', + flexDirection: 'column', + overflow: 'hidden', + }} + > + <TabList> + <Tab>Settings</Tab> + <Tab>View Jobs</Tab> + </TabList> + <TabPanel value={0} style={{ overflow: 'auto' }}> + {canLogInToHuggingFaceIsLoading && <CircularProgress />} + <Typography level="title-lg" marginBottom={2}> + Huggingface Credentials: + </Typography> + {canLogInToHuggingFace?.message === 'OK' ? ( + <Alert color="success">Login to Huggingface Successful</Alert> + ) : ( + <> + <Alert color="danger" sx={{ mb: 1 }}> + Login to Huggingface Failed. Please set credentials below. + </Alert> + <FormControl sx={{ maxWidth: '500px' }}> + <FormLabel>User Access Token</FormLabel> + {hftokenisloading ? ( + <CircularProgress /> + ) : ( + <Input + name="hftoken" + defaultValue={hftoken} + type="password" + endDecorator={ + <IconButton + onClick={() => { + const x = document.getElementsByName('hftoken')[0]; + x.type = x.type === 'text' ? 'password' : 'text'; + setShowPassword(!showPassword); + }} + > + {showPassword ? <EyeOffIcon /> : <EyeIcon />} + </IconButton> + } + /> + )} + <Button + onClick={async () => { + const token = + document.getElementsByName('hftoken')[0].value; + await fetch( + chatAPI.Endpoints.Config.Set( + 'HuggingfaceUserAccessToken', + token, + ), + ); + // Now manually log in to Huggingface + await fetch(chatAPI.Endpoints.Models.HuggingFaceLogin()); + hftokenmutate(token); + canLogInToHuggingFaceMutate(); + }} + sx={{ marginTop: 1, width: '100px', alignSelf: 'flex-end' }} + > + Save + </Button> + <FormHelperText> + A Huggingface access token is required in order to access + certain models and datasets (those marked as "Gated"). + </FormHelperText> + <FormHelperText> + Documentation here:{' '} + <a + href="https://huggingface.co/docs/hub/security-tokens" + target="_blank" + rel="noreferrer" > - {showPassword ? <EyeOffIcon /> : <EyeIcon />} - </IconButton> - } - /> - )} - <Button - onClick={async () => { - const token = document.getElementsByName('hftoken')[0].value; - await fetch(chatAPI.Endpoints.Config.Set('HuggingfaceUserAccessToken', token)); - // Now manually log in to Huggingface - await fetch(chatAPI.Endpoints.Models.HuggingFaceLogin()); - hftokenmutate(token); - canLogInToHuggingFaceMutate(); - }} - sx={{ marginTop: 1, width: '100px', alignSelf: 'flex-end' }} - > - Save - </Button> - <FormHelperText> - A Huggingface access token is required in order to access certain - models and datasets (those marked as "Gated"). - </FormHelperText> - <FormHelperText> - Documentation here:{' '} - <a - href="https://huggingface.co/docs/hub/security-tokens" - target="_blank" - rel="noreferrer" + https://huggingface.co/docs/hub/security-tokens + </a> + </FormHelperText> + </FormControl> + </> + )} + {wandbLoginStatus?.message === 'OK' ? ( + <Alert color="success"> + Login to Weights & Biases Successful + </Alert> + ) : ( + <FormControl sx={{ maxWidth: '500px', mt: 2 }}> + <FormLabel>Weights & Biases API Key</FormLabel> + <Input name="wandbToken" type="password" /> + <Button + onClick={async () => { + const token = + document.getElementsByName('wandbToken')[0].value; + await fetch( + chatAPI.Endpoints.Config.Set('WANDB_API_KEY', token), + ); + await fetch(chatAPI.Endpoints.Models.wandbLogin()); + wandbLoginMutate(); + }} + sx={{ marginTop: 1, width: '100px', alignSelf: 'flex-end' }} > - https://huggingface.co/docs/hub/security-tokens - </a> - </FormHelperText> - </FormControl> - </> - )} - {wandbLoginStatus?.message === 'OK' ? ( - <Alert color="success">Login to Weights & Biases Successful</Alert> - ) : ( - <FormControl sx={{ maxWidth: '500px', mt: 2 }}> - <FormLabel>Weights & Biases API Key</FormLabel> - <Input name="wandbToken" type="password" /> + Save + </Button> + </FormControl> + )} + <Divider sx={{ mt: 2, mb: 2 }} /> + <Typography level="title-lg" marginBottom={2}> + AI Providers & Models: + </Typography> + {/* Clickable list option */} + <Button variant="soft" onClick={() => setShowProvidersPage(true)}> + Set API Keys for AI Providers + </Button> + + <Divider sx={{ mt: 2, mb: 2 }} /> + <Typography level="title-lg" marginBottom={2}> + Application: + </Typography> <Button - onClick={async () => { - const token = document.getElementsByName('wandbToken')[0].value; - await fetch(chatAPI.Endpoints.Config.Set('WANDB_API_KEY', token)); - await fetch(chatAPI.Endpoints.Models.wandbLogin()); - wandbLoginMutate(); + variant="soft" + onClick={() => { + // find and delete all items in local storage that begin with oneTimePopup: + for (const key in localStorage) { + if (key.startsWith('oneTimePopup')) { + localStorage.removeItem(key); + } + } }} - sx={{ marginTop: 1, width: '100px', alignSelf: 'flex-end' }} > - Save + Reset all Tutorial Popup Screens </Button> - </FormControl> - )} - <Divider sx={{ mt: 2, mb: 2 }} /> - <Typography level="title-lg" marginBottom={2}> - AI Providers & Models: - </Typography> - {/* Clickable list option */} - <Button variant="soft" onClick={() => setShowProvidersPage(true)}> - Set API Keys for AI Providers - </Button> - - <Divider sx={{ mt: 2, mb: 2 }} /> - <Typography level="title-lg" marginBottom={2}> - Application: - </Typography> - <Button - variant="soft" - onClick={() => { - // find and delete all items in local storage that begin with oneTimePopup: - for (const key in localStorage) { - if (key.startsWith('oneTimePopup')) { - localStorage.removeItem(key); - } - } - }} - > - Reset all Tutorial Popup Screens - </Button> - <Divider sx={{ mt: 2, mb: 2 }} /> - <Typography level="title-lg" marginBottom={2}> - View Jobs (debug):{' '} - <IconButton onClick={() => jobsMutate()}> - <RotateCcwIcon size="14px" /> - </IconButton> - </Typography> - <Select - sx={{ width: '400px' }} - value={showJobsOfType} - onChange={(e, newValue) => { - setShowJobsOfType(newValue); - }} - > - <Option value="NONE">None</Option> - <Option value="">All</Option> - <Option value="DOWNLOAD_MODEL">Download Model</Option> - <Option value="LOAD_MODEL">Load Model</Option> - <Option value="TRAIN">Train</Option> - <Option value="GENERATE">Generate</Option> - <Option value="EVAL">Evaluate</Option> - </Select> - {showJobsOfType !== 'NONE' && ( - <Table sx={{ tableLayout: 'auto', overflow: 'scroll' }}> - <thead> - <tr> - <td>Job ID</td> - <td>Job Type</td> - <td>Job Status</td> - <td>Job Progress</td> - <td>Job Data</td> - </tr> - </thead> - <tbody> - {jobs?.map((job) => ( - <tr key={job.id}> - <td>{job.id}</td> - <td>{job.type}</td> - <td>{job.status}</td> - <td>{job.progress}</td> - <td> - <pre>{JSON.stringify(job.job_data, null, 2)}</pre> - </td> - </tr> - ))} - </tbody> - </Table> - )} + </TabPanel> + <TabPanel + value={1} + sx={{ + overflowY: 'hidden', + overflowX: 'hidden', + display: 'flex', + flexDirection: 'column', + }} + > + <ViewJobsTab /> + </TabPanel> + </Tabs> </Sheet> - </> + </Sheet> ); } diff --git a/src/renderer/components/Settings/ViewJobsTab.tsx b/src/renderer/components/Settings/ViewJobsTab.tsx new file mode 100644 index 0000000000000000000000000000000000000000..056cc99ad191c0f1efaa10d7de6e0f98d6f88b42 --- /dev/null +++ b/src/renderer/components/Settings/ViewJobsTab.tsx @@ -0,0 +1,110 @@ +import * as React from 'react'; +import { + Typography, + IconButton, + Select, + Option, + Table, + Box, + Stack, +} from '@mui/joy'; +import { RotateCcwIcon } from 'lucide-react'; +import useSWR from 'swr'; +import * as chatAPI from 'renderer/lib/transformerlab-api-sdk'; + +const fetcher = (url) => fetch(url).then((res) => res.json()); + +const jobTypes = [ + { value: 'NONE', label: 'None' }, + { value: '', label: 'All' }, + { value: 'DOWNLOAD_MODEL', label: 'Download Model' }, + { value: 'LOAD_MODEL', label: 'Load Model' }, + { value: 'TRAIN', label: 'Train' }, + { value: 'GENERATE', label: 'Generate' }, + { value: 'EVAL', label: 'Evaluate' }, +]; + +export default function ViewJobsTab() { + const [type, setType] = React.useState(''); + + const { + data: jobs, + error: jobsError, + isLoading: jobsIsLoading, + mutate: jobsMutate, + } = useSWR(chatAPI.Endpoints.Jobs.GetJobsOfType(type, ''), fetcher); + + return ( + <> + <Stack flexDirection="row" alignItems="center" marginBottom={2}> + <Typography level="title-lg">View Jobs: </Typography> + <Select + sx={{ width: '200px' }} + value={type} + onChange={(e, newValue) => { + setType(newValue); + }} + > + {jobTypes.map((jobType) => ( + <Option key={jobType.value} value={jobType.value}> + {jobType.label} + </Option> + ))} + </Select> + <IconButton onClick={() => jobsMutate()}> + <RotateCcwIcon size="14px" /> + </IconButton> + </Stack> + <Box + style={{ display: 'flex', flexDirection: 'column', overflow: 'auto' }} + > + {type !== 'NONE' && ( + <Table + stickyHeader + sx={{ width: '100%', tableLayout: 'auto', overflow: 'scroll' }} + > + <thead> + <tr> + <th>Job ID</th> + <th>Job Type</th> + <th>Job Status</th> + <th>Job Progress</th> + <th style={{ overflow: 'hidden' }}>Job Data</th> + </tr> + </thead> + <tbody> + {jobs?.map((job) => ( + <tr key={job.id}> + <td>{job.id}</td> + <td>{job.type}</td> + <td>{job.status}</td> + <td>{job.progress}</td> + <td> + <pre + style={{ + maxHeight: '100px', + width: '500px', + overflowY: 'auto', + overflowX: 'hidden', + scrollbarWidth: 'thin', // For Firefox + msOverflowStyle: 'none', // For Internet Explorer and Edge + }} + > + {JSON.stringify(job.job_data, null, 2)} + </pre> + <style> + {` + pre::-webkit-scrollbar { + width: 4px; /* For Chrome, Safari, and Opera */ + }`} + </style> + </td> + </tr> + ))} + </tbody> + </Table> + )} + </Box> + </> + ); +}