Merge pull request #2145 from danielaskdd/footnode
Feature: Add footnotes support to markdown rendering in chat messages
This commit is contained in:
commit
2645ad5587
12 changed files with 149 additions and 25 deletions
File diff suppressed because one or more lines are too long
12
lightrag/api/webui/assets/feature-retrieval-BCjqAd-A.js
generated
Normal file
12
lightrag/api/webui/assets/feature-retrieval-BCjqAd-A.js
generated
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -1,4 +1,4 @@
|
|||
import{j as o,Y as ld,O as fg,k as dg,u as ad,Z as mg,c as hg,l as gg,g as pg,S as yg,T as vg,n as bg,m as nd,o as Sg,p as Tg,$ as ud,a0 as id,a1 as cd,a2 as xg}from"./ui-vendor-CeCm8EER.js";import{d as Ag,h as Dg,r as E,u as sd,H as Ng,i as Eg,j as kf}from"./react-vendor-DEwriMA6.js";import{N as we,c as Ve,ae as od,u as qt,M as st,af as rd,ag as fd,I as us,B as Cn,D as Mg,l as zg,m as Cg,n as Og,o as _g,ah as jg,ai as Rg,aj as Ug,ak as Lg,al as Bt,am as dd,an as ss,ao as is,a1 as Hg,a2 as Bg,a3 as qg,a4 as Gg,ap as Yg,aq as Xg,ar as md,as as wg,at as hd,au as Vg,av as gd,d as Qg,R as Kg,V as Zg,g as En,aw as kg,ax as Jg,ay as Fg}from"./feature-graph-CGlNNV88.js";import{S as Jf,a as Ff,b as Pf,c as $f,e as rl,D as Pg}from"./feature-documents-BOF9chpi.js";import{R as $g}from"./feature-retrieval-B0uK1e0s.js";import{i as cs}from"./utils-vendor-BysuhMZA.js";import"./graph-vendor-B-X5JegA.js";import"./mermaid-vendor-DEhmckNu.js";import"./markdown-vendor-a_AXlAX2.js";(function(){const y=document.createElement("link").relList;if(y&&y.supports&&y.supports("modulepreload"))return;for(const N of document.querySelectorAll('link[rel="modulepreload"]'))d(N);new MutationObserver(N=>{for(const _ of N)if(_.type==="childList")for(const L of _.addedNodes)L.tagName==="LINK"&&L.rel==="modulepreload"&&d(L)}).observe(document,{childList:!0,subtree:!0});function x(N){const _={};return N.integrity&&(_.integrity=N.integrity),N.referrerPolicy&&(_.referrerPolicy=N.referrerPolicy),N.crossOrigin==="use-credentials"?_.credentials="include":N.crossOrigin==="anonymous"?_.credentials="omit":_.credentials="same-origin",_}function d(N){if(N.ep)return;N.ep=!0;const _=x(N);fetch(N.href,_)}})();var ls={exports:{}},Mn={},as={exports:{}},ns={};/**
|
||||
import{j as o,Y as ld,O as fg,k as dg,u as ad,Z as mg,c as hg,l as gg,g as pg,S as yg,T as vg,n as bg,m as nd,o as Sg,p as Tg,$ as ud,a0 as id,a1 as cd,a2 as xg}from"./ui-vendor-CeCm8EER.js";import{d as Ag,h as Dg,r as E,u as sd,H as Ng,i as Eg,j as kf}from"./react-vendor-DEwriMA6.js";import{N as we,c as Ve,ae as od,u as qt,M as st,af as rd,ag as fd,I as us,B as Cn,D as Mg,l as zg,m as Cg,n as Og,o as _g,ah as jg,ai as Rg,aj as Ug,ak as Lg,al as Bt,am as dd,an as ss,ao as is,a1 as Hg,a2 as Bg,a3 as qg,a4 as Gg,ap as Yg,aq as Xg,ar as md,as as wg,at as hd,au as Vg,av as gd,d as Qg,R as Kg,V as Zg,g as En,aw as kg,ax as Jg,ay as Fg}from"./feature-graph-CGlNNV88.js";import{S as Jf,a as Ff,b as Pf,c as $f,e as rl,D as Pg}from"./feature-documents-BOF9chpi.js";import{R as $g}from"./feature-retrieval-BCjqAd-A.js";import{i as cs}from"./utils-vendor-BysuhMZA.js";import"./graph-vendor-B-X5JegA.js";import"./mermaid-vendor-DEhmckNu.js";import"./markdown-vendor-Dv0NSOeH.js";(function(){const y=document.createElement("link").relList;if(y&&y.supports&&y.supports("modulepreload"))return;for(const N of document.querySelectorAll('link[rel="modulepreload"]'))d(N);new MutationObserver(N=>{for(const _ of N)if(_.type==="childList")for(const L of _.addedNodes)L.tagName==="LINK"&&L.rel==="modulepreload"&&d(L)}).observe(document,{childList:!0,subtree:!0});function x(N){const _={};return N.integrity&&(_.integrity=N.integrity),N.referrerPolicy&&(_.referrerPolicy=N.referrerPolicy),N.crossOrigin==="use-credentials"?_.credentials="include":N.crossOrigin==="anonymous"?_.credentials="omit":_.credentials="same-origin",_}function d(N){if(N.ep)return;N.ep=!0;const _=x(N);fetch(N.href,_)}})();var ls={exports:{}},Mn={},as={exports:{}},ns={};/**
|
||||
* @license React
|
||||
* scheduler.production.js
|
||||
*
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
8
lightrag/api/webui/index.html
generated
8
lightrag/api/webui/index.html
generated
|
|
@ -8,7 +8,7 @@
|
|||
<link rel="icon" type="image/png" href="favicon.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Lightrag</title>
|
||||
<script type="module" crossorigin src="/webui/assets/index-C5wxCqtI.js"></script>
|
||||
<script type="module" crossorigin src="/webui/assets/index-WVaad7ie.js"></script>
|
||||
<link rel="modulepreload" crossorigin href="/webui/assets/react-vendor-DEwriMA6.js">
|
||||
<link rel="modulepreload" crossorigin href="/webui/assets/ui-vendor-CeCm8EER.js">
|
||||
<link rel="modulepreload" crossorigin href="/webui/assets/graph-vendor-B-X5JegA.js">
|
||||
|
|
@ -16,10 +16,10 @@
|
|||
<link rel="modulepreload" crossorigin href="/webui/assets/feature-graph-CGlNNV88.js">
|
||||
<link rel="modulepreload" crossorigin href="/webui/assets/feature-documents-BOF9chpi.js">
|
||||
<link rel="modulepreload" crossorigin href="/webui/assets/mermaid-vendor-DEhmckNu.js">
|
||||
<link rel="modulepreload" crossorigin href="/webui/assets/markdown-vendor-a_AXlAX2.js">
|
||||
<link rel="modulepreload" crossorigin href="/webui/assets/feature-retrieval-B0uK1e0s.js">
|
||||
<link rel="modulepreload" crossorigin href="/webui/assets/markdown-vendor-Dv0NSOeH.js">
|
||||
<link rel="modulepreload" crossorigin href="/webui/assets/feature-retrieval-BCjqAd-A.js">
|
||||
<link rel="stylesheet" crossorigin href="/webui/assets/feature-graph-BipNuM18.css">
|
||||
<link rel="stylesheet" crossorigin href="/webui/assets/index-BDkGEMA8.css">
|
||||
<link rel="stylesheet" crossorigin href="/webui/assets/index-XaGZCpwe.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
|
|
|||
|
|
@ -59,6 +59,7 @@
|
|||
"tailwind-merge": "^3.0.2",
|
||||
"tailwind-scrollbar": "^4.0.1",
|
||||
"typography": "^0.16.24",
|
||||
"unist-util-visit": "^5.0.0",
|
||||
"zustand": "^5.0.3",
|
||||
},
|
||||
"devDependencies": {
|
||||
|
|
|
|||
|
|
@ -68,6 +68,7 @@
|
|||
"tailwind-merge": "^3.0.2",
|
||||
"tailwind-scrollbar": "^4.0.1",
|
||||
"typography": "^0.16.24",
|
||||
"unist-util-visit": "^5.0.0",
|
||||
"zustand": "^5.0.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import rehypeReact from 'rehype-react'
|
|||
import rehypeRaw from 'rehype-raw'
|
||||
import remarkMath from 'remark-math'
|
||||
import mermaid from 'mermaid'
|
||||
import { remarkFootnotes } from '@/utils/remarkFootnotes'
|
||||
|
||||
|
||||
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'
|
||||
|
|
@ -127,14 +128,14 @@ export const ChatMessage = ({ message }: { message: MessageWithError }) => { //
|
|||
</div>
|
||||
{/* Show thinking content when expanded and content exists, even during thinking process */}
|
||||
{isThinkingExpanded && finalThinkingContent && finalThinkingContent.trim() !== '' && (
|
||||
<div className="mt-2 pl-4 border-l-2 border-primary/20 text-sm prose dark:prose-invert max-w-none break-words prose-p:my-1 prose-headings:my-2 [&_sup]:text-[0.75em] [&_sup]:align-[0.1em] [&_sup]:leading-[0] [&_sub]:text-[0.75em] [&_sub]:align-[-0.2em] [&_sub]:leading-[0] [&_mark]:bg-yellow-200 [&_mark]:dark:bg-yellow-800 [&_u]:underline [&_del]:line-through [&_ins]:underline [&_ins]:decoration-green-500">
|
||||
<div className="mt-2 pl-4 border-l-2 border-primary/20 text-sm prose dark:prose-invert max-w-none break-words prose-p:my-1 prose-headings:my-2 [&_sup]:text-[0.75em] [&_sup]:align-[0.1em] [&_sup]:leading-[0] [&_sub]:text-[0.75em] [&_sub]:align-[-0.2em] [&_sub]:leading-[0] [&_mark]:bg-yellow-200 [&_mark]:dark:bg-yellow-800 [&_u]:underline [&_del]:line-through [&_ins]:underline [&_ins]:decoration-green-500 [&_.footnotes]:mt-6 [&_.footnotes]:pt-3 [&_.footnotes]:border-t [&_.footnotes]:border-border [&_.footnotes_ol]:text-xs [&_.footnotes_li]:my-0.5 [&_a[href^='#fn']]:text-primary [&_a[href^='#fn']]:no-underline [&_a[href^='#fn']]:hover:underline [&_a[href^='#fnref']]:text-primary [&_a[href^='#fnref']]:no-underline [&_a[href^='#fnref']]:hover:underline">
|
||||
{isThinking && (
|
||||
<div className="mb-2 text-xs text-gray-400 dark:text-gray-500 italic">
|
||||
{t('retrievePanel.chatMessage.thinkingInProgress', 'Thinking in progress...')}
|
||||
</div>
|
||||
)}
|
||||
<ReactMarkdown
|
||||
remarkPlugins={[remarkGfm, remarkMath]}
|
||||
remarkPlugins={[remarkGfm, remarkFootnotes, remarkMath]}
|
||||
rehypePlugins={[
|
||||
rehypeRaw,
|
||||
...(katexPlugin ? [[katexPlugin, { errorColor: theme === 'dark' ? '#ef4444' : '#dc2626', throwOnError: false, displayMode: false }] as any] : []),
|
||||
|
|
@ -153,8 +154,12 @@ export const ChatMessage = ({ message }: { message: MessageWithError }) => { //
|
|||
{finalDisplayContent && (
|
||||
<div className="relative">
|
||||
<ReactMarkdown
|
||||
className="prose dark:prose-invert max-w-none text-sm break-words prose-headings:mt-4 prose-headings:mb-2 prose-p:my-2 prose-ul:my-2 prose-ol:my-2 prose-li:my-1 [&_.katex]:text-current [&_.katex-display]:my-4 [&_.katex-display]:overflow-x-auto [&_sup]:text-[0.75em] [&_sup]:align-[0.1em] [&_sup]:leading-[0] [&_sub]:text-[0.75em] [&_sub]:align-[-0.2em] [&_sub]:leading-[0] [&_mark]:bg-yellow-200 [&_mark]:dark:bg-yellow-800 [&_u]:underline [&_del]:line-through [&_ins]:underline [&_ins]:decoration-green-500"
|
||||
remarkPlugins={[remarkGfm, remarkMath]}
|
||||
className={`prose dark:prose-invert max-w-none text-sm break-words prose-headings:mt-4 prose-headings:mb-2 prose-p:my-2 prose-ul:my-2 prose-ol:my-2 prose-li:my-1 [&_.katex]:text-current [&_.katex-display]:my-4 [&_.katex-display]:overflow-x-auto [&_sup]:text-[0.75em] [&_sup]:align-[0.1em] [&_sup]:leading-[0] [&_sub]:text-[0.75em] [&_sub]:align-[-0.2em] [&_sub]:leading-[0] [&_mark]:bg-yellow-200 [&_mark]:dark:bg-yellow-800 [&_u]:underline [&_del]:line-through [&_ins]:underline [&_ins]:decoration-green-500 [&_.footnotes]:mt-8 [&_.footnotes]:pt-4 [&_.footnotes]:border-t [&_.footnotes_ol]:text-sm [&_.footnotes_li]:my-1 ${
|
||||
message.role === 'user'
|
||||
? '[&_.footnotes]:border-primary-foreground/30 [&_a[href^="#fn"]]:text-primary-foreground [&_a[href^="#fn"]]:no-underline [&_a[href^="#fn"]]:hover:underline [&_a[href^="#fnref"]]:text-primary-foreground [&_a[href^="#fnref"]]:no-underline [&_a[href^="#fnref"]]:hover:underline'
|
||||
: '[&_.footnotes]:border-border [&_a[href^="#fn"]]:text-primary [&_a[href^="#fn"]]:no-underline [&_a[href^="#fn"]]:hover:underline [&_a[href^="#fnref"]]:text-primary [&_a[href^="#fnref"]]:no-underline [&_a[href^="#fnref"]]:hover:underline'
|
||||
}`}
|
||||
remarkPlugins={[remarkGfm, remarkFootnotes, remarkMath]}
|
||||
rehypePlugins={[
|
||||
rehypeRaw,
|
||||
...(katexPlugin ? [[
|
||||
|
|
|
|||
116
lightrag_webui/src/utils/remarkFootnotes.ts
Normal file
116
lightrag_webui/src/utils/remarkFootnotes.ts
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
import { visit } from 'unist-util-visit'
|
||||
import type { Plugin } from 'unified'
|
||||
import type { Root, Text, Paragraph, Html } from 'mdast'
|
||||
|
||||
// Simple footnote plugin for remark
|
||||
export const remarkFootnotes: Plugin<[], Root> = () => {
|
||||
return (tree: Root) => {
|
||||
const footnoteDefinitions = new Map<string, string>()
|
||||
|
||||
// First pass: collect footnote definitions and remove them
|
||||
const nodesToRemove: Array<{ parent: any; index: number }> = []
|
||||
|
||||
visit(tree, 'paragraph', (node: Paragraph, index, parent) => {
|
||||
if (!parent || typeof index !== 'number') return
|
||||
|
||||
// Check if this paragraph contains only a footnote definition
|
||||
if (node.children.length === 1 && node.children[0].type === 'text') {
|
||||
const text = (node.children[0] as Text).value
|
||||
const match = text.match(/^\[\^([^\]]+)\]:\s*(.+)$/)
|
||||
if (match) {
|
||||
const [, id, content] = match
|
||||
footnoteDefinitions.set(id, content.trim())
|
||||
nodesToRemove.push({ parent, index })
|
||||
return
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Remove footnote definition paragraphs
|
||||
nodesToRemove.reverse().forEach(({ parent, index }) => {
|
||||
parent.children.splice(index, 1)
|
||||
})
|
||||
|
||||
// Second pass: find footnote references and replace them
|
||||
visit(tree, 'text', (node: Text, index, parent) => {
|
||||
if (!parent || typeof index !== 'number') return
|
||||
|
||||
const text = node.value
|
||||
const footnoteRegex = /\[\^([^\]]+)\]/g
|
||||
let match
|
||||
const replacements: any[] = []
|
||||
let lastIndex = 0
|
||||
|
||||
while ((match = footnoteRegex.exec(text)) !== null) {
|
||||
const [fullMatch, id] = match
|
||||
const startIndex = match.index!
|
||||
|
||||
// Add text before footnote
|
||||
if (startIndex > lastIndex) {
|
||||
replacements.push({
|
||||
type: 'text',
|
||||
value: text.slice(lastIndex, startIndex)
|
||||
})
|
||||
}
|
||||
|
||||
// Add footnote reference as HTML
|
||||
replacements.push({
|
||||
type: 'html',
|
||||
value: `<sup><a href="#fn-${id}" id="fnref-${id}" class="footnote-ref">${id}</a></sup>`
|
||||
})
|
||||
|
||||
lastIndex = startIndex + fullMatch.length
|
||||
}
|
||||
|
||||
// Add remaining text
|
||||
if (lastIndex < text.length) {
|
||||
replacements.push({
|
||||
type: 'text',
|
||||
value: text.slice(lastIndex)
|
||||
})
|
||||
}
|
||||
|
||||
// Replace the text node if we found footnotes
|
||||
if (replacements.length > 1) {
|
||||
parent.children.splice(index, 1, ...replacements)
|
||||
}
|
||||
})
|
||||
|
||||
// Third pass: add footnotes section at the end if we have definitions
|
||||
if (footnoteDefinitions.size > 0) {
|
||||
const footnotesList: any[] = []
|
||||
|
||||
footnoteDefinitions.forEach((content, id) => {
|
||||
footnotesList.push({
|
||||
type: 'listItem',
|
||||
children: [{
|
||||
type: 'paragraph',
|
||||
children: [{
|
||||
type: 'html',
|
||||
value: `<span id="fn-${id}">${content} <a href="#fnref-${id}" class="footnote-backref">↩</a></span>`
|
||||
}]
|
||||
}]
|
||||
})
|
||||
})
|
||||
|
||||
// Add footnotes section
|
||||
tree.children.push({
|
||||
type: 'html',
|
||||
value: '<div class="footnotes">'
|
||||
} as Html)
|
||||
|
||||
tree.children.push({
|
||||
type: 'list',
|
||||
ordered: true,
|
||||
start: 1,
|
||||
spread: false,
|
||||
children: footnotesList
|
||||
})
|
||||
|
||||
tree.children.push({
|
||||
type: 'html',
|
||||
value: '</div>'
|
||||
} as Html)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -47,7 +47,8 @@ export default defineConfig({
|
|||
'rehype-raw',
|
||||
'remark-gfm',
|
||||
'remark-math',
|
||||
'react-syntax-highlighter'
|
||||
'react-syntax-highlighter',
|
||||
'unist-util-visit'
|
||||
]
|
||||
},
|
||||
// Ensure consistent chunk naming format
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue