Skip to content

Instantly share code, notes, and snippets.

@simonw
Created November 2, 2024 18:25
Show Gist options
  • Save simonw/ebc1e32b9f3ddc0875ce8d875d7100bd to your computer and use it in GitHub Desktop.
Save simonw/ebc1e32b9f3ddc0875ce8d875d7100bd to your computer and use it in GitHub Desktop.

Uploading Images and PDFs to Anthropic API

human (Nov 2, 2024, 11:16 AM)

#!/bin/sh

IMAGE_URL="https://upload.wikimedia.org/wikipedia/commons/a/a7/Camponotus_flavomarginatus_ant.jpg"
IMAGE_MEDIA_TYPE="image/jpeg"
IMAGE_BASE64=$(curl "$IMAGE_URL" | base64)

curl https://api.anthropic.com/v1/messages/count_tokens \
     --header "x-api-key: $ANTHROPIC_API_KEY" \
     --header "anthropic-version: 2023-06-01" \
     --header "anthropic-beta: token-counting-2024-11-01" \
     --header "content-type: application/json" \
     --data \
'{
    "model": "claude-3-5-sonnet-20241022",
    "messages": [
        {"role": "user", "content": [
            {"type": "image", "source": {
                "type": "base64",
                "media_type": "'$IMAGE_MEDIA_TYPE'",
                "data": "'$IMAGE_BASE64'"
            }},
            {"type": "text", "text": "Describe this image"}
        ]}
    ]
}'

And for PDFs:

curl https://api.anthropic.com/v1/messages/count_tokens \
    --header "x-api-key: $ANTHROPIC_API_KEY" \
    --header "content-type: application/json" \
    --header "anthropic-version: 2023-06-01" \
    --header "anthropic-beta: pdfs-2024-09-25,token-counting-2024-11-01" \
    --data '{
      "model": "claude-3-5-sonnet-20241022",
      "messages": [{
        "role": "user",
        "content": [
          {
            "type": "document",
            "source": {
              "type": "base64",
              "media_type": "application/pdf",
              "data": "'$(base64 -i document.pdf)'"
            }
          },
          {
            "type": "text",
            "text": "Please summarize this document."
          }
        ]
      }]
    }'

Add a file attachment UI to the tool. It should allow multiple files to be selected (or dragged and dropped). Those files should then be included in the JSON sent to the model, correctly base64 encoded, using type=document and application/pdf for PDFs and type=image and the correct image content type for images.

count.html

# Claude Token Counter

::: input-group
System prompt:
:::

::: input-group
User message:
:::

Count Tokens

::: {#error .error}
:::

::: {#output .output}
:::


assistant (Nov 2, 2024, 11:16 AM)

I'll help you create a file attachment UI for the token counter tool. I'll modify the HTML to include a file upload area and add the necessary functionality to handle multiple files.

create Token Counter with File Attachments

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Claude Token Counter</title>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/lodash/4.17.21/lodash.min.js"></script>
    <style>
        body {
            font-family: system-ui, -apple-system, sans-serif;
            max-width: 800px;
            margin: 0 auto;
            padding: 20px;
            line-height: 1.6;
        }

        .input-group {
            margin-bottom: 20px;
        }

        textarea {
            width: 100%;
            min-height: 100px;
            margin: 8px 0;
            padding: 8px;
            border: 1px solid #ccc;
            border-radius: 4px;
            font-family: inherit;
            font-size: inherit;
            resize: vertical;
        }

        button {
            background: #2563eb;
            color: white;
            border: none;
            padding: 8px 16px;
            border-radius: 4px;
            cursor: pointer;
            font-size: 16px;
        }

        button:hover {
            background: #1d4ed8;
        }

        button:disabled {
            background: #93c5fd;
            cursor: not-allowed;
        }

        .error {
            color: #dc2626;
            margin: 8px 0;
            display: none;
        }

        .output {
            margin: 16px 0;
            padding: 16px;
            background: #f1f5f9;
            border-radius: 4px;
            display: none;
        }

        .file-drop-area {
            border: 2px dashed #ccc;
            border-radius: 4px;
            padding: 20px;
            text-align: center;
            margin: 20px 0;
            background: #f8fafc;
        }

        .file-drop-area.drag-over {
            background: #e2e8f0;
            border-color: #2563eb;
        }

        .file-list {
            margin: 16px 0;
        }

        .file-item {
            display: flex;
            align-items: center;
            justify-content: space-between;
            padding: 8px;
            background: #f1f5f9;
            border-radius: 4px;
            margin: 4px 0;
        }

        .file-item button {
            background: #dc2626;
            padding: 4px 8px;
            font-size: 14px;
        }

        .file-item button:hover {
            background: #b91c1c;
        }
    </style>
</head>
<body>
    <h1>Claude Token Counter</h1>

    <div class="input-group">
        <label for="system">System prompt:</label>
        <textarea id="system"></textarea>
    </div>

    <div class="input-group">
        <label for="user">User message:</label>
        <textarea id="user"></textarea>
    </div>

    <div class="file-drop-area" id="dropArea">
        <p>Drag and drop files here or click to select</p>
        <input type="file" id="fileInput" multiple style="display: none">
        <div class="file-list" id="fileList"></div>
    </div>

    <button onclick="countTokens()" id="countButton">Count Tokens</button>

    <div id="error" class="error"></div>
    <div id="output" class="output"></div>

    <script>
        let attachedFiles = [];

        // File handling functions
        function handleFiles(files) {
            for (const file of files) {
                if (isValidFile(file)) {
                    readAndStoreFile(file);
                } else {
                    showError(`Unsupported file type: ${file.type}`);
                }
            }
        }

        function isValidFile(file) {
            return file.type.startsWith('image/') || file.type === 'application/pdf';
        }

        function readAndStoreFile(file) {
            const reader = new FileReader();
            reader.onload = function(e) {
                const base64Data = e.target.result.split(',')[1];
                attachedFiles.push({
                    name: file.name,
                    type: file.type,
                    data: base64Data
                });
                updateFileList();
            };
            reader.readAsDataURL(file);
        }

        function updateFileList() {
            const fileList = document.getElementById('fileList');
            fileList.innerHTML = attachedFiles.map((file, index) => `
                <div class="file-item">
                    <span>${file.name} (${file.type})</span>
                    <button onclick="removeFile(${index})">Remove</button>
                </div>
            `).join('');
        }

        function removeFile(index) {
            attachedFiles.splice(index, 1);
            updateFileList();
        }

        // Drag and drop handling
        const dropArea = document.getElementById('dropArea');
        const fileInput = document.getElementById('fileInput');

        dropArea.addEventListener('click', () => fileInput.click());
        fileInput.addEventListener('change', (e) => handleFiles(e.target.files));

        dropArea.addEventListener('dragover', (e) => {
            e.preventDefault();
            dropArea.classList.add('drag-over');
        });

        dropArea.addEventListener('dragleave', () => {
            dropArea.classList.remove('drag-over');
        });

        dropArea.addEventListener('drop', (e) => {
            e.preventDefault();
            dropArea.classList.remove('drag-over');
            handleFiles(e.dataTransfer.files);
        });

        // Token counting function
        async function countTokens() {
            const systemPrompt = document.getElementById('system').value;
            const userMessage = document.getElementById('user').value;
            const errorDiv = document.getElementById('error');
            const outputDiv = document.getElementById('output');
            const countButton = document.getElementById('countButton');

            errorDiv.style.display = 'none';
            outputDiv.style.display = 'none';
            countButton.disabled = true;

            try {
                // Construct messages array with files
                const messageContent = [];
                
                // Add files first
                for (const file of attachedFiles) {
                    const fileContent = {
                        type: file.type.startsWith('image/') ? 'image' : 'document',
                        source: {
                            type: 'base64',
                            media_type: file.type,
                            data: file.data
                        }
                    };
                    messageContent.push(fileContent);
                }

                // Add text content
                if (userMessage) {
                    messageContent.push({
                        type: 'text',
                        text: userMessage
                    });
                }

                const messages = [];
                
                // Add system message if present
                if (systemPrompt) {
                    messages.push({
                        role: 'system',
                        content: systemPrompt
                    });
                }

                // Add user message
                messages.push({
                    role: 'user',
                    content: messageContent
                });

                const response = await fetch('https://api.anthropic.com/v1/messages/count_tokens', {
                    method: 'POST',
                    headers: {
                        'x-api-key': 'YOUR_API_KEY', // Replace with actual key
                        'anthropic-version': '2023-06-01',
                        'anthropic-beta': 'token-counting-2024-11-01,pdfs-2024-09-25',
                        'content-type': 'application/json'
                    },
                    body: JSON.stringify({
                        model: 'claude-3-5-sonnet-20241022',
                        messages: messages
                    })
                });

                if (!response.ok) {
                    throw new Error(`API request failed: ${response.statusText}`);
                }

                const data = await response.json();
                outputDiv.textContent = `Token count: ${data.token_count}`;
                outputDiv.style.display = 'block';
            } catch (error) {
                errorDiv.textContent = error.message;
                errorDiv.style.display = 'block';
            } finally {
                countButton.disabled = false;
            }
        }
    </script>
</body>
</html>

I've created a token counter tool with the following features:

  1. File Upload UI:

    • A drag-and-drop area that accepts multiple files
    • Click to select files using the standard file picker
    • Support for both images and PDFs
    • Visual feedback when dragging files over the drop zone
  2. File Management:

    • List of attached files with their names and types
    • Ability to remove individual files
    • Validation to ensure only supported file types are accepted
  3. Message Construction:

    • Properly formats files as either image or document content types
    • Correctly includes base64-encoded file data
    • Maintains the original system prompt and user message functionality
  4. API Integration:

    • Uses the updated token counting endpoint
    • Includes necessary headers for both PDF and image support
    • Properly structures the messages array with mixed content types

To use the tool:

  1. Enter any system prompt (optional)
  2. Enter your user message (optional)
  3. Drag and drop files onto the drop area or click to select them
  4. Click "Count Tokens" to get the total token count

Note: You'll need to replace 'YOUR_API_KEY' in the code with an actual Anthropic API key before using the tool.

Would you like me to make any adjustments to the implementation?

human (Nov 2, 2024, 11:17 AM)

don't use lodash, no dependencies please - also add box-sizing border box

assistant (Nov 2, 2024, 11:17 AM)

I'll remove the lodash dependency and add box-sizing. Here's the updated version:

rewrite Token Counter with File Attachments

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Claude Token Counter</title>
    <style>
        *, *::before, *::after {
            box-sizing: border-box;
        }

        body {
            font-family: system-ui, -apple-system, sans-serif;
            max-width: 800px;
            margin: 0 auto;
            padding: 20px;
            line-height: 1.6;
        }

        .input-group {
            margin-bottom: 20px;
        }

        textarea {
            width: 100%;
            min-height: 100px;
            margin: 8px 0;
            padding: 8px;
            border: 1px solid #ccc;
            border-radius: 4px;
            font-family: inherit;
            font-size: inherit;
            resize: vertical;
        }

        button {
            background: #2563eb;
            color: white;
            border: none;
            padding: 8px 16px;
            border-radius: 4px;
            cursor: pointer;
            font-size: 16px;
        }

        button:hover {
            background: #1d4ed8;
        }

        button:disabled {
            background: #93c5fd;
            cursor: not-allowed;
        }

        .error {
            color: #dc2626;
            margin: 8px 0;
            display: none;
        }

        .output {
            margin: 16px 0;
            padding: 16px;
            background: #f1f5f9;
            border-radius: 4px;
            display: none;
        }

        .file-drop-area {
            border: 2px dashed #ccc;
            border-radius: 4px;
            padding: 20px;
            text-align: center;
            margin: 20px 0;
            background: #f8fafc;
        }

        .file-drop-area.drag-over {
            background: #e2e8f0;
            border-color: #2563eb;
        }

        .file-list {
            margin: 16px 0;
        }

        .file-item {
            display: flex;
            align-items: center;
            justify-content: space-between;
            padding: 8px;
            background: #f1f5f9;
            border-radius: 4px;
            margin: 4px 0;
        }

        .file-item button {
            background: #dc2626;
            padding: 4px 8px;
            font-size: 14px;
        }

        .file-item button:hover {
            background: #b91c1c;
        }
    </style>
</head>
<body>
    <h1>Claude Token Counter</h1>

    <div class="input-group">
        <label for="system">System prompt:</label>
        <textarea id="system"></textarea>
    </div>

    <div class="input-group">
        <label for="user">User message:</label>
        <textarea id="user"></textarea>
    </div>

    <div class="file-drop-area" id="dropArea">
        <p>Drag and drop files here or click to select</p>
        <input type="file" id="fileInput" multiple style="display: none">
        <div class="file-list" id="fileList"></div>
    </div>

    <button onclick="countTokens()" id="countButton">Count Tokens</button>

    <div id="error" class="error"></div>
    <div id="output" class="output"></div>

    <script>
        let attachedFiles = [];

        // File handling functions
        function handleFiles(files) {
            for (const file of files) {
                if (isValidFile(file)) {
                    readAndStoreFile(file);
                } else {
                    showError(`Unsupported file type: ${file.type}`);
                }
            }
        }

        function isValidFile(file) {
            return file.type.startsWith('image/') || file.type === 'application/pdf';
        }

        function readAndStoreFile(file) {
            const reader = new FileReader();
            reader.onload = function(e) {
                const base64Data = e.target.result.split(',')[1];
                attachedFiles.push({
                    name: file.name,
                    type: file.type,
                    data: base64Data
                });
                updateFileList();
            };
            reader.readAsDataURL(file);
        }

        function updateFileList() {
            const fileList = document.getElementById('fileList');
            fileList.innerHTML = attachedFiles.map((file, index) => `
                <div class="file-item">
                    <span>${file.name} (${file.type})</span>
                    <button onclick="removeFile(${index})">Remove</button>
                </div>
            `).join('');
        }

        function removeFile(index) {
            attachedFiles.splice(index, 1);
            updateFileList();
        }

        function showError(message) {
            const errorDiv = document.getElementById('error');
            errorDiv.textContent = message;
            errorDiv.style.display = 'block';
        }

        // Drag and drop handling
        const dropArea = document.getElementById('dropArea');
        const fileInput = document.getElementById('fileInput');

        dropArea.addEventListener('click', () => fileInput.click());
        fileInput.addEventListener('change', (e) => handleFiles(e.target.files));

        dropArea.addEventListener('dragover', (e) => {
            e.preventDefault();
            dropArea.classList.add('drag-over');
        });

        dropArea.addEventListener('dragleave', () => {
            dropArea.classList.remove('drag-over');
        });

        dropArea.addEventListener('drop', (e) => {
            e.preventDefault();
            dropArea.classList.remove('drag-over');
            handleFiles(e.dataTransfer.files);
        });

        // Token counting function
        async function countTokens() {
            const systemPrompt = document.getElementById('system').value;
            const userMessage = document.getElementById('user').value;
            const errorDiv = document.getElementById('error');
            const outputDiv = document.getElementById('output');
            const countButton = document.getElementById('countButton');

            errorDiv.style.display = 'none';
            outputDiv.style.display = 'none';
            countButton.disabled = true;

            try {
                // Construct messages array with files
                const messageContent = [];
                
                // Add files first
                for (const file of attachedFiles) {
                    const fileContent = {
                        type: file.type.startsWith('image/') ? 'image' : 'document',
                        source: {
                            type: 'base64',
                            media_type: file.type,
                            data: file.data
                        }
                    };
                    messageContent.push(fileContent);
                }

                // Add text content
                if (userMessage) {
                    messageContent.push({
                        type: 'text',
                        text: userMessage
                    });
                }

                const messages = [];
                
                // Add system message if present
                if (systemPrompt) {
                    messages.push({
                        role: 'system',
                        content: systemPrompt
                    });
                }

                // Add user message
                messages.push({
                    role: 'user',
                    content: messageContent
                });

                const response = await fetch('https://api.anthropic.com/v1/messages/count_tokens', {
                    method: 'POST',
                    headers: {
                        'x-api-key': 'YOUR_API_KEY', // Replace with actual key
                        'anthropic-version': '2023-06-01',
                        'anthropic-beta': 'token-counting-2024-11-01,pdfs-2024-09-25',
                        'content-type': 'application/json'
                    },
                    body: JSON.stringify({
                        model: 'claude-3-5-sonnet-20241022',
                        messages: messages
                    })
                });

                if (!response.ok) {
                    throw new Error(`API request failed: ${response.statusText}`);
                }

                const data = await response.json();
                outputDiv.textContent = `Token count: ${data.token_count}`;
                outputDiv.style.display = 'block';
            } catch (error) {
                errorDiv.textContent = error.message;
                errorDiv.style.display = 'block';
            } finally {
                countButton.disabled = false;
            }
        }
    </script>
</body>
</html>

Changes made:

  1. Removed the lodash script tag and dependency
  2. Added box-sizing: border-box for all elements using the universal selector
  3. Added a missing showError function that was previously undefined
  4. Cleaned up the code formatting slightly

The functionality remains the same, but the page will now render more predictably with border-box sizing and has no external dependencies. Would you like me to make any other changes?

human (Nov 2, 2024, 11:20 AM)

Here's the original code that worked, modify that to add the new features (no extra dependencies):

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Token Counter</title>
  <style>
  * {
    box-sizing: border-box;
  }

  body {
    font-family: Helvetica, Arial, sans-serif;
    line-height: 1.6;
    max-width: 800px;
    margin: 0 auto;
    padding: 20px;
  }

  textarea {
    width: 100%;
    min-height: 200px;
    margin: 10px 0;
    padding: 10px;
    font-size: 16px;
    font-family: monospace;
    border: 1px solid #ccc;
    border-radius: 4px;
  }

  button {
    background: #0066ff;
    color: white;
    border: none;
    padding: 10px 20px;
    border-radius: 4px;
    cursor: pointer;
    font-size: 16px;
  }

  button:disabled {
    background: #cccccc;
    cursor: not-allowed;
  }

  .output {
    background: #f5f5f5;
    padding: 20px;
    border-radius: 4px;
    margin-top: 20px;
    white-space: pre-wrap;
    font-family: monospace;
  }

  .error {
    color: #ff0000;
    margin: 10px 0;
  }

  .input-group {
    margin-bottom: 20px;
  }

  label {
    display: block;
    margin-bottom: 5px;
    font-weight: bold;
  }
  </style>
</head>
<body>
  <h1>Claude Token Counter</h1>
  
  <div class="input-group">
    <label for="system">System prompt:</label>
    <textarea id="system" placeholder="Enter system prompt (optional)"></textarea>
  </div>

  <div class="input-group">
    <label for="content">User message:</label>
    <textarea id="content" placeholder="Enter message content"></textarea>
  </div>

  <button id="count">Count Tokens</button>
  <div id="error" class="error"></div>
  <div id="output" class="output"></div>

<script type="module">
const API_URL = 'https://api.anthropic.com/v1/messages/count_tokens'
const MODEL = 'claude-3-5-sonnet-20241022'

function getApiKey() {
  let key = localStorage.getItem('ANTHROPIC_API_KEY')
  if (!key) {
    key = prompt('Please enter your Anthropic API key:')
    if (key) {
      localStorage.setItem('ANTHROPIC_API_KEY', key)
    }
  }
  return key
}

async function countTokens(system, content) {
  const apiKey = getApiKey()
  if (!apiKey) {
    throw new Error('API key is required')
  }

  const response = await fetch(API_URL, {
    method: 'POST',
    headers: {
      'x-api-key': apiKey,
      'content-type': 'application/json',
      'anthropic-version': '2023-06-01',
      'anthropic-beta': 'token-counting-2024-11-01',
      'anthropic-dangerous-direct-browser-access': 'true'
    },
    body: JSON.stringify({
      model: MODEL,
      system: system || undefined,
      messages: [{
        role: 'user',
        content: content
      }]
    })
  })

  if (!response.ok) {
    const error = await response.text()
    throw new Error(`API error: ${error}`)
  }

  return response.json()
}

const systemInput = document.getElementById('system')
const contentInput = document.getElementById('content')
const countButton = document.getElementById('count')
const errorDiv = document.getElementById('error')
const outputDiv = document.getElementById('output')

countButton.addEventListener('click', async () => {
  errorDiv.textContent = ''
  outputDiv.textContent = 'Counting tokens...'
  countButton.disabled = true

  try {
    const result = await countTokens(
      systemInput.value.trim(),
      contentInput.value.trim()
    )
    outputDiv.textContent = JSON.stringify(result, null, 2)
  } catch (error) {
    errorDiv.textContent = error.message
    outputDiv.textContent = ''
  } finally {
    countButton.disabled = false
  }
})
</script>
</body>
</html>

assistant (Nov 2, 2024, 11:20 AM)

I'll integrate the file attachment functionality into the original codebase while maintaining its simplicity:

create Token Counter with File Attachments

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Token Counter</title>
  <style>
  * {
    box-sizing: border-box;
  }

  body {
    font-family: Helvetica, Arial, sans-serif;
    line-height: 1.6;
    max-width: 800px;
    margin: 0 auto;
    padding: 20px;
  }

  textarea {
    width: 100%;
    min-height: 200px;
    margin: 10px 0;
    padding: 10px;
    font-size: 16px;
    font-family: monospace;
    border: 1px solid #ccc;
    border-radius: 4px;
  }

  button {
    background: #0066ff;
    color: white;
    border: none;
    padding: 10px 20px;
    border-radius: 4px;
    cursor: pointer;
    font-size: 16px;
  }

  button:disabled {
    background: #cccccc;
    cursor: not-allowed;
  }

  .output {
    background: #f5f5f5;
    padding: 20px;
    border-radius: 4px;
    margin-top: 20px;
    white-space: pre-wrap;
    font-family: monospace;
  }

  .error {
    color: #ff0000;
    margin: 10px 0;
  }

  .input-group {
    margin-bottom: 20px;
  }

  label {
    display: block;
    margin-bottom: 5px;
    font-weight: bold;
  }

  .file-drop-area {
    border: 2px dashed #ccc;
    border-radius: 4px;
    padding: 20px;
    text-align: center;
    margin: 20px 0;
    background: #f8fafc;
    cursor: pointer;
  }

  .file-drop-area.drag-over {
    background: #e2e8f0;
    border-color: #0066ff;
  }

  .file-list {
    margin: 10px 0;
  }

  .file-item {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 8px;
    background: #f5f5f5;
    border-radius: 4px;
    margin: 4px 0;
  }

  .file-item button {
    background: #ff3333;
    padding: 4px 8px;
    font-size: 14px;
  }

  .file-item button:hover {
    background: #cc0000;
  }
  </style>
</head>
<body>
  <h1>Claude Token Counter</h1>
  
  <div class="input-group">
    <label for="system">System prompt:</label>
    <textarea id="system" placeholder="Enter system prompt (optional)"></textarea>
  </div>

  <div class="input-group">
    <label for="content">User message:</label>
    <textarea id="content" placeholder="Enter message content"></textarea>
  </div>

  <div class="file-drop-area" id="dropArea">
    <p>Drag and drop files here or click to select</p>
    <input type="file" id="fileInput" multiple style="display: none">
    <div class="file-list" id="fileList"></div>
  </div>

  <button id="count">Count Tokens</button>
  <div id="error" class="error"></div>
  <div id="output" class="output"></div>

<script type="module">
const API_URL = 'https://api.anthropic.com/v1/messages/count_tokens'
const MODEL = 'claude-3-5-sonnet-20241022'

let attachedFiles = []

function getApiKey() {
  let key = localStorage.getItem('ANTHROPIC_API_KEY')
  if (!key) {
    key = prompt('Please enter your Anthropic API key:')
    if (key) {
      localStorage.setItem('ANTHROPIC_API_KEY', key)
    }
  }
  return key
}

function handleFiles(files) {
  for (const file of files) {
    if (file.type.startsWith('image/') || file.type === 'application/pdf') {
      readAndStoreFile(file)
    } else {
      errorDiv.textContent = `Unsupported file type: ${file.type}`
    }
  }
}

function readAndStoreFile(file) {
  const reader = new FileReader()
  reader.onload = function(e) {
    const base64Data = e.target.result.split(',')[1]
    attachedFiles.push({
      name: file.name,
      type: file.type,
      data: base64Data
    })
    updateFileList()
  }
  reader.readAsDataURL(file)
}

function updateFileList() {
  const fileList = document.getElementById('fileList')
  fileList.innerHTML = attachedFiles.map((file, index) => `
    <div class="file-item">
      <span>${file.name} (${file.type})</span>
      <button onclick="removeFile(${index})">Remove</button>
    </div>
  `).join('')
}

window.removeFile = function(index) {
  attachedFiles.splice(index, 1)
  updateFileList()
}

async function countTokens(system, content) {
  const apiKey = getApiKey()
  if (!apiKey) {
    throw new Error('API key is required')
  }

  const messageContent = []
  
  // Add files first
  for (const file of attachedFiles) {
    messageContent.push({
      type: file.type.startsWith('image/') ? 'image' : 'document',
      source: {
        type: 'base64',
        media_type: file.type,
        data: file.data
      }
    })
  }

  // Add text content if present
  if (content.trim()) {
    messageContent.push({
      type: 'text',
      text: content
    })
  }

  const messages = [{
    role: 'user',
    content: messageContent
  }]

  if (system) {
    messages.unshift({
      role: 'system',
      content: system
    })
  }

  const response = await fetch(API_URL, {
    method: 'POST',
    headers: {
      'x-api-key': apiKey,
      'content-type': 'application/json',
      'anthropic-version': '2023-06-01',
      'anthropic-beta': 'token-counting-2024-11-01,pdfs-2024-09-25',
      'anthropic-dangerous-direct-browser-access': 'true'
    },
    body: JSON.stringify({
      model: MODEL,
      messages
    })
  })

  if (!response.ok) {
    const error = await response.text()
    throw new Error(`API error: ${error}`)
  }

  return response.json()
}

const systemInput = document.getElementById('system')
const contentInput = document.getElementById('content')
const countButton = document.getElementById('count')
const errorDiv = document.getElementById('error')
const outputDiv = document.getElementById('output')
const dropArea = document.getElementById('dropArea')
const fileInput = document.getElementById('fileInput')

// File upload handling
dropArea.addEventListener('click', () => fileInput.click())
fileInput.addEventListener('change', (e) => handleFiles(e.target.files))

dropArea.addEventListener('dragover', (e) => {
  e.preventDefault()
  dropArea.classList.add('drag-over')
})

dropArea.addEventListener('dragleave', () => {
  dropArea.classList.remove('drag-over')
})

dropArea.addEventListener('drop', (e) => {
  e.preventDefault()
  dropArea.classList.remove('drag-over')
  handleFiles(e.dataTransfer.files)
})

countButton.addEventListener('click', async () => {
  errorDiv.textContent = ''
  outputDiv.textContent = 'Counting tokens...'
  countButton.disabled = true

  try {
    const result = await countTokens(
      systemInput.value.trim(),
      contentInput.value.trim()
    )
    outputDiv.textContent = JSON.stringify(result, null, 2)
  } catch (error) {
    errorDiv.textContent = error.message
    outputDiv.textContent = ''
  } finally {
    countButton.disabled = false
  }
})
</script>
</body>
</html>

Key changes made to the original code:

  1. Added file upload UI with drag-and-drop support
  2. Added file handling functionality while maintaining the original code structure
  3. Modified the token counting function to support mixed content (files + text)
  4. Added the PDF beta header when making API calls
  5. Updated the message structure to support the new format with files
  6. Kept the original API key handling and error handling logic
  7. Maintained the original styling while adding new styles for file upload UI

The tool now supports:

  • Original text-based token counting
  • Drag and drop file uploads
  • Multiple file attachments
  • PDF and image file support
  • File removal
  • Proper base64 encoding of files

Would you like me to make any adjustments to this implementation?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment