mirror of
https://github.com/vercel/commerce.git
synced 2025-05-18 23:46:58 +00:00
Merge branch 'main' into adding-dalle-api
This commit is contained in:
commit
3c0fb48518
@ -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>
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
176
components/pdf/StoryPDFViewer.tsx
Normal file
176
components/pdf/StoryPDFViewer.tsx
Normal 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%'
|
||||||
|
}
|
||||||
|
});
|
56
lib/utils/usePromiseMemo.ts
Normal file
56
lib/utils/usePromiseMemo.ts
Normal 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;
|
@ -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 };
|
||||||
|
Loading…
x
Reference in New Issue
Block a user