Skip to content

Commit

Permalink
docs(quickstart): update the quickstart and add URL encoding (Copilot…
Browse files Browse the repository at this point in the history
…Kit#1277)

Signed-off-by: Tyler Slaton <tyler@copilotkit.ai>
  • Loading branch information
tylerslaton authored Jan 29, 2025
1 parent 6823c91 commit 0a06061
Show file tree
Hide file tree
Showing 33 changed files with 257 additions and 143 deletions.
5 changes: 3 additions & 2 deletions docs/components/react/examples-carousel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,15 @@ interface CarouselExample {
}

interface ExamplesCarouselProps {
id: string
examples?: CarouselExample[];
}

const badgeStyles = cn(badgeVariants({ variant: "outline" }), "bg-indigo-500 hover:bg-indigo-600 text-white no-underline focus:ring-1 focus:ring-indigo-500");

export function ExamplesCarousel({ examples = LandingExamples }: ExamplesCarouselProps) {
export function ExamplesCarousel({ id, examples = LandingExamples }: ExamplesCarouselProps) {
return (
<Tabs items={
<Tabs groupId={id} items={
examples.map((example) => {
const Icon = example.icon;
return {
Expand Down
4 changes: 2 additions & 2 deletions docs/components/react/image-and-code.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import {Tabs, Tab} from "@/components/react/tabs"
import { Frame } from "@/components/react/frame"

export function ImageAndCode({ preview, children }: { preview: string | React.ReactNode; children: React.ReactNode }) {
export function ImageAndCode({ preview, children, id }: { preview: string | React.ReactNode; children: React.ReactNode, id: string }) {
return (
<Tabs items={["Preview", "Code"]}>
<Tabs groupId={id} items={["Preview", "Code"]}>
<Tab value="Preview">
{typeof preview === "string" ?
<Frame>
Expand Down
94 changes: 72 additions & 22 deletions docs/components/react/tabs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import * as React from 'react';
import * as TabsPrimitive from '@radix-ui/react-tabs';
import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area";
import { useEffect, useState } from 'react';
import { useRouter, useSearchParams } from 'next/navigation';

interface TabProps {
value: string;
Expand All @@ -19,64 +19,105 @@ interface TabsProps {
items: (TabItem | string)[];
children: React.ReactNode;
defaultValue?: string;
groupId?: string;
groupId: string;
persist?: boolean;
}

// Global state to sync tabs with the same groupId, bit hacky
// Global state to sync tabs with the same groupId
const tabGroups: Record<string, Set<(value: string) => void>> = {};

const getStorageKey = (groupId: string) => `copilotkit-tabs-${groupId}`;

export function Tabs({ items, children, defaultValue, groupId, persist, ...props }: TabsProps) {
const router = useRouter();
const searchParams = useSearchParams();

const normalizedItems = items.map(item =>
typeof item === 'string' ? { value: item } : item
);

const [value, setValue] = useState<string>(() => {
if (persist) {
const stored = typeof window !== 'undefined' ? localStorage.getItem(`tabs-${groupId || 'default'}`) : null;
if (stored && normalizedItems.some(item => item.value === stored)) return stored;
// Initialize value from URL or default
const [value, setValue] = React.useState(() => {
// First try URL
const urlValue = searchParams.get(groupId);
if (urlValue && normalizedItems.some(item => item.value === urlValue)) {
return urlValue;
}

// Then try localStorage if persist is enabled
if (persist && typeof window !== 'undefined') {
try {
const stored = localStorage.getItem(getStorageKey(groupId));
if (stored && normalizedItems.some(item => item.value === stored)) {
return stored;
}
} catch (e) {
console.warn('Failed to read from localStorage:', e);
}
}

return defaultValue || normalizedItems[0].value;
});

useEffect(() => {
// Subscribe to group updates
React.useEffect(() => {
if (!groupId) return;

// Create a Set for this group if it doesn't exist
if (!tabGroups[groupId]) {
tabGroups[groupId] = new Set();
}

// Add this instance's setValue to the group
tabGroups[groupId].add(setValue);
// Create a setter function that updates this instance
const setter = (newValue: string) => {
setValue(newValue);
};

// Add this instance's setter to the group
tabGroups[groupId].add(setter);

return () => {
// Cleanup: remove this instance's setValue from the group
tabGroups[groupId]?.delete(setValue);
// Cleanup: remove this instance's setter from the group
tabGroups[groupId]?.delete(setter);
if (tabGroups[groupId]?.size === 0) {
delete tabGroups[groupId];
}
};
}, [groupId]);

const handleValueChange = (newValue: string) => {
// Update URL
const newParams = new URLSearchParams(searchParams.toString());
newParams.set(groupId, newValue);
router.replace(`?${newParams.toString()}`, { scroll: false });

// Update state
setValue(newValue);

// Update all other tabs in the same group
if (groupId) {
tabGroups[groupId]?.forEach(setValueFn => setValueFn(newValue));
if (groupId && tabGroups[groupId]) {
tabGroups[groupId].forEach(setter => setter(newValue));
}

// Persist to localStorage if enabled
if (persist) {
localStorage.setItem(`tabs-${groupId || 'default'}`, newValue);
// Persist if enabled
if (persist && typeof window !== 'undefined') {
try {
localStorage.setItem(getStorageKey(groupId), newValue);
} catch (e) {
console.warn('Failed to write to localStorage:', e);
}
}
};

return (
<TabsPrimitive.Root className="border rounded-md" value={value} onValueChange={handleValueChange} {...props}>
<TabsPrimitive.Root
className="border rounded-md"
value={value}
onValueChange={handleValueChange}
{...props}
>
<ScrollArea className="w-full rounded-md rounded-b-none relative bg-secondary dark:bg-secondary/40 border-b">
<TabsPrimitive.List className="px-4 py-3 flex">
<TabsPrimitive.List className="px-4 py-3 flex" role="tablist">
{normalizedItems.map((item) => (
<TabsPrimitive.Trigger
key={item.value}
Expand All @@ -87,22 +128,31 @@ export function Tabs({ items, children, defaultValue, groupId, persist, ...props
border-black/20 dark:border-gray-500/50
data-[state=active]:bg-indigo-200/80 dark:data-[state=active]:bg-indigo-800/50
data-[state=active]:border-indigo-400 dark:data-[state=active]:border-indigo-400"
role="tab"
aria-selected={value === item.value}
>
{item.icon && <span className="w-4 h-4 flex items-center justify-center">{item.icon}</span>}
{item.icon && (
<span className="w-4 h-4 flex items-center justify-center">
{item.icon}
</span>
)}
{item.value}
</TabsPrimitive.Trigger>
))}
<ScrollBar orientation="horizontal" className=""/>
</TabsPrimitive.List>
</ScrollArea>
{children}
{React.Children.map(children, (child) => {
if (!React.isValidElement(child)) return null;
return React.cloneElement(child as React.ReactElement<TabProps>);
})}
</TabsPrimitive.Root>
);
}

export function Tab({ value, children }: TabProps) {
return (
<TabsPrimitive.Content value={value} className="px-4">
<TabsPrimitive.Content value={value} className="px-4" role="tabpanel">
{children}
</TabsPrimitive.Content>
);
Expand Down
40 changes: 31 additions & 9 deletions docs/components/react/tailored-content.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
"use client";

import cn from "classnames";
import React, { useState, ReactNode } from "react";
import React, { useState, ReactNode, useEffect } from "react";
import { useRouter, useSearchParams } from "next/navigation";

type TailoredContentOptionProps = {
title: string;
description: string;
icon: ReactNode;
children: ReactNode;
id: string;
};

export function TailoredContentOption({ title, description, icon, children }: TailoredContentOptionProps) {
Expand All @@ -19,9 +21,13 @@ type TailoredContentProps = {
children: ReactNode;
className?: string;
defaultOptionIndex?: number;
id: string;
};

export function TailoredContent({ children, className, defaultOptionIndex = 0 }: TailoredContentProps) {
export function TailoredContent({ children, className, defaultOptionIndex = 0, id }: TailoredContentProps) {
const router = useRouter();
const searchParams = useSearchParams();

// Get options from children
const options = React.Children.toArray(children).filter(
(child) => React.isValidElement(child)
Expand All @@ -31,11 +37,25 @@ export function TailoredContent({ children, className, defaultOptionIndex = 0 }:
throw new Error("TailoredContent must have at least one TailoredContentOption child");
}

if (defaultOptionIndex < 0 || defaultOptionIndex >= options.length) {
throw new Error("Default option index is out of bounds");
}
// Get the option IDs for URL handling
const optionIds = options.map((option) => option.props.id);

// Initialize selected index from URL or default
const [selectedIndex, setSelectedIndex] = useState(() => {
const urlParam = searchParams.get(id);
const indexFromUrl = optionIds.indexOf(urlParam || "");
return indexFromUrl >= 0 ? indexFromUrl : defaultOptionIndex;
});

const [selectedIndex, setSelectedIndex] = useState(defaultOptionIndex);
// Update URL when selection changes
const updateSelection = (index: number) => {
const newParams = new URLSearchParams(searchParams.toString());
newParams.set(id, optionIds[index]);

// Update URL without reload
router.replace(`?${newParams.toString()}`, { scroll: false });
setSelectedIndex(index);
};

const itemCn =
"border p-4 rounded-md flex-1 flex md:block md:space-y-1 items-center md:items-start gap-4 cursor-pointer bg-white dark:bg-secondary relative overflow-hidden group transition-all";
Expand All @@ -50,10 +70,12 @@ export function TailoredContent({ children, className, defaultOptionIndex = 0 }:
<div className="flex flex-col md:flex-row gap-3 my-2 w-full">
{options.map((option, index) => (
<div
key={index}
key={option.props.id}
className={cn(itemCn, selectedIndex === index && selectedCn)}
onClick={() => setSelectedIndex(index)}
style={{ position: "relative" }}
onClick={() => updateSelection(index)}
role="tab"
aria-selected={selectedIndex === index}
tabIndex={0}
>
<div className="my-0">
{React.cloneElement(option.props.icon as React.ReactElement, {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,9 @@ import { FaCloud, FaServer } from "react-icons/fa";
<Step>
### Setup your Copilot Runtime

<TailoredContent>
<TailoredContent id="hosting">
<TailoredContentOption
id="copilot-cloud"
title="Copilot Cloud (Recommended)"
description="I'm already using or want to use Copilot Cloud."
icon={<FaCloud />}
Expand All @@ -47,6 +48,7 @@ import { FaCloud, FaServer } from "react-icons/fa";
<CopilotCloudConfigureRemoteEndpointLangGraphSnippet components={props.components} />
</TailoredContentOption>
<TailoredContentOption
id="self-hosted"
title="Self-Hosted"
description="I'm using or want to use a self-hosted Copilot Runtime."
icon={<FaServer />}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import { FaCloud, FaServer } from "react-icons/fa";
To integrate a Python backend with your CopilotKit application, set up your project and install the necessary dependencies by choosing your dependency management solution below.


<Tabs items={['Poetry', 'pip', 'conda']} default="Poetry">
<Tabs groupId="python-pm" items={['Poetry', 'pip', 'conda']} default="Poetry">
<Tab value="Poetry">

#### Initialize a New Poetry Project
Expand Down Expand Up @@ -162,7 +162,7 @@ if __name__ == "__main__":

Since we've added the entry point in `server.py`, you can run your FastAPI server directly by executing the script:

<Tabs items={['Poetry', 'pip', 'conda']} default="Poetry">
<Tabs groupId="python-pm" items={['Poetry', 'pip', 'conda']} default="Poetry">
<Tab value="Poetry">
```bash
poetry run python3 server.py
Expand Down Expand Up @@ -190,15 +190,17 @@ Since we've added the entry point in `server.py`, you can run your FastAPI serve

Now that you've set up your FastAPI server with the backend actions, integrate it into your CopilotKit application by modifying your `CopilotRuntime` configuration.

<TailoredContent>
<TailoredContent id="hosting">
<TailoredContentOption
id="copilot-cloud"
title="Copilot Cloud (Recommended)"
description="I want to use Copilot Cloud to connect to my remote endpoint."
icon={<FaCloud />}
>
<CopilotCloudConfigureRemoteEndpointSnippet components={props.components} />
</TailoredContentOption>
<TailoredContentOption
id="self-hosted"
title="Self-Hosted Copilot Runtime"
description="I want to use a self-hosted Copilot Runtime to connect to my remote endpoint."
icon={<FaServer />}
Expand Down
2 changes: 1 addition & 1 deletion docs/content/docs/(root)/guides/copilot-textarea.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ import "@copilotkit/react-textarea/styles.css";
### Add `CopilotTextarea` to Your Component
Below you can find several examples showing how to use the `CopilotTextarea` component in your application.

<Tabs items={["Example 1", "Example 2"]}>
<Tabs groupId="example" items={["Example 1", "Example 2"]}>
<Tab value="Example 1">
```tsx title="TextAreaComponent.tsx"
import { FC, useState } from "react";
Expand Down
2 changes: 1 addition & 1 deletion docs/content/docs/(root)/guides/generative-ui.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import UseClientCalloutSnippet from "@/snippets/use-client-callout.mdx";
When a user interacts with your Copilot, you may want to render a custom UI component. [`useCopilotAction`](/reference/hooks/useCopilotAction) allows to give the LLM the
option to render your custom component through the `render` property.

<Tabs items={['Render a component', 'Fetch data & render', 'renderAndWaitForResponse (HITL)', 'Render strings', 'Catch all renders']}>
<Tabs groupId="gen-ui-type" items={['Render a component', 'Fetch data & render', 'renderAndWaitForResponse (HITL)', 'Render strings', 'Catch all renders']}>

<Tab value="Render a component">
[`useCopilotAction`](/reference/hooks/useCopilotAction) can be used with a `render` function and without a `handler` to display information or UI elements within the chat.
Expand Down
2 changes: 1 addition & 1 deletion docs/content/docs/(root)/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ You can use CopilotKit in two modes: **Standard** and **CoAgents**.
Check out some things that we've built with CopilotKit!

<div className="pb-6"/>
<ExamplesCarousel />
<ExamplesCarousel id="example" />

## Built to Scale.
CopilotKit is thoughtfully architected to scale with you, your teams, and your product.
Expand Down
Loading

0 comments on commit 0a06061

Please sign in to comment.