futureselves / form_helpers.py
Papajams's picture
chore: sync all files to Space
1858350 verified
Raw
History Blame Contribute Delete
12 kB
"""
form_helpers.py — example phrases and warmup text for the onboarding form.
This module exists to kill the blank-page feeling. Every Textbox in the
onboarding form gets:
1. A rich, in-voice placeholder
2. A row of 3-4 example chips below it (click to populate)
3. A short primer line above the form setting emotional tone
The example phrases are deliberately specific (not generic), span
multiple life arcs (not just "founder"), and are written in the
FutureSelves voice. They are starting points the user can edit, not
prescriptions.
The choice and reaction radios also get subtitles so the user can
tell what each option means without hovering or guessing.
"""
from __future__ import annotations
from typing import Optional
# ─── In-voice placeholders ──────────────────────────────────────────────────
# These appear inside the empty textbox as italic guidance. They are
# the FIRST thing the user reads, so they have to set the right tone:
# intimate, specific, evocative — not "Enter your avoiding item here."
PLACEHOLDERS = {
"name": "What should I call you?",
"city": "Where are you right now? (or skip — it's optional)",
"chapter": "A sentence or two. The chapter you're in, not the chapter you wish you were in.",
"avoiding": "The thing you keep circling. Not the thing you're already doing — the thing you're not doing.",
"afraid": "The thing that wakes you at 3am. The future you don't want to find out about.",
"draining": "The energy leak. The thing that costs you and gives you nothing back.",
"miraculous": "The version of next year that would feel like a miracle. Be specific. 'Everything works out' is not a miracle — it's a wish.",
"word": "One word. The one that fits. Not the polite one.",
"note": "What's alive in you right now? A sentence is enough. Three is plenty.",
"reply": "What to carry forward. A sentence your future self will recognize.",
}
# ─── Example chips ──────────────────────────────────────────────────────────
# Click any of these to populate the field. The user can edit afterward.
# The chips are deliberately a mix of life arcs (work, love, health,
# creativity, parenting) so the user sees themselves reflected.
EXAMPLES = {
"avoiding": [
"the email I keep composing and deleting",
"asking for help with the part I'm scared of",
"the conversation I keep rescheduling",
"the call I owe someone I love",
"the doctor's appointment I cancelled twice",
],
"afraid": [
"the launch I've been promising",
"the team I keep telling myself I'll find",
"the version of me that ships the thing",
"the people I'm losing to my own silence",
"the year I keep pushing to next year",
],
"draining": [
"the news after 9pm",
"the Sunday plan that becomes dread",
"the meeting that should have been an email",
"pretending I'm fine",
"the open tabs I never close",
],
"miraculous": [
"a co-founder who fits",
"shipping v2 before Q3",
"a quiet life with the people I love",
"my body feeling like mine again",
"the book I've been saying I'll write",
"being the parent I want to be",
],
"note": [
"I keep reaching for the same thing and pulling back",
"something happened today and I don't know what to do with it",
"I am tired in a way sleep doesn't fix",
"I want to remember this exact feeling",
"I almost said the true thing. Almost.",
],
"reply": [
"This one is for tonight. I'll act on it.",
"Save this. I want to read it again tomorrow.",
"Not quite there. Make it sharper next time.",
"I did the thing. Tell me what's next.",
],
}
# ─── Choice subtitles ───────────────────────────────────────────────────────
# Each choice in the radio gets a longer description so the user knows
# what they're picking. The first part is the short label; the second
# part is the meaning.
CHOICE_OPTIONS = [
("🚀 Toward", "toward", "moving toward something brave"),
("🌱 Steady", "steady", "holding ground where I am"),
("🕊️ Release", "release", "letting something go"),
("🪡 Repair", "repair", "mending a frayed thread"),
]
REACTION_OPTIONS = [
("✓ Did it", "did_it", "I did the thing"),
("✋ Keep close", "keep_close", "holding this for later"),
("✦ Landed", "landed", "it landed but I haven't acted"),
("↺ Not quite", "not_quite", "didn't quite reach me"),
]
MEMORY_OPTIONS = [
("✓ I'll do it", "do_it", "moving toward it tonight"),
("✋ Keep close", "keep", "holding this for later"),
("✦ Landed", "landed", "it landed, haven't acted yet"),
("↺ Not quite", "not_quite", "didn't quite reach me"),
]
MEMORY_TO_CHOICE_REACTION = {
"do_it": ("toward", "did_it"),
"keep": ("steady", "keep_close"),
"landed": ("repair", "landed"),
"not_quite": ("release", "not_quite"),
}
ARC_OPTIONS = [
("💰 Money", "money", "building wealth, stability, freedom"),
("💜 Love", "love", "relationships, family, connection"),
("🧭 Purpose", "purpose", "mission, craft, impact"),
("🌱 Health", "health", "body, mind, energy, rest"),
]
# Map arc value to emoji for dynamic headings
ARC_EMOJI = {
"money": "💰",
"love": "💜",
"purpose": "🧭",
"health": "🌱",
}
# Voice emoji mapping for step 1 heading
VOICE_EMOJI = {
"Ember": "🔥",
"Nova": "✨",
"Sol": "☀️",
"River": "🌊",
"Stone": "🪨",
"Wind": "💨",
}
# ─── Primer / warmup copy ──────────────────────────────────────────────────
# Appears above the onboarding form. Sets the emotional tone before
# the user is asked to write. The product is a private conversation,
# not a database form.
PRIMER_HTML = (
'<div class="onboard-primer">'
'<div class="primer-eyebrow">— make tomorrow more specific —</div>'
'<div class="primer-line">You can receive a first transmission with one word. '
'Answer these when you want the line to know you better.</div>'
'</div>'
)
# ─── Atmospheric button text ────────────────────────────────────────────────
# Replace cold default button text with copy that matches the chamber.
BUTTON_TEXT = {
"step1_next": "Start here →",
"step2_next": "Go deeper →",
"step3_begin": "✦ Open the line",
"checkin_submit": "Receive my transmission →",
"generate": "Open the line",
"choice_submit": "Record the move",
"reaction_submit": "Send it back",
"memory_submit": "Remember this →",
}
def chip_html(examples: list[str], field_id: str) -> str:
"""Render example chips as HTML for embedding in a Gradio layout.
Each chip is a small button that, when clicked, populates the
matching textbox via a global JS handler. We use HTML rather
than a row of gr.Button components because (a) it's lighter
visually and (b) the chips are presentation, not actions that
need their own event chain.
The JS handler dispatches a CustomEvent on the document that
the Gradio form listens for. See app.py for the listener.
Format: a flex row of small italic mono chips with paper-mute
color, amber on hover, wrapping.
"""
chips = "".join(
f'<button type="button" class="ex-chip" data-field="{field_id}" data-value="{ex}">{ex}</button>'
for ex in examples
)
return f'<div class="ex-chip-row" data-field="{field_id}">{chips}</div>'
def _word_chips_html() -> str:
"""Render example word chips (one-word prompts).
These are shown above the check-in note field so the user has
a concrete starting point. Each chip populates the word textbox.
"""
words = ["restless", "hopeful", "tired", "open", "scattered", "still"]
chips = "".join(
f'<button type="button" class="ex-chip" data-field="field-word" data-value="{w}">{w}</button>'
for w in words
)
return f'<div class="ex-chip-row" data-field="field-word">{chips}</div>'
def _choice_cards_html(selected: str = "toward") -> str:
"""Render 4 choice cards for the 'Your move' moment.
Each card is clickable, shows emoji + label + meaning, and
triggers a JavaScript handler that sets the hidden radio value.
The selected card gets an .active class.
"""
cards_data = CHOICE_OPTIONS
items = []
for emoji_label, value, subtitle in cards_data:
cls = "choice-card active" if value == selected else "choice-card"
items.append(
f'<button type="button" class="{cls}" data-value="{value}">'
f'<span class="card-emoji">{emoji_label.split()[0]}</span>'
f'<span class="card-label">{" ".join(emoji_label.split()[1:])}</span>'
f'<span class="card-sub">{subtitle}</span>'
f'</button>'
)
return f'<div class="card-grid choice-grid">{"".join(items)}</div>'
def _reaction_cards_html(selected: str = "landed") -> str:
"""Render 4 reaction cards for the 'How did it land?' moment.
Same visual as choice cards but with different copy and values.
"""
cards_data = REACTION_OPTIONS
items = []
for emoji_label, value, subtitle in cards_data:
cls = "reaction-card active" if value == selected else "reaction-card"
items.append(
f'<button type="button" class="{cls}" data-value="{value}">'
f'<span class="card-emoji">{emoji_label.split()[0]}</span>'
f'<span class="card-label">{" ".join(emoji_label.split()[1:])}</span>'
f'<span class="card-sub">{subtitle}</span>'
f'</button>'
)
return f'<div class="card-grid reaction-grid">{"".join(items)}</div>'
def _memory_cards_html(selected: str = "do_it") -> str:
"""Render 4 memory-action cards that collapse choice + reaction into one tap."""
cards_data = MEMORY_OPTIONS
items = []
for emoji_label, value, subtitle in cards_data:
cls = "memory-card active" if value == selected else "memory-card"
items.append(
f'<button type="button" class="{cls}" data-value="{value}">'
f'<span class="card-emoji">{emoji_label.split()[0]}</span>'
f'<span class="card-label">{" ".join(emoji_label.split()[1:])}</span>'
f'<span class="card-sub">{subtitle}</span>'
f'</button>'
)
return f'<div class="card-grid memory-grid">{"".join(items)}</div>'
def _arc_cards_html(selected: str = "purpose") -> str:
"""Render 4 arc selection cards for Step 2 of onboarding.
Each card is clickable, shows emoji + arc name + description, and
triggers a JavaScript handler that sets the hidden radio value.
The selected card gets an .active class.
"""
cards_data = ARC_OPTIONS
items = []
for emoji_label, value, subtitle in cards_data:
cls = "arc-card active" if value == selected else "arc-card"
items.append(
f'<button type="button" class="{cls}" data-value="{value}">'
f'<span class="card-emoji">{emoji_label.split()[0]}</span>'
f'<span class="card-label">{" ".join(emoji_label.split()[1:])}</span>'
f'<span class="card-sub">{subtitle}</span>'
f'</button>'
)
return f'<div class="card-grid arc-grid">{"".join(items)}</div>'