Skip to content

Commit 4dd76e2

Browse files
author
Allaoua Benchikh
committed
Added ability to have custom icons
1 parent 75cc539 commit 4dd76e2

File tree

7 files changed

+128
-13
lines changed

7 files changed

+128
-13
lines changed

backend/chainlit/step.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ class StepDict(TypedDict, total=False):
6363
showInput: Optional[Union[bool, str]]
6464
defaultOpen: Optional[bool]
6565
language: Optional[str]
66+
icon: Optional[str]
6667
feedback: Optional[FeedbackDict]
6768

6869

@@ -83,6 +84,7 @@ def step(
8384
tags: Optional[List[str]] = None,
8485
metadata: Optional[Dict] = None,
8586
language: Optional[str] = None,
87+
icon: Optional[str] = None,
8688
show_input: Union[bool, str] = "json",
8789
default_open: bool = False,
8890
):
@@ -106,6 +108,7 @@ async def async_wrapper(*args, **kwargs):
106108
parent_id=parent_id,
107109
tags=tags,
108110
language=language,
111+
icon=icon,
109112
show_input=show_input,
110113
default_open=default_open,
111114
metadata=metadata,
@@ -135,6 +138,7 @@ def sync_wrapper(*args, **kwargs):
135138
parent_id=parent_id,
136139
tags=tags,
137140
language=language,
141+
icon=icon,
138142
show_input=show_input,
139143
default_open=default_open,
140144
metadata=metadata,
@@ -182,6 +186,7 @@ class Step:
182186
end: Union[str, None]
183187
generation: Optional[BaseGeneration]
184188
language: Optional[str]
189+
icon: Optional[str]
185190
default_open: Optional[bool]
186191
elements: Optional[List[Element]]
187192
fail_on_persist_error: bool
@@ -196,6 +201,7 @@ def __init__(
196201
metadata: Optional[Dict] = None,
197202
tags: Optional[List[str]] = None,
198203
language: Optional[str] = None,
204+
icon: Optional[str] = None,
199205
default_open: Optional[bool] = False,
200206
show_input: Union[bool, str] = "json",
201207
thread_id: Optional[str] = None,
@@ -214,6 +220,7 @@ def __init__(
214220
self.parent_id = parent_id
215221

216222
self.language = language
223+
self.icon = icon
217224
self.default_open = default_open
218225
self.generation = None
219226
self.elements = elements or []
@@ -303,6 +310,7 @@ def to_dict(self) -> StepDict:
303310
"start": self.start,
304311
"end": self.end,
305312
"language": self.language,
313+
"icon": self.icon,
306314
"defaultOpen": self.default_open,
307315
"showInput": self.show_input,
308316
"generation": self.generation.to_dict() if self.generation else None,

backend/tests/test_emitter.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,22 @@ async def test_send_step(
5454
mock_websocket_session.emit.assert_called_once_with("new_message", step_dict)
5555

5656

57+
async def test_send_step_with_icon(
58+
emitter: ChainlitEmitter, mock_websocket_session: MagicMock
59+
) -> None:
60+
step_dict: StepDict = {
61+
"id": "test_step_with_icon",
62+
"type": "tool",
63+
"name": "Test Step with Icon",
64+
"output": "This is a test step with an icon",
65+
"icon": "search",
66+
}
67+
68+
await emitter.send_step(step_dict)
69+
70+
mock_websocket_session.emit.assert_called_once_with("new_message", step_dict)
71+
72+
5773
async def test_update_step(
5874
emitter: ChainlitEmitter, mock_websocket_session: MagicMock
5975
) -> None:
@@ -69,6 +85,22 @@ async def test_update_step(
6985
mock_websocket_session.emit.assert_called_once_with("update_message", step_dict)
7086

7187

88+
async def test_update_step_with_icon(
89+
emitter: ChainlitEmitter, mock_websocket_session: MagicMock
90+
) -> None:
91+
step_dict: StepDict = {
92+
"id": "test_step_with_icon",
93+
"type": "tool",
94+
"name": "Updated Test Step with Icon",
95+
"output": "This is an updated test step with an icon",
96+
"icon": "database",
97+
}
98+
99+
await emitter.update_step(step_dict)
100+
101+
mock_websocket_session.emit.assert_called_once_with("update_message", step_dict)
102+
103+
72104
async def test_delete_step(
73105
emitter: ChainlitEmitter, mock_websocket_session: MagicMock
74106
) -> None:
@@ -139,6 +171,20 @@ async def test_stream_start(
139171
mock_websocket_session.emit.assert_called_once_with("stream_start", step_dict)
140172

141173

174+
async def test_stream_start_with_icon(
175+
emitter: ChainlitEmitter, mock_websocket_session: MagicMock
176+
) -> None:
177+
step_dict: StepDict = {
178+
"id": "test_stream_with_icon",
179+
"type": "tool",
180+
"name": "Test Stream with Icon",
181+
"output": "This is a test stream with an icon",
182+
"icon": "cpu",
183+
}
184+
await emitter.stream_start(step_dict)
185+
mock_websocket_session.emit.assert_called_once_with("stream_start", step_dict)
186+
187+
142188
async def test_send_toast(
143189
emitter: ChainlitEmitter, mock_websocket_session: MagicMock
144190
) -> None:

cypress/e2e/step_icon/main.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import chainlit as cl
2+
3+
4+
@cl.step(name="search", type="tool", icon="search")
5+
async def search():
6+
await cl.sleep(1)
7+
return "Response from search"
8+
9+
10+
@cl.step(name="database", type="tool", icon="database")
11+
async def database():
12+
await cl.sleep(1)
13+
return "Response from database"
14+
15+
16+
@cl.step(name="regular", type="tool")
17+
async def regular():
18+
await cl.sleep(1)
19+
return "Response from regular"
20+
21+
22+
async def cpu():
23+
async with cl.Step(name="cpu", type="tool", icon="cpu") as s:
24+
await cl.sleep(1)
25+
s.output = "Response from cpu"
26+
27+
28+
@cl.on_message
29+
async def main(message: cl.Message):
30+
await search()
31+
await database()
32+
await regular()
33+
await cpu()

cypress/e2e/step_icon/spec.cy.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { submitMessage } from '../../support/testUtils';
2+
3+
describe('Step with Icon', () => {
4+
it('should be able to use steps with icons', () => {
5+
submitMessage('Hello');
6+
7+
cy.get('#step-search').should('exist').click();
8+
9+
cy.get('#step-database').should('exist').click();
10+
11+
cy.get('#step-regular').should('exist').click();
12+
13+
cy.get('#step-cpu').should('exist');
14+
15+
cy.get('.step').should('have.length', 5);
16+
});
17+
});

frontend/src/components/chat/Messages/Message/Avatar.tsx

Lines changed: 22 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
useConfig
99
} from '@chainlit/react-client';
1010

11+
import Icon from '@/components/Icon';
1112
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
1213
import { Skeleton } from '@/components/ui/skeleton';
1314
import {
@@ -21,9 +22,10 @@ interface Props {
2122
author?: string;
2223
hide?: boolean;
2324
isError?: boolean;
25+
iconName?: string;
2426
}
2527

26-
const MessageAvatar = ({ author, hide, isError }: Props) => {
28+
const MessageAvatar = ({ author, hide, isError, iconName }: Props) => {
2729
const apiClient = useContext(ChainlitContext);
2830
const { chatProfile } = useChatSession();
2931
const { config } = useConfig();
@@ -48,22 +50,29 @@ const MessageAvatar = ({ author, hide, isError }: Props) => {
4850
);
4951
}
5052

53+
// Render icon or avatar based on iconName
54+
const avatarContent = iconName ? (
55+
<span className="inline-flex">
56+
<Icon name={iconName} className="h-5 w-5 mt-[3px]" />
57+
</span>
58+
) : (
59+
<Avatar className="h-5 w-5 mt-[3px]">
60+
<AvatarImage
61+
src={avatarUrl}
62+
alt={`Avatar for ${author || 'default'}`}
63+
className="bg-transparent"
64+
/>
65+
<AvatarFallback className="bg-transparent">
66+
<Skeleton className="h-full w-full rounded-full" />
67+
</AvatarFallback>
68+
</Avatar>
69+
);
70+
5171
return (
5272
<span className={cn('inline-block', hide && 'invisible')}>
5373
<TooltipProvider>
5474
<Tooltip>
55-
<TooltipTrigger asChild>
56-
<Avatar className="h-5 w-5 mt-[3px]">
57-
<AvatarImage
58-
src={avatarUrl}
59-
alt={`Avatar for ${author || 'default'}`}
60-
className="bg-transparent"
61-
/>
62-
<AvatarFallback className="bg-transparent">
63-
<Skeleton className="h-full w-full rounded-full" />
64-
</AvatarFallback>
65-
</Avatar>
66-
</TooltipTrigger>
75+
<TooltipTrigger asChild>{avatarContent}</TooltipTrigger>
6776
<TooltipContent>
6877
<p>{author}</p>
6978
</TooltipContent>

frontend/src/components/chat/Messages/Message/index.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ const Message = memo(
9999
<MessageAvatar
100100
author={message.metadata?.avatarName || message.name}
101101
isError={message.isError}
102+
iconName={message.icon}
102103
/>
103104
) : null}
104105
{/* Display the step and its children */}

libs/react-client/src/types/step.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ export interface IStep {
1616
id: string;
1717
name: string;
1818
type: StepType;
19+
icon?: string;
1920
threadId?: string;
2021
parentId?: string;
2122
isError?: boolean;

0 commit comments

Comments
 (0)