Skip to content

Commit 0a06061

Browse files
authored
docs(quickstart): update the quickstart and add URL encoding (CopilotKit#1277)
Signed-off-by: Tyler Slaton <tyler@copilotkit.ai>
1 parent 6823c91 commit 0a06061

33 files changed

Lines changed: 257 additions & 143 deletions

docs/components/react/examples-carousel.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,14 +25,15 @@ interface CarouselExample {
2525
}
2626

2727
interface ExamplesCarouselProps {
28+
id: string
2829
examples?: CarouselExample[];
2930
}
3031

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

33-
export function ExamplesCarousel({ examples = LandingExamples }: ExamplesCarouselProps) {
34+
export function ExamplesCarousel({ id, examples = LandingExamples }: ExamplesCarouselProps) {
3435
return (
35-
<Tabs items={
36+
<Tabs groupId={id} items={
3637
examples.map((example) => {
3738
const Icon = example.icon;
3839
return {

docs/components/react/image-and-code.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import {Tabs, Tab} from "@/components/react/tabs"
22
import { Frame } from "@/components/react/frame"
33

4-
export function ImageAndCode({ preview, children }: { preview: string | React.ReactNode; children: React.ReactNode }) {
4+
export function ImageAndCode({ preview, children, id }: { preview: string | React.ReactNode; children: React.ReactNode, id: string }) {
55
return (
6-
<Tabs items={["Preview", "Code"]}>
6+
<Tabs groupId={id} items={["Preview", "Code"]}>
77
<Tab value="Preview">
88
{typeof preview === "string" ?
99
<Frame>

docs/components/react/tabs.tsx

Lines changed: 72 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import * as React from 'react';
44
import * as TabsPrimitive from '@radix-ui/react-tabs';
55
import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area";
6-
import { useEffect, useState } from 'react';
6+
import { useRouter, useSearchParams } from 'next/navigation';
77

88
interface TabProps {
99
value: string;
@@ -19,64 +19,105 @@ interface TabsProps {
1919
items: (TabItem | string)[];
2020
children: React.ReactNode;
2121
defaultValue?: string;
22-
groupId?: string;
22+
groupId: string;
2323
persist?: boolean;
2424
}
2525

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

29+
const getStorageKey = (groupId: string) => `copilotkit-tabs-${groupId}`;
30+
2931
export function Tabs({ items, children, defaultValue, groupId, persist, ...props }: TabsProps) {
32+
const router = useRouter();
33+
const searchParams = useSearchParams();
34+
3035
const normalizedItems = items.map(item =>
3136
typeof item === 'string' ? { value: item } : item
3237
);
3338

34-
const [value, setValue] = useState<string>(() => {
35-
if (persist) {
36-
const stored = typeof window !== 'undefined' ? localStorage.getItem(`tabs-${groupId || 'default'}`) : null;
37-
if (stored && normalizedItems.some(item => item.value === stored)) return stored;
39+
// Initialize value from URL or default
40+
const [value, setValue] = React.useState(() => {
41+
// First try URL
42+
const urlValue = searchParams.get(groupId);
43+
if (urlValue && normalizedItems.some(item => item.value === urlValue)) {
44+
return urlValue;
3845
}
46+
47+
// Then try localStorage if persist is enabled
48+
if (persist && typeof window !== 'undefined') {
49+
try {
50+
const stored = localStorage.getItem(getStorageKey(groupId));
51+
if (stored && normalizedItems.some(item => item.value === stored)) {
52+
return stored;
53+
}
54+
} catch (e) {
55+
console.warn('Failed to read from localStorage:', e);
56+
}
57+
}
58+
3959
return defaultValue || normalizedItems[0].value;
4060
});
4161

42-
useEffect(() => {
62+
// Subscribe to group updates
63+
React.useEffect(() => {
4364
if (!groupId) return;
4465

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

50-
// Add this instance's setValue to the group
51-
tabGroups[groupId].add(setValue);
71+
// Create a setter function that updates this instance
72+
const setter = (newValue: string) => {
73+
setValue(newValue);
74+
};
75+
76+
// Add this instance's setter to the group
77+
tabGroups[groupId].add(setter);
5278

5379
return () => {
54-
// Cleanup: remove this instance's setValue from the group
55-
tabGroups[groupId]?.delete(setValue);
80+
// Cleanup: remove this instance's setter from the group
81+
tabGroups[groupId]?.delete(setter);
5682
if (tabGroups[groupId]?.size === 0) {
5783
delete tabGroups[groupId];
5884
}
5985
};
6086
}, [groupId]);
6187

6288
const handleValueChange = (newValue: string) => {
89+
// Update URL
90+
const newParams = new URLSearchParams(searchParams.toString());
91+
newParams.set(groupId, newValue);
92+
router.replace(`?${newParams.toString()}`, { scroll: false });
93+
94+
// Update state
6395
setValue(newValue);
6496

6597
// Update all other tabs in the same group
66-
if (groupId) {
67-
tabGroups[groupId]?.forEach(setValueFn => setValueFn(newValue));
98+
if (groupId && tabGroups[groupId]) {
99+
tabGroups[groupId].forEach(setter => setter(newValue));
68100
}
69101

70-
// Persist to localStorage if enabled
71-
if (persist) {
72-
localStorage.setItem(`tabs-${groupId || 'default'}`, newValue);
102+
// Persist if enabled
103+
if (persist && typeof window !== 'undefined') {
104+
try {
105+
localStorage.setItem(getStorageKey(groupId), newValue);
106+
} catch (e) {
107+
console.warn('Failed to write to localStorage:', e);
108+
}
73109
}
74110
};
75111

76112
return (
77-
<TabsPrimitive.Root className="border rounded-md" value={value} onValueChange={handleValueChange} {...props}>
113+
<TabsPrimitive.Root
114+
className="border rounded-md"
115+
value={value}
116+
onValueChange={handleValueChange}
117+
{...props}
118+
>
78119
<ScrollArea className="w-full rounded-md rounded-b-none relative bg-secondary dark:bg-secondary/40 border-b">
79-
<TabsPrimitive.List className="px-4 py-3 flex">
120+
<TabsPrimitive.List className="px-4 py-3 flex" role="tablist">
80121
{normalizedItems.map((item) => (
81122
<TabsPrimitive.Trigger
82123
key={item.value}
@@ -87,22 +128,31 @@ export function Tabs({ items, children, defaultValue, groupId, persist, ...props
87128
border-black/20 dark:border-gray-500/50
88129
data-[state=active]:bg-indigo-200/80 dark:data-[state=active]:bg-indigo-800/50
89130
data-[state=active]:border-indigo-400 dark:data-[state=active]:border-indigo-400"
131+
role="tab"
132+
aria-selected={value === item.value}
90133
>
91-
{item.icon && <span className="w-4 h-4 flex items-center justify-center">{item.icon}</span>}
134+
{item.icon && (
135+
<span className="w-4 h-4 flex items-center justify-center">
136+
{item.icon}
137+
</span>
138+
)}
92139
{item.value}
93140
</TabsPrimitive.Trigger>
94141
))}
95142
<ScrollBar orientation="horizontal" className=""/>
96143
</TabsPrimitive.List>
97144
</ScrollArea>
98-
{children}
145+
{React.Children.map(children, (child) => {
146+
if (!React.isValidElement(child)) return null;
147+
return React.cloneElement(child as React.ReactElement<TabProps>);
148+
})}
99149
</TabsPrimitive.Root>
100150
);
101151
}
102152

103153
export function Tab({ value, children }: TabProps) {
104154
return (
105-
<TabsPrimitive.Content value={value} className="px-4">
155+
<TabsPrimitive.Content value={value} className="px-4" role="tabpanel">
106156
{children}
107157
</TabsPrimitive.Content>
108158
);

docs/components/react/tailored-content.tsx

Lines changed: 31 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
"use client";
22

33
import cn from "classnames";
4-
import React, { useState, ReactNode } from "react";
4+
import React, { useState, ReactNode, useEffect } from "react";
5+
import { useRouter, useSearchParams } from "next/navigation";
56

67
type TailoredContentOptionProps = {
78
title: string;
89
description: string;
910
icon: ReactNode;
1011
children: ReactNode;
12+
id: string;
1113
};
1214

1315
export function TailoredContentOption({ title, description, icon, children }: TailoredContentOptionProps) {
@@ -19,9 +21,13 @@ type TailoredContentProps = {
1921
children: ReactNode;
2022
className?: string;
2123
defaultOptionIndex?: number;
24+
id: string;
2225
};
2326

24-
export function TailoredContent({ children, className, defaultOptionIndex = 0 }: TailoredContentProps) {
27+
export function TailoredContent({ children, className, defaultOptionIndex = 0, id }: TailoredContentProps) {
28+
const router = useRouter();
29+
const searchParams = useSearchParams();
30+
2531
// Get options from children
2632
const options = React.Children.toArray(children).filter(
2733
(child) => React.isValidElement(child)
@@ -31,11 +37,25 @@ export function TailoredContent({ children, className, defaultOptionIndex = 0 }:
3137
throw new Error("TailoredContent must have at least one TailoredContentOption child");
3238
}
3339

34-
if (defaultOptionIndex < 0 || defaultOptionIndex >= options.length) {
35-
throw new Error("Default option index is out of bounds");
36-
}
40+
// Get the option IDs for URL handling
41+
const optionIds = options.map((option) => option.props.id);
42+
43+
// Initialize selected index from URL or default
44+
const [selectedIndex, setSelectedIndex] = useState(() => {
45+
const urlParam = searchParams.get(id);
46+
const indexFromUrl = optionIds.indexOf(urlParam || "");
47+
return indexFromUrl >= 0 ? indexFromUrl : defaultOptionIndex;
48+
});
3749

38-
const [selectedIndex, setSelectedIndex] = useState(defaultOptionIndex);
50+
// Update URL when selection changes
51+
const updateSelection = (index: number) => {
52+
const newParams = new URLSearchParams(searchParams.toString());
53+
newParams.set(id, optionIds[index]);
54+
55+
// Update URL without reload
56+
router.replace(`?${newParams.toString()}`, { scroll: false });
57+
setSelectedIndex(index);
58+
};
3959

4060
const itemCn =
4161
"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";
@@ -50,10 +70,12 @@ export function TailoredContent({ children, className, defaultOptionIndex = 0 }:
5070
<div className="flex flex-col md:flex-row gap-3 my-2 w-full">
5171
{options.map((option, index) => (
5272
<div
53-
key={index}
73+
key={option.props.id}
5474
className={cn(itemCn, selectedIndex === index && selectedCn)}
55-
onClick={() => setSelectedIndex(index)}
56-
style={{ position: "relative" }}
75+
onClick={() => updateSelection(index)}
76+
role="tab"
77+
aria-selected={selectedIndex === index}
78+
tabIndex={0}
5779
>
5880
<div className="my-0">
5981
{React.cloneElement(option.props.icon as React.ReactElement, {

docs/content/docs/(root)/guides/backend-actions/langgraph-platform-endpoint.mdx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,9 @@ import { FaCloud, FaServer } from "react-icons/fa";
2929
<Step>
3030
### Setup your Copilot Runtime
3131

32-
<TailoredContent>
32+
<TailoredContent id="hosting">
3333
<TailoredContentOption
34+
id="copilot-cloud"
3435
title="Copilot Cloud (Recommended)"
3536
description="I'm already using or want to use Copilot Cloud."
3637
icon={<FaCloud />}
@@ -47,6 +48,7 @@ import { FaCloud, FaServer } from "react-icons/fa";
4748
<CopilotCloudConfigureRemoteEndpointLangGraphSnippet components={props.components} />
4849
</TailoredContentOption>
4950
<TailoredContentOption
51+
id="self-hosted"
5052
title="Self-Hosted"
5153
description="I'm using or want to use a self-hosted Copilot Runtime."
5254
icon={<FaServer />}

docs/content/docs/(root)/guides/backend-actions/remote-backend-endpoint.mdx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import { FaCloud, FaServer } from "react-icons/fa";
1919
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.
2020

2121

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

2525
#### Initialize a New Poetry Project
@@ -162,7 +162,7 @@ if __name__ == "__main__":
162162

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

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

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

193-
<TailoredContent>
193+
<TailoredContent id="hosting">
194194
<TailoredContentOption
195+
id="copilot-cloud"
195196
title="Copilot Cloud (Recommended)"
196197
description="I want to use Copilot Cloud to connect to my remote endpoint."
197198
icon={<FaCloud />}
198199
>
199200
<CopilotCloudConfigureRemoteEndpointSnippet components={props.components} />
200201
</TailoredContentOption>
201202
<TailoredContentOption
203+
id="self-hosted"
202204
title="Self-Hosted Copilot Runtime"
203205
description="I want to use a self-hosted Copilot Runtime to connect to my remote endpoint."
204206
icon={<FaServer />}

docs/content/docs/(root)/guides/copilot-textarea.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ import "@copilotkit/react-textarea/styles.css";
3838
### Add `CopilotTextarea` to Your Component
3939
Below you can find several examples showing how to use the `CopilotTextarea` component in your application.
4040

41-
<Tabs items={["Example 1", "Example 2"]}>
41+
<Tabs groupId="example" items={["Example 1", "Example 2"]}>
4242
<Tab value="Example 1">
4343
```tsx title="TextAreaComponent.tsx"
4444
import { FC, useState } from "react";

docs/content/docs/(root)/guides/generative-ui.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import UseClientCalloutSnippet from "@/snippets/use-client-callout.mdx";
1010
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
1111
option to render your custom component through the `render` property.
1212

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

1515
<Tab value="Render a component">
1616
[`useCopilotAction`](/reference/hooks/useCopilotAction) can be used with a `render` function and without a `handler` to display information or UI elements within the chat.

docs/content/docs/(root)/index.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ You can use CopilotKit in two modes: **Standard** and **CoAgents**.
4747
Check out some things that we've built with CopilotKit!
4848

4949
<div className="pb-6"/>
50-
<ExamplesCarousel />
50+
<ExamplesCarousel id="example" />
5151

5252
## Built to Scale.
5353
CopilotKit is thoughtfully architected to scale with you, your teams, and your product.

0 commit comments

Comments
 (0)