Skip to content

Commit 7332156

Browse files
authored
Implement Sidebar (#13)
1 parent cceadc8 commit 7332156

File tree

12 files changed

+915
-244
lines changed

12 files changed

+915
-244
lines changed
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
"use client";
2+
3+
import {
4+
CopilotKitProvider,
5+
CopilotSidebar,
6+
defineToolCallRender,
7+
useConfigureSuggestions,
8+
useFrontendTool,
9+
} from "@copilotkitnext/react";
10+
import { z } from "zod";
11+
12+
export const dynamic = "force-dynamic";
13+
14+
export default function SidebarDemoPage() {
15+
const wildcardRenderer = defineToolCallRender({
16+
name: "*",
17+
render: ({ name, args, status }) => (
18+
<div className="rounded-lg border border-slate-200 bg-slate-50 p-3 text-sm text-slate-700 shadow-sm">
19+
<strong className="block text-slate-900">Unknown Tool: {name}</strong>
20+
<pre className="mt-2 whitespace-pre-wrap text-xs text-slate-600">
21+
Status: {status}
22+
{args && "\nArguments: " + JSON.stringify(args, null, 2)}
23+
</pre>
24+
</div>
25+
),
26+
});
27+
28+
return (
29+
<CopilotKitProvider runtimeUrl="/api/copilotkit" renderToolCalls={[wildcardRenderer]}>
30+
<AppLayout />
31+
</CopilotKitProvider>
32+
);
33+
}
34+
35+
function AppLayout() {
36+
return (
37+
<div className="relative min-h-screen bg-gradient-to-br from-slate-100 via-white to-slate-200">
38+
<main className="mx-auto flex w-full max-w-5xl flex-col gap-8 px-6 py-12">
39+
<section className="space-y-4">
40+
<h1 className="text-3xl font-semibold tracking-tight text-slate-900">Copilot Sidebar Demo</h1>
41+
<p className="max-w-2xl text-slate-600">
42+
This page shows the chat embedded as a right-aligned sidebar. Toggle the chat to see the main content
43+
reflow. The assistant can suggest actions and invoke custom tools just like the full-screen chat.
44+
</p>
45+
</section>
46+
47+
<section className="grid gap-6 md:grid-cols-2">
48+
{Array.from({ length: 4 }).map((_, index) => (
49+
<article
50+
key={index}
51+
className="rounded-2xl border border-slate-200 bg-white p-6 shadow-sm transition hover:shadow-md"
52+
>
53+
<h2 className="text-lg font-medium text-slate-900">Project Card {index + 1}</h2>
54+
<p className="mt-2 text-sm text-slate-600">
55+
Placeholder content to demonstrate how the sidebar pushes layout elements without overlapping the page.
56+
</p>
57+
</article>
58+
))}
59+
</section>
60+
</main>
61+
62+
<SidebarChat />
63+
</div>
64+
);
65+
}
66+
67+
function SidebarChat() {
68+
useConfigureSuggestions({
69+
instructions: "Suggest follow-up tasks based on the current page content",
70+
});
71+
72+
useFrontendTool({
73+
name: "toastNotification",
74+
parameters: z.object({
75+
message: z.string(),
76+
}),
77+
handler: async ({ message }) => {
78+
alert(`Notification: ${message}`);
79+
return `Displayed toast: ${message}`;
80+
},
81+
});
82+
83+
return <CopilotSidebar defaultOpen={true} width="50%" />;
84+
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import type { Meta, StoryObj } from "@storybook/react";
2+
import { MessageCirclePlus, Minus } from "lucide-react";
3+
import React from "react";
4+
5+
import {
6+
CopilotChatConfigurationProvider,
7+
CopilotChatToggleButton,
8+
type CopilotChatToggleButtonProps,
9+
useCopilotChatConfiguration,
10+
} from "@copilotkitnext/react";
11+
12+
const StatePreview: React.FC<CopilotChatToggleButtonProps> = (args) => {
13+
const configuration = useCopilotChatConfiguration();
14+
15+
return (
16+
<div className="flex flex-col items-center gap-3">
17+
<CopilotChatToggleButton {...args} />
18+
<span className="text-sm text-muted-foreground">
19+
{configuration?.isModalOpen ? "Chat is open" : "Chat is closed"}
20+
</span>
21+
</div>
22+
);
23+
};
24+
25+
const meta = {
26+
title: "UI/CopilotChatToggleButton",
27+
component: CopilotChatToggleButton,
28+
parameters: {
29+
layout: "centered",
30+
},
31+
render: (args) => (
32+
<CopilotChatConfigurationProvider threadId="storybook-toggle-button">
33+
<StatePreview {...args} />
34+
</CopilotChatConfigurationProvider>
35+
),
36+
} satisfies Meta<typeof CopilotChatToggleButton>;
37+
38+
export default meta;
39+
40+
type Story = StoryObj<typeof meta>;
41+
42+
export const Default: Story = {};
43+
44+
export const WithCustomIcons: Story = {
45+
args: {
46+
openIcon: (props) => (
47+
<MessageCirclePlus
48+
{...props}
49+
className={[props.className, "text-emerald-400"].filter(Boolean).join(" ")}
50+
strokeWidth={1.5}
51+
/>
52+
),
53+
closeIcon: (props) => (
54+
<Minus
55+
{...props}
56+
className={[props.className, "text-rose-400"].filter(Boolean).join(" ")}
57+
strokeWidth={2}
58+
/>
59+
),
60+
},
61+
};
62+
63+
export const Disabled: Story = {
64+
args: {
65+
disabled: true,
66+
},
67+
};
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import type { Meta, StoryObj } from "@storybook/react";
2+
import React from "react";
3+
4+
import {
5+
CopilotChatConfigurationProvider,
6+
CopilotModalHeader,
7+
CopilotSidebarView,
8+
type CopilotSidebarViewProps,
9+
} from "@copilotkitnext/react";
10+
11+
const StoryWrapper: React.FC<{ children: React.ReactNode }> = ({ children }) => (
12+
<CopilotChatConfigurationProvider threadId="story-copilot-sidebar">
13+
<div className="min-h-screen bg-background text-foreground">
14+
<div className="mx-auto grid max-w-5xl grid-cols-1 gap-6 px-6 py-10">
15+
<section className="space-y-4">
16+
<h1 className="text-3xl font-semibold tracking-tight">Project Dashboard</h1>
17+
<p className="text-muted-foreground">
18+
Toggle the assistant to draft updates, summarize discussions, and keep track of action items while you stay
19+
in context.
20+
</p>
21+
</section>
22+
23+
<div className="grid gap-4 sm:grid-cols-2">
24+
{Array.from({ length: 4 }).map((_, index) => (
25+
<article
26+
key={index}
27+
className="rounded-xl border border-border bg-card p-4 shadow-sm transition hover:shadow-md"
28+
>
29+
<h2 className="text-lg font-medium">Task {index + 1}</h2>
30+
<p className="mt-2 text-sm text-muted-foreground">
31+
Placeholder content to illustrate how the sidebar pushes the layout without overlapping.
32+
</p>
33+
</article>
34+
))}
35+
</div>
36+
</div>
37+
38+
{children}
39+
</div>
40+
</CopilotChatConfigurationProvider>
41+
);
42+
43+
const meta = {
44+
title: "UI/CopilotSidebarView",
45+
component: CopilotSidebarView,
46+
parameters: {
47+
layout: "fullscreen",
48+
},
49+
render: (args) => (
50+
<StoryWrapper>
51+
<CopilotSidebarView {...(args as CopilotSidebarViewProps)} />
52+
</StoryWrapper>
53+
),
54+
} satisfies Meta<typeof CopilotSidebarView>;
55+
56+
export default meta;
57+
58+
type Story = StoryObj<typeof meta>;
59+
60+
export const Default: Story = {
61+
args: {
62+
autoScroll: true,
63+
},
64+
};
65+
66+
export const CustomHeader: Story = {
67+
args: {
68+
header: {
69+
title: "Workspace Copilot",
70+
titleContent: (props) => (
71+
<CopilotModalHeader.Title
72+
{...props}
73+
className="text-lg font-semibold tracking-tight text-foreground"
74+
>
75+
<span>{props.children}</span>
76+
<span className="mt-1 block text-xs font-normal text-muted-foreground">
77+
Always-on teammate
78+
</span>
79+
</CopilotModalHeader.Title>
80+
),
81+
closeButton: (props) => (
82+
<CopilotModalHeader.CloseButton
83+
{...props}
84+
className="text-destructive hover:bg-destructive/10 hover:text-destructive"
85+
/>
86+
),
87+
},
88+
},
89+
};

packages/react/src/components/chat/CopilotChat.tsx

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import { CopilotChatView, CopilotChatViewProps } from "./CopilotChatView";
44
import {
55
CopilotChatConfigurationProvider,
66
CopilotChatLabels,
7-
CopilotChatDefaultLabels,
87
useCopilotChatConfiguration,
98
} from "@/providers/CopilotChatConfigurationProvider";
109
import { DEFAULT_AGENT_ID, randomUUID } from "@copilotkitnext/shared";
@@ -13,6 +12,7 @@ import { useCallback, useEffect, useMemo } from "react";
1312
import { merge } from "ts-deepmerge";
1413
import { useCopilotKit } from "@/providers/CopilotKitProvider";
1514
import { AbstractAgent, AGUIConnectNotImplementedError } from "@ag-ui/client";
15+
import { renderSlot, SlotValue } from "@/lib/slots";
1616

1717
export type CopilotChatProps = Omit<
1818
CopilotChatViewProps,
@@ -21,8 +21,10 @@ export type CopilotChatProps = Omit<
2121
agentId?: string;
2222
threadId?: string;
2323
labels?: Partial<CopilotChatLabels>;
24+
chatView?: SlotValue<typeof CopilotChatView>;
25+
isModalDefaultOpen?: boolean;
2426
};
25-
export function CopilotChat({ agentId, threadId, labels, ...props }: CopilotChatProps) {
27+
export function CopilotChat({ agentId, threadId, labels, chatView, isModalDefaultOpen, ...props }: CopilotChatProps) {
2628
// Check for existing configuration provider
2729
const existingConfig = useCopilotChatConfiguration();
2830

@@ -32,15 +34,6 @@ export function CopilotChat({ agentId, threadId, labels, ...props }: CopilotChat
3234
() => threadId ?? existingConfig?.threadId ?? randomUUID(),
3335
[threadId, existingConfig?.threadId],
3436
);
35-
const resolvedLabels: CopilotChatLabels = useMemo(
36-
() => ({
37-
...CopilotChatDefaultLabels,
38-
...(existingConfig?.labels || {}),
39-
...(labels || {}),
40-
}),
41-
[existingConfig?.labels, labels],
42-
);
43-
4437
const { agent } = useAgent({ agentId: resolvedAgentId });
4538
const { copilotkit } = useCopilotKit();
4639

@@ -138,9 +131,21 @@ export function CopilotChat({ agentId, threadId, labels, ...props }: CopilotChat
138131

139132
// Always create a provider with merged values
140133
// This ensures priority: props > existing config > defaults
134+
const RenderedChatView = renderSlot(chatView, CopilotChatView, finalProps);
135+
141136
return (
142-
<CopilotChatConfigurationProvider agentId={resolvedAgentId} threadId={resolvedThreadId} labels={resolvedLabels}>
143-
<CopilotChatView {...finalProps} />
137+
<CopilotChatConfigurationProvider
138+
agentId={resolvedAgentId}
139+
threadId={resolvedThreadId}
140+
labels={labels}
141+
isModalDefaultOpen={isModalDefaultOpen}
142+
>
143+
{RenderedChatView}
144144
</CopilotChatConfigurationProvider>
145145
);
146146
}
147+
148+
// eslint-disable-next-line @typescript-eslint/no-namespace
149+
export namespace CopilotChat {
150+
export const View = CopilotChatView;
151+
}

0 commit comments

Comments
 (0)