Skip to content
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
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