Agent Template

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.

companion chat german friendly playful assistant
Cat-WolfChannel Aug 13, 2026 2 uses

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.

Topics companion chat german friendly playful assistant
Template Preview

Template Metadata

Slug
catwolfy-friendly-companion-chat
Created By
Cat-WolfChannel
Created
Aug 13, 2026
Usage Count
2

Tags

companion chat german friendly playful assistant

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, '&amp;')
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;')
    .replace(/"/g, '&quot;')
    .replace(/'/g, '&#039;');
}

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)