CronGPT

This is a minisite to help you create cron expressions, particularly for crons on Val Town. It was inspired by Cron Prompt, but also does the timezone conversion from wherever you are to UTC (typically the server timezone).

Tech

  • Hono for routing (GET / and POST /compile.)
  • Hono JSX
  • HTML (probably overcomplicates things; should remove)
  • @stevekrouse/openai, which is a light wrapper around @std/openai
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
/** @jsxImportSource npm:hono@3/jsx */
import { chat } from "https://esm.town/v/stevekrouse/openai";
import cronstrue from "npm:cronstrue";
import { Hono } from "npm:hono@3";
const app = new Hono();
export default app.fetch;
app.get("/", (c) =>
c.html(
<html>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<script src="https://cdn.tailwindcss.com" />
<title>CronGPT</title>
</head>
<body class="flex p-6 mt-4 flex-col space-y-12 max-w-2xl mx-auto">
<div class="flex flex-col text-center space-y-2">
<h1 class="text-3xl font-bold">CronGPT</h1>
<p class="text-lg">Generate cron expressions with ChatGPT</p>
</div>
<form class="flex flex-col space-y-4">
<div class="flex flex-col">
<label for="description">Natural language description</label>
<input
name="description"
value="On weekdays at noon"
required
class="border-2 rounded-lg p-2 text-lg text-center"
/>
</div>
<div class="flex flex-col">
<label for="description">Timezone</label>
<select name="timezone" class=" border-2 rounded-lg text-lg p-2 text-center">
{Intl.supportedValuesOf("timeZone").map((tz) => (
<option value={tz} id={tz.replace("/", "-")}>{tz}</option>
))}
</select>
</div>
<button class="bg-sky-500 hover:bg-sky-700 text-white font-bold py-2 px-4 rounded disabled:bg-gray-500">
Compile
</button>
</form>
<div>
<Cron cron="0 16 * * 1-5" timezone="America/New_York" />
</div>
<div class="text-gray-500 text-center flex flex-col space-y-2">
<div>
Need a place to run cron jobs? Try <Link href="https://val.town">Val Town</Link>
</div>{" "}
<div>
<Link href="https://www.val.town/v/stevekrouse/cron">View source</Link>
{" | "} Inspired by <Link href="https://cronprompt.com/">Cron Prompt</Link>
</div>
</div>
</body>
<script dangerouslySetInnerHTML={{ __html: `${clientScript}; clientScript()` }} />
</html>,
));
app.post("/compile", async (c) => {
const form = await c.req.formData();
const description = form.get("description") as string;
const timezone = form.get("timezone") as string;
const { content } = await chat([
{
role: "system",
content: `You are an natural language to cron syntax compiler.
Return a cron expression in UTC.
The user is in ${timezone}. The timezone offset is ${getOffset(timezone)}.
Return ONLY the cron expression.`,
},
{ role: "user", content: description },
], {
max_tokens: 10,
});
const cron = content.replace("`", "");
return c.html(<Cron cron={cron} timezone={timezone} />);
});
function clientScript() {
// Set the timezone input
const tz = Intl.DateTimeFormat().resolvedOptions().timeZone;
(document.querySelector("#" + tz.replace("/", "-")) as HTMLOptionElement).selected = true;
const form = document.querySelector("form") as HTMLFormElement;
const button = document.querySelector("button") as HTMLButtonElement;
form.addEventListener("submit", async (e) => {
e.preventDefault();
button.disabled = true;
button.innerText = "Loading..";
const cron = await fetch("/compile", { method: "POST", body: new FormData(form) }).then((r) => r.text());
button.disabled = false;
button.innerText = "Compile";
(document.getElementById("cron") as HTMLDivElement).innerHTML = cron;
});
}
function Cron({ cron, timezone }) {
let translation;
try {
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
/** @jsxImportSource npm:hono@3/jsx */
import { chat } from "https://esm.town/v/stevekrouse/openai";
import cronstrue from "npm:cronstrue";
import { Hono } from "npm:hono@3";
const app = new Hono();
export default app.fetch;
app.get("/", async (c) => {
const description = c.req.query("description") || "On weekdays at noon";
const timezone = c.req.query("timezone") || "America/New_York";
const cron = c.req.query("description") ? await compile(description, timezone) : "0 16 * * 1-5";
let translated;
try {
translated = cronstrue.toString(cron, { tzOffset: getOffset(timezone), use24HourTimeFormat: false });
} catch (e)
{
translated = "Error in cron expression " + e.message;
}
return c.html(
<html>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<script src="https://cdn.tailwindcss.com" />
<title>Cron Compiler</title>
</head>
<body class="flex p-6 flex-col space-y-4 max-w-2xl mx-auto">
<div>
<h1 class="text-3xl">Cron Compiler</h1>
<p>Compile natural language to cron via ChatGPT</p>
</div>
<form class="flex space-x-2" hx-disabled-elt="button">
<input
name="description"
value={description}
required
class="w-full border-2 rounded-lg p-2"
>
</input>
<input type="hidden" name="timezone"></input>
<script
dangerouslySetInnerHTML={{ __html: `document.querySelector("input[name=timezone]").value = "${timezone}"` }}
/>
<button class="bg-purple-500 hover:bg-purple-700 text-white font-bold py-2 px-4 rounded disabled:hidden">
Compile
</button>
</form>
<div>
<pre class="font-mono text-xl text-center w-full border-2 p-2 bg-gray-200">{cron}</pre>
<p class="text-center">
{translated}
, {shortenTimezoneString(timezone)}
</p>
</div>
<div>
Inspired by <a href="https://cronprompt.com/" class="text-blue-500 hover:text-blue-700">Cron Prompt</a>.{" "}
<a href="https://www.val.town/v/stevekrouse/cron" class="text-blue-500 hover:text-blue-700">View source</a>.
</div>
</body>
</html>,
);
});
// convert a timezone string like America/New_York to "EST"
function shortenTimezoneString(timeZone: string) {
const string = new Date().toLocaleString("en-US", { timeZone, timeZoneName: "short" });
return string.split(" ").at(-1);
}
// https://stackoverflow.com/a/68593283
const getOffset = (timeZone: string) => {
const now = new Date();
const utcDate = new Date(now.toLocaleString("en-US", { timeZone: "UTC" }));
const tzDate = new Date(now.toLocaleString("en-US", { timeZone }));
return (tzDate.getTime() - utcDate.getTime()) / (60 * 60 * 1000);
};
export async function compile(description: string, timezone: string) {
const { content } = await chat([
{
role: "system",
content:
"You are a helpful assistant that converts the user's prompt into a valid crontab expression. You only reply with valid crontab expressions.",
},
{ role: "user", content: "On Juneteenth every hour" },
{ role: "assistant", content: "0 * 19 6 * *" },
{ role: "user", content: "At midnight on christmas" },
{ role: "assistant", content: "0 0 25 12 * *" },
{ role: "user", content: prompt },
], {
max_tokens: 10,
});
return content.replace("`", "");
}
1
Next