Building AI-Powered Applications with Cloudflare Workers

Learn how to leverage Cloudflare's developer ecosystem to create powerful AI
applications using Workers and Workers AI.

About This Tutorial

This tutorial guides you through building three different AI-powered applications using Cloudflare Workers and Workers AI. You'll learn how to:

Note: Cloudflare's development ecosystem offers a powerful set of tools for building applications at the edge. Workers allows you to run serverless functions globally, while Workers AI brings machine learning capabilities directly to your applications without the need for external APIs or services.

This tutorial was created to help developers get started with Cloudflare's AI tools quickly. The examples provided here demonstrate how easy it is to build sophisticated AI applications without managing complex infrastructure or dependencies. Whether you're building a personal project or enterprise application, the techniques shown here will help you leverage Cloudflare's global network for fast, reliable AI-powered applications.

This tutorial is entirely dashboard focused to welcome coders at all levels.

Prerequisites

Before you begin, you'll need:

Tutorial Applications

Choose one of the following applications to build:

App 1: AI Text Summarizer (Beginner Level)

This simple application allows users to input text and receive an AI-generated summary. You have probably seen this sort of app in the wild.

What You'll Learn

1 Create a new Worker from the Cloudflare Dashboard

  1. Log in to the Cloudflare Dashboard at dash.cloudflare.com
  2. Click on "Workers & Pages" in the left sidebar
  3. Click "Create"
  4. Select "Worker" and click "Hello World"
  5. Give your Worker a name (e.g., "text-summarizer")
  6. Click "Deploy" to get started with a basic Worker

2 Set Up Workers AI

  1. Once your Worker is created, go to "Explore Resources" > "Bindings"
  2. Under "Bindings", click "Add" and choose "Workers AI" from the list
  3. Set the Variable name to "AI" (this matches what's used in the code)
  4. Click "Deploy"

3 Implement the Application Code

  1. In the dashboard, go to your Worker and click "Edit code". You can also click into this from the top right of the previous screen
  2. Replace the existing code with the following:
export default {
            async fetch(request, env) {
              // Check if AI binding exists
              if (!env.AI) {
                return new Response("Error: AI binding is not configured. Please add an AI binding named 'AI' in the Cloudflare dashboard.", {
                  status: 500,
                  headers: { "Content-Type": "text/plain" }
                });
              }
           
              if (request.method === "POST") {
                try {
                  // Get the text from the request
                  const requestText = await request.text();
                  let text;
                   
                  try {
                    // Try to parse as JSON
                    const jsonData = JSON.parse(requestText);
                    text = jsonData.text;
                  } catch (e) {
                    // If not valid JSON, use the text directly
                    text = requestText;
                  }
           
                  if (!text || text.trim().length === 0) {
                    return new Response(JSON.stringify({ error: "No text provided" }), {
                      status: 400,
                      headers: { "Content-Type": "application/json" }
                    });
                  }
           
                  try {
                    // Generate a concise summary with token efficiency in mind
                    const summary = await env.AI.run("@cf/mistral/mistral-7b-instruct-v0.1", {
                      messages: [
                        {
                          role: "user",
                          content: `Summarise the following text very concisely. Your summary should be no longer than 3-4 sentences and focus only on the most important points:
           
          ${text}`
                        }
                      ],
                      max_tokens: 256  // Keep the default to preserve tokens
                    });
           
                    return new Response(JSON.stringify({
                      summary: summary.response
                    }), {
                      headers: { "Content-Type": "application/json" }
                    });
                  } catch (aiError) {
                    // Detailed AI error
                    return new Response(JSON.stringify({
                      error: "AI Error",
                      details: aiError.message || "Unknown AI error",
                      stack: aiError.stack
                    }), {
                      status: 500,
                      headers: { "Content-Type": "application/json" }
                    });
                  }
                } catch (error) {
                  // Generic error
                  return new Response(JSON.stringify({
                    error: "Server Error",
                    message: error.message || "Unknown error",
                    stack: error.stack
                  }), {
                    status: 500,
                    headers: { "Content-Type": "application/json" }
                  });
                }
              }
           
              // Return a simple HTML form for GET requests
              return new Response(`
                <!DOCTYPE html>
                <html>
                  <head>
                    <title>Text Summariser</title>
                    <meta charset="UTF-8">
                    <meta name="viewport" content="width=device-width, initial-scale=1.0">
                    <style>
                      body {
                        font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
                        max-width: 800px;
                        margin: 0 auto;
                        padding: 20px;
                        background-color: #f9f9fa;
                        color: #333;
                        line-height: 1.6;
                      }
                      h1, h2 {
                        color: #f48120;
                        margin-bottom: 15px;
                      }
                      textarea {
                        width: 100%;
                        height: 200px;
                        margin-bottom: 15px;
                        padding: 12px;
                        border: 1px solid #ddd;
                        border-radius: 6px;
                        font-family: inherit;
                        font-size: 16px;
                      }
                      button {
                        background-color: #f48120;
                        color: white;
                        border: none;
                        padding: 10px 18px;
                        border-radius: 6px;
                        font-size: 16px;
                        cursor: pointer;
                        transition: background-color 0.2s;
                      }
                      button:hover {
                        background-color: #e67510;
                      }
                      button:disabled {
                        background-color: #f4b583;
                        cursor: not-allowed;
                      }
                      #summary {
                        margin-top: 20px;
                        padding: 15px;
                        border: 1px solid #e4e4e7;
                        min-height: 100px;
                        background-color: white;
                        border-radius: 6px;
                        box-shadow: 0 2px 4px rgba(0,0,0,0.05);
                      }
                      .loading { color: #666; font-style: italic; }
                      .error { color: #e53e3e; background-color: #fee; padding: 10px; border-radius: 6px; }
                      .card {
                        background-color: white;
                        padding: 25px;
                        border-radius: 8px;
                        box-shadow: 0 2px 8px rgba(0,0,0,0.1);
                        margin-bottom: 20px;
                      }
                      p {
                        margin-bottom: 15px;
                      }
                    </style>
                  </head>
                  <body>
                    <div class="card">
                      <h1>Text Summariser</h1>
                      <p>Enter text below and click "Summarise" to get an AI-generated summary.</p>
                       
                      <textarea id="text" placeholder="Enter text to summarise..."></textarea>
                      <button id="summariseBtn" onclick="summarise()">Summarise</button>
                    </div>
                     
                    <div class="card">
                      <h2>Summary</h2>
                      <div id="summary"></div>
                    </div>
                     
                    <script>
                      async function summarise() {
                        const text = document.getElementById('text').value;
                        if (!text) return alert('Please enter some text');
                         
                        const summaryEl = document.getElementById('summary');
                        const button = document.getElementById('summariseBtn');
                         
                        summaryEl.innerHTML = '<p class="loading">Generating summary...</p>';
                        button.disabled = true;
                         
                        try {
                          const response = await fetch(window.location.href, {
                            method: 'POST',
                            headers: { 'Content-Type': 'application/json' },
                            body: JSON.stringify({ text })
                          });
                           
                          const data = await response.json();
                           
                          if (data.error) {
                            summaryEl.innerHTML = '<p class="error">Error: ' + (data.details || data.message || data.error) + '</p>';
                            console.error('Full error:', data);
                          } else {
                            summaryEl.innerHTML = data.summary;
                          }
                        } catch (error) {
                          summaryEl.innerHTML = '<p class="error">Error: ' + error.message + '</p>';
                          console.error(error);
                        } finally {
                          button.disabled = false;
                        }
                      }
                    </script>
                  </body>
                </html>
              `, {
                headers: { "Content-Type": "text/html" }
              });
            }
          };
  1. Click "Save and Deploy"

4 Test Your Application

  1. In the Cloudflare dashboard, click "View" to open your deployed Worker
  2. Enter some text in the textarea and click "Summarise". Pick random Wikipedia entries or text from the British Council.
  3. You should receive an AI-generated summary of your text

5 Iterate

  1. In the Cloudflare dashboard, click "View" to open your deployed Worker
  2. Enter the code to change the model from Mistral to DeepSeek. Deploy this
  3. Test and validate to see Deepseek specific response.

6 Optional

  1. Deploy this to a hostname in a domain you own

Result: https://text-summary.chattylamb.workers.dev/

Application 2: Multilingual Image Captioner (Intermediate Level)

This application allows users to upload an image and get AI-generated captions in multiple languages.

What You'll Learn

1 Create a new Worker from the Cloudflare Dashboard

  1. Log in to the Cloudflare Dashboard at dash.cloudflare.com
  2. Click on "Workers & Pages" in the left sidebar
  3. Click "Create"
  4. Select "Worker" and click "Hello World"
  5. Give your Worker a name (e.g., "image-captioner")
  6. Click "Deploy" to get started with a basic Worker

2 Set Up Workers AI

  1. Once your Worker is created, go to "Explore Resources" > "Bindings"
  2. Under "Bindings", click "Add" and choose "Workers AI" from the list
  3. Set the Variable name to "AI" (this matches what's used in the code)
  4. Click "Deploy"

3 Implement the Application Code

  1. In the dashboard, go to your Worker and click "Edit code". You can also click into this from the top right of the previous screen
  2. Replace the existing code with the following:
  1. Click "Save and Deploy"

4 Test Your Application

  1. In the Cloudflare dashboard, click "View" to open your deployed Worker
  2. Upload an image and verify that you receive captions in multiple languages
  3. Experiment with different images to test the model's capabilities

5 Optional

  1. Deploy this to a hostname in a domain you own

Result: https://image-captioner.chattylamb.workers.dev/

Application 3: Document Q&A with Vector Search (Advanced Level)

This application allows users to upload text documents, process them with AI, and ask questions about the content. Think Google's NotebookLM.

What You'll Learn

1 Create Vectorize Index via API

Before we create the Worker, we need to set up a Vectorize index using the Cloudflare API as there's no dashboard option for this.

  1. First, you'll need your Cloudflare account ID and a valid API token with the necessary permissions to create Vectorize indexes.
  2. Execute the following API request to create your index (this can be done in Python, with cURL, or any API client):
Show Code
# cURL example
  curl -X POST "https://api.cloudflare.com/client/v4/accounts/{account_id}/vectorize/indexes" \
  -H "Authorization: Bearer {api_token}" \
  -H "Content-Type: application/json" \
  -d '{
   "name": "document-qa-index",
   "dimensions": 1536,
   "metric": "cosine"
  }'
  
  # Python example using requests
  import requests
  
  url = f"https://api.cloudflare.com/client/v4/accounts/{account_id}/vectorize/indexes"
  headers = {
  "Authorization": f"Bearer {api_token}",
  "Content-Type": "application/json"
  }
  payload = {
  "name": "document-qa-index",
  "dimensions": 1536,
  "metric": "cosine"
  }
  
  response = requests.post(url, headers=headers, json=payload)
  print(response.json())

3 Create a new Worker for the Document Q&A

  1. Go to "Workers & Pages" > "Create"
  2. Name it "document-qa" and click "Deploy"

4 Set up Required Bindings

  1. Add an AI Binding with variable name "AI"
  2. Add a Vectorize Binding with:
    • Variable name: "DOC_VECTORIZE"
    • Index: "document-embeddings" (the name you created via API)
  3. Deploy

3 Implement the Application Code

  1. In the dashboard, go to your Worker and click "Edit code". You can also click into this from the top right of the previous screen
  2. Replace the existing code with the following:
Show Code
export default {
    async fetch(request, env, ctx) {
      // Check required bindings
      if (!env.AI) {
        return new Response("Error: AI binding is not configured", { status: 500 });
      }
      if (!env.DOC_VECTORIZE) {
        return new Response("Error: Vectorize binding is not configured", { status: 500 });
      }
   
      // CORS handling
      if (request.method === "OPTIONS") {
        return new Response(null, {
          headers: {
            "Access-Control-Allow-Origin": "*",
            "Access-Control-Allow-Methods": "POST, GET, OPTIONS",
            "Access-Control-Allow-Headers": "Content-Type"
          }
        });
      }
   
      // Route requests
      const url = new URL(request.url);
       
      if (url.pathname === "/api/upload" && request.method === "POST") {
        return await handleUpload(request, env);
      }
       
      if (url.pathname === "/api/query" && request.method === "POST") {
        return await handleQuery(request, env);
      }
   
      // Serve UI
      return new Response(getHtml(url), {
        headers: {
          "Content-Type": "text/html",
          "Access-Control-Allow-Origin": "*"
        }
      });
    }
  };
   
  // Handle document upload
  async function handleUpload(request, env) {
    try {
      // Get the content type
      const contentType = request.headers.get("Content-Type") || "";
   
      // Process data based on content type
      let documentTitle = "Untitled";
      let documentText = "";
   
      if (contentType.includes("multipart/form-data")) {
        const formData = await request.formData();
        const file = formData.get("documentFile");
        documentTitle = formData.get("documentTitle") || documentTitle;
   
        if (!file || !(file instanceof File)) {
          return jsonResponse({ error: "No file uploaded" }, 400);
        }
   
        documentText = await file.text();
         
        // Use filename as title if not provided
        if (documentTitle === "Untitled" && file.name) {
          documentTitle = file.name.replace(/\.[^/.]+$/, "");
        }
      } else {
        return jsonResponse({ error: "Invalid request format" }, 400);
      }
   
      // Validate document text
      if (!documentText || documentText.trim().length === 0) {
        return jsonResponse({ error: "Document is empty" }, 400);
      }
   
      // Split text into chunks
      const chunks = splitText(documentText, 4000);
      if (chunks.length === 0) {
        return jsonResponse({ error: "Failed to process document" }, 500);
      }
   
      // Generate unique ID
      const documentId = crypto.randomUUID();
   
      // Process chunks and generate embeddings
      const entries = [];
      for (const [index, chunk] of chunks.entries()) {
        try {
          // Trim chunk and ensure it meets minimum length
          const trimmedChunk = chunk.trim();
          if (trimmedChunk.length < 10) {  // Increased minimum length
            console.log(`Skipping very short chunk ${index}`);
            continue;
          }
   
          // Debug logging for each chunk
          console.log(`Processing chunk ${index}:`, {
            length: trimmedChunk.length,
            preview: trimmedChunk.substring(0, 100)
          });
   
          // Use batch embedding for single text to ensure correct input
          const response = await env.AI.run('@cf/baai/bge-large-en-v1.5', {
            text: [trimmedChunk]
          });
   
          // Detailed logging of embedding response
          console.log("Full embedding response:", JSON.stringify(response, null, 2));
   
          // More robust embedding extraction
          const embedding = response?.data?.[0];
   
          // Comprehensive embedding validation
          if (!embedding) {
            console.error(`No embedding generated for chunk ${index}`, {
              response: JSON.stringify(response),
              chunkLength: trimmedChunk.length,
              chunkPreview: trimmedChunk.substring(0, 100)
            });
            continue;
          }
   
          if (!Array.isArray(embedding) || embedding.length !== 1024) {
            console.error(`Invalid embedding shape for chunk ${index}`, {
              embeddingType: typeof embedding,
              embeddingLength: embedding.length
            });
            continue;
          }
   
          entries.push({
            id: `${documentId}-${index}`,
            values: embedding,
            metadata: {
              documentId,
              documentTitle,
              chunkIndex: index,
              text: trimmedChunk
            }
          });
   
          // Log successful embedding
          console.log(`Successfully processed chunk ${index}`);
        } catch (embeddingError) {
          console.error(`Detailed embedding error for chunk ${index}:`, embeddingError);
        }
      }
   
      // Comprehensive error reporting
      if (entries.length === 0) {
        console.error("Embedding generation failed details:", {
          totalChunks: chunks.length,
          documentTitle,
          documentTextLength: documentText.length
        });
   
        return jsonResponse({
          error: "Failed to generate embeddings. Please check console logs for details.",
          details: {
            totalChunks: chunks.length,
            documentTitle,
            documentTextLength: documentText.length
          }
        }, 500);
      }
   
      // Store in Vectorize
      await env.DOC_VECTORIZE.upsert(entries);
   
      // Return success
      return jsonResponse({
        success: true,
        documentId,
        documentTitle,
        chunkCount: entries.length
      });
   
    } catch (error) {
      console.error("Comprehensive document upload error:", error);
      return jsonResponse({
        error: error.message,
        stack: error.stack
      }, 500);
    }
  }
   
  // Handle queries
  async function handleQuery(request, env) {
    try {
      const data = await request.json();
      const query = data.query;
      const documentId = data.documentId;
   
      if (!query || query.trim().length === 0) {
        return jsonResponse({ error: "Query cannot be empty" }, 400);
      }
   
      // Generate embedding for query
      let queryEmbedding;
      try {
        queryEmbedding = await env.AI.run('@cf/baai/bge-large-en-v1.5', {
          text: [query]
        });
   
        // Detailed logging of embedding response
        console.log('Embedding Response:', JSON.stringify(queryEmbedding, null, 2));
   
        // Extract and validate embedding
        const embedding = queryEmbedding?.data?.[0];
   
        if (!embedding || !Array.isArray(embedding)) {
          console.error('Invalid embedding response', {
            responseType: typeof queryEmbedding,
            dataType: typeof queryEmbedding?.data,
            embeddingType: typeof embedding
          });
          return jsonResponse({
            error: "Failed to generate valid embedding",
            details: {
              responseType: typeof queryEmbedding,
              dataType: typeof queryEmbedding?.data
            }
          }, 500);
        }
   
        if (embedding.length !== 1024) {
          console.error('Incorrect embedding dimensions', {
            expectedDimensions: 1024,
            actualDimensions: embedding.length
          });
          return jsonResponse({
            error: `Incorrect embedding dimensions. Expected 1024, got ${embedding.length}`,
          }, 500);
        }
   
        // Search Vectorize with validated embedding
        const filter = documentId ? { documentId } : undefined;
        const searchResults = await env.DOC_VECTORIZE.query(embedding, {
          topK: 5,
          filter,
          returnMetadata: true
        });
   
        if (!searchResults.matches || searchResults.matches.length === 0) {
          return jsonResponse({
            answer: "I couldn't find any relevant information in the documents.",
            sources: []
          });
        }
   
        // Prepare context from matches
        const context = searchResults.matches.map(m => m.metadata.text).join("\n\n");
   
        // Generate answer with AI
        const result = await env.AI.run('@cf/mistral/mistral-7b-instruct-v0.1', {
          messages: [
            {
              role: "system",
              content: "You are a helpful assistant answering questions based on document information. Provide concise and accurate answers based only on the context provided."
            },
            {
              role: "user",
              content: `CONTEXT:
  ${context}
   
  QUESTION:
  ${query}
   
  INSTRUCTIONS:
  - Answer based only on the context provided
  - If the context lacks sufficient information, briefly mention this
  - Be concise (1-3 sentences)
  - Don't reference the context directly`
            }
          ],
          max_tokens: 256
        });
   
        return jsonResponse({
          answer: result.response,
          sources: searchResults.matches.map(match => ({
            documentTitle: match.metadata.documentTitle,
            snippet: match.metadata.text.substring(0, 150) + "..."
          }))
        });
   
      } catch (embeddingError) {
        console.error('Embedding Generation Error:', embeddingError);
        return jsonResponse({
          error: "Error generating embedding",
          details: embeddingError.message
        }, 500);
      }
    } catch (error) {
      console.error("Query handling error:", error);
      return jsonResponse({
        error: error.message,
        details: error.stack
      }, 500);
    }
  }
   
  // Split text into chunks for vectorization
  function splitText(text, maxSize) {
    if (!text || text.trim().length === 0) return [];
   
    const paragraphs = text.split(/\n\s*\n/);
    const chunks = [];
    let currentChunk = "";
   
    for (const para of paragraphs) {
      if (para.trim().length === 0) continue;
   
      if (currentChunk.length + para.length > maxSize) {
        if (currentChunk.length > 0) {
          chunks.push(currentChunk);
        }
   
        if (para.length > maxSize) {
          // Split long paragraphs by sentences
          const sentences = para.split(/(?<=[.!?])\s+/);
          currentChunk = "";
   
          for (const sentence of sentences) {
            if (sentence.trim().length === 0) continue;
   
            if (currentChunk.length + sentence.length > maxSize) {
              if (currentChunk.length > 0) {
                chunks.push(currentChunk);
              }
              currentChunk = sentence;
            } else {
              currentChunk += (currentChunk.length > 0 ? " " : "") + sentence;
            }
          }
        } else {
          currentChunk = para;
        }
      } else {
        currentChunk += (currentChunk.length > 0 ? "\n\n" : "") + para;
      }
    }
   
    if (currentChunk.length > 0) {
      chunks.push(currentChunk);
    }
   
    return chunks;
  }
   
  // Helper for JSON responses
  function jsonResponse(data, status = 200) {
    return new Response(JSON.stringify(data), {
      status,
      headers: {
        "Content-Type": "application/json",
        "Access-Control-Allow-Origin": "*"
      }
    });
  }
   
  // UI HTML
  function getHtml(url) {
    const baseUrl = `${url.protocol}//${url.host}`;
     
    return `
  <!DOCTYPE html>
  <html lang="en">
  <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Doc Q&A</title>
    <style>
      body {
        font-family: -apple-system, system-ui, sans-serif;
        line-height: 1.6;
        max-width: 960px;
        margin: 0 auto;
        padding: 20px;
        background: #f5f7f9;
        color: #333;
      }
      h1, h2, h3 {
        color: #f48120;
      }
      .container {
        display: flex;
        flex-wrap: wrap;
        gap: 20px;
        margin-top: 20px;
      }
      .box {
        background: white;
        border-radius: 8px;
        box-shadow: 0 2px 4px rgba(0,0,0,0.1);
        padding: 20px;
        flex: 1;
        min-width: 300px;
      }
      input, textarea, button {
        width: 100%;
        padding: 8px;
        margin-bottom: 10px;
        border: 1px solid #ddd;
        border-radius: 4px;
      }
      button {
        background: #f48120;
        color: white;
        border: none;
        padding: 10px;
        cursor: pointer;
        font-weight: 500;
      }
      button:hover {
        background: #e67510;
      }
      button:disabled {
        background: #f4b583;
      }
      .doc-item {
        padding: 10px;
        background: #f7f7f7;
        border-radius: 4px;
        margin-bottom: 8px;
        display: flex;
        justify-content: space-between;
      }
      .success {
        color: #2e7d32;
        background: #e8f5e9;
        padding: 10px;
        border-radius: 4px;
        margin-bottom: 10px;
      }
      .error {
        color: #c62828;
        background: #ffebee;
        padding: 10px;
        border-radius: 4px;
        margin-bottom: 10px;
      }
      .sources {
        margin-top: 15px;
        padding-top: 10px;
        border-top: 1px solid #eee;
      }
      .source {
        background: #f7f7f7;
        padding: 10px;
        border-radius: 4px;
        margin-bottom: 8px;
      }
      .select-btn {
        background: #4caf50;
        padding: 4px 8px;
        font-size: 12px;
      }
      #query-result {
        display: none;
        margin-top: 20px;
        background: #f7f7f7;
        padding: 15px;
        border-radius: 8px;
      }
    </style>
  </head>
  <body>
    <h1>Document Q&A System</h1>
    <p>Upload documents and ask AI-powered questions about their content.</p>
     
    <div class="container">
      <div class="box">
        <h2>Upload Document</h2>
        <form id="upload-form">
          <input type="text" id="title-input" placeholder="Document title (optional)">
          <input type="file" id="file-input" accept=".txt,.md,.csv,.html,.json,.log">
          <button type="submit">Upload Document</button>
        </form>
        <div id="upload-message"></div>
         
        <h3>My Documents</h3>
        <div id="documents-list"></div>
      </div>
       
      <div class="box">
        <h2>Ask Questions</h2>
        <input type="text" id="query-input" placeholder="Ask a question about your documents...">
        <button id="query-btn">Ask</button>
        <p>* If you have multiple documents, select one or the system will search across all of them.</p>
        <div id="query-result"></div>
      </div>
    </div>
   
    <script>
      // Document storage
      const documents = [];
      let selectedDocId = null;
       
      // Elements
      const uploadForm = document.getElementById('upload-form');
      const titleInput = document.getElementById('title-input');
      const fileInput = document.getElementById('file-input');
      const uploadMessage = document.getElementById('upload-message');
      const documentsList = document.getElementById('documents-list');
      const queryInput = document.getElementById('query-input');
      const queryBtn = document.getElementById('query-btn');
      const queryResult = document.getElementById('query-result');
       
      // Handle document upload
      uploadForm.addEventListener('submit', async function(e) {
        e.preventDefault();
         
        if (!fileInput.files || fileInput.files.length === 0) {
          showMessage('Please select a file to upload', 'error');
          return;
        }
         
        // Prepare form data
        const formData = new FormData();
        formData.append('documentFile', fileInput.files[0]);
        formData.append('documentTitle', titleInput.value);
         
        // Disable form during upload
        const submitBtn = this.querySelector('button');
        submitBtn.disabled = true;
        showMessage('Uploading and processing document...', 'normal');
         
        try {
          const response = await fetch('${baseUrl}/api/upload', {
            method: 'POST',
            body: formData
          });
           
          const result = await response.json();
           
          if (result.error) {
            showMessage(result.error, 'error');
          } else {
            // Add to documents list
            documents.push({
              id: result.documentId,
              title: result.documentTitle,
              chunks: result.chunkCount
            });
             
            // Show success and update UI
            showMessage('Document processed successfully!', 'success');
            updateDocumentsList();
             
            // Reset form
            titleInput.value = '';
            fileInput.value = '';
          }
        } catch (error) {
          showMessage('Error uploading document: ' + error.message, 'error');
        } finally {
          submitBtn.disabled = false;
        }
      });
       
      // Handle question submission
      queryBtn.addEventListener('click', async function() {
        const query = queryInput.value.trim();
         
        if (!query) {
          alert('Please enter a question');
          return;
        }
         
        if (documents.length === 0) {
          alert('Please upload at least one document first');
          return;
        }
         
        // Disable button during processing
        this.disabled = true;
        queryResult.style.display = 'block';
        queryResult.innerHTML = '<p>Looking for answers...</p>';
         
        try {
          const response = await fetch('${baseUrl}/api/query', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({
              query: query,
              documentId: selectedDocId
            })
          });
           
          const result = await response.json();
           
          if (result.error) {
            queryResult.innerHTML = '<div class="error">' + result.error + '</div>';
          } else {
            // Show answer
            let html = '<div>' + result.answer.replace(/\\n/g, '<br>') + '</div>';
             
            // Show sources
            if (result.sources && result.sources.length > 0) {
              html += '<div class="sources"><h3>Sources</h3>';
               
              for (const source of result.sources) {
                html += '<div class="source">' +
                  '<strong>' + source.documentTitle + '</strong><br>' +
                  source.snippet +
                '</div>';
              }
               
              html += '</div>';
            }
             
            queryResult.innerHTML = html;
          }
        } catch (error) {
          queryResult.innerHTML = '<div class="error">Error: ' + error.message + '</div>';
        } finally {
          this.disabled = false;
        }
      });
       
      // Show message
      function showMessage(message, type) {
        if (type === 'error') {
          uploadMessage.innerHTML = '<div class="error">' + message + '</div>';
        } else if (type === 'success') {
          uploadMessage.innerHTML = '<div class="success">' + message + '</div>';
        } else {
          uploadMessage.innerHTML = '<p>' + message + '</p>';
        }
      }
       
      // Update documents list
      function updateDocumentsList() {
        if (documents.length === 0) {
          documentsList.innerHTML = '<p>No documents uploaded yet.</p>';
          return;
        }
         
        let html = '';
         
        documents.forEach(doc => {
          html += '<div class="doc-item">' +
            '<div>' + doc.title + ' (' + doc.chunks + ' chunks)</div>' +
            '<button class="select-btn" data-id="' + doc.id + '">' +
              (selectedDocId === doc.id ? 'Selected' : 'Select') +
            '</button>' +
          '</div>';
        });
         
        documentsList.innerHTML = html;
         
        // Add event listeners
        const selectButtons = documentsList.querySelectorAll('.select-btn');
        selectButtons.forEach(btn => {
          btn.addEventListener('click', function() {
            const docId = this.getAttribute('data-id');
            selectedDocId = selectedDocId === docId ? null : docId;
            updateDocumentsList();
          });
        });
      }
    </script>
  </body>
  </html>`;
  }
  1. Click "Save and Deploy"

6 Test Your Application

  1. Click "View" to open your deployed Worker
  2. In the UI, you can:
    • Paste text documents with a title
    • Process the document which will be chunked and stored in Vectorize
    • Ask questions about the document in the right panel
    • See AI-generated answers based on the document content

7 Optional

  1. Deploy this to a hostname in a domain you own

Result: https://document-qa.chattylamb.workers.dev/

Next Steps

Now that you've built one (or all) of these AI-powered applications with Cloudflare Workers, here are some ways to enhance them:

Cloudflare's ecosystem provides all the tools you need to build, deploy, and scale AI-powered applications at the edge. Explore the Cloudflare Developers documentation to learn more.

Remember: While Workers AI is powerful and convenient, it's important to implement proper safeguards and content moderation in production applications to prevent misuse.