Skip to content

Commit 8814f0b

Browse files
committed
Merge remote-tracking branch 'origin/main' into generate-question-examples
2 parents 899403d + 876b81d commit 8814f0b

36 files changed

+1581
-271
lines changed
Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
"use client";
2+
3+
import { ChatAreaStateUpdater } from "@/(docs)/chatAreaState";
4+
import { deleteChatAction } from "@/actions/deleteChat";
5+
import { ChatWithMessages } from "@/lib/chatHistory";
6+
import { LanguageEntry, MarkdownSection, PageEntry } from "@/lib/docs";
7+
import { Heading } from "@/markdown/heading";
8+
import { StyledMarkdown } from "@/markdown/markdown";
9+
import clsx from "clsx";
10+
import Link from "next/link";
11+
import { useRouter } from "next/navigation";
12+
import { ReactNode } from "react";
13+
14+
export function ChatAreaContainer(props: { chatId: string; children: ReactNode }) {
15+
return (
16+
<aside
17+
className={clsx(
18+
// モバイルでは全画面表示する
19+
"fixed inset-0 pt-20 bg-base-100",
20+
// PCではスクロールで動かない右サイドバー
21+
"has-chat-1:sticky has-chat-1:top-16 has-sidebar:top-0 has-chat-1:pt-4",
22+
"has-chat-1:basis-2/5 has-chat-1:max-w-chat-area has-chat-1:h-[calc(100vh-4rem)] has-sidebar:h-screen",
23+
"has-chat-1:shadow-md has-chat-1:bg-base-200",
24+
// navbar(z-40)よりは下、ChatListForSectionのdropdown(デフォルトでz-999だがz-30に変えている)よりも上
25+
"z-35",
26+
"p-4",
27+
"flex flex-col",
28+
"overflow-y-auto"
29+
)}
30+
>
31+
<ChatAreaStateUpdater chatId={props.chatId} />
32+
<div className="flex flex-row items-center">
33+
<span className="flex-1 text-base font-bold opacity-40">
34+
AIへの質問
35+
</span>
36+
<Link className="btn btn-ghost" href="/chat" scroll={false}>
37+
<svg
38+
className="w-8 h-8 -scale-x-100"
39+
viewBox="0 0 24 24"
40+
fill="none"
41+
xmlns="http://www.w3.org/2000/svg"
42+
>
43+
<path
44+
d="M18 17L13 12L18 7M11 17L6 12L11 7"
45+
stroke="currentColor"
46+
strokeWidth="1.5"
47+
strokeLinecap="round"
48+
strokeLinejoin="round"
49+
/>
50+
</svg>
51+
<span className="text-lg">閉じる</span>
52+
</Link>
53+
</div>
54+
{props.children}
55+
</aside>
56+
);
57+
}
58+
59+
interface Props {
60+
chatId: string;
61+
chatData: ChatWithMessages;
62+
targetLang: LanguageEntry | undefined;
63+
targetPage: PageEntry | undefined;
64+
targetSection: MarkdownSection | undefined;
65+
}
66+
export function ChatAreaContent(props: Props) {
67+
const { chatId, chatData, targetLang, targetPage, targetSection } = props;
68+
69+
const messagesAndDiffs = [
70+
...chatData.messages.map((msg) => ({ type: "message" as const, ...msg })),
71+
...chatData.diff.map((diff) => ({ type: "diff" as const, ...diff })),
72+
];
73+
messagesAndDiffs.sort(
74+
(a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime()
75+
);
76+
77+
const router = useRouter();
78+
79+
return (
80+
<>
81+
<Heading level={2} className="mt-2!">
82+
{chatData.title}
83+
</Heading>
84+
<div className="flex-none breadcrumbs text-sm">
85+
<ul className="flex-wrap">
86+
<li>
87+
<Link href={`/${targetLang?.id}/${targetLang?.pages[0].slug}`}>
88+
{targetLang?.name}
89+
</Link>
90+
</li>
91+
<li>
92+
<Link href={`/${chatData.section.pagePath}`}>
93+
{targetPage?.index}. {targetPage?.name}
94+
</Link>
95+
</li>
96+
<li>
97+
<Link href={`/${chatData.section.pagePath}#${chatData.sectionId}`}>
98+
{targetSection?.title}
99+
</Link>
100+
</li>
101+
</ul>
102+
</div>
103+
<div className="flex flex-wrap items-center">
104+
<div className="flex-1 text-sm opacity-40" suppressHydrationWarning>
105+
{chatData.createdAt.toLocaleString()}
106+
</div>
107+
<button
108+
className="btn btn-error btn-soft btn-sm"
109+
onClick={async () => {
110+
if (confirm("このチャットを削除してもよろしいですか?")) {
111+
await deleteChatAction(chatId);
112+
router.push("/chat", { scroll: false });
113+
router.refresh();
114+
}
115+
}}
116+
>
117+
{/*<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->*/}
118+
<svg
119+
className="w-4 h-4"
120+
viewBox="0 0 24 24"
121+
fill="none"
122+
xmlns="http://www.w3.org/2000/svg"
123+
>
124+
<path
125+
d="M10 11V17"
126+
stroke="currentColor"
127+
strokeWidth="2"
128+
strokeLinecap="round"
129+
strokeLinejoin="round"
130+
/>
131+
<path
132+
d="M14 11V17"
133+
stroke="currentColor"
134+
strokeWidth="2"
135+
strokeLinecap="round"
136+
strokeLinejoin="round"
137+
/>
138+
<path
139+
d="M4 7H20"
140+
stroke="currentColor"
141+
strokeWidth="2"
142+
strokeLinecap="round"
143+
strokeLinejoin="round"
144+
/>
145+
<path
146+
d="M6 7H12H18V18C18 19.6569 16.6569 21 15 21H9C7.34315 21 6 19.6569 6 18V7Z"
147+
stroke="currentColor"
148+
strokeWidth="2"
149+
strokeLinecap="round"
150+
strokeLinejoin="round"
151+
/>
152+
<path
153+
d="M9 5C9 3.89543 9.89543 3 11 3H13C14.1046 3 15 3.89543 15 5V7H9V5Z"
154+
stroke="currentColor"
155+
strokeWidth="2"
156+
strokeLinecap="round"
157+
strokeLinejoin="round"
158+
/>
159+
</svg>
160+
削除
161+
</button>
162+
</div>
163+
<div className="divider" />
164+
{messagesAndDiffs.map((msg, index) =>
165+
msg.type === "message" ? (
166+
msg.role === "user" ? (
167+
<div key={index} className="chat chat-end">
168+
<div
169+
className="chat-bubble p-0.5! bg-secondary/30"
170+
style={{ maxWidth: "100%", wordBreak: "break-word" }}
171+
>
172+
<StyledMarkdown content={msg.content} />
173+
</div>
174+
</div>
175+
) : msg.role === "ai" ? (
176+
<div key={index} className="">
177+
<StyledMarkdown content={msg.content} />
178+
</div>
179+
) : (
180+
<div key={index} className="text-error">
181+
{msg.content}
182+
</div>
183+
)
184+
) : (
185+
<div
186+
key={index}
187+
className={clsx(
188+
"bg-base-300 rounded-lg border border-2 border-secondary/50"
189+
)}
190+
>
191+
{/* pb-0だとmargin collapsingが起きて変な隙間が空く */}
192+
<del
193+
className={clsx(
194+
"block p-2 pb-[1px] bg-error/10",
195+
"line-through decoration-[color-mix(in_oklab,var(--color-error)_70%,currentColor)]"
196+
)}
197+
>
198+
<StyledMarkdown content={msg.search} />
199+
</del>
200+
<ins className="block no-underline p-2 pt-[1px] bg-success/10">
201+
<StyledMarkdown content={msg.replace} />
202+
</ins>
203+
</div>
204+
)
205+
)}
206+
</>
207+
);
208+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { ChatAreaContainer } from "./chatArea";
2+
3+
export default function Loading() {
4+
return (
5+
<ChatAreaContainer chatId={"loading"}>
6+
<div className="skeleton h-7 w-full mt-2 mb-3">{/* heading2 */}</div>
7+
<div className="skeleton h-5 w-2/4 my-2">{/* breadcrumbs */}</div>
8+
<div className="skeleton h-5 w-35 my-1.5">{/* date */}</div>
9+
<div className="divider" />
10+
<div className="skeleton h-15 ml-auto w-2/3 my-1">{/* chat */}</div>
11+
<div className="skeleton h-40 w-full my-2">{/* <p> */}</div>
12+
<div className="skeleton h-15 ml-auto w-2/3 my-1">{/* chat */}</div>
13+
<div className="skeleton h-40 w-full my-2">{/* <p> */}</div>
14+
</ChatAreaContainer>
15+
);
16+
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import {
2+
cacheKeyForChat,
3+
ChatWithMessages,
4+
getChatOne,
5+
initContext,
6+
} from "@/lib/chatHistory";
7+
import { getMarkdownSections, getPagesList } from "@/lib/docs";
8+
import { ChatAreaContainer, ChatAreaContent } from "./chatArea";
9+
import { unstable_cacheLife, unstable_cacheTag } from "next/cache";
10+
import { isCloudflare } from "@/lib/detectCloudflare";
11+
12+
export default async function ChatPage({
13+
params,
14+
}: {
15+
params: Promise<{ chatId: string }>;
16+
}) {
17+
const { chatId } = await params;
18+
19+
const context = await initContext();
20+
const chatData = await getChatOneFromCache(chatId, context.userId);
21+
22+
if (!chatData) {
23+
// notFound(); だとページ全体が404になってしまう
24+
return (
25+
<ChatAreaContainer chatId={chatId}>
26+
<p>指定されたチャットのデータが見つかりません。</p>
27+
</ChatAreaContainer>
28+
);
29+
}
30+
31+
const pagesList = await getPagesList();
32+
const targetLang = pagesList.find(
33+
(lang) => lang.id === chatData.section.pagePath.split("/")[0]
34+
);
35+
const targetPage = targetLang?.pages.find(
36+
(page) => page.slug === chatData.section.pagePath.split("/")[1]
37+
);
38+
const sections =
39+
targetLang && targetPage
40+
? await getMarkdownSections(targetLang.id, targetPage.slug)
41+
: [];
42+
const targetSection = sections.find((sec) => sec.id === chatData.sectionId);
43+
44+
return (
45+
<ChatAreaContainer chatId={chatId}>
46+
<ChatAreaContent
47+
chatId={chatId}
48+
chatData={chatData}
49+
targetLang={targetLang}
50+
targetPage={targetPage}
51+
targetSection={targetSection}
52+
/>
53+
</ChatAreaContainer>
54+
);
55+
}
56+
57+
async function getChatOneFromCache(chatId: string, userId?: string) {
58+
"use cache";
59+
unstable_cacheLife("days");
60+
unstable_cacheTag(cacheKeyForChat(chatId));
61+
62+
if (!userId) {
63+
return null;
64+
}
65+
66+
if (isCloudflare()) {
67+
const cache = await caches.open("chatHistory");
68+
const cachedResponse = await cache.match(cacheKeyForChat(chatId));
69+
if (cachedResponse) {
70+
const data = (await cachedResponse.json()) as ChatWithMessages;
71+
return data;
72+
}
73+
}
74+
75+
const context = await initContext({ userId });
76+
const chatData = await getChatOne(chatId, context);
77+
return chatData;
78+
}

app/(docs)/@chat/chat/page.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { ChatAreaStateUpdater } from "../../chatAreaState";
2+
3+
// /chat にアクセスしたときチャットを閉じる
4+
5+
export default function EmptyPage() {
6+
return <ChatAreaStateUpdater chatId={null} />;
7+
}

app/(docs)/@chat/default.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { ChatAreaStateUpdater } from "../chatAreaState";
2+
3+
export default function EmptyPage() {
4+
return <ChatAreaStateUpdater chatId={null} />;
5+
}

app/(docs)/@chat/error.tsx

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
"use client"; // Error boundaries must be Client Components
2+
3+
import clsx from "clsx";
4+
import { ChatAreaContainer } from "./chat/[chatId]/chatArea";
5+
6+
export default function Error({
7+
error,
8+
// reset,
9+
}: {
10+
error: Error & { digest?: string };
11+
reset: () => void;
12+
}) {
13+
return (
14+
<ChatAreaContainer chatId={"error"}>
15+
<p>ページの読み込み中にエラーが発生しました。</p>
16+
<pre
17+
className={clsx(
18+
"border-2 border-current/20 mt-4 rounded-box p-4! bg-base-300! text-base-content!",
19+
"max-w-full whitespace-pre-wrap"
20+
)}
21+
>
22+
{error.message}
23+
</pre>
24+
{error.digest && (
25+
<p className="mt-2 text-sm text-base-content/50">
26+
Digest: {error.digest}
27+
</p>
28+
)}
29+
</ChatAreaContainer>
30+
);
31+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
"use client";
2+
3+
import { PagePath } from "@/lib/docs";
4+
import { usePathname, useRouter } from "next/navigation";
5+
import { useEffect } from "react";
6+
7+
export function DocsAutoRedirect(props: { path: PagePath }) {
8+
const pathname = usePathname();
9+
const router = useRouter();
10+
useEffect(() => {
11+
if (pathname === `/chat`) {
12+
router.replace(`/${props.path.lang}/${props.path.page}`, {
13+
scroll: false,
14+
});
15+
}
16+
}, [pathname, router, props.path.lang, props.path.page]);
17+
18+
return null;
19+
}

0 commit comments

Comments
 (0)