Full Code of miurla/morphic for AI

main 42f5d8029c12 cached
295 files
1012.7 KB
257.9k tokens
611 symbols
1 requests
Download .txt
Showing preview only (1,086K chars total). Download the full file or copy to clipboard to get everything.
Repository: miurla/morphic
Branch: main
Commit: 42f5d8029c12
Files: 295
Total size: 1012.7 KB

Directory structure:
gitextract_mowel1np/

├── .eslintrc.json
├── .github/
│   ├── FUNDING.yml
│   ├── ISSUE_TEMPLATE/
│   │   ├── bug_report.yml
│   │   └── feature_request.yml
│   └── workflows/
│       ├── ci.yml
│       ├── docker-build.yml
│       └── release.yml
├── .gitignore
├── .mcp.json
├── .prettierignore
├── .vscode/
│   └── settings.json
├── AGENTS.md
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── Dockerfile
├── LICENSE
├── README.md
├── app/
│   ├── api/
│   │   ├── advanced-search/
│   │   │   └── route.ts
│   │   ├── chat/
│   │   │   └── route.ts
│   │   ├── chats/
│   │   │   └── route.ts
│   │   ├── feedback/
│   │   │   ├── __tests__/
│   │   │   │   └── route.test.ts
│   │   │   └── route.ts
│   │   └── upload/
│   │       └── route.ts
│   ├── auth/
│   │   ├── confirm/
│   │   │   └── route.ts
│   │   ├── error/
│   │   │   └── page.tsx
│   │   ├── forgot-password/
│   │   │   └── page.tsx
│   │   ├── login/
│   │   │   └── page.tsx
│   │   ├── oauth/
│   │   │   └── route.ts
│   │   ├── sign-up/
│   │   │   └── page.tsx
│   │   ├── sign-up-success/
│   │   │   └── page.tsx
│   │   └── update-password/
│   │       └── page.tsx
│   ├── globals.css
│   ├── layout.tsx
│   ├── page.tsx
│   └── search/
│       ├── [id]/
│       │   └── page.tsx
│       ├── loading.tsx
│       └── page.tsx
├── components/
│   ├── __tests__/
│   │   └── research-process-section.test.tsx
│   ├── action-buttons.tsx
│   ├── answer-section.tsx
│   ├── app-sidebar.tsx
│   ├── artifact/
│   │   ├── artifact-content.tsx
│   │   ├── artifact-context.tsx
│   │   ├── artifact-root.tsx
│   │   ├── chat-artifact-container.tsx
│   │   ├── reasoning-content.tsx
│   │   ├── search-artifact-content.tsx
│   │   ├── todo-invocation-content.tsx
│   │   └── tool-invocation-content.tsx
│   ├── attachment-preview.tsx
│   ├── auth-modal.tsx
│   ├── chat-error.tsx
│   ├── chat-messages.tsx
│   ├── chat-panel.tsx
│   ├── chat-share.tsx
│   ├── chat.tsx
│   ├── citation-context.tsx
│   ├── citation-link.tsx
│   ├── collapsible-message.tsx
│   ├── current-user-avatar.tsx
│   ├── custom-link.tsx
│   ├── data-section.tsx
│   ├── default-skeleton.tsx
│   ├── drag-overlay.tsx
│   ├── dynamic-tool-display.tsx
│   ├── error-modal.tsx
│   ├── external-link-items.tsx
│   ├── feedback-modal.tsx
│   ├── fetch-section.tsx
│   ├── file-upload-button.tsx
│   ├── forgot-password-form.tsx
│   ├── guest-menu.tsx
│   ├── header.tsx
│   ├── inspector/
│   │   ├── inspector-drawer.tsx
│   │   └── inspector-panel.tsx
│   ├── login-form.tsx
│   ├── message-actions.tsx
│   ├── message.tsx
│   ├── model-type-selector.tsx
│   ├── process-header.tsx
│   ├── process-rail.tsx
│   ├── question-confirmation.tsx
│   ├── reasoning-section.tsx
│   ├── related-questions.tsx
│   ├── render-message.tsx
│   ├── research-process-section.tsx
│   ├── retry-button.tsx
│   ├── search-mode-selector.tsx
│   ├── search-results-image.tsx
│   ├── search-results.tsx
│   ├── search-section.tsx
│   ├── section.tsx
│   ├── sidebar/
│   │   ├── chat-history-client.tsx
│   │   ├── chat-history-section.tsx
│   │   ├── chat-history-skeleton.tsx
│   │   ├── chat-menu-item.tsx
│   │   └── clear-history-action.tsx
│   ├── sign-up-form.tsx
│   ├── source-favicons.tsx
│   ├── theme-menu-items.tsx
│   ├── theme-provider.tsx
│   ├── todo-list-content.tsx
│   ├── tool-badge.tsx
│   ├── tool-section.tsx
│   ├── tool-todo-display.tsx
│   ├── ui/
│   │   ├── alert-dialog.tsx
│   │   ├── animated-logo.tsx
│   │   ├── avatar.tsx
│   │   ├── badge.tsx
│   │   ├── button.tsx
│   │   ├── card.tsx
│   │   ├── carousel.tsx
│   │   ├── checkbox.tsx
│   │   ├── collapsible.tsx
│   │   ├── command.tsx
│   │   ├── dialog.tsx
│   │   ├── drawer.tsx
│   │   ├── dropdown-menu.tsx
│   │   ├── hover-card.tsx
│   │   ├── icons.tsx
│   │   ├── index.ts
│   │   ├── input.tsx
│   │   ├── label.tsx
│   │   ├── password-input.tsx
│   │   ├── popover.tsx
│   │   ├── select.tsx
│   │   ├── separator.tsx
│   │   ├── sheet.tsx
│   │   ├── sidebar.tsx
│   │   ├── skeleton.tsx
│   │   ├── slider.tsx
│   │   ├── sonner.tsx
│   │   ├── spinner.tsx
│   │   ├── status-indicator.tsx
│   │   ├── switch.tsx
│   │   ├── textarea.tsx
│   │   ├── toggle.tsx
│   │   ├── tooltip-button.tsx
│   │   └── tooltip.tsx
│   ├── update-password-form.tsx
│   ├── uploaded-file-list.tsx
│   ├── user-file-section.tsx
│   ├── user-menu.tsx
│   ├── user-text-section.tsx
│   ├── video-carousel-dialog.tsx
│   ├── video-result-grid.tsx
│   └── video-search-results.tsx
├── components.json
├── config/
│   └── models/
│       ├── cloud.json
│       └── default.json
├── docker-compose.yaml
├── docs/
│   ├── CONFIGURATION.md
│   └── DOCKER.md
├── drizzle/
│   ├── 0000_black_lifeguard.sql
│   ├── 0001_thin_supreme_intelligence.sql
│   ├── 0002_material_crystal.sql
│   ├── 0003_heavy_whirlwind.sql
│   ├── 0004_natural_wallow.sql
│   ├── 0005_awesome_riptide.sql
│   ├── 0006_brainy_wrecking_crew.sql
│   ├── 0007_illegal_mephistopheles.sql
│   ├── 0008_glamorous_riptide.sql
│   ├── 0009_thankful_may_parker.sql
│   ├── 0010_lonely_kang.sql
│   ├── meta/
│   │   ├── 0000_snapshot.json
│   │   ├── 0001_snapshot.json
│   │   ├── 0002_snapshot.json
│   │   ├── 0003_snapshot.json
│   │   ├── 0004_snapshot.json
│   │   ├── 0005_snapshot.json
│   │   ├── 0006_snapshot.json
│   │   ├── 0007_snapshot.json
│   │   ├── 0008_snapshot.json
│   │   ├── 0009_snapshot.json
│   │   ├── 0010_snapshot.json
│   │   └── _journal.json
│   ├── relations.ts
│   └── schema.ts
├── drizzle.config.ts
├── hooks/
│   ├── use-auth-check.tsx
│   ├── use-current-user-image.ts
│   ├── use-current-user-name.ts
│   ├── use-file-dropzone.ts
│   └── use-mobile.tsx
├── instrumentation.ts
├── lib/
│   ├── actions/
│   │   ├── __tests__/
│   │   │   ├── chat.test.ts
│   │   │   └── feedback.test.ts
│   │   ├── chat.ts
│   │   ├── feedback.ts
│   │   └── site-feedback.ts
│   ├── agents/
│   │   ├── generate-related-questions.ts
│   │   ├── prompts/
│   │   │   ├── related-questions-prompt.ts
│   │   │   └── search-mode-prompts.ts
│   │   ├── researcher.ts
│   │   └── title-generator.ts
│   ├── analytics/
│   │   ├── index.ts
│   │   ├── track-chat-event.ts
│   │   ├── types.ts
│   │   └── utils.ts
│   ├── auth/
│   │   └── get-current-user.ts
│   ├── config/
│   │   ├── load-models-config.ts
│   │   ├── model-types.ts
│   │   ├── ollama-validator.ts
│   │   └── search-modes.ts
│   ├── constants/
│   │   └── index.ts
│   ├── contexts/
│   │   └── user-context.tsx
│   ├── db/
│   │   ├── __tests__/
│   │   │   ├── rls-policies.integration.test.ts
│   │   │   └── with-rls.test.ts
│   │   ├── actions.ts
│   │   ├── index.ts
│   │   ├── migrate.ts
│   │   ├── relations.ts
│   │   ├── schema.ts
│   │   └── with-rls.ts
│   ├── firecrawl/
│   │   ├── client.ts
│   │   ├── index.ts
│   │   └── types.ts
│   ├── hooks/
│   │   ├── use-copy-to-clipboard.ts
│   │   └── use-media-query.ts
│   ├── ollama/
│   │   ├── client.ts
│   │   └── types.ts
│   ├── rate-limit/
│   │   ├── __tests__/
│   │   │   └── guest-limit.test.ts
│   │   ├── chat-limits.ts
│   │   └── guest-limit.ts
│   ├── schema/
│   │   ├── fetch.tsx
│   │   ├── question.ts
│   │   ├── related.tsx
│   │   └── search.tsx
│   ├── storage/
│   │   └── r2-client.ts
│   ├── streaming/
│   │   ├── __tests__/
│   │   │   ├── create-ephemeral-chat-stream-response.test.ts
│   │   │   └── prune-messages-integration.test.ts
│   │   ├── create-chat-stream-response.ts
│   │   ├── create-ephemeral-chat-stream-response.ts
│   │   ├── helpers/
│   │   │   ├── __tests__/
│   │   │   │   └── prepare-messages.test.ts
│   │   │   ├── persist-stream-results.ts
│   │   │   ├── prepare-messages.ts
│   │   │   ├── stream-related-questions.ts
│   │   │   ├── strip-reasoning-parts.ts
│   │   │   └── types.ts
│   │   └── types.ts
│   ├── supabase/
│   │   ├── client.ts
│   │   ├── middleware.ts
│   │   └── server.ts
│   ├── tools/
│   │   ├── dynamic.ts
│   │   ├── fetch.ts
│   │   ├── question.ts
│   │   ├── search/
│   │   │   └── providers/
│   │   │       ├── base.ts
│   │   │       ├── brave.ts
│   │   │       ├── exa.ts
│   │   │       ├── firecrawl.ts
│   │   │       ├── index.ts
│   │   │       ├── searxng.ts
│   │   │       └── tavily.ts
│   │   ├── search.ts
│   │   └── todo.ts
│   ├── types/
│   │   ├── agent.ts
│   │   ├── ai.ts
│   │   ├── dynamic-tools.ts
│   │   ├── index.ts
│   │   ├── message-persistence.ts
│   │   ├── model-type.ts
│   │   ├── models.ts
│   │   └── search.ts
│   └── utils/
│       ├── __tests__/
│       │   ├── citation.test.ts
│       │   ├── context-window.test.ts
│       │   ├── domain.test.ts
│       │   └── model-selection.test.ts
│       ├── citation.ts
│       ├── context-window.ts
│       ├── cookies.ts
│       ├── domain.ts
│       ├── index.ts
│       ├── message-mapping.ts
│       ├── message-utils.ts
│       ├── model-selection.ts
│       ├── perf-logging.ts
│       ├── perf-tracking.ts
│       ├── registry.ts
│       ├── retry.ts
│       ├── search-config.ts
│       ├── telemetry.ts
│       └── url.ts
├── next.config.mjs
├── package.json
├── postcss.config.mjs
├── prettier.config.js
├── proxy.ts
├── scripts/
│   ├── README.md
│   ├── chat-cli.ts
│   └── test-cache-performance.ts
├── searxng-limiter.toml
├── searxng-settings.yml
├── tsconfig.json
├── vitest.config.mts
└── vitest.setup.ts

================================================
FILE CONTENTS
================================================

================================================
FILE: .eslintrc.json
================================================
{
  "extends": "next/core-web-vitals",
  "plugins": ["simple-import-sort"],
  "rules": {
    "simple-import-sort/imports": [
      "error",
      {
        "groups": [
          // React and Next.js imports
          ["^react", "^next"],
          // Third party imports
          ["^@?\\w"],
          // Internal imports
          ["^@/types"],
          ["^@/config"],
          ["^@/lib"],
          ["^@/hooks"],
          ["^@/components/ui"],
          ["^@/components"],
          ["^@/registry"],
          ["^@/styles"],
          ["^@/app"],
          // Side effect imports
          ["^\\u0000"],
          // Parent imports
          ["^\\.\\.(?!/?$)", "^\\.\\./?$"],
          // Other relative imports
          ["^\\./(?=.*/)(?!/?$)", "^\\.(?!/?$)", "^\\./?$"],
          // Style imports
          ["^.+\\.s?css$"]
        ]
      }
    ],
    "simple-import-sort/exports": "error"
  }
}


================================================
FILE: .github/FUNDING.yml
================================================
# These are supported funding model platforms

github: miurla


================================================
FILE: .github/ISSUE_TEMPLATE/bug_report.yml
================================================
name: 🐞 Bug
description: File a bug/issue
title: '[BUG] <title>'
labels: ['Bug', 'Needs Triage']
body:
  - type: checkboxes
    attributes:
      label: Is there an existing issue for this?
      description: Please search to see if an issue already exists for the bug you encountered.
      options:
        - label: I have searched the existing issues
          required: true
  - type: checkboxes
    attributes:
      label: Vercel Runtime Logs
      description: If this is a Vercel environment issue, have you checked the Vercel Runtime Logs? (https://vercel.com/docs/observability/runtime-logs)
      options:
        - label: I have checked the Vercel Runtime Logs for errors (if applicable)
          required: false
  - type: textarea
    attributes:
      label: Current Behavior
      description: A concise description of what you're experiencing.
    validations:
      required: true
  - type: textarea
    attributes:
      label: Expected Behavior
      description: A concise description of what you expected to happen.
    validations:
      required: true
  - type: textarea
    attributes:
      label: Steps To Reproduce
      description: Steps to reproduce the behavior.
      placeholder: |
        1. In this environment...
        2. With this config...
        3. Run '...'
        4. See error...
    validations:
      required: true
  - type: textarea
    attributes:
      label: Environment
      description: |
        examples:
          - Browser: Chrome 52.0.2743.116
      value: |
        - OS:
        - Browser:
      render: markdown
    validations:
      required: true
  - type: textarea
    attributes:
      label: Anything else?
      description: |
        Links? References? Anything that will give us more context about the issue you are encountering!

        Tip: You can attach images or log files by clicking this area to highlight it and then dragging files in.
    validations:
      required: false


================================================
FILE: .github/ISSUE_TEMPLATE/feature_request.yml
================================================
name: ✨ Feature Request
description: Propose a new feature for Morphic.
labels: []
body:
  - type: markdown
    attributes:
      value: |
        This template is to propose new features for Morphic. Please fill out the following information to help us understand your feature request.
  - type: textarea
    attributes:
      label: Feature Description
      description: A detailed description of the feature you are proposing for Morphic. Include any relevant technical details.
      placeholder: |
        Feature description...
    validations:
      required: true
  - type: textarea
    attributes:
      label: Use Case
      description: Provide a use case where this feature would be beneficial
      placeholder: |
        Use case...
    validations:
      required: true
  - type: textarea
    attributes:
      label: Additional context
      description: |
        Any extra information that might help us understand your feature request.
      placeholder: |
        Additional context...


================================================
FILE: .github/workflows/ci.yml
================================================
name: CI

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  lint:
    runs-on: ubuntu-latest
    name: Lint

    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Setup Bun
        uses: oven-sh/setup-bun@v2
        with:
          bun-version: 1.2.12

      - name: Install dependencies
        run: bun install

      - name: Run linting
        run: bun lint

  typecheck:
    runs-on: ubuntu-latest
    name: Type Check

    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Setup Bun
        uses: oven-sh/setup-bun@v2
        with:
          bun-version: 1.2.12

      - name: Install dependencies
        run: bun install

      - name: Run type checking
        run: bun typecheck

  format:
    runs-on: ubuntu-latest
    name: Format Check

    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Setup Bun
        uses: oven-sh/setup-bun@v2
        with:
          bun-version: 1.2.12

      - name: Install dependencies
        run: bun install

      - name: Check formatting
        run: bun format:check

  test:
    runs-on: ubuntu-latest
    name: Test

    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Setup Bun
        uses: oven-sh/setup-bun@v2
        with:
          bun-version: 1.2.12

      - name: Install dependencies
        run: bun install

      - name: Run tests
        run: |
          if [ -f "vitest.config.ts" ] || [ -f "vitest.config.mts" ]; then
            bun run test
          fi
        env:
          DATABASE_URL: postgresql://user:pass@localhost:5432/db
          NODE_ENV: test

  build:
    runs-on: ubuntu-latest
    name: Build
    needs: [lint, typecheck, format, test]

    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Setup Bun
        uses: oven-sh/setup-bun@v2
        with:
          bun-version: 1.2.12

      - name: Install dependencies
        run: bun install

      - name: Build application
        run: bun run build
        env:
          DATABASE_URL: postgresql://user:pass@localhost:5432/db


================================================
FILE: .github/workflows/docker-build.yml
================================================
name: Docker Build and Push

on:
  push:
    branches: [main]

jobs:
  build:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write

    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Set up QEMU
        uses: docker/setup-qemu-action@v3

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3

      - name: Login to GitHub Container Registry
        uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Build and push Docker image
        uses: docker/build-push-action@v5
        with:
          context: .
          platforms: linux/amd64,linux/arm64
          push: true
          tags: |
            ghcr.io/${{ github.repository }}:latest
            ghcr.io/${{ github.repository }}:${{ github.sha }}
          cache-from: type=gha
          cache-to: type=gha,mode=max


================================================
FILE: .github/workflows/release.yml
================================================
name: Release

on:
  push:
    tags:
      - 'v*' # e.g. v1.0.0, v1.0.0-beta.5

concurrency:
  group: release-${{ github.ref_name }}
  cancel-in-progress: false

jobs:
  gate:
    name: Select target branch and validate
    runs-on: ubuntu-latest
    outputs:
      allowed: ${{ steps.check.outputs.allowed }}
      prerelease: ${{ steps.check.outputs.prerelease }}
      targetBranch: ${{ steps.check.outputs.targetBranch }}
    steps:
      - name: Checkout
        uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - name: Validate tag and choose branch
        id: check
        shell: bash
        run: |
          set -euo pipefail
          TAG="${GITHUB_REF_NAME}"

          # Validate semver-like tag: vMAJOR.MINOR.PATCH[-PRERELEASE]
          if [[ ! "$TAG" =~ ^v([0-9]+)\.([0-9]+)\.([0-9]+)(-.+)?$ ]]; then
            echo "::error::Invalid semver tag: $TAG"; exit 1
          fi

          MAJOR="${BASH_REMATCH[1]}"
          MINOR="${BASH_REMATCH[2]}"
          PATCH="${BASH_REMATCH[3]}"
          PRERE="${BASH_REMATCH[4]:-}"
          BASE="v${MAJOR}.${MINOR}.${PATCH}"

          if [[ -n "$PRERE" ]]; then
            echo "prerelease=true" >> "$GITHUB_OUTPUT"
            # Candidate release branches for pre-releases (highest priority first)
            CANDIDATES=(
              "origin/${BASE}"
              "origin/release/${BASE}"
              "origin/v${MAJOR}.${MINOR}"
              "origin/release/v${MAJOR}.${MINOR}"
            )
          else
            echo "prerelease=false" >> "$GITHUB_OUTPUT"
            CANDIDATES=("origin/main")
          fi

          git fetch --no-tags origin +refs/heads/*:refs/remotes/origin/*

          TARGET=""
          for ref in "${CANDIDATES[@]}"; do
            if git rev-parse --verify -q "$ref" >/dev/null; then
              if git merge-base --is-ancestor "$GITHUB_SHA" "$ref"; then
                TARGET="$ref"; break
              fi
            fi
          done

          if [[ -z "$TARGET" ]]; then
            echo "allowed=false" >> "$GITHUB_OUTPUT"
            echo "::warning::Tag $TAG is not contained in any target branch candidate (${CANDIDATES[*]}). Skipping release."
          else
            echo "allowed=true" >> "$GITHUB_OUTPUT"
            echo "targetBranch=$TARGET" >> "$GITHUB_OUTPUT"
            echo "Selected target: $TARGET"
          fi

  release:
    name: Build and create GitHub Release
    needs: gate
    if: needs.gate.outputs.allowed == 'true'
    runs-on: ubuntu-latest
    permissions:
      contents: write
      packages: write
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Setup Bun
        uses: oven-sh/setup-bun@v2
        with:
          bun-version: 'latest'

      - name: Install deps
        run: bun install

      - name: Typecheck
        run: bun run typecheck

      - name: Lint
        run: bun run lint

      - name: Build
        run: bun run build
        env:
          DATABASE_URL: postgresql://user:pass@localhost:5432/db

      # Optional: attach build artifacts to the release
      # - name: Archive Next.js build output
      #   run: tar -czf next-build.tar.gz .next

      - name: Create GitHub Release
        uses: ncipollo/release-action@v1
        with:
          tag: ${{ github.ref_name }}
          generateReleaseNotes: true
          prerelease: ${{ needs.gate.outputs.prerelease }}
          # artifacts: next-build.tar.gz


================================================
FILE: .gitignore
================================================
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

# dependencies
/node_modules
/.pnp
.pnp.js
.yarn/install-state.gz

# testing
/coverage

# next.js
/.next/
/out/

# production
/build

# misc
.DS_Store
*.pem

# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# local env files
.env*.local

# vercel
.vercel

# typescript
*.tsbuildinfo
next-env.d.ts

# temporary files
.tmp/

================================================
FILE: .mcp.json
================================================
{
  "mcpServers": {
    "next-devtools": {
      "type": "stdio",
      "command": "npx",
      "args": ["next-devtools-mcp@latest"],
      "env": {}
    }
  }
}


================================================
FILE: .prettierignore
================================================
# Dependencies
node_modules/
.next/
out/
build/
dist/

# Environment
.env*

# Generated files
*.lock
bun.lock
package-lock.json
yarn.lock
pnpm-lock.yaml

# IDE
.vscode/
.idea/

# Misc
*.log
.DS_Store
coverage/
.cache/

# Next.js
.next/
out/

# Production
build/

# Temporary files
tmp/
temp/

# Database
lib/db/migrations/

================================================
FILE: .vscode/settings.json
================================================
{
  "editor.formatOnSave": true,
  "editor.defaultFormatter": "esbenp.prettier-vscode",
  "cSpell.words": ["openai", "Tavily"],
  "editor.codeActionsOnSave": {
    "source.organizeImports": "always"
  }
}


================================================
FILE: AGENTS.md
================================================
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Key Commands

### Development

- `bun dev` - Start development server with Turbopack (http://localhost:3000)
- `bun run build` - Create production build
- `bun start` - Start production server
- `bun lint` - Run ESLint for code quality checks and import sorting
- `bun typecheck` - Run TypeScript type checking
- `bun format` - Format code with Prettier
- `bun format:check` - Check code formatting without modifying files
- `bun migrate` - Run database migrations
- `bun run test` - Run tests with Vitest (use this, NOT `bun test`)
- `bun run test:watch` - Run tests in watch mode

### Docker

- `docker compose up -d` - Run the application with Docker (includes PostgreSQL 17, Redis, Morphic app, and SearXNG)
- `docker compose down` - Stop all containers
- `docker compose down -v` - Stop all containers and remove volumes (deletes database data)
- `docker pull ghcr.io/miurla/morphic:latest` - Pull prebuilt Docker image

#### Docker Authentication

**Default Behavior**: Docker deployments run in **anonymous mode** (authentication disabled).

When running with Docker Compose, `ENABLE_AUTH=false` is set by default, allowing personal use without Supabase setup. All users share a single anonymous user ID.

**⚠️ Security Warning:**

- Anonymous mode is **only for personal, single-user local environments**
- All chat history is shared under one user ID
- **NOT suitable** for multi-user or production deployments
- Morphic Cloud deployments block `ENABLE_AUTH=false` automatically

**Enabling Authentication:**
To require Supabase authentication, set:

```bash
ENABLE_AUTH=true  # or remove ENABLE_AUTH from docker-compose.yaml
NEXT_PUBLIC_SUPABASE_URL=[your-supabase-url]
NEXT_PUBLIC_SUPABASE_ANON_KEY=[your-supabase-anon-key]
```

**Implementation:**

- Auth logic: [lib/auth/get-current-user.ts:22-40](lib/auth/get-current-user.ts#L22-L40)
- Always warns when `ENABLE_AUTH=false` (except in tests)
- Guards against `MORPHIC_CLOUD_DEPLOYMENT=true`

## Architecture Overview

### Tech Stack

- **Next.js 16.0.0** with App Router, React Server Components, and Turbopack
- **React 19.2.0** with TypeScript for type safety
- **Vercel AI SDK 5.0.0-alpha.2** for AI streaming and GenerativeUI
- **Supabase** for authentication and backend services
- **PostgreSQL** with Drizzle ORM for database and chat history storage
- **Redis** (Upstash or local) for SearXNG advanced search caching
- **Tailwind CSS** with shadcn/ui components

### Core Architecture

1. **App Router Structure** (`/app`)
   - `/api/` - Backend API routes for chat, search, and auth endpoints
   - `/auth/` - Authentication pages (login, signup, password reset)
   - `/search/` - Search functionality and results display
   - `/share/` - Sharing functionality for search results

2. **AI Integration** (`/lib`)
   - `/lib/agents/` - AI agents for research and question generation
   - `/lib/config/` - Model configuration management
   - `/lib/streaming/` - Stream handling for AI responses
   - `/lib/tools/` - Search and retrieval tool implementations
   - Models configured in `public/config/models.json`

3. **Database** (`/lib/db`)
   - PostgreSQL database with Drizzle ORM
   - Schema defined in `/lib/db/schema.ts`
   - Migrations in `/lib/db/migrations/`
   - Database actions in `/lib/actions/chat-db.ts`

4. **Search System**
   - Multiple providers: Tavily (default), SearXNG (self-hosted), Exa (neural), Brave (optional)
   - Brave Search is optional; if API key is not provided, type="general" searches fall back to primary provider
   - Video/image search support depends on configured providers (Brave provides best multimedia support)
   - URL-specific search capabilities
   - Configurable search depth and result limits

5. **Component Organization** (`/components`)
   - `/artifact/` - Search result and AI response display components
   - `/sidebar/` - Chat history and navigation
   - `/ui/` - Reusable UI components from shadcn/ui
   - Feature-specific components (auth forms, chat interfaces)

6. **State Management**
   - Server-side state via React Server Components
   - Client-side hooks in `/hooks/`
   - Redis for persistent chat history
   - Supabase for user data

## Environment Configuration

### Required Variables

```bash
OPENAI_API_KEY=      # Default AI provider
TAVILY_API_KEY=      # Default search provider
DATABASE_URL=        # PostgreSQL connection string
```

### Optional Features

- Chat history: Set `ENABLE_SAVE_CHAT_HISTORY=true` and configure Redis
- Alternative AI providers: Add corresponding API keys (ANTHROPIC_API_KEY, GOOGLE_GENERATIVE_AI_API_KEY, etc.)
- Alternative search: Configure SEARCH_API and provider-specific settings
- Sharing: Set `NEXT_PUBLIC_ENABLE_SHARE=true`

## Key Development Patterns

1. **AI Streaming**: Uses Vercel AI SDK's streaming capabilities for real-time responses
2. **GenerativeUI**: Dynamic UI components generated based on AI responses
3. **Type Safety**: Strict TypeScript configuration with comprehensive type definitions in `/lib/types/`
4. **Schema Validation**: Zod schemas in `/lib/schema/` for data validation
5. **Error Handling**: Comprehensive error boundaries and fallback UI components

## Testing Approach

- Unit and integration tests with Vitest
- Test files located alongside source files with `.test.ts` or `.test.tsx` extension
- **Run `bun run test` to execute all tests** (NOT `bun test` - that uses Bun's built-in test runner which lacks Vitest features)
- Run `bun run test:watch` for development with watch mode
- CI automatically runs `bun run test` to ensure all tests pass

## Pre-PR Requirements

Before creating a pull request, you MUST ensure all of the following checks pass:

1. **Linting**: Run `bun lint` and fix all ESLint errors and warnings (includes import sorting)
2. **Type checking**: Run `bun typecheck` to ensure no TypeScript errors
3. **Formatting**: Run `bun format:check` to verify code formatting (or `bun format` to auto-fix)
4. **Build**: Run `bun run build` to ensure the application builds successfully
5. **Tests**: Run `bun run test` to ensure all tests pass

These checks are enforced in CI/CD and PRs will fail if any of these steps don't pass.

Note: Import sorting is handled by ESLint using `eslint-plugin-simple-import-sort`. Run `bun lint --fix` to automatically sort imports according to the configured order.

## Model Configuration

Models are defined in `public/config/models.json` with:

- `id`: Model identifier
- `provider`: Display name
- `providerId`: Provider key for API routing
- `enabled`: Toggle availability
- `toolCallType`: "native" or "manual" for function calling
- `toolCallModel`: Optional override for tool calls

## Database Management

- Run `bun migrate` to apply database migrations
- Migrations are located in `/drizzle/` directory
- Schema changes should be made in `/lib/db/schema.ts`
- Use Drizzle Kit for generating migrations

## MCP (Model Context Protocol) Integration

This project supports MCP for enhanced AI assistant integration with Next.js 16.

### Built-in Next.js MCP Server

Next.js 16 provides a built-in MCP server at `http://localhost:3000/_next/mcp` when the dev server is running.

**Available Tools:**

- `get_project_metadata` - Get project path and dev server URL
- `get_errors` - Retrieve current error state (global errors, runtime errors, build errors)
- `get_page_metadata` - Get runtime metadata about current page renders
- `get_logs` - Access Next.js development log file path
- `get_server_action_by_id` - Locate Server Actions by ID

**Usage:**

1. Start the dev server: `bun dev`
2. MCP endpoint is automatically available at `/_next/mcp`
3. AI assistants can query real-time app state, errors, and logs

### Next DevTools MCP (External)

The project includes `.mcp.json` configuration for the Next DevTools MCP package, which provides:

- Next.js knowledge base access
- Automated migration tools
- Cache optimization guides
- Browser testing capabilities

**Setup:**
The `.mcp.json` file in the project root enables team-wide MCP tool sharing. AI assistants like Claude Code will prompt for approval before using project-scoped servers.

**Benefits:**

- Real-time access to application internal state
- Improved debugging and error diagnostics
- Context-aware code suggestions
- Live application state querying


================================================
FILE: CODE_OF_CONDUCT.md
================================================
# Code of Conduct

## Our Pledge

We as members, contributors, and leaders pledge to make participation in our
community a harassment-free experience for everyone, regardless of age, body
size, visible or invisible disability, ethnicity, sex characteristics, gender
identity and expression, level of experience, education, socio-economic status,
nationality, personal appearance, race, religion, or sexual identity
and orientation.

We pledge to act and interact in ways that contribute to an open, welcoming,
diverse, inclusive, and healthy community.

## Our Standards

Examples of behavior that contributes to a positive environment for our
community include:

- Demonstrating empathy and kindness toward other people
- Being respectful of differing opinions, viewpoints, and experiences
- Giving and gracefully accepting constructive feedback
- Accepting responsibility and apologizing to those affected by our mistakes,
  and learning from the experience
- Focusing on what is best not just for us as individuals, but for the
  overall community

Examples of unacceptable behavior include:

- The use of sexualized language or imagery, and sexual attention or
  advances of any kind
- Trolling, insulting or derogatory comments, and personal or political attacks
- Public or private harassment
- Publishing others' private information, such as a physical or email
  address, without their explicit permission
- Other conduct which could reasonably be considered inappropriate in a
  professional setting

## Enforcement Responsibilities

Community leaders are responsible for clarifying and enforcing our standards of
acceptable behavior and will take appropriate and fair corrective action in
response to any behavior that they deem inappropriate, threatening, offensive,
or harmful.

## Scope

This Code of Conduct applies within all community spaces, and also applies when
an individual is officially representing the community in public spaces.

## Enforcement

Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement at
[INSERT CONTACT METHOD].
All complaints will be reviewed and investigated promptly and fairly.

## Attribution

This Code of Conduct is adapted from the [Contributor Covenant][homepage],
version 2.0, available at
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.

[homepage]: https://www.contributor-covenant.org


================================================
FILE: CONTRIBUTING.md
================================================
# Contributing to Morphic

Thank you for your interest in contributing to Morphic! This document provides guidelines and instructions for contributing.

## Code of Conduct

By participating in this project, you are expected to uphold our [Code of Conduct](CODE_OF_CONDUCT.md).

## How to Contribute

### Reporting Issues

- Check if the issue already exists in our [GitHub Issues](https://github.com/miurla/morphic/issues)
- Use the issue templates when creating a new issue
- Provide as much context as possible

### Pull Requests

1. Fork the repository
2. Create a new branch from `main`:
   ```bash
   git checkout -b feat/your-feature-name
   ```
3. Make your changes
4. Commit your changes using conventional commits:
   ```bash
   git commit -m "feat: add new feature"
   ```
5. Push to your fork
6. Open a Pull Request

### Commit Convention

We use conventional commits. Examples:

- `feat: add new feature`
- `fix: resolve issue with X`
- `docs: update README`
- `chore: update dependencies`
- `refactor: improve code structure`

### Development Setup

Follow the [Quickstart](README.md#-quickstart) guide in the README to set up your development environment.

## License

By contributing, you agree that your contributions will be licensed under the Apache-2.0 License.


================================================
FILE: Dockerfile
================================================
# Build stage - Use Node for Next.js 16 compatibility (Bun lacks worker_threads support on arm64)
FROM node:22-slim AS builder

WORKDIR /app

# Install bun for dependency management
RUN npm install -g bun

# Install dependencies (separated for better cache utilization)
COPY package.json bun.lock ./
RUN bun install

# Copy source code and build
COPY . .
RUN npx next telemetry disable
ENV DATABASE_URL=postgresql://user:pass@localhost:5432/db
RUN npm run build

# Runtime stage
FROM oven/bun:1.2.12 AS runner
WORKDIR /app

# Copy only necessary files from builder
COPY --from=builder /app/.next ./.next
COPY --from=builder /app/public ./public
COPY --from=builder /app/package.json ./package.json
COPY --from=builder /app/bun.lock ./bun.lock
COPY --from=builder /app/node_modules ./node_modules

# Copy migration files and scripts
COPY --from=builder /app/drizzle ./drizzle
COPY --from=builder /app/lib/db ./lib/db
COPY --from=builder /app/drizzle.config.ts ./drizzle.config.ts

# Create entrypoint script for database migration
RUN echo '#!/bin/sh\n\
set -e\n\
echo "Running database migrations..."\n\
bun run migrate\n\
echo "Migrations completed. Starting server..."\n\
exec "$@"\n' > /app/docker-entrypoint.sh && chmod +x /app/docker-entrypoint.sh

# Start production server with migration
ENTRYPOINT ["/app/docker-entrypoint.sh"]
CMD ["bun", "start", "-H", "0.0.0.0"]


================================================
FILE: LICENSE
================================================
                                 Apache License
                           Version 2.0, January 2004
                        http://www.apache.org/licenses/

TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION

1.  Definitions.

    "License" shall mean the terms and conditions for use, reproduction,
    and distribution as defined by Sections 1 through 9 of this document.

    "Licensor" shall mean the copyright owner or entity authorized by
    the copyright owner that is granting the License.

    "Legal Entity" shall mean the union of the acting entity and all
    other entities that control, are controlled by, or are under common
    control with that entity. For the purposes of this definition,
    "control" means (i) the power, direct or indirect, to cause the
    direction or management of such entity, whether by contract or
    otherwise, or (ii) ownership of fifty percent (50%) or more of the
    outstanding shares, or (iii) beneficial ownership of such entity.

    "You" (or "Your") shall mean an individual or Legal Entity
    exercising permissions granted by this License.

    "Source" form shall mean the preferred form for making modifications,
    including but not limited to software source code, documentation
    source, and configuration files.

    "Object" form shall mean any form resulting from mechanical
    transformation or translation of a Source form, including but
    not limited to compiled object code, generated documentation,
    and conversions to other media types.

    "Work" shall mean the work of authorship, whether in Source or
    Object form, made available under the License, as indicated by a
    copyright notice that is included in or attached to the work
    (an example is provided in the Appendix below).

    "Derivative Works" shall mean any work, whether in Source or Object
    form, that is based on (or derived from) the Work and for which the
    editorial revisions, annotations, elaborations, or other modifications
    represent, as a whole, an original work of authorship. For the purposes
    of this License, Derivative Works shall not include works that remain
    separable from, or merely link (or bind by name) to the interfaces of,
    the Work and Derivative Works thereof.

    "Contribution" shall mean any work of authorship, including
    the original version of the Work and any modifications or additions
    to that Work or Derivative Works thereof, that is intentionally
    submitted to Licensor for inclusion in the Work by the copyright owner
    or by an individual or Legal Entity authorized to submit on behalf of
    the copyright owner. For the purposes of this definition, "submitted"
    means any form of electronic, verbal, or written communication sent
    to the Licensor or its representatives, including but not limited to
    communication on electronic mailing lists, source code control systems,
    and issue tracking systems that are managed by, or on behalf of, the
    Licensor for the purpose of discussing and improving the Work, but
    excluding communication that is conspicuously marked or otherwise
    designated in writing by the copyright owner as "Not a Contribution."

    "Contributor" shall mean Licensor and any individual or Legal Entity
    on behalf of whom a Contribution has been received by Licensor and
    subsequently incorporated within the Work.

2.  Grant of Copyright License. Subject to the terms and conditions of
    this License, each Contributor hereby grants to You a perpetual,
    worldwide, non-exclusive, no-charge, royalty-free, irrevocable
    copyright license to reproduce, prepare Derivative Works of,
    publicly display, publicly perform, sublicense, and distribute the
    Work and such Derivative Works in Source or Object form.

3.  Grant of Patent License. Subject to the terms and conditions of
    this License, each Contributor hereby grants to You a perpetual,
    worldwide, non-exclusive, no-charge, royalty-free, irrevocable
    (except as stated in this section) patent license to make, have made,
    use, offer to sell, sell, import, and otherwise transfer the Work,
    where such license applies only to those patent claims licensable
    by such Contributor that are necessarily infringed by their
    Contribution(s) alone or by combination of their Contribution(s)
    with the Work to which such Contribution(s) was submitted. If You
    institute patent litigation against any entity (including a
    cross-claim or counterclaim in a lawsuit) alleging that the Work
    or a Contribution incorporated within the Work constitutes direct
    or contributory patent infringement, then any patent licenses
    granted to You under this License for that Work shall terminate
    as of the date such litigation is filed.

4.  Redistribution. You may reproduce and distribute copies of the
    Work or Derivative Works thereof in any medium, with or without
    modifications, and in Source or Object form, provided that You
    meet the following conditions:

    (a) You must give any other recipients of the Work or
    Derivative Works a copy of this License; and

    (b) You must cause any modified files to carry prominent notices
    stating that You changed the files; and

    (c) You must retain, in the Source form of any Derivative Works
    that You distribute, all copyright, patent, trademark, and
    attribution notices from the Source form of the Work,
    excluding those notices that do not pertain to any part of
    the Derivative Works; and

    (d) If the Work includes a "NOTICE" text file as part of its
    distribution, then any Derivative Works that You distribute must
    include a readable copy of the attribution notices contained
    within such NOTICE file, excluding those notices that do not
    pertain to any part of the Derivative Works, in at least one
    of the following places: within a NOTICE text file distributed
    as part of the Derivative Works; within the Source form or
    documentation, if provided along with the Derivative Works; or,
    within a display generated by the Derivative Works, if and
    wherever such third-party notices normally appear. The contents
    of the NOTICE file are for informational purposes only and
    do not modify the License. You may add Your own attribution
    notices within Derivative Works that You distribute, alongside
    or as an addendum to the NOTICE text from the Work, provided
    that such additional attribution notices cannot be construed
    as modifying the License.

    You may add Your own copyright statement to Your modifications and
    may provide additional or different license terms and conditions
    for use, reproduction, or distribution of Your modifications, or
    for any such Derivative Works as a whole, provided Your use,
    reproduction, and distribution of the Work otherwise complies with
    the conditions stated in this License.

5.  Submission of Contributions. Unless You explicitly state otherwise,
    any Contribution intentionally submitted for inclusion in the Work
    by You to the Licensor shall be under the terms and conditions of
    this License, without any additional terms or conditions.
    Notwithstanding the above, nothing herein shall supersede or modify
    the terms of any separate license agreement you may have executed
    with Licensor regarding such Contributions.

6.  Trademarks. This License does not grant permission to use the trade
    names, trademarks, service marks, or product names of the Licensor,
    except as required for reasonable and customary use in describing the
    origin of the Work and reproducing the content of the NOTICE file.

7.  Disclaimer of Warranty. Unless required by applicable law or
    agreed to in writing, Licensor provides the Work (and each
    Contributor provides its Contributions) on an "AS IS" BASIS,
    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
    implied, including, without limitation, any warranties or conditions
    of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
    PARTICULAR PURPOSE. You are solely responsible for determining the
    appropriateness of using or redistributing the Work and assume any
    risks associated with Your exercise of permissions under this License.

8.  Limitation of Liability. In no event and under no legal theory,
    whether in tort (including negligence), contract, or otherwise,
    unless required by applicable law (such as deliberate and grossly
    negligent acts) or agreed to in writing, shall any Contributor be
    liable to You for damages, including any direct, indirect, special,
    incidental, or consequential damages of any character arising as a
    result of this License or out of the use or inability to use the
    Work (including but not limited to damages for loss of goodwill,
    work stoppage, computer failure or malfunction, or any and all
    other commercial damages or losses), even if such Contributor
    has been advised of the possibility of such damages.

9.  Accepting Warranty or Additional Liability. While redistributing
    the Work or Derivative Works thereof, You may choose to offer,
    and charge a fee for, acceptance of support, warranty, indemnity,
    or other liability obligations and/or rights consistent with this
    License. However, in accepting such obligations, You may act only
    on Your own behalf and on Your sole responsibility, not on behalf
    of any other Contributor, and only if You agree to indemnify,
    defend, and hold each Contributor harmless for any liability
    incurred by, or claims asserted against, such Contributor by reason
    of your accepting any such warranty or additional liability.

END OF TERMS AND CONDITIONS

APPENDIX: How to apply the Apache License to your work.

      To apply the Apache License to your work, attach the following
      boilerplate notice, with the fields enclosed by brackets "[]"
      replaced with your own identifying information. (Don't include
      the brackets!)  The text should be enclosed in the appropriate
      comment syntax for the file format. We also recommend that a
      file or class name and description of purpose be included on the
      same "printed page" as the copyright notice for easier
      identification within third-party archives.

Copyright 2024 Yoshiki Miura

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.


================================================
FILE: README.md
================================================
<div align="center">

# Morphic

An AI-powered search engine with a generative UI.

[![DeepWiki](https://img.shields.io/badge/DeepWiki-miurla%2Fmorphic-blue.svg?logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACwAAAAyCAYAAAAnWDnqAAAAAXNSR0IArs4c6QAAA05JREFUaEPtmUtyEzEQhtWTQyQLHNak2AB7ZnyXZMEjXMGeK/AIi+QuHrMnbChYY7MIh8g01fJoopFb0uhhEqqcbWTp06/uv1saEDv4O3n3dV60RfP947Mm9/SQc0ICFQgzfc4CYZoTPAswgSJCCUJUnAAoRHOAUOcATwbmVLWdGoH//PB8mnKqScAhsD0kYP3j/Yt5LPQe2KvcXmGvRHcDnpxfL2zOYJ1mFwrryWTz0advv1Ut4CJgf5uhDuDj5eUcAUoahrdY/56ebRWeraTjMt/00Sh3UDtjgHtQNHwcRGOC98BJEAEymycmYcWwOprTgcB6VZ5JK5TAJ+fXGLBm3FDAmn6oPPjR4rKCAoJCal2eAiQp2x0vxTPB3ALO2CRkwmDy5WohzBDwSEFKRwPbknEggCPB/imwrycgxX2NzoMCHhPkDwqYMr9tRcP5qNrMZHkVnOjRMWwLCcr8ohBVb1OMjxLwGCvjTikrsBOiA6fNyCrm8V1rP93iVPpwaE+gO0SsWmPiXB+jikdf6SizrT5qKasx5j8ABbHpFTx+vFXp9EnYQmLx02h1QTTrl6eDqxLnGjporxl3NL3agEvXdT0WmEost648sQOYAeJS9Q7bfUVoMGnjo4AZdUMQku50McDcMWcBPvr0SzbTAFDfvJqwLzgxwATnCgnp4wDl6Aa+Ax283gghmj+vj7feE2KBBRMW3FzOpLOADl0Isb5587h/U4gGvkt5v60Z1VLG8BhYjbzRwyQZemwAd6cCR5/XFWLYZRIMpX39AR0tjaGGiGzLVyhse5C9RKC6ai42ppWPKiBagOvaYk8lO7DajerabOZP46Lby5wKjw1HCRx7p9sVMOWGzb/vA1hwiWc6jm3MvQDTogQkiqIhJV0nBQBTU+3okKCFDy9WwferkHjtxib7t3xIUQtHxnIwtx4mpg26/HfwVNVDb4oI9RHmx5WGelRVlrtiw43zboCLaxv46AZeB3IlTkwouebTr1y2NjSpHz68WNFjHvupy3q8TFn3Hos2IAk4Ju5dCo8B3wP7VPr/FGaKiG+T+v+TQqIrOqMTL1VdWV1DdmcbO8KXBz6esmYWYKPwDL5b5FA1a0hwapHiom0r/cKaoqr+27/XcrS5UwSMbQAAAABJRU5ErkJggg==)](https://deepwiki.com/miurla/morphic) [![GitHub stars](https://img.shields.io/github/stars/miurla/morphic?style=flat&colorA=000000&colorB=000000)](https://github.com/miurla/morphic/stargazers) [![GitHub forks](https://img.shields.io/github/forks/miurla/morphic?style=flat&colorA=000000&colorB=000000)](https://github.com/miurla/morphic/network/members)

<a href="https://vercel.com/oss">
  <img alt="Vercel OSS Program" src="https://vercel.com/oss/program-badge.svg" />
</a>

<br />
<br />

<a href="https://trendshift.io/repositories/9207" target="_blank"><img src="https://trendshift.io/api/badge/repositories/9207" alt="miurla%2Fmorphic | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>

<img src="./public/screenshot-2026-02-07.png" />

</div>

## 🗂️ Overview

- 🛠 [Features](#-features)
- 🧱 [Stack](#-stack)
- 🚀 [Quickstart](#-quickstart)
- 🌐 [Deploy](#-deploy)
- 👥 [Contributing](#-contributing)
- 📄 [License](#-license)

📝 Explore AI-generated documentation on [DeepWiki](https://deepwiki.com/miurla/morphic)

## 🛠 Features

### Core Features

- AI-powered search with GenerativeUI
- Natural language question understanding
- Multiple search providers support (Tavily, Brave, SearXNG, Exa)
- Search modes: Quick, Planning, and Adaptive
- Model type selection: Speed vs Quality
- Inspector panel for tool execution and AI processing details

### Authentication

- User authentication powered by [Supabase Auth](https://supabase.com/docs/guides/auth)

### Guest Mode

- Allow users to try the app without creating an account
- No chat history stored for guests (ephemeral sessions)
- Optional daily rate limit per IP address
- Enable with `ENABLE_GUEST_CHAT=true`

### Chat & History

- Chat history automatically stored in PostgreSQL database
- Share search results with unique URLs
- Message feedback system
- File upload support

### AI Providers

- OpenAI (Default)
- Anthropic Claude
- Google Gemini
- Vercel AI Gateway
- Ollama

Models are configured in `config/models/*.json` with profile-based settings. When using non-OpenAI providers, update the model configuration files with compatible model IDs. See [Configuration Guide](docs/CONFIGURATION.md) for details.

### Search Capabilities

- URL-specific search
- Content extraction with Tavily or Jina
- Citation tracking and display
- Self-hosted search with SearXNG support

### Additional Features

- Docker deployment ready
- Browser search engine integration
- LLM observability with Langfuse (optional)
- Todo tracking for complex tasks

## 🧱 Stack

### Core Framework

- [Next.js](https://nextjs.org/) - React framework with App Router
- [TypeScript](https://www.typescriptlang.org/) - Type-safe development
- [Vercel AI SDK](https://ai-sdk.dev) - TypeScript toolkit for building AI-powered applications

### Authentication & Authorization

- [Supabase](https://supabase.com/) - User authentication and backend services

### AI & Search

- [OpenAI](https://openai.com/) - Default AI provider (Optional: Google AI, Anthropic)
- [Tavily AI](https://tavily.com/) - AI-optimized search with context
- [Brave Search](https://brave.com/search/api/) - Traditional web search results
- Tavily alternatives:
  - [SearXNG](https://docs.searxng.org/) - Self-hosted search
  - [Exa](https://exa.ai/) - Meaning-based search powered by embeddings
  - [Firecrawl](https://firecrawl.dev/) - Web, news, and image search with crawling, scraping, LLM-ready extraction, and [open source](https://github.com/firecrawl/firecrawl).

### Data Storage

- [PostgreSQL](https://www.postgresql.org/) - Primary database (supports Neon, Supabase, or standard PostgreSQL)
- [Drizzle ORM](https://orm.drizzle.team/) - Type-safe database ORM
- [Cloudflare R2](https://developers.cloudflare.com/r2/) - File storage (optional)

### UI & Styling

- [Tailwind CSS](https://tailwindcss.com/) - Utility-first CSS framework
- [shadcn/ui](https://ui.shadcn.com/) - Re-usable components
- [Radix UI](https://www.radix-ui.com/) - Unstyled, accessible components
- [Lucide Icons](https://lucide.dev/) - Beautiful & consistent icons

## 🚀 Quickstart

### 1. Fork and Clone repo

Fork the repo to your Github account, then run the following command to clone the repo:

```bash
git clone git@github.com:[YOUR_GITHUB_ACCOUNT]/morphic.git
```

### 2. Install dependencies

```bash
cd morphic
bun install
```

### 3. Configure environment variables

```bash
cp .env.local.example .env.local
```

Fill in the required environment variables in `.env.local`:

```bash
OPENAI_API_KEY=your_openai_key
TAVILY_API_KEY=your_tavily_key
```

### 4. Run app locally

```bash
bun dev
```

Visit http://localhost:3000 in your browser.

**Note**: By default, Morphic runs without a database or authentication. To enable chat history, authentication, and other features, see [CONFIGURATION.md](./docs/CONFIGURATION.md). For Docker setup, see the [Docker Guide](./docs/DOCKER.md).

## 🌐 Deploy

Host your own live version of Morphic with Vercel or Docker.

### Vercel

[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fmiurla%2Fmorphic&env=DATABASE_URL,OPENAI_API_KEY,TAVILY_API_KEY,BRAVE_SEARCH_API_KEY)

**Note**: For Vercel deployments, set `ENABLE_AUTH=true` and configure Supabase authentication to secure your deployment.

### Docker

See the [Docker Guide](./docs/DOCKER.md) for prebuilt images, Docker Compose setup, and deployment instructions.

## 👥 Contributing

We welcome contributions to Morphic! Whether it's bug reports, feature requests, or pull requests, all contributions are appreciated.

Please see our [Contributing Guide](CONTRIBUTING.md) for details on:

- How to submit issues
- How to submit pull requests
- Commit message conventions
- Development setup

## 📄 License

This project is licensed under the Apache License 2.0 - see the [LICENSE](LICENSE) file for details.


================================================
FILE: app/api/advanced-search/route.ts
================================================
import { NextResponse } from 'next/server'

import { Redis } from '@upstash/redis'
import http from 'http'
import { Agent } from 'http'
import https from 'https'
import { JSDOM, VirtualConsole } from 'jsdom'
import { createClient } from 'redis'

import {
  SearchResultItem,
  SearXNGResponse,
  SearXNGResult,
  SearXNGSearchResults
} from '@/lib/types'

/**
 * Maximum number of results to fetch from SearXNG.
 * Increasing this value can improve result quality but may impact performance.
 * In advanced search mode, this is multiplied by SEARXNG_CRAWL_MULTIPLIER for initial fetching.
 */
const SEARXNG_MAX_RESULTS = Math.max(
  10,
  Math.min(100, parseInt(process.env.SEARXNG_MAX_RESULTS || '50', 10))
)

const CACHE_TTL = 3600 // Cache time-to-live in seconds (1 hour)
const CACHE_EXPIRATION_CHECK_INTERVAL = 3600000 // 1 hour in milliseconds

let redisClient: Redis | ReturnType<typeof createClient> | null = null

// Initialize Redis client based on environment variables
async function initializeRedisClient() {
  if (redisClient) return redisClient

  const upstashRedisRestUrl = process.env.UPSTASH_REDIS_REST_URL
  const upstashRedisRestToken = process.env.UPSTASH_REDIS_REST_TOKEN

  // Use Upstash Redis if credentials are provided
  if (upstashRedisRestUrl && upstashRedisRestToken) {
    redisClient = new Redis({
      url: upstashRedisRestUrl,
      token: upstashRedisRestToken
    })
    return redisClient
  }

  // Otherwise, try to use local Redis (for Docker/SearXNG usage)
  try {
    const localRedisUrl =
      process.env.LOCAL_REDIS_URL || 'redis://localhost:6379'
    const client = createClient({ url: localRedisUrl })
    await client.connect()
    redisClient = client
  } catch (error) {
    console.warn(
      'Failed to connect to local Redis. Advanced search caching disabled.',
      error
    )
    redisClient = null
  }

  return redisClient
}

// Function to get cached results
async function getCachedResults(
  cacheKey: string
): Promise<SearXNGSearchResults | null> {
  try {
    const client = await initializeRedisClient()
    if (!client) return null

    let cachedData: string | null
    if (client instanceof Redis) {
      cachedData = await client.get(cacheKey)
    } else {
      cachedData = await client.get(cacheKey)
    }

    if (cachedData) {
      console.log(`Cache hit for key: ${cacheKey}`)
      return JSON.parse(cachedData)
    } else {
      console.log(`Cache miss for key: ${cacheKey}`)
      return null
    }
  } catch (error) {
    console.error('Redis cache error:', error)
    return null
  }
}

// Function to set cached results with error handling and logging
async function setCachedResults(
  cacheKey: string,
  results: SearXNGSearchResults
): Promise<void> {
  try {
    const client = await initializeRedisClient()
    if (!client) return

    const serializedResults = JSON.stringify(results)
    if (client instanceof Redis) {
      await client.set(cacheKey, serializedResults, { ex: CACHE_TTL })
    } else {
      await client.set(cacheKey, serializedResults, { EX: CACHE_TTL })
    }
    console.log(`Cached results for key: ${cacheKey}`)
  } catch (error) {
    console.error('Redis cache error:', error)
  }
}

// Function to periodically clean up expired cache entries
async function cleanupExpiredCache() {
  try {
    const client = await initializeRedisClient()
    if (!client) return

    const keys = await client.keys('search:*')
    for (const key of keys) {
      const ttl = await client.ttl(key)
      if (ttl <= 0) {
        await client.del(key)
        console.log(`Removed expired cache entry: ${key}`)
      }
    }
  } catch (error) {
    console.error('Cache cleanup error:', error)
  }
}

// Set up periodic cache cleanup
setInterval(cleanupExpiredCache, CACHE_EXPIRATION_CHECK_INTERVAL)

export async function POST(request: Request) {
  const { query, maxResults, searchDepth, includeDomains, excludeDomains } =
    await request.json()

  const SEARXNG_DEFAULT_DEPTH = process.env.SEARXNG_DEFAULT_DEPTH || 'basic'

  try {
    const cacheKey = `search:${query}:${maxResults}:${searchDepth}:${
      Array.isArray(includeDomains) ? includeDomains.join(',') : ''
    }:${Array.isArray(excludeDomains) ? excludeDomains.join(',') : ''}`

    // Try to get cached results
    const cachedResults = await getCachedResults(cacheKey)
    if (cachedResults) {
      return NextResponse.json(cachedResults)
    }

    // If not cached, perform the search
    const results = await advancedSearchXNGSearch(
      query,
      Math.min(maxResults, SEARXNG_MAX_RESULTS),
      searchDepth || SEARXNG_DEFAULT_DEPTH,
      Array.isArray(includeDomains) ? includeDomains : [],
      Array.isArray(excludeDomains) ? excludeDomains : []
    )

    // Cache the results
    await setCachedResults(cacheKey, results)

    return NextResponse.json(results)
  } catch (error) {
    console.error('Advanced search error:', error)
    return NextResponse.json(
      {
        message: 'Internal Server Error',
        error: error instanceof Error ? error.message : String(error),
        query: query,
        results: [],
        images: [],
        number_of_results: 0
      },
      { status: 500 }
    )
  }
}

async function advancedSearchXNGSearch(
  query: string,
  maxResults: number = 10,
  searchDepth: 'basic' | 'advanced' = 'advanced',
  includeDomains: string[] = [],
  excludeDomains: string[] = []
): Promise<SearXNGSearchResults> {
  const apiUrl = process.env.SEARXNG_API_URL
  if (!apiUrl) {
    throw new Error('SEARXNG_API_URL is not set in the environment variables')
  }

  const SEARXNG_ENGINES =
    process.env.SEARXNG_ENGINES || 'google,bing,duckduckgo,wikipedia'
  const SEARXNG_TIME_RANGE = process.env.SEARXNG_TIME_RANGE || 'None'
  const SEARXNG_SAFESEARCH = process.env.SEARXNG_SAFESEARCH || '0'
  const SEARXNG_CRAWL_MULTIPLIER = parseInt(
    process.env.SEARXNG_CRAWL_MULTIPLIER || '4',
    10
  )

  try {
    const url = new URL(`${apiUrl}/search`)
    url.searchParams.append('q', query)
    url.searchParams.append('format', 'json')
    url.searchParams.append('categories', 'general,images')

    // Add time_range if it's not 'None'
    if (SEARXNG_TIME_RANGE !== 'None') {
      url.searchParams.append('time_range', SEARXNG_TIME_RANGE)
    }

    url.searchParams.append('safesearch', SEARXNG_SAFESEARCH)
    url.searchParams.append('engines', SEARXNG_ENGINES)

    const resultsPerPage = 10
    const pageno = Math.ceil(maxResults / resultsPerPage)
    url.searchParams.append('pageno', String(pageno))

    //console.log('SearXNG API URL:', url.toString()) // Log the full URL for debugging

    const data:
      | SearXNGResponse
      | { error: string; status: number; data: string } =
      await fetchJsonWithRetry(url.toString(), 3)

    if ('error' in data) {
      console.error('Invalid response from SearXNG:', data)
      throw new Error(
        `Invalid response from SearXNG: ${data.error}. Status: ${data.status}. Data: ${data.data}`
      )
    }

    if (!data || !Array.isArray(data.results)) {
      console.error('Invalid response structure from SearXNG:', data)
      throw new Error('Invalid response structure from SearXNG')
    }

    let generalResults = data.results.filter(
      (result: SearXNGResult) => result && !result.img_src
    )

    // Apply domain filtering manually
    if (includeDomains.length > 0 || excludeDomains.length > 0) {
      generalResults = generalResults.filter(result => {
        const domain = new URL(result.url).hostname
        return (
          (includeDomains.length === 0 ||
            includeDomains.some(d => domain.includes(d))) &&
          (excludeDomains.length === 0 ||
            !excludeDomains.some(d => domain.includes(d)))
        )
      })
    }

    if (searchDepth === 'advanced') {
      const crawledResults = await Promise.all(
        generalResults
          .slice(0, maxResults * SEARXNG_CRAWL_MULTIPLIER)
          .map(result => crawlPage(result, query))
      )
      generalResults = crawledResults
        .filter(result => result !== null && isQualityContent(result.content))
        .map(result => result as SearXNGResult)

      const MIN_RELEVANCE_SCORE = 10
      generalResults = generalResults
        .map(result => ({
          ...result,
          score: calculateRelevanceScore(result, query)
        }))
        .filter(result => result.score >= MIN_RELEVANCE_SCORE)
        .sort((a, b) => b.score - a.score)
        .slice(0, maxResults)
    }

    generalResults = generalResults.slice(0, maxResults)

    const imageResults = (data.results || [])
      .filter((result: SearXNGResult) => result && result.img_src)
      .slice(0, maxResults)

    return {
      results: generalResults.map(
        (result: SearXNGResult): SearchResultItem => ({
          title: result.title || '',
          url: result.url || '',
          content: result.content || ''
        })
      ),
      query: data.query || query,
      images: imageResults
        .map((result: SearXNGResult) => {
          const imgSrc = result.img_src || ''
          return imgSrc.startsWith('http') ? imgSrc : `${apiUrl}${imgSrc}`
        })
        .filter(Boolean),
      number_of_results: data.number_of_results || generalResults.length
    }
  } catch (error) {
    console.error('SearchXNG API error:', error)
    return {
      results: [],
      query: query,
      images: [],
      number_of_results: 0
    }
  }
}

async function crawlPage(
  result: SearXNGResult,
  query: string
): Promise<SearXNGResult> {
  try {
    const html = await fetchHtmlWithTimeout(result.url, 20000)

    // virtual console to suppress JSDOM warnings
    const virtualConsole = new VirtualConsole()
    virtualConsole.on('error', () => {})
    virtualConsole.on('warn', () => {})

    const dom = new JSDOM(html, {
      runScripts: 'outside-only',
      resources: 'usable',
      virtualConsole
    })
    const document = dom.window.document

    // Remove script, style, nav, header, and footer elements
    document
      .querySelectorAll('script, style, nav, header, footer')
      .forEach((el: Element) => el.remove())

    const mainContent =
      document.querySelector('main') ||
      document.querySelector('article') ||
      document.querySelector('.content') ||
      document.querySelector('#content') ||
      document.body

    if (mainContent) {
      // Prioritize specific content elements
      const priorityElements = mainContent.querySelectorAll('h1, h2, h3, p')
      let extractedText = Array.from(priorityElements)
        .map(el => el.textContent?.trim())
        .filter(Boolean)
        .join('\n\n')

      // If not enough content, fall back to other elements
      if (extractedText.length < 500) {
        const contentElements = mainContent.querySelectorAll(
          'h4, h5, h6, li, td, th, blockquote, pre, code'
        )
        extractedText +=
          '\n\n' +
          Array.from(contentElements)
            .map(el => el.textContent?.trim())
            .filter(Boolean)
            .join('\n\n')
      }

      // Extract metadata
      const metaDescription =
        document
          .querySelector('meta[name="description"]')
          ?.getAttribute('content') || ''
      const metaKeywords =
        document
          .querySelector('meta[name="keywords"]')
          ?.getAttribute('content') || ''
      const ogTitle =
        document
          .querySelector('meta[property="og:title"]')
          ?.getAttribute('content') || ''
      const ogDescription =
        document
          .querySelector('meta[property="og:description"]')
          ?.getAttribute('content') || ''

      // Combine metadata with extracted text
      extractedText = `${result.title}\n\n${ogTitle}\n\n${metaDescription}\n\n${ogDescription}\n\n${metaKeywords}\n\n${extractedText}`

      // Limit the extracted text to 10000 characters
      extractedText = extractedText.substring(0, 10000)

      // Highlight query terms in the content
      result.content = highlightQueryTerms(extractedText, query)

      // Extract publication date
      const publishedDate = extractPublicationDate(document)
      if (publishedDate) {
        result.publishedDate = publishedDate.toISOString()
      }
    }

    return result
  } catch (error) {
    console.error(`Error crawling ${result.url}:`, error)
    return {
      ...result,
      content: result.content || 'Content unavailable due to crawling error.'
    }
  }
}

function highlightQueryTerms(content: string, query: string): string {
  try {
    const terms = query
      .toLowerCase()
      .split(/\s+/)
      .filter(term => term.length > 2)
      .map(term => term.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')) // Escape special characters

    let highlightedContent = content

    terms.forEach(term => {
      const regex = new RegExp(`\\b${term}\\b`, 'gi')
      highlightedContent = highlightedContent.replace(
        regex,
        match => `<mark>${match}</mark>`
      )
    })

    return highlightedContent
  } catch (error) {
    //console.error('Error in highlightQueryTerms:', error)
    return content // Return original content if highlighting fails
  }
}

function calculateRelevanceScore(result: SearXNGResult, query: string): number {
  try {
    const lowercaseContent = result.content.toLowerCase()
    const lowercaseQuery = query.toLowerCase()
    const queryWords = lowercaseQuery
      .split(/\s+/)
      .filter(word => word.length > 2)
      .map(word => word.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')) // Escape special characters

    let score = 0

    // Check for exact phrase match
    if (lowercaseContent.includes(lowercaseQuery)) {
      score += 30
    }

    // Check for individual word matches
    queryWords.forEach(word => {
      const regex = new RegExp(`\\b${word}\\b`, 'g')
      const wordCount = (lowercaseContent.match(regex) || []).length
      score += wordCount * 3
    })

    // Boost score for matches in the title
    const lowercaseTitle = result.title.toLowerCase()
    if (lowercaseTitle.includes(lowercaseQuery)) {
      score += 20
    }

    queryWords.forEach(word => {
      const regex = new RegExp(`\\b${word}\\b`, 'g')
      if (lowercaseTitle.match(regex)) {
        score += 10
      }
    })

    // Boost score for recent content (if available)
    if (result.publishedDate) {
      const publishDate = new Date(result.publishedDate)
      const now = new Date()
      const daysSincePublished =
        (now.getTime() - publishDate.getTime()) / (1000 * 3600 * 24)
      if (daysSincePublished < 30) {
        score += 15
      } else if (daysSincePublished < 90) {
        score += 10
      } else if (daysSincePublished < 365) {
        score += 5
      }
    }

    // Penalize very short content
    if (result.content.length < 200) {
      score -= 10
    } else if (result.content.length > 1000) {
      score += 5
    }

    // Boost score for content with more highlighted terms
    const highlightCount = (result.content.match(/<mark>/g) || []).length
    score += highlightCount * 2

    return score
  } catch (error) {
    //console.error('Error in calculateRelevanceScore:', error)
    return 0 // Return 0 if scoring fails
  }
}

function extractPublicationDate(document: Document): Date | null {
  const dateSelectors = [
    'meta[name="article:published_time"]',
    'meta[property="article:published_time"]',
    'meta[name="publication-date"]',
    'meta[name="date"]',
    'time[datetime]',
    'time[pubdate]'
  ]

  for (const selector of dateSelectors) {
    const element = document.querySelector(selector)
    if (element) {
      const dateStr =
        element.getAttribute('content') ||
        element.getAttribute('datetime') ||
        element.getAttribute('pubdate')
      if (dateStr) {
        const date = new Date(dateStr)
        if (!isNaN(date.getTime())) {
          return date
        }
      }
    }
  }

  return null
}

const httpAgent = new http.Agent({ keepAlive: true })
const httpsAgent = new https.Agent({
  keepAlive: true,
  rejectUnauthorized: true // change to false if you want to ignore SSL certificate errors
  //but use this with caution.
})

async function fetchJsonWithRetry(url: string, retries: number): Promise<any> {
  for (let i = 0; i < retries; i++) {
    try {
      return await fetchJson(url)
    } catch (error) {
      if (i === retries - 1) throw error
      await new Promise(resolve => setTimeout(resolve, 1000 * (i + 1)))
    }
  }
}

function fetchJson(url: string): Promise<any> {
  return new Promise((resolve, reject) => {
    const protocol = url.startsWith('https:') ? https : http
    const agent = url.startsWith('https:') ? httpsAgent : httpAgent
    const request = protocol.get(url, { agent }, res => {
      let data = ''
      res.on('data', chunk => {
        data += chunk
      })
      res.on('end', () => {
        try {
          // Check if the response is JSON
          if (res.headers['content-type']?.includes('application/json')) {
            resolve(JSON.parse(data))
          } else {
            // If not JSON, return an object with the raw data and status
            resolve({
              error: 'Invalid JSON response',
              status: res.statusCode,
              data: data.substring(0, 200) // Include first 200 characters of the response
            })
          }
        } catch (e) {
          reject(e)
        }
      })
    })
    request.on('error', reject)
    request.on('timeout', () => {
      request.destroy()
      reject(new Error('Request timed out'))
    })
    request.setTimeout(15000) // 15 second timeout
  })
}

async function fetchHtmlWithTimeout(
  url: string,
  timeoutMs: number
): Promise<string> {
  try {
    return await Promise.race([
      fetchHtml(url),
      timeout(timeoutMs, `Fetching ${url} timed out after ${timeoutMs}ms`)
    ])
  } catch (error) {
    console.error(`Error fetching ${url}:`, error)
    const errorMessage = error instanceof Error ? error.message : String(error)
    return `<html><body>Error fetching content: ${errorMessage}</body></html>`
  }
}

function fetchHtml(url: string): Promise<string> {
  return new Promise((resolve, reject) => {
    const protocol = url.startsWith('https:') ? https : http
    const agent = url.startsWith('https:') ? httpsAgent : httpAgent
    const request = protocol.get(url, { agent }, res => {
      if (
        res.statusCode &&
        res.statusCode >= 300 &&
        res.statusCode < 400 &&
        res.headers.location
      ) {
        // Handle redirects
        fetchHtml(new URL(res.headers.location, url).toString())
          .then(resolve)
          .catch(reject)
        return
      }
      let data = ''
      res.on('data', chunk => {
        data += chunk
      })
      res.on('end', () => resolve(data))
    })
    request.on('error', error => {
      //console.error(`Error fetching ${url}:`, error)
      reject(error)
    })
    request.on('timeout', () => {
      request.destroy()
      //reject(new Error(`Request timed out for ${url}`))
      resolve('')
    })
    request.setTimeout(10000) // 10 second timeout
  })
}

function timeout(ms: number, message: string): Promise<never> {
  return new Promise((_, reject) => {
    setTimeout(() => {
      reject(new Error(message))
    }, ms)
  })
}

function isQualityContent(text: string): boolean {
  const words = text.split(/\s+/).length
  const sentences = text.split(/[.!?]+/).length
  const avgWordsPerSentence = words / sentences

  return (
    words > 50 &&
    sentences > 3 &&
    avgWordsPerSentence > 5 &&
    avgWordsPerSentence < 30 &&
    !text.includes('Content unavailable due to crawling error') &&
    !text.includes('Error fetching content:')
  )
}


================================================
FILE: app/api/chat/route.ts
================================================
import { revalidateTag } from 'next/cache'
import { cookies } from 'next/headers'

import { loadChat } from '@/lib/actions/chat'
import { calculateConversationTurn, trackChatEvent } from '@/lib/analytics'
import { getCurrentUserId } from '@/lib/auth/get-current-user'
import { checkAndEnforceOverallChatLimit } from '@/lib/rate-limit/chat-limits'
import { checkAndEnforceGuestLimit } from '@/lib/rate-limit/guest-limit'
import { createChatStreamResponse } from '@/lib/streaming/create-chat-stream-response'
import { createEphemeralChatStreamResponse } from '@/lib/streaming/create-ephemeral-chat-stream-response'
import { SearchMode } from '@/lib/types/search'
import { selectModel } from '@/lib/utils/model-selection'
import { perfLog, perfTime } from '@/lib/utils/perf-logging'
import { resetAllCounters } from '@/lib/utils/perf-tracking'
import { isProviderEnabled } from '@/lib/utils/registry'

export const maxDuration = 300

export async function POST(req: Request) {
  const startTime = performance.now()
  const abortSignal = req.signal

  // Reset counters for new request (development only)
  if (process.env.ENABLE_PERF_LOGGING === 'true') {
    resetAllCounters()
  }

  try {
    const body = await req.json()
    const { message, messages, chatId, trigger, messageId, isNewChat } = body

    perfLog(
      `API Route - Start: chatId=${chatId}, trigger=${trigger}, isNewChat=${isNewChat}`
    )

    // Handle different triggers using AI SDK standard values
    if (trigger === 'regenerate-message') {
      if (!messageId) {
        return new Response('messageId is required for regeneration', {
          status: 400,
          statusText: 'Bad Request'
        })
      }
    } else if (trigger === 'submit-message') {
      if (!message) {
        return new Response('message is required for submission', {
          status: 400,
          statusText: 'Bad Request'
        })
      }
    }

    const referer = req.headers.get('referer')
    const isSharePage = referer?.includes('/share/')

    const authStart = performance.now()
    const userId = await getCurrentUserId()
    perfTime('Auth completed', authStart)

    if (isSharePage) {
      return new Response('Chat API is not available on share pages', {
        status: 403,
        statusText: 'Forbidden'
      })
    }

    const guestChatEnabled = process.env.ENABLE_GUEST_CHAT === 'true'
    const isGuest = !userId
    if (isGuest && !guestChatEnabled) {
      return new Response('Authentication required', {
        status: 401,
        statusText: 'Unauthorized'
      })
    }

    if (isGuest) {
      const forwardedFor = req.headers.get('x-forwarded-for') || ''
      const ip =
        forwardedFor.split(',')[0]?.trim() ||
        req.headers.get('x-real-ip') ||
        null
      const guestLimitResponse = await checkAndEnforceGuestLimit(ip)
      if (guestLimitResponse) return guestLimitResponse
    }

    const cookieStore = await cookies()

    // Get search mode from cookie
    const searchModeCookie = cookieStore.get('searchMode')?.value
    const searchMode: SearchMode =
      searchModeCookie && ['quick', 'adaptive'].includes(searchModeCookie)
        ? (searchModeCookie as SearchMode)
        : 'quick'

    const isCloudDeployment = process.env.MORPHIC_CLOUD_DEPLOYMENT === 'true'
    const forceSpeed = isGuest || isCloudDeployment
    const modelCookieStore = forceSpeed
      ? ({
          get: (name: string) =>
            name === 'modelType'
              ? ({ value: 'speed' } as const)
              : cookieStore.get(name)
        } as typeof cookieStore)
      : cookieStore

    // Select the appropriate model based on model type preference and search mode
    const selectedModel = selectModel({
      cookieStore: modelCookieStore,
      searchMode
    })

    if (!isProviderEnabled(selectedModel.providerId)) {
      return new Response(
        `Selected provider is not enabled ${selectedModel.providerId}`,
        {
          status: 404,
          statusText: 'Not Found'
        }
      )
    }

    // Resolve model type from cookie (forced to speed for guests and cloud)
    const modelTypeCookie = cookieStore.get('modelType')?.value
    const resolvedModelType =
      modelTypeCookie === 'quality' || modelTypeCookie === 'speed'
        ? modelTypeCookie
        : undefined
    const modelType = forceSpeed ? 'speed' : resolvedModelType
    if (!isGuest) {
      const overallLimitResponse = await checkAndEnforceOverallChatLimit(userId)
      if (overallLimitResponse) return overallLimitResponse
    }

    const streamStart = performance.now()
    perfLog(
      `createChatStreamResponse - Start: model=${selectedModel.providerId}:${selectedModel.id}, searchMode=${searchMode}, modelType=${modelType}`
    )

    const response = isGuest
      ? await createEphemeralChatStreamResponse({
          messages: Array.isArray(messages) ? messages : [],
          model: selectedModel,
          abortSignal,
          searchMode,
          modelType,
          chatId
        })
      : await createChatStreamResponse({
          message,
          model: selectedModel,
          chatId,
          userId: userId, // userId is guaranteed to be non-null after authentication check above
          trigger,
          messageId,
          abortSignal,
          isNewChat,
          searchMode,
          modelType
        })

    perfTime('createChatStreamResponse resolved', streamStart)

    // Track analytics event (non-blocking)
    // Calculate conversation turn by loading chat history
    ;(async () => {
      try {
        let conversationTurn = 1 // Default for new chats

        // For existing chats, load history and calculate turn number
        if (!isNewChat && !isGuest) {
          const chat = await loadChat(chatId, userId)
          if (chat?.messages) {
            // Add 1 to account for the current message being sent
            conversationTurn = calculateConversationTurn(chat.messages) + 1
          }
        }

        if (!isGuest && userId) {
          await trackChatEvent({
            searchMode,
            modelType: modelTypeCookie === 'quality' ? 'quality' : 'speed',
            conversationTurn,
            isNewChat: isNewChat ?? false,
            trigger:
              (trigger as 'submit-message' | 'regenerate-message') ??
              'submit-message',
            chatId,
            userId,
            modelId: selectedModel.id
          })
        }
      } catch (error) {
        // Log error but don't throw - analytics should never break the app
        console.error('Analytics tracking failed:', error)
      }
    })()

    // Invalidate the cache for this specific chat after creating the response
    // This ensures the next load will get fresh data
    if (chatId && !isGuest) {
      revalidateTag(`chat-${chatId}`, 'max')
    }

    const totalTime = performance.now() - startTime
    perfLog(`Total API route time: ${totalTime.toFixed(2)}ms`)
    perfLog(`=== Summary ===`)
    perfLog(`Chat Type: ${isNewChat ? 'NEW' : 'EXISTING'}`)
    perfLog(`Total Time: ${totalTime.toFixed(2)}ms`)
    perfLog(`================`)

    return response
  } catch (error) {
    console.error('API route error:', error)
    return new Response('Error processing your request', {
      status: 500,
      statusText: 'Internal Server Error'
    })
  }
}


================================================
FILE: app/api/chats/route.ts
================================================
import { NextRequest, NextResponse } from 'next/server'

import { getChatsPage } from '@/lib/actions/chat'
import { Chat as DBChat } from '@/lib/db/schema'

interface ChatPageResponse {
  chats: DBChat[]
  nextOffset: number | null
}

export async function GET(request: NextRequest) {
  const { searchParams } = new URL(request.url)
  const offset = parseInt(searchParams.get('offset') || '0', 10)
  const limit = parseInt(searchParams.get('limit') || '20', 10)

  try {
    const result = await getChatsPage(limit, offset)
    return NextResponse.json<ChatPageResponse>(result)
  } catch (error) {
    console.error('API route error fetching chats:', error)
    return NextResponse.json<ChatPageResponse>(
      { chats: [], nextOffset: null },
      { status: 500 }
    )
  }
}


================================================
FILE: app/api/feedback/__tests__/route.test.ts
================================================
import { beforeEach, describe, expect, it, vi } from 'vitest'

// Mock Next.js cookies API
vi.mock('next/headers', () => ({
  cookies: vi.fn(() => ({
    get: vi.fn(),
    set: vi.fn(),
    delete: vi.fn(),
    getAll: vi.fn(() => [])
  }))
}))

// Mock Supabase
vi.mock('@/lib/supabase/server', () => ({
  createClient: vi.fn(() => ({
    auth: {
      getUser: vi.fn(() =>
        Promise.resolve({ data: { user: null }, error: null })
      )
    }
  }))
}))

// Mock the modules
vi.mock('@/lib/actions/feedback', () => ({
  updateMessageFeedback: vi.fn()
}))

vi.mock('@/lib/utils/telemetry', () => ({
  isTracingEnabled: vi.fn(() => false)
}))

vi.mock('langfuse', () => ({
  Langfuse: vi.fn(() => ({
    score: vi.fn(),
    flushAsync: vi.fn(() => Promise.resolve())
  }))
}))

// Import after mocking
import { Langfuse } from 'langfuse'

import { updateMessageFeedback } from '@/lib/actions/feedback'
import { isTracingEnabled } from '@/lib/utils/telemetry'

import { POST } from '../route'

describe('Feedback API Route', () => {
  beforeEach(() => {
    vi.clearAllMocks()
  })

  describe('POST /api/feedback', () => {
    it('should record feedback successfully', async () => {
      vi.mocked(isTracingEnabled).mockReturnValue(true)
      vi.mocked(updateMessageFeedback).mockResolvedValue({
        success: true
      })

      const mockScore = vi.fn()
      const mockFlush = vi.fn().mockResolvedValue(undefined)
      vi.mocked(Langfuse).mockImplementation(
        () =>
          ({
            score: mockScore,
            flushAsync: mockFlush
          }) as any
      )

      const request = new Request('http://localhost:3000/api/feedback', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json'
        },
        body: JSON.stringify({
          traceId: 'test-trace-id',
          score: 1,
          comment: 'Great!',
          messageId: 'test-message-id'
        })
      })

      const response = await POST(request)
      const text = await response.text()

      expect(response.status).toBe(200)
      expect(text).toBe('Feedback recorded successfully')
      expect(mockScore).toHaveBeenCalledWith({
        traceId: 'test-trace-id',
        name: 'user_feedback',
        value: 1,
        comment: 'Great!'
      })
      expect(mockFlush).toHaveBeenCalled()
      expect(updateMessageFeedback).toHaveBeenCalledWith(
        'test-message-id',
        1,
        null
      )
    })

    it('should handle negative feedback', async () => {
      vi.mocked(isTracingEnabled).mockReturnValue(true)
      vi.mocked(updateMessageFeedback).mockResolvedValue({
        success: true
      })

      const mockScore = vi.fn()
      const mockFlush = vi.fn().mockResolvedValue(undefined)
      vi.mocked(Langfuse).mockImplementation(
        () =>
          ({
            score: mockScore,
            flushAsync: mockFlush
          }) as any
      )

      const request = new Request('http://localhost:3000/api/feedback', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json'
        },
        body: JSON.stringify({
          traceId: 'test-trace-id',
          score: -1,
          messageId: 'test-message-id'
        })
      })

      const response = await POST(request)

      expect(response.status).toBe(200)
      expect(mockScore).toHaveBeenCalledWith({
        traceId: 'test-trace-id',
        name: 'user_feedback',
        value: -1,
        comment: undefined
      })
    })

    it('should return 400 for missing traceId', async () => {
      const request = new Request('http://localhost:3000/api/feedback', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json'
        },
        body: JSON.stringify({
          score: 1,
          messageId: 'test-message-id'
        })
      })

      const response = await POST(request)
      const text = await response.text()

      expect(response.status).toBe(400)
      expect(text).toBe('traceId is required')
    })

    it('should return 400 for invalid score', async () => {
      const request = new Request('http://localhost:3000/api/feedback', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json'
        },
        body: JSON.stringify({
          traceId: 'test-trace-id',
          score: 0,
          messageId: 'test-message-id'
        })
      })

      const response = await POST(request)
      const text = await response.text()

      expect(response.status).toBe(400)
      expect(text).toBe('score must be 1 (good) or -1 (bad)')
    })

    it('should return 200 when tracing is disabled', async () => {
      vi.mocked(isTracingEnabled).mockReturnValue(false)

      const request = new Request('http://localhost:3000/api/feedback', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json'
        },
        body: JSON.stringify({
          traceId: 'test-trace-id',
          score: 1,
          messageId: 'test-message-id'
        })
      })

      const response = await POST(request)
      const text = await response.text()

      expect(response.status).toBe(200)
      expect(text).toBe('Feedback tracking is not enabled')
      expect(updateMessageFeedback).not.toHaveBeenCalled()
    })

    it('should continue even if database update fails', async () => {
      vi.mocked(isTracingEnabled).mockReturnValue(true)
      vi.mocked(updateMessageFeedback).mockResolvedValue({
        success: false,
        error: 'Database error'
      })

      const mockScore = vi.fn()
      const mockFlush = vi.fn().mockResolvedValue(undefined)
      vi.mocked(Langfuse).mockImplementation(
        () =>
          ({
            score: mockScore,
            flushAsync: mockFlush
          }) as any
      )

      const consoleErrorSpy = vi
        .spyOn(console, 'error')
        .mockImplementation(() => {})

      const request = new Request('http://localhost:3000/api/feedback', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json'
        },
        body: JSON.stringify({
          traceId: 'test-trace-id',
          score: 1,
          messageId: 'test-message-id'
        })
      })

      const response = await POST(request)

      expect(response.status).toBe(200)
      expect(consoleErrorSpy).toHaveBeenCalledWith(
        'Error updating message feedback:',
        'Database error'
      )

      consoleErrorSpy.mockRestore()
    })

    it('should work without messageId', async () => {
      vi.mocked(isTracingEnabled).mockReturnValue(true)

      const mockScore = vi.fn()
      const mockFlush = vi.fn().mockResolvedValue(undefined)
      vi.mocked(Langfuse).mockImplementation(
        () =>
          ({
            score: mockScore,
            flushAsync: mockFlush
          }) as any
      )

      const request = new Request('http://localhost:3000/api/feedback', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json'
        },
        body: JSON.stringify({
          traceId: 'test-trace-id',
          score: 1
        })
      })

      const response = await POST(request)

      expect(response.status).toBe(200)
      expect(updateMessageFeedback).not.toHaveBeenCalled()
    })

    it('should handle JSON parsing errors', async () => {
      const consoleErrorSpy = vi
        .spyOn(console, 'error')
        .mockImplementation(() => {})

      const request = new Request('http://localhost:3000/api/feedback', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json'
        },
        body: 'invalid json'
      })

      const response = await POST(request)
      const text = await response.text()

      expect(response.status).toBe(500)
      expect(text).toBe('Error recording feedback')
      expect(consoleErrorSpy).toHaveBeenCalled()

      consoleErrorSpy.mockRestore()
    })
  })
})


================================================
FILE: app/api/feedback/route.ts
================================================
import { Langfuse } from 'langfuse'

import { updateMessageFeedback } from '@/lib/actions/feedback'
import { createClient } from '@/lib/supabase/server'
import { isTracingEnabled } from '@/lib/utils/telemetry'

export async function POST(req: Request) {
  try {
    const body = await req.json()
    const { traceId, score, comment, messageId } = body

    if (!traceId) {
      return new Response('traceId is required', {
        status: 400,
        statusText: 'Bad Request'
      })
    }

    if (score === undefined || (score !== 1 && score !== -1)) {
      return new Response('score must be 1 (good) or -1 (bad)', {
        status: 400,
        statusText: 'Bad Request'
      })
    }

    // Check if tracing is enabled
    if (!isTracingEnabled()) {
      return new Response('Feedback tracking is not enabled', {
        status: 200
      })
    }

    // Initialize Langfuse client
    const langfuse = new Langfuse()

    // Send score to Langfuse
    langfuse.score({
      traceId,
      name: 'user_feedback',
      value: score,
      comment
    })

    // Flush to ensure the score is sent
    await langfuse.flushAsync()

    // Get current user for RLS context
    let userId: string | null = null
    const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL
    const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY

    if (supabaseUrl && supabaseAnonKey) {
      const supabase = await createClient()
      const {
        data: { user }
      } = await supabase.auth.getUser()
      userId = user?.id || null
    }

    // Update the message metadata with the feedback score using the action
    if (messageId) {
      const result = await updateMessageFeedback(messageId, score, userId)

      if (!result.success) {
        console.error('Error updating message feedback:', result.error)
        // Continue even if database update fails
      }
    }

    return new Response('Feedback recorded successfully', {
      status: 200
    })
  } catch (error) {
    console.error('Error recording feedback:', error)
    return new Response('Error recording feedback', {
      status: 500,
      statusText: 'Internal Server Error'
    })
  }
}


================================================
FILE: app/api/upload/route.ts
================================================
import { NextRequest, NextResponse } from 'next/server'

import { PutObjectCommand } from '@aws-sdk/client-s3'

import { getCurrentUserId } from '@/lib/auth/get-current-user'
import {
  getR2Client,
  R2_BUCKET_NAME,
  R2_PUBLIC_URL
} from '@/lib/storage/r2-client'

const MAX_FILE_SIZE = 5 * 1024 * 1024 // 5MB
const ALLOWED_TYPES = ['image/jpeg', 'image/png', 'application/pdf']

export async function POST(req: NextRequest) {
  try {
    const userId = await getCurrentUserId()
    if (!userId) {
      return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
    }
    const contentType = req.headers.get('content-type') || ''
    if (!contentType.includes('multipart/form-data')) {
      return NextResponse.json(
        { error: 'Invalid content type' },
        { status: 400 }
      )
    }

    const formData = await req.formData()
    const file = formData.get('file') as File
    const chatId = formData.get('chatId') as string
    if (!file) {
      return NextResponse.json({ error: 'File is required' }, { status: 400 })
    }

    if (file.size > MAX_FILE_SIZE) {
      return NextResponse.json(
        { error: 'File too large (max 5MB)' },
        { status: 400 }
      )
    }

    if (!ALLOWED_TYPES.includes(file.type)) {
      return NextResponse.json(
        { error: 'Unsupported file type' },
        { status: 400 }
      )
    }
    const result = await uploadFileToR2(file, userId, chatId)
    return NextResponse.json({ success: true, file: result }, { status: 200 })
  } catch (err: any) {
    console.error('Upload Error:', err)
    return NextResponse.json(
      { error: 'Upload failed', message: err.message },
      { status: 500 }
    )
  }
}

function sanitizeFilename(filename: string) {
  return filename.replace(/[^a-z0-9.\-_]/gi, '_').toLowerCase()
}

async function uploadFileToR2(file: File, userId: string, chatId: string) {
  const sanitizedFileName = sanitizeFilename(file.name)
  const filePath = `${userId}/chats/${chatId}/${Date.now()}-${sanitizedFileName}`

  try {
    const buffer = Buffer.from(await file.arrayBuffer())
    const r2Client = getR2Client()

    await r2Client.send(
      new PutObjectCommand({
        Bucket: R2_BUCKET_NAME,
        Key: filePath,
        Body: buffer,
        ContentType: file.type,
        CacheControl: 'max-age=3600'
      })
    )

    const publicUrl = `${R2_PUBLIC_URL}/${filePath}`

    return {
      filename: file.name,
      url: publicUrl,
      mediaType: file.type,
      type: 'file'
    }
  } catch (error: any) {
    throw new Error('Upload failed: ' + error.message)
  }
}


================================================
FILE: app/auth/confirm/route.ts
================================================
import { redirect } from 'next/navigation'
import { type NextRequest } from 'next/server'

import { type EmailOtpType } from '@supabase/supabase-js'

import { createClient } from '@/lib/supabase/server'

export async function GET(request: NextRequest) {
  const { searchParams } = new URL(request.url)
  const token_hash = searchParams.get('token_hash')
  const type = searchParams.get('type') as EmailOtpType | null
  const next = searchParams.get('next') ?? '/'

  if (token_hash && type) {
    const supabase = await createClient()

    const { error } = await supabase.auth.verifyOtp({
      type,
      token_hash
    })
    if (!error) {
      // redirect user to specified redirect URL or root of app
      redirect(next)
    } else {
      // redirect the user to an error page with some instructions
      redirect(`/auth/error?error=${error?.message}`)
    }
  }

  // redirect the user to an error page with some instructions
  redirect(`/auth/error?error=No token hash or type`)
}


================================================
FILE: app/auth/error/page.tsx
================================================
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'

export default async function Page({
  searchParams
}: {
  searchParams: Promise<{ error: string }>
}) {
  const params = await searchParams

  return (
    <div className="flex min-h-svh w-full items-center justify-center p-6 md:p-10">
      <div className="w-full max-w-sm">
        <div className="flex flex-col gap-6">
          <Card>
            <CardHeader>
              <CardTitle className="text-2xl">
                Sorry, something went wrong.
              </CardTitle>
            </CardHeader>
            <CardContent>
              {params?.error ? (
                <p className="text-sm text-muted-foreground">
                  Code error: {params.error}
                </p>
              ) : (
                <p className="text-sm text-muted-foreground">
                  An unspecified error occurred.
                </p>
              )}
            </CardContent>
          </Card>
        </div>
      </div>
    </div>
  )
}


================================================
FILE: app/auth/forgot-password/page.tsx
================================================
import { ForgotPasswordForm } from '@/components/forgot-password-form'

export default function Page() {
  return (
    <div className="flex min-h-svh w-full items-center justify-center p-6 md:p-10">
      <div className="w-full max-w-sm">
        <ForgotPasswordForm />
      </div>
    </div>
  )
}


================================================
FILE: app/auth/login/page.tsx
================================================
import { LoginForm } from '@/components/login-form'

export default function Page() {
  return (
    <div className="flex min-h-svh w-full items-center justify-center p-6 md:p-10">
      <div className="w-full max-w-sm">
        <LoginForm />
      </div>
    </div>
  )
}


================================================
FILE: app/auth/oauth/route.ts
================================================
import { NextResponse } from 'next/server'

// The client you created from the Server-Side Auth instructions
import { createClient } from '@/lib/supabase/server'

export async function GET(request: Request) {
  const { searchParams, origin } = new URL(request.url)
  const code = searchParams.get('code')
  // if "next" is in param, use it as the redirect URL
  const next = searchParams.get('next') ?? '/'

  if (code) {
    const supabase = await createClient()
    const { error } = await supabase.auth.exchangeCodeForSession(code)
    if (!error) {
      const forwardedHost = request.headers.get('x-forwarded-host') // original origin before load balancer
      const isLocalEnv = process.env.NODE_ENV === 'development'
      if (isLocalEnv) {
        // we can be sure that there is no load balancer in between, so no need to watch for X-Forwarded-Host
        return NextResponse.redirect(`${origin}${next}`)
      } else if (forwardedHost) {
        return NextResponse.redirect(`https://${forwardedHost}${next}`)
      } else {
        return NextResponse.redirect(`${origin}${next}`)
      }
    }
  }

  // return the user to an error page with instructions
  return NextResponse.redirect(`${origin}/auth/error`)
}


================================================
FILE: app/auth/sign-up/page.tsx
================================================
import { SignUpForm } from '@/components/sign-up-form'

export default function Page() {
  return (
    <div className="flex min-h-svh w-full items-center justify-center p-6 md:p-10">
      <div className="w-full max-w-sm">
        <SignUpForm />
      </div>
    </div>
  )
}


================================================
FILE: app/auth/sign-up-success/page.tsx
================================================
import {
  Card,
  CardContent,
  CardDescription,
  CardHeader,
  CardTitle
} from '@/components/ui/card'

export default function Page() {
  return (
    <div className="flex min-h-svh w-full items-center justify-center p-6 md:p-10">
      <div className="w-full max-w-sm">
        <div className="flex flex-col gap-6">
          <Card>
            <CardHeader>
              <CardTitle className="text-2xl">
                Thank you for signing up!
              </CardTitle>
              <CardDescription>Check your email to confirm</CardDescription>
            </CardHeader>
            <CardContent>
              <p className="text-sm text-muted-foreground">
                You&apos;ve successfully signed up. Please check your email to
                confirm your account before signing in.
              </p>
            </CardContent>
          </Card>
        </div>
      </div>
    </div>
  )
}


================================================
FILE: app/auth/update-password/page.tsx
================================================
import { UpdatePasswordForm } from '@/components/update-password-form'

export default function Page() {
  return (
    <div className="flex min-h-svh w-full items-center justify-center p-6 md:p-10">
      <div className="w-full max-w-sm">
        <UpdatePasswordForm />
      </div>
    </div>
  )
}


================================================
FILE: app/globals.css
================================================
@import 'tailwindcss';
@source "../node_modules/streamdown/dist/index.js";

@custom-variant dark (&:is(.dark *));

:root {
  --background: oklch(0.99 0 0);
  --foreground: oklch(0 0 0);
  --card: oklch(1 0 0);
  --card-foreground: oklch(0 0 0);
  --popover: oklch(0.99 0 0);
  --popover-foreground: oklch(0 0 0);
  --primary: oklch(0 0 0);
  --primary-foreground: oklch(1 0 0);
  --secondary: oklch(0.94 0 0);
  --secondary-foreground: oklch(0 0 0);
  --muted: oklch(0.97 0 0);
  --muted-foreground: oklch(0.44 0 0);
  --accent: oklch(0.94 0 0);
  --accent-foreground: oklch(0 0 0);
  --destructive: oklch(0.63 0.19 23.03);
  --destructive-foreground: oklch(1 0 0);
  --border: oklch(0.92 0 0);
  --input: oklch(0.94 0 0);
  --ring: oklch(0 0 0);
  --chart-1: oklch(0.81 0.17 75.35);
  --chart-2: oklch(0.55 0.22 264.53);
  --chart-3: oklch(0.72 0 0);
  --chart-4: oklch(0.92 0 0);
  --chart-5: oklch(0.56 0 0);
  --sidebar: oklch(0.97 0 0);
  --sidebar-foreground: oklch(0 0 0);
  --sidebar-primary: oklch(0 0 0);
  --sidebar-primary-foreground: oklch(1 0 0);
  --sidebar-accent: oklch(0.94 0 0);
  --sidebar-accent-foreground: oklch(0 0 0);
  --sidebar-border: oklch(0.94 0 0);
  --sidebar-ring: oklch(0 0 0);
  --font-sans: Geist, sans-serif;
  --font-serif: Georgia, serif;
  --font-mono: Geist Mono, monospace;
  --radius: 0.5rem;
  --shadow-2xs: 0px 1px 2px 0px hsl(0 0% 0% / 0.09);
  --shadow-xs: 0px 1px 2px 0px hsl(0 0% 0% / 0.09);
  --shadow-sm:
    0px 1px 2px 0px hsl(0 0% 0% / 0.18), 0px 1px 2px -1px hsl(0 0% 0% / 0.18);
  --shadow:
    0px 1px 2px 0px hsl(0 0% 0% / 0.18), 0px 1px 2px -1px hsl(0 0% 0% / 0.18);
  --shadow-md:
    0px 1px 2px 0px hsl(0 0% 0% / 0.18), 0px 2px 4px -1px hsl(0 0% 0% / 0.18);
  --shadow-lg:
    0px 1px 2px 0px hsl(0 0% 0% / 0.18), 0px 4px 6px -1px hsl(0 0% 0% / 0.18);
  --shadow-xl:
    0px 1px 2px 0px hsl(0 0% 0% / 0.18), 0px 8px 10px -1px hsl(0 0% 0% / 0.18);
  --shadow-2xl: 0px 1px 2px 0px hsl(0 0% 0% / 0.45);
  --tracking-normal: 0em;
  --spacing: 0.25rem;
}

.dark {
  --background: oklch(0 0 0);
  --foreground: oklch(1 0 0);
  --card: oklch(0.14 0 0);
  --card-foreground: oklch(1 0 0);
  --popover: oklch(0.18 0 0);
  --popover-foreground: oklch(1 0 0);
  --primary: oklch(1 0 0);
  --primary-foreground: oklch(0 0 0);
  --secondary: oklch(0.25 0 0);
  --secondary-foreground: oklch(1 0 0);
  --muted: oklch(0.23 0 0);
  --muted-foreground: oklch(0.72 0 0);
  --accent: oklch(0.32 0 0);
  --accent-foreground: oklch(1 0 0);
  --destructive: oklch(0.69 0.2 23.91);
  --destructive-foreground: oklch(0 0 0);
  --border: oklch(0.26 0 0);
  --input: oklch(0.32 0 0);
  --ring: oklch(0.72 0 0);
  --chart-1: oklch(0.81 0.17 75.35);
  --chart-2: oklch(0.58 0.21 260.84);
  --chart-3: oklch(0.56 0 0);
  --chart-4: oklch(0.44 0 0);
  --chart-5: oklch(0.92 0 0);
  --sidebar: oklch(0.23 0 0);
  --sidebar-foreground: oklch(1 0 0);
  --sidebar-primary: oklch(1 0 0);
  --sidebar-primary-foreground: oklch(0 0 0);
  --sidebar-accent: oklch(0.32 0 0);
  --sidebar-accent-foreground: oklch(1 0 0);
  --sidebar-border: oklch(0.32 0 0);
  --sidebar-ring: oklch(0.72 0 0);
  --font-sans: Geist, sans-serif;
  --font-serif: Georgia, serif;
  --font-mono: Geist Mono, monospace;
  --radius: 0.5rem;
  --shadow-2xs: 0px 1px 2px 0px hsl(0 0% 0% / 0.09);
  --shadow-xs: 0px 1px 2px 0px hsl(0 0% 0% / 0.09);
  --shadow-sm:
    0px 1px 2px 0px hsl(0 0% 0% / 0.18), 0px 1px 2px -1px hsl(0 0% 0% / 0.18);
  --shadow:
    0px 1px 2px 0px hsl(0 0% 0% / 0.18), 0px 1px 2px -1px hsl(0 0% 0% / 0.18);
  --shadow-md:
    0px 1px 2px 0px hsl(0 0% 0% / 0.18), 0px 2px 4px -1px hsl(0 0% 0% / 0.18);
  --shadow-lg:
    0px 1px 2px 0px hsl(0 0% 0% / 0.18), 0px 4px 6px -1px hsl(0 0% 0% / 0.18);
  --shadow-xl:
    0px 1px 2px 0px hsl(0 0% 0% / 0.18), 0px 8px 10px -1px hsl(0 0% 0% / 0.18);
  --shadow-2xl: 0px 1px 2px 0px hsl(0 0% 0% / 0.45);
}

@theme inline {
  --color-background: var(--background);
  --color-foreground: var(--foreground);
  --color-card: var(--card);
  --color-card-foreground: var(--card-foreground);
  --color-popover: var(--popover);
  --color-popover-foreground: var(--popover-foreground);
  --color-primary: var(--primary);
  --color-primary-foreground: var(--primary-foreground);
  --color-secondary: var(--secondary);
  --color-secondary-foreground: var(--secondary-foreground);
  --color-muted: var(--muted);
  --color-muted-foreground: var(--muted-foreground);
  --color-accent: var(--accent);
  --color-accent-foreground: var(--accent-foreground);
  --color-destructive: var(--destructive);
  --color-destructive-foreground: var(--destructive-foreground);
  --color-border: var(--border);
  --color-input: var(--input);
  --color-ring: var(--ring);
  --color-chart-1: var(--chart-1);
  --color-chart-2: var(--chart-2);
  --color-chart-3: var(--chart-3);
  --color-chart-4: var(--chart-4);
  --color-chart-5: var(--chart-5);
  --color-sidebar: var(--sidebar);
  --color-sidebar-foreground: var(--sidebar-foreground);
  --color-sidebar-primary: var(--sidebar-primary);
  --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
  --color-sidebar-accent: var(--sidebar-accent);
  --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
  --color-sidebar-border: var(--sidebar-border);
  --color-sidebar-ring: var(--sidebar-ring);

  --font-sans: var(--font-sans);
  --font-mono: var(--font-mono);
  --font-serif: var(--font-serif);

  --radius-sm: calc(var(--radius) - 4px);
  --radius-md: calc(var(--radius) - 2px);
  --radius-lg: var(--radius);
  --radius-xl: calc(var(--radius) + 4px);

  --shadow-2xs: var(--shadow-2xs);
  --shadow-xs: var(--shadow-xs);
  --shadow-sm: var(--shadow-sm);
  --shadow: var(--shadow);
  --shadow-md: var(--shadow-md);
  --shadow-lg: var(--shadow-lg);
  --shadow-xl: var(--shadow-xl);
  --shadow-2xl: var(--shadow-2xl);

  --animate-accordion-down: accordion-down 0.2s ease-out;
  --animate-accordion-up: accordion-up 0.2s ease-out;
  --animate-collapse-down: collapse-down 0.1s ease-in-out;
  --animate-collapse-up: collapse-up 0.1s ease-in-out;
  --animate-slide-in-right: slide-in-right 0.3s ease-out;
  --animate-slide-out-right: slide-out-right 0.3s ease-out;
  --animate-fade-in: fade-in 0.3s ease-out;

  @keyframes accordion-down {
    from {
      height: 0;
    }
    to {
      height: var(--radix-accordion-content-height);
    }
  }
  @keyframes accordion-up {
    from {
      height: var(--radix-accordion-content-height);
    }
    to {
      height: 0;
    }
  }
  @keyframes collapse-down {
    from {
      height: 0;
      opacity: 0;
    }
    to {
      height: var(--radix-collapsible-content-height);
      opacity: 1;
    }
  }
  @keyframes collapse-up {
    from {
      height: var(--radix-collapsible-content-height);
      opacity: 1;
    }
    to {
      height: 0;
      opacity: 0;
    }
  }
  @keyframes slide-in-right {
    from {
      transform: translateX(100%);
      opacity: 0;
    }
    to {
      transform: translateX(0);
      opacity: 1;
    }
  }
  @keyframes slide-out-right {
    from {
      transform: translateX(0);
      opacity: 1;
    }
    to {
      transform: translateX(100%);
      opacity: 0;
    }
  }
  @keyframes fade-in {
    from {
      opacity: 0;
    }
    to {
      opacity: 1;
    }
  }
  @keyframes blink {
    0%,
    100% {
      ry: 18;
    }
    50% {
      ry: 0;
    }
  }
  @keyframes lookAround {
    0%,
    100% {
      transform: translateX(0);
    }
    25% {
      transform: translateX(10px);
    }
    50% {
      transform: translateX(0);
    }
    75% {
      transform: translateX(-10px);
    }
  }
}

@utility animate-blink {
  animation: blink 0.2s ease-in-out;
}

/* Animation utilities for dropdown menu */
@utility animate-in {
  animation-name: enter;
  animation-duration: 150ms;
  animation-fill-mode: both;
}

@utility animate-out {
  animation-name: exit;
  animation-duration: 150ms;
  animation-fill-mode: both;
}

@utility fade-in-0 {
  --enter-opacity: 0;
}

@utility fade-out-0 {
  --exit-opacity: 0;
}

@utility zoom-in-95 {
  --enter-scale: 0.95;
}

@utility zoom-out-95 {
  --exit-scale: 0.95;
}

@utility slide-in-from-top-2 {
  --enter-translate-y: -0.5rem;
}

@utility slide-in-from-bottom-2 {
  --enter-translate-y: 0.5rem;
}

@utility slide-in-from-left-2 {
  --enter-translate-x: -0.5rem;
}

@utility slide-in-from-right-2 {
  --enter-translate-x: 0.5rem;
}

@keyframes enter {
  from {
    opacity: var(--enter-opacity, 1);
    transform: translate(
        var(--enter-translate-x, 0),
        var(--enter-translate-y, 0)
      )
      scale(var(--enter-scale, 1));
  }
  to {
    opacity: 1;
    transform: translate(0, 0) scale(1);
  }
}

@keyframes exit {
  from {
    opacity: 1;
    transform: translate(0, 0) scale(1);
  }
  to {
    opacity: var(--exit-opacity, 1);
    transform: translate(var(--exit-translate-x, 0), var(--exit-translate-y, 0))
      scale(var(--exit-scale, 1));
  }
}

@utility container {
  margin-inline: auto;
  padding-inline: 2rem;
  @media (width >= --theme(--breakpoint-sm)) {
    max-width: none;
  }
  @media (width >= 1400px) {
    max-width: 1400px;
  }
}

@utility prose {
  /* Heading styles for prose */
  & h1 {
    @apply text-3xl font-bold tracking-tight mt-10 mb-6;
  }

  & h2 {
    @apply text-2xl font-semibold tracking-tight mt-8 mb-4;
  }

  & h3 {
    @apply text-xl font-semibold tracking-tight mt-6 mb-3;
  }

  & h4 {
    @apply text-lg font-medium tracking-tight mt-5 mb-2;
  }

  & h5 {
    @apply text-base font-medium mt-4 mb-2;
  }

  & h6 {
    @apply text-sm font-medium mt-4 mb-2;
  }
}

@utility prose-sm {
  & h1 {
    @apply text-2xl font-bold tracking-tight mt-8 mb-4;
  }

  & h2 {
    @apply text-xl font-semibold tracking-tight mt-6 mb-3;
  }

  & h3 {
    @apply text-lg font-semibold tracking-tight mt-5 mb-2;
  }

  & h4 {
    @apply text-base font-medium tracking-tight mt-4 mb-2;
  }

  & h5 {
    @apply text-sm font-medium mt-3 mb-1;
  }

  & h6 {
    @apply text-xs font-medium mt-3 mb-1;
  }
}

@utility prose-neutral {
  /* Neutral theme styling for all headings */
  & h1 {
    @apply text-foreground border-b-2 border-border/50 pb-3;
  }

  & h2 {
    @apply text-foreground/90 border-b border-border/40 pb-2;
  }

  & h3 {
    @apply text-foreground/85;
  }

  & h4 {
    @apply text-foreground/80;
  }

  & h5 {
    @apply text-foreground/75;
  }

  & h6 {
    @apply text-foreground/70;
  }
}

@layer base {
  * {
    @apply border-border;
  }
  body {
    @apply bg-background text-foreground;
  }

  /* Tailwind v4: Restore pointer cursor for buttons */
  button:not(:disabled),
  [role='button']:not(:disabled) {
    cursor: pointer;
  }
}


================================================
FILE: app/layout.tsx
================================================
import type { Metadata, Viewport } from 'next'
import { Inter as FontSans } from 'next/font/google'

import { Analytics } from '@vercel/analytics/next'

import { UserProvider } from '@/lib/contexts/user-context'
import { createClient } from '@/lib/supabase/server'
import { cn } from '@/lib/utils'

import { SidebarProvider } from '@/components/ui/sidebar'
import { Toaster } from '@/components/ui/sonner'

import AppSidebar from '@/components/app-sidebar'
import ArtifactRoot from '@/components/artifact/artifact-root'
import Header from '@/components/header'
import { ThemeProvider } from '@/components/theme-provider'

import './globals.css'

const fontSans = FontSans({
  subsets: ['latin'],
  variable: '--font-sans'
})

const title = 'Morphic'
const description =
  'A fully open-source AI-powered answer engine with a generative UI.'

export const metadata: Metadata = {
  metadataBase: new URL('https://morphic.sh'),
  title,
  description,
  openGraph: {
    title,
    description
  },
  twitter: {
    title,
    description,
    card: 'summary_large_image',
    creator: '@miiura'
  }
}

export const viewport: Viewport = {
  width: 'device-width',
  initialScale: 1,
  minimumScale: 1,
  maximumScale: 1
}

export default async function RootLayout({
  children
}: Readonly<{
  children: React.ReactNode
}>) {
  let user = null
  const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL
  const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY

  if (supabaseUrl && supabaseAnonKey) {
    const supabase = await createClient()
    const {
      data: { user: supabaseUser }
    } = await supabase.auth.getUser()
    user = supabaseUser
  }

  return (
    <html lang="en" suppressHydrationWarning>
      <body
        className={cn(
          'min-h-screen flex flex-col font-sans antialiased overflow-hidden',
          fontSans.variable
        )}
      >
        <ThemeProvider
          attribute="class"
          defaultTheme="system"
          enableSystem
          disableTransitionOnChange
        >
          <UserProvider hasUser={!!user}>
            <SidebarProvider defaultOpen={false}>
              {user && <AppSidebar />}
              <div className="flex flex-col flex-1 min-w-0">
                <Header user={user} />
                <main className="flex flex-1 min-h-0 min-w-0 overflow-hidden">
                  <ArtifactRoot>{children}</ArtifactRoot>
                </main>
              </div>
            </SidebarProvider>
          </UserProvider>
          <Toaster />
          <Analytics />
        </ThemeProvider>
      </body>
    </html>
  )
}


================================================
FILE: app/page.tsx
================================================
import { getCurrentUserId } from '@/lib/auth/get-current-user'

import { Chat } from '@/components/chat'

export default async function Page() {
  const userId = await getCurrentUserId()
  return <Chat isGuest={!userId} />
}


================================================
FILE: app/search/[id]/page.tsx
================================================
import { notFound, redirect } from 'next/navigation'

import { UIMessage } from 'ai'

import { loadChat } from '@/lib/actions/chat'
import { getCurrentUserId } from '@/lib/auth/get-current-user'

import { Chat } from '@/components/chat'

export const maxDuration = 60

export async function generateMetadata(props: {
  params: Promise<{ id: string }>
}) {
  const { id } = await props.params
  const userId = await getCurrentUserId()

  const chat = await loadChat(id, userId)

  if (!chat) {
    return { title: 'Search' }
  }

  return {
    title: chat.title.toString().slice(0, 50) || 'Search'
  }
}

export default async function SearchPage(props: {
  params: Promise<{ id: string }>
}) {
  const { id } = await props.params
  const userId = await getCurrentUserId()

  const chat = await loadChat(id, userId)

  if (!chat) {
    notFound()
  }

  if (chat.visibility === 'private' && !userId) {
    redirect('/auth/login')
  }

  const messages: UIMessage[] = chat.messages

  return <Chat id={id} savedMessages={messages} isGuest={!userId} />
}


================================================
FILE: app/search/loading.tsx
================================================
'use client'

import { DefaultSkeleton } from '../../components/default-skeleton'

export default function Loading() {
  return (
    <div className="flex min-w-0 flex-1 flex-col items-center">
      <div className="w-full max-w-3xl px-4 pt-12">
        <DefaultSkeleton />
      </div>
    </div>
  )
}


================================================
FILE: app/search/page.tsx
================================================
import { redirect } from 'next/navigation'

import { getCurrentUserId } from '@/lib/auth/get-current-user'
import { generateUUID } from '@/lib/utils'

import { Chat } from '@/components/chat'

export const maxDuration = 60

export default async function SearchPage(props: {
  searchParams: Promise<{ q: string }>
}) {
  const { q } = await props.searchParams
  if (!q) {
    redirect('/')
  }

  const id = generateUUID()
  const userId = await getCurrentUserId()
  return <Chat id={id} query={q} isGuest={!userId} />
}


================================================
FILE: components/__tests__/research-process-section.test.tsx
================================================
import React from 'react'

import type { ReasoningPart } from '@ai-sdk/provider-utils'
import { fireEvent, render, screen } from '@testing-library/react'
import { beforeEach, describe, expect, Mock, test, vi } from 'vitest'

import type { ToolPart, UIMessage } from '@/lib/types/ai'

import { ResearchProcessSection } from '../research-process-section'

// Mock the child components
vi.mock('../reasoning-section', () => ({
  ReasoningSection: ({ content, isOpen, onOpenChange }: any) => (
    <div data-testid="reasoning-section">
      <button onClick={() => onOpenChange(!isOpen)}>
        {isOpen ? 'Close' : 'Open'} Reasoning
      </button>
      {isOpen && <div>{content.reasoning}</div>}
    </div>
  )
}))

vi.mock('../tool-section', () => ({
  ToolSection: ({ tool, isOpen, onOpenChange }: any) => (
    <div data-testid="tool-section">
      <button onClick={() => onOpenChange(!isOpen)}>
        {isOpen ? 'Close' : 'Open'} Tool
      </button>
      {isOpen && <div>{tool.type}</div>}
    </div>
  )
}))

describe('ResearchProcessSection', () => {
  const mockGetIsOpen = vi.fn()
  const mockOnOpenChange = vi.fn()
  const mockOnQuerySelect = vi.fn()
  const mockAddToolResult = vi.fn()

  beforeEach(() => {
    vi.clearAllMocks()
    mockGetIsOpen.mockReturnValue(false)
  })

  describe('Type Guards', () => {
    test('correctly identifies reasoning parts', () => {
      const reasoningPart: ReasoningPart = {
        type: 'reasoning',
        text: 'Test reasoning'
      }

      const message = {
        id: 'test-message',
        role: 'assistant' as const,
        parts: [reasoningPart]
      } as unknown as UIMessage

      render(
        <ResearchProcessSection
          message={message}
          messageId="test-1"
          getIsOpen={mockGetIsOpen}
          onOpenChange={mockOnOpenChange}
          onQuerySelect={mockOnQuerySelect}
        />
      )

      expect(screen.getByTestId('reasoning-section')).toBeInTheDocument()
    })

    test('correctly identifies tool parts', () => {
      const toolPart: ToolPart = {
        type: 'tool-search',
        toolCallId: 'tool-1',
        input: {},
        state: 'output-available'
      }

      const message = {
        id: 'test-message',
        role: 'assistant' as const,
        parts: [toolPart as any]
      } as UIMessage

      render(
        <ResearchProcessSection
          message={message}
          messageId="test-2"
          getIsOpen={mockGetIsOpen}
          onOpenChange={mockOnOpenChange}
          onQuerySelect={mockOnQuerySelect}
        />
      )

      expect(screen.getByTestId('tool-section')).toBeInTheDocument()
    })

    test('filters out empty reasoning parts', () => {
      const emptyReasoningPart: ReasoningPart = {
        type: 'reasoning',
        text: ''
      }

      const validReasoningPart: ReasoningPart = {
        type: 'reasoning',
        text: 'Valid reasoning'
      }

      const message = {
        id: 'test-message',
        role: 'assistant' as const,
        parts: [emptyReasoningPart, validReasoningPart]
      }

      render(
        <ResearchProcessSection
          message={message}
          messageId="test-3"
          getIsOpen={mockGetIsOpen}
          onOpenChange={mockOnOpenChange}
          onQuerySelect={mockOnQuerySelect}
        />
      )

      // Should only render one reasoning section (the valid one)
      const reasoningSections = screen.getAllByTestId('reasoning-section')
      expect(reasoningSections).toHaveLength(1)
    })
  })

  describe('Segmentation Logic', () => {
    test('splits parts by text correctly', () => {
      const parts: any[] = [
        { type: 'reasoning', text: 'First reasoning' } as ReasoningPart,
        {
          type: 'tool-search',
          toolCallId: 'tool-1',
          input: {},
          state: 'output-available'
        } as ToolPart,
        { type: 'text', text: 'Text separator' },
        { type: 'reasoning', text: 'Second reasoning' } as ReasoningPart
      ]

      const message: UIMessage = {
        id: 'test-message',
        role: 'assistant',
        parts
      }

      render(
        <ResearchProcessSection
          message={message}
          messageId="test-4"
          getIsOpen={mockGetIsOpen}
          onOpenChange={mockOnOpenChange}
          onQuerySelect={mockOnQuerySelect}
        />
      )

      // Should render 3 sections (2 reasoning + 1 tool, split by text)
      const allSections = [
        ...screen.getAllByTestId('reasoning-section'),
        ...screen.getAllByTestId('tool-section')
      ]
      expect(allSections).toHaveLength(3)
    })

    test('groups consecutive tool parts of same type', () => {
      const parts: any[] = [
        {
          type: 'tool-search',
          toolCallId: 'tool-1',
          input: {},
          state: 'output-available'
        } as ToolPart,
        {
          type: 'tool-search',
          toolCallId: 'tool-2',
          input: {},
          state: 'output-available'
        } as ToolPart,
        {
          type: 'tool-fetch',
          toolCallId: 'tool-3',
          input: {},
          state: 'output-available'
        } as ToolPart
      ]

      const message: UIMessage = {
        id: 'test-message',
        role: 'assistant',
        parts
      }

      render(
        <ResearchProcessSection
          message={message}
          messageId="test-5"
          getIsOpen={mockGetIsOpen}
          onOpenChange={mockOnOpenChange}
          onQuerySelect={mockOnQuerySelect}
        />
      )

      const toolSections = screen.getAllByTestId('tool-section')
      expect(toolSections).toHaveLength(3)
    })
  })

  describe('Accordion Behavior', () => {
    test('handles accordion state for grouped sections', () => {
      const parts: any[] = [
        { type: 'reasoning', text: 'First' } as ReasoningPart,
        { type: 'reasoning', text: 'Second' } as ReasoningPart
      ]

      const message: UIMessage = {
        id: 'test-message',
        role: 'assistant',
        parts
      }

      const { rerender } = render(
        <ResearchProcessSection
          message={message}
          messageId="test-6"
          getIsOpen={mockGetIsOpen}
          onOpenChange={mockOnOpenChange}
          onQuerySelect={mockOnQuerySelect}
        />
      )

      const buttons = screen.getAllByRole('button')

      // Click first button to open
      fireEvent.click(buttons[0])

      // Should call onOpenChange
      expect(mockOnOpenChange).toHaveBeenCalled()

      // Update mock to return true for the clicked item
      mockGetIsOpen.mockImplementation(id => id.includes('reasoning-0-0-0'))

      rerender(
        <ResearchProcessSection
          message={message}
          messageId="test-6"
          getIsOpen={mockGetIsOpen}
          onOpenChange={mockOnOpenChange}
          onQuerySelect={mockOnQuerySelect}
        />
      )
    })

    test('handles single sections differently from grouped sections', () => {
      const singlePart = [
        { type: 'reasoning', text: 'Single reasoning' } as ReasoningPart
      ]

      const message = {
        id: 'test-message',
        role: 'assistant' as const,
        parts: singlePart
      }

      render(
        <ResearchProcessSection
          message={message}
          messageId="test-7"
          getIsOpen={mockGetIsOpen}
          onOpenChange={mockOnOpenChange}
          onQuerySelect={mockOnQuerySelect}
        />
      )

      const button = screen.getByRole('button')
      fireEvent.click(button)

      // For single sections, should directly call onOpenChange
      expect(mockOnOpenChange).toHaveBeenCalledWith(
        expect.stringContaining('reasoning'),
        true
      )
    })
  })

  describe('Subsequent Content Detection', () => {
    test('detects subsequent content correctly', () => {
      const parts: any[] = [
        { type: 'reasoning', text: 'First' } as ReasoningPart,
        { type: 'text', text: 'Text' },
        { type: 'reasoning', text: 'Second' } as ReasoningPart
      ]

      const message: UIMessage = {
        id: 'test-message',
        role: 'assistant',
        parts
      }

      render(
        <ResearchProcessSection
          message={message}
          messageId="test-8"
          getIsOpen={mockGetIsOpen}
          onOpenChange={mockOnOpenChange}
          onQuerySelect={mockOnQuerySelect}
        />
      )

      // The first reasoning should detect subsequent content (the text part)
      expect(mockGetIsOpen).toHaveBeenCalledWith(
        expect.stringContaining('reasoning'),
        'reasoning',
        true // hasSubsequentContent should be true
      )
    })
  })

  describe('Edge Cases', () => {
    test('returns null for empty segments', () => {
      const message = {
        id: 'test-message',
        role: 'assistant' as const,
        parts: []
      }

      const { container } = render(
        <ResearchProcessSection
          message={message}
          messageId="test-9"
          getIsOpen={mockGetIsOpen}
          onOpenChange={mockOnOpenChange}
          onQuerySelect={mockOnQuerySelect}
        />
      )

      expect(container.firstChild).toBeNull()
    })

    test('handles parts override correctly', () => {
      const message = {
        id: 'test-message',
        role: 'assistant' as const,
        parts: [{ type: 'reasoning', text: 'Original' } as ReasoningPart]
      }

      const overrideParts = [
        { type: 'reasoning', text: 'Override' } as ReasoningPart
      ]

      // Mock getIsOpen to return true so content is visible
      mockGetIsOpen.mockReturnValue(true)

      render(
        <ResearchProcessSection
          message={message}
          messageId="test-10"
          getIsOpen={mockGetIsOpen}
          onOpenChange={mockOnOpenChange}
          onQuerySelect={mockOnQuerySelect}
          parts={overrideParts}
        />
      )

      // Should use override parts
      expect(screen.getByTestId('reasoning-section')).toBeInTheDocument()
      // The content should show "Override" when open
      expect(screen.getByText('Override')).toBeInTheDocument()
    })

    test('handles data parts correctly', () => {
      const parts: any[] = [{ type: 'data-test', data: 'test' }]

      const message: UIMessage = {
        id: 'test-message',
        role: 'assistant',
        parts
      }

      const { container } = render(
        <ResearchProcessSection
          message={message}
          messageId="test-11"
          getIsOpen={mockGetIsOpen}
          onOpenChange={mockOnOpenChange}
          onQuerySelect={mockOnQuerySelect}
        />
      )

      // Data parts should render the DataSection component
      // Check that the component renders (not null)
      expect(container.firstChild).toBeInTheDocument()
    })
  })

  describe('Props Handling', () => {
    test('passes status prop correctly', () => {
      const toolPart: ToolPart = {
        type: 'tool-search',
        toolCallId: 'tool-1',
        input: {},
        state: 'output-available'
      }

      const message = {
        id: 'test-message',
        role: 'assistant' as const,
        parts: [toolPart as any]
      } as UIMessage

      render(
        <ResearchProcessSection
          message={message}
          messageId="test-12"
          getIsOpen={mockGetIsOpen}
          onOpenChange={mockOnOpenChange}
          onQuerySelect={mockOnQuerySelect}
          status="streaming"
        />
      )

      expect(screen.getByTestId('tool-section')).toBeInTheDocument()
    })

    test('passes addToolResult prop correctly', () => {
      const toolPart: ToolPart = {
        type: 'tool-search',
        toolCallId: 'tool-1',
        input: {},
        state: 'output-available'
      }

      const message = {
        id: 'test-message',
        role: 'assistant' as const,
        parts: [toolPart as any]
      } as UIMessage

      render(
        <ResearchProcessSection
          message={message}
          messageId="test-13"
          getIsOpen={mockGetIsOpen}
          onOpenChange={mockOnOpenChange}
          onQuerySelect={mockOnQuerySelect}
          addToolResult={mockAddToolResult}
        />
      )

      expect(screen.getByTestId('tool-section')).toBeInTheDocument()
    })
  })
})


================================================
FILE: components/action-buttons.tsx
================================================
'use client'

import { useEffect, useRef, useState } from 'react'

import {
  FileText,
  HelpCircle,
  LucideIcon,
  Newspaper,
  Scale,
  Search
} from 'lucide-react'

import { cn } from '@/lib/utils'

import { Button } from './ui/button'

// Constants for timing delays
const FOCUS_OUT_DELAY_MS = 100 // Delay to ensure focus has actually moved

interface ActionCategory {
  icon: LucideIcon
  label: string
  key: string
}

const actionCategories: ActionCategory[] = [
  {
    icon: Search,
    label: 'Research',
    key: 'research'
  },
  {
    icon: Scale,
    label: 'Compare',
    key: 'compare'
  },
  {
    icon: Newspaper,
    label: 'Latest',
    key: 'latest'
  },
  {
    icon: FileText,
    label: 'Summarize',
    key: 'summarize'
  },
  {
    icon: HelpCircle,
    label: 'Explain',
    key: 'explain'
  }
]

const promptSamples: Record<string, string[]> = {
  research: [
    'Why is Nvidia growing so rapidly?',
    'Research the latest AI developments',
    'What are the key trends in robotics?',
    'What are the latest breakthroughs in renewable energy?'
  ],
  compare: [
    'Tesla vs BYD vs Toyota comparison',
    'Compare Next.js, Remix, and Astro',
    'AWS vs GCP vs Azure',
    'iPhone vs Android ecosystem comparison'
  ],
  latest: [
    'Latest news today',
    'What happened in tech this week?',
    'Recent breakthroughs in medicine',
    'Latest AI model releases'
  ],
  summarize: [
    'Summarize: https://arxiv.org/pdf/2504.19678',
    "Summarize this week's business news",
    'Create an executive summary of AI trends',
    'Summarize recent climate change research'
  ],
  explain: [
    'Explain neural networks simply',
    'How does blockchain work?',
    'What is quantum entanglement?',
    'Explain CRISPR gene editing'
  ]
}

interface ActionButtonsProps {
  onSelectPrompt: (prompt: string) => void
  onCategoryClick: (category: string) => void
  inputRef?: React.RefObject<HTMLTextAreaElement>
  className?: string
}

export function ActionButtons({
  onSelectPrompt,
  onCategoryClick,
  inputRef,
  className
}: ActionButtonsProps) {
  const [activeCategory, setActiveCategory] = useState<string | null>(null)
  const containerRef = useRef<HTMLDivElement>(null)

  const handleCategoryClick = (category: ActionCategory) => {
    setActiveCategory(category.key)
    onCategoryClick(category.label)
  }

  const handlePromptClick = (prompt: string) => {
    setActiveCategory(null)
    onSelectPrompt(prompt)
  }

  const resetToButtons = () => {
    setActiveCategory(null)
  }

  // Handle Escape key and clicks outside (including focus loss)
  useEffect(() => {
    const handleEscape = (e: KeyboardEvent) => {
      if (e.key === 'Escape' && activeCategory) {
        resetToButtons()
      }
    }

    const handleClickOutside = (e: MouseEvent) => {
      if (
        containerRef.current &&
        !containerRef.current.contains(e.target as Node)
      ) {
        if (activeCategory) {
          // Check if click is not on the input field
          if (!inputRef?.current?.contains(e.target as Node)) {
            resetToButtons()
          }
        }
      }
    }

    const handleFocusOut = () => {
      // Check if focus is moving outside both the container and input
      setTimeout(() => {
        const activeElement = document.activeElement
        if (
          activeCategory &&
          !containerRef.current?.contains(activeElement) &&
          activeElement !== inputRef?.current
        ) {
          resetToButtons()
        }
      }, FOCUS_OUT_DELAY_MS)
    }

    document.addEventListener('keydown', handleEscape)
    document.addEventListener('mousedown', handleClickOutside)
    document.addEventListener('focusout', handleFocusOut)

    return () => {
      document.removeEventListener('keydown', handleEscape)
      document.removeEventListener('mousedown', handleClickOutside)
      document.removeEventListener('focusout', handleFocusOut)
    }
  }, [activeCategory, inputRef])

  // Calculate max height needed for samples (4 items * ~40px + padding)
  const containerHeight = 'h-[180px]'

  return (
    <div
      ref={containerRef}
      className={cn('relative', containerHeight, className)}
    >
      <div className="relative h-full">
        {/* Action buttons */}
        <div
          className={cn(
            'absolute inset-0 flex items-start justify-center pt-2 transition-opacity duration-300',
            activeCategory ? 'opacity-0 pointer-events-none' : 'opacity-100'
          )}
        >
          <div className="flex flex-wrap justify-center gap-2 px-2">
            {actionCategories.map(category => {
              const Icon = category.icon
              return (
                <Button
                  key={category.key}
                  type="button"
                  variant="outline"
                  size="sm"
                  className={cn(
                    'flex items-center gap-2 whitespace-nowrap rounded-full',
                    'text-xs sm:text-sm px-3 sm:px-4'
                  )}
                  onClick={() => handleCategoryClick(category)}
                >
                  <Icon className="h-3 w-3 sm:h-4 sm:w-4" />
                  <span>{category.label}</span>
                </Button>
              )
            })}
          </div>
        </div>

        {/* Prompt samples */}
        <div
          className={cn(
            'absolute inset-0 py-1 space-y-1 overflow-y-auto transition-opacity duration-300',
            !activeCategory ? 'opacity-0 pointer-events-none' : 'opacity-100'
          )}
        >
          {activeCategory &&
            promptSamples[activeCategory]?.map((prompt, index) => (
              <button
                key={index}
                type="button"
                className={cn(
                  'w-full text-left px-3 py-2 rounded-md text-sm',
                  'hover:bg-muted transition-colors',
                  'flex items-center gap-2 group'
                )}
                onClick={() => handlePromptClick(prompt)}
              >
                <Search className="h-3 w-3 text-muted-foreground flex-shrink-0 group-hover:text-foreground" />
                <span className="line-clamp-1">{prompt}</span>
              </button>
            ))}
        </div>
      </div>
    </div>
  )
}


================================================
FILE: components/answer-section.tsx
================================================
'use client'

import { UseChatHelpers } from '@ai-sdk/react'
import { ChatRequestOptions } from 'ai'

import type { SearchResultItem } from '@/lib/types'
import type {
  UIDataTypes,
  UIMessage,
  UIMessageMetadata,
  UITools
} from '@/lib/types/ai'

import { CollapsibleMessage } from './collapsible-message'
import { MarkdownMessage } from './message'
import { MessageActions } from './message-actions'

export type AnswerSectionProps = {
  content: string
  isOpen: boolean
  onOpenChange: (open: boolean) => void
  chatId?: string
  showActions?: boolean
  messageId: string
  metadata?: UIMessageMetadata
  status?: UseChatHelpers<UIMessage<unknown, UIDataTypes, UITools>>['status']
  reload?: (
    messageId: string,
    options?: ChatRequestOptions
  ) => Promise<void | string | null | undefined>
  citationMaps?: Record<string, Record<number, SearchResultItem>>
  isGuest?: boolean
}

export function AnswerSection({
  content,
  isOpen,
  onOpenChange,
  chatId,
  showActions = true, // Default to true for backward compatibility
  messageId,
  metadata,
  status,
  reload,
  citationMaps,
  isGuest = false
}: AnswerSectionProps) {
  const enableShare =
    process.env.NEXT_PUBLIC_SUPABASE_URL !== undefined && !isGuest

  const handleReload = () => {
    if (reload) {
      return reload(messageId)
    }
    return Promise.resolve(undefined)
  }

  return (
    <CollapsibleMessage
      role="assistant"
      isCollapsible={false}
      isOpen={isOpen}
      onOpenChange={onOpenChange}
      showBorder={false}
      showIcon={false}
    >
      {content && (
        <div className="flex flex-col gap-1">
          <MarkdownMessage message={content} citationMaps={citationMaps} />
          <MessageActions
            message={content} // Provide original message; copy path remaps citations
            messageId={messageId}
            traceId={metadata?.traceId}
            feedbackScore={metadata?.feedbackScore}
            chatId={chatId}
            enableShare={enableShare}
            reload={handleReload}
            status={status}
            visible={showActions}
            citationMaps={citationMaps}
          />
        </div>
      )}
    </CollapsibleMessage>
  )
}


================================================
FILE: components/app-sidebar.tsx
================================================
import { Suspense } from 'react'
import Link from 'next/link'

import { Plus } from 'lucide-react'

import { cn } from '@/lib/utils'

import {
  Sidebar,
  SidebarContent,
  SidebarHeader,
  SidebarMenu,
  SidebarMenuButton,
  SidebarMenuItem,
  SidebarRail,
  SidebarTrigger
} from '@/components/ui/sidebar'

import { ChatHistorySection } from './sidebar/chat-history-section'
import { ChatHistorySkeleton } from './sidebar/chat-history-skeleton'
import { IconLogo } from './ui/icons'

export default function AppSidebar() {
  return (
    <Sidebar side="left" variant="sidebar" collapsible="offcanvas">
      <SidebarHeader className="flex flex-row justify-between items-center">
        <Link href="/" className="flex items-center gap-2 px-2 py-3">
          <IconLogo className={cn('size-5')} />
          <span className="font-semibold text-sm">Morphic</span>
        </Link>
        <SidebarTrigger />
      </SidebarHeader>
      <SidebarContent className="flex flex-col px-2 py-4 h-full">
        <SidebarMenu>
          <SidebarMenuItem>
            <SidebarMenuButton asChild>
              <Link href="/" className="flex items-center gap-2">
                <Plus className="size-4" />
                <span>New</span>
              </Link>
            </SidebarMenuButton>
          </SidebarMenuItem>
        </SidebarMenu>
        <div className="flex-1 overflow-y-auto">
          <Suspense fallback={<ChatHistorySkeleton />}>
            <ChatHistorySection />
          </Suspense>
        </div>
      </SidebarContent>
      <SidebarRail />
    </Sidebar>
  )
}


================================================
FILE: components/artifact/artifact-content.tsx
================================================
'use client'

import { Part, ToolPart } from '@/lib/types/ai'

import { ReasoningContent } from './reasoning-content'
import { TodoInvocationContent } from './todo-invocation-content'
import { ToolInvocationContent } from './tool-invocation-content'

// Type guard for Todo tool parts
function isTodoToolPart(part: Part): part is ToolPart<'todoWrite'> {
  return part.type === 'tool-todoWrite'
}

export function ArtifactContent({ part }: { part: Part | null }) {
  if (!part) return null

  switch (part.type) {
    case 'tool-search':
    case 'tool-fetch':
    case 'tool-askQuestion':
      return <ToolInvocationContent part={part} />
    case 'tool-todoWrite':
      if (isTodoToolPart(part)) {
        return <TodoInvocationContent part={part} />
      }
      return null
    case 'reasoning':
      return <ReasoningContent reasoning={part.text} />
    default:
      return (
        <div className="p-4">Details for this part type are not available</div>
      )
  }
}


================================================
FILE: components/artifact/artifact-context.tsx
================================================
'use client'

import {
  createContext,
  ReactNode,
  useCallback,
  useContext,
  useEffect,
  useReducer
} from 'react'

import type { Part } from '@/lib/types/ai'

import { useSidebar } from '../ui/sidebar'

// Animation duration should match CSS transition duration
const ANIMATION_DURATION = 300

interface ArtifactState {
  part: Part | null
  isOpen: boolean
}

type ArtifactAction =
  | { type: 'OPEN'; payload: Part }
  | { type: 'CLOSE' }
  | { type: 'CLEAR_CONTENT' }

const initialState: ArtifactState = {
  part: null,
  isOpen: false
}

function artifactReducer(
  state: ArtifactState,
  action: ArtifactAction
): ArtifactState {
  switch (action.type) {
    case 'OPEN':
      return { part: action.payload, isOpen: true }
    case 'CLOSE':
      return { ...state, isOpen: false }
    case 'CLEAR_CONTENT':
      return { part: null, isOpen: false }
    default:
      return state
  }
}

interface ArtifactContextValue {
  state: ArtifactState
  open: (part: Part) => void
  close: () => void
}

const ArtifactContext = createContext<ArtifactContextValue | undefined>(
  undefined
)

export function ArtifactProvider({ children }: { children: ReactNode }) {
  const [state, dispatch] = useReducer(artifactReducer, initialState)
  const { setOpen, open: sidebarOpen } = useSidebar()

  const close = useCallback(() => {
    dispatch({ type: 'CLOSE' })
    // Keep content for animation purposes, clear after transition
    setTimeout(() => {
      dispatch({ type: 'CLEAR_CONTENT' })
    }, ANIMATION_DURATION)
  }, [])

  // Close artifact when sidebar opens
  useEffect(() => {
    if (sidebarOpen && state.isOpen) {
      close()
    }
  }, [sidebarOpen, state.isOpen, close])

  const open = (part: Part) => {
    dispatch({ type: 'OPEN', payload: part })
    setOpen(false)
  }

  return (
    <ArtifactContext.Provider value={{ state, open, close }}>
      {children}
    </ArtifactContext.Provider>
  )
}

export function useArtifact() {
  const context = useContext(ArtifactContext)
  if (context === undefined) {
    throw new Error('useArtifact must be used within an ArtifactProvider')
  }
  return context
}


================================================
FILE: components/artifact/artifact-root.tsx
================================================
'use client'

import { ReactNode } from 'react'

import { ArtifactProvider } from './artifact-context'
import { ChatArtifactContainer } from './chat-artifact-container'

export default function ArtifactRoot({ children }: { children: ReactNode }) {
  return (
    <ArtifactProvider>
      <ChatArtifactContainer>{children}</ChatArtifactContainer>
    </ArtifactProvider>
  )
}


================================================
FILE: components/artifact/chat-artifact-container.tsx
================================================
'use client'

import React, { useCallback, useEffect, useRef, useState } from 'react'

import { useHasUser } from '@/lib/contexts/user-context'
import { cn } from '@/lib/utils'

import { SidebarTrigger, useSidebar } from '@/components/ui/sidebar'

import { InspectorDrawer } from '@/components/inspector/inspector-drawer'
import { InspectorPanel } from '@/components/inspector/inspector-panel'

import { useArtifact } from './artifact-context'

const DEFAULT_WIDTH = 500
const MIN_WIDTH = 320
const MAX_WIDTH = 800
const CHAT_MIN_WIDTH = 360
const RESIZE_OVERLAY_Z_INDEX = 9999

// Helper function to calculate allowed width bounds
function getAllowedWidthBounds(containerWidth: number): {
  allowedMin: number
  allowedMax: number
} {
  const available = Math.max(0, containerWidth - CHAT_MIN_WIDTH)
  const allowedMax = Math.min(MAX_WIDTH, available)

  // If there's no space available, hide the panel entirely
  if (allowedMax === 0) {
    return { allowedMin: 0, allowedMax: 0 }
  }

  // Ensure minimum width doesn't exceed available space
  const allowedMin = Math.min(MIN_WIDTH, allowedMax)
  return { allowedMin, allowedMax }
}

export function ChatArtifactContainer({
  children
}: {
  children: React.ReactNode
}) {
  const { state } = useArtifact()
  const containerRef = useRef<HTMLDivElement>(null)
  const [width, setWidth] = useState(DEFAULT_WIDTH)
  const [isResizing, setIsResizing] = useState(false)
  const hasUser = useHasUser()
  const { open, isMobile: isMobileSidebar } = useSidebar()

  // Load saved width after hydration
  useEffect(() => {
    const savedWidth = localStorage.getItem('artifactPanelWidth')
    if (savedWidth) {
      const parsedWidth = parseInt(savedWidth, 10)
      // Ensure parsedWidth is at least MIN_WIDTH to prevent invalid panel states
      if (
        !isNaN(parsedWidth) &&
        parsedWidth >= MIN_WIDTH &&
        parsedWidth <= MAX_WIDTH
      ) {
        // Clamp against available space considering chat minimum width
        const containerRect = containerRef.current?.getBoundingClientRect()
        if (containerRect) {
          const { allowedMin, allowedMax } = getAllowedWidthBounds(
            containerRect.width
          )
          const clamped = Math.min(
            Math.max(parsedWidth, allowedMin),
            allowedMax
          )
          setWidth(clamped)
        } else {
          setWidth(parsedWidth)
        }
      }
    }
  }, [])

  // Keep width in bounds when container resizes (e.g., window resize)
  useEffect(() => {
    const el = containerRef.current
    if (!el) return
    const ro = new ResizeObserver(entries => {
      for (const entry of entries) {
        const { allowedMin, allowedMax } = getAllowedWidthBounds(
          entry.contentRect.width
        )
        setWidth(prev => Math.min(Math.max(prev, allowedMin), allowedMax))
      }
    })
    ro.observe(el)
    return () => ro.disconnect()
  }, [])

  const startResize = useCallback((e: React.MouseEvent) => {
    e.preventDefault()
    setIsResizing(true)
  }, [])

  useEffect(() => {
    if (!isResizing) return

    const handleMouseMove = (e: MouseEvent) => {
      const containerRect = containerRef.current?.getBoundingClientRect()
      if (containerRect) {
        const newWidth = containerRect.right - e.clientX
        const { allowedMin, allowedMax } = getAllowedWidthBounds(
          containerRect.width
        )
        const clampedWidth = Math.min(
          Math.max(newWidth, allowedMin),
          allowedMax
        )
        setWidth(clampedWidth)
        localStorage.setItem('artifactPanelWidth', clampedWidth.toString())
      }
    }

    const handleMouseUp = () => {
      setIsResizing(false)
    }

    document.addEventListener('mousemove', handleMouseMove)
    document.addEventListener('mouseup', handleMouseUp)

    return () => {
      document.removeEventListener('mousemove', handleMouseMove)
      document.removeEventListener('mouseup', handleMouseUp)
    }
  }, [isResizing])

  return (
    <div className="flex-1 min-h-0 min-w-0 h-screen flex">
      <div className="absolute p-4 z-50 transition-opacity duration-1000">
        {hasUser && (!open || isMobileSidebar) && (
          <SidebarTrigger className="animate-fade-in" />
        )}
      </div>

      {/* Desktop: Independent panels like morphic-studio */}
      <div
        ref={containerRef}
        className="hidden md:flex flex-1 min-w-0 overflow-hidden"
      >
        {/* Chat Panel - Independent container */}
        <div className="flex-1 min-w-0 flex flex-col">{children}</div>

        {/* Resize Handle */}
        {state.isOpen && state.part && (
          <div
            className={cn(
              'w-1 mx-0.5 my-6 hover:bg-border transition-colors duration-200 cursor-col-resize select-none relative',
              isResizing && 'bg-border/50'
            )}
            onMouseDown={startResize}
          >
            <div className="absolute inset-0 -left-2 -right-2" />
          </div>
        )}

        {/* Right Panel - Independent with own animation */}
        <div
          className={cn(
            'bg-background overflow-hidden',
            state.isOpen && state.part ? 'opacity-100' : 'w-0 opacity-0',
            !isResizing && 'transition-all duration-300 ease-out'
          )}
          style={{
            width: state.isOpen && state.part ? `${width}px` : '0px'
          }}
        >
          <div className="h-full" style={{ width: `${width}px` }}>
            {state.isOpen && state.part && <InspectorPanel />}
          </div>
        </div>
      </div>

      {/* Resize overlay to prevent text selection */}
      {isResizing && (
        <div
          className="fixed inset-0 cursor-col-resize select-none"
          style={{ zIndex: RESIZE_OVERLAY_Z_INDEX }}
        />
      )}

      {/* Mobile: full-width chat + drawer */}
      <div className="md:hidden flex-1 h-full min-w-0">
        {children}
        <InspectorDrawer />
      </div>
    </div>
  )
}


================================================
FILE: components/artifact/reasoning-content.tsx
================================================
'use client'

import remarkGfm from 'remark-gfm'
import { Streamdown } from 'streamdown'

import { cn } from '@/lib/utils'

export function ReasoningContent({ reasoning }: { reasoning: string }) {
  return (
    <div className="overflow-auto">
      <div className={cn('prose-sm dark:prose-invert max-w-none')}>
        <Streamdown remarkPlugins={[remarkGfm]}>{reasoning}</Streamdown>
      </div>
    </div>
  )
}


================================================
FILE: components/artifact/search-artifact-content.tsx
================================================
'use client'

import type { SearchResults as TypeSearchResults } from '@/lib/types'
import type { ToolPart } from '@/lib/types/ai'

import { SearchResults } from '@/components/search-results'
import { SearchResultsImageSection } from '@/components/search-results-image'
import { Section, ToolArgsSection } from '@/components/section'
import {
  createVideoSearchResults,
  VideoSearchResults
} from '@/components/video-search-results'

export function SearchArtifactContent({ tool }: { tool: ToolPart<'search'> }) {
  // Handle streaming output states
  const output = tool.state === 'output-available' ? tool.output : undefined
  const searchResults: TypeSearchResults | undefined =
    output?.state === 'complete' ? output : undefined
  const query = tool.input?.query

  const hasResults =
    searchResults &&
    ((searchResults.results && searchResults.results.length > 0) ||
      (searchResults.videos && searchResults.videos.length > 0) ||
      (searchResults.images && searchResults.images.length > 0))

  if (!hasResults) {
    return <div className="p-4">No search results</div>
  }

  return (
    <div className="space-y-2">
      <div className="pb-2">
        <ToolArgsSection
          tool="search"
          number={
            (searchResults.results?.length || 0) +
            (searchResults.videos?.length || 0) +
            (searchResults.images?.length || 0)
          }
        >{`${query}`}</ToolArgsSection>
      </div>

      {searchResults.images && searchResults.images.length > 0 && (
        <SearchResultsImageSection
          images={searchResults.images}
          query={query}
          displayMode="full"
        />
      )}

      {searchResults.videos && searchResults.videos.length > 0 && (
        <Section title="Videos">
          <VideoSearchResults
            results={createVideoSearchResults(searchResults, query)}
            displayMode="artifact"
          />
        </Section>
      )}

      {searchResults.results && searchResults.results.length > 0 && (
        <Section title="Sources">
          <SearchResults results={searchResults.results} displayMode="list" />
        </Section>
      )}
    </div>
  )
}


================================================
FILE: components/artifact/todo-invocation-content.tsx
================================================
'use client'

import type { ToolPart } from '@/lib/types/ai'

import TodoListContent from '../todo-list-content'

interface TodoInvocationContentProps {
  part: ToolPart<'todoWrite'>
}

export function TodoInvocationContent({ part }: TodoInvocationContentProps) {
  const todos = part.output?.todos || part.input?.todos || []
  const completedCount = part.output?.completedCount
  const totalCount = part.output?.totalCount

  if (part.state === 'output-error') {
    return (
      <TodoListContent
        errorText={part.errorText || 'Failed to process todos'}
      />
    )
  }

  const message =
    part.output && 'message' in part.output
      ? (part.output.message as string | undefined)
      : undefined

  return (
    <TodoListContent
      todos={todos}
      message={message}
      completedCount={completedCount}
      totalCount={totalCount}
      showSummary={true}
      itemVariant="plain"
    />
  )
}


================================================
FILE: components/artifact/tool-invocation-content.tsx
================================================
'use client'

import type { ToolPart } from '@/lib/types/ai'

import { SearchArtifactContent } from '@/components/artifact/search-artifact-content'

export function ToolInvocationContent({ part }: { part: ToolPart }) {
  switch (part.type) {
    case 'tool-search':
      return <SearchArtifactContent tool={part as ToolPart<'search'>} />
    default:
      return <div className="p-4">Details for this tool are not available</div>
  }
}


================================================
FILE: components/attachment-preview.tsx
================================================
'use client'

import React from 'react'

interface Attachment {
  name: string | undefined
  url: string
  contentType: string
}
;[]
interface AttachmentPreviewProps {
  attachments: Attachment[]
}

export const AttachmentPreview: React.FC<AttachmentPreviewProps> = ({
  attachments
}) => {
  if (!attachments?.length) return null

  return (
    <div className="flex flex-wrap gap-4">
      {attachments.map((att, index) => {
        const isImage = att.contentType.startsWith('image/')
        const isPdf = att.contentType === 'application/pdf'

        return (
          <div key={index} className="max-w-xs break-words">
            {isImage ? (
              // eslint-disable-next-line @next/next/no-img-element
              <img
                src={att.url}
                alt={att.name}
                className="rounded-md border max-h-16 object-contain"
              />
            ) : isPdf ? (
              <a
                href={att.url}
                target="_blank"
                rel="noopener noreferrer"
                className="text-blue-600 underline"
              >
                📄 {att.name}
              </a>
            ) : (
              <a
                href={att.url}
                target="_blank"
                rel="noopener noreferrer"
                className="text-blue-600 underline"
              >
                📎 {att.name}
              </a>
            )}
          </div>
        )
      })}
    </div>
  )
}


================================================
FILE: components/auth-modal.tsx
================================================
'use client'

import Link from 'next/link'

import { Button } from '@/components/ui/button'
import {
  Dialog,
  DialogContent,
  DialogDescription,
  DialogHeader,
  DialogTitle
} from '@/components/ui/dialog'
import { IconLogo } from '@/components/ui/icons'

interface AuthModalProps {
  open: boolean
  onOpenChange: (open: boolean) => void
}

export function AuthModal({ open, onOpenChange }: AuthModalProps) {
  return (
    <Dialog open={open} onOpenChange={onOpenChange}>
      <DialogContent className="sm:max-w-md">
        <DialogHeader className="items-center text-center">
          <div className="mx-auto mb-6 flex size-20 items-center justify-center rounded-full bg-muted">
            <IconLogo className="size-14" />
          </div>
          <DialogTitle className="text-xl font-semibold">
            Continue with Morphic
          </DialogTitle>
          <DialogDescription className="text-muted-foreground">
            To use Morphic, sign in to your account or create a new one.
          </DialogDescription>
        </DialogHeader>
        <div className="mt-6 space-y-3">
          <Button asChild className="w-full" size="lg">
            <Link href="/auth/sign-up">Sign Up</Link>
          </Button>
          <Button asChild variant="outline" className="w-full" size="lg">
            <Link href="/auth/login">Sign In</Link>
          </Button>
        </div>
      </DialogContent>
    </Dialog>
  )
}


================================================
FILE: components/chat-error.tsx
================================================
import { AlertCircle } from 'lucide-react'

import { Card } from '@/components/ui/card'

interface ChatErrorProps {
  error: Error | string | null | undefined
}

export function ChatError({ error }: ChatErrorProps) {
  if (!error) return null

  let errorMessage = error instanceof Error ? error.message : error

  // Try to parse JSON error response and extract user-friendly message
  try {
    const jsonMatch = errorMessage?.match(/\{.*\}/)
    if (jsonMatch) {
      const parsed = JSON.parse(jsonMatch[0])
      if (parsed.error) {
        errorMessage = parsed.error
      }
    }
  } catch {
    // If parsing fails, use the original error message
  }

  return (
    <Card className="border-destructive bg-destructive/10 p-4">
      <div className="flex items-center gap-3">
        <AlertCircle className="size-5 text-destructive shrink-0" />
        <p className="text-sm text-destructive">{errorMessage}</p>
      </div>
    </Card>
  )
}


================================================
FILE: components/chat-messages.tsx
================================================
'use client'

import { useEffect, useMemo, useRef, useState } from 'react'

import { UseChatHelpers } from '@ai-sdk/react'

import { useMediaQuery } from '@/lib/hooks/use-media-query'
import type { UIDataTypes, UIMessage, UITools } from '@/lib/types/ai'
import { cn } from '@/lib/utils'
import { extractCitationMapsFromMessages } from '@/lib/utils/citation'

import { AnimatedLogo } from './ui/animated-logo'
import { ChatError } from './chat-error'
import { RenderMessage } from './render-message'

// Import section structure interface
interface ChatSection {
  id: string
  userMessage: UIMessage
  assistantMessages: UIMessage[]
}

interface ChatMessagesProps {
  sections: ChatSection[] // Changed from messages to sections
  onQuerySelect: (query: string) => void
  status: UseChatHelpers<UIMessage<unknown, UIDataTypes, UITools>>['status']
  chatId?: string
  isGuest?: boolean
  addToolResult?: (params: { toolCallId: string; result: any }) => void
  /** Ref for the scroll container */
  scrollContainerRef: React.RefObject<HTMLDivElement>
  onUpdateMessage?: (messageId: string, newContent: string) => Promise<void>
  reload?: (messageId: string) => Promise<void | string | null | undefined>
  error?: Error | string | null | undefined
}

export function ChatMessages({
  sections,
  onQuerySelect,
  status,
  chatId,
  isGuest = false,
  addToolResult,
  scrollContainerRef,
  onUpdateMessage,
  reload,
  error
}: ChatMessagesProps) {
  // Track user-modified states (when user explicitly opens/closes)
  const [userModifiedStates, setUserModifiedStates] = useState<
    Record<string, boolean>
  >({})
  // Cache tool counts for performance optimization
  const toolCountCacheRef = useRef<Map<string, number>>(new Map())
  const isLoading = status === 'submitted' || status === 'streaming'
  const isMobile = useMediaQuery('(max-width: 767px)')

  // Tool types definition - moved outside function for performance
  const toolTypes = [
    'tool-search',
    'tool-fetch',
    'tool-askQuestion',
    'tool-relatedQuestions'
  ]

  // Clear cache during streaming to ensure accurate tool counts
  useEffect(() => {
    if (isLoading) {
      // Clear cache for all messages during streaming
      toolCountCacheRef.current.clear()
    }
  }, [isLoading])

  // Calculate the offset height based on device type
  // Note: pt-14 (56px) on scroll-container must be included in desktop offset
  const offsetHeight = isMobile
    ? 208 // Mobile: larger offset for mobile header/input
    : 196 // Desktop: smaller offset (140px) + pt-14 (56px)

  // Extract citation maps from all messages in all sections
  const allCitationMaps = useMemo(() => {
    const allMessages: UIMessage[] = []
    sections.forEach(section => {
      allMessages.push(section.userMessage)
      allMessages.push(...section.assistantMessages)
    })
    return extractCitationMapsFromMessages(allMessages)
  }, [sections])

  if (!sections.length) return null

  // Check if loading indicator should be shown
  const showLoading = status === 'submitted' || status === 'streaming'

  // Helper function to get tool count with caching
  const getToolCount = (message?: UIMessage): number => {
    if (!message || !message.id) return 0

    // During streaming, always recalculate
    if (isLoading) {
      const count =
        message.parts?.filter(part => toolTypes.includes(part.type)).length || 0
      return count
    }

    // Check cache first when not streaming
    const cached = toolCountCacheRef.current.get(message.id)
    if (cached !== undefined) {
      return cached
    }

    // Calculate and cache
    const count =
      message.parts?.filter(part => toolTypes.includes(part.type)).length || 0
    toolCountCacheRef.current.set(message.id, count)
    return count
  }

  const getIsOpen = (
    id: string,
    partType?: string,
    hasNextPart?: boolean,
    message?: UIMessage
  ) => {
    // If user has explicitly modified this state, use that
    if (userModifiedStates.hasOwnProperty(id)) {
      return userModifiedStates[id]
    }

    // For tool types, check if there are multiple tools
    if (partType && toolTypes.includes(partType)) {
      const toolCount = getToolCount(message)
      // If multiple tools exist, default to closed
      if (toolCount > 1) {
        return false
      }
      // Single tool results stay open even if more content follows
      return true
    }

    // For tool-invocations, default to open
    if (partType === 'tool-invocation') {
      return true
    }

    // For reasoning, auto-collapse if there's a next part in the same message
    if (partType === 'reasoning') {
      return !hasNextPart
    }

    // For other types (like text), default to open
    return true
  }

  const handleOpenChange = (id: string, open: boolean) => {
    setUserModifiedStates(prev => ({
      ...prev,
      [id]: open
    }))
  }

  return (
    <div
      id="scroll-container"
      ref={scrollContainerRef}
      role="list"
      aria-roledescription="chat messages"
      className={cn(
        'relative size-full pt-14',
        sections.length > 0 ? 'flex-1 overflow-y-auto' : ''
      )}
    >
      <div className="relative mx-auto w-full max-w-full md:max-w-3xl px-4">
        {sections.map((section, sectionIndex) => (
          <div
            key={section.id}
            id={`section-${section.id}`}
            className="chat-section pb-14"
            style={
              sectionIndex === sections.length - 1
                ? { minHeight: `calc(100dvh - ${offsetHeight}px)` }
                : {}
            }
          >
            {/* User message */}
            <div className="flex flex-col gap-4 mb-4">
              <RenderMessage
                message={section.userMessage}
                messageId={section.userMessage.id}
                getIsOpen={(id, partType, hasNextPart) =>
                  getIsOpen(id, partType, hasNextPart, section.userMessage)
                }
                onOpenChange={handleOpenChange}
                onQuerySelect={onQuerySelect}
                chatId={chatId}
                isGuest={isGuest}
                status={status}
                addToolResult={addToolResult}
                onUpdateMessage={onUpdateMessage}
                reload={reload}
                citationMaps={allCitationMaps}
              />
            </div>

            {/* Assistant messages */}
            {section.assistantMessages.map((assistantMessage, messageIndex) => {
              // Check if this is the latest assistant message in the latest section
              const isLatestMessage =
                sectionIndex === sections.length - 1 &&
                messageIndex === section.assistantMessages.length - 1

              return (
                <div key={assistantMessage.id} className="flex flex-col gap-4">
                  <RenderMessage
                    message={assistantMessage}
                    messageId={assistantMessage.id}
                    getIsOpen={(id, partType, hasNextPart) =>
                      getIsOpen(id, partType, hasNextPart, assistantMessage)
                    }
                    onOpenChange={handleOpenChange}
                    onQuerySelect={onQuerySelect}
                    chatId={chatId}
                    isGuest={isGuest}
                    status={status}
                    addToolResult={addToolResult}
                    onUpdateMessage={onUpdateMessage}
                    reload={reload}
                    isLatestMessage={isLatestMessage}
                    citationMaps={allCitationMaps}
                  />
                </div>
              )
            })}
            {/* Show loading after assistant messages */}
            {showLoading && sectionIndex === sections.length - 1 && (
              <div className="flex justify-start py-4">
                <AnimatedLogo className="h-10 w-10" />
              </div>
            )}
            {sectionIndex === sections.length - 1 && (
              <ChatError error={error} />
            )}
          </div>
        ))}
      </div>
    </div>
  )
}


================================================
FILE: components/chat-panel.tsx
================================================
'use client'

import { useCallback, useEffect, useRef, useState } from 'react'
import Textarea from 'react-textarea-autosize'
import { useRouter } from 'next/navigation'

import { UseChatHelpers } from '@ai-sdk/react'
import { ArrowUp, ChevronDown, MessageCirclePlus, Square } from 'lucide-react'
import { toast } from 'sonner'

import { UploadedFile } from '@/lib/types'
import type { UIDataTypes, UIMessage, UITools } from '@/lib/types/ai'
import { cn } from '@/lib/utils'

import { useArtifact } from './artifact/artifact-context'
import { Button } from './ui/button'
import { IconBlinkingLogo } from './ui/icons'
import { ActionButtons } from './action-buttons'
import { FileUploadButton } from './file-upload-button'
import { ModelTypeSelector } from './model-type-selector'
import { SearchModeSelector } from './search-mode-selector'
import { UploadedFileList } from './uploaded-file-list'

// Constants for timing delays
const INPUT_UPDATE_DELAY_MS = 10 // Delay to ensure input value is updated before form submission

interface ChatPanelProps {
  chatId: string
  input: string
  handleInputChange: (e: React.ChangeEvent<HTMLTextAreaElement>) => void
  handleSubmit: (e: React.FormEvent<HTMLFormElement>) => void
  status: UseChatHelpers<UIMessage<unknown, UIDataTypes, UITools>>['status']
  messages: UIMessage[]
  setMessages: (messages: UIMessage[]) => void
  query?: string
  stop: () => void
  append: (message: any) => void
  /** Whether to show the scroll to bottom button */
  showScrollToBottomButton: boolean
  /** Reference to the scroll container */
  scrollContainerRef: React.RefObject<HTMLDivElement>
  uploadedFiles: UploadedFile[]
  setUploadedFiles: React.Dispatch<React.SetStateAction<UploadedFile[]>>
  /** Callback to reset chatId when starting a new chat */
  onNewChat?: () => void
  /** Whether the current session is guest */
  isGuest?: boolean
}

export function ChatPanel({
  chatId,
  input,
  handleInputChange,
  handleSubmit,
  status,
  messages,
  setMessages,
  query,
  stop,
  append,
  showScrollToBottomButton,
  uploadedFiles,
  setUploadedFiles,
  scrollContainerRef,
  onNewChat,
  isGuest = false
}: ChatPanelProps) {
  const router = useRouter()
  const inputRef = useRef<HTMLTextAreaElement>(null)
  const isFirstRender = useRef(true)
  const [isComposing, setIsComposing] = useState(false) // Composition state
  const [enterDisabled, setEnterDisabled] = useState(false) // Disable Enter after composition ends
  const [isInputFocused, setIsInputFocused] = useState(false) // Track input focus
  const { close: closeArtifact } = useArtifact()
  const isLoading = status === 'submitted' || status === 'streaming'

  const handleCompositionStart = () => setIsComposing(true)

  const handleCompositionEnd = () => {
    setIsComposing(false)
    setEnterDisabled(true)
    setTimeout(() => {
      setEnterDisabled(false)
    }, 300)
  }

  const handleNewChat = () => {
    setMessages([])
    closeArtifact()
    // Reset focus state when clearing chat
    setIsInputFocused(false)
    inputRef.current?.blur()
    // Reset chatId in parent component
    onNewChat?.()
    router.push('/')
  }

  const isToolInvocationInProgress = () => {
    if (!messages.length) return false

    const lastMessage = messages[messages.length - 1]
    if (lastMessage.role !== 'assistant' || !lastMessage.parts) return false

    const parts = lastMessage.parts
    const lastPart = parts[parts.length - 1]

    return (
      (lastPart?.type === 'tool-search' ||
        lastPart?.type === 'tool-fetch' ||
        lastPart?.type === 'tool-askQuestion') &&
      ((lastPart as any)?.state === 'input-streaming' ||
        (lastPart as any)?.state === 'input-available')
    )
  }

  // if query is not empty, submit the query
  useEffect(() => {
    if (isFirstRender.current && query && query.trim().length > 0) {
      append({
        role: 'user',
        content: query
      })
      isFirstRender.current = false
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [query])

  const handleFileRemove = useCallback(
    (index: number) => {
      setUploadedFiles(prev => prev.filter((_, i) => i !== index))
    },
    [setUploadedFiles]
  )
  // Scroll to the bottom of the container
  const handleScrollToBottom = () => {
    const scrollContainer = scrollContainerRef.current
    if (scrollContainer) {
      scrollContainer.scrollTo({
        top: scrollContainer.scrollHeight,
        behavior: 'smooth'
      })
    }
  }

  return (
    <div
      className={cn(
        'w-full bg-background group/form-container shrink-0',
        messages.length > 0 ? 'sticky bottom-0 px-2 pb-4' : 'px-6'
      )}
    >
      {messages.length === 0 && (
        <div className="mb-10 flex flex-col items-center gap-4">
          <IconBlinkingLogo className="size-12" />
          <h1 className="text-2xl font-medium text-foreground">
            What would you like to know?
          </h1>
        </div>
      )}
      {uploadedFiles.length > 0 && (
        <UploadedFileList files={uploadedFiles} onRemove={handleFileRemove} />
      )}
      <form
        onSubmit={e => {
          handleSubmit(e)
          // Reset focus state after submission
          setIsInputFocused(false)
          inputRef.current?.blur()
        }}
        className={cn('max-w-full md:max-w-3xl w-full mx-auto relative')}
      >
        {/* Scroll to bottom button - only shown when showScrollToBottomButton is true */}
        {showScrollToBottomButton && messages.length > 0 && (
          <Button
            type="button"
            variant="outline"
            size="icon"
            className="absolute -top-10 right-4 z-20 size-8 rounded-full shadow-md"
            onClick={handleScrollToBottom}
            title="Scroll to bottom"
          >
            <ChevronDown size={16} />
          </Button>
        )}

        <div
          className={cn(
            'relative flex flex-col w-full gap-2 bg-muted rounded-3xl border border-input transition-shadow',
            isInputFocused &&
              'ring-1 ring-ring/20 ring-offset-1 ring-offset-background/50'
          )}
        >
          <Textarea
            ref={inputRef}
            name="input"
            rows={2}
            maxRows={5}
            tabIndex={0}
            onCompositionStart={handleCompositionStart}
            onCompositionEnd={handleCompositionEnd}
            onFocus={() => setIsInputFocused(true)}
            onBlur={() => setIsInputFocused(false)}
            placeholder="Ask anything..."
            spellCheck={false}
            value={input}
            disabled={isLoading || isToolInvocationInProgress()}
            className="resize-none w-full min-h-12 bg-transparent border-0 p-4 text-sm placeholder:text-muted-foreground focus-visible:outline-hidden disabled:cursor-not-allowed disabled:opacity-50"
            onChange={handleInputChange}
            onKeyDown={e => {
              if (
                e.key === 'Enter' &&
                !e.shiftKey &&
                !isComposing &&
                !enterDisabled
              ) {
                if (input.trim().length === 0) {
                  e.preventDefault()
                  return
                }
                e.preventDefault()
                const textarea = e.target as HTMLTextAreaElement
                textarea.form?.requestSubmit()
                // Reset focus state after Enter key submission
                setIsInputFocused(false)
                textarea.blur()
              }
            }}
          />

          {/* Bottom menu area */}
          <div className="flex items-center justify-between p-3">
            <div className="flex items-center gap-2">
              {!isGuest && (
                <FileUploadButton
                  onFileSelect={async files => {
                    const newFiles: UploadedFile[] = files.map(file => ({
                      file,
                      status: 'uploading'
                    }))
                    setUploadedFiles(prev => [...prev, ...newFiles])
                    await Promise.all(
                      newFiles.map(async uf => {
                        const formData = new FormData()
                        formData.append('file', uf.file)
                        formData.append('chatId', chatId)
                        try {
                          const res = await fetch('/api/upload', {
                            method: 'POST',
                            body: formData
                          })

                          if (!res.ok) {
                            throw new Error('Upload failed')
                          }

                          const { file: uploaded } = await res.json()
                          setUploadedFiles(prev =>
                            prev.map(f =>
                              f.file === uf.file
                                ? {
                                    ...f,
                                    status: 'uploaded',
                                    url: uploaded.url,
                                    name: uploaded.filename,
                                    key: uploaded.key
                                  }
                                : f
                            )
                          )
                        } catch (e) {
                          toast.error(`Failed to upload ${uf.file.name}`)
                          setUploadedFiles(prev =>
                            prev.map(f =>
                              f.file === uf.file ? { ...f, status: 'error' } : f
                            )
                          )
                        }
                      })
                    )
                  }}
                />
              )}
              <SearchModeSelector />
            </div>
            <div className="flex items-center gap-2">
              {messages.length > 0 && (
                <Button
                  variant="outline"
                  size="icon"
                  onClick={handleNewChat}
                  className="shrink-0 rounded-full group"
                  type="button"
                  disabled={isLoading}
                >
                  <MessageCirclePlus className="size-4 group-hover:rotate-12 transition-all" />
                </Button>
              )}
              {process.env.NEXT_PUBLIC_MORPHIC_CLOUD_DEPLOYMENT !== 'true' && (
                <ModelTypeSelector disabled={isGuest} />
              )}
              <Button
                type={isLoading ? 'button' : 'submit'}
                size={'icon'}
                className={cn(isLoading && 'animate-pulse', 'rounded-full')}
                disabled={input.length === 0 && !isLoading}
                onClick={isLoading ? stop : undefined}
              >
                {isLoading ? <Square size={20} /> : <ArrowUp size={20} />}
              </Button>
            </div>
          </div>
        </div>

        {/* Action buttons for prompt suggestions */}
        {messages.length === 0 && (
          <ActionButtons
            onSelectPrompt={message => {
              // Set the input value and submit
              handleInputChange({
                target: { value: message }
              } as React.ChangeEvent<HTMLTextAreaElement>)
              // Submit the form after a small delay to ensure the input is updated
              setTimeout(() => {
                inputRef.current?.form?.requestSubmit()
                // Reset focus state after action button submission
                setIsInputFocused(false)
                inputRef.current?.blur()
              }, INPUT_UPDATE_DELAY_MS)
            }}
            onCategoryClick={category => {
              // Set the category in the input
              handleInputChange({
                target: { value: category }
              } as React.ChangeEvent<HTMLTextAreaElement>)
              // Focus the input
              inputRef.current?.focus()
            }}
            inputRef={inputRef}
            className="mt-2"
          />
        )}
      </form>
    </div>
  )
}


================================================
FILE: components/chat-share.tsx
================================================
'use client'

import { useState, useTransition } from 'react'

import { Share } from 'lucide-react'
import { toast } from 'sonner'

import { shareChat } from '@/lib/actions/chat'
import { useCopyToClipboard } from '@/lib/hooks/use-copy-to-clipboard'
import { cn } from '@/lib/utils'

import { Button } from './ui/button'
import {
  Dialog,
  DialogContent,
  DialogDescription,
  DialogFooter,
  DialogHeader,
  DialogTitle,
  DialogTrigger
} from './ui/dialog'
import { Spinner } from './ui/spinner'

interface ChatShareProps {
  chatId: string
  className?: string
}

export function ChatShare({ chatId, className }: ChatShareProps) {
  const [open, setOpen] = useState(false)
  const [pending, startTransition] = useTransition()
  const { copyToClipboard } = useCopyToClipboard({ timeout: 1000 })
  const [shareUrl, setShareUrl] = useState('')

  const handleShare = async () => {
    startTransition(() => {
      setOpen(true)
    })

    const sharedChatObject = await shareChat(chatId)
    if (!sharedChatObject) {
      toast.error(
        'Failed to make chat public. You may need to be logged in or own the chat.'
      )
      return
    }

    const url = new URL(
      `/search/${sharedChatObject.id}`,
      window.location.origin
    )
    setShareUrl(url.toString())
  }

  const handleCopy = () => {
    if (shareUrl) {
      copyToClipboard(shareUrl)
      toast.success('Link copied to clipboard')
      setOpen(false)
    } else {
      toast.error('No link to copy')
    }
  }

  return (
    <div className={className}>
      <Dialog
        open={open}
        onOpenChange={open => setOpen(open)}
        aria-labelledby="share-dialog-title"
        aria-describedby="share-dialog-description"
      >
        <DialogTrigger asChild>
          <Button
            className={cn('rounded-full')}
            size="icon"
            variant={'ghost'}
            onClick={() => setOpen(true)}
          >
            <Share size={14} />
          </Button>
        </DialogTrigger>
        <DialogContent>
          <DialogHeader>
            <DialogTitle>Share Chat</DialogTitle>
            <DialogDescription>
              Anyone with the link will be able to view this chat if it&apos;s
              public.
            </DialogDescription>
          </DialogHeader>
          <DialogFooter className="items-center">
            {!shareUrl && (
              <Button onClick={handleShare} disabled={pending} size="sm">
                {pending ? <Spinner /> : 'Get link'}
              </Button>
            )}
            {shareUrl && (
              <Button onClick={handleCopy} disabled={pending} size="sm">
                {'Copy link'}
              </Button>
            )}
          </DialogFooter>
        </DialogContent>
      </Dialog>
    </div>
  )
}


================================================
FILE: components/chat.tsx
================================================
'use client'

import { useEffect, useMemo, useRef, useState } from 'react'
import { useRouter } from 'next/navigation'

import { useChat } from '@ai-sdk/react'
import { DefaultChatTransport } from 'ai'
import { toast } from 'sonner'

import { generateId } from '@/lib/db/schema'
import { UploadedFile } from '@/lib/types'
import type { UIMessage } from '@/lib/types/ai'
import {
  isDynamicToolPart,
  isToolCallPart,
  isToolTypePart
} from '@/lib/types/dynamic-tools'
import { cn } from '@/lib/utils'

import { useFileDropzone } from '@/hooks/use-file-dropzone'

import { ChatMessages } from './chat-messages'
import { ChatPanel } from './chat-panel'
import { DragOverlay } from './drag-overlay'
import { ErrorModal } from './error-modal'

// Define section structure
interface ChatSection {
  id: string // User message ID
  userMessage: UIMessage
  assistantMessages: UIMessage[]
}

export function Chat({
  id: providedId,
  savedMessages = [],
  query,
  isGuest = false
}: {
  id?: string
  savedMessages?: UIMessage[]
  query?: string
  isGuest?: boolean
}) {
  const router = useRouter()

  // Generate a stable chatId on the client side
  // - If providedId exists (e.g., /search/[id]), use it for existing chats
  // - Otherwise, generate a new ID (e.g., / homepage for new chats)
  const [chatId, setChatId] = useState(() => providedId || generateId())

  // Callback to reset chat state when user clicks "New" button
  const handleNewChat = () => {
    const newId = generateId()
    setChatId(newId)
    // Clear other chat-related state that persists due to Next.js 16 component caching
    setInput('')
    setUploadedFiles([])
    setErrorModal({
      open: false,
      type: 'general',
      message: ''
    })
  }

  const scrollContainerRef = useRef<HTMLDivElement>(null)
  const [isAtBottom, setIsAtBottom] = useState(true)
  const [uploadedFiles, setUploadedFiles] = useState<UploadedFile[]>([])
  const [input, setInput] = useState('')
  const [errorModal, setErrorModal] = useState<{
    open: boolean
    type: 'rate-limit' | 'auth' | 'forbidden' | 'general'
    message: string
    details?: string
  }>({
    open: false,
    type: 'general',
    message: ''
  })

  const {
    messages,
    status,
    setMessages,
    stop,
    sendMessage,
    regenerate,
    addToolResult,
    error
  } = useChat({
    id: chatId, // use the client-generated or provided chatId
    transport: new DefaultChatTransport({
      api: '/api/chat',
      prepareSendMessagesRequest: ({ messages, trigger, messageId }) => {
        // Simplify by passing AI SDK's default trigger values directly
        const lastMessage = messages[messages.length - 1]
        const messageToRegenerate =
          trigger === 'regenerate-message'
            ? messages.find(m => m.id === messageId)
            : undefined

        return {
          body: {
            trigger, // Use AI SDK's default trigger value directly
            chatId: chatId,
            messageId,
            ...(isGuest ? { messages } : {}),
            message:
              trigger === 'regenerate-message' &&
              messageToRegenerate?.role === 'user'
                ? messageToRegenerate
                : trigger === 'submit-message'
                  ? lastMessage
                  : undefined,
            isNewChat:
              trigger === 'submit-message' &&
              messages.length === 1 &&
              savedMessages.length === 0
          }
        }
      }
    }),
    messages: savedMessages,
    onFinish: () => {
      window.dispatchEvent(new CustomEvent('chat-history-updated'))
    },
    onError: error => {
      // Handle rate limiting errors from Vercel WAF
      // Check for status codes in error message or specific rate limit indicators
      const errorMessage = error.message?.toLowerCase() || ''
      const isRateLimit =
        error.message?.includes('429') ||
        errorMessage.includes('rate limit') ||
        errorMessage.includes('too many requests') ||
        errorMessage.includes('daily limit')

      // Check for authentication errors
      const isAuthError =
        error.message?.includes('401') ||
        errorMessage.includes('unauthorized') ||
        errorMessage.includes('authentication required') ||
        errorMessage.includes('sign in to continue')

      if (isRateLimit) {
        // Try to parse JSON error response for quality mode rate limit
        let parsedError: {
          error?: string
          resetAt?: number
          remaining?: number
        } = {}
        try {
          // Extract JSON from error message if it exists
          const jsonMatch = error.message?.match(/\{.*\}/)
          if (jsonMatch) {
            parsedError = JSON.parse(jsonMatch[0])
          }
        } catch {
          // Ignore parse errors
        }

        // Use parsed error message or fallback
        const userMessage =
          parsedError.error ||
          'You have reached your daily limit for quality mode chat requests.'

        setErrorModal({
          open: true,
          type: 'rate-limit',
          message: userMessage,
          details: undefined
        })
      } else if (isAuthError) {
        setErrorModal({
          open: true,
          type: 'auth',
          message: error.message
        })
      } else if (
        error.message?.includes('403') ||
        errorMessage.includes('forbidden')
      ) {
        setErrorModal({
          open: true,
          type: 'forbidden',
          message: error.message
        })
      } else {
        // For general errors, still use toast for less intrusive notification
        toast.error(`Error in chat: ${error.message}`)
      }
    },
    experimental_throttle: 100,
    generateId
  })

  const handleInputChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
    setInput(e.target.value)
  }

  // Convert messages array to sections array
  const sections = useMemo<ChatSection[]>(() => {
    const result: ChatSection[] = []
    let currentSection: ChatSection | null = null

    for (const message of messages) {
      if (message.role === 'user') {
        // Start a new section when a user message is found
        if (currentSection) {
          result.push(currentSection)
        }
        currentSection = {
          id: message.id,
          userMessage: message,
          assistantMessages: []
        }
      } else if (currentSection && message.role === 'assistant') {
        // Add assistant message to the current section
        currentSection.assistantMessages.push(message)
      }
      // Ignore other role types like 'system' for now
    }

    // Add the last section if exists
    if (currentSection) {
      result.push(currentSection)
    }

    return result
  }, [messages])

  // Dispatch custom event when messages change
  useEffect(() => {
    window.dispatchEvent(
      new CustomEvent('messages-changed', {
        detail: { hasMessages: messages.length > 0 }
      })
    )
  }, [messages.length])

  // Detect if scroll container is at the bottom
  useEffect(() => {
    const container = scrollContainerRef.current
    if (!container) return

    const handleScroll = () => {
      const { scrollTop, scrollHeight, clientHeight } = container
      const threshold = 50 // threshold in pixels
      if (scrollHeight - scrollTop - clientHeight < threshold) {
        setIsAtBottom(true)
      } else {
        setIsAtBottom(false)
      }
    }

    container.addEventListener('scroll', handleScroll, { passive: true })
    handleScroll() // Set initial state

    return () => container.removeEventListener('scroll', handleScroll)
  }, [messages.length])

  // Check scroll position when messages change (during generation)
  useEffect(() => {
    const container = scrollContainerRef.current
    if (!container) return

    const { scrollTop, scrollHeight, clientHeight } = container
    const threshold = 50
    if (scrollHeight - scrollTop - clientHeight < threshold) {
      setIsAtBottom(true)
    } else {
      setIsAtBottom(false)
    }
  }, [messages])

  // Scroll to the section when a new user message is sent
  useEffect(() => {
    // Only scroll if this chat is currently visible in the URL
    const isCurrentChat =
      window.location.pathname === `/search/${chatId}` ||
      (window.location.pathname === '/' && sections.length > 0)

    if (isCurrentChat && sections.length > 0) {
      const lastMessage = messages[messages.length - 1]
      if (lastMessage && lastMessage.role === 'user') {
        // If the last message is from user, find the corresponding section
        const sectionId = lastMessage.id
        requestAnimationFrame(() => {
          const sectionElement = document.getElementById(`section-${sectionId}`)
          sectionElement?.scrollIntoView({ behavior: 'smooth', block: 'start' })
        })
      }
    }
  }, [sections, messages, chatId])

  const onQuerySelect = (query: string) => {
    sendMessage({
      role: 'user',
      parts: [{ type: 'text', text: query }]
    })
  }

  const handleUpdateAndReloadMessage = async (
    editedMessageId: string,
    newContentText: string
  ) => {
    if (!chatId) {
      toast.error('Chat ID is missing.')
      console.error('handleUpdateAndReloadMessage: chatId is undefined.')
      return
    }

    try {
      // Update the message locally with the same ID
      setMessages(prevMessages => {
        const messageIndex = prevMessages.findIndex(
          m => m.id === editedMessageId
        )
        if (messageIndex === -1) return prevMessages

        const updatedMessages = [...prevMessages]
        updatedMessages[messageIndex] = {
          ...updatedMessages[messageIndex],
          parts: [{ type: 'text', text: newContentText }]
        }

        return updatedMessages
      })

      // Regenerate from this message
      await regenerate({ messageId: editedMessageId })
    } catch (error) {
      console.error('Error during message edit and reload process:', error)
      toast.error(
        `Error processing edited message: ${(error as Error).message}`
      )
    }
  }

  const handleReloadFrom = async (reloadFromFollowerMessageId: string) => {
    if (!chatId) {
      toast.error('Chat ID is missing for reload.')
      return
    }

    try {
      // Use the SDK's regenerate function with the specific messageId
      await regenerate({ messageId: reloadFromFollowerMessageId })
    } catch (error) {
      console.error(
        `Error during reload from message ${reloadFromFollowerMessageId}:`,
        error
      )
      toast.error(`Failed to reload conversation: ${(error as Error).message}`)
    }
  }

  const onSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault()

    const uploaded = uploadedFiles.filter(f => f.status === 'uploaded')

    if (input.trim() || uploaded.length > 0) {
      const parts: any[] = []

      if (input.trim()) {
        parts.push({ type: 'text', text: input })
      }

      uploaded.forEach(f => {
        parts.push({
          type: 'file',
          url: f.url!,
          filename: f.name!,
          mediaType: f.file.type
        })
      })

      sendMessage({ role: 'user', parts })
      setInput('')
      setUploadedFiles([])

      // Push URL state immediately after sending message (for new chats)
      // Check if we're on the root path (new chat)
      if (!isGuest && window.location.pathname === '/') {
        window.history.pushState({}, '', `/search/${chatId}`)
      }
    }
  }

  const { isDragging, handleDragOver, handleDragLeave, handleDrop } =
    useFileDropzone({
      uploadedFiles,
      setUploadedFiles,
      chatId: chatId
    })
  const guestDragHandlers = {
    isDragging: false,
    handleDragOver: (e: React.DragEvent<HTMLDivElement>) => {
      e.preventDefault()
    },
    handleDragLeave: (e: React.DragEvent<HTMLDivElement>) => {
      e.preventDefault()
    },
    handleDrop: (e: React.DragEvent<HTMLDivElement>) => {
      e.preventDefault()
    }
  }
  const dragHandlers = isGuest
    ? guestDragHandlers
    : { isDragging, handleDragOver, handleDragLeave, handleDrop }

  return (
    <div
      className={cn(
        'relative flex h-full min-w-0 flex-1 flex-col',
        messages.length === 0 ? 'items-center justify-center' : ''
      )}
      data-testid="full-chat"
      onDragOver={dragHandlers.handleDragOver}
      onDragLeave={dragHandlers.handleDragLeave}
      onDrop={dragHandlers.handleDrop}
    >
      <ChatMessages
        sections={sections}
        onQuerySelect={onQuerySelect}
        status={status}
        chatId={chatId}
        isGuest={isGuest}
        addToolResult={({
          toolCallId,
          result
        }: {
          toolCallId: string
          result: any
        }) => {
          // Find the tool name from the message parts
          let toolName = 'unknown'

          // Optimize by breaking early once found
          outerLoop: for (const message of messages) {
            if (!message.parts) continue

            for (const part of message.parts) {
              if (isToolCallPart(part) && part.toolCallId === toolCallId) {
                toolName = part.toolName
                break outerLoop
              } else if (
                isToolTypePart(part) &&
                part.toolCallId === toolCallId
              ) {
                toolName = part.type.substring(5) // Remove 'tool-' prefix
                break outerLoop
              } else if (
                isDynamicToolPart(part) &&
                part.toolCallId === toolCallId
              ) {
                toolName = part.toolName
                break outerLoop
              }
            }
          }

          addToolResult({ tool: toolName, toolCallId, output: result })
        }}
        scrollContainerRef={scrollContainerRef}
        onUpdateMessage={handleUpdateAndReloadMessage}
        reload={handleReloadFrom}
        error={error}
      />
      <ChatPanel
        chatId={chatId}
        input={input}
        handleInputChange={handleInputChange}
        handleSubmit={onSubmit}
        status={status}
        messages={messages}
        setMessages={setMessages}
        stop={stop}
        query={query}
        append={(message: any) => {
          sendMessage(message)
        }}
        showScrollToBottomButton={!isAtBottom}
        uploadedFiles={uploadedFiles}
        setUploadedFiles={setUploadedFiles}
        scrollContainerRef={scrollContainerRef}
        onNewChat={handleNewChat}
        isGuest={isGuest}
      />
      <DragOverlay visible={dragHandlers.isDragging} />
      <ErrorModal
        open={errorModal.open}
        onOpenChange={open => setErrorModal(prev => ({ ...prev, open }))}
        error={errorModal}
        onRetry={
          errorModal.type !== 'rate-limit'
            ? () => {
                // Retry the last message if not rate limited
                if (messages.length > 0) {
                  const lastUserMessage = messages
                    .filter(m => m.role === 'user')
                    .pop()
                  if (lastUserMessage) {
                    sendMessage(lastUserMessage)
                  }
                }
              }
            : undefined
        }
        onAuthClose={() => {
          // Clear messages and navigate to root
          setMessages([])
          router.push('/')
        }}
      />
    </div>
  )
}


================================================
FILE: components/citation-context.tsx
================================================
'use client'

import { createContext, ReactNode, useContext } from 'react'

import type { SearchResultItem } from '@/lib/types'

interface CitationContextValue {
  citationMaps?: Record<string, Record<number, SearchResultItem>>
}

const CitationContext = createContext<CitationContextValue | undefined>(
  undefined
)

export function CitationProvider({
  children,
  citationMaps
}: {
  children: ReactNode
  citationMaps?: Record<string, Record<number, SearchResultItem>>
}) {
  return (
    <CitationContext.Provider value={{ citationMaps }}>
      {children}
    </CitationContext.Provider>
  )
}

export function useCitation() {
  const context = useContext(CitationContext)
  return context || { citationMaps: undefined }
}


================================================
FILE: components/citation-link.tsx
================================================
'use client'

import { memo, useState } from 'react'
import Link from 'next/link'

import type { SearchResultItem } from '@/lib/types'
import { cn } from '@/lib/utils'

import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
import {
  Popover,
  PopoverContent,
  PopoverTrigger
} from '@/components/ui/popover'

interface CitationLinkProps {
  href: string
  children: React.ReactNode
  className?: string
  citationData?: SearchResultItem
}

// Helper function to safely extract hostname from URL
const getHostname = (url: string): string => {
  try {
    return new URL(url).hostname
  } catch {
    return 'unknown'
  }
}

export const CitationLink = memo(function CitationLink({
  href,
  children,
  className,
  citationData
}: CitationLinkProps) {
  const [open, setOpen] = useState(false)
  const childrenText = children?.toString() || ''
  // Match domain names (alphanumeric and hyphens) or numbers for backward compatibility
  const isCitation = /^[\w-]+$/.test(childrenText)

  const linkClasses = cn(
    isCitation
      ? 'text-[10px] bg-muted/50 text-muted-foreground/60 rounded-full h-4 px-1.5 inline-flex items-center justify-center hover:bg-primary hover:text-primary-foreground duration-200 no-underline -translate-y-0.5 whitespace-nowrap'
      : 'hover:underline inline-flex items-center gap-1.5',
    className
  )

  // If no citation data, render as simple link
  if (!citationData) {
    return (
      <a
        href={href}
        target="_blank"
        rel="noopener noreferrer"
        className={linkClasses}
      >
        {children}
      </a>
    )
  }

  // For citations with data, show popover on hover
  if (isCitation && citationData) {
    return (
      <Popover open={open} onOpenChange={setOpen}>
        <PopoverTrigger asChild>
          <a
            href={href}
            target="_blank"
            rel="noopener noreferrer"
            className={linkClasses}
            onMouseEnter={() => setOpen(true)}
            onMouseLeave={() => setOpen(false)}
          >
            {children}
          </a>
        </PopoverTrigger>
        <PopoverContent
          className="w-80 p-0 z-50 shadow-xs"
          side="bottom"
          align="start"
          sideOffset={4}
          onPointerDownOutside={e => e.preventDefault()}
        >
          {citationData ? (
            <Link
              href={citationData.url}
              target="_blank"
              rel="noopener noreferrer"
              className="block p-3 hover:bg-accent/50 transition-colors"
            >
              <div className="space-y-2">
                <div className="flex items-center gap-2">
                  <Avatar className="h-4 w-4 shrink-0">
                    <AvatarImage
                      src={`https://www.google.com/s2/favicons?domain=${getHostname(
                        citationData.url
                      )}`}
                      alt={getHostname(citationData.url)}
                    />
                    <AvatarFallback className="text-xs">
                      {getHostname(citationData.url)[0]?.toUpperCase() || '?'}
                    </AvatarFallback>
                  </Avatar>
                  <span className="text-xs text-muted-foreground truncate">
                    {getHostname(citationData.url)}
                  </span>
                </div>
                <p className="text-sm font-medium line-clamp-1">
                  {citationData.title}
                </p>
                <p className="text-xs text-muted-foreground line-clamp-2 leading-relaxed">
                  {citationData.content}
                </p>
              </div>
            </Link>
          ) : null}
        </PopoverContent>
      </Popover>
    )
  }

  // For non-numbered citations, render as regular link
  return (
    <a
      href={href}
      target="_blank"
      rel="noopener noreferrer"
      className={linkClasses}
    >
      {children}
    </a>
  )
})


================================================
FILE: components/collapsible-message.tsx
================================================
import { ChevronDown } from 'lucide-react'

import { cn } from '@/lib/utils'

import {
  Collapsible,
  CollapsibleContent,
  CollapsibleTrigger
} from './ui/collapsible'
import { IconLogo } from './ui/icons'
import { Separator } from './ui/separator'
import { CurrentUserAvatar } from './current-user-avatar'

interface CollapsibleMessageProps {
  children: React.ReactNode
  role: 'user' | 'assistant'
  isCollapsible?: boolean
  isOpen?: boolean
  header?: React.ReactNode
  onOpenChange?: (open: boolean) => void
  showBorder?: boolean
  showIcon?: boolean
  variant?: 'default' | 'minimal' | 'process' | 'process-sub'
  showSeparator?: boolean
  renderLeft?: React.ReactNode
  chevronSize?: 'sm' | 'md'
  headerClickBehavior?: 'toggle' | 'inspect' | 'split'
}

export function CollapsibleMessage({
  children,
  role,
  isCollapsible = false,
  isOpen = true,
  header,
  onOpenChange,
  showBorder = true,
  showIcon = true,
  variant = 'default',
  showSeparator = true,
  renderLeft,
  chevronSize = 'md',
  headerClickBehavior = 'toggle'
}: CollapsibleMessageProps) {
  const content = children

  return (
    <div className="flex">
      {renderLeft ? (
        renderLeft
      ) : showIcon ? (
        <div className="relative flex flex-col items-center">
          <div className="w-5">
            {role === 'assistant' ? (
              <IconLogo className="size-5" />
            ) : (
              <CurrentUserAvatar />
            )}
          </div>
        </div>
      ) : null}

      {isCollapsible ? (
        <div
          className={cn(
            'flex-1 overflow-hidden min-w-0',
            variant === 'default' && showBorder && 'rounded-lg border bg-card',
            variant === 'default' && !showBorder && 'rounded-lg bg-card',
            variant === 'process' && 'rounded-lg border bg-card',
            variant === 'process-sub' && 'rounded-md border bg-card/50',
            isOpen && !showBorder && 'bg-background'
          )}
        >
          <Collapsible
            open={isOpen}
            onOpenChange={onOpenChange}
            className="w-full"
          >
            <div
              className={cn(
                'flex items-center w-full gap-2 overflow-hidden',
                variant === 'default' && 'justify-between px-3 py-2',
                variant === 'minimal' && 'py-1',
                variant === 'process' && 'justify-between px-1.5 py-1',
                variant === 'process-sub' && 'justify-between px-1 py-0.5'
              )}
            >
              {header && (
                <div
                  className={cn(
                    'overflow-hidden min-w-0',
                    variant === 'default' && 'text-sm flex-1',
                    variant === 'minimal' && 'text-sm flex items-center gap-1',
                    (variant === 'process' || variant === 'process-sub') &&
                      'text-xs flex-1'
                  )}
                  onClick={
                    headerClickBehavior === 'inspect'
                      ? undefined
                      : headerClickBehavior === 'split'
                        ? undefined
                        : onOpenChange
                          ? () => onOpenChange(!isOpen)
                          : undefined
                  }
                >
                  {header}
                </div>
              )}
              {(variant === 'minimal' ||
                variant === 'process' ||
                variant === 'process-sub') && (
                <CollapsibleTrigger asChild>
                  <button
                    type="button"
                    className={cn(
                      'rounded-md group cursor-pointer hover:bg-accent/50',
                      variant === 'minimal' && 'p-0.5',
                      (variant === 'process' || variant === 'process-sub') &&
                        'p-0.5'
                    )}
                    aria-label={isOpen ? 'Collapse' : 'Expand'}
                  >
                    <ChevronDown
                      className={cn(
                        'text-muted-foreground transition-transform duration-200 group-data-[state=open]:rotate-180',
                        chevronSize === 'sm' ? 'h-3 w-3' : 'h-4 w-4'
                      )}
                    />
                  </button>
                </CollapsibleTrigger>
              )}
              {variant === 'default' && (
                <CollapsibleTrigger asChild>
                  <button
                    type="button"
  
Download .txt
gitextract_mowel1np/

├── .eslintrc.json
├── .github/
│   ├── FUNDING.yml
│   ├── ISSUE_TEMPLATE/
│   │   ├── bug_report.yml
│   │   └── feature_request.yml
│   └── workflows/
│       ├── ci.yml
│       ├── docker-build.yml
│       └── release.yml
├── .gitignore
├── .mcp.json
├── .prettierignore
├── .vscode/
│   └── settings.json
├── AGENTS.md
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── Dockerfile
├── LICENSE
├── README.md
├── app/
│   ├── api/
│   │   ├── advanced-search/
│   │   │   └── route.ts
│   │   ├── chat/
│   │   │   └── route.ts
│   │   ├── chats/
│   │   │   └── route.ts
│   │   ├── feedback/
│   │   │   ├── __tests__/
│   │   │   │   └── route.test.ts
│   │   │   └── route.ts
│   │   └── upload/
│   │       └── route.ts
│   ├── auth/
│   │   ├── confirm/
│   │   │   └── route.ts
│   │   ├── error/
│   │   │   └── page.tsx
│   │   ├── forgot-password/
│   │   │   └── page.tsx
│   │   ├── login/
│   │   │   └── page.tsx
│   │   ├── oauth/
│   │   │   └── route.ts
│   │   ├── sign-up/
│   │   │   └── page.tsx
│   │   ├── sign-up-success/
│   │   │   └── page.tsx
│   │   └── update-password/
│   │       └── page.tsx
│   ├── globals.css
│   ├── layout.tsx
│   ├── page.tsx
│   └── search/
│       ├── [id]/
│       │   └── page.tsx
│       ├── loading.tsx
│       └── page.tsx
├── components/
│   ├── __tests__/
│   │   └── research-process-section.test.tsx
│   ├── action-buttons.tsx
│   ├── answer-section.tsx
│   ├── app-sidebar.tsx
│   ├── artifact/
│   │   ├── artifact-content.tsx
│   │   ├── artifact-context.tsx
│   │   ├── artifact-root.tsx
│   │   ├── chat-artifact-container.tsx
│   │   ├── reasoning-content.tsx
│   │   ├── search-artifact-content.tsx
│   │   ├── todo-invocation-content.tsx
│   │   └── tool-invocation-content.tsx
│   ├── attachment-preview.tsx
│   ├── auth-modal.tsx
│   ├── chat-error.tsx
│   ├── chat-messages.tsx
│   ├── chat-panel.tsx
│   ├── chat-share.tsx
│   ├── chat.tsx
│   ├── citation-context.tsx
│   ├── citation-link.tsx
│   ├── collapsible-message.tsx
│   ├── current-user-avatar.tsx
│   ├── custom-link.tsx
│   ├── data-section.tsx
│   ├── default-skeleton.tsx
│   ├── drag-overlay.tsx
│   ├── dynamic-tool-display.tsx
│   ├── error-modal.tsx
│   ├── external-link-items.tsx
│   ├── feedback-modal.tsx
│   ├── fetch-section.tsx
│   ├── file-upload-button.tsx
│   ├── forgot-password-form.tsx
│   ├── guest-menu.tsx
│   ├── header.tsx
│   ├── inspector/
│   │   ├── inspector-drawer.tsx
│   │   └── inspector-panel.tsx
│   ├── login-form.tsx
│   ├── message-actions.tsx
│   ├── message.tsx
│   ├── model-type-selector.tsx
│   ├── process-header.tsx
│   ├── process-rail.tsx
│   ├── question-confirmation.tsx
│   ├── reasoning-section.tsx
│   ├── related-questions.tsx
│   ├── render-message.tsx
│   ├── research-process-section.tsx
│   ├── retry-button.tsx
│   ├── search-mode-selector.tsx
│   ├── search-results-image.tsx
│   ├── search-results.tsx
│   ├── search-section.tsx
│   ├── section.tsx
│   ├── sidebar/
│   │   ├── chat-history-client.tsx
│   │   ├── chat-history-section.tsx
│   │   ├── chat-history-skeleton.tsx
│   │   ├── chat-menu-item.tsx
│   │   └── clear-history-action.tsx
│   ├── sign-up-form.tsx
│   ├── source-favicons.tsx
│   ├── theme-menu-items.tsx
│   ├── theme-provider.tsx
│   ├── todo-list-content.tsx
│   ├── tool-badge.tsx
│   ├── tool-section.tsx
│   ├── tool-todo-display.tsx
│   ├── ui/
│   │   ├── alert-dialog.tsx
│   │   ├── animated-logo.tsx
│   │   ├── avatar.tsx
│   │   ├── badge.tsx
│   │   ├── button.tsx
│   │   ├── card.tsx
│   │   ├── carousel.tsx
│   │   ├── checkbox.tsx
│   │   ├── collapsible.tsx
│   │   ├── command.tsx
│   │   ├── dialog.tsx
│   │   ├── drawer.tsx
│   │   ├── dropdown-menu.tsx
│   │   ├── hover-card.tsx
│   │   ├── icons.tsx
│   │   ├── index.ts
│   │   ├── input.tsx
│   │   ├── label.tsx
│   │   ├── password-input.tsx
│   │   ├── popover.tsx
│   │   ├── select.tsx
│   │   ├── separator.tsx
│   │   ├── sheet.tsx
│   │   ├── sidebar.tsx
│   │   ├── skeleton.tsx
│   │   ├── slider.tsx
│   │   ├── sonner.tsx
│   │   ├── spinner.tsx
│   │   ├── status-indicator.tsx
│   │   ├── switch.tsx
│   │   ├── textarea.tsx
│   │   ├── toggle.tsx
│   │   ├── tooltip-button.tsx
│   │   └── tooltip.tsx
│   ├── update-password-form.tsx
│   ├── uploaded-file-list.tsx
│   ├── user-file-section.tsx
│   ├── user-menu.tsx
│   ├── user-text-section.tsx
│   ├── video-carousel-dialog.tsx
│   ├── video-result-grid.tsx
│   └── video-search-results.tsx
├── components.json
├── config/
│   └── models/
│       ├── cloud.json
│       └── default.json
├── docker-compose.yaml
├── docs/
│   ├── CONFIGURATION.md
│   └── DOCKER.md
├── drizzle/
│   ├── 0000_black_lifeguard.sql
│   ├── 0001_thin_supreme_intelligence.sql
│   ├── 0002_material_crystal.sql
│   ├── 0003_heavy_whirlwind.sql
│   ├── 0004_natural_wallow.sql
│   ├── 0005_awesome_riptide.sql
│   ├── 0006_brainy_wrecking_crew.sql
│   ├── 0007_illegal_mephistopheles.sql
│   ├── 0008_glamorous_riptide.sql
│   ├── 0009_thankful_may_parker.sql
│   ├── 0010_lonely_kang.sql
│   ├── meta/
│   │   ├── 0000_snapshot.json
│   │   ├── 0001_snapshot.json
│   │   ├── 0002_snapshot.json
│   │   ├── 0003_snapshot.json
│   │   ├── 0004_snapshot.json
│   │   ├── 0005_snapshot.json
│   │   ├── 0006_snapshot.json
│   │   ├── 0007_snapshot.json
│   │   ├── 0008_snapshot.json
│   │   ├── 0009_snapshot.json
│   │   ├── 0010_snapshot.json
│   │   └── _journal.json
│   ├── relations.ts
│   └── schema.ts
├── drizzle.config.ts
├── hooks/
│   ├── use-auth-check.tsx
│   ├── use-current-user-image.ts
│   ├── use-current-user-name.ts
│   ├── use-file-dropzone.ts
│   └── use-mobile.tsx
├── instrumentation.ts
├── lib/
│   ├── actions/
│   │   ├── __tests__/
│   │   │   ├── chat.test.ts
│   │   │   └── feedback.test.ts
│   │   ├── chat.ts
│   │   ├── feedback.ts
│   │   └── site-feedback.ts
│   ├── agents/
│   │   ├── generate-related-questions.ts
│   │   ├── prompts/
│   │   │   ├── related-questions-prompt.ts
│   │   │   └── search-mode-prompts.ts
│   │   ├── researcher.ts
│   │   └── title-generator.ts
│   ├── analytics/
│   │   ├── index.ts
│   │   ├── track-chat-event.ts
│   │   ├── types.ts
│   │   └── utils.ts
│   ├── auth/
│   │   └── get-current-user.ts
│   ├── config/
│   │   ├── load-models-config.ts
│   │   ├── model-types.ts
│   │   ├── ollama-validator.ts
│   │   └── search-modes.ts
│   ├── constants/
│   │   └── index.ts
│   ├── contexts/
│   │   └── user-context.tsx
│   ├── db/
│   │   ├── __tests__/
│   │   │   ├── rls-policies.integration.test.ts
│   │   │   └── with-rls.test.ts
│   │   ├── actions.ts
│   │   ├── index.ts
│   │   ├── migrate.ts
│   │   ├── relations.ts
│   │   ├── schema.ts
│   │   └── with-rls.ts
│   ├── firecrawl/
│   │   ├── client.ts
│   │   ├── index.ts
│   │   └── types.ts
│   ├── hooks/
│   │   ├── use-copy-to-clipboard.ts
│   │   └── use-media-query.ts
│   ├── ollama/
│   │   ├── client.ts
│   │   └── types.ts
│   ├── rate-limit/
│   │   ├── __tests__/
│   │   │   └── guest-limit.test.ts
│   │   ├── chat-limits.ts
│   │   └── guest-limit.ts
│   ├── schema/
│   │   ├── fetch.tsx
│   │   ├── question.ts
│   │   ├── related.tsx
│   │   └── search.tsx
│   ├── storage/
│   │   └── r2-client.ts
│   ├── streaming/
│   │   ├── __tests__/
│   │   │   ├── create-ephemeral-chat-stream-response.test.ts
│   │   │   └── prune-messages-integration.test.ts
│   │   ├── create-chat-stream-response.ts
│   │   ├── create-ephemeral-chat-stream-response.ts
│   │   ├── helpers/
│   │   │   ├── __tests__/
│   │   │   │   └── prepare-messages.test.ts
│   │   │   ├── persist-stream-results.ts
│   │   │   ├── prepare-messages.ts
│   │   │   ├── stream-related-questions.ts
│   │   │   ├── strip-reasoning-parts.ts
│   │   │   └── types.ts
│   │   └── types.ts
│   ├── supabase/
│   │   ├── client.ts
│   │   ├── middleware.ts
│   │   └── server.ts
│   ├── tools/
│   │   ├── dynamic.ts
│   │   ├── fetch.ts
│   │   ├── question.ts
│   │   ├── search/
│   │   │   └── providers/
│   │   │       ├── base.ts
│   │   │       ├── brave.ts
│   │   │       ├── exa.ts
│   │   │       ├── firecrawl.ts
│   │   │       ├── index.ts
│   │   │       ├── searxng.ts
│   │   │       └── tavily.ts
│   │   ├── search.ts
│   │   └── todo.ts
│   ├── types/
│   │   ├── agent.ts
│   │   ├── ai.ts
│   │   ├── dynamic-tools.ts
│   │   ├── index.ts
│   │   ├── message-persistence.ts
│   │   ├── model-type.ts
│   │   ├── models.ts
│   │   └── search.ts
│   └── utils/
│       ├── __tests__/
│       │   ├── citation.test.ts
│       │   ├── context-window.test.ts
│       │   ├── domain.test.ts
│       │   └── model-selection.test.ts
│       ├── citation.ts
│       ├── context-window.ts
│       ├── cookies.ts
│       ├── domain.ts
│       ├── index.ts
│       ├── message-mapping.ts
│       ├── message-utils.ts
│       ├── model-selection.ts
│       ├── perf-logging.ts
│       ├── perf-tracking.ts
│       ├── registry.ts
│       ├── retry.ts
│       ├── search-config.ts
│       ├── telemetry.ts
│       └── url.ts
├── next.config.mjs
├── package.json
├── postcss.config.mjs
├── prettier.config.js
├── proxy.ts
├── scripts/
│   ├── README.md
│   ├── chat-cli.ts
│   └── test-cache-performance.ts
├── searxng-limiter.toml
├── searxng-settings.yml
├── tsconfig.json
├── vitest.config.mts
└── vitest.setup.ts
Download .txt
SYMBOL INDEX (611 symbols across 200 files)

FILE: app/api/advanced-search/route.ts
  constant SEARXNG_MAX_RESULTS (line 22) | const SEARXNG_MAX_RESULTS = Math.max(
  constant CACHE_TTL (line 27) | const CACHE_TTL = 3600 // Cache time-to-live in seconds (1 hour)
  constant CACHE_EXPIRATION_CHECK_INTERVAL (line 28) | const CACHE_EXPIRATION_CHECK_INTERVAL = 3600000 // 1 hour in milliseconds
  function initializeRedisClient (line 33) | async function initializeRedisClient() {
  function getCachedResults (line 67) | async function getCachedResults(
  function setCachedResults (line 95) | async function setCachedResults(
  function cleanupExpiredCache (line 116) | async function cleanupExpiredCache() {
  function POST (line 137) | async function POST(request: Request) {
  function advancedSearchXNGSearch (line 183) | async function advancedSearchXNGSearch(
  function crawlPage (line 313) | async function crawlPage(
  function highlightQueryTerms (line 409) | function highlightQueryTerms(content: string, query: string): string {
  function calculateRelevanceScore (line 434) | function calculateRelevanceScore(result: SearXNGResult, query: string): ...
  function extractPublicationDate (line 503) | function extractPublicationDate(document: Document): Date | null {
  function fetchJsonWithRetry (line 539) | async function fetchJsonWithRetry(url: string, retries: number): Promise...
  function fetchJson (line 550) | function fetchJson(url: string): Promise<any> {
  function fetchHtmlWithTimeout (line 586) | async function fetchHtmlWithTimeout(
  function fetchHtml (line 602) | function fetchHtml(url: string): Promise<string> {
  function timeout (line 638) | function timeout(ms: number, message: string): Promise<never> {
  function isQualityContent (line 646) | function isQualityContent(text: string): boolean {

FILE: app/api/chat/route.ts
  function POST (line 19) | async function POST(req: Request) {

FILE: app/api/chats/route.ts
  type ChatPageResponse (line 6) | interface ChatPageResponse {
  function GET (line 11) | async function GET(request: NextRequest) {

FILE: app/api/feedback/route.ts
  function POST (line 7) | async function POST(req: Request) {

FILE: app/api/upload/route.ts
  constant MAX_FILE_SIZE (line 12) | const MAX_FILE_SIZE = 5 * 1024 * 1024 // 5MB
  constant ALLOWED_TYPES (line 13) | const ALLOWED_TYPES = ['image/jpeg', 'image/png', 'application/pdf']
  function POST (line 15) | async function POST(req: NextRequest) {
  function sanitizeFilename (line 60) | function sanitizeFilename(filename: string) {
  function uploadFileToR2 (line 64) | async function uploadFileToR2(file: File, userId: string, chatId: string) {

FILE: app/auth/confirm/route.ts
  function GET (line 8) | async function GET(request: NextRequest) {

FILE: app/auth/error/page.tsx
  function Page (line 3) | async function Page({

FILE: app/auth/forgot-password/page.tsx
  function Page (line 3) | function Page() {

FILE: app/auth/login/page.tsx
  function Page (line 3) | function Page() {

FILE: app/auth/oauth/route.ts
  function GET (line 6) | async function GET(request: Request) {

FILE: app/auth/sign-up-success/page.tsx
  function Page (line 9) | function Page() {

FILE: app/auth/sign-up/page.tsx
  function Page (line 3) | function Page() {

FILE: app/auth/update-password/page.tsx
  function Page (line 3) | function Page() {

FILE: app/layout.tsx
  function RootLayout (line 52) | async function RootLayout({

FILE: app/page.tsx
  function Page (line 5) | async function Page() {

FILE: app/search/[id]/page.tsx
  function generateMetadata (line 12) | async function generateMetadata(props: {
  function SearchPage (line 29) | async function SearchPage(props: {

FILE: app/search/loading.tsx
  function Loading (line 5) | function Loading() {

FILE: app/search/page.tsx
  function SearchPage (line 10) | async function SearchPage(props: {

FILE: components/action-buttons.tsx
  constant FOCUS_OUT_DELAY_MS (line 19) | const FOCUS_OUT_DELAY_MS = 100 // Delay to ensure focus has actually moved
  type ActionCategory (line 21) | interface ActionCategory {
  type ActionButtonsProps (line 88) | interface ActionButtonsProps {
  function ActionButtons (line 95) | function ActionButtons({

FILE: components/answer-section.tsx
  type AnswerSectionProps (line 18) | type AnswerSectionProps = {
  function AnswerSection (line 35) | function AnswerSection({

FILE: components/app-sidebar.tsx
  function AppSidebar (line 23) | function AppSidebar() {

FILE: components/artifact/artifact-content.tsx
  function isTodoToolPart (line 10) | function isTodoToolPart(part: Part): part is ToolPart<'todoWrite'> {
  function ArtifactContent (line 14) | function ArtifactContent({ part }: { part: Part | null }) {

FILE: components/artifact/artifact-context.tsx
  constant ANIMATION_DURATION (line 17) | const ANIMATION_DURATION = 300
  type ArtifactState (line 19) | interface ArtifactState {
  type ArtifactAction (line 24) | type ArtifactAction =
  function artifactReducer (line 34) | function artifactReducer(
  type ArtifactContextValue (line 50) | interface ArtifactContextValue {
  function ArtifactProvider (line 60) | function ArtifactProvider({ children }: { children: ReactNode }) {
  function useArtifact (line 91) | function useArtifact() {

FILE: components/artifact/artifact-root.tsx
  function ArtifactRoot (line 8) | function ArtifactRoot({ children }: { children: ReactNode }) {

FILE: components/artifact/chat-artifact-container.tsx
  constant DEFAULT_WIDTH (line 15) | const DEFAULT_WIDTH = 500
  constant MIN_WIDTH (line 16) | const MIN_WIDTH = 320
  constant MAX_WIDTH (line 17) | const MAX_WIDTH = 800
  constant CHAT_MIN_WIDTH (line 18) | const CHAT_MIN_WIDTH = 360
  constant RESIZE_OVERLAY_Z_INDEX (line 19) | const RESIZE_OVERLAY_Z_INDEX = 9999
  function getAllowedWidthBounds (line 22) | function getAllowedWidthBounds(containerWidth: number): {
  function ChatArtifactContainer (line 39) | function ChatArtifactContainer({

FILE: components/artifact/reasoning-content.tsx
  function ReasoningContent (line 8) | function ReasoningContent({ reasoning }: { reasoning: string }) {

FILE: components/artifact/search-artifact-content.tsx
  function SearchArtifactContent (line 14) | function SearchArtifactContent({ tool }: { tool: ToolPart<'search'> }) {

FILE: components/artifact/todo-invocation-content.tsx
  type TodoInvocationContentProps (line 7) | interface TodoInvocationContentProps {
  function TodoInvocationContent (line 11) | function TodoInvocationContent({ part }: TodoInvocationContentProps) {

FILE: components/artifact/tool-invocation-content.tsx
  function ToolInvocationContent (line 7) | function ToolInvocationContent({ part }: { part: ToolPart }) {

FILE: components/attachment-preview.tsx
  type Attachment (line 5) | interface Attachment {
  type AttachmentPreviewProps (line 11) | interface AttachmentPreviewProps {

FILE: components/auth-modal.tsx
  type AuthModalProps (line 15) | interface AuthModalProps {
  function AuthModal (line 20) | function AuthModal({ open, onOpenChange }: AuthModalProps) {

FILE: components/chat-error.tsx
  type ChatErrorProps (line 5) | interface ChatErrorProps {
  function ChatError (line 9) | function ChatError({ error }: ChatErrorProps) {

FILE: components/chat-messages.tsx
  type ChatSection (line 17) | interface ChatSection {
  type ChatMessagesProps (line 23) | interface ChatMessagesProps {
  function ChatMessages (line 37) | function ChatMessages({

FILE: components/chat-panel.tsx
  constant INPUT_UPDATE_DELAY_MS (line 25) | const INPUT_UPDATE_DELAY_MS = 10 // Delay to ensure input value is updat...
  type ChatPanelProps (line 27) | interface ChatPanelProps {
  function ChatPanel (line 50) | function ChatPanel({

FILE: components/chat-share.tsx
  type ChatShareProps (line 24) | interface ChatShareProps {
  function ChatShare (line 29) | function ChatShare({ chatId, className }: ChatShareProps) {

FILE: components/chat.tsx
  type ChatSection (line 28) | interface ChatSection {
  function Chat (line 34) | function Chat({

FILE: components/citation-context.tsx
  type CitationContextValue (line 7) | interface CitationContextValue {
  function CitationProvider (line 15) | function CitationProvider({
  function useCitation (line 29) | function useCitation() {

FILE: components/citation-link.tsx
  type CitationLinkProps (line 16) | interface CitationLinkProps {

FILE: components/collapsible-message.tsx
  type CollapsibleMessageProps (line 14) | interface CollapsibleMessageProps {
  function CollapsibleMessage (line 30) | function CollapsibleMessage({

FILE: components/custom-link.tsx
  type CustomLinkProps (line 9) | type CustomLinkProps = Omit<
  function Citing (line 14) | function Citing({

FILE: components/data-section.tsx
  type DataSectionProps (line 9) | interface DataSectionProps {
  function DataSection (line 14) | function DataSection({ part, onQuerySelect }: DataSectionProps) {

FILE: components/default-skeleton.tsx
  function SearchSkeleton (line 15) | function SearchSkeleton() {

FILE: components/drag-overlay.tsx
  function DragOverlay (line 6) | function DragOverlay({ visible }: { visible: boolean }) {

FILE: components/dynamic-tool-display.tsx
  type DynamicToolPart (line 6) | type DynamicToolPart =
  type DynamicToolDisplayProps (line 38) | interface DynamicToolDisplayProps {
  function DynamicToolDisplay (line 42) | function DynamicToolDisplay({ part }: DynamicToolDisplayProps) {

FILE: components/error-modal.tsx
  type ErrorModalProps (line 17) | interface ErrorModalProps {
  function ErrorModal (line 29) | function ErrorModal({

FILE: components/external-link-items.tsx
  function ExternalLinkItems (line 26) | function ExternalLinkItems() {

FILE: components/feedback-modal.tsx
  type Sentiment (line 21) | type Sentiment = 'positive' | 'neutral' | 'negative'
  type FeedbackModalProps (line 23) | interface FeedbackModalProps {
  function FeedbackModal (line 28) | function FeedbackModal({ open, onOpenChange }: FeedbackModalProps) {

FILE: components/fetch-section.tsx
  type FetchSectionProps (line 12) | interface FetchSectionProps {
  function FetchSection (line 22) | function FetchSection({

FILE: components/file-upload-button.tsx
  function FileUploadButton (line 22) | function FileUploadButton({

FILE: components/forgot-password-form.tsx
  function ForgotPasswordForm (line 20) | function ForgotPasswordForm({

FILE: components/guest-menu.tsx
  function GuestMenu (line 27) | function GuestMenu() {

FILE: components/header.tsx
  type HeaderProps (line 19) | interface HeaderProps {

FILE: components/inspector/inspector-drawer.tsx
  function InspectorDrawer (line 13) | function InspectorDrawer() {

FILE: components/inspector/inspector-panel.tsx
  function InspectorPanel (line 18) | function InspectorPanel() {

FILE: components/login-form.tsx
  function LoginForm (line 24) | function LoginForm({

FILE: components/message-actions.tsx
  type MessageActionsProps (line 18) | interface MessageActionsProps {
  function MessageActions (line 32) | function MessageActions({

FILE: components/message.tsx
  function MarkdownMessage (line 18) | function MarkdownMessage({

FILE: components/model-type-selector.tsx
  constant MODEL_TYPE_OPTIONS (line 18) | const MODEL_TYPE_OPTIONS: { value: ModelType; label: string }[] = [
  function ModelTypeSelector (line 23) | function ModelTypeSelector({

FILE: components/process-header.tsx
  type ProcessHeaderProps (line 7) | type ProcessHeaderProps = {
  function ProcessHeader (line 16) | function ProcessHeader({

FILE: components/process-rail.tsx
  type ProcessRailProps (line 3) | interface ProcessRailProps {
  function ProcessRail (line 9) | function ProcessRail({

FILE: components/question-confirmation.tsx
  type QuestionConfirmationProps (line 14) | interface QuestionConfirmationProps {
  type QuestionOption (line 20) | interface QuestionOption {
  type QuestionInput (line 25) | interface QuestionInput {
  type QuestionOutput (line 33) | interface QuestionOutput {
  function QuestionConfirmation (line 39) | function QuestionConfirmation({

FILE: components/reasoning-section.tsx
  type ReasoningContent (line 16) | interface ReasoningContent {
  type ReasoningSectionProps (line 21) | interface ReasoningSectionProps {
  function ReasoningSection (line 32) | function ReasoningSection({

FILE: components/related-questions.tsx
  type RelatedQuestionsProps (line 14) | interface RelatedQuestionsProps {

FILE: components/render-message.tsx
  type RenderMessageProps (line 18) | interface RenderMessageProps {
  function RenderMessage (line 34) | function RenderMessage({

FILE: components/research-process-section.tsx
  type TextPart (line 29) | type TextPart = {
  type DataPart (line 34) | type DataPart = UIDataPart
  type MessagePart (line 36) | type MessagePart =
  function isReasoningPart (line 44) | function isReasoningPart(part: MessagePart): part is ReasoningPart {
  function isToolPart (line 48) | function isToolPart(part: MessagePart): part is ToolPart {
  function isTextPart (line 54) | function isTextPart(part: MessagePart): part is TextPart {
  function isDataPart (line 58) | function isDataPart(part: MessagePart): part is DataPart {
  type Props (line 62) | type Props = {
  function splitByText (line 80) | function splitByText(parts: MessagePart[]): MessagePart[][] {
  function groupConsecutiveParts (line 110) | function groupConsecutiveParts(segment: MessagePart[]): MessagePart[][] {
  function useAccordionState (line 148) | function useAccordionState(onOpenChange: (id: string, open: boolean) => ...
  function RenderPart (line 176) | function RenderPart({
  function useHasSubsequentContent (line 264) | function useHasSubsequentContent(
  function ResearchProcessSection (line 293) | function ResearchProcessSection({

FILE: components/retry-button.tsx
  type RetryButtonProps (line 7) | interface RetryButtonProps {

FILE: components/search-mode-selector.tsx
  function SearchModeSelector (line 21) | function SearchModeSelector() {

FILE: components/search-results-image.tsx
  type SearchResultsImageSectionProps (line 33) | interface SearchResultsImageSectionProps {
  type NormalizedImage (line 39) | type NormalizedImage = { id: string; url: string; description: string }
  type FilterStatus (line 41) | type FilterStatus = 'loading' | 'ready' | 'empty'
  type FilteredImagesState (line 43) | interface FilteredImagesState {

FILE: components/search-results.tsx
  type SearchResultsProps (line 13) | interface SearchResultsProps {
  function SearchResults (line 18) | function SearchResults({

FILE: components/search-section.tsx
  type SearchSectionProps (line 25) | interface SearchSectionProps {
  function SearchSection (line 35) | function SearchSection({

FILE: components/section.tsx
  type SectionProps (line 24) | type SectionProps = {
  function ToolArgsSection (line 100) | function ToolArgsSection({

FILE: components/sidebar/chat-history-client.tsx
  type ChatPageResponse (line 19) | interface ChatPageResponse {
  function ChatHistoryClient (line 24) | function ChatHistoryClient() {

FILE: components/sidebar/chat-history-section.tsx
  function ChatHistorySection (line 3) | async function ChatHistorySection() {

FILE: components/sidebar/chat-history-skeleton.tsx
  function ChatHistorySkeleton (line 7) | function ChatHistorySkeleton() {

FILE: components/sidebar/chat-menu-item.tsx
  type ChatMenuItemProps (line 38) | interface ChatMenuItemProps {
  function ChatMenuItem (line 80) | function ChatMenuItem({ chat }: ChatMenuItemProps) {

FILE: components/sidebar/clear-history-action.tsx
  type ClearHistoryActionProps (line 31) | interface ClearHistoryActionProps {
  function ClearHistoryAction (line 35) | function ClearHistoryAction({ empty }: ClearHistoryActionProps) {

FILE: components/sign-up-form.tsx
  function SignUpForm (line 22) | function SignUpForm({

FILE: components/source-favicons.tsx
  type SourceFaviconsProps (line 6) | interface SourceFaviconsProps {
  function SourceFavicons (line 15) | function SourceFavicons({

FILE: components/theme-menu-items.tsx
  function ThemeMenuItems (line 9) | function ThemeMenuItems() {

FILE: components/theme-provider.tsx
  function ThemeProvider (line 7) | function ThemeProvider({ children, ...props }: ThemeProviderProps) {

FILE: components/todo-list-content.tsx
  type TodoListContentProps (line 8) | type TodoListContentProps = {
  function TodoListContent (line 20) | function TodoListContent({

FILE: components/tool-badge.tsx
  type ToolBadgeProps (line 9) | type ToolBadgeProps = {

FILE: components/tool-section.tsx
  type ToolSectionProps (line 12) | interface ToolSectionProps {
  function ToolSection (line 24) | function ToolSection({

FILE: components/tool-todo-display.tsx
  type ToolTodoDisplayProps (line 11) | interface ToolTodoDisplayProps {
  function ToolTodoDisplay (line 36) | function ToolTodoDisplay({

FILE: components/ui/animated-logo.tsx
  function AnimatedLogo (line 5) | function AnimatedLogo({

FILE: components/ui/badge.tsx
  type BadgeProps (line 27) | interface BadgeProps
  function Badge (line 31) | function Badge({ className, variant, ...props }: BadgeProps) {

FILE: components/ui/button.tsx
  type ButtonProps (line 37) | interface ButtonProps

FILE: components/ui/carousel.tsx
  type CarouselApi (line 14) | type CarouselApi = UseEmblaCarouselType[1]
  type UseCarouselParameters (line 15) | type UseCarouselParameters = Parameters<typeof useEmblaCarousel>
  type CarouselOptions (line 16) | type CarouselOptions = UseCarouselParameters[0]
  type CarouselPlugin (line 17) | type CarouselPlugin = UseCarouselParameters[1]
  type CarouselProps (line 19) | type CarouselProps = {
  type CarouselContextProps (line 26) | type CarouselContextProps = {
  function useCarousel (line 37) | function useCarousel() {

FILE: components/ui/dropdown-menu.tsx
  function DropdownMenu (line 10) | function DropdownMenu({
  function DropdownMenuPortal (line 16) | function DropdownMenuPortal({
  function DropdownMenuTrigger (line 24) | function DropdownMenuTrigger({
  function DropdownMenuContent (line 35) | function DropdownMenuContent({
  function DropdownMenuGroup (line 55) | function DropdownMenuGroup({
  function DropdownMenuItem (line 63) | function DropdownMenuItem({
  function DropdownMenuCheckboxItem (line 86) | function DropdownMenuCheckboxItem({
  function DropdownMenuRadioGroup (line 112) | function DropdownMenuRadioGroup({
  function DropdownMenuRadioItem (line 123) | function DropdownMenuRadioItem({
  function DropdownMenuLabel (line 147) | function DropdownMenuLabel({
  function DropdownMenuSeparator (line 167) | function DropdownMenuSeparator({
  function DropdownMenuShortcut (line 180) | function DropdownMenuShortcut({
  function DropdownMenuSub (line 196) | function DropdownMenuSub({
  function DropdownMenuSubTrigger (line 202) | function DropdownMenuSubTrigger({
  function DropdownMenuSubContent (line 226) | function DropdownMenuSubContent({

FILE: components/ui/icons.tsx
  function IconLogo (line 7) | function IconLogo({ className, ...props }: React.ComponentProps<'svg'>) {
  function IconLogoOutline (line 24) | function IconLogoOutline({ className, ...props }: React.ComponentProps<'...
  function IconBlinkingLogo (line 48) | function IconBlinkingLogo({

FILE: components/ui/input.tsx
  type InputProps (line 5) | interface InputProps

FILE: components/ui/sheet.tsx
  type SheetContentProps (line 53) | interface SheetContentProps

FILE: components/ui/sidebar.tsx
  constant SIDEBAR_COOKIE_NAME (line 31) | const SIDEBAR_COOKIE_NAME = 'sidebar_state'
  constant SIDEBAR_COOKIE_MAX_AGE (line 32) | const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7
  constant SIDEBAR_WIDTH (line 33) | const SIDEBAR_WIDTH = '16rem'
  constant SIDEBAR_WIDTH_MOBILE (line 34) | const SIDEBAR_WIDTH_MOBILE = '18rem'
  constant SIDEBAR_WIDTH_ICON (line 35) | const SIDEBAR_WIDTH_ICON = '3rem'
  constant SIDEBAR_KEYBOARD_SHORTCUT (line 36) | const SIDEBAR_KEYBOARD_SHORTCUT = 'b'
  type SidebarContextProps (line 38) | type SidebarContextProps = {
  function useSidebar (line 50) | function useSidebar() {
  function getCookie (line 60) | function getCookie(name: string): string | null {

FILE: components/ui/skeleton.tsx
  function Skeleton (line 3) | function Skeleton({

FILE: components/ui/sonner.tsx
  type ToasterProps (line 7) | type ToasterProps = React.ComponentProps<typeof Sonner>

FILE: components/ui/spinner.tsx
  type SpinnerProps (line 7) | interface SpinnerProps extends React.SVGProps<SVGSVGElement> {}

FILE: components/ui/status-indicator.tsx
  type StatusIndicatorProps (line 5) | interface StatusIndicatorProps {
  function StatusIndicator (line 11) | function StatusIndicator({

FILE: components/ui/textarea.tsx
  type TextareaProps (line 5) | interface TextareaProps

FILE: components/ui/tooltip-button.tsx
  type TooltipButtonProps (line 12) | interface TooltipButtonProps extends ButtonProps {

FILE: components/update-password-form.tsx
  function UpdatePasswordForm (line 20) | function UpdatePasswordForm({

FILE: components/uploaded-file-list.tsx
  type UploadedFileListProps (line 10) | interface UploadedFileListProps {

FILE: components/user-file-section.tsx
  type UserFileSectionProps (line 5) | interface UserFileSectionProps {

FILE: components/user-menu.tsx
  type UserMenuProps (line 27) | interface UserMenuProps {
  function UserMenu (line 31) | function UserMenu({ user }: UserMenuProps) {

FILE: components/user-text-section.tsx
  type UserTextSectionProps (line 13) | interface UserTextSectionProps {

FILE: components/video-carousel-dialog.tsx
  type VideoCarouselDialogProps (line 24) | interface VideoCarouselDialogProps {
  function VideoCarouselDialog (line 31) | function VideoCarouselDialog({

FILE: components/video-result-grid.tsx
  type VideoResultGridProps (line 14) | interface VideoResultGridProps {
  function VideoResultGrid (line 20) | function VideoResultGrid({

FILE: components/video-search-results.tsx
  type VideoSearchResultsProps (line 8) | interface VideoSearchResultsProps {
  function createVideoSearchResults (line 14) | function createVideoSearchResults(
  function VideoSearchResults (line 29) | function VideoSearchResults({

FILE: drizzle/0000_black_lifeguard.sql
  type "chats" (line 1) | CREATE TABLE "chats" (
  type "messages" (line 9) | CREATE TABLE "messages" (

FILE: drizzle/0003_heavy_whirlwind.sql
  type "feedback" (line 1) | CREATE TABLE "feedback" (
  type "feedback" (line 11) | CREATE INDEX "feedback_user_id_idx" ON "feedback" USING btree ("user_id")
  type "feedback" (line 12) | CREATE INDEX "feedback_created_at_idx" ON "feedback" USING btree ("creat...

FILE: drizzle/0004_natural_wallow.sql
  type "chats" (line 1) | CREATE INDEX "chats_user_id_idx" ON "chats" USING btree ("user_id")
  type "chats" (line 2) | CREATE INDEX "chats_user_id_created_at_idx" ON "chats" USING btree ("use...
  type "chats" (line 3) | CREATE INDEX "chats_created_at_idx" ON "chats" USING btree ("created_at"...

FILE: drizzle/0009_thankful_may_parker.sql
  type "chats" (line 1) | CREATE INDEX "chats_id_user_id_idx" ON "chats" USING btree ("id","user_id")

FILE: hooks/use-auth-check.tsx
  function useAuthCheck (line 9) | function useAuthCheck() {

FILE: hooks/use-file-dropzone.ts
  type UseFileDropzoneProps (line 7) | type UseFileDropzoneProps = {
  function useFileDropzone (line 15) | function useFileDropzone({

FILE: hooks/use-mobile.tsx
  constant MOBILE_BREAKPOINT (line 3) | const MOBILE_BREAKPOINT = 768
  function useIsMobile (line 5) | function useIsMobile() {

FILE: instrumentation.ts
  function register (line 4) | async function register() {

FILE: lib/actions/chat.ts
  constant DEFAULT_CHAT_TITLE (line 14) | const DEFAULT_CHAT_TITLE = 'Untitled'
  function getChats (line 39) | async function getChats() {
  function getChatsPage (line 50) | async function getChatsPage(limit = 20, offset = 0) {
  function loadChat (line 63) | async function loadChat(
  function createChat (line 75) | async function createChat(
  function createChatAndSaveMessage (line 100) | async function createChatAndSaveMessage(
  function createChatWithFirstMessage (line 141) | async function createChatWithFirstMessage(
  function upsertMessage (line 179) | async function upsertMessage(
  function deleteChat (line 205) | async function deleteChat(chatId: string) {
  function clearChats (line 223) | async function clearChats() {
  function deleteMessagesAfter (line 243) | async function deleteMessagesAfter(chatId: string, messageId: string) {
  function shareChat (line 265) | async function shareChat(chatId: string) {
  function deleteMessagesFromIndex (line 287) | async function deleteMessagesFromIndex(
  function saveChatTitle (line 321) | async function saveChatTitle(

FILE: lib/actions/feedback.ts
  function updateMessageFeedback (line 12) | async function updateMessageFeedback(
  function getMessageFeedback (line 77) | async function getMessageFeedback(

FILE: lib/actions/site-feedback.ts
  function submitFeedback (line 8) | async function submitFeedback(data: {

FILE: lib/agents/generate-related-questions.ts
  function createRelatedQuestionsStream (line 10) | function createRelatedQuestionsStream(

FILE: lib/agents/prompts/related-questions-prompt.ts
  constant RELATED_QUESTIONS_PROMPT (line 1) | const RELATED_QUESTIONS_PROMPT = `You are a professional web researcher ...

FILE: lib/agents/prompts/search-mode-prompts.ts
  function getQuickModePrompt (line 8) | function getQuickModePrompt(): string {
  function getApproachStrategy (line 144) | function getApproachStrategy(): string {
  function getAdaptiveModePrompt (line 190) | function getAdaptiveModePrompt(): string {
  constant QUICK_MODE_PROMPT (line 316) | const QUICK_MODE_PROMPT = getQuickModePrompt()

FILE: lib/agents/researcher.ts
  function wrapSearchToolForQuickMode (line 21) | function wrapSearchToolForQuickMode<
  function createResearcher (line 68) | function createResearcher({
  function getResearcherTools (line 159) | function getResearcherTools(

FILE: lib/agents/title-generator.ts
  type GenerateChatTitleParams (line 6) | interface GenerateChatTitleParams {
  function generateChatTitle (line 19) | async function generateChatTitle({

FILE: lib/analytics/track-chat-event.ts
  function trackChatEvent (line 31) | async function trackChatEvent(data: ChatEventData): Promise<void> {

FILE: lib/analytics/types.ts
  type ChatEventData (line 12) | interface ChatEventData {
  type AnalyticsProvider (line 36) | interface AnalyticsProvider {

FILE: lib/analytics/utils.ts
  function calculateConversationTurn (line 22) | function calculateConversationTurn(messages: UIMessage[]): number {

FILE: lib/auth/get-current-user.ts
  function getCurrentUser (line 5) | async function getCurrentUser() {
  function getCurrentUserId (line 18) | async function getCurrentUserId() {

FILE: lib/config/load-models-config.ts
  type ModelsConfig (line 8) | interface ModelsConfig {
  constant VALID_MODEL_TYPES (line 19) | const VALID_MODEL_TYPES: ModelType[] = ['speed', 'quality']
  constant VALID_SEARCH_MODES (line 20) | const VALID_SEARCH_MODES: SearchMode[] = ['quick', 'adaptive']
  function validateModelsConfigStructure (line 22) | function validateModelsConfigStructure(
  function loadModelsConfig (line 62) | async function loadModelsConfig(): Promise<ModelsConfig> {
  function loadModelsConfigSync (line 79) | function loadModelsConfigSync(): ModelsConfig {
  function getModelsConfig (line 96) | function getModelsConfig(): ModelsConfig {

FILE: lib/config/model-types.ts
  function getModelForModeAndType (line 8) | function getModelForModeAndType(
  function getRelatedQuestionsModel (line 17) | function getRelatedQuestionsModel(): Model {

FILE: lib/config/ollama-validator.ts
  function getConfiguredOllamaModels (line 15) | async function getConfiguredOllamaModels(): Promise<Model[]> {
  function initializeOllamaValidation (line 46) | async function initializeOllamaValidation(): Promise<void> {
  function validateOllamaModel (line 154) | function validateOllamaModel(modelId: string): {
  function getValidatedOllamaModels (line 190) | function getValidatedOllamaModels(): string[] {

FILE: lib/config/search-modes.ts
  type SearchModeConfig (line 7) | interface SearchModeConfig {
  constant SEARCH_MODE_CONFIGS (line 16) | const SEARCH_MODE_CONFIGS: SearchModeConfig[] = [
  function getSearchModeConfig (line 34) | function getSearchModeConfig(

FILE: lib/constants/index.ts
  constant CHAT_ID (line 1) | const CHAT_ID = 'search' as const

FILE: lib/contexts/user-context.tsx
  function UserProvider (line 7) | function UserProvider({
  function useHasUser (line 17) | function useHasUser() {

FILE: lib/db/__tests__/with-rls.test.ts
  function createMockTx (line 17) | function createMockTx(overrides: Partial<TxInstance> = {}): TxInstance {

FILE: lib/db/actions.ts
  function createChat (line 23) | async function createChat({
  function getChat (line 52) | async function getChat(
  function upsertMessage (line 86) | async function upsertMessage(
  function loadChat (line 131) | async function loadChat(
  function loadChatWithMessages (line 155) | async function loadChatWithMessages(
  function deleteMessagesAfter (line 198) | async function deleteMessagesAfter(
  function deleteMessagesFromIndex (line 240) | async function deleteMessagesFromIndex(
  function getChats (line 275) | async function getChats(userId: string): Promise<Chat[]> {
  function getChatsPage (line 288) | async function getChatsPage(
  function deleteChat (line 319) | async function deleteChat(
  function updateChatVisibility (line 350) | async function updateChatVisibility(
  function updateChatTitle (line 374) | async function updateChatTitle(
  function createChatWithFirstMessageTransaction (line 394) | async function createChatWithFirstMessageTransaction({

FILE: lib/db/index.ts
  type Schema (line 65) | type Schema = typeof schema

FILE: lib/db/schema.ts
  constant ID_LENGTH (line 17) | const ID_LENGTH = 191
  constant USER_ID_LENGTH (line 18) | const USER_ID_LENGTH = 255
  constant VARCHAR_LENGTH (line 19) | const VARCHAR_LENGTH = 256
  constant FILENAME_LENGTH (line 20) | const FILENAME_LENGTH = 1024
  type Chat (line 70) | type Chat = InferSelectModel<typeof chats>
  type Message (line 120) | type Message = InferSelectModel<typeof messages>
  type Part (line 258) | type Part = InferSelectModel<typeof parts>
  type NewPart (line 259) | type NewPart = typeof parts.$inferInsert
  type Feedback (line 300) | type Feedback = InferSelectModel<typeof feedback>

FILE: lib/db/with-rls.ts
  type DbInstance (line 6) | type DbInstance = typeof db
  type TxInstance (line 7) | type TxInstance = Parameters<Parameters<typeof db.transaction>[0]>[0]
  class RLSViolationError (line 12) | class RLSViolationError extends Error {
    method constructor (line 13) | constructor(message = 'Row level security policy violation') {
  function withRLS (line 39) | async function withRLS<T>(
  function withOptionalRLS (line 80) | async function withOptionalRLS<T>(

FILE: lib/firecrawl/client.ts
  class FirecrawlClient (line 8) | class FirecrawlClient {
    method constructor (line 12) | constructor(apiKey: string) {
    method search (line 16) | async search(
    method searchImages (line 40) | async searchImages(
    method getImagesForQuery (line 58) | async getImagesForQuery(
    method getHeaders (line 82) | private getHeaders(): Record<string, string> {
    method handleResponse (line 89) | private async handleResponse<T>(response: Response): Promise<T> {

FILE: lib/firecrawl/types.ts
  type FirecrawlSource (line 1) | type FirecrawlSource = 'web' | 'news' | 'images'
  type FirecrawlSearchOptions (line 3) | type FirecrawlSearchOptions = {
  type FirecrawlImageSearchOptions (line 11) | type FirecrawlImageSearchOptions = {
  type FirecrawlImageResult (line 17) | type FirecrawlImageResult = {
  type FirecrawlWebResult (line 26) | type FirecrawlWebResult = {
  type FirecrawlNewsResult (line 34) | type FirecrawlNewsResult = {
  type FirecrawlSearchResponseData (line 42) | type FirecrawlSearchResponseData = {
  type FirecrawlSearchResponse (line 47) | type FirecrawlSearchResponse = {
  type FirecrawlImageSearchResponseData (line 52) | type FirecrawlImageSearchResponseData = {
  type FirecrawlImageSearchResponse (line 56) | type FirecrawlImageSearchResponse = {

FILE: lib/hooks/use-copy-to-clipboard.ts
  type useCopyToClipboardProps (line 5) | interface useCopyToClipboardProps {
  function useCopyToClipboard (line 9) | function useCopyToClipboard({

FILE: lib/hooks/use-media-query.ts
  function useMediaQuery (line 10) | function useMediaQuery(query: string): boolean {

FILE: lib/ollama/client.ts
  class OllamaClient (line 8) | class OllamaClient {
    method constructor (line 11) | constructor(baseUrl: string) {
    method getModels (line 19) | async getModels(): Promise<OllamaModel[]> {
    method getModelCapabilities (line 46) | async getModelCapabilities(
    method isAvailable (line 81) | async isAvailable(): Promise<boolean> {

FILE: lib/ollama/types.ts
  type OllamaModel (line 1) | interface OllamaModel {
  type OllamaModelCapabilities (line 16) | interface OllamaModelCapabilities {
  type OllamaModelsResponse (line 24) | interface OllamaModelsResponse {
  type OllamaShowResponse (line 28) | interface OllamaShowResponse {

FILE: lib/rate-limit/chat-limits.ts
  constant DAILY_CHAT_LIMIT (line 5) | const DAILY_CHAT_LIMIT = 100
  function getSecondsUntilMidnight (line 10) | function getSecondsUntilMidnight(): number {
  function getNextMidnightTimestamp (line 20) | function getNextMidnightTimestamp(): number {
  function checkOverallChatLimit (line 27) | async function checkOverallChatLimit(userId: string): Promise<{
  function checkAndEnforceOverallChatLimit (line 84) | async function checkAndEnforceOverallChatLimit(

FILE: lib/rate-limit/guest-limit.ts
  constant DEFAULT_GUEST_DAILY_LIMIT (line 3) | const DEFAULT_GUEST_DAILY_LIMIT = 10
  function getGuestDailyLimit (line 5) | function getGuestDailyLimit(): number {
  function getSecondsUntilMidnight (line 14) | function getSecondsUntilMidnight(): number {
  function getNextMidnightTimestamp (line 21) | function getNextMidnightTimestamp(): number {
  function checkGuestLimit (line 28) | async function checkGuestLimit(ip: string): Promise<{
  function checkAndEnforceGuestLimit (line 81) | async function checkAndEnforceGuestLimit(

FILE: lib/schema/fetch.tsx
  type PartialInquiry (line 14) | type PartialInquiry = DeepPartial<typeof fetchSchema>

FILE: lib/schema/question.ts
  function getQuestionSchemaForModel (line 42) | function getQuestionSchemaForModel(fullModel: string) {

FILE: lib/schema/related.tsx
  type RelatedQuestion (line 9) | type RelatedQuestion = z.infer<typeof relatedQuestionSchema>
  type Related (line 10) | type Related = z.infer<typeof relatedSchema>

FILE: lib/schema/search.tsx
  function getSearchSchemaForModel (line 81) | function getSearchSchemaForModel(fullModel: string) {
  type PartialInquiry (line 96) | type PartialInquiry = DeepPartial<typeof searchSchema>

FILE: lib/storage/r2-client.ts
  constant R2_BUCKET_NAME (line 3) | const R2_BUCKET_NAME = process.env.R2_BUCKET_NAME || 'user-uploads'
  constant R2_PUBLIC_URL (line 4) | const R2_PUBLIC_URL = process.env.R2_PUBLIC_URL || ''
  function getR2Client (line 8) | function getR2Client(): S3Client {

FILE: lib/streaming/create-chat-stream-response.ts
  constant DEFAULT_CHAT_TITLE (line 35) | const DEFAULT_CHAT_TITLE = 'Untitled'
  function createChatStreamResponse (line 37) | async function createChatStreamResponse(

FILE: lib/streaming/create-ephemeral-chat-stream-response.ts
  type EphemeralStreamConfig (line 27) | type EphemeralStreamConfig = Pick<
  function createEphemeralChatStreamResponse (line 35) | async function createEphemeralChatStreamResponse(

FILE: lib/streaming/helpers/persist-stream-results.ts
  constant DEFAULT_CHAT_TITLE (line 9) | const DEFAULT_CHAT_TITLE = 'Untitled'
  function persistStreamResults (line 11) | async function persistStreamResults(

FILE: lib/streaming/helpers/prepare-messages.ts
  constant DEFAULT_CHAT_TITLE (line 15) | const DEFAULT_CHAT_TITLE = 'Untitled'
  function prepareMessages (line 17) | async function prepareMessages(

FILE: lib/streaming/helpers/stream-related-questions.ts
  function streamRelatedQuestions (line 10) | async function streamRelatedQuestions(

FILE: lib/streaming/helpers/strip-reasoning-parts.ts
  function stripReasoningParts (line 15) | function stripReasoningParts(messages: UIMessage[]): UIMessage[] {

FILE: lib/streaming/helpers/types.ts
  type StreamContext (line 4) | interface StreamContext {

FILE: lib/streaming/types.ts
  type BaseStreamConfig (line 7) | interface BaseStreamConfig {

FILE: lib/supabase/client.ts
  function createClient (line 3) | function createClient() {

FILE: lib/supabase/middleware.ts
  function updateSession (line 5) | async function updateSession(request: NextRequest) {

FILE: lib/supabase/server.ts
  function createClient (line 5) | async function createClient() {

FILE: lib/tools/dynamic.ts
  function createDynamicTool (line 10) | function createDynamicTool(
  function createMCPTool (line 34) | function createMCPTool(
  function createCustomTool (line 48) | function createCustomTool(

FILE: lib/tools/fetch.ts
  constant CONTENT_CHARACTER_LIMIT (line 6) | const CONTENT_CHARACTER_LIMIT = 50000
  constant TITLE_CHARACTER_LIMIT (line 7) | const TITLE_CHARACTER_LIMIT = 100
  function fetchRegularData (line 9) | async function fetchRegularData(url: string): Promise<SearchResultsType> {
  function fetchJinaReaderData (line 90) | async function fetchJinaReaderData(url: string): Promise<SearchResultsTy...
  function fetchTavilyExtractData (line 123) | async function fetchTavilyExtractData(url: string): Promise<SearchResult...
  method execute (line 164) | async *execute({ url, type = 'regular' }) {
  type FetchUIToolInvocation (line 195) | type FetchUIToolInvocation = UIToolInvocation<typeof fetchTool>

FILE: lib/tools/question.ts
  function createQuestionTool (line 8) | function createQuestionTool(fullModel: string) {

FILE: lib/tools/search.ts
  function createSearchTool (line 20) | function createSearchTool(fullModel: string) {
  type SearchUIToolInvocation (line 173) | type SearchUIToolInvocation = UIToolInvocation<typeof searchTool>
  function search (line 175) | async function search(

FILE: lib/tools/search/providers/base.ts
  type SearchProvider (line 3) | interface SearchProvider {
  method validateApiKey (line 30) | protected validateApiKey(
  method validateApiUrl (line 41) | protected validateApiUrl(

FILE: lib/tools/search/providers/brave.ts
  type BraveWebResult (line 9) | interface BraveWebResult {
  type BraveVideoResult (line 15) | interface BraveVideoResult {
  type BraveImageResult (line 30) | interface BraveImageResult {
  class BraveSearchProvider (line 46) | class BraveSearchProvider implements SearchProvider {
    method constructor (line 49) | constructor() {
    method getImageThumbnailUrl (line 53) | private getImageThumbnailUrl(result: BraveImageResult): string {
    method search (line 59) | async search(
    method searchWeb (line 106) | private async searchWeb(
    method searchVideos (line 143) | private async searchVideos(
    method searchImages (line 190) | private async searchImages(

FILE: lib/tools/search/providers/exa.ts
  class ExaSearchProvider (line 7) | class ExaSearchProvider extends BaseSearchProvider {
    method search (line 8) | async search(

FILE: lib/tools/search/providers/firecrawl.ts
  class FirecrawlSearchProvider (line 10) | class FirecrawlSearchProvider extends BaseSearchProvider {
    method search (line 11) | async search(

FILE: lib/tools/search/providers/index.ts
  type SearchProviderType (line 8) | type SearchProviderType =
  constant DEFAULT_PROVIDER (line 14) | const DEFAULT_PROVIDER: SearchProviderType = 'tavily'
  function createSearchProvider (line 16) | function createSearchProvider(

FILE: lib/tools/search/providers/searxng.ts
  class SearXNGSearchProvider (line 10) | class SearXNGSearchProvider extends BaseSearchProvider {
    method search (line 11) | async search(

FILE: lib/tools/search/providers/tavily.ts
  class TavilySearchProvider (line 6) | class TavilySearchProvider extends BaseSearchProvider {
    method search (line 7) | async search(

FILE: lib/tools/todo.ts
  type TodoItem (line 18) | type TodoItem = z.infer<typeof todoItemSchema>
  function createTodoTools (line 30) | function createTodoTools() {

FILE: lib/types/agent.ts
  type ResearcherTools (line 15) | type ResearcherTools = {
  type ResearcherAgent (line 23) | type ResearcherAgent = ToolLoopAgent<never, ResearcherTools, never>
  type ResearcherUIMessage (line 26) | type ResearcherUIMessage = InferAgentUIMessage<ResearcherAgent>
  type ResearcherUITools (line 29) | type ResearcherUITools = InferUITools<ResearcherTools>
  type SearchToolInvocation (line 32) | type SearchToolInvocation = UIToolInvocation<ResearcherTools['search']>
  type FetchToolInvocation (line 33) | type FetchToolInvocation = UIToolInvocation<ResearcherTools['fetch']>
  type QuestionToolInvocation (line 34) | type QuestionToolInvocation = UIToolInvocation<
  type TodoWriteToolInvocation (line 37) | type TodoWriteToolInvocation = UIToolInvocation<
  type ResearcherToolInvocation (line 42) | type ResearcherToolInvocation =
  type ResearcherToolName (line 49) | type ResearcherToolName = keyof ResearcherTools
  function isSearchToolInvocation (line 52) | function isSearchToolInvocation(
  function isFetchToolInvocation (line 58) | function isFetchToolInvocation(
  type ResearcherResponse (line 65) | type ResearcherResponse = Response
  type ResearcherRespondOptions (line 68) | type ResearcherRespondOptions = {

FILE: lib/types/ai.ts
  type UIMessageMetadata (line 14) | interface UIMessageMetadata {
  type UIMessage (line 22) | type UIMessage<
  type RelatedQuestionsData (line 28) | interface RelatedQuestionsData {
  type UIDataTypes (line 33) | type UIDataTypes = {
  type DataRelatedQuestionsPart (line 39) | type DataRelatedQuestionsPart = {
  type DataPart (line 45) | type DataPart = DataRelatedQuestionsPart
  type UITools (line 50) | type UITools = {
  type ToolPart (line 59) | type ToolPart<T extends keyof UITools = keyof UITools> = {
  type Part (line 72) | type Part = TextPart | ReasoningPart | ToolPart

FILE: lib/types/dynamic-tools.ts
  type MCPClient (line 6) | interface MCPClient {
  type DynamicToolConfig (line 14) | interface DynamicToolConfig {
  type DynamicToolPart (line 22) | type DynamicToolPart =
  type DynamicToolPartBase (line 28) | interface DynamicToolPartBase {
  type DynamicToolPartInputStreaming (line 34) | interface DynamicToolPartInputStreaming extends DynamicToolPartBase {
  type DynamicToolPartInputAvailable (line 39) | interface DynamicToolPartInputAvailable extends DynamicToolPartBase {
  type DynamicToolPartOutputAvailable (line 44) | interface DynamicToolPartOutputAvailable extends DynamicToolPartBase {
  type DynamicToolPartOutputError (line 50) | interface DynamicToolPartOutputError extends DynamicToolPartBase {
  function isDynamicToolPart (line 57) | function isDynamicToolPart(part: unknown): part is DynamicToolPart {
  function isToolCallPart (line 66) | function isToolCallPart(
  function isToolTypePart (line 77) | function isToolTypePart(

FILE: lib/types/index.ts
  type SearchResults (line 4) | type SearchResults = {
  type SearchResultImage (line 16) | type SearchResultImage =
  type ExaSearchResults (line 24) | type ExaSearchResults = {
  type SerperSearchResults (line 28) | type SerperSearchResults = {
  type SearchResultItem (line 37) | type SearchResultItem = {
  type ExaSearchResultItem (line 43) | type ExaSearchResultItem = {
  type SerperSearchResultItem (line 52) | type SerperSearchResultItem = {
  type SearchImageItem (line 64) | type SearchImageItem = {
  type SearXNGResult (line 70) | interface SearXNGResult {
  type SearXNGResponse (line 79) | interface SearXNGResponse {
  type SearXNGImageResult (line 85) | type SearXNGImageResult = string
  type SearXNGSearchResults (line 87) | type SearXNGSearchResults = {
  type UploadedFile (line 94) | type UploadedFile = {

FILE: lib/types/message-persistence.ts
  type Metadata (line 8) | type Metadata = z.infer<typeof metadataSchema>
  type DataPart (line 12) | type DataPart = z.infer<typeof dataPartSchema>
  type ProviderMetadata (line 15) | type ProviderMetadata = Record<string, any>
  type DBMessagePart (line 18) | type DBMessagePart = typeof parts.$inferInsert
  type DBMessagePartSelect (line 19) | type DBMessagePartSelect = typeof parts.$inferSelect
  type ToolState (line 22) | type ToolState =
  type DynamicToolInput (line 29) | type DynamicToolInput = {
  type DynamicToolOutput (line 34) | type DynamicToolOutput = unknown
  type DynamicToolType (line 37) | type DynamicToolType = 'mcp' | 'dynamic' | 'custom'
  type MCPGitHubInput (line 40) | type MCPGitHubInput = {
  type DBMessage (line 51) | type DBMessage = {
  type PersistableUIMessage (line 59) | type PersistableUIMessage = UIMessage & {

FILE: lib/types/model-type.ts
  type ModelType (line 2) | type ModelType = 'speed' | 'quality'

FILE: lib/types/models.ts
  type Model (line 1) | interface Model {

FILE: lib/types/search.ts
  type SearchMode (line 2) | type SearchMode = 'quick' | 'adaptive'

FILE: lib/utils/__tests__/model-selection.test.ts
  type Matrix (line 19) | type Matrix = Record<SearchMode, Partial<Record<ModelType, Model>>>
  function setMatrixImplementation (line 51) | function setMatrixImplementation() {
  function createCookieStore (line 57) | function createCookieStore(modelType?: string): ReadonlyRequestCookies {

FILE: lib/utils/citation.ts
  function isValidUrl (line 8) | function isValidUrl(url: string): boolean {
  function extractCitationMaps (line 21) | function extractCitationMaps(
  function extractCitationMapsFromMessages (line 51) | function extractCitationMapsFromMessages(
  function processCitations (line 72) | function processCitations(

FILE: lib/utils/context-window.ts
  type ModelContextInfo (line 6) | interface ModelContextInfo {
  constant MODEL_CONTEXT_WINDOWS (line 12) | const MODEL_CONTEXT_WINDOWS: Record<string, ModelContextInfo> = {
  constant DEFAULT_CONTEXT_WINDOW (line 37) | const DEFAULT_CONTEXT_WINDOW = 16384
  constant DEFAULT_OUTPUT_TOKENS (line 38) | const DEFAULT_OUTPUT_TOKENS = 4096
  constant SAFETY_BUFFER_RATIO (line 41) | const SAFETY_BUFFER_RATIO = 0.1
  constant MODEL_TO_ENCODING (line 48) | const MODEL_TO_ENCODING: Record<string, TiktokenEncoding> = {
  function getModelContextInfo (line 68) | function getModelContextInfo(modelId: string): ModelContextInfo {
  function getMaxAllowedTokens (line 81) | function getMaxAllowedTokens(model: Model): number {
  function extractTextContent (line 98) | function extractTextContent(content: ModelMessage['content']): string {
  function getEncoder (line 125) | function getEncoder(modelId: string) {
  function estimateTokenCount (line 151) | function estimateTokenCount(
  function truncateMessages (line 189) | function truncateMessages(
  function shouldTruncateMessages (line 290) | function shouldTruncateMessages(

FILE: lib/utils/cookies.ts
  function setCookie (line 1) | function setCookie(name: string, value: string, days = 30) {
  function getCookie (line 10) | function getCookie(name: string): string | null {
  function deleteCookie (line 23) | function deleteCookie(name: string) {

FILE: lib/utils/index.ts
  function generateUUID (line 7) | function generateUUID(): string {
  function cn (line 16) | function cn(...inputs: ClassValue[]) {
  function sanitizeUrl (line 25) | function sanitizeUrl(url: string): string {
  function createModelId (line 29) | function createModelId(model: Model): string {
  function getDefaultModelId (line 33) | function getDefaultModelId(models: Model[]): string {

FILE: lib/utils/message-mapping.ts
  type TextUIPart (line 16) | type TextUIPart = { type: 'text'; text: string; providerMetadata?: any }
  type ReasoningUIPart (line 17) | type ReasoningUIPart = {
  type FileUIPart (line 22) | type FileUIPart = {
  type SourceUrlUIPart (line 28) | type SourceUrlUIPart = {
  type SourceDocumentUIPart (line 34) | type SourceDocumentUIPart = {
  type ToolCallPart (line 43) | type ToolCallPart = {
  type ToolResultPart (line 49) | type ToolResultPart = {
  type DataPart (line 55) | type DataPart = { type: string; [key: string]: any }
  type UIMessagePart (line 57) | type UIMessagePart =
  function isToolCallPart (line 68) | function isToolCallPart(part: any): part is ToolCallPart {
  function isToolResultPart (line 77) | function isToolResultPart(part: any): part is ToolResultPart {
  type ExtendedToolPart (line 86) | type ExtendedToolPart = {
  function isExtendedToolPart (line 95) | function isExtendedToolPart(part: any): part is ExtendedToolPart {
  function createToolPartMapping (line 105) | function createToolPartMapping(
  function mapUIMessagePartsToDBParts (line 127) | function mapUIMessagePartsToDBParts(
  function mapDBPartToUIMessagePart (line 344) | function mapDBPartToUIMessagePart(
  function getToolNameFromType (line 671) | function getToolNameFromType(toolName: string): string {
  function getToolNameFromCallId (line 693) | function getToolNameFromCallId(
  function getOriginalToolName (line 713) | function getOriginalToolName(dbToolName: string): string {
  function mapUIMessageToDBMessage (line 729) | function mapUIMessageToDBMessage(
  function buildUIMessageFromDB (line 748) | function buildUIMessageFromDB(

FILE: lib/utils/message-utils.ts
  type DatabaseMessageInput (line 6) | interface DatabaseMessageInput {
  function convertMessageForDB (line 16) | function convertMessageForDB(
  function convertMessagesForDB (line 55) | function convertMessagesForDB(
  function extractTitleFromMessage (line 67) | function extractTitleFromMessage(
  function getTextFromParts (line 94) | function getTextFromParts(parts?: UIMessage['parts']): string {
  function mergeUIMessages (line 109) | function mergeUIMessages(
  function hasToolCalls (line 124) | function hasToolCalls(message: UIMessage | null): boolean {

FILE: lib/utils/model-selection.ts
  constant DEFAULT_MODEL (line 9) | const DEFAULT_MODEL: Model = {
  constant VALID_MODEL_TYPES (line 22) | const VALID_MODEL_TYPES: ModelType[] = ['speed', 'quality']
  constant MODE_FALLBACK_ORDER (line 23) | const MODE_FALLBACK_ORDER: SearchMode[] = ['quick', 'adaptive']
  type ModelSelectionParams (line 25) | interface ModelSelectionParams {
  function resolveModelForModeAndType (line 30) | function resolveModelForModeAndType(
  function selectModel (line 66) | function selectModel({

FILE: lib/utils/perf-logging.ts
  function perfLog (line 5) | function perfLog(message: string) {
  function perfTime (line 11) | function perfTime(label: string, startTime: number) {

FILE: lib/utils/perf-tracking.ts
  function resetAuthCallCount (line 17) | function resetAuthCallCount() {
  function incrementAuthCallCount (line 21) | function incrementAuthCallCount() {
  function resetDbOperationCount (line 26) | function resetDbOperationCount() {
  function incrementDbOperationCount (line 30) | function incrementDbOperationCount() {
  function resetAllCounters (line 35) | function resetAllCounters() {

FILE: lib/utils/registry.ts
  function getModel (line 31) | function getModel(model: string): LanguageModel {
  function isProviderEnabled (line 37) | function isProviderEnabled(providerId: string): boolean {

FILE: lib/utils/retry.ts
  type RetryOptions (line 3) | interface RetryOptions {
  function retryWithBackoff (line 11) | async function retryWithBackoff<T>(
  function retryDatabaseOperation (line 54) | async function retryDatabaseOperation<T>(

FILE: lib/utils/search-config.ts
  function isGeneralSearchProviderAvailable (line 10) | function isGeneralSearchProviderAvailable(): boolean {
  function getGeneralSearchProviderName (line 17) | function getGeneralSearchProviderName(): string {
  function supportsMultimediaContentTypes (line 27) | function supportsMultimediaContentTypes(): boolean {
  function getSearchTypeDescription (line 35) | function getSearchTypeDescription(): string {
  function getSearchToolDescription (line 49) | function getSearchToolDescription(): string {
  function getContentTypesGuidance (line 62) | function getContentTypesGuidance(): string {
  function getSearchStrategyGuidance (line 90) | function getSearchStrategyGuidance(): string {
  function getGeneralSearchProviderType (line 113) | function getGeneralSearchProviderType(): 'brave' | null {

FILE: lib/utils/telemetry.ts
  function isTracingEnabled (line 5) | function isTracingEnabled(): boolean {

FILE: lib/utils/url.ts
  function getBaseUrlFromHeaders (line 7) | async function getBaseUrlFromHeaders(): Promise<URL> {
  function getBaseUrl (line 39) | async function getBaseUrl(): Promise<URL> {
  function getBaseUrlString (line 65) | async function getBaseUrlString(): Promise<string> {

FILE: proxy.ts
  function proxy (line 5) | async function proxy(request: NextRequest) {

FILE: scripts/chat-cli.ts
  constant DEFAULT_MESSAGE (line 11) | const DEFAULT_MESSAGE = 'Hello, how are you?'
  type ChatApiConfig (line 13) | interface ChatApiConfig {
  type UIMessage (line 23) | interface UIMessage {
  type ChatPayload (line 35) | interface ChatPayload {
  class ChatApiTester (line 43) | class ChatApiTester {
    method validateUrl (line 46) | private validateUrl(url?: string): string | undefined {
    method constructor (line 85) | constructor(config: Partial<ChatApiConfig> = {}) {
    method generateId (line 98) | private generateId(): string {
    method createUserMessage (line 102) | private createUserMessage(text: string, messageId?: string): UIMessage {
    method loadCookiesFromEnv (line 120) | private loadCookiesFromEnv(): string | undefined {
    method sendMessage (line 128) | async sendMessage(message?: string): Promise<void> {
  function parseArgs (line 312) | function parseArgs(): Partial<ChatApiConfig> {
  function main (line 427) | async function main() {

FILE: scripts/test-cache-performance.ts
  constant API_URL (line 7) | const API_URL = process.env.API_URL || 'http://localhost:3001/api/chat'
  constant COOKIES (line 8) | const COOKIES = process.env.MORPHIC_COOKIES
  function measureRequest (line 10) | async function measureRequest(
  function runPerformanceTests (line 61) | async function runPerformanceTests() {
Condensed preview — 295 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (1,109K chars).
[
  {
    "path": ".eslintrc.json",
    "chars": 906,
    "preview": "{\n  \"extends\": \"next/core-web-vitals\",\n  \"plugins\": [\"simple-import-sort\"],\n  \"rules\": {\n    \"simple-import-sort/imports"
  },
  {
    "path": ".github/FUNDING.yml",
    "chars": 62,
    "preview": "# These are supported funding model platforms\n\ngithub: miurla\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.yml",
    "chars": 1957,
    "preview": "name: 🐞 Bug\ndescription: File a bug/issue\ntitle: '[BUG] <title>'\nlabels: ['Bug', 'Needs Triage']\nbody:\n  - type: checkbo"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.yml",
    "chars": 1007,
    "preview": "name: ✨ Feature Request\ndescription: Propose a new feature for Morphic.\nlabels: []\nbody:\n  - type: markdown\n    attribut"
  },
  {
    "path": ".github/workflows/ci.yml",
    "chars": 2197,
    "preview": "name: CI\n\non:\n  push:\n    branches: [main]\n  pull_request:\n    branches: [main]\n\njobs:\n  lint:\n    runs-on: ubuntu-lates"
  },
  {
    "path": ".github/workflows/docker-build.yml",
    "chars": 999,
    "preview": "name: Docker Build and Push\n\non:\n  push:\n    branches: [main]\n\njobs:\n  build:\n    runs-on: ubuntu-latest\n    permissions"
  },
  {
    "path": ".github/workflows/release.yml",
    "chars": 3448,
    "preview": "name: Release\n\non:\n  push:\n    tags:\n      - 'v*' # e.g. v1.0.0, v1.0.0-beta.5\n\nconcurrency:\n  group: release-${{ github"
  },
  {
    "path": ".gitignore",
    "chars": 415,
    "preview": "# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.\n\n# dependencies\n/node_modules\n/.pn"
  },
  {
    "path": ".mcp.json",
    "chars": 162,
    "preview": "{\n  \"mcpServers\": {\n    \"next-devtools\": {\n      \"type\": \"stdio\",\n      \"command\": \"npx\",\n      \"args\": [\"next-devtools-"
  },
  {
    "path": ".prettierignore",
    "chars": 322,
    "preview": "# Dependencies\nnode_modules/\n.next/\nout/\nbuild/\ndist/\n\n# Environment\n.env*\n\n# Generated files\n*.lock\nbun.lock\npackage-lo"
  },
  {
    "path": ".vscode/settings.json",
    "chars": 205,
    "preview": "{\n  \"editor.formatOnSave\": true,\n  \"editor.defaultFormatter\": \"esbenp.prettier-vscode\",\n  \"cSpell.words\": [\"openai\", \"Ta"
  },
  {
    "path": "AGENTS.md",
    "chars": 8420,
    "preview": "# CLAUDE.md\n\nThis file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.\n\n## "
  },
  {
    "path": "CODE_OF_CONDUCT.md",
    "chars": 2445,
    "preview": "# Code of Conduct\n\n## Our Pledge\n\nWe as members, contributors, and leaders pledge to make participation in our\ncommunity"
  },
  {
    "path": "CONTRIBUTING.md",
    "chars": 1281,
    "preview": "# Contributing to Morphic\n\nThank you for your interest in contributing to Morphic! This document provides guidelines and"
  },
  {
    "path": "Dockerfile",
    "chars": 1374,
    "preview": "# Build stage - Use Node for Next.js 16 compatibility (Bun lacks worker_threads support on arm64)\nFROM node:22-slim AS b"
  },
  {
    "path": "LICENSE",
    "chars": 10933,
    "preview": "                                 Apache License\n                           Version 2.0, January 2004\n                   "
  },
  {
    "path": "README.md",
    "chars": 7317,
    "preview": "<div align=\"center\">\n\n# Morphic\n\nAn AI-powered search engine with a generative UI.\n\n[![DeepWiki](https://img.shields.io/"
  },
  {
    "path": "app/api/advanced-search/route.ts",
    "chars": 19750,
    "preview": "import { NextResponse } from 'next/server'\n\nimport { Redis } from '@upstash/redis'\nimport http from 'http'\nimport { Agen"
  },
  {
    "path": "app/api/chat/route.ts",
    "chars": 7354,
    "preview": "import { revalidateTag } from 'next/cache'\nimport { cookies } from 'next/headers'\n\nimport { loadChat } from '@/lib/actio"
  },
  {
    "path": "app/api/chats/route.ts",
    "chars": 780,
    "preview": "import { NextRequest, NextResponse } from 'next/server'\n\nimport { getChatsPage } from '@/lib/actions/chat'\nimport { Chat"
  },
  {
    "path": "app/api/feedback/__tests__/route.test.ts",
    "chars": 7947,
    "preview": "import { beforeEach, describe, expect, it, vi } from 'vitest'\n\n// Mock Next.js cookies API\nvi.mock('next/headers', () =>"
  },
  {
    "path": "app/api/feedback/route.ts",
    "chars": 2181,
    "preview": "import { Langfuse } from 'langfuse'\n\nimport { updateMessageFeedback } from '@/lib/actions/feedback'\nimport { createClien"
  },
  {
    "path": "app/api/upload/route.ts",
    "chars": 2596,
    "preview": "import { NextRequest, NextResponse } from 'next/server'\n\nimport { PutObjectCommand } from '@aws-sdk/client-s3'\n\nimport {"
  },
  {
    "path": "app/auth/confirm/route.ts",
    "chars": 993,
    "preview": "import { redirect } from 'next/navigation'\nimport { type NextRequest } from 'next/server'\n\nimport { type EmailOtpType } "
  },
  {
    "path": "app/auth/error/page.tsx",
    "chars": 1037,
    "preview": "import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'\n\nexport default async function Page({\n  "
  },
  {
    "path": "app/auth/forgot-password/page.tsx",
    "chars": 301,
    "preview": "import { ForgotPasswordForm } from '@/components/forgot-password-form'\n\nexport default function Page() {\n  return (\n    "
  },
  {
    "path": "app/auth/login/page.tsx",
    "chars": 273,
    "preview": "import { LoginForm } from '@/components/login-form'\n\nexport default function Page() {\n  return (\n    <div className=\"fle"
  },
  {
    "path": "app/auth/oauth/route.ts",
    "chars": 1226,
    "preview": "import { NextResponse } from 'next/server'\n\n// The client you created from the Server-Side Auth instructions\nimport { cr"
  },
  {
    "path": "app/auth/sign-up/page.tsx",
    "chars": 277,
    "preview": "import { SignUpForm } from '@/components/sign-up-form'\n\nexport default function Page() {\n  return (\n    <div className=\""
  },
  {
    "path": "app/auth/sign-up-success/page.tsx",
    "chars": 913,
    "preview": "import {\n  Card,\n  CardContent,\n  CardDescription,\n  CardHeader,\n  CardTitle\n} from '@/components/ui/card'\n\nexport defau"
  },
  {
    "path": "app/auth/update-password/page.tsx",
    "chars": 301,
    "preview": "import { UpdatePasswordForm } from '@/components/update-password-form'\n\nexport default function Page() {\n  return (\n    "
  },
  {
    "path": "app/globals.css",
    "chars": 10653,
    "preview": "@import 'tailwindcss';\n@source \"../node_modules/streamdown/dist/index.js\";\n\n@custom-variant dark (&:is(.dark *));\n\n:root"
  },
  {
    "path": "app/layout.tsx",
    "chars": 2606,
    "preview": "import type { Metadata, Viewport } from 'next'\nimport { Inter as FontSans } from 'next/font/google'\n\nimport { Analytics "
  },
  {
    "path": "app/page.tsx",
    "chars": 225,
    "preview": "import { getCurrentUserId } from '@/lib/auth/get-current-user'\n\nimport { Chat } from '@/components/chat'\n\nexport default"
  },
  {
    "path": "app/search/[id]/page.tsx",
    "chars": 1052,
    "preview": "import { notFound, redirect } from 'next/navigation'\n\nimport { UIMessage } from 'ai'\n\nimport { loadChat } from '@/lib/ac"
  },
  {
    "path": "app/search/loading.tsx",
    "chars": 304,
    "preview": "'use client'\n\nimport { DefaultSkeleton } from '../../components/default-skeleton'\n\nexport default function Loading() {\n "
  },
  {
    "path": "app/search/page.tsx",
    "chars": 520,
    "preview": "import { redirect } from 'next/navigation'\n\nimport { getCurrentUserId } from '@/lib/auth/get-current-user'\nimport { gene"
  },
  {
    "path": "components/__tests__/research-process-section.test.tsx",
    "chars": 12282,
    "preview": "import React from 'react'\n\nimport type { ReasoningPart } from '@ai-sdk/provider-utils'\nimport { fireEvent, render, scree"
  },
  {
    "path": "components/action-buttons.tsx",
    "chars": 6316,
    "preview": "'use client'\n\nimport { useEffect, useRef, useState } from 'react'\n\nimport {\n  FileText,\n  HelpCircle,\n  LucideIcon,\n  Ne"
  },
  {
    "path": "components/answer-section.tsx",
    "chars": 2213,
    "preview": "'use client'\n\nimport { UseChatHelpers } from '@ai-sdk/react'\nimport { ChatRequestOptions } from 'ai'\n\nimport type { Sear"
  },
  {
    "path": "components/app-sidebar.tsx",
    "chars": 1581,
    "preview": "import { Suspense } from 'react'\nimport Link from 'next/link'\n\nimport { Plus } from 'lucide-react'\n\nimport { cn } from '"
  },
  {
    "path": "components/artifact/artifact-content.tsx",
    "chars": 980,
    "preview": "'use client'\n\nimport { Part, ToolPart } from '@/lib/types/ai'\n\nimport { ReasoningContent } from './reasoning-content'\nim"
  },
  {
    "path": "components/artifact/artifact-context.tsx",
    "chars": 2138,
    "preview": "'use client'\n\nimport {\n  createContext,\n  ReactNode,\n  useCallback,\n  useContext,\n  useEffect,\n  useReducer\n} from 'reac"
  },
  {
    "path": "components/artifact/artifact-root.tsx",
    "chars": 376,
    "preview": "'use client'\n\nimport { ReactNode } from 'react'\n\nimport { ArtifactProvider } from './artifact-context'\nimport { ChatArti"
  },
  {
    "path": "components/artifact/chat-artifact-container.tsx",
    "chars": 5997,
    "preview": "'use client'\n\nimport React, { useCallback, useEffect, useRef, useState } from 'react'\n\nimport { useHasUser } from '@/lib"
  },
  {
    "path": "components/artifact/reasoning-content.tsx",
    "chars": 415,
    "preview": "'use client'\n\nimport remarkGfm from 'remark-gfm'\nimport { Streamdown } from 'streamdown'\n\nimport { cn } from '@/lib/util"
  },
  {
    "path": "components/artifact/search-artifact-content.tsx",
    "chars": 2175,
    "preview": "'use client'\n\nimport type { SearchResults as TypeSearchResults } from '@/lib/types'\nimport type { ToolPart } from '@/lib"
  },
  {
    "path": "components/artifact/todo-invocation-content.tsx",
    "chars": 925,
    "preview": "'use client'\n\nimport type { ToolPart } from '@/lib/types/ai'\n\nimport TodoListContent from '../todo-list-content'\n\ninterf"
  },
  {
    "path": "components/artifact/tool-invocation-content.tsx",
    "chars": 438,
    "preview": "'use client'\n\nimport type { ToolPart } from '@/lib/types/ai'\n\nimport { SearchArtifactContent } from '@/components/artifa"
  },
  {
    "path": "components/attachment-preview.tsx",
    "chars": 1476,
    "preview": "'use client'\n\nimport React from 'react'\n\ninterface Attachment {\n  name: string | undefined\n  url: string\n  contentType: "
  },
  {
    "path": "components/auth-modal.tsx",
    "chars": 1435,
    "preview": "'use client'\n\nimport Link from 'next/link'\n\nimport { Button } from '@/components/ui/button'\nimport {\n  Dialog,\n  DialogC"
  },
  {
    "path": "components/chat-error.tsx",
    "chars": 951,
    "preview": "import { AlertCircle } from 'lucide-react'\n\nimport { Card } from '@/components/ui/card'\n\ninterface ChatErrorProps {\n  er"
  },
  {
    "path": "components/chat-messages.tsx",
    "chars": 8108,
    "preview": "'use client'\n\nimport { useEffect, useMemo, useRef, useState } from 'react'\n\nimport { UseChatHelpers } from '@ai-sdk/reac"
  },
  {
    "path": "components/chat-panel.tsx",
    "chars": 12126,
    "preview": "'use client'\n\nimport { useCallback, useEffect, useRef, useState } from 'react'\nimport Textarea from 'react-textarea-auto"
  },
  {
    "path": "components/chat-share.tsx",
    "chars": 2794,
    "preview": "'use client'\n\nimport { useState, useTransition } from 'react'\n\nimport { Share } from 'lucide-react'\nimport { toast } fro"
  },
  {
    "path": "components/chat.tsx",
    "chars": 15427,
    "preview": "'use client'\n\nimport { useEffect, useMemo, useRef, useState } from 'react'\nimport { useRouter } from 'next/navigation'\n\n"
  },
  {
    "path": "components/citation-context.tsx",
    "chars": 730,
    "preview": "'use client'\n\nimport { createContext, ReactNode, useContext } from 'react'\n\nimport type { SearchResultItem } from '@/lib"
  },
  {
    "path": "components/citation-link.tsx",
    "chars": 3970,
    "preview": "'use client'\n\nimport { memo, useState } from 'react'\nimport Link from 'next/link'\n\nimport type { SearchResultItem } from"
  },
  {
    "path": "components/collapsible-message.tsx",
    "chars": 5888,
    "preview": "import { ChevronDown } from 'lucide-react'\n\nimport { cn } from '@/lib/utils'\n\nimport {\n  Collapsible,\n  CollapsibleConte"
  },
  {
    "path": "components/current-user-avatar.tsx",
    "chars": 815,
    "preview": "'use client'\n\nimport { User2 } from 'lucide-react'\n\nimport { useCurrentUserImage } from '@/hooks/use-current-user-image'"
  },
  {
    "path": "components/custom-link.tsx",
    "chars": 1563,
    "preview": "import { AnchorHTMLAttributes, DetailedHTMLProps } from 'react'\n\nimport type { SearchResultItem } from '@/lib/types'\nimp"
  },
  {
    "path": "components/data-section.tsx",
    "chars": 562,
    "preview": "'use client'\n\nimport React from 'react'\n\nimport type { DataPart } from '@/lib/types/ai'\n\nimport { RelatedQuestions } fro"
  },
  {
    "path": "components/default-skeleton.tsx",
    "chars": 621,
    "preview": "'use client'\n\nimport { Skeleton } from './ui/skeleton'\n\nexport const DefaultSkeleton = () => {\n  return (\n    <div class"
  },
  {
    "path": "components/drag-overlay.tsx",
    "chars": 605,
    "preview": "'use client'\nimport { UploadCloud } from 'lucide-react'\n\nimport { cn } from '@/lib/utils'\n\nexport function DragOverlay({"
  },
  {
    "path": "components/dynamic-tool-display.tsx",
    "chars": 4006,
    "preview": "'use client'\n\nimport React from 'react'\n\n// This matches the structure from AI SDK v5\ntype DynamicToolPart =\n  | {\n     "
  },
  {
    "path": "components/error-modal.tsx",
    "chars": 4309,
    "preview": "'use client'\n\nimport Link from 'next/link'\n\nimport { AlertCircle, Clock, RefreshCw } from 'lucide-react'\n\nimport { Butto"
  },
  {
    "path": "components/external-link-items.tsx",
    "chars": 884,
    "preview": "'use client'\n\nimport { SiDiscord, SiGithub, SiX } from 'react-icons/si'\nimport Link from 'next/link'\n\nimport { DropdownM"
  },
  {
    "path": "components/feedback-modal.tsx",
    "chars": 4135,
    "preview": "'use client'\n\nimport { useState, useTransition } from 'react'\n\nimport { Frown, Meh, Smile } from 'lucide-react'\nimport {"
  },
  {
    "path": "components/fetch-section.tsx",
    "chars": 4827,
    "preview": "'use client'\n\nimport { UseChatHelpers } from '@ai-sdk/react'\nimport { AlertCircle, Check, ExternalLink, Globe } from 'lu"
  },
  {
    "path": "components/file-upload-button.tsx",
    "chars": 2237,
    "preview": "'use client'\n\nimport { useRef, useState } from 'react'\n\nimport { Paperclip } from 'lucide-react'\nimport { toast } from '"
  },
  {
    "path": "components/forgot-password-form.tsx",
    "chars": 3544,
    "preview": "'use client'\n\nimport { useState } from 'react'\nimport Link from 'next/link'\n\nimport { createClient } from '@/lib/supabas"
  },
  {
    "path": "components/guest-menu.tsx",
    "chars": 1834,
    "preview": "'use client'\n\nimport Link from 'next/link'\n\nimport {\n  Link2,\n  LogIn,\n  Palette,\n  Settings2 // Or EllipsisVertical, et"
  },
  {
    "path": "components/header.tsx",
    "chars": 1858,
    "preview": "'use client'\n\n// import Link from 'next/link' // No longer needed directly here for Sign In button\nimport React, { useSt"
  },
  {
    "path": "components/inspector/inspector-drawer.tsx",
    "chars": 1391,
    "preview": "'use client'\n\nimport { VisuallyHidden } from '@radix-ui/react-visually-hidden'\n\nimport { useMediaQuery } from '@/lib/hoo"
  },
  {
    "path": "components/inspector/inspector-panel.tsx",
    "chars": 2512,
    "preview": "'use client'\n\nimport {\n  LightbulbIcon,\n  ListTodo,\n  MessageSquare,\n  Minimize2,\n  Search\n} from 'lucide-react'\n\nimport"
  },
  {
    "path": "components/login-form.tsx",
    "chars": 5125,
    "preview": "'use client'\n\nimport { useState } from 'react'\nimport Link from 'next/link'\nimport { useRouter } from 'next/navigation'\n"
  },
  {
    "path": "components/message-actions.tsx",
    "chars": 4354,
    "preview": "'use client'\n\nimport { useMemo, useState } from 'react'\n\nimport { UseChatHelpers } from '@ai-sdk/react'\nimport { Copy, T"
  },
  {
    "path": "components/message.tsx",
    "chars": 1515,
    "preview": "'use client'\n\nimport rehypeExternalLinks from 'rehype-external-links'\nimport rehypeKatex from 'rehype-katex'\nimport rema"
  },
  {
    "path": "components/model-type-selector.tsx",
    "chars": 2656,
    "preview": "'use client'\n\nimport { useEffect, useState } from 'react'\n\nimport { Check, ChevronDown } from 'lucide-react'\n\nimport { M"
  },
  {
    "path": "components/process-header.tsx",
    "chars": 1025,
    "preview": "'use client'\n\nimport type { ReactNode } from 'react'\n\nimport { cn } from '@/lib/utils'\n\nexport type ProcessHeaderProps ="
  },
  {
    "path": "components/process-rail.tsx",
    "chars": 1077,
    "preview": "import { cn } from '@/lib/utils'\n\ninterface ProcessRailProps {\n  isFirst: boolean\n  isLast: boolean\n  isSubStep?: boolea"
  },
  {
    "path": "components/question-confirmation.tsx",
    "chars": 6554,
    "preview": "'use client'\n\nimport { useState } from 'react'\n\nimport { ArrowRight, Check, SkipForward } from 'lucide-react'\n\nimport ty"
  },
  {
    "path": "components/reasoning-section.tsx",
    "chars": 5064,
    "preview": "'use client'\n\nimport { useEffect, useState } from 'react'\n\nimport type { ReasoningPart } from '@ai-sdk/provider-utils'\n\n"
  },
  {
    "path": "components/related-questions.tsx",
    "chars": 2970,
    "preview": "'use client'\n\nimport React from 'react'\n\nimport { ArrowRight } from 'lucide-react'\n\nimport type { RelatedQuestionsData }"
  },
  {
    "path": "components/render-message.tsx",
    "chars": 5481,
    "preview": "import { UseChatHelpers } from '@ai-sdk/react'\n\nimport type { SearchResultItem } from '@/lib/types'\nimport type {\n  UIDa"
  },
  {
    "path": "components/research-process-section.tsx",
    "chars": 12327,
    "preview": "'use client'\n\nimport { useCallback, useState } from 'react'\n\nimport type { ReasoningPart } from '@ai-sdk/provider-utils'"
  },
  {
    "path": "components/retry-button.tsx",
    "chars": 609,
    "preview": "'use client'\n\nimport { RotateCcw } from 'lucide-react'\n\nimport { Button } from './ui/button'\n\ninterface RetryButtonProps"
  },
  {
    "path": "components/search-mode-selector.tsx",
    "chars": 7539,
    "preview": "'use client'\n\nimport { useEffect, useRef, useState } from 'react'\n\nimport { Check, ChevronDown } from 'lucide-react'\n\nim"
  },
  {
    "path": "components/search-results-image.tsx",
    "chars": 10381,
    "preview": "/* eslint-disable @next/next/no-img-element */\n'use client'\n\nimport {\n  type Dispatch,\n  type SetStateAction,\n  useCallb"
  },
  {
    "path": "components/search-results.tsx",
    "chars": 4538,
    "preview": "'use client'\n\nimport { useState } from 'react'\nimport Link from 'next/link'\n\nimport { SearchResultItem } from '@/lib/typ"
  },
  {
    "path": "components/search-section.tsx",
    "chars": 6116,
    "preview": "'use client'\n\nimport { UseChatHelpers } from '@ai-sdk/react'\nimport { Check, Search as SearchIcon } from 'lucide-react'\n"
  },
  {
    "path": "components/section.tsx",
    "chars": 3043,
    "preview": "'use client'\n\nimport React from 'react'\n\nimport {\n  BookCheck,\n  Check,\n  File,\n  FileText,\n  Film,\n  Image,\n  MessageCi"
  },
  {
    "path": "components/sidebar/chat-history-client.tsx",
    "chars": 4246,
    "preview": "'use client'\n\nimport { useCallback, useEffect, useRef, useState, useTransition } from 'react'\n\nimport { toast } from 'so"
  },
  {
    "path": "components/sidebar/chat-history-section.tsx",
    "chars": 137,
    "preview": "import { ChatHistoryClient } from './chat-history-client'\n\nexport async function ChatHistorySection() {\n  return <ChatHi"
  },
  {
    "path": "components/sidebar/chat-history-skeleton.tsx",
    "chars": 368,
    "preview": "import {\n  SidebarMenu,\n  SidebarMenuItem,\n  SidebarMenuSkeleton\n} from '@/components/ui/sidebar'\n\nexport function ChatH"
  },
  {
    "path": "components/sidebar/chat-menu-item.tsx",
    "chars": 5982,
    "preview": "'use client'\n\nimport { useCallback, useState, useTransition } from 'react'\nimport Link from 'next/link'\nimport { usePath"
  },
  {
    "path": "components/sidebar/clear-history-action.tsx",
    "chars": 3693,
    "preview": "'use client'\n\nimport { useCallback, useState, useTransition } from 'react'\nimport { useRouter } from 'next/navigation'\n\n"
  },
  {
    "path": "components/sign-up-form.tsx",
    "chars": 4424,
    "preview": "'use client'\nimport { useState } from 'react'\nimport Link from 'next/link'\nimport { useRouter } from 'next/navigation'\n\n"
  },
  {
    "path": "components/source-favicons.tsx",
    "chars": 1439,
    "preview": "import Image from 'next/image'\n\nimport type { SearchResultItem } from '@/lib/types'\nimport { cn } from '@/lib/utils'\n\nin"
  },
  {
    "path": "components/theme-menu-items.tsx",
    "chars": 735,
    "preview": "'use client'\n\nimport { useTheme } from 'next-themes'\n\nimport { Laptop, Moon, Sun } from 'lucide-react'\n\nimport { Dropdow"
  },
  {
    "path": "components/theme-provider.tsx",
    "chars": 327,
    "preview": "'use client'\n\nimport * as React from 'react'\nimport { ThemeProvider as NextThemesProvider } from 'next-themes'\nimport { "
  },
  {
    "path": "components/todo-list-content.tsx",
    "chars": 2931,
    "preview": "'use client'\n\nimport { AlertCircle, Check } from 'lucide-react'\n\nimport type { TodoItem } from '@/lib/types/ai'\nimport {"
  },
  {
    "path": "components/tool-badge.tsx",
    "chars": 806,
    "preview": "import React from 'react'\n\nimport { Link, Search } from 'lucide-react'\n\nimport { cn } from '@/lib/utils'\n\nimport { Badge"
  },
  {
    "path": "components/tool-section.tsx",
    "chars": 3071,
    "preview": "'use client'\n\nimport { UseChatHelpers } from '@ai-sdk/react'\n\nimport type { ToolPart, UIDataTypes, UIMessage, UITools } "
  },
  {
    "path": "components/tool-todo-display.tsx",
    "chars": 4818,
    "preview": "import { Check, ListTodo } from 'lucide-react'\n\nimport { Part, TodoItem } from '@/lib/types/ai'\nimport { cn } from '@/li"
  },
  {
    "path": "components/ui/alert-dialog.tsx",
    "chars": 4435,
    "preview": "'use client'\n\nimport * as React from 'react'\n\nimport * as AlertDialogPrimitive from '@radix-ui/react-alert-dialog'\n\nimpo"
  },
  {
    "path": "components/ui/animated-logo.tsx",
    "chars": 635,
    "preview": "'use client'\n\nimport { cn } from '@/lib/utils'\n\nexport function AnimatedLogo({\n  className,\n  ...props\n}: React.Componen"
  },
  {
    "path": "components/ui/avatar.tsx",
    "chars": 1426,
    "preview": "'use client'\n\nimport * as React from 'react'\n\nimport * as AvatarPrimitive from '@radix-ui/react-avatar'\n\nimport { cn } f"
  },
  {
    "path": "components/ui/badge.tsx",
    "chars": 1127,
    "preview": "import * as React from 'react'\n\nimport { cva, type VariantProps } from 'class-variance-authority'\n\nimport { cn } from '@"
  },
  {
    "path": "components/ui/button.tsx",
    "chars": 1839,
    "preview": "import * as React from 'react'\n\nimport { Slot } from '@radix-ui/react-slot'\nimport { cva, type VariantProps } from 'clas"
  },
  {
    "path": "components/ui/card.tsx",
    "chars": 1883,
    "preview": "import * as React from 'react'\n\nimport { cn } from '@/lib/utils/index'\n\nconst Card = React.forwardRef<\n  HTMLDivElement,"
  },
  {
    "path": "components/ui/carousel.tsx",
    "chars": 6222,
    "preview": "'use client'\n\nimport * as React from 'react'\n\nimport useEmblaCarousel, {\n  type UseEmblaCarouselType\n} from 'embla-carou"
  },
  {
    "path": "components/ui/checkbox.tsx",
    "chars": 1073,
    "preview": "'use client'\n\nimport * as React from 'react'\n\nimport * as CheckboxPrimitive from '@radix-ui/react-checkbox'\nimport { Che"
  },
  {
    "path": "components/ui/collapsible.tsx",
    "chars": 329,
    "preview": "'use client'\n\nimport * as CollapsiblePrimitive from '@radix-ui/react-collapsible'\n\nconst Collapsible = CollapsiblePrimit"
  },
  {
    "path": "components/ui/command.tsx",
    "chars": 4904,
    "preview": "'use client'\n\nimport * as React from 'react'\n\nimport { type DialogProps } from '@radix-ui/react-dialog'\nimport { Command"
  },
  {
    "path": "components/ui/dialog.tsx",
    "chars": 3851,
    "preview": "'use client'\n\nimport * as React from 'react'\n\nimport * as DialogPrimitive from '@radix-ui/react-dialog'\nimport { X } fro"
  },
  {
    "path": "components/ui/drawer.tsx",
    "chars": 3027,
    "preview": "'use client'\n\nimport * as React from 'react'\n\nimport { Drawer as DrawerPrimitive } from 'vaul'\n\nimport { cn } from '@/li"
  },
  {
    "path": "components/ui/dropdown-menu.tsx",
    "chars": 8377,
    "preview": "'use client'\n\nimport * as React from 'react'\n\nimport * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu'\nimp"
  },
  {
    "path": "components/ui/hover-card.tsx",
    "chars": 1526,
    "preview": "'use client'\n\nimport * as React from 'react'\n\nimport * as HoverCardPrimitive from '@radix-ui/react-hover-card'\n\nimport {"
  },
  {
    "path": "components/ui/icons.tsx",
    "chars": 4638,
    "preview": "'use client'\n\nimport { useEffect, useRef } from 'react'\n\nimport { cn } from '@/lib/utils'\n\nfunction IconLogo({ className"
  },
  {
    "path": "components/ui/index.ts",
    "chars": 84,
    "preview": "export * from './button'\nexport * from './tooltip'\nexport * from './tooltip-button'\n"
  },
  {
    "path": "components/ui/input.tsx",
    "chars": 832,
    "preview": "import * as React from 'react'\n\nimport { cn } from '@/lib/utils/index'\n\nexport interface InputProps\n  extends React.Inpu"
  },
  {
    "path": "components/ui/label.tsx",
    "chars": 731,
    "preview": "'use client'\n\nimport * as React from 'react'\n\nimport * as LabelPrimitive from '@radix-ui/react-label'\nimport { cva, type"
  },
  {
    "path": "components/ui/password-input.tsx",
    "chars": 1105,
    "preview": "import * as React from 'react'\nimport { useState } from 'react'\n\nimport { Eye, EyeOff } from 'lucide-react'\n\nimport { In"
  },
  {
    "path": "components/ui/popover.tsx",
    "chars": 1247,
    "preview": "'use client'\n\nimport * as React from 'react'\n\nimport * as PopoverPrimitive from '@radix-ui/react-popover'\n\nimport { cn }"
  },
  {
    "path": "components/ui/select.tsx",
    "chars": 5622,
    "preview": "'use client'\n\nimport * as React from 'react'\n\nimport * as SelectPrimitive from '@radix-ui/react-select'\nimport { Check, "
  },
  {
    "path": "components/ui/separator.tsx",
    "chars": 771,
    "preview": "'use client'\n\nimport * as React from 'react'\n\nimport * as SeparatorPrimitive from '@radix-ui/react-separator'\n\nimport { "
  },
  {
    "path": "components/ui/sheet.tsx",
    "chars": 4285,
    "preview": "'use client'\n\nimport * as React from 'react'\n\nimport * as SheetPrimitive from '@radix-ui/react-dialog'\nimport { cva, typ"
  },
  {
    "path": "components/ui/sidebar.tsx",
    "chars": 24448,
    "preview": "'use client'\n\nimport * as React from 'react'\n\nimport { Slot } from '@radix-ui/react-slot'\nimport { cva, VariantProps } f"
  },
  {
    "path": "components/ui/skeleton.tsx",
    "chars": 267,
    "preview": "import { cn } from '@/lib/utils/index'\n\nfunction Skeleton({\n  className,\n  ...props\n}: React.HTMLAttributes<HTMLDivEleme"
  },
  {
    "path": "components/ui/slider.tsx",
    "chars": 1094,
    "preview": "'use client'\n\nimport * as React from 'react'\n\nimport * as SliderPrimitive from '@radix-ui/react-slider'\n\nimport { cn } f"
  },
  {
    "path": "components/ui/sonner.tsx",
    "chars": 893,
    "preview": "'use client'\n\nimport { useTheme } from 'next-themes'\n\nimport { Toaster as Sonner } from 'sonner'\n\ntype ToasterProps = Re"
  },
  {
    "path": "components/ui/spinner.tsx",
    "chars": 884,
    "preview": "// Based on: https://github.com/vercel/ai/blob/main/examples/next-ai-rsc/components/llm-stocks/spinner.tsx\n\nimport { cn "
  },
  {
    "path": "components/ui/status-indicator.tsx",
    "chars": 485,
    "preview": "import { ReactNode } from 'react'\n\nimport { LucideIcon } from 'lucide-react'\n\ninterface StatusIndicatorProps {\n  icon: L"
  },
  {
    "path": "components/ui/switch.tsx",
    "chars": 1156,
    "preview": "'use client'\n\nimport * as React from 'react'\n\nimport * as SwitchPrimitives from '@radix-ui/react-switch'\n\nimport { cn } "
  },
  {
    "path": "components/ui/textarea.tsx",
    "chars": 774,
    "preview": "import * as React from 'react'\n\nimport { cn } from '@/lib/utils'\n\nexport interface TextareaProps\n  extends React.Textare"
  },
  {
    "path": "components/ui/toggle.tsx",
    "chars": 1599,
    "preview": "'use client'\n\nimport * as React from 'react'\n\nimport * as TogglePrimitive from '@radix-ui/react-toggle'\nimport { cva, ty"
  },
  {
    "path": "components/ui/tooltip-button.tsx",
    "chars": 1197,
    "preview": "'use client'\n\nimport * as React from 'react'\n\nimport { Button, ButtonProps } from '@/components/ui/button'\nimport {\n  To"
  },
  {
    "path": "components/ui/tooltip.tsx",
    "chars": 1166,
    "preview": "'use client'\n\nimport * as React from 'react'\n\nimport * as TooltipPrimitive from '@radix-ui/react-tooltip'\n\nimport { cn }"
  },
  {
    "path": "components/update-password-form.tsx",
    "chars": 2464,
    "preview": "'use client'\n\nimport { useState } from 'react'\nimport { useRouter } from 'next/navigation'\n\nimport { createClient } from"
  },
  {
    "path": "components/uploaded-file-list.tsx",
    "chars": 2155,
    "preview": "'use client'\n\nimport React from 'react'\nimport Image from 'next/image'\n\nimport { Loader2, X } from 'lucide-react'\n\nimpor"
  },
  {
    "path": "components/user-file-section.tsx",
    "chars": 325,
    "preview": "import React from 'react'\n\nimport { AttachmentPreview } from './attachment-preview'\n\ninterface UserFileSectionProps {\n  "
  },
  {
    "path": "components/user-menu.tsx",
    "chars": 3292,
    "preview": "'use client'\n\nimport { useRouter } from 'next/navigation'\n\nimport { User } from '@supabase/supabase-js'\nimport { Link2, "
  },
  {
    "path": "components/user-text-section.tsx",
    "chars": 4100,
    "preview": "'use client'\n\nimport React, { useEffect, useRef, useState } from 'react'\nimport TextareaAutosize from 'react-textarea-au"
  },
  {
    "path": "components/video-carousel-dialog.tsx",
    "chars": 4189,
    "preview": "'use client'\n\nimport { useEffect, useRef, useState } from 'react'\n\nimport { SerperSearchResultItem } from '@/lib/types'\n"
  },
  {
    "path": "components/video-result-grid.tsx",
    "chars": 4151,
    "preview": "'use client'\n\nimport Image from 'next/image'\n\nimport { PlusCircle } from 'lucide-react'\n\nimport { SerperSearchResultItem"
  },
  {
    "path": "components/video-search-results.tsx",
    "chars": 1300,
    "preview": "/* eslint-disable @next/next/no-img-element */\n'use client'\n\nimport { SerperSearchResultItem, SerperSearchResults } from"
  },
  {
    "path": "components.json",
    "chars": 444,
    "preview": "{\n  \"$schema\": \"https://ui.shadcn.com/schema.json\",\n  \"style\": \"default\",\n  \"rsc\": true,\n  \"tsx\": true,\n  \"tailwind\": {\n"
  },
  {
    "path": "config/models/cloud.json",
    "chars": 1672,
    "preview": "{\n  \"version\": 1,\n  \"models\": {\n    \"byMode\": {\n      \"quick\": {\n        \"speed\": {\n          \"id\": \"gemini-3.1-flash-li"
  },
  {
    "path": "config/models/default.json",
    "chars": 1528,
    "preview": "{\n  \"version\": 1,\n  \"models\": {\n    \"byMode\": {\n      \"quick\": {\n        \"speed\": {\n          \"id\": \"gpt-5-mini\",\n      "
  },
  {
    "path": "docker-compose.yaml",
    "chars": 2098,
    "preview": "# Docker Compose configuration for the morphic-stack development environment\n\nname: morphic-stack\nservices:\n  morphic:\n "
  },
  {
    "path": "docs/CONFIGURATION.md",
    "chars": 10562,
    "preview": "# Configuration Guide\n\nThis guide covers the optional features and their configuration in Morphic.\n\n## Table of Contents"
  },
  {
    "path": "docs/DOCKER.md",
    "chars": 2830,
    "preview": "# Docker Guide\n\nThis guide covers running Morphic with Docker, including development setup, prebuilt images, and deploym"
  },
  {
    "path": "drizzle/0000_black_lifeguard.sql",
    "chars": 3146,
    "preview": "CREATE TABLE \"chats\" (\n\t\"id\" varchar(191) PRIMARY KEY NOT NULL,\n\t\"created_at\" timestamp DEFAULT now() NOT NULL,\n\t\"title\""
  },
  {
    "path": "drizzle/0001_thin_supreme_intelligence.sql",
    "chars": 132,
    "preview": "ALTER TABLE \"messages\" ADD COLUMN \"updated_at\" timestamp;--> statement-breakpoint\nALTER TABLE \"messages\" ADD COLUMN \"met"
  },
  {
    "path": "drizzle/0002_material_crystal.sql",
    "chars": 67,
    "preview": "ALTER TABLE \"messages\" ALTER COLUMN \"metadata\" SET DATA TYPE jsonb;"
  },
  {
    "path": "drizzle/0003_heavy_whirlwind.sql",
    "chars": 455,
    "preview": "CREATE TABLE \"feedback\" (\n\t\"id\" varchar(191) PRIMARY KEY NOT NULL,\n\t\"user_id\" varchar(255),\n\t\"sentiment\" varchar(256) NO"
  },
  {
    "path": "drizzle/0004_natural_wallow.sql",
    "chars": 316,
    "preview": "CREATE INDEX \"chats_user_id_idx\" ON \"chats\" USING btree (\"user_id\");--> statement-breakpoint\nCREATE INDEX \"chats_user_id"
  },
  {
    "path": "drizzle/0005_awesome_riptide.sql",
    "chars": 400,
    "preview": "ALTER TABLE \"chats\" ENABLE ROW LEVEL SECURITY;--> statement-breakpoint\nALTER TABLE \"feedback\" ENABLE ROW LEVEL SECURITY;"
  },
  {
    "path": "drizzle/0006_brainy_wrecking_crew.sql",
    "chars": 1250,
    "preview": "CREATE POLICY \"users_manage_own_chats\" ON \"chats\" AS PERMISSIVE FOR ALL TO public USING (user_id = current_setting('app."
  },
  {
    "path": "drizzle/0007_illegal_mephistopheles.sql",
    "chars": 683,
    "preview": "CREATE POLICY \"public_chats_readable\" ON \"chats\" AS PERMISSIVE FOR SELECT TO public USING (visibility = 'public');--> st"
  },
  {
    "path": "drizzle/0008_glamorous_riptide.sql",
    "chars": 49,
    "preview": "ALTER TABLE \"feedback\" ENABLE ROW LEVEL SECURITY;"
  },
  {
    "path": "drizzle/0009_thankful_may_parker.sql",
    "chars": 76,
    "preview": "CREATE INDEX \"chats_id_user_id_idx\" ON \"chats\" USING btree (\"id\",\"user_id\");"
  },
  {
    "path": "drizzle/0010_lonely_kang.sql",
    "chars": 294,
    "preview": "DO $$\nBEGIN\n  IF NOT EXISTS (\n    SELECT 1 FROM pg_policies\n    WHERE schemaname = 'public' AND tablename = 'feedback' A"
  },
  {
    "path": "drizzle/meta/0000_snapshot.json",
    "chars": 12821,
    "preview": "{\n  \"id\": \"bd2133a6-1281-44a1-8619-c1f0407dfb77\",\n  \"prevId\": \"00000000-0000-0000-0000-000000000000\",\n  \"version\": \"7\",\n"
  },
  {
    "path": "drizzle/meta/0001_snapshot.json",
    "chars": 13124,
    "preview": "{\n  \"id\": \"90075b6a-dc27-4224-beb5-1ce34abc7fc5\",\n  \"prevId\": \"bd2133a6-1281-44a1-8619-c1f0407dfb77\",\n  \"version\": \"7\",\n"
  },
  {
    "path": "drizzle/meta/0002_snapshot.json",
    "chars": 13125,
    "preview": "{\n  \"id\": \"8e9de712-037f-4132-8f06-b3d2f57dd68a\",\n  \"prevId\": \"90075b6a-dc27-4224-beb5-1ce34abc7fc5\",\n  \"version\": \"7\",\n"
  },
  {
    "path": "drizzle/meta/0003_snapshot.json",
    "chars": 15304,
    "preview": "{\n  \"id\": \"c9b8dd61-7cb9-4f3b-9e5f-811592e56563\",\n  \"prevId\": \"8e9de712-037f-4132-8f06-b3d2f57dd68a\",\n  \"version\": \"7\",\n"
  },
  {
    "path": "drizzle/meta/0004_snapshot.json",
    "chars": 16678,
    "preview": "{\n  \"id\": \"28e1690d-994a-4027-abe2-5f283bb4c720\",\n  \"prevId\": \"c9b8dd61-7cb9-4f3b-9e5f-811592e56563\",\n  \"version\": \"7\",\n"
  },
  {
    "path": "drizzle/meta/0005_snapshot.json",
    "chars": 16895,
    "preview": "{\n  \"id\": \"c2240142-0d69-482e-b6e0-a9561c421aa2\",\n  \"prevId\": \"28e1690d-994a-4027-abe2-5f283bb4c720\",\n  \"version\": \"7\",\n"
  },
  {
    "path": "drizzle/meta/0006_snapshot.json",
    "chars": 18523,
    "preview": "{\n  \"id\": \"60e18ccb-ad03-4315-bee3-286b827d26fe\",\n  \"prevId\": \"c2240142-0d69-482e-b6e0-a9561c421aa2\",\n  \"version\": \"7\",\n"
  },
  {
    "path": "drizzle/meta/0007_snapshot.json",
    "chars": 19502,
    "preview": "{\n  \"id\": \"a1b15544-fb28-4c05-82c2-8e81ea3d5689\",\n  \"prevId\": \"60e18ccb-ad03-4315-bee3-286b827d26fe\",\n  \"version\": \"7\",\n"
  },
  {
    "path": "drizzle/meta/0008_snapshot.json",
    "chars": 19501,
    "preview": "{\n  \"id\": \"c631d669-f529-4fda-8f77-1db7cc5310e4\",\n  \"prevId\": \"a1b15544-fb28-4c05-82c2-8e81ea3d5689\",\n  \"version\": \"7\",\n"
  },
  {
    "path": "drizzle/meta/0009_snapshot.json",
    "chars": 20054,
    "preview": "{\n  \"id\": \"09bf63d0-855c-4ef2-85b7-9f63220f2566\",\n  \"prevId\": \"c631d669-f529-4fda-8f77-1db7cc5310e4\",\n  \"version\": \"7\",\n"
  },
  {
    "path": "drizzle/meta/0010_snapshot.json",
    "chars": 20256,
    "preview": "{\n  \"id\": \"413889c5-099c-4a0c-8a67-b5b1d0605366\",\n  \"prevId\": \"09bf63d0-855c-4ef2-85b7-9f63220f2566\",\n  \"version\": \"7\",\n"
  },
  {
    "path": "drizzle/meta/_journal.json",
    "chars": 1665,
    "preview": "{\n  \"version\": \"7\",\n  \"dialect\": \"postgresql\",\n  \"entries\": [\n    {\n      \"idx\": 0,\n      \"version\": \"7\",\n      \"when\": "
  },
  {
    "path": "drizzle/relations.ts",
    "chars": 544,
    "preview": "import { relations } from 'drizzle-orm/relations'\n\nimport { chats, messages, parts } from './schema'\n\nexport const messa"
  },
  {
    "path": "drizzle/schema.ts",
    "chars": 7733,
    "preview": "import { sql } from 'drizzle-orm'\nimport {\n  check,\n  foreignKey,\n  index,\n  integer,\n  json,\n  jsonb,\n  pgPolicy,\n  pgT"
  },
  {
    "path": "drizzle.config.ts",
    "chars": 396,
    "preview": "import * as dotenv from 'dotenv'\nimport { defineConfig } from 'drizzle-kit'\n\nimport 'dotenv/config'\n\n// Load from .env.l"
  },
  {
    "path": "hooks/use-auth-check.tsx",
    "chars": 1139,
    "preview": "'use client'\n\nimport { useEffect, useState } from 'react'\n\nimport { User } from '@supabase/supabase-js'\n\nimport { create"
  },
  {
    "path": "hooks/use-current-user-image.ts",
    "chars": 613,
    "preview": "import { useEffect, useState } from 'react'\n\nimport { createClient } from '@/lib/supabase/client'\n\nexport const useCurre"
  },
  {
    "path": "hooks/use-current-user-name.ts",
    "chars": 632,
    "preview": "import { useEffect, useState } from 'react'\n\nimport { createClient } from '@/lib/supabase/client'\n\nexport const useCurre"
  },
  {
    "path": "hooks/use-file-dropzone.ts",
    "chars": 3078,
    "preview": "import { useCallback, useState } from 'react'\n\nimport { toast } from 'sonner'\n\nimport { UploadedFile } from '@/lib/types"
  },
  {
    "path": "hooks/use-mobile.tsx",
    "chars": 565,
    "preview": "import * as React from 'react'\n\nconst MOBILE_BREAKPOINT = 768\n\nexport function useIsMobile() {\n  const [isMobile, setIsM"
  },
  {
    "path": "instrumentation.ts",
    "chars": 582,
    "preview": "import { registerOTel } from '@vercel/otel'\nimport { LangfuseExporter } from 'langfuse-vercel'\n\nexport async function re"
  },
  {
    "path": "lib/actions/__tests__/chat.test.ts",
    "chars": 17535,
    "preview": "import { revalidateTag } from 'next/cache'\n\nimport { beforeEach, describe, expect, it, vi } from 'vitest'\n\nimport { gene"
  },
  {
    "path": "lib/actions/__tests__/feedback.test.ts",
    "chars": 6674,
    "preview": "import { beforeEach, describe, expect, it, vi } from 'vitest'\n\n// Mock the modules before any imports\nvi.mock('@/lib/db'"
  },
  {
    "path": "lib/actions/chat.ts",
    "chars": 8415,
    "preview": "'use server'\n\nimport { revalidateTag, unstable_cache } from 'next/cache'\n\nimport { generateChatTitle } from '@/lib/agent"
  },
  {
    "path": "lib/actions/feedback.ts",
    "chars": 2788,
    "preview": "'use server'\n\nimport { eq } from 'drizzle-orm'\nimport { Langfuse } from 'langfuse'\n\nimport { db } from '@/lib/db'\nimport"
  },
  {
    "path": "lib/actions/site-feedback.ts",
    "chars": 3745,
    "preview": "'use server'\n\nimport { db } from '@/lib/db'\nimport { feedback, generateId } from '@/lib/db/schema'\nimport { withOptional"
  },
  {
    "path": "lib/agents/generate-related-questions.ts",
    "chars": 1699,
    "preview": "import { type ModelMessage, Output, streamText } from 'ai'\n\nimport { getRelatedQuestionsModel } from '../config/model-ty"
  },
  {
    "path": "lib/agents/prompts/related-questions-prompt.ts",
    "chars": 1103,
    "preview": "export const RELATED_QUESTIONS_PROMPT = `You are a professional web researcher tasked with generating follow-up question"
  },
  {
    "path": "lib/agents/prompts/search-mode-prompts.ts",
    "chars": 18162,
    "preview": "import {\n  getContentTypesGuidance,\n  isGeneralSearchProviderAvailable\n} from '@/lib/utils/search-config'\n\n// Search mod"
  },
  {
    "path": "lib/agents/researcher.ts",
    "chars": 4953,
    "preview": "import { stepCountIs, tool, ToolLoopAgent } from 'ai'\n\nimport type { ResearcherTools } from '@/lib/types/agent'\nimport {"
  },
  {
    "path": "lib/agents/title-generator.ts",
    "chars": 2597,
    "preview": "import { generateText } from 'ai'\n\nimport { getModel } from '../utils/registry'\nimport { isTracingEnabled } from '../uti"
  },
  {
    "path": "lib/analytics/index.ts",
    "chars": 389,
    "preview": "/**\n * Analytics module\n *\n * Provides a unified interface for tracking analytics events.\n * Currently uses Vercel Analy"
  },
  {
    "path": "lib/analytics/track-chat-event.ts",
    "chars": 1670,
    "preview": "import { track } from '@vercel/analytics/server'\n\nimport type { ChatEventData } from './types'\n\n/**\n * Track a chat even"
  },
  {
    "path": "lib/analytics/types.ts",
    "chars": 1272,
    "preview": "/**\n * Analytics module types\n *\n * This module provides type definitions for analytics tracking.\n * The interfaces are "
  },
  {
    "path": "lib/analytics/utils.ts",
    "chars": 788,
    "preview": "import type { UIMessage } from 'ai'\n\n/**\n * Calculate the conversation turn number from message history\n *\n * The turn n"
  },
  {
    "path": "lib/auth/get-current-user.ts",
    "chars": 1449,
    "preview": "import { createClient } from '@/lib/supabase/server'\nimport { perfLog } from '@/lib/utils/perf-logging'\nimport { increme"
  }
]

// ... and 95 more files (download for full content)

About this extraction

This page contains the full source code of the miurla/morphic GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 295 files (1012.7 KB), approximately 257.9k tokens, and a symbol index with 611 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!