Skip to content

Commit

Permalink
💄 style: improve tools calling UI (lobehub#3326)
Browse files Browse the repository at this point in the history
* wip improve tools calling

* refactor style

* change icon

* improve topic loading

* fix test

* improve import

* fix lint

* add tests
  • Loading branch information
arvinxx authored Jul 30, 2024
1 parent ea798e5 commit 36cabc0
Show file tree
Hide file tree
Showing 10 changed files with 113 additions and 34 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import { memo, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { Flexbox } from 'react-layout-kit';

import BubblesLoading from '@/components/BubblesLoading';
import { LOADING_FLAT } from '@/const/message';
import { useIsMobile } from '@/hooks/useIsMobile';
import { useChatStore } from '@/store/chat';

Expand Down Expand Up @@ -160,13 +162,19 @@ const TopicContent = memo<TopicContentProps>(({ id, title, fav, showMore }) => {
spin={isLoading}
/>
{!editing ? (
<Paragraph
className={styles.title}
ellipsis={{ rows: 1, tooltip: { placement: 'left', title } }}
style={{ margin: 0 }}
>
{title}
</Paragraph>
title === LOADING_FLAT ? (
<Flexbox flex={1} height={28} justify={'center'}>
<BubblesLoading />
</Flexbox>
) : (
<Paragraph
className={styles.title}
ellipsis={{ rows: 1, tooltip: { placement: 'left', title } }}
style={{ margin: 0 }}
>
{title}
</Paragraph>
)
) : (
<EditableText
editing={editing}
Expand Down
File renamed without changes.
3 changes: 1 addition & 2 deletions src/features/Conversation/Extras/Translate.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,10 @@ import { memo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Flexbox } from 'react-layout-kit';

import BubblesLoading from '@/components/BubblesLoading';
import { useChatStore } from '@/store/chat';
import { ChatTranslate } from '@/types/message';

import BubblesLoading from '../components/BubblesLoading';

interface TranslateProps extends ChatTranslate {
id: string;
loading?: boolean;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,57 +1,84 @@
import { Icon } from '@lobehub/ui';
import { Icon, Tag } from '@lobehub/ui';
import { Typography } from 'antd';
import isEqual from 'fast-deep-equal';
import { Loader2, LucideChevronDown, LucideChevronRight, LucideToyBrick } from 'lucide-react';
import { Loader2 } from 'lucide-react';
import { CSSProperties, memo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Flexbox } from 'react-layout-kit';

import PluginAvatar from '@/features/PluginAvatar';
import { useIsMobile } from '@/hooks/useIsMobile';
import { useChatStore } from '@/store/chat';
import { chatSelectors } from '@/store/chat/selectors';
import { pluginHelpers, useToolStore } from '@/store/tool';
import { toolSelectors } from '@/store/tool/selectors';

import { ToolMessage } from '../../Tool';
import Arguments from '../../components/Arguments';
import { useStyles } from './style';

export interface InspectorProps {
apiName: string;
arguments?: string;
id: string;
identifier: string;
index: number;
messageId: string;
style: CSSProperties;
style?: CSSProperties;
}

const CallItem = memo<InspectorProps>(
({ arguments: requestArgs, messageId, index, identifier, style }) => {
({ arguments: requestArgs, apiName, messageId, id, index, identifier, style }) => {
const { t } = useTranslation('plugin');
const { styles } = useStyles();
const [open, setOpen] = useState(false);
const loading = useChatStore(chatSelectors.isToolCallStreaming(messageId, index));
const toolMessage = useChatStore(chatSelectors.getMessageByToolCallId(id));
const isMobile = useIsMobile();

const pluginMeta = useToolStore(toolSelectors.getMetaById(identifier), isEqual);

const pluginTitle = pluginHelpers.getPluginTitle(pluginMeta) ?? t('unknownPlugin');

return (
// when tool calling stop streaming, we should show the tool message
return !loading && toolMessage ? (
<ToolMessage {...toolMessage} />
) : (
<Flexbox gap={8} style={style}>
<Flexbox
align={'center'}
className={styles.container}
distribution={'space-between'}
gap={8}
height={32}
horizontal
onClick={() => {
setOpen(!open);
}}
>
<Flexbox align={'center'} gap={8} horizontal>
{loading ? <Icon icon={Loader2} spin /> : <Icon icon={LucideToyBrick} />}
{pluginTitle}
{loading ? (
<div>
<Icon icon={Loader2} spin />
</div>
) : (
<PluginAvatar identifier={identifier} size={isMobile ? 36 : undefined} />
)}
{isMobile ? (
<Flexbox>
<div>{pluginTitle}</div>
<Typography.Text className={styles.apiName} type={'secondary'}>
{apiName}
</Typography.Text>
</Flexbox>
) : (
<>
<div>{pluginTitle}</div>
<Tag>{apiName}</Tag>
</>
)}
</Flexbox>
<Icon icon={open ? LucideChevronDown : LucideChevronRight} />
</Flexbox>
{(open || loading) && <Arguments arguments={requestArgs} />}
{loading && <Arguments arguments={requestArgs} />}
</Flexbox>
);
},
Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,30 @@
import { createStyles } from 'antd-style';

export const useStyles = createStyles(({ css, token }) => ({
apiName: css`
overflow: hidden;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 1;
font-size: 12px;
text-overflow: ellipsis;
`,
container: css`
cursor: pointer;
width: fit-content;
padding-inline: 4px 6px;
padding-block: 6px;
padding-inline: 8px;
padding-inline-end: 12px;
color: ${token.colorText};
background: ${token.colorFillTertiary};
border: 1px solid ${token.colorBorder};
border-radius: 8px;
&:hover {
background: ${token.colorFillSecondary};
background: ${token.colorFillTertiary};
}
`,
plugin: css`
Expand Down
9 changes: 4 additions & 5 deletions src/features/Conversation/Messages/Assistant/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { chatSelectors } from '@/store/chat/selectors';
import { ChatMessage } from '@/types/message';

import { DefaultMessage } from '../Default';
import ToolCall from './ToolCalls';
import ToolCall from './ToolCallItem';

export const AssistantMessage = memo<
ChatMessage & {
Expand All @@ -31,17 +31,16 @@ export const AssistantMessage = memo<
/>
)}
{!editing && tools && (
<Flexbox gap={8} horizontal>
<Flexbox gap={8}>
{tools.map((toolCall, index) => (
<ToolCall
apiName={toolCall.apiName}
arguments={toolCall.arguments}
id={toolCall.id}
identifier={toolCall.identifier}
index={index}
key={toolCall.id}
messageId={id}
style={{
maxWidth: `max(${100 / tools.length}%, 300px)`,
}}
/>
))}
</Flexbox>
Expand Down
2 changes: 1 addition & 1 deletion src/features/Conversation/Messages/Default.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { ReactNode, memo } from 'react';
import { LOADING_FLAT } from '@/const/message';
import { ChatMessage } from '@/types/message';

import BubblesLoading from '../components/BubblesLoading';
import BubblesLoading from '@/components/BubblesLoading';

export const DefaultMessage = memo<
ChatMessage & {
Expand Down
3 changes: 1 addition & 2 deletions src/features/Conversation/Messages/User.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import { ReactNode, memo } from 'react';
import { Flexbox } from 'react-layout-kit';

import BubblesLoading from '@/components/BubblesLoading';
import { LOADING_FLAT } from '@/const/message';
import { FileListPreviewer } from '@/features/FileList';
import { ChatMessage } from '@/types/message';

import BubblesLoading from '../components/BubblesLoading';

export const UserMessage = memo<
ChatMessage & {
editableContent: ReactNode;
Expand Down
34 changes: 32 additions & 2 deletions src/store/chat/slices/message/selectors.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,36 @@ describe('chatSelectors', () => {
});
});

describe('getMessageByToolCallId', () => {
it('should return undefined if the message with the given id does not exist', () => {
const message = chatSelectors.getMessageByToolCallId('non-existent-id')(initialStore);
expect(message).toBeUndefined();
});

it('should return the message object with the matching tool_call_id', () => {
const toolMessage = {
id: 'msg3',
content: 'Function Message',
role: 'tool',
tool_call_id: 'ttt',
plugin: {
arguments: 'arg1',
identifier: 'func1',
apiName: 'ttt',
type: 'default',
},
} as ChatMessage;
const state = merge(initialStore, {
messagesMap: {
[messageMapKey('abc')]: [...mockMessages, toolMessage],
},
activeId: 'abc',
});
const message = chatSelectors.getMessageByToolCallId('ttt')(state);
expect(message).toMatchObject(toolMessage);
});
});

describe('currentChatsWithHistoryConfig', () => {
it('should slice the messages according to the current agent config', () => {
const state = merge(initialStore, {
Expand Down Expand Up @@ -203,15 +233,15 @@ describe('chatSelectors', () => {
});

describe('currentChatsWithGuideMessage', () => {
it('should return existing messages if there are any', () => {
it('should return existing messages except tool message', () => {
const state = merge(initialStore, {
messagesMap: {
[messageMapKey('someActiveId')]: mockMessages,
},
activeId: 'someActiveId',
});
const chats = chatSelectors.currentChatsWithGuideMessage({} as MetaData)(state);
expect(chats).toEqual(mockedChats);
expect(chats).toEqual(mockedChats.slice(0, 2));
});

it('should add a guide message if the chat is brand new', () => {
Expand Down
10 changes: 8 additions & 2 deletions src/store/chat/slices/message/selectors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,11 +65,12 @@ const showInboxWelcome = (s: ChatStore): boolean => {
return isBrandNewChat;
};

// 针对新助手添加初始化时的自定义消息
// Custom message for new assistant initialization
const currentChatsWithGuideMessage =
(meta: MetaData) =>
(s: ChatStore): ChatMessage[] => {
const data = currentChats(s);
// skip tool message
const data = currentChats(s).filter((m) => m.role !== 'tool');

const { isAgentEditable } = featureFlagsSelectors(createServerConfigStore().getState());

Expand Down Expand Up @@ -125,6 +126,10 @@ const chatsMessageString = (s: ChatStore): string => {
const getMessageById = (id: string) => (s: ChatStore) =>
chatHelpers.getMessageById(currentChats(s), id);

const getMessageByToolCallId = (id: string) => (s: ChatStore) => {
const messages = currentChats(s);
return messages.find((m) => m.tool_call_id === id);
};
const getTraceIdByMessageId = (id: string) => (s: ChatStore) => getMessageById(id)(s)?.traceId;

const latestMessage = (s: ChatStore) => currentChats(s).at(-1);
Expand Down Expand Up @@ -160,6 +165,7 @@ export const chatSelectors = {
currentChatsWithHistoryConfig,
currentToolMessages,
getMessageById,
getMessageByToolCallId,
getTraceIdByMessageId,
isAIGenerating,
isCreatingMessage,
Expand Down

0 comments on commit 36cabc0

Please sign in to comment.