Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 29 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,30 @@
https://my-code.utcode.net

## インストール

```bash
npm ci
```

ルートディレクトリに .env.local という名前のファイルを作成し、Gemini APIキーを設定してください
## 開発環境

```bash
npx prisma dev
```
を実行し、`t` キーを押して表示される DATABASE_URL をコピー

ルートディレクトリに .env.local という名前のファイルを作成し、以下の内容を記述
```dotenv
API_KEY="XXXXXXXX"
API_KEY=GeminiAPIキー
BETTER_AUTH_URL=http://localhost:3000
DATABASE_URL="postgres://... (prisma devの出力)"
```

## 開発環境
別のターミナルで、
```bash
npx drizzle-kit migrate
```
でデータベースを初期化

```bash
npm run dev
Expand All @@ -29,6 +43,18 @@ npm run lint
```
でコードをチェックします。出てくるwarningやerrorはできるだけ直しましょう。

* データベースのスキーマ(./app/schema/hoge.ts)を編集した場合、 `npx drizzle-kit generate` でmigrationファイルを作成し、 `npx drizzle-kit migrate` でデータベースに反映します。
* また、mainにマージする際に本番環境のデータベースにもmigrateをする必要があります
* スキーマのファイルを追加した場合は app/lib/drizzle.ts でimportを追加する必要があります(たぶん)
* `npx prisma dev` で立ち上げたデータベースは `npx prisma dev ls` でデータベース名の確認・ `npx prisma dev rm default` で削除ができるらしい

### 本番環境の場合

上記の環境変数以外に、
* BETTER_AUTH_SECRET に任意の文字列
* 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

## markdown仕様

````
Expand Down
12 changes: 5 additions & 7 deletions app/[docs_id]/chatForm.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
"use client";

import { useState, FormEvent, useEffect } from "react";
import { askAI } from "@/app/actions/chatActions";
import useSWR from "swr";
import {
getQuestionExample,
Expand All @@ -10,7 +9,8 @@ import {
import { getLanguageName } from "../pagesList";
import { DynamicMarkdownSection } from "./pageContent";
import { useEmbedContext } from "../terminal/embedContext";
import { ChatMessage, useChatHistoryContext } from "./chatHistory";
import { useChatHistoryContext } from "./chatHistory";
import { askAI } from "@/actions/chatActions";

interface ChatFormProps {
docs_id: string;
Expand Down Expand Up @@ -71,8 +71,6 @@ export function ChatForm({
setIsLoading(true);
setErrorMessage(null); // Clear previous error message

const userMessage: ChatMessage = { sender: "user", text: inputValue };

let userQuestion = inputValue;
if (!userQuestion && exampleData) {
// 質問が空欄なら、質問例を使用
Expand All @@ -83,19 +81,19 @@ export function ChatForm({

const result = await askAI({
userQuestion,
docsId: docs_id,
documentContent,
sectionContent,
replOutputs,
files,
execResults,
});

if (result.error) {
if (result.error !== null) {
setErrorMessage(result.error);
console.log(result.error);
} else {
const aiMessage: ChatMessage = { sender: "ai", text: result.response };
const chatId = addChat(result.targetSectionId, [userMessage, aiMessage]);
addChat(result.chat);
// TODO: chatIdが指す対象の回答にフォーカス
setInputValue("");
close();
Expand Down
81 changes: 19 additions & 62 deletions app/[docs_id]/chatHistory.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"use client";

import { ChatWithMessages } from "@/lib/chatHistory";
import {
createContext,
ReactNode,
Expand All @@ -8,15 +9,10 @@ import {
useState,
} from "react";

export interface ChatMessage {
sender: "user" | "ai" | "error";
text: string;
}

export interface IChatHistoryContext {
chatHistories: Record<string, Record<string, ChatMessage[]>>;
addChat: (sectionId: string, messages: ChatMessage[]) => string;
updateChat: (sectionId: string, chatId: string, message: ChatMessage) => void;
chatHistories: ChatWithMessages[];
addChat: (chat: ChatWithMessages) => void;
// updateChat: (sectionId: string, chatId: string, message: ChatMessage) => void;
}
const ChatHistoryContext = createContext<IChatHistoryContext | null>(null);
export function useChatHistoryContext() {
Expand All @@ -29,65 +25,26 @@ export function useChatHistoryContext() {
return context;
}

export function ChatHistoryProvider({ children }: { children: ReactNode }) {
const [chatHistories, setChatHistories] = useState<
Record<string, Record<string, ChatMessage[]>>
>({});
export function ChatHistoryProvider({
children,
initialChatHistories,
}: {
children: ReactNode;
initialChatHistories: ChatWithMessages[];
}) {
const [chatHistories, setChatHistories] =
useState<ChatWithMessages[]>(initialChatHistories);
useEffect(() => {
// Load chat histories from localStorage on mount
const chatHistories: Record<string, Record<string, ChatMessage[]>> = {};
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);
}, []);
setChatHistories(initialChatHistories);
}, [initialChatHistories]);

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])
);
}
const addChat = (chat: ChatWithMessages) => {
// サーバー側で追加された新しいchatをクライアント側にも反映する
setChatHistories([...chatHistories, chat]);
};

return (
<ChatHistoryContext.Provider value={{ chatHistories, addChat, updateChat }}>
<ChatHistoryContext.Provider value={{ chatHistories, addChat }}>
{children}
</ChatHistoryContext.Provider>
);
Expand Down
5 changes: 4 additions & 1 deletion app/[docs_id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ 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,
Expand Down Expand Up @@ -44,8 +45,10 @@ export default async function Page({

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

const initialChatHistories = await getChat(docs_id);

return (
<ChatHistoryProvider>
<ChatHistoryProvider initialChatHistories={initialChatHistories}>
<PageContent
documentContent={mdContent}
splitMdContent={splitMdContent}
Expand Down
14 changes: 7 additions & 7 deletions app/[docs_id]/pageContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -94,8 +94,8 @@ export function PageContent(props: PageContentProps) {
</div>
<div key={`${index}-chat`}>
{/* 右側に表示するチャット履歴欄 */}
{Object.entries(chatHistories[section.sectionId] ?? {}).map(
([chatId, messages]) => (
{chatHistories.filter((c) => c.sectionId === section.sectionId).map(
({chatId, messages}) => (
<div
key={chatId}
className="max-w-xs mb-2 p-2 text-sm border border-base-content/10 rounded-sm shadow-sm bg-base-100"
Expand All @@ -104,20 +104,20 @@ export function PageContent(props: PageContentProps) {
{messages.map((msg, index) => (
<div
key={index}
className={`chat ${msg.sender === "user" ? "chat-end" : "chat-start"}`}
className={`chat ${msg.role === "user" ? "chat-end" : "chat-start"}`}
>
<div
className={clsx(
"chat-bubble p-1!",
msg.sender === "user" &&
msg.role === "user" &&
"bg-primary text-primary-content",
msg.sender === "ai" &&
msg.role === "ai" &&
"bg-secondary-content dark:bg-neutral text-black dark:text-white",
msg.sender === "error" && "chat-bubble-error"
msg.role === "error" && "chat-bubble-error"
)}
style={{ maxWidth: "100%", wordBreak: "break-word" }}
>
<StyledMarkdown content={msg.text} />
<StyledMarkdown content={msg.content} />
</div>
</div>
))}
Expand Down
Loading