Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# AI Model Configuration
# Choose your provider: 'openai', 'google', or 'openrouter'
AI_PROVIDER=openai

# Specify the model ID for your chosen provider
# Examples:
# - OpenAI: gpt-4-turbo, gpt-4, gpt-3.5-turbo
# - Google: gemini-1.5-pro, gemini-1.5-flash
# - OpenRouter: openai/gpt-4-turbo, anthropic/claude-3.5-sonnet
AI_MODEL=gpt-4.1-2025-04-14

# API Keys for each provider (only set the one you're using)
OPENAI_API_KEY=
GOOGLE_GENERATIVE_AI_API_KEY=
OPENROUTER_API_KEY=
42 changes: 42 additions & 0 deletions app/api/chat/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { ProvideLinksToolSchema } from '@/lib/ai-tools/inkeep-qa-schema';
import { SearchDocsToolSchema, searchAndFetchDocs } from '@/lib/ai-tools/search-and-fetch';
import { convertToModelMessages, stepCountIs, streamText } from 'ai';
import { systemPrompt } from '@/lib/searchPrompt';
import { validateRateLimit } from '@/utils/rateLimit';
import { createModel } from '@/lib/ai-models';


export async function POST(req: Request) {
const rateLimitError = validateRateLimit(req);
if (rateLimitError) return rateLimitError;
let reqJson;
try {
reqJson = await req.json();
} catch {
return new Response('Invalid request', { status: 400 });
}

const result = streamText({
model: createModel(),
tools: {
searchDocs: {
description: 'Search the NocoDB documentation and retrieve full page content. After calling this, you MUST read the returned content and write a comprehensive answer for the user based on that content.',
inputSchema: SearchDocsToolSchema,
execute: async ({ query }: { query: string }) => {
const { markdown, links } = await searchAndFetchDocs(query, 3);
return { markdown, links };
},
},
provideLinks: {
inputSchema: ProvideLinksToolSchema,
},
},
system: systemPrompt,
messages: convertToModelMessages(reqJson.messages, {
ignoreIncompleteToolCalls: true,
}),
stopWhen: stepCountIs(10),
});

return result.toUIMessageStreamResponse();
}
111 changes: 50 additions & 61 deletions app/blog/[slug]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,13 @@ import {blogSource} from "@/lib/source";
import Link from "next/link";
import {notFound} from "next/navigation";
import {metadataImage} from "@/lib/metadata";
import {Button} from "@/components/ui/button";
import {ArrowLeft} from "lucide-react";
import {calculateReadingTime} from "@/lib/timeToRead";
import Image from "next/image";
import defaultMdxComponents from "fumadocs-ui/mdx";
import CustomToc from "@/components/blog/CustomToc";
import BlogCard from "@/components/blog/BlogCard";
import {getCategoryColor} from "@/lib/categoryColor";
import Subscribe from "@/components/blog/Subscribe";
import ShareDropdown from "@/components/blog/ShareDropdown";
import SignUp from "@/components/blog/SignUp";

export async function generateMetadata(props: {
params: Promise<{ slug?: string }>;
Expand Down Expand Up @@ -50,61 +47,52 @@ export default async function page(props: {
}).slice(0, 2);

return (
<>
<div className="container pt-[40px] lg:px-10 pb-10">
<div className="flex justify-between items-center">
<Link className="text-sm font-normal" href="/blog">
<Button className="cursor-pointer hover:underline underline-red" variant="none">
<div className="flex text-nc-content-grey-subtle items-center gap-2">
<ArrowLeft/>
Back
</div>
</Button>
</Link>
<ShareDropdown
url={`https://nocodb.com/blog/${params.slug}`}
title={page.data.title}
/>
</div>
<div className="my-8 flex flex-col gap-3">
<div
className="text-nc-content-grey-emphasis text-2xl lg:text-[40px] font-bold leading-9 lg:leading-[64px]">
{page.data?.title}
<div className="relative w-full bg-gradient-to-b from-blue-50/50 via-purple-50/30 via-30% to-white to-60%">
{/* Decorative background elements */}
<div className="absolute top-0 right-0 w-[500px] h-[500px] bg-blue-400/10 rounded-full blur-3xl -z-10" />
<div className="absolute top-[40%] left-0 w-[400px] h-[400px] bg-purple-400/10 rounded-full blur-3xl -z-10" />

{/* Hero Section */}
<div className="container py-4 lg:py-12">
{/* Category Badge */}
<Link className="inline-block mb-6" href={`/blog?category=${page.data?.category}`}>
<div
style={{backgroundColor: getCategoryColor(page?.data?.category)}}
className="rounded-lg px-3 py-1.5 text-sm font-medium text-nc-content-grey-default hover:opacity-80 transition-opacity"
>
{page.data?.category}
</div>
<Link className="w-[fit-content]" href={`/blog?category=${page.data?.category}`}>
<div style={{backgroundColor: getCategoryColor(page?.data?.category)}}
className="rounded-[6px] px-2 text-nc-content-grey-default">
{page.data?.category}
</Link>

{/* Title */}
<h1 className="text-nc-content-grey-emphasis text-3xl lg:text-5xl font-bold leading-tight lg:leading-tight mb-6 max-w-4xl">
{page.data?.title}
</h1>

{/* Metadata */}
<div className="flex flex-wrap items-center gap-4 text-nc-content-grey-subtle-2">
<div className="flex items-center gap-2">
<div className="w-10 h-10 rounded-full bg-nc-background-grey-light flex items-center justify-center text-nc-content-grey-emphasis font-semibold">
{page?.data?.author?.charAt(0).toUpperCase()}
</div>
</Link>
</div>
<div className="flex justify-center items-center leading-6 text-nc-content-grey-subtle-2">
<div className="flex-1">
<span className="mr-3">
{page?.data?.author}
</span>
|
<span className="mx-3">
{new Date(page.data.date).toLocaleDateString("en-US", {})}
</span>
</div>

<div>
{calculateReadingTime(page?.data?.structuredData)}
<span className="font-medium">{page?.data?.author}</span>
</div>
<span className="text-nc-content-grey-muted">•</span>
<span>{new Date(page.data.date).toLocaleDateString("en-US", {
month: "long",
day: "numeric",
year: "numeric"
})}</span>
<span className="text-nc-content-grey-muted">•</span>
<span>{calculateReadingTime(page?.data?.structuredData)}</span>
</div>
</div>

<div className="container mx-auto">
<div className="relative w-full aspect-video">
<Image className="w-full object-cover" src={page.data.image} alt={page.data.title} fill/>
</div>
</div>

<article className="container py-10 mx-auto">
<article className="container py-10 lg:py-16 mx-auto">
<div className="flex nc-blog-layout relative gap-8">
<div className="sticky hidden lg:block h-48 top-8">
<div className="sticky hidden lg:block top-8 self-start space-y-8">
<CustomToc toc={page.data.toc}/>
<SignUp />
</div>
<div className="prose min-w-0 flex-1">
<page.data.body components={defaultMdxComponents}/>
Expand All @@ -115,19 +103,20 @@ export default async function page(props: {

{
related?.length === 2 ? (
<div className="container mx-auto">
<h1 className="text-nc-content-grey-emphasis leading-[62px] font-bold text-[40px]">
Related
</h1>
<div className="pt-15 gap-8 lg:gap-10 grid grid-cols-1 lg:grid-cols-2 pb-20">
{related.map((post) => (
<BlogCard post={post} key={post.url}/>
))}
<div className="bg-nc-background-grey-extra-light/30 py-16">
<div className="container mx-auto">
<h2 className="text-nc-content-grey-emphasis text-3xl lg:text-4xl font-bold mb-8">
Related Articles
</h2>
<div className="flex flex-col gap-0">
{related.map((post) => (
<BlogCard post={post} key={post.url}/>
))}
</div>
</div>
</div>
) : (<div className="py-10"></div>)
}

</>
</div>
);
}
14 changes: 13 additions & 1 deletion app/blog/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ import {Footer} from "@/components/blog/home/Footer";
import {Navbar} from "@/components/blog/home/Navbar";
import { ReCaptchaProvider } from "next-recaptcha-v3";
import { NuqsAdapter } from 'nuqs/adapters/next/app';
import {blogSource} from "@/lib/source";
import TopBarNavigationClient from "@/components/blog/TopBarNavigationClient";
import {Suspense} from "react";

export const metadata = {
icons: {
Expand All @@ -13,10 +16,19 @@ export const metadata = {
}

export default function Layout({children,}: { children: ReactNode; }): React.ReactElement {
const posts = blogSource.getPages();
const categories = new Set<string>();
posts.forEach((post) => {
categories.add(post.data.category ?? "Uncategorized");
});

return (
<HomeLayout {...baseOptions} nav={{
<HomeLayout className='!pt-0' {...baseOptions} nav={{
component: Navbar()
}}>
<Suspense fallback={<div className="h-12 border-b border-nc-border-grey-medium" />}>
<TopBarNavigationClient categories={Array.from(categories)} />
</Suspense>
<NuqsAdapter>
<ReCaptchaProvider reCaptchaKey="6LcdnI0oAAAAAHYW3hwztfZw9qAjX4TiviE4fWip">
{children}
Expand Down
35 changes: 15 additions & 20 deletions app/blog/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import {blogSource} from "@/lib/source";
import BlogCard from "@/components/blog/BlogCard";
import {Separator} from "@/components/ui/separator";
import Link from "next/link";
import {CategoryTabs} from "@/components/blog/Category";
import Subscribe from "@/components/blog/Subscribe";
import {SearchInput} from "@/components/blog/SearchInput";
import Image from "next/image";
Expand Down Expand Up @@ -49,29 +48,24 @@ export default async function BlogPage({searchParams}: {
const hasMorePosts = endIndex < searchFilteredPosts.length;
const nextPage = currentPage + 1;
return (
<main className="py-8 w-full md:py-12">
<div className="container py-10 lg:pt-16 lg:pb-8">
<main className="w-full bg-gradient-to-b from-blue-50/50 via-purple-50/30 to-white">
<div className="container py-10 relative">
{/* Decorative background elements */}
<div className="absolute top-0 right-0 w-[600px] h-[600px] bg-blue-400/10 rounded-full blur-3xl -z-10" />
<div className="absolute bottom-0 left-0 w-[500px] h-[500px] bg-purple-400/10 rounded-full blur-3xl -z-10" />

<h1 className="text-nc-content-grey-emphasis text-[40px] font-semibold leading-15.5">
Blog
</h1>
<h5 className="text-nc-content-grey-subtle mt-6 lg:mt-2 text-base leading-6 font-medium">
Insights, tutorials, and updates from the team building the future of no-code databases.
</h5>
<div className="mt-6">
<SearchInput />
</div>
</div>
<Separator className="mb-12" />


<div className="container mt-5">

<CategoryTabs categories={Array.from(categories)} selectedCategory={selectedCategory} searchQuery={searchQuery}/>
<Separator className="border-nc-border-grey-medium"/>
</div>

<div className="container py-8 lg:pt-15 lg:pb-20 gap-8 lg:gap-16 grid grid-cols-1 lg:grid-cols-2">
<div className="container">
{displayedPosts.length === 0 ? (
<div className="col-span-full flex flex-col items-center justify-center py-16">
<div className="flex flex-col items-center justify-center py-16">
<Image
src="/img/no-search-result-found.png"
alt="No results found"
Expand All @@ -91,9 +85,11 @@ export default async function BlogPage({searchParams}: {
</p>
</div>
) : (
displayedPosts.map((post) => (
<BlogCard post={post} key={post.url}/>
))
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-0">
{displayedPosts.map((post) => (
<BlogCard post={post} key={post.url}/>
))}
</div>
)}
</div>

Expand All @@ -115,9 +111,8 @@ export default async function BlogPage({searchParams}: {
</Link>
</div>
)}

<Subscribe/>
<Separator/>
<Separator className="mb-12"/>
</main>
);
}
3 changes: 3 additions & 0 deletions app/docs/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type {ReactNode} from "react";
import Navbar from "@/components/layout/Navbar";
import TopBarNaigation from "@/components/layout/TopBarNaigation";
import { DocsNavigationProvider } from "./DocsNavigationProvider";
import { AISearchTrigger } from "@/components/search";

/**
* Layout component for rendering the main structure of the documentation page.
Expand All @@ -22,6 +23,8 @@ export default function Layout({children}: { children: ReactNode }) {
</div>
{children}
</DocsNavigationProvider>
<AISearchTrigger />

</main>
)
}
1 change: 1 addition & 0 deletions app/global.css
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,7 @@
--color-nc-content-grey-subtle: var(--color-grey-700);
--color-nc-content-grey-subtle-2: var(--color-grey-600);
--color-nc-content-grey-muted: var(--color-grey-500);
--color-nc-content-grey-muted-2: var(--color-grey-400);

--color-nc-content-brand-default: var(--color-brand-500);
--color-nc-content-brand-hover: var(--color-grey-300);
Expand Down
2 changes: 2 additions & 0 deletions app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import ClientAnalytics from "@/components/Analytics";
import NcSearchDialog from "@/components/layout/Search";
import {CustomThemeProvider} from "@/app/ThemeProvider";
import {RootProvider} from "fumadocs-ui/provider";
import { Toaster } from "sonner";

const inter = Inter({
subsets: ['latin'],
Expand All @@ -20,6 +21,7 @@ export default function Layout({children}: { children: ReactNode }) {
</CustomThemeProvider>
</RootProvider>
<ClientAnalytics/>
<Toaster />
</body>
</html>
);
Expand Down
11 changes: 11 additions & 0 deletions cli.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"aliases": {
"uiDir": "./components/ui",
"componentsDir": "./components",
"blockDir": "./components",
"cssDir": "./styles",
"libDir": "./lib"
},
"baseDir": "",
"commands": {}
}
Loading
Loading