diff --git a/web/src/components/markdown-editor.tsx b/web/src/components/markdown-editor.tsx
index 643e49fcbe..357320bb1c 100644
--- a/web/src/components/markdown-editor.tsx
+++ b/web/src/components/markdown-editor.tsx
@@ -7,12 +7,14 @@ interface MarkdownEditorProps {
content: string;
onChange?: (content: string) => void;
readOnly?: boolean;
+ onWikiLinkClick?: (pageType: 'concept' | 'entity', slug: string) => void;
}
export default function MarkdownEditor({
content,
onChange,
readOnly = false,
+ onWikiLinkClick,
}: MarkdownEditorProps) {
const [showSource, setShowSource] = useState(false);
const [rawContent, setRawContent] = useState(content);
@@ -61,6 +63,7 @@ export default function MarkdownEditor({
placeholder={readOnly ? '' : 'Start writing...'}
onToggleSource={toggleSource}
showSource={showSource}
+ onWikiLinkClick={onWikiLinkClick}
/>
({
queryKey: ArtifactKeys.detail(knowledgeBaseId, pageType, slug),
- enabled: !!knowledgeBaseId && !!artifact && !!pageType && !!slug,
- gcTime: 0,
+ enabled: !!knowledgeBaseId && !!artifact && !!pageType && !!slug && enabled,
queryFn: async () => {
const { data } = await getArtifactPage(knowledgeBaseId, pageType, slug);
return data?.data ?? null;
diff --git a/web/src/lib/editor/lexical-editor.tsx b/web/src/lib/editor/lexical-editor.tsx
index f314827916..85bdaaa539 100644
--- a/web/src/lib/editor/lexical-editor.tsx
+++ b/web/src/lib/editor/lexical-editor.tsx
@@ -40,6 +40,7 @@ interface Props {
placeholder?: string;
onToggleSource?: () => void;
showSource?: boolean;
+ onWikiLinkClick?: (pageType: 'concept' | 'entity', slug: string) => void;
}
function InitialContentPlugin({
@@ -133,6 +134,7 @@ function ChangeListener({
import FloatingSelectionToolbar from './plugins/floating-selection-toolbar';
import TableActionsPlugin from './plugins/table-actions-plugin';
import ToolbarPlugin from './plugins/toolbar-plugin';
+import { WikiLinkClickPlugin } from './plugins/wiki-link-click-plugin';
export default function LexicalEditor({
content,
@@ -141,6 +143,7 @@ export default function LexicalEditor({
placeholder = 'Start writing...',
onToggleSource,
showSource = false,
+ onWikiLinkClick,
}: Props): JSX.Element {
const transformers = useMemo(() => CORE_TRANSFORMERS, []);
@@ -187,6 +190,7 @@ export default function LexicalEditor({
+
diff --git a/web/src/lib/editor/plugins/wiki-link-click-plugin.tsx b/web/src/lib/editor/plugins/wiki-link-click-plugin.tsx
new file mode 100644
index 0000000000..2bccc810a1
--- /dev/null
+++ b/web/src/lib/editor/plugins/wiki-link-click-plugin.tsx
@@ -0,0 +1,50 @@
+import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
+import {
+ parseWikiLinkHref,
+ type WikiPageType,
+} from '@/pages/dataset/compilation/utils/parse-wiki-link';
+import { useEffect } from 'react';
+
+type WikiLinkClickPluginProps = {
+ onWikiLinkClick?: (pageType: WikiPageType, slug: string) => void;
+};
+
+function findAnchorElement(
+ target: EventTarget | null,
+): HTMLAnchorElement | null {
+ if (!(target instanceof HTMLElement)) return null;
+ return target.closest('a[href]') as HTMLAnchorElement | null;
+}
+
+export function WikiLinkClickPlugin({
+ onWikiLinkClick,
+}: WikiLinkClickPluginProps) {
+ const [editor] = useLexicalComposerContext();
+
+ useEffect(() => {
+ if (!onWikiLinkClick) return;
+
+ const rootElement = editor.getRootElement();
+ if (!rootElement) return;
+
+ const handleClick = (event: MouseEvent) => {
+ const anchor = findAnchorElement(event.target);
+ if (!anchor) return;
+
+ const href = anchor.getAttribute('href');
+ if (!href) return;
+
+ const parsed = parseWikiLinkHref(href);
+ if (!parsed) return;
+
+ event.preventDefault();
+ event.stopPropagation();
+ onWikiLinkClick(parsed.pageType, parsed.slug);
+ };
+
+ rootElement.addEventListener('click', handleClick);
+ return () => rootElement.removeEventListener('click', handleClick);
+ }, [editor, onWikiLinkClick]);
+
+ return null;
+}
diff --git a/web/src/pages/dataset/compilation/hooks/use-wiki-detail-content.ts b/web/src/pages/dataset/compilation/hooks/use-wiki-detail-content.ts
new file mode 100644
index 0000000000..81dc3a3dcd
--- /dev/null
+++ b/web/src/pages/dataset/compilation/hooks/use-wiki-detail-content.ts
@@ -0,0 +1,219 @@
+import { useFetchDocumentsByIds } from '@/hooks/use-document-request';
+import {
+ useFetchArtifactPage,
+ useFetchWikiCommit,
+} from '@/hooks/use-knowledge-request';
+import { Docagg } from '@/interfaces/database/chat';
+import { IArtifact, IWikiCommit } from '@/interfaces/database/dataset';
+import { downloadMarkdownFile } from '@/utils/file-util';
+import { useCallback, useEffect, useMemo } from 'react';
+
+import type { WikiPageType } from '../utils/parse-wiki-link';
+import { useCommitArtifact } from './use-commit-artifact';
+import { useWikiEditor } from './use-wiki-editor';
+import { useWikiLinkNavigation } from './use-wiki-link-navigation';
+
+type UseWikiDetailContentOptions = {
+ selectedArtifact: IArtifact | null;
+ selectedVersion: IWikiCommit | null;
+ onSelectVersion: (version: IWikiCommit | null) => void;
+ onSelectArtifact: (artifact: IArtifact) => void;
+};
+
+export function useWikiDetailContent({
+ selectedArtifact,
+ selectedVersion,
+ onSelectVersion,
+ onSelectArtifact,
+}: UseWikiDetailContentOptions) {
+ const isVersionView = !!selectedVersion;
+
+ const { data: pageData, loading: pageLoading } = useFetchArtifactPage(
+ isVersionView ? null : selectedArtifact,
+ );
+ const { data: commitDetail, loading: commitLoading } = useFetchWikiCommit(
+ selectedVersion?.id ?? null,
+ );
+
+ const {
+ currentEntry,
+ previousEntry,
+ canGoBack,
+ push,
+ goBack,
+ reset,
+ updateCurrentTitle,
+ } = useWikiLinkNavigation();
+
+ // Keep the current stack entry's title in sync with pageData.
+ useEffect(() => {
+ if (!currentEntry || !pageData) return;
+ if (currentEntry.slug !== pageData.slug) return;
+ updateCurrentTitle(pageData.title);
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [currentEntry?.slug, pageData?.slug, pageData?.title, updateCurrentTitle]);
+
+ // When selectedArtifact changes from the left panel (not from our own
+ // wiki-link navigation), reset the stack. Wiki-link clicks call push()
+ // before onSelectArtifact(), so currentEntry.slug already matches by the
+ // time this effect runs and we bail out.
+ useEffect(() => {
+ if (isVersionView || !selectedArtifact) return;
+ if (currentEntry?.slug === selectedArtifact.slug) return;
+
+ reset({
+ slug: selectedArtifact.slug,
+ title: selectedArtifact.title,
+ pageType: selectedArtifact.page_type ?? '',
+ });
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [
+ isVersionView,
+ selectedArtifact?.slug,
+ selectedArtifact?.title,
+ selectedArtifact?.page_type,
+ currentEntry?.slug,
+ reset,
+ ]);
+
+ const content = isVersionView
+ ? (commitDetail?.content_after ?? '')
+ : (pageData?.content_md_rendered ?? '');
+
+ const title =
+ currentEntry?.title ||
+ pageData?.title ||
+ selectedArtifact?.title ||
+ currentEntry?.slug ||
+ '';
+
+ const displayedArtifact = currentEntry
+ ? { slug: currentEntry.slug, title: currentEntry.title, page_type: currentEntry.pageType }
+ : selectedArtifact;
+
+ const previousEntryTitle = previousEntry?.title || previousEntry?.slug;
+
+ const editorKey = isVersionView
+ ? `${selectedArtifact?.slug}@${selectedVersion?.id}`
+ : (selectedArtifact?.slug ?? '');
+
+ const handleMarkdownLinkClick = useCallback(
+ (pageType: WikiPageType, slug: string) => {
+ if (isVersionView) return;
+ if (currentEntry?.slug === slug && currentEntry?.pageType === pageType)
+ return;
+
+ // Sync the current entry's title from pageData before pushing a new
+ // entry, so the title is preserved when this entry becomes the
+ // "previous" entry for the back button.
+ if (
+ currentEntry &&
+ pageData &&
+ pageData.slug === currentEntry.slug &&
+ pageData.title
+ ) {
+ updateCurrentTitle(pageData.title);
+ }
+
+ push({ slug, title: '', pageType });
+ onSelectArtifact({ slug, page_type: pageType, title: '' });
+ },
+ [
+ push,
+ onSelectArtifact,
+ isVersionView,
+ currentEntry,
+ pageData,
+ updateCurrentTitle,
+ ],
+ );
+
+ const handleBack = useCallback(() => {
+ if (!previousEntry) return;
+ goBack();
+ onSelectArtifact({
+ slug: previousEntry.slug,
+ page_type: previousEntry.pageType,
+ title: previousEntry.title,
+ });
+ }, [goBack, previousEntry, onSelectArtifact]);
+
+ const {
+ editedContent,
+ isDirty,
+ handleContentChange,
+ handleCancelEdit,
+ handleMarkAsSaved,
+ } = useWikiEditor({
+ content,
+ artifactSlug: editorKey,
+ });
+
+ const handleCommitSuccess = useCallback(() => {
+ handleMarkAsSaved();
+ if (isVersionView) {
+ onSelectVersion(null);
+ }
+ }, [handleMarkAsSaved, isVersionView, onSelectVersion]);
+
+ const { isOpen, open, close, form, handleConfirm, isUpdating } =
+ useCommitArtifact({
+ editedContent,
+ pageType: currentEntry?.pageType ?? selectedArtifact?.page_type ?? '',
+ slug: currentEntry?.slug ?? selectedArtifact?.slug ?? '',
+ onSuccess: handleCommitSuccess,
+ });
+
+ const { documents } = useFetchDocumentsByIds(
+ pageData?.source_doc_ids ?? [],
+ );
+
+ const referenceDocuments = useMemo
(() => {
+ return documents.map(
+ (doc): Docagg => ({
+ doc_id: doc.id,
+ doc_name: doc.name,
+ count: 0,
+ }),
+ );
+ }, [documents]);
+
+ const handleExport = useCallback(() => {
+ const filename = `${title ?? 'document'}.md`;
+ downloadMarkdownFile(editedContent, filename);
+ }, [title, editedContent]);
+
+ const loading = isVersionView ? commitLoading : pageLoading;
+
+ return {
+ isVersionView,
+ title,
+ displayedArtifact,
+ displayedContent: content,
+ displayedPageType: currentEntry?.pageType ?? pageData?.page_type ?? '',
+ displayedSlug: currentEntry?.slug ?? pageData?.slug ?? '',
+ selectedArtifact,
+ selectedVersion,
+ commitDetail,
+ canGoBack,
+ previousEntry,
+ previousEntryTitle,
+ linkNavLoading: false,
+ loading,
+ editedContent,
+ isDirty,
+ referenceDocuments,
+ isOpen,
+ open,
+ close,
+ form,
+ handleConfirm,
+ isUpdating,
+ handleCancelEdit,
+ handleContentChange,
+ handleMarkdownLinkClick,
+ handleBack,
+ handleExport,
+ onSelectVersion,
+ };
+}
diff --git a/web/src/pages/dataset/compilation/hooks/use-wiki-link-navigation.ts b/web/src/pages/dataset/compilation/hooks/use-wiki-link-navigation.ts
new file mode 100644
index 0000000000..94f4b102a8
--- /dev/null
+++ b/web/src/pages/dataset/compilation/hooks/use-wiki-link-navigation.ts
@@ -0,0 +1,64 @@
+import { useCallback, useState } from 'react';
+
+export type NavEntry = {
+ slug: string;
+ title: string;
+ pageType: string;
+};
+
+interface NavState {
+ stack: NavEntry[];
+ currentIndex: number;
+}
+
+export function useWikiLinkNavigation() {
+ const [state, setState] = useState({
+ stack: [],
+ currentIndex: -1,
+ });
+
+ const push = useCallback((entry: NavEntry) => {
+ setState((prev) => ({
+ stack: [...prev.stack.slice(0, prev.currentIndex + 1), entry],
+ currentIndex: prev.currentIndex + 1,
+ }));
+ }, []);
+
+ const goBack = useCallback(() => {
+ setState((prev) => ({
+ ...prev,
+ currentIndex: Math.max(0, prev.currentIndex - 1),
+ }));
+ }, []);
+
+ const reset = useCallback((entry: NavEntry) => {
+ setState({ stack: [entry], currentIndex: 0 });
+ }, []);
+
+ const updateCurrentTitle = useCallback((title: string) => {
+ setState((prev) => {
+ if (prev.currentIndex < 0) return prev;
+ const entry = prev.stack[prev.currentIndex];
+ if (!entry || entry.title === title) return prev;
+ const newStack = [...prev.stack];
+ newStack[prev.currentIndex] = { ...entry, title };
+ return { ...prev, stack: newStack };
+ });
+ }, []);
+
+ const currentEntry =
+ state.currentIndex >= 0 ? state.stack[state.currentIndex] : null;
+ const previousEntry =
+ state.currentIndex > 0 ? state.stack[state.currentIndex - 1] : null;
+ const canGoBack = state.currentIndex > 0;
+
+ return {
+ currentEntry,
+ previousEntry,
+ canGoBack,
+ push,
+ goBack,
+ reset,
+ updateCurrentTitle,
+ };
+}
diff --git a/web/src/pages/dataset/compilation/index.tsx b/web/src/pages/dataset/compilation/index.tsx
index c39dd4e414..e9fa3aa1a0 100644
--- a/web/src/pages/dataset/compilation/index.tsx
+++ b/web/src/pages/dataset/compilation/index.tsx
@@ -153,6 +153,7 @@ export default function Compilation() {
selectedArtifact={selectedArtifact}
selectedVersion={selectedVersion}
onSelectVersion={selectVersion}
+ onSelectArtifact={handleSelectArtifact}
/>
diff --git a/web/src/pages/dataset/compilation/utils/parse-wiki-link.ts b/web/src/pages/dataset/compilation/utils/parse-wiki-link.ts
new file mode 100644
index 0000000000..f802c004ca
--- /dev/null
+++ b/web/src/pages/dataset/compilation/utils/parse-wiki-link.ts
@@ -0,0 +1,50 @@
+export type WikiPageType = 'concept' | 'entity';
+
+/**
+ * Parse an internal wiki link href into pageType and slug.
+ *
+ * Supports the common rendered forms:
+ * artifact/{datasetId}/{pageType}/{slug}
+ * /artifact/{datasetId}/{pageType}/{slug}
+ * {pageType}/{slug}
+ * /{pageType}/{slug}
+ *
+ * Only entity/ and concept/ links are considered wiki navigation links.
+ */
+export function parseWikiLinkHref(
+ href: string,
+): { pageType: WikiPageType; slug: string } | null {
+ const normalized = href.trim();
+ if (!normalized) return null;
+
+ // Prefer the artifact/{datasetId}/{pageType}/{slug} form.
+ const artifactMatch = normalized.match(
+ /(?:^|\/)artifact\/[^/]+\/(entity|concept)\/([^/\s"']+)/,
+ );
+ if (artifactMatch) {
+ return {
+ pageType: artifactMatch[1] as WikiPageType,
+ slug: artifactMatch[2],
+ };
+ }
+
+ // Fallback to a plain {pageType}/{slug} form.
+ const simpleMatch = normalized.match(
+ /(?:^|\/)(entity|concept)\/([^/\s"']+)/,
+ );
+ if (simpleMatch) {
+ return {
+ pageType: simpleMatch[1] as WikiPageType,
+ slug: simpleMatch[2],
+ };
+ }
+
+ return null;
+}
+
+/**
+ * Check whether a link href looks like an internal wiki navigation link.
+ */
+export function isWikiLinkHref(href?: string): boolean {
+ return Boolean(href && parseWikiLinkHref(href));
+}
diff --git a/web/src/pages/dataset/compilation/wiki-detail-content.tsx b/web/src/pages/dataset/compilation/wiki-detail-content.tsx
index 0165528d84..a0b35d55d5 100644
--- a/web/src/pages/dataset/compilation/wiki-detail-content.tsx
+++ b/web/src/pages/dataset/compilation/wiki-detail-content.tsx
@@ -1,221 +1,97 @@
-import Empty from '@/components/empty/empty';
-import MarkdownEditor from '@/components/markdown-editor';
-import { ReferenceDocumentList } from '@/components/next-message-item/reference-document-list';
-import { Button } from '@/components/ui/button';
-import {
- ResizableHandle,
- ResizablePanel,
- ResizablePanelGroup,
-} from '@/components/ui/resizable';
-import { Spin } from '@/components/ui/spin';
-import {
- Tooltip,
- TooltipContent,
- TooltipTrigger,
-} from '@/components/ui/tooltip';
-import { useFetchDocumentsByIds } from '@/hooks/use-document-request';
-import {
- useFetchArtifactPage,
- useFetchWikiCommit,
-} from '@/hooks/use-knowledge-request';
-import { Docagg } from '@/interfaces/database/chat';
-import { IArtifact, IWikiCommit } from '@/interfaces/database/dataset';
-import { downloadMarkdownFile } from '@/utils/file-util';
-import { Download } from 'lucide-react';
-import { useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
-import { useCommitArtifact } from './hooks/use-commit-artifact';
-import { useWikiEditor } from './hooks/use-wiki-editor';
-import { VersionHistorySheet } from './version-history-sheet';
+
+import Empty from '@/components/empty/empty';
+import { IArtifact, IWikiCommit } from '@/interfaces/database/dataset';
+
+import { useWikiDetailContent } from './hooks/use-wiki-detail-content';
import { WikiCommitModal } from './wiki-commit-modal';
-import { WikiVersionDiffPanel } from './wiki-version-diff-panel';
+import { WikiDetailEditorPanel } from './wiki-detail-editor-panel';
+import { WikiDetailHeader } from './wiki-detail-header';
+import { WikiDetailToolbar } from './wiki-detail-toolbar';
type WikiDetailContentProps = {
selectedArtifact: IArtifact | null;
selectedVersion: IWikiCommit | null;
onSelectVersion: (version: IWikiCommit | null) => void;
+ onSelectArtifact: (artifact: IArtifact) => void;
};
export function WikiDetailContent({
selectedArtifact,
selectedVersion,
onSelectVersion,
+ onSelectArtifact,
}: WikiDetailContentProps) {
const { t } = useTranslation();
- const isVersionView = !!selectedVersion;
-
- const { data: pageData, loading: pageLoading } = useFetchArtifactPage(
- isVersionView ? null : selectedArtifact,
- );
- const { data: commitDetail, loading: commitLoading } = useFetchWikiCommit(
- selectedVersion?.id ?? null,
- );
-
- const title = pageData?.title ?? selectedArtifact?.title;
-
- const pageType = isVersionView
- ? selectedArtifact?.page_type
- : pageData?.page_type;
- const slug = isVersionView ? selectedArtifact?.slug : pageData?.slug;
- const content = isVersionView
- ? (commitDetail?.content_after ?? '')
- : (pageData?.content_md_rendered ?? '');
- const sourceDocIds = isVersionView ? [] : (pageData?.source_doc_ids ?? []);
-
- const editorKey = isVersionView
- ? `${selectedArtifact?.slug}@${selectedVersion?.id}`
- : selectedArtifact?.slug;
-
const {
+ isVersionView,
+ title,
+ displayedArtifact,
+ commitDetail,
+ canGoBack,
+ previousEntryTitle,
+ linkNavLoading,
+ loading,
editedContent,
+ displayedContent,
+ referenceDocuments,
isDirty,
- handleContentChange,
+ isOpen,
+ open,
+ close,
+ form,
+ handleConfirm,
+ isUpdating,
handleCancelEdit,
- handleMarkAsSaved,
- } = useWikiEditor({
- content,
- artifactSlug: editorKey,
+ handleContentChange,
+ handleMarkdownLinkClick,
+ handleBack,
+ handleExport,
+ } = useWikiDetailContent({
+ selectedArtifact,
+ selectedVersion,
+ onSelectVersion,
+ onSelectArtifact,
});
- const handleCommitSuccess = useCallback(() => {
- handleMarkAsSaved();
- if (isVersionView) {
- onSelectVersion(null);
- }
- }, [handleMarkAsSaved, isVersionView, onSelectVersion]);
-
- const { isOpen, open, close, form, handleConfirm, isUpdating } =
- useCommitArtifact({
- editedContent,
- pageType: pageType ?? '',
- slug: slug ?? '',
- onSuccess: handleCommitSuccess,
- });
-
- const { documents } = useFetchDocumentsByIds(sourceDocIds);
-
- const referenceDocuments = useMemo(() => {
- return documents.map(
- (doc): Docagg => ({
- doc_id: doc.id,
- doc_name: doc.name,
- count: 0,
- }),
- );
- }, [documents]);
-
- const handleExport = useCallback(() => {
- const filename = `${title ?? 'document'}.md`;
- downloadMarkdownFile(editedContent, filename);
- }, [title, editedContent]);
-
- const renderToolbarButtons = () => {
- if (isDirty) {
- return (
-
-
-
-
- );
- }
-
- return (
-
-
-
-
-
- {t('knowledgeDetails.export')}
-
-
-
- );
- };
-
- const loading = isVersionView ? commitLoading : pageLoading;
+ const toolbar = (
+
+ );
return (
{selectedArtifact ? (
<>
-
-
-
-
-
-
- {loading && !content ? (
-
-
-
- ) : (
-
- )}
-
- {referenceDocuments.length > 0 && (
-
-
- {t('knowledgeDetails.sourceDocuments')}
-
-
-
- )}
-
-
-
- {isVersionView && commitDetail && (
- <>
-
-
-
-
- >
- )}
-
-
+
void;
+ onWikiLinkClick: (pageType: WikiPageType, slug: string) => void;
+};
+
+export function WikiDetailEditorPanel({
+ loading,
+ editedContent,
+ displayedContent,
+ referenceDocuments,
+ isVersionView,
+ commitDetail,
+ onContentChange,
+ onWikiLinkClick,
+}: WikiDetailEditorPanelProps) {
+ const { t } = useTranslation();
+
+ return (
+
+
+
+
+ {loading && !displayedContent ? (
+
+
+
+ ) : (
+
+ )}
+
+ {referenceDocuments.length > 0 && (
+
+
+ {t('knowledgeDetails.sourceDocuments')}
+
+
+
+ )}
+
+
+
+ {isVersionView && commitDetail && (
+ <>
+
+
+
+
+ >
+ )}
+
+
+ );
+}
diff --git a/web/src/pages/dataset/compilation/wiki-detail-header.tsx b/web/src/pages/dataset/compilation/wiki-detail-header.tsx
new file mode 100644
index 0000000000..395ef856d5
--- /dev/null
+++ b/web/src/pages/dataset/compilation/wiki-detail-header.tsx
@@ -0,0 +1,68 @@
+import type { ReactNode } from 'react';
+import { useTranslation } from 'react-i18next';
+
+import type { IArtifact, IWikiCommit } from '@/interfaces/database/dataset';
+import { MoveLeft } from 'lucide-react';
+
+import { Button } from '@/components/ui/button';
+
+type WikiDetailHeaderProps = {
+ title: string | undefined;
+ displayedArtifact: IArtifact | null | undefined;
+ commitDetail: IWikiCommit | null | undefined;
+ isVersionView: boolean;
+ toolbar: ReactNode;
+ canGoBack: boolean;
+ previousEntryTitle: string | undefined;
+ linkNavLoading: boolean;
+ onBack: () => void;
+};
+
+export function WikiDetailHeader({
+ title,
+ displayedArtifact,
+ commitDetail,
+ isVersionView,
+ toolbar,
+ canGoBack,
+ previousEntryTitle,
+ linkNavLoading,
+ onBack,
+}: WikiDetailHeaderProps) {
+ const { t } = useTranslation();
+
+ return (
+
+ );
+}
diff --git a/web/src/pages/dataset/compilation/wiki-detail-toolbar.tsx b/web/src/pages/dataset/compilation/wiki-detail-toolbar.tsx
new file mode 100644
index 0000000000..950745e676
--- /dev/null
+++ b/web/src/pages/dataset/compilation/wiki-detail-toolbar.tsx
@@ -0,0 +1,75 @@
+import type { IArtifact, IWikiCommit } from '@/interfaces/database/dataset';
+import { Download } from 'lucide-react';
+import { useTranslation } from 'react-i18next';
+
+import { Button } from '@/components/ui/button';
+import {
+ Tooltip,
+ TooltipContent,
+ TooltipTrigger,
+} from '@/components/ui/tooltip';
+
+import { VersionHistorySheet } from './version-history-sheet';
+
+type WikiDetailToolbarProps = {
+ isDirty: boolean;
+ selectedArtifact: IArtifact | null;
+ selectedVersion: IWikiCommit | null;
+ onCancelEdit: () => void;
+ onCommitClick: () => void;
+ onExport: () => void;
+ onSelectVersion: (version: IWikiCommit | null) => void;
+};
+
+export function WikiDetailToolbar({
+ isDirty,
+ selectedArtifact,
+ selectedVersion,
+ onCancelEdit,
+ onCommitClick,
+ onExport,
+ onSelectVersion,
+}: WikiDetailToolbarProps) {
+ const { t } = useTranslation();
+
+ if (isDirty) {
+ return (
+
+
+
+
+ );
+ }
+
+ return (
+
+
+
+
+
+ {t('knowledgeDetails.export')}
+
+
+
+ );
+}