@denarolabs/email-template 1.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/ai-email-editor/AIEmailEditor.d.ts +3 -0
- package/dist/components/ai-email-editor/AIEmailEditor.js +207 -0
- package/dist/components/ai-email-editor/chat-message.d.ts +36 -0
- package/dist/components/ai-email-editor/chat-message.js +49 -0
- package/dist/components/ai-email-editor/images.d.ts +2 -0
- package/dist/components/ai-email-editor/images.js +14 -0
- package/dist/components/ai-email-editor/model-picker.d.ts +7 -0
- package/dist/components/ai-email-editor/model-picker.js +9 -0
- package/dist/components/ai-email-editor/preview.d.ts +35 -0
- package/dist/components/ai-email-editor/preview.js +728 -0
- package/dist/components/ai-email-editor/send-test-email-modal.d.ts +13 -0
- package/dist/components/ai-email-editor/send-test-email-modal.js +70 -0
- package/dist/components/ai-email-editor/stream.d.ts +20 -0
- package/dist/components/ai-email-editor/stream.js +57 -0
- package/dist/components/ai-email-editor/use-ai-email-editor.d.ts +117 -0
- package/dist/components/ai-email-editor/use-ai-email-editor.js +1308 -0
- package/dist/components/ai-email-editor/view-html-modal.d.ts +9 -0
- package/dist/components/ai-email-editor/view-html-modal.js +37 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +7 -0
- package/dist/lib/ai-stream-contract.d.ts +99 -0
- package/dist/lib/ai-stream-contract.js +35 -0
- package/dist/lib/build-default-system-prompt.d.ts +2 -0
- package/dist/lib/build-default-system-prompt.js +37 -0
- package/dist/lib/capture-email-preview.d.ts +5 -0
- package/dist/lib/capture-email-preview.js +73 -0
- package/dist/lib/cn.d.ts +2 -0
- package/dist/lib/cn.js +5 -0
- package/dist/lib/merge-tag-validation.d.ts +3 -0
- package/dist/lib/merge-tag-validation.js +45 -0
- package/dist/lib/rasterize-image-client.d.ts +4 -0
- package/dist/lib/rasterize-image-client.js +47 -0
- package/dist/lib/strip-html-code-fences.d.ts +1 -0
- package/dist/lib/strip-html-code-fences.js +6 -0
- package/dist/schemas/aiEmail.d.ts +224 -0
- package/dist/schemas/aiEmail.js +29 -0
- package/dist/schemas/aiEmailResponse.d.ts +15 -0
- package/dist/schemas/aiEmailResponse.js +15 -0
- package/dist/types.d.ts +57 -0
- package/dist/types.js +1 -0
- package/dist/ui/button.d.ts +11 -0
- package/dist/ui/button.js +34 -0
- package/dist/ui/dialog.d.ts +33 -0
- package/dist/ui/dialog.js +39 -0
- package/dist/ui/dropdown-menu.d.ts +8 -0
- package/dist/ui/dropdown-menu.js +23 -0
- package/dist/ui/input.d.ts +2 -0
- package/dist/ui/input.js +6 -0
- package/dist/ui/textarea.d.ts +2 -0
- package/dist/ui/textarea.js +7 -0
- package/package.json +74 -0
- package/src/styles.css +197 -0
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { AIEmailEditorErrorContext, AIEmailEditorSuccessContext } from "../../types";
|
|
2
|
+
export declare function ViewHtmlModal({ open, onOpenChange, getHtml, downloadFileName, onError, onSuccess, }: {
|
|
3
|
+
open: boolean;
|
|
4
|
+
onOpenChange: (open: boolean) => void;
|
|
5
|
+
getHtml: () => string;
|
|
6
|
+
downloadFileName: string;
|
|
7
|
+
onError: (error: Error, context: AIEmailEditorErrorContext) => void;
|
|
8
|
+
onSuccess?: (context: AIEmailEditorSuccessContext) => void;
|
|
9
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { Copy, Download } from "lucide-react";
|
|
4
|
+
import { useEffect, useState } from "react";
|
|
5
|
+
import { Button } from "../../ui/button";
|
|
6
|
+
import { Dialog, DialogContent, DialogFooter, DialogTitle, } from "../../ui/dialog";
|
|
7
|
+
import { Textarea } from "../../ui/textarea";
|
|
8
|
+
export function ViewHtmlModal({ open, onOpenChange, getHtml, downloadFileName, onError, onSuccess, }) {
|
|
9
|
+
const [html, setHtml] = useState("");
|
|
10
|
+
useEffect(() => {
|
|
11
|
+
if (open)
|
|
12
|
+
setHtml(getHtml());
|
|
13
|
+
}, [open, getHtml]);
|
|
14
|
+
const handleCopy = async () => {
|
|
15
|
+
if (!html)
|
|
16
|
+
return;
|
|
17
|
+
try {
|
|
18
|
+
await navigator.clipboard.writeText(html);
|
|
19
|
+
onSuccess?.({ source: "html", message: "HTML copied to clipboard" });
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
onError(new Error("Failed to copy HTML"), { source: "html" });
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
const handleExport = () => {
|
|
26
|
+
if (!html)
|
|
27
|
+
return;
|
|
28
|
+
const url = URL.createObjectURL(new Blob([html], { type: "text/html" }));
|
|
29
|
+
const link = document.createElement("a");
|
|
30
|
+
link.href = url;
|
|
31
|
+
link.download = `${downloadFileName.replace(/\s+/g, "-").toLowerCase()}.html`;
|
|
32
|
+
link.click();
|
|
33
|
+
URL.revokeObjectURL(url);
|
|
34
|
+
onSuccess?.({ source: "html", message: "HTML exported" });
|
|
35
|
+
};
|
|
36
|
+
return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { showCloseButton: true, onClose: () => onOpenChange(false), className: "flex h-[70vh] max-w-4xl flex-col gap-0 p-0", children: [_jsx(DialogTitle, { className: "sr-only", children: "Email HTML" }), _jsx("div", { className: "min-h-0 flex-1 p-4", children: _jsx(Textarea, { readOnly: true, value: html, className: "h-full min-h-[50vh] font-mono text-xs" }) }), _jsxs(DialogFooter, { className: "sm:justify-between", children: [_jsx(Button, { type: "button", variant: "outline", onClick: () => onOpenChange(false), children: "Close" }), _jsxs("div", { className: "flex gap-2", children: [_jsxs(Button, { type: "button", variant: "outline", onClick: () => void handleCopy(), disabled: !html, className: "gap-2", children: [_jsx(Copy, { className: "h-4 w-4" }), "Copy"] }), _jsxs(Button, { type: "button", onClick: handleExport, disabled: !html, className: "gap-2", children: [_jsx(Download, { className: "h-4 w-4" }), "Export"] })] })] })] }) }));
|
|
37
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { AIEmailEditor, default } from "./components/ai-email-editor/AIEmailEditor";
|
|
2
|
+
export type { AIEmailEditorProps, AIEmailModelOption, StoredEmailRecord, AIEmailEditorMode, AIEmailEditorErrorSource, AIEmailEditorErrorContext, AIEmailEditorSuccessSource, AIEmailEditorSuccessContext, SendTestEmailInput, } from "./types";
|
|
3
|
+
export { AiEmailResponseSchema, type AiEmailResponse, } from "./schemas/aiEmailResponse";
|
|
4
|
+
export { AiEmailChatMessageSchema, AiEmailHtmlSnapshotSchema, AiEmailImageSchema, AiEmailSessionSchema, type AiEmailChatMessage, type AiEmailHtmlSnapshot, type AiEmailImage, type AiEmailSession, } from "./schemas/aiEmail";
|
|
5
|
+
export { buildAiEmailStreamRequestSchema, buildResponseFormatDirective, } from "./lib/ai-stream-contract";
|
|
6
|
+
export { getMergeTagError, extractMergeTags, applyMergeTags } from "./lib/merge-tag-validation";
|
|
7
|
+
export { buildDefaultSystemPrompt } from "./lib/build-default-system-prompt";
|
|
8
|
+
export { DEFAULT_EDIT_SUGGESTIONS, DEFAULT_NEW_TEMPLATE_SUGGESTIONS, resolveInitialSuggestions, detectPhase, extractStreamParts, formatPlan, parseStructuredStream, phaseLabel, } from "./components/ai-email-editor/stream";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { AIEmailEditor, default } from "./components/ai-email-editor/AIEmailEditor";
|
|
2
|
+
export { AiEmailResponseSchema, } from "./schemas/aiEmailResponse";
|
|
3
|
+
export { AiEmailChatMessageSchema, AiEmailHtmlSnapshotSchema, AiEmailImageSchema, AiEmailSessionSchema, } from "./schemas/aiEmail";
|
|
4
|
+
export { buildAiEmailStreamRequestSchema, buildResponseFormatDirective, } from "./lib/ai-stream-contract";
|
|
5
|
+
export { getMergeTagError, extractMergeTags, applyMergeTags } from "./lib/merge-tag-validation";
|
|
6
|
+
export { buildDefaultSystemPrompt } from "./lib/build-default-system-prompt";
|
|
7
|
+
export { DEFAULT_EDIT_SUGGESTIONS, DEFAULT_NEW_TEMPLATE_SUGGESTIONS, resolveInitialSuggestions, detectPhase, extractStreamParts, formatPlan, parseStructuredStream, phaseLabel, } from "./components/ai-email-editor/stream";
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export { AiEmailResponseSchema, type AiEmailResponse, } from "../schemas/aiEmailResponse";
|
|
3
|
+
export declare function buildAiEmailStreamRequestSchema(): z.ZodObject<{
|
|
4
|
+
messages: z.ZodArray<z.ZodObject<{
|
|
5
|
+
role: z.ZodEnum<["user", "assistant"]>;
|
|
6
|
+
content: z.ZodString;
|
|
7
|
+
contextImages: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
8
|
+
url: z.ZodString;
|
|
9
|
+
label: z.ZodString;
|
|
10
|
+
}, "strip", z.ZodTypeAny, {
|
|
11
|
+
url: string;
|
|
12
|
+
label: string;
|
|
13
|
+
}, {
|
|
14
|
+
url: string;
|
|
15
|
+
label: string;
|
|
16
|
+
}>, "many">>;
|
|
17
|
+
}, "strip", z.ZodTypeAny, {
|
|
18
|
+
role: "user" | "assistant";
|
|
19
|
+
content: string;
|
|
20
|
+
contextImages?: {
|
|
21
|
+
url: string;
|
|
22
|
+
label: string;
|
|
23
|
+
}[] | undefined;
|
|
24
|
+
}, {
|
|
25
|
+
role: "user" | "assistant";
|
|
26
|
+
content: string;
|
|
27
|
+
contextImages?: {
|
|
28
|
+
url: string;
|
|
29
|
+
label: string;
|
|
30
|
+
}[] | undefined;
|
|
31
|
+
}>, "many">;
|
|
32
|
+
systemPrompt: z.ZodString;
|
|
33
|
+
model: z.ZodString;
|
|
34
|
+
sessionContextImages: z.ZodDefault<z.ZodArray<z.ZodObject<{
|
|
35
|
+
url: z.ZodString;
|
|
36
|
+
label: z.ZodString;
|
|
37
|
+
}, "strip", z.ZodTypeAny, {
|
|
38
|
+
url: string;
|
|
39
|
+
label: string;
|
|
40
|
+
}, {
|
|
41
|
+
url: string;
|
|
42
|
+
label: string;
|
|
43
|
+
}>, "many">>;
|
|
44
|
+
previewScreenshot: z.ZodOptional<z.ZodString>;
|
|
45
|
+
currentHtml: z.ZodOptional<z.ZodString>;
|
|
46
|
+
selectedBlocks: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
47
|
+
id: z.ZodString;
|
|
48
|
+
html: z.ZodString;
|
|
49
|
+
}, "strip", z.ZodTypeAny, {
|
|
50
|
+
id: string;
|
|
51
|
+
html: string;
|
|
52
|
+
}, {
|
|
53
|
+
id: string;
|
|
54
|
+
html: string;
|
|
55
|
+
}>, "many">>;
|
|
56
|
+
}, "strip", z.ZodTypeAny, {
|
|
57
|
+
messages: {
|
|
58
|
+
role: "user" | "assistant";
|
|
59
|
+
content: string;
|
|
60
|
+
contextImages?: {
|
|
61
|
+
url: string;
|
|
62
|
+
label: string;
|
|
63
|
+
}[] | undefined;
|
|
64
|
+
}[];
|
|
65
|
+
systemPrompt: string;
|
|
66
|
+
sessionContextImages: {
|
|
67
|
+
url: string;
|
|
68
|
+
label: string;
|
|
69
|
+
}[];
|
|
70
|
+
model: string;
|
|
71
|
+
previewScreenshot?: string | undefined;
|
|
72
|
+
currentHtml?: string | undefined;
|
|
73
|
+
selectedBlocks?: {
|
|
74
|
+
id: string;
|
|
75
|
+
html: string;
|
|
76
|
+
}[] | undefined;
|
|
77
|
+
}, {
|
|
78
|
+
messages: {
|
|
79
|
+
role: "user" | "assistant";
|
|
80
|
+
content: string;
|
|
81
|
+
contextImages?: {
|
|
82
|
+
url: string;
|
|
83
|
+
label: string;
|
|
84
|
+
}[] | undefined;
|
|
85
|
+
}[];
|
|
86
|
+
systemPrompt: string;
|
|
87
|
+
model: string;
|
|
88
|
+
sessionContextImages?: {
|
|
89
|
+
url: string;
|
|
90
|
+
label: string;
|
|
91
|
+
}[] | undefined;
|
|
92
|
+
previewScreenshot?: string | undefined;
|
|
93
|
+
currentHtml?: string | undefined;
|
|
94
|
+
selectedBlocks?: {
|
|
95
|
+
id: string;
|
|
96
|
+
html: string;
|
|
97
|
+
}[] | undefined;
|
|
98
|
+
}>;
|
|
99
|
+
export declare function buildResponseFormatDirective(): string;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export { AiEmailResponseSchema, } from "../schemas/aiEmailResponse";
|
|
3
|
+
const SelectedBlockSchema = z.object({
|
|
4
|
+
id: z.string(),
|
|
5
|
+
html: z.string(),
|
|
6
|
+
});
|
|
7
|
+
const ContextImageSchema = z.object({
|
|
8
|
+
url: z.string().url(),
|
|
9
|
+
label: z.string().min(1).max(120),
|
|
10
|
+
});
|
|
11
|
+
const ChatMessageSchema = z.object({
|
|
12
|
+
role: z.enum(["user", "assistant"]),
|
|
13
|
+
content: z.string(),
|
|
14
|
+
contextImages: z.array(ContextImageSchema).optional(),
|
|
15
|
+
});
|
|
16
|
+
export function buildAiEmailStreamRequestSchema() {
|
|
17
|
+
return z.object({
|
|
18
|
+
messages: z.array(ChatMessageSchema).min(1),
|
|
19
|
+
systemPrompt: z.string().min(1),
|
|
20
|
+
model: z.string().min(1),
|
|
21
|
+
sessionContextImages: z.array(ContextImageSchema).default([]),
|
|
22
|
+
previewScreenshot: z
|
|
23
|
+
.string()
|
|
24
|
+
.regex(/^data:image\/(jpeg|png|webp);base64,/, "Invalid preview screenshot")
|
|
25
|
+
.optional(),
|
|
26
|
+
currentHtml: z.string().optional(),
|
|
27
|
+
selectedBlocks: z.array(SelectedBlockSchema).optional(),
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
export function buildResponseFormatDirective() {
|
|
31
|
+
return `Return a structured response with:
|
|
32
|
+
- plan: 2-4 first-person present-tense bullets describing each change you are making
|
|
33
|
+
- suggestions: exactly 3 short follow-up prompts specific to this email (under 12 words each; do not repeat the plan)
|
|
34
|
+
- html: the complete updated HTML document`;
|
|
35
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
function formatMergeTagList(mergeTags) {
|
|
2
|
+
const tags = [...new Set(mergeTags.map((t) => t.replace(/[{}]/g, "").trim()).filter(Boolean))];
|
|
3
|
+
return tags.length ? tags.map((t) => `{{${t}}}`).join(", ") : "(none available)";
|
|
4
|
+
}
|
|
5
|
+
function buildHtmlOutputRules() {
|
|
6
|
+
return `HTML output rules:
|
|
7
|
+
- Return ONLY a single, complete HTML document. No markdown, no code fences, no commentary.
|
|
8
|
+
- Use inline CSS (and a <style> block in <head>) for consistent rendering.`;
|
|
9
|
+
}
|
|
10
|
+
function buildMergeTagRules(tagList, mergeTags) {
|
|
11
|
+
const lines = [
|
|
12
|
+
"Merge tag rules (STRICT):",
|
|
13
|
+
`- The ONLY merge tags you may use are: ${tagList}.`,
|
|
14
|
+
"- Merge tags MUST be written EXACTLY as {{tagName}} — two opening braces, two closing braces, no spaces inside.",
|
|
15
|
+
"- NEVER invent new merge tags.",
|
|
16
|
+
];
|
|
17
|
+
if (mergeTags.some((t) => t.replace(/[{}]/g, "").trim() === "unsubscribe_link")) {
|
|
18
|
+
lines.push("- Always include an unsubscribe link in the footer using {{unsubscribe_link}}.");
|
|
19
|
+
}
|
|
20
|
+
return lines.join("\n");
|
|
21
|
+
}
|
|
22
|
+
function buildEmailLayoutRules() {
|
|
23
|
+
return `Email layout rules:
|
|
24
|
+
- Use table-based layout for maximum email-client compatibility, max width ~600px, centered.
|
|
25
|
+
- Always include a clear call-to-action button.
|
|
26
|
+
- Keep copy concise, friendly, and professional.
|
|
27
|
+
- Do NOT include logo images, brand marks, or any placeholder/fake image URLs (e.g. example.com, placeholder services, or invented logo paths). Omit a logo/header image entirely unless the user explicitly provides a real image URL to use.
|
|
28
|
+
- Button text color MUST always be set as an inline style directly on the <a> tag AND on a wrapping <span> inside it using !important.`;
|
|
29
|
+
}
|
|
30
|
+
export function buildDefaultSystemPrompt(mergeTags, mode = "email") {
|
|
31
|
+
const tagList = formatMergeTagList(mergeTags);
|
|
32
|
+
const parts = [buildHtmlOutputRules(), buildMergeTagRules(tagList, mergeTags)];
|
|
33
|
+
if (mode === "email") {
|
|
34
|
+
parts.push(buildEmailLayoutRules());
|
|
35
|
+
}
|
|
36
|
+
return parts.join("\n\n");
|
|
37
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { toJpeg } from "html-to-image";
|
|
2
|
+
const PREVIEW_WIDTH_PX = 640;
|
|
3
|
+
const MAX_CAPTURE_HEIGHT_PX = 2400;
|
|
4
|
+
const JPEG_QUALITY = 0.82;
|
|
5
|
+
async function waitForImages(doc, timeoutMs = 4000) {
|
|
6
|
+
const images = [...doc.images];
|
|
7
|
+
if (images.length === 0)
|
|
8
|
+
return;
|
|
9
|
+
await Promise.race([
|
|
10
|
+
Promise.all(images.map((img) => new Promise((resolve) => {
|
|
11
|
+
if (img.complete) {
|
|
12
|
+
resolve();
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
img.addEventListener("load", () => resolve(), { once: true });
|
|
16
|
+
img.addEventListener("error", () => resolve(), { once: true });
|
|
17
|
+
}))),
|
|
18
|
+
new Promise((resolve) => setTimeout(resolve, timeoutMs)),
|
|
19
|
+
]);
|
|
20
|
+
// Allow layout to settle after images load.
|
|
21
|
+
await new Promise((resolve) => requestAnimationFrame(() => resolve()));
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Renders clean email HTML off-screen and returns a JPEG data URL for the AI.
|
|
25
|
+
* Ephemeral — not uploaded or persisted.
|
|
26
|
+
*/
|
|
27
|
+
export async function captureEmailPreviewScreenshot(cleanHtml) {
|
|
28
|
+
if (typeof document === "undefined" || !cleanHtml.trim())
|
|
29
|
+
return null;
|
|
30
|
+
const iframe = document.createElement("iframe");
|
|
31
|
+
iframe.setAttribute("sandbox", "allow-same-origin");
|
|
32
|
+
iframe.setAttribute("aria-hidden", "true");
|
|
33
|
+
iframe.style.cssText = [
|
|
34
|
+
"position:fixed",
|
|
35
|
+
"left:-10000px",
|
|
36
|
+
"top:0",
|
|
37
|
+
`width:${PREVIEW_WIDTH_PX}px`,
|
|
38
|
+
"height:0",
|
|
39
|
+
"border:0",
|
|
40
|
+
"visibility:hidden",
|
|
41
|
+
"pointer-events:none",
|
|
42
|
+
].join(";");
|
|
43
|
+
document.body.appendChild(iframe);
|
|
44
|
+
try {
|
|
45
|
+
const doc = iframe.contentDocument;
|
|
46
|
+
if (!doc)
|
|
47
|
+
return null;
|
|
48
|
+
doc.open();
|
|
49
|
+
doc.write(cleanHtml);
|
|
50
|
+
doc.close();
|
|
51
|
+
await waitForImages(doc);
|
|
52
|
+
const body = doc.body;
|
|
53
|
+
if (!body)
|
|
54
|
+
return null;
|
|
55
|
+
const height = Math.min(body.scrollHeight || body.offsetHeight, MAX_CAPTURE_HEIGHT_PX);
|
|
56
|
+
iframe.style.height = `${height}px`;
|
|
57
|
+
return await toJpeg(body, {
|
|
58
|
+
quality: JPEG_QUALITY,
|
|
59
|
+
width: PREVIEW_WIDTH_PX,
|
|
60
|
+
height,
|
|
61
|
+
pixelRatio: 1,
|
|
62
|
+
skipFonts: false,
|
|
63
|
+
cacheBust: true,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
console.warn("Failed to capture email preview screenshot:", error);
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
finally {
|
|
71
|
+
iframe.remove();
|
|
72
|
+
}
|
|
73
|
+
}
|
package/dist/lib/cn.d.ts
ADDED
package/dist/lib/cn.js
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
const MERGE_TAG_PATTERN = /\{\{([a-zA-Z0-9_]+)\}\}/g;
|
|
2
|
+
export function extractMergeTags(content) {
|
|
3
|
+
const tags = [];
|
|
4
|
+
const seen = new Set();
|
|
5
|
+
let match;
|
|
6
|
+
const pattern = new RegExp(MERGE_TAG_PATTERN.source, "g");
|
|
7
|
+
while ((match = pattern.exec(content)) !== null) {
|
|
8
|
+
const tag = match[1];
|
|
9
|
+
if (!seen.has(tag)) {
|
|
10
|
+
seen.add(tag);
|
|
11
|
+
tags.push(tag);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
return tags;
|
|
15
|
+
}
|
|
16
|
+
export function applyMergeTags(html, values) {
|
|
17
|
+
return html.replace(new RegExp(MERGE_TAG_PATTERN.source, "g"), (full, tag) => {
|
|
18
|
+
if (Object.prototype.hasOwnProperty.call(values, tag)) {
|
|
19
|
+
return values[tag];
|
|
20
|
+
}
|
|
21
|
+
return full;
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
export function getMergeTagError(content, allowedTags) {
|
|
25
|
+
const allowed = new Set();
|
|
26
|
+
for (const tag of allowedTags) {
|
|
27
|
+
const name = tag.replace(/[{}]/g, "").trim();
|
|
28
|
+
if (name)
|
|
29
|
+
allowed.add(name);
|
|
30
|
+
}
|
|
31
|
+
const unknown = new Set();
|
|
32
|
+
let match;
|
|
33
|
+
const tagPattern = new RegExp(MERGE_TAG_PATTERN.source, "g");
|
|
34
|
+
while ((match = tagPattern.exec(content)) !== null) {
|
|
35
|
+
if (!allowed.has(match[1])) {
|
|
36
|
+
unknown.add(match[1]);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
if (unknown.size === 0)
|
|
40
|
+
return null;
|
|
41
|
+
const allowedList = [...allowed].map((t) => `{{${t}}}`).join(", ");
|
|
42
|
+
return (`Unknown merge tag${unknown.size > 1 ? "s" : ""}: ` +
|
|
43
|
+
`${[...unknown].map((t) => `{{${t}}}`).join(", ")}. ` +
|
|
44
|
+
`Allowed merge tags are: ${allowedList}.`);
|
|
45
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/** MIME types Gemini accepts for vision input. */
|
|
2
|
+
export const AI_SAFE_IMAGE_TYPES = new Set([
|
|
3
|
+
"image/jpeg",
|
|
4
|
+
"image/png",
|
|
5
|
+
"image/webp",
|
|
6
|
+
]);
|
|
7
|
+
function loadImageElement(src) {
|
|
8
|
+
return new Promise((resolve, reject) => {
|
|
9
|
+
const img = new Image();
|
|
10
|
+
img.onload = () => resolve(img);
|
|
11
|
+
img.onerror = () => reject(new Error("Failed to decode image"));
|
|
12
|
+
img.src = src;
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
async function rasterizeToPngBlob(file) {
|
|
16
|
+
const objectUrl = URL.createObjectURL(file);
|
|
17
|
+
try {
|
|
18
|
+
const img = await loadImageElement(objectUrl);
|
|
19
|
+
const width = img.naturalWidth || img.width || 1200;
|
|
20
|
+
const height = img.naturalHeight || img.height || 800;
|
|
21
|
+
const canvas = document.createElement("canvas");
|
|
22
|
+
canvas.width = Math.max(1, width);
|
|
23
|
+
canvas.height = Math.max(1, height);
|
|
24
|
+
const ctx = canvas.getContext("2d");
|
|
25
|
+
if (!ctx)
|
|
26
|
+
throw new Error("Canvas unavailable");
|
|
27
|
+
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
|
|
28
|
+
const blob = await new Promise((resolve) => canvas.toBlob(resolve, "image/png"));
|
|
29
|
+
if (!blob)
|
|
30
|
+
throw new Error("Failed to rasterize image");
|
|
31
|
+
return blob;
|
|
32
|
+
}
|
|
33
|
+
finally {
|
|
34
|
+
URL.revokeObjectURL(objectUrl);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
/** Converts SVG/GIF/etc. to PNG before upload so AI vision can read them. */
|
|
38
|
+
export async function prepareImageFileForUpload(file) {
|
|
39
|
+
if (AI_SAFE_IMAGE_TYPES.has(file.type))
|
|
40
|
+
return file;
|
|
41
|
+
if (!file.type.startsWith("image/")) {
|
|
42
|
+
throw new Error("Only image files are supported.");
|
|
43
|
+
}
|
|
44
|
+
const blob = await rasterizeToPngBlob(file);
|
|
45
|
+
const baseName = (file.name || "image").replace(/\.[^.]+$/, "") || "image";
|
|
46
|
+
return new File([blob], `${baseName}.png`, { type: "image/png" });
|
|
47
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function stripHtmlCodeFences(text: string): string;
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export declare const AiEmailImageSchema: z.ZodObject<{
|
|
3
|
+
id: z.ZodString;
|
|
4
|
+
url: z.ZodString;
|
|
5
|
+
name: z.ZodOptional<z.ZodString>;
|
|
6
|
+
}, "strip", z.ZodTypeAny, {
|
|
7
|
+
id: string;
|
|
8
|
+
url: string;
|
|
9
|
+
name?: string | undefined;
|
|
10
|
+
}, {
|
|
11
|
+
id: string;
|
|
12
|
+
url: string;
|
|
13
|
+
name?: string | undefined;
|
|
14
|
+
}>;
|
|
15
|
+
export declare const AiEmailChatMessageSchema: z.ZodObject<{
|
|
16
|
+
id: z.ZodString;
|
|
17
|
+
role: z.ZodEnum<["user", "assistant"]>;
|
|
18
|
+
content: z.ZodString;
|
|
19
|
+
contextImages: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
20
|
+
id: z.ZodString;
|
|
21
|
+
url: z.ZodString;
|
|
22
|
+
name: z.ZodOptional<z.ZodString>;
|
|
23
|
+
}, "strip", z.ZodTypeAny, {
|
|
24
|
+
id: string;
|
|
25
|
+
url: string;
|
|
26
|
+
name?: string | undefined;
|
|
27
|
+
}, {
|
|
28
|
+
id: string;
|
|
29
|
+
url: string;
|
|
30
|
+
name?: string | undefined;
|
|
31
|
+
}>, "many">>;
|
|
32
|
+
appliedHtml: z.ZodOptional<z.ZodBoolean>;
|
|
33
|
+
snapshotId: z.ZodOptional<z.ZodString>;
|
|
34
|
+
selectedBlockIds: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
|
|
35
|
+
phase: z.ZodOptional<z.ZodEnum<["thinking", "planning", "applying"]>>;
|
|
36
|
+
timestamp: z.ZodNumber;
|
|
37
|
+
}, "strip", z.ZodTypeAny, {
|
|
38
|
+
id: string;
|
|
39
|
+
role: "user" | "assistant";
|
|
40
|
+
content: string;
|
|
41
|
+
timestamp: number;
|
|
42
|
+
contextImages?: {
|
|
43
|
+
id: string;
|
|
44
|
+
url: string;
|
|
45
|
+
name?: string | undefined;
|
|
46
|
+
}[] | undefined;
|
|
47
|
+
appliedHtml?: boolean | undefined;
|
|
48
|
+
snapshotId?: string | undefined;
|
|
49
|
+
selectedBlockIds?: string[] | undefined;
|
|
50
|
+
phase?: "thinking" | "planning" | "applying" | undefined;
|
|
51
|
+
}, {
|
|
52
|
+
id: string;
|
|
53
|
+
role: "user" | "assistant";
|
|
54
|
+
content: string;
|
|
55
|
+
timestamp: number;
|
|
56
|
+
contextImages?: {
|
|
57
|
+
id: string;
|
|
58
|
+
url: string;
|
|
59
|
+
name?: string | undefined;
|
|
60
|
+
}[] | undefined;
|
|
61
|
+
appliedHtml?: boolean | undefined;
|
|
62
|
+
snapshotId?: string | undefined;
|
|
63
|
+
selectedBlockIds?: string[] | undefined;
|
|
64
|
+
phase?: "thinking" | "planning" | "applying" | undefined;
|
|
65
|
+
}>;
|
|
66
|
+
export declare const AiEmailHtmlSnapshotSchema: z.ZodObject<{
|
|
67
|
+
id: z.ZodString;
|
|
68
|
+
html: z.ZodString;
|
|
69
|
+
label: z.ZodOptional<z.ZodString>;
|
|
70
|
+
timestamp: z.ZodNumber;
|
|
71
|
+
}, "strip", z.ZodTypeAny, {
|
|
72
|
+
id: string;
|
|
73
|
+
timestamp: number;
|
|
74
|
+
html: string;
|
|
75
|
+
label?: string | undefined;
|
|
76
|
+
}, {
|
|
77
|
+
id: string;
|
|
78
|
+
timestamp: number;
|
|
79
|
+
html: string;
|
|
80
|
+
label?: string | undefined;
|
|
81
|
+
}>;
|
|
82
|
+
export declare const AiEmailSessionSchema: z.ZodObject<{
|
|
83
|
+
messages: z.ZodDefault<z.ZodArray<z.ZodObject<{
|
|
84
|
+
id: z.ZodString;
|
|
85
|
+
role: z.ZodEnum<["user", "assistant"]>;
|
|
86
|
+
content: z.ZodString;
|
|
87
|
+
contextImages: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
88
|
+
id: z.ZodString;
|
|
89
|
+
url: z.ZodString;
|
|
90
|
+
name: z.ZodOptional<z.ZodString>;
|
|
91
|
+
}, "strip", z.ZodTypeAny, {
|
|
92
|
+
id: string;
|
|
93
|
+
url: string;
|
|
94
|
+
name?: string | undefined;
|
|
95
|
+
}, {
|
|
96
|
+
id: string;
|
|
97
|
+
url: string;
|
|
98
|
+
name?: string | undefined;
|
|
99
|
+
}>, "many">>;
|
|
100
|
+
appliedHtml: z.ZodOptional<z.ZodBoolean>;
|
|
101
|
+
snapshotId: z.ZodOptional<z.ZodString>;
|
|
102
|
+
selectedBlockIds: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
|
|
103
|
+
phase: z.ZodOptional<z.ZodEnum<["thinking", "planning", "applying"]>>;
|
|
104
|
+
timestamp: z.ZodNumber;
|
|
105
|
+
}, "strip", z.ZodTypeAny, {
|
|
106
|
+
id: string;
|
|
107
|
+
role: "user" | "assistant";
|
|
108
|
+
content: string;
|
|
109
|
+
timestamp: number;
|
|
110
|
+
contextImages?: {
|
|
111
|
+
id: string;
|
|
112
|
+
url: string;
|
|
113
|
+
name?: string | undefined;
|
|
114
|
+
}[] | undefined;
|
|
115
|
+
appliedHtml?: boolean | undefined;
|
|
116
|
+
snapshotId?: string | undefined;
|
|
117
|
+
selectedBlockIds?: string[] | undefined;
|
|
118
|
+
phase?: "thinking" | "planning" | "applying" | undefined;
|
|
119
|
+
}, {
|
|
120
|
+
id: string;
|
|
121
|
+
role: "user" | "assistant";
|
|
122
|
+
content: string;
|
|
123
|
+
timestamp: number;
|
|
124
|
+
contextImages?: {
|
|
125
|
+
id: string;
|
|
126
|
+
url: string;
|
|
127
|
+
name?: string | undefined;
|
|
128
|
+
}[] | undefined;
|
|
129
|
+
appliedHtml?: boolean | undefined;
|
|
130
|
+
snapshotId?: string | undefined;
|
|
131
|
+
selectedBlockIds?: string[] | undefined;
|
|
132
|
+
phase?: "thinking" | "planning" | "applying" | undefined;
|
|
133
|
+
}>, "many">>;
|
|
134
|
+
snapshots: z.ZodDefault<z.ZodArray<z.ZodObject<{
|
|
135
|
+
id: z.ZodString;
|
|
136
|
+
html: z.ZodString;
|
|
137
|
+
label: z.ZodOptional<z.ZodString>;
|
|
138
|
+
timestamp: z.ZodNumber;
|
|
139
|
+
}, "strip", z.ZodTypeAny, {
|
|
140
|
+
id: string;
|
|
141
|
+
timestamp: number;
|
|
142
|
+
html: string;
|
|
143
|
+
label?: string | undefined;
|
|
144
|
+
}, {
|
|
145
|
+
id: string;
|
|
146
|
+
timestamp: number;
|
|
147
|
+
html: string;
|
|
148
|
+
label?: string | undefined;
|
|
149
|
+
}>, "many">>;
|
|
150
|
+
snapshotIndex: z.ZodDefault<z.ZodNumber>;
|
|
151
|
+
contextImages: z.ZodDefault<z.ZodArray<z.ZodObject<{
|
|
152
|
+
id: z.ZodString;
|
|
153
|
+
url: z.ZodString;
|
|
154
|
+
name: z.ZodOptional<z.ZodString>;
|
|
155
|
+
}, "strip", z.ZodTypeAny, {
|
|
156
|
+
id: string;
|
|
157
|
+
url: string;
|
|
158
|
+
name?: string | undefined;
|
|
159
|
+
}, {
|
|
160
|
+
id: string;
|
|
161
|
+
url: string;
|
|
162
|
+
name?: string | undefined;
|
|
163
|
+
}>, "many">>;
|
|
164
|
+
}, "strip", z.ZodTypeAny, {
|
|
165
|
+
contextImages: {
|
|
166
|
+
id: string;
|
|
167
|
+
url: string;
|
|
168
|
+
name?: string | undefined;
|
|
169
|
+
}[];
|
|
170
|
+
messages: {
|
|
171
|
+
id: string;
|
|
172
|
+
role: "user" | "assistant";
|
|
173
|
+
content: string;
|
|
174
|
+
timestamp: number;
|
|
175
|
+
contextImages?: {
|
|
176
|
+
id: string;
|
|
177
|
+
url: string;
|
|
178
|
+
name?: string | undefined;
|
|
179
|
+
}[] | undefined;
|
|
180
|
+
appliedHtml?: boolean | undefined;
|
|
181
|
+
snapshotId?: string | undefined;
|
|
182
|
+
selectedBlockIds?: string[] | undefined;
|
|
183
|
+
phase?: "thinking" | "planning" | "applying" | undefined;
|
|
184
|
+
}[];
|
|
185
|
+
snapshots: {
|
|
186
|
+
id: string;
|
|
187
|
+
timestamp: number;
|
|
188
|
+
html: string;
|
|
189
|
+
label?: string | undefined;
|
|
190
|
+
}[];
|
|
191
|
+
snapshotIndex: number;
|
|
192
|
+
}, {
|
|
193
|
+
contextImages?: {
|
|
194
|
+
id: string;
|
|
195
|
+
url: string;
|
|
196
|
+
name?: string | undefined;
|
|
197
|
+
}[] | undefined;
|
|
198
|
+
messages?: {
|
|
199
|
+
id: string;
|
|
200
|
+
role: "user" | "assistant";
|
|
201
|
+
content: string;
|
|
202
|
+
timestamp: number;
|
|
203
|
+
contextImages?: {
|
|
204
|
+
id: string;
|
|
205
|
+
url: string;
|
|
206
|
+
name?: string | undefined;
|
|
207
|
+
}[] | undefined;
|
|
208
|
+
appliedHtml?: boolean | undefined;
|
|
209
|
+
snapshotId?: string | undefined;
|
|
210
|
+
selectedBlockIds?: string[] | undefined;
|
|
211
|
+
phase?: "thinking" | "planning" | "applying" | undefined;
|
|
212
|
+
}[] | undefined;
|
|
213
|
+
snapshots?: {
|
|
214
|
+
id: string;
|
|
215
|
+
timestamp: number;
|
|
216
|
+
html: string;
|
|
217
|
+
label?: string | undefined;
|
|
218
|
+
}[] | undefined;
|
|
219
|
+
snapshotIndex?: number | undefined;
|
|
220
|
+
}>;
|
|
221
|
+
export type AiEmailImage = z.infer<typeof AiEmailImageSchema>;
|
|
222
|
+
export type AiEmailChatMessage = z.infer<typeof AiEmailChatMessageSchema>;
|
|
223
|
+
export type AiEmailHtmlSnapshot = z.infer<typeof AiEmailHtmlSnapshotSchema>;
|
|
224
|
+
export type AiEmailSession = z.infer<typeof AiEmailSessionSchema>;
|