Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 53 additions & 5 deletions app/[docs_id]/chatForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,55 @@
import { useState, FormEvent } from "react";
import { askAI } from "@/app/actions/chatActions";
import { StyledMarkdown } from "./markdown";
import useSWR from "swr";
import { getQuestionExample } from "../actions/questionExample";
import { getLanguageName } from "../pagesList";

export function ChatForm({ documentContent }: { documentContent: string }) {
export function ChatForm({
docs_id,
documentContent,
}: {
docs_id: string;
documentContent: string;
}) {
const [inputValue, setInputValue] = useState("");
const [response, setResponse] = useState("");
const [isLoading, setIsLoading] = useState(false);
const [isFormVisible, setIsFormVisible] = useState(false);

const lang = getLanguageName(docs_id);
const { data: exampleData, error: exampleError } = useSWR(
// 質問フォームを開いたときだけで良い
isFormVisible ? { lang, documentContent } : null,
getQuestionExample,
{
// リクエストは古くても構わないので1回でいい
revalidateIfStale: false,
revalidateOnFocus: false,
revalidateOnReconnect: false,
}
);
if (exampleError) {
console.error("Error getting question example:", exampleError);
}
// 質問フォームを開くたびにランダムに選び直し、
// exampleData[Math.floor(exampleChoice * exampleData.length)] を採用する
const [exampleChoice, setExampleChoice] = useState<number>(0); // 0〜1

const handleSubmit = async (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
setIsLoading(true);
setResponse("");

let userQuestion = inputValue;
if(!userQuestion && exampleData){
// 質問が空欄なら、質問例を使用
userQuestion = exampleData[Math.floor(exampleChoice * exampleData.length)];
setInputValue(userQuestion);
}

const result = await askAI({
userQuestion: inputValue,
userQuestion,
documentContent: documentContent,
});

Expand All @@ -35,8 +70,18 @@ export function ChatForm({ documentContent }: { documentContent: string }) {
<div className="input-area">
<textarea
className="textarea textarea-ghost textarea-md rounded-lg"
placeholder="質問を入力してください"
style={{width: "100%", height: "110px", resize: "none", outlineStyle: "none"}}
placeholder={
"質問を入力してください" +
(exampleData
? ` (例:「${exampleData[Math.floor(exampleChoice * exampleData.length)]}」)`
: "")
}
style={{
width: "100%",
height: "110px",
resize: "none",
outlineStyle: "none",
}}
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
disabled={isLoading}
Expand Down Expand Up @@ -68,7 +113,10 @@ export function ChatForm({ documentContent }: { documentContent: string }) {
{!isFormVisible && (
<button
className="btn btn-soft btn-secondary rounded-full"
onClick={() => setIsFormVisible(true)}
onClick={() => {
setIsFormVisible(true);
setExampleChoice(Math.random());
}}
>
チャットを開く
</button>
Expand Down
2 changes: 1 addition & 1 deletion app/[docs_id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ export default async function Page({
<div className="p-4">
{splitMdContent.map((section, index) => (
<div key={index} id={`${index}`}>
<Section key={index} section={section} />
<Section key={index} docs_id={docs_id} section={section} />
</div>
))}
</div>
Expand Down
10 changes: 8 additions & 2 deletions app/[docs_id]/section.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,13 @@ const SectionCodeContext = createContext<ISectionCodeContext | null>(null);
export const useSectionCode = () => useContext(SectionCodeContext);

// 1つのセクションのタイトルと内容を表示する。内容はMarkdownとしてレンダリングする
export function Section({ section }: { section: MarkdownSection }) {
export function Section({
docs_id,
section,
}: {
docs_id: string;
section: MarkdownSection;
}) {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [replOutputs, setReplOutputs] = useState<ReplCommand[]>([]);
// eslint-disable-next-line @typescript-eslint/no-unused-vars
Expand Down Expand Up @@ -71,7 +77,7 @@ export function Section({ section }: { section: MarkdownSection }) {
<div>
<Heading level={section.level}>{section.title}</Heading>
<StyledMarkdown content={section.content} />
<ChatForm documentContent={section.content} />
<ChatForm docs_id={docs_id} documentContent={section.content} />
</div>
</SectionCodeContext.Provider>
);
Expand Down
43 changes: 43 additions & 0 deletions app/actions/questionExample.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
"use server";

import { GoogleGenerativeAI } from "@google/generative-ai";
import { z } from "zod";

const QuestionExampleSchema = z.object({
lang: z.string().min(1),
documentContent: z.string().min(1),
});

const genAI = new GoogleGenerativeAI(process.env.API_KEY!);

type QuestionExampleParams = z.input<typeof QuestionExampleSchema>;

export async function getQuestionExample(
params: QuestionExampleParams
): Promise<string[]> {
// 質問の例を複数AIに考えさせる。
// stringで複数返して、ChatForm側でその中から1つランダムに選ぶ
// 呼び出し側がSWRで、エラー処理してくれるので、全部throwでいい
// TODO: 同じドキュメントに対して2回以上生成する意味がないので、キャッシュしたいですね

const parseResult = QuestionExampleSchema.safeParse(params);

if (!parseResult.success) {
throw new Error(parseResult.error.issues.map((e) => e.message).join(", "));
}

const { lang, documentContent } = parseResult.data;

const model = genAI.getGenerativeModel({ model: "gemini-1.5-flash" });
const prompt = `
以下の${lang}チュートリアルのドキュメントに対して、想定される初心者のユーザーからの質問の例を箇条書きで複数挙げてください。
強調などはせずテキストだけで1行ごとに1つ出力してください。

# ドキュメント
${documentContent}
`;
const result = await model.generateContent(prompt);
const response = result.response;
const text = response.text();
return text.trim().split("\n");
}
10 changes: 10 additions & 0 deletions app/pagesList.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,13 @@ export const pagesList = [
],
},
] as const;

// ${lang_id}-${page_id} から言語名を取得
export function getLanguageName(docs_id: string){
const lang_id = docs_id.split("-")[0];
const lang = pagesList.find((lang) => lang.id === lang_id)?.lang;
if(!lang){
throw new Error(`Unknown language id: ${lang_id}`);
}
return lang;
}