From 30fd38d90458a7732e19160bac37f1e054ac3f10 Mon Sep 17 00:00:00 2001 From: Hirokazu Tanaka Date: Sat, 23 Aug 2025 00:14:16 +0900 Subject: [PATCH 01/16] =?UTF-8?q?=E3=83=81=E3=83=A3=E3=83=83=E3=83=88?= =?UTF-8?q?=E5=B1=A5=E6=AD=B4=E3=81=AE=E4=BF=9D=E5=AD=98=E3=81=A8=E8=A1=A8?= =?UTF-8?q?=E7=A4=BA=E6=A9=9F=E8=83=BD=E3=82=92=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/[docs_id]/chatForm.tsx | 61 ++++++++++++++++++++++++++++---------- 1 file changed, 46 insertions(+), 15 deletions(-) diff --git a/app/[docs_id]/chatForm.tsx b/app/[docs_id]/chatForm.tsx index 53c6e23..c0085c0 100644 --- a/app/[docs_id]/chatForm.tsx +++ b/app/[docs_id]/chatForm.tsx @@ -1,19 +1,45 @@ "use client"; -import { useState, FormEvent } from "react"; +import { useState, FormEvent, useEffect } from "react"; import { askAI } from "@/app/actions/chatActions"; import { StyledMarkdown } from "./markdown"; +interface Message { + sender: "user" | "ai"; + text: string; +} + +const CHAT_HISTORY_KEY = "my-code-chat-history"; + export function ChatForm({ documentContent }: { documentContent: string }) { const [inputValue, setInputValue] = useState(""); - const [response, setResponse] = useState(""); + const [messages, setMessages] = useState([]); const [isLoading, setIsLoading] = useState(false); const [isFormVisible, setIsFormVisible] = useState(false); + useEffect(() => { + try { + const savedHistory = localStorage.getItem(CHAT_HISTORY_KEY); + if (savedHistory) { + setMessages(JSON.parse(savedHistory)); + } + } catch (error) { + console.error("Failed to load chat history from localStorage", error); + } + }, []); + + useEffect(() => { + if (messages.length > 0) { + localStorage.setItem(CHAT_HISTORY_KEY, JSON.stringify(messages)); + } + }, [messages]); + const handleSubmit = async (e: FormEvent) => { e.preventDefault(); setIsLoading(true); - setResponse(""); + + const userMessage: Message = { sender: "user", text: inputValue }; + setMessages((prevMessages) => [...prevMessages, userMessage]); const result = await askAI({ userQuestion: inputValue, @@ -21,11 +47,14 @@ export function ChatForm({ documentContent }: { documentContent: string }) { }); if (result.error) { - setResponse(`エラー: ${result.error}`); + const errorMessage: Message = { sender: "ai", text: `エラー: ${result.error}` }; + setMessages((prevMessages) => [...prevMessages, errorMessage]); } else { - setResponse(result.response); + const aiMessage: Message = { sender: "ai", text: result.response }; + setMessages((prevMessages) => [...prevMessages, aiMessage]); } + setInputValue(""); setIsLoading(false); }; return ( @@ -51,8 +80,8 @@ export function ChatForm({ documentContent }: { documentContent: string }) { @@ -62,7 +91,7 @@ export function ChatForm({ documentContent }: { documentContent: string }) { className="btn btn-soft btn-circle btn-accent border-2 border-accent rounded-full" title="送信" style={{marginTop:"10px"}} - disabled={isLoading} + disabled={isLoading || !inputValue.trim()} > @@ -79,14 +108,16 @@ export function ChatForm({ documentContent }: { documentContent: string }) { )} - {response && ( -
-

AIの回答

-
-
-
+ {messages.length > 0 && ( +
+

チャット履歴

+ {messages.map((msg, index) => ( +
+
+ +
-
+ ))}
)} @@ -98,4 +129,4 @@ export function ChatForm({ documentContent }: { documentContent: string }) { ); -} +} \ No newline at end of file From 8d59f0ed82f3db6926b9b27794d9d2d33e571d4e Mon Sep 17 00:00:00 2001 From: Hirokazu Tanaka Date: Sat, 23 Aug 2025 01:32:45 +0900 Subject: [PATCH 02/16] =?UTF-8?q?=E5=80=8B=E5=88=A5=E3=81=AE=E3=83=81?= =?UTF-8?q?=E3=83=A3=E3=83=83=E3=83=88=E5=B1=A5=E6=AD=B4=E3=82=92=E4=BF=9D?= =?UTF-8?q?=E5=AD=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/[docs_id]/chatForm.tsx | 25 ++++++++++++++++--------- app/[docs_id]/page.tsx | 2 +- app/[docs_id]/section.tsx | 8 +++++++- 3 files changed, 24 insertions(+), 11 deletions(-) diff --git a/app/[docs_id]/chatForm.tsx b/app/[docs_id]/chatForm.tsx index c0085c0..db8428f 100644 --- a/app/[docs_id]/chatForm.tsx +++ b/app/[docs_id]/chatForm.tsx @@ -9,9 +9,14 @@ interface Message { text: string; } -const CHAT_HISTORY_KEY = "my-code-chat-history"; +interface ChatFormProps { + documentContent: string; + sectionId: string; +} + +export function ChatForm({ documentContent, sectionId }: ChatFormProps) { + const CHAT_HISTORY_KEY = `my-code-chat-history-${sectionId}`; -export function ChatForm({ documentContent }: { documentContent: string }) { const [inputValue, setInputValue] = useState(""); const [messages, setMessages] = useState([]); const [isLoading, setIsLoading] = useState(false); @@ -26,20 +31,22 @@ export function ChatForm({ documentContent }: { documentContent: string }) { } catch (error) { console.error("Failed to load chat history from localStorage", error); } - }, []); + }, [CHAT_HISTORY_KEY]); useEffect(() => { - if (messages.length > 0) { + if (messages.length === 0) { + localStorage.removeItem(CHAT_HISTORY_KEY); + } else { localStorage.setItem(CHAT_HISTORY_KEY, JSON.stringify(messages)); } - }, [messages]); + }, [messages, CHAT_HISTORY_KEY]); const handleSubmit = async (e: FormEvent) => { e.preventDefault(); setIsLoading(true); const userMessage: Message = { sender: "user", text: inputValue }; - setMessages((prevMessages) => [...prevMessages, userMessage]); + setMessages([userMessage]); const result = await askAI({ userQuestion: inputValue, @@ -48,10 +55,10 @@ export function ChatForm({ documentContent }: { documentContent: string }) { if (result.error) { const errorMessage: Message = { sender: "ai", text: `エラー: ${result.error}` }; - setMessages((prevMessages) => [...prevMessages, errorMessage]); + setMessages([userMessage, errorMessage]); } else { const aiMessage: Message = { sender: "ai", text: result.response }; - setMessages((prevMessages) => [...prevMessages, aiMessage]); + setMessages([userMessage, aiMessage]); } setInputValue(""); @@ -110,7 +117,7 @@ export function ChatForm({ documentContent }: { documentContent: string }) { {messages.length > 0 && (
-

チャット履歴

+

AIとのチャット

{messages.map((msg, index) => (
diff --git a/app/[docs_id]/page.tsx b/app/[docs_id]/page.tsx index cd09acd..1aac6b3 100644 --- a/app/[docs_id]/page.tsx +++ b/app/[docs_id]/page.tsx @@ -41,7 +41,7 @@ export default async function Page({ return (
{splitMdContent.map((section, index) => ( -
+
))}
); diff --git a/app/[docs_id]/section.tsx b/app/[docs_id]/section.tsx index 138e93a..5f2d90b 100644 --- a/app/[docs_id]/section.tsx +++ b/app/[docs_id]/section.tsx @@ -22,9 +22,14 @@ interface ISectionCodeContext { } const SectionCodeContext = createContext(null); export const useSectionCode = () => useContext(SectionCodeContext); +interface SectionProps { + section: MarkdownSection; + docs_id: string; + sectionIndex: number; +} // 1つのセクションのタイトルと内容を表示する。内容はMarkdownとしてレンダリングする -export function Section({ section }: { section: MarkdownSection }) { +export function Section({ section, docs_id, sectionIndex }: SectionProps) { // eslint-disable-next-line @typescript-eslint/no-unused-vars const [replOutputs, setReplOutputs] = useState([]); // eslint-disable-next-line @typescript-eslint/no-unused-vars @@ -62,6 +67,7 @@ export function Section({ section }: { section: MarkdownSection }) { // fileContents: section内にあるファイルエディターの内容 // execResults: section内にあるファイルの実行結果 // console.log(section.title, replOutputs, fileContents, execResults); + const sectionId = `${docs_id}-${sectionIndex}`; return ( Date: Sat, 23 Aug 2025 01:55:13 +0900 Subject: [PATCH 03/16] =?UTF-8?q?=E3=83=81=E3=83=A3=E3=83=83=E3=83=88?= =?UTF-8?q?=E5=B1=A5=E6=AD=B4=E3=81=AE=E8=AA=AD=E3=81=BF=E8=BE=BC=E3=81=BF?= =?UTF-8?q?=E6=99=82=E3=81=AB=E7=A9=BA=E3=81=AE=E3=83=A1=E3=83=83=E3=82=BB?= =?UTF-8?q?=E3=83=BC=E3=82=B8=E3=82=92=E8=A8=AD=E5=AE=9A=E3=81=99=E3=82=8B?= =?UTF-8?q?=E5=87=A6=E7=90=86=E3=82=92=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/[docs_id]/chatForm.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/[docs_id]/chatForm.tsx b/app/[docs_id]/chatForm.tsx index db8428f..24dd319 100644 --- a/app/[docs_id]/chatForm.tsx +++ b/app/[docs_id]/chatForm.tsx @@ -27,9 +27,12 @@ export function ChatForm({ documentContent, sectionId }: ChatFormProps) { const savedHistory = localStorage.getItem(CHAT_HISTORY_KEY); if (savedHistory) { setMessages(JSON.parse(savedHistory)); + } else { + setMessages([]); } } catch (error) { console.error("Failed to load chat history from localStorage", error); + setMessages([]); } }, [CHAT_HISTORY_KEY]); From 7c5be29d8175c6cb502e56b63b4566beefcb8d58 Mon Sep 17 00:00:00 2001 From: Hirokazu Tanaka Date: Wed, 27 Aug 2025 23:11:17 +0900 Subject: [PATCH 04/16] =?UTF-8?q?=E3=83=81=E3=83=A3=E3=83=83=E3=83=88?= =?UTF-8?q?=E5=B1=A5=E6=AD=B4=E7=AE=A1=E7=90=86=E3=81=AE=E3=81=9F=E3=82=81?= =?UTF-8?q?=E3=81=AE=E3=82=B3=E3=83=B3=E3=83=86=E3=82=AD=E3=82=B9=E3=83=88?= =?UTF-8?q?=E3=82=92=E8=BF=BD=E5=8A=A0=E3=81=97=E3=80=81=E3=83=81=E3=83=A3?= =?UTF-8?q?=E3=83=83=E3=83=88=E3=83=95=E3=82=A9=E3=83=BC=E3=83=A0=E3=81=A7?= =?UTF-8?q?=E3=81=AE=E5=B1=A5=E6=AD=B4=E3=81=AE=E4=BF=9D=E5=AD=98=E3=81=A8?= =?UTF-8?q?=E8=A1=A8=E7=A4=BA=E3=82=92=E5=AE=9F=E8=A3=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/[docs_id]/chatForm.tsx | 36 +++++----------- app/[docs_id]/page.tsx | 2 +- app/[docs_id]/section.tsx | 2 +- app/context/ChatHistoryContext.tsx | 67 ++++++++++++++++++++++++++++++ app/layout.tsx | 9 ++-- 5 files changed, 85 insertions(+), 31 deletions(-) create mode 100644 app/context/ChatHistoryContext.tsx diff --git a/app/[docs_id]/chatForm.tsx b/app/[docs_id]/chatForm.tsx index 24dd319..20b35b0 100644 --- a/app/[docs_id]/chatForm.tsx +++ b/app/[docs_id]/chatForm.tsx @@ -3,6 +3,7 @@ import { useState, FormEvent, useEffect } from "react"; import { askAI } from "@/app/actions/chatActions"; import { StyledMarkdown } from "./markdown"; +import { useChatHistory } from "../context/ChatHistoryContext"; interface Message { sender: "user" | "ai"; @@ -15,41 +16,24 @@ interface ChatFormProps { } export function ChatForm({ documentContent, sectionId }: ChatFormProps) { - const CHAT_HISTORY_KEY = `my-code-chat-history-${sectionId}`; + const { chatHistories, setChatHistory } = useChatHistory(); + const messages = chatHistories[sectionId] || []; const [inputValue, setInputValue] = useState(""); - const [messages, setMessages] = useState([]); const [isLoading, setIsLoading] = useState(false); const [isFormVisible, setIsFormVisible] = useState(false); + const [isMounted, setIsMounted] = useState(false); useEffect(() => { - try { - const savedHistory = localStorage.getItem(CHAT_HISTORY_KEY); - if (savedHistory) { - setMessages(JSON.parse(savedHistory)); - } else { - setMessages([]); - } - } catch (error) { - console.error("Failed to load chat history from localStorage", error); - setMessages([]); - } - }, [CHAT_HISTORY_KEY]); - - useEffect(() => { - if (messages.length === 0) { - localStorage.removeItem(CHAT_HISTORY_KEY); - } else { - localStorage.setItem(CHAT_HISTORY_KEY, JSON.stringify(messages)); - } - }, [messages, CHAT_HISTORY_KEY]); + setIsMounted(true); + }, []); const handleSubmit = async (e: FormEvent) => { e.preventDefault(); setIsLoading(true); const userMessage: Message = { sender: "user", text: inputValue }; - setMessages([userMessage]); + setChatHistory(sectionId, [userMessage]); const result = await askAI({ userQuestion: inputValue, @@ -58,10 +42,10 @@ export function ChatForm({ documentContent, sectionId }: ChatFormProps) { if (result.error) { const errorMessage: Message = { sender: "ai", text: `エラー: ${result.error}` }; - setMessages([userMessage, errorMessage]); + setChatHistory(sectionId, [userMessage, errorMessage]); } else { const aiMessage: Message = { sender: "ai", text: result.response }; - setMessages([userMessage, aiMessage]); + setChatHistory(sectionId, [userMessage, aiMessage]); } setInputValue(""); @@ -118,7 +102,7 @@ export function ChatForm({ documentContent, sectionId }: ChatFormProps) { )} - {messages.length > 0 && ( + {isMounted && messages.length > 0 && (

AIとのチャット

{messages.map((msg, index) => ( diff --git a/app/[docs_id]/page.tsx b/app/[docs_id]/page.tsx index 1aac6b3..4f66a70 100644 --- a/app/[docs_id]/page.tsx +++ b/app/[docs_id]/page.tsx @@ -41,7 +41,7 @@ export default async function Page({ return (
{splitMdContent.map((section, index) => ( -
+
))}
); diff --git a/app/[docs_id]/section.tsx b/app/[docs_id]/section.tsx index 5f2d90b..487f554 100644 --- a/app/[docs_id]/section.tsx +++ b/app/[docs_id]/section.tsx @@ -76,7 +76,7 @@ export function Section({ section, docs_id, sectionIndex }: SectionProps) {
{section.title} - +
); diff --git a/app/context/ChatHistoryContext.tsx b/app/context/ChatHistoryContext.tsx new file mode 100644 index 0000000..fe62648 --- /dev/null +++ b/app/context/ChatHistoryContext.tsx @@ -0,0 +1,67 @@ +"use client"; + +import { createContext, ReactNode, useCallback, useContext, useState, useEffect } from "react"; + +interface Message { + sender: "user" | "ai"; + text: string; +} + +interface IChatHistoryContext { + chatHistories: Record; + setChatHistory: (sectionId: string, messages: Message[]) => void; +} + +const ChatHistoryContext = createContext(null); + +export const useChatHistory = () => { + const context = useContext(ChatHistoryContext); + if (!context) { + throw new Error("useChatHistory must be used within a ChatHistoryProvider"); + } + return context; +}; + +const CHAT_HISTORY_STORAGE_KEY = "my-code-all-chat-histories"; + +export function ChatHistoryProvider({ children }: { children: ReactNode }) { + const [chatHistories, setChatHistories] = useState>({}); + + useEffect(() => { + try { + const savedHistories = localStorage.getItem(CHAT_HISTORY_STORAGE_KEY); + if (savedHistories) { + setChatHistories(JSON.parse(savedHistories)); + } + } catch (error) { + console.error("Failed to load chat histories from localStorage", error); + } + }, []); + + useEffect(() => { + if (Object.keys(chatHistories).length > 0) { + localStorage.setItem(CHAT_HISTORY_STORAGE_KEY, JSON.stringify(chatHistories)); + } else { + const saved = localStorage.getItem(CHAT_HISTORY_STORAGE_KEY); + if(saved) localStorage.removeItem(CHAT_HISTORY_STORAGE_KEY); + } + }, [chatHistories]); + + const setChatHistory = useCallback((sectionId: string, messages: Message[]) => { + setChatHistories((prevHistories) => { + const newHistories = { ...prevHistories }; + if (messages.length === 0) { + delete newHistories[sectionId]; + } else { + newHistories[sectionId] = messages; + } + return newHistories; + }); + }, []); + + return ( + + {children} + + ); +} \ No newline at end of file diff --git a/app/layout.tsx b/app/layout.tsx index 1165301..02cee66 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -5,6 +5,7 @@ import { Sidebar } from "./sidebar"; import { ReactNode } from "react"; import { PyodideProvider } from "./terminal/python/pyodide"; import { FileProvider } from "./terminal/file"; +import { ChatHistoryProvider } from "./context/ChatHistoryContext"; export const metadata: Metadata = { title: "Create Next App", @@ -21,9 +22,11 @@ export default function RootLayout({
- - {children} - + + + {children} + +