From a9fb37de106dee44193380cfd59e5517c6bbb494 Mon Sep 17 00:00:00 2001 From: ankit-thesys Date: Wed, 7 Jan 2026 22:19:38 +0530 Subject: [PATCH 01/41] Add CrayonChat styles and enhance BottomTray animations * Added crayonChat.scss to the main SCSS index for CrayonChat component. * Improved BottomTray animations with updated transition timings and clip-path effects for open and closed states. * Adjusted Trigger component styles for better responsiveness and interaction feedback. * Enhanced ComposedBottomTray to conditionally render the logo based on availability. --- .../src/components/BottomTray/container.scss | 22 +++++++++++++------ .../src/components/BottomTray/trigger.scss | 8 ++++++- .../CrayonChat/ComposedBottomTray.tsx | 4 +++- .../CrayonChat/composedBottomTray.scss | 8 +++++++ .../src/components/CrayonChat/crayonChat.scss | 1 + .../react-ui/src/components/index.scss | 1 + 6 files changed, 35 insertions(+), 9 deletions(-) create mode 100644 js/packages/react-ui/src/components/CrayonChat/composedBottomTray.scss diff --git a/js/packages/react-ui/src/components/BottomTray/container.scss b/js/packages/react-ui/src/components/BottomTray/container.scss index f5fa719d2..637ad6aaf 100644 --- a/js/packages/react-ui/src/components/BottomTray/container.scss +++ b/js/packages/react-ui/src/components/BottomTray/container.scss @@ -11,9 +11,13 @@ width: 448px; overflow: hidden; flex-direction: column; + + // Cone animation: expand from bottom-right (where trigger is) + transform-origin: bottom right; transition: - transform 0.3s cubic-bezier(0.4, 0, 0.2, 1), - opacity 0.3s ease; + transform 0.35s cubic-bezier(0.34, 1.56, 0.64, 1), + opacity 0.25s ease, + clip-path 0.35s cubic-bezier(0.34, 1.56, 0.64, 1); border: 1px solid cssUtils.$stroke-default; border-radius: cssUtils.$rounded-2xl cssUtils.$rounded-2xl cssUtils.$rounded-3xl @@ -26,17 +30,19 @@ box-sizing: border-box; } - // Open state + // Open state - fully expanded &--open { - transform: translateY(0); + transform: scale(1); opacity: 1; + clip-path: ellipse(150% 150% at 100% 100%); } - // Closed state + // Closed state - collapsed to trigger point with cone clip &--closed { - transform: translateY(calc(100% + cssUtils.$spacing-l)); + transform: scale(0.1); opacity: 0; pointer-events: none; + clip-path: ellipse(0% 0% at 100% 100%); } // Mobile fullscreen @@ -52,8 +58,10 @@ border-radius: 0; border: none; + // On mobile, expand from bottom-right corner where trigger was &--closed { - transform: translateY(100dvh); + transform: scale(0.1); + clip-path: ellipse(0% 0% at 100% 100%); } } } diff --git a/js/packages/react-ui/src/components/BottomTray/trigger.scss b/js/packages/react-ui/src/components/BottomTray/trigger.scss index 43b493c4b..5201bca17 100644 --- a/js/packages/react-ui/src/components/BottomTray/trigger.scss +++ b/js/packages/react-ui/src/components/BottomTray/trigger.scss @@ -14,7 +14,7 @@ color: cssUtils.$accent-primary-text; box-shadow: cssUtils.$shadow-xl; cursor: pointer; - transition: all 0.2s ease; + transition: all 0.1s ease; z-index: 1000; overflow: hidden; @@ -26,6 +26,12 @@ &:active { transform: translateY(0); + scale: 0.95; + } + + // Desktop open state - shrink the trigger + &--open { + transform: scale(0.85); } // Mobile diff --git a/js/packages/react-ui/src/components/CrayonChat/ComposedBottomTray.tsx b/js/packages/react-ui/src/components/CrayonChat/ComposedBottomTray.tsx index ee9124b76..2d9a81c2b 100644 --- a/js/packages/react-ui/src/components/CrayonChat/ComposedBottomTray.tsx +++ b/js/packages/react-ui/src/components/CrayonChat/ComposedBottomTray.tsx @@ -53,7 +53,9 @@ export const ComposedBottomTray = ({ <> {/* Trigger is always visible - toggles the tray (hidden on mobile when open) */} handleOpenChange(!isOpen)} isOpen={isOpen}> - Logo + {logoUrl ? ( + Logo + ) : null} {/* Controlled container */} diff --git a/js/packages/react-ui/src/components/CrayonChat/composedBottomTray.scss b/js/packages/react-ui/src/components/CrayonChat/composedBottomTray.scss new file mode 100644 index 000000000..4f2370cb5 --- /dev/null +++ b/js/packages/react-ui/src/components/CrayonChat/composedBottomTray.scss @@ -0,0 +1,8 @@ +@use "../../cssUtils" as cssUtils; + +.crayon-bottom-tray-trigger-logo { + width: "100%"; + height: "100%"; + object-fit: "cover"; + border-radius: cssUtils.$rounded-m; +} diff --git a/js/packages/react-ui/src/components/CrayonChat/crayonChat.scss b/js/packages/react-ui/src/components/CrayonChat/crayonChat.scss index e69de29bb..adb45f588 100644 --- a/js/packages/react-ui/src/components/CrayonChat/crayonChat.scss +++ b/js/packages/react-ui/src/components/CrayonChat/crayonChat.scss @@ -0,0 +1 @@ +@forward "./composedBottomTray.scss"; diff --git a/js/packages/react-ui/src/components/index.scss b/js/packages/react-ui/src/components/index.scss index deba20ad1..d72c76beb 100644 --- a/js/packages/react-ui/src/components/index.scss +++ b/js/packages/react-ui/src/components/index.scss @@ -44,3 +44,4 @@ @forward "./TextCallout/textCallout.scss"; @forward "./Charts/charts.scss"; @forward "./Separator/separator.scss"; +@forward "./CrayonChat/crayonChat.scss"; From 9403fbf29c2ee82c2311ec6b5cedd9f86dc90806 Mon Sep 17 00:00:00 2001 From: ankit-thesys Date: Thu, 8 Jan 2026 03:19:19 +0530 Subject: [PATCH 02/41] Enhance BottomTray component with ConversationStarter integration * Added ConversationStarter component to BottomTray stories for improved user interaction. * Updated BottomTray index and SCSS files to include ConversationStarter styles and exports. * Refactored loadThread function in stories to handle new thread cases more effectively. --- .../BottomTray/ConversationStarter.tsx | 75 +++++ .../src/components/BottomTray/bottomTray.scss | 1 + .../BottomTray/conversationStarter.scss | 36 +++ .../src/components/BottomTray/index.ts | 1 + .../BottomTray/stories/BottomTray.mdx | 265 ++++++++++++++++++ .../BottomTray/stories/BottomTray.stories.tsx | 55 ++-- js/packages/react-ui/src/index.ts | 2 + .../react-ui/src/types/ConversationStarter.ts | 6 + 8 files changed, 425 insertions(+), 16 deletions(-) create mode 100644 js/packages/react-ui/src/components/BottomTray/ConversationStarter.tsx create mode 100644 js/packages/react-ui/src/components/BottomTray/conversationStarter.scss create mode 100644 js/packages/react-ui/src/components/BottomTray/stories/BottomTray.mdx create mode 100644 js/packages/react-ui/src/types/ConversationStarter.ts diff --git a/js/packages/react-ui/src/components/BottomTray/ConversationStarter.tsx b/js/packages/react-ui/src/components/BottomTray/ConversationStarter.tsx new file mode 100644 index 000000000..36c251211 --- /dev/null +++ b/js/packages/react-ui/src/components/BottomTray/ConversationStarter.tsx @@ -0,0 +1,75 @@ +import { useThreadActions, useThreadState } from "@crayonai/react-core"; +import clsx from "clsx"; +import { ConversationStarterProps } from "../../types/ConversationStarter"; + +interface ConversationStarterItemProps extends ConversationStarterProps { + onClick: (prompt: string) => void; + disabled?: boolean; +} + +const ConversationStarterItem = ({ + displayText, + prompt, + onClick, + disabled = false, +}: ConversationStarterItemProps) => { + return ( + + ); +}; + +export interface ConversationStarterContainerProps { + starters: ConversationStarterProps[]; + className?: string; +} + +export const ConversationStarter = ({ + starters, + className, +}: ConversationStarterContainerProps) => { + const { processMessage } = useThreadActions(); + const { isRunning, messages } = useThreadState(); + + const handleClick = (prompt: string) => { + if (isRunning) return; + processMessage({ + type: "prompt", + role: "user", + message: prompt, + }); + }; + + // Only show when there are no messages + if (messages.length > 0) { + return null; + } + + if (starters.length === 0) { + return null; + } + + return ( +
+ {starters.map((item) => ( + + ))} +
+ ); +}; + +export default ConversationStarter; diff --git a/js/packages/react-ui/src/components/BottomTray/bottomTray.scss b/js/packages/react-ui/src/components/BottomTray/bottomTray.scss index 9ccbdaf8c..85cac6971 100644 --- a/js/packages/react-ui/src/components/BottomTray/bottomTray.scss +++ b/js/packages/react-ui/src/components/BottomTray/bottomTray.scss @@ -6,3 +6,4 @@ @use "./header.scss"; @use "./threadList.scss"; @use "./thread.scss"; +@use "./conversationStarter.scss"; \ No newline at end of file diff --git a/js/packages/react-ui/src/components/BottomTray/conversationStarter.scss b/js/packages/react-ui/src/components/BottomTray/conversationStarter.scss new file mode 100644 index 000000000..eefcc6821 --- /dev/null +++ b/js/packages/react-ui/src/components/BottomTray/conversationStarter.scss @@ -0,0 +1,36 @@ +@use "../../cssUtils" as cssUtils; + +.crayon-conversation-starter { + display: flex; + flex-direction: column; + gap: cssUtils.$spacing-s; + padding: 0 cssUtils.$spacing-s; + margin-bottom: cssUtils.$spacing-s; +} + +.crayon-conversation-starter-item { + width: 100%; + padding: cssUtils.$spacing-m cssUtils.$spacing-l; + background-color: cssUtils.$bg-container; + border: 1px solid cssUtils.$stroke-default; + border-radius: cssUtils.$rounded-m; + cursor: pointer; + transition: all 0.15s ease; + @include cssUtils.typography(body, default); + color: cssUtils.$primary-text; + text-align: left; + + &:not(:disabled):hover { + background-color: cssUtils.$interactive-hover; + border-color: cssUtils.$stroke-emphasis; + } + + &:not(:disabled):active { + background-color: cssUtils.$interactive-pressed; + } + + &--disabled { + opacity: 0.5; + cursor: not-allowed; + } +} diff --git a/js/packages/react-ui/src/components/BottomTray/index.ts b/js/packages/react-ui/src/components/BottomTray/index.ts index c7fc7b5aa..7648305c9 100644 --- a/js/packages/react-ui/src/components/BottomTray/index.ts +++ b/js/packages/react-ui/src/components/BottomTray/index.ts @@ -1,4 +1,5 @@ export * from "./Container"; +export * from "./ConversationStarter"; export * from "./Header"; export * from "./Thread"; export * from "./Trigger"; diff --git a/js/packages/react-ui/src/components/BottomTray/stories/BottomTray.mdx b/js/packages/react-ui/src/components/BottomTray/stories/BottomTray.mdx new file mode 100644 index 000000000..f3a850e91 --- /dev/null +++ b/js/packages/react-ui/src/components/BottomTray/stories/BottomTray.mdx @@ -0,0 +1,265 @@ +import { Meta } from '@storybook/blocks'; +import * as BottomTrayStories from './BottomTray.stories'; + + + +# BottomTray + +The BottomTray is a **composable chat interface** that appears as a floating panel in the bottom-right corner of your application. It uses a composition pattern, giving you full control over placement and styling. + +## Component Structure + +``` +ChatProvider +├── Trigger (floating button to open/close) +└── Container (the tray panel) + └── ThreadContainer + ├── Header (logo, agent name, actions) + ├── ScrollArea (scrollable message area) + │ └── Messages + ├── ConversationStarter (optional, shown when empty) + └── Composer (text input) +``` + +## Basic Usage + +```tsx +import { useState } from "react"; +import { + ChatProvider, + useThreadListManager, + useThreadManager, +} from "@crayonai/react-core"; +import { + Composer, + Container, + ConversationStarter, + Header, + MessageLoading, + Messages, + ScrollArea, + ThreadContainer, + Trigger, +} from "@crayonai/react-ui"; + +function MyApp() { + const [isOpen, setIsOpen] = useState(false); + + // Set up thread list management + const threadListManager = useThreadListManager({ + createThread: async () => ({ + threadId: crypto.randomUUID(), + title: "New Chat", + createdAt: new Date(), + isRunning: false, + }), + fetchThreadList: async () => { + // Fetch threads from your backend + return []; + }, + deleteThread: async (id) => { + // Delete thread from your backend + }, + updateThread: async (thread) => thread, + onSwitchToNew: () => {}, + onSelectThread: (threadId) => {}, + }); + + // Set up thread management + const threadManager = useThreadManager({ + threadId: threadListManager.selectedThreadId, + loadThread: async (threadId) => { + // Return empty for new thread, otherwise load messages + if (!threadId) return []; + // Fetch messages from your backend + return []; + }, + onProcessMessage: async ({ message, threadManager }) => { + // Send message to your AI backend + // Return assistant response + return [{ + id: crypto.randomUUID(), + role: "assistant", + type: "response", + message: [{ type: "text", text: "Response from AI" }], + }]; + }, + responseTemplates: [], + }); + + return ( + + {/* Trigger - floating button */} + setIsOpen(!isOpen)} + isOpen={isOpen} + /> + + {/* Container - the tray panel */} + + +
setIsOpen(false)} /> + + } /> + + + + + + + ); +} +``` + +## Components + +### Trigger + +The floating button that opens/closes the tray. Positioned fixed in the bottom-right corner. + +```tsx + setIsOpen(!isOpen)} + isOpen={isOpen} // Controls shrink animation when open + className="custom-class" // Optional custom styling +> + {/* Optional custom content, defaults to chevron icon */} + 💬 Need Help? + +``` + +### Container + +The main tray panel. Controls visibility with `isOpen` prop. + +```tsx + + {children} + +``` + +### ThreadContainer + +Wrapper for thread content. Manages layout and artifact panels. + +```tsx +
} // Artifact content +> + {children} + +``` + +### Header + +Header with logo, agent name, thread list, new chat button, and minimize. + +```tsx +
setIsOpen(false)} // Minimize callback + hideMinimizeButton={false} // Hide X button + hideNewChatButton={false} // Hide new chat button + hideThreadListContainer={false} // Hide thread list + rightChildren={} // Custom actions +/> +``` + +### ScrollArea + +Scrollable container for messages with auto-scroll behavior. + +```tsx + + } /> + +``` + +### ConversationStarter + +Clickable prompts shown when thread is empty. Automatically hides when messages exist. + +```tsx + +``` + +### Composer + +Text input with submit button. Handles enter key and running state. + +```tsx + +``` + +## Customization Examples + +### Custom Trigger Content + +```tsx + setIsOpen(!isOpen)} isOpen={isOpen}> + Chat + +``` + +### Without ConversationStarter + +Simply omit the component: + +```tsx + +
setIsOpen(false)} /> + + } /> + + + +``` + +### Minimal Header + +```tsx +
setIsOpen(false)} + hideNewChatButton + hideThreadListContainer +/> +``` + +## Animation + +The tray uses a **cone animation** that expands from the trigger button: + +- Opens with scale + clip-path animation from bottom-right +- Trigger shrinks when tray is open +- Smooth spring easing for natural feel + +## Mobile Behavior + +- Tray goes fullscreen on mobile (less than 768px) +- Trigger hides when tray is open on mobile +- Touch-friendly interactions diff --git a/js/packages/react-ui/src/components/BottomTray/stories/BottomTray.stories.tsx b/js/packages/react-ui/src/components/BottomTray/stories/BottomTray.stories.tsx index f2a6b0d41..6f6ae03b8 100644 --- a/js/packages/react-ui/src/components/BottomTray/stories/BottomTray.stories.tsx +++ b/js/packages/react-ui/src/components/BottomTray/stories/BottomTray.stories.tsx @@ -8,6 +8,7 @@ import { useState } from "react"; import { Composer, Container, + ConversationStarter, Header, MessageLoading, Messages, @@ -21,7 +22,7 @@ import logoUrl from "./thesysdev_logo.jpeg"; export default { title: "Components/BottomTray", - tags: ["dev", "!autodocs"], + tags: ["dev"], argTypes: { defaultOpen: { control: "boolean", @@ -73,7 +74,9 @@ const BottomTrayStory = ({ defaultOpen = false }: { defaultOpen?: boolean }) => const threadManager = useThreadManager({ threadId: threadListManager.selectedThreadId, - loadThread: async () => { + loadThread: async (threadId) => { + // Return empty for new thread (null), otherwise return existing messages + if (!threadId) return []; return [ { id: crypto.randomUUID(), @@ -135,6 +138,16 @@ const BottomTrayStory = ({ defaultOpen = false }: { defaultOpen?: boolean }) => } /> + @@ -183,20 +196,24 @@ const CustomTriggerStory = ({ defaultOpen = false }: { defaultOpen?: boolean }) const threadManager = useThreadManager({ threadId: threadListManager.selectedThreadId, - loadThread: async () => [ - { - id: crypto.randomUUID(), - role: "user", - type: "prompt", - message: "Hello", - }, - { - id: crypto.randomUUID(), - role: "assistant", - type: "response", - message: [{ type: "text", text: "Hello! How can I help you today?" }], - }, - ], + loadThread: async (threadId) => { + // Return empty for new thread (null), otherwise return existing messages + if (!threadId) return []; + return [ + { + id: crypto.randomUUID(), + role: "user", + type: "prompt", + message: "Hello", + }, + { + id: crypto.randomUUID(), + role: "assistant", + type: "response", + message: [{ type: "text", text: "Hello! How can I help you today?" }], + }, + ]; + }, onProcessMessage: async ({ message, threadManager }) => { const newMessage = Object.assign({}, message, { id: crypto.randomUUID() }) as Message; threadManager.appendMessages(newMessage); @@ -236,6 +253,12 @@ const CustomTriggerStory = ({ defaultOpen = false }: { defaultOpen?: boolean }) } /> + diff --git a/js/packages/react-ui/src/index.ts b/js/packages/react-ui/src/index.ts index 62411aeda..5e908fb69 100644 --- a/js/packages/react-ui/src/index.ts +++ b/js/packages/react-ui/src/index.ts @@ -52,3 +52,5 @@ export { export * from "./context/LayoutContext"; export * from "./context/PrintContext"; + +export type { ConversationStarterProps } from "./types/ConversationStarter"; diff --git a/js/packages/react-ui/src/types/ConversationStarter.ts b/js/packages/react-ui/src/types/ConversationStarter.ts new file mode 100644 index 000000000..6ea62f762 --- /dev/null +++ b/js/packages/react-ui/src/types/ConversationStarter.ts @@ -0,0 +1,6 @@ +interface ConversationStarterProps { + displayText: string; + prompt: string; +} + +export type { ConversationStarterProps }; \ No newline at end of file From 1a3c5add95bddc1647890375d29bb0dfb9dc03d5 Mon Sep 17 00:00:00 2001 From: ankit-thesys Date: Thu, 8 Jan 2026 03:24:19 +0530 Subject: [PATCH 03/41] Refactor ConversationStarter component for cleaner syntax and update SCSS imports in BottomTray * Simplified the ConversationStarter component by consolidating props destructuring. * Ensured conversationStarter.scss is correctly imported in bottomTray.scss for consistent styling. --- .../BottomTray/ConversationStarter.tsx | 5 +- .../src/components/BottomTray/bottomTray.scss | 2 +- .../BottomTray/stories/BottomTray.mdx | 74 ++++++++----------- .../react-ui/src/types/ConversationStarter.ts | 6 +- 4 files changed, 34 insertions(+), 53 deletions(-) diff --git a/js/packages/react-ui/src/components/BottomTray/ConversationStarter.tsx b/js/packages/react-ui/src/components/BottomTray/ConversationStarter.tsx index 36c251211..4ebec72ee 100644 --- a/js/packages/react-ui/src/components/BottomTray/ConversationStarter.tsx +++ b/js/packages/react-ui/src/components/BottomTray/ConversationStarter.tsx @@ -32,10 +32,7 @@ export interface ConversationStarterContainerProps { className?: string; } -export const ConversationStarter = ({ - starters, - className, -}: ConversationStarterContainerProps) => { +export const ConversationStarter = ({ starters, className }: ConversationStarterContainerProps) => { const { processMessage } = useThreadActions(); const { isRunning, messages } = useThreadState(); diff --git a/js/packages/react-ui/src/components/BottomTray/bottomTray.scss b/js/packages/react-ui/src/components/BottomTray/bottomTray.scss index 85cac6971..fc7b52961 100644 --- a/js/packages/react-ui/src/components/BottomTray/bottomTray.scss +++ b/js/packages/react-ui/src/components/BottomTray/bottomTray.scss @@ -6,4 +6,4 @@ @use "./header.scss"; @use "./threadList.scss"; @use "./thread.scss"; -@use "./conversationStarter.scss"; \ No newline at end of file +@use "./conversationStarter.scss"; diff --git a/js/packages/react-ui/src/components/BottomTray/stories/BottomTray.mdx b/js/packages/react-ui/src/components/BottomTray/stories/BottomTray.mdx index f3a850e91..4253de409 100644 --- a/js/packages/react-ui/src/components/BottomTray/stories/BottomTray.mdx +++ b/js/packages/react-ui/src/components/BottomTray/stories/BottomTray.mdx @@ -1,5 +1,5 @@ -import { Meta } from '@storybook/blocks'; -import * as BottomTrayStories from './BottomTray.stories'; +import { Meta } from "@storybook/blocks"; +import * as BottomTrayStories from "./BottomTray.stories"; @@ -25,11 +25,7 @@ ChatProvider ```tsx import { useState } from "react"; -import { - ChatProvider, - useThreadListManager, - useThreadManager, -} from "@crayonai/react-core"; +import { ChatProvider, useThreadListManager, useThreadManager } from "@crayonai/react-core"; import { Composer, Container, @@ -77,33 +73,25 @@ function MyApp() { onProcessMessage: async ({ message, threadManager }) => { // Send message to your AI backend // Return assistant response - return [{ - id: crypto.randomUUID(), - role: "assistant", - type: "response", - message: [{ type: "text", text: "Response from AI" }], - }]; + return [ + { + id: crypto.randomUUID(), + role: "assistant", + type: "response", + message: [{ type: "text", text: "Response from AI" }], + }, + ]; }, responseTemplates: [], }); return ( - + {/* Trigger - floating button */} - setIsOpen(!isOpen)} - isOpen={isOpen} - /> + setIsOpen(!isOpen)} isOpen={isOpen} /> {/* Container - the tray panel */} - +
setIsOpen(false)} /> @@ -132,8 +120,8 @@ The floating button that opens/closes the tray. Positioned fixed in the bottom-r ```tsx setIsOpen(!isOpen)} - isOpen={isOpen} // Controls shrink animation when open - className="custom-class" // Optional custom styling + isOpen={isOpen} // Controls shrink animation when open + className="custom-class" // Optional custom styling > {/* Optional custom content, defaults to chevron icon */} 💬 Need Help? @@ -146,10 +134,10 @@ The main tray panel. Controls visibility with `isOpen` prop. ```tsx {children} @@ -161,7 +149,7 @@ Wrapper for thread content. Manages layout and artifact panels. ```tsx
} // Artifact content > {children} @@ -174,11 +162,11 @@ Header with logo, agent name, thread list, new chat button, and minimize. ```tsx
setIsOpen(false)} // Minimize callback - hideMinimizeButton={false} // Hide X button - hideNewChatButton={false} // Hide new chat button - hideThreadListContainer={false} // Hide thread list - rightChildren={} // Custom actions + onMinimize={() => setIsOpen(false)} // Minimize callback + hideMinimizeButton={false} // Hide X button + hideNewChatButton={false} // Hide new chat button + hideThreadListContainer={false} // Hide thread list + rightChildren={} // Custom actions /> ``` @@ -188,8 +176,8 @@ Scrollable container for messages with auto-scroll behavior. ```tsx } /> @@ -243,11 +231,7 @@ Simply omit the component: ### Minimal Header ```tsx -
setIsOpen(false)} - hideNewChatButton - hideThreadListContainer -/> +
setIsOpen(false)} hideNewChatButton hideThreadListContainer /> ``` ## Animation diff --git a/js/packages/react-ui/src/types/ConversationStarter.ts b/js/packages/react-ui/src/types/ConversationStarter.ts index 6ea62f762..dfd718af8 100644 --- a/js/packages/react-ui/src/types/ConversationStarter.ts +++ b/js/packages/react-ui/src/types/ConversationStarter.ts @@ -1,6 +1,6 @@ interface ConversationStarterProps { - displayText: string; - prompt: string; + displayText: string; + prompt: string; } -export type { ConversationStarterProps }; \ No newline at end of file +export type { ConversationStarterProps }; From fc859a040debeb49d3628d6e8ce3a699359a8d45 Mon Sep 17 00:00:00 2001 From: ankit-thesys Date: Thu, 8 Jan 2026 14:10:43 +0530 Subject: [PATCH 04/41] Integrate ConversationStarter component into CopilotShell and enhance stories * Added ConversationStarter component to CopilotShell SCSS and index for improved functionality. * Updated Shell stories to include ConversationStarter, providing sample starters for user interaction. * Refactored message handling in stories for better response simulation and user experience. --- .../CopilotShell/ConversationStarter.tsx | 72 ++++++ .../CopilotShell/conversationStarter.scss | 35 +++ .../components/CopilotShell/copilotShell.scss | 1 + .../src/components/CopilotShell/index.ts | 1 + .../components/CopilotShell/stories/Shell.mdx | 235 ++++++++++++++++++ .../CopilotShell/stories/Shell.stories.tsx | 84 ++++++- .../components/CrayonChat/ComposedCopilot.tsx | 1 + 7 files changed, 423 insertions(+), 6 deletions(-) create mode 100644 js/packages/react-ui/src/components/CopilotShell/ConversationStarter.tsx create mode 100644 js/packages/react-ui/src/components/CopilotShell/conversationStarter.scss create mode 100644 js/packages/react-ui/src/components/CopilotShell/stories/Shell.mdx diff --git a/js/packages/react-ui/src/components/CopilotShell/ConversationStarter.tsx b/js/packages/react-ui/src/components/CopilotShell/ConversationStarter.tsx new file mode 100644 index 000000000..15d593866 --- /dev/null +++ b/js/packages/react-ui/src/components/CopilotShell/ConversationStarter.tsx @@ -0,0 +1,72 @@ +import { useThreadActions, useThreadState } from "@crayonai/react-core"; +import clsx from "clsx"; +import { ConversationStarterProps } from "../../types/ConversationStarter"; + +interface ConversationStarterItemProps extends ConversationStarterProps { + onClick: (prompt: string) => void; + disabled?: boolean; +} + +const ConversationStarterItem = ({ + displayText, + prompt, + onClick, + disabled = false, +}: ConversationStarterItemProps) => { + return ( + + ); +}; + +export interface ConversationStarterContainerProps { + starters: ConversationStarterProps[]; + className?: string; +} + +export const ConversationStarter = ({ starters, className }: ConversationStarterContainerProps) => { + const { processMessage } = useThreadActions(); + const { isRunning, messages } = useThreadState(); + + const handleClick = (prompt: string) => { + if (isRunning) return; + processMessage({ + type: "prompt", + role: "user", + message: prompt, + }); + }; + + // Only show when there are no messages + if (messages.length > 0) { + return null; + } + + if (starters.length === 0) { + return null; + } + + return ( +
+ {starters.map((item) => ( + + ))} +
+ ); +}; + +export default ConversationStarter; diff --git a/js/packages/react-ui/src/components/CopilotShell/conversationStarter.scss b/js/packages/react-ui/src/components/CopilotShell/conversationStarter.scss new file mode 100644 index 000000000..7b1d5d001 --- /dev/null +++ b/js/packages/react-ui/src/components/CopilotShell/conversationStarter.scss @@ -0,0 +1,35 @@ +@use "../../cssUtils" as cssUtils; + +.crayon-copilot-shell-conversation-starter { + display: flex; + flex-direction: column; + gap: cssUtils.$spacing-s; + padding: 0 cssUtils.$spacing-l cssUtils.$spacing-m; +} + +.crayon-copilot-shell-conversation-starter-item { + width: 100%; + padding: cssUtils.$spacing-m cssUtils.$spacing-l; + background-color: cssUtils.$bg-container; + border: 1px solid cssUtils.$stroke-default; + border-radius: cssUtils.$rounded-m; + cursor: pointer; + transition: all 0.15s ease; + @include cssUtils.typography(body, default); + color: cssUtils.$primary-text; + text-align: left; + + &:not(:disabled):hover { + background-color: cssUtils.$interactive-hover; + border-color: cssUtils.$stroke-emphasis; + } + + &:not(:disabled):active { + background-color: cssUtils.$interactive-pressed; + } + + &--disabled { + opacity: 0.5; + cursor: not-allowed; + } +} diff --git a/js/packages/react-ui/src/components/CopilotShell/copilotShell.scss b/js/packages/react-ui/src/components/CopilotShell/copilotShell.scss index 16c7f0c47..3ff84e3b8 100644 --- a/js/packages/react-ui/src/components/CopilotShell/copilotShell.scss +++ b/js/packages/react-ui/src/components/CopilotShell/copilotShell.scss @@ -1,6 +1,7 @@ @use "../../cssUtils" as cssUtils; @use "./thread.scss"; @use "./header.scss"; +@use "./conversationStarter.scss"; .crayon-copilot-shell-container { display: flex; diff --git a/js/packages/react-ui/src/components/CopilotShell/index.ts b/js/packages/react-ui/src/components/CopilotShell/index.ts index f19ab917b..2c05c1fe6 100644 --- a/js/packages/react-ui/src/components/CopilotShell/index.ts +++ b/js/packages/react-ui/src/components/CopilotShell/index.ts @@ -1,3 +1,4 @@ export * from "./Container"; +export * from "./ConversationStarter"; export * from "./Header"; export * from "./Thread"; diff --git a/js/packages/react-ui/src/components/CopilotShell/stories/Shell.mdx b/js/packages/react-ui/src/components/CopilotShell/stories/Shell.mdx new file mode 100644 index 000000000..7ef4b6399 --- /dev/null +++ b/js/packages/react-ui/src/components/CopilotShell/stories/Shell.mdx @@ -0,0 +1,235 @@ +import { Meta } from "@storybook/blocks"; +import * as ShellStories from "./Shell.stories"; + + + +# CopilotShell + +The CopilotShell is a **composable sidebar chat interface** designed to be embedded as a side panel in your application. It uses a composition pattern, giving you full control over placement and styling. + +## Component Structure + +``` +ChatProvider +└── Container (sidebar panel) + └── ThreadContainer + ├── Header (logo, agent name) + ├── ScrollArea (scrollable message area) + │ └── Messages + ├── ConversationStarter (optional, shown when empty) + └── Composer (text input) +``` + +## Basic Usage + +```tsx +import { useState } from "react"; +import { ChatProvider, useThreadListManager, useThreadManager } from "@crayonai/react-core"; +import { + Composer, + Container, + ConversationStarter, + Header, + MessageLoading, + Messages, + ScrollArea, + ThreadContainer, +} from "@crayonai/react-ui/CopilotShell"; + +function MyCopilot() { + const threadListManager = useThreadListManager({ + createThread: async () => ({ + threadId: crypto.randomUUID(), + title: "New Chat", + createdAt: new Date(), + isRunning: false, + }), + fetchThreadList: async () => [], + deleteThread: async () => {}, + updateThread: async (t) => t, + onSwitchToNew: () => {}, + onSelectThread: () => {}, + }); + + const threadManager = useThreadManager({ + threadId: threadListManager.selectedThreadId, + loadThread: async (threadId) => { + if (!threadId) return []; + // Load messages for thread + return []; + }, + onProcessMessage: async ({ message, threadManager }) => { + threadManager.appendMessages({ ...message, id: crypto.randomUUID() }); + // Send message to your AI backend + // Return assistant response + return [ + { + id: crypto.randomUUID(), + role: "assistant", + type: "response", + message: [{ type: "text", text: "Response from AI" }], + }, + ]; + }, + responseTemplates: [], + }); + + return ( + + + +
+ + } /> + + + + + + + ); +} +``` + +## Individual Components + +### Container + +The main wrapper for the CopilotShell sidebar. + +```tsx + + {children} + +``` + +### ThreadContainer + +Wrapper for the thread content with optional artifact panel support. + +```tsx +
} // Artifact content +> + {children} + +``` + +### Header + +The header with logo and agent name. + +```tsx +
+``` + +### ScrollArea + +Scrollable container for messages with smart scroll behavior. + +```tsx + + } /> + +``` + +### ConversationStarter + +Full-width clickable buttons shown when the thread is empty. Automatically submits the prompt as a message when clicked. + +```tsx + +``` + +**Props:** + +- `starters` - Array of objects with `displayText` (shown on button) and `prompt` (sent as message) +- `className` - Optional additional CSS class + +**Behavior:** + +- Only visible when there are no messages in the thread +- Clicking a button submits the `prompt` as a user message +- Buttons are disabled when a response is being generated + +### Composer + +Text input for sending messages. + +```tsx + +``` + +## Layout Integration + +CopilotShell is designed to be placed as a sidebar: + +```tsx +function AppLayout() { + return ( +
+ {/* Main content area */} +
{/* Your app content */}
+ + {/* Copilot sidebar */} + +
+ ); +} +``` + +## Comparison with BottomTray + +| Feature | CopilotShell | BottomTray | +| -------- | ------------------- | ---------------------- | +| Layout | Sidebar panel | Floating panel | +| Position | Fixed sidebar | Bottom-right corner | +| Trigger | Always visible | Floating button | +| Mobile | Responsive | Fullscreen | +| Use case | Main chat interface | Secondary/support chat | + +## CSS Classes + +All CopilotShell components use the `crayon-copilot-shell-` prefix: + +- `.crayon-copilot-shell-thread-container` +- `.crayon-copilot-shell-thread-scroll-area` +- `.crayon-copilot-shell-thread-messages` +- `.crayon-copilot-shell-thread-message-user` +- `.crayon-copilot-shell-thread-message-assistant` +- `.crayon-copilot-shell-thread-composer` +- `.crayon-copilot-shell-conversation-starter` +- `.crayon-copilot-shell-conversation-starter-item` diff --git a/js/packages/react-ui/src/components/CopilotShell/stories/Shell.stories.tsx b/js/packages/react-ui/src/components/CopilotShell/stories/Shell.stories.tsx index fecfb6aee..e9f8c0f8f 100644 --- a/js/packages/react-ui/src/components/CopilotShell/stories/Shell.stories.tsx +++ b/js/packages/react-ui/src/components/CopilotShell/stories/Shell.stories.tsx @@ -7,6 +7,7 @@ import { import { Composer, Container, + ConversationStarter, Header, MessageLoading, Messages, @@ -19,11 +20,23 @@ import logoUrl from "./thesysdev_logo.jpeg"; export default { title: "Components/CopilotShell", - tags: ["dev", "!autodocs"], + tags: ["dev"], + argTypes: { + defaultOpen: { + control: "boolean", + description: "Whether to start with messages", + }, + }, }; +const SAMPLE_STARTERS = [ + { displayText: "Help me get started", prompt: "Help me get started" }, + { displayText: "What can you do?", prompt: "What can you do?" }, + { displayText: "Tell me about your features", prompt: "Tell me about your features" }, +]; + export const Default = { - render: (args: any) => { + render: () => { const threadListManager = useThreadListManager({ createThread: async () => { return { @@ -64,7 +77,8 @@ export const Default = { const threadManager = useThreadManager({ threadId: threadListManager.selectedThreadId, - loadThread: async () => { + loadThread: async (threadId) => { + if (!threadId) return []; return [ { id: crypto.randomUUID(), @@ -76,11 +90,68 @@ export const Default = { id: crypto.randomUUID(), role: "assistant", type: "response", - message: [{ type: "text", text: "Hello" }], + message: [{ type: "text", text: "Hello! How can I help you today?" }], }, ]; }, - onProcessMessage: async ({ message, threadManager, abortController }) => { + onProcessMessage: async ({ message, threadManager }) => { + const newMessage = Object.assign({}, message, { + id: crypto.randomUUID(), + }) as Message; + threadManager.appendMessages(newMessage); + await new Promise((resolve) => setTimeout(resolve, 1000)); + return [ + { + id: crypto.randomUUID(), + role: "assistant", + type: "response", + message: [{ type: "text", text: "This is a response from the AI assistant." }], + }, + ]; + }, + responseTemplates: [], + }); + + return ( +
+
+ + + +
+ + } /> + + + + + + +
+ ); + }, +}; + +export const WithConversationStarter = { + render: () => { + const threadListManager = useThreadListManager({ + createThread: async () => ({ + threadId: crypto.randomUUID(), + title: "New Chat", + createdAt: new Date(), + isRunning: false, + }), + fetchThreadList: async () => [], + deleteThread: async () => {}, + updateThread: async (t) => t, + onSwitchToNew: () => {}, + onSelectThread: () => {}, + }); + + const threadManager = useThreadManager({ + threadId: threadListManager.selectedThreadId, + loadThread: async () => [], // Start with empty thread to show starters + onProcessMessage: async ({ message, threadManager }) => { const newMessage = Object.assign({}, message, { id: crypto.randomUUID(), }) as Message; @@ -91,7 +162,7 @@ export const Default = { id: crypto.randomUUID(), role: "assistant", type: "response", - message: [{ type: "text", text: "sadfasdf" }], + message: [{ type: "text", text: `You asked: "${message.message}"` }], }, ]; }, @@ -108,6 +179,7 @@ export const Default = { } /> + diff --git a/js/packages/react-ui/src/components/CrayonChat/ComposedCopilot.tsx b/js/packages/react-ui/src/components/CrayonChat/ComposedCopilot.tsx index 2957757e8..ac305705f 100644 --- a/js/packages/react-ui/src/components/CrayonChat/ComposedCopilot.tsx +++ b/js/packages/react-ui/src/components/CrayonChat/ComposedCopilot.tsx @@ -29,6 +29,7 @@ export const ComposedCopilot = ({
+ } /> From 0fbcb0079f2a49aea882659ce4d2580dfd2e1af5 Mon Sep 17 00:00:00 2001 From: ankit-thesys Date: Thu, 8 Jan 2026 15:43:41 +0530 Subject: [PATCH 05/41] Integrate ConversationStarter component into Shell and enhance stories * Added ConversationStarter component to Shell index and SCSS for improved user interaction. * Updated Shell stories to include ConversationStarter with sample starters for better user experience. * Refactored message handling in stories to provide more realistic AI responses and improved functionality. --- .../components/Shell/ConversationStarter.tsx | 73 +++++ .../components/Shell/conversationStarter.scss | 49 ++++ .../react-ui/src/components/Shell/index.ts | 1 + .../react-ui/src/components/Shell/shell.scss | 1 + .../src/components/Shell/stories/Shell.mdx | 275 ++++++++++++++++++ .../Shell/stories/Shell.stories.tsx | 89 +++++- 6 files changed, 482 insertions(+), 6 deletions(-) create mode 100644 js/packages/react-ui/src/components/Shell/ConversationStarter.tsx create mode 100644 js/packages/react-ui/src/components/Shell/conversationStarter.scss create mode 100644 js/packages/react-ui/src/components/Shell/stories/Shell.mdx diff --git a/js/packages/react-ui/src/components/Shell/ConversationStarter.tsx b/js/packages/react-ui/src/components/Shell/ConversationStarter.tsx new file mode 100644 index 000000000..2c5e3d6ae --- /dev/null +++ b/js/packages/react-ui/src/components/Shell/ConversationStarter.tsx @@ -0,0 +1,73 @@ +import { useThreadActions, useThreadState } from "@crayonai/react-core"; +import clsx from "clsx"; +import { ConversationStarterProps } from "../../types/ConversationStarter"; + +interface ConversationStarterItemProps extends ConversationStarterProps { + onClick: (prompt: string) => void; + disabled?: boolean; +} + +const ConversationStarterItem = ({ + displayText, + prompt, + onClick, + disabled = false, +}: ConversationStarterItemProps) => { + return ( + + ); +}; + +export interface ConversationStarterContainerProps { + starters: ConversationStarterProps[]; + className?: string; +} + +export const ConversationStarter = ({ starters, className }: ConversationStarterContainerProps) => { + const { processMessage } = useThreadActions(); + const { isRunning, messages } = useThreadState(); + + const handleClick = (prompt: string) => { + if (isRunning) return; + processMessage({ + type: "prompt", + role: "user", + message: prompt, + }); + }; + + // Only show when there are no messages + if (messages.length > 0) { + return null; + } + + if (starters.length === 0) { + return null; + } + + return ( +
+ {starters.map((item) => ( + + ))} +
+ ); +}; + +export default ConversationStarter; + diff --git a/js/packages/react-ui/src/components/Shell/conversationStarter.scss b/js/packages/react-ui/src/components/Shell/conversationStarter.scss new file mode 100644 index 000000000..70d279ef1 --- /dev/null +++ b/js/packages/react-ui/src/components/Shell/conversationStarter.scss @@ -0,0 +1,49 @@ +@use "../../cssUtils" as cssUtils; + +$center-align-spacing: calc(32px + cssUtils.$spacing-s); + +.crayon-shell-conversation-starter { + display: flex; + flex-direction: column; + gap: cssUtils.$spacing-s; + max-width: 880px; + margin: 0 auto; + padding: 0 $center-align-spacing cssUtils.$spacing-m; + + .crayon-shell-container--mobile & { + padding: 0 cssUtils.$spacing-l cssUtils.$spacing-m; + } + + .crayon-shell-thread-container--artifact-active & { + padding-left: 0; + padding-right: cssUtils.$spacing-m; + } +} + +.crayon-shell-conversation-starter-item { + width: 100%; + padding: cssUtils.$spacing-m cssUtils.$spacing-l; + background-color: cssUtils.$bg-container; + border: 1px solid cssUtils.$stroke-default; + border-radius: cssUtils.$rounded-m; + cursor: pointer; + transition: all 0.15s ease; + @include cssUtils.typography(body, default); + color: cssUtils.$primary-text; + text-align: left; + + &:not(:disabled):hover { + background-color: cssUtils.$interactive-hover; + border-color: cssUtils.$stroke-emphasis; + } + + &:not(:disabled):active { + background-color: cssUtils.$interactive-pressed; + } + + &--disabled { + opacity: 0.5; + cursor: not-allowed; + } +} + diff --git a/js/packages/react-ui/src/components/Shell/index.ts b/js/packages/react-ui/src/components/Shell/index.ts index 6df8637f3..ef5f897b8 100644 --- a/js/packages/react-ui/src/components/Shell/index.ts +++ b/js/packages/react-ui/src/components/Shell/index.ts @@ -1,4 +1,5 @@ export * from "./Container"; +export * from "./ConversationStarter"; export * from "./MobileHeader"; export * from "./NewChatButton"; export * from "./Sidebar"; diff --git a/js/packages/react-ui/src/components/Shell/shell.scss b/js/packages/react-ui/src/components/Shell/shell.scss index 6fa3ced8c..c2c7cc0a2 100644 --- a/js/packages/react-ui/src/components/Shell/shell.scss +++ b/js/packages/react-ui/src/components/Shell/shell.scss @@ -4,6 +4,7 @@ @use "./thread.scss"; @use "./mobileHeader.scss"; @use "./resizableSeparator.scss"; +@use "./conversationStarter.scss"; .crayon-shell-container { display: flex; diff --git a/js/packages/react-ui/src/components/Shell/stories/Shell.mdx b/js/packages/react-ui/src/components/Shell/stories/Shell.mdx new file mode 100644 index 000000000..14c393ab5 --- /dev/null +++ b/js/packages/react-ui/src/components/Shell/stories/Shell.mdx @@ -0,0 +1,275 @@ +import { Meta } from "@storybook/blocks"; +import * as ShellStories from "./Shell.stories"; + + + +# Shell + +The Shell is a **full-page composable chat interface** with a sidebar for thread management. It's designed for applications where chat is the primary interface, providing a complete desktop and mobile experience. + +## Component Structure + +``` +ChatProvider +└── Container (full-page wrapper) + ├── SidebarContainer (collapsible sidebar) + │ ├── SidebarHeader (logo, agent name, collapse button) + │ └── SidebarContent + │ ├── NewChatButton + │ ├── SidebarSeparator + │ └── ThreadList + └── ThreadContainer (main chat area) + ├── MobileHeader (mobile-only header) + ├── ScrollArea (scrollable message area) + │ └── Messages + ├── ConversationStarter (optional, shown when empty) + └── Composer (text input) +``` + +## Basic Usage + +```tsx +import { ChatProvider, useThreadListManager, useThreadManager } from "@crayonai/react-core"; +import { + Composer, + Container, + ConversationStarter, + MessageLoading, + Messages, + MobileHeader, + NewChatButton, + ScrollArea, + SidebarContainer, + SidebarContent, + SidebarHeader, + SidebarSeparator, + ThreadContainer, + ThreadList, +} from "@crayonai/react-ui/Shell"; + +function MyShell() { + const threadListManager = useThreadListManager({ + createThread: async () => ({ + threadId: crypto.randomUUID(), + title: "New Chat", + createdAt: new Date(), + isRunning: false, + }), + fetchThreadList: async () => [], + deleteThread: async () => {}, + updateThread: async (t) => t, + onSwitchToNew: () => {}, + onSelectThread: () => {}, + }); + + const threadManager = useThreadManager({ + threadId: threadListManager.selectedThreadId, + loadThread: async (threadId) => { + if (!threadId) return []; + // Load messages for thread + return []; + }, + onProcessMessage: async ({ message, threadManager }) => { + threadManager.appendMessages({ ...message, id: crypto.randomUUID() }); + // Send message to your AI backend + // Return assistant response + return [ + { + id: crypto.randomUUID(), + role: "assistant", + type: "response", + message: [{ type: "text", text: "Response from AI" }], + }, + ]; + }, + responseTemplates: [], + }); + + return ( + + + + + + + + + + + + + + } /> + + + + + + + ); +} +``` + +## Individual Components + +### Container + +The main full-page wrapper for the Shell. + +```tsx + + {children} + +``` + +### Sidebar Components + +The sidebar provides thread management and navigation. + +```tsx + + {/* Logo, name, collapse button */} + + {/* Creates new thread */} + {/* Visual separator */} + {/* List of existing threads */} + + +``` + +### ThreadContainer + +Wrapper for the main chat area with optional artifact panel support. + +```tsx +
} // Artifact content +> + {children} + +``` + +### MobileHeader + +Header shown only on mobile devices with sidebar toggle. + +```tsx + +``` + +### ScrollArea + +Scrollable container for messages with smart scroll behavior. + +```tsx + + } /> + +``` + +### ConversationStarter + +Full-width clickable buttons shown when the thread is empty. Automatically submits the prompt as a message when clicked. + +```tsx + +``` + +**Props:** + +- `starters` - Array of objects with `displayText` (shown on button) and `prompt` (sent as message) +- `className` - Optional additional CSS class + +**Behavior:** + +- Only visible when there are no messages in the thread +- Clicking a button submits the `prompt` as a user message +- Buttons are disabled when a response is being generated +- Respects Shell layout (centered with max-width, mobile padding) + +### Composer + +Text input for sending messages. + +```tsx + +``` + +## Desktop vs Mobile + +The Shell automatically adapts to screen size: + +| Feature | Desktop | Mobile | +| ------- | ------- | ------ | +| Sidebar | Collapsible side panel | Hidden, accessible via hamburger menu | +| Header | In sidebar | MobileHeader at top of thread | +| Artifact panel | Resizable side-by-side | Full-screen overlay | +| Conversation starters | Centered with max-width | Full-width with padding | + +## Artifact Panel + +The Shell supports a resizable artifact panel on desktop: + +```tsx + ( +
+ {/* Your artifact content (code preview, document, etc.) */} +
+ )} +> + {/* Chat content */} +
+``` + +On desktop, the artifact panel appears to the right of the chat and can be resized by dragging the separator. On mobile, it appears as a full-screen overlay. + +## Comparison with Other Components + +| Feature | Shell | CopilotShell | BottomTray | +| ------- | ----- | ------------ | ---------- | +| Layout | Full page | Sidebar panel | Floating panel | +| Sidebar | Yes (collapsible) | No | No | +| Thread list | Yes | No | Yes (in header) | +| Artifact panel | Yes (resizable) | Yes (overlay) | No | +| Mobile | Responsive | Responsive | Fullscreen | +| Use case | Primary chat app | Side panel copilot | Secondary/support chat | + +## CSS Classes + +All Shell components use the `crayon-shell-` prefix: + +- `.crayon-shell-container` +- `.crayon-shell-thread-container` +- `.crayon-shell-thread-scroll-area` +- `.crayon-shell-thread-messages` +- `.crayon-shell-thread-message-user` +- `.crayon-shell-thread-message-assistant` +- `.crayon-shell-thread-composer` +- `.crayon-shell-conversation-starter` +- `.crayon-shell-conversation-starter-item` +- `.crayon-shell-sidebar-container` +- `.crayon-shell-sidebar-header` +- `.crayon-shell-sidebar-content` + diff --git a/js/packages/react-ui/src/components/Shell/stories/Shell.stories.tsx b/js/packages/react-ui/src/components/Shell/stories/Shell.stories.tsx index eb51f4a67..4b165dfb0 100644 --- a/js/packages/react-ui/src/components/Shell/stories/Shell.stories.tsx +++ b/js/packages/react-ui/src/components/Shell/stories/Shell.stories.tsx @@ -5,6 +5,7 @@ import { useThreadManager, } from "@crayonai/react-core"; import { Container } from "../Container"; +import { ConversationStarter } from "../ConversationStarter"; import { MobileHeader } from "../MobileHeader"; import { NewChatButton } from "../NewChatButton"; import { SidebarContainer, SidebarContent, SidebarHeader, SidebarSeparator } from "../Sidebar"; @@ -14,11 +15,23 @@ import logoUrl from "./thesysdev_logo.jpeg"; export default { title: "Shell", - tags: ["!dev", "!autodocs"], + tags: ["dev"], + argTypes: { + defaultOpen: { + control: "boolean", + description: "Whether to start with messages", + }, + }, }; +const SAMPLE_STARTERS = [ + { displayText: "Help me get started", prompt: "Help me get started" }, + { displayText: "What can you do?", prompt: "What can you do?" }, + { displayText: "Tell me about your features", prompt: "Tell me about your features" }, +]; + export const Default = { - render: (args: any) => { + render: () => { const threadListManager = useThreadListManager({ createThread: async () => { return { @@ -59,7 +72,8 @@ export const Default = { const threadManager = useThreadManager({ threadId: threadListManager.selectedThreadId, - loadThread: async () => { + loadThread: async (threadId) => { + if (!threadId) return []; return [ { id: crypto.randomUUID(), @@ -71,11 +85,73 @@ export const Default = { id: crypto.randomUUID(), role: "assistant", type: "response", - message: [{ type: "text", text: "Hello" }], + message: [{ type: "text", text: "Hello! How can I help you today?" }], + }, + ]; + }, + onProcessMessage: async ({ message, threadManager }) => { + const newMessage = Object.assign({}, message, { + id: crypto.randomUUID(), + }) as Message; + threadManager.appendMessages(newMessage); + await new Promise((resolve) => setTimeout(resolve, 1000)); + return [ + { + id: crypto.randomUUID(), + role: "assistant", + type: "response", + message: [{ type: "text", text: "This is a response from the AI assistant." }], }, ]; }, - onProcessMessage: async ({ message, threadManager, abortController }) => { + responseTemplates: [], + }); + + return ( + + + + + + + + + + + + + + } /> + + + + + + + ); + }, +}; + +export const WithConversationStarter = { + render: () => { + const threadListManager = useThreadListManager({ + createThread: async () => ({ + threadId: crypto.randomUUID(), + title: "New Chat", + createdAt: new Date(), + isRunning: false, + }), + fetchThreadList: async () => [], + deleteThread: async () => {}, + updateThread: async (t) => t, + onSwitchToNew: () => {}, + onSelectThread: () => {}, + }); + + const threadManager = useThreadManager({ + threadId: threadListManager.selectedThreadId, + loadThread: async () => [], // Start with empty thread to show starters + onProcessMessage: async ({ message, threadManager }) => { const newMessage = Object.assign({}, message, { id: crypto.randomUUID(), }) as Message; @@ -86,7 +162,7 @@ export const Default = { id: crypto.randomUUID(), role: "assistant", type: "response", - message: [{ type: "text", text: "sadfasdf" }], + message: [{ type: "text", text: `You asked: "${message.message}"` }], }, ]; }, @@ -109,6 +185,7 @@ export const Default = { } /> + From 949cf81176d8f6c99b5670435f0eb0d5497aeccd Mon Sep 17 00:00:00 2001 From: ankit-thesys Date: Thu, 8 Jan 2026 15:44:49 +0530 Subject: [PATCH 06/41] format fix --- .../components/Shell/ConversationStarter.tsx | 1 - .../components/Shell/conversationStarter.scss | 1 - .../src/components/Shell/stories/Shell.mdx | 35 ++++++++----------- 3 files changed, 15 insertions(+), 22 deletions(-) diff --git a/js/packages/react-ui/src/components/Shell/ConversationStarter.tsx b/js/packages/react-ui/src/components/Shell/ConversationStarter.tsx index 2c5e3d6ae..905fb0344 100644 --- a/js/packages/react-ui/src/components/Shell/ConversationStarter.tsx +++ b/js/packages/react-ui/src/components/Shell/ConversationStarter.tsx @@ -70,4 +70,3 @@ export const ConversationStarter = ({ starters, className }: ConversationStarter }; export default ConversationStarter; - diff --git a/js/packages/react-ui/src/components/Shell/conversationStarter.scss b/js/packages/react-ui/src/components/Shell/conversationStarter.scss index 70d279ef1..c69ba1dcf 100644 --- a/js/packages/react-ui/src/components/Shell/conversationStarter.scss +++ b/js/packages/react-ui/src/components/Shell/conversationStarter.scss @@ -46,4 +46,3 @@ $center-align-spacing: calc(32px + cssUtils.$spacing-s); cursor: not-allowed; } } - diff --git a/js/packages/react-ui/src/components/Shell/stories/Shell.mdx b/js/packages/react-ui/src/components/Shell/stories/Shell.mdx index 14c393ab5..0faf3369d 100644 --- a/js/packages/react-ui/src/components/Shell/stories/Shell.mdx +++ b/js/packages/react-ui/src/components/Shell/stories/Shell.mdx @@ -219,12 +219,12 @@ Text input for sending messages. The Shell automatically adapts to screen size: -| Feature | Desktop | Mobile | -| ------- | ------- | ------ | -| Sidebar | Collapsible side panel | Hidden, accessible via hamburger menu | -| Header | In sidebar | MobileHeader at top of thread | -| Artifact panel | Resizable side-by-side | Full-screen overlay | -| Conversation starters | Centered with max-width | Full-width with padding | +| Feature | Desktop | Mobile | +| --------------------- | ----------------------- | ------------------------------------- | +| Sidebar | Collapsible side panel | Hidden, accessible via hamburger menu | +| Header | In sidebar | MobileHeader at top of thread | +| Artifact panel | Resizable side-by-side | Full-screen overlay | +| Conversation starters | Centered with max-width | Full-width with padding | ## Artifact Panel @@ -233,11 +233,7 @@ The Shell supports a resizable artifact panel on desktop: ```tsx ( -
- {/* Your artifact content (code preview, document, etc.) */} -
- )} + renderArtifact={() =>
{/* Your artifact content (code preview, document, etc.) */}
} > {/* Chat content */}
@@ -247,14 +243,14 @@ On desktop, the artifact panel appears to the right of the chat and can be resiz ## Comparison with Other Components -| Feature | Shell | CopilotShell | BottomTray | -| ------- | ----- | ------------ | ---------- | -| Layout | Full page | Sidebar panel | Floating panel | -| Sidebar | Yes (collapsible) | No | No | -| Thread list | Yes | No | Yes (in header) | -| Artifact panel | Yes (resizable) | Yes (overlay) | No | -| Mobile | Responsive | Responsive | Fullscreen | -| Use case | Primary chat app | Side panel copilot | Secondary/support chat | +| Feature | Shell | CopilotShell | BottomTray | +| -------------- | ----------------- | ------------------ | ---------------------- | +| Layout | Full page | Sidebar panel | Floating panel | +| Sidebar | Yes (collapsible) | No | No | +| Thread list | Yes | No | Yes (in header) | +| Artifact panel | Yes (resizable) | Yes (overlay) | No | +| Mobile | Responsive | Responsive | Fullscreen | +| Use case | Primary chat app | Side panel copilot | Secondary/support chat | ## CSS Classes @@ -272,4 +268,3 @@ All Shell components use the `crayon-shell-` prefix: - `.crayon-shell-sidebar-container` - `.crayon-shell-sidebar-header` - `.crayon-shell-sidebar-content` - From 9d903d6ee1f1ac6fd8993054f6fad9cc7aacca8d Mon Sep 17 00:00:00 2001 From: ankit-thesys Date: Thu, 8 Jan 2026 18:48:04 +0530 Subject: [PATCH 07/41] update shell stories and conversation starter styles --- .../src/components/Shell/conversationStarter.scss | 11 +++++++++-- .../src/components/Shell/stories/Shell.stories.tsx | 2 +- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/js/packages/react-ui/src/components/Shell/conversationStarter.scss b/js/packages/react-ui/src/components/Shell/conversationStarter.scss index c69ba1dcf..82b4cf494 100644 --- a/js/packages/react-ui/src/components/Shell/conversationStarter.scss +++ b/js/packages/react-ui/src/components/Shell/conversationStarter.scss @@ -3,15 +3,22 @@ $center-align-spacing: calc(32px + cssUtils.$spacing-s); .crayon-shell-conversation-starter { - display: flex; - flex-direction: column; + display: grid; + grid-template-columns: 1fr; gap: cssUtils.$spacing-s; + width: 100%; max-width: 880px; margin: 0 auto; padding: 0 $center-align-spacing cssUtils.$spacing-m; + // 2-column layout for larger screens (≥640px) + @media (min-width: 640px) { + grid-template-columns: repeat(2, 1fr); + } + .crayon-shell-container--mobile & { padding: 0 cssUtils.$spacing-l cssUtils.$spacing-m; + grid-template-columns: 1fr; // Force single column on mobile } .crayon-shell-thread-container--artifact-active & { diff --git a/js/packages/react-ui/src/components/Shell/stories/Shell.stories.tsx b/js/packages/react-ui/src/components/Shell/stories/Shell.stories.tsx index 4b165dfb0..a7858106b 100644 --- a/js/packages/react-ui/src/components/Shell/stories/Shell.stories.tsx +++ b/js/packages/react-ui/src/components/Shell/stories/Shell.stories.tsx @@ -14,7 +14,7 @@ import { ThreadList } from "../ThreadList"; import logoUrl from "./thesysdev_logo.jpeg"; export default { - title: "Shell", + title: "Components/Shell", tags: ["dev"], argTypes: { defaultOpen: { From 1ca9be9c3aa049c1a4887bd87261da04a3063040 Mon Sep 17 00:00:00 2001 From: ankit-thesys Date: Mon, 12 Jan 2026 11:53:02 +0530 Subject: [PATCH 08/41] styling and conversation starter type --- .cursor/rules/styling-rule.mdc | 639 ++++++++++++++---- .../react-ui/src/types/ConversationStarter.ts | 17 + 2 files changed, 526 insertions(+), 130 deletions(-) diff --git a/.cursor/rules/styling-rule.mdc b/.cursor/rules/styling-rule.mdc index f4e22a07b..6105e931b 100644 --- a/.cursor/rules/styling-rule.mdc +++ b/.cursor/rules/styling-rule.mdc @@ -88,128 +88,450 @@ Use the `.crayon-component-name` prefix with BEM-like modifiers: } ``` -### 3. Typography System +--- -#### Available Typography Mixins +## Color System + +### Background Colors + +| Token | Usage | +| ---------------------- | --------------------------------------- | +| `$bg-fill` | Main page/app background | +| `$bg-container` | Card/container backgrounds | +| `$bg-overlay` | Modal/overlay backgrounds | +| `$bg-sunk` | Input fields, recessed areas | +| `$bg-sunk-bg` | Alternative sunk background | +| `$bg-elevated` | Elevated surfaces (dropdowns, popovers) | +| `$bg-inverted` | Inverted/dark backgrounds | +| `$bg-danger` | Error/danger state backgrounds | +| `$bg-success` | Success state backgrounds | +| `$bg-info` | Informational backgrounds | +| `$bg-alert` | Warning/alert backgrounds | +| `$bg-highlight-subtle` | Subtle highlight (hover states) | +| `$bg-highlight-strong` | Strong highlight (selection) | ```scss -// Body text variants -@include cssUtils.typography(body, default); // Regular body text -@include cssUtils.typography(body, small); // Smaller body text -@include cssUtils.typography(body, large); // Larger body text -@include cssUtils.typography(body, heavy); // Bold body text - -// Label variants -@include cssUtils.typography(label, default); // Regular labels -@include cssUtils.typography(label, small); // Small labels -@include cssUtils.typography(label, large); // Large labels - -// Heading variants -@include cssUtils.typography(heading, large); // H1 equivalent -@include cssUtils.typography(heading, medium); // H2 equivalent -@include cssUtils.typography(heading, small); // H3 equivalent - -// Number variants (for data display) -@include cssUtils.typography(number, default); // Regular numbers -@include cssUtils.typography(number, large); // Large numbers -@include cssUtils.typography(number, title); // Title numbers +// Example usage +.crayon-card { + background-color: cssUtils.$bg-container; +} + +.crayon-modal-backdrop { + background-color: cssUtils.$bg-overlay; +} + +.crayon-input { + background-color: cssUtils.$bg-sunk; +} + +.crayon-error-banner { + background-color: cssUtils.$bg-danger; +} ``` -### 4. Color System +### Interactive Colors + +For buttons, clickable elements, and interactive states: + +| Token | Usage | +| ----------------------- | -------------------- | +| `$interactive-default` | Default state | +| `$interactive-hover` | Hover state | +| `$interactive-pressed` | Active/pressed state | +| `$interactive-disabled` | Disabled state | + +**Accent (Primary Actions):** -#### Background Colors +| Token | Usage | +| ------------------------------ | ----------------------- | +| `$interactive-accent` | Primary button default | +| `$interactive-accent-hover` | Primary button hover | +| `$interactive-accent-pressed` | Primary button pressed | +| `$interactive-accent-disabled` | Primary button disabled | + +**Destructive (Danger Actions):** + +| Token | Usage | +| ------------------------------------------ | --------------------------- | +| `$interactive-destructive` | Destructive button default | +| `$interactive-destructive-hover` | Destructive button hover | +| `$interactive-destructive-pressed` | Destructive button pressed | +| `$interactive-destructive-disabled` | Destructive button disabled | +| `$interactive-destructive-accent` | Filled destructive button | +| `$interactive-destructive-accent-hover` | Filled destructive hover | +| `$interactive-destructive-accent-pressed` | Filled destructive pressed | +| `$interactive-destructive-accent-disabled` | Filled destructive disabled | ```scss -cssUtils.$bg-fill // Main background -cssUtils.$bg-container // Card/container backgrounds -cssUtils.$bg-overlay // Modal/overlay backgrounds -cssUtils.$bg-sunk // Input field backgrounds -cssUtils.$bg-elevated // Elevated surfaces -cssUtils.$bg-danger // Error/danger backgrounds -cssUtils.$bg-success // Success backgrounds -cssUtils.$bg-info // Info backgrounds +// Primary button example +.crayon-button-primary { + background-color: cssUtils.$interactive-accent; + + &:not(:disabled):hover { + background-color: cssUtils.$interactive-accent-hover; + } + + &:not(:disabled):active { + background-color: cssUtils.$interactive-accent-pressed; + } + + &:disabled { + background-color: cssUtils.$interactive-accent-disabled; + } +} + +// Destructive button example +.crayon-button-destructive { + background-color: cssUtils.$interactive-destructive-accent; + + &:not(:disabled):hover { + background-color: cssUtils.$interactive-destructive-accent-hover; + } +} ``` -#### Interactive Colors +### Text Colors + +**Primary Text:** + +| Token | Usage | +| ----------------- | ---------------------------------- | +| `$primary-text` | Main body text | +| `$secondary-text` | Secondary/muted text, placeholders | +| `$disabled-text` | Disabled text | +| `$link-text` | Hyperlinks | + +**Accent Text:** + +| Token | Usage | +| ------------------------ | ------------------------------------ | +| `$accent-primary-text` | Text on accent backgrounds | +| `$accent-secondary-text` | Secondary text on accent backgrounds | +| `$accent-disabled-text` | Disabled text on accent backgrounds | + +**Status Text:** + +| Token | Usage | +| ------------------------ | --------------------------- | +| `$success-primary-text` | Success messages | +| `$success-inverted-text` | Text on success backgrounds | +| `$alert-primary-text` | Warning messages | +| `$alert-inverted-text` | Text on alert backgrounds | +| `$info-primary-text` | Info messages | +| `$info-inverted-text` | Text on info backgrounds | + +**Danger Text:** + +| Token | Usage | +| --------------------------------- | ------------------------------------ | +| `$danger-primary-text` | Error messages | +| `$danger-secondary-text` | Secondary error text | +| `$danger-disabled-text` | Disabled error text | +| `$danger-inverted-primary-text` | Primary text on danger backgrounds | +| `$danger-inverted-secondary-text` | Secondary text on danger backgrounds | +| `$danger-inverted-disabled-text` | Disabled text on danger backgrounds | ```scss -cssUtils.$interactive-default // Default button background -cssUtils.$interactive-hover // Hover state -cssUtils.$interactive-pressed // Pressed/active state -cssUtils.$interactive-disabled // Disabled state -cssUtils.$interactive-accent // Primary action color +// Example usage +.crayon-label { + color: cssUtils.$secondary-text; +} + +.crayon-error-message { + color: cssUtils.$danger-primary-text; +} + +.crayon-link { + color: cssUtils.$link-text; +} ``` -#### Text Colors +### Stroke/Border Colors + +| Token | Usage | +| --------------------------------- | ------------------------------------ | +| `$stroke-default` | Default borders | +| `$stroke-emphasis` | Emphasized borders | +| `$stroke-interactive-el` | Interactive element borders | +| `$stroke-interactive-el-selected` | Selected interactive element borders | + +**Semantic Strokes:** + +| Token | Usage | +| -------------------------- | -------------------------- | +| `$stroke-accent` | Accent/brand borders | +| `$stroke-accent-emphasis` | Emphasized accent borders | +| `$stroke-success` | Success state borders | +| `$stroke-success-emphasis` | Emphasized success borders | +| `$stroke-info` | Info state borders | +| `$stroke-info-emphasis` | Emphasized info borders | +| `$stroke-alert` | Alert/warning borders | +| `$stroke-alert-emphasis` | Emphasized alert borders | +| `$stroke-danger` | Error state borders | +| `$stroke-danger-emphasis` | Emphasized error borders | ```scss -cssUtils.$primary-text // Main text color -cssUtils.$secondary-text // Secondary/muted text -cssUtils.$disabled-text // Disabled text -cssUtils.$accent-primary-text // Primary accent text -cssUtils.$success-primary-text // Success text -cssUtils.$danger-primary-text // Error text +// Example usage +.crayon-input { + border: 1px solid cssUtils.$stroke-default; + + &:focus { + border-color: cssUtils.$stroke-emphasis; + } + + &-error { + border-color: cssUtils.$stroke-danger-emphasis; + } +} ``` -#### Stroke/Border Colors +### Chat Colors + +For chat/conversation UI components: + +| Token | Usage | +| ------------------------------- | ------------------------------- | +| `$chat-container-bg` | Chat container background | +| `$chat-assistant-response-bg` | AI/assistant message background | +| `$chat-assistant-response-text` | AI/assistant message text | +| `$chat-user-response-bg` | User message background | +| `$chat-user-response-text` | User message text | ```scss -cssUtils.$stroke-default // Default borders -cssUtils.$stroke-interactive-el // Interactive element borders -cssUtils.$stroke-emphasis // Emphasis borders -cssUtils.$stroke-accent // Accent borders -cssUtils.$stroke-danger // Error borders +// Example chat message styling +.crayon-chat-message { + &-assistant { + background-color: cssUtils.$chat-assistant-response-bg; + color: cssUtils.$chat-assistant-response-text; + } + + &-user { + background-color: cssUtils.$chat-user-response-bg; + color: cssUtils.$chat-user-response-text; + } +} ``` -### 5. Spacing System +--- + +## Spacing System -Use the predefined spacing scale: +Use the predefined spacing scale consistently: + +| Token | Value | Usage | +| -------------- | ----- | ---------------------- | +| `$spacing-0` | 0px | No spacing | +| `$spacing-3xs` | 2px | Minimal spacing | +| `$spacing-2xs` | 4px | Very tight spacing | +| `$spacing-xs` | 8px | Tight spacing | +| `$spacing-s` | 12px | Small spacing | +| `$spacing-m` | 16px | Medium/default spacing | +| `$spacing-l` | 20px | Large spacing | +| `$spacing-xl` | 24px | Extra large spacing | +| `$spacing-2xl` | 32px | Section spacing | +| `$spacing-3xl` | 40px | Large section spacing | ```scss -cssUtils.$spacing-0 // 0px -cssUtils.$spacing-3xs // 2px -cssUtils.$spacing-2xs // 4px -cssUtils.$spacing-xs // 8px -cssUtils.$spacing-s // 12px -cssUtils.$spacing-m // 16px -cssUtils.$spacing-l // 20px -cssUtils.$spacing-xl // 24px -cssUtils.$spacing-2xl // 32px -cssUtils.$spacing-3xl // 40px +// Example usage +.crayon-card { + padding: cssUtils.$spacing-m; + gap: cssUtils.$spacing-s; +} + +.crayon-section { + margin-bottom: cssUtils.$spacing-2xl; +} ``` -### 6. Border Radius System +--- + +## Border Radius System + +| Token | Value | Usage | +| --------------- | ------ | ------------------------------ | +| `$rounded-0` | 0px | Sharp corners | +| `$rounded-3xs` | 2px | Minimal rounding | +| `$rounded-2xs` | 4px | Subtle rounding | +| `$rounded-xs` | 6px | Small rounding | +| `$rounded-s` | 8px | Standard small | +| `$rounded-m` | 12px | Standard medium | +| `$rounded-l` | 16px | Large rounding | +| `$rounded-xl` | 20px | Extra large rounding | +| `$rounded-2xl` | 24px | Very large rounding | +| `$rounded-3xl` | 28px | Maximum rounding | +| `$rounded-full` | 9999px | Fully rounded (pills, circles) | ```scss -cssUtils.$rounded-0 // 0px (sharp corners) -cssUtils.$rounded-3xs // 2px -cssUtils.$rounded-2xs // 4px -cssUtils.$rounded-xs // 6px -cssUtils.$rounded-s // 8px -cssUtils.$rounded-m // 12px -cssUtils.$rounded-l // 16px -cssUtils.$rounded-xl // 20px -cssUtils.$rounded-full // 9999px (fully rounded) +// Example usage +.crayon-button { + border-radius: cssUtils.$rounded-m; +} + +.crayon-avatar { + border-radius: cssUtils.$rounded-full; +} + +.crayon-card { + border-radius: cssUtils.$rounded-l; +} ``` -### 7. Shadow System +--- + +## Shadow System + +| Token | Usage | +| ------------- | ------------------------------ | +| `$shadow-s` | Subtle elevation (buttons) | +| `$shadow-m` | Medium elevation (cards) | +| `$shadow-l` | Large elevation (dropdowns) | +| `$shadow-xl` | Extra large elevation (modals) | +| `$shadow-2xl` | High elevation (popovers) | +| `$shadow-3xl` | Maximum elevation | ```scss -cssUtils.$shadow-s // Small shadow -cssUtils.$shadow-m // Medium shadow -cssUtils.$shadow-l // Large shadow -cssUtils.$shadow-xl // Extra large shadow -cssUtils.$shadow-2xl // 2X large shadow -cssUtils.$shadow-3xl // 3X large shadow +// Example usage +.crayon-dropdown { + box-shadow: cssUtils.$shadow-l; +} + +.crayon-modal { + box-shadow: cssUtils.$shadow-xl; +} ``` -## Component Patterns +--- + +## Typography System + +Use the `typography` mixin for consistent text styling: + +```scss +@include cssUtils.typography($category, $variant); +``` + +### Body Text + +| Variant | Usage | +| ------------- | ------------------ | +| `default` | Standard body text | +| `small` | Smaller body text | +| `small-heavy` | Small bold text | +| `medium` | Medium weight body | +| `large` | Larger body text | +| `large-heavy` | Large bold text | +| `heavy` | Bold body text | +| `link` | Link text styling | -### Standard Component Variants +```scss +.crayon-paragraph { + @include cssUtils.typography(body, default); +} -Most components should support these variants: +.crayon-caption { + @include cssUtils.typography(body, small); +} -#### Size Variants +.crayon-emphasis { + @include cssUtils.typography(body, heavy); +} +``` + +### Labels + +| Variant | Usage | +| --------------------- | ----------------------- | +| `default` | Standard labels | +| `heavy` | Bold labels | +| `small` | Small labels | +| `small-heavy` | Small bold labels | +| `medium` | Medium labels | +| `medium-heavy` | Medium bold labels | +| `large` | Large labels | +| `large-heavy` | Large bold labels | +| `extra-small` | Extra small labels | +| `extra-small-heavy` | Extra small bold labels | +| `2-extra-small` | Tiny labels | +| `2-extra-small-heavy` | Tiny bold labels | + +```scss +.crayon-form-label { + @include cssUtils.typography(label, default); +} + +.crayon-badge { + @include cssUtils.typography(label, small-heavy); +} +``` + +### Headings + +| Variant | Usage | +| ------------- | ------------- | +| `large` | H1 equivalent | +| `medium` | H2 equivalent | +| `small` | H3 equivalent | +| `extra-small` | H4 equivalent | + +```scss +.crayon-page-title { + @include cssUtils.typography(heading, large); +} + +.crayon-section-title { + @include cssUtils.typography(heading, medium); +} +``` + +### Numbers + +For data display and metrics: + +| Variant | Usage | +| ------------------- | ------------------------------ | +| `default` | Standard numbers | +| `small` | Small numbers | +| `small-heavy` | Small bold numbers | +| `large` | Large numbers | +| `large-heavy` | Large bold numbers | +| `heavy` | Bold numbers | +| `extra-small` | Extra small numbers | +| `extra-small-heavy` | Extra small bold numbers | +| `title` | Display numbers (hero metrics) | +| `title-medium` | Medium display numbers | + +```scss +.crayon-metric { + @include cssUtils.typography(number, large-heavy); +} + +.crayon-stat-value { + @include cssUtils.typography(number, title); +} +``` + +--- + +## Utility Mixins + +### Button Reset + +Removes default button styling: + +```scss +.crayon-icon-button { + @include cssUtils.button-reset; + // Add your custom styles + padding: cssUtils.$spacing-xs; + border-radius: cssUtils.$rounded-m; +} +``` + +--- + +## Component Patterns + +### Standard Size Variants ```scss &-small { @@ -228,7 +550,7 @@ Most components should support these variants: } ``` -#### Button Variants +### Button Variants ```scss &-primary { @@ -240,6 +562,10 @@ Most components should support these variants: background-color: cssUtils.$interactive-accent-hover; } + &:not(:disabled):active { + background-color: cssUtils.$interactive-accent-pressed; + } + &:disabled { background-color: cssUtils.$interactive-accent-disabled; cursor: not-allowed; @@ -254,6 +580,29 @@ Most components should support these variants: &:not(:disabled):hover { background-color: cssUtils.$interactive-hover; } + + &:not(:disabled):active { + background-color: cssUtils.$interactive-pressed; + } +} + +&-destructive { + background-color: cssUtils.$interactive-destructive-accent; + color: cssUtils.$danger-inverted-primary-text; + border-color: cssUtils.$stroke-danger; + + &:not(:disabled):hover { + background-color: cssUtils.$interactive-destructive-accent-hover; + } + + &:not(:disabled):active { + background-color: cssUtils.$interactive-destructive-accent-pressed; + } + + &:disabled { + background-color: cssUtils.$interactive-destructive-accent-disabled; + cursor: not-allowed; + } } ``` @@ -266,6 +615,7 @@ Most components should support these variants: border-radius: cssUtils.$rounded-m; background-color: cssUtils.$bg-sunk; color: cssUtils.$primary-text; + padding: cssUtils.$spacing-xs cssUtils.$spacing-s; &::placeholder { color: cssUtils.$secondary-text; @@ -285,9 +635,45 @@ Most components should support these variants: &-error { border-color: cssUtils.$stroke-danger-emphasis; } + + &-success { + border-color: cssUtils.$stroke-success-emphasis; + } +} +``` + +### Status Badges Pattern + +```scss +.crayon-badge { + @include cssUtils.typography(label, small-heavy); + padding: cssUtils.$spacing-2xs cssUtils.$spacing-xs; + border-radius: cssUtils.$rounded-full; + + &-success { + background-color: cssUtils.$bg-success; + color: cssUtils.$success-primary-text; + } + + &-danger { + background-color: cssUtils.$bg-danger; + color: cssUtils.$danger-primary-text; + } + + &-info { + background-color: cssUtils.$bg-info; + color: cssUtils.$info-primary-text; + } + + &-alert { + background-color: cssUtils.$bg-alert; + color: cssUtils.$alert-primary-text; + } } ``` +--- + ## Best Practices ### 1. Avoid Magic Numbers @@ -318,20 +704,43 @@ Use the `:not(:disabled)` pattern for hover/active states: } ``` -### 3. Responsive Design +### 3. Focus States (Accessibility) -For responsive components, use CSS custom properties that can be controlled via JavaScript: +Always include visible focus states: + +```scss +&:focus-visible { + outline: 2px solid cssUtils.$stroke-accent; + outline-offset: 2px; +} +``` + +### 4. Responsive Design + +Use CSS custom properties for values that can be controlled via JavaScript: ```scss .my-responsive-component { - gap: var(--component-gap, cssUtils.$spacing-m); - padding: var(--component-padding, cssUtils.$spacing-m); + gap: var(--component-gap, #{cssUtils.$spacing-m}); + padding: var(--component-padding, #{cssUtils.$spacing-m}); } ``` -### 4. Component Composition +### 5. CSS Custom Properties for Theming -Prefer composition over complex variants. Create small, focused components that can be combined: +```scss +.crayon-themeable-component { + --component-bg: #{cssUtils.$bg-container}; + --component-text: #{cssUtils.$primary-text}; + + background-color: var(--component-bg); + color: var(--component-text); +} +``` + +### 6. Component Composition + +Prefer composition over complex variants: ```tsx // ✅ Good - Composable @@ -356,33 +765,7 @@ Prefer composition over complex variants. Create small, focused components that /> ``` -### 5. CSS Custom Properties for Theming - -Use CSS custom properties for values that might need to be overridden: - -```scss -.crayon-themeable-component { - --component-bg: #{cssUtils.$bg-container}; - --component-text: #{cssUtils.$primary-text}; - - background-color: var(--component-bg); - color: var(--component-text); -} -``` - -### 6. Accessibility Considerations - -- Always include focus states -- Use appropriate color contrasts (handled by design tokens) -- Support keyboard navigation -- Include proper ARIA attributes in component markup - -### 7. Performance - -- Avoid deeply nested selectors -- Use efficient CSS selectors -- Minimize CSS specificity conflicts -- Consider CSS-in-JS only when necessary (prefer SCSS for static styles) +--- ## When to Create New Components @@ -405,6 +788,8 @@ Use CSS custom properties for values that might need to be overridden: - Layout-only components (use CSS Grid/Flexbox) - Text styling (use typography mixins) +--- + ## Migration Guide When updating existing components: @@ -415,18 +800,12 @@ When updating existing components: 4. **Add missing states** - Include hover, focus, disabled, active states 5. **Test thoroughly** - Verify all variants work correctly +--- + ## Tooling - **SCSS Compilation**: `pnpm build:scss` -- **Linting**: ESLint with custom rules -- **Storybook**: For component development and testing -- **Design Tokens**: All available in `cssUtils.scss` - -## Questions? - -When in doubt: - -1. Check existing components for similar patterns -2. Look at `cssUtils.scss` for available tokens -3. Ask in #design-system Slack channel -4. Reference the design system documentation +- **Watch Mode**: `pnpm watch` +- **Storybook**: `pnpm storybook` +- **Linting**: `pnpm lint:check` +- **Format**: `pnpm format:check` diff --git a/js/packages/react-ui/src/types/ConversationStarter.ts b/js/packages/react-ui/src/types/ConversationStarter.ts index dfd718af8..8bb0cec49 100644 --- a/js/packages/react-ui/src/types/ConversationStarter.ts +++ b/js/packages/react-ui/src/types/ConversationStarter.ts @@ -1,6 +1,23 @@ +import { ReactNode } from "react"; + +/** + * Icon type for conversation starters + * - undefined: Show default lightbulb icon + * - "": Show no icon + * - ReactNode: Show the provided icon + */ +export type ConversationStarterIcon = ReactNode | ""; + interface ConversationStarterProps { displayText: string; prompt: string; + /** + * Optional icon to display + * - If not provided (undefined): shows default lightbulb icon + * - If empty string (""): shows no icon + * - Otherwise: shows the provided React element + */ + icon?: ConversationStarterIcon; } export type { ConversationStarterProps }; From 85fc3ce5745cbf82a6d51ad5cd513583f9ff5cbc Mon Sep 17 00:00:00 2001 From: ankit-thesys Date: Mon, 12 Jan 2026 12:43:10 +0530 Subject: [PATCH 09/41] long variant style and story book mode, types export --- .../BottomTray/ConversationStarter.tsx | 106 +++++++++++++++--- .../BottomTray/conversationStarter.scss | 99 +++++++++++++++- .../BottomTray/stories/BottomTray.stories.tsx | 77 +++++++++++-- js/packages/react-ui/src/index.ts | 7 +- 4 files changed, 263 insertions(+), 26 deletions(-) diff --git a/js/packages/react-ui/src/components/BottomTray/ConversationStarter.tsx b/js/packages/react-ui/src/components/BottomTray/ConversationStarter.tsx index 4ebec72ee..f95e7d6bf 100644 --- a/js/packages/react-ui/src/components/BottomTray/ConversationStarter.tsx +++ b/js/packages/react-ui/src/components/BottomTray/ConversationStarter.tsx @@ -1,28 +1,81 @@ import { useThreadActions, useThreadState } from "@crayonai/react-core"; import clsx from "clsx"; -import { ConversationStarterProps } from "../../types/ConversationStarter"; +import { ArrowUp, Lightbulb } from "lucide-react"; +import { Fragment, ReactNode } from "react"; +import { + ConversationStarterIcon, + ConversationStarterProps, +} from "../../types/ConversationStarter"; +import { Separator } from "../Separator"; + +export type ConversationStarterVariant = "short" | "long"; interface ConversationStarterItemProps extends ConversationStarterProps { onClick: (prompt: string) => void; disabled?: boolean; + variant: ConversationStarterVariant; } +/** + * Renders the appropriate icon based on the icon prop value + * - undefined: Show default lightbulb icon + * - "": Show no icon + * - ReactNode: Show the provided icon + */ +const renderIcon = (icon: ConversationStarterIcon | undefined): ReactNode => { + if (icon === "") { + return null; + } + if (icon === undefined) { + return ; + } + return icon; +}; + const ConversationStarterItem = ({ displayText, prompt, onClick, disabled = false, + variant, + icon, }: ConversationStarterItemProps) => { + const renderedIcon = renderIcon(icon); + + if (variant === "short") { + return ( + + ); + } + + // Long variant (detailed list style) return ( ); }; @@ -30,9 +83,19 @@ const ConversationStarterItem = ({ export interface ConversationStarterContainerProps { starters: ConversationStarterProps[]; className?: string; + /** + * Variant of the conversation starter + * - "short": Pill-style horizontal buttons (default) + * - "long": List items with icons and hover arrow + */ + variant?: ConversationStarterVariant; } -export const ConversationStarter = ({ starters, className }: ConversationStarterContainerProps) => { +export const ConversationStarter = ({ + starters, + className, + variant = "short", +}: ConversationStarterContainerProps) => { const { processMessage } = useThreadActions(); const { isRunning, messages } = useThreadState(); @@ -55,15 +118,30 @@ export const ConversationStarter = ({ starters, className }: ConversationStarter } return ( -
- {starters.map((item) => ( - +
+ {starters.map((item, index) => ( + + + {/* Add separator between items in long variant */} + {variant === "long" && index < starters.length - 1 && ( +
+ +
+ )} +
))}
); diff --git a/js/packages/react-ui/src/components/BottomTray/conversationStarter.scss b/js/packages/react-ui/src/components/BottomTray/conversationStarter.scss index eefcc6821..fc0e72cd4 100644 --- a/js/packages/react-ui/src/components/BottomTray/conversationStarter.scss +++ b/js/packages/react-ui/src/components/BottomTray/conversationStarter.scss @@ -1,14 +1,31 @@ @use "../../cssUtils" as cssUtils; +// Container styles .crayon-conversation-starter { display: flex; - flex-direction: column; - gap: cssUtils.$spacing-s; padding: 0 cssUtils.$spacing-s; margin-bottom: cssUtils.$spacing-s; + + // Short variant - horizontal pill buttons + &--short { + flex-direction: column; + gap: cssUtils.$spacing-s; + } + + // Long variant - vertical list items with separators + &--long { + flex-direction: column; + gap: cssUtils.$spacing-2xs; + } + + // Separator wrapper for long variant + &__separator { + padding: cssUtils.$spacing-3xs cssUtils.$spacing-xs; + } } -.crayon-conversation-starter-item { +// Short variant item (pill-style buttons) +.crayon-conversation-starter-item-short { width: 100%; padding: cssUtils.$spacing-m cssUtils.$spacing-l; background-color: cssUtils.$bg-container; @@ -34,3 +51,79 @@ cursor: not-allowed; } } + +// Long variant item (list-style with icon and arrow) +.crayon-conversation-starter-item-long { + display: flex; + align-items: center; + gap: cssUtils.$spacing-0; + width: 100%; + padding: cssUtils.$spacing-xs cssUtils.$spacing-xs; + background-color: transparent; + border: none; + border-radius: cssUtils.$rounded-m; + cursor: pointer; + transition: background-color 0.15s ease; + @include cssUtils.typography(body, default); + color: cssUtils.$primary-text; + text-align: left; + overflow: hidden; + + // Content wrapper (icon + text) + &__content { + display: flex; + align-items: flex-start; + gap: cssUtils.$spacing-xs; + flex: 1; + min-width: 0; + } + + // Icon container - aligned to top with padding to align with first line of text + &__icon { + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + padding-top: cssUtils.$spacing-2xs; + color: cssUtils.$primary-text; + } + + // Text - allows wrapping for long content + &__text { + flex: 1; + min-width: 0; + line-height: 1.5; + // Text wraps naturally, no truncation + } + + // Arrow icon (shown on hover) + &__arrow { + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + opacity: 0; + transition: opacity 0.15s ease; + color: cssUtils.$primary-text; + } + + // Hover state + &:not(:disabled):hover { + background-color: cssUtils.$bg-sunk; + + .crayon-conversation-starter-item-long__arrow { + opacity: 1; + } + } + + // Active/pressed state + &:not(:disabled):active { + background-color: cssUtils.$interactive-pressed; + } + + // Disabled state + &--disabled { + opacity: 0.5; + cursor: not-allowed; + } +} diff --git a/js/packages/react-ui/src/components/BottomTray/stories/BottomTray.stories.tsx b/js/packages/react-ui/src/components/BottomTray/stories/BottomTray.stories.tsx index 6f6ae03b8..361bbc3c6 100644 --- a/js/packages/react-ui/src/components/BottomTray/stories/BottomTray.stories.tsx +++ b/js/packages/react-ui/src/components/BottomTray/stories/BottomTray.stories.tsx @@ -4,6 +4,7 @@ import { useThreadListManager, useThreadManager, } from "@crayonai/react-core"; +import { HelpCircle, MessageSquare, Sparkles, Zap } from "lucide-react"; import { useState } from "react"; import { Composer, @@ -28,10 +29,21 @@ export default { control: "boolean", description: "Whether the tray starts open", }, + variant: { + control: "select", + options: ["short", "long"], + description: "Conversation starter variant", + }, }, }; -const BottomTrayStory = ({ defaultOpen = false }: { defaultOpen?: boolean }) => { +const BottomTrayStory = ({ + defaultOpen = false, + variant = "short", +}: { + defaultOpen?: boolean; + variant?: "short" | "long"; +}) => { const [isOpen, setIsOpen] = useState(defaultOpen); const threadListManager = useThreadListManager({ @@ -139,12 +151,22 @@ const BottomTrayStory = ({ defaultOpen = false }: { defaultOpen?: boolean }) => } /> , + }, + { + displayText: "Who is the president of Venezuela and where is he currently located (icon was not passed)", + prompt: "Who is the president of Venezuela and where is he currently located?", + // icon undefined = shows default lightbulb + }, + { + displayText: "Tell me about major stock (no icon empty string''", + prompt: "Tell me about major stock", + icon: "", }, ]} /> @@ -159,6 +181,7 @@ const BottomTrayStory = ({ defaultOpen = false }: { defaultOpen?: boolean }) => export const Default = { args: { defaultOpen: false, + variant: "short", }, render: (args: any) => , }; @@ -166,12 +189,27 @@ export const Default = { export const OpenByDefault = { args: { defaultOpen: true, + variant: "short", + }, + render: (args: any) => , +}; + +export const LongVariant = { + args: { + defaultOpen: true, + variant: "long", }, render: (args: any) => , }; // Example with custom trigger -const CustomTriggerStory = ({ defaultOpen = false }: { defaultOpen?: boolean }) => { +const CustomTriggerStory = ({ + defaultOpen = false, + variant = "short", +}: { + defaultOpen?: boolean; + variant?: "short" | "long"; +}) => { const [isOpen, setIsOpen] = useState(defaultOpen); const threadListManager = useThreadListManager({ @@ -254,9 +292,23 @@ const CustomTriggerStory = ({ defaultOpen = false }: { defaultOpen?: boolean }) } /> , + }, + { + displayText: "What can you help me with today? I need assistance with multiple tasks", + prompt: "What can you help me with today? I need assistance with multiple tasks", + icon: , + }, + { + displayText: "No icon example - this is a shorter prompt", + prompt: "No icon example - this is a shorter prompt", + icon: "", // Empty string = no icon + }, ]} /> @@ -270,6 +322,15 @@ const CustomTriggerStory = ({ defaultOpen = false }: { defaultOpen?: boolean }) export const CustomTrigger = { args: { defaultOpen: false, + variant: "short", + }, + render: (args: any) => , +}; + +export const CustomTriggerLongVariant = { + args: { + defaultOpen: true, + variant: "long", }, render: (args: any) => , }; diff --git a/js/packages/react-ui/src/index.ts b/js/packages/react-ui/src/index.ts index 5e908fb69..c6afc0cb8 100644 --- a/js/packages/react-ui/src/index.ts +++ b/js/packages/react-ui/src/index.ts @@ -53,4 +53,9 @@ export * from "./context/LayoutContext"; export * from "./context/PrintContext"; -export type { ConversationStarterProps } from "./types/ConversationStarter"; +// Types Export +export type { + ConversationStarterProps, + ConversationStarterIcon, +} from "./types/ConversationStarter"; +export type { ConversationStarterVariant } from "./components/BottomTray/ConversationStarter"; From f01eb4b229d71bc5a8d1189fad04bbe5734a95b1 Mon Sep 17 00:00:00 2001 From: ankit-thesys Date: Mon, 12 Jan 2026 12:56:08 +0530 Subject: [PATCH 10/41] Update BottomTray styles and stories for conversation starters * Adjusted the styling of short variant conversation starter items for better responsiveness and appearance. * Modified story examples to reflect updated display texts and prompts for clarity and relevance. --- .../BottomTray/conversationStarter.scss | 8 ++++++-- .../BottomTray/stories/BottomTray.stories.tsx | 19 ++++++++++++------- 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/js/packages/react-ui/src/components/BottomTray/conversationStarter.scss b/js/packages/react-ui/src/components/BottomTray/conversationStarter.scss index fc0e72cd4..aba6d31d0 100644 --- a/js/packages/react-ui/src/components/BottomTray/conversationStarter.scss +++ b/js/packages/react-ui/src/components/BottomTray/conversationStarter.scss @@ -26,8 +26,8 @@ // Short variant item (pill-style buttons) .crayon-conversation-starter-item-short { - width: 100%; - padding: cssUtils.$spacing-m cssUtils.$spacing-l; + width: fit-content; + padding: cssUtils.$spacing-s cssUtils.$spacing-m; background-color: cssUtils.$bg-container; border: 1px solid cssUtils.$stroke-default; border-radius: cssUtils.$rounded-m; @@ -50,6 +50,10 @@ opacity: 0.5; cursor: not-allowed; } + + @media (max-width: 480px) { + padding: cssUtils.$spacing-xs cssUtils.$spacing-s; + } } // Long variant item (list-style with icon and arrow) diff --git a/js/packages/react-ui/src/components/BottomTray/stories/BottomTray.stories.tsx b/js/packages/react-ui/src/components/BottomTray/stories/BottomTray.stories.tsx index 361bbc3c6..9d5ca7798 100644 --- a/js/packages/react-ui/src/components/BottomTray/stories/BottomTray.stories.tsx +++ b/js/packages/react-ui/src/components/BottomTray/stories/BottomTray.stories.tsx @@ -4,7 +4,7 @@ import { useThreadListManager, useThreadManager, } from "@crayonai/react-core"; -import { HelpCircle, MessageSquare, Sparkles, Zap } from "lucide-react"; +import { MessageSquare, Sparkles, Zap } from "lucide-react"; import { useState } from "react"; import { Composer, @@ -154,12 +154,14 @@ const BottomTrayStory = ({ variant={variant} starters={[ { - displayText: "Tell me about the latest stock market trends and how they affect my portfolio", - prompt: "Tell me about the latest stock market trends and how they affect my portfolio", + displayText: "Tell me about my portfolio", + prompt: + "Tell me about the latest stock market trends and how they affect my portfolio", icon: , }, { - displayText: "Who is the president of Venezuela and where is he currently located (icon was not passed)", + displayText: + "Who is the president of Venezuela and where is he currently located (icon was not passed)", prompt: "Who is the president of Venezuela and where is he currently located?", // icon undefined = shows default lightbulb }, @@ -295,12 +297,15 @@ const CustomTriggerStory = ({ variant={variant} starters={[ { - displayText: "Help me understand what features are available and how to get started with this application", - prompt: "Help me understand what features are available and how to get started with this application", + displayText: + "Help me understand what features are available and how to get started with this application", + prompt: + "Help me understand what features are available and how to get started with this application", icon: , }, { - displayText: "What can you help me with today? I need assistance with multiple tasks", + displayText: + "What can you help me with today? I need assistance with multiple tasks", prompt: "What can you help me with today? I need assistance with multiple tasks", icon: , }, From 00d0e35c31518377522632c8a5cdeab60f34b263 Mon Sep 17 00:00:00 2001 From: ankit-thesys Date: Mon, 12 Jan 2026 13:08:43 +0530 Subject: [PATCH 11/41] Enhance ConversationStarter component with variant support and styling updates * Introduced support for "short" and "long" variants in the ConversationStarter component, allowing for flexible display options. * Updated styles for both variants to improve layout and responsiveness, including icon and text alignment. * Modified stories to demonstrate the new variant functionality and provide clearer examples with icons and prompts. --- .../BottomTray/ConversationStarter.tsx | 5 +- .../BottomTray/conversationStarter.scss | 17 +++ .../CopilotShell/ConversationStarter.tsx | 117 ++++++++++++++-- .../CopilotShell/conversationStarter.scss | 127 +++++++++++++++++- .../CopilotShell/stories/Shell.stories.tsx | 42 ++++-- 5 files changed, 276 insertions(+), 32 deletions(-) diff --git a/js/packages/react-ui/src/components/BottomTray/ConversationStarter.tsx b/js/packages/react-ui/src/components/BottomTray/ConversationStarter.tsx index f95e7d6bf..c046366df 100644 --- a/js/packages/react-ui/src/components/BottomTray/ConversationStarter.tsx +++ b/js/packages/react-ui/src/components/BottomTray/ConversationStarter.tsx @@ -52,7 +52,10 @@ const ConversationStarterItem = ({ onClick={() => onClick(prompt)} disabled={disabled} > - {displayText} + {renderedIcon && ( + {renderedIcon} + )} + {displayText} ); } diff --git a/js/packages/react-ui/src/components/BottomTray/conversationStarter.scss b/js/packages/react-ui/src/components/BottomTray/conversationStarter.scss index aba6d31d0..28698b913 100644 --- a/js/packages/react-ui/src/components/BottomTray/conversationStarter.scss +++ b/js/packages/react-ui/src/components/BottomTray/conversationStarter.scss @@ -26,6 +26,9 @@ // Short variant item (pill-style buttons) .crayon-conversation-starter-item-short { + display: flex; + align-items: center; + gap: cssUtils.$spacing-xs; width: fit-content; padding: cssUtils.$spacing-s cssUtils.$spacing-m; background-color: cssUtils.$bg-container; @@ -37,6 +40,20 @@ color: cssUtils.$primary-text; text-align: left; + // Icon container + &__icon { + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + color: cssUtils.$primary-text; + } + + // Text + &__text { + flex: 1; + } + &:not(:disabled):hover { background-color: cssUtils.$interactive-hover; border-color: cssUtils.$stroke-emphasis; diff --git a/js/packages/react-ui/src/components/CopilotShell/ConversationStarter.tsx b/js/packages/react-ui/src/components/CopilotShell/ConversationStarter.tsx index 15d593866..36bf8306e 100644 --- a/js/packages/react-ui/src/components/CopilotShell/ConversationStarter.tsx +++ b/js/packages/react-ui/src/components/CopilotShell/ConversationStarter.tsx @@ -1,28 +1,92 @@ import { useThreadActions, useThreadState } from "@crayonai/react-core"; import clsx from "clsx"; -import { ConversationStarterProps } from "../../types/ConversationStarter"; +import { ArrowUp, Lightbulb } from "lucide-react"; +import { Fragment, ReactNode } from "react"; +import { + ConversationStarterIcon, + ConversationStarterProps, +} from "../../types/ConversationStarter"; +import { Separator } from "../Separator"; + +export type ConversationStarterVariant = "short" | "long"; interface ConversationStarterItemProps extends ConversationStarterProps { onClick: (prompt: string) => void; disabled?: boolean; + variant: ConversationStarterVariant; } +/** + * Renders the appropriate icon based on the icon prop value + * - undefined: Show default lightbulb icon + * - "": Show no icon + * - ReactNode: Show the provided icon + */ +const renderIcon = (icon: ConversationStarterIcon | undefined): ReactNode => { + if (icon === "") { + return null; + } + if (icon === undefined) { + return ; + } + return icon; +}; + const ConversationStarterItem = ({ displayText, prompt, onClick, disabled = false, + variant, + icon, }: ConversationStarterItemProps) => { + const renderedIcon = renderIcon(icon); + + if (variant === "short") { + return ( + + ); + } + + // Long variant (detailed list style) return ( ); }; @@ -30,9 +94,19 @@ const ConversationStarterItem = ({ export interface ConversationStarterContainerProps { starters: ConversationStarterProps[]; className?: string; + /** + * Variant of the conversation starter + * - "short": Pill-style horizontal buttons (default) + * - "long": List items with icons and hover arrow + */ + variant?: ConversationStarterVariant; } -export const ConversationStarter = ({ starters, className }: ConversationStarterContainerProps) => { +export const ConversationStarter = ({ + starters, + className, + variant = "short", +}: ConversationStarterContainerProps) => { const { processMessage } = useThreadActions(); const { isRunning, messages } = useThreadState(); @@ -55,15 +129,30 @@ export const ConversationStarter = ({ starters, className }: ConversationStarter } return ( -
- {starters.map((item) => ( - +
+ {starters.map((item, index) => ( + + + {/* Add separator between items in long variant */} + {variant === "long" && index < starters.length - 1 && ( +
+ +
+ )} +
))}
); diff --git a/js/packages/react-ui/src/components/CopilotShell/conversationStarter.scss b/js/packages/react-ui/src/components/CopilotShell/conversationStarter.scss index 7b1d5d001..86aa4ed2f 100644 --- a/js/packages/react-ui/src/components/CopilotShell/conversationStarter.scss +++ b/js/packages/react-ui/src/components/CopilotShell/conversationStarter.scss @@ -1,15 +1,36 @@ @use "../../cssUtils" as cssUtils; +// Container styles .crayon-copilot-shell-conversation-starter { display: flex; - flex-direction: column; - gap: cssUtils.$spacing-s; - padding: 0 cssUtils.$spacing-l cssUtils.$spacing-m; + padding: 0 cssUtils.$spacing-s; + margin-bottom: cssUtils.$spacing-s; + + // Short variant - horizontal pill buttons + &--short { + flex-direction: column; + gap: cssUtils.$spacing-s; + } + + // Long variant - vertical list items with separators + &--long { + flex-direction: column; + gap: cssUtils.$spacing-2xs; + } + + // Separator wrapper for long variant + &__separator { + padding: cssUtils.$spacing-3xs cssUtils.$spacing-xs; + } } -.crayon-copilot-shell-conversation-starter-item { - width: 100%; - padding: cssUtils.$spacing-m cssUtils.$spacing-l; +// Short variant item (pill-style buttons) +.crayon-copilot-shell-conversation-starter-item-short { + display: flex; + align-items: center; + gap: cssUtils.$spacing-xs; + width: fit-content; + padding: cssUtils.$spacing-s cssUtils.$spacing-m; background-color: cssUtils.$bg-container; border: 1px solid cssUtils.$stroke-default; border-radius: cssUtils.$rounded-m; @@ -19,6 +40,20 @@ color: cssUtils.$primary-text; text-align: left; + // Icon container + &__icon { + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + color: cssUtils.$primary-text; + } + + // Text + &__text { + flex: 1; + } + &:not(:disabled):hover { background-color: cssUtils.$interactive-hover; border-color: cssUtils.$stroke-emphasis; @@ -32,4 +67,84 @@ opacity: 0.5; cursor: not-allowed; } + + @media (max-width: 480px) { + padding: cssUtils.$spacing-xs cssUtils.$spacing-s; + } +} + +// Long variant item (list-style with icon and arrow) +.crayon-copilot-shell-conversation-starter-item-long { + display: flex; + align-items: center; + gap: cssUtils.$spacing-0; + width: 100%; + padding: cssUtils.$spacing-xs cssUtils.$spacing-xs; + background-color: transparent; + border: none; + border-radius: cssUtils.$rounded-m; + cursor: pointer; + transition: background-color 0.15s ease; + @include cssUtils.typography(body, default); + color: cssUtils.$primary-text; + text-align: left; + overflow: hidden; + + // Content wrapper (icon + text) + &__content { + display: flex; + align-items: flex-start; + gap: cssUtils.$spacing-xs; + flex: 1; + min-width: 0; + } + + // Icon container - aligned to top with padding to align with first line of text + &__icon { + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + padding-top: cssUtils.$spacing-2xs; + color: cssUtils.$primary-text; + } + + // Text - allows wrapping for long content + &__text { + flex: 1; + min-width: 0; + line-height: 1.5; + // Text wraps naturally, no truncation + } + + // Arrow icon (shown on hover) + &__arrow { + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + opacity: 0; + transition: opacity 0.15s ease; + color: cssUtils.$primary-text; + } + + // Hover state + &:not(:disabled):hover { + background-color: cssUtils.$bg-sunk; + + .crayon-copilot-shell-conversation-starter-item-long__arrow { + opacity: 1; + } + } + + // Active/pressed state + &:not(:disabled):active { + background-color: cssUtils.$interactive-pressed; + } + + // Disabled state + &--disabled { + opacity: 0.5; + cursor: not-allowed; + } } diff --git a/js/packages/react-ui/src/components/CopilotShell/stories/Shell.stories.tsx b/js/packages/react-ui/src/components/CopilotShell/stories/Shell.stories.tsx index e9f8c0f8f..dd435cbd8 100644 --- a/js/packages/react-ui/src/components/CopilotShell/stories/Shell.stories.tsx +++ b/js/packages/react-ui/src/components/CopilotShell/stories/Shell.stories.tsx @@ -4,6 +4,7 @@ import { useThreadListManager, useThreadManager, } from "@crayonai/react-core"; +import { Sparkles } from "lucide-react"; import { Composer, Container, @@ -22,21 +23,37 @@ export default { title: "Components/CopilotShell", tags: ["dev"], argTypes: { - defaultOpen: { - control: "boolean", - description: "Whether to start with messages", + variant: { + control: "select", + options: ["short", "long"], + description: "Conversation starter variant", }, }, }; const SAMPLE_STARTERS = [ - { displayText: "Help me get started", prompt: "Help me get started" }, - { displayText: "What can you do?", prompt: "What can you do?" }, - { displayText: "Tell me about your features", prompt: "Tell me about your features" }, + { + displayText: "Tell me about my portfolio", + prompt: "Tell me about the latest stock market trends and how they affect my portfolio", + icon: , + }, + { + displayText: "Who is the president of Venezuela and where is he currently located?", + prompt: "Who is the president of Venezuela and where is he currently located?", + // icon undefined = shows default lightbulb + }, + { + displayText: "Tell me about major stock (no icon)", + prompt: "Tell me about major stock", + icon: "", // Empty string = no icon + }, ]; export const Default = { - render: () => { + args: { + variant: "short", + }, + render: ({ variant }: { variant: "short" | "long" }) => { const threadListManager = useThreadListManager({ createThread: async () => { return { @@ -122,7 +139,7 @@ export const Default = { } /> - + @@ -132,8 +149,11 @@ export const Default = { }, }; -export const WithConversationStarter = { - render: () => { +export const LongVariant = { + args: { + variant: "long", + }, + render: ({ variant }: { variant: "short" | "long" }) => { const threadListManager = useThreadListManager({ createThread: async () => ({ threadId: crypto.randomUUID(), @@ -179,7 +199,7 @@ export const WithConversationStarter = { } /> - + From 444fa5752601233671208fb70c46324c7fa3da2a Mon Sep 17 00:00:00 2001 From: ankit-thesys Date: Mon, 12 Jan 2026 15:12:23 +0530 Subject: [PATCH 12/41] Update BottomTray and CopilotShell styles for ConversationStarter component * Added import for welcomeScreen.scss in BottomTray styles. * Changed hover and active background colors in ConversationStarter styles for both BottomTray and CopilotShell to use $bg-sunk for a consistent look. --- .../react-ui/src/components/BottomTray/bottomTray.scss | 1 + .../src/components/BottomTray/conversationStarter.scss | 4 ++-- .../src/components/CopilotShell/conversationStarter.scss | 4 ++-- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/js/packages/react-ui/src/components/BottomTray/bottomTray.scss b/js/packages/react-ui/src/components/BottomTray/bottomTray.scss index fc7b52961..164830724 100644 --- a/js/packages/react-ui/src/components/BottomTray/bottomTray.scss +++ b/js/packages/react-ui/src/components/BottomTray/bottomTray.scss @@ -7,3 +7,4 @@ @use "./threadList.scss"; @use "./thread.scss"; @use "./conversationStarter.scss"; +@use "./welcomeScreen.scss"; diff --git a/js/packages/react-ui/src/components/BottomTray/conversationStarter.scss b/js/packages/react-ui/src/components/BottomTray/conversationStarter.scss index 28698b913..6a1b076b6 100644 --- a/js/packages/react-ui/src/components/BottomTray/conversationStarter.scss +++ b/js/packages/react-ui/src/components/BottomTray/conversationStarter.scss @@ -55,12 +55,12 @@ } &:not(:disabled):hover { - background-color: cssUtils.$interactive-hover; + background-color: cssUtils.$bg-sunk; border-color: cssUtils.$stroke-emphasis; } &:not(:disabled):active { - background-color: cssUtils.$interactive-pressed; + background-color: cssUtils.$bg-sunk; } &--disabled { diff --git a/js/packages/react-ui/src/components/CopilotShell/conversationStarter.scss b/js/packages/react-ui/src/components/CopilotShell/conversationStarter.scss index 86aa4ed2f..cbd55f93b 100644 --- a/js/packages/react-ui/src/components/CopilotShell/conversationStarter.scss +++ b/js/packages/react-ui/src/components/CopilotShell/conversationStarter.scss @@ -55,12 +55,12 @@ } &:not(:disabled):hover { - background-color: cssUtils.$interactive-hover; + background-color: cssUtils.$bg-sunk; border-color: cssUtils.$stroke-emphasis; } &:not(:disabled):active { - background-color: cssUtils.$interactive-pressed; + background-color: cssUtils.$bg-sunk; } &--disabled { From 8129e49e7f073bffa5d9aff8c637a106517101c7 Mon Sep 17 00:00:00 2001 From: ankit-thesys Date: Mon, 12 Jan 2026 15:12:38 +0530 Subject: [PATCH 13/41] Add WelcomeScreen component to BottomTray and enhance stories * Exported WelcomeScreen component from BottomTray. * Created new stories to demonstrate WelcomeScreen with default and custom children, showcasing its integration and functionality within the BottomTray component. --- .../components/BottomTray/WelcomeScreen.tsx | 112 +++++++++ .../src/components/BottomTray/index.ts | 1 + .../BottomTray/stories/BottomTray.stories.tsx | 220 ++++++++++++++++++ .../components/BottomTray/welcomeScreen.scss | 55 +++++ 4 files changed, 388 insertions(+) create mode 100644 js/packages/react-ui/src/components/BottomTray/WelcomeScreen.tsx create mode 100644 js/packages/react-ui/src/components/BottomTray/welcomeScreen.scss diff --git a/js/packages/react-ui/src/components/BottomTray/WelcomeScreen.tsx b/js/packages/react-ui/src/components/BottomTray/WelcomeScreen.tsx new file mode 100644 index 000000000..0ad2fa9a8 --- /dev/null +++ b/js/packages/react-ui/src/components/BottomTray/WelcomeScreen.tsx @@ -0,0 +1,112 @@ +import { useThreadState } from "@crayonai/react-core"; +import clsx from "clsx"; +import { ReactNode } from "react"; + +interface WelcomeScreenBaseProps { + /** + * Additional CSS class name + */ + className?: string; +} + +interface WelcomeScreenWithContentProps extends WelcomeScreenBaseProps { + /** + * The greeting/title text to display + */ + title?: string; + /** + * Optional description text to add more context + */ + description?: string; + /** + * Logo URL to display as an image + */ + logoUrl?: string; + /** + * Custom icon to display instead of the default image icon + * If logoUrl is provided, this will be ignored + */ + icon?: ReactNode; + /** + * Children are not allowed when using props-based content + */ + children?: never; +} + +interface WelcomeScreenWithChildrenProps extends WelcomeScreenBaseProps { + /** + * Custom content to render inside the welcome screen + * When children are provided, title, description, logoUrl, and icon are ignored + */ + children: ReactNode; + title?: never; + description?: never; + logoUrl?: never; + icon?: never; +} + +export type WelcomeScreenProps = + | WelcomeScreenWithContentProps + | WelcomeScreenWithChildrenProps; + +export const WelcomeScreen = (props: WelcomeScreenProps) => { + const { className } = props; + const { messages } = useThreadState(); + + // Only show when there are no messages + // TODO: check with @abhithesys if this is correct + if (messages.length > 0) { + return null; + } + + // Check if children are provided + if ("children" in props && props.children) { + return ( +
+ {props.children} +
+ ); + } + + // Props-based content + const { title, description, logoUrl, icon } = + props as WelcomeScreenWithContentProps; + + const renderIcon = () => { + if (logoUrl) { + return ( + {title} + ); + } + + if (icon) { + return icon; + } + + return null; + }; + + return ( +
+ {(logoUrl || icon) && ( +
+ {renderIcon()} +
+ )} + {(title || description) && ( +
+ {title &&

{title}

} + {description && ( +

{description}

+ )} +
+ )} +
+ ); +}; + +export default WelcomeScreen; diff --git a/js/packages/react-ui/src/components/BottomTray/index.ts b/js/packages/react-ui/src/components/BottomTray/index.ts index 7648305c9..02ccb680d 100644 --- a/js/packages/react-ui/src/components/BottomTray/index.ts +++ b/js/packages/react-ui/src/components/BottomTray/index.ts @@ -3,3 +3,4 @@ export * from "./ConversationStarter"; export * from "./Header"; export * from "./Thread"; export * from "./Trigger"; +export * from "./WelcomeScreen"; \ No newline at end of file diff --git a/js/packages/react-ui/src/components/BottomTray/stories/BottomTray.stories.tsx b/js/packages/react-ui/src/components/BottomTray/stories/BottomTray.stories.tsx index 9d5ca7798..390872730 100644 --- a/js/packages/react-ui/src/components/BottomTray/stories/BottomTray.stories.tsx +++ b/js/packages/react-ui/src/components/BottomTray/stories/BottomTray.stories.tsx @@ -16,6 +16,7 @@ import { ScrollArea, ThreadContainer, Trigger, + WelcomeScreen, } from "../../BottomTray"; // @ts-ignore import styles from "./style.module.scss"; @@ -339,3 +340,222 @@ export const CustomTriggerLongVariant = { }, render: (args: any) => , }; + +// Example with WelcomeScreen +const WelcomeScreenStory = ({ + defaultOpen = true, + variant = "short", +}: { + defaultOpen?: boolean; + variant?: "short" | "long"; +}) => { + const [isOpen, setIsOpen] = useState(defaultOpen); + + const threadListManager = useThreadListManager({ + createThread: async () => ({ + threadId: crypto.randomUUID(), + title: "New Chat", + createdAt: new Date(), + isRunning: false, + }), + fetchThreadList: async () => [], + deleteThread: async () => {}, + updateThread: async (t) => t, + onSwitchToNew: () => {}, + onSelectThread: () => {}, + }); + + const threadManager = useThreadManager({ + threadId: threadListManager.selectedThreadId, + loadThread: async () => [], // Start with empty thread to show welcome screen + onProcessMessage: async ({ message, threadManager }) => { + const newMessage = Object.assign({}, message, { id: crypto.randomUUID() }) as Message; + threadManager.appendMessages(newMessage); + await new Promise((resolve) => setTimeout(resolve, 1000)); + return [ + { + id: crypto.randomUUID(), + role: "assistant", + type: "response", + message: [{ type: "text", text: `You asked: "${message.message}"` }], + }, + ]; + }, + responseTemplates: [], + }); + + return ( +
+
+

Welcome Screen Example

+

This example shows the WelcomeScreen component with title, description, and logo.

+
+ + + setIsOpen(!isOpen)} isOpen={isOpen} /> + + + +
setIsOpen(false)} /> + + + + + } /> + + , + }, + { + displayText: "What can you do?", + prompt: "What can you do?", + }, + ]} + /> + + + + +
+ ); +}; + +export const WithWelcomeScreen = { + args: { + defaultOpen: true, + variant: "short", + }, + render: (args: any) => , +}; + +export const WithWelcomeScreenLongVariant = { + args: { + defaultOpen: true, + variant: "long", + }, + render: (args: any) => , +}; + +// Example with custom children in WelcomeScreen +const CustomWelcomeScreenStory = ({ + defaultOpen = true, + variant = "short", +}: { + defaultOpen?: boolean; + variant?: "short" | "long"; +}) => { + const [isOpen, setIsOpen] = useState(defaultOpen); + + const threadListManager = useThreadListManager({ + createThread: async () => ({ + threadId: crypto.randomUUID(), + title: "New Chat", + createdAt: new Date(), + isRunning: false, + }), + fetchThreadList: async () => [], + deleteThread: async () => {}, + updateThread: async (t) => t, + onSwitchToNew: () => {}, + onSelectThread: () => {}, + }); + + const threadManager = useThreadManager({ + threadId: threadListManager.selectedThreadId, + loadThread: async () => [], + onProcessMessage: async ({ message, threadManager }) => { + const newMessage = Object.assign({}, message, { id: crypto.randomUUID() }) as Message; + threadManager.appendMessages(newMessage); + await new Promise((resolve) => setTimeout(resolve, 1000)); + return [ + { + id: crypto.randomUUID(), + role: "assistant", + type: "response", + message: [{ type: "text", text: `You asked: "${message.message}"` }], + }, + ]; + }, + responseTemplates: [], + }); + + return ( +
+
+

Custom Welcome Screen Example

+

This example shows WelcomeScreen with custom children instead of props.

+
+ + + setIsOpen(!isOpen)} isOpen={isOpen} /> + + + +
setIsOpen(false)} /> + + +
+
+ +
+

+ Welcome to AI Assistant +

+

+ Your personal AI helper for all your questions +

+
+
+ + + } /> + + , + }, + { + displayText: "What can you do?", + prompt: "What can you do?", + }, + ]} + /> + + + + +
+ ); +}; + +export const WithCustomWelcomeScreen = { + args: { + defaultOpen: true, + variant: "short", + }, + render: (args: any) => , +}; diff --git a/js/packages/react-ui/src/components/BottomTray/welcomeScreen.scss b/js/packages/react-ui/src/components/BottomTray/welcomeScreen.scss new file mode 100644 index 000000000..41855d1ba --- /dev/null +++ b/js/packages/react-ui/src/components/BottomTray/welcomeScreen.scss @@ -0,0 +1,55 @@ +@use "../../cssUtils" as cssUtils; + +.crayon-welcome-screen { + display: flex; + flex-direction: column; + gap: cssUtils.$spacing-l; + height: 100%; + justify-content: center; + align-items: center; + + // Icon container with sunk background + &__icon-container { + display: flex; + align-items: center; + justify-content: center; + width: 64px; + height: 64px; + background-color: cssUtils.$bg-sunk; + border-radius: cssUtils.$rounded-xl; + overflow: hidden; + color: cssUtils.$secondary-text; + flex-shrink: 0; + } + + // Logo image when using logoUrl + &__logo-image { + width: 100%; + height: 100%; + object-fit: cover; + } + + // Content container (title + description) + &__content { + display: flex; + flex-direction: column; + align-items: center; + gap: cssUtils.$spacing-xs; + text-align: center; + } + + // Title/greeting text + &__title { + @include cssUtils.typography(heading, small); + color: cssUtils.$primary-text; + margin: 0; + } + + // Description text + &__description { + @include cssUtils.typography(body, small); + color: cssUtils.$secondary-text; + margin: 0; + max-width: 280px; + } +} From 7a52300130a4c9b09f729b3544d9946547554306 Mon Sep 17 00:00:00 2001 From: ankit-thesys Date: Mon, 12 Jan 2026 15:24:45 +0530 Subject: [PATCH 14/41] Add WelcomeScreen integration to CopilotShell * Imported WelcomeScreen component in CopilotShell styles and updated index exports. * Enhanced Shell stories to include examples demonstrating WelcomeScreen with default and custom content, showcasing its functionality within the CopilotShell component. --- .../components/CopilotShell/WelcomeScreen.tsx | 118 +++++++++++++ .../components/CopilotShell/copilotShell.scss | 1 + .../src/components/CopilotShell/index.ts | 1 + .../CopilotShell/stories/Shell.stories.tsx | 156 ++++++++++++++++++ .../CopilotShell/welcomeScreen.scss | 55 ++++++ 5 files changed, 331 insertions(+) create mode 100644 js/packages/react-ui/src/components/CopilotShell/WelcomeScreen.tsx create mode 100644 js/packages/react-ui/src/components/CopilotShell/welcomeScreen.scss diff --git a/js/packages/react-ui/src/components/CopilotShell/WelcomeScreen.tsx b/js/packages/react-ui/src/components/CopilotShell/WelcomeScreen.tsx new file mode 100644 index 000000000..ab8666f30 --- /dev/null +++ b/js/packages/react-ui/src/components/CopilotShell/WelcomeScreen.tsx @@ -0,0 +1,118 @@ +import { useThreadState } from "@crayonai/react-core"; +import clsx from "clsx"; +import { ReactNode } from "react"; + +interface WelcomeScreenBaseProps { + /** + * Additional CSS class name + */ + className?: string; +} + +interface WelcomeScreenWithContentProps extends WelcomeScreenBaseProps { + /** + * The greeting/title text to display + */ + title?: string; + /** + * Optional description text to add more context + */ + description?: string; + /** + * Logo URL to display as an image + */ + logoUrl?: string; + /** + * Custom icon to display instead of the default image icon + * If logoUrl is provided, this will be ignored + */ + icon?: ReactNode; + /** + * Children are not allowed when using props-based content + */ + children?: never; +} + +interface WelcomeScreenWithChildrenProps extends WelcomeScreenBaseProps { + /** + * Custom content to render inside the welcome screen + * When children are provided, title, description, logoUrl, and icon are ignored + */ + children: ReactNode; + title?: never; + description?: never; + logoUrl?: never; + icon?: never; +} + +export type WelcomeScreenProps = + | WelcomeScreenWithContentProps + | WelcomeScreenWithChildrenProps; + +export const WelcomeScreen = (props: WelcomeScreenProps) => { + const { className } = props; + const { messages } = useThreadState(); + + // Only show when there are no messages + // TODO: check with @abhithesys if this is correct + if (messages.length > 0) { + return null; + } + + // Check if children are provided + if ("children" in props && props.children) { + return ( +
+ {props.children} +
+ ); + } + + // Props-based content + const { title, description, logoUrl, icon } = + props as WelcomeScreenWithContentProps; + + const renderIcon = () => { + if (logoUrl) { + return ( + {title} + ); + } + + if (icon) { + return icon; + } + + return null; + }; + + return ( +
+ {(logoUrl || icon) && ( +
+ {renderIcon()} +
+ )} + {(title || description) && ( +
+ {title && ( +

+ {title} +

+ )} + {description && ( +

+ {description} +

+ )} +
+ )} +
+ ); +}; + +export default WelcomeScreen; diff --git a/js/packages/react-ui/src/components/CopilotShell/copilotShell.scss b/js/packages/react-ui/src/components/CopilotShell/copilotShell.scss index 3ff84e3b8..32987f56b 100644 --- a/js/packages/react-ui/src/components/CopilotShell/copilotShell.scss +++ b/js/packages/react-ui/src/components/CopilotShell/copilotShell.scss @@ -2,6 +2,7 @@ @use "./thread.scss"; @use "./header.scss"; @use "./conversationStarter.scss"; +@use "./welcomeScreen.scss"; .crayon-copilot-shell-container { display: flex; diff --git a/js/packages/react-ui/src/components/CopilotShell/index.ts b/js/packages/react-ui/src/components/CopilotShell/index.ts index 2c05c1fe6..9b77f049c 100644 --- a/js/packages/react-ui/src/components/CopilotShell/index.ts +++ b/js/packages/react-ui/src/components/CopilotShell/index.ts @@ -2,3 +2,4 @@ export * from "./Container"; export * from "./ConversationStarter"; export * from "./Header"; export * from "./Thread"; +export * from "./WelcomeScreen"; \ No newline at end of file diff --git a/js/packages/react-ui/src/components/CopilotShell/stories/Shell.stories.tsx b/js/packages/react-ui/src/components/CopilotShell/stories/Shell.stories.tsx index dd435cbd8..ba38cb2ea 100644 --- a/js/packages/react-ui/src/components/CopilotShell/stories/Shell.stories.tsx +++ b/js/packages/react-ui/src/components/CopilotShell/stories/Shell.stories.tsx @@ -14,6 +14,7 @@ import { Messages, ScrollArea, ThreadContainer, + WelcomeScreen, } from "../../CopilotShell"; // @ts-ignore import styles from "./style.module.scss"; @@ -208,3 +209,158 @@ export const LongVariant = { ); }, }; + +// Example with WelcomeScreen +export const WithWelcomeScreen = { + args: { + variant: "short", + }, + render: ({ variant }: { variant: "short" | "long" }) => { + const threadListManager = useThreadListManager({ + createThread: async () => ({ + threadId: crypto.randomUUID(), + title: "New Chat", + createdAt: new Date(), + isRunning: false, + }), + fetchThreadList: async () => [], + deleteThread: async () => {}, + updateThread: async (t) => t, + onSwitchToNew: () => {}, + onSelectThread: () => {}, + }); + + const threadManager = useThreadManager({ + threadId: threadListManager.selectedThreadId, + loadThread: async () => [], // Start with empty thread to show welcome screen + onProcessMessage: async ({ message, threadManager }) => { + const newMessage = Object.assign({}, message, { + id: crypto.randomUUID(), + }) as Message; + threadManager.appendMessages(newMessage); + await new Promise((resolve) => setTimeout(resolve, 1000)); + return [ + { + id: crypto.randomUUID(), + role: "assistant", + type: "response", + message: [{ type: "text", text: `You asked: "${message.message}"` }], + }, + ]; + }, + responseTemplates: [], + }); + + return ( +
+
+ + + +
+ + + + + } /> + + + + + + +
+ ); + }, +}; + +// Example with custom children in WelcomeScreen +export const WithCustomWelcomeScreen = { + args: { + variant: "short", + }, + render: ({ variant }: { variant: "short" | "long" }) => { + const threadListManager = useThreadListManager({ + createThread: async () => ({ + threadId: crypto.randomUUID(), + title: "New Chat", + createdAt: new Date(), + isRunning: false, + }), + fetchThreadList: async () => [], + deleteThread: async () => {}, + updateThread: async (t) => t, + onSwitchToNew: () => {}, + onSelectThread: () => {}, + }); + + const threadManager = useThreadManager({ + threadId: threadListManager.selectedThreadId, + loadThread: async () => [], + onProcessMessage: async ({ message, threadManager }) => { + const newMessage = Object.assign({}, message, { + id: crypto.randomUUID(), + }) as Message; + threadManager.appendMessages(newMessage); + await new Promise((resolve) => setTimeout(resolve, 1000)); + return [ + { + id: crypto.randomUUID(), + role: "assistant", + type: "response", + message: [{ type: "text", text: `You asked: "${message.message}"` }], + }, + ]; + }, + responseTemplates: [], + }); + + return ( +
+
+ + + +
+ + +
+
+ +
+

+ Welcome to AI Assistant +

+

+ Your personal AI helper for all your questions +

+
+
+ + + } /> + + + + + + +
+ ); + }, +}; diff --git a/js/packages/react-ui/src/components/CopilotShell/welcomeScreen.scss b/js/packages/react-ui/src/components/CopilotShell/welcomeScreen.scss new file mode 100644 index 000000000..1494847bb --- /dev/null +++ b/js/packages/react-ui/src/components/CopilotShell/welcomeScreen.scss @@ -0,0 +1,55 @@ +@use "../../cssUtils" as cssUtils; + +.crayon-copilot-shell-welcome-screen { + display: flex; + flex-direction: column; + gap: cssUtils.$spacing-l; + height: 100%; + justify-content: center; + align-items: center; + + // Icon container with sunk background + &__icon-container { + display: flex; + align-items: center; + justify-content: center; + width: 64px; + height: 64px; + background-color: cssUtils.$bg-sunk; + border-radius: cssUtils.$rounded-xl; + overflow: hidden; + color: cssUtils.$secondary-text; + flex-shrink: 0; + } + + // Logo image when using logoUrl + &__logo-image { + width: 100%; + height: 100%; + object-fit: cover; + } + + // Content container (title + description) + &__content { + display: flex; + flex-direction: column; + align-items: center; + gap: cssUtils.$spacing-xs; + text-align: center; + } + + // Title/greeting text + &__title { + @include cssUtils.typography(heading, small); + color: cssUtils.$primary-text; + margin: 0; + } + + // Description text + &__description { + @include cssUtils.typography(body, small); + color: cssUtils.$secondary-text; + margin: 0; + max-width: 280px; + } +} From 4ab5426b6b85c331b83193a7048ec8e9e71f74d9 Mon Sep 17 00:00:00 2001 From: ankit-thesys Date: Mon, 12 Jan 2026 16:03:50 +0530 Subject: [PATCH 15/41] Refactor WelcomeScreen component and enhance integration in BottomTray and CopilotShell * Simplified WelcomeScreen component by removing unnecessary message checks and streamlining props handling. * Updated BottomTray and CopilotShell stories to conditionally render WelcomeScreen based on message presence, showcasing its usage with both props and custom children. * Improved documentation for WelcomeScreen in story files, detailing its props and usage scenarios. --- .../components/BottomTray/WelcomeScreen.tsx | 35 ++------ .../BottomTray/stories/BottomTray.mdx | 70 ++++++++++++++- .../BottomTray/stories/BottomTray.stories.tsx | 65 ++++++++------ .../components/CopilotShell/WelcomeScreen.tsx | 33 ++----- .../components/CopilotShell/stories/Shell.mdx | 89 +++++++++++++++++-- .../CopilotShell/stories/Shell.stories.tsx | 64 +++++++------ 6 files changed, 236 insertions(+), 120 deletions(-) diff --git a/js/packages/react-ui/src/components/BottomTray/WelcomeScreen.tsx b/js/packages/react-ui/src/components/BottomTray/WelcomeScreen.tsx index 0ad2fa9a8..b284e8fa5 100644 --- a/js/packages/react-ui/src/components/BottomTray/WelcomeScreen.tsx +++ b/js/packages/react-ui/src/components/BottomTray/WelcomeScreen.tsx @@ -45,42 +45,23 @@ interface WelcomeScreenWithChildrenProps extends WelcomeScreenBaseProps { icon?: never; } -export type WelcomeScreenProps = - | WelcomeScreenWithContentProps - | WelcomeScreenWithChildrenProps; +export type WelcomeScreenProps = WelcomeScreenWithContentProps | WelcomeScreenWithChildrenProps; export const WelcomeScreen = (props: WelcomeScreenProps) => { const { className } = props; const { messages } = useThreadState(); - // Only show when there are no messages - // TODO: check with @abhithesys if this is correct - if (messages.length > 0) { - return null; - } - // Check if children are provided if ("children" in props && props.children) { - return ( -
- {props.children} -
- ); + return
{props.children}
; } // Props-based content - const { title, description, logoUrl, icon } = - props as WelcomeScreenWithContentProps; + const { title, description, logoUrl, icon } = props as WelcomeScreenWithContentProps; const renderIcon = () => { if (logoUrl) { - return ( - {title} - ); + return {title}; } if (icon) { @@ -93,16 +74,12 @@ export const WelcomeScreen = (props: WelcomeScreenProps) => { return (
{(logoUrl || icon) && ( -
- {renderIcon()} -
+
{renderIcon()}
)} {(title || description) && (
{title &&

{title}

} - {description && ( -

{description}

- )} + {description &&

{description}

}
)}
diff --git a/js/packages/react-ui/src/components/BottomTray/stories/BottomTray.mdx b/js/packages/react-ui/src/components/BottomTray/stories/BottomTray.mdx index 4253de409..48034080c 100644 --- a/js/packages/react-ui/src/components/BottomTray/stories/BottomTray.mdx +++ b/js/packages/react-ui/src/components/BottomTray/stories/BottomTray.mdx @@ -15,6 +15,7 @@ ChatProvider └── Container (the tray panel) └── ThreadContainer ├── Header (logo, agent name, actions) + ├── WelcomeScreen (optional, shown when empty) ├── ScrollArea (scrollable message area) │ └── Messages ├── ConversationStarter (optional, shown when empty) @@ -36,6 +37,7 @@ import { ScrollArea, ThreadContainer, Trigger, + WelcomeScreen, } from "@crayonai/react-ui"; function MyApp() { @@ -85,6 +87,8 @@ function MyApp() { responseTemplates: [], }); + const hasMessages = threadManager.messages.length > 0; + return ( {/* Trigger - floating button */} @@ -94,6 +98,16 @@ function MyApp() {
setIsOpen(false)} /> + + {/* WelcomeScreen - shown when no messages */} + {!hasMessages && ( + + )} + } /> @@ -189,13 +203,65 @@ Clickable prompts shown when thread is empty. Automatically hides when messages ```tsx }, + { displayText: "What can you do?", prompt: "What can you do?" }, // Default lightbulb icon + { displayText: "No icon example", prompt: "No icon", icon: "" }, // No icon ]} /> ``` +**Props:** + +- `variant` - `"short"` (pill-style buttons) or `"long"` (list items with separators) +- `starters` - Array of objects with: + - `displayText` - Text shown on the button + - `prompt` - Message sent when clicked + - `icon` - Optional: `ReactNode` for custom icon, `undefined` for default lightbulb, `""` for no icon + +### WelcomeScreen + +A centered welcome message shown when the thread is empty. Supports either props-based content or custom children. + +**Props-based usage:** + +```tsx +{!hasMessages && ( + +)} +``` + +**Custom children usage:** + +```tsx +{!hasMessages && ( + +
+ +

Welcome!

+

Custom welcome content

+
+
+)} +``` + +**Props:** + +- `title` - Greeting/title text (optional) +- `description` - Description text (optional) +- `logoUrl` - URL for logo image (optional) +- `icon` - Custom icon as ReactNode (optional, ignored if logoUrl is provided) +- `children` - Custom content (mutually exclusive with title/description/logoUrl/icon) +- `className` - Additional CSS class (optional) + +**Note:** You must conditionally render WelcomeScreen based on `hasMessages` state. + ### Composer Text input with submit button. Handles enter key and running state. diff --git a/js/packages/react-ui/src/components/BottomTray/stories/BottomTray.stories.tsx b/js/packages/react-ui/src/components/BottomTray/stories/BottomTray.stories.tsx index 390872730..aee3b9690 100644 --- a/js/packages/react-ui/src/components/BottomTray/stories/BottomTray.stories.tsx +++ b/js/packages/react-ui/src/components/BottomTray/stories/BottomTray.stories.tsx @@ -384,6 +384,9 @@ const WelcomeScreenStory = ({ responseTemplates: [], }); + // this can be used instead of thread state + const hasMessages = threadManager.messages.length > 0; + return (
@@ -398,11 +401,13 @@ const WelcomeScreenStory = ({
setIsOpen(false)} /> - + {hasMessages ? null : ( + + )} } /> @@ -488,6 +493,8 @@ const CustomWelcomeScreenStory = ({ responseTemplates: [], }); + const hasMessages = threadManager.messages.length > 0; + return (
@@ -502,30 +509,32 @@ const CustomWelcomeScreenStory = ({
setIsOpen(false)} /> - -
-
- + {hasMessages ? null : ( + +
+
+ +
+

+ Welcome to AI Assistant +

+

+ Your personal AI helper for all your questions +

-

- Welcome to AI Assistant -

-

- Your personal AI helper for all your questions -

-
- + + )} } /> diff --git a/js/packages/react-ui/src/components/CopilotShell/WelcomeScreen.tsx b/js/packages/react-ui/src/components/CopilotShell/WelcomeScreen.tsx index ab8666f30..2d4b7860d 100644 --- a/js/packages/react-ui/src/components/CopilotShell/WelcomeScreen.tsx +++ b/js/packages/react-ui/src/components/CopilotShell/WelcomeScreen.tsx @@ -1,4 +1,3 @@ -import { useThreadState } from "@crayonai/react-core"; import clsx from "clsx"; import { ReactNode } from "react"; @@ -45,32 +44,20 @@ interface WelcomeScreenWithChildrenProps extends WelcomeScreenBaseProps { icon?: never; } -export type WelcomeScreenProps = - | WelcomeScreenWithContentProps - | WelcomeScreenWithChildrenProps; +export type WelcomeScreenProps = WelcomeScreenWithContentProps | WelcomeScreenWithChildrenProps; export const WelcomeScreen = (props: WelcomeScreenProps) => { const { className } = props; - const { messages } = useThreadState(); - - // Only show when there are no messages - // TODO: check with @abhithesys if this is correct - if (messages.length > 0) { - return null; - } // Check if children are provided if ("children" in props && props.children) { return ( -
- {props.children} -
+
{props.children}
); } // Props-based content - const { title, description, logoUrl, icon } = - props as WelcomeScreenWithContentProps; + const { title, description, logoUrl, icon } = props as WelcomeScreenWithContentProps; const renderIcon = () => { if (logoUrl) { @@ -93,21 +80,13 @@ export const WelcomeScreen = (props: WelcomeScreenProps) => { return (
{(logoUrl || icon) && ( -
- {renderIcon()} -
+
{renderIcon()}
)} {(title || description) && (
- {title && ( -

- {title} -

- )} + {title &&

{title}

} {description && ( -

- {description} -

+

{description}

)}
)} diff --git a/js/packages/react-ui/src/components/CopilotShell/stories/Shell.mdx b/js/packages/react-ui/src/components/CopilotShell/stories/Shell.mdx index 7ef4b6399..068ec0bc4 100644 --- a/js/packages/react-ui/src/components/CopilotShell/stories/Shell.mdx +++ b/js/packages/react-ui/src/components/CopilotShell/stories/Shell.mdx @@ -14,6 +14,7 @@ ChatProvider └── Container (sidebar panel) └── ThreadContainer ├── Header (logo, agent name) + ├── WelcomeScreen (optional, shown when empty) ├── ScrollArea (scrollable message area) │ └── Messages ├── ConversationStarter (optional, shown when empty) @@ -34,6 +35,7 @@ import { Messages, ScrollArea, ThreadContainer, + WelcomeScreen, } from "@crayonai/react-ui/CopilotShell"; function MyCopilot() { @@ -74,11 +76,23 @@ function MyCopilot() { responseTemplates: [], }); + const hasMessages = threadManager.messages.length > 0; + return (
+ + {/* WelcomeScreen - shown when no messages */} + {!hasMessages && ( + + )} + } /> @@ -148,14 +162,15 @@ Scrollable container for messages with smart scroll behavior. ### ConversationStarter -Full-width clickable buttons shown when the thread is empty. Automatically submits the prompt as a message when clicked. +Clickable prompts shown when the thread is empty. Automatically submits the prompt as a message when clicked. ```tsx }, + { displayText: "What can you do?", prompt: "What can you do?" }, // Default lightbulb icon + { displayText: "No icon example", prompt: "No icon", icon: "" }, // No icon ]} className="custom-class" // Optional custom styling /> @@ -163,7 +178,11 @@ Full-width clickable buttons shown when the thread is empty. Automatically submi **Props:** -- `starters` - Array of objects with `displayText` (shown on button) and `prompt` (sent as message) +- `variant` - `"short"` (pill-style buttons) or `"long"` (list items with separators) +- `starters` - Array of objects with: + - `displayText` - Text shown on the button + - `prompt` - Message sent when clicked + - `icon` - Optional: `ReactNode` for custom icon, `undefined` for default lightbulb, `""` for no icon - `className` - Optional additional CSS class **Behavior:** @@ -172,6 +191,48 @@ Full-width clickable buttons shown when the thread is empty. Automatically submi - Clicking a button submits the `prompt` as a user message - Buttons are disabled when a response is being generated +### WelcomeScreen + +A centered welcome message shown when the thread is empty. Supports either props-based content or custom children. + +**Props-based usage:** + +```tsx +{!hasMessages && ( + +)} +``` + +**Custom children usage:** + +```tsx +{!hasMessages && ( + +
+ +

Welcome!

+

Custom welcome content

+
+
+)} +``` + +**Props:** + +- `title` - Greeting/title text (optional) +- `description` - Description text (optional) +- `logoUrl` - URL for logo image (optional) +- `icon` - Custom icon as ReactNode (optional, ignored if logoUrl is provided) +- `children` - Custom content (mutually exclusive with title/description/logoUrl/icon) +- `className` - Additional CSS class (optional) + +**Note:** You must conditionally render WelcomeScreen based on `hasMessages` state. + ### Composer Text input for sending messages. @@ -186,6 +247,8 @@ CopilotShell is designed to be placed as a sidebar: ```tsx function AppLayout() { + const hasMessages = threadManager.messages.length > 0; + return (
{/* Main content area */} @@ -197,6 +260,13 @@ function AppLayout() {
+ {!hasMessages && ( + + )} } /> @@ -232,4 +302,11 @@ All CopilotShell components use the `crayon-copilot-shell-` prefix: - `.crayon-copilot-shell-thread-message-assistant` - `.crayon-copilot-shell-thread-composer` - `.crayon-copilot-shell-conversation-starter` -- `.crayon-copilot-shell-conversation-starter-item` +- `.crayon-copilot-shell-conversation-starter-item-short` +- `.crayon-copilot-shell-conversation-starter-item-long` +- `.crayon-copilot-shell-welcome-screen` +- `.crayon-copilot-shell-welcome-screen__icon-container` +- `.crayon-copilot-shell-welcome-screen__logo-image` +- `.crayon-copilot-shell-welcome-screen__content` +- `.crayon-copilot-shell-welcome-screen__title` +- `.crayon-copilot-shell-welcome-screen__description` diff --git a/js/packages/react-ui/src/components/CopilotShell/stories/Shell.stories.tsx b/js/packages/react-ui/src/components/CopilotShell/stories/Shell.stories.tsx index ba38cb2ea..e6d53eeb5 100644 --- a/js/packages/react-ui/src/components/CopilotShell/stories/Shell.stories.tsx +++ b/js/packages/react-ui/src/components/CopilotShell/stories/Shell.stories.tsx @@ -251,6 +251,8 @@ export const WithWelcomeScreen = { responseTemplates: [], }); + const hasMessages = threadManager.messages.length > 0; + return (
@@ -259,11 +261,13 @@ export const WithWelcomeScreen = {
- + {hasMessages ? null : ( + + )} } /> @@ -319,6 +323,8 @@ export const WithCustomWelcomeScreen = { responseTemplates: [], }); + const hasMessages = threadManager.messages.length > 0; + return (
@@ -327,30 +333,32 @@ export const WithCustomWelcomeScreen = {
- -
-
- + {hasMessages ? null : ( + +
+
+ +
+

+ Welcome to AI Assistant +

+

+ Your personal AI helper for all your questions +

-

- Welcome to AI Assistant -

-

- Your personal AI helper for all your questions -

-
- + + )} } /> From c8bf9a3e6e126a07a8367e405f7cf095fd88a34b Mon Sep 17 00:00:00 2001 From: ankit-thesys Date: Mon, 12 Jan 2026 16:30:43 +0530 Subject: [PATCH 16/41] Enhance ConversationStarter component with improved variant support and styling in the full screen chat --- .../components/Shell/ConversationStarter.tsx | 117 +++++++++++++-- .../components/Shell/conversationStarter.scss | 132 +++++++++++++++-- .../Shell/stories/Shell.stories.tsx | 134 +++++++++++++++--- 3 files changed, 343 insertions(+), 40 deletions(-) diff --git a/js/packages/react-ui/src/components/Shell/ConversationStarter.tsx b/js/packages/react-ui/src/components/Shell/ConversationStarter.tsx index 905fb0344..a5699d2b6 100644 --- a/js/packages/react-ui/src/components/Shell/ConversationStarter.tsx +++ b/js/packages/react-ui/src/components/Shell/ConversationStarter.tsx @@ -1,28 +1,92 @@ import { useThreadActions, useThreadState } from "@crayonai/react-core"; import clsx from "clsx"; -import { ConversationStarterProps } from "../../types/ConversationStarter"; +import { ArrowUp, Lightbulb } from "lucide-react"; +import { Fragment, ReactNode } from "react"; +import { + ConversationStarterIcon, + ConversationStarterProps, +} from "../../types/ConversationStarter"; +import { Separator } from "../Separator"; + +export type ConversationStarterVariant = "short" | "long"; interface ConversationStarterItemProps extends ConversationStarterProps { onClick: (prompt: string) => void; disabled?: boolean; + variant: ConversationStarterVariant; } +/** + * Renders the appropriate icon based on the icon prop value + * - undefined: Show default lightbulb icon + * - "": Show no icon + * - ReactNode: Show the provided icon + */ +const renderIcon = (icon: ConversationStarterIcon | undefined): ReactNode => { + if (icon === "") { + return null; + } + if (icon === undefined) { + return ; + } + return icon; +}; + const ConversationStarterItem = ({ displayText, prompt, onClick, disabled = false, + variant, + icon, }: ConversationStarterItemProps) => { + const renderedIcon = renderIcon(icon); + + if (variant === "short") { + return ( + + ); + } + + // Long variant (detailed list style) return ( ); }; @@ -30,9 +94,19 @@ const ConversationStarterItem = ({ export interface ConversationStarterContainerProps { starters: ConversationStarterProps[]; className?: string; + /** + * Variant of the conversation starter + * - "short": Pill-style buttons that wrap (default) + * - "long": Vertical list items with icons and hover arrow + */ + variant?: ConversationStarterVariant; } -export const ConversationStarter = ({ starters, className }: ConversationStarterContainerProps) => { +export const ConversationStarter = ({ + starters, + className, + variant = "short", +}: ConversationStarterContainerProps) => { const { processMessage } = useThreadActions(); const { isRunning, messages } = useThreadState(); @@ -55,15 +129,30 @@ export const ConversationStarter = ({ starters, className }: ConversationStarter } return ( -
- {starters.map((item) => ( - +
+ {starters.map((item, index) => ( + + + {/* Add separator between items in long variant */} + {variant === "long" && index < starters.length - 1 && ( +
+ +
+ )} +
))}
); diff --git a/js/packages/react-ui/src/components/Shell/conversationStarter.scss b/js/packages/react-ui/src/components/Shell/conversationStarter.scss index 82b4cf494..8f31499e9 100644 --- a/js/packages/react-ui/src/components/Shell/conversationStarter.scss +++ b/js/packages/react-ui/src/components/Shell/conversationStarter.scss @@ -2,34 +2,51 @@ $center-align-spacing: calc(32px + cssUtils.$spacing-s); +// Container styles .crayon-shell-conversation-starter { - display: grid; - grid-template-columns: 1fr; - gap: cssUtils.$spacing-s; width: 100%; max-width: 880px; margin: 0 auto; padding: 0 $center-align-spacing cssUtils.$spacing-m; - // 2-column layout for larger screens (≥640px) - @media (min-width: 640px) { - grid-template-columns: repeat(2, 1fr); + // Short variant - horizontal wrapping buttons + &--short { + display: flex; + flex-wrap: wrap; + gap: cssUtils.$spacing-s; } + // Long variant - vertical stacked list with separators + &--long { + display: flex; + flex-direction: column; + gap: cssUtils.$spacing-2xs; + } + + // Separator wrapper for long variant + &__separator { + padding: cssUtils.$spacing-3xs cssUtils.$spacing-xs; + } + + // Mobile adjustments .crayon-shell-container--mobile & { padding: 0 cssUtils.$spacing-l cssUtils.$spacing-m; - grid-template-columns: 1fr; // Force single column on mobile } + // Artifact active adjustments .crayon-shell-thread-container--artifact-active & { padding-left: 0; padding-right: cssUtils.$spacing-m; } } -.crayon-shell-conversation-starter-item { - width: 100%; - padding: cssUtils.$spacing-m cssUtils.$spacing-l; +// Short variant item (pill-style buttons that wrap) +.crayon-shell-conversation-starter-item-short { + display: flex; + align-items: center; + gap: cssUtils.$spacing-xs; + width: fit-content; + padding: cssUtils.$spacing-s cssUtils.$spacing-m; background-color: cssUtils.$bg-container; border: 1px solid cssUtils.$stroke-default; border-radius: cssUtils.$rounded-m; @@ -39,6 +56,21 @@ $center-align-spacing: calc(32px + cssUtils.$spacing-s); color: cssUtils.$primary-text; text-align: left; + // Icon container + &__icon { + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + color: cssUtils.$primary-text; + } + + // Text + &__text { + flex: 1; + white-space: nowrap; + } + &:not(:disabled):hover { background-color: cssUtils.$interactive-hover; border-color: cssUtils.$stroke-emphasis; @@ -52,4 +84,84 @@ $center-align-spacing: calc(32px + cssUtils.$spacing-s); opacity: 0.5; cursor: not-allowed; } + + @media (max-width: 480px) { + padding: cssUtils.$spacing-xs cssUtils.$spacing-s; + } +} + +// Long variant item (list-style with icon and arrow) +.crayon-shell-conversation-starter-item-long { + display: flex; + align-items: center; + gap: cssUtils.$spacing-0; + width: 100%; + padding: cssUtils.$spacing-xs cssUtils.$spacing-xs; + background-color: transparent; + border: none; + border-radius: cssUtils.$rounded-m; + cursor: pointer; + transition: background-color 0.15s ease; + @include cssUtils.typography(body, default); + color: cssUtils.$primary-text; + text-align: left; + overflow: hidden; + + // Content wrapper (icon + text) + &__content { + display: flex; + align-items: flex-start; + gap: cssUtils.$spacing-xs; + flex: 1; + min-width: 0; + } + + // Icon container - aligned to top with padding to align with first line of text + &__icon { + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + padding-top: cssUtils.$spacing-2xs; + color: cssUtils.$primary-text; + } + + // Text - allows wrapping for long content + &__text { + flex: 1; + min-width: 0; + line-height: 1.5; + // Text wraps naturally, no truncation + } + + // Arrow icon (shown on hover) + &__arrow { + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + opacity: 0; + transition: opacity 0.15s ease; + color: cssUtils.$primary-text; + } + + // Hover state + &:not(:disabled):hover { + background-color: cssUtils.$bg-sunk; + + .crayon-shell-conversation-starter-item-long__arrow { + opacity: 1; + } + } + + // Active/pressed state + &:not(:disabled):active { + background-color: cssUtils.$interactive-pressed; + } + + // Disabled state + &--disabled { + opacity: 0.5; + cursor: not-allowed; + } } diff --git a/js/packages/react-ui/src/components/Shell/stories/Shell.stories.tsx b/js/packages/react-ui/src/components/Shell/stories/Shell.stories.tsx index a7858106b..e66cb83a8 100644 --- a/js/packages/react-ui/src/components/Shell/stories/Shell.stories.tsx +++ b/js/packages/react-ui/src/components/Shell/stories/Shell.stories.tsx @@ -4,6 +4,7 @@ import { useThreadListManager, useThreadManager, } from "@crayonai/react-core"; +import { MessageSquare, Sparkles, Zap } from "lucide-react"; import { Container } from "../Container"; import { ConversationStarter } from "../ConversationStarter"; import { MobileHeader } from "../MobileHeader"; @@ -17,21 +18,60 @@ export default { title: "Components/Shell", tags: ["dev"], argTypes: { - defaultOpen: { - control: "boolean", - description: "Whether to start with messages", + variant: { + control: "select", + options: ["short", "long"], + description: "Conversation starter variant", }, }, }; const SAMPLE_STARTERS = [ - { displayText: "Help me get started", prompt: "Help me get started" }, - { displayText: "What can you do?", prompt: "What can you do?" }, - { displayText: "Tell me about your features", prompt: "Tell me about your features" }, + { + displayText: "Help me get started", + prompt: "Help me get started", + icon: , + }, + { + displayText: "What can you do?", + prompt: "What can you do?", + // icon undefined = shows default lightbulb + }, + { + displayText: "Tell me about your features", + prompt: "Tell me about your features", + icon: , + }, + { + displayText: "Show me some examples (no icon)", + prompt: "Show me some examples", + icon: "", // Empty string = no icon + }, +]; + +const LONG_STARTERS = [ + { + displayText: "Help me get started with this application and guide me through the features", + prompt: "Help me get started with this application", + icon: , + }, + { + displayText: "What can you do? I'd like to know all your capabilities and how you can help me", + prompt: "What can you do?", + icon: , + }, + { + displayText: "Tell me about your advanced features and how I can use them effectively", + prompt: "Tell me about your features", + // Default lightbulb icon + }, ]; export const Default = { - render: () => { + args: { + variant: "short", + }, + render: ({ variant }: { variant: "short" | "long" }) => { const threadListManager = useThreadListManager({ createThread: async () => { return { @@ -90,9 +130,7 @@ export const Default = { ]; }, onProcessMessage: async ({ message, threadManager }) => { - const newMessage = Object.assign({}, message, { - id: crypto.randomUUID(), - }) as Message; + const newMessage = { ...message, id: crypto.randomUUID() } as Message; threadManager.appendMessages(newMessage); await new Promise((resolve) => setTimeout(resolve, 1000)); return [ @@ -123,7 +161,7 @@ export const Default = { } /> - + @@ -133,7 +171,73 @@ export const Default = { }; export const WithConversationStarter = { - render: () => { + args: { + variant: "short", + }, + render: ({ variant }: { variant: "short" | "long" }) => { + const threadListManager = useThreadListManager({ + createThread: async () => ({ + threadId: crypto.randomUUID(), + title: "New Chat", + createdAt: new Date(), + isRunning: false, + }), + fetchThreadList: async () => [], + deleteThread: async () => {}, + updateThread: async (t) => t, + onSwitchToNew: () => {}, + onSelectThread: () => {}, + }); + + const threadManager = useThreadManager({ + threadId: threadListManager.selectedThreadId, + loadThread: async () => [], // Start with empty thread to show starters + onProcessMessage: async ({ message, threadManager }) => { + const newMessage = { ...message, id: crypto.randomUUID() } as Message; + threadManager.appendMessages(newMessage); + await new Promise((resolve) => setTimeout(resolve, 1000)); + return [ + { + id: crypto.randomUUID(), + role: "assistant", + type: "response", + message: [{ type: "text", text: `You asked: "${message.message}"` }], + }, + ]; + }, + responseTemplates: [], + }); + + return ( + + + + + + + + + + + + + + } /> + + + + + + + ); + }, +}; + +export const LongVariant = { + args: { + variant: "long", + }, + render: ({ variant }: { variant: "short" | "long" }) => { const threadListManager = useThreadListManager({ createThread: async () => ({ threadId: crypto.randomUUID(), @@ -152,9 +256,7 @@ export const WithConversationStarter = { threadId: threadListManager.selectedThreadId, loadThread: async () => [], // Start with empty thread to show starters onProcessMessage: async ({ message, threadManager }) => { - const newMessage = Object.assign({}, message, { - id: crypto.randomUUID(), - }) as Message; + const newMessage = { ...message, id: crypto.randomUUID() } as Message; threadManager.appendMessages(newMessage); await new Promise((resolve) => setTimeout(resolve, 1000)); return [ @@ -185,7 +287,7 @@ export const WithConversationStarter = { } /> - + From d24485f385a2947a3401529e60c36adf425c0161 Mon Sep 17 00:00:00 2001 From: ankit-thesys Date: Tue, 13 Jan 2026 12:35:22 +0530 Subject: [PATCH 17/41] Refactor ConversationStarter component imports and enhance WelcomeScreen integration * Consolidated imports for ConversationStarterProps and ConversationStarterIcon across multiple components. * Updated WelcomeScreen integration in Shell and CopilotShell, ensuring consistent exports and styling. * Improved story examples for WelcomeScreen, showcasing both props-based and custom children usage in various contexts. --- .../BottomTray/ConversationStarter.tsx | 5 +- .../src/components/BottomTray/index.ts | 2 +- .../BottomTray/stories/BottomTray.mdx | 44 +++-- .../CopilotShell/ConversationStarter.tsx | 5 +- .../src/components/CopilotShell/index.ts | 2 +- .../components/CopilotShell/stories/Shell.mdx | 44 +++-- .../components/Shell/ConversationStarter.tsx | 23 +-- .../react-ui/src/components/Shell/Thread.tsx | 62 +------ .../src/components/Shell/WelcomeScreen.tsx | 87 +++++++++ .../components/Shell/components/Composer.tsx | 71 ++++++++ .../components/DesktopWelcomeComposer.tsx | 84 +++++++++ .../components/Shell/components/composer.scss | 48 +++++ .../components/desktopWelcomeComposer.scss | 96 ++++++++++ .../src/components/Shell/components/index.ts | 2 + .../react-ui/src/components/Shell/index.ts | 2 + .../react-ui/src/components/Shell/shell.scss | 3 + .../Shell/stories/Shell.stories.tsx | 170 ++++++++++++++++++ .../react-ui/src/components/Shell/thread.scss | 60 ++----- .../src/components/Shell/welcomeScreen.scss | 73 ++++++++ js/packages/react-ui/src/index.ts | 4 +- 20 files changed, 714 insertions(+), 173 deletions(-) create mode 100644 js/packages/react-ui/src/components/Shell/WelcomeScreen.tsx create mode 100644 js/packages/react-ui/src/components/Shell/components/Composer.tsx create mode 100644 js/packages/react-ui/src/components/Shell/components/DesktopWelcomeComposer.tsx create mode 100644 js/packages/react-ui/src/components/Shell/components/composer.scss create mode 100644 js/packages/react-ui/src/components/Shell/components/desktopWelcomeComposer.scss create mode 100644 js/packages/react-ui/src/components/Shell/components/index.ts create mode 100644 js/packages/react-ui/src/components/Shell/welcomeScreen.scss diff --git a/js/packages/react-ui/src/components/BottomTray/ConversationStarter.tsx b/js/packages/react-ui/src/components/BottomTray/ConversationStarter.tsx index c046366df..6561367a7 100644 --- a/js/packages/react-ui/src/components/BottomTray/ConversationStarter.tsx +++ b/js/packages/react-ui/src/components/BottomTray/ConversationStarter.tsx @@ -2,10 +2,7 @@ import { useThreadActions, useThreadState } from "@crayonai/react-core"; import clsx from "clsx"; import { ArrowUp, Lightbulb } from "lucide-react"; import { Fragment, ReactNode } from "react"; -import { - ConversationStarterIcon, - ConversationStarterProps, -} from "../../types/ConversationStarter"; +import { ConversationStarterIcon, ConversationStarterProps } from "../../types/ConversationStarter"; import { Separator } from "../Separator"; export type ConversationStarterVariant = "short" | "long"; diff --git a/js/packages/react-ui/src/components/BottomTray/index.ts b/js/packages/react-ui/src/components/BottomTray/index.ts index 02ccb680d..4306ca7ac 100644 --- a/js/packages/react-ui/src/components/BottomTray/index.ts +++ b/js/packages/react-ui/src/components/BottomTray/index.ts @@ -3,4 +3,4 @@ export * from "./ConversationStarter"; export * from "./Header"; export * from "./Thread"; export * from "./Trigger"; -export * from "./WelcomeScreen"; \ No newline at end of file +export * from "./WelcomeScreen"; diff --git a/js/packages/react-ui/src/components/BottomTray/stories/BottomTray.mdx b/js/packages/react-ui/src/components/BottomTray/stories/BottomTray.mdx index 48034080c..0c6520c45 100644 --- a/js/packages/react-ui/src/components/BottomTray/stories/BottomTray.mdx +++ b/js/packages/react-ui/src/components/BottomTray/stories/BottomTray.mdx @@ -205,7 +205,11 @@ Clickable prompts shown when thread is empty. Automatically hides when messages }, + { + displayText: "Help me get started", + prompt: "Help me get started", + icon: , + }, { displayText: "What can you do?", prompt: "What can you do?" }, // Default lightbulb icon { displayText: "No icon example", prompt: "No icon", icon: "" }, // No icon ]} @@ -227,28 +231,32 @@ A centered welcome message shown when the thread is empty. Supports either props **Props-based usage:** ```tsx -{!hasMessages && ( - -)} +{ + !hasMessages && ( + + ); +} ``` **Custom children usage:** ```tsx -{!hasMessages && ( - -
- -

Welcome!

-

Custom welcome content

-
-
-)} +{ + !hasMessages && ( + +
+ +

Welcome!

+

Custom welcome content

+
+
+ ); +} ``` **Props:** diff --git a/js/packages/react-ui/src/components/CopilotShell/ConversationStarter.tsx b/js/packages/react-ui/src/components/CopilotShell/ConversationStarter.tsx index 36bf8306e..e89086a99 100644 --- a/js/packages/react-ui/src/components/CopilotShell/ConversationStarter.tsx +++ b/js/packages/react-ui/src/components/CopilotShell/ConversationStarter.tsx @@ -2,10 +2,7 @@ import { useThreadActions, useThreadState } from "@crayonai/react-core"; import clsx from "clsx"; import { ArrowUp, Lightbulb } from "lucide-react"; import { Fragment, ReactNode } from "react"; -import { - ConversationStarterIcon, - ConversationStarterProps, -} from "../../types/ConversationStarter"; +import { ConversationStarterIcon, ConversationStarterProps } from "../../types/ConversationStarter"; import { Separator } from "../Separator"; export type ConversationStarterVariant = "short" | "long"; diff --git a/js/packages/react-ui/src/components/CopilotShell/index.ts b/js/packages/react-ui/src/components/CopilotShell/index.ts index 9b77f049c..7c3629b7c 100644 --- a/js/packages/react-ui/src/components/CopilotShell/index.ts +++ b/js/packages/react-ui/src/components/CopilotShell/index.ts @@ -2,4 +2,4 @@ export * from "./Container"; export * from "./ConversationStarter"; export * from "./Header"; export * from "./Thread"; -export * from "./WelcomeScreen"; \ No newline at end of file +export * from "./WelcomeScreen"; diff --git a/js/packages/react-ui/src/components/CopilotShell/stories/Shell.mdx b/js/packages/react-ui/src/components/CopilotShell/stories/Shell.mdx index 068ec0bc4..330559e40 100644 --- a/js/packages/react-ui/src/components/CopilotShell/stories/Shell.mdx +++ b/js/packages/react-ui/src/components/CopilotShell/stories/Shell.mdx @@ -168,7 +168,11 @@ Clickable prompts shown when the thread is empty. Automatically submits the prom }, + { + displayText: "Help me get started", + prompt: "Help me get started", + icon: , + }, { displayText: "What can you do?", prompt: "What can you do?" }, // Default lightbulb icon { displayText: "No icon example", prompt: "No icon", icon: "" }, // No icon ]} @@ -198,28 +202,32 @@ A centered welcome message shown when the thread is empty. Supports either props **Props-based usage:** ```tsx -{!hasMessages && ( - -)} +{ + !hasMessages && ( + + ); +} ``` **Custom children usage:** ```tsx -{!hasMessages && ( - -
- -

Welcome!

-

Custom welcome content

-
-
-)} +{ + !hasMessages && ( + +
+ +

Welcome!

+

Custom welcome content

+
+
+ ); +} ``` **Props:** diff --git a/js/packages/react-ui/src/components/Shell/ConversationStarter.tsx b/js/packages/react-ui/src/components/Shell/ConversationStarter.tsx index a5699d2b6..b29390f7f 100644 --- a/js/packages/react-ui/src/components/Shell/ConversationStarter.tsx +++ b/js/packages/react-ui/src/components/Shell/ConversationStarter.tsx @@ -2,10 +2,7 @@ import { useThreadActions, useThreadState } from "@crayonai/react-core"; import clsx from "clsx"; import { ArrowUp, Lightbulb } from "lucide-react"; import { Fragment, ReactNode } from "react"; -import { - ConversationStarterIcon, - ConversationStarterProps, -} from "../../types/ConversationStarter"; +import { ConversationStarterIcon, ConversationStarterProps } from "../../types/ConversationStarter"; import { Separator } from "../Separator"; export type ConversationStarterVariant = "short" | "long"; @@ -53,13 +50,9 @@ const ConversationStarterItem = ({ disabled={disabled} > {renderedIcon && ( - - {renderedIcon} - + {renderedIcon} )} - - {displayText} - + {displayText} ); } @@ -76,13 +69,9 @@ const ConversationStarterItem = ({ >
{renderedIcon && ( - - {renderedIcon} - + {renderedIcon} )} - - {displayText} - + {displayText}
@@ -133,7 +122,7 @@ export const ConversationStarter = ({ className={clsx( "crayon-shell-conversation-starter", `crayon-shell-conversation-starter--${variant}`, - className + className, )} > {starters.map((item, index) => ( diff --git a/js/packages/react-ui/src/components/Shell/Thread.tsx b/js/packages/react-ui/src/components/Shell/Thread.tsx index 486d73f09..5805dd25a 100644 --- a/js/packages/react-ui/src/components/Shell/Thread.tsx +++ b/js/packages/react-ui/src/components/Shell/Thread.tsx @@ -1,17 +1,13 @@ import { Message, MessageProvider, - useThreadActions, useThreadManagerSelector, useThreadState, } from "@crayonai/react-core"; import clsx from "clsx"; -import { ArrowRight, Square } from "lucide-react"; -import React, { memo, useEffect, useLayoutEffect, useRef } from "react"; +import React, { memo, useEffect, useRef } from "react"; import { useLayoutContext } from "../../context/LayoutContext"; -import { useComposerState } from "../../hooks/useComposerState"; import { ScrollVariant, useScrollToBottom } from "../../hooks/useScrollToBottom"; -import { IconButton } from "../IconButton"; import { MessageLoading as MessageLoadingComponent } from "../MessageLoading"; import { ResizableSeparator } from "./ResizableSeparator"; import { useShellStore } from "./store"; @@ -282,57 +278,5 @@ export const Messages = ({ ); }; -export const Composer = ({ className }: { className?: string }) => { - const { textContent, setTextContent } = useComposerState(); - const { processMessage, onCancel } = useThreadActions(); - const { isRunning, isLoadingMessages } = useThreadState(); - const inputRef = useRef(null); - - const handleSubmit = () => { - if (!textContent.trim() || isRunning || isLoadingMessages) { - return; - } - - processMessage({ - type: "prompt", - role: "user", - message: textContent, - }); - - setTextContent(""); - }; - - useLayoutEffect(() => { - const input = inputRef.current; - if (!input) { - return; - } - - input.style.height = "0px"; - input.style.height = `${input.scrollHeight}px`; - }, [textContent]); - - return ( -
-
-