UI Components Easy
AI Response Card
AI-generated result card with model badge, response text area, copy/thumbs-up/thumbs-down actions, and regenerate button.
Open in Lab
MCP
css vanilla-js react vue svelte tailwind
Targets: TS JS HTML React Vue Svelte
Code
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
background: #0d1117;
color: #e6edf3;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 32px 16px;
}
.demo {
width: 100%;
max-width: 640px;
position: relative;
}
.arc-card {
background: #161b22;
border: 1px solid #30363d;
border-radius: 16px;
overflow: hidden;
}
.arc-header {
display: flex;
align-items: center;
gap: 10px;
padding: 14px 20px;
border-bottom: 1px solid #21262d;
background: #1c2128;
}
.arc-model {
display: flex;
align-items: center;
gap: 7px;
}
.model-dot {
width: 8px;
height: 8px;
background: #16a34a;
border-radius: 50%;
}
.model-label {
font-size: 13px;
font-weight: 700;
color: #374151;
}
.arc-ts {
font-size: 12px;
color: #9ca3af;
margin-left: auto;
}
.arc-body {
padding: 20px 24px;
font-size: 14px;
color: #cdd6f4;
line-height: 1.7;
display: flex;
flex-direction: column;
gap: 12px;
}
.arc-body strong {
color: #e6edf3;
}
.arc-code {
background: #0d1117;
border-radius: 10px;
padding: 16px;
overflow-x: auto;
font-family: "JetBrains Mono", Menlo, monospace;
font-size: 12.5px;
color: #a5d6ff;
line-height: 1.7;
border: 1px solid #21262d;
}
.arc-actions {
display: flex;
align-items: center;
gap: 4px;
padding: 10px 20px;
border-top: 1px solid #21262d;
background: #1c2128;
}
.arc-btn {
display: flex;
align-items: center;
gap: 5px;
background: none;
border: none;
color: #6c7086;
font-size: 12px;
font-weight: 600;
padding: 5px 10px;
border-radius: 8px;
cursor: pointer;
transition: color 0.15s, background 0.15s;
}
.arc-btn:hover {
color: #cdd6f4;
background: rgba(255, 255, 255, 0.06);
}
.arc-btn.active-up {
color: #a6e3a1;
}
.arc-btn.active-down {
color: #f38ba8;
}
.arc-btn.copied {
color: #89b4fa;
}
.arc-model-label {
color: #cdd6f4;
}
.arc-ts {
color: #6c7086;
}
.arc-tokens {
color: #4a555f;
}
.arc-btn--icon {
padding: 5px 8px;
}
.arc-spacer {
flex: 1;
}
.arc-tokens {
font-size: 11px;
color: #d1d5db;
}
/* Toast */
.feedback-toast {
position: fixed;
bottom: 24px;
left: 50%;
transform: translateX(-50%);
background: #111827;
color: #fff;
font-size: 13px;
font-weight: 600;
padding: 10px 20px;
border-radius: 10px;
animation: fadeIn 0.2s ease;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateX(-50%) translateY(8px);
}
to {
opacity: 1;
transform: translateX(-50%) translateY(0);
}
}function toast(msg, duration = 2000) {
const t = document.getElementById("feedbackToast");
t.textContent = msg;
t.hidden = false;
clearTimeout(t._timer);
t._timer = setTimeout(() => {
t.hidden = true;
}, duration);
}
document.getElementById("copyBtn").addEventListener("click", () => {
const text = document.querySelector(".arc-body").innerText;
navigator.clipboard.writeText(text);
const btn = document.getElementById("copyBtn");
const label = document.getElementById("copyLabel");
btn.classList.add("copied");
label.textContent = "Copied!";
setTimeout(() => {
btn.classList.remove("copied");
label.textContent = "Copy";
}, 2000);
});
document.getElementById("thumbUpBtn").addEventListener("click", function () {
this.classList.toggle("active-up");
document.getElementById("thumbDownBtn").classList.remove("active-down");
if (this.classList.contains("active-up")) toast("👍 Feedback recorded — thanks!");
});
document.getElementById("thumbDownBtn").addEventListener("click", function () {
this.classList.toggle("active-down");
document.getElementById("thumbUpBtn").classList.remove("active-up");
if (this.classList.contains("active-down")) toast("👎 Feedback noted — we'll improve!");
});
document.getElementById("regenBtn").addEventListener("click", function () {
this.style.animation = "spin 0.5s linear";
setTimeout(() => {
this.style.animation = "";
}, 500);
toast("⟳ Regenerating…");
});<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>AI Response Card</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="demo">
<div class="arc-card" id="arcCard">
<div class="arc-header">
<div class="arc-model">
<span class="model-dot"></span>
<span class="model-label">Claude Sonnet</span>
</div>
<span class="arc-ts">Just now</span>
</div>
<div class="arc-body">
<p>Here's how you can implement a debounce function in JavaScript:</p>
<pre class="arc-code"><code>function debounce(fn, delay) {
let timer;
return (...args) => {
clearTimeout(timer);
timer = setTimeout(() => fn(...args), delay);
};
}
// Usage
const onSearch = debounce((query) => {
console.log('Searching:', query);
}, 300);</code></pre>
<p>The function delays execution until <strong>delay</strong> milliseconds have passed since the last call. It's ideal for search inputs, resize handlers, and scroll events.</p>
</div>
<div class="arc-actions">
<button class="arc-btn" id="copyBtn" title="Copy">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>
<span id="copyLabel">Copy</span>
</button>
<button class="arc-btn arc-btn--icon" id="thumbUpBtn" title="Good response">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 9V5a3 3 0 0 0-3-3l-4 9v11h11.28a2 2 0 0 0 2-1.7l1.38-9a2 2 0 0 0-2-2.3H14z"/><path d="M7 22H4a2 2 0 0 1-2-2v-7a2 2 0 0 1 2-2h3"/></svg>
</button>
<button class="arc-btn arc-btn--icon" id="thumbDownBtn" title="Bad response">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M10 15v4a3 3 0 0 0 3 3l4-9V2H5.72a2 2 0 0 0-2 1.7l-1.38 9a2 2 0 0 0 2 2.3H10z"/><path d="M17 2h2.67A2.31 2.31 0 0 1 22 4v7a2.31 2.31 0 0 1-2.33 2H17"/></svg>
</button>
<button class="arc-btn arc-btn--icon" id="regenBtn" title="Regenerate">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="23 4 23 10 17 10"/><path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"/></svg>
</button>
<div class="arc-spacer"></div>
<span class="arc-tokens">~120 tokens</span>
</div>
</div>
<div class="feedback-toast" id="feedbackToast" hidden></div>
</div>
<script src="script.js"></script>
</body>
</html>import { useState } from "react";
const RESPONSES = [
{
model: "claude-opus-4",
provider: "#e89537",
response: `The key difference between **RAG** and **fine-tuning** lies in how they inject knowledge into a language model.
**RAG** retrieves relevant documents at inference time and appends them to the prompt. This means your knowledge can be updated without retraining — just update the vector store.
**Fine-tuning** bakes knowledge into model weights during training. It's better for teaching style, tone, or structured output formats, but updating knowledge requires expensive retraining.
For most production use cases, **RAG + a strong base model** is the better default.`,
},
{
model: "gpt-4o",
provider: "#10a37f",
response: `Great question! Here's a concise breakdown:
- **RAG** = retrieval at inference time (dynamic, updatable, grounded in sources)
- **Fine-tuning** = training on domain data (baked-in, style/format control, expensive to update)
Use RAG when your knowledge changes frequently. Use fine-tuning when you need consistent output format or specialized behavior the base model doesn't exhibit.`,
},
];
function Markdown({ text }: { text: string }) {
const html = text
.replace(/\*\*(.+?)\*\*/g, '<strong class="text-[#e6edf3] font-semibold">$1</strong>')
.replace(/\n\n/g, '</p><p class="mt-3">')
.replace(
/\n- /g,
"\n<span class=\"block pl-4 before:content-['•'] before:mr-2 before:text-[#58a6ff]\">"
)
.replace(/\n(?!<)/g, "<br/>");
return (
<p
className="text-[13px] text-[#8b949e] leading-relaxed mt-3"
dangerouslySetInnerHTML={{ __html: html }}
/>
);
}
function ResponseCard({
model,
provider,
response,
}: {
model: string;
provider: string;
response: string;
}) {
const [copied, setCopied] = useState(false);
const [vote, setVote] = useState<"up" | "down" | null>(null);
const [regenerating, setRegen] = useState(false);
const copy = () => {
navigator.clipboard.writeText(response);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
};
const regen = () => {
setRegen(true);
setTimeout(() => setRegen(false), 1500);
};
return (
<div className="bg-[#161b22] border border-[#30363d] rounded-xl overflow-hidden">
{/* Header */}
<div className="flex items-center justify-between px-4 py-2.5 bg-[#21262d] border-b border-[#30363d]">
<div className="flex items-center gap-2">
<span className="w-2 h-2 rounded-full flex-shrink-0" style={{ background: provider }} />
<span className="text-[12px] font-mono font-bold text-[#e6edf3]">{model}</span>
</div>
<span className="text-[10px] px-1.5 py-0.5 bg-green-500/10 text-green-400 border border-green-500/20 rounded-full font-semibold">
Generated
</span>
</div>
{/* Body */}
<div className={`px-5 py-4 transition-opacity ${regenerating ? "opacity-30" : ""}`}>
{regenerating ? (
<div className="flex items-center gap-2 py-4">
{[0, 1, 2].map((i) => (
<span
key={i}
className="w-2 h-2 bg-[#58a6ff] rounded-full animate-bounce"
style={{ animationDelay: `${i * 150}ms` }}
/>
))}
<span className="text-[12px] text-[#8b949e] ml-1">Regenerating…</span>
</div>
) : (
<Markdown text={response} />
)}
</div>
{/* Actions */}
<div className="flex items-center justify-between px-4 py-2.5 bg-[#0d1117]/50 border-t border-[#30363d]">
<div className="flex items-center gap-1">
{/* Copy */}
<button
onClick={copy}
className={`flex items-center gap-1.5 px-2.5 py-1.5 rounded-md text-[11px] font-semibold border transition-colors ${
copied
? "text-green-400 border-green-500/30 bg-green-500/10"
: "text-[#8b949e] border-transparent hover:border-[#30363d] hover:text-[#e6edf3]"
}`}
>
{copied ? (
<>
<svg
width="11"
height="11"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2.5"
>
<polyline points="20 6 9 17 4 12" />
</svg>
Copied
</>
) : (
<>
<svg
width="11"
height="11"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
>
<rect x="9" y="9" width="13" height="13" rx="2" />
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" />
</svg>
Copy
</>
)}
</button>
{/* Thumbs */}
{(["up", "down"] as const).map((dir) => (
<button
key={dir}
onClick={() => setVote(vote === dir ? null : dir)}
className={`p-1.5 rounded-md border transition-colors ${
vote === dir
? dir === "up"
? "text-green-400 border-green-500/30 bg-green-500/10"
: "text-red-400 border-red-500/30 bg-red-500/10"
: "text-[#8b949e] border-transparent hover:border-[#30363d] hover:text-[#e6edf3]"
}`}
>
<svg
width="13"
height="13"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
style={dir === "down" ? { transform: "scaleY(-1)" } : {}}
>
<path d="M14 9V5a3 3 0 0 0-3-3l-4 9v11h11.28a2 2 0 0 0 2-1.7l1.38-9a2 2 0 0 0-2-2.3zM7 22H4a2 2 0 0 1-2-2v-7a2 2 0 0 1 2-2h3" />
</svg>
</button>
))}
</div>
{/* Regenerate */}
<button
onClick={regen}
disabled={regenerating}
className="flex items-center gap-1.5 px-2.5 py-1.5 rounded-md text-[11px] font-semibold border border-[#30363d] text-[#8b949e] hover:text-[#e6edf3] hover:border-[#8b949e] transition-colors disabled:opacity-30"
>
<svg
width="11"
height="11"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
>
<polyline points="23 4 23 10 17 10" />
<path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10" />
</svg>
Regenerate
</button>
</div>
</div>
);
}
export default function AiResponseCardRC() {
return (
<div className="min-h-screen bg-[#0d1117] p-6 flex justify-center">
<div className="w-full max-w-[680px] space-y-4">
<p className="text-[13px] font-semibold text-[#8b949e]">
What's the difference between RAG and fine-tuning?
</p>
{RESPONSES.map((r) => (
<ResponseCard key={r.model} {...r} />
))}
</div>
</div>
);
}<script setup>
import { ref } from "vue";
const RESPONSES = [
{
model: "claude-opus-4",
provider: "#e89537",
response: `The key difference between **RAG** and **fine-tuning** lies in how they inject knowledge into a language model.\n\n**RAG** retrieves relevant documents at inference time and appends them to the prompt. This means your knowledge can be updated without retraining — just update the vector store.\n\n**Fine-tuning** bakes knowledge into model weights during training. It's better for teaching style, tone, or structured output formats, but updating knowledge requires expensive retraining.\n\nFor most production use cases, **RAG + a strong base model** is the better default.`,
},
{
model: "gpt-4o",
provider: "#10a37f",
response: `Great question! Here's a concise breakdown:\n\n- **RAG** = retrieval at inference time (dynamic, updatable, grounded in sources)\n- **Fine-tuning** = training on domain data (baked-in, style/format control, expensive to update)\n\nUse RAG when your knowledge changes frequently. Use fine-tuning when you need consistent output format or specialized behavior the base model doesn't exhibit.`,
},
];
function formatMarkdown(text) {
return text
.replace(/\*\*(.+?)\*\*/g, '<strong class="text-[#e6edf3] font-semibold">$1</strong>')
.replace(/\n\n/g, '</p><p class="mt-3">')
.replace(
/\n- /g,
"\n<span class=\"block pl-4 before:content-['•'] before:mr-2 before:text-[#58a6ff]\">"
)
.replace(/\n(?!<)/g, "<br/>");
}
const cards = ref(
RESPONSES.map((r) => ({
...r,
copied: false,
vote: null,
regenerating: false,
}))
);
function copy(index) {
navigator.clipboard.writeText(cards.value[index].response);
cards.value[index].copied = true;
setTimeout(() => {
cards.value[index].copied = false;
}, 2000);
}
function toggleVote(index, dir) {
cards.value[index].vote = cards.value[index].vote === dir ? null : dir;
}
function regen(index) {
cards.value[index].regenerating = true;
setTimeout(() => {
cards.value[index].regenerating = false;
}, 1500);
}
</script>
<template>
<div class="min-h-screen bg-[#0d1117] p-6 flex justify-center">
<div class="w-full max-w-[680px] space-y-4">
<p class="text-[13px] font-semibold text-[#8b949e]">
What's the difference between RAG and fine-tuning?
</p>
<div v-for="(card, i) in cards" :key="card.model" class="bg-[#161b22] border border-[#30363d] rounded-xl overflow-hidden">
<!-- Header -->
<div class="flex items-center justify-between px-4 py-2.5 bg-[#21262d] border-b border-[#30363d]">
<div class="flex items-center gap-2">
<span class="w-2 h-2 rounded-full flex-shrink-0" :style="{ background: card.provider }"></span>
<span class="text-[12px] font-mono font-bold text-[#e6edf3]">{{ card.model }}</span>
</div>
<span class="text-[10px] px-1.5 py-0.5 bg-green-500/10 text-green-400 border border-green-500/20 rounded-full font-semibold">
Generated
</span>
</div>
<!-- Body -->
<div class="px-5 py-4 transition-opacity" :class="{ 'opacity-30': card.regenerating }">
<div v-if="card.regenerating" class="flex items-center gap-2 py-4">
<span v-for="j in 3" :key="j" class="w-2 h-2 bg-[#58a6ff] rounded-full animate-bounce" :style="{ animationDelay: (j - 1) * 150 + 'ms' }"></span>
<span class="text-[12px] text-[#8b949e] ml-1">Regenerating…</span>
</div>
<p v-else class="text-[13px] text-[#8b949e] leading-relaxed mt-3" v-html="formatMarkdown(card.response)"></p>
</div>
<!-- Actions -->
<div class="flex items-center justify-between px-4 py-2.5 bg-[#0d1117]/50 border-t border-[#30363d]">
<div class="flex items-center gap-1">
<!-- Copy -->
<button
@click="copy(i)"
:class="card.copied
? 'text-green-400 border-green-500/30 bg-green-500/10'
: 'text-[#8b949e] border-transparent hover:border-[#30363d] hover:text-[#e6edf3]'"
class="flex items-center gap-1.5 px-2.5 py-1.5 rounded-md text-[11px] font-semibold border transition-colors"
>
<svg v-if="card.copied" width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="20 6 9 17 4 12"/></svg>
<svg v-else width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>
{{ card.copied ? 'Copied' : 'Copy' }}
</button>
<!-- Thumbs -->
<button
v-for="dir in ['up', 'down']"
:key="dir"
@click="toggleVote(i, dir)"
:class="card.vote === dir
? (dir === 'up' ? 'text-green-400 border-green-500/30 bg-green-500/10' : 'text-red-400 border-red-500/30 bg-red-500/10')
: 'text-[#8b949e] border-transparent hover:border-[#30363d] hover:text-[#e6edf3]'"
class="p-1.5 rounded-md border transition-colors"
>
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" :style="dir === 'down' ? { transform: 'scaleY(-1)' } : {}">
<path d="M14 9V5a3 3 0 0 0-3-3l-4 9v11h11.28a2 2 0 0 0 2-1.7l1.38-9a2 2 0 0 0-2-2.3zM7 22H4a2 2 0 0 1-2-2v-7a2 2 0 0 1 2-2h3"/>
</svg>
</button>
</div>
<!-- Regenerate -->
<button
@click="regen(i)"
:disabled="card.regenerating"
class="flex items-center gap-1.5 px-2.5 py-1.5 rounded-md text-[11px] font-semibold border border-[#30363d] text-[#8b949e] hover:text-[#e6edf3] hover:border-[#8b949e] transition-colors disabled:opacity-30"
>
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="23 4 23 10 17 10"/>
<path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"/>
</svg>
Regenerate
</button>
</div>
</div>
</div>
</div>
</template><script>
const RESPONSES = [
{
model: "claude-opus-4",
provider: "#e89537",
response: `The key difference between **RAG** and **fine-tuning** lies in how they inject knowledge into a language model.\n\n**RAG** retrieves relevant documents at inference time and appends them to the prompt. This means your knowledge can be updated without retraining — just update the vector store.\n\n**Fine-tuning** bakes knowledge into model weights during training. It's better for teaching style, tone, or structured output formats, but updating knowledge requires expensive retraining.\n\nFor most production use cases, **RAG + a strong base model** is the better default.`,
},
{
model: "gpt-4o",
provider: "#10a37f",
response: `Great question! Here's a concise breakdown:\n\n- **RAG** = retrieval at inference time (dynamic, updatable, grounded in sources)\n- **Fine-tuning** = training on domain data (baked-in, style/format control, expensive to update)\n\nUse RAG when your knowledge changes frequently. Use fine-tuning when you need consistent output format or specialized behavior the base model doesn't exhibit.`,
},
];
function formatMarkdown(text) {
return text
.replace(/\*\*(.+?)\*\*/g, '<strong class="text-[#e6edf3] font-semibold">$1</strong>')
.replace(/\n\n/g, '</p><p class="mt-3">')
.replace(
/\n- /g,
"\n<span class=\"block pl-4 before:content-['•'] before:mr-2 before:text-[#58a6ff]\">"
)
.replace(/\n(?!<)/g, "<br/>");
}
let cards = RESPONSES.map((r) => ({
...r,
copied: false,
vote: null,
regenerating: false,
}));
function copy(index) {
navigator.clipboard.writeText(cards[index].response);
cards[index].copied = true;
setTimeout(() => {
cards[index].copied = false;
cards = cards;
}, 2000);
cards = cards;
}
function toggleVote(index, dir) {
cards[index].vote = cards[index].vote === dir ? null : dir;
cards = cards;
}
function regen(index) {
cards[index].regenerating = true;
cards = cards;
setTimeout(() => {
cards[index].regenerating = false;
cards = cards;
}, 1500);
}
</script>
<div class="min-h-screen bg-[#0d1117] p-6 flex justify-center">
<div class="w-full max-w-[680px] space-y-4">
<p class="text-[13px] font-semibold text-[#8b949e]">
What's the difference between RAG and fine-tuning?
</p>
{#each cards as card, i}
<div class="bg-[#161b22] border border-[#30363d] rounded-xl overflow-hidden">
<!-- Header -->
<div class="flex items-center justify-between px-4 py-2.5 bg-[#21262d] border-b border-[#30363d]">
<div class="flex items-center gap-2">
<span class="w-2 h-2 rounded-full flex-shrink-0" style="background:{card.provider}"></span>
<span class="text-[12px] font-mono font-bold text-[#e6edf3]">{card.model}</span>
</div>
<span class="text-[10px] px-1.5 py-0.5 bg-green-500/10 text-green-400 border border-green-500/20 rounded-full font-semibold">
Generated
</span>
</div>
<!-- Body -->
<div class="px-5 py-4 transition-opacity" class:opacity-30={card.regenerating}>
{#if card.regenerating}
<div class="flex items-center gap-2 py-4">
{#each [0, 1, 2] as j}
<span class="w-2 h-2 bg-[#58a6ff] rounded-full animate-bounce" style="animation-delay:{j * 150}ms"></span>
{/each}
<span class="text-[12px] text-[#8b949e] ml-1">Regenerating…</span>
</div>
{:else}
<p class="text-[13px] text-[#8b949e] leading-relaxed mt-3">{@html formatMarkdown(card.response)}</p>
{/if}
</div>
<!-- Actions -->
<div class="flex items-center justify-between px-4 py-2.5 bg-[#0d1117]/50 border-t border-[#30363d]">
<div class="flex items-center gap-1">
<!-- Copy -->
<button
on:click={() => copy(i)}
class="flex items-center gap-1.5 px-2.5 py-1.5 rounded-md text-[11px] font-semibold border transition-colors {card.copied ? 'text-green-400 border-green-500/30 bg-green-500/10' : 'text-[#8b949e] border-transparent hover:border-[#30363d] hover:text-[#e6edf3]'}"
>
{#if card.copied}
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="20 6 9 17 4 12"/></svg>
Copied
{:else}
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>
Copy
{/if}
</button>
<!-- Thumbs -->
{#each ['up', 'down'] as dir}
<button
on:click={() => toggleVote(i, dir)}
class="p-1.5 rounded-md border transition-colors {card.vote === dir ? (dir === 'up' ? 'text-green-400 border-green-500/30 bg-green-500/10' : 'text-red-400 border-red-500/30 bg-red-500/10') : 'text-[#8b949e] border-transparent hover:border-[#30363d] hover:text-[#e6edf3]'}"
>
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style={dir === 'down' ? 'transform:scaleY(-1)' : ''}>
<path d="M14 9V5a3 3 0 0 0-3-3l-4 9v11h11.28a2 2 0 0 0 2-1.7l1.38-9a2 2 0 0 0-2-2.3zM7 22H4a2 2 0 0 1-2-2v-7a2 2 0 0 1 2-2h3"/>
</svg>
</button>
{/each}
</div>
<!-- Regenerate -->
<button
on:click={() => regen(i)}
disabled={card.regenerating}
class="flex items-center gap-1.5 px-2.5 py-1.5 rounded-md text-[11px] font-semibold border border-[#30363d] text-[#8b949e] hover:text-[#e6edf3] hover:border-[#8b949e] transition-colors disabled:opacity-30"
>
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="23 4 23 10 17 10"/>
<path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"/>
</svg>
Regenerate
</button>
</div>
</div>
{/each}
</div>
</div>AI response card with model name badge, response body with markdown-like formatting, and an action toolbar with copy, thumbs-up, thumbs-down, and regenerate buttons. Includes feedback state transitions.