diff --git a/.env b/.env index c0236a9..f2bcafa 100644 --- a/.env +++ b/.env @@ -4,4 +4,5 @@ VITE_FIREBASE_PROJECT_ID="infox-dc968" VITE_FIREBASE_STORAGE_BUCKET="infox-dc968.appspot.com" VITE_FIREBASE_MESSAGING_SENDER_ID="82111709736" VITE_FIREBASE_APP_ID="1:82111709736:web:0c353713d0508816a16b1d" -VITE_FIREBASE_MEASUREMENT_ID="G-6SRSWCR5D5" \ No newline at end of file +VITE_FIREBASE_MEASUREMENT_ID="G-6SRSWCR5D5" +OPENAI_API_KEY="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" \ No newline at end of file diff --git a/README.md b/README.md index 49bf1b2..e69de29 100644 --- a/README.md +++ b/README.md @@ -1,16 +0,0 @@ -# 実現したこと - -Vite + React(TypeScript) + Firebase の構成で以下の機能を持つアプリを作成し、Firebase にホスティングした。GitHub へ Push 時に Firebase にホスティングするようワークフローを設定している。 - -- サインイン、サインアウト -- メモの一覧表示 -- メモの登録、更新 -- メモの削除 - -# 注意事項 - -- .env ファイルは[Vite + React + Firebase のハンズオン](https://qiita.com/Inp/items/906100b46fcbda6fb2ee)等を参考に別途作成する必要あり - -# 詳細 - -[React(TypeScript) + Firebase でメモアプリ開発](https://zenn.dev/shoji9x9/articles/eb185b3d66567b)参照。 diff --git a/package.json b/package.json index e241da9..d090175 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "create-vite": "^5.0.0", "dompurify": "^3.0.8", "firebase": "^10.4.0", + "openai": "^4.0.0", "react": "^18.2.0", "react-beautiful-dnd": "^13.1.1", "react-dnd": "^14.0.2", diff --git a/src/App.tsx b/src/App.tsx index 08d86cd..fd7cd27 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -11,6 +11,7 @@ import MenuIcon from "@mui/icons-material/Menu"; import { IconButton } from "@mui/material"; import { useState } from "react"; import { ViewMemo } from "./services/ViewMemo"; +import {APIKeyPage} from "./pages/APIKeyPage"; function App() { @@ -33,6 +34,7 @@ function App() { } /> } /> } /> + } /> diff --git a/src/pages/APIKeyPage.tsx b/src/pages/APIKeyPage.tsx new file mode 100644 index 0000000..d2864f3 --- /dev/null +++ b/src/pages/APIKeyPage.tsx @@ -0,0 +1,62 @@ +import { useState, ChangeEvent, FormEvent, useEffect } from 'react'; +import { Button, TextField, Typography } from "@mui/material"; +import { useRecoilState } from 'recoil'; +import { userAtom } from "../states/userAtom"; +import { useNavigate } from 'react-router-dom'; + +export function APIKeyPage(): JSX.Element { + const [apiKey, setApiKey] = useState(''); + const [user, setUser] = useRecoilState(userAtom); + const navigate = useNavigate(); + + useEffect(() => { + if (user?.apiKey) { + setApiKey(user.apiKey); + } + }, [user]); + + const handleSubmit = (event: FormEvent) => { + event.preventDefault(); + setUser({ ...user, apiKey: apiKey }); + navigate('/memolist'); + }; + + const handleDelete = () => { + setApiKey(''); + setUser({ ...user, apiKey: '' }); + }; + + return ( +
+ + API Keyを入力してください。 + + {user?.apiKey && ( + <> + + 登録済みです + + + + )} + {!user?.apiKey && ( +
+ ) => setApiKey(e.target.value)} + fullWidth + margin="normal" + /> + + + )} +
+ ); +} + +export default APIKeyPage; \ No newline at end of file diff --git a/src/pages/Header.tsx b/src/pages/Header.tsx index 98b97bf..9519a0c 100644 --- a/src/pages/Header.tsx +++ b/src/pages/Header.tsx @@ -17,6 +17,7 @@ export function Header(): JSX.Element { return { userId: null, userName: null, + apiKey: null, }; }); clearUserInLocalStorage(); diff --git a/src/pages/Memo.tsx b/src/pages/Memo.tsx index 0fecc43..3233f6e 100644 --- a/src/pages/Memo.tsx +++ b/src/pages/Memo.tsx @@ -11,6 +11,8 @@ import 'react-quill/dist/quill.snow.css'; import './toolbar.css'; import { useEffect, useState } from 'react';//suzu import { WithContext as ReactTags } from 'react-tag-input';//suzu +import { OpenAI } from "openai"; + export function Memo(): JSX.Element { @@ -21,11 +23,15 @@ export function Memo(): JSX.Element { const [titleError, setTitleError] = useState(false); const [content, setContent] = useState(""); - - // タグ関連の状態とイベントハンドラー - const [tags, setTags] = useState([ - { id: '1', text: 'タグなし' } - ]); + let openai: OpenAI; + if (loginUser?.apiKey) { + openai = new OpenAI({ + apiKey: loginUser.apiKey, + dangerouslyAllowBrowser: true + }); + } + + const [tags, setTags] = useState([]); interface Tag { id: string; @@ -58,28 +64,57 @@ export function Memo(): JSX.Element { navigate("/memolist"); }; + const generateTags = async (content: string): Promise => { + const gptResponse = await openai.chat.completions.create({ + model: 'gpt-3.5-turbo', + messages: [{"role": "user", "content": "与えられたテキストから適切なハッシュタグを生成してください。テキストの主要なトピックやキーワードを考慮し、関連性の高いタグを提案してください。"}, {"role": "user", "content": content}], + temperature: 0.5, + max_tokens: 60, + }); + + const messageContent = gptResponse.choices[0].message.content; + if (messageContent === null) { + return []; + } + const tags = messageContent.split(" ").map((tag, index) => { + return { id: index.toString(), text: tag }; + }); + return tags; + }; + const save = async () => { if (!title) { setTitleError(true); return; } const updatedAt = new Date(); - let memoCreatedAt = createdAt; + const memoCreatedAt = createdAt; if (!id && !createdAt) { setCreatedAt(updatedAt); } if (memoCreatedAt) { try { - //await saveMemo({ id, title, content, updatedAt, createdAt: createdAt || updatedAt }, loginUser); - await saveMemo({ id, title, content, tags, updatedAt, createdAt: memoCreatedAt }, loginUser); + if (!loginUser.apiKey) { + await saveMemo({ id, title, content, tags:[], updatedAt, createdAt: memoCreatedAt }, loginUser); + setMessageAtom((prev) => ({ + ...prev, + ...successMessage("Saved"), + })); + navigate("/memolist"); + return; + } + else { + const generatedTags = await generateTags(content); + setTags(generatedTags); + + await saveMemo({ id, title, content, tags: generatedTags, updatedAt, createdAt: memoCreatedAt }, loginUser); setMessageAtom((prev) => ({ ...prev, ...successMessage("Saved"), })); navigate("/memolist"); - //backToMemoList(); - } catch (e) { + }} catch (e) { setMessageAtom((prev) => ({ ...prev, ...exceptionMessage(), @@ -87,7 +122,7 @@ export function Memo(): JSX.Element { } } else { // createdAt が null の場合のエラーハンドリング - console.error("createdAt is null"); + setCreatedAt(new Date()); } }; @@ -116,6 +151,7 @@ export function Memo(): JSX.Element { get(); }, [id, loginUser, setMessageAtom]); + return ( <> diff --git a/src/pages/Sidebar.tsx b/src/pages/Sidebar.tsx index acd3bcf..48b2722 100644 --- a/src/pages/Sidebar.tsx +++ b/src/pages/Sidebar.tsx @@ -30,6 +30,7 @@ const Sidebar: React.FC = ({ isOpen, onClose }) => { setUserAtom({ userId: null, userName: null, + apiKey: null, }); clearUserInLocalStorage(); navigate("/"); @@ -66,6 +67,10 @@ const Sidebar: React.FC = ({ isOpen, onClose }) => { + + + + ) : ( <> @@ -77,7 +82,6 @@ const Sidebar: React.FC = ({ isOpen, onClose }) => { アプリの機能を利用するにはサインインしてください。 - {/* 他のヘルプメニューアイテムを追加する場合はここに追加 */} )} diff --git a/src/states/userAtom.ts b/src/states/userAtom.ts index 1519472..8ff543a 100644 --- a/src/states/userAtom.ts +++ b/src/states/userAtom.ts @@ -3,6 +3,7 @@ import { atom } from "recoil"; export type LoginUser = { userId: string | null; userName: string | null; + apiKey: string | null; }; export const userAtom = atom({ @@ -10,5 +11,6 @@ export const userAtom = atom({ default: { userId: localStorage.getItem("userId") || null, userName: localStorage.getItem("userName") || null, + apiKey: localStorage.getItem("apiKey") || null, } as LoginUser, }); diff --git a/src/utils/auth.ts b/src/utils/auth.ts index 3e5a633..f976452 100644 --- a/src/utils/auth.ts +++ b/src/utils/auth.ts @@ -23,10 +23,12 @@ export function setUserToLocalStorage(user?: User) { if (user?.uid) { localStorage.setItem("userId", user.uid); localStorage.setItem("userName", user.displayName || ""); + localStorage.setItem("apiKey", user.apiKey || ""); } } export function clearUserInLocalStorage() { localStorage.removeItem("userId"); localStorage.removeItem("userName"); + localStorage.removeItem("apiKey"); }