From 44af5160dc9f3a47e4cb9bc3a5f826daa9dfefc2 Mon Sep 17 00:00:00 2001 From: udara Date: Sat, 11 Nov 2023 15:12:55 -0500 Subject: [PATCH] Add toasts --- src/renderer/App.tsx | 109 +++++++++--------- src/renderer/context/AIContext.js | 17 +-- src/renderer/context/LinksContext.js | 7 ++ src/renderer/context/ToastsContext.js | 85 ++++++++++++++ .../Editor/LinkPreviews/LinkPreview/index.jsx | 56 +++++---- src/renderer/pages/Pile/Editor/index.jsx | 4 +- src/renderer/pages/Pile/Layout.tsx | 13 +-- .../pages/Pile/Posts/Posts.module.scss | 2 +- .../Pile/Toasts/Toast/Thinking/index.jsx | 103 +++++++++++++++++ .../pages/Pile/Toasts/Toast/Toast.module.scss | 18 +++ .../pages/Pile/Toasts/Toast/Waiting/index.jsx | 105 +++++++++++++++++ .../pages/Pile/Toasts/Toast/index.jsx | 21 ++++ .../pages/Pile/Toasts/Toasts.module.scss | 12 ++ src/renderer/pages/Pile/Toasts/index.jsx | 21 ++++ 14 files changed, 462 insertions(+), 111 deletions(-) create mode 100644 src/renderer/context/ToastsContext.js create mode 100644 src/renderer/pages/Pile/Toasts/Toast/Thinking/index.jsx create mode 100644 src/renderer/pages/Pile/Toasts/Toast/Toast.module.scss create mode 100644 src/renderer/pages/Pile/Toasts/Toast/Waiting/index.jsx create mode 100644 src/renderer/pages/Pile/Toasts/Toast/index.jsx create mode 100644 src/renderer/pages/Pile/Toasts/Toasts.module.scss create mode 100644 src/renderer/pages/Pile/Toasts/index.jsx diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx index b08de3b..f44cbec 100644 --- a/src/renderer/App.tsx +++ b/src/renderer/App.tsx @@ -19,6 +19,7 @@ import { TimelineContextProvider } from './context/TimelineContext'; import { AIContextProvider } from './context/AIContext'; import { HighlightsContextProvider } from './context/HighlightsContext'; import { LinksContextProvider } from './context/LinksContext'; +import { ToastsContextProvider } from './context/ToastsContext'; if ('scrollRestoration' in history) { history.scrollRestoration = 'manual'; @@ -49,64 +50,66 @@ export default function App() { return ( - - - - - - - - - - - - } - /> - - - - } - /> - - - - } - /> - - - - } - /> - + + + + + + + + + - + + } /> - - - - - - - - - + + + + } + /> + + + + } + /> + + + + } + /> + + + + + } + /> + + + + + + + + + + ); } diff --git a/src/renderer/context/AIContext.js b/src/renderer/context/AIContext.js index 9cd39cb..d9903aa 100644 --- a/src/renderer/context/AIContext.js +++ b/src/renderer/context/AIContext.js @@ -7,24 +7,11 @@ import { } from 'react'; import OpenAI from 'openai'; -const processKey = (k) => { - if (k.startsWith('unms-')) { - k = k.substring(5); - const reversedStr = k.split('').reverse().join(''); - return 'sk-' + reversedStr; - } - - return k; -}; - export const AIContext = createContext(); export const AIContextProvider = ({ children }) => { const [ai, setAi] = useState(null); - // this keeps track of async tasks that the user is notified about - const [pendingJobs, setPendingJobs] = useState([]); - const prompt = 'You are an AI within a journaling app. Your job is to help the user reflect on their thoughts in a thoughtful and kind manner. The user can never directly address you or directly respond to you. Try not to repeat what the user said, instead try to seed new ideas, encourage or debate. Keep your responses concise, but meaningful.'; @@ -37,10 +24,10 @@ export const AIContextProvider = ({ children }) => { if (!key) return; - const processedKey = processKey(key); const openaiInstance = new OpenAI({ - apiKey: processedKey, + apiKey: key, }); + setAi(openaiInstance); }; diff --git a/src/renderer/context/LinksContext.js b/src/renderer/context/LinksContext.js index 230b8bf..c699c99 100644 --- a/src/renderer/context/LinksContext.js +++ b/src/renderer/context/LinksContext.js @@ -8,11 +8,15 @@ import { import { useLocation } from 'react-router-dom'; import { usePilesContext } from './PilesContext'; import { useAIContext } from './AIContext'; +import { useToastsContext } from './ToastsContext'; + export const LinksContext = createContext(); export const LinksContextProvider = ({ children }) => { const { currentPile, getCurrentPilePath } = usePilesContext(); const { ai, getCompletion } = useAIContext(); + const { addNotification, updateNotification, removeNotification } = + useToastsContext(); const getLink = useCallback( async (url) => { @@ -23,11 +27,14 @@ export const LinksContextProvider = ({ children }) => { url ); + addNotification(url, 'thinking', 'Generating preview...'); + // return cached preview if available if (preview) { return preview; } + updateNotification(url, 'waiting', 'Generating preview...'); // otherwise generate a new preview const _preview = await getPreview(url); const aiCard = await generateMeta(url).catch(() => { diff --git a/src/renderer/context/ToastsContext.js b/src/renderer/context/ToastsContext.js new file mode 100644 index 0000000..c5bb5d5 --- /dev/null +++ b/src/renderer/context/ToastsContext.js @@ -0,0 +1,85 @@ +import { + useState, + createContext, + useContext, + useEffect, + useCallback, + useRef, +} from 'react'; + +export const ToastsContext = createContext(); + +export const ToastsContextProvider = ({ children }) => { + // this keeps track of async tasks that the user is notified about + // by the AI. This can include general app notifications as well. + const [notifications, setNotifications] = useState([]); + const notificationTimeoutsRef = useRef({}); + + useEffect(() => { + return () => { + Object.values(notificationTimeoutsRef.current).forEach(clearTimeout); + }; + }, []); + + const addNotification = ( + targetId, + type = 'info', + message, + autoDismiss = true + ) => { + const newNotification = { id: targetId, type, message, autoDismiss }; + + setNotifications((currentNotifications) => [ + ...currentNotifications, + newNotification, + ]); + + if (autoDismiss) { + if (notificationTimeoutsRef.current[targetId]) { + clearTimeout(notificationTimeoutsRef.current[targetId]); + } + + notificationTimeoutsRef.current[targetId] = setTimeout(() => { + removeNotification(targetId); + delete notificationTimeoutsRef.current[targetId]; + }, 30000); + } + }; + + const updateNotification = (targetId, newType, newMessage) => { + setNotifications((currentNotifications) => + currentNotifications.map((notification) => + notification.id === targetId + ? { ...notification, type: newType, message: newMessage } + : notification + ) + ); + }; + + const removeNotification = (targetId) => { + setNotifications((currentNotifications) => + currentNotifications.filter( + (notification) => notification.id !== targetId + ) + ); + if (notificationTimeoutsRef.current[targetId]) { + clearTimeout(notificationTimeoutsRef.current[targetId]); + delete notificationTimeoutsRef.current[targetId]; + } + }; + + const ToastsContextValue = { + notifications, + addNotification, + updateNotification, + removeNotification, + }; + + return ( + + {children} + + ); +}; + +export const useToastsContext = () => useContext(ToastsContext); diff --git a/src/renderer/pages/Pile/Editor/LinkPreviews/LinkPreview/index.jsx b/src/renderer/pages/Pile/Editor/LinkPreviews/LinkPreview/index.jsx index 6b6d38f..b9c677b 100644 --- a/src/renderer/pages/Pile/Editor/LinkPreviews/LinkPreview/index.jsx +++ b/src/renderer/pages/Pile/Editor/LinkPreviews/LinkPreview/index.jsx @@ -132,36 +132,32 @@ export default function LinkPreview({ url }) { }; return ( - - -
- {renderImage()} -
- - {preview.title} - -
- {renderAICard()} -
- {' '} - {preview?.aiCard?.category && ( - - {preview?.aiCard?.category} - - )} - {preview?.host} -
+ +
+ {renderImage()} + - - + {renderAICard()} +
+ {' '} + {preview?.aiCard?.category && ( + {preview?.aiCard?.category} + )} + {preview?.host} +
+
+
); } diff --git a/src/renderer/pages/Pile/Editor/index.jsx b/src/renderer/pages/Pile/Editor/index.jsx index 7549531..c01d0cc 100644 --- a/src/renderer/pages/Pile/Editor/index.jsx +++ b/src/renderer/pages/Pile/Editor/index.jsx @@ -5,9 +5,9 @@ import { Extension } from '@tiptap/core'; import { useEditor, EditorContent } from '@tiptap/react'; import Link from '@tiptap/extension-link'; import StarterKit from '@tiptap/starter-kit'; -import CharacterCount from '@tiptap/extension-character-count'; import Typography from '@tiptap/extension-typography'; import Placeholder from '@tiptap/extension-placeholder'; +import CharacterCount from '@tiptap/extension-character-count'; import { DiscIcon, PhotoIcon, TrashIcon, TagIcon } from 'renderer/icons'; import { motion, AnimatePresence } from 'framer-motion'; import { postFormat } from 'renderer/utils/fileOperations'; @@ -55,7 +55,7 @@ export default function Editor({ placeholder: isAI ? 'AI is thinking...' : 'What are you thinking?', }), CharacterCount.configure({ - limit: 100000, + limit: 10000, }), ], autofocus: true, diff --git a/src/renderer/pages/Pile/Layout.tsx b/src/renderer/pages/Pile/Layout.tsx index 9324510..16e5cfc 100644 --- a/src/renderer/pages/Pile/Layout.tsx +++ b/src/renderer/pages/Pile/Layout.tsx @@ -2,13 +2,13 @@ import { useParams, Link } from 'react-router-dom'; import styles from './PileLayout.module.scss'; import { HomeIcon } from 'renderer/icons'; import Sidebar from './Sidebar/Timeline/index'; -import { CountUp } from 'use-count-up'; import { useIndexContext } from 'renderer/context/IndexContext'; import { useEffect, useCallback } from 'react'; import { DateTime } from 'luxon'; import Settings from './Settings'; import HighlightsDialog from './Highlights'; import { usePilesContext } from 'renderer/context/PilesContext'; +import Toasts from './Toasts'; export default function PileLayout({ children }) { const { pileName } = useParams(); @@ -41,15 +41,7 @@ export default function PileLayout({ children }) {
- - - {' '} - entries + {index.size} entries
@@ -76,6 +68,7 @@ export default function PileLayout({ children }) {
+ ); } diff --git a/src/renderer/pages/Pile/Posts/Posts.module.scss b/src/renderer/pages/Pile/Posts/Posts.module.scss index 926f151..ac4acc5 100644 --- a/src/renderer/pages/Pile/Posts/Posts.module.scss +++ b/src/renderer/pages/Pile/Posts/Posts.module.scss @@ -13,7 +13,7 @@ // background: var(--bg-secondary); // border: 2px solid var(--border); border-radius: 22px; - background-image: url('../../../../../assets/garden.png'); + background-image: url('../../../../../assets/garden.jpeg'); background-size: cover; background-position: bottom center; height: 380px; diff --git a/src/renderer/pages/Pile/Toasts/Toast/Thinking/index.jsx b/src/renderer/pages/Pile/Toasts/Toast/Thinking/index.jsx new file mode 100644 index 0000000..8efcccf --- /dev/null +++ b/src/renderer/pages/Pile/Toasts/Toast/Thinking/index.jsx @@ -0,0 +1,103 @@ +export default function Thinking(props) { + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +} diff --git a/src/renderer/pages/Pile/Toasts/Toast/Toast.module.scss b/src/renderer/pages/Pile/Toasts/Toast/Toast.module.scss new file mode 100644 index 0000000..0c1aa91 --- /dev/null +++ b/src/renderer/pages/Pile/Toasts/Toast/Toast.module.scss @@ -0,0 +1,18 @@ +.toast { + display: inline-flex; + align-items: center; + margin: 0 auto; + margin-bottom: 7px; + background: var(--base); + backdrop-filter: blur(30px); + padding: 7px 18px 7px 12px; + border-radius: 90px; + font-size: 0.8em; + text-align: center; + + .icon { + height: 18px; + width: 18px; + margin-right: 8px; + } +} \ No newline at end of file diff --git a/src/renderer/pages/Pile/Toasts/Toast/Waiting/index.jsx b/src/renderer/pages/Pile/Toasts/Toast/Waiting/index.jsx new file mode 100644 index 0000000..7e72999 --- /dev/null +++ b/src/renderer/pages/Pile/Toasts/Toast/Waiting/index.jsx @@ -0,0 +1,105 @@ +import styles from './Thinking.module.scss'; + +export default function Thinking(props) { + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +} diff --git a/src/renderer/pages/Pile/Toasts/Toast/index.jsx b/src/renderer/pages/Pile/Toasts/Toast/index.jsx new file mode 100644 index 0000000..ec92a83 --- /dev/null +++ b/src/renderer/pages/Pile/Toasts/Toast/index.jsx @@ -0,0 +1,21 @@ +import styles from './Toast.module.scss'; +import { motion } from 'framer-motion'; +import { useToastsContext } from 'renderer/context/ToastsContext'; +import Logo from 'renderer/pages/Home/logo'; +import Thinking from './Thinking'; + +export default function Toast({ notification }) { + return ( + +
+ + {notification.message} +
+
+ ); +} diff --git a/src/renderer/pages/Pile/Toasts/Toasts.module.scss b/src/renderer/pages/Pile/Toasts/Toasts.module.scss new file mode 100644 index 0000000..3f748f4 --- /dev/null +++ b/src/renderer/pages/Pile/Toasts/Toasts.module.scss @@ -0,0 +1,12 @@ +.container { + position: fixed; + bottom: 10px; + left: 0; + width: 100%; + display: flex; + flex-direction: column; + justify-content: flex-end; + align-items: center; + padding: 0 20px; + z-index: 99; +} \ No newline at end of file diff --git a/src/renderer/pages/Pile/Toasts/index.jsx b/src/renderer/pages/Pile/Toasts/index.jsx new file mode 100644 index 0000000..b835462 --- /dev/null +++ b/src/renderer/pages/Pile/Toasts/index.jsx @@ -0,0 +1,21 @@ +import styles from './Toasts.module.scss'; +import { useToastsContext } from 'renderer/context/ToastsContext'; +import Logo from 'renderer/pages/Home/logo'; +import Toast from './Toast'; +import { AnimatePresence } from 'framer-motion'; + +export default function Toasts() { + const { notifications, addNotification } = useToastsContext(); + + const renderNotifications = () => { + return notifications.map((n) => { + return ; + }); + }; + + return ( +
+ {renderNotifications()} +
+ ); +}