Skip to content

Commit 9421ca6

Browse files
committed
add size option for Image Elements and language option for Text Element
1 parent 099bb38 commit 9421ca6

File tree

15 files changed

+206
-130
lines changed

15 files changed

+206
-130
lines changed

cypress/e2e/global_elements/spec.cy.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ describe("Global Elements", () => {
99
cy.get(".message").should("have.length", 1);
1010

1111
// Inlined
12-
cy.get(".inlined-image").should("have.length", 1);
12+
cy.get(".inline-image").should("have.length", 1);
1313
cy.get(".element-link").eq(0).should("contain", "text1");
1414
cy.get(".element-link").eq(0).click();
1515

cypress/e2e/scoped_elements/spec.cy.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,10 @@ describe("Scoped Elements", () => {
88
it("should be able to display inlined, side and page elements", () => {
99
cy.get(".message").should("have.length", 2);
1010

11-
cy.get(".message").eq(0).find(".inlined-image").should("have.length", 0);
11+
cy.get(".message").eq(0).find(".inline-image").should("have.length", 0);
1212
cy.get(".message").eq(0).find(".element-link").should("have.length", 0);
1313

14-
cy.get(".message").eq(1).find(".inlined-image").should("have.length", 1);
14+
cy.get(".message").eq(1).find(".inline-image").should("have.length", 1);
1515
cy.get(".message").eq(1).find(".element-link").should("have.length", 2);
1616
});
1717
});

src/chainlit/client.py

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from abc import ABC, abstractmethod
44
import uuid
55
import requests
6-
from chainlit.types import ElementType
6+
from chainlit.types import ElementType, ElementSize
77
from chainlit.logger import logger
88

99

@@ -30,6 +30,8 @@ def create_element(
3030
url: str,
3131
name: str,
3232
display: str,
33+
size: ElementSize = None,
34+
language: str = None,
3335
for_id: str = None,
3436
) -> Dict[str, Any]:
3537
pass
@@ -120,7 +122,14 @@ def create_message(self, variables: Dict[str, Any]) -> int:
120122
return int(res["data"]["createMessage"]["id"])
121123

122124
def create_element(
123-
self, type: ElementType, url: str, name: str, display: str, for_id: str = None
125+
self,
126+
type: ElementType,
127+
url: str,
128+
name: str,
129+
display: str,
130+
size: ElementSize = None,
131+
language: str = None,
132+
for_id: str = None,
124133
) -> Dict[str, Any]:
125134
c_id = self.get_conversation_id()
126135

@@ -129,13 +138,15 @@ def create_element(
129138
return None
130139

131140
mutation = """
132-
mutation ($conversationId: ID!, $type: String!, $url: String!, $name: String!, $display: String!, $forId: String) {
133-
createElement(conversationId: $conversationId, type: $type, url: $url, name: $name, display: $display, forId: $forId) {
141+
mutation ($conversationId: ID!, $type: String!, $url: String!, $name: String!, $display: String!, $size: String, $language: String, $forId: String) {
142+
createElement(conversationId: $conversationId, type: $type, url: $url, name: $name, display: $display, size: $size, language: $language, forId: $forId) {
134143
id,
135144
type,
136145
url,
137146
name,
138147
display,
148+
size,
149+
language,
139150
forId
140151
}
141152
}
@@ -146,6 +157,8 @@ def create_element(
146157
"url": url,
147158
"name": name,
148159
"display": display,
160+
"size": size,
161+
"language": language,
149162
"forId": for_id,
150163
}
151164
res = self.mutation(mutation, variables)

src/chainlit/element.py

Lines changed: 49 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from abc import ABC, abstractmethod
55
from chainlit.sdk import get_sdk, BaseClient
66
from chainlit.telemetry import trace_event
7-
from chainlit.types import ElementType, ElementDisplay
7+
from chainlit.types import ElementType, ElementDisplay, ElementSize
88

99

1010
@dataclass_json
@@ -43,20 +43,23 @@ def send(self, for_id: str = None):
4343

4444

4545
@dataclass
46-
class LocalElementBase:
47-
content: bytes
46+
class LocalElement(Element):
47+
content: bytes = None
4848

49-
50-
@dataclass
51-
class LocalElement(Element, LocalElementBase):
5249
def persist(self, client: BaseClient, for_id: str = None):
50+
if not self.content:
51+
raise ValueError("Must provide content")
5352
url = client.upload_element(content=self.content)
5453
if url:
54+
size = getattr(self, "size", None)
55+
language = getattr(self, "language", None)
5556
element = client.create_element(
5657
name=self.name,
5758
url=url,
5859
type=self.type,
5960
display=self.display,
61+
size=size,
62+
language=language,
6063
for_id=for_id,
6164
)
6265
return element
@@ -67,54 +70,67 @@ class RemoteElementBase:
6770
url: str
6871

6972

73+
@dataclass
74+
class ImageBase:
75+
type: ElementType = "image"
76+
size: ElementSize = "medium"
77+
78+
7079
@dataclass
7180
class RemoteElement(Element, RemoteElementBase):
7281
def persist(self, client: BaseClient, for_id: str = None):
82+
size = getattr(self, "size", None)
83+
language = getattr(self, "language", None)
7384
element = client.create_element(
7485
name=self.name,
7586
url=self.url,
7687
type=self.type,
7788
display=self.display,
89+
size=size,
90+
language=language,
7891
for_id=for_id,
7992
)
8093
return element
8194

8295

83-
class LocalImage(LocalElement):
84-
def __init__(
85-
self,
86-
name: str,
87-
display: ElementDisplay = "side",
88-
path: str = None,
89-
content: bytes = None,
90-
):
91-
if path:
92-
with open(path, "rb") as f:
96+
@dataclass
97+
class LocalImage(ImageBase, LocalElement):
98+
"""Useful to send an image living on the local filesystem to the UI."""
99+
100+
path: str = None
101+
102+
def __post_init__(self):
103+
if self.path:
104+
with open(self.path, "rb") as f:
93105
self.content = f.read()
94-
elif content:
95-
self.content = content
106+
elif self.content:
107+
self.content = self.content
96108
else:
97109
raise ValueError("Must provide either path or content")
98110

99-
self.name = name
100-
self.display = display
101-
self.type = "image"
102111

112+
@dataclass
113+
class RemoteImage(ImageBase, RemoteElement):
114+
"""Useful to send an image based on an URL to the UI."""
115+
116+
pass
103117

104-
class RemoteImage(RemoteElement):
105-
def __init__(self, name: str, url: str, display: ElementDisplay = "side"):
106-
self.name = name
107-
self.display = display
108-
self.type = "image"
109-
self.url = url
110118

119+
@dataclass
120+
class TextBase:
121+
text: str
122+
123+
124+
@dataclass
125+
class Text(LocalElement, TextBase):
126+
"""Useful to send a text (not a message) to the UI."""
127+
128+
type: ElementType = "text"
129+
content = bytes("", "utf-8")
130+
language: str = None
111131

112-
class Text(LocalElement):
113-
def __init__(self, name: str, text: str, display: ElementDisplay = "side"):
114-
self.name = name
115-
self.display = display
116-
self.type = "text"
117-
self.content = bytes(text, "utf-8")
132+
def __post_init__(self):
133+
self.content = bytes(self.text, "utf-8")
118134

119135
def before_emit(self, text_element):
120136
if "content" in text_element and isinstance(text_element["content"], bytes):
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { Box, BoxProps } from '@mui/material/';
2+
3+
export default function ElementFrame(props: BoxProps) {
4+
return (
5+
<Box
6+
sx={{
7+
p: 1,
8+
boxSizing: 'border-box',
9+
bgcolor: (theme) =>
10+
theme.palette.mode === 'light' ? '#EEEEEE' : '#212121',
11+
borderRadius: '4px',
12+
display: 'flex'
13+
}}
14+
>
15+
{props.children}
16+
</Box>
17+
);
18+
}
Lines changed: 23 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,36 @@
1-
import { Box } from '@mui/material';
2-
import { IElement } from 'state/element';
1+
import { IImageElement } from 'state/element';
2+
import ImageFrame from './frame';
33

44
interface Props {
5-
element: IElement;
5+
element: IImageElement;
66
}
77

88
export default function ImageElement({ element }: Props) {
99
const src = element.url || URL.createObjectURL(new Blob([element.content!]));
10+
const className = `${element.display}-image`;
1011
return (
11-
<Box
12-
sx={{
13-
p: 1,
14-
boxSizing: 'border-box',
15-
bgcolor: (theme) =>
16-
theme.palette.mode === 'light' ? '#EEEEEE' : '#212121',
17-
borderRadius: '4px'
18-
}}
19-
>
12+
<ImageFrame>
2013
<img
14+
className={className}
2115
src={src}
22-
style={{ objectFit: 'cover', width: '100%' }}
16+
onClick={(e) => {
17+
if (element.display === 'inline') {
18+
const w = window.open('');
19+
const target = e.target as HTMLImageElement;
20+
w?.document.write(`<img src="${target.src}" />`);
21+
}
22+
}}
23+
style={{
24+
objectFit: 'cover',
25+
maxWidth: '100%',
26+
margin: 'auto',
27+
height: 'auto',
28+
display: 'block',
29+
cursor: element.display === 'inline' ? 'pointer' : 'default'
30+
}}
2331
alt={element.name}
32+
loading="lazy"
2433
/>
25-
</Box>
34+
</ImageFrame>
2635
);
2736
}

src/chainlit/frontend/src/components/element/inlined/image.tsx

Lines changed: 28 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,65 +1,46 @@
11
import ImageList from '@mui/material/ImageList';
22
import ImageListItem from '@mui/material/ImageListItem';
3+
import { IImageElement } from 'state/element';
4+
import ImageElement from '../image';
35

46
interface Props {
5-
items: {
6-
url?: string;
7-
src: string;
8-
title: string;
9-
}[];
7+
images: IImageElement[];
108
}
119

12-
export default function InlinedImageList({ items }: Props) {
10+
function sizeToUnit(image: IImageElement) {
11+
if (image.size === 'small') {
12+
return 1;
13+
} else if (image.size === 'medium') {
14+
return 2;
15+
} else if (image.size === 'large') {
16+
return 4;
17+
} else {
18+
return 2;
19+
}
20+
}
21+
22+
export default function InlinedImageList({ images }: Props) {
1323
return (
1424
<ImageList
1525
sx={{
1626
margin: 0,
17-
width: '100%',
18-
maxWidth: '600px',
19-
height: 200,
2027
// Promote the list into its own layer in Chrome. This costs memory, but helps keeping high FPS.
21-
transform: 'translateZ(0)'
28+
transform: 'translateZ(0)',
29+
width: '100%',
30+
maxWidth: 600,
31+
maxHeight: 400
2232
}}
23-
rowHeight={200}
24-
gap={5}
33+
variant="quilted"
34+
cols={4}
35+
gap={8}
2536
>
26-
{items.map((item) => {
27-
const cols = 1;
28-
const rows = 1;
37+
{images.map((image, i) => {
38+
const cols = sizeToUnit(image);
39+
const rows = sizeToUnit(image);
2940

3041
return (
31-
<ImageListItem
32-
key={item.src}
33-
cols={cols}
34-
rows={rows}
35-
sx={{
36-
'.MuiImageListItem-img': {
37-
height: '100%',
38-
width: 'auto',
39-
p: 1,
40-
boxSizing: 'border-box',
41-
bgcolor: (theme) =>
42-
theme.palette.mode === 'light' ? '#EEEEEE' : '#212121',
43-
borderRadius: '4px'
44-
}
45-
}}
46-
>
47-
<img
48-
className="inlined-image"
49-
src={item.src}
50-
alt={item.title}
51-
style={{
52-
objectFit: 'contain',
53-
cursor: item.url ? 'pointer' : 'default'
54-
}}
55-
onClick={() => {
56-
if (item.url) {
57-
window.open(item.url, '_blank')?.focus();
58-
}
59-
}}
60-
loading="lazy"
61-
/>
62-
{/* <ImageListItemBar title={item.title} position="top" /> */}
42+
<ImageListItem key={i} cols={cols} rows={rows}>
43+
<ImageElement element={image} />
6344
</ImageListItem>
6445
);
6546
})}

0 commit comments

Comments
 (0)