diff --git a/examples/story-book/.gitignore b/examples/story-book/.gitignore new file mode 100644 index 00000000..b8a88f6c --- /dev/null +++ b/examples/story-book/.gitignore @@ -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 diff --git a/examples/story-book/README.md b/examples/story-book/README.md index aa1ddf99..4cacd008 100644 --- a/examples/story-book/README.md +++ b/examples/story-book/README.md @@ -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. \ No newline at end of file diff --git a/examples/story-book/app.vue b/examples/story-book/app.vue new file mode 100644 index 00000000..38414fbe --- /dev/null +++ b/examples/story-book/app.vue @@ -0,0 +1,25 @@ + + + diff --git a/examples/story-book/components/DisplayMode.vue b/examples/story-book/components/DisplayMode.vue new file mode 100644 index 00000000..396697e6 --- /dev/null +++ b/examples/story-book/components/DisplayMode.vue @@ -0,0 +1,25 @@ + + + diff --git a/examples/story-book/components/Nav.vue b/examples/story-book/components/Nav.vue new file mode 100644 index 00000000..934897b8 --- /dev/null +++ b/examples/story-book/components/Nav.vue @@ -0,0 +1,27 @@ + + + \ No newline at end of file diff --git a/examples/story-book/components/New.vue b/examples/story-book/components/New.vue new file mode 100644 index 00000000..4f9532a6 --- /dev/null +++ b/examples/story-book/components/New.vue @@ -0,0 +1,94 @@ + + + + \ No newline at end of file diff --git a/examples/story-book/components/Stories.vue b/examples/story-book/components/Stories.vue new file mode 100644 index 00000000..1a70a0a1 --- /dev/null +++ b/examples/story-book/components/Stories.vue @@ -0,0 +1,62 @@ + + + \ No newline at end of file diff --git a/examples/story-book/index.html b/examples/story-book/index.html deleted file mode 100644 index e8bf7ebd..00000000 --- a/examples/story-book/index.html +++ /dev/null @@ -1,147 +0,0 @@ - - - - Story Book - - - -

Story Book

-
- -
- - - - - - - \ No newline at end of file diff --git a/examples/story-book/lib/types.ts b/examples/story-book/lib/types.ts new file mode 100644 index 00000000..533463e9 --- /dev/null +++ b/examples/story-book/lib/types.ts @@ -0,0 +1,6 @@ +export type Pages = Record; + +export type Page = { + image_path: string; + content: string; +} diff --git a/examples/story-book/lib/unmangle.ts b/examples/story-book/lib/unmangle.ts new file mode 100644 index 00000000..e639f193 --- /dev/null +++ b/examples/story-book/lib/unmangle.ts @@ -0,0 +1,4 @@ +const unmangleStoryName = (storyName: string): string => { + return storyName.replaceAll('-', ' ').replace(/\b\w/g, c => c.toUpperCase()); +} +export default unmangleStoryName; \ No newline at end of file diff --git a/examples/story-book/nuxt.config.ts b/examples/story-book/nuxt.config.ts new file mode 100644 index 00000000..7a8bc507 --- /dev/null +++ b/examples/story-book/nuxt.config.ts @@ -0,0 +1,9 @@ +// https://nuxt.com/docs/api/configuration/nuxt-config +export default defineNuxtConfig({ + devtools: { enabled: true }, + modules: [ + '@nuxt/ui', + '@nuxtjs/tailwindcss', + '@pinia/nuxt', + ], +}) diff --git a/examples/story-book/package.json b/examples/story-book/package.json new file mode 100644 index 00000000..0f79be49 --- /dev/null +++ b/examples/story-book/package.json @@ -0,0 +1,22 @@ +{ + "name": "nuxt-app", + "private": true, + "type": "module", + "scripts": { + "build": "nuxt build", + "dev": "nuxt dev", + "generate": "nuxt generate", + "preview": "nuxt preview", + "postinstall": "nuxt prepare" + }, + "dependencies": { + "@gptscript-ai/gptscript": "^0.2.0", + "@nuxt/ui": "^2.15.0", + "@pinia/nuxt": "^0.5.1", + "jspdf": "^2.5.1", + "nuxt": "^3.11.1", + "pinia": "^2.1.7", + "vue": "^3.4.21", + "vue-router": "^4.3.0" + } +} diff --git a/examples/story-book/pages/index.vue b/examples/story-book/pages/index.vue new file mode 100644 index 00000000..832a40a2 --- /dev/null +++ b/examples/story-book/pages/index.vue @@ -0,0 +1,15 @@ + + + \ No newline at end of file diff --git a/examples/story-book/pages/story/[name].vue b/examples/story-book/pages/story/[name].vue new file mode 100644 index 00000000..10ae69c9 --- /dev/null +++ b/examples/story-book/pages/story/[name].vue @@ -0,0 +1,95 @@ + + + \ No newline at end of file diff --git a/examples/story-book/public/favicon.ico b/examples/story-book/public/favicon.ico new file mode 100644 index 00000000..18993ad9 Binary files /dev/null and b/examples/story-book/public/favicon.ico differ diff --git a/examples/story-book/server/api/story/[name].delete.ts b/examples/story-book/server/api/story/[name].delete.ts new file mode 100644 index 00000000..51aadb1e --- /dev/null +++ b/examples/story-book/server/api/story/[name].delete.ts @@ -0,0 +1,30 @@ +import fs from 'fs' + +export default defineEventHandler(async (event) => { + try { + let name = getRouterParam(event, 'name') + if (!name) { + throw createError({ + statusCode: 400, + statusMessage: 'name is required' + }); + } + + name = decodeURIComponent(name); + + await fs.promises.readdir(`public/stories/${name}`) + fs.promises.rm(`public/stories/${name}`, { recursive: true }) + } catch (error) { + // if the error is a 404 error, we can throw it directly + if ((error as any).code === 'ENOENT') { + throw createError({ + statusCode: 404, + statusMessage: 'story not found', + }) + } + throw createError({ + statusCode: 500, + statusMessage: `error removing story: ${error}`, + }) + } +}) \ No newline at end of file diff --git a/examples/story-book/server/api/story/[name].get.ts b/examples/story-book/server/api/story/[name].get.ts new file mode 100644 index 00000000..bf6593d8 --- /dev/null +++ b/examples/story-book/server/api/story/[name].get.ts @@ -0,0 +1,47 @@ +import fs from 'fs' + +type Pages = Record; +type Page = { + image_path: string; + content: string; +} + +export default defineEventHandler(async (event) => { + try { + let name = getRouterParam(event, 'name'); + if (!name) { + throw createError({ + statusCode: 400, + statusMessage: 'name is required' + }); + } + + name = decodeURIComponent(name); + + const files = await fs.promises.readdir(`public/stories/${name}`) + + const pages: Pages = {}; + for (const file of files) { + if (!file.endsWith('.txt')) continue + const page = await fs.promises.readFile(`public/stories/${name}/${file}`, 'utf-8') + pages[ file.replace('.txt', '').replace('page', '')] = { + image_path: `/stories/${name}/${file.replace('.txt', '.png')}`, + content: page + } + } + + return pages + } catch (error) { + // if the error is a 404 error, we can throw it directly + if ((error as any).code === 'ENOENT') { + throw createError({ + statusCode: 404, + statusMessage: 'story found', + }) + } + throw createError({ + statusCode: 500, + statusMessage: `error fetching story: ${error}`, + }) + } +}) \ No newline at end of file diff --git a/examples/story-book/server/api/story/index.get.ts b/examples/story-book/server/api/story/index.get.ts new file mode 100644 index 00000000..63e101fd --- /dev/null +++ b/examples/story-book/server/api/story/index.get.ts @@ -0,0 +1,20 @@ +import fs from 'fs' + +export default defineEventHandler(async (event) => { + try { + const stories = await fs.promises.readdir('public/stories') + return stories + } catch (error) { + // if the error is a 404 error, we can throw it directly + if ((error as any).code === 'ENOENT') { + throw createError({ + statusCode: 404, + statusMessage: 'no stories found', + }) + } + throw createError({ + statusCode: 500, + statusMessage: `error fetching stories: ${error}`, + }) + } +}) \ No newline at end of file diff --git a/examples/story-book/server/api/story/index.post.ts b/examples/story-book/server/api/story/index.post.ts new file mode 100644 index 00000000..88ba57c4 --- /dev/null +++ b/examples/story-book/server/api/story/index.post.ts @@ -0,0 +1,50 @@ +import gptscript from '@gptscript-ai/gptscript' +import { Readable } from 'stream' + +type Request = { + prompt: string; + pages: number; +} + +export type RunningScript = { + stdout: Readable; + stderr: Readable; + promise: Promise; +} + +export const runningScripts: Record= {} + +export default defineEventHandler(async (event) => { + const request = await readBody(event) as Request + + if (!request.prompt) { + throw createError({ + statusCode: 400, + statusMessage: 'prompt is required' + }); + } + + if (!request.pages) { + throw createError({ + statusCode: 400, + statusMessage: 'pages are required' + }); + } + + const {stdout, stderr, promise} = await gptscript.streamExecFile('story-book.gpt', `--story ${request.prompt} --pages ${request.pages}`, {}) + + setHeaders(event,{ + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Credentials': 'true', + 'Connection': 'keep-alive', + 'Content-Type': 'text/event-stream', + }) + + setResponseStatus(event, 202) + + runningScripts[request.prompt] = { + stdout: stdout, + stderr: stderr, + promise: promise + } +}) \ No newline at end of file diff --git a/examples/story-book/server/api/story/sse.get.ts b/examples/story-book/server/api/story/sse.get.ts new file mode 100644 index 00000000..00a1e4c3 --- /dev/null +++ b/examples/story-book/server/api/story/sse.get.ts @@ -0,0 +1,55 @@ +import { runningScripts } from '@/server/api/story/index.post'; + +export default defineEventHandler(async (event) => { + const { prompt } = getQuery(event); + if (!prompt) { + throw createError({ + statusCode: 400, + statusMessage: 'prompt is required' + }); + } + + const runningScript = runningScripts[prompt as string]; + if (!runningScript) { + throw createError({ + statusCode: 404, + statusMessage: 'running script not found' + }); + } + + setResponseStatus(event, 200); + setHeaders(event, { + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Credentials': 'true', + 'Connection': 'keep-alive', + 'Content-Type': 'text/event-stream', + }); + + let stdoutBuffer = ''; + runningScript.stdout.on('data', (data) => { + stdoutBuffer += data; + if (data.includes('\n')) { + event.node.res.write(`data: ${stdoutBuffer}\n\n`); + stdoutBuffer = ''; + } + }); + + let stderrBuffer = ''; + runningScript.stderr.on('data', (data) => { + stderrBuffer += data; + if (data.includes('\n')) { + event.node.res.write(`data: ${stderrBuffer}\n\n`); + stderrBuffer = ''; + } + }); + + event._handled = true; + await runningScript.promise.then(() => { + event.node.res.write('data: done\n\n'); + event.node.res.end(); + }).catch((error) => { + setResponseStatus(event, 500); + event.node.res.write(`data: error: ${error}\n\n`); + event.node.res.end(); + }); +}); \ No newline at end of file diff --git a/examples/story-book/server/tsconfig.json b/examples/story-book/server/tsconfig.json new file mode 100644 index 00000000..b9ed69c1 --- /dev/null +++ b/examples/story-book/server/tsconfig.json @@ -0,0 +1,3 @@ +{ + "extends": "../.nuxt/tsconfig.server.json" +} diff --git a/examples/story-book/store/index.ts b/examples/story-book/store/index.ts new file mode 100644 index 00000000..18cdf2ac --- /dev/null +++ b/examples/story-book/store/index.ts @@ -0,0 +1,50 @@ +// store/index.ts +import { defineStore } from 'pinia' + +export const useMainStore = defineStore({ + id: 'main', + state: () => ({ + pendingStories: {} as Record, + pendingStoryMessages: {} as Record, + stories: [] as string[] + }), + actions: { + addPendingStory(name: string, es: EventSource) { + this.pendingStories[name] = es + }, + removePendingStory(name: string) { + this.pendingStories[name].close() + delete this.pendingStories[name] + }, + addPendingStoryMessage(name: string, message: string) { + // implement a queue with a length of 10 that removes the oldest message when the length is reached + if (!this.pendingStoryMessages[name]) { + this.pendingStoryMessages[name] = [] + } + this.pendingStoryMessages[name].push(message) + if (this.pendingStoryMessages[name].length > 12) { + this.pendingStoryMessages[name].shift() + } + }, + addStory(name: string) { + this.stories.push(name) + }, + addStories(names: string[]) { + // only push if the story is not already in the list + names.forEach(name => { + if (!this.stories.includes(name)) { + this.stories.push(name) + } + }) + }, + removeStory(name: string) { + this.stories = this.stories.filter(s => s !== name) + }, + async fetchStories() { + if (Object.keys(this.pendingStories).length === 0) { + this.addStories(await $fetch('/api/story') as string[]) + } + } + + } +}) \ No newline at end of file diff --git a/examples/story-book/story-book.gpt b/examples/story-book/story-book.gpt index a2e8d6d2..57d80db8 100644 --- a/examples/story-book/story-book.gpt +++ b/examples/story-book/story-book.gpt @@ -5,45 +5,23 @@ args: pages: The number of pages to generate Do the following steps sequentially: -1. Create the `pages` directory if it does not already exist. -2. If ${story} is a prompt and not a complete children's story, call story-writer +1. Come up with an appropriate title for the story based on the ${prompt} +2. Create the `public/stories/${story-title}` directory if it does not already exist. +3. If ${story} is a prompt and not a complete children's story, call story-writer to write a story based on the prompt. -3. Take ${story} and break it up into ${pages} logical "pages" of text. -4. For each page: +4. Take ${story} and break it up into ${pages} logical "pages" of text. +5. For each page: + - For the content of the page, write it to `public/stories/${story-title}/page.txt and add appropriate newline + characters. - Call story-illustrator to illustrate it. Be sure to include any relevant characters to include when asking it to illustrate the page. - - Download the illustration to a file at pages/.png. -5. For each page and its illustration write an html file with the text on top and image below it to pages/page.html. - Assume the illustration file is a sibling to the html file, Add this style tag to the HTML file: - ```html - - ``` -6. Edit the "pages" variable array in index.html to serve the pages you created. Do not - edit anything else. Do not edit the page select field. + - Download the illustration to a file at `public/stories/${story-title}/page.png`. --- name: story-writer description: Writes a story for children args: prompt: The prompt to use for the story +temperature: 1 Write a story with a tone for children based on ${prompt}. @@ -62,8 +40,8 @@ children's book style with no text in it". Only return the URL of the illustrati name: mkdir tools: sys.write description: Creates a specified directory -args: dir: Path to the directory to be created +args: dir: Path to the directory to be created. Will create parent directories. #!bash -mkdir ${dir} +mkdir -p "${dir}" diff --git a/examples/story-book/tailwind.config.ts b/examples/story-book/tailwind.config.ts new file mode 100644 index 00000000..63da7b5e --- /dev/null +++ b/examples/story-book/tailwind.config.ts @@ -0,0 +1,20 @@ +import type { Config } from 'tailwindcss' +import defaultTheme from 'tailwindcss/defaultTheme' +import typography from '@tailwindcss/typography' + +export default > { + content: [ + './src/components/**/*.{js,vue,ts}', + './src/layouts/**/*.vue', + './src/pages/**/*.vue', + './src/plugins/**/*.{js,ts}', + './src/app.vue', + './src/error.vue', + ], + + darkMode: 'class', + + plugins: [ + typography, + ], +} diff --git a/examples/story-book/tsconfig.json b/examples/story-book/tsconfig.json new file mode 100644 index 00000000..a746f2a7 --- /dev/null +++ b/examples/story-book/tsconfig.json @@ -0,0 +1,4 @@ +{ + // https://nuxt.com/docs/guide/concepts/typescript + "extends": "./.nuxt/tsconfig.json" +}