import type { Meta, StoryObj } from "@storybook/angular"; import { moduleMetadata } from "@storybook/angular"; import { CommonModule } from "@angular/common"; import { Component, EventEmitter, Injectable, Input, Output, signal, } from "@angular/core"; import { fn } from "storybook/test"; import { ChatState, CopilotChatInput, provideCopilotChatLabels, provideCopilotKit, } from "@copilotkit/angular"; import type { ToolsMenuItem } from "@copilotkit/angular"; import { CustomSendButtonComponent } from "../components/custom-send-button.component"; @Injectable() class StoryChatState extends ChatState { readonly inputValue = signal(""); submitInput(value: string): void { const trimmed = value.trim(); if (!trimmed) return; this.inputValue.set(""); } changeInput(value: string): void { this.inputValue.set(value); } } // Additional custom button components for slot demonstrations @Component({ selector: "airplane-send-button", standalone: true, template: ` `, }) class AirplaneSendButtonComponent { @Input() disabled = false; @Output() click = new EventEmitter(); handleClick(): void { if (!this.disabled) { this.click.emit(); } } } @Component({ selector: "rocket-send-button", standalone: true, template: ` `, }) class RocketSendButtonComponent { @Input() disabled = false; @Output() click = new EventEmitter(); handleClick(): void { if (!this.disabled) { this.click.emit(); } } } const meta: Meta = { title: "UI/CopilotChatInput", component: CopilotChatInput, tags: ["autodocs"], decorators: [ moduleMetadata({ imports: [ CommonModule, CopilotChatInput, CustomSendButtonComponent, AirplaneSendButtonComponent, RocketSendButtonComponent, ], providers: [ provideCopilotKit({}), { provide: ChatState, useClass: StoryChatState }, provideCopilotChatLabels({ chatInputPlaceholder: "Type a message...", chatInputToolbarToolsButtonLabel: "Tools", }), ], }), ], render: (args) => ({ props: { ...args, submitMessage: fn(), startTranscribe: fn(), cancelTranscribe: fn(), finishTranscribe: fn(), addFile: fn(), valueChange: fn(), }, template: `
`, }), parameters: { layout: "fullscreen", docs: { description: { component: ` The CopilotChatInput component provides a feature-rich chat input interface for Angular applications. ## Features - 📝 Auto-resizing textarea with configurable max rows - 🎙️ Voice recording mode with visual feedback - 🛠️ Customizable tools dropdown menu - 📎 File attachment support - 🎨 Dark/light theme support - 🔧 Fully customizable via slots and props - ♿ Accessible with ARIA labels and keyboard navigation ## Basic Usage \`\`\`typescript import { CopilotChatInput } from '@copilotkit/angular'; import { provideCopilotChatLabels } from '@copilotkit/angular'; @Component({ selector: 'app-chat', standalone: true, imports: [CopilotChatInput], providers: [ provideCopilotChatLabels({ chatInputPlaceholder: 'Type a message...', }) ], template: \` \` }) export class ChatComponent { onSubmitMessage(message: string): void { console.log('Message:', message); } } \`\`\` ## Customization The component supports extensive customization through: - **Props**: Configure behavior and appearance - **Slots**: Replace default UI elements with custom components - **Templates**: Use ng-template for fine-grained control - **Styling**: Apply custom CSS classes See individual stories below for detailed examples of each customization approach. `, }, }, }, argTypes: { mode: { control: { type: "radio" }, options: ["input", "transcribe"], description: "The input mode - text input or voice recording", table: { type: { summary: "'input' | 'transcribe'" }, defaultValue: { summary: "input" }, category: "Behavior", }, }, inputClass: { control: { type: "text" }, description: "Custom CSS class for styling the input container", table: { type: { summary: "string" }, defaultValue: { summary: "" }, category: "Appearance", }, }, value: { control: { type: "text" }, description: "The current input value (for controlled components)", table: { type: { summary: "string" }, defaultValue: { summary: "" }, category: "Data", }, }, autoFocus: { control: { type: "boolean" }, description: "Auto-focus the input when the component mounts", table: { type: { summary: "boolean" }, defaultValue: { summary: "true" }, category: "Behavior", }, }, toolsMenu: { description: "Array of menu items for the tools dropdown", table: { type: { summary: '(ToolsMenuItem | "-")[]' }, category: "Features", }, }, sendButtonComponent: { description: "Custom send button component", table: { type: { summary: "Type" }, category: "Customization", }, }, additionalToolbarItems: { description: "Additional toolbar items to display", table: { type: { summary: "TemplateRef | Component[]" }, category: "Customization", }, }, }, args: { mode: "input", inputClass: "", value: "", autoFocus: true, }, }; export default meta; type Story = StoryObj; // 1. Default story export const Default: Story = { parameters: { docs: { description: { story: "The default chat input with all standard features enabled.", }, source: { type: "code", code: `import { Component } from '@angular/core'; import { CopilotChatInput, provideCopilotChatLabels } from '@copilotkit/angular'; @Component({ selector: 'app-chat', standalone: true, imports: [CopilotChatInput], providers: [ provideCopilotChatLabels({ chatInputPlaceholder: 'Type a message...', }) ], template: \` \` }) export class ChatComponent { onSubmitMessage(message: string): void { console.log('Message submitted:', message); } onValueChange(value: string): void { console.log('Value changed:', value); } onAddFile(): void { console.log('Add file clicked'); } onStartTranscribe(): void { console.log('Started transcription'); } onCancelTranscribe(): void { console.log('Cancelled transcription'); } onFinishTranscribe(): void { console.log('Finished transcription'); } }`, language: "typescript", }, }, }, }; // 2. With Tools Menu export const WithToolsMenu: Story = { name: "With Tools Menu", args: { toolsMenu: [ { label: "Do X", action: () => { console.log("Do X clicked"); alert("Action: Do X was clicked!"); }, }, { label: "Do Y", action: () => { console.log("Do Y clicked"); alert("Action: Do Y was clicked!"); }, }, "-", { label: "Advanced", items: [ { label: "Do Advanced X", action: () => { console.log("Do Advanced X clicked"); alert("Advanced Action: Do Advanced X was clicked!"); }, }, "-", { label: "Do Advanced Y", action: () => { console.log("Do Advanced Y clicked"); alert("Advanced Action: Do Advanced Y was clicked!"); }, }, ], }, ] as (ToolsMenuItem | "-")[], }, parameters: { docs: { description: { story: ` Demonstrates a tools dropdown menu with nested items and separators. \`\`\`typescript toolsMenu: [ { label: 'Action 1', action: () => {} }, '-', // Separator { label: 'Submenu', items: [ { label: 'Sub Action', action: () => {} } ] } ] \`\`\` `, }, source: { type: "code", code: `import { Component } from '@angular/core'; import { CopilotChatInput, ToolsMenuItem } from '@copilotkit/angular'; @Component({ selector: 'app-chat', standalone: true, imports: [CopilotChatInput], template: \` \` }) export class ChatComponent { toolsMenu: (ToolsMenuItem | '-')[] = [ { label: 'Do X', action: () => { console.log('Do X clicked'); alert('Action: Do X was clicked!'); }, }, { label: 'Do Y', action: () => { console.log('Do Y clicked'); alert('Action: Do Y was clicked!'); }, }, '-', // Separator { label: 'Advanced', items: [ { label: 'Do Advanced X', action: () => { console.log('Do Advanced X clicked'); alert('Advanced Action: Do Advanced X was clicked!'); }, }, '-', { label: 'Do Advanced Y', action: () => { console.log('Do Advanced Y clicked'); alert('Advanced Action: Do Advanced Y was clicked!'); }, }, ], }, ]; onSubmitMessage(message: string): void { console.log('Message submitted:', message); } }`, language: "typescript", }, }, }, }; // 3. Transcribe Mode export const TranscribeMode: Story = { name: "Transcribe Mode", args: { mode: "transcribe", autoFocus: false, }, parameters: { docs: { description: { story: ` Voice recording mode with animated waveform visualization. \`\`\`html \`\`\` Emits: - \`(startTranscribe)\` - Recording started - \`(cancelTranscribe)\` - Recording cancelled - \`(finishTranscribe)\` - Recording completed `, }, source: { type: "code", code: `import { Component } from '@angular/core'; import { CopilotChatInput } from '@copilotkit/angular'; @Component({ selector: 'app-chat', standalone: true, imports: [CopilotChatInput], template: \` \` }) export class ChatComponent { onStartTranscribe(): void { console.log('Recording started'); } onCancelTranscribe(): void { console.log('Recording cancelled'); } onFinishTranscribe(): void { console.log('Recording finished'); } }`, language: "typescript", }, }, }, }; // 4. Custom Send Button export const CustomSendButton: Story = { name: "Custom Send Button (Template Slot)", decorators: [ moduleMetadata({ imports: [CommonModule, CopilotChatInput, CustomSendButtonComponent], providers: [ provideCopilotChatLabels({ chatInputPlaceholder: "Type a message...", chatInputToolbarToolsButtonLabel: "Tools", }), ], }), ], render: () => ({ props: { submitMessage: fn(), addFile: fn(), }, template: `
`, }), parameters: { docs: { description: { story: ` Replace the default send button using Angular's template slot system. \`\`\`html \`\`\` The template receives: - \`send\`: Function to trigger message submission - \`disabled\`: Boolean indicating if sending is allowed `, }, source: { type: "code", code: `import { Component } from '@angular/core'; import { CopilotChatInput } from '@copilotkit/angular'; // Custom send button component @Component({ selector: 'custom-send-button', standalone: true, template: \` \`, }) export class CustomSendButtonComponent { @Input() disabled = false; @Output() click = new EventEmitter(); handleClick(): void { if (!this.disabled) { this.click.emit(); } } } // Main component using the custom send button @Component({ selector: 'app-chat', standalone: true, imports: [CopilotChatInput, CustomSendButtonComponent], template: \` \` }) export class ChatComponent { onSubmitMessage(message: string): void { console.log('Message submitted:', message); } }`, language: "typescript", }, }, }, }; // 5. With Additional Toolbar Items export const WithAdditionalToolbarItems: Story = { name: "With Additional Toolbar Items", render: () => ({ props: { submitMessage: fn(), addFile: fn(), onCustomAction: () => { console.log("Custom action clicked!"); alert("Custom action clicked!"); }, onAnotherAction: () => { console.log("Another custom action clicked!"); alert("Another custom action clicked!"); }, }, template: `
`, }), parameters: { docs: { description: { story: ` Add custom toolbar items alongside the default tools. \`\`\`html \`\`\` These items appear in the toolbar area next to the default buttons. Note: The template is passed as an input property, not as content projection. `, }, source: { type: "code", code: `import { Component, ViewChild, TemplateRef } from '@angular/core'; import { CopilotChatInput } from '@copilotkit/angular'; @Component({ selector: 'app-chat', standalone: true, imports: [CopilotChatInput], template: \` \` }) export class ChatComponent { @ViewChild('additionalItems') additionalItems!: TemplateRef; onSubmitMessage(message: string): void { console.log('Message submitted:', message); } onAddFile(): void { console.log('Add file clicked'); } onCustomAction(): void { console.log('Custom action clicked!'); alert('Custom action clicked!'); } onAnotherAction(): void { console.log('Another custom action clicked!'); alert('Another custom action clicked!'); } }`, language: "typescript", }, }, }, }; // 6. Prefilled Text export const PrefilledText: Story = { name: "Prefilled Text", args: { value: "Hello, this is a prefilled message!", }, parameters: { docs: { description: { story: ` Initialize the input with pre-populated text. \`\`\`html \`\`\` Useful for: - Draft messages - Edit mode - Template messages `, }, source: { type: "code", code: `import { Component } from '@angular/core'; import { CopilotChatInput } from '@copilotkit/angular'; @Component({ selector: 'app-chat', standalone: true, imports: [CopilotChatInput], template: \` \` }) export class ChatComponent { initialMessage = 'Hello, this is a prefilled message!'; onValueChange(value: string): void { console.log('Value changed:', value); } onSubmitMessage(message: string): void { console.log('Message submitted:', message); } }`, language: "typescript", }, }, }, }; // 7. Expanded Textarea export const ExpandedTextarea: Story = { name: "Expanded Textarea", args: { value: "This is a longer message that will cause the textarea to expand.\n\nIt has multiple lines to demonstrate the auto-resize functionality.\n\nThe textarea will grow up to the maxRows limit.", }, parameters: { docs: { description: { story: ` Demonstrates auto-expanding textarea behavior with multiline content. The textarea automatically resizes based on content, up to a configurable maximum height. Features: - Smooth expansion animation - Maintains scroll position - Respects maxRows configuration `, }, source: { type: "code", code: `import { Component } from '@angular/core'; import { CopilotChatInput } from '@copilotkit/angular'; @Component({ selector: 'app-chat', standalone: true, imports: [CopilotChatInput], template: \` \` }) export class ChatComponent { multilineMessage = 'This is a longer message that will cause the textarea to expand.\\n\\n' + 'It has multiple lines to demonstrate the auto-resize functionality.\\n\\n' + 'The textarea will grow up to the maxRows limit.'; onValueChange(value: string): void { console.log('Value changed:', value); } onSubmitMessage(message: string): void { console.log('Message submitted:', message); } }`, language: "typescript", }, }, }, }; // 8. Custom Styling export const CustomStyling: Story = { name: "Custom Styling", render: (args) => ({ props: { ...args, submitMessage: fn(), startTranscribe: fn(), cancelTranscribe: fn(), finishTranscribe: fn(), addFile: fn(), valueChange: fn(), }, template: `
`, }), args: { inputClass: "custom-chat-input", }, parameters: { docs: { description: { story: ` Apply custom CSS classes for unique styling. This example demonstrates inline styles that override default component styling. \`\`\`html \`\`\` This example shows: - Custom border and background styling - Modified typography for the textarea - Hover effects on buttons - Box shadow for depth `, }, source: { type: "code", code: `import { Component } from '@angular/core'; import { CopilotChatInput } from '@copilotkit/angular'; @Component({ selector: 'app-chat', standalone: true, imports: [CopilotChatInput], template: \` \`, styles: [\` :host ::ng-deep .custom-chat-input { border: 2px solid #4f46e5 !important; border-radius: 12px !important; background: linear-gradient(to right, #f3f4f6, #ffffff) !important; box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1) !important; padding: 12px !important; } :host ::ng-deep .custom-chat-input textarea { font-family: 'Monaco', 'Consolas', monospace !important; font-size: 14px !important; color: #1e293b !important; } :host ::ng-deep .custom-chat-input button { transition: all 0.3s ease !important; } :host ::ng-deep .custom-chat-input button:hover { transform: scale(1.05) !important; } \`] }) export class ChatComponent { onSubmitMessage(message: string): void { console.log('Message submitted:', message); } }`, language: "typescript", }, }, }, }; // === SLOT CUSTOMIZATION EXAMPLES === // The following stories demonstrate Angular's powerful slot system for component customization export const SlotTemplateFullControl: Story = { name: "Slot: Template with Full Control", render: () => ({ props: { submitMessage: fn(), addFile: fn(), }, template: `

Use ng-template for complete control over the send button:

<copilot-chat-input>
  <ng-template #sendButton let-send="send" let-disabled="disabled">
    <button (click)="send()" [disabled]="disabled"
            class="custom-gradient-button">
      Send 🎯
    </button>
  </ng-template>
</copilot-chat-input>
`, }), parameters: { docs: { description: { story: ` The most flexible approach - use ng-template to completely control the send button's markup and behavior. **Benefits:** - Full control over HTML structure - Direct access to template variables - Can use any Angular directives - Perfect for complex custom components `, }, source: { type: "code", code: `import { Component } from '@angular/core'; import { CopilotChatInput } from '@copilotkit/angular'; @Component({ selector: 'app-chat', standalone: true, imports: [CopilotChatInput], template: \` \` }) export class ChatComponent { onSubmitMessage(message: string): void { console.log('Message submitted:', message); } }`, language: "typescript", }, }, }, }; export const SlotInlineButton: Story = { name: "Slot: Inline Custom Button", decorators: [ moduleMetadata({ imports: [CommonModule, CopilotChatInput, AirplaneSendButtonComponent], providers: [ provideCopilotChatLabels({ chatInputPlaceholder: "Type a message...", }), ], }), ], render: () => ({ props: { submitMessage: fn(), addFile: fn(), }, template: `

Define custom button markup inline:

<copilot-chat-input>
  <ng-template #sendButton let-send="send" let-disabled="disabled">
    <button [disabled]="disabled" (click)="send()"
            class="airplane-button">
      ✈️
    </button>
  </ng-template>
</copilot-chat-input>
`, }), parameters: { docs: { description: { story: ` Create a custom button directly in the template without a separate component. **When to use:** - Simple customizations - One-off designs - Prototyping - When you don't need reusability `, }, }, }, }; export const SlotWithComponent: Story = { name: "Slot: Using Custom Component", decorators: [ moduleMetadata({ imports: [CommonModule, CopilotChatInput, RocketSendButtonComponent], providers: [ provideCopilotChatLabels({ chatInputPlaceholder: "Type a message...", }), ], }), ], render: () => ({ props: { submitMessage: fn(), addFile: fn(), }, template: `

Use a pre-built component in the slot:

<copilot-chat-input>
  <ng-template #sendButton let-send="send" let-disabled="disabled">
    <rocket-send-button 
      [disabled]="disabled" 
      (click)="send()">
    </rocket-send-button>
  </ng-template>
</copilot-chat-input>
`, }), parameters: { docs: { description: { story: ` Use a standalone Angular component within the template slot. **Benefits:** - Reusable across multiple places - Encapsulated logic and styling - Easier testing - Better code organization **Component requirements:** - Must accept \`disabled\` input - Should emit \`clicked\` (preferred) or \`click\` event - Should be standalone or properly imported `, }, }, }, }; export const SlotDirectComponent: Story = { name: "Slot: Direct Component", decorators: [ moduleMetadata({ imports: [CommonModule, CopilotChatInput, RocketSendButtonComponent], providers: [ provideCopilotChatLabels({ chatInputPlaceholder: "Type a message...", }), ], }), ], render: () => ({ props: { submitMessage: fn(), addFile: fn(), SendButton: RocketSendButtonComponent, }, template: `

Pass component class directly (backward compatible):

// In component:
SendButton = RocketSendButtonComponent;

// In template:
<copilot-chat-input [sendButtonComponent]="SendButton">
</copilot-chat-input>
`, }), parameters: { docs: { description: { story: ` Legacy approach for backward compatibility - pass a component class directly. ⚠️ **Note:** Template slots (ng-template) are preferred for better flexibility. **Limitations:** - Less flexible than templates - Harder to pass custom props - Component must match expected interface exactly `, }, }, }, }; export const SlotMultipleCustomizations: Story = { name: "Slot: Multiple Customizations", decorators: [ moduleMetadata({ imports: [CommonModule, CopilotChatInput, AirplaneSendButtonComponent], providers: [ provideCopilotChatLabels({ chatInputPlaceholder: "Type a message...", }), ], }), ], render: () => ({ props: { submitMessage: fn(), addFile: fn(), }, template: `

Combine multiple slot customizations:

<ng-template #additionalItems>
  <button class="toolbar-btn">📎</button>
  <button class="toolbar-btn">😊</button>
</ng-template>

<copilot-chat-input 
  [additionalToolbarItems]="additionalItems">
  <ng-template #sendButton let-send="send" let-disabled="disabled">
    <airplane-send-button [disabled]="disabled" (click)="send()">
    </airplane-send-button>
  </ng-template>
</copilot-chat-input>
`, }), parameters: { docs: { description: { story: ` Customize multiple aspects of the component simultaneously using different slots. **Available slots:** - \`#sendButton\` - Replace the send button - \`#additionalToolbarItems\` - Add extra toolbar buttons - More slots coming soon! Each slot operates independently, allowing for granular customization. `, }, }, }, };