Skip to content

Commit edce53b

Browse files
authored
Merge pull request #18 from ut-code/storage
チャット履歴をローカル保存&表示
2 parents 878ed18 + 746e8ba commit edce53b

File tree

4 files changed

+105
-33
lines changed

4 files changed

+105
-33
lines changed

app/[docs_id]/chatForm.tsx

Lines changed: 50 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,26 @@
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";
68
import useSWR from "swr";
79
import { getQuestionExample } from "../actions/questionExample";
810
import { getLanguageName } from "../pagesList";
911

10-
export function ChatForm({
11-
docs_id,
12-
documentContent,
13-
}: {
14-
docs_id: string;
12+
interface ChatFormProps {
1513
documentContent: string;
16-
}) {
14+
sectionId: string;
15+
}
16+
17+
export function ChatForm({ documentContent, sectionId }: ChatFormProps) {
18+
const [messages, updateChatHistory] = useChatHistory(sectionId);
1719
const [inputValue, setInputValue] = useState("");
18-
const [response, setResponse] = useState("");
1920
const [isLoading, setIsLoading] = useState(false);
2021
const [isFormVisible, setIsFormVisible] = useState(false);
2122

22-
const lang = getLanguageName(docs_id);
23+
const lang = getLanguageName(sectionId);
2324
const { data: exampleData, error: exampleError } = useSWR(
2425
// 質問フォームを開いたときだけで良い
2526
isFormVisible ? { lang, documentContent } : null,
@@ -41,7 +42,9 @@ export function ChatForm({
4142
const handleSubmit = async (e: FormEvent<HTMLFormElement>) => {
4243
e.preventDefault();
4344
setIsLoading(true);
44-
setResponse("");
45+
46+
const userMessage: Message = { sender: "user", text: inputValue };
47+
updateChatHistory([userMessage]);
4548

4649
let userQuestion = inputValue;
4750
if(!userQuestion && exampleData){
@@ -56,13 +59,21 @@ export function ChatForm({
5659
});
5760

5861
if (result.error) {
59-
setResponse(`エラー: ${result.error}`);
62+
const errorMessage: Message = { sender: "ai", text: `エラー: ${result.error}`, isError: true };
63+
updateChatHistory([userMessage, errorMessage]);
6064
} else {
61-
setResponse(result.response);
65+
const aiMessage: Message = { sender: "ai", text: result.response };
66+
updateChatHistory([userMessage, aiMessage]);
67+
setInputValue("");
6268
}
6369

6470
setIsLoading(false);
6571
};
72+
73+
const handleClearHistory = () => {
74+
updateChatHistory([]);
75+
};
76+
6677
return (
6778
<>
6879
{isFormVisible && (
@@ -92,8 +103,8 @@ export function ChatForm({
92103
<button
93104
className="btn btn-soft btn-secondary rounded-full"
94105
onClick={() => setIsFormVisible(false)}
106+
type="button"
95107
>
96-
97108
閉じる
98109
</button>
99110
</div>
@@ -122,14 +133,33 @@ export function ChatForm({
122133
</button>
123134
)}
124135

125-
{response && (
126-
<article>
127-
<h3 className="text-lg font-semibold mb-2">AIの回答</h3>
128-
<div className="chat chat-start">
129-
<div className="chat-bubble bg-secondary-content text-black" style={{maxWidth: "100%", wordBreak: "break-word"}}>
130-
<div className="response-container"><StyledMarkdown content={response}/></div>
131-
</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>
132147
</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+
))}
133163
</article>
134164
)}
135165

@@ -141,4 +171,4 @@ export function ChatForm({
141171

142172
</>
143173
);
144-
}
174+
}

app/[docs_id]/page.tsx

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -45,11 +45,14 @@ export default async function Page({
4545

4646
return (
4747
<div className="p-4">
48-
{splitMdContent.map((section, index) => (
49-
<div key={index} id={`${index}`}>
50-
<Section key={index} docs_id={docs_id} section={section} />
51-
</div>
52-
))}
48+
{splitMdContent.map((section, index) => {
49+
const sectionId = `${docs_id}-${index}`;
50+
return (
51+
<div key={index} id={`${index}`}>
52+
<Section section={section} sectionId={sectionId} />
53+
</div>
54+
);
55+
})}
5356
</div>
5457
);
5558
}

app/[docs_id]/section.tsx

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

27-
// 1つのセクションのタイトルと内容を表示する。内容はMarkdownとしてレンダリングする
28-
export function Section({
29-
docs_id,
30-
section,
31-
}: {
32-
docs_id: string;
27+
interface SectionProps {
3328
section: MarkdownSection;
34-
}) {
29+
sectionId: string;
30+
}
31+
32+
// 1つのセクションのタイトルと内容を表示する。内容はMarkdownとしてレンダリングする
33+
export function Section({ section, sectionId }: SectionProps) {
3534
// eslint-disable-next-line @typescript-eslint/no-unused-vars
3635
const [replOutputs, setReplOutputs] = useState<ReplCommand[]>([]);
3736
// eslint-disable-next-line @typescript-eslint/no-unused-vars
@@ -77,7 +76,7 @@ export function Section({
7776
<div>
7877
<Heading level={section.level}>{section.title}</Heading>
7978
<StyledMarkdown content={section.content} />
80-
<ChatForm docs_id={docs_id} documentContent={section.content} />
79+
<ChatForm documentContent={section.content} sectionId={sectionId} />
8180
</div>
8281
</SectionCodeContext.Provider>
8382
);

app/hooks/useChathistory.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
"use client";
2+
3+
import { useState, useEffect, useCallback } from 'react';
4+
5+
export interface Message {
6+
sender: "user" | "ai";
7+
text: string;
8+
isError?: boolean;
9+
}
10+
11+
export const useChatHistory = (sectionId: string) => {
12+
const [messages, setMessages] = useState<Message[]>([]);
13+
14+
const CHAT_HISTORY_KEY = `my-code-chat-history-${sectionId}`;
15+
16+
useEffect(() => {
17+
try {
18+
const savedHistory = localStorage.getItem(CHAT_HISTORY_KEY);
19+
if (savedHistory) {
20+
setMessages(JSON.parse(savedHistory));
21+
} else {
22+
setMessages([]);
23+
}
24+
} catch (error) {
25+
console.error("Failed to load chat history from localStorage", error);
26+
setMessages([]);
27+
}
28+
}, [CHAT_HISTORY_KEY]);
29+
30+
const updateChatHistory = useCallback((newMessages: Message[]) => {
31+
setMessages(newMessages);
32+
if (newMessages.length > 0) {
33+
localStorage.setItem(CHAT_HISTORY_KEY, JSON.stringify(newMessages));
34+
} else {
35+
localStorage.removeItem(CHAT_HISTORY_KEY);
36+
}
37+
}, [CHAT_HISTORY_KEY]);
38+
39+
return [messages, updateChatHistory] as const;
40+
};

0 commit comments

Comments
 (0)