Skip to content

Commit 8ea8c96

Browse files
committed
Initial version
1 parent 6105989 commit 8ea8c96

21 files changed

+5495
-2
lines changed

.eslintrc.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"extends": ["next/core-web-vitals", "next/typescript"]
3+
}

.gitignore

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2+
3+
# dependencies
4+
/node_modules
5+
/.pnp
6+
.pnp.js
7+
.yarn/install-state.gz
8+
9+
# testing
10+
/coverage
11+
12+
# next.js
13+
/.next/
14+
/out/
15+
16+
# production
17+
/build
18+
19+
# misc
20+
.DS_Store
21+
*.pem
22+
23+
# debug
24+
npm-debug.log*
25+
yarn-debug.log*
26+
yarn-error.log*
27+
28+
# local env files
29+
.env*.local
30+
31+
# vercel
32+
.vercel
33+
34+
# typescript
35+
*.tsbuildinfo
36+
next-env.d.ts

README.md

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,36 @@
1-
# token-counter
2-
Token counter for Anthropic
1+
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
2+
3+
## Getting Started
4+
5+
First, run the development server:
6+
7+
```bash
8+
npm run dev
9+
# or
10+
yarn dev
11+
# or
12+
pnpm dev
13+
# or
14+
bun dev
15+
```
16+
17+
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
18+
19+
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
20+
21+
This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
22+
23+
## Learn More
24+
25+
To learn more about Next.js, take a look at the following resources:
26+
27+
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
28+
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
29+
30+
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
31+
32+
## Deploy on Vercel
33+
34+
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
35+
36+
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.

app/favicon.ico

25.3 KB
Binary file not shown.

app/fonts/GeistMonoVF.woff

66.3 KB
Binary file not shown.

app/fonts/GeistVF.woff

64.7 KB
Binary file not shown.

app/globals.css

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
@tailwind base;
2+
@tailwind components;
3+
@tailwind utilities;
4+
5+
body {
6+
font-family: Arial, Helvetica, sans-serif;
7+
}
8+
9+
@layer utilities {
10+
.text-balance {
11+
text-wrap: balance;
12+
}
13+
}
14+
15+
@layer base {
16+
:root {
17+
--radius: 0.5rem;
18+
}
19+
}

app/layout.tsx

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import type { Metadata } from "next";
2+
import localFont from "next/font/local";
3+
import "./globals.css";
4+
5+
const geistSans = localFont({
6+
src: "./fonts/GeistVF.woff",
7+
variable: "--font-geist-sans",
8+
weight: "100 900",
9+
});
10+
const geistMono = localFont({
11+
src: "./fonts/GeistMonoVF.woff",
12+
variable: "--font-geist-mono",
13+
weight: "100 900",
14+
});
15+
16+
export const metadata: Metadata = {
17+
title: "Create Next App",
18+
description: "Generated by create next app",
19+
};
20+
21+
export default function RootLayout({
22+
children,
23+
}: Readonly<{
24+
children: React.ReactNode;
25+
}>) {
26+
return (
27+
<html lang="en">
28+
<body
29+
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
30+
>
31+
{children}
32+
</body>
33+
</html>
34+
);
35+
}

app/page.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { TokenCounter } from "@/components/token-counter"
2+
3+
export default function Page() {
4+
return <TokenCounter />
5+
}

components.json

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
{
2+
"$schema": "https://ui.shadcn.com/schema.json",
3+
"style": "new-york",
4+
"rsc": true,
5+
"tsx": true,
6+
"tailwind": {
7+
"config": "tailwind.config.ts",
8+
"css": "app/globals.css",
9+
"baseColor": "neutral",
10+
"cssVariables": false,
11+
"prefix": ""
12+
},
13+
"aliases": {
14+
"components": "@/components",
15+
"utils": "@/lib/utils",
16+
"ui": "@/components/ui",
17+
"lib": "@/lib",
18+
"hooks": "@/hooks"
19+
},
20+
"iconLibrary": "lucide"
21+
}

components/count-tokens-route.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
"'use client'"
2+
3+
import { NextResponse } from "'next/server'"
4+
import Anthropic from "'@anthropic-ai/sdk'"
5+
6+
const client = new Anthropic({
7+
apiKey: process.env.ANTHROPIC_API_KEY
8+
})
9+
10+
export async function POST(req: Request) {
11+
try {
12+
const { text } = await req.json()
13+
14+
const response = await client.beta.messages.countTokens({
15+
betas: ["token-counting-2024-11-01"],
16+
model: "'claude-3-5-sonnet-20241022'",
17+
messages: [{
18+
role: "'user'",
19+
content: text
20+
}]
21+
})
22+
23+
return NextResponse.json({ tokens: response.input_tokens })
24+
} catch (error) {
25+
console.error("'Token counting error:'", error)
26+
return NextResponse.json({ error: "'Failed to count tokens'" }, { status: 500 })
27+
}
28+
}

components/token-counter.tsx

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
"'use client'"
2+
3+
import { useState, useTransition } from "'react'"
4+
import { Textarea } from "'@/components/ui/textarea'"
5+
import { Card } from "'@/components/ui/card'"
6+
import { AlertCircle } from "'lucide-react'"
7+
8+
async function getTokenCount(text: string): Promise<number> {
9+
try {
10+
const res = await fetch("'/api/count-tokens'", {
11+
method: "'POST'",
12+
headers: {
13+
"'Content-Type'": "'application/json'",
14+
},
15+
body: JSON.stringify({ text }),
16+
})
17+
if (!res.ok) throw new Error("'Failed to fetch token count'")
18+
const data = await res.json()
19+
return data.tokens
20+
} catch (error) {
21+
console.error("'Error fetching token count:'", error)
22+
throw error
23+
}
24+
}
25+
26+
export function TokenCounter() {
27+
const [text, setText] = useState("''")
28+
const [tokenCount, setTokenCount] = useState<number | null>(null)
29+
const [error, setError] = useState<string | null>(null)
30+
const [isPending, startTransition] = useTransition()
31+
32+
const handleTextChange = (value: string) => {
33+
setText(value)
34+
setError(null)
35+
startTransition(async () => {
36+
if (value.trim()) {
37+
try {
38+
const count = await getTokenCount(value)
39+
setTokenCount(count)
40+
} catch (err) {
41+
setError("'Failed to count tokens. Please try again.'")
42+
setTokenCount(null)
43+
}
44+
} else {
45+
setTokenCount(null)
46+
}
47+
})
48+
}
49+
50+
const wordCount = text.trim() ? text.trim().split(/\s+/).length : 0
51+
const charCount = text.length
52+
53+
return (
54+
<div className="min-h-screen bg-[#FAF9F6] text-[#1A1A1A] font-serif p-8">
55+
<div className="max-w-4xl mx-auto space-y-16">
56+
<header className="text-center space-y-6">
57+
<div className="size-12 mx-auto bg-black rounded-lg flex items-center justify-center">
58+
<div className="w-0 h-0 border-l-[10px] border-l-transparent border-b-[16px] border-b-white border-r-[10px] border-r-transparent" />
59+
</div>
60+
<h1 className="text-6xl font-normal tracking-tight">Anthropic Token Counter</h1>
61+
</header>
62+
63+
<Card className="p-10 bg-white rounded-3xl shadow-sm border-0">
64+
<div className="space-y-10">
65+
<Textarea
66+
value={text}
67+
onChange={(e) => handleTextChange(e.target.value)}
68+
placeholder="Paste your text here..."
69+
className="min-h-[400px] text-lg leading-relaxed rounded-2xl bg-gray-50
70+
focus:ring-1 focus:ring-black focus:bg-white
71+
hover:bg-gray-50/80 resize-y"
72+
/>
73+
74+
{error && (
75+
<div className="p-4 bg-red-50 border border-neutral-200 border-red-100 rounded-xl flex items-center text-red-900 text-sm dark:border-neutral-800">
76+
<AlertCircle className="w-4 h-4 mr-2 flex-shrink-0" />
77+
<span>{error}</span>
78+
</div>
79+
)}
80+
81+
<div className="grid grid-cols-3 gap-8">
82+
{[
83+
{ label: "'TOKENS'", value: tokenCount },
84+
{ label: "'WORDS'", value: wordCount },
85+
{ label: "'CHARACTERS'", value: charCount }
86+
].map(({ label, value }) => (
87+
<div key={label} className="text-center">
88+
<div className="text-sm tracking-widest text-gray-600 mb-2">{label}</div>
89+
<div className="text-4xl tabular-nums">
90+
{label === "'TOKENS'" && isPending ? "'...'" : value !== null ? value : "'-'"}
91+
</div>
92+
</div>
93+
))}
94+
</div>
95+
</div>
96+
</Card>
97+
</div>
98+
</div>
99+
)
100+
}

components/ui/card.tsx

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import * as React from "react"
2+
3+
import { cn } from "@/lib/utils"
4+
5+
const Card = React.forwardRef<
6+
HTMLDivElement,
7+
React.HTMLAttributes<HTMLDivElement>
8+
>(({ className, ...props }, ref) => (
9+
<div
10+
ref={ref}
11+
className={cn(
12+
"rounded-xl border border-neutral-200 bg-white text-neutral-950 shadow dark:border-neutral-800 dark:bg-neutral-950 dark:text-neutral-50",
13+
className
14+
)}
15+
{...props}
16+
/>
17+
))
18+
Card.displayName = "Card"
19+
20+
const CardHeader = React.forwardRef<
21+
HTMLDivElement,
22+
React.HTMLAttributes<HTMLDivElement>
23+
>(({ className, ...props }, ref) => (
24+
<div
25+
ref={ref}
26+
className={cn("flex flex-col space-y-1.5 p-6", className)}
27+
{...props}
28+
/>
29+
))
30+
CardHeader.displayName = "CardHeader"
31+
32+
const CardTitle = React.forwardRef<
33+
HTMLDivElement,
34+
React.HTMLAttributes<HTMLDivElement>
35+
>(({ className, ...props }, ref) => (
36+
<div
37+
ref={ref}
38+
className={cn("font-semibold leading-none tracking-tight", className)}
39+
{...props}
40+
/>
41+
))
42+
CardTitle.displayName = "CardTitle"
43+
44+
const CardDescription = React.forwardRef<
45+
HTMLDivElement,
46+
React.HTMLAttributes<HTMLDivElement>
47+
>(({ className, ...props }, ref) => (
48+
<div
49+
ref={ref}
50+
className={cn("text-sm text-neutral-500 dark:text-neutral-400", className)}
51+
{...props}
52+
/>
53+
))
54+
CardDescription.displayName = "CardDescription"
55+
56+
const CardContent = React.forwardRef<
57+
HTMLDivElement,
58+
React.HTMLAttributes<HTMLDivElement>
59+
>(({ className, ...props }, ref) => (
60+
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
61+
))
62+
CardContent.displayName = "CardContent"
63+
64+
const CardFooter = React.forwardRef<
65+
HTMLDivElement,
66+
React.HTMLAttributes<HTMLDivElement>
67+
>(({ className, ...props }, ref) => (
68+
<div
69+
ref={ref}
70+
className={cn("flex items-center p-6 pt-0", className)}
71+
{...props}
72+
/>
73+
))
74+
CardFooter.displayName = "CardFooter"
75+
76+
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }

components/ui/textarea.tsx

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import * as React from "react"
2+
3+
import { cn } from "@/lib/utils"
4+
5+
export interface TextareaProps
6+
extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {}
7+
8+
const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
9+
({ className, ...props }, ref) => {
10+
return (
11+
<textarea
12+
className={cn(
13+
"flex min-h-[60px] w-full rounded-md border border-neutral-200 bg-transparent px-3 py-2 text-base shadow-sm placeholder:text-neutral-500 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-neutral-950 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm dark:border-neutral-800 dark:placeholder:text-neutral-400 dark:focus-visible:ring-neutral-300",
14+
className
15+
)}
16+
ref={ref}
17+
{...props}
18+
/>
19+
)
20+
}
21+
)
22+
Textarea.displayName = "Textarea"
23+
24+
export { Textarea }

0 commit comments

Comments
 (0)