mirror of
https://github.com/katanemo/plano.git
synced 2026-04-26 17:26:26 +02:00
feat: redesign archgw -> plano + website in Next.js (#613)
* feat: redesign archgw -> plano + website * feat(www): refactor landing page sections, add new diagrams and UI improvements * feat(www): sections enhanced for clarify & diagrams added * feat(www): improvements to mobile design, layout of diagrams * feat(www): clean + typecheck * feat(www): feedback loop changes * feat(www): fix type error * fix lib/utils error * feat(www): ran biome formatting * feat(www): graphic changes * feat(www): web analytics * fea(www): changes * feat(www): introduce monorepo This change brings Turborepo monorepo to independently handle the marketing website, the docs website and any other future use cases for mutli-platform support. They are using internal @katanemo package handlers for the design system and logic. * fix(www): transpiler failure * fix(www): tsconfig issue * fix(www): next.config issue * feat(docs): hold off on docs * Delete next.config.ts * feat(www): content fix * feat(www): introduce blog * feat(www): content changes * Update package-lock.json * feat: update text * Update IntroSection.tsx * feat: Turbopack issue * fix * Update IntroSection.tsx * feat: updated Research page * refactor(www): text clarity, padding adj. * format(www) * fix: add missing lib/ files to git - fixes Vercel GitHub deployment - Updated .gitignore to properly exclude Python lib/ but include Next.js lib/ directories - Added packages/ui/src/lib/utils.ts (cn utility function) - Added apps/www/src/lib/sanity.ts (Sanity client configuration) - Fixes module resolution errors in Vercel GitHub deployments (case-sensitive filesystem) * Update .gitignore * style(www): favicon + metadata * fix(www): links * fix(www): add analytics * fix(www): add * fix(www): fix links + image * fix(www): fix links + image * fix(www): fix links * fix(www): remove from tools testing.md
This commit is contained in:
parent
48bbc7cce7
commit
0c3efdbef2
119 changed files with 27142 additions and 266 deletions
326
apps/www/src/app/api/og/[slug]/route.tsx
Normal file
326
apps/www/src/app/api/og/[slug]/route.tsx
Normal file
|
|
@ -0,0 +1,326 @@
|
|||
import { ImageResponse } from "next/og";
|
||||
import { NextRequest } from "next/server";
|
||||
import { client, urlFor } from "@/lib/sanity";
|
||||
|
||||
export const runtime = "edge";
|
||||
|
||||
// Font loading function that uses the request origin
|
||||
function loadFont(fileName: string, baseUrl: string) {
|
||||
return fetch(new URL(`/fonts/${fileName}`, baseUrl)).then((res) => {
|
||||
if (!res.ok) {
|
||||
throw new Error(
|
||||
`Failed to fetch font ${fileName}: ${res.status} ${res.statusText}`,
|
||||
);
|
||||
}
|
||||
return res.arrayBuffer();
|
||||
});
|
||||
}
|
||||
|
||||
async function getBlogPost(slug: string) {
|
||||
const query = `*[_type == "blog" && slug.current == $slug && published == true][0] {
|
||||
_id,
|
||||
title,
|
||||
slug,
|
||||
summary,
|
||||
publishedAt,
|
||||
mainImage,
|
||||
author {
|
||||
name,
|
||||
title,
|
||||
image
|
||||
}
|
||||
}`;
|
||||
|
||||
const post = await client.fetch(query, { slug });
|
||||
return post;
|
||||
}
|
||||
|
||||
function formatDate(dateString: string): string {
|
||||
const date = new Date(dateString);
|
||||
const day = date.getDate();
|
||||
const month = date.toLocaleDateString("en-US", { month: "long" });
|
||||
const year = date.getFullYear();
|
||||
|
||||
const getOrdinal = (n: number) => {
|
||||
const s = ["th", "st", "nd", "rd"];
|
||||
const v = n % 100;
|
||||
return n + (s[(v - 20) % 10] || s[v] || s[0]);
|
||||
};
|
||||
|
||||
return `${month} ${getOrdinal(day)}, ${year}`;
|
||||
}
|
||||
|
||||
export async function GET(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ slug: string }> },
|
||||
) {
|
||||
try {
|
||||
// Get base URL for font loading - use request origin in production
|
||||
const fontBaseUrl =
|
||||
process.env.NEXT_PUBLIC_APP_URL ||
|
||||
(process.env.VERCEL_URL
|
||||
? `https://${process.env.VERCEL_URL}`
|
||||
: request.nextUrl.origin);
|
||||
|
||||
// Load fonts with error handling
|
||||
let fontData;
|
||||
try {
|
||||
const [
|
||||
ibmPlexSans,
|
||||
jetbrainsMonoRegular,
|
||||
jetbrainsMonoMedium,
|
||||
jetbrainsMonoBold,
|
||||
] = await Promise.all([
|
||||
loadFont("IBMPlexSans-VariableFont_wdth,wght.otf", fontBaseUrl),
|
||||
loadFont("JetBrainsMono-Regular.otf", fontBaseUrl),
|
||||
loadFont("JetBrainsMono-Medium.otf", fontBaseUrl),
|
||||
loadFont("jetbrains-mono-bold.otf", fontBaseUrl),
|
||||
]).catch((error: Error) => {
|
||||
console.error("Error loading fonts:", error);
|
||||
throw new Error(`Failed to load fonts: ${error.message}`);
|
||||
});
|
||||
|
||||
fontData = {
|
||||
ibmPlexSans,
|
||||
jetbrainsMonoRegular,
|
||||
jetbrainsMonoMedium,
|
||||
jetbrainsMonoBold,
|
||||
};
|
||||
} catch (error: unknown) {
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : "Unknown error";
|
||||
console.error("Font loading error:", errorMessage);
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
error: "Failed to load required fonts",
|
||||
details: errorMessage,
|
||||
baseUrl: fontBaseUrl,
|
||||
}),
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
|
||||
const { slug } = await params;
|
||||
const post = await getBlogPost(slug);
|
||||
|
||||
if (!post) {
|
||||
return new Response(JSON.stringify({ error: "Post not found" }), {
|
||||
status: 404,
|
||||
});
|
||||
}
|
||||
|
||||
// Get author image URL if available
|
||||
let authorImageUrl: string | null = null;
|
||||
if (post.author?.image) {
|
||||
authorImageUrl = urlFor(post.author.image).width(120).url();
|
||||
}
|
||||
|
||||
// Use logo PNG
|
||||
const baseUrl =
|
||||
process.env.NEXT_PUBLIC_APP_URL ||
|
||||
(process.env.VERCEL_URL
|
||||
? `https://${process.env.VERCEL_URL}`
|
||||
: request.nextUrl.origin);
|
||||
const logoUrl = `${baseUrl}/Logomark.png`;
|
||||
|
||||
return new ImageResponse(
|
||||
<div
|
||||
style={{
|
||||
background: "linear-gradient(to top right, #ffffff, #dcdfff)",
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
padding: "60px 80px",
|
||||
position: "relative",
|
||||
}}
|
||||
>
|
||||
{/* Logo - Top Left - SVG as data URL */}
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: "60px",
|
||||
left: "80px",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<img
|
||||
src={logoUrl}
|
||||
alt="Plano"
|
||||
width="120"
|
||||
height="48"
|
||||
style={{
|
||||
objectFit: "contain",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Main Content - Left-Aligned, aligned with logo */}
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "flex-start",
|
||||
justifyContent: "center",
|
||||
flex: 1,
|
||||
width: "85%",
|
||||
marginTop: "150px",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "flex-start",
|
||||
width: "100%",
|
||||
}}
|
||||
>
|
||||
{/* Title - Left Aligned */}
|
||||
<h1
|
||||
style={{
|
||||
fontSize: "64px",
|
||||
lineHeight: "1.1",
|
||||
color: "#000000",
|
||||
marginBottom: "24px",
|
||||
letterSpacing: "-0.08em",
|
||||
fontFamily: "IBM Plex Sans Bold",
|
||||
textAlign: "left",
|
||||
}}
|
||||
>
|
||||
{post.title}
|
||||
</h1>
|
||||
|
||||
{/* Date - Below Title, Left Aligned */}
|
||||
{/* {post.publishedAt && (
|
||||
<div
|
||||
style={{
|
||||
fontSize: "20px",
|
||||
color: "#000000",
|
||||
marginBottom: "40px",
|
||||
letterSpacing: "-1.8px",
|
||||
fontFamily: "IBM Plex Sans Regular",
|
||||
textAlign: "left",
|
||||
}}
|
||||
>
|
||||
{formatDate(post.publishedAt)}
|
||||
</div>
|
||||
)} */}
|
||||
|
||||
{/* Author Section - Below Date, Left Aligned */}
|
||||
{post.author?.name && (
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "16px",
|
||||
marginTop: "20px",
|
||||
}}
|
||||
>
|
||||
{authorImageUrl && (
|
||||
<img
|
||||
src={authorImageUrl}
|
||||
alt={post.author.name}
|
||||
width="48"
|
||||
height="48"
|
||||
style={{
|
||||
borderRadius: "4px",
|
||||
objectFit: "cover",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "flex-start",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
fontSize: "20px",
|
||||
color: "#7780d9",
|
||||
textTransform: "uppercase",
|
||||
letterSpacing: "0.09em",
|
||||
fontFamily: "JetBrains Mono Bold",
|
||||
textAlign: "left",
|
||||
}}
|
||||
>
|
||||
{post.author.name}
|
||||
</div>
|
||||
{post.author.title && (
|
||||
<div
|
||||
style={{
|
||||
fontSize: "14px",
|
||||
color: "#28327D",
|
||||
textTransform: "uppercase",
|
||||
letterSpacing: "0.10em",
|
||||
fontFamily: "JetBrains Mono Medium",
|
||||
textAlign: "left",
|
||||
}}
|
||||
>
|
||||
{post.author.title}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>,
|
||||
{
|
||||
width: 1200,
|
||||
height: 630,
|
||||
fonts: [
|
||||
{
|
||||
name: "IBM Plex Sans Regular",
|
||||
data: fontData.ibmPlexSans,
|
||||
style: "normal",
|
||||
weight: 400,
|
||||
},
|
||||
{
|
||||
name: "IBM Plex Sans Medium",
|
||||
data: fontData.ibmPlexSans,
|
||||
style: "normal",
|
||||
weight: 500,
|
||||
},
|
||||
{
|
||||
name: "IBM Plex Sans Bold",
|
||||
data: fontData.ibmPlexSans,
|
||||
style: "normal",
|
||||
weight: 700,
|
||||
},
|
||||
{
|
||||
name: "JetBrains Mono Regular",
|
||||
data: fontData.jetbrainsMonoRegular,
|
||||
style: "normal",
|
||||
weight: 400,
|
||||
},
|
||||
{
|
||||
name: "JetBrains Mono Medium",
|
||||
data: fontData.jetbrainsMonoMedium,
|
||||
style: "normal",
|
||||
weight: 500,
|
||||
},
|
||||
{
|
||||
name: "JetBrains Mono Bold",
|
||||
data: fontData.jetbrainsMonoBold,
|
||||
style: "normal",
|
||||
weight: 600,
|
||||
},
|
||||
],
|
||||
},
|
||||
);
|
||||
} catch (error: unknown) {
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : "Unknown error";
|
||||
console.error("Error generating image response:", error);
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
error: "Failed to generate image",
|
||||
details: errorMessage,
|
||||
}),
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
}
|
||||
120
apps/www/src/app/blog/[slug]/layout.tsx
Normal file
120
apps/www/src/app/blog/[slug]/layout.tsx
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
import { Metadata } from "next";
|
||||
import { client } from "@/lib/sanity";
|
||||
|
||||
type Params = Promise<{ slug: string }>;
|
||||
|
||||
interface BlogPost {
|
||||
_id: string;
|
||||
title: string;
|
||||
slug: { current: string };
|
||||
summary?: string;
|
||||
publishedAt?: string;
|
||||
author?: {
|
||||
name?: string;
|
||||
title?: string;
|
||||
image?: any;
|
||||
};
|
||||
}
|
||||
|
||||
async function getBlogPost(slug: string): Promise<BlogPost | null> {
|
||||
const query = `*[_type == "blog" && slug.current == $slug && published == true][0] {
|
||||
_id,
|
||||
title,
|
||||
slug,
|
||||
summary,
|
||||
publishedAt,
|
||||
author
|
||||
}`;
|
||||
|
||||
const post = await client.fetch(query, { slug });
|
||||
return post || null;
|
||||
}
|
||||
|
||||
export async function generateMetadata({
|
||||
params,
|
||||
}: {
|
||||
params: Params;
|
||||
}): Promise<Metadata> {
|
||||
try {
|
||||
const resolvedParams = await params;
|
||||
const post = await getBlogPost(resolvedParams.slug);
|
||||
|
||||
if (!post) {
|
||||
return {
|
||||
title: "Post Not Found - Plano",
|
||||
description: "The requested blog post could not be found.",
|
||||
};
|
||||
}
|
||||
|
||||
// Get baseUrl - use NEXT_PUBLIC_APP_URL if set, otherwise construct from VERCEL_URL
|
||||
// Restrict to allowed hosts: localhost:3000, archgw-tau.vercel.app, or plano.katanemo.com
|
||||
let baseUrl = "http://localhost:3000";
|
||||
|
||||
if (process.env.NEXT_PUBLIC_APP_URL) {
|
||||
const url = process.env.NEXT_PUBLIC_APP_URL;
|
||||
if (
|
||||
url.includes("archgw-tau.vercel.app") ||
|
||||
url.includes("plano.katanemo.com") ||
|
||||
url.includes("localhost:3000")
|
||||
) {
|
||||
baseUrl = url;
|
||||
}
|
||||
} else if (process.env.VERCEL_URL) {
|
||||
const hostname = process.env.VERCEL_URL;
|
||||
// VERCEL_URL is just the hostname, not the full URL
|
||||
if (hostname === "archgw-tau.vercel.app") {
|
||||
baseUrl = `https://${hostname}`;
|
||||
} else if (hostname === "plano.katanemo.com") {
|
||||
baseUrl = `https://${hostname}`;
|
||||
}
|
||||
}
|
||||
|
||||
const ogImageUrl = `${baseUrl}/api/og/${resolvedParams.slug}`;
|
||||
|
||||
const metadata: Metadata = {
|
||||
title: `${post.title} - Plano Blog`,
|
||||
description: post.summary || "Read more on Plano Blog",
|
||||
openGraph: {
|
||||
title: post.title,
|
||||
description: post.summary || "Read more on Plano Blog",
|
||||
type: "article",
|
||||
publishedTime: post.publishedAt,
|
||||
authors: post.author?.name ? [post.author.name] : undefined,
|
||||
url: `${baseUrl}/blog/${resolvedParams.slug}`,
|
||||
siteName: "Plano",
|
||||
images: [
|
||||
{
|
||||
url: ogImageUrl,
|
||||
width: 1200,
|
||||
height: 630,
|
||||
alt: post.title,
|
||||
},
|
||||
],
|
||||
locale: "en_US",
|
||||
},
|
||||
twitter: {
|
||||
card: "summary_large_image",
|
||||
title: post.title,
|
||||
description: post.summary || "Read more on Plano Blog",
|
||||
images: [ogImageUrl],
|
||||
},
|
||||
};
|
||||
|
||||
return metadata;
|
||||
} catch (error) {
|
||||
console.error("Error generating metadata:", error);
|
||||
return {
|
||||
title: "Blog Post - Plano",
|
||||
description: "Read this post on Plano Blog",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
interface LayoutProps {
|
||||
children: React.ReactNode;
|
||||
params: Params;
|
||||
}
|
||||
|
||||
export default async function Layout({ children, params }: LayoutProps) {
|
||||
return <>{children}</>;
|
||||
}
|
||||
35
apps/www/src/app/blog/[slug]/not-found.tsx
Normal file
35
apps/www/src/app/blog/[slug]/not-found.tsx
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
import Link from "next/link";
|
||||
|
||||
export default function NotFound() {
|
||||
return (
|
||||
<div className="min-h-screen bg-white flex items-center justify-center">
|
||||
<div className="max-w-md mx-auto px-4 text-center">
|
||||
<h1 className="text-4xl sm:text-5xl font-normal leading-tight tracking-tighter text-black mb-4">
|
||||
<span className="font-sans">Post Not Found</span>
|
||||
</h1>
|
||||
<p className="text-lg font-sans font-[400] tracking-[-0.5px] text-black/70 mb-8">
|
||||
The blog post you're looking for doesn't exist or has been removed.
|
||||
</p>
|
||||
<Link
|
||||
href="/blog"
|
||||
className="inline-flex items-center gap-2 text-base font-medium text-black hover:text-[var(--secondary)] transition-colors"
|
||||
>
|
||||
<svg
|
||||
className="w-4 h-4"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M15 19l-7-7 7-7"
|
||||
/>
|
||||
</svg>
|
||||
Back to Blog
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
206
apps/www/src/app/blog/[slug]/page.tsx
Normal file
206
apps/www/src/app/blog/[slug]/page.tsx
Normal file
|
|
@ -0,0 +1,206 @@
|
|||
import { client, urlFor } from "@/lib/sanity";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { PortableText } from "@/components/PortableText";
|
||||
import { notFound } from "next/navigation";
|
||||
import { UnlockPotentialSection } from "@/components/UnlockPotentialSection";
|
||||
|
||||
interface BlogPost {
|
||||
_id: string;
|
||||
title: string;
|
||||
slug: { current: string };
|
||||
summary?: string;
|
||||
body?: any[];
|
||||
bodyHtml?: string;
|
||||
publishedAt?: string;
|
||||
mainImage?: any;
|
||||
mainImageUrl?: string;
|
||||
author?: {
|
||||
name?: string;
|
||||
title?: string;
|
||||
image?: any;
|
||||
};
|
||||
}
|
||||
|
||||
async function getBlogPost(slug: string): Promise<BlogPost | null> {
|
||||
const query = `*[_type == "blog" && slug.current == $slug && published == true][0] {
|
||||
_id,
|
||||
title,
|
||||
slug,
|
||||
summary,
|
||||
body[]{
|
||||
...,
|
||||
asset->{
|
||||
_id,
|
||||
url,
|
||||
metadata {
|
||||
dimensions {
|
||||
width,
|
||||
height,
|
||||
aspectRatio
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
bodyHtml,
|
||||
publishedAt,
|
||||
mainImage,
|
||||
mainImageUrl,
|
||||
author
|
||||
}`;
|
||||
|
||||
const post = await client.fetch(query, { slug });
|
||||
return post || null;
|
||||
}
|
||||
|
||||
async function getAllBlogSlugs(): Promise<string[]> {
|
||||
const query = `*[_type == "blog" && published == true] {
|
||||
"slug": slug.current
|
||||
}`;
|
||||
|
||||
const posts = await client.fetch(query);
|
||||
return posts.map((post: { slug: string }) => post.slug);
|
||||
}
|
||||
|
||||
export async function generateStaticParams() {
|
||||
const slugs = await getAllBlogSlugs();
|
||||
return slugs.map((slug) => ({ slug }));
|
||||
}
|
||||
|
||||
export default async function BlogPostPage({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ slug: string }>;
|
||||
}) {
|
||||
const { slug } = await params;
|
||||
const post = await getBlogPost(slug);
|
||||
|
||||
if (!post) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
return (
|
||||
<article className="min-h-screen">
|
||||
{/* Featured Image - First */}
|
||||
{(post.mainImage || post.mainImageUrl) && (
|
||||
<div className="">
|
||||
<div className="max-w-[89rem] mx-auto px-4 sm:px-6 lg:px-8 pt-8 sm:pt-12 lg:pt-1 pb-8 sm:pb-12">
|
||||
<div className="relative aspect-[21/8] w-full overflow-hidden rounded-lg">
|
||||
{post.mainImage ? (
|
||||
<Image
|
||||
src={urlFor(post.mainImage).width(1600).url()}
|
||||
alt={post.title}
|
||||
fill
|
||||
className="object-cover"
|
||||
priority
|
||||
/>
|
||||
) : (
|
||||
<Image
|
||||
src={post.mainImageUrl!}
|
||||
alt={post.title}
|
||||
fill
|
||||
className="object-cover"
|
||||
priority
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Content Section */}
|
||||
<div className="max-w-[58rem] mx-auto px-4 sm:px-6 lg:px-8">
|
||||
{/* Back to Blog Button */}
|
||||
<div className="pt-4 sm:pt-6 lg:pt-8 pb-4 sm:pb-6">
|
||||
<Link
|
||||
href="/blog"
|
||||
className="inline-flex items-center gap-2 text-sm font-medium text-black/60 hover:text-black transition-colors"
|
||||
>
|
||||
<svg
|
||||
className="w-4 h-4"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M15 19l-7-7 7-7"
|
||||
/>
|
||||
</svg>
|
||||
Back to Blog
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{/* Author and Date */}
|
||||
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4 sm:gap-6 pb-4">
|
||||
{post.author?.name && (
|
||||
<div className="flex items-center gap-3">
|
||||
{post.author.image ? (
|
||||
<div className="relative w-12 h-12 rounded overflow-hidden shrink-0">
|
||||
<Image
|
||||
src={urlFor(post.author.image).width(80).url()}
|
||||
alt={post.author.name}
|
||||
fill
|
||||
className="object-cover"
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="w-12 h-12 rounded bg-[var(--secondary)]/20 shrink-0" />
|
||||
)}
|
||||
<div>
|
||||
<div className="text-lg font-mono font-semibold tracking-wider text-primary uppercase">
|
||||
{post.author.name}
|
||||
</div>
|
||||
{post.author.title && (
|
||||
<div className="text-sm font-mono font-normal tracking-wider text-[#28327D] uppercase">
|
||||
{post.author.title}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{post.publishedAt && (
|
||||
<time
|
||||
dateTime={post.publishedAt}
|
||||
className="text-base font-medium tracking-[-0.9px] text-black sm:ml-auto"
|
||||
>
|
||||
{new Date(post.publishedAt).toLocaleDateString("en-US", {
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
})}
|
||||
</time>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Title */}
|
||||
<div className="pb-6 sm:pb-8 sm:-ml-1.5">
|
||||
<h1 className="text-4xl sm:text-5xl lg:text-6xl font-medium leading-tight tracking-tighter text-black">
|
||||
<span className="font-sans">{post.title}</span>
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="pb-12 sm:pb-16 lg:pb-20 ">
|
||||
{post.body && post.body.length > 0 ? (
|
||||
<div className="prose prose-lg max-w-none">
|
||||
<PortableText content={post.body} />
|
||||
</div>
|
||||
) : post.bodyHtml ? (
|
||||
<div
|
||||
className="prose prose-lg max-w-none"
|
||||
dangerouslySetInnerHTML={{ __html: post.bodyHtml }}
|
||||
/>
|
||||
) : (
|
||||
<p className="text-base sm:text-lg font-sans font-normal tracking-[-0.5px] text-black/80">
|
||||
Content coming soon...
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<UnlockPotentialSection variant="transparent" />
|
||||
</article>
|
||||
);
|
||||
}
|
||||
119
apps/www/src/app/blog/page.tsx
Normal file
119
apps/www/src/app/blog/page.tsx
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
import { client } from "@/lib/sanity";
|
||||
import type { Metadata } from "next";
|
||||
import { UnlockPotentialSection } from "@/components/UnlockPotentialSection";
|
||||
import { BlogHeader } from "@/components/BlogHeader";
|
||||
import { FeaturedBlogCard } from "@/components/FeaturedBlogCard";
|
||||
import { BlogCard } from "@/components/BlogCard";
|
||||
import { BlogSectionHeader } from "@/components/BlogSectionHeader";
|
||||
export const metadata: Metadata = {
|
||||
title: "Blog - Plano",
|
||||
description: "Latest insights, updates, and stories from Plano",
|
||||
};
|
||||
|
||||
interface BlogPost {
|
||||
_id: string;
|
||||
title: string;
|
||||
slug: { current: string };
|
||||
summary?: string;
|
||||
publishedAt?: string;
|
||||
mainImage?: any;
|
||||
mainImageUrl?: string;
|
||||
thumbnailImage?: any;
|
||||
thumbnailImageUrl?: string;
|
||||
author?: {
|
||||
name?: string;
|
||||
title?: string;
|
||||
image?: any;
|
||||
};
|
||||
featured?: boolean;
|
||||
}
|
||||
|
||||
function formatDate(dateString: string): string {
|
||||
const date = new Date(dateString);
|
||||
const day = date.getDate();
|
||||
const month = date.toLocaleDateString("en-US", { month: "long" });
|
||||
const year = date.getFullYear();
|
||||
|
||||
// Add ordinal suffix
|
||||
const getOrdinal = (n: number) => {
|
||||
const s = ["th", "st", "nd", "rd"];
|
||||
const v = n % 100;
|
||||
return n + (s[(v - 20) % 10] || s[v] || s[0]);
|
||||
};
|
||||
|
||||
return `${month} ${getOrdinal(day)}, ${year}`;
|
||||
}
|
||||
|
||||
async function getBlogPosts(): Promise<BlogPost[]> {
|
||||
const query = `*[_type == "blog" && published == true] | order(publishedAt desc) {
|
||||
_id,
|
||||
title,
|
||||
slug,
|
||||
summary,
|
||||
publishedAt,
|
||||
mainImage,
|
||||
mainImageUrl,
|
||||
thumbnailImage,
|
||||
thumbnailImageUrl,
|
||||
author,
|
||||
featured
|
||||
}`;
|
||||
|
||||
return await client.fetch(query);
|
||||
}
|
||||
|
||||
export default async function BlogPage() {
|
||||
const posts = await getBlogPosts();
|
||||
const featuredPost = posts.find((post) => post.featured) || posts[0];
|
||||
const recentPosts = posts
|
||||
.filter((post) => post._id !== featuredPost?._id)
|
||||
.slice(0, 3);
|
||||
|
||||
// Format dates in server component
|
||||
const featuredPostWithDate = featuredPost
|
||||
? {
|
||||
...featuredPost,
|
||||
formattedDate: featuredPost.publishedAt
|
||||
? formatDate(featuredPost.publishedAt)
|
||||
: undefined,
|
||||
}
|
||||
: null;
|
||||
|
||||
const recentPostsWithDates = recentPosts.map((post) => ({
|
||||
...post,
|
||||
formattedDate: post.publishedAt ? formatDate(post.publishedAt) : undefined,
|
||||
}));
|
||||
|
||||
return (
|
||||
<div className="min-h-screen">
|
||||
{/* Header Section */}
|
||||
<BlogHeader />
|
||||
|
||||
{/* Featured Post */}
|
||||
{featuredPostWithDate && (
|
||||
<section className="">
|
||||
<div className="max-w-[85rem] mx-auto px-4 sm:px-6 lg:px-8 pb-8 sm:pb-12 lg:pb-0">
|
||||
<FeaturedBlogCard post={featuredPostWithDate} />
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* Recent Posts Section */}
|
||||
{recentPostsWithDates.length > 0 && (
|
||||
<section className="border-b border-black/10 py-8 sm:py-12 lg:py-24">
|
||||
<div className="max-w-[85rem] mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<BlogSectionHeader />
|
||||
<div className="grid sm:grid-cols-2 lg:grid-cols-3 gap-6 lg:gap-8">
|
||||
{recentPostsWithDates.map((post, index) => (
|
||||
<BlogCard key={post._id} post={post} index={index} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* Call to Action Section */}
|
||||
<UnlockPotentialSection variant="transparent" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
16
apps/www/src/app/docs/page.tsx
Normal file
16
apps/www/src/app/docs/page.tsx
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
import React from "react";
|
||||
|
||||
export default function DocsPage() {
|
||||
return (
|
||||
<section className="px-4 sm:px-6 lg:px-8 py-12 sm:py-16 lg:py-24">
|
||||
<div className="max-w-[81rem] mx-auto">
|
||||
<h1 className="text-4xl sm:text-5xl lg:text-7xl font-normal leading-tight tracking-tighter text-black mb-6">
|
||||
<span className="font-sans">Documentation</span>
|
||||
</h1>
|
||||
<p className="text-lg sm:text-xl lg:text-2xl font-sans font-[400] tracking-[-1.2px] text-black/70">
|
||||
Coming soon...
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
BIN
apps/www/src/app/favicon.ico
Normal file
BIN
apps/www/src/app/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.9 KiB |
2
apps/www/src/app/globals.css
Normal file
2
apps/www/src/app/globals.css
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
/* This file is kept for backwards compatibility but styles are imported from @katanemo/shared-styles */
|
||||
@import "@katanemo/shared-styles/globals.css";
|
||||
39
apps/www/src/app/layout.tsx
Normal file
39
apps/www/src/app/layout.tsx
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
import type { Metadata } from "next";
|
||||
import Script from "next/script";
|
||||
import "@katanemo/shared-styles/globals.css";
|
||||
import { Analytics } from "@vercel/analytics/next";
|
||||
import { ConditionalLayout } from "@/components/ConditionalLayout";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Plano - Delivery Infrastructure for Agentic Apps",
|
||||
description:
|
||||
"Build agents faster, and deliver them reliably to production - by offloading the critical plumbing work to Plano!",
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<body className="antialiased">
|
||||
{/* Google tag (gtag.js) */}
|
||||
<Script
|
||||
src="https://www.googletagmanager.com/gtag/js?id=G-6J5LQH3Q9G"
|
||||
strategy="afterInteractive"
|
||||
/>
|
||||
<Script id="google-analytics" strategy="afterInteractive">
|
||||
{`
|
||||
window.dataLayer = window.dataLayer || [];
|
||||
function gtag(){dataLayer.push(arguments);}
|
||||
gtag('js', new Date());
|
||||
gtag('config', 'G-6J5LQH3Q9G');
|
||||
`}
|
||||
</Script>
|
||||
<ConditionalLayout>{children}</ConditionalLayout>
|
||||
<Analytics />
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
28
apps/www/src/app/page.tsx
Normal file
28
apps/www/src/app/page.tsx
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import { Hero } from "@/components/Hero";
|
||||
import { IntroSection } from "@/components/IntroSection";
|
||||
import { IdeaToAgentSection } from "@/components/IdeaToAgentSection";
|
||||
import { UseCasesSection } from "@/components/UseCasesSection";
|
||||
import { VerticalCarouselSection } from "@/components/VerticalCarouselSection";
|
||||
import { HowItWorksSection } from "@/components/HowItWorksSection";
|
||||
import { UnlockPotentialSection } from "@/components/UnlockPotentialSection";
|
||||
import { LogoCloud } from "@/components/LogoCloud";
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<>
|
||||
<Hero />
|
||||
<LogoCloud />
|
||||
<IntroSection />
|
||||
<IdeaToAgentSection />
|
||||
<UseCasesSection />
|
||||
<VerticalCarouselSection />
|
||||
<HowItWorksSection />
|
||||
<UnlockPotentialSection variant="transparent" />
|
||||
|
||||
{/* Rest of the sections will be refactored next */}
|
||||
</>
|
||||
);
|
||||
}
|
||||
28
apps/www/src/app/research/page.tsx
Normal file
28
apps/www/src/app/research/page.tsx
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import {
|
||||
ResearchHero,
|
||||
ResearchGrid,
|
||||
ResearchTimeline,
|
||||
ResearchCTA,
|
||||
ResearchCapabilities,
|
||||
ResearchBenchmarks,
|
||||
ResearchFamily,
|
||||
} from "@/components/research";
|
||||
import { UnlockPotentialSection } from "@/components/UnlockPotentialSection";
|
||||
|
||||
export default function ResearchPage() {
|
||||
return (
|
||||
<>
|
||||
<ResearchHero />
|
||||
<ResearchGrid />
|
||||
<ResearchTimeline />
|
||||
<ResearchCTA />
|
||||
<ResearchCapabilities />
|
||||
<ResearchBenchmarks />
|
||||
{/* <ResearchFamily /> */}
|
||||
<UnlockPotentialSection variant="transparent" />
|
||||
</>
|
||||
);
|
||||
}
|
||||
8
apps/www/src/app/studio/[[...index]]/page.tsx
Normal file
8
apps/www/src/app/studio/[[...index]]/page.tsx
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
"use client";
|
||||
|
||||
import { NextStudio } from "next-sanity/studio";
|
||||
import config from "../../../../sanity.config";
|
||||
|
||||
export default function StudioPage() {
|
||||
return <NextStudio config={config} />;
|
||||
}
|
||||
11
apps/www/src/app/studio/layout.tsx
Normal file
11
apps/www/src/app/studio/layout.tsx
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
export default function StudioLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<div className="fixed inset-0 h-screen w-screen overflow-hidden">
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
90
apps/www/src/components/AsciiDiagram.tsx
Normal file
90
apps/www/src/components/AsciiDiagram.tsx
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
import React from "react";
|
||||
|
||||
interface AsciiDiagramProps {
|
||||
title?: string;
|
||||
content: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const AsciiDiagram: React.FC<AsciiDiagramProps> = ({
|
||||
title,
|
||||
content,
|
||||
className = "",
|
||||
}) => {
|
||||
return (
|
||||
<div className={`max-w-4xl mx-auto mb-8 ${className}`}>
|
||||
{title && (
|
||||
<h2 className="text-2xl font-bold text-gray-900 dark:text-zinc-50 mb-4">
|
||||
{title}
|
||||
</h2>
|
||||
)}
|
||||
<div className="bg-gray-900 dark:bg-gray-950 rounded-lg p-6 shadow-xl overflow-x-auto">
|
||||
<pre
|
||||
className="relative font-mono text-xs leading-none text-white m-0 whitespace-pre"
|
||||
style={{ fontFamily: "var(--font-jetbrains-mono), monospace" }}
|
||||
>
|
||||
<code>{content}</code>
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// Programmatic diagram builder for non-coders
|
||||
interface DiagramStep {
|
||||
id: string;
|
||||
label: string;
|
||||
type?: "input" | "inner" | "regular";
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
|
||||
interface FlowConnection {
|
||||
from: string;
|
||||
to: string;
|
||||
label?: string;
|
||||
}
|
||||
|
||||
interface DiagramConfig {
|
||||
title: string;
|
||||
steps: DiagramStep[];
|
||||
connections: FlowConnection[];
|
||||
}
|
||||
|
||||
// Simple ASCII diagram generator
|
||||
export const createDiagram = (config: DiagramConfig): string => {
|
||||
// This is a simplified version - you can extend this to automatically generate
|
||||
// the ASCII art from the config
|
||||
// For now, return the manually created diagrams
|
||||
return "";
|
||||
};
|
||||
|
||||
// Helper to create boxes
|
||||
export const createBox = (
|
||||
label: string,
|
||||
type: "input" | "inner" | "regular" = "regular",
|
||||
width: number = 20,
|
||||
): string[] => {
|
||||
const padding = Math.max(0, Math.floor((width - label.length) / 2));
|
||||
const spaces = " ".repeat(padding);
|
||||
const remaining = width - label.length - padding;
|
||||
|
||||
let chars;
|
||||
switch (type) {
|
||||
case "input":
|
||||
chars = { tl: "╔", tr: "╗", bl: "╚", br: "╝", h: "═", v: "║" };
|
||||
break;
|
||||
case "inner":
|
||||
chars = { tl: "┏", tr: "┓", bl: "┗", br: "┛", h: "━", v: "┃" };
|
||||
break;
|
||||
case "regular":
|
||||
default:
|
||||
chars = { tl: "┌", tr: "┐", bl: "└", br: "┘", h: "─", v: "│" };
|
||||
}
|
||||
|
||||
return [
|
||||
`${chars.tl}${chars.h.repeat(width)}${chars.tr}`,
|
||||
`${chars.v}${spaces}${label}${" ".repeat(remaining)}${chars.v}`,
|
||||
`${chars.bl}${chars.h.repeat(width)}${chars.br}`,
|
||||
];
|
||||
};
|
||||
87
apps/www/src/components/BlogCard.tsx
Normal file
87
apps/www/src/components/BlogCard.tsx
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
"use client";
|
||||
|
||||
import { motion } from "framer-motion";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { urlFor } from "@/lib/sanity";
|
||||
|
||||
interface BlogCardProps {
|
||||
post: {
|
||||
_id: string;
|
||||
title: string;
|
||||
slug: { current: string };
|
||||
formattedDate?: string;
|
||||
author?: {
|
||||
name?: string;
|
||||
title?: string;
|
||||
image?: any;
|
||||
};
|
||||
};
|
||||
index?: number;
|
||||
}
|
||||
|
||||
export function BlogCard({ post, index = 0 }: BlogCardProps) {
|
||||
return (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{
|
||||
duration: 0.4,
|
||||
delay: index * 0.05,
|
||||
ease: "easeOut",
|
||||
}}
|
||||
>
|
||||
<Link href={`/blog/${post.slug.current}`} className="group block h-full">
|
||||
<motion.article
|
||||
className="h-full min-h-[320px] bg-linear-to-b from-primary/20 to-primary/1 border border-primary/20 rounded-md p-6 sm:p-8 flex flex-col"
|
||||
whileHover={{
|
||||
borderColor: "rgba(119, 128, 217, 0.5)",
|
||||
}}
|
||||
transition={{
|
||||
duration: 0.2,
|
||||
ease: "easeOut",
|
||||
}}
|
||||
>
|
||||
{post.formattedDate && (
|
||||
<div className="text-base font-medium tracking-[-0.9px] text-black mb-6">
|
||||
{post.formattedDate}
|
||||
</div>
|
||||
)}
|
||||
<h3 className="text-xl sm:text-2xl font-normal leading-tight tracking-tighter text-black group-hover:text-[var(--secondary)] transition-colors flex-grow">
|
||||
<span className="font-sans font-medium tracking-[-1.5px]">
|
||||
{post.title}
|
||||
</span>
|
||||
</h3>
|
||||
{post.author && (
|
||||
<div className="flex items-center gap-3 mt-auto pt-6">
|
||||
{post.author.image ? (
|
||||
<div className="relative w-10 h-10 rounded overflow-hidden shrink-0">
|
||||
<Image
|
||||
src={urlFor(post.author.image).width(80).url()}
|
||||
alt={post.author.name || "Author"}
|
||||
fill
|
||||
className="object-cover"
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="w-10 h-10 rounded-full bg-[var(--secondary)]/20 shrink-0" />
|
||||
)}
|
||||
<div>
|
||||
{post.author.name && (
|
||||
<div className="text-base font-mono font-semibold tracking-wider text-primary uppercase">
|
||||
{post.author.name}
|
||||
</div>
|
||||
)}
|
||||
{post.author.title && (
|
||||
<div className="text-xs font-mono font-normal tracking-wider text-[#28327D] uppercase">
|
||||
{post.author.title}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</motion.article>
|
||||
</Link>
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
25
apps/www/src/components/BlogHeader.tsx
Normal file
25
apps/www/src/components/BlogHeader.tsx
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
"use client";
|
||||
|
||||
import { motion } from "framer-motion";
|
||||
|
||||
export function BlogHeader() {
|
||||
return (
|
||||
<motion.section
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{
|
||||
duration: 0.5,
|
||||
ease: "easeOut",
|
||||
}}
|
||||
>
|
||||
<div className="max-w-[85rem] mx-auto px-4 sm:px-6 lg:px-8 pt-8 sm:pt-12 lg:pt-1 pb-8 sm:pb-12 lg:pb-16">
|
||||
<h1 className="text-4xl sm:text-5xl lg:text-7xl font-normal leading-tight tracking-tighter text-black mb-3 sm:mb-4">
|
||||
<span className="font-sans">What's new with Plano</span>
|
||||
</h1>
|
||||
<p className="text-base sm:text-lg md:text-xl lg:text-2xl font-sans font-normal tracking-[-1.0px] sm:tracking-[-1.2px] text-black max-w-3xl">
|
||||
Building the future of infrastructure and tools for AI developers.
|
||||
</p>
|
||||
</div>
|
||||
</motion.section>
|
||||
);
|
||||
}
|
||||
9
apps/www/src/components/BlogSectionHeader.tsx
Normal file
9
apps/www/src/components/BlogSectionHeader.tsx
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
"use client";
|
||||
|
||||
export function BlogSectionHeader() {
|
||||
return (
|
||||
<h2 className="text-2xl sm:text-3xl lg:text-4xl font-normal leading-tight tracking-tighter text-black mb-12">
|
||||
<span className="font-sans">The latest and greatest from our blog.</span>
|
||||
</h2>
|
||||
);
|
||||
}
|
||||
21
apps/www/src/components/ConditionalLayout.tsx
Normal file
21
apps/www/src/components/ConditionalLayout.tsx
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
"use client";
|
||||
|
||||
import { usePathname } from "next/navigation";
|
||||
import { Navbar, Footer } from "@katanemo/ui";
|
||||
|
||||
export function ConditionalLayout({ children }: { children: React.ReactNode }) {
|
||||
const pathname = usePathname();
|
||||
const isStudio = pathname?.startsWith("/studio");
|
||||
|
||||
if (isStudio) {
|
||||
return <>{children}</>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen">
|
||||
<Navbar />
|
||||
<main className="pt-2 md:pt-10">{children}</main>
|
||||
<Footer />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
34
apps/www/src/components/DiagramBuilder.tsx
Normal file
34
apps/www/src/components/DiagramBuilder.tsx
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
import React from "react";
|
||||
import { createFlowDiagram, FlowDiagramConfig } from "@/utils/asciiBuilder";
|
||||
import { AsciiDiagram } from "./AsciiDiagram";
|
||||
|
||||
interface DiagramBuilderProps {
|
||||
config: FlowDiagramConfig;
|
||||
title?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple Diagram Builder Component
|
||||
*
|
||||
* Usage:
|
||||
*
|
||||
* <DiagramBuilder
|
||||
* config={{
|
||||
* title: "My Process",
|
||||
* width: 60,
|
||||
* steps: [
|
||||
* { label: "Start", type: "regular" },
|
||||
* { label: "Process", type: "inner" },
|
||||
* { label: "End", type: "regular" }
|
||||
* ]
|
||||
* }}
|
||||
* />
|
||||
*/
|
||||
export const DiagramBuilder: React.FC<DiagramBuilderProps> = ({
|
||||
config,
|
||||
title,
|
||||
}) => {
|
||||
const asciiDiagram = createFlowDiagram(config);
|
||||
|
||||
return <AsciiDiagram content={asciiDiagram} title={title} />;
|
||||
};
|
||||
117
apps/www/src/components/FeaturedBlogCard.tsx
Normal file
117
apps/www/src/components/FeaturedBlogCard.tsx
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
"use client";
|
||||
|
||||
import { motion } from "framer-motion";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { urlFor } from "@/lib/sanity";
|
||||
|
||||
interface FeaturedBlogCardProps {
|
||||
post: {
|
||||
_id: string;
|
||||
title: string;
|
||||
slug: { current: string };
|
||||
summary?: string;
|
||||
formattedDate?: string;
|
||||
mainImage?: any;
|
||||
mainImageUrl?: string;
|
||||
author?: {
|
||||
name?: string;
|
||||
title?: string;
|
||||
image?: any;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export function FeaturedBlogCard({ post }: FeaturedBlogCardProps) {
|
||||
return (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{
|
||||
duration: 0.4,
|
||||
ease: "easeOut",
|
||||
}}
|
||||
>
|
||||
<Link href={`/blog/${post.slug.current}`} className="group block">
|
||||
<motion.div
|
||||
className="bg-linear-to-b from-primary/20 to-primary/1 border border-primary/20 rounded-md p-8 sm:p-10 lg:p-12"
|
||||
whileHover={{
|
||||
borderColor: "rgba(119, 128, 217, 0.5)",
|
||||
}}
|
||||
transition={{
|
||||
duration: 0.2,
|
||||
ease: "easeOut",
|
||||
}}
|
||||
>
|
||||
<div className="grid lg:grid-cols-2 gap-8 lg:gap-12 items-center">
|
||||
{/* Content */}
|
||||
<div className="order-1 text-left">
|
||||
{post.formattedDate && (
|
||||
<div className="text-base font-medium tracking-[-0.9px] text-black mb-4">
|
||||
{post.formattedDate}
|
||||
</div>
|
||||
)}
|
||||
<h2 className="text-3xl sm:text-4xl lg:text-4xl font-medium tracking-[-1.5px] text-black mb-4 group-hover:text-[var(--secondary)] transition-colors text-left">
|
||||
<span className="font-sans">{post.title}</span>
|
||||
</h2>
|
||||
{post.summary && (
|
||||
<p className="text-base sm:text-base font-mono font-normal tracking-[-0.9px] text-black/70 mb-6 text-left">
|
||||
{post.summary}
|
||||
</p>
|
||||
)}
|
||||
{post.author && (
|
||||
<div className="flex items-center gap-3">
|
||||
{post.author.image ? (
|
||||
<div className="relative w-12 h-12 rounded overflow-hidden shrink-0">
|
||||
<Image
|
||||
src={urlFor(post.author.image).width(80).url()}
|
||||
alt={post.author.name || "Author"}
|
||||
fill
|
||||
className="object-cover"
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="w-12 h-12 rounded-full bg-[var(--secondary)]/20 shrink-0" />
|
||||
)}
|
||||
<div>
|
||||
{post.author.name && (
|
||||
<div className="text-lg font-mono font-semibold tracking-wider text-primary uppercase">
|
||||
{post.author.name}
|
||||
</div>
|
||||
)}
|
||||
{post.author.title && (
|
||||
<div className="text-sm font-mono font-normal tracking-wider text-[#28327D] uppercase">
|
||||
{post.author.title}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Image */}
|
||||
<div className="relative aspect-[18/9] w-full overflow-hidden rounded-lg bg-black/5 order-2">
|
||||
{post.mainImage ? (
|
||||
<Image
|
||||
src={urlFor(post.mainImage).width(800).url()}
|
||||
alt={post.title}
|
||||
fill
|
||||
className="object-cover"
|
||||
/>
|
||||
) : post.mainImageUrl ? (
|
||||
<Image
|
||||
src={post.mainImageUrl}
|
||||
alt={post.title}
|
||||
fill
|
||||
className="object-cover"
|
||||
/>
|
||||
) : (
|
||||
<div className="w-full h-full bg-gradient-to-br from-[var(--secondary)]/20 to-black/10" />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</Link>
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
68
apps/www/src/components/Hero.tsx
Normal file
68
apps/www/src/components/Hero.tsx
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
import React from "react";
|
||||
import { Button } from "@katanemo/ui";
|
||||
import Link from "next/link";
|
||||
import { NetworkAnimation } from "./NetworkAnimation";
|
||||
|
||||
export function Hero() {
|
||||
return (
|
||||
<section className="relative pt-8 sm:pt-12 lg:pt-1 pb-6 px-4 sm:px-6 lg:px-8 overflow-hidden">
|
||||
<div className="max-w-[81rem] mx-auto relative">
|
||||
<div className="hidden lg:block absolute inset-0 pointer-events-none ">
|
||||
<NetworkAnimation />
|
||||
</div>
|
||||
<div className="lg:hidden absolute inset-0 pointer-events-none">
|
||||
<NetworkAnimation className="!w-[300px] !h-[300px] left-82! top-1! opacity-90! " />
|
||||
</div>
|
||||
<div className="max-w-3xl mb-3 sm:mb-4 relative z-10">
|
||||
{/* Version Badge */}
|
||||
<div className="mb-4 sm:mb-6">
|
||||
<div className="inline-flex flex-wrap items-center gap-1.5 sm:gap-2 px-3 sm:px-4 py-1 rounded-full bg-[rgba(185,191,255,0.4)] border border-[var(--secondary)] shadow backdrop-blur">
|
||||
<span className="text-xs sm:text-sm font-medium text-black/65">
|
||||
v0.4
|
||||
</span>
|
||||
<span className="text-xs sm:text-sm font-medium text-black ">
|
||||
—
|
||||
</span>
|
||||
<span className="text-xs sm:text-sm font-[600] tracking-[-0.6px]! text-black leading-tight">
|
||||
<span className="hidden sm:inline">
|
||||
Unified /v1/responses API with state management
|
||||
</span>
|
||||
<span className="sm:hidden">Unified /v1/responses API</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Main Heading */}
|
||||
<h1 className="text-4xl sm:text-4xl md:text-5xl lg:text-7xl font-normal leading-tight tracking-tighter text-black flex flex-col gap-0 sm:-space-y-2 lg:-space-y-3">
|
||||
<span className="font-sans">Delivery Infrastructure </span>
|
||||
<span className="font-sans font-medium text-[var(--secondary)]">
|
||||
for Agentic Apps
|
||||
</span>
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
{/* Subheading with CTA Buttons */}
|
||||
<div className="max-w-7xl relative z-10">
|
||||
<p className="text-base sm:text-lg md:text-xl lg:text-[22px] font-sans font-[400] tracking-[-1.0px] sm:tracking-[-1.22px]! text-black max-w-76 sm:max-w-2xl mb-6">
|
||||
Build agents faster, and deliver them reliably to production - by
|
||||
offloading the critical plumbing work to Plano.
|
||||
</p>
|
||||
|
||||
{/* CTA Buttons */}
|
||||
<div className="flex flex-col sm:flex-row items-stretch sm:items-start gap-3 sm:gap-4">
|
||||
<Button asChild className="w-full sm:w-auto">
|
||||
<Link href="https://docs.planoai.dev/get_started/quickstart" target="_blank" rel="noopener noreferrer">
|
||||
Get started
|
||||
</Link>
|
||||
</Button>
|
||||
<Button variant="secondary" asChild className="w-full sm:w-auto">
|
||||
<Link href="https://docs.planoai.dev" target="_blank" rel="noopener noreferrer">
|
||||
Documentation
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
70
apps/www/src/components/HowItWorksSection.tsx
Normal file
70
apps/www/src/components/HowItWorksSection.tsx
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import Image from "next/image";
|
||||
|
||||
export function HowItWorksSection() {
|
||||
return (
|
||||
<section className="bg-[#1a1a1a] text-white pb-16 sm:pb-20 lg:pb-28 sm:pt-0 pt-20">
|
||||
<div className="max-w-312 mx-auto sm:pl-0">
|
||||
<div className="flex flex-col gap-8 sm:gap-12 lg:gap-16">
|
||||
{/* Header and Description */}
|
||||
<div className="max-w-4xl lg:-ml-[102px] lg:pl-[102px] sm:pl-0 pl-4">
|
||||
<h2 className="font-sans font-normal text-xl sm:text-2xl lg:text-3xl tracking-[-1.6px] sm:tracking-[-2px]! text-white leading-[1.03] mb-6 sm:mb-8">
|
||||
One configuration file to orchestrate
|
||||
</h2>
|
||||
<div className="text-white w-100 sm:w-full text-sm sm:text-lg lg:text-lg">
|
||||
<p className="mb-0">
|
||||
Plano offers a delightful developer experience with a simple
|
||||
configuration file that describes the types of prompts your
|
||||
agentic app supports, a set of APIs that need to be plugged in
|
||||
for agentic scenarios (including retrieval queries) and your
|
||||
choice of LLMs.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Large Diagram - Scrollable on mobile, normal on desktop */}
|
||||
{/* Mobile: Full-width scrollable container that extends to viewport edges */}
|
||||
<div
|
||||
className="mt-5 lg:hidden relative left-1/2 right-1/2 -ml-[50vw] -mr-[50vw] w-screen overflow-x-auto overflow-y-visible"
|
||||
style={{
|
||||
scrollbarWidth: "none",
|
||||
msOverflowStyle: "none",
|
||||
WebkitOverflowScrolling: "touch",
|
||||
}}
|
||||
>
|
||||
<style jsx>{`
|
||||
.diagram-scroll-container::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
`}</style>
|
||||
<div className="diagram-scroll-container inline-block">
|
||||
<Image
|
||||
src="/HowItWorks.svg"
|
||||
alt="How Plano Works Diagram"
|
||||
width={1200}
|
||||
height={600}
|
||||
className="h-auto"
|
||||
style={{ width: "1200px", maxWidth: "none", display: "block" }}
|
||||
priority
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Desktop: Extends to container edges */}
|
||||
<div className="hidden lg:block -w-[calc(10%+20px)] -mx-[10px]">
|
||||
<Image
|
||||
src="/HowItWorks.svg"
|
||||
alt="How Plano Works Diagram"
|
||||
width={10}
|
||||
height={10}
|
||||
className="w-full h-auto"
|
||||
priority
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
202
apps/www/src/components/IdeaToAgentSection.tsx
Normal file
202
apps/www/src/components/IdeaToAgentSection.tsx
Normal file
|
|
@ -0,0 +1,202 @@
|
|||
"use client";
|
||||
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
import { Button } from "@katanemo/ui";
|
||||
import Link from "next/link";
|
||||
|
||||
const carouselData = [
|
||||
{
|
||||
id: 1,
|
||||
category: "LAUNCH FASTER",
|
||||
title: "Focus on core objectives",
|
||||
description:
|
||||
"Building agents is hard enough. The plumbing work shouldn't be. Plano handles routing, observability, and policy hooks as a models-native sidecar—so you can focus on your agent's core product logic and ship to production faster.",
|
||||
image: "/LaunchFaster.svg",
|
||||
link: "https://docs.planoai.dev/get_started/quickstart",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
category: "BUILD WITH CHOICE",
|
||||
title: "Rapidly incorporate LLMs",
|
||||
description:
|
||||
"Build with multiple LLMs or model versions with a single unified API. Plano centralizes access controls, offers resiliency for traffic to 100+ LLMs -- all without you having to write a single line of code. Use existing libraries and proxy traffic through Plano.",
|
||||
image: "/BuildWithChoice.svg",
|
||||
link: "https://docs.planoai.dev/concepts/llm_providers/llm_providers",
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
category: "RICH LEARNING SIGNALS",
|
||||
title: "Hyper-rich agent traces and logs",
|
||||
description:
|
||||
"Knowing when agents fail or delight users is a critical signal that feeds into the reinforcement learning and optimization cycle. Plano makes this trivial by sampling hyper-rich information traces from live production agentic interactions so that you can improve agent performance faster.",
|
||||
image: "/Telemetry.svg",
|
||||
link: "https://docs.planoai.dev/guides/observability/observability.html",
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
category: "SHIP CONFIDENTLY",
|
||||
title: "Centrally apply guardrail policies",
|
||||
description:
|
||||
"Plano comes built-in with a state-of-the-art guardrail model you can use for things like jailbreak detection. But you can easily extend those capabilities via plano's agent filter chain to apply custom policy checks in a centralized way and keep users engaged on topics relevant to your requirements.",
|
||||
image: "/ShipConfidently.svg",
|
||||
link: "https://docs.planoai.dev/guides/prompt_guard.html",
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
category: "SCALABLE ARCHITECTURE",
|
||||
title: "Protocol-Native Infrastructure",
|
||||
description:
|
||||
"Plano's sidecar deployment model avoids library-based abstractions - operating as a protocol-native data plane that integrates seamlessly with your existing agents via agentic APIs (like v1/responses). This decouples your core agent logic from plumbing concerns - run it alongside any framework without code changes, vendor lock-in, or performance overhead.",
|
||||
image: "/Contextual.svg",
|
||||
link: "https://docs.planoai.dev/concepts/tech_overview/tech_overview.html",
|
||||
},
|
||||
];
|
||||
|
||||
export function IdeaToAgentSection() {
|
||||
const [currentSlide, setCurrentSlide] = useState(0);
|
||||
const [isAutoPlaying, setIsAutoPlaying] = useState(true);
|
||||
|
||||
// Auto-advance slides
|
||||
useEffect(() => {
|
||||
if (!isAutoPlaying) return;
|
||||
|
||||
const interval = setInterval(() => {
|
||||
setCurrentSlide((prev) => (prev + 1) % carouselData.length);
|
||||
}, 10000); // 10 seconds per slide
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, [isAutoPlaying]);
|
||||
|
||||
const handleSlideClick = (index: number) => {
|
||||
setCurrentSlide(index);
|
||||
setIsAutoPlaying(false);
|
||||
// Resume auto-play after 10 seconds
|
||||
setTimeout(() => setIsAutoPlaying(true), 10000);
|
||||
};
|
||||
|
||||
return (
|
||||
<section className="relative py-12 sm:py-16 lg:py-24 px-4 sm:px-6 lg:px-[102px]">
|
||||
<div className="max-w-[81rem] mx-auto">
|
||||
{/* Main Heading */}
|
||||
<h2 className="font-sans font-normal text-2xl sm:text-3xl lg:text-4xl tracking-[-2px] sm:tracking-[-2.96px]! text-black mb-6 sm:mb-8 lg:mb-10">
|
||||
Idea to agent — without overhead
|
||||
</h2>
|
||||
|
||||
{/* Progress Indicators */}
|
||||
<div className="flex gap-1.5 sm:gap-2 mb-4 sm:mb-6 lg:mb-6 w-full">
|
||||
{carouselData.map((_, index) => (
|
||||
<button
|
||||
key={index}
|
||||
onClick={() => handleSlideClick(index)}
|
||||
className={`relative h-1.5 sm:h-2 rounded-full overflow-hidden transition-all duration-300 hover:opacity-80 ${
|
||||
index === currentSlide
|
||||
? "flex-1 sm:w-16 md:w-20 lg:w-[292px]"
|
||||
: "flex-1 sm:w-16 md:w-20 lg:w-[293px]"
|
||||
}`}
|
||||
>
|
||||
{/* Background */}
|
||||
<div className="absolute inset-0 bg-black/6 rounded-full" />
|
||||
|
||||
{/* Active Progress */}
|
||||
{index === currentSlide && (
|
||||
<motion.div
|
||||
className="absolute inset-0 bg-[#7780d9] rounded-full"
|
||||
initial={{ width: 0 }}
|
||||
animate={{ width: "100%" }}
|
||||
transition={{ duration: 10, ease: "linear" }}
|
||||
key={currentSlide}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Completed State */}
|
||||
{index < currentSlide && (
|
||||
<div className="absolute inset-0 bg-purple-200/90 rounded-full" />
|
||||
)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Carousel Content - Fixed height to prevent layout shift */}
|
||||
<div className="relative h-[500px] sm:h-[550px] md:h-[600px] lg:h-[500px]">
|
||||
<AnimatePresence mode="wait">
|
||||
<motion.div
|
||||
key={currentSlide}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -20 }}
|
||||
transition={{ duration: 0.4, ease: "easeInOut" }}
|
||||
className="absolute inset-0"
|
||||
>
|
||||
<div className="flex flex-col lg:flex-row lg:justify-between lg:items-center lg:gap-12 h-full">
|
||||
{/* Left Content */}
|
||||
<div className="flex-1 order-1 lg:order-1 flex flex-col justify-center">
|
||||
<div className="max-w-[692px] mt-0 lg:mt-0">
|
||||
{/* Category */}
|
||||
<p className="font-mono font-bold text-[#2a3178] text-sm sm:text-base lg:text-xl tracking-[1.44px] sm:tracking-[1.92px]! mb-3 sm:mb-4 leading-[1.102]">
|
||||
{carouselData[currentSlide].category}
|
||||
</p>
|
||||
|
||||
{/* Title */}
|
||||
<h3 className="font-sans font-medium text-[#9797ea] text-2xl sm:text-3xl lg:text-5xl tracking-tight sm:tracking-[-2.96px]! mb-4 sm:mb-6 lg:mb-7">
|
||||
{carouselData[currentSlide].title}
|
||||
</h3>
|
||||
|
||||
{/* Description */}
|
||||
<div className="text-black text-sm sm:text-base lg:text-lg max-w-full lg:max-w-140">
|
||||
<p className="mb-0">
|
||||
{carouselData[currentSlide].description}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Button asChild className="mt-6 sm:mt-8 w-full sm:w-auto">
|
||||
<Link href={carouselData[currentSlide].link}>
|
||||
Learn more
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Image - Show below on mobile, right side on desktop */}
|
||||
{carouselData[currentSlide].image && (
|
||||
<div className="flex lg:hidden shrink-0 w-full justify-center items-center mb-6 sm:mb-8 order-0 lg:order-2">
|
||||
<img
|
||||
src={carouselData[currentSlide].image}
|
||||
alt={carouselData[currentSlide].category}
|
||||
className={`w-full h-auto object-contain ${
|
||||
carouselData[currentSlide].image === "/Telemetry.svg"
|
||||
? "max-w-md sm:max-w-lg max-h-[300px] sm:max-h-[350px]"
|
||||
: "max-w-sm sm:max-w-md max-h-[250px] sm:max-h-[300px]"
|
||||
}`}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Right Image - Desktop only */}
|
||||
{carouselData[currentSlide].image && (
|
||||
<div
|
||||
className={`hidden lg:flex shrink-0 justify-end items-center order-2 ${
|
||||
carouselData[currentSlide].image === "/Telemetry.svg"
|
||||
? "w-[500px] xl:w-[600px]"
|
||||
: "w-[400px] xl:w-[500px]"
|
||||
}`}
|
||||
>
|
||||
<img
|
||||
src={carouselData[currentSlide].image}
|
||||
alt={carouselData[currentSlide].category}
|
||||
className={`w-full h-auto object-contain ${
|
||||
carouselData[currentSlide].image === "/Telemetry.svg"
|
||||
? "max-h-[550px]"
|
||||
: "max-h-[450px]"
|
||||
}`}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
54
apps/www/src/components/IntroSection.tsx
Normal file
54
apps/www/src/components/IntroSection.tsx
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
import React from "react";
|
||||
import Image from "next/image";
|
||||
|
||||
export function IntroSection() {
|
||||
return (
|
||||
<section className="relative bg-[#1a1a1a] text-white py-20 px-6 lg:px-[102px]">
|
||||
<div className="max-w-324 mx-auto">
|
||||
<div className="flex flex-col lg:flex-row gap-12">
|
||||
{/* Left Content */}
|
||||
<div className="flex-1 mt-2">
|
||||
{/* Heading */}
|
||||
<p className="font-mono font-bold text-primary-light text-xl tracking-[1.92px]! mb-4 leading-[1.102]">
|
||||
WHY PLANO?
|
||||
</p>
|
||||
<h2 className="font-sans font-medium tracking-[-1.92px]! text-[#9797ea] text-4xl leading-[1.102] mb-6 max-w-[633px]">
|
||||
Deliver prototypes to production
|
||||
<span className="italic">—fast.</span>
|
||||
</h2>
|
||||
|
||||
{/* Body Text */}
|
||||
<div className="text-white text-sm sm:text-base lg:text-lg max-w-[713px]">
|
||||
<p className="mb-0">
|
||||
Plano is a models-native proxy and dataplane for agents that
|
||||
handles critical plumbing work in AI - agent routing and
|
||||
orchestration, rich agentic traces, guardrail hooks, and smart
|
||||
model routing APIs for LLMs. Use any language, AI framework, and
|
||||
deliver agents to productions quickly with Plano.
|
||||
</p>
|
||||
<p className="mb-0 mt-4">
|
||||
Developers can focus more on core product logic of agents.
|
||||
Product teams can accelerate feedback loops for reinforcement
|
||||
learning. Engineering teams can standardize policies and access
|
||||
controls across every agent and LLM for safer, more reliable
|
||||
scaling.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right Diagram */}
|
||||
<div className="flex-1 relative w-full">
|
||||
<Image
|
||||
src="/IntroDiagram.svg"
|
||||
alt="Network Path Diagram"
|
||||
width={800}
|
||||
height={600}
|
||||
className="w-full h-auto"
|
||||
priority
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
69
apps/www/src/components/LogoCloud.tsx
Normal file
69
apps/www/src/components/LogoCloud.tsx
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
import React from "react";
|
||||
import Image from "next/image";
|
||||
|
||||
const customerLogos = [
|
||||
{
|
||||
name: "HuggingFace",
|
||||
src: "/logos/huggingface.svg",
|
||||
},
|
||||
{
|
||||
name: "T-Mobile",
|
||||
src: "/logos/tmobile.svg",
|
||||
},
|
||||
{
|
||||
name: "HP",
|
||||
src: "/logos/hp.svg",
|
||||
},
|
||||
{
|
||||
name: "SanDisk",
|
||||
src: "/logos/sandisk.svg",
|
||||
},
|
||||
{
|
||||
name: "Chase",
|
||||
src: "/logos/chase.svg",
|
||||
},
|
||||
];
|
||||
|
||||
export function LogoCloud() {
|
||||
return (
|
||||
<section className="relative py-6 sm:py-8 px-4 sm:px-6 lg:px-8 bg-transparent">
|
||||
<div className="max-w-[81rem] mx-auto">
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 lg:flex lg:flex-row lg:justify-center lg:items-center gap-4 sm:gap-6 md:gap-8 lg:gap-0 place-items-center">
|
||||
{customerLogos.map((logo, index) => {
|
||||
const isLast = index === customerLogos.length - 1;
|
||||
const isTMobile = index === 1; // T-Mobile is before HP
|
||||
const isHP = index === 2; // HP is in center
|
||||
const isSanDisk = index === 3; // SanDisk is after HP
|
||||
|
||||
// Custom spacing for logos around HP on large screens
|
||||
let spacingClass = "lg:mx-6 xl:mx-8"; // Default spacing
|
||||
if (isTMobile) {
|
||||
spacingClass = "lg:mr-3 xl:mr-4 lg:ml-6 xl:ml-8"; // Smaller gap to HP
|
||||
} else if (isHP) {
|
||||
spacingClass = "lg:mx-3 xl:mx-4"; // Smaller gaps on both sides
|
||||
} else if (isSanDisk) {
|
||||
spacingClass = "lg:ml-3 xl:ml-4 lg:mr-6 xl:mr-8"; // Smaller gap from HP
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
key={logo.name}
|
||||
className={`flex items-center justify-center opacity-60 hover:opacity-80 transition-opacity duration-300 w-full max-w-32 sm:max-w-40 md:max-w-48 h-10 sm:h-12 md:h-16 mx-auto ${spacingClass} ${
|
||||
isLast ? "col-span-2 md:col-span-3 lg:col-span-none" : ""
|
||||
}`}
|
||||
>
|
||||
<Image
|
||||
src={logo.src}
|
||||
alt={`${logo.name} logo`}
|
||||
width={128}
|
||||
height={40}
|
||||
className="w-full h-full object-contain filter grayscale hover:grayscale-0 transition-all duration-300"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
206
apps/www/src/components/NetworkAnimation.tsx
Normal file
206
apps/www/src/components/NetworkAnimation.tsx
Normal file
|
|
@ -0,0 +1,206 @@
|
|||
"use client";
|
||||
|
||||
import React, { useId } from "react";
|
||||
import { motion } from "framer-motion";
|
||||
|
||||
// Define the grid of squares with their positions and colors
|
||||
const squares = [
|
||||
// Column 1 (x=3)
|
||||
{ x: 3, y: 3, color: "#B0B7FF", col: 0, row: 0 },
|
||||
{ x: 3, y: 6, color: "#B0B7FF", col: 0, row: 1 },
|
||||
{ x: 3, y: 9, color: "#B0B7FF", col: 0, row: 2 },
|
||||
{ x: 3, y: 12, color: "#ABB2FA", col: 0, row: 3 },
|
||||
{ x: 3, y: 15, color: "#ABB2FA", col: 0, row: 4 },
|
||||
{ x: 3, y: 18, color: "#ABB2FA", col: 0, row: 5 },
|
||||
{ x: 3, y: 21, color: "#969FF4", col: 0, row: 6 },
|
||||
|
||||
// Column 2 (x=6)
|
||||
{ x: 6, y: 3, color: "#B0B7FF", col: 1, row: 0 },
|
||||
{ x: 6, y: 6, color: "#B0B7FF", col: 1, row: 1 },
|
||||
{ x: 6, y: 9, color: "#ABB2FA", col: 1, row: 2 },
|
||||
{ x: 6, y: 12, color: "#ABB2FA", col: 1, row: 3 },
|
||||
{ x: 6, y: 15, color: "#ABB2FA", col: 1, row: 4 },
|
||||
{ x: 6, y: 18, color: "#969FF4", col: 1, row: 5 },
|
||||
{ x: 6, y: 21, color: "#969FF4", col: 1, row: 6 },
|
||||
|
||||
// Column 3 (x=9)
|
||||
{ x: 9, y: 3, color: "#B0B7FF", col: 2, row: 0 },
|
||||
{ x: 9, y: 6, color: "#ABB2FA", col: 2, row: 1 },
|
||||
{ x: 9, y: 9, color: "#ABB2FA", col: 2, row: 2 },
|
||||
{ x: 9, y: 12, color: "#ABB2FA", col: 2, row: 3 },
|
||||
{ x: 9, y: 15, color: "#969FF4", col: 2, row: 4 },
|
||||
{ x: 9, y: 18, color: "#969FF4", col: 2, row: 5 },
|
||||
{ x: 9, y: 21, color: "#969FF4", col: 2, row: 6 },
|
||||
|
||||
// Column 4 (x=12)
|
||||
{ x: 12, y: 3, color: "#ABB2FA", col: 3, row: 0 },
|
||||
{ x: 12, y: 6, color: "#ABB2FA", col: 3, row: 1 },
|
||||
{ x: 12, y: 9, color: "#ABB2FA", col: 3, row: 2 },
|
||||
{ x: 12, y: 12, color: "#969FF4", col: 3, row: 3 },
|
||||
{ x: 12, y: 15, color: "#969FF4", col: 3, row: 4 },
|
||||
{ x: 12, y: 18, color: "#969FF4", col: 3, row: 5 },
|
||||
{ x: 12, y: 21, color: "#969FF4", col: 3, row: 6 },
|
||||
|
||||
// Column 5 (x=15)
|
||||
{ x: 15, y: 3, color: "#ABB2FA", col: 4, row: 0 },
|
||||
{ x: 15, y: 6, color: "#ABB2FA", col: 4, row: 1 },
|
||||
{ x: 15, y: 9, color: "#969FF4", col: 4, row: 2 },
|
||||
{ x: 15, y: 12, color: "#969FF4", col: 4, row: 3 },
|
||||
{ x: 15, y: 15, color: "#969FF4", col: 4, row: 4 },
|
||||
{ x: 15, y: 18, color: "#969FF4", col: 4, row: 5 },
|
||||
{ x: 15, y: 21, color: "#969FF4", col: 4, row: 6 },
|
||||
|
||||
// Column 6 (x=18)
|
||||
{ x: 18, y: 3, color: "#ABB2FA", col: 5, row: 0 },
|
||||
{ x: 18, y: 6, color: "#969FF4", col: 5, row: 1 },
|
||||
{ x: 18, y: 9, color: "#969FF4", col: 5, row: 2 },
|
||||
{ x: 18, y: 12, color: "#969FF4", col: 5, row: 3 },
|
||||
{ x: 18, y: 15, color: "#969FF4", col: 5, row: 4 },
|
||||
{ x: 18, y: 18, color: "#969FF4", col: 5, row: 5 },
|
||||
{ x: 18, y: 21, color: "#969FF4", col: 5, row: 6 },
|
||||
|
||||
// Column 7 (x=21)
|
||||
{ x: 21, y: 3, color: "#969FF4", col: 6, row: 0 },
|
||||
{ x: 21, y: 6, color: "#969FF4", col: 6, row: 1 },
|
||||
{ x: 21, y: 9, color: "#969FF4", col: 6, row: 2 },
|
||||
{ x: 21, y: 12, color: "#969FF4", col: 6, row: 3 },
|
||||
{ x: 21, y: 15, color: "#969FF4", col: 6, row: 4 },
|
||||
{ x: 21, y: 18, color: "#969FF4", col: 6, row: 5 },
|
||||
{ x: 21, y: 21, color: "#969FF4", col: 6, row: 6 },
|
||||
];
|
||||
|
||||
interface NetworkAnimationProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
// Deterministic seeded random number generator for consistent SSR/client values
|
||||
function seededRandom(seed: number): number {
|
||||
const x = Math.sin(seed) * 10000;
|
||||
return x - Math.floor(x);
|
||||
}
|
||||
|
||||
// Round to fixed precision to avoid floating-point precision differences
|
||||
function roundToPrecision(value: number, precision: number = 10): number {
|
||||
return Math.round(value * Math.pow(10, precision)) / Math.pow(10, precision);
|
||||
}
|
||||
|
||||
// Generate deterministic random values based on index
|
||||
function getDeterministicValues(index: number) {
|
||||
const seed1 = index * 0.1;
|
||||
const seed2 = index * 0.2;
|
||||
const seed3 = index * 0.3;
|
||||
const seed4 = index * 0.4;
|
||||
const seed5 = index * 0.5;
|
||||
const seed6 = index * 0.6;
|
||||
|
||||
return {
|
||||
duration: roundToPrecision(3 + seededRandom(seed1) * 3, 10), // 3-6 seconds
|
||||
peakOpacity: roundToPrecision(0.7 + seededRandom(seed2) * 0.3, 10),
|
||||
baseOpacity: roundToPrecision(0.3 + seededRandom(seed3) * 0.2, 10),
|
||||
midOpacity: roundToPrecision(0.5 + seededRandom(seed4) * 0.2, 10),
|
||||
baseBrightness: roundToPrecision(0.85 + seededRandom(seed5) * 0.15, 10),
|
||||
peakBrightness: roundToPrecision(1.0 + seededRandom(seed6) * 0.2, 10),
|
||||
};
|
||||
}
|
||||
|
||||
export function NetworkAnimation({ className }: NetworkAnimationProps) {
|
||||
// Generate unique IDs for gradient and mask to avoid conflicts when multiple instances exist
|
||||
const gradientId = useId().replace(/:/g, "-");
|
||||
const maskId = useId().replace(/:/g, "-");
|
||||
|
||||
return (
|
||||
<div className="absolute inset-0 pointer-events-none opacity-100">
|
||||
<motion.div
|
||||
className={`absolute
|
||||
top-[9%] right-[-3%] w-[380px] h-[380px] ${className || ""}`}
|
||||
initial={{
|
||||
rotate: 9, // Start at the same rotation as animation to prevent flicker
|
||||
}}
|
||||
animate={{
|
||||
rotate: [9, 10, 9], // Slight breathing rotation
|
||||
}}
|
||||
transition={{
|
||||
duration: 8,
|
||||
repeat: Infinity,
|
||||
ease: "easeInOut",
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
width="100%"
|
||||
height="100%"
|
||||
viewBox="0 0 32 32"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<defs>
|
||||
{/* Gradient mask: transparent at bottom, opaque at top */}
|
||||
<linearGradient id={gradientId} x1="0%" y1="0%" x2="0%" y2="100%">
|
||||
<stop offset="0%" stopColor="white" stopOpacity="1" />
|
||||
<stop offset="50%" stopColor="white" stopOpacity="0.5" />
|
||||
<stop offset="100%" stopColor="white" stopOpacity="0" />
|
||||
</linearGradient>
|
||||
<mask id={maskId}>
|
||||
<rect width="26" height="26" fill={`url(#${gradientId})`} />
|
||||
</mask>
|
||||
</defs>
|
||||
|
||||
<g mask={`url(#${maskId})`}>
|
||||
{/* Outer border */}
|
||||
<rect width="26" height="26" fill="#7780D9" />
|
||||
|
||||
{/* Inner background */}
|
||||
<rect x="2" y="2" width="22" height="22" fill="#B9BFFF" />
|
||||
|
||||
{/* Animated squares with wave effect */}
|
||||
{squares.map((square, index) => {
|
||||
// Use deterministic values based on index for SSR/client consistency
|
||||
const {
|
||||
duration,
|
||||
peakOpacity,
|
||||
baseOpacity,
|
||||
midOpacity,
|
||||
baseBrightness,
|
||||
peakBrightness,
|
||||
} = getDeterministicValues(index);
|
||||
|
||||
return (
|
||||
<motion.path
|
||||
key={`square-${index}`}
|
||||
d={`M${square.x} ${square.y}H${square.x + 2}V${square.y + 2}H${square.x}V${square.y}Z`}
|
||||
fill={square.color}
|
||||
initial={{
|
||||
opacity: roundToPrecision(baseOpacity, 10),
|
||||
filter: `brightness(${roundToPrecision(baseBrightness, 10)})`,
|
||||
}}
|
||||
animate={{
|
||||
opacity: [
|
||||
roundToPrecision(baseOpacity, 10),
|
||||
roundToPrecision(midOpacity, 10),
|
||||
roundToPrecision(peakOpacity, 10),
|
||||
roundToPrecision(midOpacity, 10),
|
||||
roundToPrecision(baseOpacity, 10),
|
||||
],
|
||||
filter: [
|
||||
`brightness(${roundToPrecision(baseBrightness, 10)})`,
|
||||
`brightness(${roundToPrecision((baseBrightness + peakBrightness) / 2, 10)})`,
|
||||
`brightness(${roundToPrecision(peakBrightness, 10)})`,
|
||||
`brightness(${roundToPrecision((baseBrightness + peakBrightness) / 2, 10)})`,
|
||||
`brightness(${roundToPrecision(baseBrightness, 10)})`,
|
||||
],
|
||||
}}
|
||||
transition={{
|
||||
duration: roundToPrecision(duration, 10),
|
||||
delay: 0, // No delay - instant start
|
||||
repeat: Infinity,
|
||||
ease: "easeInOut",
|
||||
repeatDelay: 0, // No pause between cycles
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</g>
|
||||
</svg>
|
||||
</motion.div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
125
apps/www/src/components/PortableText.tsx
Normal file
125
apps/www/src/components/PortableText.tsx
Normal file
|
|
@ -0,0 +1,125 @@
|
|||
import { PortableText as SanityPortableText } from "@portabletext/react";
|
||||
import Image from "next/image";
|
||||
import { urlFor } from "@/lib/sanity";
|
||||
import type { PortableTextBlock } from "@portabletext/types";
|
||||
|
||||
interface PortableTextProps {
|
||||
content: PortableTextBlock[];
|
||||
}
|
||||
|
||||
const components = {
|
||||
types: {
|
||||
image: ({ value }: any) => {
|
||||
if (!value?.asset) return null;
|
||||
|
||||
const imageUrl = urlFor(value);
|
||||
const asset = value.asset;
|
||||
|
||||
// Get natural dimensions if available from metadata
|
||||
const dimensions = asset.metadata?.dimensions;
|
||||
const width = dimensions?.width || 1000;
|
||||
const height = dimensions?.height || 562;
|
||||
const aspectRatio = dimensions ? height / width : 0.5625; // Default to 16:9 if no dimensions
|
||||
|
||||
return (
|
||||
<div className="my-6 lg:my-8">
|
||||
<div className="max-w-3xl mx-auto">
|
||||
<div className="relative w-full overflow-hidden rounded-lg bg-black/5">
|
||||
<div
|
||||
className="relative w-full"
|
||||
style={{ paddingBottom: `${aspectRatio * 100}%` }}
|
||||
>
|
||||
<Image
|
||||
src={imageUrl.width(Math.min(width, 1000)).url()}
|
||||
alt={value.alt || "Blog image"}
|
||||
fill
|
||||
className="object-contain"
|
||||
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 768px, 1000px"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{value.alt && (
|
||||
<p className="mt-2 text-sm text-black/60 text-center">
|
||||
{value.alt}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
block: {
|
||||
h1: (props: any) => (
|
||||
<h1 className="text-4xl sm:text-5xl lg:text-6xl font-normal leading-tight tracking-tighter text-black mt-8 mb-4 first:mt-0">
|
||||
<span className="font-sans">{props.children}</span>
|
||||
</h1>
|
||||
),
|
||||
h2: (props: any) => (
|
||||
<h2 className="text-3xl sm:text-4xl lg:text-5xl font-normal leading-tight tracking-tighter text-black mt-8 mb-4 first:mt-0">
|
||||
<span className="font-sans">{props.children}</span>
|
||||
</h2>
|
||||
),
|
||||
h3: (props: any) => (
|
||||
<h3 className="text-2xl sm:text-3xl lg:text-4xl font-normal leading-tight tracking-tighter text-black mt-6 mb-3 first:mt-0">
|
||||
<span className="font-sans">{props.children}</span>
|
||||
</h3>
|
||||
),
|
||||
h4: (props: any) => (
|
||||
<h4 className="text-xl sm:text-2xl lg:text-3xl font-normal leading-tight tracking-tighter text-black mt-6 mb-3 first:mt-0">
|
||||
<span className="font-sans">{props.children}</span>
|
||||
</h4>
|
||||
),
|
||||
normal: (props: any) => (
|
||||
<p className="text-base sm:text-lg font-sans font-[400] tracking-[-0.5px] text-black/80 mb-4 leading-relaxed">
|
||||
{props.children}
|
||||
</p>
|
||||
),
|
||||
blockquote: (props: any) => (
|
||||
<blockquote className="border-l-4 border-[var(--secondary)] pl-6 py-2 my-6 italic text-black/70">
|
||||
{props.children}
|
||||
</blockquote>
|
||||
),
|
||||
},
|
||||
list: {
|
||||
bullet: (props: any) => (
|
||||
<ul className="list-disc list-inside mb-4 space-y-2 text-base sm:text-lg font-sans font-[400] tracking-[-0.5px] text-black/80">
|
||||
{props.children}
|
||||
</ul>
|
||||
),
|
||||
number: (props: any) => (
|
||||
<ol className="list-decimal list-inside mb-4 space-y-2 text-base sm:text-lg font-sans font-[400] tracking-[-0.5px] text-black/80">
|
||||
{props.children}
|
||||
</ol>
|
||||
),
|
||||
},
|
||||
listItem: {
|
||||
bullet: (props: any) => <li className="ml-4">{props.children}</li>,
|
||||
number: (props: any) => <li className="ml-4">{props.children}</li>,
|
||||
},
|
||||
marks: {
|
||||
strong: ({ children }: { children: React.ReactNode }) => (
|
||||
<strong className="font-semibold text-black">{children}</strong>
|
||||
),
|
||||
em: ({ children }: { children: React.ReactNode }) => (
|
||||
<em className="italic">{children}</em>
|
||||
),
|
||||
link: (props: any) => (
|
||||
<a
|
||||
href={props.value?.href || "#"}
|
||||
target={props.value?.href?.startsWith("http") ? "_blank" : undefined}
|
||||
rel={
|
||||
props.value?.href?.startsWith("http")
|
||||
? "noopener noreferrer"
|
||||
: undefined
|
||||
}
|
||||
className="text-[var(--secondary)] hover:underline font-medium"
|
||||
>
|
||||
{props.children}
|
||||
</a>
|
||||
),
|
||||
},
|
||||
};
|
||||
|
||||
export function PortableText({ content }: PortableTextProps) {
|
||||
return <SanityPortableText value={content} components={components} />;
|
||||
}
|
||||
46
apps/www/src/components/UnlockPotentialSection.tsx
Normal file
46
apps/www/src/components/UnlockPotentialSection.tsx
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
import React from "react";
|
||||
import { Button } from "@katanemo/ui";
|
||||
import Link from "next/link";
|
||||
|
||||
interface UnlockPotentialSectionProps {
|
||||
variant?: "transparent" | "black";
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function UnlockPotentialSection({
|
||||
variant = "transparent",
|
||||
className = "",
|
||||
}: UnlockPotentialSectionProps) {
|
||||
const backgroundClass = variant === "black" ? "bg-[#1a1a1a]" : "";
|
||||
const textColor = variant === "black" ? "text-white" : "text-black";
|
||||
|
||||
return (
|
||||
<section
|
||||
className={`relative py-24 px-6 lg:px-[102px]`}
|
||||
style={{ background: "linear-gradient(to top right, #ffffff, #dcdfff)" }}
|
||||
>
|
||||
<div className="max-w-[81rem] mx-auto">
|
||||
<div className="max-w-4xl">
|
||||
<h2
|
||||
className={`font-sans font-normal text-[1.8rem] lg:text-4xl tracking-[-2.55px]! ${textColor} leading-[1.4] mb-8`}
|
||||
>
|
||||
Focus on prompting, not plumbing.
|
||||
<br />
|
||||
Build with{" "}
|
||||
<strong className="font-medium text-primary">plano</strong>, get
|
||||
started in less than a minute.
|
||||
</h2>
|
||||
|
||||
<div className="flex flex-col sm:flex-row gap-5">
|
||||
<Button asChild>
|
||||
<Link href="https://docs.planoai.dev/get_started/quickstart">Deploy today</Link>
|
||||
</Button>
|
||||
<Button variant="secondaryDark" asChild>
|
||||
<Link href="https://docs.planoai.dev/get_started/quickstart">Documentation</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
310
apps/www/src/components/UseCasesSection.tsx
Normal file
310
apps/www/src/components/UseCasesSection.tsx
Normal file
|
|
@ -0,0 +1,310 @@
|
|||
"use client";
|
||||
|
||||
import React, { useState } from "react";
|
||||
import {
|
||||
ArrowRightIcon,
|
||||
Network,
|
||||
Filter,
|
||||
TrendingUp,
|
||||
Shield,
|
||||
Server,
|
||||
XIcon,
|
||||
} from "lucide-react";
|
||||
import {
|
||||
Button,
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogDescription,
|
||||
DialogClose,
|
||||
} from "@katanemo/ui";
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
import Link from "next/link";
|
||||
|
||||
interface UseCase {
|
||||
id: number;
|
||||
category: string;
|
||||
title: string;
|
||||
summary: string;
|
||||
fullContent: string;
|
||||
icon: React.ComponentType<{ className?: string }>;
|
||||
gradient: string;
|
||||
}
|
||||
|
||||
const useCasesData: UseCase[] = [
|
||||
{
|
||||
id: 1,
|
||||
category: "AGENT ORCHESTRATION",
|
||||
title: "Multi-agent systems without framework lock-in",
|
||||
summary:
|
||||
"Seamless routing and orchestration for complex agent interactions",
|
||||
fullContent:
|
||||
"Plano manages agent routing and orchestration without framework dependencies, allowing seamless multi-agent interactions. This is ideal for building complex systems like automated customer support or data processing pipelines, where agents hand off tasks efficiently to deliver end-to-end solutions faster.",
|
||||
icon: Network,
|
||||
gradient:
|
||||
"from-[rgba(119,128,217,0.15)] via-[rgba(119,128,217,0.08)] to-[rgba(17,28,132,0.05)]",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
category: "CONTEXT ENGINEERING",
|
||||
title: "Reusable filters for smarter agents",
|
||||
summary:
|
||||
"Inject data, reformulate queries, and enforce policies efficiently",
|
||||
fullContent:
|
||||
"Plano's filter chain encourages reuse and decoupling for context engineering tasks like injecting data, reformulating queries, and enforcing policy before calls reach an agent or LLM. This means faster debugging, cleaner architecture, and more accurate, on-policy agents —without bespoke glue code.",
|
||||
icon: Filter,
|
||||
gradient:
|
||||
"from-[rgba(177,184,255,0.15)] via-[rgba(177,184,255,0.08)] to-[rgba(17,28,132,0.05)]",
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
category: "REINFORCEMENT LEARNING",
|
||||
title: "Production signals for continuous improvement",
|
||||
summary: "Capture rich traces to accelerate training and refinement",
|
||||
fullContent:
|
||||
"Plano captures hyper-rich tracing and log samples from production traffic, feeding into reinforcement learning and fine-tuning cycles. This accelerates iteration in areas like recommendation engines, helping teams quickly identify failures, refine prompts, and boost agent effectiveness based on real-user signals.",
|
||||
icon: TrendingUp,
|
||||
gradient:
|
||||
"from-[rgba(185,191,255,0.15)] via-[rgba(185,191,255,0.08)] to-[rgba(17,28,132,0.05)]",
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
category: "CENTRALIZED SECURITY",
|
||||
title: "Built-in guardrails and centralized policies",
|
||||
summary: "Safe scaling with jailbreak detection and access controls",
|
||||
fullContent:
|
||||
"With built-in guardrails, centralized policies, and access controls, Plano ensures safe scaling across LLMs, detecting issues like jailbreak attempts. This is critical for deployments in regulated fields like finance or healthcare, and minimizing risks while standardizing reliability and security of agents.",
|
||||
icon: Shield,
|
||||
gradient:
|
||||
"from-[rgba(119,128,217,0.15)] via-[rgba(119,128,217,0.08)] to-[rgba(17,28,132,0.05)]",
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
category: "ON-PREMISES DEPLOYMENT",
|
||||
title: "Full data control in regulated environments",
|
||||
summary: "Deploy on private infrastructure without compromising features",
|
||||
fullContent:
|
||||
"Plano's lightweight sidecar model deploys effortlessly on your private infrastructure, empowering teams in regulated sectors to maintain full data control while benefiting from unified LLM access, custom filter chains, and production-grade tracing—without compromising on security or scalability.",
|
||||
icon: Server,
|
||||
gradient:
|
||||
"from-[rgba(177,184,255,0.15)] via-[rgba(177,184,255,0.08)] to-[rgba(17,28,132,0.05)]",
|
||||
},
|
||||
];
|
||||
|
||||
export function UseCasesSection() {
|
||||
const [selectedUseCase, setSelectedUseCase] = useState<UseCase | null>(null);
|
||||
|
||||
return (
|
||||
<section className="relative py-12 sm:py-16 lg:py-10 px-4 sm:px-6 lg:px-[102px]">
|
||||
<div className="max-w-[81rem] mx-auto">
|
||||
{/* Section Header */}
|
||||
<div className="mb-8 sm:mb-12 lg:mb-14">
|
||||
{/* USE CASES Badge */}
|
||||
<div className="mb-4 sm:mb-6">
|
||||
<div className="inline-flex items-center gap-2 px-3 sm:px-4 py-1 rounded-full bg-[rgba(185,191,255,0.4)] border border-[var(--secondary)] shadow backdrop-blur">
|
||||
<span className="font-mono font-bold text-[#2a3178] text-xs sm:text-sm tracking-[1.44px] sm:tracking-[1.62px]!">
|
||||
USE CASES
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Main Heading and CTA Button */}
|
||||
<div className="flex flex-col lg:flex-row lg:items-center lg:justify-between gap-6 sm:gap-6">
|
||||
<h2 className="font-sans font-normal text-2xl sm:text-3xl lg:text-4xl tracking-[-2px] sm:tracking-[-2.88px]! text-black leading-[1.03]">
|
||||
What's possible with Plano
|
||||
</h2>
|
||||
<Button asChild className="hidden lg:block">
|
||||
<Link href="https://docs.planoai.dev/get_started/quickstart">
|
||||
Start building
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 5 Card Grid - Horizontal Row */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-5 gap-4">
|
||||
{useCasesData.map((useCase) => (
|
||||
<motion.div
|
||||
key={useCase.id}
|
||||
whileHover={{ y: -4 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
className="bg-gradient-to-b from-[rgba(177,184,255,0.16)] to-[rgba(17,28,132,0.035)] border-2 border-[rgba(171,178,250,0.27)] rounded-md p-4 sm:p-6 lg:p-6 h-auto sm:h-64 md:h-72 lg:h-90 flex flex-col justify-between cursor-pointer"
|
||||
onClick={() => setSelectedUseCase(useCase)}
|
||||
>
|
||||
{/* Category */}
|
||||
<div className="mb-4 sm:mb-6">
|
||||
<p className="font-mono font-bold text-[#2a3178] text-sm sm:text-sm tracking-[1.44px] sm:tracking-[1.92px]! mb-3 sm:mb-4">
|
||||
{useCase.category}
|
||||
</p>
|
||||
|
||||
{/* Title */}
|
||||
<h3 className="font-sans font-normal text-black text-lg sm:text-xl lg:text-2xl tracking-[-1.2px]! leading-[1.102]">
|
||||
{useCase.title}
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
{/* Learn More Link */}
|
||||
<div className="mt-auto">
|
||||
<button className="group flex items-center gap-2 font-mono font-bold text-[var(--primary)] text-sm sm:text-base tracking-[1.44px] sm:tracking-[1.92px]! leading-[1.45] hover:text-[var(--primary-dark)] transition-colors">
|
||||
LEARN MORE
|
||||
<ArrowRightIcon className="w-3.5 h-3.5 sm:w-4 sm:h-4 group-hover:translate-x-1 transition-transform" />
|
||||
</button>
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Start building button - Mobile only, appears last */}
|
||||
<div className="lg:hidden mt-8">
|
||||
<Button asChild className="w-full">
|
||||
<Link href="https://docs.planoai.dev/get_started/quickstart">
|
||||
Start building
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Modal */}
|
||||
<Dialog
|
||||
open={selectedUseCase !== null}
|
||||
onOpenChange={(open) => !open && setSelectedUseCase(null)}
|
||||
>
|
||||
<AnimatePresence>
|
||||
{selectedUseCase &&
|
||||
(() => {
|
||||
const IconComponent = selectedUseCase.icon;
|
||||
return (
|
||||
<DialogContent
|
||||
key={selectedUseCase.id}
|
||||
className="max-w-[90rem]! p-0 overflow-hidden"
|
||||
showCloseButton={false}
|
||||
>
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0.98, y: 8 }}
|
||||
animate={{ opacity: 1, scale: 1, y: 0 }}
|
||||
exit={{ opacity: 0, scale: 0.98, y: 8 }}
|
||||
transition={{ duration: 0.25, ease: [0.16, 1, 0.3, 1] }}
|
||||
className="relative"
|
||||
>
|
||||
{/* Gradient Background */}
|
||||
<div
|
||||
className={`absolute inset-0 bg-gradient-to-br ${selectedUseCase.gradient} opacity-50`}
|
||||
/>
|
||||
|
||||
{/* Decorative Border */}
|
||||
<div className="absolute inset-0 border-2 border-[rgba(171,178,250,0.3)] rounded-lg pointer-events-none" />
|
||||
|
||||
{/* Custom Close Button */}
|
||||
<DialogClose className="absolute top-4 right-4 z-50 rounded-xs opacity-70 hover:opacity-100 transition-opacity focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-[rgba(171,178,250,0.5)] bg-white/80 backdrop-blur-sm p-2 hover:bg-white/90">
|
||||
<XIcon className="w-5 h-5 text-[#2a3178]" />
|
||||
<span className="sr-only">Close</span>
|
||||
</DialogClose>
|
||||
|
||||
{/* Content Container */}
|
||||
<div className="relative z-10 p-5 sm:p-8 md:p-10 lg:p-14">
|
||||
{/* Header Section with Icon */}
|
||||
<DialogHeader className="mb-4">
|
||||
<div className="flex flex-col sm:flex-row sm:items-start gap-4 sm:gap-8 mb-8">
|
||||
{/* Icon Container - hidden on mobile */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0.95 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
transition={{
|
||||
duration: 0.3,
|
||||
ease: [0.16, 1, 0.3, 1],
|
||||
delay: 0.1,
|
||||
}}
|
||||
className="hidden sm:flex shrink-0 w-14 h-14 sm:w-16 sm:h-16 rounded-xl bg-gradient-to-br from-[rgba(119,128,217,0.2)] to-[rgba(17,28,132,0.1)] border-2 border-[rgba(171,178,250,0.4)] items-center justify-center shadow-lg backdrop-blur-sm mx-0"
|
||||
>
|
||||
<IconComponent className="w-8 h-8 text-[#2a3178]" />
|
||||
</motion.div>
|
||||
|
||||
{/* Title Section */}
|
||||
<div className="flex-1 text-left mt-4 sm:mt-0">
|
||||
<motion.p
|
||||
initial={{ opacity: 0, x: -8 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
transition={{
|
||||
duration: 0.3,
|
||||
ease: [0.16, 1, 0.3, 1],
|
||||
delay: 0.15,
|
||||
}}
|
||||
className="font-mono font-bold text-[#2a3178] text-xs tracking-[1.62px]! mb-1 uppercase"
|
||||
>
|
||||
USE CASE
|
||||
</motion.p>
|
||||
<motion.div
|
||||
initial={{ opacity: 0, x: -8 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
transition={{
|
||||
duration: 0.3,
|
||||
ease: [0.16, 1, 0.3, 1],
|
||||
delay: 0.2,
|
||||
}}
|
||||
>
|
||||
<DialogTitle className="font-sans font-medium text-2xl sm:text-3xl lg:text-4xl xl:text-4xl tracking-[-1.5px]! text-black leading-[1.1] mb-4">
|
||||
{selectedUseCase.title}
|
||||
</DialogTitle>
|
||||
<div className="inline-flex items-center px-3 py-1 rounded-full bg-[rgba(185,191,255,0.3)] border border-[rgba(171,178,250,0.4)] backdrop-blur-sm">
|
||||
<span className="font-mono font-bold text-[#2a3178] text-xs tracking-[1.44px]!">
|
||||
{selectedUseCase.category}
|
||||
</span>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</div>
|
||||
</DialogHeader>
|
||||
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 8 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{
|
||||
duration: 0.3,
|
||||
ease: [0.16, 1, 0.3, 1],
|
||||
delay: 0.3,
|
||||
}}
|
||||
className="mb-10"
|
||||
>
|
||||
<DialogDescription className="text-[#494949] text-base lg:text-base xl:text-lg leading-relaxed max-w-none mb-0">
|
||||
{selectedUseCase.fullContent}
|
||||
</DialogDescription>
|
||||
</motion.div>
|
||||
|
||||
{/* Footer with CTA - mobile friendly */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 8 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{
|
||||
duration: 0.3,
|
||||
ease: [0.16, 1, 0.3, 1],
|
||||
delay: 0.35,
|
||||
}}
|
||||
className="flex flex-col sm:flex-row items-stretch sm:items-center justify-between gap-4 pt-8 border-t border-[rgba(171,178,250,0.2)]"
|
||||
>
|
||||
{/* "Ready to get started?" is now first in column on mobile */}
|
||||
<div className="flex items-center gap-2 text-sm text-neutral-500 justify-center sm:justify-start order-0">
|
||||
<span>Ready to get started?</span>
|
||||
</div>
|
||||
<div className="flex flex-col sm:flex-row gap-3 w-full sm:w-auto order-1">
|
||||
<Button asChild className="w-full sm:w-auto">
|
||||
<Link
|
||||
href="https://docs.planoai.dev/get_started/quickstart"
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
Start building
|
||||
<ArrowRightIcon className="w-4 h-4" />
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</DialogContent>
|
||||
);
|
||||
})()}
|
||||
</AnimatePresence>
|
||||
</Dialog>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
210
apps/www/src/components/VerticalCarouselSection.tsx
Normal file
210
apps/www/src/components/VerticalCarouselSection.tsx
Normal file
|
|
@ -0,0 +1,210 @@
|
|||
"use client";
|
||||
|
||||
import React, { useState } from "react";
|
||||
import Image from "next/image";
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
import { Button } from "@katanemo/ui";
|
||||
|
||||
const verticalCarouselData = [
|
||||
{
|
||||
id: 1,
|
||||
category: "INTRODUCTION",
|
||||
title: "",
|
||||
description: [
|
||||
"Plano is a models-native data plane for AI agents - a framework-friendly, protocol-native fabric that lets you focus on what really matters: your agents' product logic.",
|
||||
"Plano takes over the plumbing work that slows teams down when handling and processing prompts, including detecting and blocking jailbreaks, routing tasks to the right model or agent for better accuracy, applying context engineering hooks, and centralizing observability across agentic interactions.",
|
||||
],
|
||||
diagram: "/IntroDiagram.svg",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
category: "OPEN SOURCE",
|
||||
title: "",
|
||||
description: [
|
||||
"No lock-in. No black boxes. Just an open, intelligent fabric for building more reliable agentic AI applications.",
|
||||
"Built by engineers with roots in the Envoy ecosystem, Plano brings production-grade reliability to agent traffic and prompt orchestration—while staying fully extensible. Shape it, extend it, and integrate it into your existing workflows without being forced into a rigid framework or a single provider.",
|
||||
],
|
||||
diagram: "/OpenSource.svg",
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
category: "BUILT ON ENVOY",
|
||||
title: "",
|
||||
description: [
|
||||
"Plano is built on Envoy and runs as a self-contained sidecar alongside your application servers. It extends Envoy's HTTP connection management, filtering, and telemetry specifically for prompt and LLM traffic—so you get production-grade routing, policy enforcement, and observability out of the box.",
|
||||
"Use Plano with any application language or framework, and connect it to any LLM provider.",
|
||||
],
|
||||
diagram: "/BuiltOnEnvoy.svg",
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
category: "PURPOSE-BUILT",
|
||||
title: "",
|
||||
description: [
|
||||
"Unlike generic API gateways, Plano is purpose-built for agent workloads, where prompts are the unit of work.",
|
||||
"Plano treats prompts as first-class traffic: it understands prompt/response flows, tool calls, model selection, and multi-agent handoffs. That means routing, policy enforcement, and observability are optimized for agent execution—not retrofitted from traditional API infrastructure—so your AI applications stay fast, reliable, and easy to evolve.",
|
||||
],
|
||||
diagram: "/PurposeBuilt.svg",
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
category: "PROGRAMMABLE ARCHITECTURE",
|
||||
title: "",
|
||||
description: [
|
||||
'As agent workloads move beyond prototypes, teams end up scattering critical logic across apps: compliance checks, context "patches," provider-specific quirks, etc. That glue code gets duplicated across agents, is hard to audit, and slows iteration because every policy or workflow change requires touching application code and redeploying.',
|
||||
"Plano keeps that logic in one place with a programmable Agent Filter Chain—hooks that can inspect, mutate, or terminate prompt traffic early, turning common steps (policy enforcement, jailbreak checks, context engineering, tool gating, routing hints) into reusable building blocks.",
|
||||
],
|
||||
diagram: "/PromptRouting.svg",
|
||||
},
|
||||
];
|
||||
|
||||
export function VerticalCarouselSection() {
|
||||
const [activeSlide, setActiveSlide] = useState(0);
|
||||
|
||||
const handleSlideClick = (index: number) => {
|
||||
setActiveSlide(index);
|
||||
};
|
||||
|
||||
return (
|
||||
<section className="relative bg-[#1a1a1a] text-white pt-20 pb-0 lg:pb-4 px-4 sm:px-6 lg:px-[102px] h-auto sm:h-[650px]">
|
||||
<div className="max-w-324 mx-auto">
|
||||
{/* Main Heading */}
|
||||
<h2 className="font-sans font-normal text-2xl sm:text-3xl lg:text-4xl tracking-[-2px] sm:tracking-[-2.88px]! text-white leading-[1.03] mb-8 sm:mb-12 lg:mb-12 max-w-4xl">
|
||||
Under the hood
|
||||
</h2>
|
||||
|
||||
{/* Mobile: Horizontal Scroller Navigation */}
|
||||
<div className="lg:hidden mb-8 -mx-4 sm:mx-0 px-4 sm:px-0">
|
||||
<div
|
||||
className="relative overflow-x-auto pb-2"
|
||||
style={{
|
||||
scrollbarWidth: "none",
|
||||
msOverflowStyle: "none",
|
||||
}}
|
||||
>
|
||||
<style jsx>{`
|
||||
.hide-scrollbar::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
`}</style>
|
||||
<div className="flex gap-4 min-w-max hide-scrollbar">
|
||||
{verticalCarouselData.map((item, index) => (
|
||||
<button
|
||||
key={item.id}
|
||||
onClick={() => handleSlideClick(index)}
|
||||
className={`relative px-4 py-2 rounded transition-all duration-300 whitespace-nowrap ${
|
||||
index === activeSlide
|
||||
? "bg-[#6363d2]/90 text-[#f9faff]"
|
||||
: "bg-[#6363d2]/10 text-[rgba(182,188,255,0.71)] hover:bg-[#6363d2]/15"
|
||||
}`}
|
||||
>
|
||||
<span className="font-mono font-bold text-sm tracking-[1.44px]!">
|
||||
{item.category}
|
||||
</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Desktop: Vertical Carousel Layout */}
|
||||
<div className="flex flex-col lg:flex-row lg:items-start">
|
||||
{/* Left Sidebar Navigation - Desktop Only */}
|
||||
<div className="hidden lg:block lg:w-72 shrink-0 lg:pt-0">
|
||||
<div className="relative space-y-6">
|
||||
<motion.div
|
||||
className="absolute left-0 top-0 w-2 h-4 bg-[#6363d2] z-10 rounded-xs"
|
||||
animate={{
|
||||
y: activeSlide * 52 + 6, // Each item is ~28px text + 24px gap = 52px, +10px to center smaller rectangle
|
||||
}}
|
||||
transition={{
|
||||
type: "spring",
|
||||
stiffness: 300,
|
||||
damping: 30,
|
||||
duration: 0.6,
|
||||
}}
|
||||
/>
|
||||
|
||||
{verticalCarouselData.map((item, index) => (
|
||||
<div
|
||||
key={item.id}
|
||||
onClick={() => handleSlideClick(index)}
|
||||
className="cursor-pointer relative pl-6 transition-all duration-300"
|
||||
>
|
||||
{/* Category Text */}
|
||||
<span
|
||||
className={`font-mono font-bold text-lg tracking-[1.69px]! transition-colors duration-300 ${
|
||||
index === activeSlide
|
||||
? "text-[#acb3fe]"
|
||||
: "text-[rgba(172,179,254,0.71)]"
|
||||
}`}
|
||||
>
|
||||
{item.category}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right Content Area - Fixed height to prevent layout shift */}
|
||||
<div className="flex-1 h-[600px] sm:h-[650px] lg:h-[600px] relative lg:-ml-8">
|
||||
<AnimatePresence mode="wait">
|
||||
<motion.div
|
||||
key={activeSlide}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -20 }}
|
||||
transition={{ duration: 0.4, ease: "easeInOut" }}
|
||||
className="w-full h-full"
|
||||
>
|
||||
<div className="flex flex-col lg:flex-row gap-6 sm:gap-8 lg:gap-12 items-start h-full">
|
||||
{/* Diagram - Above on mobile, Right Side on desktop */}
|
||||
<div className="w-full lg:flex-1 flex items-center justify-center lg:justify-start order-first lg:order-last shrink-0">
|
||||
<div className="relative w-full max-w-full sm:max-w-md lg:max-w-[600px] aspect-4/3">
|
||||
<Image
|
||||
src={verticalCarouselData[activeSlide].diagram}
|
||||
alt={verticalCarouselData[activeSlide].category}
|
||||
fill
|
||||
className="object-contain object-top"
|
||||
priority
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Text Content */}
|
||||
<div className="flex-1 max-w-2xl order-last lg:order-first flex flex-col justify-start">
|
||||
{/* Title
|
||||
<h3 className="font-sans font-medium text-primary text-xl sm:text-2xl lg:text-[34px] tracking-[-1px]! leading-[1.03] mb-4 sm:mb-6">
|
||||
{verticalCarouselData[activeSlide].title}
|
||||
</h3> */}
|
||||
|
||||
{/* Description */}
|
||||
<div className="text-white text-sm sm:text-base lg:text-lg max-w-full lg:max-w-md -mt-0.5">
|
||||
{verticalCarouselData[activeSlide].description.map(
|
||||
(paragraph, index) => (
|
||||
<p
|
||||
key={index}
|
||||
className={
|
||||
index <
|
||||
verticalCarouselData[activeSlide].description
|
||||
.length -
|
||||
1
|
||||
? "mb-4"
|
||||
: "mb-0"
|
||||
}
|
||||
>
|
||||
{paragraph}
|
||||
</p>
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
67
apps/www/src/components/research/ResearchBenchmarks.tsx
Normal file
67
apps/www/src/components/research/ResearchBenchmarks.tsx
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
import React from "react";
|
||||
import Image from "next/image";
|
||||
|
||||
export function ResearchBenchmarks() {
|
||||
return (
|
||||
<section className="relative py-12 sm:py-16 lg:py-20 px-4 sm:px-6 lg:px-[102px] bg-[#1a1a1a] border-b-2 border-white/10">
|
||||
<div className="max-w-[81rem] mx-auto">
|
||||
{/* Section Header */}
|
||||
<div className="mb-8 sm:mb-12 lg:mb-6">
|
||||
{/* BENCHMARKS Label */}
|
||||
<div className="mb-4 sm:mb-2">
|
||||
<div className="font-mono font-bold text-[#9797ea] text-sm sm:text-base lg:text-xl tracking-[1.44px] sm:tracking-[1.92px]! leading-[1.502]">
|
||||
BENCHMARKS
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Title */}
|
||||
<h2 className="text-4xl sm:text-4xl md:text-5xl lg:text-4xl font-medium leading-tight tracking-[-0.06em]! text-white">
|
||||
<span className="font-sans">
|
||||
Production excellence, outperforming frontier LLMs
|
||||
</span>
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
{/* Benchmarks Image */}
|
||||
{/* Mobile: Full-width scrollable container that extends to viewport edges */}
|
||||
<div
|
||||
className="mt-5 lg:hidden relative left-1/2 right-1/2 -ml-[50vw] -mr-[50vw] w-screen overflow-x-auto overflow-y-visible"
|
||||
style={{
|
||||
scrollbarWidth: "none",
|
||||
msOverflowStyle: "none",
|
||||
WebkitOverflowScrolling: "touch",
|
||||
}}
|
||||
>
|
||||
<style jsx>{`
|
||||
.benchmarks-scroll-container::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
`}</style>
|
||||
<div className="benchmarks-scroll-container inline-block pl-4 sm:pl-6">
|
||||
<Image
|
||||
src="/Benchmarks.svg"
|
||||
alt="Benchmarks"
|
||||
width={1200}
|
||||
height={600}
|
||||
className="h-auto"
|
||||
style={{ width: "1200px", maxWidth: "none", display: "block" }}
|
||||
priority
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Desktop: Normal display */}
|
||||
<div className="hidden lg:block w-full">
|
||||
<Image
|
||||
src="/Benchmarks.svg"
|
||||
alt="Benchmarks"
|
||||
width={1200}
|
||||
height={600}
|
||||
className="w-full h-auto"
|
||||
priority
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
46
apps/www/src/components/research/ResearchCTA.tsx
Normal file
46
apps/www/src/components/research/ResearchCTA.tsx
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
import React from "react";
|
||||
import { Button } from "@katanemo/ui";
|
||||
import Link from "next/link";
|
||||
|
||||
export function ResearchCTA() {
|
||||
return (
|
||||
<section className="relative pt-16 sm:pt-20 lg:pt-24 pb-12 sm:pb-16 lg:pb-20 px-4 sm:px-6 lg:px-[102px] bg-[#1a1a1a]">
|
||||
<div className="max-w-[81rem] mx-auto relative z-10">
|
||||
<div className="max-w-4xl">
|
||||
{/* Main Heading */}
|
||||
<h1 className="text-4xl sm:text-4xl md:text-5xl lg:text-5xl font-medium leading-tight tracking-[-0.06em]! text-white -ml-1 mb-3 mt-4">
|
||||
<span className="font-sans">
|
||||
Meet Plano-Orchestrator. Our latest models.
|
||||
</span>
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
{/* Description with CTA Buttons */}
|
||||
<div className="max-w-5xl">
|
||||
<p className="leading-relaxed sm:text-lg md:text-lg lg:text-[18px] font-sans font-normal text-white/90 mb-6">
|
||||
Plano-Orchestrator is a family of state-of-the-art routing and
|
||||
orchestration models that decides which agent(s) or LLM(s) should
|
||||
handle each request, and in what sequence. Built for multi-agent
|
||||
orchestration systems, Plano-Orchestrator excels at analyzing user
|
||||
intent and conversation context to make precise routing and
|
||||
orchestration decisions.
|
||||
</p>
|
||||
|
||||
{/* CTA Buttons */}
|
||||
<div className="flex flex-col sm:flex-row items-stretch sm:items-start gap-3 sm:gap-4">
|
||||
<Button asChild className="w-full sm:w-auto">
|
||||
<Link href="https://huggingface.co/katanemo">
|
||||
Download Plano models
|
||||
</Link>
|
||||
</Button>
|
||||
<Button variant="secondary" asChild className="w-full sm:w-auto">
|
||||
<Link href="https://docs.planoai.dev">
|
||||
Get Started with Plano
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
157
apps/www/src/components/research/ResearchCapabilities.tsx
Normal file
157
apps/www/src/components/research/ResearchCapabilities.tsx
Normal file
|
|
@ -0,0 +1,157 @@
|
|||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import { motion } from "framer-motion";
|
||||
import { MessagesSquare, GitFork, Route, RefreshCw } from "lucide-react";
|
||||
|
||||
interface Capability {
|
||||
id: number;
|
||||
title: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
const capabilitiesData: Capability[] = [
|
||||
{
|
||||
id: 1,
|
||||
title: "Multi-turn Understanding",
|
||||
description:
|
||||
"Makes routing decisions based on full conversation history, maintaining contextual awareness across extended dialogues with evolving user needs.",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: "Multi-Intent Detection",
|
||||
description:
|
||||
"Identifies when a single user message requires multiple agents simultaneously, enabling parallel/sequential routing to fulfill complex requests",
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: "Content-Dependency Routing",
|
||||
description:
|
||||
"Correctly interprets ambiguous or referential messages by leveraging prior conversation context for accurate routing decisions.",
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
title: "Conversational-Flow Handling",
|
||||
description:
|
||||
"Understands diverse interaction patterns including follow-ups, clarifications, confirmations, and corrections within ongoing conversations.",
|
||||
},
|
||||
];
|
||||
|
||||
export function ResearchCapabilities() {
|
||||
return (
|
||||
<section className="relative py-12 sm:py-16 lg:py-20 px-4 sm:px-6 lg:px-[102px] bg-[#1a1a1a]">
|
||||
<div className="max-w-[81rem] mx-auto">
|
||||
{/* Section Header */}
|
||||
<div className="mb-8 sm:mb-12 lg:mb-10">
|
||||
{/* PLANO-4B CAPABILITIES Label */}
|
||||
<div className="mb-2 sm:mb-1">
|
||||
<div className="font-mono font-bold text-[#9797ea] text-sm sm:text-base lg:text-xl tracking-[1.44px] sm:tracking-[1.92px]! leading-[1.502]">
|
||||
PLANO-ORCHESTRATOR CAPABILITIES
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Title */}
|
||||
<h2 className="text-4xl sm:text-4xl md:text-5xl lg:text-4xl font-medium leading-tight tracking-[-0.06em]! text-white mb-4">
|
||||
<span className="font-sans">
|
||||
Accurately route with confidence with no compromise
|
||||
</span>
|
||||
</h2>
|
||||
|
||||
<p className="text-white/90 w-full sm:w-[75%] text-sm sm:text-base leading-relaxed">
|
||||
Designed for real-world deployments, it delivers strong performance
|
||||
across general conversations, coding tasks, and long-context
|
||||
multi-turn conversations, while remaining efficient enough for
|
||||
low-latency production environments.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Mobile: Icon card above title/description, stacked vertically */}
|
||||
<div className="lg:hidden grid grid-cols-1 gap-8">
|
||||
{capabilitiesData.map((capability) => {
|
||||
// Map each capability to its icon
|
||||
const iconMap: Record<
|
||||
number,
|
||||
React.ComponentType<{ className?: string }>
|
||||
> = {
|
||||
1: MessagesSquare, // Multi-turn Understanding
|
||||
2: GitFork, // Multi-Intent Detection
|
||||
3: Route, // Content-Dependency Routing
|
||||
4: RefreshCw, // Conversational-Flow Handling
|
||||
};
|
||||
|
||||
const Icon = iconMap[capability.id];
|
||||
|
||||
return (
|
||||
<div key={capability.id} className="flex flex-col">
|
||||
{/* Icon Card */}
|
||||
<motion.div
|
||||
whileHover={{ y: -4 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
className="bg-gradient-to-b from-[rgba(177,184,255,0.16)] to-[rgba(17,28,132,0.035)] border-2 border-[rgba(171,178,250,0.27)] rounded-md p-6 h-40 flex items-center justify-center mb-4"
|
||||
>
|
||||
{Icon && <Icon className="w-24 h-24 text-[#9797ea]" />}
|
||||
</motion.div>
|
||||
|
||||
{/* Title */}
|
||||
<h3 className="font-sans font-medium text-white text-xl tracking-[-1.2px]! leading-[1.102] mb-3">
|
||||
{capability.title}
|
||||
</h3>
|
||||
|
||||
{/* Description */}
|
||||
<p className="text-white/90 text-base leading-relaxed">
|
||||
{capability.description}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Desktop: Icon cards separate from titles/descriptions */}
|
||||
<div className="hidden lg:grid lg:grid-cols-4 gap-6 mb-6">
|
||||
{capabilitiesData.map((capability) => {
|
||||
// Map each capability to its icon
|
||||
const iconMap: Record<
|
||||
number,
|
||||
React.ComponentType<{ className?: string }>
|
||||
> = {
|
||||
1: MessagesSquare, // Multi-turn Understanding
|
||||
2: GitFork, // Multi-Intent Detection
|
||||
3: Route, // Content-Dependency Routing
|
||||
4: RefreshCw, // Conversational-Flow Handling
|
||||
};
|
||||
|
||||
const Icon = iconMap[capability.id];
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
key={capability.id}
|
||||
whileHover={{ y: -4 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
className="bg-gradient-to-b from-[rgba(177,184,255,0.16)] to-[rgba(17,28,132,0.035)] border-2 border-[rgba(171,178,250,0.27)] rounded-md p-6 h-52 flex items-center justify-center"
|
||||
>
|
||||
{Icon && <Icon className="w-24 h-24 text-[#9797ea]" />}
|
||||
</motion.div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Desktop: Titles and Descriptions Below Boxes */}
|
||||
<div className="hidden lg:grid lg:grid-cols-4 gap-6">
|
||||
{capabilitiesData.map((capability) => (
|
||||
<div key={capability.id}>
|
||||
{/* Title */}
|
||||
<h3 className="font-sans font-medium text-white text-2xl tracking-[-1.2px]! leading-[1.102] mb-4">
|
||||
{capability.title}
|
||||
</h3>
|
||||
|
||||
{/* Description */}
|
||||
<p className="text-white/90 text-base leading-relaxed">
|
||||
{capability.description}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
111
apps/www/src/components/research/ResearchFamily.tsx
Normal file
111
apps/www/src/components/research/ResearchFamily.tsx
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import Image from "next/image";
|
||||
import { Check } from "lucide-react";
|
||||
import { motion } from "framer-motion";
|
||||
|
||||
interface ModelFeature {
|
||||
text: string;
|
||||
}
|
||||
|
||||
interface Model {
|
||||
id: number;
|
||||
name: string;
|
||||
logo: string;
|
||||
features: ModelFeature[];
|
||||
}
|
||||
|
||||
const modelsData: Model[] = [
|
||||
{
|
||||
id: 1,
|
||||
name: "Plano-4B",
|
||||
logo: "/Plano4B-Logo.svg",
|
||||
features: [
|
||||
{ text: "Optimized for production routing with sub-100ms latency" },
|
||||
{ text: "84-87% accuracy on long-context scenarios" },
|
||||
{ text: "Cost-effective model selection at scale" },
|
||||
{ text: "Seamless agent orchestration capabilities" },
|
||||
{ text: "Frontier-level performance at fraction of cost" },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: "Plano-30B-A3B",
|
||||
logo: "/Plano30B-Logo.svg",
|
||||
features: [
|
||||
{ text: "Advanced routing intelligence for complex workflows" },
|
||||
{ text: "Enhanced context understanding and preservation" },
|
||||
{ text: "Superior accuracy for multi-agent coordination" },
|
||||
{ text: "Enterprise-grade performance and reliability" },
|
||||
{ text: "Scalable architecture for high-throughput systems" },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export function ResearchFamily() {
|
||||
return (
|
||||
<section className="relative py-16 sm:py-20 lg:py-24 px-4 sm:px-6 lg:px-[102px] bg-white">
|
||||
<div className="max-w-[81rem] mx-auto">
|
||||
{/* Section Header */}
|
||||
<div className="mb-8 sm:mb-12 lg:mb-10">
|
||||
{/* PLANO FAMILY Label */}
|
||||
<div className="mb-4 sm:mb-2">
|
||||
<div className="font-mono font-bold text-[#2A3178] text-sm sm:text-base lg:text-xl tracking-[1.44px] sm:tracking-[1.92px]! leading-[1.502]">
|
||||
PLANO FAMILY
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Title */}
|
||||
<h2 className="text-4xl sm:text-4xl md:text-5xl lg:text-4xl font-medium leading-tight tracking-[-0.06em]! text-black -ml-1">
|
||||
<span className="font-sans">Plano Models</span>
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
{/* 2 Card Grid - Side by Side */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 mb-6">
|
||||
{modelsData.map((model) => (
|
||||
<motion.div
|
||||
key={model.id}
|
||||
whileHover={{ y: -4 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
className="bg-gradient-to-b from-[rgba(177,184,255,0.16)] to-[rgba(17,28,132,0.035)] border-2 border-[rgba(171,178,250,0.27)] rounded-md p-6 sm:p-6 lg:p-6 h-72"
|
||||
>
|
||||
{/* Empty box - content is below */}
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Titles and Descriptions Below Boxes */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
{modelsData.map((model) => (
|
||||
<div key={model.id}>
|
||||
{/* Logo */}
|
||||
<div className="mb-6">
|
||||
<Image
|
||||
src={model.logo}
|
||||
alt={model.name}
|
||||
width={200}
|
||||
height={60}
|
||||
className="h-12 w-auto"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Features List */}
|
||||
<div>
|
||||
{model.features.map((feature, index) => (
|
||||
<div key={index} className="flex items-start gap-3 mb-4">
|
||||
<Check className="w-5 h-5 text-[var(--primary)] flex-shrink-0 mt-0.5" />
|
||||
<p className="font-mono text-black text-sm sm:text-base tracking-[-0.8px] sm:tracking-[-1.2px]! leading-relaxed">
|
||||
{feature.text}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
49
apps/www/src/components/research/ResearchGrid.tsx
Normal file
49
apps/www/src/components/research/ResearchGrid.tsx
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
import React from "react";
|
||||
|
||||
interface ResearchItem {
|
||||
title: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
const researchItems: ResearchItem[] = [
|
||||
{
|
||||
title: "Model Routing",
|
||||
description:
|
||||
"Great agent UX starts with using the best model for a task — fast and cheap when it can be, smarter when it needs to be — and our routing research gives developers the controls to do exactly that. Our policy-based router captures your evals and preferences, while our performance-based router learns from real traffic over time, so you can evolve model choices without retraining.",
|
||||
},
|
||||
{
|
||||
title: "Governance & Learning",
|
||||
description:
|
||||
"Building an agent is easy — knowing what it does in production and how to improve it is very hard. Our research focuses on making agent behavior observable and governable: studying how agents respond to real and adversarial traffic, policy changes, and turning signals into learning loops that make agents safer and more effective over time.",
|
||||
},
|
||||
{
|
||||
title: "Agentic Performance",
|
||||
description:
|
||||
"Better system performance comes from directing traffic to the right agents for each task or workflow. We build compact orchestration models that manage traffic between agents — ensuring clean handoffs, preserved context, and reliable multi-agent collaboration across distributed systems.",
|
||||
},
|
||||
];
|
||||
|
||||
export function ResearchGrid() {
|
||||
return (
|
||||
<section className="relative py-12 sm:py-16 lg:py-20 px-4 sm:px-6 lg:px-[102px] bg-white">
|
||||
<div className="max-w-[81rem] mx-auto">
|
||||
{/* 3 Column Grid */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 sm:gap-8 lg:gap-12">
|
||||
{researchItems.map((item, index) => (
|
||||
<div key={index} className="flex flex-col">
|
||||
{/* Title */}
|
||||
<h3 className="font-sans font-medium text-2xl sm:text-3xl lg:text-3xl tracking-[-1.5px] sm:tracking-[-2px]! text-black leading-[1.1] mb-2 sm:mb-4">
|
||||
{item.title}
|
||||
</h3>
|
||||
|
||||
{/* Description */}
|
||||
<p className="text-black text-sm sm:text-base lg:text-lg leading-relaxed">
|
||||
{item.description}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
58
apps/www/src/components/research/ResearchHero.tsx
Normal file
58
apps/www/src/components/research/ResearchHero.tsx
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
import React from "react";
|
||||
import { NetworkAnimation } from "../NetworkAnimation";
|
||||
import { Button } from "@katanemo/ui";
|
||||
import Link from "next/link";
|
||||
|
||||
export function ResearchHero() {
|
||||
return (
|
||||
<section className="relative pt-8 sm:pt-12 lg:pt-1 pb-12 sm:pb-16 lg:pb-20 px-4 sm:px-6 lg:px-[102px] overflow-hidden">
|
||||
<div className="max-w-[81rem] mx-auto relative">
|
||||
<div className="hidden lg:block absolute inset-0 pointer-events-none">
|
||||
<NetworkAnimation className="!w-[500px] !h-[500px] xl:!w-[600px] xl:!h-[600px] 2xl:!w-[570px] 2xl:!h-[540px] !top-[15%]" />
|
||||
</div>
|
||||
<div className="lg:hidden absolute inset-0 pointer-events-none">
|
||||
<NetworkAnimation className="!w-[300px] !h-[300px] left-77! -top-2! opacity-90! " />
|
||||
</div>
|
||||
<div className="max-w-3xl relative z-10">
|
||||
{/* Badge */}
|
||||
<div className="mb-4 sm:mb-6">
|
||||
<div className="inline-flex flex-wrap items-center gap-1.5 sm:gap-2 px-3 sm:px-4 py-1 rounded-full bg-[rgba(185,191,255,0.4)] border border-[var(--secondary)] shadow backdrop-blur">
|
||||
<span className="text-xs sm:text-sm font-medium text-black/65">
|
||||
New!
|
||||
</span>
|
||||
<span className="text-xs sm:text-sm font-medium text-black hidden sm:inline">
|
||||
—
|
||||
</span>
|
||||
<span className="text-xs sm:text-sm font-[600] tracking-[-0.6px]! text-black leading-tight">
|
||||
<span className="">Plano Orchestrator models released</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Main Heading */}
|
||||
<h1 className="text-4xl sm:text-4xl md:text-5xl lg:text-7xl font-medium leading-tight tracking-tighter text-black -ml-1 mb-3 mt-4">
|
||||
<span className="font-sans">Research</span>
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
{/* Description */}
|
||||
<div className="max-w-70 sm:max-w-2xl relative z-10">
|
||||
<p className="text-base sm:text-lg md:text-xl lg:text-[22px] font-sans font-normal tracking-[-1.0px] sm:tracking-[-1.22px]! text-black">
|
||||
Our applied research focuses on how to deliver agents safely,
|
||||
efficiently, and with improved real-world performance — critical for
|
||||
any AI application, but work that sits outside of any agent's core
|
||||
product logic.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col sm:flex-row items-stretch sm:items-start gap-3 sm:gap-4 mt-6 sm:mt-8 relative z-10">
|
||||
<Button asChild className="w-full sm:w-auto">
|
||||
<Link href="https://huggingface.co/katanemo">
|
||||
Available on Hugging Face
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
50
apps/www/src/components/research/ResearchTimeline.tsx
Normal file
50
apps/www/src/components/research/ResearchTimeline.tsx
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
import React from "react";
|
||||
import Image from "next/image";
|
||||
|
||||
export function ResearchTimeline() {
|
||||
return (
|
||||
<section className="relative py-12 sm:py-16 lg:py-16 px-4 sm:px-6 lg:px-[102px] bg-white border-b border-gray-200">
|
||||
<div className="max-w-[81rem] mx-auto">
|
||||
{/* Timeline Image */}
|
||||
{/* Mobile: Full-width scrollable container that extends to viewport edges */}
|
||||
<div
|
||||
className="mt-5 lg:hidden relative left-1/2 right-1/2 -ml-[50vw] -mr-[50vw] w-screen overflow-x-auto overflow-y-visible"
|
||||
style={{
|
||||
scrollbarWidth: "none",
|
||||
msOverflowStyle: "none",
|
||||
WebkitOverflowScrolling: "touch",
|
||||
}}
|
||||
>
|
||||
<style jsx>{`
|
||||
.timeline-scroll-container::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
`}</style>
|
||||
<div className="timeline-scroll-container inline-block pl-4 sm:pl-6">
|
||||
<Image
|
||||
src="/Timeline.svg"
|
||||
alt="Research Timeline"
|
||||
width={1200}
|
||||
height={600}
|
||||
className="h-auto"
|
||||
style={{ width: "1200px", maxWidth: "none", display: "block" }}
|
||||
priority
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Desktop: Normal display */}
|
||||
<div className="hidden lg:block w-full">
|
||||
<Image
|
||||
src="/Timeline.svg"
|
||||
alt="Research Timeline"
|
||||
width={1200}
|
||||
height={600}
|
||||
className="w-full h-auto"
|
||||
priority
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
7
apps/www/src/components/research/index.ts
Normal file
7
apps/www/src/components/research/index.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
export { ResearchHero } from "./ResearchHero";
|
||||
export { ResearchGrid } from "./ResearchGrid";
|
||||
export { ResearchTimeline } from "./ResearchTimeline";
|
||||
export { ResearchCTA } from "./ResearchCTA";
|
||||
export { ResearchCapabilities } from "./ResearchCapabilities";
|
||||
export { ResearchBenchmarks } from "./ResearchBenchmarks";
|
||||
export { ResearchFamily } from "./ResearchFamily";
|
||||
59
apps/www/src/data/diagramTemplates.ts
Normal file
59
apps/www/src/data/diagramTemplates.ts
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
import { createFlowDiagram, FlowStep } from "@/utils/asciiBuilder";
|
||||
|
||||
/**
|
||||
* Easy-to-use diagram templates that automatically handle spacing
|
||||
* Perfect for non-coders who just want to define their flow
|
||||
*/
|
||||
|
||||
// Example: Simple 3-step process
|
||||
export const createSimpleProcess = (steps: string[]) => {
|
||||
return createFlowDiagram({
|
||||
title: "Process Flow",
|
||||
width: 60,
|
||||
steps: steps.map((label) => ({
|
||||
label,
|
||||
type: "regular" as const,
|
||||
shadow: true,
|
||||
})),
|
||||
});
|
||||
};
|
||||
|
||||
// Example: Create a nested container diagram
|
||||
export const createNestedDiagram = (
|
||||
title: string,
|
||||
innerContent: FlowStep[],
|
||||
width: number = 70,
|
||||
) => {
|
||||
return createFlowDiagram({
|
||||
title,
|
||||
width,
|
||||
steps: innerContent,
|
||||
});
|
||||
};
|
||||
|
||||
// Pre-built templates
|
||||
export const templates = {
|
||||
simpleFlow: createSimpleProcess(["Start", "Process", "End"]),
|
||||
|
||||
apiFlow: createFlowDiagram({
|
||||
title: "API Request Flow",
|
||||
width: 65,
|
||||
steps: [
|
||||
{ label: "Client Request", type: "regular", shadow: true },
|
||||
{ label: "API Gateway", type: "container", shadow: true },
|
||||
{ label: "Process", type: "inner", shadow: true },
|
||||
{ label: "Response", type: "regular", shadow: true },
|
||||
],
|
||||
}),
|
||||
|
||||
dataPipeline: createFlowDiagram({
|
||||
title: "Data Pipeline",
|
||||
width: 70,
|
||||
steps: [
|
||||
{ label: "Input Data", type: "regular", shadow: true },
|
||||
{ label: "Transform", type: "inner", shadow: true },
|
||||
{ label: "Validate", type: "regular", shadow: true },
|
||||
{ label: "Store", type: "regular", shadow: true },
|
||||
],
|
||||
}),
|
||||
};
|
||||
82
apps/www/src/data/diagrams-programmatic.ts
Normal file
82
apps/www/src/data/diagrams-programmatic.ts
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
/**
|
||||
* Programmatic ASCII Diagram Builder
|
||||
*
|
||||
* For non-coders: Define your diagram structure with simple objects,
|
||||
* and the system will automatically generate the ASCII art.
|
||||
*/
|
||||
|
||||
interface DiagramStep {
|
||||
id: string;
|
||||
label: string;
|
||||
type: "input" | "inner" | "regular";
|
||||
position: { x: number; y: number };
|
||||
}
|
||||
|
||||
interface DiagramFlow {
|
||||
from: string;
|
||||
to: string;
|
||||
arrow: "right" | "down" | "left" | "up";
|
||||
label?: string;
|
||||
}
|
||||
|
||||
interface DiagramConfig {
|
||||
title: string;
|
||||
steps: DiagramStep[];
|
||||
flows: DiagramFlow[];
|
||||
}
|
||||
|
||||
// Example: Define diagram using simple objects
|
||||
export const myFlow: DiagramConfig = {
|
||||
title: "User Registration Flow",
|
||||
steps: [
|
||||
{ id: "start", label: "User", type: "input", position: { x: 0, y: 0 } },
|
||||
{
|
||||
id: "step1",
|
||||
label: "Validate Email",
|
||||
type: "inner",
|
||||
position: { x: 2, y: 0 },
|
||||
},
|
||||
{
|
||||
id: "step2",
|
||||
label: "Create Account",
|
||||
type: "regular",
|
||||
position: { x: 2, y: 1 },
|
||||
},
|
||||
{
|
||||
id: "step3",
|
||||
label: "Send Welcome",
|
||||
type: "regular",
|
||||
position: { x: 2, y: 2 },
|
||||
},
|
||||
],
|
||||
flows: [
|
||||
{ from: "start", to: "step1", arrow: "right" },
|
||||
{ from: "step1", to: "step2", arrow: "down" },
|
||||
{ from: "step2", to: "step3", arrow: "down" },
|
||||
],
|
||||
};
|
||||
|
||||
/**
|
||||
* Convert diagram config to ASCII string
|
||||
*
|
||||
* Usage:
|
||||
* import { buildDiagram } from './ascii-builder';
|
||||
* const ascii = buildDiagram(myFlow);
|
||||
*/
|
||||
export const buildDiagram = (config: DiagramConfig): string => {
|
||||
// This function would programmatically build the ASCII
|
||||
// For now, return a placeholder
|
||||
let result = "";
|
||||
result += `╔═ ${config.title} ══╗\n`;
|
||||
result += `║ Placeholder for programmatic generation ║\n`;
|
||||
result += `╚════════════════════════════════════╝\n`;
|
||||
|
||||
// TODO: Implement automatic ASCII generation from config
|
||||
// This would:
|
||||
// 1. Layout boxes based on positions
|
||||
// 2. Add arrows based on flows
|
||||
// 3. Add shadows automatically
|
||||
// 4. Handle different box types
|
||||
|
||||
return result;
|
||||
};
|
||||
84
apps/www/src/data/diagrams.ts
Normal file
84
apps/www/src/data/diagrams.ts
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
export const diagrams = {
|
||||
intentDetection: ` ╔═ Intent Detection ═════════════════════════════╗
|
||||
╔═══════════╗ ║ ║░
|
||||
║ ║░ ║ ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ ║░
|
||||
║ agent ║░─────────║─▶┃ Cognition ┃ ║░
|
||||
║ ║░ ║ ┃ ┃ ║░
|
||||
╚═══════════╝░ ║ ┃ Action ┃ ║░
|
||||
░░░░░░░░░░░░░ ║ ┗━━━━━━━━━━━━━━━┬━━━━━━━━━━━━━━━━━━━━━━━┛ ║░
|
||||
║ │ ║░
|
||||
║ ▼ ║░
|
||||
║ ┌───────────────────────┐ ║░
|
||||
║ │ Confirm action? │ ║░
|
||||
║ └───────────┬───────────┘ ║░
|
||||
║ │ ║░
|
||||
║ ▼ ║░
|
||||
║ ┌───────────────────────┐ ║░
|
||||
║ │ Execute Action │ ║░
|
||||
║ └───────────────────────┘ ║░
|
||||
║ ║░
|
||||
╚════════════════════════════════════════════════╝░
|
||||
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░`,
|
||||
|
||||
dataFlow: ` ╔═ Data Pipeline ═════════════════════════════╗
|
||||
╔═══════════╗ ║ ║░
|
||||
║ ║░ ║ ┏━━━━━━━━━━━┓ ┏━━━━━━━━━━━━━━━━━┓ ║░
|
||||
║ Input ║░────────────║─▶┃ Process ┃────▶┃ Transform ┃ ║░
|
||||
║ ║░ ║ ┗━━━━━━━━━━━┛ ┗━━━━━━━┬━━━━━━━━━┛ ║░
|
||||
╚═══════════╝░ ║ │ ║░
|
||||
░░░░░░░░░░░░░ ║ ▼ ║░
|
||||
║ ┌──────────────────┐ ║░
|
||||
║ │ Store (DB) │ ║░
|
||||
║ └──────────────────┘ ║░
|
||||
║ ║░
|
||||
╚═════════════════════════════════════════════╝░
|
||||
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░`,
|
||||
|
||||
microservices: ` ╔═ Microservices Architecture ════════════════════╗
|
||||
╔═══════════╗ ║ ║░
|
||||
║ ║░ ║ ┌─────────────────────────────────────┐ ║░
|
||||
║ Client ║░────API─────║─▶│ API Gateway │ ║░
|
||||
║ ║░ ║ └──────────┬──────────────┬───────────┘ ║░
|
||||
╚═══════════╝░ ║ │ │ ║░
|
||||
░░░░░░░░░░░░░ ║ ▼ ▼ ║░
|
||||
║ ┏━━━━━━━━━━━━━┓ ┏━━━━━━━━━━━━━┓ ║░
|
||||
║ ┃ Service A ┃ ┃ Service B ┃ ║░
|
||||
║ ┗━━━━━━┬━━━━━━┛ ┗━━━━━━┬━━━━━━┛ ║░
|
||||
║ │ │ ║░
|
||||
║ └────────┬────────┘ ║░
|
||||
║ ▼ ║░
|
||||
║ ┌─────────────────┐ ║░
|
||||
║ │ Database │ ║░
|
||||
║ └─────────────────┘ ║░
|
||||
╚══════════════════════════════════════════════╝░
|
||||
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░`,
|
||||
|
||||
simpleFlow: `╔════════════╗ ╔════════════╗ ╔════════════╗
|
||||
║ ║░ ║ ║░ ║ ║░
|
||||
║ Step 1 ║░───▶║ Step 2 ║░───▶║ Step 3 ║░
|
||||
║ ║░ ║ ║░ ║ ║░
|
||||
╚════════════╝░ ╚════════════╝░ ╚════════════╝░
|
||||
░░░░░░░░░░░░░░ ░░░░░░░░░░░░░░ ░░░░░░░░░░░░░░`,
|
||||
|
||||
infrastructureLayer: ` ╔═ plano ════════════════════════════════════════╗
|
||||
╔═══════════╗ ║ ║░
|
||||
║ ║░ ║ ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ ║░
|
||||
║ client ║░─────────║─▶┃ Safety Guardrails ┃ ║░
|
||||
║ ║░ ║ ┃ ┃ ║░
|
||||
╚═══════════╝░ ║ ┗━━━━━━━━━━━━━━━┬━━━━━━━━━━━━━━━━━━━━━━━┛ ║░
|
||||
░░░░░░░░░░░░░ ║ │ ║░
|
||||
║ ▼ ║░
|
||||
║ ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ ║░
|
||||
║ ┃ Multi-Agent Workflows ┃ ║░
|
||||
║ ┗━━━━━━━━━━━━━━━┬━━━━━━━━━━━━━━━━━━━━━━━┛ ║░
|
||||
║ │ ║░
|
||||
║ ▼ ║░
|
||||
║ ┌───────────────────────────────┐ ║░
|
||||
║ │ Unified LLM Access │ ║░
|
||||
║ └───────────────────────────────┘ ║░
|
||||
║ ║░
|
||||
╚════════════════════════════════════════════════╝░
|
||||
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░`,
|
||||
};
|
||||
|
||||
export type DiagramKey = keyof typeof diagrams;
|
||||
20
apps/www/src/lib/sanity.ts
Normal file
20
apps/www/src/lib/sanity.ts
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
import { createClient } from "@sanity/client";
|
||||
import imageUrlBuilder from "@sanity/image-url";
|
||||
import type { SanityImageSource } from "@sanity/image-url/lib/types/types";
|
||||
|
||||
const projectId = process.env.NEXT_PUBLIC_SANITY_PROJECT_ID;
|
||||
const dataset = process.env.NEXT_PUBLIC_SANITY_DATASET;
|
||||
const apiVersion = process.env.NEXT_PUBLIC_SANITY_API_VERSION;
|
||||
|
||||
export const client = createClient({
|
||||
projectId,
|
||||
dataset,
|
||||
apiVersion,
|
||||
useCdn: true, // Set to false if statically generating pages, using ISR or using the on-demand revalidation API
|
||||
});
|
||||
|
||||
const builder = imageUrlBuilder(client);
|
||||
|
||||
export function urlFor(source: SanityImageSource) {
|
||||
return builder.image(source);
|
||||
}
|
||||
424
apps/www/src/utils/asciiBuilder.ts
Normal file
424
apps/www/src/utils/asciiBuilder.ts
Normal file
|
|
@ -0,0 +1,424 @@
|
|||
/**
|
||||
* ASCII Diagram Builder - Auto-spacing and formatting utilities
|
||||
*
|
||||
* This module provides utilities to ensure consistent spacing across ASCII diagrams
|
||||
* similar to the intent detection diagram pattern.
|
||||
*/
|
||||
|
||||
interface BoxDimensions {
|
||||
label: string;
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates proper padding to center content within a container width
|
||||
*/
|
||||
export function calculateCenterPadding(
|
||||
contentWidth: number,
|
||||
containerWidth: number,
|
||||
): number {
|
||||
return Math.floor((containerWidth - contentWidth) / 2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a horizontal arrow between two positions
|
||||
*/
|
||||
export function createArrow(
|
||||
length: number,
|
||||
direction: "→" | "↓" | "↑" | "←" = "→",
|
||||
): string {
|
||||
return direction.repeat(length);
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a box with specified dimensions, label, and box type
|
||||
*/
|
||||
export function buildBox(
|
||||
label: string,
|
||||
type: "container" | "inner" | "regular" = "regular",
|
||||
shadow: boolean = true,
|
||||
width?: number,
|
||||
): string[] {
|
||||
const actualWidth = width || Math.max(label.length + 4, 12);
|
||||
const paddedLabel = label
|
||||
.padStart(Math.floor((actualWidth - 2 + label.length) / 2), " ")
|
||||
.padEnd(actualWidth - 2, " ");
|
||||
|
||||
const symbols = {
|
||||
container: { tl: "╔", tr: "╗", bl: "╚", br: "╝", h: "═", v: "║" },
|
||||
inner: { tl: "┏", tr: "┓", bl: "┗", br: "┛", h: "━", v: "┃" },
|
||||
regular: { tl: "┌", tr: "┐", bl: "└", br: "┘", h: "─", v: "│" },
|
||||
};
|
||||
|
||||
const s = symbols[type];
|
||||
const shadowChar = "░";
|
||||
|
||||
const lines = [
|
||||
s.tl + s.h.repeat(actualWidth - 2) + s.tr + (shadow ? shadowChar : ""),
|
||||
s.v + paddedLabel + s.v + (shadow ? shadowChar : ""),
|
||||
s.bl + s.h.repeat(actualWidth - 2) + s.br + (shadow ? shadowChar : ""),
|
||||
];
|
||||
|
||||
if (shadow) {
|
||||
lines.push(" " + shadowChar.repeat(actualWidth));
|
||||
}
|
||||
|
||||
return lines;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fixes spacing in an existing diagram by analyzing and adjusting alignment
|
||||
*/
|
||||
export function fixDiagramSpacing(diagram: string): string {
|
||||
const lines = diagram.split("\n");
|
||||
if (lines.length === 0) return diagram;
|
||||
|
||||
// Find the container boundaries (look for ╔ and ╚ markers)
|
||||
let containerStart = -1;
|
||||
let containerEnd = -1;
|
||||
let containerWidth = 0;
|
||||
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i];
|
||||
if (line.includes("╔═") && line.includes("╗")) {
|
||||
containerStart = i;
|
||||
containerWidth = line.length;
|
||||
}
|
||||
if (line.includes("╚") && line.includes("╝")) {
|
||||
containerEnd = i;
|
||||
}
|
||||
}
|
||||
|
||||
if (containerStart === -1 || containerEnd === -1) {
|
||||
return diagram; // Can't fix if no container found
|
||||
}
|
||||
|
||||
// The intent detection pattern shows:
|
||||
// Line 2: title line with ╔═ {title} ═{fill}╗
|
||||
// Lines 3-19: content with ║ on sides
|
||||
// Line 20: bottom ╚══════╝
|
||||
// Line 21: shadow line
|
||||
|
||||
// For intent detection, the container content width is about 60 chars
|
||||
// Total line width including borders is about 68-70
|
||||
// Content starts around position 26
|
||||
|
||||
// Detect pattern by looking at first content line
|
||||
const firstContentLine = lines[containerStart + 1];
|
||||
if (!firstContentLine) return diagram;
|
||||
|
||||
const leftPadding = firstContentLine.indexOf("║");
|
||||
const rightPadding = containerWidth - firstContentLine.lastIndexOf("║") - 1;
|
||||
|
||||
// Now standardize all internal lines
|
||||
const fixedLines = [...lines];
|
||||
|
||||
for (let i = containerStart + 1; i < containerEnd; i++) {
|
||||
const line = lines[i];
|
||||
const shadowIndex = line.indexOf("░");
|
||||
|
||||
if (line.trim().startsWith("║")) {
|
||||
// This is a content line inside the container
|
||||
// Standardize the padding
|
||||
const content = extractContainerContent(line);
|
||||
fixedLines[i] = padContainerLine(content, containerWidth, leftPadding);
|
||||
}
|
||||
}
|
||||
|
||||
return fixedLines.join("\n");
|
||||
}
|
||||
|
||||
function extractContainerContent(line: string): string {
|
||||
// Extract content between ║ characters
|
||||
const startIdx = line.indexOf("║");
|
||||
const endIdx = line.lastIndexOf("║");
|
||||
if (startIdx === -1 || endIdx === -1 || startIdx === endIdx) return line;
|
||||
return line.substring(startIdx + 1, endIdx);
|
||||
}
|
||||
|
||||
function padContainerLine(
|
||||
content: string,
|
||||
containerWidth: number,
|
||||
targetLeftPad: number,
|
||||
): string {
|
||||
const padding = " ".repeat(targetLeftPad);
|
||||
const contentLength = content.length;
|
||||
const rightPadding = containerWidth - targetLeftPad - contentLength - 2; // -2 for two ║
|
||||
const rightPad = rightPadding > 0 ? " ".repeat(rightPadding) : "";
|
||||
|
||||
return padding + "║" + content + "║" + rightPad + "░";
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a simple flow diagram programmatically
|
||||
* Usage:
|
||||
* ```ts
|
||||
* const diagram = createFlowDiagram({
|
||||
* title: "My Process",
|
||||
* width: 60,
|
||||
* steps: [
|
||||
* { label: "Step 1", type: "regular" },
|
||||
* { label: "Step 2", type: "inner" },
|
||||
* { label: "Step 3", type: "regular" }
|
||||
* ]
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
export interface FlowStep {
|
||||
label: string;
|
||||
type?: "container" | "inner" | "regular";
|
||||
shadow?: boolean;
|
||||
}
|
||||
|
||||
export interface FlowDiagramConfig {
|
||||
title: string;
|
||||
width?: number;
|
||||
steps: FlowStep[];
|
||||
layout?: "vertical" | "horizontal";
|
||||
externalElements?: FlowStep[]; // Elements outside the container (like "agent")
|
||||
}
|
||||
|
||||
export function createFlowDiagram(config: FlowDiagramConfig): string {
|
||||
const layout = config.layout || "vertical";
|
||||
|
||||
if (layout === "horizontal") {
|
||||
return createHorizontalFlow(config);
|
||||
} else {
|
||||
return createVerticalFlow(config);
|
||||
}
|
||||
}
|
||||
|
||||
function createVerticalFlow(config: FlowDiagramConfig): string {
|
||||
const width = config.width || 60;
|
||||
const hasExternal =
|
||||
config.externalElements && config.externalElements.length > 0;
|
||||
|
||||
// Build external elements first
|
||||
let externalBoxes: string[] = [];
|
||||
let externalWidth = 0;
|
||||
|
||||
if (hasExternal) {
|
||||
externalWidth = 20;
|
||||
for (const extEl of config.externalElements!) {
|
||||
const extWidth = Math.max(extEl.label.length + 4, 12);
|
||||
const extBoxLines = buildBox(
|
||||
extEl.label,
|
||||
extEl.type || "regular",
|
||||
extEl.shadow !== false,
|
||||
extWidth,
|
||||
);
|
||||
|
||||
for (const extLine of extBoxLines) {
|
||||
externalBoxes.push(" ".repeat(2) + extLine);
|
||||
}
|
||||
|
||||
// Add vertical arrow if not last
|
||||
if (
|
||||
extEl !== config.externalElements![config.externalElements!.length - 1]
|
||||
) {
|
||||
const arrowPad = 2 + Math.floor(extWidth / 2);
|
||||
externalBoxes.push(" ".repeat(arrowPad) + "▼");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const titleLine = hasExternal
|
||||
? ` ╔═ ${config.title} ${"═".repeat(Math.max(0, width - config.title.length - 5))}╗`
|
||||
: `╔═ ${config.title} ${"═".repeat(Math.max(0, width - config.title.length - 5))}╗`;
|
||||
|
||||
const lines: string[] = [];
|
||||
lines.push(titleLine);
|
||||
|
||||
// Find max step width
|
||||
const maxStepWidth = Math.max(...config.steps.map((s) => s.label.length), 20);
|
||||
const stepWidth = maxStepWidth + 4;
|
||||
|
||||
// Build internal steps
|
||||
const internalLines: string[] = [];
|
||||
|
||||
for (let i = 0; i < config.steps.length; i++) {
|
||||
const step = config.steps[i];
|
||||
const boxLines = buildBox(
|
||||
step.label,
|
||||
step.type || "regular",
|
||||
step.shadow !== false,
|
||||
stepWidth,
|
||||
);
|
||||
|
||||
// Center each box
|
||||
const leftPadding = calculateCenterPadding(stepWidth, width);
|
||||
|
||||
for (const boxLine of boxLines) {
|
||||
internalLines.push(" ".repeat(leftPadding) + boxLine);
|
||||
}
|
||||
|
||||
// Add vertical arrow between steps (except last)
|
||||
if (i < config.steps.length - 1) {
|
||||
const arrowPad = calculateCenterPadding(1, width);
|
||||
internalLines.push(" ".repeat(arrowPad) + "│");
|
||||
internalLines.push(" ".repeat(arrowPad) + "▼");
|
||||
}
|
||||
}
|
||||
|
||||
// Combine external and internal elements
|
||||
const maxHeight = Math.max(externalBoxes.length, internalLines.length);
|
||||
|
||||
for (let row = 0; row < maxHeight; row++) {
|
||||
let line = "";
|
||||
|
||||
// External part
|
||||
if (row < externalBoxes.length) {
|
||||
line += externalBoxes[row];
|
||||
|
||||
// Add connecting arrow on middle row
|
||||
if (row === Math.floor(externalBoxes.length / 2)) {
|
||||
line += "░".repeat(6) + "─".repeat(10) + "─▶║─";
|
||||
} else {
|
||||
line += "░".repeat(6) + " ".repeat(10) + " ║ ";
|
||||
}
|
||||
} else if (hasExternal) {
|
||||
line += " ".repeat(externalWidth);
|
||||
if (row < internalLines.length) {
|
||||
line += "░".repeat(6) + " ".repeat(10) + " ║ ";
|
||||
}
|
||||
} else {
|
||||
line += " ".repeat(externalWidth);
|
||||
}
|
||||
|
||||
// Internal container part
|
||||
if (row < internalLines.length) {
|
||||
line += internalLines[row];
|
||||
} else {
|
||||
line += " ".repeat(width);
|
||||
}
|
||||
|
||||
line += "║░";
|
||||
lines.push(line);
|
||||
}
|
||||
|
||||
// Close container
|
||||
const bottomPadding = hasExternal ? " ".repeat(externalWidth + 16) : "";
|
||||
const bottomLine = bottomPadding + "╚" + "═".repeat(width - 1) + "╝░";
|
||||
lines.push(bottomLine);
|
||||
|
||||
const shadowLine =
|
||||
(hasExternal ? " ".repeat(externalWidth + 17) : " ") + "░".repeat(width);
|
||||
lines.push(shadowLine);
|
||||
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
function createHorizontalFlow(config: FlowDiagramConfig): string {
|
||||
const width = config.width || 70;
|
||||
const hasExternal =
|
||||
config.externalElements && config.externalElements.length > 0;
|
||||
const lines: string[] = [];
|
||||
|
||||
// Calculate step widths
|
||||
const maxStepWidth = Math.max(...config.steps.map((s) => s.label.length), 16);
|
||||
const stepWidth = maxStepWidth + 4;
|
||||
const arrowGap = 12;
|
||||
const totalStepWidth =
|
||||
config.steps.length * stepWidth + (config.steps.length - 1) * arrowGap;
|
||||
const containerPadding = Math.max(
|
||||
4,
|
||||
Math.floor((width - totalStepWidth) / 2),
|
||||
);
|
||||
|
||||
// Build internal boxes matrix
|
||||
const boxMatrix: string[][] = [];
|
||||
let maxHeight = 0;
|
||||
for (const step of config.steps) {
|
||||
const boxLines = buildBox(
|
||||
step.label,
|
||||
step.type || "regular",
|
||||
step.shadow !== false,
|
||||
stepWidth,
|
||||
);
|
||||
boxMatrix.push(boxLines);
|
||||
maxHeight = Math.max(maxHeight, boxLines.length);
|
||||
}
|
||||
|
||||
// Title line - position based on external elements
|
||||
const titleLeftPad = hasExternal ? 26 : 26;
|
||||
const titleRepeatCount = Math.max(0, width - config.title.length - 5);
|
||||
const titleLine =
|
||||
" ".repeat(titleLeftPad) +
|
||||
`╔═ ${config.title} ${"═".repeat(titleRepeatCount)}╗`;
|
||||
lines.push(titleLine);
|
||||
|
||||
// Build external box for rendering
|
||||
let externalBoxLines: string[] = [];
|
||||
if (hasExternal) {
|
||||
const extEl = config.externalElements![0];
|
||||
const extWidth = Math.max(extEl.label.length + 4, 16);
|
||||
externalBoxLines = buildBox(
|
||||
extEl.label,
|
||||
extEl.type || "regular",
|
||||
extEl.shadow !== false,
|
||||
extWidth,
|
||||
);
|
||||
}
|
||||
|
||||
// Render content rows
|
||||
for (let row = 0; row < maxHeight; row++) {
|
||||
let line = "";
|
||||
|
||||
// External elements on left (if present)
|
||||
if (hasExternal) {
|
||||
const extRow = row < externalBoxLines.length ? row : -1;
|
||||
|
||||
if (extRow >= 0) {
|
||||
line += " " + externalBoxLines[extRow];
|
||||
|
||||
// Add connecting arrow on middle row
|
||||
if (row === Math.floor(externalBoxLines.length / 2)) {
|
||||
line += "░".repeat(5) + "─".repeat(8) + "─▶║─";
|
||||
} else {
|
||||
line += "░".repeat(5) + " ".repeat(8) + " ║ ";
|
||||
}
|
||||
} else {
|
||||
line += " ".repeat(26) + "║ ";
|
||||
}
|
||||
} else {
|
||||
line += " ".repeat(26) + "║ ";
|
||||
}
|
||||
|
||||
// Internal container boxes with proper padding
|
||||
line += " ".repeat(containerPadding);
|
||||
|
||||
for (let i = 0; i < boxMatrix.length; i++) {
|
||||
const boxLines = boxMatrix[i];
|
||||
const boxLine =
|
||||
row < boxLines.length
|
||||
? boxLines[row]
|
||||
: " ".repeat(stepWidth + (config.steps[i].shadow !== false ? 1 : 0));
|
||||
line += boxLine;
|
||||
|
||||
// Add horizontal arrow between boxes
|
||||
if (i < boxMatrix.length - 1) {
|
||||
if (row === Math.floor(maxHeight / 2)) {
|
||||
line += "─".repeat(arrowGap) + "►";
|
||||
} else {
|
||||
line += " ".repeat(arrowGap + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Right padding and border
|
||||
const usedWidth = containerPadding + totalStepWidth;
|
||||
const rightPad = Math.max(0, width - usedWidth);
|
||||
line += " ".repeat(rightPad);
|
||||
line += "║░";
|
||||
lines.push(line);
|
||||
}
|
||||
|
||||
// Close container
|
||||
const bottomLine = " ".repeat(26) + "╚" + "═".repeat(width - 1) + "╝░";
|
||||
lines.push(bottomLine);
|
||||
const shadowLine = " ".repeat(27) + "░".repeat(width);
|
||||
lines.push(shadowLine);
|
||||
|
||||
return lines.join("\n");
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue