Merge branch 'main' into adding-dalle-api

This commit is contained in:
Patricio Dieck 2023-11-20 12:00:34 -06:00
commit 3c0fb48518
5 changed files with 287 additions and 71 deletions

View File

@ -1,59 +1,16 @@
'use-client'; '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 GenerateStoryContextProvider, { IGenerateStoryContext } from './GenerateStoryContext';
import StoryPDFViewer from 'components/pdf/StoryPDFViewer';
export default function GenerateStoryComponent() { 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 ( return (
<GenerateStoryContextProvider> <GenerateStoryContextProvider>
{({ story }: IGenerateStoryContext) => { {({ story, loading }: IGenerateStoryContext) => {
return ( return (
<main className="flex min-h-screen flex-col items-center justify-between p-24"> <div>
<button {loading ? <div>Loading...</div> : null}
onClick={getStory} {story && <StoryPDFViewer story={story} />}
className="mb-10 rounded bg-blue-500 px-4 py-2 font-bold text-white hover:bg-blue-700" </div>
>
Run A New Story
</button>
<button
onClick={getCover}
className="mb-10 rounded bg-blue-500 px-4 py-2 font-bold text-white hover:bg-blue-700"
>
Get a Title Image
</button>
{loading ? <div className="mb10">Loading...</div> : null}
{cover && <img src={cover} alt="" width={500} height={500} />}
{JSON.stringify(data)}
{JSON.stringify(data)}
</main>
); );
}} }}
</GenerateStoryContextProvider> </GenerateStoryContextProvider>

View File

@ -1,39 +1,55 @@
'use client'; 'use client';
import { IStory } from 'operations/chatOperations'; import chatOperations, { IStory } from 'operations/chatOperations';
import { PropsWithChildren, createContext, useContext, useMemo, useState } from 'react'; import {
PropsWithChildren,
createContext,
useCallback,
useContext,
useMemo,
useState
} from 'react';
export interface IGenerateStoryContext { export interface IGenerateStoryContext {
story?: IStory; story?: IStory;
setStory: (story: IStory) => void; loading: boolean;
images: string[];
setImages: (images: string[]) => void;
} }
const GenerateStoryContext = createContext<IGenerateStoryContext>({ const GenerateStoryContext = createContext<IGenerateStoryContext>({
story: undefined, story: undefined,
setStory: () => {}, loading: false
images: [],
setImages: () => {}
}); });
function GenerateStoryContextProvider({ children }: { children: PropsWithChildren<any> }) { function GenerateStoryContextProvider({ children }: { children: PropsWithChildren<any> }) {
const [loading, setLoading] = useState<boolean>(false);
const [story, setStory] = useState<IStory>(); const [story, setStory] = useState<IStory>();
/*
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<string[]>([]);
const value = useMemo<IGenerateStoryContext>( const value = useMemo<IGenerateStoryContext>(() => ({ story, loading }), [story, loading]);
() => ({ story, setStory, images, setImages }),
[story, setStory, images, setImages] /*
); 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 ( return (
<GenerateStoryContext.Provider value={value}> <GenerateStoryContext.Provider value={value}>
{typeof children === 'function' ? children(value) : children} <>
<button
onClick={getStoryAsync}
className="absolute right-24 top-5 z-10 rounded bg-blue-500 px-4 py-2 font-bold text-white hover:bg-blue-700"
>
Run A New Story
</button>
{typeof children === 'function' ? children(value) : children}
</>
</GenerateStoryContext.Provider> </GenerateStoryContext.Provider>
); );
} }

View File

@ -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 (
<div
style={{
width: '100vw',
height: 'calc(100vh - 76px)',
position: 'relative'
}}
>
<PDFViewerNoSSR style={styles.pdfContainer}>
<Document pages={story.pages} />
</PDFViewerNoSSR>
</div>
);
}
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 (
<PDFDocument>
{pages.map(({ text }, index) => {
return (
<Page size="A4" style={styles.page} key={text}>
<View key={index} style={{ ...styles.section, bottom: '10%' }}>
<Text>{text}</Text>
</View>
<Image src={imgURL} style={styles.image} />
</Page>
);
})}
</PDFDocument>
);
};
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%'
}
});

View File

@ -0,0 +1,56 @@
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import dependenciesMatch from 'utils/dependenciesMatch';
const usePromiseMemo = <T, E = unknown>(
promise: () => Promise<T>,
nextDeps: unknown[]
): { results?: T; error?: E; loading: boolean; refetch: () => void } => {
const [results, setResults] = useState<T>();
const [error, setError] = useState<E>();
const [hasFinished, setHasFinished] = useState<boolean>(false);
const dependencies = useRef<unknown[]>(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;

View File

@ -17,13 +17,20 @@ function getFunctionCallArguments<T>(response: any) {
return JSON.parse(response.text.function_call.arguments); 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 { export interface IStory {
title: string; title: string;
// titleImage?: string;
topic: string; topic: string;
introduction: string; introduction: string;
narrativeStructure: string; narrativeStructure: string;
archetypes_characters: string; archetypes_characters: string;
pages: { text: string }[]; pages: { text: string }[];
// pages: { text: string, image?: string }[];
} }
async function createStoryAsync( async function createStoryAsync(
@ -40,9 +47,13 @@ async function createStoryAsync(
content: userPrompt content: userPrompt
} }
]; ];
const data = await post('/api/open-ai/chat', generateRequestPayload(messages)); try {
// const data = await post('/api/revalidate', generateRequestPayload(messages)); const data = await post('/api/open-ai/chat', generateRequestPayload(messages));
return getFunctionCallArguments<IStory>(data); return getFunctionCallArguments<IStory>(data);
} catch (e) {
console.error(e);
throw e;
}
} }
export default { createStoryAsync }; export default { createStoryAsync };