mirror of
https://github.com/lobehub/lobehub.git
synced 2026-03-26 13:19:34 +07:00
✨ feat(notebook): add i18n, Inspector and Streaming components (#11212)
* ✨ feat(notebook): add i18n, Inspector and Streaming components - Add i18n entries for notebook tool in plugin.ts - Add zh-CN and en-US translations - Add CreateDocument Inspector component for streaming status display - Add CreateDocument Streaming component for real-time markdown preview - Add AnimatedNumber helper component - Export NotebookInspectors and NotebookStreamings from client 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * 🐛 fix(notebook): simplify Inspector to show title directly Follow WebSearch Inspector pattern - use direct string concatenation instead of Trans component 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * 🐛 fix(notebook): add isLoading state for shiny animation Match WebSearch Inspector pattern - show shiny animation during both streaming and loading states 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * refactor * improve document --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -92,6 +92,15 @@
|
||||
"builtins.lobe-local-system.inspector.noResults": "No results",
|
||||
"builtins.lobe-local-system.inspector.rename.result": "<old>{{oldName}}</old> → <new>{{newName}}</new>",
|
||||
"builtins.lobe-local-system.title": "Local System",
|
||||
"builtins.lobe-notebook.actions.copy": "Copy",
|
||||
"builtins.lobe-notebook.actions.creating": "Creating document...",
|
||||
"builtins.lobe-notebook.actions.edit": "Edit",
|
||||
"builtins.lobe-notebook.actions.expand": "Expand",
|
||||
"builtins.lobe-notebook.apiName.createDocument": "Create document",
|
||||
"builtins.lobe-notebook.apiName.deleteDocument": "Delete document",
|
||||
"builtins.lobe-notebook.apiName.getDocument": "Get document",
|
||||
"builtins.lobe-notebook.apiName.updateDocument": "Update document",
|
||||
"builtins.lobe-notebook.title": "Notebook",
|
||||
"builtins.lobe-page-agent.apiName.batchUpdate": "Batch update nodes",
|
||||
"builtins.lobe-page-agent.apiName.compareSnapshots": "Compare snapshots",
|
||||
"builtins.lobe-page-agent.apiName.convertToList": "Convert to list",
|
||||
|
||||
@@ -92,6 +92,15 @@
|
||||
"builtins.lobe-local-system.inspector.noResults": "无结果",
|
||||
"builtins.lobe-local-system.inspector.rename.result": "<old>{{oldName}}</old> → <new>{{newName}}</new>",
|
||||
"builtins.lobe-local-system.title": "本地系统",
|
||||
"builtins.lobe-notebook.actions.copy": "复制",
|
||||
"builtins.lobe-notebook.actions.creating": "文档创建中...",
|
||||
"builtins.lobe-notebook.actions.edit": "编辑",
|
||||
"builtins.lobe-notebook.actions.expand": "展开",
|
||||
"builtins.lobe-notebook.apiName.createDocument": "创建文档",
|
||||
"builtins.lobe-notebook.apiName.deleteDocument": "删除文档",
|
||||
"builtins.lobe-notebook.apiName.getDocument": "获取文档",
|
||||
"builtins.lobe-notebook.apiName.updateDocument": "更新文档",
|
||||
"builtins.lobe-notebook.title": "笔记本",
|
||||
"builtins.lobe-page-agent.apiName.batchUpdate": "批量更新节点",
|
||||
"builtins.lobe-page-agent.apiName.compareSnapshots": "比较快照",
|
||||
"builtins.lobe-page-agent.apiName.convertToList": "转换为列表",
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
'use client';
|
||||
|
||||
import type { BuiltinStreamingProps } from '@lobechat/types';
|
||||
import { Flexbox, Icon, Markdown, Text } from '@lobehub/ui';
|
||||
import { Flexbox, Icon, Text } from '@lobehub/ui';
|
||||
import { createStaticStyles } from 'antd-style';
|
||||
import { ListChecksIcon } from 'lucide-react';
|
||||
import { memo } from 'react';
|
||||
|
||||
import type { CreatePlanParams } from '../../../types';
|
||||
import StreamingMarkdown from '@/components/StreamingMarkdown';
|
||||
|
||||
const MAX_CONTENT_HEIGHT = 100;
|
||||
import type { CreatePlanParams } from '../../../types';
|
||||
|
||||
const styles = createStaticStyles(({ css, cssVar }) => ({
|
||||
container: css`
|
||||
@@ -17,15 +17,6 @@ const styles = createStaticStyles(({ css, cssVar }) => ({
|
||||
border: 1px solid ${cssVar.colorBorder};
|
||||
border-radius: 8px;
|
||||
`,
|
||||
content: css`
|
||||
overflow: hidden auto;
|
||||
|
||||
max-height: ${MAX_CONTENT_HEIGHT}px;
|
||||
padding: 12px;
|
||||
border-radius: 8px;
|
||||
|
||||
background: ${cssVar.colorFillQuaternary};
|
||||
`,
|
||||
description: css`
|
||||
font-size: 14px;
|
||||
color: ${cssVar.colorTextSecondary};
|
||||
@@ -70,13 +61,7 @@ export const CreatePlanStreaming = memo<BuiltinStreamingProps<CreatePlanParams>>
|
||||
)}
|
||||
|
||||
{/* Context content - streaming with animation */}
|
||||
{context && (
|
||||
<div className={styles.content}>
|
||||
<Markdown animated fontSize={13} variant={'chat'}>
|
||||
{context}
|
||||
</Markdown>
|
||||
</div>
|
||||
)}
|
||||
<StreamingMarkdown maxHeight={100}>{context}</StreamingMarkdown>
|
||||
</Flexbox>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
'use client';
|
||||
|
||||
import type { BuiltinInspectorProps } from '@lobechat/types';
|
||||
import { createStaticStyles, cx } from 'antd-style';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { highlightTextStyles, shinyTextStyles } from '@/styles';
|
||||
|
||||
import type { CreateDocumentArgs, CreateDocumentState } from '../../../types';
|
||||
|
||||
const styles = createStaticStyles(({ css, cssVar }) => ({
|
||||
root: css`
|
||||
overflow: hidden;
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 1;
|
||||
|
||||
color: ${cssVar.colorTextSecondary};
|
||||
`,
|
||||
}));
|
||||
|
||||
export const CreateDocumentInspector = memo<
|
||||
BuiltinInspectorProps<CreateDocumentArgs, CreateDocumentState>
|
||||
>(({ args, partialArgs, isArgumentsStreaming, isLoading }) => {
|
||||
const { t } = useTranslation('plugin');
|
||||
|
||||
const title = args?.title || partialArgs?.title;
|
||||
|
||||
// During streaming without title, show init
|
||||
if (isArgumentsStreaming && !title) {
|
||||
return (
|
||||
<div className={cx(styles.root, shinyTextStyles.shinyText)}>
|
||||
<span>{t('builtins.lobe-notebook.apiName.createDocument')}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cx(styles.root, (isArgumentsStreaming || isLoading) && shinyTextStyles.shinyText)}
|
||||
>
|
||||
<span>{t('builtins.lobe-notebook.apiName.createDocument')}: </span>
|
||||
{title && <span className={highlightTextStyles.primary}>{title}</span>}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
CreateDocumentInspector.displayName = 'CreateDocumentInspector';
|
||||
|
||||
export default CreateDocumentInspector;
|
||||
14
packages/builtin-tool-notebook/src/client/Inspector/index.ts
Normal file
14
packages/builtin-tool-notebook/src/client/Inspector/index.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { type BuiltinInspector } from '@lobechat/types';
|
||||
|
||||
import { NotebookApiName } from '../../types';
|
||||
import { CreateDocumentInspector } from './CreateDocument';
|
||||
|
||||
/**
|
||||
* Notebook Inspector Components Registry
|
||||
*
|
||||
* Inspector components customize the title/header area
|
||||
* of tool calls in the conversation UI.
|
||||
*/
|
||||
export const NotebookInspectors: Record<string, BuiltinInspector> = {
|
||||
[NotebookApiName.createDocument]: CreateDocumentInspector as BuiltinInspector,
|
||||
};
|
||||
@@ -0,0 +1,101 @@
|
||||
'use client';
|
||||
|
||||
import type { BuiltinPlaceholderProps } from '@lobechat/types';
|
||||
import { Flexbox, Markdown, ScrollShadow } from '@lobehub/ui';
|
||||
import { createStaticStyles } from 'antd-style';
|
||||
import { NotebookText } from 'lucide-react';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import NeuralNetworkLoading from '@/components/NeuralNetworkLoading';
|
||||
|
||||
import type { CreateDocumentArgs } from '../../types';
|
||||
|
||||
const styles = createStaticStyles(({ css, cssVar }) => ({
|
||||
container: css`
|
||||
position: relative;
|
||||
|
||||
overflow: hidden;
|
||||
|
||||
width: 100%;
|
||||
border: 1px solid ${cssVar.colorBorderSecondary};
|
||||
border-radius: 16px;
|
||||
|
||||
background: ${cssVar.colorBgElevated};
|
||||
`,
|
||||
content: css`
|
||||
padding-block: 16px;
|
||||
padding-inline: 16px;
|
||||
`,
|
||||
header: css`
|
||||
padding-block: 10px;
|
||||
padding-inline: 12px;
|
||||
border-block-end: 1px solid ${cssVar.colorBorderSecondary};
|
||||
`,
|
||||
icon: css`
|
||||
color: ${cssVar.colorPrimary};
|
||||
`,
|
||||
statusTag: css`
|
||||
position: absolute;
|
||||
inset-block-end: 16px;
|
||||
inset-inline-start: 50%;
|
||||
transform: translateX(-50%);
|
||||
|
||||
display: inline-flex;
|
||||
gap: 6px;
|
||||
align-items: center;
|
||||
|
||||
padding-block: 4px;
|
||||
padding-inline: 12px;
|
||||
border: 1px solid ${cssVar.colorBorderSecondary};
|
||||
border-radius: 16px;
|
||||
|
||||
font-size: 14px;
|
||||
color: ${cssVar.colorText};
|
||||
|
||||
background: ${cssVar.colorBgContainer};
|
||||
`,
|
||||
title: css`
|
||||
overflow: hidden;
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 1;
|
||||
|
||||
font-weight: 500;
|
||||
color: ${cssVar.colorText};
|
||||
`,
|
||||
}));
|
||||
|
||||
export const CreateDocumentPlaceholder = memo<BuiltinPlaceholderProps<CreateDocumentArgs>>(
|
||||
({ args }) => {
|
||||
const { t } = useTranslation('plugin');
|
||||
const { title, content } = args || {};
|
||||
|
||||
return (
|
||||
<Flexbox className={styles.container}>
|
||||
{/* Header */}
|
||||
<Flexbox align={'center'} className={styles.header} gap={8} horizontal>
|
||||
<NotebookText className={styles.icon} size={16} />
|
||||
<Flexbox flex={1}>
|
||||
<div className={styles.title}>{title}</div>
|
||||
</Flexbox>
|
||||
<NeuralNetworkLoading size={20} />
|
||||
</Flexbox>
|
||||
{/* Content skeleton */}
|
||||
<ScrollShadow className={styles.content} offset={12} size={12} style={{ maxHeight: 400 }}>
|
||||
{content && (
|
||||
<Markdown style={{ overflow: 'unset', paddingBottom: 40 }} variant={'chat'}>
|
||||
{content}
|
||||
</Markdown>
|
||||
)}
|
||||
</ScrollShadow>
|
||||
<div className={styles.statusTag}>
|
||||
<NeuralNetworkLoading size={14} />
|
||||
<span style={{ fontSize: 12 }}>{t('builtins.lobe-notebook.actions.creating')}</span>
|
||||
</div>
|
||||
</Flexbox>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
export default CreateDocumentPlaceholder;
|
||||
@@ -0,0 +1,10 @@
|
||||
import { type BuiltinPlaceholder } from '@lobechat/types';
|
||||
|
||||
import { NotebookApiName } from '../../types';
|
||||
import { CreateDocumentPlaceholder } from './CreateDocument';
|
||||
|
||||
export { CreateDocumentPlaceholder } from './CreateDocument';
|
||||
|
||||
export const NotebookPlaceholders: Record<string, BuiltinPlaceholder> = {
|
||||
[NotebookApiName.createDocument]: CreateDocumentPlaceholder as BuiltinPlaceholder,
|
||||
};
|
||||
@@ -1,36 +1,46 @@
|
||||
'use client';
|
||||
|
||||
import { Flexbox, Tag, Text } from '@lobehub/ui';
|
||||
import { ActionIcon, CopyButton, Flexbox, Markdown, ScrollShadow } from '@lobehub/ui';
|
||||
import { Button } from 'antd';
|
||||
import { createStaticStyles } from 'antd-style';
|
||||
import { FileText, NotebookText } from 'lucide-react';
|
||||
import { Maximize2, NotebookText, PencilLine } from 'lucide-react';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { useChatStore } from '@/store/chat';
|
||||
|
||||
import { NotebookDocument } from '../../../types';
|
||||
|
||||
|
||||
const styles = createStaticStyles(({ css, cssVar }) => ({
|
||||
container: css`
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
|
||||
overflow: hidden;
|
||||
|
||||
width: 100%;
|
||||
padding-block: 12px;
|
||||
padding-inline: 12px;
|
||||
border: 1px solid ${cssVar.colorBorderSecondary};
|
||||
border-radius: 8px;
|
||||
border-radius: 16px;
|
||||
|
||||
background: ${cssVar.colorBgElevated};
|
||||
|
||||
&:hover {
|
||||
background: ${cssVar.colorFillSecondary};
|
||||
}
|
||||
`,
|
||||
description: css`
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
color: ${cssVar.colorTextSecondary};
|
||||
content: css`
|
||||
padding-inline: 16px;
|
||||
|
||||
font-size: 14px;
|
||||
`,
|
||||
expandButton: css`
|
||||
position: absolute;
|
||||
inset-block-end: 16px;
|
||||
inset-inline-start: 50%;
|
||||
transform: translateX(-50%);
|
||||
|
||||
box-shadow: ${cssVar.boxShadow};
|
||||
`,
|
||||
header: css`
|
||||
padding-block: 10px;
|
||||
padding-inline: 12px;
|
||||
border-block-end: 1px solid ${cssVar.colorBorderSecondary};
|
||||
`,
|
||||
icon: css`
|
||||
color: ${cssVar.colorPrimary};
|
||||
@@ -44,9 +54,6 @@ const styles = createStaticStyles(({ css, cssVar }) => ({
|
||||
font-weight: 500;
|
||||
color: ${cssVar.colorText};
|
||||
`,
|
||||
typeTag: css`
|
||||
font-size: 11px;
|
||||
`,
|
||||
}));
|
||||
|
||||
interface DocumentCardProps {
|
||||
@@ -54,30 +61,53 @@ interface DocumentCardProps {
|
||||
}
|
||||
|
||||
const DocumentCard = memo<DocumentCardProps>(({ document }) => {
|
||||
const { t } = useTranslation('plugin');
|
||||
const openDocument = useChatStore((s) => s.openDocument);
|
||||
|
||||
const handleClick = () => {
|
||||
const handleExpand = () => {
|
||||
openDocument(document.id);
|
||||
};
|
||||
|
||||
return (
|
||||
<Flexbox className={styles.container} gap={8} onClick={handleClick}>
|
||||
<Flexbox align={'center'} gap={8} horizontal>
|
||||
{document.type === 'note' ? (
|
||||
<NotebookText className={styles.icon} size={16} />
|
||||
) : (
|
||||
<FileText className={styles.icon} size={16} />
|
||||
)}
|
||||
<div className={styles.title}>{document.title}</div>
|
||||
<Tag className={styles.typeTag} size={'small'}>
|
||||
{document.type}
|
||||
</Tag>
|
||||
<Flexbox className={styles.container}>
|
||||
{/* Header */}
|
||||
<Flexbox align={'center'} className={styles.header} gap={8} horizontal>
|
||||
<NotebookText className={styles.icon} size={16} />
|
||||
<Flexbox flex={1}>
|
||||
<div className={styles.title}>{document.title}</div>
|
||||
</Flexbox>
|
||||
<Flexbox gap={4} horizontal>
|
||||
<CopyButton
|
||||
content={document.content}
|
||||
size={'small'}
|
||||
title={t('builtins.lobe-notebook.actions.copy')}
|
||||
/>
|
||||
<ActionIcon
|
||||
icon={PencilLine}
|
||||
onClick={handleExpand}
|
||||
size={'small'}
|
||||
title={t('builtins.lobe-notebook.actions.edit')}
|
||||
/>
|
||||
</Flexbox>
|
||||
</Flexbox>
|
||||
{document.description && (
|
||||
<Text className={styles.description} ellipsis={{ rows: 2 }}>
|
||||
{document.description}
|
||||
</Text>
|
||||
)}
|
||||
{/* Content */}
|
||||
<ScrollShadow className={styles.content} offset={12} size={12} style={{ maxHeight: 400 }}>
|
||||
<Markdown style={{ overflow: 'unset', paddingBottom: 40 }} variant={'chat'}>
|
||||
{document.content}
|
||||
</Markdown>
|
||||
</ScrollShadow>
|
||||
|
||||
{/* Floating expand button */}
|
||||
<Button
|
||||
className={styles.expandButton}
|
||||
color={'default'}
|
||||
icon={<Maximize2 size={14} />}
|
||||
onClick={handleExpand}
|
||||
shape={'round'}
|
||||
variant={'outlined'}
|
||||
>
|
||||
{t('builtins.lobe-notebook.actions.expand')}
|
||||
</Button>
|
||||
</Flexbox>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
'use client';
|
||||
|
||||
import type { BuiltinStreamingProps } from '@lobechat/types';
|
||||
import { Flexbox } from '@lobehub/ui';
|
||||
import { createStaticStyles } from 'antd-style';
|
||||
import { NotebookText } from 'lucide-react';
|
||||
import { memo } from 'react';
|
||||
|
||||
import BubblesLoading from '@/components/BubblesLoading';
|
||||
import NeuralNetworkLoading from '@/components/NeuralNetworkLoading';
|
||||
import StreamingMarkdown from '@/components/StreamingMarkdown';
|
||||
|
||||
import type { CreateDocumentArgs } from '../../../types';
|
||||
|
||||
const styles = createStaticStyles(({ css, cssVar }) => ({
|
||||
container: css`
|
||||
overflow: hidden;
|
||||
|
||||
width: 100%;
|
||||
border: 1px solid ${cssVar.colorBorderSecondary};
|
||||
border-radius: 16px;
|
||||
|
||||
background: ${cssVar.colorBgElevated};
|
||||
`,
|
||||
header: css`
|
||||
padding-block: 10px;
|
||||
padding-inline: 12px;
|
||||
border-block-end: 1px solid ${cssVar.colorBorderSecondary};
|
||||
`,
|
||||
icon: css`
|
||||
color: ${cssVar.colorPrimary};
|
||||
`,
|
||||
title: css`
|
||||
overflow: hidden;
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 1;
|
||||
|
||||
font-weight: 500;
|
||||
color: ${cssVar.colorText};
|
||||
`,
|
||||
}));
|
||||
|
||||
export const CreateDocumentStreaming = memo<BuiltinStreamingProps<CreateDocumentArgs>>(
|
||||
({ args }) => {
|
||||
const { content, title } = args || {};
|
||||
|
||||
if (!content && !title) return null;
|
||||
|
||||
return (
|
||||
<Flexbox className={styles.container}>
|
||||
{/* Header */}
|
||||
<Flexbox align={'center'} className={styles.header} gap={8} horizontal>
|
||||
<NotebookText className={styles.icon} size={16} />
|
||||
<Flexbox flex={1}>
|
||||
<div className={styles.title}>{title}</div>
|
||||
</Flexbox>
|
||||
<NeuralNetworkLoading size={20} />
|
||||
</Flexbox>
|
||||
{/* Content */}
|
||||
{!content ? (
|
||||
<Flexbox paddingBlock={16} paddingInline={12}>
|
||||
<BubblesLoading />
|
||||
</Flexbox>
|
||||
) : (
|
||||
<StreamingMarkdown>{content}</StreamingMarkdown>
|
||||
)}
|
||||
</Flexbox>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
CreateDocumentStreaming.displayName = 'CreateDocumentStreaming';
|
||||
|
||||
export default CreateDocumentStreaming;
|
||||
14
packages/builtin-tool-notebook/src/client/Streaming/index.ts
Normal file
14
packages/builtin-tool-notebook/src/client/Streaming/index.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { type BuiltinStreaming } from '@lobechat/types';
|
||||
|
||||
import { NotebookApiName } from '../../types';
|
||||
import { CreateDocumentStreaming } from './CreateDocument';
|
||||
|
||||
/**
|
||||
* Notebook Streaming Components Registry
|
||||
*
|
||||
* Streaming components are used to render tool calls while arguments
|
||||
* are still being generated, allowing real-time feedback to users.
|
||||
*/
|
||||
export const NotebookStreamings: Record<string, BuiltinStreaming> = {
|
||||
[NotebookApiName.createDocument]: CreateDocumentStreaming as BuiltinStreaming,
|
||||
};
|
||||
@@ -0,0 +1,57 @@
|
||||
'use client';
|
||||
|
||||
import { memo, useEffect, useRef, useState } from 'react';
|
||||
|
||||
interface AnimatedNumberProps {
|
||||
duration?: number;
|
||||
formatter?: (value: number) => string;
|
||||
value: number;
|
||||
}
|
||||
|
||||
export const AnimatedNumber = memo<AnimatedNumberProps>(({ value, duration = 500, formatter }) => {
|
||||
const [displayValue, setDisplayValue] = useState(value);
|
||||
const frameRef = useRef<number>(undefined);
|
||||
const startTimeRef = useRef<number>(undefined);
|
||||
const startValueRef = useRef(value);
|
||||
|
||||
useEffect(() => {
|
||||
const startValue = startValueRef.current;
|
||||
const diff = value - startValue;
|
||||
|
||||
if (diff === 0) return;
|
||||
|
||||
const animate = (currentTime: number) => {
|
||||
if (!startTimeRef.current) {
|
||||
startTimeRef.current = currentTime;
|
||||
}
|
||||
|
||||
const elapsed = currentTime - startTimeRef.current;
|
||||
const progress = Math.min(elapsed / duration, 1);
|
||||
|
||||
// easeOutCubic
|
||||
const easeProgress = 1 - (1 - progress) ** 3;
|
||||
const current = startValue + diff * easeProgress;
|
||||
|
||||
setDisplayValue(current);
|
||||
|
||||
if (progress < 1) {
|
||||
frameRef.current = requestAnimationFrame(animate);
|
||||
} else {
|
||||
startValueRef.current = value;
|
||||
startTimeRef.current = undefined;
|
||||
}
|
||||
};
|
||||
|
||||
frameRef.current = requestAnimationFrame(animate);
|
||||
|
||||
return () => {
|
||||
if (frameRef.current) {
|
||||
cancelAnimationFrame(frameRef.current);
|
||||
}
|
||||
};
|
||||
}, [value, duration]);
|
||||
|
||||
return formatter ? formatter(displayValue) : Math.round(displayValue).toLocaleString();
|
||||
});
|
||||
|
||||
AnimatedNumber.displayName = 'AnimatedNumber';
|
||||
@@ -1,6 +1,18 @@
|
||||
// Inspector components (customized tool call headers)
|
||||
export { NotebookInspectors } from './Inspector';
|
||||
|
||||
// Intervention components (approval dialogs)
|
||||
export { NotebookInterventions } from './Intervention';
|
||||
|
||||
// Placeholder components (loading states)
|
||||
export { CreateDocumentPlaceholder, NotebookPlaceholders } from './Placeholder';
|
||||
|
||||
// Render components (read-only snapshots)
|
||||
export { CreateDocument, NotebookRenders } from './Render';
|
||||
|
||||
// Streaming components
|
||||
export { NotebookStreamings } from './Streaming';
|
||||
|
||||
// Re-export types and manifest for convenience
|
||||
export { NotebookManifest } from '../manifest';
|
||||
export * from '../types';
|
||||
|
||||
@@ -34,11 +34,12 @@ Note: The list of existing documents is automatically provided in the context, s
|
||||
</workflow>
|
||||
|
||||
<best_practices>
|
||||
- Use descriptive titles that summarize the content
|
||||
- Use clean, concise titles without decorations or suffixes (e.g., use "The Last Letter" instead of "《The Last Letter》 - Short Story")
|
||||
- Choose appropriate document types based on content nature
|
||||
- For long content, consider breaking into multiple documents
|
||||
- Use append mode when adding to existing documents
|
||||
- Always confirm before deleting documents
|
||||
- Do NOT include h1 headings in document content (the title field already serves as the document title)
|
||||
</best_practices>
|
||||
|
||||
<response_format>
|
||||
|
||||
@@ -65,7 +65,6 @@
|
||||
align-items: center;
|
||||
|
||||
padding: 2px 8px;
|
||||
border: 1px solid var(--brand-border-color);
|
||||
border-radius: 6px;
|
||||
|
||||
background: var(--brand-tag-bg);
|
||||
|
||||
89
src/components/StreamingMarkdown/index.tsx
Normal file
89
src/components/StreamingMarkdown/index.tsx
Normal file
@@ -0,0 +1,89 @@
|
||||
'use client';
|
||||
|
||||
import { Markdown, ScrollShadow } from '@lobehub/ui';
|
||||
import { createStaticStyles } from 'antd-style';
|
||||
import { memo, useCallback, useEffect, useRef, useState } from 'react';
|
||||
|
||||
const styles = createStaticStyles(({ css }) => ({
|
||||
container: css`
|
||||
padding-block: 12px;
|
||||
padding-inline: 16px;
|
||||
border-radius: 8px;
|
||||
|
||||
font-size: 14px;
|
||||
`,
|
||||
}));
|
||||
|
||||
interface StreamingMarkdownProps {
|
||||
children?: string;
|
||||
maxHeight?: number;
|
||||
}
|
||||
|
||||
const StreamingMarkdown = memo<StreamingMarkdownProps>(({ children, maxHeight = 400 }) => {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const [userHasScrolled, setUserHasScrolled] = useState(false);
|
||||
const isAutoScrollingRef = useRef(false);
|
||||
|
||||
// Handle user scroll detection
|
||||
const handleScroll = useCallback(() => {
|
||||
// Ignore scroll events triggered by auto-scroll
|
||||
if (isAutoScrollingRef.current) return;
|
||||
|
||||
const container = containerRef.current;
|
||||
if (!container) return;
|
||||
|
||||
// Check if user scrolled away from bottom
|
||||
const distanceToBottom = container.scrollHeight - container.scrollTop - container.clientHeight;
|
||||
const isAtBottom = distanceToBottom < 20;
|
||||
|
||||
// If user scrolled up, stop auto-scrolling
|
||||
if (!isAtBottom) {
|
||||
setUserHasScrolled(true);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Auto scroll to bottom when content changes (unless user has scrolled)
|
||||
useEffect(() => {
|
||||
if (userHasScrolled) return;
|
||||
|
||||
const container = containerRef.current;
|
||||
if (!container) return;
|
||||
|
||||
isAutoScrollingRef.current = true;
|
||||
requestAnimationFrame(() => {
|
||||
container.scrollTop = container.scrollHeight;
|
||||
// Reset the flag after scroll completes
|
||||
requestAnimationFrame(() => {
|
||||
isAutoScrollingRef.current = false;
|
||||
});
|
||||
});
|
||||
}, [children, userHasScrolled]);
|
||||
|
||||
// Reset userHasScrolled when content is cleared (new stream starts)
|
||||
useEffect(() => {
|
||||
if (!children) {
|
||||
setUserHasScrolled(false);
|
||||
}
|
||||
}, [children]);
|
||||
|
||||
if (!children) return null;
|
||||
|
||||
return (
|
||||
<ScrollShadow
|
||||
className={styles.container}
|
||||
offset={12}
|
||||
onScroll={handleScroll}
|
||||
ref={containerRef}
|
||||
size={12}
|
||||
style={{ maxHeight }}
|
||||
>
|
||||
<Markdown animated style={{ overflow: 'unset' }} variant={'chat'}>
|
||||
{children}
|
||||
</Markdown>
|
||||
</ScrollShadow>
|
||||
);
|
||||
});
|
||||
|
||||
StreamingMarkdown.displayName = 'StreamingMarkdown';
|
||||
|
||||
export default StreamingMarkdown;
|
||||
@@ -1,4 +1,3 @@
|
||||
import { LOADING_FLAT } from '@lobechat/const';
|
||||
import { type ChatToolResult, type ToolIntervention } from '@lobechat/types';
|
||||
import { safeParsePartialJSON } from '@lobechat/utils';
|
||||
import { Flexbox } from '@lobehub/ui';
|
||||
@@ -20,6 +19,7 @@ interface RenderProps {
|
||||
identifier: string;
|
||||
intervention?: ToolIntervention;
|
||||
isArgumentsStreaming?: boolean;
|
||||
isToolCalling?: boolean;
|
||||
/**
|
||||
* ContentBlock ID (not the group message ID)
|
||||
*/
|
||||
@@ -52,6 +52,7 @@ const Render = memo<RenderProps>(
|
||||
intervention,
|
||||
toolMessageId,
|
||||
isArgumentsStreaming,
|
||||
isToolCalling,
|
||||
}) => {
|
||||
if (toolMessageId && intervention?.status === 'pending') {
|
||||
return (
|
||||
@@ -125,10 +126,7 @@ const Render = memo<RenderProps>(
|
||||
/>
|
||||
);
|
||||
|
||||
// Standalone plugins always have LOADING_FLAT as content
|
||||
const inPlaceholder = result.content === LOADING_FLAT && type !== 'standalone';
|
||||
|
||||
if (inPlaceholder) return placeholder;
|
||||
if (isToolCalling) return placeholder;
|
||||
|
||||
return (
|
||||
<Suspense fallback={placeholder}>
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import { LOADING_FLAT } from '@lobechat/const';
|
||||
import { type ChatToolResult, type ToolIntervention } from '@lobechat/types';
|
||||
import { AccordionItem, Flexbox, Skeleton } from '@lobehub/ui';
|
||||
import { Divider } from 'antd';
|
||||
import dynamic from 'next/dynamic';
|
||||
import { memo, useEffect, useState } from 'react';
|
||||
|
||||
import { useChatStore } from '@/store/chat';
|
||||
import { operationSelectors } from '@/store/chat/slices/operation/selectors';
|
||||
import { useToolStore } from '@/store/tool';
|
||||
import { toolSelectors } from '@/store/tool/selectors';
|
||||
import { getBuiltinRender } from '@/tools/renders';
|
||||
@@ -71,6 +74,16 @@ const Tool = memo<GroupToolProps>(
|
||||
const hasStreamingRenderer = !!getBuiltinStreaming(identifier, apiName);
|
||||
const forceShowStreamingRender = isArgumentsStreaming && hasStreamingRenderer;
|
||||
|
||||
// Get precise tool calling state from operation
|
||||
const isToolCallingFromOperation = useChatStore(
|
||||
operationSelectors.isMessageInToolCalling(assistantMessageId),
|
||||
);
|
||||
|
||||
// Fallback: arguments completed but no final result yet
|
||||
const isToolCallingFallback =
|
||||
!isArgumentsStreaming && (!result || result.content === LOADING_FLAT || !result.content);
|
||||
const isToolCalling = isToolCallingFromOperation || isToolCallingFallback;
|
||||
|
||||
const hasCustomRender = !!getBuiltinRender(identifier, apiName);
|
||||
|
||||
// Handle expand state changes with showPluginRender
|
||||
@@ -138,6 +151,7 @@ const Tool = memo<GroupToolProps>(
|
||||
identifier={identifier}
|
||||
intervention={intervention}
|
||||
isArgumentsStreaming={isArgumentsStreaming}
|
||||
isToolCalling={isToolCalling}
|
||||
messageId={assistantMessageId}
|
||||
result={result}
|
||||
setShowPluginRender={setShowPluginRender}
|
||||
|
||||
@@ -64,7 +64,7 @@ export default function PluginEmptyState() {
|
||||
{t('dev.preview.empty.title')}
|
||||
</Text>
|
||||
<Text className={styles.description}>{t('dev.preview.empty.desc')}</Text>
|
||||
<Space align="center" direction="vertical">
|
||||
<Space align="center" orientation="vertical">
|
||||
<div className={styles.line} style={{ width: 128 }} />
|
||||
<div className={styles.line} style={{ width: 96 }} />
|
||||
<div className={styles.line} style={{ width: 48 }} />
|
||||
|
||||
@@ -93,6 +93,15 @@ export default {
|
||||
'builtins.lobe-local-system.inspector.rename.result':
|
||||
'<old>{{oldName}}</old> → <new>{{newName}}</new>',
|
||||
'builtins.lobe-local-system.title': 'Local System',
|
||||
'builtins.lobe-notebook.actions.copy': 'Copy',
|
||||
'builtins.lobe-notebook.actions.creating': 'Creating document...',
|
||||
'builtins.lobe-notebook.actions.edit': 'Edit',
|
||||
'builtins.lobe-notebook.actions.expand': 'Expand',
|
||||
'builtins.lobe-notebook.apiName.createDocument': 'Create document',
|
||||
'builtins.lobe-notebook.apiName.deleteDocument': 'Delete document',
|
||||
'builtins.lobe-notebook.apiName.getDocument': 'Get document',
|
||||
'builtins.lobe-notebook.apiName.updateDocument': 'Update document',
|
||||
'builtins.lobe-notebook.title': 'Notebook',
|
||||
'builtins.lobe-page-agent.apiName.batchUpdate': 'Batch update nodes',
|
||||
'builtins.lobe-page-agent.apiName.compareSnapshots': 'Compare snapshots',
|
||||
'builtins.lobe-page-agent.apiName.convertToList': 'Convert to list',
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
LocalSystemInspectors,
|
||||
LocalSystemManifest,
|
||||
} from '@lobechat/builtin-tool-local-system/client';
|
||||
import { NotebookInspectors, NotebookManifest } from '@lobechat/builtin-tool-notebook/client';
|
||||
import { PageAgentInspectors, PageAgentManifest } from '@lobechat/builtin-tool-page-agent/client';
|
||||
import {
|
||||
WebBrowsingInspectors,
|
||||
@@ -38,6 +39,7 @@ const BuiltinToolInspectors: Record<string, Record<string, BuiltinInspector>> =
|
||||
[GTDManifest.identifier]: GTDInspectors as Record<string, BuiltinInspector>,
|
||||
[KnowledgeBaseManifest.identifier]: KnowledgeBaseInspectors as Record<string, BuiltinInspector>,
|
||||
[LocalSystemManifest.identifier]: LocalSystemInspectors as Record<string, BuiltinInspector>,
|
||||
[NotebookManifest.identifier]: NotebookInspectors as Record<string, BuiltinInspector>,
|
||||
[PageAgentManifest.identifier]: PageAgentInspectors as Record<string, BuiltinInspector>,
|
||||
[WebBrowsingManifest.identifier]: WebBrowsingInspectors as Record<string, BuiltinInspector>,
|
||||
};
|
||||
|
||||
@@ -4,6 +4,10 @@ import {
|
||||
LocalSystemListFilesPlaceholder,
|
||||
LocalSystemSearchFilesPlaceholder,
|
||||
} from '@lobechat/builtin-tool-local-system/client';
|
||||
import {
|
||||
NotebookIdentifier,
|
||||
NotebookPlaceholders,
|
||||
} from '@lobechat/builtin-tool-notebook/client';
|
||||
import {
|
||||
WebBrowsingManifest,
|
||||
WebBrowsingPlaceholders,
|
||||
@@ -19,6 +23,7 @@ export const BuiltinToolPlaceholders: Record<string, Record<string, any>> = {
|
||||
[LocalSystemApiName.searchLocalFiles]: LocalSystemSearchFilesPlaceholder,
|
||||
[LocalSystemApiName.listLocalFiles]: LocalSystemListFilesPlaceholder,
|
||||
},
|
||||
[NotebookIdentifier]: NotebookPlaceholders as Record<string, any>,
|
||||
[WebBrowsingManifest.identifier]: WebBrowsingPlaceholders as Record<string, any>,
|
||||
};
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
LocalSystemManifest,
|
||||
LocalSystemStreamings,
|
||||
} from '@lobechat/builtin-tool-local-system/client';
|
||||
import { NotebookManifest, NotebookStreamings } from '@lobechat/builtin-tool-notebook/client';
|
||||
import { type BuiltinStreaming } from '@lobechat/types';
|
||||
|
||||
/**
|
||||
@@ -29,6 +30,7 @@ const BuiltinToolStreamings: Record<string, Record<string, BuiltinStreaming>> =
|
||||
>,
|
||||
[GTDManifest.identifier]: GTDStreamings as Record<string, BuiltinStreaming>,
|
||||
[LocalSystemManifest.identifier]: LocalSystemStreamings as Record<string, BuiltinStreaming>,
|
||||
[NotebookManifest.identifier]: NotebookStreamings as Record<string, BuiltinStreaming>,
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user