2233 lines
88 KiB
TypeScript
2233 lines
88 KiB
TypeScript
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||
|
||
import {
|
||
Database,
|
||
Gauge,
|
||
Loader2,
|
||
RefreshCw,
|
||
RotateCcw,
|
||
SlidersHorizontal,
|
||
Sparkles,
|
||
Upload,
|
||
CheckCircle2,
|
||
CircleAlert,
|
||
FolderOpen,
|
||
HardDrive,
|
||
} from 'lucide-react'
|
||
|
||
import { CodeEditor } from '@/components/CodeEditor'
|
||
import { MemoryDeleteDialog } from '@/components/memory/MemoryDeleteDialog'
|
||
import { MemoryMiniTabs } from '@/components/memory/MemoryMiniTabs'
|
||
import { Alert, AlertDescription } from '@/components/ui/alert'
|
||
import { Button } from '@/components/ui/button'
|
||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||
import {
|
||
Dialog,
|
||
DialogContent,
|
||
DialogDescription,
|
||
DialogFooter,
|
||
DialogHeader,
|
||
DialogTitle,
|
||
} from '@/components/ui/dialog'
|
||
import { Label } from '@/components/ui/label'
|
||
import { Tabs, TabsContent } from '@/components/ui/tabs'
|
||
import { Textarea } from '@/components/ui/textarea'
|
||
import { useToast } from '@/hooks/use-toast'
|
||
import { memoryProgressClient, type MemoryProgressEvent } from '@/lib/memory-progress-client'
|
||
import { cn } from '@/lib/utils'
|
||
import {
|
||
cancelMemoryImportTask,
|
||
createMemoryLpmmConvertImport,
|
||
createMemoryLpmmOpenieImport,
|
||
createMemoryMaibotMigrationImport,
|
||
createMemoryRawScanImport,
|
||
createMemoryTemporalBackfillImport,
|
||
executeMemoryDelete,
|
||
getMemoryFeedbackCorrection,
|
||
getMemoryFeedbackCorrections,
|
||
getMemoryImportPathAliases,
|
||
getMemoryImportSettings,
|
||
getMemoryImportTask,
|
||
getMemoryImportTaskChunks,
|
||
applyBestMemoryTuningProfile,
|
||
createMemoryPasteImport,
|
||
createMemoryTuningTask,
|
||
createMemoryUploadImport,
|
||
getMemoryDeleteOperation,
|
||
getMemoryDeleteOperations,
|
||
getMemoryImportTasks,
|
||
getMemoryRuntimeConfig,
|
||
getMemorySources,
|
||
getMemoryTuningProfile,
|
||
getMemoryTuningTasks,
|
||
type MemoryDeleteRequestPayload,
|
||
type MemoryImportChunkListPayload,
|
||
type MemoryImportInputMode,
|
||
type MemoryImportSettings,
|
||
type MemoryImportTaskKind,
|
||
type MemoryImportTaskPayload,
|
||
previewMemoryDelete,
|
||
refreshMemoryRuntimeSelfCheck,
|
||
rollbackMemoryFeedbackCorrection,
|
||
resolveMemoryImportPath,
|
||
retryMemoryImportTask,
|
||
restoreMemoryDelete,
|
||
type MemoryDeleteExecutePayload,
|
||
type MemoryDeleteOperationPayload,
|
||
type MemoryFeedbackActionLogPayload,
|
||
type MemoryFeedbackCorrectionDetailTaskPayload,
|
||
type MemoryFeedbackCorrectionSummaryPayload,
|
||
type MemorySourceItemPayload,
|
||
type MemoryRuntimeConfigPayload,
|
||
type MemoryTaskPayload,
|
||
} from '@/lib/memory-api'
|
||
|
||
import {
|
||
DELETE_OPERATION_FETCH_LIMIT,
|
||
DELETE_OPERATION_ITEM_PAGE_SIZE,
|
||
DELETE_OPERATION_PAGE_SIZE,
|
||
FEEDBACK_ACTION_LOG_PAGE_SIZE,
|
||
FEEDBACK_CORRECTION_FETCH_LIMIT,
|
||
FEEDBACK_CORRECTION_PAGE_SIZE,
|
||
IMPORT_CHUNK_PAGE_SIZE,
|
||
QUEUED_IMPORT_STATUS,
|
||
RUNNING_IMPORT_STATUS,
|
||
} from './knowledge-base/constants'
|
||
import {
|
||
buildFeedbackImpactSummary,
|
||
getFeedbackCorrectionPreview,
|
||
parseCommaSeparatedList,
|
||
parseOptionalPositiveInt,
|
||
summarizeFeedbackActionPayload,
|
||
} from './knowledge-base/utils'
|
||
import { DeleteTab } from './knowledge-base/tabs/DeleteTab'
|
||
import { FeedbackTab } from './knowledge-base/tabs/FeedbackTab'
|
||
import { ImportTab } from './knowledge-base/tabs/ImportTab'
|
||
import { TuningTab } from './knowledge-base/tabs/TuningTab'
|
||
import { KnowledgeGraphPage } from './knowledge-graph'
|
||
|
||
export function KnowledgeBasePage() {
|
||
const { toast } = useToast()
|
||
const [loading, setLoading] = useState(true)
|
||
const [refreshingCheck, setRefreshingCheck] = useState(false)
|
||
const [creatingImport, setCreatingImport] = useState(false)
|
||
const [creatingTuning, setCreatingTuning] = useState(false)
|
||
const [activeTab, setActiveTab] = useState<
|
||
'overview' | 'graph' | 'import' | 'tuning' | 'delete' | 'feedback'
|
||
>('overview')
|
||
|
||
const [runtimeConfig, setRuntimeConfig] = useState<MemoryRuntimeConfigPayload | null>(null)
|
||
const [selfCheckReport, setSelfCheckReport] = useState<Record<string, unknown> | null>(null)
|
||
const [importSettings, setImportSettings] = useState<MemoryImportSettings>({})
|
||
const [importPathAliases, setImportPathAliases] = useState<Record<string, string>>({})
|
||
const [importTasks, setImportTasks] = useState<MemoryImportTaskPayload[]>([])
|
||
const [selectedImportTaskId, setSelectedImportTaskId] = useState('')
|
||
const [selectedImportTask, setSelectedImportTask] = useState<MemoryImportTaskPayload | null>(null)
|
||
const [selectedImportTaskLoading, setSelectedImportTaskLoading] = useState(false)
|
||
const [selectedImportFileId, setSelectedImportFileId] = useState('')
|
||
const [importChunkOffset, setImportChunkOffset] = useState(0)
|
||
const [importChunksPayload, setImportChunksPayload] = useState<MemoryImportChunkListPayload | null>(null)
|
||
const [importChunksLoading, setImportChunksLoading] = useState(false)
|
||
const [importCreateMode, setImportCreateMode] = useState<MemoryImportTaskKind>('upload')
|
||
const [importAutoPolling, setImportAutoPolling] = useState(true)
|
||
const [importErrorText, setImportErrorText] = useState('')
|
||
const [importCommonFileConcurrency, setImportCommonFileConcurrency] = useState('2')
|
||
const [importCommonChunkConcurrency, setImportCommonChunkConcurrency] = useState('4')
|
||
const [importCommonLlmEnabled, setImportCommonLlmEnabled] = useState(true)
|
||
const [importCommonStrategyOverride, setImportCommonStrategyOverride] = useState('auto')
|
||
const [importCommonDedupePolicy, setImportCommonDedupePolicy] = useState('content_hash')
|
||
const [importCommonChatLog, setImportCommonChatLog] = useState(false)
|
||
const [importCommonChatReferenceTime, setImportCommonChatReferenceTime] = useState('')
|
||
const [importCommonForce, setImportCommonForce] = useState(false)
|
||
const [importCommonClearManifest, setImportCommonClearManifest] = useState(false)
|
||
|
||
const [uploadInputMode, setUploadInputMode] = useState<MemoryImportInputMode>('text')
|
||
const [uploadFiles, setUploadFiles] = useState<File[]>([])
|
||
|
||
const [pasteName, setPasteName] = useState('')
|
||
const [pasteMode, setPasteMode] = useState<MemoryImportInputMode>('text')
|
||
const [pasteContent, setPasteContent] = useState('')
|
||
|
||
const [rawAlias, setRawAlias] = useState('raw')
|
||
const [rawRelativePath, setRawRelativePath] = useState('')
|
||
const [rawGlob, setRawGlob] = useState('*')
|
||
const [rawInputMode, setRawInputMode] = useState<MemoryImportInputMode>('text')
|
||
const [rawRecursive, setRawRecursive] = useState(true)
|
||
|
||
const [openieAlias, setOpenieAlias] = useState('lpmm')
|
||
const [openieRelativePath, setOpenieRelativePath] = useState('')
|
||
const [openieIncludeAllJson, setOpenieIncludeAllJson] = useState(false)
|
||
|
||
const [convertAlias, setConvertAlias] = useState('lpmm')
|
||
const [convertRelativePath, setConvertRelativePath] = useState('')
|
||
const [convertTargetAlias, setConvertTargetAlias] = useState('plugin_data')
|
||
const [convertTargetRelativePath, setConvertTargetRelativePath] = useState('')
|
||
const [convertDimension, setConvertDimension] = useState('')
|
||
const [convertBatchSize, setConvertBatchSize] = useState('1024')
|
||
|
||
const [backfillAlias, setBackfillAlias] = useState('plugin_data')
|
||
const [backfillRelativePath, setBackfillRelativePath] = useState('')
|
||
const [backfillLimit, setBackfillLimit] = useState('100000')
|
||
const [backfillDryRun, setBackfillDryRun] = useState(false)
|
||
const [backfillNoCreatedFallback, setBackfillNoCreatedFallback] = useState(false)
|
||
|
||
const [maibotSourceDb, setMaibotSourceDb] = useState('')
|
||
const [maibotTimeFrom, setMaibotTimeFrom] = useState('')
|
||
const [maibotTimeTo, setMaibotTimeTo] = useState('')
|
||
const [maibotStartId, setMaibotStartId] = useState('')
|
||
const [maibotEndId, setMaibotEndId] = useState('')
|
||
const [maibotStreamIds, setMaibotStreamIds] = useState('')
|
||
const [maibotGroupIds, setMaibotGroupIds] = useState('')
|
||
const [maibotUserIds, setMaibotUserIds] = useState('')
|
||
const [maibotReadBatchSize, setMaibotReadBatchSize] = useState('2000')
|
||
const [maibotCommitWindowRows, setMaibotCommitWindowRows] = useState('20000')
|
||
const [maibotEmbedWorkers, setMaibotEmbedWorkers] = useState('')
|
||
const [maibotNoResume, setMaibotNoResume] = useState(false)
|
||
const [maibotResetState, setMaibotResetState] = useState(false)
|
||
const [maibotDryRun, setMaibotDryRun] = useState(false)
|
||
const [maibotVerifyOnly, setMaibotVerifyOnly] = useState(false)
|
||
|
||
const [pathResolveAlias, setPathResolveAlias] = useState('raw')
|
||
const [pathResolveRelativePath, setPathResolveRelativePath] = useState('')
|
||
const [pathResolveMustExist, setPathResolveMustExist] = useState(true)
|
||
const [pathResolveOutput, setPathResolveOutput] = useState('')
|
||
const [resolvingPath, setResolvingPath] = useState(false)
|
||
|
||
const [tuningTasks, setTuningTasks] = useState<MemoryTaskPayload[]>([])
|
||
const [tuningProfile, setTuningProfile] = useState<Record<string, unknown>>({})
|
||
const [tuningProfileToml, setTuningProfileToml] = useState('')
|
||
const [memorySources, setMemorySources] = useState<MemorySourceItemPayload[]>([])
|
||
const [deleteOperations, setDeleteOperations] = useState<MemoryDeleteOperationPayload[]>([])
|
||
const [selectedOperationDetail, setSelectedOperationDetail] = useState<MemoryDeleteOperationPayload | null>(null)
|
||
const [selectedOperationDetailLoading, setSelectedOperationDetailLoading] = useState(false)
|
||
const [selectedOperationDetailError, setSelectedOperationDetailError] = useState('')
|
||
const [sourceSearch, setSourceSearch] = useState('')
|
||
const [operationSearch, setOperationSearch] = useState('')
|
||
const [operationModeFilter, setOperationModeFilter] = useState('all')
|
||
const [operationStatusFilter, setOperationStatusFilter] = useState('all')
|
||
const [operationPage, setOperationPage] = useState(1)
|
||
const [selectedOperationId, setSelectedOperationId] = useState('')
|
||
const [selectedOperationItemSearch, setSelectedOperationItemSearch] = useState('')
|
||
const [selectedOperationItemPage, setSelectedOperationItemPage] = useState(1)
|
||
const [selectedSources, setSelectedSources] = useState<string[]>([])
|
||
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false)
|
||
const [deleteDialogTitle, setDeleteDialogTitle] = useState('删除预览')
|
||
const [deleteDialogDescription, setDeleteDialogDescription] = useState('')
|
||
const [deletePreview, setDeletePreview] = useState<Awaited<ReturnType<typeof previewMemoryDelete>> | null>(null)
|
||
const [deletePreviewError, setDeletePreviewError] = useState<string | null>(null)
|
||
const [deletePreviewLoading, setDeletePreviewLoading] = useState(false)
|
||
const [deleteExecuting, setDeleteExecuting] = useState(false)
|
||
const [deleteRestoring, setDeleteRestoring] = useState(false)
|
||
const [deleteResult, setDeleteResult] = useState<MemoryDeleteExecutePayload | null>(null)
|
||
const [pendingDeleteRequest, setPendingDeleteRequest] = useState<MemoryDeleteRequestPayload | null>(null)
|
||
const [feedbackCorrections, setFeedbackCorrections] = useState<MemoryFeedbackCorrectionSummaryPayload[]>([])
|
||
const [feedbackSearch, setFeedbackSearch] = useState('')
|
||
const [feedbackStatusFilter, setFeedbackStatusFilter] = useState('all')
|
||
const [feedbackRollbackFilter, setFeedbackRollbackFilter] = useState('all')
|
||
const [feedbackPage, setFeedbackPage] = useState(1)
|
||
const [selectedFeedbackTaskId, setSelectedFeedbackTaskId] = useState(0)
|
||
const [selectedFeedbackTaskDetail, setSelectedFeedbackTaskDetail] = useState<MemoryFeedbackCorrectionDetailTaskPayload | null>(null)
|
||
const [selectedFeedbackTaskLoading, setSelectedFeedbackTaskLoading] = useState(false)
|
||
const [selectedFeedbackTaskError, setSelectedFeedbackTaskError] = useState('')
|
||
const [feedbackActionLogSearch, setFeedbackActionLogSearch] = useState('')
|
||
const [feedbackActionLogPage, setFeedbackActionLogPage] = useState(1)
|
||
const [feedbackRollbackDialogOpen, setFeedbackRollbackDialogOpen] = useState(false)
|
||
const [feedbackRollbackReason, setFeedbackRollbackReason] = useState('')
|
||
const [feedbackRollingBack, setFeedbackRollingBack] = useState(false)
|
||
const [tuningObjective, setTuningObjective] = useState('precision_priority')
|
||
const [tuningIntensity, setTuningIntensity] = useState('standard')
|
||
const [tuningSampleSize, setTuningSampleSize] = useState('24')
|
||
const [tuningTopKEval, setTuningTopKEval] = useState('20')
|
||
|
||
const loadPage = useCallback(async () => {
|
||
try {
|
||
setLoading(true)
|
||
const [
|
||
runtimePayload,
|
||
importSettingsPayload,
|
||
pathAliasPayload,
|
||
importTaskPayload,
|
||
tuningProfilePayload,
|
||
tuningTaskPayload,
|
||
sourcePayload,
|
||
deleteOperationPayload,
|
||
feedbackCorrectionPayload,
|
||
] = await Promise.all([
|
||
getMemoryRuntimeConfig(),
|
||
getMemoryImportSettings(),
|
||
getMemoryImportPathAliases(),
|
||
getMemoryImportTasks(20),
|
||
getMemoryTuningProfile(),
|
||
getMemoryTuningTasks(20),
|
||
getMemorySources(),
|
||
getMemoryDeleteOperations(DELETE_OPERATION_FETCH_LIMIT),
|
||
getMemoryFeedbackCorrections({ limit: FEEDBACK_CORRECTION_FETCH_LIMIT }),
|
||
])
|
||
|
||
setRuntimeConfig(runtimePayload)
|
||
setImportSettings(importSettingsPayload.settings ?? {})
|
||
setImportPathAliases(pathAliasPayload.path_aliases ?? {})
|
||
setImportTasks(importTaskPayload.items ?? [])
|
||
setTuningProfile(tuningProfilePayload.profile ?? {})
|
||
setTuningProfileToml(tuningProfilePayload.toml ?? '')
|
||
setTuningTasks(tuningTaskPayload.items ?? [])
|
||
setMemorySources(sourcePayload.items ?? [])
|
||
setDeleteOperations(deleteOperationPayload.items ?? [])
|
||
setFeedbackCorrections(feedbackCorrectionPayload.items ?? [])
|
||
if (!selectedImportTaskId && (importTaskPayload.items ?? []).length > 0) {
|
||
const initialTaskId = String(importTaskPayload.items?.[0]?.task_id ?? '')
|
||
if (initialTaskId) {
|
||
setSelectedImportTaskId(initialTaskId)
|
||
}
|
||
}
|
||
if (!maibotSourceDb && String(importSettingsPayload.settings?.maibot_source_db_default ?? '').trim()) {
|
||
setMaibotSourceDb(String(importSettingsPayload.settings?.maibot_source_db_default ?? '').trim())
|
||
}
|
||
if (!pathResolveAlias) {
|
||
const aliasKeys = Object.keys(pathAliasPayload.path_aliases ?? {})
|
||
if (aliasKeys.length > 0) {
|
||
setPathResolveAlias(aliasKeys[0])
|
||
}
|
||
}
|
||
} catch (error) {
|
||
toast({
|
||
title: '加载长期记忆控制台失败',
|
||
description: error instanceof Error ? error.message : '未知错误',
|
||
variant: 'destructive',
|
||
})
|
||
} finally {
|
||
setLoading(false)
|
||
}
|
||
}, [maibotSourceDb, pathResolveAlias, selectedImportTaskId, toast])
|
||
|
||
useEffect(() => {
|
||
void loadPage()
|
||
}, [loadPage])
|
||
|
||
const runtimeBadges = useMemo(() => {
|
||
if (!runtimeConfig) {
|
||
return []
|
||
}
|
||
return [
|
||
{
|
||
label: '运行状态',
|
||
value: runtimeConfig.runtime_ready ? '就绪' : '未就绪',
|
||
description: runtimeConfig.embedding_degraded ? 'Embedding 降级运行' : '运行时检查通过',
|
||
icon: runtimeConfig.runtime_ready ? CheckCircle2 : CircleAlert,
|
||
className: runtimeConfig.runtime_ready ? 'border-emerald-500/20 bg-emerald-500/5' : 'border-amber-500/20 bg-amber-500/5',
|
||
iconClassName: runtimeConfig.runtime_ready ? 'text-emerald-500' : 'text-amber-500',
|
||
},
|
||
{
|
||
label: 'Embedding 维度',
|
||
value: String(runtimeConfig.embedding_dimension),
|
||
description: runtimeConfig.relation_vectors_enabled ? '关系向量已启用' : '关系向量未启用',
|
||
icon: HardDrive,
|
||
className: 'border-sky-500/20 bg-sky-500/5',
|
||
iconClassName: 'text-sky-500',
|
||
},
|
||
{
|
||
label: '自动保存',
|
||
value: runtimeConfig.auto_save ? '开启' : '关闭',
|
||
description: runtimeConfig.auto_save ? '运行数据会自动落盘' : '请留意手动保存',
|
||
icon: runtimeConfig.auto_save ? CheckCircle2 : CircleAlert,
|
||
className: runtimeConfig.auto_save ? 'border-primary/20 bg-primary/5' : 'border-muted-foreground/20 bg-muted/30',
|
||
iconClassName: runtimeConfig.auto_save ? 'text-primary' : 'text-muted-foreground',
|
||
},
|
||
{
|
||
label: '数据目录',
|
||
value: runtimeConfig.data_dir,
|
||
description: '长期记忆存储位置',
|
||
icon: FolderOpen,
|
||
className: 'border-violet-500/20 bg-violet-500/5',
|
||
iconClassName: 'text-violet-500',
|
||
},
|
||
]
|
||
}, [runtimeConfig])
|
||
|
||
const importPollInterval = useMemo(
|
||
() => Math.max(200, Number(importSettings.poll_interval_ms ?? 1000)),
|
||
[importSettings.poll_interval_ms],
|
||
)
|
||
|
||
const importAliasKeys = useMemo(
|
||
() => Object.keys(importPathAliases).sort((left, right) => left.localeCompare(right)),
|
||
[importPathAliases],
|
||
)
|
||
|
||
const runningImportTasks = useMemo(
|
||
() => importTasks.filter((task) => RUNNING_IMPORT_STATUS.has(String(task.status ?? '').trim())),
|
||
[importTasks],
|
||
)
|
||
const queuedImportTasks = useMemo(
|
||
() => importTasks.filter((task) => QUEUED_IMPORT_STATUS.has(String(task.status ?? '').trim())),
|
||
[importTasks],
|
||
)
|
||
const recentImportTasks = useMemo(
|
||
() =>
|
||
importTasks.filter((task) => {
|
||
const status = String(task.status ?? '').trim()
|
||
return !RUNNING_IMPORT_STATUS.has(status) && !QUEUED_IMPORT_STATUS.has(status)
|
||
}),
|
||
[importTasks],
|
||
)
|
||
const selectedImportTaskSummary = useMemo(() => {
|
||
if (!selectedImportTaskId) {
|
||
return null
|
||
}
|
||
return importTasks.find((task) => task.task_id === selectedImportTaskId) ?? null
|
||
}, [importTasks, selectedImportTaskId])
|
||
|
||
const selectedImportFiles = useMemo(() => {
|
||
return Array.isArray(selectedImportTask?.files) ? selectedImportTask.files : []
|
||
}, [selectedImportTask?.files])
|
||
|
||
const selectedImportChunks = useMemo(() => {
|
||
return Array.isArray(importChunksPayload?.items) ? importChunksPayload.items : []
|
||
}, [importChunksPayload?.items])
|
||
|
||
const selectedImportTaskResolved = selectedImportTask ?? selectedImportTaskSummary
|
||
const selectedImportTaskErrorText = String(selectedImportTaskResolved?.error ?? '').trim()
|
||
const selectedImportRetrySummary = selectedImportTaskResolved?.retry_summary
|
||
|
||
const importChunkTotal = Number(importChunksPayload?.total ?? 0)
|
||
const canImportChunkPrev = importChunkOffset > 0
|
||
const canImportChunkNext = importChunkOffset + IMPORT_CHUNK_PAGE_SIZE < importChunkTotal
|
||
|
||
const buildCommonImportPayload = useCallback((): Record<string, unknown> => {
|
||
const payload: Record<string, unknown> = {
|
||
llm_enabled: importCommonLlmEnabled,
|
||
strategy_override: importCommonStrategyOverride,
|
||
dedupe_policy: importCommonDedupePolicy,
|
||
chat_log: importCommonChatLog,
|
||
force: importCommonForce,
|
||
clear_manifest: importCommonClearManifest,
|
||
}
|
||
|
||
const fileConcurrency = parseOptionalPositiveInt(importCommonFileConcurrency)
|
||
const chunkConcurrency = parseOptionalPositiveInt(importCommonChunkConcurrency)
|
||
if (fileConcurrency !== undefined) {
|
||
payload.file_concurrency = fileConcurrency
|
||
}
|
||
if (chunkConcurrency !== undefined) {
|
||
payload.chunk_concurrency = chunkConcurrency
|
||
}
|
||
if (importCommonChatReferenceTime.trim()) {
|
||
payload.chat_reference_time = importCommonChatReferenceTime.trim()
|
||
}
|
||
return payload
|
||
}, [
|
||
importCommonChatLog,
|
||
importCommonChatReferenceTime,
|
||
importCommonChunkConcurrency,
|
||
importCommonClearManifest,
|
||
importCommonDedupePolicy,
|
||
importCommonFileConcurrency,
|
||
importCommonForce,
|
||
importCommonLlmEnabled,
|
||
importCommonStrategyOverride,
|
||
])
|
||
|
||
const refreshImportQueue = useCallback(async (silent: boolean = false) => {
|
||
try {
|
||
const [taskPayload, settingsPayload, pathAliasPayload] = await Promise.all([
|
||
getMemoryImportTasks(20),
|
||
getMemoryImportSettings(),
|
||
getMemoryImportPathAliases(),
|
||
])
|
||
const nextTasks = taskPayload.items ?? []
|
||
setImportTasks(nextTasks)
|
||
setImportSettings(settingsPayload.settings ?? {})
|
||
setImportPathAliases(pathAliasPayload.path_aliases ?? {})
|
||
setImportErrorText('')
|
||
|
||
if (nextTasks.length <= 0) {
|
||
setSelectedImportTaskId('')
|
||
setSelectedImportTask(null)
|
||
setSelectedImportFileId('')
|
||
setImportChunksPayload(null)
|
||
return
|
||
}
|
||
|
||
if (!selectedImportTaskId || !nextTasks.some((item) => item.task_id === selectedImportTaskId)) {
|
||
setSelectedImportTaskId(nextTasks[0].task_id)
|
||
}
|
||
} catch (error) {
|
||
const message = error instanceof Error ? error.message : '刷新导入任务失败'
|
||
setImportErrorText(message)
|
||
if (!silent) {
|
||
toast({
|
||
title: '刷新导入任务失败',
|
||
description: message,
|
||
variant: 'destructive',
|
||
})
|
||
}
|
||
}
|
||
}, [selectedImportTaskId, toast])
|
||
|
||
const loadImportChunks = useCallback(
|
||
async (
|
||
taskId: string,
|
||
fileId: string,
|
||
offset: number = 0,
|
||
silent: boolean = false,
|
||
) => {
|
||
if (!taskId || !fileId) {
|
||
setImportChunksPayload(null)
|
||
return
|
||
}
|
||
try {
|
||
setImportChunksLoading(true)
|
||
const payload = await getMemoryImportTaskChunks(taskId, fileId, offset, IMPORT_CHUNK_PAGE_SIZE)
|
||
if (!payload.success) {
|
||
throw new Error(payload.error || '加载分块详情失败')
|
||
}
|
||
setImportChunksPayload(payload)
|
||
setImportErrorText('')
|
||
} catch (error) {
|
||
const message = error instanceof Error ? error.message : '加载分块详情失败'
|
||
setImportChunksPayload(null)
|
||
setImportErrorText(message)
|
||
if (!silent) {
|
||
toast({
|
||
title: '加载分块详情失败',
|
||
description: message,
|
||
variant: 'destructive',
|
||
})
|
||
}
|
||
} finally {
|
||
setImportChunksLoading(false)
|
||
}
|
||
},
|
||
[toast],
|
||
)
|
||
|
||
const loadImportTaskDetail = useCallback(
|
||
async (taskId: string, silent: boolean = false) => {
|
||
if (!taskId) {
|
||
setSelectedImportTask(null)
|
||
setSelectedImportFileId('')
|
||
setImportChunksPayload(null)
|
||
return
|
||
}
|
||
try {
|
||
if (!silent) {
|
||
setSelectedImportTaskLoading(true)
|
||
}
|
||
const payload = await getMemoryImportTask(taskId, false)
|
||
if (!payload.success || !payload.task) {
|
||
throw new Error(payload.error || '任务不存在')
|
||
}
|
||
const task = payload.task
|
||
setSelectedImportTask(task)
|
||
setImportErrorText('')
|
||
const files = Array.isArray(task.files) ? task.files : []
|
||
const keepCurrentFile = files.some((file) => file.file_id === selectedImportFileId)
|
||
const nextFileId = keepCurrentFile ? selectedImportFileId : String(files[0]?.file_id ?? '')
|
||
const nextOffset = keepCurrentFile ? importChunkOffset : 0
|
||
if (!keepCurrentFile) {
|
||
setImportChunkOffset(0)
|
||
}
|
||
setSelectedImportFileId(nextFileId)
|
||
if (nextFileId) {
|
||
await loadImportChunks(taskId, nextFileId, nextOffset, silent)
|
||
} else {
|
||
setImportChunksPayload(null)
|
||
}
|
||
} catch (error) {
|
||
const message = error instanceof Error ? error.message : '加载导入任务详情失败'
|
||
setSelectedImportTask(null)
|
||
setSelectedImportFileId('')
|
||
setImportChunksPayload(null)
|
||
setImportErrorText(message)
|
||
if (!silent) {
|
||
toast({
|
||
title: '加载导入任务详情失败',
|
||
description: message,
|
||
variant: 'destructive',
|
||
})
|
||
}
|
||
} finally {
|
||
if (!silent) {
|
||
setSelectedImportTaskLoading(false)
|
||
}
|
||
}
|
||
},
|
||
[importChunkOffset, loadImportChunks, selectedImportFileId, toast],
|
||
)
|
||
|
||
const afterImportTaskCreated = useCallback(
|
||
async (taskId: string, successTitle: string) => {
|
||
await refreshImportQueue(true)
|
||
if (taskId) {
|
||
setSelectedImportTaskId(taskId)
|
||
await loadImportTaskDetail(taskId, true)
|
||
}
|
||
toast({
|
||
title: successTitle,
|
||
description: taskId ? `任务 ${taskId.slice(0, 12)} 已加入导入队列` : '导入任务已加入队列',
|
||
})
|
||
},
|
||
[loadImportTaskDetail, refreshImportQueue, toast],
|
||
)
|
||
|
||
const submitUploadImport = useCallback(async () => {
|
||
if (uploadFiles.length <= 0) {
|
||
toast({
|
||
title: '请选择上传文件',
|
||
description: '至少选择一个 txt/md/json 文件后再提交',
|
||
variant: 'destructive',
|
||
})
|
||
return
|
||
}
|
||
try {
|
||
setCreatingImport(true)
|
||
const payload = {
|
||
...buildCommonImportPayload(),
|
||
input_mode: uploadInputMode,
|
||
}
|
||
const result = await createMemoryUploadImport(uploadFiles, payload)
|
||
if (!result.success) {
|
||
throw new Error(result.error || '创建上传导入任务失败')
|
||
}
|
||
const taskId = String(result.task?.task_id ?? '')
|
||
setUploadFiles([])
|
||
await afterImportTaskCreated(taskId, '上传导入任务已创建')
|
||
} catch (error) {
|
||
const message = error instanceof Error ? error.message : '创建上传导入任务失败'
|
||
setImportErrorText(message)
|
||
toast({
|
||
title: '创建上传导入任务失败',
|
||
description: message,
|
||
variant: 'destructive',
|
||
})
|
||
} finally {
|
||
setCreatingImport(false)
|
||
}
|
||
}, [afterImportTaskCreated, buildCommonImportPayload, toast, uploadFiles, uploadInputMode])
|
||
|
||
const submitPasteImport = useCallback(async () => {
|
||
if (!pasteContent.trim()) {
|
||
toast({
|
||
title: '粘贴内容不能为空',
|
||
description: '请填写导入内容后再提交',
|
||
variant: 'destructive',
|
||
})
|
||
return
|
||
}
|
||
try {
|
||
setCreatingImport(true)
|
||
const result = await createMemoryPasteImport({
|
||
...buildCommonImportPayload(),
|
||
name: pasteName || undefined,
|
||
content: pasteContent,
|
||
input_mode: pasteMode,
|
||
})
|
||
if (!result.success) {
|
||
throw new Error(result.error || '创建粘贴导入任务失败')
|
||
}
|
||
const taskId = String(result.task?.task_id ?? '')
|
||
setPasteContent('')
|
||
setPasteName('')
|
||
await afterImportTaskCreated(taskId, '粘贴导入任务已创建')
|
||
} catch (error) {
|
||
const message = error instanceof Error ? error.message : '创建粘贴导入任务失败'
|
||
setImportErrorText(message)
|
||
toast({
|
||
title: '创建粘贴导入任务失败',
|
||
description: message,
|
||
variant: 'destructive',
|
||
})
|
||
} finally {
|
||
setCreatingImport(false)
|
||
}
|
||
}, [afterImportTaskCreated, buildCommonImportPayload, pasteContent, pasteMode, pasteName, toast])
|
||
|
||
const submitRawScanImport = useCallback(async () => {
|
||
try {
|
||
setCreatingImport(true)
|
||
const result = await createMemoryRawScanImport({
|
||
...buildCommonImportPayload(),
|
||
alias: rawAlias,
|
||
relative_path: rawRelativePath,
|
||
glob: rawGlob,
|
||
recursive: rawRecursive,
|
||
input_mode: rawInputMode,
|
||
})
|
||
if (!result.success) {
|
||
throw new Error(result.error || '创建本地扫描任务失败')
|
||
}
|
||
await afterImportTaskCreated(String(result.task?.task_id ?? ''), '本地扫描任务已创建')
|
||
} catch (error) {
|
||
const message = error instanceof Error ? error.message : '创建本地扫描任务失败'
|
||
setImportErrorText(message)
|
||
toast({
|
||
title: '创建本地扫描任务失败',
|
||
description: message,
|
||
variant: 'destructive',
|
||
})
|
||
} finally {
|
||
setCreatingImport(false)
|
||
}
|
||
}, [
|
||
afterImportTaskCreated,
|
||
buildCommonImportPayload,
|
||
rawAlias,
|
||
rawGlob,
|
||
rawInputMode,
|
||
rawRecursive,
|
||
rawRelativePath,
|
||
toast,
|
||
])
|
||
|
||
const submitOpenieImport = useCallback(async () => {
|
||
try {
|
||
setCreatingImport(true)
|
||
const result = await createMemoryLpmmOpenieImport({
|
||
...buildCommonImportPayload(),
|
||
alias: openieAlias,
|
||
relative_path: openieRelativePath,
|
||
include_all_json: openieIncludeAllJson,
|
||
})
|
||
if (!result.success) {
|
||
throw new Error(result.error || '创建 LPMM OpenIE 任务失败')
|
||
}
|
||
await afterImportTaskCreated(String(result.task?.task_id ?? ''), 'LPMM OpenIE 任务已创建')
|
||
} catch (error) {
|
||
const message = error instanceof Error ? error.message : '创建 LPMM OpenIE 任务失败'
|
||
setImportErrorText(message)
|
||
toast({
|
||
title: '创建 LPMM OpenIE 任务失败',
|
||
description: message,
|
||
variant: 'destructive',
|
||
})
|
||
} finally {
|
||
setCreatingImport(false)
|
||
}
|
||
}, [
|
||
afterImportTaskCreated,
|
||
buildCommonImportPayload,
|
||
openieAlias,
|
||
openieIncludeAllJson,
|
||
openieRelativePath,
|
||
toast,
|
||
])
|
||
|
||
const submitConvertImport = useCallback(async () => {
|
||
try {
|
||
setCreatingImport(true)
|
||
const result = await createMemoryLpmmConvertImport({
|
||
alias: convertAlias,
|
||
relative_path: convertRelativePath,
|
||
target_alias: convertTargetAlias,
|
||
target_relative_path: convertTargetRelativePath,
|
||
dimension: parseOptionalPositiveInt(convertDimension),
|
||
batch_size: parseOptionalPositiveInt(convertBatchSize),
|
||
})
|
||
if (!result.success) {
|
||
throw new Error(result.error || '创建 LPMM 转换任务失败')
|
||
}
|
||
await afterImportTaskCreated(String(result.task?.task_id ?? ''), 'LPMM 转换任务已创建')
|
||
} catch (error) {
|
||
const message = error instanceof Error ? error.message : '创建 LPMM 转换任务失败'
|
||
setImportErrorText(message)
|
||
toast({
|
||
title: '创建 LPMM 转换任务失败',
|
||
description: message,
|
||
variant: 'destructive',
|
||
})
|
||
} finally {
|
||
setCreatingImport(false)
|
||
}
|
||
}, [
|
||
afterImportTaskCreated,
|
||
convertAlias,
|
||
convertBatchSize,
|
||
convertDimension,
|
||
convertRelativePath,
|
||
convertTargetAlias,
|
||
convertTargetRelativePath,
|
||
toast,
|
||
])
|
||
|
||
const submitBackfillImport = useCallback(async () => {
|
||
try {
|
||
setCreatingImport(true)
|
||
const result = await createMemoryTemporalBackfillImport({
|
||
alias: backfillAlias,
|
||
relative_path: backfillRelativePath,
|
||
limit: parseOptionalPositiveInt(backfillLimit),
|
||
dry_run: backfillDryRun,
|
||
no_created_fallback: backfillNoCreatedFallback,
|
||
})
|
||
if (!result.success) {
|
||
throw new Error(result.error || '创建时序回填任务失败')
|
||
}
|
||
await afterImportTaskCreated(String(result.task?.task_id ?? ''), '时序回填任务已创建')
|
||
} catch (error) {
|
||
const message = error instanceof Error ? error.message : '创建时序回填任务失败'
|
||
setImportErrorText(message)
|
||
toast({
|
||
title: '创建时序回填任务失败',
|
||
description: message,
|
||
variant: 'destructive',
|
||
})
|
||
} finally {
|
||
setCreatingImport(false)
|
||
}
|
||
}, [
|
||
afterImportTaskCreated,
|
||
backfillAlias,
|
||
backfillDryRun,
|
||
backfillLimit,
|
||
backfillNoCreatedFallback,
|
||
backfillRelativePath,
|
||
toast,
|
||
])
|
||
|
||
const submitMaibotMigrationImport = useCallback(async () => {
|
||
try {
|
||
setCreatingImport(true)
|
||
const result = await createMemoryMaibotMigrationImport({
|
||
source_db: maibotSourceDb || undefined,
|
||
time_from: maibotTimeFrom || undefined,
|
||
time_to: maibotTimeTo || undefined,
|
||
start_id: parseOptionalPositiveInt(maibotStartId),
|
||
end_id: parseOptionalPositiveInt(maibotEndId),
|
||
stream_ids: parseCommaSeparatedList(maibotStreamIds),
|
||
group_ids: parseCommaSeparatedList(maibotGroupIds),
|
||
user_ids: parseCommaSeparatedList(maibotUserIds),
|
||
read_batch_size: parseOptionalPositiveInt(maibotReadBatchSize),
|
||
commit_window_rows: parseOptionalPositiveInt(maibotCommitWindowRows),
|
||
embed_workers: parseOptionalPositiveInt(maibotEmbedWorkers),
|
||
no_resume: maibotNoResume,
|
||
reset_state: maibotResetState,
|
||
dry_run: maibotDryRun,
|
||
verify_only: maibotVerifyOnly,
|
||
})
|
||
if (!result.success) {
|
||
throw new Error(result.error || '创建 MaiBot 迁移任务失败')
|
||
}
|
||
await afterImportTaskCreated(String(result.task?.task_id ?? ''), 'MaiBot 迁移任务已创建')
|
||
} catch (error) {
|
||
const message = error instanceof Error ? error.message : '创建 MaiBot 迁移任务失败'
|
||
setImportErrorText(message)
|
||
toast({
|
||
title: '创建 MaiBot 迁移任务失败',
|
||
description: message,
|
||
variant: 'destructive',
|
||
})
|
||
} finally {
|
||
setCreatingImport(false)
|
||
}
|
||
}, [
|
||
afterImportTaskCreated,
|
||
maibotCommitWindowRows,
|
||
maibotDryRun,
|
||
maibotEmbedWorkers,
|
||
maibotEndId,
|
||
maibotGroupIds,
|
||
maibotNoResume,
|
||
maibotReadBatchSize,
|
||
maibotResetState,
|
||
maibotSourceDb,
|
||
maibotStartId,
|
||
maibotStreamIds,
|
||
maibotTimeFrom,
|
||
maibotTimeTo,
|
||
maibotUserIds,
|
||
maibotVerifyOnly,
|
||
toast,
|
||
])
|
||
|
||
const cancelSelectedImportTask = useCallback(async () => {
|
||
if (!selectedImportTaskId) {
|
||
return
|
||
}
|
||
try {
|
||
const payload = await cancelMemoryImportTask(selectedImportTaskId)
|
||
if (!payload.success) {
|
||
throw new Error(payload.error || '取消导入任务失败')
|
||
}
|
||
await refreshImportQueue(true)
|
||
await loadImportTaskDetail(selectedImportTaskId, true)
|
||
toast({
|
||
title: '已请求取消任务',
|
||
description: `任务 ${selectedImportTaskId.slice(0, 12)} 正在取消`,
|
||
})
|
||
} catch (error) {
|
||
const message = error instanceof Error ? error.message : '取消导入任务失败'
|
||
setImportErrorText(message)
|
||
toast({
|
||
title: '取消导入任务失败',
|
||
description: message,
|
||
variant: 'destructive',
|
||
})
|
||
}
|
||
}, [loadImportTaskDetail, refreshImportQueue, selectedImportTaskId, toast])
|
||
|
||
const retrySelectedImportTask = useCallback(async () => {
|
||
if (!selectedImportTaskId) {
|
||
return
|
||
}
|
||
try {
|
||
const payload = await retryMemoryImportTask(selectedImportTaskId, {
|
||
overrides: buildCommonImportPayload(),
|
||
})
|
||
if (!payload.success) {
|
||
throw new Error(payload.error || '重试失败项失败')
|
||
}
|
||
const nextTaskId = String(payload.task?.task_id ?? '')
|
||
await refreshImportQueue(true)
|
||
if (nextTaskId) {
|
||
setSelectedImportTaskId(nextTaskId)
|
||
await loadImportTaskDetail(nextTaskId, true)
|
||
} else {
|
||
await loadImportTaskDetail(selectedImportTaskId, true)
|
||
}
|
||
toast({
|
||
title: '重试任务已创建',
|
||
description: nextTaskId ? `重试任务 ${nextTaskId.slice(0, 12)} 已进入队列` : '失败项已提交重试',
|
||
})
|
||
} catch (error) {
|
||
const message = error instanceof Error ? error.message : '重试失败项失败'
|
||
setImportErrorText(message)
|
||
toast({
|
||
title: '重试失败项失败',
|
||
description: message,
|
||
variant: 'destructive',
|
||
})
|
||
}
|
||
}, [buildCommonImportPayload, loadImportTaskDetail, refreshImportQueue, selectedImportTaskId, toast])
|
||
|
||
const resolveImportPath = useCallback(async () => {
|
||
if (!pathResolveAlias.trim()) {
|
||
return
|
||
}
|
||
try {
|
||
setResolvingPath(true)
|
||
const payload = await resolveMemoryImportPath({
|
||
alias: pathResolveAlias,
|
||
relative_path: pathResolveRelativePath,
|
||
must_exist: pathResolveMustExist,
|
||
})
|
||
const lines = [
|
||
`路径别名: ${payload.alias}`,
|
||
`相对路径: ${payload.relative_path || '(空)'}`,
|
||
`解析结果: ${payload.resolved_path}`,
|
||
`是否存在: ${String(payload.exists)}`,
|
||
`是否文件: ${String(payload.is_file)}`,
|
||
`是否目录: ${String(payload.is_dir)}`,
|
||
]
|
||
setPathResolveOutput(lines.join('\n'))
|
||
} catch (error) {
|
||
const message = error instanceof Error ? error.message : '路径解析失败'
|
||
setPathResolveOutput(`解析失败:${message}`)
|
||
} finally {
|
||
setResolvingPath(false)
|
||
}
|
||
}, [pathResolveAlias, pathResolveMustExist, pathResolveRelativePath])
|
||
|
||
const selectImportTask = useCallback(
|
||
async (taskId: string) => {
|
||
setSelectedImportTaskId(taskId)
|
||
setImportChunkOffset(0)
|
||
await loadImportTaskDetail(taskId)
|
||
},
|
||
[loadImportTaskDetail],
|
||
)
|
||
|
||
const selectImportFile = useCallback(
|
||
async (fileId: string) => {
|
||
if (!selectedImportTaskId) {
|
||
return
|
||
}
|
||
setSelectedImportFileId(fileId)
|
||
setImportChunkOffset(0)
|
||
await loadImportChunks(selectedImportTaskId, fileId, 0)
|
||
},
|
||
[loadImportChunks, selectedImportTaskId],
|
||
)
|
||
|
||
const moveImportChunkPage = useCallback(
|
||
async (direction: -1 | 1) => {
|
||
if (!selectedImportTaskId || !selectedImportFileId) {
|
||
return
|
||
}
|
||
const nextOffset =
|
||
direction < 0
|
||
? Math.max(0, importChunkOffset - IMPORT_CHUNK_PAGE_SIZE)
|
||
: importChunkOffset + IMPORT_CHUNK_PAGE_SIZE
|
||
if (nextOffset === importChunkOffset) {
|
||
return
|
||
}
|
||
setImportChunkOffset(nextOffset)
|
||
await loadImportChunks(selectedImportTaskId, selectedImportFileId, nextOffset)
|
||
},
|
||
[importChunkOffset, loadImportChunks, selectedImportFileId, selectedImportTaskId],
|
||
)
|
||
|
||
useEffect(() => {
|
||
if (importAliasKeys.length <= 0) {
|
||
return
|
||
}
|
||
const pickAlias = (current: string, preferred: string): string => {
|
||
if (current && importAliasKeys.includes(current)) {
|
||
return current
|
||
}
|
||
if (importAliasKeys.includes(preferred)) {
|
||
return preferred
|
||
}
|
||
return importAliasKeys[0]
|
||
}
|
||
setRawAlias((current) => pickAlias(current, 'raw'))
|
||
setOpenieAlias((current) => pickAlias(current, 'lpmm'))
|
||
setConvertAlias((current) => pickAlias(current, 'lpmm'))
|
||
setConvertTargetAlias((current) => pickAlias(current, 'plugin_data'))
|
||
setBackfillAlias((current) => pickAlias(current, 'plugin_data'))
|
||
setPathResolveAlias((current) => pickAlias(current, 'raw'))
|
||
}, [importAliasKeys])
|
||
|
||
useEffect(() => {
|
||
const defaultFileConcurrency = String(importSettings.default_file_concurrency ?? '').trim()
|
||
const defaultChunkConcurrency = String(importSettings.default_chunk_concurrency ?? '').trim()
|
||
if (defaultFileConcurrency && importCommonFileConcurrency === '2') {
|
||
setImportCommonFileConcurrency(defaultFileConcurrency)
|
||
}
|
||
if (defaultChunkConcurrency && importCommonChunkConcurrency === '4') {
|
||
setImportCommonChunkConcurrency(defaultChunkConcurrency)
|
||
}
|
||
const defaultSourceDb = String(importSettings.maibot_source_db_default ?? '').trim()
|
||
if (defaultSourceDb && !maibotSourceDb.trim()) {
|
||
setMaibotSourceDb(defaultSourceDb)
|
||
}
|
||
}, [
|
||
importCommonChunkConcurrency,
|
||
importCommonFileConcurrency,
|
||
importSettings.default_chunk_concurrency,
|
||
importSettings.default_file_concurrency,
|
||
importSettings.maibot_source_db_default,
|
||
maibotSourceDb,
|
||
])
|
||
|
||
useEffect(() => {
|
||
if (!selectedImportTaskId && importTasks.length > 0) {
|
||
void selectImportTask(importTasks[0].task_id)
|
||
}
|
||
}, [importTasks, selectImportTask, selectedImportTaskId])
|
||
|
||
useEffect(() => {
|
||
if (!selectedImportTaskId) {
|
||
setSelectedImportTask(null)
|
||
setSelectedImportFileId('')
|
||
setImportChunksPayload(null)
|
||
return
|
||
}
|
||
if (!importTasks.some((task) => task.task_id === selectedImportTaskId) && importTasks.length > 0) {
|
||
void selectImportTask(importTasks[0].task_id)
|
||
return
|
||
}
|
||
void loadImportTaskDetail(selectedImportTaskId, true)
|
||
}, [importTasks, loadImportTaskDetail, selectImportTask, selectedImportTaskId])
|
||
|
||
useEffect(() => {
|
||
if (!importAutoPolling) {
|
||
return
|
||
}
|
||
const timerId = window.setInterval(() => {
|
||
void refreshImportQueue(true)
|
||
if (selectedImportTaskId) {
|
||
void loadImportTaskDetail(selectedImportTaskId, true)
|
||
}
|
||
}, importPollInterval)
|
||
return () => {
|
||
window.clearInterval(timerId)
|
||
}
|
||
}, [importAutoPolling, importPollInterval, loadImportTaskDetail, refreshImportQueue, selectedImportTaskId])
|
||
|
||
// 统一 WebSocket 推送:作为轮询的实时增强;后端未广播时由轮询兜底
|
||
const selectedImportTaskIdRef = useRef<string>('')
|
||
useEffect(() => {
|
||
selectedImportTaskIdRef.current = selectedImportTaskId
|
||
}, [selectedImportTaskId])
|
||
|
||
useEffect(() => {
|
||
let cancelled = false
|
||
let unsubscribe: (() => Promise<void>) | undefined
|
||
const handleEvent = (event: MemoryProgressEvent) => {
|
||
if (event.topic === 'import_progress') {
|
||
void refreshImportQueue(true)
|
||
if (selectedImportTaskIdRef.current) {
|
||
void loadImportTaskDetail(selectedImportTaskIdRef.current, true)
|
||
}
|
||
}
|
||
}
|
||
void memoryProgressClient
|
||
.subscribe(handleEvent, ['import_progress'])
|
||
.then((cleanup) => {
|
||
if (cancelled) {
|
||
void cleanup()
|
||
return
|
||
}
|
||
unsubscribe = cleanup
|
||
})
|
||
.catch((error) => {
|
||
// 订阅失败不影响轮询兜底
|
||
console.warn('订阅长期记忆 WebSocket 失败,已退化到轮询兜底', error)
|
||
})
|
||
return () => {
|
||
cancelled = true
|
||
if (unsubscribe) {
|
||
void unsubscribe()
|
||
}
|
||
}
|
||
}, [loadImportTaskDetail, refreshImportQueue])
|
||
|
||
const filteredSources = useMemo(() => {
|
||
const keyword = sourceSearch.trim().toLowerCase()
|
||
if (!keyword) {
|
||
return memorySources
|
||
}
|
||
return memorySources.filter((item) => String(item.source ?? '').toLowerCase().includes(keyword))
|
||
}, [memorySources, sourceSearch])
|
||
|
||
const filteredDeleteOperations = useMemo(() => {
|
||
const keyword = operationSearch.trim().toLowerCase()
|
||
return deleteOperations.filter((operation) => {
|
||
const mode = String(operation.mode ?? '').trim()
|
||
const status = String(operation.status ?? '').trim()
|
||
const summary = operation.summary ?? {}
|
||
const sources = Array.isArray(summary.sources) ? summary.sources : []
|
||
|
||
if (operationModeFilter !== 'all' && mode !== operationModeFilter) {
|
||
return false
|
||
}
|
||
if (operationStatusFilter !== 'all' && status !== operationStatusFilter) {
|
||
return false
|
||
}
|
||
if (!keyword) {
|
||
return true
|
||
}
|
||
|
||
return [
|
||
operation.operation_id,
|
||
operation.reason,
|
||
operation.requested_by,
|
||
mode,
|
||
status,
|
||
...sources.map((item) => String(item)),
|
||
]
|
||
.map((item) => String(item ?? '').toLowerCase())
|
||
.some((item) => item.includes(keyword))
|
||
})
|
||
}, [deleteOperations, operationModeFilter, operationSearch, operationStatusFilter])
|
||
|
||
const deleteOperationPageCount = Math.max(1, Math.ceil(filteredDeleteOperations.length / DELETE_OPERATION_PAGE_SIZE))
|
||
const pagedDeleteOperations = useMemo(() => {
|
||
const start = (operationPage - 1) * DELETE_OPERATION_PAGE_SIZE
|
||
return filteredDeleteOperations.slice(start, start + DELETE_OPERATION_PAGE_SIZE)
|
||
}, [filteredDeleteOperations, operationPage])
|
||
|
||
const selectedDeleteOperation = useMemo(
|
||
() => filteredDeleteOperations.find((operation) => operation.operation_id === selectedOperationId) ?? pagedDeleteOperations[0] ?? null,
|
||
[filteredDeleteOperations, pagedDeleteOperations, selectedOperationId],
|
||
)
|
||
|
||
useEffect(() => {
|
||
setOperationPage(1)
|
||
}, [operationSearch, operationModeFilter, operationStatusFilter])
|
||
|
||
useEffect(() => {
|
||
if (operationPage > deleteOperationPageCount) {
|
||
setOperationPage(deleteOperationPageCount)
|
||
}
|
||
}, [deleteOperationPageCount, operationPage])
|
||
|
||
useEffect(() => {
|
||
if (!selectedDeleteOperation) {
|
||
if (selectedOperationId) {
|
||
setSelectedOperationId('')
|
||
}
|
||
setSelectedOperationDetail(null)
|
||
setSelectedOperationDetailError('')
|
||
return
|
||
}
|
||
if (selectedDeleteOperation.operation_id !== selectedOperationId) {
|
||
setSelectedOperationId(selectedDeleteOperation.operation_id)
|
||
}
|
||
}, [selectedDeleteOperation, selectedOperationId])
|
||
|
||
useEffect(() => {
|
||
const operationId = selectedDeleteOperation?.operation_id
|
||
if (!operationId) {
|
||
setSelectedOperationDetail(null)
|
||
setSelectedOperationDetailError('')
|
||
return
|
||
}
|
||
|
||
let cancelled = false
|
||
setSelectedOperationDetailLoading(true)
|
||
setSelectedOperationDetailError('')
|
||
|
||
void getMemoryDeleteOperation(operationId)
|
||
.then((payload) => {
|
||
if (cancelled) {
|
||
return
|
||
}
|
||
if (!payload.success || !payload.operation) {
|
||
setSelectedOperationDetail(null)
|
||
setSelectedOperationDetailError(payload.error || '未能加载删除操作详情')
|
||
return
|
||
}
|
||
setSelectedOperationDetail(payload.operation)
|
||
})
|
||
.catch((error) => {
|
||
if (cancelled) {
|
||
return
|
||
}
|
||
setSelectedOperationDetail(null)
|
||
setSelectedOperationDetailError(error instanceof Error ? error.message : '未能加载删除操作详情')
|
||
})
|
||
.finally(() => {
|
||
if (!cancelled) {
|
||
setSelectedOperationDetailLoading(false)
|
||
}
|
||
})
|
||
|
||
return () => {
|
||
cancelled = true
|
||
}
|
||
}, [selectedDeleteOperation?.operation_id])
|
||
|
||
const toggleSourceSelection = useCallback((source: string, checked: boolean) => {
|
||
setSelectedSources((current) => {
|
||
if (checked) {
|
||
return current.includes(source) ? current : [...current, source]
|
||
}
|
||
return current.filter((item) => item !== source)
|
||
})
|
||
}, [])
|
||
|
||
const openSourceDeletePreview = useCallback(async () => {
|
||
if (selectedSources.length <= 0) {
|
||
toast({
|
||
title: '请选择来源',
|
||
description: '至少选择一个来源后再进行删除预览',
|
||
variant: 'destructive',
|
||
})
|
||
return
|
||
}
|
||
const request: MemoryDeleteRequestPayload = {
|
||
mode: 'source',
|
||
selector: { sources: selectedSources },
|
||
reason: 'knowledge_base_source_delete',
|
||
requested_by: 'knowledge_base',
|
||
}
|
||
setDeleteDialogTitle('批量删除来源')
|
||
setDeleteDialogDescription('删除来源只会删除该来源下的段落,以及失去全部证据的关系,不会自动删除实体')
|
||
setPendingDeleteRequest(request)
|
||
setDeletePreview(null)
|
||
setDeleteResult(null)
|
||
setDeletePreviewError(null)
|
||
setDeleteDialogOpen(true)
|
||
setDeletePreviewLoading(true)
|
||
try {
|
||
const preview = await previewMemoryDelete(request)
|
||
setDeletePreview(preview)
|
||
} catch (error) {
|
||
setDeletePreviewError(error instanceof Error ? error.message : '删除预览失败')
|
||
} finally {
|
||
setDeletePreviewLoading(false)
|
||
}
|
||
}, [selectedSources, toast])
|
||
|
||
const executePendingDelete = useCallback(async () => {
|
||
if (!pendingDeleteRequest) {
|
||
return
|
||
}
|
||
try {
|
||
setDeleteExecuting(true)
|
||
const result = await executeMemoryDelete(pendingDeleteRequest)
|
||
setDeleteResult(result)
|
||
toast({
|
||
title: result.success ? '删除成功' : '删除失败',
|
||
description: result.success ? `操作 ${result.operation_id} 已完成` : result.error || '未能执行删除',
|
||
variant: result.success ? 'default' : 'destructive',
|
||
})
|
||
if (result.success) {
|
||
const [sourcePayload, deleteOperationPayload] = await Promise.all([
|
||
getMemorySources(),
|
||
getMemoryDeleteOperations(DELETE_OPERATION_FETCH_LIMIT),
|
||
])
|
||
setMemorySources(sourcePayload.items ?? [])
|
||
setDeleteOperations(deleteOperationPayload.items ?? [])
|
||
setSelectedSources([])
|
||
}
|
||
} catch (error) {
|
||
setDeletePreviewError(error instanceof Error ? error.message : '删除失败')
|
||
toast({
|
||
title: '删除失败',
|
||
description: error instanceof Error ? error.message : '未知错误',
|
||
variant: 'destructive',
|
||
})
|
||
} finally {
|
||
setDeleteExecuting(false)
|
||
}
|
||
}, [pendingDeleteRequest, toast])
|
||
|
||
const restoreDeleteOperation = useCallback(async (operationId: string) => {
|
||
try {
|
||
setDeleteRestoring(true)
|
||
await restoreMemoryDelete({ operation_id: operationId, requested_by: 'knowledge_base' })
|
||
toast({
|
||
title: '恢复成功',
|
||
description: `删除操作 ${operationId} 已恢复`,
|
||
})
|
||
setDeleteDialogOpen(false)
|
||
const [sourcePayload, deleteOperationPayload] = await Promise.all([
|
||
getMemorySources(),
|
||
getMemoryDeleteOperations(DELETE_OPERATION_FETCH_LIMIT),
|
||
])
|
||
setMemorySources(sourcePayload.items ?? [])
|
||
setDeleteOperations(deleteOperationPayload.items ?? [])
|
||
} catch (error) {
|
||
toast({
|
||
title: '恢复失败',
|
||
description: error instanceof Error ? error.message : '未知错误',
|
||
variant: 'destructive',
|
||
})
|
||
} finally {
|
||
setDeleteRestoring(false)
|
||
}
|
||
}, [toast])
|
||
|
||
const closeDeleteDialog = useCallback((open: boolean) => {
|
||
if (!open) {
|
||
setDeleteDialogOpen(false)
|
||
setDeletePreview(null)
|
||
setDeleteResult(null)
|
||
setDeletePreviewError(null)
|
||
setPendingDeleteRequest(null)
|
||
return
|
||
}
|
||
setDeleteDialogOpen(true)
|
||
}, [])
|
||
|
||
const filteredFeedbackCorrections = useMemo(() => {
|
||
const keyword = feedbackSearch.trim().toLowerCase()
|
||
return feedbackCorrections.filter((item) => {
|
||
const taskStatus = String(item.task_status ?? '').trim().toLowerCase()
|
||
const rollbackStatus = String(item.rollback_status ?? '').trim().toLowerCase()
|
||
if (feedbackStatusFilter !== 'all' && taskStatus !== feedbackStatusFilter) {
|
||
return false
|
||
}
|
||
if (feedbackRollbackFilter !== 'all' && rollbackStatus !== feedbackRollbackFilter) {
|
||
return false
|
||
}
|
||
if (!keyword) {
|
||
return true
|
||
}
|
||
return [
|
||
item.query_tool_id,
|
||
item.session_id,
|
||
item.query_text,
|
||
item.decision,
|
||
item.task_status,
|
||
item.rollback_status,
|
||
]
|
||
.map((value) => String(value ?? '').toLowerCase())
|
||
.some((value) => value.includes(keyword))
|
||
})
|
||
}, [feedbackCorrections, feedbackRollbackFilter, feedbackSearch, feedbackStatusFilter])
|
||
|
||
const feedbackPageCount = Math.max(1, Math.ceil(filteredFeedbackCorrections.length / FEEDBACK_CORRECTION_PAGE_SIZE))
|
||
const pagedFeedbackCorrections = useMemo(() => {
|
||
const start = (feedbackPage - 1) * FEEDBACK_CORRECTION_PAGE_SIZE
|
||
return filteredFeedbackCorrections.slice(start, start + FEEDBACK_CORRECTION_PAGE_SIZE)
|
||
}, [feedbackPage, filteredFeedbackCorrections])
|
||
|
||
const selectedFeedbackCorrection = useMemo(
|
||
() =>
|
||
filteredFeedbackCorrections.find((item) => item.task_id === selectedFeedbackTaskId)
|
||
?? pagedFeedbackCorrections[0]
|
||
?? null,
|
||
[filteredFeedbackCorrections, pagedFeedbackCorrections, selectedFeedbackTaskId],
|
||
)
|
||
|
||
useEffect(() => {
|
||
setFeedbackPage(1)
|
||
}, [feedbackSearch, feedbackStatusFilter, feedbackRollbackFilter])
|
||
|
||
useEffect(() => {
|
||
if (feedbackPage > feedbackPageCount) {
|
||
setFeedbackPage(feedbackPageCount)
|
||
}
|
||
}, [feedbackPage, feedbackPageCount])
|
||
|
||
useEffect(() => {
|
||
if (!selectedFeedbackCorrection) {
|
||
if (selectedFeedbackTaskId) {
|
||
setSelectedFeedbackTaskId(0)
|
||
}
|
||
setSelectedFeedbackTaskDetail(null)
|
||
setSelectedFeedbackTaskError('')
|
||
return
|
||
}
|
||
if (selectedFeedbackCorrection.task_id !== selectedFeedbackTaskId) {
|
||
setSelectedFeedbackTaskId(selectedFeedbackCorrection.task_id)
|
||
}
|
||
}, [selectedFeedbackCorrection, selectedFeedbackTaskId])
|
||
|
||
useEffect(() => {
|
||
const taskId = selectedFeedbackCorrection?.task_id
|
||
if (!taskId) {
|
||
setSelectedFeedbackTaskDetail(null)
|
||
setSelectedFeedbackTaskError('')
|
||
return
|
||
}
|
||
|
||
let cancelled = false
|
||
setSelectedFeedbackTaskLoading(true)
|
||
setSelectedFeedbackTaskError('')
|
||
|
||
void getMemoryFeedbackCorrection(taskId)
|
||
.then((payload) => {
|
||
if (cancelled) {
|
||
return
|
||
}
|
||
if (!payload.success || !payload.task) {
|
||
setSelectedFeedbackTaskDetail(null)
|
||
setSelectedFeedbackTaskError(payload.error || '未能加载纠错任务详情')
|
||
return
|
||
}
|
||
setSelectedFeedbackTaskDetail(payload.task)
|
||
})
|
||
.catch((error) => {
|
||
if (cancelled) {
|
||
return
|
||
}
|
||
setSelectedFeedbackTaskDetail(null)
|
||
setSelectedFeedbackTaskError(error instanceof Error ? error.message : '未能加载纠错任务详情')
|
||
})
|
||
.finally(() => {
|
||
if (!cancelled) {
|
||
setSelectedFeedbackTaskLoading(false)
|
||
}
|
||
})
|
||
|
||
return () => {
|
||
cancelled = true
|
||
}
|
||
}, [selectedFeedbackCorrection?.task_id])
|
||
|
||
const selectedFeedbackResolved = useMemo<MemoryFeedbackCorrectionDetailTaskPayload | null>(() => {
|
||
if (!selectedFeedbackCorrection) {
|
||
return null
|
||
}
|
||
if (selectedFeedbackTaskDetail?.task_id === selectedFeedbackCorrection.task_id) {
|
||
return {
|
||
...selectedFeedbackCorrection,
|
||
...selectedFeedbackTaskDetail,
|
||
} satisfies MemoryFeedbackCorrectionDetailTaskPayload
|
||
}
|
||
return selectedFeedbackTaskDetail ?? selectedFeedbackCorrection
|
||
}, [selectedFeedbackCorrection, selectedFeedbackTaskDetail])
|
||
const selectedFeedbackPreview = useMemo(
|
||
() => getFeedbackCorrectionPreview(selectedFeedbackResolved),
|
||
[selectedFeedbackResolved],
|
||
)
|
||
const selectedFeedbackImpactSummary = useMemo(
|
||
() => buildFeedbackImpactSummary(selectedFeedbackResolved),
|
||
[selectedFeedbackResolved],
|
||
)
|
||
|
||
const selectedFeedbackActionLogs: MemoryFeedbackActionLogPayload[] = Array.isArray(selectedFeedbackResolved?.action_logs)
|
||
? selectedFeedbackResolved.action_logs
|
||
: []
|
||
const filteredFeedbackActionLogs = useMemo(() => {
|
||
const keyword = feedbackActionLogSearch.trim().toLowerCase()
|
||
if (!keyword) {
|
||
return selectedFeedbackActionLogs
|
||
}
|
||
return selectedFeedbackActionLogs.filter((item) =>
|
||
[
|
||
item.action_type,
|
||
item.target_hash,
|
||
item.reason,
|
||
summarizeFeedbackActionPayload(item.before_payload),
|
||
summarizeFeedbackActionPayload(item.after_payload),
|
||
]
|
||
.map((value) => String(value ?? '').toLowerCase())
|
||
.some((value) => value.includes(keyword)),
|
||
)
|
||
}, [feedbackActionLogSearch, selectedFeedbackActionLogs])
|
||
const feedbackActionLogPageCount = Math.max(
|
||
1,
|
||
Math.ceil(filteredFeedbackActionLogs.length / FEEDBACK_ACTION_LOG_PAGE_SIZE),
|
||
)
|
||
const pagedFeedbackActionLogs = useMemo(() => {
|
||
const start = (feedbackActionLogPage - 1) * FEEDBACK_ACTION_LOG_PAGE_SIZE
|
||
return filteredFeedbackActionLogs.slice(start, start + FEEDBACK_ACTION_LOG_PAGE_SIZE)
|
||
}, [feedbackActionLogPage, filteredFeedbackActionLogs])
|
||
|
||
useEffect(() => {
|
||
setFeedbackActionLogPage(1)
|
||
}, [selectedFeedbackTaskId, feedbackActionLogSearch])
|
||
|
||
useEffect(() => {
|
||
if (feedbackActionLogPage > feedbackActionLogPageCount) {
|
||
setFeedbackActionLogPage(feedbackActionLogPageCount)
|
||
}
|
||
}, [feedbackActionLogPage, feedbackActionLogPageCount])
|
||
|
||
const openFeedbackRollbackDialog = useCallback(() => {
|
||
setFeedbackRollbackReason('')
|
||
setFeedbackRollbackDialogOpen(true)
|
||
}, [])
|
||
|
||
const executeFeedbackRollback = useCallback(async () => {
|
||
const taskId = selectedFeedbackResolved?.task_id
|
||
if (!taskId) {
|
||
return
|
||
}
|
||
try {
|
||
setFeedbackRollingBack(true)
|
||
const payload = await rollbackMemoryFeedbackCorrection(taskId, {
|
||
requested_by: 'knowledge_base',
|
||
reason: feedbackRollbackReason.trim(),
|
||
})
|
||
if (!payload.success) {
|
||
throw new Error(payload.error || '回退失败')
|
||
}
|
||
toast({
|
||
title: payload.already_rolled_back ? '该纠错已回退' : '纠错回退成功',
|
||
description: `任务 ${taskId} 的回退结果已写入日志`,
|
||
})
|
||
setFeedbackRollbackDialogOpen(false)
|
||
const [listPayload, detailPayload] = await Promise.all([
|
||
getMemoryFeedbackCorrections({ limit: FEEDBACK_CORRECTION_FETCH_LIMIT }),
|
||
getMemoryFeedbackCorrection(taskId),
|
||
])
|
||
setFeedbackCorrections(listPayload.items ?? [])
|
||
setSelectedFeedbackTaskDetail(detailPayload.task ?? null)
|
||
const [sourcePayload, runtimePayload] = await Promise.all([
|
||
getMemorySources(),
|
||
getMemoryRuntimeConfig(),
|
||
])
|
||
setMemorySources(sourcePayload.items ?? [])
|
||
setRuntimeConfig(runtimePayload)
|
||
} catch (error) {
|
||
toast({
|
||
title: '纠错回退失败',
|
||
description: error instanceof Error ? error.message : '未知错误',
|
||
variant: 'destructive',
|
||
})
|
||
} finally {
|
||
setFeedbackRollingBack(false)
|
||
}
|
||
}, [feedbackRollbackReason, selectedFeedbackResolved?.task_id, toast])
|
||
|
||
const selectedOperationResolved = useMemo(() => {
|
||
if (!selectedDeleteOperation) {
|
||
return null
|
||
}
|
||
if (selectedOperationDetail?.operation_id === selectedDeleteOperation.operation_id) {
|
||
return {
|
||
...selectedDeleteOperation,
|
||
...selectedOperationDetail,
|
||
} satisfies MemoryDeleteOperationPayload
|
||
}
|
||
return selectedDeleteOperation
|
||
}, [selectedDeleteOperation, selectedOperationDetail])
|
||
const selectedOperationSummaryResolved = ((selectedOperationResolved?.summary ?? {}) as Record<string, unknown>)
|
||
const selectedOperationCounts = ((selectedOperationSummaryResolved.counts as Record<string, number> | undefined) ?? {})
|
||
const selectedOperationSources = Array.isArray(selectedOperationSummaryResolved.sources)
|
||
? selectedOperationSummaryResolved.sources.map((item) => String(item)).filter(Boolean)
|
||
: []
|
||
const selectedOperationItems = Array.isArray(selectedOperationResolved?.items)
|
||
? selectedOperationResolved.items
|
||
: []
|
||
const filteredSelectedOperationItems = useMemo(() => {
|
||
const keyword = selectedOperationItemSearch.trim().toLowerCase()
|
||
if (!keyword) {
|
||
return selectedOperationItems
|
||
}
|
||
return selectedOperationItems.filter((item) => {
|
||
const payload = item.payload ?? {}
|
||
const source = String(payload.source ?? '').trim()
|
||
return [
|
||
item.item_type,
|
||
item.item_hash,
|
||
item.item_key,
|
||
source,
|
||
]
|
||
.map((value) => String(value ?? '').toLowerCase())
|
||
.some((value) => value.includes(keyword))
|
||
})
|
||
}, [selectedOperationItemSearch, selectedOperationItems])
|
||
const selectedOperationItemPageCount = Math.max(
|
||
1,
|
||
Math.ceil(filteredSelectedOperationItems.length / DELETE_OPERATION_ITEM_PAGE_SIZE),
|
||
)
|
||
const pagedSelectedOperationItems = useMemo(() => {
|
||
const start = (selectedOperationItemPage - 1) * DELETE_OPERATION_ITEM_PAGE_SIZE
|
||
return filteredSelectedOperationItems.slice(start, start + DELETE_OPERATION_ITEM_PAGE_SIZE)
|
||
}, [filteredSelectedOperationItems, selectedOperationItemPage])
|
||
|
||
useEffect(() => {
|
||
setSelectedOperationItemPage(1)
|
||
}, [selectedOperationId, selectedOperationItemSearch])
|
||
|
||
useEffect(() => {
|
||
if (selectedOperationItemPage > selectedOperationItemPageCount) {
|
||
setSelectedOperationItemPage(selectedOperationItemPageCount)
|
||
}
|
||
}, [selectedOperationItemPage, selectedOperationItemPageCount])
|
||
|
||
const refreshSelfCheck = useCallback(async () => {
|
||
try {
|
||
setRefreshingCheck(true)
|
||
const payload = await refreshMemoryRuntimeSelfCheck()
|
||
setSelfCheckReport((payload.report ?? null) as Record<string, unknown> | null)
|
||
const nextRuntime = await getMemoryRuntimeConfig()
|
||
setRuntimeConfig(nextRuntime)
|
||
toast({
|
||
title: payload.success ? '自检通过' : '自检未通过',
|
||
description: payload.success ? '运行时状态正常' : '请检查 embedding 配置和外部服务连通性',
|
||
variant: payload.success ? 'default' : 'destructive',
|
||
})
|
||
} catch (error) {
|
||
toast({
|
||
title: '运行时自检失败',
|
||
description: error instanceof Error ? error.message : '未知错误',
|
||
variant: 'destructive',
|
||
})
|
||
} finally {
|
||
setRefreshingCheck(false)
|
||
}
|
||
}, [toast])
|
||
|
||
const submitImportByMode = useCallback(async () => {
|
||
if (creatingImport) {
|
||
return
|
||
}
|
||
switch (importCreateMode) {
|
||
case 'upload':
|
||
await submitUploadImport()
|
||
break
|
||
case 'paste':
|
||
await submitPasteImport()
|
||
break
|
||
case 'raw_scan':
|
||
await submitRawScanImport()
|
||
break
|
||
case 'lpmm_openie':
|
||
await submitOpenieImport()
|
||
break
|
||
case 'lpmm_convert':
|
||
await submitConvertImport()
|
||
break
|
||
case 'temporal_backfill':
|
||
await submitBackfillImport()
|
||
break
|
||
case 'maibot_migration':
|
||
await submitMaibotMigrationImport()
|
||
break
|
||
default:
|
||
break
|
||
}
|
||
}, [
|
||
creatingImport,
|
||
importCreateMode,
|
||
submitBackfillImport,
|
||
submitConvertImport,
|
||
submitMaibotMigrationImport,
|
||
submitOpenieImport,
|
||
submitPasteImport,
|
||
submitRawScanImport,
|
||
submitUploadImport,
|
||
])
|
||
|
||
const submitTuningTask = useCallback(async () => {
|
||
try {
|
||
setCreatingTuning(true)
|
||
await createMemoryTuningTask({
|
||
objective: tuningObjective,
|
||
intensity: tuningIntensity,
|
||
sample_size: Number(tuningSampleSize),
|
||
top_k_eval: Number(tuningTopKEval),
|
||
})
|
||
const tasks = await getMemoryTuningTasks(20)
|
||
setTuningTasks(tasks.items ?? [])
|
||
toast({ title: '调优任务已创建', description: '新的检索调优任务已经进入队列' })
|
||
} catch (error) {
|
||
toast({
|
||
title: '创建调优任务失败',
|
||
description: error instanceof Error ? error.message : '未知错误',
|
||
variant: 'destructive',
|
||
})
|
||
} finally {
|
||
setCreatingTuning(false)
|
||
}
|
||
}, [toast, tuningIntensity, tuningObjective, tuningSampleSize, tuningTopKEval])
|
||
|
||
const applyBestTask = useCallback(async (taskId: string) => {
|
||
try {
|
||
await applyBestMemoryTuningProfile(taskId)
|
||
const [profilePayload, runtimePayload, tuningTaskPayload] = await Promise.all([
|
||
getMemoryTuningProfile(),
|
||
getMemoryRuntimeConfig(),
|
||
getMemoryTuningTasks(20),
|
||
])
|
||
setTuningProfile(profilePayload.profile ?? {})
|
||
setTuningProfileToml(profilePayload.toml ?? '')
|
||
setRuntimeConfig(runtimePayload)
|
||
setTuningTasks(tuningTaskPayload.items ?? [])
|
||
toast({ title: '最佳参数已应用', description: `任务 ${taskId} 的最佳轮次已经写入运行时` })
|
||
} catch (error) {
|
||
toast({
|
||
title: '应用最佳参数失败',
|
||
description: error instanceof Error ? error.message : '未知错误',
|
||
variant: 'destructive',
|
||
})
|
||
}
|
||
}, [toast])
|
||
|
||
if (loading) {
|
||
return (
|
||
<div className="flex h-full items-center justify-center">
|
||
<div className="rounded-xl border bg-background px-6 py-5 text-sm text-muted-foreground shadow-sm">
|
||
正在加载长期记忆控制台...
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
return (
|
||
<div className="flex h-full flex-col bg-gradient-to-b from-background via-background to-muted/15">
|
||
<div className="flex-none border-b bg-card/70 px-6 py-4 backdrop-blur">
|
||
<div className="flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between">
|
||
<div>
|
||
<div className="text-[11px] font-medium uppercase tracking-[0.18em] text-primary/80">
|
||
A_Memorix
|
||
</div>
|
||
<h1 className="mt-1 text-2xl font-bold leading-tight">长期记忆控制台</h1>
|
||
<p className="mt-1 text-sm text-muted-foreground">
|
||
在这里完成自检、导入资料和检索调优——一站式管理记忆库
|
||
</p>
|
||
</div>
|
||
|
||
<div className="hidden">
|
||
<Button variant="outline" size="sm" onClick={() => setActiveTab('graph')}>
|
||
<Database className="mr-2 h-4 w-4" />
|
||
打开图谱
|
||
</Button>
|
||
<Button variant="outline" size="sm" onClick={() => void loadPage()}>
|
||
<RefreshCw className="mr-2 h-4 w-4" />
|
||
刷新数据
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="flex-1 overflow-auto">
|
||
<div className="mx-auto flex w-full max-w-[1800px] flex-col gap-6 px-6 py-6">
|
||
<div className="hidden">
|
||
<Button variant="outline" size="sm" onClick={() => void loadPage()}>
|
||
<RefreshCw className="mr-2 h-4 w-4" />
|
||
刷新数据
|
||
</Button>
|
||
</div>
|
||
{/* 运行时状态条 —— 紧凑、常驻、一眼看完 */}
|
||
{runtimeBadges.length > 0 ? (
|
||
<div className="rounded-2xl border border-border/60 bg-card/60 p-4 shadow-sm backdrop-blur">
|
||
<div className="mb-3 flex items-center gap-2">
|
||
<div className="mr-auto flex items-center gap-2 text-xs font-medium uppercase tracking-wider text-muted-foreground">
|
||
<Gauge className="h-3.5 w-3.5" />
|
||
运行时状态
|
||
</div>
|
||
<Button
|
||
variant="outline"
|
||
size="sm"
|
||
className="h-7 px-2 text-xs"
|
||
onClick={() => void loadPage()}
|
||
>
|
||
<RefreshCw className="mr-1.5 h-3 w-3" />
|
||
刷新数据
|
||
</Button>
|
||
<Button
|
||
variant="ghost"
|
||
size="sm"
|
||
className="h-7 px-2 text-xs"
|
||
onClick={() => void refreshSelfCheck()}
|
||
disabled={refreshingCheck}
|
||
>
|
||
<RefreshCw className={cn('mr-1.5 h-3 w-3', refreshingCheck && 'animate-spin')} />
|
||
自检
|
||
</Button>
|
||
</div>
|
||
<div className="grid gap-2.5 sm:grid-cols-2 lg:grid-cols-4">
|
||
{runtimeBadges.map((item) => (
|
||
<div
|
||
key={item.label}
|
||
className={cn(
|
||
'flex items-center gap-3 rounded-xl border px-3 py-2.5 transition-colors',
|
||
item.className,
|
||
)}
|
||
>
|
||
<div className="flex-none rounded-lg border bg-background/70 p-1.5 shadow-sm">
|
||
<item.icon className={cn('h-4 w-4', item.iconClassName)} />
|
||
</div>
|
||
<div className="min-w-0 flex-1">
|
||
<div className="text-[11px] font-medium text-muted-foreground">{item.label}</div>
|
||
<div className="truncate text-sm font-semibold leading-snug" title={item.value}>
|
||
{item.value}
|
||
</div>
|
||
<div className="mt-0.5 truncate text-[11px] text-muted-foreground">
|
||
{item.description}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
) : null}
|
||
|
||
{/* 快速开始 Hero —— 给新用户明确的"先做什么" */}
|
||
<div className="overflow-hidden rounded-2xl border border-primary/20 bg-gradient-to-br from-primary/10 via-primary/5 to-transparent p-5 shadow-sm">
|
||
<div className="flex flex-col gap-5 lg:flex-row lg:items-center lg:justify-between">
|
||
<div className="space-y-1.5 lg:max-w-sm">
|
||
<div className="text-[11px] font-medium uppercase tracking-[0.18em] text-primary">
|
||
快速开始
|
||
</div>
|
||
<h2 className="text-lg font-semibold leading-tight">先从这三件事入手</h2>
|
||
<p className="text-sm text-muted-foreground">
|
||
不知道该做什么?挑一个最常用的入口,下面的标签页里有更详细的设置。
|
||
</p>
|
||
</div>
|
||
<div className="grid w-full gap-2.5 sm:grid-cols-3 lg:max-w-3xl">
|
||
<button
|
||
type="button"
|
||
onClick={() => setActiveTab('import')}
|
||
className="group flex items-start gap-3 rounded-xl border border-border/70 bg-background/80 p-3.5 text-left transition hover:border-primary/50 hover:bg-background hover:shadow-md"
|
||
>
|
||
<div className="flex-none rounded-lg bg-primary/10 p-2 text-primary transition-transform group-hover:scale-105">
|
||
<Upload className="h-4 w-4" />
|
||
</div>
|
||
<div className="min-w-0">
|
||
<div className="text-sm font-semibold">导入资料</div>
|
||
<div className="mt-0.5 text-xs leading-relaxed text-muted-foreground">
|
||
把文件、聊天记录写进记忆库
|
||
</div>
|
||
</div>
|
||
</button>
|
||
<button
|
||
type="button"
|
||
onClick={() => setActiveTab('tuning')}
|
||
className="group flex items-start gap-3 rounded-xl border border-border/70 bg-background/80 p-3.5 text-left transition hover:border-primary/50 hover:bg-background hover:shadow-md"
|
||
>
|
||
<div className="flex-none rounded-lg bg-amber-500/10 p-2 text-amber-500 transition-transform group-hover:scale-105">
|
||
<SlidersHorizontal className="h-4 w-4" />
|
||
</div>
|
||
<div className="min-w-0">
|
||
<div className="text-sm font-semibold">检索调优</div>
|
||
<div className="mt-0.5 text-xs leading-relaxed text-muted-foreground">
|
||
让回忆变得更准、更聪明
|
||
</div>
|
||
</div>
|
||
</button>
|
||
<button
|
||
type="button"
|
||
onClick={() => setActiveTab('graph')}
|
||
className="group flex items-start gap-3 rounded-xl border border-border/70 bg-background/80 p-3.5 text-left transition hover:border-primary/50 hover:bg-background hover:shadow-md"
|
||
>
|
||
<div className="flex-none rounded-lg bg-violet-500/10 p-2 text-violet-500 transition-transform group-hover:scale-105">
|
||
<Database className="h-4 w-4" />
|
||
</div>
|
||
<div className="min-w-0">
|
||
<div className="text-sm font-semibold">打开图谱</div>
|
||
<div className="mt-0.5 text-xs leading-relaxed text-muted-foreground">
|
||
可视化已存的实体和关系
|
||
</div>
|
||
</div>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<Tabs
|
||
value={activeTab}
|
||
onValueChange={(value) => setActiveTab(value as typeof activeTab)}
|
||
className="space-y-5"
|
||
>
|
||
<div className="sticky top-0 z-10 -mx-6 border-b border-border/40 bg-background/85 px-6 pb-2 pt-1 backdrop-blur supports-[backdrop-filter]:bg-background/70">
|
||
<MemoryMiniTabs
|
||
items={[
|
||
{ value: 'overview', label: '概览', description: '运行状态与运行时摘要' },
|
||
{ value: 'graph', label: '图谱', description: '实体关系图与证据视图' },
|
||
{ value: 'import', label: '导入', description: '创建并管理导入任务' },
|
||
{ value: 'tuning', label: '调优', description: '检索策略调优' },
|
||
{ value: 'delete', label: '删除', description: '批量删除与历史回溯' },
|
||
{ value: 'feedback', label: '纠错历史', description: '查看反馈与回滚' },
|
||
]}
|
||
triggerClassName="px-4"
|
||
/>
|
||
</div>
|
||
|
||
<TabsContent value="graph" className="h-[calc(100vh-220px)] min-h-[720px] overflow-hidden rounded-2xl border border-border/60 bg-background shadow-sm">
|
||
<KnowledgeGraphPage embedded onOpenConsole={() => setActiveTab('overview')} />
|
||
</TabsContent>
|
||
|
||
<TabsContent value="overview" className="space-y-4">
|
||
<div className="grid gap-4 xl:grid-cols-[1.15fr_0.85fr]">
|
||
<Card>
|
||
<CardHeader className="flex flex-row items-start justify-between space-y-0">
|
||
<div>
|
||
<CardTitle className="flex items-center gap-2">
|
||
<Gauge className="h-4 w-4" />
|
||
运行时自检
|
||
</CardTitle>
|
||
<CardDescription>用于确认 embedding、向量库与运行时状态是否一致</CardDescription>
|
||
</div>
|
||
<Button size="sm" onClick={() => void refreshSelfCheck()} disabled={refreshingCheck}>
|
||
<RefreshCw className={`mr-2 h-4 w-4 ${refreshingCheck ? 'animate-spin' : ''}`} />
|
||
重新自检
|
||
</Button>
|
||
</CardHeader>
|
||
<CardContent className="space-y-3">
|
||
<Alert>
|
||
<AlertDescription>
|
||
长期记忆配置已移动到主程序配置,请在“主程序配置 / 长期记忆”中调整。
|
||
</AlertDescription>
|
||
</Alert>
|
||
<CodeEditor
|
||
value={JSON.stringify(selfCheckReport ?? runtimeConfig ?? {}, null, 2)}
|
||
language="json"
|
||
readOnly
|
||
height="320px"
|
||
/>
|
||
</CardContent>
|
||
</Card>
|
||
|
||
<Card>
|
||
<CardHeader>
|
||
<CardTitle className="flex items-center gap-2">
|
||
<Sparkles className="h-4 w-4" />
|
||
关键指标
|
||
</CardTitle>
|
||
<CardDescription>用于快速判断是否需要补回向量或重新调优</CardDescription>
|
||
</CardHeader>
|
||
<CardContent className="space-y-4 text-sm">
|
||
<div className="grid gap-3 md:grid-cols-2">
|
||
<div className="rounded-lg border bg-muted/30 p-3">
|
||
<div className="text-xs text-muted-foreground">待补回段落向量</div>
|
||
<div className="mt-1 text-2xl font-semibold">{runtimeConfig?.paragraph_vector_backfill_pending ?? 0}</div>
|
||
</div>
|
||
<div className="rounded-lg border bg-muted/30 p-3">
|
||
<div className="text-xs text-muted-foreground">失败补回任务</div>
|
||
<div className="mt-1 text-2xl font-semibold">{runtimeConfig?.paragraph_vector_backfill_failed ?? 0}</div>
|
||
</div>
|
||
</div>
|
||
<details className="rounded-lg border bg-muted/30 p-3" open>
|
||
<summary className="cursor-pointer text-xs font-medium text-muted-foreground">当前调优配置</summary>
|
||
<pre className="mt-2 max-h-48 overflow-auto whitespace-pre-wrap break-words text-xs">
|
||
{JSON.stringify(tuningProfile, null, 2)}
|
||
</pre>
|
||
</details>
|
||
</CardContent>
|
||
</Card>
|
||
</div>
|
||
</TabsContent>
|
||
|
||
<ImportTab
|
||
importCreateMode={importCreateMode}
|
||
setImportCreateMode={setImportCreateMode}
|
||
importSettings={importSettings}
|
||
importCommonFileConcurrency={importCommonFileConcurrency}
|
||
setImportCommonFileConcurrency={setImportCommonFileConcurrency}
|
||
importCommonChunkConcurrency={importCommonChunkConcurrency}
|
||
setImportCommonChunkConcurrency={setImportCommonChunkConcurrency}
|
||
importCommonLlmEnabled={importCommonLlmEnabled}
|
||
setImportCommonLlmEnabled={setImportCommonLlmEnabled}
|
||
importCommonChatLog={importCommonChatLog}
|
||
setImportCommonChatLog={setImportCommonChatLog}
|
||
importCommonStrategyOverride={importCommonStrategyOverride}
|
||
setImportCommonStrategyOverride={setImportCommonStrategyOverride}
|
||
importCommonDedupePolicy={importCommonDedupePolicy}
|
||
setImportCommonDedupePolicy={setImportCommonDedupePolicy}
|
||
importCommonChatReferenceTime={importCommonChatReferenceTime}
|
||
setImportCommonChatReferenceTime={setImportCommonChatReferenceTime}
|
||
importCommonForce={importCommonForce}
|
||
setImportCommonForce={setImportCommonForce}
|
||
importCommonClearManifest={importCommonClearManifest}
|
||
setImportCommonClearManifest={setImportCommonClearManifest}
|
||
uploadInputMode={uploadInputMode}
|
||
setUploadInputMode={setUploadInputMode}
|
||
uploadFiles={uploadFiles}
|
||
setUploadFiles={setUploadFiles}
|
||
pasteName={pasteName}
|
||
setPasteName={setPasteName}
|
||
pasteMode={pasteMode}
|
||
setPasteMode={setPasteMode}
|
||
pasteContent={pasteContent}
|
||
setPasteContent={setPasteContent}
|
||
rawAlias={rawAlias}
|
||
setRawAlias={setRawAlias}
|
||
rawInputMode={rawInputMode}
|
||
setRawInputMode={setRawInputMode}
|
||
rawRelativePath={rawRelativePath}
|
||
setRawRelativePath={setRawRelativePath}
|
||
rawGlob={rawGlob}
|
||
setRawGlob={setRawGlob}
|
||
rawRecursive={rawRecursive}
|
||
setRawRecursive={setRawRecursive}
|
||
openieAlias={openieAlias}
|
||
setOpenieAlias={setOpenieAlias}
|
||
openieRelativePath={openieRelativePath}
|
||
setOpenieRelativePath={setOpenieRelativePath}
|
||
openieIncludeAllJson={openieIncludeAllJson}
|
||
setOpenieIncludeAllJson={setOpenieIncludeAllJson}
|
||
convertAlias={convertAlias}
|
||
setConvertAlias={setConvertAlias}
|
||
convertTargetAlias={convertTargetAlias}
|
||
setConvertTargetAlias={setConvertTargetAlias}
|
||
convertRelativePath={convertRelativePath}
|
||
setConvertRelativePath={setConvertRelativePath}
|
||
convertTargetRelativePath={convertTargetRelativePath}
|
||
setConvertTargetRelativePath={setConvertTargetRelativePath}
|
||
convertDimension={convertDimension}
|
||
setConvertDimension={setConvertDimension}
|
||
convertBatchSize={convertBatchSize}
|
||
setConvertBatchSize={setConvertBatchSize}
|
||
backfillAlias={backfillAlias}
|
||
setBackfillAlias={setBackfillAlias}
|
||
backfillLimit={backfillLimit}
|
||
setBackfillLimit={setBackfillLimit}
|
||
backfillRelativePath={backfillRelativePath}
|
||
setBackfillRelativePath={setBackfillRelativePath}
|
||
backfillDryRun={backfillDryRun}
|
||
setBackfillDryRun={setBackfillDryRun}
|
||
backfillNoCreatedFallback={backfillNoCreatedFallback}
|
||
setBackfillNoCreatedFallback={setBackfillNoCreatedFallback}
|
||
maibotSourceDb={maibotSourceDb}
|
||
setMaibotSourceDb={setMaibotSourceDb}
|
||
maibotTimeFrom={maibotTimeFrom}
|
||
setMaibotTimeFrom={setMaibotTimeFrom}
|
||
maibotTimeTo={maibotTimeTo}
|
||
setMaibotTimeTo={setMaibotTimeTo}
|
||
maibotStartId={maibotStartId}
|
||
setMaibotStartId={setMaibotStartId}
|
||
maibotEndId={maibotEndId}
|
||
setMaibotEndId={setMaibotEndId}
|
||
maibotStreamIds={maibotStreamIds}
|
||
setMaibotStreamIds={setMaibotStreamIds}
|
||
maibotGroupIds={maibotGroupIds}
|
||
setMaibotGroupIds={setMaibotGroupIds}
|
||
maibotUserIds={maibotUserIds}
|
||
setMaibotUserIds={setMaibotUserIds}
|
||
maibotReadBatchSize={maibotReadBatchSize}
|
||
setMaibotReadBatchSize={setMaibotReadBatchSize}
|
||
maibotCommitWindowRows={maibotCommitWindowRows}
|
||
setMaibotCommitWindowRows={setMaibotCommitWindowRows}
|
||
maibotEmbedWorkers={maibotEmbedWorkers}
|
||
setMaibotEmbedWorkers={setMaibotEmbedWorkers}
|
||
maibotNoResume={maibotNoResume}
|
||
setMaibotNoResume={setMaibotNoResume}
|
||
maibotResetState={maibotResetState}
|
||
setMaibotResetState={setMaibotResetState}
|
||
maibotDryRun={maibotDryRun}
|
||
setMaibotDryRun={setMaibotDryRun}
|
||
maibotVerifyOnly={maibotVerifyOnly}
|
||
setMaibotVerifyOnly={setMaibotVerifyOnly}
|
||
submitImportByMode={submitImportByMode}
|
||
creatingImport={creatingImport}
|
||
pathResolveAlias={pathResolveAlias}
|
||
setPathResolveAlias={setPathResolveAlias}
|
||
importAliasKeys={importAliasKeys}
|
||
pathResolveRelativePath={pathResolveRelativePath}
|
||
setPathResolveRelativePath={setPathResolveRelativePath}
|
||
pathResolveMustExist={pathResolveMustExist}
|
||
setPathResolveMustExist={setPathResolveMustExist}
|
||
resolveImportPath={resolveImportPath}
|
||
resolvingPath={resolvingPath}
|
||
pathResolveOutput={pathResolveOutput}
|
||
refreshImportQueue={refreshImportQueue}
|
||
runningImportTasks={runningImportTasks}
|
||
queuedImportTasks={queuedImportTasks}
|
||
recentImportTasks={recentImportTasks}
|
||
selectedImportTaskId={selectedImportTaskId}
|
||
selectImportTask={selectImportTask}
|
||
importAutoPolling={importAutoPolling}
|
||
setImportAutoPolling={setImportAutoPolling}
|
||
importPollInterval={importPollInterval}
|
||
importErrorText={importErrorText}
|
||
cancelSelectedImportTask={cancelSelectedImportTask}
|
||
retrySelectedImportTask={retrySelectedImportTask}
|
||
selectedImportTaskLoading={selectedImportTaskLoading}
|
||
selectedImportTaskResolved={selectedImportTaskResolved}
|
||
selectedImportRetrySummary={selectedImportRetrySummary}
|
||
selectedImportTaskErrorText={selectedImportTaskErrorText}
|
||
selectedImportFiles={selectedImportFiles}
|
||
selectedImportFileId={selectedImportFileId}
|
||
selectImportFile={selectImportFile}
|
||
importChunkTotal={importChunkTotal}
|
||
importChunkOffset={importChunkOffset}
|
||
moveImportChunkPage={moveImportChunkPage}
|
||
canImportChunkPrev={canImportChunkPrev}
|
||
canImportChunkNext={canImportChunkNext}
|
||
importChunksLoading={importChunksLoading}
|
||
selectedImportChunks={selectedImportChunks}
|
||
/>
|
||
|
||
<TuningTab
|
||
tuningObjective={tuningObjective}
|
||
setTuningObjective={setTuningObjective}
|
||
tuningIntensity={tuningIntensity}
|
||
setTuningIntensity={setTuningIntensity}
|
||
tuningSampleSize={tuningSampleSize}
|
||
setTuningSampleSize={setTuningSampleSize}
|
||
tuningTopKEval={tuningTopKEval}
|
||
setTuningTopKEval={setTuningTopKEval}
|
||
submitTuningTask={submitTuningTask}
|
||
creatingTuning={creatingTuning}
|
||
tuningProfile={tuningProfile}
|
||
tuningProfileToml={tuningProfileToml}
|
||
tuningTasks={tuningTasks}
|
||
applyBestTask={applyBestTask}
|
||
/>
|
||
|
||
<DeleteTab
|
||
sourceSearch={sourceSearch}
|
||
setSourceSearch={setSourceSearch}
|
||
selectedSources={selectedSources}
|
||
setSelectedSources={setSelectedSources}
|
||
filteredSources={filteredSources}
|
||
openSourceDeletePreview={openSourceDeletePreview}
|
||
toggleSourceSelection={toggleSourceSelection}
|
||
operationSearch={operationSearch}
|
||
setOperationSearch={setOperationSearch}
|
||
operationModeFilter={operationModeFilter}
|
||
setOperationModeFilter={setOperationModeFilter}
|
||
operationStatusFilter={operationStatusFilter}
|
||
setOperationStatusFilter={setOperationStatusFilter}
|
||
filteredDeleteOperations={filteredDeleteOperations}
|
||
deleteOperations={deleteOperations}
|
||
operationPage={operationPage}
|
||
setOperationPage={setOperationPage}
|
||
deleteOperationPageCount={deleteOperationPageCount}
|
||
pagedDeleteOperations={pagedDeleteOperations}
|
||
selectedDeleteOperation={selectedDeleteOperation}
|
||
setSelectedOperationId={setSelectedOperationId}
|
||
restoreDeleteOperation={restoreDeleteOperation}
|
||
deleteRestoring={deleteRestoring}
|
||
selectedOperationCounts={selectedOperationCounts}
|
||
selectedOperationDetailLoading={selectedOperationDetailLoading}
|
||
selectedOperationDetailError={selectedOperationDetailError}
|
||
selectedOperationSources={selectedOperationSources}
|
||
selectedOperationItems={selectedOperationItems}
|
||
filteredSelectedOperationItems={filteredSelectedOperationItems}
|
||
selectedOperationItemSearch={selectedOperationItemSearch}
|
||
setSelectedOperationItemSearch={setSelectedOperationItemSearch}
|
||
selectedOperationItemPage={selectedOperationItemPage}
|
||
setSelectedOperationItemPage={setSelectedOperationItemPage}
|
||
selectedOperationItemPageCount={selectedOperationItemPageCount}
|
||
pagedSelectedOperationItems={pagedSelectedOperationItems}
|
||
/>
|
||
|
||
<FeedbackTab
|
||
feedbackSearch={feedbackSearch}
|
||
setFeedbackSearch={setFeedbackSearch}
|
||
feedbackStatusFilter={feedbackStatusFilter}
|
||
setFeedbackStatusFilter={setFeedbackStatusFilter}
|
||
feedbackRollbackFilter={feedbackRollbackFilter}
|
||
setFeedbackRollbackFilter={setFeedbackRollbackFilter}
|
||
filteredFeedbackCorrections={filteredFeedbackCorrections}
|
||
feedbackCorrections={feedbackCorrections}
|
||
pagedFeedbackCorrections={pagedFeedbackCorrections}
|
||
feedbackPage={feedbackPage}
|
||
setFeedbackPage={setFeedbackPage}
|
||
feedbackPageCount={feedbackPageCount}
|
||
selectedFeedbackCorrection={selectedFeedbackCorrection}
|
||
setSelectedFeedbackTaskId={setSelectedFeedbackTaskId}
|
||
selectedFeedbackResolved={selectedFeedbackResolved}
|
||
selectedFeedbackPreview={selectedFeedbackPreview}
|
||
selectedFeedbackImpactSummary={selectedFeedbackImpactSummary}
|
||
openFeedbackRollbackDialog={openFeedbackRollbackDialog}
|
||
feedbackRollingBack={feedbackRollingBack}
|
||
selectedFeedbackTaskLoading={selectedFeedbackTaskLoading}
|
||
selectedFeedbackTaskError={selectedFeedbackTaskError}
|
||
feedbackActionLogPage={feedbackActionLogPage}
|
||
setFeedbackActionLogPage={setFeedbackActionLogPage}
|
||
feedbackActionLogPageCount={feedbackActionLogPageCount}
|
||
feedbackActionLogSearch={feedbackActionLogSearch}
|
||
setFeedbackActionLogSearch={setFeedbackActionLogSearch}
|
||
pagedFeedbackActionLogs={pagedFeedbackActionLogs}
|
||
selectedFeedbackActionLogs={selectedFeedbackActionLogs}
|
||
/>
|
||
</Tabs>
|
||
</div>
|
||
</div>
|
||
|
||
<MemoryDeleteDialog
|
||
open={deleteDialogOpen}
|
||
onOpenChange={closeDeleteDialog}
|
||
title={deleteDialogTitle}
|
||
description={deleteDialogDescription}
|
||
preview={deletePreview}
|
||
result={deleteResult}
|
||
loadingPreview={deletePreviewLoading}
|
||
executing={deleteExecuting}
|
||
restoring={deleteRestoring}
|
||
error={deletePreviewError}
|
||
onExecute={() => void executePendingDelete()}
|
||
onRestore={() => void (deleteResult?.operation_id ? restoreDeleteOperation(deleteResult.operation_id) : Promise.resolve())}
|
||
/>
|
||
|
||
<Dialog open={feedbackRollbackDialogOpen} onOpenChange={setFeedbackRollbackDialogOpen}>
|
||
<DialogContent className="max-w-lg" confirmOnEnter>
|
||
<DialogHeader>
|
||
<DialogTitle>回退本次纠错</DialogTitle>
|
||
<DialogDescription>
|
||
这会恢复旧关系状态、隐藏本次纠错写入的段落,并重新触发 Episode / Profile 的异步修复。
|
||
</DialogDescription>
|
||
</DialogHeader>
|
||
<div className="space-y-3">
|
||
<div className="rounded-lg border bg-muted/20 p-3 text-sm">
|
||
<div className="font-medium break-words">{selectedFeedbackResolved?.query_text || '无查询文本'}</div>
|
||
<div className="mt-1 font-mono text-[11px] break-all text-muted-foreground">
|
||
{selectedFeedbackResolved?.query_tool_id}
|
||
</div>
|
||
</div>
|
||
<div className="space-y-2">
|
||
<Label htmlFor="feedback-rollback-reason">回退原因</Label>
|
||
<Textarea
|
||
id="feedback-rollback-reason"
|
||
value={feedbackRollbackReason}
|
||
onChange={(event) => setFeedbackRollbackReason(event.target.value)}
|
||
placeholder="可选,建议填写本次人工回退原因"
|
||
/>
|
||
</div>
|
||
</div>
|
||
<DialogFooter>
|
||
<Button variant="outline" onClick={() => setFeedbackRollbackDialogOpen(false)} disabled={feedbackRollingBack}>
|
||
取消
|
||
</Button>
|
||
<Button onClick={() => void executeFeedbackRollback()} disabled={feedbackRollingBack}>
|
||
{feedbackRollingBack ? (
|
||
<>
|
||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||
回退中
|
||
</>
|
||
) : (
|
||
<>
|
||
<RotateCcw className="mr-2 h-4 w-4" />
|
||
确认回退
|
||
</>
|
||
)}
|
||
</Button>
|
||
</DialogFooter>
|
||
</DialogContent>
|
||
</Dialog>
|
||
</div>
|
||
)
|
||
}
|