Skip to content
12 changes: 11 additions & 1 deletion apps/code/src/components/profile/ProfileView.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"use client";

import { Calendar, Coins, Flame, Github, Hash, Zap } from "lucide-react";
import dynamic from "next/dynamic";
import Link from "next/link";

import {
Expand All @@ -14,7 +15,6 @@ import {
ProfileShareActions,
XIcon,
} from "@/components/profile/ProfileShareActions";
import { ProfileTokensChart } from "@/components/profile/ProfileTokensChart";
import { ProfileViewerCta } from "@/components/profile/ProfileViewerCta";
import { ProfileWrapped } from "@/components/profile/ProfileWrapped";
import { useAppConfig } from "@/lib/config";
Expand All @@ -27,6 +27,16 @@ import type { paths } from "@/lib/api/v1";
export type ProfileData =
paths["/user/profile"]["get"]["responses"][200]["content"]["application/json"]["profile"];

// The tokens chart pulls in recharts and renders below the fold, so keep it
// out of the profile page's initial bundle.
const ProfileTokensChart = dynamic(
() =>
import("@/components/profile/ProfileTokensChart").then(
(mod) => mod.ProfileTokensChart,
),
{ ssr: false, loading: () => <div className="h-52 w-full" /> },
);
Comment on lines +30 to +38

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Resolve the shared dynamic-import policy violation.

Both changed files use dynamic imports despite the repository rule that forbids them.

  • apps/code/src/components/profile/ProfileView.tsx#L30-L38: restore the top-level ProfileTokensChart import, or approve a documented exception.
  • apps/code/src/lib/utils/markdown.tsx#L1-L7: restore the top-level SyntaxHighlightedPre import, or approve a documented exception.

Verify the changed files with:

#!/bin/bash
set -euo pipefail

if rg -n 'next/dynamic|import\s*\(' \
  apps/code/src/components/profile/ProfileView.tsx \
  apps/code/src/lib/utils/markdown.tsx
then
	echo "Dynamic imports remain in files covered by the prohibition." >&2
	exit 1
fi

As per coding guidelines: use top-level import statements and never use dynamic imports.

📍 Affects 2 files
  • apps/code/src/components/profile/ProfileView.tsx#L30-L38 (this comment)
  • apps/code/src/lib/utils/markdown.tsx#L1-L7
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/code/src/components/profile/ProfileView.tsx` around lines 30 - 38,
Remove the dynamic import in ProfileView.tsx at lines 30-38 and restore a
top-level ProfileTokensChart import. Also remove the dynamic import in
markdown.tsx at lines 1-7 and restore a top-level SyntaxHighlightedPre import;
no direct change is needed elsewhere. Ensure neither file contains next/dynamic
or import() calls.


const AGENT_BY_SOURCE = new Map<string, AgentDefinition>();
for (const agent of AGENTS) {
for (const source of agent.sources) {
Expand Down
37 changes: 18 additions & 19 deletions apps/code/src/components/providers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,12 @@ import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
import { ThemeProvider } from "next-themes";
import posthog from "posthog-js";
import { PostHogProvider } from "posthog-js/react";
import { useEffect, useMemo } from "react";
import { useEffect, useState } from "react";

import { Toaster } from "@/components/ui/sonner";
import { AppConfigProvider } from "@/lib/config";

import type { AppConfig } from "@/lib/config-server";
import type { PostHogConfig } from "posthog-js";
import type { ReactNode } from "react";

interface ProvidersProps {
Expand All @@ -20,7 +19,9 @@ interface ProvidersProps {
}

export function Providers({ children, config }: ProvidersProps) {
const queryClient = useMemo(
// useState, not useMemo: React may discard a useMemo cache, which would
// silently swap in a fresh QueryClient and drop the whole query cache.
const [queryClient] = useState(
() =>
new QueryClient({
defaultOptions: {
Expand All @@ -31,29 +32,27 @@ export function Providers({ children, config }: ProvidersProps) {
},
},
}),
[],
);

const posthogOptions = useMemo<Partial<PostHogConfig>>(
() => ({
// Ingest through our own origin (see the /ingest rewrites in
// next.config.ts) so ad blockers that block *.posthog.com don't
// silently drop client events.
api_host: "/ingest",
ui_host: config.posthogHost,
capture_pageview: "history_change",
autocapture: true,
}),
[config.posthogHost],
);

useEffect(() => {
if (!config.posthogKey) {
return;
}
const key = config.posthogKey;
const host = config.posthogHost;
// Built inline and keyed on primitives: a memoized options object that
// React discards would re-run this effect, and its cleanup would cancel
// the already-queued init.
const init = () => {
posthog.init(key, posthogOptions);
posthog.init(key, {
// Ingest through our own origin (see the /ingest rewrites in
// next.config.ts) so ad blockers that block *.posthog.com don't
// silently drop client events.
api_host: "/ingest",
ui_host: host,
capture_pageview: "history_change",
autocapture: true,
});
};
// Captures fired before init() are dropped by posthog-js, so the idle
// deferral must be bounded — a busy main thread can starve
Expand All @@ -64,7 +63,7 @@ export function Providers({ children, config }: ProvidersProps) {
}
const timer = setTimeout(init, 300);
return () => clearTimeout(timer);
}, [config.posthogKey, posthogOptions]);
}, [config.posthogKey, config.posthogHost]);

return (
<AppConfigProvider config={config}>
Expand Down
8 changes: 7 additions & 1 deletion apps/code/src/lib/utils/markdown.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
import { SyntaxHighlightedPre } from "./markdown-code-block";
import dynamic from "next/dynamic";

// Defer the prism-based highlighter chunk until a markdown document actually
// renders a fenced code block.
const SyntaxHighlightedPre = dynamic(() =>
import("./markdown-code-block").then((mod) => mod.SyntaxHighlightedPre),
);

// Get markdown component options with custom styling
export function getMarkdownOptions() {
Expand Down
11 changes: 7 additions & 4 deletions apps/docs/app/(home)/[[...slug]]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import {
DocsTitle,
} from "fumadocs-ui/page";
import { notFound } from "next/navigation";
import posthog from "posthog-js";

import { LLMCopyButton, ViewOptions } from "@/components/ai/page-actions";
import { EnterpriseCTA } from "@/components/enterprise-cta";
Expand Down Expand Up @@ -90,6 +89,11 @@ export default async function Page(props: {
// Ignore errors (rate limits, network issues, missing auth in Docker builds)
}

// Left undefined when the GitHub lookup fails: fumadocs then omits the line
// entirely, which is better than stamping the build date on a page that was
// not edited — "Last updated" must only ever reflect a real content edit.
const lastUpdate = time ? new Date(time) : undefined;

const MDXContent = page.data.body;

const path = page.url === "/" ? "" : page.url;
Expand Down Expand Up @@ -120,7 +124,7 @@ export default async function Page(props: {
style: "clerk",
footer: <EnterpriseCTA />,
}}
lastUpdate={time ? new Date(time) : new Date()}
lastUpdate={lastUpdate}
>
<JsonLd data={techArticleSchema} />
<nav
Expand Down Expand Up @@ -150,9 +154,8 @@ export default async function Page(props: {
/>
</DocsBody>
<Feedback
onRateAction={async (url, feedback) => {
onRateAction={async (url) => {
"use server";
posthog.capture("on_rate_docs", feedback);
return await Promise.resolve({
githubUrl: `https://github.com/theopenco/llmgateway/blob/main/apps/docs/content${url}.mdx`,
});
Expand Down
25 changes: 2 additions & 23 deletions apps/docs/app/(home)/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,7 @@
import { buttonVariants } from "fumadocs-ui/components/ui/button";
import { DocsLayout } from "fumadocs-ui/layouts/docs";
import { MessageCircleIcon } from "lucide-react";

import { baseOptions } from "@/app/layout.config";
import {
AISearch,
AISearchPanel,
AISearchTrigger,
} from "@/components/ai/search";
import { cn } from "@/lib/cn";
import { AskAI } from "@/components/ai/ask-ai";
import { source } from "@/lib/source";

import type { DocsLayoutProps } from "fumadocs-ui/layouts/docs";
Expand Down Expand Up @@ -42,21 +35,7 @@ const docsOptions: DocsLayoutProps = {
export default function Layout({ children }: { children: ReactNode }) {
return (
<DocsLayout {...docsOptions}>
<AISearch>
<AISearchPanel />
<AISearchTrigger
position="float"
className={cn(
buttonVariants({
color: "secondary",
className: "text-fd-muted-foreground rounded-2xl",
}),
)}
>
<MessageCircleIcon className="size-4.5" />
Ask AI
</AISearchTrigger>
</AISearch>
<AskAI />
{children}
</DocsLayout>
);
Expand Down
68 changes: 68 additions & 0 deletions apps/docs/components/ai/ask-ai.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
"use client";
import { buttonVariants } from "fumadocs-ui/components/ui/button";
import { MessageCircleIcon } from "lucide-react";
import dynamic from "next/dynamic";
import { useCallback, useEffect, useEffectEvent, useState } from "react";

import { cn } from "@/lib/cn";

// The panel module pulls in @ai-sdk/react, its transport and the remark/shiki
// markdown pipeline. Loading it on demand keeps all of that out of the initial
// bundle of every docs page; the trigger below stays server-rendered so the
// button never pops in after hydration.
const AISearchPanel = dynamic(() => import("./search"), {
loading: () => null,
});
Comment on lines +13 to +15

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Remove the disallowed dynamic import.

Line 13 invokes import("./search") through next/dynamic. The TypeScript policy prohibits dynamic imports. Use a top-level AISearchPanel import. If deferred loading is required, obtain a scoped policy exception before merge.

Proposed policy-compliant change
-import dynamic from "next/dynamic";
+import AISearchPanel from "./search";
...
-const AISearchPanel = dynamic(() => import("./search"), {
-	loading: () => null,
-});
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/docs/components/ai/ask-ai.tsx` around lines 13 - 15, Replace the dynamic
AISearchPanel definition using next/dynamic and import("./search") with a
top-level static import of AISearchPanel from "./search"; preserve the
component’s existing usage and remove the unnecessary loading configuration.

Source: Coding guidelines


export function AskAI() {
const [open, setOpen] = useState(false);
// Once opened, the panel stays mounted so the conversation survives closing
// and reopening — the chat state lives inside the lazily loaded module.
const [hasOpened, setHasOpened] = useState(false);

const toggle = useCallback((next: boolean) => {
if (next) {
setHasOpened(true);
}
setOpen(next);
}, []);

const onKeyPress = useEffectEvent((e: KeyboardEvent) => {
if (e.key === "Escape" && open) {
toggle(false);
e.preventDefault();
}

if (e.key === "/" && (e.metaKey || e.ctrlKey) && !open) {
toggle(true);
e.preventDefault();
}
});

useEffect(() => {
window.addEventListener("keydown", onKeyPress);
return () => window.removeEventListener("keydown", onKeyPress);
}, []);

return (
<>
{hasOpened && <AISearchPanel open={open} setOpen={toggle} />}
<button
type="button"
data-state={open ? "open" : "closed"}
className={cn(
buttonVariants({
color: "secondary",
className: "text-fd-muted-foreground rounded-2xl",
}),
"fixed bottom-4 gap-3 w-24 inset-e-[calc(--spacing(4)+var(--removed-body-scroll-bar-size,0px))] shadow-lg z-20 transition-[translate,opacity]",
open && "translate-y-10 opacity-0",
)}
onClick={() => toggle(!open)}
>
<MessageCircleIcon className="size-4.5" />
Ask AI
</button>
</>
);
}
75 changes: 13 additions & 62 deletions apps/docs/components/ai/search.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,9 @@ import {
import {
type ComponentProps,
createContext,
type ReactNode,
type SyntheticEvent,
use,
useEffect,
useEffectEvent,
useMemo,
useRef,
useState,
Expand Down Expand Up @@ -349,8 +347,16 @@ function Message({
);
}

export function AISearch({ children }: { children: ReactNode }) {
const [open, setOpen] = useState(false);
// Default export so ask-ai.tsx can lazily import this module: it drags in the
// AI SDK, its transport and the remark/shiki markdown pipeline, none of which a
// reader who never opens Ask AI should have to download.
export default function AISearchPanel({
open,
setOpen,
}: {
open: boolean;
setOpen: (open: boolean) => void;
}) {
const chat = useChat<ChatUIMessage>({
id: "search",
transport: new DefaultChatTransport({
Expand All @@ -359,43 +365,9 @@ export function AISearch({ children }: { children: ReactNode }) {
});

return (
<Context value={useMemo(() => ({ chat, open, setOpen }), [chat, open])}>
{children}
</Context>
);
}

export function AISearchTrigger({
position = "default",
className,
...props
}: ComponentProps<"button"> & { position?: "default" | "float" }) {
const { open, setOpen } = useAISearchContext();

return (
<button
data-state={open ? "open" : "closed"}
className={cn(
position === "float" && [
"fixed bottom-4 gap-3 w-24 inset-e-[calc(--spacing(4)+var(--removed-body-scroll-bar-size,0px))] shadow-lg z-20 transition-[translate,opacity]",
open && "translate-y-10 opacity-0",
],
className,
)}
onClick={() => setOpen(!open)}
{...props}
<Context
value={useMemo(() => ({ chat, open, setOpen }), [chat, open, setOpen])}
>
{props.children}
</button>
);
}

export function AISearchPanel() {
const { open, setOpen } = useAISearchContext();
useHotKey();

return (
<>
<style>
{`
@keyframes ask-ai-open {
Expand Down Expand Up @@ -447,7 +419,7 @@ export function AISearchPanel() {
</div>
</div>
</Presence>
</>
</Context>
);
}

Expand Down Expand Up @@ -493,27 +465,6 @@ export function AISearchPanelList({
);
}

export function useHotKey() {
const { open, setOpen } = useAISearchContext();

const onKeyPress = useEffectEvent((e: KeyboardEvent) => {
if (e.key === "Escape" && open) {
setOpen(false);
e.preventDefault();
}

if (e.key === "/" && (e.metaKey || e.ctrlKey) && !open) {
setOpen(true);
e.preventDefault();
}
});

useEffect(() => {
window.addEventListener("keydown", onKeyPress);
return () => window.removeEventListener("keydown", onKeyPress);
}, []);
}

export function useAISearchContext() {
return use(Context)!;
}
Expand Down
Loading
Loading