Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

✨ feat: support search citations #271

Merged
merged 10 commits into from
Feb 19, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 90 additions & 0 deletions src/Markdown/demos/citations/cases.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
export const cases = {
general: {
citations: [
'https://www.weather.com.cn/weather/101210101.shtml',
'https://tianqi.moji.com/weather/china/zhejiang/hangzhou',
'https://weather.cma.cn/web/weather/58457.html',
'https://tianqi.so.com/weather/101210101',
'https://www.accuweather.com/zh/cn/hangzhou/106832/weather-forecast/106832',
'https://www.hzqx.com',
'https://www.hzqx.com/pc/hztq/',
],
markdown: `杭州今天和未来几天的天气预报如下:

今天(2月18日)多云,气温在4-12℃之间[2][3]。白天多云,夜间转阴[7]。风力较大,上午为偏东风2-3级,傍晚增大到4级[7]。

未来几天天气概况:
- 2月19日(明天):阴天,有小雨,气温11℃左右[3][7]
- 2月20日:阴天,小到中雨,气温7℃左右[3][7]
- 2月21日:多云,气温7℃左右[3]
- 2月22日:小雨,气温7℃左右[3]

总体来看,未来几天杭州天气较凉,以阴天和雨天为主,气温在7-11℃之间波动。建议市民适当增添衣物,注意保暖,外出时携带雨具[2][3][7]。
`,
},

withThinking: {
citations: [
'https://finance.sina.com.cn/stock/relnews/cn/2025-02-14/doc-inekknpr4619577.shtml',
'https://m.yicai.com/news/102477527.html',
'https://www.163.com/dy/article/JOHA38VO0517ACVG.html',
'http://www.news.cn/world/20250217/eb91333c0dc340e8875b2202cc97045e/c.html',
'https://news.china.com/socialgd/10000169/20250218/47987131.html',
'https://www.163.com/dy/article/JOOR6J520552GY5T.html',
'https://finance.sina.cn/2025-02-18/detail-inekxfhz3750396.d.html?vt=4',
'https://www.thepaper.cn/newsDetail_forward_30183356',
'https://finance.sina.com.cn/jjxw/2025-02-13/doc-iekizxu0502621.shtml',
'https://www.stcn.com/article/detail/1529574.html',
],
markdown: `<think>
好吧,用户现在想知道《哪吒2》的票房成绩。我得先看看提供的搜索结果,整理其中的数据。

首先,搜索结果里多个来源提到了不同的数字。例如,[1]中提到截至2月4日累计票房48.40亿元,但预测总票房达160.32亿元。而[2]则提到截至2月18日票房超123.19亿元,成为全球动画电影票房榜首。[6]则指出截至2月19日上午全球票房(含预售)123.78亿元,预测最终可能达160到170亿。

另外海外市场方面,[4]和[7]提到北美首周末票房721万美元,澳大利亚也有不错的表现。[3]则提到北美首日310万美元,但后续有权威数据不同可能存在矛盾需要注意。

还要注意各部分时间的差异。比如[2]和[7]的数据更新到2月18日的123.19亿,而[6]到19日则是123.78亿,用户当前的查询时间是2025年2月19日下午,所以可能需要采用最新的数据。

分账方面,根据[1],成本大约4-6亿,分账比例是片方43%,影院57%。衍生产品方面[5]提到销售额突破5000万。

需要注意的是不同来源提到的国内和国际票房可能有合并或单独计算的情况。[8]提到全球票房已突破121亿进入前9名,随后超过《头脑特工队2》,而[7]中的总票房(含海外)达123.19亿。

综上需要综合各数据点,按照最新的时间顺序整理国内、国际票房趋势及预测分账情况和衍生收入。同时要注意区分实时票房和预测数据,并正确引用来源。
</think>

《哪吒之魔童闹海》(简称《哪吒2》)自2025年1月29日上映以来持续刷新多项票房纪录。截至**2月19日**[6][7][8],其全球累计票房(含预售及海外市场)已达**123.78亿元**(约合17.07亿美元),目前位列**全球影史票房榜第8名**、**全球动画电影票房榜首**。以下是关键数据和分析:

### 一、国内票房表现
1. **影史地位**
- 截至2月18日,《哪吒2》以123.19亿元超越《狮子王》(全球影史第9名),跃升至全球影史票房第8位;同时超过《头脑特工队2》,成为全球动画电影票房冠军[2][7][8]。
- 仅用**21天**即打破由迪士尼和皮克斯长期主导的动画电影票房格局。

2. **区域分布**
- **城市划分**:一二线城市占全国票房的42%(约52亿元),三四线城市贡献了57.2%(约69亿元),下沉市场支撑显著[1]。
- **顶级城市数据**:北京、上海均超3亿元;成都、重庆超过2亿元;深圳、广州等紧随其后[1][2]。

3. **排片与上座率**
- 连续21天蝉联单日票房冠军,平均上座率32.5%,远超同期影片如《封神第二部》(16.03%)[2]。
- 万达影院排片场次日均超1万场,占比最大;横店影视、CGV影投等亦贡献显著场次[2]。

### 二、海外市场表现
1. **北美市场**
- 北美首周末(含超前点映)票房达**721万美元**(约5100万元人民币),创中国国产电影近20年最高纪录[4][7]。上映首日斩获310万美元(660家影院)[4][10]。

2. **其他地区**
- **澳大利亚**:首周末235万澳元(约1100万元人民币),超过前作《哪吒之魔童降世》总票房;单厅上座率击败《美国队长4》[7]。

3. **文化影响**
- **联合国放映活动**:2月17日在联合国总部举办特别放映,多国外交官参与观影;影片国际认可度提升[7][8]。

### 三、票房预测与分账
1. **国内预测趋势**
- **机构预测总票房160亿-170亿元**[6][9],或冲击全球影史前五(当前前五门槛约150亿元)。具体进展需观察影院长线表现(已延长至3月30日)及后续热度维系情况。

2. **分账与收益分配**
- **成本估算4-6亿元人民币**[1][9],按常规分账比例:片方获净票房的43%(约53-69亿元),影院与院线获得57%[1][9]。衍生品销售额突破5000万元人民币,贡献额外收入渠道[5]。

### 四、总结
《哪吒2》不仅以现象级表现为国产动画树立标杆,更通过本土市场与海外拓展的双轮驱动改写全球电影格局。未来能否再创新高取决于长线票房的稳定性及国际市场可持续性推广能力。`,
},
};
41 changes: 41 additions & 0 deletions src/Markdown/demos/citations/footnotes.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { Markdown } from '@/index';

const code = `
是的,我了解 ToDesktop。ToDesktop 是一个专门用于将 Web 应用转换成桌面应用的构建工具平台。以下是它的主要特点:

1. 快速转换:能在几分钟内将网页应用转换为桌面应用[^1]

2. 跨平台支持:
- 支持 Windows、Mac 和 Linux 三大主流操作系统
- 可以一次构建,生成所有平台的安装包[^5]

3. 使用方式:
- 提供了 ToDesktop Builder 桌面应用程序
- 通过步骤引导的方式帮助用户完成转换过程[^2]

4. 商业模式:
- 个人使用或测试可以免费创建和运行桌面应用
- 只有当需要为客户创建可分发的应用时才需要付费[^1]

5. 功能特性:
- 提供原生应用安装程序
- 包含许多开箱即用的高级功能
- 支持构建简单到复杂的桌面应用[^3]

ToDesktop 特别适合以下场景:
- 想要快速将现有 Web 应用转换为桌面应用的开发者
- 需要跨平台桌面应用分发的团队
- 希望节省开发时间和精力的项目[^3]

这是一个非常实用的工具,特别是对于那些已经有了 Web 应用,但想要提供原生桌面应用体验的开发者来说。它简化了将 Web 应用转换为桌面应用的过程,使得开发者可以专注于核心业务功能的开发。

[^1]: [ToDesktop 官网](https://www.todesktop.com/)
[^2]: [ToDesktop 基础介绍](https://www.todesktop.com/docs/introduction/basics)
[^3]: https://prod.todesktop.com/
[^4]: https://www.todesktop.com/docs/introduction/ui-concepts
[^5]: https://www.todesktop.com/features/app-installers
`;

export default () => {
return <Markdown>{code}</Markdown>;
};
50 changes: 50 additions & 0 deletions src/Markdown/demos/citations/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { Markdown, SearchResultCards } from '@lobehub/ui';
import { Divider, Radio } from 'antd';
import { useState } from 'react';
import { Flexbox } from 'react-layout-kit';

import { normalizeThinkTags, remarkCaptureThink } from '../thinking/remarkPlugin';
import { cases } from './cases';

export default () => {
const [current, setCurrent] = useState('general');

// @ts-ignore
const state = cases[current];

return (
<>
<Radio.Group
onChange={(e) => {
setCurrent(e.target.value);
}}
options={[
{ label: '普通case', value: 'general' },
{ label: '带 thinking', value: 'withThinking' },
]}
value={current}
/>
<Divider />
<div className={'citations'} style={{ marginBottom: 12 }}>
<SearchResultCards
dataSource={state.citations.map((item: string) => ({ title: item, url: item }))}
/>
</div>
<Markdown
citations={state.citations}
components={{
think: (props: any) => (
<Flexbox style={{ marginBottom: 20 }}>
<Markdown {...props} citations={state.citations} />
<Divider />
</Flexbox>
),
}}
remarkPlugins={[remarkCaptureThink]}
variant={'chat'}
>
{normalizeThinkTags(state.markdown)}
</Markdown>
</>
);
};
8 changes: 6 additions & 2 deletions src/Markdown/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,15 @@ description: Markdown is a React component used to render markdown text. It supp

## Custom Highlight

<code src="./demos/customHighlight.tsx" nopadding></code>
<code src="./demos/customHighlight.tsx"></code>

## Citations

<code src="./demos/citations/index.tsx"></code> <code src="./demos/citations/footnotes.tsx"></code>

## Think

<code src="./demos/thinking/index.tsx" nopadding></code>
<code src="./demos/thinking/index.tsx"></code>

## Custom Plugins

Expand Down
23 changes: 19 additions & 4 deletions src/Markdown/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,19 +17,23 @@ import { type MermaidProps } from '@/Mermaid';
import Image, { type ImageProps } from '@/mdx/mdxComponents/Image';
import Link from '@/mdx/mdxComponents/Link';
import { type PreProps } from '@/mdx/mdxComponents/Pre';
import Section from '@/mdx/mdxComponents/Section';
import Video, { type VideoProps } from '@/mdx/mdxComponents/Video';
import type { AProps } from '@/types';
import { CitationItem } from '@/types/citation';

import { CodeFullFeatured, CodeLite } from './CodeBlock';
import type { TypographyProps } from './Typography';
import { useStyles as useMarkdownStyles } from './markdown.style';
import { rehypeKatexDir } from './rehypePlugin';
import { rehypeFootnoteLinks, remarkCustomFootnotes } from './plugins/footnote';
import { rehypeKatexDir } from './plugins/katexDir';
import { useStyles } from './style';
import { escapeBrackets, escapeMhchem, fixMarkdownBold } from './utils';
import { escapeBrackets, escapeMhchem, fixMarkdownBold, transformCitations } from './utils';

export interface MarkdownProps extends TypographyProps {
allowHtml?: boolean;
children: string;
citations?: CitationItem[];
className?: string;
componentProps?: {
a?: Partial<AProps & AnchorProps>;
Expand All @@ -49,6 +53,7 @@ export interface MarkdownProps extends TypographyProps {
rehypePlugins?: Pluggable[];
remarkPlugins?: Pluggable[];
remarkPluginsAhead?: Pluggable[];
showFootnotes?: boolean;
style?: CSSProperties;
variant?: 'normal' | 'chat';
}
Expand All @@ -68,13 +73,15 @@ const Markdown = memo<MarkdownProps>(
fontSize,
headerMultiple,
marginMultiple,
showFootnotes,
variant = 'normal',
lineHeight,
rehypePlugins,
remarkPlugins,
remarkPluginsAhead,
components = {},
customRender,
citations,
...rest
}) => {
const { cx, styles } = useStyles({
Expand All @@ -89,12 +96,15 @@ const Markdown = memo<MarkdownProps>(

const escapedContent = useMemo(() => {
if (!enableLatex) return fixMarkdownBold(children);
return fixMarkdownBold(escapeMhchem(escapeBrackets(children)));
return transformCitations(
fixMarkdownBold(escapeMhchem(escapeBrackets(children))),
citations?.length,
);
}, [children, enableLatex]);

const memoComponents: Components = useMemo(
() => ({
a: (props: any) => <Link {...props} {...componentProps?.a} />,
a: (props: any) => <Link citations={citations} {...props} {...componentProps?.a} />,
img: enableImageGallery
? (props: any) => (
<Image
Expand Down Expand Up @@ -126,6 +136,7 @@ const Markdown = memo<MarkdownProps>(
{...componentProps?.pre}
/>
),
section: (props: any) => <Section showCitations={showFootnotes} {...props} />,
video: (props: any) => <Video {...props} {...componentProps?.video} />,
...components,
}),
Expand All @@ -135,6 +146,8 @@ const Markdown = memo<MarkdownProps>(
enableImageGallery,
enableMermaid,
fullFeaturedCodeBlock,
...(citations || []),
showFootnotes,
],
) as Components;

Expand All @@ -146,6 +159,7 @@ const Markdown = memo<MarkdownProps>(
allowHtml && rehypeRaw,
enableLatex && rehypeKatex,
enableLatex && rehypeKatexDir,
rehypeFootnoteLinks,
...innerRehypePlugins,
].filter(Boolean) as any,
[allowHtml, enableLatex, ...innerRehypePlugins],
Expand All @@ -161,6 +175,7 @@ const Markdown = memo<MarkdownProps>(
[
...innerRemarkPluginsAhead,
remarkGfm,
remarkCustomFootnotes,
enableLatex && remarkMath,
isChatMode && remarkBreaks,
...innerRemarkPlugins,
Expand Down
73 changes: 73 additions & 0 deletions src/Markdown/plugins/footnote.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { Node } from 'unist';
import { SKIP, visit } from 'unist-util-visit';

interface FootnoteLink {
alt?: string;
title?: string;
url: string;
}

// eslint-disable-next-line unicorn/consistent-function-scoping
export const remarkCustomFootnotes = () => (tree: any, file: any) => {
const footnoteLinks = new Map();

visit(tree, 'footnoteDefinition', (node) => {
let linkData: FootnoteLink | null = null;

// 查找第一个link类型的子节点
visit(node, 'link', (linkNode) => {
if (linkData) return SKIP; // 只取第一个链接

// 提取链接文本
const textNode = linkNode.children.find((n: Node) => n.type === 'text');

linkData = {
alt: textNode?.value || '',
title: textNode?.value || '',
url: linkNode.url, // 或者根据需求处理
};

return SKIP; // 找到后停止遍历
});

if (linkData) {
footnoteLinks.set(node.identifier, linkData);
}
});

// 将数据存入文件上下文
file.data.footnoteLinks = Object.fromEntries(footnoteLinks);
};

// eslint-disable-next-line unicorn/consistent-function-scoping
export const rehypeFootnoteLinks = () => (tree: any, file: any) => {
const linksData: Record<string, FootnoteLink> = file.data.footnoteLinks || {};

visit(tree, 'element', (node) => {
if (node.tagName === 'section' && node.properties.className?.includes('footnotes')) {
// 转换数据格式为数组(按identifier排序)
const sortedLinks = Object.entries(linksData)
.sort(([a], [b]) => a.localeCompare(b))
.map(([id, data]) => ({ id, ...data }));
// 注入数据属性
node.properties['data-footnote-links'] = JSON.stringify(sortedLinks);
}

if (node.tagName === 'sup') {
const link = node.children.find((n: any) => n.tagName === 'a');

if (link) {
// a node example
// {
// "href": "#user-content-fn-3",
// "id": "user-content-fnref-3-2",
// "dataFootnoteRef": true,
// }
const linkRefIndex = link.properties?.id?.replace(/^user-content-fnref-/, '')[0];

if (linkRefIndex !== undefined)
link.properties['data-link'] = JSON.stringify(linksData[linkRefIndex]);
}
}
});
};
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
// katex-directive
// 给 class="katex" 的节点加上 dir="ltr" 属性
import type { Node } from 'unist';
import { visit } from 'unist-util-visit';

// katex-directive
// 给 class="katex" 的节点加上 dir="ltr" 属性
// eslint-disable-next-line unicorn/consistent-function-scoping
export const rehypeKatexDir = () => (tree: Node) => {
visit(tree, 'element', (node: any) => {
Expand Down
Loading