Skip to content

Commit

Permalink
fix: more flexible and efficient solution for images in docs
Browse files Browse the repository at this point in the history
  • Loading branch information
mantagen committed Nov 28, 2024
1 parent eca0019 commit 57eb47b
Show file tree
Hide file tree
Showing 8 changed files with 167 additions and 173 deletions.
2 changes: 1 addition & 1 deletion apps/nextjs/next-env.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
/// <reference types="next/image-types/global" />

// NOTE: This file should not be edited
// see https://nextjs.org/docs/basic-features/typescript for more information.
// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information.
11 changes: 11 additions & 0 deletions packages/aila/src/utils/experimentalPatches/mathsQuiz.fixture.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,4 +44,15 @@ export const mathsQuizFixture: Quiz = [
"neither similar nor congruent.",
],
},
{
question:
"Here is an image ![image](http://oaknationalacademy-res.cloudinary.com/image/upload/v1706110974/fukcqeavzcevgjhmm1n4.png) and here is the same image ![image](http://oaknationalacademy-res.cloudinary.com/image/upload/v1706110974/fukcqeavzcevgjhmm1n4.png)",
answers: [
"![image](http://oaknationalacademy-res.cloudinary.com/image/upload/v1703169784/fg4uyx41rfnksbvav2nh.png)",
],
distractors: [
"![image](http://oaknationalacademy-res.cloudinary.com/image/upload/v1703163380/pz6cn5k4wmowycnjq5am.png)",
"![image](http://oaknationalacademy-res.cloudinary.com/image/upload/v1703169784/mr09mrwkqdtk1dvjdoi0.png)",
],
},
];
3 changes: 1 addition & 2 deletions packages/api/src/export/exportQuizDoc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,7 @@ import { aiLogger } from "@oakai/logger";
import * as Sentry from "@sentry/nextjs";
import { z } from "zod";

import type {
OutputSchema} from "../router/exports";
import type { OutputSchema } from "../router/exports";
import {
ailaGetExportBySnapshotId,
ailaSaveExport,
Expand Down
70 changes: 70 additions & 0 deletions packages/exports/src/gSuite/docs/findMarkdownImages.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import type { docs_v1 } from "@googleapis/docs";

export async function findMarkdownImages(
googleDocs: docs_v1.Docs,
documentId: string,
): Promise<{ url: string; altText: string; startIndex: number }[]> {
const doc = await googleDocs.documents.get({ documentId });
const body = doc.data.body?.content;

if (!body) return [];

const markdownImageRegex = /!\[(.*?)\]\((.*?)\)/g;
const matches: { url: string; altText: string; startIndex: number }[] = [];

function handleMatch(
match: RegExpExecArray,
textElement: docs_v1.Schema$ParagraphElement,
): void {
const [, altText, url] = match;
const textElementStartIndex = textElement.startIndex;
const matchIndex = match.index;
if (
typeof textElementStartIndex !== "number" ||
typeof matchIndex !== "number"
) {
return;
}
const startIndex = textElementStartIndex + matchIndex;
if (url && typeof altText === "string") {
matches.push({ url, altText, startIndex });
}
}

function processTextElements(elements: docs_v1.Schema$ParagraphElement[]) {
for (const textElement of elements) {
const textContent = textElement.textRun?.content ?? "";
let match: RegExpExecArray | null;
while ((match = markdownImageRegex.exec(textContent)) !== null) {
handleMatch(match, textElement);
}
}
}

function processTableCells(cells: docs_v1.Schema$TableCell[]) {
for (const cell of cells) {
for (const cellElement of cell.content ?? []) {
if (cellElement.paragraph?.elements) {
processTextElements(cellElement.paragraph.elements);
}
}
}
}

function processBodyElements(elements: docs_v1.Schema$StructuralElement[]) {
for (const element of elements) {
if (element.table) {
for (const row of element.table.tableRows ?? []) {
processTableCells(row.tableCells ?? []);
}
}
if (element.paragraph?.elements) {
processTextElements(element.paragraph.elements);
}
}
}

processBodyElements(body);

return matches;
}
42 changes: 0 additions & 42 deletions packages/exports/src/gSuite/docs/findPlaceholderIndex.ts

This file was deleted.

62 changes: 62 additions & 0 deletions packages/exports/src/gSuite/docs/imageReplacements.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import type { docs_v1 } from "@googleapis/docs";
import { aiLogger } from "@oakai/logger";

const log = aiLogger("exports");

export function imageReplacements(
markdownImages: { url: string; altText: string; startIndex: number }[],
): { requests: docs_v1.Schema$Request[] } {
if (markdownImages.length === 0) {
log.info("No Markdown images to process.");
return { requests: [] };
}

let cumulativeShift = 0; // Tracks the total index shift from previous operations

const requests: docs_v1.Schema$Request[] = [];

markdownImages.forEach((image) => {
// Construct the full Markdown reference
const markdownImageReference = `![${image.altText}](${image.url})`;

const markdownLength = markdownImageReference.length;

// Adjust the start and end index dynamically based on the cumulative shift
const adjustedStartIndex = image.startIndex + cumulativeShift;
const adjustedEndIndex = adjustedStartIndex + markdownLength;

// Request to delete the exact range of the Markdown reference
requests.push({
deleteContentRange: {
range: {
startIndex: adjustedStartIndex,
endIndex: adjustedEndIndex,
},
},
});

// Request to insert the inline image at the adjusted startIndex
requests.push({
insertInlineImage: {
uri: image.url,
location: {
index: adjustedStartIndex, // Insert at the same startIndex where the Markdown was removed
},
objectSize: {
height: {
magnitude: 150,
unit: "PT",
},
width: {
magnitude: 150,
unit: "PT",
},
},
},
});
const netShift = 1 - markdownLength; // Inline image adds 1, Markdown removes its length
cumulativeShift += netShift;
});

return { requests };
}
30 changes: 22 additions & 8 deletions packages/exports/src/gSuite/docs/populateDoc.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,18 @@
import type { docs_v1 } from "@googleapis/docs";
import { aiLogger } from "@oakai/logger";

import type { Result } from "../../types";
import type { ValueToString } from "../../utils";
import { defaultValueToString } from "../../utils";
<<<<<<< Updated upstream
=======
import { findMarkdownImages } from "./findMarkdownImages";
import { imageReplacements } from "./imageReplacements";
>>>>>>> Stashed changes
import { textReplacements } from "./textReplacements";

const log = aiLogger("exports");

/**
* Populates the template document with the given data, handling image replacements for all placeholders.
*/
Expand All @@ -26,13 +34,6 @@ export async function populateDoc<
try {
const missingData: string[] = [];

// Commenting out this part until the issues are resolved (see @TODOs on function defintiion)
// await processImageReplacements({
// googleDocs,
// documentId,
// data,
// });

const { requests: textRequests } = textReplacements({
data,
warnIfMissing,
Expand All @@ -48,13 +49,26 @@ export async function populateDoc<
});
}

const markdownImages = await findMarkdownImages(googleDocs, documentId);

const { requests: imageRequests } = imageReplacements(markdownImages);

if (imageRequests.length > 0) {
await googleDocs.documents.batchUpdate({
documentId,
requestBody: {
requests: imageRequests,
},
});
}

return {
data: {
missingData,
},
};
} catch (error) {
console.error("Failed to populate document:", error);
log.error("Failed to populate document:", error);
return {
error,
message: "Failed to populate doc template",
Expand Down
120 changes: 0 additions & 120 deletions packages/exports/src/gSuite/docs/processImagesReplacements.ts

This file was deleted.

0 comments on commit 57eb47b

Please sign in to comment.