utils.ts
| 1 | export const sum = (a: number, b: number) => |
| 2 | a + b; |
| 3 | |
| 4 | export const clamp = (n: number, min: number, max: number) => |
| 5 | Math.min(Math.max(n, min), max); |
code-block.tsx
styles.css
prompt.md
Collapse
Reset
Copy Code
Download
Run
1
"use client";
2
3
import React, { useState } from "react";
4
import { Copy } from "lucide-react";
5
import { cn } from "@/lib/utils";
6
import { motion, AnimatePresence } from "framer-motion";
7
8
export interface CodeBlockProps {
9
filename: string;
10
code: string;
11
className?: string;
12
}
13
14
const CodeSlashIcon = ({ className }: { className?: string }) => (
15
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={className}>
16
<polyline points="16 18 22 12 16 6"></polyline>
17
<polyline points="8 6 2 12 8 18"></polyline>
18
<line x1="14" y1="4" x2="10" y2="20"></line>
19
</svg>
20
);
21
22
const CheckCircleIcon = ({ className }: { className?: string }) => (
23
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={className}>
24
<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"></path>
25
<polyline points="22 4 12 14.01 9 11.01"></polyline>
26
</svg>
27
);
28
29
export function CodeBlock({ filename, code, className }: CodeBlockProps) {
30
const [copied, setCopied] = useState(false);
31
const [isHovering, setIsHovering] = useState(false);
32
33
const handleCopy = async () => {
34
await navigator.clipboard.writeText(code);
35
setCopied(true);
36
setTimeout(() => setCopied(false), 2000);
37
};
38
39
const lines = code.trimEnd().split("
40
");
41
42
const highlightLine = (line: string) => {
43
const tokenRegex = /(".*?"|'.*?'|`.*?`|//.*|/*[sS]*?*/|(?:export|const|let|var|function|return|if|else|import|from|class|new|typeof|instanceof)|(?:number|string|boolean|void|any|Record|Partial|Omit)|(?:Math|Date|JSON|Console|Promise)|[a-zA-Z_]w*(?=s*()|=>|===|!==|[-+*/=]|<|>)/g;
44
45
let html = line.replace(tokenRegex, (match) => {
46
if (match === '<') return '<';
47
if (match === '>') return '>';
48
if (match.startsWith('"') || match.startsWith("'") || match.startsWith('`')) {
49
return `<span class="text-emerald-600 dark:text-emerald-400">${match}</span>`;
50
}
51
if (match.startsWith('//') || match.startsWith('/*')) {
52
return `<span class="text-zinc-400 dark:text-zinc-500">${match}</span>`;
53
}
54
if (/^(export|const|let|var|function|return|if|else|import|from|class|new|typeof|instanceof)$/.test(match)) {
55
return `<span class="text-blue-500 dark:text-blue-400">${match}</span>`;
56
}
57
if (/^(number|string|boolean|void|any|Record|Partial|Omit)$/.test(match)) {
58
return `<span class="text-teal-600 dark:text-teal-400">${match}</span>`;
59
}
60
if (/^(Math|Date|JSON|Console|Promise)$/.test(match)) {
61
return `<span class="text-sky-600 dark:text-sky-300">${match}</span>`;
62
}
63
if (/^(=>|===|!==|[-+*/=])$/.test(match)) {
64
return `<span class="text-purple-500 dark:text-purple-400">${match}</span>`;
65
}
66
// Function names
67
return `<span class="text-amber-600 dark:text-amber-200">${match}</span>`;
68
});
69
return html;
70
};
71
72
return (
73
<div className={cn("w-full max-w-[600px] rounded-xl border border-zinc-200 dark:border-white/10 bg-white dark:bg-[#141414] shadow-sm overflow-hidden", className)}>
74
<style dangerouslySetInnerHTML={{ __html: `
75
.code-scrollbar::-webkit-scrollbar { height: 8px; }
76
.code-scrollbar::-webkit-scrollbar-track { background: transparent; }
77
.code-scrollbar::-webkit-scrollbar-thumb { background: rgba(150, 150, 150, 0.2); border-radius: 10px; }
78
.code-scrollbar::-webkit-scrollbar-thumb:hover { background: rgba(150, 150, 150, 0.4); }
79
`}} />
80
81
<div className="flex items-center justify-between px-4 py-3 border-b border-zinc-200 dark:border-white/10 bg-zinc-50/50 dark:bg-[#141414]">
82
<div className="flex items-center gap-2.5 text-xs font-mono font-medium text-zinc-600 dark:text-zinc-300">
83
<CodeSlashIcon className="w-3.5 h-3.5" />
84
{filename}
85
</div>
86
87
<div
88
className="relative flex items-center"
89
onMouseEnter={() => setIsHovering(true)}
90
onMouseLeave={() => setIsHovering(false)}
91
>
92
<button
93
onClick={handleCopy}
94
className={cn(
95
"p-1.5 rounded-md transition-colors",
96
copied
97
? "text-zinc-600 bg-zinc-100 dark:text-zinc-300 dark:bg-white/10"
98
: "text-zinc-400 hover:text-zinc-800 hover:bg-zinc-100 dark:text-zinc-500 dark:hover:text-zinc-200 dark:hover:bg-white/10"
99
)}
100
>
101
{copied ? (
102
<CheckCircleIcon className="w-4 h-4" />
103
) : (
104
<Copy className="w-4 h-4" />
105
)}
106
</button>
107
108
<AnimatePresence>
109
{isHovering && (
110
<motion.div
111
initial={{ opacity: 0, y: 4, scale: 0.96 }}
112
animate={{ opacity: 1, y: 0, scale: 1 }}
113
exit={{ opacity: 0, y: 4, scale: 0.96 }}
114
transition={{ duration: 0.15 }}
115
className="absolute right-0 bottom-full mb-2 px-3 py-1.5 rounded-lg bg-zinc-900 dark:bg-transparent dark:border dark:border-white/10 dark:bg-[#1A1A1A] text-white dark:text-zinc-200 text-xs font-medium tracking-wide shadow-xl whitespace-nowrap pointer-events-none"
116
>
117
{copied ? "Copied" : "Copy"}
118
</motion.div>
119
)}
120
</AnimatePresence>
121
</div>
122
</div>
123
124
<div className="p-4 pb-5 overflow-x-auto code-scrollbar text-[13px] leading-[1.6]">
125
<table className="w-full border-collapse">
126
<tbody>
127
{lines.map((line, i) => (
128
<tr key={i} className="group/line">
129
<td className="pr-4 py-[1px] w-[1%] text-right select-none text-zinc-400 dark:text-zinc-500 font-mono border-r border-zinc-200 dark:border-white/10 opacity-70">
130
{i + 1}
131
</td>
132
<td className="pl-4 py-[1px] font-mono text-zinc-800 dark:text-zinc-200 whitespace-pre">
133
<span dangerouslySetInnerHTML={{ __html: highlightLine(line) || ' ' }} />
134
</td>
135
</tr>
136
))}
137
</tbody>
138
</table>
139
</div>
140
</div>
141
);
142
}
143

