diff options
| author | Ben Sima <ben@bensima.com> | 2025-12-15 08:47:02 -0500 |
|---|---|---|
| committer | Ben Sima <ben@bensima.com> | 2025-12-15 08:47:02 -0500 |
| commit | 0baab1972e30c0e4629e67152838e660b02a2537 (patch) | |
| tree | d82d9402e4a0840777ee3d4e39ab4329f246918b /Biz/PodcastItLater/UI.py | |
| parent | adf693eb82cddd2c383cdebd3392716446ddf054 (diff) | |
t-265.6: Add feedback collection endpoint for PIL
- Add feedback table with migration in Core.py
- Add FeedbackForm and FeedbackPage UI components
- Add /feedback GET/POST routes and /api/feedback JSON endpoint
- Add admin feedback view at /admin/feedback
- Create Omni/Agent/Tools/Feedback.hs with feedback_list tool
- Wire feedback tool into Telegram agent
Diffstat (limited to 'Biz/PodcastItLater/UI.py')
| -rw-r--r-- | Biz/PodcastItLater/UI.py | 247 |
1 files changed, 247 insertions, 0 deletions
diff --git a/Biz/PodcastItLater/UI.py b/Biz/PodcastItLater/UI.py index e9ef27d..b243ae7 100644 --- a/Biz/PodcastItLater/UI.py +++ b/Biz/PodcastItLater/UI.py @@ -207,6 +207,14 @@ class PageLayout(Component[AnyChildren, PageLayoutAttrs]): classes=["dropdown-item"], ), ), + html.li( + html.a( + html.i(classes=["bi", "bi-chat-heart-fill", "me-2"]), + "Feedback", + href="/admin/feedback", + classes=["dropdown-item"], + ), + ), classes=["dropdown-menu"], aria_labelledby="adminDropdown", ), @@ -621,6 +629,245 @@ class PricingPageAttrs(Attrs): user: dict[str, typing.Any] | None +class FeedbackFormAttrs(Attrs): + """Attributes for FeedbackForm component.""" + + campaign_id: str | None + success: bool + + +class FeedbackForm(Component[AnyChildren, FeedbackFormAttrs]): + """Feedback collection form component.""" + + @override + def render(self) -> html.div: + campaign_id = self.attrs.get("campaign_id") + success = self.attrs.get("success", False) + + if success: + return html.div( + html.div( + html.div( + html.h3( + html.i( + classes=["bi", "bi-check-circle-fill", "me-2"] + ), + "Thank You!", + classes=["text-success", "mb-3"], + ), + html.p( + "Your feedback has been submitted. " + "We really appreciate you taking the time to help us " + "improve PodcastItLater.", + classes=["lead"], + ), + html.a( + html.i(classes=["bi", "bi-arrow-left", "me-2"]), + "Back to home", + href="/", + classes=["btn", "btn-primary", "mt-3"], + ), + classes=["card-body", "text-center", "py-5"], + ), + classes=["card", "shadow-sm"], + ), + ) + + source_options = [ + ("", "Select one..."), + ("outreach", "Email from Ben/Ava"), + ("search", "Search engine"), + ("social", "Social media"), + ("friend", "Friend/colleague"), + ("other", "Other"), + ] + + return html.div( + html.div( + html.div( + html.h3( + html.i(classes=["bi", "bi-chat-heart-fill", "me-2"]), + "Share Your Feedback", + classes=["card-title", "mb-4"], + ), + html.p( + "Help us make PodcastItLater better! " + "All fields are optional.", + classes=["text-muted", "mb-4"], + ), + html.form( + html.input( + type="hidden", + name="campaign_id", + value=campaign_id or "", + ) + if campaign_id + else html.div(), + # Email + html.div( + html.label( + "Email (optional)", + for_="email", + classes=["form-label"], + ), + html.input( + type="email", + name="email", + id="email", + placeholder="you@example.com", + classes=["form-control"], + ), + html.div( + "If you'd like us to follow up with you", + classes=["form-text"], + ), + classes=["mb-3"], + ), + # Source dropdown + html.div( + html.label( + "How did you hear about us?", + for_="source", + classes=["form-label"], + ), + html.select( + *[ + html.option(label, value=value) + for value, label in source_options + ], + name="source", + id="source", + classes=["form-select"], + ), + classes=["mb-3"], + ), + # Use case + html.div( + html.label( + "What would you use PodcastItLater for?", + for_="use_case", + classes=["form-label"], + ), + html.textarea( + name="use_case", + id="use_case", + rows="3", # type: ignore[call-arg] + placeholder=( + "e.g., catching up on articles during commute, " + "listening to research papers while exercising..." + ), + classes=["form-control"], + ), + classes=["mb-3"], + ), + # General feedback + html.div( + html.label( + "Any other feedback?", + for_="feedback_text", + classes=["form-label"], + ), + html.textarea( + name="feedback_text", + id="feedback_text", + rows="3", # type: ignore[call-arg] + placeholder="Suggestions, issues, feature requests...", + classes=["form-control"], + ), + classes=["mb-3"], + ), + # Rating + html.div( + html.label( + "How likely are you to recommend PIL? (1-5)", + for_="rating", + classes=["form-label"], + ), + html.div( + *[ + html.div( + html.input( + type="radio", + name="rating", + id=f"rating{i}", + value=str(i), + classes=["btn-check"], + ), + html.label( + str(i), + for_=f"rating{i}", + classes=[ + "btn", + "btn-outline-primary", + ], + ), + classes=["me-2"], + ) + for i in range(1, 6) + ], + classes=["d-flex"], + ), + html.div( + "1 = Not likely, 5 = Very likely", + classes=["form-text"], + ), + classes=["mb-4"], + ), + # Submit + html.button( + html.i(classes=["bi", "bi-send-fill", "me-2"]), + "Submit Feedback", + type="submit", + classes=["btn", "btn-primary", "btn-lg"], + ), + action="/feedback", + method="post", + ), + classes=["card-body", "p-4"], + ), + classes=["card", "shadow-sm"], + ), + ) + + +class FeedbackPageAttrs(Attrs): + """Attributes for FeedbackPage component.""" + + user: dict[str, typing.Any] | None + campaign_id: str | None + success: bool + + +class FeedbackPage(Component[AnyChildren, FeedbackPageAttrs]): + """Feedback page with layout.""" + + @override + def render(self) -> PageLayout: + user = self.attrs.get("user") + campaign_id = self.attrs.get("campaign_id") + success = self.attrs.get("success", False) + + return PageLayout( + html.div( + html.div( + html.div( + FeedbackForm( + campaign_id=campaign_id, + success=success, + ), + classes=["col-lg-8", "mx-auto"], + ), + classes=["row"], + ), + ), + user=user, + current_page="feedback", + page_title="Feedback - PodcastItLater", + error=None, + meta_tags=[], + ) + + class PricingPage(Component[AnyChildren, PricingPageAttrs]): """Pricing page component.""" |
