diff --git a/app/(docs)/@chat/chat/[chatId]/chatArea.tsx b/app/(docs)/@chat/chat/[chatId]/chatArea.tsx new file mode 100644 index 00000000..80c28aba --- /dev/null +++ b/app/(docs)/@chat/chat/[chatId]/chatArea.tsx @@ -0,0 +1,208 @@ +"use client"; + +import { ChatAreaStateUpdater } from "@/(docs)/chatAreaState"; +import { deleteChatAction } from "@/actions/deleteChat"; +import { ChatWithMessages } from "@/lib/chatHistory"; +import { LanguageEntry, MarkdownSection, PageEntry } from "@/lib/docs"; +import { Heading } from "@/markdown/heading"; +import { StyledMarkdown } from "@/markdown/markdown"; +import clsx from "clsx"; +import Link from "next/link"; +import { useRouter } from "next/navigation"; +import { ReactNode } from "react"; + +export function ChatAreaContainer(props: { chatId: string; children: ReactNode }) { + return ( + + ); +} + +interface Props { + chatId: string; + chatData: ChatWithMessages; + targetLang: LanguageEntry | undefined; + targetPage: PageEntry | undefined; + targetSection: MarkdownSection | undefined; +} +export function ChatAreaContent(props: Props) { + const { chatId, chatData, targetLang, targetPage, targetSection } = props; + + const messagesAndDiffs = [ + ...chatData.messages.map((msg) => ({ type: "message" as const, ...msg })), + ...chatData.diff.map((diff) => ({ type: "diff" as const, ...diff })), + ]; + messagesAndDiffs.sort( + (a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime() + ); + + const router = useRouter(); + + return ( + <> + + {chatData.title} + +
+ +
+
+
+ {chatData.createdAt.toLocaleString()} +
+ +
+
+ {messagesAndDiffs.map((msg, index) => + msg.type === "message" ? ( + msg.role === "user" ? ( +
+
+ +
+
+ ) : msg.role === "ai" ? ( +
+ +
+ ) : ( +
+ {msg.content} +
+ ) + ) : ( +
+ {/* pb-0だとmargin collapsingが起きて変な隙間が空く */} + + + + + + +
+ ) + )} + + ); +} diff --git a/app/(docs)/@chat/chat/[chatId]/loading.tsx b/app/(docs)/@chat/chat/[chatId]/loading.tsx new file mode 100644 index 00000000..71f939fe --- /dev/null +++ b/app/(docs)/@chat/chat/[chatId]/loading.tsx @@ -0,0 +1,16 @@ +import { ChatAreaContainer } from "./chatArea"; + +export default function Loading() { + return ( + +
{/* heading2 */}
+
{/* breadcrumbs */}
+
{/* date */}
+
+
{/* chat */}
+
{/*

*/}

+
{/* chat */}
+
{/*

*/}

+ + ); +} diff --git a/app/(docs)/@chat/chat/[chatId]/page.tsx b/app/(docs)/@chat/chat/[chatId]/page.tsx new file mode 100644 index 00000000..35462ced --- /dev/null +++ b/app/(docs)/@chat/chat/[chatId]/page.tsx @@ -0,0 +1,78 @@ +import { + cacheKeyForChat, + ChatWithMessages, + getChatOne, + initContext, +} from "@/lib/chatHistory"; +import { getMarkdownSections, getPagesList } from "@/lib/docs"; +import { ChatAreaContainer, ChatAreaContent } from "./chatArea"; +import { unstable_cacheLife, unstable_cacheTag } from "next/cache"; +import { isCloudflare } from "@/lib/detectCloudflare"; + +export default async function ChatPage({ + params, +}: { + params: Promise<{ chatId: string }>; +}) { + const { chatId } = await params; + + const context = await initContext(); + const chatData = await getChatOneFromCache(chatId, context.userId); + + if (!chatData) { + // notFound(); だとページ全体が404になってしまう + return ( + +

指定されたチャットのデータが見つかりません。

+
+ ); + } + + const pagesList = await getPagesList(); + const targetLang = pagesList.find( + (lang) => lang.id === chatData.section.pagePath.split("/")[0] + ); + const targetPage = targetLang?.pages.find( + (page) => page.slug === chatData.section.pagePath.split("/")[1] + ); + const sections = + targetLang && targetPage + ? await getMarkdownSections(targetLang.id, targetPage.slug) + : []; + const targetSection = sections.find((sec) => sec.id === chatData.sectionId); + + return ( + + + + ); +} + +async function getChatOneFromCache(chatId: string, userId?: string) { + "use cache"; + unstable_cacheLife("days"); + unstable_cacheTag(cacheKeyForChat(chatId)); + + if (!userId) { + return null; + } + + if (isCloudflare()) { + const cache = await caches.open("chatHistory"); + const cachedResponse = await cache.match(cacheKeyForChat(chatId)); + if (cachedResponse) { + const data = (await cachedResponse.json()) as ChatWithMessages; + return data; + } + } + + const context = await initContext({ userId }); + const chatData = await getChatOne(chatId, context); + return chatData; +} diff --git a/app/(docs)/@chat/chat/page.tsx b/app/(docs)/@chat/chat/page.tsx new file mode 100644 index 00000000..30497200 --- /dev/null +++ b/app/(docs)/@chat/chat/page.tsx @@ -0,0 +1,7 @@ +import { ChatAreaStateUpdater } from "../../chatAreaState"; + +// /chat にアクセスしたときチャットを閉じる + +export default function EmptyPage() { + return ; +} diff --git a/app/(docs)/@chat/default.tsx b/app/(docs)/@chat/default.tsx new file mode 100644 index 00000000..db011017 --- /dev/null +++ b/app/(docs)/@chat/default.tsx @@ -0,0 +1,5 @@ +import { ChatAreaStateUpdater } from "../chatAreaState"; + +export default function EmptyPage() { + return ; +} diff --git a/app/(docs)/@chat/error.tsx b/app/(docs)/@chat/error.tsx new file mode 100644 index 00000000..a9307c3f --- /dev/null +++ b/app/(docs)/@chat/error.tsx @@ -0,0 +1,31 @@ +"use client"; // Error boundaries must be Client Components + +import clsx from "clsx"; +import { ChatAreaContainer } from "./chat/[chatId]/chatArea"; + +export default function Error({ + error, + // reset, +}: { + error: Error & { digest?: string }; + reset: () => void; +}) { + return ( + +

ページの読み込み中にエラーが発生しました。

+
+        {error.message}
+      
+ {error.digest && ( +

+ Digest: {error.digest} +

+ )} +
+ ); +} diff --git a/app/(docs)/@docs/[lang]/[pageId]/autoRedirect.tsx b/app/(docs)/@docs/[lang]/[pageId]/autoRedirect.tsx new file mode 100644 index 00000000..97158026 --- /dev/null +++ b/app/(docs)/@docs/[lang]/[pageId]/autoRedirect.tsx @@ -0,0 +1,19 @@ +"use client"; + +import { PagePath } from "@/lib/docs"; +import { usePathname, useRouter } from "next/navigation"; +import { useEffect } from "react"; + +export function DocsAutoRedirect(props: { path: PagePath }) { + const pathname = usePathname(); + const router = useRouter(); + useEffect(() => { + if (pathname === `/chat`) { + router.replace(`/${props.path.lang}/${props.path.page}`, { + scroll: false, + }); + } + }, [pathname, router, props.path.lang, props.path.page]); + + return null; +} diff --git a/app/[lang]/[pageId]/chatForm.tsx b/app/(docs)/@docs/[lang]/[pageId]/chatForm.tsx similarity index 95% rename from app/[lang]/[pageId]/chatForm.tsx rename to app/(docs)/@docs/[lang]/[pageId]/chatForm.tsx index 0231dc86..65dfd501 100644 --- a/app/[lang]/[pageId]/chatForm.tsx +++ b/app/(docs)/@docs/[lang]/[pageId]/chatForm.tsx @@ -9,9 +9,9 @@ import { useState, FormEvent, useEffect } from "react"; // import { getLanguageName } from "../pagesList"; import { DynamicMarkdownSection } from "./pageContent"; import { useEmbedContext } from "@/terminal/embedContext"; -import { useChatHistoryContext } from "./chatHistory"; import { askAI } from "@/actions/chatActions"; import { PagePath } from "@/lib/docs"; +import { useRouter } from "next/navigation"; interface ChatFormProps { path: PagePath; @@ -25,12 +25,12 @@ export function ChatForm({ path, sectionContent, close }: ChatFormProps) { const [isLoading, setIsLoading] = useState(false); const [errorMessage, setErrorMessage] = useState(null); - const { addChat } = useChatHistoryContext(); - // const lang = getLanguageName(docs_id); const { files, replOutputs, execResults } = useEmbedContext(); + const router = useRouter(); + // const documentContentInView = sectionContent // .filter((s) => s.inView) // .map((s) => s.rawContent) @@ -87,10 +87,11 @@ export function ChatForm({ path, sectionContent, close }: ChatFormProps) { setErrorMessage(result.error); console.log(result.error); } else { - addChat(result.chat); document.getElementById(result.chat.sectionId)?.scrollIntoView({ behavior: "smooth", }); + router.push(`/chat/${result.chat.chatId}`, { scroll: false }); + router.refresh(); setInputValue(""); close(); } @@ -100,7 +101,7 @@ export function ChatForm({ path, sectionContent, close }: ChatFormProps) { return (