├── .cursor
└── rules
│ ├── create-db-functions.mdc
│ ├── create-migration.mdc
│ ├── create-rls-policies.mdc
│ ├── postgres-sql-style-guide.mdc
│ └── writing-supabase-edge-functions.mdc
├── .env.local.example
├── .eslintrc.json
├── .github
├── ISSUE_TEMPLATE
│ ├── bug_report.yml
│ └── feature_request.yml
└── workflows
│ └── docker-build.yml
├── .gitignore
├── .vscode
└── settings.json
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── Dockerfile
├── LICENSE
├── README.md
├── app
├── api
│ ├── advanced-search
│ │ └── route.ts
│ ├── chat
│ │ ├── [id]
│ │ │ └── route.ts
│ │ └── route.ts
│ └── chats
│ │ └── route.ts
├── auth
│ ├── confirm
│ │ └── route.ts
│ ├── error
│ │ └── page.tsx
│ ├── forgot-password
│ │ └── page.tsx
│ ├── login
│ │ └── page.tsx
│ ├── oauth
│ │ └── route.ts
│ ├── sign-up-success
│ │ └── page.tsx
│ ├── sign-up
│ │ └── page.tsx
│ └── update-password
│ │ └── page.tsx
├── favicon.ico
├── globals.css
├── layout.tsx
├── opengraph-image.png
├── page.tsx
├── search
│ ├── [id]
│ │ └── page.tsx
│ ├── loading.tsx
│ └── page.tsx
└── share
│ ├── [id]
│ └── page.tsx
│ └── loading.tsx
├── bun.lock
├── components.json
├── components
├── answer-section.tsx
├── app-sidebar.tsx
├── artifact
│ ├── artifact-content.tsx
│ ├── artifact-context.tsx
│ ├── artifact-root.tsx
│ ├── chat-artifact-container.tsx
│ ├── reasoning-content.tsx
│ ├── retrieve-artifact-content.tsx
│ ├── search-artifact-content.tsx
│ ├── tool-invocation-content.tsx
│ └── video-search-artifact-content.tsx
├── chat-messages.tsx
├── chat-panel.tsx
├── chat-share.tsx
├── chat.tsx
├── clear-history.tsx
├── collapsible-message.tsx
├── current-user-avatar.tsx
├── custom-link.tsx
├── default-skeleton.tsx
├── empty-screen.tsx
├── external-link-items.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-selector.tsx
├── question-confirmation.tsx
├── reasoning-section.tsx
├── related-questions.tsx
├── render-message.tsx
├── retrieve-section.tsx
├── retry-button.tsx
├── search-mode-toggle.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
├── theme-menu-items.tsx
├── theme-provider.tsx
├── tool-badge.tsx
├── tool-section.tsx
├── ui
│ ├── alert-dialog.tsx
│ ├── avatar.tsx
│ ├── badge.tsx
│ ├── button.tsx
│ ├── card.tsx
│ ├── carousel.tsx
│ ├── checkbox.tsx
│ ├── codeblock.tsx
│ ├── collapsible.tsx
│ ├── command.tsx
│ ├── dialog.tsx
│ ├── drawer.tsx
│ ├── dropdown-menu.tsx
│ ├── icons.tsx
│ ├── index.ts
│ ├── input.tsx
│ ├── label.tsx
│ ├── markdown.tsx
│ ├── popover.tsx
│ ├── resizable.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
├── user-menu.tsx
├── user-message.tsx
├── video-carousel-dialog.tsx
├── video-result-grid.tsx
├── video-search-results.tsx
└── video-search-section.tsx
├── docker-compose.yaml
├── docs
└── CONFIGURATION.md
├── hooks
├── use-current-user-image.ts
├── use-current-user-name.ts
└── use-mobile.tsx
├── lib
├── actions
│ └── chat.ts
├── agents
│ ├── generate-related-questions.ts
│ ├── manual-researcher.ts
│ └── researcher.ts
├── auth
│ └── get-current-user.ts
├── config
│ ├── default-models.json
│ └── models.ts
├── constants
│ └── index.ts
├── hooks
│ ├── use-copy-to-clipboard.ts
│ └── use-media-query.ts
├── redis
│ └── config.ts
├── schema
│ ├── question.ts
│ ├── related.tsx
│ ├── retrieve.tsx
│ └── search.tsx
├── streaming
│ ├── create-manual-tool-stream.ts
│ ├── create-tool-calling-stream.ts
│ ├── handle-stream-finish.ts
│ ├── parse-tool-call.ts
│ ├── tool-execution.ts
│ └── types.ts
├── supabase
│ ├── client.ts
│ ├── middleware.ts
│ └── server.ts
├── tools
│ ├── question.ts
│ ├── retrieve.ts
│ ├── search.ts
│ ├── search
│ │ └── providers
│ │ │ ├── base.ts
│ │ │ ├── exa.ts
│ │ │ ├── index.ts
│ │ │ ├── searxng.ts
│ │ │ └── tavily.ts
│ └── video-search.ts
├── types
│ ├── index.ts
│ └── models.ts
└── utils
│ ├── context-window.ts
│ ├── cookies.ts
│ ├── index.ts
│ ├── registry.ts
│ └── url.ts
├── middleware.ts
├── next.config.mjs
├── package.json
├── postcss.config.mjs
├── prettier.config.js
├── public
├── config
│ └── models.json
├── images
│ └── placeholder-image.png
├── providers
│ └── logos
│ │ ├── anthropic.svg
│ │ ├── azure.svg
│ │ ├── deepseek.svg
│ │ ├── fireworks.svg
│ │ ├── google.svg
│ │ ├── groq.svg
│ │ ├── ollama.svg
│ │ ├── openai-compatible.svg
│ │ ├── openai.svg
│ │ └── xai.svg
└── screenshot-2025-05-04.png
├── searxng-limiter.toml
├── searxng-settings.yml
├── tailwind.config.ts
└── tsconfig.json
/.cursor/rules/create-migration.mdc:
--------------------------------------------------------------------------------
1 | ---
2 | # Specify the following for Cursor rules
3 | description: Guidelines for writing Postgres migrations
4 | alwaysApply: false
5 | ---
6 |
7 | # Database: Create migration
8 |
9 | You are a Postgres Expert who loves creating secure database schemas.
10 |
11 | This project uses the migrations provided by the Supabase CLI.
12 |
13 | ## Creating a migration file
14 |
15 | Given the context of the user's message, create a database migration file inside the folder `supabase/migrations/`.
16 |
17 | The file MUST following this naming convention:
18 |
19 | The file MUST be named in the format `YYYYMMDDHHmmss_short_description.sql` with proper casing for months, minutes, and seconds in UTC time:
20 |
21 | 1. `YYYY` - Four digits for the year (e.g., `2024`).
22 | 2. `MM` - Two digits for the month (01 to 12).
23 | 3. `DD` - Two digits for the day of the month (01 to 31).
24 | 4. `HH` - Two digits for the hour in 24-hour format (00 to 23).
25 | 5. `mm` - Two digits for the minute (00 to 59).
26 | 6. `ss` - Two digits for the second (00 to 59).
27 | 7. Add an appropriate description for the migration.
28 |
29 | For example:
30 |
31 | ```
32 | 20240906123045_create_profiles.sql
33 | ```
34 |
35 | ## SQL Guidelines
36 |
37 | Write Postgres-compatible SQL code for Supabase migration files that:
38 |
39 | - Includes a header comment with metadata about the migration, such as the purpose, affected tables/columns, and any special considerations.
40 | - Includes thorough comments explaining the purpose and expected behavior of each migration step.
41 | - Write all SQL in lowercase.
42 | - Add copious comments for any destructive SQL commands, including truncating, dropping, or column alterations.
43 | - When creating a new table, you MUST enable Row Level Security (RLS) even if the table is intended for public access.
44 | - When creating RLS Policies
45 | - Ensure the policies cover all relevant access scenarios (e.g. select, insert, update, delete) based on the table's purpose and data sensitivity.
46 | - If the table is intended for public access the policy can simply return `true`.
47 | - RLS Policies should be granular: one policy for `select`, one for `insert` etc) and for each supabase role (`anon` and `authenticated`). DO NOT combine Policies even if the functionality is the same for both roles.
48 | - Include comments explaining the rationale and intended behavior of each security policy
49 |
50 | The generated SQL code should be production-ready, well-documented, and aligned with Supabase's best practices.
51 |
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "next/core-web-vitals"
3 | }
4 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.yml:
--------------------------------------------------------------------------------
1 | name: 🐞 Bug
2 | description: File a bug/issue
3 | title: '[BUG]
'
4 | labels: ['Bug', 'Needs Triage']
5 | body:
6 | - type: checkboxes
7 | attributes:
8 | label: Is there an existing issue for this?
9 | description: Please search to see if an issue already exists for the bug you encountered.
10 | options:
11 | - label: I have searched the existing issues
12 | required: true
13 | - type: checkboxes
14 | attributes:
15 | label: Vercel Runtime Logs
16 | description: If this is a Vercel environment issue, have you checked the Vercel Runtime Logs? (https://vercel.com/docs/observability/runtime-logs)
17 | options:
18 | - label: I have checked the Vercel Runtime Logs for errors (if applicable)
19 | required: false
20 | - type: textarea
21 | attributes:
22 | label: Current Behavior
23 | description: A concise description of what you're experiencing.
24 | validations:
25 | required: true
26 | - type: textarea
27 | attributes:
28 | label: Expected Behavior
29 | description: A concise description of what you expected to happen.
30 | validations:
31 | required: true
32 | - type: textarea
33 | attributes:
34 | label: Steps To Reproduce
35 | description: Steps to reproduce the behavior.
36 | placeholder: |
37 | 1. In this environment...
38 | 2. With this config...
39 | 3. Run '...'
40 | 4. See error...
41 | validations:
42 | required: true
43 | - type: textarea
44 | attributes:
45 | label: Environment
46 | description: |
47 | examples:
48 | - Browser: Chrome 52.0.2743.116
49 | value: |
50 | - OS:
51 | - Browser:
52 | render: markdown
53 | validations:
54 | required: true
55 | - type: textarea
56 | attributes:
57 | label: Anything else?
58 | description: |
59 | Links? References? Anything that will give us more context about the issue you are encountering!
60 |
61 | Tip: You can attach images or log files by clicking this area to highlight it and then dragging files in.
62 | validations:
63 | required: false
64 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.yml:
--------------------------------------------------------------------------------
1 | name: ✨ Feature Request
2 | description: Propose a new feature for Morphic.
3 | labels: []
4 | body:
5 | - type: markdown
6 | attributes:
7 | value: |
8 | This template is to propose new features for Morphic. Please fill out the following information to help us understand your feature request.
9 | - type: textarea
10 | attributes:
11 | label: Feature Description
12 | description: A detailed description of the feature you are proposing for Morphic. Include any relevant technical details.
13 | placeholder: |
14 | Feature description...
15 | validations:
16 | required: true
17 | - type: textarea
18 | attributes:
19 | label: Use Case
20 | description: Provide a use case where this feature would be beneficial
21 | placeholder: |
22 | Use case...
23 | validations:
24 | required: true
25 | - type: textarea
26 | attributes:
27 | label: Additional context
28 | description: |
29 | Any extra information that might help us understand your feature request.
30 | placeholder: |
31 | Additional context...
32 |
--------------------------------------------------------------------------------
/.github/workflows/docker-build.yml:
--------------------------------------------------------------------------------
1 | name: Docker Build and Push
2 |
3 | on:
4 | push:
5 | branches: [main]
6 |
7 | jobs:
8 | build:
9 | runs-on: ubuntu-latest
10 | permissions:
11 | contents: read
12 | packages: write
13 |
14 | steps:
15 | - name: Checkout repository
16 | uses: actions/checkout@v4
17 |
18 | - name: Set up QEMU
19 | uses: docker/setup-qemu-action@v3
20 |
21 | - name: Set up Docker Buildx
22 | uses: docker/setup-buildx-action@v3
23 |
24 | - name: Login to GitHub Container Registry
25 | uses: docker/login-action@v3
26 | with:
27 | registry: ghcr.io
28 | username: ${{ github.actor }}
29 | password: ${{ secrets.GITHUB_TOKEN }}
30 |
31 | - name: Build and push Docker image
32 | uses: docker/build-push-action@v5
33 | with:
34 | context: .
35 | platforms: linux/amd64,linux/arm64
36 | push: true
37 | tags: |
38 | ghcr.io/${{ github.repository }}:latest
39 | ghcr.io/${{ github.repository }}:${{ github.sha }}
40 | cache-from: type=gha
41 | cache-to: type=gha,mode=max
42 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 | .yarn/install-state.gz
8 |
9 | # testing
10 | /coverage
11 |
12 | # next.js
13 | /.next/
14 | /out/
15 |
16 | # production
17 | /build
18 |
19 | # misc
20 | .DS_Store
21 | *.pem
22 |
23 | # debug
24 | npm-debug.log*
25 | yarn-debug.log*
26 | yarn-error.log*
27 |
28 | # local env files
29 | .env*.local
30 |
31 | # vercel
32 | .vercel
33 |
34 | # typescript
35 | *.tsbuildinfo
36 | next-env.d.ts
37 |
38 | # cursor
39 | .cursor/rules/private
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "editor.formatOnSave": true,
3 | "editor.defaultFormatter": "esbenp.prettier-vscode",
4 | "cSpell.words": ["openai", "Tavily"],
5 | "editor.codeActionsOnSave": {
6 | "source.organizeImports": "always"
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | We as members, contributors, and leaders pledge to make participation in our
6 | community a harassment-free experience for everyone, regardless of age, body
7 | size, visible or invisible disability, ethnicity, sex characteristics, gender
8 | identity and expression, level of experience, education, socio-economic status,
9 | nationality, personal appearance, race, religion, or sexual identity
10 | and orientation.
11 |
12 | We pledge to act and interact in ways that contribute to an open, welcoming,
13 | diverse, inclusive, and healthy community.
14 |
15 | ## Our Standards
16 |
17 | Examples of behavior that contributes to a positive environment for our
18 | community include:
19 |
20 | - Demonstrating empathy and kindness toward other people
21 | - Being respectful of differing opinions, viewpoints, and experiences
22 | - Giving and gracefully accepting constructive feedback
23 | - Accepting responsibility and apologizing to those affected by our mistakes,
24 | and learning from the experience
25 | - Focusing on what is best not just for us as individuals, but for the
26 | overall community
27 |
28 | Examples of unacceptable behavior include:
29 |
30 | - The use of sexualized language or imagery, and sexual attention or
31 | advances of any kind
32 | - Trolling, insulting or derogatory comments, and personal or political attacks
33 | - Public or private harassment
34 | - Publishing others' private information, such as a physical or email
35 | address, without their explicit permission
36 | - Other conduct which could reasonably be considered inappropriate in a
37 | professional setting
38 |
39 | ## Enforcement Responsibilities
40 |
41 | Community leaders are responsible for clarifying and enforcing our standards of
42 | acceptable behavior and will take appropriate and fair corrective action in
43 | response to any behavior that they deem inappropriate, threatening, offensive,
44 | or harmful.
45 |
46 | ## Scope
47 |
48 | This Code of Conduct applies within all community spaces, and also applies when
49 | an individual is officially representing the community in public spaces.
50 |
51 | ## Enforcement
52 |
53 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
54 | reported to the community leaders responsible for enforcement at
55 | [INSERT CONTACT METHOD].
56 | All complaints will be reviewed and investigated promptly and fairly.
57 |
58 | ## Attribution
59 |
60 | This Code of Conduct is adapted from the [Contributor Covenant][homepage],
61 | version 2.0, available at
62 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
63 |
64 | [homepage]: https://www.contributor-covenant.org
65 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing to Morphic
2 |
3 | Thank you for your interest in contributing to Morphic! This document provides guidelines and instructions for contributing.
4 |
5 | ## Code of Conduct
6 |
7 | By participating in this project, you are expected to uphold our [Code of Conduct](CODE_OF_CONDUCT.md).
8 |
9 | ## How to Contribute
10 |
11 | ### Reporting Issues
12 |
13 | - Check if the issue already exists in our [GitHub Issues](https://github.com/miurla/morphic/issues)
14 | - Use the issue templates when creating a new issue
15 | - Provide as much context as possible
16 |
17 | ### Pull Requests
18 |
19 | 1. Fork the repository
20 | 2. Create a new branch from `main`:
21 | ```bash
22 | git checkout -b feat/your-feature-name
23 | ```
24 | 3. Make your changes
25 | 4. Commit your changes using conventional commits:
26 | ```bash
27 | git commit -m "feat: add new feature"
28 | ```
29 | 5. Push to your fork
30 | 6. Open a Pull Request
31 |
32 | ### Commit Convention
33 |
34 | We use conventional commits. Examples:
35 |
36 | - `feat: add new feature`
37 | - `fix: resolve issue with X`
38 | - `docs: update README`
39 | - `chore: update dependencies`
40 | - `refactor: improve code structure`
41 |
42 | ### Development Setup
43 |
44 | Follow the [Quickstart](README.md#-quickstart) guide in the README to set up your development environment.
45 |
46 | ## License
47 |
48 | By contributing, you agree that your contributions will be licensed under the Apache-2.0 License.
49 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | # Base image
2 | FROM oven/bun:1.2.12 AS builder
3 |
4 | WORKDIR /app
5 |
6 | # Install dependencies (separated for better cache utilization)
7 | COPY package.json bun.lock ./
8 | RUN bun install
9 |
10 | # Copy source code and build
11 | COPY . .
12 | RUN bun next telemetry disable
13 | RUN bun run build
14 |
15 | # Runtime stage
16 | FROM oven/bun:1.2.12 AS runner
17 | WORKDIR /app
18 |
19 | # Copy only necessary files from builder
20 | COPY --from=builder /app/.next ./.next
21 | COPY --from=builder /app/public ./public
22 | COPY --from=builder /app/package.json ./package.json
23 | COPY --from=builder /app/bun.lock ./bun.lock
24 | COPY --from=builder /app/node_modules ./node_modules
25 |
26 | # Start production server
27 | CMD ["bun", "start", "-H", "0.0.0.0"]
28 |
--------------------------------------------------------------------------------
/app/api/chat/[id]/route.ts:
--------------------------------------------------------------------------------
1 | import { deleteChat } from '@/lib/actions/chat'
2 | import { getCurrentUserId } from '@/lib/auth/get-current-user'
3 | import { NextRequest, NextResponse } from 'next/server'
4 |
5 | export async function DELETE(
6 | request: NextRequest,
7 | { params }: { params: Promise<{ id: string }> }
8 | ) {
9 | const enableSaveChatHistory = process.env.ENABLE_SAVE_CHAT_HISTORY === 'true'
10 | if (!enableSaveChatHistory) {
11 | return NextResponse.json(
12 | { error: 'Chat history saving is disabled.' },
13 | { status: 403 }
14 | )
15 | }
16 |
17 | const chatId = (await params).id
18 | if (!chatId) {
19 | return NextResponse.json({ error: 'Chat ID is required' }, { status: 400 })
20 | }
21 |
22 | const userId = await getCurrentUserId()
23 |
24 | try {
25 | const result = await deleteChat(chatId, userId)
26 |
27 | if (result.error) {
28 | const statusCode = result.error === 'Chat not found' ? 404 : 500
29 | return NextResponse.json({ error: result.error }, { status: statusCode })
30 | }
31 |
32 | return NextResponse.json({ ok: true })
33 | } catch (error) {
34 | console.error(`API route error deleting chat ${chatId}:`, error)
35 | return NextResponse.json(
36 | { error: 'Internal Server Error' },
37 | { status: 500 }
38 | )
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/app/api/chat/route.ts:
--------------------------------------------------------------------------------
1 | import { getCurrentUserId } from '@/lib/auth/get-current-user'
2 | import { createManualToolStreamResponse } from '@/lib/streaming/create-manual-tool-stream'
3 | import { createToolCallingStreamResponse } from '@/lib/streaming/create-tool-calling-stream'
4 | import { Model } from '@/lib/types/models'
5 | import { isProviderEnabled } from '@/lib/utils/registry'
6 | import { cookies } from 'next/headers'
7 |
8 | export const maxDuration = 30
9 |
10 | const DEFAULT_MODEL: Model = {
11 | id: 'gpt-4o-mini',
12 | name: 'GPT-4o mini',
13 | provider: 'OpenAI',
14 | providerId: 'openai',
15 | enabled: true,
16 | toolCallType: 'native'
17 | }
18 |
19 | export async function POST(req: Request) {
20 | try {
21 | const { messages, id: chatId } = await req.json()
22 | const referer = req.headers.get('referer')
23 | const isSharePage = referer?.includes('/share/')
24 | const userId = await getCurrentUserId()
25 |
26 | if (isSharePage) {
27 | return new Response('Chat API is not available on share pages', {
28 | status: 403,
29 | statusText: 'Forbidden'
30 | })
31 | }
32 |
33 | const cookieStore = await cookies()
34 | const modelJson = cookieStore.get('selectedModel')?.value
35 | const searchMode = cookieStore.get('search-mode')?.value === 'true'
36 |
37 | let selectedModel = DEFAULT_MODEL
38 |
39 | if (modelJson) {
40 | try {
41 | selectedModel = JSON.parse(modelJson) as Model
42 | } catch (e) {
43 | console.error('Failed to parse selected model:', e)
44 | }
45 | }
46 |
47 | if (
48 | !isProviderEnabled(selectedModel.providerId) ||
49 | selectedModel.enabled === false
50 | ) {
51 | return new Response(
52 | `Selected provider is not enabled ${selectedModel.providerId}`,
53 | {
54 | status: 404,
55 | statusText: 'Not Found'
56 | }
57 | )
58 | }
59 |
60 | const supportsToolCalling = selectedModel.toolCallType === 'native'
61 |
62 | return supportsToolCalling
63 | ? createToolCallingStreamResponse({
64 | messages,
65 | model: selectedModel,
66 | chatId,
67 | searchMode,
68 | userId
69 | })
70 | : createManualToolStreamResponse({
71 | messages,
72 | model: selectedModel,
73 | chatId,
74 | searchMode,
75 | userId
76 | })
77 | } catch (error) {
78 | console.error('API route error:', error)
79 | return new Response('Error processing your request', {
80 | status: 500,
81 | statusText: 'Internal Server Error'
82 | })
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/app/api/chats/route.ts:
--------------------------------------------------------------------------------
1 | import { getChatsPage } from '@/lib/actions/chat'
2 | import { getCurrentUserId } from '@/lib/auth/get-current-user'
3 | import { type Chat } from '@/lib/types'
4 | import { NextRequest, NextResponse } from 'next/server'
5 |
6 | interface ChatPageResponse {
7 | chats: Chat[]
8 | nextOffset: number | null
9 | }
10 |
11 | export async function GET(request: NextRequest) {
12 | const enableSaveChatHistory = process.env.ENABLE_SAVE_CHAT_HISTORY === 'true'
13 | if (!enableSaveChatHistory) {
14 | return NextResponse.json({ chats: [], nextOffset: null })
15 | }
16 |
17 | const { searchParams } = new URL(request.url)
18 | const offset = parseInt(searchParams.get('offset') || '0', 10)
19 | const limit = parseInt(searchParams.get('limit') || '20', 10)
20 |
21 | const userId = await getCurrentUserId()
22 |
23 | try {
24 | const result = await getChatsPage(userId, limit, offset)
25 | return NextResponse.json(result)
26 | } catch (error) {
27 | console.error('API route error fetching chats:', error)
28 | return NextResponse.json(
29 | { chats: [], nextOffset: null },
30 | { status: 500 }
31 | )
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/app/auth/confirm/route.ts:
--------------------------------------------------------------------------------
1 | import { createClient } from '@/lib/supabase/server'
2 | import { type EmailOtpType } from '@supabase/supabase-js'
3 | import { redirect } from 'next/navigation'
4 | import { type NextRequest } from 'next/server'
5 |
6 | export async function GET(request: NextRequest) {
7 | const { searchParams } = new URL(request.url)
8 | const token_hash = searchParams.get('token_hash')
9 | const type = searchParams.get('type') as EmailOtpType | null
10 | const next = searchParams.get('next') ?? '/'
11 |
12 | if (token_hash && type) {
13 | const supabase = await createClient()
14 |
15 | const { error } = await supabase.auth.verifyOtp({
16 | type,
17 | token_hash,
18 | })
19 | if (!error) {
20 | // redirect user to specified redirect URL or root of app
21 | redirect(next)
22 | } else {
23 | // redirect the user to an error page with some instructions
24 | redirect(`/auth/error?error=${error?.message}`)
25 | }
26 | }
27 |
28 | // redirect the user to an error page with some instructions
29 | redirect(`/auth/error?error=No token hash or type`)
30 | }
31 |
--------------------------------------------------------------------------------
/app/auth/error/page.tsx:
--------------------------------------------------------------------------------
1 | import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
2 |
3 | export default async function Page({ searchParams }: { searchParams: Promise<{ error: string }> }) {
4 | const params = await searchParams
5 |
6 | return (
7 |
8 |
9 |
10 |
11 |
12 | Sorry, something went wrong.
13 |
14 |
15 | {params?.error ? (
16 | Code error: {params.error}
17 | ) : (
18 | An unspecified error occurred.
19 | )}
20 |
21 |
22 |
23 |
24 |
25 | )
26 | }
27 |
--------------------------------------------------------------------------------
/app/auth/forgot-password/page.tsx:
--------------------------------------------------------------------------------
1 | import { ForgotPasswordForm } from '@/components/forgot-password-form'
2 |
3 | export default function Page() {
4 | return (
5 |
10 | )
11 | }
12 |
--------------------------------------------------------------------------------
/app/auth/login/page.tsx:
--------------------------------------------------------------------------------
1 | import { LoginForm } from '@/components/login-form'
2 |
3 | export default function Page() {
4 | return (
5 |
10 | )
11 | }
12 |
--------------------------------------------------------------------------------
/app/auth/oauth/route.ts:
--------------------------------------------------------------------------------
1 | import { NextResponse } from 'next/server'
2 | // The client you created from the Server-Side Auth instructions
3 | import { createClient } from '@/lib/supabase/server'
4 |
5 | export async function GET(request: Request) {
6 | const { searchParams, origin } = new URL(request.url)
7 | const code = searchParams.get('code')
8 | // if "next" is in param, use it as the redirect URL
9 | const next = searchParams.get('next') ?? '/'
10 |
11 | if (code) {
12 | const supabase = await createClient()
13 | const { error } = await supabase.auth.exchangeCodeForSession(code)
14 | if (!error) {
15 | const forwardedHost = request.headers.get('x-forwarded-host') // original origin before load balancer
16 | const isLocalEnv = process.env.NODE_ENV === 'development'
17 | if (isLocalEnv) {
18 | // we can be sure that there is no load balancer in between, so no need to watch for X-Forwarded-Host
19 | return NextResponse.redirect(`${origin}${next}`)
20 | } else if (forwardedHost) {
21 | return NextResponse.redirect(`https://${forwardedHost}${next}`)
22 | } else {
23 | return NextResponse.redirect(`${origin}${next}`)
24 | }
25 | }
26 | }
27 |
28 | // return the user to an error page with instructions
29 | return NextResponse.redirect(`${origin}/auth/error`)
30 | }
31 |
--------------------------------------------------------------------------------
/app/auth/sign-up-success/page.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | Card,
3 | CardContent,
4 | CardDescription,
5 | CardHeader,
6 | CardTitle,
7 | } from '@/components/ui/card'
8 |
9 | export default function Page() {
10 | return (
11 |
12 |
13 |
14 |
15 |
16 | Thank you for signing up!
17 | Check your email to confirm
18 |
19 |
20 |
21 | You've successfully signed up. Please check your email to confirm your account
22 | before signing in.
23 |
24 |
25 |
26 |
27 |
28 |
29 | )
30 | }
31 |
--------------------------------------------------------------------------------
/app/auth/sign-up/page.tsx:
--------------------------------------------------------------------------------
1 | import { SignUpForm } from '@/components/sign-up-form'
2 |
3 | export default function Page() {
4 | return (
5 |
10 | )
11 | }
12 |
--------------------------------------------------------------------------------
/app/auth/update-password/page.tsx:
--------------------------------------------------------------------------------
1 | import { UpdatePasswordForm } from '@/components/update-password-form'
2 |
3 | export default function Page() {
4 | return (
5 |
10 | )
11 | }
12 |
--------------------------------------------------------------------------------
/app/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/miurla/morphic/4800cae1f6882c2dd95a050536cfba59fee05b0a/app/favicon.ico
--------------------------------------------------------------------------------
/app/globals.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | @layer base {
6 | :root {
7 | --background: 0 0% 100%;
8 | --foreground: 0 0% 3.9%;
9 |
10 | --card: 0 0% 96.1%;
11 | --card-foreground: 0 0% 45.1%;
12 |
13 | --popover: 0 0% 100%;
14 | --popover-foreground: 0 0% 3.9%;
15 |
16 | --primary: 0 0% 9%;
17 | --primary-foreground: 0 0% 98%;
18 |
19 | --secondary: 0 0% 96.1%;
20 | --secondary-foreground: 0 0% 9%;
21 |
22 | --muted: 0 0% 96.1%;
23 | --muted-foreground: 0 0% 45.1%;
24 |
25 | --accent: 0 0% 96.1%;
26 | --accent-foreground: 0 0% 9%;
27 |
28 | --destructive: 0 84.2% 60.2%;
29 | --destructive-foreground: 0 0% 98%;
30 |
31 | --border: 0 0% 89.8%;
32 | --input: 0 0% 89.8%;
33 | --ring: 0 0% 89.8%;
34 |
35 | --radius: 0.5rem;
36 |
37 | --chart-1: 12 76% 61%;
38 |
39 | --chart-2: 173 58% 39%;
40 |
41 | --chart-3: 197 37% 24%;
42 |
43 | --chart-4: 43 74% 66%;
44 |
45 | --chart-5: 27 87% 67%;
46 |
47 | --accent-blue: 210 100% 97%;
48 | --accent-blue-foreground: 210 100% 50%;
49 | --accent-blue-border: 210 100% 90%;
50 | --sidebar-background: 0 0% 98%;
51 | --sidebar-foreground: 240 5.3% 26.1%;
52 | --sidebar-primary: 240 5.9% 10%;
53 | --sidebar-primary-foreground: 0 0% 98%;
54 | --sidebar-accent: 240 4.8% 95.9%;
55 | --sidebar-accent-foreground: 240 5.9% 10%;
56 | --sidebar-border: 220 13% 91%;
57 | --sidebar-ring: 217.2 91.2% 59.8%;
58 | }
59 |
60 | .dark {
61 | --background: 0 0% 3.9%;
62 | --foreground: 0 0% 98%;
63 |
64 | --card: 0 0% 14.9%;
65 | --card-foreground: 0 0% 63.9%;
66 |
67 | --popover: 0 0% 3.9%;
68 | --popover-foreground: 0 0% 98%;
69 |
70 | --primary: 0 0% 98%;
71 | --primary-foreground: 0 0% 9%;
72 |
73 | --secondary: 0 0% 14.9%;
74 | --secondary-foreground: 0 0% 98%;
75 |
76 | --muted: 0 0% 14.9%;
77 | --muted-foreground: 0 0% 63.9%;
78 |
79 | --accent: 0 0% 14.9%;
80 | --accent-foreground: 0 0% 98%;
81 |
82 | --destructive: 0 62.8% 30.6%;
83 | --destructive-foreground: 0 0% 98%;
84 |
85 | --border: 0 0% 14.9%;
86 | --input: 0 0% 14.9%;
87 | --ring: 0 0% 14.9%;
88 | --chart-1: 220 70% 50%;
89 | --chart-2: 160 60% 45%;
90 | --chart-3: 30 80% 55%;
91 | --chart-4: 280 65% 60%;
92 | --chart-5: 340 75% 55%;
93 |
94 | --accent-blue: 210 100% 10%;
95 | --accent-blue-foreground: 210 100% 80%;
96 | --accent-blue-border: 210 100% 25%;
97 | --sidebar-background: 240 5.9% 10%;
98 | --sidebar-foreground: 240 4.8% 95.9%;
99 | --sidebar-primary: 224.3 76.3% 48%;
100 | --sidebar-primary-foreground: 0 0% 100%;
101 | --sidebar-accent: 240 3.7% 15.9%;
102 | --sidebar-accent-foreground: 240 4.8% 95.9%;
103 | --sidebar-border: 240 3.7% 15.9%;
104 | --sidebar-ring: 217.2 91.2% 59.8%;
105 | }
106 | }
107 |
108 | @layer base {
109 | * {
110 | @apply border-border;
111 | }
112 | body {
113 | @apply bg-background text-foreground;
114 | }
115 | }
116 |
--------------------------------------------------------------------------------
/app/layout.tsx:
--------------------------------------------------------------------------------
1 | import AppSidebar from '@/components/app-sidebar'
2 | import ArtifactRoot from '@/components/artifact/artifact-root'
3 | import Header from '@/components/header'
4 | import { ThemeProvider } from '@/components/theme-provider'
5 | import { SidebarProvider } from '@/components/ui/sidebar'
6 | import { Toaster } from '@/components/ui/sonner'
7 | import { createClient } from '@/lib/supabase/server'
8 | import { cn } from '@/lib/utils'
9 | import { Analytics } from '@vercel/analytics/next'
10 | import type { Metadata, Viewport } from 'next'
11 | import { Inter as FontSans } from 'next/font/google'
12 | import './globals.css'
13 |
14 | const fontSans = FontSans({
15 | subsets: ['latin'],
16 | variable: '--font-sans'
17 | })
18 |
19 | const title = 'Morphic'
20 | const description =
21 | 'A fully open-source AI-powered answer engine with a generative UI.'
22 |
23 | export const metadata: Metadata = {
24 | metadataBase: new URL('https://morphic.sh'),
25 | title,
26 | description,
27 | openGraph: {
28 | title,
29 | description
30 | },
31 | twitter: {
32 | title,
33 | description,
34 | card: 'summary_large_image',
35 | creator: '@miiura'
36 | }
37 | }
38 |
39 | export const viewport: Viewport = {
40 | width: 'device-width',
41 | initialScale: 1,
42 | minimumScale: 1,
43 | maximumScale: 1
44 | }
45 |
46 | export default async function RootLayout({
47 | children
48 | }: Readonly<{
49 | children: React.ReactNode
50 | }>) {
51 | let user = null
52 | const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL
53 | const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY
54 |
55 | if (supabaseUrl && supabaseAnonKey) {
56 | const supabase = await createClient()
57 | const {
58 | data: { user: supabaseUser }
59 | } = await supabase.auth.getUser()
60 | user = supabaseUser
61 | }
62 |
63 | return (
64 |
65 |
71 |
77 |
78 |
79 |
80 |
81 |
82 | {children}
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 | )
92 | }
93 |
--------------------------------------------------------------------------------
/app/opengraph-image.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/miurla/morphic/4800cae1f6882c2dd95a050536cfba59fee05b0a/app/opengraph-image.png
--------------------------------------------------------------------------------
/app/page.tsx:
--------------------------------------------------------------------------------
1 | import { Chat } from '@/components/chat'
2 | import { getModels } from '@/lib/config/models'
3 | import { generateId } from 'ai'
4 |
5 | export default async function Page() {
6 | const id = generateId()
7 | const models = await getModels()
8 | return
9 | }
10 |
--------------------------------------------------------------------------------
/app/search/[id]/page.tsx:
--------------------------------------------------------------------------------
1 | import { Chat } from '@/components/chat'
2 | import { getChat } from '@/lib/actions/chat'
3 | import { getCurrentUserId } from '@/lib/auth/get-current-user'
4 | import { getModels } from '@/lib/config/models'
5 | import { convertToUIMessages } from '@/lib/utils'
6 | import { notFound, redirect } from 'next/navigation'
7 |
8 | export const maxDuration = 60
9 |
10 | export async function generateMetadata(props: {
11 | params: Promise<{ id: string }>
12 | }) {
13 | const { id } = await props.params
14 | const userId = await getCurrentUserId()
15 | const chat = await getChat(id, userId)
16 | return {
17 | title: chat?.title.toString().slice(0, 50) || 'Search'
18 | }
19 | }
20 |
21 | export default async function SearchPage(props: {
22 | params: Promise<{ id: string }>
23 | }) {
24 | const userId = await getCurrentUserId()
25 | const { id } = await props.params
26 |
27 | const chat = await getChat(id, userId)
28 | // convertToUIMessages for useChat hook
29 | const messages = convertToUIMessages(chat?.messages || [])
30 |
31 | if (!chat) {
32 | redirect('/')
33 | }
34 |
35 | if (chat?.userId !== userId && chat?.userId !== 'anonymous') {
36 | notFound()
37 | }
38 |
39 | const models = await getModels()
40 | return
41 | }
42 |
--------------------------------------------------------------------------------
/app/search/loading.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import { DefaultSkeleton } from '../../components/default-skeleton'
4 |
5 | export default function Loading() {
6 | return (
7 |
12 | )
13 | }
14 |
--------------------------------------------------------------------------------
/app/search/page.tsx:
--------------------------------------------------------------------------------
1 | import { Chat } from '@/components/chat'
2 | import { getModels } from '@/lib/config/models'
3 | import { generateId } from 'ai'
4 | import { redirect } from 'next/navigation'
5 |
6 | export const maxDuration = 60
7 |
8 | export default async function SearchPage(props: {
9 | searchParams: Promise<{ q: string }>
10 | }) {
11 | const { q } = await props.searchParams
12 | if (!q) {
13 | redirect('/')
14 | }
15 |
16 | const id = generateId()
17 | const models = await getModels()
18 | return
19 | }
20 |
--------------------------------------------------------------------------------
/app/share/[id]/page.tsx:
--------------------------------------------------------------------------------
1 | import { Chat } from '@/components/chat'
2 | import { getSharedChat } from '@/lib/actions/chat'
3 | import { getModels } from '@/lib/config/models'
4 | import { convertToUIMessages } from '@/lib/utils'
5 | import { notFound } from 'next/navigation'
6 |
7 | export async function generateMetadata(props: {
8 | params: Promise<{ id: string }>
9 | }) {
10 | const { id } = await props.params
11 | const chat = await getSharedChat(id)
12 |
13 | if (!chat || !chat.sharePath) {
14 | return notFound()
15 | }
16 |
17 | return {
18 | title: chat?.title.toString().slice(0, 50) || 'Search'
19 | }
20 | }
21 |
22 | export default async function SharePage(props: {
23 | params: Promise<{ id: string }>
24 | }) {
25 | const { id } = await props.params
26 | const chat = await getSharedChat(id)
27 |
28 | if (!chat || !chat.sharePath) {
29 | return notFound()
30 | }
31 |
32 | const models = await getModels()
33 | return (
34 |
39 | )
40 | }
41 |
--------------------------------------------------------------------------------
/app/share/loading.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import { DefaultSkeleton } from '../../components/default-skeleton'
4 |
5 | export default function Loading() {
6 | return (
7 |
12 | )
13 | }
14 |
--------------------------------------------------------------------------------
/components.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://ui.shadcn.com/schema.json",
3 | "style": "default",
4 | "rsc": true,
5 | "tsx": true,
6 | "tailwind": {
7 | "config": "tailwind.config.ts",
8 | "css": "app/globals.css",
9 | "baseColor": "neutral",
10 | "cssVariables": true,
11 | "prefix": ""
12 | },
13 | "aliases": {
14 | "components": "@/components",
15 | "utils": "@/lib/utils",
16 | "ui": "@/components/ui",
17 | "lib": "@/lib",
18 | "hooks": "@/hooks"
19 | },
20 | "iconLibrary": "lucide"
21 | }
--------------------------------------------------------------------------------
/components/answer-section.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import { ChatRequestOptions } from 'ai'
4 | import { CollapsibleMessage } from './collapsible-message'
5 | import { DefaultSkeleton } from './default-skeleton'
6 | import { BotMessage } from './message'
7 | import { MessageActions } from './message-actions'
8 |
9 | export type AnswerSectionProps = {
10 | content: string
11 | isOpen: boolean
12 | onOpenChange: (open: boolean) => void
13 | chatId?: string
14 | showActions?: boolean
15 | messageId: string
16 | reload?: (
17 | messageId: string,
18 | options?: ChatRequestOptions
19 | ) => Promise
20 | }
21 |
22 | export function AnswerSection({
23 | content,
24 | isOpen,
25 | onOpenChange,
26 | chatId,
27 | showActions = true, // Default to true for backward compatibility
28 | messageId,
29 | reload
30 | }: AnswerSectionProps) {
31 | const enableShare = process.env.NEXT_PUBLIC_ENABLE_SHARE === 'true'
32 |
33 | const handleReload = () => {
34 | if (reload) {
35 | return reload(messageId)
36 | }
37 | return Promise.resolve(undefined)
38 | }
39 |
40 | const message = content ? (
41 |
42 |
43 | {showActions && (
44 |
51 | )}
52 |
53 | ) : (
54 |
55 | )
56 | return (
57 |
65 | {message}
66 |
67 | )
68 | }
69 |
--------------------------------------------------------------------------------
/components/app-sidebar.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | Sidebar,
3 | SidebarContent,
4 | SidebarHeader,
5 | SidebarMenu,
6 | SidebarMenuButton,
7 | SidebarMenuItem,
8 | SidebarRail,
9 | SidebarTrigger
10 | } from '@/components/ui/sidebar'
11 | import { cn } from '@/lib/utils'
12 | import { Plus } from 'lucide-react'
13 | import Link from 'next/link'
14 | import { Suspense } from 'react'
15 | import { ChatHistorySection } from './sidebar/chat-history-section'
16 | import { ChatHistorySkeleton } from './sidebar/chat-history-skeleton'
17 | import { IconLogo } from './ui/icons'
18 |
19 | export default function AppSidebar() {
20 | return (
21 |
22 |
23 |
24 |
25 | Morphic
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 | New
36 |
37 |
38 |
39 |
40 |
41 | }>
42 |
43 |
44 |
45 |
46 |
47 |
48 | )
49 | }
50 |
--------------------------------------------------------------------------------
/components/artifact/artifact-content.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import { Part } from '@/components/artifact/artifact-context'
4 | import { ReasoningContent } from './reasoning-content'
5 | import { ToolInvocationContent } from './tool-invocation-content'
6 |
7 | export function ArtifactContent({ part }: { part: Part | null }) {
8 | if (!part) return null
9 |
10 | switch (part.type) {
11 | case 'tool-invocation':
12 | return
13 | case 'reasoning':
14 | return
15 | default:
16 | return (
17 | Details for this part type are not available
18 | )
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/components/artifact/artifact-context.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import type { ToolInvocation } from 'ai'
4 | import {
5 | createContext,
6 | ReactNode,
7 | useCallback,
8 | useContext,
9 | useEffect,
10 | useReducer
11 | } from 'react'
12 | import { useSidebar } from '../ui/sidebar'
13 |
14 | // Part types as seen in render-message.tsx
15 | export type TextPart = {
16 | type: 'text'
17 | text: string
18 | }
19 |
20 | export type ReasoningPart = {
21 | type: 'reasoning'
22 | reasoning: string
23 | }
24 |
25 | export type ToolInvocationPart = {
26 | type: 'tool-invocation'
27 | toolInvocation: ToolInvocation
28 | }
29 |
30 | export type Part = TextPart | ReasoningPart | ToolInvocationPart
31 |
32 | interface ArtifactState {
33 | part: Part | null
34 | isOpen: boolean
35 | }
36 |
37 | type ArtifactAction = { type: 'OPEN'; payload: Part } | { type: 'CLOSE' }
38 |
39 | const initialState: ArtifactState = {
40 | part: null,
41 | isOpen: false
42 | }
43 |
44 | function artifactReducer(
45 | state: ArtifactState,
46 | action: ArtifactAction
47 | ): ArtifactState {
48 | switch (action.type) {
49 | case 'OPEN':
50 | return { part: action.payload, isOpen: true }
51 | case 'CLOSE':
52 | return { ...state, isOpen: false }
53 | default:
54 | return state
55 | }
56 | }
57 |
58 | interface ArtifactContextValue {
59 | state: ArtifactState
60 | open: (part: Part) => void
61 | close: () => void
62 | }
63 |
64 | const ArtifactContext = createContext(
65 | undefined
66 | )
67 |
68 | export function ArtifactProvider({ children }: { children: ReactNode }) {
69 | const [state, dispatch] = useReducer(artifactReducer, initialState)
70 | const { setOpen, open: sidebarOpen } = useSidebar()
71 |
72 | const close = useCallback(() => {
73 | dispatch({ type: 'CLOSE' })
74 | }, [])
75 |
76 | // Close artifact when sidebar opens
77 | useEffect(() => {
78 | if (sidebarOpen && state.isOpen) {
79 | close()
80 | }
81 | }, [sidebarOpen, state.isOpen, close])
82 |
83 | const open = (part: Part) => {
84 | dispatch({ type: 'OPEN', payload: part })
85 | setOpen(false)
86 | }
87 |
88 | return (
89 |
90 | {children}
91 |
92 | )
93 | }
94 |
95 | export function useArtifact() {
96 | const context = useContext(ArtifactContext)
97 | if (context === undefined) {
98 | throw new Error('useArtifact must be used within an ArtifactProvider')
99 | }
100 | return context
101 | }
102 |
--------------------------------------------------------------------------------
/components/artifact/artifact-root.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import { ReactNode } from 'react'
4 | import { ArtifactProvider } from './artifact-context'
5 | import { ChatArtifactContainer } from './chat-artifact-container'
6 |
7 | export default function ArtifactRoot({ children }: { children: ReactNode }) {
8 | return (
9 |
10 | {children}
11 |
12 | )
13 | }
14 |
--------------------------------------------------------------------------------
/components/artifact/chat-artifact-container.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import { InspectorDrawer } from '@/components/inspector/inspector-drawer'
4 | import { InspectorPanel } from '@/components/inspector/inspector-panel'
5 | import {
6 | ResizableHandle,
7 | ResizablePanel,
8 | ResizablePanelGroup
9 | } from '@/components/ui/resizable'
10 | import { SidebarTrigger, useSidebar } from '@/components/ui/sidebar'
11 | import { useMediaQuery } from '@/lib/hooks/use-media-query'
12 | import { cn } from '@/lib/utils'
13 | import React, { useEffect, useState } from 'react'
14 | import { useArtifact } from './artifact-context'
15 | export function ChatArtifactContainer({
16 | children
17 | }: {
18 | children: React.ReactNode
19 | }) {
20 | const { state } = useArtifact()
21 | const isMobile = useMediaQuery('(max-width: 767px)') // Below md breakpoint
22 | const [renderPanel, setRenderPanel] = useState(state.isOpen)
23 | const { open, openMobile, isMobile: isMobileSidebar } = useSidebar()
24 |
25 | useEffect(() => {
26 | if (state.isOpen) {
27 | setRenderPanel(true)
28 | } else {
29 | setRenderPanel(false)
30 | }
31 | }, [state.isOpen])
32 |
33 | return (
34 |
35 |
36 | {(!open || isMobileSidebar) && (
37 |
38 | )}
39 |
40 | {/* Desktop: Resizable panels (Do not render on mobile) */}
41 | {!isMobile && (
42 |
46 |
52 | {children}
53 |
54 |
55 | {renderPanel && (
56 | <>
57 |
58 |
66 |
67 |
68 | >
69 | )}
70 |
71 | )}
72 |
73 | {/* Mobile: full-width chat + drawer (Do not render on desktop) */}
74 | {isMobile && (
75 |
76 | {' '}
77 | {/* Responsive classes removed */}
78 | {children}
79 | {/* ArtifactDrawer checks isMobile internally, no double check needed */}
80 |
81 |
82 | )}
83 |
84 | )
85 | }
86 |
--------------------------------------------------------------------------------
/components/artifact/reasoning-content.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import { cn } from '@/lib/utils'
4 | import remarkGfm from 'remark-gfm'
5 | import { MemoizedReactMarkdown } from '../ui/markdown'
6 |
7 | export function ReasoningContent({ reasoning }: { reasoning: string }) {
8 | return (
9 |
10 |
Reasoning
11 |
12 |
13 | {reasoning}
14 |
15 |
16 |
17 | )
18 | }
19 |
--------------------------------------------------------------------------------
/components/artifact/retrieve-artifact-content.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import { SearchResults } from '@/components/search-results'
4 | import { Section, ToolArgsSection } from '@/components/section'
5 | import type {
6 | SearchResultItem,
7 | SearchResults as TypeSearchResults
8 | } from '@/lib/types/index'
9 | import type { ToolInvocation } from 'ai'
10 | import { MemoizedReactMarkdown } from '../ui/markdown'
11 |
12 | const MAX_CONTENT_LENGTH = 1000
13 |
14 | export function RetrieveArtifactContent({ tool }: { tool: ToolInvocation }) {
15 | const searchResults: TypeSearchResults | undefined =
16 | tool.state === 'result' ? tool.result : undefined
17 | const url = tool.args?.url as string | undefined
18 |
19 | if (!searchResults?.results) {
20 | return No retrieved content
21 | }
22 |
23 | const truncatedResults: SearchResultItem[] = searchResults.results.map(
24 | result => ({
25 | ...result,
26 | content:
27 | result.content.length > MAX_CONTENT_LENGTH
28 | ? `${result.content.substring(0, MAX_CONTENT_LENGTH)}...`
29 | : result.content
30 | })
31 | )
32 |
33 | return (
34 |
35 | {url}
36 |
37 |
40 | {truncatedResults[0].content && (
41 |
42 |
43 | {truncatedResults[0].content}
44 |
45 |
46 | )}
47 |
48 | )
49 | }
50 |
--------------------------------------------------------------------------------
/components/artifact/search-artifact-content.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import { SearchResults } from '@/components/search-results'
4 | import { SearchResultsImageSection } from '@/components/search-results-image'
5 | import { Section, ToolArgsSection } from '@/components/section'
6 | import type { SearchResults as TypeSearchResults } from '@/lib/types'
7 | import type { ToolInvocation } from 'ai'
8 |
9 | export function SearchArtifactContent({ tool }: { tool: ToolInvocation }) {
10 | const searchResults: TypeSearchResults =
11 | tool.state === 'result' ? tool.result : undefined
12 | const query = tool.args?.query as string | undefined
13 |
14 | if (!searchResults?.results) {
15 | return No search results
16 | }
17 |
18 | return (
19 |
20 | {`${query}`}
21 | {searchResults.images && searchResults.images.length > 0 && (
22 |
27 | )}
28 |
29 |
32 |
33 | )
34 | }
35 |
--------------------------------------------------------------------------------
/components/artifact/tool-invocation-content.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import { RetrieveArtifactContent } from '@/components/artifact/retrieve-artifact-content'
4 | import { SearchArtifactContent } from '@/components/artifact/search-artifact-content'
5 | import { VideoSearchArtifactContent } from '@/components/artifact/video-search-artifact-content'
6 | import type { ToolInvocation } from 'ai'
7 |
8 | export function ToolInvocationContent({
9 | toolInvocation
10 | }: {
11 | toolInvocation: ToolInvocation
12 | }) {
13 | switch (toolInvocation.toolName) {
14 | case 'search':
15 | return
16 | case 'retrieve':
17 | return
18 | case 'videoSearch':
19 | return
20 | default:
21 | return Details for this tool are not available
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/components/artifact/video-search-artifact-content.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import { ToolArgsSection } from '@/components/section'
4 | import { VideoResultGrid } from '@/components/video-result-grid'
5 | import {
6 | type SerperSearchResultItem,
7 | type SerperSearchResults
8 | } from '@/lib/types'
9 | import type { ToolInvocation } from 'ai'
10 |
11 | export function VideoSearchArtifactContent({ tool }: { tool: ToolInvocation }) {
12 | const videoResults: SerperSearchResults | undefined =
13 | tool.state === 'result' ? tool.result : undefined
14 | const query = tool.args?.query as string | undefined
15 |
16 | const videos = (videoResults?.videos || []).filter(
17 | (video: SerperSearchResultItem) => {
18 | try {
19 | return new URL(video.link).pathname === '/watch'
20 | } catch (e) {
21 | console.error('Invalid video URL:', video.link)
22 | return false
23 | }
24 | }
25 | )
26 |
27 | if (videos.length === 0) {
28 | return (
29 |
30 |
{query}
31 |
No video results
32 |
33 | )
34 | }
35 |
36 | return (
37 |
38 | {query}
39 |
44 |
45 | )
46 | }
47 |
--------------------------------------------------------------------------------
/components/chat-share.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import { shareChat } from '@/lib/actions/chat'
4 | import { useCopyToClipboard } from '@/lib/hooks/use-copy-to-clipboard'
5 | import { cn } from '@/lib/utils'
6 | import { Share } from 'lucide-react'
7 | import { useState, useTransition } from 'react'
8 | import { toast } from 'sonner'
9 | import { Button } from './ui/button'
10 | import {
11 | Dialog,
12 | DialogContent,
13 | DialogDescription,
14 | DialogFooter,
15 | DialogHeader,
16 | DialogTitle,
17 | DialogTrigger
18 | } from './ui/dialog'
19 | import { Spinner } from './ui/spinner'
20 |
21 | interface ChatShareProps {
22 | chatId: string
23 | className?: string
24 | }
25 |
26 | export function ChatShare({ chatId, className }: ChatShareProps) {
27 | const [open, setOpen] = useState(false)
28 | const [pending, startTransition] = useTransition()
29 | const { copyToClipboard } = useCopyToClipboard({ timeout: 1000 })
30 | const [shareUrl, setShareUrl] = useState('')
31 |
32 | const handleShare = async () => {
33 | startTransition(() => {
34 | setOpen(true)
35 | })
36 | const result = await shareChat(chatId)
37 | if (!result) {
38 | toast.error('Failed to share chat')
39 | return
40 | }
41 |
42 | if (!result.sharePath) {
43 | toast.error('Could not copy link to clipboard')
44 | return
45 | }
46 |
47 | const url = new URL(result.sharePath, window.location.href)
48 | setShareUrl(url.toString())
49 | }
50 |
51 | const handleCopy = () => {
52 | if (shareUrl) {
53 | copyToClipboard(shareUrl)
54 | toast.success('Link copied to clipboard')
55 | setOpen(false)
56 | } else {
57 | toast.error('No link to copy')
58 | }
59 | }
60 |
61 | return (
62 |
63 |
100 |
101 | )
102 | }
103 |
--------------------------------------------------------------------------------
/components/clear-history.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import {
4 | AlertDialog,
5 | AlertDialogAction,
6 | AlertDialogCancel,
7 | AlertDialogContent,
8 | AlertDialogDescription,
9 | AlertDialogFooter,
10 | AlertDialogHeader,
11 | AlertDialogTitle,
12 | AlertDialogTrigger
13 | } from '@/components/ui/alert-dialog'
14 | import { Button } from '@/components/ui/button'
15 | import { clearChats } from '@/lib/actions/chat'
16 | import { useState, useTransition } from 'react'
17 | import { toast } from 'sonner'
18 | import { Spinner } from './ui/spinner'
19 |
20 | type ClearHistoryProps = {
21 | empty: boolean
22 | }
23 |
24 | export function ClearHistory({ empty }: ClearHistoryProps) {
25 | const [open, setOpen] = useState(false)
26 | const [isPending, startTransition] = useTransition()
27 | return (
28 |
29 |
30 |
33 |
34 |
35 |
36 | Are you absolutely sure?
37 |
38 | This action cannot be undone. This will permanently delete your
39 | history and remove your data from our servers.
40 |
41 |
42 |
43 | Cancel
44 | {
47 | event.preventDefault()
48 | startTransition(async () => {
49 | const result = await clearChats()
50 | if (result?.error) {
51 | toast.error(result.error)
52 | } else {
53 | toast.success('History cleared')
54 | }
55 | setOpen(false)
56 | })
57 | }}
58 | >
59 | {isPending ? : 'Clear'}
60 |
61 |
62 |
63 |
64 | )
65 | }
66 |
--------------------------------------------------------------------------------
/components/collapsible-message.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from '@/lib/utils'
2 | import { ChevronDown } from 'lucide-react'
3 | import { CurrentUserAvatar } from './current-user-avatar'
4 | import {
5 | Collapsible,
6 | CollapsibleContent,
7 | CollapsibleTrigger
8 | } from './ui/collapsible'
9 | import { IconLogo } from './ui/icons'
10 | import { Separator } from './ui/separator'
11 |
12 | interface CollapsibleMessageProps {
13 | children: React.ReactNode
14 | role: 'user' | 'assistant'
15 | isCollapsible?: boolean
16 | isOpen?: boolean
17 | header?: React.ReactNode
18 | onOpenChange?: (open: boolean) => void
19 | showBorder?: boolean
20 | showIcon?: boolean
21 | }
22 |
23 | export function CollapsibleMessage({
24 | children,
25 | role,
26 | isCollapsible = false,
27 | isOpen = true,
28 | header,
29 | onOpenChange,
30 | showBorder = true,
31 | showIcon = true
32 | }: CollapsibleMessageProps) {
33 | const content = {children}
34 |
35 | return (
36 |
37 | {showIcon && (
38 |
39 |
40 | {role === 'assistant' ? (
41 |
42 | ) : (
43 |
44 | )}
45 |
46 |
47 | )}
48 |
49 | {isCollapsible ? (
50 |
56 |
61 |
62 | {header &&
{header}
}
63 |
64 |
71 |
72 |
73 |
74 |
75 | {content}
76 |
77 |
78 |
79 | ) : (
80 |
86 | {content}
87 |
88 | )}
89 |
90 | )
91 | }
92 |
--------------------------------------------------------------------------------
/components/current-user-avatar.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
4 | import { useCurrentUserImage } from '@/hooks/use-current-user-image'
5 | import { useCurrentUserName } from '@/hooks/use-current-user-name'
6 | import { User2 } from 'lucide-react'
7 |
8 | export const CurrentUserAvatar = () => {
9 | const profileImage = useCurrentUserImage()
10 | const name = useCurrentUserName()
11 | const initials = name
12 | ?.split(' ')
13 | ?.map(word => word[0])
14 | ?.join('')
15 | ?.toUpperCase()
16 |
17 | return (
18 |
19 | {profileImage && }
20 |
21 | {initials === '?' ? (
22 |
23 | ) : (
24 | initials
25 | )}
26 |
27 |
28 | )
29 | }
30 |
--------------------------------------------------------------------------------
/components/custom-link.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from '@/lib/utils'
2 | import { AnchorHTMLAttributes, DetailedHTMLProps, ReactNode } from 'react'
3 |
4 | type CustomLinkProps = Omit<
5 | DetailedHTMLProps, HTMLAnchorElement>,
6 | 'ref'
7 | > & {
8 | children: ReactNode
9 | }
10 |
11 | export function Citing({
12 | href,
13 | children,
14 | className,
15 | ...props
16 | }: CustomLinkProps) {
17 | const childrenText = children?.toString() || ''
18 | const isNumber = /^\d+$/.test(childrenText)
19 | const linkClasses = cn(
20 | isNumber
21 | ? 'text-[10px] bg-muted text-muted-froreground rounded-full w-4 h-4 px-0.5 inline-flex items-center justify-center hover:bg-muted/50 duration-200 no-underline -translate-y-0.5'
22 | : 'hover:underline inline-flex items-center gap-1.5',
23 | className
24 | )
25 |
26 | return (
27 |
34 | {children}
35 |
36 | )
37 | }
38 |
--------------------------------------------------------------------------------
/components/default-skeleton.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import { Skeleton } from './ui/skeleton'
4 |
5 | export const DefaultSkeleton = () => {
6 | return (
7 |
8 |
9 |
10 |
11 | )
12 | }
13 |
14 | export function SearchSkeleton() {
15 | return (
16 |
17 | {[...Array(4)].map((_, index) => (
18 |
22 |
23 |
24 | ))}
25 |
26 | )
27 | }
28 |
--------------------------------------------------------------------------------
/components/empty-screen.tsx:
--------------------------------------------------------------------------------
1 | import { Button } from '@/components/ui/button'
2 | import { ArrowRight } from 'lucide-react'
3 |
4 | const exampleMessages = [
5 | {
6 | heading: 'What is DeepSeek R1?',
7 | message: 'What is DeepSeek R1?'
8 | },
9 | {
10 | heading: 'Why is Nvidia growing rapidly?',
11 | message: 'Why is Nvidia growing rapidly?'
12 | },
13 | {
14 | heading: 'Tesla vs Rivian',
15 | message: 'Tesla vs Rivian'
16 | },
17 | {
18 | heading: 'Summary: https://arxiv.org/pdf/2501.05707',
19 | message: 'Summary: https://arxiv.org/pdf/2501.05707'
20 | }
21 | ]
22 | export function EmptyScreen({
23 | submitMessage,
24 | className
25 | }: {
26 | submitMessage: (message: string) => void
27 | className?: string
28 | }) {
29 | return (
30 |
31 |
32 |
33 | {exampleMessages.map((message, index) => (
34 |
46 | ))}
47 |
48 |
49 |
50 | )
51 | }
52 |
--------------------------------------------------------------------------------
/components/external-link-items.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import { DropdownMenuItem } from '@/components/ui/dropdown-menu'
4 | import Link from 'next/link'
5 | import { SiDiscord, SiGithub, SiX } from 'react-icons/si'
6 |
7 | const externalLinks = [
8 | {
9 | name: 'X',
10 | href: 'https://x.com/morphic_ai',
11 | icon:
12 | },
13 | {
14 | name: 'Discord',
15 | href: 'https://discord.gg/zRxaseCuGq',
16 | icon:
17 | },
18 | {
19 | name: 'GitHub',
20 | href: 'https://git.new/morphic',
21 | icon:
22 | }
23 | ]
24 |
25 | export function ExternalLinkItems() {
26 | return (
27 | <>
28 | {externalLinks.map(link => (
29 |
30 |
31 | {link.icon}
32 | {link.name}
33 |
34 |
35 | ))}
36 | >
37 | )
38 | }
39 |
--------------------------------------------------------------------------------
/components/guest-menu.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import { Button } from '@/components/ui/button'
4 | import {
5 | DropdownMenu,
6 | DropdownMenuContent,
7 | DropdownMenuItem,
8 | DropdownMenuSeparator,
9 | DropdownMenuSub,
10 | DropdownMenuSubContent,
11 | DropdownMenuSubTrigger,
12 | DropdownMenuTrigger
13 | } from '@/components/ui/dropdown-menu'
14 | import {
15 | Link2,
16 | LogIn,
17 | Palette,
18 | Settings2 // Or EllipsisVertical, etc.
19 | } from 'lucide-react'
20 | import Link from 'next/link'
21 | import { ExternalLinkItems } from './external-link-items'
22 | import { ThemeMenuItems } from './theme-menu-items'
23 |
24 | export default function GuestMenu() {
25 | return (
26 |
27 |
28 |
32 |
33 |
34 |
35 |
36 |
37 | Sign In
38 |
39 |
40 |
41 |
42 |
43 |
44 | Theme
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 | Links
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 | )
62 | }
63 |
--------------------------------------------------------------------------------
/components/header.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import { useSidebar } from '@/components/ui/sidebar'
4 | import { cn } from '@/lib/utils'
5 | import { User } from '@supabase/supabase-js'
6 | // import Link from 'next/link' // No longer needed directly here for Sign In button
7 | import React from 'react'
8 | // import { Button } from './ui/button' // No longer needed directly here for Sign In button
9 | import GuestMenu from './guest-menu' // Import the new GuestMenu component
10 | import UserMenu from './user-menu'
11 |
12 | interface HeaderProps {
13 | user: User | null
14 | }
15 |
16 | export const Header: React.FC = ({ user }) => {
17 | const { open } = useSidebar()
18 | return (
19 |
26 | {/* This div can be used for a logo or title on the left if needed */}
27 |
28 |
29 |
30 | {user ? : }
31 |
32 |
33 | )
34 | }
35 |
36 | export default Header
37 |
--------------------------------------------------------------------------------
/components/inspector/inspector-drawer.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import { useArtifact } from '@/components/artifact/artifact-context'
4 | import { Drawer, DrawerContent, DrawerTitle } from '@/components/ui/drawer'
5 | import { useMediaQuery } from '@/lib/hooks/use-media-query'
6 | import { VisuallyHidden } from '@radix-ui/react-visually-hidden'
7 | import { InspectorPanel } from './inspector-panel'
8 |
9 | export function InspectorDrawer() {
10 | const { state, close } = useArtifact()
11 | const part = state.part
12 | const isMobile = useMediaQuery('(max-width: 767px)')
13 |
14 | // Function to get the title based on part type (mirrors ArtifactPanel logic)
15 | const getTitle = () => {
16 | if (!part) return 'Artifact' // Default title
17 | switch (part.type) {
18 | case 'tool-invocation':
19 | return part.toolInvocation.toolName
20 | case 'reasoning':
21 | return 'Reasoning'
22 | case 'text':
23 | return 'Text'
24 | default:
25 | return 'Content'
26 | }
27 | }
28 |
29 | if (!isMobile) return null
30 |
31 | return (
32 | {
35 | if (!open) close()
36 | }}
37 | modal={true}
38 | >
39 |
40 |
41 | {getTitle()}
42 |
43 |
44 |
45 |
46 | )
47 | }
48 |
--------------------------------------------------------------------------------
/components/inspector/inspector-panel.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import { ArtifactContent } from '@/components/artifact/artifact-content'
4 | import { useArtifact } from '@/components/artifact/artifact-context'
5 | import { Button } from '@/components/ui/button'
6 | import { Separator } from '@/components/ui/separator'
7 | import { TooltipButton } from '@/components/ui/tooltip-button'
8 | import {
9 | Tooltip,
10 | TooltipContent,
11 | TooltipProvider,
12 | TooltipTrigger
13 | } from '@/components/ui/tooltip'
14 | import { LightbulbIcon, MessageSquare, Minimize2, Wrench } from 'lucide-react'
15 |
16 | export function InspectorPanel() {
17 | const { state, close } = useArtifact()
18 | const part = state.part
19 | if (!part) return null
20 |
21 | // Get the icon and title based on part type
22 | const getIconAndTitle = () => {
23 | switch (part.type) {
24 | case 'tool-invocation':
25 | return {
26 | icon: ,
27 | title: part.toolInvocation.toolName
28 | }
29 | case 'reasoning':
30 | return {
31 | icon: ,
32 | title: 'Reasoning'
33 | }
34 | case 'text':
35 | return {
36 | icon: ,
37 | title: 'Text'
38 | }
39 | default:
40 | return {
41 | icon: ,
42 | title: 'Content'
43 | }
44 | }
45 | }
46 |
47 | const { icon, title } = getIconAndTitle()
48 |
49 | return (
50 |
51 |
52 |
53 |
54 |
55 |
56 | {icon}
57 |
58 | {title}
59 |
60 |
67 |
68 |
69 |
70 |
71 |
74 |
75 |
76 |
77 | )
78 | }
79 |
--------------------------------------------------------------------------------
/components/message-actions.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import { CHAT_ID } from '@/lib/constants'
4 | import { cn } from '@/lib/utils'
5 | import { useChat } from '@ai-sdk/react'
6 | import { Copy } from 'lucide-react'
7 | import { toast } from 'sonner'
8 | import { ChatShare } from './chat-share'
9 | import { RetryButton } from './retry-button'
10 | import { Button } from './ui/button'
11 |
12 | interface MessageActionsProps {
13 | message: string
14 | messageId: string
15 | reload?: () => Promise
16 | chatId?: string
17 | enableShare?: boolean
18 | className?: string
19 | }
20 |
21 | export function MessageActions({
22 | message,
23 | messageId,
24 | reload,
25 | chatId,
26 | enableShare,
27 | className
28 | }: MessageActionsProps) {
29 | const { status } = useChat({
30 | id: CHAT_ID
31 | })
32 | const isLoading = status === 'submitted' || status === 'streaming'
33 |
34 | async function handleCopy() {
35 | await navigator.clipboard.writeText(message)
36 | toast.success('Message copied to clipboard')
37 | }
38 |
39 | return (
40 |
47 | {reload && }
48 |
56 | {enableShare && chatId && }
57 |
58 | )
59 | }
60 |
--------------------------------------------------------------------------------
/components/message.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import { cn } from '@/lib/utils'
4 | import 'katex/dist/katex.min.css'
5 | import rehypeExternalLinks from 'rehype-external-links'
6 | import rehypeKatex from 'rehype-katex'
7 | import remarkGfm from 'remark-gfm'
8 | import remarkMath from 'remark-math'
9 | import { Citing } from './custom-link'
10 | import { CodeBlock } from './ui/codeblock'
11 | import { MemoizedReactMarkdown } from './ui/markdown'
12 |
13 | export function BotMessage({
14 | message,
15 | className
16 | }: {
17 | message: string
18 | className?: string
19 | }) {
20 | // Check if the content contains LaTeX patterns
21 | const containsLaTeX = /\\\[([\s\S]*?)\\\]|\\\(([\s\S]*?)\\\)/.test(
22 | message || ''
23 | )
24 |
25 | // Modify the content to render LaTeX equations if LaTeX patterns are found
26 | const processedData = preprocessLaTeX(message || '')
27 |
28 | if (containsLaTeX) {
29 | return (
30 |
41 | {processedData}
42 |
43 | )
44 | }
45 |
46 | return (
47 | ▍
60 | )
61 | }
62 |
63 | children[0] = (children[0] as string).replace('`▍`', '▍')
64 | }
65 |
66 | const match = /language-(\w+)/.exec(className || '')
67 |
68 | if (inline) {
69 | return (
70 |
71 | {children}
72 |
73 | )
74 | }
75 |
76 | return (
77 |
83 | )
84 | },
85 | a: Citing
86 | }}
87 | >
88 | {message}
89 |
90 | )
91 | }
92 |
93 | // Preprocess LaTeX equations to be rendered by KaTeX
94 | // ref: https://github.com/remarkjs/react-markdown/issues/785
95 | const preprocessLaTeX = (content: string) => {
96 | const blockProcessedContent = content.replace(
97 | /\\\[([\s\S]*?)\\\]/g,
98 | (_, equation) => `$$${equation}$$`
99 | )
100 | const inlineProcessedContent = blockProcessedContent.replace(
101 | /\\\(([\s\S]*?)\\\)/g,
102 | (_, equation) => `$${equation}$`
103 | )
104 | return inlineProcessedContent
105 | }
106 |
--------------------------------------------------------------------------------
/components/reasoning-section.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import { Badge } from '@/components/ui/badge'
4 | import { Check, Lightbulb, Loader2 } from 'lucide-react'
5 | import { CollapsibleMessage } from './collapsible-message'
6 | import { DefaultSkeleton } from './default-skeleton'
7 | import { BotMessage } from './message'
8 | import { StatusIndicator } from './ui/status-indicator'
9 |
10 | interface ReasoningContent {
11 | reasoning: string
12 | time?: number
13 | }
14 |
15 | export interface ReasoningSectionProps {
16 | content: ReasoningContent
17 | isOpen: boolean
18 | onOpenChange: (open: boolean) => void
19 | }
20 |
21 | export function ReasoningSection({
22 | content,
23 | isOpen,
24 | onOpenChange
25 | }: ReasoningSectionProps) {
26 | const reasoningHeader = (
27 |
28 |
29 |
30 |
31 |
32 | {content.time === 0
33 | ? 'Thinking...'
34 | : content.time !== undefined && content.time > 0
35 | ? `Thought for ${(content.time / 1000).toFixed(1)} seconds`
36 | : 'Thoughts'}
37 |
38 | {content.time === 0 ? (
39 |
43 | ) : (
44 |
45 | {`${content.reasoning.length.toLocaleString()} characters`}
46 |
47 | )}
48 |
49 |
50 |
51 | )
52 |
53 | if (!content) return
54 |
55 | return (
56 |
57 |
66 |
70 |
71 |
72 | )
73 | }
74 |
--------------------------------------------------------------------------------
/components/related-questions.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import { CHAT_ID } from '@/lib/constants'
4 | import { useChat } from '@ai-sdk/react'
5 | import { JSONValue } from 'ai'
6 | import { ArrowRight } from 'lucide-react'
7 | import React from 'react'
8 | import { CollapsibleMessage } from './collapsible-message'
9 | import { Section } from './section'
10 | import { Button } from './ui/button'
11 | import { Skeleton } from './ui/skeleton'
12 |
13 | export interface RelatedQuestionsProps {
14 | annotations: JSONValue[]
15 | onQuerySelect: (query: string) => void
16 | isOpen: boolean
17 | onOpenChange: (open: boolean) => void
18 | }
19 |
20 | interface RelatedQuestionsAnnotation extends Record {
21 | type: 'related-questions'
22 | data: {
23 | items: Array<{ query: string }>
24 | }
25 | }
26 |
27 | export const RelatedQuestions: React.FC = ({
28 | annotations,
29 | onQuerySelect,
30 | isOpen,
31 | onOpenChange
32 | }) => {
33 | const { status } = useChat({
34 | id: CHAT_ID
35 | })
36 | const isLoading = status === 'submitted' || status === 'streaming'
37 |
38 | if (!annotations) {
39 | return null
40 | }
41 |
42 | const lastRelatedQuestionsAnnotation = annotations[
43 | annotations.length - 1
44 | ] as RelatedQuestionsAnnotation
45 |
46 | const relatedQuestions = lastRelatedQuestionsAnnotation?.data
47 | if ((!relatedQuestions || !relatedQuestions.items) && !isLoading) {
48 | return null
49 | }
50 |
51 | if (relatedQuestions.items.length === 0 && isLoading) {
52 | return (
53 |
60 |
61 |
62 | )
63 | }
64 |
65 | return (
66 |
74 |
75 |
76 | {Array.isArray(relatedQuestions.items) ? (
77 | relatedQuestions.items
78 | ?.filter(item => item?.query !== '')
79 | .map((item, index) => (
80 |
81 |
82 |
92 |
93 | ))
94 | ) : (
95 |
Not an array
96 | )}
97 |
98 |
99 |
100 | )
101 | }
102 | export default RelatedQuestions
103 |
--------------------------------------------------------------------------------
/components/retrieve-section.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import { useArtifact } from '@/components/artifact/artifact-context'
4 | import { SearchResults } from '@/components/search-results'
5 | import { Section, ToolArgsSection } from '@/components/section'
6 | import { SearchResults as SearchResultsType } from '@/lib/types'
7 | import { ToolInvocation } from 'ai'
8 | import { CollapsibleMessage } from './collapsible-message'
9 | import { DefaultSkeleton } from './default-skeleton'
10 |
11 | interface RetrieveSectionProps {
12 | tool: ToolInvocation
13 | isOpen: boolean
14 | onOpenChange: (open: boolean) => void
15 | }
16 |
17 | export function RetrieveSection({
18 | tool,
19 | isOpen,
20 | onOpenChange
21 | }: RetrieveSectionProps) {
22 | const isLoading = tool.state === 'call'
23 | const data: SearchResultsType =
24 | tool.state === 'result' ? tool.result : undefined
25 | const url = tool.args.url as string | undefined
26 |
27 | const { open } = useArtifact()
28 | const header = (
29 |
39 | )
40 |
41 | return (
42 |
50 | {!isLoading && data ? (
51 |
54 | ) : (
55 |
56 | )}
57 |
58 | )
59 | }
60 |
61 | export default RetrieveSection
62 |
--------------------------------------------------------------------------------
/components/retry-button.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import { RotateCcw } from 'lucide-react'
4 | import { Button } from './ui/button'
5 |
6 | interface RetryButtonProps {
7 | reload: () => Promise
8 | messageId: string
9 | }
10 |
11 | export const RetryButton: React.FC = ({
12 | reload,
13 | messageId
14 | }) => {
15 | return (
16 |
27 | )
28 | }
29 |
--------------------------------------------------------------------------------
/components/search-mode-toggle.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import { cn } from '@/lib/utils'
4 | import { getCookie, setCookie } from '@/lib/utils/cookies'
5 | import { Globe } from 'lucide-react'
6 | import { useEffect, useState } from 'react'
7 | import { Toggle } from './ui/toggle'
8 |
9 | export function SearchModeToggle() {
10 | const [isSearchMode, setIsSearchMode] = useState(true)
11 |
12 | useEffect(() => {
13 | const savedMode = getCookie('search-mode')
14 | if (savedMode !== null) {
15 | setIsSearchMode(savedMode === 'true')
16 | } else {
17 | setCookie('search-mode', 'true')
18 | }
19 | }, [])
20 |
21 | const handleSearchModeChange = (pressed: boolean) => {
22 | setIsSearchMode(pressed)
23 | setCookie('search-mode', pressed.toString())
24 | }
25 |
26 | return (
27 |
40 |
41 | Search
42 |
43 | )
44 | }
45 |
--------------------------------------------------------------------------------
/components/search-section.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import { useArtifact } from '@/components/artifact/artifact-context'
4 | import { CHAT_ID } from '@/lib/constants'
5 | import type { SearchResults as TypeSearchResults } from '@/lib/types'
6 | import { useChat } from '@ai-sdk/react'
7 | import { ToolInvocation } from 'ai'
8 | import { CollapsibleMessage } from './collapsible-message'
9 | import { SearchSkeleton } from './default-skeleton'
10 | import { SearchResults } from './search-results'
11 | import { SearchResultsImageSection } from './search-results-image'
12 | import { Section, ToolArgsSection } from './section'
13 |
14 | interface SearchSectionProps {
15 | tool: ToolInvocation
16 | isOpen: boolean
17 | onOpenChange: (open: boolean) => void
18 | }
19 |
20 | export function SearchSection({
21 | tool,
22 | isOpen,
23 | onOpenChange
24 | }: SearchSectionProps) {
25 | const { status } = useChat({
26 | id: CHAT_ID
27 | })
28 | const isLoading = status === 'submitted' || status === 'streaming'
29 |
30 | const isToolLoading = tool.state === 'call'
31 | const searchResults: TypeSearchResults =
32 | tool.state === 'result' ? tool.result : undefined
33 | const query = tool.args?.query as string | undefined
34 | const includeDomains = tool.args?.includeDomains as string[] | undefined
35 | const includeDomainsString = includeDomains
36 | ? ` [${includeDomains.join(', ')}]`
37 | : ''
38 |
39 | const { open } = useArtifact()
40 | const header = (
41 |
52 | )
53 |
54 | return (
55 |
63 | {searchResults &&
64 | searchResults.images &&
65 | searchResults.images.length > 0 && (
66 |
72 | )}
73 | {isLoading && isToolLoading ? (
74 |
75 | ) : searchResults?.results ? (
76 |
79 | ) : null}
80 |
81 | )
82 | }
83 |
--------------------------------------------------------------------------------
/components/section.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import { cn } from '@/lib/utils'
4 | import {
5 | BookCheck,
6 | Check,
7 | File,
8 | Film,
9 | Image,
10 | MessageCircleMore,
11 | Newspaper,
12 | Repeat2,
13 | Search
14 | } from 'lucide-react'
15 | import React from 'react'
16 | import { ToolBadge } from './tool-badge'
17 | import { Badge } from './ui/badge'
18 | import { Separator } from './ui/separator'
19 | import { StatusIndicator } from './ui/status-indicator'
20 |
21 | type SectionProps = {
22 | children: React.ReactNode
23 | className?: string
24 | size?: 'sm' | 'md' | 'lg'
25 | title?: string
26 | separator?: boolean
27 | }
28 |
29 | export const Section: React.FC = ({
30 | children,
31 | className,
32 | size = 'md',
33 | title,
34 | separator = false
35 | }) => {
36 | const iconSize = 16
37 | const iconClassName = 'mr-1.5 text-muted-foreground'
38 | let icon: React.ReactNode
39 | let type: 'text' | 'badge' = 'text'
40 | switch (title) {
41 | case 'Images':
42 | // eslint-disable-next-line jsx-a11y/alt-text
43 | icon =
44 | break
45 | case 'Videos':
46 | icon =
47 | type = 'badge'
48 | break
49 | case 'Sources':
50 | icon =
51 | type = 'badge'
52 | break
53 | case 'Answer':
54 | icon =
55 | break
56 | case 'Related':
57 | icon =
58 | break
59 | case 'Follow-up':
60 | icon =
61 | break
62 | case 'Content':
63 | icon =
64 | type = 'badge'
65 | break
66 | default:
67 | icon =
68 | }
69 |
70 | return (
71 | <>
72 | {separator && }
73 |
79 | {title && type === 'text' && (
80 |
81 | {icon}
82 | {title}
83 |
84 | )}
85 | {title && type === 'badge' && (
86 |
87 | {icon}
88 | {title}
89 |
90 | )}
91 | {children}
92 |
93 | >
94 | )
95 | }
96 |
97 | export function ToolArgsSection({
98 | children,
99 | tool,
100 | number
101 | }: {
102 | children: React.ReactNode
103 | tool: string
104 | number?: number
105 | }) {
106 | return (
107 |
111 | {children}
112 | {number && (
113 |
114 | {number} results
115 |
116 | )}
117 |
118 | )
119 | }
120 |
--------------------------------------------------------------------------------
/components/sidebar/chat-history-section.tsx:
--------------------------------------------------------------------------------
1 | import { ChatHistoryClient } from './chat-history-client'
2 |
3 | export async function ChatHistorySection() {
4 | const enableSaveChatHistory = process.env.ENABLE_SAVE_CHAT_HISTORY === 'true'
5 | if (!enableSaveChatHistory) {
6 | return null
7 | }
8 |
9 | return
10 | }
11 |
--------------------------------------------------------------------------------
/components/sidebar/chat-history-skeleton.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | SidebarMenu,
3 | SidebarMenuItem,
4 | SidebarMenuSkeleton
5 | } from '@/components/ui/sidebar'
6 |
7 | export function ChatHistorySkeleton() {
8 | return (
9 |
10 | {Array.from({ length: 5 }).map((_, idx) => (
11 |
12 |
13 |
14 | ))}
15 |
16 | )
17 | }
18 |
--------------------------------------------------------------------------------
/components/sidebar/clear-history-action.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import {
4 | AlertDialog,
5 | AlertDialogAction,
6 | AlertDialogCancel,
7 | AlertDialogContent,
8 | AlertDialogDescription,
9 | AlertDialogFooter,
10 | AlertDialogHeader,
11 | AlertDialogTitle,
12 | AlertDialogTrigger
13 | } from '@/components/ui/alert-dialog'
14 | import {
15 | DropdownMenu,
16 | DropdownMenuContent,
17 | DropdownMenuItem,
18 | DropdownMenuTrigger
19 | } from '@/components/ui/dropdown-menu'
20 | import { SidebarGroupAction } from '@/components/ui/sidebar'
21 | import { Spinner } from '@/components/ui/spinner'
22 | import { clearChats } from '@/lib/actions/chat'
23 | import { MoreHorizontal, Trash2 } from 'lucide-react'
24 | import { useState, useTransition } from 'react'
25 | import { toast } from 'sonner'
26 |
27 | interface ClearHistoryActionProps {
28 | empty: boolean
29 | }
30 |
31 | export function ClearHistoryAction({ empty }: ClearHistoryActionProps) {
32 | const [isPending, start] = useTransition()
33 | const [open, setOpen] = useState(false)
34 |
35 | const onClear = () =>
36 | start(async () => {
37 | const res = await clearChats()
38 | res?.error ? toast.error(res.error) : toast.success('History cleared')
39 | setOpen(false)
40 | window.dispatchEvent(new CustomEvent('chat-history-updated'))
41 | })
42 |
43 | return (
44 |
45 |
46 |
47 |
48 | History Actions
49 |
50 |
51 |
52 |
53 |
54 |
55 | event.preventDefault()} // Prevent closing dropdown
59 | >
60 | Clear History
61 |
62 |
63 |
64 |
65 |
66 | Are you absolutely sure?
67 |
68 | This action cannot be undone. It will permanently delete your
69 | history.
70 |
71 |
72 |
73 | Cancel
74 |
75 | {isPending ? : 'Clear'}
76 |
77 |
78 |
79 |
80 |
81 |
82 | )
83 | }
84 |
--------------------------------------------------------------------------------
/components/theme-menu-items.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import { DropdownMenuItem } from '@/components/ui/dropdown-menu'
4 | import { Laptop, Moon, Sun } from 'lucide-react'
5 | import { useTheme } from 'next-themes'
6 |
7 | export function ThemeMenuItems() {
8 | const { setTheme } = useTheme()
9 |
10 | return (
11 | <>
12 | setTheme('light')}>
13 |
14 | Light
15 |
16 | setTheme('dark')}>
17 |
18 | Dark
19 |
20 | setTheme('system')}>
21 |
22 | System
23 |
24 | >
25 | )
26 | }
27 |
--------------------------------------------------------------------------------
/components/theme-provider.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import * as React from 'react'
4 | import { ThemeProvider as NextThemesProvider } from 'next-themes'
5 | import { type ThemeProviderProps } from 'next-themes/dist/types'
6 |
7 | export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
8 | return {children}
9 | }
10 |
--------------------------------------------------------------------------------
/components/tool-badge.tsx:
--------------------------------------------------------------------------------
1 | import { Film, Link, Search } from 'lucide-react'
2 | import React from 'react'
3 | import { Badge } from './ui/badge'
4 |
5 | type ToolBadgeProps = {
6 | tool: string
7 | children: React.ReactNode
8 | className?: string
9 | }
10 |
11 | export const ToolBadge: React.FC = ({
12 | tool,
13 | children,
14 | className
15 | }) => {
16 | const icon: Record = {
17 | search: ,
18 | retrieve: ,
19 | videoSearch:
20 | }
21 |
22 | return (
23 |
24 | {icon[tool]}
25 | {children}
26 |
27 | )
28 | }
29 |
--------------------------------------------------------------------------------
/components/tool-section.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import { ToolInvocation } from 'ai'
4 | import { QuestionConfirmation } from './question-confirmation'
5 | import RetrieveSection from './retrieve-section'
6 | import { SearchSection } from './search-section'
7 | import { VideoSearchSection } from './video-search-section'
8 |
9 | interface ToolSectionProps {
10 | tool: ToolInvocation
11 | isOpen: boolean
12 | onOpenChange: (open: boolean) => void
13 | addToolResult?: (params: { toolCallId: string; result: any }) => void
14 | }
15 |
16 | export function ToolSection({
17 | tool,
18 | isOpen,
19 | onOpenChange,
20 | addToolResult
21 | }: ToolSectionProps) {
22 | // Special handling for ask_question tool
23 | if (tool.toolName === 'ask_question') {
24 | // When waiting for user input
25 | if (tool.state === 'call' && addToolResult) {
26 | return (
27 | {
30 | addToolResult({
31 | toolCallId,
32 | result: approved
33 | ? response
34 | : {
35 | declined: true,
36 | skipped: response?.skipped,
37 | message: 'User declined this question'
38 | }
39 | })
40 | }}
41 | />
42 | )
43 | }
44 |
45 | // When result is available, display the result
46 | if (tool.state === 'result') {
47 | return (
48 | {}} // Not used in result display mode
52 | />
53 | )
54 | }
55 | }
56 |
57 | switch (tool.toolName) {
58 | case 'search':
59 | return (
60 |
65 | )
66 | case 'videoSearch':
67 | return (
68 |
73 | )
74 | case 'retrieve':
75 | return (
76 |
81 | )
82 | default:
83 | return null
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/components/ui/avatar.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import * as AvatarPrimitive from '@radix-ui/react-avatar'
4 | import * as React from 'react'
5 |
6 | import { cn } from '@/lib/utils/index'
7 |
8 | const Avatar = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(({ className, ...props }, ref) => (
12 |
20 | ))
21 | Avatar.displayName = AvatarPrimitive.Root.displayName
22 |
23 | const AvatarImage = React.forwardRef<
24 | React.ElementRef,
25 | React.ComponentPropsWithoutRef
26 | >(({ className, ...props }, ref) => (
27 |
32 | ))
33 | AvatarImage.displayName = AvatarPrimitive.Image.displayName
34 |
35 | const AvatarFallback = React.forwardRef<
36 | React.ElementRef,
37 | React.ComponentPropsWithoutRef
38 | >(({ className, ...props }, ref) => (
39 |
47 | ))
48 | AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName
49 |
50 | export { Avatar, AvatarFallback, AvatarImage }
51 |
--------------------------------------------------------------------------------
/components/ui/badge.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { cva, type VariantProps } from 'class-variance-authority'
3 |
4 | import { cn } from '@/lib/utils'
5 |
6 | const badgeVariants = cva(
7 | 'inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2',
8 | {
9 | variants: {
10 | variant: {
11 | default:
12 | 'border-transparent bg-primary text-primary-foreground hover:bg-primary/80',
13 | secondary:
14 | 'border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80',
15 | destructive:
16 | 'border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80',
17 | outline: 'text-foreground'
18 | }
19 | },
20 | defaultVariants: {
21 | variant: 'default'
22 | }
23 | }
24 | )
25 |
26 | export interface BadgeProps
27 | extends React.HTMLAttributes,
28 | VariantProps {}
29 |
30 | function Badge({ className, variant, ...props }: BadgeProps) {
31 | return (
32 |
33 | )
34 | }
35 |
36 | export { Badge, badgeVariants }
37 |
--------------------------------------------------------------------------------
/components/ui/button.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { Slot } from '@radix-ui/react-slot'
3 | import { cva, type VariantProps } from 'class-variance-authority'
4 |
5 | import { cn } from '@/lib/utils/index'
6 |
7 | const buttonVariants = cva(
8 | 'inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50',
9 | {
10 | variants: {
11 | variant: {
12 | default: 'bg-primary text-primary-foreground hover:bg-primary/90',
13 | destructive:
14 | 'bg-destructive text-destructive-foreground hover:bg-destructive/90',
15 | outline:
16 | 'border border-input bg-background hover:bg-accent hover:text-accent-foreground',
17 | secondary:
18 | 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
19 | ghost: 'hover:bg-accent hover:text-accent-foreground',
20 | link: 'text-primary underline-offset-4 hover:underline'
21 | },
22 | size: {
23 | default: 'h-10 px-4 py-2',
24 | sm: 'h-9 rounded-md px-3',
25 | lg: 'h-11 rounded-md px-8',
26 | icon: 'h-10 w-10'
27 | }
28 | },
29 | defaultVariants: {
30 | variant: 'default',
31 | size: 'default'
32 | }
33 | }
34 | )
35 |
36 | export interface ButtonProps
37 | extends React.ButtonHTMLAttributes,
38 | VariantProps {
39 | asChild?: boolean
40 | }
41 |
42 | const Button = React.forwardRef(
43 | ({ className, variant, size, asChild = false, ...props }, ref) => {
44 | const Comp = asChild ? Slot : 'button'
45 | return (
46 |
51 | )
52 | }
53 | )
54 | Button.displayName = 'Button'
55 |
56 | export { Button, buttonVariants }
57 |
--------------------------------------------------------------------------------
/components/ui/card.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 |
3 | import { cn } from '@/lib/utils/index'
4 |
5 | const Card = React.forwardRef<
6 | HTMLDivElement,
7 | React.HTMLAttributes
8 | >(({ className, ...props }, ref) => (
9 |
17 | ))
18 | Card.displayName = 'Card'
19 |
20 | const CardHeader = React.forwardRef<
21 | HTMLDivElement,
22 | React.HTMLAttributes
23 | >(({ className, ...props }, ref) => (
24 |
29 | ))
30 | CardHeader.displayName = 'CardHeader'
31 |
32 | const CardTitle = React.forwardRef<
33 | HTMLParagraphElement,
34 | React.HTMLAttributes
35 | >(({ className, ...props }, ref) => (
36 |
44 | ))
45 | CardTitle.displayName = 'CardTitle'
46 |
47 | const CardDescription = React.forwardRef<
48 | HTMLParagraphElement,
49 | React.HTMLAttributes
50 | >(({ className, ...props }, ref) => (
51 |
56 | ))
57 | CardDescription.displayName = 'CardDescription'
58 |
59 | const CardContent = React.forwardRef<
60 | HTMLDivElement,
61 | React.HTMLAttributes
62 | >(({ className, ...props }, ref) => (
63 |
64 | ))
65 | CardContent.displayName = 'CardContent'
66 |
67 | const CardFooter = React.forwardRef<
68 | HTMLDivElement,
69 | React.HTMLAttributes
70 | >(({ className, ...props }, ref) => (
71 |
76 | ))
77 | CardFooter.displayName = 'CardFooter'
78 |
79 | export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
80 |
--------------------------------------------------------------------------------
/components/ui/checkbox.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import * as React from 'react'
4 | import * as CheckboxPrimitive from '@radix-ui/react-checkbox'
5 | import { Check } from 'lucide-react'
6 |
7 | import { cn } from '@/lib/utils'
8 |
9 | const Checkbox = React.forwardRef<
10 | React.ElementRef,
11 | React.ComponentPropsWithoutRef
12 | >(({ className, ...props }, ref) => (
13 |
21 |
24 |
25 |
26 |
27 | ))
28 | Checkbox.displayName = CheckboxPrimitive.Root.displayName
29 |
30 | export { Checkbox }
31 |
--------------------------------------------------------------------------------
/components/ui/collapsible.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as CollapsiblePrimitive from "@radix-ui/react-collapsible"
4 |
5 | const Collapsible = CollapsiblePrimitive.Root
6 |
7 | const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger
8 |
9 | const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent
10 |
11 | export { Collapsible, CollapsibleTrigger, CollapsibleContent }
12 |
--------------------------------------------------------------------------------
/components/ui/icons.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import { cn } from '@/lib/utils'
4 |
5 | function IconLogo({ className, ...props }: React.ComponentProps<'svg'>) {
6 | return (
7 |
19 | )
20 | }
21 |
22 | export { IconLogo }
23 |
--------------------------------------------------------------------------------
/components/ui/index.ts:
--------------------------------------------------------------------------------
1 | export * from './button'
2 | export * from './tooltip'
3 | export * from './tooltip-button'
4 |
--------------------------------------------------------------------------------
/components/ui/input.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 |
3 | import { cn } from '@/lib/utils/index'
4 |
5 | export interface InputProps
6 | extends React.InputHTMLAttributes {}
7 |
8 | const Input = React.forwardRef(
9 | ({ className, type, ...props }, ref) => {
10 | return (
11 |
20 | )
21 | }
22 | )
23 | Input.displayName = 'Input'
24 |
25 | export { Input }
26 |
--------------------------------------------------------------------------------
/components/ui/label.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import * as React from 'react'
4 | import * as LabelPrimitive from '@radix-ui/react-label'
5 | import { cva, type VariantProps } from 'class-variance-authority'
6 |
7 | import { cn } from '@/lib/utils/index'
8 |
9 | const labelVariants = cva(
10 | 'text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70'
11 | )
12 |
13 | const Label = React.forwardRef<
14 | React.ElementRef,
15 | React.ComponentPropsWithoutRef &
16 | VariantProps
17 | >(({ className, ...props }, ref) => (
18 |
23 | ))
24 | Label.displayName = LabelPrimitive.Root.displayName
25 |
26 | export { Label }
27 |
--------------------------------------------------------------------------------
/components/ui/markdown.tsx:
--------------------------------------------------------------------------------
1 | import { FC, memo } from 'react'
2 | import ReactMarkdown, { Options } from 'react-markdown'
3 |
4 | export const MemoizedReactMarkdown: FC = memo(
5 | ReactMarkdown,
6 | (prevProps, nextProps) =>
7 | prevProps.children === nextProps.children &&
8 | prevProps.className === nextProps.className
9 | )
10 |
--------------------------------------------------------------------------------
/components/ui/popover.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as PopoverPrimitive from "@radix-ui/react-popover"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const Popover = PopoverPrimitive.Root
9 |
10 | const PopoverTrigger = PopoverPrimitive.Trigger
11 |
12 | const PopoverContent = React.forwardRef<
13 | React.ElementRef,
14 | React.ComponentPropsWithoutRef
15 | >(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
16 |
17 |
27 |
28 | ))
29 | PopoverContent.displayName = PopoverPrimitive.Content.displayName
30 |
31 | export { Popover, PopoverTrigger, PopoverContent }
32 |
--------------------------------------------------------------------------------
/components/ui/resizable.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import { GripVertical } from 'lucide-react'
4 | import * as ResizablePrimitive from 'react-resizable-panels'
5 |
6 | import { cn } from '@/lib/utils/index'
7 |
8 | const ResizablePanelGroup = ({
9 | className,
10 | ...props
11 | }: React.ComponentProps) => (
12 |
19 | )
20 |
21 | const ResizablePanel = ResizablePrimitive.Panel
22 |
23 | const ResizableHandle = ({
24 | withHandle,
25 | className,
26 | ...props
27 | }: React.ComponentProps & {
28 | withHandle?: boolean
29 | }) => (
30 | div]:rotate-90',
33 | className
34 | )}
35 | {...props}
36 | >
37 | {withHandle && (
38 |
39 |
40 |
41 | )}
42 |
43 | )
44 |
45 | export { ResizableHandle, ResizablePanel, ResizablePanelGroup }
46 |
--------------------------------------------------------------------------------
/components/ui/separator.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import * as React from 'react'
4 | import * as SeparatorPrimitive from '@radix-ui/react-separator'
5 |
6 | import { cn } from '@/lib/utils/index'
7 |
8 | const Separator = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(
12 | (
13 | { className, orientation = 'horizontal', decorative = true, ...props },
14 | ref
15 | ) => (
16 |
27 | )
28 | )
29 | Separator.displayName = SeparatorPrimitive.Root.displayName
30 |
31 | export { Separator }
32 |
--------------------------------------------------------------------------------
/components/ui/skeleton.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from '@/lib/utils/index'
2 |
3 | function Skeleton({
4 | className,
5 | ...props
6 | }: React.HTMLAttributes) {
7 | return (
8 |
12 | )
13 | }
14 |
15 | export { Skeleton }
16 |
--------------------------------------------------------------------------------
/components/ui/slider.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import * as React from 'react'
4 | import * as SliderPrimitive from '@radix-ui/react-slider'
5 |
6 | import { cn } from '@/lib/utils'
7 |
8 | const Slider = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(({ className, ...props }, ref) => (
12 |
20 |
21 |
22 |
23 |
24 |
25 | ))
26 | Slider.displayName = SliderPrimitive.Root.displayName
27 |
28 | export { Slider }
29 |
--------------------------------------------------------------------------------
/components/ui/sonner.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { useTheme } from "next-themes"
4 | import { Toaster as Sonner } from "sonner"
5 |
6 | type ToasterProps = React.ComponentProps
7 |
8 | const Toaster = ({ ...props }: ToasterProps) => {
9 | const { theme = "system" } = useTheme()
10 |
11 | return (
12 |
28 | )
29 | }
30 |
31 | export { Toaster }
32 |
--------------------------------------------------------------------------------
/components/ui/spinner.tsx:
--------------------------------------------------------------------------------
1 | // Based on: https://github.com/vercel/ai/blob/main/examples/next-ai-rsc/components/llm-stocks/spinner.tsx
2 |
3 | import { cn } from '@/lib/utils'
4 | import { IconLogo } from './icons'
5 |
6 | interface SpinnerProps extends React.SVGProps {}
7 |
8 | export const Spinner = ({ className, ...props }: SpinnerProps) => (
9 |
22 | )
23 |
24 | export const LogoSpinner = () => (
25 |
26 |
27 |
28 | )
29 |
--------------------------------------------------------------------------------
/components/ui/status-indicator.tsx:
--------------------------------------------------------------------------------
1 | import { LucideIcon } from 'lucide-react'
2 | import { ReactNode } from 'react'
3 |
4 | interface StatusIndicatorProps {
5 | icon: LucideIcon
6 | iconClassName?: string
7 | children: ReactNode
8 | }
9 |
10 | export function StatusIndicator({
11 | icon: Icon,
12 | iconClassName,
13 | children
14 | }: StatusIndicatorProps) {
15 | return (
16 |
17 |
18 | {children}
19 |
20 | )
21 | }
22 |
--------------------------------------------------------------------------------
/components/ui/switch.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import * as React from 'react'
4 | import * as SwitchPrimitives from '@radix-ui/react-switch'
5 |
6 | import { cn } from '@/lib/utils'
7 |
8 | const Switch = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(({ className, ...props }, ref) => (
12 |
20 |
25 |
26 | ))
27 | Switch.displayName = SwitchPrimitives.Root.displayName
28 |
29 | export { Switch }
30 |
--------------------------------------------------------------------------------
/components/ui/textarea.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 |
3 | import { cn } from '@/lib/utils'
4 |
5 | export interface TextareaProps
6 | extends React.TextareaHTMLAttributes {}
7 |
8 | const Textarea = React.forwardRef(
9 | ({ className, ...props }, ref) => {
10 | return (
11 |
19 | )
20 | }
21 | )
22 | Textarea.displayName = 'Textarea'
23 |
24 | export { Textarea }
25 |
--------------------------------------------------------------------------------
/components/ui/toggle.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import * as TogglePrimitive from '@radix-ui/react-toggle'
4 | import { cva, type VariantProps } from 'class-variance-authority'
5 | import * as React from 'react'
6 |
7 | import { cn } from '@/lib/utils'
8 |
9 | const toggleVariants = cva(
10 | 'inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors hover:bg-muted hover:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 gap-2',
11 | {
12 | variants: {
13 | variant: {
14 | default:
15 | 'bg-transparent data-[state=on]:bg-accent data-[state=on]:text-accent-foreground',
16 | outline:
17 | 'border border-input hover:bg-accent hover:text-accent-foreground data-[state=on]:bg-accent data-[state=on]:text-accent-foreground'
18 | },
19 | size: {
20 | default: 'h-10 px-3 min-w-10',
21 | sm: 'h-9 px-2.5 min-w-9',
22 | lg: 'h-11 px-5 min-w-11'
23 | }
24 | },
25 | defaultVariants: {
26 | variant: 'default',
27 | size: 'default'
28 | }
29 | }
30 | )
31 |
32 | const Toggle = React.forwardRef<
33 | React.ElementRef,
34 | React.ComponentPropsWithoutRef &
35 | VariantProps
36 | >(({ className, variant, size, ...props }, ref) => (
37 |
42 | ))
43 |
44 | Toggle.displayName = TogglePrimitive.Root.displayName
45 |
46 | export { Toggle, toggleVariants }
47 |
--------------------------------------------------------------------------------
/components/ui/tooltip-button.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import * as React from 'react'
4 | import { Button, ButtonProps } from '@/components/ui/button'
5 | import {
6 | Tooltip,
7 | TooltipContent,
8 | TooltipTrigger
9 | } from '@/components/ui/tooltip'
10 |
11 | interface TooltipButtonProps extends ButtonProps {
12 | /**
13 | * The tooltip content to display.
14 | * Can be a string or TooltipContent props.
15 | */
16 | tooltipContent: string | Omit, 'children'> & {
17 | children: React.ReactNode
18 | }
19 | /**
20 | * The content of the button.
21 | */
22 | children: React.ReactNode
23 | }
24 |
25 | /**
26 | * A button component with a tooltip.
27 | */
28 | export const TooltipButton = React.forwardRef<
29 | HTMLButtonElement,
30 | TooltipButtonProps
31 | >(({ tooltipContent, children, ...buttonProps }, ref) => {
32 | const tooltipProps =
33 | typeof tooltipContent === 'string'
34 | ? { children: {tooltipContent}
}
35 | : tooltipContent
36 |
37 | return (
38 |
39 |
40 |
43 |
44 |
45 |
46 | )
47 | })
48 |
49 | TooltipButton.displayName = 'TooltipButton'
50 |
--------------------------------------------------------------------------------
/components/ui/tooltip.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as TooltipPrimitive from "@radix-ui/react-tooltip"
5 |
6 | import { cn } from "@/lib/utils/index"
7 |
8 | const TooltipProvider = TooltipPrimitive.Provider
9 |
10 | const Tooltip = TooltipPrimitive.Root
11 |
12 | const TooltipTrigger = TooltipPrimitive.Trigger
13 |
14 | const TooltipContent = React.forwardRef<
15 | React.ElementRef,
16 | React.ComponentPropsWithoutRef
17 | >(({ className, sideOffset = 4, ...props }, ref) => (
18 |
27 | ))
28 | TooltipContent.displayName = TooltipPrimitive.Content.displayName
29 |
30 | export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
31 |
--------------------------------------------------------------------------------
/components/update-password-form.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import { Button } from '@/components/ui/button'
4 | import {
5 | Card,
6 | CardContent,
7 | CardDescription,
8 | CardHeader,
9 | CardTitle
10 | } from '@/components/ui/card'
11 | import { Input } from '@/components/ui/input'
12 | import { Label } from '@/components/ui/label'
13 | import { createClient } from '@/lib/supabase/client'
14 | import { cn } from '@/lib/utils/index'
15 | import { useRouter } from 'next/navigation'
16 | import { useState } from 'react'
17 |
18 | export function UpdatePasswordForm({
19 | className,
20 | ...props
21 | }: React.ComponentPropsWithoutRef<'div'>) {
22 | const [password, setPassword] = useState('')
23 | const [error, setError] = useState(null)
24 | const [isLoading, setIsLoading] = useState(false)
25 | const router = useRouter()
26 |
27 | const handleForgotPassword = async (e: React.FormEvent) => {
28 | e.preventDefault()
29 | const supabase = createClient()
30 | setIsLoading(true)
31 | setError(null)
32 |
33 | try {
34 | const { error } = await supabase.auth.updateUser({ password })
35 | if (error) throw error
36 | // Redirect to root and refresh to ensure server components get updated session.
37 | router.push('/')
38 | router.refresh()
39 | } catch (error: unknown) {
40 | setError(error instanceof Error ? error.message : 'An error occurred')
41 | } finally {
42 | setIsLoading(false)
43 | }
44 | }
45 |
46 | return (
47 |
48 |
49 |
50 | Reset Your Password
51 |
52 | Please enter your new password below.
53 |
54 |
55 |
56 |
75 |
76 |
77 |
78 | )
79 | }
80 |
--------------------------------------------------------------------------------
/components/user-message.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import { cn } from '@/lib/utils'
4 | import { Pencil } from 'lucide-react'
5 | import React, { useState } from 'react'
6 | import TextareaAutosize from 'react-textarea-autosize'
7 | import { CollapsibleMessage } from './collapsible-message'
8 | import { Button } from './ui/button'
9 |
10 | type UserMessageProps = {
11 | message: string
12 | messageId?: string
13 | onUpdateMessage?: (messageId: string, newContent: string) => Promise
14 | }
15 |
16 | export const UserMessage: React.FC = ({
17 | message,
18 | messageId,
19 | onUpdateMessage
20 | }) => {
21 | const [isEditing, setIsEditing] = useState(false)
22 | const [editedContent, setEditedContent] = useState(message)
23 |
24 | const handleEditClick = (e: React.MouseEvent) => {
25 | e.stopPropagation()
26 | setEditedContent(message)
27 | setIsEditing(true)
28 | }
29 |
30 | const handleCancelClick = () => {
31 | setIsEditing(false)
32 | }
33 |
34 | const handleSaveClick = async () => {
35 | if (!onUpdateMessage || !messageId) return
36 |
37 | setIsEditing(false)
38 |
39 | try {
40 | await onUpdateMessage(messageId, editedContent)
41 | } catch (error) {
42 | console.error('Failed to save message:', error)
43 | }
44 | }
45 |
46 | return (
47 |
48 |
52 | {isEditing ? (
53 |
54 |
setEditedContent(e.target.value)}
57 | autoFocus
58 | className="resize-none flex w-full rounded-md border border-input bg-background px-3 py-2 text-sm placeholder:text-muted-foreground focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50"
59 | minRows={2}
60 | maxRows={10}
61 | />
62 |
63 |
66 |
69 |
70 |
71 | ) : (
72 |
93 | )}
94 |
95 |
96 | )
97 | }
98 |
--------------------------------------------------------------------------------
/components/video-search-results.tsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable @next/next/no-img-element */
2 | 'use client'
3 |
4 | import { SerperSearchResultItem, SerperSearchResults } from '@/lib/types'
5 | import { VideoResultGrid } from './video-result-grid'
6 |
7 | export interface VideoSearchResultsProps {
8 | results: SerperSearchResults
9 | }
10 |
11 | export function VideoSearchResults({ results }: VideoSearchResultsProps) {
12 | const videos = results.videos.filter((video: SerperSearchResultItem) => {
13 | try {
14 | return new URL(video.link).pathname === '/watch'
15 | } catch (e) {
16 | console.error('Invalid video URL:', video.link)
17 | return false
18 | }
19 | })
20 |
21 | const query = results.searchParameters?.q || ''
22 |
23 | if (!videos || videos.length === 0) {
24 | return No videos found
25 | }
26 |
27 | return
28 | }
29 |
--------------------------------------------------------------------------------
/components/video-search-section.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import { useArtifact } from '@/components/artifact/artifact-context'
4 | import { CHAT_ID } from '@/lib/constants'
5 | import type { SerperSearchResults } from '@/lib/types'
6 | import { useChat } from '@ai-sdk/react'
7 | import { ToolInvocation } from 'ai'
8 | import { CollapsibleMessage } from './collapsible-message'
9 | import { DefaultSkeleton } from './default-skeleton'
10 | import { Section, ToolArgsSection } from './section'
11 | import { VideoSearchResults } from './video-search-results'
12 |
13 | interface VideoSearchSectionProps {
14 | tool: ToolInvocation
15 | isOpen: boolean
16 | onOpenChange: (open: boolean) => void
17 | }
18 |
19 | export function VideoSearchSection({
20 | tool,
21 | isOpen,
22 | onOpenChange
23 | }: VideoSearchSectionProps) {
24 | const { status } = useChat({
25 | id: CHAT_ID
26 | })
27 | const isLoading = status === 'submitted' || status === 'streaming'
28 |
29 | const isToolLoading = tool.state === 'call'
30 | const videoResults: SerperSearchResults =
31 | tool.state === 'result' ? tool.result : undefined
32 | const query = tool.args?.query as string | undefined
33 |
34 | const { open } = useArtifact()
35 | const header = (
36 |
46 | )
47 |
48 | return (
49 |
57 | {!isLoading && videoResults ? (
58 |
61 | ) : (
62 |
63 | )}
64 |
65 | )
66 | }
67 |
--------------------------------------------------------------------------------
/docker-compose.yaml:
--------------------------------------------------------------------------------
1 | # Docker Compose configuration for the morphic-stack development environment
2 |
3 | name: morphic-stack
4 | services:
5 | morphic:
6 | image: ghcr.io/${GITHUB_REPOSITORY:-your-username/morphic}:latest
7 | command: bun start -H 0.0.0.0
8 | build:
9 | context: .
10 | dockerfile: Dockerfile
11 | cache_from:
12 | - morphic:builder
13 | - morphic:latest
14 | env_file: .env.local # Load environment variables
15 | ports:
16 | - '3000:3000' # Maps port 3000 on the host to port 3000 in the container.
17 | depends_on:
18 | - redis
19 | - searxng
20 |
21 | redis:
22 | image: redis:alpine
23 | ports:
24 | - '6379:6379'
25 | volumes:
26 | - redis_data:/data
27 | command: redis-server --appendonly yes
28 |
29 | searxng:
30 | image: searxng/searxng
31 | ports:
32 | - '${SEARXNG_PORT:-8080}:8080'
33 | env_file: .env.local # can remove if you want to use env variables or in settings.yml
34 | volumes:
35 | - ./searxng-limiter.toml:/etc/searxng/limiter.toml
36 | - ./searxng-settings.yml:/etc/searxng/settings.yml
37 | - searxng_data:/data
38 |
39 | volumes:
40 | redis_data:
41 | searxng_data:
42 |
--------------------------------------------------------------------------------
/hooks/use-current-user-image.ts:
--------------------------------------------------------------------------------
1 | import { createClient } from '@/lib/supabase/client'
2 | import { useEffect, useState } from 'react'
3 |
4 | export const useCurrentUserImage = () => {
5 | const [image, setImage] = useState(null)
6 |
7 | useEffect(() => {
8 | const fetchUserImage = async () => {
9 | const { data, error } = await createClient().auth.getSession()
10 | if (error) {
11 | console.error(error)
12 | }
13 |
14 | setImage(data.session?.user.user_metadata.avatar_url ?? null)
15 | }
16 | fetchUserImage()
17 | }, [])
18 |
19 | return image
20 | }
21 |
--------------------------------------------------------------------------------
/hooks/use-current-user-name.ts:
--------------------------------------------------------------------------------
1 | import { createClient } from '@/lib/supabase/client'
2 | import { useEffect, useState } from 'react'
3 |
4 | export const useCurrentUserName = () => {
5 | const [name, setName] = useState(null)
6 |
7 | useEffect(() => {
8 | const fetchProfileName = async () => {
9 | const { data, error } = await createClient().auth.getSession()
10 | if (error) {
11 | console.error(error)
12 | }
13 |
14 | setName(data.session?.user.user_metadata.full_name ?? '?')
15 | }
16 |
17 | fetchProfileName()
18 | }, [])
19 |
20 | return name || '?'
21 | }
22 |
--------------------------------------------------------------------------------
/hooks/use-mobile.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | const MOBILE_BREAKPOINT = 768
4 |
5 | export function useIsMobile() {
6 | const [isMobile, setIsMobile] = React.useState(undefined)
7 |
8 | React.useEffect(() => {
9 | const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`)
10 | const onChange = () => {
11 | setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
12 | }
13 | mql.addEventListener("change", onChange)
14 | setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
15 | return () => mql.removeEventListener("change", onChange)
16 | }, [])
17 |
18 | return !!isMobile
19 | }
20 |
--------------------------------------------------------------------------------
/lib/agents/generate-related-questions.ts:
--------------------------------------------------------------------------------
1 | import { relatedSchema } from '@/lib/schema/related'
2 | import { CoreMessage, generateObject } from 'ai'
3 | import {
4 | getModel,
5 | getToolCallModel,
6 | isToolCallSupported
7 | } from '../utils/registry'
8 |
9 | export async function generateRelatedQuestions(
10 | messages: CoreMessage[],
11 | model: string
12 | ) {
13 | const lastMessages = messages.slice(-1).map(message => ({
14 | ...message,
15 | role: 'user'
16 | })) as CoreMessage[]
17 |
18 | const supportedModel = isToolCallSupported(model)
19 | const currentModel = supportedModel
20 | ? getModel(model)
21 | : getToolCallModel(model)
22 |
23 | const result = await generateObject({
24 | model: currentModel,
25 | system: `As a professional web researcher, your task is to generate a set of three queries that explore the subject matter more deeply, building upon the initial query and the information uncovered in its search results.
26 |
27 | For instance, if the original query was "Starship's third test flight key milestones", your output should follow this format:
28 |
29 | Aim to create queries that progressively delve into more specific aspects, implications, or adjacent topics related to the initial query. The goal is to anticipate the user's potential information needs and guide them towards a more comprehensive understanding of the subject matter.
30 | Please match the language of the response to the user's language.`,
31 | messages: lastMessages,
32 | schema: relatedSchema
33 | })
34 |
35 | return result
36 | }
37 |
--------------------------------------------------------------------------------
/lib/agents/manual-researcher.ts:
--------------------------------------------------------------------------------
1 | import { CoreMessage, smoothStream, streamText } from 'ai'
2 | import { getModel } from '../utils/registry'
3 |
4 | const BASE_SYSTEM_PROMPT = `
5 | Instructions:
6 |
7 | You are a helpful AI assistant providing accurate information.
8 |
9 | 1. Provide comprehensive and detailed responses to user questions
10 | 2. Use markdown to structure your responses with appropriate headings
11 | 3. Acknowledge when you are uncertain about specific details
12 | 4. Focus on maintaining high accuracy in your responses
13 | `
14 |
15 | const SEARCH_ENABLED_PROMPT = `
16 | ${BASE_SYSTEM_PROMPT}
17 |
18 | When analyzing search results:
19 | 1. Analyze the provided search results carefully to answer the user's question
20 | 2. Always cite sources using the [number](url) format, matching the order of search results
21 | 3. If multiple sources are relevant, include all of them using comma-separated citations
22 | 4. Only use information that has a URL available for citation
23 | 5. If the search results don't contain relevant information, acknowledge this and provide a general response
24 |
25 | Citation Format:
26 | [number](url)
27 | `
28 |
29 | const SEARCH_DISABLED_PROMPT = `
30 | ${BASE_SYSTEM_PROMPT}
31 |
32 | Important:
33 | 1. Provide responses based on your general knowledge
34 | 2. Be clear about any limitations in your knowledge
35 | 3. Suggest when searching for additional information might be beneficial
36 | `
37 |
38 | interface ManualResearcherConfig {
39 | messages: CoreMessage[]
40 | model: string
41 | isSearchEnabled?: boolean
42 | }
43 |
44 | type ManualResearcherReturn = Parameters[0]
45 |
46 | export function manualResearcher({
47 | messages,
48 | model,
49 | isSearchEnabled = true
50 | }: ManualResearcherConfig): ManualResearcherReturn {
51 | try {
52 | const currentDate = new Date().toLocaleString()
53 | const systemPrompt = isSearchEnabled
54 | ? SEARCH_ENABLED_PROMPT
55 | : SEARCH_DISABLED_PROMPT
56 |
57 | return {
58 | model: getModel(model),
59 | system: `${systemPrompt}\nCurrent date and time: ${currentDate}`,
60 | messages,
61 | temperature: 0.6,
62 | topP: 1,
63 | topK: 40,
64 | experimental_transform: smoothStream()
65 | }
66 | } catch (error) {
67 | console.error('Error in manualResearcher:', error)
68 | throw error
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/lib/agents/researcher.ts:
--------------------------------------------------------------------------------
1 | import { CoreMessage, smoothStream, streamText } from 'ai'
2 | import { createQuestionTool } from '../tools/question'
3 | import { retrieveTool } from '../tools/retrieve'
4 | import { createSearchTool } from '../tools/search'
5 | import { createVideoSearchTool } from '../tools/video-search'
6 | import { getModel } from '../utils/registry'
7 |
8 | const SYSTEM_PROMPT = `
9 | Instructions:
10 |
11 | You are a helpful AI assistant with access to real-time web search, content retrieval, video search capabilities, and the ability to ask clarifying questions.
12 |
13 | When asked a question, you should:
14 | 1. First, determine if you need more information to properly understand the user's query
15 | 2. **If the query is ambiguous or lacks specific details, use the ask_question tool to create a structured question with relevant options**
16 | 3. If you have enough information, search for relevant information using the search tool when needed
17 | 4. Use the retrieve tool to get detailed content from specific URLs
18 | 5. Use the video search tool when looking for video content
19 | 6. Analyze all search results to provide accurate, up-to-date information
20 | 7. Always cite sources using the [number](url) format, matching the order of search results. If multiple sources are relevant, include all of them, and comma separate them. Only use information that has a URL available for citation.
21 | 8. If results are not relevant or helpful, rely on your general knowledge
22 | 9. Provide comprehensive and detailed responses based on search results, ensuring thorough coverage of the user's question
23 | 10. Use markdown to structure your responses. Use headings to break up the content into sections.
24 | 11. **Use the retrieve tool only with user-provided URLs.**
25 |
26 | When using the ask_question tool:
27 | - Create clear, concise questions
28 | - Provide relevant predefined options
29 | - Enable free-form input when appropriate
30 | - Match the language to the user's language (except option values which must be in English)
31 |
32 | Citation Format:
33 | [number](url)
34 | `
35 |
36 | type ResearcherReturn = Parameters[0]
37 |
38 | export function researcher({
39 | messages,
40 | model,
41 | searchMode
42 | }: {
43 | messages: CoreMessage[]
44 | model: string
45 | searchMode: boolean
46 | }): ResearcherReturn {
47 | try {
48 | const currentDate = new Date().toLocaleString()
49 |
50 | // Create model-specific tools
51 | const searchTool = createSearchTool(model)
52 | const videoSearchTool = createVideoSearchTool(model)
53 | const askQuestionTool = createQuestionTool(model)
54 |
55 | return {
56 | model: getModel(model),
57 | system: `${SYSTEM_PROMPT}\nCurrent date and time: ${currentDate}`,
58 | messages,
59 | tools: {
60 | search: searchTool,
61 | retrieve: retrieveTool,
62 | videoSearch: videoSearchTool,
63 | ask_question: askQuestionTool
64 | },
65 | experimental_activeTools: searchMode
66 | ? ['search', 'retrieve', 'videoSearch', 'ask_question']
67 | : [],
68 | maxSteps: searchMode ? 5 : 1,
69 | experimental_transform: smoothStream()
70 | }
71 | } catch (error) {
72 | console.error('Error in chatResearcher:', error)
73 | throw error
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/lib/auth/get-current-user.ts:
--------------------------------------------------------------------------------
1 | import { createClient } from '@/lib/supabase/server'
2 |
3 | export async function getCurrentUser() {
4 | const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL
5 | const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY
6 |
7 | if (!supabaseUrl || !supabaseAnonKey) {
8 | return null // Supabase is not configured
9 | }
10 |
11 | const supabase = await createClient()
12 | const { data } = await supabase.auth.getUser()
13 | return data.user ?? null
14 | }
15 |
16 | export async function getCurrentUserId() {
17 | const user = await getCurrentUser()
18 | return user?.id ?? 'anonymous'
19 | }
20 |
--------------------------------------------------------------------------------
/lib/config/models.ts:
--------------------------------------------------------------------------------
1 | import { Model } from '@/lib/types/models'
2 | import { getBaseUrl } from '@/lib/utils/url'
3 | import defaultModels from './default-models.json'
4 |
5 | export function validateModel(model: any): model is Model {
6 | return (
7 | typeof model.id === 'string' &&
8 | typeof model.name === 'string' &&
9 | typeof model.provider === 'string' &&
10 | typeof model.providerId === 'string' &&
11 | typeof model.enabled === 'boolean' &&
12 | (model.toolCallType === 'native' || model.toolCallType === 'manual') &&
13 | (model.toolCallModel === undefined ||
14 | typeof model.toolCallModel === 'string')
15 | )
16 | }
17 |
18 | export async function getModels(): Promise {
19 | try {
20 | // Get the base URL using the centralized utility function
21 | const baseUrlObj = await getBaseUrl()
22 |
23 | // Construct the models.json URL
24 | const modelUrl = new URL('/config/models.json', baseUrlObj)
25 | console.log('Attempting to fetch models from:', modelUrl.toString())
26 |
27 | try {
28 | const response = await fetch(modelUrl, {
29 | cache: 'no-store',
30 | headers: {
31 | Accept: 'application/json'
32 | }
33 | })
34 |
35 | if (!response.ok) {
36 | console.warn(
37 | `HTTP error when fetching models: ${response.status} ${response.statusText}`
38 | )
39 | throw new Error(`HTTP error! status: ${response.status}`)
40 | }
41 |
42 | const text = await response.text()
43 |
44 | // Check if the response starts with HTML doctype
45 | if (text.trim().toLowerCase().startsWith('(false)
13 |
14 | const copyToClipboard = (value: string) => {
15 | if (typeof window === 'undefined' || !navigator.clipboard?.writeText) {
16 | return
17 | }
18 |
19 | if (!value) {
20 | return
21 | }
22 |
23 | navigator.clipboard.writeText(value).then(() => {
24 | setIsCopied(true)
25 |
26 | setTimeout(() => {
27 | setIsCopied(false)
28 | }, timeout)
29 | })
30 | }
31 |
32 | return { isCopied, copyToClipboard }
33 | }
34 |
--------------------------------------------------------------------------------
/lib/hooks/use-media-query.ts:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import { useEffect, useState } from 'react'
4 |
5 | /**
6 | * Custom hook to track media query matches.
7 | * @param query - The media query string (e.g., '(max-width: 767px)').
8 | * @returns boolean - True if the media query matches, false otherwise.
9 | */
10 | export function useMediaQuery(query: string): boolean {
11 | const [matches, setMatches] = useState(false)
12 |
13 | useEffect(() => {
14 | // Ensure window is available (client-side only)
15 | if (typeof window === 'undefined') {
16 | return
17 | }
18 | const mql = window.matchMedia(query)
19 | const handler = (e: MediaQueryListEvent) => setMatches(e.matches)
20 |
21 | // Set initial state
22 | setMatches(mql.matches)
23 |
24 | // Add listener
25 | mql.addEventListener('change', handler)
26 |
27 | // Cleanup listener on unmount
28 | return () => mql.removeEventListener('change', handler)
29 | }, [query])
30 |
31 | return matches
32 | }
33 |
--------------------------------------------------------------------------------
/lib/schema/question.ts:
--------------------------------------------------------------------------------
1 | import { z } from 'zod'
2 |
3 | // Standard schema with optional fields for inputLabel and inputPlaceholder
4 | export const questionSchema = z.object({
5 | question: z.string().describe('The main question to ask the user'),
6 | options: z
7 | .array(
8 | z.object({
9 | value: z.string().describe('Option identifier (always in English)'),
10 | label: z.string().describe('Display text for the option')
11 | })
12 | )
13 | .describe('List of predefined options'),
14 | allowsInput: z.boolean().describe('Whether to allow free-form text input'),
15 | inputLabel: z.string().optional().describe('Label for free-form input field'),
16 | inputPlaceholder: z
17 | .string()
18 | .optional()
19 | .describe('Placeholder text for input field')
20 | })
21 |
22 | // Strict schema with all fields required, for specific models like o3-mini
23 | export const strictQuestionSchema = z.object({
24 | question: z.string().describe('The main question to ask the user'),
25 | options: z
26 | .array(
27 | z.object({
28 | value: z.string().describe('Option identifier (always in English)'),
29 | label: z.string().describe('Display text for the option')
30 | })
31 | )
32 | .describe('List of predefined options'),
33 | allowsInput: z.boolean().describe('Whether to allow free-form text input'),
34 | inputLabel: z.string().describe('Label for free-form input field'),
35 | inputPlaceholder: z.string().describe('Placeholder text for input field')
36 | })
37 |
38 | /**
39 | * Returns the appropriate question schema based on the full model name.
40 | * Uses the strict schema for OpenAI models starting with 'o'.
41 | */
42 | export function getQuestionSchemaForModel(fullModel: string) {
43 | const [provider, modelName] = fullModel?.split(':') ?? []
44 | const useStrictSchema =
45 | (provider === 'openai' || provider === 'azure') &&
46 | modelName?.startsWith('o')
47 | return useStrictSchema ? strictQuestionSchema : questionSchema
48 | }
49 |
--------------------------------------------------------------------------------
/lib/schema/related.tsx:
--------------------------------------------------------------------------------
1 | import { DeepPartial } from 'ai'
2 | import { z } from 'zod'
3 |
4 | export const relatedSchema = z.object({
5 | items: z
6 | .array(
7 | z.object({
8 | query: z.string()
9 | })
10 | )
11 | .length(3)
12 | })
13 | export type PartialRelated = DeepPartial
14 |
15 | export type Related = z.infer
16 |
--------------------------------------------------------------------------------
/lib/schema/retrieve.tsx:
--------------------------------------------------------------------------------
1 | import { DeepPartial } from 'ai'
2 | import { z } from 'zod'
3 |
4 | export const retrieveSchema = z.object({
5 | url: z.string().describe('The url to retrieve')
6 | })
7 |
8 | export type PartialInquiry = DeepPartial
9 |
--------------------------------------------------------------------------------
/lib/schema/search.tsx:
--------------------------------------------------------------------------------
1 | import { DeepPartial } from 'ai'
2 | import { z } from 'zod'
3 |
4 | export const searchSchema = z.object({
5 | query: z.string().describe('The query to search for'),
6 | max_results: z
7 | .number()
8 | .optional()
9 | .describe('The maximum number of results to return. default is 20'),
10 | search_depth: z
11 | .string()
12 | .optional()
13 | .describe(
14 | 'The depth of the search. Allowed values are "basic" or "advanced"'
15 | ),
16 | include_domains: z
17 | .array(z.string())
18 | .optional()
19 | .describe(
20 | 'A list of domains to specifically include in the search results. Default is None, which includes all domains.'
21 | ),
22 | exclude_domains: z
23 | .array(z.string())
24 | .optional()
25 | .describe(
26 | "A list of domains to specifically exclude from the search results. Default is None, which doesn't exclude any domains."
27 | )
28 | })
29 |
30 | // Strict schema with all fields required
31 | export const strictSearchSchema = z.object({
32 | query: z.string().describe('The query to search for'),
33 | max_results: z
34 | .number()
35 | .describe('The maximum number of results to return. default is 20'),
36 | search_depth: z
37 | .enum(['basic', 'advanced'])
38 | .describe('The depth of the search'),
39 | include_domains: z
40 | .array(z.string())
41 | .describe(
42 | 'A list of domains to specifically include in the search results. Default is None, which includes all domains.'
43 | ),
44 | exclude_domains: z
45 | .array(z.string())
46 | .describe(
47 | "A list of domains to specifically exclude from the search results. Default is None, which doesn't exclude any domains."
48 | )
49 | })
50 |
51 | /**
52 | * Returns the appropriate search schema based on the full model name.
53 | * Uses the strict schema for OpenAI models starting with 'o'.
54 | */
55 | export function getSearchSchemaForModel(fullModel: string) {
56 | const [provider, modelName] = fullModel?.split(':') ?? []
57 | const useStrictSchema =
58 | (provider === 'openai' || provider === 'azure') &&
59 | modelName?.startsWith('o')
60 |
61 | // Ensure search_depth is an enum for the strict schema
62 | if (useStrictSchema) {
63 | return strictSearchSchema
64 | } else {
65 | // For the standard schema, keep search_depth as optional string
66 | return searchSchema
67 | }
68 | }
69 |
70 | export type PartialInquiry = DeepPartial
71 |
--------------------------------------------------------------------------------
/lib/streaming/create-tool-calling-stream.ts:
--------------------------------------------------------------------------------
1 | import { researcher } from '@/lib/agents/researcher'
2 | import {
3 | convertToCoreMessages,
4 | CoreMessage,
5 | createDataStreamResponse,
6 | DataStreamWriter,
7 | streamText
8 | } from 'ai'
9 | import { getMaxAllowedTokens, truncateMessages } from '../utils/context-window'
10 | import { isReasoningModel } from '../utils/registry'
11 | import { handleStreamFinish } from './handle-stream-finish'
12 | import { BaseStreamConfig } from './types'
13 |
14 | // Function to check if a message contains ask_question tool invocation
15 | function containsAskQuestionTool(message: CoreMessage) {
16 | // For CoreMessage format, we check the content array
17 | if (message.role !== 'assistant' || !Array.isArray(message.content)) {
18 | return false
19 | }
20 |
21 | // Check if any content item is a tool-call with ask_question tool
22 | return message.content.some(
23 | item => item.type === 'tool-call' && item.toolName === 'ask_question'
24 | )
25 | }
26 |
27 | export function createToolCallingStreamResponse(config: BaseStreamConfig) {
28 | return createDataStreamResponse({
29 | execute: async (dataStream: DataStreamWriter) => {
30 | const { messages, model, chatId, searchMode, userId } = config
31 | const modelId = `${model.providerId}:${model.id}`
32 |
33 | try {
34 | const coreMessages = convertToCoreMessages(messages)
35 | const truncatedMessages = truncateMessages(
36 | coreMessages,
37 | getMaxAllowedTokens(model)
38 | )
39 |
40 | let researcherConfig = await researcher({
41 | messages: truncatedMessages,
42 | model: modelId,
43 | searchMode
44 | })
45 |
46 | const result = streamText({
47 | ...researcherConfig,
48 | onFinish: async result => {
49 | // Check if the last message contains an ask_question tool invocation
50 | const shouldSkipRelatedQuestions =
51 | isReasoningModel(modelId) ||
52 | (result.response.messages.length > 0 &&
53 | containsAskQuestionTool(
54 | result.response.messages[
55 | result.response.messages.length - 1
56 | ] as CoreMessage
57 | ))
58 |
59 | await handleStreamFinish({
60 | responseMessages: result.response.messages,
61 | originalMessages: messages,
62 | model: modelId,
63 | chatId,
64 | dataStream,
65 | userId,
66 | skipRelatedQuestions: shouldSkipRelatedQuestions
67 | })
68 | }
69 | })
70 |
71 | result.mergeIntoDataStream(dataStream)
72 | } catch (error) {
73 | console.error('Stream execution error:', error)
74 | throw error
75 | }
76 | },
77 | onError: error => {
78 | // console.error('Stream error:', error)
79 | return error instanceof Error ? error.message : String(error)
80 | }
81 | })
82 | }
83 |
--------------------------------------------------------------------------------
/lib/streaming/handle-stream-finish.ts:
--------------------------------------------------------------------------------
1 | import { getChat, saveChat } from '@/lib/actions/chat'
2 | import { generateRelatedQuestions } from '@/lib/agents/generate-related-questions'
3 | import { ExtendedCoreMessage } from '@/lib/types'
4 | import { convertToExtendedCoreMessages } from '@/lib/utils'
5 | import { CoreMessage, DataStreamWriter, JSONValue, Message } from 'ai'
6 |
7 | interface HandleStreamFinishParams {
8 | responseMessages: CoreMessage[]
9 | originalMessages: Message[]
10 | model: string
11 | chatId: string
12 | dataStream: DataStreamWriter
13 | userId: string
14 | skipRelatedQuestions?: boolean
15 | annotations?: ExtendedCoreMessage[]
16 | }
17 |
18 | export async function handleStreamFinish({
19 | responseMessages,
20 | originalMessages,
21 | model,
22 | chatId,
23 | dataStream,
24 | userId,
25 | skipRelatedQuestions = false,
26 | annotations = []
27 | }: HandleStreamFinishParams) {
28 | try {
29 | const extendedCoreMessages = convertToExtendedCoreMessages(originalMessages)
30 | let allAnnotations = [...annotations]
31 |
32 | if (!skipRelatedQuestions) {
33 | // Notify related questions loading
34 | const relatedQuestionsAnnotation: JSONValue = {
35 | type: 'related-questions',
36 | data: { items: [] }
37 | }
38 | dataStream.writeMessageAnnotation(relatedQuestionsAnnotation)
39 |
40 | // Generate related questions
41 | const relatedQuestions = await generateRelatedQuestions(
42 | responseMessages,
43 | model
44 | )
45 |
46 | // Create and add related questions annotation
47 | const updatedRelatedQuestionsAnnotation: ExtendedCoreMessage = {
48 | role: 'data',
49 | content: {
50 | type: 'related-questions',
51 | data: relatedQuestions.object
52 | } as JSONValue
53 | }
54 |
55 | dataStream.writeMessageAnnotation(
56 | updatedRelatedQuestionsAnnotation.content as JSONValue
57 | )
58 | allAnnotations.push(updatedRelatedQuestionsAnnotation)
59 | }
60 |
61 | // Create the message to save
62 | const generatedMessages = [
63 | ...extendedCoreMessages,
64 | ...responseMessages.slice(0, -1),
65 | ...allAnnotations, // Add annotations before the last message
66 | ...responseMessages.slice(-1)
67 | ] as ExtendedCoreMessage[]
68 |
69 | if (process.env.ENABLE_SAVE_CHAT_HISTORY !== 'true') {
70 | return
71 | }
72 |
73 | // Get the chat from the database if it exists, otherwise create a new one
74 | const savedChat = (await getChat(chatId, userId)) ?? {
75 | messages: [],
76 | createdAt: new Date(),
77 | userId: userId,
78 | path: `/search/${chatId}`,
79 | title: originalMessages[0].content,
80 | id: chatId
81 | }
82 |
83 | // Save chat with complete response and related questions
84 | await saveChat(
85 | {
86 | ...savedChat,
87 | messages: generatedMessages
88 | },
89 | userId
90 | ).catch(error => {
91 | console.error('Failed to save chat:', error)
92 | throw new Error('Failed to save chat history')
93 | })
94 | } catch (error) {
95 | console.error('Error in handleStreamFinish:', error)
96 | throw error
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/lib/streaming/parse-tool-call.ts:
--------------------------------------------------------------------------------
1 | import { z } from 'zod'
2 |
3 | export interface ToolCall {
4 | tool: string
5 | parameters?: T
6 | }
7 |
8 | function getTagContent(xml: string, tag: string): string {
9 | const match = xml.match(new RegExp(`<${tag}>(.*?)${tag}>`, 's'))
10 | return match ? match[1].trim() : ''
11 | }
12 |
13 | export function parseToolCallXml(
14 | xml: string,
15 | schema?: z.ZodType
16 | ): ToolCall {
17 | const toolCallContent = getTagContent(xml, 'tool_call')
18 | if (!toolCallContent) {
19 | console.warn('No tool_call tag found in response')
20 | return { tool: '' }
21 | }
22 |
23 | const tool = getTagContent(toolCallContent, 'tool')
24 | if (!tool) return { tool: '' }
25 |
26 | const parametersXml = getTagContent(toolCallContent, 'parameters')
27 | if (!parametersXml || !schema) return { tool }
28 |
29 | try {
30 | // Extract all parameter values using tag names from schema
31 | const rawParameters: Record = {}
32 | if (schema instanceof z.ZodObject) {
33 | Object.keys(schema.shape).forEach(key => {
34 | const value = getTagContent(parametersXml, key)
35 | if (value) rawParameters[key] = value
36 | })
37 | }
38 |
39 | // Parse parameters using the provided schema
40 | const parameters = schema.parse({
41 | ...rawParameters,
42 | // Convert comma-separated strings to arrays for array fields with default empty arrays
43 | include_domains:
44 | rawParameters.include_domains
45 | ?.split(',')
46 | .map(d => d.trim())
47 | .filter(Boolean) ?? [],
48 | exclude_domains:
49 | rawParameters.exclude_domains
50 | ?.split(',')
51 | .map(d => d.trim())
52 | .filter(Boolean) ?? [],
53 | // Convert string to number for numeric fields
54 | max_results: rawParameters.max_results
55 | ? parseInt(rawParameters.max_results, 10)
56 | : undefined
57 | })
58 |
59 | return { tool, parameters }
60 | } catch (error) {
61 | console.error('Failed to parse parameters:', error)
62 | return { tool }
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/lib/streaming/types.ts:
--------------------------------------------------------------------------------
1 | import { Message } from 'ai'
2 | import { Model } from '../types/models'
3 |
4 | export interface BaseStreamConfig {
5 | messages: Message[]
6 | model: Model
7 | chatId: string
8 | searchMode: boolean
9 | userId: string
10 | }
11 |
--------------------------------------------------------------------------------
/lib/supabase/client.ts:
--------------------------------------------------------------------------------
1 | import { createBrowserClient } from '@supabase/ssr'
2 |
3 | export function createClient() {
4 | return createBrowserClient(
5 | process.env.NEXT_PUBLIC_SUPABASE_URL!,
6 | process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
7 | )
8 | }
9 |
--------------------------------------------------------------------------------
/lib/supabase/middleware.ts:
--------------------------------------------------------------------------------
1 | import { createServerClient } from '@supabase/ssr'
2 | import { NextResponse, type NextRequest } from 'next/server'
3 |
4 | export async function updateSession(request: NextRequest) {
5 | let supabaseResponse = NextResponse.next({
6 | request
7 | })
8 |
9 | const supabase = createServerClient(
10 | process.env.NEXT_PUBLIC_SUPABASE_URL!,
11 | process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
12 | {
13 | cookies: {
14 | getAll() {
15 | return request.cookies.getAll()
16 | },
17 | setAll(cookiesToSet) {
18 | cookiesToSet.forEach(({ name, value }) =>
19 | request.cookies.set(name, value)
20 | )
21 | supabaseResponse = NextResponse.next({
22 | request
23 | })
24 | cookiesToSet.forEach(({ name, value, options }) =>
25 | supabaseResponse.cookies.set(name, value, options)
26 | )
27 | }
28 | }
29 | }
30 | )
31 |
32 | // Do not run code between createServerClient and
33 | // supabase.auth.getUser(). A simple mistake could make it very hard to debug
34 | // issues with users being randomly logged out.
35 |
36 | // IMPORTANT: DO NOT REMOVE auth.getUser()
37 |
38 | const {
39 | data: { user }
40 | } = await supabase.auth.getUser()
41 |
42 | // Define public paths that don't require authentication
43 | const publicPaths = [
44 | '/', // Root path
45 | '/auth', // Auth-related pages
46 | '/share', // Share pages
47 | '/api' // API routes
48 | // Add other public paths here if needed
49 | ]
50 |
51 | const pathname = request.nextUrl.pathname
52 |
53 | // Redirect to login if the user is not authenticated and the path is not public
54 | if (!user && !publicPaths.some(path => pathname.startsWith(path))) {
55 | // no user, potentially respond by redirecting the user to the login page
56 | const url = request.nextUrl.clone()
57 | url.pathname = '/auth/login'
58 | return NextResponse.redirect(url)
59 | }
60 |
61 | // IMPORTANT: You *must* return the supabaseResponse object as it is.
62 | // If you're creating a new response object with NextResponse.next() make sure to:
63 | // 1. Pass the request in it, like so:
64 | // const myNewResponse = NextResponse.next({ request })
65 | // 2. Copy over the cookies, like so:
66 | // myNewResponse.cookies.setAll(supabaseResponse.cookies.getAll())
67 | // 3. Change the myNewResponse object to fit your needs, but avoid changing
68 | // the cookies!
69 | // 4. Finally:
70 | // return myNewResponse
71 | // If this is not done, you may be causing the browser and server to go out
72 | // of sync and terminate the user's session prematurely!
73 |
74 | return supabaseResponse
75 | }
76 |
--------------------------------------------------------------------------------
/lib/supabase/server.ts:
--------------------------------------------------------------------------------
1 | import { createServerClient } from '@supabase/ssr'
2 | import { cookies } from 'next/headers'
3 |
4 | export async function createClient() {
5 | const cookieStore = await cookies()
6 |
7 | return createServerClient(
8 | process.env.NEXT_PUBLIC_SUPABASE_URL!,
9 | process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
10 | {
11 | cookies: {
12 | getAll() {
13 | return cookieStore.getAll()
14 | },
15 | setAll(cookiesToSet) {
16 | try {
17 | cookiesToSet.forEach(({ name, value, options }) =>
18 | cookieStore.set(name, value, options)
19 | )
20 | } catch {
21 | // The `setAll` method was called from a Server Component.
22 | // This can be ignored if you have middleware refreshing
23 | // user sessions.
24 | }
25 | },
26 | },
27 | }
28 | )
29 | }
30 |
--------------------------------------------------------------------------------
/lib/tools/question.ts:
--------------------------------------------------------------------------------
1 | import { getQuestionSchemaForModel } from '@/lib/schema/question'
2 | import { tool } from 'ai'
3 |
4 | /**
5 | * Creates a question tool with the appropriate schema for the specified model.
6 | */
7 | export function createQuestionTool(fullModel: string) {
8 | return tool({
9 | description:
10 | 'Ask a clarifying question with multiple options when more information is needed',
11 | parameters: getQuestionSchemaForModel(fullModel)
12 | // execute function removed to enable frontend confirmation
13 | })
14 | }
15 |
16 | // Default export for backward compatibility, using a default model
17 | export const askQuestionTool = createQuestionTool('openai:gpt-4o-mini')
18 |
--------------------------------------------------------------------------------
/lib/tools/retrieve.ts:
--------------------------------------------------------------------------------
1 | import { tool } from 'ai'
2 | import { retrieveSchema } from '@/lib/schema/retrieve'
3 | import { SearchResults as SearchResultsType } from '@/lib/types'
4 |
5 | const CONTENT_CHARACTER_LIMIT = 10000
6 |
7 | async function fetchJinaReaderData(
8 | url: string
9 | ): Promise {
10 | try {
11 | const response = await fetch(`https://r.jina.ai/${url}`, {
12 | method: 'GET',
13 | headers: {
14 | Accept: 'application/json',
15 | 'X-With-Generated-Alt': 'true'
16 | }
17 | })
18 | const json = await response.json()
19 | if (!json.data || json.data.length === 0) {
20 | return null
21 | }
22 |
23 | const content = json.data.content.slice(0, CONTENT_CHARACTER_LIMIT)
24 |
25 | return {
26 | results: [
27 | {
28 | title: json.data.title,
29 | content,
30 | url: json.data.url
31 | }
32 | ],
33 | query: '',
34 | images: []
35 | }
36 | } catch (error) {
37 | console.error('Jina Reader API error:', error)
38 | return null
39 | }
40 | }
41 |
42 | async function fetchTavilyExtractData(
43 | url: string
44 | ): Promise {
45 | try {
46 | const apiKey = process.env.TAVILY_API_KEY
47 | const response = await fetch('https://api.tavily.com/extract', {
48 | method: 'POST',
49 | headers: {
50 | 'Content-Type': 'application/json'
51 | },
52 | body: JSON.stringify({ api_key: apiKey, urls: [url] })
53 | })
54 | const json = await response.json()
55 | if (!json.results || json.results.length === 0) {
56 | return null
57 | }
58 |
59 | const result = json.results[0]
60 | const content = result.raw_content.slice(0, CONTENT_CHARACTER_LIMIT)
61 |
62 | return {
63 | results: [
64 | {
65 | title: content.slice(0, 100),
66 | content,
67 | url: result.url
68 | }
69 | ],
70 | query: '',
71 | images: []
72 | }
73 | } catch (error) {
74 | console.error('Tavily Extract API error:', error)
75 | return null
76 | }
77 | }
78 |
79 | export const retrieveTool = tool({
80 | description: 'Retrieve content from the web',
81 | parameters: retrieveSchema,
82 | execute: async ({ url }) => {
83 | let results: SearchResultsType | null
84 |
85 | // Use Jina if the API key is set, otherwise use Tavily
86 | const useJina = process.env.JINA_API_KEY
87 | if (useJina) {
88 | results = await fetchJinaReaderData(url)
89 | } else {
90 | results = await fetchTavilyExtractData(url)
91 | }
92 |
93 | if (!results) {
94 | return null
95 | }
96 |
97 | return results
98 | }
99 | })
100 |
--------------------------------------------------------------------------------
/lib/tools/search/providers/base.ts:
--------------------------------------------------------------------------------
1 | import { SearchResults } from '@/lib/types'
2 |
3 | export interface SearchProvider {
4 | search(
5 | query: string,
6 | maxResults: number,
7 | searchDepth: 'basic' | 'advanced',
8 | includeDomains: string[],
9 | excludeDomains: string[]
10 | ): Promise
11 | }
12 |
13 | export abstract class BaseSearchProvider implements SearchProvider {
14 | abstract search(
15 | query: string,
16 | maxResults: number,
17 | searchDepth: 'basic' | 'advanced',
18 | includeDomains: string[],
19 | excludeDomains: string[]
20 | ): Promise
21 |
22 | protected validateApiKey(key: string | undefined, providerName: string): void {
23 | if (!key) {
24 | throw new Error(`${providerName}_API_KEY is not set in the environment variables`)
25 | }
26 | }
27 |
28 | protected validateApiUrl(url: string | undefined, providerName: string): void {
29 | if (!url) {
30 | throw new Error(`${providerName}_API_URL is not set in the environment variables`)
31 | }
32 | }
33 | }
--------------------------------------------------------------------------------
/lib/tools/search/providers/exa.ts:
--------------------------------------------------------------------------------
1 | import { SearchResults } from '@/lib/types'
2 | import Exa from 'exa-js'
3 | import { BaseSearchProvider } from './base'
4 |
5 | export class ExaSearchProvider extends BaseSearchProvider {
6 | async search(
7 | query: string,
8 | maxResults: number = 10,
9 | _searchDepth: 'basic' | 'advanced' = 'basic',
10 | includeDomains: string[] = [],
11 | excludeDomains: string[] = []
12 | ): Promise {
13 | const apiKey = process.env.EXA_API_KEY
14 | this.validateApiKey(apiKey, 'EXA')
15 |
16 | const exa = new Exa(apiKey)
17 | const exaResults = await exa.searchAndContents(query, {
18 | highlights: true,
19 | numResults: maxResults,
20 | includeDomains,
21 | excludeDomains
22 | })
23 |
24 | return {
25 | results: exaResults.results.map((result: any) => ({
26 | title: result.title,
27 | url: result.url,
28 | content: result.highlight || result.text
29 | })),
30 | query,
31 | images: [],
32 | number_of_results: exaResults.results.length
33 | }
34 | }
35 | }
--------------------------------------------------------------------------------
/lib/tools/search/providers/index.ts:
--------------------------------------------------------------------------------
1 | import { SearchProvider } from './base'
2 | import { ExaSearchProvider } from './exa'
3 | import { SearXNGSearchProvider } from './searxng'
4 | import { TavilySearchProvider } from './tavily'
5 |
6 | export type SearchProviderType = 'tavily' | 'exa' | 'searxng'
7 | export const DEFAULT_PROVIDER: SearchProviderType = 'tavily'
8 |
9 | export function createSearchProvider(type?: SearchProviderType): SearchProvider {
10 | const providerType = type || (process.env.SEARCH_API as SearchProviderType) || DEFAULT_PROVIDER
11 |
12 | switch (providerType) {
13 | case 'tavily':
14 | return new TavilySearchProvider()
15 | case 'exa':
16 | return new ExaSearchProvider()
17 | case 'searxng':
18 | return new SearXNGSearchProvider()
19 | default:
20 | // Default to TavilySearchProvider if an unknown provider is specified
21 | return new TavilySearchProvider()
22 | }
23 | }
24 |
25 | export type { ExaSearchProvider } from './exa'
26 | export { SearXNGSearchProvider } from './searxng'
27 | export { TavilySearchProvider } from './tavily'
28 | export type { SearchProvider }
29 |
30 |
--------------------------------------------------------------------------------
/lib/tools/search/providers/searxng.ts:
--------------------------------------------------------------------------------
1 | import { SearchResultItem, SearchResults, SearXNGResponse, SearXNGResult } from '@/lib/types'
2 | import { BaseSearchProvider } from './base'
3 |
4 | export class SearXNGSearchProvider extends BaseSearchProvider {
5 | async search(
6 | query: string,
7 | maxResults: number = 10,
8 | searchDepth: 'basic' | 'advanced' = 'basic',
9 | includeDomains: string[] = [],
10 | excludeDomains: string[] = []
11 | ): Promise {
12 | const apiUrl = process.env.SEARXNG_API_URL
13 | this.validateApiUrl(apiUrl, 'SEARXNG')
14 |
15 | try {
16 | // Construct the URL with query parameters
17 | const url = new URL(`${apiUrl}/search`)
18 | url.searchParams.append('q', query)
19 | url.searchParams.append('format', 'json')
20 | url.searchParams.append('categories', 'general,images')
21 |
22 | // Apply search depth settings
23 | if (searchDepth === 'advanced') {
24 | url.searchParams.append('time_range', '')
25 | url.searchParams.append('safesearch', '0')
26 | url.searchParams.append('engines', 'google,bing,duckduckgo,wikipedia')
27 | } else {
28 | url.searchParams.append('time_range', 'year')
29 | url.searchParams.append('safesearch', '1')
30 | url.searchParams.append('engines', 'google,bing')
31 | }
32 |
33 | // Apply domain filters if provided
34 | if (includeDomains.length > 0) {
35 | url.searchParams.append('site', includeDomains.join(','))
36 | }
37 |
38 | // Fetch results from SearXNG
39 | const response = await fetch(url.toString(), {
40 | method: 'GET',
41 | headers: {
42 | Accept: 'application/json'
43 | }
44 | })
45 |
46 | if (!response.ok) {
47 | const errorText = await response.text()
48 | console.error(`SearXNG API error (${response.status}):`, errorText)
49 | throw new Error(
50 | `SearXNG API error: ${response.status} ${response.statusText} - ${errorText}`
51 | )
52 | }
53 |
54 | const data: SearXNGResponse = await response.json()
55 |
56 | // Separate general results and image results, and limit to maxResults
57 | const generalResults = data.results
58 | .filter(result => !result.img_src)
59 | .slice(0, maxResults)
60 | const imageResults = data.results
61 | .filter(result => result.img_src)
62 | .slice(0, maxResults)
63 |
64 | // Format the results to match the expected SearchResults structure
65 | return {
66 | results: generalResults.map(
67 | (result: SearXNGResult): SearchResultItem => ({
68 | title: result.title,
69 | url: result.url,
70 | content: result.content
71 | })
72 | ),
73 | query: data.query,
74 | images: imageResults
75 | .map(result => {
76 | const imgSrc = result.img_src || ''
77 | return imgSrc.startsWith('http') ? imgSrc : `${apiUrl}${imgSrc}`
78 | })
79 | .filter(Boolean),
80 | number_of_results: data.number_of_results
81 | }
82 | } catch (error) {
83 | console.error('SearXNG API error:', error)
84 | throw error
85 | }
86 | }
87 | }
--------------------------------------------------------------------------------
/lib/tools/search/providers/tavily.ts:
--------------------------------------------------------------------------------
1 | import { SearchResultImage, SearchResults } from '@/lib/types'
2 | import { sanitizeUrl } from '@/lib/utils'
3 | import { BaseSearchProvider } from './base'
4 |
5 | export class TavilySearchProvider extends BaseSearchProvider {
6 | async search(
7 | query: string,
8 | maxResults: number = 10,
9 | searchDepth: 'basic' | 'advanced' = 'basic',
10 | includeDomains: string[] = [],
11 | excludeDomains: string[] = []
12 | ): Promise {
13 | const apiKey = process.env.TAVILY_API_KEY
14 | this.validateApiKey(apiKey, 'TAVILY')
15 |
16 | // Tavily API requires a minimum of 5 characters in the query
17 | const filledQuery =
18 | query.length < 5 ? query + ' '.repeat(5 - query.length) : query
19 |
20 | const includeImageDescriptions = true
21 | const response = await fetch('https://api.tavily.com/search', {
22 | method: 'POST',
23 | headers: {
24 | 'Content-Type': 'application/json'
25 | },
26 | body: JSON.stringify({
27 | api_key: apiKey,
28 | query: filledQuery,
29 | max_results: Math.max(maxResults, 5),
30 | search_depth: searchDepth,
31 | include_images: true,
32 | include_image_descriptions: includeImageDescriptions,
33 | include_answers: true,
34 | include_domains: includeDomains,
35 | exclude_domains: excludeDomains
36 | })
37 | })
38 |
39 | if (!response.ok) {
40 | throw new Error(
41 | `Tavily API error: ${response.status} ${response.statusText}`
42 | )
43 | }
44 |
45 | const data = await response.json()
46 | const processedImages = includeImageDescriptions
47 | ? data.images
48 | .map(({ url, description }: { url: string; description: string }) => ({
49 | url: sanitizeUrl(url),
50 | description
51 | }))
52 | .filter(
53 | (
54 | image: SearchResultImage
55 | ): image is { url: string; description: string } =>
56 | typeof image === 'object' &&
57 | image.description !== undefined &&
58 | image.description !== ''
59 | )
60 | : data.images.map((url: string) => sanitizeUrl(url))
61 |
62 | return {
63 | ...data,
64 | images: processedImages
65 | }
66 | }
67 | }
--------------------------------------------------------------------------------
/lib/tools/video-search.ts:
--------------------------------------------------------------------------------
1 | import { getSearchSchemaForModel } from '@/lib/schema/search'
2 | import { tool } from 'ai'
3 |
4 | /**
5 | * Creates a video search tool with the appropriate schema for the model.
6 | */
7 | export function createVideoSearchTool(fullModel: string) {
8 | return tool({
9 | description: 'Search for videos from YouTube',
10 | parameters: getSearchSchemaForModel(fullModel),
11 | execute: async ({ query }) => {
12 | try {
13 | const response = await fetch('https://google.serper.dev/videos', {
14 | method: 'POST',
15 | headers: {
16 | 'X-API-KEY': process.env.SERPER_API_KEY || '',
17 | 'Content-Type': 'application/json'
18 | },
19 | body: JSON.stringify({ q: query })
20 | })
21 |
22 | if (!response.ok) {
23 | throw new Error('Network response was not ok')
24 | }
25 |
26 | return await response.json()
27 | } catch (error) {
28 | console.error('Video Search API error:', error)
29 | return null
30 | }
31 | }
32 | })
33 | }
34 |
35 | // Default export for backward compatibility, using a default model
36 | export const videoSearchTool = createVideoSearchTool('openai:gpt-4o-mini')
37 |
--------------------------------------------------------------------------------
/lib/types/index.ts:
--------------------------------------------------------------------------------
1 | import { CoreMessage, JSONValue, Message } from 'ai'
2 |
3 | export type SearchResults = {
4 | images: SearchResultImage[]
5 | results: SearchResultItem[]
6 | number_of_results?: number
7 | query: string
8 | }
9 |
10 | // If enabled the include_images_description is true, the images will be an array of { url: string, description: string }
11 | // Otherwise, the images will be an array of strings
12 | export type SearchResultImage =
13 | | string
14 | | {
15 | url: string
16 | description: string
17 | number_of_results?: number
18 | }
19 |
20 | export type ExaSearchResults = {
21 | results: ExaSearchResultItem[]
22 | }
23 |
24 | export type SerperSearchResults = {
25 | searchParameters: {
26 | q: string
27 | type: string
28 | engine: string
29 | }
30 | videos: SerperSearchResultItem[]
31 | }
32 |
33 | export type SearchResultItem = {
34 | title: string
35 | url: string
36 | content: string
37 | }
38 |
39 | export type ExaSearchResultItem = {
40 | score: number
41 | title: string
42 | id: string
43 | url: string
44 | publishedDate: Date
45 | author: string
46 | }
47 |
48 | export type SerperSearchResultItem = {
49 | title: string
50 | link: string
51 | snippet: string
52 | imageUrl: string
53 | duration: string
54 | source: string
55 | channel: string
56 | date: string
57 | position: number
58 | }
59 |
60 | export interface Chat extends Record {
61 | id: string
62 | title: string
63 | createdAt: Date
64 | userId: string
65 | path: string
66 | messages: ExtendedCoreMessage[] // Note: Changed from AIMessage to ExtendedCoreMessage
67 | sharePath?: string
68 | }
69 |
70 | // ExtendedCoreMessage for saveing annotations
71 | export type ExtendedCoreMessage = Omit & {
72 | role: CoreMessage['role'] | 'data'
73 | content: CoreMessage['content'] | JSONValue
74 | }
75 |
76 | export type AIMessage = {
77 | role: 'user' | 'assistant' | 'system' | 'function' | 'data' | 'tool'
78 | content: string
79 | id: string
80 | name?: string
81 | type?:
82 | | 'answer'
83 | | 'related'
84 | | 'skip'
85 | | 'inquiry'
86 | | 'input'
87 | | 'input_related'
88 | | 'tool'
89 | | 'followup'
90 | | 'end'
91 | }
92 |
93 | export interface SearXNGResult {
94 | title: string
95 | url: string
96 | content: string
97 | img_src?: string
98 | publishedDate?: string
99 | score?: number
100 | }
101 |
102 | export interface SearXNGResponse {
103 | query: string
104 | number_of_results: number
105 | results: SearXNGResult[]
106 | }
107 |
108 | export type SearXNGImageResult = string
109 |
110 | export type SearXNGSearchResults = {
111 | images: SearXNGImageResult[]
112 | results: SearchResultItem[]
113 | number_of_results?: number
114 | query: string
115 | }
116 |
--------------------------------------------------------------------------------
/lib/types/models.ts:
--------------------------------------------------------------------------------
1 | export interface Model {
2 | id: string
3 | name: string
4 | provider: string
5 | providerId: string
6 | enabled: boolean
7 | toolCallType: 'native' | 'manual'
8 | toolCallModel?: string
9 | }
10 |
--------------------------------------------------------------------------------
/lib/utils/context-window.ts:
--------------------------------------------------------------------------------
1 | import { CoreMessage } from 'ai'
2 | import { Model } from '../types/models'
3 |
4 | const DEFAULT_CONTEXT_WINDOW = 128_000
5 | const DEFAULT_RESERVE_TOKENS = 30_000
6 |
7 | export function getMaxAllowedTokens(model: Model): number {
8 | let contextWindow: number
9 | let reserveTokens: number
10 |
11 | if (model.id.includes('deepseek')) {
12 | contextWindow = 64_000
13 | reserveTokens = 27_000
14 | } else if (model.id.includes('claude')) {
15 | contextWindow = 200_000
16 | reserveTokens = 40_000
17 | } else {
18 | contextWindow = DEFAULT_CONTEXT_WINDOW
19 | reserveTokens = DEFAULT_RESERVE_TOKENS
20 | }
21 |
22 | return contextWindow - reserveTokens
23 | }
24 |
25 | export function truncateMessages(
26 | messages: CoreMessage[],
27 | maxTokens: number
28 | ): CoreMessage[] {
29 | let totalTokens = 0
30 | const tempMessages: CoreMessage[] = []
31 |
32 | for (let i = messages.length - 1; i >= 0; i--) {
33 | const message = messages[i]
34 | const messageTokens = message.content?.length || 0
35 |
36 | if (totalTokens + messageTokens <= maxTokens) {
37 | tempMessages.push(message)
38 | totalTokens += messageTokens
39 | } else {
40 | break
41 | }
42 | }
43 |
44 | const orderedMessages = tempMessages.reverse()
45 |
46 | while (orderedMessages.length > 0 && orderedMessages[0].role !== 'user') {
47 | orderedMessages.shift()
48 | }
49 |
50 | return orderedMessages
51 | }
52 |
--------------------------------------------------------------------------------
/lib/utils/cookies.ts:
--------------------------------------------------------------------------------
1 | export function setCookie(name: string, value: string, days = 30) {
2 | const date = new Date()
3 | date.setTime(date.getTime() + days * 24 * 60 * 60 * 1000)
4 | const expires = `expires=${date.toUTCString()}`
5 | document.cookie = `${name}=${value};${expires};path=/`
6 | }
7 |
8 | export function getCookie(name: string): string | null {
9 | const cookies = document.cookie.split(';')
10 | for (const cookie of cookies) {
11 | const [cookieName, cookieValue] = cookie.trim().split('=')
12 | if (cookieName === name) {
13 | return cookieValue
14 | }
15 | }
16 | return null
17 | }
18 |
19 | export function deleteCookie(name: string) {
20 | document.cookie = `${name}=;expires=Thu, 01 Jan 1970 00:00:00 GMT;path=/`
21 | }
22 |
--------------------------------------------------------------------------------
/lib/utils/url.ts:
--------------------------------------------------------------------------------
1 | import { headers } from 'next/headers'
2 |
3 | /**
4 | * Helper function to get base URL from headers
5 | * Extracts URL information from Next.js request headers
6 | */
7 | export async function getBaseUrlFromHeaders(): Promise {
8 | const headersList = await headers()
9 | const baseUrl = headersList.get('x-base-url')
10 | const url = headersList.get('x-url')
11 | const host = headersList.get('x-host')
12 | const protocol = headersList.get('x-protocol') || 'http:'
13 |
14 | try {
15 | // Try to use the pre-constructed base URL if available
16 | if (baseUrl) {
17 | return new URL(baseUrl)
18 | } else if (url) {
19 | return new URL(url)
20 | } else if (host) {
21 | const constructedUrl = `${protocol}${
22 | protocol.endsWith(':') ? '//' : '://'
23 | }${host}`
24 | return new URL(constructedUrl)
25 | } else {
26 | return new URL('http://localhost:3000')
27 | }
28 | } catch (urlError) {
29 | // Fallback to default URL if any error occurs during URL construction
30 | return new URL('http://localhost:3000')
31 | }
32 | }
33 |
34 | /**
35 | * Resolves the base URL using environment variables or headers
36 | * Centralizes the base URL resolution logic used across the application
37 | * @returns A URL object representing the base URL
38 | */
39 | export async function getBaseUrl(): Promise {
40 | // Check for environment variables first
41 | const baseUrlEnv = process.env.NEXT_PUBLIC_BASE_URL || process.env.BASE_URL
42 |
43 | if (baseUrlEnv) {
44 | try {
45 | const baseUrlObj = new URL(baseUrlEnv)
46 | console.log('Using BASE_URL environment variable:', baseUrlEnv)
47 | return baseUrlObj
48 | } catch (error) {
49 | console.warn(
50 | 'Invalid BASE_URL environment variable, falling back to headers'
51 | )
52 | // Fall back to headers if the environment variable is invalid
53 | }
54 | }
55 |
56 | // If no valid environment variable is available, use headers
57 | return await getBaseUrlFromHeaders()
58 | }
59 |
60 | /**
61 | * Gets the base URL as a string
62 | * Convenience wrapper around getBaseUrl that returns a string
63 | * @returns A string representation of the base URL
64 | */
65 | export async function getBaseUrlString(): Promise {
66 | const baseUrlObj = await getBaseUrl()
67 | return baseUrlObj.toString()
68 | }
--------------------------------------------------------------------------------
/middleware.ts:
--------------------------------------------------------------------------------
1 | import { updateSession } from '@/lib/supabase/middleware'
2 | import { type NextRequest, NextResponse } from 'next/server'
3 |
4 | export async function middleware(request: NextRequest) {
5 | // Get the protocol from X-Forwarded-Proto header or request protocol
6 | const protocol =
7 | request.headers.get('x-forwarded-proto') || request.nextUrl.protocol
8 |
9 | // Get the host from X-Forwarded-Host header or request host
10 | const host =
11 | request.headers.get('x-forwarded-host') || request.headers.get('host') || ''
12 |
13 | // Construct the base URL - ensure protocol has :// format
14 | const baseUrl = `${protocol}${protocol.endsWith(':') ? '//' : '://'}${host}`
15 |
16 | // Create a response
17 | let response: NextResponse
18 |
19 | // Handle Supabase session if configured
20 | const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL
21 | const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY
22 |
23 | if (supabaseUrl && supabaseAnonKey) {
24 | response = await updateSession(request)
25 | } else {
26 | // If Supabase is not configured, just pass the request through
27 | response = NextResponse.next({
28 | request
29 | })
30 | }
31 |
32 | // Add request information to response headers
33 | response.headers.set('x-url', request.url)
34 | response.headers.set('x-host', host)
35 | response.headers.set('x-protocol', protocol)
36 | response.headers.set('x-base-url', baseUrl)
37 |
38 | return response
39 | }
40 |
41 | export const config = {
42 | matcher: [
43 | /*
44 | * Match all request paths except for the ones starting with:
45 | * - _next/static (static files)
46 | * - _next/image (image optimization files)
47 | * - favicon.ico (favicon file)
48 | * Feel free to modify this pattern to include more paths.
49 | */
50 | '/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)'
51 | ]
52 | }
53 |
--------------------------------------------------------------------------------
/next.config.mjs:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 | const nextConfig = {
3 | images: {
4 | remotePatterns: [
5 | {
6 | protocol: 'https',
7 | hostname: 'i.ytimg.com',
8 | port: '',
9 | pathname: '/vi/**'
10 | },
11 | {
12 | protocol: 'https',
13 | hostname: 'lh3.googleusercontent.com',
14 | port: '',
15 | pathname: '/a/**' // Google user content often follows this pattern
16 | }
17 | ]
18 | }
19 | }
20 |
21 | export default nextConfig
22 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "morphic",
3 | "version": "0.1.0",
4 | "private": true,
5 | "license": "Apache-2.0",
6 | "scripts": {
7 | "dev": "next dev --turbo",
8 | "build": "next build",
9 | "start": "next start",
10 | "lint": "next lint"
11 | },
12 | "dependencies": {
13 | "@ai-sdk/anthropic": "^1.2.10",
14 | "@ai-sdk/azure": "^1.1.5",
15 | "@ai-sdk/deepseek": "^0.1.6",
16 | "@ai-sdk/fireworks": "^0.1.6",
17 | "@ai-sdk/google": "^1.1.5",
18 | "@ai-sdk/groq": "^1.1.6",
19 | "@ai-sdk/openai": "^1.3.12",
20 | "@ai-sdk/xai": "^1.1.10",
21 | "@radix-ui/react-alert-dialog": "^1.0.5",
22 | "@radix-ui/react-avatar": "^1.1.9",
23 | "@radix-ui/react-checkbox": "^1.0.4",
24 | "@radix-ui/react-collapsible": "^1.0.3",
25 | "@radix-ui/react-dialog": "^1.1.11",
26 | "@radix-ui/react-dropdown-menu": "^2.0.6",
27 | "@radix-ui/react-label": "^2.1.6",
28 | "@radix-ui/react-popover": "^1.1.5",
29 | "@radix-ui/react-select": "^2.1.2",
30 | "@radix-ui/react-separator": "^1.1.4",
31 | "@radix-ui/react-slider": "^1.1.2",
32 | "@radix-ui/react-slot": "^1.2.2",
33 | "@radix-ui/react-switch": "^1.0.3",
34 | "@radix-ui/react-toggle": "^1.1.1",
35 | "@radix-ui/react-tooltip": "^1.2.4",
36 | "@supabase/auth-helpers-nextjs": "^0.10.0",
37 | "@supabase/auth-ui-react": "^0.4.7",
38 | "@supabase/auth-ui-shared": "^0.1.8",
39 | "@supabase/ssr": "^0.6.1",
40 | "@supabase/supabase-js": "^2.49.4",
41 | "@tailwindcss/typography": "^0.5.12",
42 | "@upstash/redis": "^1.34.0",
43 | "@vercel/analytics": "^1.5.0",
44 | "ai": "^4.3.6",
45 | "class-variance-authority": "^0.7.1",
46 | "clsx": "^2.1.0",
47 | "cmdk": "1.0.0",
48 | "embla-carousel-react": "^8.0.0",
49 | "exa-js": "^1.0.12",
50 | "jsdom": "^22.1.0",
51 | "katex": "^0.16.10",
52 | "lucide-react": "^0.507.0",
53 | "next": "^15.2.3",
54 | "next-themes": "^0.3.0",
55 | "node-html-parser": "^6.1.13",
56 | "ollama-ai-provider": "^1.2.0",
57 | "react": "^19.0.0",
58 | "react-dom": "^19.0.0",
59 | "react-icons": "^5.0.1",
60 | "react-markdown": "^8.0.7",
61 | "react-resizable-panels": "^3.0.0",
62 | "react-syntax-highlighter": "^15.5.0",
63 | "react-textarea-autosize": "^8.5.3",
64 | "redis": "^4.7.0",
65 | "rehype-external-links": "^3.0.0",
66 | "rehype-katex": "^6.0.0",
67 | "remark-gfm": "^3.0.1",
68 | "remark-math": "^5.1.1",
69 | "sonner": "^1.4.41",
70 | "tailwind-merge": "^2.6.0",
71 | "tailwindcss-animate": "^1.0.7",
72 | "vaul": "^1.1.2",
73 | "zod": "^3.23.8"
74 | },
75 | "devDependencies": {
76 | "@types/jsdom": "^21.1.7",
77 | "@types/node": "^20",
78 | "@types/react": "^18",
79 | "@types/react-dom": "^18",
80 | "@types/react-syntax-highlighter": "^15.5.13",
81 | "eslint": "^8",
82 | "eslint-config-next": "^14.2.25",
83 | "postcss": "^8",
84 | "tailwindcss": "^3.4.1",
85 | "typescript": "^5"
86 | },
87 | "engines": {
88 | "bun": "1.2.12"
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/postcss.config.mjs:
--------------------------------------------------------------------------------
1 | /** @type {import('postcss-load-config').Config} */
2 | const config = {
3 | plugins: {
4 | tailwindcss: {},
5 | },
6 | };
7 |
8 | export default config;
9 |
--------------------------------------------------------------------------------
/prettier.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('prettier').Config} */
2 | module.exports = {
3 | endOfLine: 'lf',
4 | semi: false,
5 | useTabs: false,
6 | singleQuote: true,
7 | arrowParens: 'avoid',
8 | tabWidth: 2,
9 | trailingComma: 'none',
10 | importOrder: [
11 | '^(react/(.*)$)|^(react$)',
12 | '^(next/(.*)$)|^(next$)',
13 | '',
14 | '',
15 | '^types$',
16 | '^@/types/(.*)$',
17 | '^@/config/(.*)$',
18 | '^@/lib/(.*)$',
19 | '^@/hooks/(.*)$',
20 | '^@/components/ui/(.*)$',
21 | '^@/components/(.*)$',
22 | '^@/registry/(.*)$',
23 | '^@/styles/(.*)$',
24 | '^@/app/(.*)$',
25 | '',
26 | '^[./]',
27 | ],
28 | importOrderSeparation: false,
29 | importOrderSortSpecifiers: true,
30 | importOrderBuiltinModulesToTop: true,
31 | importOrderParserPlugins: ['typescript', 'jsx', 'decorators-legacy'],
32 | importOrderMergeDuplicateImports: true,
33 | importOrderCombineTypeAndValueImports: true,
34 | };
35 |
--------------------------------------------------------------------------------
/public/images/placeholder-image.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/miurla/morphic/4800cae1f6882c2dd95a050536cfba59fee05b0a/public/images/placeholder-image.png
--------------------------------------------------------------------------------
/public/providers/logos/anthropic.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/public/providers/logos/azure.svg:
--------------------------------------------------------------------------------
1 |
24 |
--------------------------------------------------------------------------------
/public/providers/logos/fireworks.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/public/providers/logos/google.svg:
--------------------------------------------------------------------------------
1 |
34 |
--------------------------------------------------------------------------------
/public/providers/logos/groq.svg:
--------------------------------------------------------------------------------
1 |
12 |
--------------------------------------------------------------------------------
/public/providers/logos/openai-compatible.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/public/providers/logos/openai.svg:
--------------------------------------------------------------------------------
1 |
11 |
--------------------------------------------------------------------------------
/public/providers/logos/xai.svg:
--------------------------------------------------------------------------------
1 |
2 |
8 |
--------------------------------------------------------------------------------
/public/screenshot-2025-05-04.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/miurla/morphic/4800cae1f6882c2dd95a050536cfba59fee05b0a/public/screenshot-2025-05-04.png
--------------------------------------------------------------------------------
/searxng-limiter.toml:
--------------------------------------------------------------------------------
1 | #https://docs.searxng.org/admin/searx.limiter.html
--------------------------------------------------------------------------------
/searxng-settings.yml:
--------------------------------------------------------------------------------
1 | use_default_settings: true
2 | server:
3 | # Is overwritten by ${SEARXNG_PORT} and ${SEARXNG_BIND_ADDRESS}
4 | port: 8888
5 | bind_address: '0.0.0.0'
6 | # public URL of the instance, to ensure correct inbound links. Is overwritten
7 | # by ${SEARXNG_URL}.
8 | base_url: false # "http://example.com/location"
9 | # rate limit the number of request on the instance, block some bots.
10 | # Is overwritten by ${SEARXNG_LIMITER}
11 | limiter: false
12 | # enable features designed only for public instances.
13 | # Is overwritten by ${SEARXNG_PUBLIC_INSTANCE}
14 | public_instance: false
15 |
16 | # If your instance owns a /etc/searxng/settings.yml file, then set the following
17 | # values there.
18 |
19 | secret_key: 'ursecretkey' # Is overwritten by ${SEARXNG_SECRET}
20 | # Proxy image results through SearXNG. Is overwritten by ${SEARXNG_IMAGE_PROXY}
21 | image_proxy: false
22 | # 1.0 and 1.1 are supported
23 | http_protocol_version: '1.0'
24 | # POST queries are more secure as they don't show up in history but may cause
25 | # problems when using Firefox containers
26 | method: 'POST'
27 | default_http_headers:
28 | X-Content-Type-Options: nosniff
29 | X-Download-Options: noopen
30 | X-Robots-Tag: noindex, nofollow
31 | Referrer-Policy: no-referrer
32 |
33 | search:
34 | formats:
35 | - json
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "lib": [
4 | "dom",
5 | "dom.iterable",
6 | "esnext"
7 | ],
8 | "allowJs": true,
9 | "skipLibCheck": true,
10 | "strict": true,
11 | "noEmit": true,
12 | "esModuleInterop": true,
13 | "module": "esnext",
14 | "moduleResolution": "bundler",
15 | "resolveJsonModule": true,
16 | "isolatedModules": true,
17 | "jsx": "preserve",
18 | "incremental": true,
19 | "plugins": [
20 | {
21 | "name": "next"
22 | }
23 | ],
24 | "paths": {
25 | "@/*": [
26 | "./*"
27 | ]
28 | },
29 | "target": "ES2017"
30 | },
31 | "include": [
32 | "next-env.d.ts",
33 | "**/*.ts",
34 | "**/*.tsx",
35 | ".next/types/**/*.ts"
36 | ],
37 | "exclude": [
38 | "node_modules"
39 | ]
40 | }
41 |
--------------------------------------------------------------------------------