A small kawaii character on the left, a floating frosted-glass chat bubble on the right. Drop it onto any page. Comes with a built-in scripted reply set so it works without a backend. Wire it to any chat endpoint to upgrade to live conversation.
↑ type anything — she answers with a curious question, never an answer. (this demo runs scripted, no LLM)
Personal sites, portfolios, onboarding flows, product landing pages, "about" pages, 404s — anywhere a character feels warmer than a search bar or contact form. Works as a one-off greeting widget or wired up to a real chat backend.
Paste all three blocks into your page. No build step, no npm. Works in scripted mode by default. To upgrade to a live LLM, set window.CC_ENDPOINT to your chat endpoint URL (see JS tab).
<div class="curious-companion">
<!-- ACTION LAYER: small character (kawaii white kitten) -->
<div class="cc-actor">
<svg viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg">
<g class="cc-walker">
<g transform="translate(100, 110)">
<ellipse cx="0" cy="38" rx="48" ry="38" fill="#fdf6e3" stroke="#1a1a1a" stroke-width="2.5"/>
<ellipse cx="-12" cy="74" rx="12" ry="6" fill="#fdf6e3" stroke="#1a1a1a" stroke-width="2"/>
<ellipse cx="12" cy="74" rx="12" ry="6" fill="#fdf6e3" stroke="#1a1a1a" stroke-width="2"/>
<path d="M-30 -34 q-8 -20 -2 -30 q14 4 18 18 Z" fill="#fdf6e3" stroke="#1a1a1a" stroke-width="2.5"/>
<path d="M30 -34 q8 -20 2 -30 q-14 4 -18 18 Z" fill="#fdf6e3" stroke="#1a1a1a" stroke-width="2.5"/>
<path d="M-25 -30 q-4 -12 0 -19 q7 3 10 12 Z" fill="#ffb6c1"/>
<path d="M25 -30 q4 -12 0 -19 q-7 3 -10 12 Z" fill="#ffb6c1"/>
<circle cx="0" cy="-8" r="42" fill="#fdf6e3" stroke="#1a1a1a" stroke-width="2.5"/>
<ellipse cx="-13" cy="-8" rx="4.5" ry="6" fill="#1a1a1a"/>
<ellipse cx="13" cy="-8" rx="4.5" ry="6" fill="#1a1a1a"/>
<ellipse cx="-23" cy="4" rx="7" ry="4" fill="#ffb6c1" opacity=".8"/>
<ellipse cx="23" cy="4" rx="7" ry="4" fill="#ffb6c1" opacity=".8"/>
<path d="M-3 2 l6 0 l-3 4 Z" fill="#ff8aa8" stroke="#1a1a1a" stroke-width="1"/>
<path d="M0 6 q-3.5 4 -6 1" stroke="#1a1a1a" stroke-width="1.5" fill="none"/>
<path d="M0 6 q3.5 4 6 1" stroke="#1a1a1a" stroke-width="1.5" fill="none"/>
</g>
</g>
</svg>
</div>
<!-- COGNITION LAYER: floating bubble -->
<aside class="cc-bubble">
<span class="cc-tail"></span>
<div class="cc-label">curious companion</div>
<div class="cc-stream" id="ccStream"></div>
<div class="cc-input-row">
<input class="cc-input" id="ccInput" placeholder="say something to her..." />
<button class="cc-send" id="ccSend">→</button>
</div>
</aside>
</div>
.curious-companion{
display:flex;align-items:center;gap:24px;
max-width:760px;margin:40px auto;
font-family:system-ui,sans-serif;
}
.curious-companion .cc-actor{flex:0 0 34%;max-width:240px;}
.curious-companion .cc-actor svg{display:block;width:100%;height:auto;}
.curious-companion .cc-walker{
animation:cc-breathe 4.5s ease-in-out infinite;
transform-origin:center;
}
@keyframes cc-breathe{
0%,100%{transform:scale(1);}
50%{transform:scale(1.02);}
}
.curious-companion .cc-bubble{
flex:1 1 0;min-width:0;max-width:440px;
position:relative;
background:rgba(8,19,14,.42);
backdrop-filter:blur(14px) saturate(120%);
-webkit-backdrop-filter:blur(14px) saturate(120%);
border:1.6px solid rgba(243,238,221,.32);
border-radius:24px;
padding:20px 22px 16px;
box-shadow:0 20px 44px -16px rgba(0,0,0,.6);
color:#f3eedd;
}
.curious-companion .cc-tail{
position:absolute;left:-12px;top:42%;
width:18px;height:18px;
background:rgba(8,19,14,.42);
backdrop-filter:blur(14px) saturate(120%);
-webkit-backdrop-filter:blur(14px) saturate(120%);
border-left:1.6px solid rgba(243,238,221,.32);
border-bottom:1.6px solid rgba(243,238,221,.32);
border-radius:0 0 0 6px;
transform:translateY(-50%) rotate(45deg);
}
.curious-companion .cc-label{
font-size:9.5px;font-weight:700;letter-spacing:1.6px;
color:#ffd23f;text-transform:uppercase;
margin-bottom:10px;
}
.curious-companion .cc-stream{
display:flex;flex-direction:column;gap:8px;
max-height:240px;overflow-y:auto;
margin-bottom:10px;
}
.curious-companion .cc-msg{
padding:9px 12px;border-radius:14px;
font-size:13.5px;line-height:1.45;max-width:92%;
}
.curious-companion .cc-msg.kitten{
align-self:flex-start;
background:rgba(82,255,224,.08);
border:1px solid rgba(82,255,224,.25);
font-family:Georgia,serif;font-style:italic;font-size:16px;
}
.curious-companion .cc-msg.user{
align-self:flex-end;
background:rgba(255,210,63,.1);
border:1px solid rgba(255,210,63,.3);
}
.curious-companion .cc-input-row{
display:flex;gap:6px;
padding-top:8px;border-top:1px dashed rgba(243,238,221,.14);
}
.curious-companion .cc-input{
flex:1;background:rgba(0,0,0,.35);
border:1.5px solid rgba(243,238,221,.22);
border-radius:12px;padding:8px 12px;
color:#f3eedd;font-size:13px;
}
.curious-companion .cc-input:focus{outline:none;border-color:#52ffe0;}
.curious-companion .cc-send{
background:#ff4d8d;border:none;color:#081310;
border-radius:50%;width:34px;height:34px;
font-size:14px;font-weight:700;cursor:pointer;
}
@media (max-width:680px){
.curious-companion{flex-direction:column;gap:14px;}
.curious-companion .cc-actor{flex:initial;width:100%;max-width:200px;}
.curious-companion .cc-tail{
left:50%;top:-12px;
transform:translateX(-50%) rotate(135deg);
}
}
// ===== CURIOUS COMPANION =====
// Scripted by default. To go LLM-live, set:
// window.CC_ENDPOINT = '/your/chat/api'; // POSTs { messages } returns { reply }
//
// 175 curated curiosity questions (truncated here for brevity — full set in repo).
const CC_QUESTIONS = [
"what are you obsessing over lately?",
"what idea refuses to leave you alone?",
"what rabbit hole are we entering today?",
"what thought keeps returning?",
"what are you circling around without realizing it?",
"what are you secretly trying to solve?",
"what feels unfinished in your mind?",
"what have you been unable to ignore?",
"what are you overthinking right now?",
"what are you curious about but afraid to admit?",
"what pattern are you noticing everywhere?",
"what question follows you around?",
"what's the question underneath your actual question?",
"what's a problem you enjoy thinking about too much?",
"what are you pretending not to care about?",
"what's something you know matters, but you don't yet know why?",
"what impossible thing are you trying to understand anyway?",
"what's your latest beautiful distraction?",
"what would feel satisfying to finally understand?",
"what are you trying to understand before the world changes again?"
// ... add the rest from sophieren.com/swamp-ui or just keep these 20
];
const CC_PREFIXES = [
"*tilts head* ", "*ears perk up* ", "*small purr* — ",
"ooh — ", "huh! ", "wait wait — ", ""
];
const stream = document.getElementById('ccStream');
const input = document.getElementById('ccInput');
const send = document.getElementById('ccSend');
const walker = document.querySelector('.cc-walker');
let llmHistory = [];
function pick(arr){ return arr[Math.floor(Math.random()*arr.length)]; }
function addMsg(role, text){
const el = document.createElement('div');
el.className = 'cc-msg ' + role;
el.textContent = text;
stream.appendChild(el);
stream.scrollTop = stream.scrollHeight;
}
// SCRIPTED MODE: pick random curious question
function scriptedReply(){
return pick(CC_PREFIXES) + pick(CC_QUESTIONS);
}
// LLM MODE: POST to your endpoint
async function llmReply(history){
const res = await fetch(window.CC_ENDPOINT, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ messages: history })
});
const data = await res.json();
return data.reply || '*tiny mew*';
}
async function handleSubmit(){
const userText = input.value.trim();
if (!userText) return;
addMsg('user', userText);
input.value = '';
llmHistory.push({role:'user', content:userText});
let reply;
if (window.CC_ENDPOINT) {
try { reply = await llmReply(llmHistory); }
catch(e){ reply = '(she went quiet — check your endpoint)'; }
} else {
reply = scriptedReply();
}
await new Promise(r => setTimeout(r, 400)); // tiny delay = feels alive
addMsg('kitten', reply);
llmHistory.push({role:'assistant', content:reply});
}
// NPC: kitten gently shifts position every 4-8s
function npcMove(){
const x = (Math.random()*60) - 30;
walker.style.transform = 'translateX(' + x + 'px)';
walker.style.transition = 'transform 3s cubic-bezier(.4,0,.2,1)';
setTimeout(npcMove, 3500 + Math.random()*4000);
}
send.addEventListener('click', handleSubmit);
input.addEventListener('keydown', e => {
if (e.key === 'Enter') { e.preventDefault(); handleSubmit(); }
});
// First impression: she greets you
addMsg('kitten', scriptedReply());
npcMove();
| Variable | Default | What it does |
|---|---|---|
window.CC_ENDPOINT |
(unset = scripted mode) | URL of your chat endpoint. Receives {messages: [...]}, returns {reply: "..."}. When set, replaces the scripted random-question response. |
CC_QUESTIONS |
20 starter questions | The library of curious questions used in scripted mode. Replace with your own to fit your tone. |
CC_PREFIXES |
7 cat stage-directions | Optional kitten action prefixes (*tilts head* etc.) randomly prepended. Set to [""] to disable. |
SVG fill values |
#fdf6e3 (cream) |
Replace with any pastel for a different kitten color. Or swap the SVG entirely — the agent UI pattern is character-agnostic. |
| System prompt (LLM mode) | (your endpoint defines it) | Recommended: instruct the LLM to ask better questions, never give answers. See the Curiosity Architecture doc for full design principles. |
Swap the kitten SVG for any character — a frog, a robot, a cloud, your dog, an abstract shape. The bubble holds any text. Recolor by changing one fill color in the SVG.
Replace the scripted reply array with your own copy — greetings, jokes, daily horoscope, "did you remember to drink water". Or wire window.CC_ENDPOINT to a real chat backend for full conversation.