feat(models): add workspace starred model and processor defaults

This commit is contained in:
Hyu
2026-10-09 18:34:29 +08:00
parent 4c70977a1d
commit c5c3ec0b2c
20 changed files with 231 additions and 9 deletions
@@ -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) => (
+12
View File
@@ -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);
}
+3
View File
@@ -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:
+3
View File
@@ -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:
+3
View File
@@ -539,6 +539,9 @@ const jaJP = {
help: 'ヘルプドキュメントを見る',
},
models: {
starModel: '新規処理器の既定モデルに設定',
unstarModel: 'スターを解除',
starModelFailed: 'スター付きモデルを更新できませんでした',
codex: {
account: 'ChatGPT サブスクリプション',
description:
+3
View File
@@ -541,6 +541,9 @@ const ruRU = {
help: 'Помощь',
},
models: {
starModel: 'Отметить модель для новых обработчиков',
unstarModel: 'Снять отметку',
starModelFailed: 'Не удалось изменить избранную модель',
codex: {
account: 'Подписка ChatGPT',
description:
+3
View File
@@ -529,6 +529,9 @@ const thTH = {
help: 'ขอความช่วยเหลือ',
},
models: {
starModel: 'ติดดาวเพื่อใช้เป็นค่าเริ่มต้นสำหรับตัวประมวลผลใหม่',
unstarModel: 'ยกเลิกดาว',
starModelFailed: 'อัปเดตโมเดลที่ติดดาวไม่สำเร็จ',
codex: {
account: 'การสมัครสมาชิก ChatGPT',
description:
+3
View File
@@ -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:
+3
View File
@@ -505,6 +505,9 @@ const zhHans = {
help: '查看帮助文档',
},
models: {
starModel: '标星模型(用作新处理器的默认模型)',
unstarModel: '取消标星',
starModelFailed: '无法更新标星模型',
codex: {
account: 'ChatGPT 订阅',
description:
+3
View File
@@ -502,6 +502,9 @@ const zhHant = {
help: '查看說明文件',
},
models: {
starModel: '標星模型(用作新處理器的預設模型)',
unstarModel: '取消標星',
starModelFailed: '無法更新標星模型',
codex: {
account: 'ChatGPT 訂閱',
description: