Skip to content

Commit 285bb81

Browse files
committed
LocalAI: fix list and virtualize
1 parent d897155 commit 285bb81

File tree

2 files changed

+109
-40
lines changed

2 files changed

+109
-40
lines changed

src/modules/llms/vendors/localai/LocalAIAdmin.tsx

Lines changed: 106 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import * as React from 'react';
2+
import { useVirtualizer } from '@tanstack/react-virtual';
23

3-
import { Alert, Box, Button, Card, CircularProgress, IconButton, LinearProgress, List, ListItem, Switch, Typography } from '@mui/joy';
4+
import { Alert, Box, Button, Card, Chip, CircularProgress, IconButton, LinearProgress, List, ListItem, Sheet, Switch, Table, Typography } from '@mui/joy';
45
import CloseRoundedIcon from '@mui/icons-material/CloseRounded';
56

67
import { ExpanderAccordion } from '~/common/components/ExpanderAccordion';
@@ -123,6 +124,7 @@ export function LocalAIAdmin(props: { access: OpenAIAccessSchema, onClose: () =>
123124
// state
124125
const [installModels, setInstallModels] = React.useState<{ galleryName: string; modelName: string; }[]>([]);
125126
const [showVoiceModels, setShowVoiceModels] = React.useState(false);
127+
const parentRef = React.useRef<HTMLDivElement>(null);
126128

127129
// external state
128130
const { data, error } = apiQuery.llmOpenAI.dialectLocalAI_galleryModelsAvailable.useQuery({ access: props.access }, {
@@ -131,6 +133,18 @@ export function LocalAIAdmin(props: { access: OpenAIAccessSchema, onClose: () =>
131133

132134
// derived state
133135
const galleryNotConfigured = data === null;
136+
const filteredModels = React.useMemo(() =>
137+
data?.filter(model => showVoiceModels || !model.name?.startsWith('voice-')) || [],
138+
[data, showVoiceModels]
139+
);
140+
141+
// virtualizer
142+
const virtualizer = useVirtualizer({
143+
count: filteredModels.length,
144+
getScrollElement: () => parentRef.current,
145+
estimateSize: () => 40, // Fixed row height
146+
overscan: 5,
147+
});
134148

135149

136150
const handleAppendInstall = React.useCallback((galleryName: string, modelName: string) => {
@@ -179,47 +193,101 @@ export function LocalAIAdmin(props: { access: OpenAIAccessSchema, onClose: () =>
179193
how to configure model galleries.
180194
</>} />}
181195

182-
{/* List loading */}
196+
{/* Table loading */}
183197
{!data ? (
184198
<CircularProgress color='success' />
185199
) : (
186-
<List
187-
variant='outlined'
188-
sx={{
189-
'--ListItem-minHeight': '2.75rem',
190-
borderRadius: 'md',
191-
p: 0,
192-
}}
193-
>
194-
{data
195-
.filter(model => showVoiceModels || !model.name?.startsWith('voice-'))
196-
.map((model) => !!model.name && (
197-
<ListItem key={model.name}>
198-
199-
{capitalizeFirstLetter(model.name)}
200-
201-
<Button
202-
color='neutral'
203-
size='sm'
204-
disabled={installModels.some(p => p.galleryName === model.gallery.name && p.modelName === model.name)}
205-
onClick={() => model.name && handleAppendInstall(model.gallery.name, model.name)}
206-
sx={{
207-
ml: 'auto',
208-
}}
209-
>
210-
Install
211-
</Button>
212-
</ListItem>
213-
))}
214-
215-
<ListItemSwitch title='Show Voice Models' checked={showVoiceModels} onChange={setShowVoiceModels} />
216-
217-
<ExpanderAccordion title='Debug: show JSON' startCollapsed sx={{ fontSize: 'sm' }}>
218-
<Box sx={{ whiteSpace: 'break-spaces' }}>
219-
{JSON.stringify(data, null, 2)}
200+
<>
201+
<Sheet
202+
variant='outlined'
203+
sx={{
204+
borderRadius: 'md',
205+
overflow: 'hidden',
206+
}}
207+
>
208+
<Box
209+
ref={parentRef}
210+
sx={{
211+
height: '500px',
212+
overflow: 'auto',
213+
}}
214+
>
215+
<Table
216+
stickyHeader
217+
hoverRow
218+
>
219+
<thead>
220+
<tr>
221+
<th style={{ width: '35%' }}>Model</th>
222+
<th style={{ width: '35%' }}>Description</th>
223+
<th style={{ width: '20%' }}>Tags</th>
224+
<th>Action</th>
225+
</tr>
226+
</thead>
227+
<tbody style={{ height: `${virtualizer.getTotalSize()}px`, position: 'relative' }}>
228+
{virtualizer.getVirtualItems().map((virtualItem) => {
229+
const model = filteredModels[virtualItem.index];
230+
if (!model?.name) return null;
231+
232+
return (
233+
<tr
234+
key={virtualItem.key}
235+
style={{
236+
position: 'absolute',
237+
top: 0,
238+
left: 0,
239+
width: '100%',
240+
height: '40px',
241+
transform: `translateY(${virtualItem.start}px)`,
242+
display: 'flex',
243+
}}
244+
>
245+
<td style={{ width: '35%', minWidth: 0 }} className='agi-ellipsize'>
246+
{capitalizeFirstLetter(model.name)}
247+
</td>
248+
<td style={{ width: '35%', minWidth: 0 }} className='agi-ellipsize'>
249+
<small>{model.description || '-'}</small>
250+
</td>
251+
<td style={{ width: '20%', minWidth: 0 }} className='agi-ellipsize'>
252+
{model.tags && model.tags.length > 0 &&
253+
model.tags.slice(0, 2).map((tag, idx) => (
254+
<Chip key={idx} size='sm' variant='soft' sx={{ fontSize: 'xs' }}>
255+
{tag}
256+
</Chip>
257+
))}
258+
{model.tags && model.tags.length > 2 && (
259+
<small>+{model.tags.length - 2}</small>
260+
)}
261+
</td>
262+
<td style={{ minWidth: 0, textAlign: 'right', flexShrink: 0 }}>
263+
<Button
264+
size='sm'
265+
color='neutral'
266+
disabled={installModels.some(p => p.galleryName === model.gallery.name && p.modelName === model.name)}
267+
onClick={() => model.name && handleAppendInstall(model.gallery.name, model.name)}
268+
sx={{ minWidth: 'auto', minHeight: 'auto', px: 1, py: 0.25 }}
269+
>
270+
Install
271+
</Button>
272+
</td>
273+
</tr>
274+
);
275+
})}
276+
</tbody>
277+
</Table>
220278
</Box>
221-
</ExpanderAccordion>
222-
</List>
279+
</Sheet>
280+
281+
<List variant='outlined' sx={{ borderRadius: 'md', p: 0, mt: 2 }}>
282+
<ListItemSwitch title='Show Voice Models' checked={showVoiceModels} onChange={setShowVoiceModels} />
283+
284+
<ExpanderAccordion title='Debug: show JSON' startCollapsed sx={{ fontSize: 'sm' }}>
285+
<Box sx={{ whiteSpace: 'break-spaces' }}>
286+
{JSON.stringify(data, null, 2)}
287+
</Box>
288+
</ExpanderAccordion>
289+
</List>
290+
</>
223291
)}
224292

225293
</Box>

src/modules/llms/vendors/localai/LocalAIServiceSetup.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { ExternalLink } from '~/common/components/ExternalLink';
1212
import { FormInputKey } from '~/common/components/forms/FormInputKey';
1313
import { InlineError } from '~/common/components/InlineError';
1414
import { Link } from '~/common/components/Link';
15+
import { LocalAIIcon } from '~/common/components/icons/vendors/LocalAIIcon';
1516
import { SetupFormRefetchButton } from '~/common/components/forms/SetupFormRefetchButton';
1617

1718
import { ApproximateCosts } from '../ApproximateCosts';
@@ -99,8 +100,8 @@ export function LocalAIServiceSetup(props: { serviceId: DModelsServiceId }) {
99100
<SetupFormRefetchButton
100101
refetch={refetch} disabled={!shallFetchSucceed || isFetching} loading={isFetching} error={isError}
101102
leftButton={
102-
<Button color='neutral' variant='solid' disabled={adminOpen} onClick={() => setAdminOpen(true)}>
103-
Gallery Admin
103+
<Button color='neutral' variant='solid' disabled={adminOpen} onClick={() => setAdminOpen(true)} startDecorator={<LocalAIIcon />}>
104+
Install Models
104105
</Button>
105106
}
106107
/>

0 commit comments

Comments
 (0)