├── public ├── _robots.txt ├── favicon.ico └── images │ ├── icon-green.png │ ├── nuxt-og-image.png │ └── nuxt-og-image-1.png ├── .npmrc ├── .dockerignore ├── types ├── index.ts └── generalTypes.ts ├── server ├── utils │ ├── index.ts │ └── sendStream.ts ├── tsconfig.json ├── middleware │ └── openai.ts └── api │ ├── chat.ts │ └── ws │ └── chat.ts ├── .vscode └── settings.json ├── .env.example ├── tailwind.config.ts ├── app.config.ts ├── tsconfig.json ├── .gitignore ├── .prettierignore ├── composables ├── useParsedEscapedString.ts ├── useChat.ts └── useChatStream.ts ├── app.vue ├── components ├── Topbar.vue ├── Sidebar.vue └── ChatInput.vue ├── LICENSE.md ├── Dockerfile ├── pages ├── settings.vue └── index.vue ├── nuxt.config.ts ├── package.json ├── CONTRIBUTING.md ├── README.md └── CODE_OF_CONDUCT.md /public/_robots.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | shamefully-hoist=true -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .output 3 | .nuxt 4 | -------------------------------------------------------------------------------- /types/index.ts: -------------------------------------------------------------------------------- 1 | export * from './generalTypes' 2 | -------------------------------------------------------------------------------- /server/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './sendStream' 2 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "nuxt.isNuxtApp": false 3 | } 4 | -------------------------------------------------------------------------------- /server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../.nuxt/tsconfig.server.json" 3 | } 4 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | OPENAI_API_KEY= 2 | PINECONE_API_KEY= 3 | PINECONE_ENVIRONMENT= 4 | PINECONE_INDEX= 5 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daver987/nuxt-docu-search-ai/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /public/images/icon-green.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daver987/nuxt-docu-search-ai/HEAD/public/images/icon-green.png -------------------------------------------------------------------------------- /public/images/nuxt-og-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daver987/nuxt-docu-search-ai/HEAD/public/images/nuxt-og-image.png -------------------------------------------------------------------------------- /public/images/nuxt-og-image-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daver987/nuxt-docu-search-ai/HEAD/public/images/nuxt-og-image-1.png -------------------------------------------------------------------------------- /tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from 'tailwindcss' 2 | 3 | export default >{ 4 | content: ['docs/content/**/*.md'], 5 | } 6 | -------------------------------------------------------------------------------- /app.config.ts: -------------------------------------------------------------------------------- 1 | export default defineAppConfig({ 2 | ui: { 3 | icons: { 4 | dynamic: true 5 | }, 6 | primary: 'green', 7 | gray: 'slate', 8 | } 9 | }) 10 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | // https://nuxt.com/docs/guide/concepts/typescript 3 | "extends": "./.nuxt/tsconfig.json", 4 | "compilerOptions": { 5 | "lib": ["ESNext", "DOM"] 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Nuxt dev/build outputs 2 | .output 3 | .nuxt 4 | .nitro 5 | .cache 6 | dist 7 | 8 | # Node dependencies 9 | node_modules 10 | 11 | # Logs 12 | logs 13 | *.log 14 | 15 | # Misc 16 | .DS_Store 17 | .fleet 18 | .idea 19 | reference 20 | 21 | # Local env files 22 | .env 23 | .env.* 24 | !.env.example 25 | .aider* 26 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # Nuxt dev/build outputs 2 | .output 3 | .nuxt 4 | .nitro 5 | dist 6 | 7 | # Node dependencies 8 | node_modules 9 | 10 | # Logs 11 | logs 12 | *.log 13 | 14 | # Misc 15 | .DS_Store 16 | .fleet 17 | .idea 18 | app.config.ts 19 | public 20 | pnpm-lock.yaml 21 | 22 | 23 | # Local env files 24 | .env 25 | .env.* 26 | !.env.example 27 | -------------------------------------------------------------------------------- /composables/useParsedEscapedString.ts: -------------------------------------------------------------------------------- 1 | export function useParsedEscapedString(input: string): string { 2 | const lineFeedPattern = new RegExp('\\\\n', 'g') 3 | const singleQuotePattern = new RegExp("\\\\'", 'g') 4 | 5 | const newlineReplacement = `\n` 6 | const singleQuoteReplacement = `'` 7 | 8 | let intermediateResult = input.replace(lineFeedPattern, newlineReplacement) 9 | return intermediateResult.replace(singleQuotePattern, singleQuoteReplacement) 10 | } 11 | -------------------------------------------------------------------------------- /app.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 15 | 16 | 28 | -------------------------------------------------------------------------------- /server/middleware/openai.ts: -------------------------------------------------------------------------------- 1 | import OpenAI from 'openai' 2 | import type { H3EventContext } from 'h3' 3 | import { defineEventHandler } from 'h3' 4 | import { useRuntimeConfig } from '#imports' 5 | 6 | declare module 'h3' { 7 | interface H3EventContext { 8 | openai: OpenAI 9 | } 10 | } 11 | 12 | function initOpenai(apiKey: string) { 13 | return new OpenAI({ 14 | apiKey, 15 | }) 16 | } 17 | 18 | export default defineEventHandler((event) => { 19 | const config = useRuntimeConfig(event) 20 | const apiKey = config.OPENAI_API_KEY 21 | event.context.openai = initOpenai(apiKey) 22 | }) 23 | -------------------------------------------------------------------------------- /components/Topbar.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 33 | -------------------------------------------------------------------------------- /composables/useChat.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod' 2 | import type { FormSubmitEvent } from '#ui/types' 3 | 4 | const MessageSchema = z.object({ 5 | role: z.enum(['user', 'assistant', 'system']), 6 | content: z.string().min(2, 'Must be at least 2 characters'), 7 | id: z.number().optional(), 8 | }) 9 | 10 | type Message = z.infer 11 | 12 | export function useChat() { 13 | const input = useState('message', () => ({ 14 | role: 'user', 15 | content: '', 16 | })) 17 | const messages = useState('messages', () => []) 18 | const isLoading = ref(false) 19 | 20 | const addMessage = (message: Message) => { 21 | messages.value.push(message) 22 | } 23 | 24 | async function onSubmit(event: FormSubmitEvent) { 25 | isLoading.value = true 26 | addMessage({ ...event.data, id: messages.value.length }) 27 | const returnedMessage = await $fetch('/api/chat', { 28 | method: 'POST', 29 | body: event.data, 30 | }) 31 | } 32 | 33 | return { 34 | input, 35 | messages, 36 | isLoading, 37 | onSubmit, 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 David Robertson 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Stage 1: Build the application 2 | FROM node:20 as builder 3 | 4 | # Set the working directory in the container 5 | WORKDIR /app 6 | 7 | USER root 8 | # Enable Corepack to manage package managers 9 | RUN corepack enable 10 | 11 | # Copy package.json and other necessary files for dependency installation 12 | # Adjust this line if you have specific files needed for the build 13 | COPY package.json pnpm-lock.yaml ./ 14 | 15 | # Install dependencies 16 | RUN pnpm install 17 | 18 | # Copy the rest of your application's source code 19 | COPY . . 20 | 21 | # Build the project, which generates the .output directory 22 | RUN pnpm run build 23 | 24 | # Stage 2: Setup the runtime environment 25 | FROM node:20-slim 26 | 27 | # Set the working directory in the container 28 | WORKDIR /app 29 | 30 | # Copy the built application from the previous stage 31 | COPY --from=builder /app/.output ./.output 32 | 33 | # Since we're running as non-root, switch to the node user for better security 34 | USER node 35 | 36 | # Make port 3000 available to the outside world 37 | EXPOSE 3000 38 | 39 | # Run the application when the container launches 40 | CMD ["node", ".output/server/index.mjs"] 41 | -------------------------------------------------------------------------------- /composables/useChatStream.ts: -------------------------------------------------------------------------------- 1 | import type { Ref } from 'vue' 2 | 3 | interface ResolveStreamParams { 4 | data: Ref 5 | stream: ReadableStream 6 | onChunk?: (chunk: { data: string }) => void 7 | onReady?: (params: { data: string }) => void 8 | } 9 | 10 | const resolveStream = async ({ 11 | data, 12 | onChunk = () => {}, 13 | onReady = () => {}, 14 | stream, 15 | }: ResolveStreamParams) => { 16 | const decoder = new TextDecoder() 17 | const transformStream = new TransformStream({ 18 | transform(chunk) { 19 | const text = decoder.decode(chunk) 20 | 21 | data.value += text 22 | onChunk({ data: text }) 23 | }, 24 | }) 25 | 26 | const reader = stream.pipeThrough(transformStream).getReader() 27 | while (true) { 28 | const { done } = await reader.read() 29 | if (done) break 30 | } 31 | onReady({ data: data.value }) 32 | } 33 | 34 | interface UseChatStreamParams { 35 | stream: ReadableStream 36 | onChunk?: (chunk: { data: string }) => void 37 | onReady?: (params: { data: string }) => void 38 | } 39 | 40 | export const useChatStream = ({ 41 | onChunk = () => {}, 42 | onReady = () => {}, 43 | stream, 44 | }: UseChatStreamParams) => { 45 | const data = ref('') 46 | resolveStream({ data, onChunk, onReady, stream }) 47 | return { data: readonly(data) } 48 | } 49 | -------------------------------------------------------------------------------- /pages/settings.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 54 | -------------------------------------------------------------------------------- /nuxt.config.ts: -------------------------------------------------------------------------------- 1 | // https://nuxt.com/docs/api/configuration/nuxt-config 2 | export default defineNuxtConfig({ 3 | modules: [ 4 | '@nuxt/ui', 5 | '@vueuse/nuxt', 6 | 'nuxt-icon', 7 | '@formkit/auto-animate/nuxt', 8 | '@nuxt/image', 9 | '@nuxtjs/seo', 10 | 'nuxt-shiki', 11 | ], 12 | app: { 13 | pageTransition: { 14 | name: 'side-fade', 15 | mode: 'out-in', 16 | }, 17 | }, 18 | tailwindcss: { 19 | viewer: false, 20 | }, 21 | 22 | colorMode: { 23 | preference: 'dark', 24 | dataValue: 'theme', 25 | classSuffix: '', 26 | }, 27 | 28 | site: { 29 | url: 'https://nuxtdocusearchai.com', 30 | name: 'NuxtDocuSearchAi', 31 | description: 32 | 'Experience the power of AI in searching the Nuxt 3 documentation with NuxtDocuSearchAi. Discover content faster and more efficiently.', 33 | defaultLocale: 'en', 34 | identity: { 35 | type: 'Person', 36 | }, 37 | twitter: '@davidalexr987', 38 | trailingSlash: true, 39 | titleSeparator: '|', 40 | }, 41 | 42 | runtimeConfig: { 43 | NUXT_OPENAI_FINE_TUNED: process.env.NUXT_OPENAI_FINE_TUNED, 44 | NUXT_OPENAI_API_KEY: process.env.NUXT_OPENAI_API_KEY, 45 | LANGCHAIN_TRACING_V2: process.env.LANGCHAIN_TRACING_V2, 46 | LANGCHAIN_ENDPOINT: process.env.LANGCHAIN_ENDPOINT, 47 | LANGCHAIN_API_KEY: process.env.LANGCHAIN_API_KEY, 48 | LANGCHAIN_PROJECT: process.env.LANGCHAIN_PROJECT, 49 | }, 50 | 51 | nitro: { 52 | awsAmplify: { 53 | catchAllStaticFallback: true, 54 | }, 55 | experimental: { 56 | websocket: true, 57 | }, 58 | }, 59 | future: { 60 | typescriptBundlerResolution: true, 61 | }, 62 | devtools: { 63 | enabled: true, 64 | 65 | timeline: { 66 | enabled: true, 67 | }, 68 | }, 69 | }) 70 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nuxt-docu-search-ai", 3 | "version": "0.3.0", 4 | "description": "A single-page application that combines the power of GPT-4 and Nuxt 3 documentation to assist developers in enhancing their coding skills.", 5 | "repository": "https://github.com/daver987/nuxt-docu-search-ai", 6 | "author": "David Robertson ", 7 | "private": true, 8 | "scripts": { 9 | "build": "nuxi build", 10 | "dev": "nuxt dev", 11 | "generate": "nuxt generate", 12 | "preview": "nuxt preview", 13 | "start": "node .output/server/index.mjs", 14 | "postinstall": "nuxt prepare", 15 | "lint": "prettier --check .", 16 | "lint:fix": "prettier --write .", 17 | "release": "pnpm run lint:fix && changelogen --release && git push --follow-tags" 18 | }, 19 | "prettier": { 20 | "plugins": [ 21 | "prettier-plugin-tailwindcss", 22 | "prettier-plugin-organize-attributes" 23 | ], 24 | "trailingComma": "es5", 25 | "tabWidth": 2, 26 | "semi": false, 27 | "singleQuote": true 28 | }, 29 | "devDependencies": { 30 | "@langchain/core": "0.1.48", 31 | "@langchain/openai": "^0.0.21", 32 | "@nuxt/devtools": "latest", 33 | "@nuxt/image": "^1.4.0", 34 | "@nuxt/ui": "npm:@nuxt/ui-edge@latest", 35 | "@nuxtjs/seo": "latest", 36 | "@shikijs/markdown-it": "^1.2.0", 37 | "@tailwindcss/forms": "^0.5.7", 38 | "@types/markdown-it": "^13.0.7", 39 | "@types/node": "20.11.28", 40 | "@vueuse/components": "^10.9.0", 41 | "@vueuse/nuxt": "^10.9.0", 42 | "chalk": "^5.3.0", 43 | "langchain": "0.1.28", 44 | "nuxt": "npm:nuxt-nightly@3.11.0-28508947.4be430e1", 45 | "nuxt-icon": "^0.6.9", 46 | "nuxt-shiki": "^0.2.1", 47 | "openai": "^4.29.1", 48 | "prettier": "^3.2.5", 49 | "prettier-plugin-organize-attributes": "^1.0.0", 50 | "prettier-plugin-tailwindcss": "^0.5.12", 51 | "typescript": "latest", 52 | "zod": "^3.22.4" 53 | }, 54 | "dependencies": { 55 | "@formkit/auto-animate": "^0.8.1", 56 | "@vueuse/core": "^10.9.0", 57 | "markdown-it": "^14.0.0" 58 | }, 59 | "pnpm": { 60 | "overrides": { 61 | "@langchain/core": "0.1.48" 62 | } 63 | }, 64 | "license": "MIT", 65 | "packageManager": "pnpm@8.15.4+sha256.cea6d0bdf2de3a0549582da3983c70c92ffc577ff4410cbf190817ddc35137c2" 66 | } 67 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Nuxt 3 DocuSearch AI 2 | 3 | First off, thank you for considering contributing to Nuxt 3 DocuSearch AI. We value all the people who want to 4 | contribute to this project. Following these guidelines helps to communicate that you respect the time of the developers 5 | managing and developing this open source project. 6 | 7 | ## How Can I Contribute? 8 | 9 | ### Reporting Bugs 10 | 11 | This section guides you through submitting a bug report for Nuxt 3 DocuSearch AI. Following these guidelines helps 12 | maintainers understand your report, reproduce the behavior, and find related reports. 13 | 14 | - Use a clear and descriptive title for the issue to identify the problem. 15 | - Describe the exact steps which reproduce the problem in as many details as possible. 16 | - Provide specific examples to demonstrate the steps. 17 | 18 | ### Suggesting Enhancements 19 | 20 | This section guides you through submitting an enhancement suggestion for Nuxt 3 DocuSearch AI, including completely new 21 | features and minor improvements to existing functionality. Following these guidelines helps maintainers understand your 22 | suggestion and make a decision. 23 | 24 | - Use a clear and descriptive title for the issue to identify the suggestion. 25 | - Provide a step-by-step description of the suggested enhancement in as many details as possible. 26 | - Explain why this enhancement would be useful to most Nuxt 3 DocuSearch AI users. 27 | 28 | ### Pull Requests 29 | 30 | - Fill in the required template. 31 | - Do not include issue numbers in the PR title. 32 | - Include screenshots and animated GIFs in your pull request whenever possible. 33 | - Ensure your changes do not break any existing functionality. 34 | 35 | ## Coding Standards 36 | 37 | The codebase is built with TypeScript and Nuxt 3 (Vue 3). Please adhere to the standards and styles used in the existing 38 | code, and ensure your contributions are formatted with Prettier. 39 | 40 | ## Tests 41 | 42 | As we are still in the early stages of the project, we don't have a test suite yet. However, please make sure your 43 | contributions do not break any existing functionality. 44 | 45 | ## Conduct 46 | 47 | We expect everyone to follow our [Code of Conduct](CODE_OF_CONDUCT.md). Please make sure to read it before 48 | participating. 49 | 50 | ## Questions? 51 | 52 | If you have any questions or anything is unclear, please reach out via a GitHub issue. We appreciate your contribution 53 | and are glad to clarify any confusion. 54 | -------------------------------------------------------------------------------- /server/api/chat.ts: -------------------------------------------------------------------------------- 1 | import { ChatOpenAI } from '@langchain/openai' 2 | import { 3 | ChatPromptTemplate, 4 | MessagesPlaceholder, 5 | } from '@langchain/core/prompts' 6 | import { StringOutputParser } from '@langchain/core/output_parsers' 7 | import type { EventHandlerRequest, H3Event } from 'h3' 8 | import { defineEventHandler, sendStream } from 'h3' 9 | import { z } from 'zod' 10 | 11 | const chatSchema = z.object({ 12 | messages: z.array( 13 | z.object({ 14 | id: z.string().optional(), 15 | createdAt: z.string().optional(), 16 | content: z.string(), 17 | role: z.enum(['system', 'user', 'assistant', 'function']).optional(), 18 | }) 19 | ), 20 | }) 21 | 22 | interface Config { 23 | NUXT_OPENAI_API_KEY: string 24 | NUXT_OPENAI_FINE_TUNED: string 25 | } 26 | 27 | const prompt = ChatPromptTemplate.fromMessages([ 28 | [ 29 | 'system', 30 | "As an AI assistant specializing in Nuxt 3, it's your responsibility to provide comprehensive and insightful responses to queries about this JavaScript framework. When providing code examples, Always use TYPESCRIPT, and the Vue 3 script setup and composition api syntax. Ensure they are strictly aligned with the latest Nuxt 3 syntax, and refrain from using outdated Nuxt 2 syntax. Your responses should be detailed, informative, and aimed at enabling users to understand and effectively apply the knowledge in their projects.", 31 | ], 32 | new MessagesPlaceholder('chat_history'), 33 | ['user', '{input}'], 34 | ]) 35 | 36 | function getConfig(event: H3Event): Config { 37 | return { 38 | NUXT_OPENAI_API_KEY: useRuntimeConfig(event).NUXT_OPENAI_API_KEY, 39 | NUXT_OPENAI_FINE_TUNED: useRuntimeConfig(event).NUXT_OPENAI_FINE_TUNED, 40 | } 41 | } 42 | 43 | const initLangchain = (apiKey: string, chatModel: string) => { 44 | return new ChatOpenAI({ 45 | modelName: chatModel, 46 | openAIApiKey: apiKey, 47 | streaming: true, 48 | temperature: 0.2, 49 | }) 50 | } 51 | 52 | const initOutputParser = () => { 53 | return new StringOutputParser() 54 | } 55 | 56 | export default defineEventHandler( 57 | async (event: H3Event) => { 58 | const body = await readValidatedBody(event, chatSchema.parse) 59 | const { messages } = body 60 | 61 | const config = getConfig(event) 62 | const llm = initLangchain( 63 | config.NUXT_OPENAI_API_KEY, 64 | config.NUXT_OPENAI_FINE_TUNED 65 | ) 66 | const parser = initOutputParser() 67 | 68 | const chain = prompt.pipe(llm).pipe(parser) 69 | const stream = await chain.stream({ input: messages[0].content }) 70 | 71 | const readableStream = new ReadableStream({ 72 | async start(controller) { 73 | for await (const chunk of stream) { 74 | controller.enqueue(chunk) 75 | } 76 | controller.close() 77 | }, 78 | }) 79 | 80 | return sendStream(event, readableStream) 81 | } 82 | ) 83 | -------------------------------------------------------------------------------- /server/utils/sendStream.ts: -------------------------------------------------------------------------------- 1 | import type { H3Event, EventHandlerRequest } from 'h3' 2 | import * as net from 'net' 3 | 4 | type SocketType = net.Socket 5 | 6 | interface EventNodeRes { 7 | _data: BodyInit | ReadableStream 8 | socket?: SocketType 9 | 10 | write(chunk: Uint8Array): void 11 | 12 | end(): void 13 | } 14 | 15 | interface EnhancedEventNode { 16 | _handled: boolean 17 | node: { 18 | res: EventNodeRes 19 | } 20 | } 21 | 22 | export type ExtendedH3Event = H3Event & EnhancedEventNode 23 | 24 | export async function sendStreams( 25 | event: ExtendedH3Event, 26 | stream: ReadableStream 27 | ): Promise { 28 | if (!event.node.res) { 29 | return await handleError( 30 | new Error('Response object is missing in the event node') 31 | ) 32 | } 33 | 34 | try { 35 | const { res } = event.node 36 | 37 | setHandled(event) 38 | setDataStream(event, stream) 39 | 40 | if (!res.socket) { 41 | return handleError(new Error('No active socket connection found')) 42 | } 43 | 44 | pipeStreamToSocket(stream, res) 45 | } catch (error: unknown) { 46 | if (error instanceof Error) { 47 | await handleError(error) 48 | } else { 49 | await handleError(new Error('An unknown error occurred')) 50 | } 51 | } 52 | } 53 | 54 | function setHandled(event: ExtendedH3Event): void { 55 | event._handled = true 56 | } 57 | 58 | function setDataStream( 59 | event: ExtendedH3Event, 60 | stream: ReadableStream 61 | ): void { 62 | event.node.res._data = stream 63 | } 64 | 65 | function pipeStreamToSocket( 66 | stream: ReadableStream, 67 | res: EventNodeRes 68 | ): void { 69 | stream 70 | .pipeTo( 71 | new WritableStream({ 72 | write(chunk) { 73 | res.write(chunk) 74 | }, 75 | close() { 76 | res.end() 77 | }, 78 | }) 79 | ) 80 | .catch((error) => { 81 | handleError( 82 | new Error(`Error piping to stream: ${error.message}`), 83 | res 84 | ).catch((innerError) => { 85 | console.error('Failed to handle the error:', innerError) 86 | }) 87 | }) 88 | } 89 | 90 | async function handleError(error: Error, res?: EventNodeRes): Promise { 91 | console.error(error.message) 92 | 93 | if (error.message.includes('Error piping to stream') && res?._data) { 94 | try { 95 | const chunkedData = res._data as Uint8Array 96 | // noinspection JSUnusedLocalSymbols 97 | const jsonData = JSON.parse(new TextDecoder().decode(chunkedData)) 98 | 99 | res.write(chunkedData) 100 | res.end() 101 | 102 | console.info('Fallback to single JSON chunk succeeded.') 103 | return 104 | } catch (jsonError) { 105 | console.error('Failed to process data as a single JSON chunk:', jsonError) 106 | } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /server/api/ws/chat.ts: -------------------------------------------------------------------------------- 1 | import { ChatOpenAI } from '@langchain/openai' 2 | import type { Peer, Message, WSError } from 'crossws' 3 | import { ChatPromptTemplate } from '@langchain/core/prompts' 4 | import { StringOutputParser } from '@langchain/core/output_parsers' 5 | 6 | const prompt = ChatPromptTemplate.fromMessages([ 7 | [ 8 | 'system', 9 | "As an AI assistant specializing in Nuxt 3, it's your responsibility to provide comprehensive and insightful responses to queries about this JavaScript framework. When providing code examples, Always use TYPESCRIPT, and the Vue 3 script setup and composition api syntax. Ensure they are strictly aligned with the latest Nuxt 3 syntax, and refrain from using outdated Nuxt 2 syntax. Your responses should be detailed, informative, and aimed at enabling users to understand and effectively apply the knowledge in their projects.", 10 | ], 11 | ['user', '{input}'], 12 | ]) 13 | 14 | const initLangchain = (apiKey: string, chatModel: string) => { 15 | return new ChatOpenAI({ 16 | modelName: chatModel, 17 | openAIApiKey: apiKey, 18 | streaming: true, 19 | temperature: 0.2, 20 | }) 21 | } 22 | 23 | const initOutputParser = () => { 24 | return new StringOutputParser() 25 | } 26 | 27 | export default defineWebSocketHandler({ 28 | open(peer: Peer) { 29 | console.log('[ws] open', peer.id) 30 | }, 31 | message(peer: Peer, message: Message) { 32 | console.log('[ws] message received from', peer.id, ':', message.text()) 33 | sendWebSocketMessage(peer, message) 34 | .then(() => { 35 | console.log('[ws] message successfully sent to', peer.id) 36 | }) 37 | .catch((error: Error) => { 38 | console.error( 39 | '[ws] error sending message to', 40 | peer.id, 41 | ':', 42 | error.message 43 | ) 44 | }) 45 | }, 46 | close(peer: Peer, details: { code?: number; reason?: string }) { 47 | console.log('[ws] close', peer.id, details.code, details.reason) 48 | }, 49 | error(peer: Peer, error: WSError) { 50 | console.log('[ws] error', peer.id, error.message) 51 | }, 52 | }) 53 | 54 | async function sendWebSocketMessage( 55 | peer: Peer, 56 | message: Message 57 | ): Promise { 58 | console.log('[ws] Preparing to send message:', message.text()) 59 | const config = useRuntimeConfig() 60 | console.log('[ws] Using runtime config for API keys') 61 | const llm = initLangchain( 62 | config.NUXT_OPENAI_API_KEY as string, 63 | config.NUXT_OPENAI_FINE_TUNED as string 64 | ) 65 | console.log( 66 | '[ws] Langchain initialized with model:', 67 | config.NUXT_OPENAI_FINE_TUNED 68 | ) 69 | const parser = initOutputParser() 70 | console.log('[ws] Output parser initialized') 71 | const chain = prompt.pipe(llm).pipe(parser) 72 | console.log('[ws] Processing message content through Langchain and parser') 73 | const messageContent = JSON.parse(message.text()).content 74 | console.log('[ws] Message content parsed:', messageContent) 75 | const stream = await chain.stream({ input: messageContent }) 76 | console.log('[ws] Streaming response to peer:', peer.id) 77 | for await (const chunk of stream) { 78 | console.log('[ws] Sending chunk to peer:', peer.id) 79 | peer.send(chunk) 80 | } 81 | console.log('[ws] Message stream completed for peer:', peer.id) 82 | } 83 | -------------------------------------------------------------------------------- /types/generalTypes.ts: -------------------------------------------------------------------------------- 1 | // https://github.com/openai/openai-node/blob/07b3504e1c40fd929f4aae1651b83afc19e3baf8/src/resources/chat/completions.ts#L146-L159 2 | export interface FunctionCall { 3 | /** 4 | * The arguments to call the function with, as generated by the model in JSON 5 | * format. Note that the model does not always generate valid JSON, and may 6 | * hallucinate parameters not defined by your function schema. Validate the 7 | * arguments in your code before calling your function. 8 | */ 9 | arguments?: string 10 | 11 | /** 12 | * The name of the function to call. 13 | */ 14 | name?: string 15 | } 16 | 17 | // 18 | interface Function { 19 | /** 20 | * The name of the function to be called. Must be a-z, A-Z, 0-9, or contain 21 | * underscores and dashes, with a maximum length of 64. 22 | */ 23 | name: string 24 | 25 | /** 26 | * The parameters the functions accept, described as a JSON Schema object. See the 27 | * [guide](/docs/guides/gpt/function-calling) for examples, and the 28 | * [JSON Schema reference](https://json-schema.org/understanding-json-schema/) for 29 | * documentation about the format. 30 | * 31 | * To describe a function that accepts no parameters, provide the value 32 | * `{"type": "object", "properties": {}}`. 33 | */ 34 | parameters: Record 35 | 36 | /** 37 | * A description of what the function does, used by the model to choose when and 38 | * how to call the function. 39 | */ 40 | description?: string 41 | } 42 | 43 | /** 44 | * Shared types between the API and UI packages. 45 | */ 46 | export type Message = { 47 | id?: string 48 | createdAt?: Date 49 | content: string 50 | role: 'system' | 'user' | 'assistant' | 'function' 51 | /** 52 | * If the message has a role of `function`, the `name` field is the name of the function. 53 | * Otherwise, the name field should not be set. 54 | */ 55 | name?: string 56 | /** 57 | * If the assistant role makes a function call, the `function_call` field 58 | * contains the function call name and arguments. Otherwise, the field should 59 | * not be set. 60 | */ 61 | function_call?: string | FunctionCall 62 | } 63 | 64 | export type CreateMessage = Omit & { 65 | id?: Message['id'] 66 | } 67 | 68 | export type ChatRequest = { 69 | messages: Message[] 70 | functions?: Array 71 | function_call?: FunctionCall 72 | } 73 | 74 | export type FunctionCallHandler = ( 75 | chatMessages: Message[], 76 | functionCall: FunctionCall 77 | ) => Promise 78 | 79 | export type ChatRequestOptions = { 80 | functions?: Array 81 | function_call?: FunctionCall 82 | } 83 | 84 | export type UseChatOptions = { 85 | /** 86 | * The API endpoint that accepts a `{ messages: Message[] }` object and returns 87 | * a stream of tokens of the AI chat response. Defaults to `/api/chat`. 88 | */ 89 | api?: string 90 | 91 | /** 92 | * A unique identifier for the chat. If not provided, a random one will be 93 | * generated. When provided, the `useChat` hook with the same `id` will 94 | * have shared states across components. 95 | */ 96 | id?: string 97 | 98 | /** 99 | * Initial messages of the chat. Useful to load an existing chat history. 100 | */ 101 | initialMessages?: Message[] 102 | 103 | /** 104 | * Initial input of the chat. 105 | */ 106 | initialInput?: string 107 | 108 | /** 109 | * Callback function to be called when a function call is received. 110 | * If the function returns a `ChatRequest` object, the request will be sent 111 | * automatically to the API and will be used to update the chat. 112 | */ 113 | experimental_onFunctionCall?: FunctionCallHandler 114 | 115 | /** 116 | * Callback function to be called when an error is encountered. 117 | */ 118 | onError?: (error: Error) => void 119 | 120 | /** 121 | * Whether to send extra message fields such as `message.id` and `message.createdAt` to the API. 122 | * Defaults to `false`. When set to `true`, the API endpoint might need to 123 | * handle the extra fields before forwarding the request to the AI service. 124 | */ 125 | sendExtraMessageFields?: boolean 126 | } 127 | 128 | export type JSONValue = 129 | | null 130 | | string 131 | | number 132 | | boolean 133 | | { [x: string]: JSONValue } 134 | | Array 135 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Nuxt 3 DocuSearch AI 2 | 3 | Nuxt 3 DocuSearch AI offers a streamlined interface for developers to interact with Nuxt 3 documentation more efficiently. By leveraging a fine-tuned GPT-3.5 model and integrating directly with Nuxt 3, the application provides concise, context-aware answers to user queries, enhancing developer experience and productivity. This tool combines Nuxt 3's modern web development framework with the latest advancements in AI to offer a responsive and intuitive query-response interface, simplified by the use of websockets for real-time communication. Designed with simplicity and efficiency in mind, it is an essential tool for developers looking to navigate Nuxt 3 documentation effectively. 4 | ## Table of Contents 5 | 6 | 1. [Project Structure](#project-structure) 7 | 2. [Setup and Installation](#setup-and-installation) 8 | 3. [Usage](#usage) 9 | 4. [Building for Production](#building-for-production) 10 | 5. [Contribution](#contribution) 11 | 6. [License](#license) 12 | 13 | ## Project Structure 14 | 15 | The Nuxt 3 DocuSearch AI features a streamlined chat-like interface that facilitates an engaging way for developers to interact with Nuxt 3 documentation. At its core, the interface boasts a responsive top navigation bar and a dedicated input area at the bottom, encouraging users to submit their queries in a conversational manner. This design choice departs from traditional documentation interfaces, offering a more dynamic and interactive user experience. Responses, powered by a fine-tuned GPT-3.5 model, are delivered in real-time, thanks to the integration of websockets, ensuring prompt and relevant information retrieval. A side navigation menu provides quick access to additional features and settings, enhancing the application's usability. The application leverages LangChain for streamlined AI interactions, ShikiJS for syntax highlighting within returned code snippets, and Markdown-it for parsing and displaying markdown content, ensuring a rich and informative display of information. 16 | ## Setup and Installation 17 | 18 | To set up this project locally, please follow these steps: 19 | 20 | 1. Clone this repository to your local machine. 21 | 2. Navigate into the project directory. 22 | 3. Install the necessary packages with `pnpm install`. 23 | 4. Create an `.env` file in the root directory and provide your OpenAI API key 24 | 5. Start the development server on `http://localhost:3000` with `pnpm run dev`. 25 | 26 | Please ensure you have Node.js, npm, and Redis installed on your system before attempting to run this project. 27 | 28 | ## Usage 29 | 30 | To use the application: 31 | 32 | 1. Navigate to `http://localhost:3000` in your web browser. 33 | 2. Input your request in the provided textarea field at the bottom of the page. 34 | 3. Press the "Send" button or hit "Enter" to submit your query. 35 | 4. The application will respond with relevant information in real-time, providing concise and context-aware answers to your queries. 36 | 37 | ## Building for Production 38 | 39 | If you want to build the application for production, use the following command: 40 | 41 | ```bash 42 | pnpm run build 43 | ``` 44 | 45 | To preview the production build locally: 46 | 47 | ```bash 48 | pnpm run preview 49 | ``` 50 | 51 | ## Contribution 52 | 53 | We welcome contributions from everyone, and are grateful for every pull request! If you'd like to contribute, please 54 | consider the following steps: 55 | 56 | 1. Fork the repository. 57 | 2. Create your feature branch (`git checkout -b feature/AmazingFeature`). 58 | 3. Write clear, concise, and descriptive commit messages. 59 | 4. Commit your changes (`git commit -m 'Add some AmazingFeature'`). 60 | 5. Push to the branch (`git push origin feature/AmazingFeature`). 61 | 6. Open a pull request. 62 | 7. If your pull request addresses an issue, please include `closes #xxx` in your PR message where `xxx` is the issue 63 | number. 64 | 65 | Please ensure to adhere to this project's [Code of Conduct](CODE_OF_CONDUCT.md). Ensure your contributions pass all 66 | tests before opening a pull request. If you add or change any code, please add tests to accompany your changes. For more 67 | details, check our [Contributing Guidelines](CONTRIBUTING.md). 68 | 69 | ## Code of Conduct 70 | 71 | We aim to foster an inclusive and respectful community for everyone involved. All contributors and participants agree to 72 | adhere to our [Code of Conduct](CODE_OF_CONDUCT.md). Please make sure to read it before participating. 73 | 74 | ## License 75 | 76 | This project is licensed under the MIT License. The license allows others to use, copy, modify, merge, publish, 77 | distribute, sublicense, and/or sell copies of the Software, provided that they include the original copyright notice, 78 | this permission notice, and disclaimers of warranty. See the [LICENSE](LICENSE) file for full details. 79 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, caste, color, religion, or sexual 10 | identity and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | - Demonstrating empathy and kindness toward other people 21 | - Being respectful of differing opinions, viewpoints, and experiences 22 | - Giving and gracefully accepting constructive feedback 23 | - Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | - Focusing on what is best not just for us as individuals, but for the overall 26 | community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | - The use of sexualized language or imagery, and sexual attention or advances of 31 | any kind 32 | - Trolling, insulting or derogatory comments, and personal or political attacks 33 | - Public or private harassment 34 | - Publishing others' private information, such as a physical or email address, 35 | without their explicit permission 36 | - Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at info@drobertson.pro 63 | All complaints will be reviewed and investigated promptly and fairly. 64 | 65 | All community leaders are obligated to respect the privacy and security of the 66 | reporter of any incident. 67 | 68 | ## Enforcement Guidelines 69 | 70 | Community leaders will follow these Community Impact Guidelines in determining 71 | the consequences for any action they deem in violation of this Code of Conduct: 72 | 73 | ### 1. Correction 74 | 75 | **Community Impact**: Use of inappropriate language or other behavior deemed 76 | unprofessional or unwelcome in the community. 77 | 78 | **Consequence**: A private, written warning from community leaders, providing 79 | clarity around the nature of the violation and an explanation of why the 80 | behavior was inappropriate. A public apology may be requested. 81 | 82 | ### 2. Warning 83 | 84 | **Community Impact**: A violation through a single incident or series of 85 | actions. 86 | 87 | **Consequence**: A warning with consequences for continued behavior. No 88 | interaction with the people involved, including unsolicited interaction with 89 | those enforcing the Code of Conduct, for a specified period of time. This 90 | includes avoiding interactions in community spaces as well as external channels 91 | like social media. Violating these terms may lead to a temporary or permanent 92 | ban. 93 | 94 | ### 3. Temporary Ban 95 | 96 | **Community Impact**: A serious violation of community standards, including 97 | sustained inappropriate behavior. 98 | 99 | **Consequence**: A temporary ban from any sort of interaction or public 100 | communication with the community for a specified period of time. No public or 101 | private interaction with the people involved, including unsolicited interaction 102 | with those enforcing the Code of Conduct, is allowed during this period. 103 | Violating these terms may lead to a permanent ban. 104 | 105 | ### 4. Permanent Ban 106 | 107 | **Community Impact**: Demonstrating a pattern of violation of community 108 | standards, including sustained inappropriate behavior, harassment of an 109 | individual, or aggression toward or disparagement of classes of individuals. 110 | 111 | **Consequence**: A permanent ban from any sort of public interaction within the 112 | community. 113 | 114 | ## Attribution 115 | 116 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 117 | version 2.1, available at 118 | [https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. 119 | 120 | Community Impact Guidelines were inspired by 121 | [Mozilla's code of conduct enforcement ladder][Mozilla CoC]. 122 | 123 | For answers to common questions about this code of conduct, see the FAQ at 124 | [https://www.contributor-covenant.org/faq][FAQ]. Translations are available at 125 | [https://www.contributor-covenant.org/translations][translations]. 126 | 127 | [homepage]: https://www.contributor-covenant.org 128 | [v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html 129 | [Mozilla CoC]: https://github.com/mozilla/diversity 130 | [FAQ]: https://www.contributor-covenant.org/faq 131 | [translations]: https://www.contributor-covenant.org/translations 132 | -------------------------------------------------------------------------------- /components/Sidebar.vue: -------------------------------------------------------------------------------- 1 | 36 | 37 | 167 | -------------------------------------------------------------------------------- /pages/index.vue: -------------------------------------------------------------------------------- 1 | 117 | 118 | 194 | 195 | 200 | -------------------------------------------------------------------------------- /components/ChatInput.vue: -------------------------------------------------------------------------------- 1 | 40 |