Skip to content

Commit 0aaf2d6

Browse files
committed
質問の例をAIに考えさせる
1 parent 1f36931 commit 0aaf2d6

File tree

4 files changed

+104
-8
lines changed

4 files changed

+104
-8
lines changed

app/[docs_id]/chatForm.tsx

Lines changed: 52 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,54 @@
33
import { useState, FormEvent } from "react";
44
import { askAI } from "@/app/actions/chatActions";
55
import { StyledMarkdown } from "./markdown";
6+
import useSWR from "swr";
7+
import { getQuestionExample } from "../actions/questionExample";
68

7-
export function ChatForm({ documentContent }: { documentContent: string }) {
9+
export function ChatForm({
10+
docs_id,
11+
documentContent,
12+
}: {
13+
docs_id: string;
14+
documentContent: string;
15+
}) {
816
const [inputValue, setInputValue] = useState("");
917
const [response, setResponse] = useState("");
1018
const [isLoading, setIsLoading] = useState(false);
1119
const [isFormVisible, setIsFormVisible] = useState(false);
1220

21+
const lang = docs_id.split("-")[0];
22+
const { data: exampleData, error: exampleError } = useSWR(
23+
// 質問フォームを開いたときだけで良い
24+
isFormVisible ? { lang, documentContent } : null,
25+
getQuestionExample,
26+
{
27+
// リクエストは古くても構わないので1回でいい
28+
revalidateIfStale: false,
29+
revalidateOnFocus: false,
30+
revalidateOnReconnect: false,
31+
}
32+
);
33+
if (exampleError) {
34+
console.error("Error getting question example:", exampleError);
35+
}
36+
// 質問フォームを開くたびにランダムに選び直し、
37+
// exampleData[Math.floor(exampleChoice * exampleData.length)] を採用する
38+
const [exampleChoice, setExampleChoice] = useState<number>(0); // 0〜1
39+
1340
const handleSubmit = async (e: FormEvent<HTMLFormElement>) => {
1441
e.preventDefault();
1542
setIsLoading(true);
1643
setResponse("");
1744

45+
let userQuestion = inputValue;
46+
if(!userQuestion && exampleData){
47+
// 質問が空欄なら、質問例を使用
48+
userQuestion = exampleData[Math.floor(exampleChoice * exampleData.length)];
49+
setInputValue(userQuestion);
50+
}
51+
1852
const result = await askAI({
19-
userQuestion: inputValue,
53+
userQuestion,
2054
documentContent: documentContent,
2155
});
2256

@@ -35,8 +69,18 @@ export function ChatForm({ documentContent }: { documentContent: string }) {
3569
<div className="input-area">
3670
<textarea
3771
className="textarea textarea-ghost textarea-md rounded-lg"
38-
placeholder="質問を入力してください"
39-
style={{width: "100%", height: "110px", resize: "none", outlineStyle: "none"}}
72+
placeholder={
73+
"質問を入力してください" +
74+
(exampleData
75+
? ` (例:「${exampleData[Math.floor(exampleChoice * exampleData.length)]}」)`
76+
: "")
77+
}
78+
style={{
79+
width: "100%",
80+
height: "110px",
81+
resize: "none",
82+
outlineStyle: "none",
83+
}}
4084
value={inputValue}
4185
onChange={(e) => setInputValue(e.target.value)}
4286
disabled={isLoading}
@@ -68,7 +112,10 @@ export function ChatForm({ documentContent }: { documentContent: string }) {
68112
{!isFormVisible && (
69113
<button
70114
className="btn btn-soft btn-secondary rounded-full"
71-
onClick={() => setIsFormVisible(true)}
115+
onClick={() => {
116+
setIsFormVisible(true);
117+
setExampleChoice(Math.random());
118+
}}
72119
>
73120
チャットを開く
74121
</button>

app/[docs_id]/page.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ export default async function Page({
4242
<div className="p-4">
4343
{splitMdContent.map((section, index) => (
4444
<div key={index} id={`${index}`}>
45-
<Section key={index} section={section} />
45+
<Section key={index} docs_id={docs_id} section={section} />
4646
</div>
4747
))}
4848
</div>

app/[docs_id]/section.tsx

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,13 @@ const SectionCodeContext = createContext<ISectionCodeContext | null>(null);
2525
export const useSectionCode = () => useContext(SectionCodeContext);
2626

2727
// 1つのセクションのタイトルと内容を表示する。内容はMarkdownとしてレンダリングする
28-
export function Section({ section }: { section: MarkdownSection }) {
28+
export function Section({
29+
docs_id,
30+
section,
31+
}: {
32+
docs_id: string;
33+
section: MarkdownSection;
34+
}) {
2935
// eslint-disable-next-line @typescript-eslint/no-unused-vars
3036
const [replOutputs, setReplOutputs] = useState<ReplCommand[]>([]);
3137
// eslint-disable-next-line @typescript-eslint/no-unused-vars
@@ -71,7 +77,7 @@ export function Section({ section }: { section: MarkdownSection }) {
7177
<div>
7278
<Heading level={section.level}>{section.title}</Heading>
7379
<StyledMarkdown content={section.content} />
74-
<ChatForm documentContent={section.content} />
80+
<ChatForm docs_id={docs_id} documentContent={section.content} />
7581
</div>
7682
</SectionCodeContext.Provider>
7783
);

app/actions/questionExample.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
"use server";
2+
3+
import { GoogleGenerativeAI } from "@google/generative-ai";
4+
import { z } from "zod";
5+
6+
const QuestionExampleSchema = z.object({
7+
lang: z.string().min(1),
8+
documentContent: z.string().min(1),
9+
});
10+
11+
const genAI = new GoogleGenerativeAI(process.env.API_KEY!);
12+
13+
type QuestionExampleParams = z.input<typeof QuestionExampleSchema>;
14+
15+
export async function getQuestionExample(
16+
params: QuestionExampleParams
17+
): Promise<string[]> {
18+
// 質問の例を複数AIに考えさせる。
19+
// stringで複数返して、ChatForm側でその中から1つランダムに選ぶ
20+
// 呼び出し側がSWRで、エラー処理してくれるので、全部throwでいい
21+
// TODO: 同じドキュメントに対して2回以上生成する意味がないので、キャッシュしたいですね
22+
23+
const parseResult = QuestionExampleSchema.safeParse(params);
24+
25+
if (!parseResult.success) {
26+
throw new Error(parseResult.error.issues.map((e) => e.message).join(", "));
27+
}
28+
29+
const { lang, documentContent } = parseResult.data;
30+
31+
const model = genAI.getGenerativeModel({ model: "gemini-1.5-flash" });
32+
const prompt = `
33+
以下の${lang}チュートリアルのドキュメントに対して、想定される初心者のユーザーからの質問の例を箇条書きで複数挙げてください。
34+
強調などはせずテキストだけで1行ごとに1つ出力してください。
35+
36+
# ドキュメント
37+
${documentContent}
38+
`;
39+
const result = await model.generateContent(prompt);
40+
const response = result.response;
41+
const text = response.text();
42+
return text.trim().split("\n");
43+
}

0 commit comments

Comments
 (0)