Skip to content

Commit ee6c210

Browse files
chelprocclaude
andcommitted
AIタスク管理プロジェクトのドキュメントとサンプルコードを追加
- 音声入力、AI解析、データベースを組み合わせたタスク管理アプリのチュートリアル - ステップ1: Express + Prisma での基本的なToDoリスト - ステップ2: Web Speech API での音声入力 - ステップ3: OpenRouter API でのAI解析 - 完成版サンプルコード Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 574f70e commit ee6c210

File tree

29 files changed

+6541
-0
lines changed

29 files changed

+6541
-0
lines changed
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
node_modules/
2+
generated/
3+
.env
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import express from "express";
2+
import { PrismaClient } from "./generated/prisma/index.js";
3+
4+
const app = express();
5+
const client = new PrismaClient();
6+
7+
app.use(express.json());
8+
app.use(express.static("./public"));
9+
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+
// タスク一覧を取得
68+
app.get("/todos", async (request, response) => {
69+
const todos = await client.todo.findMany({
70+
orderBy: { createdAt: "desc" },
71+
});
72+
response.json(todos);
73+
});
74+
75+
// タスクを追加
76+
app.post("/todos", async (request, response) => {
77+
const todo = await client.todo.create({
78+
data: {
79+
title: request.body.title,
80+
due_at: request.body.due_at ? new Date(request.body.due_at) : null,
81+
},
82+
});
83+
response.json(todo);
84+
});
85+
86+
// タスクを削除
87+
app.delete("/todos/:id", async (request, response) => {
88+
await client.todo.delete({
89+
where: { id: parseInt(request.params.id) },
90+
});
91+
response.json({ success: true });
92+
});
93+
94+
app.listen(3000);

0 commit comments

Comments
 (0)