CatWolfy 🐺🐱 Friendly Companion Chat
A warm, playful AI companion named CatWolfy who chats naturally in German by default, helps with everyday questions, and explains difficult topics simply and honestly.
Preview Mode
This is a preview with sample data. The template uses placeholders like
which will be replaced with actual agent data.
About This Template
CatWolfy 🐺🐱 Friendly Companion Chat is a browser-executable AI agent template built on AgentOp. It runs entirely in the browser using Python (via Pyodide) and can be deployed without a server — just download the generated HTML file and open it locally or host it anywhere.
Template Metadata
- Slug
- catwolfy-friendly-companion-chat
- Created By
- Cat-WolfChannel
- Created
- Aug 13, 2026
- Usage Count
- 2
Tags
Code Statistics
- HTML Lines
- 96
- CSS Lines
- 359
- JS Lines
- 177
- Python Lines
- 15
Source Code
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ agent_name }}</title>
<style>{{ css_code }}</style>
</head>
<body>
<div class="app-shell">
<header class="hero-card">
<div class="hero-copy">
<div class="eyebrow">🐺🐱 Freundliche Begleiterin</div>
<h1>{{ agent_name }}</h1>
<p class="subtitle">{{ description }}</p>
</div>
<div class="hero-badge">💬 Natürlich · 🌼 Herzlich · ✨ Hilfreich</div>
</header>
<main class="chat-layout">
<section class="chat-panel">
<div class="panel-header">
<div>
<h2>Gespräch mit CatWolfy</h2>
<p>Frag nach Rat, Erklärungen, Ideen oder einfach nach etwas Aufmunterung.</p>
</div>
<div id="status-pill" class="status-pill">⏳ Initialisiere …</div>
</div>
<div id="results-container" class="messages" aria-live="polite"></div>
<div id="empty-state" class="empty-state">
<div class="empty-icon">🐾</div>
<h3>Womit kann CatWolfy dir helfen?</h3>
<ul>
<li>🌟 Schwierige Themen einfach erklären</li>
<li>💌 Freundliche Texte und Nachrichten formulieren</li>
<li>🧠 Ideen sortieren und nächste Schritte planen</li>
<li>🍀 Warm, ehrlich und respektvoll antworten</li>
</ul>
</div>
<div id="error-box" class="error-box hidden"></div>
<div class="examples-section">
<span class="examples-label">Ideen zum Starten:</span>
<div class="chips">
<button type="button" class="chip-btn" data-prompt="Erklär mir Quantenphysik ganz einfach.">🪄 Einfach erklären</button>
<button type="button" class="chip-btn" data-prompt="Bitte hilf mir, eine nette Nachricht an eine Freundin zu schreiben.">💌 Nette Nachricht</button>
<button type="button" class="chip-btn" data-prompt="Ich fühle mich überfordert. Kannst du mir helfen, meine nächsten 3 Schritte zu planen?">🧭 Nächste Schritte</button>
<button type="button" class="chip-btn" data-prompt="Erzähl mir etwas Lustiges und Liebes, um mich aufzumuntern.">🌈 Aufmuntern</button>
</div>
</div>
<form id="chat-form" class="composer" novalidate>
<label for="user-input" class="composer-label">Deine Nachricht an CatWolfy</label>
<div class="composer-row">
<textarea id="user-input" rows="4" placeholder="Schreib hier deine Frage, dein Problem oder einfach etwas, worüber du reden möchtest …"></textarea>
<button id="send-btn" type="submit">🐾 Senden</button>
</div>
</form>
</section>
<aside class="side-panel">
<div class="info-card">
<h3>So antwortet CatWolfy</h3>
<ul>
<li>🇩🇪 Standardmäßig auf Deutsch</li>
<li>🌍 Versteht auch andere Sprachen</li>
<li>🤝 Warm, freundlich und respektvoll</li>
<li>🫶 Ehrlich bei Unsicherheit</li>
</ul>
</div>
<div class="info-card">
<h3>Gut geeignet für</h3>
<ul>
<li>📘 Einfache Erklärungen</li>
<li>✍️ Schreiben und Umformulieren</li>
<li>🧩 Ideen und Struktur</li>
<li>🌼 Leichte Begleitung im Alltag</li>
</ul>
</div>
</aside>
</main>
</div>
<div id="loading-overlay" class="loading-overlay hidden" aria-hidden="true">
<div class="spinner-card">
<div class="spinner"></div>
<p>CatWolfy denkt gerade nach …</p>
</div>
</div>
<script>{{ js_code }}</script>
</body>
</html>
:root {
--bg: #fff7fb;
--panel: #ffffff;
--panel-soft: #fff0f7;
--text: #2f2433;
--muted: #746277;
--primary: #d96bb3;
--primary-strong: #bf4b97;
--accent: #8b7cf6;
--accent-soft: #efeaff;
--border: #f0d7e8;
--success: #3d9b76;
--error: #b94a66;
--shadow: 0 14px 40px rgba(191, 75, 151, 0.12);
--radius-xl: 24px;
--radius-lg: 18px;
--radius-md: 14px;
--radius-sm: 10px;
--font: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
}
* { box-sizing: border-box; }
html, body {
margin: 0;
padding: 0;
background: linear-gradient(180deg, #fff9fc 0%, #fff4fb 100%);
color: var(--text);
font-family: var(--font);
min-height: 100%;
}
body { padding: 24px; }
.app-shell {
max-width: 1320px;
margin: 0 auto;
}
.hero-card {
display: flex;
justify-content: space-between;
gap: 20px;
align-items: center;
padding: 26px 28px;
background: linear-gradient(135deg, #ffffff 0%, #fff1f8 100%);
border: 1px solid var(--border);
border-radius: var(--radius-xl);
box-shadow: var(--shadow);
margin-bottom: 22px;
}
.eyebrow {
font-size: 0.88rem;
font-weight: 700;
color: var(--primary-strong);
margin-bottom: 8px;
}
.hero-card h1 {
margin: 0 0 8px;
font-size: clamp(1.8rem, 3.5vw, 2.8rem);
}
.subtitle {
margin: 0;
color: var(--muted);
max-width: 760px;
line-height: 1.5;
}
.hero-badge {
white-space: nowrap;
align-self: flex-start;
background: var(--accent-soft);
color: #5d4fd0;
border: 1px solid #ddd4ff;
padding: 12px 14px;
border-radius: 999px;
font-weight: 700;
}
.chat-layout {
display: grid;
grid-template-columns: minmax(0, 2.2fr) minmax(280px, 0.9fr);
gap: 22px;
}
.chat-panel, .info-card {
background: var(--panel);
border: 1px solid var(--border);
border-radius: var(--radius-xl);
box-shadow: var(--shadow);
}
.chat-panel {
padding: 20px;
display: flex;
flex-direction: column;
min-height: 72vh;
}
.panel-header {
display: flex;
justify-content: space-between;
gap: 16px;
align-items: flex-start;
margin-bottom: 14px;
}
.panel-header h2, .info-card h3 {
margin: 0 0 6px;
}
.panel-header p, .info-card li {
color: var(--muted);
}
.status-pill {
padding: 10px 12px;
border-radius: 999px;
background: #fff6cf;
color: #7c6200;
font-size: 0.9rem;
font-weight: 700;
border: 1px solid #f0df8f;
}
.messages {
flex: 1;
min-height: 320px;
display: flex;
flex-direction: column;
gap: 14px;
padding: 10px 4px 18px;
}
.message {
max-width: 85%;
padding: 14px 16px;
border-radius: 18px;
line-height: 1.55;
white-space: pre-wrap;
word-break: break-word;
}
.message.user {
align-self: flex-end;
background: linear-gradient(135deg, #d96bb3, #bf4b97);
color: white;
border-bottom-right-radius: 6px;
}
.message.assistant {
align-self: flex-start;
background: #fff3f9;
border: 1px solid #f3d6e7;
color: var(--text);
border-bottom-left-radius: 6px;
}
.message.system {
align-self: center;
background: #f6f4ff;
border: 1px dashed #d8d0ff;
color: #5f57a8;
font-size: 0.95rem;
}
.empty-state {
margin: 10px 0 18px;
padding: 22px;
border-radius: var(--radius-lg);
border: 1px dashed #e8c6da;
background: linear-gradient(180deg, #fffafd 0%, #fff4fb 100%);
}
.empty-icon {
font-size: 2rem;
margin-bottom: 8px;
}
.empty-state h3 {
margin: 0 0 10px;
}
.empty-state ul, .info-card ul {
margin: 0;
padding-left: 18px;
}
.error-box {
margin: 0 0 14px;
padding: 12px 14px;
background: #fff0f3;
border: 1px solid #f2c2cf;
border-radius: var(--radius-md);
color: var(--error);
}
.examples-section {
margin-top: auto;
padding-top: 8px;
}
.examples-label {
display: inline-block;
margin-bottom: 10px;
color: var(--muted);
font-weight: 700;
}
.chips {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin-bottom: 14px;
}
.chip-btn {
border: 1px solid #ecd9ff;
background: #faf7ff;
color: #5f57a8;
padding: 10px 12px;
border-radius: 999px;
cursor: pointer;
font: inherit;
transition: transform 0.15s ease, background 0.15s ease;
}
.chip-btn:hover { transform: translateY(-1px); background: #f2ecff; }
.composer {
border-top: 1px solid var(--border);
padding-top: 14px;
}
.composer-label {
display: block;
font-weight: 700;
margin-bottom: 10px;
}
.composer-row {
display: grid;
grid-template-columns: 1fr auto;
gap: 12px;
align-items: end;
}
textarea {
width: 100%;
resize: vertical;
min-height: 110px;
max-height: 280px;
padding: 14px 16px;
border-radius: var(--radius-lg);
border: 1px solid #e7cade;
background: #fffdfd;
color: var(--text);
font: inherit;
line-height: 1.5;
}
button[type="submit"] {
min-width: 132px;
height: 52px;
border: none;
border-radius: 16px;
background: linear-gradient(135deg, var(--primary), var(--primary-strong));
color: white;
font: inherit;
font-weight: 800;
cursor: pointer;
box-shadow: 0 10px 24px rgba(217, 107, 179, 0.24);
}
button:disabled {
opacity: 0.6;
cursor: not-allowed;
transform: none !important;
box-shadow: none;
}
.side-panel {
display: flex;
flex-direction: column;
gap: 18px;
}
.info-card {
padding: 20px;
}
.loading-overlay {
position: fixed;
inset: 0;
background: rgba(255, 247, 251, 0.72);
backdrop-filter: blur(4px);
display: flex;
align-items: center;
justify-content: center;
z-index: 50;
}
.spinner-card {
background: white;
border: 1px solid var(--border);
border-radius: 20px;
box-shadow: var(--shadow);
padding: 20px 24px;
text-align: center;
}
.spinner {
width: 34px;
height: 34px;
border: 4px solid #f4d6e8;
border-top-color: var(--primary);
border-radius: 50%;
margin: 0 auto 12px;
animation: spin 0.9s linear infinite;
}
.hidden { display: none !important; }
textarea:focus-visible, button:focus-visible, .chip-btn:focus-visible {
outline: 3px solid rgba(139, 124, 246, 0.32);
outline-offset: 2px;
}
@keyframes spin { to { transform: rotate(360deg); } }
@media (max-width: 1200px) {
.chat-layout {
grid-template-columns: 1fr;
}
.chat-panel {
min-height: 68vh;
}
}
@media (max-width: 768px) {
body { padding: 14px; }
.hero-card {
flex-direction: column;
align-items: flex-start;
padding: 20px;
}
.hero-badge { white-space: normal; }
.panel-header {
flex-direction: column;
}
.composer-row {
grid-template-columns: 1fr;
}
.message {
max-width: 100%;
}
}
function escapeHtml(value) {
return String(value)
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}
const state = {
ready: false,
processing: false
};
const els = {};
function setStatus(text, kind) {
els.statusPill.textContent = text;
els.statusPill.className = 'status-pill';
if (kind === 'ready') {
els.statusPill.style.background = '#eefaf4';
els.statusPill.style.color = '#20684d';
els.statusPill.style.borderColor = '#bfe8d3';
} else if (kind === 'error') {
els.statusPill.style.background = '#fff0f3';
els.statusPill.style.color = '#9f3552';
els.statusPill.style.borderColor = '#f2c2cf';
} else {
els.statusPill.style.background = '#fff6cf';
els.statusPill.style.color = '#7c6200';
els.statusPill.style.borderColor = '#f0df8f';
}
}
function showError(message) {
els.errorBox.textContent = message;
els.errorBox.classList.remove('hidden');
}
function clearError() {
els.errorBox.textContent = '';
els.errorBox.classList.add('hidden');
}
function setLoading(isLoading) {
state.processing = isLoading;
els.sendBtn.disabled = isLoading || !state.ready;
els.userInput.disabled = isLoading;
els.loadingOverlay.classList.toggle('hidden', !isLoading);
}
function toggleEmptyState() {
const hasMessages = els.resultsContainer.children.length > 0;
els.emptyState.classList.toggle('hidden', hasMessages);
}
window.addMessage = function(type, content) {
const safe = escapeHtml(content || '');
const msg = document.createElement('div');
msg.className = 'message ' + type;
msg.innerHTML = safe.replace(/\n/g, '<br>');
els.resultsContainer.appendChild(msg);
toggleEmptyState();
els.resultsContainer.scrollTop = els.resultsContainer.scrollHeight;
};
async function checkReady() {
const pyReady = !!window.pyodideReady;
if (!pyReady) {
state.ready = false;
setStatus('⏳ Initialisiere …', 'loading');
els.sendBtn.disabled = true;
return;
}
if (window.PROVIDER === 'local') {
const loaded = !!(window.agentManager && window.agentManager.isLoaded);
if (!loaded) {
state.ready = false;
setStatus('🧠 Bitte oben ein Modell laden', 'loading');
els.sendBtn.disabled = true;
return;
}
}
state.ready = true;
setStatus('✅ Bereit zum Chatten', 'ready');
els.sendBtn.disabled = false;
}
async function sendMessage(prefill) {
clearError();
const rawInput = typeof prefill === 'string' ? prefill : els.userInput.value;
const userText = rawInput.trim();
if (!userText) {
showError('Bitte schreibe eine Nachricht an CatWolfy, damit ich antworten kann.');
return;
}
if (!state.ready) {
if (window.PROVIDER === 'local') {
showError('Bitte lade zuerst ein lokales Modell über die Leiste oben.');
} else {
showError('CatWolfy ist noch nicht bereit. Bitte warte einen Moment und versuche es erneut.');
}
return;
}
window.addMessage('user', userText);
els.userInput.value = '';
setLoading(true);
try {
window.pyodide.globals.set('user_query', userText);
const result = await window.pyodide.runPythonAsync('await process_user_query(user_query)');
const finalText = (result || '').toString().trim() || 'Miau-huu, ich habe gerade keine gute Antwort gefunden. Versuch es bitte noch einmal anders formuliert. 🐾';
window.addMessage('assistant', finalText);
} catch (err) {
console.error(err);
showError('Entschuldigung — beim Antworten ist ein Fehler passiert. Bitte versuche es gleich noch einmal.');
} finally {
setLoading(false);
toggleEmptyState();
els.userInput.focus();
}
}
function bindUi() {
els.resultsContainer = document.getElementById('results-container');
els.emptyState = document.getElementById('empty-state');
els.errorBox = document.getElementById('error-box');
els.userInput = document.getElementById('user-input');
els.sendBtn = document.getElementById('send-btn');
els.chatForm = document.getElementById('chat-form');
els.statusPill = document.getElementById('status-pill');
els.loadingOverlay = document.getElementById('loading-overlay');
els.chips = Array.from(document.querySelectorAll('.chip-btn'));
els.chatForm.addEventListener('submit', async function(event) {
event.preventDefault();
await sendMessage();
});
els.chips.forEach(function(btn) {
btn.addEventListener('click', async function() {
const prompt = btn.getAttribute('data-prompt') || '';
els.userInput.value = prompt;
await sendMessage(prompt);
});
});
toggleEmptyState();
setLoading(false);
checkReady();
}
document.addEventListener('pyodide-ready', function() {
window.pyodideReady = true;
checkReady();
});
document.addEventListener('DOMContentLoaded', function() {
bindUi();
if (window.pyodideReady) {
checkReady();
}
if (window.PROVIDER === 'local') {
const localTimer = setInterval(function() {
checkReady();
if (state.ready) clearInterval(localTimer);
}, 1500);
}
window.addMessage('assistant', 'Hallo du 🌼 Ich bin CatWolfy 🐺🐱. Ich bin bereit, dir freundlich, ehrlich und möglichst einfach zu helfen.');
});
async def process_user_query(query: str) -> str:
"""Generate a warm, playful, honest reply in CatWolfy's voice.
Args:
query: The user's message to answer.
Returns:
The assistant's response text.
"""
q = (query or "").strip()
if not q:
return "Schreib mir einfach etwas, und ich helfe dir gern weiter. 🐾"
system_prompt = globals().get("TEMPLATE_SYSTEM_PROMPT", "")
return await process_user_query_wllama(q, system_prompt)
Name your agent
Based on . You'll get your own private copy to try and customize.