├── lib ├── constants │ └── index.ts ├── firecrawl │ ├── index.ts │ ├── types.ts │ └── client.ts ├── supabase │ ├── client.ts │ ├── server.ts │ └── middleware.ts ├── schema │ ├── retrieve.tsx │ ├── related.tsx │ ├── question.ts │ └── search.tsx ├── streaming │ ├── types.ts │ ├── parse-tool-call.ts │ └── create-tool-calling-stream.ts ├── types │ ├── models.ts │ └── index.ts ├── auth │ └── get-current-user.ts ├── tools │ ├── question.ts │ ├── search │ │ └── providers │ │ │ ├── exa.ts │ │ │ ├── base.ts │ │ │ ├── index.ts │ │ │ ├── firecrawl.ts │ │ │ ├── tavily.ts │ │ │ └── searxng.ts │ ├── video-search.ts │ └── retrieve.ts ├── utils │ ├── cookies.ts │ ├── context-window.ts │ └── url.ts ├── ollama │ ├── types.ts │ ├── transformer.ts │ └── client.ts ├── hooks │ ├── use-copy-to-clipboard.ts │ └── use-media-query.ts └── agents │ ├── generate-related-questions.ts │ ├── manual-researcher.ts │ └── researcher.ts ├── searxng-limiter.toml ├── app ├── favicon.ico ├── opengraph-image.png ├── page.tsx ├── auth │ ├── login │ │ └── page.tsx │ ├── sign-up │ │ └── page.tsx │ ├── forgot-password │ │ └── page.tsx │ ├── update-password │ │ └── page.tsx │ ├── sign-up-success │ │ └── page.tsx │ ├── confirm │ │ └── route.ts │ ├── error │ │ └── page.tsx │ └── oauth │ │ └── route.ts ├── search │ ├── loading.tsx │ ├── page.tsx │ └── [id] │ │ └── page.tsx ├── share │ ├── loading.tsx │ └── [id] │ │ └── page.tsx ├── api │ ├── config │ │ └── models │ │ │ └── route.ts │ ├── ollama │ │ └── models │ │ │ └── route.ts │ ├── chats │ │ └── route.ts │ └── chat │ │ ├── [id] │ │ └── route.ts │ │ └── route.ts ├── layout.tsx └── globals.css ├── .github ├── FUNDING.yml ├── workflows │ ├── ci.yml │ └── docker-build.yml └── ISSUE_TEMPLATE │ ├── feature_request.yml │ └── bug_report.yml ├── components ├── ui │ ├── index.ts │ ├── markdown.tsx │ ├── skeleton.tsx │ ├── collapsible.tsx │ ├── status-indicator.tsx │ ├── icons.tsx │ ├── label.tsx │ ├── textarea.tsx │ ├── separator.tsx │ ├── input.tsx │ ├── spinner.tsx │ ├── sonner.tsx │ ├── checkbox.tsx │ ├── slider.tsx │ ├── switch.tsx │ ├── badge.tsx │ ├── password-input.tsx │ ├── tooltip.tsx │ ├── popover.tsx │ ├── tooltip-button.tsx │ ├── avatar.tsx │ ├── toggle.tsx │ ├── resizable.tsx │ ├── button.tsx │ └── card.tsx ├── sidebar │ ├── chat-history-section.tsx │ ├── chat-history-skeleton.tsx │ └── clear-history-action.tsx ├── theme-provider.tsx ├── artifact │ ├── artifact-root.tsx │ ├── reasoning-content.tsx │ ├── artifact-content.tsx │ ├── tool-invocation-content.tsx │ ├── search-artifact-content.tsx │ ├── video-search-artifact-content.tsx │ ├── retrieve-artifact-content.tsx │ ├── artifact-context.tsx │ └── chat-artifact-container.tsx ├── default-skeleton.tsx ├── retry-button.tsx ├── tool-badge.tsx ├── theme-menu-items.tsx ├── current-user-avatar.tsx ├── video-search-results.tsx ├── external-link-items.tsx ├── custom-link.tsx ├── header.tsx ├── search-mode-toggle.tsx ├── inspector │ ├── inspector-drawer.tsx │ └── inspector-panel.tsx ├── empty-screen.tsx ├── message-actions.tsx ├── app-sidebar.tsx ├── answer-section.tsx ├── retrieve-section.tsx ├── guest-menu.tsx ├── video-search-section.tsx ├── clear-history.tsx ├── reasoning-section.tsx ├── tool-section.tsx ├── search-section.tsx ├── update-password-form.tsx ├── collapsible-message.tsx └── chat-share.tsx ├── public ├── screenshot-2025-05-04.png ├── images │ └── placeholder-image.png └── providers │ └── logos │ ├── openai-compatible.svg │ ├── anthropic.svg │ ├── xai.svg │ ├── fireworks.svg │ ├── groq.svg │ ├── openai.svg │ ├── azure.svg │ └── google.svg ├── postcss.config.mjs ├── .vscode └── settings.json ├── prettier.config.js ├── .prettierignore ├── next.config.mjs ├── components.json ├── .gitignore ├── hooks ├── use-current-user-image.ts ├── use-current-user-name.ts └── use-mobile.tsx ├── tsconfig.json ├── Dockerfile ├── .eslintrc.json ├── docker-compose.yaml ├── searxng-settings.yml ├── CONTRIBUTING.md ├── middleware.ts ├── CODE_OF_CONDUCT.md └── package.json /lib/constants/index.ts: -------------------------------------------------------------------------------- 1 | export const CHAT_ID = 'search' as const 2 | -------------------------------------------------------------------------------- /searxng-limiter.toml: -------------------------------------------------------------------------------- 1 | #https://docs.searxng.org/admin/searx.limiter.html -------------------------------------------------------------------------------- /app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code/app-morphic/main/app/favicon.ico -------------------------------------------------------------------------------- /lib/firecrawl/index.ts: -------------------------------------------------------------------------------- 1 | export * from './client' 2 | export * from './types' 3 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: miurla 4 | -------------------------------------------------------------------------------- /app/opengraph-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code/app-morphic/main/app/opengraph-image.png -------------------------------------------------------------------------------- /components/ui/index.ts: -------------------------------------------------------------------------------- 1 | export * from './button' 2 | export * from './tooltip' 3 | export * from './tooltip-button' 4 | -------------------------------------------------------------------------------- /public/screenshot-2025-05-04.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code/app-morphic/main/public/screenshot-2025-05-04.png -------------------------------------------------------------------------------- /public/images/placeholder-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code/app-morphic/main/public/images/placeholder-image.png -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /public/providers/logos/openai-compatible.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | } 11 | -------------------------------------------------------------------------------- /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/streaming/types.ts: -------------------------------------------------------------------------------- 1 | import { Message } from 'ai' 2 | 3 | import { Model } from '../types/models' 4 | 5 | export interface BaseStreamConfig { 6 | messages: Message[] 7 | model: Model 8 | chatId: string 9 | searchMode: boolean 10 | userId: string 11 | } 12 | -------------------------------------------------------------------------------- /app/page.tsx: -------------------------------------------------------------------------------- 1 | import { generateId } from 'ai' 2 | 3 | import { getModels } from '@/lib/config/models' 4 | 5 | import { Chat } from '@/components/chat' 6 | 7 | export default async function Page() { 8 | const id = generateId() 9 | const models = await getModels() 10 | return 11 | } 12 | -------------------------------------------------------------------------------- /app/auth/login/page.tsx: -------------------------------------------------------------------------------- 1 | import { LoginForm } from '@/components/login-form' 2 | 3 | export default function Page() { 4 | return ( 5 |
6 |
7 | 8 |
9 |
10 | ) 11 | } 12 | -------------------------------------------------------------------------------- /app/auth/sign-up/page.tsx: -------------------------------------------------------------------------------- 1 | import { SignUpForm } from '@/components/sign-up-form' 2 | 3 | export default function Page() { 4 | return ( 5 |
6 |
7 | 8 |
9 |
10 | ) 11 | } 12 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | // Ollama-specific fields (only added when needed) 10 | capabilities?: string[] 11 | contextWindow?: number 12 | } 13 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /app/auth/forgot-password/page.tsx: -------------------------------------------------------------------------------- 1 | import { ForgotPasswordForm } from '@/components/forgot-password-form' 2 | 3 | export default function Page() { 4 | return ( 5 |
6 |
7 | 8 |
9 |
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 |
6 |
7 | 8 |
9 |
10 | ) 11 | } 12 | -------------------------------------------------------------------------------- /app/search/loading.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { DefaultSkeleton } from '../../components/default-skeleton' 4 | 5 | export default function Loading() { 6 | return ( 7 |
8 |
9 | 10 |
11 |
12 | ) 13 | } 14 | -------------------------------------------------------------------------------- /app/share/loading.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { DefaultSkeleton } from '../../components/default-skeleton' 4 | 5 | export default function Loading() { 6 | return ( 7 |
8 |
9 | 10 |
11 |
12 | ) 13 | } 14 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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, CollapsibleContent, CollapsibleTrigger } 12 | -------------------------------------------------------------------------------- /public/providers/logos/anthropic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | node_modules/ 3 | .next/ 4 | out/ 5 | build/ 6 | dist/ 7 | 8 | # Environment 9 | .env* 10 | 11 | # Generated files 12 | *.lock 13 | bun.lock 14 | package-lock.json 15 | yarn.lock 16 | pnpm-lock.yaml 17 | 18 | # IDE 19 | .vscode/ 20 | .idea/ 21 | 22 | # Misc 23 | *.log 24 | .DS_Store 25 | coverage/ 26 | .cache/ 27 | 28 | # Next.js 29 | .next/ 30 | out/ 31 | 32 | # Production 33 | build/ 34 | 35 | # Temporary files 36 | tmp/ 37 | temp/ -------------------------------------------------------------------------------- /components/artifact/artifact-root.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { ReactNode } from 'react' 4 | 5 | import { ArtifactProvider } from './artifact-context' 6 | import { ChatArtifactContainer } from './chat-artifact-container' 7 | 8 | export default function ArtifactRoot({ children }: { children: ReactNode }) { 9 | return ( 10 | 11 | {children} 12 | 13 | ) 14 | } 15 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | } 22 | -------------------------------------------------------------------------------- /.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 | # temporary files 39 | .tmp/ -------------------------------------------------------------------------------- /app/search/page.tsx: -------------------------------------------------------------------------------- 1 | import { redirect } from 'next/navigation' 2 | 3 | import { generateId } from 'ai' 4 | 5 | import { getModels } from '@/lib/config/models' 6 | 7 | import { Chat } from '@/components/chat' 8 | 9 | export const maxDuration = 60 10 | 11 | export default async function SearchPage(props: { 12 | searchParams: Promise<{ q: string }> 13 | }) { 14 | const { q } = await props.searchParams 15 | if (!q) { 16 | redirect('/') 17 | } 18 | 19 | const id = generateId() 20 | const models = await getModels() 21 | return 22 | } 23 | -------------------------------------------------------------------------------- /components/ui/status-indicator.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode } from 'react' 2 | 3 | import { LucideIcon } from 'lucide-react' 4 | 5 | interface StatusIndicatorProps { 6 | icon: LucideIcon 7 | iconClassName?: string 8 | children: ReactNode 9 | } 10 | 11 | export function StatusIndicator({ 12 | icon: Icon, 13 | iconClassName, 14 | children 15 | }: StatusIndicatorProps) { 16 | return ( 17 | 18 | 19 | {children} 20 | 21 | ) 22 | } 23 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /public/providers/logos/xai.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | 15 | 16 | 17 | 18 | 19 | ) 20 | } 21 | 22 | export { IconLogo } 23 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /components/artifact/reasoning-content.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import remarkGfm from 'remark-gfm' 4 | 5 | import { cn } from '@/lib/utils' 6 | 7 | import { MemoizedReactMarkdown } from '../ui/markdown' 8 | 9 | export function ReasoningContent({ reasoning }: { reasoning: string }) { 10 | return ( 11 |
12 |

Reasoning

13 |
14 | 15 | {reasoning} 16 | 17 |
18 |
19 | ) 20 | } 21 | -------------------------------------------------------------------------------- /lib/tools/question.ts: -------------------------------------------------------------------------------- 1 | import { tool } from 'ai' 2 | 3 | import { getQuestionSchemaForModel } from '@/lib/schema/question' 4 | 5 | /** 6 | * Creates a question tool with the appropriate schema for the specified model. 7 | */ 8 | export function createQuestionTool(fullModel: string) { 9 | return tool({ 10 | description: 11 | 'Ask a clarifying question with multiple options when more information is needed', 12 | parameters: getQuestionSchemaForModel(fullModel) 13 | // execute function removed to enable frontend confirmation 14 | }) 15 | } 16 | 17 | // Default export for backward compatibility, using a default model 18 | export const askQuestionTool = createQuestionTool('openai:gpt-4o-mini') 19 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["dom", "dom.iterable", "esnext"], 4 | "allowJs": true, 5 | "skipLibCheck": true, 6 | "strict": true, 7 | "noEmit": true, 8 | "esModuleInterop": true, 9 | "module": "esnext", 10 | "moduleResolution": "bundler", 11 | "resolveJsonModule": true, 12 | "isolatedModules": true, 13 | "jsx": "preserve", 14 | "incremental": true, 15 | "plugins": [ 16 | { 17 | "name": "next" 18 | } 19 | ], 20 | "paths": { 21 | "@/*": ["./*"] 22 | }, 23 | "target": "ES2017" 24 | }, 25 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 26 | "exclude": ["node_modules"] 27 | } 28 | -------------------------------------------------------------------------------- /app/api/config/models/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from 'next/server' 2 | 3 | import { getModels } from '@/lib/config/models' 4 | 5 | export async function GET() { 6 | try { 7 | const models = await getModels() 8 | 9 | return NextResponse.json( 10 | { models }, 11 | { 12 | headers: { 13 | 'Cache-Control': 'public, max-age=60, s-maxage=60', // Cache for 1 minute 14 | 'Content-Type': 'application/json' 15 | } 16 | } 17 | ) 18 | } catch (error) { 19 | console.error('Failed to fetch models from /api/config/models:', error) 20 | return NextResponse.json( 21 | { error: 'Failed to fetch models' }, 22 | { status: 500 } 23 | ) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /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/artifact/artifact-content.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { Part } from '@/components/artifact/artifact-context' 4 | 5 | import { ReasoningContent } from './reasoning-content' 6 | import { ToolInvocationContent } from './tool-invocation-content' 7 | 8 | export function ArtifactContent({ part }: { part: Part | null }) { 9 | if (!part) return null 10 | 11 | switch (part.type) { 12 | case 'tool-invocation': 13 | return 14 | case 'reasoning': 15 | return 16 | default: 17 | return ( 18 |
Details for this part type are not available
19 | ) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /components/retry-button.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { RotateCcw } from 'lucide-react' 4 | 5 | import { Button } from './ui/button' 6 | 7 | interface RetryButtonProps { 8 | reload: () => Promise 9 | messageId: string 10 | } 11 | 12 | export const RetryButton: React.FC = ({ 13 | reload, 14 | messageId 15 | }) => { 16 | return ( 17 | 28 | ) 29 | } 30 | -------------------------------------------------------------------------------- /components/tool-badge.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import { Film, Link, Search } from 'lucide-react' 4 | 5 | import { Badge } from './ui/badge' 6 | 7 | type ToolBadgeProps = { 8 | tool: string 9 | children: React.ReactNode 10 | className?: string 11 | } 12 | 13 | export const ToolBadge: React.FC = ({ 14 | tool, 15 | children, 16 | className 17 | }) => { 18 | const icon: Record = { 19 | search: , 20 | retrieve: , 21 | videoSearch: 22 | } 23 | 24 | return ( 25 | 26 | {icon[tool]} 27 | {children} 28 | 29 | ) 30 | } 31 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /lib/ollama/types.ts: -------------------------------------------------------------------------------- 1 | export interface OllamaModel { 2 | name: string 3 | model: string 4 | modified_at: string 5 | size: number 6 | digest: string 7 | details?: { 8 | format: string 9 | family: string 10 | families?: string[] 11 | parameter_size: string 12 | quantization_level: string 13 | } 14 | } 15 | 16 | export interface OllamaModelCapabilities { 17 | name: string 18 | capabilities: string[] 19 | contextWindow: number 20 | parameters: Record 21 | timestamp?: number 22 | } 23 | 24 | export interface OllamaModelsResponse { 25 | models: OllamaModel[] 26 | } 27 | 28 | export interface OllamaShowResponse { 29 | name: string 30 | capabilities: string[] 31 | context_window: number 32 | parameters: Record 33 | } 34 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | 9 | jobs: 10 | lint-typecheck-format: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - name: Checkout repository 15 | uses: actions/checkout@v4 16 | 17 | - name: Setup Bun 18 | uses: oven-sh/setup-bun@v2 19 | with: 20 | bun-version: 1.2.12 21 | 22 | - name: Install dependencies 23 | run: bun install 24 | 25 | - name: Run linting 26 | run: bun lint 27 | 28 | - name: Run type checking 29 | run: bun typecheck 30 | 31 | - name: Check formatting 32 | run: bun format:check 33 | 34 | - name: Build application 35 | run: bun run build 36 | -------------------------------------------------------------------------------- /lib/hooks/use-copy-to-clipboard.ts: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { useState } from 'react' 4 | 5 | export interface useCopyToClipboardProps { 6 | timeout?: number 7 | } 8 | 9 | export function useCopyToClipboard({ 10 | timeout = 2000 11 | }: useCopyToClipboardProps) { 12 | const [isCopied, setIsCopied] = useState(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 | -------------------------------------------------------------------------------- /public/providers/logos/fireworks.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /components/ui/label.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import * as React from 'react' 4 | 5 | import * as LabelPrimitive from '@radix-ui/react-label' 6 | import { cva, type VariantProps } from 'class-variance-authority' 7 | 8 | import { cn } from '@/lib/utils/index' 9 | 10 | const labelVariants = cva( 11 | 'text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70' 12 | ) 13 | 14 | const Label = React.forwardRef< 15 | React.ElementRef, 16 | React.ComponentPropsWithoutRef & 17 | VariantProps 18 | >(({ className, ...props }, ref) => ( 19 | 24 | )) 25 | Label.displayName = LabelPrimitive.Root.displayName 26 | 27 | export { Label } 28 | -------------------------------------------------------------------------------- /components/theme-menu-items.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { useTheme } from 'next-themes' 4 | 5 | import { Laptop, Moon, Sun } from 'lucide-react' 6 | 7 | import { DropdownMenuItem } from '@/components/ui/dropdown-menu' 8 | 9 | export function ThemeMenuItems() { 10 | const { setTheme } = useTheme() 11 | 12 | return ( 13 | <> 14 | setTheme('light')}> 15 | 16 | Light 17 | 18 | setTheme('dark')}> 19 | 20 | Dark 21 | 22 | setTheme('system')}> 23 | 24 | System 25 | 26 | 27 | ) 28 | } 29 | -------------------------------------------------------------------------------- /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 |