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.
371 lines
13 KiB
JavaScript
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 });
|