From a3509b36266fbb70dfe50ba0b5c720e8b327b222 Mon Sep 17 00:00:00 2001 From: RockChinQ Date: Fri, 11 Sep 2026 21:17:01 +0800 Subject: [PATCH] feat(models): show LangBot Models availability --- .../pkg/api/http/controller/groups/system.py | 15 +++++ src/langbot/pkg/api/http/service/space.py | 17 ++++- src/langbot/pkg/entity/dto/space_model.py | 13 +++- .../api/service/test_space_service.py | 55 ++++++++++++++- .../dynamic-form/DynamicFormItemComponent.tsx | 36 ++++++++++ .../ModelAvailabilityIndicator.tsx | 67 +++++++++++++++++++ .../useLangBotModelAvailability.ts | 63 +++++++++++++++++ .../components/models-dialog/ModelsPanel.tsx | 7 ++ .../models-dialog/components/ModelItem.tsx | 12 ++++ .../models-dialog/components/ProviderCard.tsx | 25 ++++++- web/src/app/infra/entities/api/index.ts | 18 +++++ web/src/app/infra/http/BackendClient.ts | 5 ++ web/src/i18n/locales/en-US.ts | 6 ++ web/src/i18n/locales/es-ES.ts | 6 ++ web/src/i18n/locales/ja-JP.ts | 6 ++ web/src/i18n/locales/ru-RU.ts | 6 ++ web/src/i18n/locales/th-TH.ts | 6 ++ web/src/i18n/locales/vi-VN.ts | 6 ++ web/src/i18n/locales/zh-Hans.ts | 6 ++ web/src/i18n/locales/zh-Hant.ts | 6 ++ 20 files changed, 375 insertions(+), 6 deletions(-) create mode 100644 web/src/app/home/components/model-availability/ModelAvailabilityIndicator.tsx create mode 100644 web/src/app/home/components/model-availability/useLangBotModelAvailability.ts diff --git a/src/langbot/pkg/api/http/controller/groups/system.py b/src/langbot/pkg/api/http/controller/groups/system.py index eb5ec45c1..6b11ff2f0 100644 --- a/src/langbot/pkg/api/http/controller/groups/system.py +++ b/src/langbot/pkg/api/http/controller/groups/system.py @@ -220,6 +220,21 @@ class SystemRouterGroup(group.RouterGroup): return self.http_status(503, -1, str(exc)) return self.success(data=model) + @self.route( + '/model-availability', + methods=['GET'], + auth_type=group.AuthType.USER_TOKEN, + permission=Permission.RESOURCE_VIEW, + ) + async def _(request_context: RequestContext) -> str: + """Expose Space's latest persisted model probes to the WebUI.""" + try: + models = await self.ap.space_service.get_model_selection() + except Exception as exc: + self.ap.logger.warning(f'Failed to load LangBot Models availability: {exc}') + return self.http_status(503, -1, 'Model availability is unavailable') + return self.success(data={'models': [model.model_dump(mode='json') for model in models]}) + @self.route( '/tasks', methods=['GET'], diff --git a/src/langbot/pkg/api/http/service/space.py b/src/langbot/pkg/api/http/service/space.py index 5be09a860..5d581c310 100644 --- a/src/langbot/pkg/api/http/service/space.py +++ b/src/langbot/pkg/api/http/service/space.py @@ -242,13 +242,14 @@ class SpaceService: models_data = data.get('data', {}).get('models', []) return [SpaceModel.model_validate(model_dict) for model_dict in models_data] - async def get_model_selection(self, category: str) -> typing.List[SpaceModelSelection]: + async def get_model_selection(self, category: str | None = None) -> typing.List[SpaceModelSelection]: """Return Space models in the availability-ranked selection order.""" space_url = self._get_space_config()['url'] session = httpclient.get_session() + params = {'category': category} if category else None async with session.get( f'{space_url}/api/v1/models/selection', - params={'category': category}, + params=params, ) as response: if response.status != 200: error = await httpclient.read_text_limited(response) @@ -266,7 +267,17 @@ class SpaceService: models = [] for selection in data: if isinstance(selection, dict) and isinstance(selection.get('model'), dict): - models.append(selection['model']) + model = dict(selection['model']) + availability = selection.get('availability') + if not isinstance(availability, dict): + # Accept the short-lived pre-release response shape. + availability = { + key: selection[key] + for key in ('up', 'last_probed_at', 'latency_ms', 'http_code') + if key in selection + } + model['availability'] = availability + models.append(model) else: models.append(selection) return [SpaceModelSelection.model_validate(model) for model in models] diff --git a/src/langbot/pkg/entity/dto/space_model.py b/src/langbot/pkg/entity/dto/space_model.py index 4ecd598d7..f6d171522 100644 --- a/src/langbot/pkg/entity/dto/space_model.py +++ b/src/langbot/pkg/entity/dto/space_model.py @@ -49,8 +49,19 @@ class SpaceModel(pydantic.BaseModel): updated_at: str | None = None +class SpaceModelAvailability(pydantic.BaseModel): + """Latest availability probe. ``up`` is None when no probe exists.""" + + up: bool | None = None + last_probed_at: str | None = None + latency_ms: int = 0 + http_code: int = 0 + + class SpaceModelSelection(pydantic.BaseModel): - """Minimal model identity returned by the ranked selection endpoint.""" + """Model identity and the latest persisted probe from Space.""" uuid: str model_id: str + category: str | None = None + availability: SpaceModelAvailability = pydantic.Field(default_factory=SpaceModelAvailability) diff --git a/tests/unit_tests/api/service/test_space_service.py b/tests/unit_tests/api/service/test_space_service.py index a77dd14a1..9d19af710 100644 --- a/tests/unit_tests/api/service/test_space_service.py +++ b/tests/unit_tests/api/service/test_space_service.py @@ -847,7 +847,15 @@ class TestSpaceServiceGetModelSelection: data = {'models': models} elif response_shape == 'availability-wrapper': data = [ - {'model': model, 'latency_ms': index + 10, 'http_code': 200} + { + 'model': model, + 'availability': { + 'up': True, + 'last_probed_at': '2026-09-11T12:01:18Z', + 'latency_ms': index + 10, + 'http_code': 200, + }, + } for index, model in enumerate(models) ] else: @@ -870,11 +878,56 @@ class TestSpaceServiceGetModelSelection: result = await service.get_model_selection('chat') assert [model.uuid for model in result] == ['best-model', 'fallback-model'] + if response_shape == 'availability-wrapper': + assert result[0].availability.up is True + assert result[0].availability.last_probed_at == '2026-09-11T12:01:18Z' + assert result[0].availability.latency_ms == 10 session.get.assert_called_once_with( 'https://space.langbot.app/api/v1/models/selection', params={'category': 'chat'}, ) + async def test_selection_without_category_fetches_all_model_statuses(self): + ap = SimpleNamespace(instance_config=SimpleNamespace(data={})) + service = SpaceService(ap) + payload = { + 'code': 0, + 'data': { + 'models': [ + { + 'model': { + 'uuid': 'embedding-model', + 'model_id': 'text-embedding', + 'category': 'embedding', + }, + 'availability': {'up': None, 'last_probed_at': None}, + } + ] + }, + } + mock_response = MagicMock(status=200) + + with ( + patch('langbot.pkg.api.http.service.space.httpclient.get_session') as get_session, + patch( + 'langbot.pkg.api.http.service.space.httpclient.read_json_limited', + new=AsyncMock(return_value=payload), + ), + ): + session = MagicMock() + session.get.return_value.__aenter__ = AsyncMock(return_value=mock_response) + session.get.return_value.__aexit__ = AsyncMock(return_value=None) + get_session.return_value = session + + result = await service.get_model_selection() + + assert result[0].category == 'embedding' + assert result[0].availability.up is None + session.get.assert_called_once_with( + 'https://space.langbot.app/api/v1/models/selection', + params=None, + ) + async def test_recommended_model_uses_first_selection_and_refreshes_once(self): local_model = SimpleNamespace(uuid='local-model-uuid', name='best-chat-model') persistence = SimpleNamespace( diff --git a/web/src/app/home/components/dynamic-form/DynamicFormItemComponent.tsx b/web/src/app/home/components/dynamic-form/DynamicFormItemComponent.tsx index f8b96d74b..8f1e1886c 100644 --- a/web/src/app/home/components/dynamic-form/DynamicFormItemComponent.tsx +++ b/web/src/app/home/components/dynamic-form/DynamicFormItemComponent.tsx @@ -71,6 +71,8 @@ import { LANGBOT_MODELS_PROVIDER_REQUESTER } from '@/app/home/components/models- import ReasoningLevelPicker, { REASONING_LEVELS, } from '@/app/home/components/reasoning/ReasoningLevelPicker'; +import ModelAvailabilityIndicator from '@/app/home/components/model-availability/ModelAvailabilityIndicator'; +import { useLangBotModelAvailability } from '@/app/home/components/model-availability/useLangBotModelAvailability'; function hasUsableUuid( item: T, @@ -153,6 +155,33 @@ export default function DynamicFormItemComponent({ const [modelsDialogOpen, setModelsDialogOpen] = useState(false); const [settingsSection, setSettingsSection] = useState('models'); + const isModelSelector = [ + DynamicFormItemType.LLM_MODEL_SELECTOR, + DynamicFormItemType.EMBEDDING_MODEL_SELECTOR, + DynamicFormItemType.RERANK_MODEL_SELECTOR, + DynamicFormItemType.MODEL_FALLBACK_SELECTOR, + ].includes(config.type); + const { + availability: langbotModelAvailability, + loaded: langbotModelAvailabilityLoaded, + } = useLangBotModelAvailability( + isModelSelector && !systemInfo.disable_models_service, + ); + + const renderModelAvailability = (model: { + uuid: string; + name: string; + provider?: { requester?: string }; + }) => + model.provider?.requester === LANGBOT_MODELS_PROVIDER_REQUESTER ? ( + + ) : null; const fetchLlmModels = () => { httpClient @@ -584,6 +613,7 @@ export default function DynamicFormItemComponent({ {model.name} + {renderModelAvailability(model)} {model.abilities?.includes('vision') && ( )} @@ -688,6 +718,7 @@ export default function DynamicFormItemComponent({ {model.name} + {renderModelAvailability(model)} {model.abilities?.includes('vision') && ( )} @@ -784,6 +815,7 @@ export default function DynamicFormItemComponent({ {models.map((model) => ( {model.name} + {renderModelAvailability(model)} ))} @@ -876,6 +908,7 @@ export default function DynamicFormItemComponent({ {models.map((model) => ( {model.name} + {renderModelAvailability(model)} ))} @@ -941,6 +974,7 @@ export default function DynamicFormItemComponent({ {models.map((model) => ( {model.name} + {renderModelAvailability(model)} ))} @@ -1060,6 +1094,7 @@ export default function DynamicFormItemComponent({ {model.name} + {renderModelAvailability(model)} {model.abilities?.includes('vision') && ( )} @@ -1165,6 +1200,7 @@ export default function DynamicFormItemComponent({ {model.name} + {renderModelAvailability(model)} {model.abilities?.includes('vision') && ( )} diff --git a/web/src/app/home/components/model-availability/ModelAvailabilityIndicator.tsx b/web/src/app/home/components/model-availability/ModelAvailabilityIndicator.tsx new file mode 100644 index 000000000..48499e64d --- /dev/null +++ b/web/src/app/home/components/model-availability/ModelAvailabilityIndicator.tsx @@ -0,0 +1,67 @@ +import type { LangBotModelAvailability } from '@/app/infra/entities/api'; +import { + Tooltip, + TooltipContent, + TooltipTrigger, +} from '@/components/ui/tooltip'; +import { useTranslation } from 'react-i18next'; + +interface ModelAvailabilityIndicatorProps { + availability?: LangBotModelAvailability; + show: boolean; +} + +export default function ModelAvailabilityIndicator({ + availability, + show, +}: ModelAvailabilityIndicatorProps) { + const { t, i18n } = useTranslation(); + if (!show) return null; + + const state = availability?.up; + const label = + state === true + ? t('models.availability.available') + : state === false + ? t('models.availability.unavailable') + : t('models.availability.notChecked'); + const dotClass = + state === true + ? 'bg-emerald-500' + : state === false + ? 'bg-destructive' + : 'bg-muted-foreground/50'; + const checkedAt = availability?.last_probed_at + ? new Intl.DateTimeFormat(i18n.language, { + dateStyle: 'medium', + timeStyle: 'short', + }).format(new Date(availability.last_probed_at)) + : null; + + return ( + + + event.preventDefault()} + > + + + + +
+

{label}

+ {checkedAt && ( +

+ {t('models.availability.lastChecked', { time: checkedAt })} + {availability && availability.latency_ms > 0 + ? ` · ${availability.latency_ms} ms` + : ''} +

+ )} +
+
+
+ ); +} diff --git a/web/src/app/home/components/model-availability/useLangBotModelAvailability.ts b/web/src/app/home/components/model-availability/useLangBotModelAvailability.ts new file mode 100644 index 000000000..e5d8575ae --- /dev/null +++ b/web/src/app/home/components/model-availability/useLangBotModelAvailability.ts @@ -0,0 +1,63 @@ +import { useEffect, useState } from 'react'; +import type { LangBotModelAvailability } from '@/app/infra/entities/api'; +import { httpClient } from '@/app/infra/http/HttpClient'; + +const CACHE_TTL_MS = 60_000; + +type AvailabilityMap = Record; + +let cachedAvailability: AvailabilityMap | null = null; +let cacheExpiresAt = 0; +let pendingRequest: Promise | null = null; + +async function loadAvailability(): Promise { + if (cachedAvailability && Date.now() < cacheExpiresAt) { + return cachedAvailability; + } + if (pendingRequest) return pendingRequest; + + pendingRequest = httpClient + .getLangBotModelAvailability() + .then((response) => { + const next: AvailabilityMap = {}; + for (const item of response.models) { + next[item.uuid] = item.availability; + next[item.model_id] = item.availability; + } + cachedAvailability = next; + cacheExpiresAt = Date.now() + CACHE_TTL_MS; + return next; + }) + .finally(() => { + pendingRequest = null; + }); + return pendingRequest; +} + +export function useLangBotModelAvailability(enabled = true) { + const [availability, setAvailability] = useState( + cachedAvailability ?? {}, + ); + const [loaded, setLoaded] = useState( + cachedAvailability !== null && Date.now() < cacheExpiresAt, + ); + + useEffect(() => { + if (!enabled) return; + let active = true; + loadAvailability() + .then((result) => { + if (!active) return; + setAvailability(result); + setLoaded(true); + }) + .catch(() => { + // Availability is supplementary; model configuration remains usable. + }); + return () => { + active = false; + }; + }, [enabled]); + + return { availability, loaded }; +} diff --git a/web/src/app/home/components/models-dialog/ModelsPanel.tsx b/web/src/app/home/components/models-dialog/ModelsPanel.tsx index 0292ef630..5a5521d49 100644 --- a/web/src/app/home/components/models-dialog/ModelsPanel.tsx +++ b/web/src/app/home/components/models-dialog/ModelsPanel.tsx @@ -27,6 +27,7 @@ import { CustomApiError } from '@/app/infra/entities/common'; import { PanelBody } from '../settings-dialog/panel-layout'; import { useCurrentWorkspace } from '@/app/infra/http'; import type { WorkspaceSpaceBilling } from '@/app/infra/entities/workspace'; +import { useLangBotModelAvailability } from '../model-availability/useLangBotModelAvailability'; interface ModelsPanelProps { // True when this panel is the active section and the dialog is open. @@ -89,6 +90,10 @@ export default function ModelsPanel({ const currentWorkspace = useCurrentWorkspace(); const canManage = currentWorkspace?.permissions.includes('provider_secret.manage') ?? false; + const { + availability: langbotModelAvailability, + loaded: langbotModelAvailabilityLoaded, + } = useLangBotModelAvailability(active && !systemInfo.disable_models_service); const [providers, setProviders] = useState([]); const [spaceBilling, setSpaceBilling] = @@ -554,6 +559,8 @@ export default function ModelsPanel({ isWorkspaceOwner={currentWorkspace?.membership.role === 'owner'} ownerSpaceBound={spaceBilling?.owner_space_bound ?? false} spaceCredits={spaceBilling?.credits ?? null} + modelAvailability={langbotModelAvailability} + modelAvailabilityLoaded={langbotModelAvailabilityLoaded} addModelPopoverOpen={addModelPopoverOpen} editModelPopoverOpen={editModelPopoverOpen} deleteConfirmOpen={deleteConfirmOpen} diff --git a/web/src/app/home/components/models-dialog/components/ModelItem.tsx b/web/src/app/home/components/models-dialog/components/ModelItem.tsx index fae9be86f..8a3c83d0c 100644 --- a/web/src/app/home/components/models-dialog/components/ModelItem.tsx +++ b/web/src/app/home/components/models-dialog/components/ModelItem.tsx @@ -14,6 +14,7 @@ import { useTranslation } from 'react-i18next'; import { LLMModel, EmbeddingModel, + LangBotModelAvailability, ReasoningConfig, } from '@/app/infra/entities/api'; import { @@ -24,12 +25,15 @@ import { } from '../types'; import ExtraArgsEditor from './ExtraArgsEditor'; import { userInfo } from '@/app/infra/http'; +import ModelAvailabilityIndicator from '../../model-availability/ModelAvailabilityIndicator'; interface ModelItemProps { model: LLMModel | EmbeddingModel; canManage: boolean; modelType: ModelType; isLangBotModels: boolean; + availability?: LangBotModelAvailability; + availabilityLoaded: boolean; editModelPopoverOpen: string | null; deleteConfirmOpen: string | null; onOpenEditModel: (modelId: string) => void; @@ -86,6 +90,8 @@ export default function ModelItem({ canManage, modelType, isLangBotModels, + availability, + availabilityLoaded, editModelPopoverOpen, deleteConfirmOpen, onOpenEditModel, @@ -199,6 +205,12 @@ export default function ModelItem({ >
{model.name} + {isLangBotModels && ( + + )} {modelType === 'llm' ? t('models.chat') diff --git a/web/src/app/home/components/models-dialog/components/ProviderCard.tsx b/web/src/app/home/components/models-dialog/components/ProviderCard.tsx index fd468a12a..67c1181de 100644 --- a/web/src/app/home/components/models-dialog/components/ProviderCard.tsx +++ b/web/src/app/home/components/models-dialog/components/ProviderCard.tsx @@ -9,7 +9,11 @@ import { Radar, } from 'lucide-react'; import { httpClient, systemInfo } from '@/app/infra/http/HttpClient'; -import { ModelProvider, ReasoningConfig } from '@/app/infra/entities/api'; +import { + LangBotModelAvailability, + ModelProvider, + ReasoningConfig, +} from '@/app/infra/entities/api'; import { Button } from '@/components/ui/button'; import { Collapsible, @@ -47,6 +51,8 @@ interface ProviderCardProps { isWorkspaceOwner: boolean; ownerSpaceBound: boolean; spaceCredits: number | null; + modelAvailability: Record; + modelAvailabilityLoaded: boolean; // Popover states addModelPopoverOpen: string | null; editModelPopoverOpen: string | null; @@ -115,6 +121,8 @@ export default function ProviderCard({ isWorkspaceOwner, ownerSpaceBound, spaceCredits, + modelAvailability, + modelAvailabilityLoaded, addModelPopoverOpen, editModelPopoverOpen, deleteConfirmOpen, @@ -424,6 +432,11 @@ export default function ProviderCard({ canManage={canManage} modelType="llm" isLangBotModels={isLangBotModels} + availability={ + modelAvailability[model.uuid] ?? + modelAvailability[model.name] + } + availabilityLoaded={modelAvailabilityLoaded} editModelPopoverOpen={editModelPopoverOpen} deleteConfirmOpen={deleteConfirmOpen} onOpenEditModel={onOpenEditModel} @@ -475,6 +488,11 @@ export default function ProviderCard({ canManage={canManage} modelType="embedding" isLangBotModels={isLangBotModels} + availability={ + modelAvailability[model.uuid] ?? + modelAvailability[model.name] + } + availabilityLoaded={modelAvailabilityLoaded} editModelPopoverOpen={editModelPopoverOpen} deleteConfirmOpen={deleteConfirmOpen} onOpenEditModel={onOpenEditModel} @@ -524,6 +542,11 @@ export default function ProviderCard({ canManage={canManage} modelType="rerank" isLangBotModels={isLangBotModels} + availability={ + modelAvailability[model.uuid] ?? + modelAvailability[model.name] + } + availabilityLoaded={modelAvailabilityLoaded} editModelPopoverOpen={editModelPopoverOpen} deleteConfirmOpen={deleteConfirmOpen} onOpenEditModel={onOpenEditModel} diff --git a/web/src/app/infra/entities/api/index.ts b/web/src/app/infra/entities/api/index.ts index a58593f79..f3f08d488 100644 --- a/web/src/app/infra/entities/api/index.ts +++ b/web/src/app/infra/entities/api/index.ts @@ -158,6 +158,24 @@ export interface RerankModel { extra_args?: object; } +export interface LangBotModelAvailability { + up: boolean | null; + last_probed_at: string | null; + latency_ms: number; + http_code: number; +} + +export interface LangBotModelAvailabilityItem { + uuid: string; + model_id: string; + category: string | null; + availability: LangBotModelAvailability; +} + +export interface ApiRespLangBotModelAvailability { + models: LangBotModelAvailabilityItem[]; +} + export interface ApiRespPipelines { pipelines: Pipeline[]; } diff --git a/web/src/app/infra/http/BackendClient.ts b/web/src/app/infra/http/BackendClient.ts index 8267c4cb5..600c47c2d 100644 --- a/web/src/app/infra/http/BackendClient.ts +++ b/web/src/app/infra/http/BackendClient.ts @@ -63,6 +63,7 @@ import { BotRouteDryRunRequest, BotRouteDryRunResult, BotEventRouteStatusResponse, + ApiRespLangBotModelAvailability, } from '@/app/infra/entities/api'; import { Plugin } from '@/app/infra/entities/plugin'; import type { PluginLogEntry } from '@/app/infra/entities/plugin'; @@ -1250,6 +1251,10 @@ export class BackendClient extends BaseHttpClient { return this.get('/api/v1/system/wizard/recommended-model'); } + public getLangBotModelAvailability(): Promise { + return this.get('/api/v1/system/model-availability'); + } + public getAsyncTasks(params?: { type?: string; kind?: string; diff --git a/web/src/i18n/locales/en-US.ts b/web/src/i18n/locales/en-US.ts index a2bbc9027..3c9d93ae0 100644 --- a/web/src/i18n/locales/en-US.ts +++ b/web/src/i18n/locales/en-US.ts @@ -304,6 +304,12 @@ const enUS = { usesOwnerSpaceBilling: "Uses the Workspace owner's LangBot Account billing and credits.", noModels: 'No models configured', + availability: { + available: 'Available at last check', + unavailable: 'Unavailable at last check', + notChecked: 'No check result', + lastChecked: 'Checked {{time}}', + }, langbotModels: 'LangBot Models', spaceTrialTooltip: 'Free trial credits available! Login with LangBot Account to access cloud models with zero configuration.', diff --git a/web/src/i18n/locales/es-ES.ts b/web/src/i18n/locales/es-ES.ts index 56e7f05b1..6253ce590 100644 --- a/web/src/i18n/locales/es-ES.ts +++ b/web/src/i18n/locales/es-ES.ts @@ -306,6 +306,12 @@ const esES = { loginToUseModels: 'Inicia sesión con una cuenta de LangBot para usar modelos en la nube', noModels: 'No hay modelos configurados', + availability: { + available: 'Disponible en la última comprobación', + unavailable: 'No disponible en la última comprobación', + notChecked: 'Sin resultado de comprobación', + lastChecked: 'Comprobado {{time}}', + }, langbotModels: 'Modelos LangBot', spaceTrialTooltip: '¡Créditos de prueba gratuitos disponibles! Inicia sesión con una cuenta de LangBot para acceder a modelos en la nube sin configuración.', diff --git a/web/src/i18n/locales/ja-JP.ts b/web/src/i18n/locales/ja-JP.ts index 773cb92ad..4f6f24fc9 100644 --- a/web/src/i18n/locales/ja-JP.ts +++ b/web/src/i18n/locales/ja-JP.ts @@ -309,6 +309,12 @@ const jaJP = { usesOwnerSpaceBilling: 'ワークスペース所有者の LangBot アカウント課金とクレジットを使用します。', noModels: 'モデルがありません', + availability: { + available: '前回のチェックで利用可能', + unavailable: '前回のチェックで利用不可', + notChecked: 'チェック結果なし', + lastChecked: '{{time}} にチェック', + }, langbotModels: 'LangBot モデル', spaceTrialTooltip: '無料トライアルクレジットが利用可能!LangBot アカウントでログインして、設定不要でクラウドモデルを使用できます。', diff --git a/web/src/i18n/locales/ru-RU.ts b/web/src/i18n/locales/ru-RU.ts index 235fde44e..a28d883f5 100644 --- a/web/src/i18n/locales/ru-RU.ts +++ b/web/src/i18n/locales/ru-RU.ts @@ -303,6 +303,12 @@ const ruRU = { loginToUseModels: 'Войдите с аккаунтом LangBot, чтобы использовать облачные модели', noModels: 'Модели не настроены', + availability: { + available: 'Доступна при последней проверке', + unavailable: 'Недоступна при последней проверке', + notChecked: 'Нет результата проверки', + lastChecked: 'Проверено {{time}}', + }, langbotModels: 'Модели LangBot', spaceTrialTooltip: 'Доступны бесплатные пробные кредиты! Войдите с аккаунтом LangBot, чтобы получить доступ к облачным моделям без настройки.', diff --git a/web/src/i18n/locales/th-TH.ts b/web/src/i18n/locales/th-TH.ts index 7f3d6c945..52223fa99 100644 --- a/web/src/i18n/locales/th-TH.ts +++ b/web/src/i18n/locales/th-TH.ts @@ -292,6 +292,12 @@ const thTH = { loginWithSpace: 'เข้าสู่ระบบด้วยบัญชี LangBot', loginToUseModels: 'เข้าสู่ระบบด้วยบัญชี LangBot เพื่อใช้โมเดลคลาวด์', noModels: 'ยังไม่มีโมเดลที่กำหนดค่า', + availability: { + available: 'พร้อมใช้งานในการตรวจสอบล่าสุด', + unavailable: 'ไม่พร้อมใช้งานในการตรวจสอบล่าสุด', + notChecked: 'ไม่มีผลการตรวจสอบ', + lastChecked: 'ตรวจสอบเมื่อ {{time}}', + }, langbotModels: 'โมเดล LangBot', spaceTrialTooltip: 'มีเครดิตทดลองใช้งานฟรี! เข้าสู่ระบบด้วยบัญชี LangBot เพื่อเข้าถึงโมเดลคลาวด์โดยไม่ต้องตั้งค่า', diff --git a/web/src/i18n/locales/vi-VN.ts b/web/src/i18n/locales/vi-VN.ts index 02ed4d0b7..7889a36c2 100644 --- a/web/src/i18n/locales/vi-VN.ts +++ b/web/src/i18n/locales/vi-VN.ts @@ -300,6 +300,12 @@ const viVN = { loginToUseModels: 'Đăng nhập bằng tài khoản LangBot để sử dụng mô hình đám mây', noModels: 'Chưa cấu hình mô hình nào', + availability: { + available: 'Khả dụng ở lần kiểm tra gần nhất', + unavailable: 'Không khả dụng ở lần kiểm tra gần nhất', + notChecked: 'Chưa có kết quả kiểm tra', + lastChecked: 'Đã kiểm tra {{time}}', + }, langbotModels: 'Mô hình LangBot', spaceTrialTooltip: 'Có tín dụng dùng thử miễn phí! Đăng nhập bằng tài khoản LangBot để truy cập mô hình đám mây không cần cấu hình.', diff --git a/web/src/i18n/locales/zh-Hans.ts b/web/src/i18n/locales/zh-Hans.ts index 63b131a4d..5bf58364e 100644 --- a/web/src/i18n/locales/zh-Hans.ts +++ b/web/src/i18n/locales/zh-Hans.ts @@ -290,6 +290,12 @@ const zhHans = { '工作区所有者需要绑定 LangBot 账号才能使用 LangBot 模型。', usesOwnerSpaceBilling: '使用工作区所有者的 LangBot 账号计费与积分。', noModels: '暂无模型', + availability: { + available: '上次检测可用', + unavailable: '上次检测不可用', + notChecked: '暂无检测结果', + lastChecked: '检测于 {{time}}', + }, langbotModels: 'LangBot 模型', spaceTrialTooltip: '免费试用积分已就绪!通过 LangBot 账号登录即可零配置使用云端模型。', diff --git a/web/src/i18n/locales/zh-Hant.ts b/web/src/i18n/locales/zh-Hant.ts index 2eaa84006..9402ef3ef 100644 --- a/web/src/i18n/locales/zh-Hant.ts +++ b/web/src/i18n/locales/zh-Hant.ts @@ -281,6 +281,12 @@ const zhHant = { loginWithSpace: '使用 LangBot 帳號登入', loginToUseModels: '使用 LangBot 帳號登入以使用雲端模型', noModels: '暫無模型', + availability: { + available: '上次檢測可用', + unavailable: '上次檢測不可用', + notChecked: '暫無檢測結果', + lastChecked: '檢測於 {{time}}', + }, langbotModels: 'LangBot 模型', spaceTrialTooltip: '免費試用積分已就緒!使用 LangBot 帳號登入即可零設定使用雲端模型。',