merge: 同步上游 dev 并增强人物画像查询
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import * as React from 'react'
|
||||
import * as LucideIcons from 'lucide-react'
|
||||
|
||||
import { Button } from '@/components/ui/button'
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
@@ -9,8 +10,8 @@ import {
|
||||
CardTitle,
|
||||
} from '@/components/ui/card'
|
||||
import { Separator } from '@/components/ui/separator'
|
||||
import type { ConfigSchema, FieldSchema } from '@/types/config-schema'
|
||||
import { fieldHooks, type FieldHookRegistry } from '@/lib/field-hooks'
|
||||
import type { ConfigSchema, FieldSchema } from '@/types/config-schema'
|
||||
|
||||
import { DynamicField } from './DynamicField'
|
||||
|
||||
@@ -20,53 +21,204 @@ export interface DynamicConfigFormProps {
|
||||
onChange: (field: string, value: unknown) => void
|
||||
basePath?: string
|
||||
hooks?: FieldHookRegistry
|
||||
/** 嵌套层级:0 = tab 内容层, 1 = section 内容层, 2+ = 更深嵌套 */
|
||||
/** 嵌套层级:0 = tab 内容层,1 = section 内容层,2+ = 更深嵌套 */
|
||||
level?: number
|
||||
advancedVisible?: boolean
|
||||
}
|
||||
|
||||
function buildFieldPath(basePath: string, fieldName: string) {
|
||||
return basePath ? `${basePath}.${fieldName}` : fieldName
|
||||
}
|
||||
|
||||
function hasTopLevelAdvancedFields(schema: ConfigSchema) {
|
||||
return schema.fields.some((field) => field.advanced && !schema.nested?.[field.name])
|
||||
}
|
||||
|
||||
function resolveSectionTitle(schema: ConfigSchema) {
|
||||
return schema.uiLabel || schema.classDoc || schema.className
|
||||
}
|
||||
|
||||
function resolveSectionDescription(schema: ConfigSchema, sectionTitle: string) {
|
||||
return schema.classDoc && schema.classDoc !== sectionTitle
|
||||
? schema.classDoc
|
||||
: undefined
|
||||
}
|
||||
|
||||
function SectionIcon({ iconName }: { iconName?: string }) {
|
||||
if (!iconName) return null
|
||||
const IconComponent = LucideIcons[iconName as keyof typeof LucideIcons] as
|
||||
| React.ComponentType<{ className?: string }>
|
||||
| undefined
|
||||
if (!IconComponent) return null
|
||||
return <IconComponent className="h-5 w-5 text-muted-foreground" />
|
||||
}
|
||||
|
||||
function AdvancedSettingsButton({
|
||||
active,
|
||||
onClick,
|
||||
}: {
|
||||
active: boolean
|
||||
onClick: () => void
|
||||
}) {
|
||||
return (
|
||||
<Button
|
||||
type="button"
|
||||
variant={active ? 'default' : 'outline'}
|
||||
size="sm"
|
||||
onClick={onClick}
|
||||
>
|
||||
高级设置
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
function DynamicConfigSection({
|
||||
basePath,
|
||||
hooks,
|
||||
level,
|
||||
mergedChildren = [],
|
||||
nestedSchema,
|
||||
onChange,
|
||||
sectionDescription,
|
||||
sectionKey,
|
||||
sectionTitle,
|
||||
values,
|
||||
}: {
|
||||
basePath: string
|
||||
hooks: FieldHookRegistry
|
||||
level: number
|
||||
mergedChildren?: Array<{
|
||||
key: string
|
||||
schema: ConfigSchema
|
||||
values: Record<string, unknown>
|
||||
}>
|
||||
nestedSchema: ConfigSchema
|
||||
onChange: (field: string, value: unknown) => void
|
||||
sectionDescription?: string
|
||||
sectionKey: string
|
||||
sectionTitle: string
|
||||
values: Record<string, unknown>
|
||||
}) {
|
||||
const [advancedVisible, setAdvancedVisible] = React.useState(false)
|
||||
const hasAdvanced =
|
||||
hasTopLevelAdvancedFields(nestedSchema) ||
|
||||
mergedChildren.some((child) => hasTopLevelAdvancedFields(child.schema))
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader className="pb-4">
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div className="space-y-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<SectionIcon iconName={nestedSchema.uiIcon} />
|
||||
<CardTitle className="text-lg">{sectionTitle}</CardTitle>
|
||||
</div>
|
||||
{sectionDescription && (
|
||||
<CardDescription>{sectionDescription}</CardDescription>
|
||||
)}
|
||||
</div>
|
||||
{hasAdvanced && (
|
||||
<AdvancedSettingsButton
|
||||
active={advancedVisible}
|
||||
onClick={() => setAdvancedVisible((current) => !current)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<DynamicConfigForm
|
||||
schema={nestedSchema}
|
||||
values={values}
|
||||
onChange={(field, value) => onChange(`${sectionKey}.${field}`, value)}
|
||||
basePath={basePath}
|
||||
hooks={hooks}
|
||||
level={level}
|
||||
advancedVisible={hasAdvanced ? advancedVisible : undefined}
|
||||
/>
|
||||
{mergedChildren.map((child) => {
|
||||
const childTitle = resolveSectionTitle(child.schema)
|
||||
const childDescription = resolveSectionDescription(child.schema, childTitle)
|
||||
const parentPath = basePath.includes('.')
|
||||
? basePath.replace(/\.[^.]+$/, '')
|
||||
: ''
|
||||
const childPath = buildFieldPath(parentPath, child.key)
|
||||
|
||||
return (
|
||||
<div key={child.key} className="mt-5 border-t border-border/50 pt-4">
|
||||
<div className="mb-3 space-y-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<SectionIcon iconName={child.schema.uiIcon} />
|
||||
<h3 className="text-sm font-medium">{childTitle}</h3>
|
||||
</div>
|
||||
{childDescription && (
|
||||
<p className="text-xs text-muted-foreground">{childDescription}</p>
|
||||
)}
|
||||
</div>
|
||||
<DynamicConfigForm
|
||||
schema={child.schema}
|
||||
values={child.values}
|
||||
onChange={(field, value) => onChange(`${child.key}.${field}`, value)}
|
||||
basePath={childPath}
|
||||
hooks={hooks}
|
||||
level={level}
|
||||
advancedVisible={hasAdvanced ? advancedVisible : undefined}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* DynamicConfigForm - 动态配置表单组件
|
||||
*
|
||||
*
|
||||
* 根据 ConfigSchema 渲染表单字段,支持:
|
||||
* 1. Hook 系统:通过 FieldHookRegistry 自定义字段渲染
|
||||
* - replace 模式:完全替换默认渲染
|
||||
* - wrapper 模式:包装默认渲染(通过 children 传递)
|
||||
* 2. 嵌套 schema:递归渲染 schema.nested 中的子配置,使用 Card 容器区分层级
|
||||
* 3. 默认渲染:使用 DynamicField 组件
|
||||
* 2. 嵌套 schema:递归渲染 schema.nested 中的子配置
|
||||
* 3. 高级设置:由栏目标题右侧按钮控制显示
|
||||
*/
|
||||
export const DynamicConfigForm: React.FC<DynamicConfigFormProps> = ({
|
||||
schema,
|
||||
values,
|
||||
onChange,
|
||||
basePath = '',
|
||||
hooks = fieldHooks, // 默认使用全局单例
|
||||
hooks = fieldHooks,
|
||||
level = 0,
|
||||
advancedVisible,
|
||||
}) => {
|
||||
const [localAdvancedVisible, setLocalAdvancedVisible] = React.useState(false)
|
||||
const resolvedAdvancedVisible = advancedVisible ?? localAdvancedVisible
|
||||
|
||||
const fieldMap = React.useMemo(
|
||||
() => new Map(schema.fields.map((field) => [field.name, field])),
|
||||
[schema.fields]
|
||||
[schema.fields],
|
||||
)
|
||||
const mergedChildKeys = React.useMemo(() => {
|
||||
const keys = new Set<string>()
|
||||
for (const nestedSchema of Object.values(schema.nested ?? {})) {
|
||||
for (const childKey of nestedSchema.uiMergeChildren ?? []) {
|
||||
if (schema.nested?.[childKey]) {
|
||||
keys.add(childKey)
|
||||
}
|
||||
}
|
||||
}
|
||||
return keys
|
||||
}, [schema.nested])
|
||||
|
||||
const buildFieldPath = (fieldName: string) => {
|
||||
return basePath ? `${basePath}.${fieldName}` : fieldName
|
||||
}
|
||||
|
||||
/**
|
||||
* 渲染单个字段
|
||||
* 检查是否有注册的 Hook,根据 Hook 类型选择渲染方式
|
||||
*/
|
||||
const renderField = (field: FieldSchema) => {
|
||||
const fieldPath = buildFieldPath(field.name)
|
||||
const fieldPath = buildFieldPath(basePath, field.name)
|
||||
|
||||
// 检查是否有注册的 Hook
|
||||
if (hooks.has(fieldPath)) {
|
||||
const hookEntry = hooks.get(fieldPath)
|
||||
if (!hookEntry) return null // Type guard(理论上不会发生)
|
||||
if (!hookEntry) return null
|
||||
|
||||
const HookComponent = hookEntry.component
|
||||
|
||||
if (hookEntry.type === 'replace') {
|
||||
// replace 模式:完全替换默认渲染
|
||||
return (
|
||||
<HookComponent
|
||||
fieldPath={fieldPath}
|
||||
@@ -75,27 +227,25 @@ export const DynamicConfigForm: React.FC<DynamicConfigFormProps> = ({
|
||||
schema={field}
|
||||
/>
|
||||
)
|
||||
} else {
|
||||
// wrapper 模式:包装默认渲染
|
||||
return (
|
||||
<HookComponent
|
||||
fieldPath={fieldPath}
|
||||
}
|
||||
|
||||
return (
|
||||
<HookComponent
|
||||
fieldPath={fieldPath}
|
||||
value={values[field.name]}
|
||||
onChange={(v) => onChange(field.name, v)}
|
||||
schema={field}
|
||||
>
|
||||
<DynamicField
|
||||
schema={field}
|
||||
value={values[field.name]}
|
||||
onChange={(v) => onChange(field.name, v)}
|
||||
schema={field}
|
||||
>
|
||||
<DynamicField
|
||||
schema={field}
|
||||
value={values[field.name]}
|
||||
onChange={(v) => onChange(field.name, v)}
|
||||
fieldPath={fieldPath}
|
||||
/>
|
||||
</HookComponent>
|
||||
)
|
||||
}
|
||||
fieldPath={fieldPath}
|
||||
/>
|
||||
</HookComponent>
|
||||
)
|
||||
}
|
||||
|
||||
// 无 Hook,使用默认渲染
|
||||
return (
|
||||
<DynamicField
|
||||
schema={field}
|
||||
@@ -106,44 +256,49 @@ export const DynamicConfigForm: React.FC<DynamicConfigFormProps> = ({
|
||||
)
|
||||
}
|
||||
|
||||
/** 渲染 section 图标 */
|
||||
const renderSectionIcon = (iconName?: string) => {
|
||||
if (!iconName) return null
|
||||
const IconComponent = LucideIcons[iconName as keyof typeof LucideIcons] as
|
||||
| React.ComponentType<{ className?: string }>
|
||||
| undefined
|
||||
if (!IconComponent) return null
|
||||
return <IconComponent className="h-5 w-5 text-muted-foreground" />
|
||||
}
|
||||
|
||||
// 过滤出不属于 nested 的顶层字段
|
||||
const topLevelFields = schema.fields.filter(
|
||||
(field) => !schema.nested?.[field.name]
|
||||
(field) => !schema.nested?.[field.name],
|
||||
)
|
||||
const normalFields = topLevelFields.filter((field) => !field.advanced)
|
||||
const advancedFields = topLevelFields.filter((field) => field.advanced)
|
||||
const visibleFields = resolvedAdvancedVisible
|
||||
? [...normalFields, ...advancedFields]
|
||||
: normalFields
|
||||
|
||||
const renderFieldList = (fields: FieldSchema[]) => (
|
||||
<>
|
||||
{fields.map((field, index) => (
|
||||
<React.Fragment key={field.name}>
|
||||
{index > 0 && <Separator className="my-2 bg-border/50" />}
|
||||
<div className="py-1">{renderField(field)}</div>
|
||||
</React.Fragment>
|
||||
))}
|
||||
</>
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* 渲染顶层字段 */}
|
||||
{topLevelFields.length > 0 && (
|
||||
<div className="space-y-1">
|
||||
{topLevelFields.map((field, index) => (
|
||||
<React.Fragment key={field.name}>
|
||||
{index > 0 && field.type !== 'boolean' && topLevelFields[index - 1]?.type !== 'boolean' && (
|
||||
<Separator className="my-1" />
|
||||
)}
|
||||
<div>{renderField(field)}</div>
|
||||
</React.Fragment>
|
||||
))}
|
||||
<div>
|
||||
{advancedVisible === undefined && advancedFields.length > 0 && (
|
||||
<div className="flex justify-end pb-2">
|
||||
<AdvancedSettingsButton
|
||||
active={localAdvancedVisible}
|
||||
onClick={() => setLocalAdvancedVisible((current) => !current)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{renderFieldList(visibleFields)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 渲染嵌套 schema */}
|
||||
{schema.nested &&
|
||||
Object.entries(schema.nested).map(([key, nestedSchema]) => {
|
||||
Object.entries(schema.nested)
|
||||
.filter(([key]) => !mergedChildKeys.has(key))
|
||||
.map(([key, nestedSchema]) => {
|
||||
const nestedField = fieldMap.get(key)
|
||||
const nestedFieldPath = buildFieldPath(key)
|
||||
const nestedFieldPath = buildFieldPath(basePath, key)
|
||||
|
||||
// Hook 系统处理
|
||||
if (hooks.has(nestedFieldPath)) {
|
||||
const hookEntry = hooks.get(nestedFieldPath)
|
||||
if (!hookEntry) return null
|
||||
@@ -185,67 +340,77 @@ export const DynamicConfigForm: React.FC<DynamicConfigFormProps> = ({
|
||||
)
|
||||
}
|
||||
|
||||
const sectionTitle =
|
||||
nestedSchema.uiLabel || nestedSchema.classDoc || nestedSchema.className
|
||||
const sectionDescription =
|
||||
nestedSchema.classDoc && nestedSchema.classDoc !== sectionTitle
|
||||
? nestedSchema.classDoc
|
||||
: undefined
|
||||
const sectionTitle = resolveSectionTitle(nestedSchema)
|
||||
const sectionDescription = resolveSectionDescription(nestedSchema, sectionTitle)
|
||||
const mergedChildren = (nestedSchema.uiMergeChildren ?? [])
|
||||
.map((childKey) => {
|
||||
const childSchema = schema.nested?.[childKey]
|
||||
if (!childSchema) {
|
||||
return null
|
||||
}
|
||||
|
||||
return {
|
||||
key: childKey,
|
||||
schema: childSchema,
|
||||
values: (values[childKey] as Record<string, unknown>) || {},
|
||||
}
|
||||
})
|
||||
.filter(
|
||||
(
|
||||
child,
|
||||
): child is {
|
||||
key: string
|
||||
schema: ConfigSchema
|
||||
values: Record<string, unknown>
|
||||
} => Boolean(child),
|
||||
)
|
||||
|
||||
// 一级嵌套:使用 Card 包裹,清晰的 section 边界
|
||||
if (level === 0) {
|
||||
return (
|
||||
<Card key={key}>
|
||||
<CardHeader className="pb-4">
|
||||
<div className="flex items-center gap-2">
|
||||
{renderSectionIcon(nestedSchema.uiIcon)}
|
||||
<CardTitle className="text-lg">{sectionTitle}</CardTitle>
|
||||
</div>
|
||||
{sectionDescription && (
|
||||
<CardDescription>{sectionDescription}</CardDescription>
|
||||
)}
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<DynamicConfigForm
|
||||
schema={nestedSchema}
|
||||
values={(values[key] as Record<string, unknown>) || {}}
|
||||
onChange={(field, value) => onChange(`${key}.${field}`, value)}
|
||||
basePath={nestedFieldPath}
|
||||
hooks={hooks}
|
||||
level={level + 1}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
// 二级及更深嵌套:使用左侧指示条 + 轻量分组
|
||||
return (
|
||||
<div
|
||||
key={key}
|
||||
className="relative space-y-4 rounded-lg border-l-2 border-muted-foreground/20 pl-4 pt-1 pb-1"
|
||||
>
|
||||
<div className="space-y-1">
|
||||
<div className="flex items-center gap-2">
|
||||
{renderSectionIcon(nestedSchema.uiIcon)}
|
||||
<h4 className="text-sm font-semibold">{sectionTitle}</h4>
|
||||
</div>
|
||||
{sectionDescription && (
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{sectionDescription}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<DynamicConfigForm
|
||||
schema={nestedSchema}
|
||||
<DynamicConfigSection
|
||||
key={key}
|
||||
mergedChildren={mergedChildren}
|
||||
nestedSchema={nestedSchema}
|
||||
values={(values[key] as Record<string, unknown>) || {}}
|
||||
onChange={(field, value) => onChange(`${key}.${field}`, value)}
|
||||
onChange={onChange}
|
||||
basePath={nestedFieldPath}
|
||||
hooks={hooks}
|
||||
level={level + 1}
|
||||
sectionKey={key}
|
||||
sectionTitle={sectionTitle}
|
||||
sectionDescription={sectionDescription}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Card key={key} className="border-border/70 bg-muted/20 shadow-none">
|
||||
<CardHeader className="px-4 py-3">
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div className="space-y-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<SectionIcon iconName={nestedSchema.uiIcon} />
|
||||
<CardTitle className="text-sm">{sectionTitle}</CardTitle>
|
||||
</div>
|
||||
{sectionDescription && (
|
||||
<CardDescription className="text-xs">
|
||||
{sectionDescription}
|
||||
</CardDescription>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="px-4 pb-4 pt-0">
|
||||
<DynamicConfigForm
|
||||
schema={nestedSchema}
|
||||
values={(values[key] as Record<string, unknown>) || {}}
|
||||
onChange={(field, value) => onChange(`${key}.${field}`, value)}
|
||||
basePath={nestedFieldPath}
|
||||
hooks={hooks}
|
||||
level={level + 1}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
@@ -8,6 +8,7 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@
|
||||
import { Slider } from "@/components/ui/slider"
|
||||
import { Switch } from "@/components/ui/switch"
|
||||
import { Textarea } from "@/components/ui/textarea"
|
||||
import { cn } from "@/lib/utils"
|
||||
import type { FieldSchema } from "@/types/config-schema"
|
||||
|
||||
export interface DynamicFieldProps {
|
||||
@@ -93,6 +94,28 @@ export const DynamicField: React.FC<DynamicFieldProps> = ({
|
||||
return <IconComponent className="h-4 w-4" />
|
||||
}
|
||||
|
||||
const renderFieldHeader = () => (
|
||||
<div className="flex flex-wrap items-center gap-x-2 gap-y-1">
|
||||
<Label
|
||||
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",
|
||||
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"
|
||||
: "bg-muted/60 text-foreground",
|
||||
)}
|
||||
>
|
||||
{renderIcon()}
|
||||
<span className="break-all">{schema.label}</span>
|
||||
{schema.required && <span className="text-destructive">*</span>}
|
||||
</Label>
|
||||
{schema.description && (
|
||||
<span className="text-[13px] leading-6 text-muted-foreground whitespace-pre-line">
|
||||
{schema.description}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
|
||||
/**
|
||||
* 根据 x-widget 或 type 选择并渲染对应的输入组件
|
||||
*/
|
||||
@@ -175,16 +198,9 @@ export const DynamicField: React.FC<DynamicFieldProps> = ({
|
||||
const renderSwitch = () => {
|
||||
const checked = Boolean(value)
|
||||
return (
|
||||
<div className="flex items-center justify-between rounded-lg border p-3 sm:p-4">
|
||||
<div className="space-y-0.5 pr-4">
|
||||
<Label className="text-sm font-medium flex items-center gap-2">
|
||||
{renderIcon()}
|
||||
{schema.label}
|
||||
{schema.required && <span className="text-destructive">*</span>}
|
||||
</Label>
|
||||
{schema.description && (
|
||||
<p className="text-[13px] text-muted-foreground whitespace-pre-line">{schema.description}</p>
|
||||
)}
|
||||
<div className="flex items-center justify-between gap-4 py-2">
|
||||
<div className="pr-4">
|
||||
{renderFieldHeader()}
|
||||
</div>
|
||||
<Switch
|
||||
checked={checked}
|
||||
@@ -305,28 +321,38 @@ export const DynamicField: React.FC<DynamicFieldProps> = ({
|
||||
const isBoolean =
|
||||
schema['x-widget'] === 'switch' ||
|
||||
(!schema['x-widget'] && schema.type === 'boolean')
|
||||
const supportsInlineRight =
|
||||
schema['x-layout'] === 'inline-right' &&
|
||||
['input', 'number', 'password', 'select', undefined].includes(schema['x-widget']) &&
|
||||
['string', 'number', 'integer', 'select'].includes(schema.type)
|
||||
|
||||
// Switch/Boolean 字段自带完整布局,直接返回
|
||||
if (isBoolean) {
|
||||
return renderInputComponent()
|
||||
}
|
||||
|
||||
if (supportsInlineRight) {
|
||||
return (
|
||||
<div
|
||||
className="flex flex-col gap-2 py-2 sm:flex-row sm:items-center sm:justify-between"
|
||||
style={{ '--field-input-width': schema['x-input-width'] ?? '12rem' } as React.CSSProperties}
|
||||
>
|
||||
<div className="min-w-0 flex-1">
|
||||
{renderFieldHeader()}
|
||||
</div>
|
||||
<div className="w-full shrink-0 sm:w-[var(--field-input-width)]">
|
||||
{renderInputComponent()}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
{/* Label with icon */}
|
||||
<Label className="text-sm font-medium flex items-center gap-2">
|
||||
{renderIcon()}
|
||||
{schema.label}
|
||||
{schema.required && <span className="text-destructive">*</span>}
|
||||
</Label>
|
||||
{renderFieldHeader()}
|
||||
|
||||
{/* Input component */}
|
||||
{renderInputComponent()}
|
||||
|
||||
{/* Description */}
|
||||
{schema.description && (
|
||||
<p className="text-[13px] text-muted-foreground whitespace-pre-line">{schema.description}</p>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Activity, Boxes, Database, FileSearch, FileText, Hash, Home, LayoutGrid, MessageSquare, Network, Package, Server, Settings, Sliders, Smile, UserCircle } from 'lucide-react'
|
||||
import { Activity, Boxes, Database, FileSearch, FileText, Hash, Home, MessageSquare, Network, Package, ScrollText, Server, Settings, Sliders, Smile } from 'lucide-react'
|
||||
|
||||
import type { MenuSection } from './types'
|
||||
|
||||
@@ -7,6 +7,7 @@ export const menuSections: MenuSection[] = [
|
||||
title: 'sidebar.groups.overview',
|
||||
items: [
|
||||
{ icon: Home, label: 'sidebar.menu.home', path: '/', searchDescription: 'search.items.homeDesc' },
|
||||
{ icon: Activity, label: 'sidebar.menu.maisakaMonitor', path: '/planner-monitor' },
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -15,7 +16,7 @@ export const menuSections: MenuSection[] = [
|
||||
{ icon: FileText, label: 'sidebar.menu.botMainConfig', path: '/config/bot', searchDescription: 'search.items.botConfigDesc' },
|
||||
{ icon: Server, label: 'sidebar.menu.aiModelProvider', path: '/config/modelProvider', searchDescription: 'search.items.modelProviderDesc', tourId: 'sidebar-model-provider' },
|
||||
{ icon: Boxes, label: 'sidebar.menu.modelManagement', path: '/config/model', searchDescription: 'search.items.modelDesc', tourId: 'sidebar-model-management' },
|
||||
{ icon: Sliders, label: 'sidebar.menu.adapterConfig', path: '/config/adapter' },
|
||||
{ icon: ScrollText, label: 'sidebar.menu.promptManagement', path: '/config/prompts' },
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -24,24 +25,21 @@ export const menuSections: MenuSection[] = [
|
||||
{ icon: Smile, label: 'sidebar.menu.emojiManagement', path: '/resource/emoji', searchDescription: 'search.items.emojiDesc' },
|
||||
{ icon: MessageSquare, label: 'sidebar.menu.expressionManagement', path: '/resource/expression', searchDescription: 'search.items.expressionDesc' },
|
||||
{ icon: Hash, label: 'sidebar.menu.slangManagement', path: '/resource/jargon', searchDescription: 'search.items.jargonDesc' },
|
||||
{ icon: UserCircle, label: 'sidebar.menu.personInfo', path: '/resource/person', searchDescription: 'search.items.personDesc' },
|
||||
{ icon: Network, label: 'sidebar.menu.knowledgeGraph', path: '/resource/knowledge-graph' },
|
||||
{ icon: Database, label: 'sidebar.menu.knowledgeBase', path: '/resource/knowledge-base' },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'sidebar.groups.extensionsMonitor',
|
||||
items: [
|
||||
{ icon: Package, label: 'sidebar.menu.pluginMarket', path: '/plugins', searchDescription: 'search.items.pluginsDesc' },
|
||||
{ icon: LayoutGrid, label: 'sidebar.menu.configTemplate', path: '/config/pack-market' },
|
||||
{ icon: Sliders, label: 'sidebar.menu.pluginConfig', path: '/plugin-config' },
|
||||
{ icon: FileSearch, label: 'sidebar.menu.logViewer', path: '/logs', searchDescription: 'search.items.logsDesc' },
|
||||
{ icon: Activity, label: 'sidebar.menu.maisakaMonitor', path: '/planner-monitor' },
|
||||
{ icon: Package, label: 'sidebar.menu.pluginMarket', path: '/plugins', searchDescription: 'search.items.pluginsDesc' },
|
||||
{ icon: Network, label: 'sidebar.menu.mcpSettings', path: '/mcp-settings' },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'sidebar.groups.system',
|
||||
items: [
|
||||
{ icon: FileSearch, label: 'sidebar.menu.logViewer', path: '/logs', searchDescription: 'search.items.logsDesc' },
|
||||
{ icon: Settings, label: 'sidebar.menu.settings', path: '/settings', searchDescription: 'search.items.settingsDesc' },
|
||||
],
|
||||
},
|
||||
|
||||
@@ -80,6 +80,8 @@ export function MemoryProfileManager() {
|
||||
const [selectedPersonId, setSelectedPersonId] = useState('')
|
||||
const [queryPersonId, setQueryPersonId] = useState('')
|
||||
const [queryKeyword, setQueryKeyword] = useState('')
|
||||
const [queryPlatform, setQueryPlatform] = useState('')
|
||||
const [queryUserId, setQueryUserId] = useState('')
|
||||
const [queryLimit, setQueryLimit] = useState('12')
|
||||
const [forceRefresh, setForceRefresh] = useState(false)
|
||||
const [overrideText, setOverrideText] = useState('')
|
||||
@@ -131,10 +133,11 @@ export function MemoryProfileManager() {
|
||||
}, [selectedProfile])
|
||||
|
||||
const submitQuery = useCallback(async () => {
|
||||
if (!queryPersonId.trim() && !queryKeyword.trim()) {
|
||||
const hasAccountLocator = queryPlatform.trim() && queryUserId.trim()
|
||||
if (!queryPersonId.trim() && !queryKeyword.trim() && !hasAccountLocator) {
|
||||
toast({
|
||||
title: '请输入查询条件',
|
||||
description: 'person_id 和关键词至少填写一个。',
|
||||
description: 'person_id、关键词、或平台与账号至少填写一种。',
|
||||
variant: 'destructive',
|
||||
})
|
||||
return
|
||||
@@ -144,6 +147,8 @@ export function MemoryProfileManager() {
|
||||
const payload = await queryMemoryProfile({
|
||||
personId: queryPersonId.trim(),
|
||||
personKeyword: queryKeyword.trim(),
|
||||
platform: queryPlatform.trim(),
|
||||
userId: queryUserId.trim(),
|
||||
limit: parsePositiveInt(queryLimit, 12),
|
||||
forceRefresh,
|
||||
})
|
||||
@@ -151,6 +156,7 @@ export function MemoryProfileManager() {
|
||||
const nextPersonId = String(payload.person_id ?? payload.profile?.person_id ?? queryPersonId ?? '')
|
||||
if (nextPersonId) {
|
||||
setSelectedPersonId(nextPersonId)
|
||||
setQueryPersonId(nextPersonId)
|
||||
}
|
||||
toast({
|
||||
title: '人物画像查询完成',
|
||||
@@ -166,7 +172,7 @@ export function MemoryProfileManager() {
|
||||
} finally {
|
||||
setQuerying(false)
|
||||
}
|
||||
}, [forceRefresh, loadProfiles, queryKeyword, queryLimit, queryPersonId, toast])
|
||||
}, [forceRefresh, loadProfiles, queryKeyword, queryLimit, queryPersonId, queryPlatform, queryUserId, toast])
|
||||
|
||||
const saveOverride = useCallback(async () => {
|
||||
const personId = selectedPersonId || queryPersonId.trim()
|
||||
@@ -232,7 +238,7 @@ export function MemoryProfileManager() {
|
||||
<Search className="h-4 w-4" />
|
||||
人物画像查询
|
||||
</CardTitle>
|
||||
<CardDescription>查看最近画像快照,或按 person_id / 关键词触发查询与刷新。</CardDescription>
|
||||
<CardDescription>查看最近画像快照,或按 person_id、关键词、平台账号触发查询与刷新。</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="grid gap-3 md:grid-cols-2">
|
||||
@@ -244,6 +250,24 @@ export function MemoryProfileManager() {
|
||||
<Label htmlFor="profile-keyword">人物关键词</Label>
|
||||
<Input id="profile-keyword" value={queryKeyword} onChange={(event) => setQueryKeyword(event.target.value)} />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="profile-platform">平台</Label>
|
||||
<Input
|
||||
id="profile-platform"
|
||||
value={queryPlatform}
|
||||
onChange={(event) => setQueryPlatform(event.target.value)}
|
||||
placeholder="例如 qq、telegram、webui"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="profile-user-id">平台账号</Label>
|
||||
<Input
|
||||
id="profile-user-id"
|
||||
value={queryUserId}
|
||||
onChange={(event) => setQueryUserId(event.target.value)}
|
||||
placeholder="输入平台侧 user_id"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="profile-limit">证据数量</Label>
|
||||
<Input id="profile-limit" type="number" value={queryLimit} onChange={(event) => setQueryLimit(event.target.value)} />
|
||||
|
||||
Reference in New Issue
Block a user