├── .env.example ├── .npmrc ├── .vscode └── settings.json ├── public ├── bot.jpg ├── avatar.jpg └── favicon.ico ├── .gitignore ├── tailwind.config.ts ├── tsconfig.json ├── nuxt.config.ts ├── types └── index.ts ├── app.vue ├── agents ├── facebookAgent.ts ├── index.ts ├── twitterAgent.ts └── customerSupportAgent.ts ├── utils └── index.ts ├── package.json ├── requests.http ├── components ├── IconChat.vue ├── SocialMediaPostGenerator.vue ├── UrlForm.vue ├── TemperatureSelector.vue ├── CardFacebook.vue ├── CardTwitter.vue ├── CardGeneric.vue ├── AppLoading.vue ├── ChatBubble.vue ├── ChatWidget.vue └── ChatBox.vue ├── README.md ├── server └── api │ └── ai.post.ts └── composables └── useChatAi.ts /.env.example: -------------------------------------------------------------------------------- 1 | NUXT_OPENAI_API_KEY="sk-xxx" -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | shamefully-hoist=true 2 | strict-peer-dependencies=false 3 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "dotenv.enableAutocloaking": true 3 | } 4 | -------------------------------------------------------------------------------- /public/bot.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vueschool/ai-chatbot-course/HEAD/public/bot.jpg -------------------------------------------------------------------------------- /public/avatar.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vueschool/ai-chatbot-course/HEAD/public/avatar.jpg -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vueschool/ai-chatbot-course/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.log* 3 | .nuxt 4 | .nitro 5 | .cache 6 | .output 7 | .env 8 | dist 9 | .DS_Store -------------------------------------------------------------------------------- /tailwind.config.ts: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: [require("daisyui"), require("@tailwindcss/typography")], 3 | }; 4 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | // https://nuxt.com/docs/guide/concepts/typescript 3 | "extends": "./.nuxt/tsconfig.json" 4 | } 5 | -------------------------------------------------------------------------------- /nuxt.config.ts: -------------------------------------------------------------------------------- 1 | // https://nuxt.com/docs/api/configuration/nuxt-config 2 | export default defineNuxtConfig({ 3 | modules: ["@nuxtjs/tailwindcss", "@vueuse/nuxt"], 4 | ssr: false, 5 | 6 | runtimeConfig: { 7 | OPENAI_API_KEY: "", 8 | }, 9 | 10 | compatibilityDate: "2024-12-10", 11 | }); -------------------------------------------------------------------------------- /types/index.ts: -------------------------------------------------------------------------------- 1 | export interface User { 2 | id: string; 3 | avatar: string; 4 | name: string; 5 | } 6 | export interface Message { 7 | id: string; 8 | userId: string; 9 | createdAt: Date; 10 | text: string; 11 | } 12 | export type AsyncState = null | "loading" | "error" | "complete"; 13 | -------------------------------------------------------------------------------- /app.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /agents/facebookAgent.ts: -------------------------------------------------------------------------------- 1 | import createAgent from "."; 2 | 3 | export const facebookAgent = createAgent((context) => { 4 | return { 5 | messages: [ 6 | { 7 | role: "system", 8 | content: 9 | "You are a friendly social media influencer sharing a new blog post", 10 | }, 11 | { 12 | role: "user", 13 | content: `Create a facebook post to hype the following article: ${context.url}. Use line breaks for easy reading`, 14 | }, 15 | ], 16 | }; 17 | }); 18 | -------------------------------------------------------------------------------- /utils/index.ts: -------------------------------------------------------------------------------- 1 | import type { NitroFetchOptions } from "nitropack"; 2 | export async function fetchWithTimeout( 3 | url: string, 4 | fetchOptions: NitroFetchOptions = {} 5 | ): Promise { 6 | const controller = new AbortController(); 7 | const id = setTimeout(() => { 8 | controller.abort(); 9 | throw new Error("Requet timed out"); 10 | }, 15_000); 11 | const res = await $fetch(url, { 12 | ...fetchOptions, 13 | signal: controller.signal, 14 | }); 15 | clearTimeout(id); 16 | return res; 17 | } 18 | -------------------------------------------------------------------------------- /agents/index.ts: -------------------------------------------------------------------------------- 1 | // register all agent files here 2 | export * from "./customerSupportAgent"; 3 | export * from "./facebookAgent"; 4 | export * from "./twitterAgent"; 5 | 6 | // and register types here 7 | export type Agent = "facebook" | "twitter" | "customerSupport"; 8 | 9 | // util function for creating trainings with proper typing 10 | import type OpenAI from "openai"; 11 | export default function createAgent( 12 | agent: (context: Record) => Partial 13 | ) { 14 | return agent; 15 | } 16 | -------------------------------------------------------------------------------- /agents/twitterAgent.ts: -------------------------------------------------------------------------------- 1 | import createAgent from "."; 2 | 3 | export const twitterAgent = createAgent((context) => { 4 | return { 5 | messages: [ 6 | { 7 | role: "system", 8 | content: 9 | "You are an exciting social media influencer sharing a new blog post", 10 | }, 11 | { 12 | role: "user", 13 | content: `Create a tweet about the following article: ${context.url}. Use line breaks for easy reading. MUST be shorter than 280 characters! MUST include URL`, 14 | }, 15 | ], 16 | max_tokens: 500, 17 | }; 18 | }); 19 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "build": "nuxt build", 5 | "dev": "nuxt dev", 6 | "generate": "nuxt generate", 7 | "preview": "nuxt preview", 8 | "postinstall": "nuxt prepare" 9 | }, 10 | "devDependencies": { 11 | "@tailwindcss/typography": "^0.5.9", 12 | "nuxt": "^3.14.1592", 13 | "rollup": "3.18.0" 14 | }, 15 | "dependencies": { 16 | "@nuxtjs/tailwindcss": "^6.6.4", 17 | "@vueuse/nuxt": "^9.13.0", 18 | "daisyui": "^2.51.5", 19 | "nanoid": "^4.0.1", 20 | "openai": "^4.0.0", 21 | "vue3-markdown-it": "^1.0.10" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /requests.http: -------------------------------------------------------------------------------- 1 | ### Direct request to Open AI Chat API 2 | 3 | POST https://api.openai.com/v1/chat/completions 4 | Content-Type: application/json 5 | Authorization: Bearer {{$dotenv NUXT_OPENAI_API_KEY}} 6 | 7 | { 8 | "model": "gpt-3.5-turbo", 9 | "messages": [{"role": "user", "content": "Say this is a test!"}], 10 | "temperature": 0, 11 | "n": 1 12 | } 13 | 14 | ### 15 | 16 | POST http://localhost:3000/api/ai 17 | Content-Type: application/json 18 | 19 | { 20 | "agent": "facebookAgent", 21 | "url": "https://vueschool.io/articles/vuejs-tutorials/sorting-filtering-and-paginating-data-from-a-laravel-backend-in-a-vue-js-spa/" 22 | } -------------------------------------------------------------------------------- /components/IconChat.vue: -------------------------------------------------------------------------------- 1 | 2 | 10 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /components/SocialMediaPostGenerator.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | Social Media Post Generator 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /components/UrlForm.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 14 | 15 | 21 | Generate Announcements 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /components/TemperatureSelector.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 24 | Temperature: {{ modelValue }} 25 | 26 | - More random, creative, and risky 28 | - More focused, deterministic, and safe 29 | 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Nuxt 3 Minimal Starter 2 | 3 | Look at the [Nuxt 3 documentation](https://nuxt.com/docs/getting-started/introduction) to learn more. 4 | 5 | ## Setup 6 | 7 | Make sure to install the dependencies: 8 | 9 | ```bash 10 | # yarn 11 | yarn install 12 | 13 | # npm 14 | npm install 15 | 16 | # pnpm 17 | pnpm install 18 | ``` 19 | 20 | ## Development Server 21 | 22 | Start the development server on http://localhost:3000 23 | 24 | ```bash 25 | npm run dev 26 | ``` 27 | 28 | ## Production 29 | 30 | Build the application for production: 31 | 32 | ```bash 33 | npm run build 34 | ``` 35 | 36 | Locally preview production build: 37 | 38 | ```bash 39 | npm run preview 40 | ``` 41 | 42 | Check out the [deployment documentation](https://nuxt.com/docs/getting-started/deployment) for more information. 43 | -------------------------------------------------------------------------------- /server/api/ai.post.ts: -------------------------------------------------------------------------------- 1 | import OpenAI from "openai"; 2 | import * as agents from "@/agents"; 3 | 4 | export default defineEventHandler(async (event) => { 5 | const body = await readBody(event); 6 | const agent = body.agent || "customerSupportAgent"; 7 | 8 | if (!Object.keys(agents).includes(agent)) { 9 | throw new Error(`${agent} doesn't exist`); 10 | } 11 | 12 | const { OPENAI_API_KEY } = useRuntimeConfig(); 13 | 14 | const openai = new OpenAI({ 15 | apiKey: OPENAI_API_KEY, 16 | }); 17 | 18 | const completion = await openai.chat.completions.create({ 19 | model: "gpt-3.5-turbo", 20 | messages: body.messages || [], 21 | temperature: body.temperature || 1, 22 | // @ts-expect-error checking above if the agent exists 23 | ...agents[agent](body), 24 | }); 25 | return completion; 26 | }); 27 | -------------------------------------------------------------------------------- /components/CardFacebook.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 28 | Regenerate 29 | 30 | Copy Announcement and Post 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /components/CardTwitter.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 28 | 29 | 30 | Character Count: 31 | {{ announcement?.length }} 32 | 33 | 34 | Regenerate 35 | Post 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /components/CardGeneric.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 14 | 15 | 16 | {{ title }} 17 | 18 | 19 | Error generating announcement 20 | 21 | 22 | 23 | 24 | 25 | {{ body }} 26 | 27 | 28 | Import an article to generate an announcement 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /components/AppLoading.vue: -------------------------------------------------------------------------------- 1 | 2 | 13 | 14 | 21 | 22 | 23 | 30 | 31 | 32 | 39 | 40 | 41 | 42 | 43 | 52 | -------------------------------------------------------------------------------- /components/ChatBubble.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | {{ user?.name }} 26 | {{ 27 | useTimeAgo(message?.createdAt).value 28 | }} 29 | 30 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 49 | -------------------------------------------------------------------------------- /composables/useChatAi.ts: -------------------------------------------------------------------------------- 1 | import type { Agent } from "@/agents"; 2 | import type { AsyncState } from "@/types"; 3 | import type OpenAI from "openai"; 4 | 5 | export const useChatAi = ({ agent }: { agent: Agent }) => { 6 | const state = ref(null); 7 | const error = ref(); 8 | const res = ref(); 9 | 10 | const usage = computed(() => res.value?.usage); 11 | const choices = computed(() => res.value?.choices || []); 12 | const hasChoices = computed(() => choices.value.length); 13 | const firstChoice = computed(() => choices.value.at(0)); 14 | const firstMessage = computed(() => firstChoice.value?.message); 15 | 16 | async function chat(options: Record) { 17 | try { 18 | state.value = "loading"; 19 | 20 | const result = await fetchWithTimeout( 21 | `/api/ai`, 22 | { 23 | method: "POST", 24 | body: { 25 | ...options, 26 | agent: `${agent}Agent`, 27 | }, 28 | } 29 | ); 30 | if (!result.choices || !result.usage) { 31 | throw new Error("Invalid AI response"); 32 | } 33 | 34 | res.value = result; 35 | state.value = "complete"; 36 | return res.value; 37 | } catch (err) { 38 | state.value = "error"; 39 | error.value = err; 40 | } 41 | } 42 | 43 | return { 44 | state, 45 | chat, 46 | choices, 47 | usage, 48 | firstChoice, 49 | hasChoices, 50 | firstMessage, 51 | res, 52 | }; 53 | }; 54 | -------------------------------------------------------------------------------- /components/ChatWidget.vue: -------------------------------------------------------------------------------- 1 | 56 | 57 | 64 | 65 | -------------------------------------------------------------------------------- /agents/customerSupportAgent.ts: -------------------------------------------------------------------------------- 1 | import createAgent from "."; 2 | 3 | export const customerSupportAgent = createAgent((context) => { 4 | return { 5 | messages: [ 6 | /** 7 | * Train bot to only respond to app specific questions 8 | */ 9 | { 10 | role: "system", 11 | content: `You are a helpful customer support agent for the 'Social Media Post Generator' application. 12 | This software takes an article URL and makes an announcement. Don't answer any question not related to the 'Social Media Post Generator' application.`, 13 | }, 14 | { 15 | role: "user", 16 | content: `If I ask any question NOT related to the 17 | 'Social Media Post Generator' application, DO NOT answer the question at all. 18 | Instead politely decline. 19 | `, 20 | }, 21 | { 22 | role: "assistant", 23 | content: 24 | "Ok, I will ONLY answer questions and requests related to the 'Social Media Post Generator' application. I will politely decline to answer all others.", 25 | }, 26 | 27 | /** 28 | * Train bot with app specific information 29 | */ 30 | 31 | // email 32 | { role: "user", content: "What's your email address" }, 33 | { role: "assistant", content: "support@test.com" }, 34 | 35 | // tech used 36 | { 37 | role: "user", 38 | content: "How is 'Social Media Post Generator' built?", 39 | }, 40 | { role: "assistant", content: "With GPT-3 and Vue.js! " }, 41 | 42 | // human support 43 | { role: "user", content: "Is support available 24/7" }, 44 | { 45 | role: "assistant", 46 | content: 47 | "No, but email us at support@test.com and we will respond within 1 business day", 48 | }, 49 | 50 | // how to use 51 | { role: "user", content: "Can I import posts from a URL" }, 52 | { 53 | role: "assistant", 54 | content: 55 | "Yes click the import from URL button at the top of the article page", 56 | }, 57 | 58 | // create a tweet 59 | { 60 | role: "user", 61 | content: "Can you create a tweet for this article: {any url here}", 62 | }, 63 | { 64 | role: "assistant", 65 | content: 66 | "{insert post text here}. \n [Share on Twitter](https://twitter.com/intent/tweet?text={insert post text here})", 67 | }, 68 | ...context.messages, 69 | ], 70 | temperature: 0, 71 | }; 72 | }); 73 | -------------------------------------------------------------------------------- /components/ChatBox.vue: -------------------------------------------------------------------------------- 1 | 47 | 48 | 49 | 50 | 51 | 52 | 56 | 57 | 60 | Customer Support Chat 61 | 62 | 70 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | Chat with Botman! 82 | Our A.I. powered assistant 83 | Go ahead and ask us something: 84 | 85 | What is social media post generator? 86 | How can I get human support? 87 | How was this tool built? 88 | 89 | 90 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 127 | 128 | 129 | 130 | --------------------------------------------------------------------------------
{{ body }}
Our A.I. powered assistant