Skip to content

Instantly share code, notes, and snippets.

@simonw
Last active March 20, 2024 16:09
Show Gist options
  • Save simonw/2002e2b56a97053bd9302a34e0b83074 to your computer and use it in GitHub Desktop.
Save simonw/2002e2b56a97053bd9302a34e0b83074 to your computer and use it in GitHub Desktop.

2024-03-07T21:51:11 conversation: 01hrdee0tqve83qsjy44p29d4e

Model: claude-3-opus-20240229

Prompt:

<script type="module">
const textarea = document.getElementById("id_content");
const imageInput = document.getElementById("id_image");
const processingMessage = document.getElementById("processing_message");

async function extractText(pdf) {
  const pdfjs = (
    await import("/-/static-plugins/datasette_extract/pdfjs-dist-4-0-379.js")
  ).default;
  pdfjsLib.GlobalWorkerOptions.workerSrc =
    "/-/static-plugins/datasette_extract/pdf.worker.mjs";

  const loadingTask = pdfjsLib.getDocument(pdf);
  const pdfDocument = await loadingTask.promise;

  const numPages = pdfDocument.numPages;
  let pageTextPromises = [];

  for (let i = 1; i <= numPages; i++) {
    pageTextPromises.push(
      pdfDocument.getPage(i).then((page) => page.getTextContent()),
    );
  }

  const pageTexts = await Promise.all(pageTextPromises);
  return pageTexts
    .map((item) => item.items.map((text) => text.str).join(" "))
    .join("\n\n");
}

function dragOverHandler(event) {
  event.preventDefault();
  event.stopPropagation();
  textarea.classList.add("drag-over");
}

function dragLeaveHandler(event) {
  event.preventDefault();
  event.stopPropagation();
  textarea.classList.remove("drag-over");
}

async function dropHandler(event) {
  event.preventDefault();
  event.stopPropagation();
  textarea.classList.remove("drag-over");

  const files = Array.from(event.dataTransfer.files);
  const imageFile = files.find((file) => file.type.startsWith("image/"));
  const pdfFiles = files.filter((file) => file.type === "application/pdf");
  const textFiles = files.filter((file) => file.type === "text/plain");

  if (imageFile) {
    if (imageFile.type === "image/heic") {
      processingMessage.style.display = "block";
      const heic2any = (
        await import(
          "/-/static-plugins/datasette_extract/heic2any-0.0.4.min.js"
        )
      ).default;
      try {
        const blob = await heic2any({
          blob: imageFile,
          toType: "image/jpeg",
          quality: 0.8,
        });
        const dataTransfer = new DataTransfer();
        dataTransfer.items.add(
          new File([blob], `converted-${imageFile.name}.jpeg`, { type: "image/jpeg" }),
        );
        imageInput.files = dataTransfer.files;
      } catch (error) {
        console.error("Conversion error:", error);
      }
      processingMessage.style.display = "none";
    } else {
      imageInput.files = event.dataTransfer.files;
    }
  }
  if (pdfFiles.length > 0) {
    processingMessage.style.display = "block";
    const pdfTexts = await Promise.all(
      pdfFiles.map(async (file) =>
        extractText(new Uint8Array(await file.arrayBuffer())),
      ),
    );
    textarea.value = pdfTexts.join("\n\n");
    processingMessage.style.display = "none";
  }
  if (textFiles.length > 0) {
    const textContents = await Promise.all(
      textFiles.map((file) => file.text()),
    );
    textarea.value +=
      (textarea.value ? "\n\n" : "") + textContents.join("\n\n");
  }
}

textarea.addEventListener("dragover", dragOverHandler);
textarea.addEventListener("dragleave", dragLeaveHandler);
textarea.addEventListener("drop", dropHandler);
</script>

System:

Make some changes to this code.

When a HEIC image is selected directly using the imageInput file input, it should be converted in the same way with the progress bit showing and then hiding and the converted file set back to that input

When files are dropped on the textarea, PDF and images should use the special handling - but any others should use this code instead:

      const reader = new FileReader();
      reader.onload = (e) => {
        contentToAdd += e.target.result;
        resolve(contentToAdd);
      };
      reader.onerror = (e) => {
        reject(new Error("Failed to read file"));
      };
      reader.readAsText(file);

where resolve should do textarea.value = contents.join("\n\n");

Response:

Here's the updated code with the requested changes:

<script type="module">
const textarea = document.getElementById("id_content");
const imageInput = document.getElementById("id_image");
const processingMessage = document.getElementById("processing_message");

async function extractText(pdf) {
  const pdfjs = (
    await import("/-/static-plugins/datasette_extract/pdfjs-dist-4-0-379.js")
  ).default;
  pdfjsLib.GlobalWorkerOptions.workerSrc =
    "/-/static-plugins/datasette_extract/pdf.worker.mjs";

  const loadingTask = pdfjsLib.getDocument(pdf);
  const pdfDocument = await loadingTask.promise;

  const numPages = pdfDocument.numPages;
  let pageTextPromises = [];

  for (let i = 1; i <= numPages; i++) {
    pageTextPromises.push(
      pdfDocument.getPage(i).then((page) => page.getTextContent()),
    );
  }

  const pageTexts = await Promise.all(pageTextPromises);
  return pageTexts
    .map((item) => item.items.map((text) => text.str).join(" "))
    .join("\n\n");
}

function dragOverHandler(event) {
  event.preventDefault();
  event.stopPropagation();
  textarea.classList.add("drag-over");
}

function dragLeaveHandler(event) {
  event.preventDefault();
  event.stopPropagation();
  textarea.classList.remove("drag-over");
}

async function dropHandler(event) {
  event.preventDefault();
  event.stopPropagation();
  textarea.classList.remove("drag-over");

  const files = Array.from(event.dataTransfer.files);
  const imageFiles = files.filter((file) => file.type.startsWith("image/"));
  const pdfFiles = files.filter((file) => file.type === "application/pdf");
  const otherFiles = files.filter(
    (file) =>
      !file.type.startsWith("image/") && file.type !== "application/pdf",
  );

  if (imageFiles.length > 0) {
    processingMessage.style.display = "block";
    const convertedImages = await Promise.all(
      imageFiles.map(async (file) => {
        if (file.type === "image/heic") {
          const heic2any = (
            await import(
              "/-/static-plugins/datasette_extract/heic2any-0.0.4.min.js"
            )
          ).default;
          try {
            const blob = await heic2any({
              blob: file,
              toType: "image/jpeg",
              quality: 0.8,
            });
            return new File([blob], `converted-${file.name}.jpeg`, {
              type: "image/jpeg",
            });
          } catch (error) {
            console.error("Conversion error:", error);
            return null;
          }
        }
        return file;
      }),
    );
    const validImages = convertedImages.filter((file) => file !== null);
    if (validImages.length > 0) {
      const dataTransfer = new DataTransfer();
      validImages.forEach((file) => dataTransfer.items.add(file));
      imageInput.files = dataTransfer.files;
    }
    processingMessage.style.display = "none";
  }

  if (pdfFiles.length > 0) {
    processingMessage.style.display = "block";
    const pdfTexts = await Promise.all(
      pdfFiles.map(async (file) =>
        extractText(new Uint8Array(await file.arrayBuffer())),
      ),
    );
    textarea.value = pdfTexts.join("\n\n");
    processingMessage.style.display = "none";
  }

  if (otherFiles.length > 0) {
    const otherContents = await Promise.all(
      otherFiles.map(
        (file) =>
          new Promise((resolve, reject) => {
            const reader = new FileReader();
            reader.onload = (e) => {
              resolve(e.target.result);
            };
            reader.onerror = (e) => {
              reject(new Error("Failed to read file"));
            };
            reader.readAsText(file);
          }),
      ),
    );
    textarea.value +=
      (textarea.value ? "\n\n" : "") + otherContents.join("\n\n");
  }
}

imageInput.addEventListener("change", async (event) => {
  const file = event.target.files[0];
  if (file && file.type === "image/heic") {
    processingMessage.style.display = "block";
    const heic2any = (
      await import(
        "/-/static-plugins/datasette_extract/heic2any-0.0.4.min.js"
      )
    ).default;
    try {
      const blob = await heic2any({
        blob: file,
        toType: "image/jpeg",
        quality: 0.8,
      });
      const dataTransfer = new DataTransfer();
      dataTransfer.items.add(
        new File([blob], `converted-${file.name}.jpeg`, { type: "image/jpeg" }),
      );
      imageInput.files = dataTransfer.files;
    } catch (error) {
      console.error("Conversion error:", error);
    }
    processingMessage.style.display = "none";
  }
});

textarea.addEventListener("dragover", dragOverHandler);
textarea.addEventListener("dragleave", dragLeaveHandler);
textarea.addEventListener("drop", dropHandler);
</script>

Here are the main changes:

  1. When a HEIC image is selected directly using the imageInput file input, it is now converted in the same way as when dropped, with the progress message showing and hiding, and the converted file is set back to the imageInput.

  2. When files are dropped on the textarea, PDF and image files are handled specially, while other files are processed using the provided code snippet. The contents of these other files are read as text and appended to the textarea, separated by double newlines.

Let me know if you have any further questions or if you need any additional assistance!

2024-03-07T21:53:23

Prompt:

Refactor that so that the heic2any code that is used in both places is in a function - output just the <script> block

Response:

Here's the refactored code with the heic2any code in a separate function:

<script type="module">
const textarea = document.getElementById("id_content");
const imageInput = document.getElementById("id_image");
const processingMessage = document.getElementById("processing_message");

async function extractText(pdf) {
  const pdfjs = (
    await import("/-/static-plugins/datasette_extract/pdfjs-dist-4-0-379.js")
  ).default;
  pdfjsLib.GlobalWorkerOptions.workerSrc =
    "/-/static-plugins/datasette_extract/pdf.worker.mjs";

  const loadingTask = pdfjsLib.getDocument(pdf);
  const pdfDocument = await loadingTask.promise;

  const numPages = pdfDocument.numPages;
  let pageTextPromises = [];

  for (let i = 1; i <= numPages; i++) {
    pageTextPromises.push(
      pdfDocument.getPage(i).then((page) => page.getTextContent()),
    );
  }

  const pageTexts = await Promise.all(pageTextPromises);
  return pageTexts
    .map((item) => item.items.map((text) => text.str).join(" "))
    .join("\n\n");
}

async function convertHeicToJpeg(file) {
  const heic2any = (
    await import("/-/static-plugins/datasette_extract/heic2any-0.0.4.min.js")
  ).default;
  try {
    const blob = await heic2any({
      blob: file,
      toType: "image/jpeg",
      quality: 0.8,
    });
    return new File([blob], `converted-${file.name}.jpeg`, {
      type: "image/jpeg",
    });
  } catch (error) {
    console.error("Conversion error:", error);
    return null;
  }
}

function dragOverHandler(event) {
  event.preventDefault();
  event.stopPropagation();
  textarea.classList.add("drag-over");
}

function dragLeaveHandler(event) {
  event.preventDefault();
  event.stopPropagation();
  textarea.classList.remove("drag-over");
}

async function dropHandler(event) {
  event.preventDefault();
  event.stopPropagation();
  textarea.classList.remove("drag-over");

  const files = Array.from(event.dataTransfer.files);
  const imageFiles = files.filter((file) => file.type.startsWith("image/"));
  const pdfFiles = files.filter((file) => file.type === "application/pdf");
  const otherFiles = files.filter(
    (file) =>
      !file.type.startsWith("image/") && file.type !== "application/pdf",
  );

  if (imageFiles.length > 0) {
    processingMessage.style.display = "block";
    const convertedImages = await Promise.all(
      imageFiles.map(async (file) => {
        if (file.type === "image/heic") {
          return await convertHeicToJpeg(file);
        }
        return file;
      }),
    );
    const validImages = convertedImages.filter((file) => file !== null);
    if (validImages.length > 0) {
      const dataTransfer = new DataTransfer();
      validImages.forEach((file) => dataTransfer.items.add(file));
      imageInput.files = dataTransfer.files;
    }
    processingMessage.style.display = "none";
  }

  if (pdfFiles.length > 0) {
    processingMessage.style.display = "block";
    const pdfTexts = await Promise.all(
      pdfFiles.map(async (file) =>
        extractText(new Uint8Array(await file.arrayBuffer())),
      ),
    );
    textarea.value = pdfTexts.join("\n\n");
    processingMessage.style.display = "none";
  }

  if (otherFiles.length > 0) {
    const otherContents = await Promise.all(
      otherFiles.map(
        (file) =>
          new Promise((resolve, reject) => {
            const reader = new FileReader();
            reader.onload = (e) => {
              resolve(e.target.result);
            };
            reader.onerror = (e) => {
              reject(new Error("Failed to read file"));
            };
            reader.readAsText(file);
          }),
      ),
    );
    textarea.value +=
      (textarea.value ? "\n\n" : "") + otherContents.join("\n\n");
  }
}

imageInput.addEventListener("change", async (event) => {
  const file = event.target.files[0];
  if (file && file.type === "image/heic") {
    processingMessage.style.display = "block";
    const convertedFile = await convertHeicToJpeg(file);
    if (convertedFile) {
      const dataTransfer = new DataTransfer();
      dataTransfer.items.add(convertedFile);
      imageInput.files = dataTransfer.files;
    }
    processingMessage.style.display = "none";
  }
});

textarea.addEventListener("dragover", dragOverHandler);
textarea.addEventListener("dragleave", dragLeaveHandler);
textarea.addEventListener("drop", dropHandler);
</script>

In this refactored code, the convertHeicToJpeg function encapsulates the heic2any code that was previously used in two places. This function takes a HEIC file as input, converts it to JPEG using heic2any, and returns the converted file. If the conversion fails, it logs an error and returns null.

The dropHandler and imageInput change event listener now use the convertHeicToJpeg function to convert HEIC files to JPEG.

This refactoring improves code reusability and maintainability by avoiding code duplication and encapsulating the HEIC to JPEG conversion logic in a separate function.

@mcburton
Copy link

Simon,

Separate from Claude's "vibes", I liked seeing how you formatted the chat transcript in Markdown. I've been trying to figure out some conventions for structuring my conversations with Claude and ChatGPT in Markdown. It would need to have a link about to the conversation (on the chat or workbench/playground), system prompt, and then some MD conventions for the User/Assistant turn-taking.

I'd love to know if you've thought about this. Do you have any tips, tricks, or frustrations? I'd love to see you write something up!

My goal is to eventually parse these markdown files so I can send the structured messages to the Claude/OpenAI endpoints, then update the file with the response (and so forth). Basically have a local "workbench".

@simonw
Copy link
Author

simonw commented Mar 13, 2024

This markdown format here is the format my llm command outputs when you run llm logs - the actual prompt I used was this:

<script type="module">
const textarea = document.getElementById("id_content");
const imageInput = document.getElementById("id_image");
const processingMessage = document.getElementById("processing_message");

async function extractText(pdf) {
  const pdfjs = (
    await import("/-/static-plugins/datasette_extract/pdfjs-dist-4-0-379.js")
  ).default;
  pdfjsLib.GlobalWorkerOptions.workerSrc =
    "/-/static-plugins/datasette_extract/pdf.worker.mjs";

  const loadingTask = pdfjsLib.getDocument(pdf);
  const pdfDocument = await loadingTask.promise;

  const numPages = pdfDocument.numPages;
  let pageTextPromises = [];

  for (let i = 1; i <= numPages; i++) {
    pageTextPromises.push(
      pdfDocument.getPage(i).then((page) => page.getTextContent()),
    );
  }

  const pageTexts = await Promise.all(pageTextPromises);
  return pageTexts
    .map((item) => item.items.map((text) => text.str).join(" "))
    .join("\n\n");
}

function dragOverHandler(event) {
  event.preventDefault();
  event.stopPropagation();
  textarea.classList.add("drag-over");
}

function dragLeaveHandler(event) {
  event.preventDefault();
  event.stopPropagation();
  textarea.classList.remove("drag-over");
}

async function dropHandler(event) {
  event.preventDefault();
  event.stopPropagation();
  textarea.classList.remove("drag-over");

  const files = Array.from(event.dataTransfer.files);
  const imageFile = files.find((file) => file.type.startsWith("image/"));
  const pdfFiles = files.filter((file) => file.type === "application/pdf");
  const textFiles = files.filter((file) => file.type === "text/plain");

  if (imageFile) {
    if (imageFile.type === "image/heic") {
      processingMessage.style.display = "block";
      const heic2any = (
        await import(
          "/-/static-plugins/datasette_extract/heic2any-0.0.4.min.js"
        )
      ).default;
      try {
        const blob = await heic2any({
          blob: imageFile,
          toType: "image/jpeg",
          quality: 0.8,
        });
        const dataTransfer = new DataTransfer();
        dataTransfer.items.add(
          new File([blob], `converted-${imageFile.name}.jpeg`, { type: "image/jpeg" }),
        );
        imageInput.files = dataTransfer.files;
      } catch (error) {
        console.error("Conversion error:", error);
      }
      processingMessage.style.display = "none";
    } else {
      imageInput.files = event.dataTransfer.files;
    }
  }
  if (pdfFiles.length > 0) {
    processingMessage.style.display = "block";
    const pdfTexts = await Promise.all(
      pdfFiles.map(async (file) =>
        extractText(new Uint8Array(await file.arrayBuffer())),
      ),
    );
    textarea.value = pdfTexts.join("\n\n");
    processingMessage.style.display = "none";
  }
  if (textFiles.length > 0) {
    const textContents = await Promise.all(
      textFiles.map((file) => file.text()),
    );
    textarea.value +=
      (textarea.value ? "\n\n" : "") + textContents.join("\n\n");
  }
}

textarea.addEventListener("dragover", dragOverHandler);
textarea.addEventListener("dragleave", dragLeaveHandler);
textarea.addEventListener("drop", dropHandler);
</script>

And then the system prompt was this:

Make some changes to this code.

When a HEIC image is selected directly using the imageInput file input, it should be converted in the same way with the progress bit showing and then hiding and the converted file set back to that input

When files are dropped on the textarea, PDF and images should use the special handling - but any others should use this code instead:

          const reader = new FileReader();
          reader.onload = (e) => {
            contentToAdd += e.target.result;
            resolve(contentToAdd);
          };
          reader.onerror = (e) => {
            reject(new Error("Failed to read file"));
          };
          reader.readAsText(file);


where resolve should do textarea.value = contents.join("\n\n");

I executed it on the command-line like this:

echo mycode.js | llm -m claude-3-opus --system '
Make some changes to this code.

When a HEIC image is selected directly using the imageInput file input, it should be converted in the same way with the progress bit showing and then hiding and the converted file set back to that input

When files are dropped on the textarea, PDF and images should use the special handling - but any others should use this code instead:

          const reader = new FileReader();
          reader.onload = (e) => {
            contentToAdd += e.target.result;
            resolve(contentToAdd);
          };
          reader.onerror = (e) => {
            reject(new Error("Failed to read file"));
          };
          reader.readAsText(file);


where resolve should do textarea.value = contents.join("\n\n");
'

I'm using LLM plus my https://github.com/simonw/llm-claude-3 plugin.

@simonw
Copy link
Author

simonw commented Mar 13, 2024

My LLM tool acts as my local workbench. The most useful feature is probably the logging - it logs every prompt and response to a SQLite database so I can revisit them later or log them out again to share in Gists: https://llm.datasette.io/en/stable/logging.html

@mcburton
Copy link

Ah, yes, I figured that is what you were doing.
If you have the conversation log in the database then the conversation is structured, markdown is just an output format. I was basically ruminating about a purely text-based, Markdown approach to what the database is doing for the logging. (Basically, I was thinking about implementing a workbench experience in Obsidian). Might not really be worth implementing tho...llm already does it!

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