Files
vibn-agent-runner/new-site/hero.jsx
mawkone c51c3c21b3 fix(ai): strip deepseek xml tags from chat history & secure git tools
This commit addresses the issue where DeepSeek's raw XML markup (like <tool_calls> and <think>) was leaking into chat history, causing hallucinations in subsequent turns. It also patches a vulnerability in the git commit tool where arbitrary shell injection was possible.

Additionally, it includes UX copy and color contrast adjustments for the marketing homepage breadcrumbs.
2026-05-14 11:34:42 -07:00

371 lines
13 KiB
JavaScript

// Hero: the Reddit quote headline + prompt input.
// Visitors can type into the prompt; cycling placeholders, suggestion chips, submit handler logs to console.
const HERO_PLACEHOLDERS = [
"A booking site for my dog grooming business…",
"An invoice tracker for my freelance clients…",
"A members-only recipe site for my supper club…",
"A custom CRM for our 3-person real estate team…",
"A tip calculator app for our restaurant staff…",
"A waitlist site for my new ceramics studio…",
];
const HERO_CHIPS = [
"📋 Client intake form",
"📅 Booking site",
"🧾 Invoice tracker",
"🛒 Online store",
"📰 Email newsletter",
];
function Hero({ onStart, variant = "quote" }) {
const [text, setText] = React.useState("");
const [phIdx, setPhIdx] = React.useState(0);
const [phChars, setPhChars] = React.useState(0);
const [deleting, setDeleting] = React.useState(false);
const taRef = React.useRef(null);
// Type-on placeholder when textarea is empty.
React.useEffect(() => {
if (text.length > 0) return undefined;
const full = HERO_PLACEHOLDERS[phIdx];
const speed = deleting ? 18 : 38;
const t = setTimeout(() => {
if (!deleting) {
if (phChars < full.length) setPhChars(phChars + 1);
else setTimeout(() => setDeleting(true), 1700);
} else {
if (phChars > 0) setPhChars(phChars - 1);
else {
setDeleting(false);
setPhIdx((phIdx + 1) % HERO_PLACEHOLDERS.length);
}
}
}, speed);
return () => clearTimeout(t);
}, [text, phIdx, phChars, deleting]);
const placeholder = HERO_PLACEHOLDERS[phIdx].slice(0, phChars);
const submit = () => {
const value = text || HERO_PLACEHOLDERS[phIdx];
if (onStart) onStart(value);
};
const useChip = (chip) => {
const clean = chip.replace(/^[^\w]+/, "").trim();
setText(`Build me ${clean.toLowerCase()} for my business.`);
if (taRef.current) taRef.current.focus();
};
return (
<header className="section hero">
<style>{`
.hero {
padding-top: clamp(60px, 9vh, 120px);
padding-bottom: clamp(60px, 10vh, 120px);
position: relative;
overflow: hidden;
}
.hero-inner {
position: relative;
display: flex; flex-direction: column; align-items: center;
text-align: center;
gap: 28px;
}
.hero-quote {
font-size: clamp(44px, 7.4vw, 104px);
font-weight: 500;
letter-spacing: -0.035em;
line-height: 0.98;
text-wrap: balance;
position: relative;
color: var(--fg);
}
.hero-quote .mark {
color: var(--accent);
font-family: "Geist", serif;
font-weight: 500;
line-height: 0;
vertical-align: -0.05em;
margin-inline: -0.08em;
text-shadow: 0 0 30px var(--accent-glow);
}
.hero-attribution {
font-family: var(--font-mono);
font-size: 12px;
color: var(--fg-faint);
letter-spacing: 0.04em;
margin-top: 6px;
display: inline-flex; align-items: center; gap: 8px;
}
.hero-attribution::before, .hero-attribution::after {
content: ""; width: 24px; height: 1px;
background: var(--hairline);
}
.hero-sub {
font-size: clamp(20px, 2.2vw, 28px);
color: var(--fg-dim);
letter-spacing: -0.01em;
text-wrap: balance;
max-width: 720px;
}
.hero-sub b {
color: var(--fg);
font-weight: 500;
}
/* Prompt input */
.prompt {
width: 100%;
max-width: 720px;
margin-top: 14px;
position: relative;
}
.prompt-frame {
position: relative;
border-radius: var(--r-xl);
padding: 1px;
background: linear-gradient(180deg,
oklch(0.50 0.06 35 / 0.6),
oklch(0.30 0.012 60 / 0.4) 40%,
oklch(0.25 0.012 60 / 0.4));
box-shadow:
0 30px 80px -20px oklch(0 0 0 / 0.6),
0 0 80px -20px var(--accent-glow);
}
.prompt-inner {
background: linear-gradient(180deg, oklch(0.19 0.009 60 / 0.92), oklch(0.17 0.008 60 / 0.92));
border-radius: calc(var(--r-xl) - 1px);
padding: 18px 18px 14px;
backdrop-filter: blur(20px);
}
.prompt textarea {
width: 100%;
min-height: 96px;
background: transparent;
border: 0;
color: var(--fg);
font: 17px/1.45 var(--font-sans);
resize: none;
outline: none;
padding: 6px 4px;
}
.prompt textarea::placeholder {
color: var(--fg-faint);
}
.prompt-typed {
/* simulated placeholder w/ blinking caret */
position: absolute;
top: 24px; left: 22px; right: 22px;
pointer-events: none;
color: var(--fg-faint);
font: 17px/1.45 var(--font-sans);
text-align: left;
}
.prompt-typed::after {
content: "";
display: inline-block;
width: 8px; height: 18px;
background: var(--accent);
vertical-align: -3px;
margin-left: 2px;
animation: blink 1s steps(2) infinite;
box-shadow: 0 0 12px var(--accent-glow);
}
@keyframes blink { 50% { opacity: 0; } }
.prompt-bar {
display: flex; align-items: center; justify-content: space-between;
gap: 14px;
margin-top: 6px;
padding-top: 12px;
border-top: 1px solid var(--hairline);
}
.prompt-tools {
display: flex; gap: 6px; color: var(--fg-mute);
}
.prompt-tool {
display: inline-flex; align-items: center; gap: 6px;
padding: 6px 10px; border-radius: 999px;
font-size: 12px;
color: var(--fg-mute);
border: 1px solid transparent;
transition: border-color .15s, color .15s;
}
.prompt-tool:hover { color: var(--fg-dim); border-color: var(--hairline); }
.prompt-send {
display: inline-flex; align-items: center; gap: 8px;
height: 36px; padding: 0 14px 0 16px;
border-radius: 999px;
background: var(--accent);
color: var(--accent-fg);
font-weight: 500; font-size: 14px;
box-shadow: 0 0 0 1px oklch(0.84 0.16 35 / 0.5) inset, 0 8px 28px -8px var(--accent-glow);
transition: transform .12s;
}
.prompt-send:hover { transform: translateY(-1px); }
.chips {
display: flex; flex-wrap: wrap; gap: 8px; justify-content: center;
margin-top: 12px;
font-size: 13px;
}
.chip {
padding: 7px 14px;
border-radius: 999px;
border: 1px solid var(--hairline);
background: oklch(0.20 0.009 60 / 0.4);
color: var(--fg-dim);
font-family: var(--font-sans);
transition: border-color .15s, color .15s, transform .12s;
}
.chip:hover { border-color: var(--hairline-2); color: var(--fg); transform: translateY(-1px); }
.hero-cta {
display: flex; gap: 12px; align-items: center;
margin-top: 10px;
flex-wrap: wrap; justify-content: center;
}
.live-pill {
display: inline-flex; align-items: center; gap: 8px;
padding: 6px 12px; border-radius: 999px;
background: oklch(0.78 0.16 155 / 0.10);
border: 1px solid oklch(0.78 0.16 155 / 0.35);
color: oklch(0.85 0.14 155);
font-family: var(--font-mono);
font-size: 11px; letter-spacing: 0.08em; text-transform: uppercase;
}
.live-pill .dot {
width: 6px; height: 6px; border-radius: 50%;
background: oklch(0.78 0.16 155);
box-shadow: 0 0 0 0 oklch(0.78 0.16 155 / 0.6);
animation: pulse 2s ease-out infinite;
}
@keyframes pulse {
0% { box-shadow: 0 0 0 0 oklch(0.78 0.16 155 / 0.6); }
70% { box-shadow: 0 0 0 8px oklch(0.78 0.16 155 / 0); }
100% { box-shadow: 0 0 0 0 oklch(0.78 0.16 155 / 0); }
}
@media (max-width: 760px) {
.prompt textarea { min-height: 80px; }
.prompt-typed { top: 22px; left: 20px; right: 20px; font-size: 15px; }
.prompt textarea { font-size: 15px; }
.prompt-tools { display: none; }
}
`}</style>
{/* ambient glows behind hero */}
<Glow color="oklch(0.74 0.175 35 / 0.30)" size={900}
style={{ top: "-200px", left: "50%", transform: "translateX(-50%)" }} />
<Glow color="oklch(0.45 0.10 35 / 0.30)" size={600}
style={{ top: "20%", left: "-200px" }} />
<Glow color="oklch(0.45 0.10 35 / 0.20)" size={500}
style={{ top: "30%", right: "-150px" }} />
<div className="wrap hero-inner">
<span className="live-pill"><span className="dot" /> Live from minute one</span>
{variant === "promise" ? (
<>
<h1 className="hero-quote">
Keep <span className="mark">vibing</span>.
<br/>All the way to launch.
</h1>
<div className="hero-attribution mono">idea live marketed customers</div>
<p className="hero-sub">
<b>"I built my product, now what?"</b> Vibn is the answer.
<br/>Your AI handles the technical stuff, puts your idea online, and helps you find your first customers.
</p>
</>
) : (
<>
<h1 className="hero-quote">
<span className="mark" style={{ fontSize: "0.95em" }}>"</span>I built my product,
<br/>now what<span className="mark" style={{ fontSize: "0.95em" }}>?"</span>
</h1>
<div className="hero-attribution mono">posted 2 hours ago · r/SideProject</div>
<p className="hero-sub">
<b>Keep vibing.</b> All the way to launch.
<br/>Your AI handles the technical stuff, puts your idea online, and helps you find your first customers.
</p>
</>
)}
{/* Prompt */}
<div className="prompt">
<div className="prompt-frame">
<div className="prompt-inner">
<div style={{ position: "relative" }}>
<textarea
ref={taRef}
value={text}
onChange={(e) => setText(e.target.value)}
onKeyDown={(e) => { if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) submit(); }}
placeholder=""
aria-label="Describe what you want to build"
/>
{text.length === 0 && (
<div className="prompt-typed">{placeholder}</div>
)}
</div>
<div className="prompt-bar">
<div className="prompt-tools">
<button className="prompt-tool" type="button" title="Attach a screenshot">
<PromptIcon name="paperclip" /> Screenshot
</button>
<button className="prompt-tool" type="button" title="Voice prompt">
<PromptIcon name="mic" /> Voice
</button>
<button className="prompt-tool" type="button" title="Start from a template">
<PromptIcon name="grid" /> Templates
</button>
</div>
<button className="prompt-send" type="button" onClick={submit}>
Start building <Arrow size={13} />
</button>
</div>
</div>
</div>
{/* Suggestion chips */}
<div className="chips">
{HERO_CHIPS.map((c) => (
<button className="chip" type="button" key={c} onClick={() => useChip(c)}>{c}</button>
))}
</div>
</div>
<div className="hero-cta">
<button className="btn btn-primary" type="button" onClick={submit}>
Start building free <Arrow />
</button>
<a href="#how" className="btn btn-ghost">See how it works</a>
</div>
<TrustStrip items={["No credit card", "No homework", "No new tools to learn"]} />
</div>
</header>
);
}
function PromptIcon({ name }) {
const props = { width: 13, height: 13, viewBox: "0 0 16 16", fill: "none",
stroke: "currentColor", strokeWidth: 1.5, strokeLinecap: "round", strokeLinejoin: "round" };
if (name === "paperclip") return (
<svg {...props}><path d="M11.5 6.5 6.6 11.4a2 2 0 1 1-2.8-2.8l5.4-5.4a3.5 3.5 0 1 1 5 5L8.6 13.7" /></svg>
);
if (name === "mic") return (
<svg {...props}><rect x="6" y="2" width="4" height="8" rx="2"/><path d="M3.5 8a4.5 4.5 0 0 0 9 0M8 13v2"/></svg>
);
if (name === "grid") return (
<svg {...props}><rect x="2.5" y="2.5" width="4.5" height="4.5"/><rect x="9" y="2.5" width="4.5" height="4.5"/><rect x="2.5" y="9" width="4.5" height="4.5"/><rect x="9" y="9" width="4.5" height="4.5"/></svg>
);
return null;
}
Object.assign(window, { Hero });