diff --git a/docs/site/app/[lang]/docs/[[...slug]]/page.tsx b/docs/site/app/[lang]/docs/[[...slug]]/page.tsx index cd60cc3ffb1d9..37feb1c8f8b2c 100644 --- a/docs/site/app/[lang]/docs/[[...slug]]/page.tsx +++ b/docs/site/app/[lang]/docs/[[...slug]]/page.tsx @@ -15,6 +15,7 @@ import { getMDXComponents } from "@/components/geistdocs/mdx-components"; import { OpenInChat } from "@/components/geistdocs/open-in-chat"; import { RemoteCacheCounter } from "@/components/remote-cache-counter"; import { ScrollTop } from "@/components/geistdocs/scroll-top"; + import { Separator } from "@/components/ui/separator"; import { getLLMText, getPageImage, source } from "@/lib/geistdocs/source"; diff --git a/docs/site/components/ai-elements/prompt-input.tsx b/docs/site/components/ai-elements/prompt-input.tsx index d6fdfa9d0e239..77807bef67286 100644 --- a/docs/site/components/ai-elements/prompt-input.tsx +++ b/docs/site/components/ai-elements/prompt-input.tsx @@ -984,6 +984,7 @@ export const PromptInputActionMenuItem = ({ export type PromptInputSubmitProps = ComponentProps & { status?: ChatStatus; + onStop?: () => void; }; export const PromptInputSubmit = ({ @@ -991,6 +992,7 @@ export const PromptInputSubmit = ({ variant = "default", size = "icon-sm", status, + onStop, children, ...props }: PromptInputSubmitProps) => { @@ -1004,12 +1006,22 @@ export const PromptInputSubmit = ({ Icon = ; } + const isStreaming = status === "streaming" || status === "submitted"; + + const handleClick = (e: React.MouseEvent) => { + if (isStreaming && onStop) { + e.preventDefault(); + onStop(); + } + }; + return ( diff --git a/docs/site/components/geistdocs/chat.tsx b/docs/site/components/geistdocs/chat.tsx index af24f606a1e1f..b402bc6ed5e5c 100644 --- a/docs/site/components/geistdocs/chat.tsx +++ b/docs/site/components/geistdocs/chat.tsx @@ -129,7 +129,7 @@ const ChatInner = ({ basePath, suggestions }: ChatProps) => { const { initialMessages, isLoading, saveMessages, clearMessages } = useChatPersistence(); - const { messages, sendMessage, status, setMessages } = useChat({ + const { messages, sendMessage, status, setMessages, stop } = useChat({ transport: new DefaultChatTransport({ api: basePath ? `${basePath}/api/chat` : "/api/chat" }), @@ -188,6 +188,8 @@ const ChatInner = ({ basePath, suggestions }: ChatProps) => { const handleClearChat = async () => { try { + // Cancel any active stream first + stop(); await clearMessages(); setMessages([]); toast.success("Chat history cleared"); @@ -342,7 +344,7 @@ const ChatInner = ({ basePath, suggestions }: ChatProps) => {

{localPrompt.length} / 1000

- + diff --git a/docs/site/components/geistdocs/sidebar.tsx b/docs/site/components/geistdocs/sidebar.tsx index 988e1cea1eef1..6ce9a62064b55 100644 --- a/docs/site/components/geistdocs/sidebar.tsx +++ b/docs/site/components/geistdocs/sidebar.tsx @@ -25,6 +25,7 @@ import { import { github, nav } from "@/geistdocs"; import { useSidebarContext } from "@/hooks/geistdocs/use-sidebar"; import { SearchButton } from "./search"; +import { VersionWarning } from "@/components/version-warning"; export const Sidebar = () => { const { root } = useTreeContext(); @@ -55,6 +56,7 @@ export const Sidebar = () => { data-sidebar-placeholder >
+ {renderSidebarList(root.children)}
@@ -104,7 +106,10 @@ export const Sidebar = () => { ) : null} -
{renderSidebarList(root.children)}
+
+ + {renderSidebarList(root.children)} +
diff --git a/docs/site/components/version-warning.tsx b/docs/site/components/version-warning.tsx new file mode 100644 index 0000000000000..29fdb790f0da9 --- /dev/null +++ b/docs/site/components/version-warning.tsx @@ -0,0 +1,90 @@ +"use client"; + +import { TriangleAlert } from "lucide-react"; +import Link from "next/link"; +import { useEffect, useState } from "react"; + +const PRODUCTION_DOMAIN = "turborepo.dev"; +const NPM_REGISTRY_URL = "https://registry.npmjs.org/turbo/latest"; + +/** + * Convert subdomain format to semver for comparison. + * Subdomain format: "v2-3-1" -> "2.3.1" + */ +function subdomainToSemver(subdomain: string): string { + return subdomain.replace(/^v/, "").replace(/-/g, "."); +} + +/** + * Compare two semver strings. + * Returns true if `a` is older than `b`. + */ +function isOlderVersion(a: string, b: string): boolean { + const aParts = a.split(".").map(Number); + const bParts = b.split(".").map(Number); + + for (let i = 0; i < Math.max(aParts.length, bParts.length); i++) { + const aVal = aParts[i] || 0; + const bVal = bParts[i] || 0; + if (aVal < bVal) return true; + if (aVal > bVal) return false; + } + return false; +} + +export function VersionWarning() { + const [isOldVersion, setIsOldVersion] = useState(false); + const [subdomainVersion, setSubdomainVersion] = useState(""); + + useEffect(() => { + const host = window.location.host; + + // Check if we're on a subdomain of turborepo.dev (e.g., v2-3-1.turborepo.dev) + if (host === PRODUCTION_DOMAIN || !host.endsWith(`.${PRODUCTION_DOMAIN}`)) { + return; + } + + // Extract version from subdomain (e.g., "v2-3-1" from "v2-3-1.turborepo.dev") + const subdomain = host.replace(`.${PRODUCTION_DOMAIN}`, ""); + setSubdomainVersion(subdomain); + + const currentSemver = subdomainToSemver(subdomain); + + // Fetch latest version from npm to compare + fetch(NPM_REGISTRY_URL) + .then((res) => res.json()) + .then((data) => { + const latestVersion = data.version as string; + + if (isOlderVersion(currentSemver, latestVersion)) { + setIsOldVersion(true); + } + }) + .catch(() => { + // If we can't fetch npm, assume it's old to be safe + setIsOldVersion(true); + }); + }, []); + + if (!isOldVersion) { + return null; + } + + return ( +
+
+ + Old Version ({subdomainVersion}) +
+

+ You're viewing docs for an out-of-date version of Turborepo.{" "} + + View latest docs → + +

+
+ ); +}