feat: add TuningTab component for tuning task management and utility functions for memory operations
- Implemented TuningTab component to handle tuning objectives, intensity, sample size, and evaluation settings. - Added UI elements for creating tuning tasks and displaying current configurations and recent tasks. - Introduced utility functions for normalizing and formatting memory operation data, including feedback actions and delete operations.
This commit is contained in:
File diff suppressed because it is too large
Load Diff
52
dashboard/src/routes/resource/knowledge-base/constants.ts
Normal file
52
dashboard/src/routes/resource/knowledge-base/constants.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import type { MemoryImportTaskKind } from '@/lib/memory-api'
|
||||
|
||||
export const DELETE_OPERATION_FETCH_LIMIT = 100
|
||||
export const DELETE_OPERATION_PAGE_SIZE = 6
|
||||
export const DELETE_OPERATION_ITEM_PAGE_SIZE = 8
|
||||
export const FEEDBACK_CORRECTION_FETCH_LIMIT = 100
|
||||
export const FEEDBACK_CORRECTION_PAGE_SIZE = 6
|
||||
export const FEEDBACK_ACTION_LOG_PAGE_SIZE = 8
|
||||
export const IMPORT_CHUNK_PAGE_SIZE = 50
|
||||
|
||||
export const RUNNING_IMPORT_STATUS = new Set(['preparing', 'running', 'cancel_requested'])
|
||||
export const QUEUED_IMPORT_STATUS = new Set(['queued'])
|
||||
|
||||
export const IMPORT_STATUS_TEXT: Record<string, string> = {
|
||||
queued: '排队中',
|
||||
preparing: '准备中',
|
||||
running: '运行中',
|
||||
cancel_requested: '取消中',
|
||||
cancelled: '已取消',
|
||||
completed: '已完成',
|
||||
completed_with_errors: '完成(有错误)',
|
||||
failed: '失败',
|
||||
}
|
||||
|
||||
export const IMPORT_STEP_TEXT: Record<string, string> = {
|
||||
queued: '排队中',
|
||||
preparing: '准备中',
|
||||
running: '运行中',
|
||||
splitting: '分块中',
|
||||
extracting: '抽取中',
|
||||
writing: '写入中',
|
||||
saving: '保存中',
|
||||
backfilling: '回填中',
|
||||
converting: '转换中',
|
||||
verifying: '校验中',
|
||||
switching: '切换中',
|
||||
cancel_requested: '取消中',
|
||||
cancelled: '已取消',
|
||||
completed: '已完成',
|
||||
completed_with_errors: '完成(有错误)',
|
||||
failed: '失败',
|
||||
}
|
||||
|
||||
export const IMPORT_KIND_OPTIONS: Array<{ value: MemoryImportTaskKind; label: string; description: string }> = [
|
||||
{ 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 历史数据迁移长期记忆' },
|
||||
]
|
||||
492
dashboard/src/routes/resource/knowledge-base/tabs/DeleteTab.tsx
Normal file
492
dashboard/src/routes/resource/knowledge-base/tabs/DeleteTab.tsx
Normal file
@@ -0,0 +1,492 @@
|
||||
import type { Dispatch, SetStateAction } from 'react'
|
||||
|
||||
import { CircleAlert, RotateCcw, Trash2 } from 'lucide-react'
|
||||
|
||||
import { Alert, AlertDescription } from '@/components/ui/alert'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Checkbox } from '@/components/ui/checkbox'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { ScrollArea } from '@/components/ui/scroll-area'
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
|
||||
import { TabsContent } from '@/components/ui/tabs'
|
||||
import { cn } from '@/lib/utils'
|
||||
import type { MemoryDeleteOperationPayload, MemorySourceItemPayload } from '@/lib/memory-api'
|
||||
|
||||
import { DELETE_OPERATION_ITEM_PAGE_SIZE, DELETE_OPERATION_PAGE_SIZE } from '../constants'
|
||||
import {
|
||||
formatDeleteOperationMode,
|
||||
formatDeleteOperationStatus,
|
||||
formatDeleteOperationTime,
|
||||
getDeleteOperationItemLabel,
|
||||
getDeleteOperationItemPreview,
|
||||
getDeleteOperationItemSource,
|
||||
type DeleteOperationItem,
|
||||
} from '../utils'
|
||||
|
||||
export interface DeleteTabProps {
|
||||
sourceSearch: string
|
||||
setSourceSearch: Dispatch<SetStateAction<string>>
|
||||
selectedSources: string[]
|
||||
setSelectedSources: Dispatch<SetStateAction<string[]>>
|
||||
filteredSources: MemorySourceItemPayload[]
|
||||
openSourceDeletePreview: () => Promise<void>
|
||||
toggleSourceSelection: (source: string, checked: boolean) => void
|
||||
|
||||
operationSearch: string
|
||||
setOperationSearch: Dispatch<SetStateAction<string>>
|
||||
operationModeFilter: string
|
||||
setOperationModeFilter: Dispatch<SetStateAction<string>>
|
||||
operationStatusFilter: string
|
||||
setOperationStatusFilter: Dispatch<SetStateAction<string>>
|
||||
filteredDeleteOperations: MemoryDeleteOperationPayload[]
|
||||
deleteOperations: MemoryDeleteOperationPayload[]
|
||||
operationPage: number
|
||||
setOperationPage: Dispatch<SetStateAction<number>>
|
||||
deleteOperationPageCount: number
|
||||
pagedDeleteOperations: MemoryDeleteOperationPayload[]
|
||||
selectedDeleteOperation: MemoryDeleteOperationPayload | null
|
||||
setSelectedOperationId: Dispatch<SetStateAction<string>>
|
||||
restoreDeleteOperation: (operationId: string) => Promise<void>
|
||||
deleteRestoring: boolean
|
||||
selectedOperationCounts: Record<string, number>
|
||||
selectedOperationDetailLoading: boolean
|
||||
selectedOperationDetailError: string
|
||||
selectedOperationSources: string[]
|
||||
selectedOperationItems: DeleteOperationItem[]
|
||||
filteredSelectedOperationItems: DeleteOperationItem[]
|
||||
selectedOperationItemSearch: string
|
||||
setSelectedOperationItemSearch: Dispatch<SetStateAction<string>>
|
||||
selectedOperationItemPage: number
|
||||
setSelectedOperationItemPage: Dispatch<SetStateAction<number>>
|
||||
selectedOperationItemPageCount: number
|
||||
pagedSelectedOperationItems: DeleteOperationItem[]
|
||||
}
|
||||
|
||||
export function DeleteTab(props: DeleteTabProps) {
|
||||
const {
|
||||
sourceSearch,
|
||||
setSourceSearch,
|
||||
selectedSources,
|
||||
setSelectedSources,
|
||||
filteredSources,
|
||||
openSourceDeletePreview,
|
||||
toggleSourceSelection,
|
||||
operationSearch,
|
||||
setOperationSearch,
|
||||
operationModeFilter,
|
||||
setOperationModeFilter,
|
||||
operationStatusFilter,
|
||||
setOperationStatusFilter,
|
||||
filteredDeleteOperations,
|
||||
deleteOperations,
|
||||
operationPage,
|
||||
setOperationPage,
|
||||
deleteOperationPageCount,
|
||||
pagedDeleteOperations,
|
||||
selectedDeleteOperation,
|
||||
setSelectedOperationId,
|
||||
restoreDeleteOperation,
|
||||
deleteRestoring,
|
||||
selectedOperationCounts,
|
||||
selectedOperationDetailLoading,
|
||||
selectedOperationDetailError,
|
||||
selectedOperationSources,
|
||||
selectedOperationItems,
|
||||
filteredSelectedOperationItems,
|
||||
selectedOperationItemSearch,
|
||||
setSelectedOperationItemSearch,
|
||||
selectedOperationItemPage,
|
||||
setSelectedOperationItemPage,
|
||||
selectedOperationItemPageCount,
|
||||
pagedSelectedOperationItems,
|
||||
} = props
|
||||
|
||||
return (
|
||||
<TabsContent value="delete" className="space-y-4">
|
||||
<div className="flex flex-col gap-4">
|
||||
<Card className="order-2">
|
||||
<CardHeader className="space-y-3">
|
||||
<div>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Trash2 className="h-4 w-4" />
|
||||
来源批量删除
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
用于按来源清理测试数据或指定导入批次。该操作不会直接删除实体,只会删除来源段落和失去全部证据的关系。
|
||||
</CardDescription>
|
||||
</div>
|
||||
<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>
|
||||
建议先在图谱里确认影响范围,再在这里执行批量来源删除。所有删除都会先经过预览,并支持按删除记录恢复。
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<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
|
||||
value={sourceSearch}
|
||||
onChange={(event) => setSourceSearch(event.target.value)}
|
||||
placeholder="搜索 source 名称"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2 lg:justify-end">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setSelectedSources(filteredSources.map((item) => String(item.source ?? '')).filter(Boolean))}
|
||||
>
|
||||
全选当前结果
|
||||
</Button>
|
||||
<Button onClick={() => void openSourceDeletePreview()} disabled={selectedSources.length <= 0}>
|
||||
<Trash2 className="mr-2 h-4 w-4" />
|
||||
预览删除
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap items-center gap-2 text-sm text-muted-foreground">
|
||||
<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">
|
||||
<Table>
|
||||
<TableHeader className="sticky top-0 bg-background">
|
||||
<TableRow>
|
||||
<TableHead className="w-12">选中</TableHead>
|
||||
<TableHead>来源</TableHead>
|
||||
<TableHead>段落数</TableHead>
|
||||
<TableHead>关系数</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{filteredSources.length > 0 ? filteredSources.map((item) => {
|
||||
const source = String(item.source ?? '')
|
||||
const checked = selectedSources.includes(source)
|
||||
return (
|
||||
<TableRow key={source}>
|
||||
<TableCell>
|
||||
<Checkbox checked={checked} onCheckedChange={(value) => toggleSourceSelection(source, Boolean(value))} />
|
||||
</TableCell>
|
||||
<TableCell className="font-mono text-xs break-all">{source}</TableCell>
|
||||
<TableCell>{Number(item.paragraph_count ?? 0)}</TableCell>
|
||||
<TableCell>{Number(item.relation_count ?? 0)}</TableCell>
|
||||
</TableRow>
|
||||
)
|
||||
}) : (
|
||||
<TableRow>
|
||||
<TableCell colSpan={4} className="text-center text-muted-foreground">
|
||||
当前没有可删除的来源
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</ScrollArea>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="order-1">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<RotateCcw className="h-4 w-4" />
|
||||
删除操作恢复
|
||||
</CardTitle>
|
||||
<CardDescription>按列表浏览最近的删除操作,先选中记录,再在下方确认影响范围并执行恢复</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<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)}
|
||||
placeholder="搜索 operation / reason / requested_by / source"
|
||||
/>
|
||||
<Select value={operationModeFilter} onValueChange={setOperationModeFilter}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="按模式筛选" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">全部模式</SelectItem>
|
||||
<SelectItem value="source">来源删除</SelectItem>
|
||||
<SelectItem value="mixed">混合删除</SelectItem>
|
||||
<SelectItem value="entity">实体删除</SelectItem>
|
||||
<SelectItem value="relation">关系删除</SelectItem>
|
||||
<SelectItem value="paragraph">段落删除</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Select value={operationStatusFilter} onValueChange={setOperationStatusFilter}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="按状态筛选" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">全部状态</SelectItem>
|
||||
<SelectItem value="executed">已执行</SelectItem>
|
||||
<SelectItem value="restored">已恢复</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap items-center justify-between gap-2 text-sm text-muted-foreground">
|
||||
<span>当前命中 {filteredDeleteOperations.length} 条记录,已加载最近 {deleteOperations.length} 条</span>
|
||||
<span>第 {operationPage} / {deleteOperationPageCount} 页,每页显示 {DELETE_OPERATION_PAGE_SIZE} 条</span>
|
||||
</div>
|
||||
|
||||
<ScrollArea className="h-[320px] rounded-lg border">
|
||||
<div className="space-y-3 p-3">
|
||||
{pagedDeleteOperations.length > 0 ? pagedDeleteOperations.map((operation) => {
|
||||
const summary = (operation.summary ?? {}) as Record<string, unknown>
|
||||
const counts = ((summary.counts as Record<string, number> | undefined) ?? {})
|
||||
const isSelected = selectedDeleteOperation?.operation_id === operation.operation_id
|
||||
return (
|
||||
<button
|
||||
key={operation.operation_id}
|
||||
type="button"
|
||||
onClick={() => setSelectedOperationId(operation.operation_id)}
|
||||
className={cn(
|
||||
'w-full rounded-xl border p-4 text-left transition-colors',
|
||||
isSelected
|
||||
? 'border-primary bg-primary/5 shadow-sm'
|
||||
: 'bg-muted/20 hover:border-primary/40 hover:bg-muted/40',
|
||||
)}
|
||||
>
|
||||
<div className="flex flex-col gap-3 lg:flex-row lg:items-start lg:justify-between">
|
||||
<div className="min-w-0 space-y-2">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<Badge variant={operation.status === 'restored' ? 'secondary' : 'default'}>
|
||||
{formatDeleteOperationStatus(String(operation.status ?? ''))}
|
||||
</Badge>
|
||||
<Badge variant="outline">
|
||||
{formatDeleteOperationMode(String(operation.mode ?? ''))}
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="font-mono text-xs break-all">{operation.operation_id}</div>
|
||||
<div className="text-sm text-muted-foreground">
|
||||
{operation.reason || '未填写原因'}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2 text-xs text-muted-foreground lg:max-w-[280px] lg:justify-end">
|
||||
<span>实体 {Number(counts.entities ?? 0)}</span>
|
||||
<span>关系 {Number(counts.relations ?? 0)}</span>
|
||||
<span>段落 {Number(counts.paragraphs ?? 0)}</span>
|
||||
<span>来源 {Number(counts.sources ?? 0)}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-3 text-xs text-muted-foreground">
|
||||
{formatDeleteOperationTime(operation.created_at)}
|
||||
</div>
|
||||
</button>
|
||||
)
|
||||
}) : (
|
||||
<div className="rounded-lg border border-dashed bg-muted/20 p-6 text-center text-sm text-muted-foreground">
|
||||
当前筛选条件下没有删除操作
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setOperationPage((current) => Math.max(1, current - 1))}
|
||||
disabled={operationPage <= 1}
|
||||
>
|
||||
上一页
|
||||
</Button>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
支持按删除记录、模式、状态、发起人和来源检索
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setOperationPage((current) => Math.min(deleteOperationPageCount, current + 1))}
|
||||
disabled={operationPage >= deleteOperationPageCount}
|
||||
>
|
||||
下一页
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="rounded-xl border bg-muted/20 p-4">
|
||||
{selectedDeleteOperation ? (
|
||||
<div className="space-y-4">
|
||||
<div className="flex flex-col gap-3 lg:flex-row lg:items-start lg:justify-between">
|
||||
<div className="space-y-2">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<Badge variant={selectedDeleteOperation.status === 'restored' ? 'secondary' : 'default'}>
|
||||
{formatDeleteOperationStatus(String(selectedDeleteOperation.status ?? ''))}
|
||||
</Badge>
|
||||
<Badge variant="outline">
|
||||
{formatDeleteOperationMode(String(selectedDeleteOperation.mode ?? ''))}
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="font-mono text-xs break-all">{selectedDeleteOperation.operation_id}</div>
|
||||
<div className="text-sm text-muted-foreground">
|
||||
{selectedDeleteOperation.reason || '未填写删除原因'}
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => void restoreDeleteOperation(selectedDeleteOperation.operation_id)}
|
||||
disabled={selectedDeleteOperation.status === 'restored' || deleteRestoring}
|
||||
>
|
||||
<RotateCcw className="mr-2 h-4 w-4" />
|
||||
{selectedDeleteOperation.status === 'restored' ? '已恢复' : '恢复这次删除'}
|
||||
</Button>
|
||||
</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>
|
||||
<div className="mt-1 text-sm">{selectedDeleteOperation.requested_by || '-'}</div>
|
||||
</div>
|
||||
<div className="rounded-lg border bg-background/60 p-3">
|
||||
<div className="text-xs text-muted-foreground">创建时间</div>
|
||||
<div className="mt-1 text-sm">{formatDeleteOperationTime(selectedDeleteOperation.created_at)}</div>
|
||||
</div>
|
||||
<div className="rounded-lg border bg-background/60 p-3">
|
||||
<div className="text-xs text-muted-foreground">恢复时间</div>
|
||||
<div className="mt-1 text-sm">{formatDeleteOperationTime(selectedDeleteOperation.restored_at)}</div>
|
||||
</div>
|
||||
<div className="rounded-lg border bg-background/60 p-3">
|
||||
<div className="text-xs text-muted-foreground">删除摘要</div>
|
||||
<div className="mt-1 flex flex-wrap gap-2">
|
||||
<Badge variant="outline">实体 {Number(selectedOperationCounts.entities ?? 0)}</Badge>
|
||||
<Badge variant="outline">关系 {Number(selectedOperationCounts.relations ?? 0)}</Badge>
|
||||
<Badge variant="outline">段落 {Number(selectedOperationCounts.paragraphs ?? 0)}</Badge>
|
||||
<Badge variant="outline">来源 {Number(selectedOperationCounts.sources ?? 0)}</Badge>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{selectedOperationDetailLoading ? (
|
||||
<div className="rounded-lg border bg-background/60 p-4 text-sm text-muted-foreground">
|
||||
正在加载影响对象详情...
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{selectedOperationDetailError ? (
|
||||
<Alert variant="destructive">
|
||||
<AlertDescription>{selectedOperationDetailError}</AlertDescription>
|
||||
</Alert>
|
||||
) : null}
|
||||
|
||||
{selectedOperationSources.length > 0 ? (
|
||||
<div className="space-y-2">
|
||||
<div className="text-sm font-semibold">关联来源</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{selectedOperationSources.map((source) => (
|
||||
<Badge key={source} variant="secondary" className="max-w-full break-all">
|
||||
{source}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div className="grid gap-4 xl:grid-cols-[minmax(0,0.9fr)_minmax(0,1.1fr)]">
|
||||
<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">
|
||||
{JSON.stringify(selectedDeleteOperation.selector ?? {}, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="text-sm font-semibold">影响对象</div>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
命中 {filteredSelectedOperationItems.length} / {selectedOperationItems.length} 项
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-3 lg:flex-row lg:items-center lg:justify-between">
|
||||
<Input
|
||||
value={selectedOperationItemSearch}
|
||||
onChange={(event) => setSelectedOperationItemSearch(event.target.value)}
|
||||
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">
|
||||
<span>第 {selectedOperationItemPage} / {selectedOperationItemPageCount} 页</span>
|
||||
<span>每页 {DELETE_OPERATION_ITEM_PAGE_SIZE} 项</span>
|
||||
</div>
|
||||
</div>
|
||||
<ScrollArea className="h-[280px] rounded-lg border bg-background/60">
|
||||
<div className="space-y-2 p-3">
|
||||
{pagedSelectedOperationItems.length > 0 ? pagedSelectedOperationItems.map((item) => {
|
||||
const source = getDeleteOperationItemSource(item)
|
||||
const label = getDeleteOperationItemLabel(item)
|
||||
const preview = getDeleteOperationItemPreview(item)
|
||||
return (
|
||||
<div key={`${item.item_type}:${item.item_hash}:${item.item_key ?? ''}`} className="rounded-lg border bg-muted/20 p-3">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<Badge variant="outline">{item.item_type}</Badge>
|
||||
{source ? <Badge variant="secondary">{source}</Badge> : null}
|
||||
{item.item_key && item.item_key !== item.item_hash ? (
|
||||
<span className="text-xs text-muted-foreground break-all">{item.item_key}</span>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="mt-2 text-sm font-medium break-words">
|
||||
{label}
|
||||
</div>
|
||||
{preview ? (
|
||||
<div className="mt-1 text-xs text-muted-foreground break-words">
|
||||
{preview}
|
||||
</div>
|
||||
) : null}
|
||||
<div className="mt-2 font-mono text-[11px] break-all text-muted-foreground">
|
||||
{item.item_hash}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}) : (
|
||||
<div className="rounded-lg border border-dashed bg-muted/20 p-6 text-center text-sm text-muted-foreground">
|
||||
{selectedOperationItems.length > 0 ? '当前筛选条件下没有明细项' : '当前操作没有记录明细项'}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setSelectedOperationItemPage((current) => Math.max(1, current - 1))}
|
||||
disabled={selectedOperationItemPage <= 1}
|
||||
>
|
||||
上一页
|
||||
</Button>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
支持按对象类型、哈希、对象键和来源检索
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setSelectedOperationItemPage((current) => Math.min(selectedOperationItemPageCount, current + 1))}
|
||||
disabled={selectedOperationItemPage >= selectedOperationItemPageCount}
|
||||
>
|
||||
下一页
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex min-h-[320px] items-center justify-center rounded-lg border border-dashed bg-background/40 p-6 text-center text-sm text-muted-foreground">
|
||||
当前没有可查看的删除操作详情
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</TabsContent>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,512 @@
|
||||
import type { Dispatch, SetStateAction } from 'react'
|
||||
|
||||
import { RotateCcw } from 'lucide-react'
|
||||
|
||||
import { Alert, AlertDescription } from '@/components/ui/alert'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { ScrollArea } from '@/components/ui/scroll-area'
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
|
||||
import { TabsContent } from '@/components/ui/tabs'
|
||||
import { cn } from '@/lib/utils'
|
||||
import type {
|
||||
MemoryFeedbackActionLogPayload,
|
||||
MemoryFeedbackCorrectionDetailTaskPayload,
|
||||
MemoryFeedbackCorrectionSummaryPayload,
|
||||
} from '@/lib/memory-api'
|
||||
|
||||
import { FEEDBACK_ACTION_LOG_PAGE_SIZE, FEEDBACK_CORRECTION_PAGE_SIZE } from '../constants'
|
||||
import {
|
||||
buildFeedbackImpactSummary,
|
||||
describeFeedbackActionLog,
|
||||
formatDeleteOperationTime,
|
||||
formatFeedbackActionType,
|
||||
formatFeedbackDecision,
|
||||
formatFeedbackRollbackStatus,
|
||||
formatFeedbackTaskStatus,
|
||||
getFeedbackCorrectionPreview,
|
||||
getFeedbackStatusVariant,
|
||||
summarizeFeedbackActionPayload,
|
||||
} from '../utils'
|
||||
|
||||
export interface FeedbackTabProps {
|
||||
feedbackSearch: string
|
||||
setFeedbackSearch: Dispatch<SetStateAction<string>>
|
||||
feedbackStatusFilter: string
|
||||
setFeedbackStatusFilter: Dispatch<SetStateAction<string>>
|
||||
feedbackRollbackFilter: string
|
||||
setFeedbackRollbackFilter: Dispatch<SetStateAction<string>>
|
||||
filteredFeedbackCorrections: MemoryFeedbackCorrectionSummaryPayload[]
|
||||
feedbackCorrections: MemoryFeedbackCorrectionSummaryPayload[]
|
||||
pagedFeedbackCorrections: MemoryFeedbackCorrectionSummaryPayload[]
|
||||
feedbackPage: number
|
||||
setFeedbackPage: Dispatch<SetStateAction<number>>
|
||||
feedbackPageCount: number
|
||||
selectedFeedbackCorrection: MemoryFeedbackCorrectionSummaryPayload | null
|
||||
setSelectedFeedbackTaskId: Dispatch<SetStateAction<number>>
|
||||
selectedFeedbackResolved: MemoryFeedbackCorrectionDetailTaskPayload | null
|
||||
selectedFeedbackPreview: ReturnType<typeof getFeedbackCorrectionPreview>
|
||||
selectedFeedbackImpactSummary: string[]
|
||||
openFeedbackRollbackDialog: () => void
|
||||
feedbackRollingBack: boolean
|
||||
selectedFeedbackTaskLoading: boolean
|
||||
selectedFeedbackTaskError: string | null
|
||||
feedbackActionLogPage: number
|
||||
setFeedbackActionLogPage: Dispatch<SetStateAction<number>>
|
||||
feedbackActionLogPageCount: number
|
||||
feedbackActionLogSearch: string
|
||||
setFeedbackActionLogSearch: Dispatch<SetStateAction<string>>
|
||||
pagedFeedbackActionLogs: MemoryFeedbackActionLogPayload[]
|
||||
selectedFeedbackActionLogs: MemoryFeedbackActionLogPayload[]
|
||||
}
|
||||
|
||||
export function FeedbackTab(props: FeedbackTabProps) {
|
||||
const {
|
||||
feedbackSearch,
|
||||
setFeedbackSearch,
|
||||
feedbackStatusFilter,
|
||||
setFeedbackStatusFilter,
|
||||
feedbackRollbackFilter,
|
||||
setFeedbackRollbackFilter,
|
||||
filteredFeedbackCorrections,
|
||||
feedbackCorrections,
|
||||
pagedFeedbackCorrections,
|
||||
feedbackPage,
|
||||
setFeedbackPage,
|
||||
feedbackPageCount,
|
||||
selectedFeedbackCorrection,
|
||||
setSelectedFeedbackTaskId,
|
||||
selectedFeedbackResolved,
|
||||
selectedFeedbackPreview,
|
||||
selectedFeedbackImpactSummary,
|
||||
openFeedbackRollbackDialog,
|
||||
feedbackRollingBack,
|
||||
selectedFeedbackTaskLoading,
|
||||
selectedFeedbackTaskError,
|
||||
feedbackActionLogPage,
|
||||
setFeedbackActionLogPage,
|
||||
feedbackActionLogPageCount,
|
||||
feedbackActionLogSearch,
|
||||
setFeedbackActionLogSearch,
|
||||
pagedFeedbackActionLogs,
|
||||
selectedFeedbackActionLogs,
|
||||
} = props
|
||||
|
||||
return (
|
||||
<TabsContent value="feedback" className="space-y-4">
|
||||
<div className="space-y-4">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<RotateCcw className="h-4 w-4" />
|
||||
反馈纠错历史
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
查看 feedback correction 的判定、修改轨迹与回退结果;本期仅覆盖自动纠错任务
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="grid gap-3 lg:grid-cols-[minmax(0,1fr)_180px_180px]">
|
||||
<Input
|
||||
value={feedbackSearch}
|
||||
onChange={(event) => setFeedbackSearch(event.target.value)}
|
||||
placeholder="搜索查询编号 / 会话 / 查询内容 / 原因"
|
||||
/>
|
||||
<Select value={feedbackStatusFilter} onValueChange={setFeedbackStatusFilter}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="按任务状态筛选" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">全部任务状态</SelectItem>
|
||||
<SelectItem value="applied">已应用</SelectItem>
|
||||
<SelectItem value="skipped">已跳过</SelectItem>
|
||||
<SelectItem value="error">失败</SelectItem>
|
||||
<SelectItem value="running">处理中</SelectItem>
|
||||
<SelectItem value="pending">待处理</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Select value={feedbackRollbackFilter} onValueChange={setFeedbackRollbackFilter}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="按回退状态筛选" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">全部回退状态</SelectItem>
|
||||
<SelectItem value="none">未回退</SelectItem>
|
||||
<SelectItem value="rolled_back">已回退</SelectItem>
|
||||
<SelectItem value="error">回退失败</SelectItem>
|
||||
<SelectItem value="running">回退中</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<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 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}
|
||||
type="button"
|
||||
onClick={() => setSelectedFeedbackTaskId(item.task_id)}
|
||||
className={cn(
|
||||
'w-full rounded-xl border p-4 text-left transition-colors',
|
||||
isSelected
|
||||
? 'border-primary bg-primary/5 shadow-sm'
|
||||
: 'bg-muted/20 hover:border-primary/40 hover:bg-muted/40',
|
||||
)}
|
||||
>
|
||||
<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)}
|
||||
</Badge>
|
||||
<Badge variant={getFeedbackStatusVariant(item.rollback_status)}>
|
||||
{formatFeedbackRollbackStatus(item.rollback_status)}
|
||||
</Badge>
|
||||
<Badge variant="outline">
|
||||
{formatFeedbackDecision(item.decision)}
|
||||
</Badge>
|
||||
</div>
|
||||
<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>
|
||||
</button>
|
||||
)
|
||||
}) : (
|
||||
<div className="rounded-lg border border-dashed bg-muted/20 p-6 text-center text-sm text-muted-foreground">
|
||||
当前筛选条件下没有纠错历史
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
|
||||
<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">
|
||||
<div className="space-y-2">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<Badge variant={getFeedbackStatusVariant(String(selectedFeedbackResolved?.task_status ?? ''))}>
|
||||
{formatFeedbackTaskStatus(String(selectedFeedbackResolved?.task_status ?? ''))}
|
||||
</Badge>
|
||||
<Badge variant={getFeedbackStatusVariant(String(selectedFeedbackResolved?.rollback_status ?? 'none'))}>
|
||||
{formatFeedbackRollbackStatus(String(selectedFeedbackResolved?.rollback_status ?? 'none'))}
|
||||
</Badge>
|
||||
<Badge variant="outline">
|
||||
{formatFeedbackDecision(String(selectedFeedbackResolved?.decision ?? ''))}
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="text-base font-semibold break-words">
|
||||
{selectedFeedbackPreview.headline}
|
||||
</div>
|
||||
<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>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={openFeedbackRollbackDialog}
|
||||
disabled={
|
||||
String(selectedFeedbackResolved?.task_status ?? '') !== 'applied'
|
||||
|| String(selectedFeedbackResolved?.rollback_status ?? 'none') === 'rolled_back'
|
||||
|| feedbackRollingBack
|
||||
}
|
||||
>
|
||||
<RotateCcw className="mr-2 h-4 w-4" />
|
||||
{String(selectedFeedbackResolved?.rollback_status ?? 'none') === 'rolled_back'
|
||||
? '已回退'
|
||||
: '回退本次纠错'}
|
||||
</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>
|
||||
<div className="mt-1 text-sm break-all">{selectedFeedbackResolved?.session_id || '-'}</div>
|
||||
</div>
|
||||
<div className="rounded-lg border bg-background/60 p-3">
|
||||
<div className="text-xs text-muted-foreground">反馈消息数</div>
|
||||
<div className="mt-1 text-sm">{Number(selectedFeedbackResolved?.feedback_message_count ?? 0)}</div>
|
||||
</div>
|
||||
<div className="rounded-lg border bg-background/60 p-3">
|
||||
<div className="text-xs text-muted-foreground">判定置信度</div>
|
||||
<div className="mt-1 text-sm">{Number(selectedFeedbackResolved?.decision_confidence ?? 0).toFixed(2)}</div>
|
||||
</div>
|
||||
<div className="rounded-lg border bg-background/60 p-3">
|
||||
<div className="text-xs text-muted-foreground">回退时间</div>
|
||||
<div className="mt-1 text-sm">{formatDeleteOperationTime(selectedFeedbackResolved?.rolled_back_at)}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{selectedFeedbackTaskLoading ? (
|
||||
<div className="rounded-lg border bg-background/60 p-4 text-sm text-muted-foreground">
|
||||
正在加载纠错详情...
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{selectedFeedbackTaskError ? (
|
||||
<Alert variant="destructive">
|
||||
<AlertDescription>{selectedFeedbackTaskError}</AlertDescription>
|
||||
</Alert>
|
||||
) : null}
|
||||
|
||||
{selectedFeedbackResolved?.rollback_error ? (
|
||||
<Alert variant="destructive">
|
||||
<AlertDescription>{selectedFeedbackResolved.rollback_error}</AlertDescription>
|
||||
</Alert>
|
||||
) : null}
|
||||
|
||||
<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>
|
||||
</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>
|
||||
</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>
|
||||
</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>
|
||||
|
||||
<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-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>
|
||||
<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">{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}
|
||||
</div>
|
||||
) : null}
|
||||
{item.before_payload && Object.keys(item.before_payload).length > 0 ? (
|
||||
<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-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>
|
||||
)) : (
|
||||
<div className="rounded-lg border border-dashed bg-muted/20 p-6 text-center text-sm text-muted-foreground">
|
||||
{selectedFeedbackActionLogs.length > 0 ? '当前筛选条件下没有动作日志' : '当前任务没有动作日志'}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setFeedbackActionLogPage((current) => Math.max(1, current - 1))}
|
||||
disabled={feedbackActionLogPage <= 1}
|
||||
>
|
||||
上一页
|
||||
</Button>
|
||||
<div className="text-xs text-muted-foreground">支持按动作类型、目标哈希和摘要检索</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setFeedbackActionLogPage((current) => Math.min(feedbackActionLogPageCount, current + 1))}
|
||||
disabled={feedbackActionLogPage >= feedbackActionLogPageCount}
|
||||
>
|
||||
下一页
|
||||
</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">
|
||||
当前没有可查看的纠错详情
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setFeedbackPage((current) => Math.max(1, current - 1))}
|
||||
disabled={feedbackPage <= 1}
|
||||
>
|
||||
上一页
|
||||
</Button>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
支持按查询内容、任务状态和回退状态检索
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setFeedbackPage((current) => Math.min(feedbackPageCount, current + 1))}
|
||||
disabled={feedbackPage >= feedbackPageCount}
|
||||
>
|
||||
下一页
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</TabsContent>
|
||||
)
|
||||
}
|
||||
1270
dashboard/src/routes/resource/knowledge-base/tabs/ImportTab.tsx
Normal file
1270
dashboard/src/routes/resource/knowledge-base/tabs/ImportTab.tsx
Normal file
File diff suppressed because it is too large
Load Diff
193
dashboard/src/routes/resource/knowledge-base/tabs/TuningTab.tsx
Normal file
193
dashboard/src/routes/resource/knowledge-base/tabs/TuningTab.tsx
Normal file
@@ -0,0 +1,193 @@
|
||||
import type { Dispatch, SetStateAction } from 'react'
|
||||
|
||||
import { Sparkles } from 'lucide-react'
|
||||
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { CodeEditor } from '@/components/CodeEditor'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
|
||||
import { TabsContent } from '@/components/ui/tabs'
|
||||
import type { MemoryTaskPayload } from '@/lib/memory-api'
|
||||
|
||||
import { getImportStatusVariant } from '../utils'
|
||||
|
||||
export interface TuningTabProps {
|
||||
tuningObjective: string
|
||||
setTuningObjective: Dispatch<SetStateAction<string>>
|
||||
tuningIntensity: string
|
||||
setTuningIntensity: Dispatch<SetStateAction<string>>
|
||||
tuningSampleSize: string
|
||||
setTuningSampleSize: Dispatch<SetStateAction<string>>
|
||||
tuningTopKEval: string
|
||||
setTuningTopKEval: Dispatch<SetStateAction<string>>
|
||||
submitTuningTask: () => Promise<void>
|
||||
creatingTuning: boolean
|
||||
tuningProfile: Record<string, unknown>
|
||||
tuningProfileToml: string
|
||||
tuningTasks: MemoryTaskPayload[]
|
||||
applyBestTask: (taskId: string) => Promise<void>
|
||||
}
|
||||
|
||||
export function TuningTab(props: TuningTabProps) {
|
||||
const {
|
||||
tuningObjective,
|
||||
setTuningObjective,
|
||||
tuningIntensity,
|
||||
setTuningIntensity,
|
||||
tuningSampleSize,
|
||||
setTuningSampleSize,
|
||||
tuningTopKEval,
|
||||
setTuningTopKEval,
|
||||
submitTuningTask,
|
||||
creatingTuning,
|
||||
tuningProfile,
|
||||
tuningProfileToml,
|
||||
tuningTasks,
|
||||
applyBestTask,
|
||||
} = props
|
||||
|
||||
return (
|
||||
<TabsContent value="tuning" className="space-y-4">
|
||||
<div className="grid gap-4 xl:grid-cols-[0.95fr_1.05fr]">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Sparkles className="h-4 w-4" />
|
||||
调优任务
|
||||
</CardTitle>
|
||||
<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>
|
||||
<div className="text-xs text-muted-foreground">决定本次调优更偏向准确率、召回率,还是两者平衡。</div>
|
||||
<Select value={tuningObjective} onValueChange={setTuningObjective}>
|
||||
<SelectTrigger><SelectValue /></SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="precision_priority">precision_priority</SelectItem>
|
||||
<SelectItem value="balanced">balanced</SelectItem>
|
||||
<SelectItem value="recall_priority">recall_priority</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>评估强度</Label>
|
||||
<div className="text-xs text-muted-foreground">强度越高,评估更充分,但任务耗时也更长。</div>
|
||||
<Select value={tuningIntensity} onValueChange={setTuningIntensity}>
|
||||
<SelectTrigger><SelectValue /></SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="quick">quick</SelectItem>
|
||||
<SelectItem value="standard">standard</SelectItem>
|
||||
<SelectItem value="deep">deep</SelectItem>
|
||||
</SelectContent>
|
||||
</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" />
|
||||
创建调优任务
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<div className="space-y-4">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>当前调优配置快照</CardTitle>
|
||||
<CardDescription>展示当前生效的检索调优参数,便于在应用新结果前做对照。</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
<CodeEditor
|
||||
value={JSON.stringify(tuningProfile, null, 2)}
|
||||
language="json"
|
||||
readOnly
|
||||
height="220px"
|
||||
/>
|
||||
<CodeEditor
|
||||
value={tuningProfileToml}
|
||||
language="toml"
|
||||
readOnly
|
||||
height="180px"
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>最近调优任务</CardTitle>
|
||||
<CardDescription>任务完成后,可以把最佳结果应用到当前调优配置。</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>任务</TableHead>
|
||||
<TableHead>状态</TableHead>
|
||||
<TableHead>动作</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{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>
|
||||
<Badge variant={getImportStatusVariant(String(task.status ?? ''))}>
|
||||
{String(task.status ?? '-')}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => void applyBestTask(String(task.task_id ?? ''))}
|
||||
disabled={!task.task_id}
|
||||
>
|
||||
应用最佳
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)) : (
|
||||
<TableRow>
|
||||
<TableCell colSpan={3} className="text-center text-muted-foreground">
|
||||
还没有调优任务。可以先使用默认参数创建一次评估任务。
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
)
|
||||
}
|
||||
498
dashboard/src/routes/resource/knowledge-base/utils.ts
Normal file
498
dashboard/src/routes/resource/knowledge-base/utils.ts
Normal file
@@ -0,0 +1,498 @@
|
||||
import type {
|
||||
MemoryDeleteOperationPayload,
|
||||
MemoryFeedbackActionLogPayload,
|
||||
MemoryFeedbackCorrectionDetailTaskPayload,
|
||||
MemoryFeedbackCorrectionSummaryPayload,
|
||||
MemoryImportInputMode,
|
||||
} from '@/lib/memory-api'
|
||||
|
||||
import {
|
||||
IMPORT_STATUS_TEXT,
|
||||
IMPORT_STEP_TEXT,
|
||||
QUEUED_IMPORT_STATUS,
|
||||
RUNNING_IMPORT_STATUS,
|
||||
} from './constants'
|
||||
|
||||
export type DeleteOperationItem = NonNullable<MemoryDeleteOperationPayload['items']>[number]
|
||||
|
||||
export function normalizeProgress(value: number | string | null | undefined): number {
|
||||
const numeric = Number(value ?? 0)
|
||||
if (!Number.isFinite(numeric)) {
|
||||
return 0
|
||||
}
|
||||
if (numeric < 0) {
|
||||
return 0
|
||||
}
|
||||
if (numeric > 100) {
|
||||
return 100
|
||||
}
|
||||
return numeric
|
||||
}
|
||||
|
||||
export function parseOptionalPositiveInt(input: string): number | undefined {
|
||||
const value = input.trim()
|
||||
if (!value) {
|
||||
return undefined
|
||||
}
|
||||
const parsed = Number(value)
|
||||
if (!Number.isInteger(parsed) || parsed <= 0) {
|
||||
return undefined
|
||||
}
|
||||
return parsed
|
||||
}
|
||||
|
||||
export function parseCommaSeparatedList(input: string): string[] {
|
||||
return input
|
||||
.split(',')
|
||||
.map((item) => item.trim())
|
||||
.filter(Boolean)
|
||||
}
|
||||
|
||||
export function normalizeImportInputMode(value: string): MemoryImportInputMode {
|
||||
return value === 'json' ? 'json' : 'text'
|
||||
}
|
||||
|
||||
export function getImportStatusLabel(status: string): string {
|
||||
const normalized = String(status ?? '').trim()
|
||||
if (!normalized) {
|
||||
return '-'
|
||||
}
|
||||
return IMPORT_STATUS_TEXT[normalized] ?? normalized
|
||||
}
|
||||
|
||||
export function getImportStepLabel(step: string): string {
|
||||
const normalized = String(step ?? '').trim()
|
||||
if (!normalized) {
|
||||
return '-'
|
||||
}
|
||||
return IMPORT_STEP_TEXT[normalized] ?? normalized
|
||||
}
|
||||
|
||||
export function getImportStatusVariant(status: string): 'default' | 'secondary' | 'destructive' | 'outline' {
|
||||
if (status === 'failed') {
|
||||
return 'destructive'
|
||||
}
|
||||
if (status === 'completed') {
|
||||
return 'default'
|
||||
}
|
||||
if (status === 'completed_with_errors' || status === 'cancelled') {
|
||||
return 'secondary'
|
||||
}
|
||||
if (RUNNING_IMPORT_STATUS.has(status) || QUEUED_IMPORT_STATUS.has(status)) {
|
||||
return 'outline'
|
||||
}
|
||||
return 'secondary'
|
||||
}
|
||||
|
||||
export function formatImportTime(timestamp?: number | null): string {
|
||||
if (!timestamp) {
|
||||
return '-'
|
||||
}
|
||||
const normalized = timestamp > 1_000_000_000_000 ? timestamp : timestamp * 1000
|
||||
const value = new Date(normalized)
|
||||
if (Number.isNaN(value.getTime())) {
|
||||
return '-'
|
||||
}
|
||||
return value.toLocaleString('zh-CN', {
|
||||
hour12: false,
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
})
|
||||
}
|
||||
|
||||
export function formatDeleteOperationMode(mode: string): string {
|
||||
switch (mode) {
|
||||
case 'entity':
|
||||
return '实体'
|
||||
case 'relation':
|
||||
return '关系'
|
||||
case 'paragraph':
|
||||
return '段落'
|
||||
case 'source':
|
||||
return '来源'
|
||||
case 'mixed':
|
||||
return '混合'
|
||||
default:
|
||||
return mode || '未知'
|
||||
}
|
||||
}
|
||||
|
||||
export function formatDeleteOperationStatus(status: string): string {
|
||||
switch (status) {
|
||||
case 'executed':
|
||||
return '已执行'
|
||||
case 'restored':
|
||||
return '已恢复'
|
||||
default:
|
||||
return status || '未知'
|
||||
}
|
||||
}
|
||||
|
||||
export function formatDeleteOperationTime(timestamp?: number | null): string {
|
||||
if (!timestamp) {
|
||||
return '未知时间'
|
||||
}
|
||||
const normalized = timestamp > 1_000_000_000_000 ? timestamp : timestamp * 1000
|
||||
const value = new Date(normalized)
|
||||
if (Number.isNaN(value.getTime())) {
|
||||
return '未知时间'
|
||||
}
|
||||
return value.toLocaleString('zh-CN', {
|
||||
hour12: false,
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
})
|
||||
}
|
||||
|
||||
export function trimDeleteItemText(value: string, maxLength: number = 140): string {
|
||||
const normalized = String(value ?? '').trim().replace(/\s+/g, ' ')
|
||||
if (!normalized) {
|
||||
return ''
|
||||
}
|
||||
if (normalized.length <= maxLength) {
|
||||
return normalized
|
||||
}
|
||||
return `${normalized.slice(0, maxLength)}...`
|
||||
}
|
||||
|
||||
export function formatDeleteRelationText(subject: string, predicate: string, object: string): string {
|
||||
const left = String(subject ?? '').trim()
|
||||
const middle = String(predicate ?? '').trim()
|
||||
const right = String(object ?? '').trim()
|
||||
return [left, middle, right].filter(Boolean).join(' -> ')
|
||||
}
|
||||
|
||||
export function getDeleteOperationItemLabel(item: DeleteOperationItem): string {
|
||||
const payload = item.payload ?? {}
|
||||
if (item.item_type === 'entity') {
|
||||
const entity = (payload.entity ?? {}) as Record<string, unknown>
|
||||
return String(entity.name ?? item.item_key ?? item.item_hash ?? '未命名实体')
|
||||
}
|
||||
if (item.item_type === 'relation') {
|
||||
const relation = (payload.relation ?? {}) as Record<string, unknown>
|
||||
return (
|
||||
formatDeleteRelationText(
|
||||
String(relation.subject ?? ''),
|
||||
String(relation.predicate ?? ''),
|
||||
String(relation.object ?? ''),
|
||||
) || String(item.item_key ?? item.item_hash ?? '未命名关系')
|
||||
)
|
||||
}
|
||||
if (item.item_type === 'paragraph') {
|
||||
const paragraph = (payload.paragraph ?? {}) as Record<string, unknown>
|
||||
const source = String(paragraph.source ?? '').trim()
|
||||
return source || String(item.item_key ?? item.item_hash ?? '未命名段落')
|
||||
}
|
||||
return String(item.item_key ?? item.item_hash ?? '未命名对象')
|
||||
}
|
||||
|
||||
export function getDeleteOperationItemPreview(item: DeleteOperationItem): string {
|
||||
const payload = item.payload ?? {}
|
||||
if (item.item_type === 'entity') {
|
||||
const paragraphLinks = Array.isArray(payload.paragraph_links) ? payload.paragraph_links : []
|
||||
if (paragraphLinks.length > 0) {
|
||||
return `关联段落 ${paragraphLinks.length} 个`
|
||||
}
|
||||
return '实体快照'
|
||||
}
|
||||
if (item.item_type === 'relation') {
|
||||
const relation = (payload.relation ?? {}) as Record<string, unknown>
|
||||
const paragraphHashes = Array.isArray(payload.paragraph_hashes) ? payload.paragraph_hashes : []
|
||||
const { confidence } = relation
|
||||
const parts = []
|
||||
if (paragraphHashes.length > 0) {
|
||||
parts.push(`证据段落 ${paragraphHashes.length} 个`)
|
||||
}
|
||||
if (typeof confidence === 'number') {
|
||||
parts.push(`置信度 ${confidence.toFixed(2)}`)
|
||||
}
|
||||
return parts.join(',') || '关系快照'
|
||||
}
|
||||
if (item.item_type === 'paragraph') {
|
||||
const paragraph = (payload.paragraph ?? {}) as Record<string, unknown>
|
||||
return trimDeleteItemText(String(paragraph.content ?? ''))
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
export function getDeleteOperationItemSource(item: DeleteOperationItem): string {
|
||||
const payload = item.payload ?? {}
|
||||
if (item.item_type === 'paragraph') {
|
||||
const paragraph = (payload.paragraph ?? {}) as Record<string, unknown>
|
||||
return String(paragraph.source ?? '').trim()
|
||||
}
|
||||
return String(payload.source ?? '').trim()
|
||||
}
|
||||
|
||||
export function formatFeedbackDecision(decision: string): string {
|
||||
switch (decision) {
|
||||
case 'correct':
|
||||
return '纠正'
|
||||
case 'reject':
|
||||
return '否定'
|
||||
case 'confirm':
|
||||
return '确认'
|
||||
case 'supplement':
|
||||
return '补充'
|
||||
case 'none':
|
||||
return '无动作'
|
||||
default:
|
||||
return decision || '未知'
|
||||
}
|
||||
}
|
||||
|
||||
export function formatFeedbackTaskStatus(status: string): string {
|
||||
switch (status) {
|
||||
case 'pending':
|
||||
return '待处理'
|
||||
case 'running':
|
||||
return '处理中'
|
||||
case 'applied':
|
||||
return '已应用'
|
||||
case 'skipped':
|
||||
return '已跳过'
|
||||
case 'error':
|
||||
return '失败'
|
||||
default:
|
||||
return status || '未知'
|
||||
}
|
||||
}
|
||||
|
||||
export function formatFeedbackRollbackStatus(status: string): string {
|
||||
switch (status) {
|
||||
case 'none':
|
||||
return '未回退'
|
||||
case 'running':
|
||||
return '回退中'
|
||||
case 'rolled_back':
|
||||
return '已回退'
|
||||
case 'error':
|
||||
return '回退失败'
|
||||
default:
|
||||
return status || '未知'
|
||||
}
|
||||
}
|
||||
|
||||
export function getFeedbackStatusVariant(
|
||||
status: string,
|
||||
): 'default' | 'secondary' | 'destructive' | 'outline' {
|
||||
if (status === 'applied' || status === 'rolled_back') {
|
||||
return 'default'
|
||||
}
|
||||
if (status === 'error') {
|
||||
return 'destructive'
|
||||
}
|
||||
if (status === 'running' || status === 'pending') {
|
||||
return 'outline'
|
||||
}
|
||||
return 'secondary'
|
||||
}
|
||||
|
||||
export function summarizeFeedbackActionPayload(value: Record<string, unknown> | undefined): string {
|
||||
if (!value) {
|
||||
return ''
|
||||
}
|
||||
const hash = String(value.hash ?? '').trim()
|
||||
const subject = String(value.subject ?? '').trim()
|
||||
const predicate = String(value.predicate ?? '').trim()
|
||||
const object = String(value.object ?? '').trim()
|
||||
if (subject && predicate && object) {
|
||||
return formatDeleteRelationText(subject, predicate, object)
|
||||
}
|
||||
if (hash) {
|
||||
return hash
|
||||
}
|
||||
if (Array.isArray(value.target_hashes) && value.target_hashes.length > 0) {
|
||||
return `targets ${value.target_hashes.length}`
|
||||
}
|
||||
return trimDeleteItemText(JSON.stringify(value, null, 2), 120)
|
||||
}
|
||||
|
||||
export 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
|
||||
}
|
||||
|
||||
export function formatFeedbackRelationTriplet(value: unknown): string {
|
||||
const triplet = pickFeedbackRelationTriplet(value)
|
||||
if (!triplet) {
|
||||
return ''
|
||||
}
|
||||
return formatDeleteRelationText(
|
||||
String(triplet.subject ?? ''),
|
||||
String(triplet.predicate ?? ''),
|
||||
String(triplet.object ?? ''),
|
||||
)
|
||||
}
|
||||
|
||||
export 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: '',
|
||||
}
|
||||
}
|
||||
|
||||
export 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
|
||||
}
|
||||
|
||||
export 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 || '未知动作'
|
||||
}
|
||||
}
|
||||
|
||||
export 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 || '记录了一条动作日志'
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user