Skip to content

Commit 6b7b964

Browse files
committed
refactor: overhaul story-book to be a full webapp
- in app streaming of generating stories using node module - increase temp for story-writer to 1 - can export generated stories to a PDF Signed-off-by: tylerslaton <[email protected]>
1 parent 0da1f89 commit 6b7b964

24 files changed

+759
-0
lines changed

examples/story-book/.gitignore

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# Nuxt dev/build outputs
2+
.output
3+
.data
4+
.nuxt
5+
.nitro
6+
.cache
7+
dist
8+
9+
# Node dependencies
10+
node_modules
11+
12+
# Logs
13+
logs
14+
*.log
15+
16+
# Misc
17+
.DS_Store
18+
.fleet
19+
.idea
20+
21+
# Local env files
22+
.env
23+
.env.*
24+
!.env.example
25+
26+
# AI generated files
27+
public/stories

examples/story-book/README.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# Story Book
2+
3+
Story Book is a web application that has an interface for users to input a prompt and number of pages. This information then generates a story based on the prompt. All generation is done using GPTScript on the backend.
4+
5+
## Usage Instructions
6+
7+
1. Make sure you have at least Node v20.11.1 installed. If you don't, you can install it [here](https://nodejs.org/en/download).
8+
9+
2. Navigate to the `examples/story-book` directory.
10+
11+
```bash
12+
cd examples/story-book
13+
```
14+
15+
3. Start the Nuxt application by running the following commands:
16+
17+
```bash
18+
npm i
19+
npm run dev
20+
```
21+
22+
4. Navigate to `http://localhost:3000` in your browser.

examples/story-book/app.vue

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<script lang="ts" setup>
2+
useHead({
3+
title: 'Story Book',
4+
meta: [
5+
{ charset: 'utf-8' },
6+
{ name: 'apple-mobile-web-app-capable', content: 'yes' },
7+
{ name: 'format-detection', content: 'telephone=no' },
8+
{ name: 'viewport', content: `width=device-width` },
9+
],
10+
})
11+
</script>
12+
13+
<template>
14+
<div>
15+
<div class="grid grid-rows-[1fc,1fr] fixed top-0 right-0 bottom-0 left-0">
16+
<div class="overflow-auto border-t-2 border-transparent">
17+
<Nav class="absolute right-5 top-5 z-10"/>
18+
<div class="p-5 lg:p-10 max-w-full w-full h-full mx-auto">
19+
<NuxtPage class="pb-10" />
20+
</div>
21+
</div>
22+
</div>
23+
<UNotifications />
24+
</div>
25+
</template>
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<script setup lang="ts">
2+
const colorMode = useColorMode()
3+
const isDark = computed({
4+
get() {
5+
return colorMode.value === 'dark'
6+
},
7+
set() {
8+
colorMode.preference = colorMode.value === 'dark' ? 'light' : 'dark'
9+
},
10+
})
11+
</script>
12+
13+
<template>
14+
<ClientOnly>
15+
<UButton
16+
:icon="isDark ? 'i-heroicons-moon' : 'i-heroicons-sun'"
17+
aria-label="Theme"
18+
v-bind="$attrs"
19+
@click="isDark = !isDark"
20+
/>
21+
<template #fallback>
22+
<div class="w-8 h-8" />
23+
</template>
24+
</ClientOnly>
25+
</template>
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<script setup lang="ts">
2+
const isMenuOpen = ref(false)
3+
</script>
4+
5+
<template>
6+
<div>
7+
<div class="flex space-x-4">
8+
<DisplayMode />
9+
<UButton icon="i-heroicons-home" @click="() => { useRouter().push('/'); isMenuOpen = false }" />
10+
<UButton icon="i-heroicons-bars-3" @click="isMenuOpen = true" />
11+
</div>
12+
13+
<USlideover v-model="isMenuOpen">
14+
<UCard class="flex flex-col flex-1" :ui="{ body: { base: 'flex-1' }, ring: '', divide: 'divide-y divide-gray-100 dark:divide-gray-800' }">
15+
<template #header>
16+
<div class="flex items-center justify-between">
17+
<h3 class="text-4xl">Story Book</h3>
18+
<div class="flex space-x-4">
19+
<UButton icon="i-heroicons-x-mark-20-solid" @click="isMenuOpen = false" />
20+
</div>
21+
</div>
22+
</template>
23+
<Stories />
24+
</UCard>
25+
</USlideover>
26+
</div>
27+
</template>
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
<script setup lang="ts">
2+
import { useMainStore } from '@/store'
3+
4+
const store = useMainStore()
5+
const open = ref(false)
6+
const pageOptions = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
7+
const toast = useToast()
8+
const state = reactive({
9+
prompt: '',
10+
pages: 0,
11+
})
12+
13+
async function onSubmit () {
14+
const response = await fetch('/api/story', {
15+
method: 'POST',
16+
headers: { 'Content-Type': 'application/json' },
17+
body: JSON.stringify(state)
18+
})
19+
20+
if (response.ok) {
21+
toast.add({
22+
id: 'story-generating',
23+
title: 'Story Generating',
24+
description: 'Your story is being generated. Depending on the length, this may take a few minutes.',
25+
icon: 'i-heroicons-pencil-square-solid',
26+
})
27+
28+
const es = new EventSource(`/api/story/sse?prompt=${state.prompt}`)
29+
es.onmessage = async (event) => {
30+
store.addPendingStoryMessage(state.prompt, event.data)
31+
if ((event.data as string) === 'done') {
32+
es.close()
33+
store.removePendingStory(state.prompt)
34+
store.fetchStories()
35+
toast.add({
36+
id: 'story-created',
37+
title: 'Story Created',
38+
description: 'A story you requested has been created.',
39+
icon: 'i-heroicons-check-circle-solid',
40+
})
41+
} else if ((event.data as string).includes('error')) {
42+
es.close()
43+
store.removePendingStory(state.prompt)
44+
45+
toast.add({
46+
id: 'story-generating-failed',
47+
title: 'Story Generation Failed',
48+
description: `Your story could not be generated due to an error.\n\n${event.data}.`,
49+
icon: 'i-heroicons-x-mark',
50+
timeout: 30000,
51+
})
52+
}
53+
}
54+
55+
if (state.prompt.length){
56+
store.addPendingStory(state.prompt, es)
57+
}
58+
open.value = false
59+
} else {
60+
toast.add({
61+
id: 'story-generating-failed',
62+
title: 'Story Generation Failed',
63+
description: `Your story could not be generated due to an error: ${response.statusText}.`,
64+
icon: 'i-heroicons-x-mark',
65+
})
66+
}
67+
}
68+
</script>
69+
70+
71+
<template>
72+
<UButton size="lg" class="w-full text-xl" icon="i-heroicons-plus" @click="open = true">New Story</UButton>
73+
<UModal v-if="Object.keys(store.pendingStories).length === 0" v-model="open" :ui="{width: 'sm:max-w-3/4 w-4/5 md:w-3/4 lg:w-1/2', }">
74+
<UCard class="h-full">
75+
<template #header>
76+
<div class="flex items-center justify-between">
77+
<h1 class="text-2xl">Create a new story</h1>
78+
<UButton color="gray" variant="ghost" icon="i-heroicons-x-mark-20-solid" class="-my-1" @click="open = false" />
79+
</div>
80+
</template>
81+
<div >
82+
<UForm class="h-full" :state=state @submit="onSubmit">
83+
<UFormGroup size="lg" class="my-6" label="Pages" name="pages">
84+
<USelectMenu class="w-1/4 md:w-1/6"v-model="state.pages" :options="pageOptions"/>
85+
</UFormGroup>
86+
<UFormGroup class="my-6" label="Story" name="prompt">
87+
<UTextarea :ui="{base: 'h-[20vh]'}" size="xl" class="" v-model="state.prompt" label="Prompt" placeholder="Put your full story here or prompt for a new one"/>
88+
</UFormGroup>
89+
<UButton size="xl" icon="i-heroicons-book-open" type="submit">Create Story</UButton>
90+
</UForm>
91+
</div>
92+
</UCard>
93+
</UModal>
94+
</template>
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
<script setup lang="ts">
2+
import { useMainStore } from '@/store'
3+
import unmangleStoryName from '@/lib/unmangle'
4+
5+
const toast = useToast()
6+
const store = useMainStore()
7+
8+
const pendingStories = computed(() => store.pendingStories)
9+
const pendingStoryMessages = computed(() => store.pendingStoryMessages)
10+
const stories = computed(() => store.stories)
11+
onMounted(async () => store.fetchStories() )
12+
13+
const goToStory = (name: string) => {
14+
useRouter().push(`/story/${name}`)
15+
}
16+
17+
const deleteStory = async (name: string) => {
18+
const response = await fetch(`/api/story/${name}`, { method: 'DELETE' })
19+
if (response.ok) {
20+
store.removeStory(name)
21+
toast.add({
22+
id: 'story-deleted',
23+
title: `${name} deleted`,
24+
description: 'The story has been deleted.',
25+
icon: 'i-heroicons-trash',
26+
})
27+
} else {
28+
toast.add({
29+
id: 'story-delete-failed',
30+
title: `Failed to delete ${name}`,
31+
description: 'The story could not be deleted.',
32+
icon: 'i-heroicons-x-mark',
33+
})
34+
}
35+
}
36+
</script>
37+
38+
<template>
39+
<div class="flex flex-col space-y-2">
40+
<New v-if="!Object.keys(pendingStories).length" class="w-full"/>
41+
<UPopover mode="hover" v-for="(_, name) in pendingStories" >
42+
<UButton truncate loading :key="name" size="lg" class="w-full text-xl" :label="name" icon="i-heroicons-book-open"/>
43+
44+
<template #panel>
45+
<UCard class="p-4 w-[80vw] xl:w-[40vw]">
46+
<h1 class="text-xl">Writing the perfect story...</h1>
47+
<h2 class="text-zinc-400 mb-4">GPTScript is currently building the story you requested. You can see its progress below.</h2>
48+
<pre class="h-[26vh] bg-zinc-950 px-6 text-white overflow-x-scroll rounded shadow">
49+
<p v-for="message in pendingStoryMessages[name]">> {{ message }}</p>
50+
</pre>
51+
</UCard>
52+
</template>
53+
</UPopover>
54+
55+
<div class="w-full" v-for="story in stories" >
56+
<!-- The LLM likes to ocassionally, not always, generate folders with "-" and other times with " ", handle both cases for the button -->
57+
<UButton truncate :key="story" size="lg" class="w-5/6 text-xl" :label="unmangleStoryName(story)" icon="i-heroicons-book-open" @click="goToStory(story)"/>
58+
<UButton size="lg" variant="ghost" class="w-1/7 text-xl ml-4" icon="i-heroicons-trash" @click="deleteStory(story)"/>
59+
</div>
60+
61+
</div>
62+
</template>

examples/story-book/lib/types.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
export type Pages = Record<string, Page>;
2+
3+
export type Page = {
4+
image_path: string;
5+
content: string;
6+
}

examples/story-book/lib/unmangle.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
const unmangleStoryName = (storyName: string): string => {
2+
return storyName.replaceAll('-', ' ').replace(/\b\w/g, c => c.toUpperCase());
3+
}
4+
export default unmangleStoryName;

examples/story-book/nuxt.config.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
// https://nuxt.com/docs/api/configuration/nuxt-config
2+
export default defineNuxtConfig({
3+
devtools: { enabled: true },
4+
modules: [
5+
'@nuxt/ui',
6+
'@nuxtjs/tailwindcss',
7+
'@pinia/nuxt',
8+
],
9+
})

0 commit comments

Comments
 (0)