mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-10-21 16:05:19 +08:00
feat(models): add workspace starred model and processor defaults
This commit is contained in:
@@ -103,6 +103,17 @@ export default function AgentCreateContent({
|
||||
const parameters = runnerStage ? getDefaultValues(runnerStage.config) : {};
|
||||
const config = { runner: { id: runner }, runner_config: { [runner]: parameters } };
|
||||
try {
|
||||
if (kind === 'pipeline' && runnerStage?.config.some((field) => ['llm-model-selector', 'select-llm-model', 'model-fallback-selector'].includes(field.type))) {
|
||||
const { uuid } = await httpClient.getDefaultModel();
|
||||
if (uuid) {
|
||||
for (const field of runnerStage.config) {
|
||||
if (['llm-model-selector', 'select-llm-model'].includes(field.type) && !parameters[field.name]) parameters[field.name] = uuid;
|
||||
if (field.type === 'model-fallback-selector' && !parameters[field.name]?.primary) {
|
||||
parameters[field.name] = { ...parameters[field.name], primary: uuid, fallbacks: parameters[field.name]?.fallbacks ?? [] };
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (kind === 'pipeline') {
|
||||
// Keep the created ID if configuration fails, so retry does not create duplicates.
|
||||
const uuid = createdPipeline.current ?? (await httpClient.createAgent({ kind, name: values.name, description: '', emoji: values.emoji })).uuid;
|
||||
|
||||
@@ -47,7 +47,7 @@ export default function LangBotModelMetadata({
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<span
|
||||
className="inline-flex items-center gap-1 text-xs tabular-nums text-muted-foreground"
|
||||
className="inline-flex items-center gap-1 whitespace-nowrap text-xs tabular-nums text-muted-foreground"
|
||||
onMouseDown={(event) => event.preventDefault()}
|
||||
>
|
||||
<Coins className="size-3" />
|
||||
@@ -59,12 +59,12 @@ export default function LangBotModelMetadata({
|
||||
<TooltipContent side="top" className="max-w-64">
|
||||
<div className="space-y-0.5">
|
||||
<p>{t('models.pricing.title')}</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
<p className="text-xs">
|
||||
{t('models.pricing.input', {
|
||||
credits: input,
|
||||
})}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
<p className="text-xs">
|
||||
{t('models.pricing.output', {
|
||||
credits: output,
|
||||
})}
|
||||
|
||||
@@ -25,6 +25,7 @@ import {
|
||||
TestResult,
|
||||
} from '../types';
|
||||
import ExtraArgsEditor from './ExtraArgsEditor';
|
||||
import ModelStar from './ModelStar';
|
||||
import { userInfo } from '@/app/infra/http';
|
||||
import LangBotModelMetadata from '../../model-availability/LangBotModelMetadata';
|
||||
|
||||
@@ -197,14 +198,14 @@ export default function ModelItem({
|
||||
>
|
||||
<PopoverTrigger asChild>
|
||||
<div
|
||||
className={`flex items-center justify-between py-2 px-3 rounded-md border bg-background ${
|
||||
className={`flex items-center gap-3 py-2 px-3 rounded-md border bg-background ${
|
||||
isPopoverDisabled
|
||||
? 'cursor-not-allowed opacity-60'
|
||||
: 'hover:bg-accent cursor-pointer'
|
||||
}`}
|
||||
>
|
||||
<div className="flex min-w-0 items-center gap-2 flex-wrap">
|
||||
<span className="text-sm font-medium">{model.name}</span>
|
||||
<div className="flex min-w-0 flex-1 items-center gap-2 flex-wrap">
|
||||
<span className="break-all text-sm font-medium">{model.name}</span>
|
||||
<Badge variant="secondary" className="text-xs">
|
||||
{modelType === 'llm'
|
||||
? t('models.chat')
|
||||
@@ -240,6 +241,7 @@ export default function ModelItem({
|
||||
loaded={availabilityLoaded}
|
||||
/>
|
||||
)}
|
||||
{modelType === 'llm' && <ModelStar uuid={model.uuid} disabled={!canManage || isPopoverDisabled} />}
|
||||
{canManage && !isLangBotModels && (
|
||||
<Popover
|
||||
open={isDeleteOpen}
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
import { useEffect, useSyncExternalStore } from 'react';
|
||||
import { Star } from 'lucide-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { toast } from 'sonner';
|
||||
import { backendClient, useCurrentWorkspace } from '@/app/infra/http';
|
||||
import { Button } from '@/components/ui/button';
|
||||
|
||||
const records = new Map<string, { uuid: string | null; busy: boolean; loaded: boolean }>();
|
||||
const listeners = new Set<() => void>();
|
||||
const pending = new Map<string, Promise<void>>();
|
||||
const empty = { uuid: null, busy: false, loaded: false };
|
||||
const subscribe = (listener: () => void) => { listeners.add(listener); return () => { listeners.delete(listener); }; };
|
||||
function update(workspace: string, value: typeof empty | { uuid: string | null; busy: boolean; loaded: boolean }) {
|
||||
records.set(workspace, value);
|
||||
listeners.forEach((listener) => listener());
|
||||
}
|
||||
|
||||
export default function ModelStar({ uuid, disabled }: { uuid: string; disabled: boolean }) {
|
||||
const workspace = useCurrentWorkspace()?.workspace.uuid ?? '';
|
||||
const { t } = useTranslation();
|
||||
const state = useSyncExternalStore(subscribe, () => records.get(workspace) ?? empty);
|
||||
useEffect(() => {
|
||||
if (!workspace || pending.has(workspace)) return;
|
||||
const request = backendClient.getStarredModel().then((result) => {
|
||||
update(workspace, { uuid: result.uuid, busy: false, loaded: true });
|
||||
}).catch(() => { update(workspace, { uuid: null, busy: false, loaded: false }); })
|
||||
.finally(() => pending.delete(workspace));
|
||||
pending.set(workspace, request);
|
||||
}, [workspace]);
|
||||
const starred = state.uuid === uuid;
|
||||
return <Button type="button" variant="ghost" size="icon" className={`size-7 shrink-0 ${starred ? 'text-amber-500 hover:text-amber-600' : 'text-muted-foreground'}`}
|
||||
disabled={disabled || state.busy || !state.loaded}
|
||||
aria-pressed={starred} aria-label={t(starred ? 'models.unstarModel' : 'models.starModel')}
|
||||
title={t(starred ? 'models.unstarModel' : 'models.starModel')}
|
||||
onPointerDown={(event) => event.stopPropagation()}
|
||||
onClick={async (event) => {
|
||||
event.stopPropagation();
|
||||
const next = starred ? null : uuid;
|
||||
update(workspace, { ...state, busy: true });
|
||||
try {
|
||||
await backendClient.setStarredModel(next);
|
||||
update(workspace, { uuid: next, busy: false, loaded: true });
|
||||
} catch {
|
||||
update(workspace, { ...state, busy: false });
|
||||
toast.error(t('models.starModelFailed'));
|
||||
}
|
||||
}}><Star className={`size-3.5 ${starred ? 'fill-current' : ''}`} /></Button>;
|
||||
}
|
||||
@@ -191,7 +191,7 @@ export default function SettingsDialog({
|
||||
}}
|
||||
>
|
||||
<DialogContent
|
||||
className="h-[80vh] max-h-[800px] overflow-hidden p-0 sm:max-w-[52rem] [&>button:last-child]:z-20"
|
||||
className="flex h-[80dvh] max-h-[800px] min-h-0 flex-col gap-0 overflow-hidden p-0 sm:max-w-[52rem] [&>button:last-child]:z-20"
|
||||
// Fixed height so switching sections never resizes the dialog; each
|
||||
// panel scrolls its own content internally.
|
||||
>
|
||||
@@ -202,7 +202,7 @@ export default function SettingsDialog({
|
||||
|
||||
{/* Override the SidebarProvider wrapper's default h-svh so the two
|
||||
columns fill the dialog's fixed height instead of the viewport. */}
|
||||
<SidebarProvider className="!min-h-0 h-full">
|
||||
<SidebarProvider className="!min-h-0 flex-1 overflow-hidden">
|
||||
<Sidebar
|
||||
collapsible="none"
|
||||
className="hidden h-full w-44 shrink-0 border-r md:flex"
|
||||
@@ -231,7 +231,7 @@ export default function SettingsDialog({
|
||||
</SidebarContent>
|
||||
</Sidebar>
|
||||
|
||||
<main className="flex h-full min-w-0 flex-1 flex-col overflow-hidden">
|
||||
<main className="flex min-h-0 min-w-0 flex-1 flex-col overflow-hidden">
|
||||
{/* Mobile section switcher (sidebar is hidden on small screens) */}
|
||||
<div className="flex shrink-0 items-center gap-1 overflow-x-auto border-b px-3 py-2 md:hidden">
|
||||
{navItems.map((item) => (
|
||||
|
||||
@@ -372,6 +372,18 @@ export class BackendClient extends BaseHttpClient {
|
||||
return this.post('/api/v1/agents', agent);
|
||||
}
|
||||
|
||||
public getStarredModel(): Promise<{ uuid: string | null }> {
|
||||
return this.get('/api/v1/provider/models/llm/_/starred');
|
||||
}
|
||||
|
||||
public setStarredModel(uuid: string | null): Promise<object> {
|
||||
return this.put('/api/v1/provider/models/llm/_/starred', { uuid });
|
||||
}
|
||||
|
||||
public getDefaultModel(): Promise<{ uuid: string | null }> {
|
||||
return this.get('/api/v1/provider/models/llm/_/default');
|
||||
}
|
||||
|
||||
public updateAgent(uuid: string, agent: Partial<Agent>): Promise<object> {
|
||||
return this.put(`/api/v1/agents/${uuid}`, agent);
|
||||
}
|
||||
|
||||
@@ -538,6 +538,9 @@ const enUS = {
|
||||
help: 'Get Help',
|
||||
},
|
||||
models: {
|
||||
starModel: 'Star model (default for new processors)',
|
||||
unstarModel: 'Unstar model',
|
||||
starModelFailed: 'Could not update starred model',
|
||||
codex: {
|
||||
account: 'ChatGPT subscription',
|
||||
description:
|
||||
|
||||
@@ -545,6 +545,9 @@ const esES = {
|
||||
help: 'Obtener ayuda',
|
||||
},
|
||||
models: {
|
||||
starModel: 'Marcar como favorito para nuevos procesadores',
|
||||
unstarModel: 'Quitar de favoritos',
|
||||
starModelFailed: 'No se pudo actualizar el modelo favorito',
|
||||
codex: {
|
||||
account: 'Suscripción de ChatGPT',
|
||||
description:
|
||||
|
||||
@@ -539,6 +539,9 @@ const jaJP = {
|
||||
help: 'ヘルプドキュメントを見る',
|
||||
},
|
||||
models: {
|
||||
starModel: '新規処理器の既定モデルに設定',
|
||||
unstarModel: 'スターを解除',
|
||||
starModelFailed: 'スター付きモデルを更新できませんでした',
|
||||
codex: {
|
||||
account: 'ChatGPT サブスクリプション',
|
||||
description:
|
||||
|
||||
@@ -541,6 +541,9 @@ const ruRU = {
|
||||
help: 'Помощь',
|
||||
},
|
||||
models: {
|
||||
starModel: 'Отметить модель для новых обработчиков',
|
||||
unstarModel: 'Снять отметку',
|
||||
starModelFailed: 'Не удалось изменить избранную модель',
|
||||
codex: {
|
||||
account: 'Подписка ChatGPT',
|
||||
description:
|
||||
|
||||
@@ -529,6 +529,9 @@ const thTH = {
|
||||
help: 'ขอความช่วยเหลือ',
|
||||
},
|
||||
models: {
|
||||
starModel: 'ติดดาวเพื่อใช้เป็นค่าเริ่มต้นสำหรับตัวประมวลผลใหม่',
|
||||
unstarModel: 'ยกเลิกดาว',
|
||||
starModelFailed: 'อัปเดตโมเดลที่ติดดาวไม่สำเร็จ',
|
||||
codex: {
|
||||
account: 'การสมัครสมาชิก ChatGPT',
|
||||
description:
|
||||
|
||||
@@ -535,6 +535,9 @@ const viVN = {
|
||||
help: 'Trợ giúp',
|
||||
},
|
||||
models: {
|
||||
starModel: 'Đánh dấu làm mô hình mặc định cho bộ xử lý mới',
|
||||
unstarModel: 'Bỏ đánh dấu',
|
||||
starModelFailed: 'Không thể cập nhật mô hình được đánh dấu',
|
||||
codex: {
|
||||
account: 'Gói đăng ký ChatGPT',
|
||||
description:
|
||||
|
||||
@@ -505,6 +505,9 @@ const zhHans = {
|
||||
help: '查看帮助文档',
|
||||
},
|
||||
models: {
|
||||
starModel: '标星模型(用作新处理器的默认模型)',
|
||||
unstarModel: '取消标星',
|
||||
starModelFailed: '无法更新标星模型',
|
||||
codex: {
|
||||
account: 'ChatGPT 订阅',
|
||||
description:
|
||||
|
||||
@@ -502,6 +502,9 @@ const zhHant = {
|
||||
help: '查看說明文件',
|
||||
},
|
||||
models: {
|
||||
starModel: '標星模型(用作新處理器的預設模型)',
|
||||
unstarModel: '取消標星',
|
||||
starModelFailed: '無法更新標星模型',
|
||||
codex: {
|
||||
account: 'ChatGPT 訂閱',
|
||||
description:
|
||||
|
||||
Reference in New Issue
Block a user