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