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
51 changes: 48 additions & 3 deletions app/(docs)/@docs/[lang]/[pageId]/chatForm.tsx
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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;
Expand All @@ -32,6 +33,37 @@ export function ChatForm({ path, sectionContent, close }: ChatFormProps) {
const router = useRouter();
const streamingChatContext = useStreamingChatContext();

const pathname = usePathname();
const pendingRouterPushTarget = useRef<null | string>(null);
const pendingRouterPushResolver = useRef<null | (() => 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<void>((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)
Expand Down Expand Up @@ -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;

// ストリームを非同期で読み続ける(ナビゲーション後もバックグラウンドで継続)
Expand All @@ -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);
Expand All @@ -131,14 +169,21 @@ 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") {
if (!navigated) {
setErrorMessage(event.message);
setIsLoading(false);
}
if (chatId) {
await revalidateChatAction(chatId, path);
}
streamingChatContext.finishStreaming();
router.refresh();
}
} catch {
// ignore JSON parse errors
Expand Down
16 changes: 14 additions & 2 deletions app/actions/deleteChat.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,22 @@
"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),
});
if (targetSection) {
await revalidateChat(chatId, ctx.userId, targetSection.pagePath);
}
}
26 changes: 26 additions & 0 deletions app/actions/revalidateChat.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
"use server";

import { initContext, revalidateChat } from "@/lib/chatHistory";
import { PagePath, PagePathSchema } 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("/");
pagePath = PagePathSchema.parse({ 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);
}
15 changes: 6 additions & 9 deletions app/lib/chatHistory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -126,8 +130,6 @@ export async function addChat(
chatDiffs = [] as never[];
}

await revalidateChat(newChat.chatId, userId, path);

return {
...newChat,
section: {
Expand Down Expand Up @@ -173,8 +175,6 @@ export async function addMessagesAndDiffs(
}))
);
}

await revalidateChat(chatId, userId, path);
}

export async function deleteChat(chatId: string, context: Context) {
Expand All @@ -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(
Expand Down
Loading