| <!DOCTYPE html> |
| <html lang="en"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>Personality Shopping Agent — RL Environment</title> |
| <meta name="description" content="AI agent learns your shopping personality through reinforcement learning. Just type what you want — the agent picks for you automatically."> |
| <link rel="preconnect" href="https://fonts.googleapis.com"> |
| <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&display=swap" rel="stylesheet"> |
| <style> |
| *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } |
| |
| :root { |
| --bg-primary: #06080f; |
| --bg-secondary: #0d1117; |
| --bg-card: rgba(13, 17, 23, 0.85); |
| --bg-glass: rgba(255, 255, 255, 0.03); |
| --bg-glass-hover: rgba(255, 255, 255, 0.06); |
| --border: rgba(255, 255, 255, 0.06); |
| --border-active: rgba(99, 102, 241, 0.5); |
| --text-primary: #e4e8f1; |
| --text-secondary: #8b93a7; |
| --text-muted: #525b6e; |
| --accent: #6366f1; |
| --accent-light: #818cf8; |
| --accent-glow: rgba(99, 102, 241, 0.15); |
| --green: #22c55e; |
| --green-bg: rgba(34, 197, 94, 0.1); |
| --green-border: rgba(34, 197, 94, 0.25); |
| --red: #ef4444; |
| --red-bg: rgba(239, 68, 68, 0.1); |
| --red-border: rgba(239, 68, 68, 0.25); |
| --amber: #f59e0b; |
| --amber-bg: rgba(245, 158, 11, 0.1); |
| --blue: #3b82f6; |
| --blue-bg: rgba(59, 130, 246, 0.1); |
| --pink: #ec4899; |
| --pink-bg: rgba(236, 72, 153, 0.08); |
| --radius: 16px; |
| --radius-sm: 10px; |
| --radius-xs: 6px; |
| --transition: 0.3s cubic-bezier(0.4, 0, 0.2, 1); |
| } |
| |
| html { scroll-behavior: smooth; } |
| |
| body { |
| font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif; |
| background: var(--bg-primary); |
| color: var(--text-primary); |
| min-height: 100vh; |
| overflow-x: hidden; |
| } |
| |
| |
| .ambient-bg { |
| position: fixed; inset: 0; z-index: 0; pointer-events: none; overflow: hidden; |
| } |
| .ambient-bg .orb { |
| position: absolute; border-radius: 50%; filter: blur(140px); opacity: 0.5; |
| } |
| .ambient-bg .orb-1 { |
| width: 700px; height: 700px; |
| background: radial-gradient(circle, rgba(99,102,241,0.12), transparent 70%); |
| top: -250px; left: -200px; |
| animation: float-orb 20s ease-in-out infinite; |
| } |
| .ambient-bg .orb-2 { |
| width: 500px; height: 500px; |
| background: radial-gradient(circle, rgba(139,92,246,0.08), transparent 70%); |
| bottom: -200px; right: -150px; |
| animation: float-orb 25s ease-in-out infinite reverse; |
| } |
| .ambient-bg .orb-3 { |
| width: 400px; height: 400px; |
| background: radial-gradient(circle, rgba(236,72,153,0.06), transparent 70%); |
| top: 40%; left: 50%; |
| animation: float-orb 18s ease-in-out infinite 5s; |
| } |
| |
| @keyframes float-orb { |
| 0%, 100% { transform: translate(0, 0) scale(1); } |
| 33% { transform: translate(30px, -20px) scale(1.05); } |
| 66% { transform: translate(-20px, 15px) scale(0.95); } |
| } |
| |
| |
| .header { |
| position: sticky; top: 0; z-index: 100; |
| background: rgba(6, 8, 15, 0.8); |
| backdrop-filter: blur(24px) saturate(1.4); |
| border-bottom: 1px solid var(--border); |
| padding: 14px 32px; |
| display: flex; align-items: center; justify-content: space-between; |
| } |
| .header-left { display: flex; align-items: center; gap: 14px; } |
| .header-logo { |
| width: 42px; height: 42px; border-radius: 12px; |
| background: linear-gradient(135deg, #6366f1, #a855f7, #ec4899); |
| display: flex; align-items: center; justify-content: center; |
| font-size: 22px; |
| box-shadow: 0 0 30px rgba(99,102,241,0.25); |
| animation: logo-glow 3s ease-in-out infinite; |
| } |
| @keyframes logo-glow { |
| 0%, 100% { box-shadow: 0 0 30px rgba(99,102,241,0.25); } |
| 50% { box-shadow: 0 0 50px rgba(99,102,241,0.4); } |
| } |
| .header-title { font-size: 18px; font-weight: 800; letter-spacing: -0.5px; } |
| .header-subtitle { font-size: 11px; color: var(--text-muted); font-weight: 500; letter-spacing: 0.3px; } |
| |
| .header-metrics { display: flex; gap: 10px; align-items: center; } |
| .metric { |
| display: flex; align-items: center; gap: 7px; |
| background: var(--bg-glass); |
| border: 1px solid var(--border); |
| border-radius: 100px; |
| padding: 6px 14px; |
| font-size: 12px; font-weight: 600; |
| } |
| .metric .label { color: var(--text-muted); } |
| .metric .val { color: var(--text-primary); } |
| .metric.episode .val { color: var(--accent-light); } |
| .metric.reward .val { color: var(--green); } |
| .metric.success .val { color: var(--amber); } |
| |
| |
| .container { |
| position: relative; z-index: 1; |
| max-width: 1200px; |
| margin: 0 auto; |
| padding: 32px 28px 60px; |
| } |
| |
| |
| .hero-section { |
| text-align: center; |
| padding: 48px 20px 36px; |
| } |
| .hero-title { |
| font-size: 36px; font-weight: 900; |
| letter-spacing: -1.5px; |
| background: linear-gradient(135deg, #e4e8f1 0%, #818cf8 50%, #ec4899 100%); |
| -webkit-background-clip: text; |
| -webkit-text-fill-color: transparent; |
| background-clip: text; |
| margin-bottom: 10px; |
| line-height: 1.15; |
| } |
| .hero-desc { |
| font-size: 15px; color: var(--text-secondary); |
| max-width: 640px; margin: 0 auto 28px; |
| line-height: 1.6; |
| } |
| .hero-badge { |
| display: inline-flex; align-items: center; gap: 8px; |
| padding: 6px 16px; border-radius: 100px; |
| background: rgba(34,197,94,0.08); |
| border: 1px solid rgba(34,197,94,0.2); |
| color: var(--green); |
| font-size: 12px; font-weight: 700; |
| margin-bottom: 24px; |
| text-transform: uppercase; |
| letter-spacing: 0.6px; |
| } |
| |
| |
| .prompt-bar { |
| display: flex; gap: 0; |
| max-width: 600px; margin: 0 auto 20px; |
| border-radius: var(--radius); |
| overflow: hidden; |
| box-shadow: 0 0 0 1px var(--border), 0 8px 40px rgba(0,0,0,0.4); |
| transition: box-shadow var(--transition); |
| } |
| .prompt-bar:focus-within { |
| box-shadow: 0 0 0 2px var(--accent), 0 8px 50px rgba(99,102,241,0.2); |
| } |
| .prompt-bar input { |
| flex: 1; padding: 16px 22px; |
| background: var(--bg-secondary); |
| border: none; |
| color: var(--text-primary); |
| font-family: inherit; font-size: 16px; font-weight: 500; |
| outline: none; |
| } |
| .prompt-bar input::placeholder { color: var(--text-muted); } |
| .prompt-bar button { |
| padding: 16px 28px; |
| background: linear-gradient(135deg, #6366f1, #8b5cf6); |
| border: none; |
| color: white; |
| font-family: inherit; font-size: 15px; font-weight: 700; |
| cursor: pointer; |
| transition: var(--transition); |
| white-space: nowrap; |
| } |
| .prompt-bar button:hover { |
| background: linear-gradient(135deg, #7c7ff7, #9b7cf9); |
| } |
| .prompt-bar button:disabled { |
| opacity: 0.4; cursor: not-allowed; |
| } |
| |
| |
| .category-pills { |
| display: flex; gap: 8px; justify-content: center; flex-wrap: wrap; |
| margin-bottom: 16px; |
| } |
| .pill { |
| padding: 7px 16px; |
| border-radius: 100px; |
| border: 1px solid var(--border); |
| background: transparent; |
| color: var(--text-muted); |
| font-family: inherit; font-size: 12px; font-weight: 600; |
| cursor: pointer; transition: var(--transition); |
| } |
| .pill:hover { background: var(--bg-glass-hover); color: var(--text-secondary); border-color: rgba(255,255,255,0.1); } |
| .pill.active { |
| background: rgba(99,102,241,0.12); |
| color: var(--accent-light); border-color: rgba(99,102,241,0.3); |
| } |
| |
| |
| .flow-area { |
| display: none; |
| margin-top: 12px; |
| } |
| .flow-area.active { display: block; } |
| |
| |
| .thinking-section { |
| text-align: center; |
| padding: 60px 20px; |
| display: none; |
| } |
| .thinking-section.active { display: block; } |
| .thinking-brain { |
| font-size: 56px; |
| animation: think-bounce 1.5s ease-in-out infinite; |
| margin-bottom: 16px; |
| } |
| @keyframes think-bounce { |
| 0%, 100% { transform: translateY(0) scale(1); } |
| 50% { transform: translateY(-12px) scale(1.1); } |
| } |
| .thinking-text { |
| font-size: 16px; font-weight: 600; color: var(--text-secondary); |
| animation: think-pulse 1.5s ease-in-out infinite; |
| } |
| @keyframes think-pulse { |
| 0%, 100% { opacity: 1; } |
| 50% { opacity: 0.4; } |
| } |
| .thinking-dots::after { |
| content: ''; |
| animation: dots 1.5s steps(4) infinite; |
| } |
| @keyframes dots { |
| 0% { content: ''; } |
| 25% { content: '.'; } |
| 50% { content: '..'; } |
| 75% { content: '...'; } |
| } |
| |
| |
| .attempt-section { |
| display: none; |
| max-width: 750px; margin: 0 auto; |
| } |
| .attempt-section.active { display: block; } |
| |
| .attempt-badge { |
| display: inline-flex; align-items: center; gap: 6px; |
| padding: 6px 16px; |
| border-radius: 100px; |
| font-size: 12px; font-weight: 700; |
| margin-bottom: 20px; |
| text-transform: uppercase; |
| letter-spacing: 0.8px; |
| } |
| .attempt-badge.exploring { background: var(--amber-bg); color: var(--amber); border: 1px solid rgba(245,158,11,0.25); } |
| .attempt-badge.exploiting { background: rgba(99,102,241,0.1); color: var(--accent-light); border: 1px solid rgba(99,102,241,0.25); } |
| .attempt-badge.success-badge { background: var(--green-bg); color: var(--green); border: 1px solid var(--green-border); } |
| |
| .attempt-card { |
| background: var(--bg-card); |
| border: 1px solid var(--border); |
| border-radius: 20px; |
| padding: 36px; |
| box-shadow: 0 12px 48px rgba(0,0,0,0.3); |
| position: relative; |
| overflow: hidden; |
| animation: pick-enter 0.6s cubic-bezier(0.34, 1.56, 0.64, 1); |
| } |
| @keyframes pick-enter { |
| 0% { opacity: 0; transform: translateY(30px) scale(0.95); } |
| 100% { opacity: 1; transform: translateY(0) scale(1); } |
| } |
| .attempt-card::before { |
| content: ''; |
| position: absolute; top: 0; left: 0; right: 0; |
| height: 4px; |
| } |
| .attempt-card.success-card::before { |
| background: linear-gradient(90deg, #22c55e, #4ade80, #86efac); |
| } |
| .attempt-card.fail-card::before { |
| background: linear-gradient(90deg, #6366f1, #a855f7, #ec4899); |
| } |
| .attempt-card.learning-card::before { |
| background: linear-gradient(90deg, #f59e0b, #fb923c, #f97316); |
| } |
| |
| .attempt-header { |
| display: flex; align-items: flex-start; justify-content: space-between; |
| margin-bottom: 20px; gap: 16px; |
| } |
| .attempt-label { |
| font-size: 11px; font-weight: 700; text-transform: uppercase; |
| letter-spacing: 1.5px; color: var(--accent-light); |
| margin-bottom: 6px; |
| } |
| .attempt-product-name { |
| font-size: 26px; font-weight: 800; letter-spacing: -0.5px; |
| line-height: 1.2; |
| } |
| .attempt-score-badge { |
| flex-shrink: 0; |
| display: flex; flex-direction: column; align-items: center; |
| border-radius: 16px; |
| padding: 12px 16px; |
| min-width: 80px; |
| } |
| .attempt-score-badge.good { |
| background: linear-gradient(135deg, rgba(34,197,94,0.1), rgba(34,197,94,0.05)); |
| border: 1px solid rgba(34,197,94,0.2); |
| } |
| .attempt-score-badge.mid { |
| background: linear-gradient(135deg, rgba(99,102,241,0.1), rgba(236,72,153,0.1)); |
| border: 1px solid rgba(99,102,241,0.2); |
| } |
| .attempt-score-badge.bad { |
| background: linear-gradient(135deg, rgba(239,68,68,0.1), rgba(239,68,68,0.05)); |
| border: 1px solid rgba(239,68,68,0.2); |
| } |
| .attempt-score-value { |
| font-size: 28px; font-weight: 900; |
| } |
| .attempt-score-value.good { |
| background: linear-gradient(135deg, #22c55e, #4ade80); |
| -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; |
| } |
| .attempt-score-value.mid { |
| background: linear-gradient(135deg, #818cf8, #ec4899); |
| -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; |
| } |
| .attempt-score-value.bad { |
| background: linear-gradient(135deg, #ef4444, #f87171); |
| -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; |
| } |
| .attempt-score-label { |
| font-size: 9px; font-weight: 700; text-transform: uppercase; |
| letter-spacing: 0.8px; color: var(--text-muted); |
| margin-top: 2px; |
| } |
| |
| .attempt-meta { |
| display: flex; flex-wrap: wrap; gap: 10px; |
| margin-bottom: 20px; |
| } |
| .attempt-tag { |
| display: inline-flex; align-items: center; gap: 5px; |
| padding: 6px 14px; border-radius: 100px; |
| font-size: 13px; font-weight: 600; |
| } |
| .tag-price { background: rgba(99,102,241,0.1); color: #a5b4fc; } |
| .tag-rating { background: var(--amber-bg); color: var(--amber); } |
| .tag-reviews { background: var(--bg-glass); color: var(--text-secondary); border: 1px solid var(--border); } |
| .tag-brand { background: var(--blue-bg); color: var(--blue); } |
| .tag-refund { background: var(--green-bg); color: var(--green); } |
| .tag-norefund { background: var(--red-bg); color: var(--red); } |
| .tag-seller { background: var(--bg-glass); color: var(--text-muted); border: 1px solid var(--border); } |
| |
| .attempt-features { |
| display: flex; flex-wrap: wrap; gap: 6px; |
| margin-bottom: 24px; |
| } |
| .feature-chip { |
| padding: 4px 10px; |
| border-radius: 6px; |
| background: var(--bg-glass); |
| border: 1px solid var(--border); |
| font-size: 11px; color: var(--text-muted); |
| font-weight: 500; |
| } |
| |
| .attempt-reasoning { |
| background: linear-gradient(135deg, rgba(99,102,241,0.05), rgba(139,92,246,0.05)); |
| border: 1px solid rgba(99,102,241,0.12); |
| border-radius: var(--radius-sm); |
| padding: 18px 20px; |
| margin-bottom: 20px; |
| } |
| .reasoning-title { |
| font-size: 11px; font-weight: 700; text-transform: uppercase; |
| letter-spacing: 1px; color: var(--accent-light); |
| margin-bottom: 8px; |
| display: flex; align-items: center; gap: 6px; |
| } |
| .reasoning-text { |
| font-size: 14px; line-height: 1.7; |
| color: var(--text-secondary); |
| white-space: pre-wrap; |
| } |
| |
| |
| .attempt-reward-row { |
| display: flex; align-items: center; justify-content: space-between; |
| padding: 14px 18px; |
| border-radius: var(--radius-sm); |
| margin-top: 8px; |
| } |
| .attempt-reward-row.positive { |
| background: rgba(34,197,94,0.06); |
| border: 1px solid rgba(34,197,94,0.15); |
| } |
| .attempt-reward-row.negative { |
| background: rgba(239,68,68,0.06); |
| border: 1px solid rgba(239,68,68,0.15); |
| } |
| .reward-label-text { |
| font-size: 13px; font-weight: 600; |
| } |
| .reward-label-text.positive { color: var(--green); } |
| .reward-label-text.negative { color: var(--red); } |
| .reward-number { |
| font-size: 24px; font-weight: 900; |
| } |
| .reward-number.positive { color: var(--green); } |
| .reward-number.negative { color: var(--red); } |
| |
| |
| .attempt-status { |
| text-align: center; margin-top: 18px; |
| font-size: 14px; font-weight: 600; |
| } |
| .attempt-status.learning { color: var(--amber); } |
| .attempt-status.done { color: var(--green); } |
| |
| |
| .result-section { |
| display: none; |
| max-width: 750px; margin: 0 auto; |
| } |
| .result-section.active { display: block; } |
| |
| .result-card { |
| text-align: center; |
| background: var(--bg-card); |
| border-radius: 20px; |
| padding: 48px 36px; |
| border: 1px solid var(--border); |
| box-shadow: 0 12px 48px rgba(0,0,0,0.3); |
| animation: pick-enter 0.6s cubic-bezier(0.34, 1.56, 0.64, 1); |
| } |
| .result-emoji { font-size: 64px; margin-bottom: 16px; } |
| .result-title { font-size: 24px; font-weight: 800; margin-bottom: 8px; } |
| .result-message { |
| font-size: 15px; color: var(--text-secondary); |
| line-height: 1.6; margin-bottom: 16px; |
| } |
| .result-reward { |
| font-size: 48px; font-weight: 900; |
| margin: 16px 0; |
| } |
| .result-reward.positive { |
| background: linear-gradient(135deg, #22c55e, #4ade80); |
| -webkit-background-clip: text; |
| -webkit-text-fill-color: transparent; |
| background-clip: text; |
| } |
| .result-reward.negative { |
| background: linear-gradient(135deg, #ef4444, #f87171); |
| -webkit-background-clip: text; |
| -webkit-text-fill-color: transparent; |
| background-clip: text; |
| } |
| |
| .result-ideal { |
| margin: 16px 0 24px; |
| padding: 14px 20px; |
| background: var(--bg-glass); |
| border: 1px solid var(--border); |
| border-radius: var(--radius-sm); |
| font-size: 13px; color: var(--text-secondary); |
| } |
| .result-ideal strong { color: var(--text-primary); } |
| |
| .result-actions { |
| display: flex; gap: 12px; justify-content: center; |
| margin-top: 24px; |
| } |
| |
| .btn { |
| display: inline-flex; align-items: center; gap: 8px; |
| padding: 12px 24px; |
| border-radius: var(--radius-sm); |
| border: 1px solid var(--border); |
| background: var(--bg-glass); |
| color: var(--text-secondary); |
| font-family: inherit; font-size: 14px; font-weight: 600; |
| cursor: pointer; transition: var(--transition); |
| } |
| .btn:hover { |
| background: var(--bg-glass-hover); |
| color: var(--text-primary); |
| transform: translateY(-1px); |
| } |
| .btn-primary { |
| background: linear-gradient(135deg, #6366f1, #8b5cf6); |
| border: none; color: white; |
| box-shadow: 0 4px 20px rgba(99,102,241,0.3); |
| } |
| .btn-primary:hover { |
| box-shadow: 0 6px 30px rgba(99,102,241,0.5); |
| background: linear-gradient(135deg, #7c7ff7, #9b7cf9); |
| } |
| .btn-danger { |
| background: rgba(239,68,68,0.08); |
| border-color: rgba(239,68,68,0.2); |
| color: var(--red); |
| } |
| .btn-danger:hover { |
| background: rgba(239,68,68,0.15); |
| } |
| |
| |
| .timeline-section { |
| max-width: 750px; margin: 32px auto 0; |
| display: none; |
| } |
| .timeline-section.active { display: block; } |
| |
| .timeline-title { |
| font-size: 13px; font-weight: 700; text-transform: uppercase; |
| letter-spacing: 1.2px; color: var(--text-muted); |
| margin-bottom: 16px; |
| display: flex; align-items: center; gap: 8px; |
| } |
| .timeline-title .count { |
| background: var(--accent); |
| color: white; font-size: 10px; font-weight: 800; |
| padding: 2px 8px; border-radius: 100px; |
| } |
| |
| .timeline { display: flex; flex-direction: column; gap: 10px; } |
| .timeline-item { |
| display: flex; gap: 14px; align-items: flex-start; |
| padding: 14px 18px; |
| background: var(--bg-glass); |
| border: 1px solid var(--border); |
| border-radius: var(--radius-sm); |
| animation: slideUp 0.4s ease; |
| } |
| @keyframes slideUp { |
| from { opacity: 0; transform: translateY(10px); } |
| to { opacity: 1; transform: translateY(0); } |
| } |
| .timeline-num { |
| flex-shrink: 0; |
| width: 32px; height: 32px; border-radius: 50%; |
| display: flex; align-items: center; justify-content: center; |
| font-weight: 800; font-size: 13px; |
| } |
| .timeline-num.success { background: var(--green-bg); color: var(--green); } |
| .timeline-num.fail { background: var(--red-bg); color: var(--red); } |
| .timeline-num.pending { background: rgba(99,102,241,0.1); color: var(--accent-light); } |
| .timeline-body { flex: 1; } |
| .timeline-product { font-weight: 700; font-size: 14px; margin-bottom: 2px; } |
| .timeline-detail { font-size: 12px; color: var(--text-muted); } |
| .timeline-strategy { font-size: 11px; margin-top: 2px; } |
| .timeline-strategy.explore { color: var(--amber); } |
| .timeline-strategy.exploit { color: var(--accent-light); } |
| .timeline-reward { font-weight: 700; font-size: 13px; } |
| .timeline-reward.positive { color: var(--green); } |
| .timeline-reward.negative { color: var(--red); } |
| |
| |
| .products-overview { |
| max-width: 750px; margin: 32px auto 0; |
| display: none; |
| } |
| .products-overview.active { display: block; } |
| |
| .products-toggle { |
| width: 100%; |
| padding: 12px 18px; |
| background: var(--bg-glass); |
| border: 1px solid var(--border); |
| border-radius: var(--radius-sm); |
| color: var(--text-secondary); |
| font-family: inherit; font-size: 13px; font-weight: 600; |
| cursor: pointer; transition: var(--transition); |
| display: flex; align-items: center; justify-content: space-between; |
| } |
| .products-toggle:hover { background: var(--bg-glass-hover); } |
| .products-toggle .arrow { |
| transition: transform 0.3s ease; |
| } |
| .products-toggle.open .arrow { transform: rotate(180deg); } |
| |
| .products-grid { |
| display: none; |
| grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); |
| gap: 12px; |
| margin-top: 12px; |
| } |
| .products-grid.open { |
| display: grid; |
| } |
| |
| .mini-product { |
| padding: 14px; |
| background: var(--bg-glass); |
| border: 1px solid var(--border); |
| border-radius: var(--radius-sm); |
| transition: var(--transition); |
| } |
| .mini-product.highlighted { |
| border-color: var(--green); |
| box-shadow: 0 0 0 1px var(--green), 0 0 20px rgba(34,197,94,0.15); |
| } |
| .mini-product.tried { |
| border-color: rgba(239,68,68,0.3); |
| opacity: 0.5; |
| } |
| .mini-product.ideal { |
| border-color: var(--accent); |
| box-shadow: 0 0 0 1px var(--accent), 0 0 20px var(--accent-glow); |
| } |
| .mini-product .mp-name { |
| font-size: 13px; font-weight: 700; |
| margin-bottom: 4px; |
| white-space: nowrap; overflow: hidden; text-overflow: ellipsis; |
| } |
| .mini-product .mp-meta { |
| font-size: 11px; color: var(--text-muted); |
| display: flex; gap: 8px; flex-wrap: wrap; |
| } |
| .mini-product .mp-score { |
| font-size: 11px; font-weight: 700; color: var(--pink); |
| margin-top: 4px; |
| } |
| .mini-product .mp-tag { |
| font-size: 10px; font-weight: 600; |
| margin-top: 3px; |
| } |
| |
| |
| .reward-summary { |
| max-width: 750px; margin: 24px auto 0; |
| display: none; |
| } |
| .reward-summary.active { display: block; } |
| .reward-bar-container { |
| background: var(--bg-glass); |
| border: 1px solid var(--border); |
| border-radius: var(--radius-sm); |
| padding: 16px 20px; |
| } |
| .reward-bar-header { |
| display: flex; justify-content: space-between; align-items: center; |
| margin-bottom: 10px; |
| } |
| .reward-bar-label { font-size: 12px; font-weight: 700; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.8px; } |
| .reward-bar-value { |
| font-size: 20px; font-weight: 900; |
| background: linear-gradient(135deg, #818cf8, #22c55e); |
| -webkit-background-clip: text; |
| -webkit-text-fill-color: transparent; |
| background-clip: text; |
| } |
| .reward-track { |
| height: 8px; |
| background: rgba(255,255,255,0.04); |
| border-radius: 100px; |
| overflow: hidden; |
| } |
| .reward-fill { |
| height: 100%; |
| background: linear-gradient(90deg, #6366f1, #22c55e); |
| border-radius: 100px; |
| transition: width 0.5s ease; |
| } |
| |
| |
| .personality-float { |
| position: fixed; top: 80px; right: 20px; |
| width: 280px; |
| background: rgba(13, 17, 23, 0.92); |
| backdrop-filter: blur(20px); |
| border: 1px solid var(--border); |
| border-radius: var(--radius); |
| padding: 20px; |
| z-index: 50; |
| display: none; |
| box-shadow: 0 12px 48px rgba(0,0,0,0.4); |
| max-height: calc(100vh - 100px); |
| overflow-y: auto; |
| } |
| .personality-float.active { display: block; } |
| .personality-float::-webkit-scrollbar { width: 4px; } |
| .personality-float::-webkit-scrollbar-thumb { background: var(--border); border-radius: 4px; } |
| |
| .pf-title { |
| font-size: 12px; font-weight: 700; text-transform: uppercase; |
| letter-spacing: 1px; color: var(--text-muted); |
| margin-bottom: 14px; |
| display: flex; align-items: center; gap: 6px; |
| } |
| .pf-close { |
| margin-left: auto; |
| background: none; border: none; |
| color: var(--text-muted); cursor: pointer; |
| font-size: 16px; padding: 2px 6px; |
| transition: var(--transition); |
| } |
| .pf-close:hover { color: var(--text-primary); } |
| |
| .trait-row { |
| display: flex; align-items: center; gap: 10px; |
| margin-bottom: 10px; |
| } |
| .trait-label { |
| font-size: 10px; color: var(--text-secondary); |
| font-weight: 600; width: 100px; flex-shrink: 0; |
| } |
| .trait-bar { |
| flex: 1; height: 5px; |
| background: rgba(255,255,255,0.05); |
| border-radius: 100px; overflow: hidden; |
| } |
| .trait-fill { |
| height: 100%; border-radius: 100px; |
| transition: width 0.6s ease; |
| } |
| .trait-val { |
| font-size: 10px; font-weight: 800; |
| width: 32px; text-align: right; |
| } |
| |
| .profile-toggle-btn { |
| position: fixed; top: 80px; right: 20px; |
| width: 44px; height: 44px; |
| border-radius: 50%; |
| background: var(--bg-secondary); |
| border: 1px solid var(--border); |
| color: var(--text-secondary); |
| font-size: 18px; |
| cursor: pointer; z-index: 49; |
| display: flex; align-items: center; justify-content: center; |
| transition: var(--transition); |
| box-shadow: 0 4px 16px rgba(0,0,0,0.3); |
| } |
| .profile-toggle-btn:hover { |
| background: var(--bg-glass-hover); |
| color: var(--text-primary); |
| box-shadow: 0 6px 24px rgba(0,0,0,0.4); |
| } |
| |
| |
| .loading-overlay { |
| position: fixed; inset: 0; |
| background: rgba(0,0,0,0.5); |
| backdrop-filter: blur(8px); |
| display: flex; align-items: center; justify-content: center; |
| z-index: 1000; |
| opacity: 0; pointer-events: none; |
| transition: var(--transition); |
| } |
| .loading-overlay.active { opacity: 1; pointer-events: all; } |
| .spinner { |
| width: 48px; height: 48px; |
| border: 4px solid rgba(255,255,255,0.1); |
| border-top-color: var(--accent); |
| border-radius: 50%; |
| animation: spin 0.7s linear infinite; |
| } |
| @keyframes spin { to { transform: rotate(360deg); } } |
| |
| |
| .toast-container { |
| position: fixed; bottom: 24px; right: 24px; |
| z-index: 2000; |
| display: flex; flex-direction: column; gap: 8px; |
| } |
| .toast { |
| padding: 14px 22px; |
| background: var(--bg-secondary); |
| border: 1px solid var(--border); |
| border-radius: var(--radius-sm); |
| box-shadow: 0 8px 32px rgba(0,0,0,0.4); |
| font-size: 13px; font-weight: 500; |
| animation: toastIn 0.3s ease; |
| max-width: 380px; |
| } |
| .toast.success { border-color: var(--green-border); color: var(--green); } |
| .toast.error { border-color: var(--red-border); color: var(--red); } |
| .toast.info { border-color: rgba(99,102,241,0.3); color: var(--accent-light); } |
| @keyframes toastIn { |
| from { opacity: 0; transform: translateY(10px) scale(0.95); } |
| to { opacity: 1; transform: translateY(0) scale(1); } |
| } |
| |
| |
| @media (max-width: 768px) { |
| .header { padding: 12px 16px; } |
| .container { padding: 20px 16px 40px; } |
| .hero-title { font-size: 26px; } |
| .hero-section { padding: 30px 10px 24px; } |
| .prompt-bar { border-radius: var(--radius-sm); } |
| .prompt-bar input { padding: 14px 16px; font-size: 15px; } |
| .prompt-bar button { padding: 14px 20px; font-size: 14px; } |
| .attempt-card { padding: 24px; } |
| .attempt-product-name { font-size: 20px; } |
| .personality-float { display: none !important; } |
| .profile-toggle-btn { display: none; } |
| } |
| </style> |
| </head> |
| <body> |
|
|
| |
| <div class="ambient-bg"> |
| <div class="orb orb-1"></div> |
| <div class="orb orb-2"></div> |
| <div class="orb orb-3"></div> |
| </div> |
|
|
| |
| <header class="header"> |
| <div class="header-left"> |
| <div class="header-logo">🧠</div> |
| <div> |
| <div class="header-title">Personality Shopping Agent</div> |
| <div class="header-subtitle">AUTONOMOUS RL • SELF-LEARNING</div> |
| </div> |
| </div> |
| <div class="header-metrics"> |
| <div class="metric episode"> |
| <span class="label">Episodes</span> |
| <span class="val" id="m-episodes">0</span> |
| </div> |
| <div class="metric success"> |
| <span class="label">Success</span> |
| <span class="val" id="m-success">0%</span> |
| </div> |
| <div class="metric reward"> |
| <span class="label">Reward</span> |
| <span class="val" id="m-reward">0.00</span> |
| </div> |
| </div> |
| </header> |
|
|
| |
| <div class="container"> |
|
|
| |
| <section class="hero-section" id="hero-section"> |
| <div class="hero-badge">🤖 Fully Autonomous — No Training Needed</div> |
| <h1 class="hero-title">What do you want to buy?</h1> |
| <p class="hero-desc"> |
| Type a product — the AI agent will read your personality from memory and <strong>automatically pick, score, learn, and improve</strong> on its own. |
| No clicking "buy" or "don't buy". Just watch it learn. |
| </p> |
|
|
| <div class="category-pills" id="category-pills"> |
| <button class="pill active" data-cat="lip balm">💄 Lip Balm</button> |
| <button class="pill" data-cat="earbuds">🎧 Earbuds</button> |
| <button class="pill" data-cat="backpack">🎒 Backpack</button> |
| <button class="pill" data-cat="mixer grinder">🍳 Mixer Grinder</button> |
| <button class="pill" data-cat="headphones">🎵 Headphones</button> |
| <button class="pill" data-cat="water bottle">🫗 Water Bottle</button> |
| </div> |
|
|
| <div class="prompt-bar" id="prompt-bar"> |
| <input type="text" id="prompt-input" placeholder="e.g., wireless earbuds, lip balm, laptop backpack..." value="lip balm" autocomplete="off"> |
| <button id="btn-start">🚀 Start Agent</button> |
| </div> |
| </section> |
|
|
| |
| <div class="flow-area" id="flow-area"> |
|
|
| |
| <div class="thinking-section" id="thinking-section"> |
| <div class="thinking-brain">🧠</div> |
| <div class="thinking-text">Agent is running autonomous RL episode<span class="thinking-dots"></span></div> |
| </div> |
|
|
| |
| <div class="attempt-section" id="attempt-section"> |
| <div style="text-align:center;"> |
| <div class="attempt-badge exploring" id="attempt-badge">🎲 EXPLORING</div> |
| </div> |
|
|
| <div class="attempt-card fail-card" id="attempt-card"> |
| <div class="attempt-header"> |
| <div> |
| <div class="attempt-label" id="attempt-label">🤖 Attempt #1</div> |
| <div class="attempt-product-name" id="attempt-name">—</div> |
| </div> |
| <div class="attempt-score-badge mid" id="attempt-score-badge"> |
| <div class="attempt-score-value mid" id="attempt-score">0%</div> |
| <div class="attempt-score-label">Match</div> |
| </div> |
| </div> |
|
|
| <div class="attempt-meta" id="attempt-meta"></div> |
| <div class="attempt-features" id="attempt-features"></div> |
|
|
| <div class="attempt-reasoning"> |
| <div class="reasoning-title">💭 Agent's Reasoning</div> |
| <div class="reasoning-text" id="attempt-reasoning">—</div> |
| </div> |
|
|
| <div class="attempt-reward-row negative" id="attempt-reward-row"> |
| <span class="reward-label-text negative" id="reward-label-text">Reward Signal</span> |
| <span class="reward-number negative" id="reward-number">0.00</span> |
| </div> |
| </div> |
|
|
| <div class="attempt-status learning" id="attempt-status"> |
| 🔄 Updating weights and trying again... |
| </div> |
| </div> |
|
|
| |
| <div class="result-section" id="result-section"> |
| <div class="result-card" id="result-card"> |
| <div class="result-emoji" id="result-emoji">🎉</div> |
| <div class="result-title" id="result-title">—</div> |
| <div class="result-message" id="result-message">—</div> |
| <div class="result-reward positive" id="result-reward">+1.00</div> |
| <div class="result-ideal" id="result-ideal"></div> |
| <div class="result-actions"> |
| <button class="btn btn-primary" id="btn-new-session">🔄 New Session</button> |
| <button class="btn" id="btn-retry-same">🔁 Same Query</button> |
| <button class="btn btn-danger" id="btn-reset-agent">🧹 Reset Agent Brain</button> |
| </div> |
| </div> |
| </div> |
|
|
| |
| <div class="timeline-section" id="timeline-section"> |
| <div class="timeline-title"> |
| 📋 Episode Attempts <span class="count" id="timeline-count">0</span> |
| </div> |
| <div class="timeline" id="timeline"></div> |
| </div> |
|
|
| |
| <div class="reward-summary" id="reward-summary"> |
| <div class="reward-bar-container"> |
| <div class="reward-bar-header"> |
| <span class="reward-bar-label">Episode Reward</span> |
| <span class="reward-bar-value" id="reward-value">0.00</span> |
| </div> |
| <div class="reward-track"> |
| <div class="reward-fill" id="reward-fill" style="width: 0%"></div> |
| </div> |
| </div> |
| </div> |
|
|
| |
| <div class="products-overview" id="products-overview"> |
| <button class="products-toggle" id="products-toggle"> |
| <span>🏪 All Generated Products (<span id="product-total">0</span>)</span> |
| <span class="arrow">▼</span> |
| </button> |
| <div class="products-grid" id="products-grid"></div> |
| </div> |
|
|
| </div> |
| </div> |
|
|
| |
| <button class="profile-toggle-btn" id="profile-toggle-btn" title="View Personality Profile">🧬</button> |
| <div class="personality-float" id="personality-panel"> |
| <div class="pf-title"> |
| 🧬 Your Personality |
| <button class="pf-close" id="pf-close">✕</button> |
| </div> |
| <div id="personality-traits"></div> |
| </div> |
|
|
| |
| <div class="loading-overlay" id="loading"><div class="spinner"></div></div> |
|
|
| |
| <div class="toast-container" id="toast-container"></div> |
|
|
| <script> |
| |
| |
| |
| const API = ''; |
| let appState = { |
| query: '', |
| products: [], |
| scoredProducts: [], |
| attempts: [], |
| episodeReward: 0, |
| idealProduct: null, |
| personalityTraits: {}, |
| productsExpanded: false, |
| agentStats: null, |
| }; |
| |
| |
| |
| |
| async function api(endpoint, method = 'GET', body = null) { |
| const opts = { method, headers: { 'Content-Type': 'application/json' } }; |
| if (body) opts.body = JSON.stringify(body); |
| const res = await fetch(API + endpoint, opts); |
| if (!res.ok) { |
| const err = await res.json().catch(() => ({ error: res.statusText })); |
| throw new Error(err.error || err.detail || res.statusText); |
| } |
| return res.json(); |
| } |
| |
| function showLoading(show) { |
| document.getElementById('loading').classList.toggle('active', show); |
| } |
| |
| function toast(msg, type = 'success') { |
| const c = document.getElementById('toast-container'); |
| const t = document.createElement('div'); |
| t.className = `toast ${type}`; |
| t.textContent = msg; |
| c.appendChild(t); |
| setTimeout(() => t.remove(), 4000); |
| } |
| |
| |
| |
| |
| |
| async function startSession() { |
| const query = document.getElementById('prompt-input').value.trim(); |
| if (!query) { toast('Please enter a product to shop for', 'error'); return; } |
| |
| |
| appState = { |
| query, |
| products: [], |
| scoredProducts: [], |
| attempts: [], |
| episodeReward: 0, |
| idealProduct: null, |
| personalityTraits: {}, |
| productsExpanded: false, |
| agentStats: null, |
| }; |
| |
| showLoading(true); |
| try { |
| |
| const data = await api('/ui/reset', 'POST', { query }); |
| appState.products = data.products || []; |
| appState.scoredProducts = data.scored_products || []; |
| appState.personalityTraits = data.personality_traits || {}; |
| appState.idealProduct = { |
| name: data.ideal_product, |
| score: data.ideal_score, |
| }; |
| |
| |
| document.getElementById('flow-area').classList.add('active'); |
| renderPersonality(); |
| toast(`${data.product_count} products generated for "${query}"!`); |
| |
| showLoading(false); |
| |
| |
| hideAllSections(); |
| showSection('thinking-section'); |
| |
| const autoData = await api('/auto-run', 'POST'); |
| appState.attempts = autoData.attempts || []; |
| appState.episodeReward = autoData.total_episode_reward || 0; |
| appState.agentStats = autoData.agent_stats || {}; |
| |
| |
| hideSection('thinking-section'); |
| |
| |
| await animateAttempts(autoData); |
| |
| } catch (e) { |
| toast(e.message, 'error'); |
| showLoading(false); |
| } |
| } |
| |
| |
| async function animateAttempts(autoData) { |
| const attempts = autoData.attempts || []; |
| if (attempts.length === 0) return; |
| |
| showSection('timeline-section'); |
| showSection('reward-summary'); |
| showSection('products-overview'); |
| |
| let cumulativeReward = 0; |
| |
| for (let i = 0; i < attempts.length; i++) { |
| const a = attempts[i]; |
| cumulativeReward += a.reward; |
| |
| |
| showSection('attempt-section'); |
| renderAttemptCard(a, i === attempts.length - 1); |
| |
| |
| renderTimeline(attempts.slice(0, i + 1)); |
| renderRewardBar(cumulativeReward); |
| renderProductsGrid(a.product_id, attempts.slice(0, i + 1)); |
| updateHeaderMetrics(autoData.agent_stats, i + 1); |
| |
| |
| const delay = a.is_success ? 2000 : 1500; |
| await sleep(delay); |
| |
| if (a.is_success) break; |
| } |
| |
| |
| hideSection('attempt-section'); |
| showFinalResult(autoData); |
| } |
| |
| function renderAttemptCard(attempt, isLast) { |
| const p = attempt.product; |
| const score = attempt.personality_score; |
| const scoreClass = score >= 0.8 ? 'good' : score >= 0.5 ? 'mid' : 'bad'; |
| |
| |
| const badge = document.getElementById('attempt-badge'); |
| if (attempt.is_success) { |
| badge.textContent = '✅ SUCCESS'; |
| badge.className = 'attempt-badge success-badge'; |
| } else if (attempt.strategy === 'exploration') { |
| badge.textContent = '🎲 EXPLORING'; |
| badge.className = 'attempt-badge exploring'; |
| } else { |
| badge.textContent = '🧠 EXPLOITING'; |
| badge.className = 'attempt-badge exploiting'; |
| } |
| |
| |
| const card = document.getElementById('attempt-card'); |
| card.className = `attempt-card ${attempt.is_success ? 'success-card' : attempt.strategy === 'exploration' ? 'learning-card' : 'fail-card'}`; |
| |
| card.style.animation = 'none'; |
| card.offsetHeight; |
| card.style.animation = ''; |
| |
| |
| document.getElementById('attempt-label').textContent = `🤖 Attempt #${attempt.attempt}`; |
| document.getElementById('attempt-name').textContent = p.name; |
| |
| |
| const scoreBadge = document.getElementById('attempt-score-badge'); |
| scoreBadge.className = `attempt-score-badge ${scoreClass}`; |
| const scoreVal = document.getElementById('attempt-score'); |
| scoreVal.textContent = (score * 100).toFixed(0) + '%'; |
| scoreVal.className = `attempt-score-value ${scoreClass}`; |
| |
| |
| document.getElementById('attempt-meta').innerHTML = ` |
| <span class="attempt-tag tag-price">💰 $${p.price.toFixed(2)}</span> |
| <span class="attempt-tag tag-rating">⭐ ${p.rating}</span> |
| <span class="attempt-tag tag-reviews">📊 ${p.reviews.toLocaleString()} reviews</span> |
| <span class="attempt-tag tag-brand">🏷️ ${p.brand}</span> |
| ${p.refundable |
| ? '<span class="attempt-tag tag-refund">✅ Refundable</span>' |
| : '<span class="attempt-tag tag-norefund">⚠️ No Refund</span>'} |
| <span class="attempt-tag tag-seller">🏪 ${p.seller}</span> |
| `; |
| |
| |
| document.getElementById('attempt-features').innerHTML = (p.features || []).map(f => |
| `<span class="feature-chip">${f}</span>` |
| ).join(''); |
| |
| |
| const reasoningParts = (attempt.reasoning || '').split(' | '); |
| document.getElementById('attempt-reasoning').innerHTML = reasoningParts.map(r => { |
| if (r.startsWith('🎲')) return `<span style="color:var(--amber)">${r}</span>`; |
| if (r.startsWith('🧠')) return `<span style="color:var(--accent-light)">${r}</span>`; |
| if (r.startsWith('✅')) return `<span style="color:var(--green)">${r}</span>`; |
| if (r.startsWith('❌')) return `<span style="color:var(--red)">${r}</span>`; |
| return r; |
| }).join('\n'); |
| |
| |
| const rewardRow = document.getElementById('attempt-reward-row'); |
| const rewardLabel = document.getElementById('reward-label-text'); |
| const rewardNum = document.getElementById('reward-number'); |
| const isPositive = attempt.reward >= 0; |
| rewardRow.className = `attempt-reward-row ${isPositive ? 'positive' : 'negative'}`; |
| rewardLabel.className = `reward-label-text ${isPositive ? 'positive' : 'negative'}`; |
| rewardLabel.textContent = isPositive ? '✅ Positive Reward' : '❌ Negative Reward'; |
| rewardNum.className = `reward-number ${isPositive ? 'positive' : 'negative'}`; |
| rewardNum.textContent = (isPositive ? '+' : '') + attempt.reward.toFixed(2); |
| |
| |
| const status = document.getElementById('attempt-status'); |
| if (attempt.is_success) { |
| status.className = 'attempt-status done'; |
| status.textContent = '🎯 Found the ideal product! Agent successfully matched your personality.'; |
| } else if (isLast) { |
| status.className = 'attempt-status learning'; |
| status.textContent = '📝 Ran out of products to try. Agent saved what it learned for next time.'; |
| } else { |
| status.className = 'attempt-status learning'; |
| status.textContent = `🔄 Updating weights (ε=${(attempt.epsilon * 100).toFixed(0)}%) and trying again...`; |
| } |
| } |
| |
| function showFinalResult(autoData) { |
| const success = autoData.success; |
| const attempts = autoData.attempts || []; |
| const totalReward = autoData.total_episode_reward; |
| const stats = autoData.agent_stats || {}; |
| const ideal = autoData.ideal_product; |
| |
| showSection('result-section'); |
| |
| let emoji, title, message; |
| if (success) { |
| const successAttempt = autoData.success_attempt; |
| if (successAttempt === 1) { |
| emoji = '🎉'; title = 'Perfect! First Try!'; |
| message = 'The agent nailed your personality on the very first pick.'; |
| } else if (successAttempt === 2) { |
| emoji = '✅'; title = 'Got It on Second Try!'; |
| message = 'Quick learner — adjusted weights after one mistake.'; |
| } else if (successAttempt <= 4) { |
| emoji = '👍'; title = `Found it on Attempt #${successAttempt}`; |
| message = 'The agent explored, learned, and converged to your ideal pick.'; |
| } else { |
| emoji = '💡'; title = `Found it on Attempt #${successAttempt}`; |
| message = 'Took a few tries but the agent figured out your preferences.'; |
| } |
| } else { |
| emoji = '📝'; title = 'Episode Complete — Still Learning'; |
| message = "The agent didn't find the ideal pick this time, but it updated its weights. It'll do better next episode!"; |
| } |
| |
| document.getElementById('result-emoji').textContent = emoji; |
| document.getElementById('result-title').textContent = title; |
| document.getElementById('result-message').textContent = message; |
| |
| const rewardEl = document.getElementById('result-reward'); |
| rewardEl.textContent = (totalReward >= 0 ? '+' : '') + totalReward.toFixed(2); |
| rewardEl.className = `result-reward ${totalReward >= 0 ? 'positive' : 'negative'}`; |
| |
| |
| if (ideal) { |
| document.getElementById('result-ideal').innerHTML = ` |
| 🎯 <strong>Ideal Product:</strong> ${ideal.name} (personality match: ${(ideal.score * 100).toFixed(0)}%) |
| <br><span style="font-size:11px; color:var(--text-muted);"> |
| Episodes: ${stats.episode_count || 0} · Success Rate: ${((stats.success_rate || 0) * 100).toFixed(0)}% · ε: ${((stats.epsilon || 0) * 100).toFixed(0)}% |
| </span> |
| `; |
| } |
| } |
| |
| |
| |
| |
| |
| function renderTimeline(attempts) { |
| const container = document.getElementById('timeline'); |
| const countEl = document.getElementById('timeline-count'); |
| countEl.textContent = attempts.length; |
| |
| container.innerHTML = attempts.map(a => { |
| const numClass = a.is_success ? 'success' : 'fail'; |
| const rewardClass = a.reward >= 0 ? 'positive' : 'negative'; |
| const rewardSign = a.reward >= 0 ? '+' : ''; |
| const stratClass = a.strategy === 'exploration' ? 'explore' : 'exploit'; |
| const stratIcon = a.strategy === 'exploration' ? '🎲' : '🧠'; |
| return ` |
| <div class="timeline-item"> |
| <div class="timeline-num ${numClass}">${a.attempt}</div> |
| <div class="timeline-body"> |
| <div class="timeline-product">${a.product_name}</div> |
| <div class="timeline-detail"> |
| $${a.product.price.toFixed(2)} · ${a.product.rating}★ · ${a.product.brand} |
| · Match: ${(a.personality_score * 100).toFixed(0)}% |
| </div> |
| <div class="timeline-strategy ${stratClass}">${stratIcon} ${a.strategy} (ε=${(a.epsilon * 100).toFixed(0)}%)</div> |
| </div> |
| <div class="timeline-reward ${rewardClass}"> |
| ${rewardSign}${a.reward.toFixed(2)} |
| </div> |
| </div> |
| `; |
| }).join(''); |
| } |
| |
| function renderProductsGrid(currentPickId, triedAttempts) { |
| const grid = document.getElementById('products-grid'); |
| const total = document.getElementById('product-total'); |
| total.textContent = appState.products.length; |
| |
| const triedIds = new Set((triedAttempts || []).map(a => a.product_id)); |
| const idealName = appState.idealProduct?.name; |
| |
| |
| const scoreMap = {}; |
| for (const s of appState.scoredProducts) { |
| scoreMap[s.id] = s; |
| } |
| |
| grid.innerHTML = appState.products.map(p => { |
| const isCurrent = p.id === currentPickId; |
| const isTried = triedIds.has(p.id) && !isCurrent; |
| const isIdeal = p.name === idealName; |
| const scoreInfo = scoreMap[p.id]; |
| |
| const classes = ['mini-product']; |
| if (isCurrent) classes.push('highlighted'); |
| if (isTried) classes.push('tried'); |
| if (isIdeal && !isCurrent) classes.push('ideal'); |
| |
| let tag = ''; |
| if (isCurrent) tag = '<div class="mp-tag" style="color:var(--green)">👉 Current Pick</div>'; |
| else if (isIdeal) tag = '<div class="mp-tag" style="color:var(--accent-light)">🎯 Ideal</div>'; |
| else if (isTried) tag = '<div class="mp-tag" style="color:var(--red)">❌ Tried</div>'; |
| |
| return ` |
| <div class="${classes.join(' ')}"> |
| <div class="mp-name">${p.name}</div> |
| <div class="mp-meta"> |
| <span>$${p.price.toFixed(2)}</span> |
| <span>${p.rating}★</span> |
| <span>${p.reviews} reviews</span> |
| </div> |
| ${scoreInfo ? `<div class="mp-score">Match: ${(scoreInfo.score * 100).toFixed(0)}% (#${scoreInfo.rank})</div>` : ''} |
| ${tag} |
| </div> |
| `; |
| }).join(''); |
| } |
| |
| function renderRewardBar(cumulativeReward) { |
| document.getElementById('reward-value').textContent = cumulativeReward.toFixed(2); |
| const pct = Math.max(0, Math.min(100, ((cumulativeReward + 1) / 2) * 100)); |
| document.getElementById('reward-fill').style.width = pct + '%'; |
| } |
| |
| function renderPersonality() { |
| const container = document.getElementById('personality-traits'); |
| const traits = appState.personalityTraits; |
| const keys = Object.keys(traits); |
| |
| if (keys.length === 0) { |
| container.innerHTML = '<div style="font-size:12px;color:var(--text-muted);text-align:center;">Start a session to load profile</div>'; |
| return; |
| } |
| |
| const colors = { |
| price_sensitivity: '#6366f1', |
| quality_preference: '#22c55e', |
| risk_aversion: '#ef4444', |
| research_depth: '#8b5cf6', |
| brand_trust: '#f59e0b', |
| exploration_vs_repeat: '#ec4899', |
| review_dependence: '#3b82f6', |
| return_preference: '#14b8a6', |
| decision_speed: '#f97316', |
| discount_sensitivity: '#a855f7', |
| }; |
| |
| container.innerHTML = keys.map(k => { |
| const v = traits[k]; |
| const pct = Math.round(v * 100); |
| const color = colors[k] || '#6366f1'; |
| const label = k.replace(/_/g, ' ').replace(/\b\w/g, c => c.toUpperCase()); |
| return ` |
| <div class="trait-row"> |
| <span class="trait-label">${label}</span> |
| <div class="trait-bar"> |
| <div class="trait-fill" style="width:${pct}%;background:${color};"></div> |
| </div> |
| <span class="trait-val" style="color:${color};">${pct}%</span> |
| </div> |
| `; |
| }).join(''); |
| } |
| |
| function updateHeaderMetrics(stats, attemptCount) { |
| document.getElementById('m-episodes').textContent = stats?.episode_count || 0; |
| document.getElementById('m-success').textContent = ((stats?.success_rate || 0) * 100).toFixed(0) + '%'; |
| document.getElementById('m-reward').textContent = (stats?.total_reward || 0).toFixed(2); |
| } |
| |
| |
| function showSection(id) { |
| const el = document.getElementById(id); |
| if (el) el.classList.add('active'); |
| } |
| function hideSection(id) { |
| const el = document.getElementById(id); |
| if (el) el.classList.remove('active'); |
| } |
| function hideAllSections() { |
| ['thinking-section', 'attempt-section', 'result-section', |
| 'timeline-section', 'reward-summary', 'products-overview'].forEach(hideSection); |
| } |
| |
| function sleep(ms) { |
| return new Promise(r => setTimeout(r, ms)); |
| } |
| |
| |
| |
| |
| |
| |
| document.getElementById('btn-start').addEventListener('click', startSession); |
| document.getElementById('prompt-input').addEventListener('keydown', e => { |
| if (e.key === 'Enter') startSession(); |
| }); |
| |
| |
| document.getElementById('btn-new-session').addEventListener('click', () => { |
| hideAllSections(); |
| document.getElementById('flow-area').classList.remove('active'); |
| document.getElementById('prompt-input').value = ''; |
| document.getElementById('prompt-input').focus(); |
| }); |
| document.getElementById('btn-retry-same').addEventListener('click', () => { |
| document.getElementById('prompt-input').value = appState.query; |
| startSession(); |
| }); |
| document.getElementById('btn-reset-agent').addEventListener('click', async () => { |
| if (!confirm('Reset the agent\'s brain? All learned weights will be lost.')) return; |
| try { |
| await api('/agent-reset', 'POST'); |
| toast('Agent brain reset! Starting from scratch.', 'info'); |
| updateHeaderMetrics({ episode_count: 0, success_rate: 0, total_reward: 0 }, 0); |
| } catch (e) { |
| toast(e.message, 'error'); |
| } |
| }); |
| |
| |
| document.querySelectorAll('.pill').forEach(pill => { |
| pill.addEventListener('click', () => { |
| document.querySelectorAll('.pill').forEach(p => p.classList.remove('active')); |
| pill.classList.add('active'); |
| document.getElementById('prompt-input').value = pill.dataset.cat; |
| }); |
| }); |
| |
| |
| document.getElementById('products-toggle').addEventListener('click', () => { |
| const toggle = document.getElementById('products-toggle'); |
| const grid = document.getElementById('products-grid'); |
| appState.productsExpanded = !appState.productsExpanded; |
| toggle.classList.toggle('open', appState.productsExpanded); |
| grid.classList.toggle('open', appState.productsExpanded); |
| }); |
| |
| |
| document.getElementById('profile-toggle-btn').addEventListener('click', () => { |
| document.getElementById('personality-panel').classList.toggle('active'); |
| }); |
| document.getElementById('pf-close').addEventListener('click', () => { |
| document.getElementById('personality-panel').classList.remove('active'); |
| }); |
| |
| |
| (async () => { |
| try { |
| const stats = await api('/agent-stats'); |
| updateHeaderMetrics(stats, 0); |
| } catch (e) { } |
| })(); |
| |
| </script> |
| </body> |
| </html> |
|
|