💄 style: support tool streaming and title custom render (#10976)

* support custom inspector

* support local-system inspector

* add streaming feature

* merge
This commit is contained in:
Arvin Xu
2025-12-25 23:52:57 +08:00
committed by GitHub
parent 84350b3ffc
commit 576ccd678c
38 changed files with 1136 additions and 191 deletions
+15
View File
@@ -16,6 +16,21 @@
"builtins.lobe-agent-builder.apiName.updateMeta": "更新元数据",
"builtins.lobe-agent-builder.apiName.updatePrompt": "更新系统提示词",
"builtins.lobe-agent-builder.title": "助理构建专家",
"builtins.lobe-cloud-code-interpreter.apiName.editLocalFile": "编辑文件",
"builtins.lobe-cloud-code-interpreter.apiName.executeCode": "执行代码",
"builtins.lobe-cloud-code-interpreter.apiName.exportFile": "导出文件",
"builtins.lobe-cloud-code-interpreter.apiName.getCommandOutput": "获取代码输出",
"builtins.lobe-cloud-code-interpreter.apiName.globLocalFiles": "匹配搜索文件",
"builtins.lobe-cloud-code-interpreter.apiName.grepContent": "搜索内容",
"builtins.lobe-cloud-code-interpreter.apiName.killCommand": "终止代码执行",
"builtins.lobe-cloud-code-interpreter.apiName.listLocalFiles": "查看文件列表",
"builtins.lobe-cloud-code-interpreter.apiName.moveLocalFiles": "移动文件",
"builtins.lobe-cloud-code-interpreter.apiName.readLocalFile": "读取文件内容",
"builtins.lobe-cloud-code-interpreter.apiName.renameLocalFile": "重命名",
"builtins.lobe-cloud-code-interpreter.apiName.runCommand": "执行代码",
"builtins.lobe-cloud-code-interpreter.apiName.searchLocalFiles": "搜索文件",
"builtins.lobe-cloud-code-interpreter.apiName.writeLocalFile": "写入文件",
"builtins.lobe-cloud-code-interpreter.title": "云端沙箱",
"builtins.lobe-group-agent-builder.apiName.getAvailableModels": "获取可用模型",
"builtins.lobe-group-agent-builder.apiName.installPlugin": "安装技能",
"builtins.lobe-group-agent-builder.apiName.inviteAgent": "邀请成员",
+10 -10
View File
@@ -14,15 +14,6 @@
"tts",
"stt"
],
"workspaces": [
"packages/*",
"packages/business/*",
"e2e",
"apps/desktop/src/main"
],
"overrides": {
"stylelint-config-clean-order": "7.0.0"
},
"homepage": "https://github.com/lobehub/lobe-chat",
"bugs": {
"url": "https://github.com/lobehub/lobe-chat/issues/new/choose"
@@ -34,6 +25,12 @@
"license": "MIT",
"author": "LobeHub <i@lobehub.com>",
"sideEffects": false,
"workspaces": [
"packages/*",
"packages/business/*",
"e2e",
"apps/desktop/src/main"
],
"scripts": {
"prebuild": "tsx scripts/prebuild.mts && npm run lint",
"build": "cross-env NODE_OPTIONS=--max-old-space-size=6144 next build --webpack",
@@ -134,6 +131,9 @@
"eslint --fix"
]
},
"overrides": {
"stylelint-config-clean-order": "7.0.0"
},
"dependencies": {
"@ant-design/icons": "^6.1.0",
"@ant-design/pro-components": "^2.8.10",
@@ -451,4 +451,4 @@
"access": "public",
"registry": "https://registry.npmjs.org"
}
}
}
@@ -242,7 +242,7 @@ export const LocalSystemManifest: BuiltinToolManifest = {
type: 'number',
},
},
required: ['command'],
required: ['description', 'command'],
type: 'object',
},
},
+36
View File
@@ -172,6 +172,42 @@ export interface BuiltinPlaceholderProps<T extends Record<string, any> = any> {
export type BuiltinPlaceholder = (props: BuiltinPlaceholderProps) => ReactNode;
// ==================== Inspector Renderer Types ====================
export interface BuiltinInspectorProps<Arguments = any, State = any> {
apiName: string;
args: Arguments;
identifier: string;
/**
* Whether the tool arguments are currently streaming (not yet complete)
* Use this to distinguish between "arguments streaming" vs "tool executing" states
*/
isArgumentsStreaming?: boolean;
isLoading?: boolean;
partialArgs?: Arguments;
pluginState?: State;
result?: { content: string | null; error?: any };
}
export type BuiltinInspector = <A = any, S = any>(props: BuiltinInspectorProps<A, S>) => ReactNode;
// ==================== Streaming Renderer Types ====================
/**
* Props for streaming render components
* Note: During streaming phase, only basic info is available.
* pluginState and streaming content should be fetched from store inside the component.
*/
export interface BuiltinStreamingProps<Arguments = any> {
apiName: string;
args: Arguments;
identifier: string;
messageId: string;
toolCallId: string;
}
export type BuiltinStreaming = <A = any>(props: BuiltinStreamingProps<A>) => ReactNode;
export interface BuiltinServerRuntimeOutput {
content: string;
error?: any;
+10
View File
@@ -1,3 +1,5 @@
import { parse } from 'partial-json';
export const safeParseJSON = <T = Record<string, any>>(text?: string) => {
if (typeof text !== 'string') return undefined;
@@ -10,3 +12,11 @@ export const safeParseJSON = <T = Record<string, any>>(text?: string) => {
return json;
};
export const safeParsePartialJSON = <T = Record<string, any>>(text?: string): T | undefined => {
try {
return parse(text || '{}') as T;
} catch {
return undefined;
}
};
@@ -1,14 +1,17 @@
import { MemorySourceType } from '@lobechat/types';
import { createWorkflow, serveMany } from '@upstash/workflow/nextjs';
import {
MemoryExtractionExecutor,
MemoryExtractionPayloadInput,
type MemoryExtractionPayloadInput,
normalizeMemoryExtractionPayload,
} from '@/server/services/memory/userMemory/extract';
import { MemorySourceType } from '@lobechat/types';
export const { POST } = serveMany({
'memory:user-memory:extract:users:topics:extract-layers:other-layers': createWorkflow<MemoryExtractionPayloadInput, { processed: number, results: any[] }>(async (context) => {
'memory:user-memory:extract:users:topics:extract-layers:other-layers': createWorkflow<
MemoryExtractionPayloadInput,
{ processed: number; results: any[] }
>(async (context) => {
const params = normalizeMemoryExtractionPayload(context.requestPayload || {});
if (!params.userIds.length) {
return { message: 'No user id provided for topic batch.', processed: 0, results: [] };
@@ -23,7 +26,13 @@ export const { POST } = serveMany({
const userId = params.userIds[0];
const executor = await MemoryExtractionExecutor.create();
const results: { extracted: boolean; layers: Record<string, number>; memoryIds: string[]; topicId: string, userId: string }[] = [];
const results: {
extracted: boolean;
layers: Record<string, number>;
memoryIds: string[];
topicId: string;
userId: string;
}[] = [];
for (const topicId of params.topicIds) {
const extracted = await context.run(
@@ -45,5 +54,5 @@ export const { POST } = serveMany({
}
return { processed: results.length, results };
})
}),
});
@@ -1,12 +1,12 @@
import { LayersEnum, MemorySourceType } from '@lobechat/types';
import { createWorkflow, serveMany } from '@upstash/workflow/nextjs';
import {
MemoryExtractionExecutor,
MemoryExtractionPayloadInput,
normalizeMemoryExtractionPayload,
type MemoryExtractionPayloadInput,
TOPIC_WORKFLOW_NAMES,
normalizeMemoryExtractionPayload,
} from '@/server/services/memory/userMemory/extract';
import { LayersEnum, MemorySourceType } from '@lobechat/types';
type ExtractionResult = {
extracted: boolean;
@@ -95,13 +95,28 @@ const orchestratorWorkflow = createWorkflow<
>(async (context) => {
const params = normalizeMemoryExtractionPayload(context.requestPayload || {});
if (!params.userIds.length) {
return { message: 'No user id provided for topic batch.', nextIdentityCursor: null, processedCep: 0, processedIdentity: 0 };
return {
message: 'No user id provided for topic batch.',
nextIdentityCursor: null,
processedCep: 0,
processedIdentity: 0,
};
}
if (!params.topicIds.length) {
return { message: 'No topic ids provided for extraction.', nextIdentityCursor: null, processedCep: 0, processedIdentity: 0 };
return {
message: 'No topic ids provided for extraction.',
nextIdentityCursor: null,
processedCep: 0,
processedIdentity: 0,
};
}
if (!params.sources.includes(MemorySourceType.ChatTopic)) {
return { message: 'Source not supported in topic batch.', nextIdentityCursor: null, processedCep: 0, processedIdentity: 0 };
return {
message: 'Source not supported in topic batch.',
nextIdentityCursor: null,
processedCep: 0,
processedIdentity: 0,
};
}
const userId = params.userIds[0];
@@ -119,7 +134,13 @@ const orchestratorWorkflow = createWorkflow<
const invokeIdentity = (topicId: string, pointer: number) =>
context.invoke(`memory:user-memory:extract:users:${userId}:topics:identity:${pointer}`, {
body: { ...params, identityCursor: undefined, topicIds: [topicId], userId, userIds: [userId] },
body: {
...params,
identityCursor: undefined,
topicIds: [topicId],
userId,
userIds: [userId],
},
flowControl: { key: `memory:user:${userId}:identity`, parallelism: 1 },
workflow: identityWorkflow,
});
@@ -1,4 +1,4 @@
import { ModelUsage } from '@/types/index';
import { type ModelUsage } from '@/types/index';
interface ChargeParams {
metadata: {
+1 -1
View File
@@ -1,5 +1,5 @@
/* eslint-disable unused-imports/no-unused-vars, @typescript-eslint/no-unused-vars */
import { Plans, ReferralStatusString } from '@lobechat/types';
import { type Plans, type ReferralStatusString } from '@lobechat/types';
export async function getReferralStatus(userId: string): Promise<ReferralStatusString | undefined> {
return undefined;
@@ -2,8 +2,9 @@
import { Modal } from '@lobehub/ui';
import { memo } from 'react';
import FileViewer from '@/features/FileViewer';
import { UploadFileItem } from '@/types/files/upload';
import { type UploadFileItem } from '@/types/files/upload';
interface FilePreviewModalProps {
file: UploadFileItem;
@@ -12,7 +13,6 @@ interface FilePreviewModalProps {
}
const FilePreviewModal = memo<FilePreviewModalProps>(({ file, open, onClose }) => {
// Get the best available URL for preview
const previewUrl = file.previewUrl || file.fileUrl || file.base64Url || '';
@@ -1,39 +1,74 @@
import { type ToolIntervention } from '@lobechat/types';
import { safeParseJSON, safeParsePartialJSON } from '@lobechat/utils';
import { Flexbox } from '@lobehub/ui';
import { memo } from 'react';
import { LOADING_FLAT } from '@/const/message';
import { getBuiltinInspector } from '@/tools/inspectors';
import StatusIndicator from './StatusIndicator';
import ToolTitle from './ToolTitle';
interface InspectorProps {
apiName: string;
arguments?: string;
identifier: string;
intervention?: ToolIntervention;
/**
* Whether the tool arguments are currently streaming
*/
isArgumentsStreaming?: boolean;
result?: { content: string | null; error?: any; state?: any };
}
const Inspectors = memo<InspectorProps>(({ identifier, apiName, result, intervention }) => {
const hasError = !!result?.error;
const hasSuccessResult = !!result?.content && result.content !== LOADING_FLAT;
const hasResult = hasSuccessResult || hasError;
const Inspectors = memo<InspectorProps>(
({ identifier, apiName, arguments: argsStr, result, intervention, isArgumentsStreaming }) => {
const hasError = !!result?.error;
const hasSuccessResult = !!result?.content && result.content !== LOADING_FLAT;
const hasResult = hasSuccessResult || hasError;
const isPending = intervention?.status === 'pending';
const isAborted = intervention?.status === 'aborted';
const isTitleLoading = !hasResult && !isPending && !isAborted;
const isPending = intervention?.status === 'pending';
const isAborted = intervention?.status === 'aborted';
return (
<Flexbox align={'center'} gap={6} horizontal>
<StatusIndicator intervention={intervention} result={result} />
<ToolTitle
apiName={apiName}
identifier={identifier}
isAborted={isAborted}
isLoading={isTitleLoading}
/>
</Flexbox>
);
});
// Distinguish between arguments streaming and tool executing
const isToolExecuting = !hasResult && !isPending && !isAborted && !isArgumentsStreaming;
const isTitleLoading = isArgumentsStreaming || isToolExecuting;
// Check for custom inspector renderer
const CustomInspector = getBuiltinInspector(identifier, apiName);
if (CustomInspector) {
const args = safeParseJSON(argsStr);
const partialJson = safeParsePartialJSON(argsStr);
return (
<Flexbox align={'center'} gap={6} horizontal>
<StatusIndicator intervention={intervention} result={result} />
<CustomInspector
apiName={apiName}
args={args || {}}
identifier={identifier}
isArgumentsStreaming={isArgumentsStreaming}
isLoading={isTitleLoading}
partialArgs={partialJson}
pluginState={result?.state}
result={result}
/>
</Flexbox>
);
}
return (
<Flexbox align={'center'} gap={6} horizontal>
<StatusIndicator intervention={intervention} result={result} />
<ToolTitle
apiName={apiName}
identifier={identifier}
isAborted={isAborted}
isLoading={isTitleLoading}
/>
</Flexbox>
);
},
);
export default Inspectors;
@@ -1,8 +1,11 @@
import { LOADING_FLAT } from '@lobechat/const';
import { type ChatToolResult, type ToolIntervention } from '@lobechat/types';
import { safeParsePartialJSON } from '@lobechat/utils';
import { Flexbox } from '@lobehub/ui';
import { Suspense, memo } from 'react';
import { getBuiltinStreaming } from '@/tools/streamings';
import AbortResponse from './AbortResponse';
import CustomRender from './CustomRender';
import ErrorResponse from './ErrorResponse';
@@ -16,6 +19,7 @@ interface RenderProps {
arguments?: string;
identifier: string;
intervention?: ToolIntervention;
isArgumentsStreaming?: boolean;
/**
* ContentBlock ID (not the group message ID)
*/
@@ -47,7 +51,31 @@ const Render = memo<RenderProps>(
type,
intervention,
toolMessageId,
isArgumentsStreaming,
}) => {
// Handle arguments streaming state
if (isArgumentsStreaming) {
// Check if there's a custom streaming renderer for this tool
const StreamingRenderer = getBuiltinStreaming(identifier, apiName);
if (StreamingRenderer) {
const args = safeParsePartialJSON(requestArgs);
return (
<StreamingRenderer
apiName={apiName}
args={args}
identifier={identifier}
messageId={messageId}
toolCallId={toolCallId}
/>
);
}
// No custom streaming renderer, return null
return null;
}
if (toolMessageId && intervention?.status === 'pending') {
return (
<Intervention
@@ -7,6 +7,7 @@ import { memo, useEffect, useState } from 'react';
import Actions from '@/features/Conversation/Messages/AssistantGroup/Tool/Actions';
import { useToolStore } from '@/store/tool';
import { toolSelectors } from '@/store/tool/selectors';
import { getBuiltinStreaming } from '@/tools/streamings';
import Inspectors from './Inspector';
@@ -68,6 +69,16 @@ const Tool = memo<GroupToolProps>(
const showCustomPluginRender = !isPending && !isReject && !isAbort;
let isArgumentsStreaming = false;
try {
JSON.parse(requestArgs || '{}');
} catch {
isArgumentsStreaming = true;
}
const hasStreamingRenderer = !!getBuiltinStreaming(identifier, apiName);
const forceShowStreamingRender = isArgumentsStreaming && hasStreamingRenderer;
// Wrap handleExpand to prevent collapsing when alwaysExpand is set
const wrappedHandleExpand = (expand?: boolean) => {
// Block collapse action when alwaysExpand is set
@@ -88,6 +99,10 @@ const Tool = memo<GroupToolProps>(
onCollapsibleChange?.(!isAlwaysExpand);
}, [isAlwaysExpand, onCollapsibleChange]);
useEffect(() => {
handleExpand?.(forceShowStreamingRender);
}, [forceShowStreamingRender]);
return (
<AccordionItem
action={
@@ -108,8 +123,10 @@ const Tool = memo<GroupToolProps>(
title={
<Inspectors
apiName={apiName}
arguments={requestArgs}
identifier={identifier}
intervention={intervention}
isArgumentsStreaming={isArgumentsStreaming}
result={result}
/>
}
@@ -131,6 +148,7 @@ const Tool = memo<GroupToolProps>(
arguments={requestArgs}
identifier={identifier}
intervention={intervention}
isArgumentsStreaming={isArgumentsStreaming}
messageId={assistantMessageId}
result={result}
setShowPluginRender={setShowPluginRender}
@@ -140,7 +140,13 @@ const ListView = memo<ListViewProps>(
return (
<Flexbox height={'100%'}>
<Flexbox align={'center'} className={styles.header} horizontal paddingInline={8} style={{ fontSize: 12 }}>
<Flexbox
align={'center'}
className={styles.header}
horizontal
paddingInline={8}
style={{ fontSize: 12 }}
>
<Center height={40} style={{ paddingInline: 4 }}>
<Checkbox
checked={allSelected}
@@ -180,7 +186,7 @@ const ListView = memo<ListViewProps>(
return (
<Center className={styles.loadMoreContainer} key="load-more">
<Button loading={isLoadingMore} onClick={handleLoadMore} type="default">
{t('loadMore', { ns: 'file', defaultValue: 'Load More' })}
{t('loadMore', { defaultValue: 'Load More', ns: 'file' })}
</Button>
</Center>
);
@@ -2,7 +2,7 @@
import { SiGithub, SiX } from '@icons-pack/react-simple-icons';
import { Center, Flexbox, Icon, Input, Modal, Text, TextArea, Tooltip } from '@lobehub/ui';
import { App, Divider, Form, Upload, type UploadProps } from 'antd';
import { App, Form, Upload, type UploadProps } from 'antd';
import { useTheme } from 'antd-style';
import { CircleHelp, Globe, ImagePlus, Trash2 } from 'lucide-react';
import { memo, useCallback, useEffect, useState } from 'react';
+19 -5
View File
@@ -16,6 +16,21 @@ export default {
'builtins.lobe-agent-builder.apiName.updateMeta': 'Update metadata',
'builtins.lobe-agent-builder.apiName.updatePrompt': 'Update system prompt',
'builtins.lobe-agent-builder.title': 'Agent Builder Expert',
'builtins.lobe-cloud-code-interpreter.apiName.editLocalFile': 'Edit file',
'builtins.lobe-cloud-code-interpreter.apiName.executeCode': 'Execute code',
'builtins.lobe-cloud-code-interpreter.apiName.exportFile': 'Export file',
'builtins.lobe-cloud-code-interpreter.apiName.getCommandOutput': 'Get command output',
'builtins.lobe-cloud-code-interpreter.apiName.globLocalFiles': 'Glob search files',
'builtins.lobe-cloud-code-interpreter.apiName.grepContent': 'Search content',
'builtins.lobe-cloud-code-interpreter.apiName.killCommand': 'Terminate command',
'builtins.lobe-cloud-code-interpreter.apiName.listLocalFiles': 'List files',
'builtins.lobe-cloud-code-interpreter.apiName.moveLocalFiles': 'Move files',
'builtins.lobe-cloud-code-interpreter.apiName.readLocalFile': 'Read file content',
'builtins.lobe-cloud-code-interpreter.apiName.renameLocalFile': 'Rename',
'builtins.lobe-cloud-code-interpreter.apiName.runCommand': 'Run command',
'builtins.lobe-cloud-code-interpreter.apiName.searchLocalFiles': 'Search files',
'builtins.lobe-cloud-code-interpreter.apiName.writeLocalFile': 'Write file',
'builtins.lobe-cloud-code-interpreter.title': 'Cloud Sandbox',
'builtins.lobe-group-agent-builder.apiName.getAvailableModels': 'Get available models',
'builtins.lobe-group-agent-builder.apiName.installPlugin': 'Install Skill',
'builtins.lobe-group-agent-builder.apiName.inviteAgent': 'Invite member',
@@ -176,7 +191,8 @@ export default {
'dev.mcp.headers.desc': 'Enter HTTP headers',
'dev.mcp.headers.label': 'HTTP Headers',
'dev.mcp.identifier.desc': 'Name for this MCP (English characters only)',
'dev.mcp.identifier.invalid': 'Identifier must contain only letters, numbers, hyphens, underscores',
'dev.mcp.identifier.invalid':
'Identifier must contain only letters, numbers, hyphens, underscores',
'dev.mcp.identifier.label': 'MCP name',
'dev.mcp.identifier.placeholder': 'e.g. my-mcp-plugin',
'dev.mcp.identifier.required': 'Enter MCP identifier',
@@ -287,8 +303,7 @@ export default {
'mcpInstall.configurationDescription': 'Configure required parameters for this MCP',
'mcpInstall.configurationRequired': 'Configure parameters',
'mcpInstall.continueInstall': 'Continue',
'mcpInstall.dependenciesDescription':
'Install required dependencies, then recheck to continue.',
'mcpInstall.dependenciesDescription': 'Install required dependencies, then recheck to continue.',
'mcpInstall.dependenciesRequired': 'Install system dependencies',
'mcpInstall.dependencyStatus.installed': 'Installed',
'mcpInstall.dependencyStatus.notInstalled': 'Not installed',
@@ -352,8 +367,7 @@ export default {
'protocolInstall.meta.source': 'Source',
'protocolInstall.meta.version': 'Version',
'protocolInstall.official.badge': 'LobeHub Official Skill',
'protocolInstall.official.description':
'Official LobeHub Skill, verified and security-checked.',
'protocolInstall.official.description': 'Official LobeHub Skill, verified and security-checked.',
'protocolInstall.official.loadingMessage': 'Loading Skill details…',
'protocolInstall.official.loadingTitle': 'Loading',
'protocolInstall.official.title': 'Install official Skill',
@@ -0,0 +1,65 @@
'use client';
import { type BuiltinInspectorProps } from '@lobechat/types';
import { Icon } from '@lobehub/ui';
import { createStyles } from 'antd-style';
import { ChevronRight } from 'lucide-react';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
import { shinyTextStylish } from '@/styles/loading';
import { type ExecuteCodeState } from '../../type';
const useStyles = createStyles(({ css, token }) => ({
content: css`
font-family: ${token.fontFamilyCode};
`,
root: css`
overflow: hidden;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 1;
color: ${token.colorTextDescription};
`,
shinyText: shinyTextStylish(token),
}));
interface ExecuteCodeParams {
code: string;
description: string;
language?: 'javascript' | 'python' | 'typescript';
}
export const ExecuteCodeInspector = memo<
BuiltinInspectorProps<ExecuteCodeParams, ExecuteCodeState>
>(({ args, partialArgs, isArgumentsStreaming }) => {
const { t } = useTranslation('plugin');
const { styles, cx } = useStyles();
if (isArgumentsStreaming && !partialArgs?.description)
return (
<div className={cx(styles.root, styles.shinyText)}>
<span>{t('builtins.lobe-cloud-code-interpreter.title')}</span>
<Icon icon={ChevronRight} style={{ marginInline: 4 }} />
<span>{t('builtins.lobe-cloud-code-interpreter.apiName.executeCode')}</span>
</div>
);
const displayText = args?.description || partialArgs?.description || '';
return (
<div className={cx(styles.root, isArgumentsStreaming && styles.shinyText)}>
<span>{t('builtins.lobe-cloud-code-interpreter.apiName.executeCode')}</span>
{displayText && (
<>
<Icon icon={ChevronRight} style={{ marginInline: 4 }} />
<span className={styles.content}>{displayText}</span>
</>
)}
</div>
);
});
ExecuteCodeInspector.displayName = 'ExecuteCodeInspector';
@@ -0,0 +1,9 @@
import { CodeInterpreterApiName } from '../index';
import { ExecuteCodeInspector } from './ExecuteCode';
/**
* Code Interpreter Inspector Components Registry
*/
export const CodeInterpreterInspectors = {
[CodeInterpreterApiName.executeCode]: ExecuteCodeInspector,
};
@@ -1,11 +1,9 @@
'use client';
import { CheckCircleFilled, CloseCircleFilled } from '@ant-design/icons';
import { type BuiltinRenderProps } from '@lobechat/types';
import { ActionIcon, Block, Flexbox, Highlighter, Text } from '@lobehub/ui';
import type { BuiltinRenderProps } from '@lobechat/types';
import { Block, Flexbox, Highlighter } from '@lobehub/ui';
import { createStyles } from 'antd-style';
import { ChevronDown, ChevronUp } from 'lucide-react';
import { memo, useState } from 'react';
import { memo } from 'react';
import { type ExecuteCodeState } from '../../type';
@@ -40,84 +38,41 @@ interface ExecuteCodeParams {
language?: 'javascript' | 'python' | 'typescript';
}
const languageDisplayNames: Record<string, string> = {
javascript: 'JavaScript',
python: 'Python',
typescript: 'TypeScript',
};
const ExecuteCode = memo<BuiltinRenderProps<ExecuteCodeParams, ExecuteCodeState>>(
({ args, pluginState }) => {
const { styles, theme } = useStyles();
const isSuccess = pluginState?.success;
const [expanded, setExpanded] = useState(false);
const { styles } = useStyles();
const language = args.language || 'python';
const displayLanguage = languageDisplayNames[language] || language;
const statusMessage = pluginState?.success
? 'Execution completed'
: pluginState?.error || 'Execution failed';
return (
<Flexbox className={styles.container} gap={8}>
{/* Header: Language + Status */}
<Flexbox align={'center'} className={styles.header} horizontal justify={'space-between'}>
<Flexbox gap={8} horizontal>
<Flexbox gap={4} horizontal>
{pluginState === undefined ? null : isSuccess ? (
<CheckCircleFilled
className={styles.statusIcon}
style={{ color: theme.colorSuccess }}
/>
) : (
<CloseCircleFilled
className={styles.statusIcon}
style={{ color: theme.colorError }}
/>
)}
<Text className={styles.head}>{displayLanguage}</Text>
</Flexbox>
<Text className={styles.head} type={'secondary'}>
{statusMessage}
</Text>
</Flexbox>
<Flexbox align={'center'} gap={8} horizontal>
<ActionIcon
className={`action-icon`}
icon={expanded ? ChevronUp : ChevronDown}
onClick={() => setExpanded(!expanded)}
size={'small'}
style={{ opacity: expanded ? 1 : undefined }}
title={expanded ? 'Collapse' : 'Expand'}
/>
</Flexbox>
</Flexbox>
{/* Code & Output */}
{expanded && (
<Block gap={8} padding={8} variant={'outlined'}>
<Block gap={8} padding={8} variant={'outlined'}>
<Highlighter
language={language}
showLanguage={false}
style={{ maxHeight: 200, overflow: 'auto', paddingInline: 8 }}
variant={'borderless'}
wrap
>
{args.code}
</Highlighter>
{pluginState?.output && (
<Highlighter
language={language}
language={'text'}
showLanguage={false}
style={{ paddingInline: 8 }}
variant={'borderless'}
style={{ maxHeight: 200, overflow: 'auto', paddingInline: 8 }}
variant={'filled'}
wrap
>
{args.code}
{pluginState.output}
</Highlighter>
{pluginState?.output && (
<Highlighter language={'text'} showLanguage={false} variant={'filled'} wrap>
{pluginState.output}
</Highlighter>
)}
{pluginState?.stderr && (
<Highlighter language={'text'} showLanguage={false} variant={'filled'} wrap>
{pluginState.stderr}
</Highlighter>
)}
</Block>
)}
)}
{pluginState?.stderr && (
<Highlighter language={'text'} showLanguage={false} variant={'filled'} wrap>
{pluginState.stderr}
</Highlighter>
)}
</Block>
</Flexbox>
);
},
@@ -0,0 +1,41 @@
'use client';
import { type BuiltinStreamingProps } from '@lobechat/types';
import { Highlighter } from '@lobehub/ui';
import { memo } from 'react';
interface ExecuteCodeParams {
code?: string;
description?: string;
language?: 'javascript' | 'python' | 'typescript';
}
const languageDisplayNames: Record<string, string> = {
javascript: 'JavaScript',
python: 'Python',
typescript: 'TypeScript',
};
export const ExecuteCodeStreaming = memo<BuiltinStreamingProps<ExecuteCodeParams>>(({ args }) => {
const { code, language = 'python' } = args || {};
const displayLanguage = languageDisplayNames[language] || language;
// Don't render if no code yet
if (!code) return null;
return (
<Highlighter
animated
language={displayLanguage}
showLanguage={false}
style={{ padding: '4px 8px' }}
variant={'outlined'}
wrap
>
{code}
</Highlighter>
);
});
ExecuteCodeStreaming.displayName = 'ExecuteCodeStreaming';
@@ -0,0 +1,9 @@
import { CodeInterpreterApiName } from '../index';
import { ExecuteCodeStreaming } from './ExecuteCode';
/**
* Code Interpreter Streaming Components Registry
*/
export const CodeInterpreterStreamings = {
[CodeInterpreterApiName.executeCode]: ExecuteCodeStreaming,
};
+9 -3
View File
@@ -21,6 +21,7 @@ export const CodeInterpreterApiName = {
export const CodeInterpreterIdentifier = 'lobe-cloud-code-interpreter';
/* eslint-disable sort-keys-fix/sort-keys-fix */
export const CodeInterpreterManifest: BuiltinToolManifest = {
api: [
{
@@ -30,8 +31,9 @@ export const CodeInterpreterManifest: BuiltinToolManifest = {
name: CodeInterpreterApiName.executeCode,
parameters: {
properties: {
code: {
description: 'The code to execute',
description: {
description:
'A brief description of what this code does (required for user understanding)',
type: 'string',
},
language: {
@@ -39,8 +41,12 @@ export const CodeInterpreterManifest: BuiltinToolManifest = {
enum: ['python', 'javascript', 'typescript'],
type: 'string',
},
code: {
description: 'The code to execute',
type: 'string',
},
},
required: ['code'],
required: ['description', 'language', 'code'],
type: 'object',
},
},
+38
View File
@@ -0,0 +1,38 @@
import { LocalSystemManifest } from '@lobechat/builtin-tool-local-system';
import { type BuiltinInspector } from '@lobechat/types';
import { CodeInterpreterInspectors } from './code-interpreter/Inspector';
import { CodeInterpreterIdentifier } from './code-interpreter/index';
import { LocalSystemInspectors } from './local-system/Inspector';
import { WebBrowsingInspectors } from './web-browsing/Inspector';
import { WebBrowsingManifest } from './web-browsing/index';
/**
* Builtin tools inspector registry
* Organized by toolset (identifier) -> API name
*
* Inspector components are used to customize the title/header area
* of tool calls in the conversation UI.
*/
const BuiltinToolInspectors: Record<string, Record<string, BuiltinInspector>> = {
[CodeInterpreterIdentifier]: CodeInterpreterInspectors as Record<string, BuiltinInspector>,
[LocalSystemManifest.identifier]: LocalSystemInspectors as Record<string, BuiltinInspector>,
[WebBrowsingManifest.identifier]: WebBrowsingInspectors as Record<string, BuiltinInspector>,
};
/**
* Get builtin inspector component for a specific API
* @param identifier - Tool identifier (e.g., 'lobe-code-interpreter')
* @param apiName - API name (e.g., 'executeCode')
*/
export const getBuiltinInspector = (
identifier?: string,
apiName?: string,
): BuiltinInspector | undefined => {
if (!identifier || !apiName) return undefined;
const toolset = BuiltinToolInspectors[identifier];
if (!toolset) return undefined;
return toolset[apiName];
};
@@ -0,0 +1,57 @@
'use client';
import { type EditLocalFileState } from '@lobechat/builtin-tool-local-system';
import { type EditLocalFileParams } from '@lobechat/electron-client-ipc';
import { type BuiltinInspectorProps } from '@lobechat/types';
import { Icon } from '@lobehub/ui';
import { createStyles } from 'antd-style';
import { ChevronRight } from 'lucide-react';
import path from 'path-browserify-esm';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
import { shinyTextStylish } from '@/styles/loading';
const useStyles = createStyles(({ css, token }) => ({
content: css`
font-family: ${token.fontFamilyCode};
`,
root: css`
overflow: hidden;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 1;
color: ${token.colorTextDescription};
`,
shinyText: shinyTextStylish(token),
}));
export const EditLocalFileInspector = memo<
BuiltinInspectorProps<EditLocalFileParams, EditLocalFileState>
>(({ args, isLoading }) => {
const { t } = useTranslation('plugin');
const { styles, cx } = useStyles();
// Show filename with parent directory for context
let displayPath = '';
if (args?.file_path) {
const { base, dir } = path.parse(args.file_path);
const parentDir = path.basename(dir);
displayPath = parentDir ? `${parentDir}/${base}` : base;
}
return (
<div className={cx(styles.root, isLoading && styles.shinyText)}>
<span>{t('builtins.lobe-local-system.apiName.editLocalFile')}</span>
{displayPath && (
<>
<Icon icon={ChevronRight} style={{ marginInline: 4 }} />
<span className={styles.content}>{displayPath}</span>
</>
)}
</div>
);
});
EditLocalFileInspector.displayName = 'EditLocalFileInspector';
@@ -0,0 +1,61 @@
'use client';
import { type GlobFilesState } from '@lobechat/builtin-tool-local-system';
import { type GlobFilesParams } from '@lobechat/electron-client-ipc';
import { type BuiltinInspectorProps } from '@lobechat/types';
import { Icon } from '@lobehub/ui';
import { createStyles } from 'antd-style';
import { ChevronRight } from 'lucide-react';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
import { shinyTextStylish } from '@/styles/loading';
const useStyles = createStyles(({ css, token }) => ({
content: css`
font-family: ${token.fontFamilyCode};
`,
root: css`
overflow: hidden;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 1;
color: ${token.colorTextDescription};
`,
shinyText: shinyTextStylish(token),
}));
export const GlobLocalFilesInspector = memo<BuiltinInspectorProps<GlobFilesParams, GlobFilesState>>(
({ args, isLoading }) => {
const { t } = useTranslation('plugin');
const { styles, cx } = useStyles();
const pattern = args?.pattern || '';
// When loading, show "本地系统 > 匹配搜索文件"
if (isLoading) {
return (
<div className={cx(styles.root, styles.shinyText)}>
<span>{t('builtins.lobe-local-system.title')}</span>
<Icon icon={ChevronRight} style={{ marginInline: 4 }} />
<span>{t('builtins.lobe-local-system.apiName.globLocalFiles')}</span>
</div>
);
}
return (
<div className={styles.root}>
<span>{t('builtins.lobe-local-system.apiName.globLocalFiles')}</span>
{pattern && (
<>
<Icon icon={ChevronRight} style={{ marginInline: 4 }} />
<span className={styles.content}>{pattern}</span>
</>
)}
</div>
);
},
);
GlobLocalFilesInspector.displayName = 'GlobLocalFilesInspector';
@@ -0,0 +1,61 @@
'use client';
import { type GrepContentState } from '@lobechat/builtin-tool-local-system';
import { type GrepContentParams } from '@lobechat/electron-client-ipc';
import { type BuiltinInspectorProps } from '@lobechat/types';
import { Icon } from '@lobehub/ui';
import { createStyles } from 'antd-style';
import { ChevronRight } from 'lucide-react';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
import { shinyTextStylish } from '@/styles/loading';
const useStyles = createStyles(({ css, token }) => ({
content: css`
font-family: ${token.fontFamilyCode};
`,
root: css`
overflow: hidden;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 1;
color: ${token.colorTextDescription};
`,
shinyText: shinyTextStylish(token),
}));
export const GrepContentInspector = memo<
BuiltinInspectorProps<GrepContentParams, GrepContentState>
>(({ args, isLoading }) => {
const { t } = useTranslation('plugin');
const { styles, cx } = useStyles();
const pattern = args?.pattern || '';
// When loading, show "本地系统 > 搜索内容"
if (isLoading) {
return (
<div className={cx(styles.root, styles.shinyText)}>
<span>{t('builtins.lobe-local-system.title')}</span>
<Icon icon={ChevronRight} style={{ marginInline: 4 }} />
<span>{t('builtins.lobe-local-system.apiName.grepContent')}</span>
</div>
);
}
return (
<div className={styles.root}>
<span>{t('builtins.lobe-local-system.apiName.grepContent')}</span>
{pattern && (
<>
<Icon icon={ChevronRight} style={{ marginInline: 4 }} />
<span className={styles.content}>{pattern}</span>
</>
)}
</div>
);
});
GrepContentInspector.displayName = 'GrepContentInspector';
@@ -0,0 +1,57 @@
'use client';
import type { LocalReadFileState } from '@lobechat/builtin-tool-local-system';
import { type LocalReadFileParams } from '@lobechat/electron-client-ipc';
import { type BuiltinInspectorProps } from '@lobechat/types';
import { Icon } from '@lobehub/ui';
import { createStyles } from 'antd-style';
import { ChevronRight } from 'lucide-react';
import path from 'path-browserify-esm';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
import { shinyTextStylish } from '@/styles/loading';
const useStyles = createStyles(({ css, token }) => ({
content: css`
font-family: ${token.fontFamilyCode};
`,
root: css`
overflow: hidden;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 1;
color: ${token.colorTextDescription};
`,
shinyText: shinyTextStylish(token),
}));
export const ReadLocalFileInspector = memo<
BuiltinInspectorProps<LocalReadFileParams, LocalReadFileState>
>(({ args, isLoading }) => {
const { t } = useTranslation('plugin');
const { styles, cx } = useStyles();
// Show filename with parent directory for context
let displayPath = '';
if (args?.path) {
const { base, dir } = path.parse(args.path);
const parentDir = path.basename(dir);
displayPath = parentDir ? `${parentDir}/${base}` : base;
}
return (
<div className={cx(styles.root, isLoading && styles.shinyText)}>
<span>{t('builtins.lobe-local-system.apiName.readLocalFile')}</span>
{displayPath && (
<>
<Icon icon={ChevronRight} style={{ marginInline: 4 }} />
<span className={styles.content}>{displayPath}</span>
</>
)}
</div>
);
});
ReadLocalFileInspector.displayName = 'ReadLocalFileInspector';
@@ -0,0 +1,68 @@
'use client';
import { type RunCommandParams, type RunCommandResult } from '@lobechat/electron-client-ipc';
import { type BuiltinInspectorProps } from '@lobechat/types';
import { Icon } from '@lobehub/ui';
import { createStyles } from 'antd-style';
import { ChevronRight } from 'lucide-react';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
import { shinyTextStylish } from '@/styles/loading';
const useStyles = createStyles(({ css, token }) => ({
content: css`
font-family: ${token.fontFamilyCode};
`,
root: css`
overflow: hidden;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 1;
color: ${token.colorTextDescription};
`,
shinyText: shinyTextStylish(token),
}));
interface RunCommandState {
message: string;
result: RunCommandResult;
}
export const RunCommandInspector = memo<BuiltinInspectorProps<RunCommandParams, RunCommandState>>(
({ args, isLoading }) => {
const { t } = useTranslation('plugin');
const { styles, cx } = useStyles();
// Show description if available, otherwise show command
const displayText = args?.description || args?.command || '';
// When loading, show "Local System > 执行命令"
if (isLoading) {
return (
<div className={cx(styles.root, styles.shinyText)}>
<span>{t('builtins.lobe-local-system.title')}</span>
<Icon icon={ChevronRight} style={{ marginInline: 4 }} />
<span>{t('builtins.lobe-local-system.apiName.runCommand')}</span>
</div>
);
}
return (
<div className={styles.root}>
<span>{t('builtins.lobe-local-system.apiName.runCommand')}</span>
{displayText && (
<>
<Icon icon={ChevronRight} style={{ marginInline: 4 }} />
<span className={styles.content}>{displayText}</span>
</>
)}
</div>
);
},
);
RunCommandInspector.displayName = 'RunCommandInspector';
export default RunCommandInspector;
@@ -0,0 +1,61 @@
'use client';
import { type LocalFileSearchState } from '@lobechat/builtin-tool-local-system';
import { type LocalSearchFilesParams } from '@lobechat/electron-client-ipc';
import { type BuiltinInspectorProps } from '@lobechat/types';
import { Icon } from '@lobehub/ui';
import { createStyles } from 'antd-style';
import { ChevronRight } from 'lucide-react';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
import { shinyTextStylish } from '@/styles/loading';
const useStyles = createStyles(({ css, token }) => ({
content: css`
font-family: ${token.fontFamilyCode};
`,
root: css`
overflow: hidden;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 1;
color: ${token.colorTextDescription};
`,
shinyText: shinyTextStylish(token),
}));
export const SearchLocalFilesInspector = memo<
BuiltinInspectorProps<LocalSearchFilesParams, LocalFileSearchState>
>(({ args, isLoading }) => {
const { t } = useTranslation('plugin');
const { styles, cx } = useStyles();
const keywords = args?.keywords || '';
// When loading, show "本地系统 > 搜索文件"
if (isLoading) {
return (
<div className={cx(styles.root, styles.shinyText)}>
<span>{t('builtins.lobe-local-system.title')}</span>
<Icon icon={ChevronRight} style={{ marginInline: 4 }} />
<span>{t('builtins.lobe-local-system.apiName.searchLocalFiles')}</span>
</div>
);
}
return (
<div className={styles.root}>
<span>{t('builtins.lobe-local-system.apiName.searchLocalFiles')}</span>
{keywords && (
<>
<Icon icon={ChevronRight} style={{ marginInline: 4 }} />
<span className={styles.content}>{keywords}</span>
</>
)}
</div>
);
});
SearchLocalFilesInspector.displayName = 'SearchLocalFilesInspector';
+20
View File
@@ -0,0 +1,20 @@
import { LocalSystemApiName } from '@lobechat/builtin-tool-local-system';
import { EditLocalFileInspector } from './EditLocalFile';
import { GlobLocalFilesInspector } from './GlobLocalFiles';
import { GrepContentInspector } from './GrepContent';
import { ReadLocalFileInspector } from './ReadLocalFile';
import { RunCommandInspector } from './RunCommand';
import { SearchLocalFilesInspector } from './SearchLocalFiles';
/**
* Local System Inspector Components Registry
*/
export const LocalSystemInspectors = {
[LocalSystemApiName.editLocalFile]: EditLocalFileInspector,
[LocalSystemApiName.globLocalFiles]: GlobLocalFilesInspector,
[LocalSystemApiName.grepContent]: GrepContentInspector,
[LocalSystemApiName.readLocalFile]: ReadLocalFileInspector,
[LocalSystemApiName.runCommand]: RunCommandInspector,
[LocalSystemApiName.searchLocalFiles]: SearchLocalFilesInspector,
};
@@ -1,10 +1,8 @@
import { CheckCircleFilled, CloseCircleFilled } from '@ant-design/icons';
import { type RunCommandParams, type RunCommandResult } from '@lobechat/electron-client-ipc';
import { type BuiltinRenderProps } from '@lobechat/types';
import { ActionIcon, Block, Flexbox, Highlighter, Text } from '@lobehub/ui';
import { Block, Flexbox, Highlighter } from '@lobehub/ui';
import { createStyles } from 'antd-style';
import { ChevronDown, ChevronUp } from 'lucide-react';
import { memo, useState } from 'react';
import { memo } from 'react';
const useStyles = createStyles(({ css, token }) => ({
container: css`
@@ -39,69 +37,27 @@ interface RunCommandState {
const RunCommand = memo<BuiltinRenderProps<RunCommandParams, RunCommandState>>(
({ args, pluginState }) => {
const { styles, theme } = useStyles();
const { result, message } = pluginState || {};
const isSuccess = result?.success;
const [expanded, setExpanded] = useState(false);
const { styles } = useStyles();
const { result } = pluginState || {};
return (
<Flexbox className={styles.container} gap={8}>
{/* Header: Description + Status */}
<Flexbox align={'center'} className={styles.header} horizontal justify={'space-between'}>
<Flexbox gap={8} horizontal>
<Flexbox gap={4} horizontal>
{!result ? null : isSuccess ? (
<CheckCircleFilled
className={styles.statusIcon}
style={{ color: theme.colorSuccess }}
/>
) : (
<CloseCircleFilled
className={styles.statusIcon}
style={{ color: theme.colorError }}
/>
)}
{args.description && <Text className={styles.head}>{args.description}</Text>}
</Flexbox>
{message && (
<Flexbox align={'center'} gap={4} horizontal>
<Text className={styles.head} type={'secondary'}>
{message}
</Text>
</Flexbox>
)}
</Flexbox>
<Flexbox align={'center'} gap={8} horizontal>
<ActionIcon
className={`action-icon`}
icon={expanded ? ChevronUp : ChevronDown}
onClick={() => setExpanded(!expanded)}
size={'small'}
style={{ opacity: expanded ? 1 : undefined }}
title={expanded ? 'Collapse' : 'Expand'}
/>
</Flexbox>
</Flexbox>
{/* Command & Output */}
{expanded && (
<Block gap={8} padding={8} variant={'outlined'}>
<Highlighter
language={'sh'}
showLanguage={false}
style={{ paddingInline: 8 }}
variant={'borderless'}
wrap
>
{args.command}
<Block gap={8} padding={8} variant={'outlined'}>
<Highlighter
language={'sh'}
showLanguage={false}
style={{ paddingInline: 8 }}
variant={'borderless'}
wrap
>
{args.command}
</Highlighter>
{result?.output && (
<Highlighter language={'text'} showLanguage={false} variant={'filled'} wrap>
{result.output}
</Highlighter>
{result?.output && (
<Highlighter language={'text'} showLanguage={false} variant={'filled'} wrap>
{result.output}
</Highlighter>
)}
</Block>
)}
)}
</Block>
</Flexbox>
);
},
@@ -0,0 +1,33 @@
'use client';
import { type BuiltinStreamingProps } from '@lobechat/types';
import { Highlighter } from '@lobehub/ui';
import { memo } from 'react';
interface RunCommandParams {
command?: string;
description?: string;
timeout?: number;
}
export const RunCommandStreaming = memo<BuiltinStreamingProps<RunCommandParams>>(({ args }) => {
const { command } = args || {};
// Don't render if no command yet
if (!command) return null;
return (
<Highlighter
animated
language={'sh'}
showLanguage={false}
style={{ padding: '4px 8px' }}
variant={'outlined'}
wrap
>
{command}
</Highlighter>
);
});
RunCommandStreaming.displayName = 'RunCommandStreaming';
+10
View File
@@ -0,0 +1,10 @@
import { LocalSystemApiName } from '@lobechat/builtin-tool-local-system';
import { RunCommandStreaming } from './RunCommand';
/**
* Local System Streaming Components Registry
*/
export const LocalSystemStreamings = {
[LocalSystemApiName.runCommand]: RunCommandStreaming,
};
+36
View File
@@ -0,0 +1,36 @@
import { LocalSystemManifest } from '@lobechat/builtin-tool-local-system';
import { type BuiltinStreaming } from '@lobechat/types';
import { CodeInterpreterIdentifier } from './code-interpreter';
import { CodeInterpreterStreamings } from './code-interpreter/Streaming';
import { LocalSystemStreamings } from './local-system/Streaming';
/**
* Builtin tools streaming renderer registry
* Organized by toolset (identifier) -> API name
*
* Streaming components are used to render tool calls while they are
* still executing, allowing real-time feedback to users.
* The component should fetch streaming content from store internally.
*/
const BuiltinToolStreamings: Record<string, Record<string, BuiltinStreaming>> = {
[CodeInterpreterIdentifier]: CodeInterpreterStreamings as Record<string, BuiltinStreaming>,
[LocalSystemManifest.identifier]: LocalSystemStreamings as Record<string, BuiltinStreaming>,
};
/**
* Get builtin streaming component for a specific API
* @param identifier - Tool identifier (e.g., 'lobe-code-interpreter')
* @param apiName - API name (e.g., 'executeCode')
*/
export const getBuiltinStreaming = (
identifier?: string,
apiName?: string,
): BuiltinStreaming | undefined => {
if (!identifier || !apiName) return undefined;
const toolset = BuiltinToolStreamings[identifier];
if (!toolset) return undefined;
return toolset[apiName];
};
@@ -0,0 +1,75 @@
'use client';
import { type BuiltinInspectorProps } from '@lobechat/types';
import { Icon } from '@lobehub/ui';
import { createStyles } from 'antd-style';
import { ChevronRight } from 'lucide-react';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
import { shinyTextStylish } from '@/styles/loading';
const useStyles = createStyles(({ css, token }) => ({
content: css`
font-family: ${token.fontFamilyCode};
`,
root: css`
overflow: hidden;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 1;
color: ${token.colorTextDescription};
`,
shinyText: shinyTextStylish(token),
}));
interface CrawlMultiPagesParams {
urls: string[];
}
export const CrawlMultiPagesInspector = memo<BuiltinInspectorProps<CrawlMultiPagesParams>>(
({ args, isLoading }) => {
const { t } = useTranslation('plugin');
const { styles, cx } = useStyles();
// Show count and first domain for context
let displayText = '';
if (args?.urls && args.urls.length > 0) {
const count = args.urls.length;
try {
const firstUrl = new URL(args.urls[0]);
displayText = count > 1 ? `${firstUrl.hostname} +${count - 1}` : firstUrl.hostname;
} catch {
displayText = `${count} pages`;
}
}
// When loading, show "联网搜索 > 读取多个页面内容"
if (isLoading) {
return (
<div className={cx(styles.root, styles.shinyText)}>
<span>{t('builtins.lobe-web-browsing.title')}</span>
<Icon icon={ChevronRight} style={{ marginInline: 4 }} />
<span>{t('builtins.lobe-web-browsing.apiName.crawlMultiPages')}</span>
</div>
);
}
return (
<div className={styles.root}>
<span>{t('builtins.lobe-web-browsing.apiName.crawlMultiPages')}</span>
{displayText && (
<>
<Icon icon={ChevronRight} style={{ marginInline: 4 }} />
<span className={styles.content}>{displayText}</span>
</>
)}
</div>
);
},
);
CrawlMultiPagesInspector.displayName = 'CrawlMultiPagesInspector';
export default CrawlMultiPagesInspector;
@@ -0,0 +1,63 @@
'use client';
import type { BuiltinInspectorProps } from '@lobechat/types';
import { Icon } from '@lobehub/ui';
import { createStyles } from 'antd-style';
import { ChevronRight } from 'lucide-react';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
import { shinyTextStylish } from '@/styles/loading';
const useStyles = createStyles(({ css, token }) => ({
content: css`
font-family: ${token.fontFamilyCode};
`,
root: css`
overflow: hidden;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 1;
color: ${token.colorTextDescription};
`,
shinyText: shinyTextStylish(token),
}));
interface CrawlSinglePageParams {
url: string;
}
export const CrawlSinglePageInspector = memo<BuiltinInspectorProps<CrawlSinglePageParams>>(
({ args, isLoading }) => {
const { t } = useTranslation('plugin');
const { styles, cx } = useStyles();
// When loading, show "联网搜索 > 读取页面内容"
if (isLoading) {
return (
<div className={cx(styles.root, styles.shinyText)}>
<span>{t('builtins.lobe-web-browsing.title')}</span>
<Icon icon={ChevronRight} style={{ marginInline: 4 }} />
<span>{t('builtins.lobe-web-browsing.apiName.crawlSinglePage')}</span>
</div>
);
}
return (
<div className={styles.root}>
<span>{t('builtins.lobe-web-browsing.apiName.crawlSinglePage')}</span>
{args.url && (
<>
<Icon icon={ChevronRight} style={{ marginInline: 4 }} />
<span className={styles.content}>{args.url}</span>
</>
)}
</div>
);
},
);
CrawlSinglePageInspector.displayName = 'CrawlSinglePageInspector';
export default CrawlSinglePageInspector;
@@ -0,0 +1,59 @@
'use client';
import { type BuiltinInspectorProps, type SearchQuery } from '@lobechat/types';
import { Icon } from '@lobehub/ui';
import { createStyles } from 'antd-style';
import { ChevronRight } from 'lucide-react';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
import { shinyTextStylish } from '@/styles/loading';
const useStyles = createStyles(({ css, token }) => ({
content: css`
font-family: ${token.fontFamilyCode};
`,
root: css`
overflow: hidden;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 1;
color: ${token.colorTextDescription};
`,
shinyText: shinyTextStylish(token),
}));
export const SearchInspector = memo<BuiltinInspectorProps<SearchQuery>>(({ args, isLoading }) => {
const { t } = useTranslation('plugin');
const { styles, cx } = useStyles();
const query = args?.query || '';
// When loading, show "联网搜索 > 搜索页面"
if (isLoading) {
return (
<div className={cx(styles.root, styles.shinyText)}>
<span>{t('builtins.lobe-web-browsing.title')}</span>
<Icon icon={ChevronRight} style={{ marginInline: 4 }} />
<span>{t('builtins.lobe-web-browsing.apiName.search')}</span>
</div>
);
}
return (
<div className={styles.root}>
<span>{t('builtins.lobe-web-browsing.apiName.search')}</span>
{query && (
<>
<Icon icon={ChevronRight} style={{ marginInline: 4 }} />
<span className={styles.content}>{query}</span>
</>
)}
</div>
);
});
SearchInspector.displayName = 'SearchInspector';
export default SearchInspector;
+13
View File
@@ -0,0 +1,13 @@
import { WebBrowsingApiName } from '../index';
import { CrawlMultiPagesInspector } from './CrawlMultiPages';
import { CrawlSinglePageInspector } from './CrawlSinglePage';
import { SearchInspector } from './Search';
/**
* Web Browsing Inspector Components Registry
*/
export const WebBrowsingInspectors = {
[WebBrowsingApiName.crawlMultiPages]: CrawlMultiPagesInspector,
[WebBrowsingApiName.crawlSinglePage]: CrawlSinglePageInspector,
[WebBrowsingApiName.search]: SearchInspector,
};