Skip to content

Commit 9916d88

Browse files
authored
Merge pull request #25 from ut-code/question-example
質問の例をAIに考えさせる
2 parents 1f36931 + 12dbbad commit 9916d88

File tree

5 files changed

+115
-8
lines changed

5 files changed

+115
-8
lines changed

app/[docs_id]/chatForm.tsx

Lines changed: 53 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,55 @@
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";
8+
import { getLanguageName } from "../pagesList";
69

7-
export function ChatForm({ documentContent }: { documentContent: string }) {
10+
export function ChatForm({
11+
docs_id,
12+
documentContent,
13+
}: {
14+
docs_id: string;
15+
documentContent: string;
16+
}) {
817
const [inputValue, setInputValue] = useState("");
918
const [response, setResponse] = useState("");
1019
const [isLoading, setIsLoading] = useState(false);
1120
const [isFormVisible, setIsFormVisible] = useState(false);
1221

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

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

@@ -35,8 +70,18 @@ export function ChatForm({ documentContent }: { documentContent: string }) {
3570
<div className="input-area">
3671
<textarea
3772
className="textarea textarea-ghost textarea-md rounded-lg"
38-
placeholder="質問を入力してください"
39-
style={{width: "100%", height: "110px", resize: "none", outlineStyle: "none"}}
73+
placeholder={
74+
"質問を入力してください" +
75+
(exampleData
76+
? ` (例:「${exampleData[Math.floor(exampleChoice * exampleData.length)]}」)`
77+
: "")
78+
}
79+
style={{
80+
width: "100%",
81+
height: "110px",
82+
resize: "none",
83+
outlineStyle: "none",
84+
}}
4085
value={inputValue}
4186
onChange={(e) => setInputValue(e.target.value)}
4287
disabled={isLoading}
@@ -68,7 +113,10 @@ export function ChatForm({ documentContent }: { documentContent: string }) {
68113
{!isFormVisible && (
69114
<button
70115
className="btn btn-soft btn-secondary rounded-full"
71-
onClick={() => setIsFormVisible(true)}
116+
onClick={() => {
117+
setIsFormVisible(true);
118+
setExampleChoice(Math.random());
119+
}}
72120
>
73121
チャットを開く
74122
</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+
}

app/pagesList.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,3 +30,13 @@ export const pagesList = [
3030
],
3131
},
3232
] as const;
33+
34+
// ${lang_id}-${page_id} から言語名を取得
35+
export function getLanguageName(docs_id: string){
36+
const lang_id = docs_id.split("-")[0];
37+
const lang = pagesList.find((lang) => lang.id === lang_id)?.lang;
38+
if(!lang){
39+
throw new Error(`Unknown language id: ${lang_id}`);
40+
}
41+
return lang;
42+
}

0 commit comments

Comments
 (0)