Skip to content

Commit 9b025ea

Browse files
committed
chathistory表示機能
1 parent 1fa7be7 commit 9b025ea

File tree

8 files changed

+169
-68
lines changed

8 files changed

+169
-68
lines changed

app/[docs_id]/chatForm.tsx

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,12 @@
22

33
import { useState, FormEvent, useEffect } from "react";
44
import { askAI } from "@/app/actions/chatActions";
5-
import { type Message } from "../hooks/useChathistory";
65
import useSWR from "swr";
76
import { getQuestionExample } from "../actions/questionExample";
87
import { getLanguageName } from "../pagesList";
98
import { DynamicMarkdownSection } from "./pageContent";
109
import { useEmbedContext } from "../terminal/embedContext";
10+
import { ChatMessage, useChatHistoryContext } from "./chatHistory";
1111

1212
interface ChatFormProps {
1313
docs_id: string;
@@ -26,6 +26,8 @@ export function ChatForm({
2626
const [inputValue, setInputValue] = useState("");
2727
const [isLoading, setIsLoading] = useState(false);
2828

29+
const { addChat } = useChatHistoryContext();
30+
2931
const lang = getLanguageName(docs_id);
3032

3133
const { files, replOutputs, execResults } = useEmbedContext();
@@ -61,8 +63,7 @@ export function ChatForm({
6163
e.preventDefault();
6264
setIsLoading(true);
6365

64-
// const userMessage: Message = { sender: "user", text: inputValue };
65-
// updateChatHistory([userMessage]);
66+
const userMessage: ChatMessage = { sender: "user", text: inputValue };
6667

6768
let userQuestion = inputValue;
6869
if (!userQuestion && exampleData) {
@@ -82,17 +83,18 @@ export function ChatForm({
8283
});
8384

8485
if (result.error) {
85-
const errorMessage: Message = {
86-
sender: "ai",
86+
const errorMessage: ChatMessage = {
87+
sender: "error",
8788
text: `エラー: ${result.error}`,
88-
isError: true,
8989
};
90-
// updateChatHistory([userMessage, errorMessage]);
90+
console.log(result.error);
91+
// TODO: ユーザーに表示
9192
} else {
92-
const aiMessage: Message = { sender: "ai", text: result.response };
93-
console.log(aiMessage);
94-
// updateChatHistory([userMessage, aiMessage]);
93+
const aiMessage: ChatMessage = { sender: "ai", text: result.response };
94+
const chatId = addChat(result.targetSectionId, [userMessage, aiMessage]);
95+
// TODO: chatIdが指す対象の回答にフォーカス
9596
setInputValue("");
97+
close();
9698
}
9799

98100
setIsLoading(false);

app/[docs_id]/chatHistory.tsx

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
"use client";
2+
3+
import {
4+
createContext,
5+
ReactNode,
6+
useContext,
7+
useEffect,
8+
useState,
9+
} from "react";
10+
11+
export interface ChatMessage {
12+
sender: "user" | "ai" | "error";
13+
text: string;
14+
}
15+
16+
export interface IChatHistoryContext {
17+
chatHistories: Record<string, Record<string, ChatMessage[]>>;
18+
addChat: (sectionId: string, messages: ChatMessage[]) => string;
19+
updateChat: (sectionId: string, chatId: string, message: ChatMessage) => void;
20+
}
21+
const ChatHistoryContext = createContext<IChatHistoryContext | null>(null);
22+
export function useChatHistoryContext() {
23+
const context = useContext(ChatHistoryContext);
24+
if (!context) {
25+
throw new Error(
26+
"useChatHistoryContext must be used within a ChatHistoryProvider"
27+
);
28+
}
29+
return context;
30+
}
31+
32+
export function ChatHistoryProvider({ children }: { children: ReactNode }) {
33+
const [chatHistories, setChatHistories] = useState<
34+
Record<string, Record<string, ChatMessage[]>>
35+
>({});
36+
useEffect(() => {
37+
// Load chat histories from localStorage on mount
38+
const chatHistories: Record<string, Record<string, ChatMessage[]>> = {};
39+
for (let i = 0; i < localStorage.length; i++) {
40+
const key = localStorage.key(i);
41+
if (key && key.startsWith("chat/") && key.split("/").length === 3) {
42+
const savedHistory = localStorage.getItem(key);
43+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
44+
const [_, sectionId, chatId] = key.split("/");
45+
if (savedHistory) {
46+
if (!chatHistories[sectionId]) {
47+
chatHistories[sectionId] = {};
48+
}
49+
chatHistories[sectionId][chatId] = JSON.parse(savedHistory);
50+
}
51+
}
52+
}
53+
setChatHistories(chatHistories);
54+
}, []);
55+
56+
const addChat = (sectionId: string, messages: ChatMessage[]): string => {
57+
const chatId = Date.now().toString();
58+
const newChatHistories = { ...chatHistories };
59+
if (!newChatHistories[sectionId]) {
60+
newChatHistories[sectionId] = {};
61+
}
62+
newChatHistories[sectionId][chatId] = messages;
63+
setChatHistories(newChatHistories);
64+
localStorage.setItem(
65+
`chat-${sectionId}-${chatId}`,
66+
JSON.stringify(messages)
67+
);
68+
return chatId;
69+
};
70+
const updateChat = (
71+
sectionId: string,
72+
chatId: string,
73+
message: ChatMessage
74+
) => {
75+
const newChatHistories = { ...chatHistories };
76+
if (newChatHistories[sectionId] && newChatHistories[sectionId][chatId]) {
77+
newChatHistories[sectionId][chatId] = [
78+
...newChatHistories[sectionId][chatId],
79+
message,
80+
];
81+
setChatHistories(newChatHistories);
82+
localStorage.setItem(
83+
`chat-${sectionId}-${chatId}`,
84+
JSON.stringify(newChatHistories[sectionId][chatId])
85+
);
86+
}
87+
};
88+
89+
return (
90+
<ChatHistoryContext.Provider value={{ chatHistories, addChat, updateChat }}>
91+
{children}
92+
</ChatHistoryContext.Provider>
93+
);
94+
}

app/[docs_id]/chatServer.ts

Lines changed: 0 additions & 3 deletions
This file was deleted.

app/[docs_id]/page.tsx

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { join } from "node:path";
55
import { MarkdownSection, splitMarkdown } from "./splitMarkdown";
66
import pyodideLock from "pyodide/pyodide-lock.json";
77
import { PageContent } from "./pageContent";
8+
import { ChatHistoryProvider } from "./chatHistory";
89

910
export default async function Page({
1011
params,
@@ -44,10 +45,12 @@ export default async function Page({
4445
const splitMdContent: MarkdownSection[] = splitMarkdown(mdContent);
4546

4647
return (
47-
<PageContent
48-
documentContent={mdContent}
49-
splitMdContent={splitMdContent}
50-
docs_id={docs_id}
51-
/>
48+
<ChatHistoryProvider>
49+
<PageContent
50+
documentContent={mdContent}
51+
splitMdContent={splitMdContent}
52+
docs_id={docs_id}
53+
/>
54+
</ChatHistoryProvider>
5255
);
5356
}

app/[docs_id]/pageContent.tsx

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import { ReactNode, useEffect, useRef, useState } from "react";
44
import { MarkdownSection } from "./splitMarkdown";
55
import { ChatForm } from "./chatForm";
66
import { Heading, StyledMarkdown } from "./markdown";
7+
import { ChatHistoryProvider, useChatHistoryContext } from "./chatHistory";
8+
import clsx from "clsx";
79

810
// MarkdownSectionに追加で、ユーザーが今そのセクションを読んでいるかどうか、などの動的な情報を持たせる
911
export type DynamicMarkdownSection = MarkdownSection & {
@@ -67,6 +69,8 @@ export function PageContent(props: PageContentProps) {
6769

6870
const [isFormVisible, setIsFormVisible] = useState(false);
6971

72+
const { chatHistories } = useChatHistoryContext();
73+
7074
return (
7175
<div
7276
className="p-4 mx-auto grid"
@@ -78,7 +82,7 @@ export function PageContent(props: PageContentProps) {
7882
<>
7983
<div
8084
className="max-w-200"
81-
key={index}
85+
key={`${index}-content`}
8286
id={`${index}`} // 目次からaタグで飛ぶために必要
8387
ref={(el) => {
8488
sectionRefs.current[index] = el;
@@ -88,7 +92,40 @@ export function PageContent(props: PageContentProps) {
8892
<Heading level={section.level}>{section.title}</Heading>
8993
<StyledMarkdown content={section.content} />
9094
</div>
91-
<div>{/* 右側に表示するチャット履歴欄 */}</div>
95+
<div key={`${index}-chat`}>
96+
{/* 右側に表示するチャット履歴欄 */}
97+
{Object.entries(chatHistories[section.sectionId] ?? {}).map(
98+
([chatId, messages]) => (
99+
<div
100+
key={chatId}
101+
className="max-w-xs mb-2 p-2 text-sm border border-base-content/10 rounded-sm shadow-sm bg-base-100"
102+
>
103+
<div className="max-h-60 overflow-y-auto">
104+
{messages.map((msg, index) => (
105+
<div
106+
key={index}
107+
className={`chat ${msg.sender === "user" ? "chat-end" : "chat-start"}`}
108+
>
109+
<div
110+
className={clsx(
111+
"chat-bubble p-1!",
112+
msg.sender === "user" &&
113+
"bg-primary text-primary-content",
114+
msg.sender === "ai" &&
115+
"bg-secondary-content dark:bg-neutral text-black dark:text-white",
116+
msg.sender === "error" && "chat-bubble-error"
117+
)}
118+
style={{ maxWidth: "100%", wordBreak: "break-word" }}
119+
>
120+
<StyledMarkdown content={msg.text} />
121+
</div>
122+
</div>
123+
))}
124+
</div>
125+
</div>
126+
)
127+
)}
128+
</div>
92129
</>
93130
))}
94131
{isFormVisible ? (

app/actions/chatActions.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { ReplCommand, ReplOutput } from "../terminal/repl";
88
interface FormState {
99
response: string;
1010
error: string | null;
11+
targetSectionId: string;
1112
}
1213

1314
type ChatParams = {
@@ -140,15 +141,25 @@ export async function askAI(params: ChatParams): Promise<FormState> {
140141
if (!text) {
141142
throw new Error("AIからの応答が空でした");
142143
}
143-
return { response: text, error: null };
144+
return {
145+
response: text,
146+
error: null,
147+
// TODO: どのセクションへの回答にするかをAIに決めさせる
148+
targetSectionId: sectionContent.find((s) => s.inView)?.sectionId || "",
149+
};
144150
} catch (error: unknown) {
145151
console.error("Error calling Generative AI:", error);
146152
if (error instanceof Error) {
147153
return {
148154
response: "",
149155
error: `AIへのリクエスト中にエラーが発生しました: ${error.message}`,
156+
targetSectionId: sectionContent.find((s) => s.inView)?.sectionId || "",
150157
};
151158
}
152-
return { response: "", error: "予期せぬエラーが発生しました。" };
159+
return {
160+
response: "",
161+
error: "予期せぬエラーが発生しました。",
162+
targetSectionId: sectionContent.find((s) => s.inView)?.sectionId || "",
163+
};
153164
}
154165
}

app/hooks/useChathistory.ts

Lines changed: 0 additions & 43 deletions
This file was deleted.

app/sidebar.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,9 +42,9 @@ export function Sidebar() {
4242
<path
4343
d="M18 17L13 12L18 7M11 17L6 12L11 7"
4444
stroke="currentColor"
45-
stroke-width="1.5"
46-
stroke-linecap="round"
47-
stroke-linejoin="round"
45+
strokeWidth="1.5"
46+
strokeLinecap="round"
47+
strokeLinejoin="round"
4848
/>
4949
</svg>
5050
<span className="text-lg">Close</span>

0 commit comments

Comments
 (0)