Skip to content

Commit 34db91f

Browse files
committed
Revert "Revert "Merge pull request #71 from ut-code/auth""
This reverts commit 72fc0f6.
1 parent 72fc0f6 commit 34db91f

21 files changed

+1294
-112
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,3 +42,5 @@ yarn-error.log*
4242
# typescript
4343
*.tsbuildinfo
4444
next-env.d.ts
45+
46+
/app/generated/prisma

README.md

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,31 @@ https://my-code.utcode.net
77
npm ci
88
```
99

10-
ルートディレクトリに .env.local という名前のファイルを作成し、Gemini APIキーを設定してください
10+
ルートディレクトリに .env.local という名前のファイルを作成し、以下の内容を記述
1111
```dotenv
12-
API_KEY="XXXXXXXX"
12+
API_KEY=GeminiAPIキー
13+
BETTER_AUTH_URL=http://localhost:3000
1314
```
1415

16+
prismaの開発環境を起動
17+
(.env にDATABASE_URLが自動的に追加される)
18+
```bash
19+
npx prisma dev
20+
```
21+
別ターミナルで
22+
```bash
23+
npx prisma db push
24+
```
25+
26+
### 本番環境の場合
27+
28+
上記の環境変数以外に、
29+
* BETTER_AUTH_SECRET に任意の文字列
30+
* DATABASE_URL に本番用のPostgreSQLデータベースURL
31+
* GOOGLE_CLIENT_IDとGOOGLE_CLIENT_SECRETにGoogle OAuthのクライアントIDとシークレット https://www.better-auth.com/docs/authentication/google
32+
* GITHUB_CLIENT_IDとGITHUB_CLIENT_SECRETにGitHub OAuthのクライアントIDとシークレット https://www.better-auth.com/docs/authentication/github
33+
34+
1535
## 開発環境
1636

1737
```bash

app/[docs_id]/chatForm.tsx

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
"use client";
22

33
import { useState, FormEvent, useEffect } from "react";
4-
import { askAI } from "@/app/actions/chatActions";
54
import useSWR from "swr";
65
import {
76
getQuestionExample,
@@ -10,7 +9,8 @@ import {
109
import { getLanguageName } from "../pagesList";
1110
import { DynamicMarkdownSection } from "./pageContent";
1211
import { useEmbedContext } from "../terminal/embedContext";
13-
import { ChatMessage, useChatHistoryContext } from "./chatHistory";
12+
import { useChatHistoryContext } from "./chatHistory";
13+
import { askAI } from "@/actions/chatActions";
1414

1515
interface ChatFormProps {
1616
docs_id: string;
@@ -71,8 +71,6 @@ export function ChatForm({
7171
setIsLoading(true);
7272
setErrorMessage(null); // Clear previous error message
7373

74-
const userMessage: ChatMessage = { sender: "user", text: inputValue };
75-
7674
let userQuestion = inputValue;
7775
if (!userQuestion && exampleData) {
7876
// 質問が空欄なら、質問例を使用
@@ -83,19 +81,19 @@ export function ChatForm({
8381

8482
const result = await askAI({
8583
userQuestion,
84+
docsId: docs_id,
8685
documentContent,
8786
sectionContent,
8887
replOutputs,
8988
files,
9089
execResults,
9190
});
9291

93-
if (result.error) {
92+
if (result.error !== null) {
9493
setErrorMessage(result.error);
9594
console.log(result.error);
9695
} else {
97-
const aiMessage: ChatMessage = { sender: "ai", text: result.response };
98-
const chatId = addChat(result.targetSectionId, [userMessage, aiMessage]);
96+
addChat(result.chat);
9997
// TODO: chatIdが指す対象の回答にフォーカス
10098
setInputValue("");
10199
close();

app/[docs_id]/chatHistory.tsx

Lines changed: 19 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
"use client";
22

3+
import { ChatWithMessages } from "@/lib/chatHistory";
34
import {
45
createContext,
56
ReactNode,
@@ -8,15 +9,10 @@ import {
89
useState,
910
} from "react";
1011

11-
export interface ChatMessage {
12-
sender: "user" | "ai" | "error";
13-
text: string;
14-
}
15-
1612
export interface IChatHistoryContext {
17-
chatHistories: Record<string, Record<string, ChatMessage[]>>;
18-
addChat: (sectionId: string, messages: ChatMessage[]) => string;
19-
updateChat: (sectionId: string, chatId: string, message: ChatMessage) => void;
13+
chatHistories: ChatWithMessages[];
14+
addChat: (chat: ChatWithMessages) => void;
15+
// updateChat: (sectionId: string, chatId: string, message: ChatMessage) => void;
2016
}
2117
const ChatHistoryContext = createContext<IChatHistoryContext | null>(null);
2218
export function useChatHistoryContext() {
@@ -29,65 +25,26 @@ export function useChatHistoryContext() {
2925
return context;
3026
}
3127

32-
export function ChatHistoryProvider({ children }: { children: ReactNode }) {
33-
const [chatHistories, setChatHistories] = useState<
34-
Record<string, Record<string, ChatMessage[]>>
35-
>({});
28+
export function ChatHistoryProvider({
29+
children,
30+
initialChatHistories,
31+
}: {
32+
children: ReactNode;
33+
initialChatHistories: ChatWithMessages[];
34+
}) {
35+
const [chatHistories, setChatHistories] =
36+
useState<ChatWithMessages[]>(initialChatHistories);
3637
useEffect(() => {
37-
// Load chat histories from localStorage on mount
38-
const chatHistories: Record<string, Record<string, ChatMessage[]>> = {};
39-
for (let i = 0; i < localStorage.length; i++) {
40-
const key = localStorage.key(i);
41-
if (key && key.startsWith("chat/") && key.split("/").length === 3) {
42-
const savedHistory = localStorage.getItem(key);
43-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
44-
const [_, sectionId, chatId] = key.split("/");
45-
if (savedHistory) {
46-
if (!chatHistories[sectionId]) {
47-
chatHistories[sectionId] = {};
48-
}
49-
chatHistories[sectionId][chatId] = JSON.parse(savedHistory);
50-
}
51-
}
52-
}
53-
setChatHistories(chatHistories);
54-
}, []);
38+
setChatHistories(initialChatHistories);
39+
}, [initialChatHistories]);
5540

56-
const addChat = (sectionId: string, messages: ChatMessage[]): string => {
57-
const chatId = Date.now().toString();
58-
const newChatHistories = { ...chatHistories };
59-
if (!newChatHistories[sectionId]) {
60-
newChatHistories[sectionId] = {};
61-
}
62-
newChatHistories[sectionId][chatId] = messages;
63-
setChatHistories(newChatHistories);
64-
localStorage.setItem(
65-
`chat/${sectionId}/${chatId}`,
66-
JSON.stringify(messages)
67-
);
68-
return chatId;
69-
};
70-
const updateChat = (
71-
sectionId: string,
72-
chatId: string,
73-
message: ChatMessage
74-
) => {
75-
const newChatHistories = { ...chatHistories };
76-
if (newChatHistories[sectionId] && newChatHistories[sectionId][chatId]) {
77-
newChatHistories[sectionId][chatId] = [
78-
...newChatHistories[sectionId][chatId],
79-
message,
80-
];
81-
setChatHistories(newChatHistories);
82-
localStorage.setItem(
83-
`chat/${sectionId}/${chatId}`,
84-
JSON.stringify(newChatHistories[sectionId][chatId])
85-
);
86-
}
41+
const addChat = (chat: ChatWithMessages) => {
42+
// サーバー側で追加された新しいchatをクライアント側にも反映する
43+
setChatHistories([...chatHistories, chat]);
8744
};
8845

8946
return (
90-
<ChatHistoryContext.Provider value={{ chatHistories, addChat, updateChat }}>
47+
<ChatHistoryContext.Provider value={{ chatHistories, addChat }}>
9148
{children}
9249
</ChatHistoryContext.Provider>
9350
);

app/[docs_id]/page.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { MarkdownSection, splitMarkdown } from "./splitMarkdown";
66
import pyodideLock from "pyodide/pyodide-lock.json";
77
import { PageContent } from "./pageContent";
88
import { ChatHistoryProvider } from "./chatHistory";
9+
import { getChat } from "@/lib/chatHistory";
910

1011
export default async function Page({
1112
params,
@@ -44,8 +45,10 @@ export default async function Page({
4445

4546
const splitMdContent: MarkdownSection[] = splitMarkdown(mdContent);
4647

48+
const initialChatHistories = await getChat(docs_id);
49+
4750
return (
48-
<ChatHistoryProvider>
51+
<ChatHistoryProvider initialChatHistories={initialChatHistories}>
4952
<PageContent
5053
documentContent={mdContent}
5154
splitMdContent={splitMdContent}

app/[docs_id]/pageContent.tsx

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -94,8 +94,8 @@ export function PageContent(props: PageContentProps) {
9494
</div>
9595
<div key={`${index}-chat`}>
9696
{/* 右側に表示するチャット履歴欄 */}
97-
{Object.entries(chatHistories[section.sectionId] ?? {}).map(
98-
([chatId, messages]) => (
97+
{chatHistories.filter((c) => c.sectionId === section.sectionId).map(
98+
({chatId, messages}) => (
9999
<div
100100
key={chatId}
101101
className="max-w-xs mb-2 p-2 text-sm border border-base-content/10 rounded-sm shadow-sm bg-base-100"
@@ -104,20 +104,20 @@ export function PageContent(props: PageContentProps) {
104104
{messages.map((msg, index) => (
105105
<div
106106
key={index}
107-
className={`chat ${msg.sender === "user" ? "chat-end" : "chat-start"}`}
107+
className={`chat ${msg.role === "user" ? "chat-end" : "chat-start"}`}
108108
>
109109
<div
110110
className={clsx(
111111
"chat-bubble p-1!",
112-
msg.sender === "user" &&
112+
msg.role === "user" &&
113113
"bg-primary text-primary-content",
114-
msg.sender === "ai" &&
114+
msg.role === "ai" &&
115115
"bg-secondary-content dark:bg-neutral text-black dark:text-white",
116-
msg.sender === "error" && "chat-bubble-error"
116+
msg.role === "error" && "chat-bubble-error"
117117
)}
118118
style={{ maxWidth: "100%", wordBreak: "break-word" }}
119119
>
120-
<StyledMarkdown content={msg.text} />
120+
<StyledMarkdown content={msg.content} />
121121
</div>
122122
</div>
123123
))}

app/accountMenu.tsx

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
"use client";
2+
3+
import { authClient } from "@/lib/auth-client";
4+
import { usePathname } from "next/navigation";
5+
import { useEffect } from "react";
6+
7+
export function AutoAnonymousLogin() {
8+
const { data: session, isPending } = authClient.useSession();
9+
useEffect(() => {
10+
if (!isPending && !session) {
11+
authClient.signIn.anonymous();
12+
}
13+
}, [isPending, session]);
14+
15+
return null;
16+
}
17+
18+
export function AccountMenu() {
19+
const { data: session, isPending } = authClient.useSession();
20+
const pathname = usePathname();
21+
22+
const signout = () => {
23+
if (
24+
window.confirm(
25+
"ログアウトしますか?\nチャット履歴はこの端末上で見られなくなりますが、再度ログインすることでアクセスできます。"
26+
)
27+
) {
28+
authClient.signOut({
29+
fetchOptions: {
30+
onSuccess: () => window.location.reload(),
31+
},
32+
});
33+
}
34+
};
35+
const signoutFromAnonymous = () => {
36+
if (window.confirm("チャット履歴は削除され、アクセスできなくなります。")) {
37+
authClient.signOut({
38+
fetchOptions: {
39+
onSuccess: () => window.location.reload(),
40+
},
41+
});
42+
}
43+
};
44+
45+
if (isPending) {
46+
return <div className="w-10 h-10 skeleton rounded-full"></div>;
47+
}
48+
49+
if (session && !session.user.isAnonymous) {
50+
return (
51+
<div className="dropdown dropdown-end">
52+
<label tabIndex={0} className="btn btn-ghost btn-circle avatar">
53+
<div className="w-8 h-8 rounded-full">
54+
<img
55+
src={
56+
session.user?.image ??
57+
`https://avatar.vercel.sh/${session.user?.name}`
58+
}
59+
alt="user avatar"
60+
/>
61+
</div>
62+
</label>
63+
<ul
64+
tabIndex={0}
65+
className="mt-3 z-[1] p-2 shadow menu menu-sm dropdown-content bg-base-100 rounded-box w-52"
66+
>
67+
<li>
68+
<a onClick={signout}>ログアウト</a>
69+
</li>
70+
</ul>
71+
</div>
72+
);
73+
}
74+
75+
return (
76+
<div className="dropdown dropdown-end">
77+
<label tabIndex={0} className="btn btn-ghost btn-circle avatar">
78+
<div className="w-8 h-8 rounded-full">
79+
<svg
80+
xmlns="http://www.w3.org/2000/svg"
81+
fill="none"
82+
viewBox="0 0 24 24"
83+
strokeWidth={1.5}
84+
stroke="currentColor"
85+
className="w-full h-full"
86+
>
87+
<path
88+
strokeLinecap="round"
89+
strokeLinejoin="round"
90+
d="M17.982 18.725A7.488 7.488 0 0 0 12 15.75a7.488 7.488 0 0 0-5.982 2.975m11.963 0a9 9 0 1 0-11.963 0m11.963 0A8.966 8.966 0 0 1 12 21a8.966 8.966 0 0 1-5.982-2.275M15 9.75a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z"
91+
/>
92+
</svg>
93+
</div>
94+
</label>
95+
<ul className="z-1 shadow-md dropdown-content bg-base-100 rounded-box menu w-64">
96+
<li className="menu-title">
97+
ログインすると、チャット履歴を保存し別のデバイスからもアクセスできるようになります。
98+
</li>
99+
<li>
100+
<button
101+
onClick={() =>
102+
authClient.signIn.social({
103+
provider: "github",
104+
callbackURL: pathname,
105+
})
106+
}
107+
>
108+
GitHub でログイン
109+
</button>
110+
</li>
111+
<li>
112+
<button
113+
onClick={() =>
114+
authClient.signIn.social({
115+
provider: "google",
116+
callbackURL: pathname,
117+
})
118+
}
119+
>
120+
Google でログイン
121+
</button>
122+
</li>
123+
{session?.user && (
124+
<>
125+
<div className="divider my-0" />
126+
<li>
127+
<button onClick={signoutFromAnonymous}>
128+
この端末上のデータを削除
129+
</button>
130+
</li>
131+
</>
132+
)}
133+
</ul>
134+
</div>
135+
);
136+
}

0 commit comments

Comments
 (0)