feat:为webui配置名提供中文翻译,并修改优化布局

This commit is contained in:
SengokuCola
2026-05-06 15:45:50 +08:00
parent b3d16a5705
commit 8c73424583
24 changed files with 538 additions and 132 deletions

7
.gitignore vendored
View File

@@ -28,7 +28,6 @@ nonebot-maibot-adapter/
MaiMBot-LPMM MaiMBot-LPMM
*.zip *.zip
run_bot.bat run_bot.bat
run_na.bat
run_all_in_wt.bat run_all_in_wt.bat
run.bat run.bat
log_debug/ log_debug/
@@ -37,10 +36,6 @@ run_amds.bat
run_none.bat run_none.bat
docs-mai/ docs-mai/
run.py run.py
message_queue_content.txt
message_queue_content.bat
message_queue_window.bat
message_queue_window.txt
queue_update.txt queue_update.txt
start_saka.bat start_saka.bat
.env .env
@@ -50,8 +45,6 @@ start_all.bat
config/bot_config_dev.toml config/bot_config_dev.toml
config/bot_config.toml config/bot_config.toml
config/bot_config.toml.bak config/bot_config.toml.bak
config/lpmm_config.toml
config/lpmm_config.toml.bak
template/compare/bot_config_template.toml template/compare/bot_config_template.toml
template/compare/model_config_template.toml template/compare/model_config_template.toml
# CLAUDE.md # CLAUDE.md

View File

@@ -1,7 +1,7 @@
{ {
"name": "maibot-dashboard", "name": "maibot-dashboard",
"private": true, "private": true,
"version": "1.0.5", "version": "1.0.6",
"type": "module", "type": "module",
"main": "./out/main/index.js", "main": "./out/main/index.js",
"scripts": { "scripts": {

View File

@@ -24,6 +24,7 @@ export interface DynamicConfigFormProps {
/** 嵌套层级0 = tab 内容层1 = section 内容层2+ = 更深嵌套 */ /** 嵌套层级0 = tab 内容层1 = section 内容层2+ = 更深嵌套 */
level?: number level?: number
advancedVisible?: boolean advancedVisible?: boolean
sectionColumns?: 1 | 2
} }
function buildFieldPath(basePath: string, fieldName: string) { function buildFieldPath(basePath: string, fieldName: string) {
@@ -126,6 +127,7 @@ function DynamicConfigSection({
hooks={hooks} hooks={hooks}
level={level} level={level}
advancedVisible={hasAdvanced ? advancedVisible : undefined} advancedVisible={hasAdvanced ? advancedVisible : undefined}
sectionColumns={1}
/> />
</CardContent> </CardContent>
</Card> </Card>
@@ -150,6 +152,7 @@ export const DynamicConfigForm: React.FC<DynamicConfigFormProps> = ({
hooks = fieldHooks, hooks = fieldHooks,
level = 0, level = 0,
advancedVisible, advancedVisible,
sectionColumns = 1,
}) => { }) => {
const [localAdvancedVisible, setLocalAdvancedVisible] = React.useState(false) const [localAdvancedVisible, setLocalAdvancedVisible] = React.useState(false)
const resolvedAdvancedVisible = advancedVisible ?? localAdvancedVisible const resolvedAdvancedVisible = advancedVisible ?? localAdvancedVisible
@@ -161,10 +164,12 @@ export const DynamicConfigForm: React.FC<DynamicConfigFormProps> = ({
const renderField = (field: FieldSchema) => { const renderField = (field: FieldSchema) => {
const fieldPath = buildFieldPath(basePath, field.name) const fieldPath = buildFieldPath(basePath, field.name)
const nestedSchema = schema.nested?.[field.name]
if (hooks.has(fieldPath)) { if (hooks.has(fieldPath)) {
const hookEntry = hooks.get(fieldPath) const hookEntry = hooks.get(fieldPath)
if (!hookEntry) return null if (!hookEntry) return null
if (hookEntry.type === 'hidden') return null
const HookComponent = hookEntry.component const HookComponent = hookEntry.component
@@ -174,7 +179,9 @@ export const DynamicConfigForm: React.FC<DynamicConfigFormProps> = ({
fieldPath={fieldPath} fieldPath={fieldPath}
value={values[field.name]} value={values[field.name]}
onChange={(v) => onChange(field.name, v)} onChange={(v) => onChange(field.name, v)}
onParentChange={onChange}
schema={field} schema={field}
nestedSchema={nestedSchema}
parentValues={values} parentValues={values}
/> />
) )
@@ -185,7 +192,9 @@ export const DynamicConfigForm: React.FC<DynamicConfigFormProps> = ({
fieldPath={fieldPath} fieldPath={fieldPath}
value={values[field.name]} value={values[field.name]}
onChange={(v) => onChange(field.name, v)} onChange={(v) => onChange(field.name, v)}
onParentChange={onChange}
schema={field} schema={field}
nestedSchema={nestedSchema}
parentValues={values} parentValues={values}
> >
<DynamicField <DynamicField
@@ -208,11 +217,27 @@ export const DynamicConfigForm: React.FC<DynamicConfigFormProps> = ({
) )
} }
const topLevelFields = schema.fields.filter( const shouldRenderFieldInline = (field: FieldSchema) => {
(field) => !schema.nested?.[field.name], const fieldPath = buildFieldPath(basePath, field.name)
if (hooks.get(fieldPath)?.type === 'hidden') {
return false
}
if (!schema.nested?.[field.name]) {
return true
}
return hooks.get(fieldPath)?.type === 'replace'
}
const inlineFields = schema.fields.filter(shouldRenderFieldInline)
const inlineNestedFieldNames = new Set(
inlineFields
.filter((field) => Boolean(schema.nested?.[field.name]))
.map((field) => field.name),
) )
const normalFields = topLevelFields.filter((field) => !field.advanced) const normalFields = inlineFields.filter((field) => !field.advanced)
const advancedFields = topLevelFields.filter((field) => field.advanced) const advancedFields = inlineFields.filter((field) => field.advanced)
const visibleFields = resolvedAdvancedVisible const visibleFields = resolvedAdvancedVisible
? [...normalFields, ...advancedFields] ? [...normalFields, ...advancedFields]
: normalFields : normalFields
@@ -244,23 +269,32 @@ export const DynamicConfigForm: React.FC<DynamicConfigFormProps> = ({
return rows return rows
} }
const renderRows = (rows: FieldSchema[][]) => (
<>
{rows.map((row) => (
row.length > 1 ? (
<div
key={row.map((field) => field.name).join('|')}
className="grid gap-4 py-1 md:grid-cols-[repeat(var(--field-row-count),minmax(0,1fr))]"
style={{ '--field-row-count': row.length } as React.CSSProperties}
>
{row.map((field) => (
<div key={field.name}>{renderField(field)}</div>
))}
</div>
) : (
<div key={row[0].name} className="py-1">{renderField(row[0])}</div>
)
))}
</>
)
const renderFieldList = (fields: FieldSchema[]) => ( const renderFieldList = (fields: FieldSchema[]) => (
<> <>
{groupFieldsByRow(fields).map((row, index) => ( {groupFieldsByRow(fields).map((row, index) => (
<React.Fragment key={row.map((field) => field.name).join('|')}> <React.Fragment key={row.map((field) => field.name).join('|')}>
{index > 0 && <Separator className="my-2 bg-border/50" />} {index > 0 && <Separator className="my-2 bg-border/50" />}
{row.length > 1 ? ( {renderRows([row])}
<div
className="grid gap-4 py-1 md:grid-cols-[repeat(var(--field-row-count),minmax(0,1fr))]"
style={{ '--field-row-count': row.length } as React.CSSProperties}
>
{row.map((field) => (
<div key={field.name}>{renderField(field)}</div>
))}
</div>
) : (
<div className="py-1">{renderField(row[0])}</div>
)}
</React.Fragment> </React.Fragment>
))} ))}
</> </>
@@ -268,7 +302,7 @@ export const DynamicConfigForm: React.FC<DynamicConfigFormProps> = ({
return ( return (
<div className="space-y-6"> <div className="space-y-6">
{topLevelFields.length > 0 && ( {inlineFields.length > 0 && (
<div> <div>
{advancedVisible === undefined && advancedFields.length > 0 && ( {advancedVisible === undefined && advancedFields.length > 0 && (
<div className="flex justify-end pb-2"> <div className="flex justify-end pb-2">
@@ -283,7 +317,9 @@ export const DynamicConfigForm: React.FC<DynamicConfigFormProps> = ({
)} )}
{schema.nested && {schema.nested &&
Object.entries(schema.nested) (() => {
const nestedSections = Object.entries(schema.nested)
.filter(([key]) => !inlineNestedFieldNames.has(key))
.map(([key, nestedSchema]) => { .map(([key, nestedSchema]) => {
const nestedField = fieldMap.get(key) const nestedField = fieldMap.get(key)
const nestedFieldPath = buildFieldPath(basePath, key) const nestedFieldPath = buildFieldPath(basePath, key)
@@ -299,8 +335,9 @@ export const DynamicConfigForm: React.FC<DynamicConfigFormProps> = ({
<HookComponent <HookComponent
fieldPath={nestedFieldPath} fieldPath={nestedFieldPath}
value={values[key]} value={values[key]}
onChange={(v) => onChange(key, v)} onChange={(v) => onChange(key, v)}
schema={nestedField ?? nestedSchema} onParentChange={onChange}
schema={nestedField ?? nestedSchema}
nestedSchema={nestedSchema} nestedSchema={nestedSchema}
parentValues={values} parentValues={values}
/> />
@@ -314,6 +351,7 @@ export const DynamicConfigForm: React.FC<DynamicConfigFormProps> = ({
fieldPath={nestedFieldPath} fieldPath={nestedFieldPath}
value={values[key]} value={values[key]}
onChange={(v) => onChange(key, v)} onChange={(v) => onChange(key, v)}
onParentChange={onChange}
schema={nestedField ?? nestedSchema} schema={nestedField ?? nestedSchema}
nestedSchema={nestedSchema} nestedSchema={nestedSchema}
parentValues={values} parentValues={values}
@@ -325,6 +363,7 @@ export const DynamicConfigForm: React.FC<DynamicConfigFormProps> = ({
basePath={nestedFieldPath} basePath={nestedFieldPath}
hooks={hooks} hooks={hooks}
level={level + 1} level={level + 1}
sectionColumns={1}
/> />
</HookComponent> </HookComponent>
</div> </div>
@@ -376,11 +415,27 @@ export const DynamicConfigForm: React.FC<DynamicConfigFormProps> = ({
basePath={nestedFieldPath} basePath={nestedFieldPath}
hooks={hooks} hooks={hooks}
level={level + 1} level={level + 1}
sectionColumns={1}
/> />
</CardContent> </CardContent>
</Card> </Card>
) )
})} })
const visibleNestedSections = nestedSections.filter(
(section): section is React.ReactElement => Boolean(section),
)
if (level === 0 && sectionColumns === 2 && visibleNestedSections.length > 1) {
return (
<div className="grid gap-4 md:grid-cols-2">
{visibleNestedSections}
</div>
)
}
return visibleNestedSections
})()}
</div> </div>
) )
} }

View File

@@ -1,5 +1,6 @@
import * as React from "react" import * as React from "react"
import * as LucideIcons from "lucide-react" import * as LucideIcons from "lucide-react"
import { useTranslation } from "react-i18next"
import { Input } from "@/components/ui/input" import { Input } from "@/components/ui/input"
import { KeyValueEditor } from "@/components/ui/key-value-editor" import { KeyValueEditor } from "@/components/ui/key-value-editor"
@@ -15,6 +16,7 @@ import {
TooltipTrigger, TooltipTrigger,
} from "@/components/ui/tooltip" } from "@/components/ui/tooltip"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
import { resolveFieldLabel } from "@/lib/config-label"
import type { FieldSchema } from "@/types/config-schema" import type { FieldSchema } from "@/types/config-schema"
export interface DynamicFieldProps { export interface DynamicFieldProps {
@@ -37,6 +39,8 @@ export const DynamicField: React.FC<DynamicFieldProps> = ({
value, value,
onChange, onChange,
}) => { }) => {
const { i18n } = useTranslation()
const fieldLabel = resolveFieldLabel(schema, i18n.language)
const isNumericField = schema.type === 'integer' || schema.type === 'number' const isNumericField = schema.type === 'integer' || schema.type === 'number'
const parseNumericValue = (rawValue: unknown, fallbackValue: unknown = 0) => { const parseNumericValue = (rawValue: unknown, fallbackValue: unknown = 0) => {
@@ -126,17 +130,17 @@ export const DynamicField: React.FC<DynamicFieldProps> = ({
const inlineDescription = hasOptionDescriptions ? '' : schema.description const inlineDescription = hasOptionDescriptions ? '' : schema.description
const renderFieldHeader = () => ( const renderFieldHeader = () => (
<div className="flex flex-wrap items-center gap-x-2 gap-y-1"> <div className="flex min-w-0 flex-wrap items-center gap-x-2 gap-y-1">
<Label <Label
className={cn( className={cn(
"inline-flex min-h-7 items-center gap-1.5 rounded-md border px-2 py-1 text-sm font-medium shadow-sm", "inline-flex shrink-0 items-center gap-1.5 whitespace-nowrap text-[15px] font-semibold leading-6",
schema.advanced schema.advanced
? "border-amber-300 bg-amber-50 text-amber-950 dark:border-amber-500/60 dark:bg-amber-500/15 dark:text-amber-100" ? "text-amber-800 dark:text-amber-200"
: "bg-muted/60 text-foreground", : "text-foreground",
)} )}
> >
{renderIcon()} {renderIcon()}
<span className="break-all">{schema.label}</span> <span>{fieldLabel}</span>
{schema.required && <span className="text-destructive">*</span>} {schema.required && <span className="text-destructive">*</span>}
</Label> </Label>
{inlineDescription && ( {inlineDescription && (
@@ -357,7 +361,7 @@ export const DynamicField: React.FC<DynamicFieldProps> = ({
return ( return (
<Select value={strValue} onValueChange={(val) => onChange(val)}> <Select value={strValue} onValueChange={(val) => onChange(val)}>
<SelectTrigger> <SelectTrigger>
<SelectValue placeholder={`Select ${schema.label}`} /> <SelectValue placeholder={`Select ${fieldLabel}`} />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
{hasOptionDescriptions ? ( {hasOptionDescriptions ? (
@@ -415,13 +419,13 @@ export const DynamicField: React.FC<DynamicFieldProps> = ({
if (supportsInlineRight) { if (supportsInlineRight) {
return ( return (
<div <div
className="flex flex-col gap-2 py-2 sm:flex-row sm:items-center sm:justify-between" className="flex flex-col gap-2 py-2 sm:flex-row sm:items-center"
style={{ '--field-input-width': schema['x-input-width'] ?? '12rem' } as React.CSSProperties} style={{ '--field-input-width': schema['x-input-width'] ?? '12rem' } as React.CSSProperties}
> >
<div className="min-w-0 flex-1"> <div className="shrink-0">
{renderFieldHeader()} {renderFieldHeader()}
</div> </div>
<div className="w-full shrink-0 sm:w-[var(--field-input-width)]"> <div className="min-w-20 flex-1 sm:ml-auto sm:max-w-[var(--field-input-width)]">
{renderInputComponent()} {renderInputComponent()}
</div> </div>
</div> </div>

View File

@@ -130,7 +130,7 @@ export function Layout({ children }: LayoutProps) {
key="settings-sidebar" key="settings-sidebar"
className="relative z-40 hidden shrink-0 lg:block" className="relative z-40 hidden shrink-0 lg:block"
initial={{ width: 0, opacity: 0 }} initial={{ width: 0, opacity: 0 }}
animate={{ width: sidebarOpen ? 256 : 64, opacity: 1 }} animate={{ width: sidebarOpen ? 240 : 64, opacity: 1 }}
exit={{ width: 0, opacity: 0 }} exit={{ width: 0, opacity: 0 }}
transition={{ transition={{
type: 'spring', type: 'spring',

View File

@@ -32,8 +32,8 @@ export function Sidebar({
'fixed inset-y-0 left-0 z-50 isolate flex flex-col border-r transition-all duration-300 lg:relative lg:z-0 lg:h-full', 'fixed inset-y-0 left-0 z-50 isolate flex flex-col border-r transition-all duration-300 lg:relative lg:z-0 lg:h-full',
inheritsPageBackground ? 'bg-transparent' : 'bg-card', inheritsPageBackground ? 'bg-transparent' : 'bg-card',
// 移动端始终显示完整宽度,桌面端根据 sidebarOpen 切换 // 移动端始终显示完整宽度,桌面端根据 sidebarOpen 切换
'w-64 lg:w-auto', 'w-60 lg:w-auto',
sidebarOpen ? 'lg:w-64' : 'lg:w-16', sidebarOpen ? 'lg:w-60' : 'lg:w-16',
mobileMenuOpen ? 'translate-x-0' : '-translate-x-full lg:translate-x-0' mobileMenuOpen ? 'translate-x-0' : '-translate-x-full lg:translate-x-0'
)} )}
> >

View File

@@ -1,5 +1,5 @@
import { useState, useCallback, useEffect, useMemo, useRef } from 'react' import { useState, useCallback, useEffect, useMemo, useRef } from 'react'
import { Search } from 'lucide-react' import { FileText, Search, SlidersHorizontal } from 'lucide-react'
import { useNavigate } from '@tanstack/react-router' import { useNavigate } from '@tanstack/react-router'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import type { LucideProps } from 'lucide-react' import type { LucideProps } from 'lucide-react'
@@ -15,7 +15,10 @@ import { Input } from '@/components/ui/input'
import { ShortcutKbd } from '@/components/ui/kbd' import { ShortcutKbd } from '@/components/ui/kbd'
import { menuSections } from '@/components/layout/constants' import { menuSections } from '@/components/layout/constants'
import { registeredRoutePaths } from '@/router' import { registeredRoutePaths } from '@/router'
import { getBotConfigSchema, getModelConfigSchema } from '@/lib/config-api'
import { getAllLocalizedText, resolveFieldLabel } from '@/lib/config-label'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
import type { ConfigSchema, FieldSchema } from '@/types/config-schema'
interface SearchDialogProps { interface SearchDialogProps {
open: boolean open: boolean
@@ -23,19 +26,115 @@ interface SearchDialogProps {
} }
interface SearchItem { interface SearchItem {
id: string
icon: React.ComponentType<LucideProps> icon: React.ComponentType<LucideProps>
title: string title: string
description: string description: string
path: string path: string
category: string category: string
keywords: string
}
function resolveSchemaTitle(schema: ConfigSchema, fallback: string) {
return schema.uiLabel || schema.classDoc || schema.className || fallback
}
function unwrapConfigSchema(payload: unknown): ConfigSchema | null {
if (!payload || typeof payload !== 'object') {
return null
}
if ('fields' in payload) {
return payload as ConfigSchema
}
if ('schema' in payload) {
const schema = (payload as { schema?: unknown }).schema
if (schema && typeof schema === 'object' && 'fields' in schema) {
return schema as ConfigSchema
}
}
return null
}
function getModelConfigPath(fieldPath: string) {
return fieldPath.startsWith('api_providers') ? '/config/modelProvider' : '/config/model'
}
function buildFieldSearchText(field: FieldSchema, fieldPath: string, sectionTitle: string, language?: string) {
const options = field.options?.join(' ') ?? ''
const optionDescriptions = field['x-option-descriptions']
? Object.entries(field['x-option-descriptions'])
.map(([key, value]) => `${key} ${value}`)
.join(' ')
: ''
return [
resolveFieldLabel(field, language),
...getAllLocalizedText(field.label),
field.name,
fieldPath,
field.description,
sectionTitle,
field.type,
options,
optionDescriptions,
].join(' ')
}
function collectConfigFields(
schema: ConfigSchema,
sourceLabel: string,
basePath: string,
routePath: (fieldPath: string) => string,
language?: string,
): SearchItem[] {
const items: SearchItem[] = []
const walk = (currentSchema: ConfigSchema, pathPrefix: string, sectionTrail: string[]) => {
const sectionTitle = resolveSchemaTitle(currentSchema, sourceLabel)
const nextTrail = [...sectionTrail, sectionTitle].filter(Boolean)
for (const field of currentSchema.fields) {
const fieldPath = pathPrefix ? `${pathPrefix}.${field.name}` : field.name
const nestedSchema = currentSchema.nested?.[field.name]
const fieldTitle = resolveFieldLabel(field, language)
const description = field.description || nextTrail.join(' / ') || fieldPath
const fullPath = basePath ? `${basePath}.${fieldPath}` : fieldPath
const route = routePath(fullPath)
items.push({
id: `config:${sourceLabel}:${fullPath}`,
icon: sourceLabel === '模型配置' ? SlidersHorizontal : FileText,
title: fieldTitle,
description: `${sourceLabel} / ${nextTrail.join(' / ')} / ${fullPath} · ${description}`,
path: route,
category: '配置项',
keywords: buildFieldSearchText(field, fullPath, nextTrail.join(' / '), language),
})
if (nestedSchema) {
walk(nestedSchema, fieldPath, nextTrail)
}
}
}
walk(schema, '', [])
return items
} }
export function SearchDialog({ open, onOpenChange }: SearchDialogProps) { export function SearchDialog({ open, onOpenChange }: SearchDialogProps) {
const [searchQuery, setSearchQuery] = useState('') const [searchQuery, setSearchQuery] = useState('')
const [selectedIndex, setSelectedIndex] = useState(0) const [selectedIndex, setSelectedIndex] = useState(0)
const [configSearchItems, setConfigSearchItems] = useState<SearchItem[]>([])
const inputRef = useRef<HTMLInputElement>(null) const inputRef = useRef<HTMLInputElement>(null)
const navigate = useNavigate() const navigate = useNavigate()
const { t } = useTranslation() const { i18n, t } = useTranslation()
useEffect(() => {
setConfigSearchItems([])
}, [i18n.language])
useEffect(() => { useEffect(() => {
if (!open) { if (!open) {
@@ -49,29 +148,91 @@ export function SearchDialog({ open, onOpenChange }: SearchDialogProps) {
return () => window.cancelAnimationFrame(frameId) return () => window.cancelAnimationFrame(frameId)
}, [open]) }, [open])
useEffect(() => {
if (!open || configSearchItems.length > 0) {
return
}
let cancelled = false
const loadConfigSearchItems = async () => {
const [botSchemaResult, modelSchemaResult] = await Promise.all([
getBotConfigSchema(),
getModelConfigSchema(),
])
if (cancelled) {
return
}
const nextItems: SearchItem[] = []
if (botSchemaResult.success) {
const botSchema = unwrapConfigSchema(botSchemaResult.data)
if (botSchema) {
nextItems.push(...collectConfigFields(
botSchema,
'Bot 配置',
'',
() => '/config/bot',
i18n.language,
))
}
}
if (modelSchemaResult.success) {
const modelSchema = unwrapConfigSchema(modelSchemaResult.data)
if (modelSchema) {
nextItems.push(...collectConfigFields(
modelSchema,
'模型配置',
'',
getModelConfigPath,
i18n.language,
))
}
}
setConfigSearchItems(nextItems)
}
loadConfigSearchItems().catch(() => {
if (!cancelled) {
setConfigSearchItems([])
}
})
return () => {
cancelled = true
}
}, [configSearchItems.length, i18n.language, open])
const searchItems: SearchItem[] = useMemo( const searchItems: SearchItem[] = useMemo(
() => () =>
menuSections.flatMap((section) => menuSections.flatMap((section) =>
section.items section.items
.filter((item) => registeredRoutePaths.has(item.path)) .filter((item) => registeredRoutePaths.has(item.path))
.map((item) => ({ .map((item) => ({
id: `route:${item.path}`,
icon: item.icon, icon: item.icon,
title: t(item.label), title: t(item.label),
description: item.searchDescription ? t(item.searchDescription) : item.path, description: item.searchDescription ? t(item.searchDescription) : item.path,
path: item.path, path: item.path,
category: t(section.title), category: t(section.title),
keywords: [
t(item.label),
item.path,
item.searchDescription ? t(item.searchDescription) : '',
t(section.title),
].join(' '),
})) }))
), ),
[t] [t]
) )
// 过滤搜索结果 // 过滤搜索结果
const filteredItems = searchItems.filter( const normalizedQuery = searchQuery.trim().toLowerCase()
(item) => const filteredItems = (normalizedQuery ? [...searchItems, ...configSearchItems] : searchItems)
item.title.toLowerCase().includes(searchQuery.toLowerCase()) || .filter((item) => item.keywords.toLowerCase().includes(normalizedQuery))
item.description.toLowerCase().includes(searchQuery.toLowerCase()) || .slice(0, 80)
item.category.toLowerCase().includes(searchQuery.toLowerCase())
)
// 导航到页面 // 导航到页面
const handleNavigate = useCallback((path: string) => { const handleNavigate = useCallback((path: string) => {
@@ -87,9 +248,11 @@ export function SearchDialog({ open, onOpenChange }: SearchDialogProps) {
(e: React.KeyboardEvent) => { (e: React.KeyboardEvent) => {
if (e.key === 'ArrowDown') { if (e.key === 'ArrowDown') {
e.preventDefault() e.preventDefault()
if (filteredItems.length === 0) return
setSelectedIndex((prev) => (prev + 1) % filteredItems.length) setSelectedIndex((prev) => (prev + 1) % filteredItems.length)
} else if (e.key === 'ArrowUp') { } else if (e.key === 'ArrowUp') {
e.preventDefault() e.preventDefault()
if (filteredItems.length === 0) return
setSelectedIndex((prev) => (prev - 1 + filteredItems.length) % filteredItems.length) setSelectedIndex((prev) => (prev - 1 + filteredItems.length) % filteredItems.length)
} else if (e.key === 'Enter' && filteredItems[selectedIndex]) { } else if (e.key === 'Enter' && filteredItems[selectedIndex]) {
e.preventDefault() e.preventDefault()
@@ -128,7 +291,7 @@ export function SearchDialog({ open, onOpenChange }: SearchDialogProps) {
const Icon = item.icon const Icon = item.icon
return ( return (
<button <button
key={item.path} key={item.id}
onClick={() => handleNavigate(item.path)} onClick={() => handleNavigate(item.path)}
onMouseEnter={() => setSelectedIndex(index)} onMouseEnter={() => setSelectedIndex(index)}
className={cn( className={cn(

View File

@@ -24,8 +24,8 @@
}, },
"menu": { "menu": {
"home": "首页", "home": "首页",
"botMainConfig": "麦麦主程序配置", "botMainConfig": "麦麦置",
"aiModelProvider": "AI模型厂商置", "aiModelProvider": "模型厂商置",
"modelManagement": "模型管理与分配", "modelManagement": "模型管理与分配",
"promptManagement": "Prompt 管理", "promptManagement": "Prompt 管理",
"adapterConfig": "麦麦适配器配置", "adapterConfig": "麦麦适配器配置",
@@ -40,7 +40,7 @@
"pluginConfig": "插件管理", "pluginConfig": "插件管理",
"mcpSettings": "MCP 设置", "mcpSettings": "MCP 设置",
"logViewer": "日志查看器", "logViewer": "日志查看器",
"maisakaMonitor": "MaiSaka 聊天流监控", "maisakaMonitor": "麦麦观察",
"localChat": "本地聊天室", "localChat": "本地聊天室",
"settings": "系统设置" "settings": "系统设置"
} }
@@ -773,9 +773,9 @@
"items": { "items": {
"home": "首页", "home": "首页",
"homeDesc": "查看仪表板概览", "homeDesc": "查看仪表板概览",
"botConfig": "麦麦主程序配置", "botConfig": "麦麦置",
"botConfigDesc": "配置麦麦的核心设置", "botConfigDesc": "配置麦麦的核心设置",
"modelProvider": "麦麦模型提供商配置", "modelProvider": "模型厂商设置",
"modelProviderDesc": "配置模型提供商", "modelProviderDesc": "配置模型提供商",
"model": "麦麦模型配置", "model": "麦麦模型配置",
"modelDesc": "配置模型参数", "modelDesc": "配置模型参数",

View File

@@ -183,7 +183,7 @@
--layout-space-lg: 1.5rem; --layout-space-lg: 1.5rem;
--layout-space-xl: 2rem; --layout-space-xl: 2rem;
--layout-space-2xl: 3rem; --layout-space-2xl: 3rem;
--layout-sidebar-width: 16rem; --layout-sidebar-width: 15rem;
--layout-header-height: 3.5rem; --layout-header-height: 3.5rem;
--layout-max-content-width: 1280px; --layout-max-content-width: 1280px;
@@ -542,4 +542,4 @@
min-width: 44px; min-width: 44px;
min-height: 44px; min-height: 44px;
} }
} }

View File

@@ -0,0 +1,51 @@
import type { FieldSchema, LocalizedText } from '@/types/config-schema'
const LANGUAGE_ALIASES: Record<string, string[]> = {
zh: ['zh_CN', 'zh-CN', 'zh'],
en: ['en_US', 'en-US', 'en'],
ja: ['ja_JP', 'ja-JP', 'ja'],
ko: ['ko_KR', 'ko-KR', 'ko'],
}
function getLanguageCandidates(language?: string) {
const normalized = (language || '').replace('-', '_')
const baseLanguage = normalized.split('_')[0]
return [
normalized,
language || '',
...(LANGUAGE_ALIASES[baseLanguage] ?? []),
'zh_CN',
'zh-CN',
'zh',
].filter(Boolean)
}
export function resolveLocalizedText(text: LocalizedText | undefined, language?: string, fallback = '') {
if (!text) {
return fallback
}
if (typeof text === 'string') {
return text || fallback
}
for (const key of getLanguageCandidates(language)) {
const value = text[key]
if (value) {
return value
}
}
return Object.values(text).find(Boolean) ?? fallback
}
export function getAllLocalizedText(text: LocalizedText | undefined) {
if (!text) {
return []
}
return typeof text === 'string' ? [text] : Object.values(text)
}
export function resolveFieldLabel(field: FieldSchema, language?: string) {
return resolveLocalizedText(field.label, language, field.name)
}

View File

@@ -4,7 +4,7 @@ import type { ConfigSchema, FieldSchema } from '@/types/config-schema'
/** /**
* Hook type for field-level customization * Hook type for field-level customization
*/ */
export type FieldHookType = 'replace' | 'wrapper' export type FieldHookType = 'replace' | 'wrapper' | 'hidden'
/** /**
* Props passed to a FieldHookComponent * Props passed to a FieldHookComponent
@@ -13,6 +13,7 @@ export interface FieldHookComponentProps {
fieldPath: string fieldPath: string
value: unknown value: unknown
onChange?: (value: unknown) => void onChange?: (value: unknown) => void
onParentChange?: (field: string, value: unknown) => void
children?: ReactNode children?: ReactNode
schema?: ConfigSchema | FieldSchema schema?: ConfigSchema | FieldSchema
parentValues?: Record<string, unknown> parentValues?: Record<string, unknown>

View File

@@ -5,7 +5,7 @@
* 修改此处的版本号后,所有展示版本的地方都会自动更新 * 修改此处的版本号后,所有展示版本的地方都会自动更新
*/ */
export const APP_VERSION = '1.0.5' export const APP_VERSION = '1.0.6'
export const APP_NAME = 'MaiBot Dashboard' export const APP_NAME = 'MaiBot Dashboard'
export const APP_FULL_NAME = `${APP_NAME} v${APP_VERSION}` export const APP_FULL_NAME = `${APP_NAME} v${APP_VERSION}`

View File

@@ -29,12 +29,13 @@ import { ChevronDown, ChevronUp, Code2, Info, Layout, Power, RefreshCw, Save } f
import type { ConfigSchema } from '@/types/config-schema' import type { ConfigSchema } from '@/types/config-schema'
import { import {
BotPlatformsHook, BotPlatformAccountsHook,
ChatPromptsHook, ChatPromptsHook,
ChatTalkValueRulesHook, ChatTalkValueRulesHook,
ExpressionGroupsHook, ExpressionGroupsHook,
ExpressionLearningListHook, ExpressionLearningListHook,
KeywordRulesHook, KeywordRulesHook,
HiddenFieldHook,
MCPRootItemsHook, MCPRootItemsHook,
MCPServersHook, MCPServersHook,
RegexRulesHook, RegexRulesHook,
@@ -415,7 +416,9 @@ function BotConfigPageContent() {
useEffect(() => { useEffect(() => {
const hookEntries = [ const hookEntries = [
['bot.platforms', BotPlatformsHook], ['bot.platform', BotPlatformAccountsHook, 'replace'],
['bot.qq_account', HiddenFieldHook, 'hidden'],
['bot.platforms', HiddenFieldHook, 'hidden'],
['chat.chat_prompts', ChatPromptsHook], ['chat.chat_prompts', ChatPromptsHook],
['chat.talk_value_rules', ChatTalkValueRulesHook], ['chat.talk_value_rules', ChatTalkValueRulesHook],
['expression.expression_groups', ExpressionGroupsHook], ['expression.expression_groups', ExpressionGroupsHook],
@@ -426,8 +429,8 @@ function BotConfigPageContent() {
['mcp.servers', MCPServersHook], ['mcp.servers', MCPServersHook],
] as const ] as const
for (const [fieldPath, hookComponent] of hookEntries) { for (const [fieldPath, hookComponent, hookType = 'replace'] of hookEntries) {
fieldHooks.register(fieldPath, hookComponent, 'replace') fieldHooks.register(fieldPath, hookComponent, hookType)
} }
return () => { return () => {
@@ -596,7 +599,7 @@ function BotConfigPageContent() {
setHasUnsavedChanges(false) setHasUnsavedChanges(false)
toast({ toast({
title: '保存成功', title: '保存成功',
description: '麦麦主程序配置已保存', description: '麦麦置已保存',
}) })
} catch (error) { } catch (error) {
console.error('保存配置失败:', error) console.error('保存配置失败:', error)
@@ -772,7 +775,7 @@ function BotConfigPageContent() {
<div className="flex flex-col gap-3 sm:gap-4"> <div className="flex flex-col gap-3 sm:gap-4">
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3"> <div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3">
<div className="min-w-0"> <div className="min-w-0">
<h1 className="text-xl sm:text-2xl md:text-3xl font-bold"></h1> <h1 className="text-xl sm:text-2xl md:text-3xl font-bold"></h1>
<p className="text-muted-foreground mt-1 text-xs sm:text-sm"></p> <p className="text-muted-foreground mt-1 text-xs sm:text-sm"></p>
</div> </div>
{/* 按钮组 - 桌面端靠右 */} {/* 按钮组 - 桌面端靠右 */}
@@ -1032,6 +1035,7 @@ function DynamicConfigTabs(props: DynamicConfigTabsProps) {
setHasUnsavedChanges(true) setHasUnsavedChanges(true)
}} }}
hooks={fieldHooks} hooks={fieldHooks}
sectionColumns={2}
/> />
) )
} }

View File

@@ -1,6 +1,7 @@
import { useEffect, useMemo, useState } from 'react' import { useEffect, useMemo, useState } from 'react'
import { Textarea } from '@/components/ui/textarea' import { Textarea } from '@/components/ui/textarea'
import { resolveLocalizedText } from '@/lib/config-label'
import type { FieldHookComponent } from '@/lib/field-hooks' import type { FieldHookComponent } from '@/lib/field-hooks'
import type { ConfigSchema, FieldSchema } from '@/types/config-schema' import type { ConfigSchema, FieldSchema } from '@/types/config-schema'
@@ -15,7 +16,7 @@ function resolveLabel(schema?: ConfigSchema | FieldSchema, fieldPath?: string):
return fieldPath?.split('.').at(-1) || 'JSON 配置' return fieldPath?.split('.').at(-1) || 'JSON 配置'
} }
if ('label' in schema && schema.label) { if ('label' in schema && schema.label) {
return schema.label return resolveLocalizedText(schema.label, undefined, fieldPath?.split('.').at(-1) || 'JSON 配置')
} }
if ('uiLabel' in schema && schema.uiLabel) { if ('uiLabel' in schema && schema.uiLabel) {
return schema.uiLabel return schema.uiLabel

View File

@@ -11,6 +11,7 @@ import {
CardTitle, CardTitle,
} from '@/components/ui/card' } from '@/components/ui/card'
import { DynamicConfigForm } from '@/components/dynamic-form/DynamicConfigForm' import { DynamicConfigForm } from '@/components/dynamic-form/DynamicConfigForm'
import { resolveLocalizedText } from '@/lib/config-label'
import type { FieldHookComponent } from '@/lib/field-hooks' import type { FieldHookComponent } from '@/lib/field-hooks'
import type { ConfigSchema, FieldSchema } from '@/types/config-schema' import type { ConfigSchema, FieldSchema } from '@/types/config-schema'
@@ -49,7 +50,7 @@ function resolveLabel(schema?: ConfigSchema | FieldSchema, fieldPath?: string):
return fieldPath?.split('.').at(-1) ?? '列表配置' return fieldPath?.split('.').at(-1) ?? '列表配置'
} }
if ('label' in schema && schema.label) { if ('label' in schema && schema.label) {
return schema.label return resolveLocalizedText(schema.label, undefined, fieldPath?.split('.').at(-1) ?? '列表配置')
} }
if ('uiLabel' in schema && schema.uiLabel) { if ('uiLabel' in schema && schema.uiLabel) {
return schema.uiLabel return schema.uiLabel

View File

@@ -294,6 +294,120 @@ export const BotPlatformsHook: FieldHookComponent = ({ onChange, value }) => {
) )
} }
export const HiddenFieldHook: FieldHookComponent = () => null
export const BotPlatformAccountsHook: FieldHookComponent = ({
onChange,
onParentChange,
parentValues,
value,
}) => {
const primaryPlatform = typeof value === 'string' ? value : ''
const qqAccountValue = parentValues?.qq_account
const qqAccount =
typeof qqAccountValue === 'string' || typeof qqAccountValue === 'number'
? String(qqAccountValue)
: ''
const platforms = normalizePlatformAccounts(parentValues?.platforms)
const rows = platforms.map(parsePlatformAccount)
const updateRows = (nextRows: PlatformAccountRow[]) => {
onParentChange?.('platforms', nextRows.map(formatPlatformAccount))
}
const addRow = () => {
updateRows([...rows, { platform: '', account: '' }])
}
const removeRow = (rowIndex: number) => {
updateRows(rows.filter((_, index) => index !== rowIndex))
}
const updateRow = (rowIndex: number, patch: Partial<PlatformAccountRow>) => {
updateRows(rows.map((row, index) => (index === rowIndex ? { ...row, ...patch } : row)))
}
return (
<div className="space-y-3">
<div className="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
<div className="space-y-1">
<Label className="text-[15px] font-semibold leading-6"></Label>
<p className="text-xs text-muted-foreground">
platform qq_account platforms
</p>
</div>
<Button type="button" size="sm" variant="outline" onClick={addRow}>
<Plus className="mr-2 h-4 w-4" />
</Button>
</div>
<div className="space-y-2">
<div className="grid gap-2 rounded-md border bg-muted/20 p-3 sm:grid-cols-[minmax(7rem,0.6fr)_minmax(10rem,1fr)_2.5rem]">
<div className="space-y-1">
<Label className="text-xs"></Label>
<Input
value={primaryPlatform}
placeholder="qq"
onChange={(event) => onChange?.(event.target.value)}
/>
</div>
<div className="space-y-1">
<Label className="text-xs"></Label>
<Input
className="font-mono"
value={qqAccount}
placeholder="2814567326"
onChange={(event) => onParentChange?.('qq_account', event.target.value)}
/>
</div>
<div className="flex items-end justify-end">
<span className="rounded-md bg-primary/10 px-2 py-1 text-xs font-medium text-primary">
</span>
</div>
</div>
{rows.map((row, rowIndex) => (
<div
key={rowIndex}
className="grid gap-2 rounded-md border bg-muted/20 p-3 sm:grid-cols-[minmax(7rem,0.6fr)_minmax(10rem,1fr)_2.5rem]"
>
<div className="space-y-1">
<Label className="text-xs"></Label>
<Input
value={row.platform}
placeholder="wx"
onChange={(event) => updateRow(rowIndex, { platform: event.target.value })}
/>
</div>
<div className="space-y-1">
<Label className="text-xs"></Label>
<Input
className="font-mono"
value={row.account}
placeholder="114514"
onChange={(event) => updateRow(rowIndex, { account: event.target.value })}
/>
</div>
<div className="flex items-end justify-end">
<Button
type="button"
size="icon"
variant="ghost"
aria-label={`删除其他平台 ${rowIndex + 1}`}
onClick={() => removeRow(rowIndex)}
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
</div>
))}
</div>
</div>
)
}
export const KeywordRulesHook = createListItemEditorHook({ export const KeywordRulesHook = createListItemEditorHook({
addLabel: '添加关键词规则', addLabel: '添加关键词规则',
helperText: '匹配命中后会用 reaction 内容作为额外上下文。keywords 至少填一条,或使用正则模式。', helperText: '匹配命中后会用 reaction 内容作为额外上下文。keywords 至少填一条,或使用正则模式。',

View File

@@ -12,11 +12,13 @@ export type {
} from './useAutoSave' } from './useAutoSave'
export { export {
BotPlatformsHook, BotPlatformsHook,
BotPlatformAccountsHook,
ChatPromptsHook, ChatPromptsHook,
ChatTalkValueRulesHook, ChatTalkValueRulesHook,
ExpressionGroupsHook, ExpressionGroupsHook,
ExpressionLearningListHook, ExpressionLearningListHook,
KeywordRulesHook, KeywordRulesHook,
HiddenFieldHook,
MCPRootItemsHook, MCPRootItemsHook,
MCPServersHook, MCPServersHook,
RegexRulesHook, RegexRulesHook,

View File

@@ -1,4 +1,5 @@
import { useState, useEffect, useCallback, useRef, type MouseEvent } from 'react' import { useState, useEffect, useCallback, useRef, type MouseEvent } from 'react'
import { useTranslation } from 'react-i18next'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input' import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label' import { Label } from '@/components/ui/label'
@@ -47,8 +48,9 @@ import {
import { Switch } from '@/components/ui/switch' import { Switch } from '@/components/ui/switch'
import { Slider } from '@/components/ui/slider' import { Slider } from '@/components/ui/slider'
import { Badge } from '@/components/ui/badge' import { Badge } from '@/components/ui/badge'
import { Plus, Pencil, Trash2, Save, Search, Info, Power, Check, ChevronsUpDown, RefreshCw, Loader2, GraduationCap, Share2, AlertTriangle, Settings, Lock, Unlock } from 'lucide-react' import { Plus, Pencil, Trash2, Save, Search, Info, Power, Check, ChevronsUpDown, RefreshCw, Loader2, GraduationCap, Share2, AlertTriangle, Settings } from 'lucide-react'
import { getModelConfig, getModelConfigSchema, updateModelConfig } from '@/lib/config-api' import { getModelConfig, getModelConfigSchema, updateModelConfig } from '@/lib/config-api'
import { resolveFieldLabel } from '@/lib/config-label'
import type { ConfigSchema } from '@/types/config-schema' import type { ConfigSchema } from '@/types/config-schema'
import { useToast } from '@/hooks/use-toast' import { useToast } from '@/hooks/use-toast'
import { Alert, AlertDescription } from '@/components/ui/alert' import { Alert, AlertDescription } from '@/components/ui/alert'
@@ -82,6 +84,7 @@ export function ModelConfigPage() {
// 内部实现组件 // 内部实现组件
function ModelConfigPageContent() { function ModelConfigPageContent() {
const { i18n } = useTranslation()
const [models, setModels] = useState<ModelInfo[]>([]) const [models, setModels] = useState<ModelInfo[]>([])
const [providers, setProviders] = useState<string[]>([]) const [providers, setProviders] = useState<string[]>([])
const [providerConfigs, setProviderConfigs] = useState<ProviderConfig[]>([]) const [providerConfigs, setProviderConfigs] = useState<ProviderConfig[]>([])
@@ -105,7 +108,6 @@ function ModelConfigPageContent() {
const [pageSize, setPageSize] = useState(20) const [pageSize, setPageSize] = useState(20)
const [jumpToPage, setJumpToPage] = useState('') const [jumpToPage, setJumpToPage] = useState('')
const [advancedTemperatureMode, setAdvancedTemperatureMode] = useState(false)
const [advancedModelSettingsVisible, setAdvancedModelSettingsVisible] = useState(false) const [advancedModelSettingsVisible, setAdvancedModelSettingsVisible] = useState(false)
const [advancedTaskSettingsVisible, setAdvancedTaskSettingsVisible] = useState(false) const [advancedTaskSettingsVisible, setAdvancedTaskSettingsVisible] = useState(false)
const [restartNoticeVisible, setRestartNoticeVisible] = useState( const [restartNoticeVisible, setRestartNoticeVisible] = useState(
@@ -1009,15 +1011,11 @@ function ModelConfigPageContent() {
{taskConfigSchema.fields {taskConfigSchema.fields
.filter(f => f.type === 'object' && (advancedTaskSettingsVisible || !f.advanced)) .filter(f => f.type === 'object' && (advancedTaskSettingsVisible || !f.advanced))
.map((field, index) => { .map((field, index) => {
const desc = field.description || field.name
const commaIdx = desc.search(/[,]/)
const title = commaIdx > 0 ? desc.slice(0, commaIdx).trim() : desc
const subtitle = commaIdx > 0 ? desc.slice(commaIdx + 1).trim() : ''
return ( return (
<TaskConfigCard <TaskConfigCard
key={field.name} key={field.name}
title={`${title} (${field.name})`} title={resolveFieldLabel(field, i18n.language)}
description={subtitle} description={field.description}
taskConfig={taskConfig[field.name] ?? { model_list: [] }} taskConfig={taskConfig[field.name] ?? { model_list: [] }}
modelNames={modelNames} modelNames={modelNames}
onChange={(f, value) => updateTaskConfig(field.name, f, value)} onChange={(f, value) => updateTaskConfig(field.name, f, value)}
@@ -1425,7 +1423,7 @@ function ModelConfigPageContent() {
checked={editingModel?.temperature != null} checked={editingModel?.temperature != null}
onCheckedChange={(checked) => { onCheckedChange={(checked) => {
if (checked) { if (checked) {
setEditingModel((prev) => prev ? { ...prev, temperature: 0.5 } : null) setEditingModel((prev) => prev ? { ...prev, temperature: 0.7 } : null)
} else { } else {
setEditingModel((prev) => prev ? { ...prev, temperature: null } : null) setEditingModel((prev) => prev ? { ...prev, temperature: null } : null)
} }
@@ -1437,43 +1435,28 @@ function ModelConfigPageContent() {
<div className="space-y-3 pt-2 border-t"> <div className="space-y-3 pt-2 border-t">
<div className="flex items-center justify-between gap-3"> <div className="flex items-center justify-between gap-3">
<Label className="text-sm"></Label> <Label className="text-sm"></Label>
<div className="flex items-center gap-2"> <Input
<Input type="number"
type="number" value={editingModel.temperature}
value={editingModel.temperature} onChange={(e) => {
onChange={(e) => { const value = parseFloat(e.target.value)
const value = parseFloat(e.target.value) if (!isNaN(value) && value >= 0 && value <= 2) {
if (!isNaN(value) && value >= 0 && value <= 2) { setEditingModel((prev) => prev ? { ...prev, temperature: value } : null)
setEditingModel((prev) => prev ? { ...prev, temperature: value } : null) }
} }}
}} onBlur={(e) => {
onBlur={(e) => { const value = parseFloat(e.target.value)
const value = parseFloat(e.target.value) if (isNaN(value) || value < 0) {
if (isNaN(value) || value < 0) { setEditingModel((prev) => prev ? { ...prev, temperature: 0 } : null)
setEditingModel((prev) => prev ? { ...prev, temperature: 0 } : null) } else if (value > 2) {
} else if (value > 2) { setEditingModel((prev) => prev ? { ...prev, temperature: 2 } : null)
setEditingModel((prev) => prev ? { ...prev, temperature: 2 } : null) }
} }}
}} step={0.01}
step={0.01} min={0}
min={0} max={2}
max={2} className="w-20 h-8 text-sm text-right tabular-nums"
className="w-20 h-8 text-sm text-right tabular-nums" />
/>
<Button
variant="outline"
size="sm"
onClick={() => setAdvancedTemperatureMode(!advancedTemperatureMode)}
className="h-8 px-2"
title={advancedTemperatureMode ? "切换到基础模式 (0-1)" : "解锁高级范围 (0-2)"}
>
{advancedTemperatureMode ? (
<Unlock className="h-4 w-4" />
) : (
<Lock className="h-4 w-4" />
)}
</Button>
</div>
</div> </div>
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<span className="text-xs text-muted-foreground tabular-nums">0</span> <span className="text-xs text-muted-foreground tabular-nums">0</span>
@@ -1485,25 +1468,22 @@ function ModelConfigPageContent() {
) )
} }
min={0} min={0}
max={advancedTemperatureMode ? 2 : 1} max={2}
step={advancedTemperatureMode ? 0.05 : 0.1} step={0.05}
className="flex-1" className="flex-1"
/> />
<span className="text-xs text-muted-foreground tabular-nums">{advancedTemperatureMode ? '2' : '1'}</span> <span className="text-xs text-muted-foreground tabular-nums">2</span>
</div> </div>
{advancedTemperatureMode && ( {editingModel.temperature > 1 && (
<Alert className="bg-amber-500/10 border-amber-500/20 [&>svg+div]:translate-y-0"> <Alert className="bg-amber-500/10 border-amber-500/20 [&>svg+div]:translate-y-0">
<AlertTriangle className="h-4 w-4 text-amber-500" /> <AlertTriangle className="h-4 w-4 text-amber-500" />
<AlertDescription className="text-xs text-amber-600 dark:text-amber-400"> <AlertDescription className="text-xs text-amber-600 dark:text-amber-400">
&gt; 1 使 &gt; 1 使
</AlertDescription> </AlertDescription>
</Alert> </Alert>
)} )}
<p className="text-xs text-muted-foreground"> <p className="text-xs text-muted-foreground">
{advancedTemperatureMode 0.1-0.50.5-1.01.0-2.0
? "较低0.1-0.5产生确定输出中等0.5-1.0平衡创造性较高1.0-2.0)产生极度随机输出"
: "较低的温度0.1-0.3产生更确定的输出较高的温度0.7-1.0)产生更多样化的输出"
}
</p> </p>
</div> </div>
)} )}

View File

@@ -104,11 +104,11 @@ export const TaskConfigCard = React.memo(function TaskConfigCard({
type="number" type="number"
step="0.1" step="0.1"
min="0" min="0"
max="1" max="2"
value={taskConfig.temperature ?? 0.3} value={taskConfig.temperature ?? 0.7}
onChange={(e) => { onChange={(e) => {
const value = parseFloat(e.target.value) const value = parseFloat(e.target.value)
if (!isNaN(value) && value >= 0 && value <= 1) { if (!isNaN(value) && value >= 0 && value <= 2) {
onChange('temperature', value) onChange('temperature', value)
} }
}} }}
@@ -116,10 +116,10 @@ export const TaskConfigCard = React.memo(function TaskConfigCard({
/> />
</div> </div>
<Slider <Slider
value={[taskConfig.temperature ?? 0.3]} value={[taskConfig.temperature ?? 0.7]}
onValueChange={(values) => onChange('temperature', values[0])} onValueChange={(values) => onChange('temperature', values[0])}
min={0} min={0}
max={1} max={2}
step={0.1} step={0.1}
className="w-full" className="w-full"
/> />

View File

@@ -742,7 +742,7 @@ function ModelProviderConfigPageContent() {
{/* 页面标题 */} {/* 页面标题 */}
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4"> <div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
<div> <div>
<h1 className="text-2xl sm:text-3xl font-bold">AI模型厂商配</h1> <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> <p className="text-muted-foreground mt-1 sm:mt-2 text-sm sm:text-base"> AI API </p>
</div> </div>
<div className="flex flex-col sm:flex-row gap-2"> <div className="flex flex-col sm:flex-row gap-2">

View File

@@ -15,7 +15,7 @@ export function PlannerMonitorPage() {
<div> <div>
<h1 className="text-2xl sm:text-3xl font-bold flex items-center gap-2"> <h1 className="text-2xl sm:text-3xl font-bold flex items-center gap-2">
<Activity className="h-6 w-6 sm:h-7 sm:w-7" /> <Activity className="h-6 w-6 sm:h-7 sm:w-7" />
MaiSaka
</h1> </h1>
<p className="text-muted-foreground mt-1 sm:mt-2 text-sm sm:text-base"> <p className="text-muted-foreground mt-1 sm:mt-2 text-sm sm:text-base">
MaiSaka MaiSaka

View File

@@ -22,10 +22,12 @@ export type XWidgetType =
| 'switch' | 'switch'
| 'textarea' | 'textarea'
export type LocalizedText = string | Record<string, string>
export interface FieldSchema { export interface FieldSchema {
name: string name: string
type: FieldType type: FieldType
label: string label: LocalizedText
description: string description: string
required: boolean required: boolean
default?: unknown default?: unknown

View File

@@ -36,6 +36,11 @@ class BotConfig(ConfigBase):
platform: str = Field( platform: str = Field(
default="", default="",
json_schema_extra={ json_schema_extra={
"label": {
"zh_CN": "平台",
"en_US": "Platform",
"ja_JP": "プラットフォーム",
},
"x-widget": "input", "x-widget": "input",
"x-icon": "wifi", "x-icon": "wifi",
"x-layout": "inline-right", "x-layout": "inline-right",
@@ -48,6 +53,11 @@ class BotConfig(ConfigBase):
qq_account: str = Field( qq_account: str = Field(
default="", default="",
json_schema_extra={ json_schema_extra={
"label": {
"zh_CN": "QQ账号",
"en_US": "QQ account",
"ja_JP": "QQアカウント",
},
"x-widget": "input", "x-widget": "input",
"x-icon": "user", "x-icon": "user",
"x-layout": "inline-right", "x-layout": "inline-right",
@@ -69,6 +79,11 @@ class BotConfig(ConfigBase):
nickname: str = Field( nickname: str = Field(
default="麦麦", default="麦麦",
json_schema_extra={ json_schema_extra={
"label": {
"zh_CN": "机器人昵称",
"en_US": "Bot nickname",
"ja_JP": "ボットのニックネーム",
},
"x-widget": "input", "x-widget": "input",
"x-icon": "user-circle", "x-icon": "user-circle",
}, },
@@ -333,6 +348,11 @@ class ChatConfig(ConfigBase):
chat_prompts: list["ExtraPromptItem"] = Field( chat_prompts: list["ExtraPromptItem"] = Field(
default_factory=lambda: [], default_factory=lambda: [],
json_schema_extra={ json_schema_extra={
"label": {
"zh_CN": "额外 Prompt",
"en_US": "Extra prompts",
"ja_JP": "追加プロンプト",
},
"x-widget": "custom", "x-widget": "custom",
"x-icon": "list", "x-icon": "list",
}, },
@@ -341,6 +361,11 @@ class ChatConfig(ConfigBase):
enable_talk_value_rules: bool = Field( enable_talk_value_rules: bool = Field(
default=True, default=True,
json_schema_extra={ json_schema_extra={
"label": {
"zh_CN": "启用动态发言频率规则",
"en_US": "Enable dynamic talk frequency rules",
"ja_JP": "動的な発言頻度ルールを有効化",
},
"x-widget": "switch", "x-widget": "switch",
"x-icon": "settings", "x-icon": "settings",
}, },
@@ -353,6 +378,11 @@ class ChatConfig(ConfigBase):
TalkRulesItem(platform="", item_id="", rule_type="group", time="09:00-18:59", value=1.0), TalkRulesItem(platform="", item_id="", rule_type="group", time="09:00-18:59", value=1.0),
], ],
json_schema_extra={ json_schema_extra={
"label": {
"zh_CN": "动态发言频率规则",
"en_US": "Dynamic talk frequency rules",
"ja_JP": "動的な発言頻度ルール",
},
"x-widget": "custom", "x-widget": "custom",
"x-icon": "list", "x-icon": "list",
}, },

View File

@@ -1,12 +1,17 @@
import inspect
from typing import Any, Dict, List, get_args, get_origin from typing import Any, Dict, List, get_args, get_origin
from pydantic_core import PydanticUndefined from pydantic_core import PydanticUndefined
import inspect
from src.config.config_base import ConfigBase from src.config.config_base import ConfigBase
class ConfigSchemaGenerator: class ConfigSchemaGenerator:
@staticmethod
def _build_label(label: str) -> Dict[str, str]:
return {"zh_CN": label}
@classmethod @classmethod
def generate_schema(cls, config_class: type[ConfigBase], include_nested: bool = True) -> Dict[str, Any]: def generate_schema(cls, config_class: type[ConfigBase], include_nested: bool = True) -> Dict[str, Any]:
return cls.generate_config_schema(config_class, include_nested=include_nested) return cls.generate_config_schema(config_class, include_nested=include_nested)
@@ -76,7 +81,7 @@ class ConfigSchemaGenerator:
schema: Dict[str, Any] = { schema: Dict[str, Any] = {
"name": field_name, "name": field_name,
"type": field_type, "type": field_type,
"label": field_name, "label": cls._build_label(field_name),
"description": description, "description": description,
"required": field_info.is_required(), "required": field_info.is_required(),
} }