|
| 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 | +} |
0 commit comments