Skip to content

Commit 8a0e265

Browse files
committed
feat: add crontab component
1 parent 9acb2c7 commit 8a0e265

9 files changed

Lines changed: 799 additions & 19 deletions

File tree

README.md

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
11
# 💬 概述
22

3-
一款简洁高效的在线工具箱|JSON 在线解析及格式化验证、Base64 编解码、URL 编解码
3+
一款简洁高效的在线工具箱
44

55
- [x] JSON 在线解析及格式化验证
6-
- [x] Base64 编解码
7-
- [x] URL 编解码
8-
- [ ] Crontab 执行时间计算
6+
- [x] Base64 / URL 编解码
7+
- [x] Crontab 时间计算
98
- [ ] RGB 颜色值转换
109

1110
## 🚀 本地运行

app/layout.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ export const metadata: Metadata = {
1111
template: "%s - 在线工具箱",
1212
},
1313
description:
14-
"一款简洁高效的在线工具箱|JSON 在线解析及格式化验证Base64 编解码、URL 编解码",
14+
"一款简洁高效的在线工具箱|JSON 在线解析及格式化验证,Crontab 执行时间计算,Base64 / URL 编解码",
1515
appleWebApp: {
1616
title: "在线工具箱",
1717
},

app/server.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
"use server";
2+
import { CronJob } from "cron";
3+
import { createStreamableValue } from "ai/rsc";
4+
import { CoreMessage, streamText } from "ai";
5+
import { createOpenAI } from "@ai-sdk/openai";
6+
import { CrontabPrompt } from "@/lib/constants";
7+
8+
export async function crontab(cron: string): Promise<any> {
9+
try {
10+
const job = new CronJob(cron, () => {});
11+
return job.nextDates(7).map((v) => v.toFormat("yyyy-LL-dd HH:mm:ss"));
12+
} catch (err: any) {
13+
return { error: err.message };
14+
}
15+
}
16+
17+
const model = process.env.OPENAI_MODEL ?? "gpt-3.5-turbo";
18+
const openai = createOpenAI({ baseURL: process.env.OPENAI_BASE_URL });
19+
20+
export async function crontabAI(prompt: string) {
21+
return await askAI(CrontabPrompt(prompt));
22+
}
23+
24+
async function askAI(messages: CoreMessage[]) {
25+
const stream = createStreamableValue();
26+
try {
27+
const { textStream } = await streamText({
28+
temperature: 0.2,
29+
model: openai(model),
30+
messages,
31+
});
32+
33+
(async () => {
34+
for await (const text of textStream) {
35+
stream.update(text);
36+
}
37+
})()
38+
.catch(console.log)
39+
.finally(stream.done);
40+
41+
return { data: stream.value };
42+
} catch (err: any) {
43+
stream.done();
44+
return { error: err.message ?? err };
45+
}
46+
}

components/crontab.tsx

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
"use client";
2+
import React, { useRef, useState } from "react";
3+
import { Timer } from "lucide-react";
4+
import { crontab, crontabAI } from "@/app/server";
5+
import { readStreamableValue } from "ai/rsc";
6+
7+
function Crontab() {
8+
const [isLoading, setIsLoading] = useState(false);
9+
const inputRef = useRef<HTMLInputElement>(null);
10+
const [list, setList] = useState<string[]>([]);
11+
const [err, setErr] = useState("");
12+
13+
async function onClick() {
14+
if (!inputRef.current?.value) {
15+
return;
16+
}
17+
try {
18+
const ret = await crontab(inputRef.current.value);
19+
if (ret.error) {
20+
throw ret.error;
21+
}
22+
err && setErr("");
23+
setList(ret);
24+
} catch (err: any) {
25+
list.length && setList([]);
26+
setErr(err.message ?? err);
27+
}
28+
}
29+
30+
async function onClickAI() {
31+
if (!inputRef.current?.value) {
32+
return;
33+
}
34+
err && setErr("");
35+
list.length && setList([]);
36+
setIsLoading(true);
37+
try {
38+
const { data, error } = await crontabAI(inputRef.current.value);
39+
if (error) {
40+
setErr(error);
41+
return;
42+
}
43+
let ret = "";
44+
for await (const delta of readStreamableValue(data!)) {
45+
ret += delta;
46+
inputRef.current.value = ret;
47+
}
48+
await onClick();
49+
} catch (err: any) {
50+
setErr(err.message ?? err);
51+
} finally {
52+
setIsLoading(false);
53+
}
54+
}
55+
56+
async function onKeyDown(e: React.KeyboardEvent<HTMLInputElement>) {
57+
if (e.key !== "Enter" || e.nativeEvent.isComposing) {
58+
return;
59+
}
60+
e.preventDefault();
61+
if (isLoading) {
62+
return;
63+
}
64+
await onClick();
65+
}
66+
67+
return (
68+
<>
69+
<div className="flex w-full gap-2 sm:gap-4">
70+
<label className="input input-bordered flex grow items-center gap-2 px-3">
71+
<Timer className="opacity-70" />
72+
<input
73+
ref={inputRef}
74+
className="grow"
75+
placeholder={`Crontab 或 "每两小时执行一次"`}
76+
onKeyDown={onKeyDown}
77+
disabled={isLoading}
78+
/>
79+
</label>
80+
{isLoading ? (
81+
<span className="loading loading-infinity loading-lg mx-3" />
82+
) : (
83+
<button className="btn-normal" onClick={onClickAI}>
84+
问问 AI
85+
</button>
86+
)}
87+
</div>
88+
<button className="btn-normal" onClick={onClick} disabled={isLoading}>
89+
查看执行时间
90+
</button>
91+
{list.length !== 0 && (
92+
<div className="flex flex-col gap-2 rounded-md bg-base-200 p-3 font-medium">
93+
<span>接下来 7 次的执行时间:</span>
94+
{list.map((v, i) => (
95+
<div className="rounded-md bg-base-300 p-2" key={i}>
96+
{i + 1} - {v}
97+
</div>
98+
))}
99+
</div>
100+
)}
101+
{err && (
102+
<div className="rounded-md bg-warning/50 p-3 text-warning-content">
103+
{err}
104+
</div>
105+
)}
106+
<pre className="shrink-0 overflow-x-auto rounded-md bg-base-200 p-3">
107+
{`* * * * *
108+
| | | | |
109+
| | | | +----- day of week (0 - 7) (Sunday=0 or 7) OR sun,mon,tue,wed,thu,fri,sat
110+
| | | +-------- month (1 - 12) OR jan,feb,mar,apr ...
111+
| | +----------- day of month (1 - 31)
112+
| +-------------- hour (0 - 23)
113+
+----------------- minute (0 - 59)`}
114+
</pre>
115+
</>
116+
);
117+
}
118+
119+
export default Crontab;

lib/constants.ts

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,23 @@
1-
export const VERSION = "0.1.0";
1+
import { CoreMessage } from "ai";
2+
3+
export const VERSION = "0.1.1";
24
export const GITHUB_URL = "https://github.com/sunls24/online-tools";
5+
6+
export function CrontabPrompt(prompt: string): CoreMessage[] {
7+
return [
8+
{
9+
role: "system",
10+
content:
11+
"You are a professional Crontab generator. Don't do any interpretation, just reply to the expression. If the user sends a crontab, it is returned as is.",
12+
},
13+
{
14+
role: "user",
15+
content: "每两个小时执行一次",
16+
},
17+
{ role: "assistant", content: "0 */2 * * *" },
18+
{
19+
role: "user",
20+
content: `${prompt}`,
21+
},
22+
];
23+
}

lib/tools.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,11 @@ export const tools: readonly ToolItem[] = [
1010
image: "img/json-tree-viewer.svg",
1111
route: "json",
1212
},
13+
{
14+
name: "Crontab 时间计算",
15+
image: "img/letter-counter.svg",
16+
route: "crontab",
17+
},
1318
{
1419
name: "Base64 编解码",
1520
image: "img/base64-encoder-decoder.svg",

package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,10 @@
99
"lint": "next lint"
1010
},
1111
"dependencies": {
12+
"@ai-sdk/openai": "^0.0.34",
13+
"ai": "^3.2.16",
1214
"clsx": "^2.1.1",
15+
"cron": "^3.1.7",
1316
"lucide-react": "^0.399.0",
1417
"next": "^14.2.4",
1518
"react": "^18.3.1",

0 commit comments

Comments
 (0)