Skip to content

Commit 8e740fd

Browse files
committed
refactor: overhaul story-book to be a full webapp
Signed-off-by: tylerslaton <[email protected]>
1 parent 0da1f89 commit 8e740fd

22 files changed

+637
-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-10 top-10 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: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
<script setup lang="ts">
2+
import { useMainStore } from '@/store'
3+
4+
const isMenuOpen = ref(false)
5+
const toast = useToast()
6+
const store = useMainStore()
7+
8+
const pendingStories = computed(() => store.pendingStories)
9+
const stories = computed(() => store.stories)
10+
onMounted(async () => store.fetchStories() )
11+
12+
const goToStory = (name: string) => {
13+
useRouter().push(`/story/${name}`)
14+
isMenuOpen.value = false
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>
40+
<div class="flex space-x-4">
41+
<DisplayMode />
42+
<UButton icon="i-heroicons-bars-3" @click="isMenuOpen = true" />
43+
</div>
44+
45+
<USlideover v-model="isMenuOpen">
46+
<UCard class="flex flex-col flex-1" :ui="{ body: { base: 'flex-1' }, ring: '', divide: 'divide-y divide-gray-100 dark:divide-gray-800' }">
47+
<template #header>
48+
<div class="flex items-center justify-between">
49+
<h3 class="text-4xl">Story Book</h3>
50+
<UButton icon="i-heroicons-x-mark-20-solid" class="-my-1" @click="isMenuOpen = false" />
51+
</div>
52+
</template>
53+
54+
<div class="flex flex-col space-y-2">
55+
<New class="w-full"/>
56+
<UButton truncate loading v-for="(_, name) in pendingStories" :key="name" size="lg" class="w-full text-xl" :label="name" icon="i-heroicons-book-open"/>
57+
<div class="w-full" v-for="story in stories" >
58+
<UButton truncate :key="story" size="lg" class="w-5/6 text-xl" :label="story" icon="i-heroicons-book-open" @click="goToStory(story as string)"/>
59+
<!-- delete icon -->
60+
<UButton size="lg" variant="ghost" class="w-1/7 text-xl ml-4" icon="i-heroicons-trash" @click="deleteStory(story as string)"/>
61+
</div>
62+
63+
</div>
64+
</UCard>
65+
</USlideover>
66+
</div>
67+
</template>
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
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 = function(event) {
30+
if ((event.data as string).includes('Command exited with code 0')) {
31+
es.close()
32+
store.removePendingStory(state.prompt)
33+
store.fetchStories()
34+
toast.add({
35+
id: 'story-created',
36+
title: 'Story Created',
37+
description: 'A story you requested has been created.',
38+
icon: 'i-heroicons-check-circle-solid',
39+
})
40+
}
41+
}
42+
43+
if (state.prompt.length){
44+
store.addPendingStory(state.prompt, es)
45+
}
46+
open.value = false
47+
} else {
48+
toast.add({
49+
id: 'story-generating-failed',
50+
title: 'Story Generation Failed',
51+
description: `Your story could not be generated due to an error: ${response.statusText}.`,
52+
icon: 'i-heroicons-x-mark',
53+
})
54+
}
55+
}
56+
</script>
57+
58+
59+
<template>
60+
<UPopover v-model:open="open" class="overflow-visible">
61+
<UButton size="lg" class="w-full text-xl" icon="i-heroicons-plus">New Story</UButton>
62+
<template #panel>
63+
<UForm class="w-[15vw] p-8" :state=state @submit="onSubmit">
64+
<UFormGroup label="Prompt" name="prompt">
65+
<UTextarea v-model="state.prompt" label="Prompt"/>
66+
</UFormGroup>
67+
<UFormGroup class="my-6" label="Pages" name="pages">
68+
<USelect class="overflow-visible"v-model="state.pages" :options="pageOptions" />
69+
</UFormGroup>
70+
<UButton class="w-full" type="submit">Create Story</UButton>
71+
</UForm>
72+
</template>
73+
</UPopover>
74+
</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/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+
})

examples/story-book/package.json

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
{
2+
"name": "nuxt-app",
3+
"private": true,
4+
"type": "module",
5+
"scripts": {
6+
"build": "nuxt build",
7+
"dev": "nuxt dev",
8+
"generate": "nuxt generate",
9+
"preview": "nuxt preview",
10+
"postinstall": "nuxt prepare"
11+
},
12+
"dependencies": {
13+
"@nuxt/ui": "^2.15.0",
14+
"@pinia/nuxt": "^0.5.1",
15+
"nuxt": "^3.11.1",
16+
"pinia": "^2.1.7",
17+
"vue": "^3.4.21",
18+
"vue-router": "^4.3.0"
19+
}
20+
}

examples/story-book/pages/index.vue

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<script setup lang="ts">
2+
import { useMainStore } from '@/store'
3+
4+
const store = useMainStore()
5+
const stories = computed(() => store.stories)
6+
const pendingStories = computed(() => store.pendingStories)
7+
8+
onMounted(async () => store.fetchStories() )
9+
const goToStory = (name: string) => useRouter().push(`/story/${name}`)
10+
</script>
11+
12+
<template>
13+
<div class="w-full h-full flex items-center justify-center">
14+
<div>
15+
<h1 class="text-4xl">Welcome to your story book!</h1>
16+
<UDivider class="w-full my-10">Your stories</UDivider>
17+
<div class="w-full flex flex-col space-y-4 max-h-[50vh] overflow-y-scroll">
18+
<New class="w-full"/>
19+
<UButton truncate size="xl" class="text-xl" icon="i-heroicons-trash" loading v-for="(_, story) in pendingStories">{{ story }}</UButton>
20+
<UButton truncate size="xl" class="text-xl" icon="i-heroicons-book-open" v-for="story in stories" @click="goToStory(story)">{{ story }}</UButton>
21+
</div>
22+
</div>
23+
</div>
24+
</template>

0 commit comments

Comments
 (0)