alonsosilva's picture
Add vertical scrolling
9e78d9a unverified
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,
},
]
@solara.component
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
@solara.component
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"})
@solara.component
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"])
@solara.component
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}
"""
)
@solara.component
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"})