mirror of
https://github.com/lobehub/lobehub.git
synced 2026-03-27 13:29:15 +07:00
🐛 fix: fix add message and improve local system tool (#11815)
* fix add message * fix grep content issue * fix command tool * improve loading
This commit is contained in:
@@ -548,7 +548,13 @@ export default class LocalFileCtr extends ControllerModule {
|
||||
filesToSearch = [searchPath];
|
||||
} else {
|
||||
// Use glob pattern if provided, otherwise search all files
|
||||
const globPattern = params.glob || '**/*';
|
||||
// If glob doesn't contain directory separator and doesn't start with **,
|
||||
// auto-prefix with **/ to make it recursive
|
||||
let globPattern = params.glob || '**/*';
|
||||
if (params.glob && !params.glob.includes('/') && !params.glob.startsWith('**')) {
|
||||
globPattern = `**/${params.glob}`;
|
||||
}
|
||||
|
||||
filesToSearch = await fg(globPattern, {
|
||||
absolute: true,
|
||||
cwd: searchPath,
|
||||
|
||||
@@ -15,6 +15,19 @@ import { ControllerModule, IpcMethod } from './index';
|
||||
|
||||
const logger = createLogger('controllers:ShellCommandCtr');
|
||||
|
||||
// Maximum output length to prevent context explosion
|
||||
const MAX_OUTPUT_LENGTH = 10_000;
|
||||
|
||||
/**
|
||||
* Truncate string to max length with ellipsis indicator
|
||||
*/
|
||||
const truncateOutput = (str: string, maxLength: number = MAX_OUTPUT_LENGTH): string => {
|
||||
if (str.length <= maxLength) return str;
|
||||
return (
|
||||
str.slice(0, maxLength) + '\n... [truncated, ' + (str.length - maxLength) + ' more characters]'
|
||||
);
|
||||
};
|
||||
|
||||
interface ShellProcess {
|
||||
lastReadStderr: number;
|
||||
lastReadStdout: number;
|
||||
@@ -104,8 +117,8 @@ export default class ShellCommandCtr extends ControllerModule {
|
||||
childProcess.kill();
|
||||
resolve({
|
||||
error: `Command timed out after ${effectiveTimeout}ms`,
|
||||
stderr,
|
||||
stdout,
|
||||
stderr: truncateOutput(stderr),
|
||||
stdout: truncateOutput(stdout),
|
||||
success: false,
|
||||
});
|
||||
}, effectiveTimeout);
|
||||
@@ -125,9 +138,9 @@ export default class ShellCommandCtr extends ControllerModule {
|
||||
logger.info(`${logPrefix} Command completed`, { code, success });
|
||||
resolve({
|
||||
exit_code: code || 0,
|
||||
output: stdout + stderr,
|
||||
stderr,
|
||||
stdout,
|
||||
output: truncateOutput(stdout + stderr),
|
||||
stderr: truncateOutput(stderr),
|
||||
stdout: truncateOutput(stdout),
|
||||
success,
|
||||
});
|
||||
}
|
||||
@@ -138,8 +151,8 @@ export default class ShellCommandCtr extends ControllerModule {
|
||||
logger.error(`${logPrefix} Command failed:`, error);
|
||||
resolve({
|
||||
error: error.message,
|
||||
stderr,
|
||||
stdout,
|
||||
stderr: truncateOutput(stderr),
|
||||
stdout: truncateOutput(stdout),
|
||||
success: false,
|
||||
});
|
||||
});
|
||||
@@ -205,10 +218,10 @@ export default class ShellCommandCtr extends ControllerModule {
|
||||
});
|
||||
|
||||
return {
|
||||
output,
|
||||
output: truncateOutput(output),
|
||||
running,
|
||||
stderr: newStderr,
|
||||
stdout: newStdout,
|
||||
stderr: truncateOutput(newStderr),
|
||||
stdout: truncateOutput(newStdout),
|
||||
success: true,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -552,4 +552,265 @@ describe('LocalFileCtr', () => {
|
||||
expect(result.diffText).toContain('+modified line 2');
|
||||
});
|
||||
});
|
||||
|
||||
describe('handleGrepContent', () => {
|
||||
it('should search content in a single file', async () => {
|
||||
vi.mocked(mockFsPromises.stat).mockResolvedValue({
|
||||
isFile: () => true,
|
||||
isDirectory: () => false,
|
||||
} as any);
|
||||
vi.mocked(mockFsPromises.readFile).mockResolvedValue('Hello world\nTest line\nAnother test');
|
||||
|
||||
const result = await localFileCtr.handleGrepContent({
|
||||
'pattern': 'test',
|
||||
'path': '/test/file.txt',
|
||||
'-i': true,
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.matches).toContain('/test/file.txt');
|
||||
expect(result.total_matches).toBe(1);
|
||||
});
|
||||
|
||||
it('should search content in directory with default glob pattern', async () => {
|
||||
vi.mocked(mockFsPromises.stat).mockImplementation(async (filePath) => {
|
||||
if (filePath === '/test') {
|
||||
return { isFile: () => false, isDirectory: () => true } as any;
|
||||
}
|
||||
return { isFile: () => true, isDirectory: () => false } as any;
|
||||
});
|
||||
vi.mocked(mockFg).mockResolvedValue(['/test/file1.txt', '/test/file2.txt']);
|
||||
vi.mocked(mockFsPromises.readFile).mockImplementation(async (filePath) => {
|
||||
if (filePath === '/test/file1.txt') return 'Hello world';
|
||||
if (filePath === '/test/file2.txt') return 'Test content';
|
||||
return '';
|
||||
});
|
||||
|
||||
const result = await localFileCtr.handleGrepContent({
|
||||
pattern: 'Hello',
|
||||
path: '/test',
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.matches).toContain('/test/file1.txt');
|
||||
expect(result.total_matches).toBe(1);
|
||||
expect(mockFg).toHaveBeenCalledWith('**/*', expect.objectContaining({ cwd: '/test' }));
|
||||
});
|
||||
|
||||
it('should auto-prefix glob pattern with **/ for non-recursive patterns', async () => {
|
||||
vi.mocked(mockFsPromises.stat).mockImplementation(async (filePath) => {
|
||||
if (filePath === '/test') {
|
||||
return { isFile: () => false, isDirectory: () => true } as any;
|
||||
}
|
||||
return { isFile: () => true, isDirectory: () => false } as any;
|
||||
});
|
||||
vi.mocked(mockFg).mockResolvedValue(['/test/src/file1.ts', '/test/lib/file2.tsx']);
|
||||
vi.mocked(mockFsPromises.readFile).mockResolvedValue('const test = "hello";');
|
||||
|
||||
const result = await localFileCtr.handleGrepContent({
|
||||
pattern: 'test',
|
||||
path: '/test',
|
||||
glob: '*.{ts,tsx}',
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
// Should auto-prefix *.{ts,tsx} with **/ to make it recursive
|
||||
expect(mockFg).toHaveBeenCalledWith(
|
||||
'**/*.{ts,tsx}',
|
||||
expect.objectContaining({ cwd: '/test' }),
|
||||
);
|
||||
});
|
||||
|
||||
it('should not modify glob pattern that already contains path separator', async () => {
|
||||
vi.mocked(mockFsPromises.stat).mockImplementation(async (filePath) => {
|
||||
if (filePath === '/test') {
|
||||
return { isFile: () => false, isDirectory: () => true } as any;
|
||||
}
|
||||
return { isFile: () => true, isDirectory: () => false } as any;
|
||||
});
|
||||
vi.mocked(mockFg).mockResolvedValue(['/test/src/file1.ts']);
|
||||
vi.mocked(mockFsPromises.readFile).mockResolvedValue('const test = "hello";');
|
||||
|
||||
const result = await localFileCtr.handleGrepContent({
|
||||
pattern: 'test',
|
||||
path: '/test',
|
||||
glob: 'src/*.ts',
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
// Should not modify glob pattern that already contains /
|
||||
expect(mockFg).toHaveBeenCalledWith('src/*.ts', expect.objectContaining({ cwd: '/test' }));
|
||||
});
|
||||
|
||||
it('should not modify glob pattern that starts with **', async () => {
|
||||
vi.mocked(mockFsPromises.stat).mockImplementation(async (filePath) => {
|
||||
if (filePath === '/test') {
|
||||
return { isFile: () => false, isDirectory: () => true } as any;
|
||||
}
|
||||
return { isFile: () => true, isDirectory: () => false } as any;
|
||||
});
|
||||
vi.mocked(mockFg).mockResolvedValue(['/test/src/file1.ts']);
|
||||
vi.mocked(mockFsPromises.readFile).mockResolvedValue('const test = "hello";');
|
||||
|
||||
const result = await localFileCtr.handleGrepContent({
|
||||
pattern: 'test',
|
||||
path: '/test',
|
||||
glob: '**/components/*.tsx',
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
// Should not modify glob pattern that already starts with **
|
||||
expect(mockFg).toHaveBeenCalledWith(
|
||||
'**/components/*.tsx',
|
||||
expect.objectContaining({ cwd: '/test' }),
|
||||
);
|
||||
});
|
||||
|
||||
it('should filter by type when provided', async () => {
|
||||
vi.mocked(mockFsPromises.stat).mockImplementation(async (filePath) => {
|
||||
if (filePath === '/test') {
|
||||
return { isFile: () => false, isDirectory: () => true } as any;
|
||||
}
|
||||
return { isFile: () => true, isDirectory: () => false } as any;
|
||||
});
|
||||
// fast-glob returns all files, then type filter is applied
|
||||
vi.mocked(mockFg).mockResolvedValue(['/test/file1.ts', '/test/file2.js', '/test/file3.ts']);
|
||||
vi.mocked(mockFsPromises.readFile).mockResolvedValue('unique_pattern');
|
||||
|
||||
const result = await localFileCtr.handleGrepContent({
|
||||
pattern: 'unique_pattern',
|
||||
path: '/test',
|
||||
type: 'ts',
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
// Type filter should exclude .js files from being searched
|
||||
// Only .ts files should be in the results
|
||||
expect(result.matches).not.toContain('/test/file2.js');
|
||||
// At least one .ts file should match
|
||||
expect(result.matches.length).toBeGreaterThan(0);
|
||||
expect(result.matches.every((m) => m.endsWith('.ts'))).toBe(true);
|
||||
});
|
||||
|
||||
it('should return content mode with line numbers', async () => {
|
||||
vi.mocked(mockFsPromises.stat).mockImplementation(async (filePath) => {
|
||||
if (filePath === '/test') {
|
||||
return { isFile: () => false, isDirectory: () => true } as any;
|
||||
}
|
||||
return { isFile: () => true, isDirectory: () => false } as any;
|
||||
});
|
||||
vi.mocked(mockFg).mockResolvedValue(['/test/file.txt']);
|
||||
vi.mocked(mockFsPromises.readFile).mockResolvedValue('line 1\ntest line\nline 3');
|
||||
|
||||
const result = await localFileCtr.handleGrepContent({
|
||||
'pattern': 'test',
|
||||
'path': '/test',
|
||||
'output_mode': 'content',
|
||||
'-n': true,
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.matches.some((m) => m.includes('2:'))).toBe(true);
|
||||
});
|
||||
|
||||
it('should return count mode', async () => {
|
||||
vi.mocked(mockFsPromises.stat).mockImplementation(async (filePath) => {
|
||||
if (filePath === '/test') {
|
||||
return { isFile: () => false, isDirectory: () => true } as any;
|
||||
}
|
||||
return { isFile: () => true, isDirectory: () => false } as any;
|
||||
});
|
||||
vi.mocked(mockFg).mockResolvedValue(['/test/file.txt']);
|
||||
vi.mocked(mockFsPromises.readFile).mockResolvedValue('test one\ntest two\ntest three');
|
||||
|
||||
const result = await localFileCtr.handleGrepContent({
|
||||
pattern: 'test',
|
||||
path: '/test',
|
||||
output_mode: 'count',
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.matches).toContain('/test/file.txt:3');
|
||||
expect(result.total_matches).toBe(3);
|
||||
});
|
||||
|
||||
it('should respect head_limit', async () => {
|
||||
vi.mocked(mockFsPromises.stat).mockImplementation(async (filePath) => {
|
||||
if (filePath === '/test') {
|
||||
return { isFile: () => false, isDirectory: () => true } as any;
|
||||
}
|
||||
return { isFile: () => true, isDirectory: () => false } as any;
|
||||
});
|
||||
vi.mocked(mockFg).mockResolvedValue([
|
||||
'/test/file1.txt',
|
||||
'/test/file2.txt',
|
||||
'/test/file3.txt',
|
||||
'/test/file4.txt',
|
||||
'/test/file5.txt',
|
||||
]);
|
||||
vi.mocked(mockFsPromises.readFile).mockResolvedValue('test content');
|
||||
|
||||
const result = await localFileCtr.handleGrepContent({
|
||||
pattern: 'test',
|
||||
path: '/test',
|
||||
head_limit: 2,
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.matches.length).toBe(2);
|
||||
});
|
||||
|
||||
it('should handle case insensitive search', async () => {
|
||||
vi.mocked(mockFsPromises.stat).mockResolvedValue({
|
||||
isFile: () => true,
|
||||
isDirectory: () => false,
|
||||
} as any);
|
||||
vi.mocked(mockFsPromises.readFile).mockResolvedValue('Hello World\nHELLO world\nhello WORLD');
|
||||
|
||||
const result = await localFileCtr.handleGrepContent({
|
||||
'pattern': 'hello',
|
||||
'path': '/test/file.txt',
|
||||
'-i': true,
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.matches).toContain('/test/file.txt');
|
||||
});
|
||||
|
||||
it('should handle grep error gracefully', async () => {
|
||||
vi.mocked(mockFsPromises.stat).mockRejectedValue(new Error('Path not found'));
|
||||
|
||||
const result = await localFileCtr.handleGrepContent({
|
||||
pattern: 'test',
|
||||
path: '/nonexistent',
|
||||
});
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.matches).toEqual([]);
|
||||
expect(result.total_matches).toBe(0);
|
||||
});
|
||||
|
||||
it('should skip unreadable files gracefully', async () => {
|
||||
vi.mocked(mockFsPromises.stat).mockImplementation(async (filePath) => {
|
||||
if (filePath === '/test') {
|
||||
return { isFile: () => false, isDirectory: () => true } as any;
|
||||
}
|
||||
return { isFile: () => true, isDirectory: () => false } as any;
|
||||
});
|
||||
vi.mocked(mockFg).mockResolvedValue(['/test/file1.txt', '/test/file2.txt']);
|
||||
vi.mocked(mockFsPromises.readFile).mockImplementation(async (filePath) => {
|
||||
if (filePath === '/test/file1.txt') throw new Error('Permission denied');
|
||||
return 'test content';
|
||||
});
|
||||
|
||||
const result = await localFileCtr.handleGrepContent({
|
||||
pattern: 'test',
|
||||
path: '/test',
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
// Should still find match in file2.txt despite file1.txt error
|
||||
expect(result.matches).toContain('/test/file2.txt');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -193,6 +193,42 @@ describe('ShellCommandCtr', () => {
|
||||
expect(result.stderr).toBe('error message\n');
|
||||
});
|
||||
|
||||
it('should truncate long output to prevent context explosion', async () => {
|
||||
let exitCallback: (code: number) => void;
|
||||
let stdoutCallback: (data: Buffer) => void;
|
||||
|
||||
mockChildProcess.on.mockImplementation((event: string, callback: any) => {
|
||||
if (event === 'exit') {
|
||||
exitCallback = callback;
|
||||
setTimeout(() => exitCallback(0), 10);
|
||||
}
|
||||
return mockChildProcess;
|
||||
});
|
||||
|
||||
mockChildProcess.stdout.on.mockImplementation((event: string, callback: any) => {
|
||||
if (event === 'data') {
|
||||
stdoutCallback = callback;
|
||||
// Simulate very long output (15k characters)
|
||||
const longOutput = 'x'.repeat(15_000);
|
||||
setTimeout(() => stdoutCallback(Buffer.from(longOutput)), 5);
|
||||
}
|
||||
return mockChildProcess.stdout;
|
||||
});
|
||||
|
||||
mockChildProcess.stderr.on.mockImplementation(() => mockChildProcess.stderr);
|
||||
|
||||
const result = await shellCommandCtr.handleRunCommand({
|
||||
command: 'command-with-long-output',
|
||||
description: 'long output command',
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
// Output should be truncated to ~10k + truncation message
|
||||
expect(result.stdout!.length).toBeLessThan(15_000);
|
||||
expect(result.stdout).toContain('truncated');
|
||||
expect(result.stdout).toContain('more characters');
|
||||
});
|
||||
|
||||
it('should enforce timeout limits', async () => {
|
||||
mockChildProcess.on.mockImplementation(() => mockChildProcess);
|
||||
mockChildProcess.stdout.on.mockImplementation(() => mockChildProcess.stdout);
|
||||
|
||||
@@ -1,21 +1,16 @@
|
||||
import { type EditLocalFileState } from '@lobechat/builtin-tool-local-system';
|
||||
import { type EditLocalFileParams } from '@lobechat/electron-client-ipc';
|
||||
import { type BuiltinRenderProps } from '@lobechat/types';
|
||||
import { Alert, Flexbox, Icon, Skeleton } from '@lobehub/ui';
|
||||
import { ChevronRight } from 'lucide-react';
|
||||
import { Alert, Flexbox, Skeleton } from '@lobehub/ui';
|
||||
import { useTheme } from 'next-themes';
|
||||
import path from 'path-browserify-esm';
|
||||
import React, { memo, useMemo } from 'react';
|
||||
import { Diff, Hunk, parseDiff } from 'react-diff-view';
|
||||
import 'react-diff-view/style/index.css';
|
||||
|
||||
import { LocalFile, LocalFolder } from '@/features/LocalFile';
|
||||
import '@/styles/react-diff-view.dark.css';
|
||||
|
||||
const EditLocalFile = memo<BuiltinRenderProps<EditLocalFileParams, EditLocalFileState>>(
|
||||
({ args, pluginState, pluginError }) => {
|
||||
const { base, dir } = path.parse(args.file_path);
|
||||
|
||||
// Parse diff for react-diff-view
|
||||
const files = useMemo(() => {
|
||||
const diffText = pluginState?.diffText;
|
||||
@@ -35,11 +30,6 @@ const EditLocalFile = memo<BuiltinRenderProps<EditLocalFileParams, EditLocalFile
|
||||
|
||||
return (
|
||||
<Flexbox gap={12}>
|
||||
<Flexbox horizontal>
|
||||
<LocalFolder path={dir} />
|
||||
<Icon icon={ChevronRight} />
|
||||
<LocalFile name={base} path={args.file_path} />
|
||||
</Flexbox>
|
||||
{pluginError ? (
|
||||
<Alert
|
||||
description={pluginError.message || 'Unknown error occurred'}
|
||||
|
||||
@@ -3,21 +3,16 @@ import { type ListLocalFileParams } from '@lobechat/electron-client-ipc';
|
||||
import { type BuiltinRenderProps } from '@lobechat/types';
|
||||
import React, { memo } from 'react';
|
||||
|
||||
import { LocalFolder } from '@/features/LocalFile';
|
||||
|
||||
import SearchResult from './Result';
|
||||
|
||||
const ListFiles = memo<BuiltinRenderProps<ListLocalFileParams, LocalFileListState>>(
|
||||
({ messageId, pluginError, args, pluginState }) => {
|
||||
({ messageId, pluginError, pluginState }) => {
|
||||
return (
|
||||
<>
|
||||
<LocalFolder path={args.path} />
|
||||
<SearchResult
|
||||
listResults={pluginState?.listResults}
|
||||
messageId={messageId}
|
||||
pluginError={pluginError}
|
||||
/>
|
||||
</>
|
||||
<SearchResult
|
||||
listResults={pluginState?.listResults}
|
||||
messageId={messageId}
|
||||
pluginError={pluginError}
|
||||
/>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
import { Button, Icon } from '@lobehub/ui';
|
||||
import { Plus } from 'lucide-react';
|
||||
import { memo } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
import { useActionSWR } from '@/libs/swr';
|
||||
import { useAgentStore } from '@/store/agent';
|
||||
|
||||
const AddButton = memo(() => {
|
||||
const navigate = useNavigate();
|
||||
const createAgent = useAgentStore((s) => s.createAgent);
|
||||
// Use a unique SWR key to avoid conflicts with useCreateMenuItems which uses 'agent.createAgent'
|
||||
const { mutate, isValidating } = useActionSWR('agent.createAgentFromWelcome', async () => {
|
||||
const result = await createAgent({});
|
||||
navigate(`/agent/${result.agentId}/profile`);
|
||||
return result;
|
||||
});
|
||||
|
||||
return (
|
||||
<Button
|
||||
icon={<Icon icon={Plus} size={'small'} />}
|
||||
loading={isValidating}
|
||||
onClick={() => mutate()}
|
||||
style={{
|
||||
alignItems: 'center',
|
||||
borderRadius: 4,
|
||||
height: '20px',
|
||||
justifyContent: 'center',
|
||||
padding: '0 1px',
|
||||
width: '20px',
|
||||
}}
|
||||
variant={'filled'}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
export default AddButton;
|
||||
@@ -3,7 +3,7 @@
|
||||
import { Avatar, Flexbox, Markdown, Text } from '@lobehub/ui';
|
||||
import isEqual from 'fast-deep-equal';
|
||||
import React, { memo, useMemo } from 'react';
|
||||
import { Trans, useTranslation } from 'react-i18next';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { DEFAULT_AVATAR, DEFAULT_INBOX_AVATAR } from '@/const/meta';
|
||||
import { useIsMobile } from '@/hooks/useIsMobile';
|
||||
@@ -12,7 +12,6 @@ import { agentSelectors, builtinAgentSelectors } from '@/store/agent/selectors';
|
||||
import { useUserStore } from '@/store/user';
|
||||
import { userGeneralSettingsSelectors } from '@/store/user/selectors';
|
||||
|
||||
import AddButton from './AddButton';
|
||||
import OpeningQuestions from './OpeningQuestions';
|
||||
import ToolAuthAlert from './ToolAuthAlert';
|
||||
|
||||
@@ -57,27 +56,8 @@ const InboxWelcome = memo(() => {
|
||||
{displayTitle}
|
||||
</Text>
|
||||
<Flexbox width={'min(100%, 640px)'}>
|
||||
<Markdown
|
||||
customRender={(dom, context) => {
|
||||
if (context.text.includes('<plus />')) {
|
||||
return (
|
||||
<Trans
|
||||
components={{
|
||||
br: <br />,
|
||||
plus: <AddButton />,
|
||||
}}
|
||||
i18nKey="guide.defaultMessage"
|
||||
ns="welcome"
|
||||
values={{ appName: 'Lobe AI' }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return dom;
|
||||
}}
|
||||
fontSize={fontSize}
|
||||
variant={'chat'}
|
||||
>
|
||||
{isInbox ? t('guide.defaultMessage', { appName: 'Lobe AI' }) : message}
|
||||
<Markdown fontSize={fontSize} variant={'chat'}>
|
||||
{isInbox ? t('guide.defaultMessageWithoutCreate', { appName: 'Lobe AI' }) : message}
|
||||
</Markdown>
|
||||
</Flexbox>
|
||||
{openingQuestions.length > 0 && (
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
import { Button } from '@lobehub/ui';
|
||||
import { memo } from 'react';
|
||||
|
||||
import { useActionSWR } from '@/libs/swr';
|
||||
import { useSessionStore } from '@/store/session';
|
||||
|
||||
const AddButton = memo(() => {
|
||||
const createSession = useSessionStore((s) => s.createSession);
|
||||
const { mutate, isValidating } = useActionSWR(['session.createSession', undefined], () => {
|
||||
return createSession({ group: undefined });
|
||||
});
|
||||
|
||||
return (
|
||||
<Button
|
||||
loading={isValidating}
|
||||
onClick={() => mutate()}
|
||||
style={{
|
||||
alignItems: 'center',
|
||||
borderRadius: 4,
|
||||
height: '20px',
|
||||
justifyContent: 'center',
|
||||
padding: '0 1px',
|
||||
width: '20px',
|
||||
}}
|
||||
variant={'filled'}
|
||||
>
|
||||
+
|
||||
</Button>
|
||||
);
|
||||
});
|
||||
|
||||
export default AddButton;
|
||||
@@ -3,7 +3,7 @@
|
||||
import { Flexbox, Markdown, Text } from '@lobehub/ui';
|
||||
import isEqual from 'fast-deep-equal';
|
||||
import React, { memo, useMemo } from 'react';
|
||||
import { Trans, useTranslation } from 'react-i18next';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import SupervisorAvatar from '@/app/[variants]/(main)/group/features/GroupAvatar';
|
||||
import { useIsMobile } from '@/hooks/useIsMobile';
|
||||
@@ -13,7 +13,6 @@ import { agentGroupSelectors, useAgentGroupStore } from '@/store/agentGroup';
|
||||
import { useUserStore } from '@/store/user';
|
||||
import { userGeneralSettingsSelectors } from '@/store/user/selectors';
|
||||
|
||||
import AddButton from './AddButton';
|
||||
import OpeningQuestions from './OpeningQuestions';
|
||||
import ToolAuthAlert from './ToolAuthAlert';
|
||||
|
||||
@@ -68,27 +67,8 @@ const InboxWelcome = memo(() => {
|
||||
{displayTitle}
|
||||
</Text>
|
||||
<Flexbox width={'min(100%, 640px)'}>
|
||||
<Markdown
|
||||
customRender={(dom, context) => {
|
||||
if (context.text.includes('<plus />')) {
|
||||
return (
|
||||
<Trans
|
||||
components={{
|
||||
br: <br />,
|
||||
plus: <AddButton />,
|
||||
}}
|
||||
i18nKey="guide.defaultMessage"
|
||||
ns="welcome"
|
||||
values={{ appName: 'Lobe AI' }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return dom;
|
||||
}}
|
||||
fontSize={fontSize}
|
||||
variant={'chat'}
|
||||
>
|
||||
{isInbox ? t('guide.defaultMessage', { appName: 'Lobe AI' }) : message}
|
||||
<Markdown fontSize={fontSize} variant={'chat'}>
|
||||
{isInbox ? t('guide.defaultMessageWithoutCreate', { appName: 'Lobe AI' }) : message}
|
||||
</Markdown>
|
||||
</Flexbox>
|
||||
{openingQuestions.length > 0 && (
|
||||
|
||||
@@ -68,7 +68,12 @@ const GroupMessage = memo<GroupMessageProps>(({ id, index, disableEditing, isLat
|
||||
// Get editing state from ConversationStore
|
||||
const editing = useConversationStore(messageStateSelectors.isMessageEditing(contentId || ''));
|
||||
const creating = useConversationStore(messageStateSelectors.isMessageCreating(id));
|
||||
const { minHeight } = useNewScreen({ creating, isLatestItem, messageId: id });
|
||||
const generating = useConversationStore(messageStateSelectors.isMessageGenerating(id));
|
||||
const { minHeight } = useNewScreen({
|
||||
creating: creating || generating,
|
||||
isLatestItem,
|
||||
messageId: id,
|
||||
});
|
||||
|
||||
const setMessageItemActionElementPortialContext = useSetMessageItemActionElementPortialContext();
|
||||
const setMessageItemActionTypeContext = useSetMessageItemActionTypeContext();
|
||||
|
||||
Reference in New Issue
Block a user