feat: complete public wiki and embedded previews
This commit is contained in:
1 parent
bc49df4561
commit
30943159bf
18 files changed
+926
-48
No files matched your search
@@ -1,7 +1,12 @@
|
||||
"use client";
|
||||
|
||||
import { useMemo } from "react";
|
||||
import type { AnchorHTMLAttributes, HTMLAttributes, ReactNode } from "react";
|
||||
import type {
|
||||
AnchorHTMLAttributes,
|
||||
HTMLAttributes,
|
||||
MouseEvent as ReactMouseEvent,
|
||||
ReactNode,
|
||||
} from "react";
|
||||
|
||||
import {
|
||||
MessageResponse,
|
||||
@@ -34,6 +39,12 @@ export type MarkdownContentProps = {
|
||||
className?: string;
|
||||
remarkPlugins?: MessageResponseProps["remarkPlugins"];
|
||||
components?: MessageResponseProps["components"];
|
||||
linkClassName?: (href: string, label: string) => string | undefined;
|
||||
onLinkClick?: (
|
||||
event: ReactMouseEvent<HTMLAnchorElement>,
|
||||
href: string,
|
||||
label: string,
|
||||
) => void;
|
||||
};
|
||||
|
||||
/** Renders markdown content. */
|
||||
@@ -43,6 +54,8 @@ export function MarkdownContent({
|
||||
className,
|
||||
remarkPlugins = streamdownPlugins.remarkPlugins,
|
||||
components: componentsFromProps,
|
||||
linkClassName,
|
||||
onLinkClick,
|
||||
}: MarkdownContentProps) {
|
||||
const anchorResolver = useMemo(() => createMarkdownAnchorResolver(content), [content]);
|
||||
const components = useMemo(() => {
|
||||
@@ -93,6 +106,7 @@ export function MarkdownContent({
|
||||
);
|
||||
}
|
||||
const finalHref = resolvedHref ?? props.href;
|
||||
const label = extractTextFromChildren(props.children);
|
||||
return (
|
||||
<a
|
||||
{...rest}
|
||||
@@ -100,6 +114,7 @@ export function MarkdownContent({
|
||||
className={cn(
|
||||
"text-primary decoration-primary/30 hover:decoration-primary/60 underline underline-offset-2 transition-colors",
|
||||
className,
|
||||
linkClassName?.(finalHref || "", label),
|
||||
)}
|
||||
target={target ?? (external ? "_blank" : undefined)}
|
||||
rel={rel ?? (external ? "noopener noreferrer" : undefined)}
|
||||
@@ -110,6 +125,7 @@ export function MarkdownContent({
|
||||
event.preventDefault();
|
||||
scrollToInPageAnchor(event.currentTarget, finalHref);
|
||||
}
|
||||
onLinkClick?.(event, finalHref || "", label);
|
||||
rest.onClick?.(event);
|
||||
}}
|
||||
/>
|
||||
@@ -117,7 +133,7 @@ export function MarkdownContent({
|
||||
},
|
||||
...componentsFromProps,
|
||||
};
|
||||
}, [anchorResolver, componentsFromProps]);
|
||||
}, [anchorResolver, componentsFromProps, linkClassName, onLinkClick]);
|
||||
|
||||
if (!content) return null;
|
||||
|
||||
|
||||
@@ -5,6 +5,38 @@ export type WikiPageUrlBuilder = (
|
||||
wikiSlug: string,
|
||||
) => string;
|
||||
|
||||
export type WikiPageLinkTarget = {
|
||||
slug: string;
|
||||
title: string;
|
||||
aliases?: string[];
|
||||
page_type?: string;
|
||||
summary?: string;
|
||||
category_path?: string[];
|
||||
};
|
||||
|
||||
export type WikiIndexGroup = {
|
||||
type: string;
|
||||
total: number;
|
||||
items: WikiPageLinkTarget[];
|
||||
};
|
||||
|
||||
type WikiIndexCategoryNode = {
|
||||
label: string;
|
||||
pages: WikiPageLinkTarget[];
|
||||
children: Map<string, WikiIndexCategoryNode>;
|
||||
};
|
||||
|
||||
const WIKI_INDEX_TYPE_LABELS: Record<string, string> = {
|
||||
summary: "摘要",
|
||||
entity: "实体",
|
||||
concept: "概念",
|
||||
overview: "概览",
|
||||
directory: "目录",
|
||||
article: "文章",
|
||||
};
|
||||
|
||||
const WIKI_INDEX_TYPE_ORDER = ["summary", "entity", "concept"];
|
||||
|
||||
export function stripWikiFrontmatter(value: string) {
|
||||
return value.replace(/^\s*---\s*\n[\s\S]*?\n---\s*(?:\n|$)/, "").trim();
|
||||
}
|
||||
@@ -38,18 +70,192 @@ export function wikiLinkMarkdown(
|
||||
.replace(/\[\[([^\]]+)\]\]/g, (_match, slug) => link(slug));
|
||||
}
|
||||
|
||||
function normalizeWikiLinkLabel(value: string): string {
|
||||
return value
|
||||
.replace(/\\([\\`*_[\]{}()#+\-.!])/g, "$1")
|
||||
.replace(/[*_`~]/g, "")
|
||||
.replace(/\s+/g, " ")
|
||||
.trim()
|
||||
.toLocaleLowerCase("zh-CN");
|
||||
}
|
||||
|
||||
function isLegacyWikiPlaceholderUrl(value: string): boolean {
|
||||
return /\/platform\/knowledge-bases\/[^/?#)]+/i.test(value);
|
||||
}
|
||||
|
||||
function wikiIndexTypeRank(pageType: string): number {
|
||||
const index = WIKI_INDEX_TYPE_ORDER.indexOf(pageType);
|
||||
return index >= 0 ? index : WIKI_INDEX_TYPE_ORDER.length;
|
||||
}
|
||||
|
||||
function wikiIndexPageLine(
|
||||
page: WikiPageLinkTarget,
|
||||
knowledgeBaseId: string,
|
||||
buildPageUrl: WikiPageUrlBuilder,
|
||||
): string {
|
||||
const label = (page.title || page.slug).replace(/\]/g, "\\]");
|
||||
const summary = (page.summary || "").replace(/\s+/g, " ").trim();
|
||||
return `[${label}](${buildPageUrl(knowledgeBaseId, page.slug)})${summary ? ` — ${summary}` : ""}`;
|
||||
}
|
||||
|
||||
function renderWikiIndexCategory(
|
||||
node: WikiIndexCategoryNode,
|
||||
knowledgeBaseId: string,
|
||||
buildPageUrl: WikiPageUrlBuilder,
|
||||
): string[] {
|
||||
const lines = node.label ? [`**${node.label}**`] : [];
|
||||
lines.push(...node.pages.map((page) => wikiIndexPageLine(page, knowledgeBaseId, buildPageUrl)));
|
||||
const children = Array.from(node.children.values()).sort((left, right) =>
|
||||
left.label.localeCompare(right.label, "zh-Hans-CN"),
|
||||
);
|
||||
for (const child of children) {
|
||||
lines.push(...renderWikiIndexCategory(child, knowledgeBaseId, buildPageUrl));
|
||||
}
|
||||
return lines;
|
||||
}
|
||||
|
||||
/** Build the category catalogue that WeKnora renders dynamically for its index page. */
|
||||
export function buildWikiIndexCatalogMarkdown(
|
||||
pages: WikiPageLinkTarget[],
|
||||
knowledgeBaseId: string,
|
||||
buildPageUrl: WikiPageUrlBuilder = buildWeKnoraWikiPageUrl,
|
||||
indexGroups: WikiIndexGroup[] = [],
|
||||
): string {
|
||||
if (indexGroups.length > 0) {
|
||||
const sections: string[] = [];
|
||||
for (const group of indexGroups) {
|
||||
if (group.items.length === 0) continue;
|
||||
const seenCategories = new Set<string>();
|
||||
const lines: string[] = [];
|
||||
for (const page of group.items) {
|
||||
const categories = (page.category_path || []).map((item) => item.trim()).filter(Boolean);
|
||||
for (let depth = 0; depth < categories.length; depth += 1) {
|
||||
const path = categories.slice(0, depth + 1).join("/");
|
||||
if (seenCategories.has(path)) continue;
|
||||
seenCategories.add(path);
|
||||
lines.push(`${" ".repeat(depth)}**${categories[depth]}**`);
|
||||
}
|
||||
lines.push(
|
||||
`${" ".repeat(categories.length)}${wikiIndexPageLine(page, knowledgeBaseId, buildPageUrl)}`,
|
||||
);
|
||||
}
|
||||
const label = WIKI_INDEX_TYPE_LABELS[group.type] || group.type;
|
||||
sections.push(`## ${label} (${group.total})`, lines.join("\n"));
|
||||
}
|
||||
return sections.join("\n\n");
|
||||
}
|
||||
|
||||
const groups = new Map<string, WikiPageLinkTarget[]>();
|
||||
for (const page of pages) {
|
||||
const pageType = (page.page_type || "article").trim().toLowerCase();
|
||||
const slugName = page.slug.split("/").filter(Boolean).at(-1)?.toLowerCase();
|
||||
if (pageType === "index" || ["index", "home", "readme"].includes(slugName || "")) continue;
|
||||
const group = groups.get(pageType) || [];
|
||||
group.push(page);
|
||||
groups.set(pageType, group);
|
||||
}
|
||||
|
||||
const pageTypes = Array.from(groups.keys()).sort((left, right) => {
|
||||
const rankDifference = wikiIndexTypeRank(left) - wikiIndexTypeRank(right);
|
||||
if (rankDifference !== 0) return rankDifference;
|
||||
return (WIKI_INDEX_TYPE_LABELS[left] || left).localeCompare(
|
||||
WIKI_INDEX_TYPE_LABELS[right] || right,
|
||||
"zh-Hans-CN",
|
||||
);
|
||||
});
|
||||
|
||||
const sections: string[] = [];
|
||||
for (const pageType of pageTypes) {
|
||||
const groupPages = groups.get(pageType) || [];
|
||||
const root: WikiIndexCategoryNode = { label: "", pages: [], children: new Map() };
|
||||
for (const page of groupPages) {
|
||||
let node = root;
|
||||
for (const category of page.category_path || []) {
|
||||
const label = category.trim();
|
||||
if (!label) continue;
|
||||
let child = node.children.get(label);
|
||||
if (!child) {
|
||||
child = { label, pages: [], children: new Map() };
|
||||
node.children.set(label, child);
|
||||
}
|
||||
node = child;
|
||||
}
|
||||
node.pages.push(page);
|
||||
}
|
||||
const label = WIKI_INDEX_TYPE_LABELS[pageType] || pageType;
|
||||
sections.push(
|
||||
`## ${label} (${groupPages.length})`,
|
||||
...renderWikiIndexCategory(root, knowledgeBaseId, buildPageUrl),
|
||||
);
|
||||
}
|
||||
return sections.join("\n\n");
|
||||
}
|
||||
|
||||
/**
|
||||
* WeKnora-generated index pages may contain Markdown links whose href only
|
||||
* points at the knowledge-base Wiki shell. Resolve those placeholders by the
|
||||
* visible link label so the native/public reader can open the exact page.
|
||||
*/
|
||||
export function resolveWikiIndexPageLinks(
|
||||
markdown: string,
|
||||
knowledgeBaseId: string,
|
||||
pages: WikiPageLinkTarget[],
|
||||
buildPageUrl: WikiPageUrlBuilder = buildWeKnoraWikiPageUrl,
|
||||
): string {
|
||||
const pagesByLabel = new Map<string, WikiPageLinkTarget>();
|
||||
for (const page of pages) {
|
||||
const labels = [page.title, page.slug, page.slug.split("/").filter(Boolean).at(-1), ...(page.aliases || [])];
|
||||
for (const label of labels) {
|
||||
const normalized = normalizeWikiLinkLabel(label || "");
|
||||
if (normalized && !pagesByLabel.has(normalized)) pagesByLabel.set(normalized, page);
|
||||
}
|
||||
}
|
||||
|
||||
return markdown.replace(
|
||||
/(?<!!)\[([^\]\n]+)\]\(([^)\n]+)\)/g,
|
||||
(original, rawLabel: string, rawUrl: string) => {
|
||||
if (!isLegacyWikiPlaceholderUrl(rawUrl)) return original;
|
||||
const normalizedLabel = normalizeWikiLinkLabel(rawLabel);
|
||||
const withoutSummarySuffix = normalizedLabel.replace(/\s+-\s+summary$/i, "");
|
||||
const target = pagesByLabel.get(normalizedLabel) || pagesByLabel.get(withoutSummarySuffix);
|
||||
if (!target) return original;
|
||||
return `[${rawLabel}](${buildPageUrl(knowledgeBaseId, target.slug)})`;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
export function renderableWikiMarkdown(
|
||||
title: string,
|
||||
content: string,
|
||||
knowledgeBaseId: string,
|
||||
buildPageUrl?: WikiPageUrlBuilder,
|
||||
pages: WikiPageLinkTarget[] = [],
|
||||
indexGroups: WikiIndexGroup[] = [],
|
||||
) {
|
||||
const lines = content.trim().split("\n");
|
||||
if (lines[0]?.replace(/^#+\s*/, "").trim() === title.trim()) {
|
||||
lines[0] = `# ${title}`;
|
||||
}
|
||||
const baseMarkdown = exportableWikiMarkdown(title, "", lines.join("\n"));
|
||||
const isIndex = pages.some((page) => {
|
||||
const slugName = page.slug.split("/").filter(Boolean).at(-1)?.toLowerCase();
|
||||
return page.title === title && (
|
||||
page.page_type?.toLowerCase() === "index" ||
|
||||
["index", "home", "readme"].includes(slugName || "")
|
||||
);
|
||||
});
|
||||
const existingMarkdownLinkCount = (baseMarkdown.match(/(?<!!)\[[^\]\n]+\]\([^)\n]+\)/g) || []).length;
|
||||
const catalogue = isIndex && existingMarkdownLinkCount === 0
|
||||
? buildWikiIndexCatalogMarkdown(pages, knowledgeBaseId, buildPageUrl, indexGroups)
|
||||
: "";
|
||||
const markdown = resolveWikiIndexPageLinks(
|
||||
[baseMarkdown, catalogue].filter(Boolean).join("\n\n"),
|
||||
knowledgeBaseId,
|
||||
pages,
|
||||
buildPageUrl,
|
||||
);
|
||||
return wikiLinkMarkdown(
|
||||
exportableWikiMarkdown(title, "", lines.join("\n")),
|
||||
markdown,
|
||||
knowledgeBaseId,
|
||||
buildPageUrl,
|
||||
);
|
||||
|
||||
@@ -13,6 +13,34 @@ export type WikiTree = {
|
||||
parentKeysBySlug: Map<string, string[]>;
|
||||
};
|
||||
|
||||
export type BuildWikiTreeOptions = {
|
||||
preferredRootPageSlug?: string;
|
||||
};
|
||||
|
||||
export function isWikiIndexPage(page: LlmWikiWikiPage): boolean {
|
||||
const pageType = (page.page_type || "").trim().toLowerCase();
|
||||
const title = (page.title || "").trim().toLowerCase();
|
||||
const slug = (page.slug || "").trim().replace(/^\/+|\/+$/g, "");
|
||||
const slugName = slug.split("/").pop()?.toLowerCase() || "";
|
||||
return (
|
||||
pageType === "index" ||
|
||||
title === "索引" ||
|
||||
title === "index" ||
|
||||
["index", "home", "readme"].includes(slugName)
|
||||
);
|
||||
}
|
||||
|
||||
export function selectDefaultWikiPage(
|
||||
pages: LlmWikiWikiPage[],
|
||||
): LlmWikiWikiPage | undefined {
|
||||
return (
|
||||
pages.find(isWikiIndexPage) ||
|
||||
pages.find((page) => page.page_type?.toLowerCase() === "overview") ||
|
||||
pages.find((page) => !page.parent_slug && (page.depth ?? 0) === 0) ||
|
||||
pages[0]
|
||||
);
|
||||
}
|
||||
|
||||
function pageDirectoryParts(page: LlmWikiWikiPage): string[] {
|
||||
const wikiPath = (page.wiki_path || "").split("/").map((item) => item.trim()).filter(Boolean);
|
||||
if (wikiPath.length > 2) return wikiPath.slice(1, -1);
|
||||
@@ -62,20 +90,49 @@ function attachNode(parent: WikiTreeNode | null, roots: WikiTreeNode[], node: Wi
|
||||
if (!siblings.some((item) => item.key === node.key)) siblings.push(node);
|
||||
}
|
||||
|
||||
function sortWikiNodes(nodes: WikiTreeNode[]) {
|
||||
function sortWikiNodes(
|
||||
nodes: WikiTreeNode[],
|
||||
preferredRootPageSlug?: string,
|
||||
isRoot = false,
|
||||
) {
|
||||
nodes.sort((left, right) => {
|
||||
if (isRoot && preferredRootPageSlug) {
|
||||
const leftPreferred = left.page?.slug === preferredRootPageSlug;
|
||||
const rightPreferred = right.page?.slug === preferredRootPageSlug;
|
||||
if (leftPreferred !== rightPreferred) return leftPreferred ? -1 : 1;
|
||||
}
|
||||
if (!!left.children.length !== !!right.children.length) return left.children.length ? -1 : 1;
|
||||
return left.label.localeCompare(right.label, "zh-Hans-CN");
|
||||
});
|
||||
nodes.forEach((node) => sortWikiNodes(node.children));
|
||||
}
|
||||
|
||||
function detachPageNode(nodes: WikiTreeNode[], slug: string): WikiTreeNode | null {
|
||||
const index = nodes.findIndex((node) => node.page?.slug === slug);
|
||||
if (index >= 0) return nodes.splice(index, 1)[0] || null;
|
||||
for (const node of nodes) {
|
||||
const detached = detachPageNode(node.children, slug);
|
||||
if (detached) return detached;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function pruneEmptyDirectories(nodes: WikiTreeNode[]): WikiTreeNode[] {
|
||||
return nodes.filter((node) => {
|
||||
node.children = pruneEmptyDirectories(node.children);
|
||||
return node.page !== null || node.children.length > 0;
|
||||
});
|
||||
}
|
||||
|
||||
function countPages(node: WikiTreeNode): number {
|
||||
node.count = (node.page ? 1 : 0) + node.children.reduce((sum, child) => sum + countPages(child), 0);
|
||||
return node.count;
|
||||
}
|
||||
|
||||
export function buildWikiTree(pages: LlmWikiWikiPage[]): WikiTree {
|
||||
export function buildWikiTree(
|
||||
pages: LlmWikiWikiPage[],
|
||||
options: BuildWikiTreeOptions = {},
|
||||
): WikiTree {
|
||||
const roots: WikiTreeNode[] = [];
|
||||
const pageNodes = new Map<string, WikiTreeNode>();
|
||||
const directoryNodes = new Map<string, WikiTreeNode>();
|
||||
@@ -140,7 +197,17 @@ export function buildWikiTree(pages: LlmWikiWikiPage[]): WikiTree {
|
||||
parentKeysBySlug.set(page.slug, ancestorKeys);
|
||||
}
|
||||
|
||||
const preferredSlug = options.preferredRootPageSlug?.trim();
|
||||
if (preferredSlug) {
|
||||
const preferredNode = detachPageNode(roots, preferredSlug);
|
||||
if (preferredNode) {
|
||||
roots.splice(0, roots.length, ...pruneEmptyDirectories(roots));
|
||||
roots.unshift(preferredNode);
|
||||
parentKeysBySlug.set(preferredSlug, []);
|
||||
}
|
||||
}
|
||||
|
||||
roots.forEach(countPages);
|
||||
sortWikiNodes(roots);
|
||||
sortWikiNodes(roots, preferredSlug, true);
|
||||
return { roots, parentKeysBySlug };
|
||||
}
|
||||
@@ -144,6 +144,27 @@ export interface LlmWikiWikiPageList {
|
||||
total_pages: number;
|
||||
}
|
||||
|
||||
export interface LlmWikiWikiIndexItem {
|
||||
slug: string;
|
||||
title: string;
|
||||
summary: string;
|
||||
category_path: string[];
|
||||
wiki_path: string;
|
||||
depth: number;
|
||||
}
|
||||
|
||||
export interface LlmWikiWikiIndexGroup {
|
||||
type: string;
|
||||
total: number;
|
||||
items: LlmWikiWikiIndexItem[];
|
||||
}
|
||||
|
||||
export interface LlmWikiWikiIndex {
|
||||
intro: string;
|
||||
version: number;
|
||||
groups: LlmWikiWikiIndexGroup[];
|
||||
}
|
||||
|
||||
export interface LlmWikiWikiIndexStatus {
|
||||
knowledge_base_id: string;
|
||||
state: "not_synced" | "idle" | "running" | "partial" | "failed" | "rebuilding" | "rebuild_required";
|
||||
@@ -710,6 +731,17 @@ export async function getPublicLlmWikiWikiPage(
|
||||
);
|
||||
}
|
||||
|
||||
export async function getPublicLlmWikiWikiIndex(
|
||||
knowledgeBaseId: string,
|
||||
): Promise<LlmWikiWikiIndex> {
|
||||
return readJson(
|
||||
await apiFetch(
|
||||
`${PUBLIC_API()}/knowledge-bases/${encodeURIComponent(knowledgeBaseId)}/wiki/index`,
|
||||
),
|
||||
"读取公开 Wiki 索引",
|
||||
);
|
||||
}
|
||||
|
||||
export async function createLlmWikiConversationDeposit(input: {
|
||||
thread_id: string;
|
||||
question: string;
|
||||
@@ -939,6 +971,18 @@ export function usePublicLlmWikiWikiPage(
|
||||
});
|
||||
}
|
||||
|
||||
export function usePublicLlmWikiWikiIndex(
|
||||
knowledgeBaseId: string | null,
|
||||
enabled = true,
|
||||
) {
|
||||
return useQuery({
|
||||
queryKey: ["public-llmwiki", "wiki-index", knowledgeBaseId],
|
||||
queryFn: () => getPublicLlmWikiWikiIndex(knowledgeBaseId!),
|
||||
enabled: enabled && !!knowledgeBaseId,
|
||||
staleTime: 60_000,
|
||||
});
|
||||
}
|
||||
|
||||
export function useLlmWikiWikiIndexStatus(
|
||||
knowledgeBaseId: string | null,
|
||||
enabled = true,
|
||||
|
||||
-1
@@ -273,7 +273,6 @@ export function WeKnoraKnowledgeDetailPage() {
|
||||
title={`${details.data.name || "知识库"} 详情`}
|
||||
src={frameSrc}
|
||||
className="h-full min-h-0 w-full flex-1 border-0 bg-background"
|
||||
sandbox="allow-same-origin allow-scripts allow-forms allow-downloads allow-popups allow-popups-to-escape-sandbox"
|
||||
referrerPolicy="no-referrer"
|
||||
onLoad={() => postThemeToFrame(frameRef.current)}
|
||||
/>
|
||||
|
||||
+57
-3
@@ -21,12 +21,15 @@ import {
|
||||
} from "@/components/ui/sheet";
|
||||
import { apiFetch } from "@/core/api/fetch-client";
|
||||
import { getBackendBaseURL } from "@/core/config";
|
||||
import type { WeKnoraSourceTarget } from "@/core/llmwiki/source-links";
|
||||
import {
|
||||
buildWeKnoraWikiPagePath,
|
||||
parseWeKnoraPreviewUrl,
|
||||
type WeKnoraSourceTarget,
|
||||
} from "@/core/llmwiki/source-links";
|
||||
import { SpreadsheetPreview } from "@/components/workspace/artifacts/spreadsheet-preview";
|
||||
import { MarkdownContent } from "@/components/workspace/messages/markdown-content";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { reasoningPlugins } from "@/core/streamdown";
|
||||
import { buildWeKnoraWikiPagePath } from "@/core/llmwiki/source-links";
|
||||
import {
|
||||
exportableWikiMarkdown,
|
||||
renderableWikiMarkdown,
|
||||
@@ -81,6 +84,14 @@ type CachedSourceDetail = {
|
||||
|
||||
const sourceDetailCache = new Map<string, CachedSourceDetail>();
|
||||
|
||||
function buildDrawerWikiPageUrl(knowledgeBaseId: string, wikiSlug: string) {
|
||||
const params = new URLSearchParams({
|
||||
knowledge_base_id: knowledgeBaseId,
|
||||
wiki_slug: wikiSlug,
|
||||
});
|
||||
return `/weknora-source-preview?${params.toString()}`;
|
||||
}
|
||||
|
||||
async function getAvailableWikiPage(
|
||||
knowledgeBaseId: string,
|
||||
wikiSlug: string,
|
||||
@@ -539,6 +550,7 @@ export function WeKnoraSourceDrawer({
|
||||
const [downloading, setDownloading] = useState(false);
|
||||
const [wikiDownload, setWikiDownload] = useState<"md" | "word" | null>(null);
|
||||
const [resolvedDocumentId, setResolvedDocumentId] = useState("");
|
||||
const wikiNavigationRequestRef = useRef(0);
|
||||
|
||||
const targetKey = useMemo(() => {
|
||||
if (!target) return "";
|
||||
@@ -690,7 +702,12 @@ export function WeKnoraSourceDrawer({
|
||||
const summary = cleanText(wikiPage?.summary || document?.description);
|
||||
const wikiContent = stripWikiFrontmatter(cleanText(wikiPage?.content));
|
||||
const wikiDisplayContent = wikiPage && target
|
||||
? renderableWikiMarkdown(resolvedTitle, wikiContent, target.knowledgeBaseId)
|
||||
? renderableWikiMarkdown(
|
||||
resolvedTitle,
|
||||
wikiContent,
|
||||
target.knowledgeBaseId,
|
||||
buildDrawerWikiPageUrl,
|
||||
)
|
||||
: "";
|
||||
const wikiMarkdown = wikiPage
|
||||
? wikiLinkMarkdown(
|
||||
@@ -703,6 +720,32 @@ export function WeKnoraSourceDrawer({
|
||||
? buildWeKnoraWikiPagePath(target.knowledgeBaseId, resolvedWikiSlug)
|
||||
: "";
|
||||
|
||||
const openWikiPageInDrawer = async (linkedTarget: WeKnoraSourceTarget) => {
|
||||
if (!linkedTarget.wikiSlug) return;
|
||||
const requestId = ++wikiNavigationRequestRef.current;
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const page = await getAvailableWikiPage(
|
||||
linkedTarget.knowledgeBaseId,
|
||||
linkedTarget.wikiSlug,
|
||||
);
|
||||
if (requestId === wikiNavigationRequestRef.current) {
|
||||
setWikiPage(page);
|
||||
setDocument(null);
|
||||
setResolvedDocumentId("");
|
||||
setPreview({ kind: "none" });
|
||||
setPreviewError("");
|
||||
}
|
||||
} catch (error) {
|
||||
if (requestId === wikiNavigationRequestRef.current) {
|
||||
setError(error instanceof Error ? error.message : "无法打开 Wiki 页面");
|
||||
}
|
||||
} finally {
|
||||
if (requestId === wikiNavigationRequestRef.current) setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const downloadOriginal = async () => {
|
||||
if (!target || !resolvedDocumentId) return;
|
||||
setDownloading(true);
|
||||
@@ -925,6 +968,17 @@ export function WeKnoraSourceDrawer({
|
||||
isLoading={false}
|
||||
remarkPlugins={reasoningPlugins.remarkPlugins}
|
||||
rehypePlugins={reasoningPlugins.rehypePlugins}
|
||||
linkClassName={(href) =>
|
||||
parseWeKnoraPreviewUrl(href)?.wikiSlug
|
||||
? "wiki-content-link font-medium text-[#be2323] decoration-[#be2323]/55 decoration-dashed hover:decoration-solid"
|
||||
: undefined
|
||||
}
|
||||
onLinkClick={(event, href) => {
|
||||
const linkedTarget = parseWeKnoraPreviewUrl(href);
|
||||
if (!linkedTarget?.wikiSlug) return;
|
||||
event.preventDefault();
|
||||
void openWikiPageInDrawer(linkedTarget);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
+59
-13
@@ -25,6 +25,8 @@ import {
|
||||
import {
|
||||
buildWikiTree,
|
||||
filterWikiPagesWithAncestors,
|
||||
isWikiIndexPage,
|
||||
selectDefaultWikiPage,
|
||||
type WikiTreeNode,
|
||||
} from "@/core/llmwiki/wiki-tree";
|
||||
import { usePublicBackground } from "@/core/embed/background";
|
||||
@@ -43,6 +45,7 @@ import {
|
||||
useLlmWikiWikiPage,
|
||||
useLlmWikiWikiPages,
|
||||
usePublicLlmWikiKnowledgeBase,
|
||||
usePublicLlmWikiWikiIndex,
|
||||
usePublicLlmWikiWikiPage,
|
||||
usePublicLlmWikiWikiPages,
|
||||
} from "@/strategy-components/api/llmwiki";
|
||||
@@ -92,13 +95,13 @@ function downloadBlob(blob: Blob, fileName: string) {
|
||||
window.setTimeout(() => URL.revokeObjectURL(url), 30_000);
|
||||
}
|
||||
|
||||
function selectLandingPage(pages: LlmWikiWikiPage[]): LlmWikiWikiPage | undefined {
|
||||
return (
|
||||
pages.find((page) => page.page_type?.toLowerCase() === "overview") ||
|
||||
pages.find((page) => /^(?:index|home|readme)$/i.test(page.slug)) ||
|
||||
pages.find((page) => !page.parent_slug && (page.depth ?? 0) === 0) ||
|
||||
pages[0]
|
||||
);
|
||||
function publicWikiTargetFromHref(href: string, knowledgeBaseId: string) {
|
||||
const routePrefix = `/#${buildPublicKnowledgeWikiPath(knowledgeBaseId)}`;
|
||||
if (!href.startsWith(`${routePrefix}?`)) return null;
|
||||
const route = href.slice(2);
|
||||
const queryIndex = route.indexOf("?");
|
||||
const slug = new URLSearchParams(route.slice(queryIndex + 1)).get("wikiSlug")?.trim();
|
||||
return slug ? { route, slug } : null;
|
||||
}
|
||||
|
||||
export function WeKnoraWikiPageView({
|
||||
@@ -137,7 +140,11 @@ export function WeKnoraWikiPageView({
|
||||
);
|
||||
const pages = publicEmbed ? publicPages : privatePages;
|
||||
const pagesList = useMemo(() => pages.data?.pages || [], [pages.data?.pages]);
|
||||
const landingPage = useMemo(() => selectLandingPage(pagesList), [pagesList]);
|
||||
const indexPage = useMemo(
|
||||
() => (publicEmbed ? pagesList.find(isWikiIndexPage) : undefined),
|
||||
[pagesList, publicEmbed],
|
||||
);
|
||||
const landingPage = useMemo(() => selectDefaultWikiPage(pagesList), [pagesList]);
|
||||
const wikiSlug = requestedWikiSlug || (publicEmbed ? landingPage?.slug || "" : "");
|
||||
const privatePage = useLlmWikiWikiPage(
|
||||
noteId || null,
|
||||
@@ -150,6 +157,10 @@ export function WeKnoraWikiPageView({
|
||||
!!noteId && !!wikiSlug && publicEmbed,
|
||||
);
|
||||
const page = publicEmbed ? publicPage : privatePage;
|
||||
const publicWikiIndex = usePublicLlmWikiWikiIndex(
|
||||
noteId || null,
|
||||
!!noteId && publicEmbed && !!indexPage && wikiSlug === indexPage.slug,
|
||||
);
|
||||
const [downloading, setDownloading] = useState<"md" | "word" | null>(null);
|
||||
const [directoryQuery, setDirectoryQuery] = useState("");
|
||||
const [collapsedDirectories, setCollapsedDirectories] = useState<Set<string>>(() => new Set());
|
||||
@@ -159,7 +170,12 @@ export function WeKnoraWikiPageView({
|
||||
() => filterWikiPagesWithAncestors(pagesList, directoryQuery),
|
||||
[directoryQuery, pagesList],
|
||||
);
|
||||
const wikiTree = useMemo(() => buildWikiTree(filteredPages), [filteredPages]);
|
||||
const wikiTree = useMemo(
|
||||
() => buildWikiTree(filteredPages, {
|
||||
preferredRootPageSlug: publicEmbed ? indexPage?.slug : undefined,
|
||||
}),
|
||||
[filteredPages, indexPage?.slug, publicEmbed],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!publicEmbed || requestedWikiSlug || !wikiSlug) return;
|
||||
@@ -182,8 +198,16 @@ export function WeKnoraWikiPageView({
|
||||
}, [wikiSlug, wikiTree.parentKeysBySlug]);
|
||||
|
||||
const title = pageData?.title || wikiSlug || "Wiki 页面";
|
||||
const summary = cleanText(pageData?.summary);
|
||||
const content = stripWikiFrontmatter(cleanText(pageData?.content));
|
||||
const displayTitle = publicEmbed && pageData && isWikiIndexPage(pageData)
|
||||
? "索引"
|
||||
: title;
|
||||
const currentPageIsIndex = !!pageData && isWikiIndexPage(pageData);
|
||||
const summary = publicEmbed && currentPageIsIndex ? "" : cleanText(pageData?.summary);
|
||||
const content = stripWikiFrontmatter(cleanText(
|
||||
publicEmbed && currentPageIsIndex && publicWikiIndex.data?.intro
|
||||
? publicWikiIndex.data.intro
|
||||
: pageData?.content,
|
||||
));
|
||||
const displayMarkdown = pageData
|
||||
? renderableWikiMarkdown(
|
||||
title,
|
||||
@@ -196,6 +220,8 @@ export function WeKnoraWikiPageView({
|
||||
return `/#${buildPublicKnowledgeWikiPath(knowledgeBaseId)}?${next.toString()}`;
|
||||
}
|
||||
: undefined,
|
||||
publicEmbed ? pagesList : [],
|
||||
publicEmbed ? publicWikiIndex.data?.groups || [] : [],
|
||||
)
|
||||
: "";
|
||||
const exportMarkdown = pageData
|
||||
@@ -239,7 +265,11 @@ export function WeKnoraWikiPageView({
|
||||
const hasChildren = node.children.length > 0;
|
||||
const expanded = directoryQuery.trim() ? true : !collapsedDirectories.has(node.key);
|
||||
const active = node.page?.slug === wikiSlug;
|
||||
const label = node.page ? node.label : directoryLabel(node.label);
|
||||
const label = node.page
|
||||
? publicEmbed && isWikiIndexPage(node.page)
|
||||
? "索引"
|
||||
: node.label
|
||||
: directoryLabel(node.label);
|
||||
return (
|
||||
<div key={node.key}>
|
||||
<div
|
||||
@@ -372,7 +402,7 @@ export function WeKnoraWikiPageView({
|
||||
<h1 className="truncate text-sm font-semibold text-foreground">
|
||||
{knowledgeBase.data?.name || "知识库 Wiki"}
|
||||
</h1>
|
||||
<p className="mt-0.5 truncate text-xs text-muted-foreground">{title}</p>
|
||||
<p className="mt-0.5 truncate text-xs text-muted-foreground">{displayTitle}</p>
|
||||
</div>
|
||||
<Badge variant="outline" className="shrink-0 border-emerald-200 text-emerald-700 dark:border-emerald-800 dark:text-emerald-300">
|
||||
公开 Wiki
|
||||
@@ -467,6 +497,22 @@ export function WeKnoraWikiPageView({
|
||||
isLoading={false}
|
||||
remarkPlugins={reasoningPlugins.remarkPlugins}
|
||||
rehypePlugins={reasoningPlugins.rehypePlugins}
|
||||
linkClassName={(href) =>
|
||||
publicEmbed && publicWikiTargetFromHref(href, noteId)
|
||||
? "wiki-content-link border-b border-dashed !border-[var(--primary)] font-medium !text-primary !no-underline hover:border-solid focus-visible:rounded-sm focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary"
|
||||
: undefined
|
||||
}
|
||||
onLinkClick={(event, href) => {
|
||||
if (!publicEmbed) return;
|
||||
const target = publicWikiTargetFromHref(href, noteId);
|
||||
if (!target) return;
|
||||
event.preventDefault();
|
||||
navigate(target.route, {
|
||||
state: {
|
||||
wikiPage: pagesList.find((item) => item.slug === target.slug),
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</section>
|
||||
</article>
|
||||
|
||||
Reference in new issue
Block a user