Skip to content

Commit ea6a9e5

Browse files
authored
Merge pull request #14 from ut-code/action
サーバーアクションに変更&ドキュメントをAIに読ませる
2 parents f6d1ddd + 479306d commit ea6a9e5

File tree

6 files changed

+116
-104
lines changed

6 files changed

+116
-104
lines changed

app/[docs_id]/chatForm.tsx

Lines changed: 53 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,9 @@
11
"use client";
22

33
import { useState, FormEvent } from "react";
4+
import { askAI } from "@/app/actions/chatActions";
45

5-
interface ChatApiResponse {
6-
response: string;
7-
}
8-
9-
export function ChatForm() {
6+
export function ChatForm({ documentContent }: { documentContent: string }) {
107
const [inputValue, setInputValue] = useState("");
118
const [response, setResponse] = useState("");
129
const [isLoading, setIsLoading] = useState(false);
@@ -17,96 +14,85 @@ export function ChatForm() {
1714
setIsLoading(true);
1815
setResponse("");
1916

20-
try {
21-
const res = await fetch("/api/chat", {
22-
method: "POST",
23-
headers: {
24-
"Content-Type": "application/json",
25-
},
26-
body: JSON.stringify({ message: inputValue }),
27-
});
17+
const result = await askAI({
18+
userQuestion: inputValue,
19+
documentContent: documentContent,
20+
});
2821

29-
const data = (await res.json()) as ChatApiResponse;
30-
if (!res.ok) {
31-
throw new Error(data.response || "エラーが発生しました。");
32-
}
33-
setResponse(data.response);
34-
} catch (error: unknown) {
35-
if (error instanceof Error) {
36-
setResponse(`エラー: ${error.message}`);
37-
} else {
38-
setResponse(`エラー: ${String(error)}`);
39-
}
40-
} finally {
41-
setIsLoading(false);
22+
if (result.error) {
23+
setResponse(`エラー: ${result.error}`);
24+
} else {
25+
setResponse(result.response);
4226
}
27+
28+
setIsLoading(false);
4329
};
4430
return (
4531
<>
4632
{isFormVisible && (
4733
<form className="border border-2 border-primary shadow-xl p-6 rounded-lg bg-base-100" style={{width:"100%", textAlign:"center", boxShadow:"-moz-initial"}} onSubmit={handleSubmit}>
48-
<h2 className="text-xl font-bold mb-4 text-left relative -top-2 font-mono h-2">
49-
AIへ質問
50-
</h2>
34+
<h2 className="text-xl font-bold mb-4 text-left relative -top-2 font-mono h-2">
35+
AIへ質問
36+
</h2>
5137
<div className="input-area" style={{height:"80px"}}>
52-
<textarea
53-
className="textarea textarea-white textarea-md"
54-
placeholder="質問を入力してください"
38+
<textarea
39+
className="textarea textarea-white textarea-md"
40+
placeholder="質問を入力してください"
5541
style={{width: "100%", height: "110px", resize: "none"}}
56-
value={inputValue}
57-
onChange={(e) => setInputValue(e.target.value)}
58-
disabled={isLoading}
59-
></textarea>
60-
</div>
61-
<br />
42+
value={inputValue}
43+
onChange={(e) => setInputValue(e.target.value)}
44+
disabled={isLoading}
45+
></textarea>
46+
</div>
47+
<br />
6248
<div className="controls" style={{position:"relative", top:"22px", display:"flex", alignItems:"center", justifyContent:"space-between"}}>
63-
<div className="left-icons">
64-
<button
65-
className="btn btn-soft btn-secondary rounded-full"
66-
onClick={() => setIsFormVisible(false)}
67-
>
49+
<div className="left-icons">
50+
<button
51+
className="btn btn-soft btn-secondary rounded-full"
52+
onClick={() => setIsFormVisible(false)}
53+
>
6854

69-
閉じる
70-
</button>
71-
</div>
72-
<div className="right-controls">
73-
<button
74-
type="submit"
75-
className="btn btn-soft btn-circle btn-primary rounded-full"
76-
title="送信"
55+
閉じる
56+
</button>
57+
</div>
58+
<div className="right-controls">
59+
<button
60+
type="submit"
61+
className="btn btn-soft btn-circle btn-primary rounded-full"
62+
title="送信"
7763
style={{marginTop:"10px"}}
78-
disabled={isLoading}
79-
>
80-
<span className="icon"></span>
81-
</button>
64+
disabled={isLoading}
65+
>
66+
<span className="icon"></span>
67+
</button>
68+
</div>
8269
</div>
83-
</div>
84-
</form>
70+
</form>
8571
)}
8672
{!isFormVisible && (
87-
<button
88-
className="btn btn-soft btn-secondary rounded-full"
89-
onClick={() => setIsFormVisible(true)}
90-
>
91-
チャットを開く
92-
</button>
73+
<button
74+
className="btn btn-soft btn-secondary rounded-full"
75+
onClick={() => setIsFormVisible(true)}
76+
>
77+
チャットを開く
78+
</button>
9379
)}
9480

9581
{response && (
9682
<article>
9783
<h3 className="text-lg font-semibold mb-2">AIの回答</h3>
9884
<div className="chat chat-start">
9985
<div className="chat-bubble chat-bubble-primary">
100-
<div className="response-container">{response}</div>
86+
<div className="response-container">{response}</div>
10187
</div>
10288
</div>
10389
</article>
10490
)}
10591

10692
{isLoading && (
107-
<div className="mt-2 text-l text-gray-500 animate-pulse">
108-
AIが考え中です…
109-
</div>
93+
<div className="mt-2 text-l text-gray-500 animate-pulse">
94+
AIが考え中です…
95+
</div>
11096
)}
11197

11298
</>

app/[docs_id]/section.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ export function Section({ section }: { section: MarkdownSection }) {
1111
<div>
1212
<Heading level={section.level}>{section.title}</Heading>
1313
<StyledMarkdown content={section.content} />
14-
<ChatForm />
14+
<ChatForm documentContent={section.content} />
1515
</div>
1616
);
1717
}

app/actions/chatActions.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
'use server';
2+
3+
import { GoogleGenerativeAI } from "@google/generative-ai";
4+
import { z } from "zod";
5+
6+
interface FormState {
7+
response: string;
8+
error: string | null;
9+
}
10+
11+
const ChatSchema = z.object({
12+
userQuestion: z.string().min(1, { message: "メッセージを入力してください。" }),
13+
documentContent: z.string().min(1, { message: "コンテキストとなるドキュメントがありません。"}),
14+
});
15+
16+
const genAI = new GoogleGenerativeAI(process.env.API_KEY!);
17+
18+
type ChatParams = z.input<typeof ChatSchema>;
19+
20+
export async function askAI(params: ChatParams): Promise<FormState> {
21+
const parseResult = ChatSchema.safeParse(params);
22+
23+
if (!parseResult.success) {
24+
return {
25+
response: "",
26+
error: parseResult.error.issues.map((e) => e.message).join(", "),
27+
};
28+
}
29+
30+
const { userQuestion, documentContent } = parseResult.data;
31+
32+
try {
33+
const model = genAI.getGenerativeModel({ model: "gemini-1.5-flash" });
34+
const fullMessage = documentContent + "\n\n" + userQuestion;
35+
const result = await model.generateContent(fullMessage);
36+
const response = result.response;
37+
const text = response.text();
38+
return { response: text, error: null };
39+
} catch (error: unknown) {
40+
console.error("Error calling Generative AI:", error);
41+
if (error instanceof Error) {
42+
return { response: '', error: `AIへのリクエスト中にエラーが発生しました: ${error.message}` };
43+
}
44+
return { response: '', error: '予期せぬエラーが発生しました。' };
45+
}
46+
}

app/api/chat/route.js

Lines changed: 0 additions & 31 deletions
This file was deleted.

package-lock.json

Lines changed: 14 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@
2121
"react-dom": "19.1.0",
2222
"react-markdown": "^10.1.0",
2323
"react-syntax-highlighter": "^15.6.1",
24-
"remark-gfm": "^4.0.1"
24+
"remark-gfm": "^4.0.1",
25+
"zod": "^4.0.17"
2526
},
2627
"devDependencies": {
2728
"@eslint/eslintrc": "^3",

0 commit comments

Comments
 (0)