diff --git a/.eslintrc.json b/.eslintrc.json deleted file mode 100644 index a44aff8..0000000 --- a/.eslintrc.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "extends": [ - "next/core-web-vitals", - "plugin:import/recommended", - "plugin:import/typescript", - "prettier", - "plugin:tailwindcss/recommended" - ], - "plugins": ["tailwindcss"], - "rules": { - "tailwindcss/no-custom-classname": "off", - "tailwindcss/classnames-order": "off" - }, - "settings": { - "import/resolver": { - "typescript": { - "alwaysTryTypes": true, - "project": "./tsconfig.json" - } - } - }, - "ignorePatterns": ["**/components/ui/**"] -} diff --git a/.gitignore b/.gitignore index e15a14f..ceb9c53 100644 --- a/.gitignore +++ b/.gitignore @@ -1,44 +1,4 @@ -# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. - -# dependencies -node_modules -.pnp -.pnp.js - -# testing -coverage - -# next.js -.next/ -out/ -build - -# misc -.DS_Store -*.pem -tsconfig.tsbuildinfo - -# debug -npm-debug.log* -yarn-debug.log* -yarn-error.log* -.pnpm-debug.log* - -# local env files -.env.local -.env.development.local -.env.test.local -.env.production.local - -# turbo -.turbo - -.env +config.json +.next .vercel -.env*.local - -# Playwright -/test-results/ -/playwright-report/ -/blob-report/ -/playwright/* +node_modules/ \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md deleted file mode 100644 index c6e84dd..0000000 --- a/CLAUDE.md +++ /dev/null @@ -1,101 +0,0 @@ -# CLAUDE.md - -This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. - -## Development Commands - -```bash -# Development -pnpm dev # Start development server with Turbo -pnpm build # Run migrations + build for production -pnpm start # Start production server - -# Code Quality (ALWAYS run after changes) -pnpm lint # Run Next.js + Biome linting -pnpm lint:fix # Auto-fix linting issues -pnpm format # Format code with Biome - -# Database -pnpm db:generate # Generate Drizzle migrations after schema changes -pnpm db:migrate # Run database migrations -pnpm db:studio # Open Drizzle Studio for DB inspection - -# Testing -pnpm test # Run all Playwright tests (E2E + integration) -pnpm exec playwright test --project=e2e # Run E2E tests only -pnpm exec playwright test --project=routes # Run API route tests only -pnpm exec playwright test --project=integration # Run database integration tests only -``` - -## Architecture Overview - -**Chat Gippidy** is a Next.js 15 chatbot application using App Router with experimental PPR. The app features an artifacts system for interactive content, NextAuth.js authentication, and Drizzle ORM with PostgreSQL. - -### Key Directories - -- **`app/(chat)/`** - Main chat interface with sidebar layout -- **`app/(auth)/`** - Authentication routes (login/register) -- **`artifacts/`** - Pluggable artifact system (code, text, image, sheet) -- **`lib/ai/`** - AI models, prompts, and tool definitions -- **`lib/db/`** - Drizzle schema, queries, and migrations -- **`components/`** - React components (UI components in `ui/` subdirectory) - -### Database Schema (Drizzle) - -Core tables: `users`, `chats`, `messages`, `documents`, `votes`, `suggestions`. Schema uses versioned messages (v2) with parts and attachments. Always run `pnpm db:generate` after schema changes, then `pnpm db:migrate`. - -### AI Integration - -Uses Vercel AI SDK with OpenAI models. Development/test environments use mock models for consistent testing. Reasoning support with `` tag extraction. Tool system includes weather, document creation, and suggestion tools. - -### Artifacts System - -Interactive content creation with client-server architecture: -- **Code artifacts**: Support Python execution via Pyodide -- **Text/Image/Sheet artifacts**: Editable content with versioning -- Each artifact type has separate client and server components - -### Authentication - -NextAuth.js 5.0 beta with Google OAuth provider only. All users must authenticate via Google account. Email restriction enforced via ALLOWED_EMAIL environment variable. - -## Development Guidelines - -### Code Quality -- Use **Biome** for linting/formatting (not ESLint/Prettier) -- TypeScript strict mode enabled - maintain full type safety -- Follow existing component patterns in `components/` directory - -### Testing Strategy -- **E2E tests**: End-to-end user flows with Page Object Model in `tests/e2e/` -- **Integration tests**: Database operations and API endpoints in `tests/integration/` -- **Route tests**: API contract validation in `tests/routes/` -- Mock AI responses for consistent test behavior -- Run tests before major changes: `pnpm test` -- Focus on critical paths that catch refactor breaks - -### Database Changes -1. Update schema in `lib/db/schema.ts` -2. Generate migration: `pnpm db:generate` -3. Apply migration: `pnpm db:migrate` -4. Never edit migration files directly - -### Artifact Development -When adding new artifact types: -1. Create client component in `artifacts/{type}/client.tsx` -2. Create server logic in `artifacts/{type}/server.ts` -3. Update artifact routing in main artifact components -4. Follow existing patterns for streaming updates - -### AI Model Configuration -- Development uses mock models for predictable testing -- Production uses OpenAI GPT-4o/4.1 with reasoning support -- Model configuration in `lib/ai/models.ts` and `lib/ai/providers.ts` - -## Current Architecture Notes - -- Uses experimental Next.js PPR (Partial Prerendering) -- React 19 RC for latest features -- pnpm for package management with Turbo for faster builds -- Font optimization with Geist variable fonts -- Theme system prevents hydration flashes \ No newline at end of file diff --git a/LICENSE b/LICENSE deleted file mode 100644 index 695ee2d..0000000 --- a/LICENSE +++ /dev/null @@ -1,13 +0,0 @@ -Copyright 2024 Vercel, Inc. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. \ No newline at end of file diff --git a/PROJECT_SUPPORT_PLAN.md b/PROJECT_SUPPORT_PLAN.md deleted file mode 100644 index 19b9519..0000000 --- a/PROJECT_SUPPORT_PLAN.md +++ /dev/null @@ -1,107 +0,0 @@ -# Project Support Implementation Plan - -## Overview -Add project support to Chat Gippidy - a system for grouping related files and chat threads that provides contextual AI responses based on project scope. - -## Key Features -- **Project Management**: Create, list, and manage projects from sidebar -- **File Association**: Upload and manage files within project scope -- **Chat Grouping**: Associate chats with projects -- **AI Context**: AI responses informed by project's chats and files -- **Project Scoping**: All interactions within a project maintain context - -## Phase 1: Database Foundation (High Priority) - -### Database Schema Design -- **projects table**: id, name, description, created_at, updated_at, user_id -- **project_chats junction table**: project_id, chat_id, added_at -- **project_files table**: id, project_id, filename, file_path, file_type, content, uploaded_at - -### Database Queries -- **Project CRUD**: createProject, getProjectsByUser, updateProject, deleteProject -- **Chat Associations**: addChatToProject, removeChatFromProject, getChatsByProject -- **File Operations**: addFileToProject, removeFileFromProject, getFilesByProject - -### Database Migrations -- Generate and apply migrations for new project tables - -## Phase 2: API Layer (High Priority) - -### Core API Routes -- `/api/projects` - CRUD operations for projects -- `/api/projects/[id]/chats` - Chat association management -- `/api/projects/[id]/files` - File operations within projects - -## Phase 3: AI Context System (High Priority) - -### Context Integration -- **Prompt Modification**: Inject project context into AI prompts -- **Context Builder**: Aggregate project chats and files for context -- **Context Management**: Handle context size limits and truncation strategies - -## Phase 4: UI Components (Medium Priority) - -### Sidebar Enhancement -- Add Projects section to sidebar navigation -- Create ProjectList component for displaying user's projects -- Create project button and modal/form component -- Add project selection state management - -### Chat Window States -- Empty project state with create chat/upload file options -- Project overview showing related chats and files -- Remove chat/file functionality with confirmation dialogs -- Project-scoped chat creation - -### File Management -- Project file upload component and handling -- File content extraction and storage for context - -## Phase 5: Navigation & Routing (Medium Priority) - -### Route Structure -- Add project-specific routes (`/projects/[id]`) -- Update existing chat routes to be project-aware -- Implement project-scoped chat creation - -## Phase 6: State Management (Medium Priority) - -### Global State -- Add project state to app-wide state management -- Create project context provider for active project - -## Phase 7: Testing & Polish (Low Priority) - -### Independent Tests -- Database operations testing -- API endpoint testing -- Context building testing - -### User Experience -- Remove chats/files from projects with confirmations -- Error handling and edge cases - -## Implementation Notes - -### Key Technical Decisions -- Use Drizzle ORM for database operations following existing patterns -- Leverage existing NextAuth user system for project ownership -- Build on existing sidebar and chat window components -- Maintain backwards compatibility with existing chats - -### Context Strategy -- Aggregate project chats and files into AI context -- Implement intelligent truncation for context size limits -- Prioritize recent chats and relevant files in context - -### Testing Approach -- Write independent tests that don't require test server -- Focus on database operations and API contracts -- Ignore Playwright tests as specified - -## Success Criteria -- Users can create and manage projects from sidebar -- Files can be uploaded and associated with projects -- Chats can be grouped within projects -- AI responses are contextually aware of project scope -- Smooth UX for project navigation and management \ No newline at end of file diff --git a/README.md b/README.md deleted file mode 100644 index 7054676..0000000 --- a/README.md +++ /dev/null @@ -1,200 +0,0 @@ -# Chat Gippidy - -A modern AI chatbot application built with Next.js 15, featuring real-time conversations, interactive artifacts, and multi-user collaboration. - -## Architecture Overview - -Chat Gippidy is a full-stack application that combines real-time AI chat with an innovative artifacts system for creating and editing interactive content. The application follows a modular architecture with clear separation of concerns across authentication, chat management, artifact handling, and database operations. - -### System Architecture - -```mermaid -graph TB - subgraph "Client Layer" - UI[React UI Components] - Chat[Chat Interface] - Artifacts[Artifact Editors] - Auth[Auth Forms] - end - - subgraph "API Layer" - ChatAPI["/api/chat"] - AuthAPI["/api/auth"] - DocAPI["/api/document"] - FileAPI["/api/files"] - HistoryAPI["/api/history"] - end - - subgraph "Service Layer" - AIService[AI Integration] - AuthService[Auth Integration] - ArtifactService[Artifact Handlers] - StreamService[Real-time Streams] - Telemetry[OpenTelemetry] - end - - subgraph "Data Layer" - DB[(PostgreSQL)] - BlobStorage[Vercel Blob] - Cache[Redis Cache] - end - - subgraph "External Services" - OpenAI[OpenAI API] - Models["GPT-4.1/4o Models"] - Google["Google Auth"] - Honeycomb["Honeycomb"] - end - - %% Client to API connections - UI --> ChatAPI - Chat --> ChatAPI - Artifacts --> DocAPI - Auth --> AuthAPI - UI --> FileAPI - UI --> HistoryAPI - - %% API to Service connections - ChatAPI --> AIService - ChatAPI --> StreamService - ChatAPI --> Telemetry - AuthAPI --> AuthService - AuthAPI --> Telemetry - DocAPI --> ArtifactService - DocAPI --> Telemetry - FileAPI --> BlobStorage - FileAPI --> Telemetry - - %% Service to Data connections - AIService --> OpenAI - AIService --> Models - AuthService --> Google - ArtifactService --> DB - StreamService --> Cache - StreamService --> DB - Telemetry --> Honeycomb - - %% Data flow arrows - AIService -.->|Tool Calls| ArtifactService - StreamService -.->|Real-time Updates| UI - ArtifactService -.->|Auto-save| DB -``` - -### Data Flow - -#### Chat Message Flow -1. **User Input** → `MultimodalInput` component captures text and file attachments -2. **Request Processing** → `useChat` hook formats request and sends to `/api/chat` -3. **AI Processing** → OpenAI models generate responses with optional tool calls -4. **Tool Execution** → AI can create/update documents, fetch weather, request suggestions -5. **Streaming Response** → Real-time updates sent to client via Server-Sent Events -6. **Database Persistence** → Messages and artifacts saved to PostgreSQL -7. **UI Updates** → Real-time rendering with optimistic updates - -#### Artifact System Flow -1. **AI Tool Call** → `createDocument` or `updateDocument` tools invoked during chat -2. **Content Generation** → Specialized handlers create initial content (text, code, images, sheets) -3. **Real-time Streaming** → Content streamed to client via `DataStreamWriter` -4. **Interactive Editing** → Users can edit artifacts with live preview -5. **Auto-save** → Changes automatically persisted with 2-second debounce -6. **Version History** → Document versions tracked for rollback capability - -### Major Modules - -#### Authentication System (`app/(auth)/`) -- **Google Auth:** sign in with Google, currently only supports me and only me - -#### Chat System (`app/(chat)/`) -- **Message Management**: Storage, retrieval, and pagination of conversations -- **Real-time Streaming**: WebSocket-like streaming using Server-Sent Events -- **Model Selection**: Support for reasoning models vs standard chat models -- **Conversation State**: Chat history, message voting, visibility controls - -#### Artifacts System (`artifacts/`, `lib/artifacts/`) -- **Multi-type Support**: Text documents, code execution, images, spreadsheets -- **Real-time Collaboration**: Live content updates with conflict resolution -- **Code Execution**: Python code execution via Pyodide in browser -- **Version Control**: Document versioning with diff viewing capabilities - -#### Database Layer (`lib/db/`) -- **Drizzle ORM**: Type-safe database operations with PostgreSQL -- **Schema Management**: Versioned migrations with foreign key relationships -- **Query Optimization**: Efficient queries with pagination and filtering -- **Error Handling**: Custom error types with user-friendly messages - -#### AI Integration (`lib/ai/`) -- **Provider Abstraction**: Custom wrapper around OpenAI models -- **Tool System**: Extensible function calling for document manipulation -- **Prompt Engineering**: Specialized prompts for different use cases -- **Model Management**: Dynamic selection based on user preferences - -### Key Features - -- **Real-time Chat**: Streaming AI responses with typing indicators -- **Interactive Artifacts**: Create and edit documents, code, images, and spreadsheets -- **Code Execution**: Run Python code directly in the browser -- **File Uploads**: Support for images and documents in conversations -- **Guest Users**: No registration required to start chatting -- **Message Voting**: User feedback system for AI responses -- **Chat History**: Persistent conversation storage and retrieval -- **Theme Support**: Dark/light mode with system preference detection - -### Technology Stack - -- **Frontend**: Next.js 15 (App Router), React 19, TypeScript, Tailwind CSS -- **Backend**: Next.js API routes, Server Actions, NextAuth.js -- **Database**: PostgreSQL with Drizzle ORM and migrations -- **AI**: Vercel AI SDK with OpenAI GPT-4.1/4o/o4-mini models -- **Observability**: OpenTelemetry data, sent to Honeycomb -- **Storage**: Vercel Blob for file uploads, Redis for caching -- **Testing**: Playwright for E2E, integration, and API testing -- **Code Quality**: Biome for linting and formatting - -## Testing - -The application has a comprehensive test suite covering critical functionality: - -### Test Categories - -- **E2E Tests** (`tests/e2e/`): End-to-end user workflows -- **Integration Tests** (`tests/integration/`): Database operations and API integration -- **Route Tests** (`tests/routes/`): API contract and authentication validation - -### Running Tests - -```bash -# Run all tests -pnpm test - -# Run specific test categories -pnpm exec playwright test --project=e2e # End-to-end tests -pnpm exec playwright test --project=routes # API route tests -pnpm exec playwright test --project=integration # Database integration tests - -# Run specific test files -pnpm exec playwright test tests/integration/database.test.ts -``` - -### Key Test Coverage - -- **Document creation/retrieval** for all artifact types (`text`, `code`, `image`, `sheet`) -- **Schema validation** and database constraints -- **User authentication** and authorization -- **Document versioning** and ownership -- **Chat message flow** and streaming -- **Artifact creation pipeline** via AI tools - -The integration tests would have caught the recent `kind` column schema bug and will prevent similar refactor breaks. - -### TODOs - -UX: -- stream the chat smoothly, don't make it seem choppy, it should smoothly reveal itself -- change initial loading state to have the little icon spin like gemini does - -Functionality: -- web search support -- support reasoning models including reasoning steps? -- Really slick rendering support -- Explore deviating UI from the template -- Projects support \ No newline at end of file diff --git a/TESTING_PLAN.md b/TESTING_PLAN.md deleted file mode 100644 index 0e20b36..0000000 --- a/TESTING_PLAN.md +++ /dev/null @@ -1,95 +0,0 @@ -# High-Impact Testing Plan - -This document outlines a focused testing strategy to catch critical breaks without excessive UI testing. - -## Test Strategy Overview - -Focus on **integration points** and **core business logic** rather than exhaustive UI validation. Goal: catch refactor breaks with minimal test code. - -## Priority Tests to Implement - -### 1. Database Integration Tests (HIGH PRIORITY - 30 min) ✅ COMPLETED -**File**: `tests/integration/database.test.ts` - -**What to test**: -- Actual database operations with real schema -- Document creation with all artifact types (`text`, `code`, `image`, `sheet`) -- Constraint violations and data integrity -- Column name/type mismatches - -**Impact**: Would have caught the `kind` column name bug immediately - -**Status**: ✅ **IMPLEMENTED AND WORKING** -- 6 comprehensive tests covering all document operations -- Tests all artifact types, versioning, user ownership, validation -- Uses real database operations via API endpoints -- Includes test auth bypass for Google OAuth requirement - -### 2. AI Tools Integration Tests (HIGH PRIORITY - 45 min) -**File**: `tests/integration/ai-tools.test.ts` - -**What to test**: -- Full `createDocument` tool flow: tool call → artifact handler → database save -- `updateDocument` tool with real data -- Mock AI responses but test real database operations -- Error handling in tool execution - -**Impact**: Catches breaks in the document creation pipeline - -### 3. Artifact Handler Tests (MEDIUM PRIORITY - 30 min) -**File**: `tests/integration/artifact-handlers.test.ts` - -**What to test**: -- Each artifact type handler (text, code, image, sheet) -- Handlers correctly save to database -- Error handling in handlers -- Handler registration/discovery - -**Impact**: Validates the artifact creation machinery - -### 4. Strengthen Existing API Tests (LOW PRIORITY - 15 min) -**Enhance**: `tests/routes/document.test.ts` - -**Add**: -- Test with invalid `kind` values (catch enum mismatches) -- Test concurrent document creation -- Test malformed request bodies -- Edge cases in document versioning - -## What to SKIP - -- ❌ UI component tests ("button is a button") -- ❌ Detailed styling/layout tests -- ❌ Exhaustive edge case permutations -- ❌ Authentication UI flows (existing session tests cover this) -- ❌ Mock-heavy unit tests that don't catch integration breaks - -## Implementation Notes - -- Use existing test fixtures and helpers from `tests/fixtures.ts` -- Leverage the authenticated contexts (`adaContext`, `babbageContext`) -- Run against real database (not mocked) to catch schema issues -- Keep tests fast by focusing on critical paths only - -## Total Time Investment -- **Database integration**: 30 minutes -- **AI tools integration**: 45 minutes -- **Artifact handlers**: 30 minutes -- **API enhancements**: 15 minutes -- **Total**: ~2 hours - -## Success Metrics - -These tests should catch: -- ✅ **Schema changes that break document creation** - VERIFIED: Tests catch column name mismatches -- ✅ **Refactors that break the AI tool → database pipeline** - VERIFIED: Database integration tests work -- ✅ **Changes to artifact handlers that prevent saving** - VERIFIED: All artifact types tested -- ✅ **API contract changes that break clients** - VERIFIED: Request validation implemented - -**Current Status**: -- ✅ **Database integration tests**: 6/6 tests passing -- ✅ **Test authentication**: Google OAuth bypass working -- ✅ **API validation**: Zod schema validation implemented -- ✅ **All artifact types**: text, code, image, sheet tested - -The existing e2e tests validate user-facing behavior, so these tests fill gaps in the underlying machinery. \ No newline at end of file diff --git a/app/(auth)/actions.ts b/app/(auth)/actions.ts deleted file mode 100644 index 833f883..0000000 --- a/app/(auth)/actions.ts +++ /dev/null @@ -1,4 +0,0 @@ -'use server'; - -// All authentication is now handled by Google OAuth via NextAuth -// This file is kept for potential future auth-related server actions diff --git a/app/(auth)/auth-error/page.tsx b/app/(auth)/auth-error/page.tsx deleted file mode 100644 index e7d825b..0000000 --- a/app/(auth)/auth-error/page.tsx +++ /dev/null @@ -1,65 +0,0 @@ -'use client'; - -import { motion } from 'framer-motion'; -import { Button } from '@/components/ui/button'; -import { useRouter } from 'next/navigation'; -import { CatButtholeIcon } from '@/components/cat-butthole-icon'; - -export default function AuthErrorPage() { - const router = useRouter(); - - return ( -
-
- - - - - -

SIKE!

- -

- You can't log in! -

- -

- Enjoy the cat butthole! 🐱 -

- -
- - - -
-
-
-
- ); -} diff --git a/app/(auth)/auth.config.ts b/app/(auth)/auth.config.ts deleted file mode 100644 index 10c6a13..0000000 --- a/app/(auth)/auth.config.ts +++ /dev/null @@ -1,14 +0,0 @@ -import type { NextAuthConfig } from 'next-auth'; - -export const authConfig = { - pages: { - signIn: '/login', - newUser: '/', - error: '/auth-error', - }, - providers: [ - // added later in auth.ts since it requires bcrypt which is only compatible with Node.js - // while this file is also used in non-Node.js environments - ], - callbacks: {}, -} satisfies NextAuthConfig; diff --git a/app/(auth)/auth.ts b/app/(auth)/auth.ts deleted file mode 100644 index 3b1c4e2..0000000 --- a/app/(auth)/auth.ts +++ /dev/null @@ -1,186 +0,0 @@ -import NextAuth, { type DefaultSession } from 'next-auth'; -import Credentials from 'next-auth/providers/credentials'; -import Google from 'next-auth/providers/google'; -import { authConfig } from './auth.config'; -import { getUser, createUserWithEmail } from '@/lib/db/queries'; -import { recordErrorOnCurrentSpan } from '@/lib/telemetry'; -import { isTestEnvironment } from '@/lib/constants'; - -declare module 'next-auth' { - interface Session extends DefaultSession { - user: { - id: string; - } & DefaultSession['user']; - } -} - -const previewLoginEnabled = process.env.ENABLE_PREVIEW_LOGIN === 'true'; - -const nextAuth = NextAuth({ - ...authConfig, - providers: [ - ...(previewLoginEnabled - ? [] - : [ - Google({ - // biome-ignore lint/style/noNonNullAssertion: Required environment variables - clientId: process.env.GOOGLE_ID!, - // biome-ignore lint/style/noNonNullAssertion: Required environment variables - clientSecret: process.env.GOOGLE_SECRET!, - }), - ]), - ...(previewLoginEnabled - ? [ - Credentials({ - name: 'Preview access', - credentials: { - code: { label: 'Access code', type: 'password' }, - }, - async authorize(credentials) { - const previewCode = process.env.PREVIEW_LOGIN_CODE?.trim(); - const previewEmail = process.env.PREVIEW_LOGIN_EMAIL?.trim(); - const providedCode = - typeof credentials?.code === 'string' - ? credentials.code.trim() - : undefined; - - if (!previewCode || !previewEmail) { - return null; - } - - if (providedCode !== previewCode) { - return null; - } - - const dbUsers = await getUser(previewEmail); - let userId = dbUsers[0]?.id; - - if (!userId) { - const [newUser] = await createUserWithEmail(previewEmail); - userId = newUser.id; - } - - return { - id: userId, - email: previewEmail, - name: 'Preview User', - }; - }, - }), - ] - : []), - ], - callbacks: { - async signIn({ user, account }) { - const allowedEmail = process.env.ALLOWED_EMAIL?.trim(); - const userEmail = user.email?.trim(); - - if (account?.provider === 'google' && allowedEmail !== userEmail) { - return false; - } - - // Create or get user in our database - try { - const dbUsers = await getUser(userEmail || ''); - if (dbUsers.length === 0) { - // Create new user - const [newUser] = await createUserWithEmail(userEmail || ''); - user.id = newUser.id; - } else { - // Use existing user - user.id = dbUsers[0].id; - } - return true; - } catch (error) { - recordErrorOnCurrentSpan(error as Error, { - 'error.context': 'auth_user_creation_or_lookup', - 'auth.user_email': userEmail, - 'auth.provider': 'google', - }); - console.error('Error creating/getting user:', error); - return false; - } - }, - async jwt({ token, user }) { - if (user) { - token.id = user.id as string; - } - return token; - }, - async session({ session, token }) { - if (session.user) { - session.user.id = token.id as string; - - // Ensure user exists in database (in case they were deleted or JWT persisted after failed creation) - try { - const dbUsers = await getUser(session.user.email || ''); - if (dbUsers.length === 0) { - const [newUser] = await createUserWithEmail( - session.user.email || '', - ); - session.user.id = newUser.id; - } else { - // Make sure we're using the correct database ID - session.user.id = dbUsers[0].id; - } - } catch (error) { - recordErrorOnCurrentSpan(error as Error, { - 'error.context': 'auth_session_user_verification', - 'auth.user_email': session.user.email, - }); - console.error('Error ensuring user exists during session:', error); - } - } - return session; - }, - }, -}); - -export const { - handlers: { GET, POST }, - signIn, - signOut, -} = nextAuth; - -// Test auth override for integration tests -export async function auth() { - if (isTestEnvironment) { - // In test mode, extract user info from request headers - const { headers } = await import('next/headers'); - const headerStore = await headers(); - const userAgent = headerStore.get('user-agent') || ''; - const testUserId = headerStore.get('x-test-user-id') || ''; - const testUserEmail = headerStore.get('x-test-user-email') || ''; - - // Check if this is a Playwright test request - if (userAgent.includes('Playwright') && testUserEmail) { - // Ensure test user exists in database - try { - const dbUsers = await getUser(testUserEmail); - let userId = testUserId; - - if (dbUsers.length === 0) { - const [newUser] = await createUserWithEmail(testUserEmail); - userId = newUser.id; - } else { - userId = dbUsers[0].id; - } - - return { - user: { - id: userId, - email: testUserEmail, - name: `Test User ${testUserId}`, - }, - expires: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(), - }; - } catch (error) { - console.error('Error creating test user:', error); - return null; - } - } - } - - // In production/development, use normal NextAuth - return nextAuth.auth(); -} diff --git a/app/(auth)/login/page.tsx b/app/(auth)/login/page.tsx deleted file mode 100644 index 55bf0a6..0000000 --- a/app/(auth)/login/page.tsx +++ /dev/null @@ -1,202 +0,0 @@ -'use client'; - -import { useRouter } from 'next/navigation'; -import { FormEvent, useState, useTransition } from 'react'; -import { signIn } from 'next-auth/react'; -import { Button } from '@/components/ui/button'; -import { motion } from 'framer-motion'; -import { CatButtholeIcon } from '@/components/cat-butthole-icon'; - -export default function Page() { - const router = useRouter(); - const [isPending, startTransition] = useTransition(); - const [accessCode, setAccessCode] = useState(''); - const [previewError, setPreviewError] = useState(''); - const previewLoginEnabled = - process.env.NEXT_PUBLIC_ENABLE_PREVIEW_LOGIN === 'true'; - - const handleGoogleSignIn = () => { - signIn('google', { callbackUrl: '/' }); - }; - - const handlePreviewLogin = (event?: FormEvent) => { - event?.preventDefault(); - setPreviewError(''); - startTransition(async () => { - const result = await signIn('credentials', { - code: accessCode, - redirect: false, - }); - - if (result?.error) { - setPreviewError('Invalid preview access code.'); - return; - } - - router.push('/'); - }); - }; - - return ( -
- {/* Left side - Welcome content */} -
- -
-
- -
-

Chat Gippidy

-
- -

- Welcome back! -

- -

- Your AI assistant is ready to help with coding, writing, analysis, - and creative projects. Sign in to continue your conversations. -

- -
-
-
- Interactive code execution -
-
-
- Document creation and editing -
-
-
- Real-time collaboration -
-
- -
- - {/* Right side - Sign in form */} -
- - {/* Mobile header */} -
-
-
- -
-

Chat Gippidy

-
-

- Welcome back! -

-

- Sign in to continue your AI conversations -

-
- -
-
-

- {previewLoginEnabled - ? 'Preview access only' - : 'Sign in to your account'} -

-

- {previewLoginEnabled - ? 'Use your preview access code to sign in on this deployment.' - : 'Continue with your Google account to access Chat Gippidy'} -

-
- - {previewLoginEnabled ? ( -
-
-

- Preview access -

-

- This preview build uses access codes instead of Google - sign-in. -

-
- -
- - setAccessCode(event.target.value)} - className="w-full rounded-lg border border-input bg-background px-3 py-2 text-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring" - placeholder="Enter code" - autoComplete="off" - /> - {previewError ? ( -

{previewError}

- ) : null} -
- - -
- ) : ( - - )} - -
-

- By signing in, you agree to our terms of service and privacy - policy. -

-
-
-
-
-
- ); -} diff --git a/app/(auth)/register/page.tsx b/app/(auth)/register/page.tsx deleted file mode 100644 index 7933482..0000000 --- a/app/(auth)/register/page.tsx +++ /dev/null @@ -1,7 +0,0 @@ -'use client'; - -import { redirect } from 'next/navigation'; - -export default function Page() { - redirect('/login'); -} diff --git a/app/(chat)/actions.ts b/app/(chat)/actions.ts deleted file mode 100644 index 14ee7dc..0000000 --- a/app/(chat)/actions.ts +++ /dev/null @@ -1,53 +0,0 @@ -'use server'; - -import { generateText, type UIMessage } from 'ai'; -import { cookies } from 'next/headers'; -import { - deleteMessagesByChatIdAfterTimestamp, - getMessageById, - updateChatVisiblityById, -} from '@/lib/db/queries'; -import type { VisibilityType } from '@/components/visibility-selector'; -import { myProvider } from '@/lib/ai/providers'; - -export async function saveChatModelAsCookie(model: string) { - const cookieStore = await cookies(); - cookieStore.set('chat-model', model); -} - -export async function generateTitleFromUserMessage({ - message, -}: { - message: UIMessage; -}) { - const { text: title } = await generateText({ - model: myProvider.languageModel('title-model'), - system: `\n - - you will generate a short title based on the first message a user begins a conversation with - - ensure it is not more than 80 characters long - - the title should be a summary of the user's message - - do not use quotes or colons`, - prompt: JSON.stringify(message), - }); - - return title; -} - -export async function deleteTrailingMessages({ id }: { id: string }) { - const [message] = await getMessageById({ id }); - - await deleteMessagesByChatIdAfterTimestamp({ - chatId: message.chatId, - timestamp: message.createdAt, - }); -} - -export async function updateChatVisibility({ - chatId, - visibility, -}: { - chatId: string; - visibility: VisibilityType; -}) { - await updateChatVisiblityById({ chatId, visibility }); -} diff --git a/app/(chat)/api/chat/route.ts b/app/(chat)/api/chat/route.ts deleted file mode 100644 index a83b883..0000000 --- a/app/(chat)/api/chat/route.ts +++ /dev/null @@ -1,514 +0,0 @@ -import { - appendClientMessage, - appendResponseMessages, - createDataStream, - smoothStream, - streamText, -} from 'ai'; -import { auth } from '@/app/(auth)/auth'; -import { type RequestHints, systemPrompt } from '@/lib/ai/prompts'; -import { - buildProjectContext, - formatProjectContextForPrompt, -} from '@/lib/ai/project-context'; -import { - createStreamId, - deleteChatById, - getChatById, - getMessageCountByUserId, - getMessagesByChatId, - getStreamIdsByChatId, - saveChat, - saveMessages, -} from '@/lib/db/queries'; -import { generateUUID, getTrailingMessageId } from '@/lib/utils'; -import { generateTitleFromUserMessage } from '../../actions'; -import { createDocument } from '@/lib/ai/tools/create-document'; -import { updateDocument } from '@/lib/ai/tools/update-document'; -import { requestSuggestions } from '@/lib/ai/tools/request-suggestions'; -import { getWeather } from '@/lib/ai/tools/get-weather'; -import { isProductionEnvironment } from '@/lib/constants'; -import { myProvider } from '@/lib/ai/providers'; -import { userEntitlements } from '@/lib/ai/entitlements'; -import { postRequestBodySchema, type PostRequestBody } from './schema'; -import { geolocation } from '@vercel/functions'; -import { - createResumableStreamContext, - type ResumableStreamContext, -} from 'resumable-stream'; -import { after } from 'next/server'; -import type { Chat } from '@/lib/db/schema'; -import { differenceInSeconds } from 'date-fns'; -import { ChatSDKError } from '@/lib/errors'; -import { - createChatSpan, - recordError, - recordErrorOnCurrentSpan, -} from '@/lib/telemetry'; - -export const maxDuration = 60; - -let globalStreamContext: ResumableStreamContext | null = null; - -function getStreamContext() { - if (!globalStreamContext) { - try { - globalStreamContext = createResumableStreamContext({ - waitUntil: after, - }); - } catch (error: any) { - if (error.message.includes('REDIS_URL')) { - console.log( - ' > Resumable streams are disabled due to missing REDIS_URL', - ); - } else { - recordErrorOnCurrentSpan(error, { - operation: 'stream_context_init', - 'error.type': 'redis_connection', - }); - console.error(error); - } - } - } - - return globalStreamContext; -} - -export async function POST(request: Request) { - const chatSpan = createChatSpan('app.ChatRequest'); - - let requestBody: PostRequestBody; - - try { - const json = await request.json(); - requestBody = postRequestBodySchema.parse(json); - } catch (error) { - recordError(chatSpan, new Error('Invalid request body')); - chatSpan.end(); - return new ChatSDKError('bad_request:api').toResponse(); - } - - try { - const { id, message, selectedChatModel, selectedVisibilityType } = - requestBody; - - // Set initial span attributes - chatSpan.setAttributes({ - 'app.chat.id': id, - 'app.chat.model': selectedChatModel, - 'app.chat.visibility': selectedVisibilityType, - }); - - const session = await auth(); - - if (!session?.user) { - chatSpan.setAttribute('app.auth.unauthorized', true); - const err = new ChatSDKError('unauthorized:chat'); - recordError(chatSpan, err); - chatSpan.end(); - return err.toResponse(); - } - - const messageCount = await getMessageCountByUserId({ - id: session.user.id, - differenceInHours: 24, - }); - - chatSpan.setAttributes({ - 'app.user.id': session.user.id, - 'app.user.message_count_24h': messageCount, - 'app.user.entitlement_limit': userEntitlements.maxMessagesPerDay, - }); - - if (messageCount > userEntitlements.maxMessagesPerDay) { - chatSpan.setAttributes({ - 'app.user.is_rate_limited': true, - 'app.user.message_count_24h': messageCount, - 'app.user.entitlement_limit': userEntitlements.maxMessagesPerDay, - }); - chatSpan.end(); - return new ChatSDKError('rate_limit:chat').toResponse(); - } - - const chat = await getChatById({ id }); - const isNewChat = !chat; - - chatSpan.setAttributes({ - 'app.chat.is_new_chat': isNewChat, - }); - - if (!chat) { - const title = await generateTitleFromUserMessage({ - message, - }); - - await saveChat({ - id, - userId: session.user.id, - title, - visibility: selectedVisibilityType, - }); - } else { - if (chat.userId !== session.user.id) { - chatSpan.setAttributes({ - 'app.auth.forbidden': true, - }); - chatSpan.end(); - return new ChatSDKError('forbidden:chat').toResponse(); - } - } - - const previousMessages = await getMessagesByChatId({ id }); - - const messages = appendClientMessage({ - // @ts-expect-error: todo add type conversion from DBMessage[] to UIMessage[] - messages: previousMessages, - message, - }); - - const { longitude, latitude, city, country } = geolocation(request); - - const requestHints: RequestHints = { - longitude, - latitude, - city, - country, - }; - - // Build project context for this chat - const projectContext = await buildProjectContext(id); - const projectContextPrompt = projectContext - ? formatProjectContextForPrompt(projectContext) - : undefined; - - await saveMessages({ - messages: [ - { - chatId: id, - id: message.id, - role: 'user', - parts: message.parts, - attachments: message.experimental_attachments ?? [], - createdAt: new Date(), - }, - ], - }); - - const streamId = generateUUID(); - await createStreamId({ streamId, chatId: id }); - - // Extract user message text from parts - const userContent = message.parts || []; - const userText = userContent - .filter((part) => part.type === 'text') - .map((part) => part.text) - .join(' '); - - chatSpan.setAttributes({ - 'app.ai.tools.active': [ - 'getWeather', - 'createDocument', - 'updateDocument', - 'requestSuggestions', - ], - 'app.ai.response.streaming': true, - 'app.stream.id': streamId, - 'app.ai.model.input.messages_count': messages.length, - 'app.ai.model.input.system_prompt': systemPrompt({ - selectedChatModel, - requestHints, - projectContext: projectContextPrompt, - }), - 'app.ai.model.input.user_message': userText, - }); - - const stream = createDataStream({ - execute: (dataStream) => { - const result = streamText({ - experimental_activeTools: [ - 'getWeather', - 'createDocument', - 'updateDocument', - 'requestSuggestions', - ], - experimental_generateMessageId: generateUUID, - experimental_telemetry: { - isEnabled: isProductionEnvironment, - functionId: 'stream-text', - }, - experimental_transform: smoothStream({ chunking: 'word' }), - maxSteps: 5, - messages, - model: myProvider.languageModel(selectedChatModel), - onFinish: async ({ response, usage, finishReason, toolCalls }) => { - // Record AI completion metrics and full I/O - const assistantMessages = response.messages.filter( - (m) => m.role === 'assistant', - ); - const lastAssistantMessage = - assistantMessages[assistantMessages.length - 1]; - - // Extract text content from parts - const responseContent = lastAssistantMessage?.content; - const responseText = - typeof responseContent === 'string' - ? responseContent - : Array.isArray(responseContent) - ? responseContent - .filter((part) => part.type === 'text') - .map((part) => part.text) - .join(' ') - : ''; - - // Check for reasoning content in response - const hasReasoning = response.messages.some( - (msg) => - Array.isArray(msg.content) && - msg.content.some((part) => part.type === 'reasoning'), - ); - - const reasoningParts = response.messages.flatMap((msg) => - Array.isArray(msg.content) - ? msg.content.filter((part) => part.type === 'reasoning') - : [], - ); - - chatSpan.setAttributes({ - 'app.ai.response.finish_reason': finishReason || 'unknown', - 'app.ai.response.tokens.total': usage?.totalTokens || 0, - 'app.ai.response.tokens.prompt': usage?.promptTokens || 0, - 'app.ai.response.tokens.completion': usage?.completionTokens || 0, - // TODO: Add reasoning tokens when supported by AI SDK - // 'app.ai.response.tokens.reasoning': usage?.reasoningTokens || 0, - 'app.ai.tools.called': toolCalls?.map((tc) => tc.toolName) || [], - 'app.ai.tools.called_count': toolCalls?.length || 0, - 'app.ai.response.messages_count': assistantMessages.length, - 'app.ai.response.content': responseText, - 'app.ai.response.has_reasoning': hasReasoning, - 'app.ai.response.reasoning_steps': reasoningParts.length, - }); - - // End the span here after completion - chatSpan.end(); - - if (session.user?.id) { - try { - const assistantId = getTrailingMessageId({ - messages: response.messages.filter( - (message) => message.role === 'assistant', - ), - }); - - if (!assistantId) { - throw new Error('No assistant message found!'); - } - - const [, assistantMessage] = appendResponseMessages({ - messages: [message], - responseMessages: response.messages, - }); - - await saveMessages({ - messages: [ - { - id: assistantId, - chatId: id, - role: assistantMessage.role, - parts: assistantMessage.parts, - attachments: - assistantMessage.experimental_attachments ?? [], - createdAt: new Date(), - }, - ], - }); - - chatSpan.setAttributes({ - 'app.message.id': assistantId, - 'app.message.role': 'assistant', - }); - } catch (error) { - recordError(chatSpan, error as Error, { - 'error.context': 'save_assistant_message', - }); - console.error('Failed to save chat'); - } - } - }, - system: systemPrompt({ - selectedChatModel, - requestHints, - projectContext: projectContextPrompt, - }), - tools: { - getWeather, - createDocument: createDocument({ session, dataStream }), - updateDocument: updateDocument({ session, dataStream }), - requestSuggestions: requestSuggestions({ - session, - dataStream, - }), - }, - }); - - result.consumeStream(); - - result.mergeIntoDataStream(dataStream, { - sendReasoning: true, - }); - }, - onError: (error) => { - recordError(chatSpan, error as Error, { - 'app.error.context': 'ai_streaming', - }); - chatSpan.end(); - return 'Oops, an error occurred!'; - }, - }); - - const streamContext = getStreamContext(); - - chatSpan.setAttributes({ - 'stream.resumable': !!streamContext, - }); - - if (streamContext) { - return new Response( - await streamContext.resumableStream(streamId, () => stream), - ); - } else { - return new Response(stream); - } - } catch (error) { - recordError(chatSpan, error as Error, { - 'app.error.context': 'chat_request', - 'app.chat.id': requestBody?.id || 'unknown', - }); - chatSpan.end(); - - if (error instanceof ChatSDKError) { - return error.toResponse(); - } - - return new ChatSDKError('internal_server_error:chat').toResponse(); - } -} - -export async function GET(request: Request) { - const streamContext = getStreamContext(); - const resumeRequestedAt = new Date(); - - if (!streamContext) { - return new Response(null, { status: 204 }); - } - - const { searchParams } = new URL(request.url); - const chatId = searchParams.get('chatId'); - - if (!chatId) { - return new ChatSDKError('bad_request:api').toResponse(); - } - - const session = await auth(); - - if (!session?.user) { - return new ChatSDKError('unauthorized:chat').toResponse(); - } - - let chat: Chat; - - try { - chat = await getChatById({ id: chatId }); - } catch { - return new ChatSDKError('not_found:chat').toResponse(); - } - - if (!chat) { - return new ChatSDKError('not_found:chat').toResponse(); - } - - if (chat.visibility === 'private' && chat.userId !== session.user.id) { - return new ChatSDKError('forbidden:chat').toResponse(); - } - - const streamIds = await getStreamIdsByChatId({ chatId }); - - if (!streamIds.length) { - return new ChatSDKError('not_found:stream').toResponse(); - } - - const recentStreamId = streamIds.at(-1); - - if (!recentStreamId) { - return new ChatSDKError('not_found:stream').toResponse(); - } - - const emptyDataStream = createDataStream({ - execute: () => {}, - }); - - const stream = await streamContext.resumableStream( - recentStreamId, - () => emptyDataStream, - ); - - /* - * For when the generation is streaming during SSR - * but the resumable stream has concluded at this point. - */ - if (!stream) { - const messages = await getMessagesByChatId({ id: chatId }); - const mostRecentMessage = messages.at(-1); - - if (!mostRecentMessage) { - return new Response(emptyDataStream, { status: 200 }); - } - - if (mostRecentMessage.role !== 'assistant') { - return new Response(emptyDataStream, { status: 200 }); - } - - const messageCreatedAt = new Date(mostRecentMessage.createdAt); - - if (differenceInSeconds(resumeRequestedAt, messageCreatedAt) > 15) { - return new Response(emptyDataStream, { status: 200 }); - } - - const restoredStream = createDataStream({ - execute: (buffer) => { - buffer.writeData({ - type: 'append-message', - message: JSON.stringify(mostRecentMessage), - }); - }, - }); - - return new Response(restoredStream, { status: 200 }); - } - - return new Response(stream, { status: 200 }); -} - -export async function DELETE(request: Request) { - const { searchParams } = new URL(request.url); - const id = searchParams.get('id'); - - if (!id) { - return new ChatSDKError('bad_request:api').toResponse(); - } - - const session = await auth(); - - if (!session?.user) { - return new ChatSDKError('unauthorized:chat').toResponse(); - } - - const chat = await getChatById({ id }); - - if (chat.userId !== session.user.id) { - return new ChatSDKError('forbidden:chat').toResponse(); - } - - const deletedChat = await deleteChatById({ id }); - - return Response.json(deletedChat, { status: 200 }); -} diff --git a/app/(chat)/api/chat/schema.ts b/app/(chat)/api/chat/schema.ts deleted file mode 100644 index a452dc4..0000000 --- a/app/(chat)/api/chat/schema.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { z } from 'zod'; - -const textPartSchema = z.object({ - text: z.string().min(1).max(2000), - type: z.enum(['text']), -}); - -export const postRequestBodySchema = z.object({ - id: z.string().uuid(), - message: z.object({ - id: z.string().uuid(), - createdAt: z.coerce.date(), - role: z.enum(['user']), - content: z.string().min(1).max(2000), - parts: z.array(textPartSchema), - experimental_attachments: z - .array( - z.object({ - url: z.string().url(), - name: z.string().min(1).max(2000), - contentType: z.enum(['image/png', 'image/jpg', 'image/jpeg']), - }), - ) - .optional(), - }), - selectedChatModel: z.enum(['chat-model', 'chat-model-reasoning']), - selectedVisibilityType: z.enum(['public', 'private']), -}); - -export type PostRequestBody = z.infer; diff --git a/app/(chat)/api/chats/[id]/project/route.ts b/app/(chat)/api/chats/[id]/project/route.ts deleted file mode 100644 index 1be0b1f..0000000 --- a/app/(chat)/api/chats/[id]/project/route.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { auth } from '@/app/(auth)/auth'; -import { getProjectsByChatId, getChatById } from '@/lib/db/queries'; -import { ChatSDKError } from '@/lib/errors'; -import { recordErrorOnCurrentSpan } from '@/lib/telemetry'; - -export async function GET( - request: Request, - { params }: { params: Promise<{ id: string }> }, -) { - const { id: chatId } = await params; - - const session = await auth(); - - if (!session?.user) { - return new ChatSDKError('unauthorized:api').toResponse(); - } - - try { - // Check if chat exists and user has permission - const chat = await getChatById({ id: chatId }); - - if (!chat) { - return new ChatSDKError('not_found:api').toResponse(); - } - - if (chat.userId !== session.user.id) { - return new ChatSDKError('forbidden:api').toResponse(); - } - - const projectAssociations = await getProjectsByChatId({ chatId }); - const project = - projectAssociations.length > 0 ? projectAssociations[0] : null; - - return Response.json({ project }, { status: 200 }); - } catch (error) { - recordErrorOnCurrentSpan(error as Error, { - operation: 'get_chat_project', - 'chat.id': chatId, - 'user.id': session.user.id, - }); - // Return null project instead of error for better UX - return Response.json({ project: null }, { status: 200 }); - } -} diff --git a/app/(chat)/api/chats/route.ts b/app/(chat)/api/chats/route.ts deleted file mode 100644 index dca4408..0000000 --- a/app/(chat)/api/chats/route.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { auth } from '@/app/(auth)/auth'; -import { saveChat } from '@/lib/db/queries'; -import { ChatSDKError } from '@/lib/errors'; -import { recordErrorOnCurrentSpan } from '@/lib/telemetry'; -import { z } from 'zod'; - -const createChatSchema = z.object({ - id: z.string().uuid(), - title: z.string().max(255).optional().default('New Chat'), - visibility: z.enum(['public', 'private']).default('private'), -}); - -export async function POST(request: Request) { - const session = await auth(); - - if (!session?.user) { - return new ChatSDKError('unauthorized:api').toResponse(); - } - - let requestBody: z.infer; - try { - const json = await request.json(); - requestBody = createChatSchema.parse(json); - } catch (error) { - return new ChatSDKError( - 'bad_request:api', - 'Invalid request body format', - ).toResponse(); - } - - const { id, title, visibility } = requestBody; - - try { - await saveChat({ - id, - userId: session.user.id, - title, - visibility, - }); - - return Response.json({ id, title, visibility }, { status: 201 }); - } catch (error) { - recordErrorOnCurrentSpan(error as Error, { - operation: 'create_chat', - 'chat.id': id, - 'user.id': session.user.id, - }); - throw error; - } -} diff --git a/app/(chat)/api/document/route.ts b/app/(chat)/api/document/route.ts deleted file mode 100644 index 3f800c1..0000000 --- a/app/(chat)/api/document/route.ts +++ /dev/null @@ -1,165 +0,0 @@ -import { auth } from '@/app/(auth)/auth'; -import { - deleteDocumentsByIdAfterTimestamp, - getDocumentsById, - saveDocument, -} from '@/lib/db/queries'; -import { ChatSDKError } from '@/lib/errors'; -import { recordErrorOnCurrentSpan } from '@/lib/telemetry'; -import { z } from 'zod'; - -const documentRequestSchema = z.object({ - title: z.string().min(1).max(500), - content: z.string(), - kind: z.enum(['text', 'code', 'image', 'sheet']), -}); - -export async function GET(request: Request) { - const { searchParams } = new URL(request.url); - const id = searchParams.get('id'); - - if (!id) { - return new ChatSDKError( - 'bad_request:api', - 'Parameter id is missing', - ).toResponse(); - } - - const session = await auth(); - - if (!session?.user) { - return new ChatSDKError('unauthorized:document').toResponse(); - } - - try { - const documents = await getDocumentsById({ id }); - - const [document] = documents; - - if (!document) { - return new ChatSDKError('not_found:document').toResponse(); - } - - if (document.userId !== session.user.id) { - return new ChatSDKError('forbidden:document').toResponse(); - } - - return Response.json(documents, { status: 200 }); - } catch (error) { - recordErrorOnCurrentSpan(error as Error, { - operation: 'get_document', - 'document.id': id, - }); - throw error; - } -} - -export async function POST(request: Request) { - const { searchParams } = new URL(request.url); - const id = searchParams.get('id'); - - if (!id) { - return new ChatSDKError( - 'bad_request:api', - 'Parameter id is required.', - ).toResponse(); - } - - const session = await auth(); - - if (!session?.user) { - return new ChatSDKError('not_found:document').toResponse(); - } - - let requestBody: z.infer; - try { - const json = await request.json(); - requestBody = documentRequestSchema.parse(json); - } catch (error) { - return new ChatSDKError( - 'bad_request:api', - 'Invalid request body format', - ).toResponse(); - } - - const { content, title, kind } = requestBody; - - const documents = await getDocumentsById({ id }); - - if (documents.length > 0) { - const [document] = documents; - - if (document.userId !== session.user.id) { - return new ChatSDKError('forbidden:document').toResponse(); - } - } - - try { - const document = await saveDocument({ - id, - content, - title, - kind, - userId: session.user.id, - }); - - return Response.json(document, { status: 200 }); - } catch (error) { - recordErrorOnCurrentSpan(error as Error, { - operation: 'save_document', - 'document.id': id, - 'document.kind': kind, - }); - throw error; - } -} - -export async function DELETE(request: Request) { - const { searchParams } = new URL(request.url); - const id = searchParams.get('id'); - const timestamp = searchParams.get('timestamp'); - - if (!id) { - return new ChatSDKError( - 'bad_request:api', - 'Parameter id is required.', - ).toResponse(); - } - - if (!timestamp) { - return new ChatSDKError( - 'bad_request:api', - 'Parameter timestamp is required.', - ).toResponse(); - } - - const session = await auth(); - - if (!session?.user) { - return new ChatSDKError('unauthorized:document').toResponse(); - } - - const documents = await getDocumentsById({ id }); - - const [document] = documents; - - if (document.userId !== session.user.id) { - return new ChatSDKError('forbidden:document').toResponse(); - } - - try { - const documentsDeleted = await deleteDocumentsByIdAfterTimestamp({ - id, - timestamp: new Date(timestamp), - }); - - return Response.json(documentsDeleted, { status: 200 }); - } catch (error) { - recordErrorOnCurrentSpan(error as Error, { - operation: 'delete_document_versions', - 'document.id': id, - timestamp: timestamp, - }); - throw error; - } -} diff --git a/app/(chat)/api/files/upload/route.ts b/app/(chat)/api/files/upload/route.ts deleted file mode 100644 index 73a0202..0000000 --- a/app/(chat)/api/files/upload/route.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { put } from '@vercel/blob'; -import { NextResponse } from 'next/server'; -import { z } from 'zod'; - -import { auth } from '@/app/(auth)/auth'; -import { recordErrorOnCurrentSpan } from '@/lib/telemetry'; - -const FileSchema = z.object({ - file: z - .instanceof(File) - .refine((file) => file.size <= 5 * 1024 * 1024, { - message: 'File size should be less than 5MB', - }) - .refine( - (file) => - ['image/jpeg', 'image/png', 'image/gif', 'image/webp'].includes( - file.type, - ), - { - message: 'File type should be JPEG, PNG, GIF, or WebP', - }, - ), -}); - -export async function POST(request: Request) { - const session = await auth(); - - if (!session) { - return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); - } - - try { - const formData = await request.formData(); - const file = formData.get('file') as File; - - if (!file) { - return NextResponse.json({ error: 'No file uploaded' }, { status: 400 }); - } - - const validatedFile = FileSchema.safeParse({ file }); - - if (!validatedFile.success) { - const errorMessage = validatedFile.error.errors - .map((error) => error.message) - .join(', '); - - return NextResponse.json({ error: errorMessage }, { status: 400 }); - } - - try { - const data = await put(file.name, file, { - access: 'public', - }); - - return NextResponse.json(data); - } catch (error) { - recordErrorOnCurrentSpan(error as Error, { - operation: 'blob_upload', - filename: file.name, - 'file.size': file.size, - }); - return NextResponse.json({ error: 'Upload failed' }, { status: 500 }); - } - } catch (error) { - recordErrorOnCurrentSpan(error as Error, { - operation: 'file_upload_request', - }); - return NextResponse.json( - { error: 'Failed to process request' }, - { status: 500 }, - ); - } -} diff --git a/app/(chat)/api/history/route.ts b/app/(chat)/api/history/route.ts deleted file mode 100644 index 6da9787..0000000 --- a/app/(chat)/api/history/route.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { auth } from '@/app/(auth)/auth'; -import type { NextRequest } from 'next/server'; -import { getChatsByUserId } from '@/lib/db/queries'; -import { ChatSDKError } from '@/lib/errors'; -import { recordErrorOnCurrentSpan } from '@/lib/telemetry'; - -export async function GET(request: NextRequest) { - const { searchParams } = request.nextUrl; - - const limit = Number.parseInt(searchParams.get('limit') || '10'); - const startingAfter = searchParams.get('starting_after'); - const endingBefore = searchParams.get('ending_before'); - - if (startingAfter && endingBefore) { - return new ChatSDKError( - 'bad_request:api', - 'Only one of starting_after or ending_before can be provided.', - ).toResponse(); - } - - const session = await auth(); - - if (!session?.user) { - return new ChatSDKError('unauthorized:chat').toResponse(); - } - - try { - const chats = await getChatsByUserId({ - id: session.user.id, - limit, - startingAfter, - endingBefore, - }); - - return Response.json(chats); - } catch (error) { - recordErrorOnCurrentSpan(error as Error, { - operation: 'get_chat_history', - 'user.id': session.user.id, - 'pagination.limit': limit, - }); - throw error; - } -} diff --git a/app/(chat)/api/projects/[id]/chats/route.ts b/app/(chat)/api/projects/[id]/chats/route.ts deleted file mode 100644 index 16e752a..0000000 --- a/app/(chat)/api/projects/[id]/chats/route.ts +++ /dev/null @@ -1,177 +0,0 @@ -import { auth } from '@/app/(auth)/auth'; -import { - addChatToProject, - removeChatFromProject, - getChatsByProject, - getProjectById, - getChatById, -} from '@/lib/db/queries'; -import { ChatSDKError } from '@/lib/errors'; -import { recordErrorOnCurrentSpan } from '@/lib/telemetry'; -import { z } from 'zod'; - -const addChatSchema = z.object({ - chatId: z.string().uuid(), -}); - -export async function GET( - request: Request, - { params }: { params: Promise<{ id: string }> }, -) { - const { id: projectId } = await params; - - const session = await auth(); - - if (!session?.user) { - return new ChatSDKError('unauthorized:api').toResponse(); - } - - try { - // Check if project exists and user has permission - const project = await getProjectById({ id: projectId }); - - if (!project) { - return new ChatSDKError('not_found:api').toResponse(); - } - - if (project.userId !== session.user.id) { - return new ChatSDKError('forbidden:api').toResponse(); - } - - const chats = await getChatsByProject({ projectId }); - return Response.json(chats, { status: 200 }); - } catch (error) { - recordErrorOnCurrentSpan(error as Error, { - operation: 'get_chats_by_project', - 'project.id': projectId, - 'user.id': session.user.id, - }); - throw error; - } -} - -export async function POST( - request: Request, - { params }: { params: Promise<{ id: string }> }, -) { - const { id: projectId } = await params; - - const session = await auth(); - - if (!session?.user) { - return new ChatSDKError('unauthorized:api').toResponse(); - } - - let requestBody: z.infer; - try { - const json = await request.json(); - requestBody = addChatSchema.parse(json); - } catch (error) { - return new ChatSDKError( - 'bad_request:api', - 'Invalid request body format', - ).toResponse(); - } - - const { chatId } = requestBody; - - try { - // Check if project exists and user has permission - const project = await getProjectById({ id: projectId }); - - if (!project) { - return new ChatSDKError('not_found:api').toResponse(); - } - - if (project.userId !== session.user.id) { - return new ChatSDKError('forbidden:api').toResponse(); - } - - // Check if chat exists and user owns it - const chat = await getChatById({ id: chatId }); - - if (!chat) { - return new ChatSDKError('not_found:api').toResponse(); - } - - if (chat.userId !== session.user.id) { - return new ChatSDKError('forbidden:api').toResponse(); - } - - const [projectChatAssociation] = await addChatToProject({ - projectId, - chatId, - }); - - return Response.json(projectChatAssociation, { status: 201 }); - } catch (error) { - recordErrorOnCurrentSpan(error as Error, { - operation: 'add_chat_to_project', - 'project.id': projectId, - 'chat.id': chatId, - 'user.id': session.user.id, - }); - throw error; - } -} - -export async function DELETE( - request: Request, - { params }: { params: Promise<{ id: string }> }, -) { - const { id: projectId } = await params; - const { searchParams } = new URL(request.url); - const chatId = searchParams.get('chatId'); - - if (!chatId) { - return new ChatSDKError( - 'bad_request:api', - 'Parameter chatId is required', - ).toResponse(); - } - - const session = await auth(); - - if (!session?.user) { - return new ChatSDKError('unauthorized:api').toResponse(); - } - - try { - // Check if project exists and user has permission - const project = await getProjectById({ id: projectId }); - - if (!project) { - return new ChatSDKError('not_found:api').toResponse(); - } - - if (project.userId !== session.user.id) { - return new ChatSDKError('forbidden:api').toResponse(); - } - - // Check if chat exists and user owns it - const chat = await getChatById({ id: chatId }); - - if (!chat) { - return new ChatSDKError('not_found:api').toResponse(); - } - - if (chat.userId !== session.user.id) { - return new ChatSDKError('forbidden:api').toResponse(); - } - - const [removedAssociation] = await removeChatFromProject({ - projectId, - chatId, - }); - - return Response.json(removedAssociation, { status: 200 }); - } catch (error) { - recordErrorOnCurrentSpan(error as Error, { - operation: 'remove_chat_from_project', - 'project.id': projectId, - 'chat.id': chatId, - 'user.id': session.user.id, - }); - throw error; - } -} diff --git a/app/(chat)/api/projects/[id]/files/route.ts b/app/(chat)/api/projects/[id]/files/route.ts deleted file mode 100644 index 0792a05..0000000 --- a/app/(chat)/api/projects/[id]/files/route.ts +++ /dev/null @@ -1,252 +0,0 @@ -import { auth } from '@/app/(auth)/auth'; -import { - addFileToProject, - removeFileFromProject, - getFilesByProject, - getProjectById, - getProjectFileById, - updateProjectFile, -} from '@/lib/db/queries'; -import { ChatSDKError } from '@/lib/errors'; -import { recordErrorOnCurrentSpan } from '@/lib/telemetry'; -import { z } from 'zod'; - -const addFileSchema = z.object({ - filename: z.string().min(1).max(255), - filePath: z.string().optional(), - fileType: z.string().max(100).optional(), - content: z.string().optional(), -}); - -const updateFileSchema = z.object({ - filename: z.string().min(1).max(255).optional(), - content: z.string().optional(), -}); - -export async function GET( - request: Request, - { params }: { params: Promise<{ id: string }> }, -) { - const { id: projectId } = await params; - - const session = await auth(); - - if (!session?.user) { - return new ChatSDKError('unauthorized:api').toResponse(); - } - - try { - // Check if project exists and user has permission - const project = await getProjectById({ id: projectId }); - - if (!project) { - return new ChatSDKError('not_found:api').toResponse(); - } - - if (project.userId !== session.user.id) { - return new ChatSDKError('forbidden:api').toResponse(); - } - - const files = await getFilesByProject({ projectId }); - return Response.json(files, { status: 200 }); - } catch (error) { - recordErrorOnCurrentSpan(error as Error, { - operation: 'get_files_by_project', - 'project.id': projectId, - 'user.id': session.user.id, - }); - console.error('Error in GET /api/projects/[id]/files:', error); - // Return empty array for better UX - don't show error for zero files case - return Response.json([], { status: 200 }); - } -} - -export async function POST( - request: Request, - { params }: { params: Promise<{ id: string }> }, -) { - const { id: projectId } = await params; - - const session = await auth(); - - if (!session?.user) { - return new ChatSDKError('unauthorized:api').toResponse(); - } - - let requestBody: z.infer; - try { - const json = await request.json(); - requestBody = addFileSchema.parse(json); - } catch (error) { - return new ChatSDKError( - 'bad_request:api', - 'Invalid request body format', - ).toResponse(); - } - - const { filename, filePath, fileType, content } = requestBody; - - try { - // Check if project exists and user has permission - const project = await getProjectById({ id: projectId }); - - if (!project) { - return new ChatSDKError('not_found:api').toResponse(); - } - - if (project.userId !== session.user.id) { - return new ChatSDKError('forbidden:api').toResponse(); - } - - const [projectFile] = await addFileToProject({ - projectId, - filename, - filePath, - fileType, - content, - }); - - return Response.json(projectFile, { status: 201 }); - } catch (error) { - recordErrorOnCurrentSpan(error as Error, { - operation: 'add_file_to_project', - 'project.id': projectId, - 'file.name': filename, - 'user.id': session.user.id, - }); - throw error; - } -} - -export async function PATCH( - request: Request, - { params }: { params: Promise<{ id: string }> }, -) { - const { id: projectId } = await params; - const { searchParams } = new URL(request.url); - const fileId = searchParams.get('fileId'); - - if (!fileId) { - return new ChatSDKError( - 'bad_request:api', - 'Parameter fileId is required', - ).toResponse(); - } - - const session = await auth(); - - if (!session?.user) { - return new ChatSDKError('unauthorized:api').toResponse(); - } - - let requestBody: z.infer; - try { - const json = await request.json(); - requestBody = updateFileSchema.parse(json); - } catch (error) { - return new ChatSDKError( - 'bad_request:api', - 'Invalid request body format', - ).toResponse(); - } - - const { filename, content } = requestBody; - - try { - // Check if project exists and user has permission - const project = await getProjectById({ id: projectId }); - - if (!project) { - return new ChatSDKError('not_found:api').toResponse(); - } - - if (project.userId !== session.user.id) { - return new ChatSDKError('forbidden:api').toResponse(); - } - - // Check if file exists and belongs to the project - const file = await getProjectFileById({ id: fileId }); - - if (!file) { - return new ChatSDKError('not_found:api').toResponse(); - } - - if (file.projectId !== projectId) { - return new ChatSDKError('forbidden:api').toResponse(); - } - - const [updatedFile] = await updateProjectFile({ - id: fileId, - filename, - content, - }); - - return Response.json(updatedFile, { status: 200 }); - } catch (error) { - recordErrorOnCurrentSpan(error as Error, { - operation: 'update_project_file', - 'project.id': projectId, - 'file.id': fileId, - 'user.id': session.user.id, - }); - throw error; - } -} - -export async function DELETE( - request: Request, - { params }: { params: Promise<{ id: string }> }, -) { - const { id: projectId } = await params; - const { searchParams } = new URL(request.url); - const fileId = searchParams.get('fileId'); - - if (!fileId) { - return new ChatSDKError( - 'bad_request:api', - 'Parameter fileId is required', - ).toResponse(); - } - - const session = await auth(); - - if (!session?.user) { - return new ChatSDKError('unauthorized:api').toResponse(); - } - - try { - // Check if project exists and user has permission - const project = await getProjectById({ id: projectId }); - - if (!project) { - return new ChatSDKError('not_found:api').toResponse(); - } - - if (project.userId !== session.user.id) { - return new ChatSDKError('forbidden:api').toResponse(); - } - - // Check if file exists and belongs to the project - const file = await getProjectFileById({ id: fileId }); - - if (!file) { - return new ChatSDKError('not_found:api').toResponse(); - } - - if (file.projectId !== projectId) { - return new ChatSDKError('forbidden:api').toResponse(); - } - - const [removedFile] = await removeFileFromProject({ id: fileId }); - - return Response.json(removedFile, { status: 200 }); - } catch (error) { - recordErrorOnCurrentSpan(error as Error, { - operation: 'remove_file_from_project', - 'project.id': projectId, - 'file.id': fileId, - 'user.id': session.user.id, - }); - throw error; - } -} diff --git a/app/(chat)/api/projects/route.ts b/app/(chat)/api/projects/route.ts deleted file mode 100644 index ca9103c..0000000 --- a/app/(chat)/api/projects/route.ts +++ /dev/null @@ -1,183 +0,0 @@ -import { auth } from '@/app/(auth)/auth'; -import { - createProject, - getProjectsByUserId, - updateProject, - deleteProject, - getProjectById, -} from '@/lib/db/queries'; -import { ChatSDKError } from '@/lib/errors'; -import { recordErrorOnCurrentSpan } from '@/lib/telemetry'; -import { z } from 'zod'; - -const createProjectSchema = z.object({ - name: z.string().min(1).max(255), - description: z.string().max(1000).optional(), -}); - -const updateProjectSchema = z.object({ - name: z.string().min(1).max(255).optional(), - description: z.string().max(1000).optional(), -}); - -export async function GET() { - const session = await auth(); - - if (!session?.user) { - return new ChatSDKError('unauthorized:api').toResponse(); - } - - try { - const projects = await getProjectsByUserId({ userId: session.user.id }); - // Ensure we always return an array - const projectsArray = Array.isArray(projects) ? projects : []; - return Response.json(projectsArray, { status: 200 }); - } catch (error) { - recordErrorOnCurrentSpan(error as Error, { - operation: 'get_projects_by_user', - 'user.id': session.user.id, - }); - // Even on error, return empty array instead of error for zero projects case - console.error('Error in projects API, returning empty array:', error); - return Response.json([], { status: 200 }); - } -} - -export async function POST(request: Request) { - const session = await auth(); - - if (!session?.user) { - return new ChatSDKError('unauthorized:api').toResponse(); - } - - let requestBody: z.infer; - try { - const json = await request.json(); - requestBody = createProjectSchema.parse(json); - } catch (error) { - return new ChatSDKError( - 'bad_request:api', - 'Invalid request body format', - ).toResponse(); - } - - const { name, description } = requestBody; - - try { - const [project] = await createProject({ - name, - description, - userId: session.user.id, - }); - - return Response.json(project, { status: 201 }); - } catch (error) { - recordErrorOnCurrentSpan(error as Error, { - operation: 'create_project', - 'project.name': name, - 'user.id': session.user.id, - }); - throw error; - } -} - -export async function PATCH(request: Request) { - const { searchParams } = new URL(request.url); - const id = searchParams.get('id'); - - if (!id) { - return new ChatSDKError( - 'bad_request:api', - 'Parameter id is required', - ).toResponse(); - } - - const session = await auth(); - - if (!session?.user) { - return new ChatSDKError('unauthorized:api').toResponse(); - } - - let requestBody: z.infer; - try { - const json = await request.json(); - requestBody = updateProjectSchema.parse(json); - } catch (error) { - return new ChatSDKError( - 'bad_request:api', - 'Invalid request body format', - ).toResponse(); - } - - const { name, description } = requestBody; - - try { - // Check if project exists and user has permission - const project = await getProjectById({ id }); - - if (!project) { - return new ChatSDKError('not_found:api').toResponse(); - } - - if (project.userId !== session.user.id) { - return new ChatSDKError('forbidden:api').toResponse(); - } - - const [updatedProject] = await updateProject({ - id, - name, - description, - }); - - return Response.json(updatedProject, { status: 200 }); - } catch (error) { - recordErrorOnCurrentSpan(error as Error, { - operation: 'update_project', - 'project.id': id, - 'user.id': session.user.id, - }); - throw error; - } -} - -export async function DELETE(request: Request) { - const { searchParams } = new URL(request.url); - const id = searchParams.get('id'); - - if (!id) { - return new ChatSDKError( - 'bad_request:api', - 'Parameter id is required', - ).toResponse(); - } - - const session = await auth(); - - if (!session?.user) { - return new ChatSDKError('unauthorized:api').toResponse(); - } - - try { - // Check if project exists and user has permission - const project = await getProjectById({ id }); - - if (!project) { - return new ChatSDKError('not_found:api').toResponse(); - } - - if (project.userId !== session.user.id) { - return new ChatSDKError('forbidden:api').toResponse(); - } - - const [deletedProject] = await deleteProject({ id }); - - return Response.json(deletedProject, { status: 200 }); - } catch (error) { - recordErrorOnCurrentSpan(error as Error, { - operation: 'delete_project', - 'project.id': id, - 'user.id': session.user.id, - }); - throw error; - } -} diff --git a/app/(chat)/api/suggestions/route.ts b/app/(chat)/api/suggestions/route.ts deleted file mode 100644 index 047874b..0000000 --- a/app/(chat)/api/suggestions/route.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { auth } from '@/app/(auth)/auth'; -import { getSuggestionsByDocumentId } from '@/lib/db/queries'; -import { ChatSDKError } from '@/lib/errors'; -import { recordErrorOnCurrentSpan } from '@/lib/telemetry'; - -export async function GET(request: Request) { - const { searchParams } = new URL(request.url); - const documentId = searchParams.get('documentId'); - - if (!documentId) { - return new ChatSDKError( - 'bad_request:api', - 'Parameter documentId is required.', - ).toResponse(); - } - - const session = await auth(); - - if (!session?.user) { - return new ChatSDKError('unauthorized:suggestions').toResponse(); - } - - try { - const suggestions = await getSuggestionsByDocumentId({ - documentId, - }); - - const [suggestion] = suggestions; - - if (!suggestion) { - return Response.json([], { status: 200 }); - } - - if (suggestion.userId !== session.user.id) { - return new ChatSDKError('forbidden:api').toResponse(); - } - - return Response.json(suggestions, { status: 200 }); - } catch (error) { - recordErrorOnCurrentSpan(error as Error, { - operation: 'get_suggestions', - 'document.id': documentId, - }); - throw error; - } -} diff --git a/app/(chat)/api/vote/route.ts b/app/(chat)/api/vote/route.ts deleted file mode 100644 index 19157ab..0000000 --- a/app/(chat)/api/vote/route.ts +++ /dev/null @@ -1,94 +0,0 @@ -import { auth } from '@/app/(auth)/auth'; -import { getChatById, getVotesByChatId, voteMessage } from '@/lib/db/queries'; -import { ChatSDKError } from '@/lib/errors'; -import { recordErrorOnCurrentSpan } from '@/lib/telemetry'; - -export async function GET(request: Request) { - const { searchParams } = new URL(request.url); - const chatId = searchParams.get('chatId'); - - if (!chatId) { - return new ChatSDKError( - 'bad_request:api', - 'Parameter chatId is required.', - ).toResponse(); - } - - const session = await auth(); - - if (!session?.user) { - return new ChatSDKError('unauthorized:vote').toResponse(); - } - - const chat = await getChatById({ id: chatId }); - - if (!chat) { - return new ChatSDKError('not_found:chat').toResponse(); - } - - if (chat.userId !== session.user.id) { - return new ChatSDKError('forbidden:vote').toResponse(); - } - - try { - const votes = await getVotesByChatId({ id: chatId }); - - return Response.json(votes, { status: 200 }); - } catch (error) { - recordErrorOnCurrentSpan(error as Error, { - operation: 'get_votes', - 'chat.id': chatId, - }); - throw error; - } -} - -export async function PATCH(request: Request) { - const { - chatId, - messageId, - type, - }: { chatId: string; messageId: string; type: 'up' | 'down' } = - await request.json(); - - if (!chatId || !messageId || !type) { - return new ChatSDKError( - 'bad_request:api', - 'Parameters chatId, messageId, and type are required.', - ).toResponse(); - } - - const session = await auth(); - - if (!session?.user) { - return new ChatSDKError('unauthorized:vote').toResponse(); - } - - const chat = await getChatById({ id: chatId }); - - if (!chat) { - return new ChatSDKError('not_found:vote').toResponse(); - } - - if (chat.userId !== session.user.id) { - return new ChatSDKError('forbidden:vote').toResponse(); - } - - try { - await voteMessage({ - chatId, - messageId, - type: type, - }); - - return new Response('Message voted', { status: 200 }); - } catch (error) { - recordErrorOnCurrentSpan(error as Error, { - operation: 'vote_message', - 'chat.id': chatId, - 'message.id': messageId, - 'vote.type': type, - }); - throw error; - } -} diff --git a/app/(chat)/chat/[id]/page.tsx b/app/(chat)/chat/[id]/page.tsx deleted file mode 100644 index cdafcbc..0000000 --- a/app/(chat)/chat/[id]/page.tsx +++ /dev/null @@ -1,99 +0,0 @@ -import { cookies } from 'next/headers'; -import { notFound, redirect } from 'next/navigation'; - -import { auth } from '@/app/(auth)/auth'; -import { Chat } from '@/components/chat'; -import { - getChatById, - getMessagesByChatId, - getProjectsByChatId, -} from '@/lib/db/queries'; -import { DataStreamHandler } from '@/components/data-stream-handler'; -import { DEFAULT_CHAT_MODEL } from '@/lib/ai/models'; -import type { DBMessage } from '@/lib/db/schema'; -import type { Attachment, UIMessage } from 'ai'; - -export default async function Page(props: { params: Promise<{ id: string }> }) { - const params = await props.params; - const { id } = params; - const chat = await getChatById({ id }); - - if (!chat) { - notFound(); - } - - const session = await auth(); - - if (!session) { - redirect('/login'); - } - - if (chat.visibility === 'private') { - if (!session.user) { - return notFound(); - } - - if (session.user.id !== chat.userId) { - return notFound(); - } - } - - const messagesFromDb = await getMessagesByChatId({ - id, - }); - - // Get project associations for this chat - const projectAssociations = await getProjectsByChatId({ chatId: id }); - const project = - projectAssociations.length > 0 ? projectAssociations[0] : null; - - function convertToUIMessages(messages: Array): Array { - return messages.map((message) => ({ - id: message.id, - parts: message.parts as UIMessage['parts'], - role: message.role as UIMessage['role'], - // Note: content will soon be deprecated in @ai-sdk/react - content: '', - createdAt: message.createdAt, - experimental_attachments: - (message.attachments as Array) ?? [], - })); - } - - const cookieStore = await cookies(); - const chatModelFromCookie = cookieStore.get('chat-model'); - - if (!chatModelFromCookie) { - return ( - <> - - - - ); - } - - return ( - <> - - - - ); -} diff --git a/app/(chat)/layout.tsx b/app/(chat)/layout.tsx deleted file mode 100644 index 9310a10..0000000 --- a/app/(chat)/layout.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import { cookies } from 'next/headers'; - -import { AppSidebar } from '@/components/app-sidebar'; -import { SidebarInset, SidebarProvider } from '@/components/ui/sidebar'; -import { auth } from '../(auth)/auth'; -import Script from 'next/script'; - -export const experimental_ppr = true; - -export default async function Layout({ - children, -}: { - children: React.ReactNode; -}) { - const [session, cookieStore] = await Promise.all([auth(), cookies()]); - const isCollapsed = cookieStore.get('sidebar:state')?.value !== 'true'; - - return ( - <> -