Files
mai-bot/dashboard/src/routes/resource/knowledge-base/tabs/ImportTab.tsx
DrSmoothl 9e48cd2848 feat: add TuningTab component for tuning task management and utility functions for memory operations
- Implemented TuningTab component to handle tuning objectives, intensity, sample size, and evaluation settings.
- Added UI elements for creating tuning tasks and displaying current configurations and recent tasks.
- Introduced utility functions for normalizing and formatting memory operation data, including feedback actions and delete operations.
2026-05-01 20:14:37 +08:00

1271 lines
60 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import type { Dispatch, SetStateAction } from 'react'
import { ChevronLeft, ChevronRight, Loader2, RefreshCw, Upload } from 'lucide-react'
import { MemoryMiniTabs } from '@/components/memory/MemoryMiniTabs'
import { MemoryProgressIndicator } from '@/components/memory/MemoryProgressIndicator'
import { Alert, AlertDescription } from '@/components/ui/alert'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
import { Checkbox } from '@/components/ui/checkbox'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { Progress } from '@/components/ui/progress'
import { ScrollArea } from '@/components/ui/scroll-area'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
import { Tabs, TabsContent } from '@/components/ui/tabs'
import { Textarea } from '@/components/ui/textarea'
import { cn } from '@/lib/utils'
import type {
MemoryImportChunkPayload,
MemoryImportFilePayload,
MemoryImportInputMode,
MemoryImportRetrySummary,
MemoryImportSettings,
MemoryImportTaskKind,
MemoryImportTaskPayload,
} from '@/lib/memory-api'
import { IMPORT_CHUNK_PAGE_SIZE, IMPORT_KIND_OPTIONS, RUNNING_IMPORT_STATUS } from '../constants'
import {
formatImportTime,
getImportStatusLabel,
getImportStatusVariant,
getImportStepLabel,
normalizeImportInputMode,
normalizeProgress,
} from '../utils'
export interface ImportTabProps {
importCreateMode: MemoryImportTaskKind
setImportCreateMode: Dispatch<SetStateAction<MemoryImportTaskKind>>
importSettings: MemoryImportSettings
importCommonFileConcurrency: string
setImportCommonFileConcurrency: Dispatch<SetStateAction<string>>
importCommonChunkConcurrency: string
setImportCommonChunkConcurrency: Dispatch<SetStateAction<string>>
importCommonLlmEnabled: boolean
setImportCommonLlmEnabled: Dispatch<SetStateAction<boolean>>
importCommonChatLog: boolean
setImportCommonChatLog: Dispatch<SetStateAction<boolean>>
importCommonStrategyOverride: string
setImportCommonStrategyOverride: Dispatch<SetStateAction<string>>
importCommonDedupePolicy: string
setImportCommonDedupePolicy: Dispatch<SetStateAction<string>>
importCommonChatReferenceTime: string
setImportCommonChatReferenceTime: Dispatch<SetStateAction<string>>
importCommonForce: boolean
setImportCommonForce: Dispatch<SetStateAction<boolean>>
importCommonClearManifest: boolean
setImportCommonClearManifest: Dispatch<SetStateAction<boolean>>
uploadInputMode: MemoryImportInputMode
setUploadInputMode: Dispatch<SetStateAction<MemoryImportInputMode>>
uploadFiles: File[]
setUploadFiles: Dispatch<SetStateAction<File[]>>
pasteName: string
setPasteName: Dispatch<SetStateAction<string>>
pasteMode: MemoryImportInputMode
setPasteMode: Dispatch<SetStateAction<MemoryImportInputMode>>
pasteContent: string
setPasteContent: Dispatch<SetStateAction<string>>
rawAlias: string
setRawAlias: Dispatch<SetStateAction<string>>
rawInputMode: MemoryImportInputMode
setRawInputMode: Dispatch<SetStateAction<MemoryImportInputMode>>
rawRelativePath: string
setRawRelativePath: Dispatch<SetStateAction<string>>
rawGlob: string
setRawGlob: Dispatch<SetStateAction<string>>
rawRecursive: boolean
setRawRecursive: Dispatch<SetStateAction<boolean>>
openieAlias: string
setOpenieAlias: Dispatch<SetStateAction<string>>
openieRelativePath: string
setOpenieRelativePath: Dispatch<SetStateAction<string>>
openieIncludeAllJson: boolean
setOpenieIncludeAllJson: Dispatch<SetStateAction<boolean>>
convertAlias: string
setConvertAlias: Dispatch<SetStateAction<string>>
convertTargetAlias: string
setConvertTargetAlias: Dispatch<SetStateAction<string>>
convertRelativePath: string
setConvertRelativePath: Dispatch<SetStateAction<string>>
convertTargetRelativePath: string
setConvertTargetRelativePath: Dispatch<SetStateAction<string>>
convertDimension: string
setConvertDimension: Dispatch<SetStateAction<string>>
convertBatchSize: string
setConvertBatchSize: Dispatch<SetStateAction<string>>
backfillAlias: string
setBackfillAlias: Dispatch<SetStateAction<string>>
backfillLimit: string
setBackfillLimit: Dispatch<SetStateAction<string>>
backfillRelativePath: string
setBackfillRelativePath: Dispatch<SetStateAction<string>>
backfillDryRun: boolean
setBackfillDryRun: Dispatch<SetStateAction<boolean>>
backfillNoCreatedFallback: boolean
setBackfillNoCreatedFallback: Dispatch<SetStateAction<boolean>>
maibotSourceDb: string
setMaibotSourceDb: Dispatch<SetStateAction<string>>
maibotTimeFrom: string
setMaibotTimeFrom: Dispatch<SetStateAction<string>>
maibotTimeTo: string
setMaibotTimeTo: Dispatch<SetStateAction<string>>
maibotStartId: string
setMaibotStartId: Dispatch<SetStateAction<string>>
maibotEndId: string
setMaibotEndId: Dispatch<SetStateAction<string>>
maibotStreamIds: string
setMaibotStreamIds: Dispatch<SetStateAction<string>>
maibotGroupIds: string
setMaibotGroupIds: Dispatch<SetStateAction<string>>
maibotUserIds: string
setMaibotUserIds: Dispatch<SetStateAction<string>>
maibotReadBatchSize: string
setMaibotReadBatchSize: Dispatch<SetStateAction<string>>
maibotCommitWindowRows: string
setMaibotCommitWindowRows: Dispatch<SetStateAction<string>>
maibotEmbedWorkers: string
setMaibotEmbedWorkers: Dispatch<SetStateAction<string>>
maibotNoResume: boolean
setMaibotNoResume: Dispatch<SetStateAction<boolean>>
maibotResetState: boolean
setMaibotResetState: Dispatch<SetStateAction<boolean>>
maibotDryRun: boolean
setMaibotDryRun: Dispatch<SetStateAction<boolean>>
maibotVerifyOnly: boolean
setMaibotVerifyOnly: Dispatch<SetStateAction<boolean>>
submitImportByMode: () => Promise<void>
creatingImport: boolean
pathResolveAlias: string
setPathResolveAlias: Dispatch<SetStateAction<string>>
importAliasKeys: string[]
pathResolveRelativePath: string
setPathResolveRelativePath: Dispatch<SetStateAction<string>>
pathResolveMustExist: boolean
setPathResolveMustExist: Dispatch<SetStateAction<boolean>>
resolveImportPath: () => Promise<void>
resolvingPath: boolean
pathResolveOutput: string
refreshImportQueue: () => Promise<void>
runningImportTasks: MemoryImportTaskPayload[]
queuedImportTasks: MemoryImportTaskPayload[]
recentImportTasks: MemoryImportTaskPayload[]
selectedImportTaskId: string
selectImportTask: (taskId: string) => Promise<void>
importAutoPolling: boolean
setImportAutoPolling: Dispatch<SetStateAction<boolean>>
importPollInterval: number
importErrorText: string
cancelSelectedImportTask: () => Promise<void>
retrySelectedImportTask: () => Promise<void>
selectedImportTaskLoading: boolean
selectedImportTaskResolved: MemoryImportTaskPayload | null | undefined
selectedImportRetrySummary: MemoryImportRetrySummary | null | undefined
selectedImportTaskErrorText: string
selectedImportFiles: MemoryImportFilePayload[]
selectedImportFileId: string
selectImportFile: (fileId: string) => Promise<void>
importChunkTotal: number
importChunkOffset: number
moveImportChunkPage: (direction: -1 | 1) => Promise<void>
canImportChunkPrev: boolean
canImportChunkNext: boolean
importChunksLoading: boolean
selectedImportChunks: MemoryImportChunkPayload[]
}
export function ImportTab(props: ImportTabProps) {
const {
importCreateMode,
setImportCreateMode,
importSettings,
importCommonFileConcurrency,
setImportCommonFileConcurrency,
importCommonChunkConcurrency,
setImportCommonChunkConcurrency,
importCommonLlmEnabled,
setImportCommonLlmEnabled,
importCommonChatLog,
setImportCommonChatLog,
importCommonStrategyOverride,
setImportCommonStrategyOverride,
importCommonDedupePolicy,
setImportCommonDedupePolicy,
importCommonChatReferenceTime,
setImportCommonChatReferenceTime,
importCommonForce,
setImportCommonForce,
importCommonClearManifest,
setImportCommonClearManifest,
uploadInputMode,
setUploadInputMode,
uploadFiles,
setUploadFiles,
pasteName,
setPasteName,
pasteMode,
setPasteMode,
pasteContent,
setPasteContent,
rawAlias,
setRawAlias,
rawInputMode,
setRawInputMode,
rawRelativePath,
setRawRelativePath,
rawGlob,
setRawGlob,
rawRecursive,
setRawRecursive,
openieAlias,
setOpenieAlias,
openieRelativePath,
setOpenieRelativePath,
openieIncludeAllJson,
setOpenieIncludeAllJson,
convertAlias,
setConvertAlias,
convertTargetAlias,
setConvertTargetAlias,
convertRelativePath,
setConvertRelativePath,
convertTargetRelativePath,
setConvertTargetRelativePath,
convertDimension,
setConvertDimension,
convertBatchSize,
setConvertBatchSize,
backfillAlias,
setBackfillAlias,
backfillLimit,
setBackfillLimit,
backfillRelativePath,
setBackfillRelativePath,
backfillDryRun,
setBackfillDryRun,
backfillNoCreatedFallback,
setBackfillNoCreatedFallback,
maibotSourceDb,
setMaibotSourceDb,
maibotTimeFrom,
setMaibotTimeFrom,
maibotTimeTo,
setMaibotTimeTo,
maibotStartId,
setMaibotStartId,
maibotEndId,
setMaibotEndId,
maibotStreamIds,
setMaibotStreamIds,
maibotGroupIds,
setMaibotGroupIds,
maibotUserIds,
setMaibotUserIds,
maibotReadBatchSize,
setMaibotReadBatchSize,
maibotCommitWindowRows,
setMaibotCommitWindowRows,
maibotEmbedWorkers,
setMaibotEmbedWorkers,
maibotNoResume,
setMaibotNoResume,
maibotResetState,
setMaibotResetState,
maibotDryRun,
setMaibotDryRun,
maibotVerifyOnly,
setMaibotVerifyOnly,
submitImportByMode,
creatingImport,
pathResolveAlias,
setPathResolveAlias,
importAliasKeys,
pathResolveRelativePath,
setPathResolveRelativePath,
pathResolveMustExist,
setPathResolveMustExist,
resolveImportPath,
resolvingPath,
pathResolveOutput,
refreshImportQueue,
runningImportTasks,
queuedImportTasks,
recentImportTasks,
selectedImportTaskId,
selectImportTask,
importAutoPolling,
setImportAutoPolling,
importPollInterval,
importErrorText,
cancelSelectedImportTask,
retrySelectedImportTask,
selectedImportTaskLoading,
selectedImportTaskResolved,
selectedImportRetrySummary,
selectedImportTaskErrorText,
selectedImportFiles,
selectedImportFileId,
selectImportFile,
importChunkTotal,
importChunkOffset,
moveImportChunkPage,
canImportChunkPrev,
canImportChunkNext,
importChunksLoading,
selectedImportChunks,
} = props
return (
<TabsContent
value="import"
className="space-y-6 [&_input]:h-10 [&_[role=combobox]]:h-10 [&_textarea]:min-h-[96px]"
>
<div className="grid gap-6 lg:grid-cols-[minmax(0,1fr)_minmax(0,1fr)]">
<div className="order-2 space-y-6 lg:order-1">
<Card className="rounded-2xl border-border/70 shadow-sm">
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Upload className="h-4 w-4" />
</CardTitle>
<CardDescription> </CardDescription>
</CardHeader>
<CardContent className="space-y-6">
<Tabs
value={importCreateMode}
onValueChange={(value) => setImportCreateMode(value as MemoryImportTaskKind)}
className="space-y-4"
>
<div className="space-y-2">
<Label></Label>
<MemoryMiniTabs items={IMPORT_KIND_OPTIONS} />
</div>
<div className="space-y-4 rounded-lg border bg-muted/30 p-4">
<div className="space-y-1">
<div className="text-sm font-medium"></div>
<div className="text-xs text-muted-foreground"></div>
</div>
<div className="grid gap-3 md:grid-cols-2">
<div className="space-y-1">
<Label></Label>
<div className="text-xs text-muted-foreground"></div>
<Input
type="number"
min={1}
max={Number(importSettings.max_file_concurrency ?? 128)}
value={importCommonFileConcurrency}
onChange={(event) => setImportCommonFileConcurrency(event.target.value)}
/>
</div>
<div className="space-y-1">
<Label></Label>
<div className="text-xs text-muted-foreground"></div>
<Input
type="number"
min={1}
max={Number(importSettings.max_chunk_concurrency ?? 256)}
value={importCommonChunkConcurrency}
onChange={(event) => setImportCommonChunkConcurrency(event.target.value)}
/>
</div>
<div className="rounded-md border bg-background/70 p-3">
<div className="flex items-center gap-2 text-sm">
<Checkbox
checked={importCommonLlmEnabled}
onCheckedChange={(value) => setImportCommonLlmEnabled(Boolean(value))}
/>
LLM
</div>
<div className="mt-1 text-xs text-muted-foreground"></div>
</div>
<div className="rounded-md border bg-background/70 p-3">
<div className="flex items-center gap-2 text-sm">
<Checkbox
checked={importCommonChatLog}
onCheckedChange={(value) => setImportCommonChatLog(Boolean(value))}
/>
</div>
<div className="mt-1 text-xs text-muted-foreground"></div>
</div>
</div>
<details className="rounded-md border bg-background/70 p-3 text-sm">
<summary className="cursor-pointer text-xs font-medium text-muted-foreground">
</summary>
<div className="mt-3 grid gap-3">
<div className="space-y-1">
<Label></Label>
<Input
value={importCommonStrategyOverride}
onChange={(event) => setImportCommonStrategyOverride(event.target.value)}
/>
</div>
<div className="space-y-1">
<Label></Label>
<Input
value={importCommonDedupePolicy}
onChange={(event) => setImportCommonDedupePolicy(event.target.value)}
/>
</div>
<div className="space-y-1">
<Label></Label>
<Input
value={importCommonChatReferenceTime}
onChange={(event) => setImportCommonChatReferenceTime(event.target.value)}
/>
</div>
<div className="flex items-center gap-2 text-sm">
<Checkbox
checked={importCommonForce}
onCheckedChange={(value) => setImportCommonForce(Boolean(value))}
/>
</div>
<div className="flex items-center gap-2 text-sm">
<Checkbox
checked={importCommonClearManifest}
onCheckedChange={(value) => setImportCommonClearManifest(Boolean(value))}
/>
</div>
</div>
</details>
</div>
<TabsContent value="upload" className="mt-0">
<div className="space-y-3 rounded-xl border bg-background/70 p-4">
<div className="text-xs text-muted-foreground"></div>
<div className="grid gap-3">
<div className="space-y-1">
<Label></Label>
<Select
value={uploadInputMode}
onValueChange={(value) => setUploadInputMode(normalizeImportInputMode(value))}
>
<SelectTrigger aria-label="upload-input-mode">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="text"></SelectItem>
<SelectItem value="json"> JSON</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-1">
<Label></Label>
<Input
type="file"
multiple
accept=".txt,.md,.json,.jsonl,.csv,.log,.html,.htm,.xml"
onChange={(event) => setUploadFiles(Array.from(event.target.files ?? []))}
/>
</div>
</div>
<div className="text-xs text-muted-foreground"> {uploadFiles.length} </div>
</div>
</TabsContent>
<TabsContent value="paste" className="mt-0">
<div className="space-y-3 rounded-xl border bg-background/70 p-4">
<div className="text-xs text-muted-foreground"> JSON</div>
<div className="grid gap-3">
<div className="space-y-1">
<Label></Label>
<Input value={pasteName} onChange={(event) => setPasteName(event.target.value)} />
</div>
<div className="space-y-1">
<Label></Label>
<Select
value={pasteMode}
onValueChange={(value) => setPasteMode(normalizeImportInputMode(value))}
>
<SelectTrigger aria-label="paste-input-mode">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="text"></SelectItem>
<SelectItem value="json"> JSON</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-1">
<Label></Label>
<Textarea
value={pasteContent}
onChange={(event) => setPasteContent(event.target.value)}
rows={8}
/>
</div>
</div>
</div>
</TabsContent>
<TabsContent value="raw_scan" className="mt-0">
<div className="space-y-3 rounded-xl border bg-background/70 p-4">
<div className="text-xs text-muted-foreground"></div>
<div className="grid gap-3">
<div className="space-y-1">
<Label></Label>
<Input value={rawAlias} onChange={(event) => setRawAlias(event.target.value)} />
</div>
<div className="space-y-1">
<Label></Label>
<Select
value={rawInputMode}
onValueChange={(value) => setRawInputMode(normalizeImportInputMode(value))}
>
<SelectTrigger aria-label="raw-input-mode">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="text"></SelectItem>
<SelectItem value="json"> JSON</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-1">
<Label></Label>
<Input value={rawRelativePath} onChange={(event) => setRawRelativePath(event.target.value)} />
</div>
<div className="space-y-1">
<Label>Glob</Label>
<Input value={rawGlob} onChange={(event) => setRawGlob(event.target.value)} />
</div>
</div>
<div className="flex items-center gap-2 text-sm">
<Checkbox checked={rawRecursive} onCheckedChange={(value) => setRawRecursive(Boolean(value))} />
</div>
</div>
</TabsContent>
<TabsContent value="lpmm_openie" className="mt-0">
<div className="space-y-3 rounded-xl border bg-background/70 p-4">
<div className="text-xs text-muted-foreground"> LPMM </div>
<div className="grid gap-3">
<div className="space-y-1">
<Label></Label>
<Input value={openieAlias} onChange={(event) => setOpenieAlias(event.target.value)} />
</div>
<div className="space-y-1">
<Label></Label>
<Input value={openieRelativePath} onChange={(event) => setOpenieRelativePath(event.target.value)} />
</div>
</div>
<div className="flex items-center gap-2 text-sm">
<Checkbox
checked={openieIncludeAllJson}
onCheckedChange={(value) => setOpenieIncludeAllJson(Boolean(value))}
/>
JSON
</div>
</div>
</TabsContent>
<TabsContent value="lpmm_convert" className="mt-0">
<div className="space-y-3 rounded-xl border bg-background/70 p-4">
<div className="text-xs text-muted-foreground"> LPMM </div>
<div className="grid gap-3">
<div className="space-y-1">
<Label></Label>
<Input value={convertAlias} onChange={(event) => setConvertAlias(event.target.value)} />
</div>
<div className="space-y-1">
<Label></Label>
<Input value={convertTargetAlias} onChange={(event) => setConvertTargetAlias(event.target.value)} />
</div>
<div className="space-y-1">
<Label></Label>
<Input value={convertRelativePath} onChange={(event) => setConvertRelativePath(event.target.value)} />
</div>
<div className="space-y-1">
<Label></Label>
<Input
value={convertTargetRelativePath}
onChange={(event) => setConvertTargetRelativePath(event.target.value)}
/>
</div>
<div className="space-y-1">
<Label></Label>
<Input
type="number"
min={1}
value={convertDimension}
onChange={(event) => setConvertDimension(event.target.value)}
/>
</div>
<div className="space-y-1">
<Label></Label>
<Input
type="number"
min={1}
value={convertBatchSize}
onChange={(event) => setConvertBatchSize(event.target.value)}
/>
</div>
</div>
</div>
</TabsContent>
<TabsContent value="temporal_backfill" className="mt-0">
<div className="space-y-3 rounded-xl border bg-background/70 p-4">
<div className="text-xs text-muted-foreground"></div>
<div className="grid gap-3">
<div className="space-y-1">
<Label></Label>
<Input value={backfillAlias} onChange={(event) => setBackfillAlias(event.target.value)} />
</div>
<div className="space-y-1">
<Label></Label>
<Input type="number" min={1} value={backfillLimit} onChange={(event) => setBackfillLimit(event.target.value)} />
</div>
<div className="space-y-1">
<Label></Label>
<Input value={backfillRelativePath} onChange={(event) => setBackfillRelativePath(event.target.value)} />
</div>
</div>
<div className="grid gap-2">
<div className="flex items-center gap-2 text-sm">
<Checkbox checked={backfillDryRun} onCheckedChange={(value) => setBackfillDryRun(Boolean(value))} />
</div>
<div className="flex items-center gap-2 text-sm">
<Checkbox
checked={backfillNoCreatedFallback}
onCheckedChange={(value) => setBackfillNoCreatedFallback(Boolean(value))}
/>
退
</div>
</div>
</div>
</TabsContent>
<TabsContent value="maibot_migration" className="mt-0">
<div className="space-y-3 rounded-xl border bg-background/70 p-4">
<div className="text-xs text-muted-foreground"> MaiBot </div>
<div className="grid gap-3">
<div className="space-y-1">
<Label></Label>
<Input value={maibotSourceDb} onChange={(event) => setMaibotSourceDb(event.target.value)} />
</div>
<div className="space-y-1">
<Label></Label>
<Input value={maibotTimeFrom} onChange={(event) => setMaibotTimeFrom(event.target.value)} />
</div>
<div className="space-y-1">
<Label></Label>
<Input value={maibotTimeTo} onChange={(event) => setMaibotTimeTo(event.target.value)} />
</div>
<div className="space-y-1">
<Label> ID</Label>
<Input type="number" min={1} value={maibotStartId} onChange={(event) => setMaibotStartId(event.target.value)} />
</div>
<div className="space-y-1">
<Label> ID</Label>
<Input type="number" min={1} value={maibotEndId} onChange={(event) => setMaibotEndId(event.target.value)} />
</div>
<div className="space-y-1">
<Label> ID </Label>
<Input value={maibotStreamIds} onChange={(event) => setMaibotStreamIds(event.target.value)} />
</div>
<div className="space-y-1">
<Label> ID </Label>
<Input value={maibotGroupIds} onChange={(event) => setMaibotGroupIds(event.target.value)} />
</div>
<div className="space-y-1">
<Label> ID </Label>
<Input value={maibotUserIds} onChange={(event) => setMaibotUserIds(event.target.value)} />
</div>
<div className="space-y-1">
<Label></Label>
<Input
type="number"
min={1}
value={maibotReadBatchSize}
onChange={(event) => setMaibotReadBatchSize(event.target.value)}
/>
</div>
<div className="space-y-1">
<Label></Label>
<Input
type="number"
min={1}
value={maibotCommitWindowRows}
onChange={(event) => setMaibotCommitWindowRows(event.target.value)}
/>
</div>
<div className="space-y-1">
<Label>线</Label>
<Input
type="number"
min={1}
value={maibotEmbedWorkers}
onChange={(event) => setMaibotEmbedWorkers(event.target.value)}
/>
</div>
</div>
<div className="grid gap-2">
<div className="flex items-center gap-2 text-sm">
<Checkbox checked={maibotNoResume} onCheckedChange={(value) => setMaibotNoResume(Boolean(value))} />
</div>
<div className="flex items-center gap-2 text-sm">
<Checkbox checked={maibotResetState} onCheckedChange={(value) => setMaibotResetState(Boolean(value))} />
</div>
<div className="flex items-center gap-2 text-sm">
<Checkbox checked={maibotDryRun} onCheckedChange={(value) => setMaibotDryRun(Boolean(value))} />
</div>
<div className="flex items-center gap-2 text-sm">
<Checkbox checked={maibotVerifyOnly} onCheckedChange={(value) => setMaibotVerifyOnly(Boolean(value))} />
</div>
</div>
</div>
</TabsContent>
</Tabs>
<Button onClick={() => void submitImportByMode()} disabled={creatingImport}>
{creatingImport ? <Loader2 className="mr-2 h-4 w-4 animate-spin" /> : <Upload className="mr-2 h-4 w-4" />}
</Button>
</CardContent>
</Card>
<Card className="rounded-2xl border-border/70 bg-card/85 shadow-sm">
<CardHeader>
<CardTitle></CardTitle>
<CardDescription></CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid gap-3">
<div className="space-y-1">
<Label></Label>
<div className="text-xs text-muted-foreground">访</div>
<Select value={pathResolveAlias} onValueChange={setPathResolveAlias}>
<SelectTrigger aria-label="import-path-alias">
<SelectValue />
</SelectTrigger>
<SelectContent>
{importAliasKeys.length > 0 ? importAliasKeys.map((alias) => (
<SelectItem key={alias} value={alias}>{alias}</SelectItem>
)) : (
<SelectItem value="raw">raw</SelectItem>
)}
</SelectContent>
</Select>
</div>
<div className="space-y-1">
<Label></Label>
<div className="text-xs text-muted-foreground"></div>
<Input
value={pathResolveRelativePath}
onChange={(event) => setPathResolveRelativePath(event.target.value)}
placeholder="例如 exports/weekly"
/>
</div>
</div>
<div className="flex items-center gap-2 text-sm">
<Checkbox checked={pathResolveMustExist} onCheckedChange={(value) => setPathResolveMustExist(Boolean(value))} />
</div>
<Button
variant="outline"
onClick={() => void resolveImportPath()}
disabled={resolvingPath || !pathResolveAlias.trim()}
>
{resolvingPath ? <Loader2 className="mr-2 h-4 w-4 animate-spin" /> : <RefreshCw className="mr-2 h-4 w-4" />}
</Button>
<Textarea value={pathResolveOutput} readOnly rows={6} placeholder="解析结果会显示在这里" />
</CardContent>
</Card>
</div>
<div className="order-1 space-y-6 lg:order-2">
<Card className="rounded-2xl border-border/70 bg-card/90 shadow-sm">
<CardHeader className="space-y-4 pb-4">
<div className="flex flex-wrap items-center justify-between gap-2">
<CardTitle></CardTitle>
<Button variant="outline" size="sm" onClick={() => void refreshImportQueue()}>
<RefreshCw className="mr-2 h-4 w-4" />
</Button>
</div>
<div className="flex flex-wrap items-center justify-between gap-3">
<CardDescription className="text-sm">
</CardDescription>
<div className="flex flex-wrap gap-2 text-xs text-muted-foreground">
<Badge variant="outline" className="bg-background/70"> {runningImportTasks.length}</Badge>
<Badge variant="outline" className="bg-background/70"> {queuedImportTasks.length}</Badge>
<Badge variant="outline" className="bg-background/70"> {recentImportTasks.length}</Badge>
</div>
<label className="flex items-center gap-2 text-xs text-muted-foreground">
<Checkbox checked={importAutoPolling} onCheckedChange={(value) => setImportAutoPolling(Boolean(value))} />
{importPollInterval}ms
</label>
</div>
</CardHeader>
<CardContent className="space-y-6">
{importErrorText ? (
<Alert variant="destructive">
<AlertDescription>{importErrorText}</AlertDescription>
</Alert>
) : null}
<div className="space-y-2.5">
<div className="flex items-center justify-between gap-2">
<div className="text-sm font-medium"></div>
<Badge variant="outline">{runningImportTasks.length}</Badge>
</div>
{runningImportTasks.length > 0 ? (
<ScrollArea className="h-[208px] rounded-xl border bg-muted/10">
<div className="space-y-2.5 p-2.5">
{runningImportTasks.map((task) => {
const isSelected = task.task_id === selectedImportTaskId
return (
<button
key={task.task_id}
type="button"
onClick={() => void selectImportTask(task.task_id)}
className={cn(
'w-full rounded-xl border p-4 text-left transition-all',
isSelected
? 'border-primary/70 bg-primary/5 shadow-sm'
: 'bg-background/80 hover:border-muted-foreground/40 hover:bg-muted/20',
)}
>
<div className="flex flex-wrap items-start justify-between gap-2">
<div className="min-w-0 space-y-1">
<div className="break-all font-mono text-[11px] leading-relaxed text-muted-foreground">
{task.task_id}
</div>
<div className="text-sm font-medium">{String(task.task_kind ?? task.mode ?? '-')}</div>
</div>
<Badge variant={getImportStatusVariant(String(task.status ?? ''))}>
{getImportStatusLabel(String(task.status ?? ''))}
</Badge>
</div>
<div className="mt-2 flex items-center justify-between gap-2 text-xs text-muted-foreground">
<span>{getImportStepLabel(String(task.current_step ?? 'running'))}</span>
<span>{Number(task.progress ?? 0).toFixed(1)}%</span>
</div>
<Progress value={normalizeProgress(task.progress)} className="mt-2 h-1.5" />
</button>
)
})}
</div>
</ScrollArea>
) : (
<div className="rounded-xl border bg-muted/20 p-4 text-sm text-muted-foreground"></div>
)}
</div>
<div className="space-y-2.5">
<div className="flex items-center justify-between gap-2">
<div className="text-sm font-medium"></div>
<Badge variant="outline">{queuedImportTasks.length}</Badge>
</div>
{queuedImportTasks.length > 0 ? (
<ScrollArea className="h-[188px] rounded-xl border bg-muted/10">
<div className="space-y-2.5 p-2.5">
{queuedImportTasks.map((task) => {
const isSelected = task.task_id === selectedImportTaskId
return (
<button
key={task.task_id}
type="button"
onClick={() => void selectImportTask(task.task_id)}
className={cn(
'w-full rounded-xl border p-4 text-left transition-all',
isSelected
? 'border-primary/70 bg-primary/5 shadow-sm'
: 'bg-background/80 hover:border-muted-foreground/40 hover:bg-muted/20',
)}
>
<div className="flex flex-wrap items-start justify-between gap-2">
<div className="min-w-0 space-y-1">
<div className="break-all font-mono text-[11px] leading-relaxed text-muted-foreground">
{task.task_id}
</div>
<div className="text-sm font-medium">{String(task.task_kind ?? task.mode ?? '-')}</div>
</div>
<Badge variant={getImportStatusVariant(String(task.status ?? ''))}>
{getImportStatusLabel(String(task.status ?? ''))}
</Badge>
</div>
<div className="mt-2 flex items-center justify-between gap-2 text-xs text-muted-foreground">
<span></span>
<span>{formatImportTime(task.created_at)}</span>
</div>
</button>
)
})}
</div>
</ScrollArea>
) : (
<div className="rounded-xl border bg-muted/20 p-4 text-sm text-muted-foreground"></div>
)}
</div>
<div className="space-y-2.5">
<div className="flex items-center justify-between gap-2">
<div className="text-sm font-medium"></div>
<Badge variant="secondary">{recentImportTasks.length}</Badge>
</div>
{recentImportTasks.length > 0 ? (
<ScrollArea className="h-[260px] rounded-xl border bg-muted/10">
<div className="space-y-2.5 p-2.5">
{recentImportTasks.map((task) => {
const isSelected = task.task_id === selectedImportTaskId
return (
<button
key={task.task_id}
type="button"
onClick={() => void selectImportTask(task.task_id)}
className={cn(
'w-full rounded-xl border p-4 text-left transition-all',
isSelected
? 'border-primary/70 bg-primary/5 shadow-sm'
: 'bg-background/80 hover:border-muted-foreground/40 hover:bg-muted/20',
)}
>
<div className="flex flex-wrap items-start justify-between gap-2">
<div className="min-w-0 space-y-1">
<div className="break-all font-mono text-[11px] leading-relaxed text-muted-foreground">
{task.task_id}
</div>
<div className="text-sm font-medium">{String(task.task_kind ?? task.mode ?? '-')}</div>
</div>
<Badge variant={getImportStatusVariant(String(task.status ?? ''))}>
{getImportStatusLabel(String(task.status ?? ''))}
</Badge>
</div>
<div className="mt-2 flex items-center justify-between gap-2 text-xs text-muted-foreground">
<span></span>
<span>{Number(task.progress ?? 0).toFixed(1)}%</span>
</div>
<Progress value={normalizeProgress(task.progress)} className="mt-2 h-1.5" />
</button>
)
})}
</div>
</ScrollArea>
) : (
<div className="rounded-xl border bg-muted/20 p-4 text-sm text-muted-foreground"></div>
)}
</div>
</CardContent>
</Card>
</div>
</div>
<Card className="rounded-2xl border-border/70 bg-card/90 shadow-sm">
<CardHeader className="space-y-4">
<div className="flex flex-wrap items-center justify-between gap-2">
<CardTitle></CardTitle>
<div className="flex flex-wrap gap-2">
<Button
size="sm"
variant="outline"
aria-label="取消选中导入任务"
onClick={() => void cancelSelectedImportTask()}
disabled={!selectedImportTaskId}
>
</Button>
<Button
size="sm"
aria-label="重试选中导入任务"
onClick={() => void retrySelectedImportTask()}
disabled={!selectedImportTaskId}
>
</Button>
</div>
</div>
<CardDescription></CardDescription>
</CardHeader>
<CardContent className="space-y-6">
{selectedImportTaskLoading ? (
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<Loader2 className="h-4 w-4 animate-spin" />
...
</div>
) : null}
{!selectedImportTaskResolved ? (
<div className="flex flex-col items-center justify-center gap-3 rounded-xl border border-dashed bg-muted/15 px-6 py-10 text-center">
<div className="rounded-full bg-muted/40 p-3">
<Loader2 className="h-5 w-5 text-muted-foreground" />
</div>
<div className="space-y-1">
<div className="text-sm font-medium"></div>
<div className="text-xs leading-relaxed text-muted-foreground">
/<br />
</div>
</div>
</div>
) : (
<>
<div className="space-y-2">
<div className="text-sm font-medium"></div>
<div className="overflow-auto rounded-xl border bg-muted/10">
<Table className="min-w-[680px]">
<TableBody>
<TableRow>
<TableCell className="w-[140px] text-muted-foreground"> ID</TableCell>
<TableCell className="break-all font-mono text-xs leading-relaxed">
{selectedImportTaskResolved.task_id}
</TableCell>
</TableRow>
<TableRow>
<TableCell className="text-muted-foreground"></TableCell>
<TableCell>{String(selectedImportTaskResolved.task_kind ?? selectedImportTaskResolved.mode ?? '-')}</TableCell>
</TableRow>
<TableRow>
<TableCell className="text-muted-foreground"> / </TableCell>
<TableCell>
<div className="flex flex-wrap items-center gap-2">
<Badge variant={getImportStatusVariant(String(selectedImportTaskResolved.status ?? ''))}>
{getImportStatusLabel(String(selectedImportTaskResolved.status ?? ''))}
</Badge>
<span className="text-xs text-muted-foreground">
{getImportStepLabel(String(selectedImportTaskResolved.current_step ?? ''))}
</span>
</div>
</TableCell>
</TableRow>
<TableRow>
<TableCell className="text-muted-foreground"></TableCell>
<TableCell>
<MemoryProgressIndicator
value={normalizeProgress(selectedImportTaskResolved.progress)}
statusLabel={getImportStatusLabel(String(selectedImportTaskResolved.status ?? ''))}
stepLabel={getImportStepLabel(String(selectedImportTaskResolved.current_step ?? ''))}
tone={
String(selectedImportTaskResolved.status ?? '') === 'completed'
? 'success'
: String(selectedImportTaskResolved.status ?? '') === 'failed'
? 'destructive'
: String(selectedImportTaskResolved.status ?? '') === 'cancelled'
? 'muted'
: 'default'
}
busy={RUNNING_IMPORT_STATUS.has(String(selectedImportTaskResolved.status ?? ''))}
detail={`已完成 ${Number(selectedImportTaskResolved.done_chunks ?? 0)} / ${Number(selectedImportTaskResolved.total_chunks ?? 0)} 分块`}
/>
</TableCell>
</TableRow>
<TableRow>
<TableCell className="text-muted-foreground"></TableCell>
<TableCell>{formatImportTime(selectedImportTaskResolved.created_at)}</TableCell>
</TableRow>
<TableRow>
<TableCell className="text-muted-foreground"></TableCell>
<TableCell>{formatImportTime(selectedImportTaskResolved.updated_at)}</TableCell>
</TableRow>
</TableBody>
</Table>
</div>
</div>
{selectedImportRetrySummary ? (
<div className="space-y-2">
<div className="text-sm font-medium"></div>
<div className="overflow-auto rounded-xl border bg-muted/10">
<Table>
<TableBody>
<TableRow>
<TableCell className="w-[220px] text-muted-foreground"></TableCell>
<TableCell>{Number(selectedImportRetrySummary.chunk_retry_files ?? 0)}</TableCell>
</TableRow>
<TableRow>
<TableCell className="text-muted-foreground"></TableCell>
<TableCell>{Number(selectedImportRetrySummary.chunk_retry_chunks ?? 0)}</TableCell>
</TableRow>
<TableRow>
<TableCell className="text-muted-foreground">退</TableCell>
<TableCell>{Number(selectedImportRetrySummary.file_fallback_files ?? 0)}</TableCell>
</TableRow>
<TableRow>
<TableCell className="text-muted-foreground"></TableCell>
<TableCell>{Number(selectedImportRetrySummary.skipped_files ?? 0)}</TableCell>
</TableRow>
</TableBody>
</Table>
</div>
</div>
) : null}
{selectedImportTaskErrorText ? (
<Alert variant="destructive">
<AlertDescription>{selectedImportTaskErrorText}</AlertDescription>
</Alert>
) : null}
<div className="space-y-2.5">
<div className="text-sm font-medium"></div>
{selectedImportFiles.length > 0 ? (
<ScrollArea className="h-[260px] rounded-xl border bg-muted/10">
<div className="space-y-2.5 p-2.5">
{selectedImportFiles.map((file) => {
const isSelected = file.file_id === selectedImportFileId
return (
<button
key={file.file_id}
type="button"
onClick={() => void selectImportFile(file.file_id)}
className={cn(
'w-full rounded-xl border p-4 text-left transition-all',
isSelected
? 'border-primary/70 bg-primary/5 shadow-sm'
: 'bg-background/80 hover:border-muted-foreground/40 hover:bg-muted/20',
)}
>
<div className="flex flex-wrap items-center justify-between gap-2">
<span className="truncate text-sm font-medium">{file.name || file.file_id}</span>
<Badge variant={getImportStatusVariant(String(file.status ?? ''))}>
{getImportStatusLabel(String(file.status ?? ''))}
</Badge>
</div>
<div className="mt-2 flex items-center justify-between gap-2 text-xs text-muted-foreground">
<span>{getImportStepLabel(String(file.current_step ?? ''))}</span>
<span>{Number(file.progress ?? 0).toFixed(1)}%</span>
</div>
<Progress value={normalizeProgress(file.progress)} className="mt-2 h-1.5" />
<div className="mt-2 text-xs text-muted-foreground">
{Number(file.progress ?? 0).toFixed(1)}% · {Number(file.done_chunks ?? 0)} / {Number(file.total_chunks ?? 0)}
</div>
{file.error ? (
<div className="mt-2 truncate text-xs text-destructive">{file.error}</div>
) : null}
</button>
)
})}
</div>
</ScrollArea>
) : (
<div className="rounded-xl border bg-muted/20 p-4 text-sm text-muted-foreground"></div>
)}
</div>
<div className="space-y-2.5">
<div className="flex items-center justify-between gap-2">
<div className="text-sm font-medium"></div>
<div className="flex items-center gap-2 text-xs text-muted-foreground">
<Button
size="icon"
variant="outline"
aria-label="上一页分块"
onClick={() => void moveImportChunkPage(-1)}
disabled={!canImportChunkPrev}
>
<ChevronLeft className="h-4 w-4" />
</Button>
<span>
{importChunkTotal > 0
? `${importChunkOffset + 1}-${Math.min(importChunkOffset + IMPORT_CHUNK_PAGE_SIZE, importChunkTotal)}`
: '0-0'}
{' / '}
{importChunkTotal}
</span>
<Button
size="icon"
variant="outline"
aria-label="下一页分块"
onClick={() => void moveImportChunkPage(1)}
disabled={!canImportChunkNext}
>
<ChevronRight className="h-4 w-4" />
</Button>
</div>
</div>
<div className="overflow-auto rounded-xl border bg-background/80">
<Table className="min-w-[700px]">
<TableHeader>
<TableRow>
<TableHead className="w-[72px]"></TableHead>
<TableHead className="w-[108px]"></TableHead>
<TableHead className="w-[108px]"></TableHead>
<TableHead className="w-[84px]"></TableHead>
<TableHead> / </TableHead>
</TableRow>
</TableHeader>
<TableBody>
{importChunksLoading ? (
<TableRow>
<TableCell colSpan={5} className="text-center text-muted-foreground">
...
</TableCell>
</TableRow>
) : selectedImportChunks.length > 0 ? (
selectedImportChunks.map((chunk) => (
<TableRow key={chunk.chunk_id}>
<TableCell>{chunk.index}</TableCell>
<TableCell>{getImportStatusLabel(String(chunk.status ?? ''))}</TableCell>
<TableCell>{getImportStepLabel(String(chunk.step ?? ''))}</TableCell>
<TableCell>{Number(chunk.progress ?? 0).toFixed(1)}%</TableCell>
<TableCell className="max-w-[360px]">
<div className="space-y-2">
{String(chunk.error ?? '').trim() ? (
<div className="rounded-md border border-destructive/30 bg-destructive/5 px-2.5 py-2 text-sm leading-relaxed text-destructive">
{String(chunk.error)}
</div>
) : null}
<details className="rounded-md border bg-muted/20 px-2.5 py-2 text-xs text-muted-foreground">
<summary className="cursor-pointer font-medium text-foreground">
{String(chunk.error ?? '').trim() ? '查看分块预览' : '查看内容详情'}
</summary>
<div className="mt-2 whitespace-pre-wrap break-words leading-relaxed">
{String(chunk.content_preview ?? '-') || '-'}
</div>
</details>
</div>
</TableCell>
</TableRow>
))
) : (
<TableRow>
<TableCell colSpan={5} className="text-center text-muted-foreground">
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div>
</div>
</>
)}
</CardContent>
</Card>
</TabsContent>
)
}