Files
ChemistryHelpBot/src/libs/Typst.ts
T
2026-05-21 22:41:04 -04:00

91 lines
3.5 KiB
TypeScript

// Default page settings prepended to all compile() calls so the API never falls back to A4.
// Users can override with their own #set page() — Typst's last rule wins.
const DEFAULT_PAGE = `#set page(width: auto, height: auto, margin: 8pt, fill: white)\n`;
export default class Typst {
messages: Map<string, { replyId: string; ownerId?: string }>;
private readonly apiUrl = "https://typ.sirblob.co/v1/render";
constructor() {
this.messages = new Map();
}
addMessage(userMessage: string, responseMessage: string, ownerId?: string) { this.messages.set(userMessage, { replyId: responseMessage, ownerId }); }
removeMessage(userMessage: string) { this.messages.delete(userMessage); }
hasMessage(userMessage: string) { return this.messages.has(userMessage); }
getResponse(userMessage: string) { return this.messages.get(userMessage)?.replyId; }
getOwner(userMessage: string) { return this.messages.get(userMessage)?.ownerId; }
findByReply(replyId: string): { userMessage?: string; ownerId?: string } | null {
for (const [userMessage, data] of this.messages.entries()) {
if (data.replyId === replyId) return { userMessage, ownerId: data.ownerId };
}
return null;
}
private get apiKey(): string {
const key = process.env.TYPSTDRIVE_API_KEY;
if (!key) throw new Error("TYPSTDRIVE_API_KEY environment variable is missing.");
return key;
}
async compile(code: string, _options?: { cleanup?: boolean }): Promise<{ buffers?: Buffer[]; error?: string }> {
try {
const response = await fetch(this.apiUrl, {
method: "POST",
headers: {
"Authorization": `Bearer ${this.apiKey}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ code: DEFAULT_PAGE + code, format: "png" }),
});
if (!response.ok) {
const text = await response.text().catch(() => response.statusText);
return { error: `TypstDrive API error ${response.status}: ${text}` };
}
const arrayBuffer = await response.arrayBuffer();
return { buffers: [Buffer.from(arrayBuffer)] };
} catch (error: any) {
return { error: `TypstDrive request failed: ${error?.message ?? String(error)}` };
}
}
async renderToImage(typstCode: string, metadata?: { topic?: string; difficulty?: string }): Promise<Buffer | null> {
const metadataHeader = metadata ? `
#align(center)[
#block(
fill: rgb("#e0f2fe"),
inset: 10pt,
radius: 4pt,
[
#text(weight: "bold", size: 14pt)[Topic: ${metadata.topic || "N/A"}] \\
#text(size: 12pt, fill: rgb("#64748b"))[Difficulty: ${metadata.difficulty || "N/A"}]
]
)
]
#v(0.8em)
` : '';
// 500pt wide keeps equations readable; height: auto sizes to content.
// This #set page overrides the DEFAULT_PAGE prepended in compile().
const styledCode = `
#set page(width: 500pt, height: auto, margin: 12pt, fill: white)
#set text(size: 16pt, font: "New Computer Modern")
#set par(justify: true, leading: 0.65em)
#set block(spacing: 1.2em)
${metadataHeader}${typstCode}
`;
const result = await this.compile(styledCode);
if (result.error || !result.buffers || result.buffers.length === 0) {
console.error("Typst render error:", result.error);
return null;
}
return result.buffers[0];
}
}