Feat: Optimize dataset-level wiki generation log display. (#17492)

This commit is contained in:
balibabu
2026-07-29 10:56:35 +08:00
committed by GitHub
parent 2a187e607f
commit ee388b0fa5
13 changed files with 302 additions and 174 deletions

View File

@@ -73,7 +73,7 @@ export function NextMessageInput({
null,
);
const [enableThinking, setEnableThinking] = useState('0');
const [enableThinking, setEnableThinking] = useState('1');
const [enableInternet, setEnableInternet] = useState(false);
const thinkingOptions = useMemo(

View File

@@ -469,6 +469,8 @@ Example: A 1 KB message with 1024-dim embedding uses ~9 KB. The 5 MB default lim
updateTooltip:
'{{newlyUploaded}} new, {{removed}} removed documents found. Click to compile and merge into current Wiki.',
updateSheetTitle: 'Update Wiki',
viewUpdateLogs: 'View update logs',
log: 'Log',
noSkills: 'No skills yet',
generate: 'Generate',
raptor: 'RAPTOR',
@@ -526,7 +528,7 @@ Example: A 1 KB message with 1024-dim embedding uses ~9 KB. The 5 MB default lim
graphPlaceholder: 'Graph view placeholder',
skills: 'To Skills',
llmWiki: 'Wiki',
navTree: 'Tree',
navTree: 'Tree/Page index',
structureGraph: 'Graph',
structureMindmap: 'Mind map',
structureTimeline: 'Timeline',
@@ -1147,7 +1149,7 @@ This auto-tagging feature enhances retrieval by adding another layer of domain-s
thinkingDefault: 'System default',
thinkingEnabled: 'Enabled',
thinkingDisabled: 'Disabled',
thinkingLevelNone: 'None',
thinkingLevelNone: 'Naive',
thinkingLevelLow: 'Low',
thinkingLevelLowDescription: 'Instant responses',
thinkingLevelMedium: 'Medium',

View File

@@ -419,6 +419,8 @@ export default {
updateTooltip:
'发现 {{newlyUploaded}} 个新文档,{{removed}} 个已移除文档。点击编译并合并到当前 Wiki。',
updateSheetTitle: '更新 Wiki',
viewUpdateLogs: '查看更新日志',
log: '日志',
noSkills: '暂无技能',
processingType: '处理类型',
dataPipeline: '切换或配置 ingestion pipeline。',
@@ -471,7 +473,7 @@ export default {
graphPlaceholder: '图谱视图占位',
skills: '技能',
llmWiki: 'Wiki',
navTree: '目录树',
navTree: '目录树/页面索引',
structureGraph: '图谱',
structureMindmap: '思维导图',
structureTimeline: '时间线',
@@ -1041,7 +1043,7 @@ NER使用 spaCy NER 和基于规则的关键词提取来抽取实体和关系
thinkingDefault: '系统默认',
thinkingEnabled: '开启',
thinkingDisabled: '关闭',
thinkingLevelNone: '',
thinkingLevelNone: '简单',
thinkingLevelLow: '轻度',
thinkingLevelLowDescription: '即时响应',
thinkingLevelMedium: '中',

View File

@@ -26,10 +26,15 @@ export const StructureKinds = [
export type StructureKind = (typeof StructureKinds)[number];
export const StructureKindLabelKeyMap: Record<StructureKind, string> = {
export const ViewModeLabelKeyMap: Record<ViewMode, string> = {
[ViewMode.LlmWiki]: 'knowledgeDetails.llmWiki',
[ViewMode.Skills]: 'knowledgeDetails.skills',
[ViewMode.Tree]: 'knowledgeDetails.navTree',
[ViewMode.Graph]: 'knowledgeDetails.structureGraph',
[ViewMode.MindMap]: 'knowledgeDetails.structureMindmap',
[ViewMode.Timeline]: 'knowledgeDetails.structureTimeline',
// [ViewMode.SessionEssence]: 'knowledgeDetails.structureSessionEssence',
// [ViewMode.SessionGraph]: 'knowledgeDetails.structureSessionGraph',
};
export type GenerableViewMode = Exclude<ViewMode, ViewMode.Tree>;

View File

@@ -24,9 +24,9 @@ import { useTranslation } from 'react-i18next';
import {
StructureKind,
StructureKindLabelKeyMap,
ViewMode,
ViewModeGenerateTypeMap,
ViewModeLabelKeyMap,
} from './constants';
import CompilationEmptyState from './empty-state';
import { CompilationLoadingCard } from './loading-card';
@@ -126,7 +126,7 @@ export function DatasetStructureView({ kind }: DatasetStructureViewProps) {
<div className="flex justify-between gap-4 px-4 pt-4">
<ConfirmDeleteDialog
title={t('knowledgeDetails.deleteStructureConfirm', {
name: t(StructureKindLabelKeyMap[kind]),
name: t(ViewModeLabelKeyMap[kind]),
})}
onOk={handleDeleteStructure}
>

View File

@@ -1,23 +1,23 @@
import { CirclePause, Logs, WandSparkles } from 'lucide-react';
import { CircleX, WandSparkles } from 'lucide-react';
import { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { IconFontFill } from '@/components/icon-font';
import { Button } from '@/components/ui/button';
import { cn } from '@/lib/utils';
import {
ITraceInfo,
useDatasetGenerate,
} from '@/pages/dataset/dataset/generate-button/hook';
import { useGenerateStatus } from '@/pages/dataset/dataset/generate-button/use-generate-status';
import { replaceText } from '@/pages/dataset/process-log-modal';
import { toFixed } from '@/utils/common-util';
import {
GenerableViewMode,
ViewMode,
ViewModeGenerateTypeMap,
ViewModeLabelKeyMap,
} from './constants';
import { ProgressLogPanel } from './progress-log-panel';
import { ProgressRing } from './progress-ring';
type EmptyStateType = GenerableViewMode;
@@ -37,16 +37,6 @@ const TitleKeyMap: Record<EmptyStateType, string> = {
// [ViewMode.SessionGraph]: 'knowledgeDetails.noStructureSessionGraph',
};
const LabelKeyMap: Record<EmptyStateType, string> = {
[ViewMode.LlmWiki]: 'knowledgeDetails.artifact',
[ViewMode.Skills]: 'knowledgeDetails.toSkills',
[ViewMode.Graph]: 'knowledgeDetails.structureGraph',
[ViewMode.MindMap]: 'knowledgeDetails.structureMindmap',
[ViewMode.Timeline]: 'knowledgeDetails.structureTimeline',
// [ViewMode.SessionEssence]: 'knowledgeDetails.structureSessionEssence',
// [ViewMode.SessionGraph]: 'knowledgeDetails.structureSessionGraph',
};
export function CompilationEmptyState({
type,
disabled,
@@ -84,48 +74,32 @@ export function CompilationEmptyState({
</Button>
</div>
) : (
<div className="w-full max-w-md p-6 flex flex-col gap-4">
<div className="flex items-center gap-2 text-text-primary">
<Logs className="size-5" />
<span>{t(LabelKeyMap[type])}</span>
</div>
<div className="flex items-center gap-2">
<div
className={cn('bg-border-button h-1 rounded-full', {
'w-[calc(100%-100px)]': status === 'running',
'w-[calc(100%-50px)]': status === 'failed',
})}
>
<div
className={cn('h-1 rounded-full', {
'bg-state-error': status === 'failed',
'bg-accent-primary': status === 'running',
})}
style={{ width: `${toFixed(percent)}%` }}
/>
<div className="grid h-full w-full grid-cols-[1fr_auto_1fr] items-center gap-8 p-6">
<div />
<div className="flex flex-col items-center gap-5">
<ProgressRing percent={percent} failed={status === 'failed'} />
<div className="flex items-center gap-2 text-text-primary">
<span>{t(ViewModeLabelKeyMap[type])}</span>
{status === 'failed' && (
<span className="cursor-pointer" onClick={handleGenerate}>
<IconFontFill
name="reparse"
className="text-accent-primary"
/>
</span>
)}
{status !== 'failed' && (
<span
className="text-state-error cursor-pointer"
onClick={handlePause}
>
<CircleX className="size-5 text-state-error" />
</span>
)}
</div>
{status === 'running' && (
<span>{(toFixed(percent) as string) + '%'}</span>
)}
{status === 'failed' && (
<span
className="text-state-error cursor-pointer"
onClick={handleGenerate}
>
<IconFontFill name="reparse" className="text-accent-primary" />
</span>
)}
{status !== 'failed' && (
<span
className="text-state-error cursor-pointer"
onClick={handlePause}
>
<CirclePause />
</span>
)}
</div>
<div className="whitespace-pre-line text-wrap rounded-lg max-h-[350px] overflow-y-auto scrollbar-auto p-2 bg-bg-base text-sm text-text-secondary">
{replaceText(data?.progress_msg || '')}
<div className="flex min-h-0 h-full justify-end">
<ProgressLogPanel progressMsg={data?.progress_msg} />
</div>
</div>
)}

View File

@@ -10,7 +10,7 @@ import { useCallback, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useParams } from 'react-router';
import { StructureKinds, ViewMode } from './constants';
import { StructureKinds, ViewMode, ViewModeLabelKeyMap } from './constants';
import { DatasetStructureView } from './dataset-structure-view';
import { LlmWikiView } from './llm-wiki-view';
import { NavTreeView } from './nav-tree-view';
@@ -24,40 +24,10 @@ export default function Compilation() {
const [viewMode, setViewMode] = useState<ViewMode>(ViewMode.LlmWiki);
const viewOptions = useMemo<SelectWithSearchFlagOptionType[]>(() => {
return [
{
value: ViewMode.LlmWiki,
label: t('knowledgeDetails.llmWiki'),
},
{
value: ViewMode.Skills,
label: t('knowledgeDetails.skills', 'To Skills'),
},
{
value: ViewMode.Tree,
label: t('knowledgeDetails.navTree'),
},
{
value: ViewMode.Graph,
label: t('knowledgeDetails.structureGraph'),
},
{
value: ViewMode.MindMap,
label: t('knowledgeDetails.structureMindmap'),
},
{
value: ViewMode.Timeline,
label: t('knowledgeDetails.structureTimeline'),
},
// {
// value: ViewMode.SessionEssence,
// label: t('knowledgeDetails.structureSessionEssence'),
// },
// {
// value: ViewMode.SessionGraph,
// label: t('knowledgeDetails.structureSessionGraph'),
// },
];
return Object.values(ViewMode).map((mode) => ({
value: mode,
label: t(ViewModeLabelKeyMap[mode]),
}));
}, [t]);
const handleViewModeChange = useCallback((value: string) => {

View File

@@ -0,0 +1,60 @@
import { Logs } from 'lucide-react';
import { useLayoutEffect, useMemo, useRef } from 'react';
import { useTranslation } from 'react-i18next';
import { cn } from '@/lib/utils';
import { parseProgressMsg } from './utils/parse-progress-msg';
interface IProgressLogPanelProps {
progressMsg?: string;
className?: string;
}
export function ProgressLogPanel({
progressMsg,
className,
}: IProgressLogPanelProps) {
const { t } = useTranslation();
const listRef = useRef<HTMLDivElement>(null);
const entries = useMemo(() => parseProgressMsg(progressMsg), [progressMsg]);
// Depend on progressMsg itself (not entries.length): backend head-trimming
// can keep the line count unchanged while the content has changed.
useLayoutEffect(() => {
const el = listRef.current;
if (el) {
el.scrollTop = el.scrollHeight;
}
}, [progressMsg]);
return (
<div className={cn('flex min-h-0 flex-col gap-2', className)}>
<div className="flex items-center gap-2 text-text-primary">
<Logs className="size-5" />
<span>{t('knowledgeDetails.log')}</span>
</div>
<div
ref={listRef}
className="flex min-h-0 flex-1 flex-col gap-5 overflow-y-auto scrollbar-auto"
>
{entries.map((entry) => (
<div
key={entry.key}
className="flex items-center gap-2 rounded-lg border border-border-button bg-bg-card px-3 py-2"
>
<span
className={cn(
'size-2 shrink-0 rounded-full',
entry.isError ? 'bg-state-error' : 'bg-state-success',
)}
/>
<span className="break-all text-sm text-text-primary">
{entry.text}
</span>
</div>
))}
</div>
</div>
);
}

View File

@@ -0,0 +1,65 @@
import { cn } from '@/lib/utils';
import { toFixed } from '@/utils/common-util';
interface IProgressRingProps {
percent: number;
failed?: boolean;
size?: number;
}
const DefaultRingSize = 240;
const RingStrokeWidth = 16;
export function ProgressRing({
percent,
failed = false,
size = DefaultRingSize,
}: IProgressRingProps) {
const radius = (size - RingStrokeWidth * 2) / 2;
const circumference = 2 * Math.PI * radius;
const strokeDashoffset = circumference - (percent / 100) * circumference;
return (
<div
role="progressbar"
aria-valuemin={0}
aria-valuemax={100}
aria-valuenow={Math.round(percent)}
className="relative inline-flex items-center justify-center"
style={{ width: size, height: size }}
>
<svg
className="rotate-[-90deg] transform"
width={size}
height={size}
viewBox={`0 0 ${size} ${size}`}
fill="none"
stroke="currentColor"
>
<circle
className="text-border-button"
strokeWidth={RingStrokeWidth}
cx={size / 2}
cy={size / 2}
r={radius}
/>
<circle
className={cn(
'transition-[stroke-dashoffset] duration-300 ease-linear',
failed ? 'text-state-error' : 'text-accent-primary',
)}
strokeWidth={RingStrokeWidth}
strokeLinecap="round"
strokeDasharray={circumference}
strokeDashoffset={strokeDashoffset}
cx={size / 2}
cy={size / 2}
r={radius}
/>
</svg>
<span className="absolute text-5xl font-medium text-text-primary">
{`${toFixed(percent, 0)}%`}
</span>
</div>
);
}

View File

@@ -0,0 +1,15 @@
export interface IProgressLogEntry {
key: number;
text: string;
isError: boolean;
}
export const parseProgressMsg = (progressMsg?: string): IProgressLogEntry[] =>
(progressMsg ?? '')
.split('\n')
.filter((line) => line.trim().length > 0)
.map((line, index) => ({
key: index,
text: line,
isError: line.includes('[ERROR]') || line.startsWith('ERROR:'),
}));

View File

@@ -9,7 +9,9 @@ import {
TooltipTrigger,
} from '@/components/ui/tooltip';
import { IArtifact } from '@/interfaces/database/dataset';
import { GenerateStatus } from '@/pages/dataset/dataset/generate-button/constants';
import { ITraceInfo } from '@/pages/dataset/dataset/generate-button/hook';
import { useGenerateStatus } from '@/pages/dataset/dataset/generate-button/use-generate-status';
import { Trash2, WandSparkles } from 'lucide-react';
import { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
@@ -19,6 +21,7 @@ import { useWikiClear } from './hooks/use-wiki-clear';
import { useWikiUpdate } from './hooks/use-wiki-update';
import { WikiGraphPanel } from './wiki-graph-panel';
import { WikiNavBar } from './wiki-nav-bar';
import { WikiUpdateProgress } from './wiki-update-progress';
import { WikiUpdateSheet } from './wiki-update-sheet';
type WikiLeftPanelProps = {
@@ -58,15 +61,22 @@ export function WikiLeftPanel({
loading: updateLoading,
} = useWikiUpdate();
const { status } = useGenerateStatus(traceData);
const isGenerating =
status === GenerateStatus.running || status === GenerateStatus.failed;
const handleUpdateClick = useCallback(async () => {
onUpdateSheetOpenChange(true);
if (status === GenerateStatus.running) {
return;
}
await handleUpdate();
}, [handleUpdate, onUpdateSheetOpenChange]);
}, [status, handleUpdate, onUpdateSheetOpenChange]);
return (
<aside className="size-full flex flex-col p-5">
<div className="flex items-center justify-between pb-5">
{hasChanges && (
{(hasChanges || isGenerating) && (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
@@ -75,27 +85,37 @@ export function WikiLeftPanel({
onClick={handleUpdateClick}
disabled={updateLoading}
>
{t('knowledgeDetails.update', { defaultValue: 'Update' })}
{newlyUploaded > 0 && (
<Badge variant="success" className="ml-1">
{newlyUploaded}
</Badge>
{isGenerating ? (
<WikiUpdateProgress data={traceData} />
) : (
<>
{t('knowledgeDetails.update', { defaultValue: 'Update' })}
{newlyUploaded > 0 && (
<Badge variant="success" className="ml-1">
{newlyUploaded}
</Badge>
)}
{removed > 0 && (
<Badge variant="destructive" className="ml-1">
{removed}
</Badge>
)}
<WandSparkles />
</>
)}
{removed > 0 && (
<Badge variant="destructive" className="ml-1">
{removed}
</Badge>
)}
<WandSparkles />
</Button>
</TooltipTrigger>
<TooltipContent>
{t('knowledgeDetails.updateTooltip', {
newlyUploaded,
removed,
defaultValue:
'{{newlyUploaded}} new, {{removed}} removed documents found. Click to compile and merge into current Wiki.',
})}
{isGenerating
? t('knowledgeDetails.viewUpdateLogs', {
defaultValue: 'View update logs',
})
: t('knowledgeDetails.updateTooltip', {
newlyUploaded,
removed,
defaultValue:
'{{newlyUploaded}} new, {{removed}} removed documents found. Click to compile and merge into current Wiki.',
})}
</TooltipContent>
</Tooltip>
</TooltipProvider>

View File

@@ -0,0 +1,61 @@
import { CircleX } from 'lucide-react';
import { useCallback, type MouseEvent } from 'react';
import { IconFontFill } from '@/components/icon-font';
import { cn } from '@/lib/utils';
import { GenerateType } from '@/pages/dataset/dataset/generate-button/constants';
import {
ITraceInfo,
useDatasetGenerate,
} from '@/pages/dataset/dataset/generate-button/hook';
import { useGenerateStatus } from '@/pages/dataset/dataset/generate-button/use-generate-status';
import { toFixed } from '@/utils/common-util';
type WikiUpdateProgressProps = {
data?: ITraceInfo;
};
export function WikiUpdateProgress({ data }: WikiUpdateProgressProps) {
const { pauseGenerate } = useDatasetGenerate();
const { status, percent } = useGenerateStatus(data);
const handlePause = useCallback(
(e: MouseEvent) => {
e.stopPropagation();
if (data?.id) {
pauseGenerate({ task_id: data.id, type: GenerateType.Artifact }).catch(
() => {},
);
}
},
[pauseGenerate, data?.id],
);
return (
<span className="flex items-center gap-2">
<span className="bg-border-button h-1 w-16 rounded-full">
<span
className={cn('block h-1 rounded-full', {
'bg-state-error': status === 'failed',
'bg-accent-primary': status === 'running',
})}
style={{ width: `${toFixed(percent)}%` }}
/>
</span>
{status === 'running' && (
<>
<span>{(toFixed(percent) as string) + '%'}</span>
<span
className="text-state-error cursor-pointer"
onClick={handlePause}
>
<CircleX className="size-4" />
</span>
</>
)}
{status === 'failed' && (
<IconFontFill name="reparse" className="text-accent-primary" />
)}
</span>
);
}

View File

@@ -1,5 +1,4 @@
import { CirclePause, Logs } from 'lucide-react';
import { useCallback, useEffect } from 'react';
import { useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import {
@@ -8,15 +7,11 @@ import {
SheetHeader,
SheetTitle,
} from '@/components/ui/sheet';
import { cn } from '@/lib/utils';
import { GenerateType } from '@/pages/dataset/dataset/generate-button/constants';
import {
ITraceInfo,
useDatasetGenerate,
} from '@/pages/dataset/dataset/generate-button/hook';
import { GenerateStatus } from '@/pages/dataset/dataset/generate-button/constants';
import { ITraceInfo } from '@/pages/dataset/dataset/generate-button/hook';
import { useGenerateStatus } from '@/pages/dataset/dataset/generate-button/use-generate-status';
import { replaceText } from '@/pages/dataset/process-log-modal';
import { toFixed } from '@/utils/common-util';
import { ProgressLogPanel } from '../progress-log-panel';
type WikiUpdateSheetProps = {
open: boolean;
@@ -30,23 +25,14 @@ export function WikiUpdateSheet({
data,
}: WikiUpdateSheetProps) {
const { t } = useTranslation();
const { pauseGenerate } = useDatasetGenerate();
const { status, percent } = useGenerateStatus(data);
const { status } = useGenerateStatus(data);
useEffect(() => {
if (status === 'completed') {
if (status === GenerateStatus.completed) {
onOpenChange(false);
}
}, [status, onOpenChange]);
const handlePause = useCallback(() => {
if (data?.id) {
pauseGenerate({ task_id: data.id, type: GenerateType.Artifact }).catch(
() => {},
);
}
}, [pauseGenerate, data?.id]);
return (
<Sheet open={open} onOpenChange={onOpenChange} modal={false}>
<SheetContent className="flex flex-col">
@@ -57,43 +43,11 @@ export function WikiUpdateSheet({
})}
</SheetTitle>
</SheetHeader>
<div className="flex-1 min-h-0 flex flex-col gap-4 pt-4">
<div className="flex items-center gap-2 text-text-primary">
<Logs className="size-5" />
<span>
{t('knowledgeDetails.artifact', { defaultValue: 'Artifact' })}
</span>
</div>
<div className="flex items-center gap-2">
<div
className={cn('bg-border-button h-1 rounded-full', {
'w-[calc(100%-100px)]': status === 'running',
'w-[calc(100%-50px)]': status === 'failed',
})}
>
<div
className={cn('h-1 rounded-full', {
'bg-state-error': status === 'failed',
'bg-accent-primary': status === 'running',
})}
style={{ width: `${toFixed(percent)}%` }}
/>
</div>
{status === 'running' && (
<span>{(toFixed(percent) as string) + '%'}</span>
)}
{status !== 'failed' && (
<span
className="text-state-error cursor-pointer"
onClick={handlePause}
>
<CirclePause />
</span>
)}
</div>
<div className="flex-1 whitespace-pre-line text-wrap rounded-lg overflow-y-auto scrollbar-auto p-2 bg-bg-base text-sm text-text-secondary">
{replaceText(data?.progress_msg || '')}
</div>
<div className="flex-1 min-h-0 flex flex-col pt-4">
<ProgressLogPanel
progressMsg={data?.progress_msg}
className="flex-1"
/>
</div>
</SheetContent>
</Sheet>