mirror of
https://github.com/katanemo/plano.git
synced 2026-06-17 15:25:17 +02:00
feat(www): ran biome formatting
This commit is contained in:
parent
43ced80d91
commit
96097a7b00
24 changed files with 706 additions and 521 deletions
|
|
@ -6,7 +6,8 @@
|
|||
/* Font Face Declarations */
|
||||
@font-face {
|
||||
font-family: "IBM Plex Sans";
|
||||
src: url("/fonts/IBMPlexSans-VariableFont_wdth,wght.ttf") format("truetype-variations");
|
||||
src: url("/fonts/IBMPlexSans-VariableFont_wdth,wght.ttf")
|
||||
format("truetype-variations");
|
||||
font-weight: 100 700; /* Variable font weight range */
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
|
|
@ -14,7 +15,8 @@
|
|||
|
||||
@font-face {
|
||||
font-family: "IBM Plex Sans";
|
||||
src: url("/fonts/IBMPlexSans-Italic-VariableFont_wdth,wght.ttf") format("truetype-variations");
|
||||
src: url("/fonts/IBMPlexSans-Italic-VariableFont_wdth,wght.ttf")
|
||||
format("truetype-variations");
|
||||
font-weight: 100 700; /* Variable font weight range */
|
||||
font-style: italic;
|
||||
font-display: swap;
|
||||
|
|
|
|||
|
|
@ -3,7 +3,8 @@ import "./globals.css";
|
|||
|
||||
export const metadata: Metadata = {
|
||||
title: "Plano - The AI-native network for agents",
|
||||
description: "Build and scale AI agents without handling the low-level plumbing.",
|
||||
description:
|
||||
"Build and scale AI agents without handling the low-level plumbing.",
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
|
|
@ -13,9 +14,7 @@ export default function RootLayout({
|
|||
}>) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<body className="antialiased">
|
||||
{children}
|
||||
</body>
|
||||
<body className="antialiased">{children}</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,18 +17,18 @@ export default function Home() {
|
|||
<div className="min-h-screen">
|
||||
<Navbar />
|
||||
<main className="pt-20">
|
||||
<Hero />
|
||||
<LogoCloud />
|
||||
<IntroSection />
|
||||
<IdeaToAgentSection />
|
||||
<UseCasesSection />
|
||||
<VerticalCarouselSection />
|
||||
<HowItWorksSection />
|
||||
<UnlockPotentialSection variant="black" />
|
||||
<Hero />
|
||||
<LogoCloud />
|
||||
<IntroSection />
|
||||
<IdeaToAgentSection />
|
||||
<UseCasesSection />
|
||||
<VerticalCarouselSection />
|
||||
<HowItWorksSection />
|
||||
<UnlockPotentialSection variant="black" />
|
||||
|
||||
{/* Rest of the sections will be refactored next */}
|
||||
</main>
|
||||
<Footer />
|
||||
</div>
|
||||
{/* Rest of the sections will be refactored next */}
|
||||
</main>
|
||||
<Footer />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import React from 'react';
|
||||
import React from "react";
|
||||
|
||||
interface AsciiDiagramProps {
|
||||
title?: string;
|
||||
|
|
@ -6,10 +6,10 @@ interface AsciiDiagramProps {
|
|||
className?: string;
|
||||
}
|
||||
|
||||
export const AsciiDiagram: React.FC<AsciiDiagramProps> = ({
|
||||
title,
|
||||
content,
|
||||
className = ""
|
||||
export const AsciiDiagram: React.FC<AsciiDiagramProps> = ({
|
||||
title,
|
||||
content,
|
||||
className = "",
|
||||
}) => {
|
||||
return (
|
||||
<div className={`max-w-4xl mx-auto mb-8 ${className}`}>
|
||||
|
|
@ -19,9 +19,9 @@ export const AsciiDiagram: React.FC<AsciiDiagramProps> = ({
|
|||
</h2>
|
||||
)}
|
||||
<div className="bg-gray-900 dark:bg-gray-950 rounded-lg p-6 shadow-xl overflow-x-auto">
|
||||
<pre
|
||||
<pre
|
||||
className="relative font-mono text-xs leading-none text-white m-0 whitespace-pre"
|
||||
style={{ fontFamily: 'var(--font-jetbrains-mono), monospace' }}
|
||||
style={{ fontFamily: "var(--font-jetbrains-mono), monospace" }}
|
||||
>
|
||||
<code>{content}</code>
|
||||
</pre>
|
||||
|
|
@ -34,7 +34,7 @@ export const AsciiDiagram: React.FC<AsciiDiagramProps> = ({
|
|||
interface DiagramStep {
|
||||
id: string;
|
||||
label: string;
|
||||
type?: 'input' | 'inner' | 'regular';
|
||||
type?: "input" | "inner" | "regular";
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
|
|
@ -56,35 +56,35 @@ 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 '';
|
||||
return "";
|
||||
};
|
||||
|
||||
// Helper to create boxes
|
||||
export const createBox = (
|
||||
label: string,
|
||||
type: 'input' | 'inner' | 'regular' = 'regular',
|
||||
width: number = 20
|
||||
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 spaces = " ".repeat(padding);
|
||||
const remaining = width - label.length - padding;
|
||||
|
||||
|
||||
let chars;
|
||||
switch (type) {
|
||||
case 'input':
|
||||
chars = { tl: '╔', tr: '╗', bl: '╚', br: '╝', h: '═', v: '║' };
|
||||
case "input":
|
||||
chars = { tl: "╔", tr: "╗", bl: "╚", br: "╝", h: "═", v: "║" };
|
||||
break;
|
||||
case 'inner':
|
||||
chars = { tl: '┏', tr: '┓', bl: '┗', br: '┛', h: '━', v: '┃' };
|
||||
case "inner":
|
||||
chars = { tl: "┏", tr: "┓", bl: "┗", br: "┛", h: "━", v: "┃" };
|
||||
break;
|
||||
case 'regular':
|
||||
case "regular":
|
||||
default:
|
||||
chars = { tl: '┌', tr: '┐', bl: '└', br: '┘', h: '─', v: '│' };
|
||||
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.v}${spaces}${label}${" ".repeat(remaining)}${chars.v}`,
|
||||
`${chars.bl}${chars.h.repeat(width)}${chars.br}`,
|
||||
];
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import React from 'react';
|
||||
import { createFlowDiagram, FlowDiagramConfig } from '@/utils/asciiBuilder';
|
||||
import { AsciiDiagram } from './AsciiDiagram';
|
||||
import React from "react";
|
||||
import { createFlowDiagram, FlowDiagramConfig } from "@/utils/asciiBuilder";
|
||||
import { AsciiDiagram } from "./AsciiDiagram";
|
||||
|
||||
interface DiagramBuilderProps {
|
||||
config: FlowDiagramConfig;
|
||||
|
|
@ -9,9 +9,9 @@ interface DiagramBuilderProps {
|
|||
|
||||
/**
|
||||
* Simple Diagram Builder Component
|
||||
*
|
||||
*
|
||||
* Usage:
|
||||
*
|
||||
*
|
||||
* <DiagramBuilder
|
||||
* config={{
|
||||
* title: "My Process",
|
||||
|
|
@ -24,9 +24,11 @@ interface DiagramBuilderProps {
|
|||
* }}
|
||||
* />
|
||||
*/
|
||||
export const DiagramBuilder: React.FC<DiagramBuilderProps> = ({ config, title }) => {
|
||||
export const DiagramBuilder: React.FC<DiagramBuilderProps> = ({
|
||||
config,
|
||||
title,
|
||||
}) => {
|
||||
const asciiDiagram = createFlowDiagram(config);
|
||||
|
||||
|
||||
return <AsciiDiagram content={asciiDiagram} title={title} />;
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -7,25 +7,27 @@ const footerLinks = {
|
|||
{ label: "Product", href: "/product" },
|
||||
{ label: "Use Cases", href: "/use-cases" },
|
||||
{ label: "Blog", href: "/blog" },
|
||||
{ label: "Plano LLMs", href: "/llms" }
|
||||
{ label: "Plano LLMs", href: "/llms" },
|
||||
],
|
||||
developerResources: [
|
||||
{ label: "Documentation", href: "/docs" }
|
||||
]
|
||||
developerResources: [{ label: "Documentation", href: "/docs" }],
|
||||
};
|
||||
|
||||
export function Footer() {
|
||||
return (
|
||||
<footer className="relative overflow-hidden pt-20 px-6 lg:px-[102px] pb-48" style={{ background: 'linear-gradient(to top right, #ffffff, #dcdfff)' }}>
|
||||
<footer
|
||||
className="relative overflow-hidden pt-20 px-6 lg:px-[102px] pb-48"
|
||||
style={{ background: "linear-gradient(to top right, #ffffff, #dcdfff)" }}
|
||||
>
|
||||
<div className="max-w-[81rem] mx-auto relative z-10">
|
||||
{/* Main Grid Layout */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-20">
|
||||
{/* Left Column - Tagline and Copyright */}
|
||||
<div className="flex flex-col">
|
||||
<p className="font-sans font-normal text-lg sm:text-xl lg:text-2xl text-black tracking-[-1.2px] sm:tracking-[-1.5px] lg:tracking-[-1.7px]! leading-7 mb-6 sm:mb-8">
|
||||
Plano is the powerful, intelligent platform that empowers teams to seamlessly build, automate, and scale agentic systems with ease.
|
||||
Plano is the powerful, intelligent platform that empowers teams to
|
||||
seamlessly build, automate, and scale agentic systems with ease.
|
||||
</p>
|
||||
|
||||
|
||||
{/* Copyright */}
|
||||
<div className="mt-auto">
|
||||
<p className="font-sans text-sm sm:text-base text-black/63 tracking-[-0.6px] sm:tracking-[-0.8px]!">
|
||||
|
|
@ -86,8 +88,8 @@ export function Footer() {
|
|||
height={200}
|
||||
className="w-150 h-auto opacity-30 select-none"
|
||||
style={{
|
||||
transform: 'translateY(0%)', // Push logo down more while showing top part
|
||||
transformOrigin: 'center bottom'
|
||||
transform: "translateY(0%)", // Push logo down more while showing top part
|
||||
transformOrigin: "center bottom",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -14,10 +14,16 @@ export function Hero() {
|
|||
{/* 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 hidden sm:inline">—</span>
|
||||
<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 hidden sm:inline">
|
||||
—
|
||||
</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="hidden sm:inline">
|
||||
Unified /v1/responses API with state management
|
||||
</span>
|
||||
<span className="sm:hidden">Unified /v1/responses API</span>
|
||||
</span>
|
||||
</div>
|
||||
|
|
@ -26,14 +32,17 @@ export function Hero() {
|
|||
{/* Main Heading */}
|
||||
<h1 className="text-4xl sm:text-4xl md:text-5xl lg:text-7xl font-normal leading-tight tracking-tighter text-black mb-4 sm:mb-6 flex flex-col gap-0 sm:-space-y-2 lg:-space-y-3">
|
||||
<span className="font-sans">Models-native </span>
|
||||
<span className="font-sans font-medium text-[var(--secondary)]">dataplane for agents</span>
|
||||
<span className="font-sans font-medium text-[var(--secondary)]">
|
||||
dataplane for agents
|
||||
</span>
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
{/* Subheading with CTA Buttons */}
|
||||
<div className="max-w-7xl flex flex-col lg:flex-row lg:items-end lg:justify-between gap-6">
|
||||
<p className="text-base sm:text-lg md:text-xl lg:text-2xl font-sans font-[400] tracking-[-1.2px] sm:tracking-[-1.92px]! text-black max-w-4xl">
|
||||
Build agents faster, and scale them reliably by offloading the plumbing work in AI.
|
||||
Build agents faster, and scale them reliably by offloading the
|
||||
plumbing work in AI.
|
||||
</p>
|
||||
|
||||
{/* CTA Buttons */}
|
||||
|
|
@ -50,4 +59,3 @@ export function Hero() {
|
|||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -15,14 +15,25 @@ export function HowItWorksSection() {
|
|||
</h2>
|
||||
<div className="font-mono text-white w-100 sm:w-full text-sm sm:text-lg lg:text-lg tracking-[-0.8px] sm:tracking-[-1.2px]!">
|
||||
<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.
|
||||
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" }}>
|
||||
<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;
|
||||
|
|
@ -57,4 +68,3 @@ export function HowItWorksSection() {
|
|||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -9,37 +9,42 @@ const carouselData = [
|
|||
id: 1,
|
||||
category: "LAUNCH FASTER",
|
||||
title: "Focus on core objectives",
|
||||
description: "Building AI agents is hard enough (iterate on prompts and evaluate LLMs, etc), the plumbing work shouldn't add to that complexity. Plano takes care of the critical plumbing work like routing and orchestration to agents that slows you down and locks you into rigid frameworks, freeing developers to innovate on what truly matters.",
|
||||
image: "/LaunchFaster.svg"
|
||||
description:
|
||||
"Building AI agents is hard enough (iterate on prompts and evaluate LLMs, etc), the plumbing work shouldn't add to that complexity. Plano takes care of the critical plumbing work like routing and orchestration to agents that slows you down and locks you into rigid frameworks, freeing developers to innovate on what truly matters.",
|
||||
image: "/LaunchFaster.svg",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
category: "BUILD WITH CHOICE",
|
||||
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.",
|
||||
image: "/BuildWithChoice.svg"
|
||||
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.",
|
||||
image: "/BuildWithChoice.svg",
|
||||
},
|
||||
{
|
||||
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 a 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"
|
||||
description:
|
||||
"Knowing when agents fail or delight users is a critical signal that feeds into a 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",
|
||||
},
|
||||
{
|
||||
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"
|
||||
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",
|
||||
},
|
||||
{
|
||||
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"
|
||||
}
|
||||
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",
|
||||
},
|
||||
];
|
||||
|
||||
export function IdeaToAgentSection() {
|
||||
|
|
@ -49,7 +54,7 @@ export function IdeaToAgentSection() {
|
|||
// Auto-advance slides
|
||||
useEffect(() => {
|
||||
if (!isAutoPlaying) return;
|
||||
|
||||
|
||||
const interval = setInterval(() => {
|
||||
setCurrentSlide((prev) => (prev + 1) % carouselData.length);
|
||||
}, 10000); // 10 seconds per slide
|
||||
|
|
@ -79,14 +84,14 @@ export function IdeaToAgentSection() {
|
|||
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]'
|
||||
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
|
||||
|
|
@ -97,7 +102,7 @@ export function IdeaToAgentSection() {
|
|||
key={currentSlide}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
{/* Completed State */}
|
||||
{index < currentSlide && (
|
||||
<div className="absolute inset-0 bg-purple-200/90 rounded-full" />
|
||||
|
|
@ -158,14 +163,16 @@ export function IdeaToAgentSection() {
|
|||
/>
|
||||
</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]"
|
||||
}`}>
|
||||
<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}
|
||||
|
|
|
|||
|
|
@ -20,27 +20,37 @@ export function IntroSection() {
|
|||
{/* Body Text */}
|
||||
<div className="font-mono tracking-[-0.96px]! text-white text-sm sm:text-base lg:text-lg max-w-[713px]">
|
||||
<p className="mb-0">
|
||||
Plano is a framework-friendly proxy server and dataplane for agents,
|
||||
deployed as a sidecar. Plano handles the critical plumbing work in AI
|
||||
like agent routing and orchestration, comprehensive traces for agentic
|
||||
interactions, guardrail hooks, unified APIs for LLMs —
|
||||
Plano is a framework-friendly proxy server and dataplane for
|
||||
agents, deployed as a sidecar. Plano handles the critical
|
||||
plumbing work in AI like agent routing and orchestration,
|
||||
comprehensive traces for agentic interactions, guardrail hooks,
|
||||
unified APIs for LLMs —
|
||||
</p>
|
||||
<p className="mb-0 mt-4">
|
||||
<strong><u>Developers</u></strong> can focus more on modeling workflows, <strong><u>product teams</u></strong> can accelerate
|
||||
feedback loops for reinforcement learning and <strong><u>engineering teams</u></strong> can
|
||||
standardize policies and access controls across every agent and LLM for
|
||||
safer, more reliable scaling.
|
||||
<strong>
|
||||
<u>Developers</u>
|
||||
</strong>{" "}
|
||||
can focus more on modeling workflows,{" "}
|
||||
<strong>
|
||||
<u>product teams</u>
|
||||
</strong>{" "}
|
||||
can accelerate feedback loops for reinforcement learning and{" "}
|
||||
<strong>
|
||||
<u>engineering teams</u>
|
||||
</strong>{" "}
|
||||
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}
|
||||
<Image
|
||||
src="/IntroDiagram.svg"
|
||||
alt="Network Path Diagram"
|
||||
width={800}
|
||||
height={600}
|
||||
className="w-full h-auto"
|
||||
priority
|
||||
/>
|
||||
|
|
@ -50,4 +60,3 @@ export function IntroSection() {
|
|||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -15,4 +15,3 @@ export function Logo() {
|
|||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,24 +4,24 @@ import Image from "next/image";
|
|||
const customerLogos = [
|
||||
{
|
||||
name: "HuggingFace",
|
||||
src: "/logos/huggingface.svg"
|
||||
src: "/logos/huggingface.svg",
|
||||
},
|
||||
{
|
||||
name: "T-Mobile",
|
||||
src: "/logos/tmobile.svg"
|
||||
src: "/logos/tmobile.svg",
|
||||
},
|
||||
{
|
||||
name: "Chase",
|
||||
src: "/logos/chase.svg"
|
||||
src: "/logos/chase.svg",
|
||||
},
|
||||
{
|
||||
name: "SanDisk",
|
||||
src: "/logos/sandisk.svg"
|
||||
src: "/logos/sandisk.svg",
|
||||
},
|
||||
{
|
||||
name: "Oracle",
|
||||
src: "/logos/oracle.svg"
|
||||
}
|
||||
src: "/logos/oracle.svg",
|
||||
},
|
||||
];
|
||||
|
||||
export function LogoCloud() {
|
||||
|
|
@ -35,7 +35,7 @@ export function LogoCloud() {
|
|||
<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 ${
|
||||
isLast ? 'col-span-2 md:col-span-3 lg:col-span-1' : ''
|
||||
isLast ? "col-span-2 md:col-span-3 lg:col-span-1" : ""
|
||||
}`}
|
||||
>
|
||||
<Image
|
||||
|
|
|
|||
|
|
@ -36,33 +36,34 @@ export function Navbar() {
|
|||
const navRect = nav.getBoundingClientRect();
|
||||
const dropdownBottom = navRect.bottom;
|
||||
const checkY = dropdownBottom + 20; // Just below the dropdown
|
||||
|
||||
|
||||
// First, try to find section elements directly
|
||||
const main = document.querySelector("main");
|
||||
if (main) {
|
||||
const sections = main.querySelectorAll("section");
|
||||
let foundDarkSection = false;
|
||||
|
||||
|
||||
sections.forEach((section) => {
|
||||
const rect = section.getBoundingClientRect();
|
||||
// Check if this section is visible below the navbar
|
||||
if (rect.top <= checkY && rect.bottom > checkY) {
|
||||
// Check for dark background classes
|
||||
const classList = Array.from(section.classList);
|
||||
const hasDarkBg = classList.some(cls =>
|
||||
cls.includes('bg-[#1a1a1a]') ||
|
||||
cls.includes('bg-black') ||
|
||||
cls.includes('bg-gray-900') ||
|
||||
cls.includes('bg-neutral-900') ||
|
||||
cls.includes('dark')
|
||||
const hasDarkBg = classList.some(
|
||||
(cls) =>
|
||||
cls.includes("bg-[#1a1a1a]") ||
|
||||
cls.includes("bg-black") ||
|
||||
cls.includes("bg-gray-900") ||
|
||||
cls.includes("bg-neutral-900") ||
|
||||
cls.includes("dark"),
|
||||
);
|
||||
|
||||
|
||||
if (hasDarkBg) {
|
||||
foundDarkSection = true;
|
||||
setIsDarkBackground(true);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Also check computed background
|
||||
const computed = window.getComputedStyle(section);
|
||||
const bg = computed.backgroundColor;
|
||||
|
|
@ -80,26 +81,33 @@ export function Navbar() {
|
|||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
if (foundDarkSection) return;
|
||||
}
|
||||
|
||||
|
||||
// Fallback: Check element at point
|
||||
const centerX = window.innerWidth / 2;
|
||||
const elementBelow = document.elementFromPoint(centerX, checkY);
|
||||
|
||||
|
||||
if (elementBelow) {
|
||||
let current: HTMLElement | null = elementBelow as HTMLElement;
|
||||
let backgroundColor = "";
|
||||
|
||||
|
||||
// Walk up the DOM tree
|
||||
let levels = 0;
|
||||
while (current && !backgroundColor && current !== document.body && levels < 15) {
|
||||
while (
|
||||
current &&
|
||||
!backgroundColor &&
|
||||
current !== document.body &&
|
||||
levels < 15
|
||||
) {
|
||||
const computed = window.getComputedStyle(current);
|
||||
const bg = computed.backgroundColor;
|
||||
|
||||
|
||||
if (bg && bg !== "rgba(0, 0, 0, 0)" && bg !== "transparent") {
|
||||
const rgbaMatch = bg.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*([\d.]+))?\)/);
|
||||
const rgbaMatch = bg.match(
|
||||
/rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*([\d.]+))?\)/,
|
||||
);
|
||||
if (rgbaMatch) {
|
||||
const alpha = rgbaMatch[4] ? parseFloat(rgbaMatch[4]) : 1;
|
||||
if (alpha > 0.1) {
|
||||
|
|
@ -111,18 +119,22 @@ export function Navbar() {
|
|||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
current = current.parentElement;
|
||||
levels++;
|
||||
}
|
||||
|
||||
|
||||
if (!backgroundColor) {
|
||||
const bodyBg = window.getComputedStyle(document.body).backgroundColor;
|
||||
const bodyBg = window.getComputedStyle(
|
||||
document.body,
|
||||
).backgroundColor;
|
||||
backgroundColor = bodyBg;
|
||||
}
|
||||
|
||||
|
||||
if (backgroundColor) {
|
||||
const rgbMatch = backgroundColor.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/);
|
||||
const rgbMatch = backgroundColor.match(
|
||||
/rgba?\((\d+),\s*(\d+),\s*(\d+)/,
|
||||
);
|
||||
if (rgbMatch) {
|
||||
const r = parseInt(rgbMatch[1]);
|
||||
const g = parseInt(rgbMatch[2]);
|
||||
|
|
@ -130,8 +142,17 @@ export function Navbar() {
|
|||
const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255;
|
||||
setIsDarkBackground(luminance < 0.5);
|
||||
} else {
|
||||
const darkColors = ['black', '#000', '#000000', 'rgb(0,0,0)', 'rgba(0,0,0', '#1a1a1a'];
|
||||
const isDark = darkColors.some(color => backgroundColor.toLowerCase().includes(color.toLowerCase()));
|
||||
const darkColors = [
|
||||
"black",
|
||||
"#000",
|
||||
"#000000",
|
||||
"rgb(0,0,0)",
|
||||
"rgba(0,0,0",
|
||||
"#1a1a1a",
|
||||
];
|
||||
const isDark = darkColors.some((color) =>
|
||||
backgroundColor.toLowerCase().includes(color.toLowerCase()),
|
||||
);
|
||||
setIsDarkBackground(isDark);
|
||||
}
|
||||
}
|
||||
|
|
@ -143,7 +164,7 @@ export function Navbar() {
|
|||
detectBackground();
|
||||
const scrollHandler = () => detectBackground();
|
||||
const resizeHandler = () => detectBackground();
|
||||
|
||||
|
||||
window.addEventListener("scroll", scrollHandler, { passive: true });
|
||||
window.addEventListener("resize", resizeHandler);
|
||||
|
||||
|
|
@ -176,7 +197,7 @@ export function Navbar() {
|
|||
className={cn(
|
||||
"text-lg font-medium text-[var(--muted)]",
|
||||
"hover:text-[var(--primary)] transition-colors",
|
||||
"font-mono tracking-tighter"
|
||||
"font-mono tracking-tighter",
|
||||
)}
|
||||
>
|
||||
{item.label}
|
||||
|
|
@ -253,7 +274,8 @@ export function Navbar() {
|
|||
"block px-0 py-1.5 border-b border-dashed transition-colors font-mono tracking-tighter",
|
||||
"text-sm font-medium",
|
||||
"text-white",
|
||||
)}>
|
||||
)}
|
||||
>
|
||||
{item.label}
|
||||
</Link>
|
||||
</motion.div>
|
||||
|
|
@ -266,4 +288,3 @@ export function Navbar() {
|
|||
</nav>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -28,12 +28,12 @@ const nodes: Node[] = [
|
|||
{ id: "1", x: 20, y: 35, size: 18, delay: 0 },
|
||||
{ id: "2", x: 70, y: 42, size: 12, delay: 0.8 },
|
||||
{ id: "3", x: 76, y: 38, size: 16, delay: 1.6 },
|
||||
|
||||
|
||||
// // Group 2
|
||||
// { id: "4", x: 80, y: 30, size: 18, delay: 0.4 },
|
||||
// { id: "5", x: 87, y: 36, size: 12, delay: 1.2 },
|
||||
// { id: "6", x: 92, y: 33, size: 16, delay: 2.0 },
|
||||
|
||||
|
||||
// Group 3
|
||||
{ id: "7", x: 62, y: 48, size: 10, delay: 0.6 },
|
||||
{ id: "8", x: 65, y: 52, size: 18, delay: 1.4 },
|
||||
|
|
@ -44,11 +44,11 @@ const connections: Connection[] = [
|
|||
// Group 1 connections
|
||||
{ from: "1", to: "2" },
|
||||
{ from: "2", to: "3" },
|
||||
|
||||
|
||||
// // Group 2 connections
|
||||
// { from: "4", to: "5" },
|
||||
// { from: "5", to: "6" },
|
||||
|
||||
|
||||
// Group 3 connections
|
||||
{ from: "7", to: "8" },
|
||||
{ from: "8", to: "9" },
|
||||
|
|
@ -66,7 +66,7 @@ export function NetworkAnimation() {
|
|||
connectionIndex,
|
||||
progress: 0,
|
||||
};
|
||||
|
||||
|
||||
setParticles((prev) => [...prev, particle]);
|
||||
|
||||
// Remove particle after animation completes
|
||||
|
|
@ -86,7 +86,7 @@ export function NetworkAnimation() {
|
|||
setParticles((prev) =>
|
||||
prev
|
||||
.map((p) => ({ ...p, progress: p.progress + 0.008 })) // Much slower
|
||||
.filter((p) => p.progress <= 1)
|
||||
.filter((p) => p.progress <= 1),
|
||||
);
|
||||
}, 50);
|
||||
|
||||
|
|
@ -144,8 +144,12 @@ export function NetworkAnimation() {
|
|||
const toNode = nodes.find((n) => n.id === conn.to);
|
||||
if (!fromNode || !toNode) return null;
|
||||
|
||||
const x = ((fromNode.x + (toNode.x - fromNode.x) * particle.progress) / 100) * 1920;
|
||||
const y = ((fromNode.y + (toNode.y - fromNode.y) * particle.progress) / 100) * 800;
|
||||
const x =
|
||||
((fromNode.x + (toNode.x - fromNode.x) * particle.progress) / 100) *
|
||||
1920;
|
||||
const y =
|
||||
((fromNode.y + (toNode.y - fromNode.y) * particle.progress) / 100) *
|
||||
800;
|
||||
|
||||
return (
|
||||
<motion.circle
|
||||
|
|
@ -155,13 +159,13 @@ export function NetworkAnimation() {
|
|||
r={4}
|
||||
fill="#b9bfff"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{
|
||||
opacity: [0, 0.4, 0.4, 0]
|
||||
animate={{
|
||||
opacity: [0, 0.4, 0.4, 0],
|
||||
}}
|
||||
transition={{
|
||||
transition={{
|
||||
duration: 5,
|
||||
times: [0, 0.2, 0.8, 1],
|
||||
ease: "linear"
|
||||
ease: "linear",
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -6,29 +6,32 @@ interface UnlockPotentialSectionProps {
|
|||
className?: string;
|
||||
}
|
||||
|
||||
export function UnlockPotentialSection({
|
||||
variant = "transparent",
|
||||
className = ""
|
||||
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] ${backgroundClass} ${className}`}>
|
||||
<section
|
||||
className={`relative py-24 px-6 lg:px-[102px] ${backgroundClass} ${className}`}
|
||||
>
|
||||
<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
|
||||
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>
|
||||
Deploy today
|
||||
</Button>
|
||||
<Button variant="secondaryDark">
|
||||
Documentation
|
||||
</Button>
|
||||
<Button>Deploy today</Button>
|
||||
<Button variant="secondaryDark">Documentation</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,9 +1,24 @@
|
|||
"use client";
|
||||
|
||||
import React, { useState } from "react";
|
||||
import { ArrowRightIcon, Network, Filter, TrendingUp, Shield, Server, XIcon } from "lucide-react";
|
||||
import {
|
||||
ArrowRightIcon,
|
||||
Network,
|
||||
Filter,
|
||||
TrendingUp,
|
||||
Shield,
|
||||
Server,
|
||||
XIcon,
|
||||
} from "lucide-react";
|
||||
import { Button } from "./ui/button";
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogClose } from "./ui/dialog";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogDescription,
|
||||
DialogClose,
|
||||
} from "./ui/dialog";
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
|
||||
interface UseCase {
|
||||
|
|
@ -21,47 +36,59 @@ 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.",
|
||||
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)]"
|
||||
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.",
|
||||
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)]"
|
||||
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.",
|
||||
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)]"
|
||||
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.",
|
||||
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)]"
|
||||
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.",
|
||||
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)]"
|
||||
}
|
||||
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() {
|
||||
|
|
@ -75,7 +102,9 @@ export function UseCasesSection() {
|
|||
{/* 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>
|
||||
<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>
|
||||
|
||||
|
|
@ -84,9 +113,7 @@ export function UseCasesSection() {
|
|||
<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 className="hidden lg:block">
|
||||
Start building
|
||||
</Button>
|
||||
<Button className="hidden lg:block">Start building</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -125,115 +152,143 @@ export function UseCasesSection() {
|
|||
|
||||
{/* Start building button - Mobile only, appears last */}
|
||||
<div className="lg:hidden mt-8">
|
||||
<Button className="w-full">
|
||||
Start building
|
||||
</Button>
|
||||
<Button className="w-full">Start building</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Modal */}
|
||||
<Dialog open={selectedUseCase !== null} onOpenChange={(open) => !open && setSelectedUseCase(null)}>
|
||||
<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"
|
||||
{selectedUseCase &&
|
||||
(() => {
|
||||
const IconComponent = selectedUseCase.icon;
|
||||
return (
|
||||
<DialogContent
|
||||
key={selectedUseCase.id}
|
||||
className="max-w-[90rem]! p-0 overflow-hidden"
|
||||
showCloseButton={false}
|
||||
>
|
||||
{/* 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>
|
||||
<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>
|
||||
</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="font-mono text-[#494949] text-base lg:text-base xl:text-lg leading-relaxed tracking-tight 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 font-mono text-[#494949] 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 className="w-full sm:w-auto">
|
||||
Start building
|
||||
<ArrowRightIcon className="w-4 h-4" />
|
||||
</Button>
|
||||
</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="font-mono text-[#494949] text-base lg:text-base xl:text-lg leading-relaxed tracking-tight 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 font-mono text-[#494949] 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 className="w-full sm:w-auto">
|
||||
Start building
|
||||
<ArrowRightIcon className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</DialogContent>
|
||||
);
|
||||
})()}
|
||||
</DialogContent>
|
||||
);
|
||||
})()}
|
||||
</AnimatePresence>
|
||||
</Dialog>
|
||||
</section>
|
||||
|
|
|
|||
|
|
@ -10,37 +10,42 @@ const verticalCarouselData = [
|
|||
id: 1,
|
||||
category: "INTRODUCTION",
|
||||
title: "Simple to revolutionary",
|
||||
description: "Plano is an intelligent (edge and LLM) proxy server designed for agents - to help you focus on core business objectives. Arch handles critical but the pesky tasks related to the handling and processing of prompts, which includes detecting and rejecting jailbreak attempts, intelligent task routing for improved accuracy, mapping user requests into 'backend' functions, and managing the observability of prompts and LLM in a centralized way.",
|
||||
diagram: "/IntroDiagram.svg"
|
||||
description:
|
||||
"Plano is an intelligent (edge and LLM) proxy server designed for agents - to help you focus on core business objectives. Arch handles critical but the pesky tasks related to the handling and processing of prompts, which includes detecting and rejecting jailbreak attempts, intelligent task routing for improved accuracy, mapping user requests into 'backend' functions, and managing the observability of prompts and LLM in a centralized way.",
|
||||
diagram: "/IntroDiagram.svg",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
category: "OPEN SOURCE",
|
||||
title: "Freedom to extend & deploy",
|
||||
description: "No lock-in. No black boxes. Just an open, intelligent (edge and LLM) proxy for building smarter, agentic AI applications. Created by contributors to Envoy Proxy, Arch brings enterprise-grade reliability to prompt orchestration, while giving you the flexibility to shape, extend, and integrate it into your AI workflows.",
|
||||
diagram: "/OpenSource.svg"
|
||||
description:
|
||||
"No lock-in. No black boxes. Just an open, intelligent (edge and LLM) proxy for building smarter, agentic AI applications. Created by contributors to Envoy Proxy, Arch brings enterprise-grade reliability to prompt orchestration, while giving you the flexibility to shape, extend, and integrate it into your AI workflows.",
|
||||
diagram: "/OpenSource.svg",
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
category: "BUILT ON ENVOY",
|
||||
title: "Production-proven infrastructure",
|
||||
description: "Plano takes a dependency on Envoy and is a self-contained process designed to run alongside your application servers. Plano extends Envoy's HTTP connection management subsystem, filtering, and telemetry capabilities exclusively for prompts and LLMs. Use Plano with any application language or framework, and use Plano with any LLM provider.",
|
||||
diagram: "/BuiltOnEnvoy.svg"
|
||||
title: "Production-proven infrastructure",
|
||||
description:
|
||||
"Plano takes a dependency on Envoy and is a self-contained process designed to run alongside your application servers. Plano extends Envoy's HTTP connection management subsystem, filtering, and telemetry capabilities exclusively for prompts and LLMs. Use Plano with any application language or framework, and use Plano with any LLM provider.",
|
||||
diagram: "/BuiltOnEnvoy.svg",
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
category: "PURPOSE-BUILT",
|
||||
title: "Task-optimized, efficient LLMs",
|
||||
description: "Unlike generic API gateways, Plano is purpose-built for AI agent workloads. Every feature is designed with prompt processing, model routing, and agent orchestration in mind, providing optimal performance for your AI applications.",
|
||||
diagram: "/PurposeBuilt.svg"
|
||||
description:
|
||||
"Unlike generic API gateways, Plano is purpose-built for AI agent workloads. Every feature is designed with prompt processing, model routing, and agent orchestration in mind, providing optimal performance for your AI applications.",
|
||||
diagram: "/PurposeBuilt.svg",
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
category: "PROMPT ROUTING",
|
||||
title: "Intelligent request handling",
|
||||
description: "Prompt Targets are a core concept in Plano, enabling developers to define how different types of user prompts should get processed and routed. Define prompt targets, so you can seperate business logic from the complexities of processing and handling of prompts, focusing on the quality of your application and a cleaner seperation of concerns in your codebase.",
|
||||
diagram: "/PromptRouting.svg"
|
||||
}
|
||||
description:
|
||||
"Prompt Targets are a core concept in Plano, enabling developers to define how different types of user prompts should get processed and routed. Define prompt targets, so you can seperate business logic from the complexities of processing and handling of prompts, focusing on the quality of your application and a cleaner seperation of concerns in your codebase.",
|
||||
diagram: "/PromptRouting.svg",
|
||||
},
|
||||
];
|
||||
|
||||
export function VerticalCarouselSection() {
|
||||
|
|
@ -64,7 +69,7 @@ export function VerticalCarouselSection() {
|
|||
className="relative overflow-x-auto pb-2"
|
||||
style={{
|
||||
scrollbarWidth: "none",
|
||||
msOverflowStyle: "none"
|
||||
msOverflowStyle: "none",
|
||||
}}
|
||||
>
|
||||
<style jsx>{`
|
||||
|
|
@ -79,8 +84,8 @@ export function VerticalCarouselSection() {
|
|||
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'
|
||||
? "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]!">
|
||||
|
|
@ -100,16 +105,16 @@ export function VerticalCarouselSection() {
|
|||
<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
|
||||
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
|
||||
duration: 0.6,
|
||||
}}
|
||||
/>
|
||||
|
||||
|
||||
{verticalCarouselData.map((item, index) => (
|
||||
<div
|
||||
key={item.id}
|
||||
|
|
@ -117,11 +122,13 @@ export function VerticalCarouselSection() {
|
|||
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)]'
|
||||
}`}>
|
||||
<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>
|
||||
|
|
@ -144,12 +151,12 @@ export function VerticalCarouselSection() {
|
|||
{/* 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].title}
|
||||
<Image
|
||||
src={verticalCarouselData[activeSlide].diagram}
|
||||
alt={verticalCarouselData[activeSlide].title}
|
||||
fill
|
||||
className="object-contain object-top"
|
||||
priority
|
||||
className="object-contain object-top"
|
||||
priority
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import * as React from "react"
|
||||
import { Slot } from "@radix-ui/react-slot"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
import * as React from "react";
|
||||
import { Slot } from "@radix-ui/react-slot";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
|
||||
import { cn } from "../../lib/utils";
|
||||
|
||||
|
|
@ -38,8 +38,8 @@ const buttonVariants = cva(
|
|||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
},
|
||||
);
|
||||
|
||||
function Button({
|
||||
className,
|
||||
|
|
@ -49,9 +49,9 @@ function Button({
|
|||
...props
|
||||
}: React.ComponentProps<"button"> &
|
||||
VariantProps<typeof buttonVariants> & {
|
||||
asChild?: boolean
|
||||
asChild?: boolean;
|
||||
}) {
|
||||
const Comp = asChild ? Slot : "button"
|
||||
const Comp = asChild ? Slot : "button";
|
||||
|
||||
return (
|
||||
<Comp
|
||||
|
|
@ -59,7 +59,7 @@ function Button({
|
|||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export { Button, buttonVariants }
|
||||
export { Button, buttonVariants };
|
||||
|
|
|
|||
|
|
@ -1,33 +1,33 @@
|
|||
"use client"
|
||||
"use client";
|
||||
|
||||
import * as React from "react"
|
||||
import * as DialogPrimitive from "@radix-ui/react-dialog"
|
||||
import { XIcon } from "lucide-react"
|
||||
import * as React from "react";
|
||||
import * as DialogPrimitive from "@radix-ui/react-dialog";
|
||||
import { XIcon } from "lucide-react";
|
||||
|
||||
import { cn } from "../../lib/utils"
|
||||
import { cn } from "../../lib/utils";
|
||||
|
||||
function Dialog({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Root>) {
|
||||
return <DialogPrimitive.Root data-slot="dialog" {...props} />
|
||||
return <DialogPrimitive.Root data-slot="dialog" {...props} />;
|
||||
}
|
||||
|
||||
function DialogTrigger({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
|
||||
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />
|
||||
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />;
|
||||
}
|
||||
|
||||
function DialogPortal({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Portal>) {
|
||||
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />
|
||||
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />;
|
||||
}
|
||||
|
||||
function DialogClose({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Close>) {
|
||||
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />
|
||||
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />;
|
||||
}
|
||||
|
||||
function DialogOverlay({
|
||||
|
|
@ -39,11 +39,11 @@ function DialogOverlay({
|
|||
data-slot="dialog-overlay"
|
||||
className={cn(
|
||||
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function DialogContent({
|
||||
|
|
@ -52,7 +52,7 @@ function DialogContent({
|
|||
showCloseButton = true,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Content> & {
|
||||
showCloseButton?: boolean
|
||||
showCloseButton?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<DialogPortal data-slot="dialog-portal">
|
||||
|
|
@ -61,7 +61,7 @@ function DialogContent({
|
|||
data-slot="dialog-content"
|
||||
className={cn(
|
||||
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
|
|
@ -77,7 +77,7 @@ function DialogContent({
|
|||
)}
|
||||
</DialogPrimitive.Content>
|
||||
</DialogPortal>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||
|
|
@ -87,7 +87,7 @@ function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
|
|||
className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function DialogFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||
|
|
@ -96,11 +96,11 @@ function DialogFooter({ className, ...props }: React.ComponentProps<"div">) {
|
|||
data-slot="dialog-footer"
|
||||
className={cn(
|
||||
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function DialogTitle({
|
||||
|
|
@ -113,7 +113,7 @@ function DialogTitle({
|
|||
className={cn("text-lg leading-none font-semibold", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function DialogDescription({
|
||||
|
|
@ -126,7 +126,7 @@ function DialogDescription({
|
|||
className={cn("text-muted-foreground text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
|
|
@ -140,4 +140,4 @@ export {
|
|||
DialogPortal,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { createFlowDiagram, FlowStep } from '@/utils/asciiBuilder';
|
||||
import { createFlowDiagram, FlowStep } from "@/utils/asciiBuilder";
|
||||
|
||||
/**
|
||||
* Easy-to-use diagram templates that automatically handle spacing
|
||||
|
|
@ -8,9 +8,13 @@ import { createFlowDiagram, FlowStep } from '@/utils/asciiBuilder';
|
|||
// Example: Simple 3-step process
|
||||
export const createSimpleProcess = (steps: string[]) => {
|
||||
return createFlowDiagram({
|
||||
title: 'Process Flow',
|
||||
title: "Process Flow",
|
||||
width: 60,
|
||||
steps: steps.map(label => ({ label, type: 'regular' as const, shadow: true }))
|
||||
steps: steps.map((label) => ({
|
||||
label,
|
||||
type: "regular" as const,
|
||||
shadow: true,
|
||||
})),
|
||||
});
|
||||
};
|
||||
|
||||
|
|
@ -18,39 +22,38 @@ export const createSimpleProcess = (steps: string[]) => {
|
|||
export const createNestedDiagram = (
|
||||
title: string,
|
||||
innerContent: FlowStep[],
|
||||
width: number = 70
|
||||
width: number = 70,
|
||||
) => {
|
||||
return createFlowDiagram({
|
||||
title,
|
||||
width,
|
||||
steps: innerContent
|
||||
steps: innerContent,
|
||||
});
|
||||
};
|
||||
|
||||
// Pre-built templates
|
||||
export const templates = {
|
||||
simpleFlow: createSimpleProcess(['Start', 'Process', 'End']),
|
||||
|
||||
simpleFlow: createSimpleProcess(["Start", "Process", "End"]),
|
||||
|
||||
apiFlow: createFlowDiagram({
|
||||
title: 'API Request Flow',
|
||||
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 }
|
||||
]
|
||||
{ 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',
|
||||
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 }
|
||||
]
|
||||
})
|
||||
{ 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 },
|
||||
],
|
||||
}),
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
/**
|
||||
* Programmatic ASCII Diagram Builder
|
||||
*
|
||||
*
|
||||
* For non-coders: Define your diagram structure with simple objects,
|
||||
* and the system will automatically generate the ASCII art.
|
||||
*/
|
||||
|
|
@ -8,14 +8,14 @@
|
|||
interface DiagramStep {
|
||||
id: string;
|
||||
label: string;
|
||||
type: 'input' | 'inner' | 'regular';
|
||||
type: "input" | "inner" | "regular";
|
||||
position: { x: number; y: number };
|
||||
}
|
||||
|
||||
interface DiagramFlow {
|
||||
from: string;
|
||||
to: string;
|
||||
arrow: 'right' | 'down' | 'left' | 'up';
|
||||
arrow: "right" | "down" | "left" | "up";
|
||||
label?: string;
|
||||
}
|
||||
|
||||
|
|
@ -27,23 +27,38 @@ interface DiagramConfig {
|
|||
|
||||
// Example: Define diagram using simple objects
|
||||
export const myFlow: DiagramConfig = {
|
||||
title: 'User Registration Flow',
|
||||
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 } },
|
||||
{ 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' },
|
||||
{ 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);
|
||||
|
|
@ -51,22 +66,17 @@ export const myFlow: DiagramConfig = {
|
|||
export const buildDiagram = (config: DiagramConfig): string => {
|
||||
// This function would programmatically build the ASCII
|
||||
// For now, return a placeholder
|
||||
let result = '';
|
||||
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;
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -82,4 +82,3 @@ export const diagrams = {
|
|||
};
|
||||
|
||||
export type DiagramKey = keyof typeof diagrams;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { clsx, type ClassValue } from "clsx"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
import { clsx, type ClassValue } from "clsx";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
|
@ -14,14 +14,20 @@ interface BoxDimensions {
|
|||
/**
|
||||
* Calculates proper padding to center content within a container width
|
||||
*/
|
||||
export function calculateCenterPadding(contentWidth: number, containerWidth: number): number {
|
||||
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 {
|
||||
export function createArrow(
|
||||
length: number,
|
||||
direction: "→" | "↓" | "↑" | "←" = "→",
|
||||
): string {
|
||||
return direction.repeat(length);
|
||||
}
|
||||
|
||||
|
|
@ -30,33 +36,34 @@ export function createArrow(length: number, direction: '→' | '↓' | '↑' | '
|
|||
*/
|
||||
export function buildBox(
|
||||
label: string,
|
||||
type: 'container' | 'inner' | 'regular' = 'regular',
|
||||
type: "container" | "inner" | "regular" = "regular",
|
||||
shadow: boolean = true,
|
||||
width?: number
|
||||
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 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: '│' }
|
||||
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 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 : '')
|
||||
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));
|
||||
lines.push(" " + shadowChar.repeat(actualWidth));
|
||||
}
|
||||
|
||||
|
||||
return lines;
|
||||
}
|
||||
|
||||
|
|
@ -64,79 +71,83 @@ export function buildBox(
|
|||
* Fixes spacing in an existing diagram by analyzing and adjusting alignment
|
||||
*/
|
||||
export function fixDiagramSpacing(diagram: string): string {
|
||||
const lines = diagram.split('\n');
|
||||
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('╗')) {
|
||||
if (line.includes("╔═") && line.includes("╗")) {
|
||||
containerStart = i;
|
||||
containerWidth = line.length;
|
||||
}
|
||||
if (line.includes('╚') && line.includes('╝')) {
|
||||
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;
|
||||
|
||||
|
||||
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('║')) {
|
||||
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');
|
||||
|
||||
return fixedLines.join("\n");
|
||||
}
|
||||
|
||||
function extractContainerContent(line: string): string {
|
||||
// Extract content between ║ characters
|
||||
const startIdx = line.indexOf('║');
|
||||
const endIdx = line.lastIndexOf('║');
|
||||
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);
|
||||
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 + '░';
|
||||
const rightPad = rightPadding > 0 ? " ".repeat(rightPadding) : "";
|
||||
|
||||
return padding + "║" + content + "║" + rightPad + "░";
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -156,7 +167,7 @@ function padContainerLine(content: string, containerWidth: number, targetLeftPad
|
|||
*/
|
||||
export interface FlowStep {
|
||||
label: string;
|
||||
type?: 'container' | 'inner' | 'regular';
|
||||
type?: "container" | "inner" | "regular";
|
||||
shadow?: boolean;
|
||||
}
|
||||
|
||||
|
|
@ -164,14 +175,14 @@ export interface FlowDiagramConfig {
|
|||
title: string;
|
||||
width?: number;
|
||||
steps: FlowStep[];
|
||||
layout?: 'vertical' | 'horizontal';
|
||||
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') {
|
||||
const layout = config.layout || "vertical";
|
||||
|
||||
if (layout === "horizontal") {
|
||||
return createHorizontalFlow(config);
|
||||
} else {
|
||||
return createVerticalFlow(config);
|
||||
|
|
@ -180,200 +191,234 @@ export function createFlowDiagram(config: FlowDiagramConfig): string {
|
|||
|
||||
function createVerticalFlow(config: FlowDiagramConfig): string {
|
||||
const width = config.width || 60;
|
||||
const hasExternal = config.externalElements && config.externalElements.length > 0;
|
||||
|
||||
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);
|
||||
|
||||
const extBoxLines = buildBox(
|
||||
extEl.label,
|
||||
extEl.type || "regular",
|
||||
extEl.shadow !== false,
|
||||
extWidth,
|
||||
);
|
||||
|
||||
for (const extLine of extBoxLines) {
|
||||
externalBoxes.push(' '.repeat(2) + extLine);
|
||||
externalBoxes.push(" ".repeat(2) + extLine);
|
||||
}
|
||||
|
||||
|
||||
// Add vertical arrow if not last
|
||||
if (extEl !== config.externalElements![config.externalElements!.length - 1]) {
|
||||
if (
|
||||
extEl !== config.externalElements![config.externalElements!.length - 1]
|
||||
) {
|
||||
const arrowPad = 2 + Math.floor(extWidth / 2);
|
||||
externalBoxes.push(' '.repeat(arrowPad) + '▼');
|
||||
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 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 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);
|
||||
|
||||
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);
|
||||
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) + '▼');
|
||||
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 = '';
|
||||
|
||||
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) + '─▶║─';
|
||||
line += "░".repeat(6) + "─".repeat(10) + "─▶║─";
|
||||
} else {
|
||||
line += '░'.repeat(6) + ' '.repeat(10) + ' ║ ';
|
||||
line += "░".repeat(6) + " ".repeat(10) + " ║ ";
|
||||
}
|
||||
} else if (hasExternal) {
|
||||
line += ' '.repeat(externalWidth);
|
||||
line += " ".repeat(externalWidth);
|
||||
if (row < internalLines.length) {
|
||||
line += '░'.repeat(6) + ' '.repeat(10) + ' ║ ';
|
||||
line += "░".repeat(6) + " ".repeat(10) + " ║ ";
|
||||
}
|
||||
} else {
|
||||
line += ' '.repeat(externalWidth);
|
||||
line += " ".repeat(externalWidth);
|
||||
}
|
||||
|
||||
|
||||
// Internal container part
|
||||
if (row < internalLines.length) {
|
||||
line += internalLines[row];
|
||||
} else {
|
||||
line += ' '.repeat(width);
|
||||
line += " ".repeat(width);
|
||||
}
|
||||
|
||||
line += '║░';
|
||||
|
||||
line += "║░";
|
||||
lines.push(line);
|
||||
}
|
||||
|
||||
|
||||
// Close container
|
||||
const bottomPadding = hasExternal ? ' '.repeat(externalWidth + 16) : '';
|
||||
const bottomLine = bottomPadding + '╚' + '═'.repeat(width - 1) + '╝░';
|
||||
const bottomPadding = hasExternal ? " ".repeat(externalWidth + 16) : "";
|
||||
const bottomLine = bottomPadding + "╚" + "═".repeat(width - 1) + "╝░";
|
||||
lines.push(bottomLine);
|
||||
|
||||
const shadowLine = (hasExternal ? ' '.repeat(externalWidth + 17) : ' ') + '░'.repeat(width);
|
||||
|
||||
const shadowLine =
|
||||
(hasExternal ? " ".repeat(externalWidth + 17) : " ") + "░".repeat(width);
|
||||
lines.push(shadowLine);
|
||||
|
||||
return lines.join('\n');
|
||||
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
function createHorizontalFlow(config: FlowDiagramConfig): string {
|
||||
const width = config.width || 70;
|
||||
const hasExternal = config.externalElements && config.externalElements.length > 0;
|
||||
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 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));
|
||||
|
||||
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);
|
||||
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)}╗`;
|
||||
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);
|
||||
externalBoxLines = buildBox(
|
||||
extEl.label,
|
||||
extEl.type || "regular",
|
||||
extEl.shadow !== false,
|
||||
extWidth,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
// Render content rows
|
||||
for (let row = 0; row < maxHeight; row++) {
|
||||
let line = '';
|
||||
|
||||
let line = "";
|
||||
|
||||
// External elements on left (if present)
|
||||
if (hasExternal) {
|
||||
const extRow = row < externalBoxLines.length ? row : -1;
|
||||
|
||||
|
||||
if (extRow >= 0) {
|
||||
line += ' ' + externalBoxLines[extRow];
|
||||
|
||||
line += " " + externalBoxLines[extRow];
|
||||
|
||||
// Add connecting arrow on middle row
|
||||
if (row === Math.floor(externalBoxLines.length / 2)) {
|
||||
line += '░'.repeat(5) + '─'.repeat(8) + '─▶║─';
|
||||
line += "░".repeat(5) + "─".repeat(8) + "─▶║─";
|
||||
} else {
|
||||
line += '░'.repeat(5) + ' '.repeat(8) + ' ║ ';
|
||||
line += "░".repeat(5) + " ".repeat(8) + " ║ ";
|
||||
}
|
||||
} else {
|
||||
line += ' '.repeat(26) + '║ ';
|
||||
line += " ".repeat(26) + "║ ";
|
||||
}
|
||||
} else {
|
||||
line += ' '.repeat(26) + '║ ';
|
||||
line += " ".repeat(26) + "║ ";
|
||||
}
|
||||
|
||||
|
||||
// Internal container boxes with proper padding
|
||||
line += ' '.repeat(containerPadding);
|
||||
|
||||
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));
|
||||
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) + '►';
|
||||
line += "─".repeat(arrowGap) + "►";
|
||||
} else {
|
||||
line += ' '.repeat(arrowGap + 1);
|
||||
line += " ".repeat(arrowGap + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Right padding and border
|
||||
const usedWidth = containerPadding + totalStepWidth;
|
||||
const rightPad = Math.max(0, width - usedWidth);
|
||||
line += ' '.repeat(rightPad);
|
||||
line += '║░';
|
||||
line += " ".repeat(rightPad);
|
||||
line += "║░";
|
||||
lines.push(line);
|
||||
}
|
||||
|
||||
|
||||
// Close container
|
||||
const bottomLine = ' '.repeat(26) + '╚' + '═'.repeat(width - 1) + '╝░';
|
||||
const bottomLine = " ".repeat(26) + "╚" + "═".repeat(width - 1) + "╝░";
|
||||
lines.push(bottomLine);
|
||||
const shadowLine = ' '.repeat(27) + '░'.repeat(width);
|
||||
const shadowLine = " ".repeat(27) + "░".repeat(width);
|
||||
lines.push(shadowLine);
|
||||
|
||||
return lines.join('\n');
|
||||
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue