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:
DrSmoothl
2026-05-01 20:14:37 +08:00
parent d9a509b6c2
commit 9e48cd2848
11 changed files with 3878 additions and 2546 deletions

View File

@@ -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>
)
}