Skip to content

Commit 5a20ceb

Browse files
committed
Merge remote-tracking branch 'upstream/main' into export-event
# Conflicts: # reflex/utils/export.py # reflex/utils/telemetry.py # tests/units/test_telemetry.py
2 parents d8c7903 + 9ed3692 commit 5a20ceb

54 files changed

Lines changed: 2819 additions & 1196 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

README.md

Lines changed: 6 additions & 143 deletions
Original file line numberDiff line numberDiff line change
@@ -105,153 +105,16 @@ If the `reflex` command is not on your PATH, run it through uv instead: `uv run
105105

106106
## 🫧 Example App
107107

108-
Let's go over an example: creating an image generation UI around [DALL·E](https://platform.openai.com/docs/guides/images/image-generation?context=node). For simplicity, we just call the [OpenAI API](https://platform.openai.com/docs/api-reference/authentication), but you could replace this with an ML model run locally.
109-
110-
 
108+
Build an image generation app in Python with Reflex: define the UI, manage state in a class, and call an image model from an event handler.
111109

112110
<div align="center">
113-
<img src="https://raw.githubusercontent.com/reflex-dev/reflex/main/docs/images/dalle.gif" alt="A frontend wrapper for DALL·E, shown in the process of generating an image." width="550" />
111+
<video src="https://github.com/user-attachments/assets/aaff28ad-8b3c-43bf-967e-439ee34c8a87" width="900" controls muted poster="https://raw.githubusercontent.com/reflex-dev/reflex/main/docs/images/reflex-image-generation-app.png">
112+
<a href="https://github.com/user-attachments/assets/aaff28ad-8b3c-43bf-967e-439ee34c8a87">
113+
<img src="https://raw.githubusercontent.com/reflex-dev/reflex/main/docs/images/reflex-image-generation-app.png" alt="Preview of an image generation app built with Reflex" width="900">
114+
</a>
115+
</video>
114116
</div>
115117

116-
&nbsp;
117-
118-
Here is the complete code to create this. This is all done in one Python file!
119-
120-
```python
121-
import reflex as rx
122-
import openai
123-
124-
openai_client = openai.OpenAI()
125-
126-
127-
class State(rx.State):
128-
"""The app state."""
129-
130-
prompt = ""
131-
image_url = ""
132-
processing = False
133-
complete = False
134-
135-
def get_image(self):
136-
"""Get the image from the prompt."""
137-
if self.prompt == "":
138-
return rx.window_alert("Prompt Empty")
139-
140-
self.processing, self.complete = True, False
141-
yield
142-
response = openai_client.images.generate(
143-
prompt=self.prompt, n=1, size="1024x1024"
144-
)
145-
self.image_url = response.data[0].url
146-
self.processing, self.complete = False, True
147-
148-
149-
def index():
150-
return rx.center(
151-
rx.vstack(
152-
rx.heading("DALL-E", font_size="1.5em"),
153-
rx.input(
154-
placeholder="Enter a prompt..",
155-
on_blur=State.set_prompt,
156-
width="25em",
157-
),
158-
rx.button(
159-
"Generate Image",
160-
on_click=State.get_image,
161-
width="25em",
162-
loading=State.processing,
163-
),
164-
rx.cond(
165-
State.complete,
166-
rx.image(src=State.image_url, width="20em"),
167-
),
168-
align="center",
169-
),
170-
width="100%",
171-
height="100vh",
172-
)
173-
174-
175-
# Add state and page to the app.
176-
app = rx.App()
177-
app.add_page(index, title="Reflex:DALL-E")
178-
```
179-
180-
## Let's break this down.
181-
182-
<div align="center">
183-
<img src="https://raw.githubusercontent.com/reflex-dev/reflex/main/docs/images/dalle_colored_code_example.png" alt="Explaining the differences between backend and frontend parts of the DALL-E app." width="900" />
184-
</div>
185-
186-
### **Reflex UI**
187-
188-
Let's start with the UI.
189-
190-
```python
191-
def index():
192-
return rx.center(...)
193-
```
194-
195-
This `index` function defines the frontend of the app.
196-
197-
We use different components such as `center`, `vstack`, `input`, and `button` to build the frontend. Components can be nested within each other
198-
to create complex layouts. And you can use keyword args to style them with the full power of CSS.
199-
200-
Reflex comes with [60+ built-in components](https://reflex.dev/docs/library) to help you get started. We are actively adding more components, and it's easy to [create your own components](https://reflex.dev/docs/wrapping-react/overview/).
201-
202-
### **State**
203-
204-
Reflex represents your UI as a function of your state.
205-
206-
```python
207-
class State(rx.State):
208-
"""The app state."""
209-
210-
prompt = ""
211-
image_url = ""
212-
processing = False
213-
complete = False
214-
```
215-
216-
The state defines all the variables (called vars) in an app that can change and the functions that change them.
217-
218-
Here the state is comprised of a `prompt` and `image_url`. There are also the booleans `processing` and `complete` to indicate when to disable the button (during image generation) and when to show the resulting image.
219-
220-
### **Event Handlers**
221-
222-
```python
223-
def get_image(self):
224-
"""Get the image from the prompt."""
225-
if self.prompt == "":
226-
return rx.window_alert("Prompt Empty")
227-
228-
self.processing, self.complete = True, False
229-
yield
230-
response = openai_client.images.generate(prompt=self.prompt, n=1, size="1024x1024")
231-
self.image_url = response.data[0].url
232-
self.processing, self.complete = False, True
233-
```
234-
235-
Within the state, we define functions called event handlers that change the state vars. Event handlers are the way that we can modify the state in Reflex. They can be called in response to user actions, such as clicking a button or typing in a text box. These actions are called events.
236-
237-
Our DALL·E app has an event handler, `get_image` which gets this image from the OpenAI API. Using `yield` in the middle of an event handler will cause the UI to update. Otherwise the UI will update at the end of the event handler.
238-
239-
### **Routing**
240-
241-
Finally, we define our app.
242-
243-
```python
244-
app = rx.App()
245-
```
246-
247-
We add a page from the root of the app to the index component. We also add a title that will show up in the page preview/browser tab.
248-
249-
```python
250-
app.add_page(index, title="DALL-E")
251-
```
252-
253-
You can create a multi-page app by adding more pages.
254-
255118
## 📑 Resources
256119

257120
<div align="center">

docs/advanced_onboarding/code_structure.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,10 @@ import reflex as rx
228228
class State(rx.State):
229229
v: str = "foo"
230230

231+
@rx.event
232+
def set_v(self, value: str):
233+
self.v = value
234+
231235

232236
@lru_cache
233237
def foo():

docs/app/assets/tailwind-theme.css

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
@import 'tailwindcss-scroll-mask';
12
@import "tailwindcss-animated";
23

34
@custom-variant dark (&:where(.dark, .dark *));

docs/app/reflex_docs/docgen_pipeline.py

Lines changed: 100 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
"""Pipeline for rendering reflex-shipped docs via reflex_docgen.markdown."""
22

3+
import json
34
import sys
45
import types
56
from pathlib import Path
@@ -769,20 +770,95 @@ def _parse_doc(filepath: str | Path) -> Document:
769770
return parse_document(source)
770771

771772

773+
FAQS_START_MARKER = "<!-- faqs-start -->"
774+
FAQS_END_MARKER = "<!-- faqs-end -->"
775+
776+
777+
def _extract_faqs_jsonld(source: str) -> tuple[str, rx.Component | None]:
778+
"""Strip an FAQ section from the source and return a JSON-LD script for it.
779+
780+
Looks for content between :data:`FAQS_START_MARKER` and :data:`FAQS_END_MARKER`.
781+
Inside that block, each H3 (``### Question``) heading is treated as a question
782+
and the following text blocks (until the next H3) make up the answer. The
783+
pairs are emitted as a single ``<script type="application/ld+json">`` element
784+
using the schema.org ``FAQPage`` shape.
785+
786+
Returns ``(stripped_source, jsonld_script_or_none)``. The script is ``None``
787+
if either marker is missing or no question/answer pairs were found.
788+
"""
789+
if FAQS_START_MARKER not in source or FAQS_END_MARKER not in source:
790+
return source, None
791+
before, _, rest = source.partition(FAQS_START_MARKER)
792+
faq_chunk, _, after = rest.partition(FAQS_END_MARKER)
793+
stripped = before + after
794+
795+
doc = parse_document(faq_chunk)
796+
faqs: list[tuple[str, str]] = []
797+
current_q: str | None = None
798+
current_a_parts: list[str] = []
799+
800+
def flush() -> None:
801+
if current_q is not None:
802+
answer = " ".join(p.strip() for p in current_a_parts if p.strip())
803+
if answer:
804+
faqs.append((current_q, answer))
805+
806+
for block in doc.blocks:
807+
if isinstance(block, HeadingBlock) and block.level == 3:
808+
flush()
809+
current_q = _spans_to_plaintext(block.children)
810+
current_a_parts = []
811+
elif current_q is not None and isinstance(block, TextBlock):
812+
current_a_parts.append(_spans_to_plaintext(block.children))
813+
flush()
814+
815+
if not faqs:
816+
return stripped, None
817+
818+
schema = {
819+
"@context": "https://schema.org",
820+
"@type": "FAQPage",
821+
"mainEntity": [
822+
{
823+
"@type": "Question",
824+
"name": q,
825+
"acceptedAnswer": {"@type": "Answer", "text": a},
826+
}
827+
for q, a in faqs
828+
],
829+
}
830+
jsonld = json.dumps(schema, ensure_ascii=False)
831+
return stripped, rx.el.script(jsonld, type="application/ld+json")
832+
833+
772834
def render_docgen_document(
773835
virtual_filepath: str | Path, actual_filepath: str | Path
774-
) -> rx.Component:
775-
"""Parse and render a doc file from the reflex package using reflex_docgen."""
776-
doc = _parse_doc(actual_filepath)
836+
) -> tuple[rx.Component, rx.Component | None]:
837+
"""Render a doc file as ``(body, faq_jsonld)``.
838+
839+
The FAQ section (between :data:`FAQS_START_MARKER` and
840+
:data:`FAQS_END_MARKER`, if present) is stripped from the visible body and
841+
returned as a JSON-LD ``<script>`` component for SEO. ``faq_jsonld`` is
842+
``None`` if no FAQ block is found.
843+
"""
844+
source = Path(actual_filepath).read_text(encoding="utf-8")
845+
source, faq_script = _extract_faqs_jsonld(source)
777846
transformer = ReflexDocTransformer(
778847
virtual_filepath=str(virtual_filepath), filename=str(actual_filepath)
779848
)
780-
return transformer.transform(doc)
849+
return transformer.transform(parse_document(source)), faq_script
781850

782851

783852
def get_docgen_toc(filepath: str | Path) -> list[tuple[int, str]]:
784-
"""Extract TOC headings as (level, text) tuples — same format as reflex_docgen's get_toc."""
785-
doc = _parse_doc(filepath)
853+
"""Extract TOC headings as (level, text) tuples.
854+
855+
The FAQ block (between :data:`FAQS_START_MARKER` and
856+
:data:`FAQS_END_MARKER`) is stripped before extraction so its headings do
857+
not appear in the TOC — they live only in the emitted JSON-LD.
858+
"""
859+
source = Path(filepath).read_text(encoding="utf-8")
860+
source, _ = _extract_faqs_jsonld(source)
861+
doc = parse_document(source)
786862
return [(h.level, _spans_to_plaintext(h.children)) for h in doc.headings]
787863

788864

@@ -791,3 +867,21 @@ def render_markdown(text: str) -> rx.Component:
791867
doc = parse_document(text)
792868
transformer = ReflexDocTransformer()
793869
return transformer.transform(doc)
870+
871+
872+
def render_inline_markdown(text: str, class_name: str = "") -> rx.Component:
873+
"""Render a short markdown string inline (links, code spans, emphasis).
874+
875+
Intended for places like prop-table descriptions where the source contains
876+
inline markdown (e.g. ``[Foo](/path)``, `` `code` ``) but the surrounding
877+
layout expects a single styled text node, not a stack of block-level
878+
elements. Block-level markdown (headings, lists, code fences) falls back
879+
to the full transformer.
880+
"""
881+
if not text:
882+
return rx.fragment()
883+
doc = parse_document(text)
884+
if len(doc.blocks) == 1 and isinstance(doc.blocks[0], TextBlock):
885+
children = _render_spans(doc.blocks[0].children)
886+
return rx.text(*children, class_name=class_name)
887+
return ReflexDocTransformer().transform(doc)

docs/app/reflex_docs/pages/docs/__init__.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,10 @@ def handle_library_doc(
212212
"""Handle docs/library/** docs — component API reference via multi_docs."""
213213
clist = [title, *get_components_from_frontmatter(actual_path)]
214214
previews = get_previews_from_frontmatter(actual_path)
215+
ll_actual_path = actual_path.replace(".md", "-ll.md")
216+
ll_clist: list | None = None
217+
if os.path.exists(ll_actual_path):
218+
ll_clist = [title, *get_components_from_frontmatter(ll_actual_path)]
215219
if doc.startswith("docs/library/graphing"):
216220
graphing_components[resolved.category].append(clist)
217221
else:
@@ -223,6 +227,7 @@ def handle_library_doc(
223227
previews=previews,
224228
component_list=clist,
225229
title=resolved.display_title,
230+
ll_component_list=ll_clist,
226231
)
227232

228233

@@ -238,10 +243,12 @@ def get_component_docgen(virtual_doc: str, actual_path: str, title: str):
238243
def comp(_actual=actual_path, _virtual=virtual_doc):
239244
toc = get_docgen_toc(_actual)
240245
doc_content = Path(_actual).read_text(encoding="utf-8")
241-
rendered = render_docgen_document(
246+
body, faq_script = render_docgen_document(
242247
virtual_filepath=_virtual, actual_filepath=_actual
243248
)
244-
return ((toc, doc_content), rendered)
249+
if faq_script is not None:
250+
body = rx.fragment(body, faq_script)
251+
return ((toc, doc_content), body)
245252

246253
return make_docpage(resolved.route, resolved.display_title, virtual_doc, comp)
247254

0 commit comments

Comments
 (0)