Skip to content

Commit 4a019e2

Browse files
committed
update
1 parent a507261 commit 4a019e2

File tree

5 files changed

+110
-273
lines changed

5 files changed

+110
-273
lines changed

docs/3-web-servers/14-project/_samples/ai-todo/main.mjs

Lines changed: 53 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -7,88 +7,77 @@ const client = new PrismaClient();
77
app.use(express.json());
88
app.use(express.static("./public"));
99

10-
const systemPrompt = `ユーザーの発話からタスクと時間を抽出してください。
11-
出力は必ず2行で、1行目がISO8601形式の日時(タイムゾーンは+09:00)、2行目がタスクタイトルです。
12-
時間情報がない場合は1行目を空にしてください。
13-
14-
例:
15-
入力: 明日の10時に会議
16-
出力:
17-
2024-01-21T10:00:00+09:00
18-
会議
19-
20-
入力: 買い物に行く
21-
出力:
22-
23-
買い物に行く`;
24-
25-
// 自然言語でタスクを追加(AI解析 + DB保存)
26-
app.post("/todos/ai", async (request, response) => {
27-
try {
28-
const result = await fetch(
29-
"https://openrouter.ai/api/v1/chat/completions",
30-
{
31-
method: "POST",
32-
headers: {
33-
Authorization: `Bearer ${process.env.OPENROUTER_API_KEY}`,
34-
"Content-Type": "application/json",
35-
},
36-
body: JSON.stringify({
37-
model: "google/gemini-3-flash-preview",
38-
messages: [
39-
{ role: "system", content: systemPrompt },
40-
{ role: "user", content: request.body.text },
41-
],
42-
}),
43-
},
44-
);
45-
const data = await result.json();
46-
47-
if (!data.choices || !data.choices[0]) {
48-
response.status(500).json({ error: "AIからの応答が不正です" });
49-
return;
50-
}
51-
52-
const content = data.choices[0].message.content;
53-
const lines = content.split("\n");
54-
const dueAt = lines[0] ? new Date(lines[0]) : null;
55-
const title = lines[1] || "";
56-
57-
const todo = await client.todo.create({
58-
data: { title, due_at: dueAt },
59-
});
60-
response.json(todo);
61-
} catch (error) {
62-
console.error("Parse error:", error);
63-
response.status(500).json({ error: "解析に失敗しました" });
64-
}
65-
});
66-
67-
// タスク一覧を取得
6810
app.get("/todos", async (request, response) => {
69-
const todos = await client.todo.findMany({
70-
orderBy: { createdAt: "desc" },
71-
});
11+
const todos = await client.todo.findMany({ orderBy: { id: "asc" } });
7212
response.json(todos);
7313
});
7414

75-
// タスクを追加
7615
app.post("/todos", async (request, response) => {
16+
let dueAt = null;
17+
if (request.body.dueAt) {
18+
dueAt = new Date(request.body.dueAt);
19+
}
7720
const todo = await client.todo.create({
7821
data: {
7922
title: request.body.title,
80-
due_at: request.body.due_at ? new Date(request.body.due_at) : null,
23+
dueAt: dueAt,
8124
},
8225
});
8326
response.json(todo);
8427
});
8528

86-
// タスクを削除
8729
app.delete("/todos/:id", async (request, response) => {
8830
await client.todo.delete({
8931
where: { id: parseInt(request.params.id) },
9032
});
9133
response.json({ success: true });
9234
});
9335

36+
app.post("/todos/ai", async (request, response) => {
37+
const systemPrompt = `
38+
ユーザーの入力から、ToDoのタイトルと期限を抽出してください。
39+
1行目にタイトル、2行目に期限(ISO8601形式、タイムゾーンは東京)を出力してください。
40+
現在日時: ${new Date().toISOString()}
41+
42+
例:
43+
入力: 明日の10時に会議
44+
出力:
45+
会議
46+
2024-01-21T10:00:00+09:00
47+
48+
入力: 買い物に行く
49+
出力:
50+
買い物に行く
51+
`;
52+
53+
const result = await fetch("https://openrouter.ai/api/v1/chat/completions", {
54+
method: "POST",
55+
headers: {
56+
Authorization: `Bearer ${process.env.OPENROUTER_API_KEY}`,
57+
"Content-Type": "application/json",
58+
},
59+
body: JSON.stringify({
60+
model: "openrouter/free",
61+
messages: [
62+
{ role: "system", content: systemPrompt },
63+
{ role: "user", content: request.body.instruction },
64+
],
65+
}),
66+
});
67+
const data = await result.json();
68+
console.log(data);
69+
const content = data.choices[0].message.content;
70+
console.log(content);
71+
const lines = content.split("\n");
72+
const title = lines[0];
73+
let dueAt = null;
74+
if (lines[1]) {
75+
dueAt = new Date(lines[1]);
76+
}
77+
const todo = await client.todo.create({
78+
data: { title: title, dueAt: dueAt },
79+
});
80+
response.json(todo);
81+
});
82+
9483
app.listen(3000);

docs/3-web-servers/14-project/_samples/ai-todo/prisma/schema.prisma

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,7 @@ datasource db {
99
}
1010

1111
model Todo {
12-
id Int @id @default(autoincrement())
13-
title String
14-
due_at DateTime?
15-
createdAt DateTime @default(now())
12+
id Int @id @default(autoincrement())
13+
title String
14+
dueAt DateTime?
1615
}

docs/3-web-servers/14-project/_samples/ai-todo/public/index.html

Lines changed: 12 additions & 105 deletions
Original file line numberDiff line numberDiff line change
@@ -2,115 +2,22 @@
22
<html lang="ja">
33
<head>
44
<meta charset="utf-8" />
5-
<title>AIタスク管理</title>
6-
<style>
7-
body {
8-
font-family: sans-serif;
9-
max-width: 600px;
10-
margin: 40px auto;
11-
padding: 0 20px;
12-
}
13-
h1 {
14-
color: #333;
15-
}
16-
.input-area {
17-
display: flex;
18-
gap: 10px;
19-
margin-bottom: 20px;
20-
}
21-
#task-input {
22-
flex: 1;
23-
padding: 10px;
24-
font-size: 16px;
25-
border: 1px solid #ccc;
26-
border-radius: 4px;
27-
}
28-
button {
29-
padding: 10px 20px;
30-
font-size: 16px;
31-
background-color: #4a90d9;
32-
color: white;
33-
border: none;
34-
border-radius: 4px;
35-
cursor: pointer;
36-
}
37-
button:hover {
38-
background-color: #357abd;
39-
}
40-
button:disabled {
41-
background-color: #ccc;
42-
cursor: not-allowed;
43-
}
44-
#voice-button {
45-
background-color: #27ae60;
46-
}
47-
#voice-button:hover:not(:disabled) {
48-
background-color: #1e8449;
49-
}
50-
#voice-button.recording {
51-
background-color: #e74c3c;
52-
}
53-
#todo-list {
54-
list-style: none;
55-
padding: 0;
56-
}
57-
#todo-list li {
58-
display: flex;
59-
justify-content: space-between;
60-
align-items: center;
61-
padding: 15px;
62-
border-bottom: 1px solid #eee;
63-
}
64-
.todo-info {
65-
display: flex;
66-
flex-direction: column;
67-
gap: 5px;
68-
}
69-
.todo-title {
70-
font-size: 16px;
71-
}
72-
.todo-time {
73-
font-size: 12px;
74-
color: #666;
75-
}
76-
.delete-button {
77-
background-color: #e74c3c;
78-
padding: 5px 10px;
79-
font-size: 14px;
80-
}
81-
.delete-button:hover {
82-
background-color: #c0392b;
83-
}
84-
.notice {
85-
background-color: #fff3cd;
86-
border: 1px solid #ffc107;
87-
border-radius: 4px;
88-
padding: 10px;
89-
margin-bottom: 20px;
90-
font-size: 14px;
91-
}
92-
.status {
93-
text-align: center;
94-
padding: 10px;
95-
color: #666;
96-
}
97-
.status.error {
98-
color: #e74c3c;
99-
}
100-
</style>
5+
<title>AI ToDo</title>
1016
</head>
1027
<body>
103-
<h1>AIタスク管理</h1>
104-
<div class="notice">
105-
音声認識機能はChrome/Edgeで動作します。<br />
106-
「明日の10時に会議」のように話すと、AIが時間とタスクを自動抽出します。
107-
</div>
108-
<div class="input-area">
109-
<input type="text" id="task-input" placeholder="タスクを入力(または音声入力)" />
110-
<button id="voice-button" type="button">音声入力</button>
8+
<h1>AI ToDo</h1>
9+
<h2>ToDoの追加</h2>
10+
<div>
11+
<input id="title-input" />
12+
<input id="due-at-input" type="datetime-local" />
11113
<button id="add-button" type="button">追加</button>
11214
</div>
113-
<div id="status" class="status"></div>
15+
<h2>AIでToDoを追加</h2>
16+
<div>
17+
<input id="instruction-input" />
18+
<button id="ai-button" type="button">AIで追加</button>
19+
</div>
20+
<h2>ToDoリスト</h2>
11421
<ul id="todo-list"></ul>
11522
<script src="./script.js"></script>
11623
</body>

0 commit comments

Comments
 (0)