Skip to content

Commit 1091043

Browse files
committed
Merge branch 'main' into generate-question-examples
2 parents da48a4c + fbc0519 commit 1091043

File tree

19 files changed

+585
-147
lines changed

19 files changed

+585
-147
lines changed

.github/workflows/update-docs.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ on:
44
branches: [ "main" ]
55
permissions:
66
contents: write
7+
concurrency:
8+
group: check-docs-main
9+
cancel-in-progress: false
710
jobs:
811
check-docs:
912
runs-on: ubuntu-latest

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2026 ut.code();
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

app/(docs)/@chat/chat/[chatId]/chatArea.tsx

Lines changed: 36 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -20,42 +20,49 @@ export function ChatAreaContainer(props: {
2020
<aside
2121
className={clsx(
2222
// モバイルでは全画面表示する
23-
"fixed inset-0 pt-20 bg-base-100",
23+
"fixed inset-0 bg-base-100",
2424
// PCではスクロールで動かない右サイドバー
25-
"has-chat-1:sticky has-chat-1:top-16 has-sidebar:top-0 has-chat-1:pt-4",
25+
// 左にサイドバーがない=navvarがある とき、navbar分のスペースをあける(top-16, h-[100vh-4rem])
26+
"has-chat-1:sticky has-chat-1:top-16 has-sidebar:top-0",
2627
"has-chat-1:basis-2/5 has-chat-1:max-w-chat-area has-chat-1:h-[calc(100vh-4rem)] has-sidebar:h-screen",
2728
"has-chat-1:shadow-md has-chat-1:bg-base-200",
2829
// navbar(z-40)よりは下、ChatListForSectionのdropdown(デフォルトでz-999だがz-30に変えている)よりも上
29-
"z-35",
30-
"p-4",
31-
"flex flex-col",
32-
"overflow-y-auto"
30+
"z-35"
3331
)}
3432
>
35-
<ChatAreaStateUpdater chatId={props.chatId} />
36-
<div className="flex flex-row items-center">
37-
<span className="flex-1 text-base font-bold opacity-40">
38-
AIへの質問
39-
</span>
40-
<Link className="btn btn-ghost" href="/chat" scroll={false}>
41-
<svg
42-
className="w-8 h-8 -scale-x-100"
43-
viewBox="0 0 24 24"
44-
fill="none"
45-
xmlns="http://www.w3.org/2000/svg"
46-
>
47-
<path
48-
d="M18 17L13 12L18 7M11 17L6 12L11 7"
49-
stroke="currentColor"
50-
strokeWidth="1.5"
51-
strokeLinecap="round"
52-
strokeLinejoin="round"
53-
/>
54-
</svg>
55-
<span className="text-lg">閉じる</span>
56-
</Link>
33+
<div className="absolute inset-x-0 bottom-0 h-16 bg-linear-to-t from-base-200 to-base-200/0 z-1" />
34+
<div
35+
className={clsx(
36+
"p-4 pb-16",
37+
"pt-20 has-chat-1:pt-4",
38+
"h-full flex flex-col overflow-y-auto"
39+
)}
40+
>
41+
<ChatAreaStateUpdater chatId={props.chatId} />
42+
<div className="flex flex-row items-center">
43+
<span className="flex-1 text-base font-bold opacity-40">
44+
AIへの質問
45+
</span>
46+
<Link className="btn btn-ghost" href="/chat" scroll={false}>
47+
<svg
48+
className="w-8 h-8 -scale-x-100"
49+
viewBox="0 0 24 24"
50+
fill="none"
51+
xmlns="http://www.w3.org/2000/svg"
52+
>
53+
<path
54+
d="M18 17L13 12L18 7M11 17L6 12L11 7"
55+
stroke="currentColor"
56+
strokeWidth="1.5"
57+
strokeLinecap="round"
58+
strokeLinejoin="round"
59+
/>
60+
</svg>
61+
<span className="text-lg">閉じる</span>
62+
</Link>
63+
</div>
64+
{props.children}
5765
</div>
58-
{props.children}
5966
</aside>
6067
);
6168
}

app/(docs)/@docs/[lang]/[pageId]/chatForm.tsx

Lines changed: 48 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"use client";
22

3-
import { useState, FormEvent, useEffect, useMemo } from "react";
3+
import { useState, FormEvent, useEffect, useRef, useCallback, useMemo } from "react";
44
// import useSWR from "swr";
55
// import {
66
// getQuestionExample,
@@ -9,9 +9,10 @@ import { useState, FormEvent, useEffect, useMemo } from "react";
99
// import { getLanguageName } from "../pagesList";
1010
import { useEmbedContext } from "@/terminal/embedContext";
1111
import { DynamicMarkdownSection, PagePath } from "@/lib/docs";
12-
import { useRouter } from "next/navigation";
12+
import { usePathname, useRouter } from "next/navigation";
1313
import { ChatStreamEvent } from "@/api/chat/route";
1414
import { useStreamingChatContext } from "@/(docs)/streamingChatContext";
15+
import { revalidateChatAction } from "@/actions/revalidateChat";
1516

1617
interface ChatFormProps {
1718
path: PagePath;
@@ -30,6 +31,37 @@ export function ChatForm({ path, sectionContent, close }: ChatFormProps) {
3031
const router = useRouter();
3132
const streamingChatContext = useStreamingChatContext();
3233

34+
const pathname = usePathname();
35+
const pendingRouterPushTarget = useRef<null | string>(null);
36+
const pendingRouterPushResolver = useRef<null | (() => void)>(null);
37+
// router.pushの完了を待つ関数。pathnameの変化でページ遷移の完了を検知し、解決する。
38+
const asyncRouterPush = useCallback(
39+
(url: string, options?: { scroll?: boolean }) => {
40+
if (pendingRouterPushTarget.current) {
41+
console.error(
42+
"Already navigating to",
43+
pendingRouterPushTarget.current,
44+
"can't navigate to",
45+
url
46+
);
47+
return;
48+
}
49+
pendingRouterPushTarget.current = url;
50+
return new Promise<void>((resolve) => {
51+
pendingRouterPushResolver.current = resolve;
52+
router.push(url, options);
53+
});
54+
},
55+
[router]
56+
);
57+
useEffect(() => {
58+
if (pendingRouterPushTarget.current === pathname) {
59+
pendingRouterPushResolver.current?.();
60+
pendingRouterPushTarget.current = null;
61+
pendingRouterPushResolver.current = null;
62+
}
63+
}, [pathname]);
64+
3365
const exampleData = useMemo(
3466
() =>
3567
sectionContent
@@ -97,6 +129,7 @@ export function ChatForm({ path, sectionContent, close }: ChatFormProps) {
97129
const reader = response.body!.getReader();
98130
const decoder = new TextDecoder();
99131
let buffer = "";
132+
let chatId: string | null = null;
100133
let navigated = false;
101134

102135
// ストリームを非同期で読み続ける(ナビゲーション後もバックグラウンドで継続)
@@ -117,11 +150,16 @@ export function ChatForm({ path, sectionContent, close }: ChatFormProps) {
117150
const event = JSON.parse(line) as ChatStreamEvent;
118151

119152
if (event.type === "chat") {
153+
// revalidateChatは/api/chatの中では呼ばず、別のServerActionとして呼び出す
154+
await revalidateChatAction(event.chatId, path);
155+
chatId = event.chatId;
120156
streamingChatContext.startStreaming(event.chatId);
121157
document.getElementById(event.sectionId)?.scrollIntoView({
122158
behavior: "smooth",
123159
});
124-
router.push(`/chat/${event.chatId}`, { scroll: false });
160+
await asyncRouterPush(`/chat/${event.chatId}`, {
161+
scroll: false,
162+
});
125163
router.refresh();
126164
navigated = true;
127165
setIsLoading(false);
@@ -130,14 +168,21 @@ export function ChatForm({ path, sectionContent, close }: ChatFormProps) {
130168
} else if (event.type === "chunk") {
131169
streamingChatContext.appendChunk(event.text);
132170
} else if (event.type === "done") {
171+
if (chatId) {
172+
await revalidateChatAction(chatId, path);
173+
}
133174
streamingChatContext.finishStreaming();
134175
router.refresh();
135176
} else if (event.type === "error") {
136177
if (!navigated) {
137178
setErrorMessage(event.message);
138179
setIsLoading(false);
139180
}
181+
if (chatId) {
182+
await revalidateChatAction(chatId, path);
183+
}
140184
streamingChatContext.finishStreaming();
185+
router.refresh();
141186
}
142187
} catch {
143188
// ignore JSON parse errors

app/(docs)/@docs/[lang]/[pageId]/pageContent.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,7 @@ export function PageContent(props: PageContentProps) {
146146
const [isFormVisible, setIsFormVisible] = useState(false);
147147

148148
return (
149-
<div className="flex-1 p-4 flex flex-col">
149+
<div className="flex-1 p-4 pb-16 flex flex-col">
150150
<div
151151
className="max-w-full mx-auto grid"
152152
style={{

app/actions/deleteChat.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,22 @@
11
"use server";
22

33
import { z } from "zod";
4-
import { deleteChat, initContext } from "@/lib/chatHistory";
4+
import { deleteChat, initContext, revalidateChat } from "@/lib/chatHistory";
5+
import { section } from "@/schema/chat";
6+
import { eq } from "drizzle-orm";
57

68
export async function deleteChatAction(chatId: string) {
79
chatId = z.uuid().parse(chatId);
810
const ctx = await initContext();
9-
await deleteChat(chatId, ctx);
11+
if (!ctx.userId) {
12+
throw new Error("Not authenticated");
13+
}
14+
const deletedChat = await deleteChat(chatId, ctx);
15+
16+
const targetSection = await ctx.drizzle.query.section.findFirst({
17+
where: eq(section.sectionId, deletedChat[0].sectionId),
18+
});
19+
if (targetSection) {
20+
await revalidateChat(chatId, ctx.userId, targetSection.pagePath);
21+
}
1022
}

app/actions/revalidateChat.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
"use server";
2+
3+
import { initContext, revalidateChat } from "@/lib/chatHistory";
4+
import { PagePath, PagePathSchema } from "@/lib/docs";
5+
import { z } from "zod";
6+
7+
export async function revalidateChatAction(
8+
chatId: string,
9+
pagePath: string | PagePath
10+
) {
11+
chatId = z.uuid().parse(chatId);
12+
if (typeof pagePath === "string") {
13+
if (!/^[a-z0-9_-]+\/[a-z0-9_-]+$/.test(pagePath)) {
14+
throw new Error("Invalid pagePath format");
15+
}
16+
const [lang, page] = pagePath.split("/");
17+
pagePath = PagePathSchema.parse({ lang, page });
18+
} else {
19+
pagePath = PagePathSchema.parse(pagePath);
20+
}
21+
const ctx = await initContext();
22+
if (!ctx.userId) {
23+
throw new Error("Not authenticated");
24+
}
25+
await revalidateChat(chatId, ctx.userId, pagePath);
26+
}

0 commit comments

Comments
 (0)