perf:优化webui交互体验,优化统计逻辑,优化log展示
This commit is contained in:
@@ -25,10 +25,11 @@ import { fieldHooks } from '@/lib/field-hooks'
|
||||
import { RestartProvider, useRestart } from '@/lib/restart-context'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
import { ChevronDown, ChevronUp, Code2, Info, Layout, Power, RefreshCw, Save } from 'lucide-react'
|
||||
import { ChevronLeft, ChevronRight, Code2, Info, Layout, Power, RefreshCw, Save } from 'lucide-react'
|
||||
|
||||
import type { ConfigSchema } from '@/types/config-schema'
|
||||
import {
|
||||
AliasNamesHook,
|
||||
BotPlatformAccountsHook,
|
||||
ChatPromptsHook,
|
||||
ChatTalkValueRulesHook,
|
||||
@@ -38,6 +39,7 @@ import {
|
||||
HiddenFieldHook,
|
||||
MCPRootItemsHook,
|
||||
MCPServersHook,
|
||||
MultipleReplyStyleHook,
|
||||
RegexRulesHook,
|
||||
useAutoSave,
|
||||
useConfigAutoSave,
|
||||
@@ -414,8 +416,10 @@ function BotConfigPageContent() {
|
||||
useEffect(() => {
|
||||
const hookEntries = [
|
||||
['bot.platform', BotPlatformAccountsHook, 'replace'],
|
||||
['bot.alias_names', AliasNamesHook],
|
||||
['bot.qq_account', HiddenFieldHook, 'hidden'],
|
||||
['bot.platforms', HiddenFieldHook, 'hidden'],
|
||||
['personality.multiple_reply_style', MultipleReplyStyleHook],
|
||||
['chat.chat_prompts', ChatPromptsHook],
|
||||
['chat.talk_value_rules', ChatTalkValueRulesHook],
|
||||
['expression.expression_groups', ExpressionGroupsHook],
|
||||
@@ -959,6 +963,7 @@ function DynamicConfigTabs(props: DynamicConfigTabsProps) {
|
||||
const { configSchema, sectionValues, setHasUnsavedChanges, setSectionValue, tabGroups } = props
|
||||
const [expanded, setExpanded] = useState(false)
|
||||
const [activeTab, setActiveTab] = useState(tabGroups[0]?.id ?? '')
|
||||
const [advancedVisible, setAdvancedVisible] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (!tabGroups.some((tab) => tab.id === activeTab)) {
|
||||
@@ -1028,6 +1033,7 @@ function DynamicConfigTabs(props: DynamicConfigTabsProps) {
|
||||
setHasUnsavedChanges(true)
|
||||
}}
|
||||
hooks={fieldHooks}
|
||||
advancedVisible={advancedVisible}
|
||||
sectionColumns={2}
|
||||
/>
|
||||
)
|
||||
@@ -1035,20 +1041,20 @@ function DynamicConfigTabs(props: DynamicConfigTabsProps) {
|
||||
|
||||
return (
|
||||
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full">
|
||||
<TabsList className="flex flex-wrap h-auto gap-1 p-1">
|
||||
<TabsList className="flex flex-wrap h-auto gap-1 p-1 transition-all duration-300 ease-out">
|
||||
{visibleTabGroups.map((tab) => {
|
||||
const isExpandedOnlyTab = !DEFAULT_VISIBLE_TAB_IDS.has(tab.id)
|
||||
return (
|
||||
<Fragment key={tab.id}>
|
||||
{tab.id === firstExpandedTabId && (
|
||||
<span className="mx-1 hidden h-6 w-px bg-border/80 sm:block" />
|
||||
<span className="mx-1 hidden h-6 w-px bg-border/80 transition-opacity duration-200 sm:block" />
|
||||
)}
|
||||
<TabsTrigger
|
||||
value={tab.id}
|
||||
className={cn(
|
||||
"text-xs px-2 py-1.5 sm:px-3 sm:py-2 data-[state=active]:shadow-sm",
|
||||
"px-2 py-1.5 text-sm transition-all duration-200 ease-out sm:px-3 sm:py-2 data-[state=active]:shadow-sm",
|
||||
isExpandedOnlyTab &&
|
||||
"border border-dashed border-border/70 bg-background/45 text-muted-foreground/80 hover:bg-background/70 data-[state=active]:border-primary/45 data-[state=active]:bg-primary/10 data-[state=active]:text-primary data-[state=active]:shadow-none"
|
||||
"border border-dashed border-border/70 bg-background/45 text-muted-foreground/80 motion-safe:animate-[config-tab-enter_180ms_ease-out_both] hover:bg-background/70 data-[state=active]:border-primary/45 data-[state=active]:bg-primary/10 data-[state=active]:text-primary data-[state=active]:shadow-none"
|
||||
)}
|
||||
>
|
||||
{tab.label}
|
||||
@@ -1061,20 +1067,29 @@ function DynamicConfigTabs(props: DynamicConfigTabsProps) {
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-8 px-2 text-xs sm:h-9 sm:px-3"
|
||||
className="group h-8 px-2 text-xs transition-all duration-200 ease-out sm:h-9 sm:px-3"
|
||||
onClick={toggleExpanded}
|
||||
>
|
||||
{expanded ? (
|
||||
<ChevronUp className="mr-1 h-3.5 w-3.5" />
|
||||
<ChevronLeft className="mr-1 h-3.5 w-3.5 transition-transform duration-200 group-hover:-translate-x-0.5" />
|
||||
) : (
|
||||
<ChevronDown className="mr-1 h-3.5 w-3.5" />
|
||||
<ChevronRight className="mr-1 h-3.5 w-3.5 transition-transform duration-200 group-hover:translate-x-0.5" />
|
||||
)}
|
||||
{expanded ? '收起' : '更多'}
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
type="button"
|
||||
variant={advancedVisible ? 'default' : 'outline'}
|
||||
size="sm"
|
||||
className="ml-auto h-8 px-2 text-xs transition-all duration-200 ease-out sm:h-9 sm:px-3"
|
||||
onClick={() => setAdvancedVisible((current) => !current)}
|
||||
>
|
||||
高级设置
|
||||
</Button>
|
||||
</TabsList>
|
||||
{tabGroups.map((tab) => (
|
||||
<TabsContent key={tab.id} value={tab.id} className="space-y-4">
|
||||
<TabsContent key={tab.id} value={tab.id} className="space-y-4 motion-safe:animate-[config-tab-content-enter_180ms_ease-out_both]">
|
||||
{renderTabContent(tab)}
|
||||
</TabsContent>
|
||||
))}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useCallback, useEffect, useMemo, useState, type CSSProperties } from 'react'
|
||||
import * as LucideIcons from 'lucide-react'
|
||||
import { Plus, Trash2 } from 'lucide-react'
|
||||
import { ChevronDown, ChevronUp, Plus, Trash2 } from 'lucide-react'
|
||||
|
||||
import { Button } from '@/components/ui/button'
|
||||
import {
|
||||
@@ -43,6 +43,7 @@ export interface ListItemEditorOptions {
|
||||
collapsedText?: string
|
||||
expandLabel?: string
|
||||
collapseLabel?: string
|
||||
collapseButtonDisplay?: 'text' | 'icon'
|
||||
}
|
||||
|
||||
function resolveLabel(schema?: ConfigSchema | FieldSchema, fieldPath?: string): string {
|
||||
@@ -293,6 +294,9 @@ export function createListItemEditorHook(
|
||||
const shouldCollapse = options.collapseWhen?.({ parentValues }) ?? false
|
||||
const [manuallyExpanded, setManuallyExpanded] = useState(false)
|
||||
const collapsed = shouldCollapse && !manuallyExpanded
|
||||
const collapseButtonLabel = collapsed
|
||||
? (options.expandLabel ?? '灞曞紑')
|
||||
: (options.collapseLabel ?? '鎶樺彔')
|
||||
|
||||
useEffect(() => {
|
||||
if (!shouldCollapse) {
|
||||
@@ -332,12 +336,31 @@ export function createListItemEditorHook(
|
||||
{renderLucideIcon(iconName, 'h-5 w-5 flex-shrink-0 text-muted-foreground')}
|
||||
<CardTitle className="truncate text-base">{label}</CardTitle>
|
||||
</div>
|
||||
{shouldCollapse && (
|
||||
{shouldCollapse && options.collapseButtonDisplay === 'icon' && (
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onClick={() => setManuallyExpanded((current) => !current)}
|
||||
aria-label={collapseButtonLabel}
|
||||
title={collapseButtonLabel}
|
||||
className="inline-flex items-center justify-center"
|
||||
>
|
||||
{collapsed ? (
|
||||
<ChevronDown className="h-4 w-4" />
|
||||
) : (
|
||||
<ChevronUp className="h-4 w-4" />
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
{shouldCollapse && options.collapseButtonDisplay !== 'icon' && (
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setManuallyExpanded((current) => !current)}
|
||||
aria-label={collapseButtonLabel}
|
||||
title={collapseButtonLabel}
|
||||
>
|
||||
{collapsed
|
||||
? (options.expandLabel ?? '展开')
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select'
|
||||
import { Textarea } from '@/components/ui/textarea'
|
||||
import type { FieldHookComponent } from '@/lib/field-hooks'
|
||||
|
||||
import { createJsonFieldHook } from './JsonFieldHookFactory'
|
||||
@@ -131,6 +132,98 @@ const formatPlatformAccount = (row: PlatformAccountRow): string => {
|
||||
return `${platform}:${account}`
|
||||
}
|
||||
|
||||
interface StringListHookOptions {
|
||||
addLabel: string
|
||||
emptyText: string
|
||||
label: string
|
||||
multiline?: boolean
|
||||
placeholder?: string
|
||||
}
|
||||
|
||||
function createStringListHook(options: StringListHookOptions): FieldHookComponent {
|
||||
return ({ onChange, value }) => {
|
||||
const items = Array.isArray(value) ? value.map((item) => String(item ?? '')) : []
|
||||
|
||||
const updateItems = (nextItems: string[]) => {
|
||||
onChange?.(nextItems)
|
||||
}
|
||||
|
||||
const addItem = () => {
|
||||
updateItems([...items, ''])
|
||||
}
|
||||
|
||||
const removeItem = (itemIndex: number) => {
|
||||
updateItems(items.filter((_, index) => index !== itemIndex))
|
||||
}
|
||||
|
||||
const updateItem = (itemIndex: number, nextValue: string) => {
|
||||
updateItems(items.map((item, index) => (index === itemIndex ? nextValue : item)))
|
||||
}
|
||||
|
||||
const InputComponent = options.multiline ? Textarea : Input
|
||||
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
<div className="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
|
||||
<Label className="text-[15px] leading-6">{options.label}</Label>
|
||||
<Button type="button" size="sm" variant="outline" onClick={addItem}>
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
{options.addLabel}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{items.length === 0 ? (
|
||||
<div className="rounded-md border border-dashed bg-muted/30 px-4 py-5 text-center text-sm text-muted-foreground">
|
||||
{options.emptyText}
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{items.map((item, itemIndex) => (
|
||||
<div
|
||||
key={itemIndex}
|
||||
className="grid gap-2 rounded-md border bg-muted/20 p-3 sm:grid-cols-[minmax(0,1fr)_2.5rem]"
|
||||
>
|
||||
<InputComponent
|
||||
value={item}
|
||||
placeholder={options.placeholder}
|
||||
onChange={(event) => updateItem(itemIndex, event.target.value)}
|
||||
{...(options.multiline ? { rows: 2 } : {})}
|
||||
/>
|
||||
<div className="flex items-start justify-end">
|
||||
<Button
|
||||
type="button"
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
aria-label={`删除${options.label} ${itemIndex + 1}`}
|
||||
onClick={() => removeItem(itemIndex)}
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export const AliasNamesHook = createStringListHook({
|
||||
addLabel: '添加别名',
|
||||
emptyText: '暂无别名。',
|
||||
label: '别名',
|
||||
placeholder: '小麦',
|
||||
})
|
||||
|
||||
export const MultipleReplyStyleHook = createStringListHook({
|
||||
addLabel: '添加表达风格',
|
||||
emptyText: '暂无备用表达风格。',
|
||||
label: '备用表达风格',
|
||||
multiline: true,
|
||||
placeholder: '输入一种备用表达风格',
|
||||
})
|
||||
|
||||
export const ChatTalkValueRulesHook = createListItemEditorHook({
|
||||
addLabel: '添加发言频率规则',
|
||||
addButtonPlacement: 'top',
|
||||
@@ -140,6 +233,7 @@ export const ChatTalkValueRulesHook = createListItemEditorHook({
|
||||
collapseLabel: '折叠规则',
|
||||
helperText: '可按平台/聊天流/时段分别配置发言频率,留空表示全局。',
|
||||
emptyText: '尚未配置任何规则,将使用全局默认频率。',
|
||||
collapseButtonDisplay: 'icon',
|
||||
fieldRows: [
|
||||
['platform', 'item_id', 'rule_type'],
|
||||
['time', 'value'],
|
||||
|
||||
@@ -11,6 +11,7 @@ export type {
|
||||
UseAutoSaveReturnGeneric,
|
||||
} from './useAutoSave'
|
||||
export {
|
||||
AliasNamesHook,
|
||||
BotPlatformsHook,
|
||||
BotPlatformAccountsHook,
|
||||
ChatPromptsHook,
|
||||
@@ -21,6 +22,7 @@ export {
|
||||
HiddenFieldHook,
|
||||
MCPRootItemsHook,
|
||||
MCPServersHook,
|
||||
MultipleReplyStyleHook,
|
||||
RegexRulesHook,
|
||||
} from './complexFieldHooks'
|
||||
export { ChatSectionHook } from './ChatSectionHook'
|
||||
|
||||
@@ -366,8 +366,9 @@ export const ExpressionSection = React.memo(function ExpressionSection({
|
||||
<LearningRulePreview rule={rule} />
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button size="sm" variant="ghost">
|
||||
<Button size="icon" variant="ghost" aria-label={`删除学习规则 ${index + 1}`}>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
<span className="sr-only">删除</span>
|
||||
</Button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
|
||||
@@ -1312,7 +1312,7 @@ function ModelConfigPageContent() {
|
||||
<p className="text-sm text-muted-foreground">
|
||||
管理 AI 模型厂商的 API 配置
|
||||
</p>
|
||||
<div className="flex flex-col gap-2 sm:flex-row sm:items-center">
|
||||
<div className="hidden">
|
||||
{selectedProviders.size > 0 && (
|
||||
<Button
|
||||
onClick={openProviderBatchDeleteDialog}
|
||||
@@ -1346,6 +1346,37 @@ function ModelConfigPageContent() {
|
||||
testingProviders={testingProviders}
|
||||
testResults={testResults}
|
||||
selectedProviders={selectedProviders}
|
||||
toolbarActions={(
|
||||
<>
|
||||
{selectedProviders.size > 0 && (
|
||||
<Button
|
||||
onClick={openProviderBatchDeleteDialog}
|
||||
size="sm"
|
||||
variant="destructive"
|
||||
className="w-full sm:w-auto"
|
||||
>
|
||||
<Trash2 className="mr-2 h-4 w-4" strokeWidth={2} fill="none" />
|
||||
<span className="text-sm">批量删除 ({selectedProviders.size})</span>
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
onClick={handleTestAllProviderConnections}
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="w-full sm:w-auto"
|
||||
disabled={apiProviders.length === 0 || testingProviders.size > 0}
|
||||
>
|
||||
<Zap className="mr-2 h-4 w-4" />
|
||||
<span className="text-sm">
|
||||
{testingProviders.size > 0 ? `测试中 (${testingProviders.size})` : '测试全部连接'}
|
||||
</span>
|
||||
</Button>
|
||||
<Button onClick={() => openProviderDialog(null, null)} size="sm" variant="outline" className="w-full sm:w-auto" data-tour="add-provider-button">
|
||||
<Plus className="mr-2 h-4 w-4" strokeWidth={2} fill="none" />
|
||||
<span className="text-sm">添加厂商</span>
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
onEdit={openProviderDialog}
|
||||
onDelete={openProviderDeleteDialog}
|
||||
onTest={handleTestProviderConnection}
|
||||
@@ -1359,7 +1390,7 @@ function ModelConfigPageContent() {
|
||||
<p className="text-sm text-muted-foreground">
|
||||
配置可用的模型列表
|
||||
</p>
|
||||
<div className="flex gap-2 w-full sm:w-auto">
|
||||
<div className="hidden">
|
||||
{selectedModels.size > 0 && (
|
||||
<Button
|
||||
onClick={openBatchDeleteDialog}
|
||||
@@ -1379,7 +1410,7 @@ function ModelConfigPageContent() {
|
||||
</div>
|
||||
|
||||
{/* 搜索框 */}
|
||||
<div className="flex flex-col sm:flex-row items-start sm:items-center gap-2">
|
||||
<div className="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div className="relative w-full sm:flex-1 sm:max-w-sm">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
||||
<Input
|
||||
@@ -1394,9 +1425,27 @@ function ModelConfigPageContent() {
|
||||
找到 {filteredModels.length} 个结果
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 模型列表 - 移动端卡片视图 */}
|
||||
<div className="flex w-full flex-col gap-2 sm:ml-auto sm:w-auto sm:flex-row sm:items-center sm:justify-end">
|
||||
{selectedModels.size > 0 && (
|
||||
<Button
|
||||
onClick={openBatchDeleteDialog}
|
||||
size="sm"
|
||||
variant="destructive"
|
||||
className="w-full sm:w-auto"
|
||||
>
|
||||
<Trash2 className="mr-2 h-4 w-4" strokeWidth={2} fill="none" />
|
||||
<span className="text-sm">批量删除 ({selectedModels.size})</span>
|
||||
</Button>
|
||||
)}
|
||||
<Button onClick={() => openEditDialog(null, null)} size="sm" variant="outline" className="w-full sm:w-auto" data-tour="add-model-button">
|
||||
<Plus className="mr-2 h-4 w-4" strokeWidth={2} fill="none" />
|
||||
<span className="text-sm">添加模型</span>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ModelCardList
|
||||
paginatedModels={paginatedModels}
|
||||
allModels={models}
|
||||
@@ -1834,8 +1883,8 @@ function ModelConfigPageContent() {
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="price_in">输入价格 (¥/M token)</Label>
|
||||
<div className="flex items-center gap-3">
|
||||
<Label htmlFor="price_in" className="w-36 shrink-0">输入价格 (¥/M token)</Label>
|
||||
<Input
|
||||
id="price_in"
|
||||
type="number"
|
||||
@@ -1851,11 +1900,12 @@ function ModelConfigPageContent() {
|
||||
)
|
||||
}}
|
||||
placeholder="默认: 0"
|
||||
className="flex-1"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="price_out">输出价格 (¥/M token)</Label>
|
||||
<div className="flex items-center gap-3">
|
||||
<Label htmlFor="price_out" className="w-36 shrink-0">输出价格 (¥/M token)</Label>
|
||||
<Input
|
||||
id="price_out"
|
||||
type="number"
|
||||
@@ -1871,6 +1921,7 @@ function ModelConfigPageContent() {
|
||||
)
|
||||
}}
|
||||
placeholder="默认: 0"
|
||||
className="flex-1"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1896,8 +1947,8 @@ function ModelConfigPageContent() {
|
||||
</div>
|
||||
|
||||
{editingModel?.cache && (
|
||||
<div className="grid gap-2 border-t pt-4">
|
||||
<Label htmlFor="cache_price_in">缓存输入价格 (¥/M token)</Label>
|
||||
<div className="flex items-center gap-3 border-t pt-4">
|
||||
<Label htmlFor="cache_price_in" className="w-40 shrink-0">缓存输入价格 (¥/M token)</Label>
|
||||
<Input
|
||||
id="cache_price_in"
|
||||
type="number"
|
||||
@@ -1913,6 +1964,7 @@ function ModelConfigPageContent() {
|
||||
)
|
||||
}}
|
||||
placeholder="默认: 0"
|
||||
className="flex-1"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useCallback, useMemo, useState } from 'react'
|
||||
import { useCallback, useMemo, useState, type ReactNode } from 'react'
|
||||
import type { TestConnectionResult } from '@/lib/config-api'
|
||||
import { AlertCircle, CheckCircle2, ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight, Loader2, Pencil, Search, Trash2, XCircle, Zap } from 'lucide-react'
|
||||
|
||||
@@ -18,6 +18,7 @@ interface ProviderListProps {
|
||||
testingProviders: Set<string>
|
||||
testResults: Map<string, TestConnectionResult>
|
||||
selectedProviders: Set<number>
|
||||
toolbarActions?: ReactNode
|
||||
onEdit: (provider: APIProvider, index: number) => void
|
||||
onDelete: (index: number) => void
|
||||
onTest: (name: string) => void
|
||||
@@ -30,6 +31,7 @@ export function ProviderList({
|
||||
testingProviders,
|
||||
testResults,
|
||||
selectedProviders,
|
||||
toolbarActions,
|
||||
onEdit,
|
||||
onDelete,
|
||||
onTest,
|
||||
@@ -125,20 +127,27 @@ export function ProviderList({
|
||||
return (
|
||||
<>
|
||||
{/* 搜索框 */}
|
||||
<div className="flex flex-col sm:flex-row items-start sm:items-center gap-2 mb-4">
|
||||
<div className="relative w-full sm:flex-1 sm:max-w-sm">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
||||
<Input
|
||||
placeholder="搜索提供商名称、URL 或类型..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="pl-9"
|
||||
/>
|
||||
<div className="mb-4 flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div className="flex w-full flex-col gap-2 sm:flex-row sm:items-center">
|
||||
<div className="relative w-full sm:flex-1 sm:max-w-sm">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
||||
<Input
|
||||
placeholder="搜索提供商名称、URL 或类型..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="pl-9"
|
||||
/>
|
||||
</div>
|
||||
{searchQuery && (
|
||||
<p className="text-sm text-muted-foreground whitespace-nowrap">
|
||||
找到 {filteredProviders.length} 个结果
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
{searchQuery && (
|
||||
<p className="text-sm text-muted-foreground whitespace-nowrap">
|
||||
找到 {filteredProviders.length} 个结果
|
||||
</p>
|
||||
{toolbarActions && (
|
||||
<div className="flex w-full flex-col gap-2 sm:w-auto sm:flex-row sm:items-center sm:justify-end">
|
||||
{toolbarActions}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -745,7 +745,7 @@ function ModelProviderConfigPageContent() {
|
||||
<h1 className="text-2xl sm:text-3xl font-bold">模型厂商设置</h1>
|
||||
<p className="text-muted-foreground mt-1 sm:mt-2 text-sm sm:text-base">管理 AI 模型厂商的 API 配置</p>
|
||||
</div>
|
||||
<div className="flex flex-col sm:flex-row gap-2">
|
||||
<div className="hidden">
|
||||
{selectedProviders.size > 0 && (
|
||||
<Button
|
||||
onClick={openBatchDeleteDialog}
|
||||
@@ -838,6 +838,82 @@ function ModelProviderConfigPageContent() {
|
||||
testingProviders={testingProviders}
|
||||
testResults={testResults}
|
||||
selectedProviders={selectedProviders}
|
||||
toolbarActions={(
|
||||
<>
|
||||
{selectedProviders.size > 0 && (
|
||||
<Button
|
||||
onClick={openBatchDeleteDialog}
|
||||
size="sm"
|
||||
variant="destructive"
|
||||
className="w-full sm:w-auto"
|
||||
>
|
||||
<Trash2 className="mr-2 h-4 w-4" strokeWidth={2} fill="none" />
|
||||
<span className="text-sm">批量删除 ({selectedProviders.size})</span>
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
onClick={handleTestAllConnections}
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="w-full sm:w-auto"
|
||||
disabled={providers.length === 0 || testingProviders.size > 0}
|
||||
>
|
||||
<Zap className="mr-2 h-4 w-4" />
|
||||
<span className="text-sm">
|
||||
{testingProviders.size > 0 ? `测试中 (${testingProviders.size})` : '测试全部连接'}
|
||||
</span>
|
||||
</Button>
|
||||
<Button onClick={() => openEditDialog(null, null)} size="sm" className="w-full sm:w-auto" data-tour="add-provider-button">
|
||||
<Plus className="mr-2 h-4 w-4" strokeWidth={2} fill="none" />
|
||||
<span className="text-sm">添加厂商</span>
|
||||
</Button>
|
||||
<Button
|
||||
onClick={saveConfig}
|
||||
disabled={saving || autoSaving || !hasUnsavedChanges || isRestarting}
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="w-full sm:w-auto sm:min-w-[120px]"
|
||||
>
|
||||
<Save className="mr-2 h-4 w-4" strokeWidth={2} fill="none" />
|
||||
<span className="text-sm">
|
||||
{saving ? '保存中...' : autoSaving ? '自动保存中...' : hasUnsavedChanges ? '保存配置' : '已保存'}
|
||||
</span>
|
||||
</Button>
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button
|
||||
disabled={saving || autoSaving || isRestarting}
|
||||
size="sm"
|
||||
className="w-full sm:w-auto sm:min-w-[120px]"
|
||||
>
|
||||
<Power className="mr-2 h-4 w-4" />
|
||||
{isRestarting ? '重启中...' : hasUnsavedChanges ? '保存并重启' : '重启麦麦'}
|
||||
</Button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>确认重启麦麦?</AlertDialogTitle>
|
||||
<AlertDialogDescription asChild>
|
||||
<div>
|
||||
<p>
|
||||
{hasUnsavedChanges
|
||||
? '当前有未保存的配置更改。确认后会先保存配置,然后重启麦麦使新配置生效。'
|
||||
: '即将重启麦麦主程序。配置将在重启后生效。'
|
||||
}
|
||||
</p>
|
||||
</div>
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>取消</AlertDialogCancel>
|
||||
<AlertDialogAction onClick={hasUnsavedChanges ? handleSaveAndRestart : handleRestart}>
|
||||
{hasUnsavedChanges ? '保存并重启' : '确认重启'}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</>
|
||||
)}
|
||||
onEdit={openEditDialog}
|
||||
onDelete={openDeleteDialog}
|
||||
onTest={handleTestConnection}
|
||||
|
||||
Reference in New Issue
Block a user