From f69a4e67c084eb603fc7e8311681bbc8b62ba880 Mon Sep 17 00:00:00 2001 From: zahlekhan Date: Tue, 13 Jan 2026 13:31:30 +0530 Subject: [PATCH 1/4] working --- src/index.ts | 185 +++++++++++++++++++++++++++++++++++++++++- src/styles/widget.css | 64 ++++++++++++++- src/types.ts | 22 +++++ 3 files changed, 268 insertions(+), 3 deletions(-) diff --git a/src/index.ts b/src/index.ts index 2003e99..8df5d29 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,5 @@ import { createRoot } from "react-dom/client"; -import { createElement } from "react"; +import { createElement, useEffect } from "react"; import { C1Chat, useThreadManager, @@ -8,7 +8,7 @@ import { } from "@thesysai/genui-sdk"; import type { Thread, UserMessage } from "@crayonai/react-core"; import "@crayonai/react-ui/styles/index.css"; -import type { ChatConfig, ChatInstance } from "./types"; +import type { ChatConfig, ChatInstance, QuickSuggestion } from "./types"; import { createStorageAdapter, LangGraphStorageAdapter } from "./storage"; import type { StorageAdapter } from "./storage"; import { @@ -19,6 +19,172 @@ import { import { log, handleError, normalizeError } from "./utils/logger"; import "./styles/widget.css"; +/** + * Setup quick suggestions above the composer input + * Injects a suggestion div that appears when the input is empty + */ +function setupQuickSuggestions( + container: HTMLElement, + suggestions: QuickSuggestion[] +): () => void { + let suggestionContainer: HTMLDivElement | null = null; + let observer: MutationObserver | null = null; + let inputObserver: MutationObserver | null = null; + + const COMPOSER_SELECTOR = ".crayon-shell-thread-composer__input-wrapper"; + const INPUT_SELECTOR = + '[contenteditable="true"], textarea, input[type="text"]'; + + function createSuggestionElement(): HTMLDivElement { + const wrapper = document.createElement("div"); + wrapper.className = "thesys-quick-suggestions"; + + suggestions.forEach((suggestion) => { + const chip = document.createElement("button"); + chip.className = "thesys-quick-suggestion-chip"; + chip.type = "button"; + + if (suggestion.icon) { + const icon = document.createElement("span"); + icon.className = "thesys-quick-suggestion-icon"; + icon.textContent = suggestion.icon; + chip.appendChild(icon); + } + + const text = document.createElement("span"); + text.className = "thesys-quick-suggestion-text"; + text.textContent = suggestion.text; + chip.appendChild(text); + + chip.addEventListener("click", () => { + const composerWrapper = container.querySelector(COMPOSER_SELECTOR); + if (!composerWrapper) return; + + const input = composerWrapper.querySelector(INPUT_SELECTOR) as + | HTMLElement + | HTMLInputElement + | HTMLTextAreaElement + | null; + if (!input) return; + + // Set the text in the input + if ( + input instanceof HTMLInputElement || + input instanceof HTMLTextAreaElement + ) { + input.value = suggestion.text; + input.dispatchEvent(new Event("input", { bubbles: true })); + } else if (input.isContentEditable) { + input.textContent = suggestion.text; + input.dispatchEvent( + new InputEvent("input", { bubbles: true, inputType: "insertText" }) + ); + // Move cursor to end + const range = document.createRange(); + const sel = window.getSelection(); + range.selectNodeContents(input); + range.collapse(false); + sel?.removeAllRanges(); + sel?.addRange(range); + } + + input.focus(); + updateVisibility(); + }); + + wrapper.appendChild(chip); + }); + + return wrapper; + } + + function getInputValue(): string { + const composerWrapper = container.querySelector(COMPOSER_SELECTOR); + if (!composerWrapper) return ""; + + const input = composerWrapper.querySelector(INPUT_SELECTOR) as + | HTMLElement + | HTMLInputElement + | HTMLTextAreaElement + | null; + if (!input) return ""; + + if ( + input instanceof HTMLInputElement || + input instanceof HTMLTextAreaElement + ) { + return input.value.trim(); + } else if (input.isContentEditable) { + return (input.textContent || "").trim(); + } + return ""; + } + + function updateVisibility(): void { + if (!suggestionContainer) return; + const isEmpty = getInputValue() === ""; + suggestionContainer.style.display = isEmpty ? "flex" : "none"; + } + + function injectSuggestions(): void { + const composerWrapper = container.querySelector(COMPOSER_SELECTOR); + if ( + !composerWrapper || + suggestionContainer?.parentElement === composerWrapper.parentElement + ) { + return; + } + + // Remove existing if any + suggestionContainer?.remove(); + + // Create and inject + suggestionContainer = createSuggestionElement(); + composerWrapper.parentElement?.insertBefore( + suggestionContainer, + composerWrapper + ); + + // Watch input for changes + const input = composerWrapper.querySelector( + INPUT_SELECTOR + ) as HTMLElement | null; + if (input) { + input.addEventListener("input", updateVisibility); + input.addEventListener("keyup", updateVisibility); + + // Also observe for DOM changes in contenteditable + if (input.isContentEditable) { + inputObserver?.disconnect(); + inputObserver = new MutationObserver(updateVisibility); + inputObserver.observe(input, { + childList: true, + subtree: true, + characterData: true, + }); + } + } + + updateVisibility(); + } + + // Initial injection attempt + injectSuggestions(); + + // Watch for composer to appear/change + observer = new MutationObserver(() => { + injectSuggestions(); + }); + observer.observe(container, { childList: true, subtree: true }); + + // Return cleanup function + return () => { + observer?.disconnect(); + inputObserver?.disconnect(); + suggestionContainer?.remove(); + }; +} + /** * Helper function to generate thread title from first user message */ @@ -305,6 +471,20 @@ function ChatWithPersistence({ } } + // Setup quick suggestions if configured + useEffect(() => { + if (!config.quickSuggestions || config.quickSuggestions.length === 0) { + return; + } + + // Find the container where C1Chat is rendered + const container = document.getElementById("thesys-chat-root"); + if (!container) return; + + const cleanup = setupQuickSuggestions(container, config.quickSuggestions); + return cleanup; + }, [config.quickSuggestions]); + return createElement(C1Chat, c1ChatProps); } @@ -420,4 +600,5 @@ export type { ChatFormFactor, BottomTrayOptions, N8NConfig, + QuickSuggestion, } from "./types"; diff --git a/src/styles/widget.css b/src/styles/widget.css index fc2cc5d..e67d6e6 100644 --- a/src/styles/widget.css +++ b/src/styles/widget.css @@ -1,2 +1,64 @@ /* Import Inter font from Google Fonts */ -@import url('https://fonts.googleapis.com/css2?family=Inter:wght@100..900&display=swap'); +@import url("https://fonts.googleapis.com/css2?family=Inter:wght@100..900&display=swap"); + +/* Quick Suggestions - Displayed above composer input when empty */ +.thesys-quick-suggestions { + display: flex; + flex-wrap: wrap; + gap: 8px; + padding: 12px 16px; + justify-content: center; +} + +.thesys-quick-suggestion-chip { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 8px 14px; + border-radius: 20px; + border: 1px solid var(--crayon-border-color, rgba(0, 0, 0, 0.1)); + background: var(--crayon-surface-secondary, #f5f5f5); + color: var(--crayon-text-primary, #1a1a1a); + font-family: inherit; + font-size: 14px; + font-weight: 500; + line-height: 1.4; + cursor: pointer; + transition: all 0.15s ease; + white-space: nowrap; +} + +.thesys-quick-suggestion-chip:hover { + background: var(--crayon-surface-tertiary, #eaeaea); + border-color: var(--crayon-border-hover, rgba(0, 0, 0, 0.2)); + transform: translateY(-1px); +} + +.thesys-quick-suggestion-chip:active { + transform: translateY(0); +} + +.thesys-quick-suggestion-icon { + font-size: 16px; + line-height: 1; +} + +.thesys-quick-suggestion-text { + max-width: 200px; + overflow: hidden; + text-overflow: ellipsis; +} + +/* Dark theme support */ +[data-theme="dark"] .thesys-quick-suggestion-chip, +.dark .thesys-quick-suggestion-chip { + background: var(--crayon-surface-secondary, #2a2a2a); + border-color: var(--crayon-border-color, rgba(255, 255, 255, 0.1)); + color: var(--crayon-text-primary, #f0f0f0); +} + +[data-theme="dark"] .thesys-quick-suggestion-chip:hover, +.dark .thesys-quick-suggestion-chip:hover { + background: var(--crayon-surface-tertiary, #3a3a3a); + border-color: var(--crayon-border-hover, rgba(255, 255, 255, 0.2)); +} diff --git a/src/types.ts b/src/types.ts index 68143f2..925f6fb 100644 --- a/src/types.ts +++ b/src/types.ts @@ -68,6 +68,21 @@ export interface BottomTrayOptions { defaultOpen?: boolean; } +/** + * Quick suggestion displayed above the composer input + */ +export interface QuickSuggestion { + /** + * The text to display and copy into the input on click + */ + text: string; + + /** + * Optional emoji or icon character to display before the text + */ + icon?: string; +} + /** * Configuration options for the chat widget */ @@ -147,6 +162,13 @@ export interface ChatConfig { * @default false */ enableDebugLogging?: boolean; + + /** + * Quick suggestions to display above the composer input + * Only shown when the input is empty + * Clicking a suggestion copies its text into the input + */ + quickSuggestions?: QuickSuggestion[]; } /** From beccdf38101166d329aae07594af13d8e4cff6ec Mon Sep 17 00:00:00 2001 From: zahlekhan Date: Tue, 13 Jan 2026 13:35:19 +0530 Subject: [PATCH 2/4] working --- src/index.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/index.ts b/src/index.ts index 8df5d29..f6bb71b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -31,7 +31,12 @@ function setupQuickSuggestions( let observer: MutationObserver | null = null; let inputObserver: MutationObserver | null = null; - const COMPOSER_SELECTOR = ".crayon-shell-thread-composer__input-wrapper"; + // Support all form factors: full-page, side-panel, and bottom-tray + const COMPOSER_SELECTOR = [ + ".crayon-shell-thread-composer__input-wrapper", + ".crayon-bottom-tray-thread-composer__input-wrapper", + ".crayon-copilot-shell-thread-composer__input-wrapper", + ].join(", "); const INPUT_SELECTOR = '[contenteditable="true"], textarea, input[type="text"]'; From 202e586f39f25f6094cd6d72d7a1273a5fe9c2d8 Mon Sep 17 00:00:00 2001 From: zahlekhan Date: Tue, 13 Jan 2026 13:41:23 +0530 Subject: [PATCH 3/4] autosubmit --- src/index.ts | 33 +++++++++++++++++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) diff --git a/src/index.ts b/src/index.ts index f6bb71b..e5b2ceb 100644 --- a/src/index.ts +++ b/src/index.ts @@ -72,12 +72,30 @@ function setupQuickSuggestions( | null; if (!input) return; - // Set the text in the input + // Set the text in the input using native setter to trigger React state updates if ( input instanceof HTMLInputElement || input instanceof HTMLTextAreaElement ) { - input.value = suggestion.text; + // Use native setter to properly trigger React's onChange + const nativeInputValueSetter = + input instanceof HTMLTextAreaElement + ? Object.getOwnPropertyDescriptor( + HTMLTextAreaElement.prototype, + "value" + )?.set + : Object.getOwnPropertyDescriptor( + HTMLInputElement.prototype, + "value" + )?.set; + + if (nativeInputValueSetter) { + nativeInputValueSetter.call(input, suggestion.text); + } else { + input.value = suggestion.text; + } + + // Dispatch input event to notify React input.dispatchEvent(new Event("input", { bubbles: true })); } else if (input.isContentEditable) { input.textContent = suggestion.text; @@ -95,6 +113,17 @@ function setupQuickSuggestions( input.focus(); updateVisibility(); + + // Auto-submit after small delay to ensure React state is updated + setTimeout(() => { + // Find and click the submit button (sibling of input wrapper) + const submitButton = composerWrapper.querySelector( + 'button[type="submit"], button:last-child' + ) as HTMLButtonElement | null; + if (submitButton && !submitButton.disabled) { + submitButton.click(); + } + }, 50); }); wrapper.appendChild(chip); From 9680f53db8a2121f90b00a398d51d8633358c53e Mon Sep 17 00:00:00 2001 From: zahlekhan Date: Tue, 13 Jan 2026 13:53:44 +0530 Subject: [PATCH 4/4] working --- src/index.ts | 47 +++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 45 insertions(+), 2 deletions(-) diff --git a/src/index.ts b/src/index.ts index e5b2ceb..81d8013 100644 --- a/src/index.ts +++ b/src/index.ts @@ -30,6 +30,8 @@ function setupQuickSuggestions( let suggestionContainer: HTMLDivElement | null = null; let observer: MutationObserver | null = null; let inputObserver: MutationObserver | null = null; + let conversationStarted = false; + let lastMessageSentTime = 0; // Support all form factors: full-page, side-panel, and bottom-tray const COMPOSER_SELECTOR = [ @@ -112,6 +114,10 @@ function setupQuickSuggestions( } input.focus(); + + // Mark conversation as started and hide suggestions + conversationStarted = true; + lastMessageSentTime = Date.now(); updateVisibility(); // Auto-submit after small delay to ensure React state is updated @@ -154,10 +160,45 @@ function setupQuickSuggestions( return ""; } + function hasConversationStarted(): boolean { + // If flag is set and was recently set (within 3 seconds), trust the flag + // This prevents race condition where DOM hasn't updated yet + const timeSinceLastMessage = Date.now() - lastMessageSentTime; + if (conversationStarted && timeSinceLastMessage < 3000) { + return true; + } + + // Check DOM for user messages + const messageElements = container.querySelectorAll( + ".crayon-shell-thread-message-user" + ); + + // If there are messages in DOM, conversation has started + if (messageElements.length > 0) { + conversationStarted = true; + return true; + } + + // If no messages in DOM and enough time has passed, reset the flag + // This handles the "New Chat" case + if ( + conversationStarted && + messageElements.length === 0 && + timeSinceLastMessage >= 3000 + ) { + conversationStarted = false; + } + + return conversationStarted; + } + function updateVisibility(): void { if (!suggestionContainer) return; const isEmpty = getInputValue() === ""; - suggestionContainer.style.display = isEmpty ? "flex" : "none"; + const conversationStarted = hasConversationStarted(); + // Only show suggestions when input is empty AND conversation hasn't started + suggestionContainer.style.display = + isEmpty && !conversationStarted ? "flex" : "none"; } function injectSuggestions(): void { @@ -205,9 +246,11 @@ function setupQuickSuggestions( // Initial injection attempt injectSuggestions(); - // Watch for composer to appear/change + // Watch for composer to appear/change and messages to be added observer = new MutationObserver(() => { injectSuggestions(); + // Also update visibility in case messages were added + updateVisibility(); }); observer.observe(container, { childList: true, subtree: true });