From ab6bde85e72e0efebf76971178250516f06dbfe7 Mon Sep 17 00:00:00 2001 From: na-trium-144 <100704180+na-trium-144@users.noreply.github.com> Date: Fri, 20 Mar 2026 18:01:12 +0900 Subject: [PATCH 1/4] =?UTF-8?q?=E3=83=81=E3=83=A3=E3=83=83=E3=83=88?= =?UTF-8?q?=E6=99=82=E3=81=AE=E3=82=AD=E3=83=A3=E3=83=83=E3=82=B7=E3=83=A5?= =?UTF-8?q?=E3=81=A8=E3=83=9A=E3=83=BC=E3=82=B8=E9=81=B7=E7=A7=BB=E3=82=92?= =?UTF-8?q?=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/(docs)/@docs/[lang]/[pageId]/chatForm.tsx | 51 +++++++++++++++++-- app/actions/deleteChat.ts | 14 ++++- app/actions/revalidateChat.ts | 26 ++++++++++ app/lib/chatHistory.ts | 15 +++--- 4 files changed, 92 insertions(+), 14 deletions(-) create mode 100644 app/actions/revalidateChat.ts diff --git a/app/(docs)/@docs/[lang]/[pageId]/chatForm.tsx b/app/(docs)/@docs/[lang]/[pageId]/chatForm.tsx index f38ce7ee..1a1abb6e 100644 --- a/app/(docs)/@docs/[lang]/[pageId]/chatForm.tsx +++ b/app/(docs)/@docs/[lang]/[pageId]/chatForm.tsx @@ -1,6 +1,6 @@ "use client"; -import { useState, FormEvent, useEffect } from "react"; +import { useState, FormEvent, useEffect, useRef, useCallback } from "react"; // import useSWR from "swr"; // import { // getQuestionExample, @@ -9,9 +9,10 @@ import { useState, FormEvent, useEffect } from "react"; // import { getLanguageName } from "../pagesList"; import { useEmbedContext } from "@/terminal/embedContext"; import { DynamicMarkdownSection, PagePath } from "@/lib/docs"; -import { useRouter } from "next/navigation"; +import { usePathname, useRouter } from "next/navigation"; import { ChatStreamEvent } from "@/api/chat/route"; import { useStreamingChatContext } from "@/(docs)/streamingChatContext"; +import { revalidateChatAction } from "@/actions/revalidateChat"; interface ChatFormProps { path: PagePath; @@ -32,6 +33,37 @@ export function ChatForm({ path, sectionContent, close }: ChatFormProps) { const router = useRouter(); const streamingChatContext = useStreamingChatContext(); + const pathname = usePathname(); + const pendingRouterPushTarget = useRef(null); + const pendingRouterPushResolver = useRef void)>(null); + // router.pushの完了を待つ関数。pathnameの変化でページ遷移の完了を検知し、解決する。 + const asyncRouterPush = useCallback( + (url: string, options?: { scroll?: boolean }) => { + if (pendingRouterPushTarget.current) { + console.error( + "Already navigating to", + pendingRouterPushTarget.current, + "can't navigate to", + url + ); + return; + } + pendingRouterPushTarget.current = url; + return new Promise((resolve) => { + pendingRouterPushResolver.current = resolve; + router.push(url, options); + }); + }, + [router] + ); + useEffect(() => { + if (pendingRouterPushTarget.current === pathname) { + pendingRouterPushResolver.current?.(); + pendingRouterPushTarget.current = null; + pendingRouterPushResolver.current = null; + } + }, [pathname]); + // const documentContentInView = sectionContent // .filter((s) => s.inView) // .map((s) => s.rawContent) @@ -98,6 +130,7 @@ export function ChatForm({ path, sectionContent, close }: ChatFormProps) { const reader = response.body!.getReader(); const decoder = new TextDecoder(); let buffer = ""; + let chatId: string | null = null; let navigated = false; // ストリームを非同期で読み続ける(ナビゲーション後もバックグラウンドで継続) @@ -118,11 +151,16 @@ export function ChatForm({ path, sectionContent, close }: ChatFormProps) { const event = JSON.parse(line) as ChatStreamEvent; if (event.type === "chat") { + // revalidateChatは/api/chatの中では呼ばず、別のServerActionとして呼び出す + await revalidateChatAction(event.chatId, path); + chatId = event.chatId; streamingChatContext.startStreaming(event.chatId); document.getElementById(event.sectionId)?.scrollIntoView({ behavior: "smooth", }); - router.push(`/chat/${event.chatId}`, { scroll: false }); + await asyncRouterPush(`/chat/${event.chatId}`, { + scroll: false, + }); router.refresh(); navigated = true; setIsLoading(false); @@ -131,6 +169,9 @@ export function ChatForm({ path, sectionContent, close }: ChatFormProps) { } else if (event.type === "chunk") { streamingChatContext.appendChunk(event.text); } else if (event.type === "done") { + if (chatId) { + await revalidateChatAction(chatId, path); + } streamingChatContext.finishStreaming(); router.refresh(); } else if (event.type === "error") { @@ -138,7 +179,11 @@ export function ChatForm({ path, sectionContent, close }: ChatFormProps) { setErrorMessage(event.message); setIsLoading(false); } + if (chatId) { + await revalidateChatAction(chatId, path); + } streamingChatContext.finishStreaming(); + router.refresh(); } } catch { // ignore JSON parse errors diff --git a/app/actions/deleteChat.ts b/app/actions/deleteChat.ts index 65ec9507..98fd9815 100644 --- a/app/actions/deleteChat.ts +++ b/app/actions/deleteChat.ts @@ -1,10 +1,20 @@ "use server"; import { z } from "zod"; -import { deleteChat, initContext } from "@/lib/chatHistory"; +import { deleteChat, initContext, revalidateChat } from "@/lib/chatHistory"; +import { section } from "@/schema/chat"; +import { eq } from "drizzle-orm"; export async function deleteChatAction(chatId: string) { chatId = z.uuid().parse(chatId); const ctx = await initContext(); - await deleteChat(chatId, ctx); + if (!ctx.userId) { + throw new Error("Not authenticated"); + } + const deletedChat = await deleteChat(chatId, ctx); + + const targetSection = await ctx.drizzle.query.section.findFirst({ + where: eq(section.sectionId, deletedChat[0].sectionId), + }); + await revalidateChat(chatId, ctx.userId, targetSection?.pagePath ?? ""); } diff --git a/app/actions/revalidateChat.ts b/app/actions/revalidateChat.ts new file mode 100644 index 00000000..1163901d --- /dev/null +++ b/app/actions/revalidateChat.ts @@ -0,0 +1,26 @@ +"use server"; + +import { initContext, revalidateChat } from "@/lib/chatHistory"; +import { LangId, PagePath, PagePathSchema, PageSlug } from "@/lib/docs"; +import z from "zod"; + +export async function revalidateChatAction( + chatId: string, + pagePath: string | PagePath +) { + chatId = z.uuid().parse(chatId); + if (typeof pagePath === "string") { + if (!/^[a-z0-9-_]+\/[a-z0-9-_]+$/.test(pagePath)) { + throw new Error("Invalid pagePath format"); + } + const [lang, page] = pagePath.split("/") as [LangId, PageSlug]; + pagePath = { lang, page }; + } else { + pagePath = PagePathSchema.parse(pagePath); + } + const ctx = await initContext(); + if (!ctx.userId) { + throw new Error("Not authenticated"); + } + await revalidateChat(chatId, ctx.userId, pagePath); +} diff --git a/app/lib/chatHistory.ts b/app/lib/chatHistory.ts index c2182006..e1392102 100644 --- a/app/lib/chatHistory.ts +++ b/app/lib/chatHistory.ts @@ -28,7 +28,11 @@ export function cacheKeyForChat(chatId: string) { return `${CACHE_KEY_BASE}/getChatOne?chatId=${chatId}`; } -async function revalidateChat( +// nextjsのキャッシュのrevalidateはRouteHandlerではなくServerActionから呼ばないと正しく動作しないらしい。 +// https://github.com/vercel/next.js/issues/69064 +// そのためlib/以下の関数では直接revalidateChatを呼ばず、ServerActionの関数から呼ぶようにする。 +// Nextjs 16 に更新したらこれをupdateTag()で置き換える。 +export async function revalidateChat( chatId: string, userId: string, pagePath: string | PagePath @@ -126,8 +130,6 @@ export async function addChat( chatDiffs = [] as never[]; } - await revalidateChat(newChat.chatId, userId, path); - return { ...newChat, section: { @@ -173,8 +175,6 @@ export async function addMessagesAndDiffs( })) ); } - - await revalidateChat(chatId, userId, path); } export async function deleteChat(chatId: string, context: Context) { @@ -192,10 +192,7 @@ export async function deleteChat(chatId: string, context: Context) { await drizzle.delete(message).where(eq(message.chatId, chatId)); await drizzle.delete(diff).where(eq(diff.chatId, chatId)); - const targetSection = await drizzle.query.section.findFirst({ - where: eq(section.sectionId, deletedChat[0].sectionId), - }); - await revalidateChat(chatId, userId, targetSection?.pagePath ?? ""); + return deletedChat; } export async function getAllChat( From bb695a3ad8b4fb9d8be5659fab0bb054052b1452 Mon Sep 17 00:00:00 2001 From: "k. Naka" <100704180+na-trium-144@users.noreply.github.com> Date: Fri, 20 Mar 2026 19:02:35 +0900 Subject: [PATCH 2/4] Update app/actions/revalidateChat.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- app/actions/revalidateChat.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/actions/revalidateChat.ts b/app/actions/revalidateChat.ts index 1163901d..95099cc5 100644 --- a/app/actions/revalidateChat.ts +++ b/app/actions/revalidateChat.ts @@ -2,7 +2,7 @@ import { initContext, revalidateChat } from "@/lib/chatHistory"; import { LangId, PagePath, PagePathSchema, PageSlug } from "@/lib/docs"; -import z from "zod"; +import { z } from "zod"; export async function revalidateChatAction( chatId: string, From 1011e3ee5c695e976362a81a3c160333d597c3d5 Mon Sep 17 00:00:00 2001 From: "k. Naka" <100704180+na-trium-144@users.noreply.github.com> Date: Fri, 20 Mar 2026 19:02:47 +0900 Subject: [PATCH 3/4] Update app/actions/revalidateChat.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- app/actions/revalidateChat.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/actions/revalidateChat.ts b/app/actions/revalidateChat.ts index 95099cc5..1f502ece 100644 --- a/app/actions/revalidateChat.ts +++ b/app/actions/revalidateChat.ts @@ -1,7 +1,7 @@ "use server"; import { initContext, revalidateChat } from "@/lib/chatHistory"; -import { LangId, PagePath, PagePathSchema, PageSlug } from "@/lib/docs"; +import { PagePath, PagePathSchema } from "@/lib/docs"; import { z } from "zod"; export async function revalidateChatAction( @@ -10,11 +10,11 @@ export async function revalidateChatAction( ) { chatId = z.uuid().parse(chatId); if (typeof pagePath === "string") { - if (!/^[a-z0-9-_]+\/[a-z0-9-_]+$/.test(pagePath)) { + if (!/^[a-z0-9_-]+\/[a-z0-9_-]+$/.test(pagePath)) { throw new Error("Invalid pagePath format"); } - const [lang, page] = pagePath.split("/") as [LangId, PageSlug]; - pagePath = { lang, page }; + const [lang, page] = pagePath.split("/"); + pagePath = PagePathSchema.parse({ lang, page }); } else { pagePath = PagePathSchema.parse(pagePath); } From aae14077e70315cedae7ed26245ccbaddd574644 Mon Sep 17 00:00:00 2001 From: na-trium-144 <100704180+na-trium-144@users.noreply.github.com> Date: Fri, 20 Mar 2026 19:22:57 +0900 Subject: [PATCH 4/4] =?UTF-8?q?=E7=A9=BA=E6=96=87=E5=AD=97=E5=88=97?= =?UTF-8?q?=E3=81=AEpagePath=E3=81=B8=E3=81=AE=E3=83=95=E3=82=A9=E3=83=BC?= =?UTF-8?q?=E3=83=AB=E3=83=90=E3=83=83=E3=82=AF=E3=82=92=E5=89=8A=E9=99=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/actions/deleteChat.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/actions/deleteChat.ts b/app/actions/deleteChat.ts index 98fd9815..9edcac82 100644 --- a/app/actions/deleteChat.ts +++ b/app/actions/deleteChat.ts @@ -16,5 +16,7 @@ export async function deleteChatAction(chatId: string) { const targetSection = await ctx.drizzle.query.section.findFirst({ where: eq(section.sectionId, deletedChat[0].sectionId), }); - await revalidateChat(chatId, ctx.userId, targetSection?.pagePath ?? ""); + if (targetSection) { + await revalidateChat(chatId, ctx.userId, targetSection.pagePath); + } }