Spaces:
Sleeping
Sleeping
| import streamlit as st | |
| import json | |
| import os | |
| import numpy as np | |
| import plotly.graph_objs as go | |
| from groq import Groq | |
| from dotenv import load_dotenv | |
| load_dotenv() # load .env file | |
| GROQ_API_KEY = os.environ.get("GROQ_API_KEY") | |
| # --- CONFIG --- | |
| GROQ_MODEL = "llama3-70b-8192" | |
| groq_client = Groq(api_key=GROQ_API_KEY) | |
| PERSONA_PATH = os.getenv("PERSONA_PATH", "/tmp/personas.json") | |
| # --- THEME COLORS --- | |
| neon_blue = "#00fff7" | |
| neon_green = "#7CFC00" | |
| neon_pink = "#F72585" | |
| neon_cyan = "#0ffcff" | |
| neon_bg = "#181830" | |
| neon_orange = "#FFB347" | |
| neon_shadow = "#2dfdff44" | |
| font_main = "Inter, Segoe UI, Arial, sans-serif" | |
| st.set_page_config(page_title="🚀 New Launch Studio", layout="wide", initial_sidebar_state="collapsed") | |
| # Set dark theme programmatically | |
| st.markdown( | |
| """ | |
| <style> | |
| body, .main, .stApp { | |
| background: #14151A !important; | |
| color: #fff !important; | |
| } | |
| </style> | |
| """, | |
| unsafe_allow_html=True | |
| ) | |
| # --- STYLE --- | |
| st.markdown(f""" | |
| <style> | |
| html, body, [class*="css"] {{ | |
| background-color: {neon_bg} !important; | |
| font-family: {font_main} !important; | |
| }} | |
| /* HEADERS */ | |
| .neon-title {{ | |
| font-size:2.8rem; font-weight:900; color:{neon_blue}; | |
| letter-spacing:0.02em; margin-bottom:7px; margin-top:6px; | |
| text-shadow:0 2px 24px {neon_blue}33; | |
| }} | |
| .neon-sub {{ | |
| font-size:1.25rem;font-weight:600;color:#fff; | |
| margin-bottom:2px;margin-top:0px; | |
| }} | |
| .neon-heads-up {{ | |
| font-size:1.08rem;color:{neon_pink};font-weight:700;margin-bottom:32px; | |
| margin-top:7px; | |
| }} | |
| /* BUTTONS */ | |
| .neon-btn {{ | |
| display:inline-block; | |
| font-weight:bold; | |
| padding:13px 32px; | |
| border:none; | |
| border-radius:13px; | |
| font-size:1.10em; | |
| margin-right:16px; | |
| cursor:pointer; | |
| box-shadow:0 0 13px {neon_blue}55; | |
| color:#1d1d1d !important; | |
| background:linear-gradient(90deg,{neon_green}, {neon_blue}); | |
| text-decoration:none !important; | |
| transition:transform 0.10s; | |
| }} | |
| .neon-btn-pink {{ | |
| background:linear-gradient(90deg,{neon_pink}, {neon_blue}); | |
| color:#fff !important; | |
| box-shadow:0 0 16px {neon_pink}88; | |
| }} | |
| .neon-btn:hover {{ transform:scale(1.06); }} | |
| /* PERSONA NAME BOX */ | |
| .persona-name-box {{ | |
| background: linear-gradient(90deg, {neon_blue}, {neon_pink} 80%); | |
| color: #15192A; | |
| font-size:2.2rem; | |
| font-weight:900; | |
| border-radius:28px; | |
| padding: 12px 40px 10px 25px; | |
| margin-bottom:15px; | |
| display: inline-block; | |
| box-shadow: 0 2px 26px {neon_cyan}99; | |
| letter-spacing:0.01em; | |
| margin-top:18px; | |
| }} | |
| /* PERSONA CARD CONTENTS */ | |
| .persona-section-row {{ | |
| display: flex; | |
| gap: 2.5em; | |
| margin-bottom: 0; | |
| }} | |
| .persona-section-col {{ | |
| flex: 1; | |
| min-width: 340px; | |
| }} | |
| /* LABELS */ | |
| .block-label {{ | |
| font-weight:900; | |
| font-size:1.15em; | |
| margin-bottom:8px; | |
| margin-top:8px; | |
| letter-spacing:0.01em; | |
| display:flex; | |
| align-items:center; | |
| gap:0.6em; | |
| }} | |
| .label-blue {{ color:{neon_blue}; }} | |
| .label-green {{ color:{neon_green}; }} | |
| .label-pink {{ color:{neon_pink}; }} | |
| .label-orange {{ color:{neon_orange}; }} | |
| .label-cyan {{ color:{neon_cyan}; }} | |
| /* BULLET LISTS */ | |
| ul.insight-list {{ | |
| margin-top:7px; margin-bottom:16px; | |
| padding-left:22px; | |
| }} | |
| ul.insight-list li {{ | |
| font-size:1.11em; font-weight:500; color:#fff; | |
| margin-bottom:5px; line-height:1.53; | |
| }} | |
| /* INTEREST & NOTIF */ | |
| .interest-badge {{ | |
| display:inline-block; | |
| background:linear-gradient(90deg, {neon_green}, {neon_blue} 90%); | |
| color:#15192A; font-size:1.09em; font-weight:900; | |
| border-radius:15px; padding:8px 30px 7px 18px; | |
| margin-right:14px; | |
| box-shadow:0 0 17px {neon_green}2c; | |
| margin-top:10px; | |
| }} | |
| .notification-block {{ | |
| background:linear-gradient(90deg,{neon_cyan}44,#232344 96%); | |
| border-left:5px solid {neon_blue}; | |
| padding:17px 23px 17px 23px; | |
| border-radius:14px; | |
| font-weight:700; | |
| color:{neon_blue}; | |
| font-size:1.06em; | |
| line-height:1.45; | |
| box-shadow:0 2px 18px {neon_cyan}1a; | |
| margin-bottom:8px; | |
| margin-top:10px; | |
| letter-spacing:0.01em; | |
| max-width:430px; | |
| min-width: 240px; | |
| display: inline-block; | |
| }} | |
| /* CHART/INSIGHT CARDS */ | |
| .section-card {{ | |
| background:rgba(23,28,49,0.97); | |
| border-radius: 17px; | |
| box-shadow:0 0 22px {neon_cyan}32; | |
| padding: 34px 42px 22px 42px; | |
| margin-bottom:36px; | |
| margin-top:16px; | |
| }} | |
| /* COMBINED INSIGHTS */ | |
| .insight-box {{ | |
| background:rgba(23,28,49,0.98); | |
| border-radius: 18px; | |
| box-shadow:0 0 26px {neon_blue}45; | |
| padding: 32px 34px 18px 34px; | |
| margin-bottom:33px; | |
| margin-top:20px; | |
| }} | |
| /* SUMMARY BOX */ | |
| .summary-box {{ | |
| background:rgba(23,28,49,0.97); | |
| border-radius: 15px; | |
| box-shadow:0 0 22px {neon_green}32; | |
| padding: 32px 38px 22px 38px; | |
| margin-bottom:36px; | |
| margin-top:14px; | |
| color:#fff; | |
| font-size:1.17em; | |
| }} | |
| /* RESPONSIVE */ | |
| @media (max-width: 1000px) {{ | |
| .persona-section-row {{ flex-direction: column; }} | |
| .persona-section-col {{ min-width: 100%; }} | |
| }} | |
| </style> | |
| """, unsafe_allow_html=True) | |
| # --- TITLE & DESCRIPTION --- | |
| st.markdown(f"<div class='neon-title'>🚀 New Launch Studio</div>", unsafe_allow_html=True) | |
| st.markdown(f"<div class='neon-sub'>Will your next product idea actually vibe with your audience? Pop your concept below and instantly see what your customer personas think—no fluff, just punchy, actionable feedback and a reality check on your launch.</div>", unsafe_allow_html=True) | |
| st.markdown(f"<div class='neon-heads-up'>⚡ Heads up: Our demo and market data is based on protein powder reviews—so for best results, enter a health, nutrition, or supplement product!</div>", unsafe_allow_html=True) | |
| # --- NAVIGATION BUTTONS --- | |
| st.markdown(f""" | |
| <div style="display:flex;gap:2em;justify-content:flex-start;margin-bottom:6px;"> | |
| <a href="/prt111" class="neon-btn" target="_self">🏠 Home</a> | |
| <a href="/persona" class="neon-btn neon-btn-pink" target="_self">👤 Persona Analysis</a> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| # --- PRODUCT DESCRIPTION INPUT --- | |
| st.markdown(f"<h2 style='color:{neon_blue};font-size:2.04rem;font-weight:900;margin-top:30px;margin-bottom:7px;'>1. Describe Your New Product</h2>", unsafe_allow_html=True) | |
| product_desc = st.text_area( | |
| "", | |
| height=110, | |
| placeholder="E.g. Introducing VanillaWhey: zero sugar, 25g protein, added digestive enzymes, eco-packaging, smooth vanilla flavor, perfect for fitness and daily wellness." | |
| ) | |
| # --- LOAD PERSONAS --- | |
| if os.path.exists(PERSONA_PATH): | |
| with open(PERSONA_PATH, "r", encoding="utf-8") as f: | |
| personas = json.load(f) | |
| else: | |
| personas = [] | |
| st.warning("No personas found. Please generate personas first in the Persona Analysis page.") | |
| def clean_points(text, max_points=2): | |
| lines = [l for l in text.replace('\r', '\n').split('\n') if l.strip() and not l.strip().lower().startswith( | |
| ('here is', 'here are', 'persona:', 'this is', 'for this persona', 'concerns:', 'the following', 'alignment:', '*', 'point'))] | |
| points = [] | |
| for l in lines: | |
| l = l.lstrip('-•1234567890. ').strip() | |
| if l and len(points) < max_points: | |
| points.append(l) | |
| return points if points else [text.strip()] | |
| def ai_points(prompt, max_points=2, max_tokens=120): | |
| try: | |
| chat_completion = groq_client.chat.completions.create( | |
| model=GROQ_MODEL, | |
| messages=[ | |
| {"role": "system", | |
| "content": f"You are a market research strategist. Reply with ONLY exactly {max_points} very brief, but fully written bullet points—no intros, no repetition, no generic phrases. Each point should be a full, clear sentence. Never add 'Here are' or any extra intro. Dont mention any names."}, | |
| {"role": "user", "content": prompt} | |
| ], | |
| max_tokens=max_tokens, temperature=0.7, stop=None | |
| ) | |
| return clean_points(chat_completion.choices[0].message.content.strip(), max_points) | |
| except Exception as e: | |
| return [f"Error: {e}"] | |
| def ai_notification(prompt, max_tokens=44): | |
| try: | |
| chat_completion = groq_client.chat.completions.create( | |
| model=GROQ_MODEL, | |
| messages=[ | |
| {"role": "system", | |
| "content": "You are a copywriter. Write a single, short, energetic notification or email (max 30 words, no names, no symbols), ending with a call-to-action. Make it stand out and complete."}, | |
| {"role": "user", "content": prompt} | |
| ], | |
| max_tokens=max_tokens, temperature=0.72, stop=None | |
| ) | |
| return chat_completion.choices[0].message.content.strip().replace("**", "") | |
| except Exception as e: | |
| return f"Error: {e}" | |
| def ai_percent(prompt): | |
| try: | |
| chat_completion = groq_client.chat.completions.create( | |
| model=GROQ_MODEL, | |
| messages=[{"role": "system", "content": "You are a market research strategist."}, {"role": "user", "content": prompt}], | |
| max_tokens=8, temperature=0.25 | |
| ) | |
| s = chat_completion.choices[0].message.content.strip() | |
| percent = ''.join([c for c in s if c.isdigit()]) | |
| return percent + "%" if percent else s | |
| except Exception as e: | |
| return "?" | |
| def ai_graph_insights(prompt, max_tokens=160): | |
| try: | |
| chat_completion = groq_client.chat.completions.create( | |
| model=GROQ_MODEL, | |
| messages=[ | |
| {"role": "system", | |
| "content": "You are a market analyst. Give only 4 numbered, very concise but meaningful insights in separate sentences, no intro line or extra formatting, no 'Here are', no asterisks or stars, just the facts."}, | |
| {"role": "user", "content": prompt} | |
| ], | |
| max_tokens=max_tokens, temperature=0.7, stop=None | |
| ) | |
| # Always keep only 4, no prefix text | |
| lines = [l.lstrip('-•1234567890. ').strip().replace("**", "") for l in chat_completion.choices[0].message.content.strip().split('\n') if l.strip()] | |
| return lines[:4] | |
| except Exception as e: | |
| return [f"Error: {e}"] | |
| def ai_summary(prompt, max_tokens=90): | |
| try: | |
| chat_completion = groq_client.chat.completions.create( | |
| model=GROQ_MODEL, | |
| messages=[ | |
| {"role": "system", | |
| "content": "Write a concise, professional executive summary in 3 sentences. No intro lines, no 'Here is', no asterisks. Be direct and to the point."}, | |
| {"role": "user", "content": prompt} | |
| ], | |
| max_tokens=max_tokens, temperature=0.7, stop=None | |
| ) | |
| return chat_completion.choices[0].message.content.strip().replace("**", "") | |
| except Exception as e: | |
| return f"Error: {e}" | |
| st.markdown("<div style='height:16px;'></div>", unsafe_allow_html=True) | |
| # --- GENERATE BUTTON --- | |
| test_btn = st.button( | |
| "🚦 Run Persona–Product Fit Check", | |
| help="Instantly see AI-powered feedback from every persona's perspective!", | |
| use_container_width=True | |
| ) | |
| st.markdown("<div style='height:12px;'></div>", unsafe_allow_html=True) | |
| if test_btn and product_desc and personas: | |
| st.markdown(f"<h2 style='color:{neon_blue};font-size:2.23rem;font-weight:900;margin-bottom:12px;margin-top:17px;'>2. Persona-by-Persona Results</h2>", unsafe_allow_html=True) | |
| persona_colors = [neon_blue, neon_green, neon_pink, neon_orange, neon_cyan] | |
| persona_cycle = iter(persona_colors) | |
| section_icons = { | |
| "Probable Reaction": "💡", | |
| "Alignment with Persona": "✅", | |
| "Potential Mismatches or Concerns": "⚠️", | |
| "Marketing Strategy": "📢", | |
| "Personalized Notification": "🔔", | |
| } | |
| def persona_block(persona, color): | |
| return st.container() | |
| # Pair personas 2 per row | |
| for i in range(0, len(personas), 2): | |
| cols = st.columns(2, gap="large") | |
| for j, col in enumerate(cols): | |
| if i + j < len(personas): | |
| persona = personas[i + j] | |
| color = next(persona_cycle, neon_blue) | |
| with col: | |
| st.markdown(f"<div class='persona-name-box' style='background:linear-gradient(90deg,{neon_blue},{neon_pink} 80%);margin-bottom:16px;'><span>{persona.get('icon','')} {persona['name']}</span></div>", unsafe_allow_html=True) | |
| st.markdown(f"<div style='height:4px;'></div>", unsafe_allow_html=True) | |
| st.markdown("<div class='persona-section-row'>", unsafe_allow_html=True) | |
| st.markdown("<div class='persona-section-col'>", unsafe_allow_html=True) | |
| st.markdown(f"<div class='block-label label-blue'>{section_icons['Probable Reaction']} Probable Reaction</div>", unsafe_allow_html=True) | |
| reactions = ai_points( | |
| f"Summarize two brief but complete points for this persona's likely reaction to the product: {product_desc}. Use clear, direct language.", | |
| max_points=2, max_tokens=90 | |
| ) | |
| st.markdown(f"<ul class='insight-list'>" + "".join([f"<li>{r}</li>" for r in reactions]) + "</ul>", unsafe_allow_html=True) | |
| st.markdown(f"<div class='block-label label-green'>{section_icons['Alignment with Persona']} Alignment with Persona</div>", unsafe_allow_html=True) | |
| aligns = ai_points( | |
| f"List two specific ways this persona's characteristics or needs will match with the features or benefits of the product: {product_desc}. " | |
| f"Be explicit: mention which part of the persona is satisfied by which product feature. Use clear, direct language.", | |
| max_points=2, max_tokens=100 | |
| ) | |
| st.markdown(f"<ul class='insight-list'>" + "".join([f"<li>{a}</li>" for a in aligns]) + "</ul>", unsafe_allow_html=True) | |
| st.markdown("</div>", unsafe_allow_html=True) | |
| st.markdown("<div class='persona-section-col'>", unsafe_allow_html=True) | |
| st.markdown(f"<div class='block-label label-pink'>{section_icons['Potential Mismatches or Concerns']} Potential Mismatches or Concerns</div>", unsafe_allow_html=True) | |
| mismatches = ai_points( | |
| f"List two precise concerns or mismatches: Which features or aspects of the {product_desc} may NOT align with this persona's preferences or needs? " | |
| f"Be explicit: mention which product feature is likely to be a turn-off or ignored by this persona.", | |
| max_points=2, max_tokens=100 | |
| ) | |
| st.markdown(f"<ul class='insight-list'>" + "".join([f"<li>{m}</li>" for m in mismatches]) + "</ul>", unsafe_allow_html=True) | |
| st.markdown(f"<div class='block-label label-orange'>{section_icons['Marketing Strategy']} Marketing Strategy</div>", unsafe_allow_html=True) | |
| strategy = ai_points( | |
| f"Suggest two creative, product-specific marketing strategies targeted at this persona for this product: {product_desc}. " | |
| f"Each point must clearly connect a product feature with a unique marketing approach for this persona.", | |
| max_points=2, max_tokens=100 | |
| ) | |
| st.markdown(f"<ul class='insight-list'>" + "".join([f"<li>{s}</li>" for s in strategy]) + "</ul>", unsafe_allow_html=True) | |
| st.markdown("</div>", unsafe_allow_html=True) | |
| st.markdown("</div>", unsafe_allow_html=True) | |
| st.markdown( | |
| f""" | |
| <div style='display:flex;align-items:center;gap:22px;margin-top:12px;margin-bottom:22px;'> | |
| <span class='interest-badge'>Interest Likelihood: {ai_percent('Estimate the likelihood (percent) that '+persona['name']+' would be interested in this product. Just the number and % sign, nothing else.')}</span> | |
| <div> | |
| <div class='block-label label-cyan' style='margin-bottom:3px;'>{section_icons['Personalized Notification']} Personalized Notification</div> | |
| <div class='notification-block'>{ai_notification( | |
| f"Write a concise, energetic notification or email about this product: {product_desc} aimed specifically at the persona {persona['name']}. " | |
| f"Address their top motivations and finish with a strong call-to-action. No names, no symbols." | |
| )}</div> | |
| </div> | |
| """, unsafe_allow_html=True | |
| ) | |
| st.markdown("<div style='height:4px;'></div>", unsafe_allow_html=True) | |
| # --- CHARTS (Demo) --- | |
| st.markdown(f"<h2 style='color:{neon_cyan};font-size:2.1rem;font-weight:800;margin-top:32px;'>3. Projected Market Impact</h2>", unsafe_allow_html=True) | |
| persona_names = [p['name'] for p in personas] | |
| np.random.seed(42) | |
| projected_market_share = np.random.dirichlet(np.ones(len(persona_names)), size=1)[0] | |
| projected_sentiment = projected_market_share * 0.6 + np.random.rand(len(persona_names)) * 0.4 # correlation | |
| c1, c2 = st.columns(2) | |
| with c1: | |
| st.markdown(f"<div style='font-size:1.17em;color:{neon_blue};font-weight:700;margin-bottom:6px;'>Projected Market Share by Persona</div>", unsafe_allow_html=True) | |
| fig1 = go.Figure(data=[go.Pie(labels=persona_names, values=projected_market_share, hole=0.45)]) | |
| fig1.update_traces(textinfo='percent+label') | |
| fig1.update_layout(margin=dict(l=14, r=14, b=14, t=14), showlegend=True) | |
| st.plotly_chart(fig1, use_container_width=True) | |
| with c2: | |
| st.markdown(f"<div style='font-size:1.17em;color:{neon_orange};font-weight:700;margin-bottom:6px;'>Projected Sentiment by Persona</div>", unsafe_allow_html=True) | |
| fig2 = go.Figure(data=[go.Bar(x=persona_names, y=projected_sentiment, | |
| marker=dict(color=[neon_green, neon_blue, neon_pink, neon_orange, neon_cyan][:len(persona_names)]))]) | |
| fig2.update_layout(xaxis_title="Persona", yaxis_title="Projected Sentiment", font=dict(size=15)) | |
| st.plotly_chart(fig2, use_container_width=True) | |
| # --- Combined Chart Insights --- | |
| combined_prompt = ( | |
| f"Given the projected market share {list(np.round(projected_market_share*100,1))} percent and projected sentiment {list(np.round(projected_sentiment*100,1))} for these personas: {', '.join(persona_names)}, " | |
| "summarize 4 concise points that correlate the two charts and reveal the most important market insights. Each point should be in a new line and fully written." | |
| ) | |
| insights = ai_graph_insights(combined_prompt, max_tokens=200) | |
| st.markdown( | |
| f"<div class='insight-box'><div style='font-size:1.18em;color:{neon_blue};font-weight:700;margin-bottom:10px;'>Key Combined Insights</div>" | |
| f"<ul class='insight-list'>" + "".join([f"<li>{bp}</li>" for bp in insights]) + "</ul></div>", unsafe_allow_html=True | |
| ) | |
| # --- OVERALL SUMMARY --- | |
| st.markdown(f"<h2 style='color:{neon_green};font-size:2rem;font-weight:900;margin-top:18px;'>4. Overall Summary</h2>", unsafe_allow_html=True) | |
| overall_prompt = ( | |
| f"Given these personas: {', '.join([p['name'] for p in personas])}, and the new product: {product_desc}, " | |
| "write a concise executive summary (3 sentences, no intro, no asterisks), focusing on overall fit, the main challenge, and the best next move for launch." | |
| ) | |
| summary_text = ai_summary(overall_prompt, max_tokens=1000) | |
| st.markdown( | |
| f"<div class='summary-box'>{summary_text}</div>", | |
| unsafe_allow_html=True | |
| ) | |
| st.markdown("---") | |
| elif test_btn: | |
| st.warning("Please enter your product description to see the results.") | |