alonsosilva commited on
Commit
d5bd974
·
1 Parent(s): ea96e70
Files changed (4) hide show
  1. Dockerfile +28 -0
  2. LICENSE +21 -0
  3. app.py +314 -0
  4. requirements.txt +1 -0
Dockerfile ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.11
2
+
3
+ # Set up a new user named "user" with user ID 1000
4
+ RUN useradd -m -u 1000 user
5
+
6
+ # Switch to the "user" user
7
+ USER user
8
+
9
+ # Set home to the user's home directory
10
+ ENV HOME=/home/user \
11
+ PATH=/home/user/.local/bin:$PATH
12
+
13
+ # Set the working directory to the user's home directory
14
+ WORKDIR $HOME/app
15
+
16
+ # Try and run pip command after setting the user with `USER user` to avoid permission issues with Python
17
+ RUN pip install --no-cache-dir --upgrade pip
18
+
19
+ # Copy the current directory contents into the container at $HOME/app setting the owner to the user
20
+ COPY --chown=user . $HOME/app
21
+
22
+ COPY --chown=user requirements.txt .
23
+
24
+ RUN pip install --no-cache-dir --upgrade -r requirements.txt
25
+
26
+ COPY --chown=user app.py app.py
27
+
28
+ ENTRYPOINT ["solara", "run", "app.py", "--host=0.0.0.0", "--port", "7860"]
LICENSE ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ MIT License
2
+
3
+ Copyright (c) 2023 Alonso Silva Allende
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
app.py ADDED
@@ -0,0 +1,314 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import solara
2
+ import time
3
+ import random
4
+ from typing import List
5
+ from typing_extensions import TypedDict
6
+ from solara.components.input import use_change
7
+
8
+ # Streamed response emulator
9
+ def response_generator():
10
+ response = random.choice(
11
+ [
12
+ "Hello! How can I assist you today?",
13
+ "Hey there! If you have any questions or need help with something, feel free to ask.",
14
+ ]
15
+ )
16
+ for word in response.split():
17
+ yield word + " "
18
+ time.sleep(0.05)
19
+
20
+ class MessageDict(TypedDict):
21
+ role: str
22
+ content: str
23
+
24
+ messages: solara.Reactive[List[MessageDict]] = solara.reactive([])
25
+
26
+ def add_chunk_to_ai_message(chunk: str):
27
+ messages.value = [
28
+ *messages.value[:-1],
29
+ {
30
+ "role": "assistant",
31
+ "content": messages.value[-1]["content"] + chunk,
32
+ },
33
+ ]
34
+
35
+ css = """
36
+ chat-box {
37
+ background-color: blue!important;
38
+ }
39
+
40
+ .v-application--wrap > div:nth-child(2) > div:nth-child(2){
41
+ display: none !important;
42
+ background-color: #00ff00!important;
43
+ }
44
+ """
45
+ @solara.component
46
+ def GithubAvatar(name: str, handle: str, img: str):
47
+ with solara.v.Html(tag="a", attributes={"href": f"https://github.com/{handle}/", "target": "_blank"}):
48
+ with solara.v.ListItem(class_="pa-0"):
49
+ with solara.v.ListItemAvatar(color="grey darken-3"):
50
+ solara.v.Img(
51
+ class_="elevation-6",
52
+ src=img,
53
+ )
54
+ with solara.v.ListItemContent():
55
+ solara.v.ListItemTitle(children=["By " + name])
56
+
57
+ import uuid
58
+ from typing import Callable, Dict, List, Optional, Union
59
+ from typing_extensions import Literal
60
+ @solara.component
61
+ def ChatBox(
62
+ children: List[solara.Element] = [],
63
+ style: Optional[Union[str, Dict[str, str]]] = None,
64
+ classes: List[str] = [],
65
+ ):
66
+ """
67
+ The ChatBox component is a container for ChatMessage components.
68
+ Its primary use is to ensure the proper ordering of messages,
69
+ using `flex-direction: column-reverse` together with `reversed(messages)`.
70
+
71
+ # Arguments
72
+
73
+ * `children`: A list of child components.
74
+ * `style`: CSS styles to apply to the component. Either a string or a dictionary.
75
+ * `classes`: A list of CSS classes to apply to the component.
76
+ """
77
+ style_flat = solara.util._flatten_style(style)
78
+ style_flat += " background-color:transparent!important;"
79
+ if "flex-grow" not in style_flat:
80
+ style_flat += " flex-grow: 1;"
81
+ if "flex-direction" not in style_flat:
82
+ style_flat += " flex-direction: column-reverse;"
83
+ if "overflow-y" not in style_flat:
84
+ style_flat += " overflow-y: auto;"
85
+
86
+ #classes += ["chat-box"]
87
+ with solara.Column(
88
+ style=style_flat,
89
+ classes=classes,
90
+ ):
91
+ for child in list(reversed(children)):
92
+ solara.display(child, style={"background-color":"transparent!important"})
93
+
94
+ @solara.component
95
+ def ChatInput(
96
+ send_callback: Optional[Callable] = None,
97
+ disabled: bool = False,
98
+ style: Optional[Union[str, Dict[str, str]]] = None,
99
+ classes: List[str] = [],
100
+ ):
101
+ """
102
+ The ChatInput component renders a text input together with a send button.
103
+
104
+ # Arguments
105
+
106
+ * `send_callback`: A callback function for when the user presses enter or clicks the send button.
107
+ * `disabled`: Whether the input should be disabled. Useful for disabling sending further messages while a chatbot is replying,
108
+ among other things.
109
+ * `style`: CSS styles to apply to the component. Either a string or a dictionary. These styles are applied to the container component.
110
+ * `classes`: A list of CSS classes to apply to the component. Also applied to the container.
111
+ """
112
+ message, set_message = solara.use_state("") # type: ignore
113
+ style_flat = solara.util._flatten_style(style)
114
+
115
+ if "align-items" not in style_flat:
116
+ style_flat += " align-items: center;"
117
+
118
+ with solara.Row(style=style_flat, classes=classes):
119
+
120
+ def send(*ignore_args):
121
+ if message != "" and send_callback is not None:
122
+ send_callback(message)
123
+ set_message("")
124
+
125
+ message_input = solara.v.TextField(
126
+ label="Send message...",
127
+ v_model=message,
128
+ on_v_model=set_message,
129
+ rounded=True,
130
+ filled=True,
131
+ hide_details=True,
132
+ style_="flex-grow: 1;",
133
+ disabled=disabled,
134
+ color="secondary",
135
+ )
136
+
137
+ use_change(message_input, send, update_events=["keyup.enter"])
138
+
139
+ button = solara.v.Btn(icon=True, children=[solara.v.Icon(children=["mdi-send"])], disabled=message == "")
140
+
141
+ use_change(button, send, update_events=["click"])
142
+
143
+ @solara.component
144
+ def ChatMessage(
145
+ children: Union[List[solara.Element], str],
146
+ user: bool = False,
147
+ avatar: Union[solara.Element, str, Literal[False], None] = None,
148
+ name: Optional[str] = None,
149
+ color: Optional[str] = None,
150
+ avatar_background_color: Optional[str] = None,
151
+ border_radius: Optional[str] = None,
152
+ notch: bool = False,
153
+ style: Optional[Union[str, Dict[str, str]]] = None,
154
+ classes: List[str] = [],
155
+ ):
156
+ """
157
+ The ChatMessage component renders a message. Messages with `user=True` are rendered on the right side of the screen,
158
+ all others on the left.
159
+
160
+ # Arguments
161
+
162
+ * `children`: A list of child components.
163
+ * `user`: Whether the message is from the current user or not.
164
+ * `avatar`: An avatar to display next to the message. Can be a string representation of a URL or Material design icon name,
165
+ a solara Element, False to disable avatars altogether, or None to display initials based on `name`.
166
+ * `name`: The name of the user who sent the message.
167
+ * `color`: The background color of the message. Defaults to `rgba(0,0,0,.06)`. Can be any valid CSS color.
168
+ * `avatar_background_color`: The background color of the avatar. Defaults to `color` if left as `None`.
169
+ * `border_radius`: Sets the roundness of the corners of the message. Defaults to `None`,
170
+ which applies the default border radius of a `solara.Column`, i.e. `4px`.
171
+ * `notch`: Whether to display a speech bubble style notch on the side of the message.
172
+ * `style`: CSS styles to apply to the component. Either a string or a dictionary. Applied to the container of the message.
173
+ * `classes`: A list of CSS classes to apply to the component. Applied to the same container.
174
+ """
175
+ style_flat = solara.util._flatten_style(style)
176
+
177
+ if "border-radius" not in style_flat:
178
+ style_flat += f" border-radius: {border_radius if border_radius is not None else ''};"
179
+ if f"border-top-{'right' if user else 'left'}-radius" not in style_flat:
180
+ style_flat += f" border-top-{'right' if user else 'left'}-radius: 0;"
181
+ if "padding" not in style_flat:
182
+ style_flat += " padding: .5em 1.5em;"
183
+
184
+ msg_uuid = solara.use_memo(lambda: str(uuid.uuid4()), dependencies=[])
185
+ with solara.Row(
186
+ justify="end" if user else "start",
187
+ style={"color":"transparent!important", "flex-direction": "row-reverse" if user else "row", "padding": "0px"},
188
+ ):
189
+ if avatar is not False:
190
+ with solara.v.Avatar(color=avatar_background_color if avatar_background_color is not None else color):
191
+ if avatar is None and name is not None:
192
+ initials = "".join([word[:1] for word in name.split(" ")])
193
+ solara.HTML(tag="span", unsafe_innerHTML=initials, classes=["headline"])
194
+ elif isinstance(avatar, solara.Element):
195
+ solara.display(avatar)
196
+ elif isinstance(avatar, str) and avatar.startswith("mdi-"):
197
+ solara.v.Icon(children=[avatar])
198
+ else:
199
+ solara.HTML(tag="img", attributes={"src": avatar, "width": "100%"})
200
+ classes_new = classes + ["chat-message-" + msg_uuid, "right" if user else "left"]
201
+ with solara.Column(
202
+ classes=classes_new,
203
+ gap=0,
204
+ style=style_flat,
205
+ ):
206
+ if name is not None:
207
+ solara.Text(name, style="font-weight: bold;", classes=["message-name", "right" if user else "left"])
208
+ for child in children:
209
+ if isinstance(child, solara.Element):
210
+ solara.display(child)
211
+ else:
212
+ solara.Markdown(child)
213
+ # we use the uuid to generate 'scoped' CSS, i.e. css that only applies to the component instance.
214
+ extra_styles = (
215
+ f""".chat-message-{msg_uuid}:before{{
216
+ content: '';
217
+ position: absolute;
218
+ width: 0;
219
+ height: 0;
220
+ border: 6px solid;
221
+ top: 0;
222
+ color: transparent;
223
+ }}
224
+ .chat-message-{msg_uuid}.left:before{{
225
+ left: -12px;
226
+ color: transparent;
227
+ border-color: var(--color) var(--color) transparent transparent;
228
+ }}
229
+ .chat-message-{msg_uuid}.right:before{{
230
+ right: -12px;
231
+ border-color: var(--color) transparent transparent var(--color);
232
+ color: transparent;
233
+ }}"""
234
+ if notch
235
+ else ""
236
+ )
237
+ solara.Style(
238
+ f"""
239
+ .chat-message-{msg_uuid}{{
240
+ color: transparent!important;
241
+ max-width: 75%;
242
+ position: relative;
243
+ }}
244
+ .chat-message-{msg_uuid}.left{{
245
+ color: transparent!important;
246
+ border-top-left-radius: 0;
247
+ background-color:var(--color);
248
+ { "margin-left: 10px !important;" if notch else ""}
249
+ }}
250
+ .chat-message-{msg_uuid}.right{{
251
+ color: transparent!important;
252
+ border-top-right-radius: 0;
253
+ background-color:var(--color);
254
+ { "margin-right: 10px !important;" if notch else ""}
255
+ }}
256
+ {extra_styles}
257
+ """
258
+ )
259
+
260
+ @solara.component
261
+ def Page():
262
+ solara.lab.theme.themes.light.primary = "#ff0000"
263
+ solara.lab.theme.themes.light.secondary = "#0000ff"
264
+ solara.lab.theme.themes.dark.primary = "#ff0000"
265
+ solara.lab.theme.themes.dark.secondary = "#0000ff"
266
+ solara.Style(css)
267
+ title = "Customized StreamBot"
268
+ with solara.Head():
269
+ solara.Title(f"{title}")
270
+ with solara.Sidebar():
271
+ solara.lab.ThemeToggle(enable_auto=False)
272
+ solara.Markdown(f"#{title}")
273
+ GithubAvatar(
274
+ "Alonso Silva Allende",
275
+ "alonsosilvaallende",
276
+ "https://avatars.githubusercontent.com/u/30263736?v=4",
277
+ )
278
+ with solara.Columns([1,1],style="padding: 20em 10em 20em 10em;color: transparent!important; background-color: #00ff00!important; background-image: url(https://wallpapercave.com/wp/DlGpnB5.jpg)"):
279
+ with solara.Column(align="center",style={"background-color":"red"}):
280
+ user_message_count = len([m for m in messages.value if m["role"] == "user"])
281
+
282
+ def send(message):
283
+ messages.value = [
284
+ *messages.value,
285
+ {"role": "user", "content": message},
286
+ ]
287
+
288
+ def response(message):
289
+ messages.value = [*messages.value, {"role": "assistant", "content": ""}]
290
+ for chunk in response_generator():
291
+ add_chunk_to_ai_message(chunk)
292
+
293
+
294
+ def result():
295
+ if messages.value !=[]: response(messages.value[-1]["content"])
296
+
297
+ result = solara.lab.use_task(result, dependencies=[user_message_count]) # type: ignore
298
+
299
+ with ChatBox(style={"background-color":"transparent!important","flex-grow":"1","position": "fixed", "bottom": "10rem", "width": "50%"}):
300
+ for item in messages.value:
301
+ with ChatMessage(
302
+ user=item["role"] == "user",
303
+ name="StreamBot" if item["role"] == "assistant" else "User",
304
+ notch=True,
305
+ avatar="https://avatars.githubusercontent.com/u/127238744?v=4" if item["role"] == "user" else "https://avatars.githubusercontent.com/u/784313?v=4",
306
+ avatar_background_color="#33cccc" if item["role"] == "assistant" else "#ff991f",
307
+ border_radius="20px",
308
+ style={"color":"red!important","background-color":"orange!important"} if item["role"] == "user" else {"color":"red!important", "background-color":"aqua!important"},
309
+ ):
310
+ solara.Markdown(
311
+ item["content"],
312
+ style={"color":"green!important","backgound-color":"transpartent!important"} if item["role"] == "user" else {"color":"blue!important"}
313
+ )
314
+ ChatInput(send_callback=send, style={"position": "fixed", "bottom": "3rem", "width": "60%", "color":"green!important", "background-color":"red!important"})
requirements.txt ADDED
@@ -0,0 +1 @@
 
 
1
+ solara==1.31.0