Skip to content

Commit b3fd3c9

Browse files
committed
チャットの表示をparallel routesで実装
1 parent 448c089 commit b3fd3c9

File tree

15 files changed

+305
-72
lines changed

15 files changed

+305
-72
lines changed
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import { ChatAreaStateUpdater } from "@/(docs)/chatAreaState";
2+
import { StyledMarkdown } from "@/markdown/markdown";
3+
import clsx from "clsx";
4+
import Link from "next/link";
5+
6+
export default async function ChatPage({
7+
params,
8+
}: {
9+
params: Promise<{ chatId: string }>;
10+
}) {
11+
const { chatId } = await params;
12+
13+
// TODO: 実際のchatを取得
14+
const messages = [
15+
{ role: "user", content: "a" },
16+
{ role: "ai", content: "b" },
17+
];
18+
19+
return (
20+
<aside
21+
className={clsx(
22+
// モバイルでは全画面表示する
23+
"fixed inset-0 pt-16 bg-base-100",
24+
// PCではスクロールで動かない右サイドバー
25+
"lg:sticky lg:top-0 lg:pt-0 lg:w-1/3 lg:h-screen lg:shadow-md lg:bg-base-200 ",
26+
"overflow-y-auto"
27+
)}
28+
>
29+
<ChatAreaStateUpdater chatId={chatId} />
30+
{chatId}
31+
<Link className="btn" href="/chat">
32+
閉じる
33+
</Link>
34+
{messages.map((msg, index) => (
35+
<div
36+
key={index}
37+
className={`chat ${msg.role === "user" ? "chat-end" : "chat-start"}`}
38+
>
39+
<div
40+
className={clsx(
41+
msg.role === "user" && "chat-bubble p-0.5! bg-secondary/30",
42+
msg.role === "ai" && "chat-bubble p-0.5!",
43+
msg.role === "error" && "text-error"
44+
)}
45+
style={{ maxWidth: "100%", wordBreak: "break-word" }}
46+
>
47+
<StyledMarkdown content={msg.content} />
48+
</div>
49+
</div>
50+
))}
51+
</aside>
52+
);
53+
}

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+
}
File renamed without changes.
File renamed without changes.
File renamed without changes.

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

Lines changed: 131 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,12 @@ import {
1212
MarkdownSection,
1313
PageEntry,
1414
PagePath,
15+
SectionId,
1516
} from "@/lib/docs";
1617
import { ReplacedRange } from "@/markdown/multiHighlight";
1718
import { Heading } from "@/markdown/heading";
19+
import Link from "next/link";
20+
import { useChatId } from "@/(docs)/chatAreaState";
1821

1922
/**
2023
* MarkdownSectionに追加で、動的な情報を持たせる
@@ -159,71 +162,41 @@ export function PageContent(props: PageContentProps) {
159162
const [isFormVisible, setIsFormVisible] = useState(false);
160163

161164
return (
162-
<div
163-
className="p-4 mx-auto max-w-full grid"
164-
style={{
165-
gridTemplateColumns: `1fr auto`,
166-
}}
167-
>
168-
<Heading level={1}>
169-
{pageEntry.index}章: {pageEntry.title}
170-
</Heading>
171-
<div />
172-
{dynamicMdContent.map((section, index) => (
173-
<Fragment key={section.id}>
174-
<div
175-
className="min-w-1/2 max-w-200 text-justify"
176-
id={section.id} // 目次からaタグで飛ぶために必要
177-
ref={(el) => {
178-
sectionRefs.current[index] = el;
179-
}}
180-
>
181-
{/* ドキュメントのコンテンツ */}
182-
<StyledMarkdown
183-
content={section.replacedContent}
184-
replacedRange={section.replacedRange}
185-
/>
186-
</div>
187-
<div>
188-
{/* 右側に表示するチャット履歴欄 */}
189-
{chatHistories
190-
.filter(
191-
(c) =>
192-
c.sectionId === section.id ||
193-
// 対象のセクションが存在しないものは、introセクション(index=0)にフォールバックする
194-
(index === 0 &&
195-
dynamicMdContent.every((sec) => c.sectionId !== sec.id))
196-
)
197-
.map(({ chatId, messages }) => (
198-
<div
199-
key={chatId}
200-
className="max-w-xs mb-2 p-2 text-sm border border-base-content/10 rounded-sm shadow-sm bg-base-200"
201-
>
202-
<div className="max-h-60 overflow-y-auto">
203-
{messages.map((msg, index) => (
204-
<div
205-
key={index}
206-
className={`chat ${msg.role === "user" ? "chat-end" : "chat-start"}`}
207-
>
208-
<div
209-
className={clsx(
210-
msg.role === "user" &&
211-
"chat-bubble p-0.5! bg-secondary/30",
212-
msg.role === "ai" && "chat-bubble p-0.5!",
213-
msg.role === "error" && "text-error"
214-
)}
215-
style={{ maxWidth: "100%", wordBreak: "break-word" }}
216-
>
217-
<StyledMarkdown content={msg.content} />
218-
</div>
219-
</div>
220-
))}
221-
</div>
222-
</div>
223-
))}
224-
</div>
225-
</Fragment>
226-
))}
165+
<div className="flex-1 p-4 flex flex-col">
166+
<div
167+
className="max-w-full mx-auto grid"
168+
style={{
169+
gridTemplateColumns: `1fr auto`,
170+
}}
171+
>
172+
<Heading className="max-w-200" level={1}>
173+
{pageEntry.index}章: {pageEntry.title}
174+
</Heading>
175+
<div />
176+
{dynamicMdContent.map((section, index) => (
177+
<Fragment key={section.id}>
178+
<div
179+
className="min-w-1/2 max-w-200 text-justify"
180+
id={section.id} // 目次からaタグで飛ぶために必要
181+
ref={(el) => {
182+
sectionRefs.current[index] = el;
183+
}}
184+
>
185+
{/* ドキュメントのコンテンツ */}
186+
<StyledMarkdown
187+
content={section.replacedContent}
188+
replacedRange={section.replacedRange}
189+
/>
190+
</div>
191+
<div>
192+
<ChatListForSection
193+
sectionId={section.id}
194+
dynamicMdContent={dynamicMdContent}
195+
/>
196+
</div>
197+
</Fragment>
198+
))}
199+
</div>
227200
<PageTransition
228201
lang={path.lang}
229202
prevPage={props.prevPage}
@@ -250,3 +223,96 @@ export function PageContent(props: PageContentProps) {
250223
</div>
251224
);
252225
}
226+
227+
function ChatListForSection(props: {
228+
dynamicMdContent: DynamicMarkdownSection[];
229+
sectionId: SectionId;
230+
}) {
231+
const { chatHistories } = useChatHistoryContext();
232+
const { dynamicMdContent, sectionId } = props;
233+
const filteredChatHistories = chatHistories.filter(
234+
(c) =>
235+
c.sectionId === sectionId ||
236+
// 対象のセクションが存在しないものは、introセクション(index=0)にフォールバックする
237+
(dynamicMdContent[0].id === sectionId &&
238+
dynamicMdContent.every((sec) => c.sectionId !== sec.id))
239+
);
240+
241+
const chatId = useChatId();
242+
243+
if (filteredChatHistories.length === 0) {
244+
// チャットがないなら何も表示しない
245+
return null;
246+
}
247+
248+
return (
249+
<>
250+
{/*PC表示かつチャットを表示していない → チャットリストを表示*/}
251+
<ul
252+
className={clsx(
253+
chatId === null ? "hidden lg:block" : "hidden",
254+
"mt-2 ml-4 max-w-60",
255+
"menu menu-sm",
256+
"border border-base-content/10 rounded-sm shadow-sm bg-base-200"
257+
)}
258+
>
259+
<li className="menu-title">チャット</li>
260+
{filteredChatHistories.map(({ chatId }) => (
261+
<li key={chatId} className="">
262+
<Link className="link link-info" href={`/chat/${chatId}`}>
263+
{chatId}
264+
</Link>
265+
</li>
266+
))}
267+
</ul>
268+
{/*PCでない or PC表示でチャットを表示している → 小さい吹き出しを表示*/}
269+
<details
270+
className={clsx(
271+
chatId === null ? "block lg:hidden" : "block",
272+
"dropdown dropdown-end",
273+
"mt-2 ml-2"
274+
)}
275+
>
276+
<summary className="btn btn-outline btn-secondary btn-sm">
277+
{/*<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->*/}
278+
<svg
279+
className="w-4 h-4"
280+
viewBox="3.5 2.5 18 18"
281+
fill="none"
282+
xmlns="http://www.w3.org/2000/svg"
283+
>
284+
<path
285+
fillRule="evenodd"
286+
clipRule="evenodd"
287+
d="M5.5 12C5.49988 14.613 6.95512 17.0085 9.2741 18.2127C11.5931 19.4169 14.3897 19.2292 16.527 17.726L19.5 18V12C19.5 8.13401 16.366 5 12.5 5C8.63401 5 5.5 8.13401 5.5 12Z"
288+
stroke="currentColor"
289+
strokeWidth="1.5"
290+
strokeLinecap="round"
291+
strokeLinejoin="round"
292+
/>
293+
<path
294+
d="M9.5 13.25C9.08579 13.25 8.75 13.5858 8.75 14C8.75 14.4142 9.08579 14.75 9.5 14.75V13.25ZM13.5 14.75C13.9142 14.75 14.25 14.4142 14.25 14C14.25 13.5858 13.9142 13.25 13.5 13.25V14.75ZM9.5 10.25C9.08579 10.25 8.75 10.5858 8.75 11C8.75 11.4142 9.08579 11.75 9.5 11.75V10.25ZM15.5 11.75C15.9142 11.75 16.25 11.4142 16.25 11C16.25 10.5858 15.9142 10.25 15.5 10.25V11.75ZM9.5 14.75H13.5V13.25H9.5V14.75ZM9.5 11.75H15.5V10.25H9.5V11.75Z"
295+
fill="currentColor"
296+
/>
297+
</svg>
298+
{filteredChatHistories.length}
299+
</summary>
300+
<ul
301+
className={clsx(
302+
"menu menu-sm dropdown-content",
303+
"w-max max-w-[75vw]",
304+
"border border-base-content/10 rounded-sm shadow-sm bg-base-200"
305+
)}
306+
>
307+
{filteredChatHistories.map(({ chatId }) => (
308+
<li key={chatId} className="">
309+
<Link className="link link-info" href={`/chat/${chatId}`}>
310+
{chatId}
311+
</Link>
312+
</li>
313+
))}
314+
</ul>
315+
</details>
316+
</>
317+
);
318+
}
File renamed without changes.

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
export default function EmptyPage() {
2+
return (
3+
<div className="flex-1">
4+
TODO: /chat/チャットid
5+
に直接アクセスした場合には、チャットからそれに対応するページidを取得し、そのドキュメントに自動でリダイレクトする。
6+
</div>
7+
);
8+
}

0 commit comments

Comments
 (0)