Skip to content

Commit 4d824d2

Browse files
refactor(models): move provider actions to top toolbar
- Relocated "添加模型" button from inline form to page toolbar - Moved Codex OAuth connect/disconnect controls to toolbar area - Added onRegisterAddProviderAction callback pattern via useEffect - Changed source tab variant from segmented to plain for consistency - Fixed dropdown control height inside dialog surfaces - Replaced padding-top with padding-block for form headers - Added w-fit self-start to "添加 Header" button for proper alignment - Wrapped toolbar with justify-between and flex-wrap for responsive layout
1 parent ceeb812 commit 4d824d2

4 files changed

Lines changed: 36 additions & 9 deletions

File tree

‎packages/webui/src/client/features/models/ui/ProviderFormPanel.public.tsx‎

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
// write will actually send. The editor and its commands arrive as props — the provider CRUD
1717
// is `models`' duty, not `settings`'.
1818

19-
import { useRef, useState, type Dispatch, type ReactElement, type SetStateAction } from "react";
19+
import { useEffect, useRef, useState, type Dispatch, type ReactElement, type SetStateAction } from "react";
2020
import { DndContext, KeyboardSensor, PointerSensor, closestCenter, useSensor, useSensors, type DragEndEvent } from "@dnd-kit/core";
2121
import { SortableContext, sortableKeyboardCoordinates, useSortable, verticalListSortingStrategy } from "@dnd-kit/sortable";
2222
import { CSS } from "@dnd-kit/utilities";
@@ -115,6 +115,7 @@ export interface WebuiProviderFormPanelProps {
115115
readonly setProviderActionError: (message?: string) => void;
116116
readonly editor: WebuiProviderEditor;
117117
readonly snapshot: WebuiProviderEditorSnapshot;
118+
readonly onRegisterAddProviderAction: (action?: () => void) => void;
118119
readonly providerPendingDelete?: WebuiModelProviderView;
119120
readonly setProviderPendingDelete: Dispatch<SetStateAction<WebuiModelProviderView | undefined>>;
120121
}
@@ -222,7 +223,7 @@ function ProviderPresetPicker({ controlId, presets, selected, selectedName, onSe
222223

223224
export function ProviderFormPanel({
224225
providers, presets, commands, setProviderActionError,
225-
editor, snapshot,
226+
editor, snapshot, onRegisterAddProviderAction,
226227
providerPendingDelete, setProviderPendingDelete,
227228
}: WebuiProviderFormPanelProps): ReactElement {
228229
const { dom } = useWebuiBrowserCapabilities();
@@ -271,6 +272,12 @@ export function ProviderFormPanel({
271272
try { await commands.loadPresets(); }
272273
catch (error) { setProviderActionError(error instanceof Error ? error.message : String(error)); }
273274
};
275+
const openNewProviderRef = useRef<() => void>(() => {});
276+
openNewProviderRef.current = () => { void openProviderForm(); };
277+
useEffect(() => {
278+
onRegisterAddProviderAction(() => openNewProviderRef.current());
279+
return () => onRegisterAddProviderAction(undefined);
280+
}, [onRegisterAddProviderAction]);
274281
// The dialog closes only on a successful delete; a refusal stays beside it,
275282
// in the editor's error channel.
276283
const confirmDeleteProvider = async () => {
@@ -322,7 +329,6 @@ export function ProviderFormPanel({
322329
}}><SortableContext items={ids} strategy={verticalListSortingStrategy}><div className="w-full rounded-[16px] bg-bg_grouped_tertiary p-1">{models.map((model, index) => <SortableModelRow key={ids[index]} id={ids[index]!} model={model} onTest={() => void commands.testModel({ providerId: id, modelId: textOf(model.modelId) })} onToggle={(enabled) => void toggleModel(id, model, enabled)} />)}</div></SortableContext></DndContext> : null}
323330
</WebuiStack>;
324331
})}
325-
<WebuiButton variant="plain" className="h-9 w-fit px-3" disabled={!commands.canManageProviders} onClick={() => void openProviderForm()}>添加模型</WebuiButton>
326332
<WebuiDialog id="provider-form" open={showProviderForm} title={editingProvider ? "编辑提供商" : "添加模型"} onClose={() => editor.close()} dismissOnEscape portalTarget={portalTarget} width="standard" footer={<>
327333
<WebuiInline as="label" className="webui-provider-form-footer-leading"><WebuiCheckbox checked={skipTest} onChange={(event) => editor.setSkipConnectionTest(event.target.checked)} />跳过连通检测</WebuiInline>
328334
<WebuiDialogActions>
@@ -346,7 +352,7 @@ export function ProviderFormPanel({
346352
<WebuiInline justify="between" gap="medium" className="webui-provider-form-models-header"><span className="text-[14px] font-medium leading-5 text-text_default_primary">模型</span><WebuiInline gap="small"><WebuiButton variant="outline-secondary" size="compact" onClick={() => editor.addModel()}>添加模型</WebuiButton><WebuiButton variant="quiet-highlight" size="compact" disabled={!snapshot.canDiscover || formBusy} onClick={() => void editor.discover()}>自动获取</WebuiButton></WebuiInline></WebuiInline>
347353
{draftModels.length ? draftModels.map((model, index) => <ProviderModelCard key={model.id} model={model} index={index} busy={formBusy} onChange={(patch) => editor.changeModel(model.id, patch)} onTest={() => void editor.testModel(model.id)} onRemove={() => editor.removeModel(model.id)} />) : <FeedbackEmpty className="webui-provider-form-models-empty" title={"请至少添加一个模型"} />}
348354
</WebuiStack>
349-
<WebuiStack gap="small" className="webui-provider-form-headers"><div className="text-[13px] font-medium leading-5 text-text_default_primary">自定义 Headers</div>{draftHeaders.map((header) => <WebuiInline key={header.id} gap="small" className="webui-provider-form-header-row"><WebuiTextInput controlSize="dense" className="flex-1" aria-label="Header 名称" placeholder="Header 名称" value={header.name} onChange={(event) => editor.setHeaders(draftHeaders.map((item) => item.id === header.id ? { ...item, name: event.target.value } : item))} /><WebuiTextInput controlSize="dense" className="flex-1" aria-label="Header 值" placeholder={header.persistedName ? "留空保留原值" : "Header 值"} value={header.value} onChange={(event) => editor.setHeaders(draftHeaders.map((item) => item.id === header.id ? { ...item, value: event.target.value } : item))} /><IconButton label="移除 Header" onClick={() => editor.removeHeader(header.id)}><IconTrash aria-hidden="true" size={16} stroke={1.7} /></IconButton></WebuiInline>)}<WebuiButton variant="outline-secondary" size="compact" onClick={() => editor.addHeader()}><IconPlus aria-hidden="true" size={16} stroke={1.7} /> 添加 Header</WebuiButton></WebuiStack>
355+
<WebuiStack gap="small" className="webui-provider-form-headers"><div className="text-[13px] font-medium leading-5 text-text_default_primary">自定义 Headers</div>{draftHeaders.map((header) => <WebuiInline key={header.id} gap="small" className="webui-provider-form-header-row"><WebuiTextInput controlSize="dense" className="flex-1" aria-label="Header 名称" placeholder="Header 名称" value={header.name} onChange={(event) => editor.setHeaders(draftHeaders.map((item) => item.id === header.id ? { ...item, name: event.target.value } : item))} /><WebuiTextInput controlSize="dense" className="flex-1" aria-label="Header 值" placeholder={header.persistedName ? "留空保留原值" : "Header 值"} value={header.value} onChange={(event) => editor.setHeaders(draftHeaders.map((item) => item.id === header.id ? { ...item, value: event.target.value } : item))} /><IconButton label="移除 Header" onClick={() => editor.removeHeader(header.id)}><IconTrash aria-hidden="true" size={16} stroke={1.7} /></IconButton></WebuiInline>)}<WebuiButton variant="outline-secondary" size="compact" className="w-fit self-start" onClick={() => editor.addHeader()}><IconPlus aria-hidden="true" size={16} stroke={1.7} /> 添加 Header</WebuiButton></WebuiStack>
350356
{formError ? <FeedbackError message={formError} className="webui-provider-form-error" /> : null}
351357
</WebuiStack>
352358
</form>

‎packages/webui/src/client/features/models/ui/models.css‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333
.webui-provider-picker-other { flex: 0 0 auto; margin-top: 4px; border-top: 1px solid var(--border_default); padding: 4px 0; }
3434
.webui-provider-picker-other-option { width: 100%; height: 32px; padding-inline: 12px; color: var(--text_default_secondary); text-align: left; }
3535
.webui-provider-picker-other-icon { display: flex; width: 24px; height: 24px; flex: 0 0 24px; align-items: center; justify-content: center; }
36-
.webui-provider-form-headers { padding-top: 8px; }
36+
.webui-provider-form-headers { padding-block: 8px; }
3737
.webui-provider-form-header-row { min-width: 0; }
3838
/* The model list: one card per model, each carrying its own fields. */
3939
.webui-provider-form-models { padding-top: 8px; }

‎packages/webui/src/client/features/settings/ui/UsageModelSettings.tsx‎

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useEffect, useState, type ReactElement } from "react";
1+
import { useCallback, useEffect, useState, type ReactElement } from "react";
22
import { formatWebuiCredits, recordOf, textOf } from "../../../contracts/value-readers.js";
33
import { useWebuiModelsWorkflows, useWebuiModelsWorkflowsState, useWebuiSettingsWorkflows, useWebuiSettingsWorkflowsState } from "../../../bindings/use-query-state.js";
44
import { useWebuiAccountWorkflows, useWebuiAccountWorkflowsState } from "../../../features/account/bindings/use-account-workflows.public.js";
@@ -96,6 +96,10 @@ export function UsageModelSettings({ sessionId }: Props): ReactElement {
9696
const [oauthError, setOauthError] = useState<string>();
9797
const [oauthLoginId, setOauthLoginId] = useState<string>();
9898
const [providerPendingDelete, setProviderPendingDelete] = useState<WebuiModelProviderView>();
99+
const [addProviderAction, setAddProviderAction] = useState<() => void>();
100+
const registerAddProviderAction = useCallback((action?: () => void) => {
101+
setAddProviderAction(() => action);
102+
}, []);
99103

100104
// Committed read state, derived from the owner snapshots.
101105
const quota = accountState.usage.result;
@@ -215,7 +219,8 @@ export function UsageModelSettings({ sessionId }: Props): ReactElement {
215219

216220

217221
return <SettingsPage testId="settings-usage-model">
218-
<WebuiInline gap="medium" className="h-8">
222+
<WebuiInline gap="medium" justify="between" className="min-h-8 flex-wrap">
223+
<WebuiInline gap="medium" className="h-8">
219224
<WebuiDropdown
220225
open={sourceMenuOpen}
221226
onOpen={() => setSourceMenuOpen(true)}
@@ -239,7 +244,7 @@ export function UsageModelSettings({ sessionId }: Props): ReactElement {
239244
<WebuiButton
240245
{...buttonProps}
241246
ref={ref}
242-
variant="segmented"
247+
variant="plain"
243248
size="compact"
244249
aria-pressed={sourceTab !== "custom"}
245250
data-testid="settings-usage-source-tab"
@@ -271,6 +276,17 @@ export function UsageModelSettings({ sessionId }: Props): ReactElement {
271276
</WebuiDropdown>
272277
<div className="h-3 w-[0.5px] bg-border_default" />
273278
<WebuiButton variant="segmented" size="compact" aria-pressed={sourceTab === "custom"} className="h-8 rounded-[8px] px-3 text-[14px] font-medium leading-5" onClick={() => setSourceTab("custom")}>自定义模型</WebuiButton>
279+
</WebuiInline>
280+
{sourceTab === "custom" ? <WebuiInline gap="small" className="ml-auto">
281+
<WebuiButton variant="outline-highlight" size="compact" disabled={!models?.canManageProviders || !addProviderAction} onClick={() => addProviderAction?.()}>添加模型</WebuiButton>
282+
{oauth?.state === "connected" ? <>
283+
<span className="self-center text-[13px] text-text_default_secondary">Codex OAuth 已连接</span>
284+
<WebuiButton variant="outline-highlight" size="compact" disabled={!models?.canManageProviders} onClick={() => void models?.refreshModels?.()}>获取模型列表</WebuiButton>
285+
</> : <>
286+
<WebuiButton variant="outline-highlight" size="compact" disabled={!settings?.canStartCodexOAuth || Boolean(oauthLoginId)} onClick={() => void beginCodexLogin()}>{oauthLoginId ? "正在连接 Codex OAuth" : "连接 Codex OAuth"}</WebuiButton>
287+
{oauthLoginId ? <WebuiButton variant="outline-highlight" size="compact" disabled={!settings?.canStartCodexOAuth} onClick={() => void cancelCodexLogin()}>取消</WebuiButton> : null}
288+
</>}
289+
</WebuiInline> : null}
274290
</WebuiInline>
275291

276292
{sourceTab === "token-plan" ? <WebuiStack as="section" gap="roomy" className="w-full" data-testid="settings-usage-token-plan">
@@ -304,10 +320,11 @@ export function UsageModelSettings({ sessionId }: Props): ReactElement {
304320
setProviderActionError={setProviderActionError}
305321
editor={editor}
306322
snapshot={providerEditorSnapshot}
323+
onRegisterAddProviderAction={registerAddProviderAction}
307324
providerPendingDelete={providerPendingDelete}
308325
setProviderPendingDelete={setProviderPendingDelete}
309326
/>
310-
{oauthError ? <FeedbackError message={oauthError} /> : oauth?.state === "connected" ? <WebuiInline gap="medium"><span>Codex OAuth 已连接</span><WebuiButton variant="outline" size="inline" disabled={!models?.canManageProviders} onClick={() => void models?.refreshModels?.()}>获取模型列表</WebuiButton></WebuiInline> : <WebuiInline gap="small"><WebuiButton variant="surface-quiet" size="inline" disabled={!settings?.canStartCodexOAuth || Boolean(oauthLoginId)} onClick={() => void beginCodexLogin()}>{oauthLoginId ? "正在连接 Codex OAuth" : "连接 Codex OAuth"}</WebuiButton>{oauthLoginId ? <WebuiButton variant="quiet-secondary" size="inline" disabled={!settings?.canStartCodexOAuth} onClick={() => void cancelCodexLogin()}>取消</WebuiButton> : null}</WebuiInline>}
327+
{oauthError ? <FeedbackError message={oauthError} /> : null}
311328
</>}
312329
</WebuiStack> : null}
313330
</SettingsPage>;

‎packages/webui/src/client/styles/dialog.css‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,10 @@
160160
height: 36px;
161161
min-height: 36px;
162162
}
163+
.webui-dialog-surface .webui-field-control > .webui-dropdown > * {
164+
height: 36px;
165+
min-height: 36px;
166+
}
163167
.webui-dialog-surface .webui-field-label {
164168
color: var(--text_default_secondary);
165169
font-size: var(--size_14);

0 commit comments

Comments
 (0)