diff --git a/lorem-stream/.gitignore b/lorem-stream/.gitignore new file mode 100644 index 00000000..eab0d4b0 --- /dev/null +++ b/lorem-stream/.gitignore @@ -0,0 +1,4 @@ +*.db +*.py[cod] +.web +__pycache__/ \ No newline at end of file diff --git a/lorem-stream/README.md b/lorem-stream/README.md new file mode 100644 index 00000000..846420b2 --- /dev/null +++ b/lorem-stream/README.md @@ -0,0 +1,35 @@ +# `lorem_stream` + +This example uses background tasks to concurrently generate and stream lorem +text, simulating how an app might make multiple API calls and display the +results as they become available. + +The state keeps track of several dicts that are keyed on the task id: + + * `running: bool` if the task should keep processing data + * `progress: int` how many iterations the task has completed + * `end_at: int` the task stops after this many iterations + * `text: str` the actual generated text + +## `LoremState.stream_text` + +This is the background task that does most of the work. When starting, if a +`task_id` is not provided, it assigns the next available task id to itself; +otherwise it will assume the values of the given `task_id`. + +The task then proceeds to iterate a random number of times, generating 3 lorem +words on each iteration. + +## UI + +The page initially only shows the "New Task" button. Each time it is clicked, a +new `stream_text` task is started. + +The tasks are presented as a grid of cards, each of which shows the progress of +the task, a play/pause/restart button, and a kill/delete button. Below the +controls, the text streams as it is available. + + +https://github.com/reflex-dev/reflex-examples/assets/1524005/09c832ff-ecbd-4a9d-a8a5-67779c673045 + + diff --git a/lorem-stream/assets/favicon.ico b/lorem-stream/assets/favicon.ico new file mode 100644 index 00000000..609f6abc Binary files /dev/null and b/lorem-stream/assets/favicon.ico differ diff --git a/lorem-stream/lorem_stream/__init__.py b/lorem-stream/lorem_stream/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/lorem-stream/lorem_stream/lorem_stream.py b/lorem-stream/lorem_stream/lorem_stream.py new file mode 100644 index 00000000..f0e91e5c --- /dev/null +++ b/lorem-stream/lorem_stream/lorem_stream.py @@ -0,0 +1,98 @@ +import asyncio +import random + +from lorem_text import lorem + +import reflex as rx + + +ITERATIONS_RANGE = (7, 12) + + +class LoremState(rx.State): + running: dict[int, bool] = {} + progress: dict[int, int] = {} + end_at: dict[int, int] = {} + text: dict[int, str] = {} + + _next_task_id: int = 0 + + @rx.var + def task_ids(self) -> list[int]: + return list(reversed(self.text)) + + @rx.background + async def stream_text(self, task_id: int = -1): + if task_id < 0: + async with self: + task_id = self._next_task_id + self._next_task_id += 1 + self.end_at[task_id] = random.randint(*ITERATIONS_RANGE) + + async with self: + self.running[task_id] = True + start = self.progress.get(task_id, 0) + + for progress in range(start, self.end_at.get(task_id, 0)): + await asyncio.sleep(0.5) + async with self: + if not self.running.get(task_id): + return + self.text[task_id] = self.text.get(task_id, "") + lorem.words(3) + " " + self.progress[task_id] = progress + 1 + + async with self: + self.running.pop(task_id, None) + + def toggle_running(self, task_id: int): + if self.progress.get(task_id, 0) >= self.end_at.get(task_id, 0): + self.progress[task_id] = 0 + self.end_at[task_id] = random.randint(*ITERATIONS_RANGE) + self.text[task_id] = "" + if self.running.get(task_id): + self.running[task_id] = False + else: + return LoremState.stream_text(task_id) + + def kill(self, task_id: int): + self.running.pop(task_id, None) + self.text.pop(task_id, None) + + +def render_task(task_id: int) -> rx.Component: + return rx.vstack( + rx.hstack( + rx.circular_progress( + rx.circular_progress_label(task_id), + value=LoremState.progress[task_id], + max_=LoremState.end_at[task_id], + is_indeterminate=LoremState.progress[task_id] < 1, + ), + rx.button( + rx.cond(LoremState.progress[task_id] < LoremState.end_at[task_id], "⏯️", "🔄"), + on_click=LoremState.toggle_running(task_id), + ), + rx.button("❌", on_click=LoremState.kill(task_id)), + ), + rx.text(LoremState.text[task_id], overflow_y="scroll"), + width=["180px", "190px", "210px", "240px", "300px"], + height="300px", + padding="10px", + ) + + +@rx.page(title="Lorem Streaming Background Tasks") +def index() -> rx.Component: + return rx.vstack( + rx.button("➕ New Task", on_click=LoremState.stream_text(-1)), + rx.flex( + rx.foreach(LoremState.task_ids, render_task), + flex_wrap="wrap", + width="100%", + ), + padding_top="20px", + ) + + +app = rx.App() +app.compile() diff --git a/lorem-stream/requirements.txt b/lorem-stream/requirements.txt new file mode 100644 index 00000000..5f7645ad --- /dev/null +++ b/lorem-stream/requirements.txt @@ -0,0 +1,2 @@ +reflex>=0.2.8 +lorem_text>=2.1 diff --git a/lorem-stream/rxconfig.py b/lorem-stream/rxconfig.py new file mode 100644 index 00000000..2243281f --- /dev/null +++ b/lorem-stream/rxconfig.py @@ -0,0 +1,5 @@ +import reflex as rx + +config = rx.Config( + app_name="lorem_stream", +) \ No newline at end of file