Skip to content
Open
Show file tree
Hide file tree
Changes from 16 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
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ Full-featured AI Chatbot Nuxt application with authentication, chat history, mul
- ⚡️ **Streaming AI messages** powered by the [AI SDK v5](https://sdk.vercel.ai)
- 🤖 **Multiple model support** via various AI providers with built-in AI Gateway support
- 🔐 **Authentication** via [nuxt-auth-utils](https://github.com/atinux/nuxt-auth-utils)
- 💾 **Chat history persistence** using PostgreSQL database and [Drizzle ORM](https://orm.drizzle.team)
- 💾 **Chat history persistence** using SQLite database (Turso in production) and [Drizzle ORM](https://orm.drizzle.team)
- 🚀 **Easy deploy** to Vercel with zero configuration

## Quick Start
Expand All @@ -31,7 +31,7 @@ npm create nuxt@latest -- -t ui/chat

## Deploy your own

[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-name=chat&repository-url=https%3A%2F%2Fgithub.com%2Fnuxt-ui-templates%2Fchat&env=NUXT_SESSION_PASSWORD,NUXT_OAUTH_GITHUB_CLIENT_ID,NUXT_OAUTH_GITHUB_CLIENT_SECRET&products=%5B%7B%22type%22%3A%22integration%22%2C%22group%22%3A%22postgres%22%7D%5D&demo-image=https%3A%2F%2Fui.nuxt.com%2Fassets%2Ftemplates%2Fnuxt%2Fchat-dark.png&demo-url=https%3A%2F%2Fchat-template.nuxt.dev%2F&demo-title=Nuxt%20Chat%20Template&demo-description=An%20AI%20chatbot%20template%20to%20build%20your%20own%20chatbot%20powered%20by%20Nuxt%20MDC%20and%20Vercel%20AI%20SDK.)
[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-name=chat&repository-url=https%3A%2F%2Fgithub.com%2Fnuxt-ui-templates%2Fchat&env=NUXT_SESSION_PASSWORD,NUXT_OAUTH_GITHUB_CLIENT_ID,NUXT_OAUTH_GITHUB_CLIENT_SECRET&products=%5B%7B%22type%22%3A%22integration%22%2C%22protocol%22%3A%22storage%22%2C%22productSlug%22%3A%22database%22%2C%22integrationSlug%22%3A%22tursocloud%22%7D%5D&demo-image=https%3A%2F%2Fui.nuxt.com%2Fassets%2Ftemplates%2Fnuxt%2Fchat-dark.png&demo-url=https%3A%2F%2Fchat-template.nuxt.dev%2F&demo-title=Nuxt%20Chat%20Template&demo-description=An%20AI%20chatbot%20template%20to%20build%20your%20own%20chatbot%20powered%20by%20Nuxt%20MDC%20and%20Vercel%20AI%20SDK.)

## Setup

Expand Down
7 changes: 0 additions & 7 deletions app/composables/useChats.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,5 @@
import { isToday, isYesterday, subMonths } from 'date-fns'

interface Chat {
id: string
label: string
icon: string
createdAt: string
}

export function useChats(chats: Ref<Chat[] | undefined>) {
const groups = computed(() => {
// Group chats by date
Expand Down
10 changes: 0 additions & 10 deletions drizzle.config.ts

This file was deleted.

6 changes: 6 additions & 0 deletions nuxt.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export default defineNuxtConfig({
'@nuxt/eslint',
'@nuxt/ui',
'@nuxtjs/mdc',
'@nuxthub/core',
'nuxt-auth-utils',
'nuxt-charts'
],
Expand Down Expand Up @@ -36,6 +37,11 @@ export default defineNuxtConfig({
}
},

hub: {
ai: 'vercel',
database: 'sqlite'
},

eslint: {
config: {
stylistic: {
Expand Down
8 changes: 5 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,16 +18,18 @@
"@iconify-json/logos": "^1.2.9",
"@iconify-json/lucide": "^1.2.71",
"@iconify-json/simple-icons": "^1.2.55",
"@libsql/client": "^0.15.15",
"@nuxt/ui": "^4.1.0",
"@nuxthub/core": "npm:@nuxthub/core-nightly@1.0.0-20251101-232620-31e6b29",
"@nuxtjs/mdc": "^0.18.0",
"ai": "^5.0.80",
"date-fns": "^4.1.0",
"drizzle-orm": "^0.44.7",
"nuxt": "^4.2.0",
"nuxt": "4.1.0",
"nuxt-auth-utils": "^0.5.25",
"nuxt-charts": "0.2.4",
"pg": "^8.16.3",
"shiki-stream": "^0.1.2"
"shiki-stream": "^0.1.2",
"workers-ai-provider": "^2.0.0"
},
"devDependencies": {
"@nuxt/eslint": "^1.9.0",
Expand Down
1,591 changes: 1,162 additions & 429 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions pnpm-workspace.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,8 @@ ignoredBuiltDependencies:
- unrs-resolver
- vue-demi

onlyBuiltDependencies:
- better-sqlite3

patchedDependencies:
'@nuxt/vite-builder': patches/@nuxt__vite-builder.patch
4 changes: 3 additions & 1 deletion server/api/chats.get.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { eq } from 'drizzle-orm'

export default defineEventHandler(async (event) => {
const session = await getUserSession(event)

return (await useDrizzle().select().from(tables.chats).where(eq(tables.chats.userId, session.user?.id || session.id))).sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime())
return (await db.select().from(schema.chats).where(eq(schema.chats.userId, session.user?.id || session.id))).sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime())
})
5 changes: 2 additions & 3 deletions server/api/chats.post.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,16 @@ export default defineEventHandler(async (event) => {
const session = await getUserSession(event)

const { input } = await readBody(event)
const db = useDrizzle()

const [chat] = await db.insert(tables.chats).values({
const [chat] = await db.insert(schema.chats).values({
title: '',
userId: session.user?.id || session.id
}).returning()
if (!chat) {
throw createError({ statusCode: 500, statusMessage: 'Failed to create chat' })
}

await db.insert(tables.messages).values({
await db.insert(schema.messages).values({
chatId: chat.id,
role: 'user',
parts: [{ type: 'text', text: input }]
Expand Down
6 changes: 2 additions & 4 deletions server/api/chats/[id].delete.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,7 @@ export default defineEventHandler(async (event) => {

const { id } = getRouterParams(event)

const db = useDrizzle()

return await db.delete(tables.chats)
.where(and(eq(tables.chats.id, id as string), eq(tables.chats.userId, session.user?.id || session.id)))
return await db.delete(schema.chats)
.where(and(eq(schema.chats.id, id as string), eq(schema.chats.userId, session.user?.id || session.id)))
.returning()
})
4 changes: 3 additions & 1 deletion server/api/chats/[id].get.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { and, eq } from 'drizzle-orm'

export default defineEventHandler(async (event) => {
const session = await getUserSession(event)

const { id } = getRouterParams(event)

const chat = await useDrizzle().query.chats.findFirst({
const chat = await db.query.chats.findFirst({
where: (chat, { eq }) => and(eq(chat.id, id as string), eq(chat.userId, session.user?.id || session.id)),
with: {
messages: {
Expand Down
14 changes: 6 additions & 8 deletions server/api/chats/[id].post.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { convertToModelMessages, createUIMessageStream, createUIMessageStreamResponse, generateText, smoothStream, stepCountIs, streamText } from 'ai'
import { gateway } from '@ai-sdk/gateway'
import type { UIMessage } from 'ai'
import { z } from 'zod'
import { and, eq } from 'drizzle-orm'

defineRouteMeta({
openAPI: {
Expand All @@ -22,8 +22,6 @@ export default defineEventHandler(async (event) => {
messages: z.array(z.custom<UIMessage>())
}).parse)

const db = useDrizzle()

const chat = await db.query.chats.findFirst({
where: (chat, { eq }) => and(eq(chat.id, id as string), eq(chat.userId, session.user?.id || session.id)),
with: {
Expand All @@ -36,7 +34,7 @@ export default defineEventHandler(async (event) => {

if (!chat.title) {
const { text: title } = await generateText({
model: gateway('openai/gpt-4o-mini'),
model: hubAI('openai/gpt-4o-mini'),
system: `You are a title generator for a chat:
- Generate a short title based on the first user's message
- The title should be less than 30 characters long
Expand All @@ -46,12 +44,12 @@ export default defineEventHandler(async (event) => {
prompt: JSON.stringify(messages[0])
})

await db.update(tables.chats).set({ title }).where(eq(tables.chats.id, id as string))
await db.update(schema.chats).set({ title }).where(eq(schema.chats.id, id as string))
}

const lastMessage = messages[messages.length - 1]
if (lastMessage?.role === 'user' && messages.length > 1) {
await db.insert(tables.messages).values({
await db.insert(schema.messages).values({
chatId: id as string,
role: 'user',
parts: lastMessage.parts
Expand All @@ -61,7 +59,7 @@ export default defineEventHandler(async (event) => {
const stream = createUIMessageStream({
execute: ({ writer }) => {
const result = streamText({
model: gateway(model),
model: hubAI(model),
system: `You are a knowledgeable and helpful AI assistant. ${session.user?.username ? `The user's name is ${session.user.username}.` : ''} Your goal is to provide clear, accurate, and well-structured responses.

**FORMATTING RULES (CRITICAL):**
Expand Down Expand Up @@ -112,7 +110,7 @@ export default defineEventHandler(async (event) => {
}))
},
onFinish: async ({ messages }) => {
await db.insert(tables.messages).values(messages.map(message => ({
await db.insert(schema.messages).values(messages.map(message => ({
chatId: chat.id,
role: message.role as 'user' | 'assistant',
parts: message.parts
Expand Down
32 changes: 0 additions & 32 deletions server/database/migrations/0000_amusing_gunslinger.sql

This file was deleted.

Loading