💄 style: improve loading and local-system render (#11087)

* 💄 style: improve loading

* ♻️ refactor: move local-system to builtin-tool-local-system package

* update

* remove focusThrottleInterval
This commit is contained in:
Arvin Xu
2026-01-01 13:24:17 +08:00
committed by GitHub
parent ee48742f7b
commit 44630bcfe4
55 changed files with 726 additions and 410 deletions
@@ -4,6 +4,7 @@
"private": true,
"exports": {
".": "./src/index.ts",
"./client": "./src/client/index.ts",
"./executionRuntime": "./src/ExecutionRuntime/index.ts"
},
"main": "./src/index.ts",
@@ -12,5 +13,14 @@
},
"devDependencies": {
"@lobechat/types": "workspace:*"
},
"peerDependencies": {
"@lobehub/ui": "^4",
"antd": "^6",
"antd-style": "*",
"lucide-react": "*",
"path-browserify-esm": "*",
"react": "*",
"react-i18next": "*"
}
}
@@ -0,0 +1,81 @@
'use client';
import { type EditLocalFileParams } from '@lobechat/electron-client-ipc';
import { type BuiltinInspectorProps } from '@lobechat/types';
import { createStaticStyles, cssVar, cx } from 'antd-style';
import { Check, X } from 'lucide-react';
import path from 'path-browserify-esm';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
import { highlightTextStyles, shinyTextStyles } from '@/styles';
import { type EditLocalFileState } 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};
`,
statusIcon: css`
margin-block-end: -2px;
margin-inline-start: 4px;
`,
}));
export const EditLocalFileInspector = memo<
BuiltinInspectorProps<EditLocalFileParams, EditLocalFileState>
>(({ args, partialArgs, isArgumentsStreaming, pluginState, isLoading }) => {
const { t } = useTranslation('plugin');
// Show filename with parent directory for context
const filePath = args?.file_path || partialArgs?.file_path || '';
let displayPath = '';
if (filePath) {
const { base, dir } = path.parse(filePath);
const parentDir = path.basename(dir);
displayPath = parentDir ? `${parentDir}/${base}` : base;
}
// During argument streaming
if (isArgumentsStreaming) {
if (!displayPath)
return (
<div className={cx(styles.root, shinyTextStyles.shinyText)}>
<span>{t('builtins.lobe-local-system.apiName.editLocalFile')}</span>
</div>
);
return (
<div className={cx(styles.root, shinyTextStyles.shinyText)}>
<span>{t('builtins.lobe-local-system.apiName.editLocalFile')}: </span>
<span className={highlightTextStyles.primary}>{displayPath}</span>
</div>
);
}
// Check if edit was successful (has replacements count)
const isSuccess = pluginState?.replacements !== undefined && pluginState.replacements >= 0;
return (
<div className={cx(styles.root, isLoading && shinyTextStyles.shinyText)}>
<span style={{ marginInlineStart: 2 }}>
<span>{t('builtins.lobe-local-system.apiName.editLocalFile')}: </span>
{displayPath && <span className={highlightTextStyles.primary}>{displayPath}</span>}
{isLoading ? null : pluginState ? (
isSuccess ? (
<Check className={styles.statusIcon} color={cssVar.colorSuccess} size={14} />
) : (
<X className={styles.statusIcon} color={cssVar.colorError} size={14} />
)
) : null}
</span>
</div>
);
});
EditLocalFileInspector.displayName = 'EditLocalFileInspector';
@@ -0,0 +1,73 @@
'use client';
import { type GlobFilesParams } from '@lobechat/electron-client-ipc';
import { type BuiltinInspectorProps } from '@lobechat/types';
import { createStaticStyles, cssVar, cx } from 'antd-style';
import { Check, X } from 'lucide-react';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
import { highlightTextStyles, shinyTextStyles } from '@/styles';
import { type GlobFilesState } from '../../..';
const styles = createStaticStyles(({ css, cssVar }) => ({
root: css`
overflow: hidden;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 1;
color: ${cssVar.colorTextSecondary};
`,
statusIcon: css`
margin-block-end: -2px;
margin-inline-start: 4px;
`,
}));
export const GlobLocalFilesInspector = memo<BuiltinInspectorProps<GlobFilesParams, GlobFilesState>>(
({ args, partialArgs, isArgumentsStreaming, pluginState, isLoading }) => {
const { t } = useTranslation('plugin');
const pattern = args?.pattern || partialArgs?.pattern || '';
// During argument streaming
if (isArgumentsStreaming) {
if (!pattern)
return (
<div className={cx(styles.root, shinyTextStyles.shinyText)}>
<span>{t('builtins.lobe-local-system.apiName.globLocalFiles')}</span>
</div>
);
return (
<div className={cx(styles.root, shinyTextStyles.shinyText)}>
<span>{t('builtins.lobe-local-system.apiName.globLocalFiles')}: </span>
<span className={highlightTextStyles.primary}>{pattern}</span>
</div>
);
}
// Check if glob was successful
const isSuccess = pluginState?.result?.success;
return (
<div className={cx(styles.root, isLoading && shinyTextStyles.shinyText)}>
<span style={{ marginInlineStart: 2 }}>
<span>{t('builtins.lobe-local-system.apiName.globLocalFiles')}: </span>
{pattern && <span className={highlightTextStyles.primary}>{pattern}</span>}
{isLoading ? null : pluginState?.result ? (
isSuccess ? (
<Check className={styles.statusIcon} color={cssVar.colorSuccess} size={14} />
) : (
<X className={styles.statusIcon} color={cssVar.colorError} size={14} />
)
) : null}
</span>
</div>
);
},
);
GlobLocalFilesInspector.displayName = 'GlobLocalFilesInspector';
@@ -0,0 +1,73 @@
'use client';
import { type GrepContentParams } from '@lobechat/electron-client-ipc';
import { type BuiltinInspectorProps } from '@lobechat/types';
import { createStaticStyles, cssVar, cx } from 'antd-style';
import { Check, X } from 'lucide-react';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
import { highlightTextStyles, shinyTextStyles } from '@/styles';
import { type GrepContentState } from '../../..';
const styles = createStaticStyles(({ css, cssVar }) => ({
root: css`
overflow: hidden;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 1;
color: ${cssVar.colorTextSecondary};
`,
statusIcon: css`
margin-block-end: -2px;
margin-inline-start: 4px;
`,
}));
export const GrepContentInspector = memo<
BuiltinInspectorProps<GrepContentParams, GrepContentState>
>(({ args, partialArgs, isArgumentsStreaming, pluginState, isLoading }) => {
const { t } = useTranslation('plugin');
const pattern = args?.pattern || partialArgs?.pattern || '';
// During argument streaming
if (isArgumentsStreaming) {
if (!pattern)
return (
<div className={cx(styles.root, shinyTextStyles.shinyText)}>
<span>{t('builtins.lobe-local-system.apiName.grepContent')}</span>
</div>
);
return (
<div className={cx(styles.root, shinyTextStyles.shinyText)}>
<span>{t('builtins.lobe-local-system.apiName.grepContent')}: </span>
<span className={highlightTextStyles.primary}>{pattern}</span>
</div>
);
}
// Check if grep was successful
const isSuccess = pluginState?.result?.success;
return (
<div className={cx(styles.root, isLoading && shinyTextStyles.shinyText)}>
<span style={{ marginInlineStart: 2 }}>
<span>{t('builtins.lobe-local-system.apiName.grepContent')}: </span>
{pattern && <span className={highlightTextStyles.primary}>{pattern}</span>}
{isLoading ? null : pluginState?.result ? (
isSuccess ? (
<Check className={styles.statusIcon} color={cssVar.colorSuccess} size={14} />
) : (
<X className={styles.statusIcon} color={cssVar.colorError} size={14} />
)
) : null}
</span>
</div>
);
});
GrepContentInspector.displayName = 'GrepContentInspector';
@@ -0,0 +1,81 @@
'use client';
import { type LocalReadFileParams } from '@lobechat/electron-client-ipc';
import { type BuiltinInspectorProps } from '@lobechat/types';
import { createStaticStyles, cssVar, cx } from 'antd-style';
import { Check, X } from 'lucide-react';
import path from 'path-browserify-esm';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
import { highlightTextStyles, shinyTextStyles } from '@/styles';
import { type LocalReadFileState } from '../../..';
const styles = createStaticStyles(({ css, cssVar }) => ({
root: css`
overflow: hidden;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 1;
color: ${cssVar.colorTextSecondary};
`,
statusIcon: css`
margin-block-end: -2px;
margin-inline-start: 4px;
`,
}));
export const ReadLocalFileInspector = memo<
BuiltinInspectorProps<LocalReadFileParams, LocalReadFileState>
>(({ args, partialArgs, isArgumentsStreaming, pluginState, isLoading }) => {
const { t } = useTranslation('plugin');
// Show filename with parent directory for context
const filePath = args?.path || partialArgs?.path || '';
let displayPath = '';
if (filePath) {
const { base, dir } = path.parse(filePath);
const parentDir = path.basename(dir);
displayPath = parentDir ? `${parentDir}/${base}` : base;
}
// During argument streaming
if (isArgumentsStreaming) {
if (!displayPath)
return (
<div className={cx(styles.root, shinyTextStyles.shinyText)}>
<span>{t('builtins.lobe-local-system.apiName.readLocalFile')}</span>
</div>
);
return (
<div className={cx(styles.root, shinyTextStyles.shinyText)}>
<span>{t('builtins.lobe-local-system.apiName.readLocalFile')}: </span>
<span className={highlightTextStyles.primary}>{displayPath}</span>
</div>
);
}
// Check if file was read successfully (has content)
const hasContent = !!pluginState?.fileContent;
return (
<div className={cx(styles.root, isLoading && shinyTextStyles.shinyText)}>
<span style={{ marginInlineStart: 2 }}>
<span>{t('builtins.lobe-local-system.apiName.readLocalFile')}: </span>
{displayPath && <span className={highlightTextStyles.primary}>{displayPath}</span>}
{isLoading ? null : pluginState ? (
hasContent ? (
<Check className={styles.statusIcon} color={cssVar.colorSuccess} size={14} />
) : (
<X className={styles.statusIcon} color={cssVar.colorError} size={14} />
)
) : null}
</span>
</div>
);
});
ReadLocalFileInspector.displayName = 'ReadLocalFileInspector';
@@ -0,0 +1,80 @@
'use client';
import { type RunCommandParams, type RunCommandResult } from '@lobechat/electron-client-ipc';
import { type BuiltinInspectorProps } from '@lobechat/types';
import { createStaticStyles, cssVar, cx } from 'antd-style';
import { Check, X } from 'lucide-react';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
import { highlightTextStyles, shinyTextStyles } from '@/styles';
const styles = createStaticStyles(({ css, cssVar }) => ({
root: css`
overflow: hidden;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 1;
color: ${cssVar.colorTextSecondary};
`,
statusIcon: css`
margin-block-end: -2px;
margin-inline-start: 4px;
`,
}));
interface RunCommandState {
message: string;
result: RunCommandResult;
}
export const RunCommandInspector = memo<BuiltinInspectorProps<RunCommandParams, RunCommandState>>(
({ args, partialArgs, isArgumentsStreaming, pluginState, isLoading }) => {
const { t } = useTranslation('plugin');
// Show description if available, otherwise show command
const description = args?.description || partialArgs?.description || args?.command || '';
// During argument streaming
if (isArgumentsStreaming) {
if (!description)
return (
<div className={cx(styles.root, shinyTextStyles.shinyText)}>
<span>{t('builtins.lobe-local-system.apiName.runCommand')}</span>
</div>
);
return (
<div className={cx(styles.root, shinyTextStyles.shinyText)}>
<span>{t('builtins.lobe-local-system.apiName.runCommand')}: </span>
<span className={highlightTextStyles.primary}>{description}</span>
</div>
);
}
// Get execution result from pluginState
const result = pluginState?.result;
const isSuccess = result?.success && result?.exit_code === 0;
return (
<div className={cx(styles.root, isLoading && shinyTextStyles.shinyText)}>
<span style={{ marginInlineStart: 2 }}>
<span>{t('builtins.lobe-local-system.apiName.runCommand')}: </span>
{description && <span className={highlightTextStyles.primary}>{description}</span>}
{isLoading ? null : result?.success !== undefined ? (
isSuccess ? (
<Check className={styles.statusIcon} color={cssVar.colorSuccess} size={14} />
) : (
<X className={styles.statusIcon} color={cssVar.colorError} size={14} />
)
) : null}
</span>
</div>
);
},
);
RunCommandInspector.displayName = 'RunCommandInspector';
export default RunCommandInspector;
@@ -0,0 +1,71 @@
'use client';
import { type LocalSearchFilesParams } from '@lobechat/electron-client-ipc';
import { type BuiltinInspectorProps } from '@lobechat/types';
import { createStaticStyles, cssVar, cx } from 'antd-style';
import { Check } from 'lucide-react';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
import { highlightTextStyles, shinyTextStyles } from '@/styles';
import { type LocalFileSearchState } from '../../..';
const styles = createStaticStyles(({ css, cssVar }) => ({
root: css`
overflow: hidden;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 1;
color: ${cssVar.colorTextSecondary};
`,
statusIcon: css`
margin-block-end: -2px;
margin-inline-start: 4px;
`,
}));
export const SearchLocalFilesInspector = memo<
BuiltinInspectorProps<LocalSearchFilesParams, LocalFileSearchState>
>(({ args, partialArgs, isArgumentsStreaming, pluginState, isLoading }) => {
const { t } = useTranslation('plugin');
const keywords = args?.keywords || partialArgs?.keywords || '';
// During argument streaming
if (isArgumentsStreaming) {
if (!keywords)
return (
<div className={cx(styles.root, shinyTextStyles.shinyText)}>
<span>{t('builtins.lobe-local-system.apiName.searchLocalFiles')}</span>
</div>
);
return (
<div className={cx(styles.root, shinyTextStyles.shinyText)}>
<span>{t('builtins.lobe-local-system.apiName.searchLocalFiles')}: </span>
<span className={highlightTextStyles.primary}>{keywords}</span>
</div>
);
}
// Check if search returned results
const hasResults = pluginState?.searchResults && pluginState.searchResults.length >= 0;
return (
<div className={cx(styles.root, isLoading && shinyTextStyles.shinyText)}>
<span style={{ marginInlineStart: 2 }}>
<span>{t('builtins.lobe-local-system.apiName.searchLocalFiles')}: </span>
{keywords && <span className={highlightTextStyles.primary}>{keywords}</span>}
{isLoading ? null : pluginState?.searchResults ? (
hasResults ? (
<Check className={styles.statusIcon} color={cssVar.colorSuccess} size={14} />
) : null
) : null}
</span>
</div>
);
});
SearchLocalFilesInspector.displayName = 'SearchLocalFilesInspector';
@@ -1,5 +1,4 @@
import { LocalSystemApiName } from '@lobechat/builtin-tool-local-system';
import { LocalSystemApiName } from '../..';
import { EditLocalFileInspector } from './EditLocalFile';
import { GlobLocalFilesInspector } from './GlobLocalFiles';
import { GrepContentInspector } from './GrepContent';
@@ -1,5 +1,4 @@
import { LocalSystemApiName } from '@lobechat/builtin-tool-local-system';
import { LocalSystemApiName } from '../..';
import EditLocalFile from './EditLocalFile';
import MoveLocalFiles from './MoveLocalFiles';
import RunCommand from './RunCommand';
@@ -5,7 +5,8 @@ import { memo } from 'react';
import { useChatStore } from '@/store/chat';
import { chatToolSelectors } from '@/store/chat/selectors';
import FileItem from '@/tools/local-system/components/FileItem';
import FileItem from '../../components/FileItem';
interface SearchFilesProps {
listResults?: LocalFileItem[];
@@ -1,5 +1,4 @@
import { LocalSystemApiName } from '@lobechat/builtin-tool-local-system';
import { LocalSystemApiName } from '../..';
import EditLocalFile from './EditLocalFile';
import ListFiles from './ListFiles';
import MoveLocalFiles from './MoveLocalFiles';
@@ -1,5 +1,4 @@
import { LocalSystemApiName } from '@lobechat/builtin-tool-local-system';
import { LocalSystemApiName } from '../..';
import { RunCommandStreaming } from './RunCommand';
/**
@@ -0,0 +1,20 @@
// Inspector components (customized tool call headers)
export { LocalSystemInspectors } from './Inspector';
// Render components (read-only snapshots)
export { LocalSystemRenders } from './Render';
// Intervention components (approval dialogs)
export { LocalSystemInterventions } from './Intervention';
// Streaming components
export { LocalSystemStreamings } from './Streaming';
// Placeholder components
export { ListFiles as LocalSystemListFilesPlaceholder } from './Placeholder/ListFiles';
export { default as LocalSystemSearchFilesPlaceholder } from './Placeholder/SearchFiles';
// Re-export types and manifest for convenience
export { LocalSystemManifest } from '../manifest';
export { LocalSystemIdentifier } from '../types';
export * from '../types';
@@ -1,10 +1,10 @@
'use client';
import { AccordionItem, ActionIcon, Dropdown, Flexbox, Text } from '@lobehub/ui';
import { Loader2Icon } from 'lucide-react';
import { AccordionItem, Dropdown, Flexbox, Text } from '@lobehub/ui';
import React, { Suspense, memo } from 'react';
import { useTranslation } from 'react-i18next';
import NeuralNetworkLoading from '@/components/NeuralNetworkLoading';
import SkeletonList from '@/features/NavPanel/components/SkeletonList';
import { useFetchTopics } from '@/hooks/useFetchTopics';
import { useChatStore } from '@/store/chat';
@@ -45,7 +45,7 @@ const Topic = memo<TopicProps>(({ itemKey }) => {
<Text ellipsis fontSize={12} type={'secondary'} weight={500}>
{`${t('title')} ${topicCount > 0 ? topicCount : ''}`}
</Text>
{isRevalidating && <ActionIcon icon={Loader2Icon} loading size={'small'} />}
{isRevalidating && <NeuralNetworkLoading size={14} />}
</Flexbox>
}
>
@@ -1,10 +1,10 @@
'use client';
import { AccordionItem, ActionIcon, Dropdown, Flexbox, Text } from '@lobehub/ui';
import { Loader2Icon } from 'lucide-react';
import { AccordionItem, Dropdown, Flexbox, Text } from '@lobehub/ui';
import React, { Suspense, memo } from 'react';
import { useTranslation } from 'react-i18next';
import NeuralNetworkLoading from '@/components/NeuralNetworkLoading';
import SkeletonList from '@/features/NavPanel/components/SkeletonList';
import { useFetchTopics } from '@/hooks/useFetchTopics';
import { useChatStore } from '@/store/chat';
@@ -45,7 +45,7 @@ const Topic = memo<TopicProps>(({ itemKey }) => {
<Text ellipsis fontSize={12} type={'secondary'} weight={500}>
{`${t('title')} ${topicCount > 0 ? topicCount : ''}`}
</Text>
{isRevalidating && <ActionIcon icon={Loader2Icon} loading size={'small'} />}
{isRevalidating && <NeuralNetworkLoading size={14} />}
</Flexbox>
}
>
@@ -1,10 +1,10 @@
'use client';
import { AccordionItem, ActionIcon, Dropdown, Flexbox, Text } from '@lobehub/ui';
import { Loader2Icon } from 'lucide-react';
import { AccordionItem, Dropdown, Flexbox, Text } from '@lobehub/ui';
import React, { Suspense, memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import NeuralNetworkLoading from '@/components/NeuralNetworkLoading';
import { useFetchAgentList } from '@/hooks/useFetchAgentList';
import SkeletonList from '../../../../../../../features/NavPanel/components/SkeletonList';
@@ -56,7 +56,7 @@ const Agent = memo<AgentProps>(({ itemKey }) => {
<Text ellipsis fontSize={12} type={'secondary'} weight={500}>
{t('navPanel.agent')}
</Text>
{isRevalidating && <ActionIcon icon={Loader2Icon} loading size={'small'} />}
{isRevalidating && <NeuralNetworkLoading size={14} />}
</Flexbox>
}
>
@@ -1,12 +1,13 @@
'use client';
import { ActionIcon, Dropdown } from '@lobehub/ui';
import { FileTextIcon, Loader2Icon, MoreHorizontal } from 'lucide-react';
import { FileTextIcon, MoreHorizontal } from 'lucide-react';
import { Suspense, memo } from 'react';
import { useTranslation } from 'react-i18next';
import { useNavigate } from 'react-router-dom';
import { useResourceManagerStore } from '@/app/[variants]/(main)/resource/features/store';
import NeuralNetworkLoading from '@/components/NeuralNetworkLoading';
import { useInitRecentPage } from '@/hooks/useInitRecentPage';
import { useHomeStore } from '@/store/home';
import { homeRecentSelectors } from '@/store/home/selectors';
@@ -35,7 +36,7 @@ const RecentPage = memo(() => {
<GroupBlock
action={
<>
{isRevalidating && <ActionIcon icon={Loader2Icon} loading size={'small'} />}
{isRevalidating && <NeuralNetworkLoading size={14} />}
<Dropdown
menu={{
items: [
@@ -1,12 +1,13 @@
'use client';
import { ActionIcon, Dropdown } from '@lobehub/ui';
import { Clock, Loader2Icon, MoreHorizontal } from 'lucide-react';
import { Clock, MoreHorizontal } from 'lucide-react';
import { Suspense, memo } from 'react';
import { useTranslation } from 'react-i18next';
import { useNavigate } from 'react-router-dom';
import { useResourceManagerStore } from '@/app/[variants]/(main)/resource/features/store';
import NeuralNetworkLoading from '@/components/NeuralNetworkLoading';
import { useInitRecentResource } from '@/hooks/useInitRecentResource';
import { useHomeStore } from '@/store/home';
import { homeRecentSelectors } from '@/store/home/selectors';
@@ -35,7 +36,7 @@ const RecentResource = memo(() => {
<GroupBlock
action={
<>
{isRevalidating && <ActionIcon icon={Loader2Icon} loading size={'small'} />}
{isRevalidating && <NeuralNetworkLoading size={14} />}
<Dropdown
menu={{
items: [
@@ -1,8 +1,8 @@
import { ActionIcon } from '@lobehub/ui';
import { BotMessageSquareIcon, Loader2Icon } from 'lucide-react';
import { BotMessageSquareIcon } from 'lucide-react';
import { Suspense, memo } from 'react';
import { useTranslation } from 'react-i18next';
import NeuralNetworkLoading from '@/components/NeuralNetworkLoading';
import { useInitRecentTopic } from '@/hooks/useInitRecentTopic';
import { useHomeStore } from '@/store/home';
import { homeRecentSelectors } from '@/store/home/selectors';
@@ -26,7 +26,7 @@ const RecentTopic = memo(() => {
return (
<GroupBlock
action={isRevalidating && <ActionIcon icon={Loader2Icon} loading size={'small'} />}
action={isRevalidating && <NeuralNetworkLoading size={14} />}
icon={BotMessageSquareIcon}
title={t('topic.recent')}
>
@@ -0,0 +1,181 @@
'use client';
import { createStaticStyles, keyframes } from 'antd-style';
import { CSSProperties, memo } from 'react';
const pulseAnim = keyframes`
0%, 100% {
opacity: 0.3;
}
50% {
opacity: 1;
}
`;
const flowAnim = keyframes`
0% {
transform: translateX(0);
opacity: 0.5;
}
50% {
opacity: 1;
}
100% {
transform: translateX(var(--flow-distance));
opacity: 0.5;
}
`;
const rotateAnim = keyframes`
100% {
transform: rotate(360deg);
}
`;
const scaleAnim = keyframes`
0%, 100% {
transform: scale(0.8);
opacity: 0.5;
}
50% {
transform: scale(1);
opacity: 1;
}
`;
const styles = createStaticStyles(({ css, cssVar }) => ({
center: css`
fill: ${cssVar.colorTextSecondary};
animation: ${scaleAnim} 2s infinite;
`,
connection: css`
opacity: 0.3;
stroke: ${cssVar.colorTextSecondary};
stroke-width: 0.5;
`,
container: css`
display: flex;
align-items: center;
justify-content: center;
`,
node: css`
fill: ${cssVar.colorTextSecondary};
animation: ${pulseAnim} 2s infinite;
`,
particle: css`
fill: ${cssVar.colorTextSecondary};
animation: ${flowAnim} 2s infinite;
`,
ring: css`
transform-origin: center;
fill: none;
stroke: ${cssVar.colorFill};
stroke-dasharray: 0 8;
stroke-width: 1;
animation: ${rotateAnim} 20s infinite linear;
`,
svg: css`
width: 100%;
height: 100%;
`,
}));
interface NeuralNetworkLoadingProps {
size?: number;
}
const NeuralNetworkLoading = memo<NeuralNetworkLoadingProps>(({ size = 16 }) => {
const nodeCount = 3;
const layerCount = 3;
// Generate nodes for each layer
const nodes = [];
for (let layerIndex = 0; layerIndex < layerCount; layerIndex++) {
for (let nodeIndex = 0; nodeIndex < nodeCount; nodeIndex++) {
const x = 25 + layerIndex * 25;
const y = 25 + nodeIndex * 25;
const delay = (layerIndex * nodeCount + nodeIndex) * 0.2;
nodes.push(
<circle
className={styles.node}
cx={x}
cy={y}
key={`node-${layerIndex}-${nodeIndex}`}
r="3"
style={{ animationDelay: `${delay}s` }}
/>,
);
}
}
// Generate connections between layers
const connections = [];
for (let layerIndex = 0; layerIndex < layerCount - 1; layerIndex++) {
for (let nodeIndex = 0; nodeIndex < nodeCount; nodeIndex++) {
const x1 = 25 + layerIndex * 25;
const y1 = 25 + nodeIndex * 25;
for (let targetIndex = 0; targetIndex < nodeCount; targetIndex++) {
const x2 = 25 + (layerIndex + 1) * 25;
const y2 = 25 + targetIndex * 25;
connections.push(
<line
className={styles.connection}
key={`connection-${layerIndex}-${nodeIndex}-${targetIndex}`}
x1={x1}
x2={x2}
y1={y1}
y2={y2}
/>,
);
}
}
}
// Generate particles
const particles = [0, 1, 2].map((index) => (
<circle
className={styles.particle}
cx={25}
cy={50}
key={`particle-${index}`}
r="1.5"
style={
{
'--flow-distance': '50px',
'animationDelay': `${index * 0.6}s`,
} as CSSProperties
}
/>
));
return (
<div className={styles.container} style={{ height: size, width: size }}>
<svg className={styles.svg} viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
{/* Connections */}
{connections}
{/* Nodes */}
{nodes}
{/* Particles */}
{particles}
{/* Central processing unit */}
<rect className={styles.center} height="6" width="6" x="47" y="47" />
{/* Rotating outer ring */}
<circle className={styles.ring} cx="50" cy="50" r="40" />
</svg>
</div>
);
});
export default NeuralNetworkLoading;
+1 -8
View File
@@ -1,6 +1,5 @@
import useSWR, { type SWRHook } from 'swr';
import { isDesktop } from '@/const/version';
/**
* This type of request method is relatively flexible data, which will be triggered on the first time
@@ -27,13 +26,7 @@ export const useClientDataSWR: SWRHook = (key, fetch, config) =>
// Cause issue like this: https://github.com/lobehub/lobe-chat/issues/532
// we need to set it to 0.
dedupingInterval: 0,
focusThrottleInterval:
// FIXME: desktop 云同步模式也是走 edge 请求,也应该增大延迟
// desktop 1.5s
isDesktop
? 1500
: // web 300s
5 * 60 * 1000,
focusThrottleInterval: 5 * 60 * 1000,
// Custom error retry logic: don't retry on 401 errors
onErrorRetry: (error: any, key: any, config: any, revalidate: any, { retryCount }: any) => {
// Check if error is marked as non-retryable (e.g., 401 authentication errors)
+6 -5
View File
@@ -7,16 +7,17 @@ import {
GroupManagementManifest,
} from '@lobechat/builtin-tool-group-management/client';
import { GTDInspectors, GTDManifest } from '@lobechat/builtin-tool-gtd/client';
import { LocalSystemManifest } from '@lobechat/builtin-tool-local-system';
import { PageAgentIdentifier, PageAgentInspectors } from '@lobechat/builtin-tool-page-agent/client';
import {
LocalSystemInspectors,
LocalSystemManifest,
} from '@lobechat/builtin-tool-local-system/client';
import { PageAgentInspectors, PageAgentManifest } from '@lobechat/builtin-tool-page-agent/client';
import {
WebBrowsingInspectors,
WebBrowsingManifest,
} from '@lobechat/builtin-tool-web-browsing/client';
import { type BuiltinInspector } from '@lobechat/types';
import { LocalSystemInspectors } from './local-system/Inspector';
/**
* Builtin tools inspector registry
* Organized by toolset (identifier) -> API name
@@ -32,7 +33,7 @@ const BuiltinToolInspectors: Record<string, Record<string, BuiltinInspector>> =
>,
[GTDManifest.identifier]: GTDInspectors as Record<string, BuiltinInspector>,
[LocalSystemManifest.identifier]: LocalSystemInspectors as Record<string, BuiltinInspector>,
[PageAgentIdentifier]: PageAgentInspectors as Record<string, BuiltinInspector>,
[PageAgentManifest.identifier]: PageAgentInspectors as Record<string, BuiltinInspector>,
[WebBrowsingManifest.identifier]: WebBrowsingInspectors as Record<string, BuiltinInspector>,
};
+5 -4
View File
@@ -9,13 +9,14 @@ import {
GroupManagementManifest,
} from '@lobechat/builtin-tool-group-management/client';
import { GTDInterventions, GTDManifest } from '@lobechat/builtin-tool-gtd/client';
import { LocalSystemManifest } from '@lobechat/builtin-tool-local-system';
import {
LocalSystemIdentifier,
LocalSystemInterventions,
} from '@lobechat/builtin-tool-local-system/client';
import { NotebookManifest } from '@lobechat/builtin-tool-notebook';
import { NotebookInterventions } from '@lobechat/builtin-tool-notebook/client';
import { type BuiltinIntervention } from '@lobechat/types';
import { LocalSystemInterventions } from './local-system/Intervention';
/**
* Builtin tools interventions registry
* Organized by toolset (identifier) -> API name
@@ -26,7 +27,7 @@ export const BuiltinToolInterventions: Record<string, Record<string, any>> = {
[CloudSandboxManifest.identifier]: CloudSandboxInterventions,
[GroupManagementManifest.identifier]: GroupManagementInterventions,
[GTDManifest.identifier]: GTDInterventions,
[LocalSystemManifest.identifier]: LocalSystemInterventions,
[LocalSystemIdentifier]: LocalSystemInterventions,
[NotebookManifest.identifier]: NotebookInterventions,
};
@@ -1,55 +0,0 @@
'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 { createStaticStyles, cx } from 'antd-style';
import { ChevronRight } from 'lucide-react';
import path from 'path-browserify-esm';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
import { shinyTextStyles } from '@/styles';
const styles = createStaticStyles(({ css, cssVar }) => ({
content: css`
font-family: ${cssVar.fontFamilyCode};
`,
root: css`
overflow: hidden;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 1;
color: ${cssVar.colorTextDescription};
`,
}));
export const EditLocalFileInspector = memo<
BuiltinInspectorProps<EditLocalFileParams, EditLocalFileState>
>(({ args, isLoading }) => {
const { t } = useTranslation('plugin');
// 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 && shinyTextStyles.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';
@@ -1,59 +0,0 @@
'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 { createStaticStyles, cx } from 'antd-style';
import { ChevronRight } from 'lucide-react';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
import { shinyTextStyles } from '@/styles';
const styles = createStaticStyles(({ css, cssVar }) => ({
content: css`
font-family: ${cssVar.fontFamilyCode};
`,
root: css`
overflow: hidden;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 1;
color: ${cssVar.colorTextDescription};
`,
}));
export const GlobLocalFilesInspector = memo<BuiltinInspectorProps<GlobFilesParams, GlobFilesState>>(
({ args, isLoading }) => {
const { t } = useTranslation('plugin');
const pattern = args?.pattern || '';
// When loading, show "本地系统 > 匹配搜索文件"
if (isLoading) {
return (
<div className={cx(styles.root, shinyTextStyles.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';
@@ -1,59 +0,0 @@
'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 { createStaticStyles, cx } from 'antd-style';
import { ChevronRight } from 'lucide-react';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
import { shinyTextStyles } from '@/styles';
const styles = createStaticStyles(({ css, cssVar }) => ({
content: css`
font-family: ${cssVar.fontFamilyCode};
`,
root: css`
overflow: hidden;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 1;
color: ${cssVar.colorTextDescription};
`,
}));
export const GrepContentInspector = memo<
BuiltinInspectorProps<GrepContentParams, GrepContentState>
>(({ args, isLoading }) => {
const { t } = useTranslation('plugin');
const pattern = args?.pattern || '';
// When loading, show "本地系统 > 搜索内容"
if (isLoading) {
return (
<div className={cx(styles.root, shinyTextStyles.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';
@@ -1,55 +0,0 @@
'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 { createStaticStyles, cx } from 'antd-style';
import { ChevronRight } from 'lucide-react';
import path from 'path-browserify-esm';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
import { shinyTextStyles } from '@/styles';
const styles = createStaticStyles(({ css, cssVar }) => ({
content: css`
font-family: ${cssVar.fontFamilyCode};
`,
root: css`
overflow: hidden;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 1;
color: ${cssVar.colorTextDescription};
`,
}));
export const ReadLocalFileInspector = memo<
BuiltinInspectorProps<LocalReadFileParams, LocalReadFileState>
>(({ args, isLoading }) => {
const { t } = useTranslation('plugin');
// 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 && shinyTextStyles.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';
@@ -1,66 +0,0 @@
'use client';
import { type RunCommandParams, type RunCommandResult } from '@lobechat/electron-client-ipc';
import { type BuiltinInspectorProps } from '@lobechat/types';
import { Icon } from '@lobehub/ui';
import { createStaticStyles, cx } from 'antd-style';
import { ChevronRight } from 'lucide-react';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
import { shinyTextStyles } from '@/styles';
const styles = createStaticStyles(({ css, cssVar }) => ({
content: css`
font-family: ${cssVar.fontFamilyCode};
`,
root: css`
overflow: hidden;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 1;
color: ${cssVar.colorTextDescription};
`,
}));
interface RunCommandState {
message: string;
result: RunCommandResult;
}
export const RunCommandInspector = memo<BuiltinInspectorProps<RunCommandParams, RunCommandState>>(
({ args, isLoading }) => {
const { t } = useTranslation('plugin');
// 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, shinyTextStyles.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;
@@ -1,59 +0,0 @@
'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 { createStaticStyles, cx } from 'antd-style';
import { ChevronRight } from 'lucide-react';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
import { shinyTextStyles } from '@/styles';
const styles = createStaticStyles(({ css, cssVar }) => ({
content: css`
font-family: ${cssVar.fontFamilyCode};
`,
root: css`
overflow: hidden;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 1;
color: ${cssVar.colorTextDescription};
`,
}));
export const SearchLocalFilesInspector = memo<
BuiltinInspectorProps<LocalSearchFilesParams, LocalFileSearchState>
>(({ args, isLoading }) => {
const { t } = useTranslation('plugin');
const keywords = args?.keywords || '';
// When loading, show "本地系统 > 搜索文件"
if (isLoading) {
return (
<div className={cx(styles.root, shinyTextStyles.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';
+9 -7
View File
@@ -1,21 +1,23 @@
import { LocalSystemApiName, LocalSystemManifest } from '@lobechat/builtin-tool-local-system';
import {
LocalSystemApiName,
LocalSystemIdentifier,
LocalSystemListFilesPlaceholder,
LocalSystemSearchFilesPlaceholder,
} from '@lobechat/builtin-tool-local-system/client';
import {
WebBrowsingManifest,
WebBrowsingPlaceholders,
} from '@lobechat/builtin-tool-web-browsing/client';
import { type BuiltinPlaceholder } from '@lobechat/types';
import { ListFiles as LocalSystemListFiles } from './local-system/Placeholder/ListFiles';
import LocalSystemSearchFiles from './local-system/Placeholder/SearchFiles';
/**
* Builtin tools placeholders registry
* Organized by toolset (identifier) -> API name
*/
export const BuiltinToolPlaceholders: Record<string, Record<string, any>> = {
[LocalSystemManifest.identifier]: {
[LocalSystemApiName.searchLocalFiles]: LocalSystemSearchFiles,
[LocalSystemApiName.listLocalFiles]: LocalSystemListFiles,
[LocalSystemIdentifier]: {
[LocalSystemApiName.searchLocalFiles]: LocalSystemSearchFilesPlaceholder,
[LocalSystemApiName.listLocalFiles]: LocalSystemListFilesPlaceholder,
},
[WebBrowsingManifest.identifier]: WebBrowsingPlaceholders as Record<string, any>,
};
+5 -3
View File
@@ -10,7 +10,10 @@ import { GroupManagementRenders } from '@lobechat/builtin-tool-group-management/
// gtd
import { GTDManifest, GTDRenders } from '@lobechat/builtin-tool-gtd/client';
// local-system
import { LocalSystemManifest } from '@lobechat/builtin-tool-local-system';
import {
LocalSystemIdentifier,
LocalSystemRenders,
} from '@lobechat/builtin-tool-local-system/client';
import { NotebookManifest, NotebookRenders } from '@lobechat/builtin-tool-notebook/client';
// web-browsing
import {
@@ -22,7 +25,6 @@ import { type BuiltinRender } from '@lobechat/types';
// knowledge-base
import { KnowledgeBaseManifest } from './knowledge-base';
import { KnowledgeBaseRenders } from './knowledge-base/Render';
import { LocalSystemRenders } from './local-system/Render';
/**
* Builtin tools renders registry
@@ -35,7 +37,7 @@ const BuiltinToolsRenders: Record<string, Record<string, BuiltinRender>> = {
[GTDManifest.identifier]: GTDRenders as Record<string, BuiltinRender>,
[NotebookManifest.identifier]: NotebookRenders as Record<string, BuiltinRender>,
[KnowledgeBaseManifest.identifier]: KnowledgeBaseRenders as Record<string, BuiltinRender>,
[LocalSystemManifest.identifier]: LocalSystemRenders as Record<string, BuiltinRender>,
[LocalSystemIdentifier]: LocalSystemRenders as Record<string, BuiltinRender>,
[WebBrowsingManifest.identifier]: WebBrowsingRenders as Record<string, BuiltinRender>,
};
+6 -5
View File
@@ -1,5 +1,5 @@
import {
CloudSandboxIdentifier,
CloudSandboxManifest,
CloudSandboxStreamings,
} from '@lobechat/builtin-tool-cloud-sandbox/client';
import {
@@ -7,11 +7,12 @@ import {
GroupManagementStreamings,
} from '@lobechat/builtin-tool-group-management/client';
import { GTDManifest, GTDStreamings } from '@lobechat/builtin-tool-gtd/client';
import { LocalSystemManifest } from '@lobechat/builtin-tool-local-system';
import {
LocalSystemManifest,
LocalSystemStreamings,
} from '@lobechat/builtin-tool-local-system/client';
import { type BuiltinStreaming } from '@lobechat/types';
import { LocalSystemStreamings } from './local-system/Streaming';
/**
* Builtin tools streaming renderer registry
* Organized by toolset (identifier) -> API name
@@ -21,7 +22,7 @@ import { LocalSystemStreamings } from './local-system/Streaming';
* The component should fetch streaming content from store internally.
*/
const BuiltinToolStreamings: Record<string, Record<string, BuiltinStreaming>> = {
[CloudSandboxIdentifier]: CloudSandboxStreamings as Record<string, BuiltinStreaming>,
[CloudSandboxManifest.identifier]: CloudSandboxStreamings as Record<string, BuiltinStreaming>,
[GroupManagementManifest.identifier]: GroupManagementStreamings as Record<
string,
BuiltinStreaming