This commit is contained in:
SengokuCola
2026-04-25 13:53:33 +08:00

View File

@@ -14,6 +14,10 @@ import {
Sparkles,
Trash2,
Upload,
CheckCircle2,
CircleAlert,
FolderOpen,
HardDrive,
} from 'lucide-react'
import { CodeEditor } from '@/components/CodeEditor'
@@ -143,13 +147,13 @@ const IMPORT_STEP_TEXT: Record<string, string> = {
}
const IMPORT_KIND_OPTIONS: Array<{ value: MemoryImportTaskKind; label: string; description: string }> = [
{ value: 'upload', label: '上传文件', description: '从本地批量上传文本文件' },
{ value: 'upload', label: '上传文件', description: '从本地批量上传资料文件' },
{ value: 'paste', label: '粘贴导入', description: '直接粘贴文本或 JSON 内容创建任务' },
{ value: 'raw_scan', label: '本地扫描', description: '按路径别名和匹配规则批量扫描导入' },
{ value: 'lpmm_openie', label: 'LPMM OpenIE', description: '读取 LPMM 数据并抽取关系' },
{ value: 'lpmm_convert', label: 'LPMM 转换', description: '将 LPMM 数据转换到目标目录' },
{ value: 'temporal_backfill', label: '时序回填', description: '对既有数据执行时间字段回填' },
{ value: 'maibot_migration', label: 'MaiBot 迁移', description: '从 MaiBot 历史迁移长期记忆数据' },
{ value: 'temporal_backfill', label: '时序回填', description: '为已有数据补充时间字段' },
{ value: 'maibot_migration', label: 'MaiBot 迁移', description: '从 MaiBot 历史数据迁移长期记忆' },
]
function normalizeProgress(value: number | string | null | undefined): number {
@@ -371,6 +375,185 @@ function summarizeFeedbackActionPayload(value: Record<string, unknown> | undefin
return trimDeleteItemText(JSON.stringify(value, null, 2), 120)
}
function pickFeedbackRelationTriplet(value: unknown): Record<string, unknown> | null {
if (!value || typeof value !== 'object') {
return null
}
const record = value as Record<string, unknown>
const subject = String(record.subject ?? '').trim()
const predicate = String(record.predicate ?? '').trim()
const object = String(record.object ?? '').trim()
if (!subject || !predicate || !object) {
return null
}
return record
}
function formatFeedbackRelationTriplet(value: unknown): string {
const triplet = pickFeedbackRelationTriplet(value)
if (!triplet) {
return ''
}
return formatDeleteRelationText(
String(triplet.subject ?? ''),
String(triplet.predicate ?? ''),
String(triplet.object ?? ''),
)
}
function getFeedbackCorrectionPreview(task: MemoryFeedbackCorrectionDetailTaskPayload | MemoryFeedbackCorrectionSummaryPayload | null): {
headline: string
oldRelation: string
newRelation: string
} {
if (!task) {
return {
headline: '当前没有纠错摘要',
oldRelation: '',
newRelation: '',
}
}
const detailTask = task as MemoryFeedbackCorrectionDetailTaskPayload
const rollbackPlanSummary = detailTask.rollback_plan_summary ?? {}
const forgottenRelations = Array.isArray(rollbackPlanSummary.forgotten_relations)
? rollbackPlanSummary.forgotten_relations
: []
const correctedWrite = rollbackPlanSummary.corrected_write && typeof rollbackPlanSummary.corrected_write === 'object'
? rollbackPlanSummary.corrected_write
: {}
const correctedRelations = Array.isArray((correctedWrite as Record<string, unknown>).corrected_relations)
? ((correctedWrite as Record<string, unknown>).corrected_relations as unknown[])
: []
const oldRelation = formatFeedbackRelationTriplet(forgottenRelations[0])
const newRelation = formatFeedbackRelationTriplet(correctedRelations[0])
if (oldRelation && newRelation) {
return {
headline: `将“${oldRelation}”纠正为“${newRelation}`,
oldRelation,
newRelation,
}
}
if (newRelation) {
return {
headline: `补充了新的纠错结论:“${newRelation}`,
oldRelation: '',
newRelation,
}
}
if (oldRelation) {
return {
headline: `撤销了旧记忆关系:“${oldRelation}`,
oldRelation,
newRelation: '',
}
}
return {
headline: task.query_text || '当前纠错没有可读摘要',
oldRelation: '',
newRelation: '',
}
}
function buildFeedbackImpactSummary(task: MemoryFeedbackCorrectionDetailTaskPayload | MemoryFeedbackCorrectionSummaryPayload | null): string[] {
if (!task) {
return []
}
const counts = task.affected_counts ?? {}
const items: string[] = []
if (Number(counts.relations ?? 0) > 0) {
items.push(`影响关系 ${Number(counts.relations ?? 0)}`)
}
if (Number(counts.corrected_relations ?? 0) > 0) {
items.push(`新增纠正关系 ${Number(counts.corrected_relations ?? 0)}`)
}
if (Number(counts.correction_paragraphs ?? 0) > 0) {
items.push(`写入纠错段落 ${Number(counts.correction_paragraphs ?? 0)}`)
}
if (Number(counts.stale_paragraphs ?? 0) > 0) {
items.push(`标记旧段落 ${Number(counts.stale_paragraphs ?? 0)}`)
}
if (Number(counts.episode_sources ?? 0) > 0) {
items.push(`触发 Episode 修复 ${Number(counts.episode_sources ?? 0)} 个来源`)
}
if (Number(counts.profile_person_ids ?? 0) > 0) {
items.push(`触发 Profile 刷新 ${Number(counts.profile_person_ids ?? 0)} 个对象`)
}
return items
}
function formatFeedbackActionType(actionType: string): string {
switch (actionType) {
case 'classification':
return '判定纠错'
case 'forget_relation':
return '撤销旧关系'
case 'mark_stale_paragraph':
return '标记旧段落'
case 'write_correction':
return '写入纠错'
case 'rollback_restore_relation':
return '恢复旧关系'
case 'rollback_delete_correction_paragraph':
return '隐藏纠错段落'
case 'rollback_revert_corrected_relation':
return '撤销纠正关系'
case 'rollback_clear_stale_mark':
return '清除脏段落标记'
case 'rollback_enqueue_episode_rebuild':
return '加入 Episode 修复队列'
case 'rollback_enqueue_profile_refresh':
return '加入 Profile 刷新队列'
case 'rollback_error':
return '回退失败'
case 'error':
return '处理失败'
case 'skip':
return '跳过处理'
default:
return actionType || '未知动作'
}
}
function describeFeedbackActionLog(item: MemoryFeedbackActionLogPayload): string {
const beforeSummary = summarizeFeedbackActionPayload(item.before_payload)
const afterSummary = summarizeFeedbackActionPayload(item.after_payload)
switch (item.action_type) {
case 'classification':
return afterSummary ? `系统完成判定:${afterSummary}` : '系统完成纠错判定'
case 'forget_relation':
return beforeSummary ? `旧关系已失效:${beforeSummary}` : '旧关系已被标记为失效'
case 'mark_stale_paragraph':
return '旧段落已标记为待复核,后续检索会更谨慎地使用它'
case 'write_correction':
return afterSummary ? `已写入新的纠错结果:${afterSummary}` : '已写入新的纠错段落和关系'
case 'rollback_restore_relation':
return afterSummary ? `已恢复旧关系状态:${afterSummary}` : '已恢复旧关系状态'
case 'rollback_delete_correction_paragraph':
return '已隐藏这次纠错写入的段落'
case 'rollback_revert_corrected_relation':
return '已撤销纠错阶段新增的关系'
case 'rollback_clear_stale_mark':
return '已清除旧段落的待复核标记'
case 'rollback_enqueue_episode_rebuild':
return '已重新加入 Episode 修复队列'
case 'rollback_enqueue_profile_refresh':
return '已重新加入 Profile 刷新队列'
case 'rollback_error':
return item.reason || '这次回退执行失败'
case 'error':
return item.reason || '这次纠错处理失败'
case 'skip':
return item.reason || '这次纠错被跳过'
default:
return afterSummary || beforeSummary || item.reason || '记录了一条动作日志'
}
}
type DeleteOperationItem = NonNullable<MemoryDeleteOperationPayload['items']>[number]
function trimDeleteItemText(value: string, maxLength: number = 140): string {
@@ -675,10 +858,38 @@ export function KnowledgeBasePage() {
return []
}
return [
{ label: '运行状态', value: runtimeConfig.runtime_ready ? '就绪' : '未就绪' },
{ label: 'Embedding 维度', value: String(runtimeConfig.embedding_dimension) },
{ label: '自动保存', value: runtimeConfig.auto_save ? '开启' : '关闭' },
{ label: '数据目录', value: runtimeConfig.data_dir },
{
label: '运行状态',
value: runtimeConfig.runtime_ready ? '就绪' : '未就绪',
description: runtimeConfig.embedding_degraded ? 'Embedding 降级运行' : '运行时检查通过',
icon: runtimeConfig.runtime_ready ? CheckCircle2 : CircleAlert,
className: runtimeConfig.runtime_ready ? 'border-emerald-500/20 bg-emerald-500/5' : 'border-amber-500/20 bg-amber-500/5',
iconClassName: runtimeConfig.runtime_ready ? 'text-emerald-500' : 'text-amber-500',
},
{
label: 'Embedding 维度',
value: String(runtimeConfig.embedding_dimension),
description: runtimeConfig.relation_vectors_enabled ? '关系向量已启用' : '关系向量未启用',
icon: HardDrive,
className: 'border-sky-500/20 bg-sky-500/5',
iconClassName: 'text-sky-500',
},
{
label: '自动保存',
value: runtimeConfig.auto_save ? '开启' : '关闭',
description: runtimeConfig.auto_save ? '运行数据会自动落盘' : '请留意手动保存',
icon: runtimeConfig.auto_save ? CheckCircle2 : CircleAlert,
className: runtimeConfig.auto_save ? 'border-primary/20 bg-primary/5' : 'border-muted-foreground/20 bg-muted/30',
iconClassName: runtimeConfig.auto_save ? 'text-primary' : 'text-muted-foreground',
},
{
label: '数据目录',
value: runtimeConfig.data_dir,
description: '长期记忆存储位置',
icon: FolderOpen,
className: 'border-violet-500/20 bg-violet-500/5',
iconClassName: 'text-violet-500',
},
]
}, [runtimeConfig])
@@ -1731,6 +1942,14 @@ export function KnowledgeBasePage() {
}
return selectedFeedbackTaskDetail ?? selectedFeedbackCorrection
}, [selectedFeedbackCorrection, selectedFeedbackTaskDetail])
const selectedFeedbackPreview = useMemo(
() => getFeedbackCorrectionPreview(selectedFeedbackResolved),
[selectedFeedbackResolved],
)
const selectedFeedbackImpactSummary = useMemo(
() => buildFeedbackImpactSummary(selectedFeedbackResolved),
[selectedFeedbackResolved],
)
const selectedFeedbackActionLogs: MemoryFeedbackActionLogPayload[] = Array.isArray(selectedFeedbackResolved?.action_logs)
? selectedFeedbackResolved.action_logs
@@ -2074,10 +2293,18 @@ export function KnowledgeBasePage() {
<div className="mx-auto flex w-full max-w-[1500px] flex-col gap-6">
<div className="grid gap-4 xl:grid-cols-4">
{runtimeBadges.map((item) => (
<Card key={item.label}>
<CardHeader className="pb-2">
<Card key={item.label} className={cn('overflow-hidden transition-colors', item.className)}>
<CardHeader className="space-y-3 pb-3">
<div className="flex items-start justify-between gap-3">
<div className="space-y-1">
<CardDescription>{item.label}</CardDescription>
<CardTitle className="break-all text-base">{item.value}</CardTitle>
<CardTitle className="break-all text-base leading-relaxed">{item.value}</CardTitle>
</div>
<div className="rounded-lg border bg-background/70 p-2 shadow-sm">
<item.icon className={cn('h-4 w-4', item.iconClassName)} />
</div>
</div>
<div className="text-xs text-muted-foreground">{item.description}</div>
</CardHeader>
</Card>
))}
@@ -2183,7 +2410,7 @@ export function KnowledgeBasePage() {
</CardTitle>
<CardDescription>
TOML
TOML
</CardDescription>
</div>
<div className="flex flex-wrap gap-2">
@@ -2209,7 +2436,7 @@ export function KnowledgeBasePage() {
{!rawConfigExists || rawConfigUsingDefault ? (
<Alert>
<AlertDescription>
{' '}
<code>{configPath}</code>
@@ -2252,7 +2479,7 @@ export function KnowledgeBasePage() {
<Upload className="h-4 w-4" />
</CardTitle>
<CardDescription></CardDescription>
<CardDescription> </CardDescription>
</CardHeader>
<CardContent className="space-y-6">
<Tabs
@@ -2261,7 +2488,7 @@ export function KnowledgeBasePage() {
className="space-y-4"
>
<div className="space-y-2">
<Label></Label>
<Label></Label>
<TabsList className="h-auto w-full flex-wrap justify-start gap-1 rounded-xl border bg-muted/20 p-1">
{IMPORT_KIND_OPTIONS.map((item) => (
<TabsTrigger
@@ -2275,15 +2502,15 @@ export function KnowledgeBasePage() {
</TabsList>
</div>
<div className="space-y-3 rounded-lg border bg-muted/30 p-4">
<div className="space-y-4 rounded-lg border bg-muted/30 p-4">
<div className="space-y-1">
<div className="text-sm font-medium"></div>
<div className="text-xs text-muted-foreground"></div>
<div className="text-xs text-muted-foreground"></div>
</div>
<div className="grid gap-3">
<div className="grid gap-3 md:grid-cols-2">
<div className="space-y-1">
<Label></Label>
<div className="text-xs text-muted-foreground"></div>
<div className="text-xs text-muted-foreground"></div>
<Input
type="number"
min={1}
@@ -2294,7 +2521,7 @@ export function KnowledgeBasePage() {
</div>
<div className="space-y-1">
<Label></Label>
<div className="text-xs text-muted-foreground"></div>
<div className="text-xs text-muted-foreground"></div>
<Input
type="number"
min={1}
@@ -2303,6 +2530,7 @@ export function KnowledgeBasePage() {
onChange={(event) => setImportCommonChunkConcurrency(event.target.value)}
/>
</div>
<div className="rounded-md border bg-background/70 p-3">
<div className="flex items-center gap-2 text-sm">
<Checkbox
checked={importCommonLlmEnabled}
@@ -2310,6 +2538,9 @@ export function KnowledgeBasePage() {
/>
LLM
</div>
<div className="mt-1 text-xs text-muted-foreground"></div>
</div>
<div className="rounded-md border bg-background/70 p-3">
<div className="flex items-center gap-2 text-sm">
<Checkbox
checked={importCommonChatLog}
@@ -2317,15 +2548,17 @@ export function KnowledgeBasePage() {
/>
</div>
<div className="mt-1 text-xs text-muted-foreground"></div>
</div>
</div>
<details className="rounded-md border bg-background/70 p-3 text-sm">
<summary className="cursor-pointer text-xs font-medium text-muted-foreground">
</summary>
<div className="mt-3 grid gap-3">
<div className="space-y-1">
<Label></Label>
<Label></Label>
<Input
value={importCommonStrategyOverride}
onChange={(event) => setImportCommonStrategyOverride(event.target.value)}
@@ -2357,7 +2590,7 @@ export function KnowledgeBasePage() {
checked={importCommonClearManifest}
onCheckedChange={(value) => setImportCommonClearManifest(Boolean(value))}
/>
</div>
</div>
</details>
@@ -2365,7 +2598,7 @@ export function KnowledgeBasePage() {
<TabsContent value="upload" className="mt-0">
<div className="space-y-3 rounded-xl border bg-background/70 p-4">
<div className="text-xs text-muted-foreground"></div>
<div className="text-xs text-muted-foreground"></div>
<div className="grid gap-3">
<div className="space-y-1">
<Label></Label>
@@ -2398,7 +2631,7 @@ export function KnowledgeBasePage() {
<TabsContent value="paste" className="mt-0">
<div className="space-y-3 rounded-xl border bg-background/70 p-4">
<div className="text-xs text-muted-foreground"></div>
<div className="text-xs text-muted-foreground"> JSON</div>
<div className="grid gap-3">
<div className="space-y-1">
<Label></Label>
@@ -2558,7 +2791,7 @@ export function KnowledgeBasePage() {
<div className="grid gap-2">
<div className="flex items-center gap-2 text-sm">
<Checkbox checked={backfillDryRun} onCheckedChange={(value) => setBackfillDryRun(Boolean(value))} />
</div>
<div className="flex items-center gap-2 text-sm">
<Checkbox
@@ -2638,7 +2871,7 @@ export function KnowledgeBasePage() {
<div className="grid gap-2">
<div className="flex items-center gap-2 text-sm">
<Checkbox checked={maibotNoResume} onCheckedChange={(value) => setMaibotNoResume(Boolean(value))} />
</div>
<div className="flex items-center gap-2 text-sm">
<Checkbox checked={maibotResetState} onCheckedChange={(value) => setMaibotResetState(Boolean(value))} />
@@ -2646,7 +2879,7 @@ export function KnowledgeBasePage() {
</div>
<div className="flex items-center gap-2 text-sm">
<Checkbox checked={maibotDryRun} onCheckedChange={(value) => setMaibotDryRun(Boolean(value))} />
</div>
<div className="flex items-center gap-2 text-sm">
<Checkbox checked={maibotVerifyOnly} onCheckedChange={(value) => setMaibotVerifyOnly(Boolean(value))} />
@@ -2668,13 +2901,13 @@ export function KnowledgeBasePage() {
<Card className="rounded-2xl border-border/70 bg-card/85 shadow-sm">
<CardHeader>
<CardTitle></CardTitle>
<CardDescription></CardDescription>
<CardDescription></CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid gap-3">
<div className="space-y-1">
<Label></Label>
<div className="text-xs text-muted-foreground"></div>
<div className="text-xs text-muted-foreground">访</div>
<Select value={pathResolveAlias} onValueChange={setPathResolveAlias}>
<SelectTrigger aria-label="import-path-alias">
<SelectValue />
@@ -2690,7 +2923,7 @@ export function KnowledgeBasePage() {
</div>
<div className="space-y-1">
<Label></Label>
<div className="text-xs text-muted-foreground"></div>
<div className="text-xs text-muted-foreground"></div>
<Input
value={pathResolveRelativePath}
onChange={(event) => setPathResolveRelativePath(event.target.value)}
@@ -2727,8 +2960,13 @@ export function KnowledgeBasePage() {
</div>
<div className="flex flex-wrap items-center justify-between gap-3">
<CardDescription className="text-sm">
{runningImportTasks.length} {queuedImportTasks.length} {recentImportTasks.length}
</CardDescription>
<div className="flex flex-wrap gap-2 text-xs text-muted-foreground">
<Badge variant="outline" className="bg-background/70"> {runningImportTasks.length}</Badge>
<Badge variant="outline" className="bg-background/70"> {queuedImportTasks.length}</Badge>
<Badge variant="outline" className="bg-background/70"> {recentImportTasks.length}</Badge>
</div>
<label className="flex items-center gap-2 text-xs text-muted-foreground">
<Checkbox checked={importAutoPolling} onCheckedChange={(value) => setImportAutoPolling(Boolean(value))} />
{importPollInterval}ms
@@ -3123,7 +3361,21 @@ export function KnowledgeBasePage() {
<TableCell>{getImportStepLabel(String(chunk.step ?? ''))}</TableCell>
<TableCell>{Number(chunk.progress ?? 0).toFixed(1)}%</TableCell>
<TableCell className="max-w-[360px]">
<div className="truncate text-sm">{String(chunk.error ?? '') || String(chunk.content_preview ?? '-')}</div>
<div className="space-y-2">
{String(chunk.error ?? '').trim() ? (
<div className="rounded-md border border-destructive/30 bg-destructive/5 px-2.5 py-2 text-sm leading-relaxed text-destructive">
{String(chunk.error)}
</div>
) : null}
<details className="rounded-md border bg-muted/20 px-2.5 py-2 text-xs text-muted-foreground">
<summary className="cursor-pointer font-medium text-foreground">
{String(chunk.error ?? '').trim() ? '查看分块预览' : '查看内容详情'}
</summary>
<div className="mt-2 whitespace-pre-wrap break-words leading-relaxed">
{String(chunk.content_preview ?? '-') || '-'}
</div>
</details>
</div>
</TableCell>
</TableRow>
))
@@ -3153,12 +3405,18 @@ export function KnowledgeBasePage() {
<Sparkles className="h-4 w-4" />
</CardTitle>
<CardDescription>线</CardDescription>
<CardDescription></CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-3 rounded-lg border bg-muted/20 p-4">
<div className="space-y-1">
<div className="text-sm font-medium"></div>
<div className="text-xs text-muted-foreground"> balanced / standard </div>
</div>
<div className="grid gap-4 md:grid-cols-2">
<div className="space-y-2">
<Label></Label>
<Label></Label>
<div className="text-xs text-muted-foreground"></div>
<Select value={tuningObjective} onValueChange={setTuningObjective}>
<SelectTrigger><SelectValue /></SelectTrigger>
<SelectContent>
@@ -3169,7 +3427,8 @@ export function KnowledgeBasePage() {
</Select>
</div>
<div className="space-y-2">
<Label></Label>
<Label></Label>
<div className="text-xs text-muted-foreground"></div>
<Select value={tuningIntensity} onValueChange={setTuningIntensity}>
<SelectTrigger><SelectValue /></SelectTrigger>
<SelectContent>
@@ -3180,16 +3439,25 @@ export function KnowledgeBasePage() {
</Select>
</div>
</div>
</div>
<div className="space-y-3 rounded-lg border bg-muted/20 p-4">
<div className="space-y-1">
<div className="text-sm font-medium"></div>
<div className="text-xs text-muted-foreground">使</div>
</div>
<div className="grid gap-4 md:grid-cols-2">
<div className="space-y-2">
<Label></Label>
<div className="text-xs text-muted-foreground"></div>
<Input type="number" value={tuningSampleSize} onChange={(event) => setTuningSampleSize(event.target.value)} />
</div>
<div className="space-y-2">
<Label> Top-K</Label>
<div className="text-xs text-muted-foreground"></div>
<Input type="number" value={tuningTopKEval} onChange={(event) => setTuningTopKEval(event.target.value)} />
</div>
</div>
</div>
<Button onClick={() => void submitTuningTask()} disabled={creatingTuning}>
<Sparkles className="mr-2 h-4 w-4" />
@@ -3201,6 +3469,7 @@ export function KnowledgeBasePage() {
<Card>
<CardHeader>
<CardTitle></CardTitle>
<CardDescription>便</CardDescription>
</CardHeader>
<CardContent className="space-y-3">
<CodeEditor
@@ -3221,6 +3490,7 @@ export function KnowledgeBasePage() {
<Card>
<CardHeader>
<CardTitle></CardTitle>
<CardDescription></CardDescription>
</CardHeader>
<CardContent className="space-y-3">
<Table>
@@ -3235,7 +3505,11 @@ export function KnowledgeBasePage() {
{tuningTasks.length > 0 ? tuningTasks.map((task) => (
<TableRow key={String(task.task_id ?? Math.random())}>
<TableCell className="font-mono text-xs">{String(task.task_id ?? '-')}</TableCell>
<TableCell>{String(task.status ?? '-')}</TableCell>
<TableCell>
<Badge variant={getImportStatusVariant(String(task.status ?? ''))}>
{String(task.status ?? '-')}
</Badge>
</TableCell>
<TableCell>
<Button
size="sm"
@@ -3250,7 +3524,7 @@ export function KnowledgeBasePage() {
)) : (
<TableRow>
<TableCell colSpan={3} className="text-center text-muted-foreground">
使
</TableCell>
</TableRow>
)}
@@ -3272,17 +3546,18 @@ export function KnowledgeBasePage() {
</CardTitle>
<CardDescription>
</CardDescription>
</div>
<Alert>
<Alert className="border-amber-500/30 bg-amber-500/5 text-amber-950 dark:text-amber-200">
<CircleAlert className="h-4 w-4 text-amber-500" />
<AlertDescription>
operation
</AlertDescription>
</Alert>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid gap-3 lg:grid-cols-[minmax(0,1fr)_auto] lg:items-center">
<div className="grid gap-3 rounded-xl border bg-muted/20 p-4 lg:grid-cols-[minmax(0,1fr)_auto] lg:items-center">
<div className="space-y-2">
<Label></Label>
<Input
@@ -3306,8 +3581,10 @@ export function KnowledgeBasePage() {
</div>
<div className="flex flex-wrap items-center gap-2 text-sm text-muted-foreground">
<Badge variant="outline"> {filteredSources.length} </Badge>
<Badge variant="secondary"> {selectedSources.length} </Badge>
<Badge variant="outline" className="bg-background/70"> {filteredSources.length} </Badge>
<Badge variant={selectedSources.length > 0 ? 'secondary' : 'outline'} className="bg-background/70">
{selectedSources.length}
</Badge>
</div>
<ScrollArea className="h-[320px] rounded-lg border">
@@ -3356,7 +3633,7 @@ export function KnowledgeBasePage() {
<CardDescription></CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid gap-3 lg:grid-cols-[minmax(0,1fr)_180px_180px]">
<div className="grid gap-3 rounded-xl border bg-muted/20 p-4 lg:grid-cols-[minmax(0,1fr)_180px_180px]">
<Input
value={operationSearch}
onChange={(event) => setOperationSearch(event.target.value)}
@@ -3455,7 +3732,7 @@ export function KnowledgeBasePage() {
</Button>
<div className="text-xs text-muted-foreground">
operation source
</div>
<Button
variant="outline"
@@ -3564,7 +3841,7 @@ export function KnowledgeBasePage() {
<Input
value={selectedOperationItemSearch}
onChange={(event) => setSelectedOperationItemSearch(event.target.value)}
placeholder="搜索类型 / hash / item_key / source"
placeholder="搜索对象类型 / 哈希 / 对象键 / 来源"
className="lg:max-w-sm"
/>
<div className="flex items-center justify-between gap-2 text-xs text-muted-foreground lg:min-w-[180px] lg:justify-end">
@@ -3617,7 +3894,7 @@ export function KnowledgeBasePage() {
</Button>
<div className="text-xs text-muted-foreground">
hashitem_keysource
</div>
<Button
variant="outline"
@@ -3659,7 +3936,7 @@ export function KnowledgeBasePage() {
<Input
value={feedbackSearch}
onChange={(event) => setFeedbackSearch(event.target.value)}
placeholder="搜索 query_tool_id / session / query / reason"
placeholder="搜索查询编号 / 会话 / 查询内容 / 原因"
/>
<Select value={feedbackStatusFilter} onValueChange={setFeedbackStatusFilter}>
<SelectTrigger>
@@ -3688,16 +3965,18 @@ export function KnowledgeBasePage() {
</Select>
</div>
<div className="flex flex-wrap items-center justify-between gap-2 text-sm text-muted-foreground">
<div className="flex flex-wrap items-center justify-between gap-2 rounded-xl border bg-background/70 px-3 py-2 text-sm text-muted-foreground">
<span> {filteredFeedbackCorrections.length} {feedbackCorrections.length} </span>
<span> {feedbackPage} / {feedbackPageCount} {FEEDBACK_CORRECTION_PAGE_SIZE} </span>
</div>
<div className="grid gap-4 xl:grid-cols-[minmax(0,0.92fr)_minmax(0,1.08fr)]">
<div className="grid items-start gap-4 xl:grid-cols-[minmax(0,0.92fr)_minmax(0,1.08fr)]">
<ScrollArea className="h-[720px] rounded-lg border">
<div className="space-y-3 p-3">
{pagedFeedbackCorrections.length > 0 ? pagedFeedbackCorrections.map((item) => {
const isSelected = selectedFeedbackCorrection?.task_id === item.task_id
const preview = getFeedbackCorrectionPreview(item)
const impactSummary = buildFeedbackImpactSummary(item)
return (
<button
key={item.task_id}
@@ -3711,6 +3990,7 @@ export function KnowledgeBasePage() {
)}
>
<div className="flex flex-col gap-3">
<div className="flex flex-wrap items-start justify-between gap-2">
<div className="flex flex-wrap items-center gap-2">
<Badge variant={getFeedbackStatusVariant(item.task_status)}>
{formatFeedbackTaskStatus(item.task_status)}
@@ -3722,21 +4002,47 @@ export function KnowledgeBasePage() {
{formatFeedbackDecision(item.decision)}
</Badge>
</div>
<div className="text-sm font-medium break-words">
{item.query_text || '无查询文本'}
<div className="text-[11px] text-muted-foreground">
{formatDeleteOperationTime(item.query_timestamp ?? item.created_at)}
</div>
</div>
<div className="space-y-1">
<div className="text-sm font-semibold break-words">
{preview.headline}
</div>
<div className="text-xs text-muted-foreground break-words">
{item.query_text || '无查询文本'}
</div>
</div>
{(preview.oldRelation || preview.newRelation) ? (
<div className="grid gap-2 rounded-lg border bg-background/70 p-3 text-xs shadow-sm">
<div className="grid gap-2 sm:grid-cols-[minmax(0,1fr)_auto_minmax(0,1fr)] sm:items-stretch">
<div className="rounded-md border border-amber-500/20 bg-amber-500/5 p-2">
<div className="text-[11px] font-medium text-amber-700 dark:text-amber-300"></div>
<div className="mt-1 break-words">{preview.oldRelation || '无'}</div>
</div>
<div className="hidden items-center text-muted-foreground sm:flex"></div>
<div className="rounded-md border border-emerald-500/20 bg-emerald-500/5 p-2">
<div className="text-[11px] font-medium text-emerald-700 dark:text-emerald-300"></div>
<div className="mt-1 break-words">{preview.newRelation || '无'}</div>
</div>
</div>
</div>
) : null}
<div className="flex flex-wrap gap-2">
{impactSummary.length > 0 ? impactSummary.slice(0, 3).map((summary) => (
<Badge key={`${item.task_id}:${summary}`} variant="secondary" className="font-normal">
{summary}
</Badge>
)) : (
<Badge variant="secondary" className="font-normal">
</Badge>
)}
</div>
<div className="font-mono text-[11px] break-all text-muted-foreground">
{item.query_tool_id}
</div>
<div className="flex flex-wrap gap-3 text-xs text-muted-foreground">
<span> {Number(item.affected_counts?.relations ?? 0)}</span>
<span> {Number(item.affected_counts?.stale_paragraphs ?? 0)}</span>
<span>Episode {Number(item.affected_counts?.episode_sources ?? 0)}</span>
<span>Profile {Number(item.affected_counts?.profile_person_ids ?? 0)}</span>
</div>
<div className="text-xs text-muted-foreground">
{formatDeleteOperationTime(item.query_timestamp ?? item.created_at)}
</div>
</div>
</button>
)
@@ -3748,7 +4054,7 @@ export function KnowledgeBasePage() {
</div>
</ScrollArea>
<div className="rounded-xl border bg-muted/20 p-4">
<div className="self-start rounded-xl border bg-muted/20 p-4">
{selectedFeedbackCorrection ? (
<div className="space-y-4">
<div className="flex flex-col gap-3 lg:flex-row lg:items-start lg:justify-between">
@@ -3764,10 +4070,13 @@ export function KnowledgeBasePage() {
{formatFeedbackDecision(String(selectedFeedbackResolved?.decision ?? ''))}
</Badge>
</div>
<div className="text-sm font-medium break-words">
{selectedFeedbackResolved?.query_text || '无查询文本'}
<div className="text-base font-semibold break-words">
{selectedFeedbackPreview.headline}
</div>
<div className="font-mono text-xs break-all">
<div className="text-sm text-muted-foreground break-words">
{selectedFeedbackResolved?.query_text || '无查询文本'}
</div>
<div className="font-mono text-xs break-all text-muted-foreground">
{selectedFeedbackResolved?.query_tool_id}
</div>
</div>
@@ -3788,6 +4097,40 @@ export function KnowledgeBasePage() {
</Button>
</div>
<div className="grid gap-4 xl:grid-cols-[minmax(0,1.05fr)_minmax(0,0.95fr)]">
<div className="rounded-xl border bg-background/70 p-4 shadow-sm">
<div className="text-sm font-semibold"></div>
<div className="mt-3 grid gap-3 md:grid-cols-[minmax(0,1fr)_auto_minmax(0,1fr)] md:items-stretch">
<div className="rounded-lg border border-amber-500/20 bg-amber-500/5 p-3">
<div className="text-xs font-medium text-amber-700 dark:text-amber-300"></div>
<div className="mt-2 text-sm break-words">
{selectedFeedbackPreview.oldRelation || '当前详情没有记录旧结论'}
</div>
</div>
<div className="hidden items-center justify-center text-muted-foreground md:flex"></div>
<div className="rounded-lg border border-emerald-500/20 bg-emerald-500/5 p-3">
<div className="text-xs font-medium text-emerald-700 dark:text-emerald-300"></div>
<div className="mt-2 text-sm break-words">
{selectedFeedbackPreview.newRelation || '当前详情没有记录新结论'}
</div>
</div>
</div>
</div>
<div className="rounded-xl border bg-background/70 p-4 shadow-sm">
<div className="text-sm font-semibold"></div>
<div className="mt-3 flex flex-wrap gap-2">
{selectedFeedbackImpactSummary.length > 0 ? selectedFeedbackImpactSummary.map((summary) => (
<Badge key={summary} variant="secondary" className="bg-primary/10 font-normal text-primary hover:bg-primary/15">
{summary}
</Badge>
)) : (
<div className="text-sm text-muted-foreground"></div>
)}
</div>
</div>
</div>
<div className="grid gap-3 lg:grid-cols-4">
<div className="rounded-lg border bg-background/60 p-3">
<div className="text-xs text-muted-foreground"></div>
@@ -3825,81 +4168,107 @@ export function KnowledgeBasePage() {
</Alert>
) : null}
<div className="grid gap-4 xl:grid-cols-[minmax(0,0.95fr)_minmax(0,1.05fr)]">
<div className="space-y-2">
<div className="text-sm font-semibold"></div>
<pre className="max-h-56 overflow-auto rounded-lg border bg-background/70 p-3 text-xs break-words whitespace-pre-wrap">
<div className="grid gap-4 xl:grid-cols-[minmax(0,1.05fr)_minmax(0,0.95fr)]">
<div className="rounded-xl border bg-background/70 p-4">
<div className="text-sm font-semibold">退</div>
<div className="mt-3 space-y-2 text-sm text-muted-foreground">
<div></div>
<div> Episode / Profile </div>
<div>退</div>
</div>
</div>
<div className="rounded-xl border bg-background/70 p-4">
<div className="text-sm font-semibold"></div>
<div className="mt-3 grid gap-2 text-sm text-muted-foreground">
<div>{formatFeedbackDecision(String(selectedFeedbackResolved?.decision ?? ''))}</div>
<div>{formatFeedbackTaskStatus(String(selectedFeedbackResolved?.task_status ?? ''))}</div>
<div>退{formatFeedbackRollbackStatus(String(selectedFeedbackResolved?.rollback_status ?? 'none'))}</div>
<div>{Number(selectedFeedbackResolved?.feedback_message_count ?? 0)}</div>
</div>
</div>
</div>
<div className="space-y-3">
<div className="text-sm font-semibold"></div>
<div className="grid gap-3 xl:grid-cols-2">
<details className="rounded-lg border bg-background/70 p-3">
<summary className="cursor-pointer text-sm font-medium"> JSON</summary>
<pre className="mt-3 max-h-56 overflow-auto text-xs break-words whitespace-pre-wrap">
{JSON.stringify(selectedFeedbackResolved?.query_snapshot ?? {}, null, 2)}
</pre>
</div>
<div className="space-y-2">
<div className="text-sm font-semibold"></div>
<pre className="max-h-56 overflow-auto rounded-lg border bg-background/70 p-3 text-xs break-words whitespace-pre-wrap">
</details>
<details className="rounded-lg border bg-background/70 p-3">
<summary className="cursor-pointer text-sm font-medium"> JSON</summary>
<pre className="mt-3 max-h-56 overflow-auto text-xs break-words whitespace-pre-wrap">
{JSON.stringify(selectedFeedbackResolved?.decision_payload ?? {}, null, 2)}
</pre>
</div>
</div>
<div className="grid gap-4 xl:grid-cols-[minmax(0,0.95fr)_minmax(0,1.05fr)]">
<div className="space-y-2">
<div className="text-sm font-semibold">退</div>
<pre className="max-h-64 overflow-auto rounded-lg border bg-background/70 p-3 text-xs break-words whitespace-pre-wrap">
</details>
<details className="rounded-lg border bg-background/70 p-3">
<summary className="cursor-pointer text-sm font-medium">退 JSON</summary>
<pre className="mt-3 max-h-64 overflow-auto text-xs break-words whitespace-pre-wrap">
{JSON.stringify(selectedFeedbackResolved?.rollback_plan_summary ?? {}, null, 2)}
</pre>
</div>
<div className="space-y-2">
<div className="text-sm font-semibold">退</div>
<pre className="max-h-64 overflow-auto rounded-lg border bg-background/70 p-3 text-xs break-words whitespace-pre-wrap">
</details>
<details className="rounded-lg border bg-background/70 p-3">
<summary className="cursor-pointer text-sm font-medium">退 JSON</summary>
<pre className="mt-3 max-h-64 overflow-auto text-xs break-words whitespace-pre-wrap">
{JSON.stringify(selectedFeedbackResolved?.rollback_result ?? {}, null, 2)}
</pre>
</details>
</div>
</div>
<div className="space-y-2">
<details className="rounded-xl border bg-background/70 p-4">
<summary className="cursor-pointer text-sm font-semibold">
线
</summary>
<div className="mt-4 space-y-2">
<div className="flex flex-col gap-3 lg:flex-row lg:items-center lg:justify-between">
<div className="text-sm font-semibold">线</div>
<div className="flex flex-col gap-3 lg:flex-row lg:items-center lg:justify-end">
<Input
value={feedbackActionLogSearch}
onChange={(event) => setFeedbackActionLogSearch(event.target.value)}
placeholder="搜索动作 / hash / 预览内容"
className="lg:w-80"
/>
<div className="text-xs text-muted-foreground">
{feedbackActionLogPage} / {feedbackActionLogPageCount} {FEEDBACK_ACTION_LOG_PAGE_SIZE}
</div>
<Input
value={feedbackActionLogSearch}
onChange={(event) => setFeedbackActionLogSearch(event.target.value)}
placeholder="搜索动作 / 目标哈希 / 预览内容"
className="lg:w-80"
/>
</div>
</div>
<ScrollArea className="h-[280px] rounded-lg border bg-background/60">
<ScrollArea className="h-[240px] rounded-lg border bg-background/60">
<div className="space-y-2 p-3">
{pagedFeedbackActionLogs.length > 0 ? pagedFeedbackActionLogs.map((item: MemoryFeedbackActionLogPayload) => (
<div key={`${item.id}:${item.action_type}`} className="rounded-lg border bg-muted/20 p-3">
<div className="flex flex-wrap items-center justify-between gap-2">
<div className="flex flex-wrap items-center gap-2">
<Badge variant="outline">{item.action_type}</Badge>
<Badge variant="outline">{formatFeedbackActionType(item.action_type)}</Badge>
{item.target_hash ? (
<span className="font-mono text-[11px] break-all text-muted-foreground">{item.target_hash}</span>
) : null}
</div>
<div className="text-[11px] text-muted-foreground">
{formatDeleteOperationTime(item.created_at)}
</div>
</div>
<div className="mt-2 text-sm break-words">
{describeFeedbackActionLog(item)}
</div>
{item.reason ? (
<div className="mt-2 text-xs text-muted-foreground break-words">
{item.reason}
{item.reason}
</div>
) : null}
{item.before_payload && Object.keys(item.before_payload).length > 0 ? (
<div className="mt-2 text-xs break-words">
<span className="font-medium">Before</span>
<div className="mt-3 rounded-md border bg-background/70 p-2 text-xs break-words">
<span className="font-medium"></span>
<span className="text-muted-foreground">{summarizeFeedbackActionPayload(item.before_payload)}</span>
</div>
) : null}
{item.after_payload && Object.keys(item.after_payload).length > 0 ? (
<div className="mt-1 text-xs break-words">
<span className="font-medium">After</span>
<div className="mt-2 rounded-md border bg-background/70 p-2 text-xs break-words">
<span className="font-medium"></span>
<span className="text-muted-foreground">{summarizeFeedbackActionPayload(item.after_payload)}</span>
</div>
) : null}
<div className="mt-2 text-[11px] text-muted-foreground">
{formatDeleteOperationTime(item.created_at)}
</div>
</div>
)) : (
<div className="rounded-lg border border-dashed bg-muted/20 p-6 text-center text-sm text-muted-foreground">
@@ -3917,7 +4286,7 @@ export function KnowledgeBasePage() {
>
</Button>
<div className="text-xs text-muted-foreground">hash </div>
<div className="text-xs text-muted-foreground"></div>
<Button
variant="outline"
size="sm"
@@ -3928,6 +4297,7 @@ export function KnowledgeBasePage() {
</Button>
</div>
</div>
</details>
</div>
) : (
<div className="flex min-h-[360px] items-center justify-center rounded-lg border border-dashed bg-background/40 p-6 text-center text-sm text-muted-foreground">
@@ -3947,7 +4317,7 @@ export function KnowledgeBasePage() {
</Button>
<div className="text-xs text-muted-foreground">
query退
退
</div>
<Button
variant="outline"
@@ -3986,7 +4356,7 @@ export function KnowledgeBasePage() {
<DialogHeader>
<DialogTitle>退</DialogTitle>
<DialogDescription>
relation episode/profile
Episode / Profile
</DialogDescription>
</DialogHeader>
<div className="space-y-3">