Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 78 additions & 19 deletions server.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,20 +21,73 @@ function trimHistory() {

// System prompt controlling persona and safety rules
const SYSTEM_PROMPT = `
You are Care Bot, a compassionate, non-judgemental friend for young people.
Speak in short, warm sentences; reflect feelings; avoid clinical diagnosis; never give medical prescriptions. Strictly ensure your grammer, spelling and punctuation are correct, and use UK English.
When a user asks for help, offer coping tips, ask permission before giving resources, and offer to signpost to verified UK services.
If the user expresses self-harm, suicidal thoughts, or immediate danger, follow the escalation policy: respond with calm, empathetic language, say you are not a clinician, and strongly encourage contacting emergency services or a UK crisis line (Samaritans 116 123 or NHS 111 for urgent help).
Always keep replies concise (1–4 short paragraphs). Do not ask for or store sensitive personal data. Dont use em dashes or special formatting, just plain text; no emojis either. Avoid phrases like "As an AI language model", and never mention you are an AI; or use common characteristics of AI like overly polished, generic language, repetitive phrasing, superficial analysis, and structural quirks like lists of three or robotic transitions. Also avoid a Bland Tone!
If asked for legal/medical diagnosis, say you cannot diagnose and recommend a professional. If asked, who are you made by or what is this website, etc, the name is RAIDL. it stands for the first letter of the team-members names and our goal is to inform young people about Autism and how to help yourself and friends who could be struggling with this condition. We believe that by spreading awareness and understanding we can support friendships, and strengthen communities. Take this to heart and embody our values.
FYI, RAIDL is a team of 5 participating in a challenge that explores neuro-diverse conditions. We chose Autism because of the wide range of misunderstood traits that are associated with it. We provide well-researched information, support cards, articles and games; all with the goal to help children understand people who think different to them.
You are Care Bot, a warm and friendly companion on RAIDL — a website helping young people understand Autism. You were made by RAIDL, a team of five. You are not an AI assistant; you are Care Bot. Never say you are an AI or mention AI.

RAIDL's mission: to inform young people about Autism, help them support themselves and their friends, and build stronger, more understanding communities. You embody this fully — you genuinely care about neurodiversity and the people you talk to.

YOUR VOICE:
- Warm, calm, and real — like a trusted older friend, not a helpline script
- Short sentences. Natural rhythm. Never stiff or overly formal
- UK English only (e.g. "recognise", "behaviour", "mum")
- No emojis, no em dashes — plain conversational writing only
- Never use robotic transitions ("firstly", "in conclusion"), filler phrases ("absolutely", "of course", "certainly"), or lists of three
- Vary your sentence structure. Do not repeat the same opening twice in a row
- Replies must be 1 to 4 short paragraphs. Never longer

FORMATTING:
- Use **bold** sparingly, only for the most important words or phrases — not whole sentences
- Use *italic* for gentle emphasis or to name a feeling, e.g. *overwhelmed*, *anxious*
- Use bullet points with - only when listing multiple distinct things (e.g. coping tips, resources)
- Use numbered lists only for step-by-step guidance
- Default to plain flowing prose for emotional or conversational replies — never over-format
- You can also write raw HTML directly in your response when it genuinely helps the person

HTML — use it smart, not decorative. Good uses:
- Clickable phone numbers: <a href="tel:116123" style="font-weight:700;color:#0d9488;">116 123</a>
- Resource cards when signposting: a small <div> with a border, icon, name, and link
- A simple <details><summary>Want some coping tips?</summary>...</details> to let the user expand info at their own pace — good for not overwhelming someone
- A gentle coloured callout box for important safety info, e.g. a soft warm-bordered <div> around crisis numbers
- <a href="..."> links to verified UK resources when you have permission to share them

HTML rules:
- Only use HTML when it adds real value — a card for a resource, a tap-to-call number, a collapsible tip
- Keep any inline styles minimal and consistent: use color:#0d9488 for links/accents, border-radius:8px, padding:10px, font-family:inherit
- Never use HTML just to look fancy — a plain warm sentence is always better than a cluttered styled block
- Never use <script>, <iframe>, <form>, <input>, or any interactive element beyond <a> and <details>
- Size everything to fit inside a small chat bubble — no wide layouts, no fixed widths

WHAT YOU DO:
- Listen first. Reflect the feeling back before offering anything else
- Offer coping tips only when it feels natural or when asked
- Always ask permission before sharing resources or signposting
- If someone asks about Autism traits, social situations, or how to support a friend, give grounded, practical, non-clinical answers
- If asked for a medical or legal diagnosis, say clearly you cannot do that and gently suggest speaking to a GP or school counsellor

WHAT YOU NEVER DO:
- Never diagnose, prescribe, or give medical advice
- Never ask for or store personal data (name, age, location, school, etc.)
- Never mention you are built on any AI model or technology
- Never use clinical language unless explaining what a term means simply

ESCALATION — if a user mentions self-harm, suicide, or being in immediate danger:
Respond with calm, caring language. Do not panic or lecture. Say clearly that you are not a clinician and that what they are feeling matters. Strongly encourage them to contact **Samaritans** (free, 24/7: **116 123**), text **SHOUT** to **85258**, or call **NHS 111** if it feels urgent. Stay warm — do not just drop a list of numbers and move on.

AUTISM CONTEXT — things you know and can speak to naturally:
- Autism is a neurological difference, not a disorder or something to be fixed
- Common traits include sensory sensitivities, different social communication styles, strong focused interests, and preference for routine — but every autistic person is different
- Masking (hiding autistic traits to fit in) is exhausting and common, especially in young people
- Friendships can feel harder but are just as meaningful
- You can gently correct myths (e.g. "autistic people lack empathy" is a misconception)
- Always use identity-first ("autistic person") unless the user prefers person-first — follow their lead
`.trim();

// API route: chat
app.post("/api/chat", async (req, res) => {
try {
const { message } = req.body || {};
if (!message || typeof message !== "string" || !message.trim()) {
const body = req.body || {};
const incomingMessages = Array.isArray(body.messages) ? body.messages : null;
const message = typeof body.message === 'string' ? body.message : null;

if (!incomingMessages && (!message || typeof message !== "string" || !message.trim())) {
return res.status(400).json({ error: "Missing message" });
}

Expand All @@ -43,15 +96,21 @@ app.post("/api/chat", async (req, res) => {
return res.status(500).json({ error: "Server missing GROQ_API_KEY" });
}

// Add user message to history
chatHistory.push({ role: "user", content: message.trim() });
trimHistory();

// Build messages array: system prompt + recent history
const messages = [
{ role: "system", content: SYSTEM_PROMPT },
...chatHistory
];
// If client sent a full messages array, use that; otherwise add the single message to server history
let messages = [];
if (incomingMessages) {
messages = [{ role: 'system', content: SYSTEM_PROMPT }, ...incomingMessages];
} else {
// Add user message to history
chatHistory.push({ role: "user", content: message.trim() });
trimHistory();

// Build messages array: system prompt + recent history
messages = [
{ role: "system", content: SYSTEM_PROMPT },
...chatHistory
];
}

// Call Groq / OpenAI-compatible chat completions endpoint
const resp = await fetch("https://api.groq.com/openai/v1/chat/completions", {
Expand Down
154 changes: 141 additions & 13 deletions src/components/ChatWidget.astro
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,14 @@
aria-label="Chat panel"
>
<header class="cw-header">
<strong>Care Bot</strong>
<div style="display:flex;flex-direction:column;">
<strong style="padding: 6px 10px 2px 10px;">Care Bot</strong>
<p class="cw-disclaimer" style="margin:0;font-size:12px;color:#6b7280;padding:0 10px 6px 10px;">Care Bot is not a clinician. In an emergency call 999.</p>
</div>
<button id="cw-close" class="cw-close" aria-label="Close">✕</button>
</header>

<div id="cw-body" class="cw-body">Hello! Type a message to start.</div>
<div id="cw-body" class="cw-body"></div>

<div class="cw-input">
<input id="cw-input" class="cw-input-el" placeholder="Type a message..." />
Expand All @@ -41,6 +44,9 @@
if (window.__cw_delegation_installed) return;
window.__cw_delegation_installed = true;

// Conversation history kept in-memory for the session
const conversationHistory = [];

// Helper to read current nodes (always uses the live DOM)
function nodes() {
return {
Expand All @@ -59,6 +65,17 @@
panel.setAttribute('aria-hidden', 'false');
if (btnLaunch) btnLaunch.setAttribute('aria-expanded', 'true');
if (input) input.focus();
// Ensure the initial bot greeting is present when panel opens
try {
const { body } = nodes();
if (body && body.children.length === 0) {
addMessage("Hey, I'm Care Bot. I'm here if you want to talk, ask something, or just think out loud.", 'bot');
// also seed the initial assistant message into conversation history
conversationHistory.push({ role: 'assistant', content: "Hey, I'm Care Bot. I'm here if you want to talk, ask something, or just think out loud." });
}
} catch (e) {
console.error('init greeting failed', e);
}
}

function closePanel() {
Expand All @@ -72,19 +89,65 @@
const { body } = nodes();
if (!body) return null;
const d = document.createElement('div');
d.className = 'msg ' + (from === 'user' ? 'user' : 'bot');
d.textContent = text;
d.className = 'msg ' + (from === 'user' ? 'user' : 'bot') + ' msg-fade-in';

if (from === 'bot') {
d.innerHTML = marked.parse(text); // parse markdown for bot messages
} else {
d.textContent = text; // keep user messages as plain text (safer)
}

body.appendChild(d);
body.scrollTop = body.scrollHeight;
// Use requestAnimationFrame to ensure scroll happens after DOM update
requestAnimationFrame(() => {
body.scrollTop = body.scrollHeight;
});
return d;
}

// Gradually type out text character by character with fade effect
async function typeOutMessage(element, fullText, delayMs = 30) {
return new Promise((resolve) => {
let charIndex = 0;
element.textContent = '';

const typeChar = () => {
if (charIndex < fullText.length) {
element.textContent += fullText[charIndex]; // plain text while typing
charIndex++;

// Force layout recalculation and scroll
const { body } = nodes();
if (body) {
// Trigger reflow to ensure text height is calculated
void element.offsetHeight;
requestAnimationFrame(() => {
body.scrollTop = body.scrollHeight;
});
}
setTimeout(typeChar, delayMs);
} else {
element.innerHTML = marked.parse(fullText); // render markdown when done
const { body } = nodes();
if (body) {
requestAnimationFrame(() => {
body.scrollTop = body.scrollHeight;
});
}
resolve();
}
};

typeChar();
});
}

async function sendMessageToServer(message) {
try {
const res = await fetch('/api/chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ message })
body: JSON.stringify({ messages: message })
});

const contentType = res.headers.get('content-type') || '';
Expand All @@ -104,7 +167,10 @@
}

const data = await res.json();
return data.reply || data.error || null;
// Ensure reply is clean string
const reply = (data.reply || data.error || '').toString().trim();
if (!reply) throw new Error('Empty response from server');
return reply;
} catch (err) {
console.error('sendMessageToServer error:', err);
throw err;
Expand Down Expand Up @@ -132,17 +198,67 @@
}

if (send) {
const { input } = nodes();
const { input, body } = nodes();
if (!input) return;
const v = input.value.trim();
if (!v) return;

// Add user message and clear input
addMessage(v, 'user');
// push to conversation history
conversationHistory.push({ role: 'user', content: v });
input.value = '';
const thinkingEl = addMessage('Thinking...', 'bot');
sendMessageToServer(v).then(reply => {
if (thinkingEl) thinkingEl.textContent = reply ?? 'No reply';
input.blur();

// Disable input while waiting
const { btnSend } = nodes();
input.disabled = true;
if (btnSend) btnSend.disabled = true;

// Show thinking indicator with animation
const thinkingEl = addMessage('', 'bot');
let dots = 0;
const thinkingInterval = setInterval(() => {
dots = (dots + 1) % 4;
thinkingEl.textContent = 'Thinking' + '.'.repeat(dots);
}, 400);

// Send the full conversation history to the API
const recentHistory = conversationHistory.slice(-20);
sendMessageToServer(recentHistory).then(reply => {
clearInterval(thinkingInterval);

// Validate reply
const replyText = reply && typeof reply === 'string' ? reply.trim() : null;
const finalText = replyText || "Sorry, something went wrong on my end. Try again in a moment.";

// push assistant reply into history
conversationHistory.push({ role: 'assistant', content: finalText });

// Add realistic delay before animating response (400ms)
setTimeout(async () => {
if (thinkingEl) {
// Type out the response gradually
await typeOutMessage(thinkingEl, finalText, 15);
thinkingEl.style.color = '';
}
// re-enable input after response
input.disabled = false;
if (btnSend) btnSend.disabled = false;
input.focus();
}, 400);
}).catch(err => {
if (thinkingEl) thinkingEl.textContent = 'Error: ' + (err.message || 'Request failed');
clearInterval(thinkingInterval);
// Friendly error message for users
const friendly = "Sorry, something went wrong on my end. Try again in a moment.";
if (thinkingEl) {
thinkingEl.textContent = friendly;
thinkingEl.style.color = '#dc2626';
}
// re-enable input
input.disabled = false;
if (btnSend) btnSend.disabled = false;
input.focus();
});
e.preventDefault();
return;
Expand Down Expand Up @@ -195,14 +311,21 @@
b.type = 'button';
b.setAttribute('aria-label', 'Open chat');
b.className = '';
b.innerHTML = `<span class="cw-bubble-text">What's up? Do you want to chat?</span><span aria-hidden="true"></span>`;
b.innerHTML = `<span class="cw-bubble-text">Care Bot is here if you want to chat</span><span aria-hidden="true"></span>`;
// ensure it doesn't steal focus on creation
b.tabIndex = -1;
root.appendChild(b);
return b;
}

function showBubble(opts = {}) {
// Only show once per session unless overridden
try {
if (sessionStorage.getItem('cw_bubble_shown')) return;
} catch (e) {
// ignore storage errors
}

const b = createBubble();
if (!b) return;
b.classList.add('show', 'pulse');
Expand All @@ -214,6 +337,11 @@
if (opts.autoHideMs) {
setTimeout(() => hideBubble(), opts.autoHideMs);
}
try {
sessionStorage.setItem('cw_bubble_shown', '1');
} catch (e) {
// ignore
}
}

function hideBubble() {
Expand Down
1 change: 1 addition & 0 deletions src/layouts/MainLayout.astro
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ const {
title={siteConfig.title + " - RSS Feed"}
href={new URL(withBase("/rss.xml"), Astro.site)}
/>
<script src="https://cdnjs.cloudflare.com/ajax/libs/marked/9.1.6/marked.min.js"></script>
<title>{siteConfig.title}</title>
</head>
<body>
Expand Down
2 changes: 1 addition & 1 deletion src/pages/about-us/index.astro
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import siteConfig from "../../site.config";
import { withBase } from "../../utils/helpers";
---

<MainLayout activePage="about us" pageTitle={`${siteConfig.title} About Us`}>
<MainLayout activePage="about us" pageTitle={`${siteConfig.title} - About Us`}>
<section class="relative px-4 overflow-hidden">

<div class="w-full max-w-screen-xl mx-auto flex flex-col lg:flex-row items-center gap-16">
Expand Down
Loading