99import reflex as rx
1010import reflex_ui as ui
1111from reflex .components .radix .themes .base import LiteralAccentColor
12+ from reflex .experimental .client_state import ClientStateVar
1213from reflex .utils .format import to_snake_case , to_title_case
1314
14- from pcweb .components .button import button
1515from pcweb .components .icons .icons import get_icon
16+ from pcweb .components .marketing_button import button as marketing_button
1617from pcweb .route import Route , get_path
1718from pcweb .styles .colors import c_color
1819
@@ -156,8 +157,8 @@ def footer_link_flex(heading: str, links):
156157
157158def thumb_card (score : int , icon : str ) -> rx .Component :
158159 return rx .el .button (
159- rx .icon (
160- tag = icon ,
160+ ui .icon (
161+ icon ,
161162 color = rx .cond (
162163 FeedbackState .score == score , c_color ("slate" , 11 ), c_color ("slate" , 9 )
163164 ),
@@ -173,53 +174,39 @@ def thumb_card(score: int, icon: str) -> rx.Component:
173174
174175def thumbs_cards () -> rx .Component :
175176 return rx .hstack (
176- thumb_card (1 , "thumbs-up " ),
177- thumb_card (0 , "thumbs-down " ),
177+ thumb_card (1 , "ThumbsUpIcon " ),
178+ thumb_card (0 , "ThumbsDownIcon " ),
178179 gap = "8px" ,
179180 )
180181
181182
182183def feedback_content () -> rx .Component :
183- return rx .box (
184- rx .box (
185- rx .text (
186- "Send feedback" ,
187- class_name = "font-md text-slate-11" ,
188- ),
184+ return rx .el .div (
185+ rx .el .div (
189186 rx .form (
190- rx .box (
191- rx . el .textarea (
187+ rx .el . div (
188+ ui .textarea (
192189 name = "feedback" ,
193190 placeholder = "Write a comment…" ,
194191 type = "text" ,
195192 max_length = 500 ,
196193 enter_key_submit = True ,
197194 resize = "vertical" ,
198195 required = True ,
199- class_name = "w-full h-full p-2 text-slate-11 font-small bg-white-1 border border-slate-4 rounded-[10px] max-h-[300px] min-h-[72px] outline-none overflow-y-auto placeholder-slate-9 focus:border-violet-9 focus:border-1" ,
200196 ),
201197 thumbs_cards (),
202- rx . el .input (
198+ ui .input (
203199 name = "email" ,
204200 type = "email" ,
205201 placeholder = "Contact email (optional)" ,
206202 max_length = 100 ,
207- class_name = "w-full h-full p-2 text-slate-11 font-small bg-white-1 border border-slate-4 rounded-[10px] box-border outline-none placeholder-slate-9 focus:border-violet-9 focus:border-1" ,
208203 ),
209- rx .box (
210- rx .popover .close (
211- button (
212- "Send" ,
213- type = "submit" ,
214- )
215- ),
216- rx .popover .close (
217- button (
218- "Cancel" ,
219- variant = "secondary" ,
220- )
221- ),
222- class_name = "flex flex-row gap-4 justify-between items-center" ,
204+ ui .popover .close (
205+ ui .button (
206+ "Send feedback" ,
207+ type = "submit" ,
208+ class_name = "w-full" ,
209+ )
223210 ),
224211 class_name = "w-full gap-4 flex flex-col" ,
225212 ),
@@ -229,48 +216,102 @@ def feedback_content() -> rx.Component:
229216 ),
230217 class_name = "flex flex-col gap-4 w-full" ,
231218 ),
232- class_name = "rounded-[26px] bg-white-1 w-[341px] max-h-[564px] shadow-large h-auto p-4 " ,
219+ class_name = "p-2 " ,
233220 )
234221
235222
236223def feedback_button () -> rx .Component :
237224 thumb_cn = " flex flex-row items-center justify-center gap-2 text-slate-9 whitespace-nowrap border border-slate-5 bg-slate-1 shadow-large cursor-pointer transition-bg hover:bg-slate-3 font-small"
238- return rx .popover .root (
239- rx .box (
240- rx .popover .trigger (
241- rx .box (
242- rx .icon (tag = "thumbs-up" , size = 15 , class_name = "!text-slate-9" ),
243- rx .text (
244- "Yes" ,
225+ return ui .popover .root (
226+ ui .popover .trigger (
227+ render_ = rx .el .div (
228+ rx .el .button (
229+ ui .icon ("ThumbsUpIcon" ),
230+ "Yes" ,
231+ type = "button" ,
232+ class_name = ui .cn (
233+ "w-full gap-2 border-r-0 px-3 py-0.5 rounded-[20px_0_0_20px]" ,
234+ thumb_cn ,
245235 ),
246- class_name = "w-full gap-2 border-r-0 px-3 py-0.5 rounded-[20px_0_0_20px]"
247- + thumb_cn ,
236+ aria_label = "Yes" ,
237+ on_click = FeedbackState . set_score ( 1 ) ,
248238 ),
249- custom_attrs = {"role" : "button" },
250- aria_label = "Yes" ,
251- on_click = FeedbackState .set_score (1 ),
252- ),
253- rx .popover .trigger (
254- rx .box (
255- rx .icon (tag = "thumbs-down" , size = 15 , class_name = "!text-slate-9" ),
256- rx .text (
257- "No" ,
239+ rx .el .button (
240+ ui .icon ("ThumbsDownIcon" ),
241+ "No" ,
242+ type = "button" ,
243+ class_name = ui .cn (
244+ "w-full gap-2 border-r-0 px-3 py-0.5 rounded-[0_20px_20px_0]" ,
245+ thumb_cn ,
258246 ),
259- class_name = "w-full gap-2 px-3 py-0.5 rounded-[0_20px_20px_0]"
260- + thumb_cn ,
247+ aria_label = "No" ,
248+ on_click = FeedbackState . set_score ( 0 ) ,
261249 ),
262- custom_attrs = {"role" : "button" },
263- aria_label = "No" ,
264- on_click = FeedbackState .set_score (0 ),
250+ class_name = "w-full lg:w-auto items-center flex flex-row" ,
265251 ),
266- class_name = "w-full lg:w-auto items-center flex flex-row" ,
267252 ),
268- rx .popover .content (
269- feedback_content (),
270- align = "start" ,
271- class_name = "border-none left-0 lg:left-[-255px] origin-bottom lg:origin-bottom-right !p-0 overflow-visible !bg-transparent shadow-none" ,
272- avoid_collisions = True ,
253+ ui .popover .portal (
254+ ui .popover .positioner (
255+ ui .popover .popup (
256+ render_ = feedback_content (),
257+ ),
258+ ),
259+ ),
260+ )
261+
262+
263+ def feedback_button_toc () -> rx .Component :
264+ return ui .popover (
265+ trigger = marketing_button (
266+ ui .icon ("ThumbsUpIcon" ),
267+ "Send feedback" ,
268+ variant = "ghost" ,
269+ size = "sm" ,
270+ type = "button" ,
271+ on_click = FeedbackState .set_score (1 ),
272+ class_name = "justify-start text-m-slate-7 dark:text-m-slate-6" ,
273273 ),
274+ content = feedback_content (),
275+ )
276+
277+
278+ @rx .memo
279+ def copy_to_markdown (text : str ) -> rx .Component :
280+ copied = ClientStateVar .create ("is_copied" , default = False , global_ref = False )
281+ return marketing_button (
282+ rx .cond (
283+ copied .value ,
284+ ui .icon (
285+ "CheckmarkCircle02Icon" ,
286+ ),
287+ get_icon ("markdown" , class_name = "[&_svg]:h-4 [&_svg]:w-auto" ),
288+ ),
289+ "Copy to markdown" ,
290+ type = "button" ,
291+ size = "sm" ,
292+ variant = "ghost" ,
293+ class_name = "justify-start text-m-slate-7 dark:text-m-slate-6" ,
294+ on_click = [
295+ rx .call_function (copied .set_value (True )),
296+ rx .set_clipboard (text ),
297+ ],
298+ on_mouse_down = rx .call_function (copied .set_value (False )).debounce (1500 ),
299+ )
300+
301+
302+ def ask_ai_chat () -> rx .Component :
303+ from pcweb .pages .docs import ai_builder as ai_builder_pages
304+
305+ return rx .el .a (
306+ marketing_button (
307+ ui .icon ("AiChat02Icon" ),
308+ "Ask AI about this page" ,
309+ size = "sm" ,
310+ variant = "ghost" ,
311+ class_name = "justify-start text-m-slate-7 dark:text-m-slate-6" ,
312+ native_button = False ,
313+ ),
314+ to = ai_builder_pages .integrations .mcp_overview .path ,
274315 )
275316
276317
@@ -474,11 +515,11 @@ def get_toc(source, href, component_list=None):
474515 env ["__xd" ] = xd
475516
476517 # Get the content of the document.
477- source = source .content
518+ doc_content = source .content
478519
479520 # Get the blocks in the source code.
480521 # Note: we must use reflex-web's special flexdown instance xd here - it knows about all custom block types (like DemoBlock)
481- blocks = xd .get_blocks (source , href )
522+ blocks = xd .get_blocks (doc_content , href )
482523
483524 content_pieces = []
484525 for block in blocks :
@@ -502,7 +543,7 @@ def get_toc(source, href, component_list=None):
502543 headings .append ((1 , "API Reference" ))
503544 for component_tuple in component_list :
504545 headings .append ((2 , component_tuple [1 ]))
505- return headings
546+ return headings , doc_content
506547
507548
508549def docpage (
@@ -617,13 +658,21 @@ def wrapper(*args, **kwargs) -> rx.Component:
617658 links .append (rx .fragment ())
618659
619660 toc = []
661+ doc_content = None
620662 if not isinstance (contents , rx .Component ):
621663 comp = contents (* args , ** kwargs )
622664 else :
623665 comp = contents
624666
625- if isinstance (comp , tuple ):
626- toc , comp = comp
667+ if isinstance (comp , tuple ) and len (comp ) == 2 :
668+ first , second = comp
669+ # Check if first is (toc, doc_content) from get_toc
670+ if isinstance (first , tuple ) and len (first ) == 2 :
671+ toc , doc_content = first
672+ comp = second
673+ else :
674+ # Legacy format: (toc, comp)
675+ toc , comp = first , second
627676
628677 show_right_sidebar = right_sidebar and len (toc ) >= 2
629678 return rx .box (
@@ -720,9 +769,18 @@ def wrapper(*args, **kwargs) -> rx.Component:
720769 )
721770 for level , text in toc
722771 ],
723- class_name = "flex flex-col gap-y-1 list-none shadow-[1.5px_0_0_0_var(--m-slate-4)_inset] dark:shadow-[1.5px_0_0_0_var(--m-slate-9)_inset]" ,
772+ id = "toc-navigation" ,
773+ class_name = "flex flex-col gap-y-1 list-none shadow-[1.5px_0_0_0_var(--m-slate-4)_inset] dark:shadow-[1.5px_0_0_0_var(--m-slate-9)_inset] max-h-[80vh]" ,
774+ ),
775+ rx .el .div (
776+ feedback_button_toc (),
777+ copy_to_markdown (text = doc_content )
778+ if doc_content
779+ else None ,
780+ ask_ai_chat (),
781+ class_name = "flex flex-col mt-1.5 justify-start" ,
724782 ),
725- class_name = "flex flex-col justify-start gap-y-4 max-h-[80vh] overflow-y-auto sticky top-4" ,
783+ class_name = "flex flex-col justify-start gap-y-4 overflow-y-auto sticky top-4" ,
726784 ),
727785 class_name = (
728786 "w-full h-full"
@@ -732,7 +790,6 @@ def wrapper(*args, **kwargs) -> rx.Component:
732790 " mt-[90px]" ,
733791 )
734792 ),
735- id = "toc-navigation" ,
736793 ),
737794 class_name = (
738795 "w-[240px] h-screen sticky top-0 shrink-0 hidden xl:block"
0 commit comments