Skip to content

refactor: overhaul story-book to be a full webapp #191

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Apr 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions examples/story-book/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Nuxt dev/build outputs
.output
.data
.nuxt
.nitro
.cache
dist

# Node dependencies
node_modules

# Logs
logs
*.log

# Misc
.DS_Store
.fleet
.idea

# Local env files
.env
.env.*
!.env.example

# AI generated files
public/stories
35 changes: 12 additions & 23 deletions examples/story-book/README.md
Original file line number Diff line number Diff line change
@@ -1,33 +1,22 @@
# Story Book

Story Book is a GPTScript that can generate a story based on a prompt and the number of pages you want the story to be in. It is generated in HTML format and can then be viewed
by `index.html` which has some JS/CSS to make the story styling consistent and readable.
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.

## Usage Instructions

1. **Run the `story-book.gpt` script.**
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).

In the same terminal session where the virtual environment (venv) is now activated, navigate to the `story-book` example directory and run the `story-book.gpt` script:
2. Navigate to the `examples/story-book` directory.

```shell
cd examples/story-book
gptscript story-book.gpt --prompt "Goldilocks" --pages 3
```
```bash
cd examples/story-book
```

2. **View the story.**
3. Start the Nuxt application by running the following commands:

Open `index.html` in your browser to view the generated story.
```bash
npm i
npm run dev
```

3. (optional) **Generate a new story.**

To generate another story, you'll first need to delete the existing `pages` directory. In the `examples/story-book` directory, run the following command:

```shell
rm -rf pages
```

After that, you can generate a new story by running the `story-book.gpt` script again with a different prompt or number of pages.

```shell
gptscript story-book.gpt --prompt "The Three Little Pigs" --pages 5
```
4. Navigate to `http://localhost:3000` in your browser.
25 changes: 25 additions & 0 deletions examples/story-book/app.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<script lang="ts" setup>
useHead({
title: 'Story Book',
meta: [
{ charset: 'utf-8' },
{ name: 'apple-mobile-web-app-capable', content: 'yes' },
{ name: 'format-detection', content: 'telephone=no' },
{ name: 'viewport', content: `width=device-width` },
],
})
</script>

<template>
<div>
<div class="grid grid-rows-[1fc,1fr] fixed top-0 right-0 bottom-0 left-0">
<div class="overflow-auto border-t-2 border-transparent">
<Nav class="absolute right-5 top-5 z-10"/>
<div class="p-5 lg:p-10 max-w-full w-full h-full mx-auto">
<NuxtPage class="pb-10" />
</div>
</div>
</div>
<UNotifications />
</div>
</template>
25 changes: 25 additions & 0 deletions examples/story-book/components/DisplayMode.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<script setup lang="ts">
const colorMode = useColorMode()
const isDark = computed({
get() {
return colorMode.value === 'dark'
},
set() {
colorMode.preference = colorMode.value === 'dark' ? 'light' : 'dark'
},
})
</script>

<template>
<ClientOnly>
<UButton
:icon="isDark ? 'i-heroicons-moon' : 'i-heroicons-sun'"
aria-label="Theme"
v-bind="$attrs"
@click="isDark = !isDark"
/>
<template #fallback>
<div class="w-8 h-8" />
</template>
</ClientOnly>
</template>
27 changes: 27 additions & 0 deletions examples/story-book/components/Nav.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<script setup lang="ts">
const isMenuOpen = ref(false)
</script>

<template>
<div>
<div class="flex space-x-4">
<DisplayMode />
<UButton icon="i-heroicons-home" @click="() => { useRouter().push('/'); isMenuOpen = false }" />
<UButton icon="i-heroicons-bars-3" @click="isMenuOpen = true" />
</div>

<USlideover v-model="isMenuOpen">
<UCard class="flex flex-col flex-1" :ui="{ body: { base: 'flex-1' }, ring: '', divide: 'divide-y divide-gray-100 dark:divide-gray-800' }">
<template #header>
<div class="flex items-center justify-between">
<h3 class="text-4xl">Story Book</h3>
<div class="flex space-x-4">
<UButton icon="i-heroicons-x-mark-20-solid" @click="isMenuOpen = false" />
</div>
</div>
</template>
<Stories />
</UCard>
</USlideover>
</div>
</template>
94 changes: 94 additions & 0 deletions examples/story-book/components/New.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
<script setup lang="ts">
import { useMainStore } from '@/store'

const store = useMainStore()
const open = ref(false)
const pageOptions = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
const toast = useToast()
const state = reactive({
prompt: '',
pages: 0,
})

async function onSubmit () {
const response = await fetch('/api/story', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(state)
})

if (response.ok) {
toast.add({
id: 'story-generating',
title: 'Story Generating',
description: 'Your story is being generated. Depending on the length, this may take a few minutes.',
icon: 'i-heroicons-pencil-square-solid',
})

const es = new EventSource(`/api/story/sse?prompt=${state.prompt}`)
es.onmessage = async (event) => {
store.addPendingStoryMessage(state.prompt, event.data)
if ((event.data as string) === 'done') {
es.close()
store.removePendingStory(state.prompt)
store.fetchStories()
toast.add({
id: 'story-created',
title: 'Story Created',
description: 'A story you requested has been created.',
icon: 'i-heroicons-check-circle-solid',
})
} else if ((event.data as string).includes('error')) {
es.close()
store.removePendingStory(state.prompt)

toast.add({
id: 'story-generating-failed',
title: 'Story Generation Failed',
description: `Your story could not be generated due to an error.\n\n${event.data}.`,
icon: 'i-heroicons-x-mark',
timeout: 30000,
})
}
}

if (state.prompt.length){
store.addPendingStory(state.prompt, es)
}
open.value = false
} else {
toast.add({
id: 'story-generating-failed',
title: 'Story Generation Failed',
description: `Your story could not be generated due to an error: ${response.statusText}.`,
icon: 'i-heroicons-x-mark',
})
}
}
</script>


<template>
<UButton size="lg" class="w-full text-xl" icon="i-heroicons-plus" @click="open = true">New Story</UButton>
<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', }">
<UCard class="h-full">
<template #header>
<div class="flex items-center justify-between">
<h1 class="text-2xl">Create a new story</h1>
<UButton color="gray" variant="ghost" icon="i-heroicons-x-mark-20-solid" class="-my-1" @click="open = false" />
</div>
</template>
<div >
<UForm class="h-full" :state=state @submit="onSubmit">
<UFormGroup size="lg" class="mb-6" label="Pages" name="pages">
<USelectMenu class="w-1/4 md:w-1/6"v-model="state.pages" :options="pageOptions"/>
</UFormGroup>
<UFormGroup class="my-6" label="Story" name="prompt">
<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"/>
</UFormGroup>
<UButton size="xl" icon="i-heroicons-book-open" type="submit">Create Story</UButton>
</UForm>
</div>
</UCard>
</UModal>
</template>
62 changes: 62 additions & 0 deletions examples/story-book/components/Stories.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
<script setup lang="ts">
import { useMainStore } from '@/store'
import unmangleStoryName from '@/lib/unmangle'

const toast = useToast()
const store = useMainStore()

const pendingStories = computed(() => store.pendingStories)
const pendingStoryMessages = computed(() => store.pendingStoryMessages)
const stories = computed(() => store.stories)
onMounted(async () => store.fetchStories() )

const goToStory = (name: string) => {
useRouter().push(`/story/${name}`)
}

const deleteStory = async (name: string) => {
const response = await fetch(`/api/story/${name}`, { method: 'DELETE' })
if (response.ok) {
store.removeStory(name)
toast.add({
id: 'story-deleted',
title: `${name} deleted`,
description: 'The story has been deleted.',
icon: 'i-heroicons-trash',
})
} else {
toast.add({
id: 'story-delete-failed',
title: `Failed to delete ${name}`,
description: 'The story could not be deleted.',
icon: 'i-heroicons-x-mark',
})
}
}
</script>

<template>
<div class="flex flex-col space-y-2">
<New v-if="!Object.keys(pendingStories).length" class="w-full"/>
<UPopover mode="hover" v-for="(_, name) in pendingStories" >
<UButton truncate loading :key="name" size="lg" class="w-full text-xl" :label="name" icon="i-heroicons-book-open"/>

<template #panel>
<UCard class="p-4 w-[80vw] xl:w-[40vw]">
<h1 class="text-xl">Writing the perfect story...</h1>
<h2 class="text-zinc-400 mb-4">GPTScript is currently building the story you requested. You can see its progress below.</h2>
<pre class="h-[26vh] bg-zinc-950 px-6 text-white overflow-x-scroll rounded shadow">
<p v-for="message in pendingStoryMessages[name]">> {{ message }}</p>
</pre>
</UCard>
</template>
</UPopover>

<div class="w-full" v-for="story in stories" >
<!-- The LLM likes to ocassionally, not always, generate folders with "-" and other times with " ", handle both cases for the button -->
<UButton truncate :key="story" size="lg" class="w-5/6 text-xl" :label="unmangleStoryName(story)" icon="i-heroicons-book-open" @click="goToStory(story)"/>
<UButton size="lg" variant="ghost" class="w-1/7 text-xl ml-4" icon="i-heroicons-trash" @click="deleteStory(story)"/>
</div>

</div>
</template>
Loading