Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ENH: [UI] Support unregistering custom model on web UI #735

Merged
merged 1 commit into from
Dec 8, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 58 additions & 9 deletions xinference/web/ui/src/scenes/launch_model/embeddingCard.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { RocketLaunchOutlined, UndoOutlined } from '@mui/icons-material'
import { Box, Chip, CircularProgress } from '@mui/material'
import DeleteIcon from '@mui/icons-material/Delete'
import { Box, Chip, CircularProgress, Stack } from '@mui/material'
import IconButton from '@mui/material/IconButton'
import React, { useContext, useEffect, useState } from 'react'
import { v1 as uuidv1 } from 'uuid'

Expand All @@ -8,9 +10,15 @@ import { ApiContext } from '../../components/apiContext'
const CARD_HEIGHT = 270
const CARD_WIDTH = 270

const EmbeddingCard = ({ url, modelData }) => {
const EmbeddingCard = ({
url,
modelData,
cardHeight = CARD_HEIGHT,
is_custom = false,
}) => {
const [hover, setHover] = useState(false)
const [selected, setSelected] = useState(false)
const [customDeleted, setCustomDeleted] = useState(false)
const { isCallingApi, setIsCallingApi } = useContext(ApiContext)
const { isUpdatingModel } = useContext(ApiContext)
const { setErrorMsg } = useContext(ApiContext)
Expand Down Expand Up @@ -67,7 +75,7 @@ const EmbeddingCard = ({ url, modelData }) => {
display: 'block',
position: 'relative',
width: `${CARD_WIDTH}px`,
height: `${CARD_HEIGHT}px`,
height: `${cardHeight}px`,
border: '1px solid #ddd',
borderRadius: '20px',
background: 'white',
Expand All @@ -77,7 +85,7 @@ const EmbeddingCard = ({ url, modelData }) => {
display: 'block',
position: 'relative',
width: `${CARD_WIDTH}px`,
height: `${CARD_HEIGHT}px`,
height: `${cardHeight}px`,
border: '1px solid #ddd',
borderRadius: '20px',
background: 'white',
Expand All @@ -89,18 +97,18 @@ const EmbeddingCard = ({ url, modelData }) => {
top: '-1px',
left: '-1px',
width: `${CARD_WIDTH}px`,
height: `${CARD_HEIGHT}px`,
height: `${cardHeight}px`,
border: '1px solid #ddd',
padding: '20px',
borderRadius: '20px',
background: 'white',
},
parameterCard: {
position: 'relative',
top: `-${CARD_HEIGHT + 1}px`,
top: `-${cardHeight + 1}px`,
left: '-1px',
width: `${CARD_WIDTH}px`,
height: `${CARD_HEIGHT}px`,
height: `${cardHeight}px`,
border: '1px solid #ddd',
padding: '20px',
borderRadius: '20px',
Expand Down Expand Up @@ -188,22 +196,51 @@ const EmbeddingCard = ({ url, modelData }) => {
},
}

const handeCustomDelete = (e) => {
e.stopPropagation()
fetch(url + `/v1/model_registrations/embedding/${modelData.model_name}`, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
},
})
.then(() => setCustomDeleted(true))
.catch(console.error)
}

// Set two different states based on mouse hover
return (
<Box
style={hover ? styles.containerSelected : styles.container}
onMouseEnter={() => setHover(true)}
onMouseLeave={() => setHover(false)}
onClick={() => {
if (!selected) {
if (!selected && !customDeleted) {
setSelected(true)
}
}}
>
{/* First state: show description page */}
<Box style={styles.descriptionCard}>
<div style={styles.titleContainer}>
<h2 style={styles.h2}>{modelData.model_name}</h2>
{is_custom && (
<Stack
direction="row"
justifyContent="space-evenly"
alignItems="center"
spacing={1}
>
<h2 style={styles.h2}>{modelData.model_name}</h2>
<IconButton
aria-label="delete"
onClick={handeCustomDelete}
disabled={customDeleted}
>
<DeleteIcon />
</IconButton>
</Stack>
)}
{!is_custom && <h2 style={styles.h2}>{modelData.model_name}</h2>}
<div style={styles.langRow}>
{(() => {
if (modelData.language.includes('en')) {
Expand All @@ -222,6 +259,18 @@ const EmbeddingCard = ({ url, modelData }) => {
return <Chip label="ZH" variant="outlined" size="small" />
}
})()}
{(() => {
if (is_custom && customDeleted) {
return (
<Chip
label="Deleted"
variant="outlined"
size="small"
sx={{ marginLeft: '10px' }}
/>
)
}
})()}
</div>
</div>
<div style={styles.iconRow}>
Expand Down
5 changes: 5 additions & 0 deletions xinference/web/ui/src/scenes/launch_model/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import React from 'react'

import ErrorMessageSnackBar from '../../components/errorMessageSnackBar'
import Title from '../../components/Title'
import LaunchCustom from './launchCustom'
import LaunchEmbedding from './launchEmbedding'
import LaunchLLM from './launchLLM'
import LaunchRerank from './launchRerank'
Expand All @@ -25,6 +26,7 @@ const LaunchModel = () => {
<Tab label="Language Models" value="1" />
<Tab label="Embedding Models" value="2" />
<Tab label="Rerank Models" value="3" />
<Tab label="Custom Models" value="4" />
</TabList>
</Box>
<TabPanel value="1" sx={{ padding: 0 }}>
Expand All @@ -36,6 +38,9 @@ const LaunchModel = () => {
<TabPanel value="3" sx={{ padding: 0 }}>
<LaunchRerank />
</TabPanel>
<TabPanel value="4" sx={{ padding: 0 }}>
<LaunchCustom />
</TabPanel>
</TabContext>
</Box>
)
Expand Down
160 changes: 160 additions & 0 deletions xinference/web/ui/src/scenes/launch_model/launchCustom.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
import { Box, FormControl, TextField } from '@mui/material'
import React, { useContext, useEffect, useState } from 'react'

import { ApiContext } from '../../components/apiContext'
import EmbeddingCard from './embeddingCard'
import ModelCard from './modelCard'

const LaunchCustom = () => {
let endPoint = useContext(ApiContext).endPoint
const [registrationData, setRegistrationData] = useState([])
const { isCallingApi, setIsCallingApi } = useContext(ApiContext)
const { isUpdatingModel } = useContext(ApiContext)

// States used for filtering
const [searchTerm, setSearchTerm] = useState('')

const handleChange = (event) => {
setSearchTerm(event.target.value)
}

const filter = (registration) => {
if (!registration || typeof searchTerm !== 'string') return false
const modelName = registration.model_name
? registration.model_name.toLowerCase()
: ''
return modelName.includes(searchTerm.toLowerCase())
}

const update = async () => {
if (isCallingApi || isUpdatingModel) return

try {
setIsCallingApi(true)

const embeddingResponse = await fetch(
`${endPoint}/v1/model_registrations/embedding`,
{
method: 'GET',
}
)

const embeddingRegistrations = await embeddingResponse.json()
const customEmbeddingRegistrations = embeddingRegistrations.filter(
(data) => !data.is_builtin
)

const llmResponse = await fetch(
`${endPoint}/v1/model_registrations/LLM`,
{
method: 'GET',
}
)
const llmRegistrations = await llmResponse.json()
const customLLMRegistrations = llmRegistrations.filter(
(data) => !data.is_builtin
)

const newEmbeddingData = await Promise.all(
customEmbeddingRegistrations.map(async (registration) => {
const desc = await fetch(
`${endPoint}/v1/model_registrations/embedding/${registration.model_name}`,
{
method: 'GET',
}
)

return {
...(await desc.json()),
is_builtin: registration.is_builtin,
}
})
)

const newLLMData = await Promise.all(
customLLMRegistrations.map(async (registration) => {
const desc = await fetch(
`${endPoint}/v1/model_registrations/LLM/${registration.model_name}`,
{
method: 'GET',
}
)

return {
...(await desc.json()),
is_builtin: registration.is_builtin,
}
})
)

setRegistrationData(newLLMData.concat(newEmbeddingData))
} catch (error) {
console.error('Error:', error)
} finally {
setIsCallingApi(false)
}
}

useEffect(() => {
update()
}, [])

const style = {
display: 'grid',
gridTemplateColumns: 'repeat(auto-fill, minmax(300px, 1fr))',
paddingLeft: '2rem',
gridGap: '2rem 0rem',
}

return (
<Box m="20px">
<div
style={{
display: 'grid',
gridTemplateColumns: '1fr',
margin: '30px 2rem',
}}
>
<FormControl variant="outlined" margin="normal">
<TextField
id="search"
type="search"
label="Search for custom model name"
value={searchTerm}
onChange={handleChange}
size="small"
/>
</FormControl>
</div>
<div style={style}>
{registrationData
.filter((registration) => filter(registration))
.map((filteredRegistration) => {
if (
filteredRegistration.max_tokens &&
filteredRegistration.dimensions
) {
return (
<EmbeddingCard
url={endPoint}
modelData={filteredRegistration}
cardHeight={350}
is_custom={true}
/>
)
} else {
return (
<ModelCard
url={endPoint}
modelData={filteredRegistration}
is_custom={true}
/>
)
}
})}
</div>
</Box>
)
}

export default LaunchCustom
5 changes: 4 additions & 1 deletion xinference/web/ui/src/scenes/launch_model/launchEmbedding.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,10 @@ const LaunchEmbedding = () => {
})
)

setRegistrationData(newRegistrationData)
const builtinRegistrations = newRegistrationData.filter(
(v) => v.is_builtin
)
setRegistrationData(builtinRegistrations)
} catch (error) {
console.error('Error:', error)
} finally {
Expand Down
3 changes: 2 additions & 1 deletion xinference/web/ui/src/scenes/launch_model/launchLLM.js
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,9 @@ const LaunchLLM = () => {
)

const registrations = await response.json()
const builtinRegistrations = registrations.filter((v) => v.is_builtin)

setRegistrationData(registrations)
setRegistrationData(builtinRegistrations)
} catch (error) {
console.error('Error:', error)
} finally {
Expand Down
Loading
Loading