Skip to content

Commit f72a1f9

Browse files
authored
Merge branch 'main' into changetheme
2 parents e1c7034 + 5ad930f commit f72a1f9

31 files changed

+6396
-2991
lines changed

README.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,10 +83,13 @@ npm run lint
8383

8484
- [Next.js](https://nextjs.org/docs)
8585
- 検索する際は「App Router」を含めることで古い記事に惑わされることが少なくなります。
86+
- [OpenNext](https://opennext.js.org/cloudflare)
8687
- [DaisyUI](https://daisyui.com/docs/use/) / [Tailwind CSS](https://tailwindcss.com/docs)
8788
- buttonやinputやメニューなどの基本的なコンポーネントのデザインはDaisyUIにあるものを使うと楽です
8889
- 細かくスタイルを調整したい場合はTailwind CSSを使います (CSS直接指定(`style={{...}}`)よりもちょっと楽に書ける)
8990
- よくわからなかったらstyle直接指定でも良い
91+
- [SWR](https://swr.vercel.app/ja)
9092
- [react-markdown](https://www.npmjs.com/package/react-markdown)
91-
- オプションがいろいろあり、今はほぼデフォルト設定で突っ込んでいるがあとでなんとかする
92-
- [OpenNext](https://opennext.js.org/cloudflare)
93+
- REPL・実行結果表示: [xterm.js](https://xtermjs.org/)
94+
- コードエディター: [react-ace](https://github.com/securingsincity/react-ace)
95+
- それ以外のコードブロック: [react-syntax-highlighter](https://github.com/react-syntax-highlighter/react-syntax-highlighter)

app/[docs_id]/chatForm.tsx

Lines changed: 96 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,98 @@
11
"use client";
22

33
import { useState, FormEvent } from "react";
4+
import clsx from "clsx";
45
import { askAI } from "@/app/actions/chatActions";
56
import { StyledMarkdown } from "./markdown";
7+
import { useChatHistory, type Message } from "../hooks/useChathistory";
8+
import useSWR from "swr";
9+
import { getQuestionExample } from "../actions/questionExample";
10+
import { getLanguageName } from "../pagesList";
611

7-
export function ChatForm({ documentContent }: { documentContent: string }) {
12+
interface ChatFormProps {
13+
documentContent: string;
14+
sectionId: string;
15+
}
16+
17+
export function ChatForm({ documentContent, sectionId }: ChatFormProps) {
18+
const [messages, updateChatHistory] = useChatHistory(sectionId);
819
const [inputValue, setInputValue] = useState("");
9-
const [response, setResponse] = useState("");
1020
const [isLoading, setIsLoading] = useState(false);
1121
const [isFormVisible, setIsFormVisible] = useState(false);
1222

23+
const lang = getLanguageName(sectionId);
24+
const { data: exampleData, error: exampleError } = useSWR(
25+
// 質問フォームを開いたときだけで良い
26+
isFormVisible ? { lang, documentContent } : null,
27+
getQuestionExample,
28+
{
29+
// リクエストは古くても構わないので1回でいい
30+
revalidateIfStale: false,
31+
revalidateOnFocus: false,
32+
revalidateOnReconnect: false,
33+
}
34+
);
35+
if (exampleError) {
36+
console.error("Error getting question example:", exampleError);
37+
}
38+
// 質問フォームを開くたびにランダムに選び直し、
39+
// exampleData[Math.floor(exampleChoice * exampleData.length)] を採用する
40+
const [exampleChoice, setExampleChoice] = useState<number>(0); // 0〜1
41+
1342
const handleSubmit = async (e: FormEvent<HTMLFormElement>) => {
1443
e.preventDefault();
1544
setIsLoading(true);
16-
setResponse("");
45+
46+
const userMessage: Message = { sender: "user", text: inputValue };
47+
updateChatHistory([userMessage]);
48+
49+
let userQuestion = inputValue;
50+
if(!userQuestion && exampleData){
51+
// 質問が空欄なら、質問例を使用
52+
userQuestion = exampleData[Math.floor(exampleChoice * exampleData.length)];
53+
setInputValue(userQuestion);
54+
}
1755

1856
const result = await askAI({
19-
userQuestion: inputValue,
57+
userQuestion,
2058
documentContent: documentContent,
2159
});
2260

2361
if (result.error) {
24-
setResponse(`エラー: ${result.error}`);
62+
const errorMessage: Message = { sender: "ai", text: `エラー: ${result.error}`, isError: true };
63+
updateChatHistory([userMessage, errorMessage]);
2564
} else {
26-
setResponse(result.response);
65+
const aiMessage: Message = { sender: "ai", text: result.response };
66+
updateChatHistory([userMessage, aiMessage]);
67+
setInputValue("");
2768
}
2869

2970
setIsLoading(false);
3071
};
72+
73+
const handleClearHistory = () => {
74+
updateChatHistory([]);
75+
};
76+
3177
return (
3278
<>
3379
{isFormVisible && (
3480
<form className="border border-2 border-secondary shadow-md rounded-lg bg-base-100" style={{width:"100%", textAlign:"center", boxShadow:"-moz-initial"}} onSubmit={handleSubmit}>
3581
<div className="input-area">
3682
<textarea
3783
className="textarea textarea-ghost textarea-md rounded-lg"
38-
placeholder="質問を入力してください"
39-
style={{width: "100%", height: "110px", resize: "none", outlineStyle: "none"}}
84+
placeholder={
85+
"質問を入力してください" +
86+
(exampleData
87+
? ` (例:「${exampleData[Math.floor(exampleChoice * exampleData.length)]}」)`
88+
: "")
89+
}
90+
style={{
91+
width: "100%",
92+
height: "110px",
93+
resize: "none",
94+
outlineStyle: "none",
95+
}}
4096
value={inputValue}
4197
onChange={(e) => setInputValue(e.target.value)}
4298
disabled={isLoading}
@@ -47,8 +103,8 @@ export function ChatForm({ documentContent }: { documentContent: string }) {
47103
<button
48104
className="btn btn-soft btn-secondary rounded-full"
49105
onClick={() => setIsFormVisible(false)}
106+
type="button"
50107
>
51-
52108
閉じる
53109
</button>
54110
</div>
@@ -68,20 +124,42 @@ export function ChatForm({ documentContent }: { documentContent: string }) {
68124
{!isFormVisible && (
69125
<button
70126
className="btn btn-soft btn-secondary rounded-full"
71-
onClick={() => setIsFormVisible(true)}
127+
onClick={() => {
128+
setIsFormVisible(true);
129+
setExampleChoice(Math.random());
130+
}}
72131
>
73132
チャットを開く
74133
</button>
75134
)}
76135

77-
{response && (
78-
<article>
79-
<h3 className="text-lg font-semibold mb-2">AIの回答</h3>
80-
<div className="chat chat-start">
81-
<div className="chat-bubble bg-secondary-content text-black" style={{maxWidth: "100%", wordBreak: "break-word"}}>
82-
<div className="response-container"><StyledMarkdown content={response}/></div>
83-
</div>
136+
{messages.length > 0 && (
137+
<article className="mt-4">
138+
<div className="flex justify-between items-center mb-2">
139+
<h3 className="text-lg font-semibold">AIとのチャット</h3>
140+
<button
141+
onClick={handleClearHistory}
142+
className="btn btn-ghost btn-sm text-xs"
143+
aria-label="チャット履歴を削除"
144+
>
145+
履歴を削除
146+
</button>
84147
</div>
148+
{messages.map((msg, index) => (
149+
<div key={index} className={`chat ${msg.sender === 'user' ? 'chat-end' : 'chat-start'}`}>
150+
<div
151+
className={clsx(
152+
"chat-bubble",
153+
{ "bg-primary text-primary-content": msg.sender === 'user' },
154+
{ "bg-secondary-content text-black": msg.sender === 'ai' && !msg.isError },
155+
{ "chat-bubble-error": msg.isError }
156+
)}
157+
style={{maxWidth: "100%", wordBreak: "break-word"}}
158+
>
159+
<StyledMarkdown content={msg.text} />
160+
</div>
161+
</div>
162+
))}
85163
</article>
86164
)}
87165

@@ -93,4 +171,4 @@ export function ChatForm({ documentContent }: { documentContent: string }) {
93171

94172
</>
95173
);
96-
}
174+
}

0 commit comments

Comments
 (0)