Skip to content

Commit 033c0a7

Browse files
committed
1 parent b9fa5df commit 033c0a7

File tree

1 file changed

+293
-0
lines changed

1 file changed

+293
-0
lines changed

claude-token-counter.html

+293
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,293 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8">
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
6+
<title>Token Counter</title>
7+
<style>
8+
* {
9+
box-sizing: border-box;
10+
}
11+
12+
body {
13+
font-family: Helvetica, Arial, sans-serif;
14+
line-height: 1.6;
15+
max-width: 800px;
16+
margin: 0 auto;
17+
padding: 20px;
18+
}
19+
20+
textarea {
21+
width: 100%;
22+
min-height: 200px;
23+
margin: 10px 0;
24+
padding: 10px;
25+
font-size: 16px;
26+
font-family: monospace;
27+
border: 1px solid #ccc;
28+
border-radius: 4px;
29+
}
30+
31+
button {
32+
background: #0066ff;
33+
color: white;
34+
border: none;
35+
padding: 10px 20px;
36+
border-radius: 4px;
37+
cursor: pointer;
38+
font-size: 16px;
39+
}
40+
41+
button:disabled {
42+
background: #cccccc;
43+
cursor: not-allowed;
44+
}
45+
46+
.output {
47+
background: #f5f5f5;
48+
padding: 20px;
49+
border-radius: 4px;
50+
margin-top: 20px;
51+
white-space: pre-wrap;
52+
font-family: monospace;
53+
}
54+
55+
.error {
56+
color: #ff0000;
57+
margin: 10px 0;
58+
}
59+
60+
.input-group {
61+
margin-bottom: 20px;
62+
}
63+
64+
label {
65+
display: block;
66+
margin-bottom: 5px;
67+
font-weight: bold;
68+
}
69+
70+
.file-drop-area {
71+
border: 2px dashed #ccc;
72+
border-radius: 4px;
73+
padding: 20px;
74+
text-align: center;
75+
margin: 20px 0;
76+
background: #f8fafc;
77+
cursor: pointer;
78+
}
79+
80+
.file-drop-area.drag-over {
81+
background: #e2e8f0;
82+
border-color: #0066ff;
83+
}
84+
85+
.file-list {
86+
margin: 10px 0;
87+
}
88+
89+
.file-item {
90+
display: flex;
91+
align-items: center;
92+
justify-content: space-between;
93+
padding: 8px;
94+
background: #f5f5f5;
95+
border-radius: 4px;
96+
margin: 4px 0;
97+
}
98+
99+
.file-item button {
100+
background: #ff3333;
101+
padding: 4px 8px;
102+
font-size: 14px;
103+
}
104+
105+
.file-item button:hover {
106+
background: #cc0000;
107+
}
108+
</style>
109+
</head>
110+
<body>
111+
<h1>Claude Token Counter</h1>
112+
113+
<div class="input-group">
114+
<label for="system">System prompt:</label>
115+
<textarea id="system" style="min-height: 100px" placeholder="Enter system prompt (optional)"></textarea>
116+
</div>
117+
118+
<div class="input-group">
119+
<label for="content">User message:</label>
120+
<textarea id="content" placeholder="Enter message content"></textarea>
121+
</div>
122+
123+
<div class="file-drop-area" id="dropArea">
124+
<p>Drag and drop files here or click to select</p>
125+
<input type="file" id="fileInput" multiple style="display: none">
126+
<div class="file-list" id="fileList"></div>
127+
</div>
128+
129+
<button id="count">Count Tokens</button>
130+
<div id="error" class="error"></div>
131+
<div id="output" class="output"></div>
132+
133+
<script type="module">
134+
const API_URL = 'https://api.anthropic.com/v1/messages/count_tokens'
135+
const MODEL = 'claude-3-5-sonnet-20241022'
136+
137+
let attachedFiles = []
138+
139+
function getApiKey() {
140+
let key = localStorage.getItem('ANTHROPIC_API_KEY')
141+
if (!key) {
142+
key = prompt('Please enter your Anthropic API key:')
143+
if (key) {
144+
localStorage.setItem('ANTHROPIC_API_KEY', key)
145+
}
146+
}
147+
return key
148+
}
149+
150+
function handleFiles(files) {
151+
for (const file of files) {
152+
if (file.type.startsWith('image/') || file.type === 'application/pdf') {
153+
readAndStoreFile(file)
154+
} else {
155+
errorDiv.textContent = `Unsupported file type: ${file.type}`
156+
}
157+
}
158+
}
159+
160+
function readAndStoreFile(file) {
161+
const reader = new FileReader()
162+
reader.onload = function(e) {
163+
const base64Data = e.target.result.split(',')[1]
164+
attachedFiles.push({
165+
name: file.name,
166+
type: file.type,
167+
data: base64Data
168+
})
169+
updateFileList()
170+
}
171+
reader.readAsDataURL(file)
172+
}
173+
174+
function updateFileList() {
175+
const fileList = document.getElementById('fileList')
176+
fileList.innerHTML = attachedFiles.map((file, index) => `
177+
<div class="file-item">
178+
<span>${file.name} (${file.type})</span>
179+
<button onclick="removeFile(${index})">Remove</button>
180+
</div>
181+
`).join('')
182+
}
183+
184+
window.removeFile = function(index) {
185+
attachedFiles.splice(index, 1)
186+
updateFileList()
187+
}
188+
189+
async function countTokens(system, content) {
190+
const apiKey = getApiKey()
191+
if (!apiKey) {
192+
throw new Error('API key is required')
193+
}
194+
195+
const messageContent = []
196+
197+
// Add files first
198+
for (const file of attachedFiles) {
199+
messageContent.push({
200+
type: file.type.startsWith('image/') ? 'image' : 'document',
201+
source: {
202+
type: 'base64',
203+
media_type: file.type,
204+
data: file.data
205+
}
206+
})
207+
}
208+
209+
// Add text content if present
210+
if (content.trim()) {
211+
messageContent.push({
212+
type: 'text',
213+
text: content
214+
})
215+
}
216+
217+
const messages = [{
218+
role: 'user',
219+
content: messageContent
220+
}]
221+
222+
const response = await fetch(API_URL, {
223+
method: 'POST',
224+
headers: {
225+
'x-api-key': apiKey,
226+
'content-type': 'application/json',
227+
'anthropic-version': '2023-06-01',
228+
'anthropic-beta': 'token-counting-2024-11-01,pdfs-2024-09-25',
229+
'anthropic-dangerous-direct-browser-access': 'true'
230+
},
231+
body: JSON.stringify({
232+
model: MODEL,
233+
system: system || undefined,
234+
messages
235+
})
236+
})
237+
238+
if (!response.ok) {
239+
const error = await response.text()
240+
throw new Error(`API error: ${error}`)
241+
}
242+
243+
return response.json()
244+
}
245+
246+
const systemInput = document.getElementById('system')
247+
const contentInput = document.getElementById('content')
248+
const countButton = document.getElementById('count')
249+
const errorDiv = document.getElementById('error')
250+
const outputDiv = document.getElementById('output')
251+
const dropArea = document.getElementById('dropArea')
252+
const fileInput = document.getElementById('fileInput')
253+
254+
// File upload handling
255+
dropArea.addEventListener('click', () => fileInput.click())
256+
fileInput.addEventListener('change', (e) => handleFiles(e.target.files))
257+
258+
dropArea.addEventListener('dragover', (e) => {
259+
e.preventDefault()
260+
dropArea.classList.add('drag-over')
261+
})
262+
263+
dropArea.addEventListener('dragleave', () => {
264+
dropArea.classList.remove('drag-over')
265+
})
266+
267+
dropArea.addEventListener('drop', (e) => {
268+
e.preventDefault()
269+
dropArea.classList.remove('drag-over')
270+
handleFiles(e.dataTransfer.files)
271+
})
272+
273+
countButton.addEventListener('click', async () => {
274+
errorDiv.textContent = ''
275+
outputDiv.textContent = 'Counting tokens...'
276+
countButton.disabled = true
277+
278+
try {
279+
const result = await countTokens(
280+
systemInput.value.trim(),
281+
contentInput.value.trim()
282+
)
283+
outputDiv.textContent = JSON.stringify(result, null, 2)
284+
} catch (error) {
285+
errorDiv.textContent = error.message
286+
outputDiv.textContent = ''
287+
} finally {
288+
countButton.disabled = false
289+
}
290+
})
291+
</script>
292+
</body>
293+
</html>

0 commit comments

Comments
 (0)