diff --git a/index.html b/index.html index d68f85e020..2b09b133d2 100644 --- a/index.html +++ b/index.html @@ -32,8 +32,8 @@ NODE_ENV: 'production' } }; - globalThis.packageVersion = "24.09.0-alpha.1"; - globalThis.buildNumber = "6011"; + globalThis.packageVersion = "25.1.0-alpha.1"; + globalThis.buildNumber = "6441"; diff --git a/manifest.json b/manifest.json index ea4a8b3a3d..191cb43f3b 100644 --- a/manifest.json +++ b/manifest.json @@ -3,7 +3,7 @@ "manifest_version": 9, "name": "Backend.AI Web UI", "short_name": "BackendAIWebUI", - "version": "24.09.0-alpha.1", + "version": "25.1.0-alpha.1", "start_url": "/", "display": "standalone", "background_color": "#fff", diff --git a/package.json b/package.json index 985baf826b..ed625d51f4 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "backend.ai-webui", "productName": "Backend.AI Desktop", - "version": "24.09.0-alpha.1", + "version": "25.1.0-alpha.1", "repository": "https://github.com/lablup/backend.ai-webui.git", "author": "Lablup Inc. ", "license": "LGPL-3.0-or-later", diff --git a/react/package.json b/react/package.json index b9700848a9..c7112d7a15 100644 --- a/react/package.json +++ b/react/package.json @@ -1,6 +1,6 @@ { "name": "backend-ai-webui-react", - "version": "24.09.0-alpha.1", + "version": "25.1.0-alpha.1", "private": true, "dependencies": { "@ai-sdk/openai": "^1.0.11", diff --git a/react/src/App.tsx b/react/src/App.tsx index b7556c3cf5..b0b0587a19 100644 --- a/react/src/App.tsx +++ b/react/src/App.tsx @@ -66,6 +66,13 @@ const ComputeSessionList = React.lazy( const AgentSummaryPage = React.lazy(() => import('./pages/AgentSummaryPage')); const MaintenancePage = React.lazy(() => import('./pages/MaintenancePage')); +/** + * Pages for Model Player + */ +const PlaygroundPage = React.lazy( + () => import('./components/lablupTalkativotUI/LLMPlaygroundPage'), +); +const ModelStorePage = React.lazy(() => import('./pages/ModelStorePage')); interface CustomHandle { title?: string; labelKey?: string; @@ -107,17 +114,17 @@ const router = createBrowserRouter([ children: [ { path: '/', - element: , + element: , }, { //for electron dev mode path: '/build/electron-app/app/index.html', - element: , + element: , }, { //for electron prod mode path: '/app/index.html', - element: , + element: , }, { path: '/summary', @@ -365,6 +372,19 @@ const router = createBrowserRouter([ path: '*', element: <>, }, + /** + * Pages for Model Player + */ + { + path: '/playground', + handle: { labelKey: 'webui.menu.Playground' }, + Component: PlaygroundPage, + }, + { + path: '/model-store', + handle: { labelKey: 'webui.menu.ModelStore' }, + Component: ModelStorePage, + }, ], }, ]); diff --git a/react/src/components/BAIIcons/VLLMIcon.tsx b/react/src/components/BAIIcons/VLLMIcon.tsx new file mode 100644 index 0000000000..feaf304d7f --- /dev/null +++ b/react/src/components/BAIIcons/VLLMIcon.tsx @@ -0,0 +1,23 @@ +import { ReactComponent as logo } from './vllm-color.svg'; +import Icon from '@ant-design/icons'; +import { CustomIconComponentProps } from '@ant-design/icons/lib/components/Icon'; + +interface CustomIconProps + extends Omit { + size?: number; +} + +const VLLMIcon: React.FC = (props) => { + return ( + + ); +}; + +export default VLLMIcon; diff --git a/react/src/components/BAIIcons/vllm-color.svg b/react/src/components/BAIIcons/vllm-color.svg new file mode 100644 index 0000000000..54acc3de2d --- /dev/null +++ b/react/src/components/BAIIcons/vllm-color.svg @@ -0,0 +1 @@ +vLLM \ No newline at end of file diff --git a/react/src/components/ChatContent.tsx b/react/src/components/ChatContent.tsx new file mode 100644 index 0000000000..ff6f8f159c --- /dev/null +++ b/react/src/components/ChatContent.tsx @@ -0,0 +1,122 @@ +import { useTanQuery } from '../hooks/reactQueryAlias'; +import { ChatContentEndpointDetailQuery } from './__generated__/ChatContentEndpointDetailQuery.graphql'; +import { Model } from './lablupTalkativotUI/ChatUIModal'; +import LLMChatCard from './lablupTalkativotUI/LLMChatCard'; +import { ReloadOutlined } from '@ant-design/icons'; +import { Alert, Button } from 'antd'; +import graphql from 'babel-plugin-relay/macro'; +import _ from 'lodash'; +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import { useLazyLoadQuery } from 'react-relay/hooks'; + +interface ChatContentProps { + endpointId: string; + endpointUrl: string; + basePath: string; +} + +const ChatContent: React.FC = ({ + endpointId, + endpointUrl, + basePath, +}) => { + const { t } = useTranslation(); + + const { endpoint_token_list } = + useLazyLoadQuery( + graphql` + query ChatContentEndpointDetailQuery( + $endpointId: UUID! + $tokenListOffset: Int! + $tokenListLimit: Int! + ) { + endpoint_token_list( + limit: $tokenListLimit + offset: $tokenListOffset + endpoint_id: $endpointId + ) { + total_count + items { + id + token + endpoint_id + created_at + valid_until + } + } + } + `, + { + tokenListLimit: 100, + tokenListOffset: 0, + endpointId: endpointId as string, + }, + { + fetchPolicy: 'network-only', + }, + ); + + const newestValidToken = + _.orderBy(endpoint_token_list?.items, ['valid_until'], ['desc'])[0] + ?.token ?? ''; + + const { + data: modelsResult, + // error, + refetch, + } = useTanQuery<{ + data: Array; + }>({ + queryKey: ['models', endpointUrl], + queryFn: () => { + return fetch(new URL(basePath + '/models', endpointUrl).toString(), { + headers: { + Authorization: `BackendAI ${newestValidToken}`, + }, + }) + .then((res) => res.json()) + .catch((err) => { + console.log(err); + }); + }, + }); + + return ( + ({ + id: m.id, + name: m.id, + }))} + apiKey={newestValidToken} + fetchOnClient + style={{ flex: 1 }} + allowCustomModel={false} + alert={ + _.isEmpty(modelsResult?.data) && ( + } + onClick={() => { + refetch(); + }} + > + {t('button.Refresh')} + + } + /> + ) + } + modelId={modelsResult?.data?.[0].id ?? 'custom'} + modelToken={newestValidToken} + /> + ); +}; + +export default ChatContent; diff --git a/react/src/components/ImportFromHuggingFacePanel.tsx b/react/src/components/ImportFromHuggingFacePanel.tsx new file mode 100644 index 0000000000..2f3638499b --- /dev/null +++ b/react/src/components/ImportFromHuggingFacePanel.tsx @@ -0,0 +1,61 @@ +import Flex from '../components/Flex'; +import BAICard from './BAICard'; +import { App, Button, Input, theme } from 'antd'; +import type { GetProps } from 'antd'; +import React, { useState } from 'react'; +import { useTranslation } from 'react-i18next'; + +const ImportFromHuggingFacePanel: React.FC = () => { + const { t } = useTranslation(); + const { token } = theme.useToken(); + const { message } = App.useApp(); + const [search, setSearch] = useState(''); + type SearchProps = GetProps; + + const { Search } = Input; + const onSearch: SearchProps['onSearch'] = (value, _e, info) => { + // TODO: download model from hugging face by URL + setSearch(value); + }; + + return ( + + + { + message.info({ + key: 'import-from-hugging-face', + content: 'Only available for administrators.', + }); + }} + // FIXME: Temporary use hardcoded color for the button background + style={{ color: token.colorBgBase, backgroundColor: '#FF7A00' }} + > + Download + + } + size="large" + onSearch={onSearch} + /> + + + ); +}; + +export default ImportFromHuggingFacePanel; diff --git a/react/src/components/MainLayout/WebUISider.tsx b/react/src/components/MainLayout/WebUISider.tsx index 5e4eb9c145..026ad52b8a 100644 --- a/react/src/components/MainLayout/WebUISider.tsx +++ b/react/src/components/MainLayout/WebUISider.tsx @@ -16,6 +16,7 @@ import WebUILink from '../WebUILink'; import { PluginPage, WebUIPluginType } from './MainLayout'; import { ApiOutlined, + AppstoreOutlined, BarChartOutlined, CloudUploadOutlined, ControlOutlined, @@ -24,6 +25,7 @@ import { FileDoneOutlined, HddOutlined, InfoCircleOutlined, + MessageOutlined, SolutionOutlined, ToolOutlined, UserOutlined, @@ -85,67 +87,86 @@ const WebUISider: React.FC = (props) => { const primaryColors = usePrimaryColors(); const generalMenu = filterEmptyItem([ + // { + // label: {t('webui.menu.Summary')}, + // icon: , + // key: 'summary', + // }, + // { + // label: {t('webui.menu.Sessions')}, + // icon: , + // key: 'job', + // }, { - label: {t('webui.menu.Summary')}, - icon: , - key: 'summary', - }, - { - label: {t('webui.menu.Sessions')}, - icon: , - key: 'job', - }, - supportServing && { - label: {t('webui.menu.Serving')}, - icon: , - key: 'serving', - }, - { - label: {t('webui.menu.Import&Run')}, - icon: , - key: 'import', - }, - { - label: {t('webui.menu.Data&Storage')}, - icon: , - key: 'data', - }, - supportUserCommittedImage && { label: ( - - {t('webui.menu.MyEnvironments')} - + {t('webui.menu.Playground')} ), - icon: , - key: 'my-environment', + icon: , + key: 'playground', }, - !isHideAgents && { + { label: ( - - {t('webui.menu.AgentSummary')} - + {t('webui.menu.ModelStore')} ), - icon: , - key: 'agent-summary', + icon: , + key: 'model-store', }, - { + supportServing && { label: ( - {t('webui.menu.Statistics')} + {t('modelserving.menu.MyServices')} ), - icon: , - key: 'statistics', - }, - !!fasttrackEndpoint && { - label: t('webui.menu.FastTrack'), - icon: , - key: 'pipeline', - onClick: () => { - window.open(fasttrackEndpoint, '_blank', 'noopener noreferrer'); - }, + icon: , + key: 'serving', }, + // { + // label: {t('webui.menu.Import&Run')}, + // icon: , + // key: 'import', + // }, + // { + // label: ( + // {t('modelserving.menu.ModelList')} + // ), + // icon: , + // key: 'data', + // }, + // supportUserCommittedImage && { + // label: ( + // + // {t('webui.menu.MyEnvironments')} + // + // ), + // icon: , + // key: 'my-environment', + // }, + // !isHideAgents && { + // label: ( + // + // {t('webui.menu.AgentSummary')} + // + // ), + // icon: , + // key: 'agent-summary', + // }, + // { + // label: ( + // {t('webui.menu.Statistics')} + // ), + // icon: , + // key: 'statistics', + // }, + // !!fasttrackEndpoint && { + // label: t('webui.menu.FastTrack'), + // icon: , + // key: 'pipeline', + // onClick: () => { + // window.open(fasttrackEndpoint, '_blank', 'noopener noreferrer'); + // }, + // }, ]); - const adminMenu: MenuProps['items'] = [ + const adminMenu: MenuProps['items'] = []; + /*[ { label: {t('webui.menu.Users')}, icon: , @@ -168,8 +189,10 @@ const WebUISider: React.FC = (props) => { key: 'resource-policy', }, ]; + */ - const superAdminMenu: MenuProps['items'] = [ + const superAdminMenu: MenuProps['items'] = []; + /*[ { label: {t('webui.menu.Resources')}, icon: , @@ -196,9 +219,7 @@ const WebUISider: React.FC = (props) => { icon: , key: 'information', }, - ]; - - const pluginMap: Record = { + ]*/ const pluginMap: Record = { 'menuitem-user': generalMenu, 'menuitem-admin': adminMenu, 'menuitem-superadmin': superAdminMenu, @@ -255,7 +276,7 @@ const WebUISider: React.FC = (props) => { height: themeConfig?.logo?.size?.height || 24, cursor: 'pointer', }} - onClick={() => webuiNavigate(themeConfig?.logo?.href || '/summary')} + onClick={() => webuiNavigate(themeConfig?.logo?.href || '/serving')} /> } theme={currentSiderTheme} @@ -275,7 +296,7 @@ const WebUISider: React.FC = (props) => { height: themeConfig?.logo.sizeCollapsed?.height ?? 24, cursor: 'pointer', }} - onClick={() => webuiNavigate(themeConfig?.logo?.href || '/summary')} + onClick={() => webuiNavigate(themeConfig?.logo?.href || '/serving')} /> } logoTitle={themeConfig?.logo?.logoTitle || siteDescription || 'WebUI'} @@ -340,6 +361,7 @@ const WebUISider: React.FC = (props) => { ]} items={ // TODO: add plugin menu + /* currentUserRole === 'superadmin' ? [ { @@ -381,6 +403,8 @@ const WebUISider: React.FC = (props) => { }, ] : [] + */ + [] } /> diff --git a/react/src/components/ModelCardChat.tsx b/react/src/components/ModelCardChat.tsx new file mode 100644 index 0000000000..cd4728e443 --- /dev/null +++ b/react/src/components/ModelCardChat.tsx @@ -0,0 +1,111 @@ +import { useUpdatableState } from '../hooks'; +import ChatContent from './ChatContent'; +import { ModelCardChatQuery } from './__generated__/ModelCardChatQuery.graphql'; +import { Alert, Card, theme } from 'antd/lib'; +import graphql from 'babel-plugin-relay/macro'; +import _ from 'lodash'; +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import { useLazyLoadQuery } from 'react-relay'; + +// TODO: fetch endpoint list, filter endpoint by name and send it ot LLMChatCard + +interface ModelCardChatProps { + modelName?: string; + basePath?: string; +} + +const ModelCardChat: React.FC = ({ + modelName, + basePath = 'v1', +}) => { + const { t } = useTranslation(); + const { token } = theme.useToken(); + const [fetchKey, updateFetchKey] = useUpdatableState('first'); + + const { endpoint_list } = useLazyLoadQuery( + graphql` + query ModelCardChatQuery( + $offset: Int! + $limit: Int! + $filter: String + $projectID: UUID + ) { + endpoint_list( + offset: $offset + limit: $limit + project: $projectID + filter: $filter + ) { + items { + name + endpoint_id + url + model + status + } + } + } + `, + { + limit: 100, + offset: 0, + filter: `name ilike "%${modelName}%"`, + }, + { + fetchPolicy: 'network-only', + fetchKey, + }, + ); + + const healthyEndpoint = _.filter(endpoint_list?.items, (item) => { + return item?.status == 'HEALTHY'; + }); + + // FIXME: temporally render chat UI only if at least one endpoint is healthy. + return healthyEndpoint.length > 0 ? ( + + ) : ( + , + ]} + /> + ); +}; + +export default ModelCardChat; diff --git a/react/src/components/ModelCardModal.tsx b/react/src/components/ModelCardModal.tsx index 2204697f7b..3eeeb524ae 100644 --- a/react/src/components/ModelCardModal.tsx +++ b/react/src/components/ModelCardModal.tsx @@ -1,27 +1,33 @@ import { useBackendAIImageMetaData } from '../hooks'; +import { useUpdatableState } from '../hooks'; import BAIModal, { BAIModalProps } from './BAIModal'; import Flex from './Flex'; +import ModelCardChat from './ModelCardChat'; import ModelCloneModal from './ModelCloneModal'; +import ModelTryContent from './ModelTryContent'; import ResourceNumber from './ResourceNumber'; import { ModelCardModalFragment$key } from './__generated__/ModelCardModalFragment.graphql'; -import { BankOutlined, CopyOutlined, FileOutlined } from '@ant-design/icons'; +import { BankOutlined, FileOutlined } from '@ant-design/icons'; import { Alert, Button, Card, Col, Descriptions, + Divider, Empty, Grid, Row, Tag, Typography, + Tabs, theme, + Skeleton, } from 'antd'; import graphql from 'babel-plugin-relay/macro'; import dayjs from 'dayjs'; -import _ from 'lodash'; -import { Cog, FolderX } from 'lucide-react'; +import _, { head } from 'lodash'; +import { FolderX } from 'lucide-react'; import Markdown from 'markdown-to-jsx'; import React, { Suspense, useState } from 'react'; import { useTranslation } from 'react-i18next'; @@ -29,6 +35,7 @@ import { useFragment } from 'react-relay'; interface ModelCardModalProps extends BAIModalProps { modelCardModalFrgmt?: ModelCardModalFragment$key | null; + // basePath?: string; onRequestClose: () => void; } const ModelCardModal: React.FC = ({ @@ -130,261 +137,365 @@ const ModelCardModal: React.FC = ({ , ]} > - {model_card?.error_msg ? ( - - - - - ) : ( - <> - - - {model_card?.category && ( - - {model_card?.category} - - )} - {model_card?.task && ( - }> + - {model_card?.task} - - )} - {model_card?.label && - _.map(model_card?.label, (label) => ( - - {label} - - ))} - {model_card?.license && ( - } - bordered={false} - color="geekblue" - style={{ marginRight: 0 }} + {model_card?.name === 'Talkativot UI' ? ( + // FIXME: temporally add iframe for Talkativot UI +