From 72fc0f6ae1eaade198ec9e7e73d103c5fbf288d5 Mon Sep 17 00:00:00 2001 From: na-trium-144 <100704180+na-trium-144@users.noreply.github.com> Date: Sun, 26 Oct 2025 04:13:52 +0900 Subject: [PATCH 001/211] Revert "Merge pull request #71 from ut-code/auth" This reverts commit b74201a4ab3ed2305404526830f44daeb1e3e594, reversing changes made to d8ce0f4ee3fd78a3845ef9cbccfd9b2bae8e1631. --- .gitignore | 2 - README.md | 24 +- app/[docs_id]/chatForm.tsx | 12 +- app/[docs_id]/chatHistory.tsx | 81 +++- app/[docs_id]/page.tsx | 5 +- app/[docs_id]/pageContent.tsx | 14 +- app/accountMenu.tsx | 136 ------ app/actions/chatActions.ts | 40 +- app/api/auth/[...all]/route.ts | 4 - app/layout.tsx | 2 - app/lib/auth-client.ts | 7 - app/lib/auth.ts | 38 -- app/lib/chatHistory.ts | 72 --- app/lib/prisma.ts | 5 - app/navbar.tsx | 2 - app/sidebar.tsx | 4 +- package-lock.json | 830 +-------------------------------- package.json | 12 +- prisma.config.ts | 13 - prisma/schema.prisma | 101 ---- tsconfig.json | 2 +- 21 files changed, 112 insertions(+), 1294 deletions(-) delete mode 100644 app/accountMenu.tsx delete mode 100644 app/api/auth/[...all]/route.ts delete mode 100644 app/lib/auth-client.ts delete mode 100644 app/lib/auth.ts delete mode 100644 app/lib/chatHistory.ts delete mode 100644 app/lib/prisma.ts delete mode 100644 prisma.config.ts delete mode 100644 prisma/schema.prisma diff --git a/.gitignore b/.gitignore index 485c203..efb8a61 100644 --- a/.gitignore +++ b/.gitignore @@ -42,5 +42,3 @@ yarn-error.log* # typescript *.tsbuildinfo next-env.d.ts - -/app/generated/prisma diff --git a/README.md b/README.md index d58b8a2..b4246e8 100644 --- a/README.md +++ b/README.md @@ -7,31 +7,11 @@ https://my-code.utcode.net npm ci ``` -ルートディレクトリに .env.local という名前のファイルを作成し、以下の内容を記述 +ルートディレクトリに .env.local という名前のファイルを作成し、Gemini APIキーを設定してください ```dotenv -API_KEY=GeminiAPIキー -BETTER_AUTH_URL=http://localhost:3000 +API_KEY="XXXXXXXX" ``` -prismaの開発環境を起動 -(.env にDATABASE_URLが自動的に追加される) -```bash -npx prisma dev -``` -別ターミナルで -```bash -npx prisma db push -``` - -### 本番環境の場合 - -上記の環境変数以外に、 -* BETTER_AUTH_SECRET に任意の文字列 -* DATABASE_URL に本番用のPostgreSQLデータベースURL -* GOOGLE_CLIENT_IDとGOOGLE_CLIENT_SECRETにGoogle OAuthのクライアントIDとシークレット https://www.better-auth.com/docs/authentication/google -* GITHUB_CLIENT_IDとGITHUB_CLIENT_SECRETにGitHub OAuthのクライアントIDとシークレット https://www.better-auth.com/docs/authentication/github - - ## 開発環境 ```bash diff --git a/app/[docs_id]/chatForm.tsx b/app/[docs_id]/chatForm.tsx index 381e014..932be11 100644 --- a/app/[docs_id]/chatForm.tsx +++ b/app/[docs_id]/chatForm.tsx @@ -1,6 +1,7 @@ "use client"; import { useState, FormEvent, useEffect } from "react"; +import { askAI } from "@/app/actions/chatActions"; import useSWR from "swr"; import { getQuestionExample, @@ -9,8 +10,7 @@ import { import { getLanguageName } from "../pagesList"; import { DynamicMarkdownSection } from "./pageContent"; import { useEmbedContext } from "../terminal/embedContext"; -import { useChatHistoryContext } from "./chatHistory"; -import { askAI } from "@/actions/chatActions"; +import { ChatMessage, useChatHistoryContext } from "./chatHistory"; interface ChatFormProps { docs_id: string; @@ -71,6 +71,8 @@ export function ChatForm({ setIsLoading(true); setErrorMessage(null); // Clear previous error message + const userMessage: ChatMessage = { sender: "user", text: inputValue }; + let userQuestion = inputValue; if (!userQuestion && exampleData) { // 質問が空欄なら、質問例を使用 @@ -81,7 +83,6 @@ export function ChatForm({ const result = await askAI({ userQuestion, - docsId: docs_id, documentContent, sectionContent, replOutputs, @@ -89,11 +90,12 @@ export function ChatForm({ execResults, }); - if (result.error !== null) { + if (result.error) { setErrorMessage(result.error); console.log(result.error); } else { - addChat(result.chat); + const aiMessage: ChatMessage = { sender: "ai", text: result.response }; + const chatId = addChat(result.targetSectionId, [userMessage, aiMessage]); // TODO: chatIdが指す対象の回答にフォーカス setInputValue(""); close(); diff --git a/app/[docs_id]/chatHistory.tsx b/app/[docs_id]/chatHistory.tsx index fc92e5d..65d95ce 100644 --- a/app/[docs_id]/chatHistory.tsx +++ b/app/[docs_id]/chatHistory.tsx @@ -1,6 +1,5 @@ "use client"; -import { ChatWithMessages } from "@/lib/chatHistory"; import { createContext, ReactNode, @@ -9,10 +8,15 @@ import { useState, } from "react"; +export interface ChatMessage { + sender: "user" | "ai" | "error"; + text: string; +} + export interface IChatHistoryContext { - chatHistories: ChatWithMessages[]; - addChat: (chat: ChatWithMessages) => void; - // updateChat: (sectionId: string, chatId: string, message: ChatMessage) => void; + chatHistories: Record>; + addChat: (sectionId: string, messages: ChatMessage[]) => string; + updateChat: (sectionId: string, chatId: string, message: ChatMessage) => void; } const ChatHistoryContext = createContext(null); export function useChatHistoryContext() { @@ -25,26 +29,65 @@ export function useChatHistoryContext() { return context; } -export function ChatHistoryProvider({ - children, - initialChatHistories, -}: { - children: ReactNode; - initialChatHistories: ChatWithMessages[]; -}) { - const [chatHistories, setChatHistories] = - useState(initialChatHistories); +export function ChatHistoryProvider({ children }: { children: ReactNode }) { + const [chatHistories, setChatHistories] = useState< + Record> + >({}); useEffect(() => { - setChatHistories(initialChatHistories); - }, [initialChatHistories]); + // Load chat histories from localStorage on mount + const chatHistories: Record> = {}; + for (let i = 0; i < localStorage.length; i++) { + const key = localStorage.key(i); + if (key && key.startsWith("chat/") && key.split("/").length === 3) { + const savedHistory = localStorage.getItem(key); + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const [_, sectionId, chatId] = key.split("/"); + if (savedHistory) { + if (!chatHistories[sectionId]) { + chatHistories[sectionId] = {}; + } + chatHistories[sectionId][chatId] = JSON.parse(savedHistory); + } + } + } + setChatHistories(chatHistories); + }, []); - const addChat = (chat: ChatWithMessages) => { - // サーバー側で追加された新しいchatをクライアント側にも反映する - setChatHistories([...chatHistories, chat]); + const addChat = (sectionId: string, messages: ChatMessage[]): string => { + const chatId = Date.now().toString(); + const newChatHistories = { ...chatHistories }; + if (!newChatHistories[sectionId]) { + newChatHistories[sectionId] = {}; + } + newChatHistories[sectionId][chatId] = messages; + setChatHistories(newChatHistories); + localStorage.setItem( + `chat/${sectionId}/${chatId}`, + JSON.stringify(messages) + ); + return chatId; + }; + const updateChat = ( + sectionId: string, + chatId: string, + message: ChatMessage + ) => { + const newChatHistories = { ...chatHistories }; + if (newChatHistories[sectionId] && newChatHistories[sectionId][chatId]) { + newChatHistories[sectionId][chatId] = [ + ...newChatHistories[sectionId][chatId], + message, + ]; + setChatHistories(newChatHistories); + localStorage.setItem( + `chat/${sectionId}/${chatId}`, + JSON.stringify(newChatHistories[sectionId][chatId]) + ); + } }; return ( - + {children} ); diff --git a/app/[docs_id]/page.tsx b/app/[docs_id]/page.tsx index 7cf15dc..9c89033 100644 --- a/app/[docs_id]/page.tsx +++ b/app/[docs_id]/page.tsx @@ -6,7 +6,6 @@ import { MarkdownSection, splitMarkdown } from "./splitMarkdown"; import pyodideLock from "pyodide/pyodide-lock.json"; import { PageContent } from "./pageContent"; import { ChatHistoryProvider } from "./chatHistory"; -import { getChat } from "@/lib/chatHistory"; export default async function Page({ params, @@ -45,10 +44,8 @@ export default async function Page({ const splitMdContent: MarkdownSection[] = splitMarkdown(mdContent); - const initialChatHistories = await getChat(docs_id); - return ( - +
{/* 右側に表示するチャット履歴欄 */} - {chatHistories.filter((c) => c.sectionId === section.sectionId).map( - ({chatId, messages}) => ( + {Object.entries(chatHistories[section.sectionId] ?? {}).map( + ([chatId, messages]) => (
(
- +
))} diff --git a/app/accountMenu.tsx b/app/accountMenu.tsx deleted file mode 100644 index c5d0f4c..0000000 --- a/app/accountMenu.tsx +++ /dev/null @@ -1,136 +0,0 @@ -"use client"; - -import { authClient } from "@/lib/auth-client"; -import { usePathname } from "next/navigation"; -import { useEffect } from "react"; - -export function AutoAnonymousLogin() { - const { data: session, isPending } = authClient.useSession(); - useEffect(() => { - if (!isPending && !session) { - authClient.signIn.anonymous(); - } - }, [isPending, session]); - - return null; -} - -export function AccountMenu() { - const { data: session, isPending } = authClient.useSession(); - const pathname = usePathname(); - - const signout = () => { - if ( - window.confirm( - "ログアウトしますか?\nチャット履歴はこの端末上で見られなくなりますが、再度ログインすることでアクセスできます。" - ) - ) { - authClient.signOut({ - fetchOptions: { - onSuccess: () => window.location.reload(), - }, - }); - } - }; - const signoutFromAnonymous = () => { - if (window.confirm("チャット履歴は削除され、アクセスできなくなります。")) { - authClient.signOut({ - fetchOptions: { - onSuccess: () => window.location.reload(), - }, - }); - } - }; - - if (isPending) { - return
; - } - - if (session && !session.user.isAnonymous) { - return ( -
- - -
- ); - } - - return ( -
- -
    -
  • - ログインすると、チャット履歴を保存し別のデバイスからもアクセスできるようになります。 -
  • -
  • - -
  • -
  • - -
  • - {session?.user && ( - <> -
    -
  • - -
  • - - )} -
-
- ); -} diff --git a/app/actions/chatActions.ts b/app/actions/chatActions.ts index 87dfa18..7adf58e 100644 --- a/app/actions/chatActions.ts +++ b/app/actions/chatActions.ts @@ -4,21 +4,15 @@ import { generateContent } from "./gemini"; import { DynamicMarkdownSection } from "../[docs_id]/pageContent"; import { ReplCommand, ReplOutput } from "../terminal/repl"; -import { addChat, ChatWithMessages } from "@/lib/chatHistory"; -type ChatResult = - | { - error: string; - } - | { - error: null; - // サーバー側でデータベースに新しく追加されたチャットデータ - chat: ChatWithMessages; - }; +interface FormState { + response: string; + error: string | null; + targetSectionId: string; +} type ChatParams = { userQuestion: string; - docsId: string; documentContent: string; sectionContent: DynamicMarkdownSection[]; replOutputs: Record; @@ -26,7 +20,7 @@ type ChatParams = { execResults: Record; }; -export async function askAI(params: ChatParams): Promise { +export async function askAI(params: ChatParams): Promise { // const parseResult = ChatSchema.safeParse(params); // if (!parseResult.success) { @@ -147,21 +141,25 @@ export async function askAI(params: ChatParams): Promise { if (!text) { throw new Error("AIからの応答が空でした"); } - // TODO: どのセクションへの回答にするかをAIに決めさせる - const targetSectionId = - sectionContent.find((s) => s.inView)?.sectionId || ""; - const newChat = await addChat(params.docsId, targetSectionId, [ - { role: "user", content: userQuestion }, - { role: "ai", content: text }, - ]); return { + response: text, error: null, - chat: newChat, + // TODO: どのセクションへの回答にするかをAIに決めさせる + targetSectionId: sectionContent.find((s) => s.inView)?.sectionId || "", }; } catch (error: unknown) { console.error("Error calling Generative AI:", error); + if (error instanceof Error) { + return { + response: "", + error: `AIへのリクエスト中にエラーが発生しました: ${error.message}`, + targetSectionId: sectionContent.find((s) => s.inView)?.sectionId || "", + }; + } return { - error: String(error), + response: "", + error: "予期せぬエラーが発生しました。", + targetSectionId: sectionContent.find((s) => s.inView)?.sectionId || "", }; } } diff --git a/app/api/auth/[...all]/route.ts b/app/api/auth/[...all]/route.ts deleted file mode 100644 index fcb1ef0..0000000 --- a/app/api/auth/[...all]/route.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { auth } from "@/lib/auth"; // path to your auth file -import { toNextJsHandler } from "better-auth/next-js"; - -export const { POST, GET } = toNextJsHandler(auth); diff --git a/app/layout.tsx b/app/layout.tsx index 2589c54..365472f 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -8,7 +8,6 @@ import { ReactNode } from "react"; import { PyodideProvider } from "./terminal/python/pyodide"; import { WandboxProvider } from "./terminal/wandbox/wandbox"; import { EmbedContextProvider } from "./terminal/embedContext"; -import { AutoAnonymousLogin } from "./accountMenu"; export const metadata: Metadata = { title: "Create Next App", @@ -21,7 +20,6 @@ export default function RootLayout({ return ( -
diff --git a/app/lib/auth-client.ts b/app/lib/auth-client.ts deleted file mode 100644 index 811590c..0000000 --- a/app/lib/auth-client.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { anonymousClient } from "better-auth/client/plugins"; -import { createAuthClient } from "better-auth/react"; -export const authClient = createAuthClient({ - /** The base URL of the server (optional if you're using the same domain) */ - // baseURL: "http://localhost:3000" - plugins: [anonymousClient()], -}); diff --git a/app/lib/auth.ts b/app/lib/auth.ts deleted file mode 100644 index 1b91e16..0000000 --- a/app/lib/auth.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { betterAuth } from "better-auth"; -import { prismaAdapter } from "better-auth/adapters/prisma"; -import { getCloudflareContext } from "@opennextjs/cloudflare"; -import { anonymous } from "better-auth/plugins"; -import prisma from "./prisma"; -import { migrateChatUser } from "./chatHistory"; - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -let cloudflareEnv: any; -try { - cloudflareEnv = getCloudflareContext().env; -} catch { - // @better-auth/cli generate を実行する際には initOpenNextCloudflareForDev がセットアップされていない環境になっている - cloudflareEnv = {}; -} -export const auth = betterAuth({ - database: prismaAdapter(prisma, { - provider: "postgresql", - }), - plugins: [ - anonymous({ - onLinkAccount: ({ anonymousUser, newUser }) => - migrateChatUser(anonymousUser.user.id, newUser.user.id), - }), - ], - socialProviders: { - github: { - clientId: process.env.GITHUB_CLIENT_ID ?? cloudflareEnv.GITHUB_CLIENT_ID, - clientSecret: - process.env.GITHUB_CLIENT_SECRET ?? cloudflareEnv.GITHUB_CLIENT_SECRET, - }, - google: { - clientId: process.env.GOOGLE_CLIENT_ID ?? cloudflareEnv.GOOGLE_CLIENT_ID, - clientSecret: - process.env.GOOGLE_CLIENT_SECRET ?? cloudflareEnv.GOOGLE_CLIENT_SECRET, - }, - }, -}); diff --git a/app/lib/chatHistory.ts b/app/lib/chatHistory.ts deleted file mode 100644 index a8faf0b..0000000 --- a/app/lib/chatHistory.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { headers } from "next/headers"; -import { auth } from "./auth"; -import prisma from "./prisma"; - -export interface CreateChatMessage { - role: "user" | "ai" | "error"; - content: string; -} - -export async function addChat( - docsId: string, - sectionId: string, - messages: CreateChatMessage[] -) { - const session = await auth.api.getSession({ headers: await headers() }); - if (!session) { - throw new Error("Not authenticated"); - } - - return await prisma.chat.create({ - data: { - userId: session.user.id, - docsId, - sectionId, - messages: { - createMany: { - data: messages, - }, - }, - }, - include: { - messages: true, - }, - }); -} - -export type ChatWithMessages = Awaited>; - -export async function getChat(docsId: string) { - const session = await auth.api.getSession({ headers: await headers() }); - if (!session) { - return []; - } - - return await prisma.chat.findMany({ - where: { - userId: session.user.id, - docsId, - }, - include: { - messages: { - orderBy: { - createdAt: "asc", - }, - }, - }, - orderBy: { - createdAt: "asc", - }, - }); -} - -export async function migrateChatUser(oldUserId: string, newUserId: string) { - await prisma.chat.updateMany({ - where: { - userId: oldUserId, - }, - data: { - userId: newUserId, - }, - }); -} diff --git a/app/lib/prisma.ts b/app/lib/prisma.ts deleted file mode 100644 index 72dd8ef..0000000 --- a/app/lib/prisma.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { PrismaClient } from "../generated/prisma/client"; - -const prisma = new PrismaClient(); -export default prisma; - diff --git a/app/navbar.tsx b/app/navbar.tsx index 2955d59..b599410 100644 --- a/app/navbar.tsx +++ b/app/navbar.tsx @@ -1,5 +1,4 @@ import Link from "next/link"; -import { AccountMenu } from "./accountMenu"; import { ThemeToggle } from "./[docs_id]/themeToggle"; export function Navbar() { return ( @@ -33,7 +32,6 @@ export function Navbar() { {/* サイドバーが常時表示されている場合のみ */} my.code(); -
); diff --git a/app/sidebar.tsx b/app/sidebar.tsx index 1afa75b..8e263ab 100644 --- a/app/sidebar.tsx +++ b/app/sidebar.tsx @@ -4,7 +4,6 @@ import { usePathname } from "next/navigation"; import useSWR, { Fetcher } from "swr"; import { splitMarkdown } from "./[docs_id]/splitMarkdown"; import { pagesList } from "./pagesList"; -import { AccountMenu } from "./accountMenu"; import { ThemeToggle } from "./[docs_id]/themeToggle"; const fetcher: Fetcher = (url) => @@ -21,13 +20,12 @@ export function Sidebar() { return (
{/* todo: 背景色ほんとにこれでいい? */} -

+

{/* サイドバーが常時表示されている場合のみ */} my.code(); -