-
Notifications
You must be signed in to change notification settings - Fork 1
チャットの表示をparallel routesで実装 #192
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
20 commits
Select commit
Hold shift + click to select a range
b3fd3c9
チャットの表示をparallel routesで実装
na-trium-144 6ff62d9
lib/chatHidtoryからactionとcacheを分離、chatページで実際のチャットを取得
na-trium-144 739d95a
チャットサイドバーの表示を改善
na-trium-144 0aa1904
diffの表示を追加
na-trium-144 84e4980
チャットタイトルの実装、削除ボタン追加など
na-trium-144 b4f8a52
プロンプト調整
na-trium-144 4907897
AIの回答時にそのチャットのページを開く
na-trium-144 398bb07
チャット削除を実装
na-trium-144 842caa5
chatHistoryをcontextで管理するのをやめる & 削除時のcacheクリア
na-trium-144 0792753
チャットにloadingを追加、チャットを閉じた時と初期ロードのリダイレクト追加
na-trium-144 6e6fe7e
チャットリストの見た目を改善
na-trium-144 de28263
レイアウト改善
na-trium-144 41d8512
チャットをキャッシュ
na-trium-144 419f5c7
セクションid空は困る
na-trium-144 44a177c
エラー修正
na-trium-144 2d4f7ae
空コンポーネントをnullに変更
na-trium-144 9846e7e
cache.putを忘れていた
na-trium-144 bc63846
non-null assertionを削除
na-trium-144 6d37d66
未使用のaction
na-trium-144 9c25d19
リダイレクトのエラーハンドリング
na-trium-144 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,208 @@ | ||
| "use client"; | ||
|
|
||
| import { ChatAreaStateUpdater } from "@/(docs)/chatAreaState"; | ||
| import { deleteChatAction } from "@/actions/deleteChat"; | ||
| import { ChatWithMessages } from "@/lib/chatHistory"; | ||
| import { LanguageEntry, MarkdownSection, PageEntry } from "@/lib/docs"; | ||
| import { Heading } from "@/markdown/heading"; | ||
| import { StyledMarkdown } from "@/markdown/markdown"; | ||
| import clsx from "clsx"; | ||
| import Link from "next/link"; | ||
| import { useRouter } from "next/navigation"; | ||
| import { ReactNode } from "react"; | ||
|
|
||
| export function ChatAreaContainer(props: { chatId: string; children: ReactNode }) { | ||
| return ( | ||
| <aside | ||
| className={clsx( | ||
| // モバイルでは全画面表示する | ||
| "fixed inset-0 pt-20 bg-base-100", | ||
| // PCではスクロールで動かない右サイドバー | ||
| "has-chat-1:sticky has-chat-1:top-16 has-sidebar:top-0 has-chat-1:pt-4", | ||
| "has-chat-1:basis-2/5 has-chat-1:max-w-chat-area has-chat-1:h-[calc(100vh-4rem)] has-sidebar:h-screen", | ||
| "has-chat-1:shadow-md has-chat-1:bg-base-200", | ||
| // navbar(z-40)よりは下、ChatListForSectionのdropdown(デフォルトでz-999だがz-30に変えている)よりも上 | ||
| "z-35", | ||
| "p-4", | ||
| "flex flex-col", | ||
| "overflow-y-auto" | ||
| )} | ||
| > | ||
| <ChatAreaStateUpdater chatId={props.chatId} /> | ||
| <div className="flex flex-row items-center"> | ||
| <span className="flex-1 text-base font-bold opacity-40"> | ||
| AIへの質問 | ||
| </span> | ||
| <Link className="btn btn-ghost" href="/chat" scroll={false}> | ||
| <svg | ||
| className="w-8 h-8 -scale-x-100" | ||
| viewBox="0 0 24 24" | ||
| fill="none" | ||
| xmlns="http://www.w3.org/2000/svg" | ||
| > | ||
| <path | ||
| d="M18 17L13 12L18 7M11 17L6 12L11 7" | ||
| stroke="currentColor" | ||
| strokeWidth="1.5" | ||
| strokeLinecap="round" | ||
| strokeLinejoin="round" | ||
| /> | ||
| </svg> | ||
| <span className="text-lg">閉じる</span> | ||
| </Link> | ||
| </div> | ||
| {props.children} | ||
| </aside> | ||
| ); | ||
| } | ||
|
|
||
| interface Props { | ||
| chatId: string; | ||
| chatData: ChatWithMessages; | ||
| targetLang: LanguageEntry | undefined; | ||
| targetPage: PageEntry | undefined; | ||
| targetSection: MarkdownSection | undefined; | ||
| } | ||
| export function ChatAreaContent(props: Props) { | ||
| const { chatId, chatData, targetLang, targetPage, targetSection } = props; | ||
|
|
||
| const messagesAndDiffs = [ | ||
| ...chatData.messages.map((msg) => ({ type: "message" as const, ...msg })), | ||
| ...chatData.diff.map((diff) => ({ type: "diff" as const, ...diff })), | ||
| ]; | ||
| messagesAndDiffs.sort( | ||
| (a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime() | ||
| ); | ||
|
|
||
| const router = useRouter(); | ||
|
|
||
| return ( | ||
| <> | ||
| <Heading level={2} className="mt-2!"> | ||
| {chatData.title} | ||
| </Heading> | ||
| <div className="flex-none breadcrumbs text-sm"> | ||
| <ul className="flex-wrap"> | ||
| <li> | ||
| <Link href={`/${targetLang?.id}/${targetLang?.pages[0].slug}`}> | ||
| {targetLang?.name} | ||
| </Link> | ||
| </li> | ||
| <li> | ||
| <Link href={`/${chatData.section.pagePath}`}> | ||
| {targetPage?.index}. {targetPage?.name} | ||
| </Link> | ||
| </li> | ||
| <li> | ||
| <Link href={`/${chatData.section.pagePath}#${chatData.sectionId}`}> | ||
| {targetSection?.title} | ||
| </Link> | ||
| </li> | ||
| </ul> | ||
| </div> | ||
| <div className="flex flex-wrap items-center"> | ||
| <div className="flex-1 text-sm opacity-40" suppressHydrationWarning> | ||
| {chatData.createdAt.toLocaleString()} | ||
| </div> | ||
| <button | ||
| className="btn btn-error btn-soft btn-sm" | ||
| onClick={async () => { | ||
| if (confirm("このチャットを削除してもよろしいですか?")) { | ||
| await deleteChatAction(chatId); | ||
| router.push("/chat", { scroll: false }); | ||
| router.refresh(); | ||
| } | ||
| }} | ||
| > | ||
| {/*<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->*/} | ||
| <svg | ||
| className="w-4 h-4" | ||
| viewBox="0 0 24 24" | ||
| fill="none" | ||
| xmlns="http://www.w3.org/2000/svg" | ||
| > | ||
| <path | ||
| d="M10 11V17" | ||
| stroke="currentColor" | ||
| strokeWidth="2" | ||
| strokeLinecap="round" | ||
| strokeLinejoin="round" | ||
| /> | ||
| <path | ||
| d="M14 11V17" | ||
| stroke="currentColor" | ||
| strokeWidth="2" | ||
| strokeLinecap="round" | ||
| strokeLinejoin="round" | ||
| /> | ||
| <path | ||
| d="M4 7H20" | ||
| stroke="currentColor" | ||
| strokeWidth="2" | ||
| strokeLinecap="round" | ||
| strokeLinejoin="round" | ||
| /> | ||
| <path | ||
| d="M6 7H12H18V18C18 19.6569 16.6569 21 15 21H9C7.34315 21 6 19.6569 6 18V7Z" | ||
| stroke="currentColor" | ||
| strokeWidth="2" | ||
| strokeLinecap="round" | ||
| strokeLinejoin="round" | ||
| /> | ||
| <path | ||
| d="M9 5C9 3.89543 9.89543 3 11 3H13C14.1046 3 15 3.89543 15 5V7H9V5Z" | ||
| stroke="currentColor" | ||
| strokeWidth="2" | ||
| strokeLinecap="round" | ||
| strokeLinejoin="round" | ||
| /> | ||
| </svg> | ||
| 削除 | ||
| </button> | ||
| </div> | ||
| <div className="divider" /> | ||
| {messagesAndDiffs.map((msg, index) => | ||
| msg.type === "message" ? ( | ||
| msg.role === "user" ? ( | ||
| <div key={index} className="chat chat-end"> | ||
| <div | ||
| className="chat-bubble p-0.5! bg-secondary/30" | ||
| style={{ maxWidth: "100%", wordBreak: "break-word" }} | ||
| > | ||
| <StyledMarkdown content={msg.content} /> | ||
| </div> | ||
| </div> | ||
| ) : msg.role === "ai" ? ( | ||
| <div key={index} className=""> | ||
| <StyledMarkdown content={msg.content} /> | ||
| </div> | ||
| ) : ( | ||
| <div key={index} className="text-error"> | ||
| {msg.content} | ||
| </div> | ||
| ) | ||
| ) : ( | ||
| <div | ||
| key={index} | ||
| className={clsx( | ||
| "bg-base-300 rounded-lg border border-2 border-secondary/50" | ||
| )} | ||
| > | ||
| {/* pb-0だとmargin collapsingが起きて変な隙間が空く */} | ||
| <del | ||
| className={clsx( | ||
| "block p-2 pb-[1px] bg-error/10", | ||
| "line-through decoration-[color-mix(in_oklab,var(--color-error)_70%,currentColor)]" | ||
| )} | ||
| > | ||
| <StyledMarkdown content={msg.search} /> | ||
| </del> | ||
| <ins className="block no-underline p-2 pt-[1px] bg-success/10"> | ||
| <StyledMarkdown content={msg.replace} /> | ||
| </ins> | ||
| </div> | ||
| ) | ||
| )} | ||
| </> | ||
| ); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| import { ChatAreaContainer } from "./chatArea"; | ||
|
|
||
| export default function Loading() { | ||
| return ( | ||
| <ChatAreaContainer chatId={"loading"}> | ||
| <div className="skeleton h-7 w-full mt-2 mb-3">{/* heading2 */}</div> | ||
| <div className="skeleton h-5 w-2/4 my-2">{/* breadcrumbs */}</div> | ||
| <div className="skeleton h-5 w-35 my-1.5">{/* date */}</div> | ||
| <div className="divider" /> | ||
| <div className="skeleton h-15 ml-auto w-2/3 my-1">{/* chat */}</div> | ||
| <div className="skeleton h-40 w-full my-2">{/* <p> */}</div> | ||
| <div className="skeleton h-15 ml-auto w-2/3 my-1">{/* chat */}</div> | ||
| <div className="skeleton h-40 w-full my-2">{/* <p> */}</div> | ||
| </ChatAreaContainer> | ||
| ); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,78 @@ | ||
| import { | ||
| cacheKeyForChat, | ||
| ChatWithMessages, | ||
| getChatOne, | ||
| initContext, | ||
| } from "@/lib/chatHistory"; | ||
| import { getMarkdownSections, getPagesList } from "@/lib/docs"; | ||
| import { ChatAreaContainer, ChatAreaContent } from "./chatArea"; | ||
| import { unstable_cacheLife, unstable_cacheTag } from "next/cache"; | ||
| import { isCloudflare } from "@/lib/detectCloudflare"; | ||
|
|
||
| export default async function ChatPage({ | ||
| params, | ||
| }: { | ||
| params: Promise<{ chatId: string }>; | ||
| }) { | ||
| const { chatId } = await params; | ||
|
|
||
| const context = await initContext(); | ||
| const chatData = await getChatOneFromCache(chatId, context.userId); | ||
|
|
||
| if (!chatData) { | ||
| // notFound(); だとページ全体が404になってしまう | ||
| return ( | ||
| <ChatAreaContainer chatId={chatId}> | ||
| <p>指定されたチャットのデータが見つかりません。</p> | ||
| </ChatAreaContainer> | ||
| ); | ||
| } | ||
|
|
||
| const pagesList = await getPagesList(); | ||
| const targetLang = pagesList.find( | ||
| (lang) => lang.id === chatData.section.pagePath.split("/")[0] | ||
| ); | ||
| const targetPage = targetLang?.pages.find( | ||
| (page) => page.slug === chatData.section.pagePath.split("/")[1] | ||
| ); | ||
| const sections = | ||
| targetLang && targetPage | ||
| ? await getMarkdownSections(targetLang.id, targetPage.slug) | ||
| : []; | ||
| const targetSection = sections.find((sec) => sec.id === chatData.sectionId); | ||
|
|
||
| return ( | ||
| <ChatAreaContainer chatId={chatId}> | ||
| <ChatAreaContent | ||
| chatId={chatId} | ||
| chatData={chatData} | ||
| targetLang={targetLang} | ||
| targetPage={targetPage} | ||
| targetSection={targetSection} | ||
| /> | ||
| </ChatAreaContainer> | ||
| ); | ||
| } | ||
|
|
||
| async function getChatOneFromCache(chatId: string, userId?: string) { | ||
| "use cache"; | ||
| unstable_cacheLife("days"); | ||
| unstable_cacheTag(cacheKeyForChat(chatId)); | ||
|
|
||
| if (!userId) { | ||
| return null; | ||
| } | ||
|
|
||
| if (isCloudflare()) { | ||
| const cache = await caches.open("chatHistory"); | ||
| const cachedResponse = await cache.match(cacheKeyForChat(chatId)); | ||
| if (cachedResponse) { | ||
| const data = (await cachedResponse.json()) as ChatWithMessages; | ||
| return data; | ||
| } | ||
| } | ||
|
|
||
| const context = await initContext({ userId }); | ||
| const chatData = await getChatOne(chatId, context); | ||
| return chatData; | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| import { ChatAreaStateUpdater } from "../../chatAreaState"; | ||
|
|
||
| // /chat にアクセスしたときチャットを閉じる | ||
|
|
||
| export default function EmptyPage() { | ||
| return <ChatAreaStateUpdater chatId={null} />; | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| import { ChatAreaStateUpdater } from "../chatAreaState"; | ||
|
|
||
| export default function EmptyPage() { | ||
| return <ChatAreaStateUpdater chatId={null} />; | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| "use client"; // Error boundaries must be Client Components | ||
|
|
||
| import clsx from "clsx"; | ||
| import { ChatAreaContainer } from "./chat/[chatId]/chatArea"; | ||
|
|
||
| export default function Error({ | ||
| error, | ||
| // reset, | ||
| }: { | ||
| error: Error & { digest?: string }; | ||
| reset: () => void; | ||
| }) { | ||
| return ( | ||
| <ChatAreaContainer chatId={"error"}> | ||
| <p>ページの読み込み中にエラーが発生しました。</p> | ||
| <pre | ||
| className={clsx( | ||
| "border-2 border-current/20 mt-4 rounded-box p-4! bg-base-300! text-base-content!", | ||
| "max-w-full whitespace-pre-wrap" | ||
| )} | ||
| > | ||
| {error.message} | ||
| </pre> | ||
| {error.digest && ( | ||
| <p className="mt-2 text-sm text-base-content/50"> | ||
| Digest: {error.digest} | ||
| </p> | ||
| )} | ||
| </ChatAreaContainer> | ||
| ); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| "use client"; | ||
|
|
||
| import { PagePath } from "@/lib/docs"; | ||
| import { usePathname, useRouter } from "next/navigation"; | ||
| import { useEffect } from "react"; | ||
|
|
||
| export function DocsAutoRedirect(props: { path: PagePath }) { | ||
| const pathname = usePathname(); | ||
| const router = useRouter(); | ||
| useEffect(() => { | ||
| if (pathname === `/chat`) { | ||
| router.replace(`/${props.path.lang}/${props.path.page}`, { | ||
| scroll: false, | ||
| }); | ||
| } | ||
| }, [pathname, router, props.path.lang, props.path.page]); | ||
|
|
||
| return null; | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.