diff --git a/components/generate/GenerateStoryComponent.tsx b/components/generate/GenerateStoryComponent.tsx index 02cf61efd..1f792a9b0 100644 --- a/components/generate/GenerateStoryComponent.tsx +++ b/components/generate/GenerateStoryComponent.tsx @@ -1,59 +1,16 @@ 'use-client'; -import Image from 'next/image'; -import chatOperations from 'operations/chatOperations'; -import imageOperations from 'operations/imageOperations'; -import { useState } from 'react'; import GenerateStoryContextProvider, { IGenerateStoryContext } from './GenerateStoryContext'; +import StoryPDFViewer from 'components/pdf/StoryPDFViewer'; export default function GenerateStoryComponent() { - const [loading, setLoading] = useState(false); - const [data, setData] = useState(); - const [cover, setCover] = useState(''); - - const getStory = async () => { - setLoading(true); - const data = await chatOperations.createStoryAsync(); - setData(data); - setLoading(false); - }; - - const getCover = async () => { - setLoading(true); - const data = await imageOperations.createImageAsync( - 'make a cover image of the best book in the world for children' - ); - const coverURL = data.text[0].url; - setCover(coverURL); - setLoading(false); - }; - return ( - {({ story }: IGenerateStoryContext) => { + {({ story, loading }: IGenerateStoryContext) => { return ( -
- - - - - {loading ?
Loading...
: null} - - {cover && } - - {JSON.stringify(data)} - - {JSON.stringify(data)} -
+
+ {loading ?
Loading...
: null} + {story && } +
); }}
diff --git a/components/generate/GenerateStoryContext.tsx b/components/generate/GenerateStoryContext.tsx index 79b048c88..251161cd5 100644 --- a/components/generate/GenerateStoryContext.tsx +++ b/components/generate/GenerateStoryContext.tsx @@ -1,39 +1,55 @@ 'use client'; -import { IStory } from 'operations/chatOperations'; -import { PropsWithChildren, createContext, useContext, useMemo, useState } from 'react'; +import chatOperations, { IStory } from 'operations/chatOperations'; +import { + PropsWithChildren, + createContext, + useCallback, + useContext, + useMemo, + useState +} from 'react'; export interface IGenerateStoryContext { story?: IStory; - setStory: (story: IStory) => void; - images: string[]; - setImages: (images: string[]) => void; + loading: boolean; } const GenerateStoryContext = createContext({ story: undefined, - setStory: () => {}, - images: [], - setImages: () => {} + loading: false }); function GenerateStoryContextProvider({ children }: { children: PropsWithChildren }) { + const [loading, setLoading] = useState(false); const [story, setStory] = useState(); - /* - Note(Benson): For now images is an array of urls where each index in the array - corresponds to the page number. - i.e., index 0 could be title, index 1 is the first page in pages, etc. - */ - const [images, setImages] = useState([]); - const value = useMemo( - () => ({ story, setStory, images, setImages }), - [story, setStory, images, setImages] - ); + const value = useMemo(() => ({ story, loading }), [story, loading]); + + /* + TODO(Benson -> Patricio): Make network call to get images + TODO(Benson -> Patricio): Write a helper function in this directory /generate/utils.ts + called mergeImages(story: IStory, images: string[]): IStory; + */ + const getStoryAsync = useCallback(async () => { + setLoading(true); + const story = await chatOperations.createStoryAsync(); + // const images = await imageOperations.getStoryImagesAsync(story); + setStory(story); + setLoading(false); + }, []); return ( - {typeof children === 'function' ? children(value) : children} + <> + + {typeof children === 'function' ? children(value) : children} + ); } diff --git a/components/pdf/StoryPDFViewer.tsx b/components/pdf/StoryPDFViewer.tsx new file mode 100644 index 000000000..9c66ea2a4 --- /dev/null +++ b/components/pdf/StoryPDFViewer.tsx @@ -0,0 +1,176 @@ +'use client'; + +import { + Font, + Image, + Document as PDFDocument, + Page, + StyleSheet, + Text, + View +} from '@react-pdf/renderer'; +import dynamic from 'next/dynamic'; +import { IStory } from 'operations/chatOperations'; + +export const runtime = 'edge'; + +const PDFViewerNoSSR = dynamic(() => import('@react-pdf/renderer').then((mod) => mod.PDFViewer), { + ssr: false +}); + +export default function StoryPDFViewer({ story }: { story: IStory }) { + return ( +
+ + + +
+ ); +} + +Font.register({ + family: 'JosefinSans', + fonts: [ + { src: '/fonts/Josefin_Sans/static/JosefinSans-Thin.ttf', fontWeight: 100 }, + { + src: '/fonts/Josefin_Sans/static/JosefinSans-ExtraLight.ttf', + fontWeight: 200 + }, + { + src: '/fonts/Josefin_Sans/static/JosefinSans-Light.ttf', + fontWeight: 300 + }, + { + src: '/fonts/Josefin_Sans/static/JosefinSans-Regular.ttf', + fontWeight: 400 + }, + { + src: '/fonts/Josefin_Sans/static/JosefinSans-Medium.ttf', + fontWeight: 500 + }, + { + src: '/fonts/Josefin_Sans/static/JosefinSans-SemiBold.ttf', + fontWeight: 600 + }, + { src: '/fonts/Josefin_Sans/static/JosefinSans-Bold.ttf', fontWeight: 700 }, + { + src: '/fonts/Josefin_Sans/static/JosefinSans-ExtraBold.ttf', + fontWeight: 800 + }, + { + src: '/fonts/Josefin_Sans/static/JosefinSans-Black.ttf', + fontWeight: 900 + }, + { + src: '/fonts/Josefin_Sans/static/JosefinSans-ThinItalic.ttf', + fontStyle: 'italic', + fontWeight: 100 + }, + { + src: '/fonts/Josefin_Sans/static/JosefinSans-ExtraLightItalic.ttf', + fontStyle: 'italic', + fontWeight: 200 + }, + { + src: '/fonts/Josefin_Sans/static/JosefinSans-LightItalic.ttf', + fontStyle: 'italic', + fontWeight: 300 + }, + { + src: '/fonts/Josefin_Sans/static/JosefinSans-Italic.ttf', + fontStyle: 'italic', + fontWeight: 400 + }, + { + src: '/fonts/Josefin_Sans/static/JosefinSans-MediumItalic.ttf', + fontStyle: 'italic', + fontWeight: 500 + }, + { + src: '/fonts/Josefin_Sans/static/JosefinSans-SemiBoldItalic.ttf', + fontStyle: 'italic', + fontWeight: 600 + }, + { + src: '/fonts/Josefin_Sans/static/JosefinSans-BoldItalic.ttf', + fontStyle: 'italic', + fontWeight: 700 + }, + { + src: '/fonts/Josefin_Sans/static/JosefinSans-ExtraBoldItalic.ttf', + fontStyle: 'italic', + fontWeight: 800 + }, + { + src: '/fonts/Josefin_Sans/static/JosefinSans-BlackItalic.ttf', + fontStyle: 'italic', + fontWeight: 900 + } + ] +}); + +const imgURL = + 'https://cdn.discordapp.com/attachments/989274756341706822/1175024578578366534/pinturillu_sian_couple_of_men_illustration_fantasy_Charlie_Bowa_1c51f19c-d5b9-4b53-b32f-e5468912bd1d.png?ex=6569b9ea&is=655744ea&hm=8dd0e4e5653bb9f7a7330f745983035f93e1891279351efe2dd6be7657987d88&'; + +/* +Note(Benson): PDFTest has some hardcoded implementation of generating random sections. +Right now for the Story viewer just render all the text at the bottom until we come up +with a solidified way at dispersing the text throughout the page. +*/ + +// TODO(Benson -> Patricio): replace hardcoded images. +const Document = ({ pages }: { pages: { text: string }[] }) => { + return ( + + {pages.map(({ text }, index) => { + return ( + + + {text} + + + + ); + })} + + ); +}; + +const styles = StyleSheet.create({ + pdfContainer: { width: '100%', height: '100%' }, + image: { + width: '100%', + height: '100%', + position: 'absolute', + zIndex: -1, + top: 0 + }, + page: { + position: 'relative', + display: 'flex', + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'flex-start' + }, + section: { + position: 'absolute', + margin: 30, + fontFamily: 'JosefinSans', + /* Background properties */ + backgroundColor: 'rgba(255, 255, 255, 0.5)' /* semi-transparent white background */, + /* Blur effect */ + backdropFilter: 'blur(10px)', + /* Additional styling for aesthetics */ + padding: 12, + borderRadius: 16, + boxShadow: '0 4px 8px rgba(0, 0, 0, 0.1)', + fontSize: 20, + width: '90%' + } +}); diff --git a/lib/utils/usePromiseMemo.ts b/lib/utils/usePromiseMemo.ts new file mode 100644 index 000000000..6018842b3 --- /dev/null +++ b/lib/utils/usePromiseMemo.ts @@ -0,0 +1,56 @@ +import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import dependenciesMatch from 'utils/dependenciesMatch'; + +const usePromiseMemo = ( + promise: () => Promise, + nextDeps: unknown[] +): { results?: T; error?: E; loading: boolean; refetch: () => void } => { + const [results, setResults] = useState(); + const [error, setError] = useState(); + const [hasFinished, setHasFinished] = useState(false); + const dependencies = useRef(nextDeps); + + const isMounted = useRef(true); + + const checkIfPromiseIsStillValid = useCallback( + (dependenciesAtTimeOfPromise: unknown[]): boolean => { + return ( + isMounted.current && dependenciesMatch(dependenciesAtTimeOfPromise, dependencies.current) + ); + }, + [] + ); + + const run = useCallback(() => { + setHasFinished(false); + promise() + .then((r) => checkIfPromiseIsStillValid(nextDeps) && setResults(r)) + .catch((e) => checkIfPromiseIsStillValid(nextDeps) && setError(e)) + .finally(() => checkIfPromiseIsStillValid(nextDeps) && setHasFinished(true)); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, nextDeps); + + useEffect(() => { + isMounted.current = true; + dependencies.current = nextDeps; + run(); + return () => { + isMounted.current = false; + }; + // nextDeps is already a dependency of run + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [run]); + + return useMemo( + () => ({ + results, + error, + loading: !hasFinished, + refetch: run + }), + // eslint-disable-next-line react-hooks/exhaustive-deps + [hasFinished, results] + ); +}; + +export default usePromiseMemo; diff --git a/operations/chatOperations.ts b/operations/chatOperations.ts index b9b4936e9..85344345d 100644 --- a/operations/chatOperations.ts +++ b/operations/chatOperations.ts @@ -17,13 +17,20 @@ function getFunctionCallArguments(response: any) { return JSON.parse(response.text.function_call.arguments); } +/* + TODO(Benson -> Patricio): update this typing? add images to pages? assuming images + and pages are 1:1? that means we should probably also have a titleImage property too then? + Added dummy filler. + */ export interface IStory { title: string; + // titleImage?: string; topic: string; introduction: string; narrativeStructure: string; archetypes_characters: string; pages: { text: string }[]; + // pages: { text: string, image?: string }[]; } async function createStoryAsync( @@ -40,9 +47,13 @@ async function createStoryAsync( content: userPrompt } ]; - const data = await post('/api/open-ai/chat', generateRequestPayload(messages)); - // const data = await post('/api/revalidate', generateRequestPayload(messages)); - return getFunctionCallArguments(data); + try { + const data = await post('/api/open-ai/chat', generateRequestPayload(messages)); + return getFunctionCallArguments(data); + } catch (e) { + console.error(e); + throw e; + } } export default { createStoryAsync };