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
70 changes: 50 additions & 20 deletions app/[docs_id]/chatForm.tsx
Original file line number Diff line number Diff line change
@@ -1,25 +1,26 @@
"use client";

import { useState, FormEvent } from "react";
import clsx from "clsx";
import { askAI } from "@/app/actions/chatActions";
import { StyledMarkdown } from "./markdown";
import { useChatHistory, type Message } from "../hooks/useChathistory";
import useSWR from "swr";
import { getQuestionExample } from "../actions/questionExample";
import { getLanguageName } from "../pagesList";

export function ChatForm({
docs_id,
documentContent,
}: {
docs_id: string;
interface ChatFormProps {
documentContent: string;
}) {
sectionId: string;
}

export function ChatForm({ documentContent, sectionId }: ChatFormProps) {
const [messages, updateChatHistory] = useChatHistory(sectionId);
const [inputValue, setInputValue] = useState("");
const [response, setResponse] = useState("");
const [isLoading, setIsLoading] = useState(false);
const [isFormVisible, setIsFormVisible] = useState(false);

const lang = getLanguageName(docs_id);
const lang = getLanguageName(sectionId);
const { data: exampleData, error: exampleError } = useSWR(
// 質問フォームを開いたときだけで良い
isFormVisible ? { lang, documentContent } : null,
Expand All @@ -41,7 +42,9 @@ export function ChatForm({
const handleSubmit = async (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
setIsLoading(true);
setResponse("");

const userMessage: Message = { sender: "user", text: inputValue };
updateChatHistory([userMessage]);

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

if (result.error) {
setResponse(`エラー: ${result.error}`);
const errorMessage: Message = { sender: "ai", text: `エラー: ${result.error}`, isError: true };
updateChatHistory([userMessage, errorMessage]);
} else {
setResponse(result.response);
const aiMessage: Message = { sender: "ai", text: result.response };
updateChatHistory([userMessage, aiMessage]);
setInputValue("");
}

setIsLoading(false);
};

const handleClearHistory = () => {
updateChatHistory([]);
};

return (
<>
{isFormVisible && (
Expand Down Expand Up @@ -92,8 +103,8 @@ export function ChatForm({
<button
className="btn btn-soft btn-secondary rounded-full"
onClick={() => setIsFormVisible(false)}
type="button"
>

閉じる
</button>
</div>
Expand Down Expand Up @@ -122,14 +133,33 @@ export function ChatForm({
</button>
)}

{response && (
<article>
<h3 className="text-lg font-semibold mb-2">AIの回答</h3>
<div className="chat chat-start">
<div className="chat-bubble bg-secondary-content text-black" style={{maxWidth: "100%", wordBreak: "break-word"}}>
<div className="response-container"><StyledMarkdown content={response}/></div>
</div>
{messages.length > 0 && (
<article className="mt-4">
<div className="flex justify-between items-center mb-2">
<h3 className="text-lg font-semibold">AIとのチャット</h3>
<button
onClick={handleClearHistory}
className="btn btn-ghost btn-sm text-xs"
aria-label="チャット履歴を削除"
>
履歴を削除
</button>
</div>
{messages.map((msg, index) => (
<div key={index} className={`chat ${msg.sender === 'user' ? 'chat-end' : 'chat-start'}`}>
<div
className={clsx(
"chat-bubble",
{ "bg-primary text-primary-content": msg.sender === 'user' },
{ "bg-secondary-content text-black": msg.sender === 'ai' && !msg.isError },
{ "chat-bubble-error": msg.isError }
)}
style={{maxWidth: "100%", wordBreak: "break-word"}}
>
<StyledMarkdown content={msg.text} />
</div>
</div>
))}
</article>
)}

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

</>
);
}
}
13 changes: 8 additions & 5 deletions app/[docs_id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,11 +45,14 @@ export default async function Page({

return (
<div className="p-4">
{splitMdContent.map((section, index) => (
<div key={index} id={`${index}`}>
<Section key={index} docs_id={docs_id} section={section} />
</div>
))}
{splitMdContent.map((section, index) => {
const sectionId = `${docs_id}-${index}`;
return (
<div key={index} id={`${index}`}>
<Section section={section} sectionId={sectionId} />
</div>
);
})}
</div>
);
}
15 changes: 7 additions & 8 deletions app/[docs_id]/section.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,13 @@ interface ISectionCodeContext {
const SectionCodeContext = createContext<ISectionCodeContext | null>(null);
export const useSectionCode = () => useContext(SectionCodeContext);

// 1つのセクションのタイトルと内容を表示する。内容はMarkdownとしてレンダリングする
export function Section({
docs_id,
section,
}: {
docs_id: string;
interface SectionProps {
section: MarkdownSection;
}) {
sectionId: string;
}

// 1つのセクションのタイトルと内容を表示する。内容はMarkdownとしてレンダリングする
export function Section({ section, sectionId }: SectionProps) {
// 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 @@ -77,7 +76,7 @@ export function Section({
<div>
<Heading level={section.level}>{section.title}</Heading>
<StyledMarkdown content={section.content} />
<ChatForm docs_id={docs_id} documentContent={section.content} />
<ChatForm documentContent={section.content} sectionId={sectionId} />
</div>
</SectionCodeContext.Provider>
);
Expand Down
40 changes: 40 additions & 0 deletions app/hooks/useChathistory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
"use client";

import { useState, useEffect, useCallback } from 'react';

export interface Message {
sender: "user" | "ai";
text: string;
isError?: boolean;
}

export const useChatHistory = (sectionId: string) => {
const [messages, setMessages] = useState<Message[]>([]);

const CHAT_HISTORY_KEY = `my-code-chat-history-${sectionId}`;

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]);

const updateChatHistory = useCallback((newMessages: Message[]) => {
setMessages(newMessages);
if (newMessages.length > 0) {
localStorage.setItem(CHAT_HISTORY_KEY, JSON.stringify(newMessages));
} else {
localStorage.removeItem(CHAT_HISTORY_KEY);
}
}, [CHAT_HISTORY_KEY]);

return [messages, updateChatHistory] as const;
};