Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
a9fb37d
Add CrayonChat styles and enhance BottomTray animations
ankit-thesys Jan 7, 2026
9403fbf
Enhance BottomTray component with ConversationStarter integration
ankit-thesys Jan 7, 2026
1a3c5ad
Refactor ConversationStarter component for cleaner syntax and update …
ankit-thesys Jan 7, 2026
fc859a0
Integrate ConversationStarter component into CopilotShell and enhance…
ankit-thesys Jan 8, 2026
0fbcb00
Integrate ConversationStarter component into Shell and enhance stories
ankit-thesys Jan 8, 2026
949cf81
format fix
ankit-thesys Jan 8, 2026
bf6abee
Merge branch 'main' of https://github.com/thesysdev/crayon into shell…
ankit-thesys Jan 8, 2026
9d903d6
update shell stories and conversation starter styles
ankit-thesys Jan 8, 2026
1ca9be9
styling and conversation starter type
ankit-thesys Jan 12, 2026
85fc3ce
long variant style and story book mode, types export
ankit-thesys Jan 12, 2026
f01eb4b
Update BottomTray styles and stories for conversation starters
ankit-thesys Jan 12, 2026
00d0e35
Enhance ConversationStarter component with variant support and stylin…
ankit-thesys Jan 12, 2026
444fa57
Update BottomTray and CopilotShell styles for ConversationStarter com…
ankit-thesys Jan 12, 2026
8129e49
Add WelcomeScreen component to BottomTray and enhance stories
ankit-thesys Jan 12, 2026
7a52300
Add WelcomeScreen integration to CopilotShell
ankit-thesys Jan 12, 2026
4ab5426
Refactor WelcomeScreen component and enhance integration in BottomTra…
ankit-thesys Jan 12, 2026
c8bf9a3
Enhance ConversationStarter component with improved variant support a…
ankit-thesys Jan 12, 2026
d24485f
Refactor ConversationStarter component imports and enhance WelcomeScr…
ankit-thesys Jan 13, 2026
a9fac45
format fix
ankit-thesys Jan 13, 2026
4ee1764
build fix
ankit-thesys Jan 13, 2026
ea2489a
Refactor ConversationStarter and WelcomeScreen components for improve…
ankit-thesys Jan 13, 2026
8836815
Update typography and add center alignment for ConversationStarter co…
ankit-thesys Jan 13, 2026
6e35d0a
Enhance WelcomeScreen layout with new composer starters container
ankit-thesys Jan 13, 2026
2d8c6a7
Update spacing and border radius in conversation starter styles
ankit-thesys Jan 13, 2026
b57787e
Refactor BottomTray and CopilotShell components for improved icon usa…
ankit-thesys Jan 13, 2026
27bf277
Refactor conversation starter styles in BottomTray, CopilotShell, and…
ankit-thesys Jan 13, 2026
a7b8870
Refactor lastMessage retrieval in ScrollArea component for improved r…
ankit-thesys Jan 13, 2026
71218cf
Refactor BottomTray component styles and structure for consistency
ankit-thesys Jan 13, 2026
3a2318a
Refactor lastMessage retrieval in ScrollArea component for improved c…
ankit-thesys Jan 13, 2026
93ea494
Refactor icon handling in ConversationStarter components for clarity
ankit-thesys Jan 13, 2026
61f64e4
format fix
ankit-thesys Jan 13, 2026
ba17946
Refactor WelcomeScreen component to unify image handling
ankit-thesys Jan 13, 2026
5bdf5da
Add welcome message and conversation starters support to CrayonChat c…
ankit-thesys Jan 14, 2026
2f88b11
Enhance CrayonChat types and utilities for welcome messages
ankit-thesys Jan 14, 2026
1ff4093
Refactor CrayonChat components to utilize thread list state and enhan…
ankit-thesys Jan 14, 2026
de1634e
Update loading state in useThreadManager to reflect message loading s…
ankit-thesys Jan 14, 2026
ee8e46e
Update package versions for react-core and react-ui to 0.7.7 and 0.9.…
ankit-thesys Jan 14, 2026
4bc2241
Implement thread manager initialization and loading state updates
ankit-thesys Jan 16, 2026
ae73afd
State Sync
ankit-thesys Jan 16, 2026
b3f1339
Integrate thread state management into WelcomeScreen components
ankit-thesys Jan 16, 2026
00308aa
Refactor CrayonChat components to streamline thread state usage
ankit-thesys Jan 16, 2026
e254ea6
Enhance CrayonChat components with loading state checks
ankit-thesys Jan 16, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
639 changes: 509 additions & 130 deletions .cursor/rules/styling-rule.mdc

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion js/packages/react-core/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@crayonai/react-core",
"version": "0.7.6",
"version": "0.7.7",
"description": "Generative UI SDK",
"license": "MIT",
"main": "dist/index.js",
Expand Down
1 change: 1 addition & 0 deletions js/packages/react-core/src/hooks/useThreadState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,6 @@ export const useThreadState = (): ThreadState => {
messages: useStore(threadManager, (store) => store.messages),
error: useStore(threadManager, (store) => store.error),
responseTemplates: useStore(threadManager, (store) => store.responseTemplates),
isInitialized: useStore(threadManager, (store) => store.isInitialized),
};
};
3 changes: 3 additions & 0 deletions js/packages/react-core/src/internal/useThreadManagerStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export const useThreadManagerStore = (inputThreadManager: ThreadManager) => {

const [threadManagerStore] = useState(() =>
create<ThreadManager>(() => ({
isInitialized: inputThreadManager.isInitialized,
isLoadingMessages: inputThreadManager.isLoadingMessages,
isRunning: inputThreadManager.isRunning,
messages: inputThreadManager.messages,
Expand All @@ -31,12 +32,14 @@ export const useThreadManagerStore = (inputThreadManager: ThreadManager) => {
messages: inputThreadManager.messages,
error: inputThreadManager.error,
isLoadingMessages: inputThreadManager.isLoadingMessages,
isInitialized: inputThreadManager.isInitialized,
});
}, [
inputThreadManager.isRunning,
inputThreadManager.messages,
inputThreadManager.error,
inputThreadManager.isLoadingMessages,
inputThreadManager.isInitialized,
]);

if (inputThreadManagerRef.current.responseTemplates !== inputThreadManager.responseTemplates) {
Expand Down
2 changes: 2 additions & 0 deletions js/packages/react-core/src/types/chatManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ export type ThreadState = {
* `responseTemplates` property provided to the hook.
*/
responseTemplates: Record<string, ResponseTemplate>;
/** Indicates if the thread manager is initialized and the thread can show threadList or welcome screen */
isInitialized: boolean;
};

/**
Expand Down
18 changes: 17 additions & 1 deletion js/packages/react-core/src/useThreadManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ export const useThreadManager = (params: UseThreadManagerParams): ThreadManager
propsRef.current = params;

const store = useMemo(() => {
return createStore<
const store = createStore<
ThreadManager & {
abortController: AbortController | null;
}
Expand All @@ -54,6 +54,7 @@ export const useThreadManager = (params: UseThreadManagerParams): ThreadManager
abortController: null,
isRunning: false,
isLoadingMessages: false,
isInitialized: false,
setMessages: (messages: Message[]) => {
set({ messages });
},
Expand Down Expand Up @@ -117,6 +118,21 @@ export const useThreadManager = (params: UseThreadManagerParams): ThreadManager
),
};
});

/**
* Delay initialization to ensure proper thread manager setup.
*
* Why this delay is necessary:
* - Multiple React effects must run to determine the initial `selectedThreadId`
* - Without a definitive thread ID state (null vs. valid ID), the UI cannot
* determine whether to render the thread list or welcome screen
* - This 200ms buffer allows all effects to complete, providing a stable state
* before marking initialization as complete
*/
setTimeout(() => {
store.setState({ isInitialized: true });
}, 200);
return store;
}, [propsRef]);

useEffect(() => {
Expand Down
2 changes: 1 addition & 1 deletion js/packages/react-ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"type": "module",
"name": "@crayonai/react-ui",
"license": "MIT",
"version": "0.9.11",
"version": "0.9.12",
"description": "Component library for Generative UI SDK",
"main": "dist/index.js",
"types": "dist/index.d.ts",
Expand Down
145 changes: 145 additions & 0 deletions js/packages/react-ui/src/components/BottomTray/ConversationStarter.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
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 { Separator } from "../Separator";

export type ConversationStarterVariant = "short" | "long";

interface ConversationStarterItemProps extends ConversationStarterProps {
onClick: (prompt: string) => void;
variant: ConversationStarterVariant;
}

/**
* Renders the appropriate icon based on the icon prop value
* - undefined: Show default lightbulb icon
* - ReactNode: Show the provided icon (use <></> or React.Fragment for no icon)
*/
const renderIcon = (icon: ConversationStarterIcon | undefined): ReactNode => {
if (icon === undefined) {
return <Lightbulb size={16} />;
}
return icon;
};

const ConversationStarterItem = ({
displayText,
prompt,
onClick,
variant,
icon,
}: ConversationStarterItemProps) => {
const renderedIcon = renderIcon(icon);

if (variant === "short") {
return (
<button
type="button"
className="crayon-bottom-tray-conversation-starter-item-short"
onClick={() => onClick(prompt)}
>
{renderedIcon && (
<span className="crayon-bottom-tray-conversation-starter-item-short__icon">
{renderedIcon}
</span>
)}
<span className="crayon-bottom-tray-conversation-starter-item-short__text">
{displayText}
</span>
</button>
);
}

// Long variant (detailed list style)
return (
<button
type="button"
className="crayon-bottom-tray-conversation-starter-item-long"
onClick={() => onClick(prompt)}
>
<div className="crayon-bottom-tray-conversation-starter-item-long__content">
{renderedIcon && (
<span className="crayon-bottom-tray-conversation-starter-item-long__icon">
{renderedIcon}
</span>
)}
<span className="crayon-bottom-tray-conversation-starter-item-long__text">
{displayText}
</span>
</div>
<span className="crayon-bottom-tray-conversation-starter-item-long__arrow">
<ArrowUp size={16} />
</span>
</button>
);
};

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,
variant = "short",
}: 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;
}
Comment on lines +108 to +110
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

test with C1Chat persistence, on switch thread + on new thread.
when switching thread and loading started/or not donot show

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

actually remove this case from here and add in crayonchat

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

testing, I have changed the condition so that we dont see flashes of the welcome screen.


if (starters.length === 0) {
return null;
}

return (
<div
className={clsx(
"crayon-bottom-tray-conversation-starter",
`crayon-bottom-tray-conversation-starter--${variant}`,
className,
)}
>
{starters.map((item, index) => (
<Fragment key={`${item.displayText}-${index}`}>
<ConversationStarterItem
displayText={item.displayText}
prompt={item.prompt}
icon={item.icon}
onClick={handleClick}
variant={variant}
/>
{/* Add separator between items in long variant */}
{variant === "long" && index < starters.length - 1 && (
<div className="crayon-bottom-tray-conversation-starter__separator">
<Separator />
</div>
)}
</Fragment>
))}
</div>
);
};

export default ConversationStarter;
19 changes: 16 additions & 3 deletions js/packages/react-ui/src/components/BottomTray/Thread.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
useThreadState,
} from "@crayonai/react-core";
import clsx from "clsx";
import { ArrowRight, Square } from "lucide-react";
import { ArrowUp, Square } from "lucide-react";
import React, { memo, useEffect, useLayoutEffect, useRef } from "react";
import { useComposerState } from "../../hooks/useComposerState";
import { ScrollVariant, useScrollToBottom } from "../../hooks/useScrollToBottom";
Expand Down Expand Up @@ -35,7 +35,20 @@ export const ThreadContainer = ({
setArtifactRenderer(renderArtifact);
}, [isArtifactActive, renderArtifact, setIsArtifactActive, setArtifactRenderer]);

return <div className={clsx("crayon-bottom-tray-thread-container", className)}>{children}</div>;
const { isInitialized } = useThreadManagerSelector((store) => ({
isInitialized: store.isInitialized,
}));

return (
<div
className={clsx("crayon-bottom-tray-thread-container", className)}
style={{
visibility: isInitialized ? undefined : "hidden",
}}
>
{children}
</div>
);
};

export const ScrollArea = ({
Expand Down Expand Up @@ -265,7 +278,7 @@ export const Composer = ({ className }: { className?: string }) => {
/>
<IconButton
onClick={isRunning ? onCancel : handleSubmit}
icon={isRunning ? <Square size="1em" fill="currentColor" /> : <ArrowRight size="1em" />}
icon={isRunning ? <Square size="1em" fill="currentColor" /> : <ArrowUp size="1em" />}
/>
</div>
</div>
Expand Down
106 changes: 106 additions & 0 deletions js/packages/react-ui/src/components/BottomTray/WelcomeScreen.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
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;
/**
* Image to display - can be a URL object or a ReactNode
* - { url: string }: Renders an <img> tag with default styling (64x64, object-fit: cover, rounded)
* - ReactNode: Renders the provided element directly (for custom icons, styled images, etc.)
*/
image?: { url: string } | 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, and image are ignored
*/
children: ReactNode;
title?: never;
description?: never;
image?: never;
}

export type WelcomeScreenProps = WelcomeScreenWithContentProps | WelcomeScreenWithChildrenProps;

/**
* Type guard to check if image is a URL object
*/
const isImageUrl = (image: { url: string } | ReactNode): image is { url: string } => {
return typeof image === "object" && image !== null && "url" in image;
};

export const WelcomeScreen = (props: WelcomeScreenProps) => {
const { className } = props;

const { messages } = useThreadState();

// Only show when there are no messages
if (messages.length > 0) {
return null;
}

// Check if children are provided
if ("children" in props && props.children) {
return (
<div className={clsx("crayon-bottom-tray-welcome-screen", className)}>{props.children}</div>
);
}

// Props-based content
const { title, description, image } = props as WelcomeScreenWithContentProps;

const renderImage = () => {
if (!image) return null;

if (isImageUrl(image)) {
return (
<img
src={image.url}
alt={title || ""}
className="crayon-bottom-tray-welcome-screen__image"
/>
);
}

return image;
};

return (
<div className={clsx("crayon-bottom-tray-welcome-screen", className)}>
{image && (
<div className="crayon-bottom-tray-welcome-screen__image-container">{renderImage()}</div>
)}
{(title || description) && (
<div className="crayon-bottom-tray-welcome-screen__content">
{title && <h2 className="crayon-bottom-tray-welcome-screen__title">{title}</h2>}
{description && (
<p className="crayon-bottom-tray-welcome-screen__description">{description}</p>
)}
</div>
)}
</div>
);
};

export default WelcomeScreen;
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,5 @@
@use "./header.scss";
@use "./threadList.scss";
@use "./thread.scss";
@use "./conversationStarter.scss";
@use "./welcomeScreen.scss";
Loading