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"}) | |