843 lines
28 KiB
HTML
843 lines
28 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=1920, height=1080">
|
|
<title>Stripe MCP - Autonomous Agent Demo</title>
|
|
|
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
|
|
|
<style>
|
|
:root {
|
|
--bg-primary: #0f0f1a;
|
|
--bg-secondary: #1a1a2e;
|
|
--bg-tertiary: #252542;
|
|
--text-primary: #FFFFFF;
|
|
--text-secondary: #A0A0B8;
|
|
--text-muted: #6B6B80;
|
|
--border-default: rgba(255,255,255,0.1);
|
|
--border-subtle: rgba(255,255,255,0.05);
|
|
--accent-gradient: linear-gradient(135deg, #667eea, #764ba2);
|
|
--stripe-purple: #635BFF;
|
|
--success-green: #30B566;
|
|
}
|
|
|
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
|
|
body {
|
|
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
|
|
background: var(--bg-primary);
|
|
width: 1920px;
|
|
height: 1080px;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.camera {
|
|
width: 1920px;
|
|
height: 1080px;
|
|
position: relative;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.scene {
|
|
position: absolute;
|
|
width: 3200px;
|
|
height: 2200px;
|
|
left: -640px;
|
|
top: -560px;
|
|
background:
|
|
radial-gradient(ellipse at 30% 30%, rgba(99, 91, 255, 0.12) 0%, transparent 50%),
|
|
radial-gradient(ellipse at 70% 70%, rgba(118, 75, 162, 0.12) 0%, transparent 50%),
|
|
var(--bg-primary);
|
|
}
|
|
|
|
.chat-window {
|
|
position: absolute;
|
|
top: 50%;
|
|
left: 50%;
|
|
transform: translate(-50%, -50%);
|
|
width: 1500px;
|
|
height: 950px;
|
|
background: var(--bg-secondary);
|
|
border-radius: 24px;
|
|
box-shadow: 0 30px 100px rgba(0,0,0,0.6);
|
|
display: flex;
|
|
flex-direction: column;
|
|
border: 1px solid var(--border-default);
|
|
overflow: hidden;
|
|
}
|
|
|
|
.chat-header {
|
|
height: 60px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
padding: 0 24px;
|
|
background: linear-gradient(180deg, rgba(255,255,255,0.03) 0%, transparent 100%);
|
|
border-bottom: 1px solid var(--border-subtle);
|
|
}
|
|
|
|
.window-controls { display: flex; gap: 8px; }
|
|
.window-dot { width: 14px; height: 14px; border-radius: 50%; }
|
|
.window-dot-red { background: #ff5f56; }
|
|
.window-dot-yellow { background: #ffbd2e; }
|
|
.window-dot-green { background: #27ca40; }
|
|
|
|
.chat-header-title { font-size: 15px; font-weight: 500; color: var(--text-secondary); }
|
|
|
|
.chat-messages {
|
|
flex: 1;
|
|
padding: 32px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 24px;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.message-user {
|
|
align-self: flex-end;
|
|
max-width: 60%;
|
|
padding: 18px 26px;
|
|
background: var(--accent-gradient);
|
|
color: var(--text-primary);
|
|
border-radius: 22px;
|
|
border-bottom-right-radius: 6px;
|
|
font-size: 16px;
|
|
line-height: 1.5;
|
|
opacity: 0;
|
|
transform: translateX(30px);
|
|
}
|
|
|
|
.message-ai-container {
|
|
display: flex;
|
|
gap: 16px;
|
|
align-items: flex-start;
|
|
opacity: 0;
|
|
transform: translateY(20px);
|
|
}
|
|
|
|
.ai-avatar {
|
|
width: 44px;
|
|
height: 44px;
|
|
border-radius: 50%;
|
|
background: var(--accent-gradient);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.ai-avatar svg { width: 24px; height: 24px; }
|
|
|
|
.ai-content {
|
|
flex: 1;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 16px;
|
|
max-width: 800px;
|
|
}
|
|
|
|
.message-ai-text {
|
|
background: rgba(255,255,255,0.05);
|
|
color: var(--text-primary);
|
|
padding: 18px 26px;
|
|
border-radius: 22px;
|
|
border-bottom-left-radius: 6px;
|
|
font-size: 16px;
|
|
line-height: 1.6;
|
|
}
|
|
|
|
.ai-typing {
|
|
display: flex;
|
|
gap: 6px;
|
|
padding: 18px 26px;
|
|
background: rgba(255,255,255,0.05);
|
|
border-radius: 22px;
|
|
border-bottom-left-radius: 6px;
|
|
width: fit-content;
|
|
}
|
|
|
|
.typing-dot {
|
|
width: 8px;
|
|
height: 8px;
|
|
background: var(--text-muted);
|
|
border-radius: 50%;
|
|
animation: typingBounce 1.4s ease-in-out infinite;
|
|
}
|
|
|
|
.typing-dot:nth-child(2) { animation-delay: 0.2s; }
|
|
.typing-dot:nth-child(3) { animation-delay: 0.4s; }
|
|
|
|
@keyframes typingBounce {
|
|
0%, 60%, 100% { transform: translateY(0); }
|
|
30% { transform: translateY(-8px); }
|
|
}
|
|
|
|
/* Stripe Embed */
|
|
.stripe-embed {
|
|
background: #FFFFFF;
|
|
border-radius: 16px;
|
|
overflow: hidden;
|
|
box-shadow: 0 8px 32px rgba(99, 91, 255, 0.2), 0 0 0 1px rgba(99, 91, 255, 0.15);
|
|
width: 640px;
|
|
opacity: 0;
|
|
transform: scale(0.9) translateY(20px);
|
|
}
|
|
|
|
.stripe-header {
|
|
padding: 16px 20px;
|
|
border-bottom: 1px solid #E3E8EE;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 14px;
|
|
}
|
|
|
|
.stripe-logo {
|
|
background: #635BFF;
|
|
color: white;
|
|
font-weight: 700;
|
|
font-size: 13px;
|
|
padding: 6px 12px;
|
|
border-radius: 6px;
|
|
}
|
|
|
|
.stripe-title { font-size: 15px; font-weight: 600; color: #1A1F36; }
|
|
.stripe-subtitle { font-size: 12px; color: #697386; margin-top: 2px; }
|
|
|
|
.stripe-stat-row {
|
|
display: flex;
|
|
gap: 32px;
|
|
padding: 16px 20px;
|
|
background: #F6F9FC;
|
|
border-bottom: 1px solid #E3E8EE;
|
|
}
|
|
|
|
.stripe-stat { display: flex; flex-direction: column; gap: 4px; }
|
|
.stripe-stat-value { font-size: 22px; font-weight: 600; color: #1A1F36; }
|
|
.stripe-stat-value.warning { color: #D97706; }
|
|
.stripe-stat-label { font-size: 11px; color: #697386; text-transform: uppercase; letter-spacing: 0.5px; }
|
|
|
|
.stripe-table { width: 100%; border-collapse: collapse; font-size: 13px; }
|
|
|
|
.stripe-table th {
|
|
text-align: left;
|
|
padding: 12px 20px;
|
|
font-size: 11px;
|
|
font-weight: 500;
|
|
color: #697386;
|
|
text-transform: uppercase;
|
|
background: #F6F9FC;
|
|
border-bottom: 1px solid #E3E8EE;
|
|
}
|
|
|
|
.stripe-table td {
|
|
padding: 14px 20px;
|
|
border-bottom: 1px solid #E3E8EE;
|
|
color: #1A1F36;
|
|
}
|
|
|
|
.stripe-table tr:last-child td { border-bottom: none; }
|
|
|
|
.stripe-status {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 5px;
|
|
padding: 4px 10px;
|
|
border-radius: 5px;
|
|
font-size: 11px;
|
|
font-weight: 500;
|
|
}
|
|
|
|
.stripe-status.failed { background: #FEE2E2; color: #DC2626; }
|
|
.stripe-status-dot { width: 6px; height: 6px; border-radius: 50%; background: currentColor; }
|
|
|
|
.stripe-customer { display: flex; flex-direction: column; gap: 2px; }
|
|
.stripe-customer-email { color: #1A1F36; font-weight: 500; }
|
|
.stripe-customer-id { font-size: 11px; color: #697386; font-family: monospace; }
|
|
|
|
/* Recommendation */
|
|
.recommendation {
|
|
background: rgba(217, 119, 6, 0.1);
|
|
border: 1px solid rgba(217, 119, 6, 0.3);
|
|
border-radius: 14px;
|
|
padding: 20px;
|
|
opacity: 0;
|
|
transform: translateY(10px);
|
|
}
|
|
|
|
.recommendation-title {
|
|
font-size: 14px;
|
|
font-weight: 600;
|
|
color: #D97706;
|
|
margin-bottom: 10px;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
}
|
|
|
|
.recommendation-text {
|
|
font-size: 15px;
|
|
color: var(--text-primary);
|
|
line-height: 1.6;
|
|
margin-bottom: 16px;
|
|
}
|
|
|
|
/* Yes/No Buttons */
|
|
.action-buttons {
|
|
display: flex;
|
|
gap: 12px;
|
|
opacity: 0;
|
|
transform: translateY(10px);
|
|
}
|
|
|
|
.action-btn {
|
|
padding: 12px 28px;
|
|
border-radius: 10px;
|
|
font-size: 14px;
|
|
font-weight: 600;
|
|
cursor: pointer;
|
|
transition: all 0.2s ease;
|
|
border: none;
|
|
}
|
|
|
|
.action-btn.yes {
|
|
background: var(--success-green);
|
|
color: white;
|
|
}
|
|
|
|
.action-btn.yes.clicked {
|
|
transform: scale(0.95);
|
|
box-shadow: 0 0 0 4px rgba(48, 181, 102, 0.3);
|
|
}
|
|
|
|
.action-btn.no {
|
|
background: var(--bg-tertiary);
|
|
color: var(--text-secondary);
|
|
border: 1px solid var(--border-default);
|
|
}
|
|
|
|
/* Success Notifications */
|
|
.notification {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 12px;
|
|
background: rgba(48, 181, 102, 0.15);
|
|
border: 1px solid rgba(48, 181, 102, 0.4);
|
|
border-radius: 12px;
|
|
padding: 14px 18px;
|
|
opacity: 0;
|
|
transform: translateX(-20px);
|
|
}
|
|
|
|
.notification-icon {
|
|
width: 36px;
|
|
height: 36px;
|
|
border-radius: 50%;
|
|
background: var(--success-green);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.notification-icon svg {
|
|
width: 20px;
|
|
height: 20px;
|
|
fill: white;
|
|
}
|
|
|
|
.notification-content {
|
|
flex: 1;
|
|
}
|
|
|
|
.notification-title {
|
|
font-size: 14px;
|
|
font-weight: 600;
|
|
color: var(--success-green);
|
|
}
|
|
|
|
.notification-text {
|
|
font-size: 13px;
|
|
color: var(--text-secondary);
|
|
margin-top: 2px;
|
|
}
|
|
|
|
/* Input area */
|
|
.chat-input-container {
|
|
padding: 20px 28px 28px;
|
|
border-top: 1px solid var(--border-subtle);
|
|
}
|
|
|
|
.chat-input {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 14px;
|
|
background: var(--bg-primary);
|
|
border: 2px solid var(--border-default);
|
|
border-radius: 18px;
|
|
padding: 14px 18px;
|
|
transition: all 0.3s ease;
|
|
}
|
|
|
|
.chat-input.active {
|
|
border-color: var(--stripe-purple);
|
|
box-shadow: 0 0 0 4px rgba(99, 91, 255, 0.15);
|
|
}
|
|
|
|
.chat-input-text {
|
|
flex: 1;
|
|
color: var(--text-primary);
|
|
font-size: 15px;
|
|
min-height: 22px;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.chat-input-text .cursor {
|
|
display: inline-block;
|
|
width: 2px;
|
|
height: 20px;
|
|
background: var(--stripe-purple);
|
|
margin-left: 2px;
|
|
vertical-align: text-bottom;
|
|
animation: blink 0.8s step-end infinite;
|
|
}
|
|
|
|
.chat-input-text .placeholder {
|
|
color: var(--text-muted);
|
|
}
|
|
|
|
@keyframes blink { 50% { opacity: 0; } }
|
|
|
|
.chat-input-send {
|
|
width: 40px;
|
|
height: 40px;
|
|
border-radius: 12px;
|
|
background: var(--bg-tertiary);
|
|
border: none;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
transition: all 0.3s ease;
|
|
}
|
|
|
|
.chat-input-send.ready {
|
|
background: var(--accent-gradient);
|
|
transform: scale(1.05);
|
|
}
|
|
|
|
.chat-input-send svg { width: 20px; height: 20px; fill: white; }
|
|
|
|
.embed-loading {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
padding: 40px;
|
|
background: rgba(255,255,255,0.03);
|
|
border-radius: 16px;
|
|
border: 1px dashed var(--border-default);
|
|
}
|
|
|
|
.embed-spinner {
|
|
width: 32px;
|
|
height: 32px;
|
|
border: 3px solid var(--border-default);
|
|
border-top-color: var(--stripe-purple);
|
|
border-radius: 50%;
|
|
animation: spin 0.8s linear infinite;
|
|
}
|
|
|
|
@keyframes spin { to { transform: rotate(360deg); } }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
|
|
<div class="camera">
|
|
<div class="scene" id="scene">
|
|
|
|
<div class="chat-window">
|
|
|
|
<div class="chat-header">
|
|
<div class="window-controls">
|
|
<span class="window-dot window-dot-red"></span>
|
|
<span class="window-dot window-dot-yellow"></span>
|
|
<span class="window-dot window-dot-green"></span>
|
|
</div>
|
|
<span class="chat-header-title">AI Assistant</span>
|
|
<div style="width: 50px;"></div>
|
|
</div>
|
|
|
|
<div class="chat-messages">
|
|
|
|
<div class="message-user" id="userMessage">
|
|
How can we recover some failed payments?
|
|
</div>
|
|
|
|
<div class="message-ai-container" id="aiContainer">
|
|
<div class="ai-avatar">
|
|
<svg viewBox="0 0 24 24" fill="white">
|
|
<path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5"/>
|
|
</svg>
|
|
</div>
|
|
|
|
<div class="ai-content">
|
|
<div class="ai-typing" id="aiTyping">
|
|
<span class="typing-dot"></span>
|
|
<span class="typing-dot"></span>
|
|
<span class="typing-dot"></span>
|
|
</div>
|
|
|
|
<div class="message-ai-text" id="aiText" style="display: none;">
|
|
Found 2 failed payments. Let me analyze the best recovery strategy...
|
|
</div>
|
|
|
|
<div class="embed-loading" id="embedLoading" style="display: none;">
|
|
<div class="embed-spinner"></div>
|
|
</div>
|
|
|
|
<div class="stripe-embed" id="stripeEmbed">
|
|
<div class="stripe-header">
|
|
<span class="stripe-logo">stripe</span>
|
|
<div>
|
|
<div class="stripe-title">Failed Payments</div>
|
|
<div class="stripe-subtitle">2 customers with declined cards</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="stripe-stat-row">
|
|
<div class="stripe-stat">
|
|
<span class="stripe-stat-value warning">$948</span>
|
|
<span class="stripe-stat-label">At Risk</span>
|
|
</div>
|
|
<div class="stripe-stat">
|
|
<span class="stripe-stat-value">2</span>
|
|
<span class="stripe-stat-label">Customers</span>
|
|
</div>
|
|
<div class="stripe-stat">
|
|
<span class="stripe-stat-value">14 days</span>
|
|
<span class="stripe-stat-label">Avg Overdue</span>
|
|
</div>
|
|
</div>
|
|
|
|
<table class="stripe-table">
|
|
<thead>
|
|
<tr>
|
|
<th>Customer</th>
|
|
<th>Amount</th>
|
|
<th>Status</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr>
|
|
<td>
|
|
<div class="stripe-customer">
|
|
<span class="stripe-customer-email">john@example.com</span>
|
|
<span class="stripe-customer-id">cus_Nk4x8J</span>
|
|
</div>
|
|
</td>
|
|
<td><strong>$449.00</strong></td>
|
|
<td>
|
|
<span class="stripe-status failed">
|
|
<span class="stripe-status-dot"></span>
|
|
Card declined
|
|
</span>
|
|
</td>
|
|
</tr>
|
|
<tr>
|
|
<td>
|
|
<div class="stripe-customer">
|
|
<span class="stripe-customer-email">mike@startup.io</span>
|
|
<span class="stripe-customer-id">cus_Qj3p7K</span>
|
|
</div>
|
|
</td>
|
|
<td><strong>$499.00</strong></td>
|
|
<td>
|
|
<span class="stripe-status failed">
|
|
<span class="stripe-status-dot"></span>
|
|
Card declined
|
|
</span>
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<div class="recommendation" id="recommendation">
|
|
<div class="recommendation-title">
|
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor">
|
|
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-1 15v-2h2v2h-1zm0-4V7h2v6h-2z"/>
|
|
</svg>
|
|
Smart Recovery Strategy
|
|
</div>
|
|
<div class="recommendation-text">
|
|
Based on payment history, I recommend offering a <strong>payment plan</strong> — collect 50% now, remainder in 1 week. This approach has a 92% success rate with overdue accounts.
|
|
</div>
|
|
<div class="action-buttons" id="actionButtons">
|
|
<button class="action-btn yes" id="yesBtn">Yes, reach out</button>
|
|
<button class="action-btn no">No thanks</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="notification" id="notification1">
|
|
<div class="notification-icon">
|
|
<svg viewBox="0 0 24 24"><path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z"/></svg>
|
|
</div>
|
|
<div class="notification-content">
|
|
<div class="notification-title">Payment Received</div>
|
|
<div class="notification-text">john@example.com paid <strong>$225</strong> — remaining $224 due in 1 week</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="notification" id="notification2">
|
|
<div class="notification-icon">
|
|
<svg viewBox="0 0 24 24"><path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z"/></svg>
|
|
</div>
|
|
<div class="notification-content">
|
|
<div class="notification-title">Payment Received</div>
|
|
<div class="notification-text">mike@startup.io paid <strong>$250</strong> — remaining $249 due in 1 week</div>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<div class="chat-input-container">
|
|
<div class="chat-input" id="chatInput">
|
|
<div class="chat-input-text" id="inputText"><span class="placeholder">Type a message...</span></div>
|
|
<button class="chat-input-send" id="sendBtn">
|
|
<svg viewBox="0 0 24 24">
|
|
<path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z"/>
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
const userQuestion = "How can we recover some failed payments?";
|
|
|
|
function easeOutCubic(t) { return 1 - Math.pow(1 - t, 3); }
|
|
function easeInOutCubic(t) { return t < 0.5 ? 4*t*t*t : 1 - Math.pow(-2*t + 2, 3) / 2; }
|
|
function easeOutBack(t) { const c1 = 1.70158; const c3 = c1 + 1; return 1 + c3 * Math.pow(t - 1, 3) + c1 * Math.pow(t - 1, 2); }
|
|
|
|
function getPhaseProgress(scroll, start, end) {
|
|
if (scroll < start) return 0;
|
|
if (scroll > end) return 1;
|
|
return (scroll - start) / (end - start);
|
|
}
|
|
|
|
// Camera positions
|
|
const CAMERA = {
|
|
inputLeft: { scale: 2.2, x: -320, y: 340, rotation: 0 }, // Zoomed on LEFT of input
|
|
inputRight: { scale: 2.2, x: 200, y: 340, rotation: 0 }, // Follow text to right
|
|
userMsg: { scale: 1.8, x: 320, y: -140, rotation: 0 }, // User message top-right
|
|
aiResponse: { scale: 1.5, x: -180, y: -20, rotation: 0 }, // AI area left
|
|
embed: { scale: 1.2, x: -120, y: 80, rotation: 0 }, // Embed view
|
|
recommendation: { scale: 1.3, x: -100, y: 200, rotation: 0 }, // Recommendation
|
|
notifications: { scale: 1.2, x: -80, y: 280, rotation: 0 }, // Notifications
|
|
final: { scale: 1, x: 0, y: 100, rotation: 0 } // Final wide
|
|
};
|
|
|
|
function updateScene(scrollProgress) {
|
|
const scene = document.getElementById('scene');
|
|
const inputText = document.getElementById('inputText');
|
|
const chatInput = document.getElementById('chatInput');
|
|
const sendBtn = document.getElementById('sendBtn');
|
|
const userMessage = document.getElementById('userMessage');
|
|
const aiContainer = document.getElementById('aiContainer');
|
|
const aiTyping = document.getElementById('aiTyping');
|
|
const aiText = document.getElementById('aiText');
|
|
const embedLoading = document.getElementById('embedLoading');
|
|
const stripeEmbed = document.getElementById('stripeEmbed');
|
|
const recommendation = document.getElementById('recommendation');
|
|
const actionButtons = document.getElementById('actionButtons');
|
|
const yesBtn = document.getElementById('yesBtn');
|
|
const notification1 = document.getElementById('notification1');
|
|
const notification2 = document.getElementById('notification2');
|
|
|
|
// TIMELINE (slowed down Stripe section):
|
|
// 0-5%: Fade in
|
|
// 5-25%: Type in input (camera follows left to right)
|
|
// 25-30%: Ready to send
|
|
// 30-40%: Pan to user message (top right) with see-saw
|
|
// 40-48%: Pan to AI, show typing
|
|
// 48-55%: AI text + loading spinner
|
|
// 55-68%: Stripe embed loads (SLOWER)
|
|
// 68-78%: Recommendation appears (SLOWER)
|
|
// 78-82%: Yes button clicked
|
|
// 82-90%: First notification
|
|
// 90-97%: Second notification
|
|
// 97-100%: Final wide shot
|
|
|
|
// ========== FADE IN ==========
|
|
const fadeIn = easeOutCubic(getPhaseProgress(scrollProgress, 0, 0.05));
|
|
|
|
// ========== TYPING (5-25%) ==========
|
|
const typeProgress = getPhaseProgress(scrollProgress, 0.05, 0.25);
|
|
const charsToShow = Math.floor(easeOutCubic(typeProgress) * userQuestion.length);
|
|
|
|
if (scrollProgress < 0.05) {
|
|
inputText.innerHTML = '<span class="placeholder">Type a message...</span>';
|
|
chatInput.classList.remove('active');
|
|
} else if (scrollProgress < 0.25) {
|
|
chatInput.classList.add('active');
|
|
inputText.innerHTML = (charsToShow > 0 ? userQuestion.substring(0, charsToShow) : '') + '<span class="cursor"></span>';
|
|
sendBtn.classList.toggle('ready', charsToShow === userQuestion.length);
|
|
} else if (scrollProgress < 0.30) {
|
|
inputText.innerHTML = userQuestion + '<span class="cursor"></span>';
|
|
chatInput.classList.add('active');
|
|
sendBtn.classList.add('ready');
|
|
} else {
|
|
inputText.innerHTML = '<span class="placeholder">Type a message...</span>';
|
|
chatInput.classList.remove('active');
|
|
sendBtn.classList.remove('ready');
|
|
}
|
|
|
|
// ========== USER MESSAGE (30-40%) ==========
|
|
const userMsgProgress = getPhaseProgress(scrollProgress, 0.30, 0.40);
|
|
if (scrollProgress >= 0.30) {
|
|
const appear = easeOutBack(Math.min(userMsgProgress * 2, 1));
|
|
userMessage.style.opacity = appear;
|
|
userMessage.style.transform = `translateX(${30 * (1 - appear)}px)`;
|
|
} else {
|
|
userMessage.style.opacity = 0;
|
|
userMessage.style.transform = 'translateX(30px)';
|
|
}
|
|
|
|
// ========== AI TYPING (40-48%) ==========
|
|
if (scrollProgress >= 0.40 && scrollProgress < 0.48) {
|
|
aiContainer.style.opacity = 1;
|
|
aiContainer.style.transform = 'translateY(0)';
|
|
aiTyping.style.display = 'flex';
|
|
aiText.style.display = 'none';
|
|
embedLoading.style.display = 'none';
|
|
stripeEmbed.style.opacity = 0;
|
|
} else if (scrollProgress < 0.40) {
|
|
aiContainer.style.opacity = 0;
|
|
aiContainer.style.transform = 'translateY(20px)';
|
|
}
|
|
|
|
// ========== AI TEXT + LOADING (48-55%) ==========
|
|
if (scrollProgress >= 0.48 && scrollProgress < 0.55) {
|
|
aiTyping.style.display = 'none';
|
|
aiText.style.display = 'block';
|
|
embedLoading.style.display = 'flex';
|
|
stripeEmbed.style.opacity = 0;
|
|
}
|
|
|
|
// ========== STRIPE EMBED (55-68%) ==========
|
|
const embedProgress = getPhaseProgress(scrollProgress, 0.55, 0.68);
|
|
if (scrollProgress >= 0.55) {
|
|
embedLoading.style.display = 'none';
|
|
stripeEmbed.style.opacity = easeOutCubic(embedProgress);
|
|
stripeEmbed.style.transform = `scale(${0.9 + 0.1 * easeOutCubic(embedProgress)}) translateY(${20 * (1 - easeOutCubic(embedProgress))}px)`;
|
|
}
|
|
|
|
// ========== RECOMMENDATION (68-78%) ==========
|
|
const recoProgress = getPhaseProgress(scrollProgress, 0.68, 0.78);
|
|
if (scrollProgress >= 0.68) {
|
|
recommendation.style.opacity = easeOutCubic(recoProgress);
|
|
recommendation.style.transform = `translateY(${10 * (1 - easeOutCubic(recoProgress))}px)`;
|
|
|
|
const btnProgress = getPhaseProgress(scrollProgress, 0.72, 0.78);
|
|
actionButtons.style.opacity = easeOutCubic(btnProgress);
|
|
actionButtons.style.transform = `translateY(${10 * (1 - easeOutCubic(btnProgress))}px)`;
|
|
} else {
|
|
recommendation.style.opacity = 0;
|
|
recommendation.style.transform = 'translateY(10px)';
|
|
actionButtons.style.opacity = 0;
|
|
}
|
|
|
|
// ========== YES CLICKED (78-82%) ==========
|
|
if (scrollProgress >= 0.78) {
|
|
yesBtn.classList.add('clicked');
|
|
} else {
|
|
yesBtn.classList.remove('clicked');
|
|
}
|
|
|
|
// ========== NOTIFICATION 1 (82-90%) ==========
|
|
const notif1Progress = getPhaseProgress(scrollProgress, 0.82, 0.88);
|
|
if (scrollProgress >= 0.82) {
|
|
notification1.style.opacity = easeOutCubic(notif1Progress);
|
|
notification1.style.transform = `translateX(${-20 * (1 - easeOutCubic(notif1Progress))}px)`;
|
|
} else {
|
|
notification1.style.opacity = 0;
|
|
notification1.style.transform = 'translateX(-20px)';
|
|
}
|
|
|
|
// ========== NOTIFICATION 2 (90-97%) ==========
|
|
const notif2Progress = getPhaseProgress(scrollProgress, 0.90, 0.95);
|
|
if (scrollProgress >= 0.90) {
|
|
notification2.style.opacity = easeOutCubic(notif2Progress);
|
|
notification2.style.transform = `translateX(${-20 * (1 - easeOutCubic(notif2Progress))}px)`;
|
|
} else {
|
|
notification2.style.opacity = 0;
|
|
notification2.style.transform = 'translateX(-20px)';
|
|
}
|
|
|
|
// ========== CAMERA ==========
|
|
let cam = { scale: 1, x: 0, y: 0, rotation: 0 };
|
|
|
|
function lerpCam(from, to, t) {
|
|
return {
|
|
scale: from.scale + (to.scale - from.scale) * t,
|
|
x: from.x + (to.x - from.x) * t,
|
|
y: from.y + (to.y - from.y) * t,
|
|
rotation: from.rotation + (to.rotation - from.rotation) * t
|
|
};
|
|
}
|
|
|
|
if (scrollProgress < 0.05) {
|
|
cam = CAMERA.inputLeft;
|
|
} else if (scrollProgress < 0.25) {
|
|
// Follow typing left to right
|
|
const p = easeOutCubic(getPhaseProgress(scrollProgress, 0.05, 0.25));
|
|
cam = lerpCam(CAMERA.inputLeft, CAMERA.inputRight, p);
|
|
} else if (scrollProgress < 0.30) {
|
|
cam = CAMERA.inputRight;
|
|
} else if (scrollProgress < 0.40) {
|
|
// Pan to user message with see-saw
|
|
const p = easeInOutCubic(getPhaseProgress(scrollProgress, 0.30, 0.40));
|
|
cam = lerpCam(CAMERA.inputRight, CAMERA.userMsg, p);
|
|
const seesawT = getPhaseProgress(scrollProgress, 0.30, 0.40);
|
|
cam.rotation = Math.sin(seesawT * Math.PI * 4) * 2 * (1 - seesawT);
|
|
} else if (scrollProgress < 0.48) {
|
|
// Pan to AI
|
|
const p = easeInOutCubic(getPhaseProgress(scrollProgress, 0.40, 0.48));
|
|
cam = lerpCam(CAMERA.userMsg, CAMERA.aiResponse, p);
|
|
} else if (scrollProgress < 0.55) {
|
|
cam = CAMERA.aiResponse;
|
|
} else if (scrollProgress < 0.68) {
|
|
// Move to embed
|
|
const p = easeInOutCubic(getPhaseProgress(scrollProgress, 0.55, 0.68));
|
|
cam = lerpCam(CAMERA.aiResponse, CAMERA.embed, p);
|
|
} else if (scrollProgress < 0.78) {
|
|
// Move to recommendation
|
|
const p = easeInOutCubic(getPhaseProgress(scrollProgress, 0.68, 0.78));
|
|
cam = lerpCam(CAMERA.embed, CAMERA.recommendation, p);
|
|
} else if (scrollProgress < 0.90) {
|
|
// Move to notifications
|
|
const p = easeInOutCubic(getPhaseProgress(scrollProgress, 0.78, 0.90));
|
|
cam = lerpCam(CAMERA.recommendation, CAMERA.notifications, p);
|
|
} else {
|
|
// Final wide
|
|
const p = easeInOutCubic(getPhaseProgress(scrollProgress, 0.90, 1.0));
|
|
cam = lerpCam(CAMERA.notifications, CAMERA.final, p);
|
|
}
|
|
|
|
scene.style.transform = `scale(${cam.scale}) translate(${-cam.x}px, ${-cam.y}px) rotate(${cam.rotation}deg)`;
|
|
scene.style.opacity = fadeIn;
|
|
}
|
|
|
|
window.updateScene = updateScene;
|
|
updateScene(0);
|
|
</script>
|
|
|
|
</body>
|
|
</html>
|