Files
mai-bot/dashboard/src/routes/resource/knowledge-base/tabs/FeedbackTab.tsx
DrSmoothl 9e48cd2848 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.
2026-05-01 20:14:37 +08:00

513 lines
28 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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>
)
}