Spaces:
Sleeping
Sleeping
import solara | |
import time | |
import random | |
from typing import List | |
from typing_extensions import TypedDict | |
from solara.components.input import use_change | |
# Streamed response emulator | |
def response_generator(): | |
response = random.choice( | |
[ | |
"Hello! How can I assist you today?", | |
"Hey there! If you have any questions or need help with something, feel free to ask.", | |
] | |
) | |
for word in response.split(): | |
yield word + " " | |
time.sleep(0.05) | |
class MessageDict(TypedDict): | |
role: str | |
content: str | |
messages: solara.Reactive[List[MessageDict]] = solara.reactive([]) | |
def add_chunk_to_ai_message(chunk: str): | |
messages.value = [ | |
*messages.value[:-1], | |
{ | |
"role": "assistant", | |
"content": messages.value[-1]["content"] + chunk, | |
}, | |
] | |
def GithubAvatar(name: str, handle: str, img: str): | |
with solara.v.Html(tag="a", attributes={"href": f"https://github.com/{handle}/", "target": "_blank"}): | |
with solara.v.ListItem(class_="pa-0"): | |
with solara.v.ListItemAvatar(color="grey darken-3"): | |
solara.v.Img( | |
class_="elevation-6", | |
src=img, | |
) | |
with solara.v.ListItemContent(): | |
solara.v.ListItemTitle(children=["By " + name]) | |
import uuid | |
from typing import Callable, Dict, List, Optional, Union | |
from typing_extensions import Literal | |
def ChatBox( | |
children: List[solara.Element] = [], | |
style: Optional[Union[str, Dict[str, str]]] = None, | |
classes: List[str] = [], | |
): | |
""" | |
The ChatBox component is a container for ChatMessage components. | |
Its primary use is to ensure the proper ordering of messages, | |
using `flex-direction: column-reverse` together with `reversed(messages)`. | |
# Arguments | |
* `children`: A list of child components. | |
* `style`: CSS styles to apply to the component. Either a string or a dictionary. | |
* `classes`: A list of CSS classes to apply to the component. | |
""" | |
style_flat = solara.util._flatten_style(style) | |
style_flat += " background-color:transparent!important;" | |
if "flex-grow" not in style_flat: | |
style_flat += " flex-grow: 1;" | |
if "flex-direction" not in style_flat: | |
style_flat += " flex-direction: column-reverse;" | |
if "overflow-y" not in style_flat: | |
style_flat += " overflow-y: auto;" | |
#classes += ["chat-box"] | |
with solara.Column( | |
style=style_flat, | |
classes=classes, | |
): | |
for child in list(reversed(children)): | |
solara.display(child, style={"background-color":"transparent!important"}) | |
def ChatInput( | |
send_callback: Optional[Callable] = None, | |
disabled: bool = False, | |
style: Optional[Union[str, Dict[str, str]]] = None, | |
classes: List[str] = [], | |
): | |
""" | |
The ChatInput component renders a text input together with a send button. | |
# Arguments | |
* `send_callback`: A callback function for when the user presses enter or clicks the send button. | |
* `disabled`: Whether the input should be disabled. Useful for disabling sending further messages while a chatbot is replying, | |
among other things. | |
* `style`: CSS styles to apply to the component. Either a string or a dictionary. These styles are applied to the container component. | |
* `classes`: A list of CSS classes to apply to the component. Also applied to the container. | |
""" | |
message, set_message = solara.use_state("") # type: ignore | |
style_flat = solara.util._flatten_style(style) | |
if "align-items" not in style_flat: | |
style_flat += " align-items: center;" | |
with solara.Row(style=style_flat, classes=classes): | |
def send(*ignore_args): | |
if message != "" and send_callback is not None: | |
send_callback(message) | |
set_message("") | |
message_input = solara.v.TextField( | |
label="Send message...", | |
v_model=message, | |
on_v_model=set_message, | |
rounded=True, | |
filled=True, | |
hide_details=True, | |
style_="flex-grow: 1;", | |
disabled=disabled, | |
color="secondary", | |
) | |
use_change(message_input, send, update_events=["keyup.enter"]) | |
button = solara.v.Btn(icon=True, children=[solara.v.Icon(children=["mdi-send"])], disabled=message == "") | |
use_change(button, send, update_events=["click"]) | |
def ChatMessage( | |
children: Union[List[solara.Element], str], | |
user: bool = False, | |
avatar: Union[solara.Element, str, Literal[False], None] = None, | |
name: Optional[str] = None, | |
color: Optional[str] = None, | |
avatar_background_color: Optional[str] = None, | |
border_radius: Optional[str] = None, | |
notch: bool = False, | |
style: Optional[Union[str, Dict[str, str]]] = None, | |
classes: List[str] = [], | |
): | |
""" | |
The ChatMessage component renders a message. Messages with `user=True` are rendered on the right side of the screen, | |
all others on the left. | |
# Arguments | |
* `children`: A list of child components. | |
* `user`: Whether the message is from the current user or not. | |
* `avatar`: An avatar to display next to the message. Can be a string representation of a URL or Material design icon name, | |
a solara Element, False to disable avatars altogether, or None to display initials based on `name`. | |
* `name`: The name of the user who sent the message. | |
* `color`: The background color of the message. Defaults to `rgba(0,0,0,.06)`. Can be any valid CSS color. | |
* `avatar_background_color`: The background color of the avatar. Defaults to `color` if left as `None`. | |
* `border_radius`: Sets the roundness of the corners of the message. Defaults to `None`, | |
which applies the default border radius of a `solara.Column`, i.e. `4px`. | |
* `notch`: Whether to display a speech bubble style notch on the side of the message. | |
* `style`: CSS styles to apply to the component. Either a string or a dictionary. Applied to the container of the message. | |
* `classes`: A list of CSS classes to apply to the component. Applied to the same container. | |
""" | |
style_flat = solara.util._flatten_style(style) | |
if "border-radius" not in style_flat: | |
style_flat += f" border-radius: {border_radius if border_radius is not None else ''};" | |
if f"border-top-{'right' if user else 'left'}-radius" not in style_flat: | |
style_flat += f" border-top-{'right' if user else 'left'}-radius: 0;" | |
if "padding" not in style_flat: | |
style_flat += " padding: .5em 1.5em;" | |
msg_uuid = solara.use_memo(lambda: str(uuid.uuid4()), dependencies=[]) | |
with solara.Row( | |
justify="end" if user else "start", | |
style={"color":"transparent!important", "flex-direction": "row-reverse" if user else "row", "padding": "0px"}, | |
): | |
if avatar is not False: | |
with solara.v.Avatar(color=avatar_background_color if avatar_background_color is not None else color): | |
if avatar is None and name is not None: | |
initials = "".join([word[:1] for word in name.split(" ")]) | |
solara.HTML(tag="span", unsafe_innerHTML=initials, classes=["headline"]) | |
elif isinstance(avatar, solara.Element): | |
solara.display(avatar) | |
elif isinstance(avatar, str) and avatar.startswith("mdi-"): | |
solara.v.Icon(children=[avatar]) | |
else: | |
solara.HTML(tag="img", attributes={"src": avatar, "width": "100%"}) | |
classes_new = classes + ["chat-message-" + msg_uuid, "right" if user else "left"] | |
with solara.Column( | |
classes=classes_new, | |
gap=0, | |
style=style_flat, | |
): | |
if name is not None: | |
solara.Text(name, style="font-weight: bold;", classes=["message-name", "right" if user else "left"]) | |
for child in children: | |
if isinstance(child, solara.Element): | |
solara.display(child) | |
else: | |
solara.Markdown(child) | |
# we use the uuid to generate 'scoped' CSS, i.e. css that only applies to the component instance. | |
extra_styles = ( | |
f""".chat-message-{msg_uuid}:before{{ | |
content: ''; | |
position: absolute; | |
width: 0; | |
height: 0; | |
border: 6px solid; | |
top: 0; | |
color: transparent; | |
}} | |
.chat-message-{msg_uuid}.left:before{{ | |
left: -12px; | |
color: transparent; | |
border-color: var(--color) var(--color) transparent transparent; | |
}} | |
.chat-message-{msg_uuid}.right:before{{ | |
right: -12px; | |
border-color: var(--color) transparent transparent var(--color); | |
color: transparent; | |
}}""" | |
if notch | |
else "" | |
) | |
solara.Style( | |
f""" | |
.chat-message-{msg_uuid}{{ | |
color: transparent!important; | |
max-width: 75%; | |
position: relative; | |
}} | |
.chat-message-{msg_uuid}.left{{ | |
color: transparent!important; | |
border-top-left-radius: 0; | |
background-color:var(--color); | |
{ "margin-left: 10px !important;" if notch else ""} | |
}} | |
.chat-message-{msg_uuid}.right{{ | |
color: transparent!important; | |
border-top-right-radius: 0; | |
background-color:var(--color); | |
{ "margin-right: 10px !important;" if notch else ""} | |
}} | |
{extra_styles} | |
""" | |
) | |
def Page(): | |
solara.lab.theme.themes.light.primary = "#ff0000" | |
solara.lab.theme.themes.light.secondary = "#0000ff" | |
solara.lab.theme.themes.dark.primary = "#ff0000" | |
solara.lab.theme.themes.dark.secondary = "#0000ff" | |
title = "Customized StreamBot" | |
with solara.Head(): | |
solara.Title(f"{title}") | |
with solara.AppBar(): | |
solara.lab.ThemeToggle(enable_auto=False) | |
with solara.Sidebar(): | |
solara.Markdown(f"#{title}") | |
GithubAvatar( | |
"Alonso Silva Allende", | |
"alonsosilvaallende", | |
"https://avatars.githubusercontent.com/u/30263736?v=4", | |
) | |
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)"): | |
with solara.Column(align="center",style={"background-color":"red"}): | |
user_message_count = len([m for m in messages.value if m["role"] == "user"]) | |
def send(message): | |
messages.value = [ | |
*messages.value, | |
{"role": "user", "content": message}, | |
] | |
def response(message): | |
messages.value = [*messages.value, {"role": "assistant", "content": ""}] | |
for chunk in response_generator(): | |
add_chunk_to_ai_message(chunk) | |
def result(): | |
if messages.value !=[]: response(messages.value[-1]["content"]) | |
result = solara.lab.use_task(result, dependencies=[user_message_count]) # type: ignore | |
#with ChatBox(style={"background-color":"transparent!important","flex-grow":"1","position": "fixed", "bottom": "10rem", "width": "50%"}): | |
with ChatBox(style={"position": "fixed", "overflow-y": "scroll","scrollbar-width": "none", "-ms-overflow-style": "none", "top": "4.5rem", "bottom": "10rem", "width": "50%"}): | |
for item in messages.value: | |
with ChatMessage( | |
user=item["role"] == "user", | |
name="StreamBot" if item["role"] == "assistant" else "User", | |
notch=True, | |
avatar="https://avatars.githubusercontent.com/u/127238744?v=4" if item["role"] == "user" else "https://avatars.githubusercontent.com/u/784313?v=4", | |
avatar_background_color="#33cccc" if item["role"] == "assistant" else "#ff991f", | |
border_radius="20px", | |
style={"color":"red!important","background-color":"orange!important","font-family":"Comic Sans MS"} if item["role"] == "user" else {"color":"red!important", "background-color":"aqua!important","font-family":"Comic Sans MS"}, | |
): | |
solara.Markdown( | |
item["content"], | |
style={"color":"green!important","backgound-color":"transpartent!important","font-family":"Comic Sans MS"} if item["role"] == "user" else {"color":"blue!important","font-family":"Comic Sans MS"} | |
) | |
ChatInput(send_callback=send, style={"position": "fixed", "bottom": "3rem", "width": "60%", "color":"green!important", "background-color":"red!important"}) | |